summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-07-03 18:49:00 -0300
committerGabriel Schneider <[email protected]>2026-08-01 15:02:07 -0300
commita354d34329de597b80943c78e2afd84f235ee505 (patch)
tree6b161ea390a60864dd284a7b17dd8f025269567f
parentf3b62fe53b19e36f21f8020831718cbf095d6a7b (diff)
downloadpardes-a354d34329de597b80943c78e2afd84f235ee505.tar.gz
pardes-a354d34329de597b80943c78e2afd84f235ee505.zip
improving layout
-rw-r--r--build.zig12
-rw-r--r--main.zig6
-rw-r--r--pane_ui.zig258
-rw-r--r--replay.zig6
4 files changed, 248 insertions, 34 deletions
diff --git a/build.zig b/build.zig
index bc9308c8..fa987360 100644
--- a/build.zig
+++ b/build.zig
@@ -590,6 +590,18 @@ pub fn build(b: *std.Build) void {
unit_step.dependOn(&run_petscii.step);
test_step.dependOn(&run_petscii.step);
+ const pane_ui_tests = b.addTest(.{
+ .root_module = b.createModule(.{
+ .target = target,
+ .optimize = optimize,
+ .root_source_file = b.path("pane_ui.zig"),
+ }),
+ });
+ pane_ui_tests.linkage = exe_linkage;
+ const run_pane_ui = b.addRunArtifact(pane_ui_tests);
+ unit_step.dependOn(&run_pane_ui.step);
+ test_step.dependOn(&run_pane_ui.step);
+
const dump_tests = b.addTest(.{
.root_module = b.createModule(.{
.target = target,
diff --git a/main.zig b/main.zig
index cd760ecf..742d43b4 100644
--- a/main.zig
+++ b/main.zig
@@ -1114,6 +1114,10 @@ fn termVWeight(terms: *const [MAX_TERMS]?*Term, id: usize) ?f32 {
return if (terms[id]) |t| t.vweight else null;
}
+fn setTermVWeight(terms: *[MAX_TERMS]?*Term, id: usize, weight: f32) void {
+ if (terms[id]) |t| t.vweight = weight;
+}
+
// `alloc` must be a per-frame arena, NOT freed before vx.render(): vaxis is a
// retained renderer and stores each cell's grapheme as a *slice* into the text
// we hand it, so plainString has to outlive the render call.
@@ -3019,7 +3023,7 @@ pub fn main(init: std.process.Init) !void {
}
},
.move => |d| {
- moveTerm(&layout, &rects, &col_x, &col_w, d.id, d.cur_x, d.cur_y);
+ moveTerm(&layout, &terms, &rects, &col_x, &col_w, d.id, d.cur_x, d.cur_y, setTermVWeight);
// a file moved into the left column evicts a lone unused shell there
if (terms[d.id]) |mt| if (mt.file != null) {
if (layoutFindTerm(&layout, d.id)) |f| if (f.col == 0)
diff --git a/pane_ui.zig b/pane_ui.zig
index 6091cb5f..629992b8 100644
--- a/pane_ui.zig
+++ b/pane_ui.zig
@@ -1,3 +1,5 @@
+const std = @import("std");
+
pub const TOPBAR_H: u16 = 1;
pub const GUTTER: u16 = 2;
pub const BOX_H: u16 = 1;
@@ -71,6 +73,14 @@ pub fn layoutAppendColumn(l: anytype, id: usize) void {
l.ncol += 1;
}
+pub const MovePlacement = struct {
+ preview_col: usize,
+ above_id: usize,
+ row: u16,
+ above_y: u16,
+ above_h: u16,
+};
+
pub fn computeGeom(
l: anytype,
items: anytype,
@@ -113,7 +123,27 @@ pub fn computeGeom(
}
}
-pub fn moveTerm(
+fn targetColumn(l: anytype, col_x: anytype, col_w: anytype, cur_x: u16) usize {
+ var tc: usize = if (l.ncol > 0) l.ncol - 1 else 0;
+ for (0..l.ncol) |c| {
+ if (cur_x >= col_x[c] and cur_x < col_x[c] + col_w[c]) {
+ tc = c;
+ break;
+ }
+ }
+ return tc;
+}
+
+fn splitRowForExtent(y: u16, h: u16, cur_y: u16) ?u16 {
+ if (h < 2) return null;
+ const min_each: u16 = if (h >= MINH * 2) MINH else 1;
+ const lo = y +| min_each;
+ const hi = y + h - min_each;
+ if (lo > hi) return y + h / 2;
+ return std.math.clamp(cur_y, lo, hi);
+}
+
+pub fn movePlacement(
l: anytype,
rects: anytype,
col_x: anytype,
@@ -121,33 +151,110 @@ pub fn moveTerm(
id: usize,
cur_x: u16,
cur_y: u16,
-) void {
- const src = layoutFindTerm(l, id) orelse return;
- var tc: usize = if (l.ncol > 0) l.ncol - 1 else 0;
+) ?MovePlacement {
+ const src = layoutFindTerm(l, id) orelse return null;
+ const tc = targetColumn(l, col_x, col_w, cur_x);
+ if (tc == src.col and l.col_n[src.col] == 1) return null;
+ if (tc == src.col) {
+ const sr = rects[id];
+ if (cur_y >= sr.y and cur_y < sr.y + sr.h) return null;
+ }
+
+ const max_panes = l.col_terms[0].len;
+ var heights: [max_panes]u16 = @splat(0);
for (0..l.ncol) |c| {
- if (cur_x >= col_x[c] and cur_x < col_x[c] + col_w[c]) {
- tc = c;
- break;
+ var k: usize = 0;
+ while (k < l.col_n[c]) : (k += 1) {
+ const pid = l.col_terms[c][k];
+ heights[pid] = rects[pid].h;
}
}
- var ti: usize = 0;
+ if (l.col_n[src.col] > 1) {
+ const sib = if (src.idx > 0) l.col_terms[src.col][src.idx - 1] else l.col_terms[src.col][src.idx + 1];
+ heights[sib] +|= rects[id].h;
+ }
+
+ var y: u16 = TOPBAR_H;
+ var last: ?MovePlacement = null;
var k: usize = 0;
while (k < l.col_n[tc]) : (k += 1) {
- const cid = l.col_terms[tc][k];
- if (cid == id) continue;
- const r = rects[cid];
- if (cur_y < r.y + r.h / 2) break;
- ti += 1;
+ const pid = l.col_terms[tc][k];
+ if (pid == id) continue;
+ const h = heights[pid];
+ const row = splitRowForExtent(y, h, cur_y) orelse {
+ y +|= h;
+ continue;
+ };
+ const placement: MovePlacement = .{
+ .preview_col = tc,
+ .above_id = pid,
+ .row = row,
+ .above_y = y,
+ .above_h = h,
+ };
+ last = placement;
+ if (cur_y < y + h) return placement;
+ y +|= h;
+ }
+ return last;
+}
+
+fn setColumnWeights(
+ l: anytype,
+ items: anytype,
+ col: usize,
+ heights: anytype,
+ comptime setVWeight: anytype,
+) void {
+ if (col >= l.ncol) return;
+ var k: usize = 0;
+ while (k < l.col_n[col]) : (k += 1) {
+ const pid = l.col_terms[col][k];
+ setVWeight(items, pid, @as(f32, @floatFromInt(@max(1, heights[pid]))));
+ }
+}
+
+pub fn moveTerm(
+ l: anytype,
+ items: anytype,
+ rects: anytype,
+ col_x: anytype,
+ col_w: anytype,
+ id: usize,
+ cur_x: u16,
+ cur_y: u16,
+ comptime setVWeight: anytype,
+) void {
+ const placement = movePlacement(l, rects, col_x, col_w, id, cur_x, cur_y) orelse return;
+ const src = layoutFindTerm(l, id) orelse return;
+ const source_multi = l.col_n[src.col] > 1;
+
+ const max_panes = l.col_terms[0].len;
+ var heights: [max_panes]u16 = @splat(0);
+ for (0..l.ncol) |c| {
+ var k: usize = 0;
+ while (k < l.col_n[c]) : (k += 1) {
+ const pid = l.col_terms[c][k];
+ heights[pid] = rects[pid].h;
+ }
}
- const src_solo = l.col_n[src.col] == 1;
- if (tc == src.col and src_solo) return;
layoutRemove(l, id);
- if (src_solo and src.col < tc) tc -= 1;
- if (tc >= l.ncol) tc = l.ncol - 1;
- if (ti > l.col_n[tc]) ti = l.col_n[tc];
- layoutInsert(l, tc, ti, id);
+ if (source_multi and src.col < l.ncol and l.col_n[src.col] > 0) {
+ const sib = if (src.idx > 0) l.col_terms[src.col][src.idx - 1] else l.col_terms[src.col][src.idx];
+ heights[sib] +|= rects[id].h;
+ }
+
+ const af = layoutFindTerm(l, placement.above_id) orelse return;
+ const upper_h = @max(1, placement.row -| placement.above_y);
+ const lower_h = @max(1, placement.above_h -| upper_h);
+ heights[placement.above_id] = upper_h;
+ heights[id] = lower_h;
+ layoutInsert(l, af.col, af.idx + 1, id);
+
+ setColumnWeights(l, items, af.col, &heights, setVWeight);
+ if (source_multi and src.col < l.ncol and src.col != af.col) setColumnWeights(l, items, src.col, &heights, setVWeight);
}
pub fn paneAt(rects: anytype, pane_count: usize, col: u16, row: u16) ?usize {
@@ -241,19 +348,13 @@ pub fn drawBorderH(win: anytype, x: u16, width: u16, row: u16) void {
while (col < x + width) : (col += 1) overlayDash(win, col, row, "╌");
}
-pub fn drawMovePreview(win: anytype, l: anytype, col_x: anytype, col_w: anytype, cur_x: u16, cur_y: u16, color: [3]u8) void {
- var tc: usize = if (l.ncol > 0) l.ncol - 1 else 0;
- for (0..l.ncol) |c| {
- if (cur_x >= col_x[c] and cur_x < col_x[c] + col_w[c]) {
- tc = c;
- break;
- }
- }
- var col: u16 = col_x[tc];
- while (col < col_x[tc] + col_w[tc]) : (col += 1) {
- win.writeCell(col, cur_y, .{ .char = .{ .grapheme = "╌" }, .style = .{ .fg = .{ .rgb = color } } });
+pub fn drawMovePreview(win: anytype, l: anytype, rects: anytype, col_x: anytype, col_w: anytype, id: usize, cur_x: u16, cur_y: u16, color: [3]u8) void {
+ const placement = movePlacement(l, rects, col_x, col_w, id, cur_x, cur_y) orelse return;
+ var col: u16 = col_x[placement.preview_col];
+ while (col < col_x[placement.preview_col] + col_w[placement.preview_col]) : (col += 1) {
+ win.writeCell(col, placement.row, .{ .char = .{ .grapheme = "╌" }, .style = .{ .fg = .{ .rgb = color } } });
}
- win.writeCell(cur_x, cur_y, .{ .char = .{ .grapheme = "▌" }, .style = .{ .fg = .{ .rgb = color } } });
+ win.writeCell(cur_x, placement.row, .{ .char = .{ .grapheme = "▌" }, .style = .{ .fg = .{ .rgb = color } } });
}
pub fn drawHoverResizeHints(
@@ -296,7 +397,100 @@ pub fn drawInteractionPreview(
switch (drag) {
.border_v => |d| drawBorderV(win, d.cur_x, screen_h),
.border_h => |d| drawBorderH(win, col_x[d.col], col_w[d.col], d.cur_y),
- .move => |d| drawMovePreview(win, l, col_x, col_w, d.cur_x, d.cur_y, move_color),
+ .move => |d| drawMovePreview(win, l, rects, col_x, col_w, d.id, d.cur_x, d.cur_y, move_color),
else => drawHoverResizeHints(win, l, rects, col_x, col_w, screen_h, hover_col, hover_row),
}
}
+
+const TestLayout = Layout(4, 8);
+const TestItem = struct { vweight: f32 = 1 };
+
+fn testVWeight(items: []const TestItem, id: usize) ?f32 {
+ return items[id].vweight;
+}
+
+fn setTestVWeight(items: []TestItem, id: usize, weight: f32) void {
+ items[id].vweight = weight;
+}
+
+fn expectYH(r: Rect, y: u16, h: u16) !void {
+ try std.testing.expectEqual(y, r.y);
+ try std.testing.expectEqual(h, r.h);
+}
+
+test "move into one-pane column appends at marker row" {
+ var layout: TestLayout = .{
+ .ncol = 2,
+ .col_weight = @splat(1),
+ .col_terms = undefined,
+ .col_n = @splat(0),
+ };
+ layout.col_n[0] = 2;
+ layout.col_terms[0][0] = 0;
+ layout.col_terms[0][1] = 1;
+ layout.col_n[1] = 1;
+ layout.col_terms[1][0] = 2;
+
+ var items = [_]TestItem{ .{ .vweight = 10 }, .{ .vweight = 20 }, .{ .vweight = 30 }, .{} };
+ var rects: [8]Rect = undefined;
+ var col_x: [4]u16 = undefined;
+ var col_w: [4]u16 = undefined;
+ computeGeom(&layout, items[0..], 100, 31, &rects, &col_x, &col_w, testVWeight);
+
+ const placement = movePlacement(&layout, &rects, &col_x, &col_w, 0, 75, 18) orelse return error.NoMovePlacement;
+ try std.testing.expectEqual(@as(usize, 2), placement.above_id);
+ try std.testing.expectEqual(@as(u16, 18), placement.row);
+
+ moveTerm(&layout, items[0..], &rects, &col_x, &col_w, 0, 75, 18, setTestVWeight);
+ computeGeom(&layout, items[0..], 100, 31, &rects, &col_x, &col_w, testVWeight);
+
+ try std.testing.expectEqual(@as(usize, 2), layout.ncol);
+ try std.testing.expectEqual(@as(usize, 1), layout.col_n[0]);
+ try std.testing.expectEqual(@as(usize, 2), layout.col_n[1]);
+ try std.testing.expectEqual(@as(usize, 2), layout.col_terms[1][0]);
+ try std.testing.expectEqual(@as(usize, 0), layout.col_terms[1][1]);
+ try expectYH(rects[1], 1, 30);
+ try expectYH(rects[2], 1, 17);
+ try expectYH(rects[0], 18, 13);
+}
+
+test "move into multi-pane column only splits the pane above" {
+ var layout: TestLayout = .{
+ .ncol = 2,
+ .col_weight = @splat(1),
+ .col_terms = undefined,
+ .col_n = @splat(0),
+ };
+ layout.col_n[0] = 3;
+ layout.col_terms[0][0] = 0;
+ layout.col_terms[0][1] = 1;
+ layout.col_terms[0][2] = 2;
+ layout.col_n[1] = 1;
+ layout.col_terms[1][0] = 3;
+
+ var items = [_]TestItem{ .{ .vweight = 10 }, .{ .vweight = 10 }, .{ .vweight = 10 }, .{ .vweight = 30 } };
+ var rects: [8]Rect = undefined;
+ var col_x: [4]u16 = undefined;
+ var col_w: [4]u16 = undefined;
+ computeGeom(&layout, items[0..], 100, 31, &rects, &col_x, &col_w, testVWeight);
+ const before_top = rects[0];
+ const before_bottom = rects[2];
+
+ const placement = movePlacement(&layout, &rects, &col_x, &col_w, 3, 20, 15) orelse return error.NoMovePlacement;
+ try std.testing.expectEqual(@as(usize, 1), placement.above_id);
+ try std.testing.expectEqual(@as(u16, 15), placement.row);
+
+ moveTerm(&layout, items[0..], &rects, &col_x, &col_w, 3, 20, 15, setTestVWeight);
+ computeGeom(&layout, items[0..], 100, 31, &rects, &col_x, &col_w, testVWeight);
+
+ try std.testing.expectEqual(@as(usize, 1), layout.ncol);
+ try std.testing.expectEqual(@as(usize, 4), layout.col_n[0]);
+ try std.testing.expectEqual(@as(usize, 0), layout.col_terms[0][0]);
+ try std.testing.expectEqual(@as(usize, 1), layout.col_terms[0][1]);
+ try std.testing.expectEqual(@as(usize, 3), layout.col_terms[0][2]);
+ try std.testing.expectEqual(@as(usize, 2), layout.col_terms[0][3]);
+ try expectYH(rects[0], before_top.y, before_top.h);
+ try expectYH(rects[1], 11, 4);
+ try expectYH(rects[3], 15, 6);
+ try expectYH(rects[2], before_bottom.y, before_bottom.h);
+}
diff --git a/replay.zig b/replay.zig
index e08a461c..19b894fe 100644
--- a/replay.zig
+++ b/replay.zig
@@ -165,6 +165,10 @@ fn paneVWeight(panes: []const Pane, id: usize) ?f32 {
return panes[id].vweight;
}
+fn setPaneVWeight(panes: []Pane, id: usize, weight: f32) void {
+ panes[id].vweight = weight;
+}
+
fn renderPane(alloc: std.mem.Allocator, root: ui.Window, p: *Pane, r: Rect, active: bool) !void {
if (r.w <= GUTTER or r.h == 0) return;
const text = root.child(.{ .x_off = r.x + GUTTER, .y_off = r.y, .width = r.w - GUTTER, .height = r.h });
@@ -629,7 +633,7 @@ const App = struct {
self.panes[b].vweight = pair - self.panes[a].vweight;
}
},
- .move => |d| moveTerm(&self.layout, &self.rects, &self.col_x, &self.col_w, d.id, d.cur_x, d.cur_y),
+ .move => |d| moveTerm(&self.layout, self.panes, &self.rects, &self.col_x, &self.col_w, d.id, d.cur_x, d.cur_y, setPaneVWeight),
.none => {},
}
self.drag = .none;