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