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