Files
att_wm/src/Output.zig
T

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;
}
};