bring modal or floating windows to front

This commit is contained in:
2026-08-21 23:32:29 +02:00
parent f60dd06e7e
commit 97b27f1f4c
3 changed files with 117 additions and 9 deletions
+13 -2
View File
@@ -191,8 +191,19 @@ layouts, monocle and tabbed alike: with one window filling the area there is
nothing for a border to separate it from. Floating windows keep theirs. nothing for a border to separate it from. Floating windows keep theirs.
In every layout a newly mapped window goes to the front of the arrangement In every layout a newly mapped window goes to the front of the arrangement
order — the first master slot — and takes focus, as in dwm. A window spawned order — the first master slot — and takes focus, as in dwm. A floating or
onto a tag nobody is viewing leaves both the order and focus alone. fullscreen window is not part of the arrangement, so it stays where it is, but
it takes focus like any other new window. A window spawned onto a tag nobody is
viewing leaves both the order and focus alone.
**Dialogs.** A window that names a parent — a dialog, a file picker — starts
floating, which `float_children` turns off. Floating windows are stacked by
focus, but a window and its parents and children are stacked as one family: the
family rises together on the most recent focus any of its members has had, and
within it a child is always drawn directly above its parent. Without that, a
click on the parent of a *modal* dialog would bury the dialog behind the one
window that will not answer, with no way to raise it again. A dialog opened by
a fullscreen window is drawn above the fullscreen layer for the same reason.
**tabbed** — same geometry as monocle, minus a strip at the top where att_wm **tabbed** — same geometry as monocle, minus a strip at the top where att_wm
draws one tab per window, the focused one highlighted. Each tab is clickable and draws one tab per window, the focused one highlighted. Each tab is clickable and
+42
View File
@@ -75,6 +75,11 @@ pending_close: bool = false,
/// Bumped whenever the window takes focus, giving a cheap "most recently /// Bumped whenever the window takes focus, giving a cheap "most recently
/// focused" ordering without maintaining dwm's second linked list. /// focused" ordering without maintaining dwm's second linked list.
focus_serial: u64 = 0, 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. /// Cleared once the window has had its one chance at taking focus as it maps.
wants_initial_focus: bool = true, wants_initial_focus: bool = true,
@@ -105,6 +110,38 @@ pub fn getNode(self: *Window) ?*river.NodeV1 {
return self.node; 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, /// Clamp a proposed size to the window's advertised limits. These are hints,
/// but respecting them avoids pointless configure round-trips with windows /// but respecting them avoids pointless configure round-trips with windows
/// that will refuse the size anyway. /// that will refuse the size anyway.
@@ -215,6 +252,11 @@ fn onEvent(_: *river.WindowV1, event: river.WindowV1.Event, self: *Window) void
if (config.float_children and self.parent != null and !self.floating_forced) { if (config.float_children and self.parent != null and !self.floating_forced) {
self.floating = true; 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| { .fullscreen_requested => |ev| {
+62 -7
View File
@@ -740,6 +740,13 @@ fn assignOutputs(self: *Wm) void {
/// pointer, and promoting it without moving focus would leave the keyboard /// pointer, and promoting it without moving focus would leave the keyboard
/// talking to a window that is no longer on screen. /// talking to a window that is no longer on screen.
/// ///
/// Floating and fullscreen windows are not part of the tiled arrangement, so
/// they are not attached to master, but they are focused like any other new
/// window. dwm focuses new clients whether they float or not, and a dialog
/// that appeared without being focused would also have kept a focus serial of
/// zero — putting it at the bottom of the floating stack, behind the very
/// window it belongs to.
///
/// Runs before `arrangeAll` so the serial set here is the one the layout picks /// Runs before `arrangeAll` so the serial set here is the one the layout picks
/// its top window with, and the order set here is the one it arranges. /// its top window with, and the order set here is the one it arranges.
fn focusNewWindows(self: *Wm) void { fn focusNewWindows(self: *Wm) void {
@@ -754,15 +761,12 @@ fn focusNewWindows(self: *Wm) void {
if (win.closed or !win.mapped) continue; if (win.closed or !win.mapped) continue;
win.wants_initial_focus = false; win.wants_initial_focus = false;
// Floating and fullscreen windows are not part of the stack.
if (win.floating or win.fullscreen) continue;
const out = win.output orelse continue; const out = win.output orelse continue;
// Spawned onto a tag nobody is looking at: nothing to come to the // Spawned onto a tag nobody is looking at: nothing to come to the
// front of, and stealing focus would yank the user off their tag. // front of, and stealing focus would yank the user off their tag.
if ((win.tags & out.tags) == 0) continue; if ((win.tags & out.tags) == 0) continue;
self.attachToMaster(win, out); if (!win.floating and !win.fullscreen) self.attachToMaster(win, out);
self.focus_serial += 1; self.focus_serial += 1;
win.focus_serial = self.focus_serial; win.focus_serial = self.focus_serial;
@@ -1036,10 +1040,40 @@ fn applyWindowState(self: *Wm) void {
// ─── Render sequence ───────────────────────────────────────────────────────── // ─── Render sequence ─────────────────────────────────────────────────────────
/// Work out where each window's family — a window, its parents and its
/// children — sits in the floating stack.
///
/// The floating layer is ordered by focus, which on its own puts a dialog
/// behind its parent as soon as the parent is clicked. That is fatal for a
/// modal dialog, the kind Vivado opens constantly: the parent ignores the
/// click it just took the focus and the stacking order for, so the dialog is
/// buried with no way to raise it again.
///
/// So a family rises and falls as a unit, on the most recent focus any of its
/// members has had, and within the family the deeper window is the higher one.
/// A child is therefore always drawn directly above its parent, which is what
/// the protocol asks for.
fn computeStacking(self: *Wm) void {
for (self.windows.items) |win| {
const a = win.ancestry();
win.stack_depth = a.depth;
// Staged on the root, so the loop below only ever reads focus serials.
win.stack_serial = win.focus_serial;
}
for (self.windows.items) |win| {
const root = win.ancestry().root;
root.stack_serial = @max(root.stack_serial, win.focus_serial);
}
for (self.windows.items) |win| {
win.stack_serial = win.ancestry().root.stack_serial;
}
}
fn render(self: *Wm) void { fn render(self: *Wm) void {
const gpa = self.gpa; const gpa = self.gpa;
self.scratch_order.clearRetainingCapacity(); self.scratch_order.clearRetainingCapacity();
self.computeStacking();
for (self.outputs.items) |out| { for (self.outputs.items) |out| {
// Tiled windows, bottom of the stack. // Tiled windows, bottom of the stack.
@@ -1081,16 +1115,19 @@ fn render(self: *Wm) void {
} }
// Floating windows above, oldest focus first so the most recently // Floating windows above, oldest focus first so the most recently
// focused ends up on top. // focused family ends up on top, its dialogs above their parents.
self.scratch_windows.clearRetainingCapacity(); self.scratch_windows.clearRetainingCapacity();
for (self.windows.items) |win| { for (self.windows.items) |win| {
if (win.output != out or win.closed) continue; if (win.output != out or win.closed) continue;
if (!win.floating or win.fullscreen) continue; if (!win.floating or win.fullscreen) continue;
self.setHidden(win, !win.visible); self.setHidden(win, !win.visible);
if (!win.visible) continue; if (!win.visible) continue;
// A dialog whose parent is fullscreen is drawn after the
// fullscreen layer instead; anything before it is covered.
if (win.hasFullscreenAncestor()) continue;
self.scratch_windows.append(gpa, win) catch {}; self.scratch_windows.append(gpa, win) catch {};
} }
std.mem.sort(*Window, self.scratch_windows.items, {}, lessByFocus); std.mem.sort(*Window, self.scratch_windows.items, {}, lessByStacking);
for (self.scratch_windows.items) |win| { for (self.scratch_windows.items) |win| {
self.place(win, borderWidth(win)); self.place(win, borderWidth(win));
self.scratch_order.append(gpa, win) catch {}; self.scratch_order.append(gpa, win) catch {};
@@ -1105,6 +1142,22 @@ fn render(self: *Wm) void {
win.window.setBorders(.{}, 0, 0, 0, 0, 0); win.window.setBorders(.{}, 0, 0, 0, 0, 0);
self.scratch_order.append(gpa, win) catch {}; self.scratch_order.append(gpa, win) catch {};
} }
// Everything a fullscreen window opened, above it. A modal dialog is
// the only thing its parent will talk to, so hiding it under the
// parent leaves the user stuck.
self.scratch_windows.clearRetainingCapacity();
for (self.windows.items) |win| {
if (win.output != out or win.closed or win.fullscreen) continue;
if (!win.floating or !win.visible) continue;
if (!win.hasFullscreenAncestor()) continue;
self.scratch_windows.append(gpa, win) catch {};
}
std.mem.sort(*Window, self.scratch_windows.items, {}, lessByStacking);
for (self.scratch_windows.items) |win| {
self.place(win, borderWidth(win));
self.scratch_order.append(gpa, win) catch {};
}
} }
// Applying place_top from the bottom up yields exactly the order above. // Applying place_top from the bottom up yields exactly the order above.
@@ -1120,7 +1173,9 @@ fn render(self: *Wm) void {
self.window_manager.?.renderFinish(); self.window_manager.?.renderFinish();
} }
fn lessByFocus(_: void, a: *Window, b: *Window) bool { fn lessByStacking(_: void, a: *Window, b: *Window) bool {
if (a.stack_serial != b.stack_serial) return a.stack_serial < b.stack_serial;
if (a.stack_depth != b.stack_depth) return a.stack_depth < b.stack_depth;
return a.focus_serial < b.focus_serial; return a.focus_serial < b.focus_serial;
} }