SDL: pen: Offer the current window during promixity events on most platforms.

From 25ab8c99dfefff653f95a75232d4cce960fb7897 Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Mon, 10 Nov 2025 16:07:07 -0500
Subject: [PATCH] pen: Offer the current window during promixity events on most
 platforms.

Fixes #12356.
---
 include/SDL3/SDL_events.h                   |  3 +++
 src/events/SDL_pen.c                        |  6 ++++--
 src/events/SDL_pen_c.h                      |  4 ++--
 src/video/android/SDL_androidpen.c          |  4 ++--
 src/video/cocoa/SDL_cocoapen.m              |  4 ++--
 src/video/emscripten/SDL_emscriptenevents.c |  4 ++--
 src/video/uikit/SDL_uikitpen.m              | 18 ++++++++++--------
 src/video/wayland/SDL_waylandevents.c       |  9 +++++----
 src/video/windows/SDL_windowsevents.c       |  4 ++--
 src/video/x11/SDL_x11pen.c                  |  4 ++--
 10 files changed, 34 insertions(+), 26 deletions(-)

diff --git a/include/SDL3/SDL_events.h b/include/SDL3/SDL_events.h
index 7513aa249d9b9..4c06e45b100f9 100644
--- a/include/SDL3/SDL_events.h
+++ b/include/SDL3/SDL_events.h
@@ -818,6 +818,9 @@ typedef struct SDL_PinchFingerEvent
  * is there." The pen touching and lifting off from the tablet while not
  * leaving the area are handled by SDL_EVENT_PEN_DOWN and SDL_EVENT_PEN_UP.
  *
+ * Not all platforms have a window associated with the pen during proximity
+ * events. Some wait until motion/button/etc events to offer this info.
+ *
  * \since This struct is available since SDL 3.2.0.
  */
 typedef struct SDL_PenProximityEvent
diff --git a/src/events/SDL_pen.c b/src/events/SDL_pen.c
index f8cdc60830077..18a862e865d4d 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, const SDL_PenInfo *info, void *handle)
+SDL_PenID SDL_AddPenDevice(Uint64 timestamp, const char *name, SDL_Window *window, const SDL_PenInfo *info, void *handle)
 {
     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!
@@ -262,13 +262,14 @@ SDL_PenID SDL_AddPenDevice(Uint64 timestamp, const char *name, const SDL_PenInfo
         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);
     }
 
     return result;
 }
 
-void SDL_RemovePenDevice(Uint64 timestamp, SDL_PenID instance_id)
+void SDL_RemovePenDevice(Uint64 timestamp, SDL_Window *window, SDL_PenID instance_id)
 {
     if (!instance_id) {
         return;
@@ -306,6 +307,7 @@ void SDL_RemovePenDevice(Uint64 timestamp, SDL_PenID instance_id)
         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);
     }
 }
diff --git a/src/events/SDL_pen_c.h b/src/events/SDL_pen_c.h
index 83f412c1a8e6b..69539bde2ad19 100644
--- a/src/events/SDL_pen_c.h
+++ b/src/events/SDL_pen_c.h
@@ -61,10 +61,10 @@ 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, 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);
 
 // 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_PenID instance_id);
+extern void SDL_RemovePenDevice(Uint64 timestamp, SDL_Window *window, SDL_PenID instance_id);
 
 // Backend can call this to remove all pens, probably during shutdown, with a callback to let them free their own handle.
 extern void SDL_RemoveAllPenDevices(void (*callback)(SDL_PenID instance_id, void *handle, void *userdata), void *userdata);
diff --git a/src/video/android/SDL_androidpen.c b/src/video/android/SDL_androidpen.c
index 727d8bd8626f3..7bfde272e7279 100644
--- a/src/video/android/SDL_androidpen.c
+++ b/src/video/android/SDL_androidpen.c
@@ -51,7 +51,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, &peninfo, (void *) (size_t) pen_id_in);
+        pen = SDL_AddPenDevice(0, NULL, window, &peninfo, (void *) (size_t) pen_id_in);
         if (!pen) {
             SDL_Log("error: can't add a pen device %d", pen_id_in);
             return;
@@ -78,7 +78,7 @@ void Android_OnPen(SDL_Window *window, int pen_id_in, SDL_PenDeviceType device_t
     switch (action) {
     case ACTION_CANCEL:
     case ACTION_HOVER_EXIT:
-        SDL_RemovePenDevice(0, pen);
+        SDL_RemovePenDevice(0, window, pen);
         break;
 
     case ACTION_DOWN:
diff --git a/src/video/cocoa/SDL_cocoapen.m b/src/video/cocoa/SDL_cocoapen.m
index e8a15dac78eab..b698bc571c55e 100644
--- a/src/video/cocoa/SDL_cocoapen.m
+++ b/src/video/cocoa/SDL_cocoapen.m
@@ -105,14 +105,14 @@ 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, &peninfo, handle);
+        handle->pen = SDL_AddPenDevice(Cocoa_GetEventTimestamp([event timestamp]), NULL, _data.window, &peninfo, handle);
         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]), handle->pen);
