Serenity Operating System
1/*
2 * Copyright (c) 2022-2023, Sam Atkins <atkinssj@serenityos.org>
3 * Copyright (c) 2022, the SerenityOS developers.
4 *
5 * SPDX-License-Identifier: BSD-2-Clause
6 */
7
8#include "CharacterSearchWidget.h"
9#include "SearchCharacters.h"
10#include <Applications/CharacterMap/CharacterSearchWindowGML.h>
11#include <LibCore/Debounce.h>
12
13struct SearchResult {
14 u32 code_point;
15 DeprecatedString code_point_string;
16 DeprecatedString display_text;
17};
18
19class CharacterSearchModel final : public GUI::Model {
20public:
21 CharacterSearchModel() = default;
22
23 int row_count(GUI::ModelIndex const&) const override { return m_data.size(); }
24 int column_count(GUI::ModelIndex const&) const override { return 2; }
25
26 GUI::Variant data(GUI::ModelIndex const& index, GUI::ModelRole role) const override
27 {
28 auto& result = m_data.at(index.row());
29 if (role == GUI::ModelRole::Display) {
30 if (index.column() == 0)
31 return result.code_point_string;
32 return result.display_text;
33 }
34 if (role == GUI::ModelRole::Custom)
35 return result.code_point;
36 return {};
37 }
38
39 void clear()
40 {
41 m_data.clear();
42 invalidate();
43 }
44
45 void add_result(SearchResult result)
46 {
47 m_data.append(move(result));
48 invalidate();
49 }
50
51private:
52 Vector<SearchResult> m_data;
53};
54
55CharacterSearchWidget::CharacterSearchWidget()
56{
57 load_from_gml(character_search_window_gml).release_value_but_fixme_should_propagate_errors();
58
59 m_search_input = find_descendant_of_type_named<GUI::TextBox>("search_input");
60 m_results_table = find_descendant_of_type_named<GUI::TableView>("results_table");
61
62 m_search_input->on_up_pressed = [this] { m_results_table->move_cursor(GUI::AbstractView::CursorMovement::Up, GUI::AbstractView::SelectionUpdate::Set); };
63 m_search_input->on_down_pressed = [this] { m_results_table->move_cursor(GUI::AbstractView::CursorMovement::Down, GUI::AbstractView::SelectionUpdate::Set); };
64
65 m_search_input->on_change = Core::debounce([this] { search(); }, 100);
66
67 m_results_table->horizontal_scrollbar().set_visible(false);
68 m_results_table->set_column_headers_visible(false);
69 m_results_table->set_model(adopt_ref(*new CharacterSearchModel()));
70 m_results_table->on_selection_change = [&] {
71 auto& model = static_cast<CharacterSearchModel&>(*m_results_table->model());
72 auto index = m_results_table->selection().first();
73 auto code_point = model.data(index, GUI::ModelRole::Custom).as_u32();
74 if (on_character_selected)
75 on_character_selected(code_point);
76 };
77}
78
79void CharacterSearchWidget::search()
80{
81 ScopeGuard guard { [&] { m_results_table->set_updates_enabled(true); } };
82 m_results_table->set_updates_enabled(false);
83
84 // TODO: Sort the results nicely. They're sorted by code-point for now, which is easy, but not the most useful.
85 // Sorting intelligently in a style similar to Assistant would be nicer.
86 // Note that this will mean limiting the number of results some other way.
87 auto& model = static_cast<CharacterSearchModel&>(*m_results_table->model());
88 model.clear();
89 auto query = m_search_input->text();
90 if (query.is_empty())
91 return;
92 for_each_character_containing(query, [&](auto code_point, auto display_name) {
93 StringBuilder builder;
94 builder.append_code_point(code_point);
95
96 model.add_result({ code_point, builder.to_deprecated_string(), move(display_name) });
97
98 // Stop when we reach 250 results.
99 // This is already too many for the search to be useful, and means we don't spend forever recalculating the column size.
100 if (model.row_count({}) >= 250)
101 return IterationDecision::Break;
102 return IterationDecision::Continue;
103 });
104}