diff --git a/packages/rfw/CHANGELOG.md b/packages/rfw/CHANGELOG.md index 1998c2c9a73..65db8bc2c48 100644 --- a/packages/rfw/CHANGELOG.md +++ b/packages/rfw/CHANGELOG.md @@ -1,7 +1,14 @@ +## 1.0.20 + +* Adds OverflowBox material widget. +* Updates ButtonBar material widget implementation. + ## 1.0.19 + * Add `DropdownButton` and `ClipRRect` widgets to rfw widget library. ## 1.0.18 + * Exposes `WidgetLibrary`s registered in `Runtime`. * Exposes widgets map in `LocalWidgetLibrary`. diff --git a/packages/rfw/lib/src/flutter/material_widgets.dart b/packages/rfw/lib/src/flutter/material_widgets.dart index 59fb2903464..b21bc8c2e63 100644 --- a/packages/rfw/lib/src/flutter/material_widgets.dart +++ b/packages/rfw/lib/src/flutter/material_widgets.dart @@ -37,6 +37,7 @@ import 'runtime.dart'; /// * [Scaffold] /// * [TextButton] /// * [VerticalDivider] +/// * [OverflowBar] /// /// For each, every parameter is implemented using the same name. Parameters /// that take structured types are represented using maps, with each named @@ -50,6 +51,22 @@ import 'runtime.dart'; /// * [VisualDensity] is represented in the manner described in the documentation /// of the [ArgumentDecoders.visualDensity] method. /// +/// Some features have changed in the underlying Flutter's material library and are +/// therefore no longer supported, including: +/// +/// * The [ButtonBar] widget in the Flutter's material library is planned to be +/// deprecated in favor of the [OverflowBar] widget. The [ButtonBar] widget in +/// `rfw` package uses the [OverflowBar] widget internally for backward compatibility. +/// The [ButtonBar] widget in `rfw` package is not deprecated and will continue to +/// be supported. As a result, the following [ButtonBar] parameters are no longer +/// supported: +/// +/// * `buttonMinWidth` +/// * `buttonHeight` +/// * `buttonAlignedDropdown` +/// +/// It is recommended to use the [OverflowBar] widget. +/// /// Some features are not supported: /// /// * [AppBar]s do not support [AppBar.bottom], [AppBar.flexibleSpace], and @@ -123,18 +140,66 @@ Map get _materialWidgetsDefinitions => (ButtonBarLayoutBehavior.values, source, ['layoutBehavior']) + ?? ButtonBarLayoutBehavior.padded; + + Widget overflowBar = OverflowBar( alignment: ArgumentDecoders.enumValue(MainAxisAlignment.values, source, ['alignment']) ?? MainAxisAlignment.start, - mainAxisSize: ArgumentDecoders.enumValue(MainAxisSize.values, source, ['mainAxisSize']) ?? MainAxisSize.max, - buttonMinWidth: source.v(['buttonMinWidth']), - buttonHeight: source.v(['buttonHeight']), - buttonPadding: ArgumentDecoders.edgeInsets(source, ['buttonPadding']), - buttonAlignedDropdown: source.v(['buttonAlignedDropdown']) ?? false, - layoutBehavior: ArgumentDecoders.enumValue(ButtonBarLayoutBehavior.values, source, ['layoutBehavior']), - overflowDirection: ArgumentDecoders.enumValue(VerticalDirection.values, source, ['overflowDirection']), - overflowButtonSpacing: source.v(['overflowButtonSpacing']), + spacing: buttonPadding.horizontal / 2, + overflowDirection: ArgumentDecoders.enumValue(VerticalDirection.values, source, ['overflowDirection']) ?? VerticalDirection.down, + overflowSpacing: source.v(['overflowButtonSpacing']) ?? 0.0, + children: source.childList(['children']), + ); + + switch (layoutBehavior) { + case ButtonBarLayoutBehavior.padded: + overflowBar = Padding( + padding: EdgeInsets.symmetric( + vertical: 2.0 * (buttonPadding.horizontal / 4.0), + horizontal: buttonPadding.horizontal / 2.0, + ), + child: overflowBar, + ); + case ButtonBarLayoutBehavior.constrained: + overflowBar = Container( + padding: EdgeInsets.symmetric(horizontal: buttonPadding.horizontal / 2.0), + constraints: const BoxConstraints(minHeight: 52.0), + alignment: Alignment.center, + child: overflowBar, + ); + } + + if (ArgumentDecoders.enumValue(MainAxisSize.values, source, ['mainAxisSize']) == MainAxisSize.min) { + return IntrinsicWidth(child: overflowBar); + } + + return overflowBar; + }, + + 'OverflowBar': (BuildContext context, DataSource source) { + return OverflowBar( + spacing: source.v(['spacing']) ?? 0.0, + alignment: ArgumentDecoders.enumValue(MainAxisAlignment.values, source, ['alignment']), + overflowSpacing: source.v(['overflowSpacing']) ?? 0.0, + overflowAlignment: ArgumentDecoders.enumValue(OverflowBarAlignment.values, source, ['overflowAlignment']) + ?? OverflowBarAlignment.start, + overflowDirection: ArgumentDecoders.enumValue(VerticalDirection.values, source, ['overflowDirection']) + ?? VerticalDirection.down, + textDirection: ArgumentDecoders.enumValue(TextDirection.values, source, ['textDirection']), children: source.childList(['children']), ); }, diff --git a/packages/rfw/pubspec.yaml b/packages/rfw/pubspec.yaml index b683d0dc5a6..f2f8c72ab92 100644 --- a/packages/rfw/pubspec.yaml +++ b/packages/rfw/pubspec.yaml @@ -2,7 +2,7 @@ name: rfw description: "Remote Flutter widgets: a library for rendering declarative widget description files at runtime." repository: https://github.com/flutter/packages/tree/main/packages/rfw issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+rfw%22 -version: 1.0.19 +version: 1.0.20 environment: sdk: ">=3.0.0 <4.0.0" diff --git a/packages/rfw/test/goldens/material_test.overflow_bar_properties.overflow.png b/packages/rfw/test/goldens/material_test.overflow_bar_properties.overflow.png new file mode 100644 index 00000000000..b2b9c64dfb8 Binary files /dev/null and b/packages/rfw/test/goldens/material_test.overflow_bar_properties.overflow.png differ diff --git a/packages/rfw/test/goldens/material_test.overflow_bar_properties.png b/packages/rfw/test/goldens/material_test.overflow_bar_properties.png new file mode 100644 index 00000000000..fad2df99e9d Binary files /dev/null and b/packages/rfw/test/goldens/material_test.overflow_bar_properties.png differ diff --git a/packages/rfw/test/goldens/material_test.overflow_bar_resembles_button_bar.png b/packages/rfw/test/goldens/material_test.overflow_bar_resembles_button_bar.png new file mode 100644 index 00000000000..caba08310f0 Binary files /dev/null and b/packages/rfw/test/goldens/material_test.overflow_bar_resembles_button_bar.png differ diff --git a/packages/rfw/test/material_widgets_test.dart b/packages/rfw/test/material_widgets_test.dart index b3f659bcd1f..db373487983 100644 --- a/packages/rfw/test/material_widgets_test.dart +++ b/packages/rfw/test/material_widgets_test.dart @@ -11,11 +11,18 @@ import 'package:rfw/rfw.dart'; import 'utils.dart'; void main() { + const LibraryName coreName = LibraryName(['core']); + const LibraryName materialName = LibraryName(['material']); + const LibraryName testName = LibraryName(['test']); + + Runtime setupRuntime() { + return Runtime() + ..update(coreName, createCoreWidgets()) + ..update(materialName, createMaterialWidgets()); + } + testWidgets('Material widgets', (WidgetTester tester) async { - final Runtime runtime = Runtime() - ..update(const LibraryName(['core']), createCoreWidgets()) - ..update( - const LibraryName(['material']), createMaterialWidgets()); + final Runtime runtime = setupRuntime(); final DynamicContent data = DynamicContent(); final List eventLog = []; await tester.pumpWidget( @@ -24,8 +31,7 @@ void main() { home: RemoteWidget( runtime: runtime, data: data, - widget: const FullyQualifiedWidgetName( - LibraryName(['test']), 'root'), + widget: const FullyQualifiedWidgetName(testName, 'root'), onEvent: (String eventName, DynamicMap eventArguments) { eventLog.add('$eventName $eventArguments'); }, @@ -218,4 +224,136 @@ void main() { skip: !runGoldens, ); }); + + testWidgets('OverflowBar configured to resemble ButtonBar', + (WidgetTester tester) async { + final Runtime runtime = setupRuntime(); + final DynamicContent data = DynamicContent(); + final List eventLog = []; + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(useMaterial3: false), + home: RemoteWidget( + runtime: runtime, + data: data, + widget: const FullyQualifiedWidgetName(testName, 'root'), + onEvent: (String eventName, DynamicMap eventArguments) { + eventLog.add('$eventName $eventArguments'); + }, + ), + ), + ); + expect(tester.takeException().toString(), + contains('Could not find remote widget named')); + + runtime.update(testName, parseLibraryFile(''' + import core; + import material; + widget root = Scaffold( + body: Card( + margin: [20.0], + child: Padding( + padding: [8.0], + child: OverflowBar( + spacing: 8.0, + children: [ + ElevatedButton( + onPressed: event 'button' { }, + child: Text(text: 'Elevated'), + ), + OutlinedButton( + onPressed: event 'button' { }, + child: Text(text: 'Outlined'), + ), + TextButton( + onPressed: event 'button' { }, + child: Text(text: 'Text'), + ), + ], + ), + ), + ), + ); + ''')); + await tester.pump(); + await expectLater( + find.byType(RemoteWidget), + matchesGoldenFile( + 'goldens/material_test.overflow_bar_resembles_button_bar.png'), + skip: !runGoldens, + ); + }); + + testWidgets('Implement OverflowBar properties', (WidgetTester tester) async { + final Runtime runtime = setupRuntime(); + final DynamicContent data = DynamicContent(); + final List eventLog = []; + await tester.pumpWidget( + MaterialApp( + theme: ThemeData(useMaterial3: false), + home: RemoteWidget( + runtime: runtime, + data: data, + widget: const FullyQualifiedWidgetName(testName, 'root'), + onEvent: (String eventName, DynamicMap eventArguments) { + eventLog.add('$eventName $eventArguments'); + }, + ), + ), + ); + expect(tester.takeException().toString(), + contains('Could not find remote widget named')); + + addTearDown(() async { + await tester.binding.setSurfaceSize(null); + }); + + runtime.update(testName, parseLibraryFile(''' + import core; + import material; + widget root = Scaffold( + body: Center( + child: OverflowBar( + spacing: 16.0, + alignment: 'end', + overflowSpacing: 4.0, + overflowAlignment: 'center', + overflowDirection: 'up', + children: [ + ElevatedButton( + onPressed: event 'button' { }, + child: Text(text: 'Elevated'), + ), + OutlinedButton( + onPressed: event 'button' { }, + child: Text(text: 'Outlined'), + ), + TextButton( + onPressed: event 'button' { }, + child: Text(text: 'Text'), + ), + ], + ), + ), + ); + ''')); + await tester.pump(); + + await expectLater( + find.byType(RemoteWidget), + matchesGoldenFile('goldens/material_test.overflow_bar_properties.png'), + skip: !runGoldens, + ); + + // Update the surface size for OverflowBar to overflow. + await tester.binding.setSurfaceSize(const Size(200.0, 600.0)); + await tester.pump(); + + await expectLater( + find.byType(RemoteWidget), + matchesGoldenFile( + 'goldens/material_test.overflow_bar_properties.overflow.png'), + skip: !runGoldens, + ); + }); }