This commit adds the focused window title to the left side of the bar and some WM info (primary count/ratio and # of visible/total windows) to the right side. It also adds new vertical_padding and horizontal_padding config options for the bar.
372 lines
13 KiB
Zig
372 lines
13 KiB
Zig
// SPDX-FileCopyrightText: 2025 Ben Buhse <me@benbuhse.email>
|
|
//
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
const Seat = @This();
|
|
|
|
context: *Context,
|
|
|
|
river_seat_v1: *river.SeatV1,
|
|
river_layer_shell_seat_v1: *river.LayerShellSeatV1,
|
|
|
|
focused_window: ?*Window,
|
|
focused_output: ?*Output,
|
|
layer_focus: LayerFocus = .focus_none,
|
|
|
|
pointer_op: PointerOp = .none,
|
|
|
|
/// State consumed in manage phase, reset at end of manage().
|
|
pending_manage: PendingManage = .{},
|
|
|
|
link: wl.list.Link,
|
|
|
|
// Pointer bindings for interactive move/resize
|
|
move_pointer_binding: ?*river.PointerBindingV1 = null,
|
|
resize_pointer_binding: ?*river.PointerBindingV1 = null,
|
|
|
|
// We just steal the Event's tag type to use as our enum. If another event is added
|
|
// that's *not* for focus, we'll have to create our own enum and just keep it in sync.
|
|
pub const LayerFocus = @typeInfo(river.LayerShellSeatV1.Event).@"union".tag_type.?;
|
|
|
|
pub const PendingManage = struct {
|
|
window: ?PendingWindow = null,
|
|
output: ?PendingOutput = null,
|
|
layer_focus: ?LayerFocus = null,
|
|
should_warp_pointer: bool = false,
|
|
|
|
op_delta: ?struct { dx: i32, dy: i32 } = null,
|
|
op_released: bool = false,
|
|
pointer_move_request: ?*Window = null,
|
|
pointer_resize_request: ?struct { window: *Window, edges: river.WindowV1.Edges } = null,
|
|
|
|
pub const PendingWindow = union(enum) {
|
|
window: *Window,
|
|
clear_focus,
|
|
};
|
|
|
|
pub const PendingOutput = union(enum) {
|
|
output: *Output,
|
|
clear_focus,
|
|
};
|
|
};
|
|
|
|
pub const PointerOp = union(enum) {
|
|
none,
|
|
move: struct { window: *Window, start_x: i32, start_y: i32 },
|
|
resize: struct {
|
|
window: *Window,
|
|
start_width: u31,
|
|
start_height: u31,
|
|
start_x: i32,
|
|
start_y: i32,
|
|
edges: river.WindowV1.Edges,
|
|
},
|
|
};
|
|
|
|
pub fn create(context: *Context, river_seat_v1: *river.SeatV1) !*Seat {
|
|
var seat = try utils.gpa.create(Seat);
|
|
errdefer utils.gpa.destroy(seat);
|
|
|
|
seat.* = .{
|
|
.context = context,
|
|
.river_seat_v1 = river_seat_v1,
|
|
.river_layer_shell_seat_v1 = try context.river_layer_shell_v1.getSeat(river_seat_v1),
|
|
.focused_window = null,
|
|
.focused_output = null,
|
|
.link = undefined, // Handled by the wl.list
|
|
};
|
|
|
|
seat.river_seat_v1.setListener(*Seat, seatListener, seat);
|
|
seat.river_layer_shell_seat_v1.setListener(*Seat, riverLayerShellSeatListener, seat);
|
|
|
|
return seat;
|
|
}
|
|
|
|
pub fn destroy(seat: *Seat) void {
|
|
if (seat.move_pointer_binding) |binding| binding.destroy();
|
|
if (seat.resize_pointer_binding) |binding| binding.destroy();
|
|
seat.river_seat_v1.destroy();
|
|
seat.river_layer_shell_seat_v1.destroy();
|
|
utils.gpa.destroy(seat);
|
|
}
|
|
|
|
fn seatListener(river_seat_v1: *river.SeatV1, event: river.SeatV1.Event, seat: *Seat) void {
|
|
assert(seat.river_seat_v1 == river_seat_v1);
|
|
switch (event) {
|
|
.removed => seat.destroy(),
|
|
.wl_seat => |ev| {
|
|
log.debug("initializing new river_seat_v1 corresponding to wl_seat: {d}", .{ev.name});
|
|
},
|
|
.pointer_enter => |ev| if (seat.context.config.focus_follows_pointer) {
|
|
seat.setWindowFocus(ev.window);
|
|
},
|
|
.window_interaction => |ev| seat.setWindowFocus(ev.window),
|
|
.op_delta => |ev| {
|
|
seat.pending_manage.op_delta = .{ .dx = ev.dx, .dy = ev.dy };
|
|
},
|
|
.op_release => {
|
|
seat.pending_manage.op_released = true;
|
|
},
|
|
else => |ev| {
|
|
log.debug("unhandled event: {s}", .{@tagName(ev)});
|
|
},
|
|
}
|
|
}
|
|
|
|
fn riverLayerShellSeatListener(river_layer_shell_seat_v1: *river.LayerShellSeatV1, event: river.LayerShellSeatV1.Event, seat: *Seat) void {
|
|
assert(seat.river_layer_shell_seat_v1 == river_layer_shell_seat_v1);
|
|
seat.pending_manage.layer_focus = std.meta.activeTag(event);
|
|
}
|
|
|
|
// river_window_v1 needs to be optional because ev.window is optional
|
|
fn setWindowFocus(seat: *Seat, river_window_v1: ?*river.WindowV1) void {
|
|
const wv1 = river_window_v1 orelse return;
|
|
const window: *Window = @ptrCast(@alignCast(wv1.getUserData() orelse {
|
|
log.err("river_window_v1 has no user data", .{});
|
|
return;
|
|
}));
|
|
seat.pending_manage.window = .{ .window = window };
|
|
}
|
|
|
|
pub fn manage(seat: *Seat) void {
|
|
defer seat.pending_manage = .{};
|
|
|
|
// Focus events are ignored by the compositor when a layer shell has exclusive focus.
|
|
if (seat.layer_focus != .focus_exclusive) {
|
|
if (seat.pending_manage.window) |pending_window| {
|
|
switch (pending_window) {
|
|
.window => |window| {
|
|
if (seat.focused_window) |focused| {
|
|
if (focused != window) {
|
|
// Tell the previously focused Window that it's no longer focused
|
|
focused.pending_render.focused = false;
|
|
// Update the Bar to have the newly-focused window's title
|
|
if (focused.output) |output| {
|
|
if (output.bar) |*bar| {
|
|
bar.pending_render.draw = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
seat.focused_window = window;
|
|
seat.river_seat_v1.focusWindow(window.river_window_v1);
|
|
window.pending_render.focused = true;
|
|
},
|
|
.clear_focus => {
|
|
if (seat.focused_window) |focused| {
|
|
// Tell the previously focused Window that it's no longer focused
|
|
focused.pending_render.focused = false;
|
|
}
|
|
seat.focused_window = null;
|
|
seat.river_seat_v1.clearFocus();
|
|
},
|
|
}
|
|
}
|
|
}
|
|
if (seat.pending_manage.output) |pending_output| {
|
|
switch (pending_output) {
|
|
.output => |output| {
|
|
seat.focused_output = output;
|
|
output.river_layer_shell_output_v1.setDefault();
|
|
},
|
|
.clear_focus => {
|
|
seat.focused_output = null;
|
|
},
|
|
}
|
|
}
|
|
|
|
// Handle layer focus changes after window focus so focused_window is up to date.
|
|
// This overrides whatever pending_render.focused the window focus block just set.
|
|
if (seat.pending_manage.layer_focus) |layer_focus| {
|
|
seat.layer_focus = layer_focus;
|
|
switch (layer_focus) {
|
|
.focus_exclusive,
|
|
.focus_non_exclusive,
|
|
=> {
|
|
if (seat.focused_window) |focused_window| {
|
|
focused_window.pending_render.focused = false;
|
|
}
|
|
},
|
|
.focus_none => {
|
|
if (seat.focused_window) |focused_window| {
|
|
seat.river_seat_v1.focusWindow(focused_window.river_window_v1);
|
|
focused_window.pending_render.focused = true;
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
// Focus doesn't change during .focus_exclusive, so pointer shouldn't get warped, either
|
|
if (seat.layer_focus != .focus_exclusive) {
|
|
if (seat.pending_manage.should_warp_pointer) blk: {
|
|
if (seat.context.config.pointer_warp_on_focus_change) {
|
|
const window = seat.focused_window orelse {
|
|
log.warn("Trying to warp-on-focus-change without a focused window.", .{});
|
|
break :blk;
|
|
};
|
|
// Warp pointer to center of focused window;
|
|
// because the x and y coords are set during render, we need to check if
|
|
// there are new coordinates in window.pending_render.
|
|
const pos = window.pending_render.position;
|
|
const pointer_x: i32 = (if (pos) |p| p.x else window.rect.x) + @divFloor(window.rect.width, 2);
|
|
const pointer_y: i32 = (if (pos) |p| p.y else window.rect.y) + @divFloor(window.rect.height, 2);
|
|
seat.river_seat_v1.pointerWarp(pointer_x, pointer_y);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Interactive move/resize operations
|
|
|
|
// Start move operation
|
|
if (seat.pending_manage.pointer_move_request) |window| {
|
|
if (window.floating) {
|
|
seat.pointer_op = .{
|
|
.move = .{
|
|
.window = window,
|
|
.start_x = window.floating_rect.x,
|
|
.start_y = window.floating_rect.y,
|
|
},
|
|
};
|
|
seat.river_seat_v1.opStartPointer();
|
|
}
|
|
}
|
|
|
|
// Start resize operation
|
|
if (seat.pending_manage.pointer_resize_request) |req| {
|
|
if (req.window.floating) {
|
|
seat.pointer_op = .{
|
|
.resize = .{
|
|
.window = req.window,
|
|
.start_width = req.window.floating_rect.width,
|
|
.start_height = req.window.floating_rect.height,
|
|
.start_x = req.window.floating_rect.x,
|
|
.start_y = req.window.floating_rect.y,
|
|
.edges = req.edges,
|
|
},
|
|
};
|
|
seat.river_seat_v1.opStartPointer();
|
|
req.window.river_window_v1.informResizeStart();
|
|
}
|
|
}
|
|
|
|
// Process pointer delta (mouse movement during operation)
|
|
if (seat.pending_manage.op_delta) |delta| {
|
|
switch (seat.pointer_op) {
|
|
.none => {},
|
|
.move => |op| {
|
|
op.window.floating_rect.x = op.start_x + delta.dx;
|
|
op.window.floating_rect.y = op.start_y + delta.dy;
|
|
op.window.pending_render.position = .{
|
|
.x = op.window.floating_rect.x,
|
|
.y = op.window.floating_rect.y,
|
|
};
|
|
},
|
|
.resize => |op| {
|
|
var new_width: i32 = op.start_width;
|
|
var new_height: i32 = op.start_height;
|
|
var new_x: i32 = op.start_x;
|
|
var new_y: i32 = op.start_y;
|
|
|
|
// Adjust based on which edges are being dragged
|
|
if (op.edges.right) new_width += delta.dx;
|
|
if (op.edges.left) {
|
|
new_width -= delta.dx;
|
|
new_x += delta.dx;
|
|
}
|
|
if (op.edges.bottom) new_height += delta.dy;
|
|
if (op.edges.top) {
|
|
new_height -= delta.dy;
|
|
new_y += delta.dy;
|
|
}
|
|
|
|
// Clamp to minimum size
|
|
const min_size: i32 = 50;
|
|
if (op.edges.left) new_x = @min(new_x, op.start_x + @as(i32, op.start_width) - min_size);
|
|
if (op.edges.top) new_y = @min(new_y, op.start_y + @as(i32, op.start_height) - min_size);
|
|
new_width = @max(new_width, min_size);
|
|
new_height = @max(new_height, min_size);
|
|
|
|
op.window.floating_rect.width = @intCast(new_width);
|
|
op.window.floating_rect.height = @intCast(new_height);
|
|
op.window.floating_rect.x = new_x;
|
|
op.window.floating_rect.y = new_y;
|
|
|
|
op.window.river_window_v1.proposeDimensions(
|
|
op.window.floating_rect.width,
|
|
op.window.floating_rect.height,
|
|
);
|
|
op.window.pending_render.position = .{
|
|
.x = op.window.floating_rect.x,
|
|
.y = op.window.floating_rect.y,
|
|
};
|
|
},
|
|
}
|
|
}
|
|
|
|
// Process pointer release (end of operation)
|
|
if (seat.pending_manage.op_released) {
|
|
switch (seat.pointer_op) {
|
|
.none => {},
|
|
.move => {
|
|
seat.river_seat_v1.opEnd();
|
|
seat.pointer_op = .none;
|
|
},
|
|
.resize => |op| {
|
|
op.window.river_window_v1.informResizeEnd();
|
|
seat.river_seat_v1.opEnd();
|
|
seat.pointer_op = .none;
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn render(seat: *Seat) void {
|
|
_ = seat;
|
|
}
|
|
|
|
pub fn movePointerBindingListener(_: *river.PointerBindingV1, event: river.PointerBindingV1.Event, seat: *Seat) void {
|
|
switch (event) {
|
|
.pressed => {
|
|
const window = seat.focused_window orelse return;
|
|
if (!window.floating) {
|
|
// Auto-float on drag
|
|
window.pending_manage.floating = true;
|
|
}
|
|
seat.pending_manage.pointer_move_request = window;
|
|
seat.context.wm.river_window_manager_v1.manageDirty();
|
|
},
|
|
.released => {},
|
|
}
|
|
}
|
|
|
|
pub fn resizePointerBindingListener(_: *river.PointerBindingV1, event: river.PointerBindingV1.Event, seat: *Seat) void {
|
|
switch (event) {
|
|
.pressed => {
|
|
const window = seat.focused_window orelse return;
|
|
if (!window.floating) {
|
|
// Auto-float on drag
|
|
window.pending_manage.floating = true;
|
|
}
|
|
seat.pending_manage.pointer_resize_request = .{
|
|
.window = window,
|
|
.edges = .{ .bottom = true, .right = true },
|
|
};
|
|
seat.context.wm.river_window_manager_v1.manageDirty();
|
|
},
|
|
.released => {},
|
|
}
|
|
}
|
|
|
|
const std = @import("std");
|
|
const assert = std.debug.assert;
|
|
|
|
const wayland = @import("wayland");
|
|
const wl = wayland.client.wl;
|
|
const river = wayland.client.river;
|
|
|
|
const utils = @import("utils.zig");
|
|
const Context = @import("Context.zig");
|
|
const Output = @import("Output.zig");
|
|
const Window = @import("Window.zig");
|
|
|
|
const log = std.log.scoped(.Seat);
|