╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ Commits.zig ──────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
117 // (showing its date and message), and — when there's another page — a 118 // "next" row at the bottom that links to it. 119 scroll: wgt.Scroll(ui.Widget), // wraps a vertical Box of rows 120 data: *const Self, 121 122 pub fn init(allocator: std.mem.Allocator, data: *const Self, session: *ui.Session) !View { 123 var box = wgt.Box(ui.Widget).init(.{ .border_style = null, .direction = .vert }); 124 errdefer box.deinit(allocator); 125 126 // labels and link kinds are borrowed by the rows, so they live in the 127 // page arena (as long as this page's widget tree). 128 const aa = session.page_arena.allocator(); 129 for (data.commits) |commit| { 130 const label = try std.fmt.allocPrint(aa, "{s} {s}", .{ commit.date, commit.message }); 131 try addRow(allocator, &box, label, ""); 132 } 133 if (data.next_start) |next| { 134 try addRow(allocator, &box, "next", try commitsPageLink(session.page_arena, data.identity, next)); 135 } 136 if (box.children.count() > 0) box.getFocus().child_id = box.children.keys()[0]; 137 138 var scroll = try wgt.Scroll(ui.Widget).init(allocator, .{ .box = box }, .vert); 139 errdefer scroll.deinit(allocator); 140 return .{ .scroll = scroll, .data = data }; 141 } 142 143 fn addRow(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), label: []const u8, link: []const u8) !void { 144 var row = try wgt.TextBox(ui.Widget).init(allocator, label, .{ .border_style = .hidden, .rounded_corners = true, .wrap_kind = .none }); 145 errdefer row.deinit(allocator); 146 row.getFocus().focusable = true; 147 if (link.len != 0) row.getFocus().kind = .{ .custom = link }; 148 try box.children.put(allocator, row.getFocus().id, .{ .widget = .{ .text_box = row }, .rect = null, .min_size = null }); 149 } 150 151 pub fn deinit(self: *View, allocator: std.mem.Allocator) void { 152 self.scroll.deinit(allocator); 153 } 154 155 fn innerBox(self: *View) *wgt.Box(ui.Widget) { 156 return &self.scroll.child.box; 157 } 158 159 pub fn build(self: *View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 160 self.clearGrid(); 161 // the selected row shows a border (the focused TextBox upgrades it to a 162 // double border itself); the rest stay borderless. 163 const box = self.innerBox(); 164 for (box.children.keys(), box.children.values()) |id, *child| { 165 switch (child.widget) { 166 .text_box => |*tb| tb.options.border_style = if (box.getFocus().child_id == id) .single else .hidden, 167 else => {}, 168 } 169 } 170 // clear the incoming min_size so rows size to their content rather than 171 // stretching to fill the page height; max_size still bounds the scroll 172 // viewport so a long listing clips and scrolls. 173 try self.scroll.build(allocator, .{ 174 .min_size = .{ .width = null, .height = null }, 175 .max_size = constraint.max_size, 176 }, root_focus); 177 } 178 179 // how many rows a page up/down jumps. 180 const page_rows = 10; 181 182 pub fn input(self: *View, allocator: std.mem.Allocator, key: inp.Key, root_focus: *Focus) !void { 183 _ = allocator; 184 // up/down (and the scroll wheel) move the selection a row; page up/down 185 // move it a fixed jump. the scroll wheel only reaches here in the TUI — 186 // the web build lets the browser scroll natively and never forwards it. 187 // Enter/clicks on the "next" row are turned into navigation by the host 188 // (crossPageLink). 189 switch (key) { 190 .arrow_down => try self.moveSelection(root_focus, 1), 191 .arrow_up => try self.moveSelection(root_focus, -1), 192 .page_down => try self.moveSelection(root_focus, page_rows), 193 .page_up => try self.moveSelection(root_focus, -page_rows), 194 .mouse => |mouse| switch (mouse.action) { 195 .scroll => |dir| try self.moveSelection(root_focus, if (dir == .up) -1 else 1), 196 else => {}, 197 }, 198 else => {}, 199 } 200 } 201 202 fn moveSelection(self: *View, root_focus: *Focus, delta: isize) !void { 203 const box = self.innerBox(); 204 const keys = box.children.keys(); 205 if (keys.len == 0) return; 206 const cur_id = box.getFocus().child_id orelse return; 207 const cur: isize = @intCast(box.children.getIndex(cur_id) orelse return); 208 const last: isize = @intCast(keys.len - 1); 209 const next: usize = @intCast(std.math.clamp(cur + delta, 0, last)); 210 if (next == @as(usize, @intCast(cur))) return; 211 try root_focus.setFocus(keys[next]); 212 if (box.children.values()[next].rect) |rect| self.scroll.scrollToRect(rect); 213 } 214 215 pub fn clearGrid(self: *View) void { 216 self.scroll.clearGrid(); 217 } 218 219 pub fn getGrid(self: View) ?Grid { 220 return self.scroll.getGrid(); 221 } 222 223 pub fn getFocus(self: *View) *Focus { 224 return self.scroll.getFocus(); 225 } 226 227 pub fn getSelectedIndex(self: View) ?usize { 228 const box = &self.scroll.child.box; 229 const child_id = box.focus.child_id orelse return null; 230 return box.children.getIndex(child_id); 231 } 232 }; 233 234 // the "a:" link for the commits page starting at `start_oid` within `identity`. 235 fn commitsPageLink(page_arena: *std.heap.ArenaAllocator, identity: []const u8, start_oid: []const u8) ![]const u8 { 236 const route = ui.RoutablePage.repoCommitsRoute(identity, start_oid) orelse return error.RouteTooLong; 237 const url = try route.urlAlloc(page_arena); 238 return std.fmt.allocPrint(page_arena.allocator(), "a:{s}", .{url}); 239 } 240