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
8
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 "grid_widget.hpp"
16#include "radio_widget.hpp"
18#include "../observer/observer.hpp"
19#include "../macros.hpp"
20#include <memory>
21#include <string>
22#include <array>
23#include <optional>
24#include <future>
25#include <coroutine>
26
27hi_export_module(hikogui.widgets.selection_widget);
28
29hi_export namespace hi { inline namespace v1 {
30
31template<typename Context>
33
48class selection_widget : public widget {
49public:
50 using super = widget;
51 using delegate_type = selection_delegate;
52
53 struct attributes_type {
57
58 observer<alignment> alignment = hi::alignment::middle_left();
59
60 attributes_type(attributes_type const&) noexcept = default;
61 attributes_type(attributes_type&&) noexcept = default;
62 attributes_type& operator=(attributes_type const&) noexcept = default;
63 attributes_type& operator=(attributes_type&&) noexcept = default;
64
65 template<selection_widget_attribute... Attributes>
66 explicit attributes_type(Attributes&&...attributes) noexcept
67 {
68 set_attributes(std::forward<Attributes>(attributes)...);
69 }
70
71 void set_attributes() noexcept {}
72
74 void set_attributes(First&& first, Rest&&...rest) noexcept
75 {
78 } else if constexpr (forward_of<First, observer<hi::alignment>>) {
80 } else {
81 hi_static_no_default();
82 }
83
84 set_attributes(std::forward<Rest>(rest)...);
85 }
86 };
87
88 attributes_type attributes;
89
90 std::shared_ptr<delegate_type> delegate;
91
92 template<typename... Args>
93 [[nodiscard]] static std::shared_ptr<delegate_type> make_default_delegate(Args &&...args)
94 requires requires { make_shared_ctad<default_selection_delegate>(std::forward<Args>(args)...); }
95 {
96 return make_shared_ctad<default_selection_delegate>(std::forward<Args>(args)...);
97 }
98
100 {
101 delegate->deinit(*this);
102 }
103
110 super(), attributes(std::move(attributes)), delegate(std::move(delegate))
111 {
112 _current_label_widget = std::make_unique<label_widget>(this->attributes.alignment);
113 _current_label_widget->set_parent(this);
114 _current_label_widget->set_mode(widget_mode::invisible);
115
116 _off_label_widget = std::make_unique<label_widget>(this->attributes.off_label, this->attributes.alignment);
117 _off_label_widget->set_parent(this);
118
119 _overlay_widget = std::make_unique<overlay_widget>();
120 _overlay_widget->set_parent(this);
121 _overlay_widget->set_mode(widget_mode::invisible);
122
123 _scroll_widget = &_overlay_widget->emplace<vertical_scroll_widget>();
124 _grid_widget = &_scroll_widget->emplace<grid_widget>();
125
126 _off_label_cbt = this->attributes.off_label.subscribe([&](auto...) {
127 ++global_counter<"selection_widget:off_label:constrain">;
129 });
130
131 _delegate_options_cbt = this->delegate->subscribe_on_options([&] {
132 update_options();
133 }, callback_flags::main);
134 _delegate_options_cbt();
135
136 _delegate_value_cbt = this->delegate->subscribe_on_value([&] {
137 update_value();
138 }, callback_flags::main);
139 _delegate_value_cbt();
140
141 hi_axiom_not_null(this->delegate);
142 this->delegate->init(*this);
143 }
144
157 template<
160 selection_widget_attribute... Attributes>
162 Value&& value,
163 OptionList&& option_list,
164 Attributes&&...attributes) noexcept requires requires
165 {
166 make_default_delegate(std::forward<Value>(value), std::forward<OptionList>(option_list));
168 } :
170 attributes_type{std::forward<Attributes>(attributes)...},
171 make_default_delegate(std::forward<Value>(value), std::forward<OptionList>(option_list)))
172 {
173 }
174
176 [[nodiscard]] generator<widget_intf&> children(bool include_invisible) noexcept override
177 {
178 co_yield *_overlay_widget;
179 co_yield *_current_label_widget;
180 co_yield *_off_label_widget;
181 }
182
183 [[nodiscard]] box_constraints update_constraints() noexcept override
184 {
185 hi_assert_not_null(_off_label_widget);
186 hi_assert_not_null(_current_label_widget);
187 hi_assert_not_null(_overlay_widget);
188
189 _layout = {};
190 _off_label_constraints = _off_label_widget->update_constraints();
191 _current_label_constraints = _current_label_widget->update_constraints();
192 _overlay_constraints = _overlay_widget->update_constraints();
193
194 auto const extra_size = extent2{theme().size() + theme().margin<float>() * 2.0f, theme().margin<float>() * 2.0f};
195
196 auto r = max(_off_label_constraints + extra_size, _current_label_constraints + extra_size);
197
198 // Make it so that the scroll widget can scroll vertically.
199 _scroll_widget->minimum->height() = theme().size();
200
201 r.minimum.width() = std::max(r.minimum.width(), _overlay_constraints.minimum.width() + extra_size.width());
202 r.preferred.width() = std::max(r.preferred.width(), _overlay_constraints.preferred.width() + extra_size.width());
203 r.maximum.width() = std::max(r.maximum.width(), _overlay_constraints.maximum.width() + extra_size.width());
204 r.margins = theme().margin();
205 r.alignment = resolve(*attributes.alignment, os_settings::left_to_right());
206 hi_axiom(r.holds_invariant());
207 return r;
208 }
209
210 void set_layout(widget_layout const& context) noexcept override
211 {
212 if (compare_store(_layout, context)) {
213 if (os_settings::left_to_right()) {
214 _left_box_rectangle = aarectangle{0.0f, 0.0f, theme().size(), context.height()};
215
216 // The unknown_label is located to the right of the selection box icon.
217 auto const option_rectangle = aarectangle{
218 _left_box_rectangle.right() + theme().margin<float>(),
219 0.0f,
220 context.width() - _left_box_rectangle.width() - theme().margin<float>() * 2.0f,
221 context.height()};
222 _off_label_shape = box_shape{_off_label_constraints, option_rectangle, theme().baseline_adjustment()};
223 _current_label_shape = box_shape{_off_label_constraints, option_rectangle, theme().baseline_adjustment()};
224
225 } else {
226 _left_box_rectangle = aarectangle{context.width() - theme().size(), 0.0f, theme().size(), context.height()};
227
228 // The unknown_label is located to the left of the selection box icon.
229 auto const option_rectangle = aarectangle{
230 theme().margin<float>(),
231 0.0f,
232 context.width() - _left_box_rectangle.width() - theme().margin<float>() * 2.0f,
233 context.height()};
234 _off_label_shape = box_shape{_off_label_constraints, option_rectangle, theme().baseline_adjustment()};
235 _current_label_shape = box_shape{_off_label_constraints, option_rectangle, theme().baseline_adjustment()};
236 }
237
238 _chevrons_glyph = find_glyph(elusive_icon::ChevronUp);
239 auto const chevrons_glyph_bbox = _chevrons_glyph.front_glyph_metrics().bounding_rectangle * theme().icon_size();
240 _chevrons_rectangle = align(_left_box_rectangle, chevrons_glyph_bbox, alignment::middle_center());
241 }
242
243 // The overlay itself will make sure the overlay fits the window, so we give the preferred size and position
244 // from the point of view of the selection widget.
245 // The overlay should start on the same left edge as the selection box and the same width.
246 // The height of the overlay should be the maximum height, which will show all the options.
247 auto const overlay_width = std::clamp(
248 context.width() - theme().size(), _overlay_constraints.minimum.width(), _overlay_constraints.maximum.width());
249 auto const overlay_height = _overlay_constraints.preferred.height();
250 auto const overlay_x = os_settings::left_to_right() ? theme().size() : context.width() - theme().size() - overlay_width;
251 auto const overlay_y = (context.height() - overlay_height) / 2;
252 auto const overlay_rectangle_request = aarectangle{overlay_x, overlay_y, overlay_width, overlay_height};
253 auto const overlay_rectangle = make_overlay_rectangle(overlay_rectangle_request);
254 _overlay_shape = box_shape{_overlay_constraints, overlay_rectangle, theme().baseline_adjustment()};
255 _overlay_widget->set_layout(context.transform(_overlay_shape, transform_command::overlay));
256
257 _off_label_widget->set_layout(context.transform(_off_label_shape));
258 _current_label_widget->set_layout(context.transform(_current_label_shape));
259 }
260
261 void draw(draw_context const& context) noexcept override
262 {
263 animate_overlay(context.display_time_point);
264
265 if (mode() > widget_mode::invisible) {
266 if (overlaps(context, layout())) {
267 draw_outline(context);
268 draw_left_box(context);
269 draw_chevrons(context);
270
271 _off_label_widget->draw(context);
272 _current_label_widget->draw(context);
273 }
274
275 // Overlay is outside of the overlap of the selection widget.
276 _overlay_widget->draw(context);
277 }
278 }
279
280 bool handle_event(gui_event const& event) noexcept override
281 {
282 switch (event.type()) {
283 case gui_event_type::mouse_up:
284 if (mode() >= widget_mode::partial and not delegate->empty(*this) and layout().rectangle().contains(event.mouse().position)) {
285 return handle_event(gui_event_type::gui_activate);
286 }
287 return true;
288
289 case gui_event_type::gui_activate_next:
290 // Handle gui_active_next so that the next widget will NOT get keyboard focus.
291 // The previously selected item needs the get keyboard focus instead.
292 case gui_event_type::gui_activate:
293 if (mode() >= widget_mode::partial and not delegate->empty(*this) and overlay_closed()) {
294 open_overlay();
295 } else {
296 close_overlay();
297 }
298 ++global_counter<"selection_widget:gui_activate:relayout">;
300 return true;
301
302 case gui_event_type::gui_cancel:
303 close_overlay();
304 return true;
305
306 default:;
307 }
308
309 return super::handle_event(event);
310 }
311
312 [[nodiscard]] hitbox hitbox_test(point2 position) const noexcept override
313 {
314 hi_axiom(loop::main().on_thread());
315
316 if (mode() >= widget_mode::partial) {
317 auto r = _overlay_widget->hitbox_test_from_parent(position);
318
319 if (layout().contains(position)) {
320 r = std::max(r, hitbox{id, _layout.elevation, not delegate->empty(*this) ? hitbox_type::button : hitbox_type::_default});
321 }
322
323 return r;
324 } else {
325 return {};
326 }
327 }
328
329 [[nodiscard]] bool accepts_keyboard_focus(keyboard_focus_group group) const noexcept override
330 {
331 hi_axiom(loop::main().on_thread());
332 return mode() >= widget_mode::partial and to_bool(group & hi::keyboard_focus_group::normal) and not delegate->empty(*this);
333 }
334
335 [[nodiscard]] color focus_color() const noexcept override
336 {
337 hi_axiom(loop::main().on_thread());
338
339 if (mode() >= widget_mode::partial and not overlay_closed()) {
340 return theme().accent_color();
341 } else {
342 return super::focus_color();
343 }
344 }
345
347private:
348 enum class overlay_state_type {
349 open,
350 closing,
351 closed
352 };
353
354 constexpr static std::chrono::nanoseconds _overlay_close_delay = std::chrono::milliseconds(200);
355
356 overlay_state_type _overlay_state = overlay_state_type::closed;
357 utc_nanoseconds _overlay_close_start = {};
358
359 bool _notification_from_delegate = true;
360
361 std::unique_ptr<label_widget> _current_label_widget;
362 box_constraints _current_label_constraints;
363 box_shape _current_label_shape;
364
365 std::unique_ptr<label_widget> _off_label_widget;
366 box_constraints _off_label_constraints;
367 box_shape _off_label_shape;
368
369 aarectangle _left_box_rectangle;
370
371 font_glyph_ids _chevrons_glyph;
372 aarectangle _chevrons_rectangle;
373
374 std::unique_ptr<overlay_widget> _overlay_widget;
375 box_constraints _overlay_constraints;
376 box_shape _overlay_shape;
377
378 vertical_scroll_widget *_scroll_widget = nullptr;
379 grid_widget *_grid_widget = nullptr;
380
381 callback<void()> _delegate_options_cbt;
382 callback<void()> _delegate_value_cbt;
383 callback<void(label)> _off_label_cbt;
384
385 [[nodiscard]] bool overlay_closed() const noexcept
386 {
387 return _overlay_state == overlay_state_type::closed;
388 }
389
390 void open_overlay() noexcept
391 {
392 hi_axiom(loop::main().on_thread());
393
394 if (auto focus_id = delegate->keyboard_focus_id(*this)) {
395 _overlay_state = overlay_state_type::open;
396 _overlay_widget->set_mode(widget_mode::enabled);
397 process_event(gui_event::window_set_keyboard_target(*focus_id, keyboard_focus_group::menu));
399 }
400 }
401
402 void close_overlay() noexcept
403 {
404 hi_axiom(loop::main().on_thread());
405
406 if (_overlay_state == overlay_state_type::open) {
407 _overlay_state = overlay_state_type::closing;
408 _overlay_close_start = std::chrono::utc_clock::now();
410 }
411 }
412
413 void force_close_overlay() noexcept
414 {
415 if (_overlay_state != overlay_state_type::closed) {
416 _overlay_state = overlay_state_type::closed;
417 _overlay_widget->set_mode(widget_mode::invisible);
419 }
420 }
421
422 void animate_overlay(utc_nanoseconds display_time_point) noexcept
423 {
424 hi_axiom(loop::main().on_thread());
425
426 switch (_overlay_state) {
427 case overlay_state_type::open:
428 break;
429 case overlay_state_type::closing:
430 if (display_time_point >= _overlay_close_start + _overlay_close_delay) {
431 force_close_overlay();
432 } else {
434 }
435 break;
436 case overlay_state_type::closed:
437 break;
438 default:
439 hi_no_default();
440 }
441 }
442
443 void update_options() noexcept
444 {
445 _grid_widget->clear();
446 for (auto i = 0_uz; i != delegate->size(*this); ++i) {
447 _grid_widget->push_bottom(delegate->make_option_widget(*_grid_widget, i));
448 }
449
450 ++global_counter<"selection_widget:update_options:constrain">;
452 }
453
454 void update_value() noexcept
455 {
456 if (auto selected_label = delegate->selected_label(*this)) {
457 _off_label_widget->set_mode(widget_mode::invisible);
458 _current_label_widget->label = *selected_label;
459 _current_label_widget->set_mode(widget_mode::display);
460
461 } else {
462 _off_label_widget->set_mode(widget_mode::display);
463 _current_label_widget->set_mode(widget_mode::invisible);
464 }
465
466 close_overlay();
467 }
468
469 void draw_outline(draw_context const& context) noexcept
470 {
471 context.draw_box(
472 layout(),
473 layout().rectangle(),
474 background_color(),
475 focus_color(),
476 theme().border_width(),
478 theme().rounding_radius());
479 }
480
481 void draw_left_box(draw_context const& context) noexcept
482 {
483 auto const corner_radii = os_settings::left_to_right() ?
484 hi::corner_radii(theme().rounding_radius<float>(), 0.0f, theme().rounding_radius<float>(), 0.0f) :
485 hi::corner_radii(0.0f, theme().rounding_radius<float>(), 0.0f, theme().rounding_radius<float>());
486 context.draw_box(layout(), translate_z(0.1f) * _left_box_rectangle, focus_color(), corner_radii);
487 }
488
489 void draw_chevrons(draw_context const& context) noexcept
490 {
491 context.draw_glyph(layout(), translate_z(0.2f) * _chevrons_rectangle, _chevrons_glyph, background_color());
492 }
493};
494
495}} // namespace hi::v1
Defines widget.
Defines radio_widget.
Defines label_widget.
Defines overlay_widget.
Defines scroll_widget.
Defines delegate_delegate and some default selection delegates.
Defines grid_widget.
@ window_relayout
Request that widgets get laid out on the next frame.
Definition gui_event_type.hpp:47
@ window_reconstrain
Request that widget get constraint on the next frame.
Definition gui_event_type.hpp:48
@ rectangle
The gui_event has rectangle data.
Definition gui_event_variant.hpp:44
@ partial
A widget is partially enabled.
Definition widget_state.hpp:73
@ invisible
The widget is invisible.
Definition widget_state.hpp:41
@ enabled
The widget is fully enabled.
Definition widget_state.hpp:81
@ display
The widget is in display-only mode.
Definition widget_state.hpp:55
scroll_widget< axis::vertical > vertical_scroll_widget
Vertical scroll widget.
Definition scroll_widget.hpp:203
The HikoGUI namespace.
Definition array_generic.hpp:21
The HikoGUI API version 1.
Definition array_generic.hpp:22
@ color
A color value was modified.
Definition style_modify_mask.hpp:27
@ inside
The border is drawn inside the edge of a quad.
Definition draw_context_intf.hpp:35
bool compare_store(T &lhs, U &&rhs) noexcept
Compare then store if there was a change.
Definition misc.hpp:53
@ overlay
The child widget increases the elevation by 20 and resets the layer.
Definition widget_layout.hpp:38
font_glyph_ids find_glyph(font_id font, grapheme grapheme) noexcept
Find a glyph using the given code-point.
Definition font_book.hpp:362
Horizontal/Vertical alignment combination.
Definition alignment.hpp:244
widget_id id
The numeric identifier of a widget.
Definition widget_intf.hpp:31
widget_layout const & layout() const noexcept
Get the current layout for this widget.
Definition widget_intf.hpp:241
callback< void()> subscribe(Func &&func, callback_flags flags=callback_flags::synchronous) noexcept
Subscribe a callback to be called when an action is completed by the widget.
Definition widget_intf.hpp:128
A observer pointing to the whole or part of a observed_base.
Definition observer_intf.hpp:32
A GUI widget that lays out child-widgets in a grid with variable sized cells.
Definition grid_widget.hpp:43
A delegate that controls the state of a selection_widget.
Definition selection_delegate.hpp:33
A graphical control element that allows the user to choose only one of a predefined set of mutually e...
Definition selection_widget.hpp:48
selection_widget(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:161
selection_widget(attributes_type attributes, std::shared_ptr< delegate_type > delegate) noexcept
Construct a selection widget with a delegate.
Definition selection_widget.hpp:109
Definition selection_widget.hpp:53
observer< label > off_label
The label to show when nothing is selected.
Definition selection_widget.hpp:56
void request_redraw() const noexcept override
Request the widget to be redrawn on the next frame.
Definition widget.hpp:136
widget() noexcept
Constructor for creating sub views.
Definition widget.hpp:50
box_constraints update_constraints() noexcept override
Update the constraints of the widget.
Definition widget.hpp:110
bool process_event(gui_event const &event) const noexcept override
Send a event to the window.
Definition widget.hpp:125
bool handle_event(gui_event const &event) noexcept override
Handle command.
Definition widget.hpp:145
Incompatible with another type.
Definition concepts.hpp:57
True if T is a forwarded type of Forward.
Definition concepts.hpp:137
Definition label_widget.hpp:30
Definition selection_widget.hpp:32
T align(T... args)
T forward(T... args)
T max(T... args)
T move(T... args)