use std::cell::{Cell, RefCell};
use std::rc::Rc;
use frust_core::accesskit::Role;
use frust_core::{
AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent,
LayoutCtx, PaintCtx, PaintScene, PointerButton, PointerEvent, PointerPhase, ScrollAxis,
ScrollDirection, SemanticsCtx, SliverConstraints, SliverGeometry, View, ViewSeq, Widget,
};
use frust_theme::Theme;
use kurbo::{Affine, Point, Rect, Size};
use crate::authoring::presses;
use crate::physics::effect::OverscrollEffect;
use crate::physics::{ScrollPhysics, default_overscroll_effect};
use crate::scroll::{InnerScrollState, ScrollInfo, ambient_scroll_claim};
use crate::scroll_controller::ScrollController;
use crate::scroll_core::{BallisticState, ScrollSurface, ScrollSurfaceCore};
use crate::slivers::adapter::{
axis_point, box_child_origin, box_geometry, finite_extent, non_negative,
};
pub const DEFAULT_CACHE_EXTENT: f64 = 250.0;
const MAX_LAYOUT_CYCLES: usize = 10;
type OnScroll<State> = Rc<dyn Fn(&mut State, ScrollInfo)>;
type OnRefresh<State> = Rc<dyn Fn(&mut State)>;
pub struct CustomScrollView<State: 'static> {
slivers: Vec<AnyView<State>>,
axis: ScrollAxis,
cache_extent: f64,
on_scroll: Option<OnScroll<State>>,
on_refresh_release: Option<OnRefresh<State>>,
physics: Option<Rc<dyn ScrollPhysics>>,
effect: OverscrollEffect,
controller: Option<ScrollController>,
}
pub fn custom_scroll_view<State: 'static, M>(
slivers: impl ViewSeq<State, M>,
) -> CustomScrollView<State> {
let mut list = Vec::new();
slivers.extend_views(&mut list);
CustomScrollView {
slivers: list,
axis: ScrollAxis::Vertical,
cache_extent: DEFAULT_CACHE_EXTENT,
on_scroll: None,
on_refresh_release: None,
physics: None,
effect: default_overscroll_effect(),
controller: None,
}
}
impl<State: 'static> CustomScrollView<State> {
pub fn axis(mut self, axis: ScrollAxis) -> Self {
self.axis = axis;
self
}
pub fn cache_extent(mut self, extent: f64) -> Self {
self.cache_extent = extent;
self
}
pub fn on_scroll<F: Fn(&mut State, ScrollInfo) + 'static>(mut self, callback: F) -> Self {
self.on_scroll = Some(Rc::new(callback));
self
}
pub fn on_refresh_release<F: Fn(&mut State) + 'static>(mut self, callback: F) -> Self {
self.on_refresh_release = Some(Rc::new(callback));
self
}
pub fn physics(mut self, physics: impl ScrollPhysics + 'static) -> Self {
self.physics = Some(Rc::new(physics));
self
}
pub fn overscroll_effect(mut self, effect: OverscrollEffect) -> Self {
self.effect = effect;
self
}
pub fn controller(mut self, controller: ScrollController) -> Self {
self.controller = Some(controller);
self
}
}
#[derive(Clone, Copy, Debug)]
struct Slot {
geometry: SliverGeometry,
paint_offset: f64,
is_box: bool,
}
impl Default for Slot {
fn default() -> Self {
Self {
geometry: SliverGeometry::ZERO,
paint_offset: 0.0,
is_box: false,
}
}
}
pub struct ViewportWidget {
children: Vec<ChildPod>,
slots: Vec<Slot>,
axis: ScrollAxis,
cache_extent: f64,
offset: f64,
viewport: Size,
main_extent: f64,
cross_extent: f64,
viewport_main: f64,
max_extent: f64,
laid_out_pixels: f64,
direction: ScrollDirection,
arming_down: bool,
down_inner_axis: Option<ScrollAxis>,
core: ScrollSurfaceCore,
}
impl ViewportWidget {
fn new(children: Vec<ChildPod>, axis: ScrollAxis, cache_extent: f64) -> Self {
let slots = vec![Slot::default(); children.len()];
Self {
children,
slots,
axis,
cache_extent: finite_extent(cache_extent),
offset: 0.0,
viewport: Size::ZERO,
main_extent: 0.0,
cross_extent: 0.0,
viewport_main: 0.0,
max_extent: 0.0,
laid_out_pixels: 0.0,
direction: ScrollDirection::Idle,
arming_down: false,
down_inner_axis: None,
core: ScrollSurfaceCore::with_axis(axis),
}
}
pub fn offset(&self) -> f64 {
self.offset
}
pub fn max_offset(&self) -> f64 {
self.max_extent
}
pub fn axis(&self) -> ScrollAxis {
self.core.axis()
}
pub fn sliver_count(&self) -> usize {
self.children.len()
}
pub fn sliver_geometry(&self, index: usize) -> Option<SliverGeometry> {
self.slots.get(index).map(|slot| slot.geometry)
}
pub fn is_flinging(&self) -> bool {
self.core.is_flinging()
}
fn set_offset(&mut self, value: f64) {
self.offset = value.clamp(0.0, self.max_extent);
}
fn layout_pixels(&self) -> f64 {
let pixels = match self.core.effect {
OverscrollEffect::Translate => self.offset,
OverscrollEffect::Stretch | OverscrollEffect::None => self.offset - self.displacement(),
};
if pixels.is_finite() { pixels } else { 0.0 }
}
fn user_motion(&self) -> bool {
let core = &self.core;
core.scrolling
|| core.settling
|| core.fling.is_some()
|| (core.ballistic.is_some() && !core.controller_animating)
}
fn user_scroll_direction(&self) -> ScrollDirection {
if self.user_motion() {
self.direction
} else {
ScrollDirection::Idle
}
}
fn observe_direction(&mut self) {
let pixels = self.layout_pixels();
if pixels < self.laid_out_pixels {
self.direction = ScrollDirection::Forward;
} else if pixels > self.laid_out_pixels {
self.direction = ScrollDirection::Reverse;
}
}
fn layout_slivers(&mut self, ctx: &mut LayoutCtx) {
for cycle in 0..MAX_LAYOUT_CYCLES {
let last = cycle + 1 == MAX_LAYOUT_CYCLES;
match self.attempt_layout(ctx, !last) {
Some(correction) => self.apply_correction(correction),
None => return,
}
}
}
fn apply_correction(&mut self, correction: f64) {
if !correction.is_finite() {
return;
}
self.offset += correction;
self.core.drag_position += correction;
if self.core.controller_animating {
return;
}
let Some(velocity) = self
.core
.ballistic
.as_ref()
.map(|state| state.sim.dx(state.elapsed_secs(self.core.last_frame_time)))
else {
return;
};
let metrics = self.metrics();
match self
.core
.physics
.create_ballistic_simulation(&metrics, velocity)
{
Some(sim) => self.core.ballistic = Some(BallisticState { sim, start: None }),
None => self.core.stop_ballistic(),
}
}
fn attempt_layout(&mut self, ctx: &mut LayoutCtx, allow_correction: bool) -> Option<f64> {
let axis = self.axis;
let main = self.main_extent;
let cross = self.cross_extent;
let cache = self.cache_extent;
let pixels = self.layout_pixels();
let direction = self.user_scroll_direction();
let center_offset = -pixels;
let reverse_remaining_paint = center_offset.max(0.0).min(main);
let remaining_paint = (main - center_offset).max(0.0).min(main);
let mut scroll_offset = pixels.max(0.0);
let mut layout_offset = if center_offset >= main {
center_offset
} else {
reverse_remaining_paint
};
let initial_layout_offset = layout_offset;
let mut max_paint_offset = layout_offset + pixels.min(0.0);
let mut preceding = 0.0;
for (pod, slot) in self.children.iter_mut().zip(self.slots.iter_mut()) {
let sliver_scroll_offset = scroll_offset.max(0.0);
let cache_origin = (-cache).max(-sliver_scroll_offset);
let remaining_cache =
scroll_offset + main + cache - sliver_scroll_offset - cache_origin;
let sc = sanitized(SliverConstraints {
axis,
scroll_offset: sliver_scroll_offset,
preceding_scroll_extent: preceding,
overlap: max_paint_offset - layout_offset,
remaining_paint_extent: remaining_paint - layout_offset + initial_layout_offset,
cross_axis_extent: cross,
viewport_main_axis_extent: main,
cache_origin,
remaining_cache_extent: remaining_cache,
user_scroll_direction: direction,
});
let (geometry, is_box) = match pod.layout_sliver_child(ctx, &sc) {
Some(geometry) => (geometry, false),
None => {
let size = pod.layout_child(ctx, &sc.as_box_constraints());
(box_geometry(axis.main_extent(size), &sc), true)
}
};
if let Some(correction) = geometry.scroll_offset_correction {
if allow_correction {
return Some(correction);
}
debug_assert!(
false,
"CustomScrollView: a sliver still asked for a scroll-offset correction \
({correction}) after {MAX_LAYOUT_CYCLES} layout cycles"
);
}
let effective_offset = layout_offset + geometry.paint_origin;
let paint_offset = if geometry.visible || scroll_offset > 0.0 {
effective_offset
} else {
-scroll_offset + initial_layout_offset
};
let origin = axis_point(axis, paint_offset, 0.0);
pod.set_origin(if is_box {
origin + box_child_origin(&sc).to_vec2()
} else {
origin
});
*slot = Slot {
geometry,
paint_offset,
is_box,
};
max_paint_offset = (effective_offset + geometry.paint_extent).max(max_paint_offset);
scroll_offset -= geometry.scroll_extent;
preceding += geometry.scroll_extent;
layout_offset += geometry.layout_extent;
}
let total = finite_extent(preceding);
if main.is_finite() {
self.viewport_main = main;
self.max_extent = (total - main).max(0.0);
} else {
self.viewport_main = total;
self.max_extent = 0.0;
}
self.laid_out_pixels = pixels;
None
}
fn hits(&self, index: usize, position: Point) -> bool {
let Some(slot) = self.slots.get(index) else {
return false;
};
let main = along(self.axis, position);
let across = along(transpose_axis(self.axis), position);
let local = main - slot.paint_offset;
main >= 0.0
&& main < self.viewport_main
&& across >= 0.0
&& across < self.cross_extent
&& local >= 0.0
&& local < slot.geometry.hit_test_extent
}
fn route_to_slivers(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
if event.is_broadcast()
|| event.is_focus_routed()
|| self.children.iter().any(ChildPod::is_active)
{
return crate::authoring::route_event(&mut self.children, ctx, event);
}
let position = event.position();
let mut handled = EventResult::Ignored;
let mut kept_focus = None;
for index in 0..self.children.len() {
if self.hits(index, position)
&& self.children[index].event_child(ctx, event) == EventResult::Handled
{
if self.children[index].holds_live_focus() {
kept_focus = Some(index);
}
handled = EventResult::Handled;
break;
}
}
if matches!(event, InputEvent::Pointer(p) if p.phase == PointerPhase::Down) {
for (index, pod) in self.children.iter_mut().enumerate() {
if Some(index) != kept_focus && pod.is_focused() {
pod.set_focused(false);
}
}
}
handled
}
fn pointer_on_axis(
&mut self,
ctx: &mut EventCtx,
event: &InputEvent,
p: &PointerEvent,
t_ms: f64,
) -> EventResult {
let primary_down = p.phase == PointerPhase::Down && presses(p);
let mut restore = None;
if primary_down && let Some(host) = ambient_scroll_claim() {
match enclosing_viewport(&host) {
Some(frame) if frame.axis == self.axis => frame.inner_axis.set(Some(self.axis)),
Some(_) => restore = Some((host.get(), host)),
None if self.axis == ScrollAxis::Vertical => {}
None => restore = Some((host.get(), host)),
}
}
self.arming_down = primary_down;
if primary_down {
self.down_inner_axis = None;
}
let result = self.handle_pointer(ctx, event, p, t_ms);
self.arming_down = false;
if let Some((saved, host)) = restore {
host.set(saved);
}
if primary_down
&& self.core.inner_at_down.registered
&& self.down_inner_axis.unwrap_or(ScrollAxis::Vertical) != self.axis
{
self.core.inner_at_down = InnerScrollState::default();
}
result
}
fn event_at(&mut self, ctx: &mut EventCtx, event: &InputEvent, t_ms: f64) -> EventResult {
let result = self.route_event_at(ctx, event, t_ms);
self.publish_to_controller();
result
}
fn route_event_at(&mut self, ctx: &mut EventCtx, event: &InputEvent, t_ms: f64) -> EventResult {
if !matches!(
event,
InputEvent::Pointer(p) if p.phase == PointerPhase::Cancel
) {
self.deliver_pending_scroll(ctx);
}
match event {
InputEvent::Scroll { delta, .. } => self.handle_wheel(ctx, delta),
InputEvent::Pointer(p) => self.pointer_on_axis(ctx, event, p, t_ms),
_ => self.route_to_slivers(ctx, event),
}
}
fn stretch_transform(&self, origin: Point, size: Size) -> Option<Affine> {
match self.axis {
ScrollAxis::Vertical => self.core.stretch_transform(origin, size),
ScrollAxis::Horizontal => {
let swap = Affine::new([0.0, 1.0, 1.0, 0.0, 0.0, 0.0]);
self.core
.stretch_transform(transpose(origin), Size::new(size.height, size.width))
.map(|transform| swap * transform * swap)
}
}
}
}
fn sanitized(sc: SliverConstraints) -> SliverConstraints {
let not_nan = |value: f64| if value.is_nan() { 0.0 } else { value };
SliverConstraints {
scroll_offset: non_negative(sc.scroll_offset),
preceding_scroll_extent: not_nan(sc.preceding_scroll_extent),
overlap: not_nan(sc.overlap),
remaining_paint_extent: non_negative(sc.remaining_paint_extent),
cross_axis_extent: non_negative(sc.cross_axis_extent),
viewport_main_axis_extent: non_negative(sc.viewport_main_axis_extent),
cache_origin: not_nan(sc.cache_origin).min(0.0),
remaining_cache_extent: non_negative(sc.remaining_cache_extent),
..sc
}
}
fn along(axis: ScrollAxis, point: Point) -> f64 {
match axis {
ScrollAxis::Vertical => point.y,
ScrollAxis::Horizontal => point.x,
}
}
fn transpose_axis(axis: ScrollAxis) -> ScrollAxis {
match axis {
ScrollAxis::Vertical => ScrollAxis::Horizontal,
ScrollAxis::Horizontal => ScrollAxis::Vertical,
}
}
fn transpose(point: Point) -> Point {
Point::new(point.y, point.x)
}
struct AxisFrame {
claim: Rc<Cell<InnerScrollState>>,
axis: ScrollAxis,
inner_axis: Cell<Option<ScrollAxis>>,
}
thread_local! {
static VIEWPORT_FRAMES: RefCell<Vec<Rc<AxisFrame>>> = const { RefCell::new(Vec::new()) };
}
struct AxisFrameGuard;
impl Drop for AxisFrameGuard {
fn drop(&mut self) {
VIEWPORT_FRAMES.with(|frames| {
frames.borrow_mut().pop();
});
}
}
fn with_axis_frame<R>(frame: &Rc<AxisFrame>, f: impl FnOnce() -> R) -> R {
VIEWPORT_FRAMES.with(|frames| frames.borrow_mut().push(Rc::clone(frame)));
let _guard = AxisFrameGuard;
f()
}
fn enclosing_viewport(host: &Rc<Cell<InnerScrollState>>) -> Option<Rc<AxisFrame>> {
VIEWPORT_FRAMES.with(|frames| {
frames
.borrow()
.last()
.filter(|frame| Rc::ptr_eq(&frame.claim, host))
.cloned()
})
}
impl ScrollSurface for ViewportWidget {
fn core(&self) -> &ScrollSurfaceCore {
&self.core
}
fn core_mut(&mut self) -> &mut ScrollSurfaceCore {
&mut self.core
}
fn max_scroll_extent(&self) -> f64 {
self.max_extent
}
fn viewport_extent(&self) -> f64 {
self.viewport_main
}
fn base_offset(&self) -> f64 {
self.offset
}
fn set_base_offset(&mut self, value: f64) {
self.set_offset(value);
}
fn pixels(&self) -> f64 {
self.offset
}
fn displacement(&self) -> f64 {
self.offset - self.offset.clamp(0.0, self.max_extent)
}
fn set_allowed_position(&mut self, allowed: f64) {
self.offset = allowed;
}
fn decay_displacement(&mut self, retained: f64) {
let target = self.offset.clamp(0.0, self.max_extent);
let remaining = target - self.offset;
self.offset = target - remaining * retained;
}
fn settle_to_edge(&mut self) {
self.set_offset(self.offset);
}
fn sync_origins(&mut self) {}
fn scroll_info(&self) -> ScrollInfo {
ScrollInfo {
offset: self.offset.clamp(0.0, self.max_extent),
max_offset: self.max_extent,
overscroll: self.displacement(),
}
}
fn forward_pointer(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
if !std::mem::take(&mut self.arming_down) {
return self.route_to_slivers(ctx, event);
}
let Some(claim) = ambient_scroll_claim() else {
return self.route_to_slivers(ctx, event);
};
let frame = Rc::new(AxisFrame {
claim,
axis: self.axis,
inner_axis: Cell::new(None),
});
let result = with_axis_frame(&frame, || self.route_to_slivers(ctx, event));
self.down_inner_axis = frame.inner_axis.get();
result
}
fn take_over_from_content(&mut self, ctx: &mut EventCtx, position: Point) {
let cancel = InputEvent::Pointer(PointerEvent {
phase: PointerPhase::Cancel,
position,
button: PointerButton::Primary,
});
if let Some(pod) = self.children.iter_mut().find(|pod| pod.is_active()) {
pod.event_child(ctx, &cancel);
ctx.release_captured_child(pod);
} else {
crate::authoring::route_event(&mut self.children, ctx, &cancel);
}
}
}
impl<State: 'static> View<State> for CustomScrollView<State> {
type Element = ViewportWidget;
fn build(&self, ctx: &mut BuildCtx<'_>) -> ViewportWidget {
let children = self
.slivers
.iter()
.map(|view| crate::authoring::build_child(view, ctx))
.collect();
let mut widget = ViewportWidget::new(children, self.axis, self.cache_extent);
widget.core.on_scroll = self
.on_scroll
.as_ref()
.map(crate::authoring::erase_callback_arg);
widget.core.on_refresh_release = self
.on_refresh_release
.as_ref()
.map(crate::authoring::erase_callback);
widget.core.install(self.physics.as_ref(), self.effect);
widget.core.attach_controller(self.controller.as_ref());
widget
}
fn rebuild(
&self,
prev: &Self,
element: &mut ViewportWidget,
ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
element.core.on_scroll = self
.on_scroll
.as_ref()
.map(crate::authoring::erase_callback_arg);
element.core.on_refresh_release = self
.on_refresh_release
.as_ref()
.map(crate::authoring::erase_callback);
element.core.install(self.physics.as_ref(), self.effect);
let mut flags = crate::authoring::rebuild_children(
&prev.slivers,
&self.slivers,
&mut element.children,
ctx,
|view| view,
|_| None,
);
element
.slots
.resize(element.children.len(), Slot::default());
let cache_extent = finite_extent(self.cache_extent);
if element.axis != self.axis || element.cache_extent != cache_extent {
element.axis = self.axis;
element.core.set_axis(self.axis);
element.cache_extent = cache_extent;
flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
}
if element.core.attach_controller(self.controller.as_ref()) {
flags |= ChangeFlags::PAINT;
}
if element.core.has_pending_commands() {
flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
}
flags
}
fn teardown(&self, element: &mut ViewportWidget, ctx: &mut BuildCtx<'_>) {
for (view, pod) in self.slivers.iter().zip(element.children.iter_mut()) {
crate::authoring::teardown_child(view, pod, ctx);
}
}
}
impl Widget for ViewportWidget {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
let max = bc.max();
self.cross_extent = finite_extent(self.axis.cross_extent(max));
self.main_extent = non_negative(self.axis.main_extent(max));
self.observe_direction();
self.layout_slivers(ctx);
let core = &self.core;
if !core.scrolling && !core.settling && core.ballistic.is_none() {
self.set_offset(self.offset);
if self.layout_pixels() != self.laid_out_pixels {
self.layout_slivers(ctx);
}
}
self.core.laid_out = true;
let reduce_motion = Theme::from_layout_ctx(ctx)
.map(|t| t.motion.reduce_motion)
.unwrap_or(false);
self.drain_controller(reduce_motion);
if self.layout_pixels() != self.laid_out_pixels {
self.layout_slivers(ctx);
}
self.publish_to_controller();
self.viewport = self.axis.size(self.viewport_main, self.cross_extent);
bc.constrain(self.viewport)
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
let reduce_motion = Theme::from_paint_ctx(ctx)
.map(|t| t.motion.reduce_motion)
.unwrap_or(false);
self.paint_motion(ctx, reduce_motion);
if self.layout_pixels() != self.laid_out_pixels {
ctx.request_layout();
}
let origin = ctx.origin();
scene.push_clip(origin, ctx.size());
ctx.constrain_visible_rect(Rect::from_origin_size(origin, ctx.size()));
let stretch = self.stretch_transform(origin, ctx.size());
if let Some(transform) = stretch {
scene.push_transform(transform);
}
let axis = self.axis;
let cross = self.cross_extent;
for (pod, slot) in self.children.iter_mut().zip(&self.slots).rev() {
if !slot.geometry.visible {
continue;
}
let clip = slot.is_box && slot.geometry.has_visual_overflow;
if clip {
let start = origin + axis_point(axis, slot.paint_offset, 0.0).to_vec2();
scene.push_clip(start, axis.size(slot.geometry.paint_extent, cross));
}
pod.paint_child(ctx, scene);
if clip {
scene.pop_clip();
}
}
if stretch.is_some() {
scene.pop_transform();
}
scene.pop_clip();
}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
let t = self.core.event_time_ms();
self.event_at(ctx, event, t)
}
fn semantics(&self, ctx: &mut SemanticsCtx) {
let offset = self.offset;
let max_offset = self.max_extent;
let axis = self.axis;
ctx.push_container(
Role::ScrollView,
move |node| match axis {
ScrollAxis::Vertical => {
node.set_scroll_y(offset);
node.set_scroll_y_min(0.0);
node.set_scroll_y_max(max_offset);
}
ScrollAxis::Horizontal => {
node.set_scroll_x(offset);
node.set_scroll_x_min(0.0);
node.set_scroll_x_max(max_offset);
}
},
|ctx| {
for (pod, slot) in self.children.iter().zip(&self.slots) {
if slot.geometry.visible || slot.geometry.cache_extent > 0.0 {
pod.semantics_child(ctx);
}
}
},
);
}
crate::authoring::visit_children!(children);
}
#[cfg(test)]
mod tests {
use super::*;
use frust_core::{FrameTime, RenderRoot};
use std::any::Any;
use crate::test_support::{RecordingScene, leaf};
const WINDOW: Size = Size::new(400.0, 500.0);
fn widget(root: &RenderRoot<(), CustomScrollView<()>>) -> &ViewportWidget {
let id = root.root_id().expect("root built");
(root.tree().pod(id).expect("root pod").widget() as &dyn Any)
.downcast_ref::<ViewportWidget>()
.expect("root is a ViewportWidget")
}
fn mount(
logic: &mut impl FnMut(&mut ()) -> CustomScrollView<()>,
) -> RenderRoot<(), CustomScrollView<()>> {
let mut root = RenderRoot::new();
root.rebuild(logic, &mut ());
root.layout(WINDOW);
root.paint(&mut RecordingScene::default(), FrameTime::ZERO);
root
}
#[test]
fn sanitising_drops_nan_and_negative_extents() {
let sc = sanitized(SliverConstraints {
axis: ScrollAxis::Vertical,
scroll_offset: f64::NAN,
preceding_scroll_extent: f64::NAN,
overlap: f64::NAN,
remaining_paint_extent: -3.0,
cross_axis_extent: f64::NAN,
viewport_main_axis_extent: -1.0,
cache_origin: 5.0,
remaining_cache_extent: f64::NAN,
user_scroll_direction: ScrollDirection::Idle,
});
sc.debug_assert_valid();
assert_eq!(sc.remaining_paint_extent, 0.0);
assert_eq!(sc.cache_origin, 0.0);
}
#[test]
fn a_zero_window_lays_out_without_tripping_the_constraint_checks() {
let mut logic = |_: &mut ()| custom_scroll_view((leaf(100.0, 100.0), leaf(100.0, 50.0)));
let mut root = RenderRoot::new();
root.rebuild(&mut logic, &mut ());
root.layout(Size::ZERO);
let w = widget(&root);
assert_eq!(w.max_offset(), 150.0);
assert!(!w.sliver_geometry(0).expect("child 0").visible);
}
#[test]
fn an_unbounded_main_axis_shrink_wraps() {
let mut logic = |_: &mut ()| custom_scroll_view((leaf(100.0, 100.0), leaf(100.0, 50.0)));
let mut root = RenderRoot::new();
root.rebuild(&mut logic, &mut ());
root.layout(Size::new(400.0, f64::INFINITY));
let w = widget(&root);
assert_eq!(w.viewport.height, 150.0);
assert_eq!(w.max_offset(), 0.0);
assert_eq!(w.sliver_geometry(1).expect("child 1").paint_extent, 50.0);
}
#[test]
fn plain_box_children_are_adapted_and_placed_in_sequence() {
let mut logic = |_: &mut ()| {
custom_scroll_view((leaf(400.0, 300.0), leaf(400.0, 300.0), leaf(400.0, 300.0)))
.cache_extent(0.0)
};
let root = mount(&mut logic);
let w = widget(&root);
assert_eq!(w.max_offset(), 400.0);
let g = |i| w.sliver_geometry(i).expect("child");
assert_eq!(g(0).paint_extent, 300.0);
assert_eq!(g(1).paint_extent, 200.0);
assert!(g(1).has_visual_overflow);
assert!(!g(2).visible);
assert_eq!(w.slots[1].paint_offset, 300.0);
assert!(w.slots.iter().all(|slot| slot.is_box));
}
}