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 gfx_surface *surface = nullptr;
45
49 observer<widget_mode> mode = widget_mode::enabled;
50
53 observer<bool> hover = false;
54
57 observer<bool> clicked = false;
58
61 observer<bool> focus = false;
62
65 observer<widget_state> state = widget_state::off;
66
81 size_t semantic_layer = 0_uz;
82
85 observer<extent2i> minimum = extent2i{};
86
89 observer<extent2i> maximum = extent2i::large();
90
91 widget_layout layout;
92
93 virtual ~widget() {}
94 widget(widget *parent) noexcept : parent(parent), id(narrow_cast<uint32_t>(++global_counter<"widget::id">))
95 {
96 hi_axiom(loop::main().on_thread());
97
98 if (parent) {
100 }
101
102 _mode_cbt = mode.subscribe([&](auto...) {
103 ++global_counter<"widget:mode:constrain">;
104 process_event({gui_event_type::window_reconstrain});
105 });
106 }
107
108 widget(widget const&) = delete;
109 widget(widget&&) = delete;
110 widget& operator=(widget&&) = delete;
111 widget& operator=(widget const&) = delete;
112
115 [[nodiscard]] virtual generator<widget const&> children(bool include_invisible) const noexcept
116 {
117 co_return;
118 }
119
120 [[nodiscard]] generator<widget&> children(bool include_invisible) noexcept
121 {
122 for (auto& child : const_cast<widget const *>(this)->children(include_invisible)) {
123 co_yield const_cast<widget&>(child);
124 }
125 }
126
134 [[nodiscard]] virtual hitbox hitbox_test(point2i position) const noexcept
135 {
136 return {};
137 }
138
145 [[nodiscard]] virtual hitbox hitbox_test_from_parent(point2i position) const noexcept
146 {
147 return hitbox_test(layout.from_parent * position);
148 }
149
157 [[nodiscard]] virtual hitbox hitbox_test_from_parent(point2i position, hitbox sibling_hitbox) const noexcept
158 {
159 return std::max(sibling_hitbox, hitbox_test(layout.from_parent * position));
160 }
161
165 [[nodiscard]] virtual bool accepts_keyboard_focus(keyboard_focus_group group) const noexcept
166 {
167 return false;
168 }
169
174 void reset_layout(gfx_surface *new_surface, float new_scale) noexcept
175 {
176 layout = {};
177 surface = new_surface;
178 _scale = narrow_cast<int>(std::round(new_scale * -4.0));
179 }
180
192 [[nodiscard]] virtual box_constraints update_constraints() noexcept
193 {
194 return {*minimum, *minimum, *maximum};
195 }
196
208 virtual void set_layout(widget_layout const& context) noexcept
209 {
210 layout = context;
211 }
212
227 virtual void draw(widget_draw_context& context) noexcept {};
228
229 [[nodiscard]] virtual sub_theme_selector_type sub_theme_selector() const noexcept
230 {
231 auto s = theme_state{};
232 if (*mode == widget_mode::disabled) {
233 s |= theme_state::disabled;
234 } else if (*clicked) {
235 s |= theme_state::active;
236 } else if (*hover) {
237 s |= theme_state::hover;
238 } else {
239 s |= theme_state::enabled;
240 }
241
242 if (*focus) {
243 s |= theme_state::focus;
244 }
245
246 if (*state != widget_state::off) {
247 s |= theme_state::on;
248 }
249
250 s |= static_cast<theme_state>((semantic_layer % 4) * to_underlying(theme_state::layer_1));
251 return sub_theme_selector_type{s, _scale};
252 }
253
254 virtual bool process_event(gui_event const& event) const noexcept
255 {
256 if (parent != nullptr) {
257 return parent->process_event(event);
258 } else {
259 return true;
260 }
261 }
262
265 virtual void request_redraw() const noexcept
266 {
268 }
269
274 virtual bool handle_event(gui_event const& event) noexcept
275 {
276 hi_axiom(loop::main().on_thread());
277
278 switch (event.type()) {
279 using enum hi::gui_event_type;
280 case keyboard_enter:
281 focus = true;
282 scroll_to_show(layout.rectangle());
283 ++global_counter<"widget:keyboard_enter:redraw">;
285 return true;
286
287 case keyboard_exit:
288 focus = false;
289 ++global_counter<"widget:keyboard_exit:redraw">;
291 return true;
292
293 case mouse_enter:
294 hover = true;
295 ++global_counter<"widget:mouse_enter:redraw">;
297 return true;
298
299 case mouse_exit:
300 hover = false;
301 ++global_counter<"widget:mouse_exit:redraw">;
303 return true;
304
305 case gui_widget_next:
306 process_event(
307 gui_event::window_set_keyboard_target(id, keyboard_focus_group::normal, keyboard_focus_direction::forward));
308 return true;
309
310 case gui_widget_prev:
311 process_event(
312 gui_event::window_set_keyboard_target(id, keyboard_focus_group::normal, keyboard_focus_direction::backward));
313 return true;
314
315 case gui_activate_next:
316 process_event(gui_activate);
317 return process_event(gui_widget_next);
318
319 case gui_event_type::gui_toolbar_next:
320 if (*mode >= widget_mode::partial and accepts_keyboard_focus(keyboard_focus_group::toolbar) and
321 not is_last(keyboard_focus_group::toolbar)) {
322 process_event(
323 gui_event::window_set_keyboard_target(id, keyboard_focus_group::toolbar, keyboard_focus_direction::forward));
324 return true;
325 }
326 break;
327
328 case gui_event_type::gui_toolbar_prev:
329 if (*mode >= widget_mode::partial and accepts_keyboard_focus(keyboard_focus_group::toolbar) and
330 not is_first(keyboard_focus_group::toolbar)) {
331 process_event(
332 gui_event::window_set_keyboard_target(id, keyboard_focus_group::toolbar, keyboard_focus_direction::backward));
333 return true;
334 }
335 break;
336
337 default:;
338 }
339
340 return false;
341 }
342
350 virtual bool
352 {
353 hi_axiom(loop::main().on_thread());
354
355 auto handled = false;
356
357 for (auto& child : children(false)) {
358 handled |= child.handle_event_recursive(event, reject_list);
359 }
360
361 if (not std::ranges::any_of(reject_list, [&](hilet& x) {
362 return x == id;
363 })) {
364 handled |= handle_event(event);
365 }
366
367 return handled;
368 }
369
383 [[nodiscard]] virtual widget_id find_next_widget(
384 widget_id current_keyboard_widget,
385 keyboard_focus_group group,
386 keyboard_focus_direction direction) const noexcept
387 {
388 hi_axiom(loop::main().on_thread());
389
390 auto found = false;
391
392 if (not current_keyboard_widget and accepts_keyboard_focus(group)) {
393 // If there was no current_keyboard_widget, then return this if it accepts focus.
394 return id;
395
396 } else if (current_keyboard_widget == id) {
397 found = true;
398 }
399
400 auto children_ = std::vector<widget const *>{};
401 for (auto& child : children(false)) {
402 children_.push_back(std::addressof(child));
403 }
404
405 if (direction == keyboard_focus_direction::backward) {
406 std::reverse(begin(children_), end(children_));
407 }
408
409 for (auto *child : children_) {
410 hi_axiom_not_null(child);
411
412 if (found) {
413 // Find the first focus accepting widget.
414 if (auto tmp = child->find_next_widget({}, group, direction)) {
415 return tmp;
416 }
417
418 } else {
419 auto tmp = child->find_next_widget(current_keyboard_widget, group, direction);
420 if (tmp == current_keyboard_widget) {
421 // The current widget was found, but no next widget available in the child.
422 // Try the first widget that does accept keyboard focus.
423 found = true;
424
425 } else if (tmp != nullptr) {
426 // Return the next widget that was found in the child-widget.
427 return tmp;
428 }
429 }
430 }
431
432 if (found) {
433 // Either:
434 // 1. current_keyboard_widget was nullptr; this widget, nor its child widgets accept focus.
435 // 2. current_keyboard_wigget was this; none of the child widgets accept focus.
436 // 3. current_keyboard_widget is a child; none of the following widgets accept focus.
437 return current_keyboard_widget;
438 }
439
440 return std::nullopt;
441 }
442
443 [[nodiscard]] widget_id find_first_widget(keyboard_focus_group group) const noexcept
444 {
445 hi_axiom(loop::main().on_thread());
446
447 for (auto& child : children(false)) {
448 if (child.accepts_keyboard_focus(group)) {
449 return child.id;
450 }
451 }
452 return std::nullopt;
453 }
454
455 [[nodiscard]] widget_id find_last_widget(keyboard_focus_group group) const noexcept
456 {
457 hi_axiom(loop::main().on_thread());
458
459 auto found = widget_id{};
460 for (auto& child : children(false)) {
461 if (child.accepts_keyboard_focus(group)) {
462 found = child.id;
463 }
464 }
465
466 return found;
467 }
468
469 [[nodiscard]] bool is_first(keyboard_focus_group group) const noexcept
470 {
471 hi_axiom(loop::main().on_thread());
472 return parent->find_first_widget(group) == id;
473 }
474
475 [[nodiscard]] bool is_last(keyboard_focus_group group) const noexcept
476 {
477 hi_axiom(loop::main().on_thread());
478 return parent->find_last_widget(group) == id;
479 }
480
487 virtual void scroll_to_show(hi::aarectanglei rectangle) noexcept
488 {
489 hi_axiom(loop::main().on_thread());
490
491 if (parent) {
493 }
494 }
495
499 [[nodiscard]] std::vector<widget_id> parent_chain() const noexcept
500 {
501 hi_axiom(loop::main().on_thread());
502
504
505 if (auto w = this) {
506 chain.push_back(w->id);
507 while (to_bool(w = w->parent)) {
508 chain.push_back(w->id);
509 }
510 }
511
512 return chain;
513 }
514
520 [[nodiscard]] virtual bool is_tab_button() const noexcept
521 {
522 return false;
523 }
524
525 template<forward_of<void()> Callback>
526 [[nodiscard]] callback_token subscribe(Callback&& callback, callback_flags flags = callback_flags::synchronous) const noexcept
527 {
528 return _state_changed.subscribe(std::forward<Callback>(callback), flags);
529 }
530
531 [[nodiscard]] awaiter_type operator co_await() const noexcept
532 {
533 return _state_changed.operator co_await();
534 }
535
536protected:
544 int _scale = -4;
545
548 mutable notifier<void()> _state_changed;
549
559 [[nodiscard]] aarectanglei make_overlay_rectangle(aarectanglei requested_rectangle) const noexcept
560 {
561 hi_axiom(loop::main().on_thread());
562
563 // Move the request_rectangle to window coordinates.
564 hilet requested_window_rectangle = translate2i{layout.clipping_rectangle_on_window()} * requested_rectangle;
565
566 hilet window_bounds = aarectanglei{layout.window_size} - theme<"window">.margin(this);
567 hilet response_window_rectangle = fit(window_bounds, requested_window_rectangle);
568 return layout.from_window * response_window_rectangle;
569 }
570
571private:
572 decltype(mode)::callback_token _mode_cbt;
573};
574
575inline widget *get_if(widget *start, widget_id id, bool include_invisible) noexcept
576{
577 hi_assert_not_null(start);
578
579 if (start->id == id) {
580 return start;
581 }
582 for (auto& child : start->children(include_invisible)) {
583 if (hilet r = get_if(&child, id, include_invisible); r != nullptr) {
584 return r;
585 }
586 }
587 return nullptr;
588}
589
590inline widget& get(widget& start, widget_id id, bool include_invisible)
591{
592 if (auto r = get_if(std::addressof(start), id, include_invisible); r != nullptr) {
593 return *r;
594 }
595 throw not_found_error("get widget by id");
596}
597
598template<std::invocable<widget&> F>
599inline void apply(widget& start, bool include_invisible, F const& f)
600{
601 for (auto& child : start.children(include_invisible)) {
602 apply(child, include_invisible, f);
603 }
604 f(start);
605}
606
607template<std::invocable<widget&> F>
608inline void apply(widget& start, F const& f)
609{
610 return apply(start, true, f);
611}
612
613}} // 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:578
A user interface event.
Definition gui_event.hpp:75
Definition widget.hpp:26
virtual void draw(widget_draw_context &context) noexcept
Draw the widget.
Definition widget.hpp:227
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:208
observer< extent2i > minimum
The minimum size this widget is allowed to be.
Definition widget.hpp:85
observer< bool > hover
Mouse cursor is hovering over the widget.
Definition widget.hpp:53
virtual void scroll_to_show(hi::aarectanglei rectangle) noexcept
Scroll to show the given rectangle on the window.
Definition widget.hpp:487
virtual void request_redraw() const noexcept
Request the widget to be redrawn on the next frame.
Definition widget.hpp:265
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:351
void reset_layout(gfx_surface *new_surface, float new_scale) noexcept
Reset the layout.
Definition widget.hpp:174
virtual hitbox hitbox_test_from_parent(point2i position, hitbox sibling_hitbox) const noexcept
Call hitbox_test from a parent widget.
Definition widget.hpp:157
virtual generator< widget const & > children(bool include_invisible) const noexcept
Get a list of child widgets.
Definition widget.hpp:115
virtual bool is_tab_button() const noexcept
Check if this widget is a tab-button.
Definition widget.hpp:520
virtual box_constraints update_constraints() noexcept
Update the constraints of the widget.
Definition widget.hpp:192
observer< bool > clicked
The widget is being clicked by the mouse.
Definition widget.hpp:57
gfx_surface * surface
The surface this widget is drawn on.
Definition widget.hpp:44
virtual bool accepts_keyboard_focus(keyboard_focus_group group) const noexcept
Check if the widget will accept keyboard focus.
Definition widget.hpp:165
observer< widget_state > state
The state of the widget.
Definition widget.hpp:65
virtual bool handle_event(gui_event const &event) noexcept
Handle command.
Definition widget.hpp:274
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:383
std::vector< widget_id > parent_chain() const noexcept
Get a list of parents of a given widget.
Definition widget.hpp:499
observer< extent2i > maximum
The maximum size this widget is allowed to be.
Definition widget.hpp:89
widget * parent
Pointer to the parent widget.
Definition widget.hpp:40
observer< widget_mode > mode
The widget mode.
Definition widget.hpp:49
observer< bool > focus
The widget has keyboard focus.
Definition widget.hpp:61
size_t semantic_layer
The draw layer of the widget.
Definition widget.hpp:81
virtual hitbox hitbox_test(point2i position) const noexcept
Find the widget that is under the mouse cursor.
Definition widget.hpp:134
virtual hitbox hitbox_test_from_parent(point2i position) const noexcept
Call hitbox_test from a parent widget.
Definition widget.hpp:145
Draw context for drawing using the HikoGUI shaders.
Definition widget_draw_context.hpp:205
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:279
T addressof(T... args)
T max(T... args)
T push_back(T... args)
T reverse(T... args)
T round(T... args)