Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions packages/mix/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
## Unreleased

### New features

- **`ContextVariant.widgetStateDependencies`:** Context variants now declare the
widget states they read, so custom variants participate in nested dependency
discovery instead of relying on the framework recognizing a specific variant
type. Automatic self-tracking is limited to pointer-driven hover and press;
other states still require an ancestor scope or external controller.

### Fixes

- **Nested widget-state discovery:** `Style.widgetStates` now discovers state
requirements recursively through nested and negated variants with
identity-based cycle protection, so variants like
`onDark(BoxStyler().onHovered(...))` and `onEnabled(...)` are tracked instead
of silently never activating.
- **Interaction detector is mounted only when it can help:** `StyleBuilder` now
installs its pointer-interaction detector only for the states that detector
actually drives (`hovered`/`pressed`). States such as `disabled` and `focused`
can only come from an external `WidgetStatesController` or an ancestor scope,
so styles depending solely on those no longer gain an opaque hit-test target
that swallowed pointer events aimed at widgets beneath them, and no longer
hijack the state scope of descendants that do track hover.

## 2.2.0-beta.2

### New features
Expand Down
13 changes: 13 additions & 0 deletions packages/mix/lib/src/core/internal/mix_interaction_detector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,19 @@ class MixInteractionDetector extends StatefulWidget {
this.onPointerPositionChange,
});

/// The widget states this detector derives from pointer input.
///
/// Deliberately excludes [WidgetState.disabled]: that one is driven by
/// [enabled], which the caller sets, not by interaction. Every other
/// [WidgetState] must come from an external controller or an ancestor
/// [WidgetStateProvider], so installing this detector to satisfy them adds an
/// opaque hit-test target for no behavioural gain. Callers deciding whether
/// this detector is worth mounting should check against this set.
static const Set<WidgetState> pointerDrivenStates = {
WidgetState.hovered,
WidgetState.pressed,
};

final Widget child;
final WidgetStatesController? controller;
final bool enabled;
Expand Down
32 changes: 28 additions & 4 deletions packages/mix/lib/src/core/style.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,34 @@ abstract class Style<S extends Spec<S>> extends Mix<StyleSpec<S>>

@internal
Set<WidgetState> get widgetStates {
return ($variants ?? [])
.where((v) => v.variant is WidgetStateVariant)
.map((v) => (v.variant as WidgetStateVariant).state)
.toSet();
final states = <WidgetState>{};
// Identity, not equality, and it earns its keep twice over:
// 1. Cycles. `$variants` is stored by reference, so a caller can pass a
// list to a styler constructor and then append the styler to that same
// list. Value equality would also recurse forever comparing the cycle.
// 2. Sharing. `VariantStyleMixin.onBuilder` stores the receiver itself as
// the builder's placeholder value, so each chained `onBuilder` nests a
// snapshot of the style before it. Without dedup that is O(2^n) in the
// number of chained builders.
final visited = Set<Style<S>>.identity();

void collectDependencies(Style<S> style) {
if (!visited.add(style)) return;

final variants = style.$variants;
if (variants == null) return;

for (final variantStyle in variants) {
if (variantStyle.variant case final ContextVariant variant) {
states.addAll(variant.widgetStateDependencies);
}
collectDependencies(variantStyle.value);
}
}

collectDependencies(this);

return states;
}

/// Merges all active variants with their nested variants recursively.
Expand Down
17 changes: 12 additions & 5 deletions packages/mix/lib/src/core/style_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,18 @@ class _StyleBuilderState<S extends Spec<S>> extends State<StyleBuilder<S>>
Widget build(BuildContext context) {
final style = _buildStyle(context);

// Calculate interactivity need early
final needsToTrackWidgetState =
widget.controller == null && style.widgetStates.isNotEmpty;
// Calculate interactivity need early. Only states the detector can actually
// drive justify mounting it; see [MixInteractionDetector.pointerDrivenStates].
final needsPointerStateTracking =
widget.controller == null &&
style.widgetStates.any(
MixInteractionDetector.pointerDrivenStates.contains,
);

final alreadyHasWidgetStateScope = WidgetStateProvider.of(context) != null;
// Variant resolution registers its own granular state dependencies; this
// existence check must not subscribe to every change in the model.
final alreadyHasWidgetStateScope =
context.getInheritedWidgetOfExactType<WidgetStateProvider>() != null;

Widget current = Builder(
builder: (context) {
Expand All @@ -142,7 +149,7 @@ class _StyleBuilderState<S extends Spec<S>> extends State<StyleBuilder<S>>
},
);

if (needsToTrackWidgetState && !alreadyHasWidgetStateScope) {
if (needsPointerStateTracking && !alreadyHasWidgetStateScope) {
// If we need interactivity and no MixWidgetStateModel is present,
// wrap in MixInteractionDetector
current = MixInteractionDetector(controller: _controller, child: current);
Expand Down
21 changes: 21 additions & 0 deletions packages/mix/lib/src/variants/variant.dart
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,21 @@ class ContextVariant extends Variant {
return ContextVariant.breakpoint(BreakpointToken.desktop());
}

/// Widget states that must be tracked for this variant to be evaluated.
///
/// [Style.widgetStates] uses this declaration to discover dependencies in a
/// complete nested style. [StyleBuilder] can then install automatic tracking
/// for pointer-driven states such as hovered and pressed. Other states, such
/// as focused and disabled, still require an ancestor state scope or an
/// external [WidgetStatesController]. Subclasses that read widget state —
/// directly, or by delegating to another variant the way [NotVariant] does —
/// must override this getter.
///
/// Discovery does not execute context closures, so states introduced by a
/// [ContextVariantBuilder], or read by a plain [ContextVariant] closure,
/// cannot contribute automatic dependencies.
Set<WidgetState> get widgetStateDependencies => const {};

/// Check if this variant should be active for the given context
bool when(BuildContext context) {
return shouldApply(context);
Expand Down Expand Up @@ -185,6 +200,9 @@ final class NotVariant extends ContextVariant {
bool operator ==(Object other) =>
identical(this, other) || other is NotVariant && other.inner == inner;

@override
Set<WidgetState> get widgetStateDependencies => inner.widgetStateDependencies;

@override
int get hashCode => inner.hashCode;
}
Expand Down Expand Up @@ -254,6 +272,9 @@ final class WidgetStateVariant extends ContextVariant {
identical(this, other) ||
other is WidgetStateVariant && other.state == state;

@override
Set<WidgetState> get widgetStateDependencies => {state};

@override
int get hashCode => state.hashCode;
}
Expand Down
Loading
Loading