HikoGUI
A low latency retained GUI
Loading...
Searching...
No Matches
selection_widget.hpp
1// Copyright Take Vos 2020-2021.
2// Distributed under the Boost Software License, Version 1.0.
3// (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt)
4
5#pragma once
6
7#include "abstract_container_widget.hpp"
8#include "overlay_view_widget.hpp"
9#include "scroll_view_widget.hpp"
10#include "row_column_layout_widget.hpp"
11#include "menu_item_widget.hpp"
12#include "../stencils/label_stencil.hpp"
13#include "../GUI/draw_context.hpp"
14#include "../text/font_book.hpp"
15#include "../text/elusive_icon.hpp"
16#include "../observable.hpp"
17#include <memory>
18#include <string>
19#include <array>
20#include <optional>
21#include <future>
22
23namespace tt {
24
25template<typename T>
27public:
29 using value_type = T;
31
32 observable<label> unknown_label;
35
36 template<typename Value = value_type, typename OptionList = option_list_type, typename UnknownLabel = label>
40 Value &&value = value_type{},
41 OptionList &&option_list = option_list_type{},
42 UnknownLabel &&unknown_label = label{l10n("<unknown>")}) noexcept :
44 value(std::forward<Value>(value)),
45 option_list(std::forward<OptionList>(option_list)),
46 unknown_label(std::forward<UnknownLabel>(unknown_label))
47 {
48 }
49
51
52 void init() noexcept override
53 {
54 _overlay_widget = super::make_widget<overlay_view_widget>();
55 _scroll_widget = _overlay_widget->make_widget<vertical_scroll_view_widget<>>();
56 _column_widget = _scroll_widget->make_widget<column_layout_widget>();
57
58 repopulate_options();
59
60 _value_callback = this->value.subscribe([this](auto...) {
61 _request_reconstrain = true;
62 });
63 _option_list_callback = this->option_list.subscribe([this](auto...) {
64 repopulate_options();
65 _request_reconstrain = true;
66 });
67 _unknown_label_callback = this->unknown_label.subscribe([this](auto...) {
68 _request_reconstrain = true;
69 });
70 }
71
72 [[nodiscard]] bool update_constraints(hires_utc_clock::time_point display_time_point, bool need_reconstrain) noexcept override
73 {
74 tt_axiom(gui_system_mutex.recurse_lock_count());
75
76 auto updated = super::update_constraints(display_time_point, need_reconstrain);
77
78 if (updated) {
79 ttlet index = get_value_as_index();
80 if (index == -1) {
81 _text_stencil =
82 stencil::make_unique(alignment::middle_left, *unknown_label, theme::global->placeholderLabelStyle);
83 _text_stencil_color = theme::global->placeholderLabelStyle.color;
84 } else {
85 _text_stencil =
86 stencil::make_unique(alignment::middle_left, (*option_list)[index].second, theme::global->labelStyle);
87 _text_stencil_color = theme::global->labelStyle.color;
88 }
89
90 // Calculate the size of the widget based on the largest height of a label and the width of the overlay.
91 ttlet unknown_label_size =
92 stencil::make_unique(alignment::middle_left, *unknown_label, theme::global->placeholderLabelStyle)
93 ->preferred_extent();
94
95 ttlet overlay_width = _overlay_widget->preferred_size().minimum().width();
96 ttlet option_width = std::max(overlay_width, unknown_label_size.width() + theme::global->margin * 2.0f);
97 ttlet option_height = std::max(unknown_label_size.height(), _max_option_label_height) + theme::global->margin * 2.0f;
98 ttlet chevron_width = theme::global->smallSize;
99
100 _preferred_size = interval_vec2::make_minimum(f32x4{chevron_width + option_width, option_height});
101 _preferred_base_line = relative_base_line{vertical_alignment::middle, 0.0f, 200.0f};
102 return true;
103
104 } else {
105 return false;
106 }
107 }
108
109 [[nodiscard]] void update_layout(hires_utc_clock::time_point display_time_point, bool need_layout) noexcept override
110 {
111 tt_axiom(gui_system_mutex.recurse_lock_count());
112
113 need_layout |= std::exchange(_request_relayout, false);
114
115 if (_selecting) {
116 if (need_layout) {
117 // The overlay itself will make sure the overlay fits the window, so we give the preferred size and position
118 // from the point of view of the selection widget.
119
120 // The overlay should start on the same left edge as the selection box and the same width.
121 // The height of the overlay should be the maximum height, which will show all the options.
122
123 ttlet overlay_width =
124 clamp(rectangle().width() - theme::global->smallSize, _overlay_widget->preferred_size().width());
125 ttlet overlay_height = _overlay_widget->preferred_size().maximum().height();
126 ttlet overlay_x = _window_rectangle.x() + theme::global->smallSize;
127 ttlet overlay_y = std::round(_window_rectangle.middle() - overlay_height * 0.5f);
128 ttlet overlay_rectangle = aarect{overlay_x, overlay_y, overlay_width, overlay_height};
129
130 _overlay_widget->set_layout_parameters(overlay_rectangle, overlay_rectangle);
131 }
132 }
133
134 if (need_layout) {
135 _left_box_rectangle = aarect{0.0f, 0.0f, theme::global->smallSize, rectangle().height()};
136 _chevrons_glyph = to_font_glyph_ids(elusive_icon::ChevronUp);
137 ttlet chevrons_glyph_bbox = pipeline_SDF::device_shared::getBoundingBox(_chevrons_glyph);
138 _chevrons_rectangle =
139 align(_left_box_rectangle, scale(chevrons_glyph_bbox, theme::global->small_icon_size), alignment::middle_center);
140 _chevrons_rectangle =
141 align(_left_box_rectangle, scale(chevrons_glyph_bbox, theme::global->small_icon_size), alignment::middle_center);
142
143 // The unknown_label is located to the right of the selection box icon.
144 _option_rectangle = aarect{
145 _left_box_rectangle.right() + theme::global->margin,
146 0.0f,
147 rectangle().width() - _left_box_rectangle.width() - theme::global->margin * 2.0f,
148 rectangle().height()};
149
150 _text_stencil->set_layout_parameters(_option_rectangle, base_line());
151 }
152 super::update_layout(display_time_point, need_layout);
153 }
154
155 void draw(draw_context context, hires_utc_clock::time_point display_time_point) noexcept override
156 {
157 tt_axiom(gui_system_mutex.recurse_lock_count());
158
159 if (overlaps(context, this->window_clipping_rectangle())) {
160 draw_outline(context);
161 draw_left_box(context);
162 draw_chevrons(context);
163 draw_value(context);
164 }
165
166 if (_selecting) {
167 super::draw(std::move(context), display_time_point);
168 }
169 }
170
171 bool handle_event(mouse_event const &event) noexcept override
172 {
173 ttlet lock = std::scoped_lock(gui_system_mutex);
174 auto handled = super::handle_event(event);
175
176 if (event.cause.leftButton) {
177 handled = true;
178 if (*enabled) {
179 if (event.type == mouse_event::Type::ButtonUp && _window_rectangle.contains(event.position)) {
180 handle_event(command::gui_activate);
181 }
182 }
183 }
184 return handled;
185 }
186
187 bool handle_event(command command) noexcept override
188 {
189 ttlet lock = std::scoped_lock(gui_system_mutex);
190 _request_relayout = true;
191
192 if (*enabled) {
193 switch (command) {
194 using enum tt::command;
195 case gui_activate:
196 case gui_enter:
197 if (!_selecting) {
198 start_selecting();
199 } else {
200 stop_selecting();
201 }
202 return true;
203
204 case gui_escape:
205 if (_selecting) {
206 stop_selecting();
207 }
208 return true;
209
210 default:;
211 }
212 }
213
214 return super::handle_event(command);
215 }
216
217 [[nodiscard]] hit_box hitbox_test(f32x4 window_position) const noexcept override
218 {
219 ttlet lock = std::scoped_lock(gui_system_mutex);
220 ttlet position = _from_window_transform * window_position;
221
222 auto r = hit_box{};
223
224 if (_selecting) {
225 r = super::hitbox_test(window_position);
226 }
227
228 if (window_clipping_rectangle().contains(window_position)) {
229 r = std::max(r, hit_box{weak_from_this(), _draw_layer, *enabled ? hit_box::Type::Button : hit_box::Type::Default});
230 }
231
232 return r;
233 }
234
235 [[nodiscard]] bool accepts_keyboard_focus(keyboard_focus_group group) const noexcept override
236 {
237 tt_axiom(gui_system_mutex.recurse_lock_count());
238 return is_normal(group) && *enabled;
239 }
240
242 std::shared_ptr<widget> const &current_widget,
243 keyboard_focus_group group,
244 keyboard_focus_direction direction) const noexcept
245 {
246 ttlet lock = std::scoped_lock(gui_system_mutex);
247
248 if (_selecting) {
249 return super::find_next_widget(current_widget, group, direction);
250
251 } else {
252 // Bypass the abstract_container_widget and directly use the widget implementation.
253 return widget::find_next_widget(current_widget, group, direction);
254 }
255 }
256
257 template<typename T, typename... Args>
258 std::shared_ptr<T> make_widget(Args &&...args)
259 {
260 tt_no_default();
261 }
262
263
264private:
265 typename decltype(unknown_label)::callback_ptr_type _unknown_label_callback;
266 typename decltype(value)::callback_ptr_type _value_callback;
267 typename decltype(option_list)::callback_ptr_type _option_list_callback;
268
269 std::unique_ptr<label_stencil> _text_stencil;
270 color _text_stencil_color;
271
272 float _max_option_label_height;
273
274 aarect _option_rectangle;
275 aarect _left_box_rectangle;
276
277 font_glyph_ids _chevrons_glyph;
278 aarect _chevrons_rectangle;
279
280 bool _selecting = false;
284
287
288 [[nodiscard]] ssize_t get_value_as_index() const noexcept
289 {
290 ssize_t index = 0;
291 for (ttlet & [ tag, unknown_label_text ] : *option_list) {
292 if (value == tag) {
293 return index;
294 }
295 ++index;
296 }
297
298 return -1;
299 }
300
301 [[nodiscard]] std::shared_ptr<menu_item_widget<value_type>> get_selected_menu_item() const noexcept
302 {
303 ttlet i = get_value_as_index();
304 if (i >= 0 && i < std::ssize(_menu_item_widgets)) {
305 return _menu_item_widgets[i];
306 } else {
307 return {};
308 }
309 }
310
311 void start_selecting() noexcept
312 {
313 _selecting = true;
314 if (auto selected_menu_item = get_selected_menu_item()) {
315 this->window.update_keyboard_target(selected_menu_item, keyboard_focus_group::menu);
316 }
317 }
318
319 void stop_selecting() noexcept
320 {
321 _selecting = false;
322 window.request_redraw(_overlay_widget->window_rectangle());
324 }
325
328 void repopulate_options() noexcept
329 {
330 ttlet lock = std::scoped_lock(gui_system_mutex);
331 auto option_list_ = *option_list;
332
333 // If any of the options has a an icon, all of the options should show the icon.
334 auto show_icon = false;
335 for (ttlet & [ tag, label ] : option_list_) {
336 show_icon |= label.has_icon();
337 }
338
339 _column_widget->clear();
340 _menu_item_widgets.clear();
341 _menu_item_callbacks.clear();
342 for (ttlet & [ tag, text ] : option_list_) {
343 auto menu_item = _column_widget->make_widget<menu_item_widget<value_type>>(tag, this->value);
344 menu_item->set_show_check_mark(true);
345 menu_item->set_show_icon(show_icon);
346 menu_item->label = text;
347
348 _menu_item_callbacks.push_back(menu_item->subscribe([this, tag] {
349 this->value = tag;
350 this->_selecting = false;
351 }));
352
353 _menu_item_widgets.push_back(std::move(menu_item));
354 }
355
356 _max_option_label_height = 0.0f;
357 for (ttlet & [ tag, text ] : *option_list) {
358 _max_option_label_height = std::max(
359 _max_option_label_height,
360 stencil::make_unique(alignment::middle_left, text, theme::global->labelStyle)->preferred_extent().height());
361 }
362 }
363
364 void draw_outline(draw_context context) noexcept
365 {
366 tt_axiom(gui_system_mutex.recurse_lock_count());
367
368 context.corner_shapes = f32x4::broadcast(theme::global->roundingRadius);
369 context.draw_box_with_border_inside(rectangle());
370 }
371
372 void draw_left_box(draw_context context) noexcept
373 {
374 tt_axiom(gui_system_mutex.recurse_lock_count());
375
376 context.transform = translate3{0.0, 0.0, 0.1f} * context.transform;
377 if (_selecting) {
378 context.line_color = theme::global->accentColor;
379 }
380 context.fill_color = context.line_color;
381 context.corner_shapes = f32x4{theme::global->roundingRadius, 0.0f, theme::global->roundingRadius, 0.0f};
382 context.draw_box_with_border_inside(_left_box_rectangle);
383 }
384
385 void draw_chevrons(draw_context context) noexcept
386 {
387 tt_axiom(gui_system_mutex.recurse_lock_count());
388
389 context.transform = translate3{0.0, 0.0, 0.2f} * context.transform;
390 context.line_color = *enabled ? theme::global->foregroundColor : context.fill_color;
391 context.draw_glyph(_chevrons_glyph, _chevrons_rectangle);
392 }
393
394 void draw_value(draw_context context) noexcept
395 {
396 tt_axiom(gui_system_mutex.recurse_lock_count());
397
398 context.transform = translate3{0.0, 0.0, 0.1f} * context.transform;
399 context.line_color = *enabled ? _text_stencil_color : context.line_color;
400 _text_stencil->draw(context, true);
401 }
402};
403
404} // namespace tt
Definition alignment.hpp:104
Draw context for drawing using the TTauri shaders.
Definition draw_context.hpp:33
Definition gui_window.hpp:39
void update_keyboard_target(std::shared_ptr< tt::widget > widget, keyboard_focus_group group=keyboard_focus_group::normal) noexcept
Change the keyboard focus to the given widget.
void request_redraw(aarect rectangle=aarect::infinity()) noexcept
Request a rectangle on the window to be redrawn.
Definition gui_window.hpp:115
Definition hit_box.hpp:15
Definition mouse_event.hpp:13
static aarect getBoundingBox(font_glyph_ids const &glyphs) noexcept
Get the bounding box, including draw border of a glyph.
A localizable string.
Definition l10n.hpp:12
A localized text + icon label.
Definition label.hpp:76
Definition observable.hpp:20
int recurse_lock_count() const noexcept
This function should be used in tt_axiom() to check if the lock is held by current thread.
Definition unfair_recursive_mutex.hpp:60
Definition abstract_container_widget.hpp:11
void update_layout(hires_utc_clock::time_point display_time_point, bool need_layout) noexcept
Update the internal layout of the widget.
Definition abstract_container_widget.hpp:111
void draw(draw_context context, hires_utc_clock::time_point display_time_point) noexcept
Draw the widget.
Definition abstract_container_widget.hpp:125
std::shared_ptr< widget > find_next_widget(std::shared_ptr< widget > const &current_keyboard_widget, keyboard_focus_group group, keyboard_focus_direction direction) const noexcept
Find the next widget that handles keyboard focus.
Definition abstract_container_widget.hpp:185
hit_box hitbox_test(f32x4 window_position) const noexcept
Find the widget that is under the mouse cursor.
Definition abstract_container_widget.hpp:152
bool update_constraints(hires_utc_clock::time_point display_time_point, bool need_reconstrain) noexcept
Update the constraints of the widget.
Definition abstract_container_widget.hpp:96
Definition row_column_layout_widget.hpp:16
Definition scroll_view_widget.hpp:14
Definition selection_widget.hpp:26
std::shared_ptr< widget > find_next_widget(std::shared_ptr< widget > const &current_widget, keyboard_focus_group group, keyboard_focus_direction direction) const noexcept
Find the next widget that handles keyboard focus.
Definition selection_widget.hpp:241
bool accepts_keyboard_focus(keyboard_focus_group group) const noexcept override
Check if the widget will accept keyboard focus.
Definition selection_widget.hpp:235
void draw(draw_context context, hires_utc_clock::time_point display_time_point) noexcept override
Draw the widget.
Definition selection_widget.hpp:155
bool update_constraints(hires_utc_clock::time_point display_time_point, bool need_reconstrain) noexcept override
Update the constraints of the widget.
Definition selection_widget.hpp:72
bool handle_event(command command) noexcept override
Handle command.
Definition selection_widget.hpp:187
hit_box hitbox_test(f32x4 window_position) const noexcept override
Find the widget that is under the mouse cursor.
Definition selection_widget.hpp:217
bool handle_event(mouse_event const &event) noexcept override
Definition selection_widget.hpp:171
void update_layout(hires_utc_clock::time_point display_time_point, bool need_layout) noexcept override
Update the internal layout of the widget.
Definition selection_widget.hpp:109
void init() noexcept override
Should be called right after allocating and constructing a widget.
Definition selection_widget.hpp:52
observable< bool > enabled
The widget is enabled.
Definition widget.hpp:105
virtual std::shared_ptr< widget > find_next_widget(std::shared_ptr< widget > const &current_keyboard_widget, keyboard_focus_group group, keyboard_focus_direction direction) const noexcept
Find the next widget that handles keyboard focus.
virtual bool handle_event(command command) noexcept
Handle command.
gui_window & window
Convenient reference to the Window.
Definition widget.hpp:100
float base_line() const noexcept
Get the base-line in local coordinates.
Definition widget.hpp:350
abstract_container_widget const & parent() const noexcept
Get a reference to the parent.
virtual aarect window_clipping_rectangle() const noexcept
Get the clipping-rectangle in window coordinates.
Definition widget.hpp:320
aarect window_rectangle() const noexcept
Get the rectangle in window coordinates.
Definition widget.hpp:310
aarect rectangle() const noexcept
Get the rectangle in local coordinates.
Definition widget.hpp:340
T clear(T... args)
T lock(T... args)
T max(T... args)
T move(T... args)
T push_back(T... args)
T round(T... args)