diff --git a/README.md b/README.md index 0fb0537..7001c43 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ SPDX-License-Identifier: GPL-3.0-or-later These are in rough order of my priority, though no promises I do them in this order. +- [ ] Switch all structs to idiomatic Zig init/deinit pattern (init returns value, caller decides stack/heap) +- [ ] Support per-host config using properties - [ ] Support wallpapers - [ ] Support a bar - [ ] Support starting programs at WM launch diff --git a/build.zig b/build.zig index 434e9bf..2707d47 100644 --- a/build.zig +++ b/build.zig @@ -111,6 +111,6 @@ pub fn build(b: *std.Build) void { exe_check.linkSystemLibrary("pixman-1"); exe_check.linkSystemLibrary("xkbcommon"); - const check = b.step("check", "Check if beanbag compiles"); + const check = b.step("check", "Check if beansprout compiles"); check.dependOn(&exe_check.step); } diff --git a/src/Buffer.zig b/src/Buffer.zig new file mode 100644 index 0000000..d9ed980 --- /dev/null +++ b/src/Buffer.zig @@ -0,0 +1,128 @@ +// SPDX-FileCopyrightText: 2026 Ben Buhse +// +// SPDX-License-Identifier: GPL-3.0-or-later + +const Buffer = @This(); + +width: u31, +height: u31, +stride: u31, + +busy: bool, +size: u31, +data: ?[]align(std.heap.page_size_min) u8, + +wl_buffer: *wl.Buffer, +pixman_image: *pixman.Image, + +/// Used to add Buffers to a BufferPool +node: std.DoublyLinkedList.Node = .{}, + +pub fn init(shm: *wl.Shm, width: u31, height: u31) !Buffer { + // We use argb8888 + const stride = width * 4; + const size: u31 = height * stride; + + log.debug("initializing a new buffer with size {d}", .{size}); + + // Open a memory-backed file with sealing enabled + const fd = switch (builtin.target.os.tag) { + .linux => try posix.memfd_createZ("beansprout-shm-buffer", os.linux.MFD.CLOEXEC | os.linux.MFD.ALLOW_SEALING), + .freebsd => try posix.memfd_createZ("beansprout-shm-buffer", std.c.MFD.CLOEXEC | std.c.MFD.ALLOW_SEALING), + else => @compileError("target OS not supported"), + }; + defer posix.close(fd); + + // Try to allocate it to the desired size + try posix.ftruncate(fd, size); + + // mmap the memory file for the pixman image + const data = mem.bytesAsSlice( + u8, + try posix.mmap(null, size, posix.PROT.READ | posix.PROT.WRITE, .{ .TYPE = .SHARED }, fd, 0), + ); + errdefer posix.munmap(data); + + // Seal the fd to prevent size changes. The compositor maps the same fd, + // so without sealing it could access invalid memory if the client resized it. + _ = try posix.fcntl(fd, seal.F_ADD_SEALS, seal.SEAL_GROW | seal.SEAL_SHRINK | seal.SEAL_SEAL); + + // Create a Wayland shm buffer for the same memory file. + const pool = try shm.createPool(fd, size); + defer pool.destroy(); + + const wl_buffer = try pool.createBuffer(0, width, height, stride, .argb8888); + errdefer wl_buffer.destroy(); + + // Create the pixman image. + const pixman_image = pixman.Image.createBitsNoClear( + .a8r8g8b8, + @as(c_int, @intCast(width)), + @as(c_int, @intCast(height)), + @as([*c]u32, @ptrCast(data)), + @as(c_int, @intCast(stride)), + ) orelse return error.NoPixmanImage; + + // The pixman image and the Wayland buffer now share the same memory. + return .{ + .width = width, + .height = height, + .stride = stride, + .busy = true, + .size = size, + .wl_buffer = wl_buffer, + .data = data, + .pixman_image = pixman_image, + }; +} + +pub fn deinit(buffer: *Buffer) void { + _ = buffer.pixman_image.unref(); + buffer.wl_buffer.destroy(); + if (buffer.data) |data| posix.munmap(data); +} + +// We have to do this later because of the way init() works +pub fn setListener(buffer: *Buffer) void { + buffer.wl_buffer.setListener(*Buffer, buffer_listener, buffer); +} + +fn buffer_listener(_: *wl.Buffer, event: wl.Buffer.Event, buffer: *Buffer) void { + switch (event) { + .release => buffer.busy = false, + } +} + +const std = @import("std"); +const builtin = @import("builtin"); +const mem = std.mem; +const os = std.os; +const posix = std.posix; + +const wayland = @import("wayland"); +const wl = wayland.client.wl; +const pixman = @import("pixman"); + +const utils = @import("utils.zig"); + +/// Sealing constants for memfd. Prevents the compositor from accessing +/// invalid memory by locking the fd's size after setup. +const seal = switch (builtin.target.os.tag) { + .linux => struct { + // Linux values are missing from stdlib right now, + // just take the values from fcntl.h + const F_ADD_SEALS: i32 = 1033; + const SEAL_SEAL: usize = 0x0001; + const SEAL_SHRINK: usize = 0x0002; + const SEAL_GROW: usize = 0x0004; + }, + .freebsd => struct { + const F_ADD_SEALS = std.c.F.ADD_SEALS; + const SEAL_SEAL = std.c.F.SEAL_SEAL; + const SEAL_SHRINK = std.c.F.SEAL_SHRINK; + const SEAL_GROW = std.c.F.SEAL_GROW; + }, + else => @compileError("target OS not supported"), +}; + +const log = std.log.scoped(.Buffer); diff --git a/src/BufferPool.zig b/src/BufferPool.zig new file mode 100644 index 0000000..b7d65bb --- /dev/null +++ b/src/BufferPool.zig @@ -0,0 +1,122 @@ +// SPDX-FileCopyrightText: 2026 Ben Buhse +// +// SPDX-License-Identifier: GPL-3.0-or-later + +// Borrowed and adapted from https://git.sr.ht/~leon_plickat/wayprompt +const BufferPool = @This(); + +/// The amount of buffers per surface we consider the reasonable upper limit. +/// Some compositors sometimes tripple-buffer, so three seems to be ok. +/// Note that we can absolutely work with higher buffer numbers if needed, +/// however we consider that to be an anomaly and therefore do not want to +/// keep all those extra buffers around if we can avoid it, as to not have +/// unecessary memory overhead. +const max_buffer_multiplicity = 3; + +/// The buffers. This is a linked list and not an array list, because we +/// need stable pointers for the listener of the wl_buffer object. +buffers: DoublyLinkedList = .{}, +len: usize = 0, + +/// Number of surfaces sharing this pool, used to determine when to cull extra buffers. +/// Each surface is allowed up to max_buffer_multiplicity buffers. +surface_count: usize = 0, + +/// Deinit the buffer pool, destroying all buffers and freeing all memory. +pub fn deinit(buffer_pool: *BufferPool) void { + var it = buffer_pool.buffers.first; + while (it) |node| { + // Advance before destroying, since node is embedded in buffer + it = node.next; + const buffer: *Buffer = @fieldParentPtr("node", node); + buffer.deinit(); + utils.allocator.destroy(buffer); + } +} + +/// Get a buffer with the specified dimensions. If possible, an idle buffer is +/// reused, otherwise a new one is created. +pub fn nextBuffer(buffer_pool: *BufferPool, wl_shm: *wl.Shm, width: u31, height: u31) !*Buffer { + log.debug("looking for buffer with dimensions {}x{}, total existing buffers: {}", .{ width, height, buffer_pool.len }); + defer { + // Clear up extra buffers + if (buffer_pool.len > max_buffer_multiplicity * buffer_pool.surface_count) { + buffer_pool.cullBuffers(); + } + } + if (try buffer_pool.findSuitableBuffer(wl_shm, width, height)) |buffer| { + return buffer; + } else { + return try buffer_pool.newBuffer(wl_shm, width, height); + } +} + +/// Get the first free buffer with the specified dimensions. +/// If there are no free buffers with the right dimensions, re-init a free buffer that +/// has other dimensions. If no free buffer exists at all, return null. +fn findSuitableBuffer(buffer_pool: *BufferPool, wl_shm: *wl.Shm, width: u31, height: u31) !?*Buffer { + var it = buffer_pool.buffers.first; + var first_unbusy_buffer: ?*Buffer = null; + while (it) |node| : (it = node.next) { + const buffer: *Buffer = @fieldParentPtr("node", node); + if (buffer.busy) continue; + if (buffer.width == width and buffer.height == height) { + return buffer; + } else { + first_unbusy_buffer = buffer; + } + } + + // No buffer has matching dimensions, however we do have an unbusy + // buffer which we can just re-init. + if (first_unbusy_buffer) |buffer| { + buffer.deinit(); + buffer.* = try Buffer.init(wl_shm, width, height); + return buffer; + } + + return null; +} + +fn newBuffer(buffer_pool: *BufferPool, wl_shm: *wl.Shm, width: u31, height: u31) !*Buffer { + log.debug("creating new buffer {}x{}", .{ width, height }); + const buffer = try utils.allocator.create(Buffer); + errdefer utils.allocator.destroy(buffer); + buffer.* = try Buffer.init(wl_shm, width, height); + buffer.setListener(); + buffer_pool.buffers.append(&buffer.node); + buffer_pool.len += 1; + return buffer; +} + +fn cullBuffers(buffer_pool: *BufferPool) void { + log.debug("culling extra buffers", .{}); + var overhead = buffer_pool.len - max_buffer_multiplicity * buffer_pool.surface_count; + var it = buffer_pool.buffers.first; + while (it) |node| { + if (overhead == 0) break; + // Advance before destroying, since node is embedded in buffer + it = node.next; + const buffer: *Buffer = @fieldParentPtr("node", node); + if (!buffer.busy) { + buffer.deinit(); + buffer_pool.buffers.remove(node); + utils.allocator.destroy(buffer); + buffer_pool.len -= 1; + overhead -= 1; + } + } + log.debug(" -> new buffer count: {}", .{buffer_pool.len}); +} + +const std = @import("std"); +const DoublyLinkedList = std.DoublyLinkedList; + +const wayland = @import("wayland"); +const wl = wayland.client.wl; + +const Buffer = @import("Buffer.zig"); + +const utils = @import("utils.zig"); + +const log = std.log.scoped(.BufferPool); diff --git a/src/Context.zig b/src/Context.zig index c578592..0a11926 100644 --- a/src/Context.zig +++ b/src/Context.zig @@ -12,15 +12,23 @@ initialized: bool, // Wayland globals wl_compositor: *wl.Compositor, wl_display: *wl.Display, -wl_output: *wl.Output, wl_registry: *wl.Registry, wl_shm: *wl.Shm, +wl_outputs: *std.AutoHashMapUnmanaged(u32, *wl.Output), + +zwlr_layer_shell_v1: *zwlr.LayerShellV1, // Wayland globals that we have special structs for -wallpaper_image: *WallpaperImage, wm: *WindowManager, xkb_bindings: *XkbBindings, +/// Pool of Buffers used for rendering wallpapers +buffer_pool: BufferPool = .{}, + +/// Holds a pixman.Image (and its raw pixels) for the wallpaper +/// (same image on all outputs, but scaled separately) +wallpaper_image: *WallpaperImage, + // WM Configuration config: *Config, @@ -36,12 +44,14 @@ pub const PendingManage = struct { pub const Options = struct { wl_compositor: *wl.Compositor, wl_display: *wl.Display, - wl_output: *wl.Output, wl_registry: *wl.Registry, wl_shm: *wl.Shm, - river_layer_shell_v1: *river.LayerShellV1, + wl_outputs: *std.AutoHashMapUnmanaged(u32, *wl.Output), + + river_layer_shell_v1: *river.LayerShellV1, // TODO river_window_manager_v1: *river.WindowManagerV1, river_xkb_bindings_v1: *river.XkbBindingsV1, + zwlr_layer_shell_v1: *zwlr.LayerShellV1, config: *Config, }; @@ -54,9 +64,10 @@ pub fn create(options: Options) !*Context { .initialized = false, .wl_compositor = options.wl_compositor, .wl_display = options.wl_display, - .wl_output = options.wl_output, .wl_registry = options.wl_registry, .wl_shm = options.wl_shm, + .wl_outputs = options.wl_outputs, + .zwlr_layer_shell_v1 = options.zwlr_layer_shell_v1, .wallpaper_image = try WallpaperImage.create("FIXME"), // FIXME: TODO: Get this from Config .wm = try WindowManager.create(context, options.river_window_manager_v1), .xkb_bindings = try XkbBindings.create(context, options.river_xkb_bindings_v1), @@ -70,6 +81,9 @@ pub fn destroy(context: *Context) void { context.xkb_bindings.destroy(); context.wm.destroy(); + context.wallpaper_image.destroy(); + context.buffer_pool.deinit(); + utils.allocator.destroy(context); } @@ -98,6 +112,7 @@ const zwlr = wayland.client.zwlr; const utils = @import("utils.zig"); const Config = @import("Config.zig"); +const BufferPool = @import("BufferPool.zig"); const WallpaperImage = @import("WallpaperImage.zig"); const WindowManager = @import("WindowManager.zig"); const XkbBindings = @import("XkbBindings.zig"); diff --git a/src/Output.zig b/src/Output.zig index 10d1092..4541d49 100644 --- a/src/Output.zig +++ b/src/Output.zig @@ -8,11 +8,22 @@ context: *Context, river_output_v1: *river.OutputV1, -width: i32 = 0, -height: i32 = 0, +// We have to wait for the rwm.wl_output event to get this +wl_output: ?*wl.Output = null, + +// Output geometry +scale: u31 = 0, +width: u31 = 0, +height: u31 = 0, x: i32 = 0, y: i32 = 0, +// Information for this Output's wallpaper +render_width: u31 = 0, +render_height: u31 = 0, +wl_surface: ?*wl.Surface = null, +layer_surface: ?*zwlr.LayerSurfaceV1 = null, + /// Proportion of output width taken by the primary stack primary_ratio: f32 = 0.55, @@ -25,13 +36,19 @@ tags: u32 = 0x0001, /// State consumed in manage() phase, reset at end of manage(). pending_manage: PendingManage = .{}, +// Friendly name of this output +name: ?[]const u8 = null, + +/// Used for wallpaper rendering management +configured: bool = false, + windows: wl.list.Head(Window, .link), link: wl.list.Link, pub const PendingManage = struct { - width: ?i32 = null, - height: ?i32 = null, + width: ?u31 = null, + height: ?u31 = null, x: ?i32 = null, y: ?i32 = null, @@ -53,7 +70,7 @@ pub fn create(context: *Context, river_output_v1: *river.OutputV1) !*Output { output.windows.init(); - output.river_output_v1.setListener(*Output, outputListener, output); + output.river_output_v1.setListener(*Output, riverOutputListener, output); return output; } @@ -65,6 +82,7 @@ pub fn destroy(output: *Output) void { window.destroy(); } + output.deinitWallpaperLayerSurface(); output.river_output_v1.destroy(); utils.allocator.destroy(output); } @@ -85,7 +103,8 @@ pub fn prevWindow(output: *Output, current: *Window) ?*Window { return @fieldParentPtr("link", prev_link); } -fn outputListener(river_output_v1: *river.OutputV1, event: river.OutputV1.Event, output: *Output) void { +// Used for the river_output_v1 interface +fn riverOutputListener(river_output_v1: *river.OutputV1, event: river.OutputV1.Event, output: *Output) void { assert(output.river_output_v1 == river_output_v1); switch (event) { .removed => { @@ -136,21 +155,220 @@ fn outputListener(river_output_v1: *river.OutputV1, event: river.OutputV1.Event, output.destroy(); }, .wl_output => |ev| { - log.debug("initializing new river_output_v1 corresponding to wl_output: {d}", .{ev.name}); + // It's guaranteed for the wl_output global to advertised before this event happens + output.wl_output = output.context.wl_outputs.get(ev.name) orelse unreachable; + output.wl_output.?.setListener(*Output, wlOutputListener, output); }, .dimensions => |ev| { - output.pending_manage.width = ev.width; - output.pending_manage.height = ev.height; - output.context.wm.river_window_manager_v1.manageDirty(); + // Protocol guarantees that width and height are strictly greater than zero + assert(ev.width > 0 and ev.height > 0); + output.pending_manage.width = @intCast(ev.width); + output.pending_manage.height = @intCast(ev.height); }, .position => |ev| { output.pending_manage.x = ev.x; output.pending_manage.y = ev.y; - output.context.wm.river_window_manager_v1.manageDirty(); }, } } +// Used for the wl_output global interface that corresponds to the river_output_v1 +fn wlOutputListener(_: *wl.Output, event: wl.Output.Event, output: *Output) void { + switch (event) { + .mode => |ev| { + if (ev.width < 0 or ev.height < 0) { + // I'm not actually sure if this is possible, but just to be safe + log.warn("Received wl_output.mode event with a negative width or height ({d}x{d})", .{ ev.width, ev.height }); + return; + } + + output.width = @intCast(ev.width); + output.height = @intCast(ev.height); + }, + .done => { + output.initWallpaperLayerSurface() catch |err| { + const output_name = output.name orelse "some output"; + log.err("failed to add a surface to {s}: {}", .{ output_name, err }); + return; + }; + }, + .scale => |ev| { + if (ev.factor < 0) { + // I'm not actually sure if this is possible, but just to be safe + log.warn("Received wl_output.scale event with a negative factor ({d})", .{ev.factor}); + return; + } + output.scale = @intCast(ev.factor); + }, + .name => |ev| { + output.name = utils.allocator.dupe(u8, mem.span(ev.name)) catch @panic("Out of memory"); + }, + else => {}, + } +} + +fn initWallpaperLayerSurface(output: *Output) !void { + if (output.wl_surface) |_| { + log.warn("Skipping adding a second wallpaper surface to {s}", .{output.name orelse "some output"}); + return; + } + + const context = output.context; + + const wl_surface: *wl.Surface = try context.wl_compositor.createSurface(); + + // We don't want our surface to have any input region (default is infinite) + const empty_region: *wl.Region = try context.wl_compositor.createRegion(); + defer empty_region.destroy(); + wl_surface.setInputRegion(empty_region); + + // Full surface should be opaque + const opaque_region: *wl.Region = try context.wl_compositor.createRegion(); + defer opaque_region.destroy(); + wl_surface.setOpaqueRegion(opaque_region); + + const layer_surface: *zwlr.LayerSurfaceV1 = try context.zwlr_layer_shell_v1.getLayerSurface(wl_surface, output.wl_output, .background, "beansprout"); + layer_surface.setExclusiveZone(-1); + layer_surface.setAnchor(.{ .top = true, .right = true, .bottom = true, .left = true }); + + output.wl_surface = wl_surface; + output.layer_surface = layer_surface; + context.buffer_pool.surface_count += 1; + + layer_surface.setListener(*Output, wallpaperLayerSurfaceListener, output); + wl_surface.commit(); +} + +fn deinitWallpaperLayerSurface(output: *Output) void { + if (output.layer_surface) |layer_surface| { + layer_surface.destroy(); + } + if (output.wl_surface) |wl_surface| { + wl_surface.destroy(); + } + + output.layer_surface = null; + output.wl_surface = null; + output.configured = false; + output.context.buffer_pool.surface_count -= 1; +} + +fn wallpaperLayerSurfaceListener(layer_surface: *zwlr.LayerSurfaceV1, event: zwlr.LayerSurfaceV1.Event, output: *Output) void { + switch (event) { + .configure => |ev| { + layer_surface.ackConfigure(ev.serial); + + if (ev.width < 0 or ev.height < 0) { + // I'm not actually sure if this is possible, but just to be safe + log.warn("Received zwlr_layer_surface_v1.configure event with a negative width or height ({d}x{d})", .{ ev.width, ev.height }); + return; + } + const width: u31 = @intCast(ev.width); + const height: u31 = @intCast(ev.height); + + if (output.configured and output.render_width == width and output.render_height == height) { + if (output.wl_surface) |wl_surface| { + wl_surface.commit(); + } else { + log.warn("Output is marked as configured but is missing a layer_surface for the wallpaper", .{}); + } + return; + } + + log.debug("configuring wallpaper surface with width {} and height {}", .{ width, height }); + output.render_width = width; + output.render_height = height; + output.configured = true; + + output.renderWallpaper() catch |err| { + fatal("Wallpaper render failed: E{}", .{err}); + }; + }, + .closed => { + output.deinitWallpaperLayerSurface(); + }, + } +} + +/// Calculates image_dimension / (output_dimension * scale) +fn calculate_scale(image_dimension: c_int, output_dimension: u31, scale: u31) f64 { + const numerator: f64 = @floatFromInt(image_dimension); + const denominator: f64 = @floatFromInt(output_dimension * scale); + + return numerator / denominator; +} + +/// Calculates (image_dimension / dimension_scale - output_dimension) / 2 / dimension_scale; +fn calculate_transform(image_dimension: c_int, output_dimension: u31, dimension_scale: f64) f64 { + const numerator1: f64 = @floatFromInt(image_dimension); + const denominator1: f64 = dimension_scale; + const subtruend: f64 = @floatFromInt(output_dimension); + const numerator2: f64 = numerator1 / denominator1 - subtruend; + + return numerator2 / 2 / dimension_scale; +} + +/// Render the wallpaper image onto the layer surface +fn renderWallpaper(output: *Output) !void { + const context = output.context; + const width = output.render_width; + const height = output.render_height; + const scale = output.scale; + + // Don't have anything to render + if (width == 0 or height == 0 or scale == 0) { + return; + } + const buffer: *Buffer = try context.buffer_pool.nextBuffer(context.wl_shm, width * scale, height * scale); + + // Scale our loaded image and then copy it into the Buffer's pixman.Image + const image = context.wallpaper_image.image; + const image_data = image.getData(); + const image_width = image.getWidth(); + const image_height = image.getHeight(); + const image_stride = image.getStride(); + const image_format = image.getFormat(); + + const pix = pixman.Image.createBitsNoClear(image_format, image_width, image_height, image_data, image_stride); + if (pix == null) { + log.err("failed to copy the background image for rendering", .{}); + return error.ImageCopyError; + } + defer _ = pix.?.unref(); + + // Get scale for our image compared to the monitor's scale + // XXX: This sucks in Zig but also I'm sure there's a better way to write it + var sx: f64 = @as(f64, @floatFromInt(image_width)) / @as(f64, @floatFromInt(width * scale)); + var sy: f64 = calculate_scale(image_height, height, scale); + + const s = if (sx > sy) sy else sx; + sx = s; + sy = s; + + const tx: f64 = calculate_transform(image_width, width, sx); + const ty: f64 = calculate_transform(image_height, height, sy); + + var t: pixman.FTransform = undefined; + var t2: pixman.Transform = undefined; + + pixman.FTransform.initTranslate(&t, tx, ty); + pixman.FTransform.initScale(&t, sx, sy); + _ = pixman.Transform.fromFTransform(&t2, &t); + _ = pix.?.setTransform(&t2); + _ = pix.?.setFilter(.best, &[_]pixman.Fixed{}, 0); + + pixman.Image.composite32(.src, pix.?, null, buffer.pixman_image, 0, 0, 0, 0, 0, 0, width * scale, height * scale); + + log.info("render: {}x{} (scaled from {}x{})", .{ width * scale, height * scale, image_width, image_height }); + + // Attach the buffer to the surface + const wl_surface = output.wl_surface.?; + wl_surface.setBufferScale(scale); + wl_surface.attach(buffer.wl_buffer, 0, 0); + wl_surface.damageBuffer(0, 0, width * scale, height * scale); + wl_surface.commit(); +} + pub fn manage(output: *Output) void { defer output.pending_manage = .{}; @@ -316,13 +534,18 @@ fn calculatePrimaryStackLayout(output: *Output) void { const std = @import("std"); const assert = std.debug.assert; +const fatal = std.process.fatal; +const mem = std.mem; const DoublyLinkedList = std.DoublyLinkedList; const wayland = @import("wayland"); const wl = wayland.client.wl; const river = wayland.client.river; +const zwlr = wayland.client.zwlr; +const pixman = @import("pixman"); const utils = @import("utils.zig"); +const Buffer = @import("Buffer.zig"); const Context = @import("Context.zig"); const Window = @import("Window.zig"); diff --git a/src/Window.zig b/src/Window.zig index 5acf4ff..4116886 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -35,8 +35,7 @@ pending_manage: PendingManage = .{}, /// State consumed in render() phase, reset at end of render(). pending_render: PendingRender = .{}, -/// Used to put Windows into a list in -/// WindowManager.calculatePrimaryStackLayout() +/// Used to put Windows into a list in calculatePrimaryStackLayout() active_list_node: DoublyLinkedList.Node = .{}, link: wl.list.Link, @@ -141,10 +140,10 @@ fn windowListener(river_window_v1: *river.WindowV1, event: river.WindowV1.Event, window.destroy(); }, .dimensions => |ev| { - // The protocol requires these are strictly greater than zero. + // Protocol guarantees that width and height are strictly greater than zero assert(ev.width > 0 and ev.height > 0); - window.width = @intCast(ev.width); - window.height = @intCast(ev.height); + window.pending_manage.width = @intCast(ev.width); + window.pending_manage.height = @intCast(ev.height); }, .dimensions_hint => { // TODO: Maybe could use this for floating windows diff --git a/src/WindowManager.zig b/src/WindowManager.zig index 1bbacd4..8f14382 100644 --- a/src/WindowManager.zig +++ b/src/WindowManager.zig @@ -179,9 +179,11 @@ fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: riv const output = Output.create(context, ev.id) catch @panic("Out of memory"); wm.outputs.append(output); // If there was already a seat, but no outputs, set this new output as focused - const seat = wm.seats.first() orelse return; - if (seat.focused_output == null and seat.pending_manage.output == null) { - seat.pending_manage.output = .{ .output = output }; + const first_seat = wm.seats.first(); + if (first_seat) |seat| { + if (seat.focused_output == null and seat.pending_manage.output == null) { + seat.pending_manage.output = .{ .output = output }; + } } // If there are orphan windows, send them to the new output @@ -210,7 +212,9 @@ fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: riv wm.seats.append(seat); // If there was already an output, but no seats, set the first output as focused - seat.pending_manage.output = .{ .output = wm.outputs.first() orelse return }; + if (wm.outputs.first()) |output| { + seat.pending_manage.output = .{ .output = output }; + } }, .window => |ev| { // TODO: Support multiple seats diff --git a/src/main.zig b/src/main.zig index 2237899..55fcbf2 100644 --- a/src/main.zig +++ b/src/main.zig @@ -7,10 +7,20 @@ const Globals = struct { river_layer_shell_v1: ?*river.LayerShellV1 = null, river_window_manager_v1: ?*river.WindowManagerV1 = null, river_xkb_bindings_v1: ?*river.XkbBindingsV1 = null, + wl_compositor: ?*wl.Compositor = null, - wl_output: ?*wl.Output = null, wl_shm: ?*wl.Shm = null, + wl_outputs: std.AutoHashMapUnmanaged(u32, *wl.Output) = .empty, + zwlr_layer_shell_v1: ?*zwlr.LayerShellV1 = null, + + fn deinit(globals: *Globals) void { + var it = globals.wl_outputs.valueIterator(); + while (it.next()) |output| { + output.*.release(); + } + globals.wl_outputs.deinit(utils.allocator); + } }; pub fn main() !void { @@ -27,6 +37,7 @@ pub fn main() !void { const wl_registry = try wl_display.getRegistry(); var globals: Globals = .{}; + defer globals.deinit(); wl_registry.setListener(*Globals, registryListener, &globals); const errno = wl_display.roundtrip(); @@ -34,12 +45,15 @@ pub fn main() !void { fatal("Initial roundtrip failed: E{s}", .{@tagName(errno)}); } + const wl_compositor = globals.wl_compositor orelse utils.interfaceNotAdvertised(wl.Compositor); + const wl_shm = globals.wl_shm orelse utils.interfaceNotAdvertised(wl.Shm); + // We can theoretically start with zero wl_outputs; don't panic if it's empty. + const wl_outputs = &globals.wl_outputs; + const river_layer_shell_v1 = globals.river_layer_shell_v1 orelse utils.interfaceNotAdvertised(river.LayerShellV1); const river_window_manager_v1 = globals.river_window_manager_v1 orelse utils.interfaceNotAdvertised(river.WindowManagerV1); const river_xkb_bindings_v1 = globals.river_xkb_bindings_v1 orelse utils.interfaceNotAdvertised(river.XkbBindingsV1); - const wl_compositor = globals.wl_compositor orelse utils.interfaceNotAdvertised(wl.Compositor); - const wl_output = globals.wl_output orelse utils.interfaceNotAdvertised(wl.Output); - const wl_shm = globals.wl_shm orelse utils.interfaceNotAdvertised(wl.Shm); + const zwlr_layer_shell_v1 = globals.zwlr_layer_shell_v1 orelse utils.interfaceNotAdvertised(zwlr.LayerShellV1); const config = try Config.create(); @@ -47,7 +61,7 @@ pub fn main() !void { const context = try Context.create(.{ .wl_compositor = wl_compositor, .wl_display = wl_display, - .wl_output = wl_output, + .wl_outputs = wl_outputs, .wl_registry = wl_registry, .wl_shm = wl_shm, .river_layer_shell_v1 = river_layer_shell_v1, @@ -73,20 +87,46 @@ fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, globals: * if (mem.orderZ(u8, ev.interface, wl.Compositor.interface.name) == .eq) { if (ev.version < 4) utils.versionNotSupported(wl.Compositor, ev.version, 4); globals.wl_compositor = registry.bind(ev.name, wl.Compositor, 4) catch |e| { - fatal("Failed to bind to compositor: {any}", .{@errorName(e)}); + fatal("Failed to bind to wl_compositor: {any}", .{@errorName(e)}); + }; + } else if (mem.orderZ(u8, ev.interface, wl.Output.interface.name) == .eq) { + if (ev.version < 4) utils.versionNotSupported(wl.Output, ev.version, 4); + + const wl_output = registry.bind(ev.name, wl.Output, 4) catch |e| { + fatal("Failed to bind to wl_output: {any}", .{@errorName(e)}); + }; + + // We can get multiple wl_outputs, so we have to try add them to our HashMap + // instead of just keeping the one + globals.wl_outputs.put(utils.allocator, ev.name, wl_output) catch |e| { + fatal("Failed to add wl_output to hashmap: {any}", .{@errorName(e)}); + }; + } else if (mem.orderZ(u8, ev.interface, wl.Shm.interface.name) == .eq) { + globals.wl_shm = registry.bind(ev.name, wl.Shm, 1) catch |e| { + fatal("Failed to bind to wl_shm: {any}", .{@errorName(e)}); }; } else if (mem.orderZ(u8, ev.interface, river.WindowManagerV1.interface.name) == .eq) { globals.river_window_manager_v1 = registry.bind(ev.name, river.WindowManagerV1, 3) catch |e| { - fatal("Failed to bind to window_manager_v1: {any}", .{@errorName(e)}); + fatal("Failed to bind to river_window_manager_v1: {any}", .{@errorName(e)}); }; } else if (mem.orderZ(u8, ev.interface, river.XkbBindingsV1.interface.name) == .eq) { globals.river_xkb_bindings_v1 = registry.bind(ev.name, river.XkbBindingsV1, 2) catch |e| { - fatal("Failed to bind to xkb_bindings_v1: {any}", .{@errorName(e)}); + fatal("Failed to bind to river_xkb_bindings_v1: {any}", .{@errorName(e)}); + }; + } else if (mem.orderZ(u8, ev.interface, zwlr.LayerShellV1.interface.name) == .eq) { + if (ev.version < 3) utils.versionNotSupported(zwlr.LayerShellV1, ev.version, 3); + globals.zwlr_layer_shell_v1 = registry.bind(ev.name, zwlr.LayerShellV1, 3) catch |e| { + fatal("Failed to bind to zwlr_layer_shell_v1: {any}", .{@errorName(e)}); }; } }, // We don't need .global_remove - .global_remove => {}, + .global_remove => |ev| { + // The only remove we care about is for wl_outputs + if (!globals.wl_outputs.remove(ev.name)) { + log.debug("Received a global_remove event for something other than a wl_output", .{}); + } + }, } }