From 137eac9364a4fadc071155ce65d9bfe2301dbd73 Mon Sep 17 00:00:00 2001 From: Ben Buhse Date: Sat, 24 Jan 2026 18:23:13 -0600 Subject: [PATCH 1/4] Update comments; Check river.SeatV1 version; Prepend new windows --- src/Window.zig | 4 ++-- src/WindowManager.zig | 24 +++++++++++++++++++----- src/XkbBindings.zig | 2 +- src/main.zig | 19 ++++--------------- src/utils.zig | 14 ++++++++++++++ 5 files changed, 40 insertions(+), 23 deletions(-) diff --git a/src/Window.zig b/src/Window.zig index 2c4ce3a..bd58c28 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -84,12 +84,12 @@ fn windowListener(river_window_v1: *river.WindowV1, event: river.WindowV1.Event, } pub fn manage(window: *Window) void { - // Use server-side decoration (no client-drawn title bars) - // TODO: Probably shouldn't send this for every manage(?) if (!window.initialized) { @branchHint(.unlikely); window.initialized = true; + // TODO: We might want to think about paying attention to the decoration_hint event + // If we do, this would need to move, I think? window.window_v1.useSsd(); window.window_v1.setCapabilities(.{ .fullscreen = true, .maximize = true }); diff --git a/src/WindowManager.zig b/src/WindowManager.zig index fe7b526..dc80c00 100644 --- a/src/WindowManager.zig +++ b/src/WindowManager.zig @@ -6,6 +6,8 @@ const WindowManager = @This(); const PRIMARY_RATIO: f32 = 0.55; +const MIN_RIVER_SEAT_V1_VERSION: u2 = 3; + context: *Context, window_manager_v1: *river.WindowManagerV1, @@ -38,6 +40,7 @@ pub fn create(context: *Context, window_manager_v1: *river.WindowManagerV1) !*Wi } pub fn destroy(wm: *WindowManager) void { + // TODO: Go through lists and destroy everything utils.allocator.destroy(wm); } @@ -200,22 +203,33 @@ fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: riv window_manager_v1.renderFinish(); }, .output => |ev| { - log.debug("3", .{}); + // TODO: Support multi-output var output = utils.allocator.create(Output) catch @panic("out-of-memory; exiting."); output.init(context, ev.id); wm.outputs.append(output); }, .seat => |ev| { - log.debug("4", .{}); + // 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); + } + var seat = utils.allocator.create(Seat) catch @panic("out-of-memory; exiting."); - seat.init(context, ev.id); + seat.init(context, river_seat_v1); wm.seats.append(seat); }, .window => |ev| { - log.debug("5", .{}); var window = utils.allocator.create(Window) catch @panic("out-of-memory; exiting."); window.init(context, ev.id); - wm.windows.append(window); + + // TODO: Allow appending window instead of prepending + wm.windows.prepend(window); + const seat = wm.seats.first() orelse @panic("Failed to get seat"); + seat.focused = window; + wm.window_count += 1; log.debug("window_count = {d}", .{wm.window_count}); }, diff --git a/src/XkbBindings.zig b/src/XkbBindings.zig index f7992d7..e6e29ff 100644 --- a/src/XkbBindings.zig +++ b/src/XkbBindings.zig @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2025-2026 Ben Buhse +// SPDX-FileCopyrightText: 2026 Ben Buhse // // SPDX-License-Identifier: GPL-3.0-or-later diff --git a/src/main.zig b/src/main.zig index af3b460..540fc2f 100644 --- a/src/main.zig +++ b/src/main.zig @@ -33,9 +33,9 @@ pub fn main() !void { std.posix.exit(1); } - const wl_compositor = globals.wl_compositor orelse interfaceNotAdvertised(wl.Compositor); - const river_window_manager_v1 = globals.river_window_manager_v1 orelse interfaceNotAdvertised(river.WindowManagerV1); - const river_xkb_bindings_v1 = globals.river_xkb_bindings_v1 orelse interfaceNotAdvertised(river.XkbBindingsV1); + const wl_compositor = globals.wl_compositor orelse utils.interfaceNotAdvertised(wl.Compositor); + const river_window_manager_v1 = globals.river_window_manager_v1 orelse utils.interfaceNotAdvertised(river.WindowManagerV1); + const river_xkb_bindings_v1 = globals.river_xkb_bindings_v1 orelse utils.interfaceNotAdvertised(river.XkbBindingsV1); const context = try Context.create( wl_display, @@ -62,7 +62,7 @@ fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, globals: * switch (event) { .global => |ev| { if (mem.orderZ(u8, ev.interface, wl.Compositor.interface.name) == .eq) { - if (ev.version < 4) versionNotSupported(wl.Compositor, ev.version, 4); + if (ev.version < 4) utils.versionNotSupported(wl.Compositor, ev.version, 4); globals.wl_compositor = registry.bind(ev.name, wl.Compositor, 4) catch |e| { log.err("Failed to bind to compositor: {any}", .{@errorName(e)}); std.posix.exit(1); @@ -84,17 +84,6 @@ fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, globals: * } } -// TODO: Actually use this... -fn interfaceNotAdvertised(comptime WaylandGlobal: type) noreturn { - log.err("{s} not advertised. Exiting", .{WaylandGlobal.interface.name}); - std.posix.exit(1); -} - -fn versionNotSupported(comptime WaylandGlobal: type, have_version: u32, need_version: u32) noreturn { - log.err("The compositor only advertised {s} version {d} but version {d} is required. Exiting", .{ WaylandGlobal.interface.name, have_version, need_version }); - std.posix.exit(1); -} - const std = @import("std"); const mem = std.mem; const process = std.process; diff --git a/src/utils.zig b/src/utils.zig index 48f1d1e..cba5ff2 100644 --- a/src/utils.zig +++ b/src/utils.zig @@ -65,5 +65,19 @@ fn parseRgbaHelper(bytes: [4]u8) RiverColor { }; } +/// Report that the given WaylandGlobal wasn't advertised and exit the program +pub fn interfaceNotAdvertised(comptime WaylandGlobal: type) noreturn { + log.err("{s} not advertised. Exiting", .{WaylandGlobal.interface.name}); + std.posix.exit(1); +} + +/// Report that the given WaylandGlobal was advertised but the support version was too low exit the program +pub fn versionNotSupported(comptime WaylandGlobal: type, have_version: u32, need_version: u32) noreturn { + log.err("The compositor only advertised {s} version {d} but version {d} is required. Exiting", .{ WaylandGlobal.interface.name, have_version, need_version }); + std.posix.exit(1); +} + const std = @import("std"); const fmt = std.fmt; + +const log = std.log.scoped(.utils); From bfa41f36b070849c5f7a9ff7bc481c7fc45ef9f3 Mon Sep 17 00:00:00 2001 From: Ben Buhse Date: Sun, 25 Jan 2026 12:10:00 -0600 Subject: [PATCH 2/4] Add PendingState to Seat --- src/Seat.zig | 49 +++++++++++++++++++++++++++++++++++++++---- src/WindowManager.zig | 10 ++++----- src/XkbBindings.zig | 44 ++++++++++++++++++++++++++++---------- 3 files changed, 83 insertions(+), 20 deletions(-) diff --git a/src/Seat.zig b/src/Seat.zig index 15e90f7..b135ccb 100644 --- a/src/Seat.zig +++ b/src/Seat.zig @@ -10,8 +10,23 @@ seat_v1: *river.SeatV1, focused: ?*Window, +/// Used to manage updates to various aspect of Seat's state +/// Gets reset at the end of every Seat.manage() call. +pending_state: PendingState = .{}, + link: wl.list.Link, +/// Used to manage updates to various aspect of Seat's state +pub const PendingState = struct { + pending_focus: ?PendingFocus = null, + should_warp_pointer: bool = false, + + pub const PendingFocus = union(enum) { + window: *Window, + clear_focus, + }; +}; + pub fn init(seat: *Seat, context: *Context, river_seat_v1: *river.SeatV1) void { seat.* = .{ .context = context, @@ -36,12 +51,12 @@ fn seatListener(river_seat_v1: *river.SeatV1, event: river.SeatV1.Event, seat: * .pointer_enter => |ev| { const window_v1 = ev.window orelse return; const window: *Window = @ptrCast(@alignCast(window_v1.getUserData())); - seat.focused = window; + seat.pending_state.pending_focus = .{ .window = window }; }, .window_interaction => |ev| { const window_v1 = ev.window orelse return; const window: *Window = @ptrCast(@alignCast(window_v1.getUserData())); - seat.focused = window; + seat.pending_state.pending_focus = .{ .window = window }; }, else => |ev| { log.debug("unhandled event: {s}", .{@tagName(ev)}); @@ -50,8 +65,34 @@ fn seatListener(river_seat_v1: *river.SeatV1, event: river.SeatV1.Event, seat: * } pub fn manage(seat: *Seat) void { - if (seat.focused) |window| { - seat.seat_v1.focusWindow(window.window_v1); + defer seat.pending_state = .{}; + + log.debug("Managing Seat, pending state={any}", .{seat.pending_state}); + + if (seat.pending_state.pending_focus) |pending_focus| { + switch (pending_focus) { + .window => |window| { + seat.focused = window; + seat.seat_v1.focusWindow(window.window_v1); + }, + .clear_focus => { + seat.focused = null; + seat.seat_v1.clearFocus(); + }, + } + } + + if (seat.pending_state.should_warp_pointer) blk: { + const window = seat.focused orelse { + log.err("Trying to warp pointer without focused window.", .{}); + break :blk; + }; + // TODO - CONFIG: Allow disabling this behaviour + // Warp pointer to center of newly-focused windows + const pointer_x: i32 = @divTrunc(window.x + window.width, 2); + const pointer_y: i32 = @divTrunc(window.y + window.height, 2); + seat.seat_v1.pointerWarp(pointer_x, pointer_y); + log.debug("warped pointer to {}, {}", .{ pointer_x, pointer_y }); } } diff --git a/src/WindowManager.zig b/src/WindowManager.zig index dc80c00..7440dce 100644 --- a/src/WindowManager.zig +++ b/src/WindowManager.zig @@ -69,7 +69,7 @@ pub fn getPrevWindow(wm: *WindowManager, current: *Window) ?*Window { } /// Calculate primary/stack layout positions for all windows. -/// - Single window: fullscreen +/// - Single window: maximized /// - Multiple windows: stack (45% left, vertically tiled), primary (55% right) fn calculatePrimaryStackLayout(wm: *WindowManager) void { const count = wm.window_count; @@ -149,7 +149,7 @@ fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: riv @branchHint(.cold); context.initialized = true; - const seat = wm.seats.first() orelse @panic("No river_seat_v1 found"); + const seat = wm.seats.first() orelse @panic("Failed to get seat"); const river_seat_v1 = seat.seat_v1; context.xkb_bindings.addBinding(river_seat_v1, xkbcommon.Keysym.t, .{ .mod4 = true }, .{ .spawn = &.{"foot"} }); context.xkb_bindings.addBinding(river_seat_v1, xkbcommon.Keysym.j, .{ .mod4 = true }, .focus_next); @@ -225,13 +225,13 @@ fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: riv var window = utils.allocator.create(Window) catch @panic("out-of-memory; exiting."); window.init(context, ev.id); - // TODO: Allow appending window instead of prepending + // TODO - CONFIG: Allow appending window instead of prepending wm.windows.prepend(window); const seat = wm.seats.first() orelse @panic("Failed to get seat"); - seat.focused = window; + seat.pending_state.pending_focus = .{ .window = window }; + seat.pending_state.should_warp_pointer = true; wm.window_count += 1; - log.debug("window_count = {d}", .{wm.window_count}); }, else => |ev| { log.debug("unhandled event: {s}", .{@tagName(ev)}); diff --git a/src/XkbBindings.zig b/src/XkbBindings.zig index e6e29ff..4be1408 100644 --- a/src/XkbBindings.zig +++ b/src/XkbBindings.zig @@ -49,38 +49,60 @@ const XkbBinding = struct { .spawn => |cmd| { var child = std.process.Child.init(cmd, std.heap.c_allocator); _ = child.spawn() catch |err| { - log.err("Failed to spawn foot: {}", .{err}); + log.err("Failed to spawn \"{s}\": {}", .{ cmd, err }); }; }, .focus_next => { const seat = context.wm.seats.first() orelse return; - if (seat.focused) |current| { - seat.focused = context.wm.getNextWindow(current); - } else { + const pending_focus = if (seat.focused) |current| + context.wm.getNextWindow(current) + else // No window focused, focus the first one - seat.focused = context.wm.windows.first(); + context.wm.windows.first(); + + if (pending_focus) |window| { + seat.pending_state.pending_focus = .{ .window = window }; + seat.pending_state.should_warp_pointer = true; + } else { + seat.pending_state.pending_focus = .clear_focus; } - context.wm.window_manager_v1.manageDirty(); + // context.wm.window_manager_v1.manageDirty(); }, .focus_prev => { const seat = context.wm.seats.first() orelse return; - if (seat.focused) |current| { - seat.focused = context.wm.getPrevWindow(current); - } else { + const pending_focus = if (seat.focused) |current| + context.wm.getPrevWindow(current) + else // No window focused, focus the last one - seat.focused = context.wm.windows.last(); + context.wm.windows.last(); + + if (pending_focus) |window| { + seat.pending_state.pending_focus = .{ .window = window }; + seat.pending_state.should_warp_pointer = true; + } else { + seat.pending_state.pending_focus = .clear_focus; } - context.wm.window_manager_v1.manageDirty(); + // context.wm.window_manager_v1.manageDirty(); }, .fullscreen => { const seat = context.wm.seats.first() orelse return; const window = seat.focused orelse return; window.pending_state.fullscreen = !window.fullscreen; + // context.wm.window_manager_v1.manageDirty(); }, .close_window => { const seat = context.wm.seats.first() orelse return; if (seat.focused) |window| { window.window_v1.close(); + + // Move focus to previous window in stack (if one exists) + if (context.wm.getPrevWindow(window)) |pending_focus| { + seat.pending_state.pending_focus = .{ .window = pending_focus }; + seat.pending_state.should_warp_pointer = true; + } else { + seat.pending_state.pending_focus = .clear_focus; + } + // context.wm.window_manager_v1.manageDirty(); } }, .exit => { From c953fe3d68a1bf791b8a4160409ce94dc5689599 Mon Sep 17 00:00:00 2001 From: Ben Buhse Date: Sun, 25 Jan 2026 13:16:49 -0600 Subject: [PATCH 3/4] Fix pointer warp for new and closed windows --- src/Seat.zig | 40 ++++++++++------- src/Window.zig | 100 +++++++++++++++++++++++------------------- src/WindowManager.zig | 58 ++++++++++++------------ src/XkbBindings.zig | 25 ++++------- 4 files changed, 119 insertions(+), 104 deletions(-) diff --git a/src/Seat.zig b/src/Seat.zig index b135ccb..749a01d 100644 --- a/src/Seat.zig +++ b/src/Seat.zig @@ -10,14 +10,12 @@ seat_v1: *river.SeatV1, focused: ?*Window, -/// Used to manage updates to various aspect of Seat's state -/// Gets reset at the end of every Seat.manage() call. -pending_state: PendingState = .{}, +/// State consumed in manage phase, reset at end of manage(). +pending_manage: PendingManage = .{}, link: wl.list.Link, -/// Used to manage updates to various aspect of Seat's state -pub const PendingState = struct { +pub const PendingManage = struct { pending_focus: ?PendingFocus = null, should_warp_pointer: bool = false, @@ -51,12 +49,12 @@ fn seatListener(river_seat_v1: *river.SeatV1, event: river.SeatV1.Event, seat: * .pointer_enter => |ev| { const window_v1 = ev.window orelse return; const window: *Window = @ptrCast(@alignCast(window_v1.getUserData())); - seat.pending_state.pending_focus = .{ .window = window }; + seat.pending_manage.pending_focus = .{ .window = window }; }, .window_interaction => |ev| { const window_v1 = ev.window orelse return; const window: *Window = @ptrCast(@alignCast(window_v1.getUserData())); - seat.pending_state.pending_focus = .{ .window = window }; + seat.pending_manage.pending_focus = .{ .window = window }; }, else => |ev| { log.debug("unhandled event: {s}", .{@tagName(ev)}); @@ -65,32 +63,42 @@ fn seatListener(river_seat_v1: *river.SeatV1, event: river.SeatV1.Event, seat: * } pub fn manage(seat: *Seat) void { - defer seat.pending_state = .{}; + defer seat.pending_manage = .{}; - log.debug("Managing Seat, pending state={any}", .{seat.pending_state}); - - if (seat.pending_state.pending_focus) |pending_focus| { + if (seat.pending_manage.pending_focus) |pending_focus| { switch (pending_focus) { .window => |window| { + if (seat.focused) |focused| { + // Tell the previously focused Window that it's no longer focused + if (focused != window) { + focused.pending_render.focused = false; + } + } seat.focused = window; seat.seat_v1.focusWindow(window.window_v1); + window.pending_render.focused = true; }, .clear_focus => { + if (seat.focused) |focused| { + // Tell the previously focused Window that it's no longer focused + focused.pending_render.focused = false; + } seat.focused = null; seat.seat_v1.clearFocus(); }, } } - if (seat.pending_state.should_warp_pointer) blk: { + if (seat.pending_manage.should_warp_pointer) { const window = seat.focused orelse { log.err("Trying to warp pointer without focused window.", .{}); - break :blk; + return; }; // TODO - CONFIG: Allow disabling this behaviour - // Warp pointer to center of newly-focused windows - const pointer_x: i32 = @divTrunc(window.x + window.width, 2); - const pointer_y: i32 = @divTrunc(window.y + window.height, 2); + // Warp pointer to center of focused window; + // because the x and y coords are set later, we need to check them here. + const pointer_x: i32 = (window.pending_render.x orelse window.x) + @divTrunc(window.width, 2); + const pointer_y: i32 = (window.pending_render.y orelse window.y) + @divTrunc(window.height, 2); seat.seat_v1.pointerWarp(pointer_x, pointer_y); log.debug("warped pointer to {}, {}", .{ pointer_x, pointer_y }); } diff --git a/src/Window.zig b/src/Window.zig index bd58c28..4ea099e 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -19,26 +19,29 @@ maximized: bool = false, initialized: bool = false, -/// Used to manage updates to various aspect of Window's state -/// Gets reset at the end of every Window.manage() call. -/// -/// It might be a good idea to have two separate versions of this so one -/// can be used in renders, but I think it's okay for now. -pending_state: PendingState = .{}, +/// State consumed in manage() phase, reset at end of manage(). +pending_manage: PendingManage = .{}, + +/// State consumed in render() phase, reset at end of render(). +pending_render: PendingRender = .{}, link: wl.list.Link, -/// Used to manage updates to various aspect of Window's state -pub const PendingState = struct { +pub const PendingManage = struct { width: ?u31 = null, height: ?u31 = null, - x: ?i32 = null, - y: ?i32 = null, fullscreen: ?bool = null, maximized: ?bool = null, }; +pub const PendingRender = struct { + x: ?i32 = null, + y: ?i32 = null, + + focused: ?bool = null, +}; + pub fn init(window: *Window, context: *Context, river_window_v1: *river.WindowV1) void { window.* = .{ .context = context, @@ -61,7 +64,18 @@ fn windowListener(river_window_v1: *river.WindowV1, event: river.WindowV1.Event, var it = window.context.wm.seats.iterator(.forward); while (it.next()) |seat| { if (seat.focused == window) { - seat.focused = null; + // Find another window to focus and warp pointer there + if (window.context.wm.getPrevWindow(window)) |next_focus| { + if (next_focus != window) { + seat.pending_manage.pending_focus = .{ .window = next_focus }; + seat.pending_manage.should_warp_pointer = true; + } else { + // Only window in list - clear focus + seat.pending_manage.pending_focus = .clear_focus; + } + } else { + seat.pending_manage.pending_focus = .clear_focus; + } } } } @@ -97,25 +111,19 @@ pub fn manage(window: *Window) void { } // Any new state since the last manage event - defer window.pending_state = .{}; - const pending_state = window.pending_state; + defer window.pending_manage = .{}; + const pending_manage = window.pending_manage; // Layout (pre-computed by WindowManager.calculatePrimaryStackLayout()) - if (pending_state.width) |new_width| { - if (pending_state.height) |new_height| { + if (pending_manage.width) |new_width| { + if (pending_manage.height) |new_height| { window.width = new_width; window.height = new_height; window.window_v1.proposeDimensions(new_width, new_height); } } - if (pending_state.x) |new_x| { - window.x = new_x; - } - if (pending_state.y) |new_y| { - window.y = new_y; - } // Fullscreen and maximize operations - if (pending_state.fullscreen) |fullscreen| { + if (pending_manage.fullscreen) |fullscreen| { window.fullscreen = fullscreen; if (fullscreen) { const output = window.context.wm.outputs.first() orelse @panic("failed to get output"); @@ -126,7 +134,7 @@ pub fn manage(window: *Window) void { window.window_v1.informNotFullscreen(); } } - if (pending_state.maximized) |maximized| { + if (pending_manage.maximized) |maximized| { window.maximized = maximized; if (maximized) { window.window_v1.informMaximized(); @@ -137,34 +145,38 @@ pub fn manage(window: *Window) void { } pub fn render(window: *Window) void { - // Set the window position using the pre-computed layout - window.node_v1.setPosition(window.x, window.y); + defer window.pending_render = .{}; + + // TODO: We probably could just move these back to pending_manage and have PendingRiver.new_coords: bool + // This would also simplify the pointer warp behaviour in Seat.manage() + if (window.pending_render.x) |new_x| { + if (window.pending_render.y) |new_y| { + window.x = new_x; + window.y = new_y; + + window.node_v1.setPosition(window.x, window.y); + } else { + log.err("Window.pending_render with only x set", .{}); + } + } else if (window.pending_render.y) |_| { + log.err("Window.pending_render with only y set", .{}); + } // Set borders if (!window.fullscreen) { - const border_width = window.context.config.border_width; - if (window.isFocused()) { - const border_color_focused = window.context.config.border_color_focused; - window.window_v1.setBorders(.{ .top = true, .bottom = true, .left = true, .right = true }, border_width, border_color_focused.red, border_color_focused.green, border_color_focused.blue, border_color_focused.alpha); - } else { - const border_color_unfocused = window.context.config.border_color_unfocused; - window.window_v1.setBorders(.{ .top = true, .bottom = true, .left = true, .right = true }, border_width, border_color_unfocused.red, border_color_unfocused.green, border_color_unfocused.blue, border_color_unfocused.alpha); + if (window.pending_render.focused) |focused| { + const border_width = window.context.config.border_width; + if (focused) { + const border_color_focused = window.context.config.border_color_focused; + window.window_v1.setBorders(.{ .top = true, .bottom = true, .left = true, .right = true }, border_width, border_color_focused.red, border_color_focused.green, border_color_focused.blue, border_color_focused.alpha); + } else { + const border_color_unfocused = window.context.config.border_color_unfocused; + window.window_v1.setBorders(.{ .top = true, .bottom = true, .left = true, .right = true }, border_width, border_color_unfocused.red, border_color_unfocused.green, border_color_unfocused.blue, border_color_unfocused.alpha); + } } } } -// TODO: Should probably make this better. Shouldn't be too bad since we only have one seat -// but there's no reason to do it like this -fn isFocused(window: *Window) bool { - var it = window.context.wm.seats.iterator(.forward); - while (it.next()) |seat| { - if (seat.focused == window) { - return true; - } - } - return false; -} - const std = @import("std"); const assert = std.debug.assert; diff --git a/src/WindowManager.zig b/src/WindowManager.zig index 7440dce..6d1f06f 100644 --- a/src/WindowManager.zig +++ b/src/WindowManager.zig @@ -87,11 +87,11 @@ fn calculatePrimaryStackLayout(wm: *WindowManager) void { while (it.next()) |window| { if (count == 1) { // Single window: maximize - window.pending_state.x = output_x; - window.pending_state.y = output_y; - window.pending_state.width = output_width; - window.pending_state.height = output_height; - window.pending_state.maximized = true; + 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 @@ -99,25 +99,25 @@ fn calculatePrimaryStackLayout(wm: *WindowManager) void { const stack_width: u31 = output_width - primary_width; const stack_count = count - 1; const stack_height: u31 = @divFloor(output_height, stack_count); - window.pending_state.maximized = false; + window.pending_manage.maximized = false; if (index == 0) { // Primary window (first window) - right side - window.pending_state.x = output_x + @as(i32, stack_width); - window.pending_state.y = output_y; - window.pending_state.width = primary_width; - window.pending_state.height = output_height; + 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 = index - 1; - window.pending_state.x = output_x; - window.pending_state.y = output_y + @as(i32, stack_index) * @as(i32, stack_height); - window.pending_state.width = stack_width; + 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 (index == count - 1) { - window.pending_state.height = output_height - stack_index * stack_height; + window.pending_manage.height = output_height - stack_index * stack_height; } else { - window.pending_state.height = stack_height; + window.pending_manage.height = stack_height; } } } @@ -125,10 +125,10 @@ fn calculatePrimaryStackLayout(wm: *WindowManager) void { // 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; - window.pending_state.height.? -= 2 * border_width; - window.pending_state.width.? -= 2 * border_width; - window.pending_state.x.? += border_width; - window.pending_state.y.? += border_width; + 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; index += 1; } @@ -143,6 +143,7 @@ fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: riv std.posix.exit(1); }, .manage_start => { + log.debug("manage start", .{}); if (!context.initialized) { // This code (should) only be run once while initializing the WM, so let's // mark it as cold. @@ -159,12 +160,6 @@ fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: riv context.xkb_bindings.addBinding(river_seat_v1, xkbcommon.Keysym.e, .{ .mod4 = true, .shift = true }, .exit); } - { - var it = wm.seats.iterator(.forward); - while (it.next()) |seat| { - seat.manage(); - } - } { var it = wm.outputs.iterator(.forward); while (it.next()) |output| { @@ -179,9 +174,17 @@ fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: riv window.manage(); } } + { + var it = wm.seats.iterator(.forward); + while (it.next()) |seat| { + seat.manage(); + } + } window_manager_v1.manageFinish(); + log.debug("manage end===================", .{}); }, .render_start => { + log.debug("render start", .{}); { var it = wm.seats.iterator(.forward); while (it.next()) |seat| { @@ -201,6 +204,7 @@ fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: riv } } window_manager_v1.renderFinish(); + log.debug("render end==================", .{}); }, .output => |ev| { // TODO: Support multi-output @@ -228,8 +232,8 @@ fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: riv // TODO - CONFIG: Allow appending window instead of prepending wm.windows.prepend(window); const seat = wm.seats.first() orelse @panic("Failed to get seat"); - seat.pending_state.pending_focus = .{ .window = window }; - seat.pending_state.should_warp_pointer = true; + seat.pending_manage.pending_focus = .{ .window = window }; + seat.pending_manage.should_warp_pointer = true; wm.window_count += 1; }, diff --git a/src/XkbBindings.zig b/src/XkbBindings.zig index 4be1408..2f9ea5f 100644 --- a/src/XkbBindings.zig +++ b/src/XkbBindings.zig @@ -49,7 +49,7 @@ const XkbBinding = struct { .spawn => |cmd| { var child = std.process.Child.init(cmd, std.heap.c_allocator); _ = child.spawn() catch |err| { - log.err("Failed to spawn \"{s}\": {}", .{ cmd, err }); + log.err("Failed to spawn \"{s}\": {}", .{ cmd[0], err }); }; }, .focus_next => { @@ -61,10 +61,10 @@ const XkbBinding = struct { context.wm.windows.first(); if (pending_focus) |window| { - seat.pending_state.pending_focus = .{ .window = window }; - seat.pending_state.should_warp_pointer = true; + seat.pending_manage.pending_focus = .{ .window = window }; + seat.pending_manage.should_warp_pointer = true; } else { - seat.pending_state.pending_focus = .clear_focus; + seat.pending_manage.pending_focus = .clear_focus; } // context.wm.window_manager_v1.manageDirty(); }, @@ -77,32 +77,23 @@ const XkbBinding = struct { context.wm.windows.last(); if (pending_focus) |window| { - seat.pending_state.pending_focus = .{ .window = window }; - seat.pending_state.should_warp_pointer = true; + seat.pending_manage.pending_focus = .{ .window = window }; + seat.pending_manage.should_warp_pointer = true; } else { - seat.pending_state.pending_focus = .clear_focus; + seat.pending_manage.pending_focus = .clear_focus; } // context.wm.window_manager_v1.manageDirty(); }, .fullscreen => { const seat = context.wm.seats.first() orelse return; const window = seat.focused orelse return; - window.pending_state.fullscreen = !window.fullscreen; + window.pending_manage.fullscreen = !window.fullscreen; // context.wm.window_manager_v1.manageDirty(); }, .close_window => { const seat = context.wm.seats.first() orelse return; if (seat.focused) |window| { window.window_v1.close(); - - // Move focus to previous window in stack (if one exists) - if (context.wm.getPrevWindow(window)) |pending_focus| { - seat.pending_state.pending_focus = .{ .window = pending_focus }; - seat.pending_state.should_warp_pointer = true; - } else { - seat.pending_state.pending_focus = .clear_focus; - } - // context.wm.window_manager_v1.manageDirty(); } }, .exit => { From e1a0b89cedb99ac7d38ff72a0e165ad704011329 Mon Sep 17 00:00:00 2001 From: Ben Buhse Date: Sun, 25 Jan 2026 20:57:05 -0600 Subject: [PATCH 4/4] Standardize panic messages and clean up code --- src/Seat.zig | 18 ++++++++---------- src/Window.zig | 17 ++++++++++------- src/WindowManager.zig | 29 +++++++++++++++++++++++++---- src/XkbBindings.zig | 10 ++++++++-- src/main.zig | 2 +- 5 files changed, 52 insertions(+), 24 deletions(-) diff --git a/src/Seat.zig b/src/Seat.zig index 749a01d..5abd1c6 100644 --- a/src/Seat.zig +++ b/src/Seat.zig @@ -46,22 +46,20 @@ fn seatListener(river_seat_v1: *river.SeatV1, event: river.SeatV1.Event, seat: * .wl_seat => { // log.debug("initializing new river_seat_v1 corresponding to wl_seat: {d}", .{ev.name}); }, - .pointer_enter => |ev| { - const window_v1 = ev.window orelse return; - const window: *Window = @ptrCast(@alignCast(window_v1.getUserData())); - seat.pending_manage.pending_focus = .{ .window = window }; - }, - .window_interaction => |ev| { - const window_v1 = ev.window orelse return; - const window: *Window = @ptrCast(@alignCast(window_v1.getUserData())); - seat.pending_manage.pending_focus = .{ .window = window }; - }, + .pointer_enter => |ev| seat.setWindowFocus(ev.window), + .window_interaction => |ev| seat.setWindowFocus(ev.window), else => |ev| { log.debug("unhandled event: {s}", .{@tagName(ev)}); }, } } +fn setWindowFocus(seat: *Seat, window_v1: ?*river.WindowV1) void { + const wv1 = window_v1 orelse return; + const window: *Window = @ptrCast(@alignCast(wv1.getUserData())); + seat.pending_manage.pending_focus = .{ .window = window }; +} + pub fn manage(seat: *Seat) void { defer seat.pending_manage = .{}; diff --git a/src/Window.zig b/src/Window.zig index 4ea099e..058a5a0 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -46,7 +46,7 @@ pub fn init(window: *Window, context: *Context, river_window_v1: *river.WindowV1 window.* = .{ .context = context, .window_v1 = river_window_v1, - .node_v1 = river_window_v1.getNode() catch @panic("failed to get node"), + .node_v1 = river_window_v1.getNode() catch @panic("Failed to get node"), .link = undefined, // Handled by the wl.list }; @@ -126,7 +126,7 @@ pub fn manage(window: *Window) void { if (pending_manage.fullscreen) |fullscreen| { window.fullscreen = fullscreen; if (fullscreen) { - const output = window.context.wm.outputs.first() orelse @panic("failed to get output"); + const output = window.context.wm.outputs.first() orelse @panic("Failed to get output"); window.window_v1.fullscreen(output.output_v1); window.window_v1.informFullscreen(); } else { @@ -165,18 +165,21 @@ pub fn render(window: *Window) void { // Set borders if (!window.fullscreen) { if (window.pending_render.focused) |focused| { - const border_width = window.context.config.border_width; if (focused) { - const border_color_focused = window.context.config.border_color_focused; - window.window_v1.setBorders(.{ .top = true, .bottom = true, .left = true, .right = true }, border_width, border_color_focused.red, border_color_focused.green, border_color_focused.blue, border_color_focused.alpha); + window.applyBorders(window.context.config.border_color_focused); } else { - const border_color_unfocused = window.context.config.border_color_unfocused; - window.window_v1.setBorders(.{ .top = true, .bottom = true, .left = true, .right = true }, border_width, border_color_unfocused.red, border_color_unfocused.green, border_color_unfocused.blue, border_color_unfocused.alpha); + window.applyBorders(window.context.config.border_color_unfocused); } } } } +fn applyBorders(window: *Window, color: utils.RiverColor) void { + const border_width = window.context.config.border_width; + const all_sides = river.WindowV1.Edges{ .top = true, .bottom = true, .left = true, .right = true }; + window.window_v1.setBorders(all_sides, border_width, color.red, color.green, color.blue, color.alpha); +} + const std = @import("std"); const assert = std.debug.assert; diff --git a/src/WindowManager.zig b/src/WindowManager.zig index 6d1f06f..0c8cc0e 100644 --- a/src/WindowManager.zig +++ b/src/WindowManager.zig @@ -40,7 +40,28 @@ pub fn create(context: *Context, window_manager_v1: *river.WindowManagerV1) !*Wi } pub fn destroy(wm: *WindowManager) void { - // TODO: Go through lists and destroy everything + var window_it = wm.windows.iterator(.forward); + while (window_it.next()) |window| { + window.window_v1.destroy(); + window.node_v1.destroy(); + window.link.remove(); + utils.allocator.destroy(window); + } + + var output_it = wm.outputs.iterator(.forward); + while (output_it.next()) |output| { + output.output_v1.destroy(); + output.link.remove(); + utils.allocator.destroy(output); + } + + var seat_it = wm.seats.iterator(.forward); + while (seat_it.next()) |seat| { + seat.seat_v1.destroy(); + seat.link.remove(); + utils.allocator.destroy(seat); + } + utils.allocator.destroy(wm); } @@ -208,7 +229,7 @@ fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: riv }, .output => |ev| { // TODO: Support multi-output - var output = utils.allocator.create(Output) catch @panic("out-of-memory; exiting."); + var output = utils.allocator.create(Output) catch @panic("Out of memory"); output.init(context, ev.id); wm.outputs.append(output); }, @@ -221,12 +242,12 @@ fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: riv utils.versionNotSupported(river.SeatV1, river_seat_v1_version, MIN_RIVER_SEAT_V1_VERSION); } - var seat = utils.allocator.create(Seat) catch @panic("out-of-memory; exiting."); + var seat = utils.allocator.create(Seat) catch @panic("Out of memory"); seat.init(context, river_seat_v1); wm.seats.append(seat); }, .window => |ev| { - var window = utils.allocator.create(Window) catch @panic("out-of-memory; exiting."); + var window = utils.allocator.create(Window) catch @panic("Out of memory"); window.init(context, ev.id); // TODO - CONFIG: Allow appending window instead of prepending diff --git a/src/XkbBindings.zig b/src/XkbBindings.zig index 2f9ea5f..e31bdfb 100644 --- a/src/XkbBindings.zig +++ b/src/XkbBindings.zig @@ -126,7 +126,13 @@ pub fn create(context: *Context, xkb_bindings_v1: *river.XkbBindingsV1) !*XkbBin } pub fn destroy(xkb_bindings: *XkbBindings) void { - xkb_bindings.destroy(); + var it = xkb_bindings.bindings.iterator(.forward); + while (it.next()) |binding| { + binding.link.remove(); + binding.xkb_binding_v1.destroy(); + utils.allocator.destroy(binding); + } + utils.allocator.destroy(xkb_bindings); } pub fn addBinding(xkb_bindings: *XkbBindings, river_seat_v1: *river.SeatV1, keysym: u32, modifiers: river.SeatV1.Modifiers, command: Command) void { @@ -135,7 +141,7 @@ pub fn addBinding(xkb_bindings: *XkbBindings, river_seat_v1: *river.SeatV1, keys return; }; - const xkb_binding = utils.allocator.create(XkbBinding) catch @panic("out-of-memory"); + const xkb_binding = utils.allocator.create(XkbBinding) catch @panic("Out of memory"); xkb_binding.init(xkb_binding_v1, command, xkb_bindings.context); xkb_bindings.bindings.append(xkb_binding); diff --git a/src/main.zig b/src/main.zig index 540fc2f..c304dde 100644 --- a/src/main.zig +++ b/src/main.zig @@ -11,7 +11,7 @@ const Globals = struct { pub fn main() !void { const wayland_display_var = try utils.allocator.dupeZ(u8, process.getEnvVarOwned(utils.allocator, "WAYLAND_DISPLAY") catch { - log.err("Error getting WAYLAND_DISPLAY environment variable. Exiing", .{}); + log.err("Error getting WAYLAND_DISPLAY environment variable. Exiting", .{}); std.posix.exit(1); }); defer utils.allocator.free(wayland_display_var);