initial commit

This commit is contained in:
2026-07-26 20:47:19 +02:00
commit fff3a5e734
37 changed files with 10781 additions and 0 deletions
+461
View File
@@ -0,0 +1,461 @@
//! A seat: keyboard focus, key and pointer bindings, and interactive
//! move/resize operations.
//!
//! river delivers binding and pointer events immediately but requires the
//! matching protocol requests to be made inside a manage sequence. Every
//! handler here therefore records intent in a field and calls `needsManage`;
//! `applyManage` is the only place that talks back to the compositor.
const Seat = @This();
const std = @import("std");
const wayland = @import("wayland");
const wl = wayland.client.wl;
const river = wayland.client.river;
const config = @import("config");
const action = @import("action");
const Wm = @import("Wm.zig");
const Window = @import("Window.zig");
const Output = @import("Output.zig");
const layout = @import("layout.zig");
const Box = layout.Box;
pub const KeyBinding = struct {
seat: *Seat,
/// Index into config.keys.
index: usize,
object: *river.XkbBindingV1,
};
pub const PointerBinding = struct {
seat: *Seat,
/// Index into config.buttons.
index: usize,
object: *river.PointerBindingV1,
};
pub const Op = struct {
kind: enum { move, resize },
window: *Window,
/// The window's cell when the operation started.
start: Box,
/// For resize: which corner is being dragged.
edges: river.WindowV1.Edges = .{},
};
wm: *Wm,
seat: *river.SeatV1,
layer_seat: ?*river.LayerShellSeatV1 = null,
wl_seat_name: u32 = 0,
wl_seat: ?*wl.Seat = null,
pointer: ?*wl.Pointer = null,
keys: std.ArrayList(*KeyBinding) = .empty,
buttons: std.ArrayList(*PointerBinding) = .empty,
/// Set once the bindings have been enabled in a manage sequence.
bindings_enabled: bool = false,
focused: ?*Window = null,
/// Focus we want river to apply in the next manage sequence.
pending_focus: ?*Window = null,
/// True when focus should be cleared rather than moved.
pending_clear_focus: bool = false,
/// A layer surface (a bar, a launcher) holds focus; our focus requests are
/// either ignored or would steal it.
layer_focus: enum { none, exclusive, non_exclusive } = .none,
/// Pointer position in the compositor's logical coordinate space.
pointer_x: i32 = 0,
pointer_y: i32 = 0,
/// The window the pointer is currently inside.
hovered: ?*Window = null,
op: ?Op = null,
/// An operation to start in the next manage sequence.
pending_op: ?Op = null,
/// The running operation should be ended in the next manage sequence.
pending_op_end: bool = false,
/// One of our own surfaces has pointer focus (the tab bar).
pointer_surface: ?*wl.Surface = null,
pointer_local_x: f64 = 0,
pointer_local_y: f64 = 0,
removed: bool = false,
pub fn create(wm: *Wm, seat: *river.SeatV1) !*Seat {
const self = try wm.gpa.create(Seat);
self.* = .{ .wm = wm, .seat = seat };
seat.setListener(*Seat, onEvent, self);
if (wm.layer_shell) |ls| {
self.layer_seat = ls.getSeat(seat) catch null;
if (self.layer_seat) |lseat| lseat.setListener(*Seat, onLayerEvent, self);
}
try self.createBindings();
if (config.cursor_theme) |theme| {
var buf: [256]u8 = undefined;
const z = std.fmt.bufPrintZ(&buf, "{s}", .{theme}) catch null;
if (z) |name| seat.setXcursorTheme(name, config.cursor_size);
}
return self;
}
pub fn destroy(self: *Seat) void {
const gpa = self.wm.gpa;
for (self.keys.items) |binding| {
binding.object.destroy();
gpa.destroy(binding);
}
for (self.buttons.items) |binding| {
binding.object.destroy();
gpa.destroy(binding);
}
self.keys.deinit(gpa);
self.buttons.deinit(gpa);
if (self.pointer) |p| p.release();
if (self.wl_seat) |s| s.release();
if (self.layer_seat) |l| l.destroy();
self.seat.destroy();
gpa.destroy(self);
}
fn createBindings(self: *Seat) !void {
const gpa = self.wm.gpa;
if (self.wm.xkb_bindings) |xkb_bindings| {
try self.keys.ensureTotalCapacity(gpa, config.keys.len);
for (config.keys, 0..) |key, i| {
const mods: river.SeatV1.Modifiers = @bitCast(key.mods);
const object = xkb_bindings.getXkbBinding(self.seat, @intFromEnum(key.keysym), mods) catch |err| {
std.log.err("failed to bind key {t}: {s}", .{ key.keysym, @errorName(err) });
continue;
};
const binding = try gpa.create(KeyBinding);
binding.* = .{ .seat = self, .index = i, .object = object };
object.setListener(*KeyBinding, onKeyEvent, binding);
self.keys.appendAssumeCapacity(binding);
}
}
std.log.info("registered {d}/{d} key bindings", .{ self.keys.items.len, config.keys.len });
try self.buttons.ensureTotalCapacity(gpa, config.buttons.len);
for (config.buttons, 0..) |button, i| {
const mods: river.SeatV1.Modifiers = @bitCast(button.mods);
const object = self.seat.getPointerBinding(button.button, mods) catch |err| {
std.log.err("failed to bind button {d}: {s}", .{ button.button, @errorName(err) });
continue;
};
const binding = try gpa.create(PointerBinding);
binding.* = .{ .seat = self, .index = i, .object = object };
object.setListener(*PointerBinding, onButtonEvent, binding);
self.buttons.appendAssumeCapacity(binding);
}
std.log.info("registered {d}/{d} pointer bindings", .{ self.buttons.items.len, config.buttons.len });
}
/// The output this seat is working on: the one holding the focused window,
/// else the one under the pointer.
pub fn currentOutput(self: *Seat) ?*Output {
if (self.focused) |win| {
if (win.output) |out| return out;
}
return self.wm.outputAt(self.pointer_x, self.pointer_y) orelse self.wm.firstOutput();
}
pub fn focus(self: *Seat, window: ?*Window) void {
if (window) |win| {
self.pending_focus = win;
self.pending_clear_focus = false;
} else {
self.pending_focus = null;
self.pending_clear_focus = true;
}
self.wm.needsManage();
}
pub fn startMove(self: *Seat, window: *Window) void {
if (window.fullscreen) return;
self.pending_op = .{ .kind = .move, .window = window, .start = window.cell };
self.wm.needsManage();
}
pub fn startResize(self: *Seat, window: *Window, edges: river.WindowV1.Edges) void {
if (window.fullscreen) return;
self.pending_op = .{
.kind = .resize,
.window = window,
.start = window.cell,
.edges = edges,
};
self.wm.needsManage();
}
/// Issue the requests recorded by the event handlers. Manage sequence only.
pub fn applyManage(self: *Seat) void {
if (!self.bindings_enabled) {
for (self.keys.items) |binding| binding.object.enable();
for (self.buttons.items) |binding| binding.object.enable();
self.bindings_enabled = true;
}
if (self.pending_op) |op| {
// Dragging a tiled window pops it out into floating, as in dwm.
if (!op.window.floating) {
op.window.floating = true;
op.window.floating_forced = true;
op.window.float_box = op.window.cell;
}
self.seat.opStartPointer();
self.op = op;
self.op.?.start = op.window.cell;
self.pending_op = null;
op.window.window.informResizeStart();
}
if (self.pending_op_end) {
if (self.op) |op| {
self.seat.opEnd();
if (!op.window.closed) op.window.window.informResizeEnd();
}
self.op = null;
self.pending_op_end = false;
}
// A layer surface with exclusive focus outranks us entirely.
if (self.layer_focus == .exclusive) {
self.pending_focus = null;
self.pending_clear_focus = false;
return;
}
if (self.pending_focus) |win| {
if (!win.closed and win.mapped) {
self.seat.focusWindow(win.window);
self.focused = win;
self.wm.focus_serial += 1;
win.focus_serial = self.wm.focus_serial;
self.warpTo(win);
self.wm.ipcDirty();
}
self.pending_focus = null;
} else if (self.pending_clear_focus) {
self.seat.clearFocus();
self.focused = null;
self.pending_clear_focus = false;
self.wm.ipcDirty();
}
}
/// Pull the pointer to the middle of a newly focused window.
///
/// Skipped when the pointer is already inside it, so keyboard focus following
/// the mouse does not yank the cursor out from under the user. Manage sequence
/// only.
fn warpTo(self: *Seat, win: *Window) void {
if (!config.warp_cursor) return;
if (self.op != null) return;
if (win.cell.contains(self.pointer_x, self.pointer_y)) return;
self.seat.pointerWarp(
win.cell.x + @divTrunc(win.cell.width, 2),
win.cell.y + @divTrunc(win.cell.height, 2),
);
}
fn onEvent(_: *river.SeatV1, event: river.SeatV1.Event, self: *Seat) void {
switch (event) {
.removed => {
self.removed = true;
self.wm.needsManage();
},
.wl_seat => |ev| {
self.wl_seat_name = ev.name;
self.wm.attachWlSeat(self);
},
.pointer_position => |ev| {
self.pointer_x = ev.x;
self.pointer_y = ev.y;
},
.pointer_enter => |ev| {
const win = Wm.windowFromProxy(ev.window) orelse return;
self.hovered = win;
if (config.focus_follows_mouse and self.op == null) {
if (win.mapped and win.visible) self.focus(win);
}
},
.pointer_leave => {
self.hovered = null;
},
.window_interaction => |ev| {
const win = Wm.windowFromProxy(ev.window) orelse return;
// Clicking a window focuses it and, if floating, raises it.
self.focus(win);
if (win.output) |out| self.wm.focusOutput(out);
},
.shell_surface_interaction => {
// Our own tab bar; handled through wl_pointer where we know the
// coordinates.
},
.op_delta => |ev| {
const op = self.op orelse return;
const win = op.window;
if (win.closed) return;
switch (op.kind) {
.move => {
win.cell.x = op.start.x + ev.dx;
win.cell.y = op.start.y + ev.dy;
win.float_box = win.cell;
},
.resize => {
var box = op.start;
if (op.edges.left) {
box.x = op.start.x + ev.dx;
box.width = op.start.width - ev.dx;
} else {
box.width = op.start.width + ev.dx;
}
if (op.edges.top) {
box.y = op.start.y + ev.dy;
box.height = op.start.height - ev.dy;
} else {
box.height = op.start.height + ev.dy;
}
const min = 2 * config.border_width + 1;
box.width = @max(min, box.width);
box.height = @max(min, box.height);
win.cell = box;
win.float_box = box;
},
}
self.wm.needsManage();
},
.op_release => {
self.pending_op_end = true;
self.wm.needsManage();
},
}
}
fn onLayerEvent(_: *river.LayerShellSeatV1, event: river.LayerShellSeatV1.Event, self: *Seat) void {
switch (event) {
.focus_exclusive => self.layer_focus = .exclusive,
.focus_non_exclusive => self.layer_focus = .non_exclusive,
.focus_none => {
self.layer_focus = .none;
// Hand focus back to whatever the user was using.
if (self.focused) |win| {
if (!win.closed and win.visible) self.focus(win);
}
},
}
self.wm.needsManage();
}
fn onKeyEvent(_: *river.XkbBindingV1, event: river.XkbBindingV1.Event, binding: *KeyBinding) void {
const self = binding.seat;
const key = config.keys[binding.index];
switch (event) {
.pressed => {
self.wm.perform(self, key.action);
if (key.shouldRepeat()) self.wm.startRepeat(self, binding.index);
},
.released, .stop_repeat => {
self.wm.stopRepeat(binding.index);
},
}
}
fn onButtonEvent(_: *river.PointerBindingV1, event: river.PointerBindingV1.Event, binding: *PointerBinding) void {
const self = binding.seat;
const button = config.buttons[binding.index];
switch (event) {
.pressed => {
const win = self.hovered orelse self.wm.windowAt(self.pointer_x, self.pointer_y) orelse return;
self.focus(win);
switch (button.action) {
.move => self.startMove(win),
.resize => {
// Resize from whichever corner the pointer is nearest, so
// the drag pulls the expected edge.
const mid_x = win.cell.x + @divTrunc(win.cell.width, 2);
const mid_y = win.cell.y + @divTrunc(win.cell.height, 2);
self.startResize(win, .{
.left = self.pointer_x < mid_x,
.right = self.pointer_x >= mid_x,
.top = self.pointer_y < mid_y,
.bottom = self.pointer_y >= mid_y,
});
},
}
},
.released => {
self.pending_op_end = true;
self.wm.needsManage();
},
}
}
// ─── wl_pointer, used only to click the tab bar ──────────────────────────────
pub fn onWlSeatEvent(_: *wl.Seat, event: wl.Seat.Event, self: *Seat) void {
switch (event) {
.capabilities => |ev| {
if (ev.capabilities.pointer and self.pointer == null) {
self.pointer = self.wl_seat.?.getPointer() catch null;
if (self.pointer) |p| p.setListener(*Seat, onPointerEvent, self);
}
},
.name => {},
}
}
fn onPointerEvent(_: *wl.Pointer, event: wl.Pointer.Event, self: *Seat) void {
switch (event) {
.enter => |ev| {
self.pointer_surface = ev.surface;
self.pointer_local_x = ev.surface_x.toDouble();
self.pointer_local_y = ev.surface_y.toDouble();
},
.leave => {
self.pointer_surface = null;
},
.motion => |ev| {
self.pointer_local_x = ev.surface_x.toDouble();
self.pointer_local_y = ev.surface_y.toDouble();
},
.button => |ev| {
if (ev.state != .pressed) return;
const surface = self.pointer_surface orelse return;
const out = self.wm.outputForTabBarSurface(surface) orelse return;
const gx = out.tabbar.box.x + @as(i32, @intFromFloat(self.pointer_local_x));
const gy = out.tabbar.box.y + @as(i32, @intFromFloat(self.pointer_local_y));
if (out.tabbar.windowAt(gx, gy)) |win| {
self.wm.focusOutput(out);
self.focus(win);
}
},
else => {},
}
}