diff options
| author | Gabriel Schneider <[email protected]> | 2026-07-05 20:21:50 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-01 15:02:07 -0300 |
| commit | de4def3548a6729b0dfd2120495a61beef8c8c2c (patch) | |
| tree | 12144f0f64dc96bb4741459c1f9db57f44349330 /pane_ui.zig | |
| parent | 7988d9bc6e31210ff13994f18d5622dd57ba9341 (diff) | |
| download | pardes-de4def3548a6729b0dfd2120495a61beef8c8c2c.tar.gz pardes-de4def3548a6729b0dfd2120495a61beef8c8c2c.zip | |
pardes v2: the rewrite, complete and organized. src/ (core + three shells), test/ (snapshot parity harness + 18 frozen goldens). One sans-IO core, vaxis tty + SDL3 GPU native + wasm web shells, 18/18 parity with the purged prototype, 7.6k lines vs 12.1k. Fix: gui shell pre-sized the core at init so the greet-releasing resize never fired (blank panes until first interaction); live sessions now init at defaults and get the real grid as a resize event (the shell contract, documented on Options).
Diffstat (limited to 'pane_ui.zig')
| -rw-r--r-- | pane_ui.zig | 496 |
1 files changed, 0 insertions, 496 deletions
diff --git a/pane_ui.zig b/pane_ui.zig deleted file mode 100644 index 629992b8..00000000 --- a/pane_ui.zig +++ /dev/null @@ -1,496 +0,0 @@ -const std = @import("std"); - -pub const TOPBAR_H: u16 = 1; -pub const GUTTER: u16 = 2; -pub const BOX_H: u16 = 1; -pub const MINW: u16 = 10; -pub const MINH: u16 = 3; -pub const PREFIX_W: u16 = 5; - -pub const Rect = struct { x: u16, y: u16, w: u16, h: u16 }; - -pub const MousePos = struct { col: u16, row: u16 }; - -pub const ScrollbarButton = enum { left, middle, right }; - -pub const Scrollbar = struct { - total: usize, - offset: usize, - len: usize, -}; - -pub fn Layout(comptime max_cols: usize, comptime max_panes: usize) type { - return struct { - ncol: usize, - col_weight: [max_cols]f32, - col_terms: [max_cols][max_panes]usize, - col_n: [max_cols]usize, - }; -} - -pub fn layoutFindTerm(l: anytype, id: usize) ?struct { col: usize, idx: usize } { - var c: usize = 0; - while (c < l.ncol) : (c += 1) { - var k: usize = 0; - while (k < l.col_n[c]) : (k += 1) { - if (l.col_terms[c][k] == id) return .{ .col = c, .idx = k }; - } - } - return null; -} - -pub fn layoutRemove(l: anytype, id: usize) void { - const f = layoutFindTerm(l, id) orelse return; - const c = f.col; - var k = f.idx; - while (k + 1 < l.col_n[c]) : (k += 1) l.col_terms[c][k] = l.col_terms[c][k + 1]; - l.col_n[c] -= 1; - if (l.col_n[c] == 0) { - if (l.ncol > 1) l.col_weight[if (c > 0) c - 1 else c + 1] += l.col_weight[c]; - var j = c; - while (j + 1 < l.ncol) : (j += 1) { - l.col_terms[j] = l.col_terms[j + 1]; - l.col_n[j] = l.col_n[j + 1]; - l.col_weight[j] = l.col_weight[j + 1]; - } - l.ncol -= 1; - } -} - -pub fn layoutInsert(l: anytype, c: usize, idx: usize, id: usize) void { - var k = l.col_n[c]; - while (k > idx) : (k -= 1) l.col_terms[c][k] = l.col_terms[c][k - 1]; - l.col_terms[c][idx] = id; - l.col_n[c] += 1; -} - -pub fn layoutAppendColumn(l: anytype, id: usize) void { - if (l.ncol >= l.col_weight.len) return; - const c = l.ncol; - l.col_weight[c] = 1; - l.col_terms[c][0] = id; - l.col_n[c] = 1; - 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, - sw: u16, - sh: u16, - rects: anytype, - col_x: anytype, - col_w: anytype, - comptime vweightFn: anytype, -) void { - if (l.ncol == 0) return; - var wsum: f32 = 0; - for (0..l.ncol) |c| wsum += l.col_weight[c]; - if (wsum <= 0) wsum = 1; - - var x: u16 = 0; - for (0..l.ncol) |c| { - const last = c + 1 == l.ncol; - const cw: u16 = if (last) (sw -| x) else @max(1, @as(u16, @intFromFloat(@round(@as(f32, @floatFromInt(sw)) * l.col_weight[c] / wsum)))); - col_x[c] = x; - col_w[c] = cw; - - var vsum: f32 = 0; - for (0..l.col_n[c]) |k| { - if (vweightFn(items, l.col_terms[c][k])) |vweight| vsum += vweight; - } - if (vsum <= 0) vsum = 1; - - var y: u16 = TOPBAR_H; - const avail_h = sh -| TOPBAR_H; - for (0..l.col_n[c]) |k| { - const id = l.col_terms[c][k]; - const vweight = vweightFn(items, id) orelse continue; - const lastk = k + 1 == l.col_n[c]; - const ch: u16 = if (lastk) (sh -| y) else @max(1, @as(u16, @intFromFloat(@round(@as(f32, @floatFromInt(avail_h)) * vweight / vsum)))); - rects[id] = .{ .x = x, .y = y, .w = cw, .h = ch }; - y +|= ch; - } - x +|= cw; - } -} - -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, - col_w: anytype, - id: usize, - cur_x: u16, - cur_y: u16, -) ?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| { - var k: usize = 0; - while (k < l.col_n[c]) : (k += 1) { - const pid = l.col_terms[c][k]; - heights[pid] = rects[pid].h; - } - } - - 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 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; - } - } - - layoutRemove(l, 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 { - for (0..pane_count) |i| { - const r = rects[i]; - if (col >= r.x and col < r.x + r.w and row >= r.y and row < r.y + r.h) return i; - } - return null; -} - -pub fn paneAtSlots(rects: anytype, slots: anytype, col: u16, row: u16) ?usize { - for (slots.*, 0..) |slot, i| { - if (slot == null) continue; - const r = rects[i]; - if (col >= r.x and col < r.x + r.w and row >= r.y and row < r.y + r.h) return i; - } - return null; -} - -pub fn clampMousePos(screen_w: u16, screen_h: u16, mouse: anytype) MousePos { - return .{ - .col = if (mouse.col < 0) 0 else @min(@as(u16, @intCast(mouse.col)), screen_w -| 1), - .row = if (mouse.row < 0) 0 else @min(@as(u16, @intCast(mouse.row)), screen_h -| 1), - }; -} - -pub fn inMoveBox(r: Rect, col: u16, row: u16) bool { - return col < r.x + GUTTER and row < r.y + BOX_H; -} - -pub fn scrollbarClickDelta(r: Rect, button: ScrollbarButton, col: u16, row: u16) ?i32 { - if (col >= r.x + GUTTER or row < r.y + BOX_H) return null; - const local: i32 = @as(i32, row) - @as(i32, r.y + BOX_H); - return switch (button) { - .left, .middle => -local, - .right => local, - }; -} - -pub fn drawGutter( - root: anytype, - r: Rect, - active: bool, - active_box: [3]u8, - inactive_box: [3]u8, - track: [3]u8, - thumb: [3]u8, - scrollbar: ?Scrollbar, -) void { - const gut = root.child(.{ .x_off = r.x, .y_off = r.y, .width = GUTTER, .height = r.h }); - const box_bg = if (active) active_box else inactive_box; - var gy: u16 = 0; - while (gy < r.h) : (gy += 1) { - const bg = if (gy < BOX_H) box_bg else track; - var gx: u16 = 0; - while (gx < GUTTER) : (gx += 1) { - gut.writeCell(gx, gy, .{ .char = .{ .grapheme = " " }, .style = .{ .bg = .{ .rgb = bg } } }); - } - } - - const sb = scrollbar orelse return; - const track_h: usize = if (r.h > BOX_H) r.h - BOX_H else 0; - if (track_h == 0) return; - const total = if (sb.total == 0) 1 else sb.total; - const len = @max(1, (track_h * @max(1, sb.len)) / total); - const pos = (track_h * sb.offset) / total; - var sy: usize = 0; - while (sy < track_h) : (sy += 1) { - if (sy < pos or sy >= pos + len) continue; - var gx: u16 = 0; - while (gx < GUTTER) : (gx += 1) { - gut.writeCell(gx, @intCast(BOX_H + sy), .{ .char = .{ .grapheme = " " }, .style = .{ .bg = .{ .rgb = thumb } } }); - } - } -} - -pub fn overlayDash(win: anytype, col: u16, row: u16, glyph: []const u8) void { - var cell = win.readCell(col, row) orelse return; - cell.char.grapheme = glyph; - cell.default = false; - win.writeCell(col, row, cell); -} - -pub fn drawBorderV(win: anytype, col: u16, screen_h: u16) void { - var row: u16 = TOPBAR_H; - while (row < screen_h) : (row += 1) overlayDash(win, col, row, "╎"); -} - -pub fn drawBorderH(win: anytype, x: u16, width: u16, row: u16) void { - var col = x; - while (col < x + width) : (col += 1) overlayDash(win, col, row, "╌"); -} - -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, placement.row, .{ .char = .{ .grapheme = "▌" }, .style = .{ .fg = .{ .rgb = color } } }); -} - -pub fn drawHoverResizeHints( - win: anytype, - l: anytype, - rects: anytype, - col_x: anytype, - col_w: anytype, - screen_h: u16, - hover_col: u16, - hover_row: u16, -) void { - var c: usize = 0; - while (c + 1 < l.ncol) : (c += 1) { - if (hover_col == col_x[c] + col_w[c] -| 1) drawBorderV(win, hover_col, screen_h); - } - var cc: usize = 0; - while (cc < l.ncol) : (cc += 1) { - if (hover_col < col_x[cc] or hover_col >= col_x[cc] + col_w[cc]) continue; - var k: usize = 0; - while (k + 1 < l.col_n[cc]) : (k += 1) { - const r = rects[l.col_terms[cc][k]]; - if (hover_row == r.y + r.h -| 1) drawBorderH(win, col_x[cc], col_w[cc], hover_row); - } - } -} - -pub fn drawInteractionPreview( - win: anytype, - l: anytype, - rects: anytype, - col_x: anytype, - col_w: anytype, - screen_h: u16, - drag: anytype, - hover_col: u16, - hover_row: u16, - move_color: [3]u8, -) void { - 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, 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); -} |
