From c89f808a088d8bae963120b7fd22c026b96e8f59 Mon Sep 17 00:00:00 2001 From: Luca Stancapiano Date: Wed, 28 Oct 2026 09:22:29 +0100 Subject: [PATCH] fix(macos): stop mouse clicks from being dropped after a cursor warp Disable CoreGraphics local-event suppression and refresh the reusable mouse event source so move-then-click sequences are delivered after a cursor warp. --- src/platform/macos/macos_backend.cpp | 20 ++++++++++++++++--- .../fixtures/macos_backend_test_hooks.hpp | 7 +++++++ tests/fixtures/macos_backend_test_hooks.cpp | 14 +++++++++++++ tests/unit/test_macos_backend.cpp | 6 ++++++ 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/platform/macos/macos_backend.cpp b/src/platform/macos/macos_backend.cpp index 8c027c9..16f3f4a 100644 --- a/src/platform/macos/macos_backend.cpp +++ b/src/platform/macos/macos_backend.cpp @@ -484,6 +484,11 @@ namespace lvh::detail { /** * @brief Shared macOS backend state. + * + * Local-event suppression on `source` is set to zero so cursor warps do not + * discard the following click or mouse-moved events. CoreGraphics copies + * that configuration into each event at creation time, so `mouse_event` is + * refreshed from `source` after the interval is configured. */ class MacosInputState { public: @@ -491,7 +496,16 @@ namespace lvh::detail { source {CGEventSourceCreate(kCGEventSourceStateHIDSystemState)}, keyboard_source {CGEventSourceCreate(kCGEventSourceStatePrivate)}, mouse_event {source ? CGEventCreate(source) : nullptr}, - scroll_lines_per_detent_value {read_scroll_lines_per_detent(scrollwheel_scaling)} {} + scroll_lines_per_detent_value {read_scroll_lines_per_detent(scrollwheel_scaling)} { + // Each move warps the cursor. The default 250ms suppression then drops the + // following click and mouse-moved events. The cursor image still moves. + if (source) { + CGEventSourceSetLocalEventsSuppressionInterval(source, 0); + if (mouse_event) { + CGEventSetSource(mouse_event, source); + } + } + } MacosInputState(const MacosInputState &) = delete; MacosInputState &operator=(const MacosInputState &) = delete; @@ -510,9 +524,9 @@ namespace lvh::detail { } } - CGEventSourceRef source {}; ///< CoreGraphics event source for mouse and scroll events. + CGEventSourceRef source {}; ///< Mouse/scroll source with local-event suppression disabled. CGEventSourceRef keyboard_source {}; ///< CoreGraphics event source for keyboard events. - CGEventRef mouse_event {}; ///< Reusable CoreGraphics mouse event. + CGEventRef mouse_event {}; ///< Reusable mouse event refreshed from `source` after suppression is disabled. double scrollwheel_scaling = default_scrollwheel_scaling; ///< Raw macOS scroll-wheel scaling preference. int scroll_lines_per_detent_value = default_scroll_lines_per_detent; ///< Logical lines per wheel detent. CGEventFlags keyboard_flags {}; ///< Active modifier flags applied to mouse events. diff --git a/tests/fixtures/include/fixtures/macos_backend_test_hooks.hpp b/tests/fixtures/include/fixtures/macos_backend_test_hooks.hpp index 0540fe4..1fd2a90 100644 --- a/tests/fixtures/include/fixtures/macos_backend_test_hooks.hpp +++ b/tests/fixtures/include/fixtures/macos_backend_test_hooks.hpp @@ -199,4 +199,11 @@ namespace lvh::detail::test { */ MacosBackendUtilityResult macos_backend_utilities(); + /** + * @brief Read the suppression interval stored on the backend's reusable mouse event. + * + * @return Suppression interval in seconds, or no value if event-source creation fails. + */ + std::optional macos_backend_mouse_event_suppression_interval(); + } // namespace lvh::detail::test diff --git a/tests/fixtures/macos_backend_test_hooks.cpp b/tests/fixtures/macos_backend_test_hooks.cpp index aaf0169..90567eb 100644 --- a/tests/fixtures/macos_backend_test_hooks.cpp +++ b/tests/fixtures/macos_backend_test_hooks.cpp @@ -123,6 +123,20 @@ namespace lvh::detail::test { namespace macos = lvh::detail::macos_backend_test; + std::optional macos_backend_mouse_event_suppression_interval() { + macos::MacosInputState state; + if (!state.mouse_event) { + return std::nullopt; + } + const auto source = CGEventCreateSourceFromEvent(state.mouse_event); + if (!source) { + return std::nullopt; + } + const auto interval = CGEventSourceGetLocalEventsSuppressionInterval(source); + CFRelease(source); + return interval; + } + std::optional macos_backend_key_code(KeyboardKeyCode key_code) { const auto mapped = macos::macos_key_code(key_code); if (!mapped) { diff --git a/tests/unit/test_macos_backend.cpp b/tests/unit/test_macos_backend.cpp index 908b799..06e92fa 100644 --- a/tests/unit/test_macos_backend.cpp +++ b/tests/unit/test_macos_backend.cpp @@ -21,6 +21,12 @@ */ class MacosBackendTest: public MacOSTest {}; +TEST_F(MacosBackendTest, DisablesSuppressionForReusableMouseEvent) { + const auto interval = lvh::detail::test::macos_backend_mouse_event_suppression_interval(); + ASSERT_TRUE(interval.has_value()); + EXPECT_DOUBLE_EQ(*interval, 0.0); +} + TEST_F(MacosBackendTest, TranslatesKeyboardKeys) { EXPECT_EQ(lvh::detail::test::macos_backend_key_code(0x08), kVK_Delete); EXPECT_EQ(lvh::detail::test::macos_backend_key_code(0x09), kVK_Tab);