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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/mix/lib/src/core/spec_utility.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ abstract class StyleAttributeBuilder<S extends Spec<S>> extends Style<S>
@override
List<VariantStyle<S>>? get $variants => style.$variants;

@override
bool get hasBasePayload => style.hasBasePayload;

@override
Style<S> copyWithVariants(List<VariantStyle<S>>? variants) {
return style.copyWithVariants(variants);
}

@override
List<Object?> get props => [style];
}
Expand Down Expand Up @@ -83,6 +91,14 @@ abstract class StyleMutableBuilder<S extends Spec<S>> extends Style<S>
@override
List<VariantStyle<S>>? get $variants => value.$variants;

@override
bool get hasBasePayload => value.hasBasePayload;

@override
Style<S> copyWithVariants(List<VariantStyle<S>>? variants) {
return value.copyWithVariants(variants);
}

@override
List<Object?> get props => [mutable];
}
Expand Down
196 changes: 131 additions & 65 deletions packages/mix/lib/src/core/style.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';

import '../animation/animation_config.dart';
import 'helpers.dart';
import '../modifiers/widget_modifier_config.dart';
import '../variants/variant.dart';
import 'internal/compare_mixin.dart';
Expand All @@ -11,6 +12,8 @@ import 'spec.dart';
import 'style_spec.dart';
import 'widget_modifier.dart';

export '../variants/variant.dart' show ContextVariantBuilder;

