Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-core/src/elements/new_scrollable/util.rs
1use pathfinder_geometry::{
2 rect::RectF,
3 vector::{vec2f, Vector2F},
4};
5 
6use crate::{
7 elements::{
8 project_scroll_delta_by_sensitivity, Axis, ClippedScrollStateHandle, RectFExt as _,
9 ScrollToPositionMode,
10 },
11 units::Pixels,
12 EventContext, SizeConstraint,
13};
14 
15/// Calculate the child size constraint for a given axis.
16/// For a clipped element, lay it out unbounded on the main axis but apply constraint on the cross axis.
17/// For a manual element, lay it out bounded with the incoming size constraint. Note that we need to
18/// subtract the total scrollbar offset to take into account the spacing it takes in the viewport.
19pub(super) fn child_constraint_for_axis(
20 axis: Axis,
21 constraint: SizeConstraint,
22 is_clipped: bool,
23 scrollbar_size_with_padding: Vector2F,
24) -> SizeConstraint {
25 let incoming_constraint = if is_clipped {
26 match axis {
27 Axis::Horizontal => SizeConstraint {
28 min: vec2f(0.0, constraint.min.y()),
29 max: vec2f(f32::INFINITY, constraint.max.y()),
30 },
31 Axis::Vertical => SizeConstraint {
32 min: vec2f(constraint.min.x(), 0.),
33 max: vec2f(constraint.max.x(), f32::INFINITY),
34 },
35 }
36 } else {
37 constraint
38 };
39 
40 SizeConstraint {
41 min: (incoming_constraint.min - scrollbar_size_with_padding).max(Vector2F::zero()),
42 max: (incoming_constraint.max - scrollbar_size_with_padding).max(Vector2F::zero()),
43 }
44}
45 
46/// Update the ClippedScrollStateHandle to match scrolling with the given delta.
47pub(super) fn scroll_clipped_scrollable_handle_with_delta(
48 handle: &ClippedScrollStateHandle,
49 child_size: Pixels,
50 viewport_size: Pixels,
51 delta: Pixels,
52 ctx: &mut EventContext,
53) {
54 let scroll_start = handle.scroll_start();
55 
56 if child_size > viewport_size {
57 // The max scroll start here is the total child size - viewport size.
58 // ==================
59 // | |
60 // | |
61 // | max_scroll_top |
62 // | |
63 // | |
64 // ==================
65 // | viewport |
66 // ==================
67 let new_scroll_start = (scroll_start - delta)
68 .max(Pixels::zero())
69 .min(child_size - viewport_size);
70 
71 // If the scroll start positions have changed, scroll and re-render.
72 if (scroll_start - new_scroll_start).as_f32().abs() > f32::EPSILON {
73 handle.scroll_to(new_scroll_start);
74 ctx.notify();
75 }
76 }
77}
78 
79/// Adjust scroll delta based on the set sensitivity level:
80/// - If horizontal delta * sensitivity > vertical delta, set vertical delta to zero.
81/// - If vertical delta * sensitivity > horizontal delta, set horizontal delta to zero.
82pub(super) fn adjust_scroll_delta_with_sensitivity_config(
83 delta: Vector2F,
84 sensitivity: f32,
85) -> Vector2F {
86 project_scroll_delta_by_sensitivity(delta, sensitivity)
87}
88 
89// Viewport
90// ┌──────┴───────┐
91// ┌─────┲━━━━━━━━━━━━━━┱────────┐ ┐
92// │ ┃ ┃ │ │
93// │ ┃ ┃ │ │
94// │ ┃ ┃ │ │
95// │ ┃ ┃ ┌──┐ │ │
96// │ ┃ ┃ │**│ │ ├─Viewport
97// │ ┃ ┃ └──┘ │ │
98// │ ┃ ┃ │ │
99// │ ┃ ┃ │ │
100// │ ┃ ┃ │ │
101// │ ┗━━━━━━━━━━━━━━┛ │ ┘
102// │ │
103// │ │
104// │ │
105// └─────────────────────────────┘
106// Viewport
107// ┌──────┴───────┐
108// delta
109// ┌──┴──┐
110// ┌───────────┲━━━━━━━━━━━━━━┱──┐ ┐
111// │ ┃ ┃ │ │
112// │ ┃ ┃ │ │
113// │ ┃ ┃ │ │
114// │ ┃ ┌──┨ │ │
115// │ ┃ │**┃ │ ├─Viewport
116// │ ┃ └──┨ │ │
117// │ ┃ ┃ │ │
118// │ ┃ ┃ │ │
119// │ ┃ ┃ │ │
120// │ ┗━━━━━━━━━━━━━━┛ │ ┘
121// │ │
122// │ │
123// │ │
124// └─────────────────────────────┘
125/// Calculate the scroll delta (in pixels) needed to bring the element delimited by
126/// `position_bounds` into view within `viewport_bounds` on the given axis.
127///
128/// The behaviour depends on `mode`:
129/// - [`ScrollToPositionMode::FullyIntoView`]: scrolls the minimum amount to make the
130/// entire element visible. When the element is larger than the viewport, no scroll
131/// is performed.
132/// - [`ScrollToPositionMode::TopIntoView`]: behaves like `FullyIntoView` when the
133/// element fits in the viewport. When the element is larger, aligns the element's
134/// leading edge with the viewport's leading edge.
135pub(crate) fn scroll_delta_for_axis(
136 axis: Axis,
137 viewport_bounds: RectF,
138 position_bounds: RectF,
139 mode: ScrollToPositionMode,
140) -> f32 {
141 let viewport_max_along_axis = viewport_bounds.max_along(axis);
142 let viewport_min_along_axis = viewport_bounds.min_along(axis);
143 let max_position_along_axis = position_bounds.max_along(axis);
144 let min_position_along_axis = position_bounds.min_along(axis);
145 
146 let viewport_size = viewport_max_along_axis - viewport_min_along_axis;
147 let element_size = max_position_along_axis - min_position_along_axis;
148 
149 if element_size > viewport_size {
150 match mode {
151 ScrollToPositionMode::FullyIntoView => 0.0,
152 ScrollToPositionMode::TopIntoView => min_position_along_axis - viewport_min_along_axis,
153 }
154 } else if max_position_along_axis > viewport_max_along_axis {
155 max_position_along_axis - viewport_max_along_axis
156 } else if min_position_along_axis < viewport_min_along_axis {
157 min_position_along_axis - viewport_min_along_axis
158 } else {
159 0.0
160 }
161}
162 
163#[cfg(test)]
164mod tests {
165 use super::*;
166 
167 #[test]
168 fn test_scroll_delta_for_axis_fully_into_view() {
169 let mode = ScrollToPositionMode::FullyIntoView;
170 assert_eq!(
171 scroll_delta_for_axis(
172 Axis::Horizontal,
173 RectF::new(vec2f(100., 0.), vec2f(250., 250.)),
174 RectF::new(vec2f(400., 50.), vec2f(50., 50.)),
175 mode,
176 ),
177 100.
178 );
179 assert_eq!(
180 scroll_delta_for_axis(
181 Axis::Horizontal,
182 RectF::new(vec2f(200., 0.), vec2f(250., 250.)),
183 RectF::new(vec2f(100., 50.), vec2f(50., 50.)),
184 mode,
185 ),
186 -100.
187 );
188 assert_eq!(
189 scroll_delta_for_axis(
190 Axis::Horizontal,
191 RectF::new(vec2f(100., 0.), vec2f(250., 250.)),
192 RectF::new(vec2f(325., 50.), vec2f(50., 50.)),
193 mode,
194 ),
195 25.
196 );
197 assert_eq!(
198 scroll_delta_for_axis(
199 Axis::Horizontal,
200 RectF::new(vec2f(100., 0.), vec2f(250., 250.)),
201 RectF::new(vec2f(150., 50.), vec2f(50., 50.)),
202 mode,
203 ),
204 0.
205 );
206 assert_eq!(
207 scroll_delta_for_axis(
208 Axis::Horizontal,
209 RectF::new(vec2f(100., 0.), vec2f(250., 250.)),
210 RectF::new(vec2f(50., 50.), vec2f(350., 50.)),
211 mode,
212 ),
213 0.
214 );
215 }
216 
217 #[test]
218 fn test_scroll_delta_for_axis_top_into_view() {
219 let mode = ScrollToPositionMode::TopIntoView;
220 
221 // --- Element LARGER than the viewport ---
222 
223 // Element taller than viewport, below viewport: align top with
224 // viewport top.
225 assert_eq!(
226 scroll_delta_for_axis(
227 Axis::Vertical,
228 RectF::new(vec2f(0., 100.), vec2f(250., 250.)),
229 RectF::new(vec2f(50., 400.), vec2f(50., 300.)),
230 mode,
231 ),
232 300.
233 );
234 
235 // Element taller than viewport, above viewport: align top with
236 // viewport top.
237 assert_eq!(
238 scroll_delta_for_axis(
239 Axis::Vertical,
240 RectF::new(vec2f(0., 200.), vec2f(250., 250.)),
241 RectF::new(vec2f(50., 100.), vec2f(50., 300.)),
242 mode,
243 ),
244 -100.
245 );
246 
247 // Element taller than viewport, top at viewport top: align top
248 // (delta = 0).
249 assert_eq!(
250 scroll_delta_for_axis(
251 Axis::Vertical,
252 RectF::new(vec2f(0., 100.), vec2f(250., 250.)),
253 RectF::new(vec2f(50., 100.), vec2f(50., 300.)),
254 mode,
255 ),
256 0.
257 );
258 
259 // Element taller than viewport, top visible but bottom extends
260 // past: align top with viewport top (shows max content from top).
261 assert_eq!(
262 scroll_delta_for_axis(
263 Axis::Vertical,
264 RectF::new(vec2f(0., 100.), vec2f(250., 250.)),
265 RectF::new(vec2f(50., 200.), vec2f(50., 300.)),
266 mode,
267 ),
268 100.
269 );
270 
271 // Element taller than viewport, spans entire viewport (top above,
272 // bottom below): align top with viewport top.
273 assert_eq!(
274 scroll_delta_for_axis(
275 Axis::Vertical,
276 RectF::new(vec2f(0., 100.), vec2f(250., 250.)),
277 RectF::new(vec2f(50., 50.), vec2f(50., 400.)),
278 mode,
279 ),
280 -50.
281 );
282 
283 // --- Element FITS in the viewport (delegates to FullyIntoView) ---
284 
285 // Small element below viewport: scroll down (bottom to viewport
286 // bottom).
287 assert_eq!(
288 scroll_delta_for_axis(
289 Axis::Vertical,
290 RectF::new(vec2f(0., 100.), vec2f(250., 250.)),
291 RectF::new(vec2f(50., 400.), vec2f(50., 50.)),
292 mode,
293 ),
294 100.
295 );
296 
297 // Small element above viewport: scroll up (top to viewport top).
298 assert_eq!(
299 scroll_delta_for_axis(
300 Axis::Vertical,
301 RectF::new(vec2f(0., 200.), vec2f(250., 250.)),
302 RectF::new(vec2f(50., 100.), vec2f(50., 50.)),
303 mode,
304 ),
305 -100.
306 );
307 
308 // Small element fully visible: no scroll.
309 assert_eq!(
310 scroll_delta_for_axis(
311 Axis::Vertical,
312 RectF::new(vec2f(0., 100.), vec2f(250., 250.)),
313 RectF::new(vec2f(50., 150.), vec2f(50., 50.)),
314 mode,
315 ),
316 0.
317 );
318 }
319}
320