//! Renderer for `zterm`. /// Double-buffered intermediate rendering pipeline pub const Buffered = struct { allocator: Allocator, created: bool, size: Point, screen: []Cell, virtual_screen: []Cell, pub fn init(allocator: Allocator) @This() { return .{ .allocator = allocator, .created = false, .size = undefined, .screen = undefined, .virtual_screen = undefined, }; } pub fn deinit(this: *@This()) void { if (this.created) { this.allocator.free(this.screen); this.allocator.free(this.virtual_screen); } } pub fn resize(this: *@This()) !Point { const size = terminal.getTerminalSize(); if (meta.eql(this.size, size)) return this.size; this.size = size; const n = @as(usize, this.size.x) * @as(usize, this.size.y); if (!this.created) { this.screen = this.allocator.alloc(Cell, n) catch @panic("render.zig: Out of memory."); this.virtual_screen = this.allocator.alloc(Cell, n) catch @panic("render.zig: Out of memory."); @memset(this.virtual_screen, .{}); this.created = true; } else { this.allocator.free(this.screen); this.screen = this.allocator.alloc(Cell, n) catch @panic("render.zig: Out of memory."); this.allocator.free(this.virtual_screen); this.virtual_screen = this.allocator.alloc(Cell, n) catch @panic("render.zig: Out of memory."); @memset(this.virtual_screen, .{}); } try this.clear(); return size; } /// Clear the entire screen and reset the screen buffer, to force a re-draw with the next `flush` call. pub fn clear(this: *@This()) !void { try terminal.clearScreen(); @memset(this.screen, .{}); } /// Render provided cells at size (anchor and dimension) into the *virtual screen*. pub fn render(this: *@This(), comptime Container: type, container: *Container, comptime Model: type, model: *const Model) !void { const size: Point = container.size; const origin: Point = container.origin; const cells: []const Cell = try container.content(model); if (cells.len == 0) return; var idx: usize = 0; var vs = this.virtual_screen; const anchor: usize = (@as(usize, origin.y) * @as(usize, this.size.x)) + @as(usize, origin.x); blk: for (0..size.y) |row| { for (0..size.x) |col| { vs[anchor + (row * this.size.x) + col] = cells[idx]; idx += 1; if (cells.len == idx) break :blk; } } // free immediately container.allocator.free(cells); for (container.elements.items) |*element| try this.render(Container, element, Model, model); } /// Write *virtual screen* to alternate screen (should be called once and last during each render loop iteration in the main loop). pub fn flush(this: *@This()) !void { try terminal.hideCursor(); // TODO measure timings of rendered frames? var cursor_position: ?Point = null; var writer = terminal.writer(); const s = this.screen; const vs = this.virtual_screen; for (0..this.size.y) |row| { for (0..this.size.x) |col| { const idx = (row * this.size.x) + col; const cs = s[idx]; const cvs = vs[idx]; // update the latest found cursor position if (cvs.style.cursor) { assert(cursor_position == null); cursor_position = .{ .x = @truncate(col), .y = @truncate(row), }; } if (cs.eql(cvs)) continue; // render differences found in virtual screen try terminal.setCursorPosition(.{ .y = @truncate(row), .x = @truncate(col) }); try cvs.value(&writer); // update screen to be the virtual screen for the next frame s[idx] = vs[idx]; } } if (cursor_position) |point| { try terminal.showCursor(); try terminal.setCursorPosition(point); } } }; const std = @import("std"); const meta = std.meta; const assert = std.debug.assert; const Allocator = std.mem.Allocator; const terminal = @import("terminal.zig"); const Cell = @import("cell.zig"); const Point = @import("point.zig").Point;