╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ────────╮ │src/ui/widget.zig│ ╰─────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │654 │ │655 pub fn getFocus(self: *Spacer) *Focus { │ │656 return self.focus; │ │657 + } │ │658 +}; │ │659 + │ │660 +// a tab's sub-header: an optional search box on the left, then a spacer, then │ │661 +// the clone url on the right. the owning view turns a submitted query into a │ │662 +// route, so route building stays with the tab that knows its own url. │ │663 +pub const SearchHeader = struct { │ │664 + box: wgt.Box(Widget), │ │665 + session: *ui.Session, │ │666 + │ │667 + pub fn init( │ │668 + allocator: std.mem.Allocator, │ │669 + session: *ui.Session, │ │670 + location: ui.RoutablePage.RepoLocation, │ │671 + label: []const u8, │ │672 + name: []const u8, │ │673 + text: ?[]const u8, │ │674 + show_input: bool, │ │675 + ) !SearchHeader { │ │676 + var box = try wgt.Box(Widget).init(allocator, .{ .border_style = null, .direction = .horiz }); │ │677 + errdefer box.deinit(allocator); │ │678 + │ │679 + // the search box only appears where an index can serve it. │ │680 + 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 } }); │ │687 + } │ │688 + │ │689 + // the spacer pushes the clone url to the right edge. │ │690 + { │ │691 + var spacer = try Spacer.init(allocator); │ │692 + errdefer spacer.deinit(allocator); │ │693 + try box.children.put(allocator, spacer.getFocus().id, .{ .widget = .{ .spacer = spacer }, .rect = null, .min_size = null }); │ │694 + } │ │695 + │ │696 + if (session.data.host_kind == .server) { │ │697 + if (try CopyableText.initClone(allocator, session, location)) |value| { │ │698 + var clone_url = value; │ │699 + errdefer clone_url.deinit(allocator); │ │700 + const min_width = clone_url.minWidth(); │ │701 + if (box.getFocus().child_id == null) box.getFocus().child_id = clone_url.getFocus().id; │ │702 + 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 } });│ │703 + } │ │704 + } │ │705 + │ │706 + return .{ .box = box, .session = session }; │ │707 + } │ │708 + │ │709 + pub fn deinit(self: *SearchHeader, allocator: std.mem.Allocator) void { │ │710 + self.box.deinit(allocator); │ │711 + } │ │712 + │ │713 + pub fn build(self: *SearchHeader, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { │ │714 + 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 + } │ │718 + 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 + } │ │724 + } │ │725 + │ │726 + pub fn input(self: *SearchHeader, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { │ │727 + 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) { │ │731 + if (self.cloneUrl()) |clone_url| { │ │732 + root_focus.setFocus(clone_url.getFocus().id); │ │733 + return; │ │734 + } │ │735 + } │ │736 + return text_input.input(allocator, key, root_focus); │ │737 + } │ │738 + } │ │739 + 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); │ │743 + return; │ │744 + } │ │745 + } │ │746 + try clone_url.input(allocator, key, root_focus); │ │747 + } │ │748 + │ │749 + // the typed query when enter should submit it: the box exists and holds │ │750 + // focus. the caller owns the returned text. │ │751 + pub fn submittedText(self: *SearchHeader, allocator: std.mem.Allocator) !?[]u8 { │ │752 + const text_input = self.searchInput() orelse return null; │ │753 + if (self.box.getFocus().child_id != text_input.getFocus().id) return null; │ │754 + return try text_input.text(allocator); │ │755 + } │ │756 + │ │757 + // enter the header, preferring the search box. │ │758 + pub fn focusHeader(self: *SearchHeader, root_focus: *Focus) bool { │ │759 + if (self.searchInput()) |text_input| { │ │760 + root_focus.setFocus(text_input.getFocus().id); │ │761 + return true; │ │762 + } │ │763 + const clone_url = self.cloneUrl() orelse return false; │ │764 + root_focus.setFocus(clone_url.getFocus().id); │ │765 + return true; │ │766 + } │ │767 + │ │768 + pub fn hasFocusable(self: *SearchHeader) bool { │ │769 + return self.cloneUrl() != null or self.searchInput() != null; │ │770 + } │ │771 + │ │772 + fn cloneUrl(self: *SearchHeader) ?*CopyableText { │ │773 + for (self.box.children.values()) |*child| switch (child.widget) { │ │774 + .copyable_text => |*copyable_text| return copyable_text, │ │775 + else => {}, │ │776 + }; │ │777 + return null; │ │778 + } │ │779 + │ │780 + fn searchInput(self: *SearchHeader) ?*wgt.TextInput { │ │781 + for (self.box.children.values()) |*child| switch (child.widget) { │ │782 + .text_input => |*text_input| return text_input, │ │783 + else => {}, │ │784 + }; │ │785 + return null; │ │786 + } │ │787 + │ │788 + pub fn clearGrid(self: *SearchHeader) void { │ │789 + self.box.clearGrid(); │ │790 } │ │791 + │ │792 + pub fn getGrid(self: SearchHeader) ?Grid { │ │793 + return self.box.getGrid(); │ │794 + } │ │795 + │ │796 + pub fn getFocus(self: *SearchHeader) *Focus { │ │797 + return self.box.getFocus(); │ │798 + } │ │799 }; │ │800 │ │801 // a one-row, non-focusable status bar showing the current page's url │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ added ───────╮ │src/search.zig│ ╰──────────────╯ ╭ added ──────────────╮ │src/search_commit.zig│ ╰─────────────────────╯