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/clipped_scrollable_test.rs
StratoSDK / crates / strato-ui-core / src / elements / clipped_scrollable_test.rs
1use std::collections::HashSet;
2 
3use pathfinder_geometry::vector::vec2f;
4 
5use crate::{
6 elements::{Axis, ConstrainedBox, Empty, Flex, ParentElement, SavePosition, Stack},
7 platform::WindowStyle,
8 units::IntoPixels,
9 App, Element, Entity, Presenter, TypedActionView, WindowInvalidation,
10};
11 
12use super::{ClippedScrollStateHandle, ClippedScrollable, ScrollTarget, ScrollToPositionMode};
13 
14macro_rules! assert_float_eq {
15 ($lhs:expr, $rhs:expr) => {{
16 let lhs = $lhs;
17 let rhs = $rhs;
18 assert!(
19 (lhs - rhs).abs() < f32::EPSILON,
20 "{} ({}) != {} ({})",
21 lhs,
22 stringify!($lhs),
23 rhs,
24 stringify!($rhs)
25 );
26 }};
27}
28 
29#[derive(Default)]
30struct View {
31 scroll_handle: ClippedScrollStateHandle,
32}
33 
34impl Entity for View {
35 type Event = ();
36}
37 
38impl crate::core::View for View {
39 fn ui_name() -> &'static str {
40 "View"
41 }
42 
43 fn render(&self, _: &crate::AppContext) -> Box<dyn crate::Element> {
44 let mut children = vec![];
45 for i in 0..10 {
46 children.push(
47 SavePosition::new(
48 ConstrainedBox::new(Empty::new().finish())
49 .with_height(20.)
50 .with_width(100.)
51 .finish(),
52 &format!("child_{i}"),
53 )
54 .finish(),
55 );
56 }
57 
58 let mut stack = Stack::new();
59 stack.add_child(
60 ClippedScrollable::new(
61 Axis::Vertical,
62 Flex::column().with_children(children).finish(),
63 self.scroll_handle.clone(),
64 )
65 .finish(),
66 );
67 stack.finish()
68 }
69}
70 
71impl TypedActionView for View {
72 type Action = ();
73}
74 
75#[test]
76fn test_scroll_to_position() {
77 App::test((), |mut app| async move {
78 let app = &mut app;
79 let (window_id, view) = app.add_window(WindowStyle::NotStealFocus, |_| View::default());
80 
81 let mut presenter = Presenter::new(window_id);
82 
83 let mut updated = HashSet::new();
84 updated.insert(app.root_view_id(window_id).unwrap());
85 let invalidation = WindowInvalidation {
86 updated,
87 ..Default::default()
88 };
89 
90 let scroll_state = view.read(app, |view, _| view.scroll_handle.clone());
91 let window_size = vec2f(100., 100.);
92 let scale_factor = 1.;
93 
94 app.update(move |ctx| {
95 presenter.invalidate(invalidation.clone(), ctx);
96 // The `ClippedScrollable` has 10 elements in total, each with a height of 20.
97 // The window height is 100 so, with a scroll top of 0, the first 5 elements should be
98 // in view.
99 presenter.build_scene(window_size, scale_factor, None, ctx);
100 
101 // An element fully below the scrollable area should be the last item in view
102 // after we scroll to it.
103 scroll_state.scroll_to_position(ScrollTarget {
104 position_id: "child_6".to_string(),
105 mode: ScrollToPositionMode::FullyIntoView,
106 });
107 presenter.invalidate(invalidation.clone(), ctx);
108 presenter.build_scene(window_size, scale_factor, None, ctx);
109 assert_float_eq!(scroll_state.scroll_start().as_f32(), 40.);
110 
111 // An element fully above the scrollable area should be the first item in view after
112 // it's scrolled to.
113 scroll_state.scroll_to_position(ScrollTarget {
114 position_id: "child_1".to_string(),
115 mode: ScrollToPositionMode::FullyIntoView,
116 });
117 presenter.invalidate(invalidation.clone(), ctx);
118 presenter.build_scene(window_size, scale_factor, None, ctx);
119 assert_float_eq!(scroll_state.scroll_start().as_f32(), 20.);
120 
121 // An element fully within the viewport should no-op.
122 scroll_state.scroll_to_position(ScrollTarget {
123 position_id: "child_3".to_string(),
124 mode: ScrollToPositionMode::FullyIntoView,
125 });
126 presenter.invalidate(invalidation.clone(), ctx);
127 presenter.build_scene(window_size, scale_factor, None, ctx);
128 assert_float_eq!(scroll_state.scroll_start().as_f32(), 20.);
129 
130 // An element that is partially above the viewport should be scrolled fully within the viewport.
131 // First, make the scroll top 1.0 pixels. We need to call build scene after this so the
132 // position cache is updated appropriately.
133 scroll_state.clipped_scroll_data.lock().scroll_start_px = (1_f32).into_pixels();
134 presenter.invalidate(invalidation.clone(), ctx);
135 presenter.build_scene(window_size, scale_factor, None, ctx);
136 
137 // Now we can invoke the scroll to position API and verify the correct result.
138 scroll_state.scroll_to_position(ScrollTarget {
139 position_id: "child_0".to_string(),
140 mode: ScrollToPositionMode::FullyIntoView,
141 });
142 presenter.invalidate(invalidation.clone(), ctx);
143 presenter.build_scene(window_size, scale_factor, None, ctx);
144 assert_float_eq!(scroll_state.scroll_start().as_f32(), 0.);
145 
146 // An element that is partially below the viewport should be scrolled fully within the viewport.
147 // First, make the scroll top 1.0 pixels. We need to call build scene after this so the
148 // position cache is updated appropriately.
149 scroll_state.clipped_scroll_data.lock().scroll_start_px = (1_f32).into_pixels();
150 presenter.invalidate(invalidation.clone(), ctx);
151 presenter.build_scene(window_size, scale_factor, None, ctx);
152 
153 // Now we can invoke the scroll to position API and verify the correct result.
154 scroll_state.scroll_to_position(ScrollTarget {
155 position_id: "child_5".to_string(),
156 mode: ScrollToPositionMode::FullyIntoView,
157 });
158 presenter.invalidate(invalidation.clone(), ctx);
159 presenter.build_scene(window_size, scale_factor, None, ctx);
160 assert_float_eq!(scroll_state.scroll_start().as_f32(), 20.);
161 });
162 });
163}
164