a modern tui library written in zig
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}