summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 15:43:46 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commitcd6ce6c2b3015c776644fb3866280f5dfd03addc (patch)
treed4e38e5375b71ebec21ee18fe44870e314b2c783
parent0465775799cff895e3e830d2aa19f57966cdf21c (diff)
downloadpardes-cd6ce6c2b3015c776644fb3866280f5dfd03addc.tar.gz
pardes-cd6ce6c2b3015c776644fb3866280f5dfd03addc.zip
A notice's word under the pointer is lit, in its band and the grid; mark_hover is gone
Open point 6 of docs/render-pipeline.md: notice words are Look and Exec targets, so the hover affordance headers have is back on notices, painted once in the notice's layer and joined into the grid from it. Deleted: Surface.mark_hover, Cell.hover and render's per-frame reset of it, none of which had been set since stage 5 (macOS loses its glass hover rect; its build is ignored for now). Snapshot goldens unchanged. Shared files touched: macos.zig (the hover bit's removal). Not touched: pardes.zig, Messages.zig, mouse.zig, gui.zig, detached/*.
-rw-r--r--docs/render-pipeline.md8
-rw-r--r--src/body_layer.zig4
-rw-r--r--src/draw.zig47
-rw-r--r--src/macos.zig18
-rw-r--r--src/surface.zig16
5 files changed, 51 insertions, 42 deletions
diff --git a/docs/render-pipeline.md b/docs/render-pipeline.md
index 6c5e4866..7299b357 100644
--- a/docs/render-pipeline.md
+++ b/docs/render-pipeline.md
@@ -845,9 +845,11 @@ goldens; ssh/tty byte budgets; wire version bump breaks mixed-version attach.
6. Revisit at the first visual stage (from stage 5): the notice layer lost
its hover word to keep the grid's behaviour. Notice words are Look/Exec
targets, so bring the hover affordance back in BOTH the grid and the
- layer then. Carried forward from stage 8 (a GUI-only stage) to the hover
- change after it, which also retires `mark_hover`: the affordance is a
- core paint every shell and snapshot sees.
+ layer then. Done right after stage 8: a notice's word under the pointer
+ is lit as a header's is, in the layer and so in the grid's copy joined
+ from it. The same change deleted `mark_hover`, the `Cell.hover` bit and
+ the per-frame reset of it (never set since stage 5; macOS loses its
+ glass hover rect, accepted with open point 4).
7. Revisit at stage 8 (from stage 6): a body without context rows has no
layer (paint once, straight onto the grid). If the GUI is to read every
body from a layer, give every body one then and measure the copy. The
diff --git a/src/body_layer.zig b/src/body_layer.zig
index 583a2e98..e9407719 100644
--- a/src/body_layer.zig
+++ b/src/body_layer.zig
@@ -294,9 +294,7 @@ pub fn renderBody(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pane
if (p.look_hover_preview) |preview| {
if (preview.pane == id and preview.serial == pane.serial) {
// Preserve body ink, then let every real user selection below
- // win over this quiet affordance. mark_hover carries the
- // cells this paints out to the hosts as the affordance rect —
- // macOS composes its glass overlay over exactly these.
+ // win over this quiet affordance.
const preview_bg = colors.mix(page_rgb, colors.mix(page_rgb, th.sel_bg));
if (preview.file_word) |word|
panes.File.paintWordSelection(p, s, pane, r, word.row, word.lo, word.hi, preview_bg)
diff --git a/src/draw.zig b/src/draw.zig
index 4242a6e7..3798bf59 100644
--- a/src/draw.zig
+++ b/src/draw.zig
@@ -163,10 +163,6 @@ pub fn render(p: *Pardes, arena: std.mem.Allocator) !*Surface {
// the pointer per-text (the macOS I-beam) read this as "target" and
// afford it the arrow instead.
if (p.look_hover_preview != null) s.pointer_shape = .target;
- // Stale hover bits must not survive into a frame whose pointer has
- // moved on: the ordinary content paint does not clear fields it
- // never set.
- for (s.cells) |*c| c.hover = false;
for (&s.body_layers) |*layer| layer.rows = 0;
for (&s.tag_layers) |*layer| layer.rows = 0;
s.nimages = 0;
@@ -661,10 +657,11 @@ fn renderHeaderLayer(p: *Pardes, arena: std.mem.Allocator, layer: *pardes.Layer,
var surface: Surface = .{ .cols = columns, .rows = 1, .cells = layer.cells[index * columns ..][0..columns] };
surface.fill(0, 0, columns, 1, .{ .bg = .{ .rgb = bg }, .font_role = .tagline });
_ = surface.print(0, 0, columns, shown[panes.File.rawAtDisplay(shown, scroll)..], .{ .fg = .{ .rgb = fg }, .bg = .{ .rgb = bg }, .font_role = .tagline });
- // The word under the pointer: on a header even while it is edited,
- // but not on a notice (a message is not a command line), nor on a
- // column a carried column's pointer is aiming at a place over.
- const hovered = p.pointer_inside and kind != .notice and !(kind == .column and p.drag == .column_move);
+ // The word under the pointer, a Look or Exec target: on a header even
+ // while it is edited, on a notice, and on the grid's copy of either
+ // (joined from this); not on a column a carried column's pointer is
+ // aiming at a place over.
+ const hovered = p.pointer_inside and !(kind == .column and p.drag == .column_move);
if (hovered) {
const tag_hit = p.pointer_tag_hit;
const col: ?usize = if (tag_hit) |pointer|
@@ -1033,6 +1030,40 @@ test "place puts every part where the painters and layers put it, in every layou
}
}
+test "the word under the pointer on a notice is lit in its layer and in the grid's copy" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 41, .rows = 12 });
+ defer p.deinit();
+ _ = try p.setTestFile("text\n");
+ var arena: std.heap.ArenaAllocator = .init(std.testing.allocator);
+ defer arena.deinit();
+ try std.testing.expect(p.executeBuiltinLine(p.active, "Msg Look here"));
+ p.sync();
+ var s = try p.render(arena.allocator());
+ const notice = for (s.regionList()) |region| {
+ if (region.kind == .notice) break region;
+ } else return error.TestExpectedNotice;
+ const row = s.cells[@as(usize, notice.rect.y) * s.cols ..][0..s.cols];
+ // "here", the chip's last word, before its blank edge.
+ const here = notice.rect.x + notice.rect.w - 5;
+ try std.testing.expectEqualStrings("h", row[here].grapheme());
+ p.pointer_inside = true;
+ p.hover_col = here + 1;
+ p.hover_row = notice.rect.y;
+ _ = arena.reset(.retain_capacity);
+ s = try p.render(arena.allocator());
+ const lit = s.cells[@as(usize, notice.rect.y) * s.cols ..][0..s.cols];
+ const sel: Color = .{ .rgb = p.theme().sel_bg };
+ for (lit[here..][0..4]) |cell| try std.testing.expectEqual(sel, cell.style.bg);
+ try std.testing.expect(!std.meta.eql(sel, lit[here - 2].style.bg));
+ const layer = for (s.tagLayers()) |*layer| {
+ if (layer.rows != 0 and layer.kind == .notice) break layer;
+ } else return error.TestExpectedNotice;
+ const lit_in_layer = for (layer.cells) |cell| {
+ if (std.meta.eql(sel, cell.style.bg)) break true;
+ } else false;
+ try std.testing.expect(lit_in_layer);
+}
+
test "a wide glyph cut by a tag's or a notice's edge is joined into the grid as blank, never half drawn" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 41, .rows = 12 });
defer p.deinit();
diff --git a/src/macos.zig b/src/macos.zig
index f7e03cd4..4d8f2883 100644
--- a/src/macos.zig
+++ b/src/macos.zig
@@ -1866,13 +1866,10 @@ fn blankTagCells(cells: []Cell, cols: u16, rows: u16, source: []const pardes.Cel
if (layer.rows == 0 or layer.viewport.y >= rows or layer.viewport.x >= cols) continue;
const start = @as(usize, layer.viewport.y) * cols + layer.viewport.x;
const count = @min(layer.viewport.w, cols - layer.viewport.x);
- for (cells[start..][0..count], source[start..][0..count]) |*cell, was| {
- // The tag text is the layer's to draw, but a look-hover on a tag
- // word is a canonical fact: carry the hover bit through so the
- // glass overlay still finds the affordance rect.
- cell.* = encodeCell(.{});
- cell.flags |= @as(u8, @intFromBool(was.hover)) * cell_flag_hover;
- }
+ // The tag text is the layer's to draw. (The look-hover's glass
+ // rect went with the core's hover bit, stage 8's follow-up.)
+ _ = source;
+ for (cells[start..][0..count]) |*cell| cell.* = encodeCell(.{});
}
}
@@ -2574,7 +2571,7 @@ fn encodeCell(cell: pardes.Cell) Cell {
.fg = encodeColor(cell.style.fg),
.bg = encodeColor(cell.style.bg),
.attrs = encodeAttrs(cell.style),
- .flags = encodeCellFlags(cell.default, cell.style.font_role, cell.hover),
+ .flags = encodeCellFlags(cell.default, cell.style.font_role, false),
.len = if (cell.default) 1 else cell.len,
};
if (cell.default)
@@ -3131,16 +3128,11 @@ test "mac tag layer ABI preserves logical capacity and physical grip" {
try std.testing.expectEqual(@as(u32, 11), pardes_tag_layer_value(0, 8));
try std.testing.expectEqual(@as(u8, 'Z'), pardes_tag_layer_cells(0).?[11].text[0]);
var canonical: [24]Cell = @splat(encodeCell(.{ .text = .{'G'} ++ .{0} ** 6, .len = 1, .default = false }));
- // A look-hover under the layer is a canonical fact and survives the blank;
- // the glyph it marks does not, because the layer redraws that text itself.
var source: [24]pardes.Cell = @splat(.{});
- source[11].hover = true;
blankTagCells(&canonical, 8, 3, &source, &core.surface.tag_layers);
try std.testing.expectEqual(@as(u8, 'G'), canonical[9].text[0]);
try std.testing.expectEqual(@as(u8, 'G'), canonical[18].text[0]);
try std.testing.expectEqual(encodeCell(.{}).len, canonical[10].len);
- try std.testing.expectEqual(@as(u8, 0), canonical[10].flags & cell_flag_hover);
- try std.testing.expectEqual(cell_flag_hover, canonical[11].flags & cell_flag_hover);
}
test "mac window blur builtin reaches the ABI without changing opacity" {
diff --git a/src/surface.zig b/src/surface.zig
index 07b593b1..b6f41853 100644
--- a/src/surface.zig
+++ b/src/surface.zig
@@ -38,12 +38,6 @@ pub const Cell = struct {
len: u8 = 1,
style: CellStyle = .{},
default: bool = true,
- /// The look-hover affordance is on this cell: the word under the pointer
- /// is a real Look or Exec operand, and hosts that draw the quiet tint as
- /// more than a flat fill — macOS renders it as a glass overlay — read
- /// this instead of guessing from the background colour, which a real
- /// selection shares.
- hover: bool = false,
pub fn grapheme(c: *const Cell) []const u8 {
return c.text[0..c.len];
@@ -405,10 +399,6 @@ pub const Chrome = struct {
pub const Surface = struct {
pub const PointerShape = enum(u8) { arrow, link, target };
pointer_shape: PointerShape = .arrow,
- /// Set only for the duration of the look-hover paint, so the cells that
- /// paint the quiet affordance can carry the hover bit through the ABI
- /// without every selection painter learning a new parameter.
- mark_hover: bool = false,
body_layers: [MAX_PANES]Layer = @splat(.{}),
tag_layers: [MAX_TAG_LAYERS]Layer = @splat(.{}),
cols: u16 = 0,
@@ -474,11 +464,7 @@ pub const Surface = struct {
pub fn at(s: *Surface, x: u16, y: u16) *Cell {
std.debug.assert(x < s.cols and y < s.rows);
- const cell = &s.cells[@as(usize, y) * s.cols + x];
- // The look-hover paint runs with mark_hover set, and its cells are
- // exactly the ones this marks — see render's preview block.
- if (s.mark_hover) cell.hover = true;
- return cell;
+ return &s.cells[@as(usize, y) * s.cols + x];
}
pub fn set(s: *Surface, x: u16, y: u16, text: []const u8, style: CellStyle) void {