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