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
8
9#pragma once
10
11#include "widget.hpp"
12#include "../macros.hpp"
13#include <coroutine>
14
15hi_export_module(hikogui.widgets.scroll_aperture_widget);
16
17hi_export namespace hi { inline namespace v1 {
18
27public:
28 using super = widget;
29
30 observer<float> content_width;
31 observer<float> content_height;
32 observer<float> aperture_width;
33 observer<float> aperture_height;
34 observer<float> offset_x;
35 observer<float> offset_y;
36
37 scroll_aperture_widget() noexcept : super()
38 {
39 hi_axiom(loop::main().on_thread());
40
41 _content_width_cbt = content_width.subscribe([&](auto...) {
42 ++global_counter<"scroll_aperture_widget:content_width:relayout">;
44 });
45 _content_height_cbt = content_height.subscribe([&](auto...) {
46 ++global_counter<"scroll_aperture_widget:content_height:relayout">;
48 });
49 _aperture_width_cbt = aperture_width.subscribe([&](auto...) {
50 ++global_counter<"scroll_aperture_widget:aperture_width:relayout">;
52 });
53 _aperture_height_cbt = aperture_height.subscribe([&](auto...) {
54 ++global_counter<"scroll_aperture_widget:aperture_height:relayout">;
56 });
57 _offset_x_cbt = offset_x.subscribe([&](auto...) {
58 ++global_counter<"scroll_aperture_widget:offset_x:relayout">;
60 });
61 _offset_y_cbt = offset_y.subscribe([&](auto...) {
62 ++global_counter<"scroll_aperture_widget:offset_y:relayout">;
64 });
65 _minimum_cbt = minimum.subscribe([&](auto...) {
66 ++global_counter<"scroll_aperture_widget:minimum:reconstrain">;
68 });
69 }
70
71 void set_widget(std::unique_ptr<widget> new_child) noexcept
72 {
73 if (new_child) {
74 new_child->set_parent(this);
75 }
76 auto old_child = std::exchange(_content, std::move(new_child));
77 if (old_child) {
78 old_child->set_parent(nullptr);
79 }
80 }
81
82 template<typename Widget, typename... Args>
83 Widget& emplace(Args&&...args)
84 {
85 hi_axiom(loop::main().on_thread());
86 hi_axiom(_content == nullptr);
87
88 auto tmp = std::make_unique<Widget>(std::forward<Args>(args)...);
89 auto& ref = *tmp;
90 set_widget(std::move(tmp));
91 return ref;
92 }
93
94 [[nodiscard]] bool x_axis_scrolls() const noexcept
95 {
96 return *content_width > *aperture_width;
97 }
98
99 [[nodiscard]] bool y_axis_scrolls() const noexcept
100 {
101 return *content_height > *aperture_height;
102 }
103
105 [[nodiscard]] generator<widget_intf &> children(bool include_invisible) noexcept override
106 {
107 if (_content) {
108 co_yield *_content;
109 }
110 }
111
112 [[nodiscard]] box_constraints update_constraints() noexcept override
113 {
114 _layout = {};
115 _content_constraints = _content->update_constraints();
116
117 // The aperture can scroll so its minimum width and height are zero.
118 auto aperture_constraints = _content_constraints;
119 aperture_constraints.minimum = extent2{0, 0};
120
121 aperture_constraints = aperture_constraints.constrain(*minimum, *maximum);
122 aperture_constraints += extent2{
123 _content_constraints.margins.left() + _content_constraints.margins.right(),
124 _content_constraints.margins.top() + _content_constraints.margins.bottom()};
125 aperture_constraints.margins = {};
126 return aperture_constraints;
127 }
128
129 void set_layout(widget_layout const& context) noexcept override
130 {
131 if (compare_store(_layout, context)) {
132 aperture_width = context.width() - _content_constraints.margins.left() - _content_constraints.margins.right();
133 aperture_height = context.height() - _content_constraints.margins.bottom() - _content_constraints.margins.top();
134
135 // Start scrolling with the preferred size as minimum, so
136 // that widgets in the content don't get unnecessarily squeezed.
137 content_width = *aperture_width < _content_constraints.preferred.width() ? _content_constraints.preferred.width() :
138 *aperture_width;
139 content_height = *aperture_height < _content_constraints.preferred.height() ?
140 _content_constraints.preferred.height() :
141 *aperture_height;
142 }
143
144 // Make sure the offsets are limited to the scrollable area.
145 auto const offset_x_max = std::max(*content_width - *aperture_width, 0.0f);
146 auto const offset_y_max = std::max(*content_height - *aperture_height, 0.0f);
147 offset_x = std::clamp(*offset_x, 0.0f, offset_x_max);
148 offset_y = std::clamp(*offset_y, 0.0f, offset_y_max);
149
150 // The position of the content rectangle relative to the scroll view.
151 // The size is further adjusted if the either the horizontal or vertical scroll bar is invisible.
152 _content_shape = box_shape{
153 _content_constraints,
155 -*offset_x + _content_constraints.margins.left(),
156 -*offset_y + _content_constraints.margins.bottom(),
157 *content_width,
158 *content_height},
159 theme().baseline_adjustment()};
160
161 // The content needs to be at a higher elevation, so that hitbox check
162 // will work correctly for handling scrolling with mouse wheel.
163 _content->set_layout(context.transform(_content_shape, transform_command::level, context.rectangle()));
164 }
165
166 void draw(draw_context const& context) noexcept override
167 {
168 if (mode() > widget_mode::invisible) {
169 _content->draw(context);
170 }
171 }
172
173 [[nodiscard]] hitbox hitbox_test(point2 position) const noexcept override
174 {
175 hi_axiom(loop::main().on_thread());
176
177 if (mode() >= widget_mode::partial) {
178 auto r = _content->hitbox_test_from_parent(position);
179
180 if (layout().contains(position)) {
181 r = std::max(r, hitbox{id, _layout.elevation});
182 }
183 return r;
184
185 } else {
186 return {};
187 }
188 }
189
190 bool handle_event(gui_event const& event) noexcept override
191 {
192 hi_axiom(loop::main().on_thread());
193
194 if (event == gui_event_type::mouse_wheel) {
195 auto const new_offset_x = *offset_x + std::round((unit::dips(event.mouse().wheel_delta.x()) * theme().pixel_density).in(unit::pixels));
196 auto const new_offset_y = *offset_y + std::round((unit::dips(event.mouse().wheel_delta.y()) * theme().pixel_density).in(unit::pixels));
197 auto const max_offset_x = std::max(0.0f, *content_width - *aperture_width);
198 auto const max_offset_y = std::max(0.0f, *content_height - *aperture_height);
199
200 offset_x = std::clamp(new_offset_x, 0.0f, max_offset_x);
201 offset_y = std::clamp(new_offset_y, 0.0f, max_offset_y);
202 ++global_counter<"scroll_aperture_widget:mouse_wheel:relayout">;
204 return true;
205 } else {
206 return super::handle_event(event);
207 }
208 }
209
210 void scroll_to_show(hi::aarectangle to_show) noexcept override
211 {
212 if (_layout) {
213 auto safe_rectangle = intersect(_layout.rectangle(), _layout.clipping_rectangle);
214 auto delta_x = 0.0f;
215 auto delta_y = 0.0f;
216
217 if (safe_rectangle.width() > theme().margin<float>() * 2.0f and safe_rectangle.height() > theme().margin<float>() * 2.0f) {
218 // This will look visually better, if the selected widget is moved with some margin from
219 // the edge of the scroll widget. The margins of the content do not have anything to do
220 // with the margins that are needed here.
221 safe_rectangle = safe_rectangle - theme().margin<float>();
222
223 if (to_show.right() > safe_rectangle.right()) {
224 delta_x = to_show.right() - safe_rectangle.right();
225 } else if (to_show.left() < safe_rectangle.left()) {
226 delta_x = to_show.left() - safe_rectangle.left();
227 }
228
229 if (to_show.top() > safe_rectangle.top()) {
230 delta_y = to_show.top() - safe_rectangle.top();
231 } else if (to_show.bottom() < safe_rectangle.bottom()) {
232 delta_y = to_show.bottom() - safe_rectangle.bottom();
233 }
234
235 // Scroll the widget
236 offset_x = std::round(offset_x + delta_x);
237 offset_y = std::round(offset_y + delta_y);
238 }
239
240 // There may be recursive scroll view, and they all need to move until the rectangle is visible.
241 if (auto *p = parent()) {
242 p->scroll_to_show(_layout.to_parent * translate2(delta_x, delta_y) * to_show);
243 }
244
245 } else {
246 return super::scroll_to_show(to_show);
247 }
248 }
250private:
251 box_constraints _content_constraints;
252 box_shape _content_shape;
254 callback<void(float)> _content_width_cbt;
255 callback<void(float)> _content_height_cbt;
256 callback<void(float)> _aperture_width_cbt;
257 callback<void(float)> _aperture_height_cbt;
258 callback<void(float)> _offset_x_cbt;
259 callback<void(float)> _offset_y_cbt;
260 callback<void(extent2)> _minimum_cbt;
261};
262
263}} // namespace hi::v1
Defines widget.
@ window_relayout
Request that widgets get laid out on the next frame.
Definition gui_event_type.hpp:47
@ window_reconstrain
Request that widget get constraint on the next frame.
Definition gui_event_type.hpp:48
@ partial
A widget is partially enabled.
Definition widget_state.hpp:73
@ invisible
The widget is invisible.
Definition widget_state.hpp:41
The HikoGUI namespace.
Definition array_generic.hpp:21
The HikoGUI API version 1.
Definition array_generic.hpp:22
@ pixel_density
The attributes that need to be modified when the pixel density changes.
Definition style_modify_mask.hpp:59
@ margin
A margin or padding value was modified.
Definition style_modify_mask.hpp:39
bool compare_store(T &lhs, U &&rhs) noexcept
Compare then store if there was a change.
Definition misc.hpp:53
@ level
The child widget stays at the same elevation and layer.
Definition widget_layout.hpp:26
Definition callback.hpp:77
Class which represents an axis-aligned rectangle.
Definition aarectangle.hpp:33
A high-level geometric extent.
Definition extent2.hpp:32
constexpr float & width() noexcept
Access the x-as-width element from the extent.
Definition extent2.hpp:107
constexpr float & height() noexcept
Access the y-as-height element from the extent.
Definition extent2.hpp:118
Definition translate2.hpp:18
constexpr float & x() noexcept
Access the x element from the vector.
Definition vector2.hpp:65
constexpr float & y() noexcept
Access the y element from the vector.
Definition vector2.hpp:73
Draw context for drawing using the HikoGUI shaders.
Definition draw_context_intf.hpp:209
vector2 wheel_delta
Change in wheel rotation, in points (pt).
Definition gui_event.hpp:58
A user interface event.
Definition gui_event.hpp:82
mouse_event_data & mouse() noexcept
Get the mouse event information.
Definition gui_event.hpp:264
widget_id id
The numeric identifier of a widget.
Definition widget_intf.hpp:31
widget_layout const & layout() const noexcept
Get the current layout for this widget.
Definition widget_intf.hpp:241
widget_intf * parent() const noexcept
Pointer to the parent widget.
Definition widget_intf.hpp:113
The layout of a widget.
Definition widget_layout.hpp:56
constexpr widget_layout transform(box_shape const &child_shape, transform_command command, aarectangle new_clipping_rectangle) const noexcept
Create a new widget_layout for the child widget.
Definition widget_layout.hpp:236
2D constraints.
Definition box_constraints.hpp:25
Definition box_shape.hpp:18
A observer pointing to the whole or part of a observed_base.
Definition observer_intf.hpp:32
callback< void(value_type)> subscribe(Func &&func, callback_flags flags=callback_flags::synchronous) noexcept
Subscribe a callback to this observer.
Definition observer_intf.hpp:458
A scroll aperture widget.
Definition scroll_aperture_widget.hpp:26
observer< extent2 > minimum
The minimum size this widget is allowed to be.
Definition widget.hpp:42
void scroll_to_show() noexcept
Scroll to show the important part of the widget.
Definition widget_intf.hpp:347
void set_layout(widget_layout const &context) noexcept override
Update the internal layout of the widget.
Definition widget.hpp:116
generator< widget_intf & > children(bool include_invisible) noexcept override
Get a list of child widgets.
Definition widget.hpp:61
void draw(draw_context const &context) noexcept override
Draw the widget.
Definition widget.hpp:121
widget() noexcept
Constructor for creating sub views.
Definition widget.hpp:50
hitbox hitbox_test(point2 position) const noexcept override
Find the widget that is under the mouse cursor.
Definition widget.hpp:73
box_constraints update_constraints() noexcept override
Update the constraints of the widget.
Definition widget.hpp:110
bool process_event(gui_event const &event) const noexcept override
Send a event to the window.
Definition widget.hpp:125
bool handle_event(gui_event const &event) noexcept override
Handle command.
Definition widget.hpp:145
observer< extent2 > maximum
The maximum size this widget is allowed to be.
Definition widget.hpp:46
T forward(T... args)
T max(T... args)
T move(T... args)
T round(T... args)