From 1e6bfa7bb1cff873a2238ef4fbc4c655d9e74d27 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 29 Jan 2026 15:25:44 +0800 Subject: [PATCH] fix(iPad): Magic Mouse, click (#14188) Signed-off-by: fufesou --- flutter/lib/common/widgets/remote_input.dart | 12 +++++++ flutter/lib/models/input_model.dart | 34 +++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index 2c97ea147..e35da6424 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -107,6 +107,8 @@ class _RawTouchGestureDetectorRegionState // For mouse mode, we need to block the events when the cursor is in a blocked area. // So we need to cache the last tap down position. Offset? _lastTapDownPositionForMouseMode; + // Cache global position for onTap (which lacks position info). + Offset? _lastTapDownGlobalPosition; FFI get ffi => widget.ffi; FfiModel get ffiModel => widget.ffiModel; @@ -136,6 +138,7 @@ class _RawTouchGestureDetectorRegionState onTapDown(TapDownDetails d) async { lastDeviceKind = d.kind; + _lastTapDownGlobalPosition = d.globalPosition; if (isNotTouchBasedDevice()) { return; } @@ -154,6 +157,10 @@ class _RawTouchGestureDetectorRegionState if (isNotTouchBasedDevice()) { return; } + // Filter duplicate touch tap events on iOS (Magic Mouse issue). + if (inputModel.shouldIgnoreTouchTap(d.globalPosition)) { + return; + } if (handleTouch) { final isMoved = await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); @@ -171,6 +178,11 @@ class _RawTouchGestureDetectorRegionState if (isNotTouchBasedDevice()) { return; } + // Filter duplicate touch tap events on iOS (Magic Mouse issue). + final lastPos = _lastTapDownGlobalPosition; + if (lastPos != null && inputModel.shouldIgnoreTouchTap(lastPos)) { + return; + } if (!handleTouch) { // Cannot use `_lastTapDownDetails` because Flutter calls `onTapUp` before `onTap`, clearing the cached details. // Using `_lastTapDownPositionForMouseMode` instead. diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 0eb74dbc5..97ef80a55 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -826,6 +826,9 @@ class InputModel { Map _getMouseEvent(PointerEvent evt, String type) { final Map out = {}; + bool hasStaleButtonsOnMouseUp = + type == _kMouseEventUp && evt.buttons == _lastButtons; + // Check update event type and set buttons to be sent. int buttons = _lastButtons; if (type == _kMouseEventMove) { @@ -850,7 +853,7 @@ class InputModel { buttons = evt.buttons; } } - _lastButtons = evt.buttons; + _lastButtons = hasStaleButtonsOnMouseUp ? 0 : evt.buttons; out['buttons'] = buttons; out['type'] = type; @@ -1218,6 +1221,28 @@ class InputModel { _trackpadLastDelta = Offset.zero; } + // iOS Magic Mouse duplicate event detection. + // When using Magic Mouse on iPad, iOS may emit both mouse and touch events + // for the same click in certain areas (like top-left corner). + int _lastMouseDownTimeMs = 0; + ui.Offset _lastMouseDownPos = ui.Offset.zero; + + /// Check if a touch tap event should be ignored because it's a duplicate + /// of a recent mouse event (iOS Magic Mouse issue). + bool shouldIgnoreTouchTap(ui.Offset pos) { + if (!isIOS) return false; + final nowMs = DateTime.now().millisecondsSinceEpoch; + final dt = nowMs - _lastMouseDownTimeMs; + final distance = (_lastMouseDownPos - pos).distance; + // If touch tap is within 2000ms and 80px of the last mouse down, + // it's likely a duplicate event from the same Magic Mouse click. + if (dt >= 0 && dt < 2000 && distance < 80.0) { + debugPrint("shouldIgnoreTouchTap: IGNORED (dt=$dt, dist=$distance)"); + return true; + } + return false; + } + void onPointDownImage(PointerDownEvent e) { debugPrint("onPointDownImage ${e.kind}"); _stopFling = true; @@ -1227,6 +1252,13 @@ class InputModel { if (isViewOnly && !showMyCursor) return; if (isViewCamera) return; + // Track mouse down events for duplicate detection on iOS. + final nowMs = DateTime.now().millisecondsSinceEpoch; + if (e.kind == ui.PointerDeviceKind.mouse) { + _lastMouseDownTimeMs = nowMs; + _lastMouseDownPos = e.position; + } + if (_relativeMouse.enabled.value) { _relativeMouse.updatePointerRegionTopLeftGlobal(e); }