* build(deps): bump third-party/lizardbyte-common
Bumps [third-party/lizardbyte-common](https://github.com/LizardByte/lizardbyte-common) from `06cd442` to `011ad2b`.
- [Release notes](https://github.com/LizardByte/lizardbyte-common/releases)
- [Commits](06cd442b80...011ad2bb13)
---
updated-dependencies:
- dependency-name: third-party/lizardbyte-common
dependency-version: 011ad2bb139083dcf4ec21f9c09f07470891a668
dependency-type: direct:production
...
Signed-off-by: dependabot[bot] <support@github.com>
* style: run clang-format across C++ sources
Reformatted conditionals, loops, and long function calls across core, platform, NVENC, and test helper code to match updated formatting rules, including minor spacing normalization in type declarations. These are style-only edits with no intended behavior changes.
---------
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
757 lines
34 KiB
C++
757 lines
34 KiB
C++
/**
|
|
* @file src/platform/macos/input.cpp
|
|
* @brief Definitions for macOS input handling.
|
|
*/
|
|
// standard includes
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <iostream>
|
|
#include <thread>
|
|
|
|
// platform includes
|
|
#include <ApplicationServices/ApplicationServices.h>
|
|
#import <Carbon/Carbon.h>
|
|
#include <CoreFoundation/CoreFoundation.h>
|
|
#include <IOKit/hidsystem/IOLLEvent.h>
|
|
#include <mach/mach.h>
|
|
|
|
// 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<macos_input_t *>(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<macos_input_t *>(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<macos_input_t *>(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<macos_input_t *>(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<macos_input_t *>(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<CFNumberRef>(value), kCFNumberDoubleType, &scale);
|
|
} else if (CFGetTypeID(value) == CFStringGetTypeID()) {
|
|
scale = CFStringGetDoubleValue(static_cast<CFStringRef>(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<int>(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<int>(source_pixels_per_line + 0.5) : 10;
|
|
const auto scaled_pixels = static_cast<int64_t>(high_res_distance) * std::max(1, pixels_per_line) * std::max(1, macos_input->scroll_lines_per_detent);
|
|
|
|
return static_cast<int>(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<macos_input_t *>(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<macos_input_t *>(input.get()), high_res_distance), 0);
|
|
}
|
|
|
|
void hscroll(input_t &input, int high_res_distance) {
|
|
post_scroll(input, 0, scroll_pixels(static_cast<macos_input_t *>(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<client_input_t> 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<macos_input_t *>(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<macos_input_t *>(p);
|
|
|
|
CFRelease(input->source);
|
|
CFRelease(input->keyboard_source);
|
|
CFRelease(input->mouse_event);
|
|
|
|
delete input;
|
|
}
|
|
|
|
std::vector<supported_gamepad_t> &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
|