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 "../GUI/module.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.hpp"
19#include <memory>
20#include <string>
21#include <array>
22#include <optional>
23#include <future>
24
25namespace hi { inline namespace v1 {
26
27template<typename Context>
29
44template<fixed_string Name = "">
45class selection_widget final : public widget {
46public:
47 using super = widget;
48 constexpr static auto prefix = Name / "selection";
49
50 using delegate_type = selection_delegate;
51
53
56 observer<label> off_label;
57
76 observer<alignment> alignment = hi::alignment::middle_flush();
77
79 {
80 hi_assert_not_null(delegate);
81 delegate->deinit(*this);
82 }
83
90 super(parent), delegate(std::move(delegate))
91 {
92 hi_assert_not_null(this->delegate);
93
94 _current_label_widget = std::make_unique<label_widget<prefix>>(this, alignment);
95 _current_label_widget->mode = widget_mode::invisible;
96 _off_label_widget = std::make_unique<label_widget<prefix / "off">>(this, off_label, alignment);
97
98 _overlay_widget = std::make_unique<overlay_widget<prefix>>(this);
99 _overlay_widget->mode = widget_mode::invisible;
100 _scroll_widget = &_overlay_widget->make_widget<vertical_scroll_widget<prefix>>();
101 _column_widget = &_scroll_widget->make_widget<column_widget<prefix>>();
102
103 _off_label_cbt = this->off_label.subscribe([&](auto...) {
104 ++global_counter<"selection_widget:off_label:constrain">;
105 process_event({gui_event_type::window_reconstrain});
106 });
107
108 _delegate_cbt = this->delegate->subscribe([&] {
109 _notification_from_delegate = true;
110 ++global_counter<"selection_widget:delegate:constrain">;
111 process_event({gui_event_type::window_reconstrain});
112 });
113
114 this->delegate->init(*this);
115 }
126 widget *parent,
128 selection_widget_attribute auto&& first_attribute,
129 selection_widget_attribute auto&&...attributes) noexcept :
131 {
132 set_attributes(hi_forward(first_attribute), hi_forward(attributes)...);
133 }
134
147 template<
148 different_from<std::shared_ptr<delegate_type>> Value,
149 forward_of<observer<std::vector<std::pair<observer_decay_t<Value>, label>>>> OptionList,
150 selection_widget_attribute... Attributes>
151 selection_widget(widget *parent, Value&& value, OptionList&& option_list, Attributes&&...attributes) noexcept
152 requires requires { make_default_selection_delegate(hi_forward(value), hi_forward(option_list)); }
153 :
155 parent,
157 hi_forward(attributes)...)
158 {
159 }
160
175 template<
176 different_from<std::shared_ptr<delegate_type>> Value,
177 forward_of<observer<std::vector<std::pair<observer_decay_t<Value>, label>>>> OptionList,
178 forward_of<observer<observer_decay_t<Value>>> OffValue,
179 selection_widget_attribute... Attributes>
181 widget *parent,
182 Value&& value,
183 OptionList&& option_list,
184 OffValue&& off_value,
185 Attributes&&...attributes) noexcept
186 requires requires { make_default_selection_delegate(hi_forward(value), hi_forward(option_list), hi_forward(off_value)); }
187 :
189 parent,
190 make_default_selection_delegate(hi_forward(value), hi_forward(option_list), hi_forward(off_value)),
191 hi_forward(attributes)...)
192 {
193 }
194
196 [[nodiscard]] generator<widget const&> children(bool include_invisible) const noexcept override
197 {
198 co_yield *_overlay_widget;
199 co_yield *_current_label_widget;
200 co_yield *_off_label_widget;
201 }
202
203 [[nodiscard]] box_constraints update_constraints() noexcept override
204 {
205 hi_assert_not_null(_off_label_widget);
206 hi_assert_not_null(_current_label_widget);
207 hi_assert_not_null(_overlay_widget);
208
209 if (_notification_from_delegate.exchange(false)) {
210 repopulate_options();
211 }
212
213 _off_label_constraints = _off_label_widget->update_constraints();
214 _current_label_constraints = _current_label_widget->update_constraints();
215 _overlay_constraints = _overlay_widget->update_constraints();
216
217 // The theme's width is used for the little chevron/icon element.
218 // The labels' margins are included in the size of the widget.
219 auto r = max(_off_label_constraints, _current_label_constraints);
220 r.minimum.width() += theme<prefix>.width(this) + r.margins.left() + r.margins.right();
221 r.minimum.height() += r.margins.bottom() + r.margins.top();
222 r.preferred.width() += theme<prefix>.width(this) + r.margins.left() + r.margins.right();
223 r.preferred.height() += r.margins.bottom() + r.margins.top();
224 r.maximum.width() += theme<prefix>.width(this) + r.margins.left() + r.margins.right();
225 r.maximum.height() += r.margins.bottom() + r.margins.top();
226
227 // Make it so that the scroll widget can scroll vertically.
228 // Set it to something small, this just fixes an issue when there are no menu items.
229 _scroll_widget->minimum.copy()->height() = 10;
230
231 // Increase the width to match the popup's width.
232 inplace_max(r.minimum.width(), _overlay_constraints.minimum.width() + theme<prefix>.width(this));
233 inplace_max(r.preferred.width(), _overlay_constraints.preferred.width() + theme<prefix>.width(this));
234 inplace_max(r.maximum.width(), _overlay_constraints.maximum.width() + theme<prefix>.width(this));
235
236 r.alignment = resolve(*alignment, os_settings::left_to_right());
237 r.margins = theme<prefix>.margin(this);
238 hi_axiom(r.holds_invariant());
239 return r;
240 }
241
242 void set_layout(widget_layout const& context) noexcept override
243 {
244 hilet label_margins = max(_off_label_constraints.margins, _current_label_constraints.margins);
245 hilet chevron_box_width = theme<prefix>.width(this);
246 hilet cap_height = theme<prefix>.cap_height(this);
247
248 if (compare_store(layout, context)) {
249 if (os_settings::left_to_right()) {
250 _chevron_box_rectangle = aarectanglei{0, 0, chevron_box_width, context.height()};
251
252 // The label is located to the right of the selection box icon.
253 hilet option_rectangle = aarectanglei{
254 chevron_box_width + label_margins.left(),
255 label_margins.bottom(),
256 context.width() - chevron_box_width - label_margins.left() - label_margins.right(),
257 context.height() - label_margins.bottom() - label_margins.top()};
258
259 _off_label_shape = box_shape{_off_label_constraints, option_rectangle, cap_height};
260 _current_label_shape = box_shape{_current_label_constraints, option_rectangle, cap_height};
261
262 } else {
263 _chevron_box_rectangle =
264 aarectanglei{context.width() - chevron_box_width, 0, chevron_box_width, context.height()};
265
266 // The label is located to the left of the selection box icon.
267 hilet option_rectangle = aarectanglei{
268 label_margins.left(),
269 label_margins.bottom(),
270 context.width() - _chevron_box_rectangle.width() - label_margins.left() - label_margins.right(),
271 context.height() - label_margins.bottom() - label_margins.top()};
272
273 _off_label_shape = box_shape{_off_label_constraints, option_rectangle, cap_height};
274 _current_label_shape = box_shape{_current_label_constraints, option_rectangle, cap_height};
275 }
276
277 _chevron_glyph = find_glyph(elusive_icon::ChevronUp);
278 hilet chevron_glyph_bbox =
279 narrow_cast<aarectanglei>(_chevron_glyph.get_bounding_rectangle() * theme<prefix>.line_height(this));
280 _chevron_rectangle = align(_chevron_box_rectangle, chevron_glyph_bbox, alignment::middle_center());
281 }
282
283 // The overlay itself will make sure the overlay fits the window, so we give the preferred size and position
284 // from the point of view of the selection widget.
285 // The overlay should start on the same left edge as the selection box and the same width.
286 // The height of the overlay should be the maximum height, which will show all the options.
287 hilet overlay_width = std::clamp(
288 context.width() - chevron_box_width, _overlay_constraints.minimum.width(), _overlay_constraints.maximum.width());
289 hilet overlay_height = _overlay_constraints.preferred.height();
290 hilet overlay_x = os_settings::left_to_right() ? chevron_box_width : context.width() - chevron_box_width - overlay_width;
291 hilet overlay_y = (context.height() - overlay_height) / 2;
292 hilet overlay_rectangle_request = aarectanglei{overlay_x, overlay_y, overlay_width, overlay_height};
293 hilet overlay_rectangle = make_overlay_rectangle(overlay_rectangle_request);
294 _overlay_shape = box_shape{_overlay_constraints, overlay_rectangle, cap_height};
295 _overlay_widget->set_layout(context.transform(_overlay_shape, 20.0f));
296
297 _off_label_widget->set_layout(context.transform(_off_label_shape));
298 _current_label_widget->set_layout(context.transform(_current_label_shape));
299 }
300
301 void draw(widget_draw_context& context) noexcept override
302 {
304 if (overlaps(context, layout)) {
305 draw_outline(context);
306 draw_chevron_box(context);
307 draw_chevron(context);
308
309 _off_label_widget->draw(context);
310 _current_label_widget->draw(context);
311 }
312
313 // Overlay is outside of the overlap of the selection widget.
314 _overlay_widget->draw(context);
315 }
316 }
317
318 bool handle_event(gui_event const& event) noexcept override
319 {
320 switch (event.type()) {
321 case gui_event_type::mouse_up:
322 if (*mode >= widget_mode::partial and _has_options and layout.rectangle().contains(event.mouse().position)) {
323 return handle_event(gui_event_type::gui_activate);
324 }
325 return true;
326
327 case gui_event_type::gui_activate_next:
328 // Handle gui_active_next so that the next widget will NOT get keyboard focus.
329 // The previously selected item needs the get keyboard focus instead.
330 case gui_event_type::gui_activate:
331 if (*mode >= widget_mode::partial and _has_options and not _selecting) {
332 start_selecting();
333 } else {
334 stop_selecting();
335 }
336 ++global_counter<"selection_widget:gui_activate:relayout">;
337 process_event({gui_event_type::window_relayout});
338 return true;
339
340 case gui_event_type::gui_cancel:
341 if (*mode >= widget_mode::partial and _has_options and _selecting) {
342 stop_selecting();
343 }
344 ++global_counter<"selection_widget:gui_cancel:relayout">;
345 process_event({gui_event_type::window_relayout});
346 return true;
347
348 default:;
349 }
350
351 return super::handle_event(event);
352 }
353
354 [[nodiscard]] hitbox hitbox_test(point2i position) const noexcept override
355 {
356 hi_axiom(loop::main().on_thread());
357
358 if (*mode >= widget_mode::partial) {
359 auto r = _overlay_widget->hitbox_test_from_parent(position);
360
361 if (layout.contains(position)) {
362 r = std::max(r, hitbox{id, layout.elevation, _has_options ? hitbox_type::button : hitbox_type::_default});
363 }
364
365 return r;
366 } else {
367 return {};
368 }
369 }
370
371 [[nodiscard]] bool accepts_keyboard_focus(keyboard_focus_group group) const noexcept override
372 {
373 hi_axiom(loop::main().on_thread());
374 return *mode >= widget_mode::partial and to_bool(group & hi::keyboard_focus_group::normal) and _has_options;
375 }
377private:
378 notifier<>::callback_token _delegate_cbt;
379 std::atomic<bool> _notification_from_delegate = true;
380
381 std::unique_ptr<label_widget<prefix>> _current_label_widget;
382 box_constraints _current_label_constraints;
383 box_shape _current_label_shape;
384
385 std::unique_ptr<label_widget<join_path(prefix, "off")>> _off_label_widget;
386 box_constraints _off_label_constraints;
387 box_shape _off_label_shape;
388
389 aarectanglei _chevron_box_rectangle;
390
391 font_book::font_glyph_type _chevron_glyph;
392 aarectanglei _chevron_rectangle;
393
394 bool _selecting = false;
395 bool _has_options = false;
396
398 box_constraints _overlay_constraints;
399 box_shape _overlay_shape;
400
401 vertical_scroll_widget<prefix> *_scroll_widget = nullptr;
402 column_widget<prefix> *_column_widget = nullptr;
403
404 decltype(off_label)::callback_token _off_label_cbt;
405 std::vector<menu_button_widget<prefix> *> _menu_button_widgets;
406 std::vector<notifier<>::callback_token> _menu_button_tokens;
407
408 void set_attributes() noexcept {}
409 void set_attributes(label_widget_attribute auto&& first, label_widget_attribute auto&&...rest) noexcept
410 {
411 if constexpr (forward_of<decltype(first), observer<hi::label>>) {
412 off_label = hi_forward(first);
413 } else if constexpr (forward_of<decltype(first), observer<hi::alignment>>) {
414 alignment = hi_forward(first);
415 } else {
417 }
418
419 set_attributes(hi_forward(rest)...);
420 }
421
422 [[nodiscard]] menu_button_widget<prefix> const *get_first_menu_button() const noexcept
423 {
424 hi_axiom(loop::main().on_thread());
425
426 if (ssize(_menu_button_widgets) != 0) {
427 return _menu_button_widgets.front();
428 } else {
429 return nullptr;
430 }
431 }
432
433 [[nodiscard]] menu_button_widget<prefix> const *get_selected_menu_button() const noexcept
434 {
435 hi_axiom(loop::main().on_thread());
436
437 for (hilet& button : _menu_button_widgets) {
438 if (button->state == widget_state::on) {
439 return button;
440 }
441 }
442 return nullptr;
443 }
444
445 void start_selecting() noexcept
446 {
447 hi_axiom(loop::main().on_thread());
448
449 _selecting = true;
450 _overlay_widget->mode = widget_mode::enabled;
451 if (auto selected_menu_button = get_selected_menu_button()) {
452 process_event(gui_event::window_set_keyboard_target(selected_menu_button->id, keyboard_focus_group::menu));
453
454 } else if (auto first_menu_button = get_first_menu_button()) {
455 process_event(gui_event::window_set_keyboard_target(first_menu_button->id, keyboard_focus_group::menu));
456 }
457
459 }
460
461 void stop_selecting() noexcept
462 {
463 hi_axiom(loop::main().on_thread());
464 _selecting = false;
465 _overlay_widget->mode = widget_mode::invisible;
467 }
468
471 void repopulate_options() noexcept
472 {
473 hi_axiom(loop::main().on_thread());
474 hi_assert_not_null(delegate);
475
476 _column_widget->clear();
477 _menu_button_widgets.clear();
478 _menu_button_tokens.clear();
479
480 auto [options, selected] = delegate->options_and_selected(*this);
481
482 _has_options = size(options) > 0;
483
484 // If any of the options has a an icon, all of the options should show the icon.
485 auto show_icon = false;
486 for (hilet& label : options) {
487 show_icon |= to_bool(label.icon);
488 }
489
490 decltype(selected) index = 0;
491 for (hilet& label : options) {
492 auto menu_button = &_column_widget->make_widget<menu_button_widget<prefix>>(selected, index, label, alignment);
493
494 _menu_button_tokens.push_back(menu_button->subscribe(
495 [this, index] {
496 hi_assert_not_null(delegate);
497 delegate->set_selected(*this, index);
498 stop_selecting();
499 },
500 callback_flags::main));
501
502 _menu_button_widgets.push_back(menu_button);
503
504 ++index;
505 }
506
507 if (selected == -1) {
508 _off_label_widget->mode = widget_mode::display;
509 _current_label_widget->mode = widget_mode::invisible;
510
511 } else {
512 _off_label_widget->mode = widget_mode::invisible;
513 _current_label_widget->label = options[selected];
514 _current_label_widget->mode = widget_mode::display;
515 }
516 }
517
518 void draw_outline(widget_draw_context& context) noexcept
519 {
520 context.draw_box(
521 layout,
522 layout.rectangle(),
523 theme<prefix>.background_color(this),
524 theme<prefix>.border_color(this),
525 theme<prefix>.border_width(this),
527 theme<prefix>.border_radius(this));
528 }
529
530 void draw_chevron_box(widget_draw_context& context) noexcept
531 {
532 auto border_radius = theme<prefix>.border_radius(this);
533
534 if (os_settings::left_to_right()) {
535 border_radius.right_bottom() = 0;
536 border_radius.right_top() = 0;
537 } else {
538 border_radius.left_bottom() = 0;
539 border_radius.left_top() = 0;
540 }
541
542 context.draw_box(
543 layout,
544 translate_z(0.1f) * narrow_cast<aarectangle>(_chevron_box_rectangle),
545 theme<prefix>.border_color(this),
546 border_radius);
547 }
548
549 void draw_chevron(widget_draw_context& context) noexcept
550 {
551 context.draw_glyph(
552 layout,
553 translate_z(0.2f) * narrow_cast<aarectangle>(_chevron_rectangle),
554 *_chevron_glyph.font,
555 _chevron_glyph.glyph,
556 theme<prefix>.fill_color(this));
557 }
558};
559
560}} // namespace hi::v1
Defines delegate_delegate and some default selection delegates.
Defines scroll_widget.
Defines row_column_widget.
Defines label_widget.
Defines overlay_widget.
Defines menu_button_widget.
#define hi_static_no_default(...)
This part of the code should not be reachable, unless a programming bug.
Definition assert.hpp:323
#define hi_axiom(expression,...)
Specify an axiom; an expression that is true.
Definition assert.hpp:253
#define hi_assert_not_null(x,...)
Assert if an expression is not nullptr.
Definition assert.hpp:238
#define hilet
Invariant should be the default for variables.
Definition utility.hpp:23
#define hi_forward(x)
Forward a value, based on the decltype of the value.
Definition utility.hpp:29
@ window_relayout
Request that widgets get laid out on the next frame.
@ window_reconstrain
Request that widget get constraint on the next frame.
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:152
@ 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:13
auto find_glyph(font const &font, grapheme grapheme) noexcept
Find a glyph using the given code-point.
Definition font_book.hpp:223
constexpr auto join_path(fixed_string< L > const &lhs, fixed_string< R > const &rhs) noexcept
lhs / rhs
Definition fixed_string.hpp:273
geometry/margins.hpp
Definition cache.hpp:11
@ inside
The border is drawn inside the edge of a quad.
@ on
The widget is in the on-state.
bool compare_store(T &lhs, U &&rhs) noexcept
Compare then store if there was a change.
Definition utility.hpp:212
constexpr bool contains(point< value_type, 2 > const &rhs) const noexcept
Check if a 2D coordinate is inside the rectangle.
Definition axis_aligned_rectangle.hpp:265
constexpr value_type & width() noexcept
Access the x-as-width element from the extent.
Definition extent.hpp:166
constexpr value_type & height() noexcept
Access the y-as-height element from the extent.
Definition extent.hpp:177
Definition widget.hpp:26
widget_id id
The numeric identifier of a widget.
Definition widget.hpp:35
virtual void request_redraw() const noexcept
Request the widget to be redrawn on the next frame.
Definition widget.hpp:265
virtual bool handle_event(gui_event const &event) noexcept
Handle command.
Definition widget.hpp:274
widget * parent
Pointer to the parent widget.
Definition widget.hpp:40
observer< widget_mode > mode
The widget mode.
Definition widget.hpp:49
constexpr bool contains(point3i mouse_position) const noexcept
Check if the mouse position is inside the widget.
Definition widget_layout.hpp:126
float elevation
The elevation of the widget above the window.
Definition widget_layout.hpp:72
The GUI widget displays and lays out text together with an icon.
Definition label_widget.hpp:42
A row/column widget lays out child widgets along a row or column.
Definition row_column_widget.hpp:39
The scroll widget allows a content widget to be shown in less space than is required.
Definition scroll_widget.hpp:46
A delegate that controls the state of a selection_widget.
Definition selection_delegate.hpp:22
A graphical control element that allows the user to choose only one of a predefined set of mutually e...
Definition selection_widget.hpp:45
observer< label > off_label
The label to show when nothing is selected.
Definition selection_widget.hpp:56
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:180
selection_widget(widget *parent, std::shared_ptr< delegate_type > delegate) noexcept
Construct a selection widget with a delegate.
Definition selection_widget.hpp:89
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:151
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:125
observer< alignment > alignment
How the label and icon are aligned.
Definition selection_widget.hpp:76
Definition label_widget.hpp:26
Definition selection_widget.hpp:28
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)