blob: 6e65c46cdf143bc2657d8aec91e7271798f63639 [file] [log] [blame]
// 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 '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';
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).
ui.Offset computeEventOffsetToTarget(DomMouseEvent event, DomElement 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 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).
///
/// 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.
///
/// 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.
// TODO(dit): Make this understand 3D transforms, https://github.com/flutter/flutter/issues/117091
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);
}