Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
river: implement river_xkb_binding_v1
build.zig | 1 +
build.zig.zon | 4 +-
protocol/river-window-management-v1.xml | 4 +-
river/Config.zig | 2 -
river/Keyboard.zig | 64 +++++-----
river/Mapping.zig | 97 ---------------
river/Seat.zig | 106 +++++++++++-----
river/XkbBinding.zig | 211 ++++++++++++++++++++++++++++++++
rivercompat/Seat.zig | 37 +++++-
rivercompat/WindowManager.zig | 2 +-
rivercompat/XkbBinding.zig | 56 +++++++++
11 files changed, 415 insertions(+), 169 deletions(-)
diff --git a/build.zig b/build.zig
index 250874a..17cee2f 100644
--- a/build.zig
+++ b/build.zig
@@ -202,6 +202,7 @@ pub fn build(b: *Build) !void {
rivercompat.root_module.addImport("flags", flags);
rivercompat.root_module.addImport("wayland", wayland);
+ rivercompat.root_module.addImport("xkbcommon", xkbcommon);
rivercompat.linkLibC();
rivercompat.linkSystemLibrary("wayland-client");
diff --git a/build.zig.zon b/build.zig.zon
index e967d71..0b14132 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -8,8 +8,8 @@
.hash = "12209db20ce873af176138b76632931def33a10539387cba745db72933c43d274d56",
},
.@"zig-wayland" = .{
- .url = "https://codeberg.org/ifreund/zig-wayland/archive/2500fc313608be05370ebb67426a05604f2fd937.tar.gz",
- .hash = "12200e8929e6f520bab7c6229a5d550f818bd6109077d2b99dee4aa79aa6f4835344",
+ .url = "https://codeberg.org/ifreund/zig-wayland/archive/a5e2e9b6a6d7fba638ace4d4b24a3b576a02685b.tar.gz",
+ .hash = "1220d41b23ae70e93355bb29dac1c07aa6aeb92427a2dffc4375e94b4de18111248c",
},
.@"zig-wlroots" = .{
.url = "https://codeberg.org/ifreund/zig-wlroots/archive/70a4de5cd6886003e54f55239d2753b025271bc9.tar.gz",
diff --git a/protocol/river-window-management-v1.xml b/protocol/river-window-management-v1.xml
index 31abbec..453880f 100644
--- a/protocol/river-window-management-v1.xml
+++ b/protocol/river-window-management-v1.xml
@@ -1177,7 +1177,7 @@
<entry name="mod5" value="128"/>
</enum>
- <request name="define_xkb_binding">
+ <request name="get_xkb_binding">
<description summary="define a new xkbcommon key binding">
Define a key binding in terms of an xkbcommon keysym and other
configurable properties.
@@ -1191,7 +1191,7 @@
<arg name="modifiers" type="uint" enum="modifiers"/>
</request>
- <request name="define_pointer_binding">
+ <request name="get_pointer_binding">
<description summary="define a new pointer binding">
Define a pointer binding in terms of a pointer button, modifiers, and
other configurable properties.
diff --git a/river/Config.zig b/river/Config.zig
index fd1b129..a36b73e 100644
--- a/river/Config.zig
+++ b/river/Config.zig
@@ -28,7 +28,6 @@ const util = @import("util.zig");
const Server = @import("Server.zig");
const Output = @import("Output.zig");
-const Mapping = @import("Mapping.zig");
const Switch = @import("Switch.zig");
const Window = @import("Window.zig");
@@ -51,7 +50,6 @@ border_width: u31 = 2,
/// Color of border in RGBA with premultiplied alpha
border_color: [4]f32 = [_]f32{ 0.34509804, 0.43137255, 0.45882353, 1.0 }, // Solarized base01
-mappings: std.ArrayListUnmanaged(Mapping) = .{},
pointer_mappings: std.ArrayListUnmanaged(struct {
event_code: u32,
modifiers: wlr.Keyboard.ModifierMask,
diff --git a/river/Keyboard.zig b/river/Keyboard.zig
index baf2ba2..7cf32ee 100644
--- a/river/Keyboard.zig
+++ b/river/Keyboard.zig
@@ -26,8 +26,9 @@ const globber = @import("globber");
const server = &@import("main.zig").server;
const util = @import("util.zig");
-const Seat = @import("Seat.zig");
const InputDevice = @import("InputDevice.zig");
+const Seat = @import("Seat.zig");
+const XkbBinding = @import("XkbBinding.zig");
const log = std.log.scoped(.input);
@@ -36,10 +37,12 @@ pub const Event = union(enum) {
modifiers: wlr.Keyboard.Modifiers,
};
-const KeyConsumer = enum {
- mapping,
+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 (xdg or layer shell)
+ /// Seat's focused client
focus,
};
@@ -152,23 +155,16 @@ pub fn deinit(keyboard: *Keyboard) void {
pub fn processKey(keyboard: *Keyboard, event: *const wlr.Keyboard.event.Key) void {
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.handleActivity();
-
// Translate libinput keycode -> xkbcommon
- const keycode = event.keycode + 8;
+ 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;
- // We must ref() the state here as a mapping could change the keyboard layout.
- const xkb_state = (wlr_keyboard.xkb_state orelse return).ref();
- defer xkb_state.unref();
+ const xkb_state = wlr_keyboard.xkb_state orelse return;
- const keysyms = xkb_state.keyGetSyms(keycode);
+ const keysyms = xkb_state.keyGetSyms(xkb_keycode);
for (keysyms) |sym| {
if (!released and handleBuiltinMapping(sym)) return;
@@ -179,7 +175,7 @@ pub fn processKey(keyboard: *Keyboard, event: *const wlr.Keyboard.event.Key) voi
// 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(event.keycode)) {
+ if (!released and keyboard.pressed.contains(xkb_keycode)) {
log.err("key pressed again without release, virtual-keyboard client bug?", .{});
return;
}
@@ -193,7 +189,7 @@ pub fn processKey(keyboard: *Keyboard, event: *const wlr.Keyboard.event.Key) voi
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(event.keycode) orelse return;
+ break :blk keyboard.pressed.remove(xkb_keycode) orelse return;
}
// Ignore key presses beyond 32 simultaneously pressed keys (see comments in Pressed).
@@ -201,8 +197,11 @@ pub fn processKey(keyboard: *Keyboard, event: *const wlr.Keyboard.event.Key) voi
// both the press and release mapping for certain key or neither mapping.
keyboard.pressed.keys.ensureUnusedCapacity(1) catch return;
- if (keyboard.device.seat.handleMapping(keycode, modifiers, released, xkb_state)) {
- break :blk .mapping;
+ 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;
}
@@ -211,13 +210,17 @@ pub fn processKey(keyboard: *Keyboard, event: *const wlr.Keyboard.event.Key) voi
};
if (!released) {
- keyboard.pressed.addAssumeCapacity(.{ .code = event.keycode, .consumer = consumer });
+ keyboard.pressed.addAssumeCapacity(.{ .code = xkb_keycode, .consumer = consumer });
}
switch (consumer) {
- // Press mappings are handled above when determining the consumer of the press
- // Release mappings are handled separately as they are executed independent of the consumer.
- .mapping => {},
+ .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);
@@ -228,17 +231,9 @@ pub fn processKey(keyboard: *Keyboard, event: *const wlr.Keyboard.event.Key) voi
wlr_seat.keyboardNotifyKey(event.time_msec, event.keycode, event.state);
},
}
-
- // Release mappings don't interact with anything
- if (released) _ = keyboard.device.seat.handleMapping(keycode, modifiers, released, xkb_state);
}
pub fn processModifiers(keyboard: *Keyboard, modifiers: *const wlr.Keyboard.Modifiers) void {
- 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;
-
if (keyboard.getInputMethodGrab()) |keyboard_grab| {
keyboard_grab.setKeyboard(keyboard_grab.keyboard);
keyboard_grab.sendModifiers(modifiers);
@@ -284,10 +279,19 @@ fn getInputMethodGrab(keyboard: Keyboard) ?*wlr.InputMethodV2.KeyboardGrab {
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.* } });
}
fn queueModifiers(listener: *wl.Listener(*wlr.Keyboard), wlr_keyboard: *wlr.Keyboard) void {
const keyboard: *Keyboard = @fieldParentPtr("modifiers", listener);
+
+ // 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 } });
}
diff --git a/river/Mapping.zig b/river/Mapping.zig
deleted file mode 100644
index 0393cb7..0000000
--- a/river/Mapping.zig
+++ /dev/null
@@ -1,97 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 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 Mapping = @This();
-
-const std = @import("std");
-const wlr = @import("wlroots");
-const xkb = @import("xkbcommon");
-
-const util = @import("util.zig");
-
-keysym: xkb.Keysym,
-modifiers: wlr.Keyboard.ModifierMask,
-options: Options,
-
-pub const Options = struct {
- /// When set to true the mapping will be executed on key release rather than on press
- release: bool,
- // This is set for mappings with layout-pinning
- // If set, the layout with this index is always used to translate the given keycode
- layout_index: ?u32,
-};
-
-/// Compare mapping with given keycode, modifiers and keyboard state
-pub fn match(
- mapping: Mapping,
- keycode: xkb.Keycode,
- modifiers: wlr.Keyboard.ModifierMask,
- released: bool,
- xkb_state: *xkb.State,
- method: enum { no_translate, translate },
-) bool {
- if (released != mapping.options.release) return false;
-
- const keymap = xkb_state.getKeymap();
-
- // If the mapping has no pinned layout, use the active layout.
- // It doesn't matter if the index is out of range, since xkbcommon
- // will fall back to the active layout if so.
- const layout_index = mapping.options.layout_index orelse xkb_state.keyGetLayout(keycode);
-
- switch (method) {
- .no_translate => {
- // Get keysyms from the base layer, as if modifiers didn't change keysyms.
- // E.g. pressing `Super+Shift 1` does not translate to `Super Exclam`.
- const keysyms = keymap.keyGetSymsByLevel(
- keycode,
- layout_index,
- 0,
- );
-
- if (@as(u32, @bitCast(modifiers)) == @as(u32, @bitCast(mapping.modifiers))) {
- for (keysyms) |sym| {
- if (sym == mapping.keysym) {
- return true;
- }
- }
- }
- },
- .translate => {
- // Keysyms and modifiers as translated by xkb.
- // Modifiers used to translate the key are consumed.
- // E.g. pressing `Super+Shift 1` translates to `Super Exclam`.
- const keysyms_translated = keymap.keyGetSymsByLevel(
- keycode,
- layout_index,
- xkb_state.keyGetLevel(keycode, layout_index),
- );
-
- const consumed = xkb_state.keyGetConsumedMods2(keycode, .xkb);
- const modifiers_translated = @as(u32, @bitCast(modifiers)) & ~consumed;
-
- if (modifiers_translated == @as(u32, @bitCast(mapping.modifiers))) {
- for (keysyms_translated) |sym| {
- if (sym == mapping.keysym) {
- return true;
- }
- }
- }
- },
- }
-
- return false;
-}
diff --git a/river/Seat.zig b/river/Seat.zig
index 8f9e3c0..82c9c8f 100644
--- a/river/Seat.zig
+++ b/river/Seat.zig
@@ -36,12 +36,12 @@ const InputRelay = @import("InputRelay.zig");
const Keyboard = @import("Keyboard.zig");
const KeyboardGroup = @import("KeyboardGroup.zig");
const LockSurface = @import("LockSurface.zig");
-const Mapping = @import("Mapping.zig");
const Output = @import("Output.zig");
const PointerConstraint = @import("PointerConstraint.zig");
const Switch = @import("Switch.zig");
const Tablet = @import("Tablet.zig");
const Window = @import("Window.zig");
+const XkbBinding = @import("XkbBinding.zig");
const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig");
const log = std.log.scoped(.input);
@@ -136,6 +136,8 @@ uncommitted: WmState = .{},
/// State requested by the window manager client and committed.
committed: WmState = .{},
+xkb_bindings: wl.list.Head(XkbBinding, .link),
+
/// Multiple physical mice are handled by the same Cursor
cursor: Cursor,
@@ -171,6 +173,7 @@ pub fn create(name: [*:0]const u8) !void {
.link = undefined,
.link_pending = undefined,
.link_sent = undefined,
+ .xkb_bindings = undefined,
.cursor = undefined,
.relay = undefined,
};
@@ -181,6 +184,8 @@ pub fn create(name: [*:0]const u8) !void {
seat.link_sent.init();
server.wm.dirtyPending();
+ seat.xkb_bindings.init();
+
try seat.cursor.init(seat);
seat.relay.init();
@@ -337,6 +342,26 @@ pub fn sendDirty(seat: *Seat) void {
seat.pending.window_interaction = null;
}
}
+
+ {
+ var it = seat.xkb_bindings.iterator(.forward);
+ while (it.next()) |binding| {
+ switch (binding.pending.state_change) {
+ .none => {},
+ .pressed => {
+ assert(!binding.sent_pressed);
+ binding.sent_pressed = true;
+ binding.object.sendPressed();
+ },
+ .released => {
+ assert(binding.sent_pressed);
+ binding.sent_pressed = false;
+ binding.object.sendReleased();
+ },
+ }
+ binding.pending.state_change = .none;
+ }
+ }
}
}
@@ -374,12 +399,32 @@ fn handleRequest(
.pointer_resize_window => {},
.pointer_confine_to_region => {},
.pointer_warp => {},
- .define_xkb_binding => {},
- .define_pointer_binding => {},
+ .get_xkb_binding => |args| {
+ XkbBinding.create(
+ seat,
+ seat_v1.getClient(),
+ seat_v1.getVersion(),
+ args.id,
+ @enumFromInt(args.keysym),
+ args.modifiers,
+ ) catch {
+ seat_v1.getClient().postNoMemory();
+ log.err("out of memory", .{});
+ return;
+ };
+ },
+ .get_pointer_binding => {},
}
}
pub fn commitWmState(seat: *Seat) void {
+ {
+ var it = seat.xkb_bindings.iterator(.forward);
+ while (it.next()) |binding| {
+ binding.committed = binding.uncommitted;
+ }
+ }
+
seat.committed = seat.uncommitted;
}
@@ -475,34 +520,34 @@ pub fn handleActivity(seat: Seat) void {
/// Handle any user-defined mapping for passed keycode, modifiers and keyboard state
/// Returns true if a mapping was run
-pub fn handleMapping(
- _: *Seat,
+pub fn matchXkbBinding(
+ seat: *Seat,
keycode: xkb.Keycode,
modifiers: wlr.Keyboard.ModifierMask,
- released: bool,
xkb_state: *xkb.State,
-) bool {
- const mappings = &server.config.mappings;
-
- // It is possible for more than one mapping to be matched due to the
- // existence of layout-independent mappings. It is also possible due to
+) ?*XkbBinding {
+ // It is possible for more than one binding to be matched due to the
+ // existence of layout-independent bindings. It is also possible due to
// translation by xkbcommon consuming modifiers. On the swedish layout
// for example, translating Super+Shift+Space may consume the Shift
- // modifier and confict with a mapping for Super+Space. For this reason,
+ // modifier and confict with a binding for Super+Space. For this reason,
// matching wihout xkbcommon translation is done first and after a match
// has been found all further matches are ignored.
- var found: ?*Mapping = null;
+ var found: ?*XkbBinding = null;
// First check for matches without translating keysyms with xkbcommon.
// That is, if the physical keys Mod+Shift+1 are pressed on a US layout don't
// translate the keysym 1 to an exclamation mark. This behavior is generally
// what is desired.
- for (mappings.items) |*mapping| {
- if (mapping.match(keycode, modifiers, released, xkb_state, .no_translate)) {
- if (found == null) {
- found = mapping;
- } else {
- log.debug("already found a matching mapping, ignoring additional match", .{});
+ {
+ var it = seat.xkb_bindings.iterator(.forward);
+ while (it.next()) |binding| {
+ if (binding.match(keycode, modifiers, xkb_state, .no_translate)) {
+ if (found == null) {
+ found = binding;
+ } else {
+ log.debug("already found a matching binding, ignoring additional match", .{});
+ }
}
}
}
@@ -511,23 +556,20 @@ pub fn handleMapping(
// with xkbcommon for intuitive behavior. For example, layouts may require
// translation with the numlock modifier to obtain keypad number keysyms
// (e.g. KP_1).
- for (mappings.items) |*mapping| {
- if (mapping.match(keycode, modifiers, released, xkb_state, .translate)) {
- if (found == null) {
- found = mapping;
- } else {
- log.debug("already found a matching mapping, ignoring additional match", .{});
+ {
+ var it = seat.xkb_bindings.iterator(.forward);
+ while (it.next()) |binding| {
+ if (binding.match(keycode, modifiers, xkb_state, .translate)) {
+ if (found == null) {
+ found = binding;
+ } else {
+ log.debug("already found a matching binding, ignoring additional match", .{});
+ }
}
}
}
- // The mapped command must be run outside of the loop above as it may modify
- // the list of mappings we are iterating through, possibly causing it to be re-allocated.
- if (found) |_| {
- return true;
- }
-
- return false;
+ return found;
}
/// Handle any user-defined mapping for switches
diff --git a/river/XkbBinding.zig b/river/XkbBinding.zig
new file mode 100644
index 0000000..f9644e0
--- /dev/null
+++ b/river/XkbBinding.zig
@@ -0,0 +1,211 @@
+// This file is part of river, a dynamic tiling wayland compositor.
+//
+// Copyright 2020-2024 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 XkbBinding = @This();
+
+const std = @import("std");
+const assert = std.debug.assert;
+const wlr = @import("wlroots");
+const xkb = @import("xkbcommon");
+const wayland = @import("wayland");
+const wl = wayland.server.wl;
+const river = wayland.server.river;
+
+const server = &@import("main.zig").server;
+const util = @import("util.zig");
+
+const Keyboard = @import("Keyboard.zig");
+const Seat = @import("Seat.zig");
+
+const log = std.log.scoped(.input);
+
+const WmState = struct {
+ enabled: bool = false,
+ // This is set for mappings with layout-pinning
+ // If set, the layout with this index is always used to translate the given keycode
+ layout: ?u32 = null,
+};
+
+seat: *Seat,
+object: *river.XkbBindingV1,
+
+keysym: xkb.Keysym,
+modifiers: river.SeatV1.Modifiers,
+
+pending: struct {
+ state_change: enum {
+ none,
+ pressed,
+ released,
+ } = .none,
+} = .{},
+uncommitted: WmState = .{},
+committed: WmState = .{},
+
+/// This bit of state is used to ensure that multiple simultaneous
+/// presses across multiple keyboards do not cause multiple press
+/// events to be sent to the window manager.
+sent_pressed: bool = false,
+
+/// Seat.xkb_bindings
+link: wl.list.Link,
+
+pub fn create(
+ seat: *Seat,
+ client: *wl.Client,
+ version: u32,
+ id: u32,
+ keysym: xkb.Keysym,
+ modifiers: river.SeatV1.Modifiers,
+) !void {
+ const binding = try util.gpa.create(XkbBinding);
+ errdefer util.gpa.destroy(binding);
+
+ const xkb_binding_v1 = try river.XkbBindingV1.create(client, version, id);
+ errdefer comptime unreachable;
+
+ {
+ var buffer: [64]u8 = undefined;
+ _ = keysym.getName(&buffer, buffer.len);
+ log.debug("new river_xkb_binding_v1: keysym: {d}({s}) modifiers: {d}", .{
+ @intFromEnum(keysym),
+ &buffer,
+ @as(u32, @bitCast(modifiers)),
+ });
+ }
+
+ binding.* = .{
+ .seat = seat,
+ .object = xkb_binding_v1,
+ .keysym = keysym,
+ .modifiers = modifiers,
+ .link = undefined,
+ };
+ xkb_binding_v1.setHandler(*XkbBinding, handleRequest, handleDestroy, binding);
+
+ seat.xkb_bindings.append(binding);
+}
+
+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;
+ }
+ }
+ }
+ }
+ }
+ binding.link.remove();
+ util.gpa.destroy(binding);
+}
+
+fn handleRequest(
+ xkb_binding_v1: *river.XkbBindingV1,
+ request: river.XkbBindingV1.Request,
+ binding: *XkbBinding,
+) void {
+ assert(binding.object == xkb_binding_v1);
+ switch (request) {
+ .destroy => xkb_binding_v1.destroy(),
+ .set_layout_override => |args| binding.uncommitted.layout = args.layout,
+ .enable => binding.uncommitted.enabled = true,
+ .disable => binding.uncommitted.enabled = false,
+ }
+}
+
+pub fn pressed(binding: *XkbBinding) void {
+ assert(!binding.sent_pressed);
+ // Input event processing should not continue after a press/release event
+ // until that event is sent to the window manager in an update and acked.
+ assert(binding.pending.state_change == .none);
+ binding.pending.state_change = .pressed;
+ server.wm.dirtyPending();
+}
+
+pub fn released(binding: *XkbBinding) void {
+ assert(binding.sent_pressed);
+ // Input event processing should not continue after a press/release event
+ // until that event is sent to the window manager in an update and acked.
+ assert(binding.pending.state_change == .none);
+ binding.pending.state_change = .released;
+ server.wm.dirtyPending();
+}
+
+/// Compare binding with given keycode, modifiers and keyboard state
+pub fn match(
+ binding: *const XkbBinding,
+ keycode: xkb.Keycode,
+ modifiers: wlr.Keyboard.ModifierMask,
+ xkb_state: *xkb.State,
+ method: enum { no_translate, translate },
+) bool {
+ if (!binding.committed.enabled) return false;
+
+ const keymap = xkb_state.getKeymap();
+
+ // If the binding has no pinned layout, use the active layout.
+ // It doesn't matter if the index is out of range, since xkbcommon
+ // will fall back to the active layout if so.
+ const layout = binding.committed.layout orelse xkb_state.keyGetLayout(keycode);
+
+ switch (method) {
+ .no_translate => {
+ // Get keysyms from the base layer, as if modifiers didn't change keysyms.
+ // E.g. pressing `Super+Shift 1` does not translate to `Super Exclam`.
+ const keysyms = keymap.keyGetSymsByLevel(
+ keycode,
+ layout,
+ 0,
+ );
+
+ if (@as(u32, @bitCast(modifiers)) == @as(u32, @bitCast(binding.modifiers))) {
+ for (keysyms) |sym| {
+ if (sym == binding.keysym) {
+ return true;
+ }
+ }
+ }
+ },
+ .translate => {
+ // Keysyms and modifiers as translated by xkb.
+ // Modifiers used to translate the key are consumed.
+ // E.g. pressing `Super+Shift 1` translates to `Super Exclam`.
+ const keysyms_translated = keymap.keyGetSymsByLevel(
+ keycode,
+ layout,
+ xkb_state.keyGetLevel(keycode, layout),
+ );
+
+ const consumed = xkb_state.keyGetConsumedMods2(keycode, .xkb);
+ const modifiers_translated = @as(u32, @bitCast(modifiers)) & ~consumed;
+
+ if (modifiers_translated == @as(u32, @bitCast(binding.modifiers))) {
+ for (keysyms_translated) |sym| {
+ if (sym == binding.keysym) {
+ return true;
+ }
+ }
+ }
+ },
+ }
+
+ return false;
+}
diff --git a/rivercompat/Seat.zig b/rivercompat/Seat.zig
index 72d1d15..6bf1b25 100644
--- a/rivercompat/Seat.zig
+++ b/rivercompat/Seat.zig
@@ -19,21 +19,53 @@ const Seat = @This();
const std = @import("std");
const assert = std.debug.assert;
const wayland = @import("wayland");
+const xkb = @import("xkbcommon");
const wl = wayland.client.wl;
const river = wayland.client.river;
const Window = @import("Window.zig");
+const WindowManager = @import("WindowManager.zig");
+const XkbBinding = @import("XkbBinding.zig");
const gpa = std.heap.c_allocator;
+wm: *WindowManager,
seat_v1: *river.SeatV1,
+focused: ?*Window = null,
-pub fn create(seat_v1: *river.SeatV1) void {
+pub fn create(wm: *WindowManager, seat_v1: *river.SeatV1) void {
const seat = gpa.create(Seat) catch @panic("OOM");
seat.* = .{
+ .wm = wm,
.seat_v1 = seat_v1,
};
seat_v1.setListener(*Seat, handleEvent, seat);
+
+ XkbBinding.create(seat, xkb.Keysym.n, .{ .mod1 = true });
+}
+
+pub fn focus(seat: *Seat, target: ?*Window) void {
+ if (target) |window| {
+ seat.seat_v1.focusWindow(window.window_v1);
+ seat.focused = window;
+
+ window.link.remove();
+ seat.wm.windows.prepend(window);
+
+ window.node_v1.placeTop();
+ } else {
+ seat.seat_v1.clearFocus();
+ }
+}
+
+pub fn focusNext(seat: *Seat) void {
+ if (seat.focused != null) {
+ if (seat.wm.windows.length() >= 2) {
+ seat.focus(seat.wm.windows.last().?);
+ }
+ } else if (seat.wm.windows.first()) |top| {
+ seat.focus(top);
+ }
}
fn handleEvent(seat_v1: *river.SeatV1, event: river.SeatV1.Event, seat: *Seat) void {
@@ -49,8 +81,7 @@ fn handleEvent(seat_v1: *river.SeatV1, event: river.SeatV1.Event, seat: *Seat) v
.window_interaction => |args| {
const window_v1 = args.window orelse return;
const window: *Window = @ptrCast(@alignCast(window_v1.getUserData()));
- seat_v1.focusWindow(window_v1);
- window.node_v1.placeTop();
+ seat.focus(window);
},
}
}
diff --git a/rivercompat/WindowManager.zig b/rivercompat/WindowManager.zig
index 1e3b00f..b0a42c5 100644
--- a/rivercompat/WindowManager.zig
+++ b/rivercompat/WindowManager.zig
@@ -58,7 +58,7 @@ fn handleEvent(wm_v1: *river.WindowManagerV1, event: river.WindowManagerV1.Event
_ = args;
},
.seat => |args| {
- Seat.create(args.id);
+ Seat.create(wm, args.id);
},
}
}
diff --git a/rivercompat/XkbBinding.zig b/rivercompat/XkbBinding.zig
new file mode 100644
index 0000000..6bdf856
--- /dev/null
+++ b/rivercompat/XkbBinding.zig
@@ -0,0 +1,56 @@
+// This file is part of river, a dynamic tiling wayland compositor.
+//
+// Copyright 2024 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 XkbBinding = @This();
+
+const std = @import("std");
+const assert = std.debug.assert;
+const wayland = @import("wayland");
+const wl = wayland.client.wl;
+const river = wayland.client.river;
+
+const Seat = @import("Seat.zig");
+const Window = @import("Window.zig");
+
+const gpa = std.heap.c_allocator;
+
+seat: *Seat,
+xkb_binding_v1: *river.XkbBindingV1,
+
+pub fn create(
+ seat: *Seat,
+ keysym: u32,
+ modifiers: river.SeatV1.Modifiers,
+) void {
+ const xkb_binding_v1 = seat.seat_v1.getXkbBinding(keysym, modifiers) catch @panic("OOM");
+ const binding = gpa.create(XkbBinding) catch @panic("OOM");
+ binding.* = .{
+ .seat = seat,
+ .xkb_binding_v1 = xkb_binding_v1,
+ };
+ xkb_binding_v1.setListener(*XkbBinding, handleEvent, binding);
+ xkb_binding_v1.enable();
+}
+
+fn handleEvent(xkb_binding_v1: *river.XkbBindingV1, event: river.XkbBindingV1.Event, binding: *XkbBinding) void {
+ assert(binding.xkb_binding_v1 == xkb_binding_v1);
+ switch (event) {
+ .pressed => {
+ binding.seat.focusNext();
+ },
+ .released => {},
+ }
+}