diff --git a/lib/mix.dart b/lib/mix.dart index 0db6fc50e..fe133a4ba 100644 --- a/lib/mix.dart +++ b/lib/mix.dart @@ -57,6 +57,7 @@ export 'src/specs/icon/icon_widget.dart'; export 'src/specs/image/image_attribute.dart'; export 'src/specs/image/image_spec.dart'; export 'src/specs/image/image_util.dart'; +export 'src/specs/image/image_widget.dart'; export 'src/specs/stack/stack_attribute.dart'; export 'src/specs/stack/stack_spec.dart'; export 'src/specs/stack/stack_util.dart'; diff --git a/lib/src/attributes/scalars/scalar_util.dart b/lib/src/attributes/scalars/scalar_util.dart index 57a121ef9..a1e7c896a 100644 --- a/lib/src/attributes/scalars/scalar_util.dart +++ b/lib/src/attributes/scalars/scalar_util.dart @@ -443,10 +443,12 @@ class FontFamilyUtility /// ``` /// See [ImageRepeat] for more information. class ImageRepeatUtility - extends ScalarUtility { + extends MixUtility { const ImageRepeatUtility(super.builder); + + T call() => builder(ImageRepeat.repeat); + T noRepeat() => builder(ImageRepeat.noRepeat); - T repeat() => builder(ImageRepeat.repeat); T repeatX() => builder(ImageRepeat.repeatX); T repeatY() => builder(ImageRepeat.repeatY); } @@ -782,3 +784,38 @@ class TextAlignUtility T start() => _builder(TextAlign.start); T end() => _builder(TextAlign.end); } + +class RectUtility extends ScalarUtility { + const RectUtility(super.builder); + T largest() => _builder(Rect.largest); + T zero() => _builder(Rect.zero); + + T fromCenter({ + required Offset center, + required double width, + required double height, + }) => + _builder(Rect.fromCenter(center: center, width: width, height: height)); + + T fromLTRB(double left, double top, double right, double bottom) => + _builder(Rect.fromLTRB(left, top, right, bottom)); + + T fromLTWH(double left, double top, double width, double height) => + _builder(Rect.fromLTWH(left, top, width, height)); + + T fromCircle({required Offset center, required double radius}) => + _builder(Rect.fromCircle(center: center, radius: radius)); + + T fromPoints({required Offset a, required Offset b}) => + _builder(Rect.fromPoints(a, b)); +} + +class FilterQualityUtility + extends ScalarUtility { + const FilterQualityUtility(super.builder); + + T none() => _builder(FilterQuality.none); + T low() => _builder(FilterQuality.low); + T medium() => _builder(FilterQuality.medium); + T high() => _builder(FilterQuality.high); +} diff --git a/lib/src/specs/image/image_attribute.dart b/lib/src/specs/image/image_attribute.dart index d4d5bb26f..14bcaf1b9 100644 --- a/lib/src/specs/image/image_attribute.dart +++ b/lib/src/specs/image/image_attribute.dart @@ -11,13 +11,21 @@ class ImageSpecAttribute extends SpecAttribute { final ColorDto? color; final ImageRepeat? repeat; final BoxFit? fit; + final AlignmentGeometry? alignment; + final Rect? centerSlice; + final FilterQuality? filterQuality; + final BlendMode? colorBlendMode; const ImageSpecAttribute({ + this.centerSlice, this.width, this.height, this.color, this.repeat, this.fit, + this.alignment, + this.colorBlendMode, + this.filterQuality, }); @override @@ -28,6 +36,10 @@ class ImageSpecAttribute extends SpecAttribute { color: color?.resolve(mix), repeat: repeat, fit: fit, + alignment: alignment, + centerSlice: centerSlice, + filterQuality: filterQuality, + colorBlendMode: colorBlendMode, ); } @@ -41,9 +53,23 @@ class ImageSpecAttribute extends SpecAttribute { color: other.color ?? color, repeat: other.repeat ?? repeat, fit: other.fit ?? fit, + alignment: other.alignment ?? alignment, + centerSlice: other.centerSlice ?? centerSlice, + filterQuality: other.filterQuality ?? filterQuality, + colorBlendMode: other.colorBlendMode ?? colorBlendMode, ); } @override - get props => [width, height, color, repeat, fit]; + get props => [ + width, + height, + color, + repeat, + fit, + centerSlice, + alignment, + filterQuality, + colorBlendMode, + ]; } diff --git a/lib/src/specs/image/image_spec.dart b/lib/src/specs/image/image_spec.dart index d1534a72f..eddadf80a 100644 --- a/lib/src/specs/image/image_spec.dart +++ b/lib/src/specs/image/image_spec.dart @@ -5,6 +5,7 @@ import 'package:flutter/widgets.dart'; import '../../core/attribute.dart'; import '../../factory/mix_provider_data.dart'; +import '../../helpers/lerp_helpers.dart'; import 'image_attribute.dart'; @immutable @@ -13,6 +14,10 @@ class ImageSpec extends Spec { final Color? color; final ImageRepeat? repeat; final BoxFit? fit; + final AlignmentGeometry? alignment; + final Rect? centerSlice; + final FilterQuality? filterQuality; + final BlendMode? colorBlendMode; const ImageSpec({ required this.width, @@ -20,6 +25,10 @@ class ImageSpec extends Spec { required this.color, required this.repeat, required this.fit, + required this.alignment, + required this.centerSlice, + required this.filterQuality, + required this.colorBlendMode, }); const ImageSpec.empty() @@ -27,12 +36,15 @@ class ImageSpec extends Spec { height = null, color = null, repeat = null, + alignment = null, + centerSlice = null, + filterQuality = FilterQuality.none, + colorBlendMode = BlendMode.clear, fit = null; - static ImageSpec resolve(MixData mix) { - final recipe = mix.attributeOf()?.resolve(mix); - - return recipe ?? const ImageSpecAttribute().resolve(mix); + static ImageSpec of(MixData mix) { + return mix.attributeOf()?.resolve(mix) ?? + const ImageSpec.empty(); } @override @@ -41,8 +53,12 @@ class ImageSpec extends Spec { width: lerpDouble(width, other?.width, t), height: lerpDouble(height, other?.height, t), color: Color.lerp(color, other?.color, t), - repeat: t < 0.5 ? repeat : other?.repeat, - fit: t < 0.5 ? fit : other?.fit, + centerSlice: lerpSnap(centerSlice, other?.centerSlice, t), + repeat: lerpSnap(repeat, other?.repeat, t), + fit: lerpSnap(fit, other?.fit, t), + filterQuality: lerpSnap(filterQuality, other?.filterQuality, t), + colorBlendMode: lerpSnap(colorBlendMode, other?.colorBlendMode, t), + alignment: AlignmentGeometry.lerp(alignment, other?.alignment, t), ); } @@ -54,6 +70,10 @@ class ImageSpec extends Spec { Color? color, ImageRepeat? repeat, BoxFit? fit, + AlignmentGeometry? alignment, + Rect? centerSlice, + FilterQuality? filterQuality, + BlendMode? colorBlendMode, }) { return ImageSpec( width: width ?? this.width, @@ -61,9 +81,23 @@ class ImageSpec extends Spec { color: color ?? this.color, repeat: repeat ?? this.repeat, fit: fit ?? this.fit, + centerSlice: centerSlice ?? this.centerSlice, + alignment: alignment ?? this.alignment, + filterQuality: filterQuality ?? this.filterQuality, + colorBlendMode: colorBlendMode ?? this.colorBlendMode, ); } @override - get props => [width, height, color, repeat, fit]; + get props => [ + width, + height, + color, + repeat, + fit, + centerSlice, + alignment, + filterQuality, + colorBlendMode, + ]; } diff --git a/lib/src/specs/image/image_util.dart b/lib/src/specs/image/image_util.dart index e804265a5..739c02f82 100644 --- a/lib/src/specs/image/image_util.dart +++ b/lib/src/specs/image/image_util.dart @@ -16,6 +16,10 @@ class ImageUtility extends SpecUtility { ColorDto? color, ImageRepeat? repeat, BoxFit? fit, + AlignmentGeometry? alignment, + Rect? centerSlice, + BlendMode? blendMode, + FilterQuality? filterQuality, }) { return ImageSpecAttribute( width: width, @@ -23,6 +27,10 @@ class ImageUtility extends SpecUtility { color: color, repeat: repeat, fit: fit, + alignment: alignment, + centerSlice: centerSlice, + colorBlendMode: blendMode, + filterQuality: filterQuality, ); } @@ -45,4 +53,21 @@ class ImageUtility extends SpecUtility { DoubleUtility get height { return DoubleUtility((height) => _only(height: height)); } + + AlignmentUtility get alignment { + return AlignmentUtility((alignment) => _only(alignment: alignment)); + } + + RectUtility get centerSlice { + return RectUtility((rect) => _only(centerSlice: rect)); + } + + FilterQualityUtility get filterQuality { + return FilterQualityUtility( + (filterQuality) => _only(filterQuality: filterQuality)); + } + + BlendModeUtility get blendMode { + return BlendModeUtility((blendMode) => _only(blendMode: blendMode)); + } } diff --git a/lib/src/specs/image/image_widget.dart b/lib/src/specs/image/image_widget.dart new file mode 100644 index 000000000..49bbcd96c --- /dev/null +++ b/lib/src/specs/image/image_widget.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; + +import '../../core/styled_widget.dart'; +import '../../factory/mix_provider.dart'; +import '../../factory/mix_provider_data.dart'; +import '../../utils/helper_util.dart'; +import 'image_spec.dart'; + +class StyledImage extends StyledWidget { + final ImageProvider image; + final ImageFrameBuilder? frameBuilder; + final ImageLoadingBuilder? loadingBuilder; + final ImageErrorWidgetBuilder? errorBuilder; + final String? semanticLabel; + final bool excludeFromSemantics; + + const StyledImage({ + super.key, + super.style, + super.inherit = true, + this.frameBuilder, + this.loadingBuilder, + this.errorBuilder, + this.semanticLabel, + this.excludeFromSemantics = false, + required this.image, + }); + + @override + Widget build(BuildContext context) { + return withMix(context, (mix) { + return MixedImage( + image: image, + errorBuilder: errorBuilder, + excludeFromSemantics: excludeFromSemantics, + frameBuilder: frameBuilder, + loadingBuilder: loadingBuilder, + semanticLabel: semanticLabel, + ); + }); + } +} + +class MixedImage extends StatelessWidget { + const MixedImage({ + super.key, + this.decoratorOrder = const [], + this.mix, + required this.image, + this.frameBuilder, + this.loadingBuilder, + this.errorBuilder, + this.semanticLabel, + this.excludeFromSemantics = false, + }); + + final MixData? mix; + final ImageProvider image; + final ImageFrameBuilder? frameBuilder; + final ImageLoadingBuilder? loadingBuilder; + final ImageErrorWidgetBuilder? errorBuilder; + final String? semanticLabel; + final bool excludeFromSemantics; + final List decoratorOrder; + + @override + Widget build(BuildContext context) { + final mix = this.mix ?? MixProvider.of(context); + final spec = ImageSpec.of(mix); + + final current = Image( + image: image, + frameBuilder: frameBuilder, + loadingBuilder: loadingBuilder, + errorBuilder: errorBuilder, + semanticLabel: semanticLabel, + excludeFromSemantics: excludeFromSemantics, + width: spec.width, + height: spec.height, + color: spec.color, + repeat: spec.repeat ?? ImageRepeat.noRepeat, + fit: spec.fit, + alignment: spec.alignment ?? Alignment.center, + centerSlice: spec.centerSlice, + filterQuality: spec.filterQuality ?? FilterQuality.none, + colorBlendMode: spec.colorBlendMode ?? BlendMode.clear, + ); + + return shouldApplyDecorators( + mix: mix, + orderOfDecorators: decoratorOrder, + child: current, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 5196db178..857e63c88 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -529,4 +529,4 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.2.0 <4.0.0" + dart: ">=3.2.0-194.0.dev <4.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 6a6c328cf..06f9a05e1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,3 +22,6 @@ dev_dependencies: mockito: ^5.4.2 meta: ^1.9.1 +flutter: + assets: + - test_resources/ diff --git a/test/src/attributes/scalars/scalar_util_test.dart b/test/src/attributes/scalars/scalar_util_test.dart index 2f8638347..030bdb04e 100644 --- a/test/src/attributes/scalars/scalar_util_test.dart +++ b/test/src/attributes/scalars/scalar_util_test.dart @@ -162,11 +162,11 @@ void main() { const utility = ImageRepeatUtility(UtilityTestAttribute.new); test('Properties are initialized correctly', () { expect(utility.noRepeat().value, isA()); - expect(utility.repeat().value, isA()); + expect(utility().value, isA()); expect(utility.repeatX().value, isA()); expect(utility.repeatY().value, isA()); expect(utility.noRepeat().value, ImageRepeat.noRepeat); - expect(utility.repeat().value, ImageRepeat.repeat); + expect(utility().value, ImageRepeat.repeat); expect(utility.repeatX().value, ImageRepeat.repeatX); expect(utility.repeatY().value, ImageRepeat.repeatY); }); diff --git a/test/src/specs/image/image_attribute_test.dart b/test/src/specs/image/image_attribute_test.dart index 7c83aafc0..2626f5f52 100644 --- a/test/src/specs/image/image_attribute_test.dart +++ b/test/src/specs/image/image_attribute_test.dart @@ -52,18 +52,25 @@ void main() { const attribute = ImageSpecAttribute( width: 100, height: 200, - color: ColorDto(Colors.red), + color: ColorDto(Colors.black), repeat: ImageRepeat.repeat, fit: BoxFit.cover, + alignment: Alignment.bottomCenter, + centerSlice: Rect.zero, + filterQuality: FilterQuality.low, + colorBlendMode: BlendMode.srcOver, ); final props = attribute.props; - expect(props.length, 5); expect(props[0], 100); expect(props[1], 200); - expect(props[2], const ColorDto(Colors.red)); + expect(props[2], const ColorDto(Colors.black)); expect(props[3], ImageRepeat.repeat); expect(props[4], BoxFit.cover); + expect(props[5], Rect.zero); + expect(props[6], Alignment.bottomCenter); + expect(props[7], FilterQuality.low); + expect(props[8], BlendMode.srcOver); }); }); } diff --git a/test/src/specs/image/image_spec_test.dart b/test/src/specs/image/image_spec_test.dart index 61300fd92..41a6cd681 100644 --- a/test/src/specs/image/image_spec_test.dart +++ b/test/src/specs/image/image_spec_test.dart @@ -9,7 +9,7 @@ import '../../../helpers/testing_utils.dart'; void main() { group('ImageSpec', () { test('resolve returns correct recipe', () { - final recipe = ImageSpec.resolve(EmptyMixData); + final recipe = ImageSpec.of(EmptyMixData); expect(recipe.width, null); expect(recipe.height, null); @@ -25,6 +25,10 @@ void main() { color: Colors.red, repeat: ImageRepeat.repeat, fit: BoxFit.cover, + alignment: Alignment.bottomCenter, + centerSlice: Rect.zero, + filterQuality: FilterQuality.low, + colorBlendMode: BlendMode.srcOver, ); const spec2 = ImageSpec( width: 150, @@ -32,6 +36,10 @@ void main() { color: Colors.blue, repeat: ImageRepeat.noRepeat, fit: BoxFit.fill, + alignment: Alignment.bottomCenter, + centerSlice: Rect.fromLTRB(0, 0, 0, 0), + filterQuality: FilterQuality.high, + colorBlendMode: BlendMode.colorBurn, ); final lerpSpec = spec1.lerp(spec2, 0.5); @@ -40,6 +48,10 @@ void main() { expect(lerpSpec.color, Color.lerp(Colors.red, Colors.blue, 0.5)); expect(lerpSpec.repeat, ImageRepeat.noRepeat); expect(lerpSpec.fit, BoxFit.fill); + expect(lerpSpec.alignment, Alignment.bottomCenter); + expect(lerpSpec.centerSlice, const Rect.fromLTRB(0, 0, 0, 0)); + expect(lerpSpec.filterQuality, FilterQuality.high); + expect(lerpSpec.colorBlendMode, BlendMode.colorBurn); }); test('copyWith returns correct ImageSpec', () { @@ -49,6 +61,10 @@ void main() { color: Colors.red, repeat: ImageRepeat.repeat, fit: BoxFit.cover, + alignment: Alignment.bottomCenter, + centerSlice: Rect.fromLTRB(0, 0, 0, 0), + filterQuality: FilterQuality.low, + colorBlendMode: BlendMode.srcOver, ); final copiedSpec = spec.copyWith( width: 150, @@ -56,6 +72,10 @@ void main() { color: Colors.blue, repeat: ImageRepeat.noRepeat, fit: BoxFit.fill, + alignment: Alignment.topCenter, + centerSlice: Rect.zero, + filterQuality: FilterQuality.none, + colorBlendMode: BlendMode.clear, ); expect(copiedSpec.width, 150); @@ -63,6 +83,10 @@ void main() { expect(copiedSpec.color, Colors.blue); expect(copiedSpec.repeat, ImageRepeat.noRepeat); expect(copiedSpec.fit, BoxFit.fill); + expect(copiedSpec.alignment, Alignment.topCenter); + expect(copiedSpec.centerSlice, Rect.zero); + expect(copiedSpec.filterQuality, FilterQuality.none); + expect(copiedSpec.colorBlendMode, BlendMode.clear); }); test('props returns correct list of properties', () { @@ -72,15 +96,24 @@ void main() { color: Colors.red, repeat: ImageRepeat.repeat, fit: BoxFit.cover, + alignment: Alignment.bottomCenter, + centerSlice: Rect.zero, + filterQuality: FilterQuality.low, + colorBlendMode: BlendMode.srcOver, ); + final props = spec.props; - expect(props.length, 5); + expect(props.length, 9); expect(props[0], 100); expect(props[1], 200); expect(props[2], Colors.red); expect(props[3], ImageRepeat.repeat); expect(props[4], BoxFit.cover); + expect(props[5], Rect.zero); + expect(props[6], Alignment.bottomCenter); + expect(props[7], FilterQuality.low); + expect(props[8], BlendMode.srcOver); }); }); } diff --git a/test/src/specs/image/image_widget_test.dart b/test/src/specs/image/image_widget_test.dart new file mode 100644 index 000000000..b6394c036 --- /dev/null +++ b/test/src/specs/image/image_widget_test.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mix/mix.dart'; + +void main() { + group('StyledImage', () { + TestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('receive all the attributes', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: StyledImage( + style: Style( + image.width(152), + image.height(152), + image.color.black(), + image.repeat(), + image.fit.fill(), + image.centerSlice.fromLTRB(1, 2, 3, 4), + image.alignment.bottomLeft(), + image.filterQuality.high(), + image.blendMode.colorDodge(), + ), + image: const AssetImage('test_resources/logo.png'), + ), + ), + ), + ); + + final imageWidget = tester.element(find.byType(Image)).widget as Image; + + expect(imageWidget.width, 152); + expect(imageWidget.height, 152); + expect(imageWidget.color, Colors.black); + expect(imageWidget.repeat, ImageRepeat.repeat); + expect(imageWidget.fit, BoxFit.fill); + expect(imageWidget.centerSlice, const Rect.fromLTRB(1, 2, 3, 4)); + expect(imageWidget.alignment, Alignment.bottomLeft); + expect(imageWidget.filterQuality, FilterQuality.high); + expect(imageWidget.colorBlendMode, BlendMode.colorDodge); + }); + + testWidgets('can receive a decorator', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: StyledImage( + style: Style( + image.width(152), + image.height(152), + opacity(0.5), + ), + image: const AssetImage('test_resources/logo.png'), + ), + ), + ), + ); + + final opacityWidget = + tester.element(find.byType(Opacity)).widget as Opacity; + + expect(opacityWidget.opacity, 0.5); + }); + + testWidgets('can inherit style from the parent StyledWidget', + (WidgetTester tester) async { + await tester.pumpWidget( + Box( + style: Style( + image.width(152), + image.height(152), + image.color.black(), + ), + child: const StyledImage( + image: AssetImage('test_resources/logo.png'), + ), + ), + ); + + final imageWidget = tester.element(find.byType(Image)).widget as Image; + + expect(imageWidget.width, 152); + expect(imageWidget.height, 152); + expect(imageWidget.color, Colors.black); + }); + }); +} diff --git a/test_resources/logo.png b/test_resources/logo.png new file mode 100644 index 000000000..157e9adf9 Binary files /dev/null and b/test_resources/logo.png differ diff --git a/website/pages/docs/widgets/icon.mdx b/website/pages/docs/widgets/icon.mdx index 5646800b0..3a67f1b51 100644 --- a/website/pages/docs/widgets/icon.mdx +++ b/website/pages/docs/widgets/icon.mdx @@ -18,7 +18,7 @@ StyledIcon( ## Inheritance -The **StyledIcon** widget has the `inherit` flag set to `true` by default. This means that the style attributes will be inherited from its closest `Mix` ancestor in the widget tree. +The **StyledIcon** widget has the `inherit` flag set to `true` by default. This means that the style attributes will be inherited from its closest `Style` ancestor in the widget tree. ```dart Box( diff --git a/website/pages/docs/widgets/image.mdx b/website/pages/docs/widgets/image.mdx index c52d49cfa..e1d4dde5a 100644 --- a/website/pages/docs/widgets/image.mdx +++ b/website/pages/docs/widgets/image.mdx @@ -6,8 +6,107 @@ import { Callout } from "nextra-theme-docs"; # StyleImage - - The StyledImage is in progress, if you would like to keep up to date with the progress. - +A wrapped `Image` widget that can be easily themed and styled using Mix style attributes. This simplifies the process of applying consistent and reusable styling across `Image` widgets. + +## Usage + +To use `StyledImage`, you need to pass the `ImageProvider` and apply the style using the `Style` class. + +```dart +StyledImage( + image: const AssetImage('assets/image.jpg'), + style: Style( + image.width(152), + image.height(152), + image.fit.fill(), + image.alignment.bottomLeft(), + image.blendMode.colorDodge(), + ), +), +``` + +## Inheritance + +The **StyledImage** widget has the `inherit` flag set to `true` by default. This means that the style attributes will be inherited from its closest `Style` ancestor in the widget tree. + +```dart +Box( + Style( + image.height(30), + image.color.blue(), + ), + child: StyledImage(image: const AssetImage('some_image.jpg')), +); +``` + +In the this example, the `StyledImage` widget will inherit the image height and color from the `Box` widget. However, remember that decorators attributes cannot be inherited. + +## Utilities + +The `image` constant alias is an instance of `ImageUtility`, which facilitates the construction of `ImageSpec` attributes. These attributes are used to style and manage properties of image widgets. + +#### **color** + +Change the color of the image: + +```dart +// Apply a color to the image +image.color.red(); +``` + +#### **width** +Specifies the width of the image. + +```dart +image.width(100); +``` + +#### **height** +Specifies the height of the image. + +```dart +image.height(100); +``` + +#### **repeat** +Determines how the image should be repeated over the space of the widget. + +```dart +image.repeat.repeat(); +``` + +#### **fit** +Defines how the image should be inscribed into the space allocated during layout. + +```dart +image.fit.cover(); +``` + +#### **alignment** +Specifies the alignment of the image within its bounds. + +```dart +image.alignment.center(); +``` + +#### **centerSlice** +Slices the image for nine-patch effects. + +```dart +image.centerSlice.fromLTWH(10, 10, 20, 20); +``` + +#### **blendMode** +Specifies the blending mode applied to the image's color and the underlying widget's color. + +```dart +image.blendMode.multiply(); +``` + +#### **filterQuality** +Determines the quality of the filtering operations applied to the image. + +```dart +image.filterQuality.high(); +``` -please follow the StyledImage issue. \ No newline at end of file