diff --git a/lib/mix.dart b/lib/mix.dart index 85135772a..6262f2030 100644 --- a/lib/mix.dart +++ b/lib/mix.dart @@ -52,7 +52,6 @@ export 'src/decorators/fractionally_sized_box_widget_decorator.dart'; export 'src/decorators/intrinsic_widget_decorator.dart'; export 'src/decorators/opacity_widget_decorator.dart'; export 'src/decorators/rotated_box_widget_decorator.dart'; -export 'src/decorators/scale_widget_decorator.dart'; export 'src/decorators/sized_box_widget_decorator.dart'; export 'src/decorators/transform_widget_decorator.dart'; export 'src/decorators/visibility_widget_decorator.dart'; @@ -107,6 +106,6 @@ export 'src/utils/custom_focusable_action_detector.dart'; export 'src/utils/helper_util.dart'; export 'src/utils/style_recipe.dart'; export 'src/variants/variant.dart'; -export 'src/widgets/pressable/pressable_data.notifier.dart'; +export 'src/widgets/pressable/pressable_state.dart'; export 'src/widgets/pressable/pressable_util.dart'; export 'src/widgets/pressable/pressable_widget.dart'; diff --git a/lib/src/attributes/variant_attribute.dart b/lib/src/attributes/variant_attribute.dart index 1821aef9d..2e19828ec 100644 --- a/lib/src/attributes/variant_attribute.dart +++ b/lib/src/attributes/variant_attribute.dart @@ -5,38 +5,42 @@ import '../factory/style_mix.dart'; import '../variants/variant.dart'; @immutable -class VariantAttribute extends Attribute - with Mergeable> { - final T variant; +abstract class StyleVariantAttribute extends Attribute + with Mergeable> { + final V variant; final Style _style; - - const VariantAttribute(this.variant, Style style) : _style = style; + const StyleVariantAttribute(this.variant, Style style) : _style = style; Style get value => _style; - bool matches(Iterable otherVariants) => - otherVariants.contains(variant); + bool matches(Iterable otherVariants) => + variant.matches(otherVariants); @override - VariantAttribute merge(VariantAttribute other) { - if (other.variant != variant) throw throwArgumentError(other); - - return VariantAttribute(variant, _style.merge(other._style)); - } + get props => [variant, _style]; @override Object get type => ObjectKey(variant); +} + +@immutable +class VariantAttribute extends StyleVariantAttribute { + const VariantAttribute(super.variant, super.style); @override - get props => [variant, value]; + VariantAttribute merge(VariantAttribute other) { + if (other.variant != variant) throw throwArgumentError(other); + + return VariantAttribute(variant, _style.merge(other._style)); + } } -mixin WhenVariant on VariantAttribute { +mixin WhenVariant on StyleVariantAttribute { bool when(BuildContext context); } @immutable -class ContextVariantAttribute extends VariantAttribute +class ContextVariantAttribute extends StyleVariantAttribute with WhenVariant { const ContextVariantAttribute(super.variant, super.style); @@ -51,7 +55,7 @@ class ContextVariantAttribute extends VariantAttribute } } -ArgumentError throwArgumentError(T other) { +ArgumentError throwArgumentError(T other) { throw ArgumentError.value( other.runtimeType, 'other', @@ -60,26 +64,27 @@ ArgumentError throwArgumentError(T other) { } @immutable -class MultiVariantAttribute extends VariantAttribute +class MultiVariantAttribute extends StyleVariantAttribute with WhenVariant { const MultiVariantAttribute(super.variant, super.style); // Remove all variants in given a list - VariantAttribute remove(Iterable variantsToRemove) { + StyleVariantAttribute remove(Iterable variantsToRemove) { final variant = this.variant.remove(variantsToRemove); if (variant is MultiVariant) { return MultiVariantAttribute(variant, _style); } else if (variant is ContextVariant) { return ContextVariantAttribute(variant, _style); + } else if (variant is Variant) { + return VariantAttribute(variant, _style); } - - return VariantAttribute(variant, _style); + throw ArgumentError.value( + variant, + 'variant', + 'Variant must be a Variant, ContextVariant, or MultiVariant', + ); } - @override - bool matches(Iterable otherVariants) => - variant.matches(otherVariants); - @override bool when(BuildContext context) => variant.when(context); diff --git a/lib/src/core/attribute.dart b/lib/src/core/attribute.dart index 5cfa595d5..0cfc3a1e4 100644 --- a/lib/src/core/attribute.dart +++ b/lib/src/core/attribute.dart @@ -110,3 +110,14 @@ abstract class Spec> with Comparable { /// Linearly interpolate with another [Spec] object. T lerp(covariant T? other, double t); } + +@immutable +abstract class StyleAttributeBuilder> + extends StyleAttribute { + const StyleAttributeBuilder(); + + Attribute? builder(BuildContext context); + + @override + Type get type => Self; +} diff --git a/lib/src/core/styled_widget.dart b/lib/src/core/styled_widget.dart index 76cc9db23..67f26aebf 100644 --- a/lib/src/core/styled_widget.dart +++ b/lib/src/core/styled_widget.dart @@ -57,12 +57,12 @@ abstract class StyledWidget extends StatelessWidget { } Widget applyDecorators(MixData mix, Widget child) { - return mix.animation.isAnimated + return mix.isAnimated ? RenderAnimatedDecorators( mix: mix, orderOfDecorators: orderOfDecorators, - duration: mix.animation.duration, - curve: mix.animation.curve, + duration: mix.animation!.duration, + curve: mix.animation!.curve, child: child, ) : RenderDecorators( diff --git a/lib/src/decorators/scale_widget_decorator.dart b/lib/src/decorators/scale_widget_decorator.dart deleted file mode 100644 index 83bbfa88e..000000000 --- a/lib/src/decorators/scale_widget_decorator.dart +++ /dev/null @@ -1,58 +0,0 @@ -// ignore_for_file: prefer-named-boolean-parameters - -import 'dart:ui'; - -import 'package:flutter/material.dart'; - -import '../attributes/scalars/scalar_util.dart'; -import '../core/attribute.dart'; -import '../core/decorator.dart'; -import '../factory/mix_provider_data.dart'; - -class ScaleDecoratorSpec extends DecoratorSpec { - final double scale; - const ScaleDecoratorSpec(this.scale); - - @override - ScaleDecoratorSpec lerp(ScaleDecoratorSpec? other, double t) { - return ScaleDecoratorSpec(lerpDouble(scale, other?.scale, t) ?? scale); - } - - @override - ScaleDecoratorSpec copyWith({double? scale}) { - return ScaleDecoratorSpec(scale ?? this.scale); - } - - @override - List get props => [scale]; - - @override - Widget build(Widget child) { - return Transform.scale(scale: scale, child: child); - } -} - -class ScaleDecoratorAttribute - extends DecoratorAttribute { - final double scale; - const ScaleDecoratorAttribute(this.scale); - - @override - ScaleDecoratorAttribute merge(ScaleDecoratorAttribute? other) { - return ScaleDecoratorAttribute(other?.scale ?? scale); - } - - @override - ScaleDecoratorSpec resolve(MixData mix) { - return ScaleDecoratorSpec(scale); - } - - @override - List get props => [scale]; -} - -class ScaleUtility - extends MixUtility { - const ScaleUtility(super.builder); - T call(double value) => builder(ScaleDecoratorAttribute(value)); -} diff --git a/lib/src/decorators/transform_widget_decorator.dart b/lib/src/decorators/transform_widget_decorator.dart index 2359e6631..79b7d87fb 100644 --- a/lib/src/decorators/transform_widget_decorator.dart +++ b/lib/src/decorators/transform_widget_decorator.dart @@ -9,7 +9,9 @@ import '../factory/mix_provider_data.dart'; class TransformDecoratorSpec extends DecoratorSpec { final Matrix4? transform; - const TransformDecoratorSpec({this.transform}); + final Alignment? alignment; + + const TransformDecoratorSpec({this.transform, this.alignment}); @override TransformDecoratorSpec lerp(TransformDecoratorSpec? other, double t) { @@ -19,43 +21,87 @@ class TransformDecoratorSpec extends DecoratorSpec { } @override - TransformDecoratorSpec copyWith({Matrix4? transform}) { - return TransformDecoratorSpec(transform: transform ?? this.transform); + TransformDecoratorSpec copyWith({ + Matrix4? transform, + Alignment? alignment, + }) { + return TransformDecoratorSpec( + transform: transform ?? this.transform, + alignment: alignment ?? this.alignment, + ); } @override - List get props => [transform]; + List get props => [transform, alignment]; @override Widget build(Widget child) { - return Transform(transform: transform ?? Matrix4.identity(), child: child); + return Transform( + transform: transform ?? Matrix4.identity(), + alignment: alignment ?? Alignment.center, + child: child, + ); } } class TransformDecoratorAttribute extends DecoratorAttribute< TransformDecoratorAttribute, TransformDecoratorSpec> { final Matrix4? transform; - const TransformDecoratorAttribute({this.transform}); + final Alignment? alignment; + + const TransformDecoratorAttribute({this.transform, this.alignment}); @override TransformDecoratorAttribute merge(TransformDecoratorAttribute? other) { return TransformDecoratorAttribute( - transform: other?.transform ?? transform, + transform: + other?.transform?.multiplied(transform ?? Matrix4.identity()) ?? + transform, + alignment: other?.alignment ?? alignment, ); } @override TransformDecoratorSpec resolve(MixData mix) { - return TransformDecoratorSpec(transform: transform); + return TransformDecoratorSpec( + transform: transform, + alignment: alignment, + ); } @override - List get props => [transform]; + List get props => [transform, alignment]; } class TransformUtility extends MixUtility { const TransformUtility(super.builder); + T call(Matrix4 value) => builder(TransformDecoratorAttribute(transform: value)); + + T scale(double value) => builder( + TransformDecoratorAttribute( + transform: Matrix4.diagonal3Values(value, value, 1.0), + alignment: Alignment.center, + ), + ); + + T rotate(double value) => builder( + TransformDecoratorAttribute( + transform: Matrix4.rotationZ(value), + alignment: Alignment.center, + ), + ); + + T flip(bool x, bool y) => builder( + TransformDecoratorAttribute( + transform: Matrix4.diagonal3Values( + x ? -1.0 : 1.0, + y ? -1.0 : 1.0, + 1.0, + ), + alignment: Alignment.center, + ), + ); } diff --git a/lib/src/decorators/widget_decorator_widget.dart b/lib/src/decorators/widget_decorator_widget.dart index fea3d0e3c..a0a8040d3 100644 --- a/lib/src/decorators/widget_decorator_widget.dart +++ b/lib/src/decorators/widget_decorator_widget.dart @@ -11,7 +11,6 @@ import 'clip_widget_decorator.dart'; import 'fractionally_sized_box_widget_decorator.dart'; import 'intrinsic_widget_decorator.dart'; import 'opacity_widget_decorator.dart'; -import 'scale_widget_decorator.dart'; import 'sized_box_widget_decorator.dart'; import 'transform_widget_decorator.dart'; import 'visibility_widget_decorator.dart'; @@ -51,11 +50,6 @@ const _defaultOrder = [ // which is critical for preserving the visual integrity of images and other aspect-sensitive content. AspectRatioDecoratorAttribute, - // 8. ScaleDecorator: Scales the widget according to a given scale factor. This decorator is applied after - // the aspect ratio is considered to ensure the widget scales uniformly, affecting its overall size and maintaining - // the aspect ratio integrity. - ScaleDecoratorAttribute, - // 9. TransformDecorator: 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. diff --git a/lib/src/decorators/widget_decorators_util.dart b/lib/src/decorators/widget_decorators_util.dart index 0d6df9ae8..9bc658e21 100644 --- a/lib/src/decorators/widget_decorators_util.dart +++ b/lib/src/decorators/widget_decorators_util.dart @@ -7,7 +7,6 @@ import 'fractionally_sized_box_widget_decorator.dart'; import 'intrinsic_widget_decorator.dart'; import 'opacity_widget_decorator.dart'; import 'rotated_box_widget_decorator.dart'; -import 'scale_widget_decorator.dart'; import 'sized_box_widget_decorator.dart'; import 'transform_widget_decorator.dart'; import 'visibility_widget_decorator.dart'; @@ -16,7 +15,8 @@ T selfBuilder(T decorator) => decorator; const intrinsicWidth = IntrinsicWidthWidgetUtility(selfBuilder); const intrinsicHeight = IntrinsicHeightWidgetUtility(selfBuilder); -const scale = ScaleUtility(selfBuilder); +final scale = transform.scale; +final flip = transform.flip; const opacity = OpacityUtility(selfBuilder); const rotate = RotatedBoxWidgetUtility(selfBuilder); diff --git a/lib/src/deprecations.dart b/lib/src/deprecations.dart index 4e45e365d..0c038b7a1 100644 --- a/lib/src/deprecations.dart +++ b/lib/src/deprecations.dart @@ -351,10 +351,10 @@ final large = onLarge; final hover = onHover; @Deprecated('Use onFocused instead') -final focus = onFocused; +const focus = onFocused; @Deprecated('Use onFocused instead') -final onFocus = onFocused; +const onFocus = onFocused; @Deprecated('Use onPortrait instead') final portrait = onPortrait; @@ -548,13 +548,7 @@ final textBaseline = text.style.textBaseline; @Deprecated('Use borderRadius.circular instead') final squared = borderRadius.zero; -// linearGradient -@Deprecated('Use box.decoration.gradient.linear instead') -final linearGradient = box.decoration.gradient.linear; -// radialGradient -@Deprecated('Use box.decoration.gradient.radial instead') -final radialGradient = box.decoration.gradient.radial; // flexDirection @Deprecated('Use flexDirection instead') diff --git a/lib/src/factory/mix_provider_data.dart b/lib/src/factory/mix_provider_data.dart index 305c73aa3..dc44a913c 100644 --- a/lib/src/factory/mix_provider_data.dart +++ b/lib/src/factory/mix_provider_data.dart @@ -15,7 +15,7 @@ import 'style_mix.dart'; /// decorators and token resolvers. @immutable class MixData with Comparable { - final AnimatedData animation; + final AnimatedData? animation; // Instance variables for widget attributes, widget decorators and token resolver. final AttributeMap _attributes; @@ -40,9 +40,7 @@ class MixData with Comparable { return MixData._( resolver: resolver, attributes: AttributeMap(attributeList), - animation: style is AnimatedStyle - ? style.animatedData - : const AnimatedData.notAnimated(), + animation: style is AnimatedStyle ? style.animatedData : null, ); } @@ -54,7 +52,7 @@ class MixData with Comparable { } /// Alias for animation.isAnimated - bool get isAnimated => animation.isAnimated; + bool get isAnimated => animation?.isAnimated ?? false; /// Getter for [MixTokenResolver]. /// @@ -107,7 +105,7 @@ class MixData with Comparable { return MixData._( resolver: _tokenResolver, attributes: _attributes.merge(other._attributes), - animation: other.animation, + animation: other.animation ?? animation, ); } @@ -121,21 +119,24 @@ List applyContextToVisualAttributes( BuildContext context, Style mix, ) { - Style style = Style.create(mix.styles.values); + final builtAttributes = _applyStyleBuilder(context, mix.styles.values); + + Style style = Style.create(builtAttributes); final contextVariants = mix.variants.whereType(); final multiVariants = mix.variants.whereType(); // Once there are no more context variants to apply, return the mix if (contextVariants.isEmpty) { - return mix.styles.values.toList(); + // TODO: Clean this up + return style.styles.values.toList(); } List contextVariantTypes = []; List gestureVariantTypes = []; for (ContextVariantAttribute attr in contextVariants) { - if (attr.variant is PressableDataVariant) { + if (attr.variant is PressableStateVariant) { gestureVariantTypes.add(attr); } else { contextVariantTypes.add(attr); @@ -144,7 +145,7 @@ List applyContextToVisualAttributes( for (MultiVariantAttribute attr in multiVariants) { if (attr.variant.variants - .any((variant) => variant is PressableDataVariant)) { + .any((variant) => variant is PressableStateVariant)) { gestureVariantTypes.add(attr); } else { contextVariantTypes.add(attr); @@ -191,11 +192,20 @@ class AnimatedData with Comparable { required this.curve, }); - const AnimatedData.notAnimated() - : isAnimated = false, - duration = Duration.zero, - curve = Curves.linear; - @override List get props => [isAnimated, duration, curve]; } + +Iterable _applyStyleBuilder( + BuildContext context, + List attributes, +) { + return attributes.map((attr) { + if (attr is StyleAttributeBuilder) { + return attr.builder(context); + } + + return attr; + // ignore: avoid-inferrable-type-arguments + }).whereType(); +} diff --git a/lib/src/factory/style_mix.dart b/lib/src/factory/style_mix.dart index cb07fd26d..c4a07c135 100644 --- a/lib/src/factory/style_mix.dart +++ b/lib/src/factory/style_mix.dart @@ -23,7 +23,7 @@ class AnimatedStyle extends Style { const AnimatedStyle._({ required AttributeMap styles, - required AttributeMap variants, + required AttributeMap variants, required this.animatedData, }) : super._(styles: styles, variants: variants); @@ -42,6 +42,22 @@ class AnimatedStyle extends Style { ), ); } + + /// Returns a new `Style` with the provided [styles] and [variants] merged with this mix's values. + /// + /// If [styles] or [variants] is null, the corresponding attribute map of this mix is used. + @override + AnimatedStyle copyWith({ + AttributeMap? styles, + AttributeMap? variants, + AnimatedData? animatedData, + }) { + return AnimatedStyle._( + styles: styles ?? this.styles, + variants: variants ?? this.variants, + animatedData: animatedData ?? this.animatedData, + ); + } } /// A utility class for managing a collection of styling attributes and variants. @@ -61,7 +77,7 @@ class Style with Comparable { final AttributeMap styles; /// The variant attributes contained in this mix. - final AttributeMap variants; + final AttributeMap variants; static final stack = SpreadFunctionParams(_styleType()); static final text = SpreadFunctionParams(_styleType()); @@ -134,13 +150,13 @@ class Style with Comparable { /// final style = Style.create([attribute1, attribute2]); /// ``` factory Style.create(Iterable attributes) { - final applyVariants = []; + final applyVariants = []; final styleList = []; for (final attribute in attributes) { if (attribute is StyleAttribute) { styleList.add(attribute); - } else if (attribute is VariantAttribute) { + } else if (attribute is StyleVariantAttribute) { applyVariants.add(attribute); } else if (attribute is NestedStyleAttribute) { applyVariants.addAll(attribute.value.variants.values); @@ -217,7 +233,7 @@ class Style with Comparable { /// /// Note: /// The attributes from the selected variant (`attr4` and `attr5`) are not applied to the `Style` instance until the `applyVariant` method is called. - SpreadFunctionParams get applyVariant => + SpreadFunctionParams get applyVariant => SpreadFunctionParams(applyVariants); /// Allows to create a new `Style` by using this mix as a base and adding additional attributes. @@ -233,7 +249,7 @@ class Style with Comparable { /// Returns a `AnimatedStyle` from this `Style` with the provided [duration] and [curve]. AnimatedStyle toAnimated({ - required Duration duration, + Duration duration = const Duration(milliseconds: 150), Curve curve = Curves.linear, }) { return AnimatedStyle._( @@ -257,7 +273,7 @@ class Style with Comparable { /// If [styles] or [variants] is null, the corresponding attribute map of this mix is used. Style copyWith({ AttributeMap? styles, - AttributeMap? variants, + AttributeMap? variants, }) { return Style._( styles: styles ?? this.styles, @@ -306,15 +322,15 @@ class Style with Comparable { /// /// Note: /// The attributes from the selected variants (`attr3`, `attr4`, and `attr5`) are not applied to the `Style` instance until the `applyVariants` method is called. - Style applyVariants(Iterable selectedVariants) { + Style applyVariants(Iterable selectedVariants) { /// Return the original Style if no variants were selected if (selectedVariants.isEmpty) { return this; } /// Initializing two empty lists that store the matched and remaining `Variants`, respectively. - final matchedVariants = []; - final remainingVariants = []; + final matchedVariants = []; + final remainingVariants = []; /// Loop over all VariantAttributes in variants only once instead of a nested loop, /// checking if each one matches with the selected variants. @@ -373,10 +389,10 @@ class Style with Comparable { /// The attributes `attr1` and `attr2` from the initial `Style` are ignored, and only the attributes within the specified variants are picked and applied to the new `Style`. @visibleForTesting Style pickVariants( - List pickedVariants, { + List pickedVariants, { bool isRecursive = false, }) { - final matchedVariants = []; + final matchedVariants = []; // Return an empty Style if the list of picked variants is empty @@ -417,10 +433,10 @@ class Style with Comparable { /// - The `variantSwitcher` method is called on the `Style` instance with a map of conditions and variants. /// - The conditions `useHighContratst` and `useLargeFont` are hypothetical boolean values representing user preferences. /// - If a condition is true, the corresponding `Variant` is selected and applied to the `Style` instance, creating an `updatedStyle` instance with the selected variants. - Style variantSwitcher(List> cases) { - List variantsToApply = []; + Style variantSwitcher(List> cases) { + List variantsToApply = []; - for (SwitchCondition conditionCase in cases) { + for (SwitchCondition conditionCase in cases) { if (conditionCase.condition) { variantsToApply.add(conditionCase.value); } diff --git a/lib/src/helpers/lerp_helpers.dart b/lib/src/helpers/lerp_helpers.dart index b7ce0c768..546f0c57d 100644 --- a/lib/src/helpers/lerp_helpers.dart +++ b/lib/src/helpers/lerp_helpers.dart @@ -45,6 +45,37 @@ P? lerpSnap

(P? from, P? to, double t) { return t < 0.5 ? from : to; } +TextStyle? lerpTextStyle(TextStyle? from, TextStyle? other, double t) { + if (from == null && other == null) return null; + if (from == null) return other; + if (other == null) return from; + + return TextStyle( + color: Color.lerp(from.color, other.color, t), + backgroundColor: Color.lerp(from.backgroundColor, other.backgroundColor, t), + fontSize: lerpDouble(from.fontSize, other.fontSize, t), + fontWeight: FontWeight.lerp(from.fontWeight, other.fontWeight, t), + fontStyle: t < 0.5 ? from.fontStyle : other.fontStyle, + letterSpacing: lerpDouble(from.letterSpacing, other.letterSpacing, t), + wordSpacing: lerpDouble(from.wordSpacing, other.wordSpacing, t), + textBaseline: t < 0.5 ? from.textBaseline : other.textBaseline, + height: lerpDouble(from.height, other.height, t), + locale: t < 0.5 ? from.locale : other.locale, + foreground: lerpSnap(from.foreground, other.foreground, t), + background: lerpSnap(from.background, other.background, t), + shadows: Shadow.lerpList(from.shadows, other.shadows, t), + fontFeatures: t < 0.5 ? from.fontFeatures : other.fontFeatures, + decoration: t < 0.5 ? from.decoration : other.decoration, + decorationColor: Color.lerp(from.decorationColor, other.decorationColor, t), + decorationStyle: t < 0.5 ? from.decorationStyle : other.decorationStyle, + decorationThickness: + lerpDouble(from.decorationThickness, other.decorationThickness, t), + fontFamily: t < 0.5 ? from.fontFamily : other.fontFamily, + fontFamilyFallback: + t < 0.5 ? from.fontFamilyFallback : other.fontFamilyFallback, + ); +} + /// Linearly interpolates between two [StrutStyle] objects. /// /// The [lerpStrutStyle] function takes two [StrutStyle] objects, [a] and [b], diff --git a/lib/src/specs/container/box_attribute.dart b/lib/src/specs/container/box_attribute.dart index 09153f7e7..e3af5083e 100644 --- a/lib/src/specs/container/box_attribute.dart +++ b/lib/src/specs/container/box_attribute.dart @@ -9,6 +9,7 @@ import 'box_spec.dart'; class BoxSpecAttribute extends SpecAttribute { final AlignmentGeometry? alignment; + final AlignmentGeometry? transformAlignment; final SpacingDto? padding; final SpacingDto? margin; final BoxConstraintsDto? constraints; @@ -27,6 +28,7 @@ class BoxSpecAttribute extends SpecAttribute { this.decoration, this.foregroundDecoration, this.transform, + this.transformAlignment, this.clipBehavior, this.width, this.height, @@ -42,6 +44,7 @@ class BoxSpecAttribute extends SpecAttribute { decoration: decoration?.resolve(mix), foregroundDecoration: foregroundDecoration?.resolve(mix), transform: transform, + transformAlignment: transformAlignment, clipBehavior: clipBehavior, width: width, height: height, @@ -62,6 +65,7 @@ class BoxSpecAttribute extends SpecAttribute { foregroundDecoration?.merge(other.foregroundDecoration) ?? other.foregroundDecoration, transform: other.transform ?? transform, + transformAlignment: other.transformAlignment ?? transformAlignment, clipBehavior: other.clipBehavior ?? clipBehavior, width: other.width ?? width, height: other.height ?? height, @@ -77,6 +81,7 @@ class BoxSpecAttribute extends SpecAttribute { decoration, foregroundDecoration, transform, + transformAlignment, clipBehavior, width, height, diff --git a/lib/src/specs/container/box_spec.dart b/lib/src/specs/container/box_spec.dart index 9141d17d5..c2b8828e1 100644 --- a/lib/src/specs/container/box_spec.dart +++ b/lib/src/specs/container/box_spec.dart @@ -10,6 +10,7 @@ import 'box_attribute.dart'; class BoxSpec extends Spec { final AlignmentGeometry? alignment; final EdgeInsetsGeometry? padding; + final AlignmentGeometry? transformAlignment; final EdgeInsetsGeometry? margin; final BoxConstraints? constraints; final Decoration? decoration; @@ -27,6 +28,7 @@ class BoxSpec extends Spec { required this.decoration, required this.foregroundDecoration, required this.transform, + required this.transformAlignment, required this.clipBehavior, required this.width, required this.height, @@ -39,6 +41,7 @@ class BoxSpec extends Spec { constraints = null, decoration = null, foregroundDecoration = null, + transformAlignment = null, transform = null, width = null, height = null, @@ -60,6 +63,7 @@ class BoxSpec extends Spec { double? width, double? height, Matrix4? transform, + AlignmentGeometry? transformAlignment, Clip? clipBehavior, Color? color, }) { @@ -71,6 +75,7 @@ class BoxSpec extends Spec { decoration: decoration ?? this.decoration, foregroundDecoration: foregroundDecoration ?? this.foregroundDecoration, transform: transform ?? this.transform, + transformAlignment: transformAlignment ?? this.transformAlignment, clipBehavior: clipBehavior ?? this.clipBehavior, width: width ?? this.width, height: height ?? this.height, @@ -91,6 +96,11 @@ class BoxSpec extends Spec { t, ), transform: Matrix4Tween(begin: transform, end: other.transform).lerp(t), + transformAlignment: AlignmentGeometry.lerp( + transformAlignment, + other.transformAlignment, + t, + ), clipBehavior: lerpSnap(clipBehavior, other.clipBehavior, t), width: lerpDouble(width, other.width, t), height: lerpDouble(height, other.height, t), @@ -108,6 +118,7 @@ class BoxSpec extends Spec { decoration, foregroundDecoration, transform, + transformAlignment, clipBehavior, ]; } diff --git a/lib/src/specs/container/box_util.dart b/lib/src/specs/container/box_util.dart index 74830c285..f6dc3484f 100644 --- a/lib/src/specs/container/box_util.dart +++ b/lib/src/specs/container/box_util.dart @@ -286,6 +286,9 @@ final clipBehavior = box.clipBehavior; /// - [BoxShadow] final elevation = box.elevation; +final radialGradient = box.decoration.gradient.radial; +final linearGradient = box.decoration.gradient.linear; + class BoxSpecUtility extends SpecUtility { const BoxSpecUtility(); @@ -332,6 +335,12 @@ class BoxSpecUtility extends SpecUtility { return Matrix4Utility((transform) => only(transform: transform)); } + AlignmentUtility get transformAlignment { + return AlignmentUtility( + (transformAlignment) => only(alignment: transformAlignment), + ); + } + ClipUtility get clipBehavior { return ClipUtility((clipBehavior) => only(clipBehavior: clipBehavior)); } diff --git a/lib/src/specs/container/box_widget.dart b/lib/src/specs/container/box_widget.dart index 39566c9de..aa463e4ea 100644 --- a/lib/src/specs/container/box_widget.dart +++ b/lib/src/specs/container/box_widget.dart @@ -59,8 +59,9 @@ class Box extends StyledWidget { return mix.isAnimated ? AnimatedMixedBox( mix: mix, - curve: mix.animation.curve, - duration: mix.animation.duration, + curve: mix.animation!.curve, + duration: mix.animation!.duration, + child: child, ) : MixedBox(mix: mix, child: child); }); @@ -120,6 +121,7 @@ class MixedBox extends StatelessWidget { constraints: spec.constraints, margin: spec.margin, transform: spec.transform, + transformAlignment: spec.transformAlignment, clipBehavior: spec.clipBehavior ?? Clip.none, child: child, ); @@ -170,6 +172,7 @@ class AnimatedMixedBox extends StatelessWidget { constraints: spec.constraints, margin: spec.margin, transform: spec.transform, + transformAlignment: spec.transformAlignment, clipBehavior: spec.clipBehavior ?? Clip.none, curve: curve, duration: duration, diff --git a/lib/src/specs/text/text_spec.dart b/lib/src/specs/text/text_spec.dart index 879df3792..f35942099 100644 --- a/lib/src/specs/text/text_spec.dart +++ b/lib/src/specs/text/text_spec.dart @@ -54,7 +54,8 @@ class TextSpec extends Spec { const TextSpec.empty(); @override - TextSpec lerp(TextSpec other, double t) { + TextSpec lerp(TextSpec? other, double t) { + if (other == null) return this; // Define a helper method for snapping return TextSpec( @@ -63,7 +64,7 @@ class TextSpec extends Spec { textAlign: lerpSnap(textAlign, other.textAlign, t), textScaleFactor: lerpDouble(textScaleFactor, other.textScaleFactor, t), maxLines: lerpSnap(maxLines, other.maxLines, t), - style: TextStyle.lerp(style, other.style, t), + style: lerpTextStyle(style, other.style, t), textWidthBasis: lerpSnap(textWidthBasis, other.textWidthBasis, t), textHeightBehavior: lerpSnap(textHeightBehavior, other.textHeightBehavior, t), diff --git a/lib/src/specs/text/text_widget.dart b/lib/src/specs/text/text_widget.dart index 3d183fe23..902687d9a 100644 --- a/lib/src/specs/text/text_widget.dart +++ b/lib/src/specs/text/text_widget.dart @@ -51,12 +51,21 @@ class StyledText extends StyledWidget { @override Widget build(BuildContext context) { return withMix(context, (mix) { - return MixedText( - text: text, - mix: mix, - semanticsLabel: semanticsLabel, - locale: locale, - ); + return mix.isAnimated + ? AnimatedMixedText( + text: text, + mix: mix, + curve: mix.animation!.curve, + duration: mix.animation!.duration, + semanticsLabel: semanticsLabel, + locale: locale, + ) + : MixedText( + text: text, + mix: mix, + semanticsLabel: semanticsLabel, + locale: locale, + ); }); } } @@ -111,3 +120,100 @@ class MixedText extends StatelessWidget { ); } } + +class AnimatedMixedText extends StatelessWidget { + const AnimatedMixedText({ + required this.text, + required this.mix, + required this.curve, + required this.duration, + this.semanticsLabel, + this.locale, + super.key, + }); + + final String text; + final String? semanticsLabel; + final Locale? locale; + final MixData mix; + final Curve curve; + final Duration duration; + + @override + Widget build(BuildContext context) { + final spec = TextSpec.of(mix); + + return ImplicitlyMixedText( + text: text, + spec: spec, + semanticsLabel: semanticsLabel, + duration: duration, + ); + } +} + +class ImplicitlyMixedText extends ImplicitlyAnimatedWidget { + const ImplicitlyMixedText({ + required this.text, + required this.spec, + this.semanticsLabel, + this.locale, + super.key, + required super.duration, + super.curve = Curves.linear, + }); + + final String text; + final String? semanticsLabel; + final Locale? locale; + final TextSpec spec; + + @override + AnimatedWidgetBaseState createState() => + _ImplicitlyMixedTextState(); +} + +class _ImplicitlyMixedTextState + extends AnimatedWidgetBaseState { + TextSpecTween? _textSpecTween; + + @override + // ignore: avoid-dynamic + void forEachTween(TweenVisitor visitor) { + _textSpecTween = visitor( + _textSpecTween, + widget.spec, + // ignore: avoid-dynamic + (dynamic value) => TextSpecTween(begin: value as TextSpec), + ) as TextSpecTween?; + } + + @override + Widget build(BuildContext context) { + final spec = _textSpecTween!.evaluate(animation); + + return Text( + spec.directive?.apply(widget.text) ?? widget.text, + style: spec.style, + strutStyle: spec.strutStyle, + textAlign: spec.textAlign, + textDirection: spec.textDirection, + locale: widget.locale, + softWrap: spec.softWrap, + overflow: spec.overflow, + textScaleFactor: spec.textScaleFactor, + maxLines: spec.maxLines, + semanticsLabel: widget.semanticsLabel, + textWidthBasis: spec.textWidthBasis, + textHeightBehavior: spec.textHeightBehavior, + ); + } +} + +class TextSpecTween extends Tween { + TextSpecTween({TextSpec? begin, TextSpec? end}) + : super(begin: begin, end: end); + + @override + TextSpec lerp(double t) => begin!.lerp(end, t); +} diff --git a/lib/src/utils/context_variant_util/on_breakpoint_util.dart b/lib/src/utils/context_variant_util/on_breakpoint_util.dart index 9bd31e584..0ecad7164 100644 --- a/lib/src/utils/context_variant_util/on_breakpoint_util.dart +++ b/lib/src/utils/context_variant_util/on_breakpoint_util.dart @@ -1,5 +1,4 @@ import '../../helpers/build_context_ext.dart'; -import '../../helpers/string_ext.dart'; import '../../theme/mix_theme.dart'; import '../../theme/tokens/breakpoints_token.dart'; import '../../variants/variant.dart'; @@ -29,10 +28,8 @@ ContextVariant onBreakpoint({ double maxWidth = double.infinity, }) { final constraints = Breakpoint(minWidth: minWidth, maxWidth: maxWidth); - final constraintName = - 'minWidth-${constraints.minWidth}-maxWidth-${constraints.maxWidth}'; - return ContextVariant('on-$constraintName', (context) { + return ContextVariant((context) { final size = context.screenSize; return constraints.matches(size); @@ -45,7 +42,7 @@ ContextVariant onBreakpoint({ /// and returns a [ContextVariant] that applies when the current screen size matches /// the specified breakpoint. ContextVariant onBreakpointToken(BreakpointToken token) { - return ContextVariant('on-${token.name.paramCase}', (context) { + return ContextVariant((context) { final size = context.screenSize; final selectedbreakpoint = token.resolve(context); diff --git a/lib/src/utils/context_variant_util/on_brightness_util.dart b/lib/src/utils/context_variant_util/on_brightness_util.dart index f680075eb..ed06adcc1 100644 --- a/lib/src/utils/context_variant_util/on_brightness_util.dart +++ b/lib/src/utils/context_variant_util/on_brightness_util.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import '../../helpers/string_ext.dart'; import '../../variants/variant.dart'; /// Global brightness context variants. @@ -18,7 +17,7 @@ final onLight = onBrightness(Brightness.light); /// brightness matches the specified [brightness]. It is useful for defining /// brightness-specific styles or behaviors in the application. ContextVariant onBrightness(Brightness brightness) { - return ContextVariant('on-${brightness.name.paramCase}', (context) { + return ContextVariant((context) { return Theme.of(context).brightness == brightness; }); } diff --git a/lib/src/utils/context_variant_util/on_directionality_util.dart b/lib/src/utils/context_variant_util/on_directionality_util.dart index 2f8c2316f..630c81732 100644 --- a/lib/src/utils/context_variant_util/on_directionality_util.dart +++ b/lib/src/utils/context_variant_util/on_directionality_util.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import '../../helpers/string_ext.dart'; import '../../variants/variant.dart'; /// Variant for Right-To-Left (RTL) text direction. @@ -18,7 +17,6 @@ final onLTR = onDirectionality(TextDirection.ltr); /// [direction] - The text direction (RTL or LTR) for which the variant is to be created. ContextVariant onDirectionality(TextDirection direction) { return ContextVariant( - 'on-${direction.name.paramCase}', (context) => Directionality.of(context) == direction, ); } diff --git a/lib/src/utils/context_variant_util/on_helper_util.dart b/lib/src/utils/context_variant_util/on_helper_util.dart index 0f50b4489..d70a483f3 100644 --- a/lib/src/utils/context_variant_util/on_helper_util.dart +++ b/lib/src/utils/context_variant_util/on_helper_util.dart @@ -8,8 +8,5 @@ import '../../variants/variant.dart'; /// /// [variant] - The [ContextVariant] whose condition is to be negated. ContextVariant onNot(ContextVariant variant) { - return ContextVariant( - 'not(${variant.name})', - (context) => !variant.when(context), - ); + return ContextVariant((context) => !variant.when(context)); } diff --git a/lib/src/utils/context_variant_util/on_orientation_util.dart b/lib/src/utils/context_variant_util/on_orientation_util.dart index e3dfb247e..9a3742d1b 100644 --- a/lib/src/utils/context_variant_util/on_orientation_util.dart +++ b/lib/src/utils/context_variant_util/on_orientation_util.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import '../../helpers/string_ext.dart'; import '../../variants/variant.dart'; /// Variant for portrait orientation. @@ -22,7 +21,6 @@ final onLandscape = onOrientation(Orientation.landscape); /// [orientation] - The device orientation (portrait or landscape) for which the variant is to be created. ContextVariant onOrientation(Orientation orientation) { return ContextVariant( - 'on-${orientation.name.paramCase}', (context) => MediaQuery.of(context).orientation == orientation, ); } diff --git a/lib/src/variants/variant.dart b/lib/src/variants/variant.dart index 1868f2622..4d8de3e5b 100644 --- a/lib/src/variants/variant.dart +++ b/lib/src/variants/variant.dart @@ -5,6 +5,42 @@ import '../core/attribute.dart'; import '../factory/style_mix.dart'; import '../helpers/compare_mixin.dart'; +@immutable +abstract class StyleVariant with Comparable { + const StyleVariant(); + + /// Combines this variant with another [variant] using an 'AND' operation. + /// + /// This operator returns a [MultiVariant] that represents a combination of both + /// variants. It is useful for defining styles that should be applied when + /// multiple conditions are met. + /// + /// Example: + /// ```dart + /// final combinedVariant = variant1 & variant2; + /// ``` + MultiVariant operator &(covariant StyleVariant variant) => + MultiVariant.and([this, variant]); + + /// Combines this variant with another [variant] using an 'OR' operation. + /// + /// This operator returns a [MultiVariant] that represents either of the variants. + /// It is useful for defining styles that should be applied when any one of + /// multiple conditions is met. + /// + /// Example: + /// ```dart + /// final eitherVariant = variant1 | variant2; + /// ``` + MultiVariant operator |(covariant StyleVariant variant) => + MultiVariant.or([this, variant]); + + /// Checks if this variant matches a set of provided variants. + /// Returns true if this variant is present in the provided [matchVariants]. + bool matches(Iterable matchVariants) => + matchVariants.contains(this); +} + /// An immutable class representing a styling variant. /// /// Variants encapsulate a set of styles that can be applied together under certain conditions @@ -35,7 +71,7 @@ import '../helpers/compare_mixin.dart'; /// ); /// ``` @immutable -class Variant with Comparable { +class Variant extends StyleVariant { final String name; /// Constructs a `Variant` with the given [name]. @@ -43,34 +79,10 @@ class Variant with Comparable { /// The [name] parameter uniquely identifies the variant and is used in style resolution. const Variant(this.name); - /// Combines this variant with another [variant] using an 'AND' operation. + /// Creates a new [VariantAttribute] with the given [variant] and [style]. /// - /// This operator returns a [MultiVariant] that represents a combination of both - /// variants. It is useful for defining styles that should be applied when - /// multiple conditions are met. - /// - /// Example: - /// ```dart - /// final combinedVariant = variant1 & variant2; - /// ``` - MultiVariant operator &(Variant variant) => MultiVariant.and([this, variant]); - - /// Combines this variant with another [variant] using an 'OR' operation. - /// - /// This operator returns a [MultiVariant] that represents either of the variants. - /// It is useful for defining styles that should be applied when any one of - /// multiple conditions is met. - /// - /// Example: - /// ```dart - /// final eitherVariant = variant1 | variant2; - /// ``` - MultiVariant operator |(Variant variant) => MultiVariant.or([this, variant]); - - /// Defines styles to be applied when this variant is active. - /// - /// You can define up to 20 styling [`Attribute`]s to be applied when this `Variant` is activated. - /// Null values are ignored and do not affect the resulting styling rules. + /// This method is used to create a new [VariantAttribute] instance with the given [variant] and [style]. + /// It is used internally by the `Variant` class to create a new `VariantAttribute` when the variant is applied. VariantAttribute call([ Attribute? p1, Attribute? p2, @@ -101,17 +113,10 @@ class Variant with Comparable { return VariantAttribute(this, Style.create(params)); } - bool matches(Iterable matchVariants) { - return matchVariants.contains(this); - } - @override get props => [name]; } -/// A typedef for a function that determines if a specific context condition is met. -typedef WhenContextFunction = bool Function(BuildContext context); - /// A variant of styling that is applied based on specific context conditions. /// /// `ContextVariant` extends the functionality of the `Variant` class by introducing @@ -122,7 +127,6 @@ typedef WhenContextFunction = bool Function(BuildContext context); /// Creating an `onDark` variant that applies styles when the app's theme is in dark mode: /// ```dart /// final onDark = ContextVariant( -/// 'on-dark', /// ( context) => Theme.of(context).brightness == Brightness.dark, /// ) /// @@ -137,21 +141,22 @@ typedef WhenContextFunction = bool Function(BuildContext context); /// This example defines a `ContextVariant` onDark, which checks if the current /// theme brightness is dark. If true, the associated styles are applied. @immutable -class ContextVariant extends Variant { +class ContextVariant extends StyleVariant { /// A function that defines the condition under which this variant should be applied. /// /// The [when] function takes a [BuildContext] and returns a boolean indicating whether /// the condition for this variant is met in the given context. This allows for dynamic /// styling changes based on runtime context conditions, such as theme brightness or screen size. - final WhenContextFunction when; + final bool Function(BuildContext context) when; /// Constructs a `ContextVariant` with a given [name] and a context condition function [when]. + const ContextVariant(this.when); + + /// Creates a new [ContextVariantAttribute] with the given [variant] and [style]. /// - /// The [name] uniquely identifies the variant and is used in style resolution, while the - /// [when] function determines the applicability of the variant based on the given context. - const ContextVariant(super.name, this.when); + /// This method is used to create a new [ContextVariantAttribute] instance with the given [variant] and [style]. + /// It is used internally by the `ContextVariant` class to create a new `ContextVariantAttribute` when the variant is applied. - @override ContextVariantAttribute call([ Attribute? p1, Attribute? p2, @@ -179,12 +184,11 @@ class ContextVariant extends Variant { p11, p12, p13, p14, p15, p16, p17, p18, p19, p20, ].whereType(); - // Create a ContextVariantAttribute using the collected parameters. return ContextVariantAttribute(this, Style.create(params)); } @override - get props => [name, when]; + get props => [when]; } enum MultiVariantOperator { and, or } @@ -225,9 +229,9 @@ enum MultiVariantOperator { and, or } /// based on multiple conditions. @immutable -class MultiVariant extends Variant { +class MultiVariant extends StyleVariant { /// A list of [Variant] instances contained within this `MultiVariant`. - final List variants; + final List variants; /// The type operator of this `MultiVariant`, defining its category or specific behavior. /// @@ -235,30 +239,30 @@ class MultiVariant extends Variant { /// understanding and applying their behavior. final MultiVariantOperator operatorType; - const MultiVariant._(super.name, this.variants, {required this.operatorType}); + const MultiVariant._(this.variants, {required this.operatorType}); factory MultiVariant( - Iterable variants, { + Iterable variants, { required MultiVariantOperator type, }) { final sortedVariants = variants.toList() - ..sort(((a, b) => a.name.compareTo(b.name))); - final combinedName = sortedVariants.map((e) => e.name).join('-'); + ..sort(((a, b) => + a.runtimeType.toString().compareTo(b.runtimeType.toString()))); - return MultiVariant._(combinedName, sortedVariants, operatorType: type); + return MultiVariant._(sortedVariants, operatorType: type); } /// Factory constructor to create a `MultiVariant` where all provided variants need to be active (`MultiVariantType.and`). /// /// It initializes a `MultiVariant` with the given [variants] and sets the type to `MultiVariantType.and`. - factory MultiVariant.and(Iterable variants) { + factory MultiVariant.and(Iterable variants) { return MultiVariant(variants, type: MultiVariantOperator.and); } /// Factory constructor to create a `MultiVariant` where any one of the provided variants needs to be active (`MultiVariantType.or`). /// /// It initializes a `MultiVariant` with the given [variants] and sets the type to `MultiVariantType.or`. - factory MultiVariant.or(Iterable variants) { + factory MultiVariant.or(Iterable variants) { return MultiVariant(variants, type: MultiVariantOperator.or); } @@ -275,7 +279,7 @@ class MultiVariant extends Variant { /// ``` /// In this example, `updatedVariant` will be a combination of `variantB` and `variantC`. /// This is useful for procedurally applying variants based on runtime conditions. - Variant remove(Iterable variantsToRemove) { + StyleVariant remove(Iterable variantsToRemove) { final updatedVariants = variants..removeWhere(variantsToRemove.contains); return updatedVariants.length == 1 @@ -307,36 +311,10 @@ class MultiVariant extends Variant { contextVariants.every((variant) => variant.when(context)); } - /// Determines if the current `MultiVariant` matches a set of provided variants. - /// - /// This method evaluates whether the variants within this `MultiVariant` align with the given [matchVariants] based on its `type`: - /// - `MultiVariantType.and`: Returns true if every variant in this `MultiVariant` is present in [matchVariants]. - /// - `MultiVariantType.or`: Returns true if at least one of the variants in this `MultiVariant` is present in [matchVariants]. - /// - /// This method is particularly useful for checking if a composite style, represented by this `MultiVariant`, - /// should be applied based on a specific set of active variants. - /// - /// Example: - /// ```dart - /// final combinedVariant = MultiVariant.and([variantA, variantB]); - /// bool isMatched = combinedVariant.matches([variantA, variantB, variantC]); - /// ``` - /// Here, `isMatched` will be true for `MultiVariantType.and` if both `variantA` and `variantB` are included in the provided list. - /// For `MultiVariantType.or`, `isMatched` would be true if either `variantA` or `variantB` is in the list. - @override - bool matches(Iterable matchVariants) { - final list = variants.map((e) => e.matches(matchVariants)).toList(); - - return operatorType == MultiVariantOperator.and - ? list.every((e) => e) - : list.contains(true); - } - - /// A method for creating a new `MultiVariantAttribute` instance. + /// Creates a new [MultiVariantAttribute] with the given [variant] and [style]. /// - /// It takes up to 20 optional [Attribute] parameters and creates a new `MultiVariantAttribute` using these attributes. - /// This method allows for easy creation of a `MultiVariantAttribute` with custom attributes. - @override + /// This method is used to create a new [MultiVariantAttribute] instance with the given [variant] and [style]. + /// It is used internally by the `MultiVariant` class to create a new `MultiVariantAttribute` when the variant is applied. MultiVariantAttribute call([ Attribute? p1, Attribute? p2, @@ -367,6 +345,31 @@ class MultiVariant extends Variant { return MultiVariantAttribute(this, Style.create(params)); } + /// Determines if the current `MultiVariant` matches a set of provided variants. + /// + /// This method evaluates whether the variants within this `MultiVariant` align with the given [matchVariants] based on its `type`: + /// - `MultiVariantType.and`: Returns true if every variant in this `MultiVariant` is present in [matchVariants]. + /// - `MultiVariantType.or`: Returns true if at least one of the variants in this `MultiVariant` is present in [matchVariants]. + /// + /// This method is particularly useful for checking if a composite style, represented by this `MultiVariant`, + /// should be applied based on a specific set of active variants. + /// + /// Example: + /// ```dart + /// final combinedVariant = MultiVariant.and([variantA, variantB]); + /// bool isMatched = combinedVariant.matches([variantA, variantB, variantC]); + /// ``` + /// Here, `isMatched` will be true for `MultiVariantType.and` if both `variantA` and `variantB` are included in the provided list. + /// For `MultiVariantType.or`, `isMatched` would be true if either `variantA` or `variantB` is in the list. + @override + bool matches(Iterable matchVariants) { + final list = variants.map((e) => e.matches(matchVariants)).toList(); + + return operatorType == MultiVariantOperator.and + ? list.every((e) => e) + : list.contains(true); + } + @override - get props => [name, variants]; + get props => [variants]; } diff --git a/lib/src/widgets/pressable/pressable_data.notifier.dart b/lib/src/widgets/pressable/pressable_data.notifier.dart deleted file mode 100644 index e3d2c940b..000000000 --- a/lib/src/widgets/pressable/pressable_data.notifier.dart +++ /dev/null @@ -1,138 +0,0 @@ -import 'package:flutter/widgets.dart'; - -import '../../helpers/compare_mixin.dart'; - -enum PressableDataAspect { focused, disabled, state, cursorPosition } - -@immutable -class CursorPosition { - final Alignment alignment; - final Offset offset; - - const CursorPosition({required this.alignment, required this.offset}); -} - -@immutable -class PressableStateData with Comparable { - final bool focused; - - final bool disabled; - final PressableState state; - final CursorPosition cursorPosition; - - const PressableStateData({ - required this.focused, - required this.disabled, - required this.state, - required this.cursorPosition, - }); - - const PressableStateData.none() - : focused = false, - disabled = true, - cursorPosition = const CursorPosition( - alignment: Alignment.center, - offset: Offset.zero, - ), - state = PressableState.none; - - PressableStateData copyWith({ - bool? focused, - bool? disabled, - PressableState? state, - CursorPosition? cursorPosition, - }) { - return PressableStateData( - focused: focused ?? this.focused, - disabled: disabled ?? this.disabled, - state: state ?? this.state, - cursorPosition: cursorPosition ?? this.cursorPosition, - ); - } - - @override - get props => [focused, disabled, state, cursorPosition]; -} - -enum PressableState { - none, - hovered, - pressed, - longPressed, -} - -class PressableDataNotifier extends InheritedModel { - const PressableDataNotifier({ - super.key, - required super.child, - required this.data, - }); - - static PressableStateData of( - BuildContext context, { - PressableDataAspect? aspect, - }) { - final model = InheritedModel.inheritFrom( - context, - aspect: aspect, - ); - - assert( - model != null, - 'No Pressable data found in context. Make sure to wrap your widget a Pressable widget', - ); - - return model!.data; - } - - static bool isDisabledOf(BuildContext context) { - return of(context, aspect: PressableDataAspect.disabled).disabled; - } - - static CursorPosition cursorPositionOf(BuildContext context) { - return of(context, aspect: PressableDataAspect.cursorPosition) - .cursorPosition; - } - - static bool isFocusedOf(BuildContext context) { - return of(context, aspect: PressableDataAspect.focused).focused; - } - - static PressableState stateOf(BuildContext context) { - return of(context, aspect: PressableDataAspect.state).state; - } - - final PressableStateData data; - - @override - bool updateShouldNotify(PressableDataNotifier oldWidget) { - return oldWidget.data != data; - } - - @override - bool updateShouldNotifyDependent( - PressableDataNotifier oldWidget, - Set dependencies, - ) { - if (oldWidget.data.focused != data.focused && - dependencies.contains(PressableDataAspect.focused)) { - return true; - } - if (oldWidget.data.disabled != data.disabled && - dependencies.contains(PressableDataAspect.disabled)) { - return true; - } - - if (oldWidget.data.state != data.state && - dependencies.contains(PressableDataAspect.state)) { - return true; - } - - if (oldWidget.data.cursorPosition != data.cursorPosition && - dependencies.contains(PressableDataAspect.cursorPosition)) { - return true; - } - - return false; - } -} diff --git a/lib/src/widgets/pressable/pressable_state.dart b/lib/src/widgets/pressable/pressable_state.dart new file mode 100644 index 000000000..0de70de58 --- /dev/null +++ b/lib/src/widgets/pressable/pressable_state.dart @@ -0,0 +1,164 @@ +// ignore_for_file: avoid-inferrable-type-arguments + +import 'package:flutter/widgets.dart'; + +import '../../helpers/compare_mixin.dart'; + +enum PressableStateAspect { + currentState, + enabled, + hovered, + focused, + pressed, + longPressed, + pointerPosition +} + +enum PressableCurrentState { + idle, + hovered, + pressed, + longPressed, +} + +class PressableState extends InheritedModel { + const PressableState({ + super.key, + required super.child, + required this.enabled, + required this.hovered, + required this.focused, + required this.pressed, + required this.longPressed, + required this.pointerPosition, + }); + + factory PressableState.none({Key? key, required Widget child}) { + return PressableState( + key: key, + enabled: false, + hovered: false, + focused: false, + pressed: false, + longPressed: false, + pointerPosition: null, + child: child, + ); + } + + static PressableState of( + BuildContext context, [ + PressableStateAspect? aspect, + ]) { + final PressableState? result = maybeOf(context, aspect); + assert(result != null, 'Unable to find an instance of MyModel...'); + + return result!; + } + + static PressableState? maybeOf( + BuildContext context, [ + PressableStateAspect? aspect, + ]) { + return InheritedModel.inheritFrom(context, aspect: aspect); + } + + static PressableState aspectOf( + BuildContext context, + PressableStateAspect aspect, + ) { + return of(context, aspect); + } + + static bool enabledOf(BuildContext context) { + return of(context, PressableStateAspect.enabled).enabled; + } + + static bool hoveredOf(BuildContext context) { + return of(context, PressableStateAspect.hovered).hovered; + } + + static bool focusedOf(BuildContext context) { + return of(context, PressableStateAspect.focused).focused; + } + + static bool pressedOf(BuildContext context) { + return of(context, PressableStateAspect.pressed).pressed; + } + + static bool longPressedOf(BuildContext context) { + return of(context, PressableStateAspect.longPressed).longPressed; + } + + static PointerPosition? pointerPositionOf(BuildContext context) { + return of(context, PressableStateAspect.pointerPosition).pointerPosition; + } + + static PressableCurrentState stateOf(BuildContext context) { + return of(context, PressableStateAspect.currentState).currentState; + } + + final bool enabled; + final bool hovered; + final bool focused; + final bool pressed; + final bool longPressed; + + final PointerPosition? pointerPosition; + + PressableCurrentState get currentState { + if (enabled) { + // Long pressed has priority over pressed + // Due to delay of removing the _press state + if (longPressed) return PressableCurrentState.longPressed; + + if (pressed) return PressableCurrentState.pressed; + } + + if (hovered) return PressableCurrentState.hovered; + + return PressableCurrentState.idle; + } + + bool get disabled => !enabled; + + @override + bool updateShouldNotify(PressableState oldWidget) { + return oldWidget.enabled != enabled || + oldWidget.hovered != hovered || + oldWidget.focused != focused || + oldWidget.pressed != pressed || + oldWidget.longPressed != longPressed || + oldWidget.pointerPosition != pointerPosition; + } + + @override + bool updateShouldNotifyDependent( + PressableState oldWidget, + Set dependencies, + ) { + return dependencies.contains(PressableStateAspect.enabled) && + oldWidget.enabled != enabled || + dependencies.contains(PressableStateAspect.hovered) && + oldWidget.hovered != hovered || + dependencies.contains(PressableStateAspect.focused) && + oldWidget.focused != focused || + dependencies.contains(PressableStateAspect.pressed) && + oldWidget.pressed != pressed || + dependencies.contains(PressableStateAspect.longPressed) && + oldWidget.longPressed != longPressed || + dependencies.contains(PressableStateAspect.pointerPosition) && + oldWidget.pointerPosition != pointerPosition || + dependencies.contains(PressableStateAspect.currentState) && + oldWidget.currentState != currentState; + } +} + +class PointerPosition with Comparable { + final Alignment alignment; + final Offset offset; + const PointerPosition({required this.alignment, required this.offset}); + + @override + get props => [alignment, offset]; +} diff --git a/lib/src/widgets/pressable/pressable_util.dart b/lib/src/widgets/pressable/pressable_util.dart index 83315d698..b200069d4 100644 --- a/lib/src/widgets/pressable/pressable_util.dart +++ b/lib/src/widgets/pressable/pressable_util.dart @@ -1,56 +1,180 @@ import 'package:flutter/material.dart'; -import '../../helpers/string_ext.dart'; +import '../../attributes/nested_style/nested_style_attribute.dart'; +import '../../core/attribute.dart'; +import '../../factory/style_mix.dart'; +import '../../utils/context_variant_util/on_helper_util.dart'; import '../../variants/variant.dart'; -import 'pressable_data.notifier.dart'; +import 'pressable_state.dart'; /// Global context variants for handling common widget states and gestures. /// Applies styles when the widget is pressed. -final onPressed = _onState(PressableState.pressed); +final onPressed = PressableStateVariant( + (context) => PressableState.pressedOf(context), +); /// Applies styles when the widget is long pressed. -final onLongPressed = _onState(PressableState.longPressed); +final onLongPressed = PressableStateVariant( + (context) => PressableState.longPressedOf(context), +); /// Applies styles when widget is hovered over. -final onHover = _onState(PressableState.hovered); +final onHover = PressableStateVariant( + (context) => PressableState.hoveredOf(context), +); /// Applies styles when the widget is disabled. -final onDisabled = _onDisabled(true); +final onEnabled = PressableStateVariant( + (context) => PressableState.enabledOf(context), +); /// Applies styles when the widget is enabled. -final onEnabled = _onDisabled(false); +final onDisabled = onNot(onEnabled); + +const onMouseHover = OnMouseHoverBuilder.new; /// Applies styles when the widget has focus.dar -final onFocused = ContextVariant( - 'on-focused', +const onFocused = PressableStateVariant(PressableState.focusedOf); - /// Applies the variant only when the GestureStateNotifier's focus property is true. - (context) => PressableDataNotifier.isFocusedOf(context) == true, -); +const onPressedEvent = OnPressedEventBuilder.new; +const onLongPressedEvent = OnLongPressedEventBuilder.new; +const onHoverEvent = OnHoverEventBuilder.new; +const onEnabledEvent = OnEnabledEventBuilder.new; +const onDisabledEvent = OnDisabledEventBuilder.new; +const onFocusedEvent = OnFocusedEventBuilder.new; /// Helper class for creating widget state-based context variants. @immutable -class PressableDataVariant extends ContextVariant { - const PressableDataVariant(super.name, super.when); +class PressableStateVariant extends ContextVariant { + const PressableStateVariant(super.when); } -/// Creates a [PressableDataVariant] based on the specified [state]. -/// -/// This function constructs a WidgetStateVariant with a name based on the provided state and a condition that checks if the GestureStateNotifier in the context matches the given state. -PressableDataVariant _onState(PressableState state) { - return PressableDataVariant( - 'on-${state.name.paramCase}', - (context) => PressableDataNotifier.stateOf(context) == state, - ); +@immutable +class OnDisabledEventBuilder + extends StyleAttributeBuilder { + final Attribute Function(bool disabled) fn; + const OnDisabledEventBuilder(this.fn); + + @override + Attribute builder(BuildContext context) { + return fn(!PressableState.enabledOf(context)); + } + + @override + get props => [fn]; } -/// Creates a [PressableDataVariant] based on the specified [status]. -/// -/// Similar to `_onState`, this function creates a WidgetStateVariant with a condition that checks if the GestureStateNotifier in the context matches the provided status. -PressableDataVariant _onDisabled(bool disabled) { - return PressableDataVariant( - 'on-${disabled ? 'disabled' : 'enabled'}', - (context) => PressableDataNotifier.isDisabledOf(context) == disabled, - ); +@immutable +class OnFocusedEventBuilder + extends StyleAttributeBuilder { + final Attribute Function(bool focused) fn; + const OnFocusedEventBuilder(this.fn); + + @override + Attribute builder(BuildContext context) { + return fn(PressableState.focusedOf(context)); + } + + @override + get props => [fn]; +} + +@immutable +class OnPressedEventBuilder + extends StyleAttributeBuilder { + final Attribute Function(bool pressed) fn; + const OnPressedEventBuilder(this.fn); + + @override + Attribute builder(BuildContext context) { + return fn(PressableState.pressedOf(context)); + } + + @override + get props => [fn]; +} + +@immutable +class OnLongPressedEventBuilder + extends StyleAttributeBuilder { + final Attribute Function(bool longPressed) fn; + const OnLongPressedEventBuilder(this.fn); + + @override + Attribute builder(BuildContext context) { + return fn(PressableState.longPressedOf(context)); + } + + @override + get props => [fn]; +} + +@immutable +class OnHoverEventBuilder extends StyleAttributeBuilder { + final Attribute Function(bool hovered) fn; + const OnHoverEventBuilder(this.fn); + + @override + Attribute builder(BuildContext context) { + return fn(PressableState.hoveredOf(context)); + } + + @override + get props => [fn]; +} + +@immutable +class OnEnabledEventBuilder + extends StyleAttributeBuilder { + final Attribute Function(bool enabled) fn; + const OnEnabledEventBuilder(this.fn); + + @override + Attribute builder(BuildContext context) { + return fn(PressableState.enabledOf(context)); + } + + @override + get props => [fn]; +} + +@immutable +class OnMouseHoverBuilder extends StyleAttributeBuilder + with Mergeable { + final List