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 "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
57
58 observer<alignment> alignment = hi::alignment::middle_left();
59
62 observer<semantic_text_style> text_style = semantic_text_style::label;
63
64 attributes_type(attributes_type const&) noexcept = default;
65 attributes_type(attributes_type&&) noexcept = default;
66 attributes_type& operator=(attributes_type const&) noexcept = default;
67 attributes_type& operator=(attributes_type&&) noexcept = default;
68
69 template<selection_widget_attribute... Attributes>
70 explicit attributes_type(Attributes&&...attributes) noexcept
71 {
72 set_attributes(std::forward<Attributes>(attributes)...);
73 }
74
75 void set_attributes() noexcept {}
76
78 void set_attributes(First&& first, Rest&&...rest) noexcept
79 {
81 off_label = std::forward<First>(first);
82 } else if constexpr (forward_of<First, observer<hi::alignment>>) {
83 alignment = std::forward<First>(first);
84 } else if constexpr (forward_of<First, observer<hi::semantic_text_style>>) {
85 text_style = std::forward<First>(first);
86 } else {
87 hi_static_no_default();
88 }
89
90 set_attributes(std::forward<Rest>(rest)...);
91 }
92 };
93
94 attributes_type attributes;
95
97
98 template<typename... Args>
99 [[nodiscard]] static std::shared_ptr<delegate_type> make_default_delegate(Args &&...args)
100 requires requires { make_shared_ctad<default_selection_delegate>(std::forward<Args>(args)...); }
101 {
102 return make_shared_ctad<default_selection_delegate>(std::forward<Args>(args)...);
103 }
104
105 ~selection_widget()
106 {
107 delegate->deinit(*this);
108 }
109
116 super(parent), attributes(std::move(attributes)), delegate(std::move(delegate))
117 {
118 _current_label_widget = std::make_unique<label_widget>(this, this->attributes.alignment, this->attributes.text_style);
119 _current_label_widget->set_mode(widget_mode::invisible);
120 _off_label_widget = std::make_unique<label_widget>(this, this->attributes.off_label, this->attributes.alignment, semantic_text_style::placeholder);
121
122 _overlay_widget = std::make_unique<overlay_widget>(this);
123 _overlay_widget->set_mode(widget_mode::invisible);
124 _scroll_widget = &_overlay_widget->emplace<vertical_scroll_widget>();
125 _grid_widget = &_scroll_widget->emplace<grid_widget>();
126
127 _off_label_cbt = this->attributes.off_label.subscribe([&](auto...) {
128 ++global_counter<"selection_widget:off_label:constrain">;
130 });
131
132 _delegate_options_cbt = this->delegate->subscribe_on_options([&] {
133 update_options();
134 }, callback_flags::main);
135 _delegate_options_cbt();
136
137 _delegate_value_cbt = this->delegate->subscribe_on_value([&] {
138 update_value();
139 }, callback_flags::main);
140 _delegate_value_cbt();
141
142 hi_axiom_not_null(this->delegate);
143 this->delegate->init(*this);
144 }
145
158 template<
161 selection_widget_attribute... Attributes>
163 widget_intf const* parent,
164 Value&& value,
165 OptionList&& option_list,
166 Attributes&&...attributes) noexcept requires requires
167 {
168 make_default_delegate(std::forward<Value>(value), std::forward<OptionList>(option_list));
169 attributes_type{std::forward<Attributes>(attributes)...};
170 } :
172 parent,
173 attributes_type{std::forward<Attributes>(attributes)...},
174 make_default_delegate(std::forward<Value>(value), std::forward<OptionList>(option_list)))
175 {
176 }
177
179 [[nodiscard]] generator<widget_intf&> children(bool include_invisible) noexcept override
180 {
181 co_yield *_overlay_widget;
182 co_yield *_current_label_widget;
183 co_yield *_off_label_widget;
184 }
185
186 [[nodiscard]] box_constraints update_constraints() noexcept override
187 {
188 hi_assert_not_null(_off_label_widget);
189 hi_assert_not_null(_current_label_widget);
190 hi_assert_not_null(_overlay_widget);
191
192 _layout = {};
193 _off_label_constraints = _off_label_widget->update_constraints();
194 _current_label_constraints = _current_label_widget->update_constraints();
195 _overlay_constraints = _overlay_widget->update_constraints();
196
197 auto const extra_size = extent2{theme().size() + theme().margin<float>() * 2.0f, theme().margin<float>() * 2.0f};
198
199 auto r = max(_off_label_constraints + extra_size, _current_label_constraints + extra_size);
200
201 // Make it so that the scroll widget can scroll vertically.
202 _scroll_widget->minimum->height() = theme().size();
203
204 r.minimum.width() = std::max(r.minimum.width(), _overlay_constraints.minimum.width() + extra_size.width());
205 r.preferred.width() = std::max(r.preferred.width(), _overlay_constraints.preferred.width() + extra_size.width());
206 r.maximum.width() = std::max(r.maximum.width(), _overlay_constraints.maximum.width() + extra_size.width());
207 r.margins = theme().margin();
208 r.alignment = resolve(*attributes.alignment, os_settings::left_to_right());
209 hi_axiom(r.holds_invariant());
210 return r;
211 }
212
213 void set_layout(widget_layout const& context) noexcept override
214 {
215 if (compare_store(_layout, context)) {
216 if (os_settings::left_to_right()) {
217 _left_box_rectangle = aarectangle{0.0f, 0.0f, theme().size(), context.height()};
218
219 // The unknown_label is located to the right of the selection box icon.
220 auto const option_rectangle = aarectangle{
221 _left_box_rectangle.right() + theme().margin<float>(),
222 0.0f,
223 context.width() - _left_box_rectangle.width() - theme().margin<float>() * 2.0f,
224 context.height()};
225 _off_label_shape = box_shape{_off_label_constraints, option_rectangle, theme().baseline_adjustment()};
226 _current_label_shape = box_shape{_off_label_constraints, option_rectangle, theme().baseline_adjustment()};
227
228 } else {
229 _left_box_rectangle = aarectangle{context.width() - theme().size(), 0.0f, theme().size(), context.height()};
230
231 // The unknown_label is located to the left of the selection box icon.
232 auto const option_rectangle = aarectangle{
233 theme().margin<float>(),
234 0.0f,
235 context.width() - _left_box_rectangle.width() - theme().margin<float>() * 2.0f,
236 context.height()};
237 _off_label_shape = box_shape{_off_label_constraints, option_rectangle, theme().baseline_adjustment()};
238 _current_label_shape = box_shape{_off_label_constraints, option_rectangle, theme().baseline_adjustment()};
239 }
240
241 _chevrons_glyph = find_glyph(elusive_icon::ChevronUp);
242 auto const chevrons_glyph_bbox = _chevrons_glyph.get_metrics().bounding_rectangle * theme().icon_size();
243 _chevrons_rectangle = align(_left_box_rectangle, chevrons_glyph_bbox, alignment::middle_center());
244 }
245
246 // The overlay itself will make sure the overlay fits the window, so we give the preferred size and position
247 // from the point of view of the selection widget.
248 // The overlay should start on the same left edge as the selection box and the same width.
249 // The height of the overlay should be the maximum height, which will show all the options.
250 auto const overlay_width = std::clamp(
251 context.width() - theme().size(), _overlay_constraints.minimum.width(), _overlay_constraints.maximum.width());
252 auto const overlay_height = _overlay_constraints.preferred.height();
253 auto const overlay_x = os_settings::left_to_right() ? theme().size() : context.width() - theme().size() - overlay_width;
254 auto const overlay_y = (context.height() - overlay_height) / 2;
255 auto const overlay_rectangle_request = aarectangle{overlay_x, overlay_y, overlay_width, overlay_height};
256 auto const overlay_rectangle = make_overlay_rectangle(overlay_rectangle_request);
257 _overlay_shape = box_shape{_overlay_constraints, overlay_rectangle, theme().baseline_adjustment()};
258 _overlay_widget->set_layout(context.transform(_overlay_shape, transform_command::overlay));
259
260 _off_label_widget->set_layout(context.transform(_off_label_shape));
261 _current_label_widget->set_layout(context.transform(_current_label_shape));
262 }
263
264 void draw(draw_context const& context) noexcept override
265 {
266 animate_overlay(context.display_time_point);
267
268 if (mode() > widget_mode::invisible) {
269 if (overlaps(context, layout())) {
270 draw_outline(context);
271 draw_left_box(context);
272 draw_chevrons(context);
273
274 _off_label_widget->draw(context);
275 _current_label_widget->draw(context);
276 }
277
278 // Overlay is outside of the overlap of the selection widget.
279 _overlay_widget->draw(context);
280 }
281 }
282
283 bool handle_event(gui_event const& event) noexcept override
284 {
285 switch (event.type()) {
286 case gui_event_type::mouse_up:
287 if (mode() >= widget_mode::partial and not delegate->empty(*this) and layout().rectangle().contains(event.mouse().position)) {
288 return handle_event(gui_event_type::gui_activate);
289 }
290 return true;
291
292 case gui_event_type::gui_activate_next:
293 // Handle gui_active_next so that the next widget will NOT get keyboard focus.
294 // The previously selected item needs the get keyboard focus instead.
295 case gui_event_type::gui_activate:
296 if (mode() >= widget_mode::partial and not delegate->empty(*this) and overlay_closed()) {
297 open_overlay();
298 } else {
299 close_overlay();
300 }
301 ++global_counter<"selection_widget:gui_activate:relayout">;
303 return true;
304
305 case gui_event_type::gui_cancel:
306 close_overlay();
307 return true;
308
309 default:;
310 }
311
312 return super::handle_event(event);
313 }
314
315 [[nodiscard]] hitbox hitbox_test(point2 position) const noexcept override
316 {
317 hi_axiom(loop::main().on_thread());
318
319 if (mode() >= widget_mode::partial) {
320 auto r = _overlay_widget->hitbox_test_from_parent(position);
321
322 if (layout().contains(position)) {
323 r = std::max(r, hitbox{id, _layout.elevation, not delegate->empty(*this) ? hitbox_type::button : hitbox_type::_default});
324 }
325
326 return r;
327 } else {
328 return {};
329 }
330 }
331
332 [[nodiscard]] bool accepts_keyboard_focus(keyboard_focus_group group) const noexcept override
333 {
334 hi_axiom(loop::main().on_thread());
335 return mode() >= widget_mode::partial and to_bool(group & hi::keyboard_focus_group::normal) and not delegate->empty(*this);
336 }
337
338 [[nodiscard]] color focus_color() const noexcept override
339 {
340 hi_axiom(loop::main().on_thread());
341
342 if (mode() >= widget_mode::partial and not overlay_closed()) {
343 return theme().color(semantic_color::accent);
344 } else {
345 return super::focus_color();
346 }
347 }
348
350private:
351 enum class overlay_state_type {
352 open,
353 closing,
354 closed
355 };
356
357 constexpr static std::chrono::nanoseconds _overlay_close_delay = std::chrono::milliseconds(200);
358
359 overlay_state_type _overlay_state = overlay_state_type::closed;
360 utc_nanoseconds _overlay_close_start = {};
361
362 bool _notification_from_delegate = true;
363
364 std::unique_ptr<label_widget> _current_label_widget;
365 box_constraints _current_label_constraints;
366 box_shape _current_label_shape;
367
368 std::unique_ptr<label_widget> _off_label_widget;
369 box_constraints _off_label_constraints;
370 box_shape _off_label_shape;
371
372 aarectangle _left_box_rectangle;
373
374 font_book::font_glyph_type _chevrons_glyph;
375 aarectangle _chevrons_rectangle;
376
377 std::unique_ptr<overlay_widget> _overlay_widget;
378 box_constraints _overlay_constraints;
379 box_shape _overlay_shape;
380
381 vertical_scroll_widget *_scroll_widget = nullptr;
382 grid_widget *_grid_widget = nullptr;
383
384 callback<void()> _delegate_options_cbt;
385 callback<void()> _delegate_value_cbt;
386 callback<void(label)> _off_label_cbt;
387
388 [[nodiscard]] bool overlay_closed() const noexcept
389 {
390 return _overlay_state == overlay_state_type::closed;
391 }
392
393 void open_overlay() noexcept
394 {
395 hi_axiom(loop::main().on_thread());
396
397 if (auto focus_id = delegate->keyboard_focus_id(*this)) {
398 _overlay_state = overlay_state_type::open;
399 _overlay_widget->set_mode(widget_mode::enabled);
400 process_event(gui_event::window_set_keyboard_target(*focus_id, keyboard_focus_group::menu));
402 }
403 }
404
405 void close_overlay() noexcept
406 {
407 hi_axiom(loop::main().on_thread());
408
409 if (_overlay_state == overlay_state_type::open) {
410 _overlay_state = overlay_state_type::closing;
411 _overlay_close_start = std::chrono::utc_clock::now();
413 }
414 }
415
416 void force_close_overlay() noexcept
417 {
418 if (_overlay_state != overlay_state_type::closed) {
419 _overlay_state = overlay_state_type::closed;
420 _overlay_widget->set_mode(widget_mode::invisible);
422 }
423 }
424
425 void animate_overlay(utc_nanoseconds display_time_point) noexcept
426 {
427 hi_axiom(loop::main().on_thread());
428
429 switch (_overlay_state) {
430 case overlay_state_type::open:
431 break;
432 case overlay_state_type::closing:
433 if (display_time_point >= _overlay_close_start + _overlay_close_delay) {
434 force_close_overlay();
435 } else {
437 }
438 break;
439 case overlay_state_type::closed:
440 break;
441 default:
442 hi_no_default();
443 }
444 }
445
446 void update_options() noexcept
447 {
448 _grid_widget->clear();
449 for (auto i = 0_uz; i != delegate->size(*this); ++i) {
450 _grid_widget->push_bottom(delegate->make_option_widget(*this, *_grid_widget, i));
451 }
452
453 ++global_counter<"selection_widget:update_options:constrain">;
455 }
456
457 void update_value() noexcept
458 {
459 if (auto selected_label = delegate->selected_label(*this)) {
460 _off_label_widget->set_mode(widget_mode::invisible);
461 _current_label_widget->label = *selected_label;
462 _current_label_widget->set_mode(widget_mode::display);
463
464 } else {
465 _off_label_widget->set_mode(widget_mode::display);
466 _current_label_widget->set_mode(widget_mode::invisible);
467 }
468
469 close_overlay();
470 }
471
472 void draw_outline(draw_context const& context) noexcept
473 {
474 context.draw_box(
475 layout(),
476 layout().rectangle(),
477 background_color(),
478 focus_color(),
479 theme().border_width(),
481 theme().rounding_radius());
482 }
483
484 void draw_left_box(draw_context const& context) noexcept
485 {
486 auto const corner_radii = os_settings::left_to_right() ?
487 hi::corner_radii(theme().rounding_radius<float>(), 0.0f, theme().rounding_radius<float>(), 0.0f) :
488 hi::corner_radii(0.0f, theme().rounding_radius<float>(), 0.0f, theme().rounding_radius<float>());
489 context.draw_box(layout(), translate_z(0.1f) * _left_box_rectangle, focus_color(), corner_radii);
490 }
491
492 void draw_chevrons(draw_context const& context) noexcept
493 {
494 context.draw_glyph(layout(), translate_z(0.2f) * _chevrons_rectangle, _chevrons_glyph, label_color());
495 }
496};
497
498}} // namespace hi::v1
Defines delegate_delegate and some default selection delegates.
Defines scroll_widget.
Defines widget.
Defines label_widget.
Defines radio_widget.
Defines overlay_widget.
Defines grid_widget.
@ open
Open file if it exist, or fail.
@ 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.
@ 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.
scroll_widget< axis::vertical > vertical_scroll_widget
Vertical scroll widget.
Definition scroll_widget.hpp:197
The HikoGUI namespace.
Definition array_generic.hpp:20
@ 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:53
@ overlay
The child widget increases the elevation by 20 and resets the layer.
DOXYGEN BUG.
Definition algorithm_misc.hpp:20
hi_export auto find_glyph(font const &font, grapheme grapheme) noexcept
Find a glyph using the given code-point.
Definition font_book.hpp:440
Horizontal/Vertical alignment combination.
Definition alignment.hpp:244
The 4 radii of the corners of a quad or rectangle.
Definition corner_radii.hpp:26
constexpr float & width() noexcept
Access the x-as-width element from the extent.
Definition extent2.hpp:107
constexpr float & height() noexcept
Access the y-as-height element from the extent.
Definition extent2.hpp:118
Definition widget_intf.hpp:24
widget_id id
The numeric identifier of a widget.
Definition widget_intf.hpp:30
widget_layout const & layout() const noexcept
Get the current layout for this widget.
Definition widget_intf.hpp:206
widget_intf * parent
Pointer to the parent widget.
Definition widget_intf.hpp:35
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
The scroll widget allows a content widget to be shown in less space than is required.
Definition scroll_widget.hpp:50
Widget & emplace(Args &&...args) noexcept
Add a content widget directly to this scroll widget.
Definition scroll_widget.hpp:105
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(widget_intf const *parent, attributes_type attributes, std::shared_ptr< delegate_type > delegate) noexcept
Construct a selection widget with a delegate.
Definition selection_widget.hpp:115
selection_widget(widget_intf const *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:162
Definition selection_widget.hpp:53
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:62
observer< label > off_label
The label to show when nothing is selected.
Definition selection_widget.hpp:56
An interactive graphical object as part of the user-interface.
Definition widget.hpp:37
observer< extent2 > minimum
The minimum size this widget is allowed to be.
Definition widget.hpp:41
void request_redraw() const noexcept override
Request the widget to be redrawn on the next frame.
Definition widget.hpp:141
widget() noexcept
Constructor for creating sub views.
Definition widget.hpp:55
bool process_event(gui_event const &event) const noexcept override
Send a event to the window.
Definition widget.hpp:130
bool handle_event(gui_event const &event) noexcept override
Handle command.
Definition widget.hpp:150
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 max(T... args)
T move(T... args)