intermediate #1

Merged
yves-biener merged 31 commits from intermediate into main 2025-02-16 16:02:59 +01:00
9 changed files with 184 additions and 225 deletions
Showing only changes of commit 3decc541a9 - Show all commits

View File

@@ -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);
// }),
})),
},
),

View File

@@ -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();
}
}

View File

@@ -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 {

View File

@@ -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];
}
}
}

View File

@@ -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,
};
}

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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);
}

View File

@@ -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;
}