feat: adds gap_buffer.zig
for TextInput
(#6)
Use a gap buffer for the `TextInput` widget instead of an `ArrayList`.
This commit is contained in:
parent
5e940fd295
commit
0798182012
3 changed files with 107 additions and 27 deletions
|
@ -14,6 +14,10 @@ pub fn build(b: *std.Build) void {
|
|||
.optimize = optimize,
|
||||
.target = target,
|
||||
});
|
||||
const gap_buffer_dep = b.dependency("gap_buffer", .{
|
||||
.optimize = optimize,
|
||||
.target = target,
|
||||
});
|
||||
|
||||
// Module
|
||||
const vaxis_mod = b.addModule("vaxis", .{
|
||||
|
@ -23,6 +27,7 @@ pub fn build(b: *std.Build) void {
|
|||
});
|
||||
vaxis_mod.addImport("ziglyph", ziglyph_dep.module("ziglyph"));
|
||||
vaxis_mod.addImport("zigimg", zigimg_dep.module("zigimg"));
|
||||
vaxis_mod.addImport("gap_buffer", gap_buffer_dep.module("gap_buffer"));
|
||||
|
||||
// Examples
|
||||
const example_step = b.step("example", "Run examples");
|
||||
|
@ -49,6 +54,7 @@ pub fn build(b: *std.Build) void {
|
|||
});
|
||||
tests.root_module.addImport("ziglyph", ziglyph_dep.module("ziglyph"));
|
||||
tests.root_module.addImport("zigimg", zigimg_dep.module("zigimg"));
|
||||
tests.root_module.addImport("gap_buffer", gap_buffer_dep.module("gap_buffer"));
|
||||
|
||||
const tests_run = b.addRunArtifact(tests);
|
||||
b.installArtifact(tests);
|
||||
|
|
|
@ -11,5 +11,9 @@
|
|||
.url = "https://github.com/rockorager/zigimg/archive/19a49a7.tar.gz",
|
||||
.hash = "1220ebfa8587cfd644995fc08e218dbb3ebd7344fb8e129ff02bc5a6d52a2325370d",
|
||||
},
|
||||
.gap_buffer = .{
|
||||
.url = "https://github.com/ryleelyman/GapBuffer.zig/archive/6a746497d5a2494026d0f471e42556f1f153f153.tar.gz",
|
||||
.hash = "12205354d9903e773f9d934dcfe756b0b5ffd895571ad631ab86ebc1aebba074dd82",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ const Key = @import("../Key.zig");
|
|||
const Cell = @import("../Cell.zig");
|
||||
const Window = @import("../Window.zig");
|
||||
const GraphemeIterator = @import("ziglyph").GraphemeIterator;
|
||||
const GapBuffer = @import("gap_buffer").GapBuffer;
|
||||
|
||||
const log = std.log.scoped(.text_input);
|
||||
|
||||
|
@ -16,15 +17,11 @@ const Event = union(enum) {
|
|||
// Index of our cursor
|
||||
cursor_idx: usize = 0,
|
||||
grapheme_count: usize = 0,
|
||||
|
||||
// TODO: an ArrayList is not great for this. orderedRemove is O(n) and we can
|
||||
// only remove one byte at a time. Make a bespoke ArrayList which allows removal
|
||||
// of a slice at a time, or truncating even would be nice
|
||||
buf: std.ArrayList(u8),
|
||||
buf: GapBuffer(u8),
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator) TextInput {
|
||||
return TextInput{
|
||||
.buf = std.ArrayList(u8).init(alloc),
|
||||
.buf = GapBuffer(u8).init(alloc),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -37,10 +34,10 @@ pub fn update(self: *TextInput, event: Event) !void {
|
|||
.key_press => |key| {
|
||||
if (key.matches(Key.backspace, .{})) {
|
||||
if (self.cursor_idx == 0) return;
|
||||
self.deleteBeforeCursor();
|
||||
try self.deleteBeforeCursor();
|
||||
} else if (key.matches(Key.delete, .{}) or key.matches('d', .{ .ctrl = true })) {
|
||||
if (self.cursor_idx == self.grapheme_count) return;
|
||||
self.deleteAtCursor();
|
||||
try self.deleteAtCursor();
|
||||
} else if (key.matches(Key.left, .{}) or key.matches('b', .{ .ctrl = true })) {
|
||||
if (self.cursor_idx > 0) self.cursor_idx -= 1;
|
||||
} else if (key.matches(Key.right, .{}) or key.matches('f', .{ .ctrl = true })) {
|
||||
|
@ -50,15 +47,11 @@ pub fn update(self: *TextInput, event: Event) !void {
|
|||
} else if (key.matches('e', .{ .ctrl = true })) {
|
||||
self.cursor_idx = self.grapheme_count;
|
||||
} else if (key.matches('k', .{ .ctrl = true })) {
|
||||
while (self.cursor_idx < self.grapheme_count) {
|
||||
self.deleteAtCursor();
|
||||
}
|
||||
try self.deleteToEnd();
|
||||
} else if (key.matches('u', .{ .ctrl = true })) {
|
||||
while (self.cursor_idx > 0) {
|
||||
self.deleteBeforeCursor();
|
||||
}
|
||||
try self.deleteToStart();
|
||||
} else if (key.text) |text| {
|
||||
try self.buf.insertSlice(self.byteOffsetToCursor(), text);
|
||||
try self.buf.insertSliceBefore(self.byteOffsetToCursor(), text);
|
||||
self.cursor_idx += 1;
|
||||
self.grapheme_count += 1;
|
||||
}
|
||||
|
@ -67,11 +60,13 @@ pub fn update(self: *TextInput, event: Event) !void {
|
|||
}
|
||||
|
||||
pub fn draw(self: *TextInput, win: Window) void {
|
||||
var iter = GraphemeIterator.init(self.buf.items);
|
||||
// assumption!! the gap is never within a grapheme
|
||||
// one way to _ensure_ this is to move the gap... but that's a cost we probably don't want to pay.
|
||||
var first_iter = GraphemeIterator.init(self.buf.items);
|
||||
var col: usize = 0;
|
||||
var i: usize = 0;
|
||||
var cursor_idx: usize = 0;
|
||||
while (iter.next()) |grapheme| {
|
||||
while (first_iter.next()) |grapheme| {
|
||||
const g = grapheme.slice(self.buf.items);
|
||||
const w = win.gwidth(g);
|
||||
win.writeCell(col, 0, .{
|
||||
|
@ -84,11 +79,30 @@ pub fn draw(self: *TextInput, win: Window) void {
|
|||
i += 1;
|
||||
if (i == self.cursor_idx) cursor_idx = col;
|
||||
}
|
||||
const second_half = self.buf.secondHalf();
|
||||
var second_iter = GraphemeIterator.init(second_half);
|
||||
while (second_iter.next()) |grapheme| {
|
||||
const g = grapheme.slice(second_half);
|
||||
const w = win.gwidth(g);
|
||||
win.writeCell(col, 0, .{
|
||||
.char = .{
|
||||
.grapheme = g,
|
||||
.width = 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
|
||||
// (since GapBuffers are strictly speaking not contiguous, this is a number in 0..realLength()
|
||||
// which would need to be fed to realIndex() to get an actual offset into self.buf.items.ptr)
|
||||
fn byteOffsetToCursor(self: TextInput) usize {
|
||||
// assumption! the gap is never in the middle of a grapheme
|
||||
// one way to _ensure_ this is to move the gap... but that's a cost we probably don't want to pay.
|
||||
var iter = GraphemeIterator.init(self.buf.items);
|
||||
var offset: usize = 0;
|
||||
var i: usize = 0;
|
||||
|
@ -96,20 +110,52 @@ fn byteOffsetToCursor(self: TextInput) usize {
|
|||
if (i == self.cursor_idx) break;
|
||||
offset += grapheme.len;
|
||||
i += 1;
|
||||
} else {
|
||||
var second_iter = GraphemeIterator.init(self.buf.secondHalf());
|
||||
while (second_iter.next()) |grapheme| {
|
||||
if (i == self.cursor_idx) break;
|
||||
offset += grapheme.len;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
fn deleteBeforeCursor(self: *TextInput) void {
|
||||
fn deleteToEnd(self: *TextInput) !void {
|
||||
self.cursor_idx += 1;
|
||||
const offset = self.byteOffsetToCursor();
|
||||
try self.buf.replaceRangeAfter(offset, self.buf.realLength(), &.{});
|
||||
self.grapheme_count = self.cursor_idx;
|
||||
self.cursor_idx -= 1;
|
||||
}
|
||||
|
||||
fn deleteToStart(self: *TextInput) !void {
|
||||
const offset = self.byteOffsetToCursor();
|
||||
try self.buf.replaceRangeBefore(0, offset, &.{});
|
||||
self.grapheme_count -= self.cursor_idx;
|
||||
self.cursor_idx = 0;
|
||||
}
|
||||
|
||||
fn deleteBeforeCursor(self: *TextInput) !void {
|
||||
// assumption! the gap is never in the middle of a grapheme
|
||||
// one way to _ensure_ this is to move the gap... but that's a cost we probably don't want to pay.
|
||||
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);
|
||||
try self.buf.replaceRangeBefore(offset, grapheme.len, &.{});
|
||||
self.cursor_idx -= 1;
|
||||
self.grapheme_count -= 1;
|
||||
return;
|
||||
}
|
||||
offset += grapheme.len;
|
||||
i += 1;
|
||||
} else {
|
||||
var second_iter = GraphemeIterator.init(self.buf.secondHalf());
|
||||
while (second_iter.next()) |grapheme| {
|
||||
if (i == self.cursor_idx) {
|
||||
try self.buf.replaceRangeBefore(offset, grapheme.len, &.{});
|
||||
self.cursor_idx -= 1;
|
||||
self.grapheme_count -= 1;
|
||||
return;
|
||||
|
@ -118,17 +164,27 @@ fn deleteBeforeCursor(self: *TextInput) void {
|
|||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn deleteAtCursor(self: *TextInput) void {
|
||||
fn deleteAtCursor(self: *TextInput) !void {
|
||||
// assumption! the gap is never in the middle of a grapheme
|
||||
// one way to _ensure_ this is to move the gap... but that's a cost we probably don't want to pay.
|
||||
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);
|
||||
try self.buf.replaceRangeAfter(offset, grapheme.len, &.{});
|
||||
self.grapheme_count -= 1;
|
||||
return;
|
||||
}
|
||||
offset += grapheme.len;
|
||||
i += 1;
|
||||
} else {
|
||||
var second_iter = GraphemeIterator.init(self.buf.secondHalf());
|
||||
while (second_iter.next()) |grapheme| {
|
||||
if (i == self.cursor_idx + 1) {
|
||||
try self.buf.replaceRangeAfter(offset, grapheme.len, &.{});
|
||||
self.grapheme_count -= 1;
|
||||
return;
|
||||
}
|
||||
|
@ -136,3 +192,17 @@ fn deleteAtCursor(self: *TextInput) void {
|
|||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test "assertion" {
|
||||
const astronaut = "👩🚀";
|
||||
const astronaut_emoji: Key = .{
|
||||
.text = astronaut,
|
||||
.codepoint = try std.unicode.utf8Decode(astronaut[0..4]),
|
||||
};
|
||||
var input = TextInput.init(std.testing.allocator);
|
||||
defer input.deinit();
|
||||
for (0..6) |_| {
|
||||
try input.update(.{ .key_press = astronaut_emoji });
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue