╭ 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 const evt = @import("./event.zig"); 11 12 pub const UsersAndRepos = @import("./ui/UsersAndRepos.zig"); 13 14 pub const Page = union(enum) { 15 users_and_repos: UsersAndRepos, 16 }; 17 18 pub fn run(io: std.Io, allocator: std.mem.Allocator, page: *const Page) !void { 19 var root = try initRoot(allocator, page); 20 defer root.deinit(); 21 22 var terminal = try term.Terminal.init(io, allocator); 23 defer terminal.deinit(io); 24 25 var last_size = layout.Size{ .width = 0, .height = 0 }; 26 var last_grid = try Grid.init(allocator, last_size); 27 defer last_grid.deinit(); 28 29 while (!term.quit.load(.monotonic)) { 30 const grid_changed = try terminal.render(&root, &last_grid, &last_size); 31 32 // process any inputs. 33 // 34 // if the grid didn't change, then first do a blocking 35 // read, so the thread will sleep until further input. 36 // after that, all remaining reads are non-blocking so 37 // we can process the rest of the queued inputs. 38 // 39 // if the grid *did* change, then only do non-blocking 40 // reads. we do not want to sleep the thread because 41 // there may be an animation that requires more looping. 42 var blocking = !grid_changed; 43 while (try terminal.readKey(io, blocking)) |key| { 44 switch (key) { 45 .codepoint => |cp| if (cp == 'q') return else try root.input(key, root.getFocus()), 46 .mouse => |mouse| { 47 if (mouse.action == .press and mouse.action.press == .left) { 48 const root_focus = root.getFocus(); 49 var iter = root_focus.children.iterator(); 50 while (iter.next()) |entry| { 51 const child = entry.value_ptr.*; 52 if (!child.focus.focusable) continue; 53 const r = child.rect; 54 if (mouse.x >= r.x and mouse.y >= r.y and 55 mouse.x < r.x + r.size.width and mouse.y < r.y + r.size.height) 56 { 57 try root_focus.setFocus(entry.key_ptr.*); 58 break; 59 } 60 } 61 } else { 62 try root.input(key, root.getFocus()); 63 } 64 }, 65 else => try root.input(key, root.getFocus()), 66 } 67 blocking = false; 68 } 69 70 try root.build(.{ 71 .min_size = .{ .width = null, .height = null }, 72 .max_size = .{ .width = last_size.width, .height = last_size.height }, 73 }, root.getFocus()); 74 } 75 } 76 77 pub fn initRoot(allocator: std.mem.Allocator, page: *const Page) !Widget { 78 var root: Widget = switch (page.*) { 79 .users_and_repos => |*p| .{ .users_and_repos = try UsersAndRepos.View.init(allocator, p) }, 80 }; 81 errdefer root.deinit(); 82 83 try root.build(.{ 84 .min_size = .{ .width = null, .height = null }, 85 .max_size = .{ .width = 80, .height = null }, 86 }, root.getFocus()); 87 if (root.getFocus().child_id) |child_id| { 88 try root.getFocus().setFocus(child_id); 89 } 90 91 return root; 92 } 93 94 pub const Widget = union(enum) { 95 text: wgt.Text(Widget), 96 box: wgt.Box(Widget), 97 text_box: wgt.TextBox(Widget), 98 scroll: wgt.Scroll(Widget), 99 selectable_list: SelectableList, 100 users_and_repos: UsersAndRepos.View, 101 102 pub fn deinit(self: *Widget) void { 103 switch (self.*) { 104 inline else => |*case| case.deinit(), 105 } 106 } 107 108 pub fn build(self: *Widget, constraint: layout.Constraint, root_focus: *Focus) anyerror!void { 109 switch (self.*) { 110 inline else => |*case| try case.build(constraint, root_focus), 111 } 112 } 113 114 pub fn input(self: *Widget, key: inp.Key, root_focus: *Focus) anyerror!void { 115 switch (self.*) { 116 inline else => |*case| try case.input(key, root_focus), 117 } 118 } 119 120 pub fn clearGrid(self: *Widget) void { 121 switch (self.*) { 122 inline else => |*case| case.clearGrid(), 123 } 124 } 125 126 pub fn getGrid(self: Widget) ?Grid { 127 switch (self) { 128 inline else => |*case| return case.getGrid(), 129 } 130 } 131 132 pub fn getFocus(self: *Widget) *Focus { 133 switch (self.*) { 134 inline else => |*case| return case.getFocus(), 135 } 136 } 137 }; 138 139 // a scrollable list of selectable single-line items. content is replaced 140 // via setItems, which dupes the strings into the widget's own allocation. 141 pub const SelectableList = struct { 142 allocator: std.mem.Allocator, 143 scroll: wgt.Scroll(Widget), 144 lines: std.ArrayList([]const u8), 145 146 pub fn init(allocator: std.mem.Allocator) !SelectableList { 147 var self = blk: { 148 var inner_box = try wgt.Box(Widget).init(allocator, .{ .border_style = null, .rounded_corners = true, .direction = .vert }); 149 errdefer inner_box.deinit(); 150 151 var scroll = try wgt.Scroll(Widget).init(allocator, .{ .box = inner_box }, .vert); 152 errdefer scroll.deinit(); 153 154 break :blk SelectableList{ 155 .allocator = allocator, 156 .scroll = scroll, 157 .lines = .empty, 158 }; 159 }; 160 errdefer self.deinit(); 161 return self; 162 } 163 164 pub fn deinit(self: *SelectableList) void { 165 self.scroll.deinit(); 166 for (self.lines.items) |line| self.allocator.free(line); 167 self.lines.deinit(self.allocator); 168 } 169 170 pub fn setItems(self: *SelectableList, items: []const []const u8) !void { 171 const inner_box = &self.scroll.child.box; 172 173 for (inner_box.children.values()) |*child| { 174 child.widget.deinit(); 175 } 176 inner_box.children.clearAndFree(self.allocator); 177 178 for (self.lines.items) |line| self.allocator.free(line); 179 self.lines.clearAndFree(self.allocator); 180 181 self.scroll.x = 0; 182 self.scroll.y = 0; 183 // keep the focus tree in sync with the widget tree so setFocus can 184 // descend even when the pane isn't laid out this build (e.g. narrow 185 // terminal where only the focused pane fits). build's clear+addChild 186 // would do this otherwise, but it gets skipped for unbuilt panes. 187 inner_box.getFocus().clear(); 188 inner_box.getFocus().child_id = null; 189 190 for (items) |item| { 191 const line = try self.allocator.dupe(u8, item); 192 { 193 errdefer self.allocator.free(line); 194 try self.lines.append(self.allocator, line); 195 } 196 197 var text_box = try wgt.TextBox(Widget).init(self.allocator, line, .{ .border_style = .hidden, .rounded_corners = true, .wrap_kind = .none }); 198 errdefer text_box.deinit(); 199 text_box.getFocus().focusable = true; 200 try inner_box.children.put(self.allocator, text_box.getFocus().id, .{ .widget = .{ .text_box = text_box }, .rect = null, .min_size = null }); 201 try inner_box.getFocus().addChild(text_box.getFocus(), .{ .width = 0, .height = 0 }, 0, 0); 202 } 203 204 if (inner_box.children.count() > 0) { 205 inner_box.getFocus().child_id = inner_box.children.keys()[0]; 206 } 207 } 208 209 pub fn build(self: *SelectableList, constraint: layout.Constraint, root_focus: *Focus) !void { 210 self.clearGrid(); 211 const children = &self.scroll.child.box.children; 212 for (children.keys(), children.values()) |id, *item| { 213 item.widget.text_box.options.border_style = if (self.getFocus().child_id == id) 214 (if (root_focus.grandchild_id == id) .double else .single) 215 else 216 .hidden; 217 } 218 try self.scroll.build(constraint, root_focus); 219 } 220 221 pub fn input(self: *SelectableList, key: inp.Key, root_focus: *Focus) !void { 222 if (self.getFocus().child_id) |child_id| { 223 const children = &self.scroll.child.box.children; 224 if (children.getIndex(child_id)) |current_index| { 225 var index = current_index; 226 227 switch (key) { 228 .arrow_up => { 229 index -|= 1; 230 }, 231 .arrow_down => { 232 if (index + 1 < children.count()) { 233 index += 1; 234 } 235 }, 236 .home => { 237 index = 0; 238 }, 239 .end => { 240 if (children.count() > 0) { 241 index = children.count() - 1; 242 } 243 }, 244 .page_up => { 245 if (self.getGrid()) |grid| { 246 const half_count = (grid.size.height / 3) / 2; 247 index -|= half_count; 248 } 249 }, 250 .page_down => { 251 if (self.getGrid()) |grid| { 252 if (children.count() > 0) { 253 const half_count = (grid.size.height / 3) / 2; 254 index = @min(index + half_count, children.count() - 1); 255 } 256 } 257 }, 258 .mouse => |mouse| switch (mouse.action) { 259 .scroll => |dir| switch (dir) { 260 .up => index -|= 1, 261 .down => if (index + 1 < children.count()) { 262 index += 1; 263 }, 264 }, 265 else => {}, 266 }, 267 else => {}, 268 } 269 270 if (index != current_index) { 271 try root_focus.setFocus(children.keys()[index]); 272 self.updateScroll(index); 273 } 274 } 275 } 276 } 277 278 pub fn clearGrid(self: *SelectableList) void { 279 self.scroll.clearGrid(); 280 } 281 282 pub fn getGrid(self: SelectableList) ?Grid { 283 return self.scroll.getGrid(); 284 } 285 286 pub fn getFocus(self: *SelectableList) *Focus { 287 return self.scroll.getFocus(); 288 } 289 290 pub fn getSelectedIndex(self: SelectableList) ?usize { 291 if (self.scroll.child.box.focus.child_id) |child_id| { 292 const children = &self.scroll.child.box.children; 293 return children.getIndex(child_id); 294 } else { 295 return null; 296 } 297 } 298 299 fn updateScroll(self: *SelectableList, index: usize) void { 300 const inner_box = &self.scroll.child.box; 301 if (inner_box.children.values()[index].rect) |rect| { 302 self.scroll.scrollToRect(rect); 303 } 304 } 305 }; 306