summaryrefslogtreecommitdiff
path: root/src/tui_engine.zig
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-06-16 11:49:54 -0600
committert <t@tjp.lol>2026-06-16 11:58:44 -0600
commit28cb3eacfcf1217b4d014dd864005deb2d7f0585 (patch)
tree22a49deb365589e3180a8e119870d6abb2ed308c /src/tui_engine.zig
parent92ce53385fa438cab221c6881da141b5ef0a7261 (diff)
tool collapsed state, and fixing dirty component flags
Diffstat (limited to 'src/tui_engine.zig')
-rw-r--r--src/tui_engine.zig63
1 files changed, 52 insertions, 11 deletions
diff --git a/src/tui_engine.zig b/src/tui_engine.zig
index 6c5b372..f63e269 100644
--- a/src/tui_engine.zig
+++ b/src/tui_engine.zig
@@ -485,10 +485,12 @@ pub const Engine = struct {
var cut: ?usize = null;
for (self.slots.items, 0..) |*slot, idx| {
- const first_changed = slot.comp.firstLineChanged();
+ const dirty_signal = slot.comp.firstLineChanged();
// Render only when there's a reason to: dirty signal, or first
- // paint (no cached lines yet), or a forced full redraw.
- const must_render = first_changed != null or slot.lines.len == 0 or self.force_full;
+ // paint (no cached lines yet), or a forced full redraw. The signal
+ // is ONLY a dirty/render hint; the repaint cut below is computed
+ // from the actual old-vs-new line diff after rendering.
+ const must_render = dirty_signal != null or slot.lines.len == 0 or self.force_full;
var new_lines: []const []const u8 = undefined;
if (must_render) {
@@ -503,6 +505,7 @@ pub const Engine = struct {
if (visibleWidth(line) > self.width) return Error.LineOverflow;
}
+ const first_changed = firstDiffLines(slot.lines, new_lines);
frames[idx] = .{
.new_lines = new_lines,
.first_changed = first_changed,
@@ -510,18 +513,13 @@ pub const Engine = struct {
.offset = offset,
};
- // 2. cut = min across ALL components (not just the first dirty).
+ // 2. cut = min across ALL components with an ACTUAL line diff.
+ // A dirty component whose re-rendered bytes are identical leaves
+ // first_changed null and does not move the repaint point.
if (first_changed) |fc| {
const global = offset + fc;
cut = if (cut) |c| @min(c, global) else global;
}
- // A length delta is itself a change point even when the component
- // reports firstLineChanged == null (the diff backstop will catch
- // the content, but the boundary must roll the cut back).
- if (new_lines.len != slot.line_count) {
- const boundary = offset + @min(new_lines.len, slot.line_count);
- cut = if (cut) |c| @min(c, boundary) else boundary;
- }
offset += new_lines.len;
}
@@ -634,6 +632,21 @@ pub const Engine = struct {
slot.lines = copies;
}
+ /// Lowest local line index where `new_lines` differs from `old_lines`, or
+ /// null when they are byte-identical (including line count). This is the
+ /// authoritative repaint signal for a rendered component in the current
+ /// frame; pre-render `firstLineChanged()` is treated only as a cheap dirty
+ /// hint for whether the component needs to render.
+ fn firstDiffLines(old_lines: []const []const u8, new_lines: []const []const u8) ?usize {
+ const n = @min(old_lines.len, new_lines.len);
+ var i: usize = 0;
+ while (i < n) : (i += 1) {
+ if (!std.mem.eql(u8, old_lines[i], new_lines[i])) return i;
+ }
+ if (old_lines.len != new_lines.len) return n;
+ return null;
+ }
+
/// Line-diff backstop (the CORRECTNESS FLOOR). Flattens the previous
/// baseline lines (`slot.lines`) and the new frame lines into global arrays
/// and returns the FIRST global index where they actually differ, or null
@@ -1324,6 +1337,34 @@ test "line-diff backstop corrects an inaccurate firstLineChanged" {
try testing.expect(std.mem.indexOf(u8, out, terminal.seq.full_clear) == null);
}
+test "dirty hint at 0 still repaints from actual first changed line" {
+ var buf = std.Io.Writer.Allocating.init(testing.allocator);
+ defer buf.deinit();
+ var eng = makeEngine(&buf, 80, 100);
+ defer eng.deinit();
+
+ // Simulates a component that conservatively reports dirty from its top
+ // (e.g. markDirty dropped its internal cache), while the engine still has
+ // the previous slot baseline and can cheaply find the true line diff.
+ var body = FakeComponent{
+ .scripts = &.{ &.{ "l0", "l1", "l2", "l3" }, &.{ "l0", "l1", "l2", "L3" } },
+ .first_changed = &.{ 0, 0 },
+ };
+ try eng.addComponent(body.comp());
+
+ try eng.render();
+ body.advance();
+ buf.clearRetainingCapacity();
+ try eng.render();
+
+ const out = buf.written();
+ try testing.expect(std.mem.indexOf(u8, out, "L3") != null);
+ try testing.expect(std.mem.indexOf(u8, out, "l0") == null);
+ try testing.expect(std.mem.indexOf(u8, out, "l1") == null);
+ try testing.expect(std.mem.indexOf(u8, out, "l2") == null);
+ try testing.expect(std.mem.indexOf(u8, out, terminal.seq.full_clear) == null);
+}
+
test "consecutive differential frames move the cursor up correctly (streaming append)" {
// Regression: the differential path must account for the cursor resting at
// the END of the last content line (no trailing newline), not one row