summaryrefslogtreecommitdiff
path: root/src/layout.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/layout.zig')
-rw-r--r--src/layout.zig327
1 files changed, 320 insertions, 7 deletions
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,