HikoGUI
A low latency retained GUI
Loading...
Searching...
No Matches
scroll_aperture_widget.hpp
Go to the documentation of this file.
1// Copyright Take Vos 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
13namespace hi { inline namespace v1 {
14
23public:
24 using super = widget;
25
26 observer<float> content_width;
27 observer<float> content_height;
28 observer<float> aperture_width;
29 observer<float> aperture_height;
30 observer<float> offset_x;
31 observer<float> offset_y;
32
33 scroll_aperture_widget(gui_window& window, widget *parent) noexcept : super(window, parent)
34 {
35 hi_axiom(is_gui_thread());
36 hi_axiom(parent);
37
38 // The aperture-widget will not draw itself, only its selected content.
40
41 // clang-format off
42 _content_width_cbt = content_width.subscribe([&](auto...){ request_relayout(); });
43 _content_height_cbt = content_height.subscribe([&](auto...){ request_relayout(); });
44 _aperture_width_cbt = aperture_width.subscribe([&](auto...){ request_relayout(); });
45 _aperture_height_cbt = aperture_height.subscribe([&](auto...){ request_relayout(); });
46 _offset_x_cbt = offset_x.subscribe([&](auto...){ request_relayout(); });
47 _offset_y_cbt = offset_y.subscribe([&](auto...){ request_relayout(); });
48 // clang-format off
49 }
50
51 template<typename Widget, typename... Args>
52 Widget &make_widget(Args &&...args) noexcept
53 {
54 hi_axiom(is_gui_thread());
55 hi_axiom(not _content);
56
57 auto tmp = std::make_unique<Widget>(window, this, std::forward<Args>(args)...);
58 auto &ref = *tmp;
59 _content = std::move(tmp);
60 return ref;
61 }
62
63 [[nodiscard]] bool x_axis_scrolls() const noexcept
64 {
65 return *content_width > *aperture_width;
66 }
67
68 [[nodiscard]] bool y_axis_scrolls() const noexcept
69 {
70 return *content_height > *aperture_height;
71 }
72
74 [[nodiscard]] generator<widget *> children() const noexcept override
75 {
76 co_yield _content.get();
77 }
78
79 widget_constraints const &set_constraints() noexcept override
80 {
81 _layout = {};
82
83 hi_axiom(_content);
84 hilet content_constraints = _content->set_constraints();
85
86 hilet minimum_size = extent2{
87 content_constraints.margins.left() + content_constraints.minimum.width() + content_constraints.margins.right(),
88 content_constraints.margins.top() + content_constraints.minimum.height() + content_constraints.margins.bottom()};
89 hilet preferred_size = extent2{
90 content_constraints.margins.left() + content_constraints.preferred.width() + content_constraints.margins.right(),
91 content_constraints.margins.top() + content_constraints.preferred.height() + content_constraints.margins.bottom()};
92 hilet maximum_size = extent2{
93 content_constraints.margins.left() + content_constraints.maximum.width() + content_constraints.margins.right(),
94 content_constraints.margins.top() + content_constraints.maximum.height() + content_constraints.margins.bottom()};
95
96 return _constraints = {minimum_size, preferred_size, maximum_size, margins{}};
97 }
98
99 void set_layout(widget_layout const &layout) noexcept override
100 {
101 hilet content_constraints = _content->constraints();
102 hilet margins = content_constraints.margins;
103
104 if (compare_store(_layout, layout)) {
105 hilet preferred_size = content_constraints.preferred;
106
107 aperture_width = layout.width() - margins.left() - margins.right();
108 aperture_height = layout.height() - margins.bottom() - margins.top();
109
110 // Start scrolling with the preferred size as minimum, so
111 // that widgets in the content don't get unnecessarily squeezed.
112 content_width = *aperture_width < preferred_size.width() ? preferred_size.width() : *aperture_width;
113 content_height = *aperture_height < preferred_size.height() ? preferred_size.height() : *aperture_height;
114 }
115
116 // Make sure the offsets are limited to the scrollable area.
117 hilet offset_x_max = std::max(*content_width - *aperture_width, 0.0f);
118 hilet offset_y_max = std::max(*content_height - *aperture_height, 0.0f);
119 offset_x = std::clamp(std::round(*offset_x), 0.0f, offset_x_max);
120 offset_y = std::clamp(std::round(*offset_y), 0.0f, offset_y_max);
121
122 // The position of the content rectangle relative to the scroll view.
123 // The size is further adjusted if the either the horizontal or vertical scroll bar is invisible.
124 _content_rectangle = {
125 -*offset_x + margins.left(), -*offset_y + margins.bottom(), *content_width, *content_height};
126
127 // The content needs to be at a higher elevation, so that hitbox check
128 // will work correctly for handling scrolling with mouse wheel.
129 _content->set_layout(layout.transform(_content_rectangle, 1.0f, layout.rectangle()));
130 }
131
132 void draw(draw_context const &context) noexcept
133 {
135 _content->draw(context);
136 }
137 }
138
139 [[nodiscard]] hitbox hitbox_test(point3 position) const noexcept override
140 {
141 hi_axiom(is_gui_thread());
142
143 if (*mode >= widget_mode::partial) {
144 auto r = _content->hitbox_test_from_parent(position);
145
146 if (layout().contains(position)) {
147 r = std::max(r, hitbox{this, position});
148 }
149 return r;
150
151 } else {
152 return {};
153 }
154 }
155
156 bool handle_event(gui_event const &event) noexcept override
157 {
158 hi_axiom(is_gui_thread());
159
160 if (event == gui_event_type::mouse_wheel) {
161 hilet new_offset_x = *offset_x + event.mouse().wheel_delta.x() * theme().scale;
162 hilet new_offset_y = *offset_y + event.mouse().wheel_delta.y() * theme().scale;
163 hilet max_offset_x = std::max(0.0f, *content_width - *aperture_width);
164 hilet max_offset_y = std::max(0.0f, *content_height - *aperture_height);
165
166 offset_x = std::clamp(new_offset_x, 0.0f, max_offset_x);
167 offset_y = std::clamp(new_offset_y, 0.0f, max_offset_y);
169 return true;
170 } else {
171 return super::handle_event(event);
172 }
173 }
174
175 void scroll_to_show(hi::aarectangle to_show) noexcept override
176 {
177 auto safe_rectangle = intersect(_layout.rectangle(), _layout.clipping_rectangle);
178 float delta_x = 0.0f;
179 float delta_y = 0.0f;
180
181 if (safe_rectangle.width() > theme().margin and safe_rectangle.height() > theme().margin) {
182 // This will look visually better, if the selected widget is moved with some margin from
183 // the edge of the scroll widget. The margins of the content do not have anything to do
184 // with the margins that are needed here.
185 safe_rectangle = safe_rectangle - theme().margin;
186
187 if (to_show.right() > safe_rectangle.right()) {
188 delta_x = to_show.right() - safe_rectangle.right();
189 } else if (to_show.left() < safe_rectangle.left()) {
190 delta_x = to_show.left() - safe_rectangle.left();
191 }
192
193 if (to_show.top() > safe_rectangle.top()) {
194 delta_y = to_show.top() - safe_rectangle.top();
195 } else if (to_show.bottom() < safe_rectangle.bottom()) {
196 delta_y = to_show.bottom() - safe_rectangle.bottom();
197 }
198
199 // Scroll the widget
200 offset_x += delta_x;
201 offset_y += delta_y;
202 }
203
204 // There may be recursive scroll view, and they all need to move until the rectangle is visible.
205 if (parent) {
206 parent->scroll_to_show(bounding_rectangle(_layout.to_parent * translate2(delta_x, delta_y) * to_show));
207 }
208 }
210private:
211 aarectangle _content_rectangle;
213 decltype(content_width)::callback_token _content_width_cbt;
214 decltype(content_height)::callback_token _content_height_cbt;
215 decltype(aperture_width)::callback_token _aperture_width_cbt;
216 decltype(aperture_height)::callback_token _aperture_height_cbt;
217 decltype(offset_x)::callback_token _offset_x_cbt;
218 decltype(offset_y)::callback_token _offset_y_cbt;
219};
220
221}} // namespace hi::inline v1
#define hilet
Invariant should be the default for variables.
Definition utility.hpp:23
Defines widget.
@ partial
A widget is partially enabled.
@ invisible
The widget is invisible.
DOXYGEN BUG.
Definition algorithm.hpp:15
The HikoGUI namespace.
Definition ascii.hpp:19
constexpr float & width() noexcept
Access the x-as-width element from the extent.
Definition extent.hpp:140
A user interface event.
Definition gui_event.hpp:66
A scroll aperture widget.
Definition scroll_aperture_widget.hpp:22
An interactive graphical object as part of the user-interface.
Definition widget.hpp:44
widget_layout const & layout() const noexcept
Get the current layout for this widget.
Definition widget.hpp:198
virtual void scroll_to_show(hi::aarectangle rectangle) noexcept
Scroll to show the given rectangle on the window.
int semantic_layer
The draw layer of the widget.
Definition widget.hpp:86
widget *const parent
Pointer to the parent widget.
Definition widget.hpp:53
widget(gui_window &window, widget *parent) noexcept
hi::theme const & theme() const noexcept
Get the theme.
virtual bool handle_event(gui_event const &event) noexcept
Handle command.
void request_relayout() const noexcept
Request the window to be relayout on the next frame.
void scroll_to_show() noexcept
Scroll to show the important part of the widget.
Definition widget.hpp:313
observer< widget_mode > mode
The widget mode.
Definition widget.hpp:62
gui_window & window
Convenient reference to the Window.
Definition widget.hpp:48
The constraints of a widget.
Definition widget_constraints.hpp:26
The layout of a widget.
Definition widget_layout.hpp:37
constexpr widget_layout transform(aarectangle const &child_rectangle, float elevation, aarectangle new_clipping_rectangle, widget_baseline new_baseline=widget_baseline{}) const noexcept
Create a new widget_layout for the child widget.
Definition widget_layout.hpp:186
T max(T... args)
T move(T... args)
T round(T... args)