From 5ead9d0ccef8fb52f7357c19f536e5cf20604591 Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Wed, 19 Nov 2025 14:01:53 -0500 Subject: [PATCH 1/5] wayland: Fixed pen button input (was losing button up events). --- src/video/wayland/SDL_waylandevents.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c index 26b6642f7c677..e4c40119f90cd 100644 --- a/src/video/wayland/SDL_waylandevents.c +++ b/src/video/wayland/SDL_waylandevents.c @@ -3408,7 +3408,7 @@ static void tablet_tool_handle_button(void *data, struct zwp_tablet_tool_v2 *too } SDL_assert((sdlbutton >= 1) && (sdlbutton <= SDL_arraysize(sdltool->frame.buttons))); - sdltool->frame.buttons[sdlbutton-1] = (state == ZWP_TABLET_PAD_V2_BUTTON_STATE_PRESSED) ? 1 : 0; + sdltool->frame.buttons[sdlbutton-1] = (state == ZWP_TABLET_PAD_V2_BUTTON_STATE_PRESSED) ? WAYLAND_TABLET_TOOL_BUTTON_DOWN : WAYLAND_TABLET_TOOL_BUTTON_UP; } static void tablet_tool_handle_rotation(void *data, struct zwp_tablet_tool_v2 *tool, wl_fixed_t degrees) From 00b233e77533e3cd8c4cbe76a8d7b59550272631 Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Wed, 19 Nov 2025 10:07:30 -0500 Subject: [PATCH 2/5] pen: Dramatic improvements to proximity information. Now everything will attempt to track pens through proximity changes (instead of removing the pen entirely). testpen.c has been updated to reflect this. Some platforms and devices are better at this than others, but this seems like a significant usability improvement across the board. Fixes #12992. --- include/SDL3/SDL_pen.h | 15 ++--- src/events/SDL_pen.c | 62 ++++++++++++++------- src/events/SDL_pen_c.h | 5 +- src/video/android/SDL_androidpen.c | 11 +++- src/video/cocoa/SDL_cocoapen.m | 9 ++- src/video/emscripten/SDL_emscriptenevents.c | 28 +++++++--- src/video/uikit/SDL_uikitpen.m | 2 +- src/video/wayland/SDL_waylandevents.c | 35 ++++++------ src/video/wayland/SDL_waylandevents_c.h | 5 +- src/video/windows/SDL_windowsevents.c | 40 +++++++------ src/video/x11/SDL_x11pen.c | 28 +++++++++- src/video/x11/SDL_x11pen.h | 3 + src/video/x11/SDL_x11xinput2.c | 12 ++++ test/testpen.c | 58 +++++++++++-------- 14 files changed, 208 insertions(+), 105 deletions(-) diff --git a/include/SDL3/SDL_pen.h b/include/SDL3/SDL_pen.h index d09b00b6208d8..5dc1731c738ba 100644 --- a/include/SDL3/SDL_pen.h +++ b/include/SDL3/SDL_pen.h @@ -92,13 +92,14 @@ typedef Uint32 SDL_PenID; */ typedef Uint32 SDL_PenInputFlags; -#define SDL_PEN_INPUT_DOWN (1u << 0) /**< pen is pressed down */ -#define SDL_PEN_INPUT_BUTTON_1 (1u << 1) /**< button 1 is pressed */ -#define SDL_PEN_INPUT_BUTTON_2 (1u << 2) /**< button 2 is pressed */ -#define SDL_PEN_INPUT_BUTTON_3 (1u << 3) /**< button 3 is pressed */ -#define SDL_PEN_INPUT_BUTTON_4 (1u << 4) /**< button 4 is pressed */ -#define SDL_PEN_INPUT_BUTTON_5 (1u << 5) /**< button 5 is pressed */ -#define SDL_PEN_INPUT_ERASER_TIP (1u << 30) /**< eraser tip is used */ +#define SDL_PEN_INPUT_DOWN (1u << 0) /**< pen is pressed down */ +#define SDL_PEN_INPUT_BUTTON_1 (1u << 1) /**< button 1 is pressed */ +#define SDL_PEN_INPUT_BUTTON_2 (1u << 2) /**< button 2 is pressed */ +#define SDL_PEN_INPUT_BUTTON_3 (1u << 3) /**< button 3 is pressed */ +#define SDL_PEN_INPUT_BUTTON_4 (1u << 4) /**< button 4 is pressed */ +#define SDL_PEN_INPUT_BUTTON_5 (1u << 5) /**< button 5 is pressed */ +#define SDL_PEN_INPUT_ERASER_TIP (1u << 30) /**< eraser tip is used */ +#define SDL_PEN_INPUT_IN_PROXIMITY (1u << 31) /**< pen is in proximity (since SDL 3.4.0) */ /** * Pen axis indices. diff --git a/src/events/SDL_pen.c b/src/events/SDL_pen.c index 18a862e865d4d..c6104864c2067 100644 --- a/src/events/SDL_pen.c +++ b/src/events/SDL_pen.c @@ -218,7 +218,7 @@ SDL_PenCapabilityFlags SDL_GetPenCapabilityFromAxis(SDL_PenAxis axis) return 0; // oh well. } -SDL_PenID SDL_AddPenDevice(Uint64 timestamp, const char *name, SDL_Window *window, const SDL_PenInfo *info, void *handle) +SDL_PenID SDL_AddPenDevice(Uint64 timestamp, const char *name, SDL_Window *window, const SDL_PenInfo *info, void *handle, bool in_proximity) { SDL_assert(handle != NULL); // just allocate a Uint8 so you have a unique pointer if not needed! SDL_assert(SDL_FindPenByHandle(handle) == 0); // Backends shouldn't double-add pens! @@ -256,14 +256,8 @@ SDL_PenID SDL_AddPenDevice(Uint64 timestamp, const char *name, SDL_Window *windo SDL_free(namecpy); } - if (result && SDL_EventEnabled(SDL_EVENT_PEN_PROXIMITY_IN)) { - SDL_Event event; - SDL_zero(event); - event.pproximity.type = SDL_EVENT_PEN_PROXIMITY_IN; - event.pproximity.timestamp = timestamp; - event.pproximity.which = result; - event.pproximity.windowID = window ? window->id : 0; - SDL_PushEvent(&event); + if (result) { + SDL_SendPenProximity(timestamp, result, window, in_proximity); } return result; @@ -275,6 +269,8 @@ void SDL_RemovePenDevice(Uint64 timestamp, SDL_Window *window, SDL_PenID instanc return; } + SDL_SendPenProximity(timestamp, instance_id, window, false); // bye bye + SDL_LockRWLockForWriting(pen_device_rwlock); SDL_Pen *pen = FindPenByInstanceId(instance_id); if (pen) { @@ -300,16 +296,6 @@ void SDL_RemovePenDevice(Uint64 timestamp, SDL_Window *window, SDL_PenID instanc } } SDL_UnlockRWLock(pen_device_rwlock); - - if (pen && SDL_EventEnabled(SDL_EVENT_PEN_PROXIMITY_OUT)) { - SDL_Event event; - SDL_zero(event); - event.pproximity.type = SDL_EVENT_PEN_PROXIMITY_OUT; - event.pproximity.timestamp = timestamp; - event.pproximity.which = instance_id; - event.pproximity.windowID = window ? window->id : 0; - SDL_PushEvent(&event); - } } // This presumably is happening during video quit, so we don't send PROXIMITY_OUT events here. @@ -595,3 +581,41 @@ 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) +{ + bool send_event = false; + SDL_PenInputFlags input_state = 0; + + // note that this locks for _reading_ because the lock protects the + // pen_devices array from being reallocated from under us, not the data in it; + // we assume only one thread (in the backend) is modifying an individual pen at + // a time, so it can update input state cleanly here. + 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; + } + send_event = true; + pen->input_state = input_state; // we could do an SDL_SetAtomicInt here if we run into trouble... + } + } + SDL_UnlockRWLock(pen_device_rwlock); + + const Uint32 event_type = in ? SDL_EVENT_PEN_PROXIMITY_IN : SDL_EVENT_PEN_PROXIMITY_OUT; + if (send_event && SDL_EventEnabled(event_type)) { + SDL_Event event; + SDL_zero(event); + event.pproximity.type = event_type; + event.pproximity.timestamp = timestamp; + event.pproximity.windowID = window ? window->id : 0; + event.pproximity.which = instance_id; + SDL_PushEvent(&event); + } +} + diff --git a/src/events/SDL_pen_c.h b/src/events/SDL_pen_c.h index 69539bde2ad19..d3cc0d471a187 100644 --- a/src/events/SDL_pen_c.h +++ b/src/events/SDL_pen_c.h @@ -61,7 +61,7 @@ typedef struct SDL_PenInfo // Backend calls this when a new pen device is hotplugged, plus once for each pen already connected at startup. // Note that name and info are copied but currently unused; this is placeholder for a potentially more robust API later. // Both are allowed to be NULL. -extern SDL_PenID SDL_AddPenDevice(Uint64 timestamp, const char *name, SDL_Window *window, const SDL_PenInfo *info, void *handle); +extern SDL_PenID SDL_AddPenDevice(Uint64 timestamp, const char *name, SDL_Window *window, const SDL_PenInfo *info, void *handle, bool in_proximity); // Backend calls this when an existing pen device is disconnected during runtime. They must free their own stuff separately. extern void SDL_RemovePenDevice(Uint64 timestamp, SDL_Window *window, SDL_PenID instance_id); @@ -81,6 +81,9 @@ extern void SDL_SendPenAxis(Uint64 timestamp, SDL_PenID instance_id, SDL_Window // Backend calls this when a pen's button changes, to generate events and update state. extern void SDL_SendPenButton(Uint64 timestamp, SDL_PenID instance_id, SDL_Window *window, Uint8 button, bool down); +// Backend calls this when a pen's button changes, to generate events and update state. +extern void SDL_SendPenProximity(Uint64 timestamp, SDL_PenID instance_id, SDL_Window *window, bool in); + // 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); diff --git a/src/video/android/SDL_androidpen.c b/src/video/android/SDL_androidpen.c index 7bfde272e7279..feb03bbd96c64 100644 --- a/src/video/android/SDL_androidpen.c +++ b/src/video/android/SDL_androidpen.c @@ -31,6 +31,7 @@ #define ACTION_CANCEL 3 #define ACTION_POINTER_DOWN 5 #define ACTION_POINTER_UP 6 +#define ACTION_HOVER_ENTER 9 #define ACTION_HOVER_EXIT 10 void Android_OnPen(SDL_Window *window, int pen_id_in, SDL_PenDeviceType device_type, int button, int action, float x, float y, float p) @@ -51,7 +52,7 @@ void Android_OnPen(SDL_Window *window, int pen_id_in, SDL_PenDeviceType device_t peninfo.num_buttons = 2; peninfo.subtype = SDL_PEN_TYPE_PEN; peninfo.device_type = device_type; - pen = SDL_AddPenDevice(0, NULL, window, &peninfo, (void *) (size_t) pen_id_in); + pen = SDL_AddPenDevice(0, NULL, window, &peninfo, (void *) (size_t) pen_id_in, true); if (!pen) { SDL_Log("error: can't add a pen device %d", pen_id_in); return; @@ -76,9 +77,13 @@ void Android_OnPen(SDL_Window *window, int pen_id_in, SDL_PenDeviceType device_t // button contains DOWN/ERASER_TIP on DOWN/UP regardless of pressed state, use action to distinguish // 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); + break; + case ACTION_CANCEL: - case ACTION_HOVER_EXIT: - SDL_RemovePenDevice(0, window, pen); + 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); break; case ACTION_DOWN: diff --git a/src/video/cocoa/SDL_cocoapen.m b/src/video/cocoa/SDL_cocoapen.m index b698bc571c55e..d0c3b40831150 100644 --- a/src/video/cocoa/SDL_cocoapen.m +++ b/src/video/cocoa/SDL_cocoapen.m @@ -86,6 +86,8 @@ 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); return; // already have this one. } @@ -105,15 +107,16 @@ static void Cocoa_HandlePenProximityEvent(SDL_CocoaWindowData *_data, NSEvent *e handle->deviceid = devid; handle->toolid = toolid; handle->is_eraser = is_eraser; - handle->pen = SDL_AddPenDevice(Cocoa_GetEventTimestamp([event timestamp]), NULL, _data.window, &peninfo, handle); + handle->pen = SDL_AddPenDevice(Cocoa_GetEventTimestamp([event timestamp]), NULL, _data.window, &peninfo, handle, true); if (!handle->pen) { SDL_free(handle); // oh well. } } else { // old pen leaving! Cocoa_PenHandle *handle = Cocoa_FindPenByDeviceID(devid, toolid); if (handle) { - SDL_RemovePenDevice(Cocoa_GetEventTimestamp([event timestamp]), _data.window, handle->pen); - SDL_free(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); } } } diff --git a/src/video/emscripten/SDL_emscriptenevents.c b/src/video/emscripten/SDL_emscriptenevents.c index 07f9b2e43ec51..e378ff8cc77cb 100644 --- a/src/video/emscripten/SDL_emscriptenevents.c +++ b/src/video/emscripten/SDL_emscriptenevents.c @@ -848,14 +848,23 @@ static void Emscripten_HandleMouseFocus(SDL_WindowData *window_data, const Emscr static void Emscripten_HandlePenEnter(SDL_WindowData *window_data, const Emscripten_PointerEvent *event) { SDL_assert(event->pointer_type == PTRTYPE_PEN); - // Web browsers offer almost none of this information as specifics, but can without warning offer any of these specific things. - SDL_PenInfo peninfo; - SDL_zero(peninfo); - peninfo.capabilities = SDL_PEN_CAPABILITY_PRESSURE | SDL_PEN_CAPABILITY_ROTATION | SDL_PEN_CAPABILITY_XTILT | SDL_PEN_CAPABILITY_YTILT | SDL_PEN_CAPABILITY_TANGENTIAL_PRESSURE | SDL_PEN_CAPABILITY_ERASER; - peninfo.max_tilt = 90.0f; - peninfo.num_buttons = 2; - peninfo.subtype = SDL_PEN_TYPE_PEN; - SDL_AddPenDevice(0, NULL, window_data->window, &peninfo, (void *) (size_t) event->pointerid); + +SDL_Log("PEN ENTER pointerid=%d", event->pointerid); + + SDL_PenID pen = SDL_FindPenByHandle((void *) (size_t) event->pointerid); + if (pen) { + SDL_SendPenProximity(0, pen, window_data->window, 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; + SDL_zero(peninfo); + peninfo.capabilities = SDL_PEN_CAPABILITY_PRESSURE | SDL_PEN_CAPABILITY_ROTATION | SDL_PEN_CAPABILITY_XTILT | SDL_PEN_CAPABILITY_YTILT | SDL_PEN_CAPABILITY_TANGENTIAL_PRESSURE | SDL_PEN_CAPABILITY_ERASER; + peninfo.max_tilt = 90.0f; + peninfo.num_buttons = 2; + peninfo.subtype = SDL_PEN_TYPE_PEN; + SDL_AddPenDevice(0, NULL, window_data->window, &peninfo, (void *) (size_t) event->pointerid, true); + } + Emscripten_UpdatePenFromEvent(window_data, event); } @@ -875,10 +884,11 @@ EMSCRIPTEN_KEEPALIVE void Emscripten_HandlePointerEnter(SDL_WindowData *window_d static void Emscripten_HandlePenLeave(SDL_WindowData *window_data, const Emscripten_PointerEvent *event) { +SDL_Log("PEN LEAVE pointerid=%d", event->pointerid); const SDL_PenID pen = SDL_FindPenByHandle((void *) (size_t) event->pointerid); if (pen) { Emscripten_UpdatePointerFromEvent(window_data, event); // last data updates? - SDL_RemovePenDevice(0, window_data->window, pen); + SDL_SendPenProximity(0, pen, window_data->window, false); } } diff --git a/src/video/uikit/SDL_uikitpen.m b/src/video/uikit/SDL_uikitpen.m index 9c58e17104fb4..25cd0f401353e 100644 --- a/src/video/uikit/SDL_uikitpen.m +++ b/src/video/uikit/SDL_uikitpen.m @@ -86,7 +86,7 @@ static SDL_PenID UIKit_AddPenIfNecesary(SDL_Window *window) // so we can't use it for tangential pressure. // There's only ever one Apple Pencil at most, so we just pass a non-zero value for the handle. - apple_pencil_id = SDL_AddPenDevice(0, "Apple Pencil", window, &info, (void *) (size_t) 0x1); + apple_pencil_id = SDL_AddPenDevice(0, "Apple Pencil", window, &info, (void *) (size_t) 0x1, true); } return apple_pencil_id; diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c index e4c40119f90cd..2eaaee87823c6 100644 --- a/src/video/wayland/SDL_waylandevents.c +++ b/src/video/wayland/SDL_waylandevents.c @@ -3301,6 +3301,11 @@ static void tablet_tool_handle_capability(void *data, struct zwp_tablet_tool_v2 static void tablet_tool_handle_done(void *data, struct zwp_tablet_tool_v2 *tool) { + SDL_WaylandPenTool *sdltool = (SDL_WaylandPenTool *) data; + if (sdltool->info.subtype != SDL_PEN_TYPE_UNKNOWN) { // don't tell SDL about it if we don't know its role. + SDL_Window *window = sdltool->focus ? sdltool->focus->sdlwindow : NULL; + sdltool->instance_id = SDL_AddPenDevice(0, NULL, window, &sdltool->info, sdltool, false); + } } static void tablet_tool_handle_removed(void *data, struct zwp_tablet_tool_v2 *tool) @@ -3323,7 +3328,8 @@ static void tablet_tool_handle_proximity_in(void *data, struct zwp_tablet_tool_v SDL_WindowData *windowdata = surface ? Wayland_GetWindowDataForOwnedSurface(surface) : NULL; sdltool->focus = windowdata; sdltool->proximity_serial = serial; - sdltool->frame.have_proximity_in = true; + sdltool->frame.have_proximity = true; + sdltool->frame.in_proximity = true; // According to the docs, this should be followed by a frame event, where we'll send our SDL events. } @@ -3331,7 +3337,8 @@ static void tablet_tool_handle_proximity_in(void *data, struct zwp_tablet_tool_v static void tablet_tool_handle_proximity_out(void *data, struct zwp_tablet_tool_v2 *tool) { SDL_WaylandPenTool *sdltool = (SDL_WaylandPenTool *) data; - sdltool->frame.have_proximity_out = true; + sdltool->frame.have_proximity = true; + sdltool->frame.in_proximity = false; } static void tablet_tool_handle_down(void *data, struct zwp_tablet_tool_v2 *tool, uint32_t serial) @@ -3434,22 +3441,17 @@ static void tablet_tool_handle_wheel(void *data, struct zwp_tablet_tool_v2 *tool static void tablet_tool_handle_frame(void *data, struct zwp_tablet_tool_v2 *tool, uint32_t time) { SDL_WaylandPenTool *sdltool = (SDL_WaylandPenTool *) data; + const SDL_PenID instance_id = sdltool->instance_id; + if (!instance_id) { + return; // Not a pen we report on. + } const Uint64 timestamp = Wayland_AdjustEventTimestampBase(Wayland_EventTimestampMSToNS(time)); SDL_Window *window = sdltool->focus ? sdltool->focus->sdlwindow : NULL; - if (sdltool->frame.have_proximity_in) { - SDL_assert(sdltool->instance_id == 0); // shouldn't be added at this point. - if (sdltool->info.subtype != SDL_PEN_TYPE_UNKNOWN) { // don't tell SDL about it if we don't know its role. - sdltool->instance_id = SDL_AddPenDevice(timestamp, NULL, window, &sdltool->info, sdltool); - Wayland_TabletToolUpdateCursor(sdltool); - } - } - - const SDL_PenID instance_id = sdltool->instance_id; - - if (!instance_id) { - return; // Not a pen we report on. + if (sdltool->frame.have_proximity && sdltool->frame.in_proximity) { + SDL_SendPenProximity(timestamp, instance_id, window, true); + Wayland_TabletToolUpdateCursor(sdltool); } // !!! FIXME: Should hit testing be done if pens generate pointer motion? @@ -3486,11 +3488,10 @@ static void tablet_tool_handle_frame(void *data, struct zwp_tablet_tool_v2 *tool } } - if (sdltool->frame.have_proximity_out) { + if (sdltool->frame.have_proximity && !sdltool->frame.in_proximity) { + SDL_SendPenProximity(timestamp, instance_id, window, false); sdltool->focus = NULL; Wayland_TabletToolUpdateCursor(sdltool); - SDL_RemovePenDevice(timestamp, window, sdltool->instance_id); - sdltool->instance_id = 0; } // Reset for the next frame. diff --git a/src/video/wayland/SDL_waylandevents_c.h b/src/video/wayland/SDL_waylandevents_c.h index 59488f53cb8fe..f8e52e068e4c7 100644 --- a/src/video/wayland/SDL_waylandevents_c.h +++ b/src/video/wayland/SDL_waylandevents_c.h @@ -107,9 +107,10 @@ typedef struct SDL_WaylandPenTool // a stylus, etc, on a tablet. WAYLAND_TABLET_TOOL_STATE_UP } tool_state; + bool in_proximity; + bool have_motion; - bool have_proximity_in; - bool have_proximity_out; + bool have_proximity; } frame; SDL_WaylandCursorState cursor_state; diff --git a/src/video/windows/SDL_windowsevents.c b/src/video/windows/SDL_windowsevents.c index 3c8dd7c0d31c8..5abc751ea815c 100644 --- a/src/video/windows/SDL_windowsevents.c +++ b/src/video/windows/SDL_windowsevents.c @@ -666,7 +666,7 @@ static void WIN_HandleRawMouseInput(Uint64 timestamp, SDL_VideoData *data, HANDL int screen_y = virtual_desktop ? GetSystemMetrics(SM_YVIRTUALSCREEN) : 0; if (!data->raw_input_fake_pen_id) { - data->raw_input_fake_pen_id = SDL_AddPenDevice(timestamp, "raw mouse input", window, NULL, (void *)(size_t)-1); + data->raw_input_fake_pen_id = SDL_AddPenDevice(timestamp, "raw mouse input", window, NULL, (void *)(size_t)-1, true); } SDL_SendPenMotion(timestamp, data->raw_input_fake_pen_id, window, (float)(x + screen_x - window->x), (float)(y + screen_y - window->y)); } @@ -1275,22 +1275,25 @@ LRESULT CALLBACK WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lPara break; // oh well. } else if (pointer_type != PT_PEN) { break; // we only care about pens here. - } else if (SDL_FindPenByHandle(hpointer)) { - break; // we already have this one, don't readd it. - } - - // 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 - // could be unset at this moment because Windows is still deciding what capabilities the pen has, and/or - // doesn't yet have valid data for them. As such, just say everything that the interface supports is - // available...we don't expose this information through the public API at the moment anyhow. - SDL_PenInfo info; - SDL_zero(info); - info.capabilities = SDL_PEN_CAPABILITY_PRESSURE | SDL_PEN_CAPABILITY_XTILT | SDL_PEN_CAPABILITY_YTILT | SDL_PEN_CAPABILITY_DISTANCE | SDL_PEN_CAPABILITY_ROTATION | SDL_PEN_CAPABILITY_ERASER; - info.max_tilt = 90.0f; - info.num_buttons = 1; - info.subtype = SDL_PEN_TYPE_PENCIL; - SDL_AddPenDevice(0, NULL, data->window, &info, hpointer); + } + + const SDL_PenID pen = SDL_FindPenByHandle(hpointer); + if (pen) { + SDL_SendPenProximity(WIN_GetEventTimestamp(), pen, data->window, 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 + // could be unset at this moment because Windows is still deciding what capabilities the pen has, and/or + // doesn't yet have valid data for them. As such, just say everything that the interface supports is + // available...we don't expose this information through the public API at the moment anyhow. + SDL_PenInfo info; + SDL_zero(info); + info.capabilities = SDL_PEN_CAPABILITY_PRESSURE | SDL_PEN_CAPABILITY_XTILT | SDL_PEN_CAPABILITY_YTILT | SDL_PEN_CAPABILITY_DISTANCE | SDL_PEN_CAPABILITY_ROTATION | SDL_PEN_CAPABILITY_ERASER; + info.max_tilt = 90.0f; + info.num_buttons = 1; + info.subtype = SDL_PEN_TYPE_PENCIL; + SDL_AddPenDevice(WIN_GetEventTimestamp(), NULL, data->window, &info, hpointer, true); + } returnCode = 0; } break; @@ -1306,7 +1309,8 @@ 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)) { - SDL_RemovePenDevice(WIN_GetEventTimestamp(), data->window, pen); + // 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); } returnCode = 0; } break; diff --git a/src/video/x11/SDL_x11pen.c b/src/video/x11/SDL_x11pen.c index 24bec559b0e8b..c885de2f9a483 100644 --- a/src/video/x11/SDL_x11pen.c +++ b/src/video/x11/SDL_x11pen.c @@ -167,6 +167,18 @@ static bool X11_XInput2PenWacomDeviceID(SDL_VideoDevice *_this, int deviceid, Ui return false; } +// Check if a Wacom device is in proximity of the tablet +static bool X11_XInput2PenIsInProximity(SDL_VideoDevice *_this, int deviceid, bool *in_proximity) +{ + SDL_VideoData *data = _this->internal; + Sint32 serial_id_buf[5]; + if (X11_XInput2PenGetIntProperty(_this, deviceid, data->atoms.pen_atom_wacom_serial_ids, serial_id_buf, 5) == 5) { + *in_proximity = serial_id_buf[4] != 0 || serial_id_buf[3] != 0; + return true; + } + return false; +} + typedef struct FindPenByDeviceIDData { @@ -272,7 +284,12 @@ static X11_PenHandle *X11_MaybeAddPen(SDL_VideoDevice *_this, const XIDeviceInfo handle->is_eraser = is_eraser; handle->x11_deviceid = dev->deviceid; - handle->pen = SDL_AddPenDevice(0, dev->name, NULL, &peninfo, handle); + bool in_proximity = false; + if (!X11_XInput2PenIsInProximity(_this, dev->deviceid, &in_proximity)) { + in_proximity = true; // just say it's in proximity if we can't detect this state. + } + + handle->pen = SDL_AddPenDevice(0, dev->name, NULL, &peninfo, handle, in_proximity); if (!handle->pen) { SDL_free(handle); return NULL; @@ -306,6 +323,15 @@ void X11_RemovePenByDeviceID(int deviceid) } } +void X11_NotifyPenProximityChange(SDL_VideoDevice *_this, SDL_Window *window, int deviceid) +{ + 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); + } +} + void X11_InitPen(SDL_VideoDevice *_this) { if (!X11_Xinput2IsInitialized()) { diff --git a/src/video/x11/SDL_x11pen.h b/src/video/x11/SDL_x11pen.h index de7518151eeea..fd1533b53fb2b 100644 --- a/src/video/x11/SDL_x11pen.h +++ b/src/video/x11/SDL_x11pen.h @@ -67,6 +67,9 @@ extern void X11_RemovePenByDeviceID(int deviceid); // Map X11 device ID to pen ID. extern X11_PenHandle *X11_FindPenByDeviceID(int deviceid); +// Notify that the pen has entered/left proximity +extern void X11_NotifyPenProximityChange(SDL_VideoDevice *_this, SDL_Window *window, int deviceid); + #endif // SDL_VIDEO_DRIVER_X11_XINPUT2 #endif // SDL_x11pen_h_ diff --git a/src/video/x11/SDL_x11xinput2.c b/src/video/x11/SDL_x11xinput2.c index a4487dcfabc6a..5d241617c3b01 100644 --- a/src/video/x11/SDL_x11xinput2.c +++ b/src/video/x11/SDL_x11xinput2.c @@ -130,6 +130,7 @@ static bool xinput2_version_atleast(const int version, const int wantmajor, cons return version >= ((wantmajor * 1000) + wantminor); } +// !!! FIXME: isn't this just X11_FindWindow? static SDL_WindowData *xinput2_get_sdlwindowdata(SDL_VideoData *videodata, Window window) { int i; @@ -512,6 +513,17 @@ void X11_HandleXinput2Event(SDL_VideoDevice *_this, XGenericEventCookie *cookie) //case XI_PropertyEvent: //case XI_DeviceChanged: + case XI_PropertyEvent: + { + const XIPropertyEvent *proev = (const XIPropertyEvent *)cookie->data; + // Handle pen proximity enter/leave + if (proev->what == XIPropertyModified && proev->property == videodata->atoms.pen_atom_wacom_serial_ids) { + const XIDeviceEvent *xev = (const XIDeviceEvent *)cookie->data; + SDL_WindowData *windowdata = X11_FindWindow(_this, xev->event); + X11_NotifyPenProximityChange(_this, windowdata ? windowdata->window : NULL, proev->deviceid); + } + } break; + case XI_RawMotion: { const XIRawEvent *rawev = (const XIRawEvent *)cookie->data; diff --git a/test/testpen.c b/test/testpen.c index bb6e7d2cc0ab3..6626b5145c65e 100644 --- a/test/testpen.c +++ b/test/testpen.c @@ -25,6 +25,7 @@ typedef struct Pen Uint32 buttons; bool eraser; bool touching; + bool in_proximity; struct Pen *next; } Pen; @@ -107,44 +108,49 @@ static Pen *FindPen(SDL_PenID which) SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) { Pen *pen = NULL; + Pen *i = NULL; switch (event->type) { - case SDL_EVENT_PEN_PROXIMITY_IN: { - pen = (Pen *) SDL_calloc(1, sizeof (*pen)); - if (!pen) { - SDL_Log("Out of memory!"); - return SDL_APP_FAILURE; + case SDL_EVENT_PEN_PROXIMITY_IN: + SDL_Log("Pen %" SDL_PRIu32 " enters proximity!", event->pproximity.which); + + for (i = pens.next; i != NULL; i = i->next) { + if (i->pen == event->pproximity.which) { + pen = i; + break; + } } - SDL_Log("Pen %" SDL_PRIu32 " enters proximity!", event->pproximity.which); - pen->pen = event->pproximity.which; - pen->r = (Uint8) SDL_rand(256); - pen->g = (Uint8) SDL_rand(256); - pen->b = (Uint8) SDL_rand(256); - pen->x = 320.0f; - pen->y = 240.0f; - pen->next = pens.next; - pens.next = pen; + if (!pen) { + SDL_Log("This is the first time we've seen this pen."); + pen = (Pen *) SDL_calloc(1, sizeof (*pen)); + if (!pen) { + SDL_Log("Out of memory!"); + return SDL_APP_FAILURE; + } - return SDL_APP_CONTINUE; - } + pen->pen = event->pproximity.which; + pen->r = (Uint8) SDL_rand(256); + pen->g = (Uint8) SDL_rand(256); + pen->b = (Uint8) SDL_rand(256); + pen->x = 320.0f; + pen->y = 240.0f; + pen->next = pens.next; + pens.next = pen; + } - case SDL_EVENT_PEN_PROXIMITY_OUT: { - Pen *prev = &pens; - Pen *i; + pen->in_proximity = true; + return SDL_APP_CONTINUE; + case SDL_EVENT_PEN_PROXIMITY_OUT: SDL_Log("Pen %" SDL_PRIu32 " leaves proximity!", event->pproximity.which); for (i = pens.next; i != NULL; i = i->next) { if (i->pen == event->pproximity.which) { - prev->next = i->next; - SDL_free(i); + i->in_proximity = false; break; } - prev = i; } - return SDL_APP_CONTINUE; - } case SDL_EVENT_PEN_DOWN: /*SDL_Log("Pen %" SDL_PRIu32 " down!", event->ptouch.which);*/ @@ -220,6 +226,10 @@ static void DrawOnePen(Pen *pen, int num) { int i; + if (!pen->in_proximity) { + return; + } + /* draw button presses for this pen. A square for each in the pen's color, offset down the screen so they don't overlap. */ SDL_SetRenderDrawColor(renderer, pen->r, pen->g, pen->b, 255); for (i = 0; i < 8; i++) { /* we assume you don't have more than 8 buttons atm... */ From 72bc8953ff4dbbd5fec97526a018c0c775c675a4 Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Thu, 20 Nov 2025 10:21:30 -0500 Subject: [PATCH 3/5] pen: Only allow one pen on Windows. This "fixes" proximity but limits you to a single pen device. But this is probably okay for most reasonable use cases. --- src/video/windows/SDL_windowsevents.c | 44 ++++++++++++++++----------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/src/video/windows/SDL_windowsevents.c b/src/video/windows/SDL_windowsevents.c index 5abc751ea815c..2ad327441b808 100644 --- a/src/video/windows/SDL_windowsevents.c +++ b/src/video/windows/SDL_windowsevents.c @@ -1264,19 +1264,18 @@ LRESULT CALLBACK WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lPara case WM_POINTERENTER: { - if (!data->videodata->GetPointerType) { - break; // Not on Windows8 or later? We shouldn't get this event, but just in case... - } - + // NOTE: GET_POINTERID_WPARAM(wParam) is not a tool ID! It changes for each new WM_POINTERENTER, like a finger ID on a touch display. We can't identify a specific pen through these events. const UINT32 pointerid = GET_POINTERID_WPARAM(wParam); - void *hpointer = (void *) (size_t) pointerid; POINTER_INPUT_TYPE pointer_type = PT_POINTER; - if (!data->videodata->GetPointerType(pointerid, &pointer_type)) { + if (!data->videodata->GetPointerType) { + break; // Not on Windows8 or later? We shouldn't get this event, but just in case... + } else if (!data->videodata->GetPointerType(pointerid, &pointer_type)) { break; // oh well. } else if (pointer_type != PT_PEN) { break; // we only care about pens here. } + 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); @@ -1300,8 +1299,18 @@ LRESULT CALLBACK WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lPara case WM_POINTERCAPTURECHANGED: case WM_POINTERLEAVE: { + // NOTE: GET_POINTERID_WPARAM(wParam) is not a tool ID! It changes for each new WM_POINTERENTER, like a finger ID on a touch display. We can't identify a specific pen through these events. const UINT32 pointerid = GET_POINTERID_WPARAM(wParam); - void *hpointer = (void *) (size_t) pointerid; + POINTER_INPUT_TYPE pointer_type = PT_POINTER; + if (!data->videodata->GetPointerType) { + break; // Not on Windows8 or later? We shouldn't get this event, but just in case... + } else if (!data->videodata->GetPointerType(pointerid, &pointer_type)) { + break; // oh well. + } else if (pointer_type != PT_PEN) { + break; // we only care about pens here. + } + + 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 == 0) { break; // not a pen, or not a pen we already knew about. @@ -1315,26 +1324,25 @@ LRESULT CALLBACK WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lPara returnCode = 0; } break; + case WM_POINTERDOWN: + case WM_POINTERUP: case WM_POINTERUPDATE: { + // NOTE: GET_POINTERID_WPARAM(wParam) is not a tool ID! It changes for each new WM_POINTERENTER, like a finger ID on a touch display. We can't identify a specific pen through these events. + const UINT32 pointerid = GET_POINTERID_WPARAM(wParam); POINTER_INPUT_TYPE pointer_type = PT_POINTER; - if (!data->videodata->GetPointerType || !data->videodata->GetPointerType(GET_POINTERID_WPARAM(wParam), &pointer_type)) { + if (!data->videodata->GetPointerType || !data->videodata->GetPointerType(pointerid, &pointer_type)) { break; // oh well. - } - - if (pointer_type == PT_MOUSE) { + } else if ((msg == WM_POINTERUPDATE) && (pointer_type == PT_MOUSE)) { data->last_pointer_update = lParam; returnCode = 0; break; + } else if (pointer_type != PT_PEN) { + break; // we only care about pens here. } - } - SDL_FALLTHROUGH; - case WM_POINTERDOWN: - case WM_POINTERUP: { - POINTER_PEN_INFO pen_info; - const UINT32 pointerid = GET_POINTERID_WPARAM(wParam); - void *hpointer = (void *) (size_t) pointerid; + 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); + POINTER_PEN_INFO pen_info; if (pen == 0) { break; // not a pen, or not a pen we already knew about. } else if (!data->videodata->GetPointerPenInfo || !data->videodata->GetPointerPenInfo(pointerid, &pen_info)) { From a774fcb40907586d275ea4c93ff92e9eff1d61fc Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Thu, 20 Nov 2025 16:49:50 -0500 Subject: [PATCH 4/5] pen: Only allow one pen on Emscripten. Same deal as the current Windows code: this "fixes" proximity but limits you to a single pen device. But this is probably okay for most reasonable use cases. --- src/video/emscripten/SDL_emscriptenevents.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/video/emscripten/SDL_emscriptenevents.c b/src/video/emscripten/SDL_emscriptenevents.c index e378ff8cc77cb..66cb71c2b1861 100644 --- a/src/video/emscripten/SDL_emscriptenevents.c +++ b/src/video/emscripten/SDL_emscriptenevents.c @@ -763,7 +763,7 @@ static void Emscripten_UpdateTouchFromEvent(SDL_WindowData *window_data, const E static void Emscripten_UpdatePenFromEvent(SDL_WindowData *window_data, const Emscripten_PointerEvent *event) { SDL_assert(event->pointer_type == PTRTYPE_PEN); - const SDL_PenID pen = SDL_FindPenByHandle((void *) (size_t) event->pointerid); + const SDL_PenID pen = SDL_FindPenByHandle((void *) (size_t) 1); // something > 0 for the single pen handle. if (pen) { // rescale (in case canvas is being scaled) double client_w, client_h; @@ -849,9 +849,10 @@ static void Emscripten_HandlePenEnter(SDL_WindowData *window_data, const Emscrip { SDL_assert(event->pointer_type == PTRTYPE_PEN); -SDL_Log("PEN ENTER pointerid=%d", event->pointerid); + // event->pointerid is one continuous interaction; it doesn't necessarily track a specific tool over time, like the same finger's ID changed on each new touch event. + // as such, we only expose a single pen, and when the touch ends, we say it lost proximity instead of the calling SDL_RemovePenDevice(). - SDL_PenID pen = SDL_FindPenByHandle((void *) (size_t) event->pointerid); + 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); } else { @@ -862,7 +863,7 @@ SDL_Log("PEN ENTER pointerid=%d", event->pointerid); peninfo.max_tilt = 90.0f; peninfo.num_buttons = 2; peninfo.subtype = SDL_PEN_TYPE_PEN; - SDL_AddPenDevice(0, NULL, window_data->window, &peninfo, (void *) (size_t) event->pointerid, true); + SDL_AddPenDevice(0, NULL, window_data->window, &peninfo, (void *) (size_t) 1, true); } Emscripten_UpdatePenFromEvent(window_data, event); @@ -884,8 +885,7 @@ EMSCRIPTEN_KEEPALIVE void Emscripten_HandlePointerEnter(SDL_WindowData *window_d static void Emscripten_HandlePenLeave(SDL_WindowData *window_data, const Emscripten_PointerEvent *event) { -SDL_Log("PEN LEAVE pointerid=%d", event->pointerid); - const SDL_PenID pen = SDL_FindPenByHandle((void *) (size_t) event->pointerid); + 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); From cbf84d71704f413e4cd468c19b9a3c5fe8686a66 Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Thu, 20 Nov 2025 17:20:09 -0500 Subject: [PATCH 5/5] pen: Updated documentation to meet current expectations and realities. --- include/SDL3/SDL_pen.h | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/include/SDL3/SDL_pen.h b/include/SDL3/SDL_pen.h index 5dc1731c738ba..e8270ce61da27 100644 --- a/include/SDL3/SDL_pen.h +++ b/include/SDL3/SDL_pen.h @@ -37,12 +37,28 @@ * - SDL_EVENT_PEN_BUTTON_DOWN, SDL_EVENT_PEN_BUTTON_UP (SDL_PenButtonEvent) * - SDL_EVENT_PEN_AXIS (SDL_PenAxisEvent) * - * When a pen starts providing input, SDL will assign it a unique SDL_PenID, - * which will remain for the life of the process, as long as the pen stays - * connected. - * * Pens may provide more than simple touch input; they might have other axes, * such as pressure, tilt, rotation, etc. + * + * When a pen starts providing input, SDL will assign it a unique SDL_PenID, + * which will remain for the life of the process, as long as the pen stays + * connected. A pen leaving proximity (being taken far enough away from the + * digitizer tablet that it no longer reponds) and then coming back should + * fire proximity events, but the SDL_PenID should remain consistent. + * Unplugging the digitizer and reconnecting may cause future input to have + * a new SDL_PenID, as SDL may not know that this is the same hardware. + * + * Please note that various platforms vary wildly in how (and how well) they + * support pen input. If your pen supports some piece of functionality but SDL + * doesn't seem to, it might actually be the operating system's fault. For + * example, some platforms can manage multiple devices at the same time, but + * others will make any connected pens look like a single logical device, much + * how all USB mice connected to a computer will move the same system cursor. + * cursor. Other platforms might not support pen buttons, or the distance + * axis, etc. Very few platforms can even report _what_ functionality the pen + * supports in the first place, so best practices is to either build UI to + * let the user configure their pens, or be prepared to handle new + * functionality for a pen the first time an event is reported. */ #ifndef SDL_pen_h_ @@ -65,7 +81,12 @@ extern "C" { * * These show up in pen events when SDL sees input from them. They remain * consistent as long as SDL can recognize a tool to be the same pen; but if a - * pen physically leaves the area and returns, it might get a new ID. + * pen's digitizer table is physically detached from the computer, it might get + * a new ID when reconnected, as SDL won't know it's the same device. + * + * These IDs are only stable within a single run of a program; the next time + * a program is run, the pen's ID will likely be different, even if the + * hardware hasn't been disconnected, etc. * * \since This datatype is available since SDL 3.2.0. */