summaryrefslogtreecommitdiff
path: root/main.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-07-03 14:43:30 -0300
committerGabriel Schneider <[email protected]>2026-08-01 15:02:07 -0300
commitd1ed5bd8f55935e6470c4a7559216119571a084f (patch)
tree434478ce8a5dcd9aed367334816f54da9953e56d /main.zig
parent153f0a88862507cf1f5dda71f71db3d6649dfeaa (diff)
downloadpardes-d1ed5bd8f55935e6470c4a7559216119571a084f.tar.gz
pardes-d1ed5bd8f55935e6470c4a7559216119571a084f.zip
fixed scaling and fitting on web ui
Diffstat (limited to 'main.zig')
-rw-r--r--main.zig292
1 files changed, 38 insertions, 254 deletions
diff --git a/main.zig b/main.zig
index 019d2a44..184b1058 100644
--- a/main.zig
+++ b/main.zig
@@ -6,6 +6,7 @@ const modal = @import("modal.zig");
const image = @import("image.zig");
const dump = @import("dump.zig");
const paint = @import("paint.zig");
+const pane_ui = @import("pane_ui.zig");
// A TUI can't let std.log hit stderr — it's the same tty as our alt-screen, so the
// message lands on top of the UI (this is the stray text you saw when opening yazi:
@@ -155,26 +156,26 @@ fn parseCli(args: []const [:0]const u8) !Cli {
const Loop = vaxis.Loop(@TypeOf(Command.value));
// layout constants
-const TOPBAR_H: u16 = 1; // global tagbar row at the very top (full width)
-const GUTTER: u16 = 2; // left gutter: box (top) + scrollbar (below) per pane
-const BOX_H: u16 = 1; // rows of the drag/rearrange box; scrollbar starts below it
-const MINW: u16 = 10; // min pane width (cells)
-const MINH: u16 = 3; // min pane height (cells)
+const TOPBAR_H: u16 = pane_ui.TOPBAR_H; // global tagbar row at the very top (full width)
+const GUTTER: u16 = pane_ui.GUTTER; // left gutter: box (top) + scrollbar (below) per pane
+const BOX_H: u16 = pane_ui.BOX_H; // rows of the drag/rearrange box; scrollbar starts below it
+const MINW: u16 = pane_ui.MINW; // min pane width (cells)
+const MINH: u16 = pane_ui.MINH; // min pane height (cells)
const MAX_TERMS: usize = 16;
const MAX_COLS: usize = 6;
const MAX_PTY_DRAIN_EVENTS: usize = 128;
-const PREFIX_W: u16 = 5; // file-pane line-number prefix width: "%4d "
+const PREFIX_W: u16 = pane_ui.PREFIX_W; // file-pane line-number prefix width: "%4d "
-const Rect = struct { x: u16, y: u16, w: u16, h: u16 };
+const Rect = pane_ui.Rect;
// acme model: columns own X (col_weight), windows own Y (Term.vweight). The
// per-column ordered list `col_terms[c][0..col_n[c]]` is the source of truth.
-const Layout = struct {
- ncol: usize,
- col_weight: [MAX_COLS]f32,
- col_terms: [MAX_COLS][MAX_TERMS]usize,
- col_n: [MAX_COLS]usize,
-};
+const Layout = pane_ui.Layout(MAX_COLS, MAX_TERMS);
+const layoutFindTerm = pane_ui.layoutFindTerm;
+const layoutRemove = pane_ui.layoutRemove;
+const layoutInsert = pane_ui.layoutInsert;
+const layoutAppendColumn = pane_ui.layoutAppendColumn;
+const moveTerm = pane_ui.moveTerm;
const Drag = union(enum) {
none,
@@ -878,49 +879,6 @@ fn startReader(io: std.Io, gpa: std.mem.Allocator, loop: *Loop, t: *Term, id: us
t.reader = try io.concurrent(readPty, .{ io, gpa, t.pty, id, loop });
}
-// ---- layout surgery over the per-column ordered lists ----
-
-fn layoutFindTerm(l: *const Layout, 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;
-}
-
-fn layoutRemove(l: *Layout, 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) {
- // The column just emptied: hand its width to a neighbor (the column to its
- // left, or right if it was the first) so the OTHER columns keep their widths
- // and only that neighbor grows to fill the gap. Horizontal mirror of
- // absorbVWeight; without it, wsum drops and every remaining column reflows
- // sideways — an extra movement on delete.
- 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;
- }
-}
-
-fn layoutInsert(l: *Layout, 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;
-}
-
// Before deleting `id`, hand its vertical weight to a sibling (the pane above, or
// below if it's the first) so the freed space goes to ONE neighbor and the rest of
// the column keeps its sizes — the deletion mirror of splitBelow. Call BEFORE
@@ -934,15 +892,6 @@ fn absorbVWeight(l: *const Layout, terms: *const [MAX_TERMS]?*Term, id: usize) v
if (terms[sib]) |s| s.vweight += gone.vweight;
}
-fn layoutAppendColumn(l: *Layout, id: usize) void {
- if (l.ncol >= MAX_COLS) return;
- const c = l.ncol;
- l.col_weight[c] = 1;
- l.col_terms[c][0] = id;
- l.col_n[c] = 1;
- l.ncol += 1;
-}
-
// Open a window BELOW `src` (acme-style) without rebalancing the whole column:
// shrink ONLY `src` to the height of its content so far — and hand the freed
// rows to the new window `nw`. `src`+`nw` together fill `src`'s old slot, so the
@@ -1032,63 +981,8 @@ fn ttyForDir(
return nt;
}
-// Overlay a resize-handle dash on a cell WITHOUT touching its colors: read the cell,
-// swap only the grapheme, write it back. So the boundary hint reads as a dashed line
-// over whatever content is already there — the only UI change is the character.
-fn overlayDash(win: vaxis.Window, col: u16, row: u16, glyph: []const u8) void {
- var cell = win.readCell(col, row) orelse return;
- cell.char.grapheme = glyph; // static literal — safe under vaxis's retained render
- cell.default = false; // make it draw with the kept style, not as a blank cell
- win.writeCell(col, row, cell);
-}
-
-// Tile the screen: columns by col_weight, then each column's windows by vweight.
-// Last column/window absorbs rounding so the tiling always fills the screen.
-// `out_*` written for every live term and live column.
-fn computeGeom(
- l: *const Layout,
- terms: *const [MAX_TERMS]?*Term,
- sw: u16,
- sh: u16,
- rects: *[MAX_TERMS]Rect,
- col_x: *[MAX_COLS]u16,
- col_w: *[MAX_COLS]u16,
-) 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;
-
- // Panes tile edge to edge — NO reserved inter-pane gap. The cells that used to be
- // an empty drag strip are now usable pane content; the resize handle is the shared
- // boundary itself (a pane's right/bottom edge), surfaced as a dashed line on hover.
- // Only the top row is held back, for the global tagbar.
- 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;
-
- const n = l.col_n[c];
- var vsum: f32 = 0;
- for (0..n) |k| {
- if (terms[l.col_terms[c][k]]) |t| vsum += t.vweight;
- }
- if (vsum <= 0) vsum = 1;
- const avail_h: u16 = sh -| TOPBAR_H;
-
- var y: u16 = TOPBAR_H;
- for (0..n) |k| {
- const id = l.col_terms[c][k];
- const t = terms[id] orelse continue;
- const lastk = (k + 1 == n);
- const ch: u16 = if (lastk) (sh -| y) else @max(1, @as(u16, @intFromFloat(@round(@as(f32, @floatFromInt(avail_h)) * t.vweight / vsum))));
- rects[id] = .{ .x = x, .y = y, .w = cw, .h = ch };
- y +|= ch;
- }
- x +|= cw;
- }
+fn termVWeight(terms: *const [MAX_TERMS]?*Term, id: usize) ?f32 {
+ return if (terms[id]) |t| t.vweight else null;
}
// `alloc` must be a per-frame arena, NOT freed before vx.render(): vaxis is a
@@ -1137,15 +1031,7 @@ fn renderPane(root: vaxis.Window, alloc: std.mem.Allocator, t: *Term, r: Rect, a
// frame, so this runs every render. Nothing else (text/cursor/selection) applies.
if (t.image) |*iv| {
if (r.h > BOX_H) image.draw(iv, text.child(.{ .y_off = BOX_H, .height = r.h - BOX_H }), r.w - GUTTER, r.h - BOX_H);
- const gut = root.child(.{ .x_off = r.x, .y_off = r.y, .width = GUTTER, .height = r.h });
- const box_bg = if (active) accent else accent_dim;
- var gy: u16 = 0;
- while (gy < r.h) : (gy += 1) {
- const bg = if (gy < BOX_H) box_bg else T().scroll_track;
- var gx: u16 = 0;
- while (gx < GUTTER) : (gx += 1)
- gut.writeCell(gx, gy, .{ .char = .{ .grapheme = " " }, .style = .{ .bg = .{ .rgb = bg } } });
- }
+ pane_ui.drawGutter(root, r, active, accent, accent_dim, T().scroll_track, T().scroll_thumb, null);
return;
}
@@ -1353,43 +1239,15 @@ fn renderPane(root: vaxis.Window, alloc: std.mem.Allocator, t: *Term, r: Rect, a
}
}
- // gutter: box (the drag/rearrange handle) on top, scrollbar below
- const gut = root.child(.{ .x_off = r.x, .y_off = r.y, .width = GUTTER, .height = r.h });
- const box_bg = if (active) accent else accent_dim;
- var bx: u16 = 0;
- while (bx < GUTTER) : (bx += 1) {
- var by: u16 = 0;
- while (by < BOX_H) : (by += 1)
- gut.writeCell(bx, by, .{ .char = .{ .grapheme = " " }, .style = .{ .bg = .{ .rgb = box_bg } } });
- }
- const track_h: usize = if (r.h > BOX_H) r.h - BOX_H else 0;
- if (track_h > 0) {
- // scrollbar thumb: from file lines for a file pane, else the emulator's.
- var sb_total: usize = undefined;
- var sb_off: usize = undefined;
- var sb_len: usize = undefined;
- if (t.file) |f| {
- sb_total = fileLineCount(f.content);
- sb_off = f.scroll;
- sb_len = t.rows;
- } else {
- const sb = t.term.screens.active.pages.scrollbar();
- sb_total = sb.total;
- sb_off = sb.offset;
- sb_len = sb.len;
- }
- const total: usize = if (sb_total == 0) 1 else sb_total;
- const pos = (track_h * sb_off) / total;
- const len = @max(1, (track_h * sb_len) / total);
- var gy: usize = 0;
- while (gy < track_h) : (gy += 1) {
- const thumb = gy >= pos and gy < pos + len;
- const st_bg = if (thumb) T().scroll_thumb else T().scroll_track;
- var cx: u16 = 0;
- while (cx < GUTTER) : (cx += 1)
- gut.writeCell(cx, @intCast(BOX_H + gy), .{ .char = .{ .grapheme = " " }, .style = .{ .bg = .{ .rgb = st_bg } } });
- }
- }
+ const scrollbar: pane_ui.Scrollbar = if (t.file) |f| .{
+ .total = fileLineCount(f.content),
+ .offset = f.scroll,
+ .len = t.rows,
+ } else blk: {
+ const sb = t.term.screens.active.pages.scrollbar();
+ break :blk .{ .total = sb.total, .offset = sb.offset, .len = sb.len };
+ };
+ pane_ui.drawGutter(root, r, active, accent, accent_dim, T().scroll_track, T().scroll_thumb, scrollbar);
}
// ---- modal editing helpers (wired onto Term by the key handler) ----
@@ -2750,7 +2608,7 @@ pub fn main(init: std.process.Init) !void {
var rects: [MAX_TERMS]Rect = undefined;
var col_x: [MAX_COLS]u16 = undefined;
var col_w: [MAX_COLS]u16 = undefined;
- computeGeom(&layout, &terms, screen_w, screen_h, &rects, &col_x, &col_w);
+ pane_ui.computeGeom(&layout, &terms, screen_w, screen_h, &rects, &col_x, &col_w, termVWeight);
var drag: Drag = .none;
var hover_col: u16 = 0; // last mouse position; used to tint the resize border under it
@@ -2863,14 +2721,12 @@ pub fn main(init: std.process.Init) !void {
if (!handled) {
if (hovered) |id| {
const r = rects[id];
- const in_gutter = mcol < r.x + GUTTER;
- if (in_gutter and mrow < r.y + BOX_H) {
+ if (pane_ui.inMoveBox(r, mcol, mrow)) {
drag = .{ .move = .{ .id = id, .cur_x = mcol, .cur_y = mrow } };
- } else if (in_gutter) {
+ } else if (pane_ui.scrollbarClickDelta(r, .left, mcol, mrow)) |delta| {
active = id;
if (terms[id]) |t| {
- const local: i32 = @as(i32, mrow) - @as(i32, r.y + BOX_H);
- paneScrollBy(t, -local); // left: scroll up to here
+ paneScrollBy(t, delta); // left: scroll up to here
}
} else if (mrow < r.y + BOX_H and terms[id] != null and terms[id].?.image == null) {
// left-click on the tag row (non-image): edit the tail.
@@ -2895,12 +2751,12 @@ pub fn main(init: std.process.Init) !void {
const b: Button = if (mouse.button == .middle) .middle else .right;
if (hovered) |id| {
const r = rects[id];
- if (mcol < r.x + GUTTER and mrow >= r.y + BOX_H) {
+ const sb_button: pane_ui.ScrollbarButton = if (b == .right) .right else .middle;
+ if (pane_ui.scrollbarClickDelta(r, sb_button, mcol, mrow)) |delta| {
// gutter (scrollbar): right scrolls down, mirroring left's up
if (b == .right) active = id;
if (terms[id]) |t| {
- const local: i32 = @as(i32, mrow) - @as(i32, r.y + BOX_H);
- paneScrollBy(t, if (b == .right) local else -local);
+ paneScrollBy(t, delta);
}
} else if (mcol >= r.x + GUTTER) {
if (b == .right) active = id;
@@ -3236,7 +3092,7 @@ pub fn main(init: std.process.Init) !void {
// recompute geometry from the (possibly mutated) layout, then push any
// grid-size change to each emulator + pty. Weights only change on a drag
// *release*, so mid-drag the rects are unchanged and nothing resizes.
- computeGeom(&layout, &terms, screen_w, screen_h, &rects, &col_x, &col_w);
+ pane_ui.computeGeom(&layout, &terms, screen_w, screen_h, &rects, &col_x, &col_w, termVWeight);
for (&terms, 0..) |*slot, id| {
if (slot.*) |t| {
const r = rects[id];
@@ -3301,49 +3157,19 @@ pub fn main(init: std.process.Init) !void {
// else branch, on hover.
switch (drag) {
.border_v => |d| {
- var ry: u16 = TOPBAR_H;
- while (ry < screen_h) : (ry += 1) overlayDash(win, d.cur_x, ry, "╎");
+ pane_ui.drawBorderV(win, d.cur_x, screen_h);
},
.border_h => |d| {
- const c = d.col;
- var rx: u16 = col_x[c];
- while (rx < col_x[c] + col_w[c]) : (rx += 1) overlayDash(win, rx, d.cur_y, "╌");
+ pane_ui.drawBorderH(win, col_x[d.col], col_w[d.col], d.cur_y);
},
.move => |d| {
- // insertion indicator: dashed line at cur_y across the target column
- var tc: usize = if (layout.ncol > 0) layout.ncol - 1 else 0;
- for (0..layout.ncol) |c| {
- if (d.cur_x >= col_x[c] and d.cur_x < col_x[c] + col_w[c]) {
- tc = c;
- break;
- }
- }
- var rx: u16 = col_x[tc];
- while (rx < col_x[tc] + col_w[tc]) : (rx += 1)
- win.writeCell(rx, d.cur_y, .{ .char = .{ .grapheme = "╌" }, .style = .{ .fg = .{ .rgb = gray.mid } } });
- win.writeCell(d.cur_x, d.cur_y, .{ .char = .{ .grapheme = "▌" }, .style = .{ .fg = .{ .rgb = gray.mid } } });
+ pane_ui.drawMovePreview(win, &layout, &col_x, &col_w, d.cur_x, d.cur_y, gray.mid);
},
else => {
// not dragging: hint the resize handle under the cursor with the same
// dash overlay. Vertical = the left pane's last column; horizontal =
// the upper pane's last row (a's own last line, just above b's tag).
- var c: usize = 0;
- while (c + 1 < layout.ncol) : (c += 1) {
- if (hover_col != col_x[c] + col_w[c] -| 1) continue;
- var ry: u16 = TOPBAR_H;
- while (ry < screen_h) : (ry += 1) overlayDash(win, hover_col, ry, "╎");
- }
- var cc: usize = 0;
- while (cc < layout.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 < layout.col_n[cc]) : (k += 1) {
- const r = rects[layout.col_terms[cc][k]];
- if (hover_row != r.y + r.h -| 1) continue;
- var rx: u16 = col_x[cc];
- while (rx < col_x[cc] + col_w[cc]) : (rx += 1) overlayDash(win, rx, hover_row, "╌");
- }
- }
+ pane_ui.drawHoverResizeHints(win, &layout, &rects, &col_x, &col_w, screen_h, hover_col, hover_row);
},
}
@@ -3383,48 +3209,6 @@ pub fn main(init: std.process.Init) !void {
}
}
-// On box-drag release: drop the window into the column under cur_x at the
-// vertical slot picked by cur_y (acme's coldragwin, minus the live retile).
-fn moveTerm(
- l: *Layout,
- rects: *const [MAX_TERMS]Rect,
- col_x: *const [MAX_COLS]u16,
- col_w: *const [MAX_COLS]u16,
- id: usize,
- cur_x: u16,
- cur_y: u16,
-) void {
- const src = layoutFindTerm(l, id) orelse return;
- // target column under cur_x (last column absorbs anything past the right edge)
- 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;
- }
- }
-
- // insertion index in the target column, ignoring the moving window itself
- 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 rr = rects[cid];
- if (cur_y < rr.y + rr.h / 2) break;
- ti += 1;
- }
-
- const src_solo = l.col_n[src.col] == 1;
- if (tc == src.col and src_solo) return; // dragging the only window of its column: nothing to do
-
- layoutRemove(l, id);
- if (src_solo and src.col < tc) tc -= 1; // removing src dropped a column to our left
- if (tc >= l.ncol) tc = l.ncol - 1;
- if (ti > l.col_n[tc]) ti = l.col_n[tc];
- layoutInsert(l, tc, ti, id);
-}
-
// index of the run anchored exactly at (row, col), if any.
fn runAt(t: *Term, row: i32, col: i32) ?usize {
for (t.edits[0..t.nedits], 0..) |e, i| if (e.row == row and e.col == col) return i;