Files
zterm/src/render.zig
Yves Biener feae9fa1a4
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m2s
feat(model): implement Elm architecture
Now the `App` contains a state which is a user-defined `struct` which
is passed to the `handle` and `contents` callbacks for `Container`'s and
`Element`'s. Built-in `Element`'s shall not access the `App.Model` and
should therefore never cause any side-effects.

User-defined events shall be used to act as *messages* to cause
potential side-effects for the model. This is the reason why only
the `handle` callback has a non-const pointer to the `App.Model`. The
`contents` callback can only access the `App.Model` read-only to use for
generating the *view* (in context of the elm architecture).
2025-10-26 15:58:07 +01:00

132 lines
4.5 KiB
Zig

//! 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;