╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ Refs.zig ─────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
263 } 264 } 265 266 // clear the incoming min height; each column's Scroll fills the viewport 267 // height itself (via `fill`), keeping its bar pinned to the edge. 268 try self.box.build(allocator, .{ 269 .min_size = .{ .width = null, .height = null }, 270 .max_size = constraint.max_size, 271 }, root_focus); 272 } 273 274 pub fn input(self: *View, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { 275 _ = allocator; 276 // up/down (and the scroll wheel) move within the active column a row; 277 // page up/down and home/end jump; left/right switch columns, keeping the 278 // same row offset. the parent (Repo) intercepts up at the top row to move 279 // focus to the header. 280 if (inp.rowDelta(key, self.columnRowCount())) |delta| { 281 const scroll = self.activeScroll() orelse return; 282 ui.moveRowFocus(&scroll.child.box, scroll, root_focus, delta); 283 return; 284 } 285 switch (key) { 286 .arrow_left => try self.switchColumn(root_focus, left_col), 287 .arrow_right => try self.switchColumn(root_focus, right_col), 288 else => {}, 289 } 290 } 291 292 // the active column's row count, used as a clamp-to-end delta for home/end. 293 fn columnRowCount(self: *View) isize { 294 const col = self.activeColumn() orelse return 0; 295 return @intCast(col.children.count()); 296 } 297 298 fn activeScroll(self: *View) ?*wgt.Scroll(ui.Widget) { 299 const id = self.box.getFocus().child_id orelse return null; 300 const index = self.box.children.getIndex(id) orelse return null; 301 return columnScroll(&self.box.children.values()[index].widget.box); 302 } 303 304 fn activeColumn(self: *View) ?*wgt.Box(ui.Widget) { 305 return &(self.activeScroll() orelse return null).child.box; 306 } 307 308 fn switchColumn(self: *View, root_focus: *Focus, target: usize) !void { 309 if (target >= self.box.children.count()) return; 310 const cur_col = self.activeColumn() orelse return; 311 const target_scroll = columnScroll(&self.box.children.values()[target].widget.box); 312 const target_col = &target_scroll.child.box; 313 // an empty column has nothing to select; ignore. 314 if (target_col.children.count() == 0) return; 315 // keep the same row offset, clamped to the target column's last row. 316 const cur_id = cur_col.getFocus().child_id orelse return; 317 const cur = cur_col.children.getIndex(cur_id) orelse return; 318 const last = target_col.children.count() - 1; 319 const next = @min(cur, last); 320 root_focus.setFocus(target_col.children.keys()[next]); 321 if (target_col.children.values()[next].rect) |rect| target_scroll.scrollToRect(rect); 322 } 323 324 pub fn clearGrid(self: *View) void { 325 self.box.clearGrid(); 326 } 327 328 pub fn getGrid(self: View) ?Grid { 329 return self.box.getGrid(); 330 } 331 332 pub fn getFocus(self: *View) *Focus { 333 return self.box.getFocus(); 334 } 335 336 // the selected row's offset within the active column (0 = first row), so the 337 // parent knows when up should escape to the header. 338 pub fn getSelectedIndex(self: View) ?usize { 339 var copy = self; 340 const col = copy.activeColumn() orelse return null; 341 const cur_id = col.getFocus().child_id orelse return null; 342 return col.children.getIndex(cur_id); 343 } 344 }; 345 346 // the "a:" link to `kind`'s column paginated to `after` within `identity` 347 // ("owner/name"). the other column resets to its first window. 348 fn windowLink(page_arena: *std.heap.ArenaAllocator, identity: []const u8, kind: ui.RoutablePage.RefKind, after: usize) ![]const u8 { 349 const route = ui.RoutablePage.repoRefsRoute(identity, kind, after) orelse return error.RouteTooLong; 350 const url = try route.urlAlloc(page_arena); 351 return std.fmt.allocPrint(page_arena.allocator(), "a:{s}", .{url}); 352 } 353 354 // the "a:" link to the files tab at ref `name` (a branch or tag) within 355 // `identity` ("owner/name"), at its root directory. the name is percent-encoded 356 // since it can contain a '/'. 357 fn refLink(page_arena: *std.heap.ArenaAllocator, identity: []const u8, kind: ui.RoutablePage.RefKind, name: []const u8) ![]const u8 { 358 const aa = page_arena.allocator(); 359 const ref_or_oid: ui.RoutablePage.RefOrOid = switch (kind) { 360 .branch => .branch, 361 .tag => .tag, 362 }; 363 const value = try ui.urlEncodeRef(aa, name); 364 const route = ui.RoutablePage.repoFilesRoute(identity, ref_or_oid, value, "", 0) orelse return error.RouteTooLong; 365 const url = try route.urlAlloc(page_arena); 366 return std.fmt.allocPrint(aa, "a:{s}", .{url}); 367 } 368