[CP][web] Fix event offset on transformed widgets. (#42171)

This cherry-pick backports flutter/engine#41870 to Flutter 3.10.

* git cherry-pick 4f3f7bb (+ conflict fixes)

Original commit message below:

[web] Fix event offset for transformed widgets (and text input nodes). (#41870)

Text inputs have moved outside of the shadowDOM and are now using the pointer event offset calculation algorithm that platform views use.  However, transforms (e.g. scaling) applied to the input element aren't currently accounted for, which leads to incorrect offsets and clicks being registered inaccurately.

This PR attempts to transform those offset coordinates using the transform matrix data that is included in the geometry information sent over to `text_editing.dart` from the framework.

* Fixes https://github.com/flutter/flutter/issues/125948 (text editing)
* Fixes https://github.com/flutter/flutter/issues/126661 (platform view scaling)
* Fixes https://github.com/flutter/flutter/issues/126754

[C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
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
index 6be2b9c..6e65c46 100644
--- 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
@@ -2,6 +2,10 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:typed_data';
+
+import 'package:ui/src/engine/text_editing/text_editing.dart';
+import 'package:ui/src/engine/vector_math.dart';
 import 'package:ui/ui.dart' as ui show Offset;
 
 import '../dom.dart';
@@ -19,51 +23,55 @@
 /// It also takes into account semantics being enabled to fix the case where
 /// offsetX, offsetY == 0 (TalkBack events).
 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);
   }
+
+  // On one of our text-editing nodes
+  final bool isInput = textEditing.strategy.domElement?.contains(event.target! as DomNode) ?? false;
+  if (isInput) {
+    final EditableTextGeometry? inputGeometry = textEditing.strategy.geometry;
+    if (inputGeometry != null) {
+      return _computeOffsetForInputs(event, inputGeometry);
+    }
+  }
+
+  // On another DOM Element (normally a platform view)
+  final bool isTargetOutsideOfShadowDOM = event.target != actualTarget;
+  if (isTargetOutsideOfShadowDOM) {
+    final DomRect origin = actualTarget.getBoundingClientRect();
+    // event.clientX/Y and origin.x/y are relative **to the viewport**.
+    // (This doesn't work with 3D translations of the parent element.)
+    // TODO(dit): Make this understand 3D transforms, https://github.com/flutter/flutter/issues/117091
+    return ui.Offset(event.clientX - origin.x, event.clientY - origin.y);
+  }
+
   // Return the offsetX/Y in the normal case.
   // (This works with 3D translations of the parent element.)
   return ui.Offset(event.offsetX, event.offsetY);
 }
 
-/// Computes the event offset when hovering over a platformView.
+/// Computes the offsets for input nodes, which live outside of the shadowDOM.
+/// Since inputs can be transformed (scaled, translated, etc), we can't rely on
+/// `_computeOffsetRelativeToActualTarget` to calculate accurate coordinates, as
+/// it only handles the case where inputs are translated, but will have issues
+/// for scaled inputs (see: https://github.com/flutter/flutter/issues/125948).
 ///
-/// 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)
-// TODO(dit): Make this understand 3D transforms, https://github.com/flutter/flutter/issues/117091
-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);
+/// We compute the offsets here by using the text input geometry data that is
+/// sent from the framework, which includes information on how to transform the
+/// underlying input element. We transform the `event.offset` points we receive
+/// using the values from the input's transform matrix.
+ui.Offset _computeOffsetForInputs(DomMouseEvent event, EditableTextGeometry inputGeometry) {
+  final DomElement targetElement = event.target! as DomHTMLElement;
+  final DomHTMLElement domElement = textEditing.strategy.activeDomElement;
+  assert(targetElement == domElement, 'The targeted input element must be the active input element');
+  final Float32List transformValues = inputGeometry.globalTransform;
+  assert(transformValues.length == 16);
+  final Matrix4 transform = Matrix4.fromFloat32List(transformValues);
+  final Vector3 transformedPoint = transform.perspectiveTransform(Vector3(event.offsetX, event.offsetY, 0));
+
+  return ui.Offset(transformedPoint.x, transformedPoint.y);
 }
 
 /// Computes the event offset when TalkBack is firing the event.
