diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-28 16:44:45 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:15 -0300 |
| commit | d09c1c36e4d4e1bc57aa14f5ba4c6131b6111429 (patch) | |
| tree | 62d8d68a486950118af23dc5f623dccc1824d00a /src/pardes.zig | |
| parent | b4172e6bde69eb35648f173f4497054d1fb2e0fc (diff) | |
| download | pardes-d09c1c36e4d4e1bc57aa14f5ba4c6131b6111429.tar.gz pardes-d09c1c36e4d4e1bc57aa14f5ba4c6131b6111429.zip | |
Focus on a closed pane goes to the pane that takes its place, never to the one focused before
Dogfood closed a pane in one column and focus jumped to another column, only because that pane was focused last. Now removePane decides for every route (Del, a ctl del, a command pane's end, a 9P remove, a stillborn scratch replaced): the pane that grew into the rows, or the scratch standing in an emptied column; removeColumn gives it to the pane level with it in the column that takes the width. Closing an unfocused pane leaves focus alone. The per-caller focus lines in Del and replaceStillborn are gone, and so is prevFocus.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/pardes.zig')
| -rw-r--r-- | src/pardes.zig | 87 |
1 files changed, 74 insertions, 13 deletions
diff --git a/src/pardes.zig b/src/pardes.zig index 85eae10f..20406895 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -5845,7 +5845,10 @@ pub const Pardes = struct { // ---- dump / load (acme-style: `pardes -l state.zon` restores a session) ---- /// Close a pane; its rows go to `to`, or where layout.absorbVWeight - /// sends them when that is null. + /// sends them when that is null. Focus on it follows its rows: to the + /// pane that grew into them, or to the scratch left standing in an + /// emptied column. Every route that closes a pane comes through here or + /// removeColumn, so none picks focus for itself, and none by recency. pub fn removePane(p: *Pardes, id: usize, to: ?usize) !void { const pane = p.panes[id] orelse return; // A column outlives its panes. Closing the last one leaves an empty @@ -5872,10 +5875,11 @@ pub const Pardes = struct { layout.insert(p, f.col, f.idx + 1, free); break :heir free; }; - layout.absorbVWeight(p, id, to); + const gainer = layout.absorbVWeight(p, id, to); + const beside = if (last_in_column) p.besideColumn(place.?.col, id) else null; layout.removePane(p, id); p.panes[id] = null; - if (p.active == id) p.active = heir orelse p.prevFocus(id) orelse { + if (p.active == id) p.active = heir orelse gainer orelse beside orelse p.anyPane() orelse { p.quit = true; p.emit(.quit); return; @@ -5884,6 +5888,11 @@ pub const Pardes = struct { pub fn removeColumn(p: *Pardes, id: usize) !void { const place = layout.findPane(p, id) orelse return; + const had_focus = layout.findPane(p, p.active); + const beside = if (had_focus != null and had_focus.?.col == place.col) + p.besideColumn(place.col, p.active) + else + null; var ids: [MAX_PANES]usize = undefined; var parents: [MAX_PANES]*Pane = undefined; const count = p.col_n[place.col]; @@ -5897,7 +5906,7 @@ pub const Pardes = struct { layout.removePane(p, closed); p.panes[closed] = null; } - if (p.panes[p.active] == null) p.active = p.prevFocus(p.active) orelse { + if (p.panes[p.active] == null) p.active = beside orelse p.anyPane() orelse { p.quit = true; p.emit(.quit); return; @@ -5920,16 +5929,26 @@ pub const Pardes = struct { return null; } - pub fn prevFocus(p: *Pardes, closing: usize) ?usize { - var i = p.njumps; - while (i > 0) { - i -= 1; - const id = p.jumps[i].pane; - if (id != closing and p.panes[id] != null) return id; - } - for (p.panes, 0..) |slot, k| { - if (slot != null and k != closing) return k; + /// The pane that takes over `id`'s place when column `col` goes: in the + /// column layout.removePane gives the width to, the one level with the + /// middle of `id`. Asked before the column goes, while rects still hold. + fn besideColumn(p: *const Pardes, col: usize, id: usize) ?usize { + if (p.ncol < 2) return null; + const next = if (col > 0) col - 1 else col + 1; + const r = p.rects[id]; + const mid = r.y + r.h / 2; + var pick: ?usize = null; + for (p.col_panes[next][0..p.col_n[next]]) |k| { + if (p.panes[k] == null) continue; + pick = k; + if (p.rects[k].y +| p.rects[k].h > mid) break; } + return pick; + } + + /// Any live pane: the last resort when nothing took the closed one's place. + fn anyPane(p: *const Pardes) ?usize { + for (p.panes, 0..) |slot, k| if (slot != null) return k; return null; } @@ -7004,6 +7023,48 @@ test "Del from a key asks which neighbour takes the rows; nothing else asks" { try std.testing.expect(p.panes[low] == null); } +test "focus on a closed pane goes to the pane that takes its place, not the one focused before" { + if (platform == .web) return; + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 120, .rows = 40 }); + defer p.deinit(); + const shell = p.active; + p.newScratchColumn(shell); + const right = p.active; + p.sync(); + p.newScratchBelow(shell); + const low = p.active; + p.sync(); + // The dogfood case: `right` was focused just before `low`, but `shell` + // grows into `low`'s rows, so `shell` gets the focus. + try std.testing.expect(p.executeBuiltinLine(low, "Del")); + p.sync(); + try std.testing.expect(p.panes[low] == null); + try std.testing.expectEqual(shell, p.active); + + // Closing a pane that has no focus leaves the focus where it is. + p.newScratchBelow(shell); + const other = p.active; + p.sync(); + p.active = right; + p.sync(); + try std.testing.expect(p.executeBuiltinLine(other, "Del")); + try std.testing.expectEqual(right, p.active); + + // Delcol: the column that takes the width holds, level with the closed + // pane, the pane that takes its place; `right` was focused just before. + p.newScratchBelow(right); + const right_low = p.active; + p.newScratchBelow(shell); + const left_low = p.active; + p.active = right; + p.sync(); + p.active = left_low; + p.sync(); + try p.removeColumn(left_low); + p.sync(); + try std.testing.expectEqual(right_low, p.active); +} + test "closing the last pane in a column leaves an empty one in its place" { if (platform == .web) return; const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 120, .rows = 24 }); |
