Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Create AttributeBridge #497

Closed
wants to merge 2 commits into from
Closed
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
41 changes: 41 additions & 0 deletions packages/mix/lib/src/core/styled_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'factory/mix_data.dart';
import 'factory/mix_provider.dart';
import 'factory/style_mix.dart';
import 'modifier.dart';
import 'spec.dart';
import 'widget_state/widget_state_controller.dart';

/// An abstract widget for applying custom styles.
Expand Down Expand Up @@ -150,3 +151,43 @@ class SpecBuilder extends StatelessWidget {
return current;
}
}

/// A widget that bridges a complex attribute to a simpler one.
///
/// [AttributeBridge] acts as a link between a complex attribute not directly
/// supported by another widget and simplifies it into a basic attribute that
/// can be used by a specific widget. This is particularly useful when creating
/// custom styled widgets.
class AttributeBridge<T extends SpecAttribute, V extends SpecAttribute>
extends StatelessWidget {
const AttributeBridge({
super.key,
required this.child,
required this.bridgeBuilder,
});

final V Function(T) bridgeBuilder;
final Widget child;

@override
Widget build(BuildContext context) {
final data = Mix.maybeOfInherited(context);
final attribute = data?.attributeOf<T>();

if (attribute == null) {
throw FlutterError.fromParts([
ErrorSummary('AttributeBridge requires that $T be non-null.'),
ErrorDescription('The $T was not found in the current Mix context.'),
ErrorHint(
'Make sure that the AttributeBridge is used within a Mix widget that provides the required attribute.',
),
]);
}

return SpecBuilder(
inherit: true,
style: Style(bridgeBuilder(attribute)),
builder: (context) => child,
);
}
}
100 changes: 100 additions & 0 deletions packages/mix/test/src/core/styled_widget_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -225,4 +225,104 @@ void main() {
);
});
});
group('AttributeBridge', () {
testWidgets('AttributeBridge passes correct attribute to child',
(tester) async {
const attr = _ComplexSpecAttribute(
label: TextSpecAttribute(textScaleFactor: 2),
);
await tester.pumpWidget(
Box(
style: Style(attr),
child: AttributeBridge<_ComplexSpecAttribute, TextSpecAttribute>(
bridgeBuilder: (attr) => attr.label!,
child: Builder(
builder: (context) {
final mix = Mix.of(context);
mix.attributes.length;

expect(mix.attributes.length, 2);
expect(mix.attributeOf<_ComplexSpecAttribute>(), equals(attr));
expect(
mix.attributeOf<TextSpecAttribute>(), equals(attr.label));

return const SizedBox();
},
),
),
),
);
});

testWidgets('AttributeBridge handles null attribute with an exception',
(tester) async {
await tester.pumpWidget(
AttributeBridge<_ComplexSpecAttribute, TextSpecAttribute>(
bridgeBuilder: (a) => a.label!,
child: const SizedBox(),
),
);
expect(tester.takeException(), isA<FlutterError>());
});
});
}

base class _ComplexSpecAttribute extends SpecAttribute<_ComplexSpec> {
final TextSpecAttribute? label;

const _ComplexSpecAttribute({
this.label,
super.animated,
super.modifiers,
});

@override
_ComplexSpec resolve(MixData mix) {
return _ComplexSpec(
label: label?.resolve(mix),
animated: animated?.resolve(mix) ?? mix.animation,
modifiers: modifiers?.resolve(mix),
);
}

@override
_ComplexSpecAttribute merge(covariant _ComplexSpecAttribute? other) {
if (other == null) return this;

return _ComplexSpecAttribute(
label: label?.merge(other.label) ?? other.label,
animated: animated?.merge(other.animated) ?? other.animated,
modifiers: modifiers?.merge(other.modifiers) ?? other.modifiers,
);
}

@override
List<Object?> get props => [
label,
animated,
modifiers,
];
}

base class _ComplexSpec extends Spec<_ComplexSpec> {
final TextSpec label;

const _ComplexSpec({
TextSpec? label,
super.animated,
super.modifiers,
}) : label = label ?? const TextSpec();

@override
_ComplexSpec copyWith() {
throw UnimplementedError();
}

@override
_ComplexSpec lerp(covariant _ComplexSpec? other, double t) {
throw UnimplementedError();
}

@override
List<Object?> get props => throw UnimplementedError();
}
2 changes: 2 additions & 0 deletions packages/remix/demo/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/

# IntelliJ related
Expand Down
6 changes: 4 additions & 2 deletions packages/remix/demo/lib/components/divider_use_case.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:flutter/widgets.dart';
import 'package:mix/mix.dart';
import 'package:remix/remix.dart';
import 'package:widgetbook/widgetbook.dart';
import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;
Expand All @@ -10,14 +11,15 @@ import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook;
Widget buildDivider(BuildContext context) {
return Center(
child: SizedBox(
height: 100,
width: 100,
height: 200,
width: 200,
child: Center(
child: Divider(
axis: context.knobs.list(
label: 'Axis',
options: [Axis.horizontal, Axis.vertical],
),
child: const StyledText('Divider'),
),
),
),
Expand Down
1 change: 1 addition & 0 deletions packages/remix/demo/macos/Flutter/ephemeral/.app_filename
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
demo.app
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// This is a generated file; do not edit or check into version control.
FLUTTER_ROOT=/Users/lucasoliveira/fvm/versions/stable
FLUTTER_APPLICATION_PATH=/Users/lucasoliveira/Developer/mix/packages/remix/demo
COCOAPODS_PARALLEL_CODE_SIGN=true
FLUTTER_TARGET=/Users/lucasoliveira/Developer/mix/packages/remix/demo/lib/main.dart
FLUTTER_BUILD_DIR=build
FLUTTER_BUILD_NAME=1.0.0
FLUTTER_BUILD_NUMBER=1
DART_OBFUSCATION=false
TRACK_WIDGET_CREATION=true
TREE_SHAKE_ICONS=false
PACKAGE_CONFIG=/Users/lucasoliveira/Developer/mix/packages/remix/demo/.dart_tool/package_config.json
Loading
Loading