Guarantee that pens are in proximity before motion and button events

This also delays pen proximity out events to make sure that the pen is really gone before delivering them. On Android, you get a HOVER_EXIT event when the pen contacts the surface, which we don't want to treat as the pen leaving proximity.
This commit is contained in:
Sam Lantinga
2026-02-05 10:21:08 -08:00
parent cd7fc90c87
commit bddf6d3e2a
9 changed files with 77 additions and 25 deletions

View File

@@ -1484,6 +1484,8 @@ void SDL_PumpEventMaintenance(void)
}
#endif
SDL_SendPendingPenProximity();
SDL_UpdateCursorAnimation();
SDL_UpdateTrays();

View File

@@ -37,6 +37,8 @@ typedef struct SDL_Pen
float x;
float y;
SDL_PenInputFlags input_state;
bool pending_proximity_out;
SDL_WindowID pending_proximity_window_id;
void *driverdata;
} SDL_Pen;
@@ -45,6 +47,7 @@ typedef struct SDL_Pen
static SDL_RWLock *pen_device_rwlock = NULL;
static SDL_Pen *pen_devices SDL_GUARDED_BY(pen_device_rwlock) = NULL;
static int pen_device_count SDL_GUARDED_BY(pen_device_rwlock) = 0;
static SDL_AtomicInt pending_proximity_out;
// You must hold pen_device_rwlock before calling this, and result is only safe while lock is held!
// If SDL isn't initialized, grabbing the NULL lock is a no-op and there will be zero devices, so
@@ -248,8 +251,8 @@ SDL_PenID SDL_AddPenDevice(Uint64 timestamp, const char *name, SDL_Window *windo
SDL_free(namecpy);
}
if (result) {
SDL_SendPenProximity(timestamp, result, window, in_proximity);
if (result && in_proximity) {
SDL_SendPenProximity(timestamp, result, window, true, true);
}
return result;
@@ -261,7 +264,7 @@ void SDL_RemovePenDevice(Uint64 timestamp, SDL_Window *window, SDL_PenID instanc
return;
}
SDL_SendPenProximity(timestamp, instance_id, window, false); // bye bye
SDL_SendPenProximity(timestamp, instance_id, window, false, true); // bye bye
SDL_LockRWLockForWriting(pen_device_rwlock);
SDL_Pen *pen = FindPenByInstanceId(instance_id);
@@ -453,6 +456,15 @@ void SDL_SendPenAxis(Uint64 timestamp, SDL_PenID instance_id, SDL_Window *window
}
}
static void EnsurePenProximity(Uint64 timestamp, SDL_Pen *pen, SDL_Window *window)
{
if (pen->pending_proximity_out) {
pen->pending_proximity_out = false;
} else if (!(pen->input_state & SDL_PEN_INPUT_IN_PROXIMITY)) {
SDL_SendPenProximity(timestamp, pen->instance_id, window, true, true);
}
}
void SDL_SendPenMotion(Uint64 timestamp, SDL_PenID instance_id, SDL_Window *window, float x, float y)
{
bool send_event = false;
@@ -465,6 +477,8 @@ void SDL_SendPenMotion(Uint64 timestamp, SDL_PenID instance_id, SDL_Window *wind
SDL_LockRWLockForReading(pen_device_rwlock);
SDL_Pen *pen = FindPenByInstanceId(instance_id);
if (pen) {
EnsurePenProximity(timestamp, pen, window);
if ((pen->x != x) || (pen->y != y)) {
pen->x = x; // we could do an SDL_SetAtomicInt here if we run into trouble...
pen->y = y; // we could do an SDL_SetAtomicInt here if we run into trouble...
@@ -528,6 +542,8 @@ void SDL_SendPenButton(Uint64 timestamp, SDL_PenID instance_id, SDL_Window *wind
SDL_LockRWLockForReading(pen_device_rwlock);
SDL_Pen *pen = FindPenByInstanceId(instance_id);
if (pen) {
EnsurePenProximity(timestamp, pen, window);
input_state = pen->input_state;
const Uint32 flag = (Uint32) (1u << button);
const bool current = ((input_state & flag) != 0);
@@ -579,7 +595,7 @@ void SDL_SendPenButton(Uint64 timestamp, SDL_PenID instance_id, SDL_Window *wind
}
}
void SDL_SendPenProximity(Uint64 timestamp, SDL_PenID instance_id, SDL_Window *window, bool in)
void SDL_SendPenProximity(Uint64 timestamp, SDL_PenID instance_id, SDL_Window *window, bool in, bool immediate)
{
bool send_event = false;
SDL_PenInputFlags input_state = 0;
@@ -591,16 +607,23 @@ void SDL_SendPenProximity(Uint64 timestamp, SDL_PenID instance_id, SDL_Window *w
SDL_LockRWLockForReading(pen_device_rwlock);
SDL_Pen *pen = FindPenByInstanceId(instance_id);
if (pen) {
input_state = pen->input_state;
const bool in_proximity = ((input_state & SDL_PEN_INPUT_IN_PROXIMITY) != 0);
if (in_proximity != in) {
if (in) {
input_state |= SDL_PEN_INPUT_IN_PROXIMITY;
} else {
input_state &= ~SDL_PEN_INPUT_IN_PROXIMITY;
if (in || immediate) {
input_state = pen->input_state;
const bool in_proximity = ((input_state & SDL_PEN_INPUT_IN_PROXIMITY) != 0);
if (in_proximity != in) {
if (in) {
input_state |= SDL_PEN_INPUT_IN_PROXIMITY;
} else {
input_state &= ~SDL_PEN_INPUT_IN_PROXIMITY;
}
send_event = true;
pen->input_state = input_state; // we could do an SDL_SetAtomicInt here if we run into trouble...
}
send_event = true;
pen->input_state = input_state; // we could do an SDL_SetAtomicInt here if we run into trouble...
pen->pending_proximity_out = false;
} else {
pen->pending_proximity_out = true;
pen->pending_proximity_window_id = (window ? window->id : 0);
SDL_SetAtomicInt(&pending_proximity_out, true);
}
}
SDL_UnlockRWLock(pen_device_rwlock);
@@ -617,3 +640,27 @@ void SDL_SendPenProximity(Uint64 timestamp, SDL_PenID instance_id, SDL_Window *w
}
}
void SDL_SendPendingPenProximity(void)
{
if (SDL_CompareAndSwapAtomicInt(&pending_proximity_out, true, false)) {
SDL_LockRWLockForReading(pen_device_rwlock);
for (int i = 0; i < pen_device_count; i++) {
SDL_Pen *pen = &pen_devices[i];
if (pen->pending_proximity_out) {
pen->pending_proximity_out = false;
SDL_Window *window = NULL;
if (pen->pending_proximity_window_id) {
window = SDL_GetWindowFromID(pen->pending_proximity_window_id);
if (!window) {
// The window is already gone, ignore this event
continue;
}
}
SDL_SendPenProximity(0, pen->instance_id, window, false, true);
}
}
SDL_UnlockRWLock(pen_device_rwlock);
}
}

View File

@@ -82,7 +82,10 @@ extern void SDL_SendPenAxis(Uint64 timestamp, SDL_PenID instance_id, SDL_Window
extern void SDL_SendPenButton(Uint64 timestamp, SDL_PenID instance_id, SDL_Window *window, Uint8 button, bool down);
// Backend calls this when a pen's proximity changes, to generate events and update state.
extern void SDL_SendPenProximity(Uint64 timestamp, SDL_PenID instance_id, SDL_Window *window, bool in);
extern void SDL_SendPenProximity(Uint64 timestamp, SDL_PenID instance_id, SDL_Window *window, bool in, bool immediate);
// Pumping events calls this to generate pending pen proximity events
extern void SDL_SendPendingPenProximity(void);
// Backend can optionally use this to find the SDL_PenID for the `handle` that was passed to SDL_AddPenDevice.
extern SDL_PenID SDL_FindPenByHandle(void *handle);

View File

@@ -78,12 +78,12 @@ void Android_OnPen(SDL_Window *window, int pen_id_in, SDL_PenDeviceType device_t
// we don't compare tip flags above because MotionEvent.getButtonState doesn't return stylus tip/eraser state.
switch (action) {
case ACTION_HOVER_ENTER:
SDL_SendPenProximity(0, pen, window, true);
SDL_SendPenProximity(0, pen, window, true, true);
break;
case ACTION_CANCEL:
case ACTION_HOVER_EXIT: // strictly speaking, this can mean both "proximity out" and "left the View" but close enough.
SDL_SendPenProximity(0, pen, window, false);
SDL_SendPenProximity(0, pen, window, false, false);
break;
case ACTION_DOWN:

View File

@@ -87,7 +87,7 @@ static void Cocoa_HandlePenProximityEvent(SDL_CocoaWindowData *_data, NSEvent *e
Cocoa_PenHandle *handle = Cocoa_FindPenByDeviceID(devid, toolid);
if (handle) {
handle->is_eraser = is_eraser; // in case this changed.
SDL_SendPenProximity(Cocoa_GetEventTimestamp([event timestamp]), handle->pen, _data.window, true);
SDL_SendPenProximity(Cocoa_GetEventTimestamp([event timestamp]), handle->pen, _data.window, true, true);
return; // already have this one.
}
@@ -116,7 +116,7 @@ static void Cocoa_HandlePenProximityEvent(SDL_CocoaWindowData *_data, NSEvent *e
if (handle) {
// We never remove pens (until shutdown), since Apple gives no indication when they are actually gone.
// But unless you are plugging and unplugging a tablet millions of times, generating new device IDs, this shouldn't be a massive memory drain.
SDL_SendPenProximity(Cocoa_GetEventTimestamp([event timestamp]), handle->pen, _data.window, false);
SDL_SendPenProximity(Cocoa_GetEventTimestamp([event timestamp]), handle->pen, _data.window, false, false);
}
}
}

View File

@@ -868,7 +868,7 @@ static void Emscripten_HandlePenEnter(SDL_WindowData *window_data, const Emscrip
SDL_PenID pen = SDL_FindPenByHandle((void *) (size_t) 1); // something > 0 for the single pen handle.
if (pen) {
SDL_SendPenProximity(0, pen, window_data->window, true);
SDL_SendPenProximity(0, pen, window_data->window, true, true);
} else {
// Web browsers offer almost none of this information as specifics, but can without warning offer any of these specific things.
SDL_PenInfo peninfo;
@@ -902,7 +902,7 @@ static void Emscripten_HandlePenLeave(SDL_WindowData *window_data, const Emscrip
const SDL_PenID pen = SDL_FindPenByHandle((void *) (size_t) 1); // something > 0 for the single pen handle.
if (pen) {
Emscripten_UpdatePointerFromEvent(window_data, event); // last data updates?
SDL_SendPenProximity(0, pen, window_data->window, false);
SDL_SendPenProximity(0, pen, window_data->window, false, false);
}
}

View File

@@ -3471,7 +3471,7 @@ static void tablet_tool_handle_frame(void *data, struct zwp_tablet_tool_v2 *tool
SDL_Window *window = sdltool->focus ? sdltool->focus->sdlwindow : NULL;
if (sdltool->frame.have_proximity && sdltool->frame.in_proximity) {
SDL_SendPenProximity(timestamp, instance_id, window, true);
SDL_SendPenProximity(timestamp, instance_id, window, true, true);
Wayland_TabletToolUpdateCursor(sdltool);
}
@@ -3510,7 +3510,7 @@ static void tablet_tool_handle_frame(void *data, struct zwp_tablet_tool_v2 *tool
}
if (sdltool->frame.have_proximity && !sdltool->frame.in_proximity) {
SDL_SendPenProximity(timestamp, instance_id, window, false);
SDL_SendPenProximity(timestamp, instance_id, window, false, false);
sdltool->focus = NULL;
Wayland_TabletToolUpdateCursor(sdltool);
}

View File

@@ -1282,7 +1282,7 @@ LRESULT CALLBACK WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lPara
void *hpointer = (void *)(size_t)1; // just something > 0. We're using this one ID any possible pen.
const SDL_PenID pen = SDL_FindPenByHandle(hpointer);
if (pen) {
SDL_SendPenProximity(WIN_GetEventTimestamp(), pen, data->window, true);
SDL_SendPenProximity(WIN_GetEventTimestamp(), pen, data->window, true, true);
} else {
// one can use GetPointerPenInfo() to get the current state of the pen, and check POINTER_PEN_INFO::penMask,
// but the docs aren't clear if these masks are _always_ set for pens with specific features, or if they
@@ -1323,7 +1323,7 @@ LRESULT CALLBACK WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lPara
// if this just left the _window_, we don't care. If this is no longer visible to the tablet, time to remove it!
if ((msg == WM_POINTERCAPTURECHANGED) || !IS_POINTER_INCONTACT_WPARAM(wParam)) {
// technically this isn't just _proximity_ but maybe just leaving the window. Good enough. WinTab apparently has real proximity info.
SDL_SendPenProximity(WIN_GetEventTimestamp(), pen, data->window, false);
SDL_SendPenProximity(WIN_GetEventTimestamp(), pen, data->window, false, false);
}
returnCode = 0;
} break;

View File

@@ -328,7 +328,7 @@ void X11_NotifyPenProximityChange(SDL_VideoDevice *_this, SDL_Window *window, in
bool in_proximity;
X11_PenHandle *pen = X11_FindPenByDeviceID(deviceid);
if (pen && X11_XInput2PenIsInProximity(_this, deviceid, &in_proximity)) {
SDL_SendPenProximity(0, pen->pen, window, in_proximity);
SDL_SendPenProximity(0, pen->pen, window, in_proximity, in_proximity);
}
}