diff --git a/packages/rfw/CHANGELOG.md b/packages/rfw/CHANGELOG.md index 8db008e12be..b3082c795e6 100644 --- a/packages/rfw/CHANGELOG.md +++ b/packages/rfw/CHANGELOG.md @@ -1,3 +1,9 @@ +## 1.0.24 +* Adds `InkResponse` material widget. +* Adds `Material` material widget. +* Adds the `child` to `Opacity` core widget. +* Implements more `InkWell` parameters. + ## 1.0.23 * Replaces usage of deprecated Flutter APIs. diff --git a/packages/rfw/lib/src/flutter/core_widgets.dart b/packages/rfw/lib/src/flutter/core_widgets.dart index 379edfe5aa6..e8d1d8f74b3 100644 --- a/packages/rfw/lib/src/flutter/core_widgets.dart +++ b/packages/rfw/lib/src/flutter/core_widgets.dart @@ -499,6 +499,7 @@ Map get _coreWidgetsDefinitions => (['opacity']) ?? 0.0, onEnd: source.voidHandler(['onEnd']), alwaysIncludeSemantics: source.v(['alwaysIncludeSemantics']) ?? true, + child: source.optionalChild(['child']), ); }, diff --git a/packages/rfw/lib/src/flutter/material_widgets.dart b/packages/rfw/lib/src/flutter/material_widgets.dart index cd1e515f226..1ddd71ad7e6 100644 --- a/packages/rfw/lib/src/flutter/material_widgets.dart +++ b/packages/rfw/lib/src/flutter/material_widgets.dart @@ -30,9 +30,11 @@ import 'runtime.dart'; /// * [DropdownButton] /// * [ElevatedButton] /// * [FloatingActionButton] +/// * [InkResponse] /// * [InkWell] /// * [LinearProgressIndicator] /// * [ListTile] +/// * [Material] /// * [OutlinedButton] /// * [Scaffold] /// * [TextButton] @@ -337,14 +339,58 @@ Map get _materialWidgetsDefinitions => (TapDownDetails details) => trigger()), + onTapUp: source.handler(['onTapUp'], (VoidCallback trigger) => (TapUpDetails details) => trigger()), + onTapCancel: source.voidHandler(['onTapCancel']), + onDoubleTap: source.voidHandler(['onDoubleTap']), + onLongPress: source.voidHandler(['onLongPress']), + onSecondaryTap: source.voidHandler(['onSecondaryTap']), + onSecondaryTapUp: source.handler(['onSecondaryTapUp'], (VoidCallback trigger) => (TapUpDetails details) => trigger()), + onSecondaryTapDown: source.handler(['onSecondaryTapDown'], (VoidCallback trigger) => (TapDownDetails details) => trigger()), + onSecondaryTapCancel: source.voidHandler(['onSecondaryTapCancel']), + onHighlightChanged: source.handler(['onHighlightChanged'], (VoidCallback trigger) => (bool highlighted) => trigger()), + onHover: source.handler(['onHover'], (VoidCallback trigger) => (bool hovered) => trigger()), + containedInkWell: source.v(['containedInkWell']) ?? false, + highlightShape: ArgumentDecoders.enumValue(BoxShape.values, source, ['highlightShape']) ?? BoxShape.circle, + radius: source.v(['radius']), + borderRadius: ArgumentDecoders.borderRadius(source, ['borderRadius'])?.resolve(Directionality.of(context)), + customBorder: ArgumentDecoders.shapeBorder(source, ['customBorder']), + focusColor: ArgumentDecoders.color(source, ['focusColor']), + hoverColor: ArgumentDecoders.color(source, ['hoverColor']), + highlightColor: ArgumentDecoders.color(source, ['highlightColor']), + splashColor: ArgumentDecoders.color(source, ['splashColor']), + enableFeedback: source.v(['enableFeedback']) ?? true, + excludeFromSemantics: source.v(['excludeFromSemantics']) ?? false, + canRequestFocus: source.v(['canRequestFocus']) ?? true, + onFocusChange: source.handler(['onFocusChange'], (VoidCallback trigger) => (bool focus) => trigger()), + autofocus: source.v(['autofocus']) ?? false, + hoverDuration: ArgumentDecoders.duration(source, ['hoverDuration'], context), + child: source.optionalChild(['child']), + ); + }, + 'InkWell': (BuildContext context, DataSource source) { - // not implemented: onHighlightChanged, onHover; mouseCursor; focusColor, hoverColor, highlightColor, overlayColor, splashColor; splashFactory; focusNode, onFocusChange + // not implemented: mouseCursor; overlayColor, splashFactory; focusNode, onFocusChange return InkWell( onTap: source.voidHandler(['onTap']), onDoubleTap: source.voidHandler(['onDoubleTap']), onLongPress: source.voidHandler(['onLongPress']), onTapDown: source.handler(['onTapDown'], (VoidCallback trigger) => (TapDownDetails details) => trigger()), onTapCancel: source.voidHandler(['onTapCancel']), + onSecondaryTap: source.voidHandler(['onSecondaryTap']), + onSecondaryTapUp: source.handler(['onSecondaryTapUp'], (VoidCallback trigger) => (TapUpDetails details) => trigger()), + onSecondaryTapDown: source.handler(['onSecondaryTapDown'], (VoidCallback trigger) => (TapDownDetails details) => trigger()), + onSecondaryTapCancel: source.voidHandler(['onSecondaryTapCancel']), + onHighlightChanged: source.handler(['onHighlightChanged'], (VoidCallback trigger) => (bool highlighted) => trigger()), + onHover: source.handler(['onHover'], (VoidCallback trigger) => (bool hovered) => trigger()), + focusColor: ArgumentDecoders.color(source, ['focusColor']), + hoverColor: ArgumentDecoders.color(source, ['hoverColor']), + highlightColor: ArgumentDecoders.color(source, ['highlightColor']), + splashColor: ArgumentDecoders.color(source, ['splashColor']), radius: source.v(['radius']), borderRadius: ArgumentDecoders.borderRadius(source, ['borderRadius'])?.resolve(Directionality.of(context)), customBorder: ArgumentDecoders.shapeBorder(source, ['customBorder']), @@ -395,6 +441,23 @@ Map get _materialWidgetsDefinitions => (MaterialType.values,source, ['type']) ?? MaterialType.canvas, + elevation: source.v(['elevation']) ?? 0.0, + color: ArgumentDecoders.color(source, ['color']), + shadowColor: ArgumentDecoders.color(source, ['shadowColor']), + surfaceTintColor: ArgumentDecoders.color(source, ['surfaceTintColor']), + textStyle: ArgumentDecoders.textStyle(source, ['textStyle']), + borderRadius: ArgumentDecoders.borderRadius(source, ['borderRadius']), + shape: ArgumentDecoders.shapeBorder(source, ['shape']), + borderOnForeground: source.v(['borderOnForeground']) ?? true, + clipBehavior: ArgumentDecoders.enumValue(Clip.values, source, ['clipBehavior']) ?? Clip.none, + animationDuration: ArgumentDecoders.duration(source, ['animationDuration'], context), + child: source.child(['child']), + ); + }, + 'OutlinedButton': (BuildContext context, DataSource source) { // not implemented: buttonStyle, focusNode return OutlinedButton( diff --git a/packages/rfw/pubspec.yaml b/packages/rfw/pubspec.yaml index 9dd0ddc38c9..6056af37679 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.23 +version: 1.0.24 environment: sdk: ^3.2.0 diff --git a/packages/rfw/test/core_widgets_test.dart b/packages/rfw/test/core_widgets_test.dart index 40521a66745..aaeeacefe50 100644 --- a/packages/rfw/test/core_widgets_test.dart +++ b/packages/rfw/test/core_widgets_test.dart @@ -106,7 +106,10 @@ void main() { runtime.update(const LibraryName(['test']), parseLibraryFile(''' import core; - widget root = Opacity(onEnd: event 'end' {}); + widget root = Opacity( + onEnd: event 'end' {}, + child: Placeholder(), + ); ''')); await tester.pump(); expect(tester.widget(find.byType(AnimatedOpacity)).onEnd, isNot(isNull)); @@ -226,7 +229,10 @@ void main() { child: FractionallySizedBox( widthFactor: 0.5, heightFactor: 0.8, - child: Text(text: "test"), + child: Text( + text: "test", + textScaleFactor: 3.0, + ), ), ); ''')); @@ -235,6 +241,7 @@ void main() { final Size childSize = tester.getSize(find.text('test')); expect(childSize.width, fractionallySizedBoxSize.width * 0.5); expect(childSize.height, fractionallySizedBoxSize.height * 0.8); + expect(tester.widget(find.text('test')).textScaler, const TextScaler.linear(3)); expect(tester.widget(find.byType(FractionallySizedBox)).alignment, Alignment.center); }); diff --git a/packages/rfw/test/goldens/material_test.ink_response_hover.png b/packages/rfw/test/goldens/material_test.ink_response_hover.png new file mode 100644 index 00000000000..3156e657805 Binary files /dev/null and b/packages/rfw/test/goldens/material_test.ink_response_hover.png differ diff --git a/packages/rfw/test/goldens/material_test.ink_response_tap.png b/packages/rfw/test/goldens/material_test.ink_response_tap.png new file mode 100644 index 00000000000..ea22b4cd4a3 Binary files /dev/null and b/packages/rfw/test/goldens/material_test.ink_response_tap.png differ diff --git a/packages/rfw/test/goldens/material_test.material_properties.png b/packages/rfw/test/goldens/material_test.material_properties.png new file mode 100644 index 00000000000..c7de8515b4c Binary files /dev/null and b/packages/rfw/test/goldens/material_test.material_properties.png differ diff --git a/packages/rfw/test/material_widgets_test.dart b/packages/rfw/test/material_widgets_test.dart index ac397ec5454..56da9abfd92 100644 --- a/packages/rfw/test/material_widgets_test.dart +++ b/packages/rfw/test/material_widgets_test.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:rfw/formats.dart' show parseLibraryFile; @@ -438,4 +439,151 @@ void main() { skip: !runGoldens, ); }); + + testWidgets('Implement InkResponse 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'), + ); + + runtime.update(testName, parseLibraryFile(''' + import core; + import material; + widget root = Scaffold( + body: Center( + child: InkResponse( + onTap: event 'onTap' {}, + onHover: event 'onHover' {}, + borderRadius: [{x: 8.0, y: 8.0}, {x: 8.0, y: 8.0}, {x: 8.0, y: 8.0}, {x: 8.0, y: 8.0}], + hoverColor: 0xFF00FF00, + splashColor: 0xAA0000FF, + highlightColor: 0xAAFF0000, + containedInkWell: true, + highlightShape: 'circle', + child: Text(text: 'InkResponse'), + ), + ), + ); + ''')); + await tester.pump(); + + expect(find.byType(InkResponse), findsOneWidget); + + // Hover + final Offset center = tester.getCenter(find.byType(InkResponse)); + final TestGesture gesture = + await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(); + addTearDown(gesture.removePointer); + await gesture.moveTo(center); + await tester.pumpAndSettle(); + + await expectLater( + find.byType(RemoteWidget), + matchesGoldenFile('goldens/material_test.ink_response_hover.png'), + skip: !runGoldens, + ); + expect(eventLog, contains('onHover {}')); + + // Tap + await gesture.down(center); + await tester.pump(); // start gesture + await tester.pump(const Duration( + milliseconds: 200)); // wait for splash to be well under way + + await expectLater( + find.byType(RemoteWidget), + matchesGoldenFile('goldens/material_test.ink_response_tap.png'), + skip: !runGoldens, + ); + await gesture.up(); + await tester.pump(); + + expect(eventLog, contains('onTap {}')); + }); + + testWidgets('Implement Material 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'), + ); + + runtime.update(testName, parseLibraryFile(''' + import core; + import material; + widget root = Material( + type: 'circle', + elevation: 6.0, + color: 0xFF0000FF, + shadowColor: 0xFF00FF00, + surfaceTintColor: 0xff0000ff, + animationDuration: 300, + borderOnForeground: false, + child: SizedBox( + width: 20.0, + height: 20.0, + ), + ); + ''')); + await tester.pump(); + + expect(tester.widget(find.byType(Material)).animationDuration, + const Duration(milliseconds: 300)); + expect(tester.widget(find.byType(Material)).borderOnForeground, + false); + await expectLater( + find.byType(RemoteWidget), + matchesGoldenFile('goldens/material_test.material_properties.png'), + skip: !runGoldens, + ); + + runtime.update(testName, parseLibraryFile(''' + import core; + import material; + widget root = Material( + clipBehavior: 'antiAlias', + shape: { type: 'circle', side: { width: 10.0, color: 0xFF0066FF } }, + child: SizedBox( + width: 20.0, + height: 20.0, + ), + ); + ''')); + await tester.pump(); + + expect(tester.widget(find.byType(Material)).clipBehavior, + Clip.antiAlias); + }); } diff --git a/packages/rfw/test_coverage/bin/test_coverage.dart b/packages/rfw/test_coverage/bin/test_coverage.dart index f5f0dcdc9cd..4781693cb75 100644 --- a/packages/rfw/test_coverage/bin/test_coverage.dart +++ b/packages/rfw/test_coverage/bin/test_coverage.dart @@ -21,9 +21,9 @@ import 'package:meta/meta.dart'; // Please update these targets when you update this package. // Please ensure that test coverage continues to be 100%. // Don't forget to update the lastUpdate date too! -const int targetLines = 3273; +const int targetLines = 3333; const String targetPercent = '100'; -const String lastUpdate = '2024-01-30'; +const String lastUpdate = '2024-02-26'; @immutable /* final */ class LcovLine {