From 7c80ac6df7001ba8ad37c6cf3ab0dc70b57011dd Mon Sep 17 00:00:00 2001 From: Christoph Reichenbach Date: Sun, 27 Aug 2023 06:20:29 +0000 Subject: [PATCH] API for pressure-sensitive pens + XInput2/Wayland This patch adds an API for querying pressure- sensitive pens, cf. SDL_pen.h: - Enumerate all pens - Get pen capabilities, names, GUIDs - Distinguishes pens and erasers - Distinguish attached and detached pens - Pressure and tilt support - Rotation, distance, throttle wheel support (throttle wheel untested) - Pen type and meta-information reporting (partially tested) Pen event reporting: - Three new event structures: PenTip, PenMotion, and PenButton - Report location with sub-pixel precision - Include axis and button status, is-eraser flag Internal pen tracker, intended to be independent of platform APIs, cf. SDL_pen_c.h: - Track known pens - Handle pen hotplugging Automatic test: - testautomation_pen.c Other features: - XInput2 implementation, incl. hotplugging - Wayland implementation, incl. hotplugging - Backward compatibility: pen events default to emulating pens with mouse ID SDL_PEN_MOUSEID - Can be toggled via SDL_HINT_PEN_NOT_MOUSE - Test/demo program (testpen) - Wacom pen feature identification by pen ID Acknowledgements: - Ping Cheng (Wacom) provided extensive feedback on Wacom pen features and detection so that hopefully untested Wacom devices have a realistic chance of working out of the box. --- VisualC-GDK/SDL/SDL.vcxproj | 2 + VisualC-GDK/SDL/SDL.vcxproj.filters | 6 + VisualC-WinRT/SDL-UWP.vcxproj | 2 + VisualC-WinRT/SDL-UWP.vcxproj.filters | 6 + VisualC/SDL/SDL.vcxproj | 2 + VisualC/SDL/SDL.vcxproj.filters | 6 + .../testautomation/testautomation.vcxproj | 6 + Xcode/SDL/SDL.xcodeproj/project.pbxproj | 33 + include/SDL3/SDL_events.h | 76 +- include/SDL3/SDL_hints.h | 34 + include/SDL3/SDL_pen.h | 285 +++ src/dynapi/SDL_dynapi.sym | 16 + src/dynapi/SDL_dynapi_overrides.h | 16 + src/dynapi/SDL_dynapi_procs.h | 8 + src/dynapi/gendynapi.py | 2 +- src/events/SDL_events.c | 54 +- src/events/SDL_mouse.c | 28 +- src/events/SDL_mouse_c.h | 3 + src/events/SDL_pen.c | 1083 ++++++++++ src/events/SDL_pen_c.h | 336 +++ src/video/wayland/SDL_waylandevents.c | 463 +++- src/video/wayland/SDL_waylandevents_c.h | 38 +- src/video/x11/SDL_x11events.c | 185 +- src/video/x11/SDL_x11events.h | 4 + src/video/x11/SDL_x11pen.c | 694 ++++++ src/video/x11/SDL_x11pen.h | 54 + src/video/x11/SDL_x11sym.h | 1 + src/video/x11/SDL_x11video.c | 13 +- src/video/x11/SDL_x11window.c | 38 +- src/video/x11/SDL_x11xinput2.c | 180 +- src/video/x11/SDL_x11xinput2.h | 3 +- test/CMakeLists.txt | 1 + test/testautomation.c | 1 + test/testautomation_pen.c | 1908 +++++++++++++++++ test/testautomation_suites.h | 1 + test/testpen.c | 496 +++++ 36 files changed, 5843 insertions(+), 241 deletions(-) create mode 100644 include/SDL3/SDL_pen.h create mode 100644 src/events/SDL_pen.c create mode 100644 src/events/SDL_pen_c.h create mode 100644 src/video/x11/SDL_x11pen.c create mode 100644 src/video/x11/SDL_x11pen.h create mode 100644 test/testautomation_pen.c create mode 100644 test/testpen.c diff --git a/VisualC-GDK/SDL/SDL.vcxproj b/VisualC-GDK/SDL/SDL.vcxproj index a2c5aa6db2..3d6451e2fb 100644 --- a/VisualC-GDK/SDL/SDL.vcxproj +++ b/VisualC-GDK/SDL/SDL.vcxproj @@ -330,6 +330,7 @@ + @@ -586,6 +587,7 @@ + diff --git a/VisualC-GDK/SDL/SDL.vcxproj.filters b/VisualC-GDK/SDL/SDL.vcxproj.filters index a5772a7453..2396f04b6e 100644 --- a/VisualC-GDK/SDL/SDL.vcxproj.filters +++ b/VisualC-GDK/SDL/SDL.vcxproj.filters @@ -300,6 +300,9 @@ API Headers + + API Headers + API Headers @@ -940,6 +943,9 @@ events + + events + events diff --git a/VisualC-WinRT/SDL-UWP.vcxproj b/VisualC-WinRT/SDL-UWP.vcxproj index f7eca1ef79..2fad76fd31 100644 --- a/VisualC-WinRT/SDL-UWP.vcxproj +++ b/VisualC-WinRT/SDL-UWP.vcxproj @@ -67,6 +67,7 @@ + @@ -306,6 +307,7 @@ + diff --git a/VisualC-WinRT/SDL-UWP.vcxproj.filters b/VisualC-WinRT/SDL-UWP.vcxproj.filters index fc905ac9e8..747841ba1b 100644 --- a/VisualC-WinRT/SDL-UWP.vcxproj.filters +++ b/VisualC-WinRT/SDL-UWP.vcxproj.filters @@ -105,6 +105,9 @@ Header Files + + Header Files + Header Files @@ -552,6 +555,9 @@ Source Files + + Source Files + Source Files diff --git a/VisualC/SDL/SDL.vcxproj b/VisualC/SDL/SDL.vcxproj index fc53003e73..0dee2d5006 100644 --- a/VisualC/SDL/SDL.vcxproj +++ b/VisualC/SDL/SDL.vcxproj @@ -280,6 +280,7 @@ + @@ -507,6 +508,7 @@ + diff --git a/VisualC/SDL/SDL.vcxproj.filters b/VisualC/SDL/SDL.vcxproj.filters index c9d6f36e5d..fbaadb802a 100644 --- a/VisualC/SDL/SDL.vcxproj.filters +++ b/VisualC/SDL/SDL.vcxproj.filters @@ -294,6 +294,9 @@ API Headers + + API Headers + API Headers @@ -921,6 +924,9 @@ events + + events + events diff --git a/VisualC/tests/testautomation/testautomation.vcxproj b/VisualC/tests/testautomation/testautomation.vcxproj index 260617b4a8..dbe1f3d8c2 100644 --- a/VisualC/tests/testautomation/testautomation.vcxproj +++ b/VisualC/tests/testautomation/testautomation.vcxproj @@ -209,6 +209,12 @@ + + $(ProjectDir)\..\..\..\src;%(AdditionalIncludeDirectories) + $(ProjectDir)\..\..\..\src;%(AdditionalIncludeDirectories) + $(ProjectDir)\..\..\..\src;%(AdditionalIncludeDirectories) + $(ProjectDir)\..\..\..\src;%(AdditionalIncludeDirectories) + diff --git a/Xcode/SDL/SDL.xcodeproj/project.pbxproj b/Xcode/SDL/SDL.xcodeproj/project.pbxproj index 32f2baf0f1..66ead99a18 100644 --- a/Xcode/SDL/SDL.xcodeproj/project.pbxproj +++ b/Xcode/SDL/SDL.xcodeproj/project.pbxproj @@ -43,6 +43,27 @@ 00D0D0D810675E46004B05EF /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 007317C10858E15000B2BC32 /* Carbon.framework */; platformFilters = (macos, ); }; 557D0CFA254586CA003913E3 /* CoreHaptics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F37DC5F225350EBC0002E6F7 /* CoreHaptics.framework */; platformFilters = (ios, maccatalyst, macos, tvos, ); }; 557D0CFB254586D7003913E3 /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A75FDABD23E28B6200529352 /* GameController.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + 63125C002A790B12008EF011 /* SDL_pen.h in Headers */ = {isa = PBXBuildFile; fileRef = 63125BFF2A790B12008EF011 /* SDL_pen.h */; }; + 63125C012A790B12008EF011 /* SDL_pen.h in Headers */ = {isa = PBXBuildFile; fileRef = 63125BFF2A790B12008EF011 /* SDL_pen.h */; }; + 63125C022A790B12008EF011 /* SDL_pen.h in Headers */ = {isa = PBXBuildFile; fileRef = 63125BFF2A790B12008EF011 /* SDL_pen.h */; }; + 63125C0A2A790B69008EF011 /* SDL_pen.c in Sources */ = {isa = PBXBuildFile; fileRef = 63125C092A790B69008EF011 /* SDL_pen.c */; }; + 63125C0B2A790B69008EF011 /* SDL_pen.c in Sources */ = {isa = PBXBuildFile; fileRef = 63125C092A790B69008EF011 /* SDL_pen.c */; }; + 63125C0C2A790B69008EF011 /* SDL_pen.c in Sources */ = {isa = PBXBuildFile; fileRef = 63125C092A790B69008EF011 /* SDL_pen.c */; }; + 63125C0D2A790B69008EF011 /* SDL_pen.c in Sources */ = {isa = PBXBuildFile; fileRef = 63125C092A790B69008EF011 /* SDL_pen.c */; }; + 63125C0E2A790B69008EF011 /* SDL_pen.c in Sources */ = {isa = PBXBuildFile; fileRef = 63125C092A790B69008EF011 /* SDL_pen.c */; }; + 63125C0F2A790B69008EF011 /* SDL_pen.c in Sources */ = {isa = PBXBuildFile; fileRef = 63125C092A790B69008EF011 /* SDL_pen.c */; }; + 63125C102A790B69008EF011 /* SDL_pen.c in Sources */ = {isa = PBXBuildFile; fileRef = 63125C092A790B69008EF011 /* SDL_pen.c */; }; + 63125C112A790B69008EF011 /* SDL_pen.c in Sources */ = {isa = PBXBuildFile; fileRef = 63125C092A790B69008EF011 /* SDL_pen.c */; }; + 63125C122A790B69008EF011 /* SDL_pen.c in Sources */ = {isa = PBXBuildFile; fileRef = 63125C092A790B69008EF011 /* SDL_pen.c */; }; + 63125C142A790B9A008EF011 /* SDL_pen_c.h in Headers */ = {isa = PBXBuildFile; fileRef = 63125C132A790B9A008EF011 /* SDL_pen_c.h */; }; + 63125C152A790B9A008EF011 /* SDL_pen_c.h in Headers */ = {isa = PBXBuildFile; fileRef = 63125C132A790B9A008EF011 /* SDL_pen_c.h */; }; + 63125C162A790B9A008EF011 /* SDL_pen_c.h in Headers */ = {isa = PBXBuildFile; fileRef = 63125C132A790B9A008EF011 /* SDL_pen_c.h */; }; + 63125C172A790B9A008EF011 /* SDL_pen_c.h in Headers */ = {isa = PBXBuildFile; fileRef = 63125C132A790B9A008EF011 /* SDL_pen_c.h */; }; + 63125C182A790B9A008EF011 /* SDL_pen_c.h in Headers */ = {isa = PBXBuildFile; fileRef = 63125C132A790B9A008EF011 /* SDL_pen_c.h */; }; + 63125C192A790B9A008EF011 /* SDL_pen_c.h in Headers */ = {isa = PBXBuildFile; fileRef = 63125C132A790B9A008EF011 /* SDL_pen_c.h */; }; + 63125C1A2A790B9A008EF011 /* SDL_pen_c.h in Headers */ = {isa = PBXBuildFile; fileRef = 63125C132A790B9A008EF011 /* SDL_pen_c.h */; }; + 63125C1B2A790B9A008EF011 /* SDL_pen_c.h in Headers */ = {isa = PBXBuildFile; fileRef = 63125C132A790B9A008EF011 /* SDL_pen_c.h */; }; + 63125C1C2A790B9A008EF011 /* SDL_pen_c.h in Headers */ = {isa = PBXBuildFile; fileRef = 63125C132A790B9A008EF011 /* SDL_pen_c.h */; }; 5616CA4C252BB2A6005D5928 /* SDL_url.c in Sources */ = {isa = PBXBuildFile; fileRef = 5616CA49252BB2A5005D5928 /* SDL_url.c */; }; 5616CA4D252BB2A6005D5928 /* SDL_sysurl.h in Headers */ = {isa = PBXBuildFile; fileRef = 5616CA4A252BB2A6005D5928 /* SDL_sysurl.h */; }; 5616CA4E252BB2A6005D5928 /* SDL_sysurl.m in Sources */ = {isa = PBXBuildFile; fileRef = 5616CA4B252BB2A6005D5928 /* SDL_sysurl.m */; }; @@ -52,6 +73,9 @@ 566E26D8246274CC00718109 /* SDL_locale.c in Sources */ = {isa = PBXBuildFile; fileRef = 566E26CD246274CB00718109 /* SDL_locale.c */; }; 566E26E1246274CC00718109 /* SDL_syslocale.h in Headers */ = {isa = PBXBuildFile; fileRef = 566E26CE246274CC00718109 /* SDL_syslocale.h */; }; 56A2373329F9C113003CCA5F /* SDL_sysrwlock.c in Sources */ = {isa = PBXBuildFile; fileRef = 56A2373229F9C113003CCA5F /* SDL_sysrwlock.c */; }; + 63134A222A7902CF0021E9A6 /* SDL_pen.h in Headers */ = {isa = PBXBuildFile; fileRef = 63134A212A7902CF0021E9A6 /* SDL_pen.h */; }; + 63134A252A7902FD0021E9A6 /* SDL_pen_c.h in Headers */ = {isa = PBXBuildFile; fileRef = 63134A232A7902FD0021E9A6 /* SDL_pen_c.h */; }; + 63134A262A7902FD0021E9A6 /* SDL_pen.c in Sources */ = {isa = PBXBuildFile; fileRef = 63134A242A7902FD0021E9A6 /* SDL_pen.c */; }; 75E0915A241EA924004729E1 /* SDL_virtualjoystick.c in Sources */ = {isa = PBXBuildFile; fileRef = 75E09158241EA924004729E1 /* SDL_virtualjoystick.c */; }; 75E09163241EA924004729E1 /* SDL_virtualjoystick_c.h in Headers */ = {isa = PBXBuildFile; fileRef = 75E09159241EA924004729E1 /* SDL_virtualjoystick_c.h */; }; 9846B07C287A9020000C35C8 /* SDL_hidapi_shield.c in Sources */ = {isa = PBXBuildFile; fileRef = 9846B07B287A9020000C35C8 /* SDL_hidapi_shield.c */; }; @@ -534,6 +558,9 @@ 566E26CD246274CB00718109 /* SDL_locale.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = SDL_locale.c; path = locale/SDL_locale.c; sourceTree = ""; }; 566E26CE246274CC00718109 /* SDL_syslocale.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SDL_syslocale.h; path = locale/SDL_syslocale.h; sourceTree = ""; }; 56A2373229F9C113003CCA5F /* SDL_sysrwlock.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_sysrwlock.c; sourceTree = ""; }; + 63134A212A7902CF0021E9A6 /* SDL_pen.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SDL_pen.h; path = SDL3/SDL_pen.h; sourceTree = ""; }; + 63134A232A7902FD0021E9A6 /* SDL_pen_c.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_pen_c.h; sourceTree = ""; }; + 63134A242A7902FD0021E9A6 /* SDL_pen.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_pen.c; sourceTree = ""; }; 75E09158241EA924004729E1 /* SDL_virtualjoystick.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_virtualjoystick.c; sourceTree = ""; }; 75E09159241EA924004729E1 /* SDL_virtualjoystick_c.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDL_virtualjoystick_c.h; sourceTree = ""; }; 9846B07B287A9020000C35C8 /* SDL_hidapi_shield.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SDL_hidapi_shield.c; sourceTree = ""; }; @@ -1093,6 +1120,7 @@ F3F7D8C92933074B00816151 /* SDL_opengles2_gl2platform.h */, F3F7D8B12933074900816151 /* SDL_opengles2_khrplatform.h */, F3F7D8C72933074B00816151 /* SDL_opengles2.h */, + 63134A212A7902CF0021E9A6 /* SDL_pen.h */, F3F7D8B52933074A00816151 /* SDL_pixels.h */, F3B38CCB296E2E52005DA6D3 /* SDL_platform_defines.h */, F3F7D8AB2933074900816151 /* SDL_platform.h */, @@ -2059,6 +2087,8 @@ A7D8A93823E2514000DCD162 /* SDL_keyboard.c */, A7D8A92B23E2514000DCD162 /* SDL_mouse_c.h */, A7D8A92A23E2514000DCD162 /* SDL_mouse.c */, + 63134A232A7902FD0021E9A6 /* SDL_pen_c.h */, + 63134A242A7902FD0021E9A6 /* SDL_pen.c */, A7D8A93C23E2514000DCD162 /* SDL_quit.c */, A7D8A93723E2514000DCD162 /* SDL_touch_c.h */, A7D8A93E23E2514000DCD162 /* SDL_touch.c */, @@ -2365,6 +2395,8 @@ A7D8B3D423E2514300DCD162 /* yuv_rgb.h in Headers */, A7D8B3C823E2514200DCD162 /* yuv_rgb_sse_func.h in Headers */, A7D8B3CE23E2514300DCD162 /* yuv_rgb_std_func.h in Headers */, + 63134A222A7902CF0021E9A6 /* SDL_pen.h in Headers */, + 63134A252A7902FD0021E9A6 /* SDL_pen_c.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2685,6 +2717,7 @@ A7D8AEA023E2514100DCD162 /* SDL_cocoavulkan.m in Sources */, A7D8AB6123E2514100DCD162 /* SDL_offscreenwindow.c in Sources */, 566E26D8246274CC00718109 /* SDL_locale.c in Sources */, + 63134A262A7902FD0021E9A6 /* SDL_pen.c in Sources */, 000040E76FDC6AE48CBF0000 /* SDL_hashtable.c in Sources */, 0000A4DA2F45A31DC4F00000 /* SDL_sysmain_callbacks.m in Sources */, 000028F8113A53F4333E0000 /* SDL_main_callbacks.c in Sources */, diff --git a/include/SDL3/SDL_events.h b/include/SDL3/SDL_events.h index dcc9ece04a..186f9f4078 100644 --- a/include/SDL3/SDL_events.h +++ b/include/SDL3/SDL_events.h @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -113,6 +114,8 @@ typedef enum SDL_EVENT_WINDOW_RESTORED, /**< Window has been restored to normal size and position */ SDL_EVENT_WINDOW_MOUSE_ENTER, /**< Window has gained mouse focus */ SDL_EVENT_WINDOW_MOUSE_LEAVE, /**< Window has lost mouse focus */ + SDL_EVENT_WINDOW_PEN_ENTER, /**< Window has gained focus of the pressure-sensitive pen with ID "data1" */ + SDL_EVENT_WINDOW_PEN_LEAVE, /**< Window has lost focus of the pressure-sensitive pen with ID "data1" */ SDL_EVENT_WINDOW_FOCUS_GAINED, /**< Window has gained keyboard focus */ SDL_EVENT_WINDOW_FOCUS_LOST, /**< Window has lost keyboard focus */ SDL_EVENT_WINDOW_CLOSE_REQUESTED, /**< The window manager requests that the window be closed */ @@ -191,6 +194,13 @@ typedef enum /* Sensor events */ SDL_EVENT_SENSOR_UPDATE = 0x1200, /**< A sensor was updated */ + /* Pressure-sensitive pen events */ + SDL_EVENT_PEN_DOWN = 0x1300, /**< Pressure-sensitive pen touched drawing surface */ + SDL_EVENT_PEN_UP, /**< Pressure-sensitive pen stopped touching drawing surface */ + SDL_EVENT_PEN_MOTION, /**< Pressure-sensitive pen moved, or angle/pressure changed */ + SDL_EVENT_PEN_BUTTON_DOWN, /**< Pressure-sensitive pen button pressed */ + SDL_EVENT_PEN_BUTTON_UP, /**< Pressure-sensitive pen button released */ + /* Render events */ SDL_EVENT_RENDER_TARGETS_RESET = 0x2000, /**< The render targets have been reset and their contents need to be updated */ SDL_EVENT_RENDER_DEVICE_RESET, /**< The device has been reset and all textures need to be recreated */ @@ -296,7 +306,7 @@ typedef struct SDL_MouseMotionEvent Uint32 type; /**< ::SDL_EVENT_MOUSE_MOTION */ Uint64 timestamp; /**< In nanoseconds, populated using SDL_GetTicksNS() */ SDL_WindowID windowID; /**< The window with mouse focus, if any */ - SDL_MouseID which; /**< The mouse instance id, or SDL_TOUCH_MOUSEID */ + SDL_MouseID which; /**< The mouse instance id, SDL_TOUCH_MOUSEID, or SDL_PEN_MOUSEID */ Uint32 state; /**< The current button state */ float x; /**< X coordinate, relative to window */ float y; /**< Y coordinate, relative to window */ @@ -312,7 +322,7 @@ typedef struct SDL_MouseButtonEvent Uint32 type; /**< ::SDL_EVENT_MOUSE_BUTTON_DOWN or ::SDL_EVENT_MOUSE_BUTTON_UP */ Uint64 timestamp; /**< In nanoseconds, populated using SDL_GetTicksNS() */ SDL_WindowID windowID; /**< The window with mouse focus, if any */ - SDL_MouseID which; /**< The mouse instance id, or SDL_TOUCH_MOUSEID */ + SDL_MouseID which; /**< The mouse instance id, SDL_TOUCH_MOUSEID, or SDL_PEN_MOUSEID */ Uint8 button; /**< The mouse button index */ Uint8 state; /**< ::SDL_PRESSED or ::SDL_RELEASED */ Uint8 clicks; /**< 1 for single-click, 2 for double-click, etc. */ @@ -329,7 +339,7 @@ typedef struct SDL_MouseWheelEvent Uint32 type; /**< ::SDL_EVENT_MOUSE_WHEEL */ Uint64 timestamp; /**< In nanoseconds, populated using SDL_GetTicksNS() */ SDL_WindowID windowID; /**< The window with mouse focus, if any */ - SDL_MouseID which; /**< The mouse instance id, or SDL_TOUCH_MOUSEID */ + SDL_MouseID which; /**< The mouse instance id, SDL_TOUCH_MOUSEID, or SDL_PEN_MOUSEID */ float x; /**< The amount scrolled horizontally, positive to the right and negative to the left */ float y; /**< The amount scrolled vertically, positive away from the user and negative toward the user */ Uint32 direction; /**< Set to one of the SDL_MOUSEWHEEL_* defines. When FLIPPED the values in X and Y will be opposite. Multiply by -1 to change them back */ @@ -512,6 +522,63 @@ typedef struct SDL_TouchFingerEvent #define SDL_DROPEVENT_DATA_SIZE 64 +/** + * Pressure-sensitive pen touched or stopped touching surface (event.ptip.*) + */ +typedef struct SDL_PenTipEvent +{ + Uint32 type; /**< ::SDL_EVENT_PEN_DOWN or ::SDL_EVENT_PEN_UP */ + Uint64 timestamp; /**< In nanoseconds, populated using SDL_GetTicksNS() */ + Uint32 windowID; /**< The window with pen focus, if any */ + SDL_PenID which; /**< The pen instance id */ + Uint8 tip; /**< ::SDL_PEN_TIP_INK when using a regular pen tip, or ::SDL_PEN_TIP_ERASER if the pen is being used as an eraser (e.g., flipped to use the eraser tip) */ + Uint8 state; /**< ::SDL_PRESSED on ::SDL_EVENT_PEN_DOWN and ::SDL_RELEASED on ::SDL_EVENT_PEN_UP */ + Uint16 pen_state; /**< Pen button masks (where SDL_BUTTON(1) is the first button, SDL_BUTTON(2) is the second button etc.), + ::SDL_PEN_DOWN_MASK is set if the pen is touching the surface, and + ::SDL_PEN_ERASER_MASK is set if the pen is (used as) an eraser. */ + float x; /**< X coordinate, relative to window */ + float y; /**< Y coordinate, relative to window */ + float axes[SDL_PEN_NUM_AXES]; /**< Pen axes such as pressure and tilt (ordered as per ::SDL_PenAxis) */ +} SDL_PenTipEvent; + +/** + * Pressure-sensitive pen motion / pressure / angle event structure (event.pmotion.*) + */ +typedef struct SDL_PenMotionEvent +{ + Uint32 type; /**< ::SDL_EVENT_PEN_MOTION */ + Uint64 timestamp; /**< In nanoseconds, populated using SDL_GetTicksNS() */ + Uint32 windowID; /**< The window with pen focus, if any */ + SDL_PenID which; /**< The pen instance id */ + Uint8 padding1; + Uint8 padding2; + Uint16 pen_state; /**< Pen button masks (where SDL_BUTTON(1) is the first button, SDL_BUTTON(2) is the second button etc.), + ::SDL_PEN_DOWN_MASK is set if the pen is touching the surface, and + ::SDL_PEN_ERASER_MASK is set if the pen is (used as) an eraser. */ + float x; /**< X coordinate, relative to window */ + float y; /**< Y coordinate, relative to window */ + float axes[SDL_PEN_NUM_AXES]; /**< Pen axes such as pressure and tilt (ordered as per ::SDL_PenAxis) */ +} SDL_PenMotionEvent; + +/** + * Pressure-sensitive pen button event structure (event.pbutton.*) + */ +typedef struct SDL_PenButtonEvent +{ + Uint32 type; /**< ::SDL_EVENT_PEN_BUTTON_DOWN or ::SDL_EVENT_PEN_BUTTON_UP */ + Uint64 timestamp; /**< In nanoseconds, populated using SDL_GetTicksNS() */ + Uint32 windowID; /**< The window with pen focus, if any */ + SDL_PenID which; /**< The pen instance id */ + Uint8 button; /**< The pen button index (1 represents the pen tip for compatibility with mouse events) */ + Uint8 state; /**< ::SDL_PRESSED or ::SDL_RELEASED */ + Uint16 pen_state; /**< Pen button masks (where SDL_BUTTON(1) is the first button, SDL_BUTTON(2) is the second button etc.), + ::SDL_PEN_DOWN_MASK is set if the pen is touching the surface, and + ::SDL_PEN_ERASER_MASK is set if the pen is (used as) an eraser. */ + float x; /**< X coordinate, relative to window */ + float y; /**< Y coordinate, relative to window */ + float axes[SDL_PEN_NUM_AXES]; /**< Pen axes such as pressure and tilt (ordered as per ::SDL_PenAxis) */ +} SDL_PenButtonEvent; + /** * An event used to drop text or request a file open by the system (event.drop.*) * @@ -603,6 +670,9 @@ typedef union SDL_Event SDL_QuitEvent quit; /**< Quit request event data */ SDL_UserEvent user; /**< Custom event data */ SDL_TouchFingerEvent tfinger; /**< Touch finger event data */ + SDL_PenTipEvent ptip; /**< Pen tip touching or leaving drawing surface */ + SDL_PenMotionEvent pmotion; /**< Pen change in position, pressure, or angle */ + SDL_PenButtonEvent pbutton; /**< Pen button press */ SDL_DropEvent drop; /**< Drag and drop event data */ SDL_ClipboardEvent clipboard; /**< Clipboard event data */ diff --git a/include/SDL3/SDL_hints.h b/include/SDL3/SDL_hints.h index 0e4bf75652..91d7f0670c 100644 --- a/include/SDL3/SDL_hints.h +++ b/include/SDL3/SDL_hints.h @@ -1220,6 +1220,40 @@ extern "C" { */ #define SDL_HINT_MOUSE_AUTO_CAPTURE "SDL_MOUSE_AUTO_CAPTURE" +/** + * Treat pen movement as separate from mouse movement + * + * By default, pens report both ::SDL_MouseMotionEvent and ::SDL_PenMotionEvent updates + * (analogously for button presses). This hint allows decoupling mouse and pen updates. + * + * This variable toggles between the following behaviour: + * "0" - (Default) Pen acts as a mouse with mouse ID ::SDL_PEN_MOUSEID. + * Use case: client application is not pen aware, user wants to + * use pen instead of mouse to interact. + * "1" - Pen reports mouse clicks and movement events but does not update + * SDL-internal mouse state (buttons pressed, current mouse location). + * Use case: client application is not pen aware, user frequently + * alternates between pen and "real" mouse. + * "2" - Pen reports no mouse events. + * Use case: pen-aware client application uses this hint to allow user to + * toggle between pen+mouse mode ("2") and pen-only mode ("1" or "0"). + */ +#define SDL_HINT_PEN_NOT_MOUSE "SDL_HINT_PEN_NOT_MOUSE" + +/** + * Pen mouse button emulation triggers only when the pen touches the tablet surface + * + * "0" - The pen reports mouse button press/release immediately when the pen + * button is pressed/released, and the pen tip touching the surface counts + * as left mouse button press. + * "1" - (Default) Mouse button presses are sent when the pen first touches + * the tablet (analogously for releases). Not pressing a pen button + * simulates mouse button 1, pressing the first pen button simulates + * mouse button 2 etc.; it is not possible to report multiple buttons + * as pressed at the same time. + */ +#define SDL_HINT_PEN_DELAY_MOUSE_BUTTON "SDL_HINT_PEN_DELAY_MOUSE_BUTTON" + /** * Tell SDL not to catch the SIGINT or SIGTERM signals. * diff --git a/include/SDL3/SDL_pen.h b/include/SDL3/SDL_pen.h new file mode 100644 index 0000000000..0f48da90ab --- /dev/null +++ b/include/SDL3/SDL_pen.h @@ -0,0 +1,285 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2023 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +/** + * \file SDL_pen.h + * + * Include file for SDL pen event handling. + * + * This file describes operations for pressure-sensitive pen (stylus and/or eraser) handling, e.g., for input + * and drawing tablets or suitably equipped mobile / tablet devices. + * + * To get started with pens: + * - Listen to ::SDL_PenMotionEvent and ::SDL_PenButtonEvent + * - To avoid treating pen events as mouse events, ignore ::SDL_MouseMotionEvent and ::SDL_MouseButtonEvent + * whenever "which" == ::SDL_PEN_MOUSEID. + * + * This header file describes advanced functionality that can be useful for managing user configuration + * and understanding the capabilities of the attached pens. + * + * We primarily identify pens by ::SDL_PenID. The implementation makes a best effort to relate each :SDL_PenID + * to the same physical device during a session. Formerly valid ::SDL_PenID values remain valid + * even if a device disappears. + * + * For identifying pens across sessions, the API provides the type ::SDL_GUID . + */ + +#ifndef SDL_pen_h_ +#define SDL_pen_h_ + +#include "SDL_error.h" +#include "SDL_guid.h" +#include "SDL_stdinc.h" + +/* Set up for C function definitions, even when using C++ */ +#ifdef __cplusplus +extern "C" { +#endif + +typedef Uint32 SDL_PenID; /**< SDL_PenIDs identify pens uniquely within a session */ + +#define SDL_PEN_INVALID ((Uint32)0) /**< Reserved invalid ::SDL_PenID is valid */ + +#define SDL_PEN_MOUSEID ((Uint32)-2) /**< Device ID for mouse events triggered by pen events */ + +#define SDL_PEN_INFO_UNKNOWN (-1) /**< Marks unknown information when querying the pen */ + +/** + * Pen axis indices + * + * Below are the valid indices to the "axis" array from ::SDL_PenMotionEvent and ::SDL_PenButtonEvent. + * The axis indices form a contiguous range of ints from 0 to ::SDL_PEN_AXIS_LAST, inclusive. + * All "axis[]" entries are either normalised to 0..1 or report a (positive or negative) + * angle in degrees, with 0.0 representing the centre. + * Not all pens/backends support all axes: unsupported entries are always "0.0f". + * + * To convert angles for tilt and rotation into vector representation, use + * \link SDL_sinf \endlink on the XTILT, YTILT, or ROTATION component, e.g., "SDL_sinf(xtilt * SDL_PI_F / 180.0)". + */ +typedef enum +{ + SDL_PEN_AXIS_PRESSURE = 0, /**< Pen pressure. Unidirectional: 0..1.0 */ + SDL_PEN_AXIS_XTILT, /**< Pen horizontal tilt angle. Bidirectional: -90.0..90.0 (left-to-right). + The physical max/min tilt may be smaller than -90.0 / 90.0, cf. \link SDL_PenCapabilityInfo \endlink */ + SDL_PEN_AXIS_YTILT, /**< Pen vertical tilt angle. Bidirectional: -90.0..90.0 (top-to-down). + The physical max/min tilt may be smaller than -90.0 / 90.0, cf. \link SDL_PenCapabilityInfo \endlink */ + SDL_PEN_AXIS_DISTANCE, /**< Pen distance to drawing surface. Unidirectional: 0.0..1.0 */ + SDL_PEN_AXIS_ROTATION, /**< Pen barrel rotation. Bidirectional: -180..179.9 (clockwise, 0 is facing up, -180.0 is facing down). */ + SDL_PEN_AXIS_SLIDER, /**< Pen finger wheel or slider (e.g., Airbrush Pen). Unidirectional: 0..1.0 */ + SDL_PEN_NUM_AXES, /**< Last valid axis index */ + SDL_PEN_AXIS_LAST = SDL_PEN_NUM_AXES - 1 /**< Last axis index plus 1 */ +} SDL_PenAxis; + +/* Pen flags. These share a bitmask space with ::SDL_BUTTON_LEFT and friends. */ +#define SDL_PEN_FLAG_DOWN_BIT_INDEX 13 /* Bit for storing that pen is touching the surface */ +#define SDL_PEN_FLAG_INK_BIT_INDEX 14 /* Bit for storing has-non-eraser-capability status */ +#define SDL_PEN_FLAG_ERASER_BIT_INDEX 15 /* Bit for storing is-eraser or has-eraser-capability property */ +#define SDL_PEN_FLAG_AXIS_BIT_OFFSET 16 /* Bit for storing has-axis-0 property */ + +#define SDL_PEN_CAPABILITY(capbit) (1ul << (capbit)) +#define SDL_PEN_AXIS_CAPABILITY(axis) SDL_PEN_CAPABILITY((axis) + SDL_PEN_FLAG_AXIS_BIT_OFFSET) + +/** + * Pen tips + * @{ + */ +#define SDL_PEN_TIP_INK SDL_PEN_FLAG_INK_BIT_INDEX /**< Regular pen tip (for drawing) touched the surface */ +#define SDL_PEN_TIP_ERASER SDL_PEN_FLAG_ERASER_BIT_INDEX /**< Eraser pen tip touched the surface */ +/** @} */ + + +/** + * \defgroup SDL_PEN_CAPABILITIES Pen capabilities + * Pen capabilities reported by ::SDL_GetPenCapabilities + * @{ + */ +#define SDL_PEN_DOWN_MASK SDL_PEN_CAPABILITY(SDL_PEN_FLAG_DOWN_BIT_INDEX) /**< Pen tip is currently touching the drawing surface. */ +#define SDL_PEN_INK_MASK SDL_PEN_CAPABILITY(SDL_PEN_FLAG_INK_BIT_INDEX) /**< Pen has a regular drawing tip (::SDL_GetPenCapabilities). For events (::SDL_PenButtonEvent, ::SDL_PenMotionEvent, ::SDL_GetPenStatus) this flag is mutually exclusive with ::SDL_PEN_ERASER_MASK . */ +#define SDL_PEN_ERASER_MASK SDL_PEN_CAPABILITY(SDL_PEN_FLAG_ERASER_BIT_INDEX) /**< Pen has an eraser tip (::SDL_GetPenCapabilities) or is being used as eraser (::SDL_PenButtonEvent , ::SDL_PenMotionEvent , ::SDL_GetPenStatus) */ +#define SDL_PEN_AXIS_PRESSURE_MASK SDL_PEN_AXIS_CAPABILITY(SDL_PEN_AXIS_PRESSURE) /**< Pen provides pressure information in axis ::SDL_PEN_AXIS_PRESSURE */ +#define SDL_PEN_AXIS_XTILT_MASK SDL_PEN_AXIS_CAPABILITY(SDL_PEN_AXIS_XTILT) /**< Pen provides horizontal tilt information in axis ::SDL_PEN_AXIS_XTILT */ +#define SDL_PEN_AXIS_YTILT_MASK SDL_PEN_AXIS_CAPABILITY(SDL_PEN_AXIS_YTILT) /**< Pen provides vertical tilt information in axis ::SDL_PEN_AXIS_YTILT */ +#define SDL_PEN_AXIS_DISTANCE_MASK SDL_PEN_AXIS_CAPABILITY(SDL_PEN_AXIS_DISTANCE) /**< Pen provides distance to drawing tablet in ::SDL_PEN_AXIS_DISTANCE */ +#define SDL_PEN_AXIS_ROTATION_MASK SDL_PEN_AXIS_CAPABILITY(SDL_PEN_AXIS_ROTATION) /**< Pen provides barrel rotation information in axis ::SDL_PEN_AXIS_ROTATION */ +#define SDL_PEN_AXIS_SLIDER_MASK SDL_PEN_AXIS_CAPABILITY(SDL_PEN_AXIS_SLIDER) /**< Pen provides slider / finger wheel or similar in axis ::SDL_PEN_AXIS_SLIDER */ + +#define SDL_PEN_AXIS_BIDIRECTIONAL_MASKS (SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK) +/**< Masks for all axes that may be bidirectional */ +/** @} */ + +/** + * Pen types + * + * Some pens identify as a particular type of drawing device (e.g., an airbrush or a pencil). + * + */ +typedef enum +{ + SDL_PEN_TYPE_ERASER = 1, /**< Eraser */ + SDL_PEN_TYPE_PEN, /**< Generic pen; this is the default. */ + SDL_PEN_TYPE_PENCIL, /**< Pencil */ + SDL_PEN_TYPE_BRUSH, /**< Brush-like device */ + SDL_PEN_TYPE_AIRBRUSH, /**< Airbrush device that "sprays" ink */ + SDL_PEN_TYPE_LAST = SDL_PEN_TYPE_AIRBRUSH /**< Last valid pen type */ +} SDL_PenSubtype; + + +/* Function prototypes */ + +/** + * Retrieves all pens that are connected to the system. + * + * Yields an array of ::SDL_PenID values. These identify and track pens throughout a session. + * To track pens across sessions (program restart), use ::SDL_GUID . + * + * \param[out] count The number of pens in the array (number of array elements minus 1, i.e., not + * counting the terminator 0). + * + * \returns A 0 terminated array of ::SDL_PenID values, or NULL on error. + * The array must be freed with ::SDL_free(). + * On a NULL return, ::SDL_GetError() is set. + * + * \since This function is available since SDL 3.TBD + */ +extern DECLSPEC SDL_PenID *SDLCALL SDL_GetPens(int *count); + +/** + * Retrieves the pen's current status. + * + * If the pen is detached (cf. ::SDL_PenConnected), this operation may return + * default values. + * + * \param instance_id The pen to query. + * \param[out] x Out-mode parameter for pen x coordinate. May be NULL. + * \param[out] y Out-mode parameter for pen y coordinate. May be NULL. + * \param[out] axes Out-mode parameter for axis information. May be null. The axes are in the same order as for + * ::SDL_PenAxis . + * \param num_axes Maximum number of axes to write to "axes". + * + * \returns a bit mask with the current pen button states (::SDL_BUTTON_LMASK etc.), + * possibly ::SDL_PEN_DOWN_MASK, and exactly one of + * ::SDL_PEN_INK_MASK or ::SDL_PEN_ERASER_MASK , or 0 on error (see ::SDL_GetError()). + * + * \since This function is available since SDL 3.TBD + */ +extern DECLSPEC Uint32 SDLCALL SDL_GetPenStatus(SDL_PenID instance_id, float *x, float *y, float *axes, size_t num_axes); + +/** + * Retrieves an ::SDL_PenID for the given ::SDL_GUID. + * + * \param guid A pen GUID. + * + * \returns A valid ::SDL_PenID, or ::SDL_PEN_INVALID if there is no matching SDL_PenID. + * + * \since This function is available since SDL 3.TBD + * + * \sa SDL_GUID() + */ +extern DECLSPEC SDL_PenID SDLCALL SDL_GetPenFromGUID(SDL_GUID guid); + +/** + * Retrieves the ::SDL_GUID for a given ::SDL_PenID. + * + * \param instance_id The pen to query. + * + * \returns The corresponding pen GUID; persistent across multiple sessions. + * If "instance_id" is ::SDL_PEN_INVALID, returns an all-zeroes GUID. + * + * \since This function is available since SDL 3.TBD + * + * \sa SDL_PenForID() + */ +extern DECLSPEC SDL_GUID SDLCALL SDL_GetPenGUID(SDL_PenID instance_id); + +/** + * Checks whether a pen is still attached. + * + * If a pen is detached, it will not show up for ::SDL_GetPens(). + * Other operations will still be available but may return default values. + * + * \param instance_id A pen ID. + * \returns SDL_TRUE if "instance_id" is valid and the corresponding pen is attached, or + * SDL_FALSE otherwise. + * + * \since This function is available since SDL 3.TBD + */ +extern DECLSPEC SDL_bool SDLCALL SDL_PenConnected(SDL_PenID instance_id); + +/** + * Retrieves a human-readable description for a ::SDL_PenID. + * + * \param instance_id The pen to query. + * + * \returns A string that contains the name of the pen, intended for human consumption. + * The string might or might not be localised, depending on platform settings. + * It is not guaranteed to be unique; use ::SDL_GetPenGUID() for (best-effort) + * unique identifiers. + * The pointer is managed by the SDL pen subsystem and must not be deallocated. + * The pointer remains valid until SDL is shut down. + * Returns NULL on error (cf. ::SDL_GetError()) + * + * \since This function is available since SDL 3.TBD + */ +extern DECLSPEC const char *SDLCALL SDL_GetPenName(SDL_PenID instance_id); + +/** + * Pen capabilities, as reported by ::SDL_GetPenCapabilities() + */ +typedef struct SDL_PenCapabilityInfo +{ + float max_tilt; /**< Physical maximum tilt angle, for XTILT and YTILT, or SDL_PEN_INFO_UNKNOWN . Pens cannot typically tilt all the way to 90 degrees, so this value is usually less than 90.0. */ + Uint32 wacom_id; /**< For Wacom devices: wacom tool type ID, otherwise 0 (useful e.g. with libwacom) */ + Sint8 num_buttons; /**< Number of pen buttons (not counting the pen tip), or SDL_PEN_INFO_UNKNOWN */ +} SDL_PenCapabilityInfo; + +/** + * Retrieves capability flags for a given ::SDL_PenID. + * + * \param instance_id The pen to query. + * \param[out] capabilities Detail information about pen capabilities, such as the number of buttons + * + * \returns a set of capability flags, cf. \link SDL_PEN_CAPABILITIES \endlink. Returns 0 on error + * (cf. ::SDL_GetError()) + * + * \since This function is available since SDL 3.TBD + */ +extern DECLSPEC Uint32 SDLCALL SDL_GetPenCapabilities(SDL_PenID instance_id, SDL_PenCapabilityInfo *capabilities); + +/** + * Retrieves the pen type for a given ::SDL_PenID. + * + * \param instance_id The pen to query. + * \returns The corresponding pen type (cf. ::SDL_PenSubtype) or 0 on error. Note that the pen type does not + * dictate whether the pen tip is ::SDL_PEN_TIP_INK or ::SDL_PEN_TIP_ERASER; to determine whether a pen + * is being used for drawing or in eraser mode, check either the pen tip on ::SDL_EVENT_PEN_DOWN, or the + * flag ::SDL_PEN_ERASER_MASK in the pen state. + * \since This function is available since SDL 3.TBD + */ +extern DECLSPEC SDL_PenSubtype SDLCALL SDL_GetPenType(SDL_PenID instance_id); + +/* Ends C function definitions when using C++ */ +#ifdef __cplusplus +} +#endif + +#endif /* SDL_pen_h_ */ + +/* vi: set ts=4 sw=4 expandtab: */ diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym index 8d7f465261..972a6ee45d 100644 --- a/src/dynapi/SDL_dynapi.sym +++ b/src/dynapi/SDL_dynapi.sym @@ -940,6 +940,22 @@ SDL3_0.0.0 { SDL_GetVideoCaptureDevices; SDL_GetGamepadButtonLabelForType; SDL_GetGamepadButtonLabel; + SDL_GetPens; + SDL_GetPenStatus; + SDL_GetPenFromGUID; + SDL_GetPenGUID; + SDL_PenConnected; + SDL_GetPenName; + SDL_GetPenCapabilities; + SDL_GetPenType; + SDL_GetPens; + SDL_GetPenStatus; + SDL_GetPenFromGUID; + SDL_GetPenGUID; + SDL_PenConnected; + SDL_GetPenName; + SDL_GetPenCapabilities; + SDL_GetPenType; # extra symbols go here (don't modify this line) local: *; }; diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h index e45318e2c9..41a95138b0 100644 --- a/src/dynapi/SDL_dynapi_overrides.h +++ b/src/dynapi/SDL_dynapi_overrides.h @@ -965,3 +965,19 @@ #define SDL_GetVideoCaptureDevices SDL_GetVideoCaptureDevices_REAL #define SDL_GetGamepadButtonLabelForType SDL_GetGamepadButtonLabelForType_REAL #define SDL_GetGamepadButtonLabel SDL_GetGamepadButtonLabel_REAL +#define SDL_GetPens SDL_GetPens_REAL +#define SDL_GetPenStatus SDL_GetPenStatus_REAL +#define SDL_GetPenFromGUID SDL_GetPenFromGUID_REAL +#define SDL_GetPenGUID SDL_GetPenGUID_REAL +#define SDL_PenConnected SDL_PenConnected_REAL +#define SDL_GetPenName SDL_GetPenName_REAL +#define SDL_GetPenCapabilities SDL_GetPenCapabilities_REAL +#define SDL_GetPenType SDL_GetPenType_REAL +#define SDL_GetPens SDL_GetPens_REAL +#define SDL_GetPenStatus SDL_GetPenStatus_REAL +#define SDL_GetPenFromGUID SDL_GetPenFromGUID_REAL +#define SDL_GetPenGUID SDL_GetPenGUID_REAL +#define SDL_PenConnected SDL_PenConnected_REAL +#define SDL_GetPenName SDL_GetPenName_REAL +#define SDL_GetPenCapabilities SDL_GetPenCapabilities_REAL +#define SDL_GetPenType SDL_GetPenType_REAL diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h index fb8f1c4578..07f1bd1e2d 100644 --- a/src/dynapi/SDL_dynapi_procs.h +++ b/src/dynapi/SDL_dynapi_procs.h @@ -998,3 +998,11 @@ SDL_DYNAPI_PROC(void,SDL_CloseVideoCapture,(SDL_VideoCaptureDevice *a),(a),) SDL_DYNAPI_PROC(SDL_VideoCaptureDeviceID*,SDL_GetVideoCaptureDevices,(int *a),(a),return) SDL_DYNAPI_PROC(SDL_GamepadButtonLabel,SDL_GetGamepadButtonLabelForType,(SDL_GamepadType a, SDL_GamepadButton b),(a,b),return) SDL_DYNAPI_PROC(SDL_GamepadButtonLabel,SDL_GetGamepadButtonLabel,(SDL_Gamepad *a, SDL_GamepadButton b),(a,b),return) +SDL_DYNAPI_PROC(SDL_PenID*,SDL_GetPens,(int *a),(a),return) +SDL_DYNAPI_PROC(Uint32,SDL_GetPenStatus,(SDL_PenID a, float *b, float *c, float *d, size_t e),(a,b,c,d,e),return) +SDL_DYNAPI_PROC(SDL_PenID,SDL_GetPenFromGUID,(SDL_GUID a),(a),return) +SDL_DYNAPI_PROC(SDL_GUID,SDL_GetPenGUID,(SDL_PenID a),(a),return) +SDL_DYNAPI_PROC(SDL_bool,SDL_PenConnected,(SDL_PenID a),(a),return) +SDL_DYNAPI_PROC(const char*,SDL_GetPenName,(SDL_PenID a),(a),return) +SDL_DYNAPI_PROC(Uint32,SDL_GetPenCapabilities,(SDL_PenID a, SDL_PenCapabilityInfo *b),(a,b),return) +SDL_DYNAPI_PROC(SDL_PenSubtype,SDL_GetPenType,(SDL_PenID a),(a),return) diff --git a/src/dynapi/gendynapi.py b/src/dynapi/gendynapi.py index b91f2ee7f2..e08ce2cc60 100755 --- a/src/dynapi/gendynapi.py +++ b/src/dynapi/gendynapi.py @@ -379,7 +379,7 @@ def check_comment(): if header != 'SDL_stdinc.h': parameter_name = i['parameter_name'] for n in parameter_name: - if n != "" and "\\param " + n not in comment: + if n != "" and "\\param " + n not in comment and "\\param[out] " + n not in comment: check_comment_header() print(" In file %s: function %s() missing '\\param %s'" % (header, name, n)); diff --git a/src/events/SDL_events.c b/src/events/SDL_events.c index f79f46970e..5879629ba0 100644 --- a/src/events/SDL_events.c +++ b/src/events/SDL_events.c @@ -192,7 +192,7 @@ static void SDLCALL SDL_PollSentinelChanged(void *userdata, const char *name, co * Verbosity of logged events as defined in SDL_HINT_EVENT_LOGGING: * - 0: (default) no logging * - 1: logging of most events - * - 2: as above, plus mouse and finger motion + * - 2: as above, plus mouse, pen, and finger motion */ static int SDL_EventLoggingVerbosity = 0; @@ -206,10 +206,11 @@ static void SDL_LogEvent(const SDL_Event *event) char name[64]; char details[128]; - /* sensor/mouse/finger motion are spammy, ignore these if they aren't demanded. */ + /* sensor/mouse/pen/finger motion are spammy, ignore these if they aren't demanded. */ if ((SDL_EventLoggingVerbosity < 2) && ((event->type == SDL_EVENT_MOUSE_MOTION) || (event->type == SDL_EVENT_FINGER_MOTION) || + (event->type == SDL_EVENT_PEN_MOTION) || (event->type == SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION) || (event->type == SDL_EVENT_GAMEPAD_SENSOR_UPDATE) || (event->type == SDL_EVENT_SENSOR_UPDATE))) { @@ -302,6 +303,8 @@ static void SDL_LogEvent(const SDL_Event *event) SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_RESTORED); SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_MOUSE_ENTER); SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_MOUSE_LEAVE); + SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_PEN_ENTER); + SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_PEN_LEAVE); SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_FOCUS_GAINED); SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_FOCUS_LOST); SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_CLOSE_REQUESTED); @@ -470,6 +473,53 @@ static void SDL_LogEvent(const SDL_Event *event) break; #undef PRINT_FINGER_EVENT +#define PRINT_PTIP_EVENT(event) \ + (void)SDL_snprintf(details, sizeof(details), " (timestamp=%u windowid=%u which=%u tip=%u state=%s x=%g y=%g)", \ + (uint)event->ptip.timestamp, (uint)event->ptip.windowID, \ + (uint)event->ptip.which, (uint)event->ptip.tip, \ + event->ptip.state == SDL_PRESSED ? "down" : "up", \ + event->ptip.x, event->ptip.y) + SDL_EVENT_CASE(SDL_EVENT_PEN_DOWN) + PRINT_PTIP_EVENT(event); + break; + SDL_EVENT_CASE(SDL_EVENT_PEN_UP) + PRINT_PTIP_EVENT(event); + break; +#undef PRINT_PTIP_EVENT + + SDL_EVENT_CASE(SDL_EVENT_PEN_MOTION) + (void)SDL_snprintf(details, sizeof(details), " (timestamp=%u windowid=%u which=%u state=%08x x=%g y=%g [%g, %g, %g, %g, %g, %g])", + (uint)event->pmotion.timestamp, (uint)event->pmotion.windowID, + (uint)event->pmotion.which, (uint)event->pmotion.pen_state, + event->pmotion.x, event->pmotion.y, + event->pmotion.axes[SDL_PEN_AXIS_PRESSURE], + event->pmotion.axes[SDL_PEN_AXIS_XTILT], + event->pmotion.axes[SDL_PEN_AXIS_YTILT], + event->pmotion.axes[SDL_PEN_AXIS_DISTANCE], + event->pmotion.axes[SDL_PEN_AXIS_ROTATION], + event->pmotion.axes[SDL_PEN_AXIS_SLIDER]); + break; + +#define PRINT_PBUTTON_EVENT(event) \ + (void)SDL_snprintf(details, sizeof(details), " (timestamp=%u windowid=%u which=%u tip=%u state=%s x=%g y=%g axes=[%g, %g, %g, %g, %g, %g])", \ + (uint)event->pbutton.timestamp, (uint)event->pbutton.windowID, \ + (uint)event->pbutton.which, (uint)event->pbutton.button, \ + event->pbutton.state == SDL_PRESSED ? "pressed" : "released", \ + event->pbutton.x, event->pbutton.y, \ + event->pbutton.axes[SDL_PEN_AXIS_PRESSURE], \ + event->pbutton.axes[SDL_PEN_AXIS_XTILT], \ + event->pbutton.axes[SDL_PEN_AXIS_YTILT], \ + event->pbutton.axes[SDL_PEN_AXIS_DISTANCE], \ + event->pbutton.axes[SDL_PEN_AXIS_ROTATION], \ + event->pbutton.axes[SDL_PEN_AXIS_SLIDER]) + SDL_EVENT_CASE(SDL_EVENT_PEN_BUTTON_DOWN) + PRINT_PBUTTON_EVENT(event); + break; + SDL_EVENT_CASE(SDL_EVENT_PEN_BUTTON_UP) + PRINT_PBUTTON_EVENT(event); + break; +#undef PRINT_PBUTTON_EVENT + #define PRINT_DROP_EVENT(event) (void)SDL_snprintf(details, sizeof(details), " (data='%s' timestamp=%u windowid=%u x=%f y=%f)", event->drop.data, (uint)event->drop.timestamp, (uint)event->drop.windowID, event->drop.x, event->drop.y) SDL_EVENT_CASE(SDL_EVENT_DROP_FILE) PRINT_DROP_EVENT(event); diff --git a/src/events/SDL_mouse.c b/src/events/SDL_mouse.c index cd4e6a7195..6487ca1818 100644 --- a/src/events/SDL_mouse.c +++ b/src/events/SDL_mouse.c @@ -22,9 +22,11 @@ /* General mouse handling code for SDL */ -#include "SDL_events_c.h" #include "../SDL_hints_c.h" #include "../video/SDL_sysvideo.h" +#include "SDL_events_c.h" +#include "SDL_mouse_c.h" +#include "SDL_pen_c.h" #if defined(__WIN32__) || defined(__GDK__) #include "../core/windows/SDL_windows.h" // For GetDoubleClickTime() #endif @@ -221,6 +223,8 @@ void SDL_PostInitMouse(void) SDL_DestroySurface(surface); } } + + SDL_PenInit(); } void SDL_SetDefaultCursor(SDL_Cursor *cursor) @@ -351,17 +355,25 @@ void SDL_SetMouseFocus(SDL_Window *window) SDL_SetCursor(NULL); } +SDL_bool SDL_MousePositionInWindow(SDL_Window *window, SDL_MouseID mouseID, float x, float y) +{ + if (!window) { + return SDL_FALSE; + } + + if (window && !(window->flags & SDL_WINDOW_MOUSE_CAPTURE)) { + if (x < 0.0f || y < 0.0f || x >= (float)window->w || y >= (float)window->h) { + return SDL_FALSE; + } + } + return SDL_TRUE; +} + /* Check to see if we need to synthesize focus events */ static SDL_bool SDL_UpdateMouseFocus(SDL_Window *window, float x, float y, Uint32 buttonstate, SDL_bool send_mouse_motion) { SDL_Mouse *mouse = SDL_GetMouse(); - SDL_bool inWindow = SDL_TRUE; - - if (window && !(window->flags & SDL_WINDOW_MOUSE_CAPTURE)) { - if (x < 0.0f || y < 0.0f || x >= (float)window->w || y >= (float)window->h) { - inWindow = SDL_FALSE; - } - } + SDL_bool inWindow = SDL_MousePositionInWindow(window, mouse->mouseID, x, y); if (!inWindow) { if (window == mouse->focus) { diff --git a/src/events/SDL_mouse_c.h b/src/events/SDL_mouse_c.h index 0887b5626b..cd08f1640b 100644 --- a/src/events/SDL_mouse_c.h +++ b/src/events/SDL_mouse_c.h @@ -163,6 +163,9 @@ extern void SDL_PerformWarpMouseInWindow(SDL_Window *window, float x, float y, S extern void SDL_ResetMouse(void); #endif /* 0 */ +/* Check if mouse position is within window or captured by window */ +extern SDL_bool SDL_MousePositionInWindow(SDL_Window *window, SDL_MouseID mouseID, float x, float y); + /* Shutdown the mouse subsystem */ extern void SDL_QuitMouse(void); diff --git a/src/events/SDL_pen.c b/src/events/SDL_pen.c new file mode 100644 index 0000000000..78e6b617c0 --- /dev/null +++ b/src/events/SDL_pen.c @@ -0,0 +1,1083 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2023 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "SDL_internal.h" + +/* Pressure-sensitive pen handling code for SDL */ + +#include "../SDL_hints_c.h" +#include "SDL_events_c.h" +#include "SDL_pen_c.h" + +#define PEN_MOUSE_EMULATE 0 /* pen behaves like mouse */ +#define PEN_MOUSE_STATELESS 1 /* pen does not update mouse state */ +#define PEN_MOUSE_DISABLED 2 /* pen does not send mouse events */ + +/* flags that are not SDL_PEN_FLAG_ */ +#define PEN_FLAGS_CAPABILITIES (~(SDL_PEN_FLAG_NEW | SDL_PEN_FLAG_DETACHED | SDL_PEN_FLAG_STALE)) + +#define PEN_GET_PUBLIC_STATUS_MASK(pen) (((pen)->header.flags & (SDL_PEN_ERASER_MASK | SDL_PEN_DOWN_MASK))) + +static int pen_mouse_emulation_mode = PEN_MOUSE_EMULATE; /* SDL_HINT_PEN_NOT_MOUSE */ + +static int pen_delay_mouse_button_mode = 1; /* SDL_HINT_PEN_DELAY_MOUSE_BUTTON */ + +#ifndef SDL_THREADS_DISABLED +static SDL_Mutex *SDL_pen_access_lock; +# define SDL_LOCK_PENS() SDL_LockMutex(SDL_pen_access_lock) +# define SDL_UNLOCK_PENS() SDL_UnlockMutex(SDL_pen_access_lock) +#else +# define SDL_LOCK_PENS() +# define SDL_UNLOCK_PENS() +#endif + +static struct +{ + SDL_Pen *pens; /* if "sorted" is SDL_TRUE: + sorted by: (is-attached, id): + - first all attached pens, in ascending ID order + - then all detached pens, in ascending ID order */ + size_t pens_allocated; /* # entries allocated to "pens" */ + size_t pens_known; /* <= pens_allocated; this includes detached pens */ + size_t pens_attached; /* <= pens_known; all attached pens are at the beginning of "pens" */ + SDL_bool sorted; /* This is SDL_FALSE in the period between SDL_PenGCMark() and SDL_PenGCSWeep() */ +} pen_handler; + +static SDL_PenID pen_invalid = { SDL_PEN_INVALID }; + +static SDL_GUID pen_guid_zero = { { 0 } }; + +#define SDL_LOAD_LOCK_PEN(penvar, instance_id, err_return) \ + SDL_Pen *penvar; \ + if (instance_id == SDL_PEN_INVALID) { \ + SDL_SetError("Invalid SDL_PenID"); \ + return (err_return); \ + } \ + SDL_LOCK_PENS();\ + penvar = SDL_GetPenPtr(instance_id); \ + if (!(penvar)) { \ + SDL_SetError("Stale SDL_PenID"); \ + return (err_return); \ + } + +static int SDL_GUIDCompare(SDL_GUID lhs, SDL_GUID rhs) +{ + return SDL_memcmp(lhs.data, rhs.data, sizeof(lhs.data)); +} + +static int SDLCALL pen_compare(const void *lhs, const void *rhs) +{ + int left_inactive = (((const SDL_Pen *)lhs)->header.flags & SDL_PEN_FLAG_DETACHED); + int right_inactive = (((const SDL_Pen *)rhs)->header.flags & SDL_PEN_FLAG_DETACHED); + if (left_inactive && !right_inactive) { + return 1; + } + if (!left_inactive && right_inactive) { + return -1; + } + return ((const SDL_Pen *)lhs)->header.id - ((const SDL_Pen *)rhs)->header.id; +} + +static int SDLCALL pen_header_compare(const void *lhs, const void *rhs) +{ + const struct SDL_Pen_header *l = lhs; + const struct SDL_Pen_header *r = rhs; + int l_detached = l->flags & SDL_PEN_FLAG_DETACHED; + int r_detached = r->flags & SDL_PEN_FLAG_DETACHED; + + if (l_detached != r_detached) { + if (l_detached) { + return -1; + } + return 1; + } + + return l->id - r->id; +} + +SDL_Pen *SDL_GetPenPtr(const Uint32 instance_id) +{ + unsigned int i; + + if (!pen_handler.pens) { + return NULL; + } + + if (pen_handler.sorted) { + struct SDL_Pen_header key = { 0, 0 }; + SDL_Pen *pen; + + key.id = instance_id; + + pen = SDL_bsearch(&key, pen_handler.pens, + pen_handler.pens_known, sizeof(SDL_Pen), + pen_header_compare); + if (pen) { + return pen; + } + /* If the pen is not active, fall through */ + } + + /* fall back to linear search */ + for (i = 0; i < pen_handler.pens_known; ++i) { + if (pen_handler.pens[i].header.id == instance_id) { + return &pen_handler.pens[i]; + } + } + return NULL; +} + +SDL_PenID *SDL_GetPens(int *count) +{ + int i; + int pens_nr = (int)pen_handler.pens_attached; + SDL_PenID *pens = SDL_calloc(pens_nr + 1, sizeof(SDL_PenID)); + if (!pens) { /* OOM */ + return pens; + } + + for (i = 0; i < pens_nr; ++i) { + pens[i] = pen_handler.pens[i].header.id; + } + + if (count) { + *count = pens_nr; + } + return pens; +} + +SDL_PenID SDL_GetPenFromGUID(SDL_GUID guid) +{ + unsigned int i; + /* Must do linear search */ + SDL_LOCK_PENS(); + for (i = 0; i < pen_handler.pens_known; ++i) { + SDL_Pen *pen = &pen_handler.pens[i]; + + if (0 == SDL_GUIDCompare(guid, pen->guid)) { + SDL_UNLOCK_PENS(); + return pen->header.id; + } + } + SDL_UNLOCK_PENS(); + return pen_invalid; +} + +SDL_bool SDL_PenConnected(SDL_PenID instance_id) +{ + SDL_Pen *pen; + SDL_bool result; + + if (instance_id == SDL_PEN_INVALID) { + return SDL_FALSE; + } + + SDL_LOCK_PENS(); + pen = SDL_GetPenPtr(instance_id); + if (!pen) { + SDL_UNLOCK_PENS(); + return SDL_FALSE; + } + result = (pen->header.flags & SDL_PEN_FLAG_DETACHED) ? SDL_FALSE : SDL_TRUE; + SDL_UNLOCK_PENS(); + return result; +} + +SDL_GUID SDL_GetPenGUID(SDL_PenID instance_id) +{ + SDL_GUID result; + SDL_LOAD_LOCK_PEN(pen, instance_id, pen_guid_zero); + result = pen->guid; + SDL_UNLOCK_PENS(); + return result; +} + +const char *SDL_GetPenName(SDL_PenID instance_id) +{ + const char *result; + SDL_LOAD_LOCK_PEN(pen, instance_id, NULL); + result = pen->name; /* Allocated separately from the pen table, so it is safe to hand to client code */ + SDL_UNLOCK_PENS(); + return result; +} + +SDL_PenSubtype SDL_GetPenType(SDL_PenID instance_id) +{ + SDL_PenSubtype result; + SDL_LOAD_LOCK_PEN(pen, instance_id, 0u); + result = pen->type; + SDL_UNLOCK_PENS(); + return result; +} + +Uint32 SDL_GetPenCapabilities(SDL_PenID instance_id, SDL_PenCapabilityInfo *info) +{ + Uint32 result; + SDL_LOAD_LOCK_PEN(pen, instance_id, 0u); + if (info) { + *info = pen->info; + } + result = pen->header.flags & PEN_FLAGS_CAPABILITIES; + SDL_UNLOCK_PENS(); + return result; +} + +Uint32 SDL_GetPenStatus(SDL_PenID instance_id, float *x, float *y, float *axes, size_t num_axes) +{ + Uint32 result; + SDL_LOAD_LOCK_PEN(pen, instance_id, 0u); + + if (x) { + *x = pen->last.x; + } + if (y) { + *y = pen->last.y; + } + if (axes && num_axes) { + size_t axes_to_copy = SDL_min(num_axes, SDL_PEN_NUM_AXES); + SDL_memcpy(axes, pen->last.axes, sizeof(float) * axes_to_copy); + } + result = pen->last.buttons | (pen->header.flags & (SDL_PEN_INK_MASK | SDL_PEN_ERASER_MASK | SDL_PEN_DOWN_MASK)); + SDL_UNLOCK_PENS(); + return result; +} + +/* Backend functionality */ + +/* Sort all pens. Only safe during SDL_LOCK_PENS. */ +static void pen_sort(void) +{ + SDL_qsort(pen_handler.pens, + pen_handler.pens_known, + sizeof(SDL_Pen), + pen_compare); + pen_handler.sorted = SDL_TRUE; +} + +SDL_Pen *SDL_PenModifyBegin(const Uint32 instance_id) +{ + SDL_PenID id = { 0 }; + const size_t alloc_growth_constant = 1; /* Expect few pens */ + SDL_Pen *pen; + + id = instance_id; + + if (id == SDL_PEN_INVALID) { + SDL_SetError("Invalid SDL_PenID"); + return NULL; + } + + SDL_LOCK_PENS(); + pen = SDL_GetPenPtr(id); + + if (!pen) { + if (!pen_handler.pens || pen_handler.pens_known == pen_handler.pens_allocated) { + size_t pens_to_allocate = pen_handler.pens_allocated + alloc_growth_constant; + SDL_Pen *pens; + if (pen_handler.pens) { + pens = SDL_realloc(pen_handler.pens, sizeof(SDL_Pen) * pens_to_allocate); + SDL_memset(pens + pen_handler.pens_known, 0, + sizeof(SDL_Pen) * (pens_to_allocate - pen_handler.pens_allocated)); + } else { + pens = SDL_calloc(sizeof(SDL_Pen), pens_to_allocate); + } + pen_handler.pens = pens; + pen_handler.pens_allocated = pens_to_allocate; + } + pen = &pen_handler.pens[pen_handler.pens_known]; + pen_handler.pens_known += 1; + + /* Default pen initialisation */ + pen->header.id = id; + pen->header.flags = SDL_PEN_FLAG_NEW; + pen->info.num_buttons = SDL_PEN_INFO_UNKNOWN; + pen->info.max_tilt = SDL_PEN_INFO_UNKNOWN; + pen->type = SDL_PEN_TYPE_PEN; + pen->name = SDL_calloc(1, SDL_PEN_MAX_NAME); /* Never deallocated */ + } + return pen; +} + +void SDL_PenModifyAddCapabilities(SDL_Pen *pen, Uint32 capabilities) +{ + if (capabilities & SDL_PEN_ERASER_MASK) { + pen->header.flags &= ~SDL_PEN_INK_MASK; + } else if (capabilities & SDL_PEN_INK_MASK) { + pen->header.flags &= ~SDL_PEN_ERASER_MASK; + } + pen->header.flags |= (capabilities & PEN_FLAGS_CAPABILITIES); +} + +static void pen_hotplug_attach(SDL_Pen *pen) +{ + if (!pen->header.window) { + /* Attach to default window */ + const SDL_Mouse *mouse = SDL_GetMouse(); + SDL_SendPenWindowEvent(0, pen->header.id, mouse->focus); + } +} + +static void pen_hotplug_detach(SDL_Pen *pen) +{ + SDL_SendPenWindowEvent(0, pen->header.id, NULL); +} + +void SDL_PenModifyEnd(SDL_Pen *pen, SDL_bool attach) +{ + SDL_bool is_new = pen->header.flags & SDL_PEN_FLAG_NEW; + SDL_bool was_attached = !(pen->header.flags & (SDL_PEN_FLAG_DETACHED | SDL_PEN_FLAG_NEW)); + SDL_bool broke_sort_order = SDL_FALSE; + + if (pen->type == SDL_PEN_TYPE_NONE) { + /* remove pen */ + if (!is_new) { + if (!(pen->header.flags & SDL_PEN_FLAG_ERROR)) { + SDL_Log("Error: attempt to remove known pen %lu", (unsigned long) pen->header.id); + pen->header.flags |= SDL_PEN_FLAG_ERROR; + } + + /* Treat as detached pen of unknown type instead */ + pen->type = SDL_PEN_TYPE_PEN; + attach = SDL_FALSE; + } else { + pen_handler.pens_known -= 1; + SDL_memset(pen, 0, sizeof(SDL_Pen)); + SDL_UNLOCK_PENS(); + return; + } + } + + pen->header.flags &= ~(SDL_PEN_FLAG_NEW | SDL_PEN_FLAG_STALE | SDL_PEN_FLAG_DETACHED); + if (attach == SDL_FALSE) { + pen->header.flags |= SDL_PEN_FLAG_DETACHED; + if (was_attached) { + broke_sort_order = SDL_TRUE; + if (!is_new) { + pen_handler.pens_attached -= 1; + } + pen_hotplug_detach(pen); + } + } else if (!was_attached || is_new) { + broke_sort_order = SDL_TRUE; + pen_handler.pens_attached += 1; + pen_hotplug_attach(pen); + } + + if (is_new) { + /* default: name */ + if (!pen->name[0]) { + SDL_snprintf(pen->name, SDL_PEN_MAX_NAME, + "%s %lu", + pen->type == SDL_PEN_TYPE_ERASER ? "Eraser" : "Pen", + (unsigned long) pen->header.id); + } + + /* default: enabled axes */ + if (!(pen->header.flags & (SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK))) { + pen->info.max_tilt = 0; /* no tilt => no max_tilt */ + } + + /* sanity-check GUID */ + if (0 == SDL_GUIDCompare(pen->guid, pen_guid_zero)) { + SDL_Log("Error: pen %lu: has GUID 0", (unsigned long) pen->header.id); + } + + /* pen or eraser? */ + if (pen->type == SDL_PEN_TYPE_ERASER || pen->header.flags & SDL_PEN_ERASER_MASK) { + pen->header.flags = (pen->header.flags & ~SDL_PEN_INK_MASK) | SDL_PEN_ERASER_MASK; + pen->type = SDL_PEN_TYPE_ERASER; + } else { + pen->header.flags = (pen->header.flags & ~SDL_PEN_ERASER_MASK) | SDL_PEN_INK_MASK; + } + + broke_sort_order = SDL_TRUE; + } + if (broke_sort_order && pen_handler.sorted) { + /* Maintain sortedness invariant */ + pen_sort(); + } + SDL_UNLOCK_PENS(); +} + +void SDL_PenGCMark(void) +{ + unsigned int i; + SDL_LOCK_PENS(); + for (i = 0; i < pen_handler.pens_known; ++i) { + SDL_Pen *pen = &pen_handler.pens[i]; + pen->header.flags |= SDL_PEN_FLAG_STALE; + } + pen_handler.sorted = SDL_FALSE; + SDL_UNLOCK_PENS(); +} + +void SDL_PenGCSweep(void *context, void (*free_deviceinfo)(Uint32, void *, void *)) +{ + unsigned int i; + pen_handler.pens_attached = 0; + + SDL_LOCK_PENS(); + /* We don't actually free the SDL_Pen entries, so that we can still answer queries about + formerly active SDL_PenIDs later. */ + for (i = 0; i < pen_handler.pens_known; ++i) { + SDL_Pen *pen = &pen_handler.pens[i]; + + if (pen->header.flags & SDL_PEN_FLAG_STALE) { + pen->header.flags |= SDL_PEN_FLAG_DETACHED; + pen_hotplug_detach(pen); + if (pen->deviceinfo) { + free_deviceinfo(pen->header.id, pen->deviceinfo, context); + pen->deviceinfo = NULL; + } + } else { + pen_handler.pens_attached += 1; + } + + pen->header.flags &= ~SDL_PEN_FLAG_STALE; + } + pen_sort(); + /* We could test for changes in the above and send a hotplugging event here */ + SDL_UNLOCK_PENS(); +} + +static void pen_relative_coordinates(SDL_Window *window, SDL_bool window_relative, float *x, float *y) +{ + int win_x, win_y; + + if (window_relative) { + return; + } + SDL_GetWindowPosition(window, &win_x, &win_y); + *x -= win_x; + *y -= win_y; +} + +/* Initialises timestamp, windowID, which, pen_state, x, y, and axes */ +static void event_setup(const SDL_Pen *pen, const SDL_Window *window, Uint64 timestamp, const SDL_PenStatusInfo *status, SDL_Event *event) +{ + Uint16 last_buttons = pen->last.buttons; + + if (timestamp == 0) { + /* Generate timestamp ourselves, if needed, so that we report the same timestamp + for the pen event and for any emulated mouse event */ + timestamp = SDL_GetTicksNS(); + } + + /* This code assumes that all of the SDL_Pen*Event structures have the same layout for these fields. + * This is checked by testautomation_pen.c, pen_memoryLayout(). */ + event->pmotion.timestamp = timestamp; + event->pmotion.windowID = window ? window->id : 0; + event->pmotion.which = pen->header.id; + event->pmotion.pen_state = (Uint16)last_buttons | PEN_GET_PUBLIC_STATUS_MASK(pen); + event->pmotion.x = status->x; + event->pmotion.y = status->y; + SDL_memcpy(event->pmotion.axes, status->axes, SDL_PEN_NUM_AXES * sizeof(float)); +} + + +int SDL_SendPenMotion(Uint64 timestamp, + SDL_PenID instance_id, + SDL_bool window_relative, + const SDL_PenStatusInfo *status) +{ + const SDL_Mouse *mouse = SDL_GetMouse(); + int i; + SDL_Pen *pen = SDL_GetPenPtr(instance_id); + SDL_Event event; + SDL_bool posted = SDL_FALSE; + float x = status->x; + float y = status->y; + float last_x = pen->last.x; + float last_y = pen->last.y; + /* Suppress mouse updates for axis changes or sub-pixel movement: */ + SDL_bool send_mouse_update; + SDL_bool axes_changed = SDL_FALSE; + SDL_Window *window; + + if (!pen) { + return SDL_FALSE; + } + window = pen->header.window; + if (!window) { + return SDL_FALSE; + } + + pen_relative_coordinates(window, window_relative, &x, &y); + + /* Check if the event actually modifies any cached axis or location, update as neeed */ + if (x != last_x || y != last_y) { + axes_changed = SDL_TRUE; + pen->last.x = status->x; + pen->last.y = status->y; + } + for (i = 0; i < SDL_PEN_NUM_AXES; ++i) { + if ((pen->header.flags & SDL_PEN_AXIS_CAPABILITY(i)) && (status->axes[i] != pen->last.axes[i])) { + axes_changed = SDL_TRUE; + pen->last.axes[i] = status->axes[i]; + } + } + if (!axes_changed) { + /* No-op event */ + return SDL_FALSE; + } + + send_mouse_update = (x != last_x) || (y != last_y); + + if (!(SDL_MousePositionInWindow(window, mouse->mouseID, x, y))) { + return SDL_FALSE; + } + + if (SDL_EventEnabled(SDL_EVENT_PEN_MOTION)) { + event_setup(pen, window, timestamp, status, &event); + event.pmotion.type = SDL_EVENT_PEN_MOTION; + + posted = SDL_PushEvent(&event) > 0; + + if (!posted) { + return SDL_FALSE; + } + } + + if (send_mouse_update) { + switch (pen_mouse_emulation_mode) { + case PEN_MOUSE_EMULATE: + return (SDL_SendMouseMotion(0, window, SDL_PEN_MOUSEID, 0, x, y)) || posted; + + case PEN_MOUSE_STATELESS: + /* Report mouse event but don't update mouse state */ + if (SDL_EventEnabled(SDL_EVENT_MOUSE_MOTION)) { + event.motion.windowID = event.pmotion.windowID; + event.motion.timestamp = timestamp; + event.motion.which = SDL_PEN_MOUSEID; + event.motion.type = SDL_EVENT_MOUSE_MOTION; + event.motion.state = pen->last.buttons | PEN_GET_PUBLIC_STATUS_MASK(pen); + event.motion.x = x; + event.motion.y = y; + event.motion.xrel = last_x - x; + event.motion.yrel = last_y - y; + return (SDL_PushEvent(&event) > 0) || posted; + } + + default: + break; + } + } + return posted; +} + +int SDL_SendPenTipEvent(Uint64 timestamp, SDL_PenID instance_id, Uint8 state) +{ + SDL_Mouse *mouse = SDL_GetMouse(); + SDL_Pen *pen = SDL_GetPenPtr(instance_id); + SDL_Event event; + SDL_bool posted = SDL_FALSE; + SDL_PenStatusInfo *last = &pen->last; + int mouse_button = SDL_BUTTON_LEFT; + SDL_Window *window; + + if (!pen) { + return SDL_FALSE; + } + window = pen->header.window; + + if ((state == SDL_PRESSED) && !(window && SDL_MousePositionInWindow(window, mouse->mouseID, last->x, last->y))) { + return SDL_FALSE; + } + + if (state == SDL_PRESSED) { + event.pbutton.type = SDL_EVENT_PEN_DOWN; + pen->header.flags |= SDL_PEN_DOWN_MASK; + } else { + event.pbutton.type = SDL_EVENT_PEN_UP; + pen->header.flags &= ~SDL_PEN_DOWN_MASK; + } + + if (SDL_EventEnabled(event.ptip.type)) { + event_setup(pen, window, timestamp, &pen->last, &event); + + /* Used as eraser? Report eraser event, otherwise ink event */ + event.ptip.tip = (pen->header.flags & SDL_PEN_ERASER_MASK) ? SDL_PEN_TIP_ERASER : SDL_PEN_TIP_INK; + event.ptip.state = state == SDL_PRESSED ? SDL_PRESSED : SDL_RELEASED; + + posted = SDL_PushEvent(&event) > 0; + + if (!posted) { + return SDL_FALSE; + } + } + + /* Mouse emulation */ + if (pen_delay_mouse_button_mode) { + /* Send button events when pen touches / leaves surface */ + mouse_button = pen->last_mouse_button; + if (0 == mouse_button) { + mouse_button = SDL_BUTTON_LEFT; /* No current button? Instead report left mouse button */ + } + } + + switch (pen_mouse_emulation_mode) { + case PEN_MOUSE_EMULATE: + return (SDL_SendMouseButton(timestamp, window, SDL_PEN_MOUSEID, state, mouse_button)) || posted; + + case PEN_MOUSE_STATELESS: + /* Report mouse event without updating mouse state */ + event.button.type = state == SDL_PRESSED ? SDL_EVENT_MOUSE_BUTTON_DOWN : SDL_EVENT_MOUSE_BUTTON_UP; + if (SDL_EventEnabled(event.button.type)) { + event.button.windowID = window ? window->id : 0; + event.button.timestamp = timestamp; + event.button.which = SDL_PEN_MOUSEID; + + event.button.state = state; + event.button.button = mouse_button; + event.button.clicks = 1; + event.button.x = last->x; + event.button.y = last->y; + return (SDL_PushEvent(&event) > 0) || posted; + } + break; + + default: + break; + } + return posted; +} + +int SDL_SendPenButton(Uint64 timestamp, + SDL_PenID instance_id, + Uint8 state, Uint8 button) +{ + SDL_Mouse *mouse = SDL_GetMouse(); + SDL_Pen *pen = SDL_GetPenPtr(instance_id); + SDL_Event event; + SDL_bool posted = SDL_FALSE; + SDL_PenStatusInfo *last = &pen->last; + int mouse_button = button + 1; /* For mouse emulation, PEN_DOWN counts as button 1, so the first actual button is mouse button 2 */ + SDL_Window *window; + + if (!pen) { + return SDL_FALSE; + } + window = pen->header.window; + + if ((state == SDL_PRESSED) && !(window && SDL_MousePositionInWindow(window, mouse->mouseID, last->x, last->y))) { + return SDL_FALSE; + } + + if (state == SDL_PRESSED) { + event.pbutton.type = SDL_EVENT_PEN_BUTTON_DOWN; + pen->last.buttons |= (1 << (button - 1)); + } else { + event.pbutton.type = SDL_EVENT_PEN_BUTTON_UP; + pen->last.buttons &= ~(1 << (button - 1)); + } + + if (SDL_EventEnabled(event.pbutton.type)) { + event_setup(pen, window, timestamp, &pen->last, &event); + + event.pbutton.button = button; + event.pbutton.state = state == SDL_PRESSED ? SDL_PRESSED : SDL_RELEASED; + + posted = SDL_PushEvent(&event) > 0; + + if (!posted) { + return SDL_FALSE; + } + } + + /* Mouse emulation */ + if (pen_delay_mouse_button_mode) { + /* Can only change active mouse button while not touching the surface */ + if (!(pen->header.flags & SDL_PEN_DOWN_MASK)) { + if (state == SDL_RELEASED) { + pen->last_mouse_button = 0; + } else { + pen->last_mouse_button = mouse_button; + } + } + /* Defer emulation event */ + return SDL_TRUE; + } + + switch (pen_mouse_emulation_mode) { + case PEN_MOUSE_EMULATE: + return (SDL_SendMouseButton(timestamp, window, SDL_PEN_MOUSEID, state, mouse_button)) || posted; + + case PEN_MOUSE_STATELESS: + /* Report mouse event without updating mouse state */ + event.button.type = state == SDL_PRESSED ? SDL_EVENT_MOUSE_BUTTON_DOWN : SDL_EVENT_MOUSE_BUTTON_UP; + if (SDL_EventEnabled(event.button.type)) { + event.button.windowID = window ? window->id : 0; + event.button.timestamp = timestamp; + event.button.which = SDL_PEN_MOUSEID; + + event.button.state = state; + event.button.button = mouse_button; + event.button.clicks = 1; + event.button.x = last->x; + event.button.y = last->y; + return (SDL_PushEvent(&event) > 0) || posted; + } + break; + + default: + break; + } + return posted; +} + +int SDL_SendPenWindowEvent(Uint64 timestamp, SDL_PenID instance_id, SDL_Window *window) +{ + SDL_EventType event_id = window ? SDL_EVENT_WINDOW_PEN_ENTER : SDL_EVENT_WINDOW_PEN_LEAVE; + SDL_Event event = { 0 }; + SDL_Pen *pen = SDL_GetPenPtr(instance_id); + SDL_bool posted; + + if (!pen) { + return SDL_FALSE; + } + + if (pen->header.window == window) { /* (TRIVIAL-EVENT) Nothing new to report */ + return SDL_FALSE; + } + + if (timestamp == 0) { + /* Generate timestamp ourselves, if needed, so that we report the same timestamp + for the pen event and for any emulated mouse event */ + timestamp = SDL_GetTicksNS(); + } + + event.window.type = event_id; + /* If window == NULL and not (TRIVIAL-EVENT), then pen->header.window != NULL */ + event.window.timestamp = timestamp; + event.window.windowID = window ? window->id : pen->header.window->id; + event.window.data1 = instance_id; + posted = (SDL_PushEvent(&event) > 0); + + /* Update after assembling event */ + pen->header.window = window; + + switch (pen_mouse_emulation_mode) { + case PEN_MOUSE_EMULATE: + SDL_SetMouseFocus(event_id == SDL_EVENT_WINDOW_PEN_ENTER ? window : NULL); + break; + + case PEN_MOUSE_STATELESS: + /* Send event without going through mouse API */ + if (event_id == SDL_EVENT_WINDOW_PEN_ENTER) { + event.window.type = SDL_EVENT_WINDOW_MOUSE_ENTER; + } else { + event.window.type = SDL_EVENT_WINDOW_MOUSE_LEAVE; + } + posted = posted || (SDL_PushEvent(&event) > 0); + break; + + default: + break; + } + + return posted; +} + +static void SDLCALL SDL_PenUpdateHint(void *userdata, const char *name, const char *oldvalue, const char *newvalue) +{ + int *var = userdata; + if (newvalue == NULL) { + return; + } + + if (0 == SDL_strcmp("2", newvalue)) { + *var = 2; + } else if (0 == SDL_strcmp("1", newvalue)) { + *var = 1; + } else if (0 == SDL_strcmp("0", newvalue)) { + *var = 0; + } else { + SDL_Log("Unexpected value for pen hint: '%s'", newvalue); + } +} + +void SDL_PenInit(void) +{ +#if (SDL_PEN_DEBUG_NOID | SDL_PEN_DEBUG_NONWACOM | SDL_PEN_DEBUG_UNKNOWN_WACOM) + printf("[pen] Debugging enabled: noid=%d nonwacom=%d unknown-wacom=%d\n", + SDL_PEN_DEBUG_NOID, SDL_PEN_DEBUG_NONWACOM, SDL_PEN_DEBUG_UNKNOWN_WACOM); +#endif + SDL_AddHintCallback(SDL_HINT_PEN_NOT_MOUSE, + SDL_PenUpdateHint, &pen_mouse_emulation_mode); + + SDL_AddHintCallback(SDL_HINT_PEN_DELAY_MOUSE_BUTTON, + SDL_PenUpdateHint, &pen_delay_mouse_button_mode); +#ifndef SDL_THREADS_DISABLED + SDL_pen_access_lock = SDL_CreateMutex(); +#endif +} + +SDL_bool SDL_PenPerformHitTest(void) +{ + return pen_mouse_emulation_mode == PEN_MOUSE_EMULATE; +} + +/* Vendor-specific bits */ + +/* Default pen names */ +#define PEN_NAME_AES 0 +#define PEN_NAME_ART 1 +#define PEN_NAME_AIRBRUSH 2 +#define PEN_NAME_GENERAL 3 +#define PEN_NAME_GRIP 4 +#define PEN_NAME_INKING 5 +#define PEN_NAME_PRO 6 +#define PEN_NAME_PRO2 7 +#define PEN_NAME_PRO3 8 +#define PEN_NAME_PRO3D 9 +#define PEN_NAME_PRO_SLIM 10 +#define PEN_NAME_STROKE 11 + +#define PEN_NAME_LAST PEN_NAME_STROKE +#define PEN_NUM_NAMES (PEN_NAME_LAST + 1) + +const static char *default_pen_names[PEN_NUM_NAMES] = { + /* PEN_NAME_AES */ + "AES Pen", + /* PEN_NAME_ART */ + "Art Pen", + /* PEN_NAME_AIRBRUSH */ + "Airbrush Pen", + /* PEN_NAME_GENERAL */ + "Pen", + /* PEN_NAME_GRIP */ + "Grip Pen", + /* PEN_NAME_INKING */ + "Inking Pen", + /* PEN_NAME_PRO */ + "Pro Pen", + /* PEN_NAME_PRO2 */ + "Pro Pen 2", + /* PEN_NAME_PRO3 */ + "Pro Pen 3", + /* PEN_NAME_PRO3D */ + "Pro Pen 3D", + /* PEN_NAME_PRO_SLIM */ + "Pro Pen Slim", + /* PEN_NAME_STROKE */ + "Stroke Pen" +}; + +#define PEN_SPEC_TYPE_SHIFT 0 +#define PEN_SPEC_TYPE_MASK 0x0000000fu +#define PEN_SPEC_BUTTONS_SHIFT 4 +#define PEN_SPEC_BUTTONS_MASK 0x000000f0u +#define PEN_SPEC_NAME_SHIFT 8 +#define PEN_SPEC_NAME_MASK 0x00000f00u +#define PEN_SPEC_AXES_SHIFT 0 +#define PEN_SPEC_AXES_MASK 0xffff0000u + +#define PEN_WACOM_ID_INVALID 0xffffffffu /* Generic "invalid ID" marker */ + +#define PEN_SPEC(name, buttons, type, axes) (0 | (PEN_SPEC_NAME_MASK & ((name) << PEN_SPEC_NAME_SHIFT)) | (PEN_SPEC_BUTTONS_MASK & ((buttons) << PEN_SPEC_BUTTONS_SHIFT)) | (PEN_SPEC_TYPE_MASK & ((type) << PEN_SPEC_TYPE_SHIFT)) | (PEN_SPEC_AXES_MASK & ((axes) << PEN_SPEC_AXES_SHIFT))) + +/* Returns a suitable pen name string from default_pen_names on success, otherwise NULL. */ +static const char *pen_wacom_identify_tool(Uint32 requested_wacom_id, int *num_buttons, int *tool_type, int *axes) +{ + int i; + + /* List of known Wacom pens, extracted from libwacom.stylus and wacom_wac.c in the Linux kernel. + Could be complemented by dlopen()ing libwacom, in the future (if new pens get added). */ + struct + { + /* Compress properties to 8 bytes per device in order to keep memory cost well below 1k. + Could be compressed further with more complex code. */ + Uint32 wacom_id; /* Must be != PEN_WACOM_ID_INVALID */ + Uint32 properties; + } tools[] = { + { 0x0001, PEN_SPEC(PEN_NAME_AES, 1, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK) }, + { 0x0011, PEN_SPEC(PEN_NAME_AES, 1, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK) }, + { 0x0019, PEN_SPEC(PEN_NAME_AES, 1, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK) }, + { 0x0021, PEN_SPEC(PEN_NAME_AES, 1, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK) }, + { 0x0031, PEN_SPEC(PEN_NAME_AES, 1, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK) }, + { 0x0039, PEN_SPEC(PEN_NAME_AES, 1, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK) }, + { 0x0049, PEN_SPEC(PEN_NAME_GENERAL, 1, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK) }, + { 0x0071, PEN_SPEC(PEN_NAME_GENERAL, 1, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK) }, + { 0x0200, PEN_SPEC(PEN_NAME_PRO3, 3, SDL_PEN_TYPE_PEN, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x0221, PEN_SPEC(PEN_NAME_AES, 1, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK) }, + { 0x0231, PEN_SPEC(PEN_NAME_AES, 1, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK) }, + { 0x0271, PEN_SPEC(PEN_NAME_GENERAL, 1, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK) }, + { 0x0421, PEN_SPEC(PEN_NAME_AES, 1, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK) }, + { 0x0431, PEN_SPEC(PEN_NAME_AES, 1, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK) }, + { 0x0621, PEN_SPEC(PEN_NAME_AES, 1, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK) }, + { 0x0631, PEN_SPEC(PEN_NAME_AES, 1, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK) }, + { 0x0801, PEN_SPEC(PEN_NAME_INKING, 0, SDL_PEN_TYPE_PENCIL, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x0802, PEN_SPEC(PEN_NAME_GRIP, 2, SDL_PEN_TYPE_PEN, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x0804, PEN_SPEC(PEN_NAME_ART, 2, SDL_PEN_TYPE_PEN, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK | SDL_PEN_AXIS_ROTATION_MASK) }, + { 0x080a, PEN_SPEC(PEN_NAME_GRIP, 2, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x080c, PEN_SPEC(PEN_NAME_ART, 2, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x0812, PEN_SPEC(PEN_NAME_INKING, 0, SDL_PEN_TYPE_PENCIL, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x0813, PEN_SPEC(PEN_NAME_GENERAL, 2, SDL_PEN_TYPE_PEN, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x081b, PEN_SPEC(PEN_NAME_GENERAL, 2, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x0822, PEN_SPEC(PEN_NAME_GENERAL, 2, SDL_PEN_TYPE_PEN, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x0823, PEN_SPEC(PEN_NAME_GRIP, 2, SDL_PEN_TYPE_PEN, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x082a, PEN_SPEC(PEN_NAME_GENERAL, 2, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x082b, PEN_SPEC(PEN_NAME_GRIP, 2, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x0832, PEN_SPEC(PEN_NAME_STROKE, 0, SDL_PEN_TYPE_BRUSH, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x0842, PEN_SPEC(PEN_NAME_PRO2, 2, SDL_PEN_TYPE_PEN, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x084a, PEN_SPEC(PEN_NAME_PRO2, 2, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x0852, PEN_SPEC(PEN_NAME_GRIP, 2, SDL_PEN_TYPE_PEN, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x085a, PEN_SPEC(PEN_NAME_GRIP, 2, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x0862, PEN_SPEC(PEN_NAME_GENERAL, 2, SDL_PEN_TYPE_PEN, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x0885, PEN_SPEC(PEN_NAME_ART, 0, SDL_PEN_TYPE_PEN, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK | SDL_PEN_AXIS_ROTATION_MASK) }, + { 0x08e2, PEN_SPEC(PEN_NAME_GENERAL, 2, SDL_PEN_TYPE_PEN, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x0902, PEN_SPEC(PEN_NAME_AIRBRUSH, 1, SDL_PEN_TYPE_AIRBRUSH, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK | SDL_PEN_AXIS_SLIDER_MASK) }, + { 0x090a, PEN_SPEC(PEN_NAME_AIRBRUSH, 1, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x0912, PEN_SPEC(PEN_NAME_AIRBRUSH, 1, SDL_PEN_TYPE_AIRBRUSH, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK | SDL_PEN_AXIS_SLIDER_MASK) }, + { 0x0913, PEN_SPEC(PEN_NAME_AIRBRUSH, 1, SDL_PEN_TYPE_AIRBRUSH, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x091a, PEN_SPEC(PEN_NAME_AIRBRUSH, 1, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x091b, PEN_SPEC(PEN_NAME_AIRBRUSH, 1, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x0d12, PEN_SPEC(PEN_NAME_AIRBRUSH, 1, SDL_PEN_TYPE_AIRBRUSH, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK | SDL_PEN_AXIS_SLIDER_MASK) }, + { 0x0d1a, PEN_SPEC(PEN_NAME_AIRBRUSH, 1, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x8051, PEN_SPEC(PEN_NAME_AES, 0, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK) }, + { 0x805b, PEN_SPEC(PEN_NAME_AES, 1, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK) }, + { 0x806b, PEN_SPEC(PEN_NAME_AES, 1, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK) }, + { 0x807b, PEN_SPEC(PEN_NAME_GENERAL, 1, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK) }, + { 0x826b, PEN_SPEC(PEN_NAME_AES, 1, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK) }, + { 0x846b, PEN_SPEC(PEN_NAME_AES, 1, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK) }, + { 0x2802, PEN_SPEC(PEN_NAME_INKING, 0, SDL_PEN_TYPE_PENCIL, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x4200, PEN_SPEC(PEN_NAME_PRO3, 3, SDL_PEN_TYPE_PEN, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x4802, PEN_SPEC(PEN_NAME_GENERAL, 2, SDL_PEN_TYPE_PEN, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x480a, PEN_SPEC(PEN_NAME_GENERAL, 2, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x8842, PEN_SPEC(PEN_NAME_PRO3D, 3, SDL_PEN_TYPE_PEN, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x10802, PEN_SPEC(PEN_NAME_GRIP, 2, SDL_PEN_TYPE_PEN, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x10804, PEN_SPEC(PEN_NAME_ART, 2, SDL_PEN_TYPE_PEN, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK | SDL_PEN_AXIS_ROTATION_MASK) }, + { 0x1080a, PEN_SPEC(PEN_NAME_GRIP, 2, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x1080c, PEN_SPEC(PEN_NAME_ART, 2, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x10842, PEN_SPEC(PEN_NAME_PRO_SLIM, 2, SDL_PEN_TYPE_PEN, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x1084a, PEN_SPEC(PEN_NAME_PRO_SLIM, 2, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x10902, PEN_SPEC(PEN_NAME_AIRBRUSH, 1, SDL_PEN_TYPE_AIRBRUSH, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK | SDL_PEN_AXIS_SLIDER_MASK) }, + { 0x1090a, PEN_SPEC(PEN_NAME_AIRBRUSH, 1, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x12802, PEN_SPEC(PEN_NAME_INKING, 0, SDL_PEN_TYPE_PENCIL, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x14802, PEN_SPEC(PEN_NAME_GENERAL, 2, SDL_PEN_TYPE_PEN, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x1480a, PEN_SPEC(PEN_NAME_GENERAL, 2, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x16802, PEN_SPEC(PEN_NAME_PRO, 2, SDL_PEN_TYPE_PEN, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x1680a, PEN_SPEC(PEN_NAME_PRO, 2, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x18802, PEN_SPEC(PEN_NAME_GENERAL, 2, SDL_PEN_TYPE_PEN, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0x1880a, PEN_SPEC(PEN_NAME_GENERAL, 2, SDL_PEN_TYPE_ERASER, SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK) }, + { 0, 0 } + }; + + /* The list of pens is sorted, so we could do binary search, but this call should be pretty rare. */ + for (i = 0; tools[i].wacom_id; ++i) { + if (tools[i].wacom_id == requested_wacom_id) { + Uint32 properties = tools[i].properties; + int name_index = (properties & PEN_SPEC_NAME_MASK) >> PEN_SPEC_NAME_SHIFT; + + *num_buttons = (properties & PEN_SPEC_BUTTONS_MASK) >> PEN_SPEC_BUTTONS_SHIFT; + *tool_type = (properties & PEN_SPEC_TYPE_MASK) >> PEN_SPEC_TYPE_SHIFT; + *axes = (properties & PEN_SPEC_AXES_MASK) >> PEN_SPEC_AXES_SHIFT; + + return default_pen_names[name_index]; + } + } + return NULL; +} + +void SDL_PenUpdateGUIDForGeneric(SDL_GUID *guid, Uint32 upper, Uint32 lower) +{ + int i; + + for (i = 0; i < 4; ++i) { + guid->data[8 + i] = (lower >> (i * 8)) & 0xff; + } + + for (i = 0; i < 4; ++i) { + guid->data[12 + i] = (upper >> (i * 8)) & 0xff; + } +} + +void SDL_PenUpdateGUIDForType(SDL_GUID *guid, SDL_PenSubtype pentype) +{ + guid->data[7] = pentype; +} + +void SDL_PenUpdateGUIDForWacom(SDL_GUID *guid, Uint32 wacom_devicetype_id, Uint32 wacom_serial_id) +{ + int i; + + for (i = 0; i < 4; ++i) { + guid->data[0 + i] = (wacom_serial_id >> (i * 8)) & 0xff; + } + + for (i = 0; i < 3; ++i) { /* 24 bit values */ + guid->data[4 + i] = (wacom_devicetype_id >> (i * 8)) & 0xff; + } +} + +int SDL_PenModifyForWacomID(SDL_Pen *pen, Uint32 wacom_devicetype_id, Uint32 *axis_flags) +{ + const char *name = NULL; + int num_buttons; + int tool_type; + int axes; + +#if SDL_PEN_DEBUG_UNKNOWN_WACOM + wacom_devicetype_id = PEN_WACOM_ID_INVALID; /* force detection to fail */ +#endif + +#if defined(__LINUX__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) + /* According to Ping Cheng, the curent Wacom for Linux maintainer, device IDs on Linux + squeeze a "0" nibble after the 3rd (least significant) nibble. + This may also affect the *BSDs, so they are heuristically included here. + On those platforms, we first try the "patched" ID: */ + if (0 == (wacom_devicetype_id & 0x0000f000u)) { + const Uint32 lower_mask = 0xfffu; + int wacom_devicetype_alt_id = ((wacom_devicetype_id & ~lower_mask) >> 4) | (wacom_devicetype_id & lower_mask); + + name = pen_wacom_identify_tool(wacom_devicetype_alt_id, &num_buttons, &tool_type, &axes); + if (name) { + wacom_devicetype_id = wacom_devicetype_alt_id; + } + } +#endif + if (name == NULL) { + name = pen_wacom_identify_tool(wacom_devicetype_id, &num_buttons, &tool_type, &axes); + } + + if (!name) { + return SDL_FALSE; + } + + *axis_flags = axes; + + /* Override defaults */ + if (pen->info.num_buttons == SDL_PEN_INFO_UNKNOWN) { + pen->info.num_buttons = num_buttons; + } + if (pen->type == SDL_PEN_TYPE_PEN) { + pen->type = (SDL_PenSubtype)tool_type; + } + if (pen->info.max_tilt == SDL_PEN_INFO_UNKNOWN) { + /* supposedly: 64 degrees left, 63 right, as reported by the Wacom X11 driver */ + pen->info.max_tilt = 64.0f; + } + pen->info.wacom_id = wacom_devicetype_id; + if (0 == pen->name[0]) { + SDL_snprintf(pen->name, SDL_PEN_MAX_NAME, + "Wacom %s%s", name, (tool_type == SDL_PEN_TYPE_ERASER) ? " Eraser" : ""); + } + return SDL_TRUE; +} diff --git a/src/events/SDL_pen_c.h b/src/events/SDL_pen_c.h new file mode 100644 index 0000000000..0488d5d92f --- /dev/null +++ b/src/events/SDL_pen_c.h @@ -0,0 +1,336 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2023 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#include "../SDL_internal.h" + +#ifndef SDL_pen_c_h_ +#define SDL_pen_c_h_ + +#include "../../include/SDL3/SDL_pen.h" +#include "SDL_mouse_c.h" + +/* For testing alternate code paths: */ +#define SDL_PEN_DEBUG_NOID 0 /* Pretend that pen device does not supply ID / ID is some default value \ + affects: SDL_x11pen.c \ + SDL_waylandevents.c */ +#define SDL_PEN_DEBUG_NONWACOM 0 /* Pretend that no attached device is a Wacom device \ + affects: SDL_x11pen.c \ + SDL_waylandevents.c */ +#define SDL_PEN_DEBUG_UNKNOWN_WACOM 0 /* Pretend that any attached Wacom device is of an unknown make \ + affects: SDL_PenModifyFromWacomID() */ +#define SDL_PEN_DEBUG_NOSERIAL_WACOM 0 /* Pretend that any attached Wacom device has serial number 0 \ + affects: SDL_x11pen.c \ + SDL_waylandevents.c */ + +#define SDL_PEN_TYPE_NONE 0 /**< Pen type for non-pens (use to cancel pen registration) */ + +#define SDL_PEN_MAX_NAME 64 + +#define SDL_PEN_FLAG_ERROR (1ul << 28) /* Printed an internal API usage error about this pen (used to prevent spamming) */ +#define SDL_PEN_FLAG_NEW (1ul << 29) /* Pen was registered in most recent call to SDL_PenRegisterBegin() */ +#define SDL_PEN_FLAG_DETACHED (1ul << 30) /* Detached (not re-registered before last SDL_PenGCSweep()) */ +#define SDL_PEN_FLAG_STALE (1ul << 31) /* Not re-registered since last SDL_PenGCMark() */ + +typedef struct SDL_PenStatusInfo +{ + float x, y; + float axes[SDL_PEN_NUM_AXES]; + Uint32 buttons; /* SDL_BUTTON(1) | SDL_BUTTON(2) | ... | SDL_PEN_DOWN_MASK */ +} SDL_PenStatusInfo; + +/** + * Internal (backend driver-independent) pen representation + * + * Implementation-specific backend drivers may read and write most of this structure, and + * are expected to initialise parts of it when registering a new pen. They must not write + * to the "header" section. + */ +typedef struct SDL_Pen +{ + /* Backend driver MUST NOT not write to: */ + struct SDL_Pen_header + { + SDL_PenID id; /* id determines sort order unless SDL_PEN_FLAG_DETACHED is set */ + Uint32 flags; /* SDL_PEN_FLAG_* | SDK_PEN_DOWN_MASK | SDL_PEN_INK_MASK | SDL_PEN_ERASER_MASK | SDL_PEN_AXIS_* */ + SDL_Window *window; /* Current SDL window for this pen, or NULL */ + } header; + + SDL_PenStatusInfo last; /* Last reported status, normally read-only for backend */ + + /* Backend: MUST initialise this block when pen is first registered: */ + SDL_GUID guid; /* GUID, MUST be set by backend. + MUST be unique (no other pen ID with same GUID). + SHOULD be persistent across sessions. */ + + /* Backend: SHOULD initialise this block when pen is first registered if it can + Otherwise: Set to sane default values during SDL_PenModifyEnd() */ + SDL_PenCapabilityInfo info; /* Detail information about the pen (buttons, tilt) */ + SDL_PenSubtype type; + Uint8 last_mouse_button; /* For mouse button emulation: last emulated button */ + char *name; /* Preallocated; set via SDL_strlcpy(pen->name, src, SDL_PEN_MAX_NAME) */ + /* We hand this exact pointer to client code, so it must not be modified after + creation. */ + + void *deviceinfo; /* implementation-specific information */ +} SDL_Pen; + +/* ---- API for backend driver only ---- */ + +/** + * (Only for backend driver) Look up a pen by pen ID + * + * \param instance_id A Uint32 pen identifier (driver-dependent meaning). Must not be 0 = SDL_PEN_INVALID. + * The same ID is exposed to clients as SDL_PenID. + * + * The pen pointer is only valid until the next call to SDL_PenModifyEnd() or SDL_PenGCSweep() + * + * \return pen, if it exists, or NULL + */ +extern SDL_Pen *SDL_GetPenPtr(Uint32 instance_id); + +/** + * (Only for backend driver) Start registering a new pen or updating an existing pen. + * + * Acquires the pen mutex, which is held until the next call to SDL_PenModifyEnd() . + * + * If the PenID already exists, returns the existing entry. Otherwise initialise fresh SDL_Pen. + * For new pens, sets SDL_PEN_FLAG_NEW. + * + * Usage: + * - SDL_PenModifyStart() + * - update pen object, in any order: + * - SDL_PenModifyAddCapabilities() + * - pen->guid (MUST be set for new pens, e.g. via ::SDL_PenUpdateGUIDForGeneric and related operations) + * - pen->info.num_buttons + * - pen->info.max_tilt + * - pen->type + * - pen->name + * - pen->deviceinfo (backend-specific) + * - SDL_PenModifyEnd() + * + * For new pens, sets defaults for: + * - num_buttons (SDL_PEN_INFO_UNKNOWN) + * - max_tilt (SDL_PEN_INFO_UNKNOWN) + * - pen_type (SDL_PEN_TYPE_PEN) + * - Zeroes all other (non-header) fields + * + * \param instance_id Pen ID to allocate (must not be 0 = SDL_PEN_ID_INVALID) + * \returns SDL_Pen pointer; only valid until the call to SDL_PenModifyEnd() + */ +extern SDL_Pen *SDL_PenModifyBegin(Uint32 instance_id); + +/** + * (Only for backend driver) Add capabilities to a pen (cf. SDL_PenModifyBegin()). + * + * Adds capabilities to a pen obtained via SDL_PenModifyBegin(). Can be called more than once. + * + * \param pen The pen to update + * \param capabilities Capabilities flags, out of: SDL_PEN_AXIS_*, SDL_PEN_ERASER_MASK, SDL_PEN_INK_MASK + * Setting SDL_PEN_ERASER_MASK will clear SDL_PEN_INK_MASK, and vice versa. + */ +extern void SDL_PenModifyAddCapabilities(SDL_Pen *pen, Uint32 capabilities); + +/** + * Set up a pen structure for a Wacom device. + * + * Some backends (e.g., XInput2, Wayland) can only partially identify the capabilities of a given + * pen but can identify Wacom pens and obtain their Wacom-specific device type identifiers. + * This function partly automates device setup in those cases. + * + * This function does NOT set up the pen's GUID. Use ::SD_PenModifyGUIDForWacom instead. + * + * This function does NOT call SDL_PenModifyAddCapabilities() ifself, since some backends may + * not have access to all pen axes (e.g., Xinput2). + * + * \param pen The pen to initialise + * \param wacom_devicetype_id The Wacom-specific device type identifier + * \param[out] axis_flags The set of physically supported axes for this pen, suitable for passing to + * SDL_PenModifyAddCapabilities() + * + * \returns SDL_TRUE if the device ID could be identified, otherwise SDL_FALSE + */ +extern int SDL_PenModifyForWacomID(SDL_Pen *pen, Uint32 wacom_devicetype_id, Uint32 *axis_flags); + +/** + * Updates a GUID for a generic pen device. + * + * Assumes that the GUID has been pre-initialised (typically to 0). + * Idempotent, and commutative with ::SDL_PenUpdateGUIDForWacom and ::SDL_PenUpdateGUIDForType + * + * \param[out] guid The GUID to update + * \param upper Upper half of the device ID (assume lower entropy than "lower"; pass 0 if not available) + * \param lower Lower half of the device ID (assume higher entropy than "upper") + */ +extern void SDL_PenUpdateGUIDForGeneric(SDL_GUID *guid, Uint32 upper, Uint32 lower); + +/** + * Updates a GUID based on a pen type + * + * Assumes that the GUID has been pre-initialised (typically to 0). + * Idempotent, and commutative with ::SDL_PenUpdateGUIDForWacom and ::SDL_PenUpdateGUIDForGeneric + * + * \param[out] guid The GUID to update + * \param pentype The pen type to insert + */ +extern void SDL_PenUpdateGUIDForType(SDL_GUID *guid, SDL_PenSubtype pentype); + +/** + * Updates a GUID for a Wacom pen device. + * + * Assumes that the GUID has been pre-initialised (typically to 0). + * Idempotent, and commutative with ::SDL_PenUpdateGUIDForType and ::SDL_PenUpdateGUIDForGeneric + * + * This update is identical to the one written by ::SDL_PenModifyFromWacomID . + * + * \param[out] guid The GUID to update + * \param wacom_devicetype_id The Wacom-specific device type identifier + * \param wacom_serial_id The Wacom-specific serial number + */ +extern void SDL_PenUpdateGUIDForWacom(SDL_GUID *guid, Uint32 wacom_devicetype_id, Uint32 wacom_serial_id); + +/** + * (Only for backend driver) Finish updating a pen. + * + * Releases the pen mutex acquired by SDL_PenModifyBegin() . + * + * If pen->type == SDL_PEN_TYPE_NONE, removes the pen entirely (only + * for new pens). This allows backends to start registering a + * potential pen device and to abort if the device turns out to not be + * a pen. + * + * For new pens, this call will also set the following: + * - name (default name, if not yet set) + * + * \param pen The pen to register. That pointer is no longer valid after this call. + * \param attach Whether the pen should be attached (SDL_TRUE) or detached (SDL_FALSE). + * + * If the pen is detached or removed, it is the caller's responsibility to free + * and null "deviceinfo". + */ +extern void SDL_PenModifyEnd(SDL_Pen *pen, SDL_bool attach); + +/** + * (Only for backend driver) Mark all current pens for garbage collection. + * + * Must not be called while the pen mutex is held (by SDL_PenModifyBegin() ). + * + * SDL_PenGCMark() / SDL_PenGCSweep() provide a simple mechanism for + * detaching all known pens that are not discoverable. This allows + * backends to use the same code for pen discovery and for + * hotplugging: + * + * - SDL_PenGCMark() and start backend-specific discovery + * - for each discovered pen: SDL_PenModifyBegin() + SDL_PenModifyEnd() (this will retain existing state) + * - SDL_PenGCSweep() (will now detach all pens that were not re-registered). + */ +extern void SDL_PenGCMark(void); + +/** + * (Only for backend driver) Detach pens that haven't been reported attached since the last call to SDL_PenGCMark(). + * + * Must not be called while the pen mutex is held (by SDL_PenModifyBegin() ). + * + * See SDL_PenGCMark() for details. + * + * \param context Extra parameter to pass through to "free_deviceinfo" + * \param free_deviceinfo Operation to call on any non-NULL "backend.deviceinfo". + * + * \sa SDL_PenGCMark() + */ +extern void SDL_PenGCSweep(void *context, void (*free_deviceinfo)(Uint32 instance_id, void *deviceinfo, void *context)); + +/** + * (Only for backend driver) Send a pen motion event. + * + * Suppresses pen motion events that do not change the current pen state. + * May also send a mouse motion event, if mouse emulation is enabled and the pen position has + * changed sufficiently for the motion to be visible to mouse event listeners. + * + * \param timestamp Event timestamp in nanoseconds, or 0 to ask SDL to use SDL_GetTicksNS() . + * While 0 is safe to report, your backends may be able to report more precise + * timing information. + * Keep in mind that you should never report timestamps that are greater than + * SDL_GetTicksNS() . In particular, SDL_GetTicksNS() reports nanoseconds since the start + * of the SDL session, and your backend may use a different starting point as "timestamp zero". + * \param instance_id Pen + * \param window_relative Coordinates are already window-relative + * \param status Coordinates and axes (buttons are ignored) + * + * \returns SDL_TRUE if at least one event was sent + */ +extern int SDL_SendPenMotion(Uint64 timestamp, SDL_PenID instance_id, SDL_bool window_relative, const SDL_PenStatusInfo *status); + +/** + * (Only for backend driver) Send a pen button event + * + * \param timestamp Event timestamp in nanoseconds, or 0 to ask SDL to use SDL_GetTicksNS() . + * See SDL_SendPenMotion() for a discussion about how to handle timestamps. + * \param instance_id Pen + * \param state SDL_PRESSED or SDL_RELEASED + * \param button Button number: 1 (first physical button) etc. + * + * \returns SDL_TRUE if at least one event was sent + */ +extern int SDL_SendPenButton(Uint64 timestamp, SDL_PenID instance_id, Uint8 state, Uint8 button); + +/** + * (Only for backend driver) Send a pen tip event (touching or no longer touching the surface) + * + * Note: the backend should perform hit testing on window decoration elements to allow the pen + * to e.g. resize/move the window, just as for mouse events, unless ::SDL_SendPenTipEvent is false. + * + * \param timestamp Event timestamp in nanoseconds, or 0 to ask SDL to use SDL_GetTicksNS() . + * See SDL_SendPenMotion() for a discussion about how to handle timestamps. + * \param instance_id Pen + * \param state SDL_PRESSED (for PEN_DOWN) or SDL_RELEASED (for PEN_UP) + * + * \returns SDL_TRUE if at least one event was sent + */ +extern int SDL_SendPenTipEvent(Uint64 timestamp, SDL_PenID instance_id, Uint8 state); + +/** + * (Only for backend driver) Check if a PEN_DOWN event should perform hit box testing. + * + * \returns SDL_TRUE if and only if the backend should perform hit testing + */ +extern SDL_bool SDL_PenPerformHitTest(void); + +/** + * (Only for backend driver) Send a pen window event. + * + * Tracks when a pen has entered/left a window. + * Don't call this when reporting new pens or removing known pens; those cases are handled automatically. + * + * \param timestamp Event timestamp in nanoseconds, or 0 to ask SDL to use SDL_GetTicksNS() . + * See SDL_SendPenMotion() for a discussion about how to handle timestamps. + * \param instance_id Pen + * \param window Window to enter, or NULL to exit + */ +extern int SDL_SendPenWindowEvent(Uint64 timestamp, SDL_PenID instance_id, SDL_Window *window); + +/** + * Initialises the pen subsystem. + */ +extern void SDL_PenInit(void); + +#endif /* SDL_pen_c_h_ */ + +/* vi: set ts=4 sw=4 expandtab: */ diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c index 7d3fab4efe..1a954c59cc 100644 --- a/src/video/wayland/SDL_waylandevents.c +++ b/src/video/wayland/SDL_waylandevents.c @@ -572,13 +572,15 @@ static void pointer_handle_leave(void *data, struct wl_pointer *pointer, } } -static SDL_bool ProcessHitTest(struct SDL_WaylandInput *input, uint32_t serial) +static SDL_bool ProcessHitTest(SDL_WindowData *window_data, + struct wl_seat *seat, + wl_fixed_t sx_w, wl_fixed_t sy_w, + uint32_t serial) { - SDL_WindowData *window_data = input->pointer_focus; SDL_Window *window = window_data->sdlwindow; if (window->hit_test) { - const SDL_Point point = { wl_fixed_to_int(input->sx_w), wl_fixed_to_int(input->sy_w) }; + const SDL_Point point = { wl_fixed_to_int(sx_w), wl_fixed_to_int(sy_w) }; const SDL_HitTestResult rc = window->hit_test(window, &point, window->hit_test_data); static const uint32_t directions[] = { @@ -602,14 +604,14 @@ static SDL_bool ProcessHitTest(struct SDL_WaylandInput *input, uint32_t serial) #ifdef HAVE_LIBDECOR_H if (window_data->shell_surface_type == WAYLAND_SURFACE_LIBDECOR) { if (window_data->shell_surface.libdecor.frame) { - libdecor_frame_move(window_data->shell_surface.libdecor.frame, input->seat, serial); + libdecor_frame_move(window_data->shell_surface.libdecor.frame, seat, serial); } } else #endif if (window_data->shell_surface_type == WAYLAND_SURFACE_XDG_TOPLEVEL) { if (window_data->shell_surface.xdg.roleobj.toplevel) { xdg_toplevel_move(window_data->shell_surface.xdg.roleobj.toplevel, - input->seat, + seat, serial); } } @@ -626,14 +628,14 @@ static SDL_bool ProcessHitTest(struct SDL_WaylandInput *input, uint32_t serial) #ifdef HAVE_LIBDECOR_H if (window_data->shell_surface_type == WAYLAND_SURFACE_LIBDECOR) { if (window_data->shell_surface.libdecor.frame) { - libdecor_frame_resize(window_data->shell_surface.libdecor.frame, input->seat, serial, directions_libdecor[rc - SDL_HITTEST_RESIZE_TOPLEFT]); + libdecor_frame_resize(window_data->shell_surface.libdecor.frame, seat, serial, directions_libdecor[rc - SDL_HITTEST_RESIZE_TOPLEFT]); } } else #endif if (window_data->shell_surface_type == WAYLAND_SURFACE_XDG_TOPLEVEL) { if (window_data->shell_surface.xdg.roleobj.toplevel) { xdg_toplevel_resize(window_data->shell_surface.xdg.roleobj.toplevel, - input->seat, + seat, serial, directions[rc - SDL_HITTEST_RESIZE_TOPLEFT]); } @@ -660,7 +662,7 @@ static void pointer_handle_button_common(struct SDL_WaylandInput *input, uint32_ switch (button) { case BTN_LEFT: sdl_button = SDL_BUTTON_LEFT; - if (ProcessHitTest(input, serial)) { + if (ProcessHitTest(input->pointer_focus, input->seat, input->sx_w, input->sy_w, serial)) { return; /* don't pass this event on to app. */ } break; @@ -832,19 +834,19 @@ static void pointer_handle_axis(void *data, struct wl_pointer *pointer, } static void pointer_handle_axis_relative_direction(void *data, struct wl_pointer *pointer, - uint32_t axis, uint32_t axis_relative_direction) + uint32_t axis, uint32_t axis_relative_direction) { struct SDL_WaylandInput *input = data; if (axis != WL_POINTER_AXIS_VERTICAL_SCROLL) { return; } switch (axis_relative_direction) { - case WL_POINTER_AXIS_RELATIVE_DIRECTION_IDENTICAL: - input->pointer_curr_axis_info.direction = SDL_MOUSEWHEEL_NORMAL; - break; - case WL_POINTER_AXIS_RELATIVE_DIRECTION_INVERTED: - input->pointer_curr_axis_info.direction = SDL_MOUSEWHEEL_FLIPPED; - break; + case WL_POINTER_AXIS_RELATIVE_DIRECTION_IDENTICAL: + input->pointer_curr_axis_info.direction = SDL_MOUSEWHEEL_NORMAL; + break; + case WL_POINTER_AXIS_RELATIVE_DIRECTION_INVERTED: + input->pointer_curr_axis_info.direction = SDL_MOUSEWHEEL_FLIPPED; + break; } } @@ -928,11 +930,11 @@ static const struct wl_pointer_listener pointer_listener = { pointer_handle_motion, pointer_handle_button, pointer_handle_axis, - pointer_handle_frame, /* Version 5 */ - pointer_handle_axis_source, /* Version 5 */ - pointer_handle_axis_stop, /* Version 5 */ - pointer_handle_axis_discrete, /* Version 5 */ - pointer_handle_axis_value120, /* Version 8 */ + pointer_handle_frame, /* Version 5 */ + pointer_handle_axis_source, /* Version 5 */ + pointer_handle_axis_stop, /* Version 5 */ + pointer_handle_axis_discrete, /* Version 5 */ + pointer_handle_axis_value120, /* Version 8 */ pointer_handle_axis_relative_direction /* Version 9 */ }; @@ -2361,40 +2363,202 @@ void Wayland_add_text_input_manager(SDL_VideoData *d, uint32_t id, uint32_t vers } } +static SDL_PenID Wayland_get_penid(void *data, struct zwp_tablet_tool_v2 *tool) +{ + struct SDL_WaylandTool *sdltool = data; + return sdltool->penid; +} + +/* For registering pens */ +static SDL_Pen *Wayland_get_current_pen(void *data, struct zwp_tablet_tool_v2 *tool) +{ + struct SDL_WaylandTool *sdltool = data; + struct SDL_WaylandTabletInput *input = sdltool->tablet; + + if (!input->current_pen.builder) { + /* Starting new pen or updating one? */ + SDL_PenID penid = sdltool->penid; + + if (penid == 0) { + /* Found completely new pen? */ + penid = ++input->num_pens; + sdltool->penid = penid; + } + input->current_pen.builder = SDL_GetPenPtr(penid); + if (!input->current_pen.builder) { + /* Must register as new pen */ + input->current_pen.builder = SDL_PenModifyBegin(penid); + } + } + return input->current_pen.builder; +} + static void tablet_tool_handle_type(void *data, struct zwp_tablet_tool_v2 *tool, uint32_t type) { - /* unimplemented */ + SDL_Pen *pen = Wayland_get_current_pen(data, tool); + + switch (type) { + case ZWP_TABLET_TOOL_V2_TYPE_ERASER: + pen->type = SDL_PEN_TYPE_ERASER; + break; + + case ZWP_TABLET_TOOL_V2_TYPE_PEN: + pen->type = SDL_PEN_TYPE_PEN; + break; + + case ZWP_TABLET_TOOL_V2_TYPE_PENCIL: + pen->type = SDL_PEN_TYPE_PENCIL; + break; + + case ZWP_TABLET_TOOL_V2_TYPE_AIRBRUSH: + pen->type = SDL_PEN_TYPE_AIRBRUSH; + break; + + case ZWP_TABLET_TOOL_V2_TYPE_BRUSH: + pen->type = SDL_PEN_TYPE_BRUSH; + break; + + case ZWP_TABLET_TOOL_V2_TYPE_FINGER: + case ZWP_TABLET_TOOL_V2_TYPE_MOUSE: + case ZWP_TABLET_TOOL_V2_TYPE_LENS: + default: + pen->type = SDL_PEN_TYPE_NONE; /* Mark for deregistration */ + } + + SDL_PenUpdateGUIDForType(&pen->guid, pen->type); } static void tablet_tool_handle_hardware_serial(void *data, struct zwp_tablet_tool_v2 *tool, uint32_t serial_hi, uint32_t serial_lo) { - /* unimplemented */ +#if !(SDL_PEN_DEBUG_NOID) + struct SDL_WaylandTool *sdltool = data; + struct SDL_WaylandTabletInput *input = sdltool->tablet; + + if (!input->current_pen.builder_guid_complete) { + SDL_Pen *pen = Wayland_get_current_pen(data, tool); + SDL_PenUpdateGUIDForGeneric(&pen->guid, serial_hi, serial_lo); + if (serial_hi || serial_lo) { + input->current_pen.builder_guid_complete = SDL_TRUE; + } + } +#endif } static void tablet_tool_handle_hardware_id_wacom(void *data, struct zwp_tablet_tool_v2 *tool, uint32_t id_hi, uint32_t id_lo) { - /* unimplemented */ +#if !(SDL_PEN_DEBUG_NOID | SDL_PEN_DEBUG_NONWACOM) + struct SDL_WaylandTool *sdltool = data; + struct SDL_WaylandTabletInput *input = sdltool->tablet; + SDL_Pen *pen = Wayland_get_current_pen(data, tool); + Uint32 axis_flags; + +#if SDL_PEN_DEBUG_NOSERIAL_WACOM /* Check: have we disabled pen serial ID decoding for testing? */ + id_hi = 0; +#endif + + SDL_PenUpdateGUIDForWacom(&pen->guid, id_lo, id_hi); + if (id_hi) { /* Have a serial number? */ + input->current_pen.builder_guid_complete = SDL_TRUE; + } + + if (SDL_PenModifyForWacomID(pen, id_lo, &axis_flags)) { + SDL_PenModifyAddCapabilities(pen, axis_flags); + } +#endif } static void tablet_tool_handle_capability(void *data, struct zwp_tablet_tool_v2 *tool, uint32_t capability) { - /* unimplemented */ + SDL_Pen *pen = Wayland_get_current_pen(data, tool); + + switch (capability) { + case ZWP_TABLET_TOOL_V2_CAPABILITY_TILT: + SDL_PenModifyAddCapabilities(pen, SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK); + break; + + case ZWP_TABLET_TOOL_V2_CAPABILITY_PRESSURE: + SDL_PenModifyAddCapabilities(pen, SDL_PEN_AXIS_PRESSURE_MASK); + break; + + case ZWP_TABLET_TOOL_V2_CAPABILITY_DISTANCE: + SDL_PenModifyAddCapabilities(pen, SDL_PEN_AXIS_DISTANCE_MASK); + break; + + case ZWP_TABLET_TOOL_V2_CAPABILITY_ROTATION: + SDL_PenModifyAddCapabilities(pen, SDL_PEN_AXIS_ROTATION_MASK); + break; + + case ZWP_TABLET_TOOL_V2_CAPABILITY_SLIDER: + SDL_PenModifyAddCapabilities(pen, SDL_PEN_AXIS_SLIDER_MASK); + break; + + case ZWP_TABLET_TOOL_V2_CAPABILITY_WHEEL: + /* Presumably for tools other than pens? */ + break; + + default: + break; + } +} + +static void Wayland_tool_builder_reset(struct SDL_WaylandTabletInput *input) +{ + input->current_pen.builder = NULL; + input->current_pen.builder_guid_complete = SDL_FALSE; } static void tablet_tool_handle_done(void *data, struct zwp_tablet_tool_v2 *tool) { - /* unimplemented */ + SDL_Pen *pen = Wayland_get_current_pen(data, tool); + struct SDL_WaylandTool *sdltool = data; + struct SDL_WaylandTabletInput *input = sdltool->tablet; + + if (!input->current_pen.builder_guid_complete) { + /* No complete GUID? Use tablet and tool device index */ + SDL_PenUpdateGUIDForGeneric(&pen->guid, input->id, sdltool->penid); + } + + SDL_PenModifyEnd(pen, SDL_TRUE); + + Wayland_tool_builder_reset(input); } +static void Wayland_tool_destroy(struct zwp_tablet_tool_v2 *tool) +{ + if (tool) { + struct SDL_WaylandTool *waypen = zwp_tablet_tool_v2_get_user_data(tool); + if (waypen) { + SDL_free(waypen); + } + zwp_tablet_tool_v2_destroy(tool); + } +} + +static void tablet_object_list_remove(struct SDL_WaylandTabletObjectListNode *head, void *object); + static void tablet_tool_handle_removed(void *data, struct zwp_tablet_tool_v2 *tool) { - /* unimplemented */ + struct SDL_WaylandTool *waypen = zwp_tablet_tool_v2_get_user_data(tool); + struct SDL_WaylandTool *sdltool = data; + struct SDL_WaylandTabletInput *input = sdltool->tablet; + SDL_Pen *pen = Wayland_get_current_pen(data, tool); + if (pen) { + SDL_PenModifyEnd(pen, SDL_FALSE); + Wayland_tool_builder_reset(waypen->tablet); + Wayland_tool_destroy(tool); + } else { + zwp_tablet_tool_v2_destroy(tool); + } + + tablet_object_list_remove(input->tools, tool); } static void tablet_tool_handle_proximity_in(void *data, struct zwp_tablet_tool_v2 *tool, uint32_t serial, struct zwp_tablet_v2 *tablet, struct wl_surface *surface) { - struct SDL_WaylandTabletInput *input = data; + struct SDL_WaylandTool *sdltool = data; + struct SDL_WaylandTabletInput *input = sdltool->tablet; SDL_WindowData *window; + SDL_PenID penid = Wayland_get_penid(data, tool); if (!surface) { return; @@ -2410,155 +2574,214 @@ static void tablet_tool_handle_proximity_in(void *data, struct zwp_tablet_tool_v input->tool_focus = window; input->tool_prox_serial = serial; - input->is_down = SDL_FALSE; - - input->btn_stylus = SDL_FALSE; - input->btn_stylus2 = SDL_FALSE; - input->btn_stylus3 = SDL_FALSE; - - SDL_SetMouseFocus(window->sdlwindow); + if (penid) { + SDL_SendPenWindowEvent(0, penid, window->sdlwindow); + } else { + SDL_SetMouseFocus(window->sdlwindow); + } SDL_SetCursor(NULL); } } static void tablet_tool_handle_proximity_out(void *data, struct zwp_tablet_tool_v2 *tool) { - struct SDL_WaylandTabletInput *input = data; - + struct SDL_WaylandTool *sdltool = data; + struct SDL_WaylandTabletInput *input = sdltool->tablet; + SDL_PenID penid = Wayland_get_penid(data, tool); if (input->tool_focus) { - SDL_SetMouseFocus(NULL); + if (penid) { + SDL_SendPenWindowEvent(0, penid, NULL); + } else { + SDL_SetMouseFocus(NULL); + } input->tool_focus = NULL; } } -static uint32_t tablet_tool_btn_to_sdl_button(struct SDL_WaylandTabletInput *input) -{ - unsigned int tool_btn = input->btn_stylus3 << 2 | input->btn_stylus2 << 1 | input->btn_stylus << 0; - switch (tool_btn) { - case 0b000: - return SDL_BUTTON_LEFT; - case 0b001: - return SDL_BUTTON_RIGHT; - case 0b010: - return SDL_BUTTON_MIDDLE; - case 0b100: - return SDL_BUTTON_X1; - default: - return SDL_BUTTON_LEFT; - } -} - static void tablet_tool_handle_down(void *data, struct zwp_tablet_tool_v2 *tool, uint32_t serial) { - struct SDL_WaylandTabletInput *input = data; - SDL_WindowData *window = input->tool_focus; - input->is_down = SDL_TRUE; - Wayland_UpdateImplicitGrabSerial(input->sdlWaylandInput, serial); - if (!window) { - /* tablet_tool_handle_proximity_out gets called when moving over the libdecoration csd. - * that sets input->tool_focus (window) to NULL, but handle_{down,up} events are still - * received. To prevent SIGSEGV this returns when this is the case. - */ - return; - } + struct SDL_WaylandTool *sdltool = data; + struct SDL_WaylandTabletInput *input = sdltool->tablet; - SDL_SendMouseButton(0, window->sdlwindow, 0, SDL_PRESSED, tablet_tool_btn_to_sdl_button(input)); + input->current_pen.buttons_pressed |= SDL_PEN_DOWN_MASK; + + input->current_pen.serial = serial; } static void tablet_tool_handle_up(void *data, struct zwp_tablet_tool_v2 *tool) { - struct SDL_WaylandTabletInput *input = data; - SDL_WindowData *window = input->tool_focus; - - input->is_down = SDL_FALSE; - - if (!window) { - /* tablet_tool_handle_proximity_out gets called when moving over the libdecoration csd. - * that sets input->tool_focus (window) to NULL, but handle_{down,up} events are still - * received. To prevent SIGSEGV this returns when this is the case. - */ - return; - } - - SDL_SendMouseButton(0, window->sdlwindow, 0, SDL_RELEASED, tablet_tool_btn_to_sdl_button(input)); + struct SDL_WaylandTool *sdltool = data; + struct SDL_WaylandTabletInput *input = sdltool->tablet; + input->current_pen.buttons_released |= SDL_PEN_DOWN_MASK; } static void tablet_tool_handle_motion(void *data, struct zwp_tablet_tool_v2 *tool, wl_fixed_t sx_w, wl_fixed_t sy_w) { - struct SDL_WaylandTabletInput *input = data; + struct SDL_WaylandTool *sdltool = data; + struct SDL_WaylandTabletInput *input = sdltool->tablet; SDL_WindowData *window = input->tool_focus; + SDL_PenID penid = Wayland_get_penid(data, tool); input->sx_w = sx_w; input->sy_w = sy_w; + if (input->tool_focus) { - float sx = (float)(wl_fixed_to_double(sx_w) * window->pointer_scale_x); - float sy = (float)(wl_fixed_to_double(sy_w) * window->pointer_scale_y); - SDL_SendMouseMotion(0, window->sdlwindow, 0, 0, sx, sy); + const float sx_f = (float)wl_fixed_to_double(sx_w); + const float sy_f = (float)wl_fixed_to_double(sy_w); + const float sx = sx_f * window->pointer_scale_x; + const float sy = sy_f * window->pointer_scale_y; + + if (penid != SDL_PEN_INVALID) { + input->current_pen.update_status.x = sx; + input->current_pen.update_status.y = sy; + input->current_pen.update_window = window; + } else { + /* Plain mouse event */ + SDL_SendMouseMotion(0, window->sdlwindow, 0, 0, sx, sy); + } } } static void tablet_tool_handle_pressure(void *data, struct zwp_tablet_tool_v2 *tool, uint32_t pressure) { - /* unimplemented */ + struct SDL_WaylandTool *sdltool = data; + struct SDL_WaylandTabletInput *input = sdltool->tablet; + input->current_pen.update_status.axes[SDL_PEN_AXIS_PRESSURE] = pressure / 65535.0f; + if (pressure) { + input->current_pen.update_status.axes[SDL_PEN_AXIS_DISTANCE] = 0.0f; + } } static void tablet_tool_handle_distance(void *data, struct zwp_tablet_tool_v2 *tool, uint32_t distance) { - /* unimplemented */ + struct SDL_WaylandTool *sdltool = data; + struct SDL_WaylandTabletInput *input = sdltool->tablet; + input->current_pen.update_status.axes[SDL_PEN_AXIS_DISTANCE] = distance / 65535.0f; + if (distance) { + input->current_pen.update_status.axes[SDL_PEN_AXIS_PRESSURE] = 0.0f; + } } static void tablet_tool_handle_tilt(void *data, struct zwp_tablet_tool_v2 *tool, wl_fixed_t xtilt, wl_fixed_t ytilt) { - /* unimplemented */ + struct SDL_WaylandTool *sdltool = data; + struct SDL_WaylandTabletInput *input = sdltool->tablet; + + input->current_pen.update_status.axes[SDL_PEN_AXIS_XTILT] = (float)(wl_fixed_to_double(xtilt)); + input->current_pen.update_status.axes[SDL_PEN_AXIS_YTILT] = (float)(wl_fixed_to_double(ytilt)); } static void tablet_tool_handle_button(void *data, struct zwp_tablet_tool_v2 *tool, uint32_t serial, uint32_t button, uint32_t state) { - struct SDL_WaylandTabletInput *input = (struct SDL_WaylandTabletInput*)data; + struct SDL_WaylandTool *sdltool = data; + struct SDL_WaylandTabletInput *input = sdltool->tablet; + Uint16 mask = 0; + SDL_bool pressed = state == ZWP_TABLET_PAD_V2_BUTTON_STATE_PRESSED ? SDL_TRUE : SDL_FALSE; - if (input->is_down) { - tablet_tool_handle_up(data, tool); - input->is_down = SDL_TRUE; - } - - Wayland_UpdateImplicitGrabSerial(input->sdlWaylandInput, serial); + /* record event serial number to report it later in tablet_tool_handle_frame() */ + input->current_pen.serial = serial; switch (button) { /* see %{_includedir}/linux/input-event-codes.h */ case 0x14b: /* BTN_STYLUS */ - input->btn_stylus = (state == ZWP_TABLET_PAD_V2_BUTTON_STATE_PRESSED); + mask = SDL_BUTTON_LMASK; break; case 0x14c: /* BTN_STYLUS2 */ - input->btn_stylus2 = (state == ZWP_TABLET_PAD_V2_BUTTON_STATE_PRESSED); + mask = SDL_BUTTON_MMASK; break; case 0x149: /* BTN_STYLUS3 */ - input->btn_stylus3 = (state == ZWP_TABLET_PAD_V2_BUTTON_STATE_PRESSED); + mask = SDL_BUTTON_RMASK; break; } - if (input->is_down) { - tablet_tool_handle_down(data, tool, serial); + if (pressed) { + input->current_pen.buttons_pressed |= mask; + } else { + input->current_pen.buttons_released |= mask; } } static void tablet_tool_handle_rotation(void *data, struct zwp_tablet_tool_v2 *tool, wl_fixed_t degrees) { - /* unimplemented */ + struct SDL_WaylandTool *sdltool = data; + struct SDL_WaylandTabletInput *input = sdltool->tablet; + float rotation = (float)(wl_fixed_to_double(degrees)); + + /* map to -180.0f ... 179.0f range: */ + input->current_pen.update_status.axes[SDL_PEN_AXIS_ROTATION] = rotation > 180.0f ? rotation - 360.0f : rotation; } static void tablet_tool_handle_slider(void *data, struct zwp_tablet_tool_v2 *tool, int32_t position) { - /* unimplemented */ + struct SDL_WaylandTool *sdltool = data; + struct SDL_WaylandTabletInput *input = sdltool->tablet; + input->current_pen.update_status.axes[SDL_PEN_AXIS_SLIDER] = position / 65535.0; } static void tablet_tool_handle_wheel(void *data, struct zwp_tablet_tool_v2 *tool, int32_t degrees, int32_t clicks) { - /* unimplemented */ + /* not supported at the moment */ } static void tablet_tool_handle_frame(void *data, struct zwp_tablet_tool_v2 *tool, uint32_t time) { - /* unimplemented */ + struct SDL_WaylandTool *sdltool = data; + struct SDL_WaylandTabletInput *input = sdltool->tablet; + SDL_PenID penid = Wayland_get_penid(data, tool); + SDL_WindowData *window = input->current_pen.update_window; + SDL_PenStatusInfo *status = &input->current_pen.update_status; + int button; + int button_mask; + Uint64 timestamp = Wayland_GetEventTimestamp(SDL_MS_TO_NS(time)); + + if (penid == 0 || !window) { /* Not a pen, or event reported out of focus */ + return; + } + /* window == input->tool_focus */ + + /* All newly released buttons + PEN_UP event */ + button_mask = input->current_pen.buttons_released; + if (button_mask & SDL_PEN_DOWN_MASK) { + /* Perform hit test, if appropriate */ + if (!SDL_PenPerformHitTest() + || !ProcessHitTest(window, input->sdlWaylandInput->seat, input->sx_w, input->sy_w, input->current_pen.serial)) { + SDL_SendPenTipEvent(timestamp, penid, SDL_RELEASED); + } + } + button_mask &= ~SDL_PEN_DOWN_MASK; + + for (button = 1; button_mask; ++button, button_mask >>= 1) { + if (button_mask & 1) { + SDL_SendPenButton(timestamp, penid, SDL_RELEASED, button); + } + } + + /* All newly pressed buttons + PEN_DOWN event */ + button_mask = input->current_pen.buttons_pressed; + if (button_mask & SDL_PEN_DOWN_MASK) { + /* Perform hit test, if appropriate */ + if (!SDL_PenPerformHitTest() + || !ProcessHitTest(window, input->sdlWaylandInput->seat, input->sx_w, input->sy_w, input->current_pen.serial)) { + SDL_SendPenTipEvent(timestamp, penid, SDL_PRESSED); + } + } + button_mask &= ~SDL_PEN_DOWN_MASK; + + for (button = 1; button_mask; ++button, button_mask >>= 1) { + if (button_mask & 1) { + SDL_SendPenButton(timestamp, penid, SDL_PRESSED, button); + } + } + + SDL_SendPenMotion(timestamp, penid, SDL_TRUE, status); + + /* Wayland_UpdateImplicitGrabSerial will ignore serial 0, so it is safe to call with the default value */ + Wayland_UpdateImplicitGrabSerial(input->sdlWaylandInput, input->current_pen.serial); + + /* Reset masks for next tool frame */ + input->current_pen.buttons_pressed = 0; + input->current_pen.buttons_released = 0; + input->current_pen.serial = 0; } static const struct zwp_tablet_tool_v2_listener tablet_tool_listener = { @@ -2624,6 +2847,26 @@ static void tablet_object_list_destroy(struct SDL_WaylandTabletObjectListNode *h } } +void tablet_object_list_remove(struct SDL_WaylandTabletObjectListNode *head, void *object) +{ + struct SDL_WaylandTabletObjectListNode **head_p = &head; + while (*head_p && (*head_p)->object != object) { + head_p = &((*head_p)->next); + } + + if (*head_p) { + struct SDL_WaylandTabletObjectListNode *object_head = *head_p; + + if (object_head == head) { + /* Must not remove head node */ + head->object = NULL; + } else { + *head_p = object_head->next; + SDL_free(object_head); + } + } +} + static void tablet_seat_handle_tablet_added(void *data, struct zwp_tablet_seat_v2 *seat, struct zwp_tablet_v2 *tablet) { struct SDL_WaylandTabletInput *input = data; @@ -2634,9 +2877,12 @@ static void tablet_seat_handle_tablet_added(void *data, struct zwp_tablet_seat_v static void tablet_seat_handle_tool_added(void *data, struct zwp_tablet_seat_v2 *seat, struct zwp_tablet_tool_v2 *tool) { struct SDL_WaylandTabletInput *input = data; + struct SDL_WaylandTool *sdltool = SDL_calloc(1, sizeof(struct SDL_WaylandTool)); - zwp_tablet_tool_v2_add_listener(tool, &tablet_tool_listener, data); - zwp_tablet_tool_v2_set_user_data(tool, data); + zwp_tablet_tool_v2_add_listener(tool, &tablet_tool_listener, sdltool); + zwp_tablet_tool_v2_set_user_data(tool, sdltool); + + sdltool->tablet = input; tablet_object_list_append(input->tools, tool); } @@ -2657,6 +2903,7 @@ static const struct zwp_tablet_seat_v2_listener tablet_seat_listener = { void Wayland_input_add_tablet(struct SDL_WaylandInput *input, struct SDL_WaylandTabletManager *tablet_manager) { struct SDL_WaylandTabletInput *tablet_input; + static Uint32 num_tablets = 0; if (!tablet_manager || !input || !input->seat) { return; @@ -2675,6 +2922,7 @@ void Wayland_input_add_tablet(struct SDL_WaylandInput *input, struct SDL_Wayland tablet_input->tablets = tablet_object_list_new_node(NULL); tablet_input->tools = tablet_object_list_new_node(NULL); tablet_input->pads = tablet_object_list_new_node(NULL); + tablet_input->id = num_tablets++; zwp_tablet_seat_v2_add_listener((struct zwp_tablet_seat_v2 *)tablet_input->seat, &tablet_seat_listener, tablet_input); } @@ -2683,7 +2931,7 @@ void Wayland_input_add_tablet(struct SDL_WaylandInput *input, struct SDL_Wayland void Wayland_input_destroy_tablet(struct SDL_WaylandInput *input) { tablet_object_list_destroy(input->tablet->pads, TABLET_OBJECT_LIST_DELETER(zwp_tablet_pad_v2_destroy)); - tablet_object_list_destroy(input->tablet->tools, TABLET_OBJECT_LIST_DELETER(zwp_tablet_tool_v2_destroy)); + tablet_object_list_destroy(input->tablet->tools, TABLET_OBJECT_LIST_DELETER(Wayland_tool_destroy)); tablet_object_list_destroy(input->tablet->tablets, TABLET_OBJECT_LIST_DELETER(zwp_tablet_v2_destroy)); zwp_tablet_seat_v2_destroy(input->tablet->seat); @@ -2808,7 +3056,8 @@ void Wayland_display_destroy_input(SDL_VideoData *d) wl_touch_destroy(input->touch); } - wl_list_for_each_safe (tp, tmp, &touch_points, link) { + wl_list_for_each_safe(tp, tmp, &touch_points, link) + { WAYLAND_wl_list_remove(&tp->link); SDL_free(tp); } diff --git a/src/video/wayland/SDL_waylandevents_c.h b/src/video/wayland/SDL_waylandevents_c.h index 5dd5840841..0d4e66ec5e 100644 --- a/src/video/wayland/SDL_waylandevents_c.h +++ b/src/video/wayland/SDL_waylandevents_c.h @@ -25,6 +25,7 @@ #define SDL_waylandevents_h_ #include "../../events/SDL_mouse_c.h" +#include "../../events/SDL_pen_c.h" #include "SDL_waylandvideo.h" #include "SDL_waylandwindow.h" @@ -55,31 +56,38 @@ struct SDL_WaylandTabletInput struct SDL_WaylandTabletObjectListNode *tools; struct SDL_WaylandTabletObjectListNode *pads; + Uint32 id; + Uint32 num_pens; /* next pen ID is num_pens+1 */ + struct SDL_WaylandCurrentPen + { + SDL_Pen *builder; /* pen that is being defined or receiving updates, if any */ + SDL_bool builder_guid_complete; /* have complete/precise GUID information */ + SDL_PenStatusInfo update_status; /* collects pen update information before sending event */ + Uint16 buttons_pressed; /* Mask of newly pressed buttons, plus SDL_PEN_DOWN_MASK for PEN_DOWN */ + Uint16 buttons_released; /* Mask of newly pressed buttons, plus SDL_PEN_DOWN_MASK for PEN_UP */ + Uint32 serial; /* Most recent serial event number observed, or 0 */ + SDL_WindowData *update_window; /* NULL while no event is in progress, otherwise the affected window */ + } current_pen; + SDL_WindowData *tool_focus; uint32_t tool_prox_serial; - /* Last motion location */ + /* Last motion end location (kept separate from sx_w, sy_w for the mouse pointer) */ wl_fixed_t sx_w; wl_fixed_t sy_w; - - SDL_bool is_down; - - SDL_bool btn_stylus; - SDL_bool btn_stylus2; - SDL_bool btn_stylus3; }; typedef struct { - int32_t repeat_rate; /* Repeat rate in range of [1, 1000] character(s) per second */ - int32_t repeat_delay_ms; /* Time to first repeat event in milliseconds */ + int32_t repeat_rate; /* Repeat rate in range of [1, 1000] character(s) per second */ + int32_t repeat_delay_ms; /* Time to first repeat event in milliseconds */ SDL_bool is_initialized; SDL_bool is_key_down; uint32_t key; - Uint64 wl_press_time_ns; /* Key press time as reported by the Wayland API */ - Uint64 sdl_press_time_ns; /* Key press time expressed in SDL ticks */ - Uint64 next_repeat_ns; /* Next repeat event in nanoseconds */ + Uint64 wl_press_time_ns; /* Key press time as reported by the Wayland API */ + Uint64 sdl_press_time_ns; /* Key press time expressed in SDL ticks */ + Uint64 next_repeat_ns; /* Next repeat event in nanoseconds */ uint32_t scancode; char text[8]; } SDL_WaylandKeyboardRepeat; @@ -169,6 +177,12 @@ struct SDL_WaylandInput SDL_Keymod locked_modifiers; }; +struct SDL_WaylandTool +{ + SDL_PenID penid; + struct SDL_WaylandTabletInput *tablet; +}; + extern Uint64 Wayland_GetTouchTimestamp(struct SDL_WaylandInput *input, Uint32 wl_timestamp_ms); extern void Wayland_PumpEvents(SDL_VideoDevice *_this); diff --git a/src/video/x11/SDL_x11events.c b/src/video/x11/SDL_x11events.c index 75acc17af7..52ef9575ae 100644 --- a/src/video/x11/SDL_x11events.c +++ b/src/video/x11/SDL_x11events.c @@ -179,14 +179,14 @@ static SDL_bool X11_KeyRepeat(Display *display, XEvent *event) return d.found; } -static SDL_bool X11_IsWheelEvent(Display *display, XEvent *event, int *xticks, int *yticks) +static SDL_bool X11_IsWheelEvent(Display *display, int button, int *xticks, int *yticks) { /* according to the xlib docs, no specific mouse wheel events exist. However, the defacto standard is that the vertical wheel is X buttons 4 (up) and 5 (down) and a horizontal wheel is 6 (left) and 7 (right). */ /* Xlib defines "Button1" through 5, so we just use literals here. */ - switch (event->xbutton.button) { + switch (button) { case 4: *yticks = 1; return SDL_TRUE; @@ -333,13 +333,15 @@ void SDL_SetX11EventHook(SDL_X11EventHook callback, void *userdata) } #ifdef SDL_VIDEO_DRIVER_X11_SUPPORTS_GENERIC_EVENTS -static void X11_HandleGenericEvent(SDL_VideoData *videodata, XEvent *xev) +static void X11_HandleGenericEvent(SDL_VideoDevice *_this, XEvent *xev) { + SDL_VideoData *videodata = (SDL_VideoData *)_this->driverdata; + /* event is a union, so cookie == &event, but this is type safe. */ XGenericEventCookie *cookie = &xev->xcookie; if (X11_XGetEventData(videodata->display, cookie)) { if (!g_X11EventHook || g_X11EventHook(g_X11EventHookData, xev)) { - X11_HandleXinput2Event(videodata, cookie); + X11_HandleXinput2Event(_this, cookie); } X11_XFreeEventData(videodata->display, cookie); } @@ -557,12 +559,12 @@ static void InitiateWindowResize(SDL_VideoDevice *_this, const SDL_WindowData *d X11_XSync(display, 0); } -static SDL_bool ProcessHitTest(SDL_VideoDevice *_this, const SDL_WindowData *data, const XEvent *xev) +SDL_bool X11_ProcessHitTest(SDL_VideoDevice *_this, const SDL_WindowData *data, const float x, const float y) { SDL_Window *window = data->window; if (window->hit_test) { - const SDL_Point point = { xev->xbutton.x, xev->xbutton.y }; + const SDL_Point point = { x, y }; const SDL_HitTestResult rc = window->hit_test(window, &point, window->hit_test_data); static const int directions[] = { _NET_WM_MOVERESIZE_SIZE_TOPLEFT, _NET_WM_MOVERESIZE_SIZE_TOP, @@ -761,14 +763,14 @@ static Bool isReparentNotify(Display *display, XEvent *ev, XPointer arg) static SDL_bool IsHighLatin1(const char *string, int length) { - while (length-- > 0) { - Uint8 ch = (Uint8)*string; - if (ch >= 0x80) { - return SDL_TRUE; - } - ++string; - } - return SDL_FALSE; + while (length-- > 0) { + Uint8 ch = (Uint8)*string; + if (ch >= 0x80) { + return SDL_TRUE; + } + ++string; + } + return SDL_FALSE; } static int XLookupStringAsUTF8(XKeyEvent *event_struct, char *buffer_return, int bytes_buffer, KeySym *keysym_return, XComposeStatus *status_in_out) @@ -787,6 +789,78 @@ static int XLookupStringAsUTF8(XKeyEvent *event_struct, char *buffer_return, int return result; } +SDL_WindowData *X11_FindWindow(SDL_VideoDevice *_this, Window window) +{ + const SDL_VideoData *videodata = (SDL_VideoData *)_this->driverdata; + int i; + + if (videodata && videodata->windowlist) { + for (i = 0; i < videodata->numwindows; ++i) { + if ((videodata->windowlist[i] != NULL) && + (videodata->windowlist[i]->xwindow == window)) { + return videodata->windowlist[i]; + } + } + } + return NULL; +} + +void X11_HandleButtonPress(SDL_VideoDevice *_this, SDL_WindowData *windowdata, int button, const float x, const float y, const unsigned long time) +{ + SDL_Window *window = windowdata->window; + const SDL_VideoData *videodata = (SDL_VideoData *)_this->driverdata; + Display *display = videodata->display; + int xticks = 0, yticks = 0; +#ifdef DEBUG_XEVENTS + printf("window %p: ButtonPress (X11 button = %d)\n", window, button); +#endif + if (X11_IsWheelEvent(display, button, &xticks, &yticks)) { + SDL_SendMouseWheel(0, window, 0, (float)-xticks, (float)yticks, SDL_MOUSEWHEEL_NORMAL); + } else { + SDL_bool ignore_click = SDL_FALSE; + if (button == Button1) { + if (X11_ProcessHitTest(_this, windowdata, x, y)) { + SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_HIT_TEST, 0, 0); + return; /* don't pass this event on to app. */ + } + } else if (button > 7) { + /* X button values 4-7 are used for scrolling, so X1 is 8, X2 is 9, ... + => subtract (8-SDL_BUTTON_X1) to get value SDL expects */ + button -= (8 - SDL_BUTTON_X1); + } + if (windowdata->last_focus_event_time) { + const int X11_FOCUS_CLICK_TIMEOUT = 10; + if (SDL_GetTicks() < (windowdata->last_focus_event_time + X11_FOCUS_CLICK_TIMEOUT)) { + ignore_click = !SDL_GetHintBoolean(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, SDL_FALSE); + } + windowdata->last_focus_event_time = 0; + } + if (!ignore_click) { + SDL_SendMouseButton(0, window, 0, SDL_PRESSED, button); + } + } + X11_UpdateUserTime(windowdata, time); +} + +void X11_HandleButtonRelease(SDL_VideoDevice *_this, SDL_WindowData *windowdata, int button) +{ + SDL_Window *window = windowdata->window; + const SDL_VideoData *videodata = (SDL_VideoData *)_this->driverdata; + Display *display = videodata->display; + /* The X server sends a Release event for each Press for wheels. Ignore them. */ + int xticks = 0, yticks = 0; +#ifdef DEBUG_XEVENTS + printf("window %p: ButtonRelease (X11 button = %d)\n", data, xevent->xbutton.button); +#endif + if (!X11_IsWheelEvent(display, button, &xticks, &yticks)) { + if (button > 7) { + /* see explanation at case ButtonPress */ + button -= (8 - SDL_BUTTON_X1); + } + SDL_SendMouseButton(0, window, 0, SDL_RELEASED, button); + } +} + void X11_GetBorderValues(SDL_WindowData *data) { SDL_VideoData *videodata = data->videodata; @@ -861,7 +935,7 @@ static void X11_DispatchEvent(SDL_VideoDevice *_this, XEvent *xevent) #ifdef SDL_VIDEO_DRIVER_X11_SUPPORTS_GENERIC_EVENTS if (xevent->type == GenericEvent) { - X11_HandleGenericEvent(videodata, xevent); + X11_HandleGenericEvent(_this, xevent); return; } #endif @@ -886,19 +960,19 @@ static void X11_DispatchEvent(SDL_VideoDevice *_this, XEvent *xevent) #ifdef SDL_VIDEO_DRIVER_X11_XFIXES if (SDL_X11_HAVE_XFIXES && - xevent->type == X11_GetXFixesSelectionNotifyEvent()) { - XFixesSelectionNotifyEvent *ev = (XFixesSelectionNotifyEvent *) xevent; + xevent->type == X11_GetXFixesSelectionNotifyEvent()) { + XFixesSelectionNotifyEvent *ev = (XFixesSelectionNotifyEvent *)xevent; /* !!! FIXME: cache atoms */ Atom XA_CLIPBOARD = X11_XInternAtom(display, "CLIPBOARD", 0); #ifdef DEBUG_XEVENTS printf("window CLIPBOARD: XFixesSelectionNotify (selection = %s)\n", - X11_XGetAtomName(display, ev->selection)); + X11_XGetAtomName(display, ev->selection)); #endif if (ev->selection == XA_PRIMARY || - (XA_CLIPBOARD != None && ev->selection == XA_CLIPBOARD)) { + (XA_CLIPBOARD != None && ev->selection == XA_CLIPBOARD)) { SDL_SendClipboardUpdate(); return; } @@ -911,16 +985,8 @@ static void X11_DispatchEvent(SDL_VideoDevice *_this, XEvent *xevent) return; } - data = NULL; - if (videodata && videodata->windowlist) { - for (i = 0; i < videodata->numwindows; ++i) { - if ((videodata->windowlist[i] != NULL) && - (videodata->windowlist[i]->xwindow == xevent->xany.window)) { - data = videodata->windowlist[i]; - break; - } - } - } + data = X11_FindWindow(_this, xevent->xany.window); + if (!data) { /* The window for KeymapNotify, etc events is 0 */ if (xevent->type == KeymapNotify) { @@ -1227,8 +1293,9 @@ static void X11_DispatchEvent(SDL_VideoDevice *_this, XEvent *xevent) xevent->xconfigure.x, xevent->xconfigure.y, xevent->xconfigure.width, xevent->xconfigure.height); #endif - /* Real configure notify events are relative to the parent, synthetic events are absolute. */ - if (!xevent->xconfigure.send_event) { + /* Real configure notify events are relative to the parent, synthetic events are absolute. */ + if (!xevent->xconfigure.send_event) + { unsigned int NumChildren; Window ChildReturn, Root, Parent; Window *Children; @@ -1318,7 +1385,7 @@ static void X11_DispatchEvent(SDL_VideoDevice *_this, XEvent *xevent) root_y = xevent->xclient.data.l[2] & 0xffff; /* Translate from root to current window position */ X11_XTranslateCoordinates(display, DefaultRootWindow(display), data->xwindow, - root_x, root_y, &window_x, &window_y, &ChildReturn); + root_x, root_y, &window_x, &window_y, &ChildReturn); SDL_SendDropPosition(data->window, (float)window_x, (float)window_y); } @@ -1402,6 +1469,12 @@ static void X11_DispatchEvent(SDL_VideoDevice *_this, XEvent *xevent) SDL_SendWindowEvent(data->window, SDL_EVENT_WINDOW_EXPOSED, 0, 0); } break; + /* Use XInput2 instead of the xevents API if possible, for: + - MotionNotify + - ButtonPress + - ButtonRelease + XInput2 has more precise information, e.g., to distinguish different input devices. */ +#ifndef SDL_VIDEO_DRIVER_X11_XINPUT2 case MotionNotify: { SDL_Mouse *mouse = SDL_GetMouse(); @@ -1416,55 +1489,15 @@ static void X11_DispatchEvent(SDL_VideoDevice *_this, XEvent *xevent) case ButtonPress: { - int xticks = 0, yticks = 0; -#ifdef DEBUG_XEVENTS - printf("window %p: ButtonPress (X11 button = %d)\n", data, xevent->xbutton.button); -#endif - if (X11_IsWheelEvent(display, xevent, &xticks, &yticks)) { - SDL_SendMouseWheel(0, data->window, 0, (float)-xticks, (float)yticks, SDL_MOUSEWHEEL_NORMAL); - } else { - SDL_bool ignore_click = SDL_FALSE; - int button = xevent->xbutton.button; - if (button == Button1) { - if (ProcessHitTest(_this, data, xevent)) { - SDL_SendWindowEvent(data->window, SDL_EVENT_WINDOW_HIT_TEST, 0, 0); - break; /* don't pass this event on to app. */ - } - } else if (button > 7) { - /* X button values 4-7 are used for scrolling, so X1 is 8, X2 is 9, ... - => subtract (8-SDL_BUTTON_X1) to get value SDL expects */ - button -= (8 - SDL_BUTTON_X1); - } - if (data->last_focus_event_time) { - const int X11_FOCUS_CLICK_TIMEOUT = 10; - if (SDL_GetTicks() < (data->last_focus_event_time + X11_FOCUS_CLICK_TIMEOUT)) { - ignore_click = !SDL_GetHintBoolean(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, SDL_FALSE); - } - data->last_focus_event_time = 0; - } - if (!ignore_click) { - SDL_SendMouseButton(0, data->window, 0, SDL_PRESSED, button); - } - } - X11_UpdateUserTime(data, xevent->xbutton.time); + X11_HandleButtonPress(_this, data, xevent->xbutton.button, + xevent->xbutton.x, xevent->xbutton.y, xevent->xbutton.time); } break; case ButtonRelease: { - int button = xevent->xbutton.button; - /* The X server sends a Release event for each Press for wheels. Ignore them. */ - int xticks = 0, yticks = 0; -#ifdef DEBUG_XEVENTS - printf("window %p: ButtonRelease (X11 button = %d)\n", data, xevent->xbutton.button); -#endif - if (!X11_IsWheelEvent(display, xevent, &xticks, &yticks)) { - if (button > 7) { - /* see explanation at case ButtonPress */ - button -= (8 - SDL_BUTTON_X1); - } - SDL_SendMouseButton(0, data->window, 0, SDL_RELEASED, button); - } + X11_HandleButtonRelease(_this, data, xevent->xbutton.button); } break; +#endif /* !SDL_VIDEO_DRIVER_X11_XINPUT2 */ case PropertyNotify: { diff --git a/src/video/x11/SDL_x11events.h b/src/video/x11/SDL_x11events.h index 58cdb8dabb..02c6665405 100644 --- a/src/video/x11/SDL_x11events.h +++ b/src/video/x11/SDL_x11events.h @@ -29,5 +29,9 @@ extern void X11_SendWakeupEvent(SDL_VideoDevice *_this, SDL_Window *window); extern int X11_SuspendScreenSaver(SDL_VideoDevice *_this); extern void X11_ReconcileKeyboardState(SDL_VideoDevice *_this); extern void X11_GetBorderValues(SDL_WindowData *data); +extern void X11_HandleButtonPress(SDL_VideoDevice *_this, SDL_WindowData *wdata, int button, const float x, const float y, const unsigned long time); +extern void X11_HandleButtonRelease(SDL_VideoDevice *_this, SDL_WindowData *wdata, int button); +extern SDL_WindowData *X11_FindWindow(SDL_VideoDevice *_this, Window window); +extern SDL_bool X11_ProcessHitTest(SDL_VideoDevice *_this, const SDL_WindowData *data, const float x, const float y); #endif /* SDL_x11events_h_ */ diff --git a/src/video/x11/SDL_x11pen.c b/src/video/x11/SDL_x11pen.c new file mode 100644 index 0000000000..94b4a5c1b9 --- /dev/null +++ b/src/video/x11/SDL_x11pen.c @@ -0,0 +1,694 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2023 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "../../SDL_internal.h" + +#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2 + +#include "../../events/SDL_pen_c.h" +#include "../SDL_sysvideo.h" +#include "SDL_pen.h" +#include "SDL_x11pen.h" +#include "SDL_x11video.h" +#include "SDL_x11xinput2.h" + +#define PEN_ERASER_ID_MAXLEN 256 /* Max # characters of device name to scan */ +#define PEN_ERASER_NAME_TAG "eraser" /* String constant to identify erasers */ + +#define DEBUG_PEN (SDL_PEN_DEBUG_NOID | SDL_PEN_DEBUG_NONWACOM | SDL_PEN_DEBUG_UNKNOWN_WACOM | SDL_PEN_DEBUG_NOSERIAL_WACOM) + +#define SDL_PEN_AXIS_VALUATOR_MISSING -1 + +/* X11-specific information attached to each pen */ +typedef struct xinput2_pen +{ + float axis_min[SDL_PEN_NUM_AXES]; + float axis_max[SDL_PEN_NUM_AXES]; + float slider_bias; /* shift value to add to PEN_AXIS_SLIDER (before normalisation) */ + float rotation_bias; /* rotation to add to PEN_AXIS_ROTATION (after normalisation) */ + Sint8 valuator_for_axis[SDL_PEN_NUM_AXES]; /* SDL_PEN_AXIS_VALUATOR_MISSING if not supported */ +} xinput2_pen; + +/* X11 atoms */ +static struct +{ + int initialized; /* initialised to 0 */ + Atom device_product_id; + Atom abs_pressure; + Atom abs_tilt_x; + Atom abs_tilt_y; + Atom wacom_serial_ids; + Atom wacom_tool_type; +} pen_atoms; + +/* + * Mapping from X11 device IDs to pen IDs + * + * In X11, the same device ID may represent any number of pens. We + * thus cannot directly use device IDs as pen IDs. + */ +static struct +{ + int num_pens_known; /* Number of currently known pens (based on their GUID); used to give pen ID to new pens */ + int num_entries; /* Number of X11 device IDs that correspond to pens */ + + struct pen_device_id_mapping + { + Uint32 deviceid; + Uint32 pen_id; + } * entries; /* Current pen to device ID mappings */ +} pen_map; + +typedef enum +{ + SDL_PEN_VENDOR_UNKNOWN = 0, + SDL_PEN_VENDOR_WACOM +} sdl_pen_vendor; + +/* Information to identify pens during discovery */ +typedef struct +{ + sdl_pen_vendor vendor; + SDL_GUID guid; + SDL_PenSubtype heuristic_type; /* Distinguish pen+eraser devices with shared bus ID */ + Uint32 devicetype_id, serial; /* used by PEN_VENDOR_WACOM */ + Uint32 deviceid; +} pen_identity; + +int X11_PenIDFromDeviceID(int deviceid) +{ + int i; + for (i = 0; i < pen_map.num_entries; ++i) { + if (pen_map.entries[i].deviceid == deviceid) { + return pen_map.entries[i].pen_id; + } + } + return SDL_PEN_INVALID; +} + +static void pen_atoms_ensure_initialized(SDL_VideoDevice *_this) +{ + SDL_VideoData *data = (SDL_VideoData *)_this->driverdata; + + if (pen_atoms.initialized) { + return; + } + /* Create atoms if they don't exist yet to pre-empt hotplugging updates */ + pen_atoms.device_product_id = X11_XInternAtom(data->display, "Device Product ID", False); + pen_atoms.wacom_serial_ids = X11_XInternAtom(data->display, "Wacom Serial IDs", False); + pen_atoms.wacom_tool_type = X11_XInternAtom(data->display, "Wacom Tool Type", False); + pen_atoms.abs_pressure = X11_XInternAtom(data->display, "Abs Pressure", True); + pen_atoms.abs_tilt_x = X11_XInternAtom(data->display, "Abs Tilt X", True); + pen_atoms.abs_tilt_y = X11_XInternAtom(data->display, "Abs Tilt Y", True); + + pen_atoms.initialized = 1; +} + +/* Read out an integer property and store into a preallocated Sint32 array, extending 8 and 16 bit values suitably. + Returns number of Sint32s written (<= max_words), or 0 on error. */ +static size_t xinput2_pen_get_int_property(SDL_VideoDevice *_this, int deviceid, Atom property, Sint32 *dest, size_t max_words) +{ + const SDL_VideoData *data = (SDL_VideoData *)_this->driverdata; + Atom type_return; + int format_return; + unsigned long num_items_return; + unsigned long bytes_after_return; + unsigned char *output; + + if (property == None) { + return 0; + } + + if (Success != X11_XIGetProperty(data->display, deviceid, + property, + 0, max_words, False, + XA_INTEGER, &type_return, &format_return, + &num_items_return, &bytes_after_return, + &output) || + num_items_return == 0 || output == NULL) { + return 0; + } + + if (type_return == XA_INTEGER) { + int k; + const int to_copy = SDL_min(max_words, num_items_return); + + if (format_return == 8) { + Sint8 *numdata = (Sint8 *)output; + for (k = 0; k < to_copy; ++k) { + dest[k] = numdata[k]; + } + } else if (format_return == 16) { + Sint16 *numdata = (Sint16 *)output; + for (k = 0; k < to_copy; ++k) { + dest[k] = numdata[k]; + } + } else { + SDL_memcpy(dest, output, sizeof(Sint32) * to_copy); + } + X11_XFree(output); + return to_copy; + } + return 0; /* type mismatch */ +} + +/* 32 bit vendor + device ID from evdev */ +static Uint32 xinput2_pen_evdevid(SDL_VideoDevice *_this, int deviceid) +{ +#if !(SDL_PEN_DEBUG_NOID) + Sint32 ids[2]; + + pen_atoms_ensure_initialized(_this); + + if (2 != xinput2_pen_get_int_property(_this, deviceid, pen_atoms.device_product_id, ids, 2)) { + return 0; + } + return ((ids[0] << 16) | (ids[1] & 0xffff)); +#else /* Testing: pretend that we have no ID (not sure if this can happen IRL) */ + return 0; +#endif +} + +/* Gets reasonably-unique GUID for the device */ +static void xinput2_pen_update_generic_guid(SDL_VideoDevice *_this, pen_identity *pident, int deviceid) +{ + Uint32 evdevid = xinput2_pen_evdevid(_this, deviceid); /* also initialises pen_atoms */ + + if (!evdevid) { + /* Fallback: if no evdevid is available; try to at least distinguish devices within the + current session. This is a poor GUID and our last resort. */ + evdevid = deviceid; + } + SDL_PenUpdateGUIDForGeneric(&pident->guid, 0, evdevid); +} + +/* Identify Wacom devices (if SDL_TRUE is returned) and extract their device type and serial IDs */ +static SDL_bool xinput2_wacom_deviceid(SDL_VideoDevice *_this, int deviceid, Uint32 *wacom_devicetype_id, Uint32 *wacom_serial) +{ +#if !(SDL_PEN_DEBUG_NONWACOM) /* Can be disabled for testing */ + Sint32 serial_id_buf[3]; + int result; + + pen_atoms_ensure_initialized(_this); + + if ((result = xinput2_pen_get_int_property(_this, deviceid, pen_atoms.wacom_serial_ids, serial_id_buf, 3)) == 3) { + *wacom_devicetype_id = serial_id_buf[2]; + *wacom_serial = serial_id_buf[1]; +#if SDL_PEN_DEBUG_NOSERIAL_WACOM /* Disabled for testing? */ + *wacom_serial = 0; +#endif + return SDL_TRUE; + } +#endif + return SDL_FALSE; +} + +/* Heuristically determines if device is an eraser */ +static SDL_bool xinput2_pen_is_eraser(SDL_VideoDevice *_this, int deviceid, char *devicename) +{ + SDL_VideoData *data = (SDL_VideoData *)_this->driverdata; + char dev_name[PEN_ERASER_ID_MAXLEN]; + int k; + + pen_atoms_ensure_initialized(_this); + + if (pen_atoms.wacom_tool_type != None) { + Atom type_return; + int format_return; + unsigned long num_items_return; + unsigned long bytes_after_return; + unsigned char *tooltype_name_info = NULL; + + /* Try Wacom-specific method */ + if (Success == X11_XIGetProperty(data->display, deviceid, + pen_atoms.wacom_tool_type, + 0, 32, False, + AnyPropertyType, &type_return, &format_return, + &num_items_return, &bytes_after_return, + &tooltype_name_info) && + tooltype_name_info != NULL && num_items_return > 0) { + + SDL_bool result = SDL_FALSE; + char *tooltype_name = NULL; + + if (type_return == XA_ATOM) { + /* Atom instead of string? Un-intern */ + Atom atom = *((Atom *)tooltype_name_info); + if (atom != None) { + tooltype_name = X11_XGetAtomName(data->display, atom); + } + } else if (type_return == XA_STRING && format_return == 8) { + tooltype_name = (char *)tooltype_name_info; + } + + if (tooltype_name) { + if (0 == SDL_strcasecmp(tooltype_name, PEN_ERASER_NAME_TAG)) { + result = SDL_TRUE; + } + X11_XFree(tooltype_name_info); + + return result; + } + } + } + /* Non-Wacom device? */ + + /* We assume that a device is an eraser if its name contains the string "eraser". + * Unfortunately there doesn't seem to be a clean way to distinguish these cases (as of 2022-03). */ + + SDL_strlcpy(dev_name, devicename, PEN_ERASER_ID_MAXLEN); + /* lowercase device name string so we can use strstr() */ + for (k = 0; dev_name[k]; ++k) { + dev_name[k] = tolower(dev_name[k]); + } + + return (SDL_strstr(dev_name, PEN_ERASER_NAME_TAG)) ? SDL_TRUE : SDL_FALSE; +} + +/* Gets GUID and other identifying information for the device using the best known method */ +static pen_identity xinput2_identify_pen(SDL_VideoDevice *_this, int deviceid, char *name) +{ + pen_identity pident; + + pident.devicetype_id = 0ul; + pident.serial = 0ul; + pident.deviceid = deviceid; + pident.heuristic_type = SDL_PEN_TYPE_PEN; + SDL_memset(pident.guid.data, 0, sizeof(pident.guid.data)); + + if (xinput2_pen_is_eraser(_this, deviceid, name)) { + pident.heuristic_type = SDL_PEN_TYPE_ERASER; + } + + if (xinput2_wacom_deviceid(_this, deviceid, &pident.devicetype_id, &pident.serial)) { + pident.vendor = SDL_PEN_VENDOR_WACOM; + SDL_PenUpdateGUIDForWacom(&pident.guid, pident.devicetype_id, pident.serial); + +#if DEBUG_PEN + printf("[pen] Pen %d reports Wacom device_id %x\n", + deviceid, pident.devicetype_id); +#endif + + } else { + pident.vendor = SDL_PEN_VENDOR_UNKNOWN; + } + if (!pident.serial) { + /* If the pen has a serial number, we can move it across tablets and retain its identity. + Otherwise, we use the evdev ID as part of its GUID, which may mean that we identify it with the tablet. */ + xinput2_pen_update_generic_guid(_this, &pident, deviceid); + } + SDL_PenUpdateGUIDForType(&pident.guid, pident.heuristic_type); + return pident; +} + +static void xinput2_pen_free_deviceinfo(Uint32 deviceid, void *x11_peninfo, void *context) +{ + SDL_free(x11_peninfo); +} + +static void xinput2_merge_deviceinfo(xinput2_pen *dest, xinput2_pen *src) +{ + *dest = *src; +} + +/** + * Fill in vendor-specific device information, if available + * + * For Wacom pens: identify number of buttons and extra axis (if present) + * + * \param _this global state + * \param dev The device to analyse + * \param pen The pen to initialise + * \param pident Pen identity information + * \param[out] valuator_5 Meaning of the valuator with offset 5, if any + * (written only if known and if the device has a 6th axis, + * e.g., for the Wacom Art Pen and Wacom Airbrush Pen) + * \param[out] axes Bitmask of all possibly supported axes + * + * This function identifies Wacom device types through a Wacom-specific device ID. + * It then fills in pen details from an internal database. + * If the device seems to be a Wacom pen/eraser but can't be identified, the function + * leaves "axes" untouched and sets the other outputs to common defaults. + * + * There is no explicit support for other vendors, though vendors that + * emulate the Wacom API might be supported. + * + * Unsupported devices will keep the default settings. + */ +static void xinput2_vendor_peninfo(SDL_VideoDevice *_this, const XIDeviceInfo *dev, SDL_Pen *pen, pen_identity pident, int *valuator_5, Uint32 *axes) +{ + switch (pident.vendor) { + case SDL_PEN_VENDOR_WACOM: + { + if (SDL_PenModifyForWacomID(pen, pident.devicetype_id, axes)) { + if (*axes & SDL_PEN_AXIS_SLIDER_MASK) { + /* Air Brush Pen or eraser */ + *valuator_5 = SDL_PEN_AXIS_SLIDER; + } else if (*axes & SDL_PEN_AXIS_ROTATION_MASK) { + /* Art Pen or eraser, or 6D Art Pen */ + *valuator_5 = SDL_PEN_AXIS_ROTATION; + } + return; + } else { +#if DEBUG_PEN + printf("[pen] Could not identify wacom pen %d with device id %x, using default settings\n", + pident.deviceid, pident.devicetype_id); +#endif + break; + } + } + + default: +#if DEBUG_PEN + printf("[pen] Pen %d is not from a known vendor\n", pident.deviceid); +#endif + break; + } + + /* Fall back to default heuristics for identifying device type */ + + SDL_strlcpy(pen->name, dev->name, SDL_PEN_MAX_NAME); + + pen->type = pident.heuristic_type; +} + +/* Does this device have a valuator for pressure sensitivity? */ +static SDL_bool xinput2_device_is_pen(const XIDeviceInfo *dev) +{ + int classct; + for (classct = 0; classct < dev->num_classes; ++classct) { + const XIAnyClassInfo *classinfo = dev->classes[classct]; + + switch (classinfo->type) { + case XIValuatorClass: + { + XIValuatorClassInfo *val_classinfo = (XIValuatorClassInfo *)classinfo; + Atom vname = val_classinfo->label; + + if (vname == pen_atoms.abs_pressure) { + return SDL_TRUE; + } + } + } + } + return SDL_FALSE; +} + +void X11_InitPen(SDL_VideoDevice *_this) +{ + SDL_VideoData *data = (SDL_VideoData *)_this->driverdata; + int i; + XIDeviceInfo *device_info; + int num_device_info; + + device_info = X11_XIQueryDevice(data->display, XIAllDevices, &num_device_info); + if (!device_info) { + return; + } + + /* Reset the device id -> pen map */ + if (pen_map.entries) { + SDL_free(pen_map.entries); + pen_map.entries = NULL; + pen_map.num_entries = 0; + } + + SDL_PenGCMark(); + + for (i = 0; i < num_device_info; ++i) { + const XIDeviceInfo *dev = &device_info[i]; + int classct; + xinput2_pen pen_device; + Uint32 capabilities = 0; + Uint32 axis_mask = ~0; /* Permitted axes (default: all) */ + int valuator_5_axis = -1; /* For Wacom devices, the 6th valuator (offset 5) has a model-specific meaning */ + pen_identity pident; + SDL_PenID pen_id; + SDL_Pen *pen; + int old_num_pens_known = pen_map.num_pens_known; + int k; + + /* Only track physical devices that are enabled */ + if (dev->use != XISlavePointer || dev->enabled == 0 || !xinput2_device_is_pen(dev)) { + continue; + } + + pen_device.slider_bias = 0.0f; + pen_device.rotation_bias = 0.0f; + for (k = 0; k < SDL_PEN_NUM_AXES; ++k) { + pen_device.valuator_for_axis[k] = SDL_PEN_AXIS_VALUATOR_MISSING; + } + + pident = xinput2_identify_pen(_this, dev->deviceid, dev->name); + + pen_id = SDL_GetPenFromGUID(pident.guid); + if (pen_id == SDL_PEN_INVALID) { + /* We have never met this pen */ + pen_id = ++pen_map.num_pens_known; /* start at 1 */ + } + pen = SDL_PenModifyBegin(pen_id); + + /* Complement XF86 driver information with vendor-specific details */ + xinput2_vendor_peninfo(_this, dev, pen, pident, &valuator_5_axis, &axis_mask); + + for (classct = 0; classct < dev->num_classes; ++classct) { + const XIAnyClassInfo *classinfo = dev->classes[classct]; + + switch (classinfo->type) { + case XIValuatorClass: + { + XIValuatorClassInfo *val_classinfo = (XIValuatorClassInfo *)classinfo; + Sint8 valuator_nr = val_classinfo->number; + Atom vname = val_classinfo->label; + int axis = -1; + + float min = val_classinfo->min; + float max = val_classinfo->max; + + if (vname == pen_atoms.abs_pressure) { + axis = SDL_PEN_AXIS_PRESSURE; + } else if (vname == pen_atoms.abs_tilt_x) { + axis = SDL_PEN_AXIS_XTILT; + } else if (vname == pen_atoms.abs_tilt_y) { + axis = SDL_PEN_AXIS_YTILT; + } + + if (axis == -1 && valuator_nr == 5) { + /* Wacom model-specific axis support */ + /* The meaning of the various axes is highly underspecitied in Xinput2. + * As of 2023-08-26, Wacom seems to be the only vendor to support these axes, so the code below + * captures the de-facto standard. */ + axis = valuator_5_axis; + + switch (axis) { + case SDL_PEN_AXIS_SLIDER: + /* cf. xinput2_wacom_peninfo for how this axis is used. + In all current cases, our API wants this value in 0..1, but the xf86 driver + starts at a negative offset, so we normalise here. */ + pen_device.slider_bias = -min; + max -= min; + min = 0.0f; + break; + + case SDL_PEN_AXIS_ROTATION: + /* The "0" value points to the left, rather than up, so we must + rotate 90 degrees counter-clockwise to have 0 point to the top. */ + + pen_device.rotation_bias = -90.0f; + break; + + default: + break; + } + } + + if (axis >= 0) { + capabilities |= SDL_PEN_AXIS_CAPABILITY(axis); + + pen_device.valuator_for_axis[axis] = valuator_nr; + pen_device.axis_min[axis] = min; + pen_device.axis_max[axis] = max; + } + break; + } + default: + break; + } + } + + /* We have a pen if and only if the device measures pressure */ + if (capabilities & SDL_PEN_AXIS_PRESSURE_MASK) { + xinput2_pen *xinput2_deviceinfo; + Uint64 guid_a, guid_b; + + /* Done collecting data, write to pen */ + SDL_PenModifyAddCapabilities(pen, capabilities); + pen->guid = pident.guid; + + if (pen->deviceinfo) { + /* Updating a known pen */ + xinput2_deviceinfo = (xinput2_pen *)pen->deviceinfo; + xinput2_merge_deviceinfo(xinput2_deviceinfo, &pen_device); + } else { + /* Registering a new pen */ + xinput2_deviceinfo = SDL_malloc(sizeof(xinput2_pen)); + SDL_memcpy(xinput2_deviceinfo, &pen_device, sizeof(xinput2_pen)); + } + pen->deviceinfo = xinput2_deviceinfo; + +#if DEBUG_PEN + printf("[pen] pen %d [%04x] valuators pressure=%d, xtilt=%d, ytilt=%d [%s]\n", + pen->header.id, pen->header.flags, + pen_device.valuator_for_axis[SDL_PEN_AXIS_PRESSURE], + pen_device.valuator_for_axis[SDL_PEN_AXIS_XTILT], + pen_device.valuator_for_axis[SDL_PEN_AXIS_YTILT], + pen->name); +#endif + SDL_memcpy(&guid_a, &pen->guid.data[0], 8); + SDL_memcpy(&guid_b, &pen->guid.data[8], 8); + if (!(guid_a | guid_b)) { +#if DEBUG_PEN + printf("[pen] (pen eliminated due to zero GUID)\n"); +#endif + pen->type = SDL_PEN_TYPE_NONE; + } + + } else { + /* Not a pen, mark for deletion */ + pen->type = SDL_PEN_TYPE_NONE; + } + SDL_PenModifyEnd(pen, SDL_TRUE); + + if (pen->type != SDL_PEN_TYPE_NONE) { + const int map_pos = pen_map.num_entries; + + /* We found a pen: add mapping */ + if (pen_map.entries == NULL) { + pen_map.entries = SDL_calloc(sizeof(struct pen_device_id_mapping), 1); + pen_map.num_entries = 1; + } else { + pen_map.num_entries += 1; + pen_map.entries = SDL_realloc(pen_map.entries, + pen_map.num_entries * (sizeof(struct pen_device_id_mapping))); + } + pen_map.entries[map_pos].deviceid = dev->deviceid; + pen_map.entries[map_pos].pen_id = pen_id; + } else { + /* Revert pen number allocation */ + pen_map.num_pens_known = old_num_pens_known; + } + } + X11_XIFreeDeviceInfo(device_info); + + SDL_PenGCSweep(NULL, xinput2_pen_free_deviceinfo); +} + +static void xinput2_normalize_pen_axes(const SDL_Pen *peninfo, + const xinput2_pen *xpen, + /* inout-mode paramters: */ + float *coords) +{ + int axis; + + /* Normalise axes */ + for (axis = 0; axis < SDL_PEN_NUM_AXES; ++axis) { + int valuator = xpen->valuator_for_axis[axis]; + if (valuator != SDL_PEN_AXIS_VALUATOR_MISSING) { + float value = coords[axis]; + float min = xpen->axis_min[axis]; + float max = xpen->axis_max[axis]; + + if (axis == SDL_PEN_AXIS_SLIDER) { + value += xpen->slider_bias; + } + + /* min ... 0 ... max */ + if (min < 0.0) { + /* Normalise so that 0 remains 0.0 */ + if (value < 0) { + value = value / (-min); + } else { + if (max == 0.0) { + value = 0.0f; + } else { + value = value / max; + } + } + } else { + /* 0 ... min ... max */ + /* including 0.0 = min */ + if (max == 0.0) { + value = 0.0f; + } else { + value = (value - min) / max; + } + } + + switch (axis) { + case SDL_PEN_AXIS_XTILT: + case SDL_PEN_AXIS_YTILT: + if (peninfo->info.max_tilt > 0.0f) { + value *= peninfo->info.max_tilt; /* normalise to physical max */ + } + break; + + case SDL_PEN_AXIS_ROTATION: + /* normalised to -1..1, so let's convert to degrees */ + value *= 180.0; + value += xpen->rotation_bias; + + /* handle simple over/underflow */ + if (value >= 180.0f) { + value -= 360.0f; + } else if (value < -180.0f) { + value += 360.0f; + } + break; + + default: + break; + } + coords[axis] = value; + } + } +} + +void X11_PenAxesFromValuators(const SDL_Pen *peninfo, + const double *input_values, const unsigned char *mask, const int mask_len, + /* out-mode parameters: */ + float axis_values[SDL_PEN_NUM_AXES]) +{ + const xinput2_pen *pen = (xinput2_pen *)peninfo->deviceinfo; + int i; + + for (i = 0; i < SDL_PEN_NUM_AXES; ++i) { + const int valuator = pen->valuator_for_axis[i]; + if (valuator == SDL_PEN_AXIS_VALUATOR_MISSING || valuator >= mask_len * 8 || !(XIMaskIsSet(mask, valuator))) { + axis_values[i] = 0.0f; + } else { + axis_values[i] = input_values[valuator]; + } + } + xinput2_normalize_pen_axes(peninfo, pen, axis_values); +} + +#endif /* SDL_VIDEO_DRIVER_X11_XINPUT2 */ + +/* vi: set ts=4 sw=4 expandtab: */ diff --git a/src/video/x11/SDL_x11pen.h b/src/video/x11/SDL_x11pen.h new file mode 100644 index 0000000000..04de528c87 --- /dev/null +++ b/src/video/x11/SDL_x11pen.h @@ -0,0 +1,54 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2023 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ +#include "../../SDL_internal.h" + +#ifndef SDL_x11pen_h_ +#define SDL_x11pen_h_ + +#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2 + +#include "SDL_x11video.h" +#include "../../events/SDL_pen_c.h" + +/* Pressure-sensitive pen */ + +/* Forward definition for SDL_x11video.h */ +struct SDL_VideoData; + +/* Function definitions */ + +/* Detect XINPUT2 devices that are pens / erasers, or update the list after hotplugging */ +extern void X11_InitPen(SDL_VideoDevice *_this); + +/* Converts XINPUT2 valuators into pen axis information, including normalisation */ +extern void X11_PenAxesFromValuators(const SDL_Pen *pen, + const double *input_values, const unsigned char *mask, const int mask_len, + /* out-mode parameters: */ + float axis_values[SDL_PEN_NUM_AXES]); + +/* Map X11 device ID to pen ID */ +extern int X11_PenIDFromDeviceID(int deviceid); + +#endif /* SDL_VIDEO_DRIVER_X11_XINPUT2 */ + +#endif /* SDL_x11pen_h_ */ + +/* vi: set ts=4 sw=4 expandtab: */ diff --git a/src/video/x11/SDL_x11sym.h b/src/video/x11/SDL_x11sym.h index fea44c9b86..67f74d4b0f 100644 --- a/src/video/x11/SDL_x11sym.h +++ b/src/video/x11/SDL_x11sym.h @@ -289,6 +289,7 @@ SDL_X11_SYM(Status,XIQueryVersion,(Display *a,int *b,int *c),(a,b,c),return) SDL_X11_SYM(XIEventMask*,XIGetSelectedEvents,(Display *a,Window b,int *c),(a,b,c),return) SDL_X11_SYM(Bool,XIGetClientPointer,(Display *a,Window b,int *c),(a,b,c),return) SDL_X11_SYM(Bool,XIWarpPointer,(Display *a,int b,Window c,Window d,double e,double f,int g,int h,double i,double j),(a,b,c,d,e,f,g,h,i,j),return) +SDL_X11_SYM(Status,XIGetProperty,(Display *a,int b,Atom c,long d,long e,Bool f, Atom g, Atom *h, int *i, unsigned long *j, unsigned long *k, unsigned char **l),(a,b,c,d,e,f,g,h,i,j,k,l),return); #endif /* XRandR support */ diff --git a/src/video/x11/SDL_x11video.c b/src/video/x11/SDL_x11video.c index a27e409815..1def27f26c 100644 --- a/src/video/x11/SDL_x11video.c +++ b/src/video/x11/SDL_x11video.c @@ -24,16 +24,17 @@ #include /* For getpid() and readlink() */ -#include "../SDL_sysvideo.h" -#include "../SDL_pixels_c.h" #include "../../core/linux/SDL_system_theme.h" +#include "../SDL_pixels_c.h" +#include "../SDL_sysvideo.h" -#include "SDL_x11video.h" #include "SDL_x11framebuffer.h" +#include "SDL_x11pen.h" #include "SDL_x11shape.h" #include "SDL_x11touch.h" -#include "SDL_x11xinput2.h" +#include "SDL_x11video.h" #include "SDL_x11xfixes.h" +#include "SDL_x11xinput2.h" #ifdef SDL_VIDEO_OPENGL_EGL #include "SDL_x11opengles.h" @@ -432,6 +433,10 @@ int X11_VideoInit(SDL_VideoDevice *_this) X11_InitTouch(_this); +#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2 + X11_InitPen(_this); +#endif + return 0; } diff --git a/src/video/x11/SDL_x11window.c b/src/video/x11/SDL_x11window.c index 459a55a55f..1f3e9090db 100644 --- a/src/video/x11/SDL_x11window.c +++ b/src/video/x11/SDL_x11window.c @@ -592,8 +592,8 @@ int X11_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window) X11_ConstrainPopup(window); } SDL_RelativeToGlobalForWindow(window, - window->windowed.x, window->windowed.y, - &win_x, &win_y); + window->windowed.x, window->windowed.y, + &win_x, &win_y); /* Always create this with the window->windowed.* fields; if we're creating a windowed mode window, that's fine. If we're creating a @@ -754,12 +754,20 @@ int X11_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window) X11_Xinput2SelectTouch(_this, window); - X11_XSelectInput(display, w, - (FocusChangeMask | EnterWindowMask | LeaveWindowMask | - ExposureMask | ButtonPressMask | ButtonReleaseMask | - PointerMotionMask | KeyPressMask | KeyReleaseMask | - PropertyChangeMask | StructureNotifyMask | - KeymapStateMask | fevent)); + { + unsigned int x11_pointer_events = ButtonPressMask | ButtonReleaseMask | PointerMotionMask; + if (X11_Xinput2SelectMouse(_this, window)) { + /* If XInput2 can handle pointer events, we don't track them here */ + x11_pointer_events = 0; + } + + X11_XSelectInput(display, w, + (FocusChangeMask | EnterWindowMask | LeaveWindowMask | ExposureMask | + x11_pointer_events | + KeyPressMask | KeyReleaseMask | + PropertyChangeMask | StructureNotifyMask | + KeymapStateMask | fevent)); + } /* For _ICC_PROFILE. */ X11_XSelectInput(display, RootWindow(display, screen), PropertyChangeMask); @@ -832,7 +840,8 @@ static int X11_CatchAnyError(Display *d, XErrorEvent *e) return 0; } -enum check_method { +enum check_method +{ COMPARE_POSITION = 1, COMPARE_SIZE = 2, COMPARE_DOUBLE_ATTEMPT = 3, @@ -842,8 +851,8 @@ enum check_method { /* Wait a brief time, or not, to see if the window manager decided to move/resize the window. * Send MOVED and RESIZED window events */ static void X11_WaitAndSendWindowEvents(SDL_Window *window, int param_timeout, enum check_method method, - int orig_x, int orig_y, int dest_x, int dest_y, - int orig_w, int orig_h, int dest_w, int dest_h) + int orig_x, int orig_y, int dest_x, int dest_y, + int orig_w, int orig_h, int dest_w, int dest_h) { SDL_WindowData *data = window->driverdata; Display *display = data->videodata->display; @@ -934,7 +943,6 @@ static void X11_WaitAndSendWindowEvents(SDL_Window *window, int param_timeout, e caught_x11_error = SDL_FALSE; } - int X11_SetWindowIcon(SDL_VideoDevice *_this, SDL_Window *window, SDL_Surface *icon) { SDL_WindowData *data = window->driverdata; @@ -974,8 +982,8 @@ int X11_SetWindowIcon(SDL_VideoDevice *_this, SDL_Window *window, SDL_Surface *i } X11_XChangeProperty(display, data->xwindow, _NET_WM_ICON, XA_CARDINAL, - 32, PropModeReplace, (unsigned char *)propdata, - propsize); + 32, PropModeReplace, (unsigned char *)propdata, + propsize); SDL_free(propdata); if (caught_x11_error) { @@ -1351,7 +1359,6 @@ void X11_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window) if (data->border_left == 0 && data->border_right == 0 && data->border_top == 0 && data->border_bottom == 0) { X11_GetBorderValues(data); } - } void X11_HideWindow(SDL_VideoDevice *_this, SDL_Window *window) @@ -1485,7 +1492,6 @@ static void X11_SetWindowMaximized(SDL_VideoDevice *_this, SDL_Window *window, S /* Send MOVED/RESIZED event, if needed. Compare with initial position and size. Timeout 1000 */ X11_WaitAndSendWindowEvents(window, 1000, COMPARE_ORIG, orig_x, orig_y, 0, 0, orig_w, orig_h, 0, 0); - } else { X11_SetNetWMState(_this, data->xwindow, window->flags); } diff --git a/src/video/x11/SDL_x11xinput2.c b/src/video/x11/SDL_x11xinput2.c index eb7f89a747..68eaaf5d88 100644 --- a/src/video/x11/SDL_x11xinput2.c +++ b/src/video/x11/SDL_x11xinput2.c @@ -22,9 +22,12 @@ #ifdef SDL_VIDEO_DRIVER_X11 +#include "SDL_x11pen.h" #include "SDL_x11video.h" #include "SDL_x11xinput2.h" +#include "../../events/SDL_events_c.h" #include "../../events/SDL_mouse_c.h" +#include "../../events/SDL_pen_c.h" #include "../../events/SDL_touch_c.h" #define MAX_AXIS 16 @@ -74,19 +77,25 @@ static SDL_bool xinput2_version_atleast(const int version, const int wantmajor, return version >= ((wantmajor * 1000) + wantminor); } -#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH -static SDL_Window *xinput2_get_sdlwindow(SDL_VideoData *videodata, Window window) +static SDL_WindowData *xinput2_get_sdlwindowdata(SDL_VideoData *videodata, Window window) { int i; for (i = 0; i < videodata->numwindows; i++) { SDL_WindowData *d = videodata->windowlist[i]; if (d->xwindow == window) { - return d->window; + return d; } } return NULL; } +static SDL_Window *xinput2_get_sdlwindow(SDL_VideoData *videodata, Window window) +{ + const SDL_WindowData *windowdata = xinput2_get_sdlwindowdata(videodata, window); + return windowdata ? windowdata->window : NULL; +} + +#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH static void xinput2_normalize_touch_coordinates(SDL_Window *window, double in_x, double in_y, float *out_x, float *out_y) { if (window) { @@ -262,18 +271,51 @@ static SDL_XInput2DeviceInfo *xinput2_get_device_info(SDL_VideoData *videodata, return devinfo; } + +static void xinput2_pen_ensure_window(SDL_VideoDevice *_this, const SDL_Pen *pen, Window window) +{ + /* When "flipping" a Wacom eraser pen, we get an XI_DeviceChanged event + * with the newly-activated pen, but this event is global for the display. + * We won't get a window until the pen starts triggering motion or + * button events, so we instead hook the pen to its window at that point. */ + const SDL_WindowData *windowdata = X11_FindWindow(_this, window); + if (windowdata) { + SDL_SendPenWindowEvent(0, pen->header.id, windowdata->window); + } +} #endif -int X11_HandleXinput2Event(SDL_VideoData *videodata, XGenericEventCookie *cookie) +int X11_HandleXinput2Event(SDL_VideoDevice *_this, XGenericEventCookie *cookie) { #ifdef SDL_VIDEO_DRIVER_X11_XINPUT2 + SDL_VideoData *videodata = (SDL_VideoData *)_this->driverdata; + if (cookie->extension != xinput2_opcode) { return 0; } switch (cookie->evtype) { + case XI_PropertyEvent: + case XI_DeviceChanged: + { + X11_InitPen(_this); + } break; + + case XI_Enter: + case XI_Leave: + { + const XIEnterEvent *enterev = (const XIEnterEvent *)cookie->data; + const SDL_WindowData *windowdata = X11_FindWindow(_this, enterev->event); + const SDL_Pen *pen = SDL_GetPenPtr(X11_PenIDFromDeviceID(enterev->sourceid)); + SDL_Window *window = (windowdata && (cookie->evtype == XI_Enter)) ? windowdata->window : NULL; + if (pen) { + SDL_SendPenWindowEvent(0, pen->header.id, window); + } + } break; + case XI_RawMotion: { const XIRawEvent *rawev = (const XIRawEvent *)cookie->data; + const SDL_bool is_pen = X11_PenIDFromDeviceID(rawev->sourceid) != SDL_PEN_INVALID; SDL_Mouse *mouse = SDL_GetMouse(); SDL_XInput2DeviceInfo *devinfo; double coords[2]; @@ -281,6 +323,11 @@ int X11_HandleXinput2Event(SDL_VideoData *videodata, XGenericEventCookie *cookie int i; videodata->global_mouse_changed = SDL_TRUE; + if (is_pen) { + return 0; /* Pens check for XI_Motion instead */ + } + + /* Non-pen: */ if (!mouse->relative_mode || mouse->relative_mode_warp) { return 0; @@ -317,6 +364,7 @@ int X11_HandleXinput2Event(SDL_VideoData *videodata, XGenericEventCookie *cookie xinput2_remove_device_info(videodata, hierev->info[i].deviceid); } } + X11_InitPen(_this); } break; case XI_RawButtonPress: @@ -326,17 +374,94 @@ int X11_HandleXinput2Event(SDL_VideoData *videodata, XGenericEventCookie *cookie case XI_RawTouchUpdate: case XI_RawTouchEnd: #endif + { videodata->global_mouse_changed = SDL_TRUE; - break; + } break; -#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH - /* With multitouch, register to receive XI_Motion (which desctivates MotionNotify), - * so that we can distinguish real mouse motions from synthetic one. */ + case XI_ButtonPress: + case XI_ButtonRelease: + { + const XIDeviceEvent *xev = (const XIDeviceEvent *)cookie->data; + const SDL_Pen *pen = SDL_GetPenPtr(X11_PenIDFromDeviceID(xev->deviceid)); + const int button = xev->detail; + const SDL_bool pressed = (cookie->evtype == XI_ButtonPress) ? SDL_TRUE : SDL_FALSE; + + if (pen) { + xinput2_pen_ensure_window(_this, pen, xev->event); + + /* Only report button event; if there was also pen movement / pressure changes, we expect + an XI_Motion event first anyway */ + if (button == 1) { + /* button 1 is the pen tip */ + if (pressed && SDL_PenPerformHitTest()) { + /* Check whether we should handle window resize / move events */ + const SDL_WindowData *windowdata = X11_FindWindow(_this, xev->event); + + if (X11_ProcessHitTest(_this, windowdata, pen->last.x, pen->last.y)) { + SDL_SendWindowEvent(windowdata->window, SDL_EVENT_WINDOW_HIT_TEST, 0, 0); + return 1; /* Don't pass on this event */ + } + } + SDL_SendPenTipEvent(0, pen->header.id, + pressed ? SDL_PRESSED : SDL_RELEASED); + } else { + SDL_SendPenButton(0, pen->header.id, + pressed ? SDL_PRESSED : SDL_RELEASED, + button - 1); + } + return 1; + } else { + /* Otherwise assume a regular mouse */ + SDL_WindowData *windowdata = xinput2_get_sdlwindowdata(videodata, xev->event); + + if (xev->deviceid != xev->sourceid) { + /* Discard events from "Master" devices to avoid duplicates. */ + return 1; + } + + if (pressed) { + X11_HandleButtonPress(_this, windowdata, button, + xev->event_x, xev->event_y, xev->time); + } else { + X11_HandleButtonRelease(_this, windowdata, button); + } + } + } break; + + /* Register to receive XI_Motion (which deactivates MotionNotify), so that we can distinguish + real mouse motions from synthetic ones, for multitouch and pen support. */ case XI_Motion: { const XIDeviceEvent *xev = (const XIDeviceEvent *)cookie->data; + const SDL_Pen *pen = SDL_GetPenPtr(X11_PenIDFromDeviceID(xev->deviceid)); +#if SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH int pointer_emulated = (xev->flags & XIPointerEmulated); +#endif /* SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH */ + if (xev->deviceid != xev->sourceid) { + /* Discard events from "Master" devices to avoid duplicates. */ + return 1; + } + + if (pen) { + SDL_PenStatusInfo pen_status; + + pen_status.x = xev->event_x; + pen_status.y = xev->event_y; + + X11_PenAxesFromValuators(pen, + xev->valuators.values, xev->valuators.mask, xev->valuators.mask_len, + &pen_status.axes[0]); + + xinput2_pen_ensure_window(_this, pen, xev->event); + + SDL_SendPenMotion(0, pen->header.id, + SDL_TRUE, + &pen_status); + return 1; + } + +#if SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH if (!pointer_emulated) { SDL_Mouse *mouse = SDL_GetMouse(); if (!mouse->relative_mode || mouse->relative_mode_warp) { @@ -347,8 +472,10 @@ int X11_HandleXinput2Event(SDL_VideoData *videodata, XGenericEventCookie *cookie } } return 1; +#endif /* SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH */ } break; +#if SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH case XI_TouchBegin: { const XIDeviceEvent *xev = (const XIDeviceEvent *)cookie->data; @@ -376,10 +503,9 @@ int X11_HandleXinput2Event(SDL_VideoData *videodata, XGenericEventCookie *cookie SDL_SendTouchMotion(0, xev->sourceid, xev->detail, window, x, y, 1.0); return 1; } break; - -#endif +#endif /* SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH */ } -#endif +#endif /* SDL_VIDEO_DRIVER_X11_XINPUT2 */ return 0; } @@ -460,6 +586,38 @@ int X11_Xinput2IsInitialized(void) #endif } +SDL_bool X11_Xinput2SelectMouse(SDL_VideoDevice *_this, SDL_Window *window) +{ +#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2 + const SDL_VideoData *data = (SDL_VideoData *)_this->driverdata; + XIEventMask eventmask; + unsigned char mask[4] = { 0, 0, 0, 0 }; + SDL_WindowData *window_data = (SDL_WindowData *)window->driverdata; + + eventmask.mask_len = sizeof(mask); + eventmask.mask = mask; + eventmask.deviceid = XIAllDevices; + + XISetMask(mask, XI_ButtonPress); + XISetMask(mask, XI_ButtonRelease); + XISetMask(mask, XI_Motion); + XISetMask(mask, XI_Enter); + XISetMask(mask, XI_Leave); + /* Hotplugging: */ + XISetMask(mask, XI_DeviceChanged); + XISetMask(mask, XI_HierarchyChanged); + XISetMask(mask, XI_PropertyEvent); /* E.g., when swapping tablet pens */ + + if (X11_XISelectEvents(data->display, + window_data->xwindow, + &eventmask, 1) == Success) { + return SDL_TRUE; + } + SDL_LogWarn(SDL_LOG_CATEGORY_INPUT, "Could not enable XInput2 mouse event handling\n"); +#endif + return SDL_FALSE; +} + int X11_Xinput2IsMultitouchSupported(void) { #ifdef SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_MULTITOUCH diff --git a/src/video/x11/SDL_x11xinput2.h b/src/video/x11/SDL_x11xinput2.h index 2d4573a81a..4fea6465b4 100644 --- a/src/video/x11/SDL_x11xinput2.h +++ b/src/video/x11/SDL_x11xinput2.h @@ -32,11 +32,12 @@ typedef struct XGenericEventCookie XGenericEventCookie; extern void X11_InitXinput2(SDL_VideoDevice *_this); extern void X11_InitXinput2Multitouch(SDL_VideoDevice *_this); -extern int X11_HandleXinput2Event(SDL_VideoData *videodata, XGenericEventCookie *cookie); +extern int X11_HandleXinput2Event(SDL_VideoDevice *_this, XGenericEventCookie *cookie); extern int X11_Xinput2IsInitialized(void); extern int X11_Xinput2IsMultitouchSupported(void); extern void X11_Xinput2SelectTouch(SDL_VideoDevice *_this, SDL_Window *window); extern void X11_Xinput2GrabTouch(SDL_VideoDevice *_this, SDL_Window *window); extern void X11_Xinput2UngrabTouch(SDL_VideoDevice *_this, SDL_Window *window); +extern SDL_bool X11_Xinput2SelectMouse(SDL_VideoDevice *_this, SDL_Window *window); #endif /* SDL_x11xinput2_h_ */ diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index bac37027a2..9171dfd1cd 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -328,6 +328,7 @@ add_sdl_test_executable(testgles2 SOURCES testgles2.c) add_sdl_test_executable(testgles2_sdf NEEDS_RESOURCES TESTUTILS SOURCES testgles2_sdf.c) add_sdl_test_executable(testhaptic SOURCES testhaptic.c) add_sdl_test_executable(testhotplug SOURCES testhotplug.c) +add_sdl_test_executable(testpen SOURCES testpen.c) add_sdl_test_executable(testrumble SOURCES testrumble.c) add_sdl_test_executable(testthread NONINTERACTIVE NONINTERACTIVE_TIMEOUT 40 SOURCES testthread.c) add_sdl_test_executable(testiconv NEEDS_RESOURCES TESTUTILS SOURCES testiconv.c) diff --git a/test/testautomation.c b/test/testautomation.c index 9a5bf803db..40dda20c49 100644 --- a/test/testautomation.c +++ b/test/testautomation.c @@ -33,6 +33,7 @@ static SDLTest_TestSuiteReference *testSuites[] = { &mainTestSuite, &mathTestSuite, &mouseTestSuite, + &penTestSuite, &pixelsTestSuite, &platformTestSuite, &propertiesTestSuite, diff --git a/test/testautomation_pen.c b/test/testautomation_pen.c new file mode 100644 index 0000000000..d200656603 --- /dev/null +++ b/test/testautomation_pen.c @@ -0,0 +1,1908 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2023 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#include + +/** + * Pen test suite + */ + +#define SDL_internal_h_ /* Inhibit dynamic symbol redefinitions that clash with ours */ + +/* ================= System Under Test (SUT) ================== */ +/* Renaming SUT operations to avoid link-time symbol clashes */ +#define SDL_GetPens SDL_SUT_GetPens +#define SDL_GetPenStatus SDL_SUT_GetPenStatus +#define SDL_GetPenFromGUID SDL_SUT_GetPenFromGUID +#define SDL_GetPenGUID SDL_SUT_GetPenGUID +#define SDL_PenConnected SDL_SUT_PenConnected +#define SDL_GetPenName SDL_SUT_GetPenName +#define SDL_GetPenCapabilities SDL_SUT_GetPenCapabilities +#define SDL_GetPenType SDL_SUT_GetPenType + +#define SDL_GetPenPtr SDL_SUT_GetPenPtr +#define SDL_PenModifyBegin SDL_SUT_PenModifyBegin +#define SDL_PenModifyAddCapabilities SDL_SUT_PenModifyAddCapabilities +#define SDL_PenModifyForWacomID SDL_SUT_PenModifyForWacomID +#define SDL_PenUpdateGUIDForWacom SDL_SUT_PenUpdateGUIDForWacom +#define SDL_PenUpdateGUIDForType SDL_SUT_PenUpdateGUIDForType +#define SDL_PenUpdateGUIDForGeneric SDL_SUT_PenUpdateGUIDForGeneric +#define SDL_PenModifyEnd SDL_SUT_PenModifyEnd +#define SDL_PenGCMark SDL_SUT_PenGCMark +#define SDL_PenGCSweep SDL_SUT_PenGCSweep +#define SDL_SendPenMotion SDL_SUT_SendPenMotion +#define SDL_SendPenButton SDL_SUT_SendPenButton +#define SDL_SendPenTipEvent SDL_SUT_SendPenTipEvent +#define SDL_SendPenWindowEvent SDL_SUT_SendPenWindowEvent +#define SDL_PenPerformHitTest SDL_SUT_PenPerformHitTest +#define SDL_PenInit SDL_SUT_PenInit + +/* ================= Mock API ================== */ + +#include +#include +#include +/* For SDL_Window, SDL_Mouse, SDL_MouseID: */ +#include "../src/events/SDL_mouse_c.h" +/* Divert calls to mock mouse API: */ +#define SDL_SendMouseMotion SDL_Mock_SendMouseMotion +#define SDL_SendMouseButton SDL_Mock_SendMouseButton +#define SDL_GetMouse SDL_Mock_GetMouse +#define SDL_MousePositionInWindow SDL_Mock_MousePositionInWindow +#define SDL_SetMouseFocus SDL_Mock_SetMouseFocus + +/* Mock mouse API */ +static int SDL_SendMouseMotion(Uint64 timestamp, SDL_Window *window, SDL_MouseID mouseID, int relative, float x, float y); +static int SDL_SendMouseButton(Uint64 timestamp, SDL_Window *window, SDL_MouseID mouseID, Uint8 state, Uint8 button); +static SDL_Mouse *SDL_GetMouse(void); +static SDL_bool SDL_MousePositionInWindow(SDL_Window *window, SDL_MouseID mouseID, float x, float y); +static void SDL_SetMouseFocus(SDL_Window *window); + +/* Import SUT code with macro-renamed function names */ +#define SDL_waylanddyn_h_ /* hack: suppress spurious build problem with libdecor.h on Wayland */ +#include "../src/events/SDL_pen.c" +#include "../src/events/SDL_pen_c.h" + + +/* ================= Internal SDL API Compatibility ================== */ +/* Mock implementations of Pen -> Mouse calls */ +/* Not thread-safe! */ + +static SDL_bool SDL_MousePositionInWindow(SDL_Window *window, SDL_MouseID mouseID, float x, float y) +{ + return SDL_TRUE; +} + +static int _mouseemu_last_event = 0; +static float _mouseemu_last_x = 0.0f; +static float _mouseemu_last_y = 0.0f; +static int _mouseemu_last_mouseid = 0; +static int _mouseemu_last_button = 0; +static int _mouseemu_last_relative = 0; +static int _mouseemu_last_focus = -1; + +static int SDL_SendMouseButton(Uint64 timestamp, SDL_Window *window, SDL_MouseID mouseID, Uint8 state, Uint8 button) +{ + if (mouseID == SDL_PEN_MOUSEID) { + _mouseemu_last_event = (state == SDL_PRESSED) ? SDL_EVENT_MOUSE_BUTTON_DOWN : SDL_EVENT_MOUSE_BUTTON_UP; + _mouseemu_last_button = button; + _mouseemu_last_mouseid = mouseID; + } + return 1; +} + +static int SDL_SendMouseMotion(Uint64 timestamp, SDL_Window *window, SDL_MouseID mouseID, int relative, float x, float y) +{ + if (mouseID == SDL_PEN_MOUSEID) { + _mouseemu_last_event = SDL_EVENT_MOUSE_MOTION; + _mouseemu_last_x = x; + _mouseemu_last_y = y; + _mouseemu_last_mouseid = mouseID; + _mouseemu_last_relative = relative; + } + return 1; +} + +static SDL_Mouse *SDL_GetMouse(void) +{ + static SDL_Mouse dummy_mouse; + + dummy_mouse.focus = NULL; + dummy_mouse.mouseID = 0; + + return &dummy_mouse; +} + +static void SDL_SetMouseFocus(SDL_Window *window) +{ + _mouseemu_last_focus = window ? 1 : 0; +} + +/* ================= Test Case Support ================== */ + +#define PEN_NUM_TEST_IDS 8 + +/* Helper functions */ + +/* Iterate over all pens to find index for pen ID, otherwise -1 */ +static int _pen_iterationFindsPenIDAt(SDL_PenID needle) +{ + int i; + int num_pens = -1; + + SDL_PenID *pens = SDL_GetPens(&num_pens); + /* Check for (a) consistency and (b) ability to handle NULL parameter */ + SDL_PenID *pens2 = SDL_GetPens(NULL); + + SDLTest_AssertCheck(num_pens >= 0, + "SDL_GetPens() yielded %d pens", num_pens); + SDLTest_AssertCheck(pens[num_pens] == 0, + "SDL_GetPens() not 0 terminated (num_pens = %d)", num_pens); + SDLTest_AssertCheck(pens2[num_pens] == 0, + "SDL_GetPens(NULL) not 0 terminated (num_pens = %d)", num_pens); + + for (i = 0; i < num_pens; ++i) { + SDLTest_AssertCheck(pens[i] == pens2[i], + "SDL_GetPens(&i) and SDL_GetPens(NULL) disagree at index %d/%d", i, num_pens); + SDLTest_AssertCheck(pens[i] != SDL_PEN_INVALID, + "Invalid pen ID %08lx at index %d/%d after SDL_GetPens()", (unsigned long) pens[i], i, num_pens); + } + SDL_free(pens2); + + for (i = 0; pens[i]; ++i) { + SDL_PenID pen_id = pens[i]; + + SDLTest_AssertCheck(pen_id != SDL_PEN_INVALID, + "Invalid pen ID %08lx at index %d/%d after SDL_GetPens()", (unsigned long) pen_id, i, num_pens); + if (pen_id == needle) { + SDL_free(pens); + return i; + } + } + SDL_free(pens); + return -1; +} + +/* Retrieve number of pens and sanity-check SDL_GetPens() */ +static int +_num_pens(void) +{ + int num_pens = -1; + SDL_PenID *pens = SDL_GetPens(&num_pens); + SDLTest_AssertCheck(pens != NULL, + "SDL_GetPens() => NULL"); + SDLTest_AssertCheck(num_pens >= 0, + "SDL_GetPens() reports %d pens", num_pens); + SDLTest_AssertCheck(pens[num_pens] == 0, + "SDL_GetPens()[%d] != 0", num_pens); + SDL_free(pens); + return num_pens; +} + +/* Assert number of pens is as expected */ +static void _AssertCheck_num_pens(int expected, char *location) +{ + int num_pens = _num_pens(); + SDLTest_AssertCheck(expected == num_pens, + "Expected SDL_GetPens() =>count = %d, actual = %d: %s", expected, num_pens, location); +} + +/* ---------------------------------------- */ +/* Test device deallocation */ + +typedef struct /* Collection of pen (de)allocation information */ +{ + unsigned int deallocated_id_flags; /* ith bits set to 1 if the ith test_id is deallocated */ + unsigned int deallocated_deviceinfo_flags; /* ith bits set to 1 if deviceinfo as *int with value i was deallocated */ + SDL_PenID ids[PEN_NUM_TEST_IDS]; + SDL_GUID guids[PEN_NUM_TEST_IDS]; + SDL_Window *window; + int num_ids; + int initial_pen_count; +} pen_testdata; + +/* SDL_PenGCSweep(): callback for tracking pen deallocation */ +static void _pen_testdata_callback(Uint32 deviceid, void *deviceinfo, void *tracker_ref) +{ + pen_testdata *tracker = (pen_testdata *)tracker_ref; + int offset = -1; + int i; + + for (i = 0; i < tracker->num_ids; ++i) { + if (deviceid == tracker->ids[i]) { + tracker->deallocated_id_flags |= (1 << i); + } + } + + SDLTest_AssertCheck(deviceinfo != NULL, + "Device %lu has deviceinfo", + (unsigned long) deviceid); + offset = *((int *)deviceinfo); + SDLTest_AssertCheck(offset >= 0 && offset <= 31, + "Device %lu has well-formed deviceinfo %d", + (unsigned long) deviceid, offset); + tracker->deallocated_deviceinfo_flags |= 1 << offset; + SDL_free(deviceinfo); +} + +/* GC Sweep tracking: update "tracker->deallocated_id_flags" and "tracker->deallocated_deviceinfo_flags" to record deallocations */ +static void _pen_trackGCSweep(pen_testdata *tracker) +{ + tracker->deallocated_id_flags = 0; + tracker->deallocated_deviceinfo_flags = 0; + SDL_PenGCSweep(tracker, _pen_testdata_callback); +} + +/* Finds a number of unused pen IDs (does not allocate them). Also initialises GUIDs. */ +static void _pen_unusedIDs(pen_testdata *tracker, int count) +{ + static int guidmod = 0; /* Ensure uniqueness as long as we use no more than 256 test pens */ + Uint32 synthetic_penid = 1000u; + int index = 0; + + tracker->num_ids = count; + SDLTest_AssertCheck(count < PEN_NUM_TEST_IDS, "Test setup: Valid number of test IDs requested: %d", (int)count); + + while (count--) { + int k; + + while (SDL_GetPenPtr(synthetic_penid)) { + ++synthetic_penid; + } + tracker->ids[index] = synthetic_penid; + for (k = 0; k < 15; ++k) { + tracker->guids[index].data[k] = (16 * k) + index; + } + tracker->guids[index].data[15] = ++guidmod; + + ++synthetic_penid; + ++index; + } +} + +#define DEVICEINFO_UNCHANGED -17 + +/* Allocate deviceinfo for pen */ +static void _pen_setDeviceinfo(SDL_Pen *pen, int deviceinfo) +{ + if (deviceinfo == DEVICEINFO_UNCHANGED) { + SDLTest_AssertCheck(pen->deviceinfo != NULL, + "pen->deviceinfo was already set for %p (%lu), as expected", + pen, (unsigned long) pen->header.id); + } else { + int *data = (int *)SDL_malloc(sizeof(int)); + *data = deviceinfo; + + SDLTest_AssertCheck(pen->deviceinfo == NULL, + "pen->deviceinfo was NULL for %p (%lu) when requesting deviceinfo %d", + pen, (unsigned long) pen->header.id, deviceinfo); + + pen->deviceinfo = data; + } + SDL_PenModifyEnd(pen, SDL_TRUE); +} + +/* ---------------------------------------- */ +/* Back up and restore device information */ + +typedef struct deviceinfo_backup +{ + Uint32 deviceid; + void *deviceinfo; + struct deviceinfo_backup *next; +} deviceinfo_backup; + +/* SDL_PenGCSweep(): Helper callback for collecting all deviceinfo records */ +static void _pen_accumulate_gc_sweep(Uint32 deviceid, void *deviceinfo, void *backup_ref) +{ + deviceinfo_backup **db_ref = (deviceinfo_backup **)backup_ref; + deviceinfo_backup *next = *db_ref; + + *db_ref = SDL_calloc(sizeof(deviceinfo_backup), 1); + (*db_ref)->deviceid = deviceid; + (*db_ref)->deviceinfo = deviceinfo; + (*db_ref)->next = next; +} + +/* SDL_PenGCSweep(): Helper callback that must never be called */ +static void _pen_assert_impossible(Uint32 deviceid, void *deviceinfo, void *backup_ref) +{ + SDLTest_AssertCheck(0, "Deallocation for deviceid %lu during enableAndRestore: not expected", + (unsigned long) deviceid); +} + +/* Disable all pens and store their status */ +static deviceinfo_backup *_pen_disableAndBackup(void) +{ + deviceinfo_backup *backup = NULL; + + SDL_PenGCMark(); + SDL_PenGCSweep(&backup, _pen_accumulate_gc_sweep); + return backup; +} + +/* Restore all pens to their previous status */ +static void _pen_enableAndRestore(deviceinfo_backup *backup, int test_marksweep) +{ + if (test_marksweep) { + SDL_PenGCMark(); + } + while (backup) { + SDL_Pen *disabledpen = SDL_GetPenPtr(backup->deviceid); + deviceinfo_backup *next = backup->next; + + SDL_PenModifyEnd(SDL_PenModifyBegin(disabledpen->header.id), + SDL_TRUE); + disabledpen->deviceinfo = backup->deviceinfo; + + SDL_free(backup); + backup = next; + } + if (test_marksweep) { + SDL_PenGCSweep(NULL, _pen_assert_impossible); + } +} + +static struct SDL_Window _test_window = { 0 }; + +/* ---------------------------------------- */ +/* Default set-up and tear down routines */ + +/* Back up existing pens, allocate fresh ones but don't assign them yet */ +static deviceinfo_backup *_setup_test(pen_testdata *ptest, int pens_for_testing) +{ + int i; + deviceinfo_backup *backup; + + /* Get number of pens */ + SDL_free(SDL_GetPens(&ptest->initial_pen_count)); + + /* Provide fake window for window enter/exit simulation */ + _test_window.id = 0x7e57da7a; + _test_window.w = 1600; + _test_window.h = 1200; + ptest->window = &_test_window; + + /* Grab unused pen IDs for testing */ + _pen_unusedIDs(ptest, pens_for_testing); + for (i = 0; i < pens_for_testing; ++i) { + int index = _pen_iterationFindsPenIDAt(ptest->ids[i]); + SDLTest_AssertCheck(-1 == index, + "Registered PenID(%lu) since index %d == -1", + (unsigned long) ptest->ids[i], index); + } + + /* Remove existing pens, but back up */ + backup = _pen_disableAndBackup(); + + _AssertCheck_num_pens(0, "after disabling and backing up all current pens"); + SDLTest_AssertPass("Removed existing pens"); + + return backup; +} + +static void _teardown_test_general(pen_testdata *ptest, deviceinfo_backup *backup, int with_gc_test) +{ + /* Restore previously existing pens */ + _pen_enableAndRestore(backup, with_gc_test); + + /* validate */ + SDLTest_AssertPass("Restored pens to pre-test state"); + _AssertCheck_num_pens(ptest->initial_pen_count, "after restoring all initial pens"); +} + +static void _teardown_test(pen_testdata *ptest, deviceinfo_backup *backup) +{ + _teardown_test_general(ptest, backup, 0); +} + +static void _teardown_test_with_gc(pen_testdata *ptest, deviceinfo_backup *backup) +{ + _teardown_test_general(ptest, backup, 1); +} + +/* ---------------------------------------- */ +/* Pen simulation */ + +#define SIMPEN_ACTION_DONE 0 +#define SIMPEN_ACTION_MOVE_X 1 +#define SIMPEN_ACTION_MOVE_Y 2 +#define SIMPEN_ACTION_AXIS 3 +#define SIMPEN_ACTION_MOTION_EVENT 4 /* epxlicit motion event */ +#define SIMPEN_ACTION_MOTION_EVENT_S 5 /* send motion event but expect it to be suppressed */ +#define SIMPEN_ACTION_PRESS 6 /* implicit update event */ +#define SIMPEN_ACTION_RELEASE 7 /* implicit update event */ +#define SIMPEN_ACTION_DOWN 8 /* implicit update event */ +#define SIMPEN_ACTION_UP 9 /* implicit update event */ +#define SIMPEN_ACTION_ERASER_MODE 10 + +/* Individual action in pen simulation script */ +typedef struct simulated_pen_action +{ + int type; + int pen_index; /* index into the list of simulated pens */ + int index; /* button or axis number, if needed */ + float update; /* x,y; for AXIS, update[0] is the updated axis */ +} simulated_pen_action; + +static simulated_pen_action _simpen_event(int type, int pen_index, int index, float v, int line_nr) +{ + simulated_pen_action action; + action.type = type; + action.pen_index = pen_index; + action.index = index; + action.update = v; + + /* Sanity check-- turned out to be necessary */ + if ((type == SIMPEN_ACTION_PRESS || type == SIMPEN_ACTION_RELEASE) && index == 0) { + SDL_Log("Error: SIMPEN_EVENT_BUTTON must have button > 0 (first button has number 1!), in line %d!", line_nr); + exit(1); + } + return action; +} + +/* STEP is passed in later (C macros use dynamic scoping) */ + +#define SIMPEN_DONE() \ + STEP _simpen_event(SIMPEN_ACTION_DONE, 0, 0, 0.0f, __LINE__) +#define SIMPEN_MOVE(pen_index, x, y) \ + STEP _simpen_event(SIMPEN_ACTION_MOVE_X, (pen_index), 0, (x), __LINE__); \ + STEP _simpen_event(SIMPEN_ACTION_MOVE_Y, (pen_index), 0, (y), __LINE__) + +#define SIMPEN_AXIS(pen_index, axis, y) \ + STEP _simpen_event(SIMPEN_ACTION_AXIS, (pen_index), (axis), (y), __LINE__) + +#define SIMPEN_EVENT_MOTION(pen_index) \ + STEP _simpen_event(SIMPEN_ACTION_MOTION_EVENT, (pen_index), 0, 0.0f, __LINE__) + +#define SIMPEN_EVENT_MOTION_SUPPRESSED(pen_index) \ + STEP _simpen_event(SIMPEN_ACTION_MOTION_EVENT_S, (pen_index), 0, 0.0f, __LINE__) + +#define SIMPEN_EVENT_BUTTON(pen_index, push, button) \ + STEP _simpen_event((push) ? SIMPEN_ACTION_PRESS : SIMPEN_ACTION_RELEASE, (pen_index), (button), 0.0f, __LINE__) + +#define SIMPEN_EVENT_TIP(pen_index, touch, tip) \ + STEP _simpen_event((touch) ? SIMPEN_ACTION_DOWN : SIMPEN_ACTION_UP, (pen_index), tip, 0.0f, __LINE__) + +#define SIMPEN_SET_ERASER(pen_index, eraser_mode) \ + STEP _simpen_event(SIMPEN_ACTION_ERASER_MODE, (pen_index), eraser_mode, 0.0f, __LINE__) + +static void +_pen_dump(const char *prefix, SDL_Pen *pen) +{ + int i; + char *axes_str; + + if (!pen) { + SDL_Log("(NULL pen)"); + return; + } + + axes_str = SDL_strdup(""); + for (i = 0; i < SDL_PEN_NUM_AXES; ++i) { + char *old_axes_str = axes_str; + SDL_asprintf(&axes_str, "%s\t%f", old_axes_str, pen->last.axes[i]); + SDL_free(old_axes_str); + } + SDL_Log("%s: pen %lu (%s): status=%04lx, flags=%lx, x,y=(%f, %f) axes = %s", + prefix, + (unsigned long) pen->header.id, + pen->name, + (unsigned long) pen->last.buttons, + (unsigned long) pen->header.flags, + pen->last.x, pen->last.y, + axes_str); + SDL_free(axes_str); +} + +/* Runs until the next event has been issued or we are done and returns pointer to it. + Returns NULL once we hit SIMPEN_ACTION_DONE. + Updates simulated_pens accordingly. There must be as many simulated_pens as the highest pen_index used in + any of the "steps". + Also validates the internal state with expectations (via SDL_GetPenStatus()) and updates the, but does not poll SDL events. */ +static simulated_pen_action * +_pen_simulate(simulated_pen_action *steps, int *step_counter, SDL_Pen *simulated_pens, int num_pens) +{ + SDL_bool done = SDL_FALSE; + SDL_bool dump_pens = SDL_FALSE; + unsigned int mask; + int pen_nr; + + do { + simulated_pen_action step = steps[*step_counter]; + SDL_Pen *simpen = &simulated_pens[step.pen_index]; + + if (step.pen_index >= num_pens) { + SDLTest_AssertCheck(0, + "Unexpected pen index %d at step %d, action %d", step.pen_index, *step_counter, step.type); + return NULL; + } + + switch (step.type) { + case SIMPEN_ACTION_DONE: + SDLTest_AssertPass("SIMPEN_ACTION_DONE"); + return NULL; + + case SIMPEN_ACTION_MOVE_X: + SDLTest_AssertPass("SIMPEN_ACTION_MOVE_X [pen %d] : y <- %f", step.pen_index, step.update); + simpen->last.x = step.update; + break; + + case SIMPEN_ACTION_MOVE_Y: + SDLTest_AssertPass("SIMPEN_ACTION_MOVE_Y [pen %d] : x <- %f", step.pen_index, step.update); + simpen->last.y = step.update; + break; + + case SIMPEN_ACTION_AXIS: + SDLTest_AssertPass("SIMPEN_ACTION_AXIS [pen %d] : axis[%d] <- %f", step.pen_index, step.index, step.update); + simpen->last.axes[step.index] = step.update; + break; + + case SIMPEN_ACTION_MOTION_EVENT: + done = SDL_TRUE; + SDLTest_AssertCheck(SDL_SendPenMotion(0, simpen->header.id, SDL_TRUE, + &simpen->last), + "SIMPEN_ACTION_MOTION_EVENT [pen %d]", step.pen_index); + break; + + case SIMPEN_ACTION_MOTION_EVENT_S: + SDLTest_AssertCheck(!SDL_SendPenMotion(0, simpen->header.id, SDL_TRUE, + &simpen->last), + "SIMPEN_ACTION_MOTION_EVENT_SUPPRESSED [pen %d]", step.pen_index); + break; + + case SIMPEN_ACTION_PRESS: + mask = (1 << (step.index - 1)); + simpen->last.buttons |= mask; + SDLTest_AssertCheck(SDL_SendPenButton(0, simpen->header.id, SDL_PRESSED, step.index), + "SIMPEN_ACTION_PRESS [pen %d]: button %d (mask %x)", step.pen_index, step.index, mask); + done = SDL_TRUE; + break; + + case SIMPEN_ACTION_RELEASE: + mask = ~(1 << (step.index - 1)); + simpen->last.buttons &= mask; + SDLTest_AssertCheck(SDL_SendPenButton(0, simpen->header.id, SDL_RELEASED, step.index), + "SIMPEN_ACTION_RELEASE [pen %d]: button %d (mask %x)", step.pen_index, step.index, mask); + done = SDL_TRUE; + break; + + case SIMPEN_ACTION_DOWN: + simpen->last.buttons |= SDL_PEN_DOWN_MASK; + SDLTest_AssertCheck(SDL_SendPenTipEvent(0, simpen->header.id, SDL_PRESSED), + "SIMPEN_ACTION_DOWN [pen %d]: (mask %lx)", step.pen_index, SDL_PEN_DOWN_MASK); + done = SDL_TRUE; + break; + + case SIMPEN_ACTION_UP: + simpen->last.buttons &= ~SDL_PEN_DOWN_MASK; + SDLTest_AssertCheck(SDL_SendPenTipEvent(0, simpen->header.id, SDL_RELEASED), + "SIMPEN_ACTION_UP [pen %d]: (mask %lx)", step.pen_index, ~SDL_PEN_DOWN_MASK); + done = SDL_TRUE; + break; + + case SIMPEN_ACTION_ERASER_MODE: { + Uint32 pmask; + SDL_Pen *pen = SDL_PenModifyBegin(simpen->header.id); + + if (step.index) { + pmask = SDL_PEN_ERASER_MASK; + } else { + pmask = SDL_PEN_INK_MASK; + } + + SDL_PenModifyAddCapabilities(pen, pmask); + SDL_PenModifyEnd(pen, SDL_TRUE); + + simpen->header.flags &= ~(SDL_PEN_INK_MASK | SDL_PEN_ERASER_MASK); + simpen->header.flags |= pmask; + break; + } + + default: + SDLTest_AssertCheck(0, + "Unexpected pen simulation action %d", step.type); + return NULL; + } + ++(*step_counter); + } while (!done); + + for (pen_nr = 0; pen_nr < num_pens; ++pen_nr) { + SDL_Pen *simpen = &simulated_pens[pen_nr]; + float x = -1.0f, y = -1.0f; + float axes[SDL_PEN_NUM_AXES]; + Uint32 actual_flags = SDL_GetPenStatus(simpen->header.id, &x, &y, axes, SDL_PEN_NUM_AXES); + int i; + + if (simpen->last.x != x || simpen->last.y != y) { + SDLTest_AssertCheck(0, "Coordinate mismatch in pen %d", pen_nr); + dump_pens = SDL_TRUE; + } + if ((actual_flags & ~(SDL_PEN_INK_MASK | SDL_PEN_ERASER_MASK)) != (simpen->last.buttons & ~(SDL_PEN_INK_MASK | SDL_PEN_ERASER_MASK))) { + SDLTest_AssertCheck(0, "Status mismatch in pen %d (reported: %08x)", pen_nr, (unsigned int)actual_flags); + dump_pens = SDL_TRUE; + } + if ((actual_flags & (SDL_PEN_INK_MASK | SDL_PEN_ERASER_MASK)) != (simpen->header.flags & (SDL_PEN_INK_MASK | SDL_PEN_ERASER_MASK))) { + SDLTest_AssertCheck(0, "Flags mismatch in pen %d (reported: %08x)", pen_nr, (unsigned int)actual_flags); + dump_pens = SDL_TRUE; + } + for (i = 0; i < SDL_PEN_NUM_AXES; ++i) { + if (axes[i] != simpen->last.axes[i]) { + SDLTest_AssertCheck(0, "Axis %d mismatch in pen %d", pen_nr, i); + dump_pens = SDL_TRUE; + } + } + } + + if (dump_pens) { + int i; + for (i = 0; i < num_pens; ++i) { + SDL_Log("==== pen #%d", i); + _pen_dump("expect", simulated_pens + i); + _pen_dump("actual", SDL_GetPenPtr(simulated_pens[i].header.id)); + } + } + + return &steps[(*step_counter) - 1]; +} + +/* Init simulated_pens with suitable initial state */ +static void +_pen_simulate_init(pen_testdata *ptest, SDL_Pen *simulated_pens, int num_pens) +{ + int i; + for (i = 0; i < num_pens; ++i) { + simulated_pens[i] = *SDL_GetPenPtr(ptest->ids[i]); + } +} + +/* ---------------------------------------- */ +/* Other helper functions */ + +/* "standard" pen registration process */ +static SDL_Pen * +_pen_register(SDL_PenID penid, SDL_GUID guid, char *name, Uint32 flags) +{ + SDL_Pen *pen = SDL_PenModifyBegin(penid); + pen->guid = guid; + SDL_strlcpy(pen->name, name, SDL_PEN_MAX_NAME); + SDL_PenModifyAddCapabilities(pen, flags); + return pen; +} + +/* Test whether EXPECTED and ACTUAL of type TY agree. Their C format string must be FMT. + MESSAGE is a string with one format string, passed as ARG0. */ +#define SDLTest_AssertEq1(TY, FMT, EXPECTED, ACTUAL, MESSAGE, ARG0) \ + { \ + TY _t_expect = (EXPECTED); \ + TY _t_actual = (ACTUAL); \ + SDLTest_AssertCheck(_t_expect == _t_actual, "L%d: " MESSAGE ": expected " #EXPECTED " = " FMT ", actual = " FMT, __LINE__, (ARG0), _t_expect, _t_actual); \ + } + +/* ================= Test Case Implementation ================== */ + +/** + * @brief Check basic pen device introduction and iteration, as well as basic queries + * + * @sa SDL_GetPens, SDL_GetPenName, SDL_GetPenCapabilities + */ +static int +pen_iteration(void *arg) +{ + pen_testdata ptest; + int i; + char long_pen_name[SDL_PEN_MAX_NAME + 10]; + const char *name; + deviceinfo_backup *backup; + + /* Check initial pens */ + SDL_PumpEvents(); + SDLTest_AssertPass("SDL_GetPens() => count = %d", _num_pens()); + + /* Grab unused pen IDs for testing */ + backup = _setup_test(&ptest, 3); /* validates that we have zero pens */ + + /* Re-run GC, track deallocations */ + SDL_PenGCMark(); + _pen_trackGCSweep(&ptest); + _AssertCheck_num_pens(0, "after second GC pass"); + SDLTest_AssertCheck(ptest.deallocated_id_flags == 0, "No unexpected device deallocations"); + SDLTest_AssertCheck(ptest.deallocated_deviceinfo_flags == 0, "No unexpected deviceinfo deallocations"); + SDLTest_AssertPass("Validated that GC on empty pen set is idempotent"); + + /* Add three pens, validate */ + SDL_PenGCMark(); + + SDL_memset(long_pen_name, 'x', sizeof(long_pen_name)); /* Include pen name that is too long */ + long_pen_name[sizeof(long_pen_name) - 1] = 0; + + _pen_setDeviceinfo(_pen_register(ptest.ids[0], ptest.guids[0], "pen 0", + SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK), + 16); + _pen_setDeviceinfo(_pen_register(ptest.ids[2], ptest.guids[2], long_pen_name, + SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK), + 20); + _pen_setDeviceinfo(_pen_register(ptest.ids[1], ptest.guids[1], "pen 1", + SDL_PEN_ERASER_MASK | SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_YTILT_MASK), + 24); + _pen_trackGCSweep(&ptest); + + _AssertCheck_num_pens(3, "after allocating three pens"); + + SDLTest_AssertCheck(ptest.deallocated_id_flags == 0, "No unexpected device deallocations"); + SDLTest_AssertCheck(ptest.deallocated_deviceinfo_flags == 0, "No unexpected deviceinfo deallocations"); + + for (i = 0; i < 3; ++i) { + /* Check that all pens are accounted for */ + int index = _pen_iterationFindsPenIDAt(ptest.ids[i]); + SDLTest_AssertCheck(-1 != index, "Found PenID(%lu)", (unsigned long) ptest.ids[i]); + } + SDLTest_AssertPass("Validated that all three pens are indexable"); + + /* Check pen properties */ + SDLTest_AssertCheck(0 == SDL_strcmp("pen 0", SDL_GetPenName(ptest.ids[0])), + "Pen #0 name"); + SDLTest_AssertCheck((SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK) == SDL_GetPenCapabilities(ptest.ids[0], NULL), + "Pen #0 capabilities"); + + SDLTest_AssertCheck(0 == SDL_strcmp("pen 1", SDL_GetPenName(ptest.ids[1])), + "Pen #1 name"); + SDLTest_AssertCheck((SDL_PEN_ERASER_MASK | SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_YTILT_MASK) == SDL_GetPenCapabilities(ptest.ids[1], NULL), + "Pen #1 capabilities"); + + name = SDL_GetPenName(ptest.ids[2]); + SDLTest_AssertCheck(SDL_PEN_MAX_NAME - 1 == SDL_strlen(name), + "Pen #2 name length"); + SDLTest_AssertCheck(0 == SDL_memcmp(name, long_pen_name, SDL_PEN_MAX_NAME - 1), + "Pen #2 name contents"); + SDLTest_AssertCheck((SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK) == SDL_GetPenCapabilities(ptest.ids[2], NULL), + "Pen #2 capabilities"); + SDLTest_AssertPass("Pen registration and basic queries"); + + /* Re-run GC, track deallocations */ + SDL_PenGCMark(); + _pen_trackGCSweep(&ptest); + _AssertCheck_num_pens(0, "after third GC pass"); + SDLTest_AssertCheck(ptest.deallocated_id_flags == 0x07, + "No unexpected device deallocation : %08x", ptest.deallocated_id_flags); + SDLTest_AssertCheck(ptest.deallocated_deviceinfo_flags == 0x01110000, + "No unexpected deviceinfo deallocation : %08x ", ptest.deallocated_deviceinfo_flags); + SDLTest_AssertPass("Validated that GC on empty pen set is idempotent"); + + /* tear down and finish */ + _teardown_test(&ptest, backup); + return TEST_COMPLETED; +} + +static void +_expect_pen_attached(SDL_PenID penid) +{ + SDLTest_AssertCheck(-1 != _pen_iterationFindsPenIDAt(penid), + "Found PenID(%lu)", (unsigned long) penid); + SDLTest_AssertCheck(SDL_PenConnected(penid), + "Pen %lu was attached, as expected", (unsigned long) penid); +} + +static void +_expect_pen_detached(SDL_PenID penid) +{ + SDLTest_AssertCheck(-1 == _pen_iterationFindsPenIDAt(penid), + "Did not find PenID(%lu), as expected", (unsigned long) penid); + SDLTest_AssertCheck(!SDL_PenConnected(penid), + "Pen %lu was detached, as expected", (unsigned long) penid); +} + +#define ATTACHED(i) (1 << (i)) + +static void +_expect_pens_attached_or_detached(SDL_PenID *pen_ids, int ids, Uint32 mask) +{ + int i; + int attached_count = 0; + for (i = 0; i < ids; ++i) { + if (mask & (1 << i)) { + ++attached_count; + _expect_pen_attached(pen_ids[i]); + } else { + _expect_pen_detached(pen_ids[i]); + } + } + _AssertCheck_num_pens(attached_count, "While checking attached/detached status"); +} + +/** + * @brief Check pen device hotplugging + * + * @sa SDL_GetPens, SDL_GetPenName, SDL_GetPenCapabilities, SDL_PenConnected + */ +static int +pen_hotplugging(void *arg) +{ + pen_testdata ptest; + deviceinfo_backup *backup = _setup_test(&ptest, 3); + SDL_GUID checkguid; + + /* Add two pens */ + SDL_PenGCMark(); + + _pen_setDeviceinfo(_pen_register(ptest.ids[0], ptest.guids[0], "pen 0", SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK), + 16); + _pen_setDeviceinfo(_pen_register(ptest.ids[2], ptest.guids[2], "pen 2", SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK), + 24); + _pen_trackGCSweep(&ptest); + + _AssertCheck_num_pens(2, "after allocating two pens (pass 1)"); + SDLTest_AssertCheck(ptest.deallocated_id_flags == 0, "No unexpected device deallocation (pass 1)"); + SDLTest_AssertCheck(ptest.deallocated_deviceinfo_flags == 0, "No unexpected deviceinfo deallocation (pass 1)"); + + _expect_pens_attached_or_detached(ptest.ids, 3, ATTACHED(0) | ATTACHED(2)); + SDLTest_AssertPass("Validated hotplugging (pass 1): attachmend of two pens"); + + /* Introduce pen #1, remove pen #2 */ + SDL_PenGCMark(); + _pen_setDeviceinfo(_pen_register(ptest.ids[0], ptest.guids[0], "pen 0", SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK), + DEVICEINFO_UNCHANGED); + _pen_setDeviceinfo(_pen_register(ptest.ids[1], ptest.guids[1], "pen 1", SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK), + 20); + _pen_trackGCSweep(&ptest); + + _AssertCheck_num_pens(2, "after allocating two pens (pass 2)"); + SDLTest_AssertCheck(ptest.deallocated_id_flags == 0x04, "No unexpected device deallocation (pass 2): %x", ptest.deallocated_id_flags); + SDLTest_AssertCheck(ptest.deallocated_deviceinfo_flags == 0x01000000, "No unexpected deviceinfo deallocation (pass 2): %x", ptest.deallocated_deviceinfo_flags); + + _expect_pens_attached_or_detached(ptest.ids, 3, ATTACHED(0) | ATTACHED(1)); + SDLTest_AssertPass("Validated hotplugging (pass 2): unplug one, attach another"); + + /* Return to previous state (#0 and #2 attached) */ + SDL_PenGCMark(); + + _pen_setDeviceinfo(_pen_register(ptest.ids[0], ptest.guids[0], "pen 0", SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_YTILT), + DEVICEINFO_UNCHANGED); + _pen_setDeviceinfo(_pen_register(ptest.ids[2], ptest.guids[2], "pen 2", SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK), + 24); + _pen_trackGCSweep(&ptest); + + _AssertCheck_num_pens(2, "after allocating two pens (pass 3)"); + SDLTest_AssertCheck(ptest.deallocated_id_flags == 0x02, "No unexpected device deallocation (pass 3)"); + SDLTest_AssertCheck(ptest.deallocated_deviceinfo_flags == 0x00100000, "No unexpected deviceinfo deallocation (pass 3)"); + + _expect_pens_attached_or_detached(ptest.ids, 3, ATTACHED(0) | ATTACHED(2)); + SDLTest_AssertPass("Validated hotplugging (pass 3): return to state of pass 1"); + + /* Introduce pen #1, remove pen #0 */ + SDL_PenGCMark(); + _pen_setDeviceinfo(_pen_register(ptest.ids[1], ptest.guids[1], "pen 1", SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK), + 20); + _pen_setDeviceinfo(_pen_register(ptest.ids[2], ptest.guids[2], "pen 2", SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK), + DEVICEINFO_UNCHANGED); + _pen_trackGCSweep(&ptest); + + _AssertCheck_num_pens(2, "after allocating two pens (pass 4)"); + SDLTest_AssertCheck(ptest.deallocated_id_flags == 0x01, "No unexpected device deallocation (pass 4): %x", ptest.deallocated_id_flags); + SDLTest_AssertCheck(ptest.deallocated_deviceinfo_flags == 0x00010000, "No unexpected deviceinfo deallocation (pass 4): %x", ptest.deallocated_deviceinfo_flags); + + _expect_pens_attached_or_detached(ptest.ids, 3, ATTACHED(1) | ATTACHED(2)); + SDLTest_AssertPass("Validated hotplugging (pass 5)"); + + /* Check detached pen */ + SDLTest_AssertCheck(0 == SDL_strcmp("pen 0", SDL_GetPenName(ptest.ids[0])), + "Pen #0 name"); + checkguid = SDL_GetPenGUID(ptest.ids[0]); + SDLTest_AssertCheck(0 == SDL_memcmp(ptest.guids[0].data, checkguid.data, sizeof(ptest.guids[0].data)), + "Pen #0 guid"); + SDLTest_AssertCheck((SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_YTILT) == SDL_GetPenCapabilities(ptest.ids[0], NULL), + "Pen #0 capabilities"); + SDLTest_AssertPass("Validated that detached pens retained name, GUID, axis info after pass 5"); + + /* Individually detach #1 dn #2 */ + _expect_pens_attached_or_detached(ptest.ids, 3, ATTACHED(1) | ATTACHED(2)); + SDL_PenModifyEnd(SDL_PenModifyBegin(ptest.ids[1]), SDL_FALSE); + _expect_pens_attached_or_detached(ptest.ids, 3, ATTACHED(2)); + + SDL_PenModifyEnd(SDL_PenModifyBegin(ptest.ids[2]), SDL_FALSE); + _expect_pens_attached_or_detached(ptest.ids, 3, 0); + + SDLTest_AssertPass("Validated individual hotplugging (pass 6)"); + + /* Individually attach all */ + SDL_PenModifyEnd(SDL_PenModifyBegin(ptest.ids[2]), SDL_TRUE); + _expect_pens_attached_or_detached(ptest.ids, 3, ATTACHED(2)); + + SDL_PenModifyEnd(SDL_PenModifyBegin(ptest.ids[0]), SDL_TRUE); + _expect_pens_attached_or_detached(ptest.ids, 3, ATTACHED(0) | ATTACHED(2)); + + SDL_PenModifyEnd(SDL_PenModifyBegin(ptest.ids[1]), SDL_TRUE); + _expect_pens_attached_or_detached(ptest.ids, 3, ATTACHED(0) | ATTACHED(1) | ATTACHED(2)); + SDLTest_AssertPass("Validated individual hotplugging (pass 7)"); + + SDL_PenGCMark(); + _pen_trackGCSweep(&ptest); + _AssertCheck_num_pens(0, "after hotplugging test (cleanup)"); + SDLTest_AssertCheck(ptest.deallocated_id_flags == 0x06, "No unexpected device deallocation (cleanup): %x", ptest.deallocated_id_flags); + SDLTest_AssertCheck(ptest.deallocated_deviceinfo_flags == 0x01100000, "No unexpected deviceinfo deallocation (pass 4): %x", ptest.deallocated_deviceinfo_flags); + + _teardown_test_with_gc(&ptest, backup); + + return TEST_COMPLETED; +} + +/** + * @brief Check pen device GUID handling + * + * @sa SDL_GetPenGUID + */ +static int +pen_GUIDs(void *arg) +{ + int i; + char *names[4] = { "pen 0", "pen 1", "pen 2", "pen 3" }; + pen_testdata ptest; + deviceinfo_backup *backup; + + backup = _setup_test(&ptest, 4); + + /* Define four pens */ + SDL_PenGCMark(); + for (i = 0; i < 4; ++i) { + _pen_setDeviceinfo(_pen_register(ptest.ids[i], ptest.guids[i], names[i], SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK), + 20); + } + _pen_trackGCSweep(&ptest); + + /* Detach pens 0 and 2 */ + SDL_PenGCMark(); + for (i = 1; i < 4; i += 2) { + _pen_setDeviceinfo(_pen_register(ptest.ids[i], ptest.guids[i], names[i], SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK), + DEVICEINFO_UNCHANGED); + } + _pen_trackGCSweep(&ptest); + + for (i = 0; i < 4; ++i) { + SDLTest_AssertCheck(ptest.ids[i] == SDL_GetPenFromGUID(ptest.guids[i]), + "GUID search succeeded for %d", i); + } + + /* detach all */ + SDL_PenGCMark(); + _pen_trackGCSweep(&ptest); + + _teardown_test(&ptest, backup); + SDLTest_AssertPass("Pen ID lookup by GUID"); + + return TEST_COMPLETED; +} + +/** + * @brief Check pen device button reporting + * + */ +static int +pen_buttonReporting(void *arg) +{ + int i; + int button_nr, pen_nr; + pen_testdata ptest; + SDL_Event event; + SDL_PenStatusInfo update; + float axes[SDL_PEN_NUM_AXES + 1]; + const float expected_x[2] = { 10.0f, 20.0f }; + const float expected_y[2] = { 11.0f, 21.0f }; + const Uint32 all_axes = SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK | SDL_PEN_AXIS_ROTATION_MASK | SDL_PEN_AXIS_SLIDER_MASK; + + /* Register pen */ + deviceinfo_backup *backup = _setup_test(&ptest, 2); + SDL_PenGCMark(); + _pen_setDeviceinfo(_pen_register(ptest.ids[0], ptest.guids[0], "test pen", + SDL_PEN_INK_MASK | all_axes), + 20); + _pen_setDeviceinfo(_pen_register(ptest.ids[1], ptest.guids[1], "test eraser", + SDL_PEN_ERASER_MASK | all_axes), + 24); + _pen_trackGCSweep(&ptest); + + /* Position mouse suitably before we start */ + for (i = 0; i <= SDL_PEN_NUM_AXES; ++i) { + axes[i] = 0.0625f * i; /* initialise with numbers that can be represented precisely in IEEE 754 and + are > 0.0f and <= 1.0f */ + } + + /* Let pens enter the test window */ + SDL_SendPenWindowEvent(0, ptest.ids[0], ptest.window); + SDL_SendPenWindowEvent(0, ptest.ids[1], ptest.window); + + update.x = expected_x[0]; + update.y = expected_y[0]; + SDL_memcpy(update.axes, axes, sizeof(float) * SDL_PEN_NUM_AXES); + SDL_SendPenMotion(0, ptest.ids[0], SDL_TRUE, &update); + update.x = expected_x[1]; + update.y = expected_y[1]; + SDL_memcpy(update.axes, axes + 1, sizeof(float) * SDL_PEN_NUM_AXES); + SDL_SendPenMotion(0, ptest.ids[1], SDL_TRUE, &update); + + while (SDL_PollEvent(&event)) + ; /* Flush event queue */ + + /* Trigger pen tip events for PEN_DOWN */ + SDLTest_AssertPass("Touch pens to surface"); + + for (pen_nr = 0; pen_nr < 2; ++pen_nr) { + float *expected_axes = axes + pen_nr; + SDL_bool found_event = SDL_FALSE; + Uint16 pen_state = 0x0000 | SDL_PEN_DOWN_MASK; + Uint8 tip = SDL_PEN_TIP_INK; + + if (pen_nr == 1) { + pen_state |= SDL_PEN_ERASER_MASK; + tip = SDL_PEN_TIP_ERASER; + } + + SDL_SendPenTipEvent(0, ptest.ids[pen_nr], SDL_PRESSED); + + while (SDL_PollEvent(&event)) { + if (event.type == SDL_EVENT_PEN_DOWN) { + SDLTest_AssertCheck(event.ptip.which == ptest.ids[pen_nr], + "Received SDL_EVENT_PEN_DOWN from correct pen"); + SDLTest_AssertCheck(event.ptip.tip == (pen_nr == 0)? SDL_PEN_TIP_INK : SDL_PEN_TIP_ERASER, + "Received SDL_EVENT_PEN_DOWN for correct tip"); + SDLTest_AssertCheck(event.ptip.state == SDL_PRESSED, + "Received SDL_EVENT_PEN_DOWN but and marked SDL_PRESSED"); + SDLTest_AssertCheck(event.ptip.tip == tip, + "Received tip %x but expected %x", event.ptip.tip, tip); + SDLTest_AssertCheck(event.ptip.pen_state == pen_state, + "Received SDL_EVENT_PEN_DOWN, and state %04x == %04x (expected)", + event.pbutton.pen_state, pen_state); + SDLTest_AssertCheck((event.ptip.x == expected_x[pen_nr]) && (event.ptip.y == expected_y[pen_nr]), + "Received SDL_EVENT_PEN_DOWN event at correct coordinates: (%f, %f) vs (%f, %f) (expected)", + event.pbutton.x, event.pbutton.y, expected_x[pen_nr], expected_y[pen_nr]); + SDLTest_AssertCheck(0 == SDL_memcmp(expected_axes, event.pbutton.axes, sizeof(float) * SDL_PEN_NUM_AXES), + "Received SDL_EVENT_PEN_DOWN event with correct axis values"); + found_event = SDL_TRUE; + } + SDLTest_AssertCheck(found_event, + "Received the expected SDL_EVENT_PEN_DOWN event"); + } + } + + SDLTest_AssertPass("Pen and eraser set up for button testing"); + + /* Actual tests start: pen, then eraser */ + for (pen_nr = 0; pen_nr < 2; ++pen_nr) { + Uint16 pen_state = 0x0000 | SDL_PEN_DOWN_MASK; + float *expected_axes = axes + pen_nr; + + if (pen_nr == 1) { + pen_state |= SDL_PEN_ERASER_MASK; + } + for (button_nr = 1; button_nr <= 8; ++button_nr) { + SDL_bool found_event = SDL_FALSE; + pen_state |= (1 << (button_nr - 1)); + + SDL_SendPenButton(0, ptest.ids[pen_nr], SDL_PRESSED, button_nr); + while (SDL_PollEvent(&event)) { + if (event.type == SDL_EVENT_PEN_BUTTON_DOWN) { + SDLTest_AssertCheck(event.pbutton.which == ptest.ids[pen_nr], + "Received SDL_EVENT_PEN_BUTTON_DOWN from correct pen"); + SDLTest_AssertCheck(event.pbutton.button == button_nr, + "Received SDL_EVENT_PEN_BUTTON_DOWN from correct button"); + SDLTest_AssertCheck(event.pbutton.state == SDL_PRESSED, + "Received SDL_EVENT_PEN_BUTTON_DOWN but and marked SDL_PRESSED"); + SDLTest_AssertCheck(event.pbutton.pen_state == pen_state, + "Received SDL_EVENT_PEN_BUTTON_DOWN, and state %04x == %04x (expected)", + event.pbutton.pen_state, pen_state); + SDLTest_AssertCheck((event.pbutton.x == expected_x[pen_nr]) && (event.pbutton.y == expected_y[pen_nr]), + "Received SDL_EVENT_PEN_BUTTON_DOWN event at correct coordinates: (%f, %f) vs (%f, %f) (expected)", + event.pbutton.x, event.pbutton.y, expected_x[pen_nr], expected_y[pen_nr]); + SDLTest_AssertCheck(0 == SDL_memcmp(expected_axes, event.pbutton.axes, sizeof(float) * SDL_PEN_NUM_AXES), + "Received SDL_EVENT_PEN_BUTTON_DOWN event with correct axis values"); + if (0 != SDL_memcmp(expected_axes, event.pbutton.axes, sizeof(float) * SDL_PEN_NUM_AXES)) { + int ax; + for (ax = 0; ax < SDL_PEN_NUM_AXES; ++ax) { + SDL_Log("\tax %d\t%.5f\t%.5f expected (equal=%d)", + ax, + event.pbutton.axes[ax], expected_axes[ax], + event.pbutton.axes[ax] == expected_axes[ax]); + } + } + found_event = SDL_TRUE; + } + } + SDLTest_AssertCheck(found_event, + "Received the expected SDL_EVENT_PEN_BUTTON_DOWN event"); + } + } + SDLTest_AssertPass("Pressed all buttons"); + + /* Release every other button */ + for (pen_nr = 0; pen_nr < 2; ++pen_nr) { + Uint16 pen_state = 0x00ff | SDL_PEN_DOWN_MASK; /* 8 buttons pressed */ + float *expected_axes = axes + pen_nr; + + if (pen_nr == 1) { + pen_state |= SDL_PEN_ERASER_MASK; + } + for (button_nr = pen_nr + 1; button_nr <= 8; button_nr += 2) { + SDL_bool found_event = SDL_FALSE; + pen_state &= ~(1 << (button_nr - 1)); + + SDL_SendPenButton(0, ptest.ids[pen_nr], SDL_RELEASED, button_nr); + while (SDL_PollEvent(&event)) { + if (event.type == SDL_EVENT_PEN_BUTTON_UP) { + SDLTest_AssertCheck(event.pbutton.which == ptest.ids[pen_nr], + "Received SDL_EVENT_PEN_BUTTON_UP from correct pen"); + SDLTest_AssertCheck(event.pbutton.button == button_nr, + "Received SDL_EVENT_PEN_BUTTON_UP from correct button"); + SDLTest_AssertCheck(event.pbutton.state == SDL_RELEASED, + "Received SDL_EVENT_PEN_BUTTON_UP and is marked SDL_RELEASED"); + SDLTest_AssertCheck(event.pbutton.pen_state == pen_state, + "Received SDL_EVENT_PEN_BUTTON_UP, and state %04x == %04x (expected)", + event.pbutton.pen_state, pen_state); + SDLTest_AssertCheck((event.pbutton.x == expected_x[pen_nr]) && (event.pbutton.y == expected_y[pen_nr]), + "Received SDL_EVENT_PEN_BUTTON_UP event at correct coordinates"); + SDLTest_AssertCheck(0 == SDL_memcmp(expected_axes, event.pbutton.axes, sizeof(float) * SDL_PEN_NUM_AXES), + "Received SDL_EVENT_PEN_BUTTON_UP event with correct axis values"); + found_event = SDL_TRUE; + } + } + SDLTest_AssertCheck(found_event, + "Received the expected SDL_EVENT_PEN_BUTTON_UP event"); + } + } + SDLTest_AssertPass("Released every other button"); + + /* Trigger pen tip events for PEN_UP */ + SDLTest_AssertPass("Remove pens from surface"); + + for (pen_nr = 0; pen_nr < 2; ++pen_nr) { + float *expected_axes = axes + pen_nr; + SDL_bool found_event = SDL_FALSE; + Uint16 pen_state = 0x0000; + Uint8 tip = SDL_PEN_TIP_INK; + + if (pen_nr == 1) { + pen_state |= SDL_PEN_ERASER_MASK; + tip = SDL_PEN_TIP_ERASER; + } + + SDL_SendPenTipEvent(0, ptest.ids[pen_nr], SDL_RELEASED); + + while (SDL_PollEvent(&event)) { + if (event.type == SDL_EVENT_PEN_UP) { + SDLTest_AssertCheck(event.ptip.which == ptest.ids[pen_nr], + "Received SDL_EVENT_PEN_UP from correct pen"); + SDLTest_AssertCheck(event.ptip.tip == (pen_nr == 0)? SDL_PEN_TIP_INK : SDL_PEN_TIP_ERASER, + "Received SDL_EVENT_PEN_UP for correct tip"); + SDLTest_AssertCheck(event.ptip.state == SDL_RELEASED, + "Received SDL_EVENT_PEN_UP but and marked SDL_RELEASED"); + SDLTest_AssertCheck(event.ptip.tip == tip, + "Received tip %x but expected %x", event.ptip.tip, tip); + SDLTest_AssertCheck((event.ptip.pen_state & 0xff00) == (pen_state & 0xff00), + "Received SDL_EVENT_PEN_UP, and state %04x == %04x (expected)", + event.pbutton.pen_state, pen_state); + SDLTest_AssertCheck((event.ptip.x == expected_x[pen_nr]) && (event.ptip.y == expected_y[pen_nr]), + "Received SDL_EVENT_PEN_UP event at correct coordinates: (%f, %f) vs (%f, %f) (expected)", + event.pbutton.x, event.pbutton.y, expected_x[pen_nr], expected_y[pen_nr]); + SDLTest_AssertCheck(0 == SDL_memcmp(expected_axes, event.pbutton.axes, sizeof(float) * SDL_PEN_NUM_AXES), + "Received SDL_EVENT_PEN_UP event with correct axis values"); + found_event = SDL_TRUE; + } + SDLTest_AssertCheck(found_event, + "Received the expected SDL_EVENT_PEN_UP event"); + } + } + + /* Cleanup */ + SDL_PenGCMark(); + _pen_trackGCSweep(&ptest); + _teardown_test(&ptest, backup); + + return TEST_COMPLETED; +} + +/** + * @brief Check pen device movement and axis update reporting + * + * Also tests SDL_GetPenStatus for agreement with the most recently reported events + * + * @sa SDL_GetPenStatus + */ +static int +pen_movementAndAxes(void *arg) +{ + pen_testdata ptest; + SDL_Event event; +#define MAX_STEPS 80 + /* Pen simulation */ + simulated_pen_action steps[MAX_STEPS]; + size_t num_steps = 0; + + SDL_Pen simulated_pens[2]; + int sim_pc = 0; + simulated_pen_action *last_action; + + /* Register pen */ + deviceinfo_backup *backup = _setup_test(&ptest, 2); + + /* Pen simulation program */ +#define STEP steps[num_steps++] = + + /* #1: Check basic reporting */ + /* Hover eraser, tilt axes */ + SIMPEN_MOVE(0, 30.0f, 31.0f); + SIMPEN_AXIS(0, SDL_PEN_AXIS_PRESSURE, 0.0f); + SIMPEN_AXIS(0, SDL_PEN_AXIS_XTILT, 22.5f); + SIMPEN_AXIS(0, SDL_PEN_AXIS_YTILT, 45.0f); + SIMPEN_EVENT_MOTION(0); + + /* #2: Check that motion events without motion aren't reported */ + SIMPEN_EVENT_MOTION_SUPPRESSED(0); + SIMPEN_EVENT_MOTION_SUPPRESSED(0); + + /* #3: Check multiple pens being reported */ + /* Move pen and touch surface, don't tilt */ + SIMPEN_MOVE(1, 40.0f, 41.0f); + SIMPEN_AXIS(1, SDL_PEN_AXIS_PRESSURE, 0.25f); + SIMPEN_EVENT_MOTION(1); + + /* $4: Multi-buttons */ + /* Press eraser buttons */ + SIMPEN_EVENT_TIP(0, "down", SDL_PEN_TIP_ERASER); + SIMPEN_EVENT_BUTTON(0, "push", 2); + SIMPEN_EVENT_BUTTON(0, "push", 1); + SIMPEN_EVENT_BUTTON(0, 0, 2); /* release again */ + SIMPEN_EVENT_BUTTON(0, "push", 3); + + /* #5: Check move + button actions connecting */ + /* Move and tilt pen, press some pen buttons */ + SIMPEN_MOVE(1, 3.0f, 8.0f); + SIMPEN_AXIS(1, SDL_PEN_AXIS_PRESSURE, 0.5f); + SIMPEN_AXIS(1, SDL_PEN_AXIS_XTILT, -21.0f); + SIMPEN_AXIS(1, SDL_PEN_AXIS_YTILT, -25.0f); + SIMPEN_EVENT_MOTION(1); + SIMPEN_EVENT_BUTTON(1, "push", 2); + SIMPEN_EVENT_TIP(1, "down", SDL_PEN_TIP_INK); + + /* #6: Check nonterference between pens */ + /* Eraser releases buttons */ + SIMPEN_EVENT_BUTTON(0, 0, 1); + SIMPEN_EVENT_TIP(0, 0, SDL_PEN_TIP_ERASER); + + /* #7: Press-move-release action */ + /* Eraser press-move-release */ + SIMPEN_EVENT_BUTTON(0, "push", 1); + SIMPEN_MOVE(0, 99.0f, 88.0f); + SIMPEN_AXIS(0, SDL_PEN_AXIS_PRESSURE, 0.625f); + SIMPEN_EVENT_MOTION(0); + SIMPEN_MOVE(0, 44.5f, 42.25f); + SIMPEN_EVENT_MOTION(0); + SIMPEN_EVENT_BUTTON(0, 0, 1); + + /* #8: Intertwining button release actions some more */ + /* Pen releases button */ + SIMPEN_EVENT_BUTTON(1, 0, 2); + SIMPEN_EVENT_TIP(1, 0, SDL_PEN_TIP_INK); + + /* Push one more pen button, then release all ereaser buttons */ + SIMPEN_EVENT_TIP(1, "down", SDL_PEN_TIP_INK); + SIMPEN_EVENT_BUTTON(0, 0, 2); + SIMPEN_EVENT_BUTTON(0, 0, 3); + + /* Lift up pen, flip it so it becomes an eraser, and touch it again */ + SIMPEN_EVENT_TIP(1, 0, SDL_PEN_TIP_INK); + SIMPEN_SET_ERASER(1, 1); + SIMPEN_EVENT_TIP(1, "push", SDL_PEN_TIP_ERASER); + + /* And back again */ + SIMPEN_EVENT_TIP(1, 0, SDL_PEN_TIP_ERASER); + SIMPEN_SET_ERASER(1, 0); + SIMPEN_EVENT_TIP(1, "push", SDL_PEN_TIP_INK); + + /* #9: Suppress move on unsupported axis */ + SIMPEN_AXIS(1, SDL_PEN_AXIS_DISTANCE, 0.25f); + SIMPEN_EVENT_MOTION_SUPPRESSED(0); + + SIMPEN_DONE(); +#undef STEP + /* End of pen simulation program */ + + SDLTest_AssertCheck(num_steps < MAX_STEPS, "Pen simulation program does not exceed buffer size"); +#undef MAX_STEPS + + SDL_PenGCMark(); + _pen_setDeviceinfo(_pen_register(ptest.ids[0], ptest.guids[0], "test eraser", + SDL_PEN_ERASER_MASK | SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK), + 20); + _pen_setDeviceinfo(_pen_register(ptest.ids[1], ptest.guids[1], "test pen", + SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK), + 24); + _pen_trackGCSweep(&ptest); + SDL_SendPenWindowEvent(0, ptest.ids[0], ptest.window); + SDL_SendPenWindowEvent(0, ptest.ids[1], ptest.window); + while (SDL_PollEvent(&event)) + ; /* Flush event queue */ + SDLTest_AssertPass("Pen and eraser set up for testing"); + + _pen_simulate_init(&ptest, simulated_pens, 2); + /* Simulate pen movements */ + while ((last_action = _pen_simulate(steps, &sim_pc, &simulated_pens[0], 2))) { + int attempts = 0; + SDL_Pen *simpen = &simulated_pens[last_action->pen_index]; + SDL_PenID reported_which = -1; + float reported_x = -1.0f, reported_y = -1.0f; + float *reported_axes = NULL; + Uint32 reported_pen_state = 0; + Uint32 expected_pen_state = simpen->header.flags & SDL_PEN_ERASER_MASK; + SDL_bool dump_pens = SDL_FALSE; + + do { + SDL_PumpEvents(); + SDL_PollEvent(&event); + if (++attempts > 10000) { + SDLTest_AssertCheck(0, "Never got the anticipated event"); + return TEST_ABORTED; + } + } while (event.type != SDL_EVENT_PEN_DOWN + && event.type != SDL_EVENT_PEN_UP + && event.type != SDL_EVENT_PEN_MOTION + && event.type != SDL_EVENT_PEN_BUTTON_UP + && event.type != SDL_EVENT_PEN_BUTTON_DOWN); /* skip boring events */ + + expected_pen_state |= simpen->last.buttons; + + SDLTest_AssertCheck(0 != event.type, + "Received the anticipated event"); + + switch (last_action->type) { + case SIMPEN_ACTION_MOTION_EVENT: + SDLTest_AssertCheck(event.type == SDL_EVENT_PEN_MOTION, "Expected pen motion event (but got 0x%lx)", (unsigned long) event.type); + reported_which = event.pmotion.which; + reported_x = event.pmotion.x; + reported_y = event.pmotion.y; + reported_pen_state = event.pmotion.pen_state; + reported_axes = &event.pmotion.axes[0]; + break; + + case SIMPEN_ACTION_PRESS: + SDLTest_AssertCheck(event.type == SDL_EVENT_PEN_BUTTON_DOWN, "Expected PENBUTTONDOWN event (but got 0x%lx)", (unsigned long) event.type); + SDLTest_AssertCheck(event.pbutton.state == SDL_PRESSED, "Expected PRESSED button"); + /* Fall through */ + case SIMPEN_ACTION_RELEASE: + if (last_action->type == SIMPEN_ACTION_RELEASE) { + SDLTest_AssertCheck(event.type == SDL_EVENT_PEN_BUTTON_UP, "Expected PENBUTTONUP event (but got 0x%lx)", (unsigned long) event.type); + SDLTest_AssertCheck(event.pbutton.state == SDL_RELEASED, "Expected RELEASED button"); + } + SDLTest_AssertCheck(event.pbutton.button == last_action->index, "Expected button %d, but got %d", + last_action->index, event.pbutton.button); + reported_which = event.pbutton.which; + reported_x = event.pbutton.x; + reported_y = event.pbutton.y; + reported_pen_state = event.pbutton.pen_state; + reported_axes = &event.pbutton.axes[0]; + break; + + case SIMPEN_ACTION_DOWN: + SDLTest_AssertCheck(event.type == SDL_EVENT_PEN_DOWN, "Expected PENBUTTONDOWN event (but got 0x%lx)", (unsigned long) event.type); + SDLTest_AssertCheck(event.ptip.state == SDL_PRESSED, "Expected PRESSED button"); + /* Fall through */ + case SIMPEN_ACTION_UP: + if (last_action->type == SIMPEN_ACTION_UP) { + SDLTest_AssertCheck(event.type == SDL_EVENT_PEN_UP, "Expected PENBUTTONUP event (but got 0x%lx)", (unsigned long) event.type); + SDLTest_AssertCheck(event.ptip.state == SDL_RELEASED, "Expected RELEASED button"); + } + SDLTest_AssertCheck(event.ptip.tip == last_action->index, "Expected tip %d, but got %d", + last_action->index, event.ptip.tip); + reported_which = event.ptip.which; + reported_x = event.ptip.x; + reported_y = event.ptip.y; + reported_pen_state = event.ptip.pen_state; + reported_axes = &event.ptip.axes[0]; + break; + + case SIMPEN_ACTION_ERASER_MODE: + break; + + default: + SDLTest_AssertCheck(0, "Error in pen simulator: unexpected action %d", last_action->type); + return TEST_ABORTED; + } + + if (reported_which != simpen->header.id) { + dump_pens = SDL_TRUE; + SDLTest_AssertCheck(0, "Expected report for pen %lu but got report for pen %lu", + (unsigned long) simpen->header.id, + (unsigned long) reported_which); + } + if (reported_x != simpen->last.x || reported_y != simpen->last.y) { + dump_pens = SDL_TRUE; + SDLTest_AssertCheck(0, "Mismatch in pen coordinates"); + } + if (reported_x != simpen->last.x || reported_y != simpen->last.y) { + dump_pens = SDL_TRUE; + SDLTest_AssertCheck(0, "Mismatch in pen coordinates"); + } + if (reported_pen_state != expected_pen_state) { + dump_pens = SDL_TRUE; + SDLTest_AssertCheck(0, "Mismatch in pen state: %lx vs %lx (expected)", + (unsigned long) reported_pen_state, + (unsigned long) expected_pen_state); + } + if (0 != SDL_memcmp(reported_axes, simpen->last.axes, sizeof(float) * SDL_PEN_NUM_AXES)) { + dump_pens = SDL_TRUE; + SDLTest_AssertCheck(0, "Mismatch in axes"); + } + + if (dump_pens) { + SDL_Log("----- Pen #%d:", last_action->pen_index); + _pen_dump("expect", simpen); + _pen_dump("actual", SDL_GetPenPtr(simpen->header.id)); + } + } + SDLTest_AssertPass("Pen and eraser move and report events correctly and independently"); + + /* Cleanup */ + SDL_PenGCMark(); + _pen_trackGCSweep(&ptest); + _teardown_test(&ptest, backup); + return TEST_COMPLETED; +} + +static void +_expect_pen_config(SDL_PenID penid, + SDL_GUID expected_guid, + SDL_bool expected_attached, + char *expected_name, + int expected_type, + int expected_num_buttons, + float expected_max_tilt, + int expected_axes) +{ + SDL_PenCapabilityInfo actual_info = { 0 }; + const char *actual_name = SDL_GetPenName(penid); + + if (penid == SDL_PEN_INVALID) { + SDLTest_Assert(0, "Invalid pen ID"); + return; + } + + SDLTest_AssertEq1(int, "%d", 0, SDL_GUIDCompare(expected_guid, SDL_GetPenGUID(penid)), + "Pen %lu guid equality", (unsigned long) penid); + + SDLTest_AssertCheck(0 == SDL_strcmp(expected_name, actual_name), + "Expected name='%s' vs actual='%s'", expected_name, actual_name); + + SDLTest_AssertEq1(int, "%d", expected_attached, SDL_PenConnected(penid), + "Pen %lu is attached", (unsigned long) penid); + SDLTest_AssertEq1(int, "%d", expected_type, SDL_GetPenType(penid), + "Pen %lu type", (unsigned long) penid); + SDLTest_AssertEq1(int, "%x", expected_axes, SDL_GetPenCapabilities(penid, &actual_info), + "Pen %lu axis flags", (unsigned long) penid); + SDLTest_AssertEq1(int, "%d", expected_num_buttons, actual_info.num_buttons, + "Pen %lu number of buttons", (unsigned long) penid); + SDLTest_AssertEq1(float, "%f", expected_max_tilt, actual_info.max_tilt, + "Pen %lu max tilt", (unsigned long) penid); +} + +/** + * @brief Check backend pen iniitalisation and pen meta-information + * + * @sa SDL_GetPenCapabilities, SDL_PenAxisInfo + */ +static int +pen_initAndInfo(void *arg) +{ + pen_testdata ptest; + SDL_Pen *pen; + Uint32 mask; + char strbuf[SDL_PEN_MAX_NAME]; + + /* Init */ + deviceinfo_backup *backup = _setup_test(&ptest, 7); + + /* Register default pen */ + _expect_pens_attached_or_detached(ptest.ids, 7, 0); + + /* Register completely default pen */ + pen = SDL_PenModifyBegin(ptest.ids[0]); + SDL_memcpy(pen->guid.data, ptest.guids[0].data, sizeof(ptest.guids[0].data)); + SDL_PenModifyEnd(pen, SDL_TRUE); + + SDL_snprintf(strbuf, sizeof(strbuf), + "Pen %lu", (unsigned long) ptest.ids[0]); + _expect_pen_config(ptest.ids[0], ptest.guids[0], SDL_TRUE, + strbuf, SDL_PEN_TYPE_PEN, SDL_PEN_INFO_UNKNOWN, 0.0f, + SDL_PEN_INK_MASK); + _expect_pens_attached_or_detached(ptest.ids, 7, ATTACHED(0)); + SDLTest_AssertPass("Pass #1: default pen"); + + /* Register mostly-default pen with buttons and custom name */ + pen = SDL_PenModifyBegin(ptest.ids[1]); + SDL_PenModifyAddCapabilities(pen, SDL_PEN_AXIS_PRESSURE_MASK); + SDL_memcpy(pen->guid.data, ptest.guids[1].data, sizeof(ptest.guids[1].data)); + SDL_strlcpy(strbuf, "My special test pen", SDL_PEN_MAX_NAME); + SDL_strlcpy(pen->name, strbuf, SDL_PEN_MAX_NAME); + pen->info.num_buttons = 7; + SDL_PenModifyEnd(pen, SDL_TRUE); + + _expect_pen_config(ptest.ids[1], ptest.guids[1], SDL_TRUE, + strbuf, SDL_PEN_TYPE_PEN, 7, 0.0f, + SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK); + _expect_pens_attached_or_detached(ptest.ids, 7, ATTACHED(0) | ATTACHED(1)); + SDLTest_AssertPass("Pass #2: default pen with button and name info"); + + /* Register eraser with default name, but keep initially detached */ + pen = SDL_PenModifyBegin(ptest.ids[2]); + SDL_memcpy(pen->guid.data, ptest.guids[2].data, sizeof(ptest.guids[2].data)); + pen->type = SDL_PEN_TYPE_ERASER; + SDL_PenModifyAddCapabilities(pen, SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK); + SDL_PenModifyEnd(pen, SDL_FALSE); + + SDL_snprintf(strbuf, sizeof(strbuf), + "Eraser %lu", (unsigned long) ptest.ids[2]); + _expect_pen_config(ptest.ids[2], ptest.guids[2], SDL_FALSE, + strbuf, SDL_PEN_TYPE_ERASER, SDL_PEN_INFO_UNKNOWN, SDL_PEN_INFO_UNKNOWN, + SDL_PEN_ERASER_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK); + _expect_pens_attached_or_detached(ptest.ids, 7, ATTACHED(0) | ATTACHED(1)); + /* now make available */ + SDL_PenModifyEnd(SDL_PenModifyBegin(ptest.ids[2]), SDL_TRUE); + _expect_pen_config(ptest.ids[2], ptest.guids[2], SDL_TRUE, + strbuf, SDL_PEN_TYPE_ERASER, SDL_PEN_INFO_UNKNOWN, SDL_PEN_INFO_UNKNOWN, + SDL_PEN_ERASER_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK); + _expect_pens_attached_or_detached(ptest.ids, 7, ATTACHED(0) | ATTACHED(1) | ATTACHED(2)); + SDLTest_AssertPass("Pass #3: eraser-type pen initially detached, then attached"); + + /* Abort pen registration */ + pen = SDL_PenModifyBegin(ptest.ids[3]); + SDL_memcpy(pen->guid.data, ptest.guids[3].data, sizeof(ptest.guids[3].data)); + SDL_PenModifyAddCapabilities(pen, SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK); + pen->type = SDL_PEN_TYPE_NONE; + SDL_PenModifyEnd(pen, SDL_TRUE); + _expect_pens_attached_or_detached(ptest.ids, 7, ATTACHED(0) | ATTACHED(1) | ATTACHED(2)); + SDLTest_AssertCheck(NULL == SDL_GetPenName(ptest.ids[3]), "Pen with aborted registration remains unknown"); + SDLTest_AssertPass("Pass #4: aborted pen registration"); + + /* Brush with custom axes */ + pen = SDL_PenModifyBegin(ptest.ids[4]); + SDL_memcpy(pen->guid.data, ptest.guids[4].data, sizeof(ptest.guids[4].data)); + SDL_strlcpy(pen->name, "Testish Brush", SDL_PEN_MAX_NAME); + pen->type = SDL_PEN_TYPE_BRUSH; + pen->info.num_buttons = 1; + SDL_PenModifyAddCapabilities(pen, SDL_PEN_AXIS_ROTATION_MASK); + pen->info.max_tilt = 72.5f; + SDL_PenModifyAddCapabilities(pen, SDL_PEN_AXIS_XTILT_MASK); + SDL_PenModifyAddCapabilities(pen, SDL_PEN_AXIS_PRESSURE_MASK); + SDL_PenModifyEnd(pen, SDL_TRUE); + _expect_pen_config(ptest.ids[4], ptest.guids[4], SDL_TRUE, + "Testish Brush", SDL_PEN_TYPE_BRUSH, 1, 72.5f, + SDL_PEN_INK_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_ROTATION_MASK | SDL_PEN_AXIS_PRESSURE_MASK); + _expect_pens_attached_or_detached(ptest.ids, 7, ATTACHED(0) | ATTACHED(1) | ATTACHED(2) | ATTACHED(4)); + SDLTest_AssertPass("Pass #5: brush-type pen with unusual axis layout"); + + /* Wacom airbrush pen */ + { + const Uint32 wacom_type_id = 0x0912; + const Uint32 wacom_serial_id = 0xa0b1c2d3; + SDL_GUID guid = { + { 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0 } + }; + guid.data[0] = (wacom_serial_id >> 0) & 0xff; + guid.data[1] = (wacom_serial_id >> 8) & 0xff; + guid.data[2] = (wacom_serial_id >> 16) & 0xff; + guid.data[3] = (wacom_serial_id >> 24) & 0xff; + guid.data[4] = (wacom_type_id >> 0) & 0xff; + guid.data[5] = (wacom_type_id >> 8) & 0xff; + guid.data[6] = (wacom_type_id >> 16) & 0xff; + guid.data[7] = (wacom_type_id >> 24) & 0xff; + + pen = SDL_PenModifyBegin(ptest.ids[5]); + SDL_PenModifyForWacomID(pen, wacom_type_id, &mask); + SDL_PenUpdateGUIDForWacom(&pen->guid, wacom_type_id, wacom_serial_id); + SDL_PenModifyAddCapabilities(pen, mask); + SDL_PenModifyEnd(pen, SDL_TRUE); + _expect_pen_config(ptest.ids[5], guid, SDL_TRUE, + "Wacom Airbrush Pen", SDL_PEN_TYPE_AIRBRUSH, 1, 64.0f, /* Max tilt angle */ + SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT_MASK | SDL_PEN_AXIS_YTILT_MASK | SDL_PEN_AXIS_DISTANCE_MASK | SDL_PEN_AXIS_SLIDER_MASK); + _expect_pens_attached_or_detached(ptest.ids, 7, ATTACHED(0) | ATTACHED(1) | ATTACHED(2) | ATTACHED(4) | ATTACHED(5)); + } + SDLTest_AssertPass("Pass #6: wacom airbrush pen"); + + /* Cleanup */ + SDL_PenGCMark(); + _pen_trackGCSweep(&ptest); + _teardown_test(&ptest, backup); + return TEST_COMPLETED; +} + +#define SET_POS(update, xpos, ypos) \ + (update).x = (xpos); \ + (update).y = (ypos); + +static void +_penmouse_expect_button(int type, int button) +{ + SDL_bool press = type == SDL_PRESSED; + SDLTest_AssertCheck((press ? SDL_EVENT_MOUSE_BUTTON_DOWN : SDL_EVENT_MOUSE_BUTTON_UP) == _mouseemu_last_event, + "Mouse button %s: %x", + (press ? "press" : "release"), _mouseemu_last_event); + SDLTest_AssertCheck(button == _mouseemu_last_button, + "Observed the expected simulated button: %d", _mouseemu_last_button); + SDLTest_AssertCheck(SDL_PEN_MOUSEID == _mouseemu_last_mouseid, + "Observed the expected mouse ID: 0x%x", _mouseemu_last_mouseid); + + _mouseemu_last_event = 0; +} + +/** + * @brief Check pen device mouse emulation and event suppression without SDL_HINT_PEN_DELAY_MOUSE_BUTTON + * + * Since we include SDL_pen.c, we link it against our own mock implementations of SDL_PSendMouseButton + * and SDL_SendMouseMotion; see tehere for details. + */ +static int +pen_mouseEmulation(void *arg) +{ + pen_testdata ptest; + SDL_Event event; + int i; + SDL_PenStatusInfo update; + deviceinfo_backup *backup; + + pen_delay_mouse_button_mode = 0; + pen_mouse_emulation_mode = PEN_MOUSE_EMULATE; /* to trigger our own SDL_SendMouseButton */ + + /* Register pen */ + backup = _setup_test(&ptest, 1); + SDL_PenGCMark(); + _pen_setDeviceinfo(_pen_register(ptest.ids[0], ptest.guids[0], "testpen", + SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT | SDL_PEN_AXIS_YTILT), + 20); + _pen_trackGCSweep(&ptest); + + /* Move pen into window */ + SDL_SendPenWindowEvent(0, ptest.ids[0], ptest.window); + + /* Initialise pen location */ + SDL_memset(update.axes, 0, sizeof(update.axes)); + SET_POS(update, 100.0f, 100.0f); + SDL_SendPenMotion(0, ptest.ids[0], SDL_TRUE, &update); + while (SDL_PollEvent(&event)) + ; /* Flush event queue */ + + /* Test motion forwarding */ + _mouseemu_last_event = 0; + SET_POS(update, 121.25f, 110.75f); + SDL_SendPenMotion(0, ptest.ids[0], SDL_TRUE, &update); + SDLTest_AssertCheck(SDL_EVENT_MOUSE_MOTION == _mouseemu_last_event, + "Mouse motion event: %d", _mouseemu_last_event); + SDLTest_AssertCheck(121.25f == _mouseemu_last_x && 110.75f == _mouseemu_last_y, + "Motion to correct position: %f,%f", _mouseemu_last_x, _mouseemu_last_y); + SDLTest_AssertCheck(SDL_PEN_MOUSEID == _mouseemu_last_mouseid, + "Observed the expected mouse ID: 0x%x", _mouseemu_last_mouseid); + SDLTest_AssertCheck(0 == _mouseemu_last_relative, + "Absolute motion event"); + SDLTest_AssertPass("Motion emulation"); + + /* Test redundant motion report suppression */ + _mouseemu_last_event = 0; + + SET_POS(update, 121.25f, 110.75f); + SDL_SendPenMotion(0, ptest.ids[0], SDL_TRUE, &update); + + SET_POS(update, 121.25f, 110.75f); + SDL_SendPenMotion(0, ptest.ids[0], SDL_TRUE, &update); + + update.axes[0] = 1.0f; + SDL_SendPenMotion(0, ptest.ids[0], SDL_TRUE, &update); + + SET_POS(update, 121.25f, 110.75f); + update.axes[0] = 0.0f; + update.axes[1] = 0.75f; + SDL_SendPenMotion(0, ptest.ids[0], SDL_TRUE, &update); + + SDLTest_AssertCheck(0 == _mouseemu_last_event, + "Redundant mouse motion suppressed: %d", _mouseemu_last_event); + SDLTest_AssertPass("Redundant motion suppression"); + + /* Test button press reporting */ + SDL_SendPenTipEvent(0, ptest.ids[0], SDL_PRESSED); + _penmouse_expect_button(SDL_PRESSED, 1); + + for (i = 1; i <= 3; ++i) { + SDL_SendPenButton(0, ptest.ids[0], SDL_PRESSED, i); + _penmouse_expect_button(SDL_PRESSED, i + 1); + } + SDLTest_AssertPass("Button press mouse emulation"); + + /* Test button release reporting */ + SDL_SendPenTipEvent(0, ptest.ids[0], SDL_RELEASED); + _penmouse_expect_button(SDL_RELEASED, 1); + + for (i = 1; i <= 3; ++i) { + SDL_SendPenButton(0, ptest.ids[0], SDL_RELEASED, i); + _penmouse_expect_button(SDL_RELEASED, i + 1); + } + SDLTest_AssertPass("Button release mouse emulation"); + + /* Cleanup */ + SDL_PenGCMark(); + _pen_trackGCSweep(&ptest); + _teardown_test(&ptest, backup); + return TEST_COMPLETED; +} + +/** + * @brief Check pen device mouse emulation when SDL_HINT_PEN_DELAY_MOUSE_BUTTON is enabled (default) + */ +static int +pen_mouseEmulationDelayed(void *arg) +{ + pen_testdata ptest; + SDL_Event event; + int i; + SDL_PenStatusInfo update; + deviceinfo_backup *backup; + + pen_delay_mouse_button_mode = 1; + pen_mouse_emulation_mode = PEN_MOUSE_EMULATE; /* to trigger our own SDL_SendMouseButton */ + + /* Register pen */ + backup = _setup_test(&ptest, 1); + SDL_PenGCMark(); + _pen_setDeviceinfo(_pen_register(ptest.ids[0], ptest.guids[0], "testpen", + SDL_PEN_INK_MASK | SDL_PEN_AXIS_PRESSURE_MASK | SDL_PEN_AXIS_XTILT | SDL_PEN_AXIS_YTILT), + 20); + _pen_trackGCSweep(&ptest); + + /* Move pen into window */ + SDL_SendPenWindowEvent(0, ptest.ids[0], ptest.window); + + /* Initialise pen location */ + SDL_memset(update.axes, 0, sizeof(update.axes)); + SET_POS(update, 100.0f, 100.0f); + SDL_SendPenMotion(0, ptest.ids[0], SDL_TRUE, &update); + while (SDL_PollEvent(&event)) + ; /* Flush event queue */ + + /* Test motion forwarding */ + _mouseemu_last_event = 0; + SET_POS(update, 121.25f, 110.75f); + SDL_SendPenMotion(0, ptest.ids[0], SDL_TRUE, &update); + SDLTest_AssertCheck(SDL_EVENT_MOUSE_MOTION == _mouseemu_last_event, + "Mouse motion event: %d", _mouseemu_last_event); + SDLTest_AssertCheck(121.25f == _mouseemu_last_x && 110.75f == _mouseemu_last_y, + "Motion to correct position: %f,%f", _mouseemu_last_x, _mouseemu_last_y); + SDLTest_AssertCheck(SDL_PEN_MOUSEID == _mouseemu_last_mouseid, + "Observed the expected mouse ID: 0x%x", _mouseemu_last_mouseid); + SDLTest_AssertCheck(0 == _mouseemu_last_relative, + "Absolute motion event"); + SDLTest_AssertPass("Motion emulation"); + _mouseemu_last_event = 0; + + /* Test button press reporting */ + for (i = 1; i <= 2; ++i) { + SDL_SendPenButton(0, ptest.ids[0], SDL_PRESSED, i); + SDLTest_AssertCheck(0 == _mouseemu_last_event, + "Non-touching button press suppressed: %d", _mouseemu_last_event); + SDL_SendPenButton(0, ptest.ids[0], SDL_RELEASED, i); + SDLTest_AssertCheck(0 == _mouseemu_last_event, + "Non-touching button release suppressed: %d", _mouseemu_last_event); + } + + /* Touch surface */ + SDL_SendPenTipEvent(0, ptest.ids[0], SDL_PRESSED); + _penmouse_expect_button(SDL_PRESSED, 1); + SDL_SendPenTipEvent(0, ptest.ids[0], SDL_RELEASED); + _penmouse_expect_button(SDL_RELEASED, 1); + + /* Test button press reporting, releasing extra button AFTER lifting pen */ + for (i = 1; i <= 2; ++i) { + SDL_SendPenButton(0, ptest.ids[0], SDL_PRESSED, i); + SDLTest_AssertCheck(0 == _mouseemu_last_event, + "Non-touching button press suppressed (A.1): %d", _mouseemu_last_event); + SDL_SendPenTipEvent(0, ptest.ids[0], SDL_PRESSED); + _penmouse_expect_button(SDL_PRESSED, i + 1); + + SDL_SendPenTipEvent(0, ptest.ids[0], SDL_RELEASED); + _penmouse_expect_button(SDL_RELEASED, i + 1); + + SDL_SendPenButton(0, ptest.ids[0], SDL_RELEASED, i); + SDLTest_AssertCheck(0 == _mouseemu_last_event, + "Non-touching button press suppressed (A.2): %d", _mouseemu_last_event); + } + SDLTest_AssertPass("Delayed button press mouse emulation, touching without releasing button"); + + /* Test button press reporting, releasing extra button BEFORE lifting pen */ + for (i = 1; i <= 2; ++i) { + SDL_SendPenButton(0, ptest.ids[0], SDL_PRESSED, i); + SDLTest_AssertCheck(0 == _mouseemu_last_event, + "Non-touching button press suppressed (B.1): %d", _mouseemu_last_event); + SDL_SendPenTipEvent(0, ptest.ids[0], SDL_PRESSED); + _penmouse_expect_button(SDL_PRESSED, i + 1); + + SDL_SendPenButton(0, ptest.ids[0], SDL_RELEASED, i); + SDLTest_AssertCheck(0 == _mouseemu_last_event, + "Non-touching button press suppressed (B.2): %d", _mouseemu_last_event); + SDL_SendPenTipEvent(0, ptest.ids[0], SDL_RELEASED); + _penmouse_expect_button(SDL_RELEASED, i + 1); + } + SDLTest_AssertPass("Delayed button press mouse emulation, touching and then releasing button"); + + /* Cleanup */ + SDL_PenGCMark(); + _pen_trackGCSweep(&ptest); + _teardown_test(&ptest, backup); + return TEST_COMPLETED; +} + +/** + * @brief Ensure that all SDL_Pen*Event structures have compatible memory layout, as expected by SDL_pen.c + */ +static int +pen_memoryLayout(void *arg) +{ +#define LAYOUT_COMPATIBLE(field) \ + SDLTest_AssertCheck(offsetof(SDL_PenTipEvent, field) == offsetof(SDL_PenMotionEvent, field), \ + "Memory layout SDL_PenTipEvent and SDL_PenMotionEvent compatibility: '" #field "'"); \ + SDLTest_AssertCheck(offsetof(SDL_PenTipEvent, field) == offsetof(SDL_PenButtonEvent, field), \ + "Memory layout SDL_PenTipEvent and SDL_PenBUttonEvent compatibility: '" #field "'"); + + LAYOUT_COMPATIBLE(which); + LAYOUT_COMPATIBLE(x); + LAYOUT_COMPATIBLE(y); + LAYOUT_COMPATIBLE(axes); + + return TEST_COMPLETED; +} + +/* ================= Test References ================== */ + +/* Mouse test cases */ +static const SDLTest_TestCaseReference penTest1 = { (SDLTest_TestCaseFp)pen_iteration, "pen_iteration", "Iterate over all pens with SDL_PenIDForIndex", TEST_ENABLED }; + +static const SDLTest_TestCaseReference penTest2 = { (SDLTest_TestCaseFp)pen_hotplugging, "pen_hotplugging", "Hotplug pens and validate their status, including SDL_PenConnected", TEST_ENABLED }; + +static const SDLTest_TestCaseReference penTest3 = { (SDLTest_TestCaseFp)pen_GUIDs, "pen_GUIDs", "Check Pen SDL_GUID operations", TEST_ENABLED }; + +static const SDLTest_TestCaseReference penTest4 = { (SDLTest_TestCaseFp)pen_buttonReporting, "pen_buttonReporting", "Check pen button presses", TEST_ENABLED }; + +static const SDLTest_TestCaseReference penTest5 = { (SDLTest_TestCaseFp)pen_movementAndAxes, "pen_movementAndAxes", "Check pen movement and axis update reporting", TEST_ENABLED }; + +static const SDLTest_TestCaseReference penTest6 = { (SDLTest_TestCaseFp)pen_initAndInfo, "pen_info", "Check pen self-description and initialisation", TEST_ENABLED }; + +static const SDLTest_TestCaseReference penTest7 = { (SDLTest_TestCaseFp)pen_mouseEmulation, "pen_mouseEmulation", "Check pen-as-mouse event forwarding (direct)", TEST_ENABLED }; + +static const SDLTest_TestCaseReference penTest8 = { (SDLTest_TestCaseFp)pen_mouseEmulationDelayed, "pen_mouseEmulationDelayed", "Check pen-as-mouse event forwarding (delayed)", TEST_ENABLED }; + +static const SDLTest_TestCaseReference penTest9 = { (SDLTest_TestCaseFp)pen_memoryLayout, "pen_memoryLayout", "Check that all pen events have compatible layout (required by SDL_pen.c)", TEST_ENABLED }; + +/* Sequence of Mouse test cases */ +static const SDLTest_TestCaseReference *penTests[] = { + &penTest1, &penTest2, &penTest3, &penTest4, &penTest5, &penTest6, &penTest7, &penTest8, &penTest9, NULL +}; + +/* Mouse test suite (global) */ +SDLTest_TestSuiteReference penTestSuite = { + "Pen", + NULL, + penTests, + NULL +}; diff --git a/test/testautomation_suites.h b/test/testautomation_suites.h index 143ec15369..6d04b4f972 100644 --- a/test/testautomation_suites.h +++ b/test/testautomation_suites.h @@ -20,6 +20,7 @@ extern SDLTest_TestSuiteReference keyboardTestSuite; extern SDLTest_TestSuiteReference mainTestSuite; extern SDLTest_TestSuiteReference mathTestSuite; extern SDLTest_TestSuiteReference mouseTestSuite; +extern SDLTest_TestSuiteReference penTestSuite; extern SDLTest_TestSuiteReference pixelsTestSuite; extern SDLTest_TestSuiteReference platformTestSuite; extern SDLTest_TestSuiteReference propertiesTestSuite; diff --git a/test/testpen.c b/test/testpen.c new file mode 100644 index 0000000000..aa92f7f54d --- /dev/null +++ b/test/testpen.c @@ -0,0 +1,496 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2023 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#include +#include +#include +#include +#include + +#define WIDTH 1600 +#define HEIGHT 1200 + +#define VERBOSE 0 + +#define ALWAYS_SHOW_PRESSURE_BOX 1 + +static SDLTest_CommonState *state; +static int quitting = 0; + +static float last_x, last_y; +static float last_xtilt, last_ytilt, last_pressure, last_distance, last_rotation; +static int last_button; +static int last_touching; /* tip touches surface */ +static int last_was_eraser; + +static SDL_Texture *offscreen_texture = NULL; + +static void DrawScreen(SDL_Renderer *renderer) +{ + float xdelta, ydelta, endx, endy; + /* off-screen texture to render into */ + SDL_Texture *window_texture; + const float X = 128.0f, Y = 128.0f; /* mid-point in the off-screen texture */ + SDL_FRect dest_rect; + float tilt_vec_x = SDL_sinf(last_xtilt * SDL_PI_F / 180.0f); + float tilt_vec_y = SDL_sinf(last_ytilt * SDL_PI_F / 180.0f); + int color = last_button + 1; + + if (!renderer) { + return; + } + + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); + SDL_SetRenderDrawColor(renderer, 0x40, 0x40, 0x40, 0xff); + SDL_RenderClear(renderer); + + if (offscreen_texture == NULL) { + offscreen_texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, (int)(X * 2.0f), (int)(Y * 2.0f)); + } + + /* Render into off-screen texture so we can do pixel-precise rendering later */ + window_texture = SDL_GetRenderTarget(renderer); + SDL_SetRenderTarget(renderer, offscreen_texture); + + /* Rendering starts here */ + SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); + SDL_SetRenderDrawColor(renderer, 0x40, 0x40, 0x40, 0xff); + SDL_RenderClear(renderer); + + SDL_SetRenderDrawColor(renderer, 0xa0, 0xa0, 0xa0, 0xff); + if (last_touching) { + SDL_FRect rect; + + rect.x = 0; + rect.y = 0; + rect.w = 2.0f * X - 1.0f; + rect.h = 2.0f * Y - 1.0f; + + SDL_RenderRect(renderer, &rect); + } else { + /* Show where the pen is rotating when it isn't touching the surface. + Otherwise we draw the rotation angle below together with pressure information. */ + float rot_vecx = SDL_sinf(last_rotation / 180.0f * SDL_PI_F); + float rot_vecy = -SDL_cosf(last_rotation / 180.0f * SDL_PI_F); + float px = X + rot_vecx * 100.0f; + float py = Y + rot_vecy * 100.0f; + float px2 = X + rot_vecx * 80.0f; + float py2 = Y + rot_vecy * 80.0f; + + SDL_RenderLine(renderer, + px, py, + px2 + rot_vecy * 20.0f, + py2 - rot_vecx * 20.0f); + SDL_RenderLine(renderer, + px, py, + px2 - rot_vecy * 20.0f, + py2 + rot_vecx * 20.0f); + } + + if (last_was_eraser) { + SDL_FRect rect; + + rect.x = X - 10.0f; + rect.y = Y - 10.0f; + rect.w = 21.0f; + rect.h = 21.0f; + + SDL_SetRenderDrawColor(renderer, 0x00, 0xff, 0xff, 0xff); + SDL_RenderFillRect(renderer, &rect); + } else { + float distance = last_distance * 50.0f; + + SDL_SetRenderDrawColor(renderer, 0xff, 0, 0, 0xff); + SDL_RenderLine(renderer, + X - 10.0f - distance, Y, + X - distance, Y); + SDL_RenderLine(renderer, + X + 10.0f + distance, Y, + X + distance, Y); + SDL_RenderLine(renderer, + X, Y - 10.0f - distance, + X, Y - distance); + SDL_RenderLine(renderer, + X, Y + 10.0f + distance, + X, Y + distance); + + } + + /* Draw a cone based on the direction the pen is leaning as if it were shining a light. */ + /* Colour derived from pens, intensity based on pressure: */ + SDL_SetRenderDrawColor(renderer, + (color & 0x01) ? 0xff : 0, + (color & 0x02) ? 0xff : 0, + (color & 0x04) ? 0xff : 0, + (int)(0xff)); + + xdelta = -tilt_vec_x * 100.0f; + ydelta = -tilt_vec_y * 100.0f; + endx = X + xdelta; + endy = Y + ydelta; + SDL_RenderLine(renderer, X, Y, endx, endy); + + SDL_SetRenderDrawColor(renderer, + (color & 0x01) ? 0xff : 0, + (color & 0x02) ? 0xff : 0, + (color & 0x04) ? 0xff : 0, + (int)(0xff * last_pressure)); + /* Cone base width based on pressure: */ + SDL_RenderLine(renderer, X, Y, endx + (ydelta * last_pressure / 3.0f), endy - (xdelta * last_pressure / 3.0f)); + SDL_RenderLine(renderer, X, Y, endx - (ydelta * last_pressure / 3.0f), endy + (xdelta * last_pressure / 3.0f)); + + /* If tilt is very small (or zero, for pens that don't have tilt), add some extra lines, rotated by the current rotation value */ + if (ALWAYS_SHOW_PRESSURE_BOX || (fabs(tilt_vec_x) < 0.2f && fabs(tilt_vec_y) < 0.2f)) { + int rot; + float pressure = last_pressure * 80.0f; + + /* Four times, rotated 90 degrees, so that we get a box */ + for (rot = 0; rot < 4; ++rot) { + + float vecx = SDL_cosf((last_rotation + (rot * 90.0f)) / 180.0f * SDL_PI_F); + float vecy = SDL_sinf((last_rotation + (rot * 90.0f)) / 180.0f * SDL_PI_F); + + float px = X + vecx * pressure; + float py = Y + vecy * pressure; + + SDL_RenderLine(renderer, + px + vecy * 10.0f, py - vecx * 10.0f, + px - vecy * 10.0f, py + vecx * 10.0f); + + if (rot == 3) { + int r = 0; + for (; r >= 0; r -= 2) { + float delta = 10.0f - ((float) r); + + SDL_RenderLine(renderer, + px + vecy * delta, py - vecx * delta, + px + (vecx * pressure * 0.4f), + py + (vecy * pressure * 0.4f)); + SDL_RenderLine(renderer, + px - vecy * delta, py + vecx * delta, + px + (vecx * pressure * 0.4f), + py + (vecy * pressure * 0.4f)); + } + } + } + } + + SDL_SetRenderTarget(renderer, window_texture); + /* Now render to pixel-precise position */ + dest_rect.x = last_x - X; + dest_rect.y = last_y - Y; + dest_rect.w = X * 2.0f; + dest_rect.h = Y * 2.0f; + SDL_RenderTexture(renderer, offscreen_texture, NULL, &dest_rect); + SDL_RenderPresent(renderer); +} + +static void dump_state(void) +{ + int i; + int pens_nr; + + /* Make sure this also works with a NULL parameter */ + SDL_PenID* pens = SDL_GetPens(NULL); + if (pens) { + SDL_free(pens); + } + + pens = SDL_GetPens(&pens_nr); + if (!pens) { + SDL_Log("Couldn't get pens: %s\n", SDL_GetError()); + return; + } + SDL_Log("Found %d pens (terminated by %u)\n", pens_nr, (unsigned) pens[pens_nr]); + + for (i = 0; i < pens_nr; ++i) { + SDL_PenID penid = pens[i]; + SDL_GUID guid = SDL_GetPenGUID(penid); + char guid_str[33]; + float axes[SDL_PEN_NUM_AXES]; + float x, y; + int k; + SDL_PenCapabilityInfo info; + Uint32 status = SDL_GetPenStatus(penid, &x, &y, axes, SDL_PEN_NUM_AXES); + Uint32 capabilities = SDL_GetPenCapabilities(penid, &info); + char *type; + char *buttons_str; + + SDL_GUIDToString(guid, guid_str, 33); + + switch (SDL_GetPenType(penid)) { + case SDL_PEN_TYPE_ERASER: + type = "Eraser"; + break; + case SDL_PEN_TYPE_PEN: + type = "Pen"; + break; + case SDL_PEN_TYPE_PENCIL: + type = "Pencil"; + break; + case SDL_PEN_TYPE_BRUSH: + type = "Brush"; + break; + case SDL_PEN_TYPE_AIRBRUSH: + type = "Airbrush"; + break; + default: + type = "Unknown (bug?)"; + } + + switch (info.num_buttons) { + case SDL_PEN_INFO_UNKNOWN: + SDL_asprintf(&buttons_str, "? buttons"); + break; + case 1: + SDL_asprintf(&buttons_str, "1 button"); + break; + default: + SDL_asprintf(&buttons_str, "%d button", info.num_buttons); + break; + } + + SDL_Log("%s %lu: [%s] attached=%d, %s [cap= %08lx:%08lx =status] '%s'\n", + type, + (unsigned long) penid, guid_str, + SDL_PenConnected(penid), /* should always be SDL_TRUE during iteration */ + buttons_str, + (unsigned long) capabilities, + (unsigned long) status, + SDL_GetPenName(penid)); + SDL_free(buttons_str); + SDL_Log(" pos=(%.2f, %.2f)", x, y); + for (k = 0; k < SDL_PEN_NUM_AXES; ++k) { + SDL_bool supported = capabilities & SDL_PEN_AXIS_CAPABILITY(k); + if (supported) { + if (k == SDL_PEN_AXIS_XTILT || k == SDL_PEN_AXIS_YTILT) { + if (info.max_tilt == SDL_PEN_INFO_UNKNOWN) { + SDL_Log(" axis %d: %.3f (max tilt unknown)", k, axes[k]); + } else { + SDL_Log(" axis %d: %.3f (tilt -%.1f..%.1f)", k, axes[k], + info.max_tilt, info.max_tilt); + } + } else { + SDL_Log(" axis %d: %.3f", k, axes[k]); + } + } else { + SDL_Log(" axis %d: unsupported (%.3f)", k, axes[k]); + } + } + } + SDL_free(pens); +} + +static void update_axes(float *axes) +{ + last_xtilt = axes[SDL_PEN_AXIS_XTILT]; + last_ytilt = axes[SDL_PEN_AXIS_YTILT]; + last_pressure = axes[SDL_PEN_AXIS_PRESSURE]; + last_distance = axes[SDL_PEN_AXIS_DISTANCE]; + last_rotation = axes[SDL_PEN_AXIS_ROTATION]; +} + +static void process_event(SDL_Event event) +{ + SDLTest_CommonEvent(state, &event, &quitting); + + switch (event.type) { + case SDL_EVENT_KEY_DOWN: + { + dump_state(); + break; + } + case SDL_EVENT_MOUSE_MOTION: + case SDL_EVENT_MOUSE_BUTTON_DOWN: + case SDL_EVENT_MOUSE_BUTTON_UP: +#if VERBOSE + { + float x, y; + SDL_GetMouseState(&x, &y); + if (event.type == SDL_EVENT_MOUSE_MOTION) { + SDL_Log("[%lu] mouse motion: mouse ID %d is at (%.2f, %.2f) (state: %.2f,%.2f) delta (%.2f, %.2f)\n", + event.motion.timestamp, + event.motion.which, + event.motion.x, event.motion.y, + event.motion.xrel, event.motion.yrel, + x, y); + } else { + SDL_Log("[%lu] mouse button: mouse ID %d is at (%.2f, %.2f) (state: %.2f,%.2f)\n", + event.button.timestamp, + event.button.which, + event.button.x, event.button.y, + x, y); + } + } +#endif + if (event.motion.which != SDL_PEN_MOUSEID) { + SDL_ShowCursor(); + } break; + + case SDL_EVENT_PEN_MOTION: + { + SDL_PenMotionEvent *ev = &event.pmotion; + + SDL_HideCursor(); + last_x = ev->x; + last_y = ev->y; + update_axes(ev->axes); + last_was_eraser = ev->pen_state & SDL_PEN_ERASER_MASK; +#if VERBOSE + SDL_Log("[%lu] pen motion: %s %u at (%.4f, %.4f); pressure=%.3f, tilt=%.3f/%.3f, dist=%.3f [buttons=%02x]\n", + (unsigned long) ev->timestamp, + last_was_eraser ? "eraser" : "pen", + (unsigned int)ev->which, ev->x, ev->y, last_pressure, last_xtilt, last_ytilt, last_distance, + ev->pen_state); +#endif + } break; + + case SDL_EVENT_PEN_UP: + case SDL_EVENT_PEN_DOWN: { + SDL_PenTipEvent *ev = &event.ptip; + last_x = ev->x; + last_y = ev->y; + update_axes(ev->axes); + last_was_eraser = ev->tip == SDL_PEN_TIP_ERASER; + last_button = ev->pen_state & 0xf; /* button mask */ + last_touching = (event.type == SDL_EVENT_PEN_DOWN); + } break; + + case SDL_EVENT_PEN_BUTTON_UP: + case SDL_EVENT_PEN_BUTTON_DOWN: + { + SDL_PenButtonEvent *ev = &event.pbutton; + + SDL_HideCursor(); + last_x = ev->x; + last_y = ev->y; + update_axes(ev->axes); + if (last_pressure > 0.0f && !last_touching) { + SDL_LogWarn(SDL_LOG_CATEGORY_TEST, + "[%lu] : reported pressure %.5f even though pen is not touching surface", + (unsigned long) ev->timestamp, last_pressure); + + } + last_was_eraser = ev->pen_state & SDL_PEN_ERASER_MASK; + last_button = ev->pen_state & 0xf; /* button mask */ + if ((ev->pen_state & SDL_PEN_DOWN_MASK) && !last_touching) { + SDL_LogWarn(SDL_LOG_CATEGORY_TEST, + "[%lu] : reported flags %x (SDL_PEN_FLAG_DOWN_MASK) despite not receiving SDL_EVENT_PEN_DOWN", + (unsigned long) ev->timestamp, ev->pen_state); + + } + if (!(ev->pen_state & SDL_PEN_DOWN_MASK) && last_touching) { + SDL_LogWarn(SDL_LOG_CATEGORY_TEST, + "[%lu] : reported flags %x (no SDL_PEN_FLAG_DOWN_MASK) despite receiving SDL_EVENT_PEN_DOWN without SDL_EVENT_PEN_UP afterwards", + (unsigned long) ev->timestamp, ev->pen_state); + + } +#if VERBOSE + SDL_Log("[%lu] pen button: %s %u at (%.4f, %.4f); BUTTON %d reported %s with event %s [pressure=%.3f, tilt=%.3f/%.3f, dist=%.3f]\n", + (unsigned long) ev->timestamp, + last_was_eraser ? "eraser" : "pen", + (unsigned int)ev->which, ev->x, ev->y, + ev->button, + (ev->state == SDL_PRESSED) ? "PRESSED" + : ((ev->state == SDL_RELEASED) ? "RELEASED" : "--invalid--"), + event.type == SDL_EVENT_PEN_BUTTON_UP ? "PENBUTTONUP" : "PENBUTTONDOWN", + last_pressure, last_xtilt, last_ytilt, last_distance); +#endif + } break; + + case SDL_EVENT_WINDOW_PEN_ENTER: + SDL_Log("[%lu] Pen %lu entered window %lx", + (unsigned long) event.window.timestamp, + (unsigned long) event.window.data1, + (unsigned long) event.window.windowID); + break; + + case SDL_EVENT_WINDOW_PEN_LEAVE: + SDL_Log("[%lu] Pen %lu left window %lx", + (unsigned long) event.window.timestamp, + (unsigned long) event.window.data1, + (unsigned long) event.window.windowID); + break; + +#if VERBOSE + case SDL_EVENT_WINDOW_MOUSE_ENTER: + SDL_Log("[%lu] Mouse entered window %lx", + (unsigned long) event.window.timestamp, + (unsigned long) event.window.windowID); + break; + + case SDL_EVENT_WINDOW_MOUSE_LEAVE: + SDL_Log("[%lu] Mouse left window %lx", + (unsigned long) event.window.timestamp, + (unsigned long) event.window.windowID); + break; +#endif + + default: + break; + } +} + +static void loop(void) +{ + SDL_Event event; + int i; + + for (i = 0; i < state->num_windows; ++i) { + if (state->renderers[i]) { + DrawScreen(state->renderers[i]); + } + } + + if (SDL_WaitEventTimeout(&event, 10)) { + process_event(event); + } + while (SDL_PollEvent(&event)) { + process_event(event); + } +} + +int main(int argc, char *argv[]) +{ + state = SDLTest_CommonCreateState(argv, SDL_INIT_VIDEO); + if (!state) { + return 1; + } + + SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "2"); + + state->window_title = "Pressure-Sensitive Pen Test"; + state->window_w = WIDTH; + state->window_h = HEIGHT; + state->skip_renderer = SDL_FALSE; + + if (!SDLTest_CommonDefaultArgs(state, argc, argv) || !SDLTest_CommonInit(state)) { + SDLTest_CommonQuit(state); + return 1; + } + + while (!quitting) { + loop(); + } + + SDLTest_CommonQuit(state); + return 0; +}