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,
|
.optimize = optimize,
|
||||||
.target = target,
|
.target = target,
|
||||||
});
|
});
|
||||||
|
const gap_buffer_dep = b.dependency("gap_buffer", .{
|
||||||
|
.optimize = optimize,
|
||||||
|
.target = target,
|
||||||
|
});
|
||||||
|
|
||||||
// Module
|
// Module
|
||||||
const vaxis_mod = b.addModule("vaxis", .{
|
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("ziglyph", ziglyph_dep.module("ziglyph"));
|
||||||
vaxis_mod.addImport("zigimg", zigimg_dep.module("zigimg"));
|
vaxis_mod.addImport("zigimg", zigimg_dep.module("zigimg"));
|
||||||
|
vaxis_mod.addImport("gap_buffer", gap_buffer_dep.module("gap_buffer"));
|
||||||
|
|
||||||
// Examples
|
// Examples
|
||||||
const example_step = b.step("example", "Run 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("ziglyph", ziglyph_dep.module("ziglyph"));
|
||||||
tests.root_module.addImport("zigimg", zigimg_dep.module("zigimg"));
|
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);
|
const tests_run = b.addRunArtifact(tests);
|
||||||
b.installArtifact(tests);
|
b.installArtifact(tests);
|
||||||
|
|
|
@ -11,5 +11,9 @@
|
||||||
.url = "https://github.com/rockorager/zigimg/archive/19a49a7.tar.gz",
|
.url = "https://github.com/rockorager/zigimg/archive/19a49a7.tar.gz",
|
||||||
.hash = "1220ebfa8587cfd644995fc08e218dbb3ebd7344fb8e129ff02bc5a6d52a2325370d",
|
.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 Cell = @import("../Cell.zig");
|
||||||
const Window = @import("../Window.zig");
|
const Window = @import("../Window.zig");
|
||||||
const GraphemeIterator = @import("ziglyph").GraphemeIterator;
|
const GraphemeIterator = @import("ziglyph").GraphemeIterator;
|
||||||
|
const GapBuffer = @import("gap_buffer").GapBuffer;
|
||||||
|
|
||||||
const log = std.log.scoped(.text_input);
|
const log = std.log.scoped(.text_input);
|
||||||
|
|
||||||
|
@ -16,15 +17,11 @@ const Event = union(enum) {
|
||||||
// Index of our cursor
|
// Index of our cursor
|
||||||
cursor_idx: usize = 0,
|
cursor_idx: usize = 0,
|
||||||
grapheme_count: usize = 0,
|
grapheme_count: usize = 0,
|
||||||
|
buf: GapBuffer(u8),
|
||||||
// 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),
|
|
||||||
|
|
||||||
pub fn init(alloc: std.mem.Allocator) TextInput {
|
pub fn init(alloc: std.mem.Allocator) TextInput {
|
||||||
return 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| {
|
.key_press => |key| {
|
||||||
if (key.matches(Key.backspace, .{})) {
|
if (key.matches(Key.backspace, .{})) {
|
||||||
if (self.cursor_idx == 0) return;
|
if (self.cursor_idx == 0) return;
|
||||||
self.deleteBeforeCursor();
|
try self.deleteBeforeCursor();
|
||||||
} else if (key.matches(Key.delete, .{}) or key.matches('d', .{ .ctrl = true })) {
|
} else if (key.matches(Key.delete, .{}) or key.matches('d', .{ .ctrl = true })) {
|
||||||
if (self.cursor_idx == self.grapheme_count) return;
|
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 })) {
|
} else if (key.matches(Key.left, .{}) or key.matches('b', .{ .ctrl = true })) {
|
||||||
if (self.cursor_idx > 0) self.cursor_idx -= 1;
|
if (self.cursor_idx > 0) self.cursor_idx -= 1;
|
||||||
} else if (key.matches(Key.right, .{}) or key.matches('f', .{ .ctrl = true })) {
|
} 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 })) {
|
} else if (key.matches('e', .{ .ctrl = true })) {
|
||||||
self.cursor_idx = self.grapheme_count;
|
self.cursor_idx = self.grapheme_count;
|
||||||
} else if (key.matches('k', .{ .ctrl = true })) {
|
} else if (key.matches('k', .{ .ctrl = true })) {
|
||||||
while (self.cursor_idx < self.grapheme_count) {
|
try self.deleteToEnd();
|
||||||
self.deleteAtCursor();
|
|
||||||
}
|
|
||||||
} else if (key.matches('u', .{ .ctrl = true })) {
|
} else if (key.matches('u', .{ .ctrl = true })) {
|
||||||
while (self.cursor_idx > 0) {
|
try self.deleteToStart();
|
||||||
self.deleteBeforeCursor();
|
|
||||||
}
|
|
||||||
} else if (key.text) |text| {
|
} else if (key.text) |text| {
|
||||||
try self.buf.insertSlice(self.byteOffsetToCursor(), text);
|
try self.buf.insertSliceBefore(self.byteOffsetToCursor(), text);
|
||||||
self.cursor_idx += 1;
|
self.cursor_idx += 1;
|
||||||
self.grapheme_count += 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 {
|
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 col: usize = 0;
|
||||||
var i: usize = 0;
|
var i: usize = 0;
|
||||||
var cursor_idx: usize = 0;
|
var cursor_idx: usize = 0;
|
||||||
while (iter.next()) |grapheme| {
|
while (first_iter.next()) |grapheme| {
|
||||||
const g = grapheme.slice(self.buf.items);
|
const g = grapheme.slice(self.buf.items);
|
||||||
const w = win.gwidth(g);
|
const w = win.gwidth(g);
|
||||||
win.writeCell(col, 0, .{
|
win.writeCell(col, 0, .{
|
||||||
|
@ -84,11 +79,30 @@ pub fn draw(self: *TextInput, win: Window) void {
|
||||||
i += 1;
|
i += 1;
|
||||||
if (i == self.cursor_idx) cursor_idx = col;
|
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);
|
win.showCursor(cursor_idx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns the number of bytes before the cursor
|
// 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 {
|
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 iter = GraphemeIterator.init(self.buf.items);
|
||||||
var offset: usize = 0;
|
var offset: usize = 0;
|
||||||
var i: usize = 0;
|
var i: usize = 0;
|
||||||
|
@ -96,20 +110,52 @@ fn byteOffsetToCursor(self: TextInput) usize {
|
||||||
if (i == self.cursor_idx) break;
|
if (i == self.cursor_idx) break;
|
||||||
offset += grapheme.len;
|
offset += grapheme.len;
|
||||||
i += 1;
|
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;
|
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 iter = GraphemeIterator.init(self.buf.items);
|
||||||
var offset: usize = 0;
|
var offset: usize = 0;
|
||||||
var i: usize = 1;
|
var i: usize = 1;
|
||||||
while (iter.next()) |grapheme| {
|
while (iter.next()) |grapheme| {
|
||||||
if (i == self.cursor_idx) {
|
if (i == self.cursor_idx) {
|
||||||
var j: usize = 0;
|
try self.buf.replaceRangeBefore(offset, grapheme.len, &.{});
|
||||||
while (j < grapheme.len) : (j += 1) {
|
self.cursor_idx -= 1;
|
||||||
_ = self.buf.orderedRemove(offset);
|
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.cursor_idx -= 1;
|
||||||
self.grapheme_count -= 1;
|
self.grapheme_count -= 1;
|
||||||
return;
|
return;
|
||||||
|
@ -118,17 +164,27 @@ fn deleteBeforeCursor(self: *TextInput) void {
|
||||||
i += 1;
|
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 iter = GraphemeIterator.init(self.buf.items);
|
||||||
var offset: usize = 0;
|
var offset: usize = 0;
|
||||||
var i: usize = 1;
|
var i: usize = 1;
|
||||||
while (iter.next()) |grapheme| {
|
while (iter.next()) |grapheme| {
|
||||||
if (i == self.cursor_idx + 1) {
|
if (i == self.cursor_idx + 1) {
|
||||||
var j: usize = 0;
|
try self.buf.replaceRangeAfter(offset, grapheme.len, &.{});
|
||||||
while (j < grapheme.len) : (j += 1) {
|
self.grapheme_count -= 1;
|
||||||
_ = self.buf.orderedRemove(offset);
|
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;
|
self.grapheme_count -= 1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -136,3 +192,17 @@ fn deleteAtCursor(self: *TextInput) void {
|
||||||
i += 1;
|
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