layout/master: new windows goes into master and gets focus

This commit is contained in:
2026-07-26 21:15:05 +02:00
parent fff3a5e734
commit 4c80b60b93
4 changed files with 41 additions and 30 deletions
+5 -2
View File
@@ -182,8 +182,11 @@ separate setting, `repeat`, under [Input](#input).
column is divided, so the bottom edge always lands exactly on the output edge. column is divided, so the bottom edge always lands exactly on the output edge.
**monocle** — every window gets the full area; only the most recently focused **monocle** — every window gets the full area; only the most recently focused
one is rendered. A window spawned into monocle or tabbed takes focus as it one is rendered.
maps, so it comes up in front instead of hidden behind the current one.
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
onto a tag nobody is viewing leaves both the order and focus alone.
**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 solid colour block per window, the focused one highlighted. The blocks draws one solid colour block per window, the focused one highlighted. The blocks
+33 -14
View File
@@ -602,23 +602,27 @@ fn assignOutputs(self: *Wm) void {
} }
} }
/// Hand a freshly mapped window the newest focus serial, and focus it, when it /// Put a freshly mapped window at the front of its output's arrangement order
/// arrives into a stacking layout. /// and focus it.
/// ///
/// Those layouts render only the most recently focused window, so a new window /// This is dwm's attach() plus focus(): a new client lands in master rather
/// that keeps its initial serial of zero is hidden the instant it appears and /// than at the bottom of the stack, and the keyboard follows it there. In a
/// cannot even be reached with the pointer. Making it the top of the stack /// stacking layout the move is not optional — those layouts render only the
/// without also moving focus would be worse still — the keyboard would go on /// most recently focused window, so a window that kept its initial serial of
/// talking to a window that is no longer on screen — so the two move together, /// zero would be hidden the instant it appeared and unreachable even with the
/// which is what dwm does for every new client anyway. /// pointer, and promoting it without moving focus would leave the keyboard
/// /// talking to a window that is no longer on screen.
/// In master a new window is visible wherever it lands in the order, so focus
/// is left where the user put it.
/// ///
/// 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. /// its top window with, and the order set here is the one it arranges.
fn focusNewWindows(self: *Wm) void { fn focusNewWindows(self: *Wm) void {
for (self.windows.items) |win| { // `attachToMaster` reorders `windows`, so re-read the list each time round
// rather than holding a slice across the move. It only ever moves a window
// to a lower index, so nothing after `i` shifts and no new window is
// skipped.
var i: usize = 0;
while (i < self.windows.items.len) : (i += 1) {
const win = self.windows.items[i];
if (!win.wants_initial_focus) continue; if (!win.wants_initial_focus) continue;
if (win.closed or !win.mapped) continue; if (win.closed or !win.mapped) continue;
win.wants_initial_focus = false; win.wants_initial_focus = false;
@@ -627,11 +631,12 @@ fn focusNewWindows(self: *Wm) void {
if (win.floating or win.fullscreen) continue; if (win.floating or win.fullscreen) continue;
const out = win.output orelse continue; const out = win.output orelse continue;
if (!layout.stacks(out.state().layout)) 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);
self.focus_serial += 1; self.focus_serial += 1;
win.focus_serial = self.focus_serial; win.focus_serial = self.focus_serial;
for (self.seats.items) |seat| { for (self.seats.items) |seat| {
@@ -640,6 +645,20 @@ fn focusNewWindows(self: *Wm) void {
} }
} }
/// Move `win` ahead of every other tiled window on `out`, so it takes the
/// first master slot. Windows on other outputs and on other tags keep their
/// relative order.
fn attachToMaster(self: *Wm, win: *Window, out: *Output) void {
for (self.windows.items) |other| {
if (other == win) return; // already the first tiled window here
if (other.output != out) continue;
if ((other.tags & out.tags) == 0) continue;
if (other.floating or other.fullscreen) continue;
self.moveToFront(win, other);
return;
}
}
/// If a seat's focused window is no longer visible, move focus the way dwm /// If a seat's focused window is no longer visible, move focus the way dwm
/// does: to the most recently focused visible window on the same output. /// does: to the most recently focused visible window on the same output.
fn validateFocus(self: *Wm, seat: *Seat) void { fn validateFocus(self: *Wm, seat: *Seat) void {
-10
View File
@@ -56,16 +56,6 @@ pub const Result = struct {
stacked: bool = false, stacked: bool = false,
}; };
/// Whether a layout puts every window in the same place, so only the top one
/// is rendered. `arrange` reports the same thing after the fact; this answers
/// it for callers that need to know before the geometry is computed.
pub fn stacks(layout: Layout) bool {
return switch (layout) {
.master => false,
.monocle, .tabbed => true,
};
}
/// Fill `cells` with one rectangle per window, in arrangement order. /// Fill `cells` with one rectangle per window, in arrangement order.
/// ///
/// Each cell is the *outer* rectangle including space for the border; the /// Each cell is the *outer* rectangle including space for the border; the
+3 -4
View File
@@ -102,13 +102,12 @@ test "monocle gives every window the full area and reports stacked" {
for (cells) |cell| try testing.expectEqual(area, cell); for (cells) |cell| try testing.expectEqual(area, cell);
} }
test "stacks agrees with what arrange reports" { test "master does not report stacked" {
var cells: [2]Box = undefined; var cells: [2]Box = undefined;
var p = params(1, 0.55); var p = params(1, 0.55);
p.tabbar_height = 22; p.tabbar_height = 22;
for ([_]layout.Layout{ .master, .monocle, .tabbed }) |mode| {
try testing.expectEqual(layout.arrange(mode, p, &cells).stacked, layout.stacks(mode)); try testing.expect(!layout.arrange(.master, p, &cells).stacked);
}
} }
test "tabbed reserves the bar strip above the windows" { test "tabbed reserves the bar strip above the windows" {