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 // Because the super class `abstract_container_widget` forces the same semantic layer
49 // as the a parent, we need to force it back as if this is a normal widget.
50 _semantic_layer = parent->semantic_layer() + 1;
51 }
52
54
55 void init() noexcept override
56 {
57 _overlay_widget = super::make_widget<overlay_view_widget>();
58 _scroll_widget = _overlay_widget->make_widget<vertical_scroll_view_widget<>>();
59 _column_widget = _scroll_widget->make_widget<column_layout_widget>();
60
61 repopulate_options();
62
63 _value_callback = this->value.subscribe([this](auto...) {
64 _request_reconstrain = true;
65 });
66 _option_list_callback = this->option_list.subscribe([this](auto...) {
67 repopulate_options();
68 _request_reconstrain = true;
69 });
70 _unknown_label_callback = this->unknown_label.subscribe([this](auto...) {
71 _request_reconstrain = true;
72 });
73 }
74
75 [[nodiscard]] bool update_constraints(hires_utc_clock::time_point display_time_point, bool need_reconstrain) noexcept override
76 {
77 tt_axiom(gui_system_mutex.recurse_lock_count());
78
79 auto updated = super::update_constraints(display_time_point, need_reconstrain);
80
81 if (updated) {
82 ttlet index = get_value_as_index();
83 if (index == -1) {
84 _text_stencil =
85 stencil::make_unique(alignment::middle_left, *unknown_label, theme::global->placeholderLabelStyle);
86 _text_stencil_color = theme::global->placeholderLabelStyle.color;
87 } else {
88 _text_stencil =
89 stencil::make_unique(alignment::middle_left, (*option_list)[index].second, theme::global->labelStyle);
90 _text_stencil_color = theme::global->labelStyle.color;
91 }
92
93 // Calculate the size of the widget based on the largest height of a label and the width of the overlay.
94 ttlet unknown_label_size =
95 stencil::make_unique(alignment::middle_left, *unknown_label, theme::global->placeholderLabelStyle)
96 ->preferred_extent();
97
98 ttlet overlay_width = _overlay_widget->preferred_size().minimum().width();
99 ttlet option_width = std::max(overlay_width, unknown_label_size.width() + theme::global->margin * 2.0f);
100 ttlet option_height = std::max(unknown_label_size.height(), _max_option_label_height) + theme::global->margin * 2.0f;
101 ttlet chevron_width = theme::global->smallSize;
102
103 _preferred_size = interval_extent2::make_minimum(extent2{chevron_width + option_width, option_height});
104 return true;
105
106 } else {
107 return false;
108 }
109 }
110
111 [[nodiscard]] void update_layout(hires_utc_clock::time_point display_time_point, bool need_layout) noexcept override
112 {
113 tt_axiom(gui_system_mutex.recurse_lock_count());
114
115 need_layout |= std::exchange(_request_relayout, false);
116
117 if (need_layout) {
118 // The overlay itself will make sure the overlay fits the window, so we give the preferred size and position
119 // from the point of view of the selection widget.
120
121 // The overlay should start on the same left edge as the selection box and the same width.
122 // The height of the overlay should be the maximum height, which will show all the options.
123
124 ttlet overlay_width =
125 clamp(rectangle().width() - theme::global->smallSize, _overlay_widget->preferred_size().width());
126 ttlet overlay_height = _overlay_widget->preferred_size().maximum().height();
127 ttlet overlay_x = theme::global->smallSize;
128 ttlet overlay_y = std::round(_size.height() * 0.5f - overlay_height * 0.5f);
129 ttlet overlay_rectangle_request = aarectangle{overlay_x, overlay_y, overlay_width, overlay_height};
130
131 ttlet overlay_rectangle = _overlay_widget->make_overlay_rectangle_from_parent(overlay_rectangle_request);
132 ttlet overlay_clipping_rectangle = expand(overlay_rectangle, _overlay_widget->margin());
133
134 _overlay_widget->set_layout_parameters_from_parent(
135 overlay_rectangle, overlay_clipping_rectangle, _overlay_widget->draw_layer() - _draw_layer);
136
137 _left_box_rectangle = aarectangle{0.0f, 0.0f, theme::global->smallSize, rectangle().height()};
138 _chevrons_glyph = to_font_glyph_ids(elusive_icon::ChevronUp);
139 ttlet chevrons_glyph_bbox = pipeline_SDF::device_shared::getBoundingBox(_chevrons_glyph);
140 _chevrons_rectangle =
141 align(_left_box_rectangle, scale(chevrons_glyph_bbox, theme::global->small_icon_size), alignment::middle_center);
142 _chevrons_rectangle =
143 align(_left_box_rectangle, scale(chevrons_glyph_bbox, theme::global->small_icon_size), alignment::middle_center);
144
145 // The unknown_label is located to the right of the selection box icon.
146 _option_rectangle = aarectangle{
147 _left_box_rectangle.right() + theme::global->margin,
148 0.0f,
149 rectangle().width() - _left_box_rectangle.width() - theme::global->margin * 2.0f,
150 rectangle().height()};
151
152 _text_stencil->set_layout_parameters(_option_rectangle, base_line());
153 }
154 super::update_layout(display_time_point, need_layout);
155 }
156
157 void draw(draw_context context, hires_utc_clock::time_point display_time_point) noexcept override
158 {
159 tt_axiom(gui_system_mutex.recurse_lock_count());
160
161 if (overlaps(context, this->_clipping_rectangle)) {
162 draw_outline(context);
163 draw_left_box(context);
164 draw_chevrons(context);
165 draw_value(context);
166 }
167
168 if (_selecting) {
169 super::draw(std::move(context), display_time_point);
170 }
171 }
172
173 void request_redraw() const noexcept override
174 {
175 super::request_redraw();
176 _overlay_widget->request_redraw();
177 }
178
179 bool handle_event(mouse_event const &event) noexcept override
180 {
181 ttlet lock = std::scoped_lock(gui_system_mutex);
182 auto handled = super::handle_event(event);
183
184 if (event.cause.leftButton) {
185 handled = true;
186 if (*enabled) {
187 if (event.type == mouse_event::Type::ButtonUp && rectangle().contains(event.position)) {
188 handle_event(command::gui_activate);
189 }
190 }
191 }
192 return handled;
193 }
194
195 bool handle_event(command command) noexcept override
196 {
197 ttlet lock = std::scoped_lock(gui_system_mutex);
198 _request_relayout = true;
199
200 if (*enabled) {
201 switch (command) {
202 using enum tt::command;
203 case gui_activate:
204 case gui_enter:
205 if (!_selecting) {
206 start_selecting();
207 } else {
208 stop_selecting();
209 }
210 return true;
211
212 case gui_escape:
213 if (_selecting) {
214 stop_selecting();
215 }
216 return true;
217
218 default:;
219 }
220 }
221
222 return super::handle_event(command);
223 }
224
225 [[nodiscard]] hit_box hitbox_test(point2 position) const noexcept override
226 {
227 tt_axiom(gui_system_mutex.recurse_lock_count());
228
229 auto r = hit_box{};
230
231 if (_selecting) {
232 r = super::hitbox_test(position);
233 }
234
235 if (_visible_rectangle.contains(position)) {
236 r = std::max(r, hit_box{weak_from_this(), _draw_layer, *enabled ? hit_box::Type::Button : hit_box::Type::Default});
237 }
238
239 return r;
240 }
241
242 [[nodiscard]] bool accepts_keyboard_focus(keyboard_focus_group group) const noexcept override
243 {
244 tt_axiom(gui_system_mutex.recurse_lock_count());
245 return is_normal(group) && *enabled;
246 }
247
249 std::shared_ptr<widget> const &current_widget,
250 keyboard_focus_group group,
251 keyboard_focus_direction direction) const noexcept
252 {
253 ttlet lock = std::scoped_lock(gui_system_mutex);
254
255 if (_selecting) {
256 return super::find_next_widget(current_widget, group, direction);
257
258 } else {
259 // Bypass the abstract_container_widget and directly use the widget implementation.
260 return widget::find_next_widget(current_widget, group, direction);
261 }
262 }
263
264 template<typename T, typename... Args>
265 std::shared_ptr<T> make_widget(Args &&...args)
266 {
267 tt_no_default();
268 }
269
270 [[nodiscard]] color focus_color() const noexcept override
271 {
272 if (*enabled && _selecting) {
273 return theme::global->accentColor;
274 } else {
275 return super::focus_color();
276 }
277 }
278
279private:
280 typename decltype(unknown_label)::callback_ptr_type _unknown_label_callback;
281 typename decltype(value)::callback_ptr_type _value_callback;
282 typename decltype(option_list)::callback_ptr_type _option_list_callback;
283
284 std::unique_ptr<label_stencil> _text_stencil;
285 color _text_stencil_color;
286
287 float _max_option_label_height;
288
289 aarectangle _option_rectangle;
290 aarectangle _left_box_rectangle;
291
292 font_glyph_ids _chevrons_glyph;
293 aarectangle _chevrons_rectangle;
294
295 bool _selecting = false;
299
302
303 [[nodiscard]] ssize_t get_value_as_index() const noexcept
304 {
305 ssize_t index = 0;
306 for (ttlet & [ tag, unknown_label_text ] : *option_list) {
307 if (value == tag) {
308 return index;
309 }
310 ++index;
311 }
312
313 return -1;
314 }
315
316 [[nodiscard]] std::shared_ptr<menu_item_widget<value_type>> get_first_menu_item() const noexcept
317 {
318 if (std::ssize(_menu_item_widgets) != 0) {
319 return _menu_item_widgets.front();
320 } else {
321 return {};
322 }
323 }
324
325 [[nodiscard]] std::shared_ptr<menu_item_widget<value_type>> get_selected_menu_item() const noexcept
326 {
327 ttlet i = get_value_as_index();
328 if (i >= 0 && i < std::ssize(_menu_item_widgets)) {
329 return _menu_item_widgets[i];
330 } else {
331 return {};
332 }
333 }
334
335 void start_selecting() noexcept
336 {
337 tt_axiom(gui_system_mutex.recurse_lock_count());
338
339 _selecting = true;
340 if (auto selected_menu_item = get_selected_menu_item()) {
341 this->window.update_keyboard_target(selected_menu_item, keyboard_focus_group::menu);
342
343 } else if (auto first_menu_item = get_first_menu_item()) {
344 this->window.update_keyboard_target(first_menu_item, keyboard_focus_group::menu);
345
346 }
347
348 request_redraw();
349 }
350
351 void stop_selecting() noexcept
352 {
353 tt_axiom(gui_system_mutex.recurse_lock_count());
354
355 _selecting = false;
356 request_redraw();
357 }
358
361 void repopulate_options() noexcept
362 {
363 ttlet lock = std::scoped_lock(gui_system_mutex);
364 auto option_list_ = *option_list;
365
366 // If any of the options has a an icon, all of the options should show the icon.
367 auto show_icon = false;
368 for (ttlet & [ tag, label ] : option_list_) {
369 show_icon |= label.has_icon();
370 }
371
372 _column_widget->clear();
373 _menu_item_widgets.clear();
374 _menu_item_callbacks.clear();
375 for (ttlet & [ tag, text ] : option_list_) {
376 auto menu_item = _column_widget->make_widget<menu_item_widget<value_type>>(tag, this->value);
377 menu_item->set_show_check_mark(true);
378 menu_item->set_show_icon(show_icon);
379 menu_item->label = text;
380
381 _menu_item_callbacks.push_back(menu_item->subscribe([this, tag] {
382 this->value = tag;
383 this->_selecting = false;
384 }));
385
386 _menu_item_widgets.push_back(std::move(menu_item));
387 }
388
389 _max_option_label_height = 0.0f;
390 for (ttlet & [ tag, text ] : *option_list) {
391 _max_option_label_height = std::max(
392 _max_option_label_height,
393 stencil::make_unique(alignment::middle_left, text, theme::global->labelStyle)->preferred_extent().height());
394 }
395 }
396
397 void draw_outline(draw_context context) noexcept
398 {
399 tt_axiom(gui_system_mutex.recurse_lock_count());
400
401 context.draw_box_with_border_inside(
402 rectangle(), background_color(), focus_color(), corner_shapes{theme::global->roundingRadius});
403 }
404
405 void draw_left_box(draw_context context) noexcept
406 {
407 tt_axiom(gui_system_mutex.recurse_lock_count());
408
409 ttlet corner_shapes = tt::corner_shapes{theme::global->roundingRadius, 0.0f, theme::global->roundingRadius, 0.0f};
410 context.draw_box(translate_z(0.1f) * _left_box_rectangle, focus_color(), corner_shapes);
411 }
412
413 void draw_chevrons(draw_context context) noexcept
414 {
415 tt_axiom(gui_system_mutex.recurse_lock_count());
416
417 context.draw_glyph(_chevrons_glyph, translate_z(0.2f) * _chevrons_rectangle, label_color());
418 }
419
420 void draw_value(draw_context context) noexcept
421 {
422 tt_axiom(gui_system_mutex.recurse_lock_count());
423 _text_stencil->draw(context, label_color(), translate_z(0.1f));
424 }
425};
426
427} // namespace tt
Class which represents an axis-aligned rectangle.
Definition axis_aligned_rectangle.hpp:18
Definition corner_shapes.hpp:9
Class which represents an rectangle.
Definition rectangle.hpp:16
Draw context for drawing using the TTauri shaders.
Definition draw_context.hpp:33
Definition gui_window.hpp:37
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.
Definition hit_box.hpp:15
Definition mouse_event.hpp:15
static aarectangle 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:110
void draw(draw_context context, hires_utc_clock::time_point display_time_point) noexcept
Draw the widget.
Definition abstract_container_widget.hpp:124
hit_box hitbox_test(point2 position) const noexcept override
Find the widget that is under the mouse cursor.
Definition abstract_container_widget.hpp:154
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:187
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:95
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:248
bool accepts_keyboard_focus(keyboard_focus_group group) const noexcept override
Check if the widget will accept keyboard focus.
Definition selection_widget.hpp:242
void draw(draw_context context, hires_utc_clock::time_point display_time_point) noexcept override
Draw the widget.
Definition selection_widget.hpp:157
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:75
bool handle_event(command command) noexcept override
Handle command.
Definition selection_widget.hpp:195
bool handle_event(mouse_event const &event) noexcept override
Definition selection_widget.hpp:179
hit_box hitbox_test(point2 position) const noexcept override
Find the widget that is under the mouse cursor.
Definition selection_widget.hpp:225
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:111
void init() noexcept override
Should be called right after allocating and constructing a widget.
Definition selection_widget.hpp:55
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.
aarectangle rectangle() const noexcept
Get the rectangle in local coordinates.
Definition widget.hpp:342
virtual bool handle_event(command command) noexcept
Handle command.
gui_window & window
Convenient reference to the Window.
Definition widget.hpp:101
virtual float base_line() const noexcept
Return the base-line where the text should be located.
Definition widget.hpp:351
abstract_container_widget const & parent() const noexcept
Get a reference to the parent.
int semantic_layer() const noexcept
The semantic layer of the widget.
Definition widget.hpp:189
T clear(T... args)
T front(T... args)
T lock(T... args)
T max(T... args)
T move(T... args)
T push_back(T... args)
T round(T... args)