diff --git a/examples/todo_list/lib/main.dart b/examples/todo_list/lib/main.dart index 0a4e4df2e..36639870f 100644 --- a/examples/todo_list/lib/main.dart +++ b/examples/todo_list/lib/main.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:mix/mix.dart'; -import 'package:todo_list/pages/todo_list_page.dart'; +import 'pages/todo_list_page.dart'; import 'style/design_tokens.dart'; void main() { diff --git a/examples/todo_list/lib/style/components/checkbox.dart b/examples/todo_list/lib/style/components/checkbox.dart index d3c47fc54..dd0dd1c45 100644 --- a/examples/todo_list/lib/style/components/checkbox.dart +++ b/examples/todo_list/lib/style/components/checkbox.dart @@ -30,7 +30,15 @@ class TodoCheckbox extends StatelessWidget { $box.borderRadius(3), scaleEffect(), outlinePattern(), + $icon.size(16), + $icon.color.ref($token.color.surface), + $icon.wrap.opacity(0), + $icon.wrap.padding.top(5), + $icon.wrap.scale(0.5), _CheckboxVariant.checked( + $icon.wrap.padding.top(0), + $icon.wrap.scale(2), + $icon.wrap.opacity(1), $box.color.ref($token.color.primary), $box.border.color.ref($token.color.primary), ), @@ -41,24 +49,8 @@ class TodoCheckbox extends StatelessWidget { .animate( duration: const Duration(milliseconds: 150), ), - child: StyledIcon( + child: const StyledIcon( Icons.check, - style: Style( - $icon.weight(16), - $icon.color.ref($token.color.surface), - $with.opacity(0), - $with.padding.top(5), - _CheckboxVariant.checked( - $with.padding.top(0), - $with.opacity(1), - ), - ) - .applyVariant( - value ? _CheckboxVariant.checked : _CheckboxVariant.unchecked, - ) - .animate( - duration: const Duration(milliseconds: 300), - ), ), ); } diff --git a/melos.yaml b/melos.yaml index 877a6c11d..60d809a22 100644 --- a/melos.yaml +++ b/melos.yaml @@ -43,6 +43,10 @@ scripts: custom_lint_analyze: run: dart pub global activate custom_lint && melos exec --depends-on="mix_lint" custom_lint + mix_exports: + run: melos exec --scope="packages/mix" dart run ./scripts/exports.dart + description: Generate exports for the mix package + analyze: run: | dcm analyze --fatal-warnings --fatal-style --fatal-performance . diff --git a/packages/mix/lib/src/attributes/attributes.dart b/packages/mix/lib/src/attributes/attributes.dart index 1b754a305..b0ce4de3f 100644 --- a/packages/mix/lib/src/attributes/attributes.dart +++ b/packages/mix/lib/src/attributes/attributes.dart @@ -24,6 +24,9 @@ export 'enum/enum_util.dart'; export 'gap/gap_util.dart'; export 'gap/spacing_side_dto.dart'; export 'gradient/gradient_dto.dart'; +export 'modifiers/widget_modifiers_data.dart'; +export 'modifiers/widget_modifiers_data_dto.dart'; +export 'modifiers/widget_modifiers_util.dart'; export 'nested_style/nested_style_attribute.dart'; export 'nested_style/nested_style_util.dart'; export 'scalars/scalar_util.dart'; diff --git a/packages/mix/lib/src/attributes/modifiers/widget_modifiers_data.dart b/packages/mix/lib/src/attributes/modifiers/widget_modifiers_data.dart new file mode 100644 index 000000000..deb4f86fe --- /dev/null +++ b/packages/mix/lib/src/attributes/modifiers/widget_modifiers_data.dart @@ -0,0 +1,7 @@ +import '../../core/modifier.dart'; + +class WidgetModifiersData { + // ignore: avoid-dynamic + final List> value; + const WidgetModifiersData(this.value); +} diff --git a/packages/mix/lib/src/attributes/modifiers/widget_modifiers_data_dto.dart b/packages/mix/lib/src/attributes/modifiers/widget_modifiers_data_dto.dart new file mode 100644 index 000000000..628411454 --- /dev/null +++ b/packages/mix/lib/src/attributes/modifiers/widget_modifiers_data_dto.dart @@ -0,0 +1,29 @@ +import '../../core/core.dart'; +import 'widget_modifiers_data.dart'; + +class WidgetModifiersDataDto extends Dto { + final List value; + + const WidgetModifiersDataDto(this.value); + + @override + WidgetModifiersDataDto merge(WidgetModifiersDataDto? other) { + if (other == null) return this; + final thisMap = AttributeMap(value); + final otherMap = AttributeMap(other.value); + final mergedMap = thisMap.merge(otherMap).values; + + return WidgetModifiersDataDto(mergedMap); + } + + @override + WidgetModifiersData resolve(MixData mix) { + return WidgetModifiersData(value.map((e) => e.resolve(mix)).toList()); + } + + @override + WidgetModifiersData get defaultValue => const WidgetModifiersData([]); + + @override + List get props => [value]; +} diff --git a/packages/mix/lib/src/attributes/modifiers/widget_modifiers_util.dart b/packages/mix/lib/src/attributes/modifiers/widget_modifiers_util.dart new file mode 100644 index 000000000..e57f5a5aa --- /dev/null +++ b/packages/mix/lib/src/attributes/modifiers/widget_modifiers_util.dart @@ -0,0 +1,13 @@ +import '../../core/core.dart'; +import '../../modifiers/modifiers.dart'; +import 'widget_modifiers_data_dto.dart'; + +final class SpecModifierUtility + extends ModifierUtility { + SpecModifierUtility(super.builder); + + @override + T only(WidgetModifierAttribute attribute) { + return builder(WidgetModifiersDataDto([attribute])); + } +} diff --git a/packages/mix/lib/src/core/attributes_map.dart b/packages/mix/lib/src/core/attributes_map.dart index a983690f0..4d94fca14 100644 --- a/packages/mix/lib/src/core/attributes_map.dart +++ b/packages/mix/lib/src/core/attributes_map.dart @@ -53,7 +53,8 @@ class AttributeMap { Attr? attributeOfType() => _map?[Attr] as Attr?; - Iterable whereType() => _map?.values.whereType() ?? []; + Iterable whereType() => + _map?.values.whereType() ?? []; AttributeMap merge(AttributeMap? other) { return other == null ? this : AttributeMap([...values, ...other.values]); diff --git a/packages/mix/lib/src/core/factory/mix_data.dart b/packages/mix/lib/src/core/factory/mix_data.dart index 3e3d5fe26..4f734698c 100644 --- a/packages/mix/lib/src/core/factory/mix_data.dart +++ b/packages/mix/lib/src/core/factory/mix_data.dart @@ -1,3 +1,4 @@ +// ignore_for_file: avoid-dynamic import 'package:flutter/widgets.dart'; import '../../attributes/animated/animated_data.dart'; @@ -59,6 +60,13 @@ class MixData { @visibleForTesting AttributeMap get attributes => _attributes; + List> get modifiers { + return _attributes + .whereType() + .map((e) => e.resolve(this)) + .toList(); + } + MixData toInheritable() { final inheritableAttributes = _attributes.values.where( (attr) => attr is! WidgetModifierAttribute, @@ -75,6 +83,11 @@ class MixData { return _mergeAttributes(attributes) ?? attributes.last; } + List> + modifiersOf>() { + return modifiers.whereType().toList(); + } + Iterable whereType() { return _attributes.whereType(); } diff --git a/packages/mix/lib/src/core/modifier.dart b/packages/mix/lib/src/core/modifier.dart index 853142cdc..f0b24df93 100644 --- a/packages/mix/lib/src/core/modifier.dart +++ b/packages/mix/lib/src/core/modifier.dart @@ -7,7 +7,6 @@ import 'utility.dart'; abstract base class WidgetModifierSpec> extends Spec { - @override const WidgetModifierSpec({super.animated}); static WidgetModifierSpec? lerpValue( diff --git a/packages/mix/lib/src/core/spec.dart b/packages/mix/lib/src/core/spec.dart index d94bd1b3c..7df22de68 100644 --- a/packages/mix/lib/src/core/spec.dart +++ b/packages/mix/lib/src/core/spec.dart @@ -1,7 +1,10 @@ import 'package:flutter/foundation.dart'; +import 'package:mix_annotations/mix_annotations.dart'; import '../attributes/animated/animated_data.dart'; import '../attributes/animated/animated_data_dto.dart'; +import '../attributes/modifiers/widget_modifiers_data.dart'; +import '../attributes/modifiers/widget_modifiers_data_dto.dart'; import '../internal/compare_mixin.dart'; import 'attribute.dart'; import 'factory/mix_data.dart'; @@ -11,7 +14,10 @@ import 'utility.dart'; abstract class Spec> with EqualityMixin { final AnimatedData? animated; - const Spec({this.animated}); + @MixableProperty(utilities: [MixableUtility(alias: 'wrap')]) + final WidgetModifiersData? modifiers; + + const Spec({this.animated, this.modifiers}); Type get type => T; @@ -31,8 +37,9 @@ abstract class Spec> with EqualityMixin { /// The [Self] type represents the concrete implementation of the attribute, while the [Value] type represents the resolvable value. abstract base class SpecAttribute extends StyledAttribute { final AnimatedDataDto? animated; + final WidgetModifiersDataDto? modifiers; - const SpecAttribute({this.animated}); + const SpecAttribute({this.animated, this.modifiers}); Value resolve(MixData mix); @override diff --git a/packages/mix/lib/src/core/styled_widget.dart b/packages/mix/lib/src/core/styled_widget.dart index e029e5ee2..1fa95e541 100644 --- a/packages/mix/lib/src/core/styled_widget.dart +++ b/packages/mix/lib/src/core/styled_widget.dart @@ -1,9 +1,7 @@ import 'package:flutter/widgets.dart'; -import '../modifiers/render_widget_modifier.dart'; -import 'factory/mix_data.dart'; -import 'factory/mix_provider.dart'; -import 'factory/style_mix.dart'; +import '../modifiers/modifiers.dart'; +import 'core.dart'; /// An abstract widget for applying custom styles. /// @@ -60,16 +58,21 @@ abstract class StyledWidget extends StatelessWidget { } Widget applyModifiers(MixData mix, Widget child) { + final modifiers = mix + .whereType() + .map((e) => e.resolve(mix)) + .toList(); + return mix.isAnimated ? RenderAnimatedModifiers( - mix: mix, - orderOfModifiers: orderOfModifiers, + modifiers: modifiers, duration: mix.animation!.duration, + orderOfModifiers: orderOfModifiers, curve: mix.animation!.curve, child: child, ) : RenderModifiers( - mix: mix, + modifiers: modifiers, orderOfModifiers: orderOfModifiers, child: child, ); diff --git a/packages/mix/lib/src/modifiers/render_widget_modifier.dart b/packages/mix/lib/src/modifiers/render_widget_modifier.dart index b1249188e..35201af45 100644 --- a/packages/mix/lib/src/modifiers/render_widget_modifier.dart +++ b/packages/mix/lib/src/modifiers/render_widget_modifier.dart @@ -1,10 +1,10 @@ // ignore_for_file: avoid-dynamic -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; -import '../core/attributes_map.dart'; import '../core/factory/mix_data.dart'; import '../core/modifier.dart'; +import '../core/spec.dart'; import 'align_widget_modifier.dart'; import 'aspect_ratio_widget_modifier.dart'; import 'clip_widget_modifier.dart'; @@ -15,79 +15,80 @@ import 'sized_box_widget_modifier.dart'; import 'transform_widget_modifier.dart'; import 'visibility_widget_modifier.dart'; -// Default order of modifiers and their logic: const _defaultOrder = [ // 1. VisibilityModifier: Controls overall visibility. If the widget is set to be invisible, // none of the subsequent decorations are processed, providing an early exit and optimizing performance. - VisibilityModifierAttribute, + VisibilityModifierSpec, // 2. SizedBoxModifier: Explicitly sets the size of the widget before any other transformations are applied. // This ensures that the widget occupies a predetermined space, which is crucial for layouts that require exact dimensions. - SizedBoxModifierAttribute, + SizedBoxModifierSpec, // 3. FractionallySizedBoxModifier: Adjusts the widget's size relative to its parent's size, // allowing for responsive layouts that scale with the parent widget. This modifier is applied after // explicit sizing to refine the widget's dimensions based on available space. - FractionallySizedBoxModifierAttribute, + FractionallySizedBoxModifierSpec, // 4. AlignModifier: Aligns the widget within its allocated space, which is especially important // for positioning the widget correctly before applying any transformations that could affect its position. // Alignment is based on the size constraints established by previous modifiers. - AlignModifierAttribute, + AlignModifierSpec, // 5. IntrinsicHeightModifier: Adjusts the widget's height to fit its child's intrinsic height, // ensuring that the widget does not force its children to conform to an unnatural height. This is particularly // useful for widgets that should size themselves based on content. - IntrinsicHeightModifierAttribute, + IntrinsicHeightModifierSpec, // 6. IntrinsicWidthModifier: Similar to the IntrinsicHeightModifier, this adjusts the widget's width // to its child's intrinsic width. This modifier allows for content-driven width adjustments, making it ideal // for widgets that need to wrap their content tightly. - IntrinsicWidthModifierAttribute, + IntrinsicWidthModifierSpec, // 7. AspectRatioModifier: Maintains the widget's aspect ratio after sizing adjustments. // This modifier ensures that the widget scales correctly within its given aspect ratio constraints, // which is critical for preserving the visual integrity of images and other aspect-sensitive content. - AspectRatioModifierAttribute, + AspectRatioModifierSpec, // 9. TransformModifier: Applies arbitrary transformations, such as rotation, scaling, and translation. // Transformations are applied after all sizing and positioning adjustments to modify the widget's appearance // and position in more complex ways without altering the logical layout. - TransformModifierAttribute, + TransformModifierSpec, // 10. Clip Modifiers: Applies clipping in various shapes to the transformed widget, shaping the final appearance. // Clipping is one of the last steps to ensure it is applied to the widget's final size, position, and transformation state. - ClipOvalModifierAttribute, - ClipRRectModifierAttribute, - ClipPathModifierAttribute, - ClipTriangleModifierAttribute, - ClipRectModifierAttribute, + ClipOvalModifierSpec, + ClipRRectModifierSpec, + ClipPathModifierSpec, + ClipTriangleModifierSpec, + ClipRectModifierSpec, // 11. OpacityModifier: Modifies the widget's opacity as the final decoration step. Applying opacity last ensures // that it does not influence the layout or transformations, serving purely as a visual effect to alter the transparency // of the widget and its decorations. - OpacityModifierAttribute, + OpacityModifierSpec, ]; class RenderModifiers extends StatelessWidget { const RenderModifiers({ - required this.mix, required this.child, - super.key, + this.modifiers = const [], + @Deprecated("Use modifiers parameter") this.mix, required this.orderOfModifiers, + super.key, }); - final MixData mix; final Widget child; + final MixData? mix; final List orderOfModifiers; + final List> modifiers; @override Widget build(BuildContext context) { - final specs = resolveModifierSpecs(orderOfModifiers, mix); - var current = child; - for (final spec in specs) { + final orderedSpecs = _combineModifiers(mix, modifiers, orderOfModifiers); + + for (final spec in orderedSpecs.reversed) { current = spec.build(current); } @@ -96,19 +97,23 @@ class RenderModifiers extends StatelessWidget { } class RenderAnimatedModifiers extends ImplicitlyAnimatedWidget { - const RenderAnimatedModifiers({ - required this.mix, + RenderAnimatedModifiers({ + //TODO Should be required in the next version + this.modifiers = const [], required this.child, + required super.duration, + @Deprecated("Use modifiers parameter") this.mix, required this.orderOfModifiers, super.key, - required super.duration, super.curve = Curves.linear, super.onEnd, - }); + }) : _appliedModifiers = _combineModifiers(mix, modifiers, orderOfModifiers); - final MixData mix; final Widget child; + final MixData? mix; final List orderOfModifiers; + final List> modifiers; + final List> _appliedModifiers; @override RenderAnimatedModifiersState createState() => RenderAnimatedModifiersState(); @@ -118,14 +123,40 @@ class RenderAnimatedModifiersState extends AnimatedWidgetBaseState { final Map _specs = {}; + @override + void didUpdateWidget(covariant RenderAnimatedModifiers oldWidget) { + if (oldWidget.modifiers != widget.modifiers || + oldWidget.mix != widget.mix || + oldWidget.orderOfModifiers != widget.orderOfModifiers) { + cleanUpSpecs(); + } + + super.didUpdateWidget(oldWidget); + } + @override void forEachTween(TweenVisitor visitor) { - final specs = resolveModifierSpecs(widget.orderOfModifiers, widget.mix); + updateModifiersSpecs(visitor); + } - for (final spec in specs) { + Map cleanUpSpecs() { + final difference = _specs.keys + .toSet() + .difference(widget._appliedModifiers.map((e) => e.runtimeType).toSet()); + + if (difference.isNotEmpty) { + for (var e in difference) { + _specs.remove(e); + } + } + + return _specs; + } + + void updateModifiersSpecs(TweenVisitor visitor) { + for (final spec in widget._appliedModifiers.reversed) { final specType = spec.runtimeType; final previousSpec = _specs[specType]; - _specs[specType] = visitor( previousSpec, spec, @@ -148,34 +179,71 @@ class RenderAnimatedModifiersState } } -Set resolveModifierSpecs( +@visibleForTesting +List> resolveModifierSpecs( List orderOfModifiers, MixData mix, ) { - final modifiers = mix.whereType(); - - if (modifiers.isEmpty) return {}; - final modifierMap = AttributeMap(modifiers).toMap(); + return orderModifiers(orderOfModifiers, mix.modifiers); +} - final listOfModifiers = { +@visibleForTesting +List> orderModifiers( + List orderOfModifiers, + List> modifiers, +) { + final listOfModifiers = ({ // Prioritize the order of modifiers provided by the user. ...orderOfModifiers, // Add the default order of modifiers. ..._defaultOrder, // Add any remaining modifiers that were not included in the order. - ...modifierMap.keys, - }.toList().reversed; + ...modifiers.map((e) => e.type), + }).toList(); - final specs = []; + final specs = >[]; for (final modifierType in listOfModifiers) { // Resolve the modifier and add it to the list of specs. - final modifier = modifierMap.remove(modifierType); + final modifier = modifiers.where((e) => e.type == modifierType).firstOrNull; if (modifier == null) continue; - specs.add(modifier.resolve(mix) as WidgetModifierSpec); + // ignore: avoid-unnecessary-type-casts + specs.add(modifier as WidgetModifierSpec>); } - return specs.toSet(); + return specs; +} + +class RenderSpecModifiers extends StatelessWidget { + const RenderSpecModifiers({ + required this.orderOfModifiers, + required this.child, + required this.spec, + super.key, + }); + + final Widget child; + final List orderOfModifiers; + final Spec spec; + + @override + Widget build(BuildContext context) { + final modifiers = spec.modifiers?.value ?? []; + + return spec.isAnimated + ? RenderAnimatedModifiers( + modifiers: modifiers, + duration: spec.animated!.duration, + orderOfModifiers: orderOfModifiers, + curve: spec.animated!.curve, + child: child, + ) + : RenderModifiers( + modifiers: modifiers, + orderOfModifiers: orderOfModifiers, + child: child, + ); + } } class ModifierSpecTween extends Tween { @@ -190,3 +258,40 @@ class ModifierSpecTween extends Tween { WidgetModifierSpec lerp(double t) => WidgetModifierSpec.lerpValue(begin, end, t)!; } + +List _normalizeOrderedTypes(MixData? mix, List? orderedTypes) { + orderedTypes ??= []; + if (mix == null) return orderedTypes; + + final modifierAttributes = mix.whereType(); + + final specs = [...orderedTypes]; + + for (int i = 0; i < orderedTypes.length; i++) { + final type = orderedTypes[i]; + // Resolve the modifier and replace the attribute type with the spec type. + final modifier = + modifierAttributes.where((e) => e.runtimeType == type).firstOrNull; + if (modifier != null) { + specs[i] = modifier.resolve(mix).runtimeType; + } + } + + return specs; +} + +List> _combineModifiers( + MixData? mix, + List> modifiers, + List orderOfModifiers, +) { + final normalizedModifiers = _normalizeOrderedTypes(mix, orderOfModifiers); + + final mergedModifiers = [...modifiers]; + + if (mix != null) { + mergedModifiers.addAll(mix.modifiers); + } + + return orderModifiers(normalizedModifiers, mergedModifiers); +} diff --git a/packages/mix/lib/src/modifiers/widget_modifiers_util.dart b/packages/mix/lib/src/modifiers/widget_modifiers_util.dart index ca28f269e..4bebfd998 100644 --- a/packages/mix/lib/src/modifiers/widget_modifiers_util.dart +++ b/packages/mix/lib/src/modifiers/widget_modifiers_util.dart @@ -15,33 +15,44 @@ import 'sized_box_widget_modifier.dart'; import 'transform_widget_modifier.dart'; import 'visibility_widget_modifier.dart'; -class WithModifierUtility - extends MixUtility { - late final intrinsicWidth = IntrinsicWidthWidgetUtility(builder); - late final intrinsicHeight = IntrinsicHeightWidgetUtility(builder); - late final rotate = RotatedBoxWidgetUtility(builder); - late final opacity = OpacityUtility(builder); - late final clipPath = ClipPathUtility(builder); - late final clipRRect = ClipRRectUtility(builder); - late final clipOval = ClipOvalUtility(builder); - late final clipRect = ClipRectUtility(builder); - late final clipTriangle = ClipTriangleUtility(builder); - late final visibility = VisibilityUtility(builder); +abstract class ModifierUtility + extends MixUtility { + late final intrinsicWidth = IntrinsicWidthWidgetUtility(only); + late final intrinsicHeight = IntrinsicHeightWidgetUtility(only); + late final rotate = RotatedBoxWidgetUtility(only); + late final opacity = OpacityUtility(only); + late final clipPath = ClipPathUtility(only); + late final clipRRect = ClipRRectUtility(only); + late final clipOval = ClipOvalUtility(only); + late final clipRect = ClipRectUtility(only); + late final clipTriangle = ClipTriangleUtility(only); + late final visibility = VisibilityUtility(only); late final show = visibility.on; late final hide = visibility.off; - late final aspectRatio = AspectRatioUtility(builder); - late final flexible = FlexibleModifierUtility(builder); + late final aspectRatio = AspectRatioUtility(only); + late final flexible = FlexibleModifierUtility(only); late final expanded = flexible.expanded; - late final transform = TransformUtility(builder); + late final transform = TransformUtility(only); late final scale = transform.scale; - late final align = AlignWidgetUtility(builder); - late final fractionallySizedBox = - FractionallySizedBoxModifierUtility(builder); - late final sizedBox = SizedBoxModifierUtility(builder); - late final padding = SpacingUtility(PaddingModifierUtility(builder).call); + late final align = AlignWidgetUtility(only); + late final fractionallySizedBox = FractionallySizedBoxModifierUtility(only); + late final sizedBox = SizedBoxModifierUtility(only); + late final padding = SpacingUtility(PaddingModifierUtility(only).call); + + ModifierUtility(super.builder); + + T only(WidgetModifierAttribute attribute); +} +class WithModifierUtility + extends ModifierUtility { static final self = WithModifierUtility(MixUtility.selfBuilder); WithModifierUtility(super.builder); + + @override + T only(WidgetModifierAttribute attribute) { + return builder(attribute); + } } diff --git a/packages/mix/lib/src/specs/box/box_spec.dart b/packages/mix/lib/src/specs/box/box_spec.dart index ef3a31a13..c94f84e2b 100644 --- a/packages/mix/lib/src/specs/box/box_spec.dart +++ b/packages/mix/lib/src/specs/box/box_spec.dart @@ -96,17 +96,23 @@ final class BoxSpec extends Spec with _$BoxSpec { this.clipBehavior, this.width, this.height, + super.modifiers, super.animated, }); - Widget call({Widget? child}) { + Widget call({Widget? child, List orderOfModifiers = const []}) { return isAnimated ? AnimatedBoxSpecWidget( spec: this, duration: animated!.duration, curve: animated!.curve, + orderOfModifiers: orderOfModifiers, child: child, ) - : BoxSpecWidget(spec: this, child: child); + : BoxSpecWidget( + spec: this, + orderOfModifiers: orderOfModifiers, + child: child, + ); } } diff --git a/packages/mix/lib/src/specs/box/box_spec.g.dart b/packages/mix/lib/src/specs/box/box_spec.g.dart index f916dddc8..0a373d908 100644 --- a/packages/mix/lib/src/specs/box/box_spec.g.dart +++ b/packages/mix/lib/src/specs/box/box_spec.g.dart @@ -45,6 +45,7 @@ base mixin _$BoxSpec on Spec { Clip? clipBehavior, double? width, double? height, + WidgetModifiersData? modifiers, AnimatedData? animated, }) { return BoxSpec( @@ -59,6 +60,7 @@ base mixin _$BoxSpec on Spec { clipBehavior: clipBehavior ?? _$this.clipBehavior, width: width ?? _$this.width, height: height ?? _$this.height, + modifiers: modifiers ?? _$this.modifiers, animated: animated ?? _$this.animated, ); } @@ -81,7 +83,7 @@ base mixin _$BoxSpec on Spec { /// - [MixHelpers.lerpMatrix4] for [transform]. /// - [MixHelpers.lerpDouble] for [width] and [height]. - /// For [clipBehavior] and [animated], the interpolation is performed using a step function. + /// For [clipBehavior] and [modifiers] and [animated], the interpolation is performed using a step function. /// If [t] is less than 0.5, the value from the current [BoxSpec] is used. Otherwise, the value /// from the [other] [BoxSpec] is used. /// @@ -106,6 +108,7 @@ base mixin _$BoxSpec on Spec { clipBehavior: t < 0.5 ? _$this.clipBehavior : other.clipBehavior, width: MixHelpers.lerpDouble(_$this.width, other.width, t), height: MixHelpers.lerpDouble(_$this.height, other.height, t), + modifiers: t < 0.5 ? _$this.modifiers : other.modifiers, animated: t < 0.5 ? _$this.animated : other.animated, ); } @@ -127,6 +130,7 @@ base mixin _$BoxSpec on Spec { _$this.clipBehavior, _$this.width, _$this.height, + _$this.modifiers, _$this.animated, ]; @@ -165,6 +169,7 @@ final class BoxSpecAttribute extends SpecAttribute { this.clipBehavior, this.width, this.height, + super.modifiers, super.animated, }); @@ -190,6 +195,7 @@ final class BoxSpecAttribute extends SpecAttribute { clipBehavior: clipBehavior, width: width, height: height, + modifiers: modifiers?.resolve(mix), animated: animated?.resolve(mix) ?? mix.animation, ); } @@ -219,6 +225,7 @@ final class BoxSpecAttribute extends SpecAttribute { clipBehavior: other.clipBehavior ?? clipBehavior, width: other.width ?? width, height: other.height ?? height, + modifiers: modifiers?.merge(other.modifiers) ?? other.modifiers, animated: animated?.merge(other.animated) ?? other.animated, ); } @@ -240,6 +247,7 @@ final class BoxSpecAttribute extends SpecAttribute { clipBehavior, width, height, + modifiers, animated, ]; } @@ -340,6 +348,9 @@ base class BoxSpecUtility /// Utility for defining [BoxSpecAttribute.height] late final height = DoubleUtility((v) => only(height: v)); + /// Utility for defining [BoxSpecAttribute.modifiers] + late final wrap = SpecModifierUtility((v) => only(modifiers: v)); + /// Utility for defining [BoxSpecAttribute.animated] late final animated = AnimatedUtility((v) => only(animated: v)); @@ -361,6 +372,7 @@ base class BoxSpecUtility Clip? clipBehavior, double? width, double? height, + WidgetModifiersDataDto? modifiers, AnimatedDataDto? animated, }) { return builder(BoxSpecAttribute( @@ -375,6 +387,7 @@ base class BoxSpecUtility clipBehavior: clipBehavior, width: width, height: height, + modifiers: modifiers, animated: animated, )); } diff --git a/packages/mix/lib/src/specs/box/box_widget.dart b/packages/mix/lib/src/specs/box/box_widget.dart index dfa5787e7..81cf14743 100644 --- a/packages/mix/lib/src/specs/box/box_widget.dart +++ b/packages/mix/lib/src/specs/box/box_widget.dart @@ -2,6 +2,7 @@ import 'package:flutter/widgets.dart'; import '../../core/factory/mix_provider.dart'; import '../../core/styled_widget.dart'; +import '../../modifiers/render_widget_modifier.dart'; import 'box_spec.dart'; /// A [Container] equivalent widget for applying styles using Mix. @@ -53,39 +54,42 @@ class Box extends StyledWidget { return withMix(context, (context) { final spec = BoxSpec.of(context); - return spec.isAnimated - ? AnimatedBoxSpecWidget( - spec: spec, - duration: spec.animated!.duration, - curve: spec.animated!.curve, - child: child, - ) - : BoxSpecWidget(spec: spec, child: child); + return spec(child: child); }); } } class BoxSpecWidget extends StatelessWidget { - const BoxSpecWidget({required this.spec, super.key, this.child}); + const BoxSpecWidget({ + required this.spec, + super.key, + this.child, + this.orderOfModifiers = const [], + }); final Widget? child; final BoxSpec? spec; + final List orderOfModifiers; @override Widget build(BuildContext context) { - return Container( - alignment: spec?.alignment, - padding: spec?.padding, - decoration: spec?.decoration, - foregroundDecoration: spec?.foregroundDecoration, - width: spec?.width, - height: spec?.height, - constraints: spec?.constraints, - margin: spec?.margin, - transform: spec?.transform, - transformAlignment: spec?.transformAlignment, - clipBehavior: spec?.clipBehavior ?? Clip.none, - child: child, + return RenderSpecModifiers( + orderOfModifiers: orderOfModifiers, + spec: spec ?? const BoxSpec(), + child: Container( + alignment: spec?.alignment, + padding: spec?.padding, + decoration: spec?.decoration, + foregroundDecoration: spec?.foregroundDecoration, + width: spec?.width, + height: spec?.height, + constraints: spec?.constraints, + margin: spec?.margin, + transform: spec?.transform, + transformAlignment: spec?.transformAlignment, + clipBehavior: spec?.clipBehavior ?? Clip.none, + child: child, + ), ); } } @@ -98,10 +102,12 @@ class AnimatedBoxSpecWidget extends ImplicitlyAnimatedWidget { required super.duration, super.curve = Curves.linear, super.onEnd, + this.orderOfModifiers = const [], }); final Widget? child; final BoxSpec spec; + final List orderOfModifiers; @override AnimatedWidgetBaseState createState() => diff --git a/packages/mix/lib/src/specs/flex/flex_spec.dart b/packages/mix/lib/src/specs/flex/flex_spec.dart index 6a4e2bc02..74377de13 100644 --- a/packages/mix/lib/src/specs/flex/flex_spec.dart +++ b/packages/mix/lib/src/specs/flex/flex_spec.dart @@ -46,6 +46,7 @@ final class FlexSpec extends Spec with _$FlexSpec { this.clipBehavior, this.gap, super.animated, + super.modifiers, }); Widget call({List children = const [], required Axis direction}) { diff --git a/packages/mix/lib/src/specs/flex/flex_spec.g.dart b/packages/mix/lib/src/specs/flex/flex_spec.g.dart index 2fb8d07dd..d1c8deade 100644 --- a/packages/mix/lib/src/specs/flex/flex_spec.g.dart +++ b/packages/mix/lib/src/specs/flex/flex_spec.g.dart @@ -45,6 +45,7 @@ base mixin _$FlexSpec on Spec { Clip? clipBehavior, double? gap, AnimatedData? animated, + WidgetModifiersData? modifiers, }) { return FlexSpec( crossAxisAlignment: crossAxisAlignment ?? _$this.crossAxisAlignment, @@ -57,6 +58,7 @@ base mixin _$FlexSpec on Spec { clipBehavior: clipBehavior ?? _$this.clipBehavior, gap: gap ?? _$this.gap, animated: animated ?? _$this.animated, + modifiers: modifiers ?? _$this.modifiers, ); } @@ -73,7 +75,7 @@ base mixin _$FlexSpec on Spec { /// /// - [MixHelpers.lerpDouble] for [gap]. - /// For [crossAxisAlignment] and [mainAxisAlignment] and [mainAxisSize] and [verticalDirection] and [direction] and [textDirection] and [textBaseline] and [clipBehavior] and [animated], the interpolation is performed using a step function. + /// For [crossAxisAlignment] and [mainAxisAlignment] and [mainAxisSize] and [verticalDirection] and [direction] and [textDirection] and [textBaseline] and [clipBehavior] and [animated] and [modifiers], the interpolation is performed using a step function. /// If [t] is less than 0.5, the value from the current [FlexSpec] is used. Otherwise, the value /// from the [other] [FlexSpec] is used. /// @@ -97,6 +99,7 @@ base mixin _$FlexSpec on Spec { clipBehavior: t < 0.5 ? _$this.clipBehavior : other.clipBehavior, gap: MixHelpers.lerpDouble(_$this.gap, other.gap, t), animated: t < 0.5 ? _$this.animated : other.animated, + modifiers: t < 0.5 ? _$this.modifiers : other.modifiers, ); } @@ -116,6 +119,7 @@ base mixin _$FlexSpec on Spec { _$this.clipBehavior, _$this.gap, _$this.animated, + _$this.modifiers, ]; FlexSpec get _$this => this as FlexSpec; @@ -150,6 +154,7 @@ final class FlexSpecAttribute extends SpecAttribute { this.clipBehavior, this.gap, super.animated, + super.modifiers, }); /// Resolves to [FlexSpec] using the provided [MixData]. @@ -173,6 +178,7 @@ final class FlexSpecAttribute extends SpecAttribute { clipBehavior: clipBehavior, gap: gap?.resolve(mix), animated: animated?.resolve(mix) ?? mix.animation, + modifiers: modifiers?.resolve(mix), ); } @@ -199,6 +205,7 @@ final class FlexSpecAttribute extends SpecAttribute { clipBehavior: other.clipBehavior ?? clipBehavior, gap: gap?.merge(other.gap) ?? other.gap, animated: animated?.merge(other.animated) ?? other.animated, + modifiers: modifiers?.merge(other.modifiers) ?? other.modifiers, ); } @@ -218,6 +225,7 @@ final class FlexSpecAttribute extends SpecAttribute { clipBehavior, gap, animated, + modifiers, ]; } @@ -267,6 +275,9 @@ base class FlexSpecUtility /// Utility for defining [FlexSpecAttribute.animated] late final animated = AnimatedUtility((v) => only(animated: v)); + /// Utility for defining [FlexSpecAttribute.modifiers] + late final wrap = SpecModifierUtility((v) => only(modifiers: v)); + FlexSpecUtility(super.builder); static final self = FlexSpecUtility((v) => v); @@ -284,6 +295,7 @@ base class FlexSpecUtility Clip? clipBehavior, SpacingSideDto? gap, AnimatedDataDto? animated, + WidgetModifiersDataDto? modifiers, }) { return builder(FlexSpecAttribute( crossAxisAlignment: crossAxisAlignment, @@ -296,6 +308,7 @@ base class FlexSpecUtility clipBehavior: clipBehavior, gap: gap, animated: animated, + modifiers: modifiers, )); } } diff --git a/packages/mix/lib/src/specs/flex/flex_widget.dart b/packages/mix/lib/src/specs/flex/flex_widget.dart index d23623610..83ad00938 100644 --- a/packages/mix/lib/src/specs/flex/flex_widget.dart +++ b/packages/mix/lib/src/specs/flex/flex_widget.dart @@ -3,6 +3,7 @@ import 'package:flutter/widgets.dart'; import '../../core/styled_widget.dart'; +import '../../modifiers/render_widget_modifier.dart'; import '../box/box_spec.dart'; import '../box/box_widget.dart'; import 'flex_spec.dart'; @@ -46,6 +47,7 @@ class StyledFlex extends StyledWidget { return FlexSpecWidget( spec: spec, direction: direction, + orderOfModifiers: orderOfModifiers, children: children, ); }); @@ -58,11 +60,13 @@ class FlexSpecWidget extends StatelessWidget { this.spec, required this.children, required this.direction, + this.orderOfModifiers = const [], }); final List children; final Axis direction; final FlexSpec? spec; + final List orderOfModifiers; List _buildChildren(double? gap) { if (gap == null) return children; @@ -81,8 +85,7 @@ class FlexSpecWidget extends StatelessWidget { @override Widget build(BuildContext context) { final gap = spec?.gap; - - return Flex( + final flexWidget = Flex( direction: direction, mainAxisAlignment: spec?.mainAxisAlignment ?? _defaultFlex.mainAxisAlignment, @@ -93,6 +96,14 @@ class FlexSpecWidget extends StatelessWidget { spec?.verticalDirection ?? _defaultFlex.verticalDirection, children: _buildChildren(gap), ); + + return spec == null + ? flexWidget + : RenderSpecModifiers( + orderOfModifiers: orderOfModifiers, + spec: spec!, + child: flexWidget, + ); } } diff --git a/packages/mix/lib/src/specs/icon/icon_spec.dart b/packages/mix/lib/src/specs/icon/icon_spec.dart index 745d1f2d1..77fe11d92 100644 --- a/packages/mix/lib/src/specs/icon/icon_spec.dart +++ b/packages/mix/lib/src/specs/icon/icon_spec.dart @@ -34,6 +34,7 @@ final class IconSpec extends Spec with _$IconSpec { this.applyTextScaling, this.fill, super.animated, + super.modifiers, }); Widget call(IconData? icon, {String? semanticLabel}) { diff --git a/packages/mix/lib/src/specs/icon/icon_spec.g.dart b/packages/mix/lib/src/specs/icon/icon_spec.g.dart index 84878f37d..abab8837f 100644 --- a/packages/mix/lib/src/specs/icon/icon_spec.g.dart +++ b/packages/mix/lib/src/specs/icon/icon_spec.g.dart @@ -45,6 +45,7 @@ base mixin _$IconSpec on Spec { bool? applyTextScaling, double? fill, AnimatedData? animated, + WidgetModifiersData? modifiers, }) { return IconSpec( color: color ?? _$this.color, @@ -57,6 +58,7 @@ base mixin _$IconSpec on Spec { applyTextScaling: applyTextScaling ?? _$this.applyTextScaling, fill: fill ?? _$this.fill, animated: animated ?? _$this.animated, + modifiers: modifiers ?? _$this.modifiers, ); } @@ -74,7 +76,7 @@ base mixin _$IconSpec on Spec { /// - [Color.lerp] for [color]. /// - [MixHelpers.lerpDouble] for [size] and [weight] and [grade] and [opticalSize] and [fill]. - /// For [shadows] and [textDirection] and [applyTextScaling] and [animated], the interpolation is performed using a step function. + /// For [shadows] and [textDirection] and [applyTextScaling] and [animated] and [modifiers], the interpolation is performed using a step function. /// If [t] is less than 0.5, the value from the current [IconSpec] is used. Otherwise, the value /// from the [other] [IconSpec] is used. /// @@ -97,6 +99,7 @@ base mixin _$IconSpec on Spec { t < 0.5 ? _$this.applyTextScaling : other.applyTextScaling, fill: MixHelpers.lerpDouble(_$this.fill, other.fill, t), animated: t < 0.5 ? _$this.animated : other.animated, + modifiers: t < 0.5 ? _$this.modifiers : other.modifiers, ); } @@ -116,6 +119,7 @@ base mixin _$IconSpec on Spec { _$this.applyTextScaling, _$this.fill, _$this.animated, + _$this.modifiers, ]; IconSpec get _$this => this as IconSpec; @@ -150,6 +154,7 @@ final class IconSpecAttribute extends SpecAttribute { this.applyTextScaling, this.fill, super.animated, + super.modifiers, }); /// Resolves to [IconSpec] using the provided [MixData]. @@ -173,6 +178,7 @@ final class IconSpecAttribute extends SpecAttribute { applyTextScaling: applyTextScaling, fill: fill, animated: animated?.resolve(mix) ?? mix.animation, + modifiers: modifiers?.resolve(mix), ); } @@ -199,6 +205,7 @@ final class IconSpecAttribute extends SpecAttribute { applyTextScaling: other.applyTextScaling ?? applyTextScaling, fill: other.fill ?? fill, animated: animated?.merge(other.animated) ?? other.animated, + modifiers: modifiers?.merge(other.modifiers) ?? other.modifiers, ); } @@ -218,6 +225,7 @@ final class IconSpecAttribute extends SpecAttribute { applyTextScaling, fill, animated, + modifiers, ]; } @@ -258,6 +266,9 @@ base class IconSpecUtility /// Utility for defining [IconSpecAttribute.animated] late final animated = AnimatedUtility((v) => only(animated: v)); + /// Utility for defining [IconSpecAttribute.modifiers] + late final wrap = SpecModifierUtility((v) => only(modifiers: v)); + IconSpecUtility(super.builder); static final self = IconSpecUtility((v) => v); @@ -275,6 +286,7 @@ base class IconSpecUtility bool? applyTextScaling, double? fill, AnimatedDataDto? animated, + WidgetModifiersDataDto? modifiers, }) { return builder(IconSpecAttribute( color: color, @@ -287,6 +299,7 @@ base class IconSpecUtility applyTextScaling: applyTextScaling, fill: fill, animated: animated, + modifiers: modifiers, )); } } diff --git a/packages/mix/lib/src/specs/icon/icon_widget.dart b/packages/mix/lib/src/specs/icon/icon_widget.dart index 38ba008b4..3de0b5134 100644 --- a/packages/mix/lib/src/specs/icon/icon_widget.dart +++ b/packages/mix/lib/src/specs/icon/icon_widget.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../../core/styled_widget.dart'; +import '../../modifiers/render_widget_modifier.dart'; import 'icon_spec.dart'; class StyledIcon extends StyledWidget { @@ -11,8 +12,15 @@ class StyledIcon extends StyledWidget { super.key, super.inherit = true, this.textDirection, - super.orderOfModifiers = const [], - }); + @Deprecated('Use orderOfModifiers parameter instead') + List modifierOrder = const [], + List orderOfModifiers = const [], + }) : assert(modifierOrder == const [] || + orderOfModifiers == const []), + super( + orderOfModifiers: + orderOfModifiers == const [] ? modifierOrder : orderOfModifiers, + ); final IconData? icon; final String? semanticLabel; @@ -50,28 +58,37 @@ class IconSpecWidget extends StatelessWidget { this.semanticLabel, super.key, this.textDirection, - this.modifierOrder = const [], - }); + @Deprecated('Use orderOfModifiers parameter instead') + List modifierOrder = const [], + List orderOfModifiers = const [], + }) : assert(modifierOrder == const [] || + orderOfModifiers == const []), + orderOfModifiers = + orderOfModifiers == const [] ? modifierOrder : orderOfModifiers; final IconData? icon; final IconSpec? spec; final String? semanticLabel; final TextDirection? textDirection; - final List modifierOrder; + final List orderOfModifiers; @override Widget build(BuildContext context) { - return Icon( - icon, - size: spec?.size, - fill: spec?.fill, - weight: spec?.weight, - grade: spec?.grade, - opticalSize: spec?.opticalSize, - color: spec?.color, - shadows: spec?.shadows, - semanticLabel: semanticLabel, - textDirection: textDirection, + return RenderSpecModifiers( + orderOfModifiers: orderOfModifiers, + spec: spec ?? const IconSpec(), + child: Icon( + icon, + size: spec?.size, + fill: spec?.fill, + weight: spec?.weight, + grade: spec?.grade, + opticalSize: spec?.opticalSize, + color: spec?.color, + shadows: spec?.shadows, + semanticLabel: semanticLabel, + textDirection: textDirection, + ), ); } } @@ -85,8 +102,16 @@ class AnimatedStyledIcon extends StyledWidget { required this.progress, super.inherit, this.textDirection, - super.orderOfModifiers = const [], - }); + @Deprecated('Use orderOfModifiers parameter instead') + List modifierOrder = const [], + List orderOfModifiers = const [], + }) : assert(modifierOrder == const [] || + orderOfModifiers == const []), + super( + orderOfModifiers: orderOfModifiers == const [] + ? modifierOrder + : orderOfModifiers, + ); final AnimatedIconData icon; final String? semanticLabel; @@ -117,17 +142,23 @@ class AnimatedIconSpecWidget extends ImplicitlyAnimatedWidget { super.key, this.semanticLabel, this.textDirection, - this.modifierOrder = const [], super.curve, required super.duration, super.onEnd, - }); + @Deprecated('Use orderOfModifiers parameter instead') + List modifierOrder = const [], + List orderOfModifiers = const [], + }) : assert(modifierOrder == const [] || + orderOfModifiers == const []), + orderOfModifiers = orderOfModifiers == const [] + ? modifierOrder + : orderOfModifiers; final IconData? icon; final IconSpec spec; final String? semanticLabel; final TextDirection? textDirection; - final List modifierOrder; + final List orderOfModifiers; @override // ignore: library_private_types_in_public_api @@ -158,6 +189,7 @@ class _AnimatedIconSpecState spec: spec, semanticLabel: widget.semanticLabel, textDirection: widget.textDirection, + orderOfModifiers: widget.orderOfModifiers, ); } } diff --git a/packages/mix/lib/src/specs/image/image_spec.dart b/packages/mix/lib/src/specs/image/image_spec.dart index a590f6647..3dd8286a2 100644 --- a/packages/mix/lib/src/specs/image/image_spec.dart +++ b/packages/mix/lib/src/specs/image/image_spec.dart @@ -32,6 +32,7 @@ final class ImageSpec extends Spec with _$ImageSpec { this.filterQuality, this.colorBlendMode, super.animated, + super.modifiers, }); Widget call({ diff --git a/packages/mix/lib/src/specs/image/image_spec.g.dart b/packages/mix/lib/src/specs/image/image_spec.g.dart index c34e6e75a..4018a1fc9 100644 --- a/packages/mix/lib/src/specs/image/image_spec.g.dart +++ b/packages/mix/lib/src/specs/image/image_spec.g.dart @@ -45,6 +45,7 @@ base mixin _$ImageSpec on Spec { FilterQuality? filterQuality, BlendMode? colorBlendMode, AnimatedData? animated, + WidgetModifiersData? modifiers, }) { return ImageSpec( width: width ?? _$this.width, @@ -57,6 +58,7 @@ base mixin _$ImageSpec on Spec { filterQuality: filterQuality ?? _$this.filterQuality, colorBlendMode: colorBlendMode ?? _$this.colorBlendMode, animated: animated ?? _$this.animated, + modifiers: modifiers ?? _$this.modifiers, ); } @@ -76,7 +78,7 @@ base mixin _$ImageSpec on Spec { /// - [AlignmentGeometry.lerp] for [alignment]. /// - [Rect.lerp] for [centerSlice]. - /// For [repeat] and [fit] and [filterQuality] and [colorBlendMode] and [animated], the interpolation is performed using a step function. + /// For [repeat] and [fit] and [filterQuality] and [colorBlendMode] and [animated] and [modifiers], the interpolation is performed using a step function. /// If [t] is less than 0.5, the value from the current [ImageSpec] is used. Otherwise, the value /// from the [other] [ImageSpec] is used. /// @@ -97,6 +99,7 @@ base mixin _$ImageSpec on Spec { filterQuality: t < 0.5 ? _$this.filterQuality : other.filterQuality, colorBlendMode: t < 0.5 ? _$this.colorBlendMode : other.colorBlendMode, animated: t < 0.5 ? _$this.animated : other.animated, + modifiers: t < 0.5 ? _$this.modifiers : other.modifiers, ); } @@ -116,6 +119,7 @@ base mixin _$ImageSpec on Spec { _$this.filterQuality, _$this.colorBlendMode, _$this.animated, + _$this.modifiers, ]; ImageSpec get _$this => this as ImageSpec; @@ -150,6 +154,7 @@ final class ImageSpecAttribute extends SpecAttribute { this.filterQuality, this.colorBlendMode, super.animated, + super.modifiers, }); /// Resolves to [ImageSpec] using the provided [MixData]. @@ -173,6 +178,7 @@ final class ImageSpecAttribute extends SpecAttribute { filterQuality: filterQuality, colorBlendMode: colorBlendMode, animated: animated?.resolve(mix) ?? mix.animation, + modifiers: modifiers?.resolve(mix), ); } @@ -199,6 +205,7 @@ final class ImageSpecAttribute extends SpecAttribute { filterQuality: other.filterQuality ?? filterQuality, colorBlendMode: other.colorBlendMode ?? colorBlendMode, animated: animated?.merge(other.animated) ?? other.animated, + modifiers: modifiers?.merge(other.modifiers) ?? other.modifiers, ); } @@ -218,6 +225,7 @@ final class ImageSpecAttribute extends SpecAttribute { filterQuality, colorBlendMode, animated, + modifiers, ]; } @@ -258,6 +266,9 @@ base class ImageSpecUtility /// Utility for defining [ImageSpecAttribute.animated] late final animated = AnimatedUtility((v) => only(animated: v)); + /// Utility for defining [ImageSpecAttribute.modifiers] + late final wrap = SpecModifierUtility((v) => only(modifiers: v)); + ImageSpecUtility(super.builder); static final self = ImageSpecUtility((v) => v); @@ -275,6 +286,7 @@ base class ImageSpecUtility FilterQuality? filterQuality, BlendMode? colorBlendMode, AnimatedDataDto? animated, + WidgetModifiersDataDto? modifiers, }) { return builder(ImageSpecAttribute( width: width, @@ -287,6 +299,7 @@ base class ImageSpecUtility filterQuality: filterQuality, colorBlendMode: colorBlendMode, animated: animated, + modifiers: modifiers, )); } } diff --git a/packages/mix/lib/src/specs/image/image_widget.dart b/packages/mix/lib/src/specs/image/image_widget.dart index 455a523dc..e23d535f4 100644 --- a/packages/mix/lib/src/specs/image/image_widget.dart +++ b/packages/mix/lib/src/specs/image/image_widget.dart @@ -2,6 +2,7 @@ import 'package:flutter/widgets.dart'; import '../../core/styled_widget.dart'; import '../../internal/constants.dart'; +import '../../modifiers/render_widget_modifier.dart'; import 'image_spec.dart'; class StyledImage extends StyledWidget { @@ -74,7 +75,7 @@ class StyledImage extends StyledWidget { class ImageSpecWidget extends StatelessWidget { const ImageSpecWidget({ super.key, - this.modifierOrder = const [], + this.orderOfModifiers = const [], this.spec, required this.image, this.frameBuilder, @@ -95,7 +96,7 @@ class ImageSpecWidget extends StatelessWidget { final ImageErrorWidgetBuilder? errorBuilder; final String? semanticLabel; final bool excludeFromSemantics; - final List modifierOrder; + final List orderOfModifiers; final bool gaplessPlayback; final bool isAntiAlias; final bool matchTextDirection; @@ -103,26 +104,30 @@ class ImageSpecWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Image( - image: image, - frameBuilder: frameBuilder, - loadingBuilder: loadingBuilder, - errorBuilder: errorBuilder, - semanticLabel: semanticLabel, - excludeFromSemantics: excludeFromSemantics, - width: spec?.width, - height: spec?.height, - color: spec?.color, - opacity: opacity, - colorBlendMode: spec?.colorBlendMode ?? BlendMode.clear, - fit: spec?.fit, - alignment: spec?.alignment ?? Alignment.center, - repeat: spec?.repeat ?? ImageRepeat.noRepeat, - centerSlice: spec?.centerSlice, - matchTextDirection: matchTextDirection, - gaplessPlayback: gaplessPlayback, - isAntiAlias: isAntiAlias, - filterQuality: spec?.filterQuality ?? FilterQuality.low, + return RenderSpecModifiers( + orderOfModifiers: orderOfModifiers, + spec: spec ?? const ImageSpec(), + child: Image( + image: image, + frameBuilder: frameBuilder, + loadingBuilder: loadingBuilder, + errorBuilder: errorBuilder, + semanticLabel: semanticLabel, + excludeFromSemantics: excludeFromSemantics, + width: spec?.width, + height: spec?.height, + color: spec?.color, + opacity: opacity, + colorBlendMode: spec?.colorBlendMode ?? BlendMode.clear, + fit: spec?.fit, + alignment: spec?.alignment ?? Alignment.center, + repeat: spec?.repeat ?? ImageRepeat.noRepeat, + centerSlice: spec?.centerSlice, + matchTextDirection: matchTextDirection, + gaplessPlayback: gaplessPlayback, + isAntiAlias: isAntiAlias, + filterQuality: spec?.filterQuality ?? FilterQuality.low, + ), ); } } diff --git a/packages/mix/lib/src/specs/stack/stack_spec.dart b/packages/mix/lib/src/specs/stack/stack_spec.dart index 2925b1783..a30ccf43c 100644 --- a/packages/mix/lib/src/specs/stack/stack_spec.dart +++ b/packages/mix/lib/src/specs/stack/stack_spec.dart @@ -22,6 +22,7 @@ final class StackSpec extends Spec with _$StackSpec { this.textDirection, this.clipBehavior, super.animated, + super.modifiers, }); Widget call({List children = const []}) { diff --git a/packages/mix/lib/src/specs/stack/stack_spec.g.dart b/packages/mix/lib/src/specs/stack/stack_spec.g.dart index 68528321b..49acd8db8 100644 --- a/packages/mix/lib/src/specs/stack/stack_spec.g.dart +++ b/packages/mix/lib/src/specs/stack/stack_spec.g.dart @@ -40,6 +40,7 @@ base mixin _$StackSpec on Spec { TextDirection? textDirection, Clip? clipBehavior, AnimatedData? animated, + WidgetModifiersData? modifiers, }) { return StackSpec( alignment: alignment ?? _$this.alignment, @@ -47,6 +48,7 @@ base mixin _$StackSpec on Spec { textDirection: textDirection ?? _$this.textDirection, clipBehavior: clipBehavior ?? _$this.clipBehavior, animated: animated ?? _$this.animated, + modifiers: modifiers ?? _$this.modifiers, ); } @@ -63,7 +65,7 @@ base mixin _$StackSpec on Spec { /// /// - [AlignmentGeometry.lerp] for [alignment]. - /// For [fit] and [textDirection] and [clipBehavior] and [animated], the interpolation is performed using a step function. + /// For [fit] and [textDirection] and [clipBehavior] and [animated] and [modifiers], the interpolation is performed using a step function. /// If [t] is less than 0.5, the value from the current [StackSpec] is used. Otherwise, the value /// from the [other] [StackSpec] is used. /// @@ -79,6 +81,7 @@ base mixin _$StackSpec on Spec { textDirection: t < 0.5 ? _$this.textDirection : other.textDirection, clipBehavior: t < 0.5 ? _$this.clipBehavior : other.clipBehavior, animated: t < 0.5 ? _$this.animated : other.animated, + modifiers: t < 0.5 ? _$this.modifiers : other.modifiers, ); } @@ -93,6 +96,7 @@ base mixin _$StackSpec on Spec { _$this.textDirection, _$this.clipBehavior, _$this.animated, + _$this.modifiers, ]; StackSpec get _$this => this as StackSpec; @@ -117,6 +121,7 @@ final class StackSpecAttribute extends SpecAttribute { this.textDirection, this.clipBehavior, super.animated, + super.modifiers, }); /// Resolves to [StackSpec] using the provided [MixData]. @@ -135,6 +140,7 @@ final class StackSpecAttribute extends SpecAttribute { textDirection: textDirection, clipBehavior: clipBehavior, animated: animated?.resolve(mix) ?? mix.animation, + modifiers: modifiers?.resolve(mix), ); } @@ -156,6 +162,7 @@ final class StackSpecAttribute extends SpecAttribute { textDirection: other.textDirection ?? textDirection, clipBehavior: other.clipBehavior ?? clipBehavior, animated: animated?.merge(other.animated) ?? other.animated, + modifiers: modifiers?.merge(other.modifiers) ?? other.modifiers, ); } @@ -170,6 +177,7 @@ final class StackSpecAttribute extends SpecAttribute { textDirection, clipBehavior, animated, + modifiers, ]; } @@ -195,6 +203,9 @@ base class StackSpecUtility /// Utility for defining [StackSpecAttribute.animated] late final animated = AnimatedUtility((v) => only(animated: v)); + /// Utility for defining [StackSpecAttribute.modifiers] + late final wrap = SpecModifierUtility((v) => only(modifiers: v)); + StackSpecUtility(super.builder); static final self = StackSpecUtility((v) => v); @@ -207,6 +218,7 @@ base class StackSpecUtility TextDirection? textDirection, Clip? clipBehavior, AnimatedDataDto? animated, + WidgetModifiersDataDto? modifiers, }) { return builder(StackSpecAttribute( alignment: alignment, @@ -214,6 +226,7 @@ base class StackSpecUtility textDirection: textDirection, clipBehavior: clipBehavior, animated: animated, + modifiers: modifiers, )); } } diff --git a/packages/mix/lib/src/specs/stack/stack_widget.dart b/packages/mix/lib/src/specs/stack/stack_widget.dart index 035df7b47..55e9d36d0 100644 --- a/packages/mix/lib/src/specs/stack/stack_widget.dart +++ b/packages/mix/lib/src/specs/stack/stack_widget.dart @@ -1,6 +1,7 @@ import 'package:flutter/widgets.dart'; import '../../core/styled_widget.dart'; +import '../../modifiers/render_widget_modifier.dart'; import '../box/box_spec.dart'; import '../box/box_widget.dart'; import 'stack_spec.dart'; @@ -40,20 +41,30 @@ class StyledStack extends StyledWidget { } class StackSpecWidget extends StatelessWidget { - const StackSpecWidget({this.spec, super.key, this.children}); + const StackSpecWidget({ + this.spec, + this.children, + this.orderOfModifiers = const [], + super.key, + }); final List? children; final StackSpec? spec; + final List orderOfModifiers; @override Widget build(BuildContext context) { // The Stack widget is used here, applying the resolved styles from StackSpec. - return Stack( - alignment: spec?.alignment ?? _defaultStack.alignment, - textDirection: spec?.textDirection, - fit: spec?.fit ?? _defaultStack.fit, - clipBehavior: spec?.clipBehavior ?? _defaultStack.clipBehavior, - children: children ?? const [], + return RenderSpecModifiers( + orderOfModifiers: orderOfModifiers, + spec: spec ?? const StackSpec(), + child: Stack( + alignment: spec?.alignment ?? _defaultStack.alignment, + textDirection: spec?.textDirection, + fit: spec?.fit ?? _defaultStack.fit, + clipBehavior: spec?.clipBehavior ?? _defaultStack.clipBehavior, + children: children ?? const [], + ), ); } } diff --git a/packages/mix/lib/src/specs/text/text_spec.dart b/packages/mix/lib/src/specs/text/text_spec.dart index d23695621..a0b390fea 100644 --- a/packages/mix/lib/src/specs/text/text_spec.dart +++ b/packages/mix/lib/src/specs/text/text_spec.dart @@ -54,6 +54,7 @@ final class TextSpec extends Spec with _$TextSpec { this.softWrap, this.directive, super.animated, + super.modifiers, }); Widget call(String text, {String? semanticLabel, Locale? locale}) { diff --git a/packages/mix/lib/src/specs/text/text_spec.g.dart b/packages/mix/lib/src/specs/text/text_spec.g.dart index 2d5222d37..5bb1242cf 100644 --- a/packages/mix/lib/src/specs/text/text_spec.g.dart +++ b/packages/mix/lib/src/specs/text/text_spec.g.dart @@ -48,6 +48,7 @@ base mixin _$TextSpec on Spec { bool? softWrap, TextDirective? directive, AnimatedData? animated, + WidgetModifiersData? modifiers, }) { return TextSpec( overflow: overflow ?? _$this.overflow, @@ -63,6 +64,7 @@ base mixin _$TextSpec on Spec { softWrap: softWrap ?? _$this.softWrap, directive: directive ?? _$this.directive, animated: animated ?? _$this.animated, + modifiers: modifiers ?? _$this.modifiers, ); } @@ -81,7 +83,7 @@ base mixin _$TextSpec on Spec { /// - [MixHelpers.lerpDouble] for [textScaleFactor]. /// - [MixHelpers.lerpTextStyle] for [style]. - /// For [overflow] and [textAlign] and [textScaler] and [maxLines] and [textWidthBasis] and [textHeightBehavior] and [textDirection] and [softWrap] and [directive] and [animated], the interpolation is performed using a step function. + /// For [overflow] and [textAlign] and [textScaler] and [maxLines] and [textWidthBasis] and [textHeightBehavior] and [textDirection] and [softWrap] and [directive] and [animated] and [modifiers], the interpolation is performed using a step function. /// If [t] is less than 0.5, the value from the current [TextSpec] is used. Otherwise, the value /// from the [other] [TextSpec] is used. /// @@ -108,6 +110,7 @@ base mixin _$TextSpec on Spec { softWrap: t < 0.5 ? _$this.softWrap : other.softWrap, directive: t < 0.5 ? _$this.directive : other.directive, animated: t < 0.5 ? _$this.animated : other.animated, + modifiers: t < 0.5 ? _$this.modifiers : other.modifiers, ); } @@ -130,6 +133,7 @@ base mixin _$TextSpec on Spec { _$this.softWrap, _$this.directive, _$this.animated, + _$this.modifiers, ]; TextSpec get _$this => this as TextSpec; @@ -170,6 +174,7 @@ final class TextSpecAttribute extends SpecAttribute { this.softWrap, this.directive, super.animated, + super.modifiers, }); /// Resolves to [TextSpec] using the provided [MixData]. @@ -196,6 +201,7 @@ final class TextSpecAttribute extends SpecAttribute { softWrap: softWrap, directive: directive?.resolve(mix), animated: animated?.resolve(mix) ?? mix.animation, + modifiers: modifiers?.resolve(mix), ); } @@ -225,6 +231,7 @@ final class TextSpecAttribute extends SpecAttribute { softWrap: other.softWrap ?? softWrap, directive: directive?.merge(other.directive) ?? other.directive, animated: animated?.merge(other.animated) ?? other.animated, + modifiers: modifiers?.merge(other.modifiers) ?? other.modifiers, ); } @@ -247,6 +254,7 @@ final class TextSpecAttribute extends SpecAttribute { softWrap, directive, animated, + modifiers, ]; } @@ -313,6 +321,9 @@ base class TextSpecUtility /// Utility for defining [TextSpecAttribute.animated] late final animated = AnimatedUtility((v) => only(animated: v)); + /// Utility for defining [TextSpecAttribute.modifiers] + late final wrap = SpecModifierUtility((v) => only(modifiers: v)); + TextSpecUtility(super.builder); static final self = TextSpecUtility((v) => v); @@ -333,6 +344,7 @@ base class TextSpecUtility bool? softWrap, TextDirectiveDto? directive, AnimatedDataDto? animated, + WidgetModifiersDataDto? modifiers, }) { return builder(TextSpecAttribute( overflow: overflow, @@ -348,6 +360,7 @@ base class TextSpecUtility softWrap: softWrap, directive: directive, animated: animated, + modifiers: modifiers, )); } } diff --git a/packages/mix/lib/src/specs/text/text_widget.dart b/packages/mix/lib/src/specs/text/text_widget.dart index 64a54834b..e39755286 100644 --- a/packages/mix/lib/src/specs/text/text_widget.dart +++ b/packages/mix/lib/src/specs/text/text_widget.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../../core/styled_widget.dart'; +import '../../modifiers/render_widget_modifier.dart'; import 'text_spec.dart'; /// [StyledText] - A styled widget for displaying text with a mix of styles. @@ -77,6 +78,7 @@ class TextSpecWidget extends StatelessWidget { required this.spec, this.semanticsLabel, this.locale, + this.orderOfModifiers = const [], super.key, }); @@ -84,27 +86,31 @@ class TextSpecWidget extends StatelessWidget { final String? semanticsLabel; final Locale? locale; final TextSpec? spec; + final List orderOfModifiers; @override Widget build(BuildContext context) { // The Text widget is used here, applying the resolved styles and properties from TextSpec. - - return Text( - spec?.directive?.apply(text) ?? text, - style: spec?.style, - strutStyle: spec?.strutStyle, - textAlign: spec?.textAlign, - textDirection: spec?.textDirection, - locale: locale, - softWrap: spec?.softWrap, - overflow: spec?.overflow, - // ignore: deprecated_member_use, deprecated_member_use_from_same_package - textScaleFactor: spec?.textScaleFactor, - textScaler: spec?.textScaler, - maxLines: spec?.maxLines, - semanticsLabel: semanticsLabel, - textWidthBasis: spec?.textWidthBasis, - textHeightBehavior: spec?.textHeightBehavior, + return RenderSpecModifiers( + orderOfModifiers: const [], + spec: spec ?? const TextSpec(), + child: Text( + spec?.directive?.apply(text) ?? text, + style: spec?.style, + strutStyle: spec?.strutStyle, + textAlign: spec?.textAlign, + textDirection: spec?.textDirection, + locale: locale, + softWrap: spec?.softWrap, + overflow: spec?.overflow, + // ignore: deprecated_member_use, deprecated_member_use_from_same_package + textScaleFactor: spec?.textScaleFactor, + textScaler: spec?.textScaler, + maxLines: spec?.maxLines, + semanticsLabel: semanticsLabel, + textWidthBasis: spec?.textWidthBasis, + textHeightBehavior: spec?.textHeightBehavior, + ), ); } } diff --git a/packages/mix/test/deprecated/widget_modifier_widget_test.dart b/packages/mix/test/deprecated/widget_modifier_widget_test.dart new file mode 100644 index 000000000..bf279e6e5 --- /dev/null +++ b/packages/mix/test/deprecated/widget_modifier_widget_test.dart @@ -0,0 +1,413 @@ +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mix/mix.dart'; + +import '../helpers/testing_utils.dart'; + +void main() { + final style = Style( + $with.scale(2.0), + $with.opacity(0.5), + $with.visibility.on(), + $with.clipOval(), + $with.aspectRatio(2.0), + const CustomModifierAttribute(), + ); + + final mixData = MixData.create(MockBuildContext(), style); + + group('RenderModifiers', () { + testWidgets('Renders modifiers in the correct order', (tester) async { + await tester.pumpMaterialApp( + RenderModifiers( + mix: mixData, + orderOfModifiers: const [], + child: const Text('child'), + ), + ); + + expect(find.byType(RenderModifiers), findsOneWidget); + + expect( + find.descendant( + of: find.byType(RenderModifiers), + matching: find.byType(Visibility), + ), + findsOneWidget, + ); + + // Similarly, check for AspectRatio, Scale, Clip, and Opacity + // Ensure each widget is a descendant of the previous one in the correct order + final aspectRatioFinder = find.descendant( + of: find.byType(Visibility), + matching: find.byType(AspectRatio), + ); + expect(aspectRatioFinder, findsOneWidget); + + final scaleFinder = find.descendant( + of: aspectRatioFinder, + matching: find.byType(Transform), // Assuming Scale uses Transform + ); + expect(scaleFinder, findsOneWidget); + + final clipFinder = find.descendant( + of: scaleFinder, + matching: find.byType(ClipOval), // Assuming Clip uses ClipOval + ); + expect(clipFinder, findsOneWidget); + + final opacityFinder = find.descendant( + of: clipFinder, + matching: find.byType(Opacity), + ); + expect(opacityFinder, findsOneWidget); + + final customWidgetFinder = find.descendant( + of: opacityFinder, + matching: find.byType(Padding), + ); + + expect(customWidgetFinder, findsOneWidget); + + expect( + find.descendant( + of: customWidgetFinder, + matching: find.text('child'), + ), + findsOneWidget, + ); + }); + + testWidgets('Renders child when no modifiers are provided', (tester) async { + final mixData = MixData.create(MockBuildContext(), Style()); + + await tester.pumpMaterialApp( + RenderModifiers( + mix: mixData, + orderOfModifiers: const [], + child: const Text('child'), + ), + ); + + expect(find.text('child'), findsOneWidget); + expect(find.byType(RenderModifiers), findsOneWidget); + }); + + testWidgets('Renders child when orderOfModifiers is empty', (tester) async { + final mixData = MixData.create(MockBuildContext(), style); + + await tester.pumpMaterialApp( + RenderModifiers( + mix: mixData, + orderOfModifiers: const [], + child: const Text('child'), + ), + ); + + expect(find.text('child'), findsOneWidget); + expect(find.byType(RenderModifiers), findsOneWidget); + }); + + testWidgets( + 'Renders modifiers in the correct order with many overrides', + (tester) async { + await tester.pumpMaterialApp( + RenderModifiers( + mix: mixData, + orderOfModifiers: const [ + ClipOvalModifierAttribute, + AspectRatioModifierAttribute, + TransformModifierAttribute, + OpacityModifierAttribute, + VisibilityModifierAttribute, + ], + child: const Text('child'), + ), + ); + + expect(find.byType(RenderModifiers), findsOneWidget); + + expect( + find.descendant( + of: find.byType(RenderModifiers), + matching: find.byType(ClipOval), + ), + findsOneWidget, + ); + + expect( + find.descendant( + of: find.byType(ClipOval), + matching: find.byType(AspectRatio), + ), + findsOneWidget, + ); + + expect( + find.descendant( + of: find.byType(AspectRatio), + matching: find.byType(Transform), + ), + findsOneWidget, + ); + + expect( + find.descendant( + of: find.byType(Transform), + matching: find.byType(Opacity), + ), + findsOneWidget, + ); + + expect( + find.descendant( + of: find.byType(Opacity), + matching: find.byType(Padding), + ), + findsOneWidget, + ); + + expect( + find.descendant( + of: find.byType(Padding), + matching: find.text('child'), + ), + findsOneWidget, + ); + }, + ); + + // Allow for only a few overrides + testWidgets( + 'Renders modifiers in the correct order with a few overrides', + (tester) async { + await tester.pumpMaterialApp( + RenderModifiers( + mix: mixData, + orderOfModifiers: const [ + ClipOvalModifierAttribute, + AspectRatioModifierAttribute + ], + child: const Text('child'), + ), + ); + + expect(find.byType(RenderModifiers), findsOneWidget); + + expect( + find.descendant( + of: find.byType(RenderModifiers), + matching: find.byType(ClipOval), + ), + findsOneWidget, + ); + + expect( + find.descendant( + of: find.byType(ClipOval), + matching: find.byType(AspectRatio), + ), + findsOneWidget, + ); + + expect( + find.descendant( + of: find.byType(AspectRatio), + matching: find.byType(Visibility), + ), + findsOneWidget, + ); + + expect( + find.descendant( + of: find.byType(Visibility), + matching: find.byType(Transform), + ), + findsOneWidget, + ); + + expect( + find.descendant( + of: find.byType(Transform), + matching: find.byType(Opacity), + ), + findsOneWidget, + ); + + expect( + find.descendant( + of: find.byType(Opacity), + matching: find.byType(Padding), + ), + findsOneWidget, + ); + + expect( + find.descendant( + of: find.byType(Padding), + matching: find.text('child'), + ), + findsOneWidget, + ); + }, + ); + }); + + group('RenderAnimatedModifiers', () { + testWidgets('Renders animated modifiers', (tester) async { + final mixData = MixData.create(MockBuildContext(), style); + + await tester.pumpMaterialApp( + RenderAnimatedModifiers( + mix: mixData, + orderOfModifiers: const [], + duration: const Duration(milliseconds: 300), + child: const Text('child'), + ), + ); + + expect(find.text('child'), findsOneWidget); + expect(find.byType(RenderAnimatedModifiers), findsOneWidget); + + // Trigger animation and pump frames + await tester.pump(const Duration(milliseconds: 150)); + await tester.pump(const Duration(milliseconds: 150)); + }); + }); + + group('Modifiers attributes', () { + testWidgets( + 'should be applied to the first one. The children wont inherit even though the second one is set to inherit', + (tester) async { + const key = Key('box'); + + await tester.pumpWidget( + Box( + style: Style( + $with.scale(2.0), + $with.opacity(0.5), + $with.visibility.on(), + $with.clipOval(), + $with.aspectRatio(2.0), + ), + child: Box( + key: key, + inherit: true, + child: Builder(builder: (context) { + final inheritedMix = Mix.maybeOf(context)!; + + expect(inheritedMix.attributes.length, 0); + + return const SizedBox(); + }), + ), + ), + ); + + expect( + find.descendant( + of: find.byKey(key), + matching: find.byType(Transform), + ), + findsNothing, + ); + + expect( + find.descendant( + of: find.byKey(key), + matching: find.byType(Opacity), + ), + findsNothing, + ); + + expect( + find.descendant( + of: find.byKey(key), + matching: find.byType(RotatedBox), + ), + findsNothing, + ); + + expect( + find.descendant( + of: find.byKey(key), + matching: find.byType(Visibility), + ), + findsNothing, + ); + + expect( + find.descendant( + of: find.byKey(key), + matching: find.byType(AspectRatio), + ), + findsNothing, + ); + }, + ); + + testWidgets( + 'If there are no modifier attributes in Style, RenderModifierAttributes shouldnt exist in the widget tree', + (tester) async { + const key = Key('box'); + + await tester.pumpWidget( + Box( + style: Style($box.color.red(), $box.height(100), $box.width(100)), + key: key, + ), + ); + + expect( + find.ancestor( + of: find.byKey(key), + matching: find.byType(RenderModifiers), + ), + findsNothing, + ); + }, + ); + }); + + group('resolveModifierSpecs', () { + test('Returns empty set when no modifiers are provided', () { + final result = resolveModifierSpecs(const [], EmptyMixData); + expect(result, isEmpty); + }); + + test('Returns resolved modifier specs in the correct order', () { + final style = Style( + $with.scale(2.0), + $with.opacity(0.5), + $with.visibility.on(), + $with.clipOval(), + $with.aspectRatio(2.0), + ); + + final mix = style.of(MockBuildContext()); + final result = resolveModifierSpecs( + [ + ClipOvalModifierAttribute, + AspectRatioModifierAttribute, + TransformModifierAttribute, + OpacityModifierAttribute, + VisibilityModifierAttribute, + ], + mix, + ); + + expect(result, { + const VisibilityModifierSpec(true), + const OpacityModifierSpec(0.5), + TransformModifierSpec( + transform: Matrix4.diagonal3Values(2.0, 2.0, 1.0), + alignment: Alignment.center, + ), + const AspectRatioModifierSpec(2.0), + const ClipOvalModifierSpec(), + }); + }); + }); +} diff --git a/packages/mix/test/deprecated/widget_modifiers_util_test.dart b/packages/mix/test/deprecated/widget_modifiers_util_test.dart new file mode 100644 index 000000000..d1dd975f6 --- /dev/null +++ b/packages/mix/test/deprecated/widget_modifiers_util_test.dart @@ -0,0 +1,443 @@ +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mix/mix.dart'; + +import '../empty_widget.dart'; +import '../helpers/testing_utils.dart'; + +void main() { + group('Modifiers: ', () { + const aspectRatio = AspectRatioUtility(UtilityTestAttribute.new); + const flexible = FlexibleModifierUtility(UtilityTestAttribute.new); + const visibility = VisibilityUtility(UtilityTestAttribute.new); + const transform = TransformUtility(UtilityTestAttribute.new); + + const opacity = OpacityUtility(UtilityTestAttribute.new); + const rotate = RotatedBoxWidgetUtility(UtilityTestAttribute.new); + const clipPath = ClipPathUtility(UtilityTestAttribute.new); + const clipRRect = ClipRRectUtility(UtilityTestAttribute.new); + const clipOval = ClipOvalUtility(UtilityTestAttribute.new); + const clipRect = ClipRectUtility(UtilityTestAttribute.new); + const clipTriangle = ClipTriangleUtility(UtilityTestAttribute.new); + final sizedBox = SizedBoxModifierUtility(UtilityTestAttribute.new); + const fractionallySizedBox = + FractionallySizedBoxModifierUtility(UtilityTestAttribute.new); + const intrinsicHeight = + IntrinsicHeightWidgetUtility(UtilityTestAttribute.new); + const intrinsicWidth = + IntrinsicWidthWidgetUtility(UtilityTestAttribute.new); + const align = AlignWidgetUtility(UtilityTestAttribute.new); + + test('aspectRatio creates AspectRatioModifier correctly', () { + final aspectRatioModifier = aspectRatio(2.0); + + expect(aspectRatioModifier.value.aspectRatio, 2.0); + }); + + test('expanded creates FlexibleModifierUtility correctly', () { + const flexible = FlexibleModifierUtility(UtilityTestAttribute.new); + final flexibleModifier = flexible.expanded(); + + expect(flexibleModifier.value.fit, FlexFit.tight); + }); + + test('default flexible creates FlexibleModifierUtility correctly', () { + final flexibleModifier = flexible(); + final widget = flexibleModifier.value + .resolve(EmptyMixData) + .build(const Empty()) as Flexible; + + expect(flexibleModifier.value.fit, null); + expect(widget, isA()); + expect(widget.fit, FlexFit.loose); + expect(widget.flex, 1); + }); + + test('opacity creates OpacityModifier correctly', () { + final opacityModifier = opacity(0.5); + + expect(opacityModifier.value.opacity, 0.5); + }); + + test('rotate creates RotateModifier correctly', () { + final rotateModifier = rotate(2); + + expect(rotateModifier.value.quarterTurns, 2); + }); + + test('rotate90 creates RotateModifier correctly', () { + final rotateModifier = rotate.d90(); + + expect(rotateModifier.value.quarterTurns, 1); + }); + + test('rotate180 creates RotateModifier correctly', () { + final rotateModifier = rotate.d180(); + + expect(rotateModifier.value.quarterTurns, 2); + }); + + test('rotate270 creates RotateModifier correctly', () { + final rotateModifier = rotate.d270(); + + expect(rotateModifier.value.quarterTurns, 3); + }); + + test('clipRRect creates ClipRRectModifier correctly', () { + final clipRRectModifier = + clipRRect(borderRadius: BorderRadius.circular(10.0)); + + final modifier = clipRRectModifier.value; + + expect(modifier.borderRadius, BorderRadius.circular(10.0)); + }); + + test('clipOval creates ClipOvalModifier correctly', () { + final clipOvalModifier = clipOval(); + + expect( + clipOvalModifier.value.resolve(EmptyMixData).build(const Empty()), + isA(), + ); + }); + test('clipPath creates ClipPathModifier correctly', () { + final clipPathModifier = clipPath(); + + expect( + clipPathModifier.value.resolve(EmptyMixData).build(const Empty()), + isA(), + ); + }); + + // clipTriangle + test('clipTriangle creates ClipTriangleModifier correctly', () { + final clipTriangleModifier = clipTriangle(); + + expect( + clipTriangleModifier.value.resolve(EmptyMixData).build(const Empty()), + isA(), + ); + }); + + test('intrinsicHeight creates IntrinsicHeightModifier correctly', () { + final widget = intrinsicHeight() + .value + .resolve(EmptyMixData) + .build(const Empty()) as IntrinsicHeight; + + expect(widget, isA()); + }); + + test('intrinsicWidth creates IntrinsicWidthModifier correctly', () { + final widget = intrinsicWidth() + .value + .resolve(EmptyMixData) + .build(const Empty()) as IntrinsicWidth; + + expect(widget, isA()); + }); + + test('clipRect creates ClipRectModifier correctly', () { + final clipRectModifier = clipRect(); + + expect( + clipRectModifier.value.resolve(EmptyMixData).build(const Empty()), + isA(), + ); + }); + + test('visibility creates VisibilityModifier correctly', () { + final visibilityModifier = visibility.on(); + expect(visibilityModifier.value.visible, true); + }); + + test('transform creates TransformModifier correctly', () { + final transformModifier = transform(Matrix4.identity()); + + expect(transformModifier.value.transform, Matrix4.identity()); + }); + test('sizedBox creates SizedBoxModifier correctly', () { + final sizedBoxModifier = sizedBox(height: 100, width: 100); + + final widget = sizedBoxModifier.value + .resolve(EmptyMixData) + .build(const Empty()) as SizedBox; + + expect(widget.width, 100); + expect(widget.height, 100); + }); + + test( + 'fractionallySizedBox creates FractionallySizedBoxModifier correctly', + () { + final fractionallySizedBoxModifier = fractionallySizedBox( + heightFactor: 0.5, + widthFactor: 0.5, + ); + + final widget = fractionallySizedBoxModifier.value + .resolve(EmptyMixData) + .build(const Empty()) as FractionallySizedBox; + + expect(widget.widthFactor, 0.5); + expect(widget.heightFactor, 0.5); + }, + ); + + // align + test('align creates AlignModifier correctly', () { + final alignModifier = align(alignment: Alignment.center); + + final widget = alignModifier.value + .resolve(EmptyMixData) + .build(const Empty()) as Align; + + expect(widget.alignment, Alignment.center); + }); + }); + group('Applying Modifiers in Style', () { + testWidgets( + 'Applying an intrinsicHeight must add an IntrinsicHeight in widget tree', + (tester) async { + await tester.pumpWidget( + _TestableRenderModifier( + Style( + $with.intrinsicHeight(), + ), + ), + ); + + _expectOneWidgetOfType(); + }, + ); + + testWidgets( + 'Applying a scale must add a ScaleTransition in widget tree', + (tester) async { + await tester.pumpWidget( + _TestableRenderModifier( + Style( + $with.scale(2.0), + ), + ), + ); + + _expectOneWidgetOfType(); + }, + ); + + testWidgets( + 'Applying an opacity must add an Opacity in widget tree', + (tester) async { + await tester.pumpWidget( + _TestableRenderModifier( + Style( + $with.opacity(0.5), + ), + ), + ); + + _expectOneWidgetOfType(); + }, + ); + + testWidgets( + 'Applying a clipPath must add a ClipPath in widget tree', + (tester) async { + await tester.pumpWidget( + _TestableRenderModifier( + Style( + $with.clipPath(), + ), + ), + ); + + _expectOneWidgetOfType(); + }, + ); + + testWidgets( + 'Applying a clipRRect must add a ClipRRect in widget tree', + (tester) async { + await tester.pumpWidget( + _TestableRenderModifier( + Style( + $with.clipRRect(), + ), + ), + ); + + _expectOneWidgetOfType(); + }, + ); + + testWidgets( + 'Applying a clipOval must add a ClipOval in widget tree', + (tester) async { + await tester.pumpWidget( + _TestableRenderModifier( + Style( + $with.clipOval(), + ), + ), + ); + + _expectOneWidgetOfType(); + }, + ); + + testWidgets( + 'Applying a clipRect must add a ClipRect in widget tree', + (tester) async { + await tester.pumpWidget( + _TestableRenderModifier( + Style( + $with.clipRect(), + ), + ), + ); + + _expectOneWidgetOfType(); + }, + ); + + testWidgets( + 'Applying a visibility must add a Visibility widget in the widget tree', + (tester) async { + await tester.pumpWidget( + _TestableRenderModifier( + Style( + $with.visibility.off(), + ), + ), + ); + + _expectOneWidgetOfType(); + }, + ); + + testWidgets( + 'Applying an aspectRatio must add an AspectRatio widget in the widget tree', + (tester) async { + await tester.pumpWidget( + _TestableRenderModifier( + Style( + $with.aspectRatio(2), + ), + ), + ); + + _expectOneWidgetOfType(); + }, + ); + + testWidgets( + 'Applying a flexible must add a Flexible widget in the widget tree', + (tester) async { + await tester.pumpWidget( + Column( + children: [ + _TestableRenderModifier( + Style( + $with.flexible(), + ), + ), + ], + ), + ); + + _expectOneWidgetOfType(); + }, + ); + + testWidgets( + 'Applying a transform must add a Transform widget in the widget tree', + (tester) async { + await tester.pumpWidget( + _TestableRenderModifier( + Style( + $with.transform(Matrix4.identity()), + ), + ), + ); + + _expectOneWidgetOfType(); + }, + ); + + testWidgets( + 'Applying an align must add an Align widget in the widget tree', + (tester) async { + await tester.pumpWidget( + _TestableRenderModifier( + Style( + $with.align(), + ), + ), + ); + + _expectOneWidgetOfType(); + }, + ); + + testWidgets( + 'Applying a fractionallySizedBox must add a FractionallySizedBox widget in the widget tree', + (tester) async { + await tester.pumpWidget( + _TestableRenderModifier( + Style( + $with.fractionallySizedBox(), + ), + ), + ); + + _expectOneWidgetOfType(); + }, + ); + + testWidgets( + 'Applying a sizedBox must add a SizedBox widget in the widget tree', + (tester) async { + await tester.pumpWidget( + _TestableRenderModifier( + Style( + $with.sizedBox(), + ), + ), + ); + + _expectOneWidgetOfType(); + }, + ); + }); +} + +void _expectOneWidgetOfType() { + expect( + find.descendant( + of: find.byType(_TestableRenderModifier), + matching: find.byType(T), + ), + findsOneWidget, + ); +} + +class _TestableRenderModifier extends StatelessWidget { + const _TestableRenderModifier(this.style); + + final Style style; + + @override + Widget build(BuildContext context) { + return RenderModifiers( + orderOfModifiers: const [], + mix: MixData.create( + context, + style, + ), + child: Container(), + ); + } +} diff --git a/packages/mix/test/modifiers/render_widget_modifier_test.dart b/packages/mix/test/modifiers/render_widget_modifier_test.dart new file mode 100644 index 000000000..05508d59e --- /dev/null +++ b/packages/mix/test/modifiers/render_widget_modifier_test.dart @@ -0,0 +1,61 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mix/mix.dart'; + +void main() { + group('orderSpecs', () { + test('should order modifiers based on provided order', () { + final orderOfModifiers = [ + ClipOvalModifierSpec, + OpacityModifierSpec, + AlignModifierSpec, + TransformModifierSpec, + ]; + final modifiers = [ + const OpacityModifierSpec(1), + TransformModifierSpec(transform: Matrix4.rotationX(3)), + const AlignModifierSpec(alignment: Alignment.center), + const ClipOvalModifierSpec(), + ]; + + final result = orderModifiers(orderOfModifiers, modifiers); + + expect(result.map((e) => e.type).toList(), orderOfModifiers); + }); + + test('should include default order specs', () { + final modifiers = [ + TransformModifierSpec(transform: Matrix4.rotationX(3)), + const OpacityModifierSpec(1), + const AlignModifierSpec(alignment: Alignment.center), + ]; + + final result = orderModifiers([], modifiers); + + expect(result.map((e) => e.type), + [AlignModifierSpec, TransformModifierSpec, OpacityModifierSpec]); + }); + + test('should handle empty modifiers', () { + final orderOfModifiers = [TransformModifierSpec]; + final modifiers = []; + + final result = orderModifiers(orderOfModifiers, modifiers); + + expect(result, isEmpty); + }); + + test('should handle duplicate modifiers', () { + final orderOfModifiers = [TransformModifierSpec]; + final modifiers = [ + const OpacityModifierSpec(1), + const OpacityModifierSpec(0.4), + ]; + + final result = orderModifiers(orderOfModifiers, modifiers); + + expect(result.length, 1); + expect(result.first.type, OpacityModifierSpec); + }); + }); +} diff --git a/packages/mix/test/src/attributes/modifiers/modifiers_data_test.dart b/packages/mix/test/src/attributes/modifiers/modifiers_data_test.dart new file mode 100644 index 000000000..c23f62db1 --- /dev/null +++ b/packages/mix/test/src/attributes/modifiers/modifiers_data_test.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mix/mix.dart'; + +import '../../../helpers/testing_utils.dart'; + +void main() { + group('ModifiersDataDto', () { + test('should create an instance with default values', () { + const dto = WidgetModifiersDataDto([]); + expect(dto.value, isEmpty); + }); + + test('should create an instance with provided values', () { + const modifier1 = TestModifierSpecAttribute(); + const modifier2 = TestModifierSpecAttribute(); + // ignore: equal_elements_in_set + const dto = WidgetModifiersDataDto([modifier1, modifier2]); + expect(dto.value, contains(modifier1)); + }); + + test('should merge with another instance', () { + const dto1 = WidgetModifiersDataDto([TestModifierSpecAttribute()]); + const dto2 = WidgetModifiersDataDto([TestModifierSpecAttribute(2)]); + final merged = dto1.merge(dto2); + expect(merged.value, hasLength(1)); + expect(merged.value.first, const TestModifierSpecAttribute(2)); + }); + + test('should resolve to a ModifiersData instance', () { + const modifier = TestModifierSpecAttribute(); + const dto = WidgetModifiersDataDto([modifier]); + final modifiersData = dto.resolve(EmptyMixData); + expect(modifiersData.value.length, 1); + expect(modifiersData.value.first, const TestModifierSpec()); + }); + + // test equality + test('should be equal to another instance', () { + const dto1 = WidgetModifiersDataDto([TestModifierSpecAttribute()]); + const dto2 = WidgetModifiersDataDto([TestModifierSpecAttribute()]); + expect(dto1, equals(dto2)); + }); + + test('should not be equal to another instance', () { + const dto1 = WidgetModifiersDataDto([TestModifierSpecAttribute()]); + const dto2 = WidgetModifiersDataDto([TestModifierSpecAttribute(2)]); + expect(dto1, isNot(equals(dto2))); + }); + }); + + group('ModifiersData', () { + test('should create an instance with provided values', () { + const modifier1 = TestModifierSpec(); + const modifier2 = TestModifierSpec(); + // ignore: equal_elements_in_set + const modifiersData = WidgetModifiersData([modifier1, modifier2]); + expect(modifiersData.value, contains(modifier1)); + }); + + // equality + test('should be equal to another instance', () { + const modifiersData1 = WidgetModifiersData([TestModifierSpec()]); + const modifiersData2 = WidgetModifiersData([TestModifierSpec()]); + expect(modifiersData1, equals(modifiersData2)); + }); + + test('should not be equal to another instance', () { + const modifiersData1 = WidgetModifiersData([TestModifierSpec()]); + const modifiersData2 = WidgetModifiersData([]); + expect(modifiersData1, isNot(equals(modifiersData2))); + }); + }); +} + +final class TestModifierSpec extends WidgetModifierSpec { + const TestModifierSpec([this.value = 1]); + final int value; + @override + Widget build(Widget child) { + throw UnimplementedError(); + } + + @override + TestModifierSpec lerp(TestModifierSpec other, double t) { + return this; + } + + @override + TestModifierSpec copyWith() { + throw UnimplementedError(); + } + + @override + List get props => [value]; +} + +final class TestModifierSpecAttribute extends WidgetModifierAttribute< + TestModifierSpecAttribute, TestModifierSpec> { + const TestModifierSpecAttribute([this.value = 1]); + + final int value; + + @override + List get props => [value]; + + @override + SpecAttribute merge( + covariant SpecAttribute? other) { + return other ?? this; + } + + @override + TestModifierSpec resolve(MixData mix) { + return const TestModifierSpec(); + } +} diff --git a/packages/mix/test/src/factory/mix_provider_data_test.dart b/packages/mix/test/src/factory/mix_provider_data_test.dart index de338c601..d74eb058a 100644 --- a/packages/mix/test/src/factory/mix_provider_data_test.dart +++ b/packages/mix/test/src/factory/mix_provider_data_test.dart @@ -1,5 +1,6 @@ // ignore_for_file: non_constant_identifier_names +import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mix/mix.dart'; @@ -107,6 +108,51 @@ void main() { ); }); + test( + 'modifiersOf returns a list of resolved WidgetModifierSpec of the specified type', + () { + final style = Style( + $with.scale(2.0), + $with.opacity(0.5), + $with.visibility.on(), + $with.clipOval(), + $with.aspectRatio(2.0), + ); + + final mixData = MixData.create(MockBuildContext(), style); + + final modifiers = mixData.modifiersOf(); + + expect(modifiers.length, 5); + + final scaleModifiers = mixData.modifiersOf(); + expect(scaleModifiers, [ + TransformModifierSpec( + transform: Matrix4.diagonal3Values(2.0, 2.0, 1.0), + alignment: Alignment.center, + ), + ]); + + final opacityModifiers = mixData.modifiersOf(); + expect(opacityModifiers, [const OpacityModifierSpec(0.5)]); + + final visibilityModifiers = mixData.modifiersOf(); + expect(visibilityModifiers, [const VisibilityModifierSpec(true)]); + + final clipModifiers = mixData.modifiersOf(); + expect(clipModifiers, [const ClipOvalModifierSpec()]); + + final aspectRatioModifiers = + mixData.modifiersOf(); + expect(aspectRatioModifiers, [const AspectRatioModifierSpec(2.0)]); + + final customModifiers = mixData.modifiersOf(); + expect(customModifiers, isEmpty); + + final nonExistentModifiers = mixData.modifiersOf(); + expect(nonExistentModifiers, isEmpty); + }); + group('applyContextToVisualAttributes', () { test( 'must return the same Style that was inputted when there is not ContextVariant in the Style (simple variant)', diff --git a/packages/mix/test/src/modifiers/widget_modifier_widget_test.dart b/packages/mix/test/src/modifiers/widget_modifier_widget_test.dart index c89d30df7..b06b17eaf 100644 --- a/packages/mix/test/src/modifiers/widget_modifier_widget_test.dart +++ b/packages/mix/test/src/modifiers/widget_modifier_widget_test.dart @@ -1,3 +1,5 @@ +// ignore_for_file: deprecated_member_use_from_same_package + import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mix/mix.dart'; @@ -20,7 +22,7 @@ void main() { testWidgets('Renders modifiers in the correct order', (tester) async { await tester.pumpMaterialApp( RenderModifiers( - mix: mixData, + modifiers: mixData.modifiers, orderOfModifiers: const [], child: const Text('child'), ), @@ -83,7 +85,7 @@ void main() { await tester.pumpMaterialApp( RenderModifiers( - mix: mixData, + modifiers: mixData.modifiers, orderOfModifiers: const [], child: const Text('child'), ), @@ -98,7 +100,7 @@ void main() { await tester.pumpMaterialApp( RenderModifiers( - mix: mixData, + modifiers: mixData.modifiers, orderOfModifiers: const [], child: const Text('child'), ), @@ -113,7 +115,7 @@ void main() { (tester) async { await tester.pumpMaterialApp( RenderModifiers( - mix: mixData, + modifiers: mixData.modifiers, orderOfModifiers: const [ ClipOvalModifierAttribute, AspectRatioModifierAttribute, @@ -121,6 +123,7 @@ void main() { OpacityModifierAttribute, VisibilityModifierAttribute, ], + mix: mixData, child: const Text('child'), ), ); @@ -184,6 +187,7 @@ void main() { await tester.pumpMaterialApp( RenderModifiers( mix: mixData, + modifiers: mixData.modifiers, orderOfModifiers: const [ ClipOvalModifierAttribute, AspectRatioModifierAttribute @@ -259,7 +263,7 @@ void main() { await tester.pumpMaterialApp( RenderAnimatedModifiers( - mix: mixData, + modifiers: mixData.modifiers, orderOfModifiers: const [], duration: const Duration(milliseconds: 300), child: const Text('child'), @@ -273,6 +277,84 @@ void main() { await tester.pump(const Duration(milliseconds: 150)); await tester.pump(const Duration(milliseconds: 150)); }); + + testWidgets('Renders animated modifiers', (tester) async { + await tester.pumpWidget( + _TestableAnimatedModifiers( + (isActive) => RenderAnimatedModifiers( + duration: const Duration(milliseconds: 200), + orderOfModifiers: const [], + modifiers: [ + OpacityModifierSpec(isActive ? 1.0 : 0.0), + SizedBoxModifierSpec( + height: isActive ? 50.0 : 0.0, + width: isActive ? 50.0 : 0.0, + ), + ], + child: Container(), + ), + ), + ); + + final gestureFinder = find.byType(GestureDetector); + expect(gestureFinder, findsOneWidget); + + final finder = find.byType(Opacity); + expect(finder, findsOneWidget); + + final finderSizedBox = find.byType(SizedBox); + expect(finderSizedBox, findsOneWidget); + + await tester.tap(gestureFinder); + await tester.pump(); + + await tester.pump(const Duration(milliseconds: 100)); + expect(tester.widget(finder).opacity, 0.5); + expect(tester.widget(finderSizedBox).height, 25); + expect(tester.widget(finderSizedBox).width, 25); + + await tester.pump(const Duration(milliseconds: 100)); + expect(tester.widget(finder).opacity, 0.0); + expect(tester.widget(finderSizedBox).height, 0); + expect(tester.widget(finderSizedBox).width, 0); + }); + + testWidgets('Transition correctly when there is conditional specs', + (tester) async { + gestureFinder() => find.byType(GestureDetector); + + await tester.pumpWidget( + _TestableAnimatedModifiers( + (isActive) => RenderAnimatedModifiers( + duration: const Duration(milliseconds: 200), + orderOfModifiers: const [], + modifiers: [ + const OpacityModifierSpec(0.0), + if (!isActive) + TransformModifierSpec(transform: Matrix4.rotationZ(0.5)), + ], + child: Container(), + ), + ), + ); + + expect(find.byType(Opacity), findsOneWidget); + expect(find.byType(Transform), findsNothing); + + await tester.tap(gestureFinder()); + await tester.pump(); + + expect(find.byType(Opacity), findsOneWidget); + expect(find.byType(Transform), findsOneWidget); + await tester.pumpAndSettle(const Duration(milliseconds: 200)); + + await tester.tap(gestureFinder()); + await tester.pump(); + + expect(find.byType(Opacity), findsOneWidget); + expect(find.byType(Transform), findsNothing); + await tester.pumpAndSettle(const Duration(milliseconds: 200)); + }); }); group('Modifiers attributes', () { @@ -409,3 +491,34 @@ void main() { }); }); } + +class _TestableAnimatedModifiers extends StatefulWidget { + const _TestableAnimatedModifiers( + this.child, + ); + + final Widget Function(bool) child; + + @override + State<_TestableAnimatedModifiers> createState() => + _TestableAnimatedModifiersState(); +} + +class _TestableAnimatedModifiersState + extends State<_TestableAnimatedModifiers> { + bool _isActive = true; + + void _handleToggle() { + setState(() { + _isActive = !_isActive; + }); + } + + @override + Widget build(BuildContext context) { + return Pressable( + onPress: _handleToggle, + child: widget.child(_isActive), + ); + } +} diff --git a/packages/mix/test/src/modifiers/widget_modifiers_util_test.dart b/packages/mix/test/src/modifiers/widget_modifiers_util_test.dart index 1cea84330..2c87c29c4 100644 --- a/packages/mix/test/src/modifiers/widget_modifiers_util_test.dart +++ b/packages/mix/test/src/modifiers/widget_modifiers_util_test.dart @@ -429,12 +429,10 @@ class _TestableRenderModifier extends StatelessWidget { @override Widget build(BuildContext context) { + final mix = MixData.create(MockBuildContext(), style); return RenderModifiers( + modifiers: mix.modifiers, orderOfModifiers: const [], - mix: MixData.create( - context, - style, - ), child: Container(), ); } diff --git a/packages/mix/test/src/specs/box/box_attribute_test.dart b/packages/mix/test/src/specs/box/box_attribute_test.dart index ffeb74d33..9300e36e8 100644 --- a/packages/mix/test/src/specs/box/box_attribute_test.dart +++ b/packages/mix/test/src/specs/box/box_attribute_test.dart @@ -22,6 +22,10 @@ void main() { clipBehavior: Clip.antiAlias, width: 100, height: 100, + modifiers: const WidgetModifiersDataDto([ + OpacityModifierAttribute(0.5), + SizedBoxModifierAttribute(height: 10, width: 10), + ]), ); expect(containerSpecAttribute.alignment, Alignment.center); @@ -47,26 +51,35 @@ void main() { ); expect(containerSpecAttribute.transform, Matrix4.identity()); expect(containerSpecAttribute.width, 100); + expect( + containerSpecAttribute.modifiers, + const WidgetModifiersDataDto([ + OpacityModifierAttribute(0.5), + SizedBoxModifierAttribute(height: 10, width: 10), + ])); }); // resolve() test('resolve() returns correct instance', () { final containerSpecAttribute = BoxSpecAttribute( - alignment: Alignment.center, - padding: SpacingDto.only(top: 20, bottom: 20, left: 20, right: 20), - margin: SpacingDto.only( - top: 10, - bottom: 10, - left: 10, - right: 10, - ), - constraints: const BoxConstraintsDto(maxHeight: 100), - decoration: const BoxDecorationDto(color: ColorDto(Colors.blue)), - transform: Matrix4.identity(), - clipBehavior: Clip.antiAlias, - width: 100, - height: 100, - ); + alignment: Alignment.center, + padding: SpacingDto.only(top: 20, bottom: 20, left: 20, right: 20), + margin: SpacingDto.only( + top: 10, + bottom: 10, + left: 10, + right: 10, + ), + constraints: const BoxConstraintsDto(maxHeight: 100), + decoration: const BoxDecorationDto(color: ColorDto(Colors.blue)), + transform: Matrix4.identity(), + clipBehavior: Clip.antiAlias, + width: 100, + height: 100, + modifiers: const WidgetModifiersDataDto([ + OpacityModifierAttribute(0.5), + SizedBoxModifierAttribute(height: 10, width: 10), + ])); final containerSpec = containerSpecAttribute.resolve(EmptyMixData); @@ -90,26 +103,33 @@ void main() { ); expect(containerSpec.transform, Matrix4.identity()); expect(containerSpec.width, 100); + expect(containerSpec.modifiers!.value, [ + const OpacityModifierSpec(0.5), + const SizedBoxModifierSpec(height: 10, width: 10), + ]); }); // merge() test('merge() returns correct instance', () { final containerSpecAttribute = BoxSpecAttribute( - alignment: Alignment.center, - padding: SpacingDto.only(top: 20, bottom: 20, left: 20, right: 20), - margin: SpacingDto.only( - top: 10, - bottom: 10, - left: 10, - right: 10, - ), - constraints: const BoxConstraintsDto(maxHeight: 100), - decoration: const BoxDecorationDto(color: ColorDto(Colors.blue)), - transform: Matrix4.identity(), - clipBehavior: Clip.antiAlias, - width: 100, - height: 100, - ); + alignment: Alignment.center, + padding: SpacingDto.only(top: 20, bottom: 20, left: 20, right: 20), + margin: SpacingDto.only( + top: 10, + bottom: 10, + left: 10, + right: 10, + ), + constraints: const BoxConstraintsDto(maxHeight: 100), + decoration: const BoxDecorationDto(color: ColorDto(Colors.blue)), + transform: Matrix4.identity(), + clipBehavior: Clip.antiAlias, + width: 100, + height: 100, + modifiers: const WidgetModifiersDataDto([ + OpacityModifierAttribute(0.5), + SizedBoxModifierAttribute(height: 10, width: 10), + ])); final mergedBoxSpecAttribute = containerSpecAttribute.merge( BoxSpecAttribute( @@ -127,6 +147,9 @@ void main() { clipBehavior: Clip.antiAliasWithSaveLayer, width: 200, height: 200, + modifiers: const WidgetModifiersDataDto([ + SizedBoxModifierAttribute(width: 20), + ]), ), ); @@ -153,26 +176,35 @@ void main() { ); expect(mergedBoxSpecAttribute.transform, Matrix4.identity()); expect(mergedBoxSpecAttribute.width, 200); + expect( + mergedBoxSpecAttribute.modifiers, + const WidgetModifiersDataDto([ + OpacityModifierAttribute(0.5), + SizedBoxModifierAttribute(height: 10, width: 20), + ])); }); // equality test('equality', () { final containerSpecAttribute = BoxSpecAttribute( - alignment: Alignment.center, - padding: SpacingDto.only(top: 20, bottom: 20, left: 20, right: 20), - margin: SpacingDto.only( - top: 10, - bottom: 10, - left: 10, - right: 10, - ), - constraints: const BoxConstraintsDto(maxHeight: 100), - decoration: const BoxDecorationDto(color: ColorDto(Colors.blue)), - transform: Matrix4.identity(), - clipBehavior: Clip.antiAlias, - width: 100, - height: 100, - ); + alignment: Alignment.center, + padding: SpacingDto.only(top: 20, bottom: 20, left: 20, right: 20), + margin: SpacingDto.only( + top: 10, + bottom: 10, + left: 10, + right: 10, + ), + constraints: const BoxConstraintsDto(maxHeight: 100), + decoration: const BoxDecorationDto(color: ColorDto(Colors.blue)), + transform: Matrix4.identity(), + clipBehavior: Clip.antiAlias, + width: 100, + height: 100, + modifiers: const WidgetModifiersDataDto([ + OpacityModifierAttribute(0.5), + SizedBoxModifierAttribute(height: 10, width: 10), + ])); expect( containerSpecAttribute, @@ -192,6 +224,12 @@ void main() { clipBehavior: Clip.antiAlias, width: 100, height: 100, + modifiers: const WidgetModifiersDataDto( + [ + OpacityModifierAttribute(0.5), + SizedBoxModifierAttribute(height: 10, width: 10), + ], + ), ), ), ); @@ -240,6 +278,12 @@ void main() { clipBehavior: Clip.antiAliasWithSaveLayer, width: 200, height: 200, + modifiers: const WidgetModifiersDataDto( + [ + OpacityModifierAttribute(0.4), + SizedBoxModifierAttribute(height: 20, width: 10), + ], + ), ), ), ), diff --git a/packages/mix/test/src/specs/box/box_spec_test.dart b/packages/mix/test/src/specs/box/box_spec_test.dart index 46e2267c5..6680ef1b9 100644 --- a/packages/mix/test/src/specs/box/box_spec_test.dart +++ b/packages/mix/test/src/specs/box/box_spec_test.dart @@ -21,6 +21,10 @@ void main() { decoration: const BoxDecorationDto(color: ColorDto(Colors.blue)), transform: Matrix4.translationValues(10.0, 10.0, 0.0), clipBehavior: Clip.antiAlias, + modifiers: const WidgetModifiersDataDto([ + OpacityModifierAttribute(1), + SizedBoxModifierAttribute(height: 10, width: 10), + ]), width: 300, height: 200, ), @@ -39,6 +43,10 @@ void main() { expect(spec.decoration, const BoxDecoration(color: Colors.blue)); expect(spec.transform, Matrix4.translationValues(10.0, 10.0, 0.0)); + expect(spec.modifiers!.value, [ + const OpacityModifierSpec(1), + const SizedBoxModifierSpec(height: 10, width: 10), + ]); expect(spec.clipBehavior, Clip.antiAlias); }); @@ -55,9 +63,19 @@ void main() { transformAlignment: Alignment.center, width: 300, height: 200, + modifiers: const WidgetModifiersData([ + OpacityModifierSpec(0.5), + SizedBoxModifierSpec(height: 10, width: 10), + ]), ); - final copiedSpec = spec.copyWith(width: 250.0, height: 150.0); + final copiedSpec = spec.copyWith( + width: 250.0, + height: 150.0, + modifiers: const WidgetModifiersData([ + OpacityModifierSpec(1), + ]), + ); expect(copiedSpec.alignment, Alignment.center); expect(copiedSpec.padding, const EdgeInsets.all(16.0)); @@ -75,6 +93,13 @@ void main() { expect(copiedSpec.transform, Matrix4.translationValues(10.0, 10.0, 0.0)); expect(copiedSpec.clipBehavior, Clip.antiAlias); expect(copiedSpec.width, 250.0); + + expect( + copiedSpec.modifiers!.value, + const WidgetModifiersData( + [OpacityModifierSpec(1)], + ).value, + ); expect(copiedSpec.height, 150.0); }); @@ -179,6 +204,10 @@ void main() { clipBehavior: Clip.none, width: 300, height: 200, + modifiers: const WidgetModifiersData([ + OpacityModifierSpec(0.5), + SizedBoxModifierSpec(height: 10, width: 10), + ]), ); final spec2 = BoxSpec( @@ -193,6 +222,10 @@ void main() { clipBehavior: Clip.none, width: 300, height: 200, + modifiers: const WidgetModifiersData([ + OpacityModifierSpec(0.5), + SizedBoxModifierSpec(height: 10, width: 10), + ]), ); expect(spec1, spec2); @@ -217,6 +250,10 @@ void main() { clipBehavior: Clip.antiAlias, width: 100, height: 100, + modifiers: const WidgetModifiersDataDto([ + OpacityModifierAttribute(0.5), + SizedBoxModifierAttribute(height: 10, width: 10), + ]), ); final mergedBoxSpecAttribute = containerSpecAttribute.merge( @@ -237,6 +274,9 @@ void main() { clipBehavior: Clip.antiAliasWithSaveLayer, width: 200, height: 200, + modifiers: const WidgetModifiersDataDto([ + SizedBoxModifierAttribute(width: 100), + ]), ), ); @@ -266,6 +306,13 @@ void main() { ); expect(mergedBoxSpecAttribute.transform, Matrix4.identity()); expect(mergedBoxSpecAttribute.width, 200); + expect( + mergedBoxSpecAttribute.modifiers!.value, + [ + const OpacityModifierAttribute(0.5), + const SizedBoxModifierAttribute(height: 10, width: 100), + ], + ); }); }); } diff --git a/packages/mix/test/src/specs/image/image_spec_test.dart b/packages/mix/test/src/specs/image/image_spec_test.dart index 4a98034a5..887b94873 100644 --- a/packages/mix/test/src/specs/image/image_spec_test.dart +++ b/packages/mix/test/src/specs/image/image_spec_test.dart @@ -114,8 +114,9 @@ void main() { expect(getValueOf(spec.centerSlice), Rect.zero); expect(getValueOf(spec.filterQuality), FilterQuality.low); expect(getValueOf(spec.colorBlendMode), BlendMode.srcOver); + expect(getValueOf(spec.modifiers), null); expect(getValueOf(spec.animated), const AnimatedData.withDefaults()); - expect(spec.props.length, 10); + expect(spec.props.length, 11); }); }); } diff --git a/packages/mix/test/src/specs/stack/stack_attribute_test.dart b/packages/mix/test/src/specs/stack/stack_attribute_test.dart index f9dee178d..227b87371 100644 --- a/packages/mix/test/src/specs/stack/stack_attribute_test.dart +++ b/packages/mix/test/src/specs/stack/stack_attribute_test.dart @@ -70,7 +70,7 @@ void main() { ); final props = attribute.props; - expect(props.length, 5); + expect(props.length, 6); expect(props[0], Alignment.center); expect(props[1], StackFit.expand); expect(props[2], TextDirection.ltr); diff --git a/packages/mix/test/src/theme/tokens/text_style_token_test.dart b/packages/mix/test/src/theme/tokens/text_style_token_test.dart index 091061fac..ff1d3556f 100644 --- a/packages/mix/test/src/theme/tokens/text_style_token_test.dart +++ b/packages/mix/test/src/theme/tokens/text_style_token_test.dart @@ -1,5 +1,3 @@ -import 'dart:ui'; - import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mix/mix.dart'; diff --git a/packages/mix/test/src/widgets/box/box_test.dart b/packages/mix/test/src/widgets/box/box_test.dart index 2369d8b02..6369a5953 100644 --- a/packages/mix/test/src/widgets/box/box_test.dart +++ b/packages/mix/test/src/widgets/box/box_test.dart @@ -151,7 +151,7 @@ void main() { of: find.byKey(key), matching: find.byType(RenderModifiers), ), - findsOneWidget, + findsNWidgets(2), ); expect( diff --git a/packages/mix/test/src/widgets/pressable/pressable_widget_test.dart b/packages/mix/test/src/widgets/pressable/pressable_widget_test.dart index fe66ea3a9..5015eb66b 100644 --- a/packages/mix/test/src/widgets/pressable/pressable_widget_test.dart +++ b/packages/mix/test/src/widgets/pressable/pressable_widget_test.dart @@ -208,12 +208,41 @@ void main() { expect(wasPressed, isTrue); }); + testWidgets('animates correctly on hover', (WidgetTester tester) async { + await tester.pumpMaterialApp( + PressableBox( + unpressDelay: const Duration(milliseconds: 200), + style: Style( + $with.opacity(1.0), + $on.hover( + $with.opacity(0.0), + ), + ).animate( + duration: const Duration(milliseconds: 200), + ), + child: const Box(), + ), + ); + + final finder = find.byType(Opacity); + expect(finder, findsOneWidget); + + Opacity opacityWidget = tester.widget(finder); + expect(opacityWidget.opacity, 1.0); + + await tester.hover(finder); + await tester.pump(const Duration(milliseconds: 100)); + + opacityWidget = tester.widget(finder); + expect(opacityWidget.opacity, 0.5); + }); + testWidgets(r'must change to attributes in $on.hover variant when hovered', (WidgetTester tester) async { await pumpTestCase( tester: tester, condition: $on.hover, - action: () => tester.hover(find.byType(PressableBox)), + action: () => tester.hoverAndSettle(find.byType(PressableBox)), ); }); @@ -246,7 +275,7 @@ void main() { await pumpTestCase( tester: tester, condition: ($on.longPress | $on.hover), - action: () => tester.hover(find.byType(PressableBox)), + action: () => tester.hoverAndSettle(find.byType(PressableBox)), ); }); @@ -311,7 +340,7 @@ void main() { duration: const Duration(milliseconds: 250), condition: ($on.hover | $on.press), action: () async { - await tester.hover(find.byType(PressableBox)); + await tester.hoverAndSettle(find.byType(PressableBox)); }, ); }); @@ -363,7 +392,7 @@ void main() { tester: tester, condition: ($on.longPress | $on.press), action: () async { - await tester.hover(find.byType(PressableBox)); + await tester.hoverAndSettle(find.byType(PressableBox)); }, finalExpectedOpacity: 0.5, ); @@ -377,7 +406,7 @@ void main() { duration: const Duration(milliseconds: 250), condition: ($on.hover & $on.press), action: () async { - await tester.hover(find.byType(PressableBox)); + await tester.hoverAndSettle(find.byType(PressableBox)); await tester.pump(); await tester.tap(find.byType(PressableBox)); await tester.pump(); @@ -392,7 +421,7 @@ void main() { tester: tester, condition: ($on.hover & $on.longPress), action: () async { - await tester.hover(find.byType(PressableBox)); + await tester.hoverAndSettle(find.byType(PressableBox)); await tester.pump(); // Custom way to long press @@ -414,6 +443,11 @@ void main() { } extension WidgetTesterExt on WidgetTester { + Future hoverAndSettle(Finder finder) async { + await hover(finder); + await pumpAndSettle(); + } + Future hover(Finder finder) async { FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; @@ -424,8 +458,6 @@ extension WidgetTesterExt on WidgetTester { await pump(); await gesture.moveTo(getCenter(finder)); - await pumpAndSettle(); - addTearDown(gesture.removePointer); } } diff --git a/packages/mix/test/src/widgets/styled_icon_test.dart b/packages/mix/test/src/widgets/styled_icon_test.dart index be4f93bea..a5a52724c 100644 --- a/packages/mix/test/src/widgets/styled_icon_test.dart +++ b/packages/mix/test/src/widgets/styled_icon_test.dart @@ -51,7 +51,7 @@ void main() { of: find.byKey(key), matching: find.byType(RenderModifiers), ), - findsOneWidget, + findsNWidgets(2), ); expect( diff --git a/packages/mix_generator/lib/src/helpers/type_ref_repository.dart b/packages/mix_generator/lib/src/helpers/type_ref_repository.dart index b796dbd2d..5a70adff8 100644 --- a/packages/mix_generator/lib/src/helpers/type_ref_repository.dart +++ b/packages/mix_generator/lib/src/helpers/type_ref_repository.dart @@ -16,6 +16,7 @@ class TypeRefRepository { static Map _utilityOverrides = { 'EdgeInsetsGeometry': 'SpacingUtility', 'AnimatedData': 'AnimatedUtility', + 'WidgetModifiersData': 'SpecModifierUtility', }; static final _dtoMap = { @@ -23,6 +24,7 @@ class TypeRefRepository { 'BoxConstraints': 'BoxConstraintsDto', 'Decoration': 'DecorationDto', 'Color': 'ColorDto', + 'WidgetModifiersData': 'WidgetModifiersDataDto', 'AnimatedData': 'AnimatedDataDto', 'TextStyle': 'TextStyleDto', 'ShapeBorder': 'ShapeBorderDto',