HikoGUI
A low latency retained GUI
Loading...
Searching...
No Matches
widget.hpp
1// Copyright Take Vos 2023.
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
5#pragma once
6
7#include "widget_id.hpp"
8#include "widget_mode.hpp"
9#include "widget_state.hpp"
10#include "widget_layout.hpp"
11#include "hitbox.hpp"
12#include "keyboard_focus_group.hpp"
13#include "gui_event.hpp"
14#include "widget_draw_context.hpp"
15#include "../theme/module.hpp"
16#include "../geometry/module.hpp"
17#include "../layout/box_constraints.hpp"
18#include "../observer.hpp"
19#include "../generator.hpp"
20#include "../loop.hpp"
21
22namespace hi { inline namespace v1 {
23class gui_window;
24class gfx_surface;
25
26class widget {
27public:
28 using callback_token = notifier<void()>::callback_token;
29 using awaiter_type = notifier<void()>::awaiter_type;
30
35 widget_id id = {};
36
40 widget *parent = nullptr;
41
44 gui_window *window = nullptr;
45
48 gfx_surface *surface = nullptr;
49
53 observer<widget_mode> mode = widget_mode::enabled;
54
57 observer<bool> hover = false;
58
61 observer<bool> clicked = false;
62
65 observer<bool> focus = false;
66
69 observer<widget_state> state = widget_state::off;
70
85 size_t semantic_layer = 0_uz;
86
89 observer<extent2i> minimum = extent2i{};
90
93 observer<extent2i> maximum = extent2i::large();
94
95 widget_layout layout;
96
97 virtual ~widget() {}
98 widget(widget *parent) noexcept : parent(parent), id(narrow_cast<uint32_t>(++global_counter<"widget::id">))
99 {
100 hi_axiom(loop::main().on_thread());
101
102 if (parent) {
104 }
105
106 _mode_cbt = mode.subscribe([&](auto...) {
107 ++global_counter<"widget:mode:constrain">;
108 process_event({gui_event_type::window_reconstrain});
109 });
110 }
111
112 widget(widget const&) = delete;
113 widget(widget&&) = delete;
114 widget& operator=(widget&&) = delete;
115 widget& operator=(widget const&) = delete;
116
119 [[nodiscard]] virtual generator<widget const&> children(bool include_invisible) const noexcept
120 {
121 co_return;
122 }
123
124 [[nodiscard]] generator<widget&> children(bool include_invisible) noexcept
125 {
126 for (auto& child : const_cast<widget const *>(this)->children(include_invisible)) {
127 co_yield const_cast<widget&>(child);
128 }
129 }
130
138 [[nodiscard]] virtual hitbox hitbox_test(point2i position) const noexcept
139 {
140 return {};
141 }
142
149 [[nodiscard]] virtual hitbox hitbox_test_from_parent(point2i position) const noexcept
150 {
151 return hitbox_test(layout.from_parent * position);
152 }
153
161 [[nodiscard]] virtual hitbox hitbox_test_from_parent(point2i position, hitbox sibling_hitbox) const noexcept
162 {
163 return std::max(sibling_hitbox, hitbox_test(layout.from_parent * position));
164 }
165
169 [[nodiscard]] virtual bool accepts_keyboard_focus(keyboard_focus_group group) const noexcept
170 {
171 return false;
172 }
173
178 void reset_layout(gui_window *new_window, gfx_surface *new_surface, float new_scale) noexcept
179 {
180 layout = {};
181 window = new_window;
182 surface = new_surface;
183 _scale = narrow_cast<int>(std::round(new_scale * -4.0));
184 }
185
197 [[nodiscard]] virtual box_constraints update_constraints() noexcept
198 {
199 return {*minimum, *minimum, *maximum};
200 }
201
213 virtual void set_layout(widget_layout const& context) noexcept
214 {
215 layout = context;
216 }
217
232 virtual void draw(widget_draw_context const& context) noexcept {};
233
234 [[nodiscard]] virtual sub_theme_selector_type sub_theme_selector() const noexcept
235 {
236 auto s = theme_state{};
237 if (*mode == widget_mode::disabled) {
238 s |= theme_state::disabled;
239 } else if (*clicked) {
240 s |= theme_state::active;
241 } else if (*hover) {
242 s |= theme_state::hover;
243 } else {
244 s |= theme_state::enabled;
245 }
246
247 if (*focus) {
248 s |= theme_state::focus;
249 }
250
251 if (*state != widget_state::off) {
252 s |= theme_state::on;
253 }
254
255 s |= static_cast<theme_state>((semantic_layer % 4) * to_underlying(theme_state::layer_1));
256 return sub_theme_selector_type{s, _scale};
257 }
258
259 virtual bool process_event(gui_event const& event) const noexcept
260 {
261 if (parent != nullptr) {
262 return parent->process_event(event);
263 } else {
264 return true;
265 }
266 }
267
270 virtual void request_redraw() const noexcept
271 {
273 }
274
279 virtual bool handle_event(gui_event const& event) noexcept
280 {
281 hi_axiom(loop::main().on_thread());
282
283 switch (event.type()) {
284 using enum hi::gui_event_type;
285 case keyboard_enter:
286 focus = true;
287 scroll_to_show(layout.rectangle());
288 ++global_counter<"widget:keyboard_enter:redraw">;
290 return true;
291
292 case keyboard_exit:
293 focus = false;
294 ++global_counter<"widget:keyboard_exit:redraw">;
296 return true;
297
298 case mouse_enter:
299 hover = true;
300 ++global_counter<"widget:mouse_enter:redraw">;
302 return true;
303
304 case mouse_exit:
305 hover = false;
306 ++global_counter<"widget:mouse_exit:redraw">;
308 return true;
309
310 case gui_widget_next:
311 process_event(
312 gui_event::window_set_keyboard_target(id, keyboard_focus_group::normal, keyboard_focus_direction::forward));
313 return true;
314
315 case gui_widget_prev:
316 process_event(
317 gui_event::window_set_keyboard_target(id, keyboard_focus_group::normal, keyboard_focus_direction::backward));
318 return true;
319
320 case gui_activate_next:
321 process_event(gui_activate);
322 return process_event(gui_widget_next);
323
324 case gui_event_type::gui_toolbar_next:
325 if (*mode >= widget_mode::partial and accepts_keyboard_focus(keyboard_focus_group::toolbar) and
326 not is_last(keyboard_focus_group::toolbar)) {
327 process_event(
328 gui_event::window_set_keyboard_target(id, keyboard_focus_group::toolbar, keyboard_focus_direction::forward));
329 return true;
330 }
331 break;
332
333 case gui_event_type::gui_toolbar_prev:
334 if (*mode >= widget_mode::partial and accepts_keyboard_focus(keyboard_focus_group::toolbar) and
335 not is_first(keyboard_focus_group::toolbar)) {
336 process_event(
337 gui_event::window_set_keyboard_target(id, keyboard_focus_group::toolbar, keyboard_focus_direction::backward));
338 return true;
339 }
340 break;
341
342 default:;
343 }
344
345 return false;
346 }
347
355 virtual bool
357 {
358 hi_axiom(loop::main().on_thread());
359
360 auto handled = false;
361
362 for (auto& child : children(false)) {
363 handled |= child.handle_event_recursive(event, reject_list);
364 }
365
366 if (not std::ranges::any_of(reject_list, [&](hilet& x) {
367 return x == id;
368 })) {
369 handled |= handle_event(event);
370 }
371
372 return handled;
373 }
374
388 [[nodiscard]] virtual widget_id find_next_widget(
389 widget_id current_keyboard_widget,
390 keyboard_focus_group group,
391 keyboard_focus_direction direction) const noexcept
392 {
393 hi_axiom(loop::main().on_thread());
394
395 auto found = false;
396
397 if (not current_keyboard_widget and accepts_keyboard_focus(group)) {
398 // If there was no current_keyboard_widget, then return this if it accepts focus.
399 return id;
400
401 } else if (current_keyboard_widget == id) {
402 found = true;
403 }
404
405 auto children_ = std::vector<widget const *>{};
406 for (auto& child : children(false)) {
407 children_.push_back(std::addressof(child));
408 }
409
410 if (direction == keyboard_focus_direction::backward) {
411 std::reverse(begin(children_), end(children_));
412 }
413
414 for (auto *child : children_) {
415 hi_axiom_not_null(child);
416
417 if (found) {
418 // Find the first focus accepting widget.
419 if (auto tmp = child->find_next_widget({}, group, direction)) {
420 return tmp;
421 }
422
423 } else {
424 auto tmp = child->find_next_widget(current_keyboard_widget, group, direction);
425 if (tmp == current_keyboard_widget) {
426 // The current widget was found, but no next widget available in the child.
427 // Try the first widget that does accept keyboard focus.
428 found = true;
429
430 } else if (tmp != nullptr) {
431 // Return the next widget that was found in the child-widget.
432 return tmp;
433 }
434 }
435 }
436
437 if (found) {
438 // Either:
439 // 1. current_keyboard_widget was nullptr; this widget, nor its child widgets accept focus.
440 // 2. current_keyboard_wigget was this; none of the child widgets accept focus.
441 // 3. current_keyboard_widget is a child; none of the following widgets accept focus.
442 return current_keyboard_widget;
443 }
444
445 return std::nullopt;
446 }
447
448 [[nodiscard]] widget_id find_first_widget(keyboard_focus_group group) const noexcept
449 {
450 hi_axiom(loop::main().on_thread());
451
452 for (auto& child : children(false)) {
453 if (child.accepts_keyboard_focus(group)) {
454 return child.id;
455 }
456 }
457 return std::nullopt;
458 }
459
460 [[nodiscard]] widget_id find_last_widget(keyboard_focus_group group) const noexcept
461 {
462 hi_axiom(loop::main().on_thread());
463
464 auto found = widget_id{};
465 for (auto& child : children(false)) {
466 if (child.accepts_keyboard_focus(group)) {
467 found = child.id;
468 }
469 }
470
471 return found;
472 }
473
474 [[nodiscard]] bool is_first(keyboard_focus_group group) const noexcept
475 {
476 hi_axiom(loop::main().on_thread());
477 return parent->find_first_widget(group) == id;
478 }
479
480 [[nodiscard]] bool is_last(keyboard_focus_group group) const noexcept
481 {
482 hi_axiom(loop::main().on_thread());
483 return parent->find_last_widget(group) == id;
484 }
485
492 virtual void scroll_to_show(hi::aarectanglei rectangle) noexcept
493 {
494 hi_axiom(loop::main().on_thread());
495
496 if (parent) {
498 }
499 }
500
504 [[nodiscard]] std::vector<widget_id> parent_chain() const noexcept
505 {
506 hi_axiom(loop::main().on_thread());
507
509
510 if (auto w = this) {
511 chain.push_back(w->id);
512 while (to_bool(w = w->parent)) {
513 chain.push_back(w->id);
514 }
515 }
516
517 return chain;
518 }
519
525 [[nodiscard]] virtual bool is_tab_button() const noexcept
526 {
527 return false;
528 }
529
530 template<forward_of<void()> Callback>
531 [[nodiscard]] callback_token subscribe(Callback&& callback, callback_flags flags = callback_flags::synchronous) const noexcept
532 {
533 return _state_changed.subscribe(std::forward<Callback>(callback), flags);
534 }
535
536 [[nodiscard]] awaiter_type operator co_await() const noexcept
537 {
538 return _state_changed.operator co_await();
539 }
540
541protected:
549 int _scale = -4;
550
553 mutable notifier<void()> _state_changed;
554
564 [[nodiscard]] aarectanglei make_overlay_rectangle(aarectanglei requested_rectangle) const noexcept
565 {
566 hi_axiom(loop::main().on_thread());
567
568 // Move the request_rectangle to window coordinates.
569 hilet requested_window_rectangle = translate2i{layout.clipping_rectangle_on_window()} * requested_rectangle;
570
571 hilet window_bounds = aarectanglei{layout.window_size} - theme<"window">.margin(this);
572 hilet response_window_rectangle = fit(window_bounds, requested_window_rectangle);
573 return layout.from_window * response_window_rectangle;
574 }
575
576private:
577 decltype(mode)::callback_token _mode_cbt;
578};
579
580inline widget *get_if(widget *start, widget_id id, bool include_invisible) noexcept
581{
582 hi_assert_not_null(start);
583
584 if (start->id == id) {
585 return start;
586 }
587 for (auto& child : start->children(include_invisible)) {
588 if (hilet r = get_if(&child, id, include_invisible); r != nullptr) {
589 return r;
590 }
591 }
592 return nullptr;
593}
594
595inline widget& get(widget& start, widget_id id, bool include_invisible)
596{
597 if (auto r = get_if(std::addressof(start), id, include_invisible); r != nullptr) {
598 return *r;
599 }
600 throw not_found_error("get widget by id");
601}
602
603template<std::invocable<widget&> F>
604inline void apply(widget& start, bool include_invisible, F const& f)
605{
606 for (auto& child : start.children(include_invisible)) {
607 apply(child, include_invisible, f);
608 }
609 f(start);
610}
611
612template<std::invocable<widget&> F>
613inline void apply(widget& start, F const& f)
614{
615 return apply(start, true, f);
616}
617
618}} // namespace hi::v1
Definition of GUI event types.
#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 hi_axiom_not_null(expression,...)
Assert if an expression is not nullptr.
Definition assert.hpp:272
#define hilet
Invariant should be the default for variables.
Definition utility.hpp:23
@ end
Start from the end of the file.
@ begin
Start from the beginning of the file.
gui_event_type
GUI event type.
Definition gui_event_type.hpp:21
@ window_redraw
Request that part of the window gets redrawn on the next frame.
@ window_reconstrain
Request that widget get constraint on the next frame.
@ rectangle
The gui_event has rectangle data.
@ disabled
The widget is disabled.
@ partial
A widget is partially enabled.
@ enabled
The widget is fully enabled.
DOXYGEN BUG.
Definition algorithm.hpp:13
geometry/margins.hpp
Definition cache.hpp:11
aarectangle fit(aarectangle const &bounds, aarectangle const &rectangle) noexcept
Make a rectangle fit inside bounds.
@ off
The widget in the off-state.
auto theme
A tagged global variable to a theme model for a widget's component.
Definition theme_model.hpp:433
A user interface event.
Definition gui_event.hpp:75
Definition widget.hpp:26
virtual void draw(widget_draw_context const &context) noexcept
Draw the widget.
Definition widget.hpp:232
widget_id id
The numeric identifier of a widget.
Definition widget.hpp:35
virtual void set_layout(widget_layout const &context) noexcept
Update the internal layout of the widget.
Definition widget.hpp:213
observer< extent2i > minimum
The minimum size this widget is allowed to be.
Definition widget.hpp:89
observer< bool > hover
Mouse cursor is hovering over the widget.
Definition widget.hpp:57
virtual void scroll_to_show(hi::aarectanglei rectangle) noexcept
Scroll to show the given rectangle on the window.
Definition widget.hpp:492
virtual void request_redraw() const noexcept
Request the widget to be redrawn on the next frame.
Definition widget.hpp:270
void reset_layout(gui_window *new_window, gfx_surface *new_surface, float new_scale) noexcept
Reset the layout.
Definition widget.hpp:178
virtual bool handle_event_recursive(gui_event const &event, std::vector< widget_id > const &reject_list=std::vector< widget_id >{}) noexcept
Handle command recursive.
Definition widget.hpp:356
virtual hitbox hitbox_test_from_parent(point2i position, hitbox sibling_hitbox) const noexcept
Call hitbox_test from a parent widget.
Definition widget.hpp:161
virtual generator< widget const & > children(bool include_invisible) const noexcept
Get a list of child widgets.
Definition widget.hpp:119
virtual bool is_tab_button() const noexcept
Check if this widget is a tab-button.
Definition widget.hpp:525
virtual box_constraints update_constraints() noexcept
Update the constraints of the widget.
Definition widget.hpp:197
observer< bool > clicked
The widget is being clicked by the mouse.
Definition widget.hpp:61
gfx_surface * surface
The surface this widget is drawn on.
Definition widget.hpp:48
virtual bool accepts_keyboard_focus(keyboard_focus_group group) const noexcept
Check if the widget will accept keyboard focus.
Definition widget.hpp:169
observer< widget_state > state
The state of the widget.
Definition widget.hpp:69
virtual bool handle_event(gui_event const &event) noexcept
Handle command.
Definition widget.hpp:279
virtual widget_id find_next_widget(widget_id current_keyboard_widget, keyboard_focus_group group, keyboard_focus_direction direction) const noexcept
Find the next widget that handles keyboard focus.
Definition widget.hpp:388
std::vector< widget_id > parent_chain() const noexcept
Get a list of parents of a given widget.
Definition widget.hpp:504
gui_window * window
The window this widget is on.
Definition widget.hpp:44
observer< extent2i > maximum
The maximum size this widget is allowed to be.
Definition widget.hpp:93
widget * parent
Pointer to the parent widget.
Definition widget.hpp:40
observer< widget_mode > mode
The widget mode.
Definition widget.hpp:53
observer< bool > focus
The widget has keyboard focus.
Definition widget.hpp:65
size_t semantic_layer
The draw layer of the widget.
Definition widget.hpp:85
virtual hitbox hitbox_test(point2i position) const noexcept
Find the widget that is under the mouse cursor.
Definition widget.hpp:138
virtual hitbox hitbox_test_from_parent(point2i position) const noexcept
Call hitbox_test from a parent widget.
Definition widget.hpp:149
Draw context for drawing using the HikoGUI shaders.
Definition widget_draw_context.hpp:204
The layout of a widget.
Definition widget_layout.hpp:37
extent2i window_size
Size of the window.
Definition widget_layout.hpp:68
translate2i to_parent
This matrix transforms local coordinates to the coordinates of the parent widget.
Definition widget_layout.hpp:52
translate2i from_parent
This matrix transforms parent widget's coordinates to local coordinates.
Definition widget_layout.hpp:56
translate2i from_window
This matrix transforms window coordinates to local coordinates.
Definition widget_layout.hpp:64
constexpr aarectanglei clipping_rectangle_on_window() const noexcept
Get the clipping rectangle in window coordinate system.
Definition widget_layout.hpp:145
2D constraints.
Definition box_constraints.hpp:22
Definition theme_model.hpp:148
T addressof(T... args)
T max(T... args)
T push_back(T... args)
T reverse(T... args)
T round(T... args)