[client] X11: synchronize keyboard input state

Replay held keys after focus enters the client window.

Track modifiers and lock LEDs for cooked and grabbed raw input.

Handle external XKB indicator changes while the window stays focused.
This commit is contained in:
Geoffrey McRae
2026-08-10 17:36:21 +10:00
parent f8c4ef3360
commit 15e0cbbcc4
5 changed files with 162 additions and 83 deletions

View File

@@ -42,18 +42,19 @@ void x11InputInit(X11Input * input, const LG_DSInputSink * sink,
input->opaque = opaque; input->opaque = opaque;
} }
bool x11InputFocus(X11Input * input, bool focused, double x, double y, bool x11InputFocus(X11Input * input, bool focused,
const uint32_t * keys, size_t count) const uint32_t * keys, size_t count)
{ {
(void)keys;
(void)count;
if (input->focused == focused) if (input->focused == focused)
return false; return false;
input->focused = focused; input->focused = focused;
input->sink->position(input->opaque, x, y);
input->sink->focus(input->opaque, focused); input->sink->focus(input->opaque, focused);
if (focused)
for (size_t i = 0; i < count; ++i)
input->sink->key(input->opaque, keys[i], true);
return true; return true;
} }
@@ -147,8 +148,6 @@ void x11InputKeyboardState(X11Input * input,
bool ctrl, bool shift, bool alt, bool super, bool ctrl, bool shift, bool alt, bool super,
bool numLock, bool capsLock, bool scrollLock) bool numLock, bool capsLock, bool scrollLock)
{ {
(void)numLock;
(void)capsLock;
(void)scrollLock;
input->sink->modifiers(input->opaque, ctrl, shift, alt, super); input->sink->modifiers(input->opaque, ctrl, shift, alt, super);
input->sink->leds(input->opaque, numLock, capsLock, scrollLock);
} }

View File

@@ -40,7 +40,7 @@ X11Input;
void x11InputInit(X11Input * input, const LG_DSInputSink * sink, void x11InputInit(X11Input * input, const LG_DSInputSink * sink,
void * opaque); void * opaque);
bool x11InputFocus(X11Input * input, bool focused, double x, double y, bool x11InputFocus(X11Input * input, bool focused,
const uint32_t * keys, size_t count); const uint32_t * keys, size_t count);
bool x11InputPointerEnter(X11Input * input, bool mainWindow, bool x11InputPointerEnter(X11Input * input, bool mainWindow,
bool normal, double x, double y); bool normal, double x, double y);

View File

