render: use different internal model of screen

We use two screens: one which the user provides a slice of bytes for the
graphemes, and the user owns the bytes. We copy those bytes to our
internal model so that we can compare between frames

Signed-off-by: Tim Culverhouse <tim@timculverhouse.com>
This commit is contained in:
Tim Culverhouse 2024-01-22 20:09:35 -06:00
parent 1b7608f469
commit d2f02897dc
5 changed files with 179 additions and 35 deletions

View file

@ -31,7 +31,8 @@ pub fn main() !void {
// We'll adjust the color index every keypress for the border // We'll adjust the color index every keypress for the border
var color_idx: u8 = 0; var color_idx: u8 = 0;
var text_input: TextInput = .{}; var text_input = TextInput.init(alloc);
defer text_input.deinit();
// The main event loop. Vaxis provides a thread safe, blocking, buffered // The main event loop. Vaxis provides a thread safe, blocking, buffered
// queue which can serve as the primary event queue for an application // queue which can serve as the primary event queue for an application
@ -47,7 +48,7 @@ pub fn main() !void {
255 => 0, 255 => 0,
else => color_idx + 1, else => color_idx + 1,
}; };
text_input.update(.{ .key_press = key }); try text_input.update(.{ .key_press = key });
if (key.codepoint == 'c' and key.mods.ctrl) { if (key.codepoint == 'c' and key.mods.ctrl) {
break :outer; break :outer;
} }

73
src/InternalScreen.zig Normal file
View file

@ -0,0 +1,73 @@
const std = @import("std");
const assert = std.debug.assert;
const Style = @import("cell.zig").Style;
const Cell = @import("cell.zig").Cell;
const log = std.log.scoped(.internal_screen);
const InternalScreen = @This();
pub const InternalCell = struct {
char: std.ArrayList(u8) = undefined,
style: Style = .{},
pub fn eql(self: InternalCell, cell: Cell) bool {
return std.mem.eql(u8, self.char.items, cell.char.grapheme) and std.meta.eql(self.style, cell.style);
}
};
width: usize = 0,
height: usize = 0,
buf: []InternalCell = undefined,
cursor_row: usize = 0,
cursor_col: usize = 0,
cursor_vis: bool = false,
/// sets each cell to the default cell
pub fn init(alloc: std.mem.Allocator, w: usize, h: usize) !InternalScreen {
var screen = InternalScreen{};
screen.buf = try alloc.alloc(InternalCell, w * h);
for (screen.buf, 0..) |_, i| {
screen.buf[i] = .{
.char = try std.ArrayList(u8).initCapacity(alloc, 1),
};
}
screen.width = w;
screen.height = h;
return screen;
}
pub fn deinit(self: *InternalScreen, alloc: std.mem.Allocator) void {
for (self.buf, 0..) |_, i| {
self.buf[i].char.deinit();
}
alloc.free(self.buf);
}
/// writes a cell to a location. 0 indexed
pub fn writeCell(
self: *InternalScreen,
col: usize,
row: usize,
char: []const u8,
style: Style,
) void {
if (self.width < col) {
// column out of bounds
return;
}
if (self.height < row) {
// height out of bounds
return;
}
const i = (row * self.width) + col;
assert(i < self.buf.len);
self.buf[i].char.clearRetainingCapacity();
self.buf[i].char.appendSlice(char) catch {
log.warn("couldn't write grapheme", .{});
};
self.buf[i].style = style;
}

View file

