Merge pull request 'Add multi-output support' (#7) from multi-output-support into main

Reviewed-on: https://codeberg.org/bwbuhse/beansprout/pulls/7
This commit is contained in:
Ben Buhse 2026-02-04 03:12:41 +01:00
commit 26949d7b8a
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 };
}
}
};