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