Merge pull request 'Implement river-tag-overlay clone' (#13) from tag-overlay into main
Reviewed-on: https://codeberg.org/bwbuhse/beansprout/pulls/13
This commit is contained in:
commit
c1c9eb24f7
13 changed files with 1031 additions and 185 deletions
|
|
@ -27,6 +27,7 @@ Needed at both build-time and runtime:
|
|||
| wayland-client | `dev-libs/wayland` | `libwayland-dev` |
|
||||
| pixman | `x11-libs/pixman` | `libpixman-1-dev` |
|
||||
| xkbcommon | `x11-libs/libxkbcommon` | `libxkbcommon-dev` |
|
||||
| fcft | `media-libs/fcft` | `libfcft-dev` |
|
||||
|
||||
Only needed at build-time:
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ wallpaper_image_path "~/Pictures/wallpaper.png"
|
|||
|------------------------------|--------|---------|-----------------------------------------------------|
|
||||
| `attach_mode` | enum | `top` | Where new windows go in the stack (`top` or `bottom`) |
|
||||
| `primary_count` | u8 | `1` | Number of windows in the primary stack (0+) |
|
||||
| `primary_ratio` | float | `0.55` | Proportion of output width for the primary stack (0.10–0.90) |
|
||||
| `primary_ratio` | float | `0.55` | Proportion of output width for the primary stack (0.10-0.90) |
|
||||
| `focus_follows_pointer` | bool | `#true` | Focus follows the pointer between windows |
|
||||
| `pointer_warp_on_focus_change` | bool | `#true` | Warp pointer to center of newly-focused windows |
|
||||
| `wallpaper_image_path` | string | none | Path to wallpaper image |
|
||||
|
|
@ -71,6 +71,62 @@ borders {
|
|||
|
||||
Colors are specified in `0xRRGGBB` or `0xRRGGBBAA` hex format.
|
||||
|
||||
## Tag Overlay
|
||||
|
||||
The tag overlay is an optional widget that briefly shows your tag state when switching tags.
|
||||
It is only created when a `tag_overlay` block is present in the config. All settings have
|
||||
defaults, with the color based on the Catppuccin Mocha theme. An empty block can be used
|
||||
to enable the widget with all defaults:
|
||||
|
||||
```kdl
|
||||
tag_overlay {
|
||||
}
|
||||
```
|
||||
|
||||
### Tag Overlay Settings
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
|-------------------------------------|-------|--------------|-------------------------------------------|
|
||||
| `border_width` | u8 | `2` | Widget border width in pixels |
|
||||
| `tag_amount` | u8 | `9` | Number of displayed tags (1-32) |
|
||||
| `tags_per_row` | u8 | `32` | Tags per row (1-32) |
|
||||
| `square_size` | u8 | `40` | Size of tag squares in pixels |
|
||||
| `square_inner_padding` | u8 | `10` | Padding around occupied indicator |
|
||||
| `square_padding` | u8 | `15` | Padding around tag squares |
|
||||
| `square_border_width` | u8 | `1` | Border width of tag squares |
|
||||
| `timeout` | u32 | `500` | Display duration in milliseconds |
|
||||
| `background_color` | color | `0x1e1e2e` | Widget background color |
|
||||
| `border_color` | color | `0x6c7086` | Widget border color |
|
||||
| `square_active_background_color` | color | `0x89b4fa` | Active tag square background |
|
||||
| `square_active_border_color` | color | `0x6c7086` | Active tag square border |
|
||||
| `square_active_occupied_color` | color | `0xcdd6f4` | Active tag occupied indicator |
|
||||
| `square_inactive_background_color` | color | `0x585b70` | Inactive tag square background |
|
||||
| `square_inactive_border_color` | color | `0x6c7086` | Inactive tag square border |
|
||||
| `square_inactive_occupied_color` | color | `0xcdd6f4` | Inactive tag occupied indicator |
|
||||
|
||||
### Anchors
|
||||
|
||||
The `anchors` child block controls which edge(s) of the screen the overlay
|
||||
attaches to. Each direction is a boolean (`#true` / `#false`). Default: none, i.e. centered on output.
|
||||
|
||||
| Setting | Type | Default |
|
||||
|----------|------|----------|
|
||||
| `top` | bool | `#false` |
|
||||
| `right` | bool | `#false` |
|
||||
| `bottom` | bool | `#false` |
|
||||
| `left` | bool | `#false` |
|
||||
|
||||
### Margins
|
||||
|
||||
The `margins` child block sets pixel offsets from the anchored edge(s).
|
||||
|
||||
| Setting | Type | Default |
|
||||
|----------|------|---------|
|
||||
| `top` | i32 | `0` |
|
||||
| `right` | i32 | `0` |
|
||||
| `bottom` | i32 | `0` |
|
||||
| `left` | i32 | `0` |
|
||||
|
||||
## Keybinds
|
||||
|
||||
Keyboard bindings are placed inside a `keybinds` block. Each binding has the
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@
|
|||
|
||||
These are in rough order of my priority, though no promises I do them in this order.
|
||||
|
||||
- [ ] Implement a river-tag-overlay clone
|
||||
- [ ] Add options to the bar and river-tag-overlay
|
||||
- [ ] Add options to the bar
|
||||
- [ ] Make a Rect struct to combine x, y, width, and height
|
||||
- [ ] Support window rules (float/tags/SSD by app-id/title)
|
||||
- [ ] Support overriding config location
|
||||
|
|
@ -16,6 +15,7 @@ These are in rough order of my priority, though no promises I do them in this or
|
|||
- [ ] Save window positions between restarts
|
||||
- [ ] Support multiple seats
|
||||
- [ ] Support clipping floating windows on edge of/between outputs
|
||||
- [ ] Use per-output timerfds for tag overlay instead of a single shared one
|
||||
- [x] Support changeable primary ratio
|
||||
- [x] Support changeable primary count
|
||||
- [x] Support multiple outputs
|
||||
|
|
@ -29,3 +29,5 @@ These are in rough order of my priority, though no promises I do them in this or
|
|||
- [x] Implement primary count/ratio per tagmask
|
||||
- [x] Add primary_count and primary_ratio to Config
|
||||
- [x] Implement an optional clock bar
|
||||
- [x] Implement a river-tag-overlay clone
|
||||
- [x] Add options to the tag overlay
|
||||
|
|
|
|||
|
|
@ -18,6 +18,19 @@ borders {
|
|||
color_focused "0x89b4fa"
|
||||
color_unfocused "0x1e1e2e"
|
||||
}
|
||||
// Tag overlay widget — shown briefly when switching tags
|
||||
// Remove this block to disable the overlay entirely
|
||||
tag_overlay {
|
||||
tag_amount 10
|
||||
background_color "0x1e1e2e"
|
||||
border_color "0x6c7086"
|
||||
square_active_background_color "0x89b4fa"
|
||||
square_active_border_color "0x6c7086"
|
||||
square_active_occupied_color "0xcdd6f4"
|
||||
square_inactive_background_color "0x585b70"
|
||||
square_inactive_border_color "0x6c7086"
|
||||
square_inactive_occupied_color "0xcdd6f4"
|
||||
}
|
||||
keybinds {
|
||||
// Swap a window
|
||||
spawn Mod4 T foot
|
||||
|
|
|
|||
73
src/Bar.zig
73
src/Bar.zig
|
|
@ -21,23 +21,23 @@ output: *Output,
|
|||
width: u31 = 0,
|
||||
height: u31 = 0,
|
||||
|
||||
wl_surface: ?*wl.Surface = null,
|
||||
layer_surface: ?*zwlr.LayerSurfaceV1 = null,
|
||||
surfaces: ?struct {
|
||||
wl_surface: *wl.Surface,
|
||||
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, &context.env);
|
||||
errdefer timezone.deinit();
|
||||
|
||||
const timezone = try zeit.local(utils.gpa, &env);
|
||||
const fonts = try getFcftFonts("monospace:size=14", 1);
|
||||
errdefer fonts.destroy();
|
||||
|
||||
return .{
|
||||
.context = context,
|
||||
.fonts = try getFcftFonts("monospace:size=14", 1),
|
||||
.fonts = fonts,
|
||||
.timezone = timezone,
|
||||
.output = output,
|
||||
};
|
||||
|
|
@ -45,34 +45,37 @@ pub fn init(context: *Context, output: *Output) !Bar {
|
|||
|
||||
// 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
|
||||
if (bar.surfaces) |_| {
|
||||
// This bar already has a 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();
|
||||
|
||||
const layer_surface = try context
|
||||
.zwlr_layer_shell_v1
|
||||
.getLayerSurface(wl_surface, bar.output.wl_output, .top, "beansprout-bar");
|
||||
errdefer layer_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");
|
||||
// TODO: Add padding to config
|
||||
const vertical_padding = 5;
|
||||
const bar_height: u31 = @intCast(bar.fonts.height + 2 * vertical_padding);
|
||||
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;
|
||||
bar.surfaces = .{
|
||||
.wl_surface = wl_surface,
|
||||
.layer_surface = layer_surface,
|
||||
};
|
||||
context.buffer_pool.surface_count += 1;
|
||||
|
||||
layer_surface.setListener(*Bar, layerSurfaceListener, bar);
|
||||
|
|
@ -82,11 +85,10 @@ pub fn initSurface(bar: *Bar) !void {
|
|||
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.fonts.destroy();
|
||||
if (bar.surfaces) |surfaces| {
|
||||
surfaces.wl_surface.destroy();
|
||||
surfaces.layer_surface.destroy();
|
||||
bar.context.buffer_pool.surface_count -= 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -96,6 +98,7 @@ pub fn layerSurfaceListener(
|
|||
event: zwlr.LayerSurfaceV1.Event,
|
||||
bar: *Bar,
|
||||
) void {
|
||||
assert(bar.surfaces.?.layer_surface == layer_surface);
|
||||
switch (event) {
|
||||
.configure => |ev| {
|
||||
layer_surface.ackConfigure(ev.serial);
|
||||
|
|
@ -107,18 +110,14 @@ pub fn layerSurfaceListener(
|
|||
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", .{});
|
||||
}
|
||||
bar.surfaces.?.wl_surface.commit();
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug("configuring bar surface with width {} and height {}", .{ width, height });
|
||||
log.debug("Configuring bar surface with width {} and height {}", .{ width, height });
|
||||
bar.width = width;
|
||||
bar.height = height;
|
||||
// Excluse zone == the bar's height
|
||||
// Exclusive zone == the bar's height
|
||||
layer_surface.setExclusiveZone(bar.height);
|
||||
|
||||
// Full surface should be opaque
|
||||
|
|
@ -126,10 +125,9 @@ pub fn layerSurfaceListener(
|
|||
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);
|
||||
opaque_region.add(0, 0, bar.width, bar.height);
|
||||
bar.surfaces.?.wl_surface.setOpaqueRegion(opaque_region);
|
||||
|
||||
bar.configured = true;
|
||||
|
||||
|
|
@ -143,7 +141,6 @@ pub fn layerSurfaceListener(
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Configure number of visible tags
|
||||
/// Renders the bar and its components
|
||||
pub fn render(bar: *Bar) !void {
|
||||
const context = bar.context;
|
||||
|
|
@ -223,7 +220,8 @@ pub fn render(bar: *Bar) !void {
|
|||
try bar.renderChars(codepoints, buffer, &x, y, color);
|
||||
|
||||
// Finally, attach the buffer to the surface
|
||||
const wl_surface = bar.wl_surface orelse return;
|
||||
const surfaces = bar.surfaces orelse return error.NoSurfaces;
|
||||
const wl_surface = surfaces.wl_surface;
|
||||
wl_surface.setBufferScale(scale);
|
||||
wl_surface.attach(buffer.wl_buffer, 0, 0);
|
||||
wl_surface.damageBuffer(0, 0, render_width, render_height);
|
||||
|
|
@ -365,6 +363,7 @@ fn getFcftFonts(fonts: []const u8, scale: u31) !*fcft.Font {
|
|||
}
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const io = std.io;
|
||||
const mem = std.mem;
|
||||
const process = std.process;
|
||||
|
|
|
|||
|
|
@ -84,15 +84,55 @@ pub fn deinit(buffer: *Buffer) void {
|
|||
|
||||
// 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);
|
||||
buffer.wl_buffer.setListener(*Buffer, bufferListener, buffer);
|
||||
}
|
||||
|
||||
fn buffer_listener(_: *wl.Buffer, event: wl.Buffer.Event, buffer: *Buffer) void {
|
||||
fn bufferListener(_: *wl.Buffer, event: wl.Buffer.Event, buffer: *Buffer) void {
|
||||
switch (event) {
|
||||
.release => buffer.busy = false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn borderedRectangle(
|
||||
buffer: Buffer,
|
||||
x: u31,
|
||||
y: u31,
|
||||
width: u31,
|
||||
height: u31,
|
||||
border_width: u31,
|
||||
scale: u31,
|
||||
background_color: *const pixman.Color,
|
||||
border_color: *const pixman.Color,
|
||||
) void {
|
||||
const render_x: i16 = @intCast(x * scale);
|
||||
const render_y: i16 = @intCast(y * scale);
|
||||
const render_width: u16 = @intCast(width * scale);
|
||||
const render_height: u16 = @intCast(height * scale);
|
||||
const render_border_width: u16 = @intCast(border_width * scale);
|
||||
|
||||
// Background fill
|
||||
_ = pixman.Image.fillRectangles(.src, buffer.pixman_image, background_color, 1, &[1]pixman.Rectangle16{.{ .x = render_x, .y = render_y, .width = render_width, .height = render_height }});
|
||||
|
||||
// Border: top, bottom, left, right
|
||||
_ = pixman.Image.fillRectangles(.src, buffer.pixman_image, border_color, 4, &[4]pixman.Rectangle16{
|
||||
.{
|
||||
.x = render_x,
|
||||
.y = render_y,
|
||||
.width = render_width,
|
||||
.height = render_border_width,
|
||||
},
|
||||
.{
|
||||
.x = render_x,
|
||||
.y = render_y + @as(i16, @intCast(render_height -
|
||||
render_border_width)),
|
||||
.width = render_width,
|
||||
.height = render_border_width,
|
||||
},
|
||||
.{ .x = render_x, .y = render_y + @as(i16, @intCast(render_border_width)), .width = render_border_width, .height = render_height - 2 * render_border_width },
|
||||
.{ .x = render_x + @as(i16, @intCast(render_width - render_border_width)), .y = render_y + @as(i16, @intCast(render_border_width)), .width = render_border_width, .height = render_height - 2 * render_border_width },
|
||||
});
|
||||
}
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const mem = std.mem;
|
||||
|
|
|
|||
269
src/Config.zig
269
src/Config.zig
|
|
@ -31,6 +31,9 @@ pointer_warp_on_focus_change: bool = true,
|
|||
/// Path to the wallpaper image
|
||||
wallpaper_image_path: ?[]const u8 = null,
|
||||
|
||||
/// Tag overlay configuration. If null, no overlay is created.
|
||||
tag_overlay: ?TagOverlayConfig = null,
|
||||
|
||||
/// Tag bind entries parsed from config (tag_bind nodes in keybinds block)
|
||||
tag_binds: std.ArrayList(Keybind) = .{},
|
||||
keybinds: std.ArrayList(Keybind) = .{},
|
||||
|
|
@ -85,6 +88,66 @@ pub const AttachMode = enum {
|
|||
bottom,
|
||||
};
|
||||
|
||||
pub const TagOverlayConfig = struct {
|
||||
border_width: u8 = 2,
|
||||
tag_amount: u8 = 9,
|
||||
tags_per_row: u8 = 32,
|
||||
square_size: u8 = 40,
|
||||
square_inner_padding: u8 = 10,
|
||||
square_padding: u8 = 15,
|
||||
square_border_width: u8 = 1,
|
||||
background_color: pixman.Color = utils.parseRgbaPixmanComptime("0x1e1e2e"),
|
||||
border_color: pixman.Color = utils.parseRgbaPixmanComptime("0x6c7086"),
|
||||
square_active_background_color: pixman.Color = utils.parseRgbaPixmanComptime("0x89b4fa"),
|
||||
square_active_border_color: pixman.Color = utils.parseRgbaPixmanComptime("0x6c7086"),
|
||||
square_active_occupied_color: pixman.Color = utils.parseRgbaPixmanComptime("0xcdd6f4"),
|
||||
square_inactive_background_color: pixman.Color = utils.parseRgbaPixmanComptime("0x585b70"),
|
||||
square_inactive_border_color: pixman.Color = utils.parseRgbaPixmanComptime("0x6c7086"),
|
||||
square_inactive_occupied_color: pixman.Color = utils.parseRgbaPixmanComptime("0xcdd6f4"),
|
||||
timeout: u32 = 500,
|
||||
anchor_top: bool = false,
|
||||
anchor_right: bool = false,
|
||||
anchor_bottom: bool = false,
|
||||
anchor_left: bool = false,
|
||||
margin_top: i32 = 0,
|
||||
margin_right: i32 = 0,
|
||||
margin_bottom: i32 = 0,
|
||||
margin_left: i32 = 0,
|
||||
|
||||
pub fn toTagOverlayOptions(self: TagOverlayConfig) TagOverlay.Options {
|
||||
return .{
|
||||
.border_width = self.border_width,
|
||||
.tag_amount = @intCast(std.math.clamp(@as(u32, self.tag_amount), 1, 32)),
|
||||
.tags_per_row = @intCast(std.math.clamp(@as(u32, self.tags_per_row), 1, 32)),
|
||||
.square_size = self.square_size,
|
||||
.square_inner_padding = self.square_inner_padding,
|
||||
.square_padding = self.square_padding,
|
||||
.square_border_width = self.square_border_width,
|
||||
.background_color = self.background_color,
|
||||
.border_color = self.border_color,
|
||||
.square_active_background_color = self.square_active_background_color,
|
||||
.square_active_border_color = self.square_active_border_color,
|
||||
.square_active_occupied_color = self.square_active_occupied_color,
|
||||
.square_inactive_background_color = self.square_inactive_background_color,
|
||||
.square_inactive_border_color = self.square_inactive_border_color,
|
||||
.square_inactive_occupied_color = self.square_inactive_occupied_color,
|
||||
.anchors = .{
|
||||
.top = self.anchor_top,
|
||||
.right = self.anchor_right,
|
||||
.bottom = self.anchor_bottom,
|
||||
.left = self.anchor_left,
|
||||
},
|
||||
.margins = .{
|
||||
.top = self.margin_top,
|
||||
.right = self.margin_right,
|
||||
.bottom = self.margin_bottom,
|
||||
.left = self.margin_left,
|
||||
},
|
||||
.timeout = self.timeout,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const NodeName = enum {
|
||||
attach_mode,
|
||||
primary_count,
|
||||
|
|
@ -97,6 +160,7 @@ const NodeName = enum {
|
|||
keybinds,
|
||||
pointer_binds,
|
||||
input,
|
||||
tag_overlay,
|
||||
};
|
||||
|
||||
const BorderNodeName = enum {
|
||||
|
|
@ -105,6 +169,30 @@ const BorderNodeName = enum {
|
|||
color_unfocused,
|
||||
};
|
||||
|
||||
const TagOverlayNodeName = enum {
|
||||
border_width,
|
||||
tag_amount,
|
||||
tags_per_row,
|
||||
square_size,
|
||||
square_inner_padding,
|
||||
square_padding,
|
||||
square_border_width,
|
||||
background_color,
|
||||
border_color,
|
||||
square_active_background_color,
|
||||
square_active_border_color,
|
||||
square_active_occupied_color,
|
||||
square_inactive_background_color,
|
||||
square_inactive_border_color,
|
||||
square_inactive_occupied_color,
|
||||
timeout,
|
||||
anchors,
|
||||
margins,
|
||||
};
|
||||
|
||||
const TagOverlayAnchorsNodeName = enum { top, right, bottom, left };
|
||||
const TagOverlayMarginsNodeName = enum { top, right, bottom, left };
|
||||
|
||||
const PointerBindNodeName = enum {
|
||||
move_window,
|
||||
resize_window,
|
||||
|
|
@ -315,6 +403,9 @@ fn load(config: *Config, reader: *Io.Reader) !void {
|
|||
null;
|
||||
next_child_block = .input;
|
||||
},
|
||||
.tag_overlay => {
|
||||
next_child_block = .tag_overlay;
|
||||
},
|
||||
}
|
||||
} else {
|
||||
logWarnInvalidNode(node.name);
|
||||
|
|
@ -330,6 +421,7 @@ fn load(config: *Config, reader: *Io.Reader) !void {
|
|||
try config.loadInputChildBlock(&parser, pending_input_name, hostname);
|
||||
pending_input_name = null; // ownership transferred
|
||||
},
|
||||
.tag_overlay => try config.loadTagOverlayChildBlock(&parser, hostname),
|
||||
else => {
|
||||
// Nothing else should ever be marked as a next_child_block
|
||||
unreachable;
|
||||
|
|
@ -398,16 +490,162 @@ fn loadBordersChildBlock(config: *Config, parser: *kdl.Parser, hostname: ?[]cons
|
|||
}
|
||||
}
|
||||
|
||||
fn loadTagOverlayChildBlock(config: *Config, parser: *kdl.Parser, hostname: ?[]const u8) !void {
|
||||
config.tag_overlay = .{}; // Presence of block = enabled; initialize with defaults
|
||||
|
||||
const TagOverlayChild = enum { anchors, margins };
|
||||
var next_child_block: ?TagOverlayChild = null;
|
||||
|
||||
while (try parser.next()) |event| {
|
||||
switch (event) {
|
||||
.node => |node| {
|
||||
if (next_child_block) |child| {
|
||||
log.warn("Expected child block for tag_overlay.{s}, got node instead. Ignoring", .{@tagName(child)});
|
||||
next_child_block = null;
|
||||
}
|
||||
const node_name = std.meta.stringToEnum(TagOverlayNodeName, node.name);
|
||||
if (node_name) |name| {
|
||||
if (!hostMatches(node, parser, hostname)) {
|
||||
logDebugHostMismatch(name);
|
||||
continue;
|
||||
}
|
||||
const val_str = utils.stripQuotes(node.arg(parser, 0) orelse "");
|
||||
switch (name) {
|
||||
.anchors => next_child_block = .anchors,
|
||||
.margins => next_child_block = .margins,
|
||||
// These are all u8s, so we can inline the branch
|
||||
inline .border_width,
|
||||
.tag_amount,
|
||||
.tags_per_row,
|
||||
.square_size,
|
||||
.square_inner_padding,
|
||||
.square_padding,
|
||||
.square_border_width,
|
||||
=> |tag| {
|
||||
const val = fmt.parseInt(u8, val_str, 10) catch {
|
||||
logWarnInvalidNodeArg(name, val_str);
|
||||
continue;
|
||||
};
|
||||
@field(config.tag_overlay.?, @tagName(tag)) = val;
|
||||
logDebugSettingNode(name, val_str);
|
||||
},
|
||||
.timeout => {
|
||||
config.tag_overlay.?.timeout = fmt.parseInt(u32, val_str, 10) catch {
|
||||
logWarnInvalidNodeArg(name, val_str);
|
||||
continue;
|
||||
};
|
||||
logDebugSettingNode(name, val_str);
|
||||
},
|
||||
inline .background_color,
|
||||
.border_color,
|
||||
.square_active_background_color,
|
||||
.square_active_border_color,
|
||||
.square_active_occupied_color,
|
||||
.square_inactive_background_color,
|
||||
.square_inactive_border_color,
|
||||
.square_inactive_occupied_color,
|
||||
=> |tag| {
|
||||
@field(config.tag_overlay.?, @tagName(tag)) = utils.parseRgbaPixman(val_str) catch {
|
||||
logWarnInvalidNodeArg(name, val_str);
|
||||
continue;
|
||||
};
|
||||
logDebugSettingNode(name, val_str);
|
||||
},
|
||||
}
|
||||
} else {
|
||||
logWarnInvalidNode(node.name);
|
||||
}
|
||||
},
|
||||
.child_block_begin => {
|
||||
if (next_child_block) |child| {
|
||||
switch (child) {
|
||||
.anchors => try config.loadTagOverlayAnchorsBlock(parser, hostname),
|
||||
.margins => try config.loadTagOverlayMarginsBlock(parser, hostname),
|
||||
}
|
||||
next_child_block = null;
|
||||
} else {
|
||||
try config.skipChildBlock(parser);
|
||||
}
|
||||
},
|
||||
.child_block_end => return,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn loadTagOverlayAnchorsBlock(config: *Config, parser: *kdl.Parser, hostname: ?[]const u8) !void {
|
||||
while (try parser.next()) |event| {
|
||||
switch (event) {
|
||||
.node => |node| {
|
||||
const node_name = std.meta.stringToEnum(TagOverlayAnchorsNodeName, node.name);
|
||||
if (node_name) |name| {
|
||||
if (!hostMatches(node, parser, hostname)) {
|
||||
logDebugHostMismatch(name);
|
||||
continue;
|
||||
}
|
||||
const val_str = utils.stripQuotes(node.arg(parser, 0) orelse "");
|
||||
if (boolFromKdlStr(val_str)) |val| {
|
||||
switch (name) {
|
||||
.top => config.tag_overlay.?.anchor_top = val,
|
||||
.right => config.tag_overlay.?.anchor_right = val,
|
||||
.bottom => config.tag_overlay.?.anchor_bottom = val,
|
||||
.left => config.tag_overlay.?.anchor_left = val,
|
||||
}
|
||||
logDebugSettingNode(name, val_str);
|
||||
} else {
|
||||
logWarnInvalidNodeArg(name, val_str);
|
||||
}
|
||||
} else {
|
||||
logWarnInvalidNode(node.name);
|
||||
}
|
||||
},
|
||||
.child_block_begin => try config.skipChildBlock(parser),
|
||||
.child_block_end => return,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn loadTagOverlayMarginsBlock(config: *Config, parser: *kdl.Parser, hostname: ?[]const u8) !void {
|
||||
while (try parser.next()) |event| {
|
||||
switch (event) {
|
||||
.node => |node| {
|
||||
const node_name = std.meta.stringToEnum(TagOverlayMarginsNodeName, node.name);
|
||||
if (node_name) |name| {
|
||||
if (!hostMatches(node, parser, hostname)) {
|
||||
logDebugHostMismatch(name);
|
||||
continue;
|
||||
}
|
||||
const val_str = utils.stripQuotes(node.arg(parser, 0) orelse "");
|
||||
const val = fmt.parseInt(i32, val_str, 10) catch {
|
||||
logWarnInvalidNodeArg(name, val_str);
|
||||
continue;
|
||||
};
|
||||
switch (name) {
|
||||
.top => config.tag_overlay.?.margin_top = val,
|
||||
.right => config.tag_overlay.?.margin_right = val,
|
||||
.bottom => config.tag_overlay.?.margin_bottom = val,
|
||||
.left => config.tag_overlay.?.margin_left = val,
|
||||
}
|
||||
logDebugSettingNode(name, val_str);
|
||||
} else {
|
||||
logWarnInvalidNode(node.name);
|
||||
}
|
||||
},
|
||||
.child_block_begin => try config.skipChildBlock(parser),
|
||||
.child_block_end => return,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn loadKeybindsChildBlock(config: *Config, parser: *kdl.Parser, hostname: ?[]const u8) !void {
|
||||
while (try parser.next()) |event| {
|
||||
switch (event) {
|
||||
.node => |node| {
|
||||
if (!hostMatches(node, parser, hostname)) {
|
||||
logDebugHostMismatch(node.name);
|
||||
continue;
|
||||
}
|
||||
// tag_bind is a special case node name
|
||||
// tag_bind is a special case node name not in KeybindNodeName
|
||||
if (mem.eql(u8, node.name, "tag_bind")) {
|
||||
if (!hostMatches(node, parser, hostname)) {
|
||||
log.debug("Skipping \"keybind.tag_bind\" (host mismatch)", .{});
|
||||
continue;
|
||||
}
|
||||
const mod_str = utils.stripQuotes(node.arg(parser, 0) orelse {
|
||||
log.warn("tag_bind: missing modifier argument. Ignoring", .{});
|
||||
continue;
|
||||
|
|
@ -443,13 +681,16 @@ fn loadKeybindsChildBlock(config: *Config, parser: *kdl.Parser, hostname: ?[]con
|
|||
.keysym = null, // Tag binds don't need a keysym (automatically 1-9)
|
||||
});
|
||||
|
||||
// TODO: Make a logger for keybind settings
|
||||
log.debug("tag_bind: {s} {s}", .{ mod_str, cmd_str });
|
||||
continue;
|
||||
}
|
||||
// Handle the rest of the possiblities like all the other types of block
|
||||
// Handle the rest of the possibilities like all the other types of block
|
||||
const node_name = std.meta.stringToEnum(KeybindNodeName, node.name);
|
||||
if (node_name) |name| {
|
||||
if (!hostMatches(node, parser, hostname)) {
|
||||
logDebugHostMismatch(name);
|
||||
continue;
|
||||
}
|
||||
// All nodes should have at least a command, modifiers, and a keysym
|
||||
// Some may have more arguments handled in the switch
|
||||
const mod_str = utils.stripQuotes(node.arg(parser, 0) orelse {
|
||||
|
|
@ -787,6 +1028,9 @@ fn logWarnInvalidNodeArg(node_name: anytype, node_value: []const u8) void {
|
|||
KeybindNodeName => log.warn("Invalid \"keybind.{s}\" ({s}). Ignoring", .{ @tagName(node_name), node_value }),
|
||||
PointerBindNodeName => log.warn("Invalid \"pointer_binds.{s}\" ({s}). Ignoring", .{ @tagName(node_name), node_value }),
|
||||
InputConfigNodeName => log.warn("Invalid \"input.{s}\" ({s}). Ignoring", .{ @tagName(node_name), node_value }),
|
||||
TagOverlayNodeName => log.warn("Invalid \"tag_overlay.{s}\" ({s}). Using default value", .{ @tagName(node_name), node_value }),
|
||||
TagOverlayAnchorsNodeName => log.warn("Invalid \"tag_overlay.anchors.{s}\" ({s}). Using default value", .{ @tagName(node_name), node_value }),
|
||||
TagOverlayMarginsNodeName => log.warn("Invalid \"tag_overlay.margins.{s}\" ({s}). Using default value", .{ @tagName(node_name), node_value }),
|
||||
else => @compileError("This function does not (yet) support type \"" ++ @typeName(node_name_type) ++ "\""),
|
||||
}
|
||||
}
|
||||
|
|
@ -798,6 +1042,7 @@ fn logWarnMissingNodeArg(node_name: anytype, comptime arg: []const u8) void {
|
|||
KeybindNodeName => log.warn("\"keybind.{s}\" missing " ++ arg ++ " argument. Ignoring", .{@tagName(node_name)}),
|
||||
PointerBindNodeName => log.warn("\"pointer_binds.{s}\" missing " ++ arg ++ " argument. Ignoring", .{@tagName(node_name)}),
|
||||
InputConfigNodeName => log.warn("\"input.{s}\" missing " ++ arg ++ " argument. Ignoring", .{@tagName(node_name)}),
|
||||
TagOverlayNodeName => log.warn("\"tag_overlay.{s}\" missing " ++ arg ++ " argument. Ignoring", .{@tagName(node_name)}),
|
||||
else => @compileError("This function does not (yet) support type \"" ++ @typeName(node_name_type) ++ "\""),
|
||||
}
|
||||
}
|
||||
|
|
@ -821,7 +1066,10 @@ fn logDebugHostMismatch(node_name: anytype) void {
|
|||
BorderNodeName => log.debug("Skipping \"border.{s}\" (host mismatch)", .{@tagName(node_name)}),
|
||||
PointerBindNodeName => log.debug("Skipping \"pointer_binds.{s}\" (host mismatch)", .{@tagName(node_name)}),
|
||||
InputConfigNodeName => log.debug("Skipping \"input.{s}\" (host mismatch)", .{@tagName(node_name)}),
|
||||
[]const u8 => log.debug("Skipping \"keybind.{s}\" (host mismatch)", .{node_name}),
|
||||
TagOverlayNodeName => log.debug("Skipping \"tag_overlay.{s}\" (host mismatch)", .{@tagName(node_name)}),
|
||||
TagOverlayAnchorsNodeName => log.debug("Skipping \"tag_overlay.anchors.{s}\" (host mismatch)", .{@tagName(node_name)}),
|
||||
TagOverlayMarginsNodeName => log.debug("Skipping \"tag_overlay.margins.{s}\" (host mismatch)", .{@tagName(node_name)}),
|
||||
KeybindNodeName => log.debug("Skipping \"keybind.{s}\" (host mismatch)", .{@tagName(node_name)}),
|
||||
else => @compileError("This function does not (yet) support type \"" ++ @typeName(node_name_type) ++ "\""),
|
||||
}
|
||||
}
|
||||
|
|
@ -831,6 +1079,9 @@ fn logDebugSettingNode(node_name: anytype, node_value: []const u8) void {
|
|||
switch (node_name_type) {
|
||||
NodeName => log.debug("Setting {s} to {s}", .{ @tagName(node_name), node_value }),
|
||||
BorderNodeName => log.debug("Setting border.{s} to {s}", .{ @tagName(node_name), node_value }),
|
||||
TagOverlayNodeName => log.debug("Setting tag_overlay.{s} to {s}", .{ @tagName(node_name), node_value }),
|
||||
TagOverlayAnchorsNodeName => log.debug("Setting tag_overlay.anchors.{s} to {s}", .{ @tagName(node_name), node_value }),
|
||||
TagOverlayMarginsNodeName => log.debug("Setting tag_overlay.margins.{s} to {s}", .{ @tagName(node_name), node_value }),
|
||||
else => @compileError("This function does not (yet) support type \"" ++ @typeName(@TypeOf(node_name)) ++ "\""),
|
||||
}
|
||||
}
|
||||
|
|
@ -879,10 +1130,12 @@ const ThreeFingerDragState = river.LibinputDeviceV1.ThreeFingerDragState;
|
|||
|
||||
const kdl = @import("kdl");
|
||||
const known_folders = @import("known_folders");
|
||||
const pixman = @import("pixman");
|
||||
const xkbcommon = @import("xkbcommon");
|
||||
|
||||
const utils = @import("utils.zig");
|
||||
const RiverColor = utils.RiverColor;
|
||||
const TagOverlay = @import("TagOverlay.zig");
|
||||
const XkbBindings = @import("XkbBindings.zig");
|
||||
|
||||
const log = std.log.scoped(.Config);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ const Context = @This();
|
|||
|
||||
initialized: bool,
|
||||
|
||||
env: process.EnvMap,
|
||||
|
||||
// Wayland globals
|
||||
wl_compositor: *wl.Compositor,
|
||||
wl_display: *wl.Display,
|
||||
|
|
@ -32,6 +34,10 @@ wallpaper_image: ?*WallpaperImage,
|
|||
// WM Configuration
|
||||
config: *Config,
|
||||
|
||||
/// Shared timerfd for hiding tag overlays after their timeout expires.
|
||||
/// This stays null if no tag overlays exist.
|
||||
tag_overlay_timer_fd: ?posix.fd_t,
|
||||
|
||||
/// State consumed in manage() phase, reset at end of manage().
|
||||
pending_manage: PendingManage = .{},
|
||||
|
||||
|
|
@ -60,10 +66,29 @@ pub const Options = struct {
|
|||
|
||||
pub fn create(options: Options) !*Context {
|
||||
const context = try utils.gpa.create(Context);
|
||||
errdefer context.destroy();
|
||||
errdefer utils.gpa.destroy(context);
|
||||
|
||||
const im = try InputManager.create(context, options.river_input_manager_v1, options.river_libinput_config_v1);
|
||||
errdefer im.destroy();
|
||||
const wm = try WindowManager.create(context, options.river_window_manager_v1);
|
||||
errdefer wm.destroy();
|
||||
const xkb_bindings = try XkbBindings.create(context, options.river_xkb_bindings_v1);
|
||||
errdefer xkb_bindings.destroy();
|
||||
|
||||
const env = try process.getEnvMap(utils.gpa);
|
||||
errdefer env.deinit();
|
||||
|
||||
const tag_overlay_timer_fd: ?posix.fd_t = if (options.config.tag_overlay) |_|
|
||||
posix.timerfd_create(.MONOTONIC, .{ .CLOEXEC = true }) catch |e| blk: {
|
||||
log.err("Failed to create tag overlay timer: {}", .{e});
|
||||
break :blk null;
|
||||
}
|
||||
else
|
||||
null;
|
||||
|
||||
context.* = .{
|
||||
.initialized = false,
|
||||
.env = env,
|
||||
.wl_compositor = options.wl_compositor,
|
||||
.wl_display = options.wl_display,
|
||||
.wl_registry = options.wl_registry,
|
||||
|
|
@ -72,22 +97,26 @@ pub fn create(options: Options) !*Context {
|
|||
.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),
|
||||
.wm = try WindowManager.create(context, options.river_window_manager_v1),
|
||||
.xkb_bindings = try XkbBindings.create(context, options.river_xkb_bindings_v1),
|
||||
.im = im,
|
||||
.wm = wm,
|
||||
.xkb_bindings = xkb_bindings,
|
||||
.config = options.config,
|
||||
.tag_overlay_timer_fd = tag_overlay_timer_fd,
|
||||
};
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
pub fn destroy(context: *Context) void {
|
||||
context.xkb_bindings.destroy();
|
||||
context.env.deinit();
|
||||
context.im.destroy();
|
||||
context.wm.destroy();
|
||||
context.xkb_bindings.destroy();
|
||||
|
||||
if (context.wallpaper_image) |wallpaper_image| {
|
||||
wallpaper_image.destroy();
|
||||
}
|
||||
if (context.tag_overlay_timer_fd) |fd| posix.close(fd);
|
||||
context.buffer_pool.deinit();
|
||||
|
||||
utils.gpa.destroy(context);
|
||||
|
|
@ -120,6 +149,42 @@ pub fn manage(context: *Context) void {
|
|||
libinput_device.should_manage = true;
|
||||
}
|
||||
|
||||
// Handle tag overlay config changes
|
||||
const had_overlay = context.config.tag_overlay != null;
|
||||
const has_overlay = new_config.tag_overlay != null;
|
||||
|
||||
if (!had_overlay and has_overlay) {
|
||||
// Create timerfd for newly enabled tag overlay
|
||||
context.tag_overlay_timer_fd = posix.timerfd_create(.MONOTONIC, .{ .CLOEXEC = true }) catch |e| blk: {
|
||||
log.err("Failed to create tag overlay timer: {}", .{e});
|
||||
break :blk null;
|
||||
};
|
||||
} else if (had_overlay and !has_overlay) {
|
||||
// Close timerfd for disabled tag overlay
|
||||
if (context.tag_overlay_timer_fd) |fd| posix.close(fd);
|
||||
context.tag_overlay_timer_fd = null;
|
||||
}
|
||||
|
||||
// Recreate or destroy tag overlays on all outputs
|
||||
if (had_overlay or has_overlay) {
|
||||
var out_it = context.wm.outputs.iterator(.forward);
|
||||
while (out_it.next()) |output| {
|
||||
// Destroy existing overlay
|
||||
if (output.tag_overlay) |*tag_overlay| {
|
||||
tag_overlay.deinit();
|
||||
output.tag_overlay = null;
|
||||
}
|
||||
// Create new overlay if configured
|
||||
// Create new overlay struct if configured (surfaces created on-demand)
|
||||
if (new_config.tag_overlay) |tag_overlay_config| {
|
||||
output.tag_overlay = TagOverlay.init(context, output, tag_overlay_config.toTagOverlayOptions()) catch |e| {
|
||||
log.err("Failed to create tag overlay: {}", .{e});
|
||||
continue;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (wallpaper_changed) {
|
||||
if (context.wallpaper_image) |img| img.destroy();
|
||||
context.wallpaper_image = loadWallpaperImage(new_config);
|
||||
|
|
@ -128,7 +193,7 @@ pub fn manage(context: *Context) void {
|
|||
while (out_it.next()) |output| {
|
||||
if (context.wallpaper_image == null) {
|
||||
output.deinitWallpaperLayerSurface();
|
||||
} else if (output.wl_surface != null) {
|
||||
} else if (output.surfaces != null) {
|
||||
output.renderWallpaper() catch |err| {
|
||||
log.err("Wallpaper re-render failed: {}", .{err});
|
||||
};
|
||||
|
|
@ -165,6 +230,8 @@ fn pathsEqual(a: ?[]const u8, b: ?[]const u8) bool {
|
|||
|
||||
const std = @import("std");
|
||||
const mem = std.mem;
|
||||
const posix = std.posix;
|
||||
const process = std.process;
|
||||
|
||||
const wayland = @import("wayland");
|
||||
const river = wayland.client.river;
|
||||
|
|
@ -175,6 +242,8 @@ const utils = @import("utils.zig");
|
|||
const BufferPool = @import("BufferPool.zig");
|
||||
const Config = @import("Config.zig");
|
||||
const InputManager = @import("InputManager.zig");
|
||||
const Output = @import("Output.zig");
|
||||
const TagOverlay = @import("TagOverlay.zig");
|
||||
const WallpaperImage = @import("WallpaperImage.zig");
|
||||
const WindowManager = @import("WindowManager.zig");
|
||||
const XkbBindings = @import("XkbBindings.zig");
|
||||
|
|
|
|||
|
|
@ -17,66 +17,57 @@ input_device: ?*InputDevice = null,
|
|||
/// reloaded.
|
||||
should_manage: bool = true,
|
||||
|
||||
send_events_support: SendEventsModes = .{},
|
||||
send_events_current: ?SendEventsModes = null,
|
||||
|
||||
/// The number of fingers supported for tap-to-click/drag.
|
||||
/// If finger_count is 0, tap-to-click and drag are unsupported.
|
||||
tap_support: u31 = 0,
|
||||
tap_current: ?TapState = null,
|
||||
|
||||
tap_button_map_current: ?TapButtonMap = null,
|
||||
|
||||
drag_current: ?DragState = null,
|
||||
|
||||
drag_lock_current: ?DragLockState = null,
|
||||
|
||||
/// The number of fingers supported for three/four finger drag.
|
||||
/// If finger_count is less than 3, three finger drag is unsupported.
|
||||
three_finger_drag_support: u31 = 0,
|
||||
three_finger_drag_current: ?ThreeFingerDragState = null,
|
||||
|
||||
/// A calibration matrix is supported if the supported argument is non-zero.
|
||||
calibration_matrix_support: bool = false,
|
||||
calibration_matrix_current: ?[]f32 = null,
|
||||
|
||||
accel_profiles_support: ?AccelProfiles = null,
|
||||
accel_profile_current: ?AccelProfile = null,
|
||||
|
||||
accel_speed_current: ?f64 = null,
|
||||
|
||||
natural_scroll_support: bool = false,
|
||||
natural_scroll_current: ?NaturalScrollState = null,
|
||||
|
||||
left_handed_support: bool = false,
|
||||
left_handed_current: ?LeftHandedState = null,
|
||||
|
||||
click_method_support: ?ClickMethods = null,
|
||||
click_method_current: ?ClickMethod = null,
|
||||
|
||||
clickfinger_button_map_current: ?ClickfingerButtonMap = null,
|
||||
|
||||
middle_emulation_support: bool = false,
|
||||
middle_emulation_current: ?MiddleEmulationState = null,
|
||||
|
||||
scroll_method_support: ?ScrollMethods = null,
|
||||
scroll_method_current: ?ScrollMethod = null,
|
||||
/// Supported if scroll_methods.on_button_down is supported.
|
||||
scroll_button_current: ?u32 = null,
|
||||
/// Supported if scroll_methods.on_button_down is supported.
|
||||
scroll_button_lock_current: ?ScrollButtonLockState = null,
|
||||
|
||||
dwt_support: bool = false,
|
||||
dwt_current: ?DwtState = null,
|
||||
|
||||
dwtp_support: bool = false,
|
||||
dwtp_current: ?DwtpState = null,
|
||||
|
||||
rotation_support: bool = false,
|
||||
rotation_current: ?u32 = null,
|
||||
capabilities: Capabilities = .{},
|
||||
state: CurrentState = .{},
|
||||
|
||||
link: wl.list.Link,
|
||||
|
||||
pub const Capabilities = struct {
|
||||
send_events: SendEventsModes = .{},
|
||||
/// The number of fingers supported for tap-to-click/drag.
|
||||
/// If finger_count is 0, tap-to-click and drag are unsupported.
|
||||
tap: u31 = 0,
|
||||
/// The number of fingers supported for three/four finger drag.
|
||||
/// If finger_count is less than 3, three finger drag is unsupported.
|
||||
three_finger_drag: u31 = 0,
|
||||
/// A calibration matrix is supported if the supported argument is non-zero.
|
||||
calibration_matrix: bool = false,
|
||||
accel_profiles: ?AccelProfiles = null,
|
||||
natural_scroll: bool = false,
|
||||
left_handed: bool = false,
|
||||
click_methods: ?ClickMethods = null,
|
||||
middle_emulation: bool = false,
|
||||
scroll_methods: ?ScrollMethods = null,
|
||||
dwt: bool = false,
|
||||
dwtp: bool = false,
|
||||
rotation: bool = false,
|
||||
};
|
||||
|
||||
pub const CurrentState = struct {
|
||||
send_events: ?SendEventsModes = null,
|
||||
tap: ?TapState = null,
|
||||
tap_button_map: ?TapButtonMap = null,
|
||||
drag: ?DragState = null,
|
||||
drag_lock: ?DragLockState = null,
|
||||
three_finger_drag: ?ThreeFingerDragState = null,
|
||||
calibration_matrix: ?[]f32 = null,
|
||||
accel_profile: ?AccelProfile = null,
|
||||
accel_speed: ?f64 = null,
|
||||
natural_scroll: ?NaturalScrollState = null,
|
||||
left_handed: ?LeftHandedState = null,
|
||||
click_method: ?ClickMethod = null,
|
||||
clickfinger_button_map: ?ClickfingerButtonMap = null,
|
||||
middle_emulation: ?MiddleEmulationState = null,
|
||||
scroll_method: ?ScrollMethod = null,
|
||||
/// Supported if scroll_methods.on_button_down is supported.
|
||||
scroll_button: ?u32 = null,
|
||||
/// Supported if scroll_methods.on_button_down is supported.
|
||||
scroll_button_lock: ?ScrollButtonLockState = null,
|
||||
dwt: ?DwtState = null,
|
||||
dwtp: ?DwtpState = null,
|
||||
rotation: ?u32 = null,
|
||||
};
|
||||
|
||||
pub fn create(context: *Context, river_libinput_device_v1: *river.LibinputDeviceV1) !*LibinputDevice {
|
||||
const libinput_device = try utils.gpa.create(LibinputDevice);
|
||||
errdefer utils.gpa.destroy(libinput_device);
|
||||
|
|
@ -123,39 +114,39 @@ fn riverLibinputDeviceV1Listener(river_libinput_device_v1: *river.LibinputDevice
|
|||
}
|
||||
}
|
||||
},
|
||||
.send_events_support => |ev| libinput_device.send_events_support = ev.modes,
|
||||
.send_events_current => |ev| libinput_device.send_events_current = ev.mode,
|
||||
.tap_support => |ev| libinput_device.tap_support = @intCast(ev.finger_count),
|
||||
.tap_current => |ev| libinput_device.tap_current = ev.state,
|
||||
.tap_button_map_current => |ev| libinput_device.tap_button_map_current = ev.button_map,
|
||||
.drag_current => |ev| libinput_device.drag_current = ev.state,
|
||||
.drag_lock_current => |ev| libinput_device.drag_lock_current = ev.state,
|
||||
.three_finger_drag_support => |ev| libinput_device.three_finger_drag_support = @intCast(ev.finger_count),
|
||||
.three_finger_drag_current => |ev| libinput_device.three_finger_drag_current = ev.state,
|
||||
.calibration_matrix_support => |ev| libinput_device.calibration_matrix_support = ev.supported != 0,
|
||||
.calibration_matrix_current => |ev| libinput_device.calibration_matrix_current = ev.matrix.slice(f32),
|
||||
.accel_profiles_support => |ev| libinput_device.accel_profiles_support = ev.profiles,
|
||||
.accel_profile_current => |ev| libinput_device.accel_profile_current = ev.profile,
|
||||
.accel_speed_current => |ev| libinput_device.accel_speed_current = ev.speed.slice(f64)[0],
|
||||
.natural_scroll_support => |ev| libinput_device.natural_scroll_support = ev.supported != 0,
|
||||
.natural_scroll_current => |ev| libinput_device.natural_scroll_current = ev.state,
|
||||
.left_handed_support => |ev| libinput_device.left_handed_support = ev.supported != 0,
|
||||
.left_handed_current => |ev| libinput_device.left_handed_current = ev.state,
|
||||
.click_method_support => |ev| libinput_device.click_method_support = ev.methods,
|
||||
.click_method_current => |ev| libinput_device.click_method_current = ev.method,
|
||||
.clickfinger_button_map_current => |ev| libinput_device.clickfinger_button_map_current = ev.button_map,
|
||||
.middle_emulation_support => |ev| libinput_device.middle_emulation_support = ev.supported != 0,
|
||||
.middle_emulation_current => |ev| libinput_device.middle_emulation_current = ev.state,
|
||||
.scroll_method_support => |ev| libinput_device.scroll_method_support = ev.methods,
|
||||
.scroll_method_current => |ev| libinput_device.scroll_method_current = ev.method,
|
||||
.scroll_button_current => |ev| libinput_device.scroll_button_current = ev.button,
|
||||
.scroll_button_lock_current => |ev| libinput_device.scroll_button_lock_current = ev.state,
|
||||
.dwt_support => |ev| libinput_device.dwt_support = ev.supported != 0,
|
||||
.dwt_current => |ev| libinput_device.dwt_current = ev.state,
|
||||
.dwtp_support => |ev| libinput_device.dwtp_support = ev.supported != 0,
|
||||
.dwtp_current => |ev| libinput_device.dwtp_current = ev.state,
|
||||
.rotation_support => |ev| libinput_device.rotation_support = ev.supported != 0,
|
||||
.rotation_current => |ev| libinput_device.rotation_current = ev.angle,
|
||||
.send_events_support => |ev| libinput_device.capabilities.send_events = ev.modes,
|
||||
.send_events_current => |ev| libinput_device.state.send_events = ev.mode,
|
||||
.tap_support => |ev| libinput_device.capabilities.tap = @intCast(ev.finger_count),
|
||||
.tap_current => |ev| libinput_device.state.tap = ev.state,
|
||||
.tap_button_map_current => |ev| libinput_device.state.tap_button_map = ev.button_map,
|
||||
.drag_current => |ev| libinput_device.state.drag = ev.state,
|
||||
.drag_lock_current => |ev| libinput_device.state.drag_lock = ev.state,
|
||||
.three_finger_drag_support => |ev| libinput_device.capabilities.three_finger_drag = @intCast(ev.finger_count),
|
||||
.three_finger_drag_current => |ev| libinput_device.state.three_finger_drag = ev.state,
|
||||
.calibration_matrix_support => |ev| libinput_device.capabilities.calibration_matrix = ev.supported != 0,
|
||||
.calibration_matrix_current => |ev| libinput_device.state.calibration_matrix = ev.matrix.slice(f32),
|
||||
.accel_profiles_support => |ev| libinput_device.capabilities.accel_profiles = ev.profiles,
|
||||
.accel_profile_current => |ev| libinput_device.state.accel_profile = ev.profile,
|
||||
.accel_speed_current => |ev| libinput_device.state.accel_speed = ev.speed.slice(f64)[0],
|
||||
.natural_scroll_support => |ev| libinput_device.capabilities.natural_scroll = ev.supported != 0,
|
||||
.natural_scroll_current => |ev| libinput_device.state.natural_scroll = ev.state,
|
||||
.left_handed_support => |ev| libinput_device.capabilities.left_handed = ev.supported != 0,
|
||||
.left_handed_current => |ev| libinput_device.state.left_handed = ev.state,
|
||||
.click_method_support => |ev| libinput_device.capabilities.click_methods = ev.methods,
|
||||
.click_method_current => |ev| libinput_device.state.click_method = ev.method,
|
||||
.clickfinger_button_map_current => |ev| libinput_device.state.clickfinger_button_map = ev.button_map,
|
||||
.middle_emulation_support => |ev| libinput_device.capabilities.middle_emulation = ev.supported != 0,
|
||||
.middle_emulation_current => |ev| libinput_device.state.middle_emulation = ev.state,
|
||||
.scroll_method_support => |ev| libinput_device.capabilities.scroll_methods = ev.methods,
|
||||
.scroll_method_current => |ev| libinput_device.state.scroll_method = ev.method,
|
||||
.scroll_button_current => |ev| libinput_device.state.scroll_button = ev.button,
|
||||
.scroll_button_lock_current => |ev| libinput_device.state.scroll_button_lock = ev.state,
|
||||
.dwt_support => |ev| libinput_device.capabilities.dwt = ev.supported != 0,
|
||||
.dwt_current => |ev| libinput_device.state.dwt = ev.state,
|
||||
.dwtp_support => |ev| libinput_device.capabilities.dwtp = ev.supported != 0,
|
||||
.dwtp_current => |ev| libinput_device.state.dwtp = ev.state,
|
||||
.rotation_support => |ev| libinput_device.capabilities.rotation = ev.supported != 0,
|
||||
.rotation_current => |ev| libinput_device.state.rotation = ev.angle,
|
||||
else => |ev| {
|
||||
// We don't keep track of any default states right now
|
||||
log.debug("unhandled event: {s}", .{@tagName(ev)});
|
||||
|
|
@ -187,29 +178,29 @@ pub fn applyInputConfigs(libinput_device: *LibinputDevice) void {
|
|||
|
||||
log.debug("Applying input config to {s}", .{device_name});
|
||||
|
||||
if (@as(u32, @bitCast(libinput_device.send_events_support)) != 0) {
|
||||
if (@as(u32, @bitCast(libinput_device.capabilities.send_events)) != 0) {
|
||||
if (input_config.send_events) |val| {
|
||||
const mode: SendEventsModes = @bitCast(@as(u32, @intCast(@intFromEnum(val))));
|
||||
const mode_bits: u32 = @bitCast(mode);
|
||||
const support_bits: u32 = @bitCast(libinput_device.send_events_support);
|
||||
const support_bits: u32 = @bitCast(libinput_device.capabilities.send_events);
|
||||
if (mode_bits == 0 or mode_bits & support_bits == mode_bits) {
|
||||
applyResult(dev.setSendEvents(mode));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (libinput_device.tap_support > 0) {
|
||||
if (libinput_device.capabilities.tap > 0) {
|
||||
if (input_config.tap) |val| applyResult(dev.setTap(val));
|
||||
if (input_config.tap_button_map) |val| applyResult(dev.setTapButtonMap(val));
|
||||
if (input_config.drag) |val| applyResult(dev.setDrag(val));
|
||||
if (input_config.drag_lock) |val| applyResult(dev.setDragLock(val));
|
||||
}
|
||||
|
||||
if (libinput_device.three_finger_drag_support >= 3) {
|
||||
if (libinput_device.capabilities.three_finger_drag >= 3) {
|
||||
if (input_config.three_finger_drag) |val| applyResult(dev.setThreeFingerDrag(val));
|
||||
}
|
||||
|
||||
if (libinput_device.accel_profiles_support) |support| {
|
||||
if (libinput_device.capabilities.accel_profiles) |support| {
|
||||
if (input_config.accel_profile) |val| {
|
||||
if (isSupported(AccelProfile, AccelProfiles, val, support)) {
|
||||
applyResult(dev.setAccelProfile(val));
|
||||
|
|
@ -226,15 +217,15 @@ pub fn applyInputConfigs(libinput_device: *LibinputDevice) void {
|
|||
}
|
||||
}
|
||||
|
||||
if (libinput_device.natural_scroll_support) {
|
||||
if (libinput_device.capabilities.natural_scroll) {
|
||||
if (input_config.natural_scroll) |val| applyResult(dev.setNaturalScroll(val));
|
||||
}
|
||||
|
||||
if (libinput_device.left_handed_support) {
|
||||
if (libinput_device.capabilities.left_handed) {
|
||||
if (input_config.left_handed) |val| applyResult(dev.setLeftHanded(val));
|
||||
}
|
||||
|
||||
if (libinput_device.click_method_support) |support| {
|
||||
if (libinput_device.capabilities.click_methods) |support| {
|
||||
if (input_config.click_method) |val| {
|
||||
if (isSupported(ClickMethod, ClickMethods, val, support)) {
|
||||
applyResult(dev.setClickMethod(val));
|
||||
|
|
@ -243,11 +234,11 @@ pub fn applyInputConfigs(libinput_device: *LibinputDevice) void {
|
|||
if (input_config.clickfinger_button_map) |val| applyResult(dev.setClickfingerButtonMap(val));
|
||||
}
|
||||
|
||||
if (libinput_device.middle_emulation_support) {
|
||||
if (libinput_device.capabilities.middle_emulation) {
|
||||
if (input_config.middle_emulation) |val| applyResult(dev.setMiddleEmulation(val));
|
||||
}
|
||||
|
||||
if (libinput_device.scroll_method_support) |support| {
|
||||
if (libinput_device.capabilities.scroll_methods) |support| {
|
||||
if (input_config.scroll_method) |val| {
|
||||
if (isSupported(ScrollMethod, ScrollMethods, val, support)) {
|
||||
applyResult(dev.setScrollMethod(val));
|
||||
|
|
@ -259,15 +250,15 @@ pub fn applyInputConfigs(libinput_device: *LibinputDevice) void {
|
|||
}
|
||||
}
|
||||
|
||||
if (libinput_device.dwt_support) {
|
||||
if (libinput_device.capabilities.dwt) {
|
||||
if (input_config.dwt) |val| applyResult(dev.setDwt(val));
|
||||
}
|
||||
|
||||
if (libinput_device.dwtp_support) {
|
||||
if (libinput_device.capabilities.dwtp) {
|
||||
if (input_config.dwtp) |val| applyResult(dev.setDwtp(val));
|
||||
}
|
||||
|
||||
if (libinput_device.rotation_support) {
|
||||
if (libinput_device.capabilities.rotation) {
|
||||
if (input_config.rotation) |val| applyResult(dev.setRotation(val));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,12 +32,15 @@ usable_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,
|
||||
|
||||
surfaces: ?struct {
|
||||
wl_surface: *wl.Surface,
|
||||
layer_surface: *zwlr.LayerSurfaceV1,
|
||||
} = null,
|
||||
|
||||
// TODO: Make Bar a user option, can disable if they want
|
||||
// This Output's bar
|
||||
bar: ?Bar,
|
||||
tag_overlay: ?TagOverlay,
|
||||
|
||||
/// Proportion of output width taken by the primary stack
|
||||
primary_ratio: f32,
|
||||
|
|
@ -89,16 +92,26 @@ pub fn create(context: *Context, river_output_v1: *river.OutputV1) !*Output {
|
|||
var output = try utils.gpa.create(Output);
|
||||
errdefer utils.gpa.destroy(output);
|
||||
|
||||
const bar = Bar.init(context, output) catch |e| blk: {
|
||||
var bar = Bar.init(context, output) catch |e| blk: {
|
||||
log.err("Failed to create a bar: {}", .{e});
|
||||
break :blk null;
|
||||
};
|
||||
errdefer if (bar) |*b| b.deinit();
|
||||
|
||||
var tag_overlay = if (context.config.tag_overlay) |tag_overlay_config| blk: {
|
||||
break :blk TagOverlay.init(context, output, tag_overlay_config.toTagOverlayOptions()) catch |e| {
|
||||
log.err("Failed to create a tag overlay: {}", .{e});
|
||||
break :blk null;
|
||||
};
|
||||
} else null;
|
||||
errdefer if (tag_overlay) |*to| to.deinit();
|
||||
|
||||
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,
|
||||
.tag_overlay = tag_overlay,
|
||||
.primary_count = context.config.primary_count,
|
||||
.primary_ratio = context.config.primary_ratio,
|
||||
.windows = undefined, // we will initialize this shortly
|
||||
|
|
@ -119,6 +132,10 @@ pub fn destroy(output: *Output) void {
|
|||
window.link.remove();
|
||||
window.destroy();
|
||||
}
|
||||
|
||||
if (output.bar) |*bar| bar.deinit();
|
||||
if (output.tag_overlay) |*tag_overlay| tag_overlay.deinit();
|
||||
|
||||
output.tag_layout_overrides.deinit(utils.gpa);
|
||||
output.deinitWallpaperLayerSurface();
|
||||
output.river_output_v1.destroy();
|
||||
|
|
@ -227,6 +244,8 @@ fn riverOutputListener(river_output_v1: *river.OutputV1, event: river.OutputV1.E
|
|||
return;
|
||||
};
|
||||
}
|
||||
// Tag overlay surfaces are created on-demand when tags change,
|
||||
// so we don't init them here.
|
||||
},
|
||||
.dimensions => |ev| {
|
||||
// Protocol guarantees that width and height are strictly greater than zero
|
||||
|
|
@ -319,8 +338,8 @@ pub fn initWallpaperLayerSurface(output: *Output) !void {
|
|||
return;
|
||||
}
|
||||
|
||||
if (output.layer_surface) |_| {
|
||||
// This output already has a layer surface, we can exit early
|
||||
if (output.surfaces) |_| {
|
||||
// This output already has a surface, we can exit early
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -329,6 +348,9 @@ pub fn initWallpaperLayerSurface(output: *Output) !void {
|
|||
const wl_surface = try context.wl_compositor.createSurface();
|
||||
errdefer wl_surface.destroy();
|
||||
|
||||
const layer_surface = try context.zwlr_layer_shell_v1.getLayerSurface(wl_surface, output.wl_output, .background, "beansprout-wallpaper");
|
||||
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();
|
||||
|
|
@ -340,12 +362,13 @@ pub fn initWallpaperLayerSurface(output: *Output) !void {
|
|||
defer opaque_region.destroy();
|
||||
wl_surface.setOpaqueRegion(opaque_region);
|
||||
|
||||
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 });
|
||||
|
||||
output.wl_surface = wl_surface;
|
||||
output.layer_surface = layer_surface;
|
||||
output.surfaces = .{
|
||||
.wl_surface = wl_surface,
|
||||
.layer_surface = layer_surface,
|
||||
};
|
||||
context.buffer_pool.surface_count += 1;
|
||||
|
||||
layer_surface.setListener(*Output, wallpaperLayerSurfaceListener, output);
|
||||
|
|
@ -353,16 +376,13 @@ pub fn initWallpaperLayerSurface(output: *Output) !void {
|
|||
}
|
||||
|
||||
pub fn deinitWallpaperLayerSurface(output: *Output) void {
|
||||
if (output.layer_surface) |layer_surface| {
|
||||
layer_surface.destroy();
|
||||
}
|
||||
if (output.wl_surface) |wl_surface| {
|
||||
wl_surface.destroy();
|
||||
if (output.surfaces) |surfaces| {
|
||||
surfaces.wl_surface.destroy();
|
||||
surfaces.layer_surface.destroy();
|
||||
output.context.buffer_pool.surface_count -= 1;
|
||||
}
|
||||
|
||||
output.layer_surface = null;
|
||||
output.wl_surface = null;
|
||||
output.surfaces = null;
|
||||
output.configured = false;
|
||||
}
|
||||
|
||||
|
|
@ -379,10 +399,10 @@ fn wallpaperLayerSurfaceListener(layer_surface: *zwlr.LayerSurfaceV1, event: zwl
|
|||
output.wallpaper_render_height == height and
|
||||
output.scale == output.wallpaper_render_scale)
|
||||
{
|
||||
if (output.wl_surface) |wl_surface| {
|
||||
wl_surface.commit();
|
||||
if (output.surfaces) |surfaces| {
|
||||
surfaces.wl_surface.commit();
|
||||
} else {
|
||||
log.warn("Output is marked as configured but is missing a layer_surface for the wallpaper", .{});
|
||||
log.warn("Output is marked as configured but is missing its surfaces.", .{});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -440,7 +460,7 @@ pub fn renderWallpaper(output: *Output) !void {
|
|||
const image_stride = image.getStride();
|
||||
const image_format = image.getFormat();
|
||||
|
||||
const buffer: *Buffer = try context.buffer_pool.nextBuffer(context.wl_shm, width * scale, height * scale);
|
||||
const buffer = try context.buffer_pool.nextBuffer(context.wl_shm, width * scale, height * scale);
|
||||
|
||||
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", .{});
|
||||
|
|
@ -485,7 +505,8 @@ pub fn renderWallpaper(output: *Output) !void {
|
|||
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.?;
|
||||
const surfaces = output.surfaces orelse return error.NoSurfaces;
|
||||
const wl_surface = surfaces.wl_surface;
|
||||
wl_surface.setBufferScale(scale);
|
||||
wl_surface.attach(buffer.wl_buffer, 0, 0);
|
||||
wl_surface.damageBuffer(0, 0, width * scale, height * scale);
|
||||
|
|
@ -548,6 +569,33 @@ pub fn manage(output: *Output) void {
|
|||
}
|
||||
|
||||
output.tags = new_tags;
|
||||
|
||||
// Show tag overlay and arm the hide timer
|
||||
if (output.tag_overlay) |*tag_overlay| {
|
||||
if (tag_overlay.surfaces) |_| {
|
||||
// The overlay is arleady visible, but we still need to re-render
|
||||
tag_overlay.render() catch |err| {
|
||||
log.err("tag_overlay render failed: {}", .{err});
|
||||
};
|
||||
} else {
|
||||
// Create surface; the configure handler renders for us
|
||||
tag_overlay.initSurface() catch |err| {
|
||||
log.err("tag_overlay initSurface failed: {}", .{err});
|
||||
};
|
||||
}
|
||||
if (output.context.tag_overlay_timer_fd) |fd| {
|
||||
const timeout_ms: isize = tag_overlay.options.timeout;
|
||||
posix.timerfd_settime(fd, .{}, &.{
|
||||
.it_interval = .{ .sec = 0, .nsec = 0 },
|
||||
.it_value = .{
|
||||
.sec = @divFloor(timeout_ms, 1000),
|
||||
.nsec = @mod(timeout_ms, 1000) * std.time.ns_per_ms,
|
||||
},
|
||||
}, null) catch |err| {
|
||||
log.err("Failed to arm tag overlay timer: {}", .{err});
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate layout before managing windows
|
||||
|
|
@ -696,6 +744,7 @@ pub fn occupiedTags(output: *Output) u32 {
|
|||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const mem = std.mem;
|
||||
const posix = std.posix;
|
||||
const DoublyLinkedList = std.DoublyLinkedList;
|
||||
|
||||
const wayland = @import("wayland");
|
||||
|
|
@ -708,6 +757,7 @@ const utils = @import("utils.zig");
|
|||
const Bar = @import("Bar.zig");
|
||||
const Buffer = @import("Buffer.zig");
|
||||
const Context = @import("Context.zig");
|
||||
const TagOverlay = @import("TagOverlay.zig");
|
||||
const Window = @import("Window.zig");
|
||||
|
||||
const log = std.log.scoped(.Output);
|
||||
|
|
|
|||
262
src/TagOverlay.zig
Normal file
262
src/TagOverlay.zig
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
// SPDX-FileCopyrightText: 2026 Ben Buhse <me@benbuhse.email>
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
//! TagOverlay is a Zig clone of Leon Plickat's river-tag-overlay but built for beansprout
|
||||
//! Find river-tag-overlay at: https://git.sr.ht/~leon_plickat/river-tag-overlay/
|
||||
|
||||
const TagOverlay = @This();
|
||||
|
||||
context: *Context,
|
||||
|
||||
options: Options,
|
||||
|
||||
output: *Output,
|
||||
|
||||
// Overlay geometry
|
||||
width: u31 = 0,
|
||||
height: u31 = 0,
|
||||
scale: u31 = 1,
|
||||
|
||||
rows: u31,
|
||||
|
||||
surfaces: ?struct {
|
||||
wl_surface: *wl.Surface,
|
||||
layer_surface: *zwlr.LayerSurfaceV1,
|
||||
} = null,
|
||||
|
||||
configured: bool = false,
|
||||
|
||||
pub const Options = struct {
|
||||
/// Width of the widget border in pixels
|
||||
border_width: u8,
|
||||
/// Number of displayed tags (1-32)
|
||||
tag_amount: u6,
|
||||
/// Amount of tags per row (1-32)
|
||||
tags_per_row: u6,
|
||||
/// Size of tag squares in pixels
|
||||
square_size: u8,
|
||||
/// Padding around the tag occupied indicator in pixels
|
||||
square_inner_padding: u8,
|
||||
/// Padding around tag squares in pixels
|
||||
square_padding: u8,
|
||||
/// Border width of the tag squares in pixels
|
||||
square_border_width: u8,
|
||||
/// Widget background color
|
||||
background_color: pixman.Color,
|
||||
/// Widget border color
|
||||
border_color: pixman.Color,
|
||||
/// Background color of active tag squares
|
||||
square_active_background_color: pixman.Color,
|
||||
/// Border color of active tag squares
|
||||
square_active_border_color: pixman.Color,
|
||||
/// Occupied indicator color of active tag squares
|
||||
square_active_occupied_color: pixman.Color,
|
||||
/// Background color of inactive tag squares
|
||||
square_inactive_background_color: pixman.Color,
|
||||
/// Border color of inactive tag squares
|
||||
square_inactive_border_color: pixman.Color,
|
||||
/// Occupied indicator color of inactive tag squares
|
||||
square_inactive_occupied_color: pixman.Color,
|
||||
// XXX: We do not support urgent windows right now
|
||||
// /// Background color of urgent tag squares
|
||||
// square_urgent_background_color: pixman.Color,
|
||||
// /// Border color of urgent tag squares
|
||||
// square_urgent_border_color: pixman.Color,
|
||||
// /// Occupied indicator color of urgent tag squares
|
||||
// square_urgent_occupied_color: pixman.Color,
|
||||
/// Directional anchors top, right bottom, left; true for on, false for off
|
||||
anchors: zwlr.LayerSurfaceV1.Anchor,
|
||||
/// Directional margins top, right, bottom, left, in pixels
|
||||
margins: struct { top: i32 = 0, right: i32 = 0, bottom: i32 = 0, left: i32 = 0 } = .{},
|
||||
/// Duration of widget display in milliseconds
|
||||
timeout: u32,
|
||||
};
|
||||
|
||||
pub fn init(context: *Context, output: *Output, options: Options) !TagOverlay {
|
||||
const rows = try math.divCeil(u31, options.tag_amount, options.tags_per_row);
|
||||
|
||||
return .{
|
||||
.context = context,
|
||||
.options = options,
|
||||
.rows = rows,
|
||||
.output = output,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn initSurface(tag_overlay: *TagOverlay) !void {
|
||||
if (tag_overlay.surfaces) |_| {
|
||||
// This tag overlay already has a layer surface, we can exit early
|
||||
return;
|
||||
}
|
||||
|
||||
const context = tag_overlay.context;
|
||||
const options = tag_overlay.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, tag_overlay.output.wl_output, .overlay, "beansprout-tag-overlay");
|
||||
errdefer layer_surface.destroy();
|
||||
layer_surface.setExclusiveZone(-1);
|
||||
|
||||
// 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 surface_width: u31 = @as(u31, @min(options.tag_amount, options.tags_per_row)) * (@as(u31, options.square_size) + options.square_padding) + options.square_padding + 2 * @as(u31, options.border_width);
|
||||
const surface_height: u31 = @as(u31, tag_overlay.rows) * (@as(u31, options.square_size) + options.square_padding) + options.square_padding + 2 * @as(u31, options.border_width);
|
||||
layer_surface.setSize(surface_width, surface_height);
|
||||
|
||||
layer_surface.setAnchor(options.anchors);
|
||||
layer_surface.setMargin(options.margins.top, options.margins.right, options.margins.bottom, options.margins.left);
|
||||
|
||||
tag_overlay.surfaces = .{ .wl_surface = wl_surface, .layer_surface = layer_surface };
|
||||
context.buffer_pool.surface_count += 1;
|
||||
|
||||
layer_surface.setListener(*TagOverlay, layerSurfaceListener, tag_overlay);
|
||||
wl_surface.commit();
|
||||
}
|
||||
|
||||
/// Destroy surfaces only (used to hide the overlay). The TagOverlay struct stays valid
|
||||
/// and can be re-shown by calling initSurface() again.
|
||||
pub fn deinitSurfaces(tag_overlay: *TagOverlay) void {
|
||||
tag_overlay.configured = false;
|
||||
if (tag_overlay.surfaces) |surfaces| {
|
||||
surfaces.layer_surface.destroy();
|
||||
surfaces.wl_surface.destroy();
|
||||
tag_overlay.context.buffer_pool.surface_count -= 1;
|
||||
tag_overlay.surfaces = null;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deinit(tag_overlay: *TagOverlay) void {
|
||||
tag_overlay.deinitSurfaces();
|
||||
}
|
||||
|
||||
pub fn layerSurfaceListener(
|
||||
layer_surface: *zwlr.LayerSurfaceV1,
|
||||
event: zwlr.LayerSurfaceV1.Event,
|
||||
tag_overlay: *TagOverlay,
|
||||
) void {
|
||||
assert(tag_overlay.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);
|
||||
|
||||
if (tag_overlay.configured and
|
||||
tag_overlay.width == width and
|
||||
tag_overlay.height == height and
|
||||
tag_overlay.output.scale == tag_overlay.scale)
|
||||
{
|
||||
tag_overlay.surfaces.?.wl_surface.commit();
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug("Configuring tag_overlay surface with width {} and height {}", .{ width, height });
|
||||
tag_overlay.width = width;
|
||||
tag_overlay.height = height;
|
||||
|
||||
// Full surface should be opaque
|
||||
const opaque_region: *wl.Region = tag_overlay.context.wl_compositor.createRegion() catch |e| {
|
||||
log.err("Failed to create opaque region for tag_overlay: {}", .{e});
|
||||
return;
|
||||
};
|
||||
defer opaque_region.destroy();
|
||||
opaque_region.add(0, 0, tag_overlay.width, tag_overlay.height);
|
||||
tag_overlay.surfaces.?.wl_surface.setOpaqueRegion(opaque_region);
|
||||
|
||||
tag_overlay.configured = true;
|
||||
|
||||
tag_overlay.render() catch |err| {
|
||||
log.err("tag_overlay render failed: {}", .{err});
|
||||
};
|
||||
},
|
||||
.closed => {
|
||||
tag_overlay.deinit();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(tag_overlay: *TagOverlay) !void {
|
||||
const context = tag_overlay.context;
|
||||
const options = tag_overlay.options;
|
||||
|
||||
// Scaled width/height
|
||||
const scale = tag_overlay.output.scale;
|
||||
const render_width = tag_overlay.width * scale;
|
||||
const render_height = tag_overlay.height * scale;
|
||||
|
||||
// Don't have anything to render
|
||||
if (render_width == 0 or render_height == 0) {
|
||||
return;
|
||||
}
|
||||
const buffer = try context.buffer_pool.nextBuffer(context.wl_shm, render_width, render_height);
|
||||
|
||||
buffer.borderedRectangle(0, 0, tag_overlay.width, tag_overlay.height, options.border_width, scale, &options.background_color, &options.border_color);
|
||||
|
||||
const focused_tags = tag_overlay.output.tags;
|
||||
const occupied_tags = tag_overlay.output.occupiedTags();
|
||||
|
||||
for (0..tag_overlay.rows) |row| {
|
||||
for (0..options.tags_per_row) |tag| {
|
||||
const current_tag = tag + row * options.tags_per_row;
|
||||
if (current_tag >= options.tag_amount) break;
|
||||
|
||||
const bg_color, const border_color, const occupied_color =
|
||||
// Check whether this tag is focused or not to decide colors
|
||||
if (focused_tags & (@as(u32, 1) << @intCast(current_tag)) != 0)
|
||||
.{ &options.square_active_background_color, &options.square_active_border_color, &options.square_active_occupied_color }
|
||||
else
|
||||
.{ &options.square_inactive_background_color, &options.square_inactive_border_color, &options.square_inactive_occupied_color };
|
||||
|
||||
const x = options.border_width + @as(u31, @intCast((tag + 1) * options.square_padding)) + @as(u31, @intCast(tag * options.square_size));
|
||||
const y = options.border_width + @as(u31, @intCast((row + 1) * options.square_padding)) + @as(u31, @intCast(row * options.square_size));
|
||||
|
||||
buffer.borderedRectangle(x, y, options.square_size, options.square_size, options.square_border_width, scale, bg_color, border_color);
|
||||
|
||||
if (occupied_tags & (@as(u32, 1) << @intCast(current_tag)) != 0) {
|
||||
buffer.borderedRectangle(
|
||||
x + options.square_inner_padding,
|
||||
y + options.square_inner_padding,
|
||||
options.square_size - 2 * options.square_inner_padding,
|
||||
options.square_size - 2 * options.square_inner_padding,
|
||||
options.square_border_width,
|
||||
scale,
|
||||
occupied_color,
|
||||
border_color,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, attach the buffer to the surface
|
||||
const surfaces = tag_overlay.surfaces orelse return error.NoSurfaces;
|
||||
const wl_surface = surfaces.wl_surface;
|
||||
wl_surface.setBufferScale(scale);
|
||||
wl_surface.attach(buffer.wl_buffer, 0, 0);
|
||||
wl_surface.damageBuffer(0, 0, render_width, render_height);
|
||||
wl_surface.commit();
|
||||
}
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const math = std.math;
|
||||
|
||||
const wayland = @import("wayland");
|
||||
const wl = wayland.client.wl;
|
||||
const zwlr = wayland.client.zwlr;
|
||||
const fcft = @import("fcft");
|
||||
const pixman = @import("pixman");
|
||||
|
||||
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(.TagOverlay);
|
||||
35
src/main.zig
35
src/main.zig
|
|
@ -115,12 +115,13 @@ fn run(wl_display: *wl.Display, context: *Context) !void {
|
|||
|
||||
posix.sigprocmask(posix.SIG.BLOCK, &mask, null);
|
||||
|
||||
const sig_fd = try posix.signalfd(-1, &mask, 0);
|
||||
const sig_fd = try posix.signalfd(-1, &mask, @as(u32, @bitCast(posix.O{ .CLOEXEC = true })));
|
||||
|
||||
const poll_wayland = 0;
|
||||
const poll_sig = 1;
|
||||
const poll_tag_overlay_timer = 2;
|
||||
|
||||
var pollfds: [2]posix.pollfd = undefined;
|
||||
var pollfds: [3]posix.pollfd = undefined;
|
||||
|
||||
pollfds[poll_wayland] = .{
|
||||
.fd = wl_display.getFd(),
|
||||
|
|
@ -132,6 +133,12 @@ fn run(wl_display: *wl.Display, context: *Context) !void {
|
|||
.events = posix.POLL.IN,
|
||||
.revents = 0,
|
||||
};
|
||||
pollfds[poll_tag_overlay_timer] = .{
|
||||
// poll ignores negative fds
|
||||
.fd = context.tag_overlay_timer_fd orelse -1,
|
||||
.events = posix.POLL.IN,
|
||||
.revents = 0,
|
||||
};
|
||||
|
||||
while (true) {
|
||||
const errno = wl_display.flush();
|
||||
|
|
@ -165,22 +172,46 @@ fn run(wl_display: *wl.Display, context: *Context) !void {
|
|||
|
||||
// Handle fds that became ready
|
||||
if (pollfds[poll_wayland].revents & posix.POLL.HUP != 0) {
|
||||
@branchHint(.cold);
|
||||
log.info("Disconnected by compositor", .{});
|
||||
break;
|
||||
}
|
||||
if (pollfds[poll_wayland].revents & posix.POLL.IN != 0) {
|
||||
if (wl_display.dispatch() != .SUCCESS) {
|
||||
@branchHint(.cold);
|
||||
fatal("Wayland display dispatch failed", .{});
|
||||
}
|
||||
// Re-sync in case a config reload created or destroyed the timerfd
|
||||
pollfds[poll_tag_overlay_timer].fd = context.tag_overlay_timer_fd orelse -1;
|
||||
}
|
||||
|
||||
if (pollfds[poll_sig].revents & posix.POLL.HUP != 0) {
|
||||
@branchHint(.cold);
|
||||
fatal("Signal fd hung up", .{});
|
||||
}
|
||||
if (pollfds[poll_sig].revents & posix.POLL.IN != 0) {
|
||||
@branchHint(.cold);
|
||||
log.info("Exiting beansprout", .{});
|
||||
break;
|
||||
}
|
||||
|
||||
if (pollfds[poll_tag_overlay_timer].revents & posix.POLL.HUP != 0) {
|
||||
@branchHint(.cold);
|
||||
fatal("Tag overlay timer fd hung up", .{});
|
||||
}
|
||||
if (pollfds[poll_tag_overlay_timer].revents & posix.POLL.IN != 0) {
|
||||
// Read to consume the timer event
|
||||
var buf: [8]u8 = undefined;
|
||||
_ = posix.read(context.tag_overlay_timer_fd.?, &buf) catch {};
|
||||
|
||||
// Hide all tag overlays by destroying their surfaces
|
||||
var it = context.wm.outputs.iterator(.forward);
|
||||
while (it.next()) |output| {
|
||||
if (output.tag_overlay) |*tag_overlay| {
|
||||
tag_overlay.deinitSurfaces();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ pub fn parseRgba(s: []const u8) !RiverColor {
|
|||
}
|
||||
|
||||
const bytes: [4]u8 = @as([4]u8, @bitCast(color));
|
||||
return parseRgbaHelper(bytes);
|
||||
return bytesToRiverColor(bytes);
|
||||
}
|
||||
|
||||
/// Parse a color in the format 0xRRGGBB or 0xRRGGBBAA and convert it to
|
||||
|
|
@ -43,10 +43,50 @@ pub fn parseRgbaComptime(comptime s: []const u8) RiverColor {
|
|||
}
|
||||
|
||||
const bytes = @as([4]u8, @bitCast(color));
|
||||
return parseRgbaHelper(bytes);
|
||||
return bytesToRiverColor(bytes);
|
||||
}
|
||||
|
||||
fn parseRgbaHelper(bytes: [4]u8) RiverColor {
|
||||
/// Parse a color in the format 0xRRGGBB or 0xRRGGBBAA and convert it to
|
||||
/// 16-bit color values.
|
||||
pub fn parseRgbaPixman(s: []const u8) !pixman.Color {
|
||||
if (s.len != 8 and s.len != 10) return error.InvalidRgba;
|
||||
if (s[0] != '0' or s[1] != 'x') return error.InvalidRgba;
|
||||
|
||||
var color = try fmt.parseUnsigned(u32, s[2..], 16);
|
||||
if (s.len == 8) {
|
||||
color <<= 8;
|
||||
color |= 0xff;
|
||||
}
|
||||
|
||||
return bytesToPixmanColor(@bitCast(color));
|
||||
}
|
||||
|
||||
/// Parse a color in the format 0xRRGGBB or 0xRRGGBBAA and convert it to
|
||||
/// 16-bit color values at comptime.
|
||||
pub fn parseRgbaPixmanComptime(comptime s: []const u8) pixman.Color {
|
||||
@setEvalBranchQuota(2000);
|
||||
if (s.len != 8 and s.len != 10) @compileError("Invalid RGBA");
|
||||
if (s[0] != '0' or s[1] != 'x') @compileError("Invalid RGBA");
|
||||
|
||||
comptime var color = try fmt.parseUnsigned(u32, s[2..], 16);
|
||||
if (s.len == 8) {
|
||||
color <<= 8;
|
||||
color |= 0xff;
|
||||
}
|
||||
|
||||
return bytesToPixmanColor(@bitCast(color));
|
||||
}
|
||||
|
||||
fn bytesToPixmanColor(bytes: [4]u8) pixman.Color {
|
||||
return .{
|
||||
.red = @as(u16, bytes[3]) * 0x101,
|
||||
.green = @as(u16, bytes[2]) * 0x101,
|
||||
.blue = @as(u16, bytes[1]) * 0x101,
|
||||
.alpha = @as(u16, bytes[0]) * 0x101,
|
||||
};
|
||||
}
|
||||
|
||||
fn bytesToRiverColor(bytes: [4]u8) RiverColor {
|
||||
const r: u32 = bytes[3];
|
||||
const g: u32 = bytes[2];
|
||||
const b: u32 = bytes[1];
|
||||
|
|
@ -137,6 +177,7 @@ const mem = std.mem;
|
|||
|
||||
const wayland = @import("wayland");
|
||||
const river = wayland.client.river;
|
||||
const pixman = @import("pixman");
|
||||
|
||||
const utils = @import("utils.zig");
|
||||
|
||||
|
|
@ -191,6 +232,44 @@ test "parseRgbaComptime with alpha" {
|
|||
try testing.expectEqual(@as(u32, 0xff << 24), color.alpha);
|
||||
}
|
||||
|
||||
test "parseRgbaPixman 0xRRGGBB" {
|
||||
const color = try parseRgbaPixman("0x89b4fa");
|
||||
try testing.expectEqual(@as(u16, 0x8989), color.red);
|
||||
try testing.expectEqual(@as(u16, 0xb4b4), color.green);
|
||||
try testing.expectEqual(@as(u16, 0xfafa), color.blue);
|
||||
try testing.expectEqual(@as(u16, 0xffff), color.alpha);
|
||||
}
|
||||
|
||||
test "parseRgbaPixman 0xRRGGBBAA" {
|
||||
const color = try parseRgbaPixman("0x1e1e2e80");
|
||||
try testing.expectEqual(@as(u16, 0x1e1e), color.red);
|
||||
try testing.expectEqual(@as(u16, 0x1e1e), color.green);
|
||||
try testing.expectEqual(@as(u16, 0x2e2e), color.blue);
|
||||
try testing.expectEqual(@as(u16, 0x8080), color.alpha);
|
||||
}
|
||||
|
||||
test "parseRgbaPixman invalid" {
|
||||
try testing.expectError(error.InvalidRgba, parseRgbaPixman("0x123"));
|
||||
try testing.expectError(error.InvalidRgba, parseRgbaPixman("xx123456"));
|
||||
try testing.expectError(error.InvalidCharacter, parseRgbaPixman("0xGGGGGG"));
|
||||
}
|
||||
|
||||
test "parseRgbaPixmanComptime" {
|
||||
const color = parseRgbaPixmanComptime("0x89b4fa");
|
||||
try testing.expectEqual(@as(u16, 0x8989), color.red);
|
||||
try testing.expectEqual(@as(u16, 0xb4b4), color.green);
|
||||
try testing.expectEqual(@as(u16, 0xfafa), color.blue);
|
||||
try testing.expectEqual(@as(u16, 0xffff), color.alpha);
|
||||
}
|
||||
|
||||
test "parseRgbaPixmanComptime with alpha" {
|
||||
const color = parseRgbaPixmanComptime("0x1e1e2e80");
|
||||
try testing.expectEqual(@as(u16, 0x1e1e), color.red);
|
||||
try testing.expectEqual(@as(u16, 0x1e1e), color.green);
|
||||
try testing.expectEqual(@as(u16, 0x2e2e), color.blue);
|
||||
try testing.expectEqual(@as(u16, 0x8080), color.alpha);
|
||||
}
|
||||
|
||||
test "stripQuotes removes surrounding quotes" {
|
||||
try testing.expectEqualStrings("hello", stripQuotes("\"hello\""));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue