Add multi-output support

Primary ratio is per-output.

If an output is disconnected/disabled, its windows get sent to the
previous output in the output list. If all outputs are disconnected,
windows are added to an orphan list in the WM. Once an output is
re-added, the orphans are all given to that output.

When a window is sent to a new output, it keeps the same tags as it
had before. I may add an option to take the new output's tags.

- Rename focus_next/focus_prev to focus_next_window/focus_prev_window
- Add focus_next_output/focus_prev_output
- Add send_to_next_output/send_to_prev_output commands to move windows
    between outputs

Split Seat.PendingManage.PendingFocus into separate pending output
and pending window structs

Fix window outputs when closing outputs
This commit is contained in:
Ben Buhse 2026-02-03 17:17:29 -06:00
commit 0f85278aea
No known key found for this signature in database
GPG key ID: 7916ACFCD38FD0B4
8 changed files with 478 additions and 269 deletions

View file

@ -18,11 +18,7 @@ fullscreen: bool = false,
maximized: bool = false,
tags: u32 = 0x0001,
// output: ?*Output,
// XXX: Do I really even need to store `show`?
// I think I only would if I was trying not to send extraneous requests.
show: bool = true,
output: ?*Output,
initialized: bool = false,
@ -46,7 +42,12 @@ pub const PendingManage = struct {
maximized: ?bool = null,
tags: ?u32 = null,
// output: ?*Output = null,
pending_output: ?PendingOutput = null,
pub const PendingOutput = union(enum) {
output: *Output,
clear_output,
};
};
pub const PendingRender = struct {
@ -58,7 +59,7 @@ pub const PendingRender = struct {
show: ?bool = null,
};
pub fn create(context: *Context, river_window_v1: *river.WindowV1, output: *Output) !*Window {
pub fn create(context: *Context, river_window_v1: *river.WindowV1, output: ?*Output) !*Window {
var window = try utils.allocator.create(Window);
errdefer window.destroy();
@ -66,8 +67,8 @@ pub fn create(context: *Context, river_window_v1: *river.WindowV1, output: *Outp
.context = context,
.river_window_v1 = river_window_v1,
.river_node_v1 = river_window_v1.getNode() catch @panic("Failed to get node"),
// .output = output,
.tags = if (output.tags != 0) output.tags else 0x0001,
.output = output,
.tags = if (output) |o| o.tags else 0x0001,
.link = undefined, // Handled by the wl.list
};
@ -87,22 +88,23 @@ fn windowListener(river_window_v1: *river.WindowV1, event: river.WindowV1.Event,
assert(window.river_window_v1 == river_window_v1);
switch (event) {
.closed => {
window.context.wm.window_count -= 1;
{
// If there's no output, we don't really care about focus and can skip this event
const output = if (window.output) |output| output else return;
var it = window.context.wm.seats.iterator(.forward);
while (it.next()) |seat| {
if (seat.focused == window) {
if (seat.focused_window == window) {
// Find another window to focus and warp pointer there
if (window.context.wm.getPrevWindow(window)) |next_focus| {
if (output.prevWindow(window)) |next_focus| {
if (next_focus != window) {
seat.pending_manage.pending_focus = .{ .window = next_focus };
seat.pending_manage.window = .{ .window = next_focus };
seat.pending_manage.should_warp_pointer = true;
} else {
// Only window in list - clear focus
seat.pending_manage.pending_focus = .clear_focus;
seat.pending_manage.window = .clear_focus;
}
} else {
seat.pending_manage.pending_focus = .clear_focus;
seat.pending_manage.window = .clear_focus;
}
}
}
@ -111,7 +113,7 @@ fn windowListener(river_window_v1: *river.WindowV1, event: river.WindowV1.Event,
window.destroy();
},
.dimensions => |ev| {
// The protocol requires these are strictly greather than zero.
// The protocol requires these are strictly greater than zero.
assert(ev.width > 0 and ev.height > 0);
window.width = @intCast(ev.width);
window.height = @intCast(ev.height);
@ -151,10 +153,11 @@ pub fn manage(window: *Window) void {
}
}
// Fullscreen and maximize operations
if (pending_manage.fullscreen) |fullscreen| {
if (pending_manage.fullscreen) |fullscreen| blk: {
window.fullscreen = fullscreen;
if (fullscreen) {
const output = window.context.wm.outputs.first() orelse @panic("Failed to get output");
// If the window isn't on an output, just skip fullscreening
const output = window.output orelse break :blk;
window.river_window_v1.fullscreen(output.river_output_v1);
window.river_window_v1.informFullscreen();
} else {
@ -174,6 +177,14 @@ pub fn manage(window: *Window) void {
if (pending_manage.tags) |tags| {
window.tags = tags;
}
if (pending_manage.pending_output) |pending_output| {
switch (pending_output) {
.output => |output| {
window.output = output;
},
.clear_output => window.output = null,
}
}
}
pub fn render(window: *Window) void {