beansprout-custom/src/WindowManager.zig
Ben Buhse bb612c273e
Add REUSE licensing for non-code files
CC-BY-4.0 for documentation, CC0-1.0 for examples and .gitignore,
HPND for wlr-layer-shell protocol.

Also switch to GPL-3.0-only
2026-02-11 14:50:04 -06:00

259 lines
9.3 KiB
Zig

// SPDX-FileCopyrightText: 2025 Ben Buhse <me@benbuhse.email>
//
// SPDX-License-Identifier: GPL-3.0-only
const WindowManager = @This();
const MIN_RIVER_SEAT_V1_VERSION: u2 = 3;
context: *Context,
river_window_manager_v1: *river.WindowManagerV1,
seats: wl.list.Head(Seat, .link),
outputs: wl.list.Head(Output, .link),
/// 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);
errdefer wm.destroy();
wm.* = .{
.context = context,
.river_window_manager_v1 = window_manager_v1,
.seats = undefined, // we will initialize these shortly
.outputs = undefined,
.orphan_windows = undefined,
};
wm.seats.init();
wm.outputs.init();
wm.orphan_windows.init();
wm.river_window_manager_v1.setListener(*WindowManager, windowManagerV1Listener, wm);
return wm;
}
pub fn destroy(wm: *WindowManager) void {
{
var it = wm.outputs.safeIterator(.forward);
while (it.next()) |output| {
output.link.remove();
output.destroy();
}
}
{
var it = wm.seats.safeIterator(.forward);
while (it.next()) |seat| {
seat.link.remove();
seat.destroy();
}
}
utils.allocator.destroy(wm);
}
/// 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 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 {
const river_window_manager_v1 = wm.river_window_manager_v1;
const context = wm.context;
// This gets used shortly, so it goes at the beginning
context.manage();
if (!context.initialized) {
// This code runs during initial startup and after config reloads.
@branchHint(.cold);
context.initialized = true;
const seat = wm.seats.first() orelse @panic("Failed to get seat");
const river_seat_v1 = seat.river_seat_v1;
// Tag bindings
for (context.config.tag_binds.items) |tag_bind| {
comptime var i: u8 = 1;
comptime var buffer: [1]u8 = undefined;
inline while (i <= 9) : (i += 1) {
const tags: u32 = 1 << (i - 1);
buffer[0] = i + '0';
const command: XkbBindings.Command = switch (tag_bind.command) {
.set_output_tags => .{ .set_output_tags = tags },
.set_window_tags => .{ .set_window_tags = tags },
.toggle_output_tags => .{ .toggle_output_tags = tags },
.toggle_window_tags => .{ .toggle_window_tags = tags },
else => unreachable,
};
context.xkb_bindings.addBinding(river_seat_v1, @field(xkbcommon.Keysym, &buffer), tag_bind.modifiers, command);
}
}
// Rest of the keybinds
for (context.config.keybinds.items) |keybind| {
// Keysyms should only be null in tag_binds (above)
std.debug.assert(keybind.keysym != null);
context.xkb_bindings.addBinding(river_seat_v1, keybind.keysym.?, keybind.modifiers, keybind.command);
}
// Pointer bindings
for (context.config.pointer_binds.items) |pointer_bind| {
const binding = river_seat_v1.getPointerBinding(pointer_bind.button, pointer_bind.modifiers) catch {
log.err("Failed to create pointer binding", .{});
continue;
};
switch (pointer_bind.action) {
.move_window => {
if (seat.move_pointer_binding) |old| old.destroy();
binding.setListener(*Seat, Seat.movePointerBindingListener, seat);
seat.move_pointer_binding = binding;
},
.resize_window => {
if (seat.resize_pointer_binding) |old| old.destroy();
binding.setListener(*Seat, Seat.resizePointerBindingListener, seat);
seat.resize_pointer_binding = binding;
},
}
binding.enable();
}
}
{
var it = wm.outputs.iterator(.forward);
while (it.next()) |output| {
output.manage();
}
}
{
var it = wm.seats.iterator(.forward);
while (it.next()) |seat| {
seat.manage();
}
}
river_window_manager_v1.manageFinish();
}
fn render_start(wm: *WindowManager) void {
const river_window_manager_v1 = wm.river_window_manager_v1;
{
var it = wm.seats.iterator(.forward);
while (it.next()) |seat| {
seat.render();
}
}
{
var it = wm.outputs.iterator(.forward);
while (it.next()) |output| {
output.render();
}
}
river_window_manager_v1.renderFinish();
}
fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: river.WindowManagerV1.Event, wm: *WindowManager) void {
assert(wm.river_window_manager_v1 == window_manager_v1);
const context = wm.context;
switch (event) {
.unavailable => {
fatal("Window manager unavailable (some other wm instance is running). Exiting", .{});
},
.manage_start => wm.manage_start(),
.render_start => wm.render_start(),
.output => |ev| {
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 first_seat = wm.seats.first();
if (first_seat) |seat| {
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;
}
// We clear any orphaned_windows if an output is added
output.windows.appendList(&wm.orphan_windows);
},
.seat => |ev| {
// TODO: Support multi-seat (maybe ?)
const river_seat_v1 = ev.id;
const river_seat_v1_version = river_seat_v1.getVersion();
if (river_seat_v1_version < MIN_RIVER_SEAT_V1_VERSION) {
@branchHint(.cold); // If we're in here, the program is exiting anyways
utils.versionNotSupported(river.SeatV1, river_seat_v1_version, MIN_RIVER_SEAT_V1_VERSION);
}
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
if (wm.outputs.first()) |output| {
seat.pending_manage.output = .{ .output = output };
}
},
.window => |ev| {
// TODO: Support multiple seats
const seat = wm.seats.first() orelse @panic("Failed to get seat");
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 => window_list.prepend(window),
.bottom => window_list.append(window),
}
seat.pending_manage.window = .{ .window = window };
seat.pending_manage.should_warp_pointer = true;
},
else => |ev| {
log.debug("unhandled event: {s}", .{@tagName(ev)});
},
}
}
const std = @import("std");
const assert = std.debug.assert;
const fatal = std.process.fatal;
const wayland = @import("wayland");
const wl = wayland.client.wl;
const river = wayland.client.river;
const xkbcommon = @import("xkbcommon");
const utils = @import("utils.zig");
const Context = @import("Context.zig");
const Output = @import("Output.zig");
const Seat = @import("Seat.zig");
const Window = @import("Window.zig");
const XkbBindings = @import("XkbBindings.zig");
const log = std.log.scoped(.WindowManager);