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}