//! 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, /// A screen to warp the pointer onto in the next manage sequence. Set when /// focus moves to an output with no window on it to warp to. pending_warp_output: ?*Output = 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 focus was last moved to, else the one under the pointer. /// /// That middle case is what makes an empty screen a place the user can be. /// Moving to a screen with nothing on it clears the focused window, and with /// only the pointer to fall back on the seat would go on reporting the screen it /// came from — so the tag keys, the layout keys and the next window spawned /// would all land back on the monitor just left. pub fn currentOutput(self: *Seat) ?*Output { if (self.focused) |win| { if (win.output) |out| return out; } if (self.wm.focused_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(); } if (self.pending_warp_output) |out| { self.warpToOutput(out); self.pending_warp_output = null; } } /// 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), ); } /// Pull the pointer onto a screen the keyboard has just moved to, when there is /// no window there to warp to instead. /// /// Without it, moving to an empty screen leaves the cursor on the one before — /// and with `focus_follows_mouse` on, the first window the pointer then brushes /// past takes the focus straight back. Manage sequence only. fn warpToOutput(self: *Seat, out: *Output) void { if (!config.warp_cursor) return; if (self.op != null) return; const area = out.layoutArea(); if (area.width <= 0 or area.height <= 0) return; if (area.contains(self.pointer_x, self.pointer_y)) return; self.seat.pointerWarp( area.x + @divTrunc(area.width, 2), area.y + @divTrunc(area.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; // Sloppy focus crosses screens too, as dwm's motion handler does: // the pointer leaving a monitor is what moves the seat to the next // one, so the tag and layout keys follow the cursor even over a // screen with no window on it to focus. if (config.focus_follows_mouse and self.op == null) { if (self.wm.outputAt(ev.x, ev.y)) |out| { if (self.wm.focused_output != out) self.wm.enterOutput(self, out); } } }, .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 => {}, } }