From 5ff05ab09e40c67c3cd955d6a5fbb393b478c893 Mon Sep 17 00:00:00 2001 From: Ben Buhse Date: Tue, 3 Feb 2026 20:56:54 -0600 Subject: [PATCH] Implement changeable primary count There are new increment_primary_count and decrement_primary_count config options --- README.md | 2 +- examples/config.kdl | 2 + src/Config.zig | 2 + src/Output.zig | 101 +++++++++++++++++++++++++++----------------- src/XkbBindings.zig | 17 ++++++++ 5 files changed, 85 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 28a0aa0..5c3a3cb 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,10 @@ These are in rough order of my priority, though no promises I do them in this or - [ ] 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 count - [x] Support changeable primary ratio - [x] Support multiple outputs diff --git a/examples/config.kdl b/examples/config.kdl index 5ce7e57..a3acd1e 100644 --- a/examples/config.kdl +++ b/examples/config.kdl @@ -17,6 +17,8 @@ keybinds { zoom Mod4 Z change_ratio Mod4 H +0.05 change_ratio Mod4 L -0.05 + increment_primary_count Mod4 I + decrement_primary_count Mod4 D reload_config Mod4+Shift R toggle_fullscreen Mod4 F close_window Mod4+Shift Q diff --git a/src/Config.zig b/src/Config.zig index c804136..3e5795b 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -334,6 +334,8 @@ fn loadKeybindsChildBlock(config: *Config, parser: *kdl.Parser) !void { .reload_config, .toggle_fullscreen, .close_window, + .increment_primary_count, + .decrement_primary_count, => |cmd| { // None of these have arguments, just create the union and give it back break :sw @unionInit(XkbBindings.Command, @tagName(cmd), {}); diff --git a/src/Output.zig b/src/Output.zig index 0ecfbf3..8bf432a 100644 --- a/src/Output.zig +++ b/src/Output.zig @@ -16,6 +16,9 @@ y: i32 = 0, /// Proportion of output width taken by the primary stack primary_ratio: f32 = 0.55, +/// Number of windows in the primary stack +primary_count: u8 = 1, + /// Tags are 32-bit bitfield. A window can be active on one(?) or more tags. tags: u32 = 0x0001, @@ -34,6 +37,7 @@ pub const PendingManage = struct { tags: ?u32 = null, primary_ratio: ?f32 = null, + primary_count: ?u8 = null, }; pub fn create(context: *Context, river_output_v1: *river.OutputV1) !*Output { @@ -170,6 +174,10 @@ pub fn manage(output: *Output) void { // 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); } + if (output.pending_manage.primary_count) |primary_count| { + // Don't allow less than 1 primary + output.primary_count = @max(1, primary_count); + } // Calculate layout before managing windows output.calculatePrimaryStackLayout(); @@ -214,52 +222,69 @@ fn calculatePrimaryStackLayout(output: *Output) void { const output_height: u31 = @intCast(output.height); const output_x = output.x; const output_y = output.y; + const border_width = output.context.config.border_width; - // Iterate through the active windows and apply the tags + // Single window: maximize and return early + if (active_count == 1) { + const window: *Window = @fieldParentPtr("active_list_node", active_list.popFirst().?); + window.pending_render.x = output_x + border_width; + window.pending_render.y = output_y + border_width; + window.pending_manage.width = output_width - 2 * border_width; + window.pending_manage.height = output_height - 2 * border_width; + window.pending_manage.maximized = true; + return; + } + + // Multiple windows: primary/stack layout + const primary_count = @min(active_count, output.primary_count); + const stack_count = active_count - primary_count; + + // Primary width is equal to output width when all windows are primaries + // (since there would be no secondaries) + const primary_width: u31 = if (primary_count == active_count) + output_width + else + @intFromFloat(@as(f32, @floatFromInt(output_width)) * output.primary_ratio); + const primary_height: u31 = @divFloor(output_height, primary_count); + + const stack_width: u31 = output_width - primary_width; + const stack_height: u31 = if (stack_count > 0) + @divFloor(output_height, stack_count) + else + 0; + + // Iterate through the active windows and apply positions 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; + 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; + if (i < primary_count) { + // Primary window(s) - right side + window.pending_render.x = output_x + @as(i32, stack_width); + window.pending_render.y = output_y + @as(i32, i) * @as(i32, primary_height); + window.pending_manage.width = primary_width; + // Last primary window gets remaining height to avoid gaps from integer division + if (i == primary_count - 1) { + window.pending_manage.height = output_height - i * primary_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; - } + window.pending_manage.height = primary_height; + } + } else { + // Stack window(s) - left side + const stack_index = i - primary_count; + 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 (stack_index == stack_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 + + // Make space for borders window.pending_manage.height.? -= 2 * border_width; window.pending_manage.width.? -= 2 * border_width; window.pending_render.x.? += border_width; diff --git a/src/XkbBindings.zig b/src/XkbBindings.zig index 3cf340e..4d85a80 100644 --- a/src/XkbBindings.zig +++ b/src/XkbBindings.zig @@ -15,6 +15,9 @@ pub const Command = union(enum) { zoom, // Changes the ratio on the focused output only change_ratio: f32, + // Changes the primary count on the focus output only + increment_primary_count, + decrement_primary_count, reload_config, toggle_fullscreen, close_window, @@ -72,6 +75,7 @@ const XkbBinding = struct { fn executeCommand(xkb_binding: *XkbBinding) void { const context = xkb_binding.context; + // TODO: Should I log.warn when commands return early? switch (xkb_binding.command) { .spawn => |cmd| { var child = std.process.Child.init(cmd, utils.allocator); @@ -118,6 +122,19 @@ const XkbBinding = struct { const seat = context.wm.seats.first() orelse return; const output = seat.focused_output orelse return; output.pending_manage.primary_ratio = output.primary_ratio + diff; + context.wm.river_window_manager_v1.manageDirty(); + }, + .increment_primary_count => { + const seat = context.wm.seats.first() orelse return; + const output = seat.focused_output orelse return; + output.pending_manage.primary_count = output.primary_count + 1; + context.wm.river_window_manager_v1.manageDirty(); + }, + .decrement_primary_count => { + const seat = context.wm.seats.first() orelse return; + const output = seat.focused_output orelse return; + output.pending_manage.primary_count = output.primary_count - 1; + context.wm.river_window_manager_v1.manageDirty(); }, .reload_config => { // Try create new config