╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ ui.zig ───────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
276 if (self.getFocus().child_id) |child_id| { 277 const children = &self.scroll.child.box.children; 278 if (children.getIndex(child_id)) |current_index| { 279 var index = current_index; 280 281 switch (key) { 282 .arrow_up => { 283 index -|= 1; 284 }, 285 .arrow_down => { 286 if (index + 1 < children.count()) { 287 index += 1; 288 } 289 }, 290 .home => { 291 index = 0; 292 }, 293 .end => { 294 if (children.count() > 0) { 295 index = children.count() - 1; 296 } 297 }, 298 .page_up => { 299 if (self.getGrid()) |grid| { 300 const half_count = (grid.size.height / 3) / 2; 301 index -|= half_count; 302 } 303 }, 304 .page_down => { 305 if (self.getGrid()) |grid| { 306 if (children.count() > 0) { 307 const half_count = (grid.size.height / 3) / 2; 308 index = @min(index + half_count, children.count() - 1); 309 } 310 } 311 }, 312 .mouse => |mouse| switch (mouse.action) { 313 .scroll => |dir| switch (dir) { 314 .up => index -|= 1, 315 .down => if (index + 1 < children.count()) { 316 index += 1; 317 }, 318 }, 319 else => {}, 320 }, 321 else => {}, 322 } 323 324 if (index != current_index) { 325 try root_focus.setFocus(children.keys()[index]); 326 self.updateScroll(index); 327 } 328 } 329 } 330 } 331 332 pub fn clearGrid(self: *SelectableList) void { 333 self.scroll.clearGrid(); 334 } 335 336 pub fn getGrid(self: SelectableList) ?Grid { 337 return self.scroll.getGrid(); 338 } 339 340 pub fn getFocus(self: *SelectableList) *Focus { 341 return self.scroll.getFocus(); 342 } 343 344 pub fn getSelectedIndex(self: SelectableList) ?usize { 345 if (self.scroll.child.box.focus.child_id) |child_id| { 346 const children = &self.scroll.child.box.children; 347 return children.getIndex(child_id); 348 } else { 349 return null; 350 } 351 } 352 353 fn updateScroll(self: *SelectableList, index: usize) void { 354 const inner_box = &self.scroll.child.box; 355 if (inner_box.children.values()[index].rect) |rect| { 356 self.scroll.scrollToRect(rect); 357 } 358 } 359 }; 360 361 // horizontal two-pane view: users on the left, the selected user's 362 // repos on the right. selecting a user repopulates the repo pane. 363 const UserRepoView = struct { 364 allocator: std.mem.Allocator, 365 box: wgt.Box(Widget), 366 page: *const pg.UserRepoPage, 367 last_user_index: ?usize, 368 369 const user_list_index: usize = 0; 370 const repo_list_index: usize = 1; 371 372 pub fn init(allocator: std.mem.Allocator, page: *const pg.UserRepoPage) !UserRepoView { 373 var self = blk: { 374 var box = try wgt.Box(Widget).init(allocator, .{ .border_style = null, .rounded_corners = true, .direction = .horiz }); 375 errdefer box.deinit(); 376 377 { 378 var user_list = try SelectableList.init(allocator); 379 errdefer user_list.deinit(); 380 try box.children.put(allocator, user_list.getFocus().id, .{ .widget = .{ .selectable_list = user_list }, .rect = null, .min_size = .{ .width = 30, .height = null } }); 381 } 382 383 { 384 var repo_list = try SelectableList.init(allocator); 385 errdefer repo_list.deinit(); 386 try box.children.put(allocator, repo_list.getFocus().id, .{ .widget = .{ .selectable_list = repo_list }, .rect = null, .min_size = .{ .width = 40, .height = null } }); 387 } 388 389 break :blk UserRepoView{ 390 .allocator = allocator, 391 .box = box, 392 .page = page, 393 .last_user_index = null, 394 }; 395 }; 396 errdefer self.deinit(); 397 398 // populate the user list once at init using a temporary arena 399 // for the formatted lines (setItems dupes them). 400 var arena = std.heap.ArenaAllocator.init(allocator); 401 defer arena.deinit(); 402 const aa = arena.allocator(); 403 404 const user_lines = try aa.alloc([]const u8, page.users.len); 405 for (page.users, 0..) |uwr, i| { 406 user_lines[i] = try std.fmt.allocPrint(aa, "{s} ({s})", .{ uwr.user.name, uwr.user.display_name }); 407 } 408 const user_list = &self.box.children.values()[user_list_index].widget.selectable_list; 409 try user_list.setItems(user_lines); 410 411 self.getFocus().child_id = self.box.children.keys()[user_list_index]; 412 try self.updateRepos(); 413 414 return self; 415 } 416 417 pub fn deinit(self: *UserRepoView) void { 418 self.box.deinit(); 419 } 420 421 pub fn build(self: *UserRepoView, constraint: layout.Constraint, root_focus: *Focus) !void { 422 self.clearGrid(); 423 // refresh the repo list here (rather than in input) so it stays in 424 // sync regardless of how the user selection changed — including 425 // clicks that go straight through setFocus without touching input. 426 // updateRepos is a no-op when the selected user hasn't changed. 427 try self.updateRepos(); 428 try self.box.build(constraint, root_focus); 429 } 430 431 pub fn input(self: *UserRepoView, key: inp.Key, root_focus: *Focus) !void { 432 if (self.getFocus().child_id) |child_id| { 433 if (self.box.children.getIndex(child_id)) |current_index| { 434 const child = &self.box.children.values()[current_index].widget; 435 436 const index = blk: { 437 switch (key) { 438 .arrow_left => { 439 if (current_index == repo_list_index) { 440 break :blk user_list_index; 441 } 442 }, 443 .arrow_right => { 444 if (current_index == user_list_index) { 445 break :blk repo_list_index; 446 } 447 }, 448 .codepoint => { 449 switch (key.codepoint) { 450 13 => { 451 if (current_index == user_list_index) { 452 break :blk repo_list_index; 453 } 454 }, 455 127, '\x1B' => { 456 if (current_index == repo_list_index) { 457 break :blk user_list_index; 458 } 459 }, 460 else => {}, 461 } 462 }, 463 else => {}, 464 } 465 try child.input(key, root_focus); 466 break :blk current_index; 467 }; 468 469 if (index != current_index) { 470 try root_focus.setFocus(self.box.children.keys()[index]); 471 } 472 } 473 } 474 } 475 476 pub fn clearGrid(self: *UserRepoView) void { 477 self.box.clearGrid(); 478 } 479 480 pub fn getGrid(self: UserRepoView) ?Grid { 481 return self.box.getGrid(); 482 } 483 484 pub fn getFocus(self: *UserRepoView) *Focus { 485 return self.box.getFocus(); 486 } 487 488 fn updateRepos(self: *UserRepoView) !void { 489 const user_list = &self.box.children.values()[user_list_index].widget.selectable_list; 490 const repo_list = &self.box.children.values()[repo_list_index].widget.selectable_list; 491 492 const user_index = user_list.getSelectedIndex(); 493 if (user_index == self.last_user_index) return; 494 self.last_user_index = user_index; 495 496 const repos = if (user_index) |i| self.page.users[i].repos else &.{}; 497 498 var arena = std.heap.ArenaAllocator.init(self.allocator); 499 defer arena.deinit(); 500 const aa = arena.allocator(); 501 502 const repo_lines = try aa.alloc([]const u8, repos.len); 503 for (repos, 0..) |repo_event, i| { 504 repo_lines[i] = try std.fmt.allocPrint(aa, "{s} - {s}", .{ repo_event.name, repo_event.description }); 505 } 506 try repo_list.setItems(repo_lines); 507 } 508 }; 509