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/module.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
83 grid_cell cell;
84
85 virtual ~widget() {}
86 widget(widget *parent) noexcept : parent(parent), id(narrow_cast<uint32_t>(++global_counter<"widget::id">))
87 {
88 hi_axiom(loop::main().on_thread());
89
90 if (parent) {
92 }
93
94 _mode_cbt = mode.subscribe([&](auto...) {
95 ++global_counter<"widget:mode:constrain">;
97 });
98 }
99
100 widget(widget const&) = delete;
101 widget(widget&&) = delete;
102 widget& operator=(widget&&) = delete;
103 widget& operator=(widget const&) = delete;
104
107 [[nodiscard]] virtual generator<widget const&> children(bool include_invisible) const noexcept
108 {
109 co_return;
110 }
111
112 [[nodiscard]] generator<widget&> children(bool include_invisible) noexcept
113 {
114 for (auto& child : const_cast<widget const *>(this)->children(include_invisible)) {
115 co_yield const_cast<widget&>(child);
116 }
117 }
118
126 [[nodiscard]] virtual hitbox hitbox_test(point2 position) const noexcept
127 {
128 return {};
129 }
130
137 [[nodiscard]] virtual hitbox hitbox_test_from_parent(point2 position) const noexcept
138 {
139 return hitbox_test(layout.from_parent * position);
140 }
141
149 [[nodiscard]] virtual hitbox hitbox_test_from_parent(point2 position, hitbox sibling_hitbox) const noexcept
150 {
151 return std::max(sibling_hitbox, hitbox_test(layout.from_parent * position));
152 }
153
157 [[nodiscard]] virtual bool accepts_keyboard_focus(keyboard_focus_group group) const noexcept
158 {
159 return false;
160 }
161
166 void reset_layout(gfx_surface *new_surface, float new_scale) noexcept
167 {
168 layout = {};
169 surface = new_surface;
170 _scale = -new_scale;
171 }
172
173 virtual void layout() noexcept {}
174
189 virtual void draw(widget_draw_context& context) noexcept {};
190
191 [[nodiscard]] virtual sub_theme_selector_type sub_theme_selector() const noexcept
192 {
193 auto s = theme_state{};
194 if (*mode == widget_mode::disabled) {
195 s |= theme_state::disabled;
196 } else if (*clicked) {
197 s |= theme_state::active;
198 } else if (*hover) {
199 s |= theme_state::hover;
200 } else {
201 s |= theme_state::enabled;
202 }
203
204 if (*focus) {
205 s |= theme_state::focus;
206 }
207
208 if (*state != widget_state::off) {
209 s |= theme_state::on;
210 }
211
212 s |= static_cast<theme_state>((semantic_layer % 4) * to_underlying(theme_state::layer_1));
213 return sub_theme_selector_type{s, _scale};
214 }
215
216 virtual bool process_event(gui_event const& event) const noexcept
217 {
218 if (parent != nullptr) {
219 return parent->process_event(event);
220 } else {
221 return true;
222 }
223 }
224
227 virtual void request_redraw() const noexcept
228 {
229 process_event({gui_event_type::window_redraw, layout.clipping_rectangle_on_window()});
230 }
231
236 virtual bool handle_event(gui_event const& event) noexcept
237 {
238 hi_axiom(loop::main().on_thread());
239
240 switch (event.type()) {
241 using enum hi::gui_event_type;
242 case keyboard_enter:
243 focus = true;
244 scroll_to_show(layout.rectangle());
245 ++global_counter<"widget:keyboard_enter:redraw">;
247 return true;
248
249 case keyboard_exit:
250 focus = false;
251 ++global_counter<"widget:keyboard_exit:redraw">;
253 return true;
254
255 case mouse_enter:
256 hover = true;
257 ++global_counter<"widget:mouse_enter:redraw">;
259 return true;
260
261 case mouse_exit:
262 hover = false;
263 ++global_counter<"widget:mouse_exit:redraw">;
265 return true;
266
267 case gui_widget_next:
268 process_event(
269 gui_event::window_set_keyboard_target(id, keyboard_focus_group::normal, keyboard_focus_direction::forward));
270 return true;
271
272 case gui_widget_prev:
273 process_event(
274 gui_event::window_set_keyboard_target(id, keyboard_focus_group::normal, keyboard_focus_direction::backward));
275 return true;
276
277 case gui_activate_next:
278 process_event(gui_activate);
279 return process_event(gui_widget_next);
280
281 case gui_event_type::gui_toolbar_next:
282 if (*mode >= widget_mode::partial and accepts_keyboard_focus(keyboard_focus_group::toolbar) and
283 not is_last(keyboard_focus_group::toolbar)) {
284 process_event(
285 gui_event::window_set_keyboard_target(id, keyboard_focus_group::toolbar, keyboard_focus_direction::forward));
286 return true;
287 }
288 break;
289
290 case gui_event_type::gui_toolbar_prev:
291 if (*mode >= widget_mode::partial and accepts_keyboard_focus(keyboard_focus_group::toolbar) and
292 not is_first(keyboard_focus_group::toolbar)) {
293 process_event(
294 gui_event::window_set_keyboard_target(id, keyboard_focus_group::toolbar, keyboard_focus_direction::backward));
295 return true;
296 }
297 break;
298
299 default:;
300 }
301
302 return false;
303 }
304
312 virtual bool
314 {
315 hi_axiom(loop::main().on_thread());
316
317 auto handled = false;
318
319 for (auto& child : children(false)) {
320 handled |= child.handle_event_recursive(event, reject_list);
321 }
322
323 if (not std::ranges::any_of(reject_list, [&](hilet& x) {
324 return x == id;
325 })) {
326 handled |= handle_event(event);
327 }
328
329 return handled;
330 }
331
345 [[nodiscard]] virtual widget_id find_next_widget(
346 widget_id current_keyboard_widget,
347 keyboard_focus_group group,
348 keyboard_focus_direction direction) const noexcept
349 {
350 hi_axiom(loop::main().on_thread());
351
352 auto found = false;
353
354 if (not current_keyboard_widget and accepts_keyboard_focus(group)) {
355 // If there was no current_keyboard_widget, then return this if it accepts focus.
356 return id;
357
358 } else if (current_keyboard_widget == id) {
359 found = true;
360 }
361
362 auto children_ = std::vector<widget const *>{};
363 for (auto& child : children(false)) {
364 children_.push_back(std::addressof(child));
365 }
366
367 if (direction == keyboard_focus_direction::backward) {
368 std::reverse(begin(children_), end(children_));
369 }
370
371 for (auto *child : children_) {
372 hi_axiom_not_null(child);
373
374 if (found) {
375 // Find the first focus accepting widget.
376 if (auto tmp = child->find_next_widget({}, group, direction)) {
377 return tmp;
378 }
379
380 } else {
381 auto tmp = child->find_next_widget(current_keyboard_widget, group, direction);
382 if (tmp == current_keyboard_widget) {
383 // The current widget was found, but no next widget available in the child.
384 // Try the first widget that does accept keyboard focus.
385 found = true;
386
387 } else if (tmp != nullptr) {
388 // Return the next widget that was found in the child-widget.
389 return tmp;
390 }
391 }
392 }
393
394 if (found) {
395 // Either:
396 // 1. current_keyboard_widget was nullptr; this widget, nor its child widgets accept focus.
397 // 2. current_keyboard_wigget was this; none of the child widgets accept focus.
398 // 3. current_keyboard_widget is a child; none of the following widgets accept focus.
399 return current_keyboard_widget;
400 }
401
402 return std::nullopt;
403 }
404
405 [[nodiscard]] widget_id find_first_widget(keyboard_focus_group group) const noexcept
406 {
407 hi_axiom(loop::main().on_thread());
408
409 for (auto& child : children(false)) {
410 if (child.accepts_keyboard_focus(group)) {
411 return child.id;
412 }
413 }
414 return std::nullopt;
415 }
416
417 [[nodiscard]] widget_id find_last_widget(keyboard_focus_group group) const noexcept
418 {
419 hi_axiom(loop::main().on_thread());
420
421 auto found = widget_id{};
422 for (auto& child : children(false)) {
423 if (child.accepts_keyboard_focus(group)) {
424 found = child.id;
425 }
426 }
427
428 return found;
429 }
430
431 [[nodiscard]] bool is_first(keyboard_focus_group group) const noexcept
432 {
433 hi_axiom(loop::main().on_thread());
434 return parent->find_first_widget(group) == id;
435 }
436
437 [[nodiscard]] bool is_last(keyboard_focus_group group) const noexcept
438 {
439 hi_axiom(loop::main().on_thread());
440 return parent->find_last_widget(group) == id;
441 }
442
450 {
451 hi_axiom(loop::main().on_thread());
452
453 if (parent) {
454 parent->scroll_to_show(layout.to_parent * rectangle);
455 }
456 }
457
461 [[nodiscard]] std::vector<widget_id> parent_chain() const noexcept
462 {
463 hi_axiom(loop::main().on_thread());
464
466
467 if (auto w = this) {
468 chain.push_back(w->id);
469 while (to_bool(w = w->parent)) {
470 chain.push_back(w->id);
471 }
472 }
473
474 return chain;
475 }
476
482 [[nodiscard]] virtual bool is_tab_button() const noexcept
483 {
484 return false;
485 }
486
487 template<forward_of<void()> Callback>
488 [[nodiscard]] callback_token subscribe(Callback&& callback, callback_flags flags = callback_flags::synchronous) const noexcept
489 {
490 return _state_changed.subscribe(std::forward<Callback>(callback), flags);
491 }
492
493 [[nodiscard]] awaiter_type operator co_await() const noexcept
494 {
495 return _state_changed.operator co_await();
496 }
497
498protected:
505 float _scale = -1;
506
509 mutable notifier<void()> _state_changed;
510
520 [[nodiscard]] aarectangle make_overlay_rectangle(aarectangle requested_rectangle) const noexcept
521 {
522 hi_axiom(loop::main().on_thread());
523
524 // Move the request_rectangle to window coordinates.
525 hilet requested_window_rectangle = translate2{layout.clipping_rectangle_on_window()} * requested_rectangle;
526
527 hilet window_bounds = aarectangle{layout.window_size} - theme<"window">.margin(this);
528 hilet response_window_rectangle = fit(window_bounds, requested_window_rectangle);
529 return layout.from_window * response_window_rectangle;
530 }
531
532private:
533 decltype(mode)::callback_token _mode_cbt;
534};
535
536inline widget *get_if(widget *start, widget_id id, bool include_invisible) noexcept
537{
538 hi_assert_not_null(start);
539
540 if (start->id == id) {
541 return start;
542 }
543 for (auto& child : start->children(include_invisible)) {
544 if (hilet r = get_if(&child, id, include_invisible); r != nullptr) {
545 return r;
546 }
547 }
548 return nullptr;
549}
550
551inline widget& get(widget& start, widget_id id, bool include_invisible)
552{
553 if (auto r = get_if(std::addressof(start), id, include_invisible); r != nullptr) {
554 return *r;
555 }
556 throw not_found_error("get widget by id");
557}
558
559template<std::invocable<widget&> F>
560inline void apply(widget& start, bool include_invisible, F const& f)
561{
562 for (auto& child : start.children(include_invisible)) {
563 apply(child, include_invisible, f);
564 }
565 f(start);
566}
567
568template<std::invocable<widget&> F>
569inline void apply(widget& start, F const& f)
570{
571 return apply(start, true, f);
572}
573
574}} // 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:545
Class which represents an axis-aligned rectangle.
Definition axis_aligned_rectangle.hpp:27
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:189
virtual void scroll_to_show(hi::aarectangle rectangle) noexcept
Scroll to show the given rectangle on the window.
Definition widget.hpp:449
widget_id id
The numeric identifier of a widget.
Definition widget.hpp:35
observer< bool > hover
Mouse cursor is hovering over the widget.
Definition widget.hpp:53
virtual hitbox hitbox_test(point2 position) const noexcept
Find the widget that is under the mouse cursor.
Definition widget.hpp:126
virtual hitbox hitbox_test_from_parent(point2 position, hitbox sibling_hitbox) const noexcept
Call hitbox_test from a parent widget.
Definition widget.hpp:149
virtual void request_redraw() const noexcept
Request the widget to be redrawn on the next frame.
Definition widget.hpp:227
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:313
void reset_layout(gfx_surface *new_surface, float new_scale) noexcept
Reset the layout.
Definition widget.hpp:166
virtual generator< widget const & > children(bool include_invisible) const noexcept
Get a list of child widgets.
Definition widget.hpp:107
virtual bool is_tab_button() const noexcept
Check if this widget is a tab-button.
Definition widget.hpp:482
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:157
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:236
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:345
std::vector< widget_id > parent_chain() const noexcept
Get a list of parents of a given widget.
Definition widget.hpp:461
virtual hitbox hitbox_test_from_parent(point2 position) const noexcept
Call hitbox_test from a parent widget.
Definition widget.hpp:137
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
Draw context for drawing using the HikoGUI shaders.
Definition widget_draw_context.hpp:204
A cell in a grid.
Definition grid_cell.hpp:141
Definition theme_model.hpp:259
T addressof(T... args)
T max(T... args)
T push_back(T... args)
T reverse(T... args)