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