mirror of
https://github.com/libsdl-org/SDL.git
synced 2026-03-23 10:12:47 +08:00
Base GCMouse raw input implementation
Fix duplicate button/scroll events when GCMouse active Fix duplicate events and add thread-safe atomic for GCMouse Fix GCMouse relative mode sync when connected after mode enabled Respect SDL_HINT_MOUSE_RELATIVE_SYSTEM_SCALE in GCMouse handler Fix variable shadowing in GCMouse motion handler
This commit is contained in:
committed by
Sam Lantinga
parent
a48dee5ac1
commit
ad91384704
@@ -246,6 +246,22 @@ You are free to modify your Cocoa app with generally no consequence
|
||||
to SDL. You cannot, however, easily change the SDL window itself.
|
||||
Functionality may be added in the future to help this.
|
||||
|
||||
|
||||
## Raw Mouse Input
|
||||
|
||||
On macOS 11.0 (Big Sur) and later, SDL uses the Game Controller framework's
|
||||
GCMouse API to provide raw, unaccelerated mouse input in relative mode. This
|
||||
is ideal for games and applications requiring precise 1:1 mouse movement.
|
||||
|
||||
On older macOS versions, SDL falls back to NSEvent-based mouse input, which
|
||||
includes system mouse acceleration.
|
||||
|
||||
To use accelerated (system-scaled) mouse movement on macOS 11.0+, set the hint:
|
||||
|
||||
```c
|
||||
SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_SYSTEM_SCALE, "1");
|
||||
```
|
||||
|
||||
# Bug reports
|
||||
|
||||
Bugs are tracked at [the GitHub issue tracker](https://github.com/libsdl-org/SDL/issues/).
|
||||
|
||||
@@ -32,6 +32,11 @@ extern void Cocoa_HandleMouseWheel(SDL_Window *window, NSEvent *event);
|
||||
extern void Cocoa_HandleMouseWarp(CGFloat x, CGFloat y);
|
||||
extern void Cocoa_QuitMouse(SDL_VideoDevice *_this);
|
||||
|
||||
extern void Cocoa_InitGCMouse(void);
|
||||
extern bool Cocoa_GCMouseRelativeMode(void);
|
||||
extern bool Cocoa_HasGCMouse(void);
|
||||
extern void Cocoa_QuitGCMouse(void);
|
||||
|
||||
struct SDL_CursorData
|
||||
{
|
||||
NSTimer *frameTimer;
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
|
||||
#include "../../events/SDL_mouse_c.h"
|
||||
|
||||
#import <GameController/GameController.h>
|
||||
|
||||
#if 0
|
||||
#define DEBUG_COCOAMOUSE
|
||||
#endif
|
||||
@@ -254,6 +256,219 @@ static SDL_Cursor *Cocoa_CreateDefaultCursor(void)
|
||||
return Cocoa_CreateSystemCursor(id);
|
||||
}
|
||||
|
||||
// GCMouse support for raw (unaccelerated) mouse input on macOS 11.0+
|
||||
static id cocoa_mouse_connect_observer = nil;
|
||||
static id cocoa_mouse_disconnect_observer = nil;
|
||||
// Atomic for thread-safe access during high-frequency mouse input
|
||||
static SDL_AtomicInt cocoa_gcmouse_relative_mode;
|
||||
static bool cocoa_has_gcmouse = false;
|
||||
static SDL_MouseWheelDirection cocoa_mouse_scroll_direction = SDL_MOUSEWHEEL_NORMAL;
|
||||
|
||||
static void Cocoa_UpdateGCMouseScrollDirection(void)
|
||||
{
|
||||
Boolean keyExistsAndHasValidFormat = NO;
|
||||
Boolean naturalScrollDirection = CFPreferencesGetAppBooleanValue(
|
||||
CFSTR("com.apple.swipescrolldirection"),
|
||||
kCFPreferencesAnyApplication,
|
||||
&keyExistsAndHasValidFormat);
|
||||
if (!keyExistsAndHasValidFormat) {
|
||||
// Couldn't read the preference, assume natural scrolling direction
|
||||
naturalScrollDirection = YES;
|
||||
}
|
||||
if (naturalScrollDirection) {
|
||||
cocoa_mouse_scroll_direction = SDL_MOUSEWHEEL_FLIPPED;
|
||||
} else {
|
||||
cocoa_mouse_scroll_direction = SDL_MOUSEWHEEL_NORMAL;
|
||||
}
|
||||
}
|
||||
|
||||
static bool Cocoa_SetGCMouseRelativeMode(bool enabled)
|
||||
{
|
||||
SDL_SetAtomicInt(&cocoa_gcmouse_relative_mode, enabled ? 1 : 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void Cocoa_OnGCMouseButtonChanged(SDL_MouseID mouseID, Uint8 button,
|
||||
BOOL pressed)
|
||||
{
|
||||
Uint64 timestamp = SDL_GetTicksNS();
|
||||
SDL_SendMouseButton(timestamp, SDL_GetMouseFocus(), mouseID, button,
|
||||
pressed);
|
||||
}
|
||||
|
||||
static void Cocoa_OnGCMouseConnected(GCMouse *mouse)
|
||||
API_AVAILABLE(macos(11.0))
|
||||
{
|
||||
SDL_MouseID mouseID = (SDL_MouseID)(uintptr_t)mouse;
|
||||
|
||||
SDL_AddMouse(mouseID, NULL);
|
||||
cocoa_has_gcmouse = true;
|
||||
|
||||
// Sync with SDL's current relative mode state (may have been set before
|
||||
// GCMouse connected)
|
||||
SDL_Mouse *sdl_mouse = SDL_GetMouse();
|
||||
if (sdl_mouse && sdl_mouse->relative_mode) {
|
||||
SDL_SetAtomicInt(&cocoa_gcmouse_relative_mode, 1);
|
||||
}
|
||||
|
||||
mouse.mouseInput.leftButton.pressedChangedHandler =
|
||||
^(GCControllerButtonInput *button, float value, BOOL pressed) {
|
||||
Cocoa_OnGCMouseButtonChanged(mouseID, SDL_BUTTON_LEFT, pressed);
|
||||
};
|
||||
mouse.mouseInput.middleButton.pressedChangedHandler =
|
||||
^(GCControllerButtonInput *button, float value, BOOL pressed) {
|
||||
Cocoa_OnGCMouseButtonChanged(mouseID, SDL_BUTTON_MIDDLE, pressed);
|
||||
};
|
||||
mouse.mouseInput.rightButton.pressedChangedHandler =
|
||||
^(GCControllerButtonInput *button, float value, BOOL pressed) {
|
||||
Cocoa_OnGCMouseButtonChanged(mouseID, SDL_BUTTON_RIGHT, pressed);
|
||||
};
|
||||
|
||||
int auxiliary_button = SDL_BUTTON_X1;
|
||||
for (GCControllerButtonInput *btn in mouse.mouseInput.auxiliaryButtons) {
|
||||
const int current_button = auxiliary_button;
|
||||
btn.pressedChangedHandler =
|
||||
^(GCControllerButtonInput *button, float value, BOOL pressed) {
|
||||
Cocoa_OnGCMouseButtonChanged(mouseID, current_button, pressed);
|
||||
};
|
||||
++auxiliary_button;
|
||||
}
|
||||
|
||||
mouse.mouseInput.mouseMovedHandler =
|
||||
^(GCMouseInput *mouseInput, float deltaX, float deltaY) {
|
||||
if (Cocoa_GCMouseRelativeMode()) {
|
||||
// Skip raw input if user wants system-scaled (accelerated) deltas
|
||||
SDL_Mouse *m = SDL_GetMouse();
|
||||
if (m && m->enable_relative_system_scale) {
|
||||
return;
|
||||
}
|
||||
Uint64 timestamp = SDL_GetTicksNS();
|
||||
SDL_SendMouseMotion(timestamp, SDL_GetMouseFocus(), mouseID,
|
||||
true, deltaX, -deltaY);
|
||||
}
|
||||
};
|
||||
|
||||
mouse.mouseInput.scroll.valueChangedHandler =
|
||||
^(GCControllerDirectionPad *dpad, float xValue, float yValue) {
|
||||
Uint64 timestamp = SDL_GetTicksNS();
|
||||
// Raw scroll values: vertical in first axis, horizontal in second.
|
||||
// Vertical values are inverted compared to SDL conventions.
|
||||
float vertical = -xValue;
|
||||
float horizontal = yValue;
|
||||
|
||||
if (cocoa_mouse_scroll_direction == SDL_MOUSEWHEEL_FLIPPED) {
|
||||
vertical = -vertical;
|
||||
horizontal = -horizontal;
|
||||
}
|
||||
SDL_SendMouseWheel(timestamp, SDL_GetMouseFocus(), mouseID,
|
||||
horizontal, vertical,
|
||||
cocoa_mouse_scroll_direction);
|
||||
};
|
||||
Cocoa_UpdateGCMouseScrollDirection();
|
||||
|
||||
// Use high-priority queue for low-latency input
|
||||
dispatch_queue_t queue = dispatch_queue_create("org.libsdl.input.mouse",
|
||||
DISPATCH_QUEUE_SERIAL);
|
||||
dispatch_set_target_queue(queue,
|
||||
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0));
|
||||
mouse.handlerQueue = queue;
|
||||
}
|
||||
|
||||
static void Cocoa_OnGCMouseDisconnected(GCMouse *mouse)
|
||||
API_AVAILABLE(macos(11.0))
|
||||
{
|
||||
SDL_MouseID mouseID = (SDL_MouseID)(uintptr_t)mouse;
|
||||
|
||||
mouse.mouseInput.mouseMovedHandler = nil;
|
||||
mouse.mouseInput.leftButton.pressedChangedHandler = nil;
|
||||
mouse.mouseInput.middleButton.pressedChangedHandler = nil;
|
||||
mouse.mouseInput.rightButton.pressedChangedHandler = nil;
|
||||
mouse.mouseInput.scroll.valueChangedHandler = nil;
|
||||
|
||||
for (GCControllerButtonInput *button in mouse.mouseInput.auxiliaryButtons) {
|
||||
button.pressedChangedHandler = nil;
|
||||
}
|
||||
|
||||
SDL_RemoveMouse(mouseID);
|
||||
|
||||
// Check if any GCMouse devices remain
|
||||
if (@available(macOS 11.0, *)) {
|
||||
cocoa_has_gcmouse = ([GCMouse mice].count > 0);
|
||||
}
|
||||
}
|
||||
|
||||
void Cocoa_InitGCMouse(void)
|
||||
{
|
||||
@autoreleasepool {
|
||||
if (@available(macOS 11.0, *)) {
|
||||
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
|
||||
|
||||
cocoa_mouse_connect_observer = [center
|
||||
addObserverForName:GCMouseDidConnectNotification
|
||||
object:nil
|
||||
queue:nil
|
||||
usingBlock:^(NSNotification *note) {
|
||||
GCMouse *mouse = note.object;
|
||||
Cocoa_OnGCMouseConnected(mouse);
|
||||
}];
|
||||
|
||||
cocoa_mouse_disconnect_observer = [center
|
||||
addObserverForName:GCMouseDidDisconnectNotification
|
||||
object:nil
|
||||
queue:nil
|
||||
usingBlock:^(NSNotification *note) {
|
||||
GCMouse *mouse = note.object;
|
||||
Cocoa_OnGCMouseDisconnected(mouse);
|
||||
}];
|
||||
|
||||
// Enumerate already-connected mice
|
||||
for (GCMouse *mouse in [GCMouse mice]) {
|
||||
Cocoa_OnGCMouseConnected(mouse);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Cocoa_GCMouseRelativeMode(void)
|
||||
{
|
||||
return SDL_GetAtomicInt(&cocoa_gcmouse_relative_mode) != 0;
|
||||
}
|
||||
|
||||
bool Cocoa_HasGCMouse(void)
|
||||
{
|
||||
return cocoa_has_gcmouse;
|
||||
}
|
||||
|
||||
void Cocoa_QuitGCMouse(void)
|
||||
{
|
||||
@autoreleasepool {
|
||||
if (@available(macOS 11.0, *)) {
|
||||
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
|
||||
|
||||
if (cocoa_mouse_connect_observer) {
|
||||
[center removeObserver:cocoa_mouse_connect_observer
|
||||
name:GCMouseDidConnectNotification
|
||||
object:nil];
|
||||
cocoa_mouse_connect_observer = nil;
|
||||
}
|
||||
|
||||
if (cocoa_mouse_disconnect_observer) {
|
||||
[center removeObserver:cocoa_mouse_disconnect_observer
|
||||
name:GCMouseDidDisconnectNotification
|
||||
object:nil];
|
||||
cocoa_mouse_disconnect_observer = nil;
|
||||
}
|
||||
|
||||
for (GCMouse *mouse in [GCMouse mice]) {
|
||||
Cocoa_OnGCMouseDisconnected(mouse);
|
||||
}
|
||||
|
||||
cocoa_has_gcmouse = false;
|
||||
SDL_SetAtomicInt(&cocoa_gcmouse_relative_mode, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void Cocoa_FreeCursor(SDL_Cursor *cursor)
|
||||
{
|
||||
@autoreleasepool {
|
||||
@@ -360,19 +575,29 @@ static bool Cocoa_SetRelativeMouseMode(bool enabled)
|
||||
{
|
||||
CGError result;
|
||||
|
||||
// Update GCMouse relative mode state if available
|
||||
if (Cocoa_HasGCMouse()) {
|
||||
Cocoa_SetGCMouseRelativeMode(enabled);
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
SDL_Window *window = SDL_GetKeyboardFocus();
|
||||
if (window) {
|
||||
/* We will re-apply the relative mode when the window finishes being moved,
|
||||
* if it is being moved right now.
|
||||
/* We will re-apply the relative mode when the window finishes
|
||||
* being moved, if it is being moved right now.
|
||||
*/
|
||||
SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal;
|
||||
SDL_CocoaWindowData *data =
|
||||
(__bridge SDL_CocoaWindowData *)window->internal;
|
||||
if ([data.listener isMovingOrFocusClickPending]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// make sure the mouse isn't at the corner of the window, as this can confuse things if macOS thinks a window resize is happening on the first click.
|
||||
const CGPoint point = CGPointMake((float)(window->x + (window->w / 2)), (float)(window->y + (window->h / 2)));
|
||||
// Make sure the mouse isn't at the corner of the window, as this
|
||||
// can confuse things if macOS thinks a window resize is happening
|
||||
// on the first click.
|
||||
const CGPoint point = CGPointMake(
|
||||
(float)(window->x + (window->w / 2)),
|
||||
(float)(window->y + (window->h / 2)));
|
||||
Cocoa_HandleMouseWarp(point.x, point.y);
|
||||
CGWarpMouseCursorPosition(point);
|
||||
}
|
||||
@@ -590,6 +815,17 @@ void Cocoa_HandleMouseEvent(SDL_VideoDevice *_this, NSEvent *event)
|
||||
return;
|
||||
}
|
||||
|
||||
// When GCMouse is active in relative mode, it handles motion events
|
||||
// directly with raw (unaccelerated) deltas. Skip NSEvent-based motion
|
||||
// unless the user wants system-scaled (accelerated) input.
|
||||
if (Cocoa_HasGCMouse() && Cocoa_GCMouseRelativeMode()) {
|
||||
if (!mouse->enable_relative_system_scale) {
|
||||
// GCMouse is providing raw input, skip NSEvent deltas
|
||||
return;
|
||||
}
|
||||
// SYSTEM_SCALE is enabled: use NSEvent accelerated deltas instead
|
||||
}
|
||||
|
||||
// Ignore events that aren't inside the client area (i.e. title bar.)
|
||||
if ([event window]) {
|
||||
NSRect windowRect = [[[event window] contentView] frame];
|
||||
@@ -606,14 +842,21 @@ void Cocoa_HandleMouseEvent(SDL_VideoDevice *_this, NSEvent *event)
|
||||
deltaX += (lastMoveX - data->lastWarpX);
|
||||
deltaY += ((videodata.mainDisplayHeight - lastMoveY) - data->lastWarpY);
|
||||
|
||||
DLog("Motion was (%g, %g), offset to (%g, %g)", [event deltaX], [event deltaY], deltaX, deltaY);
|
||||
DLog("Motion was (%g, %g), offset to (%g, %g)", [event deltaX],
|
||||
[event deltaY], deltaX, deltaY);
|
||||
}
|
||||
|
||||
SDL_SendMouseMotion(Cocoa_GetEventTimestamp([event timestamp]), mouse->focus, mouseID, true, deltaX, deltaY);
|
||||
SDL_SendMouseMotion(Cocoa_GetEventTimestamp([event timestamp]),
|
||||
mouse->focus, mouseID, true, deltaX, deltaY);
|
||||
}
|
||||
|
||||
void Cocoa_HandleMouseWheel(SDL_Window *window, NSEvent *event)
|
||||
{
|
||||
// GCMouse handles scroll events directly, skip NSEvent path to avoid duplicates
|
||||
if (Cocoa_HasGCMouse()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_MouseID mouseID = SDL_DEFAULT_MOUSE_ID;
|
||||
SDL_MouseWheelDirection direction;
|
||||
CGFloat x, y;
|
||||
|
||||
@@ -209,10 +209,14 @@ static bool Cocoa_VideoInit(SDL_VideoDevice *_this)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Assume we have a mouse and keyboard
|
||||
// We could use GCMouse and GCKeyboard if we needed to, as is done in SDL_uikitevents.m
|
||||
// Initialize GCMouse for raw input on macOS 11.0+
|
||||
Cocoa_InitGCMouse();
|
||||
|
||||
// Add default keyboard and mouse if GCMouse didn't provide any
|
||||
SDL_AddKeyboard(SDL_DEFAULT_KEYBOARD_ID, NULL);
|
||||
SDL_AddMouse(SDL_DEFAULT_MOUSE_ID, NULL);
|
||||
if (!Cocoa_HasGCMouse()) {
|
||||
SDL_AddMouse(SDL_DEFAULT_MOUSE_ID, NULL);
|
||||
}
|
||||
|
||||
data.allow_spaces = SDL_GetHintBoolean(SDL_HINT_VIDEO_MAC_FULLSCREEN_SPACES, true);
|
||||
data.trackpad_is_touch_only = SDL_GetHintBoolean(SDL_HINT_TRACKPAD_IS_TOUCH_ONLY, false);
|
||||
@@ -233,6 +237,7 @@ void Cocoa_VideoQuit(SDL_VideoDevice *_this)
|
||||
SDL_CocoaVideoData *data = (__bridge SDL_CocoaVideoData *)_this->internal;
|
||||
Cocoa_QuitModes(_this);
|
||||
Cocoa_QuitKeyboard(_this);
|
||||
Cocoa_QuitGCMouse();
|
||||
Cocoa_QuitMouse(_this);
|
||||
Cocoa_QuitPen(_this);
|
||||
SDL_DestroyMutex(data.swaplock);
|
||||
|
||||
@@ -1717,6 +1717,11 @@ static NSCursor *Cocoa_GetDesiredCursor(void)
|
||||
|
||||
static void Cocoa_SendMouseButtonClicks(SDL_Mouse *mouse, NSEvent *theEvent, SDL_Window *window, Uint8 button, bool down)
|
||||
{
|
||||
// GCMouse handles button events directly, skip NSEvent path to avoid duplicates
|
||||
if (Cocoa_HasGCMouse()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_MouseID mouseID = SDL_DEFAULT_MOUSE_ID;
|
||||
//const int clicks = (int)[theEvent clickCount];
|
||||
SDL_Window *focus = SDL_GetKeyboardFocus();
|
||||
|
||||
Reference in New Issue
Block a user