diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-24 09:45:50 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:14 -0300 |
| commit | a39fb20cb770c3adb911cec7d1f3d033669ed1bb (patch) | |
| tree | e7bb88af15990e502b5f4deac5493ea2957e8b24 /src/pardes.zig | |
| parent | 39cdba439c1ae47dbc7c3243a9273aa011c6affa (diff) | |
| download | pardes-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/pardes.zig')
| -rw-r--r-- | src/pardes.zig | 86 |
1 files changed, 45 insertions, 41 deletions
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) { |
