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