Răsfoiți Sursa

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.
Ryan C. Gordon 3 săptămâni în urmă
părinte
comite
99d7dad7e6

+ 8 - 7
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.

+ 43 - 19
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);
+    }
+}
+

+ 4 - 1
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);
 

+ 8 - 3
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:

+ 6 - 3
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);
         }
     }
 }

+ 19 - 9
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);
     }
 }
 

+ 1 - 1
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;

+ 18 - 17
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.

+ 3 - 2
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;

+ 22 - 18
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;

+ 27 - 1
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()) {

+ 3 - 0
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_

+ 12 - 0
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;

+ 34 - 24
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... */