diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 8827a5f39f9f5..44e22db4c727c 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -4413,6 +4413,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/platform_views/message_handler. FILE: ../../../flutter/lib/web_ui/lib/src/engine/platform_views/slots.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/plugins.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/pointer_binding.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/pointer_converter.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/profiler.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/raw_keyboard.dart diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index 9249171337ba5..e86ffb97942ff 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -122,6 +122,7 @@ export 'engine/platform_views/message_handler.dart'; export 'engine/platform_views/slots.dart'; export 'engine/plugins.dart'; export 'engine/pointer_binding.dart'; +export 'engine/pointer_binding/event_position_helper.dart'; export 'engine/pointer_converter.dart'; export 'engine/profiler.dart'; export 'engine/raw_keyboard.dart'; diff --git a/lib/web_ui/lib/src/engine/dom.dart b/lib/web_ui/lib/src/engine/dom.dart index 2793c54d6fdc8..e982f04707538 100644 --- a/lib/web_ui/lib/src/engine/dom.dart +++ b/lib/web_ui/lib/src/engine/dom.dart @@ -462,6 +462,9 @@ class DomHTMLElement extends DomElement {} extension DomHTMLElementExtension on DomHTMLElement { external double get offsetWidth; + external double get offsetLeft; + external double get offsetTop; + external DomHTMLElement? get offsetParent; } @JS() @@ -1090,6 +1093,8 @@ extension DomMouseEventExtension on DomMouseEvent { external double get clientY; external double get offsetX; external double get offsetY; + external double get pageX; + external double get pageY; DomPoint get client => DomPoint(clientX, clientY); DomPoint get offset => DomPoint(offsetX, offsetY); external double get button; diff --git a/lib/web_ui/lib/src/engine/pointer_binding.dart b/lib/web_ui/lib/src/engine/pointer_binding.dart index 3c1db1caa0788..d53a5c8cefd75 100644 --- a/lib/web_ui/lib/src/engine/pointer_binding.dart +++ b/lib/web_ui/lib/src/engine/pointer_binding.dart @@ -12,6 +12,7 @@ import '../engine.dart' show registerHotRestartListener; import 'browser_detection.dart'; import 'dom.dart'; import 'platform_dispatcher.dart'; +import 'pointer_binding/event_position_helper.dart'; import 'pointer_converter.dart'; import 'safe_browser_api.dart'; import 'semantics.dart'; @@ -342,27 +343,6 @@ abstract class _BaseAdapter { ((milliseconds - ms) * Duration.microsecondsPerMillisecond).toInt(); return Duration(milliseconds: ms, microseconds: micro); } - - /// Returns an [ui.Offset] of the position of [event], relative to the position of [actualTarget]. - /// - /// The offset is *not* multiplied by DPR or anything else, it's the closest - /// to what the DOM would return if we had currentTarget readily available. - /// - // TODO(dit): Make this understand 3D transforms in the platform view case, https://github.com/flutter/flutter/issues/117091 - static ui.Offset computeEventOffsetToTarget(DomMouseEvent event, DomElement actualTarget) { - if (event.target != actualTarget) { - // We're on top of a platform view. - final DomElement target = event.target! as DomElement; - // We can't use currentTarget because it gets lost when the PointerEvents - // are coalesced! - final DomRect targetRect = target.getBoundingClientRect(); - final DomRect actualTargetRect = actualTarget.getBoundingClientRect(); - final double offsetTop = targetRect.y - actualTargetRect.y; - final double offsetLeft = targetRect.x - actualTargetRect.x; - return ui.Offset(event.offsetX + offsetLeft, event.offsetY + offsetTop); - } - return ui.Offset(event.offsetX, event.offsetY); - } } mixin _WheelEventListenerMixin on _BaseAdapter { @@ -472,7 +452,7 @@ mixin _WheelEventListenerMixin on _BaseAdapter { } final List data = []; - final ui.Offset offset = _BaseAdapter.computeEventOffsetToTarget(event, glassPaneElement); + final ui.Offset offset = computeEventOffsetToTarget(event, glassPaneElement); _pointerDataConverter.convert( data, change: ui.PointerChange.hover, @@ -844,7 +824,7 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin { final double tilt = _computeHighestTilt(event); final Duration timeStamp = _BaseAdapter._eventTimeStampToDuration(event.timeStamp!); final num? pressure = event.pressure; - final ui.Offset offset = _BaseAdapter.computeEventOffsetToTarget(event, glassPaneElement); + final ui.Offset offset = computeEventOffsetToTarget(event, glassPaneElement); _pointerDataConverter.convert( data, change: details.change, @@ -1170,7 +1150,7 @@ class _MouseAdapter extends _BaseAdapter with _WheelEventListenerMixin { assert(data != null); assert(event != null); assert(details != null); - final ui.Offset offset = _BaseAdapter.computeEventOffsetToTarget(event, glassPaneElement); + final ui.Offset offset = computeEventOffsetToTarget(event, glassPaneElement); _pointerDataConverter.convert( data, change: details.change, diff --git a/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart b/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart new file mode 100644 index 0000000000000..d954e1c823a1d --- /dev/null +++ b/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart @@ -0,0 +1,113 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:ui/ui.dart' as ui show Offset; + +import '../dom.dart'; +import '../semantics.dart' show EngineSemanticsOwner; + +/// Returns an [ui.Offset] of the position of [event], relative to the position of [actualTarget]. +/// +/// The offset is *not* multiplied by DPR or anything else, it's the closest +/// to what the DOM would return if we had currentTarget readily available. +/// +/// This needs an `actualTarget`, because the `event.currentTarget` (which is what +/// this would really need to use) gets lost when the `event` comes from a "coalesced" +/// event. +/// +/// It also takes into account semantics being enabled to fix the case where +/// offsetX, offsetY == 0 (TalkBack events). +// +// TODO(dit): Make this understand 3D transforms in the platform view case, https://github.com/flutter/flutter/issues/117091 +ui.Offset computeEventOffsetToTarget(DomMouseEvent event, DomElement actualTarget) { + // On top of a platform view + if (event.target != actualTarget) { + return _computeOffsetOnPlatformView(event, actualTarget); + } + // On a TalkBack event + if (EngineSemanticsOwner.instance.semanticsEnabled && event.offsetX == 0 && event.offsetY == 0) { + return _computeOffsetForTalkbackEvent(event, actualTarget); + } + // Return the offsetX/Y in the normal case. + return ui.Offset(event.offsetX, event.offsetY); +} + +/// Computes the event offset when hovering over a platformView. +/// +/// This still uses offsetX/Y, but adds the offset from the top/left corner of the +/// platform view to the glass pane (`actualTarget`). +/// +/// ×--FlutterView(actualTarget)--------------+ +/// |\ | +/// | x1,y1 | +/// | | +/// | | +/// | ×-PlatformView(target)---------+ | +/// | |\ | | +/// | | x2,y2 | | +/// | | | | +/// | | × (event) | | +/// | | \ | | +/// | | offsetX, offsetY | | +/// | | (Relative to PlatformView) | | +/// | +------------------------------+ | +/// +-----------------------------------------+ +/// +/// Offset between PlatformView and FlutterView (xP, yP) = (x2 - x1, y2 - y1) +/// +/// Event offset relative to FlutterView = (offsetX + xP, offsetY + yP) +ui.Offset _computeOffsetOnPlatformView(DomMouseEvent event, DomElement actualTarget) { + final DomElement target = event.target! as DomElement; + final DomRect targetRect = target.getBoundingClientRect(); + final DomRect actualTargetRect = actualTarget.getBoundingClientRect(); + final double offsetTop = targetRect.y - actualTargetRect.y; + final double offsetLeft = targetRect.x - actualTargetRect.x; + return ui.Offset(event.offsetX + offsetLeft, event.offsetY + offsetTop); +} + +/// Computes the event offset when TalkBack is firing the event. +/// +/// In this case, we need to use the clientX/Y position of the event (which are +/// relative to the absolute top-left corner of the page, including scroll), then +/// deduct the offsetLeft/Top from every offsetParent of the `actualTarget`. +/// +/// ×-Page----║-------------------------------+ +/// | ║ | +/// | ×-------║--------offsetParent(s)-----+ | +/// | |\ | | +/// | | offsetLeft, offsetTop | | +/// | | | | +/// | | | | +/// | | ×-----║-------------actualTarget-+ | | +/// | | | | | | +/// ═════ × ─ (scrollLeft, scrollTop)═ ═ ═ +/// | | | | | | +/// | | | × | | | +/// | | | \ | | | +/// | | | clientX, clientY | | | +/// | | | (Relative to Page + Scroll) | | | +/// | | +-----║--------------------------+ | | +/// | +-------║----------------------------+ | +/// +---------║-------------------------------+ +/// +/// Computing the offset of the event relative to the actualTarget requires to +/// compute the clientX, clientY of the actualTarget. To do that, we iterate +/// up the offsetParent elements of actualTarget adding their offset and scroll +/// positions. Finally, we deduct that from clientX, clientY of the event. + +ui.Offset _computeOffsetForTalkbackEvent(DomMouseEvent event, DomElement actualTarget) { + assert(EngineSemanticsOwner.instance.semanticsEnabled); + // Use clientX/clientY as the position of the event (this is relative to + // the top left of the page, including scroll) + double offsetX = event.clientX; + double offsetY = event.clientY; + // Compute the scroll offset of actualTarget + DomHTMLElement parent = actualTarget as DomHTMLElement; + while(parent.offsetParent != null){ + offsetX -= parent.offsetLeft - parent.scrollLeft; + offsetY -= parent.offsetTop - parent.scrollTop; + parent = parent.offsetParent!; + } + return ui.Offset(offsetX, offsetY); +}