//! A single managed window. //! //! Event handlers here only ever mutate plain fields. Every protocol request //! that changes window management or rendering state is issued from Wm's //! manage/render sequence handlers, because river only permits those requests //! between manage_start/manage_finish and render_start/render_finish. const Window = @This(); const std = @import("std"); const Allocator = std.mem.Allocator; const wayland = @import("wayland"); const river = wayland.client.river; const config = @import("config"); const Wm = @import("Wm.zig"); const Output = @import("Output.zig"); const layout = @import("layout.zig"); const Box = layout.Box; wm: *Wm, window: *river.WindowV1, /// Created lazily: the protocol allows get_node exactly once per window. node: ?*river.NodeV1 = null, title: ?[]u8 = null, app_id: ?[]u8 = null, identifier: ?[]u8 = null, parent: ?*Window = null, tags: u32 = 0, output: ?*Output = null, floating: bool = false, /// Set by rules or by the user; distinguishes "floating because it is a /// dialog" from "floating because it was asked to be". floating_forced: bool = false, fullscreen: bool = false, /// What we last told the window, so we only send changes. informed_fullscreen: bool = false, /// Target rectangle including the border, computed by the layout. cell: Box = .{}, /// Geometry to restore when a floating window stops being fullscreen. float_box: Box = .{}, /// The content size river last reported. content_width: i32 = 0, content_height: i32 = 0, /// The last size we proposed, so we do not re-propose every manage sequence. proposed_width: i32 = -1, proposed_height: i32 = -1, min_width: i32 = 0, min_height: i32 = 0, max_width: i32 = 0, max_height: i32 = 0, /// True once river has sent dimensions, i.e. the window is on screen. mapped: bool = false, /// True once we have sent the one-time setup requests. configured: bool = false, /// Computed each manage sequence. visible: bool = false, /// Whether the window is currently hidden, so we only send changes. hidden: bool = false, /// river has closed this window; it must be reaped and not touched again. closed: bool = false, /// A close was requested. `close` modifies window management state, so it has /// to wait for the next manage sequence like everything else. pending_close: bool = false, /// Bumped whenever the window takes focus, giving a cheap "most recently /// focused" ordering without maintaining dwm's second linked list. focus_serial: u64 = 0, /// Where this window's family of parents and children sits in the floating /// stack, and how far down that family the window is. Recomputed each render /// sequence; see `Wm.computeStacking`. stack_serial: u64 = 0, stack_depth: u32 = 0, /// Cleared once the window has had its one chance at taking focus as it maps. wants_initial_focus: bool = true, pub fn create(wm: *Wm, window: *river.WindowV1) !*Window { const self = try wm.gpa.create(Window); self.* = .{ .wm = wm, .window = window }; window.setListener(*Window, onEvent, self); return self; } pub fn destroy(self: *Window) void { const gpa = self.wm.gpa; if (self.node) |node| node.destroy(); self.window.destroy(); if (self.title) |t| gpa.free(t); if (self.app_id) |a| gpa.free(a); if (self.identifier) |i| gpa.free(i); gpa.destroy(self); } /// The node is needed to position and stack the window; create it on demand. pub fn getNode(self: *Window) ?*river.NodeV1 { if (self.node) |node| return node; self.node = self.window.getNode() catch |err| { std.log.err("failed to create node for window: {s}", .{@errorName(err)}); return null; }; return self.node; } /// The window at the top of this window's parent chain, and how many parents /// away it is. A window with no parent is its own root, at depth zero. /// /// The protocol promises the parent links form a tree, but a client that /// breaks that promise must not hang the compositor, so the walk is bounded. pub fn ancestry(self: *Window) struct { root: *Window, depth: u32 } { var root = self; var depth: u32 = 0; while (root.parent) |p| { if (depth >= max_parent_depth) break; root = p; depth += 1; } return .{ .root = root, .depth = depth }; } /// Whether this window belongs to a window that is currently fullscreen. Such /// a dialog has to be drawn above the fullscreen layer: its parent covers the /// whole output, and everything below it with it. pub fn hasFullscreenAncestor(self: *Window) bool { var next = self.parent; var depth: u32 = 0; while (next) |p| : (depth += 1) { if (depth >= max_parent_depth) break; if (p.fullscreen and p.visible and !p.closed) return true; next = p.parent; } return false; } const max_parent_depth = 32; /// Clamp a proposed size to the window's advertised limits. These are hints, /// but respecting them avoids pointless configure round-trips with windows /// that will refuse the size anyway. pub fn clampSize(self: *const Window, width: i32, height: i32) struct { i32, i32 } { var w = width; var h = height; if (self.min_width > 0) w = @max(w, self.min_width); if (self.min_height > 0) h = @max(h, self.min_height); if (self.max_width > 0) w = @min(w, self.max_width); if (self.max_height > 0) h = @min(h, self.max_height); return .{ @max(1, w), @max(1, h) }; } /// Apply matching rules from config to a newly created window. pub fn applyRules(self: *Window) void { for (config.rules) |rule| { if (rule.app_id) |want| { const have = self.app_id orelse continue; if (!std.mem.eql(u8, want, have)) continue; } if (rule.title) |want| { const have = self.title orelse continue; if (std.mem.indexOf(u8, have, want) == null) continue; } if (rule.tags) |t| self.tags = t; if (rule.floating) |f| { self.floating = f; self.floating_forced = f; } } } /// Whether this window currently has a tab drawn for it, i.e. it is tiled and /// visible on an output that is showing the tabbed layout. fn onTabBar(self: *Window) bool { if (config.tabbar_height <= 0) return false; if (!self.visible or self.floating or self.fullscreen or self.closed) return false; const out = self.output orelse return false; return out.state().layout == .tabbed; } fn setString(self: *Window, field: *?[]u8, value: ?[*:0]const u8) void { const gpa = self.wm.gpa; if (field.*) |old| gpa.free(old); field.* = null; if (value) |v| { field.* = gpa.dupe(u8, std.mem.span(v)) catch null; } } fn onEvent(_: *river.WindowV1, event: river.WindowV1.Event, self: *Window) void { switch (event) { .closed => { self.closed = true; self.wm.needsManage(); }, .dimensions => |ev| { self.content_width = ev.width; self.content_height = ev.height; if (!self.mapped) { self.mapped = true; // Focus and stacking for a new window are settled in the // manage sequence, and only once it is mapped. self.wm.needsManage(); self.wm.ipcDirty(); } // A window may resize itself; if it is floating its cell must // follow, otherwise the border is drawn around the wrong area. if (self.floating and !self.fullscreen) { const bw = config.border_width; self.cell.width = ev.width + 2 * bw; self.cell.height = ev.height + 2 * bw; self.float_box = self.cell; } }, .dimensions_hint => |ev| { self.min_width = ev.min_width; self.min_height = ev.min_height; self.max_width = ev.max_width; self.max_height = ev.max_height; }, .app_id => |ev| { self.setString(&self.app_id, ev.app_id); self.applyRules(); self.wm.ipcDirty(); }, .title => |ev| { self.setString(&self.title, ev.title); self.applyRules(); self.wm.ipcDirty(); // The tab bar has this title painted into it, so a window that is // wearing a tab needs the strip redrawn. Terminals retitle // themselves constantly, so ask only when it will show. if (self.onTabBar()) self.wm.needsManage(); }, .identifier => |ev| { self.setString(&self.identifier, ev.identifier); }, .parent => |ev| { self.parent = if (ev.parent) |p| Wm.windowFromProxy(p) else null; // Dialogs and file pickers float, as in dwm. if (config.float_children and self.parent != null and !self.floating_forced) { self.floating = true; } // A parent may be set after the window has already been mapped and // laid out — Vivado's dialogs do exactly that — so the change of // both float state and stacking has to be arranged for. self.wm.needsManage(); self.wm.ipcDirty(); }, .fullscreen_requested => |ev| { self.fullscreen = true; if (ev.output) |o| { if (Wm.outputFromProxy(o)) |out| self.output = out; } self.wm.needsManage(); }, .exit_fullscreen_requested => { self.fullscreen = false; self.wm.needsManage(); }, .pointer_move_requested => |ev| { if (Wm.seatFromProxy(ev.seat)) |seat| seat.startMove(self); }, .pointer_resize_requested => |ev| { if (Wm.seatFromProxy(ev.seat)) |seat| seat.startResize(self, ev.edges); }, // We advertise only the fullscreen capability, so these should not // arrive; ignoring them is the documented option either way. .maximize_requested, .unmaximize_requested, .minimize_requested, .show_window_menu_requested, .decoration_hint, .unreliable_pid, .presentation_hint, => {}, } }