From 5c427234d7f2ac94f534d6c9f9e0b6785129568b Mon Sep 17 00:00:00 2001 From: Ben Buhse Date: Sat, 14 Feb 2026 11:29:00 -0600 Subject: [PATCH] Add layer shell support and fix floating windows Now, I actually save the river-layer-shell-v1 and keep track of the non-exclusive area. The layout calculation uses the usable area instead of the entire output's geometry. I removed boundary clamping for the floating windows because it was a bit janky when hitting the edges. I'll probably add it back at some point. I also made windows default to 75% of the usable area instead of keeping their tiled size so that maximized windows look decent when floating for the first time. Finally, since I removed the clamping, I added a center_float keybind to center a floating window. If you're cycling through focused windows and one isn't on the screen, you can use the center_float bind to get the window visible again. Replaced all divTrunc with divFloor to be consistent. I think they should all be positive, anyways, so they'd be the same, but I like just having one. --- docs/CONFIGURATION.md | 1 + docs/TODO.md | 1 + src/Config.zig | 1 + src/Context.zig | 2 ++ src/Output.zig | 81 +++++++++++++++++++++++++++++++++++-------- src/Seat.zig | 37 +++++--------------- src/Window.zig | 15 ++++---- src/XkbBindings.zig | 39 ++++++++++----------- src/main.zig | 2 +- 9 files changed, 108 insertions(+), 71 deletions(-) diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 1eb329a..a02b79d 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -146,6 +146,7 @@ Full command reference: | `move_right` | pixels | Move floating window right | | `resize_width` | pixels | Resize floating window width (negative to shrink) | | `resize_height` | pixels | Resize floating window height (negative to shrink)| +| `center_float` | | Center the focused floating window on its output | | `set_output_tags` | tags (u32 bitmask) | Set the tags on the focused output | | `set_window_tags` | tags (u32 bitmask) | Set the tags on the focused window | | `toggle_output_tags` | tags (u32 bitmask) | Toggle a tag on the focused output | diff --git a/docs/TODO.md b/docs/TODO.md index 4577eb8..6bcd6f5 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -4,6 +4,7 @@ These are in rough order of my priority, though no promises I do them in this or - [ ] Implement a river-tag-overlay clone - [ ] Implement an optional clock bar +- [ ] Make a Rect struct to combine x, y, width, and height - [ ] Support window rules (float/tags/SSD by app-id/title) - [ ] Support overriding config location - [ ] Support configuring primary vs secondary stack side diff --git a/src/Config.zig b/src/Config.zig index 8ac3d59..2c2376d 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -506,6 +506,7 @@ fn loadKeybindsChildBlock(config: *Config, parser: *kdl.Parser, hostname: ?[]con .decrement_primary_count, .swap_next, .swap_prev, + .center_float, => |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/Context.zig b/src/Context.zig index b1c70c2..2c8eeda 100644 --- a/src/Context.zig +++ b/src/Context.zig @@ -14,6 +14,7 @@ wl_registry: *wl.Registry, wl_shm: *wl.Shm, wl_outputs: *std.AutoHashMapUnmanaged(u32, *wl.Output), +river_layer_shell_v1: *river.LayerShellV1, zwlr_layer_shell_v1: *zwlr.LayerShellV1, // Wayland globals that we have special structs for @@ -68,6 +69,7 @@ pub fn create(options: Options) !*Context { .wl_registry = options.wl_registry, .wl_shm = options.wl_shm, .wl_outputs = options.wl_outputs, + .river_layer_shell_v1 = options.river_layer_shell_v1, .zwlr_layer_shell_v1 = options.zwlr_layer_shell_v1, .wallpaper_image = loadWallpaperImage(options.config), .im = try InputManager.create(context, options.river_input_manager_v1, options.river_libinput_config_v1), diff --git a/src/Output.zig b/src/Output.zig index 672604e..e1e34a5 100644 --- a/src/Output.zig +++ b/src/Output.zig @@ -7,6 +7,7 @@ const Output = @This(); context: *Context, river_output_v1: *river.OutputV1, +river_layer_shell_output_v1: *river.LayerShellOutputV1, // We have to wait for the rwm.wl_output event to get this wl_output: ?*wl.Output = null, @@ -16,10 +17,16 @@ name: ?[]const u8 = null, // Output geometry scale: u31 = 1, -width: u31 = 0, -height: u31 = 0, x: i32 = 0, y: i32 = 0, +width: u31 = 0, +height: u31 = 0, + +// Area left after layer shell surfaces take exclusive area +usable_x: i32 = 0, +usable_y: i32 = 0, +usable_width: u31 = 0, +usable_height: u31 = 0, // Information for this Output's wallpaper wallpaper_render_width: u31 = 0, @@ -62,10 +69,15 @@ pub const TagLayoutOverride = struct { }; pub const PendingManage = struct { - width: ?u31 = null, - height: ?u31 = null, x: ?i32 = null, y: ?i32 = null, + width: ?u31 = null, + height: ?u31 = null, + + usable_x: ?i32 = null, + usable_y: ?i32 = null, + usable_width: ?u31 = null, + usable_height: ?u31 = null, tags: ?u32 = null, primary_ratio: ?f32 = null, @@ -84,6 +96,7 @@ pub fn create(context: *Context, river_output_v1: *river.OutputV1) !*Output { output.* = .{ .context = context, .river_output_v1 = river_output_v1, + .river_layer_shell_output_v1 = try context.river_layer_shell_v1.getOutput(river_output_v1), .bar = bar, .primary_count = context.config.primary_count, .primary_ratio = context.config.primary_ratio, @@ -94,6 +107,7 @@ pub fn create(context: *Context, river_output_v1: *river.OutputV1) !*Output { output.windows.init(); output.river_output_v1.setListener(*Output, riverOutputListener, output); + output.river_layer_shell_output_v1.setListener(*Output, riverLayerShellOutputListener, output); return output; } @@ -268,6 +282,24 @@ fn wlOutputListener(_: *wl.Output, event: wl.Output.Event, output: *Output) void } } +// Used for the river_layer_shell_output_v1 interface +fn riverLayerShellOutputListener( + river_layer_shell_output_v1: *river.LayerShellOutputV1, + event: river.LayerShellOutputV1.Event, + output: *Output, +) void { + assert(output.river_layer_shell_output_v1 == river_layer_shell_output_v1); + switch (event) { + .non_exclusive_area => |ev| { + output.pending_manage.usable_x = ev.x; + output.pending_manage.usable_y = ev.y; + output.pending_manage.usable_width = @intCast(ev.width); + output.pending_manage.usable_height = @intCast(ev.height); + output.context.wm.river_window_manager_v1.manageDirty(); + }, + } +} + pub fn initWallpaperLayerSurface(output: *Output) !void { if (output.context.wallpaper_image == null) { // No wallpaper image, so we don't need any surfaces @@ -446,17 +478,30 @@ pub fn renderWallpaper(output: *Output) !void { pub fn manage(output: *Output) void { defer output.pending_manage = .{}; + if (output.pending_manage.x) |x| { + output.x = x; + } + if (output.pending_manage.y) |y| { + output.y = y; + } if (output.pending_manage.width) |width| { output.width = width; } if (output.pending_manage.height) |height| { output.height = height; } - if (output.pending_manage.x) |x| { - output.x = x; + + if (output.pending_manage.usable_x) |usable_x| { + output.usable_x = usable_x; } - if (output.pending_manage.y) |y| { - output.y = y; + if (output.pending_manage.usable_y) |usable_y| { + output.usable_y = usable_y; + } + if (output.pending_manage.usable_width) |usable_width| { + output.usable_width = usable_width; + } + if (output.pending_manage.usable_height) |usable_height| { + output.usable_height = usable_height; } if (output.pending_manage.primary_ratio) |primary_ratio| { @@ -542,12 +587,11 @@ fn calculatePrimaryStackLayout(output: *Output) void { if (active_count == 0) return; - // Output dimensions come as i32 from the protocol, convert to u31 for window dimensions - // since they can't be negative. - const output_width: u31 = @intCast(output.width); - const output_height: u31 = @intCast(output.height); - const output_x = output.x; - const output_y = output.y; + // We have to use the usable area for the layout so windows don't overlap with widgets + const output_x = output.usable_x; + const output_y = output.usable_y; + const output_width = output.usable_width; + const output_height = output.usable_height; const border_width = output.context.config.border_width; // Single window: maximize and return early @@ -621,6 +665,15 @@ fn calculatePrimaryStackLayout(output: *Output) void { assert(active_list.first == null); } +pub fn occupiedTags(output: *Output) u32 { + var occupied_tags: u32 = 0x0000; + var it = output.windows.iterator(.forward); + while (it.next()) |window| { + occupied_tags |= window.tags; + } + return occupied_tags; +} + const std = @import("std"); const assert = std.debug.assert; const mem = std.mem; diff --git a/src/Seat.zig b/src/Seat.zig index e764bf9..ba7c4dd 100644 --- a/src/Seat.zig +++ b/src/Seat.zig @@ -159,8 +159,8 @@ pub fn manage(seat: *Seat) void { // Warp pointer to center of focused window; // because the x and y coords are set during render, we need to check if // there are new coordinates in window.pending_render. - 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); + const pointer_x: i32 = (window.pending_render.x orelse window.x) + @divFloor(window.width, 2); + const pointer_y: i32 = (window.pending_render.y orelse window.y) + @divFloor(window.height, 2); seat.river_seat_v1.pointerWarp(pointer_x, pointer_y); } } @@ -204,17 +204,8 @@ pub fn manage(seat: *Seat) void { switch (seat.pointer_op) { .none => {}, .move => |op| { - const output = op.window.output orelse { - log.err("window has no output during move operation", .{}); - return; - }; - const min_x = output.x; - const max_x = output.x + output.width - @as(i32, op.window.float_width); - const min_y = output.y; - const max_y = output.y + output.height - @as(i32, op.window.float_height); - - op.window.float_x = std.math.clamp(op.start_x + delta.dx, min_x, @max(min_x, max_x)); - op.window.float_y = std.math.clamp(op.start_y + delta.dy, min_y, @max(min_y, max_y)); + op.window.float_x = op.start_x + delta.dx; + op.window.float_y = op.start_y + delta.dy; op.window.pending_render.x = op.window.float_x; op.window.pending_render.y = op.window.float_y; }, @@ -238,22 +229,10 @@ pub fn manage(seat: *Seat) void { // Clamp to minimum size const min_size: i32 = 50; - if (new_width < min_size) { - if (op.edges.left) new_x -= min_size - new_width; - new_width = min_size; - } - if (new_height < min_size) { - if (op.edges.top) new_y -= min_size - new_height; - new_height = min_size; - } - - // Clamp position to output bounds - const output = op.window.output orelse { - log.err("window has no output during resize operation", .{}); - return; - }; - new_x = std.math.clamp(new_x, output.x, @max(output.x, output.x + output.width - new_width)); - new_y = std.math.clamp(new_y, output.y, @max(output.y, output.y + output.height - new_height)); + if (op.edges.left) new_x = @min(new_x, op.start_x + @as(i32, op.start_width) - min_size); + if (op.edges.top) new_y = @min(new_y, op.start_y + @as(i32, op.start_height) - min_size); + new_width = @max(new_width, min_size); + new_height = @max(new_height, min_size); op.window.float_width = @intCast(new_width); op.window.float_height = @intCast(new_height); diff --git a/src/Window.zig b/src/Window.zig index d838151..d3c7f1e 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -184,14 +184,17 @@ pub fn manage(window: *Window) void { river_window_v1.setTiled(.{}); if (window.float_width == 0) { - // Never floated before; use current dimensions but centered on output - window.float_width = window.width; - window.float_height = window.height; + // Never floated before; use 75% of usable area, centered on output if (window.output) |output| { - // Need to find center and then subtract half of the window's width/height - window.float_x = output.x + @divTrunc(output.width, 2) - @divTrunc(window.width, 2); - window.float_y = output.y + @divTrunc(output.height, 2) - @divTrunc(window.height, 2); + window.float_width = @divFloor(output.usable_width * 3, 4); + window.float_height = @divFloor(output.usable_height * 3, 4); + window.float_x = output.usable_x + @divFloor(output.usable_width, 2) - @divFloor(window.float_width, 2); + window.float_y = output.usable_y + @divFloor(output.usable_height, 2) - @divFloor(window.float_height, 2); + } else { + window.float_width = window.width; + window.float_height = window.height; } + river_window_v1.proposeDimensions(window.float_width, window.float_height); } else { // Window has floated before; re-use its old dimensions river_window_v1.proposeDimensions(window.float_width, window.float_height); diff --git a/src/XkbBindings.zig b/src/XkbBindings.zig index 8253e43..20e9ae9 100644 --- a/src/XkbBindings.zig +++ b/src/XkbBindings.zig @@ -41,6 +41,9 @@ pub const Command = union(enum) { resize_width: i32, resize_height: i32, + // Center floating window on its output + center_float, + // Swap window position in stack swap_next, swap_prev, @@ -237,6 +240,7 @@ const XkbBinding = struct { .move_right => |pixels| moveFloatingWindow(context, pixels, 0), .resize_width => |delta| resizeFloatingWindow(context, delta, 0), .resize_height => |delta| resizeFloatingWindow(context, 0, delta), + .center_float => centerFloatingWindow(context), .swap_next => swapWindow(context, .next), .swap_prev => swapWindow(context, .prev), } @@ -336,18 +340,9 @@ const XkbBinding = struct { const seat = context.wm.seats.first() orelse return; const window = seat.focused_window orelse return; if (!window.floating) return; - const output = window.output orelse { - log.err("focused floating window has no output during move", .{}); - return; - }; - const min_x = output.x; - const max_x = output.x + output.width - @as(i32, window.float_width); - const min_y = output.y; - const max_y = output.y + output.height - @as(i32, window.float_height); - - window.float_x = std.math.clamp(window.float_x + dx, min_x, @max(min_x, max_x)); - window.float_y = std.math.clamp(window.float_y + dy, min_y, @max(min_y, max_y)); + window.float_x += dx; + window.float_y += dy; window.pending_render.x = window.float_x; window.pending_render.y = window.float_y; context.wm.river_window_manager_v1.manageDirty(); @@ -357,25 +352,27 @@ const XkbBinding = struct { const seat = context.wm.seats.first() orelse return; const window = seat.focused_window orelse return; if (!window.floating) return; - const output = window.output orelse { - log.err("focused floating window has no output during resize", .{}); - return; - }; const new_width: i32 = @as(i32, window.float_width) + dw; const new_height: i32 = @as(i32, window.float_height) + dh; window.float_width = @intCast(@max(50, new_width)); window.float_height = @intCast(@max(50, new_height)); - // Clamp position to keep window on screen after resize - const max_x = output.x + output.width - @as(i32, window.float_width); - const max_y = output.y + output.height - @as(i32, window.float_height); - window.float_x = std.math.clamp(window.float_x, output.x, @max(output.x, max_x)); - window.float_y = std.math.clamp(window.float_y, output.y, @max(output.y, max_y)); + window.pending_manage.width = window.float_width; + window.pending_manage.height = window.float_height; + context.wm.river_window_manager_v1.manageDirty(); + } + fn centerFloatingWindow(context: *Context) void { + const seat = context.wm.seats.first() orelse return; + const window = seat.focused_window orelse return; + if (!window.floating) return; + const output = window.output orelse return; + + window.float_x = output.usable_x + @divFloor(output.usable_width, 2) - @divFloor(window.float_width, 2); + window.float_y = output.usable_y + @divFloor(output.usable_height, 2) - @divFloor(window.float_height, 2); window.pending_render.x = window.float_x; window.pending_render.y = window.float_y; - window.river_window_v1.proposeDimensions(window.float_width, window.float_height); context.wm.river_window_manager_v1.manageDirty(); } diff --git a/src/main.zig b/src/main.zig index f0094b9..0caa199 100644 --- a/src/main.zig +++ b/src/main.zig @@ -145,7 +145,7 @@ fn run(wl_display: *wl.Display, context: *Context) !void { log.err("Got a negative time ({d})", .{time}); return error.InvalidTime; } - const timeout: i32 = @intCast((@divTrunc(time, 60) * 60 + 60 - time) * 1000); + const timeout: i32 = @intCast((@divFloor(time, 60) * 60 + 60 - time) * 1000); const poll_rc = posix.poll(&pollfds, timeout) catch |err| { fatal("Failed to poll {s}", .{@errorName(err)});