Skip to main content

frust_widgets/slivers/
fill_remaining.rs

1//! `sliver_fill_remaining`: one box child sized to fill what is left of the
2//! viewport — Flutter's `SliverFillRemaining` (`rendering/sliver_fill.dart`).
3//! Meant as the last sliver of a [`CustomScrollView`](crate::CustomScrollView).
4//!
5//! Two modes, picked by [`SliverFillRemainingView::has_scroll_body`]:
6//!
7//! * **With a scroll body** (the default, Flutter's
8//!   `RenderSliverFillRemainingWithScrollable`): the child is laid out tight
9//!   to the remaining paint extent (less a negative overlap), and the sliver
10//!   scrolls by a whole viewport extent — so the slivers before it can scroll
11//!   away and the child grows to the full viewport as they do. A child placed
12//!   beyond the paint space but inside the cache area is laid out up to the
13//!   cached extent instead of at zero, so it is ready when it scrolls in.
14//! * **Without** (`has_scroll_body(false)`, Flutter's
15//!   `RenderSliverFillRemaining`): the child fills the viewport extent the
16//!   slivers before it leave (`viewport_main_axis_extent −
17//!   preceding_scroll_extent`, never negative), or its own extent when that is
18//!   larger. The sliver's scroll extent is that extent, so it adds scrolling
19//!   only when the child is taller than the space left. Where Flutter asks the
20//!   child for its max intrinsic extent, the child here is laid out once with
21//!   the space left as its *minimum* extent and an unbounded maximum, and the
22//!   larger of the two is kept.
23//!
24//! Either way the child is placed at `−scroll_offset` and clipped to the paint
25//! extent when cut, exactly as [`sliver_box`](crate::sliver_box) places its
26//! child. Under a shrink-wrapping viewport (an infinite remaining paint
27//! extent) there is no remainder to fill and the child keeps its own extent.
28//! Laid out as a box itself (outside a viewport), the sliver is transparent:
29//! the child is laid out under the incoming constraints.
30
31use frust_core::{
32    AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent,
33    LayoutCtx, PaintCtx, PaintScene, SemanticsCtx, SliverConstraints, SliverGeometry, SliverWidget,
34    View, Widget, any,
35};
36use kurbo::{Point, Size};
37
38use super::adapter::{box_child_origin, box_geometry, finite_extent};
39use super::{cache_offset, paint_offset};
40
41/// A box child filling the rest of the viewport. See the [module docs](self).
42pub struct SliverFillRemainingView<State: 'static> {
43    child: AnyView<State>,
44    has_scroll_body: bool,
45}
46
47/// Fill what is left of the viewport with `child` — with a scroll body by
48/// default; see [`SliverFillRemainingView::has_scroll_body`] and the
49/// [module docs](self).
50///
51/// ```
52/// use frust_widgets::{CustomScrollView, custom_scroll_view, sliver_fill_remaining, text};
53/// let view: CustomScrollView<()> = custom_scroll_view((
54///     text("header"),
55///     sliver_fill_remaining(text("the rest")).has_scroll_body(false),
56/// ));
57/// # let _ = view;
58/// ```
59pub fn sliver_fill_remaining<State: 'static, V: View<State>>(
60    child: V,
61) -> SliverFillRemainingView<State> {
62    SliverFillRemainingView {
63        child: any(child),
64        has_scroll_body: true,
65    }
66}
67
68impl<State: 'static> SliverFillRemainingView<State> {
69    /// Whether the child is (or holds) its own scrollable body — `true` by
70    /// default. `true` sizes the child to exactly the remaining paint extent
71    /// and scrolls the sliver by a whole viewport; `false` sizes it to the
72    /// space the earlier slivers leave, or its own extent when larger, and
73    /// scrolls only by that. See the [module docs](self).
74    pub fn has_scroll_body(mut self, has_scroll_body: bool) -> Self {
75        self.has_scroll_body = has_scroll_body;
76        self
77    }
78}
79
80/// The retained widget for a [`SliverFillRemainingView`]: the child's pod,
81/// the mode, and whether the last sliver layout cut the child (so paint
82/// clips it).
83pub struct SliverFillRemainingWidget {
84    child: ChildPod,
85    has_scroll_body: bool,
86    /// Whether the last layout left part of the child outside this sliver's
87    /// paint extent. Always `false` after a plain box layout.
88    clip: bool,
89}
90
91impl<State: 'static> View<State> for SliverFillRemainingView<State> {
92    type Element = SliverFillRemainingWidget;
93
94    fn build(&self, ctx: &mut BuildCtx<'_>) -> SliverFillRemainingWidget {
95        SliverFillRemainingWidget {
96            child: crate::authoring::build_child(&self.child, ctx),
97            has_scroll_body: self.has_scroll_body,
98            clip: false,
99        }
100    }
101
102    fn rebuild(
103        &self,
104        prev: &Self,
105        element: &mut SliverFillRemainingWidget,
106        ctx: &mut BuildCtx<'_>,
107    ) -> ChangeFlags {
108        let mut flags =
109            crate::authoring::rebuild_child(&prev.child, &self.child, &mut element.child, ctx);
110        if element.has_scroll_body != self.has_scroll_body {
111            element.has_scroll_body = self.has_scroll_body;
112            flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
113        }
114        flags
115    }
116
117    fn teardown(&self, element: &mut SliverFillRemainingWidget, ctx: &mut BuildCtx<'_>) {
118        crate::authoring::teardown_child(&self.child, &mut element.child, ctx);
119    }
120}
121
122impl SliverFillRemainingWidget {
123    /// The scroll-body mode: the child tight to the remaining paint extent,
124    /// the sliver scrolling by a whole viewport. `None` when there is no
125    /// finite remainder to fill (a shrink-wrapping viewport).
126    fn layout_scroll_body(
127        &mut self,
128        ctx: &mut LayoutCtx,
129        sc: &SliverConstraints,
130    ) -> Option<SliverGeometry> {
131        let extent = sc.remaining_paint_extent - sc.overlap.min(0.0);
132        if !extent.is_finite() {
133            return None;
134        }
135        let viewport = finite_extent(sc.viewport_main_axis_extent);
136        let cache = cache_offset(sc, 0.0, viewport);
137        // Beyond the paint space but inside the cache area: lay the child out
138        // up to the cached extent, so it is ready when it scrolls in.
139        let max_extent = if extent == 0.0 && cache > 0.0 {
140            cache
141        } else {
142            extent
143        };
144        let axis = sc.axis;
145        let cross = sc.cross_axis_extent;
146        self.child.layout_child(
147            ctx,
148            &BoxConstraints::new(axis.size(extent, cross), axis.size(max_extent, cross)),
149        );
150        let paint_extent = paint_offset(sc, 0.0, extent);
151        Some(SliverGeometry {
152            cache_extent: cache.max(paint_extent),
153            has_visual_overflow: extent > sc.remaining_paint_extent || sc.scroll_offset > 0.0,
154            ..SliverGeometry::new(viewport, paint_extent, paint_extent)
155        })
156    }
157
158    /// The no-scroll-body mode: the child fills the space the earlier slivers
159    /// leave, or its own extent when that is larger.
160    fn layout_remainder(&mut self, ctx: &mut LayoutCtx, sc: &SliverConstraints) -> SliverGeometry {
161        let axis = sc.axis;
162        let cross = sc.cross_axis_extent;
163        let remainder = finite_extent(sc.viewport_main_axis_extent - sc.preceding_scroll_extent);
164        let size = self.child.layout_child(
165            ctx,
166            &BoxConstraints::new(axis.size(remainder, cross), axis.size(f64::INFINITY, cross)),
167        );
168        box_geometry(finite_extent(axis.main_extent(size)).max(remainder), sc)
169    }
170}
171
172impl Widget for SliverFillRemainingWidget {
173    /// Outside a viewport there is nothing to fill: the child is laid out
174    /// under `bc` at the origin, uncut.
175    fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
176        let size = self.child.layout_child(ctx, bc);
177        self.child.set_origin(Point::ZERO);
178        self.clip = false;
179        size
180    }
181
182    fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
183        if self.clip {
184            scene.push_clip(ctx.origin(), ctx.size());
185        }
186        self.child.paint_child(ctx, scene);
187        if self.clip {
188            scene.pop_clip();
189        }
190    }
191
192    /// The viewport only delivers a positional event inside this sliver's
193    /// paint extent (or to a captured child), so the ordinary single-child
194    /// routing applies.
195    fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
196        crate::authoring::route_event_single(&mut self.child, ctx, event)
197    }
198
199    fn semantics(&self, ctx: &mut SemanticsCtx) {
200        self.child.semantics_child(ctx);
201    }
202
203    fn as_sliver(&mut self) -> Option<&mut dyn SliverWidget> {
204        Some(self)
205    }
206
207    crate::authoring::visit_children!(child);
208}
209
210impl SliverWidget for SliverFillRemainingWidget {
211    fn layout_sliver(&mut self, ctx: &mut LayoutCtx, sc: &SliverConstraints) -> SliverGeometry {
212        let filled = if self.has_scroll_body {
213            self.layout_scroll_body(ctx, sc)
214        } else {
215            Some(self.layout_remainder(ctx, sc))
216        };
217        let geometry = filled.unwrap_or_else(|| {
218            // Nothing finite to fill: the child keeps its own extent.
219            let size = self.child.layout_child(ctx, &sc.as_box_constraints());
220            box_geometry(sc.axis.main_extent(size), sc)
221        });
222        self.child.set_origin(box_child_origin(sc));
223        self.clip = geometry.has_visual_overflow;
224        geometry
225    }
226}