use kurbo::Size;
use crate::layout::BoxConstraints;
use crate::widget::{LayoutCtx, Widget};
pub const PRECISION_ERROR_TOLERANCE: f64 = 1e-10;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum ScrollAxis {
#[default]
Vertical,
Horizontal,
}
impl ScrollAxis {
pub fn size(self, main: f64, cross: f64) -> Size {
match self {
ScrollAxis::Vertical => Size::new(cross, main),
ScrollAxis::Horizontal => Size::new(main, cross),
}
}
pub fn main_extent(self, size: Size) -> f64 {
match self {
ScrollAxis::Vertical => size.height,
ScrollAxis::Horizontal => size.width,
}
}
pub fn cross_extent(self, size: Size) -> f64 {
match self {
ScrollAxis::Vertical => size.width,
ScrollAxis::Horizontal => size.height,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum GrowthDirection {
#[default]
Forward,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum ScrollDirection {
#[default]
Idle,
Forward,
Reverse,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SliverConstraints {
pub axis: ScrollAxis,
pub scroll_offset: f64,
pub preceding_scroll_extent: f64,
pub overlap: f64,
pub remaining_paint_extent: f64,
pub cross_axis_extent: f64,
pub viewport_main_axis_extent: f64,
pub cache_origin: f64,
pub remaining_cache_extent: f64,
pub user_scroll_direction: ScrollDirection,
}
impl SliverConstraints {
pub fn growth_direction(&self) -> GrowthDirection {
GrowthDirection::Forward
}
pub fn as_box_constraints(&self) -> BoxConstraints {
let cross = self.cross_axis_extent;
BoxConstraints::new(
self.axis.size(0.0, cross),
self.axis.size(f64::INFINITY, cross),
)
}
pub fn debug_assert_valid(&self) {
debug_assert!(
self.scroll_offset >= 0.0,
"SliverConstraints: scroll_offset {} is negative or NaN",
self.scroll_offset
);
debug_assert!(
self.cross_axis_extent >= 0.0,
"SliverConstraints: cross_axis_extent {} is negative or NaN",
self.cross_axis_extent
);
debug_assert!(
self.viewport_main_axis_extent >= 0.0,
"SliverConstraints: viewport_main_axis_extent {} is negative or NaN",
self.viewport_main_axis_extent
);
debug_assert!(
self.remaining_paint_extent >= 0.0,
"SliverConstraints: remaining_paint_extent {} is negative or NaN",
self.remaining_paint_extent
);
debug_assert!(
self.remaining_cache_extent >= 0.0,
"SliverConstraints: remaining_cache_extent {} is negative or NaN",
self.remaining_cache_extent
);
debug_assert!(
self.cache_origin <= 0.0,
"SliverConstraints: cache_origin {} is positive or NaN",
self.cache_origin
);
debug_assert!(
!self.preceding_scroll_extent.is_nan(),
"SliverConstraints: preceding_scroll_extent is NaN"
);
debug_assert!(!self.overlap.is_nan(), "SliverConstraints: overlap is NaN");
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SliverGeometry {
pub scroll_extent: f64,
pub paint_extent: f64,
pub paint_origin: f64,
pub layout_extent: f64,
pub max_paint_extent: f64,
pub hit_test_extent: f64,
pub visible: bool,
pub has_visual_overflow: bool,
pub scroll_offset_correction: Option<f64>,
pub cache_extent: f64,
}
impl SliverGeometry {
pub const ZERO: SliverGeometry = SliverGeometry {
scroll_extent: 0.0,
paint_extent: 0.0,
paint_origin: 0.0,
layout_extent: 0.0,
max_paint_extent: 0.0,
hit_test_extent: 0.0,
visible: false,
has_visual_overflow: false,
scroll_offset_correction: None,
cache_extent: 0.0,
};
pub fn new(scroll_extent: f64, paint_extent: f64, max_paint_extent: f64) -> Self {
SliverGeometry {
scroll_extent,
paint_extent,
layout_extent: paint_extent,
max_paint_extent,
hit_test_extent: paint_extent,
visible: paint_extent > 0.0,
cache_extent: paint_extent,
..SliverGeometry::ZERO
}
}
pub fn debug_assert_valid(&self, constraints: &SliverConstraints) {
debug_assert!(
self.scroll_extent >= 0.0,
"SliverGeometry: scroll_extent {} is negative or NaN",
self.scroll_extent
);
debug_assert!(
self.paint_extent >= 0.0,
"SliverGeometry: paint_extent {} is negative or NaN",
self.paint_extent
);
debug_assert!(
self.layout_extent >= 0.0,
"SliverGeometry: layout_extent {} is negative or NaN",
self.layout_extent
);
debug_assert!(
self.cache_extent >= 0.0,
"SliverGeometry: cache_extent {} is negative or NaN",
self.cache_extent
);
debug_assert!(
self.hit_test_extent >= 0.0,
"SliverGeometry: hit_test_extent {} is negative or NaN",
self.hit_test_extent
);
debug_assert!(
self.layout_extent - self.paint_extent <= PRECISION_ERROR_TOLERANCE,
"SliverGeometry: layout_extent {} exceeds paint_extent {}",
self.layout_extent,
self.paint_extent
);
debug_assert!(
self.paint_extent - self.max_paint_extent <= PRECISION_ERROR_TOLERANCE,
"SliverGeometry: max_paint_extent {} is less than paint_extent {}",
self.max_paint_extent,
self.paint_extent
);
debug_assert!(
self.paint_extent - constraints.remaining_paint_extent <= PRECISION_ERROR_TOLERANCE,
"SliverGeometry: paint_extent {} exceeds the constraints' remaining_paint_extent {}",
self.paint_extent,
constraints.remaining_paint_extent
);
debug_assert!(
self.layout_extent - self.cache_extent <= PRECISION_ERROR_TOLERANCE,
"SliverGeometry: cache_extent {} is less than layout_extent {}",
self.cache_extent,
self.layout_extent
);
if let Some(correction) = self.scroll_offset_correction {
debug_assert!(
correction != 0.0 && correction.is_finite(),
"SliverGeometry: scroll_offset_correction {correction} is zero or not finite \
(use None for no correction)"
);
}
}
}
pub trait SliverWidget: Widget {
fn layout_sliver(&mut self, ctx: &mut LayoutCtx, sc: &SliverConstraints) -> SliverGeometry;
}
#[cfg(test)]
mod tests {
use super::*;
fn constraints(axis: ScrollAxis) -> SliverConstraints {
SliverConstraints {
axis,
scroll_offset: 0.0,
preceding_scroll_extent: 0.0,
overlap: 0.0,
remaining_paint_extent: 600.0,
cross_axis_extent: 400.0,
viewport_main_axis_extent: 600.0,
cache_origin: 0.0,
remaining_cache_extent: 850.0,
user_scroll_direction: ScrollDirection::Idle,
}
}
#[test]
fn axis_packs_main_and_cross() {
assert_eq!(ScrollAxis::Vertical.size(10.0, 20.0), Size::new(20.0, 10.0));
assert_eq!(
ScrollAxis::Horizontal.size(10.0, 20.0),
Size::new(10.0, 20.0)
);
let size = Size::new(3.0, 7.0);
assert_eq!(ScrollAxis::Vertical.main_extent(size), 7.0);
assert_eq!(ScrollAxis::Vertical.cross_extent(size), 3.0);
assert_eq!(ScrollAxis::Horizontal.main_extent(size), 3.0);
assert_eq!(ScrollAxis::Horizontal.cross_extent(size), 7.0);
}
#[test]
fn box_constraints_are_tight_across_and_unbounded_along() {
let bc = constraints(ScrollAxis::Vertical).as_box_constraints();
assert_eq!(bc.min(), Size::new(400.0, 0.0));
assert_eq!(bc.max(), Size::new(400.0, f64::INFINITY));
let bc = constraints(ScrollAxis::Horizontal).as_box_constraints();
assert_eq!(bc.min(), Size::new(0.0, 400.0));
assert_eq!(bc.max(), Size::new(f64::INFINITY, 400.0));
}
#[test]
fn growth_is_forward() {
assert_eq!(
constraints(ScrollAxis::Vertical).growth_direction(),
GrowthDirection::Forward
);
}
#[test]
fn new_applies_the_flutter_defaults() {
let g = SliverGeometry::new(1000.0, 600.0, 1000.0);
assert_eq!(g.layout_extent, 600.0);
assert_eq!(g.hit_test_extent, 600.0);
assert_eq!(g.cache_extent, 600.0);
assert!(g.visible);
assert!(!SliverGeometry::new(1000.0, 0.0, 1000.0).visible);
g.debug_assert_valid(&constraints(ScrollAxis::Vertical));
SliverGeometry::ZERO.debug_assert_valid(&constraints(ScrollAxis::Vertical));
}
#[test]
fn valid_constraints_pass() {
constraints(ScrollAxis::Vertical).debug_assert_valid();
SliverConstraints {
remaining_paint_extent: f64::INFINITY,
preceding_scroll_extent: f64::INFINITY,
cache_origin: -250.0,
..constraints(ScrollAxis::Horizontal)
}
.debug_assert_valid();
}
#[test]
fn equal_constraints_compare_equal_and_nan_never_does() {
let sc = constraints(ScrollAxis::Vertical);
assert_eq!(sc, sc);
let moved = SliverConstraints {
scroll_offset: 5.0,
..sc
};
assert_ne!(sc, moved);
let nan = SliverConstraints {
overlap: f64::NAN,
..sc
};
assert_ne!(nan, nan);
}
#[cfg(debug_assertions)]
#[test]
#[should_panic(expected = "scroll_offset")]
fn negative_scroll_offset_is_rejected() {
SliverConstraints {
scroll_offset: -1.0,
..constraints(ScrollAxis::Vertical)
}
.debug_assert_valid();
}
#[cfg(debug_assertions)]
#[test]
#[should_panic(expected = "cache_origin")]
fn positive_cache_origin_is_rejected() {
SliverConstraints {
cache_origin: 1.0,
..constraints(ScrollAxis::Vertical)
}
.debug_assert_valid();
}
#[cfg(debug_assertions)]
#[test]
#[should_panic(expected = "remaining_paint_extent")]
fn paint_past_the_remaining_extent_is_rejected() {
SliverGeometry::new(1000.0, 601.0, 1000.0)
.debug_assert_valid(&constraints(ScrollAxis::Vertical));
}
#[cfg(debug_assertions)]
#[test]
#[should_panic(expected = "exceeds paint_extent")]
fn layout_past_paint_is_rejected() {
SliverGeometry {
layout_extent: 200.0,
..SliverGeometry::new(1000.0, 100.0, 1000.0)
}
.debug_assert_valid(&constraints(ScrollAxis::Vertical));
}
#[cfg(debug_assertions)]
#[test]
#[should_panic(expected = "max_paint_extent")]
fn paint_past_max_paint_is_rejected() {
SliverGeometry::new(1000.0, 300.0, 200.0)
.debug_assert_valid(&constraints(ScrollAxis::Vertical));
}
#[cfg(debug_assertions)]
#[test]
#[should_panic(expected = "scroll_offset_correction")]
fn zero_correction_is_rejected() {
SliverGeometry {
scroll_offset_correction: Some(0.0),
..SliverGeometry::ZERO
}
.debug_assert_valid(&constraints(ScrollAxis::Vertical));
}
#[test]
fn tolerance_absorbs_float_noise() {
SliverGeometry::new(1000.0, 600.0 + 1e-12, 1000.0)
.debug_assert_valid(&constraints(ScrollAxis::Vertical));
}
}