@ -16,24 +16,21 @@ cursor_row: usize = 0,
cursor_col: usize = 0, cursor_col: usize = 0,
cursor_vis: bool = false, cursor_vis: bool = false,
/// sets each cell to the default cell pub fn init(alloc: std.mem.Allocator, w: usize, h: usize) !Screen {
pub fn init(self: *Screen) void { var self = Screen{
.buf = try alloc.alloc(Cell, w * h),
.width = w,
.height = h,
};
for (self.buf, 0..) |_, i| { for (self.buf, 0..) |_, i| {
self.buf[i] = .{}; self.buf[i] = .{};
} }
return self;
} }
pub fn deinit(self: *Screen, alloc: std.mem.Allocator) void { pub fn deinit(self: *Screen, alloc: std.mem.Allocator) void {
alloc.free(self.buf); alloc.free(self.buf);
} }
pub fn resize(self: *Screen, alloc: std.mem.Allocator, w: usize, h: usize) !void {
alloc.free(self.buf);
self.buf = try alloc.alloc(Cell, w * h);
self.width = w;
self.height = h;
}
/// writes a cell to a location. 0 indexed /// writes a cell to a location. 0 indexed
pub fn writeCell(self: *Screen, col: usize, row: usize, cell: Cell) void { pub fn writeCell(self: *Screen, col: usize, row: usize, cell: Cell) void {
if (self.width < col) { if (self.width < col) {

View file

@ -6,6 +6,7 @@ const Tty = @import("Tty.zig");
const Winsize = Tty.Winsize; const Winsize = Tty.Winsize;
const Key = @import("Key.zig"); const Key = @import("Key.zig");
const Screen = @import("Screen.zig"); const Screen = @import("Screen.zig");
const InternalScreen = @import("InternalScreen.zig");
const Window = @import("Window.zig"); const Window = @import("Window.zig");
const Options = @import("Options.zig"); const Options = @import("Options.zig");
const Style = @import("cell.zig").Style; const Style = @import("cell.zig").Style;
@ -38,7 +39,7 @@ pub fn Vaxis(comptime T: type) type {
screen: Screen, screen: Screen,
// The last screen we drew. We keep this so we can efficiently update on // The last screen we drew. We keep this so we can efficiently update on
// the next render // the next render
screen_last: Screen, screen_last: InternalScreen = undefined,
alt_screen: bool, alt_screen: bool,
@ -113,11 +114,14 @@ pub fn Vaxis(comptime T: type) type {
/// freed when resizing /// freed when resizing
pub fn resize(self: *Self, alloc: std.mem.Allocator, winsize: Winsize) !void { pub fn resize(self: *Self, alloc: std.mem.Allocator, winsize: Winsize) !void {
log.debug("resizing screen: width={d} height={d}", .{ winsize.cols, winsize.rows }); log.debug("resizing screen: width={d} height={d}", .{ winsize.cols, winsize.rows });
try self.screen.resize(alloc, winsize.cols, winsize.rows); self.screen.deinit(alloc);
self.screen = try Screen.init(alloc, winsize.cols, winsize.rows);
// try self.screen.int(alloc, winsize.cols, winsize.rows);
// we only init our current screen. This has the effect of redrawing // we only init our current screen. This has the effect of redrawing
// every cell // every cell
self.screen.init(); self.screen_last.deinit(alloc);
try self.screen_last.resize(alloc, winsize.cols, winsize.rows); self.screen_last = try InternalScreen.init(alloc, winsize.cols, winsize.rows);
// try self.screen_last.resize(alloc, winsize.cols, winsize.rows);
} }
/// returns a Window comprising of the entire terminal screen /// returns a Window comprising of the entire terminal screen
@ -214,7 +218,7 @@ pub fn Vaxis(comptime T: type) type {
} }
// If cell is the same as our last frame, we don't need to do // If cell is the same as our last frame, we don't need to do
// anything // anything
if (std.meta.eql(cell, self.screen_last.buf[i])) { if (self.screen_last.buf[i].eql(cell)) {
reposition = true; reposition = true;
// Close any osc8 sequence we might be in before // Close any osc8 sequence we might be in before
// repositioning // repositioning
@ -225,7 +229,7 @@ pub fn Vaxis(comptime T: type) type {
} }
defer cursor = cell.style; defer cursor = cell.style;
// Set this cell in the last frame // Set this cell in the last frame
self.screen_last.buf[i] = cell; self.screen_last.writeCell(col, row, cell.char.grapheme, cell.style);
// reposition the cursor, if needed // reposition the cursor, if needed
if (reposition) { if (reposition) {

View file

@ -16,27 +16,43 @@ const Event = union(enum) {
// Index of our cursor // Index of our cursor
cursor_idx: usize = 0, cursor_idx: usize = 0,
grapheme_count: usize = 0,
// the actual line of input buf: std.ArrayList(u8),
buffer: [4096]u8 = undefined,
buffer_idx: usize = 0,
pub fn update(self: *TextInput, event: Event) void { pub fn init(alloc: std.mem.Allocator) TextInput {
return TextInput{
.buf = std.ArrayList(u8).init(alloc),
};
}
pub fn deinit(self: *TextInput) void {
self.buf.deinit();
}
pub fn update(self: *TextInput, event: Event) !void {
switch (event) { switch (event) {
.key_press => |key| { .key_press => |key| {
if (key.text) |text| { if (key.text) |text| {
@memcpy(self.buffer[self.buffer_idx .. self.buffer_idx + text.len], text); try self.buf.insertSlice(self.byteOffsetToCursor(), text);
self.buffer_idx += text.len; self.cursor_idx += 1;
self.cursor_idx += strWidth(text, .full) catch 1; self.grapheme_count += 1;
} }
switch (key.codepoint) { switch (key.codepoint) {
Key.backspace => { Key.backspace => {
// TODO: this only works at the end of the array. Then if (self.cursor_idx == 0) return;
// again, we don't have any means to move the cursor yet // Get the grapheme behind our cursor
// This also doesn't work with graphemes yet self.deleteBeforeCursor();
if (self.buffer_idx == 0) return; },
self.buffer_idx -= 1; Key.delete => {
self.cursor_idx -= 1; if (self.cursor_idx == self.grapheme_count) return;
self.deleteAtCursor();
},
Key.left => {
if (self.cursor_idx > 0) self.cursor_idx -= 1;
},
Key.right => {
if (self.cursor_idx < self.grapheme_count) self.cursor_idx += 1;
}, },
else => {}, else => {},
} }
@ -45,11 +61,12 @@ pub fn update(self: *TextInput, event: Event) void {
} }
pub fn draw(self: *TextInput, win: Window) void { pub fn draw(self: *TextInput, win: Window) void {
const input = self.buffer[0..self.buffer_idx]; var iter = GraphemeIterator.init(self.buf.items);
var iter = GraphemeIterator.init(input);
var col: usize = 0; var col: usize = 0;
var i: usize = 0;
var cursor_idx: usize = 0;
while (iter.next()) |grapheme| { while (iter.next()) |grapheme| {
const g = grapheme.slice(input); const g = grapheme.slice(self.buf.items);
const w = strWidth(g, .full) catch 1; const w = strWidth(g, .full) catch 1;
win.writeCell(col, 0, .{ win.writeCell(col, 0, .{
.char = .{ .char = .{
@ -58,6 +75,58 @@ pub fn draw(self: *TextInput, win: Window) void {
}, },
}); });
col += w; col += w;
i += 1;
if (i == self.cursor_idx) cursor_idx = col;
}
win.showCursor(cursor_idx, 0);
}
// returns the number of bytes before the cursor
fn byteOffsetToCursor(self: TextInput) usize {
var iter = GraphemeIterator.init(self.buf.items);
var offset: usize = 0;
var i: usize = 0;
while (iter.next()) |grapheme| {
if (i == self.cursor_idx) break;
offset += grapheme.len;
i += 1;
}
return offset;
}
fn deleteBeforeCursor(self: *TextInput) void {
var iter = GraphemeIterator.init(self.buf.items);
var offset: usize = 0;
var i: usize = 1;
while (iter.next()) |grapheme| {
if (i == self.cursor_idx) {
var j: usize = 0;
while (j < grapheme.len) : (j += 1) {
_ = self.buf.orderedRemove(offset);
}
self.cursor_idx -= 1;
self.grapheme_count -= 1;
return;
}
offset += grapheme.len;
i += 1;
}
}
fn deleteAtCursor(self: *TextInput) void {
var iter = GraphemeIterator.init(self.buf.items);
var offset: usize = 0;
var i: usize = 1;
while (iter.next()) |grapheme| {
if (i == self.cursor_idx + 1) {
var j: usize = 0;
while (j < grapheme.len) : (j += 1) {
_ = self.buf.orderedRemove(offset);
}
self.grapheme_count -= 1;
return;
}
offset += grapheme.len;
i += 1;
} }
win.showCursor(self.cursor_idx, 0);
} }