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