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;
68
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);
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 not_null<std::shared_ptr<delegate_type>> make_default_delegate(Args &&...args)
100 requires requires { make_shared_ctad_not_null<default_selection_delegate>(std::forward<Args>(args)...); }
101 {
102 return make_shared_ctad_not_null<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 this->delegate->init(*this);
143 }
144
157 template<
163 Value&& value,
165 Attributes&&...attributes) noexcept requires requires
166 {
167 make_default_delegate(std::forward<Value>(value), std::forward<OptionList>(option_list));
168 attributes_type{std::forward<Attributes>(attributes)...};
169 } :
171 parent,
172 attributes_type{std::forward<Attributes>(attributes)...},
173 make_default_delegate(std::forward<Value>(value), std::forward<OptionList>(option_list)))
174 {
175 }
176
178 [[nodiscard]] generator<widget_intf&> children(bool include_invisible) noexcept override
179 {
180 co_yield *_overlay_widget;
181 co_yield *_current_label_widget;
182 co_yield *_off_label_widget;
183 }
184
185 [[nodiscard]] box_constraints update_constraints() noexcept override
186 {
187 hi_assert_not_null(_off_label_widget);
188 hi_assert_not_null(_current_label_widget);
189 hi_assert_not_null(_overlay_widget);
190
191 _layout = {};
192 _off_label_constraints = _off_label_widget->update_constraints();
193 _current_label_constraints = _current_label_widget->update_constraints();
194 _overlay_constraints = _overlay_widget->update_constraints();
195
196 auto const extra_size = extent2{theme().size() + theme().margin<float>() * 2.0f, theme().margin<float>() * 2.0f};
197
198 auto r = max(_off_label_constraints + extra_size, _current_label_constraints + extra_size);
199
200 // Make it so that the scroll widget can scroll vertically.
201 _scroll_widget->minimum->height() = theme().size();
202
203 r.minimum.width() = std::max(r.minimum.width(), _overlay_constraints.minimum.width() + extra_size.width());
204 r.preferred.width() = std::max(r.preferred.width(), _overlay_constraints.preferred.width() + extra_size.width());
205 r.maximum.width() = std::max(r.maximum.width(), _overlay_constraints.maximum.width() + extra_size.width());
206 r.margins = theme().margin();
207 r.alignment = resolve(*attributes.alignment, os_settings::left_to_right());
208 hi_axiom(r.holds_invariant());
209 return r;
210 }
211
212 void set_layout(widget_layout const& context) noexcept override
213 {
214 if (compare_store(_layout, context)) {
215 if (os_settings::left_to_right()) {
216 _left_box_rectangle = aarectangle{0.0f, 0.0f, theme().size(), context.height()};
217
218 // The unknown_label is located to the right of the selection box icon.
219 auto const option_rectangle = aarectangle{
220 _left_box_rectangle.right() + theme().margin<float>(),
221 0.0f,
222 context.width() - _left_box_rectangle.width() - theme().margin<float>() * 2.0f,
223 context.height()};
224 _off_label_shape = box_shape{_off_label_constraints, option_rectangle, theme().baseline_adjustment()};
225 _current_label_shape = box_shape{_off_label_constraints, option_rectangle, theme().baseline_adjustment()};
226
227 } else {
228 _left_box_rectangle = aarectangle{context.width() - theme().size(), 0.0f, theme().size(), context.height()};
229
230 // The unknown_label is located to the left of the selection box icon.
231 auto const option_rectangle = aarectangle{
232 theme().margin<float>(),
233 0.0f,
234 context.width() - _left_box_rectangle.width() - theme().margin<float>() * 2.0f,
235 context.height()};
236 _off_label_shape = box_shape{_off_label_constraints, option_rectangle, theme().baseline_adjustment()};
237 _current_label_shape = box_shape{_off_label_constraints, option_rectangle, theme().baseline_adjustment()};
238 }
239
240 _chevrons_glyph = find_glyph(elusive_icon::ChevronUp);
241 auto const chevrons_glyph_bbox = _chevrons_glyph.get_metrics().bounding_rectangle * theme().icon_size();
242 _chevrons_rectangle = align(_left_box_rectangle, chevrons_glyph_bbox, alignment::middle_center());
243 }
244
245 // The overlay itself will make sure the overlay fits the window, so we give the preferred size and position
246 // from the point of view of the selection widget.
247 // The overlay should start on the same left edge as the selection box and the same width.
248 // The height of the overlay should be the maximum height, which will show all the options.
249 auto const overlay_width = std::clamp(
250 context.width() - theme().size(), _overlay_constraints.minimum.width(), _overlay_constraints.maximum.width());
251 auto const overlay_height = _overlay_constraints.preferred.height();
252 auto const overlay_x = os_settings::left_to_right() ? theme().size() : context.width() - theme().size() - overlay_width;
253 auto const overlay_y = (context.height() - overlay_height) / 2;
255 auto const overlay_rectangle = make_overlay_rectangle(overlay_rectangle_request);
256 _overlay_shape = box_shape{_overlay_constraints, overlay_rectangle, theme().baseline_adjustment()};
257 _overlay_widget->set_layout(context.transform(_overlay_shape, transform_command::overlay));
258
259 _off_label_widget->set_layout(context.transform(_off_label_shape));
260 _current_label_widget->set_layout(context.transform(_current_label_shape));
261 }
262
263 void draw(draw_context const& context) noexcept override
264 {
265 animate_overlay(context.display_time_point);
266
267 if (mode() > widget_mode::invisible) {
268 if (overlaps(context, layout())) {
269 draw_outline(context);
270 draw_left_box(context);
271 draw_chevrons(context);
272
273 _off_label_widget->draw(context);
274 _current_label_widget->draw(context);
275 }
276
277 // Overlay is outside of the overlap of the selection widget.
278 _overlay_widget->draw(context);
279 }
280 }
281
282 bool handle_event(gui_event const& event) noexcept override
283 {
284 switch (event.type()) {
285 case gui_event_type::mouse_up:
286 if (mode() >= widget_mode::partial and not delegate->empty(*this) and layout().rectangle().contains(event.mouse().position)) {
287 return handle_event(gui_event_type::gui_activate);
288 }
289 return true;
290
291 case gui_event_type::gui_activate_next:
292 // Handle gui_active_next so that the next widget will NOT get keyboard focus.
293 // The previously selected item needs the get keyboard focus instead.
294 case gui_event_type::gui_activate:
295 if (mode() >= widget_mode::partial and not delegate->empty(*this) and overlay_closed()) {
296 open_overlay();
297 } else {
298 close_overlay();
299 }
300 ++global_counter<"selection_widget:gui_activate:relayout">;
302 return true;
303
304 case gui_event_type::gui_cancel:
305 close_overlay();
306 return true;
307
308 default:;
309 }
310
312 }
313
314 [[nodiscard]] hitbox hitbox_test(point2 position) const noexcept override
315 {
316 hi_axiom(loop::main().on_thread());
317
318 if (mode() >= widget_mode::partial) {
319 auto r = _overlay_widget->hitbox_test_from_parent(position);
320
321 if (layout().contains(position)) {
322 r = std::max(r, hitbox{id, _layout.elevation, not delegate->empty(*this) ? hitbox_type::button : hitbox_type::_default});
323 }
324
325 return r;
326 } else {
327 return {};
328 }
329 }
330
331 [[nodiscard]] bool accepts_keyboard_focus(keyboard_focus_group group) const noexcept override
332 {
333 hi_axiom(loop::main().on_thread());
334 return mode() >= widget_mode::partial and to_bool(group & hi::keyboard_focus_group::normal) and not delegate->empty(*this);
335 }
336
337 [[nodiscard]] color focus_color() const noexcept override
338 {
339 hi_axiom(loop::main().on_thread());
340
341 if (mode() >= widget_mode::partial and not overlay_closed()) {
342 return theme().color(semantic_color::accent);
343 } else {
344 return super::focus_color();
345 }
346 }
347
349private:
350 enum class overlay_state_type {
351 open,
352 closing,
353 closed
354 };
355
356 constexpr static std::chrono::nanoseconds _overlay_close_delay = std::chrono::milliseconds(200);
357
358 overlay_state_type _overlay_state = overlay_state_type::closed;
359 utc_nanoseconds _overlay_close_start = {};
360
361 bool _notification_from_delegate = true;
362
363 std::unique_ptr<label_widget> _current_label_widget;
364 box_constraints _current_label_constraints;
365 box_shape _current_label_shape;
366
367 std::unique_ptr<label_widget> _off_label_widget;
368 box_constraints _off_label_constraints;
369 box_shape _off_label_shape;
370
371 aarectangle _left_box_rectangle;
372
373 font_book::font_glyph_type _chevrons_glyph;
374 aarectangle _chevrons_rectangle;
375
376 std::unique_ptr<overlay_widget> _overlay_widget;
377 box_constraints _overlay_constraints;
378 box_shape _overlay_shape;
379
380 vertical_scroll_widget *_scroll_widget = nullptr;
381 grid_widget *_grid_widget = nullptr;
382
383 callback<void()> _delegate_options_cbt;
384 callback<void()> _delegate_value_cbt;
385 callback<void(label)> _off_label_cbt;
386
387 [[nodiscard]] bool overlay_closed() const noexcept
388 {
389 return _overlay_state == overlay_state_type::closed;
390 }
391
392 void open_overlay() noexcept
393 {
394 hi_axiom(loop::main().on_thread());
395
396 if (auto focus_id = delegate->keyboard_focus_id(*this)) {
397 _overlay_state = overlay_state_type::open;
398 _overlay_widget->set_mode(widget_mode::enabled);
399 process_event(gui_event::window_set_keyboard_target(*focus_id, keyboard_focus_group::menu));
401 }
402 }
403
404 void close_overlay() noexcept
405 {
406 hi_axiom(loop::main().on_thread());
407
408 if (_overlay_state == overlay_state_type::open) {
409 _overlay_state = overlay_state_type::closing;
410 _overlay_close_start = std::chrono::utc_clock::now();
412 }
413 }
414
415 void force_close_overlay() noexcept
416 {
417 if (_overlay_state != overlay_state_type::closed) {
418 _overlay_state = overlay_state_type::closed;
419 _overlay_widget->set_mode(widget_mode::invisible);
421 }
422 }
423
424 void animate_overlay(utc_nanoseconds display_time_point) noexcept
425 {
426 hi_axiom(loop::main().on_thread());
427
428 switch (_overlay_state) {
429 case overlay_state_type::open:
430 break;
431 case overlay_state_type::closing:
432 if (display_time_point >= _overlay_close_start + _overlay_close_delay) {
433 force_close_overlay();
434 } else {
436 }
437 break;
438 case overlay_state_type::closed:
439 break;
440 default:
441 hi_no_default();
442 }
443 }
444
445 void update_options() noexcept
446 {
447 _grid_widget->clear();
448 for (auto i = 0_uz; i != delegate->size(*this); ++i) {
449 _grid_widget->push_bottom(delegate->make_option_widget(*this, *_grid_widget, i));
450 }
451
452 ++global_counter<"selection_widget:update_options:constrain">;
454 }
455
456 void update_value() noexcept
457 {
458 if (auto selected_label = delegate->selected_label(*this)) {
459 _off_label_widget->set_mode(widget_mode::invisible);
460 _current_label_widget->label = *selected_label;
461 _current_label_widget->set_mode(widget_mode::display);
462
463 } else {
464 _off_label_widget->set_mode(widget_mode::display);
465 _current_label_widget->set_mode(widget_mode::invisible);
466 }
467
468 close_overlay();
469 }
470
471 void draw_outline(draw_context const& context) noexcept
472 {
473 context.draw_box(
474 layout(),
475 layout().rectangle(),
476 background_color(),
477 focus_color(),
478 theme().border_width(),
480 theme().rounding_radius());
481 }
482
483 void draw_left_box(draw_context const& context) noexcept
484 {
485 auto const corner_radii = os_settings::left_to_right() ?
486 hi::corner_radii(theme().rounding_radius<float>(), 0.0f, theme().rounding_radius<float>(), 0.0f) :
487 hi::corner_radii(0.0f, theme().rounding_radius<float>(), 0.0f, theme().rounding_radius<float>());
488 context.draw_box(layout(), translate_z(0.1f) * _left_box_rectangle, focus_color(), corner_radii);
489 }
490
491 void draw_chevrons(draw_context const& context) noexcept
492 {
493 context.draw_glyph(layout(), translate_z(0.2f) * _chevrons_rectangle, _chevrons_glyph, label_color());
494 }
495};
496
497}} // 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
DOXYGEN BUG.
Definition algorithm_misc.hpp:20
hi_export hi_inline auto find_glyph(font const &font, grapheme grapheme) noexcept
Find a glyph using the given code-point.
Definition font_book.hpp:440
The HikoGUI namespace.
Definition recursive_iterator.hpp:15
@ 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.
constexpr Out narrow_cast(In const &rhs) noexcept
Cast numeric values without loss of precision.
Definition cast.hpp:378
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
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
Definition not_null.hpp:22
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(not_null< 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:161
selection_widget(not_null< widget_intf const * > parent, attributes_type attributes, not_null< std::shared_ptr< delegate_type > > delegate) noexcept
Construct a selection widget with a delegate.
Definition selection_widget.hpp:115
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:135
widget(widget_intf const *parent) noexcept
Definition widget.hpp:49
bool process_event(gui_event const &event) const noexcept override
Send a event to the window.
Definition widget.hpp:124
bool handle_event(gui_event const &event) noexcept override
Handle command.
Definition widget.hpp:144
Incompatible with another type.
Definition concepts.hpp:56
True if T is a forwarded type of Forward.
Definition concepts.hpp:136
Definition label_widget.hpp:30
Definition selection_widget.hpp:32
T align(T... args)
T max(T... args)
T move(T... args)