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}