diff options
| -rw-r--r-- | build.zig | 12 | ||||
| -rw-r--r-- | main.zig | 6 | ||||
| -rw-r--r-- | pane_ui.zig | 258 | ||||
| -rw-r--r-- | replay.zig | 6 |
4 files changed, 248 insertions, 34 deletions
@@ -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, @@ -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); +} @@ -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; |