diff --git a/lib/web_ui/test/engine/pointer_binding/event_position_helper_test.dart b/lib/web_ui/test/engine/pointer_binding/event_position_helper_test.dart
new file mode 100644
index 0000000..3640cb7
--- /dev/null
+++ b/lib/web_ui/test/engine/pointer_binding/event_position_helper_test.dart
@@ -0,0 +1,115 @@
+// 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.
+
+@TestOn('browser')
+library;
+
+import 'dart:async';
+
+import 'package:test/bootstrap/browser.dart';
+import 'package:test/test.dart';
+import 'package:ui/src/engine/dom.dart';
+import 'package:ui/src/engine/embedder.dart';
+import 'package:ui/src/engine/pointer_binding/event_position_helper.dart';
+import 'package:ui/ui.dart' as ui show Offset;
+
+void main() {
+  internalBootstrapBrowserTest(() => doTests);
+}
+
+void doTests() {
+  ensureFlutterViewEmbedderInitialized();
+
+  late DomElement target;
+  late DomElement eventSource;
+  final StreamController<DomEvent> events = StreamController<DomEvent>.broadcast();
+
+  /// Dispatches an event `e` on `target`, and returns it after it's gone through the browser.
+  Future<DomPointerEvent> dispatchAndCatch(DomElement target, DomPointerEvent e) async {
+    final Future<DomEvent> nextEvent = events.stream.first;
+    target.dispatchEvent(e);
+    return (await nextEvent) as DomPointerEvent;
+  }
+
+  group('computeEventOffsetToTarget', () {
+    setUp(() {
+      target = createDomElement('div-target');
+      eventSource = createDomElement('div-event-source');
+      target.append(eventSource);
+      domDocument.body!.append(target);
+
+      // make containers known fixed sizes, absolutely positioned elements, so
+      // we can reason about screen coordinates relatively easily later!
+      target.style
+        ..position = 'absolute'
+        ..width = '320px'
+        ..height = '240px'
+        ..top = '0px'
+        ..left = '0px';
+
+      eventSource.style
+        ..position = 'absolute'
+        ..width = '100px'
+        ..height = '80px'
+        ..top = '100px'
+        ..left = '120px';
+
+      target.addEventListener('click', createDomEventListener((DomEvent e) {
+        events.add(e);
+      }));
+    });
+
+    tearDown(() {
+      target.remove();
+    });
+
+    test('Event dispatched by target returns offsetX, offsetY', () async {
+      // Fire an event contained within target...
+      final DomMouseEvent event = await dispatchAndCatch(target, createDomPointerEvent(
+        'click',
+        <String, Object>{
+          'bubbles': true,
+          'clientX': 10,
+          'clientY': 20,
+        }
+      ));
+
+      expect(event.offsetX, 10);
+      expect(event.offsetY, 20);
+
+      final ui.Offset offset = computeEventOffsetToTarget(event, target);
+
+      expect(offset.dx, event.offsetX);
+      expect(offset.dy, event.offsetY);
+    });
+
+    test('Event dispatched on child re-computes offset (offsetX/Y invalid)', () async {
+      // Fire an event contained within target...
+      final DomMouseEvent event = await dispatchAndCatch(eventSource, createDomPointerEvent(
+        'click',
+        <String, Object>{
+          'bubbles': true, // So it can be caught in `target`
+          'clientX': 140, // x = 20px into `eventSource`.
+          'clientY': 110, // y = 10px into `eventSource`.
+        }
+      ));
+
+      expect(event.offsetX, 20);
+      expect(event.offsetY, 10);
+
+      final ui.Offset offset = computeEventOffsetToTarget(event, target);
+
+      expect(offset.dx, 140);
+      expect(offset.dy, 110);
+    });
+
+    test('Event dispatched by TalkBack gets a computed offset', () async {
+      // Fill this in to test _computeOffsetForTalkbackEvent
+    }, skip: 'To be implemented!');
+
+    test('Event dispatched on text editing node computes offset with framework geometry', () async {
+      // Fill this in to test _computeOffsetForInputs
+    }, skip: 'To be implemented!');
+  });
+}