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() + r.margins.left() + r.margins.right();
221 r.minimum.height() += r.margins.bottom() + r.margins.top();
222 r.preferred.width() += theme<prefix>.width() + r.margins.left() + r.margins.right();
223 r.preferred.height() += r.margins.bottom() + r.margins.top();
224 r.maximum.width() += theme<prefix>.width() + 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());
233 inplace_max(r.preferred.width(), _overlay_constraints.preferred.width() + theme<prefix>.width());
234 inplace_max(r.maximum.width(), _overlay_constraints.maximum.width() + theme<prefix>.width());
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{_off_label_constraints, option_rectangle, cap_height};
261
262 } else {
263 _chevron_box_rectangle = aarectanglei{context.width() - label_offset, 0, label_offset, context.height()};
264
265 // The label is located to the left of the selection box icon.
266 hilet option_rectangle = aarectanglei{
267 label_margins.left(),
268 label_margins.bottom(),
269 context.width() - _chevron_box_rectangle.width() - label_margins.left() - label_margins.right(),
270 context.height() - label_margins.bottom() - label_margins.top()};
271
272 _off_label_shape = box_shape{_off_label_constraints, option_rectangle, cap_height};
273 _current_label_shape = box_shape{_off_label_constraints, option_rectangle, cap_height};
274 }
275
276 _chevron_glyph = find_glyph(elusive_icon::ChevronUp);
277 hilet chevron_glyph_bbox =
278 narrow_cast<aarectanglei>(_chevron_glyph.get_bounding_rectangle() * theme<prefix>.line_height(this));
279 _chevron_rectangle = align(_chevron_box_rectangle, chevron_glyph_bbox, alignment::middle_center());
280 }
281
282 // The overlay itself will make sure the overlay fits the window, so we give the preferred size and position
283 // from the point of view of the selection widget.
284 // The overlay should start on the same left edge as the selection box and the same width.
285 // The height of the overlay should be the maximum height, which will show all the options.
286 hilet overlay_width = std::clamp(
287 context.width() - label_offset, _overlay_constraints.minimum.width(), _overlay_constraints.maximum.width());
288 hilet overlay_height = _overlay_constraints.preferred.height();
289 hilet overlay_x = os_settings::left_to_right() ? label_offset : context.width() - label_offset - overlay_width;
290 hilet overlay_y = (context.height() - overlay_height) / 2;
291 hilet overlay_rectangle_request = aarectanglei{overlay_x, overlay_y, overlay_width, overlay_height};
292 hilet overlay_rectangle = make_overlay_rectangle(overlay_rectangle_request);
293 _overlay_shape = box_shape{_overlay_constraints, overlay_rectangle, cap_height};
294 _overlay_widget->set_layout(context.transform(_overlay_shape, 20.0f));
295
296 _off_label_widget->set_layout(context.transform(_off_label_shape));
297 _current_label_widget->set_layout(context.transform(_current_label_shape));
298 }
299
300 void draw(widget_draw_context const& context) noexcept override
301 {
303 if (overlaps(context, layout)) {
304 draw_outline(context);
305 draw_chevron_box(context);
306 draw_chevron(context);
307
308 _off_label_widget->draw(context);
309 _current_label_widget->draw(context);
310 }
311
312 // Overlay is outside of the overlap of the selection widget.
313 _overlay_widget->draw(context);
314 }
315 }
316
317 bool handle_event(gui_event const& event) noexcept override
318 {
319 switch (event.type()) {
320 case gui_event_type::mouse_up:
321 if (*mode >= widget_mode::partial and _has_options and layout.rectangle().contains(event.mouse().position)) {
322 return handle_event(gui_event_type::gui_activate);
323 }
324 return true;
325
326 case gui_event_type::gui_activate_next:
327 // Handle gui_active_next so that the next widget will NOT get keyboard focus.
328 // The previously selected item needs the get keyboard focus instead.
329 case gui_event_type::gui_activate:
330 if (*mode >= widget_mode::partial and _has_options and not _selecting) {
331 start_selecting();
332 } else {
333 stop_selecting();
334 }
335 ++global_counter<"selection_widget:gui_activate:relayout">;
336 process_event({gui_event_type::window_relayout});
337 return true;
338
339 case gui_event_type::gui_cancel:
340 if (*mode >= widget_mode::partial and _has_options and _selecting) {
341 stop_selecting();
342 }
343 ++global_counter<"selection_widget:gui_cancel:relayout">;
344 process_event({gui_event_type::window_relayout});
345 return true;
346
347 default:;
348 }
349
350 return super::handle_event(event);
351 }
352
353 [[nodiscard]] hitbox hitbox_test(point2i position) const noexcept override
354 {
355 hi_axiom(loop::main().on_thread());
356
357 if (*mode >= widget_mode::partial) {
358 auto r = _overlay_widget->hitbox_test_from_parent(position);
359
360 if (layout.contains(position)) {
361 r = std::max(r, hitbox{id, layout.elevation, _has_options ? hitbox_type::button : hitbox_type::_default});
362 }
363
364 return r;
365 } else {
366 return {};
367 }
368 }
369
370 [[nodiscard]] bool accepts_keyboard_focus(keyboard_focus_group group) const noexcept override
371 {
372 hi_axiom(loop::main().on_thread());
373 return *mode >= widget_mode::partial and to_bool(group & hi::keyboard_focus_group::normal) and _has_options;
374 }
376private:
377 notifier<>::callback_token _delegate_cbt;
378 std::atomic<bool> _notification_from_delegate = true;
379
380 std::unique_ptr<label_widget<prefix>> _current_label_widget;
381 box_constraints _current_label_constraints;
382 box_shape _current_label_shape;
383
384 std::unique_ptr<label_widget<join_path(prefix, "off")>> _off_label_widget;
385 box_constraints _off_label_constraints;
386 box_shape _off_label_shape;
387
388 aarectanglei _chevron_box_rectangle;
389
390 font_book::font_glyph_type _chevron_glyph;
391 aarectanglei _chevron_rectangle;
392
393 bool _selecting = false;
394 bool _has_options = false;
395
397 box_constraints _overlay_constraints;
398 box_shape _overlay_shape;
399
400 vertical_scroll_widget<prefix> *_scroll_widget = nullptr;
401 column_widget<prefix> *_column_widget = nullptr;
402
403 decltype(off_label)::callback_token _off_label_cbt;
404 std::vector<menu_button_widget<prefix> *> _menu_button_widgets;
405 std::vector<notifier<>::callback_token> _menu_button_tokens;
406
407 void set_attributes() noexcept {}
408 void set_attributes(label_widget_attribute auto&& first, label_widget_attribute auto&&...rest) noexcept
409 {
410 if constexpr (forward_of<decltype(first), observer<hi::label>>) {
411 off_label = hi_forward(first);
412 } else if constexpr (forward_of<decltype(first), observer<hi::alignment>>) {
413 alignment = hi_forward(first);
414 } else {
416 }
417
418 set_attributes(hi_forward(rest)...);
419 }
420
421 [[nodiscard]] menu_button_widget<prefix> const *get_first_menu_button() const noexcept
422 {
423 hi_axiom(loop::main().on_thread());
424
425 if (ssize(_menu_button_widgets) != 0) {
426 return _menu_button_widgets.front();
427 } else {
428 return nullptr;
429 }
430 }
431
432 [[nodiscard]] menu_button_widget<prefix> const *get_selected_menu_button() const noexcept
433 {
434 hi_axiom(loop::main().on_thread());
435
436 for (hilet& button : _menu_button_widgets) {
437 if (button->state == widget_state::on) {
438 return button;
439 }
440 }
441 return nullptr;
442 }
443
444 void start_selecting() noexcept
445 {
446 hi_axiom(loop::main().on_thread());
447
448 _selecting = true;
449 _overlay_widget->mode = widget_mode::enabled;
450 if (auto selected_menu_button = get_selected_menu_button()) {
451 process_event(gui_event::window_set_keyboard_target(selected_menu_button->id, keyboard_focus_group::menu));
452
453 } else if (auto first_menu_button = get_first_menu_button()) {
454 process_event(gui_event::window_set_keyboard_target(first_menu_button->id, keyboard_focus_group::menu));
455 }
456
458 }
459
460 void stop_selecting() noexcept
461 {
462 hi_axiom(loop::main().on_thread());
463 _selecting = false;
464 _overlay_widget->mode = widget_mode::invisible;
466 }
467
470 void repopulate_options() noexcept
471 {
472 hi_axiom(loop::main().on_thread());
473 hi_assert_not_null(delegate);
474
475 _column_widget->clear();
476 _menu_button_widgets.clear();
477 _menu_button_tokens.clear();
478
479 auto [options, selected] = delegate->options_and_selected(*this);
480
481 _has_options = size(options) > 0;
482
483 // If any of the options has a an icon, all of the options should show the icon.
484 auto show_icon = false;
485 for (hilet& label : options) {
486 show_icon |= to_bool(label.icon);
487 }
488
489 decltype(selected) index = 0;
490 for (hilet& label : options) {
491 auto menu_button = &_column_widget->make_widget<menu_button_widget<prefix>>(selected, index, label, alignment);
492
493 _menu_button_tokens.push_back(menu_button->subscribe(
494 [this, index] {
495 hi_assert_not_null(delegate);
496 delegate->set_selected(*this, index);
497 stop_selecting();
498 },
499 callback_flags::main));
500
501 _menu_button_widgets.push_back(menu_button);
502
503 ++index;
504 }
505
506 if (selected == -1) {
507 _off_label_widget->mode = widget_mode::display;
508 _current_label_widget->mode = widget_mode::invisible;
509
510 } else {
511 _off_label_widget->mode = widget_mode::invisible;
512 _current_label_widget->label = options[selected];
513 _current_label_widget->mode = widget_mode::display;
514 }
515 }
516
517 void draw_outline(widget_draw_context const& context) noexcept
518 {
519 context.draw_box(
520 layout,
521 layout.rectangle(),
522 theme<prefix>.background_color(this),
523 theme<prefix>.border_color(this),
524 theme<prefix>.border_width(this),
526 theme<prefix>.corner_radius(this));
527 }
528
529 void draw_chevron_box(widget_draw_context const& context) noexcept
530 {
531 auto corner_radius = theme<prefix>.corner_radius(this);
532
533 if (os_settings::left_to_right()) {
534 corner_radius.bottom_right() = 0.0f;
535 corner_radius.top_right() = 0.0f;
536 } else {
537 corner_radius.bottom_left() = 0.0f;
538 corner_radius.top_left() = 0.0f;
539 }
540
541 context.draw_box(
542 layout,
543 translate_z(0.1f) * narrow_cast<aarectangle>(_chevron_box_rectangle),
544 theme<prefix>.border_color(this),
545 corner_radius);
546 }
547
548 void draw_chevron(widget_draw_context const& context) noexcept
549 {
550 context.draw_glyph(
551 layout,
552 translate_z(0.2f) * narrow_cast<aarectangle>(_chevron_rectangle),
553 *_chevron_glyph.font,
554 _chevron_glyph.glyph,
555 theme<prefix>.fill_color(this));
556 }
557};
558
559}} // 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:234
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:270
virtual bool handle_event(gui_event const &event) noexcept
Handle command.
Definition widget.hpp:279
widget * parent
Pointer to the parent widget.
Definition widget.hpp:40
observer< widget_mode > mode
The widget mode.
Definition widget.hpp:53
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)