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

@ -7,12 +7,16 @@ SPDX-License-Identifier: GPL-3.0-or-later
# beansprout wm
## TODOs
[ ] Support multiple outputs
[ ] Support multiple seats
[ ] Support floating windows
[ ] Support wallpapers
[ ] Support a bar
[ ] Support starting programs at WM launch
[ ] Support changeable primary ratio
[ ] Support changeable primary count
[ ] Support overriding config location
These are in rough order of my priority, though no promises I do them in this order.
- [ ] Support floating windows
- [ ] Support wallpapers
- [ ] Support a bar
- [ ] Support changeable primary count
- [ ] Support starting programs at WM launch
- [ ] Support overriding config location
- [ ] Add support for multimedia/brightness keys
- [ ] Support multiple seats
- [x] Support changeable primary ratio
- [x] Support multiple outputs

View file

@ -8,11 +8,16 @@ borders {
}
keybinds {
spawn Mod4 T foot
focus_next Mod4 J
focus_prev Mod4 K
focus_next_window Mod4 J
focus_prev_window Mod4 K
focus_next_output Mod4+Shift J
focus_prev_output Mod4+Shift K
send_to_next_output Mod1+Shift J
send_to_prev_output Mod1+Shift K
zoom Mod4 Z
change_ratio Mod4 H +0.05
change_ratio Mod4 L -0.05
reload_config Mod4+Shift R
toggle_fullscreen Mod4 F
close_window Mod4+Shift Q
// Generates keybinds for keys 1-9 → tags 1<<0 through 1<<9

View file

@ -324,13 +324,19 @@ fn loadKeybindsChildBlock(config: *Config, parser: *kdl.Parser) !void {
};
break :sw .{ .change_ratio = diff };
},
inline .focus_next, .focus_prev, .zoom, .reload_config, .toggle_fullscreen, .close_window => |cmd| {
inline .focus_next_window,
.focus_prev_window,
.focus_next_output,
.focus_prev_output,
.send_to_next_output,
.send_to_prev_output,
.zoom,
.reload_config,
.toggle_fullscreen,
.close_window,
=> |cmd| {
// None of these have arguments, just create the union and give it back
break :sw @unionInit(
XkbBindings.Command,
@tagName(cmd),
{},
);
break :sw @unionInit(XkbBindings.Command, @tagName(cmd), {});
},
inline .set_output_tags, .set_window_tags, .toggle_output_tags, .toggle_window_tags => |cmd| {
const tags_str = utils.stripQuotes(node.arg(parser, 2) orelse {
@ -341,11 +347,7 @@ fn loadKeybindsChildBlock(config: *Config, parser: *kdl.Parser) !void {
logWarnInvalidNodeArg(name, tags_str);
continue;
};
break :sw @unionInit(
XkbBindings.Command,
@tagName(cmd),
tags,
);
break :sw @unionInit(XkbBindings.Command, @tagName(cmd), tags);
},
};

View file

@ -13,15 +13,27 @@ 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 {
@ -31,33 +43,106 @@ pub fn create(context: *Context, river_output_v1: *river.OutputV1) !*Output {
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 => output.destroy(),
.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.width = ev.width;
output.height = ev.height;
output.pending_manage.width = ev.width;
output.pending_manage.height = ev.height;
output.context.wm.river_window_manager_v1.manageDirty();
},
.position => |ev| {
output.x = ev.x;
output.y = ev.y;
output.pending_manage.x = ev.x;
output.pending_manage.y = ev.y;
output.context.wm.river_window_manager_v1.manageDirty();
},
}
}
@ -65,17 +150,129 @@ fn outputListener(river_output_v1: *river.OutputV1, event: river.OutputV1.Event,
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 {
_ = output;
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;
@ -83,5 +280,6 @@ 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);

View file

@ -8,7 +8,8 @@ context: *Context,
river_seat_v1: *river.SeatV1,
focused: ?*Window,
focused_window: ?*Window,
focused_output: ?*Output,
/// State consumed in manage phase, reset at end of manage().
pending_manage: PendingManage = .{},
@ -16,13 +17,19 @@ pending_manage: PendingManage = .{},
link: wl.list.Link,
pub const PendingManage = struct {
pending_focus: ?PendingFocus = null,
window: ?PendingWindow = null,
output: ?PendingOutput = null,
should_warp_pointer: bool = false,
pub const PendingFocus = union(enum) {
pub const PendingWindow = union(enum) {
window: *Window,
clear_focus,
};
pub const PendingOutput = union(enum) {
output: *Output,
clear_focus,
};
};
pub fn create(context: *Context, river_seat_v1: *river.SeatV1) !*Seat {
@ -32,7 +39,8 @@ pub fn create(context: *Context, river_seat_v1: *river.SeatV1) !*Seat {
seat.* = .{
.context = context,
.river_seat_v1 = river_seat_v1,
.focused = null,
.focused_window = null,
.focused_output = null,
.link = undefined, // Handled by the wl.list
};
@ -66,41 +74,51 @@ fn seatListener(river_seat_v1: *river.SeatV1, event: river.SeatV1.Event, seat: *
// 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()));
seat.pending_manage.pending_focus = .{ .window = window };
const window: *Window = @ptrCast(@alignCast(wv1.getUserData() orelse return));
seat.pending_manage.window = .{ .window = window };
}
pub fn manage(seat: *Seat) void {
defer seat.pending_manage = .{};
if (seat.pending_manage.pending_focus) |pending_focus| {
switch (pending_focus) {
if (seat.pending_manage.window) |pending_window| {
switch (pending_window) {
.window => |window| {
if (seat.focused) |focused| {
if (seat.focused_window) |focused| {
// Tell the previously focused Window that it's no longer focused
if (focused != window) {
focused.pending_render.focused = false;
}
}
seat.focused = window;
seat.focused_window = window;
seat.river_seat_v1.focusWindow(window.river_window_v1);
window.pending_render.focused = true;
},
.clear_focus => {
if (seat.focused) |focused| {
if (seat.focused_window) |focused| {
// Tell the previously focused Window that it's no longer focused
focused.pending_render.focused = false;
}
seat.focused = null;
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;
},
.clear_focus => {
seat.focused_output = null;
},
}
}
if (seat.pending_manage.should_warp_pointer) blk: {
if (seat.context.config.pointer_warp_on_focus_change) {
const window = seat.focused orelse {
log.err("Trying to warp-on-focus-change without a focused window.", .{});
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;
@ -126,6 +144,7 @@ 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);

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 {

View file

@ -12,18 +12,9 @@ river_window_manager_v1: *river.WindowManagerV1,
seats: wl.list.Head(Seat, .link),
outputs: wl.list.Head(Output, .link),
windows: wl.list.Head(Window, .link),
window_count: u8 = 0,
primary_ratio: f32 = 0.55,
/// State consumed in manage phase, reset at end of manage_start().
pending_manage: PendingManage = .{},
pub const PendingManage = struct {
primary_ratio: ?f32 = null,
};
/// Place to store windows if all Outputs have been disconnected
orphan_windows: wl.list.Head(Window, .link),
pub fn create(context: *Context, window_manager_v1: *river.WindowManagerV1) !*WindowManager {
const wm = try utils.allocator.create(WindowManager);
@ -34,12 +25,12 @@ pub fn create(context: *Context, window_manager_v1: *river.WindowManagerV1) !*Wi
.river_window_manager_v1 = window_manager_v1,
.seats = undefined, // we will initialize these shortly
.outputs = undefined,
.windows = undefined,
.orphan_windows = undefined,
};
wm.seats.init();
wm.outputs.init();
wm.windows.init();
wm.orphan_windows.init();
wm.river_window_manager_v1.setListener(*WindowManager, windowManagerV1Listener, wm);
@ -47,13 +38,6 @@ pub fn create(context: *Context, window_manager_v1: *river.WindowManagerV1) !*Wi
}
pub fn destroy(wm: *WindowManager) void {
{
var it = wm.windows.safeIterator(.forward);
while (it.next()) |window| {
window.link.remove();
window.destroy();
}
}
{
var it = wm.outputs.safeIterator(.forward);
while (it.next()) |output| {
@ -72,118 +56,23 @@ pub fn destroy(wm: *WindowManager) void {
utils.allocator.destroy(wm);
}
/// Get the next window in the list, wrapping to first if at end
pub fn getNextWindow(wm: *WindowManager, current: *Window) ?*Window {
var it = wm.windows.iterator(.forward);
while (it.next()) |window| {
if (window == current) {
return it.next() orelse wm.windows.first();
}
}
return wm.windows.first();
/// Get the next output in the list, wrapping to first if at end
pub fn nextOutput(wm: *WindowManager, current: *Output) ?*Output {
const next_link = current.link.next orelse return wm.outputs.first();
// If we've reached the sentinel (head's link), wrap to first
if (next_link == &wm.outputs.link) return wm.outputs.first();
return @fieldParentPtr("link", next_link);
}
/// Get the previous window in the list, wrapping to last if at beginning
pub fn getPrevWindow(wm: *WindowManager, current: *Window) ?*Window {
var prev: ?*Window = null;
var it = wm.windows.iterator(.forward);
while (it.next()) |window| {
if (window == current) {
return prev orelse wm.windows.last();
}
prev = window;
}
return wm.windows.last();
}
/// Calculate primary/stack layout positions for all windows.
/// - Single window: maximized
/// - Multiple windows: stack (45% left, vertically tiled), primary (55% right)
fn calculatePrimaryStackLayout(wm: *WindowManager) void {
// TODO: Support multiple outputs
const output = wm.outputs.first() orelse return;
// Get a list of active windows
var active_list: DoublyLinkedList = .{};
var active_count: u31 = 0;
var it = wm.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)) * wm.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 = wm.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);
/// Get the previous output in the list, wrapping to last if at beginning
pub fn prevOutput(wm: *WindowManager, current: *Output) ?*Output {
const prev_link = current.link.prev orelse return wm.outputs.last();
// If we've reached the sentinel (head's link), wrap to last
if (prev_link == &wm.outputs.link) return wm.outputs.last();
return @fieldParentPtr("link", prev_link);
}
fn manage_start(wm: *WindowManager) void {
defer wm.pending_manage = .{};
const river_window_manager_v1 = wm.river_window_manager_v1;
const context = wm.context;
@ -223,27 +112,12 @@ fn manage_start(wm: *WindowManager) void {
}
}
// Manage the WM itself before outputs/windows/seats
if (wm.pending_manage.primary_ratio) |primary_ratio| {
// Ratios outside of this range can cause crashes anyways (when doing the layout calulcation)
std.debug.assert(primary_ratio >= 0.10 and primary_ratio <= 0.90);
wm.primary_ratio = primary_ratio;
}
{
var it = wm.outputs.iterator(.forward);
while (it.next()) |output| {
output.manage();
}
}
{
// Calculate layout before managing windows
wm.calculatePrimaryStackLayout();
var it = wm.windows.iterator(.forward);
while (it.next()) |window| {
window.manage();
}
}
{
var it = wm.seats.iterator(.forward);
while (it.next()) |seat| {
@ -267,12 +141,6 @@ fn render_start(wm: *WindowManager) void {
output.render();
}
}
{
var it = wm.windows.iterator(.forward);
while (it.next()) |window| {
window.render();
}
}
river_window_manager_v1.renderFinish();
}
@ -286,10 +154,25 @@ fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: riv
.manage_start => wm.manage_start(),
.render_start => wm.render_start(),
.output => |ev| {
log.debug("initializing new wl_output: {d}", .{ev.id.getId()});
// TODO: Support multi-output
const output = Output.create(context, ev.id) catch @panic("Out of memory");
wm.outputs.append(output);
// If there was already a seat, but no outputs, set this new output as focused
const seat = wm.seats.first() orelse return;
if (seat.focused_output == null and seat.pending_manage.output == null) {
seat.pending_manage.output = .{ .output = output };
}
// If there are orphan windows, send them to the new output
var it = wm.orphan_windows.iterator(.forward);
while (it.next()) |window| {
// We need to make sure to set up their new output
window.pending_manage.pending_output = .{ .output = output };
}
if (wm.orphan_windows.first()) |first| {
// and focus the first one
first.pending_render.focused = true;
}
output.windows.appendList(&wm.orphan_windows);
},
.seat => |ev| {
// TODO: Support multi-seat (maybe ?)
@ -302,22 +185,26 @@ fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: riv
const seat = Seat.create(context, river_seat_v1) catch @panic("Out of memory");
wm.seats.append(seat);
// If there was already an output, but no seats, set the first output as focused
seat.pending_manage.output = .{ .output = wm.outputs.first() orelse return };
},
.window => |ev| {
// TODO: Support multiple seats
const seat = wm.seats.first() orelse @panic("Failed to get seat");
// TODO: Support multiple outputs
const output = wm.outputs.first() orelse @panic("Failed to get output");
const window = Window.create(context, ev.id, output) catch @panic("Out of memory");
const focused_output = seat.focused_output;
const window_list = if (focused_output) |output|
&output.windows
else
&wm.orphan_windows;
const window = Window.create(context, ev.id, focused_output) catch @panic("Out of memory");
switch (context.config.attach_mode) {
.top => wm.windows.prepend(window),
.bottom => wm.windows.append(window),
.top => window_list.prepend(window),
.bottom => window_list.append(window),
}
seat.pending_manage.pending_focus = .{ .window = window };
seat.pending_manage.window = .{ .window = window };
seat.pending_manage.should_warp_pointer = true;
wm.window_count += 1;
},
else => |ev| {
log.debug("unhandled event: {s}", .{@tagName(ev)});
@ -328,7 +215,6 @@ fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: riv
const std = @import("std");
const assert = std.debug.assert;
const fatal = std.process.fatal;
const DoublyLinkedList = std.DoublyLinkedList;
const wayland = @import("wayland");
const wl = wayland.client.wl;

View file

@ -6,9 +6,14 @@ const XkbBindings = @This();
pub const Command = union(enum) {
spawn: []const []const u8,
focus_next,
focus_prev,
focus_next_window,
focus_prev_window,
focus_next_output,
focus_prev_output,
send_to_next_output,
send_to_prev_output,
zoom,
// Changes the ratio on the focused output only
change_ratio: f32,
reload_config,
toggle_fullscreen,
@ -29,6 +34,8 @@ const XkbBinding = struct {
context: *Context,
link: wl.list.Link,
const FocusDirection = enum { next, prev };
fn create(xkb_binding_v1: *river.XkbBindingV1, command: Command, context: *Context) !*XkbBinding {
var xkb_binding = try utils.allocator.create(XkbBinding);
errdefer xkb_binding.destroy();
@ -67,55 +74,36 @@ const XkbBinding = struct {
const context = xkb_binding.context;
switch (xkb_binding.command) {
.spawn => |cmd| {
var child = std.process.Child.init(cmd, std.heap.c_allocator);
var child = std.process.Child.init(cmd, utils.allocator);
_ = child.spawn() catch |err| {
log.err("Failed to spawn \"{s}\": {}", .{ cmd[0], err });
};
},
.focus_next => {
const seat = context.wm.seats.first() orelse return;
const pending_focus = if (seat.focused) |current|
context.wm.getNextWindow(current)
else
// No window focused, focus the first one
context.wm.windows.first();
if (pending_focus) |window| {
seat.pending_manage.pending_focus = .{ .window = window };
seat.pending_manage.should_warp_pointer = true;
} else {
seat.pending_manage.pending_focus = .clear_focus;
}
// context.wm.window_manager_v1.manageDirty();
},
.focus_prev => {
const seat = context.wm.seats.first() orelse return;
const pending_focus = if (seat.focused) |current|
context.wm.getPrevWindow(current)
else
// No window focused, focus the last one
context.wm.windows.last();
if (pending_focus) |window| {
seat.pending_manage.pending_focus = .{ .window = window };
seat.pending_manage.should_warp_pointer = true;
} else {
seat.pending_manage.pending_focus = .clear_focus;
}
},
.focus_next_window => focusWindow(context, .next),
.focus_prev_window => focusWindow(context, .prev),
.focus_next_output => focusOutput(context, .next),
.focus_prev_output => focusOutput(context, .prev),
.send_to_next_output => sendWindowToOutput(context, .next),
.send_to_prev_output => sendWindowToOutput(context, .prev),
.zoom => {
const wm = context.wm;
const seat = wm.seats.first() orelse return;
const current_focus = if (seat.pending_manage.pending_focus) |pending_focus| blk: {
const current_focus = if (seat.pending_manage.window) |pending_focus| blk: {
switch (pending_focus) {
.clear_focus => return,
.window => |window| break :blk window,
}
} else seat.focused orelse return;
const first_window: *Window = if (wm.windows.first()) |first| blk: {
} else seat.focused_window orelse return;
const output = current_focus.output orelse return;
const first_window: *Window = if (output.windows.first()) |first| blk: {
if (current_focus == first) {
// Try get the second window instead
break :blk @fieldParentPtr("link", first.link.next orelse return);
const next = first.link.next orelse return;
// next is the sentinel; there's only one window
if (next == &output.windows.link) {
return;
}
break :blk @fieldParentPtr("link", next);
} else {
seat.pending_manage.should_warp_pointer = true;
break :blk first;
@ -127,7 +115,9 @@ const XkbBinding = struct {
current_focus.link.swapWith(&first_window.link);
},
.change_ratio => |diff| {
context.wm.pending_manage.primary_ratio = std.math.clamp(context.wm.primary_ratio + diff, 0.10, 0.90);
const seat = context.wm.seats.first() orelse return;
const output = seat.focused_output orelse return;
output.pending_manage.primary_ratio = output.primary_ratio + diff;
},
.reload_config => {
// Try create new config
@ -137,25 +127,30 @@ const XkbBinding = struct {
log.err("Failed to reload Config. Not deleting old one", .{});
return;
};
if (context.pending_manage.config) |old_pending| {
// Need to prevent memory leaks in case multiple reloads are sent before a manage
old_pending.destroy();
}
// Send the config to the WM to handle during next manage
context.pending_manage.config = new_config;
context.wm.river_window_manager_v1.manageDirty();
},
.toggle_fullscreen => {
const seat = context.wm.seats.first() orelse return;
const window = seat.focused orelse return;
const window = seat.focused_window orelse return;
window.pending_manage.fullscreen = !window.fullscreen;
// context.wm.window_manager_v1.manageDirty();
context.wm.river_window_manager_v1.manageDirty();
},
.close_window => {
const seat = context.wm.seats.first() orelse return;
if (seat.focused) |window| {
if (seat.focused_window) |window| {
window.river_window_v1.close();
}
},
.set_output_tags => |tags| {
// TODO: Support multiple outputs
const output = context.wm.outputs.first() orelse return;
// TODO: Support multiple seats
const seat = context.wm.seats.first() orelse return;
const output = seat.focused_output orelse return;
output.pending_manage.tags = tags;
context.wm.river_window_manager_v1.manageDirty();
},
@ -163,12 +158,14 @@ const XkbBinding = struct {
const seat = context.wm.seats.first() orelse return;
// TODO: I don't think pending_focus should ever be set at this point?
// const window = seat.pending_manage.pending_focus orelse seat.focused;
const window = seat.focused orelse return;
const window = seat.focused_window orelse return;
window.pending_manage.tags = tags;
context.wm.river_window_manager_v1.manageDirty();
},
.toggle_output_tags => |tags| {
const output = context.wm.outputs.first() orelse return;
// TODO: Support multiple seats
const seat = context.wm.seats.first() orelse return;
const output = seat.focused_output orelse return;
const old_tags = output.pending_manage.tags orelse output.tags;
const new_tags = old_tags ^ tags;
if (new_tags != 0) {
@ -180,7 +177,7 @@ const XkbBinding = struct {
const seat = context.wm.seats.first() orelse return;
// TODO: I don't think pending_focus should ever be set at this point?
// const window = seat.pending_manage.pending_focus orelse seat.focused;
const window = seat.focused orelse return;
const window = seat.focused_window orelse return;
const old_tags = window.pending_manage.tags orelse window.tags;
const new_tags = old_tags ^ tags;
if (new_tags != 0) {
@ -188,9 +185,96 @@ const XkbBinding = struct {
context.wm.river_window_manager_v1.manageDirty();
}
},
// .spawn_tagmask, .focus_previous_tags, .send_to_previous_tags => {
// @panic("Unimplemented");
// },
}
}
fn focusWindow(context: *Context, direction: FocusDirection) void {
const seat = context.wm.seats.first() orelse return;
const output = seat.focused_output orelse return;
const pending_focus = if (seat.focused_window) |current| blk: {
assert(current.output == output);
break :blk switch (direction) {
.next => output.nextWindow(current),
.prev => output.prevWindow(current),
};
} else switch (direction) {
.next => output.windows.first(),
.prev => output.windows.last(),
};
if (pending_focus) |window| {
seat.pending_manage.window = .{ .window = window };
seat.pending_manage.should_warp_pointer = true;
} else {
seat.pending_manage.window = .clear_focus;
}
}
fn focusOutput(context: *Context, direction: FocusDirection) void {
const wm = context.wm;
const seat = wm.seats.first() orelse return;
if (seat.focused_window) |window| {
assert(window.output == seat.focused_output);
}
const pending_focus = if (seat.focused_output) |current|
switch (direction) {
.next => wm.nextOutput(current),
.prev => wm.prevOutput(current),
}
else switch (direction) {
.next => wm.outputs.first(),
.prev => wm.outputs.last(),
};
if (pending_focus) |output| {
seat.pending_manage.output = .{ .output = output };
// We got the new output, but we need to switch window focus, too
// First tell the old one
if (seat.focused_window) |current_focus| {
current_focus.pending_render.focused = false;
}
// Then set the new one
if (output.windows.first()) |window| {
seat.pending_manage.window = .{ .window = window };
// Pointer won't warp if window is empty
seat.pending_manage.should_warp_pointer = true;
} else {
// Clear old focus
seat.pending_manage.window = .clear_focus;
}
} else {
seat.pending_manage.output = .clear_focus;
}
}
// TODO - CONFIG: Allow configuring whether focus follows the window
// TODO - CONFIG: Allow configuring whether window is prepended or appended
// TODO - CONFIG: Allow taking new output's tags
fn sendWindowToOutput(context: *Context, direction: FocusDirection) void {
const wm = context.wm;
const seat = wm.seats.first() orelse return;
const window = seat.focused_window orelse return;
assert(window.output == seat.focused_output);
const pending_output = if (seat.focused_output) |current|
switch (direction) {
.next => wm.nextOutput(current),
.prev => wm.prevOutput(current),
}
else switch (direction) {
.next => wm.outputs.first(),
.prev => wm.outputs.last(),
};
if (pending_output) |output| {
// We have to remove window from current output's windows list first
window.link.remove();
output.windows.append(window);
seat.pending_manage.output = .{ .output = output };
seat.pending_manage.should_warp_pointer = true;
window.pending_manage.pending_output = .{ .output = output };
}
}
};