/** * @file src/platform/macos/input.cpp * @brief Definitions for macOS input handling. */ // standard includes #include #include #include #include #include #include // platform includes #include #import #include #include #include // local includes #include "src/display_device.h" #include "src/input.h" #include "src/logging.h" #include "src/platform/common.h" #include "src/utility.h" constexpr auto MULTICLICK_DELAY_MS = std::chrono::milliseconds(500); ///< Maximum gap between clicks that macOS should treat as a double click. namespace platf { using namespace std::literals; constexpr int WHEEL_DELTA = 120; ///< Protocol or platform constant for wheel delta. constexpr double DEFAULT_SCROLLWHEEL_SCALING = 0.3125; ///< Protocol or platform constant for default scrollwheel scaling. constexpr int DEFAULT_SCROLL_LINES_PER_DETENT = 5; ///< Protocol or platform constant for default scroll lines per detent. /** * @brief macOS input source and target display state. */ struct macos_input_t { public: CGDirectDisplayID display {}; ///< CoreGraphics identifier for the display receiving injected input. CGFloat displayScaling {}; ///< Scale factor used to translate client coordinates to display pixels. CGEventSourceRef source {}; ///< CoreGraphics event source used for mouse and scroll events. // keyboard related stuff CGEventSourceRef keyboard_source {}; ///< CoreGraphics event source used for keyboard injection. CGEventFlags kb_flags {}; ///< Active keyboard modifier flags currently held down by the client. // mouse related stuff CGEventRef mouse_event {}; ///< Reusable CoreGraphics mouse event updated before posting. double scrollwheel_scaling {DEFAULT_SCROLLWHEEL_SCALING}; ///< Multiplier applied to incoming scroll-wheel deltas. int scroll_lines_per_detent {DEFAULT_SCROLL_LINES_PER_DETENT}; ///< Number of logical scroll lines represented by one wheel detent. /** * @brief Tracks whether the mouse button is currently pressed. */ bool mouse_down[3] {}; // mouse button status /** * @brief Last mouse event. */ std::chrono::steady_clock::steady_clock::time_point last_mouse_event[3][2]; // timestamp of last mouse events }; // A struct to hold a Windows keycode to Mac virtual keycode mapping. /** * @brief Mapping from Sunshine key symbols to macOS virtual key codes. */ struct KeyCodeMap { int win_keycode; ///< Sunshine/Windows virtual key code received from the client. int mac_keycode; ///< macOS virtual key code sent through CoreGraphics. }; // Customized less operator for using std::lower_bound() on a KeyCodeMap array. /** * @brief Order key mappings by Sunshine key code for binary search. * * @param a Left-hand mapping being compared. * @param b Right-hand mapping being compared. * @return True when `a` has a lower Sunshine key code than `b`. */ bool operator<(const KeyCodeMap &a, const KeyCodeMap &b) { return a.win_keycode < b.win_keycode; } // clang-format off /** * @brief Key codes map. */ const KeyCodeMap kKeyCodesMap[] = { { 0x08 /* VKEY_BACK */, kVK_Delete }, { 0x09 /* VKEY_TAB */, kVK_Tab }, { 0x0A /* VKEY_BACKTAB */, 0x21E4 }, { 0x0C /* VKEY_CLEAR */, kVK_ANSI_KeypadClear }, { 0x0D /* VKEY_RETURN */, kVK_Return }, { 0x10 /* VKEY_SHIFT */, kVK_Shift }, { 0x11 /* VKEY_CONTROL */, kVK_Control }, { 0x12 /* VKEY_MENU */, kVK_Option }, { 0x13 /* VKEY_PAUSE */, -1 }, { 0x14 /* VKEY_CAPITAL */, kVK_CapsLock }, { 0x15 /* VKEY_KANA */, kVK_JIS_Kana }, { 0x15 /* VKEY_HANGUL */, -1 }, { 0x17 /* VKEY_JUNJA */, -1 }, { 0x18 /* VKEY_FINAL */, -1 }, { 0x19 /* VKEY_HANJA */, -1 }, { 0x19 /* VKEY_KANJI */, -1 }, { 0x1B /* VKEY_ESCAPE */, kVK_Escape }, { 0x1C /* VKEY_CONVERT */, -1 }, { 0x1D /* VKEY_NONCONVERT */, -1 }, { 0x1E /* VKEY_ACCEPT */, -1 }, { 0x1F /* VKEY_MODECHANGE */, -1 }, { 0x20 /* VKEY_SPACE */, kVK_Space }, { 0x21 /* VKEY_PRIOR */, kVK_PageUp }, { 0x22 /* VKEY_NEXT */, kVK_PageDown }, { 0x23 /* VKEY_END */, kVK_End }, { 0x24 /* VKEY_HOME */, kVK_Home }, { 0x25 /* VKEY_LEFT */, kVK_LeftArrow }, { 0x26 /* VKEY_UP */, kVK_UpArrow }, { 0x27 /* VKEY_RIGHT */, kVK_RightArrow }, { 0x28 /* VKEY_DOWN */, kVK_DownArrow }, { 0x29 /* VKEY_SELECT */, -1 }, { 0x2A /* VKEY_PRINT */, -1 }, { 0x2B /* VKEY_EXECUTE */, -1 }, { 0x2C /* VKEY_SNAPSHOT */, -1 }, { 0x2D /* VKEY_INSERT */, kVK_Help }, { 0x2E /* VKEY_DELETE */, kVK_ForwardDelete }, { 0x2F /* VKEY_HELP */, kVK_Help }, { 0x30 /* VKEY_0 */, kVK_ANSI_0 }, { 0x31 /* VKEY_1 */, kVK_ANSI_1 }, { 0x32 /* VKEY_2 */, kVK_ANSI_2 }, { 0x33 /* VKEY_3 */, kVK_ANSI_3 }, { 0x34 /* VKEY_4 */, kVK_ANSI_4 }, { 0x35 /* VKEY_5 */, kVK_ANSI_5 }, { 0x36 /* VKEY_6 */, kVK_ANSI_6 }, { 0x37 /* VKEY_7 */, kVK_ANSI_7 }, { 0x38 /* VKEY_8 */, kVK_ANSI_8 }, { 0x39 /* VKEY_9 */, kVK_ANSI_9 }, { 0x41 /* VKEY_A */, kVK_ANSI_A }, { 0x42 /* VKEY_B */, kVK_ANSI_B }, { 0x43 /* VKEY_C */, kVK_ANSI_C }, { 0x44 /* VKEY_D */, kVK_ANSI_D }, { 0x45 /* VKEY_E */, kVK_ANSI_E }, { 0x46 /* VKEY_F */, kVK_ANSI_F }, { 0x47 /* VKEY_G */, kVK_ANSI_G }, { 0x48 /* VKEY_H */, kVK_ANSI_H }, { 0x49 /* VKEY_I */, kVK_ANSI_I }, { 0x4A /* VKEY_J */, kVK_ANSI_J }, { 0x4B /* VKEY_K */, kVK_ANSI_K }, { 0x4C /* VKEY_L */, kVK_ANSI_L }, { 0x4D /* VKEY_M */, kVK_ANSI_M }, { 0x4E /* VKEY_N */, kVK_ANSI_N }, { 0x4F /* VKEY_O */, kVK_ANSI_O }, { 0x50 /* VKEY_P */, kVK_ANSI_P }, { 0x51 /* VKEY_Q */, kVK_ANSI_Q }, { 0x52 /* VKEY_R */, kVK_ANSI_R }, { 0x53 /* VKEY_S */, kVK_ANSI_S }, { 0x54 /* VKEY_T */, kVK_ANSI_T }, { 0x55 /* VKEY_U */, kVK_ANSI_U }, { 0x56 /* VKEY_V */, kVK_ANSI_V }, { 0x57 /* VKEY_W */, kVK_ANSI_W }, { 0x58 /* VKEY_X */, kVK_ANSI_X }, { 0x59 /* VKEY_Y */, kVK_ANSI_Y }, { 0x5A /* VKEY_Z */, kVK_ANSI_Z }, { 0x5B /* VKEY_LWIN */, kVK_Command }, { 0x5C /* VKEY_RWIN */, kVK_RightCommand }, { 0x5D /* VKEY_APPS */, kVK_RightCommand }, { 0x5F /* VKEY_SLEEP */, -1 }, { 0x60 /* VKEY_NUMPAD0 */, kVK_ANSI_Keypad0 }, { 0x61 /* VKEY_NUMPAD1 */, kVK_ANSI_Keypad1 }, { 0x62 /* VKEY_NUMPAD2 */, kVK_ANSI_Keypad2 }, { 0x63 /* VKEY_NUMPAD3 */, kVK_ANSI_Keypad3 }, { 0x64 /* VKEY_NUMPAD4 */, kVK_ANSI_Keypad4 }, { 0x65 /* VKEY_NUMPAD5 */, kVK_ANSI_Keypad5 }, { 0x66 /* VKEY_NUMPAD6 */, kVK_ANSI_Keypad6 }, { 0x67 /* VKEY_NUMPAD7 */, kVK_ANSI_Keypad7 }, { 0x68 /* VKEY_NUMPAD8 */, kVK_ANSI_Keypad8 }, { 0x69 /* VKEY_NUMPAD9 */, kVK_ANSI_Keypad9 }, { 0x6A /* VKEY_MULTIPLY */, kVK_ANSI_KeypadMultiply }, { 0x6B /* VKEY_ADD */, kVK_ANSI_KeypadPlus }, { 0x6C /* VKEY_SEPARATOR */, -1 }, { 0x6D /* VKEY_SUBTRACT */, kVK_ANSI_KeypadMinus }, { 0x6E /* VKEY_DECIMAL */, kVK_ANSI_KeypadDecimal }, { 0x6F /* VKEY_DIVIDE */, kVK_ANSI_KeypadDivide }, { 0x70 /* VKEY_F1 */, kVK_F1 }, { 0x71 /* VKEY_F2 */, kVK_F2 }, { 0x72 /* VKEY_F3 */, kVK_F3 }, { 0x73 /* VKEY_F4 */, kVK_F4 }, { 0x74 /* VKEY_F5 */, kVK_F5 }, { 0x75 /* VKEY_F6 */, kVK_F6 }, { 0x76 /* VKEY_F7 */, kVK_F7 }, { 0x77 /* VKEY_F8 */, kVK_F8 }, { 0x78 /* VKEY_F9 */, kVK_F9 }, { 0x79 /* VKEY_F10 */, kVK_F10 }, { 0x7A /* VKEY_F11 */, kVK_F11 }, { 0x7B /* VKEY_F12 */, kVK_F12 }, { 0x7C /* VKEY_F13 */, kVK_F13 }, { 0x7D /* VKEY_F14 */, kVK_F14 }, { 0x7E /* VKEY_F15 */, kVK_F15 }, { 0x7F /* VKEY_F16 */, kVK_F16 }, { 0x80 /* VKEY_F17 */, kVK_F17 }, { 0x81 /* VKEY_F18 */, kVK_F18 }, { 0x82 /* VKEY_F19 */, kVK_F19 }, { 0x83 /* VKEY_F20 */, kVK_F20 }, { 0x84 /* VKEY_F21 */, -1 }, { 0x85 /* VKEY_F22 */, -1 }, { 0x86 /* VKEY_F23 */, -1 }, { 0x87 /* VKEY_F24 */, -1 }, { 0x90 /* VKEY_NUMLOCK */, -1 }, { 0x91 /* VKEY_SCROLL */, -1 }, { 0xA0 /* VKEY_LSHIFT */, kVK_Shift }, { 0xA1 /* VKEY_RSHIFT */, kVK_RightShift }, { 0xA2 /* VKEY_LCONTROL */, kVK_Control }, { 0xA3 /* VKEY_RCONTROL */, kVK_RightControl }, { 0xA4 /* VKEY_LMENU */, kVK_Option }, { 0xA5 /* VKEY_RMENU */, kVK_RightOption }, { 0xA6 /* VKEY_BROWSER_BACK */, -1 }, { 0xA7 /* VKEY_BROWSER_FORWARD */, -1 }, { 0xA8 /* VKEY_BROWSER_REFRESH */, -1 }, { 0xA9 /* VKEY_BROWSER_STOP */, -1 }, { 0xAA /* VKEY_BROWSER_SEARCH */, -1 }, { 0xAB /* VKEY_BROWSER_FAVORITES */, -1 }, { 0xAC /* VKEY_BROWSER_HOME */, -1 }, { 0xAD /* VKEY_VOLUME_MUTE */, -1 }, { 0xAE /* VKEY_VOLUME_DOWN */, -1 }, { 0xAF /* VKEY_VOLUME_UP */, -1 }, { 0xB0 /* VKEY_MEDIA_NEXT_TRACK */, -1 }, { 0xB1 /* VKEY_MEDIA_PREV_TRACK */, -1 }, { 0xB2 /* VKEY_MEDIA_STOP */, -1 }, { 0xB3 /* VKEY_MEDIA_PLAY_PAUSE */, -1 }, { 0xB4 /* VKEY_MEDIA_LAUNCH_MAIL */, -1 }, { 0xB5 /* VKEY_MEDIA_LAUNCH_MEDIA_SELECT */, -1 }, { 0xB6 /* VKEY_MEDIA_LAUNCH_APP1 */, -1 }, { 0xB7 /* VKEY_MEDIA_LAUNCH_APP2 */, -1 }, { 0xBA /* VKEY_OEM_1 */, kVK_ANSI_Semicolon }, { 0xBB /* VKEY_OEM_PLUS */, kVK_ANSI_Equal }, { 0xBC /* VKEY_OEM_COMMA */, kVK_ANSI_Comma }, { 0xBD /* VKEY_OEM_MINUS */, kVK_ANSI_Minus }, { 0xBE /* VKEY_OEM_PERIOD */, kVK_ANSI_Period }, { 0xBF /* VKEY_OEM_2 */, kVK_ANSI_Slash }, { 0xC0 /* VKEY_OEM_3 */, kVK_ANSI_Grave }, { 0xDB /* VKEY_OEM_4 */, kVK_ANSI_LeftBracket }, { 0xDC /* VKEY_OEM_5 */, kVK_ANSI_Backslash }, { 0xDD /* VKEY_OEM_6 */, kVK_ANSI_RightBracket }, { 0xDE /* VKEY_OEM_7 */, kVK_ANSI_Quote }, { 0xDF /* VKEY_OEM_8 */, -1 }, { 0xE2 /* VKEY_OEM_102 */, -1 }, { 0xE5 /* VKEY_PROCESSKEY */, -1 }, { 0xE7 /* VKEY_PACKET */, -1 }, { 0xF6 /* VKEY_ATTN */, -1 }, { 0xF7 /* VKEY_CRSEL */, -1 }, { 0xF8 /* VKEY_EXSEL */, -1 }, { 0xF9 /* VKEY_EREOF */, -1 }, { 0xFA /* VKEY_PLAY */, -1 }, { 0xFB /* VKEY_ZOOM */, -1 }, { 0xFC /* VKEY_NONAME */, -1 }, { 0xFD /* VKEY_PA1 */, -1 }, { 0xFE /* VKEY_OEM_CLEAR */, kVK_ANSI_KeypadClear } }; // clang-format on /** * @brief Translate a platform keycode to a Sunshine key symbol. * * @param keycode Platform keycode being translated or emitted. * @return Sunshine key symbol, or 0 when the keycode is unmapped. */ int keysym(int keycode) { KeyCodeMap key_map {}; key_map.win_keycode = keycode; const KeyCodeMap *temp_map = std::lower_bound( kKeyCodesMap, kKeyCodesMap + sizeof(kKeyCodesMap) / sizeof(kKeyCodesMap[0]), key_map ); if (temp_map >= kKeyCodesMap + sizeof(kKeyCodesMap) / sizeof(kKeyCodesMap[0]) || temp_map->win_keycode != keycode || temp_map->mac_keycode == -1) { return -1; } return temp_map->mac_keycode; } /** * @brief macOS modifier flags split into generic and device-specific bits. */ struct modifier_flags_t { CGEventFlags generic {}; ///< Modifier bits represented by device-independent CoreGraphics flags. CGEventFlags device {}; ///< Modifier bits represented by left/right device-specific flags. CGEventFlags all_devices {}; ///< Mask covering all device-specific variants for this modifier. }; /** * @brief Resolve the CoreGraphics modifier masks associated with a key code. * * @param key Sunshine key code that may represent a modifier key. * @param flags Output masks for the matching CoreGraphics modifier. * @return True when modifier flags were found for the key. */ bool modifier_flags_for_key(int key, modifier_flags_t &flags) { switch (key) { case kVK_Shift: flags = {kCGEventFlagMaskShift, NX_DEVICELSHIFTKEYMASK, NX_DEVICELSHIFTKEYMASK | NX_DEVICERSHIFTKEYMASK}; return true; case kVK_RightShift: flags = {kCGEventFlagMaskShift, NX_DEVICERSHIFTKEYMASK, NX_DEVICELSHIFTKEYMASK | NX_DEVICERSHIFTKEYMASK}; return true; case kVK_Command: flags = {kCGEventFlagMaskCommand, NX_DEVICELCMDKEYMASK, NX_DEVICELCMDKEYMASK | NX_DEVICERCMDKEYMASK}; return true; case kVK_RightCommand: flags = {kCGEventFlagMaskCommand, NX_DEVICERCMDKEYMASK, NX_DEVICELCMDKEYMASK | NX_DEVICERCMDKEYMASK}; return true; case kVK_Option: flags = {kCGEventFlagMaskAlternate, NX_DEVICELALTKEYMASK, NX_DEVICELALTKEYMASK | NX_DEVICERALTKEYMASK}; return true; case kVK_RightOption: flags = {kCGEventFlagMaskAlternate, NX_DEVICERALTKEYMASK, NX_DEVICELALTKEYMASK | NX_DEVICERALTKEYMASK}; return true; case kVK_Control: flags = {kCGEventFlagMaskControl, NX_DEVICELCTLKEYMASK, NX_DEVICELCTLKEYMASK | NX_DEVICERCTLKEYMASK}; return true; case kVK_RightControl: flags = {kCGEventFlagMaskControl, NX_DEVICERCTLKEYMASK, NX_DEVICELCTLKEYMASK | NX_DEVICERCTLKEYMASK}; return true; default: return false; } } void keyboard_update(input_t &input, uint16_t modcode, bool release, uint8_t flags) { auto key = keysym(modcode); BOOST_LOG(debug) << "got keycode: 0x"sv << std::hex << modcode << ", translated to: 0x" << std::hex << key << ", release:" << release; if (key < 0) { return; } auto macos_input = ((macos_input_t *) input.get()); CGEventRef event = nullptr; modifier_flags_t modifier_flags; if (modifier_flags_for_key(key, modifier_flags)) { event = CGEventCreateKeyboardEvent(macos_input->keyboard_source, key, !release); if (!event) { return; } CGEventSetIntegerValueField(event, kCGKeyboardEventKeycode, key); if (release) { macos_input->kb_flags &= ~modifier_flags.device; if ((macos_input->kb_flags & modifier_flags.all_devices) == 0) { macos_input->kb_flags &= ~modifier_flags.generic; } } else { macos_input->kb_flags |= modifier_flags.generic | modifier_flags.device; } CGEventSetType(event, kCGEventFlagsChanged); } else { event = CGEventCreateKeyboardEvent(macos_input->keyboard_source, key, !release); if (!event) { return; } CGEventSetType(event, release ? kCGEventKeyUp : kCGEventKeyDown); } CGEventSetFlags(event, macos_input->kb_flags); CGEventPost(kCGSessionEventTap, event); CFRelease(event); } void unicode(input_t &input, char *utf8, int size) { BOOST_LOG(info) << "unicode: Unicode input not yet implemented for MacOS."sv; } int alloc_gamepad(input_t &input, const gamepad_id_t &id, const gamepad_arrival_t &metadata, feedback_queue_t feedback_queue) { BOOST_LOG(info) << "alloc_gamepad: Gamepad not yet implemented for MacOS."sv; return -1; } void free_gamepad(input_t &input, int nr) { BOOST_LOG(info) << "free_gamepad: Gamepad not yet implemented for MacOS."sv; } void gamepad_update(input_t &input, int nr, const gamepad_state_t &gamepad_state) { BOOST_LOG(info) << "gamepad: Gamepad not yet implemented for MacOS."sv; } // returns current mouse location: util::point_t get_mouse_loc(input_t &input) { // Creating a new event every time to avoid any reuse risk const auto macos_input = static_cast(input.get()); const auto snapshot_event = CGEventCreate(macos_input->source); const auto current = CGEventGetLocation(snapshot_event); CFRelease(snapshot_event); return util::point_t { current.x, current.y }; } /** * @brief Post a mouse event at the clamped display location. * * @param input Platform input context. * @param button Mouse button. * @param type CoreGraphics mouse event type. * @param raw_location Requested mouse location. * @param previous_location Previous mouse location. * @param click_count Click count for the event. */ void post_mouse( input_t &input, const CGMouseButton button, const CGEventType type, const util::point_t raw_location, const util::point_t previous_location, const int click_count ) { BOOST_LOG(debug) << "mouse_event: "sv << button << ", type: "sv << type << ", location:"sv << raw_location.x << ":"sv << raw_location.y << " click_count: "sv << click_count; const auto macos_input = static_cast(input.get()); const auto display = macos_input->display; const auto event = macos_input->mouse_event; // get display bounds for current display const CGRect display_bounds = CGDisplayBounds(display); // limit mouse to current display bounds const auto location = CGPoint { std::clamp(raw_location.x, display_bounds.origin.x, display_bounds.origin.x + display_bounds.size.width - 1), std::clamp(raw_location.y, display_bounds.origin.y, display_bounds.origin.y + display_bounds.size.height - 1) }; CGEventSetType(event, type); CGEventSetLocation(event, location); CGEventSetIntegerValueField(event, kCGMouseEventButtonNumber, button); CGEventSetIntegerValueField(event, kCGMouseEventClickState, click_count); // Include deltas so some 3D applications can consume changes (game cameras, etc) const double deltaX = raw_location.x - previous_location.x; const double deltaY = raw_location.y - previous_location.y; CGEventSetDoubleValueField(event, kCGMouseEventDeltaX, deltaX); CGEventSetDoubleValueField(event, kCGMouseEventDeltaY, deltaY); // Inject modifier flags into mouse events so that shift+click and similar combinations work correctly. CGEventSetFlags(event, macos_input->kb_flags); CGEventPost(kCGHIDEventTap, event); // For why this is here, see: // https://stackoverflow.com/questions/15194409/simulated-mouseevent-not-working-properly-osx CGWarpMouseCursorPosition(location); } /** * @brief Choose the CoreGraphics mouse event type for the current button action. * * @param input Platform input backend that receives the event. * @return CoreGraphics mouse event type for button press or release. */ inline CGEventType event_type_mouse(input_t &input) { const auto macos_input = static_cast(input.get()); if (macos_input->mouse_down[0]) { return kCGEventLeftMouseDragged; } if (macos_input->mouse_down[1]) { return kCGEventOtherMouseDragged; } if (macos_input->mouse_down[2]) { return kCGEventRightMouseDragged; } return kCGEventMouseMoved; } void move_mouse( input_t &input, const int deltaX, const int deltaY ) { const auto current = get_mouse_loc(input); const auto location = util::point_t {current.x + deltaX, current.y + deltaY}; post_mouse(input, kCGMouseButtonLeft, event_type_mouse(input), location, current, 0); } void abs_mouse( input_t &input, const touch_port_t &touch_port, const float x, const float y ) { const auto macos_input = static_cast(input.get()); const auto scaling = macos_input->displayScaling; const auto display = macos_input->display; auto location = util::point_t {x * scaling, y * scaling}; CGRect display_bounds = CGDisplayBounds(display); // in order to get the correct mouse location for capturing display , we need to add the display bounds to the location location.x += display_bounds.origin.x; location.y += display_bounds.origin.y; post_mouse(input, kCGMouseButtonLeft, event_type_mouse(input), location, get_mouse_loc(input), 0); } void button_mouse(input_t &input, const int button, const bool release) { CGMouseButton mac_button; CGEventType event; const auto macos_input = static_cast(input.get()); switch (button) { case 1: mac_button = kCGMouseButtonLeft; event = release ? kCGEventLeftMouseUp : kCGEventLeftMouseDown; break; case 2: mac_button = kCGMouseButtonCenter; event = release ? kCGEventOtherMouseUp : kCGEventOtherMouseDown; break; case 3: mac_button = kCGMouseButtonRight; event = release ? kCGEventRightMouseUp : kCGEventRightMouseDown; break; default: BOOST_LOG(warning) << "Unsupported mouse button for MacOS: "sv << button; return; } macos_input->mouse_down[mac_button] = !release; // if the last mouse down was less than MULTICLICK_DELAY_MS, we send a double click event const auto now = std::chrono::steady_clock::now(); const auto mouse_position = get_mouse_loc(input); if (now < macos_input->last_mouse_event[mac_button][release] + MULTICLICK_DELAY_MS) { post_mouse(input, mac_button, event, mouse_position, mouse_position, 2); } else { post_mouse(input, mac_button, event, mouse_position, mouse_position, 1); } macos_input->last_mouse_event[mac_button][release] = now; } /** * @brief Get scroll lines per detent. * * @param scrollwheel_scaling Scrollwheel scaling. * @return Number of logical lines represented by one wheel detent. */ 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))); } /** * @brief Convert a high-resolution wheel delta to CoreGraphics scroll pixels. * * @param macos_input macOS input state containing scroll scaling settings. * @param high_res_distance Wheel delta in Windows high-resolution units. * @return Pixel distance to send to CoreGraphics. */ 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); } /** * @brief Post a macOS scroll event to the target display. * * @param input Platform input backend that receives the event. * @param wheelY Wheel y. * @param wheelX Wheel x. */ 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) { post_scroll(input, scroll_pixels(static_cast(input.get()), high_res_distance), 0); } void hscroll(input_t &input, int high_res_distance) { post_scroll(input, 0, scroll_pixels(static_cast(input.get()), high_res_distance)); } /** * @brief Allocates a context to store per-client input data. * @param input The global input context. * @return A unique pointer to a per-client input data context. */ std::unique_ptr allocate_client_input_context(input_t &input) { // Unused return nullptr; } /** * @brief Sends a touch event to the OS. * @param input The client-specific input context. * @param touch_port The current viewport for translating to screen coordinates. * @param touch The touch event. */ void touch_update(client_input_t *input, const touch_port_t &touch_port, const touch_input_t &touch) { // Unimplemented feature - platform_caps::pen_touch } /** * @brief Sends a pen event to the OS. * @param input The client-specific input context. * @param touch_port The current viewport for translating to screen coordinates. * @param pen The pen event. */ void pen_update(client_input_t *input, const touch_port_t &touch_port, const pen_input_t &pen) { // Unimplemented feature - platform_caps::pen_touch } /** * @brief Sends a gamepad touch event to the OS. * @param input The global input context. * @param touch The touch event. */ void gamepad_touch(input_t &input, const gamepad_touch_t &touch) { // Unimplemented feature - platform_caps::controller_touch } /** * @brief Sends a gamepad motion event to the OS. * @param input The global input context. * @param motion The motion event. */ void gamepad_motion(input_t &input, const gamepad_motion_t &motion) { // Unimplemented } /** * @brief Sends a gamepad battery event to the OS. * @param input The global input context. * @param battery The battery event. */ void gamepad_battery(input_t &input, const gamepad_battery_t &battery) { // Unimplemented } input_t input() { input_t result {new macos_input_t()}; const auto macos_input = static_cast(result.get()); // Default to main display macos_input->display = CGMainDisplayID(); auto output_name = display_device::map_output_name(config::video.output_name); // If output_name is set, try to find the display with that display id if (!output_name.empty()) { const int MAX_DISPLAYS = 32; uint32_t max_display = MAX_DISPLAYS; uint32_t display_count; CGDirectDisplayID displays[MAX_DISPLAYS]; if (CGGetActiveDisplayList(max_display, displays, &display_count) != kCGErrorSuccess) { BOOST_LOG(error) << "Unable to get active display list , error: "sv << std::endl; } else { for (int i = 0; i < display_count; i++) { CGDirectDisplayID display_id = displays[i]; if (display_id == std::atoi(output_name.c_str())) { macos_input->display = display_id; } } } } // Input coordinates are based on the virtual resolution not the physical, so we need the scaling factor const CGDisplayModeRef mode = CGDisplayCopyDisplayMode(macos_input->display); macos_input->displayScaling = ((CGFloat) CGDisplayPixelsWide(macos_input->display)) / ((CGFloat) CGDisplayModeGetPixelWidth(mode)); CFRelease(mode); 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; macos_input->mouse_event = CGEventCreate(macos_input->source); macos_input->mouse_down[0] = false; 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; } void freeInput(void *p) { const auto *input = static_cast(p); CFRelease(input->source); CFRelease(input->keyboard_source); CFRelease(input->mouse_event); delete input; } std::vector &supported_gamepads(input_t *input) { static std::vector gamepads { supported_gamepad_t {"", false, "gamepads.macos_not_implemented"} }; return gamepads; } /** * @brief Returns the supported platform capabilities to advertise to the client. * @return Capability flags. */ platform_caps::caps_t get_capabilities() { return 0; } } // namespace platf