summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-07-31 23:21:17 -0300
committerGabriel Schneider <[email protected]>2026-08-01 15:02:08 -0300
commitbc6abf1716a4824513c325204302f2fd229c2974 (patch)
tree12d55d882ce32c86f67163894203913fe69e19c5 /src/pardes.zig
parentf4ac5ee26851ced516b826953fa340008fa7069c (diff)
downloadpardes-bc6abf1716a4824513c325204302f2fd229c2974.tar.gz
pardes-bc6abf1716a4824513c325204302f2fd229c2974.zip
a corner grab drags the column edge and the pane edge at once
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig188
1 files changed, 157 insertions, 31 deletions
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];