1ad73c5e
refactor: one clipped render, a row subset or all of them
a73x 2026-08-29 10:01
Commit message
src/tui/interact.zig
| Old | New | ||
|---|---|---|---|
| @@ -1530,7 +1530,7 @@ pub const Core = struct { | |||
| 1530 | if (!self.beginPaint()) return; | 1530 | if (!self.beginPaint()) return; |
| 1531 | defer self.endPaint(); | 1531 | defer self.endPaint(); |
| 1532 | const hl = self.highlight(); | 1532 | const hl = self.highlight(); |
| 1533 | try paint_mod.renderClipped(self.alloc, self.rep.eng, self.viewport(), hl.sink(), self.owns_screen, self.out_fd); | 1533 | try paint_mod.renderClipped(self.alloc, self.rep.eng, self.viewport(), hl.sink(), null, self.owns_screen, self.out_fd); |
| 1534 | paintOverlay(self.alloc, &self.overlay, self.rep.eng.cursorPos(), self.viewport(), self.out_fd); | 1534 | paintOverlay(self.alloc, &self.overlay, self.rep.eng.cursorPos(), self.viewport(), self.out_fd); |
| 1535 | } | 1535 | } |
| 1536 | 1536 | ||
| @@ -1603,7 +1603,7 @@ pub const Core = struct { | |||
| 1603 | if (!self.beginPaint()) return; | 1603 | if (!self.beginPaint()) return; |
| 1604 | defer self.endPaint(); | 1604 | defer self.endPaint(); |
| 1605 | const hl = self.highlight(); | 1605 | const hl = self.highlight(); |
| 1606 | try paint_mod.renderClipped(self.alloc, self.rep.eng, self.viewport(), hl.sink(), self.owns_screen, self.out_fd); | 1606 | try paint_mod.renderClipped(self.alloc, self.rep.eng, self.viewport(), hl.sink(), null, self.owns_screen, self.out_fd); |
| 1607 | } | 1607 | } |
| 1608 | 1608 | ||
| 1609 | /// A one-line marker in the corner, painted over by the next full | 1609 | /// A one-line marker in the corner, painted over by the next full |
| @@ -1675,7 +1675,7 @@ pub const Core = struct { | |||
| 1675 | // between them would put the two halves of this frame | 1675 | // between them would put the two halves of this frame |
| 1676 | // on two different screens), and the sink is not reentrant. | 1676 | // on two different screens), and the sink is not reentrant. |
| 1677 | const hl = self.highlight(); | 1677 | const hl = self.highlight(); |
| 1678 | try paint_mod.renderClipped(self.alloc, self.rep.eng, self.viewport(), hl.sink(), self.owns_screen, self.out_fd); | 1678 | try paint_mod.renderClipped(self.alloc, self.rep.eng, self.viewport(), hl.sink(), null, self.owns_screen, self.out_fd); |
| 1679 | self.repaint_after_resync = false; | 1679 | self.repaint_after_resync = false; |
| 1680 | } else { | 1680 | } else { |
| 1681 | const hl = self.highlight(); | 1681 | const hl = self.highlight(); |
src/tui/paint.zig
| Old | New | ||
|---|---|---|---|
| @@ -61,12 +61,13 @@ pub fn clampCursor(cur: Engine.CursorPos, vp: Viewport) Engine.CursorPos { | |||
| 61 | } | 61 | } |
| 62 | 62 | ||
| 63 | /// The replica may exceed the tty under latest-wins; rows clip at the | 63 | /// The replica may exceed the tty under latest-wins; rows clip at the |
| 64 | /// right edge because DECAWM is off from attach. | 64 | /// right edge (DECAWM off). `rows` null is every row of the viewport. |
| 65 | pub fn renderClipped( | 65 | pub fn renderClipped( |
| 66 | alloc: std.mem.Allocator, | 66 | alloc: std.mem.Allocator, |
| 67 | replica: *Engine, | 67 | replica: *Engine, |
| 68 | vp: Viewport, | 68 | vp: Viewport, |
| 69 | hl: Highlight, | 69 | hl: Highlight, |
| 70 | rows: ?[]const u16, | ||
| 70 | owns_screen: bool, | 71 | owns_screen: bool, |
| 71 | out_fd: std.posix.fd_t, | 72 | out_fd: std.posix.fd_t, |
| 72 | ) !void { | 73 | ) !void { |
| @@ -82,11 +83,16 @@ pub fn renderClipped( | |||
| 82 | const view: Engine.RowView = .{ .col_off = vp.left, .cols = vp.cols }; | 83 | const view: Engine.RowView = .{ .col_off = vp.left, .cols = vp.cols }; |
| 83 | var ech_buf: [16]u8 = undefined; | 84 | var ech_buf: [16]u8 = undefined; |
| 84 | const ech = std.fmt.bufPrint(&ech_buf, "\x1b[{d}X", .{vp.cols}) catch ""; | 85 | const ech = std.fmt.bufPrint(&ech_buf, "\x1b[{d}X", .{vp.cols}) catch ""; |
| 85 | var y: u16 = 0; | 86 | // Span-bounded ECH per row where the screen was not cleared, so a |
| 86 | while (y < limit) : (y += 1) { | 87 | // shorter row cannot leave the old one showing and a tile cannot reach |
| 88 | // a neighbour's cells or a rail. | ||
| 89 | const clear: []const u8 = if (owns_screen) "" else ech; | ||
| 90 | const n = if (rows) |r| r.len else limit; | ||
| 91 | for (0..n) |k| { | ||
| 92 | const y: u16 = if (rows) |r| r[k] else @intCast(k); | ||
| 93 | if (y >= limit) continue; | ||
| 87 | var cup: [24]u8 = undefined; | 94 | var cup: [24]u8 = undefined; |
| 88 | const clear: []const u8 = if (owns_screen) "" else ech; | 95 | try paint.appendSlice(alloc, try std.fmt.bufPrint(&cup, "\x1b[{d};{d}H{s}", .{ @as(u32, y) + vp.top + 1, vp.left + 1, clear })); |
| 89 | try paint.appendSlice(alloc, try std.fmt.bufPrint(&cup, "\x1b[{d};{d}H{s}", .{ y + vp.top + 1, vp.left + 1, clear })); | ||
| 90 | const row = try dumpRow(alloc, replica, y, hl, view); | 96 | const row = try dumpRow(alloc, replica, y, hl, view); |
| 91 | defer alloc.free(row); | 97 | defer alloc.free(row); |
| 92 | try paint.appendSlice(alloc, row); | 98 | try paint.appendSlice(alloc, row); |
| @@ -110,31 +116,7 @@ pub fn renderRowsClipped( | |||
| 110 | out_fd: std.posix.fd_t, | 116 | out_fd: std.posix.fd_t, |
| 111 | ) !void { | 117 | ) !void { |
| 112 | if (rows.len == 0) return; | 118 | if (rows.len == 0) return; |
| 113 | var paint: std.ArrayList(u8) = .empty; | 119 | return renderClipped(alloc, replica, vp, hl, rows, false, out_fd); |
| 114 | defer paint.deinit(alloc); | ||
| 115 | try paint.appendSlice(alloc, sync_begin); | ||
| 116 | |||
| 117 | const grid_rows: u16 = @intCast(replica.term.rows); | ||
| 118 | const limit = @min(grid_rows, vp.rows); | ||
| 119 | const view: Engine.RowView = .{ .col_off = vp.left, .cols = vp.cols }; | ||
| 120 | var ech_buf: [16]u8 = undefined; | ||
| 121 | const ech = std.fmt.bufPrint(&ech_buf, "\x1b[{d}X", .{vp.cols}) catch ""; | ||
| 122 | for (rows) |y| { | ||
| 123 | if (y >= limit) continue; | ||
| 124 | // ECH per row: unlike a full render this never clears the | ||
| 125 | // screen, so a shorter row would leave the old one showing. | ||
| 126 | var cup: [24]u8 = undefined; | ||
| 127 | try paint.appendSlice(alloc, try std.fmt.bufPrint(&cup, "\x1b[{d};{d}H{s}", .{ @as(u32, y) + vp.top + 1, vp.left + 1, ech })); | ||
| 128 | const row = try dumpRow(alloc, replica, y, hl, view); | ||
| 129 | defer alloc.free(row); | ||
| 130 | try paint.appendSlice(alloc, row); | ||
| 131 | } | ||
| 132 | |||
| 133 | const cur = clampCursor(replica.cursorPos(), vp); | ||
| 134 | var cbuf: [16]u8 = undefined; | ||
| 135 | try paint.appendSlice(alloc, try std.fmt.bufPrint(&cbuf, "\x1b[{d};{d}H", .{ cur.y + vp.top + 1, cur.x + vp.left + 1 })); | ||
| 136 | try paint.appendSlice(alloc, sync_end); | ||
| 137 | try proto.writeAllFd(out_fd, paint.items); | ||
| 138 | } | 120 | } |
| 139 | 121 | ||
| 140 | /// Bounded by the REPLICA's grid, not the tty: the answer feeds | 122 | /// Bounded by the REPLICA's grid, not the tty: the answer feeds |
| @@ -345,7 +327,7 @@ test "renderClipped inverts the highlighted rows and leaves the rest alone" { | |||
| 345 | const pipe = try std.posix.pipe(); | 327 | const pipe = try std.posix.pipe(); |
| 346 | defer std.posix.close(pipe[0]); | 328 | defer std.posix.close(pipe[0]); |
| 347 | var h: TestHighlight = .{}; | 329 | var h: TestHighlight = .{}; |
| 348 | try renderClipped(alloc, replica, .{ .top = 0, .left = 0, .rows = 24, .cols = 80 }, h.hl(), true, pipe[1]); | 330 | try renderClipped(alloc, replica, .{ .top = 0, .left = 0, .rows = 24, .cols = 80 }, h.hl(), null, true, pipe[1]); |
| 349 | std.posix.close(pipe[1]); | 331 | std.posix.close(pipe[1]); |
| 350 | 332 | ||
| 351 | var out: [8192]u8 = undefined; | 333 | var out: [8192]u8 = undefined; |
| @@ -594,7 +576,7 @@ test "renderClipped paints only rows that fit and clamps the cursor" { | |||
| 594 | const pipe = try std.posix.pipe(); | 576 | const pipe = try std.posix.pipe(); |
| 595 | defer std.posix.close(pipe[0]); | 577 | defer std.posix.close(pipe[0]); |
| 596 | // Local tty is smaller than the 100x30 grid. | 578 | // Local tty is smaller than the 100x30 grid. |
| 597 | try renderClipped(alloc, replica, .{ .top = 0, .left = 0, .rows = 24, .cols = 80 }, .{}, true, pipe[1]); | 579 | try renderClipped(alloc, replica, .{ .top = 0, .left = 0, .rows = 24, .cols = 80 }, .{}, null, true, pipe[1]); |
| 598 | std.posix.close(pipe[1]); | 580 | std.posix.close(pipe[1]); |
| 599 | 581 | ||
| 600 | var out: std.ArrayList(u8) = .empty; | 582 | var out: std.ArrayList(u8) = .empty; |
| @@ -633,7 +615,7 @@ test "renderClipped stops at the grid when the tty is the larger one" { | |||
| 633 | 615 | ||
| 634 | const pipe = try std.posix.pipe(); | 616 | const pipe = try std.posix.pipe(); |
| 635 | defer std.posix.close(pipe[0]); | 617 | defer std.posix.close(pipe[0]); |
| 636 | try renderClipped(alloc, replica, .{ .top = 0, .left = 0, .rows = 24, .cols = 80 }, .{}, true, pipe[1]); | 618 | try renderClipped(alloc, replica, .{ .top = 0, .left = 0, .rows = 24, .cols = 80 }, .{}, null, true, pipe[1]); |
| 637 | std.posix.close(pipe[1]); | 619 | std.posix.close(pipe[1]); |
| 638 | 620 | ||
| 639 | var out: std.ArrayList(u8) = .empty; | 621 | var out: std.ArrayList(u8) = .empty; |
| @@ -671,6 +653,7 @@ test "paint: a tile at a row and column offset paints only inside its rect" { | |||
| 671 | replica, | 653 | replica, |
| 672 | .{ .top = row_off, .left = beside_off, .rows = tile_rows, .cols = beside_width }, | 654 | .{ .top = row_off, .left = beside_off, .rows = tile_rows, .cols = beside_width }, |
| 673 | .{}, | 655 | .{}, |
| 656 | null, | ||
| 674 | false, | 657 | false, |
| 675 | pipe[1], | 658 | pipe[1], |
| 676 | ); | 659 | ); |
| @@ -875,7 +858,7 @@ test "a pane off the left edge paints inside its own span" { | |||
| 875 | 858 | ||
| 876 | const pipe = try std.posix.pipe(); | 859 | const pipe = try std.posix.pipe(); |
| 877 | defer std.posix.close(pipe[0]); | 860 | defer std.posix.close(pipe[0]); |
| 878 | try renderClipped(alloc, replica, .{ .top = 0, .left = 40, .rows = 24, .cols = 39 }, .{}, false, pipe[1]); | 861 | try renderClipped(alloc, replica, .{ .top = 0, .left = 40, .rows = 24, .cols = 39 }, .{}, null, false, pipe[1]); |
| 879 | std.posix.close(pipe[1]); | 862 | std.posix.close(pipe[1]); |
| 880 | 863 | ||
| 881 | var out: [8192]u8 = undefined; | 864 | var out: [8192]u8 = undefined; |
| @@ -961,7 +944,7 @@ test "paint: a pane narrower than the grid paints no cell past its own edge" { | |||
| 961 | 944 | ||
| 962 | const pipe = try std.posix.pipe(); | 945 | const pipe = try std.posix.pipe(); |
| 963 | defer std.posix.close(pipe[0]); | 946 | defer std.posix.close(pipe[0]); |
| 964 | try renderClipped(alloc, replica, .{ .top = 0, .left = 0, .rows = 4, .cols = 12 }, .{}, false, pipe[1]); | 947 | try renderClipped(alloc, replica, .{ .top = 0, .left = 0, .rows = 4, .cols = 12 }, .{}, null, false, pipe[1]); |
| 965 | std.posix.close(pipe[1]); | 948 | std.posix.close(pipe[1]); |
| 966 | var out: [8192]u8 = undefined; | 949 | var out: [8192]u8 = undefined; |
| 967 | const n = try std.posix.read(pipe[0], &out); | 950 | const n = try std.posix.read(pipe[0], &out); |
| @@ -982,7 +965,7 @@ test "paint: a wide cell astride the pane's edge is dropped, not halved" { | |||
| 982 | 965 | ||
| 983 | const pipe = try std.posix.pipe(); | 966 | const pipe = try std.posix.pipe(); |
| 984 | defer std.posix.close(pipe[0]); | 967 | defer std.posix.close(pipe[0]); |
| 985 | try renderClipped(alloc, replica, .{ .top = 0, .left = 0, .rows = 4, .cols = 12 }, .{}, false, pipe[1]); | 968 | try renderClipped(alloc, replica, .{ .top = 0, .left = 0, .rows = 4, .cols = 12 }, .{}, null, false, pipe[1]); |
| 986 | std.posix.close(pipe[1]); | 969 | std.posix.close(pipe[1]); |
| 987 | var out: [8192]u8 = undefined; | 970 | var out: [8192]u8 = undefined; |
| 988 | const n = try std.posix.read(pipe[0], &out); | 971 | const n = try std.posix.read(pipe[0], &out); |
| @@ -1009,6 +992,7 @@ test "paint: a highlight in an offset pane inverts inside the pane" { | |||
| 1009 | replica, | 992 | replica, |
| 1010 | .{ .top = 0, .left = beside_off, .rows = 4, .cols = beside_width }, | 993 | .{ .top = 0, .left = beside_off, .rows = 4, .cols = beside_width }, |
| 1011 | h.hl(), | 994 | h.hl(), |
| 995 | null, | ||
| 1012 | false, | 996 | false, |
| 1013 | pipe[1], | 997 | pipe[1], |
| 1014 | ); | 998 | ); |