// Copyright 2014 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:ui' as ui show PointerChange, PointerData, PointerSignalKind; import 'events.dart'; export 'dart:ui' show PointerData; export 'events.dart' show PointerEvent; // Add `kPrimaryButton` to [buttons] when a pointer of certain devices is down. // // TODO(tongmu): This patch is supposed to be done by embedders. Patching it // in framework is a workaround before [PointerEventConverter] is moved to embedders. // https://github.com/flutter/flutter/issues/30454 int _synthesiseDownButtons(int buttons, PointerDeviceKind kind) { switch (kind) { case PointerDeviceKind.mouse: case PointerDeviceKind.trackpad: return buttons; case PointerDeviceKind.touch: case PointerDeviceKind.stylus: case PointerDeviceKind.invertedStylus: return buttons == 0 ? kPrimaryButton : buttons; case PointerDeviceKind.unknown: // We have no information about the device but we know we never want // buttons to be 0 when the pointer is down. return buttons == 0 ? kPrimaryButton : buttons; } } /// Signature for a callback that returns the device pixel ratio of a /// [FlutterView] identified by the provided `viewId`. /// /// Returns null if no view with the provided ID exists. /// /// Used by [PointerEventConverter.expand]. /// /// See also: /// /// * [FlutterView.devicePixelRatio] for an explanation of device pixel ratio. typedef DevicePixelRatioGetter = double? Function(int viewId); /// Converts from engine pointer data to framework pointer events. /// /// This takes [PointerDataPacket] objects, as received from the engine via /// [dart:ui.PlatformDispatcher.onPointerDataPacket], and converts them to /// [PointerEvent] objects. abstract final class PointerEventConverter { /// Expand the given packet of pointer data into a sequence of framework /// pointer events. /// /// The `devicePixelRatioForView` is used to obtain the device pixel ratio for /// the view a particular event occurred in to convert its data from physical /// coordinates to logical pixels. See the discussion at [PointerEvent] for /// more details on the [PointerEvent] coordinate space. static Iterable<PointerEvent> expand(Iterable<ui.PointerData> data, DevicePixelRatioGetter devicePixelRatioForView) { return data .where((ui.PointerData datum) => datum.signalKind != ui.PointerSignalKind.unknown) .map<PointerEvent?>((ui.PointerData datum) { final double? devicePixelRatio = devicePixelRatioForView(datum.viewId); if (devicePixelRatio == null) { // View doesn't exist anymore. return null; } final Offset position = Offset(datum.physicalX, datum.physicalY) / devicePixelRatio; final Offset delta = Offset(datum.physicalDeltaX, datum.physicalDeltaY) / devicePixelRatio; final double radiusMinor = _toLogicalPixels(datum.radiusMinor, devicePixelRatio); final double radiusMajor = _toLogicalPixels(datum.radiusMajor, devicePixelRatio); final double radiusMin = _toLogicalPixels(datum.radiusMin, devicePixelRatio); final double radiusMax = _toLogicalPixels(datum.radiusMax, devicePixelRatio); final Duration timeStamp = datum.timeStamp; final PointerDeviceKind kind = datum.kind; switch (datum.signalKind ?? ui.PointerSignalKind.none) { case ui.PointerSignalKind.none: switch (datum.change) { case ui.PointerChange.add: return PointerAddedEvent( viewId: datum.viewId, timeStamp: timeStamp, kind: kind, device: datum.device, position: position, obscured: datum.obscured, pressureMin: datum.pressureMin, pressureMax: datum.pressureMax, distance: datum.distance, distanceMax: datum.distanceMax, radiusMin: radiusMin, radiusMax: radiusMax, orientation: datum.orientation, tilt: datum.tilt, embedderId: datum.embedderId, ); case ui.PointerChange.hover: return PointerHoverEvent( viewId: datum.viewId, timeStamp: timeStamp, kind: kind, device: datum.device, position: position, delta: delta, buttons: datum.buttons, obscured: datum.obscured, pressureMin: datum.pressureMin, pressureMax: datum.pressureMax, distance: datum.distance, distanceMax: datum.distanceMax, size: datum.size, radiusMajor: radiusMajor, radiusMinor: radiusMinor, radiusMin: radiusMin, radiusMax: radiusMax, orientation: datum.orientation, tilt: datum.tilt, synthesized: datum.synthesized, embedderId: datum.embedderId, ); case ui.PointerChange.down: return PointerDownEvent( viewId: datum.viewId, timeStamp: timeStamp, pointer: datum.pointerIdentifier, kind: kind, device: datum.device, position: position, buttons: _synthesiseDownButtons(datum.buttons, kind), obscured: datum.obscured, pressure: datum.pressure, pressureMin: datum.pressureMin, pressureMax: datum.pressureMax, distanceMax: datum.distanceMax, size: datum.size, radiusMajor: radiusMajor, radiusMinor: radiusMinor, radiusMin: radiusMin, radiusMax: radiusMax, orientation: datum.orientation, tilt: datum.tilt, embedderId: datum.embedderId, ); case ui.PointerChange.move: return PointerMoveEvent( viewId: datum.viewId, timeStamp: timeStamp, pointer: datum.pointerIdentifier, kind: kind, device: datum.device, position: position, delta: delta, buttons: _synthesiseDownButtons(datum.buttons, kind), obscured: datum.obscured, pressure: datum.pressure, pressureMin: datum.pressureMin, pressureMax: datum.pressureMax, distanceMax: datum.distanceMax, size: datum.size, radiusMajor: radiusMajor, radiusMinor: radiusMinor, radiusMin: radiusMin, radiusMax: radiusMax, orientation: datum.orientation, tilt: datum.tilt, platformData: datum.platformData, synthesized: datum.synthesized, embedderId: datum.embedderId, ); case ui.PointerChange.up: return PointerUpEvent( viewId: datum.viewId, timeStamp: timeStamp, pointer: datum.pointerIdentifier, kind: kind, device: datum.device, position: position, buttons: datum.buttons, obscured: datum.obscured, pressure: datum.pressure, pressureMin: datum.pressureMin, pressureMax: datum.pressureMax, distance: datum.distance, distanceMax: datum.distanceMax, size: datum.size, radiusMajor: radiusMajor, radiusMinor: radiusMinor, radiusMin: radiusMin, radiusMax: radiusMax, orientation: datum.orientation, tilt: datum.tilt, embedderId: datum.embedderId, ); case ui.PointerChange.cancel: return PointerCancelEvent( viewId: datum.viewId, timeStamp: timeStamp, pointer: datum.pointerIdentifier, kind: kind, device: datum.device, position: position, buttons: datum.buttons, obscured: datum.obscured, pressureMin: datum.pressureMin, pressureMax: datum.pressureMax, distance: datum.distance, distanceMax: datum.distanceMax, size: datum.size, radiusMajor: radiusMajor, radiusMinor: radiusMinor, radiusMin: radiusMin, radiusMax: radiusMax, orientation: datum.orientation, tilt: datum.tilt, embedderId: datum.embedderId, ); case ui.PointerChange.remove: return PointerRemovedEvent( viewId: datum.viewId, timeStamp: timeStamp, kind: kind, device: datum.device, position: position, obscured: datum.obscured, pressureMin: datum.pressureMin, pressureMax: datum.pressureMax, distanceMax: datum.distanceMax, radiusMin: radiusMin, radiusMax: radiusMax, embedderId: datum.embedderId, ); case ui.PointerChange.panZoomStart: return PointerPanZoomStartEvent( viewId: datum.viewId, timeStamp: timeStamp, pointer: datum.pointerIdentifier, device: datum.device, position: position, embedderId: datum.embedderId, synthesized: datum.synthesized, ); case ui.PointerChange.panZoomUpdate: final Offset pan = Offset(datum.panX, datum.panY) / devicePixelRatio; final Offset panDelta = Offset(datum.panDeltaX, datum.panDeltaY) / devicePixelRatio; return PointerPanZoomUpdateEvent( viewId: datum.viewId, timeStamp: timeStamp, pointer: datum.pointerIdentifier, device: datum.device, position: position, pan: pan, panDelta: panDelta, scale: datum.scale, rotation: datum.rotation, embedderId: datum.embedderId, synthesized: datum.synthesized, ); case ui.PointerChange.panZoomEnd: return PointerPanZoomEndEvent( viewId: datum.viewId, timeStamp: timeStamp, pointer: datum.pointerIdentifier, device: datum.device, position: position, embedderId: datum.embedderId, synthesized: datum.synthesized, ); } case ui.PointerSignalKind.scroll: if (!datum.scrollDeltaX.isFinite || !datum.scrollDeltaY.isFinite || devicePixelRatio <= 0) { return null; } final Offset scrollDelta = Offset(datum.scrollDeltaX, datum.scrollDeltaY) / devicePixelRatio; return PointerScrollEvent( viewId: datum.viewId, timeStamp: timeStamp, kind: kind, device: datum.device, position: position, scrollDelta: scrollDelta, embedderId: datum.embedderId, ); case ui.PointerSignalKind.scrollInertiaCancel: return PointerScrollInertiaCancelEvent( viewId: datum.viewId, timeStamp: timeStamp, kind: kind, device: datum.device, position: position, embedderId: datum.embedderId, ); case ui.PointerSignalKind.scale: return PointerScaleEvent( viewId: datum.viewId, timeStamp: timeStamp, kind: kind, device: datum.device, position: position, embedderId: datum.embedderId, scale: datum.scale, ); case ui.PointerSignalKind.unknown: throw StateError('Unreachable'); } }).whereType<PointerEvent>(); } static double _toLogicalPixels(double physicalPixels, double devicePixelRatio) => physicalPixels / devicePixelRatio; }