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
80 template<typename Widget, typename... Args>
81 Widget &make_widget(Args &&...args) noexcept
82 {
83 tt_axiom(is_gui_thread());
84 tt_axiom(not _content);
85
86 auto &widget = super::make_widget<Widget>(std::forward<Args>(args)...);
87 _content = &widget;
88 return widget;
89 }
90
92 void init() noexcept override
93 {
95
96 _horizontal_scroll_bar =
97 &super::make_widget<horizontal_scroll_bar_widget>(_scroll_content_width, _scroll_aperture_width, _scroll_offset_x);
98 _vertical_scroll_bar =
99 &super::make_widget<vertical_scroll_bar_widget>(_scroll_content_height, _scroll_aperture_height, _scroll_offset_y);
100
101 if (auto delegate = _delegate.lock()) {
102 delegate->init(*this);
103 }
104 }
105
106 void deinit() noexcept override
107 {
108 if (auto delegate = _delegate.lock()) {
109 delegate->deinit(*this);
110 }
112 }
113
114 [[nodiscard]] float margin() const noexcept override
115 {
116 return 0.0f;
117 }
118
119 [[nodiscard]] bool constrain(hires_utc_clock::time_point display_time_point, bool need_reconstrain) noexcept override
120 {
121 tt_axiom(is_gui_thread());
122 tt_axiom(_content);
123
124 auto has_updated_contraints = super::constrain(display_time_point, need_reconstrain);
125
126 // Recurse into the selected widget.
127 if (has_updated_contraints) {
128 // As the widget will always add scrollbars if needed the minimum size is dictated to
129 // the size of the scrollbars.
130 _minimum_size = _content->minimum_size();
131 _preferred_size = _content->preferred_size();
132 _maximum_size = _content->maximum_size();
133
134 // When there are scrollbars the minimum size is the minimum length of the scrollbar.
135 // The maximum size is the minimum size of the content.
136 if constexpr (any(axis & axis::horizontal)) {
137 // The content could be smaller than the scrollbar.
138 _minimum_size.width() = _horizontal_scroll_bar->minimum_size().width();
139 _preferred_size.width() = std::max(_preferred_size.width(), _horizontal_scroll_bar->minimum_size().width());
140 _maximum_size.width() = std::max(_maximum_size.width(), _horizontal_scroll_bar->minimum_size().width());
141 }
142 if constexpr (any(axis & axis::vertical)) {
143 _minimum_size.height() = _vertical_scroll_bar->minimum_size().height();
144 _preferred_size.height() = std::max(_preferred_size.height(), _vertical_scroll_bar->minimum_size().height());
145 _maximum_size.height() = std::max(_maximum_size.height(), _vertical_scroll_bar->minimum_size().height());
146 }
147
148 // Make room for the scroll bars.
149 if constexpr (any(axis & axis::horizontal)) {
150 _minimum_size.height() += _horizontal_scroll_bar->preferred_size().height();
151 _preferred_size.height() += _horizontal_scroll_bar->preferred_size().height();
152 _maximum_size.height() += _horizontal_scroll_bar->preferred_size().height();
153 }
154 if constexpr (any(axis & axis::vertical)) {
155 _minimum_size.width() += _vertical_scroll_bar->preferred_size().width();
156 _preferred_size.width() += _vertical_scroll_bar->preferred_size().width();
157 _maximum_size.width() += _vertical_scroll_bar->preferred_size().width();
158 }
159 }
160 tt_axiom(_minimum_size <= _preferred_size && _preferred_size <= _maximum_size);
161 return has_updated_contraints;
162 }
163
164 [[nodiscard]] void layout(hires_utc_clock::time_point display_time_point, bool need_layout) noexcept override
165 {
166 tt_axiom(is_gui_thread());
167 tt_axiom(_content);
168
169 need_layout |= _request_layout.exchange(false);
170 if (need_layout) {
171 ttlet vertical_scroll_bar_width = _vertical_scroll_bar->preferred_size().width();
172 ttlet horizontal_scroll_bar_height = _horizontal_scroll_bar->preferred_size().height();
173
174 std::tie(_horizontal_scroll_bar->visible, _vertical_scroll_bar->visible) = needed_scrollbars();
175
176 ttlet height_adjustment = _horizontal_scroll_bar->visible ? horizontal_scroll_bar_height : 0.0f;
177 ttlet width_adjustment = _vertical_scroll_bar->visible ? vertical_scroll_bar_width : 0.0f;
178
179 ttlet vertical_scroll_bar_rectangle = aarectangle{
180 width() - vertical_scroll_bar_width, height_adjustment, vertical_scroll_bar_width, height() - height_adjustment};
181
182 ttlet horizontal_scroll_bar_rectangle =
183 aarectangle{0.0f, 0.0f, width() - width_adjustment, horizontal_scroll_bar_height};
184
185 _vertical_scroll_bar->set_layout_parameters_from_parent(vertical_scroll_bar_rectangle);
186 _horizontal_scroll_bar->set_layout_parameters_from_parent(horizontal_scroll_bar_rectangle);
187
188 _aperture_rectangle = aarectangle{0.0f, height_adjustment, width() - width_adjustment, height() - height_adjustment};
189
190 // We use the preferred size of the content for determining what to scroll.
191 // This means it is possible for the scroll_content_width or scroll_content_height to be smaller
192 // than the aperture.
193 _scroll_content_width = _content->preferred_size().width();
194 _scroll_content_height = _content->preferred_size().height();
195 _scroll_aperture_width = _aperture_rectangle.width();
196 _scroll_aperture_height = _aperture_rectangle.height();
197
198 ttlet scroll_offset_x_max = std::max(_scroll_content_width - _scroll_aperture_width, 0.0f);
199 ttlet scroll_offset_y_max = std::max(_scroll_content_height - _scroll_aperture_height, 0.0f);
200
201 _scroll_offset_x = std::clamp(std::round(*_scroll_offset_x), 0.0f, scroll_offset_x_max);
202 _scroll_offset_y = std::clamp(std::round(*_scroll_offset_y), 0.0f, scroll_offset_y_max);
203
204 // Its size scroll content size, or the size of the aperture whichever is bigger.
205 ttlet content_size = extent2{
206 std::max(*_scroll_content_width, _aperture_rectangle.width()),
207 std::max(*_scroll_content_height, _aperture_rectangle.height())};
208
209 // The position of the content rectangle relative to the scroll view.
210 // The size is further adjusted if the either the horizontal or vertical scroll bar is invisible.
211 ttlet content_rectangle = aarectangle{
212 -_scroll_offset_x, -_scroll_offset_y - height_adjustment, content_size.width(), content_size.height()};
213
214 // Make a clipping rectangle that fits the aperture_rectangle exactly.
215 _content->set_layout_parameters_from_parent(
216 content_rectangle, _aperture_rectangle, _content->draw_layer - draw_layer);
217
218 if constexpr (controls_window) {
219 window.set_resize_border_priority(
220 true, not _vertical_scroll_bar->visible, not _horizontal_scroll_bar->visible, true);
221 }
222 }
223
224 super::layout(display_time_point, need_layout);
225 }
226
227 [[nodiscard]] hitbox hitbox_test(point2 position) const noexcept override
228 {
229 tt_axiom(is_gui_thread());
230 tt_axiom(_content);
231
232 auto r = super::hitbox_test(position);
233
234 if (_visible_rectangle.contains(position)) {
235 // Claim mouse events for scrolling.
236 r = std::max(r, hitbox{this, draw_layer});
237 }
238
239 return r;
240 }
241
242 bool handle_event(mouse_event const &event) noexcept override
243 {
244 tt_axiom(is_gui_thread());
245 auto handled = super::handle_event(event);
246
247 if (event.type == mouse_event::Type::Wheel) {
248 handled = true;
249 _scroll_offset_x += event.wheelDelta.x();
250 _scroll_offset_y += event.wheelDelta.y();
251 _request_layout = true;
252 return true;
253 }
254 return handled;
255 }
256
257 void scroll_to_show(tt::rectangle rectangle) noexcept override
258 {
259 auto rectangle_ = aarectangle{rectangle};
260
261 float delta_x = 0.0f;
262 if (rectangle_.right() > _aperture_rectangle.right()) {
263 delta_x = rectangle_.right() - _aperture_rectangle.right();
264 } else if (rectangle_.left() < _aperture_rectangle.left()) {
265 delta_x = rectangle_.left() - _aperture_rectangle.left();
266 }
267
268 float delta_y = 0.0f;
269 if (rectangle_.top() > _aperture_rectangle.top()) {
270 delta_y = rectangle_.top() - _aperture_rectangle.top();
271 } else if (rectangle_.bottom() < _aperture_rectangle.bottom()) {
272 delta_y = rectangle_.bottom() - _aperture_rectangle.bottom();
273 }
274
275 _scroll_offset_x += delta_x;
276 _scroll_offset_y += delta_y;
277
278 // There may be recursive scroll view, and they all need to move until the rectangle is visible.
279 if (parent) {
280 parent->scroll_to_show(_local_to_parent * translate2(delta_x, delta_y) * rectangle);
281 }
282 }
283 // @endprivatesection
284private:
286 widget *_content = nullptr;
287 horizontal_scroll_bar_widget *_horizontal_scroll_bar = nullptr;
288 vertical_scroll_bar_widget *_vertical_scroll_bar = nullptr;
289
290 observable<float> _scroll_content_width;
291 observable<float> _scroll_content_height;
292 observable<float> _scroll_aperture_width;
293 observable<float> _scroll_aperture_height;
294 observable<float> _scroll_offset_x;
295 observable<float> _scroll_offset_y;
296
297 aarectangle _aperture_rectangle;
298
302 [[nodiscard]] std::pair<bool, bool> needed_scrollbars() const noexcept
303 {
304 ttlet content_size = _content->preferred_size();
305
306 if (content_size <= size()) {
307 return {false, false};
308 } else if (content_size.width() - _vertical_scroll_bar->preferred_size().width() <= width()) {
309 return {false, true};
310 } else if (content_size.height() - _horizontal_scroll_bar->preferred_size().height() <= height()) {
311 return {true, false};
312 } else {
313 return {true, true};
314 }
315 }
316};
317
318template<bool ControlsWindow = false>
319using vertical_scroll_widget = scroll_widget<axis::vertical, ControlsWindow>;
320
321template<bool ControlsWindow = false>
322using horizontal_scroll_widget = scroll_widget<axis::horizontal, ControlsWindow>;
323
324} // namespace tt
STL namespace.
constexpr float & width() noexcept
Access the x-as-width element from the extent.
Definition extent.hpp:131
constexpr float & height() noexcept
Access the y-as-height element from the extent.
Definition extent.hpp:142
Class which represents an rectangle.
Definition rectangle.hpp:16
Definition gui_window.hpp:36
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:81
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:39
widget *const parent
Pointer to the parent widget.
Definition widget.hpp:48
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:92
observable< bool > visible
The widget is visible.
Definition widget.hpp:62
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:43
virtual bool constrain(hires_utc_clock::time_point display_time_point, bool need_reconstrain) noexcept
Update the constraints 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:76
virtual void deinit() noexcept
Should be called right after allocating and constructing a widget.
virtual void layout(hires_utc_clock::time_point display_time_point, bool need_layout) noexcept
Update the internal layout of the widget.
T max(T... args)
T round(T... args)
T tie(T... args)