From 682e237df7e8b22f820d14a4adee58e6e2f84268 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Fri, 11 Sep 2026 12:38:56 -0300 Subject: trunk: resume before the Reload experiment Empty marker on the last pre-Reload change. Keep the Reload experiment on reload (3801914), its first change on reload-start (200a1fc), and the unfinished performance investigation on reload-perf-wip. --- src/layout.zig | 327 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 320 insertions(+), 7 deletions(-) (limited to 'src/layout.zig') diff --git a/src/layout.zig b/src/layout.zig index 51944856..6c5f4474 100644 --- a/src/layout.zig +++ b/src/layout.zig @@ -669,7 +669,7 @@ pub fn movePlacement(p: *Pardes, id: usize, cur_x: u16, cur_y: u16) ?MovePlaceme const sib = if (src.idx > 0) p.col_panes[src.col][src.idx - 1] else p.col_panes[src.col][src.idx + 1]; heights[sib] +|= p.rects[id].h; } - var y: u16 = TOPBAR_H; + var y: u16 = TOPBAR_H + p.columnBarHeight(); var last: ?MovePlacement = null; for (0..p.col_n[tc]) |k| { const pid = p.col_panes[tc][k]; @@ -723,7 +723,9 @@ pub fn setColumnWeights(p: *Pardes, col: usize, heights: *const [MAX_PANES]u16) if (col >= p.ncol) return; for (0..p.col_n[col]) |k| { const pid = p.col_panes[col][k]; - if (p.panes[pid]) |pane| pane.vweight = @floatFromInt(@max(1, heights[pid])); + if (p.panes[pid]) |pane| if (!pane.collapsed) { + pane.vweight = @floatFromInt(@max(1, heights[pid])); + }; } } @@ -736,6 +738,17 @@ pub fn applyRowSplit(p: *Pardes, cc: usize, k: usize, cur_y: u16) void { 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)); + 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. + if ((a.collapsed and nt > BOX_H) or (b.collapsed and combined - nt > BOX_H)) { + snapColWeights(p, cc); + a.vweight = @floatFromInt(@max(1, ra.h)); + b.vweight = @floatFromInt(@max(1, rb.h)); + if (nt > BOX_H) a.collapsed = false; + if (combined - nt > BOX_H) b.collapsed = false; + } + if (a.collapsed or b.collapsed) return; const pair = a.vweight + b.vweight; a.vweight = pair * (nt / combined); b.vweight = pair - a.vweight; @@ -770,8 +783,11 @@ pub fn removePane(p: *Pardes, id: usize) void { p.col_panes[j] = p.col_panes[j + 1]; p.col_n[j] = p.col_n[j + 1]; p.col_weight[j] = p.col_weight[j + 1]; + p.col_tags[j] = p.col_tags[j + 1]; } p.ncol -= 1; + p.col_tags[p.ncol] = .{}; + p.topbar_col = null; } } @@ -788,8 +804,11 @@ pub fn joinCol(p: *Pardes) void { p.col_panes[j] = p.col_panes[j + 1]; p.col_n[j] = p.col_n[j + 1]; p.col_weight[j] = p.col_weight[j + 1]; + p.col_tags[j] = p.col_tags[j + 1]; } p.ncol -= 1; + p.col_tags[p.ncol] = .{}; + p.topbar_col = null; } pub fn canSplitColumn(p: *Pardes, source_id: usize) bool { @@ -833,8 +852,11 @@ pub fn splitColumn(p: *Pardes, source_id: usize, id: usize, before: bool) bool { p.col_panes[j] = p.col_panes[j - 1]; p.col_n[j] = p.col_n[j - 1]; p.col_weight[j] = p.col_weight[j - 1]; + p.col_tags[j] = p.col_tags[j - 1]; } p.col_weight[c] = new_weight; + p.col_tags[c] = .{}; + p.topbar_col = null; p.col_panes[c][0] = id; p.col_n[c] = 1; p.ncol += 1; @@ -844,7 +866,9 @@ pub fn splitColumn(p: *Pardes, source_id: usize, id: usize, before: bool) bool { pub fn snapColWeights(p: *Pardes, c: usize) void { for (0..p.col_n[c]) |k| { const pid = p.col_panes[c][k]; - if (p.panes[pid]) |pp| pp.vweight = @floatFromInt(@max(1, p.rects[pid].h)); + if (p.panes[pid]) |pp| if (!pp.collapsed) { + pp.vweight = @floatFromInt(@max(1, p.rects[pid].h)); + }; } } @@ -886,11 +910,11 @@ pub fn splitBelow(p: *Pardes, src_id: usize, nw: *Pane) void { const keep = std.math.clamp(cur, 1, @max(1, body -| (BOX_H + 3))); if (findPane(p, src_id)) |f| for (0..p.col_n[f.col]) |k| { const pid = p.col_panes[f.col][k]; - if (p.panes[pid]) |pp| if (pp != nw) { + if (p.panes[pid]) |pp| if (pp != nw and !pp.collapsed) { pp.vweight = @floatFromInt(@max(1, p.rects[pid].h)); }; }; - src.vweight = @floatFromInt(BOX_H + keep); + if (!src.collapsed) src.vweight = @floatFromInt(BOX_H + keep); nw.vweight = @floatFromInt(@max(1, src_h -| (BOX_H + keep))); if (nw.file) |f| if (!panes.Output.fileTraits(f.output).doc) { // trimmed: every row ends in a newline, and the empty line after @@ -924,6 +948,81 @@ pub fn columnBoundary(width: u16, prefix: u128, total: u128) u16 { return @intCast(@min(@as(u128, width), pixels)); } +pub fn toggleCollapse(p: *Pardes, id: usize) void { + const pane = p.panes[id] orelse return; + const at = findPane(p, id) orelse return; + compute(p); + const was_collapsed = pane.collapsed; + const old_height = p.rects[id].h; + const remembered_height = pane.vweight; + // Convert only visible panes to row weights. The toggle then changes one + // pair, rather than letting the column redistribute the released space. + snapColWeights(p, at.col); + const neighbor = collapseNeighbor(p, at.col, at.idx); + if (was_collapsed) { + if (neighbor) |other| { + const wanted: u16 = @intFromFloat(@min(@as(f32, @floatFromInt(std.math.maxInt(u16))), @max(1, @round(remembered_height)))); + const give = @min(wanted -| old_height, p.rects[other].h -| BOX_H); + p.panes[other].?.vweight -= @floatFromInt(give); + pane.vweight = @floatFromInt(@max(1, old_height + give)); + } + pane.collapsed = false; + } else { + // A folded pane's weight stores its former height, including its tag, + // so Dump/Restore and a later expansion retain a useful target size. + pane.vweight = @floatFromInt(@max(1, old_height)); + pane.collapsed = true; + if (neighbor) |other| + p.panes[other].?.vweight += @floatFromInt(old_height -| BOX_H); + } + compute(p); +} + +fn collapseNeighbor(p: *Pardes, col: usize, index: usize) ?usize { + var above = index; + while (above > 0) { + above -= 1; + const id = p.col_panes[col][above]; + if (p.panes[id]) |pane| if (!pane.collapsed) return id; + } + for (p.col_panes[col][index + 1 .. p.col_n[col]]) |id| + if (p.panes[id]) |pane| if (!pane.collapsed) return id; + return null; +} + +fn computeCollapsedColumn(p: *Pardes, c: usize, x: u16, width: u16, folded: usize) void { + var y: u16 = TOPBAR_H + p.columnBarHeight(); + const available = p.screen_h -| y; + var expanded_left: usize = 0; + var weight: f32 = 0; + for (p.col_panes[c][0..p.col_n[c]]) |id| if (p.panes[id]) |pane| { + if (!pane.collapsed) { + expanded_left += 1; + weight += pane.vweight; + } + }; + const expanded_height = available -| @as(u16, @intCast(folded)); + var budget = expanded_height; + var folded_left = folded; + for (p.col_panes[c][0..p.col_n[c]]) |id| { + const pane = p.panes[id] orelse continue; + const room = p.screen_h -| y; + const height: u16 = if (pane.collapsed) blk: { + folded_left -= 1; + break :blk @min(room, BOX_H); + } else blk: { + expanded_left -= 1; + const limit = @min(budget, room -| @as(u16, @intCast(folded_left))); + const share = @as(f32, @floatFromInt(expanded_height)) * pane.vweight / (if (weight > 0) weight else 1); + const h = if (expanded_left == 0) limit else @min(limit -| @as(u16, @intCast(expanded_left)), @max(1, @as(u16, @intFromFloat(@round(share))))); + budget -|= h; + break :blk h; + }; + p.rects[id] = .{ .x = x, .y = y, .w = width, .h = height }; + y +|= height; + } +} + pub fn compute(p: *Pardes) void { p.rects = @splat(.{}); if (p.ncol == 0) return; @@ -945,14 +1044,23 @@ pub fn compute(p: *Pardes) void { p.col_x[c] = x; p.col_w[c] = cw; + var folded: usize = 0; + for (p.col_panes[c][0..p.col_n[c]]) |id| if (p.panes[id]) |pane| { + if (pane.collapsed) folded += 1; + }; + if (folded > 0) { + computeCollapsedColumn(p, c, x, cw, folded); + continue; + } + var vsum: f32 = 0; for (0..p.col_n[c]) |k| { if (p.panes[p.col_panes[c][k]]) |pane| vsum += pane.vweight; } if (vsum <= 0) vsum = 1; - var y: u16 = TOPBAR_H; - const avail_h = p.screen_h -| TOPBAR_H; + var y: u16 = TOPBAR_H + p.columnBarHeight(); + const avail_h = p.screen_h -| y; for (0..p.col_n[c]) |k| { const id = p.col_panes[c][k]; const pane = p.panes[id] orelse continue; @@ -968,6 +1076,211 @@ pub fn compute(p: *Pardes) void { pub const ascii_max_movement_frames: u16 = 12; +test "collapse transfers height upward and restores the original layout" { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 32 }); + defer p.deinit(); + _ = try p.newShell(1, ""); + _ = try p.newShell(2, ""); + insert(p, 0, 1, 1); + insert(p, 0, 2, 2); + p.panes[0].?.vweight = 3; + p.panes[1].?.vweight = 2; + compute(p); + const original = p.rects; + toggleCollapse(p, 1); + try std.testing.expectEqual(@as(u16, 1), p.rects[1].h); + try std.testing.expectEqual(@as(f32, @floatFromInt(original[1].h)), p.panes[1].?.vweight); + try std.testing.expectEqual(p.screen_h, p.rects[2].y + p.rects[2].h); + try std.testing.expect(p.rects[0].h > original[0].h); + try std.testing.expectEqualDeep(original[2], p.rects[2]); + toggleCollapse(p, 1); + try std.testing.expectEqualDeep(original, p.rects); + toggleCollapse(p, 2); + try std.testing.expectEqual(@as(u16, 1), p.rects[2].h); + try std.testing.expectEqual(p.screen_h, p.rects[2].y + p.rects[2].h); +} + +test "collapse changes only one neighbor at the top middle and bottom" { + for (0..4) |target| { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 58 }); + defer p.deinit(); + for (1..4) |id| { + _ = try p.newShell(id, ""); + insert(p, 0, id, id); + } + for ([_]f32{ 11, 7, 5, 3 }, 0..) |weight, id| p.panes[id].?.vweight = weight; + compute(p); + const original = p.rects; + const recipient = if (target > 0) target - 1 else 1; + for (0..5) |_| { + toggleCollapse(p, target); + try std.testing.expectEqual(@as(u16, BOX_H), p.rects[target].h); + try std.testing.expectEqual(original[recipient].h + original[target].h - BOX_H, p.rects[recipient].h); + for (0..4) |id| if (id != target and id != recipient) + try std.testing.expectEqualDeep(original[id], p.rects[id]); + toggleCollapse(p, target); + try std.testing.expectEqualDeep(original, p.rects); + } + } +} + +test "collapse skips folded neighbors and expansion reclaims only its neighbor" { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 62 }); + defer p.deinit(); + for (1..5) |id| { + _ = try p.newShell(id, ""); + insert(p, 0, id, id); + } + compute(p); + toggleCollapse(p, 1); + toggleCollapse(p, 2); + const original = p.rects; + toggleCollapse(p, 3); + try std.testing.expectEqual(original[0].h + original[3].h - BOX_H, p.rects[0].h); + try std.testing.expectEqualDeep(original[4], p.rects[4]); + for (1..4) |id| try std.testing.expectEqual(@as(u16, BOX_H), p.rects[id].h); + toggleCollapse(p, 3); + try std.testing.expectEqualDeep(original, p.rects); + // With no expanded pane above, give the space to the first one below. + toggleCollapse(p, 0); + const with_folded_above = p.rects; + toggleCollapse(p, 3); + try std.testing.expectEqual(with_folded_above[4].h + with_folded_above[3].h - BOX_H, p.rects[4].h); + for (0..3) |id| try std.testing.expectEqualDeep(with_folded_above[id], p.rects[id]); + toggleCollapse(p, 3); + try std.testing.expectEqualDeep(with_folded_above, p.rects); +} + +test "collapse local transfers remain bounded through repeated tiny window resizes" { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 58 }); + defer p.deinit(); + for (1..5) |id| { + _ = try p.newShell(id, ""); + insert(p, 0, id, id); + } + for (0..180) |step| { + p.screen_h = @intCast((step * 17) % 81); + p.settings.column_tags = step % 2 == 0; + p.settings.tag_bottom = step % 3 == 0; + compute(p); + toggleCollapse(p, step % 5); + var used: u16 = 0; + for (0..5) |id| { + const pane = p.panes[id].?; + const rect = p.rects[id]; + try std.testing.expect(std.math.isFinite(pane.vweight) and pane.vweight > 0); + try std.testing.expect(rect.h <= p.screen_h -| rect.y); + if (pane.collapsed) try std.testing.expect(rect.h <= BOX_H); + used += rect.h; + } + try std.testing.expect(used <= p.screen_h); + } +} + +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(); + for (1..4) |id| { + _ = try p.newShell(id, ""); + insert(p, 0, id, id); + } + compute(p); + toggleCollapse(p, 2); + snapColWeights(p, 0); + // An intervening resize gave most of the donor's space to another pane. + p.panes[0].?.vweight += p.panes[1].?.vweight - 2; + p.panes[1].?.vweight = 2; + compute(p); + const before = p.rects; + toggleCollapse(p, 2); + try std.testing.expectEqual(@as(u16, 2), p.rects[2].h); + try std.testing.expectEqual(@as(u16, BOX_H), p.rects[1].h); + try std.testing.expectEqualDeep(before[0], p.rects[0]); + try std.testing.expectEqualDeep(before[3], p.rects[3]); +} + +test "collapse single and all panes leave unused column space" { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 32 }); + defer p.deinit(); + toggleCollapse(p, 0); + try std.testing.expectEqual(@as(u16, 1), p.rects[0].h); + _ = try p.newShell(1, ""); + insert(p, 0, 1, 1); + toggleCollapse(p, 1); + try std.testing.expectEqual(@as(u16, 1), p.rects[0].h); + try std.testing.expectEqual(@as(u16, 1), p.rects[1].h); + try std.testing.expectEqual(p.rects[0].y + 1, p.rects[1].y); + try std.testing.expect(p.rects[1].y + 1 < p.screen_h); + toggleCollapse(p, 0); + try std.testing.expectEqual(p.screen_h - 1, p.rects[1].y); +} + +test "collapse survives tiny resizes column tags and TagBottom" { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 32 }); + defer p.deinit(); + _ = try p.newShell(1, ""); + _ = try p.newShell(2, ""); + insert(p, 0, 1, 1); + insert(p, 0, 2, 2); + p.panes[1].?.collapsed = true; + for ([_]bool{ false, true }) |bottom| { + p.settings.tag_bottom = bottom; + for ([_]bool{ false, true }) |column_tags| { + p.settings.column_tags = column_tags; + for (0..50) |height| { + p.screen_h = @intCast(height); + compute(p); + var used: u16 = 0; + for (p.col_panes[0][0..p.col_n[0]]) |id| { + const rect = p.rects[id]; + try std.testing.expect(rect.h <= p.screen_h -| rect.y); + used += rect.h; + } + try std.testing.expect(used <= p.screen_h); + if (height >= 5) try std.testing.expectEqual(@as(u16, 1), p.rects[1].h); + } + } + } + p.screen_h = 32; + compute(p); + try std.testing.expectEqual(@as(u16, 1), p.rects[1].h); +} + +test "collapse can be expanded by dragging its boundary" { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 32 }); + defer p.deinit(); + _ = try p.newShell(1, ""); + insert(p, 0, 1, 1); + toggleCollapse(p, 0); + applyRowSplit(p, 0, 0, p.rects[0].y + 5); + compute(p); + try std.testing.expect(!p.panes[0].?.collapsed); + try std.testing.expectEqual(@as(u16, 6), p.rects[0].h); + toggleCollapse(p, 1); + p.settings.tag_bottom = true; + applyRowSplit(p, 0, 0, p.rects[1].y - 5); + compute(p); + try std.testing.expect(!p.panes[1].?.collapsed); + try std.testing.expectEqual(@as(u16, 6), p.rects[1].h); +} + +test "collapse dump restores folded state and retained expansion weight" { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 32 }); + defer p.deinit(); + p.panes[0].?.vweight = 7; + compute(p); + const original_height = p.rects[0].h; + toggleCollapse(p, 0); + try p.dumpState(); + const restored = try Pardes.initFromDump(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 32 }, p.dump_out.?); + defer restored.deinit(); + try std.testing.expect(restored.panes[0].?.collapsed); + try std.testing.expectEqual(@as(f32, @floatFromInt(original_height)), restored.panes[0].?.vweight); + try std.testing.expectEqual(@as(u16, 1), restored.rects[0].h); + toggleCollapse(restored, 0); + try std.testing.expectEqual(restored.screen_h - restored.rects[0].y, restored.rects[0].h); +} + pub const Easing = enum(u8) { linear, smooth, -- cgit v1.3