summaryrefslogtreecommitdiff
path: root/src/tty
diff options
context:
space:
mode:
Diffstat (limited to 'src/tty')
-rw-r--r--src/tty/panel_compositor.zig140
1 files changed, 57 insertions, 83 deletions
diff --git a/src/tty/panel_compositor.zig b/src/tty/panel_compositor.zig
index 647c36ff..1891c623 100644
--- a/src/tty/panel_compositor.zig
+++ b/src/tty/panel_compositor.zig
@@ -1,8 +1,9 @@
//! TTY implementation of panel transitions.
//!
-//! The core publishes final cells plus POD tracks. This backend copies that
-//! grid and performs the transition in cell space; no terminal escape trick
-//! or pixel assumption leaks into the core.
+//! The core publishes presentation cells plus POD tracks. It has already
+//! composed PanelAscii because byte stepping is semantic, while this backend
+//! copies the grid for geometry and dissolve operations. No terminal escape
+//! trick or pixel assumption leaks into the core.
const std = @import("std");
const pardes = @import("../pardes.zig");
@@ -22,7 +23,7 @@ pub fn hidesAttachment(tracks: []const Track, serial: u32) bool {
if (track.effect != .slide and track.effect != .zoom and track.effect != .vertical) continue;
// The last active sample is already the exact canonical frame. Place
// the image there too, rather than making it pop in one tick later.
- if (track.frame + 1 < track.effect.frames()) return true;
+ if (track.frame + 1 < track.frames()) return true;
}
return false;
}
@@ -83,7 +84,9 @@ pub fn compose(
.off => {},
.slide, .zoom => transform(out, source, track),
.dissolve => dissolve(out, source, track),
- .ascii => ascii(out, source, track),
+ // Pardes core has already composed the byte-wise ASCII diff
+ // into source.cells. TTY only owns cursor suppression here.
+ .ascii => {},
.vertical => vertical(out, source, track),
}
}
@@ -146,33 +149,6 @@ fn dissolve(out: *pardes.Surface, source: *const pardes.Surface, track: Track) v
}
}
-fn ascii(out: *pardes.Surface, source: *const pardes.Surface, track: Track) void {
- const area = clipped(track.to, out.cols, out.rows) orelse return;
- const amount = track.amount();
- var y = area.y0;
- while (y < area.y1) : (y += 1) {
- var x = area.x0;
- while (x < area.x1) : (x += 1) {
- if (!source.panelCellChanged(x, y)) continue;
- const material = panel_animation.asciiCell(track.serial, x - area.x0, y - area.y0, amount);
- if (material.reveal) continue;
- const cell = out.at(x, y);
- if (material.glyph == ' ') {
- cell.* = previousCell(source, x, y).*;
- continue;
- }
- // The wave itself wears the new cell's colors, then yields to its
- // real glyph. Before the wave arrives the complete old cell—not a
- // synthetic blank—remains visible above.
- cell.* = source.cells[@as(usize, y) * source.cols + x];
- cell.text[0] = material.glyph;
- cell.len = 1;
- cell.default = false;
- cell.style.dim = true;
- }
- }
-}
-
fn previousCell(source: *const pardes.Surface, x: u16, y: u16) *const pardes.Cell {
std.debug.assert(source.hasPanelDiff());
return &source.previous_cells[@as(usize, y) * source.cols + x];
@@ -258,10 +234,10 @@ fn clipped(box: Box, cols: u16, rows: u16) ?GridBox {
return if (area.x0 < area.x1 and area.y0 < area.y1) area else null;
}
-test "ASCII materialization happens in copied TTY cells" {
+test "TTY consumes core-composed ASCII cells without reinterpreting glyphs" {
var cells: [8]pardes.Cell = @splat(.{});
var previous: [8]pardes.Cell = @splat(.{});
- const changed: [8]u8 = @splat(1);
+ const diffs: [8]pardes.PanelCellDiff = @splat(.visual);
for (&cells) |*cell| {
cell.text[0] = 'X';
cell.len = 1;
@@ -277,7 +253,7 @@ test "ASCII materialization happens in copied TTY cells" {
.rows = 2,
.cells = &cells,
.previous_cells = &previous,
- .changed_cells = &changed,
+ .cell_diffs = &diffs,
};
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
@@ -291,9 +267,7 @@ test "ASCII materialization happens in copied TTY cells" {
const animated = try compose(arena.allocator(), &surface, &.{track}, null);
try std.testing.expect(animated != &surface);
try std.testing.expectEqualStrings("X", surface.at(0, 0).grapheme());
- var differs = false;
- for (animated.cells) |cell| differs = differs or !std.mem.eql(u8, cell.grapheme(), "X");
- try std.testing.expect(differs);
+ for (animated.cells) |cell| try std.testing.expectEqualStrings("X", cell.grapheme());
}
test "dissolve changes only diff cells from old to new" {
@@ -309,13 +283,13 @@ test "dissolve changes only diff cells from old to new" {
cell.len = 1;
cell.default = false;
}
- const changed = [_]u8{ 1, 0 };
+ const diffs = [_]pardes.PanelCellDiff{ .{ .ascii = .{ .from = 'O', .to = 'N' } }, .unchanged };
var surface: pardes.Surface = .{
.cols = 2,
.rows = 1,
.cells = &cells,
.previous_cells = &previous,
- .changed_cells = &changed,
+ .cell_diffs = &diffs,
};
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
@@ -337,45 +311,45 @@ test "dissolve changes only diff cells from old to new" {
try std.testing.expectEqual(cells[1], final.at(1, 0).*);
}
-test "ASCII diff travels old through punctuation to new" {
- var cells = [_]pardes.Cell{.{}};
- var previous = [_]pardes.Cell{.{}};
- cells[0].text[0] = 'N';
- cells[0].default = false;
- previous[0].text[0] = 'O';
- previous[0].default = false;
- const changed = [_]u8{1};
+test "TTY content effects never touch cells outside the published diff" {
+ var cells: [4]pardes.Cell = @splat(.{});
+ var previous: [4]pardes.Cell = @splat(.{});
+ for (&cells, "NUQV") |*cell, glyph| {
+ cell.text[0] = glyph;
+ cell.len = 1;
+ cell.default = false;
+ }
+ // Deliberately disagree even where the mask says unchanged. This makes
+ // the test prove that the compositor obeys the core cell diff rather than
+ // merely appearing correct because old and new happened to match there.
+ for (&previous, "OXRY") |*cell, glyph| {
+ cell.text[0] = glyph;
+ cell.len = 1;
+ cell.default = false;
+ }
+ const diffs = [_]pardes.PanelCellDiff{ .visual, .unchanged, .visual, .unchanged };
var surface: pardes.Surface = .{
- .cols = 1,
+ .cols = 4,
.rows = 1,
.cells = &cells,
.previous_cells = &previous,
- .changed_cells = &changed,
+ .cell_diffs = &diffs,
};
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
- var track: Track = .{
- .serial = 9,
- .effect = .ascii,
- .from = .{ .w = 1, .h = 1 },
- .to = .{ .w = 1, .h = 1 },
- };
- const old_frame = try compose(arena.allocator(), &surface, &.{track}, null);
- try std.testing.expectEqualStrings("O", old_frame.at(0, 0).grapheme());
-
- _ = arena.reset(.retain_capacity);
- track.frame = 7;
- const wave = panel_animation.asciiCell(track.serial, 0, 0, track.amount());
- try std.testing.expect(!wave.reveal and wave.glyph != ' ');
- const punctuation = try compose(arena.allocator(), &surface, &.{track}, null);
- try std.testing.expectEqualSlices(u8, &.{wave.glyph}, punctuation.at(0, 0).grapheme());
- try std.testing.expect(punctuation.at(0, 0).style.dim);
-
- _ = arena.reset(.retain_capacity);
- track.frame = track.effect.frames() - 1;
- const new_frame = try compose(arena.allocator(), &surface, &.{track}, null);
- try std.testing.expectEqual(cells[0], new_frame.at(0, 0).*);
+ for ([_]panel_animation.Transition{ .ascii, .dissolve }) |effect| {
+ const track: Track = .{
+ .serial = 17,
+ .effect = effect,
+ .from = .{ .w = 4, .h = 1 },
+ .to = .{ .w = 4, .h = 1 },
+ };
+ const animated = try compose(arena.allocator(), &surface, &.{track}, null);
+ try std.testing.expectEqual(cells[1], animated.at(1, 0).*);
+ try std.testing.expectEqual(cells[3], animated.at(3, 0).*);
+ _ = arena.reset(.retain_capacity);
+ }
}
test "content transition without a diff snaps to canonical surface" {
@@ -405,14 +379,14 @@ test "content transition without a diff snaps to canonical surface" {
test "exact transition endpoint preserves the canonical cursor" {
var cells = [_]pardes.Cell{.{}};
var previous = cells;
- const changed = [_]u8{1};
+ const diffs = [_]pardes.PanelCellDiff{.visual};
var surface: pardes.Surface = .{
.cols = 1,
.rows = 1,
.cells = &cells,
.cursor = .{ .x = 0, .y = 0 },
.previous_cells = &previous,
- .changed_cells = &changed,
+ .cell_diffs = &diffs,
};
const track: Track = .{
.effect = .ascii,
@@ -441,13 +415,13 @@ test "vertical opening rises through a fixed old-grid clip" {
cell.len = 1;
cell.default = false;
}
- const changed: [12]u8 = @splat(1);
+ const diffs: [12]pardes.PanelCellDiff = @splat(.visual);
var surface: pardes.Surface = .{
.cols = 3,
.rows = 4,
.cells = &cells,
.previous_cells = &previous,
- .changed_cells = &changed,
+ .cell_diffs = &diffs,
};
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
@@ -481,7 +455,7 @@ test "vertical opening rises through a fixed old-grid clip" {
try std.testing.expectEqualStrings("c", final.at(0, 2).grapheme());
}
-test "vertical closing slides frozen content upward over canonical cells" {
+test "vertical closing drops frozen content over canonical cells" {
var cells: [12]pardes.Cell = @splat(.{});
var previous: [12]pardes.Cell = @splat(.{});
for (&cells, "aaabbbcccddd") |*cell, glyph| {
@@ -494,13 +468,13 @@ test "vertical closing slides frozen content upward over canonical cells" {
cell.len = 1;
cell.default = false;
}
- const changed: [12]u8 = @splat(1);
+ const diffs: [12]pardes.PanelCellDiff = @splat(.visual);
var surface: pardes.Surface = .{
.cols = 3,
.rows = 4,
.cells = &cells,
.previous_cells = &previous,
- .changed_cells = &changed,
+ .cell_diffs = &diffs,
};
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
@@ -518,10 +492,10 @@ test "vertical closing slides frozen content upward over canonical cells" {
try std.testing.expectEqualStrings("2", first.at(0, 2).grapheme());
_ = arena.reset(.retain_capacity);
- track.frame = 2;
+ track.frame = 8;
const leaving = try compose(arena.allocator(), &surface, &.{track}, null);
- try std.testing.expectEqualStrings("2", leaving.at(0, 1).grapheme());
- try std.testing.expectEqualStrings("c", leaving.at(0, 2).grapheme());
+ try std.testing.expectEqualStrings("b", leaving.at(0, 1).grapheme());
+ try std.testing.expectEqualStrings("1", leaving.at(0, 2).grapheme());
try std.testing.expectEqualStrings("a", leaving.at(0, 0).grapheme());
try std.testing.expectEqualStrings("d", leaving.at(0, 3).grapheme());
@@ -539,13 +513,13 @@ test "closing content paints after opening content regardless of track order" {
cells[0].default = false;
previous[0].text[0] = 'O';
previous[0].default = false;
- const changed = [_]u8{1};
+ const diffs = [_]pardes.PanelCellDiff{.visual};
var surface: pardes.Surface = .{
.cols = 1,
.rows = 1,
.cells = &cells,
.previous_cells = &previous,
- .changed_cells = &changed,
+ .cell_diffs = &diffs,
};
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();