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
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
// queue which can serve as the primary event queue for an application
@ -47,7 +48,7 @@ pub fn main() !void {
255 => 0,
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) {
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_vis: bool = false,
/// sets each cell to the default cell
pub fn init(self: *Screen) void {
pub fn init(alloc: std.mem.Allocator, w: usize, h: usize) !Screen {
var self = Screen{
.buf = try alloc.alloc(Cell, w * h),
.width = w,
.height = h,
};
for (self.buf, 0..) |_, i| {
self.buf[i] = .{};
}
return self;
}
pub fn deinit(self: *Screen, alloc: std.mem.Allocator) void {
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
pub fn writeCell(self: *Screen, col: usize, row: usize, cell: Cell) void {
if (self.width < col) {

View file

@ -6,6 +6,7 @@ const Tty = @import("Tty.zig");
const Winsize = Tty.Winsize;
const Key = @import("Key.zig");
const Screen = @import("Screen.zig");
const InternalScreen = @import("InternalScreen.zig");
const Window = @import("Window.zig");
const Options = @import("Options.zig");
const Style = @import("cell.zig").Style;
@ -38,7 +39,7 @@ pub fn Vaxis(comptime T: type) type {
screen: Screen,
// The last screen we drew. We keep this so we can efficiently update on
// the next render
screen_last: Screen,
screen_last: InternalScreen = undefined,
alt_screen: bool,
@ -113,11 +114,14 @@ pub fn Vaxis(comptime T: type) type {
/// freed when resizing
pub fn resize(self: *Self, alloc: std.mem.Allocator, winsize: Winsize) !void {
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
// every cell
self.screen.init();
try self.screen_last.resize(alloc, winsize.cols, winsize.rows);
self.screen_last.deinit(alloc);
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
@ -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
// anything
if (std.meta.eql(cell, self.screen_last.buf[i])) {
if (self.screen_last.buf[i].eql(cell)) {
reposition = true;
// Close any osc8 sequence we might be in before
// repositioning
@ -225,7 +229,7 @@ pub fn Vaxis(comptime T: type) type {
}
defer cursor = cell.style;
// 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
if (reposition) {

View file

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