346 lines
12 KiB
Zig
346 lines
12 KiB
Zig
//! A logical output, and the window management state that dwm keeps per
|
|
//! monitor: the visible tag set, the layout, nmaster and mfact.
|
|
//!
|
|
//! The arrangement settings are stored per tag rather than per output, as
|
|
//! dwm's pertag patch does, so switching tags restores the layout that tag was
|
|
//! last arranged with.
|
|
|
|
const Output = @This();
|
|
|
|
const std = @import("std");
|
|
const Allocator = std.mem.Allocator;
|
|
|
|
const wayland = @import("wayland");
|
|
const wl = wayland.client.wl;
|
|
const river = wayland.client.river;
|
|
|
|
const config = @import("config");
|
|
const action = @import("action");
|
|
|
|
const Wm = @import("Wm.zig");
|
|
const Window = @import("Window.zig");
|
|
const shm = @import("shm.zig");
|
|
const color = @import("color.zig");
|
|
const font = @import("font.zig");
|
|
const layout = @import("layout.zig");
|
|
const Box = layout.Box;
|
|
|
|
wm: *Wm,
|
|
output: *river.OutputV1,
|
|
layer_output: ?*river.LayerShellOutputV1 = null,
|
|
|
|
/// The global name of the corresponding wl_output, used to pair the two up.
|
|
wl_output_name: u32 = 0,
|
|
/// Owned by Wm.wl_outputs; holds the human readable output name.
|
|
wl_output: ?*Wm.WlOutput = null,
|
|
|
|
/// Full output area in the compositor's logical coordinate space.
|
|
box: Box = .{},
|
|
/// The part of `box` not covered by layer-shell exclusive zones. Windows are
|
|
/// laid out here so bars are not overlapped.
|
|
usable: Box = .{},
|
|
/// Whether `usable` has been reported; before that it tracks `box`.
|
|
have_usable: bool = false,
|
|
|
|
tags: u32 = config.default_tags,
|
|
prev_tags: u32 = config.default_tags,
|
|
|
|
/// One slot per tag, plus slot 0 for views of more than one tag. Indexed
|
|
/// through `state()`, never directly.
|
|
tag_state: [action.tag_count + 1]TagState = @splat(.{}),
|
|
|
|
removed: bool = false,
|
|
|
|
/// Written by the layout pass each manage sequence, read by the render pass.
|
|
tabbar_box: ?Box = null,
|
|
/// True when the current layout stacks windows, so only `stack_top` shows.
|
|
stacked: bool = false,
|
|
stack_top: ?*Window = null,
|
|
|
|
tabbar: TabBar,
|
|
|
|
pub fn create(wm: *Wm, output: *river.OutputV1) !*Output {
|
|
const self = try wm.gpa.create(Output);
|
|
self.* = .{
|
|
.wm = wm,
|
|
.output = output,
|
|
.tabbar = .{ .wm = wm },
|
|
};
|
|
output.setListener(*Output, onEvent, self);
|
|
|
|
if (wm.layer_shell) |ls| {
|
|
self.layer_output = ls.getOutput(output) catch null;
|
|
if (self.layer_output) |lo| {
|
|
lo.setListener(*Output, onLayerEvent, self);
|
|
}
|
|
}
|
|
return self;
|
|
}
|
|
|
|
pub fn destroy(self: *Output) void {
|
|
const gpa = self.wm.gpa;
|
|
self.tabbar.deinit();
|
|
if (self.layer_output) |lo| lo.destroy();
|
|
// The wl_output entry is owned by Wm; just break the back reference.
|
|
if (self.wl_output) |entry| entry.output = null;
|
|
self.output.destroy();
|
|
gpa.destroy(self);
|
|
}
|
|
|
|
pub fn displayName(self: *const Output) []const u8 {
|
|
const entry = self.wl_output orelse return "?";
|
|
return entry.name orelse "?";
|
|
}
|
|
|
|
/// The area windows are laid out in.
|
|
pub fn layoutArea(self: *const Output) Box {
|
|
return if (self.have_usable) self.usable else self.box;
|
|
}
|
|
|
|
/// How one tag is arranged. dwm's pertag patch keeps exactly these four.
|
|
pub const TagState = struct {
|
|
layout: action.Layout = config.default_layout,
|
|
prev_layout: action.Layout = config.default_layout,
|
|
nmaster: i32 = config.nmaster,
|
|
mfact: f32 = config.mfact,
|
|
};
|
|
|
|
/// The arrangement settings in force on this output right now, i.e. those of
|
|
/// the tag being viewed. See `action.tagSlot` for how a view picks its slot.
|
|
pub fn state(self: *Output) *TagState {
|
|
return &self.tag_state[action.tagSlot(self.tags)];
|
|
}
|
|
|
|
pub fn setLayout(self: *Output, mode: action.Layout) void {
|
|
const st = self.state();
|
|
if (mode == st.layout) return;
|
|
st.prev_layout = st.layout;
|
|
st.layout = mode;
|
|
}
|
|
|
|
pub fn setTags(self: *Output, tags: u32) void {
|
|
const masked = tags & action.all_tags;
|
|
if (masked == 0 or masked == self.tags) return;
|
|
self.prev_tags = self.tags;
|
|
self.tags = masked;
|
|
}
|
|
|
|
fn onEvent(_: *river.OutputV1, event: river.OutputV1.Event, self: *Output) void {
|
|
switch (event) {
|
|
.removed => {
|
|
self.removed = true;
|
|
self.wm.needsManage();
|
|
},
|
|
.position => |ev| {
|
|
self.box.x = ev.x;
|
|
self.box.y = ev.y;
|
|
self.wm.needsManage();
|
|
},
|
|
.dimensions => |ev| {
|
|
self.box.width = ev.width;
|
|
self.box.height = ev.height;
|
|
self.wm.needsManage();
|
|
},
|
|
.wl_output => |ev| {
|
|
self.wl_output_name = ev.name;
|
|
self.wm.attachWlOutput(self);
|
|
},
|
|
}
|
|
}
|
|
|
|
fn onLayerEvent(_: *river.LayerShellOutputV1, event: river.LayerShellOutputV1.Event, self: *Output) void {
|
|
switch (event) {
|
|
.non_exclusive_area => |ev| {
|
|
self.usable = .{ .x = ev.x, .y = ev.y, .width = ev.width, .height = ev.height };
|
|
self.have_usable = true;
|
|
self.wm.needsManage();
|
|
},
|
|
}
|
|
}
|
|
|
|
/// The strip of tabs drawn above the windows in the tabbed layout. One tab per
|
|
/// window, labelled with its title and the focused one highlighted. The same
|
|
/// titles are published over IPC, for bars that would rather draw the tabs
|
|
/// themselves with a real font.
|
|
pub const TabBar = struct {
|
|
wm: *Wm,
|
|
|
|
surface: ?*wl.Surface = null,
|
|
shell: ?*river.ShellSurfaceV1 = null,
|
|
node: ?*river.NodeV1 = null,
|
|
pool: ?shm.Pool = null,
|
|
|
|
/// Currently mapped, i.e. showing a buffer.
|
|
mapped: bool = false,
|
|
/// Where the bar is, in global coordinates.
|
|
box: Box = .{},
|
|
/// Hit rectangles for the tabs currently drawn, in global coordinates,
|
|
/// parallel to `windows`.
|
|
rects: std.ArrayList(Box) = .empty,
|
|
windows: std.ArrayList(*Window) = .empty,
|
|
|
|
pub fn deinit(self: *TabBar) void {
|
|
const gpa = self.wm.gpa;
|
|
self.rects.deinit(gpa);
|
|
self.windows.deinit(gpa);
|
|
if (self.pool) |*p| p.deinit();
|
|
if (self.node) |n| n.destroy();
|
|
if (self.shell) |s| s.destroy();
|
|
if (self.surface) |s| s.destroy();
|
|
}
|
|
|
|
fn ensureSurface(self: *TabBar) !void {
|
|
if (self.surface != null) return;
|
|
|
|
const compositor = self.wm.compositor orelse return error.NoCompositor;
|
|
const wm_proxy = self.wm.window_manager orelse return error.NoWindowManager;
|
|
const wl_shm = self.wm.shm orelse return error.NoShm;
|
|
|
|
const surface = try compositor.createSurface();
|
|
errdefer surface.destroy();
|
|
|
|
const shell = try wm_proxy.getShellSurface(surface);
|
|
errdefer shell.destroy();
|
|
|
|
const node = try shell.getNode();
|
|
|
|
self.surface = surface;
|
|
self.shell = shell;
|
|
self.node = node;
|
|
self.pool = shm.Pool.init(self.wm.gpa, wl_shm);
|
|
}
|
|
|
|
/// Draw and place the bar. Must be called during a render sequence.
|
|
pub fn show(self: *TabBar, box: Box, windows: []const *Window, focused: ?*Window) void {
|
|
self.ensureSurface() catch |err| {
|
|
std.log.err("tab bar: {s}", .{@errorName(err)});
|
|
return;
|
|
};
|
|
if (box.width <= 0 or box.height <= 0 or windows.len == 0) {
|
|
self.hide();
|
|
return;
|
|
}
|
|
|
|
const gpa = self.wm.gpa;
|
|
const pool = &self.pool.?;
|
|
|
|
const buffer = pool.acquire(box.width, box.height) catch |err| {
|
|
std.log.err("tab bar buffer: {s}", .{@errorName(err)});
|
|
return;
|
|
};
|
|
|
|
// Recompute the hit rectangles, in buffer-local coordinates first.
|
|
self.rects.clearRetainingCapacity();
|
|
self.windows.clearRetainingCapacity();
|
|
self.rects.ensureTotalCapacity(gpa, windows.len) catch return;
|
|
self.windows.appendSlice(gpa, windows) catch return;
|
|
self.rects.resize(gpa, windows.len) catch return;
|
|
|
|
const local: Box = .{ .x = 0, .y = 0, .width = box.width, .height = box.height };
|
|
layout.tabRects(local, windows.len, self.rects.items);
|
|
|
|
const sep = color.toArgb8888(config.tab_separator);
|
|
buffer.fill(local, sep);
|
|
|
|
const scale = textScale(box.height);
|
|
for (self.rects.items, windows) |rect, win| {
|
|
const is_focused = focused != null and focused.? == win;
|
|
const argb = color.toArgb8888(if (is_focused) config.tab_focused else config.tab_normal);
|
|
// Leave a one pixel separator on the right of every tab but the
|
|
// last, which the background colour shows through.
|
|
const inner: Box = .{
|
|
.x = rect.x,
|
|
.y = rect.y,
|
|
.width = @max(0, rect.width - 1),
|
|
.height = rect.height,
|
|
};
|
|
buffer.fill(inner, argb);
|
|
|
|
const ink = color.toArgb8888(if (is_focused) config.tab_text_focused else config.tab_text_normal);
|
|
drawLabel(buffer, inner, label(win), scale, ink);
|
|
}
|
|
|
|
// Translate the hit rectangles into global coordinates for click
|
|
// handling, now that drawing is done.
|
|
for (self.rects.items) |*rect| {
|
|
rect.x += box.x;
|
|
rect.y += box.y;
|
|
}
|
|
|
|
const surface = self.surface.?;
|
|
buffer.busy = true;
|
|
surface.attach(buffer.wl_buffer, 0, 0);
|
|
surface.damageBuffer(0, 0, box.width, box.height);
|
|
self.shell.?.syncNextCommit();
|
|
surface.commit();
|
|
|
|
self.node.?.setPosition(box.x, box.y);
|
|
self.box = box;
|
|
self.mapped = true;
|
|
}
|
|
|
|
/// Unmap the bar. Must be called during a render sequence if it was
|
|
/// previously shown.
|
|
pub fn hide(self: *TabBar) void {
|
|
if (!self.mapped) return;
|
|
const surface = self.surface orelse return;
|
|
surface.attach(null, 0, 0);
|
|
self.shell.?.syncNextCommit();
|
|
surface.commit();
|
|
self.mapped = false;
|
|
self.rects.clearRetainingCapacity();
|
|
self.windows.clearRetainingCapacity();
|
|
}
|
|
|
|
/// What to write on a window's tab: its title, or its app id while it has
|
|
/// not set one, so that a tab is never blank.
|
|
fn label(win: *const Window) []const u8 {
|
|
if (win.title) |t| {
|
|
if (t.len > 0) return t;
|
|
}
|
|
if (win.app_id) |a| {
|
|
if (a.len > 0) return a;
|
|
}
|
|
return "?";
|
|
}
|
|
|
|
/// Whole-pixel scale for the font: the largest that leaves a little air
|
|
/// above and below in a bar this tall. `config.tab_font_scale` overrides
|
|
/// it for anyone wanting big letters in a thin bar, or the reverse.
|
|
fn textScale(height: i32) i32 {
|
|
if (config.tab_font_scale > 0) return config.tab_font_scale;
|
|
var s: i32 = 1;
|
|
while (s < 4 and font.cell_height * (s + 1) + 4 <= height) s += 1;
|
|
return s;
|
|
}
|
|
|
|
/// Centre a title in one tab, eliding it with an ellipsis when the tab is
|
|
/// too narrow to hold it.
|
|
fn drawLabel(buffer: *shm.Buffer, tab: Box, text: []const u8, scale: i32, argb: u32) void {
|
|
const pad = 2 * scale;
|
|
const avail = tab.width - 2 * pad;
|
|
if (avail <= 0) return;
|
|
|
|
const f = font.fit(text, font.cellsForWidth(avail, scale));
|
|
const cells = f.cells + @intFromBool(f.elided);
|
|
if (cells == 0) return;
|
|
|
|
const x = tab.x + @divFloor(tab.width - font.textWidth(cells, scale), 2);
|
|
// Centre the part above the baseline; the descender row may hang into
|
|
// the padding, which is what it is for.
|
|
const y = tab.y + @divFloor(tab.height - (font.cell_height - 1) * scale, 2);
|
|
|
|
const pen = buffer.drawText(text[0..f.end], x, y, scale, argb, tab);
|
|
if (f.elided) buffer.drawGlyph(font.ellipsis, pen, y, scale, argb, tab);
|
|
}
|
|
|
|
/// Which window's tab covers this global coordinate, if any.
|
|
pub fn windowAt(self: *const TabBar, x: i32, y: i32) ?*Window {
|
|
if (!self.mapped) return null;
|
|
for (self.rects.items, self.windows.items) |rect, win| {
|
|
if (rect.contains(x, y)) return win;
|
|
}
|
|
return null;
|
|
}
|
|
};
|