From 07e4819ecda37a611d56928e7973e1bc23bc84a3 Mon Sep 17 00:00:00 2001 From: Yves Biener Date: Tue, 12 Nov 2024 19:11:19 +0100 Subject: [PATCH] feat(renderer): render cells instead of raw u8 array contents --- src/layout/Framing.zig | 2 +- src/render.zig | 77 +++++++++++++++++++++++++----------------- src/terminal.zig | 4 +-- src/terminal/Cell.zig | 63 ++++++++++++++++++++++++++++++++++ src/widget/RawText.zig | 12 +++++-- src/zterm.zig | 2 +- 6 files changed, 122 insertions(+), 38 deletions(-) create mode 100644 src/terminal/Cell.zig diff --git a/src/layout/Framing.zig b/src/layout/Framing.zig index e0bc6a2..ff61031 100644 --- a/src/layout/Framing.zig +++ b/src/layout/Framing.zig @@ -8,7 +8,7 @@ const terminal = @import("../terminal.zig"); const isTaggedUnion = @import("../event.zig").isTaggedUnion; const Error = @import("../event.zig").Error; const Key = terminal.Key; -const Style = terminal.Style; +const Style = terminal.Cell.Style; const log = std.log.scoped(.layout_framing); diff --git a/src/render.zig b/src/render.zig index 45a122b..de62c8d 100644 --- a/src/render.zig +++ b/src/render.zig @@ -14,6 +14,7 @@ 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; @@ -136,16 +137,18 @@ pub fn Direct(comptime _: bool) type { } } - pub fn render(this: *@This(), size: Size, contents: []u8) !void { + pub fn render(this: *@This(), size: Size, cells: Cells) !void { _ = this; try terminal.setCursorPosition(size.anchor); var row: u16 = 0; - var idx: usize = 0; - var skip_next_line = false; - for (contents, 0..) |item, i| { - if (item == '\n') { // do not print newlines - if (!skip_next_line) { - _ = try terminal.write(contents[idx..i]); // does not include '\n' at position _i_ + 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 @@ -154,34 +157,46 @@ pub fn Direct(comptime _: bool) type { .col = size.anchor.col, .row = size.anchor.row + row, }); - } - skip_next_line = false; - idx = i + 1; // skip over '\n' - continue; - } - if (i - idx == size.cols) { - // FIXME: this will introduce another additional line which is not accounted for and will cut off these lines in the end from rendering - // -> *current solution*: cut of the line and skip that remaining content (not sure if that should be done) - // - however the widget has actually knowledge about the size and could change the reported contents which fit to the size or simply don't care and leave it to the renderer? - // flush line - if (!skip_next_line) { - _ = try terminal.write(contents[idx..i]); - row += 1; - if (row >= size.rows) { - return; // we are done + 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 } - try terminal.setCursorPosition(.{ - .col = size.anchor.col, - .row = size.anchor.row + row, - }); } - skip_next_line = true; - idx = i; + // written all cell contents + if (idx >= cell.content.len) { + break; // go to next cell + } } } - if (idx < contents.len) { - _ = try terminal.write(contents[idx..]); - } } pub fn flush(this: @This()) !void { diff --git a/src/terminal.zig b/src/terminal.zig index 0ba219c..077600d 100644 --- a/src/terminal.zig +++ b/src/terminal.zig @@ -2,7 +2,7 @@ const std = @import("std"); pub const Key = @import("terminal/Key.zig"); pub const Size = @import("terminal/Size.zig"); pub const Position = @import("terminal/Position.zig"); -pub const Style = @import("terminal/Style.zig"); +pub const Cell = @import("terminal/Cell.zig"); pub const code_point = @import("code_point"); const log = std.log.scoped(.terminal); @@ -229,5 +229,5 @@ fn getReportMode(ps: u8) ReportMode { } test { - _ = Style; + _ = Cell; } diff --git a/src/terminal/Cell.zig b/src/terminal/Cell.zig new file mode 100644 index 0000000..b6d11d0 --- /dev/null +++ b/src/terminal/Cell.zig @@ -0,0 +1,63 @@ +const std = @import("std"); +pub const Style = @import("Style.zig"); + +style: Style = .{}, +content: []u8 = undefined, + +pub const Result = struct { + idx: usize, + newline: bool, +}; + +pub fn len(this: @This(), start: usize) usize { + std.debug.assert(this.content.len > start); + return this.content[start..].len; +} + +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 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 +} + +test { + _ = Style; +} diff --git a/src/widget/RawText.zig b/src/widget/RawText.zig index 26c3599..1324729 100644 --- a/src/widget/RawText.zig +++ b/src/widget/RawText.zig @@ -78,17 +78,23 @@ pub fn Widget(comptime Event: type, comptime Renderer: type) type { pub fn render(this: *@This(), renderer: *Renderer) !void { try renderer.clear(this.size); if (this.size.rows >= this.line_index.items.len) { - try renderer.render(this.size, this.contents.items); + try renderer.render(this.size, &[_]terminal.Cell{ + .{ .content = this.contents.items, .style = .{ .dim = true, .fg = .{ .index = 8 } } }, + }); } else { // more rows than we can display const i = this.line_index.items[this.line]; const e = this.size.rows + this.line; if (e > this.line_index.items.len) { - try renderer.render(this.size, this.contents.items[i..]); + try renderer.render(this.size, &[_]terminal.Cell{ + .{ .content = this.contents.items[i..], .style = .{ .dim = true, .fg = .{ .index = 7 } } }, + }); return; } const x = this.line_index.items[e]; - try renderer.render(this.size, this.contents.items[i..x]); + try renderer.render(this.size, &[_]terminal.Cell{ + .{ .content = this.contents.items[i..x], .style = .{ .dim = true, .fg = .{ .index = 9 } } }, + }); } } }; diff --git a/src/zterm.zig b/src/zterm.zig index 13b85c6..a5707ca 100644 --- a/src/zterm.zig +++ b/src/zterm.zig @@ -8,7 +8,7 @@ pub const Renderer = @import("render.zig"); pub const Key = terminal.Key; pub const Position = terminal.Position; pub const Size = terminal.Size; -pub const Style = terminal.Style; +pub const Cell = terminal.Cell; test { _ = @import("terminal.zig");