diff --git a/build.zig b/build.zig index d2d0363..03c2d0d 100644 --- a/build.zig +++ b/build.zig @@ -16,10 +16,12 @@ pub fn build(b: *std.Build) void { const scanner = Scanner.create(b, .{}); const wayland = b.createModule(.{ .root_source_file = scanner.result }); // Rest of the deps + const fcft = b.dependency("fcft", .{}).module("fcft"); const kdl = b.dependency("kdl", .{}).module("kdl"); const known_folders = b.dependency("known_folders", .{}).module("known-folders"); const pixman = b.dependency("pixman", .{}).module("pixman"); const xkbcommon = b.dependency("xkbcommon", .{}).module("xkbcommon"); + const zeit = b.dependency("zeit", .{}).module("zeit"); const zigimg = b.dependency("zigimg", .{}).module("zigimg"); scanner.addCustomProtocol(b.path("protocol/river-input-management-v1.xml")); @@ -57,14 +59,17 @@ pub fn build(b: *std.Build) void { beansprout.root_module.addOptions("build_options", options); beansprout.root_module.addImport("wayland", wayland); + beansprout.root_module.addImport("fcft", fcft); beansprout.root_module.addImport("kdl", kdl); beansprout.root_module.addImport("known_folders", known_folders); beansprout.root_module.addImport("pixman", pixman); beansprout.root_module.addImport("xkbcommon", xkbcommon); beansprout.root_module.addImport("zigimg", zigimg); + beansprout.root_module.addImport("zeit", zeit); beansprout.linkLibC(); beansprout.linkSystemLibrary("wayland-client"); + beansprout.linkSystemLibrary("fcft"); beansprout.linkSystemLibrary("pixman-1"); beansprout.linkSystemLibrary("xkbcommon"); @@ -117,6 +122,14 @@ const manifest: struct { url: []const u8, hash: []const u8, }, + fcft: struct { + url: []const u8, + hash: []const u8, + }, + zeit: struct { + url: []const u8, + hash: []const u8, + }, }, paths: []const []const u8, } = @import("build.zig.zon"); diff --git a/build.zig.zon b/build.zig.zon index 7d8ddc2..5f688aa 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -28,13 +28,21 @@ .hash = "known_folders-0.0.0-Fy-PJv3LAAABBRVoZWVrKZdyLoUfl5VRY5fqRRRdnF5L", }, .pixman = .{ - .url = "https://codeberg.org/ifreund/zig-pixman/archive/387f04a0289a69d159c88b5f70396ec12a8ddb00.tar.gz", - .hash = "pixman-0.4.0-dev-LClMn0eVAAAlXnMK-MVBZkOG0urNdOPGVQdaGecXPM5O", + .url = "https://codeberg.org/ifreund/zig-pixman/archive/v0.3.0.tar.gz", + .hash = "pixman-0.3.0-LClMnz2VAAAs7QSCGwLimV5VUYx0JFnX5xWU6HwtMuDX", }, .zigimg = .{ .url = "https://github.com/zigimg/zigimg/archive/fb74dfb7c6d83f2bd01a229826669451525a4ba8.tar.gz", .hash = "zigimg-0.1.0-8_eo2kSGFwADIkeZYTgfnLOV-khh6ZRoGmK6F2-s_QbY", }, + .fcft = .{ + .url = "https://git.sr.ht/~novakane/zig-fcft/archive/4bf5be61c869d08d5bcb0306049c63a9cb0795a7.tar.gz", + .hash = "fcft-3.0.0-zcx6CxQfAADhnwm8SjyCkQF-VFHGiVarigc2de3ciInC", + }, + .zeit = .{ + .url = "https://github.com/rockorager/zeit/archive/7ac64d72dbfb1a4ad549102e7d4e232a687d32d8.tar.gz", + .hash = "zeit-0.6.0-5I6bk36tAgATpSl9wjFmRPMqYN2Mn0JQHgIcRNcqDpJA", + }, }, .paths = .{ 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 a044508..6872db3 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -3,7 +3,8 @@ These are in rough order of my priority, though no promises I do them in this order. - [ ] Implement a river-tag-overlay clone -- [ ] Implement an optional clock bar +- [ ] Add options to the bar and river-tag-overlay +- [ ] 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 @@ -12,6 +13,7 @@ These are in rough order of my priority, though no promises I do them in this or - [ ] Support solid `background-color` fallback (no wallpaper) - [ ] Support per-output wallpapers - [ ] Support `focus-follows-cursor` granularity (`normal` vs `always`) +- [ ] Save window positions between restarts - [ ] Support multiple seats - [ ] Support clipping floating windows on edge of/between outputs - [x] Support changeable primary ratio @@ -26,3 +28,4 @@ These are in rough order of my priority, though no promises I do them in this or - [x] Support per-host config using properties - [x] Implement primary count/ratio per tagmask - [x] Add primary_count and primary_ratio to Config +- [x] Implement an optional clock bar diff --git a/src/Bar.zig b/src/Bar.zig new file mode 100644 index 0000000..3055092 --- /dev/null +++ b/src/Bar.zig @@ -0,0 +1,385 @@ +// SPDX-FileCopyrightText: 2026 Ben Buhse +// +// SPDX-License-Identifier: GPL-3.0-only + +const Bar = @This(); + +/// Standard base DPI at a scale of 1 +const base_dpi = 96; + +context: *Context, + +/// The timezone of the computer. +timezone: zeit.timezone.TimeZone, + +fonts: *fcft.Font, +font_scale: u31 = 0, + +output: *Output, + +// Bar geometry +width: u31 = 0, +height: u31 = 0, + +wl_surface: ?*wl.Surface = null, +layer_surface: ?*zwlr.LayerSurfaceV1 = null, + +configured: bool = false, + +pub fn init(context: *Context, output: *Output) !Bar { + // Get the local environment + // Needed for the timezone + // XXX: It might be better to store this in Context? + var env = try process.getEnvMap(utils.gpa); + defer env.deinit(); + + const timezone = try zeit.local(utils.gpa, &env); + + return .{ + .context = context, + .fonts = try getFcftFonts("monospace:size=14", 1), + .timezone = timezone, + .output = output, + }; +} + +// TODO: Add config options for whether it's top or bottom +pub fn initSurface(bar: *Bar) !void { + if (bar.layer_surface) |_| { + // This bar already has a layer surface, we can exit early + return; + } + + const context = bar.context; + + // TODO: Add padding to config + const vertical_padding = 5; + const bar_height: u31 = @intCast(bar.fonts.height + 2 * vertical_padding); + + const wl_surface = try context.wl_compositor.createSurface(); + errdefer wl_surface.destroy(); + + // TODO: Allow clicking on tags to switch between them? + // 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 layer_surface = try context + .zwlr_layer_shell_v1 + .getLayerSurface(wl_surface, bar.output.wl_output, .top, "beansprout-bar"); + layer_surface.setSize(0, bar_height); + layer_surface.setAnchor(.{ .top = true, .right = true, .left = true }); + + bar.wl_surface = wl_surface; + bar.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.wl_surface) |wl_surface| { + wl_surface.destroy(); + } + if (bar.layer_surface) |layer_surface| { + layer_surface.destroy(); + bar.context.buffer_pool.surface_count -= 1; + } +} + +pub fn layerSurfaceListener( + layer_surface: *zwlr.LayerSurfaceV1, + event: zwlr.LayerSurfaceV1.Event, + bar: *Bar, +) void { + switch (event) { + .configure => |ev| { + layer_surface.ackConfigure(ev.serial); + const width: u31 = @intCast(ev.width); + const height: u31 = @intCast(ev.height); + + if (bar.configured and + bar.width == width and + bar.height == height and + bar.output.scale == bar.font_scale) + { + if (bar.wl_surface) |wl_surface| { + wl_surface.commit(); + } else { + log.warn("Bar is marked as configured but is missing a layer_surface for the wallpaper", .{}); + } + return; + } + + log.debug("configuring bar surface with width {} and height {}", .{ width, height }); + bar.width = width; + bar.height = height; + // Excluse 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; + }; + // TODO: Need to change the x/y if we support anchoring to the bottom + opaque_region.add(0, 0, bar.width, bar.height); + defer opaque_region.destroy(); + bar.wl_surface.?.setOpaqueRegion(opaque_region); + + bar.configured = true; + + bar.render() catch |err| { + log.err("Bar render failed: {}", .{err}); + }; + }, + .closed => { + bar.deinit(); + }, + } +} + +// TODO: Configure number of visible tags +/// Renders the bar and its components +pub fn render(bar: *Bar) !void { + const context = bar.context; + + const scale = bar.output.scale; + + // Recreate fonts at the output's new scale + if (scale != bar.font_scale) { + bar.fonts.destroy(); + bar.fonts = try getFcftFonts("monospace:size=14", scale); + bar.font_scale = scale; + } + + // Scaled width/height + const render_width = bar.width * scale; + const render_height = bar.height * scale; + + // Don't have anything to render + if (render_width == 0 or render_height == 0 or scale == 0) { + return; + } + const buffer = try context.buffer_pool.nextBuffer(bar.context.wl_shm, render_width, render_height); + + // Fill with a solid color (e.g., dark background) + // TODO: Configure text/bg colors + const bg_color: pixman.Color = .{ .red = 0x1e1e, .green = 0x1e1e, .blue = 0x2e2e, .alpha = 0xffff }; + _ = pixman.Image.fillRectangles( + .src, + buffer.pixman_image, + &bg_color, + 1, + &[1]pixman.Rectangle16{.{ + .x = 0, + .y = 0, + .width = @intCast(render_width), + .height = @intCast(render_height), + }}, + ); + + // Set-up text color + const text_color: pixman.Color = .{ .red = 0xffff, .green = 0xffff, .blue = 0xffff, .alpha = 0xffff }; + const color = pixman.Image.createSolidFill(&text_color) orelse return error.FailedToCreatePixmanImage; + defer _ = color.unref(); + + // Get the current time in seconds since the epoch, + // then load the local timezone, + // then convert `now` to the `local` timezone + const now = try zeit.instant(.{}); + const now_local = now.in(&bar.timezone); + + // Generate date/time info for this instant + const dt = now_local.time(); + + // Convert time to a string + var buf: [255:0]u8 = undefined; + var fbs = io.fixedBufferStream(&buf); + // TODO: Support 12-hour clock (%I:%M) + try dt.strftime(fbs.writer(), "%H:%M"); + + // Convert ASCII text string to unicode + // XXX: Not sure if this even needs to be converted to unicode + var codepoint_it = (try unicode.Utf8View.init(fbs.getWritten())).iterator(); + const codepoint_count = try unicode.utf8CountCodepoints(fbs.getWritten()); + // We use u32 for fcft even if zig uses u21 + var codepoints: []u32 = try utils.gpa.alloc(u32, codepoint_count); + defer utils.gpa.free(codepoints); + var i: usize = 0; + while (codepoint_it.nextCodepoint()) |cp| : (i += 1) { + codepoints[i] = cp; + } + + const text_width = try bar.textWidth(codepoints); + var x: i32 = @divFloor(buffer.width - text_width, 2); + const y: i32 = @divFloor(buffer.height - bar.fonts.height, 2); + + // Actually render the unicode codepoints + try bar.renderChars(codepoints, buffer, &x, y, color); + + // Finally, attach the buffer to the surface + const wl_surface = bar.wl_surface orelse return; + wl_surface.setBufferScale(scale); + wl_surface.attach(buffer.wl_buffer, 0, 0); + wl_surface.damageBuffer(0, 0, render_width, render_height); + wl_surface.commit(); +} + +// TODO: This should be moved to utils once fonts are in config +/// Computes the pixel width of a text string. +fn textWidth(bar: *Bar, text: []const u32) !i32 { + var width: i32 = 0; + for (text, 0..) |cp, i| { + const glyph = try bar.fonts.rasterizeCharUtf32(cp, .default); + width += glyph.advance.x; + if (i > 0) { + var x_kern: c_long = 0; + if (bar.fonts.kerning(text[i - 1], cp, &x_kern, null)) { + width += @intCast(x_kern); + } + } + } + return width; +} + +// Borrowed and modified from https://git.sr.ht/~novakane/zig-fcft-example +fn renderChars( + bar: *Bar, + text: []const u32, + buffer: *Buffer, + x: *i32, + y: i32, + color: *pixman.Image, +) !void { + const glyphs = try utils.gpa.alloc(*const fcft.Glyph, text.len); + const kerns = try utils.gpa.alloc(c_long, text.len); + defer utils.gpa.free(glyphs); + defer utils.gpa.free(kerns); + + var i: usize = 0; + while (i < text.len) : (i += 1) { + glyphs[i] = try bar.fonts.rasterizeCharUtf32(text[i], .default); + + kerns[i] = 0; + if (i > 0) { + var x_kern: c_long = 0; + if (bar.fonts.kerning(text[i - 1], text[i], &x_kern, null)) { + kerns[i] = x_kern; + } + } + } + + bar.renderGlyphs(buffer, x, y, color, text.len, glyphs.ptr, kerns); +} + +// Borrowed https://git.sr.ht/~novakane/zig-fcft-example +fn renderGlyphs( + bar: *Bar, + buffer: *Buffer, + x: *i32, + y: i32, + color: *pixman.Image, + len: usize, + glyphs: [*]*const fcft.Glyph, + kerns: ?[]c_long, +) void { + var i: usize = 0; + while (i < len) : (i += 1) { + if (kerns) |kern| x.* += @intCast(kern[i]); + + switch (pixman.Image.getFormat(glyphs[i].pix)) { + // Glyph is a pre-rendered image. (e.g. a color emoji) + .a8r8g8b8 => { + pixman.Image.composite32( + .over, + glyphs[i].pix, + null, + buffer.pixman_image, + 0, + 0, + 0, + 0, + x.* + @as(i32, @intCast(glyphs[i].x)), + y + bar.fonts.ascent - @as(i32, @intCast(glyphs[i].y)), + glyphs[i].width, + glyphs[i].height, + ); + }, + // Glyph is an alpha mask. + else => { + pixman.Image.composite32( + .over, + color, + glyphs[i].pix, + buffer.pixman_image, + 0, + 0, + 0, + 0, + x.* + @as(i32, @intCast(glyphs[i].x)), + y + bar.fonts.ascent - @as(i32, @intCast(glyphs[i].y)), + glyphs[i].width, + glyphs[i].height, + ); + }, + } + + x.* += glyphs[i].advance.x; + } +} + +// Borrowed and modified from https://git.sr.ht/~novakane/zig-fcft-example +fn getFcftFonts(fonts: []const u8, scale: u31) !*fcft.Font { + // Create an arena to free just for this function; + // It makes cleaning up the ArrayList easier. + var arena = std.heap.ArenaAllocator.init(utils.gpa); + defer arena.deinit(); + const arena_alloc = arena.allocator(); + + var list = try std.ArrayList([*:0]const u8).initCapacity(arena_alloc, 2); + + var it = mem.tokenizeScalar(u8, fonts, ','); + while (it.next()) |font| { + if (scale > 1) { + // If scale >1, we append :dpi so we can scale the font + const scaled = try arena_alloc.dupeZ( + u8, + try std.fmt.allocPrint(arena_alloc, "{s}:dpi={}", .{ font, @as(u32, base_dpi) * scale }), + ); + try list.append(arena_alloc, scaled); + } else { + try list.append(arena_alloc, try arena_alloc.dupeZ(u8, font)); + } + } + + const fcft_fonts = try fcft.Font.fromName(list.items[0..], null); + errdefer fcft_fonts.destroy(); + fcft_fonts.setEmojiPresentation(.default); + + return fcft_fonts; +} + +const std = @import("std"); +const io = std.io; +const mem = std.mem; +const process = std.process; +const unicode = std.unicode; + +const wayland = @import("wayland"); +const wl = wayland.client.wl; +const zwlr = wayland.client.zwlr; +const fcft = @import("fcft"); +const pixman = @import("pixman"); +const zeit = @import("zeit"); + +const utils = @import("utils.zig"); +const Buffer = @import("Buffer.zig"); +const Context = @import("Context.zig"); +const Output = @import("Output.zig"); + +const log = std.log.scoped(.Bar); diff --git a/src/Buffer.zig b/src/Buffer.zig index 9d8c7f7..f588ee8 100644 --- a/src/Buffer.zig +++ b/src/Buffer.zig @@ -61,7 +61,7 @@ pub fn init(shm: *wl.Shm, width: u31, height: u31) !Buffer { @as(c_int, @intCast(height)), @as([*c]u32, @ptrCast(data)), @as(c_int, @intCast(stride)), - ) orelse return error.NoPixmanImage; + ) orelse return error.FailedToCreatePixmanImage; // The pixman image and the Wayland buffer now share the same memory. return .{ diff --git a/src/Config.zig b/src/Config.zig index 8ac3d59..be6746b 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -477,7 +477,22 @@ fn loadKeybindsChildBlock(config: *Config, parser: *kdl.Parser, hostname: ?[]con logWarnMissingNodeArg(name, "command"); continue; }); - const split_exec = try utils.tokenizeToOwnedSlices(exec_str, ' '); + var split_exec = try utils.tokenizeToOwnedSlices(exec_str, ' '); + if (split_exec.len > 0) { + // Expand ~ in executable paths + const expanded = expandTilde(split_exec[0]) catch |e| { + if (e == error.HomeNotSet) { + // No ~, just return what we had. + break :sw .{ .spawn = split_exec }; + } else { + return e; + } + }; + // tokenizeToOwnedSlices dupes each token, so we have to + // free the original value before replacing it. + utils.gpa.free(split_exec[0]); + split_exec[0] = expanded; + } break :sw .{ .spawn = split_exec }; }, .change_ratio => { @@ -506,6 +521,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/InputDevice.zig b/src/InputDevice.zig index b63eee0..ed72384 100644 --- a/src/InputDevice.zig +++ b/src/InputDevice.zig @@ -19,7 +19,7 @@ link: wl.list.Link, pub fn create(river_input_device_v1: *river.InputDeviceV1) !*InputDevice { const input_device = try utils.gpa.create(InputDevice); - errdefer input_device.destroy(); + errdefer utils.gpa.destroy(input_device); input_device.* = .{ .river_input_device_v1 = river_input_device_v1, diff --git a/src/InputManager.zig b/src/InputManager.zig index f5cbfb1..b99a3c5 100644 --- a/src/InputManager.zig +++ b/src/InputManager.zig @@ -18,7 +18,7 @@ libinput_devices: wl.list.Head(LibinputDevice, .link), pub fn create(context: *Context, river_input_manager_v1: *river.InputManagerV1, river_libinput_config_v1: *river.LibinputConfigV1) !*InputManager { log.debug("Creating new InputManager", .{}); const im = try utils.gpa.create(InputManager); - errdefer im.destroy(); + errdefer utils.gpa.destroy(im); im.* = .{ .context = context, diff --git a/src/LibinputDevice.zig b/src/LibinputDevice.zig index cb4ca97..0f693a0 100644 --- a/src/LibinputDevice.zig +++ b/src/LibinputDevice.zig @@ -79,7 +79,7 @@ link: wl.list.Link, pub fn create(context: *Context, river_libinput_device_v1: *river.LibinputDeviceV1) !*LibinputDevice { const libinput_device = try utils.gpa.create(LibinputDevice); - errdefer libinput_device.destroy(); + errdefer utils.gpa.destroy(libinput_device); libinput_device.* = .{ .context = context, @@ -107,7 +107,6 @@ pub fn destroy(libinput_device: *LibinputDevice) void { fn riverLibinputDeviceV1Listener(river_libinput_device_v1: *river.LibinputDeviceV1, event: river.LibinputDeviceV1.Event, libinput_device: *LibinputDevice) void { assert(libinput_device.river_libinput_device_v1 == river_libinput_device_v1); const im = libinput_device.context.im; - log.debug("bwbuhse: {s} for {d}", .{ @tagName(event), river_libinput_device_v1.getId() }); switch (event) { .removed => { river_libinput_device_v1.destroy(); diff --git a/src/Output.zig b/src/Output.zig index 8f4204c..02df08a 100644 --- a/src/Output.zig +++ b/src/Output.zig @@ -7,23 +7,38 @@ 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, +// Friendly name of this output +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 -render_width: u31 = 0, -render_height: u31 = 0, +wallpaper_render_scale: u31 = 0, +wallpaper_render_width: u31 = 0, +wallpaper_render_height: u31 = 0, wl_surface: ?*wl.Surface = null, layer_surface: ?*zwlr.LayerSurfaceV1 = null, +// TODO: Make Bar a user option, can disable if they want +// This Output's bar +bar: ?Bar, + /// Proportion of output width taken by the primary stack primary_ratio: f32, @@ -41,9 +56,6 @@ 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, @@ -58,10 +70,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, @@ -70,11 +87,18 @@ pub const PendingManage = struct { pub fn create(context: *Context, river_output_v1: *river.OutputV1) !*Output { var output = try utils.gpa.create(Output); - errdefer output.destroy(); + errdefer utils.gpa.destroy(output); + + const bar = Bar.init(context, output) catch |e| blk: { + log.err("Failed to create a bar: {}", .{e}); + break :blk null; + }; 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, .windows = undefined, // we will initialize this shortly @@ -84,6 +108,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; } @@ -94,10 +119,10 @@ pub fn destroy(output: *Output) void { window.link.remove(); window.destroy(); } - output.tag_layout_overrides.deinit(utils.gpa); output.deinitWallpaperLayerSurface(); output.river_output_v1.destroy(); + output.river_layer_shell_output_v1.destroy(); utils.gpa.destroy(output); } @@ -188,14 +213,20 @@ fn riverOutputListener(river_output_v1: *river.OutputV1, event: river.OutputV1.E output.wl_output = output.context.wl_outputs.get(ev.name).?; output.wl_output.?.setListener(*Output, wlOutputListener, output); - // The wl_output's initial events (mode, scale, name, done) were likely - // already delivered during the initial roundtrip before we set our - // listener, so the .done event that triggers wallpaper init was lost. - // Explicitly init the wallpaper surface here. + // The wl_output's initial events come during the initial roundtrip + // before we set our listener, so the .done event that triggers + // wallpaper init was lost. Explicitly init the surfaces here. output.initWallpaperLayerSurface() catch |err| { const output_name = output.name orelse "some output"; log.err("failed to add a surface to {s}: {}", .{ output_name, err }); }; + 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; + }; + } }, .dimensions => |ev| { // Protocol guarantees that width and height are strictly greater than zero @@ -229,6 +260,25 @@ fn wlOutputListener(_: *wl.Output, event: wl.Output.Event, output: *Output) void log.err("failed to add a surface to {s}: {}", .{ output_name, err }); 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}); + }; + } + } + // Re-render wallpaper if scale changed + if (output.configured and output.scale != output.wallpaper_render_scale) { + output.renderWallpaper() catch |err| { + log.err("Wallpaper render failed: {}", .{err}); + }; + } }, .scale => |ev| { if (ev.factor < 0) { @@ -245,32 +295,52 @@ 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 return; } - if (output.wl_surface) |_| { + if (output.layer_surface) |_| { // This output already has a layer surface, we can exit early return; } const context = output.context; - const wl_surface: *wl.Surface = try context.wl_compositor.createSurface(); + const wl_surface = try context.wl_compositor.createSurface(); + errdefer wl_surface.destroy(); // We don't want our surface to have any input region (default is infinite) - const empty_region: *wl.Region = try context.wl_compositor.createRegion(); + const empty_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(); + const opaque_region = try context.wl_compositor.createRegion(); + opaque_region.add(0, 0, output.width, output.height); 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"); + const layer_surface = try context.zwlr_layer_shell_v1.getLayerSurface(wl_surface, output.wl_output, .background, "beansprout-wallpaper"); layer_surface.setExclusiveZone(-1); layer_surface.setAnchor(.{ .top = true, .right = true, .bottom = true, .left = true }); @@ -301,15 +371,14 @@ fn wallpaperLayerSurfaceListener(layer_surface: *zwlr.LayerSurfaceV1, event: zwl .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.configured and + output.wallpaper_render_width == width and + output.wallpaper_render_height == height and + output.scale == output.wallpaper_render_scale) + { if (output.wl_surface) |wl_surface| { wl_surface.commit(); } else { @@ -319,8 +388,8 @@ fn wallpaperLayerSurfaceListener(layer_surface: *zwlr.LayerSurfaceV1, event: zwl } log.debug("configuring wallpaper surface with width {} and height {}", .{ width, height }); - output.render_width = width; - output.render_height = height; + output.wallpaper_render_width = width; + output.wallpaper_render_height = height; output.configured = true; output.renderWallpaper() catch |err| { @@ -334,15 +403,15 @@ fn wallpaperLayerSurfaceListener(layer_surface: *zwlr.LayerSurfaceV1, event: zwl } /// Calculates image_dimension / (output_dimension * scale) -fn calculate_scale(image_dimension: c_int, output_dimension: u31, scale: u31) f64 { +fn calculateScale(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 { +/// Calculates (image_dimension / dimension_scale - output_dimension) / 2 / dimension_scale +fn calculateTransform(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); @@ -354,8 +423,8 @@ fn calculate_transform(image_dimension: c_int, output_dimension: u31, dimension_ /// Render the wallpaper image onto the layer surface pub fn renderWallpaper(output: *Output) !void { const context = output.context; - const width = output.render_width; - const height = output.render_height; + const width = output.wallpaper_render_width; + const height = output.wallpaper_render_height; const scale = output.scale; // Don't have anything to render @@ -375,13 +444,13 @@ pub fn renderWallpaper(output: *Output) !void { const pix = pixman.Image.createBitsNoClear(image_format, image_width, image_height, image_data, image_stride) orelse { log.err("Failed to copy the wallpaper image for rendering", .{}); - return error.ImageCopyError; + return error.FailedToCreatePixmanImage; }; defer _ = pix.unref(); // Calculate image scale var sx: f64 = @as(f64, @floatFromInt(image_width)) / @as(f64, @floatFromInt(width * scale)); - var sy: f64 = calculate_scale(image_height, height, scale); + var sy: f64 = calculateScale(image_height, height, scale); const s = if (sx > sy) sy else sx; sx = s; @@ -389,8 +458,8 @@ pub fn renderWallpaper(output: *Output) !void { // Calculate translation offsets to center the image on the output. // If the scaled image is larger than the output, the offset crops equally from both sides. - const tx: f64 = calculate_transform(image_width, width, sx); - const ty: f64 = calculate_transform(image_height, height, sy); + const tx: f64 = calculateTransform(image_width, width * scale, sx); + const ty: f64 = calculateTransform(image_height, height * scale, sy); // Build a combined source-to-destination transform matrix. // Pixman transforms map destination pixels back to source pixels, so: @@ -421,22 +490,37 @@ pub fn renderWallpaper(output: *Output) !void { wl_surface.attach(buffer.wl_buffer, 0, 0); wl_surface.damageBuffer(0, 0, width * scale, height * scale); wl_surface.commit(); + + output.wallpaper_render_scale = scale; } 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| { @@ -522,12 +606,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 @@ -601,6 +684,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; @@ -613,6 +705,7 @@ const zwlr = wayland.client.zwlr; const pixman = @import("pixman"); const utils = @import("utils.zig"); +const Bar = @import("Bar.zig"); const Buffer = @import("Buffer.zig"); const Context = @import("Context.zig"); const Window = @import("Window.zig"); diff --git a/src/Seat.zig b/src/Seat.zig index 37b3d59..0bcda3e 100644 --- a/src/Seat.zig +++ b/src/Seat.zig @@ -7,9 +7,11 @@ const Seat = @This(); context: *Context, river_seat_v1: *river.SeatV1, +river_layer_shell_seat_v1: *river.LayerShellSeatV1, focused_window: ?*Window, focused_output: ?*Output, +layer_focus: LayerFocus = .focus_none, pointer_op: PointerOp = .none, @@ -22,9 +24,14 @@ link: wl.list.Link, move_pointer_binding: ?*river.PointerBindingV1 = null, resize_pointer_binding: ?*river.PointerBindingV1 = null, +// We just steal the Event's tag type to use as our enum. If another event is added +// that's *not* for focus, we'll have to create our own enum and just keep it in sync. +pub const LayerFocus = @typeInfo(river.LayerShellSeatV1.Event).@"union".tag_type.?; + pub const PendingManage = struct { window: ?PendingWindow = null, output: ?PendingOutput = null, + layer_focus: ?LayerFocus = null, should_warp_pointer: bool = false, op_delta: ?struct { dx: i32, dy: i32 } = null, @@ -58,17 +65,19 @@ pub const PointerOp = union(enum) { pub fn create(context: *Context, river_seat_v1: *river.SeatV1) !*Seat { var seat = try utils.gpa.create(Seat); - errdefer seat.destroy(); + errdefer utils.gpa.destroy(seat); seat.* = .{ .context = context, .river_seat_v1 = river_seat_v1, + .river_layer_shell_seat_v1 = try context.river_layer_shell_v1.getSeat(river_seat_v1), .focused_window = null, .focused_output = null, .link = undefined, // Handled by the wl.list }; seat.river_seat_v1.setListener(*Seat, seatListener, seat); + seat.river_layer_shell_seat_v1.setListener(*Seat, riverLayerShellSeatListener, seat); return seat; } @@ -77,6 +86,7 @@ pub fn destroy(seat: *Seat) void { if (seat.move_pointer_binding) |binding| binding.destroy(); if (seat.resize_pointer_binding) |binding| binding.destroy(); seat.river_seat_v1.destroy(); + seat.river_layer_shell_seat_v1.destroy(); utils.gpa.destroy(seat); } @@ -103,6 +113,11 @@ fn seatListener(river_seat_v1: *river.SeatV1, event: river.SeatV1.Event, seat: * } } +fn riverLayerShellSeatListener(river_layer_shell_seat_v1: *river.LayerShellSeatV1, event: river.LayerShellSeatV1.Event, seat: *Seat) void { + assert(seat.river_layer_shell_seat_v1 == river_layer_shell_seat_v1); + seat.pending_manage.layer_focus = std.meta.activeTag(event); +} + // river_window_v1 needs to be optional because ev.window is optional fn setWindowFocus(seat: *Seat, river_window_v1: ?*river.WindowV1) void { const wv1 = river_window_v1 orelse return; @@ -116,33 +131,37 @@ fn setWindowFocus(seat: *Seat, river_window_v1: ?*river.WindowV1) void { pub fn manage(seat: *Seat) void { defer seat.pending_manage = .{}; - if (seat.pending_manage.window) |pending_window| { - switch (pending_window) { - .window => |window| { - if (seat.focused_window) |focused| { - // Tell the previously focused Window that it's no longer focused - if (focused != window) { + // Focus events are ignored by the compositor when a layer shell has exclusive focus. + if (seat.layer_focus != .focus_exclusive) { + if (seat.pending_manage.window) |pending_window| { + switch (pending_window) { + .window => |window| { + if (seat.focused_window) |focused| { + // Tell the previously focused Window that it's no longer focused + if (focused != window) { + focused.pending_render.focused = false; + } + } + seat.focused_window = window; + seat.river_seat_v1.focusWindow(window.river_window_v1); + window.pending_render.focused = true; + }, + .clear_focus => { + if (seat.focused_window) |focused| { + // Tell the previously focused Window that it's no longer focused focused.pending_render.focused = false; } - } - seat.focused_window = window; - seat.river_seat_v1.focusWindow(window.river_window_v1); - window.pending_render.focused = true; - }, - .clear_focus => { - if (seat.focused_window) |focused| { - // Tell the previously focused Window that it's no longer focused - focused.pending_render.focused = false; - } - seat.focused_window = null; - seat.river_seat_v1.clearFocus(); - }, + seat.focused_window = null; + seat.river_seat_v1.clearFocus(); + }, + } } } if (seat.pending_manage.output) |pending_output| { switch (pending_output) { .output => |output| { seat.focused_output = output; + output.river_layer_shell_output_v1.setDefault(); }, .clear_focus => { seat.focused_output = null; @@ -150,18 +169,42 @@ pub fn manage(seat: *Seat) void { } } - if (seat.pending_manage.should_warp_pointer) blk: { - if (seat.context.config.pointer_warp_on_focus_change) { - const window = seat.focused_window orelse { - log.warn("Trying to warp-on-focus-change without a focused window.", .{}); - break :blk; - }; - // 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); - seat.river_seat_v1.pointerWarp(pointer_x, pointer_y); + // Handle layer focus changes after window focus so focused_window is up to date. + // This overrides whatever pending_render.focused the window focus block just set. + if (seat.pending_manage.layer_focus) |layer_focus| { + seat.layer_focus = layer_focus; + switch (layer_focus) { + .focus_exclusive, + .focus_non_exclusive, + => { + if (seat.focused_window) |focused_window| { + focused_window.pending_render.focused = false; + } + }, + .focus_none => { + if (seat.focused_window) |focused_window| { + seat.river_seat_v1.focusWindow(focused_window.river_window_v1); + focused_window.pending_render.focused = true; + } + }, + } + } + + // Focus doesn't change during .focus_exclusive, so pointer shouldn't get warped, either + if (seat.layer_focus != .focus_exclusive) { + if (seat.pending_manage.should_warp_pointer) blk: { + if (seat.context.config.pointer_warp_on_focus_change) { + const window = seat.focused_window orelse { + log.warn("Trying to warp-on-focus-change without a focused window.", .{}); + break :blk; + }; + // 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) + @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 +247,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 +272,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 c6a135a..d3c7f1e 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -69,7 +69,7 @@ pub const PendingRender = struct { pub fn create(context: *Context, river_window_v1: *river.WindowV1, output: ?*Output) !*Window { var window = try utils.gpa.create(Window); - errdefer window.destroy(); + errdefer utils.gpa.destroy(window); window.* = .{ .context = context, @@ -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/WindowManager.zig b/src/WindowManager.zig index f736a7c..78a3461 100644 --- a/src/WindowManager.zig +++ b/src/WindowManager.zig @@ -18,7 +18,7 @@ orphan_windows: wl.list.Head(Window, .link), pub fn create(context: *Context, window_manager_v1: *river.WindowManagerV1) !*WindowManager { const wm = try utils.gpa.create(WindowManager); - errdefer wm.destroy(); + errdefer utils.gpa.destroy(wm); wm.* = .{ .context = context, diff --git a/src/XkbBindings.zig b/src/XkbBindings.zig index 26032d0..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, @@ -56,7 +59,7 @@ const XkbBinding = struct { fn create(xkb_binding_v1: *river.XkbBindingV1, command: Command, context: *Context) !*XkbBinding { var xkb_binding = try utils.gpa.create(XkbBinding); - errdefer xkb_binding.destroy(); + errdefer utils.gpa.destroy(xkb_binding); xkb_binding.* = .{ .xkb_binding_v1 = xkb_binding_v1, @@ -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(); } @@ -405,7 +402,7 @@ bindings: wl.list.Head(XkbBinding, .link), pub fn create(context: *Context, xkb_bindings_v1: *river.XkbBindingsV1) !*XkbBindings { const xkb_bindings = try utils.gpa.create(XkbBindings); - errdefer xkb_bindings.destroy(); + errdefer utils.gpa.destroy(xkb_bindings); xkb_bindings.* = .{ .context = context, diff --git a/src/flags.zig b/src/flags.zig index f7f9536..6cfffff 100644 --- a/src/flags.zig +++ b/src/flags.zig @@ -11,7 +11,7 @@ pub const Flag = struct { kind: enum { boolean, arg }, }; -pub fn parser(comptime Arg: type, comptime flags: []const Flag) type { +pub fn Parser(comptime Arg: type, comptime flags: []const Flag) type { switch (Arg) { // TODO consider allowing []const u8 [:0]const u8, [*:0]const u8 => {}, // ok diff --git a/src/main.zig b/src/main.zig index 82a3ef5..0caa199 100644 --- a/src/main.zig +++ b/src/main.zig @@ -37,46 +37,17 @@ const usage: []const u8 = ; pub fn main() !void { - const result = flags.parser([*:0]const u8, &.{ - .{ .name = "h", .kind = .boolean }, - .{ .name = "version", .kind = .boolean }, - .{ .name = "log-level", .kind = .arg }, - }).parse(std.os.argv[1..]) catch { - try stderr.writeAll(usage); - try stderr.flush(); - posix.exit(1); - }; - if (result.flags.h) { - try stdout.writeAll(usage); - try stdout.flush(); - posix.exit(0); - } - if (result.args.len != 0) { - log.err("unknown option '{s}'", .{result.args[0]}); - try stderr.writeAll(usage); - try stderr.flush(); - posix.exit(1); - } + parseArgs(); - if (result.flags.version) { - try stdout.writeAll(build_options.version ++ "\n"); - try stdout.flush(); - posix.exit(0); - } - if (result.flags.@"log-level") |level| { - if (mem.eql(u8, level, "error")) { - runtime_log_level = .err; - } else if (mem.eql(u8, level, "warning")) { - runtime_log_level = .warn; - } else if (mem.eql(u8, level, "info")) { - runtime_log_level = .info; - } else if (mem.eql(u8, level, "debug")) { - runtime_log_level = .debug; - } else { - log.err("invalid log level '{s}'", .{level}); - posix.exit(1); - } - } + // Initialize fcft + const fcft_log_level: fcft.LogClass = switch (runtime_log_level) { + .err => .err, + .warn => .warning, + .info => .info, + .debug => .debug, + }; + _ = fcft.init(.auto, false, fcft_log_level); + defer fcft.fini(); const wayland_display_var = try utils.gpa.dupeZ(u8, process.getEnvVarOwned(utils.gpa, "WAYLAND_DISPLAY") catch { fatal("Error getting WAYLAND_DISPLAY environment variable. Exiting", .{}); @@ -130,13 +101,130 @@ pub fn main() !void { }); defer context.destroy(); + try run(wl_display, context); +} + +/// Function to handle the main event loop +/// +/// Since we've added a bar with a clock,we need +fn run(wl_display: *wl.Display, context: *Context) !void { + var mask = posix.sigemptyset(); + + posix.sigaddset(&mask, posix.SIG.INT); + posix.sigaddset(&mask, posix.SIG.QUIT); + + posix.sigprocmask(posix.SIG.BLOCK, &mask, null); + + const sig_fd = try posix.signalfd(-1, &mask, 0); + + const poll_wayland = 0; + const poll_sig = 1; + + var pollfds: [2]posix.pollfd = undefined; + + pollfds[poll_wayland] = .{ + .fd = wl_display.getFd(), + .events = posix.POLL.IN, + .revents = 0, + }; + pollfds[poll_sig] = .{ + .fd = sig_fd, + .events = posix.POLL.IN, + .revents = 0, + }; + while (true) { - if (wl_display.dispatch() != .SUCCESS) { - fatal("wayland display dispatch failed", .{}); + const errno = wl_display.flush(); + if (errno != .SUCCESS) { + fatal("wl_display flush failed: E{s}", .{@tagName(errno)}); + } + + // Get the number of milliseconds to the top of the next minute + const time = std.time.timestamp(); + if (time < 0) { + log.err("Got a negative time ({d})", .{time}); + return error.InvalidTime; + } + 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)}); + }; + if (poll_rc == 0) { + // If poll returns 0, it timed out, meaning we hit the top of the minute + // and need to update the clock. + 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}); + }; + } + } + } + + // Handle fds that became ready + if (pollfds[poll_wayland].revents & posix.POLL.HUP != 0) { + log.info("Disconnected by compositor", .{}); + break; + } + if (pollfds[poll_wayland].revents & posix.POLL.IN != 0) { + if (wl_display.dispatch() != .SUCCESS) { + fatal("Wayland display dispatch failed", .{}); + } + } + + if (pollfds[poll_sig].revents & posix.POLL.HUP != 0) { + fatal("Signal fd hung up", .{}); + } + if (pollfds[poll_sig].revents & posix.POLL.IN != 0) { + log.info("Exiting beansprout", .{}); + break; } } +} - log.info("Exiting beansprout", .{}); +fn parseArgs() void { + const result = flags.Parser([*:0]const u8, &.{ + .{ .name = "h", .kind = .boolean }, + .{ .name = "version", .kind = .boolean }, + .{ .name = "log-level", .kind = .arg }, + }).parse(os.argv[1..]) catch { + stderr.writeAll(usage) catch {}; + stderr.flush() catch {}; + posix.exit(1); + }; + if (result.flags.h) { + stdout.writeAll(usage) catch {}; + stdout.flush() catch {}; + posix.exit(0); + } + if (result.args.len != 0) { + log.err("unknown option '{s}'", .{result.args[0]}); + stderr.writeAll(usage) catch {}; + stderr.flush() catch {}; + posix.exit(1); + } + + if (result.flags.version) { + stdout.writeAll(build_options.version ++ "\n") catch {}; + stdout.flush() catch {}; + posix.exit(0); + } + if (result.flags.@"log-level") |level| { + if (mem.eql(u8, level, "error")) { + runtime_log_level = .err; + } else if (mem.eql(u8, level, "warning")) { + runtime_log_level = .warn; + } else if (mem.eql(u8, level, "info")) { + runtime_log_level = .info; + } else if (mem.eql(u8, level, "debug")) { + runtime_log_level = .debug; + } else { + log.err("invalid log level '{s}'", .{level}); + posix.exit(1); + } + } } fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, globals: *Globals) void { @@ -239,6 +327,7 @@ const std = @import("std"); const fatal = std.process.fatal; const fs = std.fs; const mem = std.mem; +const os = std.os; const posix = std.posix; const process = std.process; @@ -246,6 +335,7 @@ const wayland = @import("wayland"); const river = wayland.client.river; const wl = wayland.client.wl; const zwlr = wayland.client.zwlr; +const fcft = @import("fcft"); const flags = @import("flags.zig"); const utils = @import("utils.zig"); diff --git a/src/utils.zig b/src/utils.zig index 7691461..94cadba 100644 --- a/src/utils.zig +++ b/src/utils.zig @@ -103,7 +103,7 @@ pub fn parseModifiers(s: []const u8) !?river.SeatV1.Modifiers { return modifiers; } -pub fn tokenizeToOwnedSlices(input: []const u8, delimiter: u8) ![]const []const u8 { +pub fn tokenizeToOwnedSlices(input: []const u8, delimiter: u8) ![][]const u8 { var list: std.ArrayList([]const u8) = .empty; var it = std.mem.tokenizeScalar(u8, input, delimiter); while (it.next()) |part| {