a modern tui library written in zig
at main 7.2 kB view raw
1const std = @import("std"); 2const vaxis = @import("../main.zig"); 3const uucode = @import("uucode"); 4const ScrollView = vaxis.widgets.ScrollView; 5 6/// Simple grapheme representation to replace Graphemes.Grapheme 7const Grapheme = struct { 8 len: u16, 9 offset: u32, 10}; 11 12pub const BufferWriter = struct { 13 pub const Error = error{OutOfMemory}; 14 pub const Writer = std.io.GenericWriter(@This(), Error, write); 15 16 allocator: std.mem.Allocator, 17 buffer: *Buffer, 18 19 pub fn write(self: @This(), bytes: []const u8) Error!usize { 20 try self.buffer.append(self.allocator, .{ 21 .bytes = bytes, 22 }); 23 return bytes.len; 24 } 25 26 pub fn writer(self: @This()) Writer { 27 return .{ .context = self }; 28 } 29}; 30 31pub const Buffer = struct { 32 const StyleList = std.ArrayListUnmanaged(vaxis.Style); 33 const StyleMap = std.HashMapUnmanaged(usize, usize, std.hash_map.AutoContext(usize), std.hash_map.default_max_load_percentage); 34 35 pub const Content = struct { 36 bytes: []const u8, 37 }; 38 39 pub const Style = struct { 40 begin: usize, 41 end: usize, 42 style: vaxis.Style, 43 }; 44 45 pub const Error = error{OutOfMemory}; 46 47 grapheme: std.MultiArrayList(Grapheme) = .{}, 48 content: std.ArrayListUnmanaged(u8) = .{}, 49 style_list: StyleList = .{}, 50 style_map: StyleMap = .{}, 51 rows: usize = 0, 52 cols: usize = 0, 53 // used when appending to a buffer 54 last_cols: usize = 0, 55 56 pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void { 57 self.style_map.deinit(allocator); 58 self.style_list.deinit(allocator); 59 self.grapheme.deinit(allocator); 60 self.content.deinit(allocator); 61 self.* = undefined; 62 } 63 64 /// Clears all buffer data. 65 pub fn clear(self: *@This(), allocator: std.mem.Allocator) void { 66 self.deinit(allocator); 67 self.* = .{}; 68 } 69 70 /// Replaces contents of the buffer, all previous buffer data is lost. 71 pub fn update(self: *@This(), allocator: std.mem.Allocator, content: Content) Error!void { 72 self.clear(allocator); 73 errdefer self.clear(allocator); 74 try self.append(allocator, content); 75 } 76 77 /// Appends content to the buffer. 78 pub fn append(self: *@This(), allocator: std.mem.Allocator, content: Content) Error!void { 79 var cols: usize = self.last_cols; 80 var iter = uucode.grapheme.Iterator(uucode.utf8.Iterator).init(.init(content.bytes)); 81 82 var grapheme_start: usize = 0; 83 var prev_break: bool = true; 84 85 while (iter.next()) |result| { 86 if (prev_break and !result.is_break) { 87 // Start of a new grapheme 88 const cp_len: usize = std.unicode.utf8CodepointSequenceLength(result.cp) catch 1; 89 grapheme_start = iter.i - cp_len; 90 } 91 92 if (result.is_break) { 93 // End of a grapheme 94 const grapheme_end = iter.i; 95 const grapheme_len = grapheme_end - grapheme_start; 96 97 try self.grapheme.append(allocator, .{ 98 .len = @intCast(grapheme_len), 99 .offset = @intCast(self.content.items.len + grapheme_start), 100 }); 101 102 const cluster = content.bytes[grapheme_start..grapheme_end]; 103 if (std.mem.eql(u8, cluster, "\n")) { 104 self.cols = @max(self.cols, cols); 105 cols = 0; 106 } else { 107 // Calculate width using gwidth 108 const w = vaxis.gwidth.gwidth(cluster, .unicode); 109 cols +|= w; 110 } 111 112 grapheme_start = grapheme_end; 113 } 114 prev_break = result.is_break; 115 } 116 117 // Flush the last grapheme if we ended mid-cluster 118 if (!prev_break and grapheme_start < content.bytes.len) { 119 const grapheme_len = content.bytes.len - grapheme_start; 120 121 try self.grapheme.append(allocator, .{ 122 .len = @intCast(grapheme_len), 123 .offset = @intCast(self.content.items.len + grapheme_start), 124 }); 125 126 const cluster = content.bytes[grapheme_start..]; 127 if (!std.mem.eql(u8, cluster, "\n")) { 128 const w = vaxis.gwidth.gwidth(cluster, .unicode); 129 cols +|= w; 130 } 131 } 132 133 try self.content.appendSlice(allocator, content.bytes); 134 self.last_cols = cols; 135 self.cols = @max(self.cols, cols); 136 self.rows +|= std.mem.count(u8, content.bytes, "\n"); 137 } 138 139 /// Clears all styling data. 140 pub fn clearStyle(self: *@This(), allocator: std.mem.Allocator) void { 141 self.style_list.deinit(allocator); 142 self.style_map.deinit(allocator); 143 } 144 145 /// Update style for range of the buffer contents. 146 pub fn updateStyle(self: *@This(), allocator: std.mem.Allocator, style: Style) Error!void { 147 const style_index = blk: { 148 for (self.style_list.items, 0..) |s, i| { 149 if (std.meta.eql(s, style.style)) { 150 break :blk i; 151 } 152 } 153 try self.style_list.append(allocator, style.style); 154 break :blk self.style_list.items.len - 1; 155 }; 156 for (style.begin..style.end) |i| { 157 try self.style_map.put(allocator, i, style_index); 158 } 159 } 160 161 pub fn writer( 162 self: *@This(), 163 allocator: std.mem.Allocator, 164 ) BufferWriter.Writer { 165 return .{ 166 .context = .{ 167 .allocator = allocator, 168 .buffer = self, 169 }, 170 }; 171 } 172}; 173 174scroll_view: ScrollView = .{}, 175 176pub fn input(self: *@This(), key: vaxis.Key) void { 177 self.scroll_view.input(key); 178} 179 180pub fn draw(self: *@This(), win: vaxis.Window, buffer: Buffer) void { 181 self.scroll_view.draw(win, .{ .cols = buffer.cols, .rows = buffer.rows }); 182 const Pos = struct { x: usize = 0, y: usize = 0 }; 183 var pos: Pos = .{}; 184 var byte_index: usize = 0; 185 const bounds = self.scroll_view.bounds(win); 186 for (buffer.grapheme.items(.len), buffer.grapheme.items(.offset), 0..) |g_len, g_offset, index| { 187 if (bounds.above(pos.y)) { 188 break; 189 } 190 191 const cluster = buffer.content.items[g_offset..][0..g_len]; 192 defer byte_index += cluster.len; 193 194 if (std.mem.eql(u8, cluster, "\n")) { 195 if (index == buffer.grapheme.len - 1) { 196 break; 197 } 198 pos.y +|= 1; 199 pos.x = 0; 200 continue; 201 } else if (bounds.below(pos.y)) { 202 continue; 203 } 204 205 const width = win.gwidth(cluster); 206 defer pos.x +|= width; 207 208 if (!bounds.colInside(pos.x)) { 209 continue; 210 } 211 212 const style: vaxis.Style = blk: { 213 if (buffer.style_map.get(byte_index)) |style_index| { 214 break :blk buffer.style_list.items[style_index]; 215 } 216 break :blk .{}; 217 }; 218 219 self.scroll_view.writeCell(win, pos.x, pos.y, .{ 220 .char = .{ .grapheme = cluster, .width = @intCast(width) }, 221 .style = style, 222 }); 223 } 224}