summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 16:44:45 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commitd09c1c36e4d4e1bc57aa14f5ba4c6131b6111429 (patch)
tree62d8d68a486950118af23dc5f623dccc1824d00a /src/pardes.zig
parentb4172e6bde69eb35648f173f4497054d1fb2e0fc (diff)
downloadpardes-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.zig87
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 });