summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 17:15:04 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commite6745a1756c50dd78c4b85641bd2f0e67681dc5d (patch)
treed053a1d4231bae1c5550caed7dae99bae27d07df /src
parent3c981a1b9f3e1328e8ee4b0181d90b7c87f5061a (diff)
downloadpardes-e6745a1756c50dd78c4b85641bd2f0e67681dc5d.tar.gz
pardes-e6745a1756c50dd78c4b85641bd2f0e67681dc5d.zip
Faded ink on blank cells is not sent to the terminal: a focus change sent every blank of the other panes again
Diffstat (limited to 'src')
-rw-r--r--src/tty/tty.zig125
1 files changed, 116 insertions, 9 deletions
diff --git a/src/tty/tty.zig b/src/tty/tty.zig
index 84552ddc..97c720ea 100644
--- a/src/tty/tty.zig
+++ b/src/tty/tty.zig
@@ -1442,7 +1442,7 @@ const Shell = struct {
const shadowed = chipShadows(s.frame.allocator(), surface, s.core, truecolor) catch surface;
const flashed = s.flash.apply(s.frame.allocator(), shadowed, s.core, truecolor) catch shadowed;
const trailed = s.trail.apply(s.frame.allocator(), flashed, s.core, truecolor) catch flashed;
- paintCells(win, trailed.cells, trailed.cols, trailed.rows);
+ paintCells(vx, win, trailed.cells, trailed.cols, trailed.rows);
tz_cells.end();
for (surface.images[0..surface.nimages]) |maybe| {
const place = maybe orelse continue;
@@ -2159,22 +2159,129 @@ fn mouseEvent(m: vaxis.Mouse) ?pardes.Event {
} };
}
-fn paintCells(win: vaxis.Window, cells: []const pardes.Cell, cols: u16, rows: u16) void {
+fn paintCells(vx: *const vaxis.Vaxis, win: vaxis.Window, cells: []const pardes.Cell, cols: u16, rows: u16) void {
win.clear();
var y: u16 = 0;
while (y < rows) : (y += 1) {
+ const row = cells[@as(usize, y) * cols ..][0..cols];
var x: u16 = 0;
+ // The cell to the left as written, for a blank that needs it.
+ var left: ?Written = null;
while (x < cols) : (x += 1) {
- const cell = &cells[@as(usize, y) * cols + x];
- if (cell.default) continue;
- win.writeCell(x, y, .{
- .char = .{ .grapheme = cell.grapheme() },
- .style = vaxisStyle(cell.style),
- });
+ const cell = &row[x];
+ if (cell.default) {
+ left = null;
+ continue;
+ }
+ const grapheme = cell.grapheme();
+ var style = vaxisStyle(cell.style);
+ if (inkless(grapheme, style)) blankInk(vx, row, x, y, &style, left);
+ left = .{ .grapheme = grapheme, .style = style };
+ win.writeCell(x, y, .{ .char = .{ .grapheme = grapheme }, .style = style });
}
}
}
+const Written = struct { grapheme: []const u8, style: vaxis.Style };
+
+/// A blank's ink shows nowhere (no underline, strike or reverse to show it).
+fn inkless(grapheme: []const u8, style: vaxis.Style) bool {
+ return std.mem.eql(u8, grapheme, " ") and !style.reverse and !style.strikethrough and style.ul_style == .off;
+}
+
+/// A blank whose ink alone changed (a fade: InactiveDim on a focus change)
+/// keeps the ink the terminal has there and so is not sent at all:
+/// otherwise every blank of every pane a fade touched went, 7 KB a click
+/// for three shells. Only a short gap between two cells that are sent
+/// anyway is cheaper sent than jumped (a cursor move): it takes the ink of
+/// the cell before it, so the run needs no colour change either.
+fn blankInk(vx: *const vaxis.Vaxis, row: []const pardes.Cell, x: u16, y: u16, style: *vaxis.Style, left: ?Written) void {
+ const last = lastCell(vx, x, y) orelse return;
+ if (vx.refresh or last.default or last.skipped or !std.mem.eql(u8, last.char.items, " ")) return;
+ if (sameColor(last.style.fg, style.fg) or !sameButInk(last.style, style.*)) return;
+ var kept = style.*;
+ kept.fg = last.style.fg;
+ if (left) |l| if (sends(vx, x - 1, y, l.grapheme, l.style)) {
+ var end = x + 1;
+ while (end < row.len and end - x < 8) : (end += 1) {
+ const next = &row[end];
+ if (next.default) break;
+ const next_style = vaxisStyle(next.style);
+ if (inkless(next.grapheme(), next_style)) continue;
+ if (sends(vx, end, y, next.grapheme(), next_style)) {
+ style.fg = l.style.fg;
+ return;
+ }
+ break;
+ }
+ };
+ style.* = kept;
+}
+
+/// `Style.eql` but for the ink, and cheap: this runs for every blank of
+/// every frame.
+fn sameButInk(a: vaxis.Style, b: vaxis.Style) bool {
+ return sameColor(a.bg, b.bg) and sameColor(a.ul, b.ul) and a.ul_style == b.ul_style and
+ a.bold == b.bold and a.dim == b.dim and a.italic == b.italic and a.blink == b.blink and
+ a.reverse == b.reverse and a.invisible == b.invisible and a.strikethrough == b.strikethrough;
+}
+
+fn sameColor(a: vaxis.Color, b: vaxis.Color) bool {
+ return switch (a) {
+ .default => b == .default,
+ .index => |i| b == .index and b.index == i,
+ .rgb => |rgb| b == .rgb and std.mem.eql(u8, &rgb, &b.rgb),
+ };
+}
+
+/// Whether vaxis sends this cell: it differs from what the terminal shows.
+fn sends(vx: *const vaxis.Vaxis, x: u16, y: u16, grapheme: []const u8, style: vaxis.Style) bool {
+ const last = lastCell(vx, x, y) orelse return true;
+ return vx.refresh or last.default or last.skipped or !std.mem.eql(u8, last.char.items, grapheme) or
+ !sameColor(last.style.fg, style.fg) or !sameButInk(last.style, style);
+}
+
+fn lastCell(vx: *const vaxis.Vaxis, x: u16, y: u16) ?*const vaxis.AllocatingScreen.InternalCell {
+ if (x >= vx.screen.width) return null;
+ const i = @as(usize, y) * vx.screen.width + x;
+ return if (i < vx.screen_last.buf.len) &vx.screen_last.buf[i] else null;
+}
+
+test "a fade sends the text it dims, not the blanks beside it" {
+ const gpa = std.testing.allocator;
+ var env = try std.testing.environ.createMap(gpa);
+ defer env.deinit();
+ var vx = try vaxis.init(std.testing.io, gpa, &env, .{});
+ var out: std.Io.Writer.Allocating = .init(gpa);
+ defer out.deinit();
+ defer vx.deinit(gpa, &out.writer);
+ try vx.resize(gpa, &out.writer, .{ .rows = 1, .cols = 32, .x_pixel = 0, .y_pixel = 0 });
+ // `a`, a long gap, `b`, a two-cell gap, `c`, a long gap, an underlined
+ // blank (which shows its ink) and one more blank.
+ var cells: [32]pardes.Cell = @splat(.{ .default = false });
+ cells[0].text[0] = 'a';
+ cells[12].text[0] = 'b';
+ cells[15].text[0] = 'c';
+ cells[30].style.ul = .single;
+ var sent: [2][]u8 = .{ &.{}, &.{} };
+ defer for (sent) |bytes| gpa.free(bytes);
+ for ([_][3]u8{ .{ 200, 200, 200 }, .{ 90, 90, 90 } }, 0..) |ink, frame| {
+ for (&cells) |*cell| cell.style.fg = .{ .rgb = ink };
+ out.clearRetainingCapacity();
+ paintCells(&vx, vx.window(), &cells, 32, 1);
+ try vx.render(&out.writer);
+ sent[frame] = try out.toOwnedSlice();
+ }
+ // Against a cleared screen every cell goes. After the fade the letters
+ // go, the short gap between two of them (cheaper than a cursor move),
+ // and the underlined blank; the long gaps are jumped.
+ try std.testing.expectEqual(@as(usize, 29), std.mem.count(u8, sent[0], " "));
+ try std.testing.expectEqual(@as(usize, 3), std.mem.count(u8, sent[1], " "));
+ try std.testing.expect(std.mem.indexOf(u8, sent[1], "b c") != null);
+ try std.testing.expect(std.mem.indexOf(u8, sent[1], "90:90:90") != null);
+ try std.testing.expect(std.mem.indexOfScalar(u8, sent[1], 'a') != null);
+}
+
/// T1 (§9.2): each notice chip casts a shadow one cell right and one down,
/// inside its own pane's body: its ground 35% toward black in OKLab on a
/// cell with text; on a blank one a half block (▌ on the right, ▀ below,
@@ -2570,7 +2677,7 @@ const Attach = struct {
fn paint(a: *Attach) void {
a.dirty = false;
const win = a.vx.window();
- paintCells(win, a.client.grid.items, a.client.cols, a.client.rows);
+ paintCells(a.vx, win, a.client.grid.items, a.client.cols, a.client.rows);
if (a.client.cursor) |cur| paintCursor(win, cur.x, cur.y, cur.bar);
a.vx.render(a.tty.writer()) catch {};
}