Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
river: implement river_pointer_binding_v1
protocol/river-window-management-v1.xml | 2 +-
river/Config.zig | 17 ----
river/Cursor.zig | 143 +++++++++++++++++--------------
river/PointerBinding.zig | 146 ++++++++++++++++++++++++++++++++
river/Seat.zig | 73 +++++++++++++++-
rivercompat/PointerBinding.zig | 58 +++++++++++++
rivercompat/Seat.zig | 6 +-
rivercompat/c.zig | 3 +
8 files changed, 364 insertions(+), 84 deletions(-)
diff --git a/protocol/river-window-management-v1.xml b/protocol/river-window-management-v1.xml
index 453880f..b503882 100644
--- a/protocol/river-window-management-v1.xml
+++ b/protocol/river-window-management-v1.xml
@@ -1199,7 +1199,7 @@
The button argument is a Linux input event code defined in the
linux/input-event-codes.h header file (e.g. BTN_RIGHT).
- The new key binding is not enabled until initial configuration is
+ The new pointer binding is not enabled until initial configuration is
completed, the enable request is made, and the change is committed with
a river_window_manager_v1.commit request.
</description>
diff --git a/river/Config.zig b/river/Config.zig
index a36b73e..438cc9d 100644
--- a/river/Config.zig
+++ b/river/Config.zig
@@ -19,7 +19,6 @@ const Config = @This();
const std = @import("std");
const fmt = std.fmt;
const mem = std.mem;
-const globber = @import("globber");
const wlr = @import("wlroots");
const xkb = @import("xkbcommon");
@@ -27,19 +26,7 @@ const server = &@import("main.zig").server;
const util = @import("util.zig");
const Server = @import("Server.zig");
-const Output = @import("Output.zig");
const Switch = @import("Switch.zig");
-const Window = @import("Window.zig");
-
-pub const Position = struct {
- x: u31,
- y: u31,
-};
-
-pub const Dimensions = struct {
- width: u31,
- height: u31,
-};
/// Color of background in RGBA with premultiplied alpha (alpha should only affect nested sessions)
background_color: [4]f32 = [_]f32{ 0.0, 0.16862745, 0.21176471, 1.0 }, // Solarized base03
@@ -50,10 +37,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
-pointer_mappings: std.ArrayListUnmanaged(struct {
- event_code: u32,
- modifiers: wlr.Keyboard.ModifierMask,
-}) = .{},
switch_mappings: std.ArrayListUnmanaged(struct {
switch_type: Switch.Type,
switch_state: Switch.State,
diff --git a/river/Cursor.zig b/river/Cursor.zig
index 6405ab4..057f4dd 100644
--- a/river/Cursor.zig
+++ b/river/Cursor.zig
@@ -35,6 +35,7 @@ const DragIcon = @import("DragIcon.zig");
const InputDevice = @import("InputDevice.zig");
const LockSurface = @import("LockSurface.zig");
const Output = @import("Output.zig");
+const PointerBinding = @import("PointerBinding.zig");
const PointerConstraint = @import("PointerConstraint.zig");
const Scene = @import("Scene.zig");
const Seat = @import("Seat.zig");
@@ -46,7 +47,10 @@ const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig");
const log = std.log.scoped(.input);
const Mode = union(enum) {
- passthrough: void,
+ passthrough,
+ /// This mode is entered when a binding is triggered and exited when there
+ /// are no longer any buttons pressed.
+ ignore,
down: struct {
// TODO: To handle the surface with pointer focus being moved during
// down mode we need to store the starting location of the surface as
@@ -127,8 +131,8 @@ xcursor_manager: *wlr.XcursorManager,
/// surface to be used as the cursor shape instead.
xcursor_name: ?[*:0]const u8 = null,
-/// Number of distinct buttons currently pressed
-pressed_count: u32 = 0,
+/// The set of currently pressed pointer buttons and the corresponding pointer mapping if any.
+pressed: std.AutoHashMapUnmanaged(u32, ?*PointerBinding) = .{},
/// The pointer constraint for the surface that currently has keyboard focus, if any.
/// This constraint is not necessarily active, activation only occurs once the cursor
@@ -233,6 +237,8 @@ pub fn init(cursor: *Cursor, seat: *Seat) !void {
pub fn deinit(cursor: *Cursor) void {
cursor.xcursor_manager.destroy();
cursor.wlr_cursor.destroy();
+ cursor.pressed.deinit(util.gpa);
+ cursor.touch_points.deinit(util.gpa);
}
/// Set the cursor theme for the given seat, as well as the xwayland theme if
@@ -332,13 +338,14 @@ pub fn processMotionRelative(cursor: *Cursor, event: *const wlr.Pointer.event.Mo
}
switch (cursor.mode) {
- .passthrough, .down => {
+ .passthrough, .ignore, .down => {
cursor.wlr_cursor.move(event.device, dx, dy);
switch (cursor.mode) {
.passthrough => {
cursor.passthrough(event.time_msec);
},
+ .ignore => {},
.down => |data| {
cursor.seat.wlr_seat.pointerNotifyMotion(
event.time_msec,
@@ -438,57 +445,84 @@ pub fn processMotionAbsolute(cursor: *Cursor, event: *const wlr.Pointer.event.Mo
}
pub fn processButton(cursor: *Cursor, event: *const wlr.Pointer.event.Button) void {
- if (event.state == .released) {
- assert(cursor.pressed_count > 0);
- cursor.pressed_count -= 1;
- if (cursor.pressed_count == 0 and cursor.mode != .passthrough) {
- log.debug("leaving {s} mode", .{@tagName(cursor.mode)});
+ if (event.state == .pressed) {
+ const result = cursor.pressed.getOrPut(util.gpa, event.button) catch {
+ log.err("out of memory", .{});
+ return;
+ };
+ if (result.found_existing) {
+ log.err("ignoring duplicate pointer button {d} press", .{event.button});
+ return;
+ }
+
+ if (cursor.seat.matchPointerBinding(event.button)) |binding| {
+ result.value_ptr.* = binding;
+ binding.pressed();
+ log.debug("entering cursor mode ignore", .{});
+ cursor.mode = .ignore;
+ cursor.clearFocus();
+ return;
+ }
+
+ result.value_ptr.* = null;
+
+ switch (cursor.mode) {
+ .passthrough => {
+ if (server.scene.at(cursor.wlr_cursor.x, cursor.wlr_cursor.y)) |at| {
+ cursor.interact(at);
+
+ if (at.surface != null) {
+ _ = cursor.seat.wlr_seat.pointerNotifyButton(event.time_msec, event.button, event.state);
+ log.debug("entering cursor mode down", .{});
+ cursor.mode = .{
+ .down = .{
+ .lx = cursor.wlr_cursor.x,
+ .ly = cursor.wlr_cursor.y,
+ .sx = at.sx,
+ .sy = at.sy,
+ },
+ };
+ return;
+ }
+ }
+
+ log.debug("entering cursor mode ignore", .{});
+ cursor.mode = .ignore;
+ cursor.clearFocus();
+ return;
+ },
+ // Pointer focus does not change while in down mode.
+ .down => {
+ _ = cursor.seat.wlr_seat.pointerNotifyButton(event.time_msec, event.button, event.state);
+ },
+ // No client has pointer focus while in ignore/move/resize mode.
+ .ignore, .move, .resize => {},
+ }
+ } else {
+ assert(event.state == .released);
+ const result = cursor.pressed.fetchRemove(event.button);
+ if (result) |kv| {
+ if (kv.value) |binding| {
+ binding.released();
+ }
switch (cursor.mode) {
.passthrough => unreachable,
.down => {
- // If we were in down mode, we need pass along the release event
_ = cursor.seat.wlr_seat.pointerNotifyButton(event.time_msec, event.button, event.state);
},
- .move, .resize => {},
+ // No client has pointer focus while in ignore/move/resize mode.
+ .ignore, .move, .resize => {},
+ }
+ if (cursor.pressed.count() == 0) {
+ log.debug("exiting cursor mode {s}", .{@tagName(cursor.mode)});
+ cursor.mode = .passthrough;
+ cursor.passthrough(event.time_msec);
}
-
- cursor.mode = .passthrough;
- cursor.passthrough(event.time_msec);
} else {
- _ = cursor.seat.wlr_seat.pointerNotifyButton(event.time_msec, event.button, event.state);
- }
- return;
- }
-
- assert(event.state == .pressed);
- cursor.pressed_count += 1;
-
- if (cursor.pressed_count > 1) {
- _ = cursor.seat.wlr_seat.pointerNotifyButton(event.time_msec, event.button, event.state);
- return;
- }
-
- if (server.scene.at(cursor.wlr_cursor.x, cursor.wlr_cursor.y)) |result| {
- if (result.data == .window and cursor.handlePointerMapping(event, result.data.window)) {
- // If a mapping is triggered don't send events to clients.
+ log.err("ignoring duplicate pointer button {d} release", .{event.button});
return;
}
-
- cursor.interact(result);
-
- _ = cursor.seat.wlr_seat.pointerNotifyButton(event.time_msec, event.button, event.state);
-
- if (result.surface != null) {
- cursor.mode = .{
- .down = .{
- .lx = cursor.wlr_cursor.x,
- .ly = cursor.wlr_cursor.y,
- .sx = result.sx,
- .sy = result.sy,
- },
- };
- }
}
}
@@ -673,21 +707,6 @@ fn handleTabletToolButton(
tool.button(tablet, event);
}
-/// Handle the mapping for the passed button if any. Returns true if there
-/// was a mapping and the button was handled.
-fn handlePointerMapping(cursor: *Cursor, event: *const wlr.Pointer.event.Button, _: *Window) bool {
- const wlr_keyboard = cursor.seat.wlr_seat.getKeyboard() orelse return false;
- // XXX this is not ok, we need to store current modifiers per-Keyboard ourselves
- const modifiers = wlr_keyboard.getModifiers();
-
- return for (server.config.pointer_mappings.items) |mapping| {
- if (event.button == mapping.event_code and std.meta.eql(modifiers, mapping.modifiers)) {
- // trigger action
- break true;
- }
- } else false;
-}
-
pub fn startMove(cursor: *Cursor, window: *Window) void {
if (cursor.constraint) |constraint| {
if (constraint.state == .active) constraint.deactivate();
@@ -797,7 +816,7 @@ pub fn updateState(cursor: *Cursor) void {
cursor.passthrough(msec);
},
// TODO: Leave down mode if the target surface is no longer visible.
- .down => {},
+ .ignore, .down => {},
.move, .resize => {
// Moving and resizing of windows is handled through the transaction system. Therefore,
// we must inspect the inflight_mode instead if a move or a resize is in progress.
@@ -814,7 +833,7 @@ pub fn updateState(cursor: *Cursor) void {
// Therefore, the user already expects the cursor to be free from the window and
// we should not warp it back to the fixed offset of the move/resize.
switch (cursor.inflight_mode) {
- .passthrough, .down => {},
+ .passthrough, .ignore, .down => {},
inline .move, .resize => |data, mode| {
// These conditions are checked in WindowManager.dirtyPending()
diff --git a/river/PointerBinding.zig b/river/PointerBinding.zig
new file mode 100644
index 0000000..b7ee0fe
--- /dev/null
+++ b/river/PointerBinding.zig
@@ -0,0 +1,146 @@
+// 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 PointerBinding = @This();
+
+const std = @import("std");
+const assert = std.debug.assert;
+const wlr = @import("wlroots");
+const wayland = @import("wayland");
+const wl = wayland.server.wl;
+const river = wayland.server.river;
+
+const c = @import("c.zig");
+const server = &@import("main.zig").server;
+const util = @import("util.zig");
+
+const Seat = @import("Seat.zig");
+
+const log = std.log.scoped(.input);
+
+const WmState = struct {
+ enabled: bool = false,
+};
+
+seat: *Seat,
+object: *river.PointerBindingV1,
+
+button: u32,
+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.pointer_bindings
+link: wl.list.Link,
+
+pub fn create(
+ seat: *Seat,
+ client: *wl.Client,
+ version: u32,
+ id: u32,
+ button: u32,
+ modifiers: river.SeatV1.Modifiers,
+) !void {
+ const binding = try util.gpa.create(PointerBinding);
+ errdefer util.gpa.destroy(binding);
+
+ const pointer_binding_v1 = try river.PointerBindingV1.create(client, version, id);
+ errdefer comptime unreachable;
+
+ log.debug("new river_pointer_binding_v1: button: {d}({?s}) modifiers: {d}", .{
+ button,
+ c.libevdev_event_code_get_name(c.EV_KEY, button),
+ @as(u32, @bitCast(modifiers)),
+ });
+
+ binding.* = .{
+ .seat = seat,
+ .object = pointer_binding_v1,
+ .button = button,
+ .modifiers = modifiers,
+ .link = undefined,
+ };
+ pointer_binding_v1.setHandler(*PointerBinding, handleRequest, handleDestroy, binding);
+
+ seat.pointer_bindings.append(binding);
+}
+
+fn handleDestroy(_: *river.PointerBindingV1, binding: *PointerBinding) void {
+ if (binding.seat.cursor.pressed.getPtr(binding.button)) |value_ptr| {
+ // It is possible for the window manager to create duplicate pointer bindings.
+ if (value_ptr.* == binding) {
+ value_ptr.* = null;
+ }
+ }
+
+ binding.link.remove();
+ util.gpa.destroy(binding);
+}
+
+fn handleRequest(
+ pointer_binding_v1: *river.PointerBindingV1,
+ request: river.PointerBindingV1.Request,
+ binding: *PointerBinding,
+) void {
+ assert(binding.object == pointer_binding_v1);
+ switch (request) {
+ .destroy => pointer_binding_v1.destroy(),
+ .enable => binding.uncommitted.enabled = true,
+ .disable => binding.uncommitted.enabled = false,
+ }
+}
+
+pub fn pressed(binding: *PointerBinding) 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: *PointerBinding) 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();
+}
+
+pub fn match(
+ binding: *const PointerBinding,
+ button: u32,
+ modifiers: wlr.Keyboard.ModifierMask,
+) bool {
+ if (!binding.committed.enabled) return false;
+
+ return button == binding.button and
+ @as(u32, @bitCast(modifiers)) == @as(u32, @bitCast(binding.modifiers));
+}
diff --git a/river/Seat.zig b/river/Seat.zig
index 82c9c8f..643cd6f 100644
--- a/river/Seat.zig
+++ b/river/Seat.zig
@@ -37,6 +37,7 @@ 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");
const PointerConstraint = @import("PointerConstraint.zig");
const Switch = @import("Switch.zig");
const Tablet = @import("Tablet.zig");
@@ -137,6 +138,7 @@ uncommitted: WmState = .{},
committed: WmState = .{},
xkb_bindings: wl.list.Head(XkbBinding, .link),
+pointer_bindings: wl.list.Head(PointerBinding, .link),
/// Multiple physical mice are handled by the same Cursor
cursor: Cursor,
@@ -174,6 +176,7 @@ pub fn create(name: [*:0]const u8) !void {
.link_pending = undefined,
.link_sent = undefined,
.xkb_bindings = undefined,
+ .pointer_bindings = undefined,
.cursor = undefined,
.relay = undefined,
};
@@ -185,6 +188,7 @@ pub fn create(name: [*:0]const u8) !void {
server.wm.dirtyPending();
seat.xkb_bindings.init();
+ seat.pointer_bindings.init();
try seat.cursor.init(seat);
seat.relay.init();
@@ -362,6 +366,25 @@ pub fn sendDirty(seat: *Seat) void {
binding.pending.state_change = .none;
}
}
+ {
+ var it = seat.pointer_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;
+ }
+ }
}
}
@@ -413,7 +436,20 @@ fn handleRequest(
return;
};
},
- .get_pointer_binding => {},
+ .get_pointer_binding => |args| {
+ PointerBinding.create(
+ seat,
+ seat_v1.getClient(),
+ seat_v1.getVersion(),
+ args.id,
+ args.button,
+ args.modifiers,
+ ) catch {
+ seat_v1.getClient().postNoMemory();
+ log.err("out of memory", .{});
+ return;
+ };
+ },
}
}
@@ -424,6 +460,12 @@ pub fn commitWmState(seat: *Seat) void {
binding.committed = binding.uncommitted;
}
}
+ {
+ var it = seat.pointer_bindings.iterator(.forward);
+ while (it.next()) |binding| {
+ binding.committed = binding.uncommitted;
+ }
+ }
seat.committed = seat.uncommitted;
}
@@ -546,7 +588,7 @@ pub fn matchXkbBinding(
if (found == null) {
found = binding;
} else {
- log.debug("already found a matching binding, ignoring additional match", .{});
+ log.debug("already found a matching xkb_binding, ignoring additional match", .{});
}
}
}
@@ -563,7 +605,32 @@ pub fn matchXkbBinding(
if (found == null) {
found = binding;
} else {
- log.debug("already found a matching binding, ignoring additional match", .{});
+ log.debug("already found a matching xkb_binding, ignoring additional match", .{});
+ }
+ }
+ }
+ }
+
+ return found;
+}
+
+pub fn matchPointerBinding(
+ seat: *Seat,
+ button: u32,
+) ?*PointerBinding {
+ const wlr_keyboard = seat.wlr_seat.getKeyboard() orelse return null;
+ // XXX this is not ok, we need to store current modifiers per-Keyboard ourselves
+ const modifiers = wlr_keyboard.getModifiers();
+
+ var found: ?*PointerBinding = null;
+ {
+ var it = seat.pointer_bindings.iterator(.forward);
+ while (it.next()) |binding| {
+ if (binding.match(button, modifiers)) {
+ if (found == null) {
+ found = binding;
+ } else {
+ log.debug("already found a matching pointer binding, ignoring additional match", .{});
}
}
}
diff --git a/rivercompat/PointerBinding.zig b/rivercompat/PointerBinding.zig
new file mode 100644
index 0000000..5cdf0fb
--- /dev/null
+++ b/rivercompat/PointerBinding.zig
@@ -0,0 +1,58 @@
+// 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 PointerBinding = @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,
+pointer_binding_v1: *river.PointerBindingV1,
+
+pub fn create(
+ seat: *Seat,
+ button: u32,
+ modifiers: river.SeatV1.Modifiers,
+) void {
+ const pointer_binding_v1 = seat.seat_v1.getPointerBinding(button, modifiers) catch @panic("OOM");
+ const binding = gpa.create(PointerBinding) catch @panic("OOM");
+ binding.* = .{
+ .seat = seat,
+ .pointer_binding_v1 = pointer_binding_v1,
+ };
+ pointer_binding_v1.setListener(*PointerBinding, handleEvent, binding);
+ pointer_binding_v1.enable();
+}
+
+fn handleEvent(pointer_binding_v1: *river.PointerBindingV1, event: river.PointerBindingV1.Event, binding: *PointerBinding) void {
+ assert(binding.pointer_binding_v1 == pointer_binding_v1);
+ switch (event) {
+ .pressed => {
+ if (binding.seat.focused) |window| {
+ window.window_v1.close();
+ }
+ },
+ .released => {},
+ }
+}
diff --git a/rivercompat/Seat.zig b/rivercompat/Seat.zig
index 6bf1b25..7b04b7e 100644
--- a/rivercompat/Seat.zig
+++ b/rivercompat/Seat.zig
@@ -23,9 +23,12 @@ const xkb = @import("xkbcommon");
const wl = wayland.client.wl;
const river = wayland.client.river;
+const c = @import("c.zig");
+
const Window = @import("Window.zig");
const WindowManager = @import("WindowManager.zig");
const XkbBinding = @import("XkbBinding.zig");
+const PointerBinding = @import("PointerBinding.zig");
const gpa = std.heap.c_allocator;
@@ -41,7 +44,8 @@ pub fn create(wm: *WindowManager, seat_v1: *river.SeatV1) void {
};
seat_v1.setListener(*Seat, handleEvent, seat);
- XkbBinding.create(seat, xkb.Keysym.n, .{ .mod1 = true });
+ XkbBinding.create(seat, xkb.Keysym.n, .{ .mod4 = true });
+ PointerBinding.create(seat, c.BTN_RIGHT, .{ .mod4 = true });
}
pub fn focus(seat: *Seat, target: ?*Window) void {
diff --git a/rivercompat/c.zig b/rivercompat/c.zig
new file mode 100644
index 0000000..38eb7c2
--- /dev/null
+++ b/rivercompat/c.zig
@@ -0,0 +1,3 @@
+pub usingnamespace @cImport({
+ @cInclude("linux/input-event-codes.h");
+});