Skip to main content

frust_widgets/
repaint_boundary.rs

1//! Retained-page boundaries: [`repaint_boundary(child)`](repaint_boundary) and
2//! the boundary state its adopters (the navigator's pages, the pattern
3//! switcher's children, a [`.cached(true)`](crate::motion::AnimatedOpacityView::cached)
4//! `AnimatedOpacity`) share.
5//!
6//! # What a boundary paints
7//!
8//! A boundary paints its child's body into a page the renderer keeps across
9//! frames (a `PushRetained`/`PopRetained` bracket keyed by the boundary's own
10//! [`RetainedKey`]) and, on a later frame where nothing under the child changed
11//! and the renderer's resident-key ledger still holds that page, composites the
12//! page (`DrawRetained`) instead of painting the child again. Per frame:
13//!
14//! - **Draw the page** when the child's subtree is clean
15//!   ([`PaintCtx::subtree_clean`]), the boundary recorded its page under the
16//!   linear part (scale/rotation/skew) the scene is at now, the body's last
17//!   paint used no paint-time side channel ([`PaintCtx::paint_publications`]),
18//!   the ledger reports the key resident at the extent and scale the page
19//!   would be drawn at ([`PaintCtx::retained_available`]), and the ledger
20//!   proves the page holds the boundary's latest recording (see *Which
21//!   recording the page holds* below). The child is not painted; its pod stays
22//!   clean.
23//! - **Record the page** otherwise, when recording can pay off: the body is
24//!   bracketed and painted.
25//! - **Paint the body directly** (a `push_layer` when the composite alpha is
26//!   below 1) when retained layers are off or no ledger was threaded
27//!   ([`PaintCtx::retained_enabled`] — the `FRUST_NO_RETAINED_LAYERS` kill
28//!   switch), or the caller says the body cannot be recorded this frame.
29//!
30//! # Composite parameters
31//!
32//! The bracket's `scale` is always `1.0`: a page is served only at the linear
33//! part it was rasterized at, so asking for one at another scale would record
34//! a new page every frame and gain nothing. A boundary composites at
35//! [`alpha`](RepaintBoundaryView::alpha) and at its own origin; a page drawn at
36//! another origin than it was recorded at is moved through a translation
37//! transform, which the renderer rounds to whole device pixels (a fractional
38//! slide sits up to half a pixel off the inline frame). A container whose
39//! transition step has a scale component paints that frame inline instead.
40//!
41//! # Staleness
42//!
43//! "Clean" means unchanged since the body was last *painted*, not since it was
44//! last *recorded*, so every paint of the body outside the bracket while it was
45//! dirty forgets the recorded page: the next draw must follow a fresh record.
46//! An adopter therefore paints its child only through its boundary
47//! ([`RetainedBoundary::paint`] or [`RetainedBoundary::paint_plain`]).
48//!
49//! # Which recording the page holds
50//!
51//! A resident page is not necessarily the boundary's latest recording: the
52//! scene carrying that recording can be dropped on its way to the renderer
53//! (the render split's scene slot is latest-wins), leaving the previous page
54//! resident under the same key. The ledger's
55//! [`frame`](frust_core::RetainedLedger::frame) — the paint serial
56//! ([`PaintCtx::paint_serial`]) of the last scene the renderer committed — is
57//! what tells them apart, and a boundary keeps a **run**: the serials of the
58//! consecutive paints, starting at `serial`, that each recorded the current
59//! body at the same origin, size and linear part. Every scene in a run carries
60//! the same recording, so a ledger whose frame falls inside the run proves the
61//! page holds it — the page is then *confirmed* and may be drawn until the body
62//! changes. A recording at another origin starts a new run: a body the parent
63//! moved between two recordings must not be vouched for by a ledger that only
64//! describes the earlier one.
65//!
66//! Until then the page is drawn only when the ledger covers the run's start
67//! **and** the boundary recorded on the immediately previous paint (so no
68//! scene after the run, which might have committed without the recording,
69//! can be the one the ledger describes). Otherwise it records again: on the
70//! next paint after a dirty one, a clean body that stays put continues the
71//! run, so a ledger that lags the paints by a frame or two still lands inside
72//! it — a resting boundary never records forever behind a renderer that is
73//! merely late. A body its parent moves on every paint restarts its run each
74//! time, so it is confirmed only once the ledger lags by at most one paint;
75//! until then it records every frame (correct pixels, no page reuse).
76//! A context no render root numbered (serial `0`, a widget unit test) skips
77//! the serial check.
78//!
79//! See `docs/WIDGETS_ARCHITECTURE.md` for where boundaries sit in the widget set
80//! and the renderer contract in `frust-engine`'s `cache::retained` module docs.
81
82use frust_core::{
83    BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent, LayoutCtx,
84    PaintCtx, PaintScene, RetainedKey, SemanticsCtx, View, Widget,
85};
86use kurbo::{Affine, Point, Rect, Size};
87
88/// How far apart two linear parts may be and still be read as one — the
89/// renderer's own tolerance for serving a page (`RetainedPlacement::LINEAR_EPSILON`
90/// in `frust-engine`), tighter than the ledger's scale tolerance, so a page is
91/// never drawn under a linear part the renderer would refuse.
92const LINEAR_EPSILON: f64 = 1e-6;
93
94/// How a [`RetainedBoundary`] painted its body this frame.
95#[derive(Clone, Copy, Debug, PartialEq, Eq)]
96pub(crate) enum BoundaryPaint {
97    /// Composited the retained page; the body was not painted.
98    Retained,
99    /// Painted the body inside a `PushRetained`/`PopRetained` bracket.
100    Recorded,
101    /// Painted the body with no bracket.
102    Direct,
103}
104
105/// What a boundary remembers of the page it last recorded.
106#[derive(Clone, Copy, Debug, PartialEq)]
107struct Recorded {
108    /// The scene's linear part (`[a, b, c, d]`) the bracket was recorded under.
109    linear: [f64; 4],
110    /// The bracket rectangle's origin (the body's paint origin).
111    origin: Point,
112    /// The bracket rectangle's size.
113    size: Size,
114    /// The paint serial the run of recordings of this body started at — the
115    /// first of the consecutive paints that each recorded it at this origin,
116    /// size and linear part (see the [module docs](self)).
117    serial: u64,
118    /// The paint serial of the latest recording in the run.
119    last: u64,
120    /// Whether a ledger has proven that the renderer holds this recording.
121    confirmed: bool,
122}
123
124impl Recorded {
125    /// Whether the page may still be vouched for at paint `serial`: confirmed,
126    /// recorded on the immediately previous paint, or painted with no serial
127    /// at all.
128    fn usable_at(&self, serial: u64) -> bool {
129        self.confirmed || serial == 0 || self.last.wrapping_add(1) == serial
130    }
131}
132
133/// The per-owner state of one retained-page boundary: its key, the placement
134/// of the page it last recorded, and whether its body uses a paint-time side
135/// channel. One per navigator page, per switcher child, per cached
136/// `AnimatedOpacity`, per [`RepaintBoundaryWidget`]. See the
137/// [module docs](self).
138#[derive(Debug)]
139pub(crate) struct RetainedBoundary {
140    key: RetainedKey,
141    recorded: Option<Recorded>,
142    publishes: bool,
143}
144
145impl RetainedBoundary {
146    /// A boundary with a freshly minted, process-unique key.
147    pub(crate) fn new() -> Self {
148        Self::with_key(RetainedKey::mint())
149    }
150
151    /// A boundary drawing on `key` (an owner that reserved a key range).
152    pub(crate) fn with_key(key: RetainedKey) -> Self {
153        Self {
154            key,
155            recorded: None,
156            publishes: false,
157        }
158    }
159
160    /// The key this boundary records and draws its page under.
161    pub(crate) fn key(&self) -> RetainedKey {
162        self.key
163    }
164
165    /// Paint `pod`'s body through the boundary at `origin`/`size` (absolute
166    /// paint coordinates — the body's own paint origin) composited at `alpha`
167    /// (clamped to `[0, 1]`): draw the retained page, record it, or paint the
168    /// body directly — see the [module docs](self). `may_record` is the
169    /// caller's word that the body sits wholly inside every clip around it this
170    /// frame (a renderer never retains a clipped body, so bracketing one is
171    /// wasted work); a scroll ancestor's visible rect is checked here as well.
172    #[allow(clippy::too_many_arguments)]
173    pub(crate) fn paint(
174        &mut self,
175        pod: &mut ChildPod,
176        ctx: &mut PaintCtx,
177        scene: &mut dyn PaintScene,
178        origin: Point,
179        size: Size,
180        alpha: f32,
181        may_record: bool,
182    ) -> BoundaryPaint {
183        let alpha = alpha.clamp(0.0, 1.0);
184        let transform = scene.current_transform().unwrap_or(Affine::IDENTITY);
185        let finite = transform.as_coeffs().iter().all(|c| c.is_finite());
186        if !ctx.retained_enabled() || !finite {
187            self.paint_direct(pod, ctx, scene, origin, size, alpha);
188            return BoundaryPaint::Direct;
189        }
190        let [a, b, c, d, _, _] = transform.as_coeffs();
191        let linear = [a, b, c, d];
192        let serial = ctx.paint_serial();
193
194        if let Some(recorded) = self.recorded
195            && self.can_draw(pod, ctx, size, linear, recorded)
196        {
197            // Drawing proves nothing new once confirmed; the first draw is
198            // the proof (see the module docs).
199            self.recorded = Some(Recorded {
200                confirmed: true,
201                ..recorded
202            });
203            // The page sits where the body was recorded; move it to where the
204            // body paints now. The renderer reads a composite's position from
205            // its transform alone, so the move is a translation, never a new
206            // rectangle origin.
207            let shift = origin - recorded.origin;
208            let moved = shift.hypot2() > 0.0;
209            if moved {
210                scene.push_transform(Affine::translate(shift));
211            }
212            scene.draw_retained(self.key.get(), recorded.origin, size, alpha, 1.0);
213            if moved {
214                scene.pop_transform();
215            }
216            return BoundaryPaint::Retained;
217        }
218
219        let visible = ctx
220            .visible_rect()
221            .is_none_or(|v| rect_contains(v, Rect::from_origin_size(origin, size)));
222        if may_record && visible {
223            // A clean body recorded again on the paint right after its last
224            // recording, under the same linear part, origin and size, continues
225            // that run: the two scenes carry the same page. A recording at
226            // another origin starts a new run, so a ledger can only confirm the
227            // page recorded where the body now paints. Size is compared too:
228            // `can_draw` checks the drawn size against the ledger's extent, but
229            // a run must never mix two recordings of different sizes.
230            let run_start = self
231                .recorded
232                .filter(|r| {
233                    !r.confirmed
234                        && serial != 0
235                        && r.last.wrapping_add(1) == serial
236                        && r.origin == origin
237                        && r.size == size
238                        && same_linear(r.linear, linear)
239                        && ctx.subtree_clean(pod)
240                })
241                .map_or(serial, |r| r.serial);
242            scene.push_retained(self.key.get(), origin, size, alpha, 1.0);
243            self.paint_body(pod, ctx, scene);
244            scene.pop_retained();
245            self.recorded = Some(Recorded {
246                linear,
247                origin,
248                size,
249                serial: run_start,
250                last: serial,
251                confirmed: false,
252            });
253            return BoundaryPaint::Recorded;
254        }
255
256        self.paint_direct(pod, ctx, scene, origin, size, alpha);
257        BoundaryPaint::Direct
258    }
259
260    /// Paint `pod`'s body with no bracket and no composite of its own — for a
261    /// caller that wraps the paint itself (a scale step, an alpha-zero
262    /// `DiscardScene` redirect, an owner at rest). Forgets the recorded page
263    /// when the body was dirty, since painting it moves it past that page.
264    pub(crate) fn paint_plain(
265        &mut self,
266        pod: &mut ChildPod,
267        ctx: &mut PaintCtx,
268        scene: &mut dyn PaintScene,
269    ) {
270        if !ctx.subtree_clean(pod) {
271            self.recorded = None;
272        }
273        self.paint_body(pod, ctx, scene);
274    }
275
276    /// Paint the body directly under a `push_layer` when `alpha < 1`.
277    fn paint_direct(
278        &mut self,
279        pod: &mut ChildPod,
280        ctx: &mut PaintCtx,
281        scene: &mut dyn PaintScene,
282        origin: Point,
283        size: Size,
284        alpha: f32,
285    ) {
286        let layered = alpha < 1.0;
287        if layered {
288            scene.push_layer(origin, size, alpha);
289        }
290        self.paint_plain(pod, ctx, scene);
291        if layered {
292            scene.pop_layer();
293        }
294    }
295
296    /// Paint the body, noting whether it used a paint-time side channel.
297    fn paint_body(&mut self, pod: &mut ChildPod, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
298        let before = ctx.paint_publications();
299        pod.paint_child(ctx, scene);
300        self.publishes = ctx.paint_publications() != before;
301    }
302
303    /// Whether the page recorded at `recorded` can stand in for the body this
304    /// frame, drawn under `linear` at `size`.
305    fn can_draw(
306        &self,
307        pod: &ChildPod,
308        ctx: &PaintCtx,
309        size: Size,
310        linear: [f64; 4],
311        recorded: Recorded,
312    ) -> bool {
313        if self.publishes
314            || !ctx.subtree_clean(pod)
315            || !recorded.usable_at(ctx.paint_serial())
316            || !same_linear(recorded.linear, linear)
317        {
318            return false;
319        }
320        // The extent and scale the renderer reports for a page drawn under
321        // `linear` (its `RetainedPlacement::extent`/`scale`).
322        let [a, b, c, d] = linear;
323        let extent = Size::new(size.width * a.hypot(b), size.height * c.hypot(d));
324        let scale = (a * d - b * c).abs().sqrt() as f32;
325        ctx.retained_available(self.key, extent, scale, recorded.serial)
326    }
327}
328
329/// Whether two linear parts are one, to [`LINEAR_EPSILON`].
330fn same_linear(a: [f64; 4], b: [f64; 4]) -> bool {
331    a.iter()
332        .zip(b.iter())
333        .all(|(x, y)| (x - y).abs() <= LINEAR_EPSILON)
334}
335
336/// Whether `inner` lies within `outer`, to a hair of floating-point slack.
337fn rect_contains(outer: Rect, inner: Rect) -> bool {
338    const SLACK: f64 = 1e-6;
339    inner.x0 >= outer.x0 - SLACK
340        && inner.y0 >= outer.y0 - SLACK
341        && inner.x1 <= outer.x1 + SLACK
342        && inner.y1 <= outer.y1 + SLACK
343}
344
345/// Whether a body at `origin`/`size` lies wholly inside `clip_origin`/
346/// `clip_size` — what a container that clips its children asks before letting
347/// a boundary record one.
348pub(crate) fn within(origin: Point, size: Size, clip_origin: Point, clip_size: Size) -> bool {
349    rect_contains(
350        Rect::from_origin_size(clip_origin, clip_size),
351        Rect::from_origin_size(origin, size),
352    )
353}
354
355// --- RepaintBoundary ---------------------------------------------------------
356
357/// A declarative retained-page boundary. See the [module docs](self).
358pub struct RepaintBoundaryView<V> {
359    child: V,
360    alpha: f32,
361}
362
363/// Paint `child` through a retained-page boundary: its body is recorded once
364/// and composited from the renderer's cached page on every later frame where
365/// nothing under it changed — see the [module docs](self). Layout, hit-testing
366/// and semantics are the child's own, unchanged.
367///
368/// Worth it for a body that is expensive to paint and often unchanged while
369/// something around it animates or moves (a scrolled card, a panel under a
370/// fade). Never nest two boundaries over the same body.
371pub fn repaint_boundary<V>(child: V) -> RepaintBoundaryView<V> {
372    RepaintBoundaryView { child, alpha: 1.0 }
373}
374
375impl<V> RepaintBoundaryView<V> {
376    /// Composite the body at `alpha` (clamped to `[0, 1]`; default `1.0`). A
377    /// fade over an unchanged body composites the retained page at no paint
378    /// cost.
379    pub fn alpha(mut self, alpha: f32) -> Self {
380        self.alpha = alpha.clamp(0.0, 1.0);
381        self
382    }
383}
384
385/// The retained widget for a [`RepaintBoundaryView`].
386pub struct RepaintBoundaryWidget {
387    child: ChildPod,
388    alpha: f32,
389    boundary: RetainedBoundary,
390}
391
392impl RepaintBoundaryWidget {
393    /// The key this boundary records and draws its page under.
394    pub fn retained_key(&self) -> RetainedKey {
395        self.boundary.key()
396    }
397}
398
399impl<State: 'static, V: View<State>> View<State> for RepaintBoundaryView<V> {
400    type Element = RepaintBoundaryWidget;
401
402    fn build(&self, ctx: &mut BuildCtx<'_>) -> RepaintBoundaryWidget {
403        RepaintBoundaryWidget {
404            child: crate::authoring::build_child(&self.child, ctx),
405            alpha: self.alpha,
406            boundary: RetainedBoundary::new(),
407        }
408    }
409
410    fn rebuild(
411        &self,
412        prev: &Self,
413        element: &mut RepaintBoundaryWidget,
414        ctx: &mut BuildCtx<'_>,
415    ) -> ChangeFlags {
416        let mut flags = ChangeFlags::NONE;
417        if prev.alpha != self.alpha {
418            element.alpha = self.alpha;
419            flags |= ChangeFlags::PAINT;
420        }
421        flags |= crate::authoring::rebuild_child(&prev.child, &self.child, &mut element.child, ctx);
422        flags
423    }
424
425    fn teardown(&self, element: &mut RepaintBoundaryWidget, ctx: &mut BuildCtx<'_>) {
426        crate::authoring::teardown_child(&self.child, &mut element.child, ctx);
427    }
428}
429
430impl Widget for RepaintBoundaryWidget {
431    fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
432        // Transparent wrapper: exactly the child's size.
433        let size = self.child.layout_child(ctx, bc);
434        self.child.set_origin(Point::ZERO);
435        size
436    }
437
438    fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
439        let origin = ctx.origin();
440        let size = ctx.size();
441        self.boundary
442            .paint(&mut self.child, ctx, scene, origin, size, self.alpha, true);
443    }
444
445    fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
446        crate::authoring::route_event_single(&mut self.child, ctx, event)
447    }
448
449    fn semantics(&self, ctx: &mut SemanticsCtx) {
450        self.child.semantics_child(ctx);
451    }
452
453    crate::authoring::visit_children!(child);
454}