+            SDL_RemovePenDevice(Cocoa_GetEventTimestamp([event timestamp]), _data.window, handle->pen);
             SDL_free(handle);
         }
     }
diff --git a/src/video/emscripten/SDL_emscriptenevents.c b/src/video/emscripten/SDL_emscriptenevents.c
index 87f0cf1b31273..07f9b2e43ec51 100644
--- a/src/video/emscripten/SDL_emscriptenevents.c
+++ b/src/video/emscripten/SDL_emscriptenevents.c
@@ -855,7 +855,7 @@ static void Emscripten_HandlePenEnter(SDL_WindowData *window_data, const Emscrip
     peninfo.max_tilt = 90.0f;
     peninfo.num_buttons = 2;
     peninfo.subtype = SDL_PEN_TYPE_PEN;
-    SDL_AddPenDevice(0, NULL, &peninfo, (void *) (size_t) event->pointerid);
+    SDL_AddPenDevice(0, NULL, window_data->window, &peninfo, (void *) (size_t) event->pointerid);
     Emscripten_UpdatePenFromEvent(window_data, event);
 }
 
@@ -878,7 +878,7 @@ static void Emscripten_HandlePenLeave(SDL_WindowData *window_data, const Emscrip
     const SDL_PenID pen = SDL_FindPenByHandle((void *) (size_t) event->pointerid);
     if (pen) {
         Emscripten_UpdatePointerFromEvent(window_data, event);  // last data updates?
-        SDL_RemovePenDevice(0, pen);
+        SDL_RemovePenDevice(0, window_data->window, pen);
     }
 }
 
diff --git a/src/video/uikit/SDL_uikitpen.m b/src/video/uikit/SDL_uikitpen.m
index 366580dcb7be6..9c58e17104fb4 100644
--- a/src/video/uikit/SDL_uikitpen.m
+++ b/src/video/uikit/SDL_uikitpen.m
@@ -63,7 +63,7 @@ bool UIKit_InitPen(SDL_VideoDevice *_this)
 // we only have one Apple Pencil at a time, and it must be paired to the iOS device.
 // We only know about its existence when it first sends an event, so add an single SDL pen
 // device here if we haven't already.
