╭ changed ────────╮ │src/ui/widget.zig│ ╰─────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭────────────────────────────────────────────────────╮ │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 } │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │799 // the typed query when enter should submit it: the box exists and holds │ │800 // focus. the caller owns the returned text. │ │801 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); │ │802 + const search_box = self.searchBox() orelse return null; │ │803 + if (self.box.getFocus().child_id != search_box.getFocus().id) return null; │ │804 + return try search_box.text(allocator); │ │805 } │ │806 │ │807 // enter the header, preferring the search box. │ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────╮ │808 pub fn focusHeader(self: *SearchHeader, root_focus: *Focus) bool {│ │759 - if (self.searchInput()) |text_input| { │ │760 - root_focus.setFocus(text_input.getFocus().id); │ │809 + if (self.searchBox()) |search_box| { │ │810 + root_focus.setFocus(search_box.getFocus().id); │ │811 return true; │ │812 } │ │813 const clone_url = self.cloneUrl() orelse return false; │ ╰───────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────╮ │816 } │ │817 │ │818 pub fn hasFocusable(self: *SearchHeader) bool { │ │769 - return self.cloneUrl() != null or self.searchInput() != null;│ │819 + return self.cloneUrl() != null or self.searchBox() != null; │ │820 } │ │821 │ │822 fn cloneUrl(self: *SearchHeader) ?*CopyableText { │ ╰──────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