From bc6abf1716a4824513c325204302f2fd229c2974 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Fri, 31 Jul 2026 23:21:17 -0300 Subject: a corner grab drags the column edge and the pane edge at once --- src/config.zig | 2 +- src/pardes.zig | 188 +++++++++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 158 insertions(+), 32 deletions(-) (limited to 'src') diff --git a/src/config.zig b/src/config.zig index 46bd261e..714ec177 100644 --- a/src/config.zig +++ b/src/config.zig @@ -310,7 +310,7 @@ pub const file_pane_builtins_str = "Save Del"; /// here would be pushed into one cell as a single grapheme and come out wrong. /// A font missing the glyph draws a blank box, which is exactly what the box /// drew before there was anything in it. -pub const tag_normal = "•"; +pub const tag_normal = " "; pub const tag_insert = "^"; pub const tag_tty = "$"; /// ...and `img` stays a WORD at the head of an image pane's tagline, because diff --git a/src/pardes.zig b/src/pardes.zig index b303acbb..6245d1f4 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -1012,7 +1012,25 @@ pub const Pane = struct { const Drag = union(enum) { none, - border_v: struct { left_col: usize, cur_x: u16 }, + /// `corner_idx` is what makes this a CORNER grab: the press landed on a + /// cell that is both this v-border and one of left_col's own h-borders, + /// and then the one drag moves both boundaries — cur_x the column pair, + /// cur_y left_col's pane pair at index corner_idx. null is a plain edge + /// drag and cur_y is only carried along for the preview. + /// + /// It is the LEFT column's h-splits a corner honours, and only those. The + /// v handle IS left_col's last cell, so left_col's horizontal hint is + /// drawn straight THROUGH it while the right column's spans start one cell + /// further right — the corners this finds are exactly the cells where the + /// user can see the two lines cross. Honouring the right column too would + /// scatter corners across rows with no visible crossing. + /// + /// ponytail: so a corner moves exactly TWO boundaries, never three — a + /// right-column h-split that happens to sit at the same row is left alone, + /// even though the eye may read the whole row as one line. Add a second + /// optional index (the right column's k) here and a second applyRowSplit + /// call if that ever reads as a bug rather than as restraint. + border_v: struct { left_col: usize, cur_x: u16, corner_idx: ?usize = null, cur_y: u16 = 0 }, border_h: struct { col: usize, top_idx: usize, cur_y: u16 }, move: struct { id: usize, cur_x: u16, cur_y: u16 }, /// a left sweep along a pane's TAG row: it drives the tag's own cursor and @@ -1024,6 +1042,73 @@ const Drag = union(enum) { select: struct { id: usize, button: Mouse.Button, chorded: bool = false, ctrl: bool = false }, }; +// The two border clamps, pulled out as plain arithmetic on plain numbers for +// one reason: a CORNER drag runs both of them off the same mouse position, and +// the thing that has to hold is that each one only ever looks at its own axis. +// A clamp that consulted the other axis — or a single "is this point legal" +// test over the pair — would freeze the whole gesture the moment either edge +// hit its stop, when what the hand expects is the free axis to keep tracking +// and the pinned one to sit at the wall. Being pure, they are also the piece +// worth a test; see below. + +/// Where a vertical border drag settles: `mcol` clamped so neither column of +/// the pair falls under MINW. `lx`/`lw` are the left column's x and width, +/// `rw` the right column's. Degenerate pairs (a window too narrow to hold two +/// minimums at all) pass the mouse through rather than snapping to a lie. +fn clampBorderCol(lx: u16, lw: u16, rw: u16, mcol: u16) u16 { + const lo = lx + config.MINW; + const hi = lx +| lw +| rw -| config.MINW; + return if (lo <= hi) std.math.clamp(mcol, lo, hi) else mcol; +} + +/// Where a horizontal border drag settles: `mrow` clamped so either pane may +/// shrink to just its tag row (BOX_H) but no further. `ay`/`ah` are the upper +/// pane's y and height, `bh` the lower pane's. +fn clampBorderRow(ay: u16, ah: u16, bh: u16, mrow: u16) u16 { + const lo = ay + BOX_H - 1; + const hi = ay +| ah +| bh -| (BOX_H + 1); + return if (lo <= hi) std.math.clamp(mrow, lo, hi) else mrow; +} + +test "a corner drag's two axes clamp independently" { + // a 100-wide window split 50/50, and a 30-row column split 15/14 under the + // topbar: the shape windowops.snap drives + const lx: u16 = 0; + const lw: u16 = 50; + const rw: u16 = 50; + const ay: u16 = TOPBAR_H; + const ah: u16 = 15; + const bh: u16 = 14; + // the walls, spelled out: the handle is the upper pane's LAST row, so the + // upper pane bottoms out with its tag row alone at ay, and the lower pane + // does the same one row above the pair's end + const row_lo: u16 = ay + BOX_H - 1; + const row_hi: u16 = ay + ah + bh - (BOX_H + 1); + + // each axis alone, at both walls + try std.testing.expectEqual(@as(u16, config.MINW), clampBorderCol(lx, lw, rw, 0)); + try std.testing.expectEqual(@as(u16, 100 - config.MINW), clampBorderCol(lx, lw, rw, 999)); + try std.testing.expectEqual(row_lo, clampBorderRow(ay, ah, bh, 0)); + try std.testing.expectEqual(row_hi, clampBorderRow(ay, ah, bh, 999)); + + // THE corner property: drag off the right edge while staying mid-height — + // x sits at its wall, y still tracks the mouse row exactly + try std.testing.expectEqual(@as(u16, 100 - config.MINW), clampBorderCol(lx, lw, rw, 999)); + try std.testing.expectEqual(@as(u16, 12), clampBorderRow(ay, ah, bh, 12)); + // and the mirror: pinned at the bottom, x still free + try std.testing.expectEqual(row_hi, clampBorderRow(ay, ah, bh, 999)); + try std.testing.expectEqual(@as(u16, 40), clampBorderCol(lx, lw, rw, 40)); + // both pinned at once is just both walls, not a stuck gesture + try std.testing.expectEqual(@as(u16, config.MINW), clampBorderCol(lx, lw, rw, 0)); + try std.testing.expectEqual(row_hi, clampBorderRow(ay, ah, bh, 999)); + + // degenerate pairs pass through untouched, each on its own terms: a pane + // pair with no room stays draggable even when the column pair has room + try std.testing.expectEqual(@as(u16, 7), clampBorderCol(0, 5, 5, 7)); + try std.testing.expectEqual(@as(u16, 40), clampBorderCol(lx, lw, rw, 40)); + try std.testing.expectEqual(@as(u16, 9), clampBorderRow(TOPBAR_H, 1, 0, 9)); +} + pub const Rect = struct { x: u16, y: u16, w: u16, h: u16 }; pub const Options = struct { @@ -5412,10 +5497,23 @@ pub const Pardes = struct { } } // resize handles next: a pane's own trailing edge (v: the - // left column's last col; h: the upper pane's last row) + // left column's last col; h: the upper pane's last row). + // The v test still wins outright, but it now also asks + // whether this same cell is one of ITS OWN column's h + // handles — that cell is the corner where the two lines + // cross, and grabbing it drags both boundaries at once + // (see Drag.border_v for why only the left column counts). for (0..p.ncol -| 1) |c| { if (mcol == p.col_x[c] + p.col_w[c] -| 1) { - p.drag = .{ .border_v = .{ .left_col = c, .cur_x = mcol } }; + var corner: ?usize = null; + for (0..p.col_n[c] -| 1) |k| { + const r = p.rects[p.col_terms[c][k]]; + if (mrow == r.y + r.h -| 1) { + corner = k; + break; + } + } + p.drag = .{ .border_v = .{ .left_col = c, .cur_x = mcol, .corner_idx = corner, .cur_y = mrow } }; return; } } @@ -5520,11 +5618,22 @@ pub const Pardes = struct { switch (p.drag) { .border_v => |*d| { const c = d.left_col; - if (c + 1 < p.ncol) { - const lo = p.col_x[c] + config.MINW; - const hi = p.col_x[c] +| p.col_w[c] +| p.col_w[c + 1] -| config.MINW; - d.cur_x = if (lo <= hi) std.math.clamp(mcol, lo, hi) else mcol; - } else d.cur_x = mcol; + d.cur_x = if (c + 1 < p.ncol) + clampBorderCol(p.col_x[c], p.col_w[c], p.col_w[c + 1], mcol) + else + mcol; + // a corner also drives left_col's pane pair, off the SAME mouse + // position but through its own clamp — the geometry a clamp + // reads (widths for x, heights for y) is frozen for the whole + // drag and never crosses axes, so one edge parked at its stop + // leaves the other tracking the mouse + if (d.corner_idx) |k| if (k + 1 < p.col_n[c]) { + const a = p.rects[p.col_terms[c][k]]; + const b = p.rects[p.col_terms[c][k + 1]]; + d.cur_y = clampBorderRow(a.y, a.h, b.h, mrow); + } else { + d.cur_y = mrow; + }; }, .border_h => |*d| { const cc = d.col; @@ -5532,10 +5641,7 @@ pub const Pardes = struct { if (k + 1 < p.col_n[cc]) { const a = p.rects[p.col_terms[cc][k]]; const b = p.rects[p.col_terms[cc][k + 1]]; - // either pane may shrink to just its tag row (BOX_H) - const lo = a.y + BOX_H - 1; - const hi = a.y +| a.h +| b.h -| (BOX_H + 1); - d.cur_y = if (lo <= hi) std.math.clamp(mrow, lo, hi) else mrow; + d.cur_y = clampBorderRow(a.y, a.h, b.h, mrow); } else d.cur_y = mrow; }, .move => |*d| { @@ -5582,23 +5688,13 @@ pub const Pardes = struct { p.col_weight[c] = pair * (nl / combined); p.col_weight[c + 1] = pair - p.col_weight[c]; } + // and the corner's other half. The two commits are independent + // — column weights are widths, pane vweights are heights, and + // neither reads the other — so the order here does not matter + // and a failed one cannot lose the other. + if (d.corner_idx) |k| p.applyRowSplit(c, k, d.cur_y); }, - .border_h => |d| { - const cc = d.col; - const k = d.top_idx; - if (k + 1 < p.col_n[cc]) { - const a = p.panes[p.col_terms[cc][k]] orelse return; - const b = p.panes[p.col_terms[cc][k + 1]] orelse return; - const ra = p.rects[p.col_terms[cc][k]]; - const rb = p.rects[p.col_terms[cc][k + 1]]; - const combined: f32 = @floatFromInt(ra.h + rb.h); - var nt: f32 = @floatFromInt((d.cur_y + 1) -| ra.y); - nt = std.math.clamp(nt, @as(f32, BOX_H), @max(@as(f32, BOX_H), combined - BOX_H)); - const pair = a.vweight + b.vweight; - a.vweight = pair * (nt / combined); - b.vweight = pair - a.vweight; - } - }, + .border_h => |d| p.applyRowSplit(d.col, d.top_idx, d.cur_y), .move => |d| { p.moveTerm(d.id, d.cur_x, d.cur_y); // a file moved into the left column evicts a lone unused shell @@ -5662,6 +5758,25 @@ pub const Pardes = struct { } } + /// Commit a horizontal border to the pane weights: in column `cc`, the pane + /// at index `k` and the one under it split their combined height at row + /// `cur_y`. Shared by the plain h-drag and the h half of a corner drag — + /// the same gesture landing on the same boundary must settle identically + /// whether or not a column edge came along for the ride. + fn applyRowSplit(p: *Pardes, cc: usize, k: usize, cur_y: u16) void { + if (k + 1 >= p.col_n[cc]) return; + const a = p.panes[p.col_terms[cc][k]] orelse return; + const b = p.panes[p.col_terms[cc][k + 1]] orelse return; + const ra = p.rects[p.col_terms[cc][k]]; + const rb = p.rects[p.col_terms[cc][k + 1]]; + const combined: f32 = @floatFromInt(ra.h + rb.h); + var nt: f32 = @floatFromInt((cur_y + 1) -| ra.y); + nt = std.math.clamp(nt, @as(f32, BOX_H), @max(@as(f32, BOX_H), combined - BOX_H)); + const pair = a.vweight + b.vweight; + a.vweight = pair * (nt / combined); + b.vweight = pair - a.vweight; + } + /// acme 1-2 (Cut) / 1-3 (Paste): middle or right tapped while the left /// button holds a selection drag. The first tap converts the mouse /// selection into the modal one (vsel anchored at the drag start, cursor @@ -6905,9 +7020,20 @@ pub const Pardes = struct { // mid-drag (tiling WM, font-size change) can leave them off the new // surface — every arm below checks before it draws. switch (p.drag) { - .border_v => |d| if (d.cur_x < s.cols) { - var row: u16 = TOPBAR_H; - while (row < s.rows) : (row += 1) s.overlayDash(d.cur_x, row, "╎"); + .border_v => |d| { + if (d.cur_x < s.cols) { + var row: u16 = TOPBAR_H; + while (row < s.rows) : (row += 1) s.overlayDash(d.cur_x, row, "╎"); + } + // a corner lights BOTH splits, which is the whole tell that you + // grabbed the crossing and not an edge. The horizontal half + // stops at the vertical preview rather than at the column's + // current right edge, so the two dashes stay joined at the cell + // under the mouse and together show the shape being committed. + if (d.corner_idx != null and d.cur_y < s.rows) { + var col = p.col_x[d.left_col]; + while (col <= d.cur_x and col < s.cols) : (col += 1) s.overlayDash(col, d.cur_y, "╌"); + } }, .border_h => |d| if (d.cur_y < s.rows) { var col = p.col_x[d.col]; -- cgit v1.3