From 92e82f38f54af270a9426a1cfea175fdf3ad8215 Mon Sep 17 00:00:00 2001 From: Ben Buhse Date: Sat, 24 Jan 2026 14:45:33 -0600 Subject: [PATCH] Implement fullscreening --- src/Window.zig | 84 +++++++++++++++++++++++++++++++++---------- src/WindowManager.zig | 57 ++++++++++++++--------------- src/XkbBindings.zig | 6 ++++ 3 files changed, 101 insertions(+), 46 deletions(-) diff --git a/src/Window.zig b/src/Window.zig index 5909f67..4124b12 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -14,15 +14,31 @@ height: u31 = 0, x: i32 = 0, y: i32 = 0, -new_width: u31 = 0, -new_height: u31 = 0, -new_x: i32 = 0, -new_y: i32 = 0, +fullscreen: bool = false, +maximized: bool = false, -is_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 = .{}, link: wl.list.Link, +/// Used to manage updates to various aspect of Window's state +pub const PendingState = struct { + width: ?u31 = null, + height: ?u31 = null, + x: ?i32 = null, + y: ?i32 = null, + + fullscreen: ?bool = null, + maximized: ?bool = null, +}; + pub fn init(window: *Window, context: *Context, river_window_v1: *river.WindowV1) void { window.* = .{ .context = context, @@ -70,32 +86,62 @@ 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(?) - window.window_v1.useSsd(); + if (!window.initialized) { + @branchHint(.unlikely); + window.initialized = true; - // Layout has been pre-computed by WindowManager.calculatePrimaryStackLayout() - // Apply the computed dimensions - if (window.new_width > 0 and window.new_height > 0) { - window.window_v1.proposeDimensions(window.new_width, window.new_height); + window.window_v1.useSsd(); - // Inform the window about its tiled/fullscreen state - if (window.context.wm.window_count == 1) { - // Single window is fullscreen - window.window_v1.setTiled(.{ .top = false, .bottom = false, .left = false, .right = false }); + window.window_v1.setCapabilities(.{ .fullscreen = true, .maximize = true }); + window.window_v1.setTiled(.{ .top = true, .bottom = true, .left = true, .right = true }); + } + + // Any new state since the last manage event + defer window.pending_state = .{}; + const pending_state = window.pending_state; + // Layout (pre-computed by WindowManager.calculatePrimaryStackLayout()) + if (pending_state.width) |new_width| { + if (pending_state.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| { + window.fullscreen = fullscreen; + if (fullscreen) { + 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 { - // Tiled windows - set tiled edges - window.window_v1.setTiled(.{ .top = true, .bottom = true, .left = true, .right = true }); + window.window_v1.exitFullscreen(); window.window_v1.informNotFullscreen(); } } + if (pending_state.maximized) |maximized| { + window.maximized = maximized; + if (maximized) { + window.window_v1.informMaximized(); + } else { + window.window_v1.informUnmaximized(); + } + } } pub fn render(window: *Window) void { // Set the window position using the pre-computed layout - window.node_v1.setPosition(window.new_x, window.new_y); + window.node_v1.setPosition(window.x, window.y); // Set borders - if (!window.is_maximized) { + if (!window.fullscreen) { const border_width = window.context.config.border_width; if (window.isFocused()) { const border_color_focused = window.context.config.border_color_focused; @@ -107,6 +153,8 @@ pub fn render(window: *Window) void { } } +// 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| { diff --git a/src/WindowManager.zig b/src/WindowManager.zig index eb37fb5..5a4116a 100644 --- a/src/WindowManager.zig +++ b/src/WindowManager.zig @@ -75,50 +75,50 @@ fn calculatePrimaryStackLayout(wm: *WindowManager) void { var it = wm.windows.iterator(.forward); while (it.next()) |window| { if (count == 1) { - // Single window: fullscreen - // TODO: Disable borders when maximized - window.new_x = output_x; - window.new_y = output_y; - window.new_width = output_width; - window.new_height = output_height; - window.is_maximized = true; + // 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; } else { // Multiple windows: primary/stack layout - // TODO: Support multiple primaries + // TODO: Support multiple windows in primary stack const primary_width: u31 = @intFromFloat(@as(f32, @floatFromInt(output_width)) * PRIMARY_RATIO); const stack_width: u31 = output_width - primary_width; const stack_count = count - 1; const stack_height: u31 = @divFloor(output_height, stack_count); - window.is_maximized = false; + window.pending_state.maximized = false; if (index == 0) { // Primary window (first window) - right side - window.new_x = output_x + @as(i32, stack_width); - window.new_y = output_y; - window.new_width = primary_width; - window.new_height = output_height; + 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; } else { // Stack window(s) - left side const stack_index = index - 1; - window.new_x = output_x; - window.new_y = output_y + @as(i32, stack_index) * @as(i32, stack_height); - window.new_width = stack_width; + 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; // Last stack window gets remaining height to avoid gaps from integer division if (index == count - 1) { - window.new_height = output_height - stack_index * stack_height; + window.pending_state.height = output_height - stack_index * stack_height; } else { - window.new_height = stack_height; + window.pending_state.height = stack_height; } } - - // Make space for borders; this is the same for all windows - // (unless we disable borders when maximized?) - const border_width = wm.context.config.border_width; - window.new_height -= 2 * border_width; - window.new_width -= 2 * border_width; - window.new_x += border_width; - window.new_y += border_width; } + // 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; + 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; + index += 1; } } @@ -140,8 +140,9 @@ fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: riv context.xkb_bindings.addBinding(xkbcommon.Keysym.t, .{ .mod4 = true }, .{ .spawn = &.{"foot"} }); context.xkb_bindings.addBinding(xkbcommon.Keysym.j, .{ .mod4 = true }, .focus_next); context.xkb_bindings.addBinding(xkbcommon.Keysym.k, .{ .mod4 = true }, .focus_prev); - context.xkb_bindings.addBinding(xkbcommon.Keysym.q, .{ .mod4 = true, .shift = true }, XkbBindings.Command.close_window); - context.xkb_bindings.addBinding(xkbcommon.Keysym.e, .{ .mod4 = true, .shift = true }, XkbBindings.Command.exit); + context.xkb_bindings.addBinding(xkbcommon.Keysym.f, .{ .mod4 = true }, .fullscreen); + context.xkb_bindings.addBinding(xkbcommon.Keysym.q, .{ .mod4 = true, .shift = true }, .close_window); + context.xkb_bindings.addBinding(xkbcommon.Keysym.e, .{ .mod4 = true, .shift = true }, .exit); } { diff --git a/src/XkbBindings.zig b/src/XkbBindings.zig index 0f0b771..2c04401 100644 --- a/src/XkbBindings.zig +++ b/src/XkbBindings.zig @@ -8,6 +8,7 @@ pub const Command = union(enum) { spawn: []const []const u8, focus_next, focus_prev, + fullscreen, close_window, exit, }; @@ -71,6 +72,11 @@ const XkbBinding = struct { } 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; + }, .close_window => { const seat = context.wm.seats.first() orelse return; if (seat.focused) |window| {