Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
input: rewrite keyboard handling
This fixes the fact that we queued key events but did not queue the
changes to wlr_keyboard internal state. We now maintain our own
wlr_keyboard internal state in KeyboardGroup.state, solving some
of the trickiest XXX comments remaining.
There is still a bit of work to do here to make input configuration
and keymap changes based on keybindings work properly, everything
else regarding input configuration is also TODO right now though and
this is a big step up over the status quo.
build.zig.zon | 4 +-
river/Cursor.zig | 22 ++--
river/InputDevice.zig | 32 ++---
river/Keyboard.zig | 299 +++++++++++++--------------------------------
river/KeyboardGroup.zig | 317 ++++++++++++++++++++++++++++++++++++++++++++++++
river/Seat.zig | 41 +++----
river/XkbBinding.zig | 13 +-
7 files changed, 449 insertions(+), 279 deletions(-)
diff --git a/build.zig.zon b/build.zig.zon
index c341993..0a9a658 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -18,8 +18,8 @@
.hash = "wayland-0.3.0-lQa1kjPIAQDmhGYpY-zxiRzQJFHQ2VqhJkQLbKKdt5wl",
},
.wlroots = .{
- .url = "https://codeberg.org/ifreund/zig-wlroots/archive/f92ba27133ecf702d85c9d3894f98a336389bbd9.tar.gz",
- .hash = "wlroots-0.19.3-dev-jmOlcr7_AwClfjFwW8oOkWoqAbt9oPLqgdvfFYEXqlOF",
+ .url = "git+https://codeberg.org/ifreund/zig-wlroots#e96d90ce224d10278fc31ca7c3150cbd1919ef1c",
+ .hash = "wlroots-0.19.3-dev-jmOlcrcBBAB4-xzzlJBEPEIJwD9Fnzk2VSPHPCWIgTjV",
},
.xkbcommon = .{
.url = "https://codeberg.org/ifreund/zig-xkbcommon/archive/v0.3.0.tar.gz",
diff --git a/river/Cursor.zig b/river/Cursor.zig
index 3e21d2d..446e31b 100644
--- a/river/Cursor.zig
+++ b/river/Cursor.zig
@@ -741,55 +741,55 @@ fn updateDragIcons(cursor: *Cursor) void {
fn queueMotionRelative(listener: *wl.Listener(*wlr.Pointer.event.Motion), event: *wlr.Pointer.event.Motion) void {
const cursor: *Cursor = @fieldParentPtr("motion_relative", listener);
- cursor.seat.queueEvent(.{ .pointer_motion_relative = event.* });
+ cursor.seat.queueEvent(.{ .pointer_motion_relative = event.* }) catch {};
}
fn queueMotionAbsolute(listener: *wl.Listener(*wlr.Pointer.event.MotionAbsolute), event: *wlr.Pointer.event.MotionAbsolute) void {
const cursor: *Cursor = @fieldParentPtr("motion_absolute", listener);
- cursor.seat.queueEvent(.{ .pointer_motion_absolute = event.* });
+ cursor.seat.queueEvent(.{ .pointer_motion_absolute = event.* }) catch {};
}
fn queueButton(listener: *wl.Listener(*wlr.Pointer.event.Button), event: *wlr.Pointer.event.Button) void {
const cursor: *Cursor = @fieldParentPtr("button", listener);
- cursor.seat.queueEvent(.{ .pointer_button = event.* });
+ cursor.seat.queueEvent(.{ .pointer_button = event.* }) catch {};
}
fn queueAxis(listener: *wl.Listener(*wlr.Pointer.event.Axis), event: *wlr.Pointer.event.Axis) void {
const cursor: *Cursor = @fieldParentPtr("axis", listener);
- cursor.seat.queueEvent(.{ .pointer_axis = event.* });
+ cursor.seat.queueEvent(.{ .pointer_axis = event.* }) catch {};
}
fn queueFrame(listener: *wl.Listener(*wlr.Cursor), _: *wlr.Cursor) void {
const cursor: *Cursor = @fieldParentPtr("frame", listener);
- cursor.seat.queueEvent(.pointer_frame);
+ cursor.seat.queueEvent(.pointer_frame) catch {};
}
fn queuePinchBegin(listener: *wl.Listener(*wlr.Pointer.event.PinchBegin), event: *wlr.Pointer.event.PinchBegin) void {
const cursor: *Cursor = @fieldParentPtr("pinch_begin", listener);
- cursor.seat.queueEvent(.{ .pointer_pinch_begin = event.* });
+ cursor.seat.queueEvent(.{ .pointer_pinch_begin = event.* }) catch {};
}
fn queuePinchUpdate(listener: *wl.Listener(*wlr.Pointer.event.PinchUpdate), event: *wlr.Pointer.event.PinchUpdate) void {
const cursor: *Cursor = @fieldParentPtr("pinch_update", listener);
- cursor.seat.queueEvent(.{ .pointer_pinch_update = event.* });
+ cursor.seat.queueEvent(.{ .pointer_pinch_update = event.* }) catch {};
}
fn queuePinchEnd(listener: *wl.Listener(*wlr.Pointer.event.PinchEnd), event: *wlr.Pointer.event.PinchEnd) void {
const cursor: *Cursor = @fieldParentPtr("pinch_end", listener);
- cursor.seat.queueEvent(.{ .pointer_pinch_end = event.* });
+ cursor.seat.queueEvent(.{ .pointer_pinch_end = event.* }) catch {};
}
fn queueSwipeBegin(listener: *wl.Listener(*wlr.Pointer.event.SwipeBegin), event: *wlr.Pointer.event.SwipeBegin) void {
const cursor: *Cursor = @fieldParentPtr("swipe_begin", listener);
- cursor.seat.queueEvent(.{ .pointer_swipe_begin = event.* });
+ cursor.seat.queueEvent(.{ .pointer_swipe_begin = event.* }) catch {};
}
fn queueSwipeUpdate(listener: *wl.Listener(*wlr.Pointer.event.SwipeUpdate), event: *wlr.Pointer.event.SwipeUpdate) void {
const cursor: *Cursor = @fieldParentPtr("swipe_update", listener);
- cursor.seat.queueEvent(.{ .pointer_swipe_update = event.* });
+ cursor.seat.queueEvent(.{ .pointer_swipe_update = event.* }) catch {};
}
fn queueSwipeEnd(listener: *wl.Listener(*wlr.Pointer.event.SwipeEnd), event: *wlr.Pointer.event.SwipeEnd) void {
const cursor: *Cursor = @fieldParentPtr("swipe_end", listener);
- cursor.seat.queueEvent(.{ .pointer_swipe_end = event.* });
+ cursor.seat.queueEvent(.{ .pointer_swipe_end = event.* }) catch {};
}
diff --git a/river/InputDevice.zig b/river/InputDevice.zig
index 3c1c5e7..09a6767 100644
--- a/river/InputDevice.zig
+++ b/river/InputDevice.zig
@@ -90,20 +90,16 @@ pub fn init(device: *InputDevice, seat: *Seat, wlr_device: *wlr.InputDevice) !vo
wlr_device.events.destroy.add(&device.destroy);
- // Keyboard groups are implemented as "virtual" input devices which we don't want to expose
- // in riverctl list-inputs as they can't be configured.
- if (!isKeyboardGroup(wlr_device)) {
- // Apply all matching input device configuration.
- for (server.input_manager.configs.items) |input_config| {
- if (globber.match(identifier, input_config.glob)) {
- input_config.apply(device);
- }
+ // Apply all matching input device configuration.
+ for (server.input_manager.configs.items) |input_config| {
+ if (globber.match(identifier, input_config.glob)) {
+ input_config.apply(device);
}
-
- server.input_manager.devices.append(device);
- seat.updateCapabilities();
}
+ server.input_manager.devices.append(device);
+ seat.updateCapabilities();
+
log.debug("new input device: {s}", .{identifier});
}
@@ -112,21 +108,14 @@ pub fn deinit(device: *InputDevice) void {
util.gpa.free(device.identifier);
- if (!isKeyboardGroup(device.wlr_device)) {
- device.link.remove();
- device.seat.updateCapabilities();
- }
+ device.link.remove();
+ device.seat.updateCapabilities();
device.wlr_device.data = null;
device.* = undefined;
}
-fn isKeyboardGroup(wlr_device: *wlr.InputDevice) bool {
- return wlr_device.type == .keyboard and
- wlr.KeyboardGroup.fromKeyboard(wlr_device.toKeyboard()) != null;
-}
-
fn handleDestroy(listener: *wl.Listener(*wlr.InputDevice), _: *wlr.InputDevice) void {
const device: *InputDevice = @fieldParentPtr("destroy", listener);
@@ -135,8 +124,7 @@ fn handleDestroy(listener: *wl.Listener(*wlr.InputDevice), _: *wlr.InputDevice)
switch (device.wlr_device.type) {
.keyboard => {
const keyboard: *Keyboard = @fieldParentPtr("device", device);
- keyboard.deinit();
- util.gpa.destroy(keyboard);
+ keyboard.deviceDestroy();
},
.pointer, .touch => {
device.deinit();
diff --git a/river/Keyboard.zig b/river/Keyboard.zig
index e3111ed..4d58844 100644
--- a/river/Keyboard.zig
+++ b/river/Keyboard.zig
@@ -1,6 +1,6 @@
// This file is part of river, a dynamic tiling wayland compositor.
//
-// Copyright 2020 - 2024 The River Developers
+// Copyright 2020 - 2025 The River Developers
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
@@ -21,84 +21,33 @@ const assert = std.debug.assert;
const wlr = @import("wlroots");
const wl = @import("wayland").server.wl;
const xkb = @import("xkbcommon");
-const globber = @import("globber");
const server = &@import("main.zig").server;
const util = @import("util.zig");
const InputDevice = @import("InputDevice.zig");
+const KeyboardGroup = @import("KeyboardGroup.zig");
const Seat = @import("Seat.zig");
-const XkbBinding = @import("XkbBinding.zig");
const log = std.log.scoped(.input);
-pub const Event = union(enum) {
- key: wlr.Keyboard.event.Key,
- modifiers: wlr.Keyboard.Modifiers,
-};
-
-const KeyConsumer = union(enum) {
- /// A null value indicates that the xkb_binding_v1 was destroyed or that
- /// a press event was already sent due to a press on a different keyboard.
- binding: ?*XkbBinding,
- im_grab,
- /// Seat's focused client
- focus,
-};
-
-pub const Pressed = struct {
- const Key = struct {
- code: u32,
- consumer: KeyConsumer,
- };
-
- pub const capacity = 32;
-
- comptime {
- // wlroots uses a buffer of length 32 to track pressed keys and does not track pressed
- // keys beyond that limit. It seems likely that this can cause some inconsistency within
- // wlroots in the case that someone has 32 fingers and the hardware supports N-key rollover.
- //
- // Furthermore, wlroots will continue to forward key press/release events to river if more
- // than 32 keys are pressed. Therefore river chooses to ignore keypresses that would take
- // the keyboard beyond 32 simultaneously pressed keys.
- assert(capacity == @typeInfo(std.meta.fieldInfo(wlr.Keyboard, .keycodes).type).array.len);
- }
-
- keys: std.BoundedArray(Key, capacity) = .{},
-
- fn contains(pressed: *Pressed, code: u32) bool {
- for (pressed.keys.constSlice()) |item| {
- if (item.code == code) return true;
- }
- return false;
- }
-
- fn addAssumeCapacity(pressed: *Pressed, new: Key) void {
- assert(!pressed.contains(new.code));
- pressed.keys.appendAssumeCapacity(new);
- }
-
- fn remove(pressed: *Pressed, code: u32) ?KeyConsumer {
- for (pressed.keys.constSlice(), 0..) |item, idx| {
- if (item.code == code) return pressed.keys.swapRemove(idx).consumer;
- }
-
- return null;
- }
-};
-
device: InputDevice,
+device_destroyed: bool = false,
+queued_events: u32 = 0,
-/// Pressed keys along with where their press event has been sent
-pressed: Pressed = .{},
+group: *KeyboardGroup,
key: wl.Listener(*wlr.Keyboard.event.Key) = .init(queueKey),
modifiers: wl.Listener(*wlr.Keyboard) = .init(queueModifiers),
+keymap: wl.Listener(*wlr.Keyboard) = .init(queueKeymap),
+
+pub fn create(seat: *Seat, wlr_device: *wlr.InputDevice, virtual: bool) !*Keyboard {
+ const keyboard = try util.gpa.create(Keyboard);
+ errdefer util.gpa.destroy(keyboard);
-pub fn init(keyboard: *Keyboard, seat: *Seat, wlr_device: *wlr.InputDevice, virtual: bool) !void {
keyboard.* = .{
.device = undefined,
+ .group = undefined,
};
try keyboard.device.init(seat, wlr_device);
errdefer keyboard.device.deinit();
@@ -106,187 +55,109 @@ pub fn init(keyboard: *Keyboard, seat: *Seat, wlr_device: *wlr.InputDevice, virt
const wlr_keyboard = keyboard.device.wlr_device.toKeyboard();
wlr_keyboard.data = keyboard;
- if (!virtual) {
- // wlroots will log a more detailed error if this fails.
- if (!wlr_keyboard.setKeymap(server.config.keymap)) return error.OutOfMemory;
-
- if (wlr.KeyboardGroup.fromKeyboard(wlr_keyboard) == null) {
- // wlroots will log an error on failure
- _ = seat.keyboard_group.addKeyboard(wlr_keyboard);
+ keyboard.group = blk: {
+ if (virtual) {
+ // Virtual keyboards set their own keymap and require independent modifier state.
+ // Therefore, they are always placed in their own group of one.
+ break :blk try KeyboardGroup.create(seat, wlr_keyboard.keymap, true);
+ } else {
+ var it = seat.keyboard_groups.iterator(.forward);
+ while (it.next()) |group| {
+ // TODO input configuration will require sorting keyboards into
+ // groups based on keymap and repeat info.
+ if (true) {
+ break :blk group.ref();
+ }
+ }
+ break :blk try KeyboardGroup.create(seat, server.config.keymap, false);
}
- }
-
- wlr_keyboard.setRepeatInfo(server.config.repeat_rate, server.config.repeat_delay);
+ };
wlr_keyboard.events.key.add(&keyboard.key);
wlr_keyboard.events.modifiers.add(&keyboard.modifiers);
+ wlr_keyboard.events.keymap.add(&keyboard.keymap);
+
+ return keyboard;
}
-pub fn deinit(keyboard: *Keyboard) void {
+pub fn deviceDestroy(keyboard: *Keyboard) void {
+ assert(!keyboard.device_destroyed);
+ keyboard.device_destroyed = true;
+
keyboard.key.link.remove();
keyboard.modifiers.link.remove();
-
- const seat = keyboard.device.seat;
- const wlr_keyboard = keyboard.device.wlr_device.toKeyboard();
+ keyboard.keymap.link.remove();
keyboard.device.deinit();
- // If the currently active keyboard of a seat is destroyed we need to set
- // a new active keyboard. Otherwise wlroots may send an enter event without
- // first having sent a keymap event if Seat.keyboardNotifyEnter() is called
- // before a new active keyboard is set.
- if (seat.wlr_seat.getKeyboard() == wlr_keyboard) {
- var it = server.input_manager.devices.iterator(.forward);
- while (it.next()) |device| {
- if (device.seat == seat and device.wlr_device.type == .keyboard) {
- seat.wlr_seat.setKeyboard(device.wlr_device.toKeyboard());
- }
- }
- }
-
- keyboard.* = undefined;
+ keyboard.maybeDestroy();
}
-pub fn processKey(keyboard: *Keyboard, event: *const wlr.Keyboard.event.Key) void {
- const wlr_keyboard = keyboard.device.wlr_device.toKeyboard();
-
- // Translate libinput keycode -> xkbcommon
- const xkb_keycode = event.keycode + 8;
-
- // XXX this is not ok, we need to store current modifiers per-Keyboard ourselves
- const modifiers = wlr_keyboard.getModifiers();
- const released = event.state == .released;
-
- const xkb_state = wlr_keyboard.xkb_state orelse return;
-
- const keysyms = xkb_state.keyGetSyms(xkb_keycode);
-
- for (keysyms) |sym| {
- if (!released and handleBuiltinMapping(sym)) return;
- }
-
- // Some virtual_keyboard clients are buggy and press a key twice without
- // releasing it in between. There is no good way for river to handle this
- // other than to ignore any newer presses. No need to worry about pairing
- // the correct release, as the client is unlikely to send all of them
- // (and we already ignore releasing keys we don't know were pressed).
- if (!released and keyboard.pressed.contains(xkb_keycode)) {
- log.err("key pressed again without release, virtual-keyboard client bug?", .{});
+fn maybeDestroy(keyboard: *Keyboard) void {
+ if (!keyboard.device_destroyed or keyboard.queued_events > 0) {
return;
}
- // Every sent press event, to a regular client or the input method, should have
- // the corresponding release event sent to the same client.
- // Similarly, no press event means no release event.
-
- const consumer: KeyConsumer = blk: {
- // Decision is made on press; release only follows it
- if (released) {
- // The released key might not be in the pressed set when switching from a different tty
- // or if the press was ignored due to >32 keys being pressed simultaneously.
- break :blk keyboard.pressed.remove(xkb_keycode) orelse return;
- }
-
- // Ignore key presses beyond 32 simultaneously pressed keys (see comments in Pressed).
- // We must ensure capacity before calling handleMapping() to ensure that we either run
- // both the press and release mapping for certain key or neither mapping.
- keyboard.pressed.keys.ensureUnusedCapacity(1) catch return;
+ keyboard.group.unref();
- if (keyboard.device.seat.matchXkbBinding(xkb_keycode, modifiers, xkb_state)) |binding| {
- log.debug("matched xkb binding", .{});
- break :blk .{
- .binding = if (binding.sent_pressed) null else binding,
- };
- } else if (keyboard.getInputMethodGrab() != null) {
- break :blk .im_grab;
- }
-
- break :blk .focus;
- };
-
- if (!released) {
- keyboard.pressed.addAssumeCapacity(.{ .code = xkb_keycode, .consumer = consumer });
- }
-
- switch (consumer) {
- .binding => |b| if (b) |binding| {
- if (released) {
- binding.released();
- } else {
- binding.pressed();
- }
- },
- .im_grab => if (keyboard.getInputMethodGrab()) |keyboard_grab| {
- keyboard_grab.setKeyboard(keyboard_grab.keyboard);
- keyboard_grab.sendKey(event.time_msec, event.keycode, event.state);
- },
- .focus => {
- const wlr_seat = keyboard.device.seat.wlr_seat;
- wlr_seat.setKeyboard(keyboard.device.wlr_device.toKeyboard());
- wlr_seat.keyboardNotifyKey(event.time_msec, event.keycode, event.state);
- },
- }
+ util.gpa.destroy(keyboard);
}
-pub fn processModifiers(keyboard: *Keyboard, modifiers: *const wlr.Keyboard.Modifiers) void {
- if (keyboard.getInputMethodGrab()) |keyboard_grab| {
- keyboard_grab.setKeyboard(keyboard_grab.keyboard);
- keyboard_grab.sendModifiers(modifiers);
- } else {
- keyboard.device.seat.wlr_seat.setKeyboard(keyboard.device.wlr_device.toKeyboard());
- keyboard.device.seat.wlr_seat.keyboardNotifyModifiers(modifiers);
- }
+pub fn processKey(keyboard: *Keyboard, key: *const wlr.Keyboard.event.Key) void {
+ keyboard.group.processKey(key);
+ keyboard.queued_events -= 1;
+ keyboard.maybeDestroy();
}
-/// Handle any builtin, harcoded compsitor mappings such as VT switching.
-/// Returns true if the keysym was handled.
-fn handleBuiltinMapping(keysym: xkb.Keysym) bool {
- switch (@intFromEnum(keysym)) {
- xkb.Keysym.XF86Switch_VT_1...xkb.Keysym.XF86Switch_VT_12 => {
- log.debug("switch VT keysym received", .{});
- if (server.session) |session| {
- const vt = @intFromEnum(keysym) - xkb.Keysym.XF86Switch_VT_1 + 1;
- std.log.info("switching to VT {}", .{vt});
- session.changeVt(vt) catch std.log.err("changing VT failed", .{});
- }
- return true;
- },
- else => return false,
- }
+pub fn processModifiers(keyboard: *Keyboard, modifiers: wlr.Keyboard.Modifiers) void {
+ keyboard.group.processModifiers(modifiers);
+ keyboard.queued_events -= 1;
+ keyboard.maybeDestroy();
}
-/// Returns null if the keyboard is not grabbed by an input method,
-/// or if event is from a virtual keyboard of the same client as the grab.
-/// TODO: see https://gitlab.freedesktop.org/wlroots/wlroots/-/issues/2322
-fn getInputMethodGrab(keyboard: Keyboard) ?*wlr.InputMethodV2.KeyboardGrab {
- if (keyboard.device.seat.relay.input_method) |input_method| {
- if (input_method.keyboard_grab) |keyboard_grab| {
- if (keyboard.device.wlr_device.getVirtualKeyboard()) |virtual_keyboard| {
- if (virtual_keyboard.resource.getClient() == keyboard_grab.resource.getClient()) {
- return null;
- }
- }
- return keyboard_grab;
- }
- }
- return null;
+pub fn processKeymap(keyboard: *Keyboard, keymap: *xkb.Keymap) void {
+ defer keymap.unref();
+ keyboard.group.processKeymap(keymap);
+ keyboard.queued_events -= 1;
+ keyboard.maybeDestroy();
}
fn queueKey(listener: *wl.Listener(*wlr.Keyboard.event.Key), event: *wlr.Keyboard.event.Key) void {
const keyboard: *Keyboard = @fieldParentPtr("key", listener);
- const wlr_keyboard = keyboard.device.wlr_device.toKeyboard();
-
- // If the keyboard is in a group, this event will be handled by the group's Keyboard instance.
- if (wlr_keyboard.group != null) return;
-
- keyboard.device.seat.queueEvent(.{ .keyboard_key = .{ .keyboard = keyboard, .key = event.* } });
+ assert(!keyboard.device_destroyed);
+ keyboard.queued_events += 1;
+ keyboard.device.seat.queueEvent(.{ .keyboard_key = .{
+ .keyboard = keyboard,
+ .key = event.*,
+ } }) catch {
+ keyboard.queued_events -= 1;
+ };
}
-fn queueModifiers(listener: *wl.Listener(*wlr.Keyboard), wlr_keyboard: *wlr.Keyboard) void {
+fn queueModifiers(listener: *wl.Listener(*wlr.Keyboard), _: *wlr.Keyboard) void {
const keyboard: *Keyboard = @fieldParentPtr("modifiers", listener);
+ assert(!keyboard.device_destroyed);
+ const wlr_keyboard = keyboard.device.wlr_device.toKeyboard();
+ keyboard.queued_events += 1;
+ keyboard.device.seat.queueEvent(.{ .keyboard_modifiers = .{
+ .keyboard = keyboard,
+ .modifiers = wlr_keyboard.modifiers,
+ } }) catch {
+ keyboard.queued_events -= 1;
+ };
+}
- // If the keyboard is in a group, this event will be handled by the group's Keyboard instance.
- if (wlr_keyboard.group != null) return;
-
- keyboard.device.seat.queueEvent(.{ .keyboard_modifiers = .{ .keyboard = keyboard, .modifiers = wlr_keyboard.modifiers } });
+fn queueKeymap(listener: *wl.Listener(*wlr.Keyboard), _: *wlr.Keyboard) void {
+ const keyboard: *Keyboard = @fieldParentPtr("keymap", listener);
+ assert(!keyboard.device_destroyed);
+ const wlr_keyboard = keyboard.device.wlr_device.toKeyboard();
+ const keymap = wlr_keyboard.keymap orelse return;
+ keyboard.queued_events += 1;
+ keyboard.device.seat.queueEvent(.{ .keyboard_keymap = .{
+ .keyboard = keyboard,
+ .keymap = keymap.ref(),
+ } }) catch {
+ keyboard.queued_events -= 1;
+ keymap.unref();
+ };
}
diff --git a/river/KeyboardGroup.zig b/river/KeyboardGroup.zig
new file mode 100644
index 0000000..89f3f15
--- /dev/null
+++ b/river/KeyboardGroup.zig
@@ -0,0 +1,317 @@
+// This file is part of river, a dynamic tiling wayland compositor.
+//
+// Copyright 2025 The River Developers
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+const KeyboardGroup = @This();
+
+const std = @import("std");
+const assert = std.debug.assert;
+const wlr = @import("wlroots");
+const wl = @import("wayland").server.wl;
+const xkb = @import("xkbcommon");
+
+const server = &@import("main.zig").server;
+const util = @import("util.zig");
+
+const Seat = @import("Seat.zig");
+const XkbBinding = @import("XkbBinding.zig");
+
+const log = std.log.scoped(.input);
+
+const KeyConsumer = union(enum) {
+ /// Builtin compositor binding, e.g. VT switching
+ builtin,
+ /// A null value indicates that the xkb_binding_v1 was destroyed or that
+ /// a press event was already sent due to a press on a different keyboard.
+ binding: ?*XkbBinding,
+ im_grab,
+ /// Seat's focused client
+ focus,
+};
+
+pub const Pressed = struct {
+ const Key = struct {
+ /// The raw libinput keycode, not the xkb keycode
+ code: u32,
+ consumer: KeyConsumer,
+ count: u32,
+ };
+
+ pub const capacity = 32;
+
+ comptime {
+ // wlroots uses a buffer of length 32 to track pressed keys and does not track pressed
+ // keys beyond that limit. It seems likely that this can cause some inconsistency within
+ // wlroots in the case that someone has 32 fingers and the hardware supports N-key rollover.
+ //
+ // Furthermore, wlroots will continue to forward key press/release events to river if more
+ // than 32 keys are pressed. Therefore river chooses to ignore keypresses that would take
+ // the keyboard beyond 32 simultaneously pressed keys.
+ assert(capacity == @typeInfo(std.meta.fieldInfo(wlr.Keyboard, .keycodes).type).array.len);
+ }
+
+ keys: std.BoundedArray(Key, capacity) = .{},
+
+ fn get(pressed: *Pressed, code: u32) ?*Key {
+ for (pressed.keys.slice()) |*key| {
+ if (key.code == code) return key;
+ }
+ return null;
+ }
+
+ fn add(pressed: *Pressed, new: Key) void {
+ assert(pressed.get(new.code) == null);
+ pressed.keys.appendAssumeCapacity(new);
+ }
+
+ /// Asserts that the key is present and has count == 0.
+ fn remove(pressed: *Pressed, code: u32) KeyConsumer {
+ for (pressed.keys.constSlice(), 0..) |key, idx| {
+ if (key.code == code) {
+ assert(key.count == 0);
+ return pressed.keys.swapRemove(idx).consumer;
+ }
+ }
+ unreachable;
+ }
+};
+
+ref_count: u32 = 1,
+
+seat: *Seat,
+/// Seat.keyboard_groups
+link: wl.list.Link,
+
+virtual: bool,
+
+/// This is the keyboard that actually gets passed to wlr_seat functions for
+/// setting keyboard focus.
+state: wlr.Keyboard,
+
+/// Pressed keys along with where their press event has been sent
+pressed: Pressed = .{},
+
+key: wl.Listener(*wlr.Keyboard.event.Key) = .init(handleKey),
+modifiers: wl.Listener(*wlr.Keyboard) = .init(handleModifiers),
+
+pub fn create(seat: *Seat, keymap: ?*xkb.Keymap, virtual: bool) !*KeyboardGroup {
+ const group = try util.gpa.create(KeyboardGroup);
+ errdefer util.gpa.destroy(group);
+ group.* = .{
+ .seat = seat,
+ .virtual = virtual,
+ .state = undefined,
+ .link = undefined,
+ };
+ seat.keyboard_groups.append(group);
+
+ group.state.init(&.{
+ .name = "river.KeyboardGroup",
+ .led_update = null, // TODO
+ }, "river.KeyboardGroup");
+ group.state.data = group;
+
+ // wlroots will log an error on failure, there's not much we can do to recover unfortunately.
+ _ = group.state.setKeymap(keymap);
+ group.state.setRepeatInfo(server.config.repeat_rate, server.config.repeat_delay);
+
+ group.state.events.key.add(&group.key);
+ group.state.events.modifiers.add(&group.modifiers);
+
+ return group;
+}
+
+pub fn ref(group: *KeyboardGroup) *KeyboardGroup {
+ group.ref_count += 1;
+ return group;
+}
+
+pub fn unref(group: *KeyboardGroup) void {
+ group.ref_count -= 1;
+ if (group.ref_count > 0) {
+ return;
+ }
+
+ group.link.remove();
+
+ group.key.link.remove();
+ group.modifiers.link.remove();
+
+ // If the currently active keyboard of a seat is destroyed we need to set
+ // a new active keyboard. Otherwise wlroots may send an enter event without
+ // first having sent a keymap event if Seat.keyboardNotifyEnter() is called
+ // before a new active keyboard is set.
+ if (group.seat.wlr_seat.getKeyboard() == &group.state) {
+ if (group.seat.keyboard_groups.first()) |other| {
+ group.seat.wlr_seat.setKeyboard(&other.state);
+ }
+ }
+
+ group.state.finish();
+
+ util.gpa.destroy(group);
+}
+
+pub fn processKey(group: *KeyboardGroup, event: *const wlr.Keyboard.event.Key) void {
+ if (group.pressed.get(event.keycode)) |key| {
+ assert(key.count > 0);
+ if (event.state == .pressed) {
+ key.count += 1;
+ } else {
+ key.count -= 1;
+ if (key.count == 0) {
+ var key_event: wlr.Keyboard.event.Key = .{
+ .time_msec = event.time_msec,
+ .keycode = event.keycode,
+ .update_state = true,
+ .state = .released,
+ };
+ // Calls handleKey(), which will remove from pressed
+ group.state.notifyKey(&key_event);
+ }
+ }
+ } else if (event.state == .pressed) {
+ if (group.pressed.keys.ensureUnusedCapacity(1)) {
+ var key_event: wlr.Keyboard.event.Key = .{
+ .time_msec = event.time_msec,
+ .keycode = event.keycode,
+ .update_state = true,
+ .state = .pressed,
+ };
+ // Calls handleKey(), which will add to pressed
+ group.state.notifyKey(&key_event);
+ } else |_| {}
+ }
+ // Release events without a prior press event are ignored.
+}
+
+fn handleKey(listener: *wl.Listener(*wlr.Keyboard.event.Key), event: *wlr.Keyboard.event.Key) void {
+ const group: *KeyboardGroup = @fieldParentPtr("key", listener);
+
+ const xkb_state = group.state.xkb_state orelse {
+ log.err("no xkb_state available", .{});
+ return;
+ };
+
+ // Every sent press event, to a regular client or the input method, should have
+ // the corresponding release event sent to the same client.
+ // Similarly, no press event means no release event.
+ const consumer: KeyConsumer = blk: {
+ if (event.state == .released) {
+ // Decision is made on press; release only follows it
+ break :blk group.pressed.remove(event.keycode);
+ }
+ // Translate libinput keycode -> xkbcommon
+ const xkb_keycode = event.keycode + 8;
+ for (xkb_state.keyGetSyms(xkb_keycode)) |sym| {
+ if (handleBuiltinBinding(sym)) {
+ log.debug("matched builtin binding", .{});
+ break :blk .builtin;
+ }
+ }
+ const modifiers = group.state.getModifiers();
+ if (group.seat.matchXkbBinding(xkb_keycode, modifiers, xkb_state)) |binding| {
+ log.debug("matched xkb binding", .{});
+ break :blk .{
+ .binding = if (binding.sent_pressed) null else binding,
+ };
+ }
+ if (group.getInputMethodGrab() != null) {
+ break :blk .im_grab;
+ }
+ break :blk .focus;
+ };
+
+ if (event.state == .pressed) {
+ group.pressed.add(.{
+ .code = event.keycode,
+ .consumer = consumer,
+ .count = 1,
+ });
+ }
+
+ switch (consumer) {
+ .builtin => {},
+ .binding => |b| if (b) |binding| {
+ if (event.state == .pressed) {
+ binding.pressed();
+ } else {
+ binding.released();
+ }
+ },
+ .im_grab => if (group.getInputMethodGrab()) |keyboard_grab| {
+ keyboard_grab.setKeyboard(&group.state);
+ keyboard_grab.sendKey(event.time_msec, event.keycode, event.state);
+ },
+ .focus => {
+ group.seat.wlr_seat.setKeyboard(&group.state);
+ group.seat.wlr_seat.keyboardNotifyKey(event.time_msec, event.keycode, event.state);
+ },
+ }
+}
+
+pub fn processModifiers(group: *KeyboardGroup, modifiers: wlr.Keyboard.Modifiers) void {
+ group.state.notifyModifiers(modifiers);
+}
+
+fn handleModifiers(listener: *wl.Listener(*wlr.Keyboard), _: *wlr.Keyboard) void {
+ const group: *KeyboardGroup = @fieldParentPtr("modifiers", listener);
+ if (group.getInputMethodGrab()) |keyboard_grab| {
+ keyboard_grab.setKeyboard(&group.state);
+ keyboard_grab.sendModifiers(&group.state.modifiers);
+ } else {
+ group.seat.wlr_seat.setKeyboard(&group.state);
+ group.seat.wlr_seat.keyboardNotifyModifiers(&group.state.modifiers);
+ }
+}
+
+/// Handle any builtin, hardcoded compositor keybindings such as VT switching.
+/// Returns true if the keysym was handled.
+fn handleBuiltinBinding(keysym: xkb.Keysym) bool {
+ switch (@intFromEnum(keysym)) {
+ xkb.Keysym.XF86Switch_VT_1...xkb.Keysym.XF86Switch_VT_12 => {
+ log.debug("switch VT keysym received", .{});
+ if (server.session) |session| {
+ const vt = @intFromEnum(keysym) - xkb.Keysym.XF86Switch_VT_1 + 1;
+ std.log.info("switching to VT {}", .{vt});
+ session.changeVt(vt) catch std.log.err("changing VT failed", .{});
+ }
+ return true;
+ },
+ else => return false,
+ }
+}
+
+/// Returns null if the keyboard is not grabbed by an input method,
+/// or if the group is for a virtual keyboard.
+/// TODO: it would be good if virtual keyboards that are not associated with the
+/// input method client would pass through the input method grab.
+/// See https://gitlab.freedesktop.org/wlroots/wlroots/-/issues/2322
+fn getInputMethodGrab(group: *KeyboardGroup) ?*wlr.InputMethodV2.KeyboardGrab {
+ if (group.virtual) {
+ return null;
+ }
+ if (group.seat.relay.input_method) |input_method| {
+ if (input_method.keyboard_grab) |keyboard_grab| {
+ return keyboard_grab;
+ }
+ }
+ return null;
+}
+
+pub fn processKeymap(group: *KeyboardGroup, keymap: *xkb.Keymap) void {
+ // wlroots will log an error on failure, there's not much we can do to recover unfortunately.
+ _ = group.state.setKeymap(keymap);
+}
diff --git a/river/Seat.zig b/river/Seat.zig
index 3447059..4322691 100644
--- a/river/Seat.zig
+++ b/river/Seat.zig
@@ -34,6 +34,7 @@ const InputDevice = @import("InputDevice.zig");
const InputManager = @import("InputManager.zig");
const InputRelay = @import("InputRelay.zig");
const Keyboard = @import("Keyboard.zig");
+const KeyboardGroup = @import("KeyboardGroup.zig");
const LockSurface = @import("LockSurface.zig");
const Output = @import("Output.zig");
const PointerBinding = @import("PointerBinding.zig");
@@ -56,6 +57,11 @@ pub const Event = union(enum) {
keyboard: *Keyboard,
modifiers: wlr.Keyboard.Modifiers,
},
+ /// This event is really just for virtual keyboards, which set their own keymaps.
+ keyboard_keymap: struct {
+ keyboard: *Keyboard,
+ keymap: *xkb.Keymap,
+ },
pointer_motion_relative: wlr.Pointer.event.Motion,
pointer_motion_absolute: wlr.Pointer.event.MotionAbsolute,
@@ -97,10 +103,6 @@ pub const Focus = union(enum) {
};
/// XXX experiment with different sizes here, consider making dynamic
-/// XXX There's a bug here when a keyboard is destroyed while events
-/// for that keyboard are still in the queue. We need our own separate
-/// keyboard state anyways for proper modifier handling (currently modifiers
-/// effectively bypass the queue due to how wlr_keyboard is implemented).
const EventQueue = std.fifo.LinearFifo(Event, .{ .Static = 1024 });
wlr_seat: *wlr.Seat,
@@ -161,7 +163,7 @@ op: ?struct {
relay: InputRelay,
-keyboard_group: *wlr.KeyboardGroup,
+keyboard_groups: wl.list.Head(KeyboardGroup, .link),
focused: Focus = .none,
@@ -191,7 +193,7 @@ pub fn create(name: [*:0]const u8) !void {
.pointer_bindings = undefined,
.cursor = undefined,
.relay = undefined,
- .keyboard_group = try wlr.KeyboardGroup.create(),
+ .keyboard_groups = undefined,
};
seat.wlr_seat.data = seat;
@@ -205,7 +207,7 @@ pub fn create(name: [*:0]const u8) !void {
try seat.cursor.init(seat);
seat.relay.init();
- try seat.tryAddDevice(&seat.keyboard_group.keyboard.base, false);
+ seat.keyboard_groups.init();
seat.wlr_seat.events.request_set_selection.add(&seat.request_set_selection);
seat.wlr_seat.events.request_start_drag.add(&seat.request_start_drag);
@@ -224,8 +226,6 @@ pub fn destroy(seat: *Seat) void {
seat.cursor.deinit();
- seat.keyboard_group.destroy();
-
seat.request_set_selection.link.remove();
seat.request_start_drag.link.remove();
seat.start_drag.link.remove();
@@ -233,12 +233,12 @@ pub fn destroy(seat: *Seat) void {
seat.request_set_primary_selection.link.remove();
}
-pub fn queueEvent(seat: *Seat, event: Event) void {
+pub fn queueEvent(seat: *Seat, event: Event) !void {
seat.handleActivity();
seat.event_queue.writeItem(event) catch {
log.err("dropping {s} event, no space in event queue", .{@tagName(event)});
- return;
+ return error.QueueFull;
};
if (server.wm.state == .idle) {
@@ -260,7 +260,8 @@ pub fn processEvents(seat: *Seat) void {
const pg = server.input_manager.pointer_gestures;
switch (event) {
.keyboard_key => |ev| ev.keyboard.processKey(&ev.key),
- .keyboard_modifiers => |ev| ev.keyboard.processModifiers(&ev.modifiers),
+ .keyboard_modifiers => |ev| ev.keyboard.processModifiers(ev.modifiers),
+ .keyboard_keymap => |ev| ev.keyboard.processKeymap(ev.keymap),
.pointer_motion_relative => |ev| seat.cursor.processMotionRelative(&ev),
.pointer_motion_absolute => |ev| seat.cursor.processMotionAbsolute(&ev),
@@ -577,18 +578,17 @@ pub fn keyboardEnterOrLeave(seat: *Seat, target_surface: ?*wlr.Surface) void {
fn keyboardNotifyEnter(seat: *Seat, wlr_surface: *wlr.Surface) void {
if (seat.wlr_seat.getKeyboard()) |wlr_keyboard| {
- const keyboard: *Keyboard = @alignCast(@ptrCast(wlr_keyboard.data));
+ const group: *KeyboardGroup = @alignCast(@ptrCast(wlr_keyboard.data));
- var keycodes: std.BoundedArray(u32, Keyboard.Pressed.capacity) = .{};
- for (keyboard.pressed.keys.constSlice()) |item| {
+ var keycodes: std.BoundedArray(u32, KeyboardGroup.Pressed.capacity) = .{};
+ for (group.pressed.keys.constSlice()) |item| {
if (item.consumer == .focus) keycodes.appendAssumeCapacity(item.code);
}
seat.wlr_seat.keyboardNotifyEnter(
wlr_surface,
keycodes.constSlice(),
- // XXX this is not ok, use our own stored modifiers
- &wlr_keyboard.modifiers,
+ &group.state.modifiers,
);
} else {
seat.wlr_seat.keyboardNotifyEnter(wlr_surface, &.{}, null);
@@ -707,12 +707,9 @@ pub fn addDevice(seat: *Seat, wlr_device: *wlr.InputDevice, virtual: bool) void
fn tryAddDevice(seat: *Seat, wlr_device: *wlr.InputDevice, virtual: bool) !void {
switch (wlr_device.type) {
.keyboard => {
- const keyboard = try util.gpa.create(Keyboard);
- errdefer util.gpa.destroy(keyboard);
-
- try keyboard.init(seat, wlr_device, virtual);
+ const keyboard = try Keyboard.create(seat, wlr_device, virtual);
- seat.wlr_seat.setKeyboard(keyboard.device.wlr_device.toKeyboard());
+ seat.wlr_seat.setKeyboard(&keyboard.group.state);
if (seat.wlr_seat.keyboard_state.focused_surface) |wlr_surface| {
seat.keyboardNotifyEnter(wlr_surface);
}
diff --git a/river/XkbBinding.zig b/river/XkbBinding.zig
index 02b6b63..b76bc18 100644
--- a/river/XkbBinding.zig
+++ b/river/XkbBinding.zig
@@ -98,14 +98,11 @@ pub fn create(
fn handleDestroy(_: *river.XkbBindingV1, binding: *XkbBinding) void {
{
- var it = server.input_manager.devices.iterator(.forward);
- while (it.next()) |device| {
- if (device.seat == binding.seat and device.wlr_device.type == .keyboard) {
- const keyboard: *Keyboard = @fieldParentPtr("device", device);
- for (keyboard.pressed.keys.slice()) |*key| {
- if (key.consumer == .binding and key.consumer.binding == binding) {
- key.consumer.binding = null;
- }
+ var it = binding.seat.keyboard_groups.iterator(.forward);
+ while (it.next()) |group| {
+ for (group.pressed.keys.slice()) |*key| {
+ if (key.consumer == .binding and key.consumer.binding == binding) {
+ key.consumer.binding = null;
}
}
}