@@ -93,6 +93,10 @@ static int x11EventThread(void * unused);
static void x11XInputEvent(XGenericEventCookie *cookie); static void x11XInputEvent(XGenericEventCookie *cookie);
static void x11XPresentEvent(XGenericEventCookie *cookie); static void x11XPresentEvent(XGenericEventCookie *cookie);
static void x11UpdateKeyboardGroup(void); static void x11UpdateKeyboardGroup(void);
static int x11GetIndicatorIndex(const char * name);
static void emitKeyboardState(unsigned int indicators);
static void updateKeyboardState(void);
static void setFocus(bool focused);
static void x11GrabPointer(void); static void x11GrabPointer(void);
static void inputPosition(void * opaque, double x, double y) static void inputPosition(void * opaque, double x, double y)
@@ -442,6 +446,10 @@ static bool x11Init(const LG_DSInitParams params)
atomic_init(&x11.pointerGrabbed, false); atomic_init(&x11.pointerGrabbed, false);
x11.xValuator = -1; x11.xValuator = -1;
x11.yValuator = -1; x11.yValuator = -1;
x11.numLockIndicator = -1;
x11.capsLockIndicator = -1;
x11.scrollLockIndicator = -1;
x11.xkbEvent = -1;
x11.display = XOpenDisplay(NULL); x11.display = XOpenDisplay(NULL);
x11.jitRender = params.jitRender; x11.jitRender = params.jitRender;
x11.wm = &X11WM_Default; x11.wm = &X11WM_Default;
@@ -741,6 +749,22 @@ static bool x11Init(const LG_DSInitParams params)
XDisplayKeycodes(x11.display, &x11.minKeycode, &x11.maxKeycode); XDisplayKeycodes(x11.display, &x11.minKeycode, &x11.maxKeycode);
x11UpdateKeyboardGroup(); x11UpdateKeyboardGroup();
x11.numLockIndicator = x11GetIndicatorIndex("Num Lock");
x11.capsLockIndicator = x11GetIndicatorIndex("Caps Lock");
x11.scrollLockIndicator = x11GetIndicatorIndex("Scroll Lock");
int xkbOpcode;
int xkbEvent;
int xkbError;
int xkbMajor = XkbMajorVersion;
int xkbMinor = XkbMinorVersion;
if (XkbQueryExtension(x11.display, &xkbOpcode, &xkbEvent,
&xkbError, &xkbMajor, &xkbMinor))
{
x11.xkbEvent = xkbEvent;
XkbSelectEvents(x11.display, XkbUseCoreKbd,
XkbIndicatorStateNotifyMask, XkbIndicatorStateNotifyMask);
}
XIFreeDeviceInfo(devinfo); XIFreeDeviceInfo(devinfo);
@@ -1089,6 +1113,14 @@ static int x11EventThread(void * unused)
continue; continue;
} }
if (xe.type == x11.xkbEvent)
{
const XkbEvent * event = (const XkbEvent *)&xe;
if (event->any.xkb_type == XkbIndicatorStateNotify)
emitKeyboardState(event->indicators.state);
continue;
}
// call the clipboard handling code // call the clipboard handling code
if (x11CBEventThread(&xe)) if (x11CBEventThread(&xe))
continue; continue;
@@ -1201,10 +1233,7 @@ static int x11EventThread(void * unused)
} }
if (x11.ewmhHasFocusEvent && x11.input.focused != focused) if (x11.ewmhHasFocusEvent && x11.input.focused != focused)
{ setFocus(focused);
x11.input.focused = focused;
inputFocus(x11.input.opaque, focused);
}
x11.fullscreen = fullscreen; x11.fullscreen = fullscreen;
XFree(data); XFree(data);
@@ -1262,14 +1291,58 @@ static enum Modifiers keySymToModifier(KeySym sym)
} }
} }
static void updateModifiers(void) static int x11GetIndicatorIndex(const char * name)
{
const Atom atom = XInternAtom(x11.display, name, True);
int index;
if (atom == None ||
!XkbGetNamedIndicator(x11.display, atom, &index, NULL, NULL, NULL))
return -1;
return index;
}
static bool indicatorActive(unsigned int state, int index)
{
return index >= 0 && index < XkbNumIndicators &&
(state & (UINT32_C(1) << index));
}
static void emitKeyboardState(unsigned int indicators)
{ {
x11InputKeyboardState(&x11.input, x11InputKeyboardState(&x11.input,
x11.modifiers[MOD_CTRL_LEFT] || x11.modifiers[MOD_CTRL_RIGHT], x11.modifiers[MOD_CTRL_LEFT] || x11.modifiers[MOD_CTRL_RIGHT],
x11.modifiers[MOD_SHIFT_LEFT] || x11.modifiers[MOD_SHIFT_RIGHT], x11.modifiers[MOD_SHIFT_LEFT] || x11.modifiers[MOD_SHIFT_RIGHT],
x11.modifiers[MOD_ALT_LEFT] || x11.modifiers[MOD_ALT_RIGHT], x11.modifiers[MOD_ALT_LEFT] || x11.modifiers[MOD_ALT_RIGHT],
x11.modifiers[MOD_SUPER_LEFT] || x11.modifiers[MOD_SUPER_RIGHT], x11.modifiers[MOD_SUPER_LEFT] || x11.modifiers[MOD_SUPER_RIGHT],
false, false, false); indicatorActive(indicators, x11.numLockIndicator),
indicatorActive(indicators, x11.capsLockIndicator),
indicatorActive(indicators, x11.scrollLockIndicator));
}
static void updateKeyboardState(void)
{
unsigned int indicators = 0;
XkbGetIndicatorState(x11.display, XkbUseCoreKbd, &indicators);
emitKeyboardState(indicators);
}
static void updateKeyState(unsigned int keycode, bool pressed)
{
const KeySym sym = XkbKeycodeToKeysym(x11.display, keycode,
atomic_load(&x11.keyboardGroup), 0);
const int modifier = keySymToModifier(sym);
if (modifier >= 0)
{
x11.modifiers[modifier] = pressed;
updateKeyboardState();
return;
}
if (pressed &&
(sym == XK_Num_Lock || sym == XK_Caps_Lock || sym == XK_Scroll_Lock))
updateKeyboardState();
} }
static void x11UpdateKeyboardGroup(void) static void x11UpdateKeyboardGroup(void)
@@ -1279,15 +1352,41 @@ static void x11UpdateKeyboardGroup(void)
atomic_store(&x11.keyboardGroup, state.group); atomic_store(&x11.keyboardGroup, state.group);
} }
static void setFocus(bool focused, double x, double y) static void setFocus(bool focused)
{ {
if (x11.input.focused == focused) if (x11.input.focused == focused)
return; return;
uint32_t keys[256];
size_t count = 0;
if (focused) if (focused)
{
x11UpdateKeyboardGroup(); x11UpdateKeyboardGroup();
x11InputFocus(&x11.input, focused, x, y, NULL, 0); char keymap[32] = { 0 };
XQueryKeymap(x11.display, keymap);
memset(x11.modifiers, 0, sizeof(x11.modifiers));
for (int keycode = x11.minKeycode;
keycode <= x11.maxKeycode && keycode < 256; ++keycode)
{
const unsigned char keyByte = keymap[keycode / 8];
if (!(keyByte & (1U << (keycode % 8))))
continue;
keys[count++] = keycode - x11.minKeycode;
const KeySym sym = XkbKeycodeToKeysym(x11.display, keycode,
atomic_load(&x11.keyboardGroup), 0);
const int modifier = keySymToModifier(sym);
if (modifier >= 0)
x11.modifiers[modifier] = true;
}
}
x11InputFocus(&x11.input, focused, keys, count);
if (focused)
updateKeyboardState();
} }
static bool x11GetKeyLabel(int sc, char * label, size_t size) static bool x11GetKeyLabel(int sc, char * label, size_t size)
@@ -1316,7 +1415,7 @@ static void x11XInputEvent(XGenericEventCookie *cookie)
{ {
// if meta ungrab for move/resize // if meta ungrab for move/resize
if (xie->mode == XINotifyUngrab) if (xie->mode == XINotifyUngrab)
setFocus(true, xie->event_x, xie->event_y); setFocus(true);
return; return;
} }
@@ -1330,7 +1429,7 @@ static void x11XInputEvent(XGenericEventCookie *cookie)
return; return;
setFocus(true, xie->event_x, xie->event_y); setFocus(true);
return; return;
} }
@@ -1341,7 +1440,7 @@ static void x11XInputEvent(XGenericEventCookie *cookie)
{ {
// if meta grab for move/resize // if meta grab for move/resize
if (xie->mode == XINotifyGrab) if (xie->mode == XINotifyGrab)
setFocus(false, xie->event_x, xie->event_y); setFocus(false);
return; return;
} }
@@ -1354,7 +1453,7 @@ static void x11XInputEvent(XGenericEventCookie *cookie)
xie->mode != XINotifyGrab) xie->mode != XINotifyGrab)
return; return;
setFocus(false, xie->event_x, xie->event_y); setFocus(false);
return; return;
} }
@@ -1428,13 +1527,11 @@ static void x11XInputEvent(XGenericEventCookie *cookie)
x11InputKeyboardKey(&x11.input, device->detail, x11.minKeycode, x11InputKeyboardKey(&x11.input, device->detail, x11.minKeycode,
true, false, true, NULL); true, false, true, NULL);
if (!x11.xic || !app_isOverlayMode()) if (x11.xic && app_isOverlayMode())
return; {
char buffer[128]; char buffer[128];
KeySym sym; KeySym sym;
Status status; Status status;
int count;
XKeyPressedEvent ev = { XKeyPressedEvent ev = {
.display = x11.display, .display = x11.display,
.window = x11.window, .window = x11.window,
@@ -1443,30 +1540,19 @@ static void x11XInputEvent(XGenericEventCookie *cookie)
.state = device->mods.effective, .state = device->mods.effective,
}; };
count = Xutf8LookupString(x11.xic, &ev, buffer, sizeof(buffer), const int count = Xutf8LookupString(x11.xic, &ev, buffer,
&sym, &status); sizeof(buffer), &sym, &status);
if (status == XBufferOverflow || count >= sizeof(buffer)) if (status == XBufferOverflow || count >= sizeof(buffer))
{
DEBUG_WARN("Typing too many characters at once, ignoring"); DEBUG_WARN("Typing too many characters at once, ignoring");
return; else if (status == XLookupChars || status == XLookupBoth)
}
if (status == XLookupChars || status == XLookupBoth)
{ {
buffer[count] = '\0'; buffer[count] = '\0';
inputText(x11.input.opaque, buffer); inputText(x11.input.opaque, buffer);
} }
}
if (status == XLookupKeySym || status == XLookupBoth) updateKeyState(device->detail, true);
{
int modifier = keySymToModifier(sym);
if (modifier >= 0)
{
x11.modifiers[modifier] = true;
updateModifiers();
}
}
return; return;
} }
@@ -1478,41 +1564,31 @@ static void x11XInputEvent(XGenericEventCookie *cookie)
XIDeviceEvent *device = cookie->data; XIDeviceEvent *device = cookie->data;
x11InputKeyboardKey(&x11.input, device->detail, x11.minKeycode, x11InputKeyboardKey(&x11.input, device->detail, x11.minKeycode,
false, false, true, NULL); false, false, true, NULL);
updateKeyState(device->detail, false);
if (!x11.xic || !app_isOverlayMode())
return;
XKeyPressedEvent ev = {
.display = x11.display,
.window = x11.window,
.type = KeyRelease,
.keycode = device->detail,
.state = device->mods.effective,
};
KeySym sym = XLookupKeysym(&ev, 0);
int modifier = keySymToModifier(sym);
if (modifier >= 0)
{
x11.modifiers[modifier] = false;
updateModifiers();
}
return; return;
} }
case XI_RawKeyPress: case XI_RawKeyPress:
{ {
if (!x11.input.focused || !x11.keyboardGrabbed)
return;
XIRawEvent *raw = cookie->data; XIRawEvent *raw = cookie->data;
x11InputKeyboardKey(&x11.input, raw->detail, x11.minKeycode, x11InputKeyboardKey(&x11.input, raw->detail, x11.minKeycode,
true, true, x11.input.focused, NULL); true, true, true, NULL);
updateKeyState(raw->detail, true);
return; return;
} }
case XI_RawKeyRelease: case XI_RawKeyRelease:
{ {
if (!x11.input.focused || !x11.keyboardGrabbed)
return;
XIRawEvent *raw = cookie->data; XIRawEvent *raw = cookie->data;
x11InputKeyboardKey(&x11.input, raw->detail, x11.minKeycode, x11InputKeyboardKey(&x11.input, raw->detail, x11.minKeycode,
false, true, x11.input.focused, NULL); false, true, true, NULL);
updateKeyState(raw->detail, false);
return; return;
} }

