diff options
| -rw-r--r-- | docs/tags.md | 9 | ||||
| -rw-r--r-- | src/layout.zig | 69 | ||||
| -rw-r--r-- | src/mouse.zig | 9 | ||||
| -rw-r--r-- | src/pardes.zig | 6 | ||||
| -rw-r--r-- | test/panes.zig | 28 | ||||
| -rw-r--r-- | test/snapshots/cornerdrag.golden | 8 | ||||
| -rw-r--r-- | test/snapshots/shrinktag.golden | 20 | ||||
| -rw-r--r-- | test/snapshots/shrinktag.snap | 20 | ||||
| -rw-r--r-- | test/snapshots/splitparent.golden | 24 | ||||
| -rw-r--r-- | test/snapshots/splitparent.snap | 9 |
10 files changed, 148 insertions, 54 deletions
diff --git a/docs/tags.md b/docs/tags.md index 3db7f465..46c2defa 100644 --- a/docs/tags.md +++ b/docs/tags.md @@ -248,6 +248,15 @@ 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 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. + +After placement a terminal keeps that floor too: no drag of a grip, no +squeeze of its column by the others' weights and no smaller window takes +it below its tag and two body rows (a one-row terminal loses its prompt's +mark and would read busy for ever); its neighbours give the rows, and only +a window too short for every floor leaves it less. A text pane keeps +acme's way and can be dragged down to its tag alone; acme has no +terminals to follow here. A folded terminal (Collapse) is a tag by choice. +`pty/ctl`'s `winsize` likewise gives a pty two rows at least. A `+Errors` pane goes to any column with room, the last first; with none, what it would have shown (an Edit's `p` or `=`, a write to `errors`) is logged as `msg` records, a line each, and the Edit still succeeds. diff --git a/src/layout.zig b/src/layout.zig index 644054ed..cbf0fd96 100644 --- a/src/layout.zig +++ b/src/layout.zig @@ -274,7 +274,11 @@ pub fn applyRowSplit(p: *Pardes, cc: usize, k: usize, cur_y: u16) void { const rb = p.rects[p.col_panes[cc][k + 1]]; const combined: f32 = @floatFromInt(ra.h + rb.h); var nt: f32 = @floatFromInt(if (p.settings.tag_bottom) cur_y -| ra.y else (cur_y + 1) -| ra.y); - nt = std.math.clamp(nt, @as(f32, BOX_H), @max(@as(f32, BOX_H), combined - BOX_H)); + // A terminal keeps its tag and two body rows (floorRows); a text pane + // gives up all but its tag, as acme's do. + const lo: f32 = @floatFromInt(@max(BOX_H, floorRows(p, p.col_panes[cc][k]))); + const hi = combined - @as(f32, @floatFromInt(@max(BOX_H, floorRows(p, p.col_panes[cc][k + 1])))); + nt = std.math.clamp(nt, @min(lo, @max(@as(f32, BOX_H), hi)), @max(@as(f32, BOX_H), hi)); if (combined <= 0) return; // Dragging a folded tag open is an explicit resize, so switch this pair // back to visible-height weights without consuming other folded weights. @@ -794,16 +798,62 @@ pub fn compute(p: *Pardes) void { if (vsum <= 0) vsum = 1; var y: u16 = p.topBarHeight() + p.columnBarHeight(); + const top = y; const avail_h = p.screen_h -| y; - for (0..p.col_n[c]) |k| { + var hs: [MAX_PANES]u16 = undefined; + const n = p.col_n[c]; + for (0..n) |k| { const id = p.col_panes[c][k]; - const pane = p.panes[id] orelse continue; - const lastk = k + 1 == p.col_n[c]; + const pane = p.panes[id] orelse { + hs[k] = 0; + continue; + }; + const lastk = k + 1 == n; const fh = @as(f32, @floatFromInt(avail_h)) * pane.vweight / vsum; const room = p.screen_h -| y; - const ch: u16 = if (lastk) room else @min(room, @max(1, @as(u16, @intFromFloat(@round(fh))))); - p.rects[id] = .{ .x = x, .y = y, .w = cw, .h = ch }; - y +|= ch; + hs[k] = if (lastk) room else @min(room, @max(1, @as(u16, @intFromFloat(@round(fh))))); + y +|= hs[k]; + } + keepTerminalFloors(p, c, hs[0..n]); + y = top; + for (0..n) |k| { + const id = p.col_panes[c][k]; + if (p.panes[id] == null) continue; + p.rects[id] = .{ .x = x, .y = y, .w = cw, .h = hs[k] }; + y +|= hs[k]; + } + } +} + +/// The fewest rows a pane is laid out with: a terminal keeps its tag and +/// `min_body_rows` (a shorter one loses its prompt and reads busy for +/// ever), a text pane may shrink to its tag, as acme's windows do. +fn floorRows(p: *Pardes, id: usize) u16 { + const pane = p.panes[id] orelse return 0; + return if (pane.isTerminal()) @max(1, pane.tag_rows) + min_body_rows else 1; +} + +/// Raises each terminal in column `c` below its floor to it, a row at a +/// time from whichever pane has the most rows above its own floor. Where +/// the column has not the rows, the terminal keeps what it could get. +// ponytail: a row per step, O(panes x rows); a column holds few of both. +fn keepTerminalFloors(p: *Pardes, c: usize, hs: []u16) void { + for (hs, 0..) |*h, k| { + const floor = floorRows(p, p.col_panes[c][k]); + while (h.* < floor) { + var giver: ?usize = null; + var spare: u16 = 0; + for (hs, 0..) |other, j| { + if (j == k) continue; + const extra = other -| floorRows(p, p.col_panes[c][j]); + if (extra > spare) { + spare = extra; + giver = j; + } + } + const g = giver orelse break; + hs[g] -= 1; + h.* += 1; } } } @@ -911,9 +961,10 @@ test "collapse local transfers remain bounded through repeated tiny window resiz test "collapse expansion is limited to space available in one neighbor" { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 42 }); defer p.deinit(); + // Text panes: a terminal would keep its tag and two rows (floorRows). for (1..4) |id| { - _ = try p.newShell(id, ""); - insert(p, 0, id, id); + p.newScratchBelow(0); + try std.testing.expectEqual(id, p.col_panes[0][id]); } compute(p); toggleCollapse(p, 2); diff --git a/src/mouse.zig b/src/mouse.zig index cb5512ad..9eefa9a1 100644 --- a/src/mouse.zig +++ b/src/mouse.zig @@ -109,20 +109,23 @@ test "a pane's last row of text is no resize handle; the rule over the next pane try std.testing.expect(nudgeRightSeam(p, 2) == seam + 2); } -test "a pane's grip dragged up or down its own column moves its top, down to a tag alone above" { +test "a pane's grip dragged up or down its own column moves its top, down to the floor of the pane above" { if (platform == .web) return; const p = try cornerFixture(std.testing.allocator); defer p.deinit(); const upper = p.col_panes[1][0]; const lower = p.col_panes[1][1]; const grip_x = p.rects[lower].x; - // Up, past the pane above's first row: it keeps one row, its tag. + // Up, past the pane above's first row: a text pane keeps its tag, a + // terminal its tag and two rows (layout.floorRows). const top = p.rects[upper].y; + const above = p.panes[upper].?; + const floor: u16 = if (above.isTerminal()) above.tag_rows + layout.min_body_rows else 1; p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = grip_x, .row = p.rects[lower].y } }); p.update(.{ .mouse = .{ .button = .left, .kind = .drag, .col = grip_x, .row = top } }); p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = grip_x, .row = top } }); p.sync(); - try std.testing.expectEqual(@as(u16, 1), p.rects[upper].h); + try std.testing.expectEqual(floor, p.rects[upper].h); try std.testing.expectEqual(lower, p.col_panes[1][1]); // Down, inside itself: its top follows, the pane above taking the rows. const to = p.rects[lower].y + 6; diff --git a/src/pardes.zig b/src/pardes.zig index 8e9d3c7e..98b8819c 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -6598,9 +6598,9 @@ pub const Pardes = struct { pane.tag_top = @intCast(@min(top, lines - pane.tag_rows)); } else pane.tag_top = 0; const cols = @max(1, r.w -| config.GUTTER); - // The tag steals the top rows. The pty is what the pane shows, - // even one row dragged short: a taller pty would put its cursor - // below the pane (only winsize keeps two, ctlfs.pty). + // The tag steals the top rows. The pty is what the pane shows + // (layout keeps a terminal two body rows where the window has + // them): a taller pty would put its cursor below the pane. const rows = @max(1, r.h -| pane.tag_rows); // A terminal's pty follows its pane down to a row, whatever the // resize; only a folded one keeps the size it had, and one not diff --git a/test/panes.zig b/test/panes.zig index ca53af3d..42dd6b77 100644 --- a/test/panes.zig +++ b/test/panes.zig @@ -4891,7 +4891,7 @@ test { _ = LookResetTests; } -test "a terminal's pty follows its pane down to its last row" { +test "a terminal keeps its tag and two body rows through a squeeze, a drag and a smaller window; a text pane does not" { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 30 }); defer p.deinit(); p.presentation.enabled = false; @@ -4903,16 +4903,34 @@ test "a terminal's pty follows its pane down to its last row" { p.newScratchBelow(term); p.sync(); while (p.nextEffect()) |_| {} - // The terminal squeezed to its tag alone, folded by nobody. + const col = layout.findPane(p, term).?.col; + const floor = p.panes[term].?.tag_rows + layout.min_body_rows; + // Squeezed by the weights: the others give it its floor. p.panes[term].?.vweight = 0.01; - for (p.col_panes[0][0..p.col_n[0]]) |k| if (k != term) { + for (p.col_panes[col][0..p.col_n[col]]) |k| if (k != term) { p.panes[k].?.vweight = 1000; }; p.sync(); - try std.testing.expect(p.rects[term].h <= p.panes[term].?.tag_rows); + try std.testing.expectEqual(floor, p.rects[term].h); var rows: ?u16 = null; while (p.nextEffect()) |effect| if (effect == .resize_pty and effect.resize_pty.pane == term) { rows = effect.resize_pty.rows; }; - try std.testing.expectEqual(@as(?u16, 1), rows); + try std.testing.expectEqual(@as(?u16, layout.min_body_rows), rows); + // A drag of the grip under it up to its tag stops at the floor; one + // the other way takes a text pane down to its tag alone. + const k = layout.findPane(p, term).?.idx; + if (k + 1 < p.col_n[col]) { + layout.applyRowSplit(p, col, k, p.rects[term].y); + p.sync(); + try std.testing.expectEqual(floor, p.rects[term].h); + const text = p.col_panes[col][k + 1]; + layout.applyRowSplit(p, col, k, p.screen_h); + p.sync(); + try std.testing.expect(p.rects[text].h <= p.panes[text].?.tag_rows); + } + // A window too short for all: the terminal keeps its floor still. + p.update(.{ .resize = .{ .cols = 100, .rows = 10 } }); + p.sync(); + try std.testing.expect(p.rects[term].h >= p.panes[term].?.tag_rows + layout.min_body_rows); } diff --git a/test/snapshots/cornerdrag.golden b/test/snapshots/cornerdrag.golden index 730cc2bf..30c1830a 100644 --- a/test/snapshots/cornerdrag.golden +++ b/test/snapshots/cornerdrag.golden @@ -243,7 +243,7 @@ |24: |25: |26: -|27: -|28: -|29: -|30: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filter +|27: /tmp/pardes-snap/cornerdrag/cwd Tty+bash Save Mode Filter +|28: Collapse Del +|29: w.txt +|30: diff --git a/test/snapshots/shrinktag.golden b/test/snapshots/shrinktag.golden index 552722f9..9aa29e11 100644 --- a/test/snapshots/shrinktag.golden +++ b/test/snapshots/shrinktag.golden @@ -21,23 +21,25 @@ | ls | w.txt == snap shrunk grid=100x31 cursor=2,9 -|3: /tmp/pardes-snap/shrinktag/cwd Tty+bash Save Mo de Filter Collapse Del -|4: de Filter Collapse Del ls -|5: ls w.txt -|6: w.txt -|8: +|4: w.txt ls +|5: w.txt +|6: /tmp/pardes-snap/shrinktag/cwd Tty+bash Save Mo +|7: de Filter Collapse Del +|8: ls +|9: w.txt |17: |18: |19: |20: == snap move-preview grid=100x31 cursor=2,9 -|8: ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌▌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ +|8: ls ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌▌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ == snap restored grid=100x31 cursor=2,9 -|3: de Filter Collapse Del de Filter Collapse Del |4: ls ls |5: w.txt w.txt |6: +|7: |8: echo one; echo two +|9: |17: /tmp/pardes-snap/shrinktag/cwd Tty+bash Save Mo |18: de Filter Collapse Del |19: ls @@ -47,4 +49,6 @@ |18: |19: |20: -|30: /tmp/pardes-snap/shrinktag/cwd Tty+bash Save Mo +|27: /tmp/pardes-snap/shrinktag/cwd Tty+bash Save Mo +|28: de Filter Collapse Del +|29: w.txt diff --git a/test/snapshots/shrinktag.snap b/test/snapshots/shrinktag.snap index 31edad14..5264be74 100644 --- a/test/snapshots/shrinktag.snap +++ b/test/snapshots/shrinktag.snap @@ -1,8 +1,8 @@ -# a pane can shrink to just its tagline: the pane below dragged by its grip up -# to the tag row (0 body rows) -- a terminal has no rule between panes, so -# the grip is the handle, as acme's button is -- the gutter box still grabs a -# move, and dragging back down restores the body intact (the hidden pane's -# pty/vt is never resized) +# a terminal shrinks only to its tag and 2 body rows (a text pane would go +# to its tag alone): the pane below dragged by its grip up to the tag row +# stops there -- a terminal has no rule between panes, so the grip is the +# handle, as acme's button is -- the gutter box still grabs a move, and +# dragging back down restores the body intact file w.txt x start 31 100 -n 3 wait 8000 w.txt @@ -18,7 +18,7 @@ key esc settle 100 stable 400 5000 snap before -# drag the lower pane's grip up under pane 0's tag row: tag-only pane +# drag the lower pane's grip up under pane 0's tag row: it keeps 2 body rows press left 1 18 drag left 1 3 release left 1 3 @@ -34,14 +34,16 @@ snap move-preview drag left 20 3 release left 20 3 stable 400 5000 -# re-enlarge: the lower pane's grip, now under pane 0's tag, pulled back down -press left 1 4 +# re-enlarge: the lower pane's grip, now under pane 0's 2 body rows, pulled +# back down +press left 1 7 drag left 1 18 release left 1 18 motion 40 25 stable 500 8000 snap restored -# the bottom pane can shrink to ITS tag too: its grip dragged past the bottom +# the bottom pane stops at ITS tag and 2 rows too: its grip dragged past the +# bottom press left 1 18 drag left 1 31 release left 1 31 diff --git a/test/snapshots/splitparent.golden b/test/snapshots/splitparent.golden index 48dcf0c8..e626cea7 100644 --- a/test/snapshots/splitparent.golden +++ b/test/snapshots/splitparent.golden @@ -2,13 +2,16 @@ |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol | /tmp/pardes-snap/splitparent/cwd Tty+bash Save /tmp/pardes-snap/splitparent/cwd Tty+bash Save -| /tmp/pardes-snap/splitparent/cwd Tty+bash Save Mode Filter Collapse Del -| Mode Filter Collapse Del ls +| Mode Filter Collapse Del Mode Filter Collapse Del +| ls ls +| +| /tmp/pardes-snap/splitparent/cwd Tty+bash Save +| Mode Filter Collapse Del | ls -== snap swap-parent grid=100x31 cursor=4,10 -|7: /tmp/pardes-snap/splitparent/cwd Tty+bash Save -|8: Mode Filter Collapse Del -|9: ls +== snap swap-parent grid=100x31 cursor=4,13 +|10: /tmp/pardes-snap/splitparent/cwd Tty+bash Save +|11: Mode Filter Collapse Del +|12: ls == snap tallest grid=100x6 cursor=4,5 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol @@ -16,12 +19,15 @@ | /tmp/pardes-snap/splitparent/cwd Tty+bash Save Mode Filter Collapse Del | /tmp/pardes-snap/splitparent/cwd Tty+bash Save ls | e in that column: each keeps its tag and 2 rows -== snap regrown grid=100x31 cursor=4,10 +== snap regrown grid=100x31 cursor=4,13 |Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Exit | New Tty Find Grep Joincol Delcol New Tty Find Grep Joincol Delcol | /tmp/pardes-snap/splitparent/cwd Tty+bash Save /tmp/pardes-snap/splitparent/cwd Tty+bash Save -| /tmp/pardes-snap/splitparent/cwd Tty+bash Save Mode Filter Collapse Del -| Mode Filter Collapse Del ls +| Mode Filter Collapse Del Mode Filter Collapse Del +| ls ls +| +| /tmp/pardes-snap/splitparent/cwd Tty+bash Save +| Mode Filter Collapse Del | ls | | /tmp/pardes-snap/splitparent/cwd Tty+bash Save diff --git a/test/snapshots/splitparent.snap b/test/snapshots/splitparent.snap index 14391467..93ed9e21 100644 --- a/test/snapshots/splitparent.snap +++ b/test/snapshots/splitparent.snap @@ -1,4 +1,4 @@ -# a new pane never opens under a too-short parent: Alt-n from a tag-only pane +# a new pane never opens under a too-short parent: Alt-n from a pane too short to halve # splits the tall sibling in its column instead; when NO pane could leave the # newcomer 2 body rows, the tallest one anywhere is split, and when that one # cannot keep its own tag and 2 rows either, the pane is refused @@ -6,15 +6,16 @@ config Placement pardes start 31 100 -n 3 stable 700 20000 -# shrink pane 0 (top-left) to just its tagline: the pane below's grip dragged -# up under it (a terminal has no rule between panes; the grip is the handle) +# shrink pane 0 (top-left), a terminal, as far as it goes, its tag and 2 body +# rows: the pane below's grip dragged up under its tag (a terminal has no +# rule between panes; the grip is the handle) press left 1 18 drag left 1 3 release left 1 3 motion 40 25 stable 500 8000 snap shrunk -# Alt-n from the tag-only pane: the new shell opens below the tall sibling +# Alt-n from the short pane: the new shell opens below the tall sibling # underneath it, not as another 1-row sliver key a-n stable 700 15000 |
