//! Renderer which holds the screen to compare with the previous screen for efficient rendering. //! Each renderer should at least implement these functions: //! - init(allocator: std.mem.Allocator) @This() {} //! - deinit(this: *@This()) void {} //! - resize(this: *@This(), size: Size) void {} //! - clear(this: *@This(), size: Size) !void {} //! - render(this: *@This(), size: Size, contents: []u8) !void {} //! - flush(this: @This()) !void {} //! //! Each `Renderer` should be able to be used interchangeable without having to //! change any code of any `Layout` or `Widget`. The only change should be the //! passed type to `zterm.App` _R_ parameter. const std = @import("std"); const terminal = @import("terminal.zig"); const Contents = std.ArrayList(u8); // TODO: this may contain more than just a single character! (i.e. styled) const Cells = []const terminal.Cell; const Position = terminal.Position; const Size = terminal.Size; pub fn Buffered(comptime _: bool) type { const log = std.log.scoped(.renderer_buffered); return struct { size: terminal.Size = undefined, frame: Contents = undefined, const EMPTY: u8 = 0; pub fn init(allocator: std.mem.Allocator) @This() { _ = log; return .{ .frame = Contents.init(allocator), }; } pub fn deinit(this: *@This()) void { this.frame.deinit(); this.* = undefined; } pub fn resize(this: *@This(), size: Size) void { std.debug.assert(size.anchor.col == 1 and size.anchor.row == 1); this.size = size; this.frame.resize(size.cols * size.rows) catch @panic("OOM"); } pub fn clear(this: *@This(), size: Size) error{}!void { for (size.anchor.row..size.anchor.row + size.rows) |row| { const y = this.size.cols * (row - 1); for (size.anchor.col..size.anchor.col + size.cols) |col| { // log.debug("clearing index[{d}]/[{d}] at .{{ .col = {d}, .row = {d} }}", .{ y + col - 1, this.size.cols * this.size.rows, col, row }); std.debug.assert(y + col - 1 < this.frame.items.len); this.frame.items[y + col - 1] = EMPTY; } } } pub fn render(this: *@This(), size: Size, contents: []u8) !void { var c_idx: usize = 0; row: for (size.anchor.row..size.anchor.row + size.rows) |row| { var fill_empty = false; const y = this.size.cols * (row - 1); for (size.anchor.col..size.anchor.col + size.cols) |col| { std.debug.assert(y + col - 1 < this.frame.items.len); const idx = y + col - 1; const item = contents[c_idx]; if (item == '\n') { // do not print newlines // reached end of line // fill rest of col's of this row with EMPTY fill_empty = true; } if (fill_empty) { this.frame.items[idx] = EMPTY; } else { this.frame.items[idx] = item; c_idx += 1; } } // printed row // are there still items for this row (even though there is no space left?) if (!fill_empty) { // find end of line and update c_idx accordingly for (c_idx..contents.len) |c| { if (contents[c] == '\n') { c_idx = c + 1; continue :row; } } } else { c_idx += 1; // skip over '\n' } } } pub fn flush(this: @This()) !void { try terminal.clearScreen(); for (this.size.anchor.row..this.size.anchor.row + this.size.rows) |row| { const y = this.size.cols * (row - 1); for (1..this.size.cols) |col| { std.debug.assert(y + col - 1 < this.frame.items.len); const item = this.frame.items[y + col - 1]; if (item == EMPTY) { continue; } const pos: Position = .{ .col = @truncate(col), .row = @truncate(row) }; try terminal.setCursorPosition(pos); _ = try terminal.write(&.{item}); } } } }; } pub fn Direct(comptime _: bool) type { const log = std.log.scoped(.renderer_direct); return struct { pub fn init(allocator: std.mem.Allocator) @This() { _ = allocator; _ = log; return .{}; } pub fn deinit(this: *@This()) void { this.* = undefined; } pub fn resize(this: *@This(), size: Size) void { _ = this; _ = size; } pub fn clear(this: *@This(), size: Size) !void { _ = this; // TODO: this should instead by dynamic and correct of size (terminal could be too large currently) std.debug.assert(1028 > size.cols); var buf: [1028]u8 = undefined; @memset(buf[0..], ' '); for (0..size.rows) |r| { const row: u16 = @truncate(r); try terminal.setCursorPosition(.{ .col = size.anchor.col, .row = size.anchor.row + row, }); _ = try terminal.write(buf[0..size.cols]); } } pub fn render(this: *@This(), size: Size, cells: Cells) !void { _ = this; try terminal.setCursorPosition(size.anchor); var row: u16 = 0; var remaining_cols = size.cols; const writer = terminal.writer(); for (cells) |cell| { var idx: usize = 0; print_cell: while (true) { const cell_len = cell.len(idx); if (cell_len > remaining_cols) { const result = try cell.writeUpToNewline(writer, idx, idx + remaining_cols); row += 1; if (row >= size.rows) { return; // we are done } try terminal.setCursorPosition(.{ .col = size.anchor.col, .row = size.anchor.row + row, }); remaining_cols = size.cols; idx = result.idx; if (result.newline) { idx += 1; // skip over newline } else { // there is still content to the newline (which will not be printed) for (idx..cell.content.len) |i| { if (cell.content[i] == '\n') { idx = i + 1; continue :print_cell; } } break; // go to next cell (as we went to the end of the cell and do not print on the next line) } } else { // print rest of cell const result = try cell.writeUpToNewline(writer, idx, idx + cell_len); if (result.newline) { row += 1; if (row >= size.rows) { return; // we are done } try terminal.setCursorPosition(.{ .col = size.anchor.col, .row = size.anchor.row + row, }); remaining_cols = size.cols; idx = result.idx + 1; // skip over newline } else { remaining_cols -= @truncate(cell_len - idx); idx = 0; break; // go to next cell } } // written all cell contents if (idx >= cell.content.len) { break; // go to next cell } } } } pub fn flush(this: @This()) !void { _ = this; } }; }