From 3decc541a9ccaef8f93bf5bd45dddd7feff6a943 Mon Sep 17 00:00:00 2001 From: Yves Biener Date: Thu, 30 Jan 2025 20:53:01 +0100 Subject: [PATCH] mod(renderer): initial version of double buffer intermediate renderer This branch will implement the necessary changes for the widgets and their implementations to use the new renderer correctly. --- examples/stack.zig | 27 ++++--- examples/tui.zig | 74 ++++++++---------- src/layout/Framing.zig | 24 +++--- src/render.zig | 168 ++++++++++++++++++++++------------------- src/terminal.zig | 6 +- src/terminal/Cell.zig | 58 +++----------- src/terminal/Size.zig | 2 +- src/terminal/Style.zig | 24 ++++-- src/widget/Text.zig | 26 ++----- 9 files changed, 184 insertions(+), 225 deletions(-) diff --git a/examples/stack.zig b/examples/stack.zig index 62077b9..b5ff52a 100644 --- a/examples/stack.zig +++ b/examples/stack.zig @@ -3,7 +3,7 @@ const zterm = @import("zterm"); const App = zterm.App( union(enum) {}, - zterm.Renderer.Direct, + zterm.Renderer.Buffered, true, ); const Key = zterm.Key; @@ -26,7 +26,8 @@ pub fn main() !void { const allocator = gpa.allocator(); var app: App = .{}; - var renderer: App.Renderer = .{}; + var renderer = App.Renderer.init(allocator); + defer renderer.deinit(); // TODO: when not running fullscreen, the application needs to screen down accordingly to display the contents // -> size hint how much should it use? @@ -75,17 +76,19 @@ pub fn main() !void { }, .{ .layout = Layout.createFrom(Layout.VStack.init(allocator, .{ - Widget.createFrom(blk: { - const file = try std.fs.cwd().openFile("./examples/stack.zig", .{}); - defer file.close(); - break :blk Widget.RawText.init(allocator, file); - }), + // Widget.createFrom(blk: { + // const file = try std.fs.cwd().openFile("./examples/stack.zig", .{}); + // defer file.close(); + // break :blk Widget.RawText.init(allocator, file); + // }), Widget.createFrom(Widget.Spacer.init(allocator)), - Widget.createFrom(blk: { - const file = try std.fs.cwd().openFile("./examples/stack.zig", .{}); - defer file.close(); - break :blk Widget.RawText.init(allocator, file); - }), + Widget.createFrom(Widget.Spacer.init(allocator)), + Widget.createFrom(Widget.Spacer.init(allocator)), + // Widget.createFrom(blk: { + // const file = try std.fs.cwd().openFile("./examples/stack.zig", .{}); + // defer file.close(); + // break :blk Widget.RawText.init(allocator, file); + // }), })), }, ), diff --git a/examples/tui.zig b/examples/tui.zig index b3211d1..0ca5886 100644 --- a/examples/tui.zig +++ b/examples/tui.zig @@ -7,7 +7,7 @@ const App = zterm.App( tui, // view instance to the corresponding view for 'tui' }, }, - zterm.Renderer.Direct, + zterm.Renderer.Buffered, true, ); const Cell = zterm.Cell; @@ -24,49 +24,26 @@ const Tui = struct { pub fn init(allocator: std.mem.Allocator) *Tui { var tui = allocator.create(Tui) catch @panic("Out of memory: tui.zig"); tui.allocator = allocator; - // FIXME: the layout creates an 'incorrect alignment'? - tui.layout = Layout.createFrom(Layout.VContainer.init(allocator, .{ - .{ - Layout.createFrom(Layout.Framing.init(allocator, .{ - .title = .{ - .str = "Welcome to my terminal website", - .style = .{ - .ul = .{ .index = 6 }, - .ul_style = .single, - }, - }, - }, .{ - .layout = Layout.createFrom(Layout.HContainer.init(allocator, .{ - .{ - Widget.createFrom(Widget.Text.init(allocator, .left, &[1]Cell{ - .{ .content = "Yves Biener", .style = .{ .bold = true } }, - })), - 25, - }, - .{ - Widget.createFrom(Widget.Text.init(allocator, .left, &[1]Cell{ - .{ .content = "File name", .style = .{ .bold = true } }, - })), - 50, - }, - .{ - Widget.createFrom(Widget.Text.init(allocator, .left, &[1]Cell{ - .{ .content = "Contacts", .style = .{ .bold = true } }, - })), - 25, - }, - })), + tui.layout = Layout.createFrom(Layout.VStack.init(allocator, .{ + Layout.createFrom(Layout.HStack.init(allocator, .{ + Widget.createFrom(Widget.Text.init(allocator, .left, &[1]Cell{ + .{ .rune = 'Y', .style = .{ .fg = .{ .index = 6 }, .bold = true } }, })), - 10, - }, - .{ - Layout.createFrom(Layout.Margin.init(allocator, .{ .left = 15, .right = 15 }, .{ - .widget = Widget.createFrom(Widget.Text.init(allocator, .default, &[1]Cell{ - .{ .content = "Does this change anything", .style = .{ .ul = .default, .ul_style = .single } }, - })), + Widget.createFrom(Widget.Text.init(allocator, .left, &[1]Cell{ + .{ .rune = 'F', .style = .{ .fg = .{ .index = 6 }, .bold = true } }, })), - 90, - }, + Widget.createFrom(Widget.Text.init(allocator, .left, &[1]Cell{ + .{ .rune = 'C', .style = .{ .fg = .{ .index = 6 }, .bold = true } }, + })), + })), + // .{ + // Layout.createFrom(Layout.Margin.init(allocator, .{ .left = 15, .right = 15 }, .{ + // .widget = Widget.createFrom(Widget.Text.init(allocator, .default, &[1]Cell{ + // .{ .rune = 'D', .style = .{ .ul = .default, .ul_style = .single } }, + // })), + // })), + // 90, + // }, })); return tui; } @@ -76,6 +53,14 @@ const Tui = struct { this.allocator.destroy(this); } + pub fn enable(this: *Tui) void { + _ = this; + } + + pub fn disable(this: *Tui) void { + _ = this; + } + pub fn handle(this: *Tui, event: App.Event) !*Events { return try this.layout.handle(event); } @@ -99,7 +84,9 @@ pub fn main() !void { const allocator = arena.allocator(); var app: App = .{}; - var renderer: App.Renderer = .{}; + var renderer = App.Renderer.init(allocator); + defer renderer.deinit(); + var view: View = undefined; var tui_view = View.createFrom(Tui.init(allocator)); @@ -147,5 +134,6 @@ pub fn main() !void { app.postEvent(e); } try view.render(&renderer); + try renderer.flush(); } } diff --git a/src/layout/Framing.zig b/src/layout/Framing.zig index cad9bf1..3885e0f 100644 --- a/src/layout/Framing.zig +++ b/src/layout/Framing.zig @@ -126,8 +126,8 @@ pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: t return &this.events; } - const round_frame = .{ "╭", "─", "╮", "│", "╰", "╯" }; - const square_frame = .{ "┌", "─", "┐", "│", "└", "┘" }; + const round_frame: [6][]const u8 = .{ "╭", "─", "╮", "│", "╰", "╯" }; + const square_frame: [6][]const u8 = .{ "┌", "─", "┐", "│", "└", "┘" }; fn renderFrame(this: *@This(), renderer: *Renderer) !void { // FIXME: use renderer instead! @@ -140,14 +140,14 @@ pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: t // render top: +---+ try terminal.setCursorPosition(this.size.anchor); const writer = terminal.writer(); - try this.config.style.value(writer, frame[0]); - if (this.config.title.str.len > 0) { - try this.config.title.style.value(writer, this.config.title.str); + // try this.config.style.value(writer, frame[0]); + for (0..this.config.title.str.len) |i| { + try this.config.title.style.value(writer, this.config.title.str[i]); } for (0..this.size.cols -| 2 -| this.config.title.str.len) |_| { - try this.config.style.value(writer, frame[1]); + // try this.config.style.value(writer, frame[1]); } - try this.config.style.value(writer, frame[2]); + // try this.config.style.value(writer, frame[2]); // render left: | for (1..this.size.rows -| 1) |r| { const row: u16 = @truncate(r); @@ -155,7 +155,7 @@ pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: t .col = this.size.anchor.col, .row = this.size.anchor.row + row, }); - try this.config.style.value(writer, frame[3]); + // try this.config.style.value(writer, frame[3]); } // render right: | for (1..this.size.rows -| 1) |r| { @@ -164,18 +164,18 @@ pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: t .col = this.size.anchor.col + this.size.cols -| 1, .row = this.size.anchor.row + row, }); - try this.config.style.value(writer, frame[3]); + // try this.config.style.value(writer, frame[3]); } // render bottom: +---+ try terminal.setCursorPosition(.{ .col = this.size.anchor.col, .row = this.size.anchor.row + this.size.rows - 1, }); - try this.config.style.value(writer, frame[4]); + // try this.config.style.value(writer, frame[4]); for (0..this.size.cols -| 2) |_| { - try this.config.style.value(writer, frame[1]); + // try this.config.style.value(writer, frame[1]); } - try this.config.style.value(writer, frame[5]); + // try this.config.style.value(writer, frame[5]); } pub fn render(this: *@This(), renderer: *Renderer) !void { diff --git a/src/render.zig b/src/render.zig index efc6e92..729fc18 100644 --- a/src/render.zig +++ b/src/render.zig @@ -10,97 +10,107 @@ const std = @import("std"); const terminal = @import("terminal.zig"); -const Cells = []const terminal.Cell; +const Cell = terminal.Cell; const Position = terminal.Position; const Size = terminal.Size; -pub fn Direct(comptime fullscreen: bool) type { - const log = std.log.scoped(.renderer_direct); - _ = log; +/// Double-buffered intermediate rendering pipeline +pub fn Buffered(comptime fullscreen: bool) type { + const log = std.log.scoped(.renderer_buffered); + // _ = log; _ = fullscreen; return struct { - size: Size = undefined, + allocator: std.mem.Allocator, + created: bool, + size: Size, + screen: []Cell, + virtual_screen: []Cell, - pub fn resize(this: *@This(), size: Size) void { - this.size = size; + pub fn init(allocator: std.mem.Allocator) @This() { + return .{ + .allocator = allocator, + .created = false, + .size = undefined, + .screen = undefined, + .virtual_screen = undefined, + }; } - pub fn clear(this: *@This(), size: Size) !void { - _ = this; - // NOTE: clear on the entire screen may introduce too much overhead and could instead clear the entire screen instead. - // - it could also then try to optimize for further *clear* calls, that result in pretty much a nop? -> how to identify those clear calls? - // 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 deinit(this: *@This()) void { + if (this.created) { + this.allocator.free(this.screen); + this.allocator.free(this.virtual_screen); } } - 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; + pub fn resize(this: *@This(), size: Size) void { + this.size = size; + if (!this.created) { + this.screen = this.allocator.alloc(Cell, size.cols * size.rows) catch @panic("render.zig: Out of memory."); + this.virtual_screen = this.allocator.alloc(Cell, size.cols * size.rows) catch @panic("render.zig: Out of memory."); + this.created = true; + return; + } + if (this.allocator.resize(this.screen, size.cols * size.rows)) { + @panic("render.zig: Could not resize `screen` buffer"); + } + if (this.allocator.resize(this.virtual_screen, size.cols * size.rows)) { + @panic("render.zig: Could not resize `virtual screen` buffer."); + } + } + + pub fn clear(this: *@This(), size: Size) !void { + log.debug("renderer::clear", .{}); + var vs = this.virtual_screen; + const anchor = (size.anchor.row * this.size.rows) + size.anchor.col; + for (0..size.rows) |row| { + for (0..size.cols) |col| { + vs[anchor + (row * this.size.cols) + col].reset(); + } + } + } + + /// Render provided cells at size (anchor and dimension) into the *virtual screen*. + pub fn render(this: *@This(), size: Size, cells: []const Cell) !void { + log.debug("renderer:render: cells: {any}", .{cells}); + std.debug.assert(cells.len > 0); + + var idx: usize = 0; + var vs = this.virtual_screen; + const anchor = (size.anchor.row * this.size.rows) + size.anchor.col; + + for (0..size.rows) |row| { + for (0..size.cols) |col| { + const cell = cells[idx]; + idx += 1; + + vs[anchor + (row * this.size.cols) + col].style = cell.style; + vs[anchor + (row * this.size.cols) + col].rune = cell.rune; + + if (cells.len == idx) return; + } + } + } + + /// 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 { + log.debug("renderer::flush", .{}); 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 - } + const s = this.screen; + const vs = this.virtual_screen; + for (0..this.size.rows) |row| { + for (0..this.size.cols) |col| { + const idx = (row * this.size.cols) + col; + const cs = s[idx]; + const cvs = vs[idx]; + if (cs.eql(cvs)) + continue; + // render differences found in virtual screen + // TODO: improve the writing speed (many unecessary writes (i.e. the style for every character..)) + try terminal.setCursorPosition(.{ .row = @truncate(row), .col = @truncate(col) }); + try cvs.value(writer); + // update screen to be the virtual screen for the next frame + s[idx] = vs[idx]; } } } diff --git a/src/terminal.zig b/src/terminal.zig index 9a14480..9ca3710 100644 --- a/src/terminal.zig +++ b/src/terminal.zig @@ -20,7 +20,7 @@ pub const ReportMode = enum { pub fn getTerminalSize() Size { var ws: std.posix.winsize = undefined; _ = std.posix.system.ioctl(std.posix.STDIN_FILENO, std.posix.T.IOCGWINSZ, @intFromPtr(&ws)); - return .{ .cols = ws.col, .rows = ws.row }; + return .{ .cols = ws.col - 1, .rows = ws.row - 1 }; } pub fn saveScreen() !void { @@ -126,8 +126,8 @@ pub fn getCursorPosition() !Position { } return .{ - .row = try std.fmt.parseInt(u16, row[0..ridx], 10), - .col = try std.fmt.parseInt(u16, col[0..cidx], 10), + .row = try std.fmt.parseInt(u16, row[0..ridx], 10) - 1, + .col = try std.fmt.parseInt(u16, col[0..cidx], 10) - 1, }; } diff --git a/src/terminal/Cell.zig b/src/terminal/Cell.zig index 5bc726c..2ff7327 100644 --- a/src/terminal/Cell.zig +++ b/src/terminal/Cell.zig @@ -2,60 +2,24 @@ const std = @import("std"); pub const Style = @import("Style.zig"); style: Style = .{}, -content: []const u8 = undefined, +rune: u8 = ' ', -pub const Result = struct { - idx: usize, - newline: bool, +pub const Character = struct { + grapheme: []const u8, + width: u8, }; -pub fn len(this: @This(), start: usize) usize { - std.debug.assert(this.content.len > start); - return this.content[start..].len; +pub fn eql(this: @This(), other: @This()) bool { + return this.rune == other.rune and this.style.eql(other.style); } -pub fn write(this: @This(), writer: anytype, start: usize, end: usize) !void { - std.debug.assert(this.content.len > start); - std.debug.assert(this.content.len >= end); - std.debug.assert(start < end); - - try this.style.value(writer, this.content[start..end]); +pub fn reset(this: *@This()) void { + this.style = .{ .fg = .default, .bg = .default, .ul = .default, .ul_style = .off }; + this.rune = ' '; } -pub fn writeUpToNewline(this: @This(), writer: anytype, start: usize, end: usize) !Result { - std.debug.assert(this.content.len > start); - std.debug.assert(this.content.len >= end); - std.debug.assert(start < end); - - for (start..end) |i| { - if (this.content[i] == '\n') { - if (start < i) { - // this is just an empty line with a newline - try this.value(writer, start, i); - } - return .{ - .idx = i, - .newline = true, - }; - } - } - try this.write(writer, start, end); - return .{ - .idx = end, - .newline = false, - }; -} - -pub fn value(this: @This(), writer: anytype, start: usize, end: usize) !void { - std.debug.assert(start < this.content.len); - std.debug.assert(this.content.len >= end); - std.debug.assert(start < end); - try this.style.value(writer, this.content[start..end]); -} - -// not really supported -pub fn format(this: @This(), writer: anytype, comptime fmt: []const u8, args: anytype) !void { - try this.style.format(writer, fmt, args); // NOTE: args should contain this.content[start..end] or this.content +pub fn value(this: @This(), writer: anytype) !void { + try this.style.value(writer, this.rune); } test { diff --git a/src/terminal/Size.zig b/src/terminal/Size.zig index c492048..eb2b18f 100644 --- a/src/terminal/Size.zig +++ b/src/terminal/Size.zig @@ -1,5 +1,5 @@ const Position = @import("Position.zig"); -anchor: Position = .{ .col = 1, .row = 1 }, // top left corner by default +anchor: Position = .{ .col = 0, .row = 0 }, // top left corner by default cols: u16, rows: u16, diff --git a/src/terminal/Style.zig b/src/terminal/Style.zig index 7f739bb..6538d77 100644 --- a/src/terminal/Style.zig +++ b/src/terminal/Style.zig @@ -113,6 +113,20 @@ reverse: bool = false, invisible: bool = false, strikethrough: bool = false, +pub fn eql(this: @This(), other: @This()) bool { + return this.fg.eql(other.fg) and + this.bg.eql(other.bg) and + this.ul.eql(other.ul) and + other.ul_style == this.ul_style and + other.bold == this.bold and + other.dim == this.dim and + other.italic == this.italic and + other.blink == this.blink and + other.reverse == this.reverse and + other.invisible == this.invisible and + other.strikethrough == this.strikethrough; +} + /// Merge _other_ `Style` to _this_ style and overwrite _this_ `Style`'s value /// if the _other_ value differs from the default value. pub fn merge(this: *@This(), other: @This()) void { @@ -284,15 +298,9 @@ fn end(this: @This(), writer: anytype) !void { } } -pub fn format(this: @This(), writer: anytype, comptime content: []const u8, args: anytype) !void { +pub fn value(this: @This(), writer: anytype, content: u8) !void { try this.start(writer); - try std.fmt.format(writer, content, args); - try this.end(writer); -} - -pub fn value(this: @This(), writer: anytype, content: []const u8) !void { - try this.start(writer); - _ = try writer.write(content); + _ = try writer.write(&[_]u8{content}); try this.end(writer); } diff --git a/src/widget/Text.zig b/src/widget/Text.zig index 33465db..30cefc2 100644 --- a/src/widget/Text.zig +++ b/src/widget/Text.zig @@ -62,10 +62,7 @@ pub fn Widget(comptime Event: type, comptime Renderer: type) type { switch (this.alignment) { .default => break :blk this.size, .center => { - var length_usize: usize = 0; - for (this.contents) |content| { - length_usize += content.content.len; - } + const length_usize = this.contents.len; const length: u16 = @truncate(length_usize); const cols = @min(length, this.size.cols); const rows = cols / length; @@ -79,10 +76,7 @@ pub fn Widget(comptime Event: type, comptime Renderer: type) type { }; }, .top => { - var length_usize: usize = 0; - for (this.contents) |content| { - length_usize += content.content.len; - } + const length_usize = this.contents.len; const length: u16 = @truncate(length_usize); const cols = @min(length, this.size.cols); const rows = cols / length; @@ -96,10 +90,7 @@ pub fn Widget(comptime Event: type, comptime Renderer: type) type { }; }, .bottom => { - var length_usize: usize = 0; - for (this.contents) |content| { - length_usize += content.content.len; - } + const length_usize = this.contents.len; const length: u16 = @truncate(length_usize); const cols = @min(length, this.size.cols); const rows = cols / length; @@ -113,10 +104,7 @@ pub fn Widget(comptime Event: type, comptime Renderer: type) type { }; }, .left => { - var length_usize: usize = 0; - for (this.contents) |content| { - length_usize += content.content.len; - } + const length_usize = this.contents.len; const length: u16 = @truncate(length_usize); const cols = @min(length, this.size.cols); const rows = cols / length; @@ -130,10 +118,7 @@ pub fn Widget(comptime Event: type, comptime Renderer: type) type { }; }, .right => { - var length_usize: usize = 0; - for (this.contents) |content| { - length_usize += content.content.len; - } + const length_usize = this.contents.len; const length: u16 = @truncate(length_usize); const cols = @min(length, this.size.cols); const rows = cols / length; @@ -148,6 +133,7 @@ pub fn Widget(comptime Event: type, comptime Renderer: type) type { }, } }; + log.debug("Text.contents: {any}", .{this.contents}); try renderer.render(size, this.contents); this.require_render = false; }