diff --git a/src/layout.zig b/src/layout.zig index 16ff1d0..27495ee 100644 --- a/src/layout.zig +++ b/src/layout.zig @@ -1,5 +1,5 @@ //! Dynamic dispatch for layout implementations. -//! Each layout should at last implement these functions: +//! Each layout should at least implement these functions: //! - handle(this: *@This(), event: Event) anyerror!*std.ArrayList(Event) {} //! - render(this: *@This(), renderer: *Renderer) anyerror!void {} //! - deinit(this: *@This()) void {} diff --git a/src/main.zig b/src/main.zig index a47e6a6..6e174f9 100644 --- a/src/main.zig +++ b/src/main.zig @@ -26,10 +26,30 @@ pub fn main() !void { const allocator = gpa.allocator(); var app: App = .{}; - var renderer: App.Renderer = .{}; + var renderer = App.Renderer.init(allocator); + defer renderer.deinit(); // FIX: when not running fullscreen, the application needs to screen down accordingly to display the contents // -> size hint how much should it use? + // var layout = Layout.createFrom(layout: { + // var stack = Layout.HStack.init(allocator, .{ + // Widget.createFrom(blk: { + // var spacer = Widget.Spacer.init(); + // break :blk &spacer; + // }), + // Widget.createFrom(blk: { + // const file = try std.fs.cwd().openFile("./src/app.zig", .{}); + // defer file.close(); + // var widget = Widget.RawText.init(allocator, file); + // break :blk &widget; + // }), + // Widget.createFrom(blk: { + // var spacer = Widget.Spacer.init(); + // break :blk &spacer; + // }), + // }); + // break :layout &stack; + // }); var layout = Layout.createFrom(layout: { var hstack = Layout.HStack.init(allocator, .{ Widget.createFrom(blk: { @@ -113,6 +133,9 @@ pub fn main() !void { switch (event) { .quit => break, + .resize => |size| { + renderer.resize(size); + }, .key => |key| { // ctrl+c to quit if (Key.matches(key, .{ .cp = 'c', .mod = .{ .ctrl = true } })) { @@ -135,7 +158,6 @@ pub fn main() !void { .err => |err| { log.err("Received {any} with message: {s}", .{ err.err, err.msg }); }, - else => {}, } // NOTE: this currently re-renders the screen for every key-press -> which might be a bit of an overkill const events = try layout.handle(event); @@ -143,5 +165,6 @@ pub fn main() !void { app.postEvent(e); } try layout.render(&renderer); + try renderer.flush(); } } diff --git a/src/render.zig b/src/render.zig index 5d8a9b4..f3cbc75 100644 --- a/src/render.zig +++ b/src/render.zig @@ -1,49 +1,128 @@ //! 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 interchangable 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); +const Contents = std.ArrayList(u8); // TODO: this may contain more than just a single character! (i.e. styled) const Position = terminal.Position; const Size = terminal.Size; pub fn Buffered(comptime _: bool) type { + const log = std.log.scoped(.renderer_buffered); return struct { - refresh: bool = false, size: terminal.Size = undefined, - next_frame: Contents = undefined, frame: Contents = undefined, + const EMPTY: u8 = 0; + pub fn init(allocator: std.mem.Allocator) @This() { + _ = log; return .{ - .next_frame = Contents.init(allocator), .frame = Contents.init(allocator), }; } pub fn deinit(this: *@This()) void { - this.screen.deinit(); + this.frame.deinit(); this.* = undefined; } pub fn resize(this: *@This(), size: Size) void { - // TODO: are there size changes which impact the corresponding rendered content? - // -> can I even be sure nothing needs to be re-rendered? + std.debug.assert(size.anchor.col == 1 and size.anchor.row == 1); this.size = size; - this.refresh = true; + this.frame.resize(size.cols * size.rows) catch @panic("OOM"); } - pub fn render(this: *@This(), content: *Contents) !void { - // TODO: put the corresponding screen to the terminal - // -> determine diff between screen and new content and only update the corresponding characters of the terminal - _ = this; - _ = content; - @panic("Not yet implemented."); + 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; @@ -104,5 +183,9 @@ pub fn Direct(comptime _: bool) type { _ = try terminal.write(contents[idx..]); } } + + pub fn flush(this: @This()) !void { + _ = this; + } }; } diff --git a/src/widget.zig b/src/widget.zig index 40421ea..f5e902c 100644 --- a/src/widget.zig +++ b/src/widget.zig @@ -1,5 +1,5 @@ //! Dynamic dispatch for widget implementations. -//! Each widget should at last implement these functions: +//! Each widget should at least implement these functions: //! - handle(this: *@This(), event: Event) ?Event {} //! - render(this: *@This(), renderer: *Renderer) !void {} //! - deinit(this: *@This()) void {} diff --git a/src/widget/RawText.zig b/src/widget/RawText.zig index 7f4b336..26c3599 100644 --- a/src/widget/RawText.zig +++ b/src/widget/RawText.zig @@ -77,7 +77,6 @@ pub fn Widget(comptime Event: type, comptime Renderer: type) type { pub fn render(this: *@This(), renderer: *Renderer) !void { try renderer.clear(this.size); - try terminal.setCursorPosition(this.size.anchor); if (this.size.rows >= this.line_index.items.len) { try renderer.render(this.size, this.contents.items); } else {