/// Marker interface for style-related elements.
@internal
sealed class StyleElement {
Expand All @@ -22,6 +25,21 @@ sealed class StyleElement {
/// Provides variant support, modifiers, and animation configuration for styled elements.
abstract class Style<S extends Spec<S>> extends Mix<StyleSpec<S>>
implements StyleElement {
static int _activeVariantMergeDepth = 0;
static final _visitedStyles = <int>{};

@internal
static bool get isResolvingActiveVariants => _activeVariantMergeDepth > 0;

static T _withActiveVariantMergeGuard<T>(T Function() fn) {
_activeVariantMergeDepth++;
try {
return fn();
} finally {
_activeVariantMergeDepth--;
}
}

final List<VariantStyle<S>>? $variants;

final WidgetModifierConfig? $modifier;
Expand All @@ -35,6 +53,38 @@ abstract class Style<S extends Spec<S>> extends Mix<StyleSpec<S>>
$animation = animation,
$variants = variants;

@internal
bool get hasBasePayload;

/// Copies current style payload while replacing only variants.
@internal
Style<S> copyWithVariants(List<VariantStyle<S>>? variants);

@internal
Style<S>? deferMerge(covariant Style<S>? other) {
if (other == null) return null;
if (isResolvingActiveVariants) return null;

final hasBuilders =
$variants?.any((v) => v.variant is ContextVariantBuilder) ?? false;
if (!hasBuilders) return null;

List<VariantStyle<S>>? deferredVariants;
if (other.hasBasePayload) {
final payload = other.copyWithVariants(null);
deferredVariants = [
VariantStyle<S>(DeferredVariant<S>(payload), payload),
];
}

return copyWithVariants(
MixOps.mergeVariants(
[...?$variants, ...?deferredVariants],
other.$variants,
),
);
}

/// Gets the closest [Style] from the widget tree.
///
/// Throws a [FlutterError] if no [Style] is found in the widget tree.
Expand Down Expand Up @@ -84,74 +134,90 @@ abstract class Style<S extends Spec<S>> extends Mix<StyleSpec<S>>
BuildContext context, {
required Set<NamedVariant> namedVariants,
}) {
// Filter variants that should be active in this context
final activeVariants = ($variants ?? [])
.where(
(variantAttr) => switch (variantAttr.variant) {
(ContextVariant variant) => variant.when(context),
(NamedVariant variant) => namedVariants.contains(variant),
(ContextVariantBuilder _) => true,
},
)
.toList();

// Sort by priority: WidgetStateVariant gets applied last (highest priority)
activeVariants.sort(
(a, b) => Comparable.compare(
a.variant is WidgetStateVariant ? 1 : 0,
b.variant is WidgetStateVariant ? 1 : 0,
),
);

// Extract the style from each active variant
final stylesToMerge = <(Style<S>, bool)>[]; // (style, isFromStyleVariation)

for (final variantAttr in activeVariants) {
final result = switch (variantAttr.variant) {
ContextVariantBuilder variant => (
variant.build(context) as Style<S>,
false,
),
(ContextVariant() || NamedVariant()) => () {
// Check if the value is a StyleVariation
// ignore: avoid-unrelated-type-assertions
if (variantAttr.value is StyleVariation<S>) {
// ignore: avoid-unrelated-type-casts
final styleVariation = variantAttr.value as StyleVariation<S>;
// Only apply if this variant is active
if (namedVariants.contains(styleVariation.variantType)) {
return (
styleVariation.styleBuilder(this, namedVariants, context),
true,
);
final styleId = identityHashCode(this);
if (!_visitedStyles.add(styleId)) return this;

try {
// Filter variants that should be active in this context.
final activeVariants = ($variants ?? [])
.where(
(variantAttr) => switch (variantAttr.variant) {
(ContextVariant variant) => variant.when(context),
(NamedVariant variant) => namedVariants.contains(variant),
(ContextVariantBuilder _) => true,
(DeferredVariant _) => true,
},
)
.toList();

// Preserve insertion order while still applying WidgetStateVariant last.
final prioritizedVariants = <VariantStyle<S>>[];
final widgetStateVariants = <VariantStyle<S>>[];
for (final variantAttr in activeVariants) {
if (variantAttr.variant is WidgetStateVariant) {
widgetStateVariants.add(variantAttr);
} else {
prioritizedVariants.add(variantAttr);
}
}
prioritizedVariants.addAll(widgetStateVariants);

// Extract the style from each active variant
final stylesToMerge =
<(Style<S>, bool)>[]; // (style, isFromStyleVariation)

for (final variantAttr in prioritizedVariants) {
final result = switch (variantAttr.variant) {
ContextVariantBuilder variant => (
variant.build(context) as Style<S>,
false,
),
DeferredVariant variant => (variant.payload as Style<S>, false),
(ContextVariant() || NamedVariant()) => () {
// Check if the value is a StyleVariation
// ignore: avoid-unrelated-type-assertions
if (variantAttr.value is StyleVariation<S>) {
// ignore: avoid-unrelated-type-casts
final styleVariation = variantAttr.value as StyleVariation<S>;
// Only apply if this variant is active
if (namedVariants.contains(styleVariation.variantType)) {
return (
styleVariation.styleBuilder(this, namedVariants, context),
true,
);
}
}
}

return (variantAttr.value, false);
}(),
};
stylesToMerge.add(result);
}

// Start with current style as base
Style<S> mergedStyle = this;

// Merge each variant style, recursively resolving nested variants
for (final (variantStyle, isFromStyleVariation) in stylesToMerge) {
final fullyResolvedStyle = isFromStyleVariation
// For StyleVariation results, we don't recursively resolve variants
// since StyleVariation.styleBuilder should handle its own variant logic
// and return a final style. This prevents infinite recursion.
? variantStyle
// For regular variants, recursively resolve any nested variants
: variantStyle.mergeActiveVariants(
context,
namedVariants: namedVariants,
);
mergedStyle = mergedStyle.merge(fullyResolvedStyle);
return (variantAttr.value, false);
}(),
};
stylesToMerge.add(result);
}

// Start with current style as base
Style<S> mergedStyle = this;

// Merge each variant style, recursively resolving nested variants
for (final (variantStyle, isFromStyleVariation) in stylesToMerge) {
final fullyResolvedStyle = isFromStyleVariation
// For StyleVariation results, we don't recursively resolve variants
// since StyleVariation.styleBuilder should handle its own variant logic
// and return a final style. This prevents infinite recursion.
? variantStyle
// For regular variants, recursively resolve any nested variants
: variantStyle.mergeActiveVariants(
context,
namedVariants: namedVariants,
);
mergedStyle = _withActiveVariantMergeGuard(
() => mergedStyle.merge(fullyResolvedStyle),
);
}

return mergedStyle;
} finally {
_visitedStyles.remove(styleId);
}

return mergedStyle;
}

/// Resolves this attribute to its concrete value using the provided [BuildContext].
Expand Down
35 changes: 35 additions & 0 deletions packages/mix/lib/src/specs/box/box_style.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 35 additions & 0 deletions packages/mix/lib/src/specs/flex/flex_style.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions packages/mix/lib/src/specs/flexbox/flexbox_style.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading