a modern tui library written in zig
1const std = @import("std");
2const Cell = @import("../cell.zig").Cell;
3const Key = @import("../Key.zig");
4const Window = @import("../Window.zig");
5const GraphemeIterator = @import("ziglyph").GraphemeIterator;
6
7const log = std.log.scoped(.text_input);
8
9const TextInput = @This();
10
11/// The events that this widget handles
12const Event = union(enum) {
13 key_press: Key,
14};
15
16// Index of our cursor
17cursor_idx: usize = 0,
18grapheme_count: usize = 0,
19
20// TODO: an ArrayList is not great for this. orderedRemove is O(n) and we can
21// only remove one byte at a time. Make a bespoke ArrayList which allows removal
22// of a slice at a time, or truncating even would be nice
23buf: std.ArrayList(u8),
24
25pub fn init(alloc: std.mem.Allocator) TextInput {
26 return TextInput{
27 .buf = std.ArrayList(u8).init(alloc),
28 };
29}
30
31pub fn deinit(self: *TextInput) void {
32 self.buf.deinit();
33}
34
35pub fn update(self: *TextInput, event: Event) !void {
36 switch (event) {
37 .key_press => |key| {
38 if (key.matches(Key.backspace, .{})) {
39 if (self.cursor_idx == 0) return;
40 self.deleteBeforeCursor();
41 } else if (key.matches(Key.delete, .{}) or key.matches('d', .{ .ctrl = true })) {
42 if (self.cursor_idx == self.grapheme_count) return;
43 self.deleteAtCursor();
44 } else if (key.matches(Key.left, .{}) or key.matches('b', .{ .ctrl = true })) {
45 if (self.cursor_idx > 0) self.cursor_idx -= 1;
46 } else if (key.matches(Key.right, .{}) or key.matches('f', .{ .ctrl = true })) {
47 if (self.cursor_idx < self.grapheme_count) self.cursor_idx += 1;
48 } else if (key.matches('a', .{ .ctrl = true })) {
49 self.cursor_idx = 0;
50 } else if (key.matches('e', .{ .ctrl = true })) {
51 self.cursor_idx = self.grapheme_count;
52 } else if (key.matches('k', .{ .ctrl = true })) {
53 while (self.cursor_idx < self.grapheme_count) {
54 self.deleteAtCursor();
55 }
56 } else if (key.matches('u', .{ .ctrl = true })) {
57 while (self.cursor_idx > 0) {
58 self.deleteBeforeCursor();
59 }
60 } else if (key.text) |text| {
61 try self.buf.insertSlice(self.byteOffsetToCursor(), text);
62 self.cursor_idx += 1;
63 self.grapheme_count += 1;
64 }
65 },
66 }
67}
68
69pub fn draw(self: *TextInput, win: Window) void {
70 var iter = GraphemeIterator.init(self.buf.items);
71 var col: usize = 0;
72 var i: usize = 0;
73 var cursor_idx: usize = 0;
74 while (iter.next()) |grapheme| {
75 const g = grapheme.slice(self.buf.items);
76 const w = win.gwidth(g);
77 win.writeCell(col, 0, .{
78 .char = .{
79 .grapheme = g,
80 .width = w,
81 },
82 });
83 col += w;
84 i += 1;
85 if (i == self.cursor_idx) cursor_idx = col;
86 }
87 win.showCursor(cursor_idx, 0);
88}
89
90// returns the number of bytes before the cursor
91fn byteOffsetToCursor(self: TextInput) usize {
92 var iter = GraphemeIterator.init(self.buf.items);
93 var offset: usize = 0;
94 var i: usize = 0;
95 while (iter.next()) |grapheme| {
96 if (i == self.cursor_idx) break;
97 offset += grapheme.len;
98 i += 1;
99 }
100 return offset;
101}
102
103fn deleteBeforeCursor(self: *TextInput) void {
104 var iter = GraphemeIterator.init(self.buf.items);
105 var offset: usize = 0;
106 var i: usize = 1;
107 while (iter.next()) |grapheme| {
108 if (i == self.cursor_idx) {
109 var j: usize = 0;
110 while (j < grapheme.len) : (j += 1) {
111 _ = self.buf.orderedRemove(offset);
112 }
113 self.cursor_idx -= 1;
114 self.grapheme_count -= 1;
115 return;
116 }
117 offset += grapheme.len;
118 i += 1;
119 }
120}
121
122fn deleteAtCursor(self: *TextInput) void {
123 var iter = GraphemeIterator.init(self.buf.items);
124 var offset: usize = 0;
125 var i: usize = 1;
126 while (iter.next()) |grapheme| {
127 if (i == self.cursor_idx + 1) {
128 var j: usize = 0;
129 while (j < grapheme.len) : (j += 1) {
130 _ = self.buf.orderedRemove(offset);
131 }
132 self.grapheme_count -= 1;
133 return;
134 }
135 offset += grapheme.len;
136 i += 1;
137 }
138}