╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Patches.zig│ ╰───────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1008 │ │1009 // tabs switching between the patches page's views │ │1010 pub fn initHeader(allocator: std.mem.Allocator, session: *ui.Session, data: *const Self) !Header {│ │1004 - var header = try Header.init(allocator); │ │1011 + var header = try Header.init(allocator, session, data.search); │ │1012 errdefer header.deinit(allocator); │ │1013 const aa = session.page_arena.allocator(); │ │1014 const selected_index = View.viewIndex(data.view); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1017 // a list tab per status, labeled with its listing's patch count │ │1018 const status_labels = [_][]const u8{ open_tab_label, closed_tab_label, merged_tab_label }; │ │1019 for ([_]Status{ .open, .closed, .merged }, status_labels, 0..) |status, status_label, index| { │ │1013 - const route = ui.RoutablePage.repoPatchesRoute(data.identity, status, data.tag, "") orelse return error.RouteTooLong; │ │1020 + const route = try thread.searchRoute(ui.RoutablePage.repoPatchesRoute(data.identity, status, data.tag, ""), data.search);│ │1021 const link = try ui.inPageTabLink(session, route, page_selected and selected_index == index); │ │1022 var label_buf: [64]u8 = undefined; │ │1016 - const label = try std.fmt.bufPrint(&label_buf, "{s} ({d})", .{ status_label, data.window(status).count }); │ │1023 + const label = try thread.countLabel(&label_buf, status_label, data.window(status).*); │ │1024 try header.addTab(allocator, label, link, index); │ │1025 } │ │1026 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1027 // tags tab, labeled with the active tag filter │ │1028 { │ │1022 - const tags_route = ui.RoutablePage.repoThreadTagsRoute(.patch, data.identity, data.tag) orelse return error.RouteTooLong; │ │1029 + const tags_route = try thread.searchRoute(ui.RoutablePage.repoThreadTagsRoute(.patch, data.identity, data.tag), data.search);│ │1030 const tags_link = try ui.inPageTabLink(session, tags_route, page_selected and selected_index == View.viewIndex(.tags)); │ │1031 const label = if (data.tag.len == 0) tags_tab_label else blk: { │ │1032 const decoded = std.Uri.percentDecodeInPlace(try aa.dupe(u8, data.tag)); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────╮ │src/ui/widget.zig│ ╰─────────────────╯ ╭────────────────────────────────────────────────────╮ │38 user_header: ui.User.Header.View, │ │39 repo_header: ui.Repo.Header.View, │ │40 fork_header: ui.Fork.Header.View, │ │41 + search_box: SearchBox, │ │42 search_header: SearchHeader, │ │43 repo_issues_header: ui.Repo.Issues.Header, │ │44 repo_patches_header: ui.Repo.Patches.Header,│ ╰────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │655 │ │656 pub fn getFocus(self: *Spacer) *Focus { │ │657 return self.focus; │ │658 + } │ │659 +}; │ │660 + │ │661 +// a sub-header's search box: a labeled text input whose enter the owning view │ │662 +// turns into a navigation. shared by the files, commits and thread tabs. │ │663 +pub const SearchBox = struct { │ │664 + text_input: wgt.TextInput, │ │665 + session: *ui.Session, │ │666 + │ │667 + // the room the box asks its row for │ │668 + pub const min_size: layout.MaybeSize = .{ .width = 20, .height = 3 }; │ │669 + │ │670 + pub fn init(allocator: std.mem.Allocator, session: *ui.Session, label: []const u8, name: []const u8, text_value: ?[]const u8) !SearchBox { │ │671 + var text_input = try wgt.TextInput.init(allocator, .{ .label = label, .name = name, .rounded_corners = true, .visible_width = 18, .render_content = session.is_terminal });│ │672 + errdefer text_input.deinit(allocator); │ │673 + text_input.getFocus().mode = .all; │ │674 + if (text_value) |value| try text_input.setContent(allocator, value); │ │675 + return .{ .text_input = text_input, .session = session }; │ │676 + } │ │677 + │ │678 + pub fn deinit(self: *SearchBox, allocator: std.mem.Allocator) void { │ │679 + self.text_input.deinit(allocator); │ │680 + } │ │681 + │ │682 + pub fn build(self: *SearchBox, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { │ │683 + self.text_input.options.bottom_label = if (root_focus.grandchild_id == self.getFocus().id) " press enter " else ""; │ │684 + try self.text_input.build(allocator, constraint, root_focus); │ │685 + if (!self.session.is_terminal) { │ │686 + try self.session.text_inputs.put(self.session.arena.allocator(), self.getFocus().id, &self.text_input); │ │687 + } │ │688 + } │ │689 + │ │690 + pub fn input(self: *SearchBox, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { │ │691 + try self.text_input.input(allocator, key, root_focus); │ │692 + } │ │693 + │ │694 + // the typed text; the caller owns it │ │695 + pub fn text(self: *SearchBox, allocator: std.mem.Allocator) ![]u8 { │ │696 + return self.text_input.text(allocator); │ │697 + } │ │698 + │ │699 + // whether the cursor sits past the last character, where right-arrow leaves │ │700 + // the box rather than moving within it │ │701 + pub fn atEnd(self: *SearchBox) bool { │ │702 + return self.text_input.cursor == self.text_input.content.items.len; │ │703 + } │ │704 + │ │705 + pub fn clearGrid(self: *SearchBox) void { │ │706 + self.text_input.clearGrid(); │ │707 + } │ │708 + │ │709 + pub fn getGrid(self: SearchBox) ?Grid { │ │710 + return self.text_input.getGrid(); │ │711 + } │ │712 + │ │713 + pub fn getFocus(self: *SearchBox) *Focus { │ │714 + return self.text_input.getFocus(); │ │715 } │ │716 }; │ │717 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │736 │ │737 // the search box only appears where an index can serve it. │ │738 if (show_input) { │ │681 - var text_input = try wgt.TextInput.init(allocator, .{ .label = label, .name = name, .rounded_corners = true, .visible_width = 18, .render_content = session.is_terminal });│ │682 - errdefer text_input.deinit(allocator); │ │683 - text_input.getFocus().mode = .all; │ │684 - if (text) |value| try text_input.setContent(allocator, value); │ │685 - box.getFocus().child_id = text_input.getFocus().id; │ │686 - try box.children.put(allocator, text_input.getFocus().id, .{ .widget = .{ .text_input = text_input }, .rect = null, .min_size = .{ .width = 20, .height = 3 } }); │ │739 + var search_box = try SearchBox.init(allocator, session, label, name, text); │ │740 + errdefer search_box.deinit(allocator); │ │741 + box.getFocus().child_id = search_box.getFocus().id; │ │742 + try box.children.put(allocator, search_box.getFocus().id, .{ .widget = .{ .search_box = search_box }, .rect = null, .min_size = SearchBox.min_size }); │ │743 } │ │744 │ │745 // the spacer pushes the clone url to the right edge. │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │768 │ │769 pub fn build(self: *SearchHeader, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { │ │770 self.clearGrid(); │ │715 - if (self.searchInput()) |text_input| { │ │716 - text_input.options.bottom_label = if (root_focus.grandchild_id == text_input.getFocus().id) " press enter " else "";│ │717 - } │ │771 try self.box.build(allocator, constraint, root_focus); │ │719 - if (!self.session.is_terminal) { │ │720 - if (self.searchInput()) |text_input| { │ │721 - try self.session.text_inputs.put(self.session.arena.allocator(), text_input.getFocus().id, text_input); │ │722 - } │ │723 - } │ │772 } │ │773 │ │774 pub fn input(self: *SearchHeader, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮ │775 const current = self.box.getFocus().child_id orelse return; │ │728 - if (self.searchInput()) |text_input| { │ │729 - if (current == text_input.getFocus().id) { │ │730 - if (key == .arrow_right and text_input.cursor == text_input.content.items.len) {│ │776 + if (self.searchBox()) |search_box| { │ │777 + if (current == search_box.getFocus().id) { │ │778 + // tab, and right-arrow past the last character, step to the │ │779 + // clone url beside the box │ │780 + if (key == .tab or (key == .arrow_right and search_box.atEnd())) { │ │781 if (self.cloneUrl()) |clone_url| { │ │782 root_focus.setFocus(clone_url.getFocus().id); │ │783 return; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────╮ │784 } │ │785 } │ │736 - return text_input.input(allocator, key, root_focus);│ │786 + return search_box.input(allocator, key, root_focus);│ │787 } │ │788 } │ │789 const clone_url = self.cloneUrl() orelse return; │ ╰─────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────╮ │740 - if (key == .arrow_left and clone_url.selected == 0) { │ │741 - if (self.searchInput()) |text_input| { │ │742 - root_focus.setFocus(text_input.getFocus().id); │ │790 + if (key == .back_tab or (key == .arrow_left and clone_url.selected == 0)) {│ │791 + if (self.searchBox()) |search_box| { │ │792 + root_focus.setFocus(search_box.getFocus().id); │ │793 return; │ │794 } │ │795 } │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