1/*
2 Simple DirectMedia Layer
3 Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
4
5 This software is provided 'as-is', without any express or implied
6 warranty. In no event will the authors be held liable for any damages
7 arising from the use of this software.
8
9 Permission is granted to anyone to use this software for any purpose,
10 including commercial applications, and to alter it and redistribute it
11 freely, subject to the following restrictions:
12
13 1. The origin of this software must not be misrepresented; you must not
14 claim that you wrote the original software. If you use this software
15 in a product, an acknowledgment in the product documentation would be
16 appreciated but is not required.
17 2. Altered source versions must be plainly marked as such, and must not be
18 misrepresented as being the original software.
19 3. This notice may not be removed or altered from any source distribution.
20*/
21#include "../../SDL_internal.h"
22
23#include "../../events/SDL_pen_c.h"
24#include "../SDL_sysvideo.h"
25#include "SDL_x11pen.h"
26#include "SDL_x11video.h"
27#include "SDL_x11xinput2.h"
28
29#ifdef SDL_VIDEO_DRIVER_X11_XINPUT2
30
31// Does this device have a valuator for pressure sensitivity?
32static bool X11_XInput2DeviceIsPen(SDL_VideoDevice *_this, const XIDeviceInfo *dev)
33{
34 const SDL_VideoData *data = _this->internal;
35 for (int i = 0; i < dev->num_classes; i++) {
36 const XIAnyClassInfo *classinfo = dev->classes[i];
37 if (classinfo->type == XIValuatorClass) {
38 const XIValuatorClassInfo *val_classinfo = (const XIValuatorClassInfo *)classinfo;
39 if (val_classinfo->label == data->atoms.pen_atom_abs_pressure) {
40 return true;
41 }
42 }
43 }
44
45 return false;
46}
47
48// Heuristically determines if device is an eraser
49static bool X11_XInput2PenIsEraser(SDL_VideoDevice *_this, int deviceid, char *devicename)
50{
51 #define PEN_ERASER_NAME_TAG "eraser" // String constant to identify erasers
52 SDL_VideoData *data = _this->internal;
53
54 if (data->atoms.pen_atom_wacom_tool_type != None) {
55 Atom type_return;
56 int format_return;
57 unsigned long num_items_return;
58 unsigned long bytes_after_return;
59 unsigned char *tooltype_name_info = NULL;
60
61 // Try Wacom-specific method
62 if (Success == X11_XIGetProperty(data->display, deviceid,
63 data->atoms.pen_atom_wacom_tool_type,
64 0, 32, False,
65 AnyPropertyType, &type_return, &format_return,
66 &num_items_return, &bytes_after_return,
67 &tooltype_name_info) &&
68 tooltype_name_info != NULL && num_items_return > 0) {
69
70 bool result = false;
71 char *tooltype_name = NULL;
72
73 if (type_return == XA_ATOM) {
74 // Atom instead of string? Un-intern
75 Atom atom = *((Atom *)tooltype_name_info);
76 if (atom != None) {
77 tooltype_name = X11_XGetAtomName(data->display, atom);
78 }
79 } else if (type_return == XA_STRING && format_return == 8) {
80 tooltype_name = (char *)tooltype_name_info;
81 }
82
83 if (tooltype_name) {
84 if (SDL_strcasecmp(tooltype_name, PEN_ERASER_NAME_TAG) == 0) {
85 result = true;
86 }
87 if (tooltype_name != (char *)tooltype_name_info) {
88 X11_XFree(tooltype_name_info);
89 }
90 X11_XFree(tooltype_name);
91
92 return result;
93 }
94 }
95 }
96
97 // Non-Wacom device?
98
99 /* We assume that a device is an eraser if its name contains the string "eraser".
100 * Unfortunately there doesn't seem to be a clean way to distinguish these cases (as of 2022-03). */
101 return (SDL_strcasestr(devicename, PEN_ERASER_NAME_TAG)) ? true : false;
102}
103
104// Read out an integer property and store into a preallocated Sint32 array, extending 8 and 16 bit values suitably.
105// Returns number of Sint32s written (<= max_words), or 0 on error.
106static size_t X11_XInput2PenGetIntProperty(SDL_VideoDevice *_this, int deviceid, Atom property, Sint32 *dest, size_t max_words)
107{
108 const SDL_VideoData *data = _this->internal;
109 Atom type_return;
110 int format_return;
111 unsigned long num_items_return;
112 unsigned long bytes_after_return;
113 unsigned char *output;
114
115 if (property == None) {
116 return 0;
117 }
118
119 if (Success != X11_XIGetProperty(data->display, deviceid,
120 property,
121 0, max_words, False,
122 XA_INTEGER, &type_return, &format_return,
123 &num_items_return, &bytes_after_return,
124 &output) ||
125 num_items_return == 0 || output == NULL) {
126 return 0;
127 }
128
129 if (type_return == XA_INTEGER) {
130 int k;
131 const int to_copy = SDL_min(max_words, num_items_return);
132
133 if (format_return == 8) {
134 Sint8 *numdata = (Sint8 *)output;
135 for (k = 0; k < to_copy; ++k) {
136 dest[k] = numdata[k];
137 }
138 } else if (format_return == 16) {
139 Sint16 *numdata = (Sint16 *)output;
140 for (k = 0; k < to_copy; ++k) {
141 dest[k] = numdata[k];
142 }
143 } else {
144 SDL_memcpy(dest, output, sizeof(Sint32) * to_copy);
145 }
146 X11_XFree(output);
147 return to_copy;
148 }
149
150 return 0; // type mismatch
151}
152
153// Identify Wacom devices (if true is returned) and extract their device type and serial IDs
154static bool X11_XInput2PenWacomDeviceID(SDL_VideoDevice *_this, int deviceid, Uint32 *wacom_devicetype_id, Uint32 *wacom_serial)
155{
156 SDL_VideoData *data = _this->internal;
157 Sint32 serial_id_buf[3];
158 int result;
159
160 if ((result = X11_XInput2PenGetIntProperty(_this, deviceid, data->atoms.pen_atom_wacom_serial_ids, serial_id_buf, 3)) == 3) {
161 *wacom_devicetype_id = serial_id_buf[2];
162 *wacom_serial = serial_id_buf[1];
163 return true;
164 }
165
166 *wacom_devicetype_id = *wacom_serial = 0;
167 return false;
168}
169
170
171typedef struct FindPenByDeviceIDData
172{
173 int x11_deviceid;
174 void *handle;
175} FindPenByDeviceIDData;
176
177static bool FindPenByDeviceID(void *handle, void *userdata)
178{
179 const X11_PenHandle *x11_handle = (const X11_PenHandle *) handle;
180 FindPenByDeviceIDData *data = (FindPenByDeviceIDData *) userdata;
181 if (x11_handle->x11_deviceid != data->x11_deviceid) {
182 return false;
183 }
184 data->handle = handle;
185 return true;
186}
187
188X11_PenHandle *X11_FindPenByDeviceID(int deviceid)
189{
190 FindPenByDeviceIDData data;
191 data.x11_deviceid = deviceid;
192 data.handle = NULL;
193 SDL_FindPenByCallback(FindPenByDeviceID, &data);
194 return (X11_PenHandle *) data.handle;
195}
196
197static X11_PenHandle *X11_MaybeAddPen(SDL_VideoDevice *_this, const XIDeviceInfo *dev)
198{
199 SDL_VideoData *data = _this->internal;
200 SDL_PenCapabilityFlags capabilities = 0;
201 X11_PenHandle *handle = NULL;
202
203 if ((dev->use != XISlavePointer && (dev->use != XIFloatingSlave)) || dev->enabled == 0 || !X11_XInput2DeviceIsPen(_this, dev)) {
204 return NULL; // Only track physical devices that are enabled and look like pens
205 } else if ((handle = X11_FindPenByDeviceID(dev->deviceid)) != 0) {
206 return handle; // already have this pen, skip it.
207 } else if ((handle = SDL_calloc(1, sizeof (*handle))) == NULL) {
208 return NULL; // oh well.
209 }
210
211 for (int i = 0; i < SDL_arraysize(handle->valuator_for_axis); i++) {
212 handle->valuator_for_axis[i] = SDL_X11_PEN_AXIS_VALUATOR_MISSING; // until proven otherwise
213 }
214
215 int total_buttons = 0;
216 for (int i = 0; i < dev->num_classes; i++) {
217 const XIAnyClassInfo *classinfo = dev->classes[i];
218 if (classinfo->type == XIButtonClass) {
219 const XIButtonClassInfo *button_classinfo = (const XIButtonClassInfo *)classinfo;
220 total_buttons += button_classinfo->num_buttons;
221 } else if (classinfo->type == XIValuatorClass) {
222 const XIValuatorClassInfo *val_classinfo = (const XIValuatorClassInfo *)classinfo;
223 const Sint8 valuator_nr = val_classinfo->number;
224 const Atom vname = val_classinfo->label;
225 const float min = (float)val_classinfo->min;
226 const float max = (float)val_classinfo->max;
227 bool use_this_axis = true;
228 SDL_PenAxis axis = SDL_PEN_AXIS_COUNT;
229
230 // afaict, SDL_PEN_AXIS_DISTANCE is never reported by XInput2 (Wayland can offer it, though)
231 if (vname == data->atoms.pen_atom_abs_pressure) {
232 axis = SDL_PEN_AXIS_PRESSURE;
233 } else if (vname == data->atoms.pen_atom_abs_tilt_x) {
234 axis = SDL_PEN_AXIS_XTILT;
235 } else if (vname == data->atoms.pen_atom_abs_tilt_y) {
236 axis = SDL_PEN_AXIS_YTILT;
237 } else {
238 use_this_axis = false;
239 }
240
241 // !!! FIXME: there are wacom-specific hacks for getting SDL_PEN_AXIS_(ROTATION|SLIDER) on some devices, but for simplicity, we're skipping all that for now.
242
243 if (use_this_axis) {
244 capabilities |= SDL_GetPenCapabilityFromAxis(axis);
245 handle->valuator_for_axis[axis] = valuator_nr;
246 handle->axis_min[axis] = min;
247 handle->axis_max[axis] = max;
248 }
249 }
250 }
251
252 // We have a pen if and only if the device measures pressure.
253 // We checked this in X11_XInput2DeviceIsPen, so just assert it here.
254 SDL_assert(capabilities & SDL_PEN_CAPABILITY_PRESSURE);
255
256 const bool is_eraser = X11_XInput2PenIsEraser(_this, dev->deviceid, dev->name);
257 Uint32 wacom_devicetype_id = 0;
258 Uint32 wacom_serial = 0;
259 X11_XInput2PenWacomDeviceID(_this, dev->deviceid, &wacom_devicetype_id, &wacom_serial);
260
261 SDL_PenInfo peninfo;
262 SDL_zero(peninfo);
263 peninfo.capabilities = capabilities;
264 peninfo.max_tilt = -1;
265 peninfo.wacom_id = wacom_devicetype_id;
266 peninfo.num_buttons = total_buttons;
267 peninfo.subtype = is_eraser ? SDL_PEN_TYPE_ERASER : SDL_PEN_TYPE_PEN;
268 if (is_eraser) {
269 peninfo.capabilities |= SDL_PEN_CAPABILITY_ERASER;
270 }
271
272 handle->is_eraser = is_eraser;
273 handle->x11_deviceid = dev->deviceid;
274
275 handle->pen = SDL_AddPenDevice(0, dev->name, &peninfo, handle);
276 if (!handle->pen) {
277 SDL_free(handle);
278 return NULL;
279 }
280
281 return handle;
282}
283
284X11_PenHandle *X11_MaybeAddPenByDeviceID(SDL_VideoDevice *_this, int deviceid)
285{
286 SDL_VideoData *data = _this->internal;
287 int num_device_info = 0;
288 XIDeviceInfo *device_info = X11_XIQueryDevice(data->display, deviceid, &num_device_info);
289 if (device_info) {
290 SDL_assert(num_device_info == 1);
291 X11_PenHandle *handle = X11_MaybeAddPen(_this, device_info);
292 X11_XIFreeDeviceInfo(device_info);
293 return handle;
294 }
295 return NULL;
296}
297
298void X11_RemovePenByDeviceID(int deviceid)
299{
300 X11_PenHandle *handle = X11_FindPenByDeviceID(deviceid);
301 if (handle) {
302 SDL_RemovePenDevice(0, handle->pen);
303 SDL_free(handle);
304 }
305}
306
307void X11_InitPen(SDL_VideoDevice *_this)
308{
309 SDL_VideoData *data = _this->internal;
310
311 #define LOOKUP_PEN_ATOM(X) X11_XInternAtom(data->display, X, False)
312 data->atoms.pen_atom_device_product_id = LOOKUP_PEN_ATOM("Device Product ID");
313 data->atoms.pen_atom_wacom_serial_ids = LOOKUP_PEN_ATOM("Wacom Serial IDs");
314 data->atoms.pen_atom_wacom_tool_type = LOOKUP_PEN_ATOM("Wacom Tool Type");
315 data->atoms.pen_atom_abs_pressure = LOOKUP_PEN_ATOM("Abs Pressure");
316 data->atoms.pen_atom_abs_tilt_x = LOOKUP_PEN_ATOM("Abs Tilt X");
317 data->atoms.pen_atom_abs_tilt_y = LOOKUP_PEN_ATOM("Abs Tilt Y");
318 #undef LOOKUP_PEN_ATOM
319
320 // Do an initial check on devices. After this, we'll add/remove individual pens when XI_HierarchyChanged events alert us.
321 int num_device_info = 0;
322 XIDeviceInfo *device_info = X11_XIQueryDevice(data->display, XIAllDevices, &num_device_info);
323 if (device_info) {
324 for (int i = 0; i < num_device_info; i++) {
325 X11_MaybeAddPen(_this, &device_info[i]);
326 }
327 X11_XIFreeDeviceInfo(device_info);
328 }
329}
330
331static void X11_FreePenHandle(SDL_PenID instance_id, void *handle, void *userdata)
332{
333 SDL_free(handle);
334}
335
336void X11_QuitPen(SDL_VideoDevice *_this)
337{
338 SDL_RemoveAllPenDevices(X11_FreePenHandle, NULL);
339}
340
341static void X11_XInput2NormalizePenAxes(const X11_PenHandle *pen, float *coords)
342{
343 // Normalise axes
344 for (int axis = 0; axis < SDL_PEN_AXIS_COUNT; ++axis) {
345 const int valuator = pen->valuator_for_axis[axis];
346 if (valuator == SDL_X11_PEN_AXIS_VALUATOR_MISSING) {
347 continue;
348 }
349
350 float value = coords[axis];
351 const float min = pen->axis_min[axis];
352 const float max = pen->axis_max[axis];
353
354 if (axis == SDL_PEN_AXIS_SLIDER) {
355 value += pen->slider_bias;
356 }
357
358 // min ... 0 ... max
359 if (min < 0.0) {
360 // Normalise so that 0 remains 0.0
361 if (value < 0) {
362 value = value / (-min);
363 } else {
364 if (max == 0.0f) {
365 value = 0.0f;
366 } else {
367 value = value / max;
368 }
369 }
370 } else {
371 // 0 ... min ... max
372 // including 0.0 = min
373 if (max == 0.0f) {
374 value = 0.0f;
375 } else {
376 value = (value - min) / max;
377 }
378 }
379
380 switch (axis) {
381 case SDL_PEN_AXIS_XTILT:
382 case SDL_PEN_AXIS_YTILT:
383 //if (peninfo->info.max_tilt > 0.0f) {
384 // value *= peninfo->info.max_tilt; // normalize to physical max
385 //}
386 break;
387
388 case SDL_PEN_AXIS_ROTATION:
389 // normalised to -1..1, so let's convert to degrees
390 value *= 180.0f;
391 value += pen->rotation_bias;
392
393 // handle simple over/underflow
394 if (value >= 180.0f) {
395 value -= 360.0f;
396 } else if (value < -180.0f) {
397 value += 360.0f;
398 }
399 break;
400
401 default:
402 break;
403 }
404
405 coords[axis] = value;
406 }
407}
408
409void X11_PenAxesFromValuators(const X11_PenHandle *pen,
410 const double *input_values, const unsigned char *mask, int mask_len,
411 float axis_values[SDL_PEN_AXIS_COUNT])
412{
413 for (int i = 0; i < SDL_PEN_AXIS_COUNT; i++) {
414 const int valuator = pen->valuator_for_axis[i];
415 if ((valuator == SDL_X11_PEN_AXIS_VALUATOR_MISSING) || (valuator >= mask_len * 8) || !(XIMaskIsSet(mask, valuator))) {
416 axis_values[i] = 0.0f;
417 } else {
418 axis_values[i] = (float)input_values[valuator];
419 }
420 }
421 X11_XInput2NormalizePenAxes(pen, axis_values);
422}
423
424#else
425
426void X11_InitPen(SDL_VideoDevice *_this)
427{
428 (void) _this;
429}
430
431void X11_QuitPen(SDL_VideoDevice *_this)
432{
433 (void) _this;
434}
435
436#endif // SDL_VIDEO_DRIVER_X11_XINPUT2
437
438