a modern tui library written in zig
1const std = @import("std");
2const ziglyph = @import("ziglyph");
3const WordIterator = ziglyph.WordIterator;
4const GraphemeIterator = ziglyph.GraphemeIterator;
5
6const Screen = @import("Screen.zig");
7const Cell = @import("cell.zig").Cell;
8const Segment = @import("cell.zig").Segment;
9const gw = @import("gwidth.zig");
10
11const log = std.log.scoped(.window);
12
13const Window = @This();
14
15pub const Size = union(enum) {
16 expand,
17 limit: usize,
18};
19
20/// horizontal offset from the screen
21x_off: usize,
22/// vertical offset from the screen
23y_off: usize,
24/// width of the window. This can't be larger than the terminal screen
25width: usize,
26/// height of the window. This can't be larger than the terminal screen
27height: usize,
28
29screen: *Screen,
30
31/// Creates a new window with offset relative to parent and size clamped to the
32/// parent's size. Windows do not retain a reference to their parent and are
33/// unaware of resizes.
34pub fn initChild(
35 self: Window,
36 x_off: usize,
37 y_off: usize,
38 width: Size,
39 height: Size,
40) Window {
41 const resolved_width = switch (width) {
42 .expand => self.width - x_off,
43 .limit => |w| blk: {
44 if (w + x_off > self.width) {
45 break :blk self.width - x_off;
46 }
47 break :blk w;
48 },
49 };
50 const resolved_height = switch (height) {
51 .expand => self.height - y_off,
52 .limit => |h| blk: {
53 if (h + y_off > self.height) {
54 break :blk self.height - y_off;
55 }
56 break :blk h;
57 },
58 };
59 return Window{
60 .x_off = x_off + self.x_off,
61 .y_off = y_off + self.y_off,
62 .width = resolved_width,
63 .height = resolved_height,
64 .screen = self.screen,
65 };
66}
67
68/// writes a cell to the location in the window
69pub fn writeCell(self: Window, col: usize, row: usize, cell: Cell) void {
70 if (self.height == 0 or self.width == 0) return;
71 if (self.height <= row or self.width <= col) return;
72 self.screen.writeCell(col + self.x_off, row + self.y_off, cell);
73}
74
75/// fills the window with the default cell
76pub fn clear(self: Window) void {
77 self.fill(.{});
78}
79
80/// returns the width of the grapheme. This depends on the terminal capabilities
81pub fn gwidth(self: Window, str: []const u8) usize {
82 const m: gw.Method = if (self.screen.unicode) .unicode else .wcwidth;
83 return gw.gwidth(str, m) catch 1;
84}
85
86/// fills the window with the provided cell
87pub fn fill(self: Window, cell: Cell) void {
88 var row: usize = self.y_off;
89 while (row < (self.height + self.y_off)) : (row += 1) {
90 var col: usize = self.x_off;
91 while (col < (self.width + self.x_off)) : (col += 1) {
92 self.screen.writeCell(col, row, cell);
93 }
94 }
95}
96
97/// hide the cursor
98pub fn hideCursor(self: Window) void {
99 self.screen.cursor_vis = false;
100}
101
102/// show the cursor at the given coordinates, 0 indexed
103pub fn showCursor(self: Window, col: usize, row: usize) void {
104 if (self.height == 0 or self.width == 0) return;
105 if (self.height <= row or self.width <= col) return;
106 self.screen.cursor_vis = true;
107 self.screen.cursor_row = row + self.y_off;
108 self.screen.cursor_col = col + self.x_off;
109}
110
111/// prints text in the window with simple word wrapping.
112pub fn wrap(self: Window, segments: []Segment) !void {
113 // pub fn wrap(self: Window, str: []const u8) !void {
114 var row: usize = 0;
115 var col: usize = 0;
116 var wrapped: bool = false;
117 for (segments) |segment| {
118 var word_iter = try WordIterator.init(segment.text);
119 while (word_iter.next()) |word| {
120 // break lines when we need
121 if (isLineBreak(word.bytes)) {
122 row += 1;
123 col = 0;
124 wrapped = false;
125 continue;
126 }
127 // break lines when we can't fit this word, and the word isn't longer
128 // than our width
129 const word_width = self.gwidth(word.bytes);
130 if (word_width + col >= self.width and word_width < self.width) {
131 row += 1;
132 col = 0;
133 wrapped = true;
134 }
135 // don't print whitespace in the first column, unless we had a hard
136 // break
137 if (col == 0 and std.mem.eql(u8, word.bytes, " ") and wrapped) continue;
138 var iter = GraphemeIterator.init(word.bytes);
139 while (iter.next()) |grapheme| {
140 if (col >= self.width) {
141 row += 1;
142 col = 0;
143 wrapped = true;
144 }
145 const s = grapheme.slice(word.bytes);
146 const w = self.gwidth(s);
147 self.writeCell(col, row, .{
148 .char = .{
149 .grapheme = s,
150 .width = w,
151 },
152 .style = segment.style,
153 .link = segment.link,
154 });
155 col += w;
156 }
157 }
158 }
159}
160
161fn isLineBreak(str: []const u8) bool {
162 if (std.mem.eql(u8, str, "\r\n")) {
163 return true;
164 } else if (std.mem.eql(u8, str, "\r")) {
165 return true;
166 } else if (std.mem.eql(u8, str, "\n")) {
167 return true;
168 } else {
169 return false;
170 }
171}
172
173test "Window size set" {
174 var parent = Window{
175 .x_off = 0,
176 .y_off = 0,
177 .width = 20,
178 .height = 20,
179 .screen = undefined,
180 };
181
182 const child = parent.initChild(1, 1, .expand, .expand);
183 try std.testing.expectEqual(19, child.width);
184 try std.testing.expectEqual(19, child.height);
185}
186
187test "Window size set too big" {
188 var parent = Window{
189 .x_off = 0,
190 .y_off = 0,
191 .width = 20,
192 .height = 20,
193 .screen = undefined,
194 };
195
196 const child = parent.initChild(0, 0, .{ .limit = 21 }, .{ .limit = 21 });
197 try std.testing.expectEqual(20, child.width);
198 try std.testing.expectEqual(20, child.height);
199}
200
201test "Window size set too big with offset" {
202 var parent = Window{
203 .x_off = 0,
204 .y_off = 0,
205 .width = 20,
206 .height = 20,
207 .screen = undefined,
208 };
209
210 const child = parent.initChild(10, 10, .{ .limit = 21 }, .{ .limit = 21 });
211 try std.testing.expectEqual(10, child.width);
212 try std.testing.expectEqual(10, child.height);
213}
214
215test "Window size nested offsets" {
216 var parent = Window{
217 .x_off = 1,
218 .y_off = 1,
219 .width = 20,
220 .height = 20,
221 .screen = undefined,
222 };
223
224 const child = parent.initChild(10, 10, .{ .limit = 21 }, .{ .limit = 21 });
225 try std.testing.expectEqual(11, child.x_off);
226 try std.testing.expectEqual(11, child.y_off);
227}