summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/builtins.zig5
-rw-r--r--src/exec.zig4
-rw-r--r--src/layout.zig14
-rw-r--r--src/pardes.zig87
4 files changed, 86 insertions, 24 deletions
diff --git a/src/builtins.zig b/src/builtins.zig
index 76d7dfb0..ed42dfe3 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -790,7 +790,8 @@ pub const Newcol = struct {
};
/// Close the pane. `Del k` gives its rows to the open pane above it and
-/// `Del j` to the one below, each falling back to the other side.
+/// `Del j` to the one below, each falling back to the other side. Focus on
+/// it goes with its rows.
pub const Del = struct {
pub const takes_arg = true;
pub fn run(c: Ctx) void {
@@ -821,8 +822,6 @@ pub const Del = struct {
return;
}
c.p.removePane(c.id, to) catch |err| return c.p.reportError(c.id, "close", err);
- // Del k|j is a direction: focus lands where the rows went.
- if (to) |gainer| c.p.active = gainer;
}
};
diff --git a/src/exec.zig b/src/exec.zig
index bf3ed1f0..9a4e0a89 100644
--- a/src/exec.zig
+++ b/src/exec.zig
@@ -347,7 +347,7 @@ pub fn evictLonePristineTty(p: *Pardes, col: usize, keep_id: usize) void {
panes.Terminal.scrollbar(tt).total > tt.rows) return;
p.deinitPane(tt) catch |err| return p.reportError(tty_id, "close", err);
layout.compute(p); // a just-stacked doc has no rect yet; absorb snaps to rows
- layout.absorbVWeight(p, tty_id, null);
+ _ = layout.absorbVWeight(p, tty_id, null);
layout.removePane(p, tty_id);
p.panes[tty_id] = null;
if (p.active == tty_id) p.active = keep_id;
@@ -372,10 +372,8 @@ pub fn replaceStillborn(p: *Pardes) void {
if (out.from != .cmd or out.from.cmd != .New) return;
if (file.revision != 0 or file.content.len != 0) return;
if (old.tag.own) |own| if (!std.mem.eql(u8, own, tagline.defaultTail(old))) return;
- const was_active = p.active == old_id;
layout.compute(p); // the joiner may have no rect yet; absorb snaps to rows
p.removePane(old_id, new_id) catch |err| return p.reportError(old_id, "close", err);
- if (was_active) p.active = new_id;
}
// ---- REPLs ----
diff --git a/src/layout.zig b/src/layout.zig
index 8e44f1d9..855a3eb9 100644
--- a/src/layout.zig
+++ b/src/layout.zig
@@ -1000,7 +1000,7 @@ pub fn splitColumn(p: *Pardes, source_id: usize, id: usize, before: bool) bool {
if (needs_rebase) old_weight *= 2;
if (id == source_id) {
if (p.col_n[source_col] <= 1) return false;
- absorbVWeight(p, id, null);
+ _ = absorbVWeight(p, id, null);
removePane(p, id);
} else if (findPane(p, id) != null) return false;
@@ -1041,9 +1041,11 @@ pub fn snapColWeights(p: *Pardes, c: usize) void {
/// Hand a closing pane's rows to `to`. With none named they go up: past any
/// results listing to the nearest pane above that is not one, or to the
/// pane below when the closing one is first in its column.
-pub fn absorbVWeight(p: *Pardes, id: usize, to: ?usize) void {
- const f = findPane(p, id) orelse return;
- if (p.col_n[f.col] <= 1) return;
+/// Give a closing pane's rows to `to`, or to the nearest document above it
+/// (the pane below for the top one); the pane that got them is returned.
+pub fn absorbVWeight(p: *Pardes, id: usize, to: ?usize) ?usize {
+ const f = findPane(p, id) orelse return null;
+ if (p.col_n[f.col] <= 1) return null;
snapColWeights(p, f.col);
var sib = to orelse if (f.idx > 0) p.col_panes[f.col][f.idx - 1] else p.col_panes[f.col][f.idx + 1];
var k = f.idx;
@@ -1051,7 +1053,9 @@ pub fn absorbVWeight(p: *Pardes, id: usize, to: ?usize) void {
sib = p.col_panes[f.col][k - 1];
if (p.panes[sib]) |pp| if (if (pp.file) |ff| panes.Output.fileTraits(ff.output).doc else true) break;
}
- if (p.panes[sib]) |s| s.vweight += @as(f32, @floatFromInt(@max(1, p.rects[id].h)));
+ const s = p.panes[sib] orelse return null;
+ s.vweight += @as(f32, @floatFromInt(@max(1, p.rects[id].h)));
+ return sib;
}
pub fn splitParent(p: *Pardes, want: usize) usize {
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 });