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:
parent
1b7608f469
commit
d2f02897dc
5 changed files with 179 additions and 35 deletions
|
@ -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
73
src/InternalScreen.zig
Normal 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;
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue