╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ Commits.zig ──────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
510 fn diffInput(self: *View, key: inp.Key, root_focus: *Focus) !void { 511 const sc = self.diffScroll(); 512 switch (key) { 513 // left scrolls horizontally, then leaves for the list once flush left. 514 .arrow_left => { 515 if (sc.x > 0) { 516 sc.x -= 1; 517 self.clampDiffScroll(); 518 } else try self.focusList(root_focus); 519 }, 520 .arrow_right => { 521 sc.x += 1; 522 self.clampDiffScroll(); 523 }, 524 .arrow_up => try self.moveDiff(root_focus, -1), 525 .arrow_down => try self.moveDiff(root_focus, 1), 526 .page_up => try self.pageDiff(root_focus, -page_rows), 527 .page_down => try self.pageDiff(root_focus, page_rows), 528 // Enter / a click on the "load more" row follow its "a:" link in the 529 // host (it reloads the page with more hunks), so they don't reach here. 530 .mouse => |mouse| switch (mouse.action) { 531 .scroll => |dir| try self.moveDiff(root_focus, if (dir == .up) -1 else 1), 532 else => {}, 533 }, 534 else => {}, 535 } 536 } 537 538 // move focus one hunk in `dir` (+1 down, -1 up). if the adjacent hunk is 539 // already visible, just move focus to it — don't scroll. only when it isn't 540 // visible yet do we scroll one line toward it (and focus it once it appears), 541 // so scrolling never skips past an on-screen hunk. when nothing fits to 542 // scroll (e.g. the web, where the browser scrolls the page), the clamp keeps 543 // the offset put and focus simply steps to the adjacent hunk. 544 fn moveDiff(self: *View, root_focus: *Focus, dir: isize) !void { 545 const inner = self.diffInner(); 546 const keys = inner.children.keys(); 547 if (keys.len == 0) return; 548 const cur: usize = if (root_focus.grandchild_id) |g| (inner.children.getIndex(g) orelse 0) else 0; 549 const target = @as(isize, @intCast(cur)) + dir; 550 const in_range = target >= 0 and target < @as(isize, @intCast(keys.len)); 551 552 if (in_range and self.hunkVisible(@intCast(target))) { 553 try root_focus.setFocus(keys[@intCast(target)]); 554 return; 555 } 556 557 const sc = self.diffScroll(); 558 sc.y += dir; 559 self.clampDiffScroll(); 560 if (in_range and self.hunkVisible(@intCast(target))) 561 try root_focus.setFocus(keys[@intCast(target)]); 562 } 563 564 // page a fixed number of lines, then focus the leading visible hunk 565 // (bottom-most when paging down, top-most when paging up). 566 fn pageDiff(self: *View, root_focus: *Focus, delta: isize) !void { 567 const sc = self.diffScroll(); 568 sc.y += delta; 569 self.clampDiffScroll(); 570 try self.focusVisible(root_focus, delta > 0); 571 } 572 573 // whether hunk `index` is at least partly within the diff viewport, per the 574 // last build's layout rects (content space) and the current scroll offset. 575 fn hunkVisible(self: *View, index: usize) bool { 576 const inner = self.diffInner(); 577 const sc = self.diffScroll(); 578 const vp = sc.grid orelse return false; 579 const r = inner.children.values()[index].rect orelse return false; 580 const top = sc.y; 581 const bottom = sc.y + @as(isize, @intCast(vp.size.height)); 582 return (r.y + @as(isize, @intCast(r.size.height))) > top and r.y < bottom; 583 } 584 585 // focus the visible hunk at the scroll's leading edge. `prefer_last` picks 586 // the bottom-most visible (for downward motion), else the top-most. 587 fn focusVisible(self: *View, root_focus: *Focus, prefer_last: bool) !void { 588 const inner = self.diffInner(); 589 const sc = self.diffScroll(); 590 const vp = sc.grid orelse return; 591 const top = sc.y; 592 const bottom = sc.y + @as(isize, @intCast(vp.size.height)); 593 var chosen: ?usize = null; 594 for (inner.children.keys(), inner.children.values()) |id, *child| { 595 const r = child.rect orelse continue; // content-space layout rect 596 const r_top = r.y; 597 const r_bot = r.y + @as(isize, @intCast(r.size.height)); 598 if (r_bot <= top or r_top >= bottom) continue; // not visible 599 chosen = id; 600 if (!prefer_last) break; // first visible 601 } 602 if (chosen) |id| try root_focus.setFocus(id); 603 } 604 605 // keep the diff scroll within its content, using the last build's grids. the 606 // scroll bar's reserved column/row isn't part of the content viewport, so 607 // exclude it (as scrollToRect does) or the last column/row stays unreachable. 608 fn clampDiffScroll(self: *View) void { 609 const sc = self.diffScroll(); 610 const vp = sc.grid orelse return; 611 const content = sc.child.box.grid orelse return; 612 const view_w = vp.size.width - sc.bar_w; 613 const view_h = vp.size.height - sc.bar_h; 614 const max_y: isize = if (content.size.height > view_h) @intCast(content.size.height - view_h) else 0; 615 const max_x: isize = if (content.size.width > view_w) @intCast(content.size.width - view_w) else 0; 616 sc.y = std.math.clamp(sc.y, 0, max_y); 617 sc.x = std.math.clamp(sc.x, 0, max_x); 618 } 619 620 // enter the diff pane by focusing the hunk that was focused last (the box's 621 // child_id remembers it across the trip to the commit list); fall back to the 622 // first hunk on a fresh diff. the host arrives here on right-arrow or Enter 623 // from the list. a diff with no hunks can't be entered. 624 fn focusDiff(self: *View, root_focus: *Focus) !void { 625 const inner = self.diffInner(); 626 if (inner.children.count() == 0) return; 627 const target = inner.getFocus().child_id orelse inner.children.keys()[0]; 628 try root_focus.setFocus(target); 629 if (root_focus.grandchild_id == target) return; 630 // the diff pane wasn't laid out last build (too narrow to show beside 631 // the list), so its hunks aren't in the focus tree yet. select the pane 632 // at the box level, and the hunk will be focused after the next build. 633 self.box.getFocus().child_id = self.diffOuter().getFocus().id; 634 } 635 636 fn focusList(self: *View, root_focus: *Focus) !void { 637 const lb = self.listBox(); 638 const id = lb.getFocus().child_id orelse (if (lb.children.count() > 0) lb.children.keys()[0] else return); 639 try root_focus.setFocus(id); 640 } 641 642 fn moveSelection(self: *View, root_focus: *Focus, delta: isize) !void { 643 const lb = self.listBox(); 644 const keys = lb.children.keys(); 645 if (keys.len == 0) return; 646 const cur_id = lb.getFocus().child_id orelse return; 647 const cur: isize = @intCast(lb.children.getIndex(cur_id) orelse return); 648 const last: isize = @intCast(keys.len - 1); 649 const next: usize = @intCast(std.math.clamp(cur + delta, 0, last)); 650 if (next == @as(usize, @intCast(cur))) return; 651 try root_focus.setFocus(keys[next]); 652 if (lb.children.values()[next].rect) |rect| self.listScroll().scrollToRect(rect); 653 } 654 655 pub fn clearGrid(self: *View) void { 656 self.box.clearGrid(); 657 } 658 659 pub fn getGrid(self: View) ?Grid { 660 return self.box.getGrid(); 661 } 662 663 pub fn getFocus(self: *View) *Focus { 664 return self.box.getFocus(); 665 } 666 667 // for the parent's "scroll up at the top jumps to the header" check. in the 668 // diff pane that means the first hunk is focused and the diff can't scroll up 669 // any further; otherwise up-arrow stays in the pane (scrolling or moving 670 // between hunks). when the list holds focus, report its selection directly. 671 pub fn getSelectedIndex(self: *View) ?usize { 672 if (self.diffActive()) { 673 const inner = self.diffInner(); 674 const cid = inner.getFocus().child_id orelse return 1; 675 const first_focused = inner.children.count() > 0 and cid == inner.children.keys()[0]; 676 return if (first_focused and self.diffScroll().y == 0) 0 else 1; 677 } 678 const lb = self.listBox(); 679 const cid = lb.getFocus().child_id orelse return null; 680 return lb.children.getIndex(cid); 681 } 682 }; 683 684 // the "a:" navigation link for the commits page starting at `start_oid` within 685 // `identity`, showing the selected commit's first `after` hunks (0 = default). 686 fn commitsLink(page_arena: *std.heap.ArenaAllocator, identity: []const u8, start_oid: []const u8, after: usize) ![]const u8 { 687 const route = ui.RoutablePage.repoCommitsRoute(identity, start_oid, after) orelse return error.RouteTooLong; 688 const url = try route.urlAlloc(page_arena); 689 return std.fmt.allocPrint(page_arena.allocator(), "a:{s}", .{url}); 690 } 691