╭ ui.zig ───────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const xit = @import("xit"); 3 const xitui = xit.xitui; 4 const term = xitui.terminal; 5 const wgt = xitui.widget; 6 const layout = xitui.layout; 7 const inp = xitui.input; 8 const Grid = xitui.grid.Grid; 9 const Focus = xitui.focus.Focus; 10 11 pub fn run(io: std.Io, allocator: std.mem.Allocator) !void { 12 var root = try initRoot(allocator); 13 defer root.deinit(); 14 15 var terminal = try term.Terminal.init(io, allocator); 16 defer terminal.deinit(io); 17 18 var last_size = layout.Size{ .width = 0, .height = 0 }; 19 var last_grid = try Grid.init(allocator, last_size); 20 defer last_grid.deinit(); 21 22 while (!term.quit.load(.monotonic)) { 23 const grid_changed = try terminal.render(&root, &last_grid, &last_size); 24 25 // process any inputs. 26 // 27 // if the grid didn't change, then first do a blocking 28 // read, so the thread will sleep until further input. 29 // after that, all remaining reads are non-blocking so 30 // we can process the rest of the queued inputs. 31 // 32 // if the grid *did* change, then only do non-blocking 33 // reads. we do not want to sleep the thread because 34 // there may be an animation that requires more looping. 35 var blocking = !grid_changed; 36 while (try terminal.readKey(io, blocking)) |key| { 37 switch (key) { 38 .codepoint => |cp| if (cp == 'q') return else try root.input(key, root.getFocus()), 39 else => try root.input(key, root.getFocus()), 40 } 41 blocking = false; 42 } 43 44 try root.build(.{ 45 .min_size = .{ .width = null, .height = null }, 46 .max_size = .{ .width = last_size.width, .height = last_size.height }, 47 }, root.getFocus()); 48 } 49 } 50 51 pub fn initRoot(allocator: std.mem.Allocator) !Widget { 52 var root = Widget{ .widget_list = try WidgetList.init(allocator) }; 53 errdefer root.deinit(); 54 55 try root.build(.{ 56 .min_size = .{ .width = null, .height = null }, 57 .max_size = .{ .width = 80, .height = 24 }, 58 }, root.getFocus()); 59 if (root.getFocus().child_id) |child_id| { 60 try root.getFocus().setFocus(child_id); 61 } 62 63 return root; 64 } 65 66 pub fn generateHtml(allocator: std.mem.Allocator, root: *Widget) ![]const u8 { 67 const grid = root.getGrid() orelse return error.MissingGrid; 68 69 var out: std.ArrayList(u8) = .empty; 70 errdefer out.deinit(allocator); 71 72 for (0..grid.size.height) |y| { 73 for (0..grid.size.width) |x| { 74 const rune = grid.cells.items[try grid.cells.at(.{ y, x })].rune orelse " "; 75 try appendEscapedHtml(allocator, &out, rune); 76 } 77 try out.append(allocator, '\n'); 78 } 79 80 return try out.toOwnedSlice(allocator); 81 } 82 83 fn appendEscapedHtml(allocator: std.mem.Allocator, out: *std.ArrayList(u8), input: []const u8) !void { 84 for (input) |ch| { 85 switch (ch) { 86 '&' => try out.appendSlice(allocator, "&amp;"), 87 '<' => try out.appendSlice(allocator, "&lt;"), 88 '>' => try out.appendSlice(allocator, "&gt;"), 89 '"' => try out.appendSlice(allocator, "&quot;"), 90 '\'' => try out.appendSlice(allocator, "&#39;"), 91 else => try out.append(allocator, ch), 92 } 93 } 94 } 95 96 const WidgetList = struct { 97 allocator: std.mem.Allocator, 98 scroll: wgt.Scroll(Widget), 99 100 pub fn init(allocator: std.mem.Allocator) !WidgetList { 101 var self = blk: { 102 var inner_box = try wgt.Box(Widget).init(allocator, null, .vert); 103 errdefer inner_box.deinit(); 104 105 var scroll = try wgt.Scroll(Widget).init(allocator, .{ .box = inner_box }, .vert); 106 errdefer scroll.deinit(); 107 108 break :blk WidgetList{ 109 .allocator = allocator, 110 .scroll = scroll, 111 }; 112 }; 113 errdefer self.deinit(); 114 115 const inner_box = &self.scroll.child.box; 116 117 { 118 var text_box = try wgt.TextBox(Widget).init(allocator, "this is a TextBox", .single, .none); 119 errdefer text_box.deinit(); 120 text_box.getFocus().focusable = true; 121 try inner_box.children.put(allocator, text_box.getFocus().id, .{ .widget = .{ .text_box = text_box }, .rect = null, .min_size = null }); 122 } 123 124 { 125 var text_box = try wgt.TextBox(Widget).init(allocator, "this is a\nmulti-line TextBox", .single, .none); 126 errdefer text_box.deinit(); 127 text_box.getFocus().focusable = true; 128 try inner_box.children.put(allocator, text_box.getFocus().id, .{ .widget = .{ .text_box = text_box }, .rect = null, .min_size = null }); 129 } 130 131 if (inner_box.children.count() > 0) { 132 self.scroll.getFocus().child_id = inner_box.children.keys()[0]; 133 } 134 135 return self; 136 } 137 138 pub fn deinit(self: *WidgetList) void { 139 self.scroll.deinit(); 140 } 141 142 pub fn build(self: *WidgetList, constraint: layout.Constraint, root_focus: *Focus) !void { 143 self.clearGrid(); 144 const children = &self.scroll.child.box.children; 145 for (children.keys(), children.values()) |id, *commit| { 146 commit.widget.text_box.border_style = if (self.getFocus().child_id == id) 147 (if (root_focus.grandchild_id == id) .double else .single) 148 else 149 .hidden; 150 } 151 try self.scroll.build(constraint, root_focus); 152 } 153 154 pub fn input(self: *WidgetList, key: inp.Key, root_focus: *Focus) !void { 155 if (self.getFocus().child_id) |child_id| { 156 const children = &self.scroll.child.box.children; 157 if (children.getIndex(child_id)) |current_index| { 158 var index = current_index; 159 160 switch (key) { 161 .arrow_up => { 162 index -|= 1; 163 }, 164 .arrow_down => { 165 if (index + 1 < children.count()) { 166 index += 1; 167 } 168 }, 169 .home => { 170 index = 0; 171 }, 172 .end => { 173 if (children.count() > 0) { 174 index = children.count() - 1; 175 } 176 }, 177 .page_up => { 178 if (self.getGrid()) |grid| { 179 const half_count = (grid.size.height / 3) / 2; 180 index -|= half_count; 181 } 182 }, 183 .page_down => { 184 if (self.getGrid()) |grid| { 185 if (children.count() > 0) { 186 const half_count = (grid.size.height / 3) / 2; 187 index = @min(index + half_count, children.count() - 1); 188 } 189 } 190 }, 191 else => {}, 192 } 193 194 if (index != current_index) { 195 try root_focus.setFocus(children.keys()[index]); 196 self.updateScroll(index); 197 } 198 } 199 } 200 } 201 202 pub fn clearGrid(self: *WidgetList) void { 203 self.scroll.clearGrid(); 204 } 205 206 pub fn getGrid(self: WidgetList) ?Grid { 207 return self.scroll.getGrid(); 208 } 209 210 pub fn getFocus(self: *WidgetList) *Focus { 211 return self.scroll.getFocus(); 212 } 213 214 pub fn getSelectedIndex(self: WidgetList) ?usize { 215 if (self.scroll.child.box.focus.child_id) |child_id| { 216 const children = &self.scroll.child.box.children; 217 return children.getIndex(child_id); 218 } else { 219 return null; 220 } 221 } 222 223 fn updateScroll(self: *WidgetList, index: usize) void { 224 const left_box = &self.scroll.child.box; 225 if (left_box.children.values()[index].rect) |rect| { 226 self.scroll.scrollToRect(rect); 227 } 228 } 229 }; 230 231 pub const Widget = union(enum) { 232 text: wgt.Text(Widget), 233 box: wgt.Box(Widget), 234 text_box: wgt.TextBox(Widget), 235 scroll: wgt.Scroll(Widget), 236 widget_list: WidgetList, 237 238 pub fn deinit(self: *Widget) void { 239 switch (self.*) { 240 inline else => |*case| case.deinit(), 241 } 242 } 243 244 pub fn build(self: *Widget, constraint: layout.Constraint, root_focus: *Focus) anyerror!void { 245 switch (self.*) { 246 inline else => |*case| try case.build(constraint, root_focus), 247 } 248 } 249 250 pub fn input(self: *Widget, key: inp.Key, root_focus: *Focus) anyerror!void { 251 switch (self.*) { 252 inline else => |*case| try case.input(key, root_focus), 253 } 254 } 255 256 pub fn clearGrid(self: *Widget) void { 257 switch (self.*) { 258 inline else => |*case| case.clearGrid(), 259 } 260 } 261 262 pub fn getGrid(self: Widget) ?Grid { 263 switch (self) { 264 inline else => |*case| return case.getGrid(), 265 } 266 } 267 268 pub fn getFocus(self: *Widget) *Focus { 269 switch (self.*) { 270 inline else => |*case| return case.getFocus(), 271 } 272 } 273 }; 274