summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-24 09:45:50 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:14 -0300
commita39fb20cb770c3adb911cec7d1f3d033669ed1bb (patch)
treee7bb88af15990e502b5f4deac5493ea2957e8b24 /src
parent39cdba439c1ae47dbc7c3243a9273aa011c6affa (diff)
downloadpardes-a39fb20cb770c3adb911cec7d1f3d033669ed1bb.tar.gz
pardes-a39fb20cb770c3adb911cec7d1f3d033669ed1bb.zip
Light a held column grip, and rail its landing beside the rule where the GUI shows it
The column grip worked in the GUI but showed nothing anyone could see. Driving the real SDL window with pixel mouse events, press, drag and release all reach the core and reorder or resize the column; the feedback is what failed. The grip kept its muted color while held, and a reorder's insertion rail was drawn in the gutter cell at the destination edge. The GUI paints that cell as the column's scroll rail, and at the window's left edge, the common case of moving the right column first, the dashes were lost in it. From the leftmost column, a drag short of a neighbour's middle shows no rail, rightly, since nothing would move, but nothing said the grip was even held. A held grip now lights in column_box, the accent its muted color is mixed from, and the rail takes that color. The rail runs in the seam cell left of where the column's rule will land, where a border drag's rail runs, for a reorder and a left-edge move alike; at the window's left edge, which has no seam, it runs just past the scroll rail, leaving column 0's grip and pane boxes whole. A carried column's pointer aims at a place, not a word, so the column tags no longer light the word under it, and a release snaps the layout as a border drag's does rather than animating it. The preview and the release both ask layout.columnDrop where the column goes, so the rail stands where the release puts the edge, and the drag no longer carries its unused column index. The GUI test feed takes ESC]777;mouse;<down|up|motion>;<button>;<x>;<y> in window pixels and dispatches it as an SDL event. test/column_grip.py uses it to drive the grip through the same pixel-to-cell and tag hit path as a hand on the mouse, and checks the grip, the rail and the result against GPU captures and the 9P grid. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/gui/gui.zig41
-rw-r--r--src/layout.zig18
-rw-r--r--src/pardes.zig86
3 files changed, 100 insertions, 45 deletions
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index a4cc7672..989f6b25 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -2876,6 +2876,43 @@ const StdinFeed = struct {
var seq_start: usize = 0;
while (seq_start < len) {
+ // ESC]777;mouse;<down|up|motion>;<SDL button, 0 for none>;<x>;<y>BEL
+ // A mouse event at window pixels, built as SDL would and run
+ // through dispatch, so a driver exercises the real pointer path
+ // (pixel to cell, tag and body hits). SGR mouse carries only cells.
+ const mouse_prefix = "\x1b]777;mouse;";
+ if (std.mem.startsWith(u8, f.buf[seq_start..len], mouse_prefix)) {
+ const body = f.buf[seq_start + mouse_prefix.len .. len];
+ const end = std.mem.indexOfScalar(u8, body, 0x07) orelse {
+ std.mem.copyForwards(u8, f.buf[0 .. len - seq_start], f.buf[seq_start..len]);
+ f.fill = len - seq_start;
+ break;
+ };
+ if (g) |gp| {
+ var parts = std.mem.splitScalar(u8, body[0..end], ';');
+ const kind = parts.next() orelse "";
+ const button = std.fmt.parseInt(u8, parts.next() orelse "", 10) catch 0;
+ const x = std.fmt.parseFloat(f32, parts.next() orelse "") catch 0;
+ const y = std.fmt.parseFloat(f32, parts.next() orelse "") catch 0;
+ var sev = std.mem.zeroes(c.SDL_Event);
+ if (std.mem.eql(u8, kind, "motion")) {
+ sev.type = c.SDL_EVENT_MOUSE_MOTION;
+ sev.motion.x = x;
+ sev.motion.y = y;
+ sev.motion.state = if (button == 0 or button > 5) 0 else @as(u32, 1) << @intCast(button - 1);
+ } else {
+ sev.type = if (std.mem.eql(u8, kind, "down")) c.SDL_EVENT_MOUSE_BUTTON_DOWN else c.SDL_EVENT_MOUSE_BUTTON_UP;
+ sev.button.button = button;
+ sev.button.down = std.mem.eql(u8, kind, "down");
+ sev.button.x = x;
+ sev.button.y = y;
+ }
+ dispatch(gp, in, &sev);
+ out.n_events += 1;
+ }
+ seq_start += mouse_prefix.len + end + 1;
+ continue;
+ }
const prefix = "\x1b]777;finger;";
if (std.mem.startsWith(u8, f.buf[seq_start..len], prefix)) {
const body = f.buf[seq_start + prefix.len .. len];
@@ -2909,7 +2946,9 @@ const StdinFeed = struct {
seq_start += prefix.len + end + 1;
continue;
}
- if (len - seq_start < prefix.len and std.mem.startsWith(u8, prefix, f.buf[seq_start..len])) {
+ if ((len - seq_start < prefix.len and std.mem.startsWith(u8, prefix, f.buf[seq_start..len])) or
+ (len - seq_start < mouse_prefix.len and std.mem.startsWith(u8, mouse_prefix, f.buf[seq_start..len])))
+ {
std.mem.copyForwards(u8, f.buf[0 .. len - seq_start], f.buf[seq_start..len]);
f.fill = len - seq_start;
break;
diff --git a/src/layout.zig b/src/layout.zig
index 8db05a07..3637f30e 100644
--- a/src/layout.zig
+++ b/src/layout.zig
@@ -713,14 +713,26 @@ pub fn columnBySerial(p: *const Pardes, serial: u32) ?usize {
return null;
}
-pub fn columnMoveTarget(p: *const Pardes, source: usize, x: u16) usize {
- if (source >= p.ncol) return source;
+/// What releasing column `source`'s grip at `x` does: past a neighbour's
+/// middle it takes that column's place; short of it, as in acme, the
+/// column's left edge moves to `x - grab`, clamped as setColumnPairWidth
+/// clamps it. Still on the grip, or at the leftmost column, nothing moves.
+/// The drag preview and the release both ask here, so they cannot disagree.
+pub const ColumnDrop = union(enum) { stay, reorder: usize, edge: u16 };
+
+pub fn columnDrop(p: *const Pardes, source: usize, x: u16, grab: u16) ColumnDrop {
+ if (source >= p.ncol) return .stay;
var insertion: usize = 0;
for (0..p.ncol) |column| {
if (column == source) continue;
if (x >= p.col_x[column] + p.col_w[column] / 2) insertion += 1;
}
- return insertion;
+ if (insertion != source) return .{ .reorder = insertion };
+ if (source == 0 or (x >= p.col_x[source] and x < p.col_x[source] + config.GUTTER)) return .stay;
+ if (p.col_w[source - 1] + p.col_w[source] < 2 * config.MINW) return .stay;
+ const lo = p.col_x[source - 1] + config.MINW;
+ const hi = (p.col_x[source] + p.col_w[source]) -| config.MINW;
+ return .{ .edge = std.math.clamp(x -| grab, lo, @max(lo, hi)) };
}
/// Share out the width of columns `left` and `left + 1` so the left one is
diff --git a/src/pardes.zig b/src/pardes.zig
index f968511a..4786bd70 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -5155,7 +5155,7 @@ const Drag = union(enum) {
move: struct { id: usize, cur_x: u16, cur_y: u16 },
/// `grab`: how far into the grip it was pressed, so the column's left
/// edge lands at `cur_x - grab`. A release still on the grip moves nothing.
- column_move: struct { column: usize, serial: u32, cur_x: u16, grab: u16 = 0 },
+ column_move: struct { serial: u32, cur_x: u16, grab: u16 = 0 },
tag: struct { id: usize },
/// chorded: a 1-2/1-3 cut/paste chord fired during this left drag —
/// the drag's own release is then inert
@@ -7793,7 +7793,7 @@ pub const Pardes = struct {
.key => |key| p.handleKey(key),
.mouse => |m| {
if (m.kind == .release) switch (p.drag) {
- .border_v, .border_h, .move => p.presentation.snap_once = true,
+ .border_v, .border_h, .move, .column_move => p.presentation.snap_once = true,
else => {},
};
p.handleMouse(m);
@@ -12017,7 +12017,7 @@ pub const Pardes = struct {
if (mcol < p.col_x[column] or mcol >= p.col_x[column] + @min(config.GUTTER, p.col_w[column])) continue;
if (m.button == config.select_button) {
p.header_drag = false;
- p.drag = .{ .column_move = .{ .column = column, .serial = p.col_serial[column], .cur_x = mcol, .grab = mcol - p.col_x[column] } };
+ p.drag = .{ .column_move = .{ .serial = p.col_serial[column], .cur_x = mcol, .grab = mcol - p.col_x[column] } };
}
return;
}
@@ -12488,15 +12488,10 @@ pub const Pardes = struct {
},
.border_h => |d| if (d.cur_y != seamRowOf(p, d.col, d.top_idx))
layout.applyRowSplit(p, d.col, d.top_idx, d.cur_y),
- .column_move => |d| if (layout.columnBySerial(p, d.serial)) |column| {
- const target = layout.columnMoveTarget(p, column, d.cur_x);
- if (target != column) {
- layout.reorderColumn(p, column, target);
- } else if (column > 0 and (d.cur_x < p.col_x[column] or d.cur_x >= p.col_x[column] + config.GUTTER)) {
- // Dropped short of another column's place: as in acme,
- // the column's left edge moves to where it was dropped.
- layout.setColumnPairWidth(p, column - 1, (d.cur_x -| d.grab) -| p.col_x[column - 1]);
- }
+ .column_move => |d| if (layout.columnBySerial(p, d.serial)) |column| switch (layout.columnDrop(p, column, d.cur_x, d.grab)) {
+ .stay => {},
+ .reorder => |target| layout.reorderColumn(p, column, target),
+ .edge => |x| layout.setColumnPairWidth(p, column - 1, x - p.col_x[column - 1]),
},
.move => |d| {
layout.movePane(p, d.id, d.cur_x, d.cur_y);
@@ -14091,7 +14086,10 @@ pub const Pardes = struct {
const active_col = if (layout.findPane(p, p.active)) |f| f.col == c else false;
const bg = if (active_col and p.settings.focus_tint) chrome.tag_active_bg else chrome.tag_bg;
const fg = if (active_col and p.settings.focus_tint) chrome.tag_active_fg else chrome.tag_fg;
- const grip_bg = chrome.column_box_dim;
+ // A grip is muted until it is grabbed; then it lights, as a pane's
+ // box does for the active pane, so the drag shows what it holds.
+ const grabbed = p.drag == .column_move and layout.columnBySerial(p, p.drag.column_move.serial) == c;
+ const grip_bg = if (grabbed) chrome.column_box else chrome.column_box_dim;
const lum = (@as(u16, grip_bg[0]) * 3 + @as(u16, grip_bg[1]) * 6 + @as(u16, grip_bg[2])) / 10;
const ink: [3]u8 = if (lum > 140) .{ 0, 0, 0 } else .{ 255, 255, 255 };
const grip_style: CellStyle = .{ .bg = .{ .rgb = grip_bg }, .fg = .{ .rgb = ink }, .font_role = .tagline };
@@ -14106,7 +14104,8 @@ pub const Pardes = struct {
.bg = .{ .rgb = bg },
.font_role = .tagline,
});
- if (p.pointer_inside and p.hover_row == p.topBarHeight() and p.hover_col >= x and p.hover_col < x + w) {
+ // A carried column's pointer is aiming at a place, not a word.
+ if (p.pointer_inside and p.drag != .column_move and p.hover_row == p.topBarHeight() and p.hover_col >= x and p.hover_col < x + w) {
const bar = p.columnTag(c);
if (wordBoundsAtCol(bar, panes.File.rawAtDisplay(bar, p.hover_col - x + bar_scroll))) |bounds| {
var col = panes.File.rawDisplayCol(bar, bounds.lo) -| bar_scroll;
@@ -14154,33 +14153,38 @@ pub const Pardes = struct {
var col = p.col_x[d.col];
while (col < p.col_x[d.col] + p.col_w[d.col]) : (col += 1) s.overlayDash(col, d.cur_y, "╌");
},
- .column_move => |d| {
- const source = layout.columnBySerial(p, d.serial) orelse d.column;
- const target = layout.columnMoveTarget(p, source, d.cur_x);
- if (source < p.ncol and target != source) {
- const boundary = if (target < source) p.col_x[target] else p.col_x[target] + p.col_w[target];
- const x = @min(boundary, s.cols -| 1);
- // Leave the outside insertion rail visible above compact
- // text layers. This clips presentation only, never reflows.
- if (boundary == s.cols) {
- for (&s.body_layers) |*layer| if (layer.rows > 0 and layer.viewport.x + layer.viewport.w == s.cols) {
- layer.viewport.w -|= 1;
- };
- for (&s.tag_layers) |*layer| if (layer.cols > 0 and layer.viewport.x + layer.viewport.w == s.cols and layer.viewport.y >= p.topBarHeight()) {
- layer.viewport.w -|= 1;
- };
- }
- var row: u16 = p.topBarHeight();
- while (row < s.rows) : (row += 1) s.overlayDash(x, row, "╎");
- } else if (source > 0 and source < p.ncol and (d.cur_x < p.col_x[source] or d.cur_x >= p.col_x[source] + config.GUTTER) and
- p.col_w[source - 1] + p.col_w[source] >= 2 * config.MINW)
- {
- // Where the left edge would land: the border drag's rail.
- const lo = p.col_x[source - 1] + config.MINW;
- const hi = (p.col_x[source] + p.col_w[source]) -| config.MINW;
- const x = @min(std.math.clamp(d.cur_x -| d.grab, lo, @max(lo, hi)), s.cols -| 1);
- var row: u16 = p.topBarHeight();
- while (row < s.rows) : (row += 1) s.overlayDash(x, row, "╎");
+ .column_move => |d| rail: {
+ const source = layout.columnBySerial(p, d.serial) orelse break :rail;
+ // Where the column's left edge would land, reordered or not.
+ const edge: u16 = switch (layout.columnDrop(p, source, d.cur_x, d.grab)) {
+ .stay => break :rail,
+ .reorder => |target| if (target < source) p.col_x[target] else p.col_x[target] + p.col_w[target],
+ .edge => |x| x,
+ };
+ // Leave the outside insertion rail visible above compact
+ // text layers. This clips presentation only, never reflows.
+ if (edge == s.cols) {
+ for (&s.body_layers) |*layer| if (layer.rows > 0 and layer.viewport.x + layer.viewport.w == s.cols) {
+ layer.viewport.w -|= 1;
+ };
+ for (&s.tag_layers) |*layer| if (layer.cols > 0 and layer.viewport.x + layer.viewport.w == s.cols and layer.viewport.y >= p.topBarHeight()) {
+ layer.viewport.w -|= 1;
+ };
+ }
+ // The rail runs in the seam cell left of the edge, where a
+ // border drag's does: the edge cell itself is a gutter, which
+ // the GUI paints as a scroll rail that swallowed the dashes.
+ // At the window's left edge there is no seam, so it runs just
+ // past that scroll rail instead. It takes the lit grip's
+ // color: this is where the grabbed column lands.
+ const x = @min(if (edge == 0) config.GUTTER - 1 else edge - 1, s.cols -| 1);
+ var row: u16 = p.topBarHeight();
+ while (row < s.rows) : (row += 1) {
+ // Past the left edge's scroll rail the rail would cut
+ // through column 0's grip and pane boxes: leave them whole.
+ if (edge == 0 and s.at(x, row).style.font_role == .tagline) continue;
+ s.overlayDash(x, row, "╎");
+ s.at(x, row).style.fg = .{ .rgb = chrome.column_box };
}
},
.move => |d| if (d.cur_x < s.cols) {