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(widget *parent) noexcept : super(parent)
34 {
35 hi_axiom(loop::main().on_thread());
37
38 // The aperture-widget will not draw itself, only its selected content.
40
41 _content_width_cbt = content_width.subscribe([&](auto...) {
42 ++global_counter<"scroll_aperture_widget:content_width:relayout">;
43 process_event({gui_event_type::window_relayout});
44 });
45 _content_height_cbt = content_height.subscribe([&](auto...) {
46 ++global_counter<"scroll_aperture_widget:content_height:relayout">;
47 process_event({gui_event_type::window_relayout});
48 });
49 _aperture_width_cbt = aperture_width.subscribe([&](auto...) {
50 ++global_counter<"scroll_aperture_widget:aperture_width:relayout">;
51 process_event({gui_event_type::window_relayout});
52 });
53 _aperture_height_cbt = aperture_height.subscribe([&](auto...) {
54 ++global_counter<"scroll_aperture_widget:aperture_height:relayout">;
55 process_event({gui_event_type::window_relayout});
56 });
57 _offset_x_cbt = offset_x.subscribe([&](auto...) {
58 ++global_counter<"scroll_aperture_widget:offset_x:relayout">;
59 process_event({gui_event_type::window_relayout});
60 });
61 _offset_y_cbt = offset_y.subscribe([&](auto...) {
62 ++global_counter<"scroll_aperture_widget:offset_y:relayout">;
63 process_event({gui_event_type::window_relayout});
64 });
65 }
66
67 template<typename Widget, typename... Args>
68 Widget& make_widget(Args&&...args) noexcept
69 {
70 hi_axiom(loop::main().on_thread());
71 hi_axiom(_content == nullptr);
72
73 auto tmp = std::make_unique<Widget>(this, std::forward<Args>(args)...);
74 auto& ref = *tmp;
75 _content = std::move(tmp);
76 return ref;
77 }
78
79 [[nodiscard]] bool x_axis_scrolls() const noexcept
80 {
81 return *content_width > *aperture_width;
82 }
83
84 [[nodiscard]] bool y_axis_scrolls() const noexcept
85 {
86 return *content_height > *aperture_height;
87 }
88
90 [[nodiscard]] generator<widget *> children() const noexcept override
91 {
92 co_yield _content.get();
93 }
94
95 widget_constraints const& set_constraints(set_constraints_context const& context) noexcept override
96 {
97 _layout = {};
98
99 hi_assert_not_null(_content);
100 hilet content_constraints = _content->set_constraints(context);
101
102 hilet minimum_size = extent2{
103 content_constraints.margins.left() + content_constraints.minimum.width() + content_constraints.margins.right(),
104 content_constraints.margins.top() + content_constraints.minimum.height() + content_constraints.margins.bottom()};
105 hilet preferred_size = extent2{
106 content_constraints.margins.left() + content_constraints.preferred.width() + content_constraints.margins.right(),
107 content_constraints.margins.top() + content_constraints.preferred.height() + content_constraints.margins.bottom()};
108 hilet maximum_size = extent2{
109 content_constraints.margins.left() + content_constraints.maximum.width() + content_constraints.margins.right(),
110 content_constraints.margins.top() + content_constraints.maximum.height() + content_constraints.margins.bottom()};
111
112 return _constraints = {minimum_size, preferred_size, maximum_size, margins{}};
113 }
114
115 void set_layout(widget_layout const& context) noexcept override
116 {
117 hilet content_constraints = _content->constraints();
118 hilet margins = content_constraints.margins;
119
120 if (compare_store(_layout, context)) {
121 hilet preferred_size = content_constraints.preferred;
122
123 aperture_width = context.width() - margins.left() - margins.right();
124 aperture_height = context.height() - margins.bottom() - 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 < preferred_size.width() ? preferred_size.width() : *aperture_width;
129 content_height = *aperture_height < preferred_size.height() ? preferred_size.height() : *aperture_height;
130 }
131
132 // Make sure the offsets are limited to the scrollable area.
133 hilet offset_x_max = std::max(*content_width - *aperture_width, 0.0f);
134 hilet offset_y_max = std::max(*content_height - *aperture_height, 0.0f);
135 offset_x = std::clamp(std::round(*offset_x), 0.0f, offset_x_max);
136 offset_y = std::clamp(std::round(*offset_y), 0.0f, offset_y_max);
137
138 // The position of the content rectangle relative to the scroll view.
139 // The size is further adjusted if the either the horizontal or vertical scroll bar is invisible.
140 _content_rectangle = {-*offset_x + margins.left(), -*offset_y + margins.bottom(), *content_width, *content_height};
141
142 // The content needs to be at a higher elevation, so that hitbox check
143 // will work correctly for handling scrolling with mouse wheel.
144 _content->set_layout(context.transform(_content_rectangle, 1.0f, context.rectangle()));
145 }
146
147 void draw(draw_context const& context) noexcept
148 {
150 _content->draw(context);
151 }
152 }
153
154 [[nodiscard]] hitbox hitbox_test(point3 position) const noexcept override
155 {
156 hi_axiom(loop::main().on_thread());
157
158 if (*mode >= widget_mode::partial) {
159 auto r = _content->hitbox_test_from_parent(position);
160
161 if (layout().contains(position)) {
162 r = std::max(r, hitbox{this, position});
163 }
164 return r;
165
166 } else {
167 return {};
168 }
169 }
170
171 bool handle_event(gui_event const& event) noexcept override
172 {
173 hi_axiom(loop::main().on_thread());
174
175 if (event == gui_event_type::mouse_wheel) {
176 hilet new_offset_x = *offset_x + event.mouse().wheel_delta.x() * _layout.theme->scale;
177 hilet new_offset_y = *offset_y + event.mouse().wheel_delta.y() * _layout.theme->scale;
178 hilet max_offset_x = std::max(0.0f, *content_width - *aperture_width);
179 hilet max_offset_y = std::max(0.0f, *content_height - *aperture_height);
180
181 offset_x = std::clamp(new_offset_x, 0.0f, max_offset_x);
182 offset_y = std::clamp(new_offset_y, 0.0f, max_offset_y);
183 ++global_counter<"scroll_aperture_widget:mouse_wheel:relayout">;
184 process_event({gui_event_type::window_relayout});
185 return true;
186 } else {
187 return super::handle_event(event);
188 }
189 }
190
191 void scroll_to_show(hi::aarectangle to_show) noexcept override
192 {
193 if (_layout) {
194 auto safe_rectangle = intersect(_layout.rectangle(), _layout.clipping_rectangle);
195 float delta_x = 0.0f;
196 float delta_y = 0.0f;
197
198 if (safe_rectangle.width() > _layout.theme->margin * 2.0f and
199 safe_rectangle.height() > _layout.theme->margin * 2.0f) {
200 // This will look visually better, if the selected widget is moved with some margin from
201 // the edge of the scroll widget. The margins of the content do not have anything to do
202 // with the margins that are needed here.
203 safe_rectangle = safe_rectangle - _layout.theme->margin;
204
205 if (to_show.right() > safe_rectangle.right()) {
206 delta_x = to_show.right() - safe_rectangle.right();
207 } else if (to_show.left() < safe_rectangle.left()) {
208 delta_x = to_show.left() - safe_rectangle.left();
209 }
210
211 if (to_show.top() > safe_rectangle.top()) {
212 delta_y = to_show.top() - safe_rectangle.top();
213 } else if (to_show.bottom() < safe_rectangle.bottom()) {
214 delta_y = to_show.bottom() - safe_rectangle.bottom();
215 }
216
217 // Scroll the widget
218 offset_x += delta_x;
219 offset_y += delta_y;
220 }
221
222 // There may be recursive scroll view, and they all need to move until the rectangle is visible.
223 if (parent) {
224 parent->scroll_to_show(bounding_rectangle(_layout.to_parent * translate2(delta_x, delta_y) * to_show));
225 }
226
227 } else {
228 return super::scroll_to_show(to_show);
229 }
230 }
232private:
233 aarectangle _content_rectangle;
235 decltype(content_width)::callback_token _content_width_cbt;
236 decltype(content_height)::callback_token _content_height_cbt;
237 decltype(aperture_width)::callback_token _aperture_width_cbt;
238 decltype(aperture_height)::callback_token _aperture_height_cbt;
239 decltype(offset_x)::callback_token _offset_x_cbt;
240 decltype(offset_y)::callback_token _offset_y_cbt;
241};
242
243}} // namespace hi::v1
#define hi_axiom(expression)
Specify an axiom; an expression that is true.
Definition assert.hpp:133
#define hi_assert_not_null(x)
Assert if an expression is not nullptr.
Definition assert.hpp:118
#define hilet
Invariant should be the default for variables.
Definition utility.hpp:23
Defines widget.
@ window_relayout
Request that widgets get laid out on the next frame.
@ partial
A widget is partially enabled.
@ invisible
The widget is invisible.
DOXYGEN BUG.
Definition algorithm.hpp:15
The HikoGUI namespace.
Definition ascii.hpp:19
bool compare_store(T &lhs, U &&rhs) noexcept
Compare then store if there was a change.
Definition utility.hpp:196
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:76
A scroll aperture widget.
Definition scroll_aperture_widget.hpp:22
Definition set_constraints_context.hpp:15
An interactive graphical object as part of the user-interface.
Definition widget.hpp:45
widget_layout const & layout() const noexcept
Get the current layout for this widget.
Definition widget.hpp:181
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:83
widget(widget *parent) noexcept
virtual bool handle_event(gui_event const &event) noexcept
Handle command.
widget * parent
Pointer to the parent widget.
Definition widget.hpp:50
void scroll_to_show() noexcept
Scroll to show the important part of the widget.
Definition widget.hpp:274
observer< widget_mode > mode
The widget mode.
Definition widget.hpp:59
The constraints of a widget.
Definition widget_constraints.hpp:26
The layout of a widget.
Definition widget_layout.hpp:40
T max(T... args)
T move(T... args)
T round(T... args)