use frust_core::{
AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent,
LayoutCtx, PaintCtx, PaintScene, ScrollAxis, SemanticsCtx, SliverConstraints, SliverGeometry,
SliverWidget, View, Widget, any,
};
use kurbo::{Point, Size};
pub(crate) fn non_negative(value: f64) -> f64 {
if value.is_nan() { 0.0 } else { value.max(0.0) }
}
pub(crate) fn finite_extent(value: f64) -> f64 {
if value.is_finite() {
value.max(0.0)
} else {
0.0
}
}
pub(crate) fn axis_point(axis: ScrollAxis, main: f64, cross: f64) -> Point {
match axis {
ScrollAxis::Vertical => Point::new(cross, main),
ScrollAxis::Horizontal => Point::new(main, cross),
}
}
fn paint_offset_of(sc: &SliverConstraints, from: f64, to: f64) -> f64 {
let a = sc.scroll_offset;
let b = a + sc.remaining_paint_extent;
let span = to.max(a).min(b) - from.max(a).min(b);
span.max(0.0).min(sc.remaining_paint_extent)
}
fn cache_offset_of(sc: &SliverConstraints, from: f64, to: f64) -> f64 {
let a = sc.scroll_offset + sc.cache_origin;
let b = a + sc.remaining_cache_extent;
let span = to.max(a).min(b) - from.max(a).min(b);
span.max(0.0).min(sc.remaining_cache_extent)
}
pub(crate) fn box_geometry(extent: f64, sc: &SliverConstraints) -> SliverGeometry {
let extent = finite_extent(extent);
let paint_extent = paint_offset_of(sc, 0.0, extent);
let cache_extent = cache_offset_of(sc, 0.0, extent).max(paint_extent);
SliverGeometry {
cache_extent,
has_visual_overflow: extent > sc.remaining_paint_extent || sc.scroll_offset > 0.0,
..SliverGeometry::new(extent, paint_extent, extent)
}
}
pub(crate) fn box_child_origin(sc: &SliverConstraints) -> Point {
axis_point(sc.axis, -sc.scroll_offset, 0.0)
}
pub struct SliverBoxView<State: 'static> {
child: AnyView<State>,
}
pub fn sliver_box<State: 'static, V: View<State>>(child: V) -> SliverBoxView<State> {
SliverBoxView { child: any(child) }
}
pub struct SliverBoxWidget {
child: ChildPod,
clip: bool,
}
impl<State: 'static> View<State> for SliverBoxView<State> {
type Element = SliverBoxWidget;
fn build(&self, ctx: &mut BuildCtx<'_>) -> SliverBoxWidget {
SliverBoxWidget {
child: crate::authoring::build_child(&self.child, ctx),
clip: false,
}
}
fn rebuild(
&self,
prev: &Self,
element: &mut SliverBoxWidget,
ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
crate::authoring::rebuild_child(&prev.child, &self.child, &mut element.child, ctx)
}
fn teardown(&self, element: &mut SliverBoxWidget, ctx: &mut BuildCtx<'_>) {
crate::authoring::teardown_child(&self.child, &mut element.child, ctx);
}
}
impl Widget for SliverBoxWidget {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
let size = self.child.layout_child(ctx, bc);
self.child.set_origin(Point::ZERO);
self.clip = false;
size
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
if self.clip {
scene.push_clip(ctx.origin(), ctx.size());
}
self.child.paint_child(ctx, scene);
if self.clip {
scene.pop_clip();
}
}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
crate::authoring::route_event_single(&mut self.child, ctx, event)
}
fn semantics(&self, ctx: &mut SemanticsCtx) {
self.child.semantics_child(ctx);
}
fn as_sliver(&mut self) -> Option<&mut dyn SliverWidget> {
Some(self)
}
crate::authoring::visit_children!(child);
}
impl SliverWidget for SliverBoxWidget {
fn layout_sliver(&mut self, ctx: &mut LayoutCtx, sc: &SliverConstraints) -> SliverGeometry {
let size = self.child.layout_child(ctx, &sc.as_box_constraints());
let geometry = box_geometry(sc.axis.main_extent(size), sc);
self.child.set_origin(box_child_origin(sc));
self.clip = geometry.has_visual_overflow;
geometry
}
}
#[cfg(test)]
mod tests {
use super::*;
use frust_core::ScrollDirection;
fn constraints(scroll_offset: f64, remaining_paint_extent: f64) -> SliverConstraints {
SliverConstraints {
axis: ScrollAxis::Vertical,
scroll_offset,
preceding_scroll_extent: 0.0,
overlap: 0.0,
remaining_paint_extent,
cross_axis_extent: 400.0,
viewport_main_axis_extent: 500.0,
cache_origin: -scroll_offset.min(250.0),
remaining_cache_extent: remaining_paint_extent + 250.0 + scroll_offset.min(250.0),
user_scroll_direction: ScrollDirection::Idle,
}
}
#[test]
fn a_fully_visible_child_paints_its_whole_extent_uncut() {
let sc = constraints(0.0, 500.0);
let g = box_geometry(300.0, &sc);
assert_eq!(g.scroll_extent, 300.0);
assert_eq!(g.paint_extent, 300.0);
assert_eq!(g.layout_extent, 300.0);
assert_eq!(g.max_paint_extent, 300.0);
assert_eq!(g.cache_extent, 300.0);
assert!(g.visible && !g.has_visual_overflow);
g.debug_assert_valid(&sc);
}
#[test]
fn a_scrolled_child_paints_what_is_left_and_is_cut() {
let sc = constraints(250.0, 500.0);
let g = box_geometry(300.0, &sc);
assert_eq!(g.paint_extent, 50.0);
assert_eq!(g.hit_test_extent, 50.0);
assert!(g.has_visual_overflow);
assert_eq!(box_child_origin(&sc), Point::new(0.0, -250.0));
g.debug_assert_valid(&sc);
}
#[test]
fn a_child_in_the_cache_region_is_cached_but_invisible() {
let sc = SliverConstraints {
remaining_paint_extent: 0.0,
remaining_cache_extent: 250.0,
cache_origin: 0.0,
..constraints(0.0, 0.0)
};
let g = box_geometry(100.0, &sc);
assert_eq!(g.paint_extent, 0.0);
assert!(!g.visible);
assert_eq!(g.cache_extent, 100.0);
g.debug_assert_valid(&sc);
}
#[test]
fn an_unbounded_or_nan_extent_scrolls_nothing() {
let sc = constraints(0.0, 500.0);
assert_eq!(box_geometry(f64::INFINITY, &sc), SliverGeometry::ZERO);
assert_eq!(box_geometry(f64::NAN, &sc), SliverGeometry::ZERO);
assert_eq!(box_geometry(-5.0, &sc), SliverGeometry::ZERO);
}
#[test]
fn extents_sanitise_without_panicking() {
assert_eq!(non_negative(f64::NAN), 0.0);
assert_eq!(non_negative(-1.0), 0.0);
assert_eq!(non_negative(f64::INFINITY), f64::INFINITY);
assert_eq!(finite_extent(f64::INFINITY), 0.0);
assert_eq!(finite_extent(12.5), 12.5);
}
}