a modern tui library written in zig
at v0.1.0 4.4 kB view raw
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}