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
|
// 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
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_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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue