diff --git a/lib/ui/fixtures/ui_test.dart b/lib/ui/fixtures/ui_test.dart index f091211c62958..731e7b137c8a3 100644 --- a/lib/ui/fixtures/ui_test.dart +++ b/lib/ui/fixtures/ui_test.dart @@ -538,6 +538,37 @@ void hooksTests() async { expectEquals(x.countryCode, y.countryCode); }); + await test('PlatformDispatcher.view getter returns view with provided ID', () { + const int viewId = 123456789; + _callHook( + '_updateWindowMetrics', + 21, + viewId, // window Id + 1.0, // devicePixelRatio + 800.0, // width + 600.0, // height + 50.0, // paddingTop + 0.0, // paddingRight + 40.0, // paddingBottom + 0.0, // paddingLeft + 0.0, // insetTop + 0.0, // insetRight + 0.0, // insetBottom + 0.0, // insetLeft + 0.0, // systemGestureInsetTop + 0.0, // systemGestureInsetRight + 0.0, // systemGestureInsetBottom + 0.0, // systemGestureInsetLeft + 22.0, // physicalTouchSlop + [], // display features bounds + [], // display features types + [], // display features states + 0, // Display ID + ); + + expectEquals(PlatformDispatcher.instance.view(id: viewId)?.viewId, viewId); + }); + await test('View padding/insets/viewPadding/systemGestureInsets', () { _callHook( '_updateWindowMetrics', @@ -602,7 +633,7 @@ void hooksTests() async { expectEquals(window.systemGestureInsets.bottom, 44.0); }); - await test('Window physical touch slop', () { + await test('Window physical touch slop', () { _callHook( '_updateWindowMetrics', 21, @@ -816,6 +847,25 @@ void hooksTests() async { expectEquals(action, 4); }); + await test('onSemanticsActionEvent preserves callback zone', () { + late Zone innerZone; + late Zone runZone; + late SemanticsActionEvent action; + + runZoned(() { + innerZone = Zone.current; + PlatformDispatcher.instance.onSemanticsActionEvent = (SemanticsActionEvent actionEvent) { + runZone = Zone.current; + action = actionEvent; + }; + }); + + _callHook('_dispatchSemanticsAction', 3, 1234, 4, null); + expectIdentical(runZone, innerZone); + expectEquals(action.nodeId, 1234); + expectEquals(action.type.index, 4); + }); + await test('onPlatformMessage preserves callback zone', () { late Zone innerZone; late Zone runZone; diff --git a/lib/ui/hooks.dart b/lib/ui/hooks.dart index 5d5f129021412..c391d273e1e59 100644 --- a/lib/ui/hooks.dart +++ b/lib/ui/hooks.dart @@ -32,7 +32,7 @@ void _updateDisplays( @pragma('vm:entry-point') void _updateWindowMetrics( - Object id, + int id, double devicePixelRatio, double width, double height, diff --git a/lib/ui/platform_dispatcher.dart b/lib/ui/platform_dispatcher.dart index 42a56a36c345a..cbd56a11adc68 100644 --- a/lib/ui/platform_dispatcher.dart +++ b/lib/ui/platform_dispatcher.dart @@ -33,8 +33,12 @@ typedef PointerDataPacketCallback = void Function(PointerDataPacket packet); typedef KeyDataCallback = bool Function(KeyData data); /// Signature for [PlatformDispatcher.onSemanticsAction]. +// TODO(goderbauer): Deprecate/remove this when the framework has migrated to SemanticsActionEventCallback. typedef SemanticsActionCallback = void Function(int nodeId, SemanticsAction action, ByteData? args); +/// Signature for [PlatformDispatcher.onSemanticsActionEvent]. +typedef SemanticsActionEventCallback = void Function(SemanticsActionEvent action); + /// Signature for responses to platform messages. /// /// Used as a parameter to [PlatformDispatcher.sendPlatformMessage] and @@ -174,7 +178,11 @@ class PlatformDispatcher { /// /// If any of their configurations change, [onMetricsChanged] will be called. Iterable get views => _views.values; - final Map _views = {}; + final Map _views = {}; + + /// Returns the [FlutterView] with the provided ID if one exists, or null + /// otherwise. + FlutterView? view({required int id}) => _views[id]; // A map of opaque platform view identifiers to view configurations. final Map _viewConfigurations = {}; @@ -250,7 +258,7 @@ class PlatformDispatcher { // // Updates the metrics of the window with the given id. void _updateWindowMetrics( - Object id, + int id, double devicePixelRatio, double width, double height, @@ -436,6 +444,7 @@ class PlatformDispatcher { for (int i = 0; i < length; ++i) { int offset = i * _kPointerDataFieldCount; data.add(PointerData( + // TODO(goderbauer): Wire up viewId. embedderId: packet.getInt64(kStride * offset++, _kFakeHostEndian), timeStamp: Duration(microseconds: packet.getInt64(kStride * offset++, _kFakeHostEndian)), change: PointerChange.values[packet.getInt64(kStride * offset++, _kFakeHostEndian)], @@ -1150,6 +1159,7 @@ class PlatformDispatcher { /// /// The framework invokes this callback in the same zone in which the /// callback was set. + // TODO(goderbauer): Deprecate/remove this when the framework has migrated to onSemanticsActionEvent. SemanticsActionCallback? get onSemanticsAction => _onSemanticsAction; SemanticsActionCallback? _onSemanticsAction; Zone _onSemanticsActionZone = Zone.root; @@ -1158,6 +1168,22 @@ class PlatformDispatcher { _onSemanticsActionZone = Zone.current; } + /// A callback that is invoked whenever the user requests an action to be + /// performed on a semantics node. + /// + /// This callback is used when the user expresses the action they wish to + /// perform based on the semantics node supplied by updateSemantics. + /// + /// The framework invokes this callback in the same zone in which the + /// callback was set. + SemanticsActionEventCallback? get onSemanticsActionEvent => _onSemanticsActionEvent; + SemanticsActionEventCallback? _onSemanticsActionEvent; + Zone _onSemanticsActionEventZone = Zone.root; + set onSemanticsActionEvent(SemanticsActionEventCallback? callback) { + _onSemanticsActionEvent = callback; + _onSemanticsActionEventZone = Zone.current; + } + // Called from the engine via hooks.dart. void _updateFrameData(int frameNumber) { final FrameData previous = _frameData; @@ -1190,6 +1216,16 @@ class PlatformDispatcher { SemanticsAction.fromIndex(action)!, args, ); + _invoke1( + onSemanticsActionEvent, + _onSemanticsActionEventZone, + SemanticsActionEvent( + type: SemanticsAction.fromIndex(action)!, + nodeId: nodeId, + viewId: 0, // TODO(goderbauer): Wire up the real view ID. + arguments: args, + ), + ); } ErrorCallback? _onError; @@ -2350,3 +2386,49 @@ enum DartPerformanceMode { /// frequently performing work. memory, } + +/// An event to request a [SemanticsAction] of [type] to be performed on the +/// [SemanticsNode] identified by [nodeId] owned by the [FlutterView] identified +/// by [viewId]. +/// +/// Used by [SemanticsBinding.performSemanticsAction]. +class SemanticsActionEvent { + /// Creates a [SemanticsActionEvent]. + const SemanticsActionEvent({ + required this.type, + required this.viewId, + required this.nodeId, + this.arguments, + }); + + /// The type of action to be performed. + final SemanticsAction type; + + /// The id of the [FlutterView] the [SemanticsNode] identified by [nodeId] is + /// associated with. + final int viewId; + + /// The id of the [SemanticsNode] on which the action is to be performed. + final int nodeId; + + /// Optional arguments for the action. + final Object? arguments; + + static const Object _noArgumentPlaceholder = Object(); + + /// Create a clone of the [SemanticsActionEvent] but with provided parameters + /// replaced. + SemanticsActionEvent copyWith({ + SemanticsAction? type, + int? viewId, + int? nodeId, + Object? arguments = _noArgumentPlaceholder, + }) { + return SemanticsActionEvent( + type: type ?? this.type, + viewId: viewId ?? this.viewId, + nodeId: nodeId ?? this.nodeId, + arguments: arguments == _noArgumentPlaceholder ? this.arguments : arguments, + ); + } +} diff --git a/lib/ui/pointer.dart b/lib/ui/pointer.dart index 608a4d49aded5..48d5e7dfee1b4 100644 --- a/lib/ui/pointer.dart +++ b/lib/ui/pointer.dart @@ -141,6 +141,7 @@ enum PointerSignalKind { class PointerData { /// Creates an object that represents the state of a pointer. const PointerData({ + this.viewId = 0, this.embedderId = 0, this.timeStamp = Duration.zero, this.change = PointerChange.cancel, @@ -178,11 +179,16 @@ class PointerData { this.rotation = 0.0, }); - /// Unique identifier that ties the [PointerEvent] to embedder event created it. + /// The ID of the [FlutterView] this [PointerEvent] originated from. + final int viewId; + + /// Unique identifier that ties the [PointerEvent] to the embedder + /// event that created it. + /// it. /// - /// No two pointer events can have the same [embedderId]. This is different from - /// [pointerIdentifier] - used for hit-testing, whereas [embedderId] is used to - /// identify the platform event. + /// No two pointer events can have the same [embedderId]. This is different + /// from [pointerIdentifier] - used for hit-testing, whereas [embedderId] is + /// used to identify the platform event. final int embedderId; /// Time of event dispatch, relative to an arbitrary timeline. diff --git a/lib/ui/window.dart b/lib/ui/window.dart index 67bbda67e3c34..1dbf84e6a92ca 100644 --- a/lib/ui/window.dart +++ b/lib/ui/window.dart @@ -89,7 +89,7 @@ class FlutterView { FlutterView._(this.viewId, this.platformDispatcher); /// The opaque ID for this view. - final Object viewId; + final int viewId; /// The platform dispatcher that this view is registered with, and gets its /// information from. diff --git a/lib/web_ui/lib/platform_dispatcher.dart b/lib/web_ui/lib/platform_dispatcher.dart index 2059122d8c8a5..6d1870068e225 100644 --- a/lib/web_ui/lib/platform_dispatcher.dart +++ b/lib/web_ui/lib/platform_dispatcher.dart @@ -10,6 +10,7 @@ typedef TimingsCallback = void Function(List timings); typedef PointerDataPacketCallback = void Function(PointerDataPacket packet); typedef KeyDataCallback = bool Function(KeyData data); typedef SemanticsActionCallback = void Function(int nodeId, SemanticsAction action, ByteData? args); +typedef SemanticsActionEventCallback = void Function(SemanticsActionEvent action); typedef PlatformMessageResponseCallback = void Function(ByteData? data); typedef PlatformMessageCallback = void Function( String name, ByteData? data, PlatformMessageResponseCallback? callback); @@ -33,6 +34,8 @@ abstract class PlatformDispatcher { Iterable get views; + FlutterView? view({required int id}); + FlutterView? get implicitView; VoidCallback? get onMetricsChanged; @@ -135,6 +138,9 @@ abstract class PlatformDispatcher { SemanticsActionCallback? get onSemanticsAction; set onSemanticsAction(SemanticsActionCallback? callback); + SemanticsActionEventCallback? get onSemanticsActionEvent; + set onSemanticsActionEvent(SemanticsActionEventCallback? callback); + ErrorCallback? get onError; set onError(ErrorCallback? callback); @@ -493,3 +499,33 @@ enum DartPerformanceMode { throughput, memory, } + +class SemanticsActionEvent { + const SemanticsActionEvent({ + required this.type, + required this.viewId, + required this.nodeId, + this.arguments, + }); + + final SemanticsAction type; + final int viewId; + final int nodeId; + final Object? arguments; + + static const Object _noArgumentPlaceholder = Object(); + + SemanticsActionEvent copyWith({ + SemanticsAction? type, + int? viewId, + int? nodeId, + Object? arguments = _noArgumentPlaceholder, + }) { + return SemanticsActionEvent( + type: type ?? this.type, + viewId: viewId ?? this.viewId, + nodeId: nodeId ?? this.nodeId, + arguments: arguments == _noArgumentPlaceholder ? this.arguments : arguments, + ); + } +} diff --git a/lib/web_ui/lib/pointer.dart b/lib/web_ui/lib/pointer.dart index 542f5c70c90c3..469730c371731 100644 --- a/lib/web_ui/lib/pointer.dart +++ b/lib/web_ui/lib/pointer.dart @@ -36,6 +36,7 @@ enum PointerSignalKind { class PointerData { const PointerData({ + this.viewId = 0, this.embedderId = 0, this.timeStamp = Duration.zero, this.change = PointerChange.cancel, @@ -72,6 +73,7 @@ class PointerData { this.scale = 0.0, this.rotation = 0.0, }); + final int viewId; final int embedderId; final Duration timeStamp; final PointerChange change; diff --git a/lib/web_ui/lib/src/engine/platform_dispatcher.dart b/lib/web_ui/lib/src/engine/platform_dispatcher.dart index 4101440a22a30..e0cbf4897367b 100644 --- a/lib/web_ui/lib/src/engine/platform_dispatcher.dart +++ b/lib/web_ui/lib/src/engine/platform_dispatcher.dart @@ -165,7 +165,12 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { /// The current list of windows. @override Iterable get views => viewData.values; - final Map viewData = {}; + final Map viewData = {}; + + /// Returns the [FlutterView] with the provided ID if one exists, or null + /// otherwise. + @override + ui.FlutterView? view({required int id}) => viewData[id]; /// A map of opaque platform window identifiers to window configurations. /// @@ -1206,12 +1211,38 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { _onSemanticsActionZone = Zone.current; } + /// A callback that is invoked whenever the user requests an action to be + /// performed on a semantics node. + /// + /// This callback is used when the user expresses the action they wish to + /// perform based on the semantics node supplied by updateSemantics. + /// + /// The framework invokes this callback in the same zone in which the + /// callback was set. + @override + ui.SemanticsActionEventCallback? get onSemanticsActionEvent => _onSemanticsActionEvent; + ui.SemanticsActionEventCallback? _onSemanticsActionEvent; + Zone _onSemanticsActionEventZone = Zone.root; + @override + set onSemanticsActionEvent(ui.SemanticsActionEventCallback? callback) { + _onSemanticsActionEvent = callback; + _onSemanticsActionEventZone = Zone.current; + } + /// Engine code should use this method instead of the callback directly. /// Otherwise zones won't work properly. void invokeOnSemanticsAction( int nodeId, ui.SemanticsAction action, ByteData? args) { invoke3( _onSemanticsAction, _onSemanticsActionZone, nodeId, action, args); + invoke1( + _onSemanticsActionEvent, _onSemanticsActionEventZone, ui.SemanticsActionEvent( + type: action, + nodeId: nodeId, + viewId: 0, // TODO(goderbauer): Wire up the real view ID. + arguments: args, + ), + ); } // TODO(dnfield): make this work on web. diff --git a/lib/web_ui/lib/src/engine/window.dart b/lib/web_ui/lib/src/engine/window.dart index 8785ce356b234..707be6b38ee76 100644 --- a/lib/web_ui/lib/src/engine/window.dart +++ b/lib/web_ui/lib/src/engine/window.dart @@ -51,7 +51,7 @@ class EngineFlutterWindow extends ui.SingletonFlutterWindow { } @override - final Object viewId; + final int viewId; @override final ui.PlatformDispatcher platformDispatcher; diff --git a/lib/web_ui/lib/window.dart b/lib/web_ui/lib/window.dart index 2cb8750205711..f16ff8b850f72 100644 --- a/lib/web_ui/lib/window.dart +++ b/lib/web_ui/lib/window.dart @@ -13,7 +13,7 @@ abstract class Display { abstract class FlutterView { PlatformDispatcher get platformDispatcher; - Object get viewId; + int get viewId; double get devicePixelRatio; Rect get physicalGeometry; Size get physicalSize; diff --git a/shell/platform/embedder/fixtures/main.dart b/shell/platform/embedder/fixtures/main.dart index 6d113f72b3e7a..183da8898793a 100644 --- a/shell/platform/embedder/fixtures/main.dart +++ b/shell/platform/embedder/fixtures/main.dart @@ -105,19 +105,12 @@ Future get accessibilityFeaturesChanged { return featuresChanged.future; } -class SemanticsActionData { - const SemanticsActionData(this.id, this.action, this.args); - final int id; - final SemanticsAction action; - final ByteData? args; -} - -Future get semanticsAction { - final Completer actionReceived = - Completer(); - PlatformDispatcher.instance.onSemanticsAction = - (int id, SemanticsAction action, ByteData? args) { - actionReceived.complete(SemanticsActionData(id, action, args)); +Future get semanticsActionEvent { + final Completer actionReceived = + Completer(); + PlatformDispatcher.instance.onSemanticsActionEvent = + (SemanticsActionEvent action) { + actionReceived.complete(action); }; return actionReceived.future; } @@ -285,12 +278,12 @@ void a11y_main() async { signalNativeTest(); // 6: Await semantics action from embedder. - final SemanticsActionData data = await semanticsAction; + final SemanticsActionEvent data = await semanticsActionEvent; final List actionArgs = [ - data.args!.getInt8(0), - data.args!.getInt8(1) + (data.arguments! as ByteData).getInt8(0), + (data.arguments! as ByteData).getInt8(1) ]; - notifySemanticsAction(data.id, data.action.index, actionArgs); + notifySemanticsAction(data.nodeId, data.type.index, actionArgs); // 7: Await semantics disabled from embedder. await semanticsChanged;