diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json deleted file mode 100644 index b3db758e4..000000000 --- a/.fvm/fvm_config.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "flutterSdkVersion": "stable", - "flavors": {} -} \ No newline at end of file diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 000000000..c300356c3 --- /dev/null +++ b/.fvmrc @@ -0,0 +1,3 @@ +{ + "flutter": "stable" +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 130a0e14e..7d22bf370 100644 --- a/.gitignore +++ b/.gitignore @@ -83,4 +83,6 @@ yarn.lock node_modules # FVM -.fvm/flutter_sdk \ No newline at end of file +.fvm/flutter_sdk +# FVM Version Cache +.fvm diff --git a/.vscode/settings.json b/.vscode/settings.json index 9d895d2a8..f747ff802 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,15 +1,8 @@ { - "dart.flutterSdkPath": ".fvm/flutter_sdk", - // Remove .fvm files from search - "search.exclude": { - "**/.fvm": true - }, - // Remove from file watching - "files.watcherExclude": { - "**/.fvm": true - }, "editor.codeActionsOnSave": { "source.organizeImports": true, - "source.fixAll": true - } -} + "source.fixAll": true, + "source.dcm.fixAll": true + }, + "dart.flutterSdkPath": ".fvm/versions/stable" +} \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml index b827dfb14..3c62b3fad 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -17,17 +17,22 @@ analyzer: dart_code_metrics: metrics: cyclomatic-complexity: 20 - number-of-parameters: 4 maximum-nesting-level: 5 metrics-exclude: - test/** rules: + - avoid-dynamic + - avoid-passing-async-when-sync-expected + - avoid-redundant-async + - avoid-unnecessary-type-assertions + - avoid-unnecessary-type-casts + - avoid-unrelated-type-assertions + - avoid-unused-parameters + - avoid-nested-conditional-expressions - newline-before-return - no-boolean-literal-compare - no-empty-block - prefer-trailing-comma + - prefer-conditional-expressions - no-equal-then-else - - anti-patterns: - - long-method - - long-parameter-list + - prefer-moving-to-variable \ No newline at end of file diff --git a/demo/lib/theme.dart b/demo/lib/theme.dart index 0430b9523..42eefca08 100644 --- a/demo/lib/theme.dart +++ b/demo/lib/theme.dart @@ -39,7 +39,7 @@ MaterialStateProperty getPropertyForTrack(ThemeData theme) { if (states.contains(MaterialState.selected)) { return theme.colorScheme.secondary; } - + return null; }); } @@ -59,7 +59,7 @@ ThemeData get darkTheme { if (states.contains(MaterialState.hovered)) { return Colors.white; } - + return Colors.white.withAlpha(230); }), ), diff --git a/demo/lib/views/button_example.dart b/demo/lib/views/button_example.dart new file mode 100644 index 000000000..d6daa4a8e --- /dev/null +++ b/demo/lib/views/button_example.dart @@ -0,0 +1,181 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:mix/mix.dart'; + +enum SizeVariantEnum { + small, + medium, + large, +} + +enum LeadingPosition { left, center, right } + +@immutable +class ButtonSizeVariant extends StyleVariant { + ButtonSizeVariant(T variant) : super(variant.name); +} + +class ButtonSizeVariants { + static final small = ButtonSizeVariant(SizeVariantEnum.small); + static final medium = ButtonSizeVariant(SizeVariantEnum.medium); + static final large = ButtonSizeVariant(SizeVariantEnum.large); +} + +StyleMix get _baseStyle => StyleMix( + rounded(4), + animation( + curve: Curves.easeIn, + duration: 100, + ), + onPress( + scale(0.95), + ), + mainAxisAlignment(MainAxisAlignment.center), + textStyle( + // added because of lack of style parameters (yellow lines) + decoration: TextDecoration.none, + fontWeight: FontWeight.w600, + fontFamily: $M3Text.bodySmall.fontFamily, + ), + mainAxisSize(MainAxisSize.min), // For flexbox + ButtonSizeVariants.small( + paddingHorizontal(10), + paddingVertical(10), + textStyle( + fontSize: 16, + ), + iconSize(24), + ), + ButtonSizeVariants.medium( + paddingHorizontal(4), + paddingVertical(16), + textStyle( + fontSize: 16, + ), + iconSize(24), + ), + ButtonSizeVariants.large( + paddingHorizontal(4), + paddingVertical(2), + textStyle( + fontSize: 16, + ), + iconSize(24), + ), + ); + +abstract class Button extends StatelessWidget { + const Button( + this.text, { + super.key, + this.size, + this.style, + this.onPressed, + this.onLongPressed, + this.leading, + this.leadingPosition = LeadingPosition.left, + this.interPadding = 12, // ultrashotTheme.spacing.w15 + }); + + final String text; + final ButtonSizeVariant? size; + final StyleMix? style; + final VoidCallback? onPressed; + final VoidCallback? onLongPressed; + final Widget? leading; + final LeadingPosition? leadingPosition; + final double? interPadding; + + Widget get _leftContent { + if (leading == null || + leading != null && leadingPosition != LeadingPosition.left) { + return SizedBox.fromSize(size: Size.zero); + } + + return Padding( + padding: EdgeInsets.only(right: interPadding!), + child: leading, + ); + } + + Widget get _centerContent { + if (leading != null && leadingPosition == LeadingPosition.center) { + return leading!; + } + + return StyledText( + text, + inherit: true, + style: StyleMix(), + ); + } + + Widget get _rightContent { + if (leading == null || + leading != null && leadingPosition != LeadingPosition.right) { + return SizedBox.fromSize(size: Size.zero); + } + + return Padding( + padding: EdgeInsets.only(left: interPadding!), + child: leading, + ); + } + + @override + Widget build(BuildContext context) { + final mergedStyle = _baseStyle + .selectVariant(size ?? ButtonSizeVariants.medium) + .mergeNullable(style); + + return Pressable( + onPressed: onPressed, + onLongPress: onLongPressed, + child: HBox( + style: mergedStyle, + children: [ + _leftContent, + _centerContent, + _rightContent, + ], + ), + ); + } +} + +StyleMix get _style => StyleMix( + textStyle( + color: const Color(0xFFFF004C), + ), + backgroundColor(const Color(0x0F07E2FF)), + iconColor($MDColorScheme.onBackground), + onDisabled( + backgroundColor($MDColorScheme.background.withOpacity(0.3)), + textStyle(color: $MDColorScheme.onBackground.withOpacity(0.3)), + iconColor($MDColorScheme.onBackground.withOpacity(0.3)), + ), + ); + +class PrimaryButton extends Button { + PrimaryButton( + String text, { + super.key, + super.size, + super.onPressed, + super.onLongPressed, + super.leading, + super.leadingPosition, + }) : super(text, style: _style); +} + +class ButtonExample extends HookWidget { + const ButtonExample({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return PrimaryButton( + 'Primary Button', + onPressed: () => {}, + ); + } +} diff --git a/demo/macos/Podfile.lock b/demo/macos/Podfile.lock index aeacdfbb8..689c202d5 100644 --- a/demo/macos/Podfile.lock +++ b/demo/macos/Podfile.lock @@ -22,7 +22,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: desktop_window: fb7c4f12c1129f947ac482296b6f14059d57a3c3 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9 + path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7 diff --git a/demo/macos/Runner.xcodeproj/project.pbxproj b/demo/macos/Runner.xcodeproj/project.pbxproj index 1621f70a6..eef3ddc64 100644 --- a/demo/macos/Runner.xcodeproj/project.pbxproj +++ b/demo/macos/Runner.xcodeproj/project.pbxproj @@ -203,7 +203,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { diff --git a/demo/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/demo/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 6ee9022e4..15b420ad7 100644 --- a/demo/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/demo/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ =3.0.0 <4.0.0" - flutter: ">=3.3.0" + dart: ">=3.1.0-185.0.dev <4.0.0" + flutter: ">=3.7.0" diff --git a/lib/src/attributes/attribute.dart b/lib/src/attributes/attribute.dart index cac8c38e7..9c19f9d24 100644 --- a/lib/src/attributes/attribute.dart +++ b/lib/src/attributes/attribute.dart @@ -1,3 +1,7 @@ +import 'dart:math'; + +import 'package:flutter/foundation.dart'; + import '../helpers/equality_mixin/equality_mixin.dart'; /// Base attribute @@ -11,6 +15,25 @@ abstract class StyleAttribute with EqualityMixin { mixin Mergeable { T merge(covariant T? other); + + static List mergeLists( + List? list, + List? other, + ) { + if (other == null || other.isEmpty) return list ?? []; + if (list == null || list.isEmpty) return other; + + if (listEquals(list, other)) return list; + + final maxLength = max(list.length, other.length); + + return List.generate(maxLength, (int index) { + final otherValue = index < other.length ? other[index] : null; + final thisValue = index < list.length ? list[index] : null; + + return thisValue?.merge(otherValue) ?? otherValue!; + }); + } } /// An interface that add support to custom attributes for [MixContext]. diff --git a/lib/src/dtos/border/box_border.dto.dart b/lib/src/dtos/border/box_border.dto.dart index 245f46992..efb4960c6 100644 --- a/lib/src/dtos/border/box_border.dto.dart +++ b/lib/src/dtos/border/box_border.dto.dart @@ -4,8 +4,6 @@ import '../dto.dart'; import 'border.dto.dart'; import 'border_side.dto.dart'; -enum BoxBorderSide { top, bottom, left, right, start, end } - abstract class BoxBorderDto extends Dto { const BoxBorderDto(); diff --git a/lib/src/dtos/edge_insets/edge_insets_directional.dto.dart b/lib/src/dtos/edge_insets/edge_insets_directional.dto.dart index a9ab071f2..14def412f 100644 --- a/lib/src/dtos/edge_insets/edge_insets_directional.dto.dart +++ b/lib/src/dtos/edge_insets/edge_insets_directional.dto.dart @@ -66,7 +66,6 @@ class EdgeInsetsDirectionalDto double? get _start => start; double? get _end => end; - EdgeInsetsDirectionalDto copyWith({ double? top, diff --git a/lib/src/dtos/shadow/box_shadow.dto.dart b/lib/src/dtos/shadow/box_shadow.dto.dart index b3d4c6724..41252293c 100644 --- a/lib/src/dtos/shadow/box_shadow.dto.dart +++ b/lib/src/dtos/shadow/box_shadow.dto.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import '../../factory/mix_provider_data.dart'; -import '../../helpers/mergeable_list.dart'; import '../color.dto.dart'; import 'shadow.dto.dart'; @@ -66,13 +65,3 @@ class BoxShadowDto extends ShadowDto { @override get props => [color, offset, blurRadius, spreadRadius]; } - -extension BoxShadowDtoExt on List { - List resolve(MixData mix) { - return map((e) => e.resolve(mix)).toList(); - } - - List merge(List? other) { - return combineMergeableLists(this, other); - } -} diff --git a/lib/src/dtos/shadow/shadow.dto.dart b/lib/src/dtos/shadow/shadow.dto.dart index 3841306a4..90cec8004 100644 --- a/lib/src/dtos/shadow/shadow.dto.dart +++ b/lib/src/dtos/shadow/shadow.dto.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import '../../../mix.dart'; -import '../../helpers/mergeable_list.dart'; import '../color.dto.dart'; import '../dto.dart'; @@ -65,13 +64,3 @@ class ShadowDto extends Dto with Mergeable { @override get props => [color, offset, blurRadius]; } - -extension ShadowDtoExt on List { - List resolve(MixData mix) { - return map((e) => e.resolve(mix)).toList(); - } - - List merge(List? other) { - return combineMergeableLists(this, other); - } -} diff --git a/lib/src/dtos/text_style.dto.dart b/lib/src/dtos/text_style.dto.dart index b6d5c90fb..d6bee4e5c 100644 --- a/lib/src/dtos/text_style.dto.dart +++ b/lib/src/dtos/text_style.dto.dart @@ -177,7 +177,7 @@ class TextStyleDto extends Dto { textBaseline: textBaseline ?? this.textBaseline, color: color ?? this.color, backgroundColor: backgroundColor ?? this.backgroundColor, - shadows: this.shadows?.merge(shadows) ?? shadows, + shadows: Mergeable.mergeLists(this.shadows, shadows), fontFeatures: fontFeatures ?? this.fontFeatures, decoration: decoration ?? this.decoration, decorationColor: decorationColor ?? this.decorationColor, diff --git a/lib/src/factory/mix_provider_data.dart b/lib/src/factory/mix_provider_data.dart index 9a0dd4a6d..78646c233 100644 --- a/lib/src/factory/mix_provider_data.dart +++ b/lib/src/factory/mix_provider_data.dart @@ -1,3 +1,4 @@ +// Necessary packages are imported at the start of the file. import 'package:flutter/material.dart'; import '../attributes/attribute.dart'; @@ -9,14 +10,22 @@ import '../variants/variant_attribute.dart'; import 'style_mix.dart'; import 'style_mix_data.dart'; +/// This refers to the deprecated class MixData and it's here for the purpose of maintaining compatibility. @Deprecated('Use MixData instead.') typedef MixContext = MixData; +/// This class is used for encapsulating all [MixData] related operations. +/// It contains a mixture of properties and methods useful for handling different attributes, +/// decorators and token resolvers. class MixData with EqualityMixin { + // instance variables for widget attributes, widget decorators and token resolver. final MergeableMap _widgetAttributes; final MergeableMap _widgetDecorators; final MixTokenResolver _tokenResolver; + /// A Private constructor for the [MixData] class that initializes its main variables. + /// + /// It takes in [widgetAttributes], [widgetDecorators] and [tokenResolver] as required parameters. MixData._({ required MergeableMap widgetAttributes, required MergeableMap widgetDecorators, @@ -25,6 +34,8 @@ class MixData with EqualityMixin { _widgetDecorators = widgetDecorators, _tokenResolver = tokenResolver; + /// Factory constructor that uses the provided [context] and [style] + /// to create an instance of [MixData]. factory MixData.create({ required BuildContext context, required StyleMix style, @@ -46,14 +57,20 @@ class MixData with EqualityMixin { final combinedValues = currentValues.merge(StyleMixData.create(attributes)); return MixData._( - widgetAttributes: combinedValues.attributes ?? MergeableMap([]), - widgetDecorators: combinedValues.decorators ?? MergeableMap([]), + widgetAttributes: combinedValues.attributes ?? const MergeableMap.empty(), + widgetDecorators: combinedValues.decorators ?? const MergeableMap.empty(), tokenResolver: MixTokenResolver(context), ); } + /// Getter method for [MixTokenResolver]. + /// + /// Returns current [_tokenResolver]. MixTokenResolver get resolveToken => _tokenResolver; + /// Internal method to handle application of context variants to [attributes]. + /// + /// It accepts the [context] and the list of [attributes] as parameters. static List _applyContextVariants( BuildContext context, Iterable attributes, @@ -84,31 +101,44 @@ class MixData with EqualityMixin { ); } - /// Returns an instance of the specified [StyledWidgetAttributes] type from the [MixData]. + /// Retrieves an instance of the specified [StyledWidgetAttributes] type from the [MixData]. + /// + /// Accepts a type parameter [A] which extends [StyledWidgetAttributes]. + /// Returns the instance of type [A] if found, else returns null. A? attributesOfType() { - return _widgetAttributes[A] as A?; + return _widgetAttributes.get(A) as A?; } + /// Retrieves an instance of attributes based on the type provided. + /// + /// The type [T] here refers to the type extending [StyledWidgetAttributes]. + /// An exception is thrown if no attribute of the required type is found. T dependOnAttributesOfType() { final attribute = attributesOfType(); if (attribute is! T) { - throw ''' - No $T could be found starting from MixContext - when call mixContext.attributesOfType<$T>(). This can happen because you - have not create a Mix with $T. - '''; + throw Exception( + 'No $T could be found starting from MixContext ' + 'when call mixContext.attributesOfType<$T>(). This can happen because you ' + 'have not create a Mix with $T.', + ); } return attribute; } + /// A getter method for [_widgetDecorators]. + /// + /// Returns a list of all [WidgetDecorator]. List? get decorators { return _widgetDecorators.values.toList(); } /// This is similar to a merge behavior however it prioritizes the current properties /// over the other properties, essentially using [MixData] `other` as the base for this instance. + /// + /// [other] is the other [MixData] instance that is to be merged with current instance. + /// Returns a new instance of [MixData] which is actually a merge of current and other instance. MixData inheritFrom(MixData other) { return MixData._( widgetAttributes: other._widgetAttributes.merge(_widgetAttributes), @@ -117,6 +147,9 @@ class MixData with EqualityMixin { ); } + /// Overrides the getter function of [props] from [EqualityMixin] to specify properties necessary for distinguishing instances. + /// + /// Returns a list of properties [_widgetAttributes] & [_widgetDecorators]. @override get props => [_widgetAttributes, _widgetDecorators]; } diff --git a/lib/src/factory/style_mix_data.dart b/lib/src/factory/style_mix_data.dart index 58de40fc0..118c6f0d6 100644 --- a/lib/src/factory/style_mix_data.dart +++ b/lib/src/factory/style_mix_data.dart @@ -41,8 +41,8 @@ class StyleMixData with EqualityMixin { } return StyleMixData( - attributes: MergeableMap(attributeList), - decorators: MergeableMap(decoratorList), + attributes: MergeableMap.fromList(attributeList), + decorators: MergeableMap.fromList(decoratorList), variants: variantList, contextVariants: contextVariantList, ); @@ -64,7 +64,7 @@ class StyleMixData with EqualityMixin { /// Returns an instance of the specified [StyledWidgetAttributes] type from the [MixData]. A? attributesOfType() { - return attributes?[A] as A?; + return attributes?.get(A) as A?; } /// Returns an [Iterable] of [StyleAttribute]s containing all attributes, variants, and directives. diff --git a/lib/src/helpers/mergeable_list.dart b/lib/src/helpers/mergeable_list.dart deleted file mode 100644 index 8ba40f458..000000000 --- a/lib/src/helpers/mergeable_list.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/foundation.dart'; - -import '../attributes/attribute.dart'; - -List combineMergeableLists( - List list, - List? other, -) { - if (other == null || listEquals(other, list)) return list; - - final maxLength = max(list.length, other.length); - - return List.generate(maxLength, (int index) { - final otherValue = index < other.length ? other[index] : null; - final thisValue = index < list.length ? list[index] : null; - - return thisValue?.merge(otherValue) ?? otherValue!; - }); -} diff --git a/lib/src/helpers/mergeable_map.dart b/lib/src/helpers/mergeable_map.dart index c2de81a04..e3af01eec 100644 --- a/lib/src/helpers/mergeable_map.dart +++ b/lib/src/helpers/mergeable_map.dart @@ -1,123 +1,129 @@ -import 'dart:collection'; +import 'package:flutter/foundation.dart'; import '../attributes/attribute.dart'; -/// A map-like data structure that can merge values with the same key based on -/// their runtime type. Maintains insertion order of keys. +/// Represents a map that can merge values of type [Mergeable] based on their runtime type. +/// Maintains the insertion order of keys. /// -/// This is used to merge different attributes. +/// The type parameter [T] must extend [Mergeable], ensuring that the values can be merged. +/// +/// This class is useful for scenarios where you need to merge objects based on their types +/// while preserving the order in which they were inserted. +@immutable class MergeableMap { - final LinkedHashMap _map = LinkedHashMap(); + // Internal list to hold the keys to maintain insertion order + final List _keys; + + // Internal map to associate keys to values + final Map _map; - /// Creates a new [MergeableMap] instance with the given [iterable]. + /// Private constructor used by factory methods. /// - /// The optional [keyMapper] function is used to map the iterable items to - /// their respective keys. If not provided, the runtime type of the item is - /// used as the key. - MergeableMap(List iterable) { + /// Both [_keys] and [_map] must not be null. + const MergeableMap._(this._keys, this._map); + + /// Factory constructor to create an empty [MergeableMap]. + /// + /// Useful for initializing an empty map. + const MergeableMap.empty() + : _keys = const [], + _map = const {}; + + /// Creates a [MergeableMap] instance from an iterable of type [T]. + /// + /// Iterates through each item in the [iterable], merging items of the same type. + factory MergeableMap.fromList(List iterable) { + final keys = []; + final map = {}; for (final item in iterable) { - _update(item); + final key = item.runtimeType; + if (map.containsKey(key)) { + map[key] = map[key]!.merge(item); + } else { + keys.add(key); + map[key] = item; + } } - } - /// Creates an empty [MergeableMap] instance. - factory MergeableMap.empty() { - return MergeableMap([]); + return MergeableMap._(keys, map); } - void _update(T value) { - _map.update( - value.runtimeType, - (existingValue) => existingValue.merge(value), - ifAbsent: () => value, - ); - } - - /// Retrieves the value associated with the given [key] or `null` if not found. - T? operator [](Type key) => _map[key]; - - /// Associates the [value] with the given [key]. If the key already exists, - /// the value is merged with the existing value. - void operator []=(Type key, covariant T value) { - _update(value); - } + /// Retrieves the value associated with the given [key]. + /// + /// Returns `null` if the [key] is not found. + T? get(Type key) => _map[key]; - /// Merges this [MergeableMap] with another [MergeableMap] and returns a new - /// [MergeableMap] containing the merged elements. + /// Merges this [MergeableMap] with another [MergeableMap]. /// - /// If the `other` map is `null`, it returns a new [MergeableMap] that is - /// identical to the current map. If the `other` map is not `null`, it - /// iterates over the keys of both maps and creates a new [MergeableMap] - /// containing the union of both maps. In case of overlapping keys, the value - /// from the `other` map will overwrite the value from the current map. + /// Returns a new instance containing the merged elements. + /// If [other] is null, returns the current instance. MergeableMap merge(MergeableMap? other) { if (other == null) return this; - final mergedMap = MergeableMap.empty(); - mergedMap._map.addAll(_map); - other._map.forEach((key, value) { - mergedMap[key] = value; - }); + final mergedKeys = List.from(_keys); + final mergedMap = Map.from(_map); - return mergedMap; + for (var key in other._keys) { + if (mergedMap.containsKey(key)) { + mergedMap[key] = mergedMap[key]!.merge(other._map[key]!); + } else { + mergedKeys.add(key); + mergedMap[key] = other._map[key]!; + } + } + + return MergeableMap._(mergedKeys, mergedMap); } - /// Creates a new [MergeableMap] instance with the same key-value pairs as - /// this map. + /// Creates a new [MergeableMap] instance identical to this instance. + /// + /// This method is typically used when a copy of the map is needed, so the original + /// map can be preserved while the copy is manipulated. MergeableMap clone() { - final clonedMap = MergeableMap.empty(); - clonedMap._map.addAll(_map); - - return clonedMap; + return MergeableMap._(List.from(_keys), Map.from(_map)); } - /// Returns an [Iterable] of the values in the map in insertion order. - Iterable get values => _map.values; - - /// Returns the number of key-value pairs in the map. - int get length => _map.length; - - /// Removes all key-value pairs from the map. - void clear() { - _map.clear(); - } + /// Returns an iterable of the values in the map, maintaining the order of insertion. + /// + /// The order of return is determined by the order the key/value pairs were inserted in the map. + Iterable get values => _keys.map((key) => _map[key]!); - T firstWhere(bool Function(T) test) { - return _map.values.firstWhere(test); - } + /// Returns the number of key/value pairs in the map. + /// + /// A length of zero means the map is empty. + int get length => _keys.length; - /// Returns `true` if the map contains no key-value pairs. - bool get isEmpty => _map.isEmpty; + /// Finds the first value in the map that satisfies the given [test]. + /// + /// This is a simplified way to find a value in the map without iterating manually. + /// + /// The [test] function should return `true` for the desired value. + T firstWhere(bool Function(T) test) => values.firstWhere(test); - /// Returns `true` if the map contains at least one key-value pair. + /// Returns `true` if the map contains no key/value pairs. + /// + /// This is a quick way to verify if the map is empty or not. + bool get isEmpty => _keys.isEmpty; - bool get isNotEmpty => _map.isNotEmpty; + /// Returns `true` if the map contains at least one key/value pair. + /// + /// This is a quick way to verify if the map has stored values. + bool get isNotEmpty => _keys.isNotEmpty; - /// Determines the equality of two [MergeableMap] instances. + /// Overrides the equality operator to compare [MergeableMap] instances. /// - /// Returns true if both instances have the same key-value pairs and - /// insertion order. + /// Two instances are considered equal if they have identical keys and values. + /// This checks the length of keys and then verifies that each key has identical values in both maps. @override bool operator ==(Object other) { if (identical(this, other)) return true; - if (other is MergeableMap) { - if (other._map.length != _map.length) return false; - - for (final key in _map.keys) { - if (!other._map.containsKey(key) || _map[key] != other._map[key]) { - return false; - } - } - - return true; - } - - return false; + return other is MergeableMap && + _keys.length == other._keys.length && + _keys.every((key) => + other._map.containsKey(key) && _map[key] == other._map[key]); } - /// Generates the hash code for this [MergeableMap] instance based on its - /// key-value pairs. @override - int get hashCode => _map.hashCode; + int get hashCode => _keys.hashCode ^ _map.hashCode; } diff --git a/lib/src/variants/context_variant.dart b/lib/src/variants/context_variant.dart index 60edce133..381812ff8 100644 --- a/lib/src/variants/context_variant.dart +++ b/lib/src/variants/context_variant.dart @@ -61,4 +61,4 @@ class ContextStyleVariant extends StyleVariant { } get props => [name, _inverse, _shouldApply]; -} \ No newline at end of file +} diff --git a/lib/src/variants/variant.dart b/lib/src/variants/variant.dart index 5f47a2bd1..67bdb807b 100644 --- a/lib/src/variants/variant.dart +++ b/lib/src/variants/variant.dart @@ -30,6 +30,7 @@ class StyleVariant { /// Applies the variant to a set of attributes and creates a [VariantAttribute] instance. /// Up to 12 optional [StyleAttribute] parameters can be provided. // ignore: long-parameter-list + VariantAttribute call([ StyleAttribute? p1, StyleAttribute? p2, diff --git a/lib/src/widgets/container/container.attributes.dart b/lib/src/widgets/container/container.attributes.dart index 593936cf4..fb7bc076b 100644 --- a/lib/src/widgets/container/container.attributes.dart +++ b/lib/src/widgets/container/container.attributes.dart @@ -74,7 +74,7 @@ class StyledContainerAttributes extends StyledWidgetAttributes { // Mergeble values border: this.border?.merge(border) ?? border, borderRadius: this.borderRadius?.merge(borderRadius) ?? borderRadius, - boxShadow: this.boxShadow?.merge(boxShadow) ?? boxShadow, + boxShadow: Mergeable.mergeLists(this.boxShadow, boxShadow), margin: this.margin?.merge(margin) ?? margin, padding: this.padding?.merge(padding) ?? padding, transform: this.transform?.merge(transform) ?? transform, diff --git a/lib/src/widgets/container/container.descriptor.dart b/lib/src/widgets/container/container.descriptor.dart index 6ad9bd387..034a43dda 100644 --- a/lib/src/widgets/container/container.descriptor.dart +++ b/lib/src/widgets/container/container.descriptor.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import '../../../mix.dart'; -import '../../dtos/shadow/box_shadow.dto.dart'; import '../../helpers/equality_mixin/equality_mixin.dart'; class StyledContainerDescriptor with EqualityMixin { @@ -57,7 +56,7 @@ class StyledContainerDescriptor with EqualityMixin { height: attributes?.height, border: attributes?.border?.resolve(mix), borderRadius: attributes?.borderRadius?.resolve(mix), - boxShadow: attributes?.boxShadow?.resolve(mix), + boxShadow: attributes?.boxShadow?.map((e) => e.resolve(mix)).toList(), maxHeight: attributes?.maxHeight, maxWidth: attributes?.maxWidth, minHeight: attributes?.minHeight, diff --git a/lib/src/widgets/container/container.utilities.dart b/lib/src/widgets/container/container.utilities.dart index 57510eee1..cdc0036ed 100644 --- a/lib/src/widgets/container/container.utilities.dart +++ b/lib/src/widgets/container/container.utilities.dart @@ -148,7 +148,8 @@ class ContainerStyleUtilities { StyledContainerAttributes paddingInsets(EdgeInsetsGeometry insets) { return StyledContainerAttributes( - padding: EdgeInsetsGeometryDto.from(insets),); + padding: EdgeInsetsGeometryDto.from(insets), + ); } StyledContainerAttributes paddingTop(double value) { diff --git a/lib/src/widgets/text/text.utilities.dart b/lib/src/widgets/text/text.utilities.dart index 63443d29c..4be7b3198 100644 --- a/lib/src/widgets/text/text.utilities.dart +++ b/lib/src/widgets/text/text.utilities.dart @@ -1,3 +1,5 @@ +// ignore_for_file: long-parameter-list + import 'dart:ui'; import 'package:flutter/material.dart'; @@ -11,7 +13,6 @@ import 'text_directives/text_directives.dart'; class TextUtility { const TextUtility._(); - // ignore: long-parameter-list static StyledTextAttributes textStyle({ String? fontFamily, FontWeight? fontWeight, diff --git a/pubspec.lock b/pubspec.lock index 9a953ae85..d208bb106 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -189,10 +189,18 @@ packages: dependency: "direct dev" description: name: dart_code_metrics - sha256: "219607f5abbf4c0d254ca39ee009f9ff28df91c40aef26718fde15af6b7a6c24" + sha256: "3dede3f7abc077a4181ec7445448a289a9ce08e2981e6a4d49a3fb5099d47e1f" url: "https://pub.dev" source: hosted - version: "4.21.3" + version: "5.7.6" + dart_code_metrics_presets: + dependency: transitive + description: + name: dart_code_metrics_presets + sha256: b71eadf02a3787ebd5c887623f83f6fdc204d45c75a081bd636c4104b3fd8b73 + url: "https://pub.dev" + source: hosted + version: "1.8.0" dart_style: dependency: transitive description: @@ -431,10 +439,10 @@ packages: dependency: transitive description: name: pub_updater - sha256: "42890302ab2672adf567dc2b20e55b4ecc29d7e19c63b6b98143ab68dd717d3a" + sha256: "05ae70703e06f7fdeb05f7f02dd680b8aad810e87c756a618f33e1794635115c" url: "https://pub.dev" source: hosted - version: "0.2.4" + version: "0.3.0" pubspec_parse: dependency: transitive description: @@ -544,6 +552,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + uuid: + dependency: transitive + description: + name: uuid + sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + url: "https://pub.dev" + source: hosted + version: "3.0.7" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 1d80b3da4..3e917f9bb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ version: 0.0.7 homepage: https://github.com/leoafarias/mix environment: - sdk: ">=2.17.0 <3.0.0" + sdk: ">=2.17.0 <4.0.0" flutter: ">=3.3.0" dependencies: @@ -14,7 +14,7 @@ dependencies: dev_dependencies: build_runner: ^2.3.2 test_cov_console: ^0.2.2 - flutter_lints: ^2.0.1 + flutter_lints: ^2.0.3 flutter_test: sdk: flutter - dart_code_metrics: ^4.21.2 + dart_code_metrics: ^5.7.6 diff --git a/test/helpers/mergeable_map_test.dart b/test/helpers/mergeable_map_test.dart new file mode 100644 index 000000000..6b7eb73dd --- /dev/null +++ b/test/helpers/mergeable_map_test.dart @@ -0,0 +1,44 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mix/src/attributes/exports.dart'; +import 'package:mix/src/helpers/mergeable_map.dart'; + +class MergeableInt with Mergeable { + final int value; + + MergeableInt(this.value); + + @override + MergeableInt merge(MergeableInt? other) { + return MergeableInt(value + (other?.value ?? 0)); + } +} + +void main() { + test('Initialization', () { + const map = MergeableMap.empty(); + expect(map.isEmpty, true); + }); + + test('FromList Constructor', () { + final map = MergeableMap.fromList([MergeableInt(1), MergeableInt(2)]); + expect(map.length, 2); + // Further verification. + }); + + test('Merge Functionality', () { + final map1 = MergeableMap.fromList([MergeableInt(1)]); + final map2 = MergeableMap.fromList([MergeableInt(2)]); + final merged = map1.merge(map2); + }); + + test('Insertion Order', () { + // Create and merge maps + // Check if the key order is maintained + }); + + test('Clone Functionality', () { + final map = MergeableMap.fromList([MergeableInt(1)]); + final clone = map.clone(); + expect(clone, map); + }); +} diff --git a/test/widgets/box_test.dart b/test/widgets/box_test.dart index 2029ed8bb..be4c1771c 100644 --- a/test/widgets/box_test.dart +++ b/test/widgets/box_test.dart @@ -207,7 +207,8 @@ void main() { StyleMix( const StyledContainerAttributes(color: ColorDto(Colors.purple)), const StyledContainerAttributes( - borderRadius: borderRadiusProps,), + borderRadius: borderRadiusProps, + ), StyledContainerAttributes(border: borderProps), ), ),