507 lines
17 KiB
Zig
507 lines
17 KiB
Zig
//! 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 => {},
|
|
}
|
|
}
|