64 : _backend{&backend}, _options{std::move(options)}, _root{backend.createStack(parent,
Axis::
Vertical)} {
65 _root->setTestId(
"navigator");
67 _search->setTestId(
"navigator_search");
68 _search->setPlaceholder(
"Search");
69 _search->setAccessibleName(
"Search screens");
70 _search->setOnChange([
this](std::string
const&
text) { filter(text); });
72 _pinRow->setTestId(
"navigator_pins");
74 std::vector<std::string> stored = std::move(_options.pins);
75 _options.pins.clear();
76 for (std::string& screenId : stored) {
77 if (_pins.size() < _options.maxPins && known(screenId) && !pinned(screenId)) {
78 _pins.push_back(std::move(screenId));
100 bool pin(std::string
const& screenId) {
101 if (!known(screenId) || pinned(screenId) || _pins.size() >= _options.maxPins) {
104 _pins.push_back(screenId);
112 bool unpin(std::string
const& screenId) {
113 if (std::erase(_pins, screenId) == 0) {
122 [[nodiscard]] std::vector<std::string>
const&
pins() const noexcept {
return _pins; }
128 if (screenId == _current) {
131 if (Item
const*
const previous = itemOf(_current)) {
132 previous->button->setSelected(
false);
134 _current = std::string{screenId};
135 if (Item
const*
const next = itemOf(_current)) {
136 next->button->setSelected(
true);
144 [[nodiscard]] std::string
const&
current() const noexcept {
return _current; }
149 std::vector<std::string> titles;
150 std::unique_ptr<ButtonWidget>
button;
156 std::unique_ptr<TextWidget> heading;
157 std::vector<Item> items;
160 [[nodiscard]]
static std::string lower(std::string_view text) {
161 std::string out{text};
162 std::ranges::transform(out, out.begin(), [](
char byte) {
163 return static_cast<char>(std::tolower(static_cast<unsigned char>(byte)));
168 [[nodiscard]] std::vector<std::string> titlesOf(CatalogEntry
const& entry)
const {
169 if (_options.titles) {
170 std::vector<std::string> titles = _options.titles(entry);
171 if (!titles.empty()) {
178 [[nodiscard]] CatalogEntry
const* entryOf(std::string_view screenId)
const {
179 auto const found = std::ranges::find(_options.entries, screenId, &
CatalogEntry::id);
180 return found == _options.entries.end() ? nullptr : &*found;
183 [[nodiscard]]
bool known(std::string_view screenId)
const {
return entryOf(screenId) !=
nullptr; }
185 [[nodiscard]] Item
const* itemOf(std::string_view screenId)
const {
186 for (Group
const& group : _groups) {
187 auto const found = std::ranges::find_if(
188 group.items, [screenId](Item
const& item) { return !screenId.empty() && item.entry->id == screenId; });
189 if (found != group.items.end()) {
196 [[nodiscard]]
bool pinned(std::string_view screenId)
const {
197 return std::ranges::find(_pins, screenId) != _pins.end();
201 void wire(ButtonWidget& button, CatalogEntry
const& entry, std::string
const& title) {
203 button.setEnabled(entry.available);
204 if (!entry.available) {
205 button.setTooltip(entry.reason.value_or(std::string{}));
208 std::string
const screenId = entry.id;
209 button.setOnClick([
this, screenId] {
210 if (_options.onOpen) {
211 _options.onOpen(screenId);
214 button.setOnHover([
this, screenId] {
215 if (_options.onPrefetch) {
216 _options.onPrefetch(screenId);
222 for (CatalogEntry
const& entry : _options.entries) {
223 bool const seen = std::ranges::any_of(_groups, [&entry](Group
const& candidate) {
224 return candidate.module == entry.module && candidate.group == entry.group;
227 _groups.push_back(Group{.module = entry.module, .group = entry.group, .heading = {}, .items = {}});
230 for (Group& group : _groups) {
231 group.heading = _backend->createText(_root.get());
232 group.heading->setRole(TextRole::Section);
233 group.heading->setTestId(
"navigator_group_" + group.module +
"/" + group.group);
234 std::string heading = group.module;
235 if (!group.group.empty()) {
236 heading += heading.empty() ? group.group :
" / " + group.group;
238 group.heading->setText(heading);
239 for (CatalogEntry
const& entry : _options.entries) {
240 if (entry.module != group.module || entry.group != group.group) {
243 Item item{.entry = &entry, .titles = titlesOf(entry), .button = _backend->createButton(_root.get())};
244 item.button->setTestId(
"nav_" + entry.id);
245 item.button->setStyle(ButtonStyle::Item);
247 wire(*item.button, entry, item.titles.front());
248 group.items.push_back(std::move(item));
253 void filter(std::string
const& text) {
254 std::string
const wanted = lower(text);
255 for (Group& group : _groups) {
257 for (Item& item : group.items) {
259 wanted.empty() || std::ranges::any_of(item.titles, [&wanted](std::string
const& title) {
260 return lower(title).contains(wanted);
262 item.button->setVisible(shown);
265 group.heading->setVisible(any);
271 for (std::string
const& screenId : _pins) {
272 CatalogEntry
const*
const entry = entryOf(screenId);
273 auto button = _backend->createButton(_pinRow.get());
274 button->setTestId(
"pin_" + screenId);
275 wire(*button, *entry, titlesOf(*entry).front());
276 _pinButtons.push_back(std::move(button));
282 if (_options.onPins) {
283 _options.onPins(_pins);
287 IViewBackend* _backend;
288 NavigatorOptions _options;
289 std::unique_ptr<StackWidget> _root;
290 std::unique_ptr<TextInputWidget> _search;
291 std::unique_ptr<StackWidget> _pinRow;
292 std::vector<std::unique_ptr<ButtonWidget>> _pinButtons;
293 std::vector<Group> _groups;
294 std::vector<std::string> _pins;
295 std::string _current;
One catalog entry (spec 6 §8).