Skip to content
Merged
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
6 changes: 4 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
with:
flutter-version: 'stable'
run-dcm: true
# DCM requires CI credentials; the reusable workflow still runs Dart analysis.
run-dcm: false
melos-commands: |
melos run analyze
melos run schema:inventory
melos run analyze:dart
4 changes: 4 additions & 0 deletions packages/mix/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

### New features

- **Style-state override scope:** `WidgetStateStyleOverride` lets tooling and
tests force widget-state variants through normal `style` resolution, taking
precedence over controllers and nested interaction providers without
changing component behavior.
- **`CssKeywordLinearTransform`:** Adds a reusable bounds-aware
`GradientTransform` for CSS linear-gradient keyword directions, so Tailwind
corner gradients can round-trip through schema tooling without losing visual
Expand Down
1 change: 1 addition & 0 deletions packages/mix/lib/mix.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export 'src/core/prop_source.dart';
export 'src/core/providers/style_provider.dart';
export 'src/core/providers/style_spec_provider.dart';
export 'src/core/providers/widget_state_provider.dart';
export 'src/core/providers/widget_state_style_override.dart';
export 'src/core/spec.dart';
export 'src/core/style.dart' hide StyleElement;
export 'src/core/style_builder.dart';
Expand Down
77 changes: 21 additions & 56 deletions packages/mix/lib/src/core/providers/widget_state_provider.dart
Original file line number Diff line number Diff line change
@@ -1,17 +1,6 @@
import 'package:flutter/foundation.dart' show setEquals;
import 'package:flutter/widgets.dart'
show WidgetState, InheritedModel, BuildContext, WidgetStatesController;

/// Extension on [Set<WidgetState>] to provide convenient boolean checks
/// for specific widget states.
extension on Set<WidgetState> {
bool get hasDisabled => contains(WidgetState.disabled);
bool get hasHovered => contains(WidgetState.hovered);
bool get hasFocused => contains(WidgetState.focused);
bool get hasPressed => contains(WidgetState.pressed);
bool get hasDragged => contains(WidgetState.dragged);
bool get hasSelected => contains(WidgetState.selected);
bool get hasError => contains(WidgetState.error);
}
show BuildContext, InheritedModel, WidgetState, WidgetStatesController;

/// Provider for widget state information using Flutter's [InheritedModel].
///
Expand All @@ -23,13 +12,7 @@ class WidgetStateProvider extends InheritedModel<WidgetState> {
super.key,
required Set<WidgetState> states,
required super.child,
}) : disabled = states.hasDisabled,
hovered = states.hasHovered,
focused = states.hasFocused,
pressed = states.hasPressed,
dragged = states.hasDragged,
selected = states.hasSelected,
error = states.hasError;
}) : states = Set.unmodifiable(states);

/// Retrieves the [WidgetStateProvider] from the widget tree.
///
Expand All @@ -46,67 +29,49 @@ class WidgetStateProvider extends InheritedModel<WidgetState> {
///
/// Returns false if no [WidgetStateProvider] is found in the widget tree.
static bool hasStateOf(BuildContext context, WidgetState state) {
final model = of(context, state);
if (model == null) {
return false;
}

return switch (state) {
.disabled => model.disabled,
.hovered => model.hovered,
.focused => model.focused,
.pressed => model.pressed,
.dragged => model.dragged,
.selected => model.selected,
.error => model.error,
.scrolledUnder => false,
};
return of(context, state)?.states.contains(state) ?? false;
}

/// The active widget states.
final Set<WidgetState> states;

/// Whether the widget is disabled.
final bool disabled;
bool get disabled => states.contains(WidgetState.disabled);

/// Whether the widget is being hovered.
final bool hovered;
bool get hovered => states.contains(WidgetState.hovered);

/// Whether the widget has focus.
final bool focused;
bool get focused => states.contains(WidgetState.focused);

/// Whether the widget is being pressed.
final bool pressed;
bool get pressed => states.contains(WidgetState.pressed);

/// Whether the widget is being dragged.
final bool dragged;
bool get dragged => states.contains(WidgetState.dragged);

/// Whether the widget is selected.
final bool selected;
bool get selected => states.contains(WidgetState.selected);

/// Whether the widget has an error.
final bool error;
bool get error => states.contains(WidgetState.error);

/// Whether the widget is scrolled under another widget.
bool get scrolledUnder => states.contains(WidgetState.scrolledUnder);

@override
bool updateShouldNotify(WidgetStateProvider oldWidget) {
return oldWidget.disabled != disabled ||
oldWidget.hovered != hovered ||
oldWidget.focused != focused ||
oldWidget.pressed != pressed ||
oldWidget.dragged != dragged ||
oldWidget.selected != selected ||
oldWidget.error != error;
return !setEquals(oldWidget.states, states);
}

@override
bool updateShouldNotifyDependent(
WidgetStateProvider oldWidget,
Set<WidgetState> dependencies,
) {
return oldWidget.disabled != disabled && dependencies.hasDisabled ||
oldWidget.hovered != hovered && dependencies.hasHovered ||
oldWidget.focused != focused && dependencies.hasFocused ||
oldWidget.pressed != pressed && dependencies.hasPressed ||
oldWidget.dragged != dragged && dependencies.hasDragged ||
oldWidget.selected != selected && dependencies.hasSelected ||
oldWidget.error != error && dependencies.hasError;
return dependencies.any(
(state) => oldWidget.states.contains(state) != states.contains(state),
);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';

/// Overrides widget states used for style resolution below this scope.
///
/// This is intended for tooling and tests that need to render a component in a
/// specific visual state without changing the component's interaction state.
/// When present, the override takes precedence over explicit controllers and
/// ordinary [WidgetStateProvider] scopes when resolving widget-state variants.
class WidgetStateStyleOverride extends InheritedWidget {
WidgetStateStyleOverride({
super.key,
required Set<WidgetState> states,
required super.child,
}) : states = Set.unmodifiable(states);

/// Returns the nearest style-state override, if one exists.
static WidgetStateStyleOverride? maybeOf(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType();
}

/// The widget states to use when resolving descendant Mix styles.
final Set<WidgetState> states;

@override
bool updateShouldNotify(WidgetStateStyleOverride oldWidget) {
return !setEquals(oldWidget.states, states);
}
}
4 changes: 4 additions & 0 deletions packages/mix/lib/src/variants/variant.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:flutter/material.dart';

import '../core/breakpoint.dart';
import '../core/providers/widget_state_provider.dart';
import '../core/providers/widget_state_style_override.dart';
import '../core/spec.dart';
import '../core/style.dart';
import '../theme/tokens/token_refs.dart';
Expand Down Expand Up @@ -242,6 +243,9 @@ final class WidgetStateVariant extends ContextVariant {

WidgetStateVariant(this.state)
: super('widget_state_${state.name}', (context) {
final override = WidgetStateStyleOverride.maybeOf(context);
if (override != null) return override.states.contains(state);

return WidgetStateProvider.hasStateOf(context, state);
});

Expand Down
Loading
Loading