From 7f77c0af33d29e25791067b6f377e666ee292158 Mon Sep 17 00:00:00 2001 From: marton Date: Sat, 16 May 2026 23:58:55 -0400 Subject: [PATCH] fix(macos): scroll input fixes --- src/platform/macos/input.cpp | 74 +++++++++++++++++++++++++++++++----- 1 file changed, 64 insertions(+), 10 deletions(-) diff --git a/src/platform/macos/input.cpp b/src/platform/macos/input.cpp index de721819e25..6eed2c1d365 100644 --- a/src/platform/macos/input.cpp +++ b/src/platform/macos/input.cpp @@ -3,7 +3,10 @@ * @brief Definitions for macOS input handling. */ // standard includes +#include #include +#include +#include #include #include @@ -30,6 +33,10 @@ constexpr std::chrono::milliseconds MULTICLICK_DELAY_MS(500); namespace platf { using namespace std::literals; + constexpr int WHEEL_DELTA = 120; + constexpr double DEFAULT_SCROLLWHEEL_SCALING = 0.3125; + constexpr int DEFAULT_SCROLL_LINES_PER_DETENT = 5; + struct macos_input_t { public: CGDirectDisplayID display {}; @@ -42,6 +49,8 @@ namespace platf { // mouse related stuff CGEventRef mouse_event {}; // mouse event source + double scrollwheel_scaling {DEFAULT_SCROLLWHEEL_SCALING}; + int scroll_lines_per_detent {DEFAULT_SCROLL_LINES_PER_DETENT}; bool mouse_down[3] {}; // mouse button status std::chrono::steady_clock::steady_clock::time_point last_mouse_event[3][2]; // timestamp of last mouse events }; @@ -485,20 +494,63 @@ const KeyCodeMap kKeyCodesMap[] = { macos_input->last_mouse_event[mac_button][release] = now; } + int get_scroll_lines_per_detent(double &scrollwheel_scaling) { + double scale = DEFAULT_SCROLLWHEEL_SCALING; + const auto value = CFPreferencesCopyValue(CFSTR("com.apple.scrollwheel.scaling"), kCFPreferencesAnyApplication, kCFPreferencesCurrentUser, kCFPreferencesAnyHost); + if (value) { + if (CFGetTypeID(value) == CFNumberGetTypeID()) { + CFNumberGetValue(static_cast(value), kCFNumberDoubleType, &scale); + } else if (CFGetTypeID(value) == CFStringGetTypeID()) { + scale = CFStringGetDoubleValue(static_cast(value)); + } + CFRelease(value); + } + + if (!std::isfinite(scale)) { + scale = DEFAULT_SCROLLWHEEL_SCALING; + } + + scrollwheel_scaling = scale; + + // com.apple.scrollwheel.scaling stores the Mouse scroll speed slider position, not + // the scroll multiplier itself. The slider is 0..1 and Apple's default is 0.3125, + // so anchor 0 at one line per wheel detent and 0.3125 at five lines. + const auto scroll_scale = std::clamp(scale, 0.0, 1.0); + constexpr double lines_per_scroll_scale = (DEFAULT_SCROLL_LINES_PER_DETENT - 1.0) / DEFAULT_SCROLLWHEEL_SCALING; + + return std::max(1, static_cast(std::ceil(1.0 + scroll_scale * lines_per_scroll_scale))); + } + + int scroll_pixels(const macos_input_t *macos_input, const int high_res_distance) { + const auto source_pixels_per_line = CGEventSourceGetPixelsPerLine(macos_input->source); + const auto pixels_per_line = source_pixels_per_line > 0 ? static_cast(source_pixels_per_line + 0.5) : 10; + const auto scaled_pixels = static_cast(high_res_distance) * std::max(1, pixels_per_line) * std::max(1, macos_input->scroll_lines_per_detent); + + return static_cast(scaled_pixels / WHEEL_DELTA); + } + + void post_scroll(input_t &input, const int wheelY, const int wheelX) { + if (wheelY == 0 && wheelX == 0) { + return; + } + + const auto macos_input = static_cast(input.get()); + CGEventRef event = CGEventCreateScrollWheelEvent(macos_input->source, kCGScrollEventUnitPixel, 2, wheelY, wheelX); + if (!event) { + return; + } + + CGEventSetIntegerValueField(event, kCGScrollWheelEventIsContinuous, 1); + CGEventPost(kCGHIDEventTap, event); + CFRelease(event); + } + void scroll(input_t &input, const int high_res_distance) { - int wheelY = high_res_distance / 120; - int wheelX = 0; - CGEventRef upEvent = CGEventCreateScrollWheelEvent(nullptr, kCGScrollEventUnitLine, 2, wheelY, wheelX); - CGEventPost(kCGHIDEventTap, upEvent); - CFRelease(upEvent); + post_scroll(input, scroll_pixels(static_cast(input.get()), high_res_distance), 0); } void hscroll(input_t &input, int high_res_distance) { - int wheelY = 0; - int wheelX = high_res_distance / 120; - CGEventRef upEvent = CGEventCreateScrollWheelEvent(nullptr, kCGScrollEventUnitLine, 2, wheelY, wheelX); - CGEventPost(kCGHIDEventTap, upEvent); - CFRelease(upEvent); + post_scroll(input, 0, scroll_pixels(static_cast(input.get()), high_res_distance)); } /** @@ -592,6 +644,7 @@ const KeyCodeMap kKeyCodesMap[] = { macos_input->source = CGEventSourceCreate(kCGEventSourceStateHIDSystemState); macos_input->keyboard_source = CGEventSourceCreate(kCGEventSourceStatePrivate); + macos_input->scroll_lines_per_detent = get_scroll_lines_per_detent(macos_input->scrollwheel_scaling); macos_input->kb_flags = 0; @@ -600,6 +653,7 @@ const KeyCodeMap kKeyCodesMap[] = { macos_input->mouse_down[1] = false; macos_input->mouse_down[2] = false; + BOOST_LOG(debug) << "macOS scroll speed: com.apple.scrollwheel.scaling="sv << macos_input->scrollwheel_scaling << ", lines per detent="sv << macos_input->scroll_lines_per_detent << ", pixels per line="sv << CGEventSourceGetPixelsPerLine(macos_input->source); BOOST_LOG(debug) << "Display "sv << macos_input->display << ", pixel dimension: " << CGDisplayPixelsWide(macos_input->display) << "x"sv << CGDisplayPixelsHigh(macos_input->display); return result;