HikoGUI
A low latency retained GUI
Loading...
Searching...
No Matches
scroll_aperture_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
9namespace hi::inline v1 {
10
12public:
13 using super = widget;
14
15 observable<float> content_width;
16 observable<float> content_height;
17 observable<float> aperture_width;
18 observable<float> aperture_height;
19 observable<float> offset_x;
20 observable<float> offset_y;
21
22 scroll_aperture_widget(gui_window& window, widget *parent) noexcept : super(window, parent)
23 {
24 hi_axiom(is_gui_thread());
25 hi_axiom(parent);
26
27 // The aperture-widget will not draw itself, only its selected content.
28 semantic_layer = parent->semantic_layer;
29
30 // clang-format off
31 _content_width_cbt = content_width.subscribe([&](auto...){ request_relayout(); });
32 _content_height_cbt = content_height.subscribe([&](auto...){ request_relayout(); });
33 _aperture_width_cbt = aperture_width.subscribe([&](auto...){ request_relayout(); });
34 _aperture_height_cbt = aperture_height.subscribe([&](auto...){ request_relayout(); });
35 _offset_x_cbt = offset_x.subscribe([&](auto...){ request_relayout(); });
36 _offset_y_cbt = offset_y.subscribe([&](auto...){ request_relayout(); });
37 // clang-format off
38 }
39
40 template<typename Widget, typename... Args>
41 Widget &make_widget(Args &&...args) noexcept
42 {
43 hi_axiom(is_gui_thread());
44 hi_axiom(not _content);
45
46 auto tmp = std::make_unique<Widget>(window, this, std::forward<Args>(args)...);
47 auto &ref = *tmp;
48 _content = std::move(tmp);
49 return ref;
50 }
51
52 [[nodiscard]] bool x_axis_scrolls() const noexcept
53 {
54 return *content_width > *aperture_width;
55 }
56
57 [[nodiscard]] bool y_axis_scrolls() const noexcept
58 {
59 return *content_height > *aperture_height;
60 }
61
63 [[nodiscard]] generator<widget *> children() const noexcept override
64 {
65 co_yield _content.get();
66 }
67
68 widget_constraints const &set_constraints() noexcept override
69 {
70 _layout = {};
71
72 hi_axiom(_content);
73 hilet content_constraints = _content->set_constraints();
74
75 hilet minimum_size = extent2{
76 content_constraints.margins.left() + content_constraints.minimum.width() + content_constraints.margins.right(),
77 content_constraints.margins.top() + content_constraints.minimum.height() + content_constraints.margins.bottom()};
78 hilet preferred_size = extent2{
79 content_constraints.margins.left() + content_constraints.preferred.width() + content_constraints.margins.right(),
80 content_constraints.margins.top() + content_constraints.preferred.height() + content_constraints.margins.bottom()};
81 hilet maximum_size = extent2{
82 content_constraints.margins.left() + content_constraints.maximum.width() + content_constraints.margins.right(),
83 content_constraints.margins.top() + content_constraints.maximum.height() + content_constraints.margins.bottom()};
84
85 return _constraints = {minimum_size, preferred_size, maximum_size, margins{}};
86 }
87
88 void set_layout(widget_layout const &layout) noexcept override
89 {
90 hilet content_constraints = _content->constraints();
91 hilet margins = content_constraints.margins;
92
93 if (compare_store(_layout, layout)) {
94 hilet preferred_size = content_constraints.preferred;
95
96 aperture_width = layout.width() - margins.left() - margins.right();
97 aperture_height = layout.height() - margins.bottom() - margins.top();
98
99 // Start scrolling with the preferred size as minimum, so
100 // that widgets in the content don't get unnecessarily squeezed.
101 content_width = *aperture_width < preferred_size.width() ? preferred_size.width() : *aperture_width;
102 content_height = *aperture_height < preferred_size.height() ? preferred_size.height() : *aperture_height;
103 }
104
105 // Make sure the offsets are limited to the scrollable area.
106 hilet offset_x_max = std::max(*content_width - *aperture_width, 0.0f);
107 hilet offset_y_max = std::max(*content_height - *aperture_height, 0.0f);
108 offset_x = std::clamp(std::round(*offset_x), 0.0f, offset_x_max);
109 offset_y = std::clamp(std::round(*offset_y), 0.0f, offset_y_max);
110
111 // The position of the content rectangle relative to the scroll view.
112 // The size is further adjusted if the either the horizontal or vertical scroll bar is invisible.
113 _content_rectangle = {
114 -*offset_x + margins.left(), -*offset_y + margins.bottom(), *content_width, *content_height};
115
116 // The content needs to be at a higher elevation, so that hitbox check
117 // will work correctly for handling scrolling with mouse wheel.
118 _content->set_layout(layout.transform(_content_rectangle, 1.0f, layout.rectangle()));
119 }
120
121 void draw(draw_context const &context) noexcept
122 {
123 if (*mode > widget_mode::invisible) {
124 _content->draw(context);
125 }
126 }
127
128 [[nodiscard]] hitbox hitbox_test(point3 position) const noexcept override
129 {
130 hi_axiom(is_gui_thread());
131
132 if (*mode >= widget_mode::partial) {
133 auto r = _content->hitbox_test_from_parent(position);
134
135 if (layout().contains(position)) {
136 r = std::max(r, hitbox{this, position});
137 }
138 return r;
139
140 } else {
141 return {};
142 }
143 }
144
145 bool handle_event(gui_event const &event) noexcept override
146 {
147 hi_axiom(is_gui_thread());
148
149 if (event == gui_event_type::mouse_wheel) {
150 hilet new_offset_x = *offset_x + event.mouse().wheel_delta.x() * theme().scale;
151 hilet new_offset_y = *offset_y + event.mouse().wheel_delta.y() * theme().scale;
152 hilet max_offset_x = std::max(0.0f, *content_width - *aperture_width);
153 hilet max_offset_y = std::max(0.0f, *content_height - *aperture_height);
154
155 offset_x = std::clamp(new_offset_x, 0.0f, max_offset_x);
156 offset_y = std::clamp(new_offset_y, 0.0f, max_offset_y);
157 request_relayout();
158 return true;
159 } else {
160 return super::handle_event(event);
161 }
162 }
163
164 void scroll_to_show(hi::aarectangle to_show) noexcept override
165 {
166 auto safe_rectangle = intersect(_layout.rectangle(), _layout.clipping_rectangle);
167 float delta_x = 0.0f;
168 float delta_y = 0.0f;
169
170 if (safe_rectangle.width() > theme().margin and safe_rectangle.height() > theme().margin) {
171 // This will look visually better, if the selected widget is moved with some margin from
172 // the edge of the scroll widget. The margins of the content do not have anything to do
173 // with the margins that are needed here.
174 safe_rectangle = safe_rectangle - theme().margin;
175
176 if (to_show.right() > safe_rectangle.right()) {
177 delta_x = to_show.right() - safe_rectangle.right();
178 } else if (to_show.left() < safe_rectangle.left()) {
179 delta_x = to_show.left() - safe_rectangle.left();
180 }
181
182 if (to_show.top() > safe_rectangle.top()) {
183 delta_y = to_show.top() - safe_rectangle.top();
184 } else if (to_show.bottom() < safe_rectangle.bottom()) {
185 delta_y = to_show.bottom() - safe_rectangle.bottom();
186 }
187
188 // Scroll the widget
189 offset_x += delta_x;
190 offset_y += delta_y;
191 }
192
193 // There may be recursive scroll view, and they all need to move until the rectangle is visible.
194 if (parent) {
195 parent->scroll_to_show(bounding_rectangle(_layout.to_parent * translate2(delta_x, delta_y) * to_show));
196 }
197 }
199private:
200 aarectangle _content_rectangle;
202 decltype(content_width)::token_type _content_width_cbt;
203 decltype(content_height)::token_type _content_height_cbt;
204 decltype(aperture_width)::token_type _aperture_width_cbt;
205 decltype(aperture_height)::token_type _aperture_height_cbt;
206 decltype(offset_x)::token_type _offset_x_cbt;
207 decltype(offset_y)::token_type _offset_y_cbt;
208};
209
210} // namespace hi::inline v1
#define hilet
Invariant should be the default for variables.
Definition required.hpp:23
A return value for a generator-function.
Definition generator.hpp:28
Class which represents an axis-aligned rectangle.
Definition axis_aligned_rectangle.hpp:20
constexpr float & width() noexcept
Access the x-as-width element from the extent.
Definition extent.hpp:140
Definition margins.hpp:11
Definition translate.hpp:15
Draw context for drawing using the HikoGUI shaders.
Definition draw_context.hpp:52
A user interface event.
Definition gui_event.hpp:58
Definition gui_window.hpp:39
Definition hitbox.hpp:16
Definition theme.hpp:21
float scale
The scale factor used to convert pt to physical pixel size.
Definition theme.hpp:31
float margin
Distance between widgets and between widgets and the border of the container.
Definition theme.hpp:35
An observable value.
Definition observable.hpp:359
Definition scroll_aperture_widget.hpp:11
An interactive graphical object as part of the user-interface.
Definition widget.hpp:39
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:81
Definition widget_constraints.hpp:13
Definition widget_layout.hpp:18
constexpr widget_layout transform(aarectangle const &child_rectangle, float elevation, aarectangle new_clipping_rectangle, widget_baseline new_baseline=widget_baseline{}) const noexcept
Create a new widget_layout for the child widget.
Definition widget_layout.hpp:167
T max(T... args)
T move(T... args)
T round(T... args)