HikoGUI
A low latency retained GUI
Loading...
Searching...
No Matches
selection_widget.hpp
Go to the documentation of this file.
1// Copyright Take Vos 2020-2022.
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
9#pragma once
10
11#include "widget.hpp"
12#include "label_widget.hpp"
13#include "overlay_widget.hpp"
14#include "scroll_widget.hpp"
15#include "row_column_widget.hpp"
18#include "../observer/module.hpp"
19#include "../macros.hpp"
20#include <memory>
21#include <string>
22#include <array>
23#include <optional>
24#include <future>
25
26namespace hi { inline namespace v1 {
27
28template<typename Context>
30
46public:
47 using super = widget;
48 using delegate_type = selection_delegate;
49
51
55
74 observer<alignment> alignment = hi::alignment::middle_flush();
75
78 observer<semantic_text_style> text_style = semantic_text_style::label;
79
81 {
82 hi_assert_not_null(delegate);
83 delegate->deinit(*this);
84 }
85
92 super(parent), delegate(std::move(delegate))
93 {
94 hi_assert_not_null(this->delegate);
95
96 _current_label_widget = std::make_unique<label_widget>(this, alignment, text_style);
97 _current_label_widget->mode = widget_mode::invisible;
98 _off_label_widget = std::make_unique<label_widget>(this, off_label, alignment, semantic_text_style::placeholder);
99
100 _overlay_widget = std::make_unique<overlay_widget>(this);
101 _overlay_widget->mode = widget_mode::invisible;
102 _scroll_widget = &_overlay_widget->make_widget<vertical_scroll_widget>();
103 _column_widget = &_scroll_widget->make_widget<column_widget>();
104
105 _off_label_cbt = this->off_label.subscribe([&](auto...) {
106 ++global_counter<"selection_widget:off_label:constrain">;
108 });
109
110 _delegate_cbt = this->delegate->subscribe([&] {
111 _notification_from_delegate = true;
112 ++global_counter<"selection_widget:delegate:constrain">;
114 });
115
116 this->delegate->init(*this);
117 }
118
129 widget *parent,
134 {
135 set_attributes(hi_forward(first_attribute), hi_forward(attributes)...);
136 }
137
150 template<
155 widget *parent,
156 Value&& value,
158 Attributes&&...attributes) noexcept requires requires
159 {
160 make_default_selection_delegate(hi_forward(value), hi_forward(option_list));
161 } :
163 parent,
164 make_default_selection_delegate(hi_forward(value), hi_forward(option_list)),
165 hi_forward(attributes)...)
166 {
167 }
168
183 template<
187 selection_widget_attribute... Attributes>
189 widget *parent,
190 Value&& value,
192 OffValue&& off_value,
193 Attributes&&...attributes) noexcept requires requires
194 {
195 make_default_selection_delegate(hi_forward(value), hi_forward(option_list), hi_forward(off_value));
196 } :
198 parent,
199 make_default_selection_delegate(hi_forward(value), hi_forward(option_list), hi_forward(off_value)),
200 hi_forward(attributes)...)
201 {
202 }
203
205 [[nodiscard]] generator<widget_intf&> children(bool include_invisible) noexcept override
206 {
207 co_yield *_overlay_widget;
208 co_yield *_current_label_widget;
209 co_yield *_off_label_widget;
210 }
211
212 [[nodiscard]] box_constraints update_constraints() noexcept override
213 {
214 hi_assert_not_null(_off_label_widget);
215 hi_assert_not_null(_current_label_widget);
216 hi_assert_not_null(_overlay_widget);
217
218 if (_notification_from_delegate.exchange(false)) {
219 repopulate_options();
220 }
221
222 _layout = {};
223 _off_label_constraints = _off_label_widget->update_constraints();
224 _current_label_constraints = _current_label_widget->update_constraints();
225 _overlay_constraints = _overlay_widget->update_constraints();
226
227 hilet extra_size = extent2{theme().size() + theme().margin<float>() * 2.0f, theme().margin<float>() * 2.0f};
228
229 auto r = max(_off_label_constraints + extra_size, _current_label_constraints + extra_size);
230
231 // Make it so that the scroll widget can scroll vertically.
232 _scroll_widget->minimum.copy()->height() = theme().size();
233
234 r.minimum.width() = std::max(r.minimum.width(), _overlay_constraints.minimum.width() + extra_size.width());
235 r.preferred.width() = std::max(r.preferred.width(), _overlay_constraints.preferred.width() + extra_size.width());
236 r.maximum.width() = std::max(r.maximum.width(), _overlay_constraints.maximum.width() + extra_size.width());
237 r.margins = theme().margin();
238 r.padding = theme().margin();
239 r.alignment = resolve(*alignment, os_settings::left_to_right());
240 hi_axiom(r.holds_invariant());
241 return r;
242 }
243
244 void set_layout(widget_layout const& context) noexcept override
245 {
246 if (compare_store(_layout, context)) {
247 if (os_settings::left_to_right()) {
248 _left_box_rectangle = aarectangle{0.0f, 0.0f, theme().size(), context.height()};
249
250 // The unknown_label is located to the right of the selection box icon.
251 hilet option_rectangle = aarectangle{
252 _left_box_rectangle.right() + theme().margin<float>(),
253 0.0f,
254 context.width() - _left_box_rectangle.width() - theme().margin<float>() * 2.0f,
255 context.height()};
256 _off_label_shape = box_shape{_off_label_constraints, option_rectangle, theme().baseline_adjustment()};
257 _current_label_shape = box_shape{_off_label_constraints, option_rectangle, theme().baseline_adjustment()};
258
259 } else {
260 _left_box_rectangle = aarectangle{context.width() - theme().size(), 0.0f, theme().size(), context.height()};
261
262 // The unknown_label is located to the left of the selection box icon.
263 hilet option_rectangle = aarectangle{
264 theme().margin<float>(),
265 0.0f,
266 context.width() - _left_box_rectangle.width() - theme().margin<float>() * 2.0f,
267 context.height()};
268 _off_label_shape = box_shape{_off_label_constraints, option_rectangle, theme().baseline_adjustment()};
269 _current_label_shape = box_shape{_off_label_constraints, option_rectangle, theme().baseline_adjustment()};
270 }
271
272 _chevrons_glyph = find_glyph(elusive_icon::ChevronUp);
273 hilet chevrons_glyph_bbox = _chevrons_glyph.get_metrics().bounding_rectangle * theme().icon_size();
274 _chevrons_rectangle = align(_left_box_rectangle, chevrons_glyph_bbox, alignment::middle_center());
275 }
276
277 // The overlay itself will make sure the overlay fits the window, so we give the preferred size and position
278 // from the point of view of the selection widget.
279 // The overlay should start on the same left edge as the selection box and the same width.
280 // The height of the overlay should be the maximum height, which will show all the options.
281 hilet overlay_width = std::clamp(
282 context.width() - theme().size(), _overlay_constraints.minimum.width(), _overlay_constraints.maximum.width());
283 hilet overlay_height = _overlay_constraints.preferred.height();
284 hilet overlay_x = os_settings::left_to_right() ? theme().size() : context.width() - theme().size() - overlay_width;
285 hilet overlay_y = (context.height() - overlay_height) / 2;
287 hilet overlay_rectangle = make_overlay_rectangle(overlay_rectangle_request);
288 _overlay_shape = box_shape{_overlay_constraints, overlay_rectangle, theme().baseline_adjustment()};
289 _overlay_widget->set_layout(context.transform(_overlay_shape, 20.0f));
290
291 _off_label_widget->set_layout(context.transform(_off_label_shape));
292 _current_label_widget->set_layout(context.transform(_current_label_shape));
293 }
294
295 void draw(draw_context const& context) noexcept override
296 {
298 if (overlaps(context, layout())) {
299 draw_outline(context);
300 draw_left_box(context);
301 draw_chevrons(context);
302
303 _off_label_widget->draw(context);
304 _current_label_widget->draw(context);
305 }
306
307 // Overlay is outside of the overlap of the selection widget.
308 _overlay_widget->draw(context);
309 }
310 }
311
312 bool handle_event(gui_event const& event) noexcept override
313 {
314 switch (event.type()) {
315 case gui_event_type::mouse_up:
316 if (*mode >= widget_mode::partial and _has_options and layout().rectangle().contains(event.mouse().position)) {
317 return handle_event(gui_event_type::gui_activate);
318 }
319 return true;
320
321 case gui_event_type::gui_activate_next:
322 // Handle gui_active_next so that the next widget will NOT get keyboard focus.
323 // The previously selected item needs the get keyboard focus instead.
324 case gui_event_type::gui_activate:
325 if (*mode >= widget_mode::partial and _has_options and not _selecting) {
326 start_selecting();
327 } else {
328 stop_selecting();
329 }
330 ++global_counter<"selection_widget:gui_activate:relayout">;
332 return true;
333
334 case gui_event_type::gui_cancel:
335 if (*mode >= widget_mode::partial and _has_options and _selecting) {
336 stop_selecting();
337 }
338 ++global_counter<"selection_widget:gui_cancel:relayout">;
340 return true;
341
342 default:;
343 }
344
346 }
347
348 [[nodiscard]] hitbox hitbox_test(point2 position) const noexcept override
349 {
350 hi_axiom(loop::main().on_thread());
351
352 if (*mode >= widget_mode::partial) {
353 auto r = _overlay_widget->hitbox_test_from_parent(position);
354
355 if (layout().contains(position)) {
356 r = std::max(r, hitbox{id, _layout.elevation, _has_options ? hitbox_type::button : hitbox_type::_default});
357 }
358
359 return r;
360 } else {
361 return {};
362 }
363 }
364
365 [[nodiscard]] bool accepts_keyboard_focus(keyboard_focus_group group) const noexcept override
366 {
367 hi_axiom(loop::main().on_thread());
368 return *mode >= widget_mode::partial and to_bool(group & hi::keyboard_focus_group::normal) and _has_options;
369 }
370
371 [[nodiscard]] color focus_color() const noexcept override
372 {
373 hi_axiom(loop::main().on_thread());
374
375 if (*mode >= widget_mode::partial and _has_options and _selecting) {
376 return theme().color(semantic_color::accent);
377 } else {
378 return super::focus_color();
379 }
380 }
381
383private:
384 notifier<>::callback_token _delegate_cbt;
385 std::atomic<bool> _notification_from_delegate = true;
386
387 std::unique_ptr<label_widget> _current_label_widget;
388 box_constraints _current_label_constraints;
389 box_shape _current_label_shape;
390
391 std::unique_ptr<label_widget> _off_label_widget;
392 box_constraints _off_label_constraints;
393 box_shape _off_label_shape;
394
395 aarectangle _left_box_rectangle;
396
397 font_book::font_glyph_type _chevrons_glyph;
398 aarectangle _chevrons_rectangle;
399
400 bool _selecting = false;
401 bool _has_options = false;
402
403 std::unique_ptr<overlay_widget> _overlay_widget;
404 box_constraints _overlay_constraints;
405 box_shape _overlay_shape;
406
407 vertical_scroll_widget *_scroll_widget = nullptr;
408 column_widget *_column_widget = nullptr;
409
410 decltype(off_label)::callback_token _off_label_cbt;
411 std::vector<menu_button_widget *> _menu_button_widgets;
412 std::vector<notifier<>::callback_token> _menu_button_tokens;
413
414 void set_attributes() noexcept {}
415 void set_attributes(label_widget_attribute auto&& first, label_widget_attribute auto&&...rest) noexcept
416 {
417 if constexpr (forward_of<decltype(first), observer<hi::label>>) {
418 off_label = hi_forward(first);
419 } else if constexpr (forward_of<decltype(first), observer<hi::alignment>>) {
420 alignment = hi_forward(first);
421 } else if constexpr (forward_of<decltype(first), observer<hi::semantic_text_style>>) {
422 text_style = hi_forward(first);
423 } else {
424 hi_static_no_default();
425 }
426
427 set_attributes(hi_forward(rest)...);
428 }
429
430 [[nodiscard]] menu_button_widget const *get_first_menu_button() const noexcept
431 {
432 hi_axiom(loop::main().on_thread());
433
434 if (ssize(_menu_button_widgets) != 0) {
435 return _menu_button_widgets.front();
436 } else {
437 return nullptr;
438 }
439 }
440
441 [[nodiscard]] menu_button_widget const *get_selected_menu_button() const noexcept
442 {
443 hi_axiom(loop::main().on_thread());
444
445 for (hilet& button : _menu_button_widgets) {
446 if (button->state() == button_state::on) {
447 return button;
448 }
449 }
450 return nullptr;
451 }
452
453 void start_selecting() noexcept
454 {
455 hi_axiom(loop::main().on_thread());
456
457 _selecting = true;
458 _overlay_widget->mode = widget_mode::enabled;
459 if (auto selected_menu_button = get_selected_menu_button()) {
460 process_event(gui_event::window_set_keyboard_target(selected_menu_button->id, keyboard_focus_group::menu));
461
462 } else if (auto first_menu_button = get_first_menu_button()) {
463 process_event(gui_event::window_set_keyboard_target(first_menu_button->id, keyboard_focus_group::menu));
464 }
465
467 }
468
469 void stop_selecting() noexcept
470 {
471 hi_axiom(loop::main().on_thread());
472 _selecting = false;
473 _overlay_widget->mode = widget_mode::invisible;
475 }
476
477 void repopulate_options() noexcept
478 {
479 hi_axiom(loop::main().on_thread());
480 hi_assert_not_null(delegate);
481
482 _column_widget->clear();
483 _menu_button_widgets.clear();
484 _menu_button_tokens.clear();
485
486 auto [options, selected] = delegate->options_and_selected(*this);
487
488 _has_options = size(options) > 0;
489
490 // If any of the options has a an icon, all of the options should show the icon.
491 auto show_icon = false;
492 for (hilet& label : options) {
493 show_icon |= to_bool(label.icon);
494 }
495
496 decltype(selected) index = 0;
497 for (hilet& label : options) {
498 auto menu_button = &_column_widget->make_widget<menu_button_widget>(selected, index, label, alignment, text_style);
499
500 _menu_button_tokens.push_back(menu_button->pressed.subscribe(
501 [this, index] {
502 hi_assert_not_null(delegate);
503 delegate->set_selected(*this, index);
504 stop_selecting();
505 },
506 callback_flags::main));
507
508 _menu_button_widgets.push_back(menu_button);
509
510 ++index;
511 }
512
513 if (selected == -1) {
514 _off_label_widget->mode = widget_mode::display;
515 _current_label_widget->mode = widget_mode::invisible;
516
517 } else {
518 _off_label_widget->mode = widget_mode::invisible;
519 _current_label_widget->label = options[selected];
520 _current_label_widget->mode = widget_mode::display;
521 }
522 }
523
524 void draw_outline(draw_context const& context) noexcept
525 {
526 context.draw_box(
527 layout(),
528 layout().rectangle(),
529 background_color(),
530 focus_color(),
531 theme().border_width(),
533 theme().rounding_radius());
534 }
535
536 void draw_left_box(draw_context const& context) noexcept
537 {
538 hilet corner_radii = os_settings::left_to_right() ?
539 hi::corner_radii(theme().rounding_radius<float>(), 0.0f, theme().rounding_radius<float>(), 0.0f) :
540 hi::corner_radii(0.0f, theme().rounding_radius<float>(), 0.0f, theme().rounding_radius<float>());
541 context.draw_box(layout(), translate_z(0.1f) * _left_box_rectangle, focus_color(), corner_radii);
542 }
543
544 void draw_chevrons(draw_context const& context) noexcept
545 {
546 context.draw_glyph(layout(), translate_z(0.2f) * _chevrons_rectangle, _chevrons_glyph, label_color());
547 }
548};
549
550}} // namespace hi::v1
Defines delegate_delegate and some default selection delegates.
Defines scroll_widget.
Defines row_column_widget.
Defines widget.
Defines label_widget.
Defines overlay_widget.
Defines menu_button_widget.
@ window_relayout
Request that widgets get laid out on the next frame.
@ window_reconstrain
Request that widget get constraint on the next frame.
@ rectangle
The gui_event has rectangle data.
row_column_widget< axis::y > column_widget
Lays out children in a column.
Definition row_column_widget.hpp:163
scroll_widget< axis::vertical > vertical_scroll_widget
Vertical scroll widget.
Definition scroll_widget.hpp:198
std::shared_ptr< selection_delegate > make_default_selection_delegate(auto &&value, auto &&options, auto &&...off_value) noexcept
Create a shared pointer to a default selection delegate.
Definition selection_delegate.hpp:154
@ on
The 'on' state of a button.
@ partial
A widget is partially enabled.
@ invisible
The widget is invisible.
@ enabled
The widget is fully enabled.
@ display
The widget is in display-only mode.
DOXYGEN BUG.
Definition algorithm.hpp:16
hi_export auto find_glyph(font const &font, grapheme grapheme) noexcept
Find a glyph using the given code-point.
Definition font_book.hpp:437
geometry/margins.hpp
Definition lookahead_iterator.hpp:5
@ inside
The border is drawn inside the edge of a quad.
bool compare_store(T &lhs, U &&rhs) noexcept
Compare then store if there was a change.
Definition misc.hpp:56
constexpr Out narrow_cast(In const &rhs) noexcept
Cast numeric values without loss of precision.
Definition cast.hpp:377
Horizontal/Vertical alignment combination.
Definition alignment.hpp:242
The 4 radii of the corners of a quad or rectangle.
Definition corner_radii.hpp:19
constexpr float & width() noexcept
Access the x-as-width element from the extent.
Definition extent2.hpp:104
constexpr float & height() noexcept
Access the y-as-height element from the extent.
Definition extent2.hpp:115
widget_id id
The numeric identifier of a widget.
Definition widget_intf.hpp:23
widget_intf * parent
Pointer to the parent widget.
Definition widget_intf.hpp:28
A row/column widget lays out child widgets along a row or column.
Definition row_column_widget.hpp:41
The scroll widget allows a content widget to be shown in less space than is required.
Definition scroll_widget.hpp:47
Widget & make_widget(Args &&...args) noexcept
Add a content widget directly to this scroll widget.
Definition scroll_widget.hpp:106
A delegate that controls the state of a selection_widget.
Definition selection_delegate.hpp:24
A graphical control element that allows the user to choose only one of a predefined set of mutually e...
Definition selection_widget.hpp:45
selection_widget(widget *parent, Value &&value, OptionList &&option_list, OffValue &&off_value, Attributes &&...attributes) noexcept
Construct a selection widget which will monitor an option list and a value.
Definition selection_widget.hpp:188
selection_widget(widget *parent, std::shared_ptr< delegate_type > delegate, selection_widget_attribute auto &&first_attribute, selection_widget_attribute auto &&...attributes) noexcept
Construct a selection widget with a delegate.
Definition selection_widget.hpp:128
selection_widget(widget *parent, std::shared_ptr< delegate_type > delegate) noexcept
Construct a selection widget with a delegate.
Definition selection_widget.hpp:91
observer< label > off_label
The label to show when nothing is selected.
Definition selection_widget.hpp:54
observer< semantic_text_style > text_style
The text style to display the label's text in and color of the label's (non-color) icon.
Definition selection_widget.hpp:78
observer< alignment > alignment
How the label and icon are aligned.
Definition selection_widget.hpp:74
selection_widget(widget *parent, Value &&value, OptionList &&option_list, Attributes &&...attributes) noexcept
Construct a selection widget which will monitor an option list and a value.
Definition selection_widget.hpp:154
An interactive graphical object as part of the user-interface.
Definition widget.hpp:37
widget_layout const & layout() const noexcept override
Get the current layout for this widget.
Definition widget.hpp:169
observer< extent2 > minimum
The minimum size this widget is allowed to be.
Definition widget.hpp:79
widget(widget *parent) noexcept
Definition widget.hpp:87
void request_redraw() const noexcept override
Request the widget to be redrawn on the next frame.
Definition widget.hpp:189
bool process_event(gui_event const &event) const noexcept override
Send a event to the window.
Definition widget.hpp:178
observer< widget_mode > mode
The widget mode.
Definition widget.hpp:42
bool handle_event(gui_event const &event) noexcept override
Handle command.
Definition widget.hpp:198
Definition concepts.hpp:54
True if T is a forwarded type of Forward.
Definition concepts.hpp:131
Definition label_widget.hpp:27
Definition selection_widget.hpp:29
T align(T... args)
T clear(T... args)
T exchange(T... args)
T front(T... args)
T max(T... args)
T move(T... args)
T push_back(T... args)