diff --git a/include/SDL3/SDL_pen.h b/include/SDL3/SDL_pen.h index d09b00b6208d8..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. */ @@ -92,13 +113,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..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; @@ -848,14 +848,24 @@ 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); + + // 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) 1); // something > 0 for the single pen handle. + 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) 1, true); + } + Emscripten_UpdatePenFromEvent(window_data, event); } @@ -875,10 +885,10 @@ EMSCRIPTEN_KEEPALIVE void Emscripten_HandlePointerEnter(SDL_WindowData *window_d static void Emscripten_HandlePenLeave(SDL_WindowData *window_data, const Emscripten_PointerEvent *event) { - 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_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 26b6642f7c677..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) @@ -3408,7 +3415,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) @@ -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..2ad327441b808 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)); } @@ -1264,41 +1264,53 @@ 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. - } 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); + } + + 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); + } 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; 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. @@ -1306,31 +1318,31 @@ 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; + 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)) { 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... */