diff options
| author | Gabriel Schneider <[email protected]> | 2026-07-03 14:43:30 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-01 15:02:07 -0300 |
| commit | d1ed5bd8f55935e6470c4a7559216119571a084f (patch) | |
| tree | 434478ce8a5dcd9aed367334816f54da9953e56d /main.zig | |
| parent | 153f0a88862507cf1f5dda71f71db3d6649dfeaa (diff) | |
| download | pardes-d1ed5bd8f55935e6470c4a7559216119571a084f.tar.gz pardes-d1ed5bd8f55935e6470c4a7559216119571a084f.zip | |
fixed scaling and fitting on web ui
Diffstat (limited to 'main.zig')
| -rw-r--r-- | main.zig | 292 |
1 files changed, 38 insertions, 254 deletions
@@ -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; |
