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#ifdef SDL_VIDEO_DRIVER_X11
24
25#include "SDL_x11video.h"
26
27#include "../../events/SDL_keyboard_c.h"
28#include "../../events/SDL_scancode_tables_c.h"
29
30#include <X11/keysym.h>
31#include <X11/XKBlib.h>
32
33#include "../../events/imKStoUCS.h"
34#include "../../events/SDL_keysym_to_scancode_c.h"
35#include "../../events/SDL_keysym_to_keycode_c.h"
36
37#ifdef X_HAVE_UTF8_STRING
38#include <locale.h>
39#endif
40
41static SDL_ScancodeTable scancode_set[] = {
42 SDL_SCANCODE_TABLE_DARWIN,
43 SDL_SCANCODE_TABLE_XFREE86_1,
44 SDL_SCANCODE_TABLE_XFREE86_2,
45 SDL_SCANCODE_TABLE_XVNC,
46};
47
48static bool X11_ScancodeIsRemappable(SDL_Scancode scancode)
49{
50 /*
51 * XKB remappings can assign different keysyms for these scancodes, but
52 * as these keys are in fixed positions, the scancodes themselves shouldn't
53 * be switched. Mark them as not being remappable.
54 */
55 switch (scancode) {
56 case SDL_SCANCODE_ESCAPE:
57 case SDL_SCANCODE_CAPSLOCK:
58 case SDL_SCANCODE_NUMLOCKCLEAR:
59 case SDL_SCANCODE_LSHIFT:
60 case SDL_SCANCODE_RSHIFT:
61 case SDL_SCANCODE_LCTRL:
62 case SDL_SCANCODE_RCTRL:
63 case SDL_SCANCODE_LALT:
64 case SDL_SCANCODE_RALT:
65 case SDL_SCANCODE_LGUI:
66 case SDL_SCANCODE_RGUI:
67 return false;
68 default:
69 return true;
70 }
71}
72
73// This function only correctly maps letters and numbers for keyboards in US QWERTY layout
74static SDL_Scancode X11_KeyCodeToSDLScancode(SDL_VideoDevice *_this, KeyCode keycode)
75{
76 const KeySym keysym = X11_KeyCodeToSym(_this, keycode, 0, 0);
77
78 if (keysym == NoSymbol) {
79 return SDL_SCANCODE_UNKNOWN;
80 }
81
82 return SDL_GetScancodeFromKeySym(keysym, keycode);
83}
84
85KeySym X11_KeyCodeToSym(SDL_VideoDevice *_this, KeyCode keycode, unsigned char group, unsigned int mod_mask)
86{
87 SDL_VideoData *data = _this->internal;
88 KeySym keysym;
89 unsigned int mods_ret[16];
90
91 SDL_zero(mods_ret);
92
93#ifdef SDL_VIDEO_DRIVER_X11_HAS_XKBLOOKUPKEYSYM
94 if (data->xkb.desc_ptr) {
95 int num_groups = XkbKeyNumGroups(data->xkb.desc_ptr, keycode);
96 unsigned char info = XkbKeyGroupInfo(data->xkb.desc_ptr, keycode);
97
98 if (num_groups && group >= num_groups) {
99
100 int action = XkbOutOfRangeGroupAction(info);
101
102 if (action == XkbRedirectIntoRange) {
103 group = XkbOutOfRangeGroupNumber(info);
104 if (group >= num_groups) {
105 group = 0;
106 }
107 } else if (action == XkbClampIntoRange) {
108 group = num_groups - 1;
109 } else {
110 group %= num_groups;
111 }
112 }
113
114 if (X11_XkbLookupKeySym(data->display, keycode, XkbBuildCoreState(mod_mask, group), mods_ret, &keysym) == NoSymbol) {
115 keysym = NoSymbol;
116 }
117 } else
118#endif
119 {
120 // TODO: Handle groups and modifiers on the legacy path.
121 keysym = X11_XKeycodeToKeysym(data->display, keycode, 0);
122 }
123
124 return keysym;
125}
126
127bool X11_InitKeyboard(SDL_VideoDevice *_this)
128{
129 SDL_VideoData *data = _this->internal;
130 int i = 0;
131 int j = 0;
132 int min_keycode, max_keycode;
133 struct
134 {
135 SDL_Scancode scancode;
136 KeySym keysym;
137 int value;
138 } fingerprint[] = {
139 { SDL_SCANCODE_HOME, XK_Home, 0 },
140 { SDL_SCANCODE_PAGEUP, XK_Prior, 0 },
141 { SDL_SCANCODE_UP, XK_Up, 0 },
142 { SDL_SCANCODE_LEFT, XK_Left, 0 },
143 { SDL_SCANCODE_DELETE, XK_Delete, 0 },
144 { SDL_SCANCODE_KP_ENTER, XK_KP_Enter, 0 },
145 };
146 int best_distance;
147 int best_index;
148 int distance;
149 Bool xkb_repeat = 0;
150
151#ifdef SDL_VIDEO_DRIVER_X11_HAS_XKBLOOKUPKEYSYM
152 {
153 int xkb_major = XkbMajorVersion;
154 int xkb_minor = XkbMinorVersion;
155
156 if (X11_XkbQueryExtension(data->display, NULL, &data->xkb.event, NULL, &xkb_major, &xkb_minor)) {
157 data->xkb.desc_ptr = X11_XkbGetMap(data->display, XkbAllClientInfoMask, XkbUseCoreKbd);
158 }
159
160 // This will remove KeyRelease events for held keys
161 X11_XkbSetDetectableAutoRepeat(data->display, True, &xkb_repeat);
162 }
163#endif
164
165 // Open a connection to the X input manager
166#ifdef X_HAVE_UTF8_STRING
167 if (SDL_X11_HAVE_UTF8) {
168 /* Set the locale, and call XSetLocaleModifiers before XOpenIM so that
169 Compose keys will work correctly. */
170 char *prev_locale = setlocale(LC_ALL, NULL);
171 char *prev_xmods = X11_XSetLocaleModifiers(NULL);
172
173 if (prev_locale) {
174 prev_locale = SDL_strdup(prev_locale);
175 }
176
177 if (prev_xmods) {
178 prev_xmods = SDL_strdup(prev_xmods);
179 }
180
181 (void)setlocale(LC_ALL, "");
182 X11_XSetLocaleModifiers("");
183
184 data->im = X11_XOpenIM(data->display, NULL, NULL, NULL);
185
186 /* Reset the locale + X locale modifiers back to how they were,
187 locale first because the X locale modifiers depend on it. */
188 (void)setlocale(LC_ALL, prev_locale);
189 X11_XSetLocaleModifiers(prev_xmods);
190
191 if (prev_locale) {
192 SDL_free(prev_locale);
193 }
194
195 if (prev_xmods) {
196 SDL_free(prev_xmods);
197 }
198 }
199#endif
200 // Try to determine which scancodes are being used based on fingerprint
201 best_distance = SDL_arraysize(fingerprint) + 1;
202 best_index = -1;
203 X11_XDisplayKeycodes(data->display, &min_keycode, &max_keycode);
204 for (i = 0; i < SDL_arraysize(fingerprint); ++i) {
205 fingerprint[i].value = X11_XKeysymToKeycode(data->display, fingerprint[i].keysym) - min_keycode;
206 }
207 for (i = 0; i < SDL_arraysize(scancode_set); ++i) {
208 int table_size;
209 const SDL_Scancode *table = SDL_GetScancodeTable(scancode_set[i], &table_size);
210
211 distance = 0;
212 for (j = 0; j < SDL_arraysize(fingerprint); ++j) {
213 if (fingerprint[j].value < 0 || fingerprint[j].value >= table_size) {
214 distance += 1;
215 } else if (table[fingerprint[j].value] != fingerprint[j].scancode) {
216 distance += 1;
217 }
218 }
219 if (distance < best_distance) {
220 best_distance = distance;
221 best_index = i;
222 }
223 }
224 if (best_index < 0 || best_distance > 2) {
225 // This is likely to be SDL_SCANCODE_TABLE_XFREE86_2 with remapped keys, double check a rarely remapped value
226 int fingerprint_value = X11_XKeysymToKeycode(data->display, 0x1008FF5B /* XF86Documents */) - min_keycode;
227 if (fingerprint_value == 235) {
228 for (i = 0; i < SDL_arraysize(scancode_set); ++i) {
229 if (scancode_set[i] == SDL_SCANCODE_TABLE_XFREE86_2) {
230 best_index = i;
231 best_distance = 0;
232 break;
233 }
234 }
235 }
236 }
237 if (best_index >= 0 && best_distance <= 2) {
238 int table_size;
239 const SDL_Scancode *table = SDL_GetScancodeTable(scancode_set[best_index], &table_size);
240
241#ifdef DEBUG_KEYBOARD
242 SDL_Log("Using scancode set %d, min_keycode = %d, max_keycode = %d, table_size = %d", best_index, min_keycode, max_keycode, table_size);
243#endif
244 // This should never happen, but just in case...
245 if (table_size > (SDL_arraysize(data->key_layout) - min_keycode)) {
246 table_size = (SDL_arraysize(data->key_layout) - min_keycode);
247 }
248 SDL_memcpy(&data->key_layout[min_keycode], table, sizeof(SDL_Scancode) * table_size);
249
250 /* Scancodes represent physical locations on the keyboard, unaffected by keyboard mapping.
251 However, there are a number of extended scancodes that have no standard location, so use
252 the X11 mapping for all non-character keys.
253 */
254 for (i = min_keycode; i <= max_keycode; ++i) {
255 SDL_Scancode scancode = X11_KeyCodeToSDLScancode(_this, i);
256#ifdef DEBUG_KEYBOARD
257 {
258 KeySym sym;
259 sym = X11_KeyCodeToSym(_this, (KeyCode)i, 0);
260 SDL_Log("code = %d, sym = 0x%X (%s) ", i - min_keycode,
261 (unsigned int)sym, sym == NoSymbol ? "NoSymbol" : X11_XKeysymToString(sym));
262 }
263#endif
264 if (scancode == data->key_layout[i]) {
265 continue;
266 }
267 if ((SDL_GetKeymapKeycode(NULL, scancode, SDL_KMOD_NONE) & (SDLK_SCANCODE_MASK | SDLK_EXTENDED_MASK)) && X11_ScancodeIsRemappable(scancode)) {
268 // Not a character key and the scancode is safe to remap
269#ifdef DEBUG_KEYBOARD
270 SDL_Log("Changing scancode, was %d (%s), now %d (%s)", data->key_layout[i], SDL_GetScancodeName(data->key_layout[i]), scancode, SDL_GetScancodeName(scancode));
271#endif
272 data->key_layout[i] = scancode;
273 }
274 }
275 } else {
276#ifdef DEBUG_SCANCODES
277 SDL_Log("Keyboard layout unknown, please report the following to the SDL forums/mailing list (https://discourse.libsdl.org/):");
278#endif
279
280 // Determine key_layout - only works on US QWERTY layout
281 for (i = min_keycode; i <= max_keycode; ++i) {
282 SDL_Scancode scancode = X11_KeyCodeToSDLScancode(_this, i);
283#ifdef DEBUG_SCANCODES
284 {
285 KeySym sym;
286 sym = X11_KeyCodeToSym(_this, (KeyCode)i, 0);
287 SDL_Log("code = %d, sym = 0x%X (%s) ", i - min_keycode,
288 (unsigned int)sym, sym == NoSymbol ? "NoSymbol" : X11_XKeysymToString(sym));
289 }
290 if (scancode == SDL_SCANCODE_UNKNOWN) {
291 SDL_Log("scancode not found");
292 } else {
293 SDL_Log("scancode = %d (%s)", scancode, SDL_GetScancodeName(scancode));
294 }
295#endif
296 data->key_layout[i] = scancode;
297 }
298 }
299
300 X11_UpdateKeymap(_this, false);
301
302 SDL_SetScancodeName(SDL_SCANCODE_APPLICATION, "Menu");
303
304 X11_ReconcileKeyboardState(_this);
305
306 return true;
307}
308
309static unsigned X11_GetNumLockModifierMask(SDL_VideoDevice *_this)
310{
311 SDL_VideoData *videodata = _this->internal;
312 Display *display = videodata->display;
313 unsigned num_mask = 0;
314 int i, j;
315 XModifierKeymap *xmods;
316 unsigned n;
317
318 xmods = X11_XGetModifierMapping(display);
319 n = xmods->max_keypermod;
320 for (i = 3; i < 8; i++) {
321 for (j = 0; j < n; j++) {
322 KeyCode kc = xmods->modifiermap[i * n + j];
323 if (videodata->key_layout[kc] == SDL_SCANCODE_NUMLOCKCLEAR) {
324 num_mask = 1 << i;
325 break;
326 }
327 }
328 }
329 X11_XFreeModifiermap(xmods);
330
331 return num_mask;
332}
333
334static unsigned X11_GetScrollLockModifierMask(SDL_VideoDevice *_this)
335{
336 SDL_VideoData *videodata = _this->internal;
337 Display *display = videodata->display;
338 unsigned num_mask = 0;
339 int i, j;
340 XModifierKeymap *xmods;
341 unsigned n;
342
343 xmods = X11_XGetModifierMapping(display);
344 n = xmods->max_keypermod;
345 for (i = 3; i < 8; i++) {
346 for (j = 0; j < n; j++) {
347 KeyCode kc = xmods->modifiermap[i * n + j];
348 if (videodata->key_layout[kc] == SDL_SCANCODE_SCROLLLOCK) {
349 num_mask = 1 << i;
350 break;
351 }
352 }
353 }
354 X11_XFreeModifiermap(xmods);
355
356 return num_mask;
357}
358
359void X11_UpdateKeymap(SDL_VideoDevice *_this, bool send_event)
360{
361 struct Keymod_masks
362 {
363 SDL_Keymod sdl_mask;
364 unsigned int xkb_mask;
365 } const keymod_masks[] = {
366 { SDL_KMOD_NONE, 0 },
367 { SDL_KMOD_SHIFT, ShiftMask },
368 { SDL_KMOD_CAPS, LockMask },
369 { SDL_KMOD_SHIFT | SDL_KMOD_CAPS, ShiftMask | LockMask },
370 { SDL_KMOD_MODE, Mod5Mask },
371 { SDL_KMOD_MODE | SDL_KMOD_SHIFT, Mod5Mask | ShiftMask },
372 { SDL_KMOD_MODE | SDL_KMOD_CAPS, Mod5Mask | LockMask },
373 { SDL_KMOD_MODE | SDL_KMOD_SHIFT | SDL_KMOD_CAPS, Mod5Mask | ShiftMask | LockMask },
374 { SDL_KMOD_LEVEL5, Mod3Mask },
375 { SDL_KMOD_LEVEL5 | SDL_KMOD_SHIFT, Mod3Mask | ShiftMask },
376 { SDL_KMOD_LEVEL5 | SDL_KMOD_CAPS, Mod3Mask | LockMask },
377 { SDL_KMOD_LEVEL5 | SDL_KMOD_SHIFT | SDL_KMOD_CAPS, Mod3Mask | ShiftMask | LockMask },
378 { SDL_KMOD_LEVEL5 | SDL_KMOD_MODE, Mod5Mask | Mod3Mask },
379 { SDL_KMOD_LEVEL5 | SDL_KMOD_MODE | SDL_KMOD_SHIFT, Mod3Mask | Mod5Mask | ShiftMask },
380 { SDL_KMOD_LEVEL5 | SDL_KMOD_MODE | SDL_KMOD_CAPS, Mod3Mask | Mod5Mask | LockMask },
381 { SDL_KMOD_LEVEL5 | SDL_KMOD_MODE | SDL_KMOD_SHIFT | SDL_KMOD_CAPS, Mod3Mask | Mod5Mask | ShiftMask | LockMask }
382 };
383
384 SDL_VideoData *data = _this->internal;
385 SDL_Scancode scancode;
386 SDL_Keymap *keymap = SDL_CreateKeymap();
387
388#ifdef SDL_VIDEO_DRIVER_X11_HAS_XKBLOOKUPKEYSYM
389 if (data->xkb.desc_ptr) {
390 XkbStateRec state;
391 X11_XkbGetUpdatedMap(data->display, XkbAllClientInfoMask, data->xkb.desc_ptr);
392
393 if (X11_XkbGetState(data->display, XkbUseCoreKbd, &state) == Success) {
394 data->xkb.current_group = state.group;
395 }
396 }
397#endif
398
399 for (int m = 0; m < SDL_arraysize(keymod_masks); ++m) {
400 for (int i = 0; i < SDL_arraysize(data->key_layout); ++i) {
401 // Make sure this is a valid scancode
402 scancode = data->key_layout[i];
403 if (scancode == SDL_SCANCODE_UNKNOWN) {
404 continue;
405 }
406
407 const KeySym keysym = X11_KeyCodeToSym(_this, i, data->xkb.current_group, keymod_masks[m].xkb_mask);
408
409 if (keysym != NoSymbol) {
410 SDL_Keycode keycode = SDL_GetKeyCodeFromKeySym(keysym, i, keymod_masks[m].sdl_mask);
411
412 if (!keycode) {
413 switch (scancode) {
414 case SDL_SCANCODE_RETURN:
415 keycode = SDLK_RETURN;
416 break;
417 case SDL_SCANCODE_ESCAPE:
418 keycode = SDLK_ESCAPE;
419 break;
420 case SDL_SCANCODE_BACKSPACE:
421 keycode = SDLK_BACKSPACE;
422 break;
423 case SDL_SCANCODE_DELETE:
424 keycode = SDLK_DELETE;
425 break;
426 default:
427 keycode = SDL_SCANCODE_TO_KEYCODE(scancode);
428 break;
429 }
430 }
431
432 SDL_SetKeymapEntry(keymap, scancode, keymod_masks[m].sdl_mask, keycode);
433 }
434 }
435 }
436
437 data->xkb.numlock_mask = X11_GetNumLockModifierMask(_this);
438 data->xkb.scrolllock_mask = X11_GetScrollLockModifierMask(_this);
439 SDL_SetKeymap(keymap, send_event);
440}
441
442void X11_QuitKeyboard(SDL_VideoDevice *_this)
443{
444 SDL_VideoData *data = _this->internal;
445
446#ifdef SDL_VIDEO_DRIVER_X11_HAS_XKBLOOKUPKEYSYM
447 if (data->xkb.desc_ptr) {
448 X11_XkbFreeKeyboard(data->xkb.desc_ptr, 0, True);
449 data->xkb.desc_ptr = NULL;
450 }
451#endif
452}
453
454void X11_ClearComposition(SDL_WindowData *data)
455{
456 if (data->preedit_length > 0) {
457 data->preedit_text[0] = '\0';
458 data->preedit_length = 0;
459 }
460
461 if (data->ime_needs_clear_composition) {
462 SDL_SendEditingText("", 0, 0);
463 data->ime_needs_clear_composition = false;
464 }
465}
466
467static void X11_SendEditingEvent(SDL_WindowData *data)
468{
469 if (data->preedit_length == 0) {
470 X11_ClearComposition(data);
471 return;
472 }
473
474 bool in_highlight = false;
475 int start = -1, length = 0, i;
476 for (i = 0; i < data->preedit_length; ++i) {
477 if (data->preedit_feedback[i] & (XIMReverse | XIMHighlight)) {
478 if (start < 0) {
479 start = i;
480 in_highlight = true;
481 }
482 } else if (in_highlight) {
483 // Found the end of the highlight
484 break;
485 }
486 }
487 if (in_highlight) {
488 length = (i - start);
489 } else {
490 start = SDL_clamp(data->preedit_cursor, 0, data->preedit_length);
491 }
492 SDL_SendEditingText(data->preedit_text, start, length);
493
494 data->ime_needs_clear_composition = true;
495}
496
497static int preedit_start_callback(XIC xic, XPointer client_data, XPointer call_data)
498{
499 // No limit on preedit text length
500 return -1;
501}
502
503static void preedit_done_callback(XIC xic, XPointer client_data, XPointer call_data)
504{
505}
506
507static void preedit_draw_callback(XIC xic, XPointer client_data, XIMPreeditDrawCallbackStruct *call_data)
508{
509 SDL_WindowData *data = (SDL_WindowData *)client_data;
510 int chg_first = SDL_clamp(call_data->chg_first, 0, data->preedit_length);
511 int chg_length = SDL_clamp(call_data->chg_length, 0, data->preedit_length - chg_first);
512
513 const char *start = data->preedit_text;
514 if (chg_length > 0) {
515 // Delete text in range
516 for (int i = 0; start && *start && i < chg_first; ++i) {
517 SDL_StepUTF8(&start, NULL);
518 }
519
520 const char *end = start;
521 for (int i = 0; end && *end && i < chg_length; ++i) {
522 SDL_StepUTF8(&end, NULL);
523 }
524
525 if (end > start) {
526 SDL_memmove((char *)start, end, SDL_strlen(end) + 1);
527 if ((chg_first + chg_length) > data->preedit_length) {
528 SDL_memmove(&data->preedit_feedback[chg_first], &data->preedit_feedback[chg_first + chg_length], (data->preedit_length - chg_first - chg_length) * sizeof(*data->preedit_feedback));
529 }
530 }
531 data->preedit_length -= chg_length;
532 }
533
534 XIMText *text = call_data->text;
535 if (text) {
536 // Insert text in range
537 SDL_assert(!text->encoding_is_wchar);
538
539 // The text length isn't calculated as directed by the spec, recalculate it now
540 if (text->string.multi_byte) {
541 text->length = SDL_utf8strlen(text->string.multi_byte);
542 }
543
544 size_t string_size = SDL_strlen(text->string.multi_byte);
545 size_t size = string_size + 1;
546 if (data->preedit_text) {
547 size += SDL_strlen(data->preedit_text);
548 }
549 char *preedit_text = (char *)SDL_malloc(size * sizeof(*preedit_text));
550 if (preedit_text) {
551 size_t pre_size = (start - data->preedit_text);
552 size_t post_size = start ? SDL_strlen(start) : 0;
553 if (pre_size > 0) {
554 SDL_memcpy(&preedit_text[0], data->preedit_text, pre_size);
555 }
556 SDL_memcpy(&preedit_text[pre_size], text->string.multi_byte, string_size);
557 if (post_size > 0) {
558 SDL_memcpy(&preedit_text[pre_size + string_size], start, post_size);
559 }
560 preedit_text[size - 1] = '\0';
561 }
562
563 size_t feedback_size = data->preedit_length + text->length;
564 XIMFeedback *feedback = (XIMFeedback *)SDL_malloc(feedback_size * sizeof(*feedback));
565 if (feedback) {
566 size_t pre_size = (size_t)chg_first;
567 size_t post_size = (size_t)data->preedit_length - pre_size;
568 if (pre_size > 0) {
569 SDL_memcpy(&feedback[0], data->preedit_feedback, pre_size * sizeof(*feedback));
570 }
571 SDL_memcpy(&feedback[pre_size], text->feedback, text->length * sizeof(*feedback));
572 if (post_size > 0) {
573 SDL_memcpy(&feedback[pre_size + text->length], &data->preedit_feedback[pre_size], post_size * sizeof(*feedback));
574 }
575 }
576
577 if (preedit_text && feedback) {
578 SDL_free(data->preedit_text);
579 data->preedit_text = preedit_text;
580
581 SDL_free(data->preedit_feedback);
582 data->preedit_feedback = feedback;
583
584 data->preedit_length += text->length;
585 } else {
586 SDL_free(preedit_text);
587 SDL_free(feedback);
588 }
589 }
590
591 data->preedit_cursor = call_data->caret;
592
593#ifdef DEBUG_XIM
594 if (call_data->chg_length > 0) {
595 SDL_Log("Draw callback deleted %d characters at %d", call_data->chg_length, call_data->chg_first);
596 }
597 if (text) {
598 SDL_Log("Draw callback inserted %s at %d, caret: %d", text->string.multi_byte, call_data->chg_first, call_data->caret);
599 }
600 SDL_Log("Pre-edit text: %s", data->preedit_text);
601#endif
602
603 X11_SendEditingEvent(data);
604}
605
606static void preedit_caret_callback(XIC xic, XPointer client_data, XIMPreeditCaretCallbackStruct *call_data)
607{
608 SDL_WindowData *data = (SDL_WindowData *)client_data;
609
610 switch (call_data->direction) {
611 case XIMAbsolutePosition:
612 if (call_data->position != data->preedit_cursor) {
613 data->preedit_cursor = call_data->position;
614 X11_SendEditingEvent(data);
615 }
616 break;
617 case XIMDontChange:
618 break;
619 default:
620 // Not currently supported
621 break;
622 }
623}
624
625void X11_CreateInputContext(SDL_WindowData *data)
626{
627#ifdef X_HAVE_UTF8_STRING
628 SDL_VideoData *videodata = data->videodata;
629
630 if (SDL_X11_HAVE_UTF8 && videodata->im) {
631 const char *hint = SDL_GetHint(SDL_HINT_IME_IMPLEMENTED_UI);
632 if (hint && SDL_strstr(hint, "composition")) {
633 XIMCallback draw_callback;
634 draw_callback.client_data = (XPointer)data;
635 draw_callback.callback = (XIMProc)preedit_draw_callback;
636
637 XIMCallback start_callback;
638 start_callback.client_data = (XPointer)data;
639 start_callback.callback = (XIMProc)preedit_start_callback;
640
641 XIMCallback done_callback;
642 done_callback.client_data = (XPointer)data;
643 done_callback.callback = (XIMProc)preedit_done_callback;
644
645 XIMCallback caret_callback;
646 caret_callback.client_data = (XPointer)data;
647 caret_callback.callback = (XIMProc)preedit_caret_callback;
648
649 XVaNestedList attr = X11_XVaCreateNestedList(0,
650 XNPreeditStartCallback, &start_callback,
651 XNPreeditDoneCallback, &done_callback,
652 XNPreeditDrawCallback, &draw_callback,
653 XNPreeditCaretCallback, &caret_callback,
654 NULL);
655 if (attr) {
656 data->ic = X11_XCreateIC(videodata->im,
657 XNInputStyle, XIMPreeditCallbacks | XIMStatusCallbacks,
658 XNPreeditAttributes, attr,
659 XNClientWindow, data->xwindow,
660 NULL);
661 X11_XFree(attr);
662 }
663 }
664 if (!data->ic) {
665 data->ic = X11_XCreateIC(videodata->im,
666 XNInputStyle, XIMPreeditNothing | XIMStatusNothing,
667 XNClientWindow, data->xwindow,
668 NULL);
669 }
670 data->xim_spot.x = -1;
671 data->xim_spot.y = -1;
672 }
673#endif // X_HAVE_UTF8_STRING
674}
675
676static void X11_ResetXIM(SDL_VideoDevice *_this, SDL_Window *window)
677{
678#ifdef X_HAVE_UTF8_STRING
679 SDL_WindowData *data = window->internal;
680
681 if (data && data->ic) {
682 // Clear any partially entered dead keys
683 char *contents = X11_Xutf8ResetIC(data->ic);
684 if (contents) {
685 X11_XFree(contents);
686 }
687 }
688#endif // X_HAVE_UTF8_STRING
689}
690
691bool X11_StartTextInput(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID props)
692{
693 X11_ResetXIM(_this, window);
694
695 return X11_UpdateTextInputArea(_this, window);
696}
697
698bool X11_StopTextInput(SDL_VideoDevice *_this, SDL_Window *window)
699{
700 X11_ResetXIM(_this, window);
701 return true;
702}
703
704bool X11_UpdateTextInputArea(SDL_VideoDevice *_this, SDL_Window *window)
705{
706#ifdef X_HAVE_UTF8_STRING
707 SDL_WindowData *data = window->internal;
708
709 if (data && data->ic) {
710 XPoint spot;
711 spot.x = window->text_input_rect.x + window->text_input_cursor;
712 spot.y = window->text_input_rect.y + window->text_input_rect.h;
713 if (spot.x != data->xim_spot.x || spot.y != data->xim_spot.y) {
714 XVaNestedList attr = X11_XVaCreateNestedList(0, XNSpotLocation, &spot, NULL);
715 if (attr) {
716 X11_XSetICValues(data->ic, XNPreeditAttributes, attr, NULL);
717 X11_XFree(attr);
718 }
719 SDL_copyp(&data->xim_spot, &spot);
720 }
721 }
722#endif
723 return true;
724}
725
726bool X11_HasScreenKeyboardSupport(SDL_VideoDevice *_this)
727{
728 SDL_VideoData *videodata = _this->internal;
729 return videodata->is_steam_deck;
730}
731
732void X11_ShowScreenKeyboard(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID props)
733{
734 SDL_VideoData *videodata = _this->internal;
735
736 if (videodata->is_steam_deck) {
737 /* For more documentation of the URL parameters, see:
738 * https://partner.steamgames.com/doc/api/ISteamUtils#ShowFloatingGamepadTextInput
739 */
740 const int k_EFloatingGamepadTextInputModeModeSingleLine = 0; // Enter dismisses the keyboard
741 const int k_EFloatingGamepadTextInputModeModeMultipleLines = 1; // User needs to explicitly dismiss the keyboard
742 const int k_EFloatingGamepadTextInputModeModeEmail = 2; // Keyboard is displayed in a special mode that makes it easier to enter emails
743 const int k_EFloatingGamepadTextInputModeModeNumeric = 3; // Numeric keypad is shown
744 char deeplink[128];
745 int mode;
746
747 switch (SDL_GetTextInputType(props)) {
748 case SDL_TEXTINPUT_TYPE_TEXT_EMAIL:
749 mode = k_EFloatingGamepadTextInputModeModeEmail;
750 break;
751 case SDL_TEXTINPUT_TYPE_NUMBER:
752 case SDL_TEXTINPUT_TYPE_NUMBER_PASSWORD_HIDDEN:
753 case SDL_TEXTINPUT_TYPE_NUMBER_PASSWORD_VISIBLE:
754 mode = k_EFloatingGamepadTextInputModeModeNumeric;
755 break;
756 default:
757 if (SDL_GetTextInputMultiline(props)) {
758 mode = k_EFloatingGamepadTextInputModeModeMultipleLines;
759 } else {
760 mode = k_EFloatingGamepadTextInputModeModeSingleLine;
761 }
762 break;
763 }
764 (void)SDL_snprintf(deeplink, sizeof(deeplink),
765 "steam://open/keyboard?XPosition=0&YPosition=0&Width=0&Height=0&Mode=%d",
766 mode);
767 SDL_OpenURL(deeplink);
768 videodata->steam_keyboard_open = true;
769 }
770}
771
772void X11_HideScreenKeyboard(SDL_VideoDevice *_this, SDL_Window *window)
773{
774 SDL_VideoData *videodata = _this->internal;
775
776 if (videodata->is_steam_deck) {
777 SDL_OpenURL("steam://close/keyboard");
778 videodata->steam_keyboard_open = false;
779 }
780}
781
782bool X11_IsScreenKeyboardShown(SDL_VideoDevice *_this, SDL_Window *window)
783{
784 SDL_VideoData *videodata = _this->internal;
785
786 return videodata->steam_keyboard_open;
787}
788
789#endif // SDL_VIDEO_DRIVER_X11
790