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
285 lines
10 KiB
Zig
285 lines
10 KiB
Zig
// SPDX-FileCopyrightText: 2025 Ben Buhse <me@benbuhse.email>
|
|
//
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
const Output = @This();
|
|
|
|
context: *Context,
|
|
|
|
river_output_v1: *river.OutputV1,
|
|
|
|
width: i32 = 0,
|
|
height: i32 = 0,
|
|
x: i32 = 0,
|
|
y: i32 = 0,
|
|
|
|
/// Proportion of output width taken by the primary stack
|
|
primary_ratio: f32 = 0.55,
|
|
|
|
/// Tags are 32-bit bitfield. A window can be active on one(?) or more tags.
|
|
tags: u32 = 0x0001,
|
|
|
|
/// State consumed in manage() phase, reset at end of manage().
|
|
pending_manage: PendingManage = .{},
|
|
|
|
windows: wl.list.Head(Window, .link),
|
|
|
|
link: wl.list.Link,
|
|
|
|
pub const PendingManage = struct {
|
|
width: ?i32 = null,
|
|
height: ?i32 = null,
|
|
x: ?i32 = null,
|
|
y: ?i32 = null,
|
|
|
|
tags: ?u32 = null,
|
|
primary_ratio: ?f32 = null,
|
|
};
|
|
|
|
pub fn create(context: *Context, river_output_v1: *river.OutputV1) !*Output {
|
|
var output = try utils.allocator.create(Output);
|
|
errdefer output.destroy();
|
|
|
|
output.* = .{
|
|
.context = context,
|
|
.river_output_v1 = river_output_v1,
|
|
.windows = undefined, // we will initialize this shortly
|
|
.link = undefined, // Handled by the wl.list
|
|
};
|
|
|
|
output.windows.init();
|
|
|
|
output.river_output_v1.setListener(*Output, outputListener, output);
|
|
|
|
return output;
|
|
}
|
|
|
|
pub fn destroy(output: *Output) void {
|
|
var it = output.windows.safeIterator(.forward);
|
|
while (it.next()) |window| {
|
|
window.link.remove();
|
|
window.destroy();
|
|
}
|
|
|
|
output.river_output_v1.destroy();
|
|
utils.allocator.destroy(output);
|
|
}
|
|
|
|
/// Get the next window in the list, wrapping to first if at end
|
|
pub fn nextWindow(output: *Output, current: *Window) ?*Window {
|
|
const next_link = current.link.next orelse return output.windows.first();
|
|
// If we've reached the sentinel (head's link), wrap to first
|
|
if (next_link == &output.windows.link) return output.windows.first();
|
|
return @fieldParentPtr("link", next_link);
|
|
}
|
|
|
|
/// Get the previous window in the list, wrapping to last if at beginning
|
|
pub fn prevWindow(output: *Output, current: *Window) ?*Window {
|
|
const prev_link = current.link.prev orelse return output.windows.last();
|
|
// If we've reached the sentinel (head's link), wrap to last
|
|
if (prev_link == &output.windows.link) return output.windows.last();
|
|
return @fieldParentPtr("link", prev_link);
|
|
}
|
|
|
|
fn outputListener(river_output_v1: *river.OutputV1, event: river.OutputV1.Event, output: *Output) void {
|
|
assert(output.river_output_v1 == river_output_v1);
|
|
switch (event) {
|
|
.removed => {
|
|
const context = output.context;
|
|
const wm = context.wm;
|
|
|
|
// Move windows to the previous output in the list.
|
|
// If this was the only output, windows become orphans.
|
|
const prev_output: ?*Output = if (wm.prevOutput(output)) |prev| blk: {
|
|
if (prev == output) break :blk null; // Only output; wrapped to itself
|
|
break :blk prev; // We got the previous list
|
|
} else unreachable;
|
|
|
|
const window_pending_output: Window.PendingManage.PendingOutput =
|
|
if (prev_output) |prev|
|
|
.{ .output = prev }
|
|
else
|
|
.clear_output;
|
|
|
|
// Update each window's output before moving the list
|
|
var it = output.windows.iterator(.forward);
|
|
while (it.next()) |window| {
|
|
window.pending_manage.pending_output = window_pending_output;
|
|
}
|
|
|
|
// Move windows to new destination
|
|
const dest_list = if (prev_output) |prev| &prev.windows else &wm.orphan_windows;
|
|
dest_list.appendList(&output.windows);
|
|
|
|
blk: {
|
|
// If the removed output was focused, move focus to the next
|
|
// available output (and its first window, if any).
|
|
// TODO: Support multiple seats
|
|
const seat = wm.seats.first() orelse break :blk;
|
|
if (seat.focused_output != output) break :blk;
|
|
|
|
const next_output = wm.nextOutput(output);
|
|
if (next_output == output) break :blk;
|
|
const o = next_output orelse break :blk;
|
|
|
|
seat.pending_manage.output = .{ .output = o };
|
|
if (o.windows.first()) |window| {
|
|
seat.pending_manage.window = .{ .window = window };
|
|
}
|
|
}
|
|
|
|
output.link.remove();
|
|
output.destroy();
|
|
},
|
|
.wl_output => |ev| {
|
|
log.debug("initializing new river_output_v1 corresponding to wl_output: {d}", .{ev.name});
|
|
},
|
|
.dimensions => |ev| {
|
|
output.pending_manage.width = ev.width;
|
|
output.pending_manage.height = ev.height;
|
|
output.context.wm.river_window_manager_v1.manageDirty();
|
|
},
|
|
.position => |ev| {
|
|
output.pending_manage.x = ev.x;
|
|
output.pending_manage.y = ev.y;
|
|
output.context.wm.river_window_manager_v1.manageDirty();
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn manage(output: *Output) void {
|
|
defer output.pending_manage = .{};
|
|
|
|
if (output.pending_manage.width) |width| {
|
|
output.width = width;
|
|
}
|
|
if (output.pending_manage.height) |height| {
|
|
output.height = height;
|
|
}
|
|
if (output.pending_manage.x) |x| {
|
|
output.x = x;
|
|
}
|
|
if (output.pending_manage.y) |y| {
|
|
output.y = y;
|
|
}
|
|
|
|
if (output.pending_manage.tags) |tags| {
|
|
output.tags = tags;
|
|
}
|
|
if (output.pending_manage.primary_ratio) |primary_ratio| {
|
|
// Ratios outside of this range could cause crashes (when doing the layout calculation)
|
|
output.primary_ratio = std.math.clamp(primary_ratio, 0.10, 0.90);
|
|
}
|
|
|
|
// Calculate layout before managing windows
|
|
output.calculatePrimaryStackLayout();
|
|
var it = output.windows.iterator(.forward);
|
|
while (it.next()) |window| {
|
|
window.manage();
|
|
}
|
|
}
|
|
|
|
pub fn render(output: *Output) void {
|
|
var it = output.windows.iterator(.forward);
|
|
while (it.next()) |window| {
|
|
window.render();
|
|
}
|
|
}
|
|
|
|
// TODO - CONFIG: Allow primary on the left
|
|
// TODO - CONFIG: Allow setting a ratio for single-window width (useful for ultrawides)
|
|
/// Calculate primary/stack layout positions for all windows.
|
|
/// - Single window: maximized
|
|
/// - Multiple windows: stack (45% left, vertically tiled), primary (55% right)
|
|
fn calculatePrimaryStackLayout(output: *Output) void {
|
|
// Get a list of active windows
|
|
var active_list: DoublyLinkedList = .{};
|
|
var active_count: u31 = 0;
|
|
var it = output.windows.iterator(.forward);
|
|
while (it.next()) |window| {
|
|
if (output.tags & window.tags != 0x0000) {
|
|
active_list.append(&window.active_list_node);
|
|
active_count += 1;
|
|
window.pending_render.show = true;
|
|
} else {
|
|
window.pending_render.show = false;
|
|
}
|
|
}
|
|
|
|
if (active_count == 0) return;
|
|
|
|
// Output dimensions come as i32 from the protocol, convert to u31 for window dimensions
|
|
// since they can't be negative.
|
|
const output_width: u31 = @intCast(output.width);
|
|
const output_height: u31 = @intCast(output.height);
|
|
const output_x = output.x;
|
|
const output_y = output.y;
|
|
|
|
// Iterate through the active windows and apply the tags
|
|
var i: u31 = 0;
|
|
while (active_list.popFirst()) |node| : (i += 1) {
|
|
const window: *Window = @fieldParentPtr("active_list_node", node);
|
|
if (active_count == 1) {
|
|
// Single window: maximize
|
|
window.pending_render.x = output_x;
|
|
window.pending_render.y = output_y;
|
|
window.pending_manage.width = output_width;
|
|
window.pending_manage.height = output_height;
|
|
window.pending_manage.maximized = true;
|
|
} else {
|
|
// Multiple windows: primary/stack layout
|
|
// TODO: Support multiple windows in primary stack
|
|
const primary_width: u31 = @intFromFloat(@as(f32, @floatFromInt(output_width)) * output.primary_ratio);
|
|
const stack_width: u31 = output_width - primary_width;
|
|
const stack_count = active_count - 1;
|
|
const stack_height: u31 = @divFloor(output_height, stack_count);
|
|
window.pending_manage.maximized = false;
|
|
|
|
if (i == 0) {
|
|
// Primary window (first window) - right side
|
|
window.pending_render.x = output_x + @as(i32, stack_width);
|
|
window.pending_render.y = output_y;
|
|
window.pending_manage.width = primary_width;
|
|
window.pending_manage.height = output_height;
|
|
} else {
|
|
// Stack window(s) - left side
|
|
const stack_index = i - 1;
|
|
window.pending_render.x = output_x;
|
|
window.pending_render.y = output_y + @as(i32, stack_index) * @as(i32, stack_height);
|
|
window.pending_manage.width = stack_width;
|
|
// Last stack window gets remaining height to avoid gaps from integer division
|
|
if (i == active_count - 1) {
|
|
window.pending_manage.height = output_height - stack_index * stack_height;
|
|
} else {
|
|
window.pending_manage.height = stack_height;
|
|
}
|
|
}
|
|
}
|
|
// Make space for borders; this is the same for all windows.
|
|
// Borders are automatically disabled when a window is fullscreened so we don't
|
|
// have to worry about that.
|
|
const border_width = output.context.config.border_width;
|
|
// We use .? because we know we set the windows height, width, x, and y above
|
|
window.pending_manage.height.? -= 2 * border_width;
|
|
window.pending_manage.width.? -= 2 * border_width;
|
|
window.pending_render.x.? += border_width;
|
|
window.pending_render.y.? += border_width;
|
|
}
|
|
|
|
// Make sure we went through the whole list
|
|
assert(active_list.first == null);
|
|
}
|
|
|
|
const std = @import("std");
|
|
const assert = std.debug.assert;
|
|
const DoublyLinkedList = std.DoublyLinkedList;
|
|
|
|
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 Window = @import("Window.zig");
|
|
|
|
const log = std.log.scoped(.Output);
|