SDL: pen: Send virtual mouse and touch events for pen input.

From dabc93a631903570cc80a56e3a9002ac13535d57 Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Tue, 14 Jan 2025 23:30:10 -0500
Subject: [PATCH] pen: Send virtual mouse and touch events for pen input.

Fixes #11948.
---
 include/SDL3/SDL_hints.h              | 30 +++++++++++
 include/SDL3/SDL_pen.h                | 16 ++++++
 src/events/SDL_mouse.c                | 30 +++++++++++
 src/events/SDL_mouse_c.h              |  2 +
 src/events/SDL_pen.c                  | 77 +++++++++++++++++++++++++--
 src/events/SDL_pen_c.h                |  8 +--
 src/events/SDL_touch.c                | 35 +++++-------
 src/video/wayland/SDL_waylandevents.c |  2 +-
 8 files changed, 168 insertions(+), 32 deletions(-)

diff --git a/include/SDL3/SDL_hints.h b/include/SDL3/SDL_hints.h
index 0a4bc48f7dae0..a33b8174c3ef2 100644
--- a/include/SDL3/SDL_hints.h
+++ b/include/SDL3/SDL_hints.h
@@ -4185,6 +4185,36 @@ extern "C" {
  */
 #define SDL_HINT_ASSERT "SDL_ASSERT"
 
+/**
+ * A variable controlling whether pen events should generate synthetic mouse
+ * events.
+ *
+ * The variable can be set to the following values:
+ *
+ * - "0": Pen events will not generate mouse events.
+ * - "1": Pen events will generate mouse events. (default)
+ *
+ * This hint can be set anytime.
+ *
+ * \since This hint is available since SDL 3.2.0.
+ */
+#define SDL_HINT_PEN_MOUSE_EVENTS "SDL_PEN_MOUSE_EVENTS"
+
+/**
+ * A variable controlling whether pen events should generate synthetic touch
+ * events.
+ *
+ * The variable can be set to the following values:
+ *
+ * - "0": Pen events will not generate touch events.
+ * - "1": Pen events will generate touch events. (default)
+ *
+ * This hint can be set anytime.
+ *
+ * \since This hint is available since SDL 3.2.0.
+ */
+#define SDL_HINT_PEN_TOUCH_EVENTS "SDL_PEN_TOUCH_EVENTS"
+
 
 /**
  * An enumeration of hint priorities.
diff --git a/include/SDL3/SDL_pen.h b/include/SDL3/SDL_pen.h
index 838be6d4f0447..61305905a13b2 100644
--- a/include/SDL3/SDL_pen.h
+++ b/include/SDL3/SDL_pen.h
@@ -40,6 +40,8 @@
 #define SDL_pen_h_
 
 #include <SDL3/SDL_stdinc.h>
+#include <SDL3/SDL_mouse.h>
+#include <SDL3/SDL_touch.h>
 
 /* Set up for C function definitions, even when using C++ */
 #ifdef __cplusplus
@@ -59,6 +61,20 @@ extern "C" {
  */
 typedef Uint32 SDL_PenID;
 
+/**
+ * The SDL_MouseID for mouse events simulated with pen input.
+ *
+ * \since This macro is available since SDL 3.1.3.
+ */
+#define SDL_PEN_MOUSEID ((SDL_MouseID)-2)
+
+/**
+ * The SDL_TouchID for touch events simulated with pen input.
+ *
+ * \since This macro is available since SDL 3.1.3.
+ */
+#define SDL_PEN_TOUCHID ((SDL_TouchID)-2)
+
 
 /**
  * Pen input flags, as reported by various pen events' `pen_state` field.
diff --git a/src/events/SDL_mouse.c b/src/events/SDL_mouse.c
index becfe28e5d1f1..932cf13610c37 100644
--- a/src/events/SDL_mouse.c
+++ b/src/events/SDL_mouse.c
@@ -173,6 +173,24 @@ static void SDLCALL SDL_MouseTouchEventsChanged(void *userdata, const char *name
     }
 }
 
+static void SDLCALL SDL_PenMouseEventsChanged(void *userdata, const char *name, const char *oldValue, const char *hint)
+{
+    SDL_Mouse *mouse = (SDL_Mouse *)userdata;
+
+    mouse->pen_mouse_events = SDL_GetStringBoolean(hint, true);
+}
+
+static void SDLCALL SDL_PenTouchEventsChanged(void *userdata, const char *name, const char *oldValue, const char *hint)
+{
+    SDL_Mouse *mouse = (SDL_Mouse *)userdata;
+
+    mouse->pen_touch_events = SDL_GetStringBoolean(hint, true);
+
+    if (mouse->pen_touch_events) {
+        SDL_AddTouch(SDL_PEN_TOUCHID, SDL_TOUCH_DEVICE_DIRECT, "pen_input");
+    }
+}
+
 static void SDLCALL SDL_MouseAutoCaptureChanged(void *userdata, const char *name, const char *oldValue, const char *hint)
 {
     SDL_Mouse *mouse = (SDL_Mouse *)userdata;
@@ -239,6 +257,12 @@ bool SDL_PreInitMouse(void)
     SDL_AddHintCallback(SDL_HINT_MOUSE_TOUCH_EVENTS,
                         SDL_MouseTouchEventsChanged, mouse);
 
+    SDL_AddHintCallback(SDL_HINT_PEN_MOUSE_EVENTS,
+                        SDL_PenMouseEventsChanged, mouse);
+
+    SDL_AddHintCallback(SDL_HINT_PEN_TOUCH_EVENTS,
+                        SDL_PenTouchEventsChanged, mouse);
+
     SDL_AddHintCallback(SDL_HINT_MOUSE_AUTO_CAPTURE,
                         SDL_MouseAutoCaptureChanged, mouse);
 
@@ -1043,6 +1067,12 @@ void SDL_QuitMouse(void)
     SDL_RemoveHintCallback(SDL_HINT_MOUSE_TOUCH_EVENTS,
                         SDL_MouseTouchEventsChanged, mouse);
 
+    SDL_RemoveHintCallback(SDL_HINT_PEN_MOUSE_EVENTS,
+                        SDL_PenMouseEventsChanged, mouse);
+
+    SDL_RemoveHintCallback(SDL_HINT_PEN_TOUCH_EVENTS,
+                        SDL_PenTouchEventsChanged, mouse);
+
     SDL_RemoveHintCallback(SDL_HINT_MOUSE_AUTO_CAPTURE,
                         SDL_MouseAutoCaptureChanged, mouse);
 
diff --git a/src/events/SDL_mouse_c.h b/src/events/SDL_mouse_c.h
index 81dad3ace8b7f..01974d9a87590 100644
--- a/src/events/SDL_mouse_c.h
+++ b/src/events/SDL_mouse_c.h
@@ -118,6 +118,8 @@ typedef struct
     int double_click_radius;
     bool touch_mouse_events;
     bool mouse_touch_events;
+    bool pen_mouse_events;
+    bool pen_touch_events;
     bool was_touch_mouse_events; // Was a touch-mouse event pending?
 #ifdef SDL_PLATFORM_VITA
     Uint8 vita_touch_mouse_device;
diff --git a/src/events/SDL_pen.c b/src/events/SDL_pen.c
index 028d59b0d9241..6db1b0ee14413 100644
--- a/src/events/SDL_pen.c
+++ b/src/events/SDL_pen.c
@@ -26,6 +26,8 @@
 #include "SDL_events_c.h"
 #include "SDL_pen_c.h"
 
+static SDL_PenID pen_touching = 0;  // used for synthetic mouse/touch events.
+
 typedef struct SDL_Pen
 {
     SDL_PenID instance_id;
@@ -111,6 +113,7 @@ void SDL_QuitPen(void)
     SDL_free(pen_devices);
     pen_devices = NULL;
     pen_device_count = 0;
+    pen_touching = 0;
 }
 
 #if 0 // not a public API at the moment.
@@ -309,7 +312,7 @@ void SDL_RemoveAllPenDevices(void (*callback)(SDL_PenID instance_id, void *handl
     SDL_UnlockRWLock(pen_device_rwlock);
 }
 
-void SDL_SendPenTouch(Uint64 timestamp, SDL_PenID instance_id, const SDL_Window *window, bool eraser, bool down)
+void SDL_SendPenTouch(Uint64 timestamp, SDL_PenID instance_id, SDL_Window *window, bool eraser, bool down)
 {
     bool send_event = false;
     SDL_PenInputFlags input_state = 0;
@@ -363,10 +366,45 @@ void SDL_SendPenTouch(Uint64 timestamp, SDL_PenID instance_id, const SDL_Window
             event.ptouch.down = down;
             SDL_PushEvent(&event);
         }
+
+        SDL_Mouse *mouse = SDL_GetMouse();
+        if (mouse && window) {
+            if (mouse->pen_mouse_events) {
+                if (down) {
+                    if (!pen_touching) {
+                        SDL_SendMouseMotion(timestamp, window, SDL_PEN_MOUSEID, false, x, y);
+                        SDL_SendMouseButton(timestamp, window, SDL_PEN_MOUSEID, SDL_BUTTON_LEFT, true);
+                    }
+                } else {
+                    if (pen_touching == instance_id) {
+                        SDL_SendMouseButton(timestamp, window, SDL_PEN_MOUSEID, SDL_BUTTON_LEFT, false);
+                    }
+                }
+            }
+
+            if (mouse->pen_touch_events) {
+                const SDL_EventType touchtype = down ? SDL_EVENT_FINGER_DOWN : SDL_EVENT_FINGER_UP;
+                const float normalized_x = x / (float)window->w;
+                const float normalized_y = y / (float)window->h;
+                if (!pen_touching || (pen_touching == instance_id)) {
+                    SDL_SendTouch(timestamp, SDL_PEN_TOUCHID, SDL_BUTTON_LEFT, window, touchtype, normalized_x, normalized_y, pen->axes[SDL_PEN_AXIS_PRESSURE]);
+                }
+            }
+        }
+
+        if (down) {
+            if (!pen_touching) {
+                pen_touching = instance_id;
+            }
+        } else {
+            if (pen_touching == instance_id) {
+                pen_touching = 0;
+            }
+        }
     }
 }
 
-void SDL_SendPenAxis(Uint64 timestamp, SDL_PenID instance_id, const SDL_Window *window, SDL_PenAxis axis, float value)
+void SDL_SendPenAxis(Uint64 timestamp, SDL_PenID instance_id, SDL_Window *window, SDL_PenAxis axis, float value)
 {
     SDL_assert((axis >= 0) && (axis < SDL_PEN_AXIS_COUNT));  // fix the backend if this triggers.
 
@@ -405,10 +443,19 @@ void SDL_SendPenAxis(Uint64 timestamp, SDL_PenID instance_id, const SDL_Window *
         event.paxis.axis = axis;
         event.paxis.value = value;
         SDL_PushEvent(&event);
+
+        if (window && (axis == SDL_PEN_AXIS_PRESSURE) && (pen_touching == instance_id)) {
+            SDL_Mouse *mouse = SDL_GetMouse();
+            if (mouse && mouse->pen_touch_events) {
+                const float normalized_x = x / (float)window->w;
+                const float normalized_y = y / (float)window->h;
+                SDL_SendTouchMotion(timestamp, SDL_PEN_TOUCHID, SDL_BUTTON_LEFT, window, normalized_x, normalized_y, value);
+            }
+        }
     }
 }
 
-void SDL_SendPenMotion(Uint64 timestamp, SDL_PenID instance_id, const SDL_Window *window, float x, float y)
+void SDL_SendPenMotion(Uint64 timestamp, SDL_PenID instance_id, SDL_Window *window, float x, float y)
 {
     bool send_event = false;
     SDL_PenInputFlags input_state = 0;
@@ -440,10 +487,25 @@ void SDL_SendPenMotion(Uint64 timestamp, SDL_PenID instance_id, const SDL_Window
         event.pmotion.x = x;
         event.pmotion.y = y;
         SDL_PushEvent(&event);
+
+        if (window && (pen_touching == instance_id)) {
+            SDL_Mouse *mouse = SDL_GetMouse();
+            if (mouse) {
+                if (mouse->pen_mouse_events) {
+                    SDL_SendMouseMotion(timestamp, window, SDL_PEN_MOUSEID, false, x, y);
+                }
+
+                if (mouse->pen_touch_events) {
+                    const float normalized_x = x / (float)window->w;
+                    const float normalized_y = y / (float)window->h;
+                    SDL_SendTouchMotion(timestamp, SDL_PEN_TOUCHID, SDL_BUTTON_LEFT, window, normalized_x, normalized_y, pen->axes[SDL_PEN_AXIS_PRESSURE]);
+                }
+            }
+        }
     }
 }
 
-void SDL_SendPenButton(Uint64 timestamp, SDL_PenID instance_id, const SDL_Window *window, Uint8 button, bool down)
+void SDL_SendPenButton(Uint64 timestamp, SDL_PenID instance_id, SDL_Window *window, Uint8 button, bool down)
 {
     bool send_event = false;
     SDL_PenInputFlags input_state = 0;
@@ -492,6 +554,13 @@ void SDL_SendPenButton(Uint64 timestamp, SDL_PenID instance_id, const SDL_Window
             event.pbutton.button = button;
             event.pbutton.down = down;
             SDL_PushEvent(&event);
+
+            if (window && (pen_touching == instance_id)) {
+                SDL_Mouse *mouse = SDL_GetMouse();
+                if (mouse && mouse->pen_mouse_events) {
+                    SDL_SendMouseButton(timestamp, window, SDL_PEN_MOUSEID, button + 1, down);
+                }
+            }
         }
     }
 }
diff --git a/src/events/SDL_pen_c.h b/src/events/SDL_pen_c.h
index 7175483a53861..1eff47f230716 100644
--- a/src/events/SDL_pen_c.h
+++ b/src/events/SDL_pen_c.h
@@ -67,16 +67,16 @@ extern void SDL_RemovePenDevice(Uint64 timestamp, SDL_PenID instance_id);
 extern void SDL_RemoveAllPenDevices(void (*callback)(SDL_PenID instance_id, void *handle, void *userdata), void *userdata);
 
 // Backend calls this when a pen's button changes, to generate events and update state.
-extern void SDL_SendPenTouch(Uint64 timestamp, SDL_PenID instance_id, const SDL_Window *window, bool eraser, bool down);
+extern void SDL_SendPenTouch(Uint64 timestamp, SDL_PenID instance_id, SDL_Window *window, bool eraser, bool down);
 
 // Backend calls this when a pen moves on the tablet, to generate events and update state.
-extern void SDL_SendPenMotion(Uint64 timestamp, SDL_PenID instance_id, const SDL_Window *window, float x, float y);
+extern void SDL_SendPenMotion(Uint64 timestamp, SDL_PenID instance_id, SDL_Window *window, float x, float y);
 
 // Backend calls this when a pen's axis changes, to generate events and update state.
-extern void SDL_SendPenAxis(Uint64 timestamp, SDL_PenID instance_id, const SDL_Window *window, SDL_PenAxis axis, float value);
+extern void SDL_SendPenAxis(Uint64 timestamp, SDL_PenID instance_id, SDL_Window *window, SDL_PenAxis axis, float value);
 
 // 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, const SDL_Window *window, Uint8 button, bool down);
+extern void SDL_SendPenButton(Uint64 timestamp, SDL_PenID instance_id, SDL_Window *window, Uint8 button, bool down);
 
 // 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/events/SDL_touch.c b/src/events/SDL_touch.c
index de9b6915a78bb..3a4f54bd070c1 100644
--- a/src/events/SDL_touch.c
+++ b/src/events/SDL_touch.c
@@ -29,14 +29,9 @@ static int SDL_num_touch = 0;
 static SDL_Touch **SDL_touchDevices = NULL;
 
 // for mapping touch events to mice
-
-#define SYNTHESIZE_TOUCH_TO_MOUSE 1
-
-#if SYNTHESIZE_TOUCH_TO_MOUSE
 static bool finger_touching = false;
 static SDL_FingerID track_fingerid;
 static SDL_TouchID track_touchid;
-#endif
 
 // Public functions
 bool SDL_InitTouch(void)
@@ -257,7 +252,6 @@ static void SDL_DelFinger(SDL_Touch *touch, SDL_FingerID fingerid)
 void SDL_SendTouch(Uint64 timestamp, SDL_TouchID id, SDL_FingerID fingerid, SDL_Window *window, SDL_EventType type, float x, float y, float pressure)
 {
     SDL_Finger *finger;
-    SDL_Mouse *mouse;
     bool down = (type == SDL_EVENT_FINGER_DOWN);
 
     SDL_Touch *touch = SDL_GetTouch(id);
@@ -265,19 +259,18 @@ void SDL_SendTouch(Uint64 timestamp, SDL_TouchID id, SDL_FingerID fingerid, SDL_
         return;
     }
 
-    mouse = SDL_GetMouse();
+    SDL_Mouse *mouse = SDL_GetMouse();
 
-#if SYNTHESIZE_TOUCH_TO_MOUSE
     // SDL_HINT_TOUCH_MOUSE_EVENTS: controlling whether touch events should generate synthetic mouse events
     // SDL_HINT_VITA_TOUCH_MOUSE_DEVICE: controlling which touchpad should generate synthetic mouse events, PSVita-only
     {
+        // FIXME: maybe we should only restrict to a few SDL_TouchDeviceType
+        if ((id != SDL_MOUSE_TOUCHID) && (id != SDL_PEN_TOUCHID)) {
 #ifdef SDL_PLATFORM_VITA
-        if (mouse->touch_mouse_events && ((mouse->vita_touch_mouse_device == id) || (mouse->vita_touch_mouse_device == 2))) {
+            if (mouse->touch_mouse_events && ((mouse->vita_touch_mouse_device == id) || (mouse->vita_touch_mouse_device == 2))) {
 #else
-        if (mouse->touch_mouse_events) {
+            if (mouse->touch_mouse_events) {
 #endif
-            // FIXME: maybe we should only restrict to a few SDL_TouchDeviceType
-            if (id != SDL_MOUSE_TOUCHID) {
                 if (window) {
                     if (down) {
                         if (finger_touching == false) {
@@ -318,13 +311,12 @@ void SDL_SendTouch(Uint64 timestamp, SDL_TouchID id, SDL_FingerID fingerid, SDL_
             }
         }
     }
-#endif
 
     // SDL_HINT_MOUSE_TOUCH_EVENTS: if not set, discard synthetic touch events coming from platform layer
-    if (mouse->mouse_touch_events == 0) {
-        if (id == SDL_MOUSE_TOUCHID) {
-            return;
-        }
+    if (!mouse->mouse_touch_events && (id == SDL_MOUSE_TOUCHID)) {
+        return;
+    } else if (!mouse->pen_touch_events && (id == SDL_PEN_TOUCHID)) {
+        return;
     }
 
     finger = SDL_GetFinger(touch, fingerid);
@@ -384,7 +376,6 @@ void SDL_SendTouchMotion(Uint64 timestamp, SDL_TouchID id, SDL_FingerID fingerid
 {
     SDL_Touch *touch;
     SDL_Finger *finger;
-    SDL_Mouse *mouse;
     float xrel, yrel, prel;
 
     touch = SDL_GetTouch(id);
@@ -392,13 +383,12 @@ void SDL_SendTouchMotion(Uint64 timestamp, SDL_TouchID id, SDL_FingerID fingerid
         return;
     }
 
-    mouse = SDL_GetMouse();
+    SDL_Mouse *mouse = SDL_GetMouse();
 
-#if SYNTHESIZE_TOUCH_TO_MOUSE
     // SDL_HINT_TOUCH_MOUSE_EVENTS: controlling whether touch events should generate synthetic mouse events
     {
-        if (mouse->touch_mouse_events) {
-            if (id != SDL_MOUSE_TOUCHID) {
+        if ((id != SDL_MOUSE_TOUCHID) && (id != SDL_PEN_TOUCHID)) {
+            if (mouse->touch_mouse_events) {
                 if (window) {
                     if (finger_touching == true && track_touchid == id && track_fingerid == fingerid) {
                         float pos_x = (x * (float)window->w);
@@ -421,7 +411,6 @@ void SDL_SendTouchMotion(Uint64 timestamp, SDL_TouchID id, SDL_FingerID fingerid
             }
         }
     }
-#endif
 
     // SDL_HINT_MOUSE_TOUCH_EVENTS: if not set, discard synthetic touch events coming from platform layer
     if (mouse->mouse_touch_events == 0) {
diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c
index 36e2933970763..03982fbad4109 100644
--- a/src/video/wayland/SDL_waylandevents.c
+++ b/src/video/wayland/SDL_waylandevents.c
@@ -2957,7 +2957,7 @@ static void tablet_tool_handle_frame(void *data, struct zwp_tablet_tool_v2 *tool
 
     const Uint64 timestamp = Wayland_GetEventTimestamp(SDL_MS_TO_NS(time));
     const SDL_PenID instance_id = sdltool->instance_id;
-    const SDL_Window *window = sdltool->tool_focus;
+    SDL_Window *window = sdltool->tool_focus;
 
     // I don't know if this is necessary (or makes sense), but send motion before pen downs, but after pen ups, so you don't get unexpected lines drawn.
     if (sdltool->frame_motion_set && (sdltool->frame_pen_down != -1)) {