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 "../GUI/module.hpp"
12
13namespace hi { inline namespace v1 {
14
22template<fixed_string Name = "">
24public:
25 using super = widget;
26 constexpr static auto prefix = Name / "aperture";
27
28 observer<float> content_width;
29 observer<float> content_height;
30 observer<float> aperture_width;
31 observer<float> aperture_height;
32 observer<float> offset_x;
33 observer<float> offset_y;
34
35 scroll_aperture_widget(widget *parent) noexcept : super(parent)
36 {
37 hi_axiom(loop::main().on_thread());
39
40 // The aperture-widget will not draw itself, only its selected content.
42
43 _content_width_cbt = content_width.subscribe([&](auto...) {
44 ++global_counter<"scroll_aperture_widget:content_width:relayout">;
45 process_event({gui_event_type::window_relayout});
46 });
47 _content_height_cbt = content_height.subscribe([&](auto...) {
48 ++global_counter<"scroll_aperture_widget:content_height:relayout">;
49 process_event({gui_event_type::window_relayout});
50 });
51 _aperture_width_cbt = aperture_width.subscribe([&](auto...) {
52 ++global_counter<"scroll_aperture_widget:aperture_width:relayout">;
53 process_event({gui_event_type::window_relayout});
54 });
55 _aperture_height_cbt = aperture_height.subscribe([&](auto...) {
56 ++global_counter<"scroll_aperture_widget:aperture_height:relayout">;
57 process_event({gui_event_type::window_relayout});
58 });
59 _offset_x_cbt = offset_x.subscribe([&](auto...) {
60 ++global_counter<"scroll_aperture_widget:offset_x:relayout">;
61 process_event({gui_event_type::window_relayout});
62 });
63 _offset_y_cbt = offset_y.subscribe([&](auto...) {
64 ++global_counter<"scroll_aperture_widget:offset_y:relayout">;
65 process_event({gui_event_type::window_relayout});
66 });
67 _minimum_cbt = minimum.subscribe([&](auto...) {
68 ++global_counter<"scroll_aperture_widget:minimum:reconstrain">;
70 });
71 }
72
73 template<typename Widget, typename... Args>
74 Widget& make_widget(Args&&...args) noexcept
75 {
76 hi_axiom(loop::main().on_thread());
77 hi_axiom(_content == nullptr);
78
79 auto tmp = std::make_unique<Widget>(this, std::forward<Args>(args)...);
80 auto& ref = *tmp;
81 _content = std::move(tmp);
82 return ref;
83 }
84
85 [[nodiscard]] bool x_axis_scrolls() const noexcept
86 {
87 return *content_width > *aperture_width;
88 }
89
90 [[nodiscard]] bool y_axis_scrolls() const noexcept
91 {
92 return *content_height > *aperture_height;
93 }
94
96 [[nodiscard]] generator<widget const&> children(bool include_invisible) const noexcept override
97 {
98 co_yield *_content;
99 }
100
101 [[nodiscard]] box_constraints update_constraints() noexcept override
102 {
103 _content_constraints = _content->update_constraints();
104
105 hilet margins = _content_constraints.margins;
106
107 // The aperture can scroll so its minimum width and height are zero.
108 hilet aperture_constraints = box_constraints{
109 extent2{},
110 extent2{
111 _content_constraints.preferred.width() - margins.left() - margins.right(),
112 _content_constraints.preferred.height() - margins.bottom() - margins.top()},
113 extent2{
114 _content_constraints.maximum.width() - margins.left() - margins.right(),
115 _content_constraints.maximum.height() - margins.bottom() - margins.top()}};
116
117 return aperture_constraints.constrain(*minimum, *maximum);
118 }
119
120 void set_layout(widget_layout const& context) noexcept override
121 {
122 if (compare_store(layout, context)) {
123 aperture_width = context.width() - _content_constraints.margins.left() - _content_constraints.margins.right();
124 aperture_height = context.height() - _content_constraints.margins.bottom() - _content_constraints.margins.top();
125
126 // Start scrolling with the preferred size as minimum, so
127 // that widgets in the content don't get unnecessarily squeezed.
128 content_width = *aperture_width < _content_constraints.preferred.width() ? _content_constraints.preferred.width() :
129 *aperture_width;
130 content_height = *aperture_height < _content_constraints.preferred.height() ?
131 _content_constraints.preferred.height() :
132 *aperture_height;
133 }
134
135 // Make sure the offsets are limited to the scrollable area.
136 hilet offset_x_max = std::max(*content_width - *aperture_width, 0.0f);
137 hilet offset_y_max = std::max(*content_height - *aperture_height, 0.0f);
138 offset_x = std::clamp(*offset_x, 0.0f, offset_x_max);
139 offset_y = std::clamp(*offset_y, 0.0f, offset_y_max);
140
141 // The position of the content rectangle relative to the scroll view.
142 // The size is further adjusted if the either the horizontal or vertical scroll bar is invisible.
143 _content_shape = box_shape{
144 _content_constraints,
146 -*offset_x + _content_constraints.margins.left(),
147 -*offset_y + _content_constraints.margins.bottom(),
148 *content_width,
149 *content_height},
150 theme<prefix>.cap_height(this)};
151
152 // The content needs to be at a higher elevation, so that hitbox check
153 // will work correctly for handling scrolling with mouse wheel.
154 _content->set_layout(context.transform(_content_shape, 1.0f, context.rectangle()));
155 }
156
157 void draw(widget_draw_context& context) noexcept override
158 {
160 _content->draw(context);
161 }
162 }
163
164 [[nodiscard]] hitbox hitbox_test(point2 position) const noexcept override
165 {
166 hi_axiom(loop::main().on_thread());
167
168 if (*mode >= widget_mode::partial) {
169 auto r = _content->hitbox_test_from_parent(position);
170
171 if (layout.contains(position)) {
172 r = std::max(r, hitbox{id, layout.elevation});
173 }
174 return r;
175
176 } else {
177 return {};
178 }
179 }
180
181 bool handle_event(gui_event const& event) noexcept override
182 {
183 hi_axiom(loop::main().on_thread());
184
185 if (event == gui_event_type::mouse_wheel) {
186 hilet new_offset_x = *offset_x + event.mouse().wheel_delta.x() * _scale;
187 hilet new_offset_y = *offset_y + event.mouse().wheel_delta.y() * _scale;
188 hilet max_offset_x = std::max(0.0f, *content_width - *aperture_width);
189 hilet max_offset_y = std::max(0.0f, *content_height - *aperture_height);
190
191 offset_x = std::clamp(new_offset_x, 0.0f, max_offset_x);
192 offset_y = std::clamp(new_offset_y, 0.0f, max_offset_y);
193 ++global_counter<"scroll_aperture_widget:mouse_wheel:relayout">;
194 process_event({gui_event_type::window_relayout});
195 return true;
196 } else {
197 return super::handle_event(event);
198 }
199 }
200
201 void scroll_to_show(hi::aarectangle to_show) noexcept override
202 {
203 if (layout) {
204 auto safe_rectangle = intersect(layout.rectangle(), layout.clipping_rectangle);
205 auto delta_x = 0.0f;
206 auto delta_y = 0.0f;
207
208 hilet margin = std::max(
209 {theme<prefix>.margin_left(this),
210 theme<prefix>.margin_right(this),
211 theme<prefix>.margin_top(this),
212 theme<prefix>.margin_bottom(this)});
213 if (safe_rectangle.width() > margin * 2.0f and safe_rectangle.height() > margin * 2.0f) {
214 // This will look visually better, if the selected widget is moved with some margin from
215 // the edge of the scroll widget. The margins of the content do not have anything to do
216 // with the margins that are needed here.
217 safe_rectangle = safe_rectangle - margin;
218
219 if (to_show.right() > safe_rectangle.right()) {
220 delta_x = to_show.right() - safe_rectangle.right();
221 } else if (to_show.left() < safe_rectangle.left()) {
222 delta_x = to_show.left() - safe_rectangle.left();
223 }
224
225 if (to_show.top() > safe_rectangle.top()) {
226 delta_y = to_show.top() - safe_rectangle.top();
227 } else if (to_show.bottom() < safe_rectangle.bottom()) {
228 delta_y = to_show.bottom() - safe_rectangle.bottom();
229 }
230
231 // Scroll the widget
232 offset_x += delta_x;
233 offset_y += delta_y;
234 }
235
236 // There may be recursive scroll view, and they all need to move until the rectangle is visible.
237 if (parent) {
238 parent->scroll_to_show(layout.to_parent * translate2(delta_x, delta_y) * to_show);
239 }
240
241 } else {
242 return super::scroll_to_show(to_show);
243 }
244 }
246private:
247 box_constraints _content_constraints;
248 box_shape _content_shape;
250 decltype(content_width)::callback_token _content_width_cbt;
251 decltype(content_height)::callback_token _content_height_cbt;
252 decltype(aperture_width)::callback_token _aperture_width_cbt;
253 decltype(aperture_height)::callback_token _aperture_height_cbt;
254 decltype(offset_x)::callback_token _offset_x_cbt;
255 decltype(offset_y)::callback_token _offset_y_cbt;
256 decltype(minimum)::callback_token _minimum_cbt;
257};
258}} // namespace hi::v1
#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 hilet
Invariant should be the default for variables.
Definition utility.hpp:23
@ window_relayout
Request that widgets get laid out on the next frame.
@ window_reconstrain
Request that widget get constraint on the next frame.
@ partial
A widget is partially enabled.
@ invisible
The widget is invisible.
DOXYGEN BUG.
Definition algorithm.hpp:13
geometry/margins.hpp
Definition cache.hpp:11
bool compare_store(T &lhs, U &&rhs) noexcept
Compare then store if there was a change.
Definition utility.hpp:212
Class which represents an axis-aligned rectangle.
Definition axis_aligned_rectangle.hpp:27
constexpr value_type & width() noexcept
Access the x-as-width element from the extent.
Definition extent.hpp:166
constexpr value_type & height() noexcept
Access the y-as-height element from the extent.
Definition extent.hpp:177
A user interface event.
Definition gui_event.hpp:75
Definition widget.hpp:26
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
virtual bool handle_event(gui_event const &event) noexcept
Handle command.
Definition widget.hpp:236
widget * parent
Pointer to the parent widget.
Definition widget.hpp:40
observer< widget_mode > mode
The widget mode.
Definition widget.hpp:49
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
The layout of a widget.
Definition widget_layout.hpp:37
constexpr widget_layout transform(box_shape const &child_shape, float child_elevation, aarectangle new_clipping_rectangle) const noexcept
Create a new widget_layout for the child widget.
Definition widget_layout.hpp:203
2D constraints.
Definition box_constraints.hpp:22
Definition box_shape.hpp:15
A scroll aperture widget.
Definition scroll_aperture_widget.hpp:23
T max(T... args)
T move(T... args)