╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ Issues.zig ───────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
582 return; 583 } 584 switch (key) { 585 .enter, .arrow_right => try self.focusDetail(root_focus), 586 else => {}, 587 } 588 } 589 590 fn detailInput(self: *View, key: Key, root_focus: *Focus) !void { 591 if (self.tagsFocused()) { 592 try self.tagsInput(key, root_focus); 593 } else { 594 try self.descriptionInput(key, root_focus); 595 } 596 } 597 598 fn descriptionInput(self: *View, key: Key, root_focus: *Focus) !void { 599 const sc = self.detailScroll(); 600 switch (key) { 601 .arrow_left => return self.focusList(root_focus), 602 // once the scroll can't move further, cross into the tags (or the 603 // sub header tabs when the issue has none). 604 .arrow_up => { 605 const before = sc.y; 606 sc.y -= 1; 607 sc.clampToContent(); 608 if (sc.y == before) { 609 if (self.tagFlow() != null) try self.focusTags(root_focus) else self.focusSubHeader(root_focus); 610 } 611 return; 612 }, 613 .arrow_down => sc.y += 1, 614 .page_up => sc.y -= 10, 615 .page_down => sc.y += 10, 616 .home => sc.y = 0, 617 .end => sc.y = std.math.maxInt(isize), 618 .mouse => |mouse| switch (mouse.action) { 619 .scroll => |dir| sc.y += if (dir == .up) @as(isize, -1) else 1, 620 else => {}, 621 }, 622 else => return, 623 } 624 sc.clampToContent(); 625 } 626 627 // arrow keys move the tag selection; at the flow's edges focus crosses to 628 // the neighboring widgets. 629 fn tagsInput(self: *View, key: Key, root_focus: *Focus) !void { 630 const tf = self.tagFlow() orelse return; 631 const cid = tf.focus.child_id orelse return; 632 const cur = tf.indexOfFocusId(cid) orelse return; 633 const count = tf.text_boxes.items.len; 634 const sc = self.detailScroll(); 635 switch (key) { 636 .arrow_left => if (cur > 0) self.focusTag(tf, root_focus, cur - 1) else try self.focusList(root_focus), 637 .arrow_right => if (cur + 1 < count) self.focusTag(tf, root_focus, cur + 1), 638 .arrow_up => if (tf.rowStep(cur, false)) |i| self.focusTag(tf, root_focus, i) else self.focusSubHeader(root_focus), 639 .arrow_down => if (tf.rowStep(cur, true)) |i| self.focusTag(tf, root_focus, i) else try self.focusDescription(root_focus), 640 .home => self.focusTag(tf, root_focus, 0), 641 .end => self.focusTag(tf, root_focus, count - 1), 642 .mouse => |mouse| switch (mouse.action) { 643 .scroll => |dir| { 644 sc.y += if (dir == .up) @as(isize, -1) else 1; 645 sc.clampToContent(); 646 }, 647 else => {}, 648 }, 649 else => {}, 650 } 651 } 652 653 const tags_child_index: usize = 0; 654 655 fn tagFlow(self: *View) ?*ui.TagFlow { 656 const inner = self.detailInner(); 657 if (inner.children.count() == 0) return null; 658 return switch (inner.children.values()[tags_child_index].widget) { 659 .tag_flow => |*tf| tf, 660 else => null, 661 }; 662 } 663 664 fn tagsFocused(self: *View) bool { 665 const inner = self.detailInner(); 666 const cid = inner.getFocus().child_id orelse return false; 667 return inner.children.getIndex(cid) == tags_child_index and self.tagFlow() != null; 668 } 669 670 fn focusTag(self: *View, tf: *ui.TagFlow, root_focus: *Focus, index: usize) void { 671 root_focus.setFocus(tf.text_boxes.items[index].getFocus().id); 672 // keep the tag visible on the terminal: its rect offset by the flow's 673 // position in the pane. 674 if (self.session.is_terminal and index < tf.rects.items.len) { 675 if (self.detailInner().children.values()[tags_child_index].rect) |flow_rect| { 676 var rect = tf.rects.items[index]; 677 rect.x += flow_rect.x; 678 rect.y += flow_rect.y; 679 self.detailScroll().scrollToRect(rect); 680 } 681 } 682 } 683 684 fn focusTags(self: *View, root_focus: *Focus) !void { 685 const tf = self.tagFlow() orelse return; 686 if (tf.text_boxes.items.len == 0) return; 687 const cid = tf.focus.child_id orelse tf.text_boxes.items[0].getFocus().id; 688 const index = tf.indexOfFocusId(cid) orelse 0; 689 self.focusTag(tf, root_focus, index); 690 } 691 692 fn focusDescription(self: *View, root_focus: *Focus) !void { 693 if (self.description_id) |id| root_focus.setFocus(id); 694 } 695 696 // enter the detail pane. an empty pane (no issues) can't be entered. 697 fn focusDetail(self: *View, root_focus: *Focus) !void { 698 if (self.detailInner().children.count() == 0) return; 699 root_focus.setFocus(self.detailOuter().getFocus().id); 700 } 701 702 // return to the list. 703 fn focusList(self: *View, root_focus: *Focus) !void { 704 root_focus.setFocus(self.listScroll().getFocus().id); 705 } 706 707 // cross to the sub header tabs above the split. 708 fn focusSubHeader(self: *View, root_focus: *Focus) void { 709 root_focus.setFocus(self.box.children.keys()[sub_header_index]); 710 } 711 712 pub fn clearGrid(self: *View) void { 713 self.box.clearGrid(); 714 } 715 716 pub fn getGrid(self: View) ?Grid { 717 return self.box.getGrid(); 718 } 719 720 pub fn getFocus(self: *View) *Focus { 721 return self.box.getFocus(); 722 } 723 724 // for the parent's "scroll up at the top jumps to the header" check: at the 725 // top only while the sub header tabs hold focus, so up from the split 726 // crosses into the tabs first. 727 pub fn getSelectedIndex(self: *View) ?usize { 728 return if (self.subHeaderActive()) 0 else 1; 729 } 730 }; 731 732 // the "a:" navigation link for the issues page filtered to the url-encoded 733 // `tag` and rooted at issue `id` within `identity` ("owner/name"). 734 fn issuesLink(page_arena: *std.heap.ArenaAllocator, identity: []const u8, tag: []const u8, id: []const u8) ![]const u8 { 735 const route = ui.RoutablePage.repoIssuesRoute(identity, tag, id) orelse return error.RouteTooLong; 736 const url = try route.urlAlloc(page_arena); 737 return std.fmt.allocPrint(page_arena.allocator(), "a:{s}", .{url}); 738 } 739 740 // the in-page "ai:" anchor for selecting issue `id` in `identity`'s list; the 741 // href is only followed with js off. 742 fn issueRowLink(page_arena: *std.heap.ArenaAllocator, identity: []const u8, id: []const u8) ![]const u8 { 743 const route = ui.RoutablePage.repoIssuesRoute(identity, "", id) orelse return error.RouteTooLong; 744 const url = try route.urlAlloc(page_arena); 745 return std.fmt.allocPrint(page_arena.allocator(), "ai:{s}", .{url}); 746 } 747 748 // the "a:" link to the issues list filtered to `tag` (raw; encoded here). 749 fn tagLink(page_arena: *std.heap.ArenaAllocator, identity: []const u8, tag: []const u8) ![]const u8 { 750 const encoded = try ui.urlEncodeRef(page_arena.allocator(), tag); 751 const route = ui.RoutablePage.repoIssuesRoute(identity, encoded, "") orelse return error.RouteTooLong; 752 const url = try route.urlAlloc(page_arena); 753 return std.fmt.allocPrint(page_arena.allocator(), "a:{s}", .{url}); 754 } 755