View File

@@ -64,6 +64,10 @@ struct X11DSState
int minKeycode, maxKeycode; int minKeycode, maxKeycode;
_Atomic(unsigned int) keyboardGroup; _Atomic(unsigned int) keyboardGroup;
int numLockIndicator;
int capsLockIndicator;
int scrollLockIndicator;
int xkbEvent;
//Extended Window Manager Hints //Extended Window Manager Hints
//ref: https://specifications.freedesktop.org/wm-spec/latest/ //ref: https://specifications.freedesktop.org/wm-spec/latest/

View File

@@ -533,7 +533,7 @@ static void keyboardEnter(struct Fixture * fixture, bool mainSurface,
if (fixture->backend == BACKEND_WAYLAND) if (fixture->backend == BACKEND_WAYLAND)
wlInputKeyboardEnter(&fixture->input.wayland, mainSurface, keys, count); wlInputKeyboardEnter(&fixture->input.wayland, mainSurface, keys, count);
else if (mainSurface) else if (mainSurface)
x11InputFocus(&fixture->input.x11, true, 40.0, 30.0, keys, count); x11InputFocus(&fixture->input.x11, true, keys, count);
} }
static void keyboardLeave(struct Fixture * fixture, bool mainSurface) static void keyboardLeave(struct Fixture * fixture, bool mainSurface)
@@ -541,7 +541,7 @@ static void keyboardLeave(struct Fixture * fixture, bool mainSurface)
if (fixture->backend == BACKEND_WAYLAND) if (fixture->backend == BACKEND_WAYLAND)
wlInputKeyboardLeave(&fixture->input.wayland, mainSurface); wlInputKeyboardLeave(&fixture->input.wayland, mainSurface);
else if (mainSurface) else if (mainSurface)
x11InputFocus(&fixture->input.x11, false, 40.0, 30.0, NULL, 0); x11InputFocus(&fixture->input.x11, false, NULL, 0);
} }
static void keyboardKey(struct Fixture * fixture, unsigned int key, static void keyboardKey(struct Fixture * fixture, unsigned int key,