StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | use pathfinder_geometry::{ |
| 2 | rect::RectF, |
| 3 | vector::{vec2f, Vector2F}, |
| 4 | }; |
| 5 | |
| 6 | use 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. |
| 19 | pub(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. |
| 47 | pub(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. |
| 82 | pub(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. |
| 135 | pub(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)] |
| 164 | mod 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 |