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 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; } } pub fn moveTerm( l: anytype, rects: anytype, col_x: anytype, col_w: anytype, 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; for (0..l.ncol) |c| { if (cur_x >= col_x[c] and cur_x < col_x[c] + col_w[c]) { tc = c; break; } } var ti: usize = 0; 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 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); } 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, 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 } } }); } win.writeCell(cur_x, cur_y, .{ .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, col_x, col_w, d.cur_x, d.cur_y, move_color), else => drawHoverResizeHints(win, l, rects, col_x, col_w, screen_h, hover_col, hover_row), } }