summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 10:11:14 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:16 -0300
commitf207f1d4d56d1012766e6206e713f2b3da70579d (patch)
tree307fb71b210cb4757f2d3fa56094effe20e4d3cf
parentdf6106af80e13cc4f0121b7f2caf5315a4453a9d (diff)
downloadpardes-f207f1d4d56d1012766e6206e713f2b3da70579d.tar.gz
pardes-f207f1d4d56d1012766e6206e713f2b3da70579d.zip
A column takes panes until their minimums fill it, not until none can be halved
Placement halved the tallest pane or refused, so 11 panes filled 48 rows that hold 16 at their tag and two rows each: the refusal did not match the documented arithmetic. When no pane can be halved but the minimums with the new pane's fit, the column's rows are shared out again; and a refused pane's removal leaves no pane under its minimum. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--docs/tags.md7
-rw-r--r--src/exec.zig13
-rw-r--r--src/layout.zig46
-rw-r--r--src/ninep/ctl.zig18
-rw-r--r--src/pardes.zig6
5 files changed, 87 insertions, 3 deletions
diff --git a/docs/tags.md b/docs/tags.md
index 46c2defa..018536e6 100644
--- a/docs/tags.md
+++ b/docs/tags.md
@@ -244,8 +244,11 @@ p` is its leader path, and `Config` reports it.
Under either, no placement leaves a pane, new or split, shorter than its tag
and two body rows (acme's minht keeps one). Where the place chosen has not
-that room, the column's tallest pane is halved instead; where no pane of the
-column can give it, the new pane is refused and closed, with `no space for a
+that room, the column's tallest pane is halved instead; where no one pane
+can give it but the column's rows hold every pane's tag and two rows with
+the new one's, the rows are shared out again, each at least its minimum;
+only when they do not -- the arithmetic, not the halving, decides -- is
+the new pane refused and closed, with `no space for a
pane in that column: each keeps its tag and 2 rows` (a 9P write or open of
`pane/new` fails with it, ENOSPC). A pane alone in its column always fits.
diff --git a/src/exec.zig b/src/exec.zig
index 79717963..60c4172d 100644
--- a/src/exec.zig
+++ b/src/exec.zig
@@ -977,9 +977,12 @@ pub fn placeNew(p: *Pardes, from: usize, origin: ?usize, id: usize, kind: NewPan
layout.snapColWeights(p, f.col);
if (!layout.splitTallest(p, f.col, id) or !layout.placedRoomy(p, id, &before)) {
if (layout.findPane(p, id) == null) layout.insert(p, f.col, p.col_n[f.col], id);
+ // No pane to halve, but the column holds every pane's minimum
+ // with the new one's: the rows are shared out again, so what is
+ // refused is only what the documented arithmetic cannot hold.
// +Errors goes wherever there is room, last column first; with
// none, its maker says the text some other way (appendErrors).
- if (kind == .errors) {
+ if (layout.shareColumn(p, f.col)) {} else if (kind == .errors) {
layout.removePane(p, id);
var c = p.ncol;
while (c > 0) {
@@ -999,12 +1002,20 @@ pub fn placeNew(p: *Pardes, from: usize, origin: ?usize, id: usize, kind: NewPan
return;
}
} else {
+ // Refused, it closes at the step's end holding no weight, so
+ // its column's rows come back as they were, none rounded off.
+ p.panes[id].?.vweight = 0;
p.unplaced.set(id);
p.reportError(from, "placement", error.NoPaneRoom);
return;
}
}
};
+ // Placed: no pane of its column left under its minimum by the rows
+ // rounding or an earlier placement gave it; shared out again if so.
+ if (layout.findPane(p, id)) |f| if (!layout.columnAtMinimums(p, f.col)) {
+ _ = layout.shareColumn(p, f.col);
+ };
if (kind == .command) return;
tagline.exitHeader(p);
p.active = id;
diff --git a/src/layout.zig b/src/layout.zig
index 09cf23b3..16af1ec5 100644
--- a/src/layout.zig
+++ b/src/layout.zig
@@ -441,6 +441,52 @@ pub fn minimaFit(p: *Pardes) bool {
return true;
}
+/// Whether every unfolded pane of column `c` has its `minRows` (a pane
+/// alone in its column always does).
+pub fn columnAtMinimums(p: *Pardes, c: usize) bool {
+ compute(p);
+ if (p.col_n[c] < 2) return true;
+ for (p.col_panes[c][0..p.col_n[c]]) |k| {
+ const pane = p.panes[k] orelse continue;
+ if (!pane.collapsed and p.rects[k].h < minRows(p, k)) return false;
+ }
+ return true;
+}
+
+/// Shares column `c`'s rows out again so every unfolded pane has its
+/// `minRows`, as evenly as that allows; false, changing nothing, when the
+/// column has not the rows for all of them (or holds a folded pane).
+pub fn shareColumn(p: *Pardes, c: usize) bool {
+ compute(p);
+ const n = p.col_n[c];
+ if (n == 0) return false;
+ const avail = p.screen_h -| (p.topBarHeight() + p.columnBarHeight());
+ var mins: [MAX_PANES]u16 = undefined;
+ var need: u32 = 0;
+ for (p.col_panes[c][0..n], 0..) |k, i| {
+ const pane = p.panes[k] orelse return false;
+ if (pane.collapsed) return false;
+ p.rects[k].w = p.col_w[c];
+ mins[i] = minRows(p, k);
+ need += mins[i];
+ }
+ if (need > avail) return false;
+ // Each its minimum, then what is left a row at a time to the smallest.
+ var hs: [MAX_PANES]u16 = undefined;
+ @memcpy(hs[0..n], mins[0..n]);
+ var left = avail - need;
+ while (left > 0) : (left -= 1) {
+ var low: usize = 0;
+ for (1..n) |i| if (hs[i] < hs[low]) {
+ low = i;
+ };
+ hs[low] += 1;
+ }
+ for (p.col_panes[c][0..n], 0..) |k, i| p.panes[k].?.vweight = @floatFromInt(hs[i]);
+ compute(p);
+ return true;
+}
+
/// After a resize the minima fit: each unfolded pane the resize took below
/// its `minRows` (it had them in `before`) gets them back, a row at a time
/// from whichever pane of its column has the most above its own. A pane
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index fbda6113..6631e5f9 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -2088,6 +2088,24 @@ test "a bad event record says what is wrong with it" {
_ = call(p, .{ .tag = 3, .op = .release, .node = event, .handle = h });
}
+test "a column takes panes until their documented minimums fill it, not until none can be halved" {
+ const p = try Pardes.init(testing.allocator, .{ .tty_only = true, .cols = 160, .rows = 50 });
+ defer p.deinit();
+ while (p.nextEffect()) |_| {}
+ const new = @intFromEnum(tree.TopFile.new);
+ while (call(p, .{ .tag = 1, .op = .open, .node = new }).reply.status == .ok) p.sync();
+ p.sync();
+ const avail = p.screen_h - (p.topBarHeight() + p.columnBarHeight());
+ for (0..p.ncol) |c| {
+ var need: u32 = 0;
+ for (p.col_panes[c][0..p.col_n[c]]) |k| need += layout.minRows(p, k);
+ // Refused only when one more pane's tag and two rows would not fit.
+ if (p.col_n[c] > 1) try testing.expect(avail - need < 3);
+ // Every pane keeps its minimum.
+ for (p.col_panes[c][0..p.col_n[c]]) |k| try testing.expect(p.rects[k].h >= layout.minRows(p, k));
+ }
+}
+
test "size is monotonic: growing is never refused, and a size once taken is taken again" {
const p = try Pardes.init(testing.allocator, .{ .tty_only = true, .cols = 160, .rows = 60 });
defer p.deinit();
diff --git a/src/pardes.zig b/src/pardes.zig
index 225dbf9e..f7aee16c 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -6630,10 +6630,16 @@ pub const Pardes = struct {
}
pub fn sync(p: *Pardes) void {
+ var refused = false;
while (p.unplaced.findFirstSet()) |id| {
p.unplaced.unset(id);
if (p.panes[id] != null) p.removePane(id, null) catch {};
+ refused = true;
}
+ // A refused pane gone, no pane it squeezed stays under its minimum.
+ if (refused) for (0..p.ncol) |c| if (!layout.columnAtMinimums(p, c)) {
+ _ = layout.shareColumn(p, c);
+ };
tagline.flattenHeaders(p);
if (p.header_column != null and p.columnBarHeight() == 0) {
tagline.exitHeader(p);