╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ Commits.zig ──────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
591 } 592 593 fn diffOuter(self: *View) *wgt.Box(ui.Widget) { 594 return &self.contentBox().children.values()[diff_index].widget.box; 595 } 596 597 fn diffScroll(self: *View) *wgt.Scroll(ui.Widget) { 598 return &self.diffOuter().children.values()[0].widget.scroll; 599 } 600 601 fn diffInner(self: *View) *wgt.Box(ui.Widget) { 602 return &self.diffScroll().child.box; 603 } 604 605 fn diffActive(self: *View) bool { 606 const content = self.contentBox(); 607 const cid = content.getFocus().child_id orelse return false; 608 return content.children.getIndex(cid) == diff_index; 609 } 610 611 // what the pane shows for the commit at `sel`. the page's content only 612 // applies to the commit it walks from; the rest show their plain diff. 613 fn paneContent(self: *View, sel: usize) Self.Content { 614 return if (sel == 0) self.data.content else .{ .diff = .{} }; 615 } 616 617 // the selected commit's index, or null when the "next" row is selected. 618 fn selectedCommitIndex(self: *View) ?usize { 619 const lb = self.listBox(); 620 const cid = lb.getFocus().child_id orelse return null; 621 const idx = lb.children.getIndex(cid) orelse return null; 622 if (idx >= self.data.commits.len) return null; 623 return idx; 624 } 625 626 pub fn build(self: *View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 627 self.clearGrid(); 628 629 // swap the diff pane to the selected commit when it changes. 630 try self.refreshDiff(allocator); 631 632 // the selected list row shows a border (the focused TextBox upgrades it 633 // to a double border itself); the rest stay borderless. 634 const lb = self.listBox(); 635 for (lb.children.keys(), lb.children.values()) |id, *child| { 636 switch (child.widget) { 637 .text_box => |*tb| tb.options.border_style = if (lb.getFocus().child_id == id) .single else .hidden, 638 else => {}, 639 } 640 } 641 642 // cap the list at list_max_width only while the diff pane fits beside it. 643 // the box drops the diff when the width can't hold both minimums, so when 644 // it's that narrow we lift the cap and let the list fill the whole width. 645 const both_panes_fit = if (constraint.max_size.width) |w| w >= list_max_width + diff_min_width else true; 646 self.contentBox().children.values()[list_index].max_size = if (both_panes_fit) .{ .width = list_max_width, .height = null } else null; 647 648 // stretch the diff pane across the rest of the width so it fills the area 649 // rather than shrinking to its content; its scroll fills the pane. when 650 // too narrow for both, it fills the whole width. 651 const diff_width: usize = if (constraint.max_size.width) |w| 652 (if (both_panes_fit) w - list_max_width else w) 653 else 654 diff_min_width; 655 self.contentBox().children.values()[diff_index].min_size = .{ .width = diff_width, .height = null }; 656 657 // the message is the pane's only wrapping row, and wrapping needs a 658 // bounded width, which the pane's scroll doesn't grant (it scrolls 659 // horizontally too). cap it to the pane, leaving room for the border 660 // and the scrollbar column. re-capped each build so it tracks resizes. 661 for (self.diffInner().children.values()) |*child| switch (child.widget) { 662 .text_box => |text_box| if (text_box.options.wrap_kind == .word) { 663 child.max_size = .{ .width = diff_width -| 3, .height = null }; 664 }, 665 else => {}, 666 }; 667 668 // the web bounds the layout to the browser viewport like the terminal; 669 // each Scroll's web-native mode hands its full content to a real 670 // scrollable element, so we no longer build unbounded here. 671 // 672 // crossing panes only re-selects the content box's child (focusDiff / 673 // focusList); when the window is too narrow to show both, the pane that 674 // held focus is dropped here. the framework recovers focus after the 675 // top-level build by re-deriving it down the selected-child chain, so 676 // there's nothing to fix up afterward. 677 try self.box.build(allocator, constraint, root_focus); 678 } 679 680 fn refreshDiff(self: *View, allocator: std.mem.Allocator) !void { 681 const sel = self.selectedCommitIndex() orelse return; 682 if (self.diffed_index) |d| if (d == sel) return; 683 try self.populateDiff(allocator, sel); 684 self.diffed_index = sel; 685 } 686 687 fn populateDiff(self: *View, allocator: std.mem.Allocator, sel: usize) !void { 688 const commit = self.data.commits[sel]; 689 const inner = self.diffInner(); 690 691 for (inner.children.values()) |*child| child.widget.deinit(allocator); 692 inner.children.clearAndFree(allocator); 693 inner.getFocus().child_id = null; 694 695 switch (self.paneContent(sel)) { 696 .message => { 697 try self.addNavLink(allocator, inner, "← back to diff", commit.oid, 0, ""); 698 try self.addMessageBox(allocator, inner, commit, .whole); 699 }, 700 .diff => |d| { 701 if (d.path.len != 0) { 702 // the file's path, then a row returning to this commit's unfiltered diff. 703 var label = try wgt.TextBox.init(allocator, d.path, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); 704 errdefer label.deinit(allocator); 705 label.getFocus().focusable = true; 706 try inner.children.put(allocator, label.getFocus().id, .{ .widget = .{ .text_box = label }, .rect = null, .min_size = null }); 707 try self.addNavLink(allocator, inner, "← all files", commit.oid, 0, ""); 708 } else { 709 try self.addMessageBox(allocator, inner, commit, .preview); 710 if (commit.author != .unknown) { 711 var tb = try ui.authorBox(allocator, self.session.page_arena, commit.author); 712 errdefer tb.deinit(allocator); 713 try inner.children.put(allocator, tb.getFocus().id, .{ .widget = .{ .text_box = tb }, .rect = null, .min_size = null }); 714 } 715 try self.addViewFilesLink(allocator, inner, commit.oid); 716 } 717 718 // a "previous" row and a "next" row at the bottom reload the page 719 // on the adjacent diff window. 720 if (commit.has_prev) try self.addNavLink(allocator, inner, "← previous", commit.oid, commit.window_start -| diff_page, d.path); 721 for (commit.hunks) |hunk| { 722 if (d.path.len == 0) if (hunk.path) |hunk_path| try self.addPathBox(allocator, inner, hunk_path, commit.oid); 723 try self.addHunkBox(allocator, inner, hunk.lines); 724 } 725 if (commit.has_more) try self.addNavLink(allocator, inner, "next →", commit.oid, commit.window_start + diff_page, d.path); 726 }, 727 } 728 729 // point the pane at its first row so focus recovery can land here. 730 if (inner.children.count() > 0) inner.getFocus().child_id = inner.children.keys()[0]; 731 732 // reset the scroll to the top for the newly-shown commit: directly on the 733 // terminal (the wasm offset), and via a version bump on the web (so the 734 // renderer's scroll id changes and JS drops the preserved position). 735 const sc = self.diffScroll(); 736 sc.x = 0; 737 sc.y = 0; 738 sc.getFocus().version +%= 1; 739 } 740 741 pub fn input(self: *View, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { 742 if (self.headerActive()) { 743 if (key == .arrow_down) { 744 root_focus.setFocus(self.contentBox().getFocus().id); 745 } else { 746 try self.header().input(allocator, key, root_focus); 747 } 748 return; 749 } 750 if (key == .arrow_up and self.contentAtTop() and self.header().focusCloneUrl(root_focus)) return; 751 if (self.diffActive()) { 752 try self.diffInput(key, root_focus); 753 } else { 754 try self.listInput(key, root_focus); 755 } 756 } 757 758 fn listInput(self: *View, key: Key, root_focus: *Focus) !void { 759 // up/down (and the scroll wheel) move the selection a row; page up/down 760 // jump a fixed amount. right/Enter cross into the diff pane. Enter/clicks 761 // on the "next" row become navigation in the host before reaching here. 762 if (inp.rowDelta(key, @intCast(self.listBox().children.count()))) |delta| { 763 ui.widget.moveRowFocus(self.listBox(), self.listScroll(), root_focus, delta); 764 return; 765 } 766 switch (key) { 767 .enter => if (self.selectedCommitIndex() != null) 768 try self.focusDiff(root_focus) 769 else if (self.data.next_start) |next| { 770 if (self.data.location.commitsRoute(.object, next, 0, "")) |route| 771 try self.session.navigate(route); 772 }, 773 .arrow_right => try self.focusDiff(root_focus), 774 else => {}, 775 } 776 } 777 778 fn diffInput(self: *View, key: Key, root_focus: *Focus) !void { 779 const sc = self.diffScroll(); 780 switch (key) { 781 // left scrolls horizontally, then leaves for the list once flush left. 782 .arrow_left => { 783 if (sc.x > 0) { 784 sc.x -= 1; 785 self.diffScroll().clampToContent(); 786 } else try self.focusList(root_focus); 787 }, 788 .arrow_right => { 789 sc.x += 1; 790 self.diffScroll().clampToContent(); 791 }, 792 .arrow_up => try self.moveDiff(root_focus, -1), 793 .arrow_down => try self.moveDiff(root_focus, 1), 794 .page_up => try self.pageDiff(root_focus, -10), 795 .page_down => try self.pageDiff(root_focus, 10), 796 .home => try self.jumpDiff(root_focus, false), 797 .end => try self.jumpDiff(root_focus, true), 798 // Enter / a click on a "previous"/"next" row follow its "a:" link in 799 // the host (it reloads the adjacent window), so they don't reach here. 800 .mouse => |mouse| switch (mouse.action) { 801 .scroll => |dir| try self.moveDiff(root_focus, if (dir == .up) -1 else 1), 802 else => {}, 803 }, 804 else => {}, 805 } 806 } 807 808 // move one step in `delta` (+ down, - up). in the terminal, `delta` is a 809 // line count unless there is a new hunk visible (in which case it is a 810 // hunk count). on the web `delta` is always a hunk count. 811 fn moveDiff(self: *View, root_focus: *Focus, delta: isize) !void { 812 const inner = self.diffInner(); 813 const keys = inner.children.keys(); 814 if (keys.len == 0) return; 815 const cur: usize = if (root_focus.grandchild_id) |g| (inner.children.getIndex(g) orelse 0) else 0; 816 const target = @as(isize, @intCast(cur)) + delta; 817 const in_range = target >= 0 and target < @as(isize, @intCast(keys.len)); 818 819 if (self.session.is_terminal) { 820 if (in_range and self.hunkVisible(@intCast(target))) { 821 root_focus.setFocus(keys[@intCast(target)]); 822 return; 823 } 824 825 const sc = self.diffScroll(); 826 sc.y += delta * 5; // magnify because it's a line count 827 self.diffScroll().clampToContent(); 828 if (in_range and self.hunkVisible(@intCast(target))) { 829 root_focus.setFocus(keys[@intCast(target)]); 830 } 831 } else { 832 if (in_range) { 833 root_focus.setFocus(keys[@intCast(target)]); 834 } 835 } 836 } 837 838 // page a fixed number of lines, then focus the leading visible hunk 839 // (bottom-most when paging down, top-most when paging up). in the 840 // terminal, `delta` is a line count, and on the web it's a hunk count. 841 fn pageDiff(self: *View, root_focus: *Focus, delta: isize) !void { 842 if (self.session.is_terminal) { 843 const sc = self.diffScroll(); 844 sc.y += delta * 5; // magnify because it's a line count 845 self.diffScroll().clampToContent(); 846 try self.focusVisible(root_focus, delta > 0); 847 } else { 848 const inner = self.diffInner(); 849 const keys = inner.children.keys(); 850 if (keys.len == 0) return; 851 const cur: isize = if (root_focus.grandchild_id) |g| @intCast(inner.children.getIndex(g) orelse 0) else 0; 852 const target: usize = @intCast(std.math.clamp(cur + delta, 0, @as(isize, @intCast(keys.len - 1)))); 853 root_focus.setFocus(keys[target]); 854 } 855 } 856 857 // jump to the first or last hunk. on the web the browser scrolls to the 858 // focused hunk; on the terminal pin the scroll to the top/bottom too. 859 fn jumpDiff(self: *View, root_focus: *Focus, to_end: bool) !void { 860 const inner = self.diffInner(); 861 const keys = inner.children.keys(); 862 if (keys.len == 0) return; 863 if (self.session.is_terminal) { 864 const sc = self.diffScroll(); 865 sc.y = if (to_end) std.math.maxInt(isize) else 0; 866 self.diffScroll().clampToContent(); 867 } 868 root_focus.setFocus(if (to_end) keys[keys.len - 1] else keys[0]); 869 } 870 871 // whether hunk `index` is at least partly within the diff viewport, per the 872 // last build's layout rects (content space) and the current scroll offset. 873 fn hunkVisible(self: *View, index: usize) bool { 874 const inner = self.diffInner(); 875 const sc = self.diffScroll(); 876 const vp = sc.grid orelse return false; 877 const r = inner.children.values()[index].rect orelse return false; 878 const top = sc.y; 879 const bottom = sc.y + @as(isize, @intCast(vp.size.height - sc.bar_h)); 880 return (r.y + @as(isize, @intCast(r.size.height))) > top and r.y < bottom; 881 } 882 883 // focus the visible hunk at the scroll's leading edge. `prefer_last` picks 884 // the bottom-most visible (for downward motion), else the top-most. 885 fn focusVisible(self: *View, root_focus: *Focus, prefer_last: bool) !void { 886 const inner = self.diffInner(); 887 const sc = self.diffScroll(); 888 const vp = sc.grid orelse return; 889 const top = sc.y; 890 const bottom = sc.y + @as(isize, @intCast(vp.size.height - sc.bar_h)); 891 var chosen: ?usize = null; 892 for (inner.children.keys(), inner.children.values()) |id, *child| { 893 const r = child.rect orelse continue; // content-space layout rect 894 const r_top = r.y; 895 const r_bot = r.y + @as(isize, @intCast(r.size.height)); 896 if (r_bot <= top or r_top >= bottom) continue; // not visible 897 chosen = id; 898 if (!prefer_last) break; // first visible 899 } 900 if (chosen) |id| root_focus.setFocus(id); 901 } 902 903 // enter the diff pane. the host arrives here on right-arrow or Enter from the 904 // list. a diff with no rows can't be entered. setFocus handles the too-narrow 905 // case where the pane isn't laid out yet (it gets selected, then focused after 906 // the next build, landing on its remembered hunk). 907 fn focusDiff(self: *View, root_focus: *Focus) !void { 908 if (self.diffInner().children.count() == 0) return; 909 root_focus.setFocus(self.diffOuter().getFocus().id); 910 } 911 912 // return to the list. 913 fn focusList(self: *View, root_focus: *Focus) !void { 914 root_focus.setFocus(self.listScroll().getFocus().id); 915 } 916 917 pub fn clearGrid(self: *View) void { 918 self.box.clearGrid(); 919 } 920 921 pub fn getGrid(self: View) ?Grid { 922 return self.box.getGrid(); 923 } 924 925 pub fn getFocus(self: *View) *Focus { 926 return self.box.getFocus(); 927 } 928 929 fn contentAtTop(self: *View) bool { 930 if (self.diffActive()) { 931 const inner = self.diffInner(); 932 const cid = inner.getFocus().child_id orelse return true; 933 const first_focused = inner.children.count() > 0 and cid == inner.children.keys()[0]; 934 return first_focused and self.diffScroll().y == 0; 935 } 936 const lb = self.listBox(); 937 const cid = lb.getFocus().child_id orelse return true; 938 return lb.children.getIndex(cid) == 0; 939 } 940 941 // only the clone-url row sits directly below the repository header. 942 pub fn getSelectedIndex(self: *View) ?usize { 943 if (self.headerActive()) return 0; 944 return if (self.header().hasCloneUrl() or !self.contentAtTop()) 1 else 0; 945 } 946 947 pub fn focusCloneUrl(self: *View, root_focus: *Focus) bool { 948 return self.header().focusCloneUrl(root_focus); 949 } 950 }; 951 952 // the "a:" navigation link for the commits page walking from commit `oid` within 953 // `location`, windowing the selected commit's diff from hunk `start`, filtered 954 // to `path` ("" = every file). 955 fn commitsLink(page_arena: *std.heap.ArenaAllocator, location: ui.RoutablePage.RepoLocation, oid: []const u8, start: usize, path: []const u8) ![]const u8 { 956 const route = location.commitsRoute(.object, oid, start, path) orelse return error.RouteTooLong; 957 const url = try route.toUrl(page_arena); 958 return std.fmt.allocPrint(page_arena.allocator(), "a:{s}", .{url}); 959 } 960 961 // the "a:" link to the files tab at commit `oid` (an object ref), at its root 962 // directory, within `location`. 963 fn filesObjectLink(page_arena: *std.heap.ArenaAllocator, location: ui.RoutablePage.RepoLocation, oid: []const u8) ![]const u8 { 964 const route = location.filesRoute(.object, oid, "", 0) orelse return error.RouteTooLong; 965 const url = try route.toUrl(page_arena); 966 return std.fmt.allocPrint(page_arena.allocator(), "a:{s}", .{url}); 967 } 968 969 // the in-page "ai:" anchor for selecting a commit with its current pane content 970 // and window. the href is only followed with js off. 971 fn commitRowLink(page_arena: *std.heap.ArenaAllocator, location: ui.RoutablePage.RepoLocation, commit: Commit, content: Content) ![]const u8 { 972 const route = (switch (content) { 973 .message => location.commitMessageRoute(.object, commit.oid), 974 .diff => |diff| location.commitsRoute(.object, commit.oid, commit.window_start, diff.path), 975 }) orelse return error.RouteTooLong; 976 const url = try route.toUrl(page_arena); 977 return std.fmt.allocPrint(page_arena.allocator(), "ai:{s}", .{url}); 978 } 979 980 // the "a:" link to the page showing `oid`'s message on its own. 981 fn messageLink(page_arena: *std.heap.ArenaAllocator, location: ui.RoutablePage.RepoLocation, oid: []const u8) ![]const u8 { 982 const route = location.commitMessageRoute(.object, oid) orelse return error.RouteTooLong; 983 const url = try route.toUrl(page_arena); 984 return std.fmt.allocPrint(page_arena.allocator(), "a:{s}", .{url}); 985 } 986 987 // the "<ref_or_oid> <value>" banner shown above the log. 988 pub const Header = struct { 989 content: []const u8, 990 991 // `value` arrives url-encoded, so decode it for display. 992 pub fn init(aa: std.mem.Allocator, ref_or_oid: ui.RoutablePage.RefOrOid, value: []const u8) !Header { 993 const decoded = std.Uri.percentDecodeInPlace(try aa.dupe(u8, value)); 994 return .{ 995 .content = try std.fmt.allocPrint(aa, "{s} {s}", .{ @tagName(ref_or_oid), decoded }), 996 }; 997 } 998 999 pub const View = struct { 1000 box: wgt.Box(ui.Widget), 1001 data: *const Header, 1002 1003 pub fn init(allocator: std.mem.Allocator, data: *const Header, session: *ui.Session, location: ui.RoutablePage.RepoLocation) !Header.View { 1004 var box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .horiz }); 1005 errdefer box.deinit(allocator); 1006 1007 if (!session.data.is_local) { 1008 if (try ui.widget.CopyableText.initClone(allocator, session, location)) |value| { 1009 var clone_url = value; 1010 errdefer clone_url.deinit(allocator); 1011 const min_width = clone_url.minWidth(); 1012 box.getFocus().child_id = clone_url.getFocus().id; 1013 try box.children.put(allocator, clone_url.getFocus().id, .{ .widget = .{ .copyable_text = clone_url }, .rect = null, .min_size = .{ .width = min_width, .height = 3 }, .max_size = .{ .width = min_width, .height = 3 } }); 1014 } 1015 } 1016 1017 var spacer = try ui.widget.Spacer.init(allocator); 1018 errdefer spacer.deinit(allocator); 1019 try box.children.put(allocator, spacer.getFocus().id, .{ .widget = .{ .spacer = spacer }, .rect = null, .min_size = null }); 1020 1021 var text_box = try wgt.TextBox.init(allocator, data.content, .{ .border_style = .hidden, .wrap_kind = .none }); 1022 errdefer text_box.deinit(allocator); 1023 try box.children.put(allocator, text_box.getFocus().id, .{ .widget = .{ .text_box = text_box }, .rect = null, .min_size = null, .flex = .shrink }); 1024 1025 return .{ .box = box, .data = data }; 1026 } 1027 1028 pub fn deinit(self: *Header.View, allocator: std.mem.Allocator) void { 1029 self.box.deinit(allocator); 1030 } 1031 1032 pub fn build(self: *Header.View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 1033 self.clearGrid(); 1034 try self.box.build(allocator, constraint, root_focus); 1035 } 1036 1037 pub fn input(self: *Header.View, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { 1038 const clone_url = self.cloneUrl() orelse return; 1039 try clone_url.input(allocator, key, root_focus); 1040 } 1041 1042 pub fn focusCloneUrl(self: *Header.View, root_focus: *Focus) bool { 1043 const clone_url = self.cloneUrl() orelse return false; 1044 root_focus.setFocus(clone_url.getFocus().id); 1045 return true; 1046 } 1047 1048 pub fn hasCloneUrl(self: *Header.View) bool { 1049 return self.cloneUrl() != null; 1050 } 1051 1052 fn cloneUrl(self: *Header.View) ?*ui.widget.CopyableText { 1053 for (self.box.children.values()) |*child| switch (child.widget) { 1054 .copyable_text => |*copyable_text| return copyable_text, 1055 else => {}, 1056 }; 1057 return null; 1058 } 1059 1060 pub fn clearGrid(self: *Header.View) void { 1061 self.box.clearGrid(); 1062 } 1063 1064 pub fn getGrid(self: Header.View) ?Grid { 1065 return self.box.getGrid(); 1066 } 1067 1068 pub fn getFocus(self: *Header.View) *Focus { 1069 return self.box.getFocus(); 1070 } 1071 }; 1072 }; 1073