From a9473204ad8086e92123c62f9d16749728590b81 Mon Sep 17 00:00:00 2001 From: Ben Buhse Date: Thu, 26 Feb 2026 16:34:48 -0600 Subject: [PATCH] Convert Bar to use river-shell-surface I want to implement more functionality to the bar, similar to what machi has in its bar, but it seems a lot easier to just handle the bar with the rest of the manage/render loop that rwm and beansprout use. To do that, I had to convert the bar to use river-shell-surface instead of zwlr-layer-shell. In that process, I also removed support for zwlr-layer-shell exclusive zones. It made calculating the usable area for the layout more annoying. If someone *really* wants, I would consider adding it back, but the only thing I can think of that requires exclusive area is a bar, and we don't really support other bars, so I don't think it's needed. I also switched a couple of places to use saturating subtraction on unsigned ints. --- docs/TODO.md | 4 +- src/Bar.zig | 205 +++++++++++++++++++++++++----------------------- src/Context.zig | 6 -- src/Output.zig | 87 +++++++++++--------- src/Window.zig | 5 +- src/main.zig | 5 +- 6 files changed, 159 insertions(+), 153 deletions(-) diff --git a/docs/TODO.md b/docs/TODO.md index b93bd2d..e47f7a0 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -2,10 +2,11 @@ These are in rough order of my priority, though no promises I do them in this order. -- [ ] Support switch handling (e.g. lid close) +- [ ] Add build-time options for including the wallpaper (and maybe bar) - [ ] Check pointer position and only warp if not on focused window already - [ ] Change focus direction when closing window - [ ] Use set_xcursor_theme request +- [ ] Support configuring bar item positions (left/center/right) - [ ] Add focused window title to bar - [ ] Support overriding config location - [ ] Add support for center-primary layout @@ -25,6 +26,7 @@ These are in rough order of my priority, though no promises I do them in this or - [ ] Support configurable prepend/append on send-to-output - [ ] Support taking new output's tags on send-to-output - [ ] Add `spawn_tagmask`, `focus_previous_tags`, `send_to_previous_tags` commands +- [ ] Support switch handling (e.g. lid close) - [x] Support changeable primary ratio - [x] Support changeable primary count - [x] Support multiple outputs diff --git a/src/Bar.zig b/src/Bar.zig index 17fe155..4935bed 100644 --- a/src/Bar.zig +++ b/src/Bar.zig @@ -20,16 +20,28 @@ font_scale: u31 = 1, output: *Output, // Bar geometry -width: u31 = 0, -height: u31 = 0, +geometry: Rect = .{}, surfaces: ?struct { wl_surface: *wl.Surface, - layer_surface: *zwlr.LayerSurfaceV1, + river_shell_surface: *river.ShellSurfaceV1, + node: *river.NodeV1, } = null, +pending_manage: PendingManage = .{}, +pending_render: PendingRender = .{}, + configured: bool = false, +pub const PendingManage = struct { + output_geometry: bool = false, +}; + +pub const PendingRender = struct { + position: ?struct { x: i32, y: i32 } = null, + draw: bool = false, +}; + pub const Position = enum { top, bottom }; pub const Options = struct { @@ -54,6 +66,25 @@ pub fn init(context: *Context, output: *Output, options: Options) !Bar { const fcft_fonts = try getFcftFonts(options.fonts, scale); errdefer fcft_fonts.destroy(); + const wl_surface = try context.wl_compositor.createSurface(); + errdefer wl_surface.destroy(); + + const river_shell_surface = try context + .wm + .river_window_manager_v1 + .getShellSurface(wl_surface); + errdefer river_shell_surface.destroy(); + + const node = try river_shell_surface.getNode(); + errdefer node.destroy(); + + // We don't want our surface to have any input region (default is infinite) + const empty_region = try context.wl_compositor.createRegion(); + defer empty_region.destroy(); + wl_surface.setInputRegion(empty_region); + + context.buffer_pool.surface_count += 1; + return .{ .context = context, .options = options, @@ -61,125 +92,97 @@ pub fn init(context: *Context, output: *Output, options: Options) !Bar { .font_scale = scale, .timezone = timezone, .output = output, + .surfaces = .{ + .wl_surface = wl_surface, + .river_shell_surface = river_shell_surface, + .node = node, + }, + .configured = true, }; } -pub fn initSurface(bar: *Bar) !void { - if (bar.surfaces) |_| { - // This bar already has a surface, we can exit early - return; - } - - const context = bar.context; - const options = bar.options; - - const wl_surface = try context.wl_compositor.createSurface(); - errdefer wl_surface.destroy(); - - const layer_surface = try context - .zwlr_layer_shell_v1 - .getLayerSurface(wl_surface, bar.output.wl_output, .top, "beansprout-bar"); - errdefer layer_surface.destroy(); - - // We don't want our surface to have any input region (default is infinite) - const empty_region = try context.wl_compositor.createRegion(); - defer empty_region.destroy(); - wl_surface.setInputRegion(empty_region); - - const vertical_padding = 5; - // Set size wants logical pixels, so we have to scale the height - const logical_font_height = @divFloor(bar.fcft_fonts.height, @as(i32, bar.font_scale)); - const bar_height: u31 = @intCast(logical_font_height + 2 * vertical_padding); - layer_surface.setSize(0, bar_height); - - layer_surface.setAnchor(.{ .top = options.position == .top, .bottom = options.position == .bottom, .left = true, .right = true }); - layer_surface.setMargin(options.margins.top, options.margins.right, options.margins.bottom, options.margins.left); - - bar.surfaces = .{ - .wl_surface = wl_surface, - .layer_surface = layer_surface, - }; - context.buffer_pool.surface_count += 1; - - layer_surface.setListener(*Bar, layerSurfaceListener, bar); - wl_surface.commit(); -} - pub fn deinit(bar: *Bar) void { bar.configured = false; bar.timezone.deinit(); if (bar.surfaces) |surfaces| { - surfaces.layer_surface.destroy(); + surfaces.node.destroy(); + surfaces.river_shell_surface.destroy(); surfaces.wl_surface.destroy(); bar.context.buffer_pool.surface_count -= 1; } } -pub fn layerSurfaceListener( - layer_surface: *zwlr.LayerSurfaceV1, - event: zwlr.LayerSurfaceV1.Event, - bar: *Bar, -) void { - assert(bar.surfaces.?.layer_surface == layer_surface); - switch (event) { - .configure => |ev| { - layer_surface.ackConfigure(ev.serial); - const width: u31 = @intCast(ev.width); - const height: u31 = @intCast(ev.height); +pub fn manage(bar: *Bar) !void { + if (!bar.configured) return; + defer bar.pending_manage = .{}; - if (bar.configured and - bar.width == width and - bar.height == height and - bar.output.scale == bar.font_scale) - { - bar.surfaces.?.wl_surface.commit(); - return; - } + // The only manage actions we need to do are when the output changes geometry + if (!bar.pending_manage.output_geometry) return; - log.debug("Configuring bar surface with width {} and height {}", .{ width, height }); - bar.width = width; - bar.height = height; - // Exclusive zone == the bar's height - layer_surface.setExclusiveZone(bar.height); - - // Full surface should be opaque - const opaque_region: *wl.Region = bar.context.wl_compositor.createRegion() catch |e| { - log.err("Failed to create opaque region for bar: {}", .{e}); - return; - }; - defer opaque_region.destroy(); - opaque_region.add(0, 0, bar.width, bar.height); - bar.surfaces.?.wl_surface.setOpaqueRegion(opaque_region); - - bar.configured = true; - - bar.render() catch |err| { - log.err("Bar render failed: {}", .{err}); - }; - }, - .closed => { - bar.deinit(); - }, - } -} - -/// Renders the bar and its components -pub fn render(bar: *Bar) !void { - const context = bar.context; + const output = bar.output; const options = bar.options; - const scale = bar.output.scale; - - // Recreate fonts at the output's new scale + // Recreate fonts if the output scale changed, so geometry calculations + // below use the correct font metrics. + const scale = output.scale; if (scale != bar.font_scale) { bar.fcft_fonts.destroy(); bar.fcft_fonts = try getFcftFonts(bar.options.fonts, scale); bar.font_scale = scale; } + const vertical_padding = 5; + const logical_font_height = @divFloor(bar.fcft_fonts.height, @as(i32, bar.font_scale)); + const height: u31 = @intCast(logical_font_height + 2 * vertical_padding); + const width: u31 = output.geometry.width; + + if (bar.geometry.width != width or bar.geometry.height != height) { + bar.geometry.width = width; + bar.geometry.height = height; + + const opaque_region = try bar.context.wl_compositor.createRegion(); + defer opaque_region.destroy(); + opaque_region.add(0, 0, width, height); + bar.surfaces.?.wl_surface.setOpaqueRegion(opaque_region); + } + + const x = output.geometry.x + options.margins.left; + const y = switch (options.position) { + .top => output.geometry.y + options.margins.top, + .bottom => output.geometry.y + output.geometry.height - bar.geometry.height - options.margins.bottom, + }; + bar.pending_render.position = .{ .x = x, .y = y }; + bar.pending_render.draw = true; +} + +pub fn render(bar: *Bar) void { + if (!bar.configured) return; + defer bar.pending_render = .{}; + + const surfaces = bar.surfaces orelse return; + + if (bar.pending_render.position) |position| { + surfaces.node.setPosition(position.x, position.y); + surfaces.node.placeTop(); + } + + if (bar.pending_render.draw) { + bar.draw() catch |err| { + log.err("Bar draw failed: {}", .{err}); + }; + } +} + +/// Draw the bar and its components (clock, title, etc.) +pub fn draw(bar: *Bar) !void { + const context = bar.context; + const options = bar.options; + + const scale = bar.font_scale; + // Scaled width/height - const render_width = bar.width * scale; - const render_height = bar.height * scale; + const render_width = bar.geometry.width * scale; + const render_height = bar.geometry.height * scale; // Don't have anything to render if (render_width == 0 or render_height == 0 or scale == 0) { @@ -243,6 +246,7 @@ pub fn render(bar: *Bar) !void { // Finally, attach the buffer to the surface const surfaces = bar.surfaces orelse return error.NoSurfaces; const wl_surface = surfaces.wl_surface; + surfaces.river_shell_surface.syncNextCommit(); wl_surface.setBufferScale(scale); wl_surface.attach(buffer.wl_buffer, 0, 0); wl_surface.damageBuffer(0, 0, render_width, render_height); @@ -393,12 +397,13 @@ const unicode = std.unicode; const wayland = @import("wayland"); const wl = wayland.client.wl; -const zwlr = wayland.client.zwlr; +const river = wayland.client.river; const fcft = @import("fcft"); const pixman = @import("pixman"); const zeit = @import("zeit"); const utils = @import("utils.zig"); +const Rect = utils.Rect; const Buffer = @import("Buffer.zig"); const Context = @import("Context.zig"); const Output = @import("Output.zig"); diff --git a/src/Context.zig b/src/Context.zig index 9ccc976..2afe40a 100644 --- a/src/Context.zig +++ b/src/Context.zig @@ -216,12 +216,6 @@ pub fn manage(context: *Context) void { log.err("Failed to create bar: {}", .{e}); continue; }; - // If the output already has a wl_output, init the surface immediately - if (output.wl_output != null) { - output.bar.?.initSurface() catch |e| { - log.err("Failed to init bar surface: {}", .{e}); - }; - } } } } diff --git a/src/Output.zig b/src/Output.zig index d282d85..03c934f 100644 --- a/src/Output.zig +++ b/src/Output.zig @@ -19,7 +19,11 @@ name: ?[]const u8 = null, scale: u31 = 1, geometry: Rect = .{}, -// Area left after layer shell surfaces take exclusive area +// Area available for window layout (output geometry minus bar space) +// Maybe I'll re-add support for layer shell exclusive areas later, +// but adding that makes it more work for me and I don't personally +// know of anything that makes me want them since external bars won't +// work with beansprout. usable_geometry: Rect = .{}, // Information for this Output's wallpaper @@ -72,8 +76,6 @@ pub const PendingManage = struct { position: ?struct { x: i32, y: i32 } = null, dimensions: ?struct { width: u31, height: u31 } = null, - usable_geometry: ?Rect = null, - tags: ?u32 = null, primary_ratio: ?f32 = null, primary_count: ?u8 = null, @@ -116,7 +118,6 @@ 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; } @@ -285,16 +286,10 @@ fn wlOutputListener(_: *wl.Output, event: wl.Output.Event, output: *Output) void return; }; if (output.bar) |*bar| { - bar.initSurface() catch |err| { - const output_name = output.name orelse "some output"; - log.err("failed to init bar for {s}: {}", .{ output_name, err }); - return; - }; - // Re-render bar if the scale changed - if (bar.configured and output.scale != bar.font_scale) { - bar.render() catch |err| { - log.err("Bar render failed: {}", .{err}); - }; + // Trigger a full manage cycle if the scale changed so that + // fonts are reloaded and bar geometry is recalculated. + if (output.scale != bar.font_scale) { + bar.pending_manage.output_geometry = true; } } // Re-render wallpaper if scale changed @@ -320,26 +315,6 @@ 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_geometry = .{ - .x = ev.x, - .y = ev.y, - .width = @intCast(ev.width), - .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 @@ -535,8 +510,10 @@ pub fn manage(output: *Output) void { output.geometry.height = dimensions.height; } - if (output.pending_manage.usable_geometry) |usable_geometry| { - output.usable_geometry = usable_geometry; + if (output.pending_manage.position != null or output.pending_manage.dimensions != null) { + if (output.bar) |*bar| { + bar.pending_manage.output_geometry = true; + } } if (output.pending_manage.primary_ratio) |primary_ratio| { @@ -605,6 +582,34 @@ pub fn manage(output: *Output) void { } } + if (output.bar) |*bar| { + bar.manage() catch |err| { + log.err("Bar manage failed: {}", .{err}); + }; + } + + // Compute usable geometry from output geometry minus bar space. + // We don't use non_exclusive_area from layer shell since we don't support + // other layer shell clients with exclusive zones (layer shell clients that + // don't use exclusive areas are fine). + output.usable_geometry = output.geometry; + if (output.bar) |bar| { + if (bar.geometry.height > 0) { + const bar_height: i32 = bar.geometry.height; + const margins = bar.options.margins; + const reserved: u31 = @intCast(bar_height + margins.top + margins.bottom); + switch (bar.options.position) { + .top => { + output.usable_geometry.y += bar_height + margins.top + margins.bottom; + output.usable_geometry.height -|= reserved; + }, + .bottom => { + output.usable_geometry.height -|= reserved; + }, + } + } + } + // Calculate layout before managing windows, but only if output dimensions are initialized if (output.usable_geometry.width > 0 and output.usable_geometry.height > 0) { output.calculateLayout(); @@ -617,6 +622,10 @@ pub fn manage(output: *Output) void { } pub fn render(output: *Output) void { + if (output.bar) |*bar| { + bar.render(); + } + const seat = output.context.wm.seats.first(); const focused = if (seat) |s| s.focused_window else null; @@ -670,7 +679,7 @@ fn calculateLayout(output: *Output) void { if (active_count == 0) return; - // We have to use the usable area for the layout so windows don't overlap with widgets + // Use the usable area for layout so windows don't overlap the bar const output_x = output.usable_geometry.x; const output_y = output.usable_geometry.y; const output_width = output.usable_geometry.width; @@ -681,14 +690,14 @@ fn calculateLayout(output: *Output) void { if (active_count == 1) { const window: *Window = @fieldParentPtr("active_list_node", active_list.popFirst().?); - const width = @as(u31, @intFromFloat(@as(f32, @floatFromInt(output_width)) * output.single_window_ratio)) - + 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 = width, - .height = output_height - 2 * border_width, + .height = output_height -| 2 * border_width, }; window.pending_manage.maximized = true; return; diff --git a/src/Window.zig b/src/Window.zig index c6e8d16..2cbe4fa 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -56,10 +56,7 @@ pub const PendingManage = struct { }; pub const PendingRender = struct { - position: ?struct { - x: i32, - y: i32, - } = null, + position: ?struct { x: i32, y: i32 } = null, focused: ?bool = null, diff --git a/src/main.zig b/src/main.zig index 22fb011..4191091 100644 --- a/src/main.zig +++ b/src/main.zig @@ -146,9 +146,8 @@ fn run(wl_display: *wl.Display, context: *Context) !void { var it = context.wm.outputs.iterator(.forward); while (it.next()) |output| { if (output.bar) |*bar| { - bar.render() catch |err| { - log.err("Bar timer render failed: {}", .{err}); - }; + bar.pending_render.draw = true; + context.wm.river_window_manager_v1.manageDirty(); } } }