╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─────────────╮ │src/ui/Repo/Issues.zig│ ╰──────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │724 } │ │725 │ │726 // for the parent's "scroll up at the top jumps to the header" check: at the│ │729 - // top only while the sub header tabs hold focus, so up from the split │ │727 + // top only while the header tabs hold focus, so up from the split │ │728 // crosses into the tabs first. │ │729 pub fn getSelectedIndex(self: *View) ?usize { │ │732 - return if (self.subHeaderActive()) 0 else 1; │ │730 + return if (self.headerActive()) 0 else 1; │ │731 } │ │732 }; │ │733 │ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │755 return std.fmt.allocPrint(page_arena.allocator(), "a:{s}", .{url}); │ │756 } │ │757 │ │758 +// tabs switching between the issues page's views. │ │759 +pub const Header = struct { │ │760 + box: wgt.Box(ui.Widget), │ │761 + tab_ids: std.AutoArrayHashMapUnmanaged(usize, void), │ │762 + │ │763 + // `tag` is the url-encoded tag filter ("" = unfiltered). │ │764 + pub fn init(allocator: std.mem.Allocator, session: *ui.Session, identity: []const u8, count: usize, tag: []const u8) !Header { │ │765 + var box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = .hidden, .rounded_corners = true, .direction = .horiz }); │ │766 + errdefer box.deinit(allocator); │ │767 + │ │768 + var tab_ids: std.AutoArrayHashMapUnmanaged(usize, void) = .empty; │ │769 + errdefer tab_ids.deinit(allocator); │ │770 + │ │771 + const aa = session.page_arena.allocator(); │ │772 + │ │773 + const results_route = ui.RoutablePage.repoIssuesRoute(identity, tag, "") orelse return error.RouteTooLong; │ │774 + const results_link = try std.fmt.allocPrint(aa, "ai:{s}", .{try results_route.urlAlloc(session.page_arena)}); │ │775 + const tags_route = ui.RoutablePage.repoIssuesTagsRoute(identity) orelse return error.RouteTooLong; │ │776 + const tags_link = try std.fmt.allocPrint(aa, "ai:{s}", .{try tags_route.urlAlloc(session.page_arena)}); │ │777 + │ │778 + // results tab, labeled with the listing's issue count │ │779 + { │ │780 + const label = try std.fmt.allocPrint(aa, "results ({d})", .{count}); │ │781 + var text_box = try wgt.TextBox(ui.Widget).init(allocator, label, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none });│ │782 + errdefer text_box.deinit(allocator); │ │783 + text_box.getFocus().focusable = true; │ │784 + text_box.getFocus().kind = .{ .custom = results_link }; │ │785 + try tab_ids.put(allocator, text_box.getFocus().id, {}); │ │786 + try box.children.put(allocator, text_box.getFocus().id, .{ │ │787 + .widget = .{ .text_box = text_box }, │ │788 + .rect = null, │ │789 + .min_size = .{ .width = label.len + 2, .height = null }, │ │790 + }); │ │791 + } │ │792 + │ │793 + // tags tab, labeled with the active tag filter │ │794 + { │ │795 + const label = if (tag.len == 0) "tags" else blk: { │ │796 + const decoded = std.Uri.percentDecodeInPlace(try aa.dupe(u8, tag)); │ │797 + break :blk try std.fmt.allocPrint(aa, "tags ({s})", .{decoded}); │ │798 + }; │ │799 + var text_box = try wgt.TextBox(ui.Widget).init(allocator, label, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none });│ │800 + errdefer text_box.deinit(allocator); │ │801 + text_box.getFocus().focusable = true; │ │802 + text_box.getFocus().kind = .{ .custom = tags_link }; │ │803 + try tab_ids.put(allocator, text_box.getFocus().id, {}); │ │804 + try box.children.put(allocator, text_box.getFocus().id, .{ │ │805 + .widget = .{ .text_box = text_box }, │ │806 + .rect = null, │ │807 + .min_size = .{ .width = label.len + 2, .height = null }, │ │808 + }); │ │809 + } │ │810 + │ │811 + var self = Header{ .box = box, .tab_ids = tab_ids }; │ │812 + // the tab matching the incoming route's view is selected initially. │ │813 + const selected_index: usize = switch (session.data.current_page) { │ │814 + .repo_issues => |i| switch (i.view) { │ │815 + .results => 0, │ │816 + .tags => 1, │ │817 + }, │ │818 + else => 0, │ │819 + }; │ │820 + self.getFocus().child_id = self.tab_ids.keys()[selected_index]; │ │821 + return self; │ │822 + } │ │823 + │ │824 + pub fn deinit(self: *Header, allocator: std.mem.Allocator) void { │ │825 + self.box.deinit(allocator); │ │826 + self.tab_ids.deinit(allocator); │ │827 + } │ │828 + │ │829 + pub fn build(self: *Header, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { │ │830 + self.clearGrid(); │ │831 + // only the selected tab shows its border │ │832 + for (self.box.children.keys(), self.box.children.values()) |id, *child| { │ │833 + switch (child.widget) { │ │834 + .text_box => |*tb| tb.options.border_style = if (self.getFocus().child_id == id) .single else .hidden, │ │835 + else => {}, │ │836 + } │ │837 + } │ │838 + try self.box.build(allocator, constraint, root_focus); │ │839 + } │ │840 + │ │841 + pub fn input(self: *Header, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { │ │842 + _ = allocator; │ │843 + const current_tab = self.currentTabIndex() orelse return; │ │844 + if (inp.moveTab(key, current_tab, self.tab_ids.count())) |new_tab| { │ │845 + root_focus.setFocus(self.tab_ids.keys()[new_tab]); │ │846 + } │ │847 + } │ │848 + │ │849 + pub fn clearGrid(self: *Header) void { │ │850 + self.box.clearGrid(); │ │851 + } │ │852 + │ │853 + pub fn getGrid(self: Header) ?Grid { │ │854 + return self.box.getGrid(); │ │855 + } │ │856 + │ │857 + pub fn getFocus(self: *Header) *Focus { │ │858 + return self.box.getFocus(); │ │859 + } │ │860 + │ │861 + pub fn getSelectedIndex(self: Header) ?usize { │ │862 + return self.currentTabIndex(); │ │863 + } │ │864 + │ │865 + fn currentTabIndex(self: Header) ?usize { │ │866 + const child_id = self.box.focus.child_id orelse return null; │ │867 + return self.tab_ids.getIndex(child_id); │ │868 + } │ │869 +}; │ │870 + │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ removed ───────────────────────╮ │src/ui/Repo/Issues/SubHeader.zig│ ╰────────────────────────────────╯