1use kurbo::Size;
12
13use crate::layout::BoxConstraints;
14use crate::widget::{LayoutCtx, Widget};
15
16pub const PRECISION_ERROR_TOLERANCE: f64 = 1e-10;
19
20#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
22pub enum ScrollAxis {
23 #[default]
25 Vertical,
26 Horizontal,
28}
29
30impl ScrollAxis {
31 pub fn size(self, main: f64, cross: f64) -> Size {
33 match self {
34 ScrollAxis::Vertical => Size::new(cross, main),
35 ScrollAxis::Horizontal => Size::new(main, cross),
36 }
37 }
38
39 pub fn main_extent(self, size: Size) -> f64 {
41 match self {
42 ScrollAxis::Vertical => size.height,
43 ScrollAxis::Horizontal => size.width,
44 }
45 }
46
47 pub fn cross_extent(self, size: Size) -> f64 {
49 match self {
50 ScrollAxis::Vertical => size.width,
51 ScrollAxis::Horizontal => size.height,
52 }
53 }
54}
55
56#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
59#[non_exhaustive]
60pub enum GrowthDirection {
61 #[default]
63 Forward,
64}
65
66#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
68pub enum ScrollDirection {
69 #[default]
71 Idle,
72 Forward,
74 Reverse,
76}
77
78#[derive(Clone, Copy, Debug, PartialEq)]
82pub struct SliverConstraints {
83 pub axis: ScrollAxis,
84 pub scroll_offset: f64,
87 pub preceding_scroll_extent: f64,
89 pub overlap: f64,
91 pub remaining_paint_extent: f64,
94 pub cross_axis_extent: f64,
95 pub viewport_main_axis_extent: f64,
96 pub cache_origin: f64,
98 pub remaining_cache_extent: f64,
100 pub user_scroll_direction: ScrollDirection,
101}
102
103impl SliverConstraints {
104 pub fn growth_direction(&self) -> GrowthDirection {
106 GrowthDirection::Forward
107 }
108
109 pub fn as_box_constraints(&self) -> BoxConstraints {
112 let cross = self.cross_axis_extent;
113 BoxConstraints::new(
114 self.axis.size(0.0, cross),
115 self.axis.size(f64::INFINITY, cross),
116 )
117 }
118
119 pub fn debug_assert_valid(&self) {
122 debug_assert!(
123 self.scroll_offset >= 0.0,
124 "SliverConstraints: scroll_offset {} is negative or NaN",
125 self.scroll_offset
126 );
127 debug_assert!(
128 self.cross_axis_extent >= 0.0,
129 "SliverConstraints: cross_axis_extent {} is negative or NaN",
130 self.cross_axis_extent
131 );
132 debug_assert!(
133 self.viewport_main_axis_extent >= 0.0,
134 "SliverConstraints: viewport_main_axis_extent {} is negative or NaN",
135 self.viewport_main_axis_extent
136 );
137 debug_assert!(
138 self.remaining_paint_extent >= 0.0,
139 "SliverConstraints: remaining_paint_extent {} is negative or NaN",
140 self.remaining_paint_extent
141 );
142 debug_assert!(
143 self.remaining_cache_extent >= 0.0,
144 "SliverConstraints: remaining_cache_extent {} is negative or NaN",
145 self.remaining_cache_extent
146 );
147 debug_assert!(
148 self.cache_origin <= 0.0,
149 "SliverConstraints: cache_origin {} is positive or NaN",
150 self.cache_origin
151 );
152 debug_assert!(
153 !self.preceding_scroll_extent.is_nan(),
154 "SliverConstraints: preceding_scroll_extent is NaN"
155 );
156 debug_assert!(!self.overlap.is_nan(), "SliverConstraints: overlap is NaN");
157 }
158}
159
160#[derive(Clone, Copy, Debug, PartialEq)]
163pub struct SliverGeometry {
164 pub scroll_extent: f64,
166 pub paint_extent: f64,
168 pub paint_origin: f64,
171 pub layout_extent: f64,
173 pub max_paint_extent: f64,
175 pub hit_test_extent: f64,
177 pub visible: bool,
178 pub has_visual_overflow: bool,
180 pub scroll_offset_correction: Option<f64>,
183 pub cache_extent: f64,
185}
186
187impl SliverGeometry {
188 pub const ZERO: SliverGeometry = SliverGeometry {
190 scroll_extent: 0.0,
191 paint_extent: 0.0,
192 paint_origin: 0.0,
193 layout_extent: 0.0,
194 max_paint_extent: 0.0,
195 hit_test_extent: 0.0,
196 visible: false,
197 has_visual_overflow: false,
198 scroll_offset_correction: None,
199 cache_extent: 0.0,
200 };
201
202 pub fn new(scroll_extent: f64, paint_extent: f64, max_paint_extent: f64) -> Self {
205 SliverGeometry {
206 scroll_extent,
207 paint_extent,
208 layout_extent: paint_extent,
209 max_paint_extent,
210 hit_test_extent: paint_extent,
211 visible: paint_extent > 0.0,
212 cache_extent: paint_extent,
213 ..SliverGeometry::ZERO
214 }
215 }
216
217 pub fn debug_assert_valid(&self, constraints: &SliverConstraints) {
220 debug_assert!(
221 self.scroll_extent >= 0.0,
222 "SliverGeometry: scroll_extent {} is negative or NaN",
223 self.scroll_extent
224 );
225 debug_assert!(
226 self.paint_extent >= 0.0,
227 "SliverGeometry: paint_extent {} is negative or NaN",
228 self.paint_extent
229 );
230 debug_assert!(
231 self.layout_extent >= 0.0,
232 "SliverGeometry: layout_extent {} is negative or NaN",
233 self.layout_extent
234 );
235 debug_assert!(
236 self.cache_extent >= 0.0,
237 "SliverGeometry: cache_extent {} is negative or NaN",
238 self.cache_extent
239 );
240 debug_assert!(
241 self.hit_test_extent >= 0.0,
242 "SliverGeometry: hit_test_extent {} is negative or NaN",
243 self.hit_test_extent
244 );
245 debug_assert!(
246 self.layout_extent - self.paint_extent <= PRECISION_ERROR_TOLERANCE,
247 "SliverGeometry: layout_extent {} exceeds paint_extent {}",
248 self.layout_extent,
249 self.paint_extent
250 );
251 debug_assert!(
252 self.paint_extent - self.max_paint_extent <= PRECISION_ERROR_TOLERANCE,
253 "SliverGeometry: max_paint_extent {} is less than paint_extent {}",
254 self.max_paint_extent,
255 self.paint_extent
256 );
257 debug_assert!(
258 self.paint_extent - constraints.remaining_paint_extent <= PRECISION_ERROR_TOLERANCE,
259 "SliverGeometry: paint_extent {} exceeds the constraints' remaining_paint_extent {}",
260 self.paint_extent,
261 constraints.remaining_paint_extent
262 );
263 debug_assert!(
264 self.layout_extent - self.cache_extent <= PRECISION_ERROR_TOLERANCE,
265 "SliverGeometry: cache_extent {} is less than layout_extent {}",
266 self.cache_extent,
267 self.layout_extent
268 );
269 if let Some(correction) = self.scroll_offset_correction {
270 debug_assert!(
271 correction != 0.0 && correction.is_finite(),
272 "SliverGeometry: scroll_offset_correction {correction} is zero or not finite \
273 (use None for no correction)"
274 );
275 }
276 }
277}
278
279pub trait SliverWidget: Widget {
285 fn layout_sliver(&mut self, ctx: &mut LayoutCtx, sc: &SliverConstraints) -> SliverGeometry;
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293
294 fn constraints(axis: ScrollAxis) -> SliverConstraints {
295 SliverConstraints {
296 axis,
297 scroll_offset: 0.0,
298 preceding_scroll_extent: 0.0,
299 overlap: 0.0,
300 remaining_paint_extent: 600.0,
301 cross_axis_extent: 400.0,
302 viewport_main_axis_extent: 600.0,
303 cache_origin: 0.0,
304 remaining_cache_extent: 850.0,
305 user_scroll_direction: ScrollDirection::Idle,
306 }
307 }
308
309 #[test]
310 fn axis_packs_main_and_cross() {
311 assert_eq!(ScrollAxis::Vertical.size(10.0, 20.0), Size::new(20.0, 10.0));
312 assert_eq!(
313 ScrollAxis::Horizontal.size(10.0, 20.0),
314 Size::new(10.0, 20.0)
315 );
316 let size = Size::new(3.0, 7.0);
317 assert_eq!(ScrollAxis::Vertical.main_extent(size), 7.0);
318 assert_eq!(ScrollAxis::Vertical.cross_extent(size), 3.0);
319 assert_eq!(ScrollAxis::Horizontal.main_extent(size), 3.0);
320 assert_eq!(ScrollAxis::Horizontal.cross_extent(size), 7.0);
321 }
322
323 #[test]
324 fn box_constraints_are_tight_across_and_unbounded_along() {
325 let bc = constraints(ScrollAxis::Vertical).as_box_constraints();
326 assert_eq!(bc.min(), Size::new(400.0, 0.0));
327 assert_eq!(bc.max(), Size::new(400.0, f64::INFINITY));
328 let bc = constraints(ScrollAxis::Horizontal).as_box_constraints();
329 assert_eq!(bc.min(), Size::new(0.0, 400.0));
330 assert_eq!(bc.max(), Size::new(f64::INFINITY, 400.0));
331 }
332
333 #[test]
334 fn growth_is_forward() {
335 assert_eq!(
336 constraints(ScrollAxis::Vertical).growth_direction(),
337 GrowthDirection::Forward
338 );
339 }
340
341 #[test]
342 fn new_applies_the_flutter_defaults() {
343 let g = SliverGeometry::new(1000.0, 600.0, 1000.0);
344 assert_eq!(g.layout_extent, 600.0);
345 assert_eq!(g.hit_test_extent, 600.0);
346 assert_eq!(g.cache_extent, 600.0);
347 assert!(g.visible);
348 assert!(!SliverGeometry::new(1000.0, 0.0, 1000.0).visible);
349 g.debug_assert_valid(&constraints(ScrollAxis::Vertical));
350 SliverGeometry::ZERO.debug_assert_valid(&constraints(ScrollAxis::Vertical));
351 }
352
353 #[test]
354 fn valid_constraints_pass() {
355 constraints(ScrollAxis::Vertical).debug_assert_valid();
356 SliverConstraints {
357 remaining_paint_extent: f64::INFINITY,
358 preceding_scroll_extent: f64::INFINITY,
359 cache_origin: -250.0,
360 ..constraints(ScrollAxis::Horizontal)
361 }
362 .debug_assert_valid();
363 }
364
365 #[test]
366 fn equal_constraints_compare_equal_and_nan_never_does() {
367 let sc = constraints(ScrollAxis::Vertical);
368 assert_eq!(sc, sc);
369 let moved = SliverConstraints {
370 scroll_offset: 5.0,
371 ..sc
372 };
373 assert_ne!(sc, moved);
374 let nan = SliverConstraints {
375 overlap: f64::NAN,
376 ..sc
377 };
378 assert_ne!(nan, nan);
379 }
380
381 #[cfg(debug_assertions)]
382 #[test]
383 #[should_panic(expected = "scroll_offset")]
384 fn negative_scroll_offset_is_rejected() {
385 SliverConstraints {
386 scroll_offset: -1.0,
387 ..constraints(ScrollAxis::Vertical)
388 }
389 .debug_assert_valid();
390 }
391
392 #[cfg(debug_assertions)]
393 #[test]
394 #[should_panic(expected = "cache_origin")]
395 fn positive_cache_origin_is_rejected() {
396 SliverConstraints {
397 cache_origin: 1.0,
398 ..constraints(ScrollAxis::Vertical)
399 }
400 .debug_assert_valid();
401 }
402
403 #[cfg(debug_assertions)]
404 #[test]
405 #[should_panic(expected = "remaining_paint_extent")]
406 fn paint_past_the_remaining_extent_is_rejected() {
407 SliverGeometry::new(1000.0, 601.0, 1000.0)
408 .debug_assert_valid(&constraints(ScrollAxis::Vertical));
409 }
410
411 #[cfg(debug_assertions)]
412 #[test]
413 #[should_panic(expected = "exceeds paint_extent")]
414 fn layout_past_paint_is_rejected() {
415 SliverGeometry {
416 layout_extent: 200.0,
417 ..SliverGeometry::new(1000.0, 100.0, 1000.0)
418 }
419 .debug_assert_valid(&constraints(ScrollAxis::Vertical));
420 }
421
422 #[cfg(debug_assertions)]
423 #[test]
424 #[should_panic(expected = "max_paint_extent")]
425 fn paint_past_max_paint_is_rejected() {
426 SliverGeometry::new(1000.0, 300.0, 200.0)
427 .debug_assert_valid(&constraints(ScrollAxis::Vertical));
428 }
429
430 #[cfg(debug_assertions)]
431 #[test]
432 #[should_panic(expected = "scroll_offset_correction")]
433 fn zero_correction_is_rejected() {
434 SliverGeometry {
435 scroll_offset_correction: Some(0.0),
436 ..SliverGeometry::ZERO
437 }
438 .debug_assert_valid(&constraints(ScrollAxis::Vertical));
439 }
440
441 #[test]
442 fn tolerance_absorbs_float_noise() {
443 SliverGeometry::new(1000.0, 600.0 + 1e-12, 1000.0)
444 .debug_assert_valid(&constraints(ScrollAxis::Vertical));
445 }
446}