Files
zterm/src/render.zig
Yves Biener bc1bc757d4
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 32s
add(layout/Marging): relative margins for Elements
2024-11-13 18:09:54 +01:00

215 lines
8.6 KiB
Zig

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