305 lines
10 KiB
Zig
305 lines
10 KiB
Zig
//! 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Replace an owned string field, returning whether the value actually
|
|
/// changed. Clients re-send strings that have not moved — a terminal animating
|
|
/// a spinner in its title re-sets the same text several times a second — and
|
|
/// the caller can use the answer to skip the IPC broadcast and the tab bar
|
|
/// redraw those no-op updates would otherwise trigger.
|
|
fn setString(self: *Window, field: *?[]u8, value: ?[*:0]const u8) bool {
|
|
const gpa = self.wm.gpa;
|
|
const new: ?[]const u8 = if (value) |v| std.mem.span(v) else null;
|
|
|
|
const same = if (field.*) |old|
|
|
if (new) |n| std.mem.eql(u8, old, n) else false
|
|
else
|
|
new == null;
|
|
if (same) return false;
|
|
|
|
if (field.*) |old| gpa.free(old);
|
|
field.* = null;
|
|
if (new) |n| {
|
|
// A failed dupe leaves the field null, which compares unequal to the
|
|
// next event carrying the same text, so the copy is retried then.
|
|
field.* = gpa.dupe(u8, n) catch null;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
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| {
|
|
if (self.setString(&self.app_id, ev.app_id)) {
|
|
self.applyRules();
|
|
self.wm.ipcDirty();
|
|
}
|
|
},
|
|
|
|
.title => |ev| {
|
|
if (self.setString(&self.title, ev.title)) {
|
|
self.applyRules();
|
|
self.wm.ipcDirty();
|
|
// No needsManage: river sent this event, so it already knows
|
|
// the title moved and starts a manage sequence of its own.
|
|
// Asking for one here bought a second, identical manage and
|
|
// render pass for every retitle — and terminals retitle
|
|
// themselves constantly.
|
|
}
|
|
},
|
|
|
|
.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,
|
|
=> {},
|
|
}
|
|
}
|