HikoGUI
A low latency retained GUI
Loading...
Searching...
No Matches
scroll_widget.hpp
1// Copyright Take Vos 2021.
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.hpp"
8#include "scroll_bar_widget.hpp"
9#include "scroll_delegate.hpp"
10#include "../GUI/gui_window.hpp"
11#include "../geometry/axis.hpp"
12
13namespace tt {
14
42template<axis Axis = axis::both, bool ControlsWindow = false>
43class scroll_widget final : public widget {
44public:
45 static_assert(Axis == axis::horizontal or Axis == axis::vertical or Axis == axis::both);
46
47 using super = widget;
49
50 static constexpr tt::axis axis = Axis;
51 static constexpr bool controls_window = ControlsWindow;
52
61 super(window, parent), _delegate(std::move(delegate))
62 {
63 tt_axiom(is_gui_thread());
64
65 if (parent) {
66 // The tab-widget will not draw itself, only its selected content.
68 }
69
70 _layout_callback = std::make_shared<std::function<void()>>([this]() {
71 _request_layout = true;
72 });
73
74 _scroll_content_width.subscribe(_layout_callback);
75 _scroll_content_height.subscribe(_layout_callback);
76 _scroll_aperture_width.subscribe(_layout_callback);
77 _scroll_aperture_height.subscribe(_layout_callback);
78 _scroll_offset_x.subscribe(_layout_callback);
79 _scroll_offset_y.subscribe(_layout_callback);
80 }
81
91 template<typename Widget, typename... Args>
92 Widget &make_widget(Args &&...args) noexcept
93 {
94 tt_axiom(is_gui_thread());
95 tt_axiom(not _content);
96
97 auto &widget = super::make_widget<Widget>(std::forward<Args>(args)...);
98 _content = &widget;
99 return widget;
100 }
101
103 void init() noexcept override
104 {
105 super::init();
106
107 _horizontal_scroll_bar =
108 &super::make_widget<horizontal_scroll_bar_widget>(_scroll_content_width, _scroll_aperture_width, _scroll_offset_x);
109 _vertical_scroll_bar =
110 &super::make_widget<vertical_scroll_bar_widget>(_scroll_content_height, _scroll_aperture_height, _scroll_offset_y);
111
112 if (auto delegate = _delegate.lock()) {
113 delegate->init(*this);
114 }
115 }
116
117 void deinit() noexcept override
118 {
119 if (auto delegate = _delegate.lock()) {
120 delegate->deinit(*this);
121 }
123 }
124
125 [[nodiscard]] float margin() const noexcept override
126 {
127 return 0.0f;
128 }
129
130 [[nodiscard]] bool constrain(utc_nanoseconds display_time_point, bool need_reconstrain) noexcept override
131 {
132 tt_axiom(is_gui_thread());
133 tt_axiom(_content);
134
135 auto has_updated_contraints = super::constrain(display_time_point, need_reconstrain);
136
137 // Recurse into the selected widget.
138 if (has_updated_contraints) {
139 // As the widget will always add scrollbars if needed the minimum size is dictated to
140 // the size of the scrollbars.
141 _minimum_size = _content->minimum_size();
142 _preferred_size = _content->preferred_size();
143 _maximum_size = _content->maximum_size();
144
145 // When there are scrollbars the minimum size is the minimum length of the scrollbar.
146 // The maximum size is the minimum size of the content.
147 if constexpr (any(axis & axis::horizontal)) {
148 // The content could be smaller than the scrollbar.
149 _minimum_size.width() = _horizontal_scroll_bar->minimum_size().width();
150 _preferred_size.width() = std::max(_preferred_size.width(), _horizontal_scroll_bar->minimum_size().width());
151 _maximum_size.width() = std::max(_maximum_size.width(), _horizontal_scroll_bar->minimum_size().width());
152 }
153 if constexpr (any(axis & axis::vertical)) {
154 _minimum_size.height() = _vertical_scroll_bar->minimum_size().height();
155 _preferred_size.height() = std::max(_preferred_size.height(), _vertical_scroll_bar->minimum_size().height());
156 _maximum_size.height() = std::max(_maximum_size.height(), _vertical_scroll_bar->minimum_size().height());
157 }
158
159 // Make room for the scroll bars.
160 if constexpr (any(axis & axis::horizontal)) {
161 _minimum_size.height() += _horizontal_scroll_bar->preferred_size().height();
162 _preferred_size.height() += _horizontal_scroll_bar->preferred_size().height();
163 _maximum_size.height() += _horizontal_scroll_bar->preferred_size().height();
164 }
165 if constexpr (any(axis & axis::vertical)) {
166 _minimum_size.width() += _vertical_scroll_bar->preferred_size().width();
167 _preferred_size.width() += _vertical_scroll_bar->preferred_size().width();
168 _maximum_size.width() += _vertical_scroll_bar->preferred_size().width();
169 }
170 }
171 tt_axiom(_minimum_size <= _preferred_size && _preferred_size <= _maximum_size);
172 return has_updated_contraints;
173 }
174
175 [[nodiscard]] void layout(utc_nanoseconds display_time_point, bool need_layout) noexcept override
176 {
177 tt_axiom(is_gui_thread());
178 tt_axiom(_content);
179
180 need_layout |= _request_layout.exchange(false);
181 if (need_layout) {
182 ttlet vertical_scroll_bar_width = _vertical_scroll_bar->preferred_size().width();
183 ttlet horizontal_scroll_bar_height = _horizontal_scroll_bar->preferred_size().height();
184
185 std::tie(_horizontal_scroll_bar->visible, _vertical_scroll_bar->visible) = needed_scrollbars();
186
187 ttlet height_adjustment = _horizontal_scroll_bar->visible ? horizontal_scroll_bar_height : 0.0f;
188 ttlet width_adjustment = _vertical_scroll_bar->visible ? vertical_scroll_bar_width : 0.0f;
189
190 ttlet vertical_scroll_bar_rectangle = aarectangle{
191 width() - vertical_scroll_bar_width, height_adjustment, vertical_scroll_bar_width, height() - height_adjustment};
192
193 ttlet horizontal_scroll_bar_rectangle =
194 aarectangle{0.0f, 0.0f, width() - width_adjustment, horizontal_scroll_bar_height};
195
196 _vertical_scroll_bar->set_layout_parameters_from_parent(vertical_scroll_bar_rectangle);
197 _horizontal_scroll_bar->set_layout_parameters_from_parent(horizontal_scroll_bar_rectangle);
198
199 _aperture_rectangle = aarectangle{0.0f, height_adjustment, width() - width_adjustment, height() - height_adjustment};
200
201 // We use the preferred size of the content for determining what to scroll.
202 // This means it is possible for the scroll_content_width or scroll_content_height to be smaller
203 // than the aperture.
204 _scroll_content_width = _content->preferred_size().width();
205 _scroll_content_height = _content->preferred_size().height();
206 _scroll_aperture_width = _aperture_rectangle.width();
207 _scroll_aperture_height = _aperture_rectangle.height();
208
209 ttlet scroll_offset_x_max = std::max(_scroll_content_width - _scroll_aperture_width, 0.0f);
210 ttlet scroll_offset_y_max = std::max(_scroll_content_height - _scroll_aperture_height, 0.0f);
211
212 _scroll_offset_x = std::clamp(std::round(*_scroll_offset_x), 0.0f, scroll_offset_x_max);
213 _scroll_offset_y = std::clamp(std::round(*_scroll_offset_y), 0.0f, scroll_offset_y_max);
214
215 // Its size scroll content size, or the size of the aperture whichever is bigger.
216 ttlet content_size = extent2{
217 std::max(*_scroll_content_width, _aperture_rectangle.width()),
218 std::max(*_scroll_content_height, _aperture_rectangle.height())};
219
220 // The position of the content rectangle relative to the scroll view.
221 // The size is further adjusted if the either the horizontal or vertical scroll bar is invisible.
222 ttlet content_rectangle = aarectangle{
223 -_scroll_offset_x, -_scroll_offset_y - height_adjustment, content_size.width(), content_size.height()};
224
225 // Make a clipping rectangle that fits the aperture_rectangle exactly.
226 _content->set_layout_parameters_from_parent(
227 content_rectangle, _aperture_rectangle, _content->draw_layer - draw_layer);
228
229 if constexpr (controls_window) {
230 window.set_resize_border_priority(
231 true, not _vertical_scroll_bar->visible, not _horizontal_scroll_bar->visible, true);
232 }
233 }
234
235 super::layout(display_time_point, need_layout);
236 }
237
238 [[nodiscard]] hitbox hitbox_test(point2 position) const noexcept override
239 {
240 tt_axiom(is_gui_thread());
241 tt_axiom(_content);
242
243 auto r = super::hitbox_test(position);
244
245 if (_visible_rectangle.contains(position)) {
246 // Claim mouse events for scrolling.
247 r = std::max(r, hitbox{this, draw_layer});
248 }
249
250 return r;
251 }
252
253 bool handle_event(mouse_event const &event) noexcept override
254 {
255 tt_axiom(is_gui_thread());
256 auto handled = super::handle_event(event);
257
258 if (event.type == mouse_event::Type::Wheel) {
259 handled = true;
260 _scroll_offset_x += event.wheelDelta.x();
261 _scroll_offset_y += event.wheelDelta.y();
262 _request_layout = true;
263 return true;
264 }
265 return handled;
266 }
267
268 void scroll_to_show(tt::rectangle rectangle) noexcept override
269 {
270 auto rectangle_ = aarectangle{rectangle};
271
272 float delta_x = 0.0f;
273 if (rectangle_.right() > _aperture_rectangle.right()) {
274 delta_x = rectangle_.right() - _aperture_rectangle.right();
275 } else if (rectangle_.left() < _aperture_rectangle.left()) {
276 delta_x = rectangle_.left() - _aperture_rectangle.left();
277 }
278
279 float delta_y = 0.0f;
280 if (rectangle_.top() > _aperture_rectangle.top()) {
281 delta_y = rectangle_.top() - _aperture_rectangle.top();
282 } else if (rectangle_.bottom() < _aperture_rectangle.bottom()) {
283 delta_y = rectangle_.bottom() - _aperture_rectangle.bottom();
284 }
285
286 _scroll_offset_x += delta_x;
287 _scroll_offset_y += delta_y;
288
289 // There may be recursive scroll view, and they all need to move until the rectangle is visible.
290 if (parent) {
291 parent->scroll_to_show(_local_to_parent * translate2(delta_x, delta_y) * rectangle);
292 }
293 }
294 // @endprivatesection
295private:
297 widget *_content = nullptr;
298 horizontal_scroll_bar_widget *_horizontal_scroll_bar = nullptr;
299 vertical_scroll_bar_widget *_vertical_scroll_bar = nullptr;
300
301 observable<float> _scroll_content_width;
302 observable<float> _scroll_content_height;
303 observable<float> _scroll_aperture_width;
304 observable<float> _scroll_aperture_height;
305 observable<float> _scroll_offset_x;
306 observable<float> _scroll_offset_y;
307 observable<float>::callback_ptr_type _layout_callback;
308
309 aarectangle _aperture_rectangle;
310
314 [[nodiscard]] std::pair<bool, bool> needed_scrollbars() const noexcept
315 {
316 ttlet content_size = _content->preferred_size();
317
318 if (content_size <= size()) {
319 return {false, false};
320 } else if (content_size.width() - _vertical_scroll_bar->preferred_size().width() <= width()) {
321 return {false, true};
322 } else if (content_size.height() - _horizontal_scroll_bar->preferred_size().height() <= height()) {
323 return {true, false};
324 } else {
325 return {true, true};
326 }
327 }
328};
329
330template<bool ControlsWindow = false>
331using vertical_scroll_widget = scroll_widget<axis::vertical, ControlsWindow>;
332
333template<bool ControlsWindow = false>
334using horizontal_scroll_widget = scroll_widget<axis::horizontal, ControlsWindow>;
335
336} // namespace tt
STL namespace.
constexpr float & width() noexcept
Access the x-as-width element from the extent.
Definition extent.hpp:142
constexpr float & height() noexcept
Access the y-as-height element from the extent.
Definition extent.hpp:153
Class which represents an rectangle.
Definition rectangle.hpp:16
Definition gui_window.hpp:39
The scroll widget allows a content widget to be shown in less space than is required.
Definition scroll_widget.hpp:43
Widget & make_widget(Args &&...args) noexcept
Add a content widget directly to this scroll widget.
Definition scroll_widget.hpp:92
scroll_widget(gui_window &window, widget *parent, std::weak_ptr< delegate_type > delegate={}) noexcept
Constructs an empty scroll widget.
Definition scroll_widget.hpp:60
Definition scroll_delegate.hpp:16
An interactive graphical object as part of the user-interface.
Definition widget.hpp:37
widget *const parent
Pointer to the parent widget.
Definition widget.hpp:46
virtual bool constrain(utc_nanoseconds display_time_point, bool need_reconstrain) noexcept
Update the constraints of the widget.
virtual void init() noexcept
Should be called right after allocating and constructing a widget.
int semantic_layer
The draw layer of the widget.
Definition widget.hpp:90
observable< bool > visible
The widget is visible.
Definition widget.hpp:60
aarectangle rectangle() const noexcept
Get the rectangle in local coordinates.
extent2 maximum_size() const noexcept
Maximum size.
virtual bool handle_event(command command) noexcept
Handle command.
gui_window & window
Convenient reference to the Window.
Definition widget.hpp:41
virtual void layout(utc_nanoseconds display_time_point, bool need_layout) noexcept
Update the internal layout of the widget.
extent2 minimum_size() const noexcept
Minimum size.
extent2 preferred_size() const noexcept
Preferred size.
virtual hitbox hitbox_test(point2 position) const noexcept
Find the widget that is under the mouse cursor.
widget(gui_window &window, widget *parent) noexcept
virtual void scroll_to_show(tt::rectangle rectangle) noexcept
Scroll to show the given rectangle on the window.
float draw_layer
The draw layer of the widget.
Definition widget.hpp:74
virtual void deinit() noexcept
Should be called right after allocating and constructing a widget.
T make_shared(T... args)
T max(T... args)
T round(T... args)
T tie(T... args)