-static SDL_PenID UIKit_AddPenIfNecesary()
+static SDL_PenID UIKit_AddPenIfNecesary(SDL_Window *window)
 {
     if (!apple_pencil_id) {
         SDL_PenInfo info;
@@ -86,7 +86,7 @@ static SDL_PenID UIKit_AddPenIfNecesary()
         // 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", &info, (void *) (size_t) 0x1);
+        apple_pencil_id = SDL_AddPenDevice(0, "Apple Pencil", window, &info, (void *) (size_t) 0x1);
     }
 
     return apple_pencil_id;
@@ -95,7 +95,7 @@ static SDL_PenID UIKit_AddPenIfNecesary()
 static void UIKit_HandlePenAxes(SDL_Window *window, NSTimeInterval nstimestamp, float zOffset, const CGPoint *point, float force,
                                 float maximumPossibleForce, float azimuthAngleInView, float altitudeAngle, float rollAngle)
 {
-    const SDL_PenID penId = UIKit_AddPenIfNecesary();
+    const SDL_PenID penId = UIKit_AddPenIfNecesary(window);
     if (penId) {
         const Uint64 timestamp = UIKit_GetEventTimestamp(nstimestamp);
         const float radians_to_degrees = 180.0f / SDL_PI_F;
@@ -188,18 +188,20 @@ void UIKit_HandlePenMotion(SDL_uikitview *view, UITouch *pencil)
 
 void UIKit_HandlePenPress(SDL_uikitview *view, UITouch *pencil)
 {
-    const SDL_PenID penId = UIKit_AddPenIfNecesary();
+    SDL_Window *window = [view getSDLWindow];
+    const SDL_PenID penId = UIKit_AddPenIfNecesary(window);
     if (penId) {
         UIKit_HandlePenAxesFromUITouch(view, pencil);
-        SDL_SendPenTouch(UIKit_GetEventTimestamp([pencil timestamp]), penId, [view getSDLWindow], false, true);
+        SDL_SendPenTouch(UIKit_GetEventTimestamp([pencil timestamp]), penId, window, false, true);
     }
 }
 
 void UIKit_HandlePenRelease(SDL_uikitview *view, UITouch *pencil)
 {
-    const SDL_PenID penId = UIKit_AddPenIfNecesary();
+    SDL_Window *window = [view getSDLWindow];
+    const SDL_PenID penId = UIKit_AddPenIfNecesary(window);
     if (penId) {
-        SDL_SendPenTouch(UIKit_GetEventTimestamp([pencil timestamp]), penId, [view getSDLWindow], false, false);
+        SDL_SendPenTouch(UIKit_GetEventTimestamp([pencil timestamp]), penId, window, false, false);
         UIKit_HandlePenAxesFromUITouch(view, pencil);
     }
 }
@@ -207,7 +209,7 @@ void UIKit_HandlePenRelease(SDL_uikitview *view, UITouch *pencil)
 void UIKit_QuitPen(SDL_VideoDevice *_this)
 {
     if (apple_pencil_id) {
-        SDL_RemovePenDevice(0, apple_pencil_id);
+        SDL_RemovePenDevice(0, NULL, apple_pencil_id);
         apple_pencil_id = 0;
     }
 }
diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c
index ee1378dec22e4..59154dab51065 100644
--- a/src/video/wayland/SDL_waylandevents.c
+++ b/src/video/wayland/SDL_waylandevents.c
@@ -3306,7 +3306,8 @@ static void tablet_tool_handle_removed(void *data, struct zwp_tablet_tool_v2 *to
 {
     SDL_WaylandPenTool *sdltool = (SDL_WaylandPenTool *) data;
     if (sdltool->instance_id) {
-        SDL_RemovePenDevice(0, sdltool->instance_id);
+        SDL_Window *window = sdltool->focus ? sdltool->focus->sdlwindow : NULL;
+        SDL_RemovePenDevice(0, window, sdltool->instance_id);
     }
 
     Wayland_CursorStateRelease(&sdltool->cursor_state);
@@ -3439,7 +3440,7 @@ static void tablet_tool_handle_frame(void *data, struct zwp_tablet_tool_v2 *tool
     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, &sdltool->info, sdltool);
+            sdltool->instance_id = SDL_AddPenDevice(timestamp, NULL, window, &sdltool->info, sdltool);
             Wayland_TabletToolUpdateCursor(sdltool);
         }
     }
@@ -3487,7 +3488,7 @@ static void tablet_tool_handle_frame(void *data, struct zwp_tablet_tool_v2 *tool
     if (sdltool->frame.have_proximity_out) {
         sdltool->focus = NULL;
         Wayland_TabletToolUpdateCursor(sdltool);
-        SDL_RemovePenDevice(timestamp, sdltool->instance_id);
+        SDL_RemovePenDevice(timestamp, window, sdltool->instance_id);
         sdltool->instance_id = 0;
     }
 
@@ -3656,7 +3657,7 @@ void Wayland_DisplayRemoveWindowReferencesFromSeats(SDL_VideoData *display, SDL_
                 tool->focus = NULL;
                 Wayland_TabletToolUpdateCursor(tool);
                 if (tool->instance_id) {
-                    SDL_RemovePenDevice(0, tool->instance_id);
+                    SDL_RemovePenDevice(0, window->sdlwindow, tool->instance_id);
                     tool->instance_id = 0;
                 }
             }
diff --git a/src/video/windows/SDL_windowsevents.c b/src/video/windows/SDL_windowsevents.c
index b9f6e9dba0813..3f7e5c60085ca 100644
--- a/src/video/windows/SDL_windowsevents.c
+++ b/src/video/windows/SDL_windowsevents.c
@@ -1277,7 +1277,7 @@ LRESULT CALLBACK WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lPara
         info.max_tilt = 90.0f;
         info.num_buttons = 1;
         info.subtype = SDL_PEN_TYPE_PENCIL;
-        SDL_AddPenDevice(0, NULL, &info, hpointer);
+        SDL_AddPenDevice(0, NULL, data->window, &info, hpointer);
         returnCode = 0;
     } break;
 
@@ -1293,7 +1293,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)) {
-            SDL_RemovePenDevice(WIN_GetEventTimestamp(), pen);
+            SDL_RemovePenDevice(WIN_GetEventTimestamp(), data->window, pen);
         }
         returnCode = 0;
     } break;
diff --git a/src/video/x11/SDL_x11pen.c b/src/video/x11/SDL_x11pen.c
index c29c629c4a6b0..9374b23c844a8 100644
--- a/src/video/x11/SDL_x11pen.c
+++ b/src/video/x11/SDL_x11pen.c
@@ -272,7 +272,7 @@ 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, &peninfo, handle);
+    handle->pen = SDL_AddPenDevice(0, dev->name, NULL, &peninfo, handle);
     if (!handle->pen) {
         SDL_free(handle);
         return NULL;
@@ -301,7 +301,7 @@ void X11_RemovePenByDeviceID(int deviceid)
 {
     X11_PenHandle *handle = X11_FindPenByDeviceID(deviceid);
     if (handle) {
-        SDL_RemovePenDevice(0, handle->pen);
+        SDL_RemovePenDevice(0, NULL, handle->pen);
         SDL_free(handle);
     }
 }