Skip to main content

frust_widgets/slivers/
keep_alive.rs

1//! `keep_alive`: keeps a lazy list's row alive when it scrolls out of the
2//! list's window — Flutter's `KeepAlive` / `AutomaticKeepAlive`
3//! (`widgets/automatic_keep_alive.dart`), answered by
4//! [`sliver_list`](crate::sliver_list).
5//!
6//! # The request
7//!
8//! A row asks to be kept through a [`KeepAliveHandle`]: one acquired while the
9//! list builds or rebuilds that row ([`KeepAliveHandle::acquire`]) binds to
10//! the row, whatever depth of its subtree acquired it, and the row is kept
11//! while any handle bound to it holds a request ([`KeepAliveHandle::set`]).
12//! [`keep_alive`] is the declarative form — a transparent wrapper holding one
13//! handle, requesting while [`KeepAliveView::enabled`] (the default) — and a
14//! custom widget that owns a resource (a player, a socket, a long animation)
15//! acquires its own handle in [`View::build`] instead. Dropping a handle, or
16//! tearing down the widget holding it, withdraws its request.
17//!
18//! The binding is a scope that only [`sliver_list`](crate::sliver_list) opens
19//! around each row it builds or rebuilds. A `ListView`,
20//! `sliver_fixed_extent_list` or `sliver_grid` opens none, so a request inside
21//! one of their rows, nested in a `sliver_list` row, binds to the nearest
22//! enclosing `sliver_list` row and pins that whole row. Outside every
23//! `sliver_list` there is no scope, the handle is `None`, and `keep_alive` is
24//! inert.
25//!
26//! Parked rows are uncapped: memory grows with the rows scrolled past while
27//! they stay kept alive.
28//!
29//! # What the list does with it
30//!
31//! A row leaving the list's window while its request holds — and whose key the
32//! data still holds — is parked in a side map keyed by its [`ChildKey`]
33//! instead of being torn down: not laid out, painted, hit tested or visited,
34//! its pod and every bit of retained state untouched. When
35//! its key re-enters the window the parked pod is re-adopted and rebuilt to the
36//! current view. A gesture the row had captured is cancelled as it is parked
37//! (nothing would ever route its end to it); a focus link it holds stays
38//! recorded but receives nothing while parked. A parked row whose key leaves
39//! the data is torn down, and the list tears every parked row down with
40//! itself.
41//!
42//! [`ChildKey`]: crate::ChildKey
43
44use std::any::Any;
45use std::cell::{Cell, RefCell};
46use std::rc::Rc;
47
48use frust_core::{
49    AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent,
50    LayoutCtx, PaintCtx, PaintScene, SemanticsCtx, View, Widget, any,
51};
52use kurbo::{Point, Size};
53
54/// How many keep-alive requests one row's handles hold.
55type Requests = Rc<Cell<u32>>;
56
57thread_local! {
58    /// The rows currently being built or rebuilt, innermost last — the scopes
59    /// a [`KeepAliveHandle`] binds to. UI-thread-affine like the build pass.
60    static ROW_SCOPES: RefCell<Vec<Requests>> = const { RefCell::new(Vec::new()) };
61}
62
63/// Pops [`ROW_SCOPES`] on drop, so an unwinding build cannot leave a stale
64/// scope behind.
65struct ScopeGuard;
66
67impl Drop for ScopeGuard {
68    fn drop(&mut self) {
69        ROW_SCOPES.with(|scopes| {
70            scopes.borrow_mut().pop();
71        });
72    }
73}
74
75/// Run `f` with `requests` as the innermost row scope.
76fn with_row_scope<R>(requests: &Requests, f: impl FnOnce() -> R) -> R {
77    ROW_SCOPES.with(|scopes| scopes.borrow_mut().push(Rc::clone(requests)));
78    let _guard = ScopeGuard;
79    f()
80}
81
82/// A keep-alive request bound to the lazy-list row it was acquired in. See
83/// the [module docs](self).
84pub struct KeepAliveHandle {
85    requests: Requests,
86    held: bool,
87}
88
89impl KeepAliveHandle {
90    /// A handle bound to the lazy-list row being built or rebuilt right now —
91    /// call it from [`View::build`] (or `rebuild`). `None` outside every lazy
92    /// list's row, where keep-alive means nothing. The handle starts holding no
93    /// request.
94    pub fn acquire() -> Option<Self> {
95        ROW_SCOPES
96            .with(|scopes| scopes.borrow().last().cloned())
97            .map(|requests| Self {
98                requests,
99                held: false,
100            })
101    }
102
103    /// Hold (`true`) or withdraw (`false`) this handle's request. Idempotent:
104    /// a handle counts once however often it is set.
105    pub fn set(&mut self, keep_alive: bool) {
106        if keep_alive == self.held {
107            return;
108        }
109        self.held = keep_alive;
110        let count = self.requests.get();
111        self.requests.set(if keep_alive {
112            count.saturating_add(1)
113        } else {
114            count.saturating_sub(1)
115        });
116    }
117
118    /// Whether this handle currently holds a request.
119    pub fn is_held(&self) -> bool {
120        self.held
121    }
122}
123
124impl Drop for KeepAliveHandle {
125    fn drop(&mut self) {
126        self.set(false);
127    }
128}
129
130/// Keep `child`'s lazy-list row alive outside the list's window (see the
131/// [module docs](self)). Transparent everywhere else: `child` is laid out,
132/// painted and routed to exactly as if it stood alone.
133///
134/// ```
135/// use frust_widgets::{
136///     ChildKey, CustomScrollView, custom_scroll_view, keep_alive, sliver_list, text,
137/// };
138/// let view: CustomScrollView<()> = custom_scroll_view(sliver_list(
139///     100,
140///     |i| ChildKey::new(i as u64),
141///     |i| keep_alive(text(format!("row {i}"))).enabled(i == 0),
142/// ));
143/// # let _ = view;
144/// ```
145pub fn keep_alive<State: 'static, V: View<State>>(child: V) -> KeepAliveView<State> {
146    KeepAliveView {
147        child: any(child),
148        enabled: true,
149    }
150}
151
152/// A keep-alive wrapper. See [`keep_alive`].
153pub struct KeepAliveView<State: 'static> {
154    child: AnyView<State>,
155    enabled: bool,
156}
157
158impl<State: 'static> KeepAliveView<State> {
159    /// Whether the wrapper requests keep-alive — `true` unless set. A row whose
160    /// request is withdrawn is torn down the next time it leaves the window.
161    pub fn enabled(mut self, enabled: bool) -> Self {
162        self.enabled = enabled;
163        self
164    }
165}
166
167/// The retained widget for a [`KeepAliveView`]: the child's pod and the handle
168/// bound to its row (`None` outside a lazy list).
169pub struct KeepAliveWidget {
170    child: ChildPod,
171    handle: Option<KeepAliveHandle>,
172}
173
174impl<State: 'static> View<State> for KeepAliveView<State> {
175    type Element = KeepAliveWidget;
176
177    fn build(&self, ctx: &mut BuildCtx<'_>) -> KeepAliveWidget {
178        let mut handle = KeepAliveHandle::acquire();
179        if let Some(handle) = handle.as_mut() {
180            handle.set(self.enabled);
181        }
182        KeepAliveWidget {
183            child: crate::authoring::build_child(&self.child, ctx),
184            handle,
185        }
186    }
187
188    fn rebuild(
189        &self,
190        prev: &Self,
191        element: &mut KeepAliveWidget,
192        ctx: &mut BuildCtx<'_>,
193    ) -> ChangeFlags {
194        if let Some(handle) = element.handle.as_mut() {
195            handle.set(self.enabled);
196        }
197        crate::authoring::rebuild_child(&prev.child, &self.child, &mut element.child, ctx)
198    }
199
200    fn teardown(&self, element: &mut KeepAliveWidget, ctx: &mut BuildCtx<'_>) {
201        if let Some(handle) = element.handle.as_mut() {
202            handle.set(false);
203        }
204        crate::authoring::teardown_child(&self.child, &mut element.child, ctx);
205    }
206}
207
208impl Widget for KeepAliveWidget {
209    fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
210        let size = self.child.layout_child(ctx, bc);
211        self.child.set_origin(Point::ZERO);
212        size
213    }
214
215    fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
216        self.child.paint_child(ctx, scene);
217    }
218
219    fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
220        crate::authoring::route_event_single(&mut self.child, ctx, event)
221    }
222
223    fn semantics(&self, ctx: &mut SemanticsCtx) {
224        self.child.semantics_child(ctx);
225    }
226
227    crate::authoring::visit_children!(child);
228}
229
230// --- The row scope a lazy list wraps each row in --------------------------------
231
232/// A lazy list's row: `child` built and rebuilt inside the row's keep-alive
233/// scope, so a [`KeepAliveHandle`] acquired anywhere in it binds to the row.
234pub(crate) struct RowScope<V> {
235    child: V,
236}
237
238impl<V> RowScope<V> {
239    pub(crate) fn new(child: V) -> Self {
240        Self { child }
241    }
242}
243
244/// The retained widget for a [`RowScope`]: the row's pod and the requests its
245/// handles hold. A transparent single-child box.
246pub(crate) struct RowScopeWidget {
247    child: ChildPod,
248    requests: Requests,
249}
250
251impl<State: 'static, V: View<State>> View<State> for RowScope<V> {
252    type Element = RowScopeWidget;
253
254    fn build(&self, ctx: &mut BuildCtx<'_>) -> RowScopeWidget {
255        let requests = Requests::default();
256        let child = with_row_scope(&requests, || {
257            crate::authoring::build_child(&self.child, ctx)
258        });
259        RowScopeWidget { child, requests }
260    }
261
262    fn rebuild(
263        &self,
264        prev: &Self,
265        element: &mut RowScopeWidget,
266        ctx: &mut BuildCtx<'_>,
267    ) -> ChangeFlags {
268        let requests = Rc::clone(&element.requests);
269        with_row_scope(&requests, || {
270            crate::authoring::rebuild_child(&prev.child, &self.child, &mut element.child, ctx)
271        })
272    }
273
274    fn teardown(&self, element: &mut RowScopeWidget, ctx: &mut BuildCtx<'_>) {
275        crate::authoring::teardown_child(&self.child, &mut element.child, ctx);
276    }
277}
278
279impl Widget for RowScopeWidget {
280    fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
281        let size = self.child.layout_child(ctx, bc);
282        self.child.set_origin(Point::ZERO);
283        size
284    }
285
286    fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
287        self.child.paint_child(ctx, scene);
288    }
289
290    fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
291        crate::authoring::route_event_single(&mut self.child, ctx, event)
292    }
293
294    fn semantics(&self, ctx: &mut SemanticsCtx) {
295        self.child.semantics_child(ctx);
296    }
297
298    crate::authoring::visit_children!(child);
299}
300
301/// Whether the row `pod` holds — a [`RowScope`] built through an `AnyView` or
302/// directly — currently asks to be kept alive.
303pub(crate) fn row_wants_keep_alive(pod: &ChildPod) -> bool {
304    let widget: &dyn Any = pod.widget();
305    let scope = match widget.downcast_ref::<Box<dyn Widget>>() {
306        Some(boxed) => {
307            let inner: &dyn Any = &**boxed;
308            inner.downcast_ref::<RowScopeWidget>()
309        }
310        None => widget.downcast_ref::<RowScopeWidget>(),
311    };
312    scope.is_some_and(|scope| scope.requests.get() > 0)
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318
319    #[test]
320    fn a_handle_binds_to_the_innermost_row_and_counts_once() {
321        assert!(KeepAliveHandle::acquire().is_none(), "no row, no handle");
322        let outer = Requests::default();
323        let inner = Requests::default();
324        let (mut a, mut b) = with_row_scope(&outer, || {
325            let a = KeepAliveHandle::acquire().expect("in a row");
326            let b = with_row_scope(&inner, || KeepAliveHandle::acquire().expect("nested row"));
327            (a, b)
328        });
329        assert!(KeepAliveHandle::acquire().is_none(), "scopes closed");
330        a.set(true);
331        a.set(true);
332        b.set(true);
333        assert_eq!((outer.get(), inner.get()), (1, 1));
334        a.set(false);
335        assert_eq!(outer.get(), 0);
336        drop(b);
337        assert_eq!(inner.get(), 0, "dropping a handle withdraws its request");
338        assert!(!a.is_held());
339    }
340}