diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index dd60996..a06f0b7 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -45,6 +45,7 @@ wallpaper_image_path "~/Pictures/wallpaper.png" | `attach_mode` | enum | `top` | Where new windows go in the stack (`top` or `bottom`) | | `primary_count` | u8 | `1` | Number of windows in the primary stack (0+) | | `primary_ratio` | float | `0.55` | Proportion of output width for the primary stack (0.10-0.90) | +| `single_window_ratio` | float | `1.00` | Proportion of output width taken when a single tiled window is visible (0.10-1.00) | | `focus_follows_pointer` | bool | `#true` | Focus follows the pointer between windows | | `pointer_warp_on_focus_change` | bool | `#true` | Warp pointer to center of newly-focused windows | | `wallpaper_image_path` | string | none | Path to wallpaper image | diff --git a/examples/config.kdl b/examples/config.kdl index 63e09e6..c9857d5 100644 --- a/examples/config.kdl +++ b/examples/config.kdl @@ -4,6 +4,11 @@ attach_mode top primary_count 1 // Proportion of output width taken by the primary stack primary_ratio 0.55 +// Proportion of output width taken by a window if it's the only visible tiled window +// This is intended to be useful for ultrawides where a very wide window might not look very nice +// When this is < 1.0 and only one window is being tiled, the window will have width +// output_width * single_window_ratio and be centered on the output +single_window_ratio 0.70 // Whether mousing over a new window should move focus focus_follows_pointer #true // Whether the focus should warp to the center of newly-focused windows diff --git a/src/Config.zig b/src/Config.zig index 11b2942..507b5d7 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -4,7 +4,10 @@ const Config = @This(); -const CONFIG_FILE = "beansprout/config.kdl"; +const config_file = "beansprout/config.kdl"; + +pub const min_primary_ratio = 0.10; +pub const max_primary_ratio = 0.90; /// Width of window borders in pixels border_width: u8 = 2, @@ -19,6 +22,11 @@ primary_count: u8 = 1, /// Proportion of output width taken by the primary stack /// This is a global default, but each tagmask can have its own value primary_ratio: f32 = 0.55, +/// Proportion of output width taken by a window if it's the only visible tiled window +/// This is intended to be useful for ultrawides where a very wide window might not look very nice +/// When this is < 1.0 and only one window is being tiled, the window will have width +/// output_width * single_window_ratio and be centered on the output +single_window_ratio: f32 = 1.0, /// Where a new window should attach, top or bottom of the stack attach_mode: AttachMode = .top, @@ -58,6 +66,7 @@ const NodeName = enum { attach_mode, primary_count, primary_ratio, + single_window_ratio, focus_follows_pointer, pointer_warp_on_focus_change, wallpaper_image_path, @@ -80,7 +89,7 @@ pub fn create() !*Config { defer utils.gpa.free(config_dir); var path_buf: [std.fs.max_path_bytes]u8 = undefined; - const config_path = std.fmt.bufPrint(&path_buf, "{s}/{s}", .{ config_dir, CONFIG_FILE }) catch return config; + const config_path = std.fmt.bufPrint(&path_buf, "{s}/{s}", .{ config_dir, config_file }) catch return config; const file = fs.openFileAbsolute(config_path, .{}) catch break :blk; @@ -193,7 +202,24 @@ fn load(config: *Config, reader: *Io.Reader) !void { logWarnInvalidNodeArg(name, ratio_str); continue; }; - config.primary_ratio = std.math.clamp(ratio, 0.10, 0.90); + config.primary_ratio = std.math.clamp(ratio, min_primary_ratio, max_primary_ratio); + if (ratio != config.primary_ratio) { + log.warn("primary_ratio outside of valid range ({d} to {d}); clamping it", .{ min_primary_ratio, max_primary_ratio }); + } + logDebugSettingNode(name, ratio_str); + }, + .single_window_ratio => { + const ratio_str = utils.stripQuotes(node.arg(&parser, 0) orelse ""); + const ratio = fmt.parseFloat(f32, ratio_str) catch { + logWarnInvalidNodeArg(name, ratio_str); + continue; + }; + // We use 1.00 here because it doesn't make sense for a window to have a ratio > 1, + // i.e. for it to be wider than the output + config.single_window_ratio = std.math.clamp(ratio, min_primary_ratio, 1.00); + if (ratio != config.single_window_ratio) { + log.warn("single_window_ratio outside of valid range ({d} to {d}); clamping it", .{ min_primary_ratio, 1.00 }); + } logDebugSettingNode(name, ratio_str); }, .attach_mode => { diff --git a/src/Context.zig b/src/Context.zig index d894f2c..9ccc976 100644 --- a/src/Context.zig +++ b/src/Context.zig @@ -151,6 +151,14 @@ pub fn manage(context: *Context) void { context.config = new_config; context.initialized = false; + // Update output defaults from new config + var out_it_cfg = context.wm.outputs.iterator(.forward); + while (out_it_cfg.next()) |output| { + output.primary_ratio = new_config.primary_ratio; + output.primary_count = new_config.primary_count; + output.single_window_ratio = new_config.single_window_ratio; + } + // Mark all libinput devices as needing config re-application var dev_it = context.im.libinput_devices.iterator(.forward); while (dev_it.next()) |libinput_device| { diff --git a/src/Output.zig b/src/Output.zig index 1772ae7..cb09719 100644 --- a/src/Output.zig +++ b/src/Output.zig @@ -41,6 +41,9 @@ primary_ratio: f32, /// Number of windows in the primary stack primary_count: u8, +/// Proportion of output width taken by a window when it is the only visible tiled window +single_window_ratio: f32, + /// Per-tagmask layout overrides /// These only get added when the user modifies primary count or ratio /// Any tagmask NOT in this map keeps using the defaults from Config @@ -74,6 +77,7 @@ pub const PendingManage = struct { tags: ?u32 = null, primary_ratio: ?f32 = null, primary_count: ?u8 = null, + single_window_ratio: ?f32 = null, }; pub fn create(context: *Context, river_output_v1: *river.OutputV1) !*Output { @@ -88,6 +92,7 @@ pub fn create(context: *Context, river_output_v1: *river.OutputV1) !*Output { .tag_overlay = null, .primary_count = context.config.primary_count, .primary_ratio = context.config.primary_ratio, + .single_window_ratio = context.config.single_window_ratio, .windows = undefined, // we will initialize this shortly .link = undefined, // Handled by the wl.list }; @@ -536,12 +541,24 @@ pub fn manage(output: *Output) void { 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); + output.primary_ratio = std.math.clamp( + primary_ratio, + Config.min_primary_ratio, + Config.max_primary_ratio, + ); } if (output.pending_manage.primary_count) |primary_count| { // Don't allow less than 1 primary output.primary_count = @max(1, primary_count); } + if (output.pending_manage.single_window_ratio) |single_window_ratio| { + output.single_window_ratio = std.math.clamp( + single_window_ratio, + Config.min_primary_ratio, + 1.00, + ); + } + if (output.pending_manage.tags) |new_tags| { // Save current layout for the old tagmask output.tag_layout_overrides.put(utils.gpa, output.tags, .{ @@ -627,7 +644,8 @@ pub fn render(output: *Output) void { fn calculateLayout(output: *Output) void { // Shouldn't be called if height/width are not positive assert(output.geometry.width > 0 and output.geometry.height > 0); - // Get a list of active windows + // Get a list of active tiled windows + // i.e. any windows that are on this output with at least one active tag and aren't fullscreen or floating var active_list: DoublyLinkedList = .{}; var active_count: u31 = 0; var it = output.windows.iterator(.forward); @@ -661,9 +679,14 @@ fn calculateLayout(output: *Output) void { // Single window: maximize and return early if (active_count == 1) { const window: *Window = @fieldParentPtr("active_list_node", active_list.popFirst().?); - window.pending_render.position = .{ .x = output_x + border_width, .y = output_y + border_width }; + + const width = @as(u31, @intFromFloat(@as(f32, @floatFromInt(output_width)) * output.single_window_ratio)) - + 2 * border_width; + const x = output_x + @divFloor(output_width - width, 2); + + window.pending_render.position = .{ .x = x, .y = output_y + border_width }; window.pending_manage.dimensions = .{ - .width = output_width - 2 * border_width, + .width = width, .height = output_height - 2 * border_width, }; window.pending_manage.maximized = true; @@ -771,6 +794,7 @@ const utils = @import("utils.zig"); const Rect = utils.Rect; const Bar = @import("Bar.zig"); const Buffer = @import("Buffer.zig"); +const Config = @import("Config.zig"); const Context = @import("Context.zig"); const TagOverlay = @import("TagOverlay.zig"); const Window = @import("Window.zig"); diff --git a/src/WindowManager.zig b/src/WindowManager.zig index 2c8de3c..67bc845 100644 --- a/src/WindowManager.zig +++ b/src/WindowManager.zig @@ -4,7 +4,7 @@ const WindowManager = @This(); -const MIN_RIVER_SEAT_V1_VERSION: u2 = 3; +const min_river_seat_v1_version: u2 = 3; context: *Context, @@ -239,9 +239,9 @@ fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: riv .seat => |ev| { 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) { + 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); + 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");