Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 34 additions & 12 deletions include/SDL3/SDL_pen.h
Original file line number Diff line number Diff line change
Expand Up @@ -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_
Expand All @@ -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.
*/
Expand All @@ -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.
Expand Down
62 changes: 43 additions & 19 deletions src/events/SDL_pen.c
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down Expand Up @@ -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;
Expand All @@ -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) {
Expand All @@ -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.
Expand Down Expand Up @@ -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);
}
}

5 changes: 4 additions & 1 deletion src/events/SDL_pen_c.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);

Expand Down
11 changes: 8 additions & 3 deletions src/video/android/SDL_androidpen.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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;
Expand All @@ -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:
Expand Down
9 changes: 6 additions & 3 deletions src/video/cocoa/SDL_cocoapen.m
Original file line number Diff line number Diff line change
Expand Up @@ -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.
}

Expand All @@ -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);
}
}
}
Expand Down
32 changes: 21 additions & 11 deletions src/video/emscripten/SDL_emscriptenevents.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

Expand All @@ -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);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/video/uikit/SDL_uikitpen.m
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
37 changes: 19 additions & 18 deletions src/video/wayland/SDL_waylandevents.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -3323,15 +3328,17 @@ 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.
}

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)
Expand Down Expand Up @@ -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)
Expand All @@ -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?
Expand Down Expand Up @@ -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.
Expand Down
Loading
Loading