Skip to main content

frust_core/
widget.rs

1//! Layer 2: the retained [`Widget`] trait and its layout/paint contexts.
2//!
3//! Widgets are the long-lived counterpart to [`crate::view::View`]s. A view is
4//! rebuilt every frame; the widget it produced persists in the arena and is
5//! mutated in place. Widgets participate in two passes:
6//!
7//! * **layout** — receive [`BoxConstraints`] and return a chosen [`Size`].
8//! * **paint** — emit draw commands into a scene.
9
10use std::any::Any;
11use std::borrow::Cow;
12use std::cell::{Cell, RefCell};
13use std::collections::HashMap;
14use std::num::NonZeroU64;
15use std::sync::Mutex;
16use std::sync::atomic::{AtomicU64, Ordering};
17use std::time::Duration;
18
19/// The per-corner radii and dash-pattern vocabulary [`PaintScene`]'s own
20/// signatures name, re-exported from `frust-scene` so a widget calling
21/// [`PaintScene::fill_rounded_rect_radii`]/[`PaintScene::push_clip_rounded_radii`]/
22/// [`PaintScene::stroke_path_dashed`] can name their arguments through the same
23/// paint surface it already paints through (mirrors the crate-root `accesskit`
24/// re-export).
25pub use frust_scene::{CornerRadii, DashPattern};
26use frust_scene::{GlyphRun, SceneBuilder, ShaderProgram};
27use kurbo::{Affine, BezPath, Point, Rect, Size};
28use peniko::{Brush, Color};
29
30use crate::anim::FrameTime;
31use crate::event::{EventCtx, EventResult, ImeState, InputEvent};
32use crate::insets::WindowInsets;
33use crate::layout::BoxConstraints;
34use crate::semantics::SemanticsCtx;
35use crate::sliver::{SliverConstraints, SliverGeometry, SliverWidget};
36
37/// The renderer-agnostic paint target a widget draws into.
38///
39/// This trait was introduced as a **local stand-in** for
40/// `frust_scene::SceneBuilder` while the scene crate was still a stub, and the
41/// two were later reconciled *additively*: rather than churn the `Widget::paint`
42/// signature (and every widget/test written against it), `SceneBuilder` now
43/// [implements this trait](#impl-PaintScene-for-SceneBuilder), so widgets keep
44/// painting through `&mut dyn PaintScene` while the shell hands them a real
45/// `SceneBuilder` whose commands reach the GPU backend.
46///
47/// The original `fill_rect`/`draw_text` shape is retained for source
48/// compatibility with existing recorder-style test scenes; real text rendering
49/// goes through [`PaintScene::draw_glyph_run`], which carries shaped glyphs from
50/// `frust-text`.
51///
52/// # Retained-page contract
53///
54/// A renderer that does not retain and never publishes a ledger paints every
55/// [`PaintScene::push_retained`] body inline and is never sent
56/// [`PaintScene::draw_retained`]. A renderer that publishes a ledger must serve
57/// every key that ledger lists or refuse the frame.
58pub trait PaintScene {
59    /// Emit a filled axis-aligned rectangle at `origin` with `size`, filled with
60    /// the solid `color`.
61    fn fill_rect(&mut self, origin: Point, size: Size, color: Color);
62
63    /// Emit a filled axis-aligned rectangle with uniformly rounded corners.
64    ///
65    /// Defaulted to a no-op so pre-existing recorder scenes stay valid; the
66    /// `SceneBuilder` implementation records a real rounded-rect command.
67    fn fill_rounded_rect(&mut self, _origin: Point, _size: Size, _radius: f64, _color: Color) {}
68
69    /// Emit a filled axis-aligned rectangle with per-corner `radii` — the
70    /// shape a uniform [`PaintScene::fill_rounded_rect`] cannot express (a
71    /// bottom-anchored sheet with only its top corners rounded, a segmented
72    /// control's end caps).
73    ///
74    /// Defaulted to the uniform call with the *largest* corner rather than to a
75    /// no-op: a recorder scene that only implements `fill_rounded_rect` still
76    /// sees a rect painted here, in the spirit of
77    /// [`PaintScene::fill_rect_brush`]'s "see something rather than nothing"
78    /// fallback. The `SceneBuilder` implementation overrides it and records
79    /// every corner faithfully.
80    fn fill_rounded_rect_radii(
81        &mut self,
82        origin: Point,
83        size: Size,
84        radii: CornerRadii,
85        color: Color,
86    ) {
87        self.fill_rounded_rect(origin, size, radii.largest(), color);
88    }
89
90    /// Stroke a straight line from `p0` to `p1` with the given `width` and solid
91    /// `color`.
92    ///
93    /// Defaulted to a no-op so pre-existing recorder scenes stay valid; the
94    /// `SceneBuilder` implementation records a real stroked-line command.
95    fn stroke_line(&mut self, _p0: Point, _p1: Point, _width: f64, _color: Color) {}
96
97    /// Push a rectangular clip (at `origin`/`size`) onto the backend clip stack;
98    /// subsequent draws are clipped to it until the matching [`PaintScene::pop_clip`].
99    ///
100    /// Defaulted to a no-op so recorder scenes stay valid; the `SceneBuilder`
101    /// implementation honors the clip by recording a push/pop command pair.
102    fn push_clip(&mut self, _origin: Point, _size: Size) {}
103
104    /// Push a clip with uniformly rounded corners (at `origin`/`size`, corner
105    /// `radius`) onto the backend clip stack; subsequent draws are clipped to
106    /// the rounded shape until the matching [`PaintScene::pop_clip`] — the same
107    /// pop [`PaintScene::push_clip`] uses, since there is one clip stack.
108    ///
109    /// Lets paint code express a radiused mask over content a rectangular clip
110    /// cannot shape — a rounded bitmap (avatar/thumbnail) being the motivating
111    /// case. Defaulted to a no-op so recorder scenes stay valid; the
112    /// `SceneBuilder` implementation honors it by recording a real
113    /// [`frust_scene::Command::PushClipRounded`]/[`frust_scene::Command::PopClip`]
114    /// pair.
115    fn push_clip_rounded(&mut self, _origin: Point, _size: Size, _radius: f64) {}
116
117    /// Push a clip with per-corner `radii` onto the backend clip stack, popped
118    /// by the same [`PaintScene::pop_clip`] as every other push.
119    ///
120    /// Defaulted to the uniform [`PaintScene::push_clip_rounded`] with the
121    /// largest corner rather than to a no-op — a defaulted *push* against an
122    /// implemented *pop* would unbalance a recorder scene's clip stack, so this
123    /// one delegates for correctness, not just for visibility (see
124    /// [`PaintScene::fill_rounded_rect_radii`]).
125    fn push_clip_rounded_radii(&mut self, origin: Point, size: Size, radii: CornerRadii) {
126        self.push_clip_rounded(origin, size, radii.largest());
127    }
128
129    /// Push a clip to an arbitrary closed outline onto the backend clip stack;
130    /// subsequent draws are clipped to `path`'s interior (nonzero winding)
131    /// until the matching [`PaintScene::pop_clip`] — the same pop every other
132    /// clip uses.
133    ///
134    /// `path` is in the widget's local coordinate space and `origin`
135    /// translates it into the parent's, exactly as [`PaintScene::fill_path`]'s
136    /// does. The renderer lowers it to a coverage mask, never a layer page, so
137    /// a [`PaintScene::push_retained`] body under it paints inline rather than
138    /// being retained (see [`frust_scene::Command::PushClipPath`]).
139    ///
140    /// Defaulted to a rectangular [`PaintScene::push_clip`] of the path's
141    /// bounding box rather than to a no-op — like
142    /// [`PaintScene::push_clip_rounded_radii`], a defaulted *push* against an
143    /// implemented *pop* would unbalance a recorder scene's clip stack, and the
144    /// bounding box is the tightest rectangle that never clips away anything
145    /// the path would have kept. The `SceneBuilder` implementation records a
146    /// real [`frust_scene::Command::PushClipPath`].
147    fn push_clip_path(&mut self, origin: Point, path: &BezPath) {
148        let bounds = kurbo::Shape::bounding_box(path);
149        self.push_clip(origin + bounds.origin().to_vec2(), bounds.size());
150    }
151
152    /// Pop the most recently pushed clip, rectangular, rounded or path.
153    /// Defaulted to a no-op; see [`PaintScene::push_clip`].
154    fn pop_clip(&mut self) {}
155
156    /// Emit a run of *unshaped* text anchored at `origin`.
157    ///
158    /// This records intent only — glyph shaping lives in `frust-text`.
159    /// Real rendering uses [`PaintScene::draw_glyph_run`]; the
160    /// `SceneBuilder` implementation treats this as a no-op.
161    fn draw_text(&mut self, origin: Point, text: &str);
162
163    /// Emit a run of already-shaped glyphs into the scene.
164    ///
165    /// Defaulted to a no-op so pre-existing recorder scenes (which predate the
166    /// text pipeline) stay valid without modification; the `SceneBuilder`
167    /// implementation overrides it to record a real glyph-run command.
168    fn draw_glyph_run(&mut self, _run: GlyphRun) {}
169
170    /// Draw an already-decoded image, scaled from its natural
171    /// (`data.width`x`data.height`) size to fill the absolute `dest` rect.
172    ///
173    /// A single additive method (added for `frust-widgets::Image`) on this
174    /// otherwise layer-2 trait — authorized because `Command::Image`'s
175    /// `peniko::ImageData` payload has to reach the scene through the same
176    /// `&mut dyn PaintScene` seam every other paint call uses. Defaulted to a
177    /// no-op so pre-existing recorder scenes stay valid; the `SceneBuilder`
178    /// implementation records a real image command.
179    fn draw_image(&mut self, _data: &peniko::ImageData, _dest: Rect) {}
180
181    /// Draw a fragment-shader-filled rectangle, scaled to fill `dest`.
182    ///
183    /// An additive method (added for the shader-showcase feature) on this otherwise layer-2
184    /// trait — authorized because the shader program and destination have to
185    /// reach the scene through the same `&mut dyn PaintScene` seam every other
186    /// paint call uses. `program` carries the WGSL source and process-unique
187    /// id; `time` is seconds, app-supplied. Defaulted to a no-op so pre-existing
188    /// recorder scenes stay valid; the `SceneBuilder` implementation records a
189    /// real shader-quad command.
190    ///
191    /// **Renders on the engine renderer** via the external-texture path (see
192    /// `crates/frust-engine/src/effects/shader_quad.rs`); there is no CPU-oracle
193    /// golden for a user-supplied fragment shader, so engine-side correctness
194    /// is proven on a real device instead — see `docs/LIMITATIONS.md`'s
195    /// `engine-shader-quad-goldens-uncomparable`.
196    ///
197    /// # Cache-once contract
198    ///
199    /// `program` must be a retained, already-created `ShaderProgram` handle
200    /// (see `ShaderProgram::new`'s own doc for the full contract) — never a
201    /// fresh one minted inline in the call that invokes this method. This
202    /// method is reached from [`Widget::paint`], which re-runs every frame,
203    /// so a `ShaderProgram::new` call written directly at a `draw_shader`
204    /// call site there mints a new process-unique id (and therefore a new
205    /// GPU pipeline cache miss) every frame; the same applies to a
206    /// [`crate::component::Component`]'s `build`, which re-runs every
207    /// rebuild. Build the `ShaderProgram` once — in a `Component`'s `init`,
208    /// or other retained widget state — and clone the handle in; a
209    /// [`View::build`](crate::view::View::build) call, by contrast, runs
210    /// exactly once per widget instance and is a correct place to construct
211    /// one.
212    fn draw_shader(&mut self, _program: &ShaderProgram, _dest: Rect, _time: f32) {}
213
214    /// Composite a bound scene texture (pre-rendered via [`ExternalPass`](frust_gpu::ExternalPass))
215    /// into the scene.
216    ///
217    /// An additive method on this otherwise layer-2 trait — authorized because the
218    /// texture id and destination have to reach the scene through the same
219    /// `&mut dyn PaintScene` seam every other paint call uses. `id` is a
220    /// `SceneTextureId::get()`; `dest` is in the scene's coordinate space.
221    /// Defaulted to a no-op so pre-existing recorder scenes stay valid; the
222    /// `SceneBuilder` implementation records a real scene-texture command.
223    ///
224    /// The engine renders nothing if the id is unbound and warns once per
225    /// process. Output is always blended (never replaces). The binding is
226    /// established outside this paint call by [`ExternalPass::record`](frust_gpu::ExternalPass::record);
227    /// a texture whose pass has not bound anything yet simply draws nothing
228    /// that frame, never an error.
229    fn draw_scene_texture(&mut self, _id: u64, _dest: Rect) {}
230
231    /// Draw a gaussian-blurred rounded-rectangle elevation shadow (an
232    /// approximation of a CSS `box-shadow`) at `origin`/`size`.
233    ///
234    /// Defaulted to a no-op so pre-existing recorder scenes stay valid; the
235    /// `SceneBuilder` implementation records a real
236    /// [`frust_scene::Command::BlurredRoundedRect`].
237    fn draw_shadow(
238        &mut self,
239        _origin: Point,
240        _size: Size,
241        _radius: f64,
242        _std_dev: f64,
243        _color: Color,
244    ) {
245    }
246
247    /// Emit a filled axis-aligned rectangle at `origin` with `size`, filled
248    /// with an arbitrary `brush` (solid color or gradient).
249    ///
250    /// Default implementation delegates to [`PaintScene::fill_rect`] using
251    /// the brush's solid color where possible (a `Brush::Solid` unwraps
252    /// directly; a gradient brush falls back to transparent black, since a
253    /// pre-existing recorder scene has no gradient concept to approximate
254    /// it with) — so callers that only override `fill_rect` still see
255    /// *something* painted rather than nothing. The `SceneBuilder`
256    /// implementation records the brush faithfully via
257    /// [`frust_scene::Command::RoundedRect`]'s zero-radius sibling
258    /// (`FillRect`).
259    fn fill_rect_brush(&mut self, origin: Point, size: Size, brush: &Brush) {
260        let color = match brush {
261            Brush::Solid(color) => *color,
262            _ => Color::TRANSPARENT,
263        };
264        self.fill_rect(origin, size, color);
265    }
266
267    /// Emit a filled axis-aligned rectangle with uniformly rounded corners,
268    /// filled with an arbitrary `brush` (solid color or gradient).
269    ///
270    /// Defaulted to a no-op so pre-existing recorder scenes stay valid; the
271    /// `SceneBuilder` implementation records a real
272    /// [`frust_scene::Command::RoundedRect`] carrying the brush.
273    fn fill_rounded_rect_brush(
274        &mut self,
275        _origin: Point,
276        _size: Size,
277        _radius: f64,
278        _brush: &Brush,
279    ) {
280    }
281
282    /// Push a translucent layer (at `origin`/`size`) onto the backend layer
283    /// stack; subsequent draws are composited at `alpha` until the matching
284    /// [`PaintScene::pop_layer`].
285    ///
286    /// Defaulted to a no-op so pre-existing recorder scenes stay valid; the
287    /// `SceneBuilder` implementation records a real
288    /// [`frust_scene::Command::PushLayer`]/[`frust_scene::Command::PopLayer`]
289    /// pair, nesting correctly with [`PaintScene::push_clip`]/[`PaintScene::pop_clip`].
290    fn push_layer(&mut self, _origin: Point, _size: Size, _alpha: f32) {}
291
292    /// Pop the most recently pushed layer. Defaulted to a no-op; see
293    /// [`PaintScene::push_layer`].
294    fn pop_layer(&mut self) {}
295
296    /// Clear an axis-aligned rectangle (at `origin`/`size`) to full
297    /// transparency (alpha 0), erasing everything already painted below it in
298    /// this scene — a real destination-clearing composite, not merely skipping
299    /// paint over the region.
300    ///
301    /// The platform-view hole-punch (`frust-widgets`' `PlatformViewWidget`) is
302    /// the sole v1 consumer: on a translucent (Mode B) surface a slot punches
303    /// its rect so an opaque app backdrop painted below it (the catalog's
304    /// `AppBackground`) doesn't seal the hole the hosted native view shows
305    /// through. Gated on [`PaintCtx::is_translucent`] by the widget — clearing
306    /// on an opaque surface would erase real app content, and the clear is
307    /// disregarded there anyway (see [`frust_scene::Command::ClearRect`]).
308    ///
309    /// Defaulted to a no-op so pre-existing recorder scenes stay valid; the
310    /// `SceneBuilder` implementation records a real
311    /// [`frust_scene::Command::ClearRect`].
312    fn clear_rect(&mut self, _origin: Point, _size: Size) {}
313
314    /// Fill an arbitrary vector path (e.g. an arc — see
315    /// [`frust_scene::arc_path`]) at `origin`, using the nonzero winding
316    /// rule and `brush`.
317    ///
318    /// `path` is in the widget's local coordinate space; `origin` translates
319    /// it into the parent's space, mirroring every other `PaintScene`
320    /// method's origin convention. Defaulted to a
321    /// no-op so pre-existing recorder scenes stay valid; the `SceneBuilder`
322    /// implementation records a real [`frust_scene::Command::Path`].
323    fn fill_path(&mut self, _origin: Point, _path: &BezPath, _brush: &Brush) {}
324
325    /// Stroke an arbitrary vector path (e.g. an arc) at `origin` with `width`
326    /// and round caps/joins, using `brush`.
327    ///
328    /// Defaulted to a no-op so pre-existing recorder scenes stay valid; see
329    /// [`PaintScene::fill_path`].
330    fn stroke_path(&mut self, _origin: Point, _path: &BezPath, _width: f64, _brush: &Brush) {}
331
332    /// Stroke an arbitrary vector path at `origin` as a dashed line: the same
333    /// stroke [`PaintScene::stroke_path`] paints, broken into `dash`'s on/off
334    /// runs by the render crate at encode time.
335    ///
336    /// Defaulted to the solid [`PaintScene::stroke_path`] rather than to a
337    /// no-op — dashing is a visual refinement, so a scene that cannot express
338    /// it still draws the path.
339    fn stroke_path_dashed(
340        &mut self,
341        origin: Point,
342        path: &BezPath,
343        width: f64,
344        _dash: DashPattern,
345        brush: &Brush,
346    ) {
347        self.stroke_path(origin, path, width, brush);
348    }
349
350    /// Push an affine `transform`, composed with the current one, onto the
351    /// backend transform stack; subsequent draws are transformed until the
352    /// matching [`PaintScene::pop_transform`].
353    ///
354    /// Defaulted to a no-op so pre-existing recorder scenes stay valid; the
355    /// `SceneBuilder` implementation composes and records it. Unlike the
356    /// origin-offset convention every other method uses (a pure translation),
357    /// this is the one seam that also carries scale/rotation — the
358    /// shared-element ("hero") morph is the first consumer, repainting a tagged
359    /// subtree under a rect→rect transform (position **and** scale) so it
360    /// morphs between two pages during a navigation transition.
361    fn push_transform(&mut self, _transform: Affine) {}
362
363    /// Pop the most recently pushed transform, restoring the previous one.
364    /// Defaulted to a no-op; see [`PaintScene::push_transform`].
365    fn pop_transform(&mut self) {}
366
367    /// Push a retained bracket: the body is rasterizable once and cached by
368    /// `key` across frames. The `alpha` and `scale` are presentation parameters
369    /// applied to the whole cached body (alpha blending, uniform scale).
370    ///
371    /// The default implementation emulates the presentation for recorders
372    /// that don't have a retained-layer concept: it pushes a transform (scale
373    /// about the rect's center), then a layer (at the rect with the given
374    /// alpha). A renderer may cache and apply both as a whole; a recorder sees
375    /// the component pieces. The matching [`PaintScene::pop_retained`] pops
376    /// both in the reverse order.
377    ///
378    /// [`SceneBuilder`] overrides this to call the retained-aware builder
379    /// methods directly, which record `Command::PushRetained`/`PopRetained`
380    /// and no extra transform/layer commands.
381    fn push_retained(&mut self, _key: u64, origin: Point, size: Size, alpha: f32, scale: f64) {
382        let center = Point::new(origin.x + size.width / 2.0, origin.y + size.height / 2.0);
383        self.push_transform(Affine::scale_about(scale, center));
384        self.push_layer(origin, size, alpha);
385    }
386
387    /// Pop the most recently pushed retained bracket. Defaulted to the reverse
388    /// of [`PaintScene::push_retained`]'s default: pop layer, then transform.
389    /// Recorders that don't override both may see unbalanced stacks if only one
390    /// is overridden; the default pair is provided for source compatibility.
391    fn pop_retained(&mut self) {
392        self.pop_layer();
393        self.pop_transform();
394    }
395
396    /// Draw the body retained under `key` from its cached page (see
397    /// [`Command::DrawRetained`]). Only valid when the resident-key ledger
398    /// said `key` is resident; the default is a no-op for recorders with no
399    /// retained-layer concept, which never receive such a call.
400    ///
401    /// Contract: a renderer that does not retain and never publishes a ledger
402    /// paints every `push_retained` body inline and is never sent this call; a
403    /// renderer that publishes a ledger must serve every key it lists or refuse
404    /// the frame.
405    fn draw_retained(&mut self, _key: u64, _origin: Point, _size: Size, _alpha: f32, _scale: f64) {}
406
407    /// The transform the next command would be recorded under — every
408    /// [`PaintScene::push_transform`] still open, the shell's device scale
409    /// included — or `None` for a scene that does not track one.
410    ///
411    /// A retained-page boundary reads it to ask the resident-key ledger for the
412    /// page at the scale it is about to be drawn at (a page is reusable only
413    /// under its own linear part). `None` is read as the identity. Defaulted so
414    /// recorder scenes stay valid; [`SceneBuilder`] reports its own.
415    fn current_transform(&self) -> Option<Affine> {
416        None
417    }
418
419    /// Blur whatever is already painted beneath the rectangle at
420    /// `origin`/`size` — a frosted-glass backdrop — with a Gaussian of
421    /// standard deviation `radius` (see
422    /// [`frust_scene::Command::BackdropBlur`]).
423    ///
424    /// Everything painted before this call is the backdrop; whatever the
425    /// widget paints after it lands on top of the blurred region. A clip open
426    /// around the call masks the blurred region like any other paint.
427    /// Defaulted to a no-op so recorder scenes stay valid; the
428    /// [`SceneBuilder`] implementation records the real command.
429    fn backdrop_blur(&mut self, _origin: Point, _size: Size, _radius: f64) {}
430
431    /// How many commands this scene has recorded so far in the current frame —
432    /// the painter-order index the next recorded command will occupy.
433    ///
434    /// A platform-view slot stamps it onto its [`PlatformViewFrame`]
435    /// ([`PlatformViewFrame::command_index`]) so the shell-side differ knows
436    /// which recorded commands paint above which native slot. Defaulted to `0`
437    /// for recorder scenes with no command list (every slot then reads as
438    /// painted first, before any frust content); [`SceneBuilder`] reports its
439    /// scene's live command count.
440    fn command_index(&self) -> usize {
441        0
442    }
443}
444
445/// A [`PaintScene`] sink that accepts every paint command and records
446/// nothing — the zero-GPU-cost target for a subtree that still needs its
447/// `paint` pass driven for the pass's *side effects* (hero-rect reporting
448/// through [`PaintCtx::with_hero_registry`], other paint-time widget state)
449/// even though its pixels will never be composited. The navigator's
450/// transition machinery is the motivating case: a page whose resolved alpha
451/// is 0 still has to paint to report hero rects and advance animating
452/// children, but every command it emits is otherwise wasted GPU work.
453///
454/// # Guarantee
455///
456/// No paint command reaches any scene: every [`PaintScene`] method on
457/// [`DiscardScene`] is a no-op, whether overridden here directly or
458/// inherited from the trait's own no-op (or no-op-delegating) default.
459/// Anything driven through [`PaintCtx`] rather than through
460/// `&mut dyn PaintScene` — the hero registry, the frame clock, paced-class
461/// bubbling — is unaffected, since none of that machinery routes through the
462/// scene it's handed.
463pub struct DiscardScene;
464
465impl PaintScene for DiscardScene {
466    fn fill_rect(&mut self, _origin: Point, _size: Size, _color: Color) {}
467
468    fn draw_text(&mut self, _origin: Point, _text: &str) {}
469}
470
471/// Bridges the provisional [`PaintScene`] boundary onto the real
472/// `frust_scene::SceneBuilder`.
473///
474/// Widgets paint through `&mut dyn PaintScene`; the desktop shell
475/// hands them a `SceneBuilder`, so filled rectangles and shaped glyph runs land
476/// in the display list under the builder's current transform. Unshaped
477/// [`PaintScene::draw_text`] is intentionally dropped here — text must be shaped
478/// (by `frust-text`) into glyph runs before it can be drawn.
479impl PaintScene for SceneBuilder<'_> {
480    fn fill_rect(&mut self, origin: Point, size: Size, color: Color) {
481        SceneBuilder::fill_rect(self, rect_at(origin, size), Brush::Solid(color));
482    }
483
484    fn fill_rounded_rect(&mut self, origin: Point, size: Size, radius: f64, color: Color) {
485        SceneBuilder::fill_rounded_rect(self, rect_at(origin, size), radius, Brush::Solid(color));
486    }
487
488    fn fill_rounded_rect_radii(
489        &mut self,
490        origin: Point,
491        size: Size,
492        radii: CornerRadii,
493        color: Color,
494    ) {
495        SceneBuilder::fill_rounded_rect_radii(
496            self,
497            rect_at(origin, size),
498            radii,
499            Brush::Solid(color),
500        );
501    }
502
503    fn stroke_line(&mut self, p0: Point, p1: Point, width: f64, color: Color) {
504        SceneBuilder::stroke_line(self, p0, p1, width, Brush::Solid(color));
505    }
506
507    fn push_clip(&mut self, origin: Point, size: Size) {
508        SceneBuilder::push_clip(self, rect_at(origin, size));
509    }
510
511    fn push_clip_rounded(&mut self, origin: Point, size: Size, radius: f64) {
512        SceneBuilder::push_clip_rounded(self, rect_at(origin, size), radius);
513    }
514
515    fn push_clip_rounded_radii(&mut self, origin: Point, size: Size, radii: CornerRadii) {
516        SceneBuilder::push_clip_rounded_radii(self, rect_at(origin, size), radii);
517    }
518
519    fn push_clip_path(&mut self, origin: Point, path: &BezPath) {
520        SceneBuilder::push_clip_path(self, path_at(origin, path));
521    }
522
523    fn pop_clip(&mut self) {
524        SceneBuilder::pop_clip(self);
525    }
526
527    fn draw_text(&mut self, _origin: Point, _text: &str) {
528        // Unshaped text is not renderable; real text arrives as glyph runs.
529    }
530
531    fn draw_glyph_run(&mut self, run: GlyphRun) {
532        SceneBuilder::draw_glyph_run(self, run);
533    }
534
535    fn draw_image(&mut self, data: &peniko::ImageData, dest: Rect) {
536        SceneBuilder::draw_image(self, data, dest);
537    }
538
539    fn draw_shader(&mut self, program: &ShaderProgram, dest: Rect, time: f32) {
540        SceneBuilder::draw_shader(self, program, dest, time);
541    }
542
543    fn draw_scene_texture(&mut self, id: u64, dest: Rect) {
544        SceneBuilder::scene_texture(self, id, dest);
545    }
546
547    fn draw_shadow(&mut self, origin: Point, size: Size, radius: f64, std_dev: f64, color: Color) {
548        SceneBuilder::draw_blurred_rounded_rect(
549            self,
550            rect_at(origin, size),
551            radius,
552            std_dev,
553            color,
554        );
555    }
556
557    fn fill_rect_brush(&mut self, origin: Point, size: Size, brush: &Brush) {
558        SceneBuilder::fill_rect(self, rect_at(origin, size), brush.clone());
559    }
560
561    fn fill_rounded_rect_brush(&mut self, origin: Point, size: Size, radius: f64, brush: &Brush) {
562        SceneBuilder::fill_rounded_rect(self, rect_at(origin, size), radius, brush.clone());
563    }
564
565    fn push_layer(&mut self, origin: Point, size: Size, alpha: f32) {
566        SceneBuilder::push_layer(self, rect_at(origin, size), alpha);
567    }
568
569    fn pop_layer(&mut self) {
570        SceneBuilder::pop_layer(self);
571    }
572
573    fn clear_rect(&mut self, origin: Point, size: Size) {
574        SceneBuilder::clear_rect(self, rect_at(origin, size));
575    }
576
577    fn fill_path(&mut self, origin: Point, path: &BezPath, brush: &Brush) {
578        SceneBuilder::fill_path(self, path_at(origin, path), brush.clone());
579    }
580
581    fn stroke_path(&mut self, origin: Point, path: &BezPath, width: f64, brush: &Brush) {
582        SceneBuilder::stroke_path(self, path_at(origin, path), width, brush.clone());
583    }
584
585    fn stroke_path_dashed(
586        &mut self,
587        origin: Point,
588        path: &BezPath,
589        width: f64,
590        dash: DashPattern,
591        brush: &Brush,
592    ) {
593        SceneBuilder::stroke_path_dashed(self, path_at(origin, path), width, dash, brush.clone());
594    }
595
596    fn push_transform(&mut self, transform: Affine) {
597        SceneBuilder::push_transform(self, transform);
598    }
599
600    fn pop_transform(&mut self) {
601        SceneBuilder::pop_transform(self);
602    }
603
604    fn push_retained(&mut self, key: u64, origin: Point, size: Size, alpha: f32, scale: f64) {
605        SceneBuilder::push_retained(self, key, rect_at(origin, size), alpha, scale);
606    }
607
608    fn pop_retained(&mut self) {
609        SceneBuilder::pop_retained(self);
610    }
611
612    fn draw_retained(&mut self, key: u64, origin: Point, size: Size, alpha: f32, scale: f64) {
613        SceneBuilder::draw_retained(self, key, rect_at(origin, size), alpha, scale);
614    }
615
616    fn current_transform(&self) -> Option<Affine> {
617        Some(SceneBuilder::current_transform(self))
618    }
619
620    fn backdrop_blur(&mut self, origin: Point, size: Size, radius: f64) {
621        SceneBuilder::backdrop_blur(self, rect_at(origin, size), radius);
622    }
623
624    fn command_index(&self) -> usize {
625        SceneBuilder::command_count(self)
626    }
627}
628
629/// Build an origin/size pair into the `kurbo::Rect` the scene builder speaks.
630fn rect_at(origin: Point, size: Size) -> Rect {
631    Rect::new(
632        origin.x,
633        origin.y,
634        origin.x + size.width,
635        origin.y + size.height,
636    )
637}
638
639/// Translates `path` (in the widget's local coordinate space) by `origin`,
640/// mirroring [`rect_at`]'s origin/size convention for [`PaintScene::fill_path`]/
641/// [`PaintScene::stroke_path`].
642fn path_at(origin: Point, path: &BezPath) -> BezPath {
643    Affine::translate((origin.x, origin.y)) * path.clone()
644}
645
646/// Context passed to [`Widget::layout`].
647///
648/// Beyond the (still-empty) container seam, it optionally carries the shared,
649/// heavyweight text-shaping context the render root threads down for text
650/// layout, plus the app's active theme (design tokens).
651/// Both resources are **type-erased** (`&mut dyn Any` / `&dyn Any`) so
652/// `frust-core` stays independent of `frust-text` (and thus of parley)
653/// and of `frust-theme`; text widgets recover the shaping context with
654/// [`LayoutCtx::text_context`] and themed widgets recover the theme with
655/// [`LayoutCtx::theme_as`].
656pub struct LayoutCtx<'a> {
657    text_ctx: Option<&'a mut dyn Any>,
658    /// The app's active theme, threaded down type-erased by the render root so
659    /// this crate needs no `frust-theme` dependency. `None` in bare-core
660    /// tests and pre-theme apps — a *supported* state (unlike the text context,
661    /// whose absence at a text widget is a wiring bug), so [`LayoutCtx::theme_as`]
662    /// returns `Option` rather than panicking.
663    theme: Option<&'a dyn Any>,
664    /// The window's insets ([`WindowInsets`]), threaded down by the render root
665    /// (see [`crate::app::RenderRoot::set_insets`]). Unlike the theme this is a
666    /// concrete core-owned type carried by copy — global (origin-independent,
667    /// see the [`crate::insets`] module docs), so the single layout context the
668    /// render root threads down carries it unchanged to every widget in the
669    /// tree — except inside a [`LayoutCtx::with_window_insets`] scope, where a
670    /// consuming ancestor (`SafeArea`) installs a reduced value for its subtree.
671    /// Defaults to the zero inset in bare-core tests and pre-insets apps.
672    window_insets: WindowInsets,
673    /// The window's logical size, threaded down by the render root
674    /// ([`crate::app::RenderRoot::layout`]) exactly like `window_insets` above —
675    /// one layout context reaches the whole tree, so the value is set once at the
676    /// root and every widget reads the same one. [`Size::ZERO`] in bare-core
677    /// tests and before the first layout, a supported state.
678    ///
679    /// Its consumer is the overlay portal: a widget that floats a pod
680    /// ([`crate::overlay`]) lays that pod out against the *window*, not against
681    /// its own constraints, because the pod will be painted at an absolute window
682    /// rect rather than inside its owner. See [`LayoutCtx::window_size`].
683    window_size: Size,
684    /// The render root's global layout epoch for this pass
685    /// ([`LayoutCtx::layout_epoch`]). A [`ChildPod`] records the epoch it was
686    /// last laid out under and lays out again whenever it moved, so bumping it
687    /// invalidates every pod's cached layout at once — the root does so for
688    /// every *global* layout input that does not flow through constraints (a
689    /// theme swap, an inset change, a window resize). `0` outside a root.
690    epoch: u64,
691    /// Whether this pass lays out every pod regardless of its cached state
692    /// ([`LayoutCtx::is_forced`]) — the layout-skip kill switch. `true` for a
693    /// context built outside a [`crate::app::RenderRoot`] (every public
694    /// constructor), so a widget harness that lays out a pod directly keeps the
695    /// always-lay-out behaviour; the root seeds it from
696    /// [`crate::app::RenderRoot::set_layout_skip`].
697    force: bool,
698}
699
700impl<'a> LayoutCtx<'a> {
701    /// Create a layout context with no shared resources.
702    ///
703    /// Used by leaf-only unit tests and by containers that never lay out text.
704    pub fn new() -> LayoutCtx<'static> {
705        LayoutCtx {
706            text_ctx: None,
707            theme: None,
708            window_insets: WindowInsets::default(),
709            window_size: Size::ZERO,
710            epoch: 0,
711            force: true,
712        }
713    }
714
715    /// Create a layout context carrying the shared text-shaping context.
716    ///
717    /// The render root builds this so text widgets can shape their content
718    /// during the layout pass; the concrete type is erased to keep this crate
719    /// free of a `frust-text` dependency.
720    pub fn with_text_context(text_ctx: &'a mut dyn Any) -> Self {
721        LayoutCtx {
722            text_ctx: Some(text_ctx),
723            theme: None,
724            window_insets: WindowInsets::default(),
725            window_size: Size::ZERO,
726            epoch: 0,
727            force: true,
728        }
729    }
730
731    /// Create a layout context carrying both an optional text-shaping context
732    /// and an optional type-erased theme.
733    ///
734    /// The render root uses this to thread both resources it owns into the
735    /// layout pass in one shot (see [`crate::app::RenderRoot::layout`]).
736    pub fn with_resources(text_ctx: Option<&'a mut dyn Any>, theme: Option<&'a dyn Any>) -> Self {
737        LayoutCtx {
738            text_ctx,
739            theme,
740            window_insets: WindowInsets::default(),
741            window_size: Size::ZERO,
742            epoch: 0,
743            force: true,
744        }
745    }
746
747    /// Attach the app's active theme, type-erased. Chainable builder used by the
748    /// render root when it lends a stored theme into the layout pass.
749    pub fn with_theme(mut self, theme: &'a dyn Any) -> Self {
750        self.theme = Some(theme);
751        self
752    }
753
754    /// Recover the shared text-shaping context as `&mut T`.
755    ///
756    /// Panics if no context was threaded into this pass, or if its concrete
757    /// type differs from `T` — both are shell-wiring bugs, not runtime-data
758    /// conditions.
759    pub fn text_context<T: Any>(&mut self) -> &mut T {
760        self.text_ctx
761            .as_deref_mut()
762            .expect("no text context threaded into this layout pass")
763            .downcast_mut::<T>()
764            .expect("threaded layout resource is not the expected text-context type")
765    }
766
767    /// Recover the threaded theme as `&T`, or `None` if no theme was threaded
768    /// into this pass (a supported state — bare-core tests and pre-theme apps)
769    /// or its concrete type differs from `T`.
770    ///
771    /// Mirrors [`LayoutCtx::text_context`] but returns `Option` rather than
772    /// panicking, because a missing theme is a valid runtime state, not a
773    /// wiring bug. Widgets that read `frust_theme::Theme` downcast through
774    /// this (or the `Theme::from_layout_ctx` convenience wrapper).
775    pub fn theme_as<T: Any>(&self) -> Option<&T> {
776        self.theme?.downcast_ref::<T>()
777    }
778
779    /// The window's insets ([`WindowInsets`]) for this layout pass (a cheap
780    /// copy). Global and origin-independent (see the [`crate::insets`] module
781    /// docs), so every widget reads the same value regardless of its position —
782    /// save that a consuming ancestor may have narrowed it for its subtree via
783    /// [`LayoutCtx::with_window_insets`]; defaults to the zero inset when no
784    /// shell pushed one. A `SafeArea` widget insets by [`WindowInsets::padding`].
785    pub fn window_insets(&self) -> WindowInsets {
786        self.window_insets
787    }
788
789    /// Seed the window insets lent by the render root
790    /// ([`crate::app::RenderRoot::layout`]). One layout context is threaded down
791    /// the whole tree, so this is set once at the root; the insets are global,
792    /// so no per-child adjustment is needed.
793    pub(crate) fn set_window_insets(&mut self, insets: WindowInsets) {
794        self.window_insets = insets;
795    }
796
797    /// Run `f` with `insets` installed as this context's window insets, then
798    /// restore the previous value and return `f`'s result.
799    ///
800    /// The window insets are otherwise a single root-seeded, global value that
801    /// every widget reads unchanged. This scoped override is how a widget that
802    /// has already padded its subtree by some inset edges removes them from
803    /// that subtree (Flutter's `MediaQuery.removePadding`): `SafeArea` lays its
804    /// child out inside
805    /// `ctx.with_window_insets(ctx.window_insets().consuming(..), |ctx| ..)`, so
806    /// a self-insetting descendant reads zero padding on the consumed edges
807    /// instead of insetting a second time. Pair it with
808    /// [`PaintCtx::with_window_insets`] around the matching `paint_child` so a
809    /// paint-time read agrees with the layout-time one.
810    ///
811    /// The restore is a plain assignment after `f` returns — there is no drop
812    /// guard, so if `f` panics the override is not undone (the pass is being
813    /// unwound anyway).
814    pub fn with_window_insets<R>(
815        &mut self,
816        insets: WindowInsets,
817        f: impl FnOnce(&mut Self) -> R,
818    ) -> R {
819        let saved = std::mem::replace(&mut self.window_insets, insets);
820        let result = f(self);
821        self.window_insets = saved;
822        result
823    }
824
825    /// The window's logical size for this layout pass (a cheap copy).
826    ///
827    /// Global and origin-independent like [`LayoutCtx::window_insets`], so every
828    /// widget in the tree reads the same value regardless of where it sits or
829    /// what constraints its parent handed it; [`Size::ZERO`] when no root has laid
830    /// out yet (bare-core leaf tests).
831    ///
832    /// **The constraint an overlay pod is laid out against.** A widget floating a
833    /// pod through [`crate::overlay`] sizes it with
834    /// `BoxConstraints::loose(ctx.window_size())` rather than with its own `bc`:
835    /// the pod escapes its owner's box entirely, so the owner's constraints say
836    /// nothing about how much room the floated surface has, and the window is the
837    /// only bound that does.
838    pub fn window_size(&self) -> Size {
839        self.window_size
840    }
841
842    /// Seed the window size lent by the render root
843    /// ([`crate::app::RenderRoot::layout`]). One layout context is threaded down
844    /// the whole tree, so this is set once at the root and inherited unchanged,
845    /// exactly like [`LayoutCtx::set_window_insets`].
846    pub(crate) fn set_window_size(&mut self, size: Size) {
847        self.window_size = size;
848    }
849
850    /// The render root's global layout epoch for this pass — `0` for a context
851    /// built outside a root. Read-only: a [`ChildPod`] compares it against the
852    /// epoch it last laid out under (see [`ChildPod::layout_child`]).
853    pub fn layout_epoch(&self) -> u64 {
854        self.epoch
855    }
856
857    /// Seed the layout epoch lent by the render root
858    /// ([`crate::app::RenderRoot::layout`]); set once at the root and inherited
859    /// unchanged, exactly like [`LayoutCtx::set_window_size`].
860    pub(crate) fn set_layout_epoch(&mut self, epoch: u64) {
861        self.epoch = epoch;
862    }
863
864    /// Whether this pass lays out every pod regardless of its cached layout
865    /// state — the layout skip switched off. `true` for every context built
866    /// through a public constructor (no root, nothing to skip against); a
867    /// [`crate::app::RenderRoot`] pass reports `!`[`layout_skip`](crate::app::RenderRoot::layout_skip).
868    pub fn is_forced(&self) -> bool {
869        self.force
870    }
871
872    /// Seed the force flag for this pass ([`LayoutCtx::is_forced`]); the render
873    /// root sets it from [`crate::app::RenderRoot::set_layout_skip`].
874    pub(crate) fn set_layout_force(&mut self, force: bool) {
875        self.force = force;
876    }
877}
878
879impl Default for LayoutCtx<'static> {
880    fn default() -> Self {
881        Self::new()
882    }
883}
884
885/// The *class* of a continuation-frame request a widget makes during paint —
886/// how urgent the next frame is, so the mobile frame gate can decide whether it
887/// may be paced (see [`crate::app::RenderRoot::paint`] and the frame gate).
888///
889/// A widget continues an animation by asking for another frame during paint
890/// ([`PaintCtx::request_frame`] / [`PaintCtx::request_frame_paced`]); this tag
891/// says whether that next frame is user-visible motion that must land on the
892/// very next vsync ([`TickClass::Transition`]) or a decorative loop whose cadence
893/// can be throttled without a perceptible glitch ([`TickClass::CosmeticLoop`]).
894///
895/// **Aggregation is a max-lattice**: `Transition` dominates `CosmeticLoop`. Over
896/// a whole paint pass, ANY [`TickClass::Transition`] request makes the frame
897/// unpaced (must run every vsync, today's behavior); only when *every* request
898/// this frame is [`TickClass::CosmeticLoop`] may the gate pace it. No request at
899/// all leaves the frame as it is today — the class is only meaningful once a
900/// frame was actually requested (see [`PaintCtx::frame_class`]).
901///
902/// A `CosmeticLoop` request may additionally name *how often* it wants to be
903/// re-run ([`PaintCtx::request_frame_paced_at`]); those intervals aggregate on
904/// their own **MIN**-lattice, orthogonal to this max-lattice over classes (see
905/// [`PaintCtx::paced_interval`]).
906///
907/// The *gate-side* pacing behavior is implemented separately (the mobile frame
908/// gate); this type is only the vocabulary a widget uses to declare intent.
909#[derive(Clone, Copy, Debug, PartialEq, Eq)]
910pub enum TickClass {
911    /// A pacable decorative loop (e.g. a skeleton shimmer, an idle pulse) — the
912    /// gate may throttle its cadence when this is the *only* class requested
913    /// this frame. Never dominates a concurrent [`TickClass::Transition`].
914    CosmeticLoop,
915    /// User-visible motion that must reproduce every vsync — a page transition,
916    /// a fling, a caret blink, a layout animation. Today's `request_frame`
917    /// behavior, and the dominant class in the aggregation above.
918    Transition,
919}
920
921/// Context passed to [`Widget::paint`].
922///
923/// Carries the widget's resolved geometry (as stored in its pod after layout) so
924/// paint code can position itself in the parent coordinate space, plus the v1
925/// animation-driver signal ([`PaintCtx::request_frame`]): a widget whose paint
926/// advances animation state (e.g. a scroll fling) must call it so the shell keeps
927/// scheduling frames even absent external input. The flag bubbles up through
928/// [`ChildPod::paint_child`] and out of [`crate::app::RenderRoot::paint`] as a
929/// [`PaintOutcome`], mirroring how [`EventCtx::request_redraw`] surfaces through
930/// [`crate::event::EventOutcome`].
931///
932/// A frame request also carries a [`TickClass`] (see [`PaintCtx::request_frame`]
933/// vs [`PaintCtx::request_frame_paced`]): the aggregate class over the whole
934/// paint pass — Transition-dominates-CosmeticLoop — surfaces on
935/// [`PaintOutcome::needs_frame_paced_only`] for the mobile frame gate to pace a
936/// purely-cosmetic frame.
937pub struct PaintCtx<'a> {
938    origin: Point,
939    size: Size,
940    needs_frame: bool,
941    /// Whether a widget whose animation changes its *layout* (not just paint)
942    /// asked, via [`PaintCtx::request_layout`], to have layout re-run next frame.
943    /// Bubbles up through [`ChildPod::paint_child`] exactly like `needs_frame`
944    /// and out of [`crate::app::RenderRoot::paint`] as [`PaintOutcome::needs_layout`],
945    /// which folds into the render root's pending [`ChangeFlags`] so the mobile
946    /// intra-frame layout skip re-runs layout while the animation is in flight.
947    /// [`PaintCtx::request_frame`] alone never sets it: a bare frame request
948    /// reaches the pending flags through `layout_path_dirty` instead, and is not
949    /// reported on [`PaintOutcome::needs_layout`].
950    needs_layout: bool,
951    /// Whether a pod at or below this context was marked layout-dirty by this
952    /// (sub)paint in a way that must force a pass — the paint pass's path bit,
953    /// the mirror of the event pass's (see [`EventCtx::is_layout_path_dirty`]).
954    /// [`ChildPod::paint_child`] sets it when its child asked for layout or for
955    /// an unpaced frame, on the one trailing paint after any request, and when
956    /// a child's bit is set, so every pod on the path is marked and
957    /// [`crate::app::RenderRoot::paint`] folds it into the pending
958    /// [`ChangeFlags`] `LAYOUT`: a shell gating its layout pass on those flags
959    /// then actually runs the pass that lays the dirtied pods out. A paced-only
960    /// ([`TickClass::CosmeticLoop`]) request still marks its pods but does not
961    /// set it, so the frame gate can keep pacing the loop.
962    layout_path_dirty: bool,
963    /// Whether any continuation-frame request this (sub)paint was
964    /// [`TickClass::Transition`] (unpaced, must run every vsync). The
965    /// max-lattice half of the tick-class aggregation: it starts `false` and
966    /// only ever flips `true` (a [`ChildPod::paint_child`]/
967    /// [`PaintCtx::with_hero_registry`] bubble ORs it up), so ANY Transition
968    /// request over the whole pass wins. Meaningful only when `needs_frame` is
969    /// set: `needs_frame && !frame_unpaced` is the "paced-only" state the frame
970    /// gate may throttle (see [`PaintCtx::frame_class`],
971    /// [`PaintCtx::needs_frame_paced_only`]). `request_layout` implies
972    /// Transition (a layout animation is user-visible motion), and the
973    /// unchanged `request_frame` sets it too (today's every-vsync behavior).
974    frame_unpaced: bool,
975    /// The **MIN** over every paced ([`TickClass::CosmeticLoop`]) request's
976    /// named interval this (sub)paint — the tightest cadence anything asked
977    /// for. `None` until the first paced request; a bare
978    /// [`PaintCtx::request_frame_paced`] contributes [`Duration::ZERO`] ("as
979    /// often as the theme cap allows"), which is the MIN-lattice's absorbing
980    /// element and therefore dominates any slower explicit request.
981    ///
982    /// Orthogonal to `frame_unpaced` (the class max-lattice): it is only
983    /// *meaningful* while the aggregate class is `CosmeticLoop`, but a later
984    /// Transition request never clears it. Bubbles up through
985    /// [`ChildPod::paint_child`]/[`PaintCtx::with_hero_registry`] exactly like
986    /// `frame_unpaced`, and surfaces on [`PaintOutcome::paced_interval`] for the
987    /// mobile frame gate to resolve against the theme's cap.
988    paced_interval: Option<Duration>,
989    ime_state: Option<ImeState>,
990    /// Whether the widget being painted currently holds the focus path — seeded
991    /// from its pod's recorded link *on the live session*
992    /// ([`ChildPod::holds_live_focus`]'s composition, spelled out inline) by
993    /// [`ChildPod::paint_child`], and from [`crate::app::RenderRoot`]'s
994    /// `focus_active` at the root. The paint-pass mirror of
995    /// [`EventCtx::has_focus`]: an editable gates its focus chrome (accent
996    /// border, caret, blink `request_frame`, IME republish) on it, so a
997    /// container-routed blur — which clears the pod's focus path without calling
998    /// the widget's `event()` — is finally observed here (see
999    /// [`PaintCtx::has_focus`]).
1000    has_focus: bool,
1001    /// Whether the widget being painted is on the recorded hover path (it or a
1002    /// descendant holds the link) — seeded from its
1003    /// pod's recorded hover stamp ([`ChildPod::hover_epoch`], compared against
1004    /// `hover_epoch` below) by [`ChildPod::paint_child`], and from
1005    /// [`crate::app::RenderRoot`]'s hover mirror at the root. The paint-pass
1006    /// mirror of [`EventCtx::is_hovered`] and the *authoritative* hover read for a
1007    /// widget's own chrome: a pointer that left the widget routes its next move
1008    /// elsewhere, so the widget's own `event()` never hears about the loss (see
1009    /// [`PaintCtx::is_hovered`]).
1010    hovered: bool,
1011    /// The live hover epoch — the epoch of the last completed hover pass, threaded
1012    /// from [`crate::app::RenderRoot::paint`] and copied into each child by
1013    /// [`ChildPod::paint_child`] (global, like the clock). A pod's recorded stamp
1014    /// counts as hovered only while it equals this; see the [`crate::event`] module
1015    /// docs for the epoch mechanism.
1016    hover_epoch: u64,
1017    /// The live focus epoch — the identity of the focus session the root
1018    /// currently holds, threaded from [`crate::app::RenderRoot::paint`] and
1019    /// copied into each child by [`ChildPod::paint_child`] (global, like the
1020    /// clock and the hover epoch).
1021    ///
1022    /// A pod's recorded focus stamp ([`ChildPod::focus_epoch`]) counts as a link
1023    /// on the live session only while it equals this, which is what strands the
1024    /// link of a branch the session has left — including one no container can
1025    /// reach to clear. See [`set_live_focus_session`].
1026    focus_epoch: u64,
1027    /// The shell-provided time for this frame, threaded from
1028    /// [`crate::app::RenderRoot::paint`] and seeded into each child by
1029    /// [`ChildPod::paint_child`]. Defaults to [`FrameTime::ZERO`] (the "no time
1030    /// available" fallback) for a paint context built without a clock (leaf unit
1031    /// tests, recorder scenes). A widget differences it against a stored earlier
1032    /// value to advance animation state — see [`PaintCtx::frame_time`].
1033    frame_time: FrameTime,
1034    /// The app's active theme, threaded down type-erased by the render root
1035    /// ([`crate::app::RenderRoot::paint`]) and seeded into each child by
1036    /// [`ChildPod::paint_child`], mirroring how `frame_time`/`has_focus` flow.
1037    /// `None` in bare-core tests and pre-theme apps — a supported state, so
1038    /// [`PaintCtx::theme_as`] returns `Option` rather than panicking.
1039    theme: Option<&'a dyn Any>,
1040    /// The window's insets ([`WindowInsets`]), threaded down by the render root
1041    /// ([`crate::app::RenderRoot::paint`]) and seeded into each child by
1042    /// [`ChildPod::paint_child`], mirroring how `frame_time`/`theme` flow. A
1043    /// concrete core-owned type carried by copy; global/origin-independent (see
1044    /// the [`crate::insets`] module docs). Defaults to the zero inset in
1045    /// bare-core tests and pre-insets apps.
1046    window_insets: WindowInsets,
1047    /// The shell's running count of frames the render thread has actually
1048    /// presented, threaded down by the render root
1049    /// ([`crate::app::RenderRoot::paint`]) and seeded into each child by
1050    /// [`ChildPod::paint_child`], mirroring how `frame_time`/`theme`/
1051    /// `window_insets` flow. `None` when no shell pushed one (bare-core tests,
1052    /// pre-wiring shells) so a widget can fall back — see
1053    /// [`PaintCtx::presented_frames`]. Unlike the clock/theme/insets this is a
1054    /// pure *observation* the render root stores WITHOUT dirtying
1055    /// [`ChangeFlags`] (see [`crate::app::RenderRoot::set_presented_frames`]), so
1056    /// a ticking presented count never forces a relayout or feeds the mobile
1057    /// frame gate.
1058    presented_frames: Option<u64>,
1059    /// A tagged-rect ("hero") reporter a container installs over a subtree via
1060    /// [`PaintCtx::with_hero_registry`], threaded to descendants by
1061    /// [`ChildPod::paint_child`] like the theme/clock. `None` in the normal
1062    /// case (no shared-element transition in flight), so
1063    /// [`PaintCtx::report_hero`] is a no-op returning [`HeroDirective::Normal`].
1064    hero: Option<&'a RefCell<HeroFrames>>,
1065    /// The absolute (global-coordinate) rectangle a scroll ancestor is currently
1066    /// showing, threaded down by [`ChildPod::paint_child`] like the theme/clock so
1067    /// a container can cull paint of children fully outside it. `None` (the
1068    /// default) means "no viewport constraint — paint everything", so every
1069    /// pre-culling behavior is unchanged. A [`ScrollView`](crate::app) sets it to
1070    /// its viewport via [`PaintCtx::constrain_visible_rect`], which *intersects*
1071    /// (never widens) a nested rect, so an inner scroll surface can only ever
1072    /// narrow the visible region an outer one already established. Consulted by
1073    /// `Flex` (see [`PaintCtx::visible_rect`]); coordinates match
1074    /// [`PaintCtx::origin`]'s absolute space, so a child's absolute bounds test
1075    /// directly against it.
1076    visible_rect: Option<Rect>,
1077    /// [`PlatformViewFrame`]s published this (sub)paint via
1078    /// [`PaintCtx::publish_platform_view`], in paint order.
1079    ///
1080    /// Unlike `ime_state` above (an `Option` — at most one focused editable
1081    /// publishes per pass), this is a `Vec`: any number of platform-view slots
1082    /// can paint in the same pass, so [`ChildPod::paint_child`] must EXTEND it
1083    /// from each child rather than overwrite, or every slot but the last
1084    /// child's would silently vanish. See [`PaintCtx::publish_platform_view`].
1085    platform_views: Vec<PlatformViewFrame>,
1086    /// Absolute-coordinate z-shield rects reported this (sub)paint via
1087    /// [`PaintCtx::report_input_shield`], in paint order.
1088    ///
1089    /// The same `Vec`-extend discipline as `platform_views` above and for the
1090    /// same reason: any number of shields can paint in one pass, so
1091    /// [`ChildPod::paint_child`] EXTENDS rather than overwrites. Consumed by
1092    /// the shell-side platform-view differ, which intersects them against each
1093    /// interactive slot's rect — core stays dumb about what a shield means (see
1094    /// [`PaintCtx::report_input_shield`]).
1095    input_shields: Vec<Rect>,
1096    /// Whether the shell created a translucent (alpha-channel, "Mode B") GPU
1097    /// surface for this frame, threaded down by the render root
1098    /// ([`crate::app::RenderRoot::paint`], seeded from
1099    /// [`crate::app::RenderRoot::set_surface_translucent`]) and copied into each
1100    /// child by [`ChildPod::paint_child`], mirroring `theme`/`window_insets`.
1101    /// `false` in the normal opaque ("Mode A") case, bare-core tests, and every
1102    /// desktop app. Read by the platform-view hole-punch (see
1103    /// [`PaintCtx::is_translucent`]).
1104    translucent: bool,
1105    /// The root's paint epoch, copied into each child like `translucent`: a
1106    /// pod painted under another epoch is not clean ([`PaintCtx::subtree_clean`]).
1107    /// Covers the paint-only global inputs the layout epoch does not (surface
1108    /// translucency) and every layout-epoch bump (theme, insets, window size).
1109    paint_epoch: u64,
1110    /// Whether retained layers are enabled ([`crate::app::RenderRoot::set_retained_layers`]);
1111    /// `false` makes [`PaintCtx::subtree_clean`] always answer `false`.
1112    retained_layers: bool,
1113    /// The renderer's resident-key ledger ([`crate::app::RenderRoot::set_retained_ledger`]),
1114    /// copied down like `paint_epoch`; `None` reads as empty.
1115    retained_ledger: Option<&'a crate::retained::RetainedLedger>,
1116    /// The serial the render root minted for this paint pass, copied down like
1117    /// `paint_epoch` ([`PaintCtx::paint_serial`]); `0` for a context no root
1118    /// numbered.
1119    paint_serial: u64,
1120    /// How many paint-time side-channel publications this (sub)paint made — an
1121    /// IME surface, a platform view, an input shield, an overlay registration,
1122    /// a selection-toolbar request, a hero report — summed up through
1123    /// [`ChildPod::paint_child`] like `platform_views`. See
1124    /// [`PaintCtx::paint_publications`].
1125    paint_publications: u32,
1126}
1127
1128impl<'a> PaintCtx<'a> {
1129    /// Create a paint context for a widget at `origin` with `size`.
1130    ///
1131    /// The frame time defaults to [`FrameTime::ZERO`] and no theme is threaded
1132    /// in; the render root seeds the real shell clock via
1133    /// [`PaintCtx::set_frame_time`] and the active theme via
1134    /// [`PaintCtx::set_theme`] before painting the root widget, and both flow to
1135    /// children through [`ChildPod::paint_child`].
1136    pub fn new(origin: Point, size: Size) -> Self {
1137        Self {
1138            origin,
1139            size,
1140            needs_frame: false,
1141            needs_layout: false,
1142            layout_path_dirty: false,
1143            frame_unpaced: false,
1144            paced_interval: None,
1145            ime_state: None,
1146            has_focus: false,
1147            hovered: false,
1148            hover_epoch: 0,
1149            focus_epoch: 0,
1150            frame_time: FrameTime::ZERO,
1151            theme: None,
1152            window_insets: WindowInsets::default(),
1153            presented_frames: None,
1154            hero: None,
1155            visible_rect: None,
1156            platform_views: Vec::new(),
1157            input_shields: Vec::new(),
1158            translucent: false,
1159            paint_epoch: 0,
1160            retained_layers: true,
1161            retained_ledger: None,
1162            paint_serial: 0,
1163            paint_publications: 0,
1164        }
1165    }
1166
1167    /// Attach the app's active theme, type-erased. Chainable builder mirroring
1168    /// [`LayoutCtx::with_theme`] — used by widget unit tests that paint against a
1169    /// known theme; the render root threads it via [`PaintCtx::set_theme`].
1170    pub fn with_theme(mut self, theme: &'a dyn Any) -> Self {
1171        self.theme = Some(theme);
1172        self
1173    }
1174
1175    /// Mark this paint pass as running against a translucent ("Mode B") surface.
1176    /// Chainable builder mirroring [`PaintCtx::with_theme`] — used by widget unit
1177    /// tests exercising the platform-view hole-punch; the render root threads the
1178    /// real flag via [`PaintCtx::set_translucent`] (see
1179    /// [`PaintCtx::is_translucent`]).
1180    pub fn with_translucent(mut self, translucent: bool) -> Self {
1181        self.translucent = translucent;
1182        self
1183    }
1184
1185    /// Thread a renderer's resident-key ledger into this pass. Chainable
1186    /// builder mirroring [`PaintCtx::with_theme`] — used by widget unit tests
1187    /// exercising a retained-page boundary, which paints with no bracket at all
1188    /// when no ledger is threaded ([`PaintCtx::retained_enabled`]); the render
1189    /// root threads the real one ([`crate::app::RenderRoot::set_retained_ledger`]).
1190    pub fn with_retained_ledger(mut self, ledger: &'a crate::retained::RetainedLedger) -> Self {
1191        self.retained_ledger = Some(ledger);
1192        self
1193    }
1194
1195    /// Build a paint context at `origin`/`size` seeded with an arbitrary
1196    /// [`FrameTime`], for exercising clock-dependent paint logic (caret blink,
1197    /// a hand-advanced [`crate::anim::AnimationController`], …) from outside
1198    /// this crate.
1199    ///
1200    /// [`PaintCtx::set_frame_time`] is deliberately `pub(crate)` — only
1201    /// [`crate::app::RenderRoot::paint`] (the shell-owned clock source) may
1202    /// advance it in a production build — so an app crate testing a widget
1203    /// from its own `src/` has no other way to construct a `PaintCtx` at a
1204    /// chosen time. This constructor is that sanctioned seam. Gated behind
1205    /// `cfg(test)`/the `test-support` feature so the symbol does not exist in
1206    /// a normal app build; see the crate's `test-support` feature docs in
1207    /// `Cargo.toml`.
1208    #[cfg(any(test, feature = "test-support"))]
1209    pub fn for_test(origin: Point, size: Size, frame_time: FrameTime) -> Self {
1210        let mut ctx = Self::new(origin, size);
1211        ctx.frame_time = frame_time;
1212        ctx
1213    }
1214
1215    /// Recover the threaded theme as `&T`, or `None` if no theme was threaded
1216    /// into this pass (a supported state — bare-core tests and pre-theme apps)
1217    /// or its concrete type differs from `T`.
1218    ///
1219    /// The paint-pass mirror of [`LayoutCtx::theme_as`]. Widgets that read
1220    /// `frust_theme::Theme` downcast through this (or the
1221    /// `Theme::from_paint_ctx` convenience wrapper).
1222    pub fn theme_as<T: Any>(&self) -> Option<&T> {
1223        self.theme?.downcast_ref::<T>()
1224    }
1225
1226    /// Seed the type-erased theme lent by the render root. Called by
1227    /// [`crate::app::RenderRoot::paint`] at the root and by
1228    /// [`ChildPod::paint_child`] for each child, mirroring how `frame_time` is
1229    /// threaded. The `Option<&dyn Any>` is copied down unchanged so a nested
1230    /// widget observes the same theme instance without re-borrowing the parent
1231    /// context.
1232    pub(crate) fn set_theme(&mut self, theme: Option<&'a dyn Any>) {
1233        self.theme = theme;
1234    }
1235
1236    /// The type-erased theme reference this context carries, for re-lending to a
1237    /// child context (copied, so it does not hold a borrow of `self`).
1238    pub(crate) fn theme_ref(&self) -> Option<&'a dyn Any> {
1239        self.theme
1240    }
1241
1242    /// The window's insets ([`WindowInsets`]) for this paint pass (a cheap
1243    /// copy). The paint-pass mirror of [`LayoutCtx::window_insets`]:
1244    /// global/origin-independent, so every widget reads the same value (unless a
1245    /// consuming ancestor narrowed it via [`PaintCtx::with_window_insets`]);
1246    /// defaults to the zero inset when no shell pushed one.
1247    pub fn window_insets(&self) -> WindowInsets {
1248        self.window_insets
1249    }
1250
1251    /// Seed the window insets lent by the render root. Called by
1252    /// [`crate::app::RenderRoot::paint`] at the root and by
1253    /// [`ChildPod::paint_child`] for each child, mirroring how `theme`/
1254    /// `frame_time` are threaded (copied down unchanged).
1255    pub(crate) fn set_window_insets(&mut self, insets: WindowInsets) {
1256        self.window_insets = insets;
1257    }
1258
1259    /// The window insets this context carries, for re-lending to a child context
1260    /// (copied, so it holds no borrow of `self`).
1261    pub(crate) fn window_insets_ref(&self) -> WindowInsets {
1262        self.window_insets
1263    }
1264
1265    /// Run `f` with `insets` installed as this context's window insets, then
1266    /// restore the previous value and return `f`'s result.
1267    ///
1268    /// The paint-pass mirror of [`LayoutCtx::with_window_insets`]. The value is
1269    /// otherwise root-seeded and copied down unchanged by
1270    /// [`ChildPod::paint_child`]; because `paint_child` copies it from the
1271    /// parent context it is handed, wrapping `paint_child` in this scope hands
1272    /// the override to the whole painted subtree. `SafeArea` does exactly that
1273    /// with the same consumed value it laid its child out under, so a widget
1274    /// that reads insets at paint time sees what it was laid out with.
1275    ///
1276    /// The restore is a plain assignment after `f` returns — there is no drop
1277    /// guard, so if `f` panics the override is not undone (the pass is being
1278    /// unwound anyway).
1279    pub fn with_window_insets<R>(
1280        &mut self,
1281        insets: WindowInsets,
1282        f: impl FnOnce(&mut Self) -> R,
1283    ) -> R {
1284        let saved = std::mem::replace(&mut self.window_insets, insets);
1285        let result = f(self);
1286        self.window_insets = saved;
1287        result
1288    }
1289
1290    /// The shell's running count of frames the render thread has actually
1291    /// presented, or `None` when no shell wired one in (bare-core tests,
1292    /// pre-wiring shells) — a supported state, so a widget can fall back to a
1293    /// paint-cadence measure.
1294    ///
1295    /// Under the render-thread split the UI thread paints faster than the render
1296    /// thread presents (a gate-skipped or coalesced frame is never presented), so
1297    /// a widget measuring *frames per second* must difference this presented
1298    /// count — not its own paint count — to report the rate a user actually sees
1299    /// (`examples/shadertoy`'s HUD is the reference consumer). A widget only ever
1300    /// *differences* two reads (`wrapping_sub`); the absolute value is a
1301    /// free-running monotonic counter it must never interpret directly. Seeded
1302    /// from the shell at the root ([`crate::app::RenderRoot::paint`]) and threaded
1303    /// unchanged into every child by [`ChildPod::paint_child`], mirroring
1304    /// `frame_time`.
1305    pub fn presented_frames(&self) -> Option<u64> {
1306        self.presented_frames
1307    }
1308
1309    /// Seed the shell's presented-frame count. Called by
1310    /// [`crate::app::RenderRoot::paint`] at the root and by
1311    /// [`ChildPod::paint_child`] for each child, mirroring how `frame_time`/
1312    /// `window_insets` are threaded (copied down unchanged).
1313    pub(crate) fn set_presented_frames(&mut self, presented: Option<u64>) {
1314        self.presented_frames = presented;
1315    }
1316
1317    /// The widget's absolute origin in window-space coordinates.
1318    ///
1319    /// This origin is accumulated as the paint pass descends the widget tree:
1320    /// [`ChildPod::paint_child`] adds the child's parent-relative `origin` to the
1321    /// parent's already-absolute `ctx.origin()`, threading the result down through
1322    /// nested levels. Contrast [`ChildPod::origin`], which is parent-relative and
1323    /// correct as documented, and [`EventCtx::origin`], which is **also**
1324    /// parent-relative — the event pass translates the event into the child's
1325    /// local space instead of accumulating the origin. This is therefore the only
1326    /// context origin an overlay, popup, or reported window-space rect may be
1327    /// anchored from.
1328    pub fn origin(&self) -> Point {
1329        self.origin
1330    }
1331
1332    /// The widget's resolved size.
1333    pub fn size(&self) -> Size {
1334        self.size
1335    }
1336
1337    /// Whether the widget being painted holds the focus path.
1338    ///
1339    /// Threaded down from the widget's pod ([`ChildPod::is_focused`], seeded at
1340    /// the root from `RenderRoot::focus_active`), this is the *authoritative*
1341    /// focus signal during paint — a widget must prefer it over any focus flag
1342    /// it tracks internally. A container-routed blur clears the pod's focus path
1343    /// but never dispatches to the widget's `event()`, so a widget-internal flag
1344    /// can lag; reading `has_focus()` here (and self-correcting the internal
1345    /// flag against it) lets the widget converge one frame after the blur.
1346    /// Mirrors [`EventCtx::has_focus`].
1347    pub fn has_focus(&self) -> bool {
1348        self.has_focus
1349    }
1350
1351    /// Seed whether the widget being painted holds focus. Called by
1352    /// [`ChildPod::paint_child`] (from the pod's recorded link, its session stamp
1353    /// and the chain above it) and by [`crate::app::RenderRoot::paint`] (from
1354    /// `focus_active`) — the paint mirror of [`EventCtx::set_has_focus`].
1355    pub(crate) fn set_has_focus(&mut self, has_focus: bool) {
1356        self.has_focus = has_focus;
1357    }
1358
1359    /// Whether the pointer is over the widget being painted **or over a descendant
1360    /// of it** — i.e. whether this widget is on the recorded hover path.
1361    ///
1362    /// A container therefore reads `true` while the pointer is over a claiming
1363    /// child, the way CSS `:hover` applies to an element while the pointer is over
1364    /// one of its descendants; a sibling or any other off-path widget reads
1365    /// `false`.
1366    ///
1367    /// This is the **authoritative** hover signal, for the same reason
1368    /// [`PaintCtx::has_focus`] is authoritative for focus, only more strongly: a
1369    /// pointer leaving a widget routes its next move to whatever it moved *onto*,
1370    /// so the widget it left never receives an event telling it so. A hover
1371    /// consumer keeps its own hover flag (that is what earns it a repaint on entry
1372    /// — see [`EventCtx::claim_hover`] for the whole contract) and **self-corrects
1373    /// that flag from this read every paint**, which is what fixes it whenever an
1374    /// event never came.
1375    ///
1376    /// Hover is opt-in: a widget in a tree where nothing ever calls
1377    /// [`claim_hover`](EventCtx::claim_hover) always reads `false` here.
1378    pub fn is_hovered(&self) -> bool {
1379        self.hovered
1380    }
1381
1382    /// Seed whether the widget being painted holds the hover link. Called by
1383    /// [`ChildPod::paint_child`] (from the pod's recorded stamp) and by
1384    /// [`crate::app::RenderRoot::paint`] (from its hover mirror) — the paint mirror
1385    /// of [`EventCtx::set_hovered`].
1386    pub(crate) fn set_hovered(&mut self, hovered: bool) {
1387        self.hovered = hovered;
1388    }
1389
1390    /// Seed the live hover epoch. Called by [`crate::app::RenderRoot::paint`] at
1391    /// the root and by [`ChildPod::paint_child`] for each child, mirroring how
1392    /// `frame_time` is threaded (copied down unchanged).
1393    pub(crate) fn set_hover_epoch(&mut self, epoch: u64) {
1394        self.hover_epoch = epoch;
1395    }
1396
1397    /// The live hover epoch a pod's recorded stamp is compared against.
1398    pub(crate) fn hover_epoch(&self) -> u64 {
1399        self.hover_epoch
1400    }
1401
1402    /// Seed the live focus epoch. Called by
1403    /// [`crate::app::RenderRoot::paint`] at the root (and for each floated pod)
1404    /// and threaded unchanged into every child by [`ChildPod::paint_child`],
1405    /// mirroring how the hover epoch flows.
1406    pub(crate) fn set_focus_epoch(&mut self, epoch: u64) {
1407        self.focus_epoch = epoch;
1408    }
1409
1410    /// The live focus epoch: a pod whose recorded stamp equals this holds a link
1411    /// on the session the root currently has.
1412    pub(crate) fn focus_epoch(&self) -> u64 {
1413        self.focus_epoch
1414    }
1415
1416    /// The shell-provided time for this frame (monotonic, arbitrary origin).
1417    ///
1418    /// This is the single shared clock the whole paint pass sees: seeded from the
1419    /// shell at the root ([`crate::app::RenderRoot::paint`]) and threaded
1420    /// unchanged into every child by [`ChildPod::paint_child`], so sibling and
1421    /// nested animations advance against one consistent timestamp. A widget may
1422    /// only *difference* it against an earlier `frame_time` it stored (via
1423    /// [`FrameTime::saturating_sub`] / [`crate::anim::AnimationController::advance`]),
1424    /// never interpret it absolutely — the origin varies per shell. Defaults to
1425    /// [`FrameTime::ZERO`] when no clock was threaded in (leaf unit tests).
1426    pub fn frame_time(&self) -> FrameTime {
1427        self.frame_time
1428    }
1429
1430    /// Seed the shell-provided frame time. Called by
1431    /// [`crate::app::RenderRoot::paint`] at the root and by
1432    /// [`ChildPod::paint_child`] for each child, mirroring how `has_focus` is
1433    /// threaded.
1434    pub(crate) fn set_frame_time(&mut self, frame_time: FrameTime) {
1435        self.frame_time = frame_time;
1436    }
1437
1438    /// Signal that this paint advanced animation state and needs to be
1439    /// re-invoked to continue, even with no intervening input event.
1440    ///
1441    /// The desktop shell honors this with a `window.request_redraw()` (its
1442    /// `ControlFlow::Wait` loop would otherwise idle); the mobile shells'
1443    /// continuous per-frame loops already schedule the next frame and can ignore
1444    /// it. Mirrors [`EventCtx::request_redraw`].
1445    ///
1446    /// This requests a [`TickClass::Transition`] frame — the unpaced,
1447    /// every-vsync class, unchanged from today's behavior. A widget whose next
1448    /// frame is a *pacable* decorative loop calls [`Self::request_frame_paced`]
1449    /// (or [`Self::request_frame_class`]) instead so the mobile frame gate may
1450    /// throttle it.
1451    pub fn request_frame(&mut self) {
1452        self.request_frame_class(TickClass::Transition);
1453    }
1454
1455    /// Request a continuation frame whose next tick is a *pacable* decorative
1456    /// loop ([`TickClass::CosmeticLoop`]) — a shimmer, an idle pulse, a spinner
1457    /// whose exact cadence is imperceptible.
1458    ///
1459    /// Bubbles like [`Self::request_frame`], but leaves the frame paceable: only
1460    /// if *every* request this frame is `CosmeticLoop` may the frame gate throttle
1461    /// it (see [`TickClass`]'s max-lattice aggregation). Any concurrent
1462    /// [`Self::request_frame`]/[`Self::request_layout`] elsewhere in the tree
1463    /// re-forces every-vsync cadence, so a paced request is never a downgrade of
1464    /// user-visible motion.
1465    ///
1466    /// This names no interval, which means "at the theme's own
1467    /// `MotionScheme::cosmetic_loop_rate`" — exactly
1468    /// `request_frame_paced_at(Duration::ZERO)`, since that rate is the cap every
1469    /// paced request resolves against (see [`Self::request_frame_paced_at`]).
1470    pub fn request_frame_paced(&mut self) {
1471        self.request_frame_paced_at(Duration::ZERO);
1472    }
1473
1474    /// Request a pacable decorative repaint **no more often than** once per
1475    /// `interval` — [`Self::request_frame_paced`] with an explicit cadence, for a
1476    /// loop far slower than the theme's cosmetic rate (a ~500ms caret blink
1477    /// against a 30Hz shimmer cap).
1478    ///
1479    /// Same class as [`Self::request_frame_paced`] ([`TickClass::CosmeticLoop`]);
1480    /// only the requested cadence differs. Two contracts govern the value:
1481    ///
1482    /// - **MIN-lattice aggregation.** Multiple paced requests in one paint pass
1483    ///   fold to the *tightest* interval, so every requester is repainted at
1484    ///   least as often as it asked. A 30Hz shimmer (a bare
1485    ///   [`Self::request_frame_paced`], i.e. [`Duration::ZERO`]) beside a 500ms
1486    ///   caret paces the frame at 30Hz — the caret is then simply repainted more
1487    ///   often than it needs, which is visually indistinguishable from its own
1488    ///   cadence and costs nothing beyond frames the shimmer already forced. That
1489    ///   asymmetry is by design: a *slow* request can never starve a fast one.
1490    /// - **The theme rate is a ceiling, not a target.** The frame gate resolves
1491    ///   the aggregate against `1 / MotionScheme::cosmetic_loop_rate` and takes
1492    ///   the *longer* of the two, so an interval tighter than the cap is clamped
1493    ///   to it. Motion that genuinely must run every vsync is not cosmetic —
1494    ///   use [`Self::request_frame`] ([`TickClass::Transition`]) for that.
1495    ///
1496    /// `frust-core` never reads a clock or a theme, so neither the MIN-lattice
1497    /// fold above nor the shell-side theme-cap clamp happens here: the
1498    /// aggregate rides out on [`PaintOutcome::paced_interval`] and the shell's
1499    /// frame gate resolves it.
1500    ///
1501    /// One clamp DOES happen here, though: `interval` is capped at
1502    /// [`Self::MAX_PACED_INTERVAL`] before it is folded in, so no caller
1503    /// (buggy or otherwise) can push a runaway value out to the shell's
1504    /// pacing arithmetic. See that constant's doc comment for the full
1505    /// rationale. This is a pure ceiling, never a target — every real cadence
1506    /// in this codebase (a bare [`Duration::ZERO`] "theme rate" request, the
1507    /// ~500ms caret blink above, or any plausible slow pulse) sits far below
1508    /// it and passes through completely unchanged.
1509    pub fn request_frame_paced_at(&mut self, interval: Duration) {
1510        self.needs_frame = true;
1511        self.merge_paced_interval(interval.min(Self::MAX_PACED_INTERVAL));
1512    }
1513
1514    /// Ceiling on the `interval` [`Self::request_frame_paced_at`] accepts.
1515    ///
1516    /// 10 seconds comfortably clears every real cosmetic cadence in this
1517    /// codebase — a bare [`Duration::ZERO`] "theme rate" request, the ~500ms
1518    /// caret blink, muxr's ~550ms blink, or any plausible slow pulse — while
1519    /// keeping the shell's downstream pacing arithmetic
1520    /// (`frust-shell-common::frame_gate`'s `interval * 2` /
1521    /// `interval.as_nanos() as u64`, `frust-shell-desktop::paced_wake`'s
1522    /// `Instant + interval`) far below overflow or truncation even at the
1523    /// widest legal input. [`Self::request_frame_paced_at`] is the single
1524    /// entry point every paced interval flows through (see
1525    /// [`Self::merge_paced_interval`]'s doc comment), so clamping here bounds
1526    /// every downstream consumer for free — this must never change behavior
1527    /// for any existing caller, since every shipped cadence is orders of
1528    /// magnitude under it.
1529    pub const MAX_PACED_INTERVAL: Duration = Duration::from_secs(10);
1530
1531    /// Request a continuation frame of an explicit [`TickClass`] — the general
1532    /// form behind [`Self::request_frame`] (Transition) and
1533    /// [`Self::request_frame_paced`] (CosmeticLoop, at the theme's own rate).
1534    ///
1535    /// Always sets `needs_frame`; a [`TickClass::Transition`] request additionally
1536    /// marks the aggregate unpaced (the max-lattice OR — see [`TickClass`]). A
1537    /// `CosmeticLoop` request never clears an already-unpaced aggregate, and —
1538    /// naming no interval — folds [`Duration::ZERO`] into the MIN-lattice like
1539    /// [`Self::request_frame_paced`] does.
1540    pub fn request_frame_class(&mut self, class: TickClass) {
1541        match class {
1542            TickClass::Transition => {
1543                self.needs_frame = true;
1544                self.frame_unpaced = true;
1545            }
1546            TickClass::CosmeticLoop => self.request_frame_paced(),
1547        }
1548    }
1549
1550    /// Fold one paced request's interval into this context's MIN-lattice
1551    /// aggregate — the single mutation point for `paced_interval`, called
1552    /// directly by [`Self::request_frame_paced_at`] and, for an already-`Some`
1553    /// bubbled interval, by [`Self::absorb_paced_interval`] (the two paint
1554    /// bubble sites' shared entry point).
1555    fn merge_paced_interval(&mut self, interval: Duration) {
1556        self.paced_interval = Some(match self.paced_interval {
1557            Some(current) => current.min(interval),
1558            None => interval,
1559        });
1560    }
1561
1562    /// Fold a bubbled child's paced interval into this context's own
1563    /// MIN-lattice aggregate — the one rule shared by both paced-interval
1564    /// absorb sites ([`ChildPod::paint_child`], [`Self::with_hero_registry`]):
1565    /// skip entirely when the child named none, rather than defaulting to
1566    /// [`Duration::ZERO`] (the lattice's own tightest/absorbing element,
1567    /// meaning "at the theme's own rate"). Folding that default in for a
1568    /// child that named no interval at all would silently re-tighten this
1569    /// context to the theme cap even though nothing downstream actually asked
1570    /// for a frame at all — see [`Self::request_frame_paced_at`]'s MIN-lattice
1571    /// contract.
1572    fn absorb_paced_interval(&mut self, interval: Option<Duration>) {
1573        if let Some(interval) = interval {
1574            self.merge_paced_interval(interval);
1575        }
1576    }
1577
1578    /// Whether a continuation frame was requested during this (sub)paint.
1579    pub fn needs_frame(&self) -> bool {
1580        self.needs_frame
1581    }
1582
1583    /// The aggregate [`TickClass`] requested during this (sub)paint, or `None`
1584    /// if no frame was requested.
1585    ///
1586    /// Follows the [`TickClass`] max-lattice: [`TickClass::Transition`] if any
1587    /// request this pass was Transition-class (unpaced), else
1588    /// [`TickClass::CosmeticLoop`] when at least one paced request was made and
1589    /// no Transition one was. `None` means "as today — no continuation frame".
1590    pub fn frame_class(&self) -> Option<TickClass> {
1591        if !self.needs_frame {
1592            None
1593        } else if self.frame_unpaced {
1594            Some(TickClass::Transition)
1595        } else {
1596            Some(TickClass::CosmeticLoop)
1597        }
1598    }
1599
1600    /// Whether a frame was requested and *every* request this (sub)paint was
1601    /// [`TickClass::CosmeticLoop`] — the paced-only state the mobile frame gate
1602    /// may throttle. Convenience for
1603    /// `frame_class() == Some(TickClass::CosmeticLoop)`.
1604    pub fn needs_frame_paced_only(&self) -> bool {
1605        self.needs_frame && !self.frame_unpaced
1606    }
1607
1608    /// The tightest (MIN) interval any paced request named during this
1609    /// (sub)paint, or `None` if no paced request was made at all.
1610    ///
1611    /// [`Duration::ZERO`] — what a bare [`Self::request_frame_paced`] folds in —
1612    /// means "at the theme's own `cosmetic_loop_rate`", so `Some(Duration::ZERO)`
1613    /// and `None` resolve identically at the frame gate; the distinction is only
1614    /// whether *any* paced request was made. Meaningful only while
1615    /// [`Self::frame_class`] is [`TickClass::CosmeticLoop`] — a concurrent
1616    /// Transition request makes the whole frame unpaced, at which point no
1617    /// interval applies (see [`PaintOutcome::paced_interval`]).
1618    pub fn paced_interval(&self) -> Option<Duration> {
1619        self.paced_interval
1620    }
1621
1622    /// Signal that this paint advanced animation state that changes the widget's
1623    /// *layout* (not just its paint), so layout must re-run next frame.
1624    ///
1625    /// This is the layout counterpart to [`Self::request_frame`]: a widget whose
1626    /// animation only repaints (a color fade, a caret blink) calls
1627    /// `request_frame` alone — its path is still laid out again, conservatively,
1628    /// but that pass is mostly skipped and nothing reports
1629    /// [`PaintOutcome::needs_layout`] — whereas a widget whose animation
1630    /// resizes/repositions its children (an expanding accordion) calls this so
1631    /// layout is re-run while the animation is in flight. The flag bubbles up through
1632    /// [`ChildPod::paint_child`] exactly like `needs_frame` and out of
1633    /// [`crate::app::RenderRoot::paint`] as [`PaintOutcome::needs_layout`], which
1634    /// folds into the render root's pending [`crate::view::ChangeFlags`]
1635    /// (`LAYOUT`) so the *next* frame relayouts.
1636    ///
1637    /// Calling this also implies [`Self::request_frame`] (a widget animating its
1638    /// layout necessarily wants another frame), so a caller needs only one call
1639    /// per animating-layout frame. That implied frame is
1640    /// [`TickClass::Transition`] (unpaced): a layout animation is user-visible
1641    /// motion, so it never leaves the frame paceable.
1642    pub fn request_layout(&mut self) {
1643        self.needs_layout = true;
1644        // A widget animating its layout necessarily wants another frame; setting
1645        // `needs_frame` too means one call suffices per animating-layout frame.
1646        // A layout animation is user-visible motion, so the implied frame is
1647        // Transition-class (unpaced) — mark the aggregate accordingly.
1648        self.request_frame_class(TickClass::Transition);
1649    }
1650
1651    /// Whether a layout re-run was requested during this (sub)paint.
1652    pub fn needs_layout(&self) -> bool {
1653        self.needs_layout
1654    }
1655
1656    /// Whether this (sub)paint marked a pod at or below this context
1657    /// layout-dirty (see the `layout_path_dirty` field).
1658    pub(crate) fn is_layout_path_dirty(&self) -> bool {
1659        self.layout_path_dirty
1660    }
1661
1662    /// Publish the focused editable's current IME surface during paint.
1663    ///
1664    /// The event pass ([`EventCtx::publish_ime_state`]) refreshes the shell's
1665    /// IME view on every edit, but an *app-driven* controlled change — a
1666    /// submit clearing the field, applied by the next rebuild rather than by
1667    /// an event — never crosses the event pass, so the event-published value
1668    /// goes stale. A focused editable therefore also republishes here, in the
1669    /// paint that runs after every rebuild, so [`crate::app::RenderRoot::ime_state`]
1670    /// tracks the field's current text/caret regardless of what drove the
1671    /// change. Bubbles up through [`ChildPod::paint_child`], mirroring
1672    /// [`Self::request_frame`].
1673    pub fn publish_ime_state(&mut self, state: ImeState) {
1674        self.note_publication();
1675        self.ime_state = Some(state);
1676    }
1677
1678    /// Take the IME surface published during this (sub)paint, if any.
1679    pub fn take_ime_state(&mut self) -> Option<ImeState> {
1680        self.ime_state.take()
1681    }
1682
1683    /// Publish a platform-view child's paint-time frame (a
1684    /// `PlatformViewSlot`) for this paint pass.
1685    ///
1686    /// Pushes onto a `Vec` rather than setting an `Option` — deliberately NOT
1687    /// the same shape as [`PaintCtx::publish_ime_state`]. IME state has a
1688    /// single focused surface at most, so an overwrite is correct there; a
1689    /// platform view has no such "the" instance, so two slots publishing in
1690    /// one pass must both survive. [`ChildPod::paint_child`] bubbles this by
1691    /// `extend`, never overwrite, for exactly that reason.
1692    pub fn publish_platform_view(&mut self, frame: PlatformViewFrame) {
1693        self.note_publication();
1694        self.platform_views.push(frame);
1695    }
1696
1697    /// Take (and clear) every platform-view frame published during this
1698    /// (sub)paint, in paint order.
1699    pub fn take_platform_views(&mut self) -> Vec<PlatformViewFrame> {
1700        std::mem::take(&mut self.platform_views)
1701    }
1702
1703    /// Report an absolute-coordinate region where frust content painted OVER a
1704    /// platform-view slot must keep winning pointer input (the "z-shield").
1705    ///
1706    /// The auto-collection half of the Mode B input contract: an interactive
1707    /// slot hands a touch-DOWN inside its rect to the
1708    /// native sibling, EXCEPT inside a shield. `frust-widgets`' `shield(child)`
1709    /// wrapper is the reporter — it paints its child unchanged and reports its
1710    /// own painted rect here — so an app marks chrome that overlaps a slot
1711    /// rather than hand-listing rects on the slot itself.
1712    ///
1713    /// Core stays dumb, exactly as it does for [`PlatformViewFrame`]: this is a
1714    /// flat, pass-scoped rect list with no slot association at all. The
1715    /// shell-side differ (`frust-shell-common::platform_view`) owns the
1716    /// intersection rule that decides which shields belong to which slot.
1717    /// Pushes (never overwrites) — see the `input_shields` field doc.
1718    pub fn report_input_shield(&mut self, rect: Rect) {
1719        self.note_publication();
1720        self.input_shields.push(rect);
1721    }
1722
1723    /// Take (and clear) every z-shield rect reported during this (sub)paint, in
1724    /// paint order. The [`PaintCtx::take_platform_views`] sibling for the shield
1725    /// channel (see [`PaintCtx::report_input_shield`]).
1726    pub fn take_input_shields(&mut self) -> Vec<Rect> {
1727        std::mem::take(&mut self.input_shields)
1728    }
1729
1730    /// Float `entry`'s pod above the whole app for this frame, and register its
1731    /// rect for the next frame's input routing.
1732    ///
1733    /// # The owner must not paint the pod
1734    ///
1735    /// A registered pod is painted by [`crate::app::RenderRoot::paint`], **after**
1736    /// the main tree — that is the only way it escapes its owner's paint order
1737    /// and every ancestor's clip. An owner that also paints it itself draws the
1738    /// surface twice: once clipped in place, once floated.
1739    ///
1740    /// # Per pass, in registration order
1741    ///
1742    /// The registry is cleared when each paint pass begins, so a surface stays
1743    /// alive only while its owner keeps registering it — there is nothing to
1744    /// unregister, and an owner that stops (or is unmounted) simply disappears
1745    /// from the routing table after the next paint. Within a
1746    /// [band](crate::overlay::OverlayBand), later registration paints and
1747    /// hit-tests above earlier; the band itself outranks registration order.
1748    ///
1749    /// [`OverlayEntry::window_rect`](crate::overlay::OverlayEntry::window_rect)
1750    /// is absolute logical window space, so an owner computes it from
1751    /// [`PaintCtx::origin`] — the only absolute anchor a widget has. Recomputing
1752    /// it every paint is what makes an anchored surface follow its owner with no
1753    /// subscription of any kind.
1754    ///
1755    /// Registering outside a root-driven paint pass (a leaf unit test painting a
1756    /// bare [`PaintCtx`]) is harmless: the entry is dropped by the next real
1757    /// pass's clear rather than leaking into it.
1758    pub fn register_overlay(&mut self, entry: crate::overlay::OverlayEntry) {
1759        self.note_publication();
1760        crate::overlay::register(entry);
1761    }
1762
1763    /// Publish "this field is focused, these verbs apply, and here is where a
1764    /// menu would go" for this frame.
1765    ///
1766    /// Resolved by [`crate::app::RenderRoot::paint`] into
1767    /// [`RenderRoot::selection_toolbar`](crate::app::RenderRoot::selection_toolbar)
1768    /// plus a generation a shell diffs
1769    /// ([`selection_toolbar_generation`](crate::app::RenderRoot::selection_toolbar_generation)),
1770    /// for the platform edit-menu route.
1771    ///
1772    /// **Publish under either policy.** A field drawing its own toolbar through
1773    /// the overlay portal ([`crate::selection_toolbar::SelectionToolbarPolicy::Framework`])
1774    /// publishes this too: it costs one pointer-sized write, and it keeps a single
1775    /// code path rather than one per route.
1776    ///
1777    /// **A focused field publishes whether or not it has a selection, and whether
1778    /// or not any bar is up.** The verbs are a level a platform responder chain
1779    /// must be able to read at any moment — a hardware Cmd+C/X/V/A is asked for
1780    /// with no menu on screen — so gating the publish on a bar being open is what
1781    /// used to leave those shortcuts unanswerable. Whether a menu should be
1782    /// *presented* rides in the request's own flag instead.
1783    ///
1784    /// Pass-scoped and last-writer-wins, like every other paint-time request: a
1785    /// pass in which nothing publishes resolves to "no field is focused", which is
1786    /// what puts the toolbar away on a blur without any widget having to retract
1787    /// anything.
1788    pub fn publish_selection_toolbar(
1789        &mut self,
1790        request: crate::selection_toolbar::SelectionToolbarRequest,
1791    ) {
1792        self.note_publication();
1793        crate::selection_toolbar::publish(request);
1794    }
1795
1796    /// Report a tagged ("hero") element's absolute paint `bounds` and read back
1797    /// what it should do this frame.
1798    ///
1799    /// A no-op returning [`HeroDirective::Normal`] unless a container installed
1800    /// a reporter via [`PaintCtx::with_hero_registry`] over this subtree
1801    /// (the normal case — no shared-element transition in flight). When a
1802    /// reporter is installed, `bounds` is recorded (page-local, i.e. relative
1803    /// to the reporter's reference origin, so it stays stable under a page's
1804    /// per-frame animated transition offset), and the directive the installer
1805    /// set for `tag` is returned — [`HeroDirective::Suppress`] (skip painting,
1806    /// this endpoint is morphed by its counterpart) or
1807    /// [`HeroDirective::Morph`] (repaint under a rect→rect transform to the
1808    /// morph destination).
1809    pub fn report_hero(&mut self, tag: &str, bounds: Rect) -> HeroDirective {
1810        self.note_publication();
1811        match self.hero {
1812            Some(cell) => {
1813                let mut frames = cell.borrow_mut();
1814                let local = Rect::from_origin_size(
1815                    bounds.origin() - frames.reference.to_vec2(),
1816                    bounds.size(),
1817                );
1818                frames.captured.insert(tag.to_string(), local);
1819                frames
1820                    .directives
1821                    .get(tag)
1822                    .copied()
1823                    .unwrap_or(HeroDirective::Normal)
1824            }
1825            None => HeroDirective::Normal,
1826        }
1827    }
1828
1829    /// Whether a shared-element ("hero") transition is currently in flight over
1830    /// this subtree — i.e. an ancestor installed a hero reporter via
1831    /// [`PaintCtx::with_hero_registry`], the same condition that makes
1832    /// [`PaintCtx::report_hero`] record rather than no-op.
1833    ///
1834    /// The public, boolean sibling of the crate-private
1835    /// [`PaintCtx::hero_ref`], exposed so a container that culls far-offscreen
1836    /// children (a `Flex` under a `ScrollView`) can *stop* culling while a hero
1837    /// is morphing: a tagged descendant scrolled beyond the warm margin would
1838    /// otherwise never paint, and so never report its bounds
1839    /// ([`PaintCtx::report_hero`]) for the morph. `false` in the normal case
1840    /// (no transition), so culling is unaffected off the transition path.
1841    pub fn hero_active(&self) -> bool {
1842        self.hero.is_some()
1843    }
1844
1845    /// Run `f` with a paint context that has `registry` installed as the
1846    /// tagged-rect ("hero") reporter, threading this context's clock/theme/
1847    /// focus/geometry down unchanged. A container paints a subtree inside the
1848    /// closure; descendants report through [`PaintCtx::report_hero`], and the
1849    /// container reads the captured rects back from `registry` afterward. Any
1850    /// continuation-frame request or IME publish made inside bubbles back onto
1851    /// `self`, mirroring [`ChildPod::paint_child`]'s absorb.
1852    pub fn with_hero_registry(
1853        &mut self,
1854        registry: &RefCell<HeroFrames>,
1855        f: impl FnOnce(&mut PaintCtx),
1856    ) {
1857        let mut child = PaintCtx {
1858            origin: self.origin,
1859            size: self.size,
1860            needs_frame: false,
1861            needs_layout: false,
1862            layout_path_dirty: false,
1863            frame_unpaced: false,
1864            paced_interval: None,
1865            ime_state: None,
1866            has_focus: self.has_focus,
1867            hovered: self.hovered,
1868            hover_epoch: self.hover_epoch,
1869            focus_epoch: self.focus_epoch,
1870            frame_time: self.frame_time,
1871            theme: self.theme,
1872            window_insets: self.window_insets,
1873            presented_frames: self.presented_frames,
1874            hero: Some(registry),
1875            visible_rect: self.visible_rect,
1876            platform_views: Vec::new(),
1877            input_shields: Vec::new(),
1878            translucent: self.translucent,
1879            paint_epoch: self.paint_epoch,
1880            retained_layers: self.retained_layers,
1881            retained_ledger: self.retained_ledger,
1882            paint_serial: self.paint_serial,
1883            paint_publications: 0,
1884        };
1885        f(&mut child);
1886        self.paint_publications = self
1887            .paint_publications
1888            .saturating_add(child.paint_publications);
1889        if child.needs_frame {
1890            self.needs_frame = true;
1891        }
1892        if child.needs_layout {
1893            self.needs_layout = true;
1894        }
1895        if child.layout_path_dirty {
1896            self.layout_path_dirty = true;
1897        }
1898        // Max-lattice OR: any Transition request inside dominates, keeping the
1899        // aggregate unpaced (mirrors `ChildPod::paint_child`'s absorb).
1900        if child.frame_unpaced {
1901            self.frame_unpaced = true;
1902        }
1903        // MIN-lattice fold of the paced interval, the orthogonal half of the
1904        // same absorb: a slower interval inside never loosens the outer
1905        // aggregate, and a tighter one tightens it. `absorb_paced_interval`
1906        // is the shared rule with `ChildPod::paint_child`'s own bubble below —
1907        // skip on `None` rather than folding in `Duration::ZERO`.
1908        self.absorb_paced_interval(child.paced_interval);
1909        if let Some(ime) = child.ime_state.take() {
1910            self.ime_state = Some(ime);
1911        }
1912        // EXTEND, never overwrite — mirrors `ChildPod::paint_child`'s absorb;
1913        // see `PaintCtx::publish_platform_view`'s doc comment for why this
1914        // channel is a `Vec` merge rather than the `ime_state` `Option` merge
1915        // above.
1916        self.platform_views.extend(child.platform_views);
1917        // The z-shield channel merges the same way, for the same reason (see
1918        // `PaintCtx::report_input_shield`).
1919        self.input_shields.extend(child.input_shields);
1920    }
1921
1922    /// Seed the hero reporter lent by an ancestor. Called by
1923    /// [`ChildPod::paint_child`] for each child, mirroring how `theme` is
1924    /// threaded, so a nested hero wrapper observes the same reporter.
1925    pub(crate) fn set_hero(&mut self, hero: Option<&'a RefCell<HeroFrames>>) {
1926        self.hero = hero;
1927    }
1928
1929    /// The hero reporter this context carries, for re-lending to a child
1930    /// context (copied, so it does not hold a borrow of `self`).
1931    pub(crate) fn hero_ref(&self) -> Option<&'a RefCell<HeroFrames>> {
1932        self.hero
1933    }
1934
1935    /// The absolute-coordinate visible rectangle a scroll ancestor has threaded
1936    /// down, or `None` when no viewport constraint is in effect (paint
1937    /// everything). A container that culls offscreen children (`Flex` under a
1938    /// `ScrollView`) tests each child's absolute bounds against this; a widget
1939    /// with no interest in culling ignores it entirely. See
1940    /// [`PaintCtx::constrain_visible_rect`] for how a scroll surface sets it.
1941    pub fn visible_rect(&self) -> Option<Rect> {
1942        self.visible_rect
1943    }
1944
1945    /// Constrain the threaded visible rectangle to `rect` (in absolute paint
1946    /// coordinates), the seam a [`ScrollView`](crate::app) uses to publish its
1947    /// viewport to descendants.
1948    ///
1949    /// When no rect is threaded yet this installs `rect`; when one already is
1950    /// (a nested scroll surface), the two are **intersected** — the visible
1951    /// region can only ever narrow, never widen, as scroll surfaces nest, so an
1952    /// inner viewport never re-reveals content an outer one clipped away. The
1953    /// value flows to children unchanged via [`ChildPod::paint_child`], mirroring
1954    /// how `window_insets`/`theme` are threaded.
1955    pub fn constrain_visible_rect(&mut self, rect: Rect) {
1956        self.visible_rect = Some(match self.visible_rect {
1957            Some(existing) => existing.intersect(rect),
1958            None => rect,
1959        });
1960    }
1961
1962    /// Seed the visible rectangle lent by an ancestor. Called by
1963    /// [`ChildPod::paint_child`] for each child, mirroring how `theme` is
1964    /// threaded (copied down unchanged), so a descendant container observes the
1965    /// same viewport constraint the scroll ancestor established.
1966    pub(crate) fn set_visible_rect(&mut self, rect: Option<Rect>) {
1967        self.visible_rect = rect;
1968    }
1969
1970    /// The visible rectangle this context carries, for re-lending to a child
1971    /// context (copied, so it holds no borrow of `self`).
1972    pub(crate) fn visible_rect_ref(&self) -> Option<Rect> {
1973        self.visible_rect
1974    }
1975
1976    /// Whether the shell created a translucent (alpha-channel, "Mode B") GPU
1977    /// surface for this frame — threaded from the render root and copied down to
1978    /// every descendant like the theme/insets.
1979    ///
1980    /// `false` in the normal opaque ("Mode A") case, bare-core tests, and every
1981    /// desktop app. The platform-view hole-punch reads it: a slot only clears
1982    /// its rect ([`PaintScene::clear_rect`]) when this is `true`, so punching
1983    /// never erases app content on an opaque surface (see `frust-widgets`'
1984    /// `PlatformViewWidget::paint`).
1985    pub fn is_translucent(&self) -> bool {
1986        self.translucent
1987    }
1988
1989    /// Seed the shell's surface-translucency flag. Called by
1990    /// [`crate::app::RenderRoot::paint`] at the root and by
1991    /// [`ChildPod::paint_child`] for each child, mirroring how `theme` is
1992    /// threaded (copied down unchanged).
1993    pub(crate) fn set_translucent(&mut self, translucent: bool) {
1994        self.translucent = translucent;
1995    }
1996
1997    /// Seed the root's paint epoch, the retained-layers switch, the renderer's
1998    /// ledger and the pass's paint serial. Called by [`crate::app::RenderRoot`]
1999    /// at each paint root and by [`ChildPod::paint_child`] for each child
2000    /// (copied down unchanged).
2001    pub(crate) fn set_paint_state(
2002        &mut self,
2003        paint_epoch: u64,
2004        retained_layers: bool,
2005        retained_ledger: Option<&'a crate::retained::RetainedLedger>,
2006        paint_serial: u64,
2007    ) {
2008        self.paint_epoch = paint_epoch;
2009        self.retained_layers = retained_layers;
2010        self.retained_ledger = retained_ledger;
2011        self.paint_serial = paint_serial;
2012    }
2013
2014    /// The serial [`crate::app::RenderRoot::paint`] minted for this pass: `1`
2015    /// for a root's first paint, one more for each paint after it, and `0` for
2016    /// a context no root numbered ([`PaintCtx::new`], [`PaintCtx::for_test`]).
2017    /// The same value leaves the pass on [`PaintOutcome::paint_serial`], rides
2018    /// to the renderer with the scene, and comes back as the
2019    /// [`frame`](crate::retained::RetainedLedger::frame) of the ledger
2020    /// published once that scene was committed.
2021    ///
2022    /// A boundary widget remembers the serial it recorded its page in and
2023    /// hands it to [`PaintCtx::retained_available`]: the ledger vouches for a
2024    /// resident page only once it covers the scene that recorded it.
2025    pub fn paint_serial(&self) -> u64 {
2026        self.paint_serial
2027    }
2028
2029    /// Whether the renderer still holds a retained page for `key` at `rect_px`
2030    /// (physical-pixel extent, rounded to whole pixels) and `scale`, **and**
2031    /// had committed the scene painted at `recorded_serial` (the
2032    /// [`PaintCtx::paint_serial`] of the pass that recorded the page) when it
2033    /// said so — the query a boundary widget makes to choose between drawing
2034    /// the page (`PaintScene::draw_retained`) and painting its body again.
2035    ///
2036    /// Answers from the ledger the renderer published after an earlier frame,
2037    /// so it can be stale; the serial is what keeps a stale answer from naming
2038    /// an older recording than the widget's latest one (a scene dropped on its
2039    /// way to the renderer leaves the previous page resident under the same
2040    /// key — see [`crate::retained`]). Always `false` when retained layers are
2041    /// switched off ([`crate::app::RenderRoot::set_retained_layers`]) or no
2042    /// ledger was set.
2043    pub fn retained_available(
2044        &self,
2045        key: crate::retained::RetainedKey,
2046        rect_px: kurbo::Size,
2047        scale: f32,
2048        recorded_serial: u64,
2049    ) -> bool {
2050        let extent = (
2051            rect_px.width.round().max(0.0) as u32,
2052            rect_px.height.round().max(0.0) as u32,
2053        );
2054        self.retained_layers
2055            && self
2056                .retained_ledger
2057                .is_some_and(|l| l.covers(recorded_serial) && l.is_resident(key, extent, scale))
2058    }
2059
2060    /// Whether nothing under `pod` changed since it was last painted — the
2061    /// query a boundary widget makes about the child pod it owns **before**
2062    /// painting it. The boundary's own pod is dirty whenever anything beneath
2063    /// it changed, so ask about the child, not about yourself.
2064    ///
2065    /// Clean means: no dirty source reached the pod since its last paint (a
2066    /// PAINT/LAYOUT rebuild, layout running, a frame or layout request, a
2067    /// consumed or redraw-requesting event), and the global paint epoch (theme,
2068    /// insets, window size, surface translucency, the hover link, the focus
2069    /// session) is unchanged. Always `false` when retained layers are switched
2070    /// off ([`crate::app::RenderRoot::set_retained_layers`]), and for a pod
2071    /// that has never been painted.
2072    ///
2073    /// **Hover and focus.** [`PaintCtx::is_hovered`] and
2074    /// [`PaintCtx::has_focus`] are read at paint time from links the root moves
2075    /// without visiting the pod that loses one (the pointer leaving a claimant
2076    /// for a widget elsewhere routes along the new hit path only; a new focus
2077    /// session strands the old holder by arithmetic). So the root folds both
2078    /// into the paint epoch: a paint that follows a hover-link change (a hover
2079    /// beginning, moving to another claimant, or ending) or a focus-session
2080    /// change bumps it, and every boundary records its page again on that
2081    /// paint. The cost is one re-record per boundary per hover/focus
2082    /// transition — a transition already repaints the frame; a pointer move that
2083    /// keeps the same claimant, or none, moves nothing.
2084    pub fn subtree_clean(&self, pod: &ChildPod) -> bool {
2085        self.retained_layers && !pod.paint_dirty && pod.painted_epoch == self.paint_epoch
2086    }
2087
2088    /// Whether a boundary widget may bracket its body at all: retained layers
2089    /// are switched on ([`crate::app::RenderRoot::set_retained_layers`]) and a
2090    /// renderer ledger was threaded down (every [`crate::app::RenderRoot`]
2091    /// paint threads one, empty until a shell publishes; a bare
2092    /// [`PaintCtx::new`] carries none). When `false` a boundary paints its body
2093    /// with no bracket — the renderer would still record a layer for one.
2094    pub fn retained_enabled(&self) -> bool {
2095        self.retained_layers && self.retained_ledger.is_some()
2096    }
2097
2098    /// How many paint-time side-channel publications this (sub)paint has made
2099    /// so far: [`PaintCtx::publish_ime_state`],
2100    /// [`PaintCtx::publish_platform_view`], [`PaintCtx::report_input_shield`],
2101    /// [`PaintCtx::register_overlay`], [`PaintCtx::publish_selection_toolbar`]
2102    /// and [`PaintCtx::report_hero`] calls, including every descendant's.
2103    ///
2104    /// A boundary widget reads it before and after painting its body: those
2105    /// channels are re-published every paint, so a body that used one must be
2106    /// painted every frame — drawing its retained page instead would publish
2107    /// nothing, and the surface, view or rect would vanish.
2108    pub fn paint_publications(&self) -> u32 {
2109        self.paint_publications
2110    }
2111
2112    /// Count one paint-time side-channel publication (see
2113    /// [`PaintCtx::paint_publications`]).
2114    fn note_publication(&mut self) {
2115        self.paint_publications = self.paint_publications.saturating_add(1);
2116    }
2117}
2118
2119/// A tagged-rect reporter threaded through the paint pass, letting a container
2120/// discover where tagged ("hero") descendants painted and drive a
2121/// shared-element morph between two of them across a navigation transition.
2122///
2123/// Generic vocabulary — `frust-core` carries no navigation knowledge here,
2124/// the same way its [`semantics`](crate::semantics) node collector carries no
2125/// widget-catalog knowledge. A container installs one over a subtree with
2126/// [`PaintCtx::with_hero_registry`]; descendants report through
2127/// [`PaintCtx::report_hero`].
2128#[derive(Debug, Default)]
2129pub struct HeroFrames {
2130    /// The absolute top-left of the enclosing surface this paint, subtracted
2131    /// from each reported rect so captures are stored surface-local (stable
2132    /// across a page's per-frame animated transition offset).
2133    reference: Point,
2134    /// Surface-local rects reported by tagged descendants this paint.
2135    captured: HashMap<String, Rect>,
2136    /// Per-tag paint directive the installer set for this paint.
2137    directives: HashMap<String, HeroDirective>,
2138}
2139
2140impl HeroFrames {
2141    /// A reporter with `reference` as the enclosing surface's absolute top-left
2142    /// and per-tag paint `directives` (empty for a pure discovery pass).
2143    pub fn new(reference: Point, directives: HashMap<String, HeroDirective>) -> Self {
2144        Self {
2145            reference,
2146            captured: HashMap::new(),
2147            directives,
2148        }
2149    }
2150
2151    /// The surface-local rects captured this paint, keyed by tag.
2152    pub fn captured(&self) -> &HashMap<String, Rect> {
2153        &self.captured
2154    }
2155
2156    /// Consume the reporter, returning the captured surface-local rects.
2157    pub fn into_captured(self) -> HashMap<String, Rect> {
2158        self.captured
2159    }
2160}
2161
2162/// What a reported hero should do this paint — the reply
2163/// [`PaintCtx::report_hero`] hands back to a tagged ("hero") wrapper.
2164#[derive(Clone, Copy, Debug, PartialEq)]
2165pub enum HeroDirective {
2166    /// Paint normally (no active flight involves this tag) — the default.
2167    Normal,
2168    /// Skip painting the child: this endpoint is being morphed by the other
2169    /// surface's matching hero, so it must not also paint at its rest position.
2170    Suppress,
2171    /// Paint the child under a transform mapping the hero's own absolute paint
2172    /// bounds onto `dest` (absolute) — the morph overlay for this frame.
2173    Morph {
2174        /// The absolute destination rect the hero's bounds morph onto.
2175        dest: Rect,
2176    },
2177}
2178
2179/// A process-wide monotonic counter backing [`next_slot_id`].
2180static NEXT_SLOT_ID: AtomicU64 = AtomicU64::new(1);
2181
2182/// Allocate a fresh, stable platform-view slot id.
2183///
2184/// Called once per widget instance at construction (a
2185/// `PlatformViewSlot`) — the same stability class as [`ChildPod`]'s
2186/// semantics base id: identity that must survive a tree reorder, so it is
2187/// never derived from tree position. A flat process-wide counter rather than
2188/// a per-[`crate::app::RenderRoot`] allocator, since a widget has no
2189/// `RenderRoot` handle to draw one from at construction time.
2190pub fn next_slot_id() -> u64 {
2191    NEXT_SLOT_ID.fetch_add(1, Ordering::Relaxed)
2192}
2193
2194/// Upper bound on the undrained retire list (see [`report_retired_slot`]).
2195///
2196/// Only reachable when nothing drains — a desktop app (no native compositor,
2197/// so no drain) that churns platform-view slots, or a mobile shell whose frame
2198/// loop has stopped. Past the cap the OLDEST id is dropped: the shell-side
2199/// differ's missing-streak backstop still disposes that slot, so a dropped id
2200/// costs a slower teardown, never a leak. Sized far above any realistic
2201/// per-frame teardown burst.
2202const MAX_PENDING_RETIRED_SLOTS: usize = 256;
2203
2204/// The process-wide pending-retire list backing [`report_retired_slot`] /
2205/// [`take_retired_slots`].
2206///
2207/// A `Mutex<Vec<_>>` rather than a `RenderRoot` field for the same reason
2208/// [`NEXT_SLOT_ID`] is a process-wide counter: a widget being torn down has no
2209/// `RenderRoot` handle to reach — and unlike `build`/`rebuild`, the id-space is
2210/// already process-global, so a global drain is coherent. Same single-root
2211/// caveat as `next_slot_id`, revisited together with it if multi-root ever
2212/// lands. Panics are impossible while the lock is held (a `Vec` push/take), but
2213/// the poison-tolerant `unwrap_or_else(into_inner)` idiom is used anyway,
2214/// matching `frust-shell-common`'s process-global slots.
2215static RETIRED_SLOTS: Mutex<Vec<u64>> = Mutex::new(Vec::new());
2216
2217/// Record that the widget owning `slot_id` was torn down (its `View::teardown`
2218/// ran), so the shell can dispose the native view promptly instead of waiting
2219/// out the differ's missing-streak heuristic.
2220///
2221/// The teardown half of the platform-view frame channel: `paint` says "this
2222/// slot exists here", this says "this slot is gone for good". Kept a flat
2223/// process-wide list (not a per-pass channel) because teardown does NOT run in
2224/// the paint pass — it runs mid-rebuild, arbitrarily deep inside a
2225/// `Component`'s own nested build context, so there is no threaded per-frame
2226/// sink every teardown can reach.
2227///
2228/// Drained by [`crate::app::RenderRoot::take_retired_platform_views`]; a shell
2229/// with no native compositor simply never drains, which is why the list is
2230/// capped (see [`MAX_PENDING_RETIRED_SLOTS`]).
2231pub fn report_retired_slot(slot_id: u64) {
2232    let mut pending = RETIRED_SLOTS.lock().unwrap_or_else(|e| e.into_inner());
2233    if pending.len() >= MAX_PENDING_RETIRED_SLOTS {
2234        // Drop-oldest: the differ's missing-streak backstop still catches the
2235        // dropped id (see the constant's doc).
2236        pending.remove(0);
2237    }
2238    pending.push(slot_id);
2239}
2240
2241/// Take (and clear) every slot id reported to [`report_retired_slot`] since the
2242/// last call, in teardown order. Drained once per frame by a shell through
2243/// [`crate::app::RenderRoot::take_retired_platform_views`].
2244pub fn take_retired_slots() -> Vec<u64> {
2245    let mut pending = RETIRED_SLOTS.lock().unwrap_or_else(|e| e.into_inner());
2246    std::mem::take(&mut *pending)
2247}
2248
2249/// A platform-view child's paint-time frame — everything a shell's native
2250/// compositor needs to place, size, clip, and dispose a native
2251/// sibling view for one paint pass.
2252///
2253/// All rects are logical px, **absolute window coordinates** — the same
2254/// space [`PaintCtx::report_hero`] callers use, built from the painting
2255/// pod's [`PaintCtx::origin`]/[`PaintCtx::size`]. Frames are
2256/// paint-pass-scoped: [`crate::app::RenderRoot::paint`] replaces the whole
2257/// collection every pass, so a slot that didn't paint this pass (a culled
2258/// subtree) simply has no frame in
2259/// [`crate::app::RenderRoot::platform_view_frames`] — the shell's differ owns
2260/// absent-means-hide/dispose semantics, not this crate.
2261#[derive(Clone, Debug, PartialEq)]
2262pub struct PlatformViewFrame {
2263    /// Stable per-widget-instance id, allocated once via [`next_slot_id`] at
2264    /// construction — never derived from tree position, so a reorder keeps
2265    /// identity.
2266    pub slot_id: u64,
2267    /// `"dev.frust.<Factory>"` convention naming which native view factory
2268    /// creates this slot's platform view.
2269    pub view_type: String,
2270    /// Opaque creation params for the native factory (may be empty).
2271    pub params_json: String,
2272    /// Bumped by the widget whenever `params_json` changes; this
2273    /// crate only ever carries the number through.
2274    pub params_generation: u64,
2275    /// Absolute paint bounds.
2276    pub rect: Rect,
2277    /// `visible_rect` (see [`PaintCtx::visible_rect`]) intersected with
2278    /// `rect`, or `None` when fully visible — no scroll ancestor is clipping
2279    /// it.
2280    pub clip: Option<Rect>,
2281    /// `false` ⇒ hidden (offscreen/culled by the widget itself, distinct from
2282    /// simply being absent from the collection this pass).
2283    pub visible: bool,
2284    /// Mode B input forwarding: whether
2285    /// the hosted native view should receive pointer input — a touch-DOWN
2286    /// inside `rect` (and outside every `shields` rect) hands the whole
2287    /// gesture to the native sibling in the embedding. `false` (the default)
2288    /// keeps the v1 no-input contract: the frust surface consumes everything.
2289    pub interactive: bool,
2290    /// The z-shield list: absolute-coordinate regions where frust content
2291    /// drawn OVER this slot must keep winning input. Only consulted when
2292    /// `interactive`. Same coordinate space as `rect`.
2293    ///
2294    /// Carries only the slot's own **manually declared** shields
2295    /// (`PlatformViewView::shield_local`, the escape hatch). The ordinary
2296    /// source is auto-collection: a `shield(child)` wrapper reports its painted
2297    /// rect through [`PaintCtx::report_input_shield`], and the shell-side differ
2298    /// merges whichever of those intersect this `rect` into the command it
2299    /// emits — so the shipped wire shape is the union of both, assembled one
2300    /// layer up.
2301    pub shields: Vec<Rect>,
2302    /// The slot's position in painter order: how many commands the scene had
2303    /// recorded when the slot published ([`PaintScene::command_index`]),
2304    /// taken **before** the slot's own Mode B punch
2305    /// ([`PaintScene::clear_rect`]) so the punch never counts as frust content
2306    /// painted above the slot. Every command at or past this index paints over
2307    /// the native view; the shell-side differ cuts the frame's command list at
2308    /// these indices to decide which frust content needs an overlay above
2309    /// which slot. `0` under a recorder scene with no command list.
2310    pub command_index: usize,
2311}
2312
2313/// The result of a whole [`crate::app::RenderRoot::paint`] pass.
2314///
2315/// `needs_frame` is whether any widget advanced animation state during paint and
2316/// asked (via [`PaintCtx::request_frame`]) to be re-invoked to continue. The
2317/// shell turns it into another scheduled frame — the desktop shell via
2318/// `window.request_redraw()`, the mobile shells implicitly through their
2319/// continuous loop. Mirrors [`crate::event::EventOutcome`]'s `needs_redraw`.
2320///
2321/// `needs_layout` is whether any widget asked (via [`PaintCtx::request_layout`])
2322/// to have layout re-run next frame because its animation changed its layout, not
2323/// just its paint. [`crate::app::RenderRoot::paint`] folds it into the render
2324/// root's pending [`crate::view::ChangeFlags`] (`LAYOUT`) so the next frame's
2325/// `take_change_flags().needs_layout()` reports it — driving the mobile
2326/// intra-frame layout skip to relayout while the animation is in flight.
2327///
2328/// `needs_frame_paced_only` is the aggregated [`TickClass`] verdict: `true` only
2329/// when a frame was requested and *every* request this frame was
2330/// [`TickClass::CosmeticLoop`] (a pacable decorative loop), `false` the instant
2331/// any [`TickClass::Transition`] request (including any `request_layout`) joined
2332/// in. The mobile frame gate may throttle such a purely-cosmetic frame
2333/// to a lower cadence; a `false` here means the frame runs every vsync as today.
2334/// Only meaningful when `needs_frame` is `true`.
2335///
2336/// `paced_interval` is *how fast* that paced frame asked to be re-run: the MIN
2337/// over every paced request this pass (see [`PaintCtx::request_frame_paced_at`]).
2338/// Only meaningful alongside `needs_frame_paced_only`.
2339#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
2340pub struct PaintOutcome {
2341    /// Whether the shell should schedule another frame to continue an animation.
2342    pub needs_frame: bool,
2343    /// Whether the render root folded a layout-continuation request into its
2344    /// pending change flags (a widget called [`PaintCtx::request_layout`]). The
2345    /// root also folds `LAYOUT` for a pod an unpaced frame request or a
2346    /// trailing landing paint dirtied; that fold is not reported here.
2347    pub needs_layout: bool,
2348    /// Whether a frame was requested and every request this frame was
2349    /// [`TickClass::CosmeticLoop`] — the paced-only state the mobile frame gate
2350    /// may throttle (see [`PaintCtx::needs_frame_paced_only`]). Meaningful only
2351    /// when `needs_frame` is set.
2352    pub needs_frame_paced_only: bool,
2353    /// The tightest (MIN) interval any paced request this frame named — see
2354    /// [`PaintCtx::paced_interval`]. `None` (no paced request) and
2355    /// `Some(Duration::ZERO)` (a bare [`PaintCtx::request_frame_paced`]) both
2356    /// mean "the theme's own `cosmetic_loop_rate`"; a longer value asks the gate
2357    /// to pace this loop slower than that cap. Meaningful only alongside
2358    /// `needs_frame_paced_only`; a shell latches it beside that flag and hands it
2359    /// to the frame gate, which resolves it against the live theme's cap.
2360    pub paced_interval: Option<Duration>,
2361    /// The serial the render root minted for this paint
2362    /// ([`PaintCtx::paint_serial`]): `1` for a root's first paint, one more for
2363    /// each paint after it. A shell carries it with the painted scene and hands
2364    /// it to the renderer before encoding that scene, so the resident-key
2365    /// ledger the renderer publishes names the last scene it committed (see
2366    /// [`crate::retained`]). `0` only from a `Default` outcome.
2367    pub paint_serial: u64,
2368}
2369
2370/// A retained UI element living in the widget tree.
2371///
2372/// `Widget: Any` so the render root can downcast a boxed widget back to the
2373/// concrete element type its originating view produced (needed during rebuild).
2374/// Implementing types get [`Widget::downcast_mut`] for free — no boilerplate
2375/// method to write.
2376pub trait Widget: Any {
2377    /// Choose a size within `bc` and return it. The chosen size must satisfy
2378    /// `bc` (callers may additionally clamp via [`BoxConstraints::constrain`]).
2379    ///
2380    /// # Layout may be skipped
2381    ///
2382    /// Under a [`crate::app::RenderRoot`] a widget sitting in a [`ChildPod`] is
2383    /// **not** laid out every frame: [`ChildPod::layout_child`] returns the size
2384    /// it recorded last time, without calling this method, when the pod is clean,
2385    /// its constraints are equal to last time's, the window insets it sees are
2386    /// unchanged, and the root's layout epoch has not moved (see that method for
2387    /// the full rule). So the result of `layout` must be a function of `bc`, the
2388    /// context's global inputs, and widget state that only changes through one of
2389    /// the channels that dirty the pod:
2390    ///
2391    /// * a rebuild returning [`ChangeFlags::LAYOUT`](crate::view::ChangeFlags::LAYOUT);
2392    /// * [`PaintCtx::request_layout`] (or [`EventCtx::request_layout`]);
2393    /// * [`PaintCtx::request_frame`] (any frame request — an animating subtree is
2394    ///   laid out again on the next pass, conservatively; a paced
2395    ///   [`TickClass::CosmeticLoop`] request marks the pod but forces no pass of
2396    ///   its own, so a loop that changes layout must not be paced);
2397    /// * an event the widget consumed ([`EventResult::Handled`]) or that asked for
2398    ///   a redraw ([`EventCtx::request_redraw`]).
2399    ///
2400    /// **A widget must not change layout-affecting state any other way** — a
2401    /// change made behind the pod's back is invisible to the skip and lays out
2402    /// stale until something else dirties the path.
2403    fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size;
2404
2405    /// Emit draw commands for this widget into `scene`.
2406    ///
2407    /// Paint **may** advance a widget's own animation state (e.g. a scroll
2408    /// fling integrated from a monotonic clock) as a v1 seam. A widget that does
2409    /// so must call [`PaintCtx::request_frame`] while the animation is still
2410    /// running so the shell re-invokes paint absent any external event —
2411    /// otherwise the desktop `ControlFlow::Wait` loop idles and the animation
2412    /// stalls. It must stop signalling once the animation reaches rest.
2413    fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene);
2414
2415    /// Handle an input event, optionally mutating application state through
2416    /// `ctx` and reporting whether it was consumed.
2417    ///
2418    /// Defaulted to [`EventResult::Ignored`] so non-interactive widgets are
2419    /// unaffected. Interactive widgets
2420    /// (Button/Checkbox/Slider) override this; containers forward to
2421    /// their [`ChildPod`] children via [`ChildPod::event_child`].
2422    fn event(&mut self, _ctx: &mut EventCtx, _event: &InputEvent) -> EventResult {
2423        EventResult::Ignored
2424    }
2425
2426    /// Contribute this widget's accessibility node(s) into `ctx`.
2427    ///
2428    /// Defaulted to a no-op so non-semantic widgets (and every widget written
2429    /// before this seam) are unaffected — exactly like [`Widget::event`]. A leaf
2430    /// widget overrides it to call [`SemanticsCtx::push_node`] with its role and
2431    /// state; a container overrides it to forward to each child via
2432    /// [`ChildPod::semantics_child`] (a transparent container contributes no node
2433    /// of its own, only recursion). The pass runs *after* layout, so
2434    /// [`SemanticsCtx::origin`]/[`SemanticsCtx::size`] carry valid absolute
2435    /// geometry.
2436    fn semantics(&self, _ctx: &mut SemanticsCtx) {}
2437
2438    /// This widget's concrete type name, for read-only tooling.
2439    ///
2440    /// Defaulted to [`core::any::type_name`] of the implementing type, so every
2441    /// widget reports its own name through the vtable — including one reached
2442    /// as a `dyn Widget`, where the concrete type is otherwise unrecoverable.
2443    /// Asking the live widget beats recording a name when it was built: a
2444    /// rebuild that swaps a child's concrete type cannot leave a stale name
2445    /// behind, not even through wrapper views that re-box an inner `AnyView`
2446    /// (`docs/LIMITATIONS.md`'s `focus-wrapper-erasure-swap-blind`).
2447    ///
2448    /// Diagnostic only: `type_name`'s output is not a stable contract across
2449    /// compiler versions, so never parse or match on it. Overriding it is
2450    /// sanctioned only for a transparent wrapper reporting what it wraps (the
2451    /// `Box<dyn Widget>` blanket impl is the one in-crate case).
2452    fn type_name(&self) -> &'static str {
2453        core::any::type_name::<Self>()
2454    }
2455
2456    /// Visit this widget's owned [`ChildPod`]s, in declaration (paint) order —
2457    /// the read-only seam that makes the retained hierarchy enumerable.
2458    ///
2459    /// Containers own their children as `ChildPod` fields rather than as arena
2460    /// nodes (see [`ChildPod`]), so nothing outside a container could walk into
2461    /// its subtree; this is that walk, and
2462    /// [`WidgetTree::inspect`](crate::tree::WidgetTree::inspect) is its one
2463    /// in-crate consumer.
2464    ///
2465    /// **Defaulted to visiting nothing**, exactly like [`Widget::event`] and
2466    /// [`Widget::semantics`]: a leaf widget needs no impl and pays nothing, and
2467    /// a container that never overrides it simply reads as a leaf to tooling. It
2468    /// runs behind `&self` and must not mutate anything a pass depends on — no
2469    /// build/layout/paint/event behavior may be routed through it.
2470    ///
2471    /// **No cycles by construction.** A `ChildPod` owns its widget (`Box<dyn
2472    /// Widget>`); ownership is a tree, so a descent through `visit_children`
2473    /// terminates without any runtime cycle check. A widget that handed the
2474    /// visitor a pod it does not own would break that, which is why the visitor
2475    /// takes `&ChildPod` — there is no way to publish a shared one.
2476    fn visit_children(&self, _visitor: &mut dyn FnMut(&ChildPod)) {}
2477
2478    /// This widget as a sliver, if it speaks the sliver protocol — the opt-in
2479    /// seam a scrolling viewport lays its children out through (see
2480    /// [`crate::sliver`] and [`ChildPod::layout_sliver_child`]).
2481    ///
2482    /// **Defaulted to `None`**, exactly like [`Widget::visit_children`]: a box
2483    /// widget needs no impl and is laid out through [`Widget::layout`] as
2484    /// always. A type implementing [`SliverWidget`] overrides it to return
2485    /// `Some(self)`. A transparent wrapper (a type-erasing box, a component
2486    /// boundary, an either-arm) forwards it to what it wraps, so a sliver stays
2487    /// a sliver however it was erased.
2488    fn as_sliver(&mut self) -> Option<&mut dyn SliverWidget> {
2489        None
2490    }
2491}
2492
2493impl dyn Widget {
2494    /// Attempt to downcast this trait object to a concrete widget type.
2495    ///
2496    /// Uses trait upcasting (`Widget: Any`, stable since Rust 1.86; the
2497    /// workspace MSRV is 1.88) — no `unsafe`.
2498    pub fn downcast_mut<W: Widget>(&mut self) -> Option<&mut W> {
2499        let any: &mut dyn Any = self;
2500        any.downcast_mut::<W>()
2501    }
2502}
2503
2504/// A boxed widget is itself a [`Widget`], delegating every pass to its contents.
2505///
2506/// This blanket impl is what makes type-erased children work: a
2507/// [`crate::view::AnyView`]'s element is a `Box<dyn Widget>`, and
2508/// `View::Element` must implement `Widget` — so the box has to be a widget too.
2509/// It also lets a `Box<dyn Widget>` be stored inside a [`ChildPod`] like any
2510/// concrete widget. `Box<dyn Widget>` is `'static` (hence `Any`), so it satisfies
2511/// the `Widget: Any` bound and can be recovered by
2512/// [`downcast_mut`](dyn Widget::downcast_mut) during rebuild.
2513impl Widget for Box<dyn Widget> {
2514    fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2515        (**self).layout(ctx, bc)
2516    }
2517
2518    fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
2519        (**self).paint(ctx, scene);
2520    }
2521
2522    fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
2523        (**self).event(ctx, event)
2524    }
2525
2526    fn semantics(&self, ctx: &mut SemanticsCtx) {
2527        (**self).semantics(ctx);
2528    }
2529
2530    fn visit_children(&self, visitor: &mut dyn FnMut(&ChildPod)) {
2531        (**self).visit_children(visitor);
2532    }
2533
2534    /// The *boxed* widget's sliver face, so type erasure never hides a sliver
2535    /// from the viewport laying it out.
2536    fn as_sliver(&mut self) -> Option<&mut dyn SliverWidget> {
2537        (**self).as_sliver()
2538    }
2539
2540    /// Report the *boxed* widget's name, not `Box<dyn Widget>` — the box is a
2541    /// storage detail of type erasure, never a tree element in its own right,
2542    /// and it nests (a doubly-boxed pod resolves through both layers).
2543    fn type_name(&self) -> &'static str {
2544        (**self).type_name()
2545    }
2546}
2547
2548/// A container's owned child: a boxed widget plus the layout geometry and
2549/// capture bookkeeping the container maintains for it.
2550///
2551/// Containers own their children directly as `ChildPod`s (a `Vec` for
2552/// Flex/Stack, named fields for Padding/Align) rather than as arena nodes — the
2553/// [`WidgetTree`](crate::tree::WidgetTree) arena stays single-root. This is a
2554/// deliberate divergence from the masonry "everything in the arena" model:
2555/// it needs zero global-id plumbing and no
2556/// disjoint-borrow gymnastics for the container features this crate ships. Arena-backed children
2557/// (for damage tracking / a11y global access) are deferred to a later phase.
2558///
2559/// `origin`/`size` are in the **container's** local coordinate space;
2560/// [`ChildPod::event_child`] translates events into the child's local space and
2561/// [`ChildPod::paint_child`] offsets the child's paint origin accordingly.
2562///
2563/// Because the arena cannot see into a container, the pod also carries what
2564/// read-only tooling needs to describe the element it wraps —
2565/// [`type_name`](ChildPod::type_name), [`debug_label`](ChildPod::debug_label),
2566/// [`inspect_id`](ChildPod::inspect_id), plus the geometry above — and
2567/// [`Widget::visit_children`] is how a walk reaches it.
2568pub struct ChildPod {
2569    widget: Box<dyn Widget>,
2570    origin: Point,
2571    size: Size,
2572    /// An optional transform placing the child under an arbitrary
2573    /// [`Affine`] relative to its `origin` — `None` (the default) for every pod
2574    /// that never calls [`ChildPod::set_transform`], which then takes exactly the
2575    /// untransformed paint/event/hit-test/semantics path. See
2576    /// [`ChildPod::set_transform`] for the mapping contract.
2577    transform: Option<Affine>,
2578    /// Set when the child captured the pointer, so the container can route
2579    /// subsequent moves/releases straight to it (capture-by-recorded-path).
2580    /// Cleared by the container on `Up`/`Cancel` via [`ChildPod::set_active`].
2581    active: bool,
2582    /// Whether the child widget itself called
2583    /// [`EventCtx::capture_contacts`] on the capture that set `active` — the
2584    /// pod a non-claimant contact's forward-only walk ends at (see
2585    /// [`ChildPod::event_child`]). Meaningful only while `active`; reset when a
2586    /// fresh capture is recorded and whenever the link is cleared.
2587    contacts_captor: bool,
2588    /// Whether the child or a widget below it called
2589    /// [`EventCtx::capture_contacts`] on the capture that set `active` — what
2590    /// [`EventCtx::release_captured_child`] asks to decide whether releasing
2591    /// this pod ends the gesture's contact opt-in. Same lifetime as
2592    /// `contacts_captor`.
2593    contacts_path: bool,
2594    /// Set when the child holds the focus path, so the container can route
2595    /// keyboard/IME events straight to it with no hit test (focus is the
2596    /// second recorded path, a mirror of `active`). Maintained by
2597    /// [`ChildPod::event_child`] on a `focus_requested`/`focus_released` bubble.
2598    ///
2599    /// **A set flag on its own says nothing about the live session.** It records
2600    /// that a claim once passed through this pod, and the only routes that clear
2601    /// it are a container's own blur sweep and a reconciler severing the link —
2602    /// neither of which can reach a pod held off-tree (a floated overlay
2603    /// surface). `focus_epoch` below is what makes the record falsifiable; read
2604    /// [`ChildPod::holds_live_focus`], not this flag, to decide anything.
2605    focused: bool,
2606    /// The focus epoch this pod's recorded link belongs to — stamped by
2607    /// [`ChildPod::set_focused`]`(true)` from the session standing on this
2608    /// thread ([`set_live_focus_session`]) and never cleared.
2609    ///
2610    /// The focus analog of `hover_epoch` below, and for the same reason: a
2611    /// session that moves has no way to visit the branch it left. The root
2612    /// advances its epoch around every dispatch that could record a new chain,
2613    /// so a claim recorded in that dispatch carries the new value and every link
2614    /// recorded by an older claim is stranded by arithmetic — including one
2615    /// belonging to a pod no container owns. `0` until a claim first passes
2616    /// through.
2617    focus_epoch: u64,
2618    /// The identity of the [`RenderRoot`](crate::app::RenderRoot) whose session
2619    /// `focus_epoch` names, recorded with it and never cleared either. `0` until
2620    /// a claim first passes through.
2621    ///
2622    /// Exactly `hover_root`'s job below: epoch counters are per-root and collide
2623    /// by construction, so the identity is what keeps one root's pod from
2624    /// reading as a holder of another root's identically-numbered session — and
2625    /// what keeps a dying pod from ending a session it was never part of.
2626    focus_root: u64,
2627    /// The hover epoch this pod's recorded hover link belongs to — stamped by
2628    /// [`ChildPod::event_child`] when a [`EventCtx::claim_hover`] bubbles through
2629    /// it, and never explicitly cleared. `0` until a claim first passes through —
2630    /// a [`RenderRoot`](crate::app::RenderRoot)'s live epoch starts past `0`, so a
2631    /// never-claimed pod reads unhovered.
2632    ///
2633    /// The hover analog of `active`/`focused`, but a stamp rather than a flag,
2634    /// because hover has no leave event to clear it with: the pointer moving
2635    /// elsewhere advances the live epoch, which strands every stale stamp at once
2636    /// without the container that owns it having to hear about the move. See the
2637    /// [`crate::event`] module docs.
2638    ///
2639    /// The one move the epoch cannot strand is the pod's own removal, which is
2640    /// why this field is also what `Drop` reports on (see the `Drop` impl and
2641    /// `mark_hover_orphaned`).
2642    hover_epoch: u64,
2643    /// The identity of the [`RenderRoot`](crate::app::RenderRoot) whose pass
2644    /// stamped `hover_epoch`, recorded with it and never cleared either. `0` until
2645    /// a claim first passes through.
2646    ///
2647    /// Epoch counters are per-root and all start at `1`, so two roots on one
2648    /// thread produce colliding integers by construction. Only `Drop` reads this:
2649    /// it is what tells this pod's own root's live link from another root's
2650    /// identically-numbered epoch, so a dying pod can never end a hover it was
2651    /// never part of. The event/paint comparisons need no such qualifier — a pod
2652    /// is only ever visited by the root that owns its tree.
2653    hover_root: u64,
2654    /// This pod's persistent semantics base id, lazily assigned on
2655    /// the pod's first [`ChildPod::semantics_child`] visit from the
2656    /// [`RenderRoot`](crate::app::RenderRoot) allocator and reused for the whole
2657    /// pod lifetime — so the node id a widget contributes is stable across frames
2658    /// (and survives a keyed reorder, which relocates the whole pod). Interior
2659    /// mutability because `semantics_child` runs behind `&self` (mirroring
2660    /// `Widget::semantics`); `NonZeroU64` niche-packs the `Option` and encodes
2661    /// "id 0 is not a valid base". `None` until the first semantics pass reaches
2662    /// this pod.
2663    semantics_id: Cell<Option<NonZeroU64>>,
2664    /// An optional human name for tooling, `None` unless something calls
2665    /// [`ChildPod::set_debug_label`]. Mirrors
2666    /// [`WidgetPod::debug_label`](crate::tree::WidgetPod::debug_label).
2667    debug_label: Option<Cow<'static, str>>,
2668    /// This pod's persistent tooling id, lazily assigned on its first
2669    /// [`ChildPod::inspect_id`] call and reused for the whole pod lifetime —
2670    /// the same shape (and the same reason) as `semantics_id` above: an id a
2671    /// devtools client selected must survive the next frame's rebuild, and must
2672    /// survive a keyed reorder, which relocates the whole pod. `None` until a
2673    /// walk first reaches this pod, so a process that never inspects allocates
2674    /// nothing.
2675    inspect_id: Cell<Option<NonZeroU64>>,
2676    /// Whether something this pod's subtree depends on may have changed since
2677    /// its last layout — the pod must call [`Widget::layout`] on the next
2678    /// [`ChildPod::layout_child`] whatever its constraints. `true` for a fresh
2679    /// pod (it has never been laid out); set by every dirty source (a rebuild
2680    /// returning `LAYOUT`, a paint-time layout or frame request, a consumed or
2681    /// redraw-requesting event, [`ChildPod::widget_mut`],
2682    /// [`ChildPod::mark_layout_dirty`]); cleared only
2683    /// by a layout that actually ran.
2684    layout_dirty: bool,
2685    /// Whether anything under this pod may have changed since its subtree was
2686    /// last painted ([`PaintCtx::subtree_clean`]). `true` for a fresh pod. Set
2687    /// by every layout-dirty source (those marks imply it), by
2688    /// [`ChildPod::mark_paint_dirty`] (a PAINT-only rebuild), and by this pod's
2689    /// own layout actually running; cleared only by a pass whose
2690    /// [`ChildPod::paint_child`] painted the child.
2691    paint_dirty: bool,
2692    /// The paint epoch the last paint that cleared `paint_dirty` ran under.
2693    painted_epoch: u64,
2694    /// The constraints the last layout that ran was handed — `None` until one
2695    /// has. Half of the skip key: a parent handing different constraints lays
2696    /// the child out again.
2697    last_constraints: Option<BoxConstraints>,
2698    /// The root layout epoch ([`LayoutCtx::layout_epoch`]) the last layout ran
2699    /// under. A root bumps it for a global layout input (theme, insets, window
2700    /// size), which invalidates every pod at once.
2701    last_epoch: u64,
2702    /// The window insets ([`LayoutCtx::window_insets`]) the last layout ran
2703    /// under. Global insets moving already bump the epoch; this additionally
2704    /// covers an ancestor's *scoped* override
2705    /// ([`LayoutCtx::with_window_insets`]) changing for this subtree alone.
2706    last_insets: WindowInsets,
2707    /// Whether the child's last paint requested a frame or a layout — so the
2708    /// paint *after* an animation's last request still dirties the pod once
2709    /// more (see [`ChildPod::paint_child`]'s layout-skip note).
2710    painted_animating: bool,
2711    /// The sliver constraints the last sliver layout that ran was handed —
2712    /// the [`ChildPod::layout_sliver_child`] half of the skip key, beside
2713    /// `last_constraints`. At most one of the two is `Some`: a layout under
2714    /// either protocol clears the other, so a pod switching protocol never
2715    /// skips on a stale result.
2716    last_sliver_constraints: Option<SliverConstraints>,
2717    /// The geometry the last sliver layout reported ([`ChildPod::sliver_geometry`]);
2718    /// `None` until one has run and again after a box layout.
2719    sliver_geometry: Option<SliverGeometry>,
2720}
2721
2722thread_local! {
2723    /// The next tooling id [`ChildPod::inspect_id`] hands out.
2724    ///
2725    /// Thread-local and UI-thread-affine, mirroring `mark_focus_orphaned`'s
2726    /// shape: the widget tree is single-threaded, and an inspect walk runs
2727    /// behind `&self` with no allocator in scope to thread down.
2728    static NEXT_INSPECT_ID: Cell<u64> = const { Cell::new(ChildPod::INSPECT_ID_BASE) };
2729
2730    /// The pod that recorded this dispatch's hover claim — the claimant's own
2731    /// pod, the first (deepest) one to stamp the claim on its way back up —
2732    /// named by its [`ChildPod::inspect_id`]. Cleared by the render root before
2733    /// a dispatch and taken after it ([`take_hover_claimant`]), so the root can
2734    /// tell a hover that moved to another claimant from one that stayed: the
2735    /// epoch alone advances on every hover pass.
2736    static HOVER_CLAIMANT: Cell<Option<u64>> = const { Cell::new(None) };
2737
2738    /// The focus session standing on this thread right now, as `(root identity,
2739    /// live epoch, claim epoch)` — published by [`crate::app::RenderRoot`] and
2740    /// compared against by every [`ChildPod`] that records, reads or drops a
2741    /// focus link.
2742    ///
2743    /// The two epochs are the same value except during a dispatch, where the
2744    /// root opens a *candidate* session the claim recorded in that dispatch is
2745    /// stamped with (see [`focus_claim_stamp`]) while reads still answer against
2746    /// the session standing when the dispatch began. That is hover's
2747    /// `hover_epoch`/`hover_claim_epoch` split, in a channel rather than on the
2748    /// context — and the reason both are visible at once is that a claim
2749    /// recorded on the way back up must be observable to the container that is
2750    /// still unwinding: a blur sweep deciding which child *kept* focus, and a
2751    /// portal noticing that its surface just took the session, both ask after
2752    /// the claim they are reacting to.
2753    ///
2754    /// `(0, 0, 0)` until a root publishes, which is also what a dispatch driven
2755    /// without any root at all (a widget unit test) sees: a pod claiming focus
2756    /// there stamps `0` too, so its link reads live and routing behaves exactly
2757    /// as it did before this channel existed. A real root's identity starts at
2758    /// `1` and its epoch at `1`, so it can never publish that triple.
2759    static LIVE_FOCUS_SESSION: Cell<(u64, u64, u64)> = const { Cell::new((0, 0, 0)) };
2760}
2761
2762/// Publish the focus session standing on this thread: `live` is the session a
2763/// recorded link must name to count, `claim` the one a link recorded *now* takes
2764/// (equal to `live` outside a dispatch).
2765///
2766/// # Why a thread-local, and not the running context
2767///
2768/// The hover epoch rides [`EventCtx`]/[`PaintCtx`] because every reader of it is
2769/// inside a pass the root itself seeded. A focus link has two readers that are
2770/// not: a container deciding where a focus-routed event goes runs its own
2771/// dispatch (a component boundary and an overlay slot both substitute a context
2772/// of their own on the way down, so a value threaded from the root does not
2773/// survive the trip), and a pod's destructor runs with no context at all. The
2774/// triple is therefore published where both can reach it, exactly as the hover
2775/// link's own `(root, epoch)` pair already is for the destructor's sake
2776/// (`crate::event::set_live_hover_link`).
2777///
2778/// It mirrors **one** root: a second [`crate::app::RenderRoot`] driving passes on
2779/// the same thread republishes before each of its own passes, which is why the
2780/// identity rides along — a pod of the other root can then never match by an
2781/// epoch integer the two happen to share.
2782pub(crate) fn set_live_focus_session(root: u64, live: u64, claim: u64) {
2783    LIVE_FOCUS_SESSION.with(|slot| slot.set((root, live, claim)));
2784}
2785
2786/// The `(root identity, epoch)` a link recorded right now is stamped with — the
2787/// candidate session while a dispatch is open, the live one otherwise.
2788fn focus_claim_stamp() -> (u64, u64) {
2789    let (root, _live, claim) = LIVE_FOCUS_SESSION.with(|slot| slot.get());
2790    (root, claim)
2791}
2792
2793/// Whether `(root, epoch)` names the session standing on this thread — either
2794/// the live one, or the candidate a dispatch currently open is recording claims
2795/// against.
2796fn names_live_focus_session(root: u64, epoch: u64) -> bool {
2797    let (live_root, live, claim) = LIVE_FOCUS_SESSION.with(|slot| slot.get());
2798    root == live_root && (epoch == live || epoch == claim)
2799}
2800
2801impl Drop for ChildPod {
2802    /// Report a **live** hover link severed by the pod's own removal, so
2803    /// [`RenderRoot::rebuild`](crate::app::RenderRoot::rebuild) ends the hover
2804    /// before the frame ends.
2805    ///
2806    /// The epoch mechanism strands a stale stamp on every hover pass, but a pass
2807    /// is exactly what a removed widget no longer gets: a rebuild that drops the
2808    /// claimant leaves the root's mirror standing (`is_hover_active()` keeps
2809    /// reporting a link nothing holds) and leaves every surviving ancestor of the
2810    /// dead claimant reading hovered off its own still-matching stamp, until some
2811    /// later `Move` happens to re-derive — which never arrives on a pointer the
2812    /// user has stopped moving. This destructor is the hover analog of the
2813    /// focus-orphan mark, and lives here rather than in the reconcilers because
2814    /// the stamp has no setter for a container to cooperate through; see
2815    /// `crate::event::mark_hover_orphaned` for the full rationale and the
2816    /// "only when the link was live" invariant this comparison enforces.
2817    ///
2818    /// The comparison is against the published `(root, epoch)` pair, not the epoch
2819    /// alone: per-root counters collide, so an unqualified match would let a pod
2820    /// of one root end another root's live hover (see `hover_root`).
2821    ///
2822    /// Costs one predictable branch on a `u64` field per pod dropped; the
2823    /// thread-local read happens only for the pod chain that has actually held a
2824    /// claim at some point.
2825    fn drop(&mut self) {
2826        if crate::event::live_hover_link_is(self.hover_root, self.hover_epoch) {
2827            crate::event::mark_hover_orphaned(self.hover_root);
2828        }
2829        // The focus half of the same report. The reconcilers raise this mark for
2830        // every pod they sever themselves (`teardown_child` and friends), which
2831        // covers the whole main tree; a pod floated by the overlay portal is
2832        // reached by none of them — its owner holds it behind an `Rc` and can
2833        // drop it with no view to tear it down through — so the pod reports its
2834        // own severance here, with nothing for an owner to remember to call.
2835        //
2836        // Gated on the stamp exactly as the hover half is, and additionally on a
2837        // non-zero epoch: a pod that never held a link carries `(0, 0)`, which is
2838        // precisely what a dispatch driven without a root publishes, and a
2839        // rootless test dropping pods owes no release.
2840        if self.focused
2841            && self.focus_epoch != 0
2842            && names_live_focus_session(self.focus_root, self.focus_epoch)
2843        {
2844            crate::event::mark_focus_orphaned();
2845        }
2846    }
2847}
2848
2849/// Forget any hover claimant recorded on this thread — called by the render root
2850/// before a dispatch, so [`take_hover_claimant`] answers for that dispatch alone.
2851pub(crate) fn clear_hover_claimant() {
2852    HOVER_CLAIMANT.with(|claimant| claimant.set(None));
2853}
2854
2855/// Take the [`ChildPod::inspect_id`] of the pod that recorded the last
2856/// dispatch's hover claim (see `HOVER_CLAIMANT`), or `None` when no pod did —
2857/// no claim at all, or one the root widget made itself.
2858pub(crate) fn take_hover_claimant() -> Option<u64> {
2859    HOVER_CLAIMANT.with(Cell::take)
2860}
2861
2862impl ChildPod {
2863    /// Where [`ChildPod::inspect_id`]'s allocator starts.
2864    ///
2865    /// Pod ids and arena [`WidgetId`](crate::view::WidgetId)s share one
2866    /// namespace in an inspect snapshot, and the arena's are allocated from zero
2867    /// upward by `BuildCtx::alloc_id`. Starting the pod allocator at 2^48 keeps
2868    /// the two apart for any tree an app could plausibly build (the arena would
2869    /// have to allocate 281 trillion ids to reach it) without threading a shared
2870    /// counter through a read-only walk.
2871    pub const INSPECT_ID_BASE: u64 = 1 << 48;
2872
2873    /// Wrap a freshly built child widget at the origin, with zero size until its
2874    /// first layout.
2875    pub fn new(widget: Box<dyn Widget>) -> Self {
2876        Self {
2877            widget,
2878            origin: Point::ZERO,
2879            size: Size::ZERO,
2880            transform: None,
2881            active: false,
2882            contacts_captor: false,
2883            contacts_path: false,
2884            focused: false,
2885            focus_epoch: 0,
2886            focus_root: 0,
2887            hover_epoch: 0,
2888            hover_root: 0,
2889            semantics_id: Cell::new(None),
2890            debug_label: None,
2891            inspect_id: Cell::new(None),
2892            // A fresh pod has never been laid out.
2893            layout_dirty: true,
2894            paint_dirty: true,
2895            painted_epoch: 0,
2896            last_constraints: None,
2897            last_epoch: 0,
2898            last_insets: WindowInsets::default(),
2899            painted_animating: false,
2900            last_sliver_constraints: None,
2901            sliver_geometry: None,
2902        }
2903    }
2904
2905    /// The wrapped widget's concrete type name, asked of the live widget
2906    /// ([`Widget::type_name`]) rather than recorded at build time — so a
2907    /// rebuild that swapped the child's type can never leave a stale name here,
2908    /// and the double box `build_child` stores resolves through both layers.
2909    ///
2910    /// Diagnostic only: `type_name`'s output is not a stable contract across
2911    /// compiler versions, so never parse or match on it.
2912    pub fn type_name(&self) -> &'static str {
2913        self.widget.type_name()
2914    }
2915
2916    /// The human name attached for tooling, if any. `None` by default.
2917    pub fn debug_label(&self) -> Option<&str> {
2918        self.debug_label.as_deref()
2919    }
2920
2921    /// Attach a human name for tooling (an inspector shows it beside the type
2922    /// name). Purely descriptive — nothing in the build/layout/paint/event path
2923    /// reads it.
2924    pub fn set_debug_label(&mut self, label: impl Into<Cow<'static, str>>) {
2925        self.debug_label = Some(label.into());
2926    }
2927
2928    /// Drop any attached debug label.
2929    pub fn clear_debug_label(&mut self) {
2930        self.debug_label = None;
2931    }
2932
2933    /// The attached label as an owned [`Cow`] for a snapshot, cloning the
2934    /// borrowed case for free — what [`crate::tree::WidgetTree::inspect`] needs
2935    /// and [`ChildPod::debug_label`]'s `&str` cannot give.
2936    pub(crate) fn debug_label_cow(&self) -> Option<Cow<'static, str>> {
2937        self.debug_label.clone()
2938    }
2939
2940    /// This pod's tooling id, assigning one on the first call and reusing it
2941    /// thereafter — so the id a devtools client holds keeps naming the same pod
2942    /// across frames, and across a keyed reorder that relocates the pod.
2943    ///
2944    /// Behind `&self` (interior mutability) because the whole introspection
2945    /// seam is read-only; ids come from [`INSPECT_ID_BASE`](ChildPod::INSPECT_ID_BASE)
2946    /// upward and never collide with an arena `WidgetId`.
2947    pub fn inspect_id(&self) -> crate::view::WidgetId {
2948        let id = match self.inspect_id.get() {
2949            Some(id) => id,
2950            None => {
2951                let id = NEXT_INSPECT_ID.with(|next| {
2952                    let id = next.get();
2953                    next.set(id + 1);
2954                    NonZeroU64::new(id).expect("the allocator starts at 2^48, never zero")
2955                });
2956                self.inspect_id.set(Some(id));
2957                id
2958            }
2959        };
2960        crate::view::WidgetId(id.get())
2961    }
2962
2963    /// Shared access to the boxed child widget.
2964    pub fn widget(&self) -> &dyn Widget {
2965        &*self.widget
2966    }
2967
2968    /// Mutable access to the boxed child widget (e.g. to downcast during a
2969    /// container's own rebuild).
2970    ///
2971    /// **Marks the pod layout-dirty**, conservatively: a caller holding the
2972    /// widget mutably may change state its layout depends on, and nothing tells
2973    /// the layout skip whether it did (see [`ChildPod::layout_child`]). A
2974    /// hand-rolled reconciler that rebuilds a child's own sub-pods through this
2975    /// is thereby covered without knowing about the skip. A reconciler that
2976    /// marks the pod itself from the rebuild's
2977    /// [`ChangeFlags`](crate::view::ChangeFlags) uses
2978    /// [`ChildPod::widget_mut_for_rebuild`] instead, so a rebuild that changed
2979    /// nothing layout-visible leaves the pod clean.
2980    pub fn widget_mut(&mut self) -> &mut dyn Widget {
2981        self.layout_dirty = true;
2982        self.paint_dirty = true;
2983        &mut *self.widget
2984    }
2985
2986    /// Mutable access to the boxed child widget **without** marking the pod
2987    /// layout-dirty — for a reconciler that rebuilds the child through it and
2988    /// then marks the pod itself with [`ChildPod::mark_layout_dirty`] when the
2989    /// rebuild returns [`ChangeFlags::LAYOUT`](crate::view::ChangeFlags::LAYOUT)
2990    /// (`frust-widgets`' `rebuild_child`/`rebuild_children` and
2991    /// [`crate::component::ComponentView`] do). Taking this accessor is taking
2992    /// on that duty: a layout-affecting change made through it and not marked
2993    /// lays out stale.
2994    pub fn widget_mut_for_rebuild(&mut self) -> &mut dyn Widget {
2995        &mut *self.widget
2996    }
2997
2998    /// Replace the boxed child widget (used when a type-changing rebuild swaps
2999    /// the underlying widget). The fresh widget has never been laid out, so the
3000    /// pod is marked layout-dirty.
3001    pub fn set_widget(&mut self, widget: Box<dyn Widget>) {
3002        self.widget = widget;
3003        self.layout_dirty = true;
3004        self.paint_dirty = true;
3005    }
3006
3007    /// Whether the next [`ChildPod::layout_child`] must lay the child out even
3008    /// under unchanged constraints (see that method for the rule). Read-only;
3009    /// for tooling and tests ([`crate::tree::WidgetTree::inspect_layout_dirty`]).
3010    pub fn is_layout_dirty(&self) -> bool {
3011        self.layout_dirty
3012    }
3013
3014    /// Mark this pod layout-dirty: its next [`ChildPod::layout_child`] calls
3015    /// [`Widget::layout`] whatever its constraints.
3016    ///
3017    /// The reconcilers call this when a child's rebuild returns
3018    /// [`ChangeFlags::LAYOUT`](crate::view::ChangeFlags::LAYOUT). **It marks this
3019    /// pod only** — a dirty pod is reached only if every pod above it is laid
3020    /// out too, so a caller outside a rebuild that reports `LAYOUT` upward must
3021    /// also dirty its own path (by calling [`PaintCtx::request_layout`] or
3022    /// [`EventCtx::request_layout`]).
3023    pub fn mark_layout_dirty(&mut self) {
3024        self.layout_dirty = true;
3025        self.paint_dirty = true;
3026    }
3027
3028    /// Whether the pod's subtree must be painted afresh (see
3029    /// [`PaintCtx::subtree_clean`]). Read-only; for tooling and tests
3030    /// ([`crate::tree::WidgetTree::inspect_paint_dirty`]).
3031    pub fn is_paint_dirty(&self) -> bool {
3032        self.paint_dirty
3033    }
3034
3035    /// Mark this pod paint-dirty only: layout is untouched. A reconciler calls
3036    /// this when a child's rebuild returns
3037    /// [`ChangeFlags::PAINT`](crate::view::ChangeFlags::PAINT) (a LAYOUT rebuild
3038    /// already implies it through [`ChildPod::mark_layout_dirty`]). Like that
3039    /// method it marks this pod only; the pods above are marked by their own
3040    /// reconcilers from the same flags.
3041    pub fn mark_paint_dirty(&mut self) {
3042        self.paint_dirty = true;
3043    }
3044
3045    /// The child's origin in the container's coordinate space.
3046    pub fn origin(&self) -> Point {
3047        self.origin
3048    }
3049
3050    /// The child's resolved size (valid after [`ChildPod::layout_child`]).
3051    pub fn size(&self) -> Size {
3052        self.size
3053    }
3054
3055    /// Place the child at `origin` within the container's coordinate space.
3056    pub fn set_origin(&mut self, origin: Point) {
3057        self.origin = origin;
3058    }
3059
3060    /// The child's transform, if any (see [`ChildPod::set_transform`]).
3061    pub fn transform(&self) -> Option<Affine> {
3062        self.transform
3063    }
3064
3065    /// Place the child under an arbitrary `transform`, or clear it with `None`.
3066    ///
3067    /// The transform is applied in the child's own local space, **before** the
3068    /// `origin` offset: a child-local point `p` lands at
3069    /// `origin + transform * p` in the container's space. So
3070    /// `Affine::scale_about(2.0, center)` zooms the child about its own local
3071    /// `center`, and a pan/zoom container can leave `origin` at zero and drive
3072    /// the whole placement through the transform.
3073    ///
3074    /// While one is set:
3075    ///
3076    /// * [`ChildPod::paint_child`] wraps the child's paint in
3077    ///   [`PaintScene::push_transform`]/[`PaintScene::pop_transform`], and maps
3078    ///   an ancestor's visible rect back into the child's frame (the bounding box
3079    ///   of its inverse image), so a culling descendant still culls against what
3080    ///   is really on screen;
3081    /// * [`ChildPod::contains`] maps the point through the inverse before the
3082    ///   bounds test, so it hits exactly what paint drew — a rotated child hits
3083    ///   along its rotated edges, not its axis-aligned bounding box (the same
3084    ///   test as [`crate::hit::point_in_transformed_rect`]);
3085    /// * [`ChildPod::event_child`] maps pointer, scroll and scale positions
3086    ///   through the inverse ([`InputEvent::transformed`]), so the child sees
3087    ///   local coordinates exactly as an untransformed child does;
3088    /// * [`ChildPod::semantics_child`] reports the subtree at the
3089    ///   **axis-aligned bounding box** of the transformed child rect. This is an
3090    ///   approximation (v1): the pod's frame becomes that box, and descendants
3091    ///   are offset from its corner unscaled and unrotated.
3092    ///
3093    /// A transform with no inverse (a zero scale, a collapsed axis, a non-finite
3094    /// coefficient — see [`crate::hit::checked_inverse`]) draws nothing and hits
3095    /// nothing: `contains` is `false` and paint skips the subtree. An event that
3096    /// still reaches the child through a recorded capture or focus path is
3097    /// delivered translated by `-origin` only, so a capture can always release.
3098    ///
3099    /// Out of v1's reach: rects a descendant reports in window space from
3100    /// `paint` (platform-view frames, input shields, hero rects, overlay anchors)
3101    /// are not mapped through the transform.
3102    pub fn set_transform(&mut self, transform: Option<Affine>) {
3103        self.transform = transform;
3104    }
3105
3106    /// The child's local→container mapping for a set `transform`:
3107    /// `translate(origin) * transform`.
3108    fn local_to_container(&self, transform: Affine) -> Affine {
3109        Affine::translate(self.origin.to_vec2()) * transform
3110    }
3111
3112    /// Whether this child currently holds the recorded active (captured) path.
3113    pub fn is_active(&self) -> bool {
3114        self.active
3115    }
3116
3117    /// Set (or clear) the recorded active path — the container clears this on
3118    /// `Up`/`Cancel` when capture auto-releases.
3119    ///
3120    /// **The link is keyed on the gesture's claimant.** While the root is
3121    /// delivering a *non-claimant* contact down a live capture's path (rule (c)
3122    /// of [`InputEvent::PointerContact`](crate::event::InputEvent::PointerContact)'s
3123    /// multi-contact contract), a clear is refused: that contact's `Up`/`Cancel`
3124    /// reaches every container on the path, and a container clearing its link on
3125    /// it would strand the claimant's own follow-ups. Outside such a pass — every
3126    /// single-pointer dispatch, a rebuild, a container's own teardown — a clear
3127    /// takes effect as always.
3128    ///
3129    /// A container that clears the link to take the gesture over from its
3130    /// child (rather than because the gesture ended) should release it through
3131    /// [`EventCtx::release_captured_child`] instead, which also ends the
3132    /// gesture's contact opt-in when the released subtree held it.
3133    pub fn set_active(&mut self, active: bool) {
3134        if !active && crate::event::in_secondary_contact_pass() {
3135            return;
3136        }
3137        self.active = active;
3138        if !active {
3139            self.contacts_captor = false;
3140            self.contacts_path = false;
3141        }
3142    }
3143
3144    /// Whether this pod's recorded active path leads to the widget that opted
3145    /// into the gesture's other contacts ([`EventCtx::capture_contacts`]) —
3146    /// the child itself, or a widget below it.
3147    pub(crate) fn holds_contact_opt_in(&self) -> bool {
3148        self.active && self.contacts_path
3149    }
3150
3151    /// Whether this child has a recorded focus path at all — **not** whether
3152    /// that record belongs to the session the root currently holds.
3153    ///
3154    /// The raw flag, kept for the things that legitimately want it: a paint-time
3155    /// culling exemption, a blur sweep clearing whatever it finds, a container
3156    /// asking "did I ever record a link here". Deciding *where a focus-routed
3157    /// event goes*, or whether a widget may speak for the focus session, needs
3158    /// [`ChildPod::holds_live_focus`] instead — a link this flag reports can be
3159    /// one a moved session left behind, and there is no pass that visits an
3160    /// abandoned branch to clear it.
3161    pub fn is_focused(&self) -> bool {
3162        self.focused
3163    }
3164
3165    /// Record (or drop) this child's focus path **against the live session**.
3166    ///
3167    /// Setting it stamps the session standing on this thread
3168    /// (`set_live_focus_session`) beside the flag, so the link says which session
3169    /// it belongs to rather than merely that one existed; clearing it leaves the
3170    /// stamp alone, which costs nothing because the flag gates every read.
3171    /// Containers clear it on blur-on-outside-tap and set it when a child
3172    /// requests focus; the flag is maintained automatically by
3173    /// [`ChildPod::event_child`] on a `focus_requested`/`focus_released` bubble.
3174    pub fn set_focused(&mut self, focused: bool) {
3175        self.focused = focused;
3176        if focused {
3177            let (root, epoch) = focus_claim_stamp();
3178            self.focus_root = root;
3179            self.focus_epoch = epoch;
3180        }
3181    }
3182
3183    /// The focus epoch this pod's recorded link was stamped with — the focus
3184    /// counterpart of [`ChildPod::hover_epoch`], and just as much an identity
3185    /// rather than an ordering or a boolean.
3186    ///
3187    /// Diagnostic only, for the same reason the hover stamp's accessor is: a
3188    /// stamp is never cleared, only stranded by the next session, so a non-zero
3189    /// value means "a claim passed through here once", never "focused". Ask
3190    /// [`ChildPod::holds_live_focus`], which is the comparison this exists for.
3191    pub fn focus_epoch(&self) -> u64 {
3192        self.focus_epoch
3193    }
3194
3195    /// Whether this child holds a focus link **on the session the root currently
3196    /// has** — the read every routing and provenance decision wants.
3197    ///
3198    /// `true` only while the flag is set *and* the stamp names the live session
3199    /// (`set_live_focus_session`). Two branches can both carry a set flag — an
3200    /// overlay pod's claim reaches no container's blur sweep, so the branch it
3201    /// superseded keeps its own record — and this is what tells them apart
3202    /// without either branch having to be visited.
3203    ///
3204    /// A dispatch driven with no root at all reads `(0, 0)` on both sides, so a
3205    /// pod that claimed focus during such a dispatch answers `true`: with no
3206    /// session to be stale relative to, the flag is all there is.
3207    pub fn holds_live_focus(&self) -> bool {
3208        self.focused && names_live_focus_session(self.focus_root, self.focus_epoch)
3209    }
3210
3211    /// Drop a recorded focus link that the live session has already stranded,
3212    /// reporting whether one was dropped.
3213    ///
3214    /// The retirement seam a [`RenderRoot`](crate::app::RenderRoot) needs and
3215    /// cannot otherwise have. A pod floated by the overlay portal lives off the
3216    /// tree, behind its owner's `Rc`, and the root borrows it for exactly the
3217    /// length of the paint pass that floats it — so the one moment the root can
3218    /// act on such a link is that pass, and the one thing it can honestly say
3219    /// about it is what the stamp already decides. Calling this keeps the raw
3220    /// flag and the stamp telling the same story, which is what the consumers of
3221    /// [`ChildPod::is_focused`] that cannot consult an epoch depend on.
3222    ///
3223    /// Deliberately *not* a bare `set_focused(false)` at the call site: the
3224    /// condition is the whole contract, and a pod whose link is live must never
3225    /// be retired by a pass that merely walked past it.
3226    pub fn retire_stale_focus_link(&mut self) -> bool {
3227        if self.focused && !self.holds_live_focus() {
3228            self.focused = false;
3229            true
3230        } else {
3231            false
3232        }
3233    }
3234
3235    /// The hover epoch this pod's recorded hover link belongs to — the hover
3236    /// counterpart of [`ChildPod::is_focused`], reported as a stamp rather than a
3237    /// flag because "is it hovered?" is only answerable against the live epoch
3238    /// (`0` means no claim has ever passed through this pod).
3239    ///
3240    /// There is no setter: the stamp is maintained solely by
3241    /// [`ChildPod::event_child`] from a claim bubble, so a container cannot record
3242    /// or clear a hover link by hand — which is what keeps at most one path
3243    /// hovered. See the [`crate::event`] module docs.
3244    ///
3245    /// # A bare stamp answers nothing
3246    ///
3247    /// The returned `u64` is an identity, not an ordering and not a boolean. It
3248    /// means something only compared **for equality against the live epoch**, and
3249    /// that comparison is the pipeline's own: the live epoch rides the running
3250    /// context (`PaintCtx::hover_epoch`/`EventCtx::hover_epoch`), both
3251    /// crate-private, and the comparison is already ANDed with the ancestor chain
3252    /// by `paint_child`/`event_child` before any widget sees it. Treating a
3253    /// non-zero stamp as "hovered", or ordering two pods' stamps, reads reasonable
3254    /// and is wrong: a stamp is never cleared, only stranded by the next epoch
3255    /// advance, so a pod the pointer left an hour ago still carries a non-zero
3256    /// one, and the counter wraps.
3257    ///
3258    /// **Read [`PaintCtx::is_hovered`] instead** (authoritative), or
3259    /// [`EventCtx::is_hovered`] for the state as of the previous pass; between
3260    /// them they answer "is this widget or its subtree hovered" — which is the
3261    /// question a widget actually has. A container asking the narrower "*which* of
3262    /// my children" answers it with the same hit test its own claim rides, not
3263    /// from here. This accessor is diagnostic — tooling, tests, and the debug
3264    /// dump — the way [`ChildPod::type_name`] is.
3265    pub fn hover_epoch(&self) -> u64 {
3266        self.hover_epoch
3267    }
3268
3269    /// Lay the child out under `bc`, recording and returning its chosen size —
3270    /// or, when nothing it depends on can have changed, return the size it
3271    /// recorded last time **without calling [`Widget::layout`]**.
3272    ///
3273    /// # The skip rule
3274    ///
3275    /// The child is laid out again unless **all** of these hold:
3276    ///
3277    /// * the pass is not forced ([`LayoutCtx::is_forced`] — a context built
3278    ///   outside a root, or a root with
3279    ///   [`set_layout_skip(false)`](crate::app::RenderRoot::set_layout_skip));
3280    /// * the pod is clean ([`ChildPod::is_layout_dirty`]);
3281    /// * `bc` equals the constraints the last layout ran with;
3282    /// * the root's layout epoch ([`LayoutCtx::layout_epoch`]) equals the one the
3283    ///   last layout ran under — a root bumps it for a theme swap, an inset change
3284    ///   and a window resize;
3285    /// * the window insets the context carries equal last time's (an ancestor's
3286    ///   scoped [`LayoutCtx::with_window_insets`] override counts).
3287    ///
3288    /// A pod is dirtied — and, because each source travels the pod chain, so is
3289    /// every pod between it and the root — by:
3290    ///
3291    /// * a rebuild that returned [`ChangeFlags::LAYOUT`](crate::view::ChangeFlags::LAYOUT)
3292    ///   for it (the reconcilers call [`ChildPod::mark_layout_dirty`]; a
3293    ///   PAINT-only rebuild does not dirty layout), or a widget swap;
3294    /// * a paint in which the child (or anything below it) called
3295    ///   [`PaintCtx::request_layout`] **or any frame request** — conservatively,
3296    ///   an animating subtree is laid out again on the next pass — and the one
3297    ///   paint after the last such request, so the frame an animation lands on
3298    ///   (the first to request nothing) is laid out too;
3299    /// * an event the child consumed ([`EventResult::Handled`]), asked a redraw
3300    ///   for, or called [`EventCtx::request_layout`] on;
3301    /// * mutable access through [`ChildPod::widget_mut`] (conservatively — a
3302    ///   reconciler marking from flags uses [`ChildPod::widget_mut_for_rebuild`]);
3303    /// * being freshly built.
3304    ///
3305    /// A layout that runs records `bc`, the epoch and the insets, and clears the
3306    /// dirty flag. Widgets keep their side of the contract on
3307    /// [`Widget::layout`]: no layout-affecting state may change except through
3308    /// one of the sources above.
3309    pub fn layout_child(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3310        if !ctx.force
3311            && !self.layout_dirty
3312            && self.last_constraints == Some(*bc)
3313            && self.last_epoch == ctx.epoch
3314            && self.last_insets == ctx.window_insets
3315        {
3316            return self.size;
3317        }
3318        let size = self.widget.layout(ctx, bc);
3319        self.size = size;
3320        self.last_constraints = Some(*bc);
3321        self.last_sliver_constraints = None;
3322        self.sliver_geometry = None;
3323        self.last_epoch = ctx.epoch;
3324        self.last_insets = ctx.window_insets;
3325        self.layout_dirty = false;
3326        // Layout actually ran: geometry may differ, so the painted output is stale.
3327        self.paint_dirty = true;
3328        size
3329    }
3330
3331    /// Lay the child out as a sliver under `sc`, recording and returning its
3332    /// geometry — or `None`, touching nothing, when the child does not speak the
3333    /// sliver protocol ([`Widget::as_sliver`]); the caller then lays it out as a
3334    /// box, typically through an adapter.
3335    ///
3336    /// On a sliver the pod records the geometry ([`ChildPod::sliver_geometry`])
3337    /// and sizes itself to what was painted: `(cross_axis_extent, paint_extent)`
3338    /// in axis terms ([`ScrollAxis::size`](crate::sliver::ScrollAxis::size)), so
3339    /// [`ChildPod::contains`], [`ChildPod::paint_child`] and
3340    /// [`ChildPod::semantics_child`] all see the painted extent. The caller
3341    /// positions the pod with [`ChildPod::set_origin`] as for a box child.
3342    ///
3343    /// # The skip rule
3344    ///
3345    /// Exactly [`ChildPod::layout_child`]'s, with `sc` in place of the box
3346    /// constraints: the stored geometry is returned without calling
3347    /// [`SliverWidget::layout_sliver`] when the pass is not forced, the pod is
3348    /// clean, `sc` equals the sliver constraints the last layout ran with, and
3349    /// the layout epoch and window insets are unchanged. The dirty sources are
3350    /// the same ones. A box layout in between clears the recorded sliver
3351    /// constraints (and the reverse), so switching protocol always lays out.
3352    ///
3353    /// One addition: a geometry carrying a
3354    /// [`scroll_offset_correction`](SliverGeometry::scroll_offset_correction)
3355    /// leaves the pod dirty, because a correction asks the viewport to lay out
3356    /// again and must never be replayed from the cache.
3357    ///
3358    /// In debug builds `sc` and the reported geometry are checked with
3359    /// [`SliverConstraints::debug_assert_valid`] and
3360    /// [`SliverGeometry::debug_assert_valid`].
3361    pub fn layout_sliver_child(
3362        &mut self,
3363        ctx: &mut LayoutCtx,
3364        sc: &SliverConstraints,
3365    ) -> Option<SliverGeometry> {
3366        if !ctx.force
3367            && !self.layout_dirty
3368            && self.last_sliver_constraints == Some(*sc)
3369            && self.last_epoch == ctx.epoch
3370            && self.last_insets == ctx.window_insets
3371            && let Some(geometry) = self.sliver_geometry
3372        {
3373            return Some(geometry);
3374        }
3375        sc.debug_assert_valid();
3376        let geometry = self.widget.as_sliver()?.layout_sliver(ctx, sc);
3377        geometry.debug_assert_valid(sc);
3378        self.size = sc.axis.size(geometry.paint_extent, sc.cross_axis_extent);
3379        self.sliver_geometry = Some(geometry);
3380        self.last_sliver_constraints = Some(*sc);
3381        self.last_constraints = None;
3382        self.last_epoch = ctx.epoch;
3383        self.last_insets = ctx.window_insets;
3384        self.layout_dirty = geometry.scroll_offset_correction.is_some();
3385        self.paint_dirty = true;
3386        Some(geometry)
3387    }
3388
3389    /// The geometry the child reported from its last sliver layout
3390    /// ([`ChildPod::layout_sliver_child`]) — `None` before one has run, or once
3391    /// the pod has been laid out as a box since.
3392    pub fn sliver_geometry(&self) -> Option<SliverGeometry> {
3393        self.sliver_geometry
3394    }
3395
3396    /// Whether the child speaks the sliver protocol ([`Widget::as_sliver`]).
3397    ///
3398    /// Takes `&mut self` only because `as_sliver` does; unlike
3399    /// [`ChildPod::widget_mut`] it does not mark the pod layout-dirty. What a
3400    /// transparent wrapper holding its child in a pod asks to decide its own
3401    /// [`Widget::as_sliver`] answer.
3402    pub fn is_sliver(&mut self) -> bool {
3403        self.widget.as_sliver().is_some()
3404    }
3405
3406    /// Paint the child, offsetting its paint origin by the container's origin
3407    /// (`ctx.origin()`) so the child draws at its absolute position.
3408    ///
3409    /// Bubbles the child's animation-continuation request ([`PaintCtx::needs_frame`])
3410    /// back into the parent `ctx`, mirroring [`ChildPod::event_child`]'s absorb of
3411    /// the child's redraw/capture flags — so a nested flinging widget keeps the
3412    /// whole tree's frames coming.
3413    ///
3414    /// A pod with a [`transform`](ChildPod::set_transform) additionally wraps
3415    /// that paint in [`PaintScene::push_transform`]/[`PaintScene::pop_transform`]
3416    /// (and skips it entirely when the transform has no inverse); an
3417    /// untransformed pod pushes nothing.
3418    pub fn paint_child(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
3419        // Fast path first: an untransformed pod is the pre-transform code path,
3420        // threading the ancestor's visible rect down unchanged.
3421        let Some(transform) = self.transform else {
3422            let visible_rect = ctx.visible_rect_ref();
3423            self.paint_child_in(ctx, scene, visible_rect);
3424            return;
3425        };
3426        // A singular transform collapses the child to a line or a point: there is
3427        // nothing to draw, and no inverse to map the visible rect through.
3428        if crate::hit::checked_inverse(&transform).is_none() {
3429            return;
3430        }
3431        // The scene speaks absolute coordinates, and the child paints at its
3432        // absolute origin, so conjugate the child-local transform by that
3433        // origin: a local point `p` drawn at `abs + p` lands at
3434        // `abs + transform * p`.
3435        let abs = (ctx.origin() + self.origin.to_vec2()).to_vec2();
3436        let around = Affine::translate(abs) * transform * Affine::translate(-abs);
3437        // Culling descendants compare their own (untransformed) absolute rects
3438        // against the visible rect, so hand them its preimage: the bounding box
3439        // of the visible rect mapped back through the inverse — conservative,
3440        // never culling anything actually on screen.
3441        let visible_rect = match (ctx.visible_rect_ref(), crate::hit::checked_inverse(&around)) {
3442            (Some(rect), Some(inverse)) => Some(inverse.transform_rect_bbox(rect)),
3443            _ => None,
3444        };
3445        scene.push_transform(around);
3446        self.paint_child_in(ctx, scene, visible_rect);
3447        scene.pop_transform();
3448    }
3449
3450    /// [`ChildPod::paint_child`]'s body: paint the child at its absolute origin
3451    /// with `visible_rect` as the child's culling rect.
3452    fn paint_child_in(
3453        &mut self,
3454        ctx: &mut PaintCtx,
3455        scene: &mut dyn PaintScene,
3456        visible_rect: Option<Rect>,
3457    ) {
3458        let child_origin = ctx.origin() + self.origin.to_vec2();
3459        let mut child_ctx = PaintCtx::new(child_origin, self.size);
3460        // Thread the shared shell clock down unchanged so every widget in the
3461        // frame advances animations against one consistent timestamp.
3462        child_ctx.set_frame_time(ctx.frame_time());
3463        // Thread the app's active theme down unchanged (copied ref, so the child
3464        // context holds no borrow of the parent), mirroring the clock.
3465        child_ctx.set_theme(ctx.theme_ref());
3466        // Thread the window insets down unchanged (copied — global and
3467        // origin-independent), mirroring the theme.
3468        child_ctx.set_window_insets(ctx.window_insets_ref());
3469        // Thread the shell's presented-frame count down unchanged (copied
3470        // `Option<u64>`), mirroring the clock/insets — global, origin-independent.
3471        child_ctx.set_presented_frames(ctx.presented_frames());
3472        // Thread the tagged-rect ("hero") reporter down the same way, so a hero
3473        // wrapper nested arbitrarily deep under an installer sees it. `None` in
3474        // the normal case (no shared-element transition in flight).
3475        child_ctx.set_hero(ctx.hero_ref());
3476        // Thread the scroll ancestor's visible rect down unchanged (absolute
3477        // coords, so a nested container tests its children directly against it),
3478        // mirroring the theme/insets. `None` in the normal case (no scroll
3479        // ancestor culling), so paint descends into every child as before. A
3480        // transformed pod passes the rect's preimage instead (see `paint_child`).
3481        child_ctx.set_visible_rect(visible_rect);
3482        // Thread the shell's surface-translucency flag down unchanged (copied
3483        // bool, global and origin-independent), mirroring the theme/insets — the
3484        // platform-view hole-punch reads it (see `PaintCtx::is_translucent`).
3485        child_ctx.set_translucent(ctx.is_translucent());
3486        child_ctx.set_paint_state(
3487            ctx.paint_epoch,
3488            ctx.retained_layers,
3489            ctx.retained_ledger,
3490            ctx.paint_serial,
3491        );
3492        // Thread the live focus epoch down unchanged (global, like the clock),
3493        // and seed this child's focus from its own recorded link against it —
3494        // the mirror of how `event_child` seeds the child `EventCtx`, so a
3495        // focus-dependent widget observes a blur that never reached its
3496        // `event()`. Three conjuncts, each load-bearing:
3497        //
3498        // * `self.focused` — a link was recorded here at all;
3499        // * the stamp — that link belongs to the session the root has *now*, not
3500        //   to one it has since left. Paint descends into every branch
3501        //   unconditionally and a branch the session left is reached by no pass
3502        //   that could clear its flag, so without this a field the user has
3503        //   walked away from goes on painting a caret and republishing the
3504        //   surface it described while it still had the session;
3505        // * `ctx.has_focus()` — the ancestor chain, since a blur clears the link
3506        //   at the nearest common ancestor only and flags deeper in the blurred
3507        //   subtree legitimately go stale.
3508        //
3509        // Exactly the composition the hover seed below uses, for exactly the
3510        // same reason.
3511        child_ctx.set_focus_epoch(ctx.focus_epoch());
3512        child_ctx.set_has_focus(
3513            self.focused && self.focus_epoch == ctx.focus_epoch() && ctx.has_focus(),
3514        );
3515        // Thread the live hover epoch down unchanged (global, like the clock), and
3516        // seed this child's hover link from its own recorded stamp against it —
3517        // ANDed with the ancestor's hover, exactly like `focused` above. Both halves
3518        // are load-bearing: the stamp is what distinguishes the current claim path
3519        // from a stale one no container ever cleared, and the chain is what keeps a
3520        // context carrying no live epoch at all (a bare `PaintCtx::new`, whose `0`
3521        // would match a never-claimed pod's `0`) from reading as hovered.
3522        child_ctx.set_hover_epoch(ctx.hover_epoch());
3523        child_ctx.set_hovered(self.hover_epoch == ctx.hover_epoch() && ctx.is_hovered());
3524        self.widget.paint(&mut child_ctx, scene);
3525        ctx.paint_publications = ctx
3526            .paint_publications
3527            .saturating_add(child_ctx.paint_publications);
3528        // Bubble the child's continuation-frame request AND its tick class up
3529        // unchanged: forwarding the aggregate class (rather than always calling
3530        // `request_frame`, which is Transition) is what lets a purely-cosmetic
3531        // subtree stay paceable through nested containers. `frame_class()` is
3532        // `None` when the child asked for nothing, so a still child bubbles
3533        // nothing (the max-lattice identity).
3534        match child_ctx.frame_class() {
3535            Some(TickClass::Transition) => ctx.request_frame_class(TickClass::Transition),
3536            // Deliberately NOT `request_frame_class(CosmeticLoop)`: that folds
3537            // `Duration::ZERO` (the theme cap) into the parent's MIN-lattice and
3538            // would silently re-tighten a child that asked for a *slower*
3539            // cadence. Forward the child's own aggregate interval instead — the
3540            // MIN-lattice's bubbling identity — via `absorb_paced_interval`,
3541            // the same rule `with_hero_registry` uses.
3542            Some(TickClass::CosmeticLoop) => {
3543                // Bubble `needs_frame` unconditionally: a CosmeticLoop
3544                // `frame_class` means the child genuinely requested a frame,
3545                // independent of whether an interval merges below (mirrors
3546                // `with_hero_registry`'s unconditional `needs_frame` bubble,
3547                // which is likewise separate from its interval fold).
3548                ctx.needs_frame = true;
3549                // Invariant: `frame_class() == Some(CosmeticLoop)` requires
3550                // `needs_frame && !frame_unpaced`, and the only paths that can
3551                // produce that pair — `request_frame_paced_at` directly, or a
3552                // nested `paint_child`/`with_hero_registry` bubble grounded in
3553                // the same call by this identical induction — always merge a
3554                // paced interval in the same step. So `paced_interval()` is
3555                // never actually `None` here through the public
3556                // `request_frame_paced*` API; this documents that belief
3557                // rather than silently trusting it. `absorb_paced_interval`
3558                // (below) is what actually implements the fallback, so
3559                // behavior stays correct even if a future caller manages to
3560                // trip this.
3561                debug_assert!(
3562                    child_ctx.paced_interval().is_some(),
3563                    "CosmeticLoop frame_class with no merged paced_interval — \
3564                     a new caller must be bypassing request_frame_paced_at"
3565                );
3566                ctx.absorb_paced_interval(child_ctx.paced_interval());
3567            }
3568            None => {}
3569        }
3570        // Bubble the child's layout-continuation request the same way as
3571        // `needs_frame`, so a nested widget animating its layout keeps layout
3572        // re-running up the whole tree. (`request_layout` also re-forces the
3573        // Transition class via `request_frame_class` above's contract.)
3574        if child_ctx.needs_layout() {
3575            ctx.request_layout();
3576        }
3577        // The layout-skip half of the same two bubbles: a child that asked for
3578        // layout, or for any frame at all (an animating subtree — the
3579        // conservative rule), is laid out again on the next pass. Both flags
3580        // bubble, so every pod from the requester up to the root is marked.
3581        //
3582        // One trailing paint more: the paint on which an animation *lands* is
3583        // typically the first that asks for nothing (an `advance` that snaps
3584        // onto its target returns `false`), yet it is the one that recorded the
3585        // final value a paint-fed layout reads — so a pod whose previous paint
3586        // asked is dirtied once more, and the settled geometry is laid out.
3587        //
3588        // The mark is reported upward as the path bit (and a child's bit marks
3589        // this pod too), which the root folds into its pending `LAYOUT` so a
3590        // shell running layout only on that flag reaches the dirtied pods. Two
3591        // marks stay unreported. A paced-only request (`CosmeticLoop`, a
3592        // decorative loop such as a caret blink) is marked but forces no pass:
3593        // a pending flag would make the frame gate run every vsync and undo the
3594        // pacing, and the next pass that does run still lays the pod out. And a
3595        // loop still running reports nothing through `painted_animating`; the
3596        // landing paint does, once — `painted_animating` tracks the child's own
3597        // request, never the path bit, so the paint after it reports nothing.
3598        let animating = child_ctx.needs_layout() || child_ctx.needs_frame();
3599        let dirtied = animating || self.painted_animating || child_ctx.layout_path_dirty;
3600        if dirtied {
3601            self.layout_dirty = true;
3602        }
3603        // The paint-dirty twin of the same channels: the subtree was painted
3604        // this pass, so the bit clears unless the paint itself asked for another
3605        // frame/layout (the output is still changing), the previous paint did
3606        // (the landing paint), or a pod below was dirtied during it.
3607        self.paint_dirty = dirtied;
3608        self.painted_epoch = ctx.paint_epoch;
3609        let unpaced = child_ctx.needs_layout()
3610            || (child_ctx.needs_frame() && !child_ctx.needs_frame_paced_only());
3611        let landing = self.painted_animating && !animating;
3612        if unpaced || landing || child_ctx.layout_path_dirty {
3613            ctx.layout_path_dirty = true;
3614        }
3615        self.painted_animating = animating;
3616        // Bubble a focused editable's republished IME surface up the paint path,
3617        // so `RenderRoot::paint` can refresh the shell-facing state after a
3618        // rebuild-driven controlled change (see `PaintCtx::publish_ime_state`).
3619        if let Some(ime) = child_ctx.take_ime_state() {
3620            ctx.publish_ime_state(ime);
3621        }
3622        // Bubble any platform-view frames published this paint by EXTENDING
3623        // the parent's Vec — deliberately NOT the `ime_state` overwrite shape
3624        // above. Two sibling slots publishing in the same pass must both
3625        // survive; an Option-based merge here would silently drop every slot
3626        // but the last child painted (see `PaintCtx::publish_platform_view`).
3627        ctx.platform_views.extend(child_ctx.take_platform_views());
3628        // Bubble any z-shield rects reported this paint the same way, and for
3629        // the same reason — two sibling shields must both survive (see
3630        // `PaintCtx::report_input_shield`).
3631        ctx.input_shields.extend(child_ctx.take_input_shields());
3632    }
3633
3634    /// Collect the child's semantics, translating the current absolute origin
3635    /// into the child's space exactly like [`ChildPod::paint_child`] offsets its
3636    /// paint origin (`child_origin = ctx.origin() + self.origin`).
3637    ///
3638    /// A container's [`Widget::semantics`] calls this for each of its
3639    /// [`ChildPod`]s so their nodes attach under the container's node (or, for a
3640    /// transparent container, under whatever encloses it — see
3641    /// [`crate::semantics`]).
3642    ///
3643    /// A pod with a [`transform`](ChildPod::set_transform) reports its subtree at
3644    /// the axis-aligned bounding box of the transformed child rect — an
3645    /// approximation: descendants are offset from that box's corner, unscaled
3646    /// and unrotated.
3647    pub fn semantics_child(&self, ctx: &mut SemanticsCtx) {
3648        let base = self.semantics_base(ctx);
3649        let (offset, size) = match self.transform {
3650            None => (self.origin.to_vec2(), self.size),
3651            Some(transform) => {
3652                let bounds = self
3653                    .local_to_container(transform)
3654                    .transform_rect_bbox(self.size.to_rect());
3655                if bounds.is_finite() {
3656                    (bounds.origin().to_vec2(), bounds.size())
3657                } else {
3658                    // A non-finite transform has no meaningful box: report an
3659                    // empty frame at the origin rather than NaN bounds.
3660                    (self.origin.to_vec2(), Size::ZERO)
3661                }
3662            }
3663        };
3664        ctx.descend_into_pod(base, offset, size, |ctx| {
3665            self.widget.semantics(ctx);
3666        });
3667    }
3668
3669    /// This pod's stable semantics base id, assigning one from the allocator on
3670    /// the first visit and reusing the cached value thereafter —
3671    /// the mechanism that keeps a widget's node id stable across frames and keyed
3672    /// reorders. See [`ChildPod::semantics_id`].
3673    fn semantics_base(&self, ctx: &mut SemanticsCtx) -> NonZeroU64 {
3674        match self.semantics_id.get() {
3675            Some(id) => id,
3676            None => {
3677                let id = ctx.alloc_base();
3678                self.semantics_id.set(Some(id));
3679                id
3680            }
3681        }
3682    }
3683
3684    /// Route an event into the child, translating its position into the child's
3685    /// local space and folding the child's redraw/capture flags back into `ctx`.
3686    ///
3687    /// If the child captured the pointer, this records the active path
3688    /// ([`ChildPod::is_active`]); the container clears it on `Up`/`Cancel`.
3689    ///
3690    /// Containers should not call this directly gated on an ad-hoc
3691    /// `contains()` check — that drops a captured gesture the instant it moves
3692    /// outside the child's bounds. Route through `frust-widgets`'
3693    /// `route_event`/`route_event_single` helpers instead, which check
3694    /// [`ChildPod::is_active`] first and forward unconditionally to a captured
3695    /// child.
3696    ///
3697    /// A pod with a [`transform`](ChildPod::set_transform) maps positions
3698    /// through the inverse of its local→container mapping instead
3699    /// ([`InputEvent::transformed`]); everything else about the dispatch —
3700    /// capture, contact and focus bookkeeping, hover — is identical.
3701    ///
3702    /// # Another contact walks the active path forward-only
3703    ///
3704    /// A non-claimant contact the root delivers down a live capture (rule (c) of
3705    /// [`InputEvent::PointerContact`]'s contract) is meant for the widget that
3706    /// opted in with [`EventCtx::capture_contacts`] — **the captor** — and for
3707    /// nothing above it. The containers between the root and the captor must
3708    /// not run their own pointer handling on it: a scroll view that saw a second
3709    /// finger's `Down` as its own would re-arm its drag from the wrong finger and
3710    /// take the gesture away from the captor on the claimant's next move.
3711    ///
3712    /// A pod cannot reach into its child widget's own pods, so the walk travels
3713    /// on the one route every container already provides without running its
3714    /// pointer machinery: the broadcast-first rule. While the root walks such a
3715    /// contact, each container on the path is handed an inert carrier (an
3716    /// [`InputEvent::Overlay`] addressed to a key no overlay owner holds, which
3717    /// every widget ignores and every container forwards to its children before
3718    /// anything else), and the real event rides beside it, re-based into each
3719    /// pod's space on the way down. This method then decides, per pod:
3720    ///
3721    /// * **Off the recorded active path:** nothing — the child widget is not
3722    ///   called at all.
3723    /// * **The captor** (the child itself opted in on the capture that made
3724    ///   this pod active): the child receives the real event, translated as
3725    ///   usual, with the walk closed, so it — and whatever it routes below
3726    ///   itself — handles the contact the ordinary way. The walk ends here: a
3727    ///   widget below the captor that also opted in hears the contact only if
3728    ///   the captor forwards it.
3729    /// * **On the path, above the captor:** the child receives the carrier, so
3730    ///   its own handler runs but none of its gesture, capture, focus, blur or
3731    ///   hit-test logic does; the walk continues into its children. The result
3732    ///   reported upward is the captor's, not the carrier's `Ignored`. If the
3733    ///   carrier never reaches a pod on the path (a container whose captured
3734    ///   child is not one of its broadcast targets — an overlay owner whose
3735    ///   captured pod is a floated surface), this child alone is handed the
3736    ///   real event the ordinary way instead.
3737    ///
3738    /// Everything this method folds back into `ctx` — redraw, capture, focus,
3739    /// IME — still bubbles from the captor through every pod on the way back
3740    /// up. No hover, cursor or blur bookkeeping moves: the root opens no hover
3741    /// pass for a non-claimant contact, and no container above the captor runs
3742    /// the blur sweep it would run on a `Down`. Every other dispatch, including
3743    /// the claimant's own events, takes exactly the path described above this
3744    /// section.
3745    pub fn event_child(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
3746        if let Some(real) = crate::event::secondary_walk_event(event) {
3747            return self.walk_secondary(ctx, &real);
3748        }
3749        let local = self.localize(event);
3750        self.dispatch_local(ctx, &local)
3751    }
3752
3753    /// `event` (in the container's space) mapped into the child's local space —
3754    /// translated by `-origin`, or through the inverse of a set transform.
3755    fn localize(&self, event: &InputEvent) -> InputEvent {
3756        match self.transform {
3757            None => event.translated(-self.origin.to_vec2()),
3758            Some(transform) => {
3759                match crate::hit::checked_inverse(&self.local_to_container(transform)) {
3760                    Some(inverse) => event.transformed(&inverse),
3761                    // No inverse: `contains` never hits such a pod, so only a
3762                    // recorded capture/focus path reaches here. Deliver the event
3763                    // translated as if untransformed so that path can still end.
3764                    None => event.translated(-self.origin.to_vec2()),
3765                }
3766            }
3767        }
3768    }
3769
3770    /// One step of a non-claimant contact's forward-only walk (see
3771    /// [`ChildPod::event_child`]); `real` is the contact's event in the
3772    /// container's space.
3773    fn walk_secondary(&mut self, ctx: &mut EventCtx, real: &InputEvent) -> EventResult {
3774        if !self.active {
3775            return EventResult::Ignored;
3776        }
3777        let local = self.localize(real);
3778        let result = if self.contacts_captor {
3779            crate::event::without_secondary_walk(|| self.dispatch_local(ctx, &local))
3780        } else {
3781            let carrier = crate::event::secondary_walk_carrier();
3782            let (_, delivered) = crate::event::run_secondary_walk(local.clone(), || {
3783                self.dispatch_local(ctx, &carrier)
3784            });
3785            match delivered {
3786                Some(result) => result,
3787                None => crate::event::without_secondary_walk(|| self.dispatch_local(ctx, &local)),
3788            }
3789        };
3790        crate::event::note_secondary_delivered(result);
3791        result
3792    }
3793
3794    /// Dispatch `local` (already in the child's space) to the child widget and
3795    /// fold everything it bubbled back into `ctx`.
3796    fn dispatch_local(&mut self, ctx: &mut EventCtx, local: &InputEvent) -> EventResult {
3797        // The child's hover link, composed with the ancestor chain exactly like
3798        // `focused` (see `paint_child`).
3799        let hovered = self.hover_epoch == ctx.hover_epoch() && ctx.is_hovered();
3800        // Two narrowings on top of whatever the root allowed, both structural:
3801        // a pod holding the capture path can never let a claim through (a drag
3802        // must not paint hover under the pointer, whatever the root's own capture
3803        // mirror says), and a pass that already recorded a claim is closed to
3804        // further ones, so the first claim recorded wins. With topmost-first hit
3805        // testing and containers claiming only *after* they route (see
3806        // `EventCtx::claim_hover`'s contract), that first claim is the topmost
3807        // claimant's; a container that claims before it forwards is recorded first
3808        // instead and closes the pass to its own subtree.
3809        let hover_eligible = ctx.is_hover_eligible() && !ctx.is_hover_claimed() && !self.active;
3810        let claim_epoch = ctx.hover_claim_epoch();
3811        // The child context inherits `ctx.pointer_id()` unchanged (see
3812        // `EventCtx::child_ctx`), so every widget on the routed path reports the
3813        // same contact.
3814        let (
3815            (opted_in, opted_in_at_or_below),
3816            captured,
3817            contacts,
3818            released,
3819            hover_claimed,
3820            focus_req,
3821            focus_rel,
3822            redraw,
3823            (layout_requested, layout_path_dirty),
3824            ime,
3825            result,
3826        ) = {
3827            let mut child_ctx = ctx.child_ctx(
3828                self.origin,
3829                self.size,
3830                self.focused,
3831                hovered,
3832                hover_eligible,
3833            );
3834            // The frame attributes a `capture_contacts` call to this child (or
3835            // to a widget below it) across a component boundary, and marks the
3836            // dispatch as running under the live opt-in's holder.
3837            let frame = crate::event::ContactFrame::enter(self.active && self.contacts_captor);
3838            let result = self.widget.event(&mut child_ctx, local);
3839            let (opted_in, opted_in_at_or_below) = frame.close();
3840            (
3841                (opted_in, opted_in_at_or_below),
3842                child_ctx.is_pointer_captured(),
3843                child_ctx.is_contact_capture_requested(),
3844                child_ctx.is_capture_released(),
3845                child_ctx.is_hover_claimed(),
3846                child_ctx.is_focus_requested(),
3847                child_ctx.is_focus_released(),
3848                child_ctx.needs_redraw(),
3849                (child_ctx.needs_layout(), child_ctx.is_layout_path_dirty()),
3850                child_ctx.take_ime_state(),
3851                result,
3852            )
3853        };
3854        // The layout-skip dirty source of the event pass: a child that consumed
3855        // the event, asked for a redraw or layout, or has a dirtied pod below it
3856        // is laid out again on the next pass. The path bit is folded into `ctx`
3857        // so the pod above marks itself in turn — a broadcast's container returns
3858        // `Ignored` whatever its child did, so the result alone would not reach
3859        // the root.
3860        let path_dirty = layout_path_dirty
3861            || layout_requested
3862            || redraw
3863            || matches!(result, EventResult::Handled);
3864        if path_dirty {
3865            self.layout_dirty = true;
3866            self.paint_dirty = true;
3867        }
3868        ctx.absorb_layout(layout_requested, path_dirty);
3869        if captured {
3870            if !self.active {
3871                // A fresh capture: whatever opt-in the last gesture recorded
3872                // here is gone.
3873                self.contacts_captor = false;
3874                self.contacts_path = false;
3875            }
3876            self.active = true;
3877            self.contacts_captor |= opted_in;
3878            self.contacts_path |= opted_in_at_or_below;
3879        }
3880        // Stamp the claim onto this pod so the whole path from the claimant up to
3881        // the root carries the epoch the next paint compares against. Never
3882        // cleared: a stale stamp is stranded by the next epoch advance instead.
3883        // The running root's identity rides along, so this pod's destructor can
3884        // tell its own root's live link from another root's identical epoch
3885        // integer (see `hover_root`).
3886        if hover_claimed {
3887            self.hover_epoch = claim_epoch;
3888            self.hover_root = ctx.hover_root();
3889            // The first stamp of the pass is the claimant's own pod (stamps land
3890            // bottom-up, as each nested dispatch returns); record which pod it
3891            // was for the root's hover-link comparison.
3892            if HOVER_CLAIMANT.with(Cell::get).is_none() {
3893                let id = self.inspect_id().0;
3894                HOVER_CLAIMANT.with(|claimant| claimant.set(Some(id)));
3895            }
3896        }
3897        // Focus is the second recorded path, maintained exactly like `active`: a
3898        // `focus_requested` bubble records this child as the focused one; a
3899        // `focus_released` bubble drops it. A request wins over a release in the
3900        // rare case both fire in one dispatch (a re-focus supersedes a blur).
3901        //
3902        // The record goes through `set_focused`, which stamps the session the
3903        // claim belongs to beside the flag: the whole chain from the claimant up
3904        // to the root is on one bubble and therefore takes one stamp, which is
3905        // what lets a container later ask which of two flagged children is on the
3906        // live chain.
3907        if focus_rel {
3908            self.set_focused(false);
3909        }
3910        if focus_req {
3911            self.set_focused(true);
3912        }
3913        // The contact opt-in bubbles exactly like the capture it accompanies; the
3914        // `active` link above stays keyed on the claimant (see `set_active`).
3915        ctx.absorb_child(
3916            redraw,
3917            captured,
3918            contacts,
3919            released,
3920            hover_claimed,
3921            focus_req,
3922            focus_rel,
3923            ime,
3924        );
3925        result
3926    }
3927
3928    /// Whether `point` (in the container's coordinate space) lies within this
3929    /// child's bounds — the container's hit test.
3930    ///
3931    /// This is only the *initial* hit test (deciding which child a fresh
3932    /// `Down`/first contact goes to). Once a child has captured the pointer
3933    /// ([`ChildPod::is_active`]), subsequent events must bypass this check and
3934    /// go straight to the captured child regardless of where the point now
3935    /// falls — see `frust-widgets`' `route_event`/`route_event_single`.
3936    ///
3937    /// A pod with a [`transform`](ChildPod::set_transform) maps `point` back
3938    /// through the transform first, so the test agrees with what paint drew; a
3939    /// transform with no inverse hits nothing.
3940    pub fn contains(&self, point: Point) -> bool {
3941        if let Some(transform) = self.transform {
3942            return crate::hit::point_in_transformed_rect(
3943                point,
3944                self.size.to_rect(),
3945                &self.local_to_container(transform),
3946            );
3947        }
3948        point.x >= self.origin.x
3949            && point.x < self.origin.x + self.size.width
3950            && point.y >= self.origin.y
3951            && point.y < self.origin.y + self.size.height
3952    }
3953}
3954
3955#[cfg(test)]
3956mod focus_link_tests {
3957    use super::*;
3958
3959    /// A guard restoring the focus channel this thread started with, so a test
3960    /// that publishes a session of its own cannot leak it into a neighbour.
3961    struct Session(u64, u64, u64);
3962
3963    impl Session {
3964        fn enter(root: u64, live: u64, claim: u64) -> Self {
3965            let previous = LIVE_FOCUS_SESSION.with(|slot| slot.get());
3966            set_live_focus_session(root, live, claim);
3967            Session(previous.0, previous.1, previous.2)
3968        }
3969    }
3970
3971    impl Drop for Session {
3972        fn drop(&mut self) {
3973            set_live_focus_session(self.0, self.1, self.2);
3974        }
3975    }
3976
3977    /// A do-nothing leaf: these tests exercise the pod's own bookkeeping, so
3978    /// the widget inside it never runs.
3979    struct Inert;
3980
3981    impl Widget for Inert {
3982        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3983            bc.constrain(Size::new(10.0, 10.0))
3984        }
3985        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
3986    }
3987
3988    fn pod() -> ChildPod {
3989        ChildPod::new(Box::new(Inert))
3990    }
3991
3992    #[test]
3993    fn a_recorded_link_counts_only_while_its_stamp_names_the_live_session() {
3994        let _session = Session::enter(7, 3, 3);
3995        let mut pod = pod();
3996        pod.set_focused(true);
3997        assert!(pod.holds_live_focus(), "recorded against the live session");
3998        assert_eq!(pod.focus_epoch(), 3);
3999
4000        // The session moves on. Nothing visits this pod; its flag is untouched.
4001        set_live_focus_session(7, 4, 4);
4002        assert!(
4003            pod.is_focused(),
4004            "the raw record survives, as it always did"
4005        );
4006        assert!(
4007            !pod.holds_live_focus(),
4008            "but it no longer names the session the root has"
4009        );
4010    }
4011
4012    #[test]
4013    fn a_link_of_another_root_never_counts_however_the_epochs_line_up() {
4014        let _session = Session::enter(7, 3, 3);
4015        let mut pod = pod();
4016        pod.set_focused(true);
4017
4018        // A second root on the same thread, with the identical epoch integer —
4019        // which two roots hold as a rule, not as a fluke.
4020        set_live_focus_session(8, 3, 3);
4021        assert!(!pod.holds_live_focus());
4022    }
4023
4024    #[test]
4025    fn a_claim_recorded_during_a_dispatch_counts_before_the_dispatch_commits_it() {
4026        // The root opened a candidate session: reads still answer against the
4027        // live one, and a link recorded now takes the candidate.
4028        let _session = Session::enter(7, 3, 4);
4029        let mut claimed = pod();
4030        claimed.set_focused(true);
4031        assert_eq!(claimed.focus_epoch(), 4, "stamped with the candidate");
4032        assert!(
4033            claimed.holds_live_focus(),
4034            "and observable to the container still unwinding around the claim"
4035        );
4036
4037        let mut standing = pod();
4038        standing.focused = true;
4039        standing.focus_root = 7;
4040        standing.focus_epoch = 3;
4041        assert!(
4042            standing.holds_live_focus(),
4043            "while a link recorded before this dispatch still reads live"
4044        );
4045
4046        // The dispatch commits the claim: the older link is stranded.
4047        set_live_focus_session(7, 4, 4);
4048        assert!(claimed.holds_live_focus());
4049        assert!(!standing.holds_live_focus());
4050    }
4051
4052    #[test]
4053    fn retiring_a_link_drops_only_one_the_live_session_has_already_stranded() {
4054        let _session = Session::enter(7, 3, 3);
4055        let mut pod = pod();
4056        pod.set_focused(true);
4057        assert!(
4058            !pod.retire_stale_focus_link(),
4059            "a live link is never retired by a pass that merely walked past it"
4060        );
4061        assert!(pod.is_focused());
4062
4063        set_live_focus_session(7, 4, 4);
4064        assert!(pod.retire_stale_focus_link(), "a stranded link is dropped");
4065        assert!(
4066            !pod.is_focused(),
4067            "so the raw flag and the stamp tell one story"
4068        );
4069        assert!(
4070            !pod.retire_stale_focus_link(),
4071            "and retiring is idempotent — there is nothing left to drop"
4072        );
4073    }
4074
4075    #[test]
4076    fn a_pod_that_never_held_a_link_reports_nothing_to_retire() {
4077        let _session = Session::enter(7, 3, 3);
4078        let mut pod = pod();
4079        assert!(!pod.holds_live_focus());
4080        assert!(!pod.retire_stale_focus_link());
4081    }
4082}
4083
4084#[cfg(test)]
4085mod tests {
4086    use super::*;
4087    use crate::event::{PointerButton, PointerEvent, PointerPhase};
4088
4089    /// A leaf widget that paints a filled box of a fixed intrinsic size.
4090    struct FixedBox {
4091        intrinsic: Size,
4092    }
4093
4094    impl Widget for FixedBox {
4095        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4096            bc.constrain(self.intrinsic)
4097        }
4098
4099        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
4100            scene.fill_rect(ctx.origin(), ctx.size(), Color::BLACK);
4101        }
4102    }
4103
4104    /// A leaf widget that advances no state but signals it wants another frame
4105    /// on every paint — stands in for an animating widget (e.g. a fling).
4106    struct Animator;
4107
4108    impl Widget for Animator {
4109        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4110            bc.max()
4111        }
4112
4113        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
4114            ctx.request_frame();
4115        }
4116    }
4117
4118    /// A leaf widget whose animation changes its layout: it signals
4119    /// [`PaintCtx::request_layout`] on every paint — stands in for an animating
4120    /// widget that resizes/repositions (e.g. an expanding accordion).
4121    struct LayoutAnimator;
4122
4123    impl Widget for LayoutAnimator {
4124        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4125            bc.max()
4126        }
4127
4128        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
4129            ctx.request_layout();
4130        }
4131    }
4132
4133    /// A leaf widget whose animation is a *pacable* decorative loop: it signals
4134    /// [`PaintCtx::request_frame_paced`] on every paint — stands in for a
4135    /// shimmer/idle-pulse whose cadence the frame gate may throttle.
4136    struct PacedAnimator;
4137
4138    impl Widget for PacedAnimator {
4139        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4140            bc.max()
4141        }
4142
4143        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
4144            ctx.request_frame_paced();
4145        }
4146    }
4147
4148    /// The interval a [`SlowPacedAnimator`] asks for — a ~2Hz caret blink,
4149    /// deliberately far slower than any theme's cosmetic-loop cap.
4150    const SLOW_PACE: Duration = Duration::from_millis(500);
4151
4152    /// A leaf widget whose decorative loop names its own (slow) cadence via
4153    /// [`PaintCtx::request_frame_paced_at`] — stands in for a blinking caret.
4154    struct SlowPacedAnimator;
4155
4156    impl Widget for SlowPacedAnimator {
4157        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4158            bc.max()
4159        }
4160
4161        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
4162            ctx.request_frame_paced_at(SLOW_PACE);
4163        }
4164    }
4165
4166    /// A scene recorder used to assert paint output without any GPU dependency.
4167    #[derive(Default)]
4168    struct RecordingScene {
4169        rects: Vec<(Point, Size)>,
4170        texts: Vec<(Point, String)>,
4171        shaders: Vec<(u64, Rect, f32)>,
4172        scene_textures: Vec<(u64, Rect)>,
4173    }
4174
4175    impl PaintScene for RecordingScene {
4176        fn fill_rect(&mut self, origin: Point, size: Size, _color: Color) {
4177            self.rects.push((origin, size));
4178        }
4179        fn draw_text(&mut self, origin: Point, text: &str) {
4180            self.texts.push((origin, text.to_string()));
4181        }
4182        fn draw_shader(&mut self, program: &ShaderProgram, dest: Rect, time: f32) {
4183            self.shaders.push((program.id(), dest, time));
4184        }
4185        fn draw_scene_texture(&mut self, id: u64, dest: Rect) {
4186            self.scene_textures.push((id, dest));
4187        }
4188    }
4189
4190    /// A leaf widget that records the local position of the last event it saw,
4191    /// mutates a `u32` app state, and optionally captures the pointer on `Down`.
4192    struct Probe {
4193        last_pos: Option<Point>,
4194        capture_on_down: bool,
4195    }
4196
4197    impl Widget for Probe {
4198        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4199            bc.max()
4200        }
4201        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
4202        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
4203            self.last_pos = Some(event.position());
4204            *ctx.state_mut::<u32>() += 1;
4205            ctx.request_redraw();
4206            if self.capture_on_down
4207                && matches!(
4208                    event,
4209                    InputEvent::Pointer(PointerEvent {
4210                        phase: PointerPhase::Down,
4211                        ..
4212                    })
4213                )
4214            {
4215                ctx.capture_pointer();
4216            }
4217            EventResult::Handled
4218        }
4219    }
4220
4221    fn down(x: f64, y: f64) -> InputEvent {
4222        InputEvent::Pointer(PointerEvent {
4223            phase: PointerPhase::Down,
4224            position: Point::new(x, y),
4225            button: PointerButton::Primary,
4226        })
4227    }
4228
4229    /// A leaf that requests focus on `Down`, releases it on Escape, records
4230    /// whether it saw a `Key`/`Ime` event, and reports whether it had focus when
4231    /// the last event arrived.
4232    #[derive(Default)]
4233    struct FocusProbe {
4234        saw_key: bool,
4235        had_focus_on_key: bool,
4236    }
4237
4238    impl Widget for FocusProbe {
4239        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4240            bc.max()
4241        }
4242        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
4243        fn event(&mut self, ctx: &mut EventCtx, event: &crate::event::InputEvent) -> EventResult {
4244            match event {
4245                InputEvent::Pointer(PointerEvent {
4246                    phase: PointerPhase::Down,
4247                    ..
4248                }) => {
4249                    ctx.request_focus();
4250                    EventResult::Handled
4251                }
4252                InputEvent::Key(_) | InputEvent::Ime(_) => {
4253                    self.saw_key = true;
4254                    self.had_focus_on_key = ctx.has_focus();
4255                    EventResult::Handled
4256                }
4257                _ => EventResult::Ignored,
4258            }
4259        }
4260    }
4261
4262    fn key_enter() -> InputEvent {
4263        InputEvent::Key(crate::event::KeyEvent {
4264            key: crate::event::Key::Named(crate::event::NamedKey::Enter),
4265            modifiers: crate::event::Modifiers::default(),
4266            repeat: false,
4267        })
4268    }
4269
4270    #[test]
4271    fn event_child_records_focus_on_request() {
4272        let mut pod = ChildPod::new(Box::new(FocusProbe::default()));
4273        pod.set_origin(Point::new(5.0, 5.0));
4274        assert!(!pod.is_focused());
4275
4276        let mut count = 0u32;
4277        let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::new(50.0, 50.0));
4278        pod.event_child(&mut ctx, &down(6.0, 6.0));
4279
4280        // The child requested focus → the pod records the focus path and it
4281        // bubbles up into the parent context.
4282        assert!(pod.is_focused());
4283        assert!(ctx.is_focus_requested());
4284    }
4285
4286    #[test]
4287    fn event_child_threads_has_focus_into_child() {
4288        // A focused pod seeds `has_focus` on the child dispatch; a Key event
4289        // reaching a focused child sees `has_focus() == true`.
4290        let mut pod = ChildPod::new(Box::new(FocusProbe::default()));
4291        pod.set_focused(true);
4292
4293        let mut count = 0u32;
4294        let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::new(10.0, 10.0));
4295        pod.event_child(&mut ctx, &key_enter());
4296
4297        let probe = pod.widget_mut().downcast_mut::<FocusProbe>().unwrap();
4298        assert!(probe.saw_key);
4299        assert!(probe.had_focus_on_key);
4300    }
4301
4302    #[test]
4303    fn widget_layout_respects_constraints() {
4304        let mut w = FixedBox {
4305            intrinsic: Size::new(1000.0, 1000.0),
4306        };
4307        let mut ctx = LayoutCtx::new();
4308        let bc = BoxConstraints::loose(Size::new(200.0, 100.0));
4309        assert_eq!(w.layout(&mut ctx, &bc), Size::new(200.0, 100.0));
4310    }
4311
4312    #[test]
4313    fn with_window_insets_scopes_and_restores_on_both_contexts() {
4314        use crate::insets::EdgeInsets;
4315        let root = WindowInsets::new(EdgeInsets::new(1.0, 2.0, 3.0, 4.0), EdgeInsets::ZERO);
4316        let scoped = root.consuming(true, true, true, true);
4317
4318        let mut lctx = LayoutCtx::new();
4319        lctx.set_window_insets(root);
4320        let inside = lctx.with_window_insets(scoped, |ctx| ctx.window_insets());
4321        assert_eq!(inside, scoped);
4322        assert_eq!(lctx.window_insets(), root, "layout override restored");
4323
4324        let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
4325        pctx.set_window_insets(root);
4326        let inside = pctx.with_window_insets(scoped, |ctx| ctx.window_insets());
4327        assert_eq!(inside, scoped);
4328        assert_eq!(pctx.window_insets(), root, "paint override restored");
4329    }
4330
4331    #[test]
4332    fn widget_paint_emits_into_scene() {
4333        let mut w = FixedBox {
4334            intrinsic: Size::new(50.0, 20.0),
4335        };
4336        let mut scene = RecordingScene::default();
4337        let mut ctx = PaintCtx::new(Point::new(5.0, 7.0), Size::new(50.0, 20.0));
4338        w.paint(&mut ctx, &mut scene);
4339        assert_eq!(
4340            scene.rects,
4341            vec![(Point::new(5.0, 7.0), Size::new(50.0, 20.0))]
4342        );
4343    }
4344
4345    #[test]
4346    fn widget_paint_emits_shader_into_scene() {
4347        /// A leaf widget that paints a shader quad.
4348        struct ShaderWidget {
4349            program: ShaderProgram,
4350            dest: Rect,
4351            time: f32,
4352        }
4353
4354        impl Widget for ShaderWidget {
4355            fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4356                bc.max()
4357            }
4358
4359            fn paint(&mut self, _ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
4360                scene.draw_shader(&self.program, self.dest, self.time);
4361            }
4362        }
4363
4364        let program = ShaderProgram::new("fn main() {}");
4365        let dest = Rect::new(10.0, 20.0, 100.0, 150.0);
4366        let time = 1.5;
4367        let mut w = ShaderWidget {
4368            program: program.clone(),
4369            dest,
4370            time,
4371        };
4372        let mut scene = RecordingScene::default();
4373        let mut ctx = PaintCtx::new(Point::ZERO, Size::new(200.0, 200.0));
4374        w.paint(&mut ctx, &mut scene);
4375        assert_eq!(scene.shaders.len(), 1);
4376        assert_eq!(scene.shaders[0].0, program.id());
4377        assert_eq!(scene.shaders[0].1, dest);
4378        assert_eq!(scene.shaders[0].2, time);
4379    }
4380
4381    #[test]
4382    fn widget_paint_emits_scene_texture_into_scene() {
4383        /// A leaf widget that paints a scene texture.
4384        struct SceneTextureWidget {
4385            id: u64,
4386            dest: Rect,
4387        }
4388
4389        impl Widget for SceneTextureWidget {
4390            fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4391                bc.max()
4392            }
4393
4394            fn paint(&mut self, _ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
4395                scene.draw_scene_texture(self.id, self.dest);
4396            }
4397        }
4398
4399        let id = 42u64;
4400        let dest = Rect::new(10.0, 20.0, 100.0, 150.0);
4401        let mut w = SceneTextureWidget { id, dest };
4402        let mut scene = RecordingScene::default();
4403        let mut ctx = PaintCtx::new(Point::ZERO, Size::new(200.0, 200.0));
4404        w.paint(&mut ctx, &mut scene);
4405        assert_eq!(scene.scene_textures.len(), 1);
4406        assert_eq!(scene.scene_textures[0].0, id);
4407        assert_eq!(scene.scene_textures[0].1, dest);
4408    }
4409
4410    #[test]
4411    fn downcast_recovers_concrete_widget() {
4412        let mut boxed: Box<dyn Widget> = Box::new(FixedBox {
4413            intrinsic: Size::new(3.0, 4.0),
4414        });
4415        let concrete = boxed.downcast_mut::<FixedBox>().expect("downcast");
4416        assert_eq!(concrete.intrinsic, Size::new(3.0, 4.0));
4417    }
4418
4419    #[test]
4420    fn boxed_widget_delegates_every_pass() {
4421        // The `Box<dyn Widget>: Widget` blanket impl must forward layout/paint/event.
4422        let mut boxed: Box<dyn Widget> = Box::new(Probe {
4423            last_pos: None,
4424            capture_on_down: false,
4425        });
4426        let mut ctx = LayoutCtx::new();
4427        assert_eq!(
4428            boxed.layout(&mut ctx, &BoxConstraints::tight(Size::new(4.0, 5.0))),
4429            Size::new(4.0, 5.0)
4430        );
4431        let mut count = 0u32;
4432        let mut ectx = EventCtx::new(&mut count, Point::ZERO, Size::new(4.0, 5.0));
4433        assert_eq!(
4434            boxed.event(&mut ectx, &down(1.0, 1.0)),
4435            EventResult::Handled
4436        );
4437        assert_eq!(count, 1);
4438    }
4439
4440    #[test]
4441    fn child_pod_layout_and_paint_offset_by_origin() {
4442        let mut pod = ChildPod::new(Box::new(FixedBox {
4443            intrinsic: Size::new(30.0, 10.0),
4444        }));
4445        pod.set_origin(Point::new(12.0, 8.0));
4446
4447        let mut lctx = LayoutCtx::new();
4448        let size = pod.layout_child(&mut lctx, &BoxConstraints::loose(Size::new(100.0, 100.0)));
4449        assert_eq!(size, Size::new(30.0, 10.0));
4450        assert_eq!(pod.size(), Size::new(30.0, 10.0));
4451
4452        // Paint under a container placed at (100, 200): child draws at the sum.
4453        let mut scene = RecordingScene::default();
4454        let mut pctx = PaintCtx::new(Point::new(100.0, 200.0), Size::new(300.0, 300.0));
4455        pod.paint_child(&mut pctx, &mut scene);
4456        assert_eq!(
4457            scene.rects,
4458            vec![(Point::new(112.0, 208.0), Size::new(30.0, 10.0))]
4459        );
4460    }
4461
4462    #[test]
4463    fn child_pod_translates_event_into_child_space() {
4464        let mut pod = ChildPod::new(Box::new(Probe {
4465            last_pos: None,
4466            capture_on_down: false,
4467        }));
4468        pod.set_origin(Point::new(10.0, 20.0));
4469
4470        let mut count = 0u32;
4471        let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::new(200.0, 200.0));
4472        // Event at container-space (25, 35) lands at child-local (15, 15).
4473        let result = pod.event_child(&mut ctx, &down(25.0, 35.0));
4474        assert_eq!(result, EventResult::Handled);
4475        assert_eq!(count, 1); // state mutated through the reborrowed context
4476        let probe = pod.widget_mut().downcast_mut::<Probe>().unwrap();
4477        assert_eq!(probe.last_pos, Some(Point::new(15.0, 15.0)));
4478    }
4479
4480    #[test]
4481    fn child_pod_propagates_capture_flag() {
4482        let mut pod = ChildPod::new(Box::new(Probe {
4483            last_pos: None,
4484            capture_on_down: true,
4485        }));
4486        pod.set_origin(Point::new(5.0, 5.0));
4487        assert!(!pod.is_active());
4488
4489        let mut count = 0u32;
4490        let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::new(50.0, 50.0));
4491        pod.event_child(&mut ctx, &down(6.0, 6.0));
4492
4493        // The child captured → the pod records the active path and the flag
4494        // bubbles up to the parent context.
4495        assert!(pod.is_active());
4496        assert!(ctx.is_pointer_captured());
4497        assert!(ctx.needs_redraw());
4498    }
4499
4500    #[test]
4501    fn child_pod_bubbles_needs_frame_from_child_paint() {
4502        // A non-animating child leaves the parent's frame flag clear.
4503        let mut still = ChildPod::new(Box::new(FixedBox {
4504            intrinsic: Size::new(10.0, 10.0),
4505        }));
4506        let mut lctx = LayoutCtx::new();
4507        still.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
4508        let mut scene = RecordingScene::default();
4509        let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
4510        still.paint_child(&mut pctx, &mut scene);
4511        assert!(!pctx.needs_frame());
4512
4513        // An animating child bubbles its request into the parent context.
4514        let mut anim = ChildPod::new(Box::new(Animator));
4515        anim.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
4516        let mut pctx2 = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
4517        assert!(!pctx2.needs_frame());
4518        anim.paint_child(&mut pctx2, &mut scene);
4519        assert!(pctx2.needs_frame());
4520    }
4521
4522    /// A leaf widget that publishes a fixed [`PlatformViewFrame`] on every
4523    /// paint, unless `should_publish` is false — the `false` arm stands in for
4524    /// a slot that didn't paint this pass (culled subtree), exercising the
4525    /// "no publishers this pass" behavior at the `RenderRoot`
4526    /// level (see `app.rs`'s tests).
4527    struct PlatformViewProbe {
4528        slot_id: u64,
4529        should_publish: bool,
4530    }
4531
4532    impl Widget for PlatformViewProbe {
4533        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4534            bc.max()
4535        }
4536
4537        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
4538            if self.should_publish {
4539                ctx.publish_platform_view(PlatformViewFrame {
4540                    slot_id: self.slot_id,
4541                    view_type: "dev.frust.Probe".to_string(),
4542                    params_json: String::new(),
4543                    params_generation: 0,
4544                    rect: Rect::from_origin_size(ctx.origin(), ctx.size()),
4545                    clip: None,
4546                    visible: true,
4547                    interactive: false,
4548                    shields: Vec::new(),
4549                    command_index: 0,
4550                });
4551            }
4552        }
4553    }
4554
4555    #[test]
4556    fn scene_builder_command_index_reports_the_live_count() {
4557        let mut scene = frust_scene::Scene::new();
4558        {
4559            let mut builder = frust_scene::SceneBuilder::new(&mut scene);
4560            let paint: &mut dyn PaintScene = &mut builder;
4561            assert_eq!(paint.command_index(), 0);
4562
4563            paint.fill_rect(Point::ZERO, Size::new(4.0, 4.0), Color::BLACK);
4564            assert_eq!(paint.command_index(), 1);
4565
4566            // Structural commands count too: the index is a position in the
4567            // scene's command list, not a tally of what draws.
4568            paint.push_clip(Point::ZERO, Size::new(4.0, 4.0));
4569            paint.pop_clip();
4570            assert_eq!(paint.command_index(), 3);
4571
4572            // A transform push records nothing, so the index stays put.
4573            paint.push_transform(Affine::translate((1.0, 1.0)));
4574            assert_eq!(paint.command_index(), 3);
4575            paint.pop_transform();
4576        }
4577        assert_eq!(scene.commands().len(), 3);
4578    }
4579
4580    #[test]
4581    fn default_command_index_is_zero() {
4582        // A recorder with no command list keeps the defaulted 0, however much
4583        // it is asked to paint.
4584        let mut discard = DiscardScene;
4585        discard.fill_rect(Point::ZERO, Size::new(4.0, 4.0), Color::BLACK);
4586        assert_eq!(discard.command_index(), 0);
4587
4588        let mut recording = RecordingScene::default();
4589        recording.fill_rect(Point::ZERO, Size::new(4.0, 4.0), Color::BLACK);
4590        assert_eq!(recording.command_index(), 0);
4591    }
4592
4593    #[test]
4594    fn child_pod_extends_platform_views_never_overwrites() {
4595        // Two slots publishing across two `paint_child` calls in one pass must
4596        // BOTH survive, in paint order — the regression test for the
4597        // Option-overwrite hazard: an ime_state-shaped merge here would leave
4598        // only the second slot's frame (see `PaintCtx::publish_platform_view`'s
4599        // doc comment).
4600        let mut lctx = LayoutCtx::new();
4601
4602        let mut first = ChildPod::new(Box::new(PlatformViewProbe {
4603            slot_id: 1,
4604            should_publish: true,
4605        }));
4606        first.set_origin(Point::new(0.0, 0.0));
4607        first.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
4608
4609        let mut second = ChildPod::new(Box::new(PlatformViewProbe {
4610            slot_id: 2,
4611            should_publish: true,
4612        }));
4613        second.set_origin(Point::new(20.0, 0.0));
4614        second.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
4615
4616        let mut scene = RecordingScene::default();
4617        let mut pctx = PaintCtx::new(Point::ZERO, Size::new(100.0, 100.0));
4618        first.paint_child(&mut pctx, &mut scene);
4619        second.paint_child(&mut pctx, &mut scene);
4620
4621        let frames = pctx.take_platform_views();
4622        assert_eq!(
4623            frames.len(),
4624            2,
4625            "both slots' frames must survive, not just the last-painted one"
4626        );
4627        assert_eq!(frames[0].slot_id, 1);
4628        assert_eq!(frames[1].slot_id, 2);
4629    }
4630
4631    /// A container widget wrapping a single child pod at a fixed offset —
4632    /// stands in for `frust-widgets::Padding` to test that a published frame's
4633    /// rect compounds correctly under nesting rather than staying local to the
4634    /// innermost pod.
4635    struct TranslatingWrapper {
4636        child: ChildPod,
4637    }
4638
4639    impl Widget for TranslatingWrapper {
4640        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4641            self.child.layout_child(ctx, bc)
4642        }
4643
4644        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
4645            self.child.paint_child(ctx, scene);
4646        }
4647    }
4648
4649    #[test]
4650    fn platform_view_frame_rect_is_absolute_under_nested_translation() {
4651        // Wrap a publishing probe under two levels of translation — an inner
4652        // pod at (5, 7) inside a wrapper placed at (100, 200) — the published
4653        // rect must land in ABSOLUTE window coordinates (the same space
4654        // `PaintCtx::report_hero` callers use), not local to either level.
4655        let mut lctx = LayoutCtx::new();
4656
4657        let inner = ChildPod::new(Box::new(PlatformViewProbe {
4658            slot_id: 9,
4659            should_publish: true,
4660        }));
4661        let mut wrapper = TranslatingWrapper { child: inner };
4662        wrapper.child.set_origin(Point::new(5.0, 7.0));
4663        wrapper
4664            .child
4665            .layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
4666
4667        let mut outer = ChildPod::new(Box::new(wrapper));
4668        outer.set_origin(Point::new(100.0, 200.0));
4669        outer.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
4670
4671        let mut scene = RecordingScene::default();
4672        let mut pctx = PaintCtx::new(Point::ZERO, Size::new(400.0, 400.0));
4673        outer.paint_child(&mut pctx, &mut scene);
4674
4675        let frames = pctx.take_platform_views();
4676        assert_eq!(frames.len(), 1);
4677        assert_eq!(
4678            frames[0].rect,
4679            Rect::from_origin_size(Point::new(105.0, 207.0), Size::new(10.0, 10.0))
4680        );
4681    }
4682
4683    #[test]
4684    fn paint_ctx_origin_is_absolute_through_nested_offsets() {
4685        // Nest a leaf widget under two levels of offset containers: the leaf
4686        // must observe the summed absolute window-space origin, not a
4687        // parent-relative one. This pins the accumulation contract that external
4688        // design systems (anchored overlays) depend on.
4689        use std::cell::Cell;
4690
4691        struct OriginRecorder {
4692            recorded_origin: Cell<Option<Point>>,
4693        }
4694
4695        impl Widget for OriginRecorder {
4696            fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4697                bc.max()
4698            }
4699
4700            fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
4701                self.recorded_origin.set(Some(ctx.origin()));
4702            }
4703        }
4704
4705        let mut lctx = LayoutCtx::new();
4706
4707        // Inner recorder at (20, 30) relative to middle container.
4708        let recorder = OriginRecorder {
4709            recorded_origin: Cell::new(None),
4710        };
4711        let inner = ChildPod::new(Box::new(recorder));
4712        let mut middle = TranslatingWrapper { child: inner };
4713        middle.child.set_origin(Point::new(20.0, 30.0));
4714        middle
4715            .child
4716            .layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
4717
4718        // Middle container at (50, 70) relative to outer.
4719        let mut outer = ChildPod::new(Box::new(middle));
4720        outer.set_origin(Point::new(50.0, 70.0));
4721        outer.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
4722
4723        // Paint from root at (0, 0).
4724        let mut scene = RecordingScene::default();
4725        let mut pctx = PaintCtx::new(Point::ZERO, Size::new(400.0, 400.0));
4726        outer.paint_child(&mut pctx, &mut scene);
4727
4728        // Verify: leaf's origin must be sum of all offsets:
4729        // root(0,0) + outer(50,70) + middle(20,30) = (70, 100).
4730        let leaf = outer
4731            .widget_mut()
4732            .downcast_mut::<TranslatingWrapper>()
4733            .unwrap()
4734            .child
4735            .widget_mut()
4736            .downcast_mut::<OriginRecorder>()
4737            .unwrap();
4738        assert_eq!(
4739            leaf.recorded_origin.get(),
4740            Some(Point::new(70.0, 100.0)),
4741            "leaf must observe summed absolute origin"
4742        );
4743    }
4744
4745    #[test]
4746    fn request_frame_alone_does_not_set_needs_layout() {
4747        // A paint-only animation (request_frame, no request_layout) must leave
4748        // `needs_layout` clear — the mobile intra-frame layout-skip depends on this.
4749        let mut anim = ChildPod::new(Box::new(Animator));
4750        let mut lctx = LayoutCtx::new();
4751        anim.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
4752        let mut scene = RecordingScene::default();
4753        let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
4754        anim.paint_child(&mut pctx, &mut scene);
4755        assert!(pctx.needs_frame(), "request_frame sets needs_frame");
4756        assert!(
4757            !pctx.needs_layout(),
4758            "request_frame alone must NOT set needs_layout"
4759        );
4760    }
4761
4762    #[test]
4763    fn request_layout_implies_needs_frame() {
4764        // `request_layout` also sets `needs_frame` so one call per
4765        // animating-layout frame suffices.
4766        let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
4767        assert!(!ctx.needs_frame());
4768        assert!(!ctx.needs_layout());
4769        ctx.request_layout();
4770        assert!(ctx.needs_layout());
4771        assert!(ctx.needs_frame());
4772    }
4773
4774    #[test]
4775    fn child_pod_bubbles_needs_layout_from_child_paint() {
4776        // A non-layout-animating child leaves the parent's layout flag clear.
4777        let mut still = ChildPod::new(Box::new(FixedBox {
4778            intrinsic: Size::new(10.0, 10.0),
4779        }));
4780        let mut lctx = LayoutCtx::new();
4781        still.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
4782        let mut scene = RecordingScene::default();
4783        let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
4784        still.paint_child(&mut pctx, &mut scene);
4785        assert!(!pctx.needs_layout());
4786
4787        // A layout-animating child bubbles its request into the parent context.
4788        let mut anim = ChildPod::new(Box::new(LayoutAnimator));
4789        anim.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
4790        let mut pctx2 = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
4791        assert!(!pctx2.needs_layout());
4792        anim.paint_child(&mut pctx2, &mut scene);
4793        assert!(pctx2.needs_layout());
4794        // Bubbling `request_layout` also carries the implied `needs_frame`.
4795        assert!(pctx2.needs_frame());
4796    }
4797
4798    #[test]
4799    fn needs_layout_bubbles_through_nested_containers() {
4800        // A container holding a single `ChildPod` forwards paint via
4801        // `paint_child`; a layout-animating leaf two levels deep must still
4802        // surface `needs_layout` at the outermost paint context.
4803        struct SingleChildContainer {
4804            child: ChildPod,
4805        }
4806        impl Widget for SingleChildContainer {
4807            fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4808                self.child.layout_child(ctx, bc)
4809            }
4810            fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
4811                self.child.paint_child(ctx, scene);
4812            }
4813        }
4814
4815        let inner = SingleChildContainer {
4816            child: ChildPod::new(Box::new(LayoutAnimator)),
4817        };
4818        let mut outer = ChildPod::new(Box::new(SingleChildContainer {
4819            child: ChildPod::new(Box::new(inner)),
4820        }));
4821        let mut lctx = LayoutCtx::new();
4822        outer.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
4823        let mut scene = RecordingScene::default();
4824        let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
4825        outer.paint_child(&mut pctx, &mut scene);
4826        assert!(
4827            pctx.needs_layout(),
4828            "needs_layout bubbles up nested containers"
4829        );
4830        assert!(pctx.needs_frame());
4831    }
4832
4833    /// A single-`ChildPod` container that forwards paint via `paint_child`,
4834    /// reused by the tick-class bubbling tests below.
4835    struct SingleChildContainer {
4836        child: ChildPod,
4837    }
4838    impl Widget for SingleChildContainer {
4839        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4840            self.child.layout_child(ctx, bc)
4841        }
4842        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
4843            self.child.paint_child(ctx, scene);
4844        }
4845    }
4846
4847    #[test]
4848    fn no_request_yields_no_frame_class() {
4849        // No frame requested at all → `frame_class()` is `None` and the frame is
4850        // not paced-only (as today: nothing to schedule).
4851        let ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
4852        assert!(ctx.frame_class().is_none());
4853        assert!(!ctx.needs_frame());
4854        assert!(!ctx.needs_frame_paced_only());
4855    }
4856
4857    #[test]
4858    fn request_frame_is_transition_unpaced() {
4859        // The unchanged `request_frame` is a Transition request: it must NOT be
4860        // paced-only, preserving today's every-vsync behavior for existing callers.
4861        let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
4862        ctx.request_frame();
4863        assert!(ctx.needs_frame());
4864        assert_eq!(ctx.frame_class(), Some(TickClass::Transition));
4865        assert!(
4866            !ctx.needs_frame_paced_only(),
4867            "request_frame stays unpaced (Transition), unchanged behavior"
4868        );
4869    }
4870
4871    #[test]
4872    fn request_frame_paced_is_cosmetic_loop() {
4873        let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
4874        ctx.request_frame_paced();
4875        assert!(ctx.needs_frame());
4876        assert_eq!(ctx.frame_class(), Some(TickClass::CosmeticLoop));
4877        assert!(ctx.needs_frame_paced_only());
4878        // The bare form names no interval: `Duration::ZERO` is "at the theme's
4879        // own cosmetic rate" — unchanged behavior for all six shimmer widgets.
4880        assert_eq!(ctx.paced_interval(), Some(Duration::ZERO));
4881    }
4882
4883    #[test]
4884    fn request_frame_paced_at_carries_its_interval() {
4885        // The explicit form is the same class, only slower: a ~500ms caret.
4886        let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
4887        ctx.request_frame_paced_at(Duration::from_millis(500));
4888        assert!(ctx.needs_frame());
4889        assert_eq!(ctx.frame_class(), Some(TickClass::CosmeticLoop));
4890        assert!(ctx.needs_frame_paced_only());
4891        assert_eq!(ctx.paced_interval(), Some(Duration::from_millis(500)));
4892    }
4893
4894    #[test]
4895    fn request_frame_paced_at_clamps_past_the_ten_second_ceiling() {
4896        // Past the ceiling: clamps DOWN to it rather than carrying the raw
4897        // caller value out to the shell's pacing arithmetic unbounded.
4898        let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
4899        ctx.request_frame_paced_at(Duration::from_secs(10) + Duration::from_secs(1));
4900        assert_eq!(ctx.paced_interval(), Some(PaintCtx::MAX_PACED_INTERVAL));
4901    }
4902
4903    #[test]
4904    fn request_frame_paced_at_leaves_a_real_cadence_untouched() {
4905        // Every shipped cadence (the ~500ms caret above, muxr's ~550ms blink)
4906        // sits nowhere near the ceiling and must pass through byte-for-byte —
4907        // the clamp must never change behavior for any existing caller.
4908        let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
4909        ctx.request_frame_paced_at(Duration::from_millis(550));
4910        assert_eq!(ctx.paced_interval(), Some(Duration::from_millis(550)));
4911    }
4912
4913    #[test]
4914    fn paced_intervals_aggregate_on_the_min_lattice() {
4915        // Two paced requests at different rates in one pass: the TIGHTEST wins,
4916        // so both are honored (the slower one is merely repainted more often
4917        // than it asked — see `request_frame_paced_at`'s contract).
4918        let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
4919        ctx.request_frame_paced_at(Duration::from_millis(500));
4920        ctx.request_frame_paced_at(Duration::from_millis(33));
4921        assert_eq!(ctx.paced_interval(), Some(Duration::from_millis(33)));
4922        assert!(ctx.needs_frame_paced_only());
4923
4924        // Order-independent (a lattice fold, not a last-writer-wins slot).
4925        let mut reversed = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
4926        reversed.request_frame_paced_at(Duration::from_millis(33));
4927        reversed.request_frame_paced_at(Duration::from_millis(500));
4928        assert_eq!(reversed.paced_interval(), Some(Duration::from_millis(33)));
4929
4930        // A bare `request_frame_paced` (the theme cap, `Duration::ZERO`) is the
4931        // absorbing element: a 30Hz shimmer beside a 2Hz caret paces at 30Hz.
4932        let mut shimmer_and_caret = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
4933        shimmer_and_caret.request_frame_paced_at(Duration::from_millis(500));
4934        shimmer_and_caret.request_frame_paced();
4935        assert_eq!(shimmer_and_caret.paced_interval(), Some(Duration::ZERO));
4936    }
4937
4938    #[test]
4939    fn request_frame_class_cosmetic_matches_the_bare_paced_request() {
4940        // The general form must stay interchangeable with `request_frame_paced`
4941        // (the widgets' `cull_pacing`/`pacing_integration` suites drive it).
4942        let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
4943        ctx.request_frame_class(TickClass::CosmeticLoop);
4944        assert_eq!(ctx.frame_class(), Some(TickClass::CosmeticLoop));
4945        assert_eq!(ctx.paced_interval(), Some(Duration::ZERO));
4946    }
4947
4948    #[test]
4949    fn no_paced_request_names_no_interval() {
4950        // Nothing requested, and a Transition-only request, both leave the
4951        // interval unset — there is no paced loop to pace.
4952        let ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
4953        assert_eq!(ctx.paced_interval(), None);
4954        let mut transition = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
4955        transition.request_frame();
4956        assert_eq!(transition.paced_interval(), None);
4957    }
4958
4959    #[test]
4960    fn transition_dominates_cosmetic_regardless_of_order() {
4961        // Paced then Transition → unpaced.
4962        let mut a = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
4963        a.request_frame_paced();
4964        a.request_frame();
4965        assert_eq!(a.frame_class(), Some(TickClass::Transition));
4966        assert!(!a.needs_frame_paced_only());
4967
4968        // Transition then paced → still unpaced (a CosmeticLoop never clears it).
4969        let mut b = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
4970        b.request_frame();
4971        b.request_frame_paced();
4972        assert_eq!(b.frame_class(), Some(TickClass::Transition));
4973        assert!(!b.needs_frame_paced_only());
4974    }
4975
4976    #[test]
4977    fn request_layout_implies_transition_class() {
4978        // `request_layout` is user-visible motion, so it re-forces the unpaced
4979        // Transition class even if a paced request preceded it.
4980        let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
4981        ctx.request_frame_paced();
4982        assert!(ctx.needs_frame_paced_only());
4983        ctx.request_layout();
4984        assert_eq!(ctx.frame_class(), Some(TickClass::Transition));
4985        assert!(
4986            !ctx.needs_frame_paced_only(),
4987            "request_layout implies Transition, leaving the frame unpaced"
4988        );
4989    }
4990
4991    #[test]
4992    fn child_pod_bubbles_paced_class_from_child_paint() {
4993        // A purely-cosmetic child bubbles a paced request into the parent — the
4994        // parent's aggregate stays paced-only.
4995        let mut anim = ChildPod::new(Box::new(PacedAnimator));
4996        let mut lctx = LayoutCtx::new();
4997        anim.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
4998        let mut scene = RecordingScene::default();
4999        let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
5000        anim.paint_child(&mut pctx, &mut scene);
5001        assert!(pctx.needs_frame());
5002        assert_eq!(pctx.frame_class(), Some(TickClass::CosmeticLoop));
5003        assert!(pctx.needs_frame_paced_only());
5004    }
5005
5006    #[test]
5007    fn paced_class_bubbles_through_nested_containers() {
5008        // A cosmetic-loop leaf two levels deep must still surface as paced-only
5009        // at the outermost paint context (the bubbling identity of the lattice).
5010        let inner = SingleChildContainer {
5011            child: ChildPod::new(Box::new(PacedAnimator)),
5012        };
5013        let mut outer = ChildPod::new(Box::new(SingleChildContainer {
5014            child: ChildPod::new(Box::new(inner)),
5015        }));
5016        let mut lctx = LayoutCtx::new();
5017        outer.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
5018        let mut scene = RecordingScene::default();
5019        let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
5020        outer.paint_child(&mut pctx, &mut scene);
5021        assert!(pctx.needs_frame());
5022        assert!(
5023            pctx.needs_frame_paced_only(),
5024            "a nested cosmetic-loop leaf stays paced-only up the tree"
5025        );
5026    }
5027
5028    #[test]
5029    fn child_pod_bubbles_a_named_interval_unchanged() {
5030        // A slow caret nested under a container must reach the root with its own
5031        // interval intact — bubbling must never re-tighten it to the theme cap
5032        // (which a blanket `request_frame_class(CosmeticLoop)` forward would).
5033        let mut outer = ChildPod::new(Box::new(SingleChildContainer {
5034            child: ChildPod::new(Box::new(SlowPacedAnimator)),
5035        }));
5036        let mut lctx = LayoutCtx::new();
5037        outer.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
5038        let mut scene = RecordingScene::default();
5039        let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
5040        outer.paint_child(&mut pctx, &mut scene);
5041        assert!(pctx.needs_frame_paced_only());
5042        assert_eq!(
5043            pctx.paced_interval(),
5044            Some(SLOW_PACE),
5045            "a nested slow paced loop keeps its own cadence up the tree"
5046        );
5047    }
5048
5049    #[test]
5050    fn absorb_paced_interval_skips_on_none_without_tightening_to_zero() {
5051        // The one rule shared by both paced-interval absorb sites
5052        // (`ChildPod::paint_child`'s CosmeticLoop arm, `with_hero_registry`):
5053        // a `None` interval must leave the aggregate untouched rather than
5054        // defaulting to `Duration::ZERO` (the MIN-lattice's own tightest,
5055        // most-tightening value) — the exact defect class `unwrap_or_default`
5056        // used to risk in `paint_child`.
5057        //
5058        // Exercised directly against the shared fold rather than through
5059        // `paint_child`'s real bubble: that call site's `debug_assert!`
5060        // documents `frame_class() == Some(CosmeticLoop)` with no merged
5061        // interval as unreachable through the public `request_frame_paced*`
5062        // API (a provable invariant — see its doc comment), so forcing that
5063        // exact state through the full `ChildPod::paint_child` path would
5064        // trip the tripwire instead of exercising the fallback it guards.
5065        let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
5066        ctx.absorb_paced_interval(None);
5067        assert_eq!(
5068            ctx.paced_interval(),
5069            None,
5070            "a None interval must not tighten the aggregate to Duration::ZERO"
5071        );
5072
5073        // A pre-existing aggregate is likewise untouched by a `None` fold.
5074        let mut ctx2 = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
5075        ctx2.request_frame_paced_at(Duration::from_millis(500));
5076        ctx2.absorb_paced_interval(None);
5077        assert_eq!(
5078            ctx2.paced_interval(),
5079            Some(Duration::from_millis(500)),
5080            "a None fold must not override an already-merged interval either"
5081        );
5082    }
5083
5084    #[test]
5085    fn sibling_paced_intervals_fold_to_the_tightest() {
5086        // A shimmer (theme cap) beside a slow caret under one container: the
5087        // container's aggregate paces at the shimmer's rate. The caret is then
5088        // repainted more often than it asked — no visual harm, by design.
5089        struct TwoChildContainer {
5090            a: ChildPod,
5091            b: ChildPod,
5092        }
5093        impl Widget for TwoChildContainer {
5094            fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
5095                self.a.layout_child(ctx, bc);
5096                self.b.layout_child(ctx, bc)
5097            }
5098            fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
5099                self.a.paint_child(ctx, scene);
5100                self.b.paint_child(ctx, scene);
5101            }
5102        }
5103
5104        let mut outer = ChildPod::new(Box::new(TwoChildContainer {
5105            a: ChildPod::new(Box::new(SlowPacedAnimator)),
5106            b: ChildPod::new(Box::new(PacedAnimator)),
5107        }));
5108        let mut lctx = LayoutCtx::new();
5109        outer.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
5110        let mut scene = RecordingScene::default();
5111        let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
5112        outer.paint_child(&mut pctx, &mut scene);
5113        assert!(pctx.needs_frame_paced_only());
5114        assert_eq!(
5115            pctx.paced_interval(),
5116            Some(Duration::ZERO),
5117            "the tightest sibling request (the theme cap) wins the fold"
5118        );
5119    }
5120
5121    #[test]
5122    fn mixed_sibling_requests_aggregate_to_unpaced() {
5123        // Two sibling children under one container: one paced, one Transition.
5124        // The container's aggregate must be unpaced (any Transition dominates).
5125        struct TwoChildContainer {
5126            a: ChildPod,
5127            b: ChildPod,
5128        }
5129        impl Widget for TwoChildContainer {
5130            fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
5131                self.a.layout_child(ctx, bc);
5132                self.b.layout_child(ctx, bc)
5133            }
5134            fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
5135                self.a.paint_child(ctx, scene);
5136                self.b.paint_child(ctx, scene);
5137            }
5138        }
5139
5140        let mut outer = ChildPod::new(Box::new(TwoChildContainer {
5141            a: ChildPod::new(Box::new(PacedAnimator)),
5142            b: ChildPod::new(Box::new(Animator)),
5143        }));
5144        let mut lctx = LayoutCtx::new();
5145        outer.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
5146        let mut scene = RecordingScene::default();
5147        let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
5148        outer.paint_child(&mut pctx, &mut scene);
5149        assert!(pctx.needs_frame());
5150        assert_eq!(pctx.frame_class(), Some(TickClass::Transition));
5151        assert!(
5152            !pctx.needs_frame_paced_only(),
5153            "a mixed paced+transition sibling set aggregates to unpaced"
5154        );
5155    }
5156
5157    #[test]
5158    fn with_hero_registry_bubbles_paced_class() {
5159        // The hero-reporter sub-context absorbs a paced request the same way it
5160        // absorbs `needs_frame`/`needs_layout`.
5161        let registry = RefCell::new(HeroFrames::default());
5162        let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
5163        ctx.with_hero_registry(&registry, |child| {
5164            child.request_frame_paced();
5165        });
5166        assert!(ctx.needs_frame());
5167        assert!(
5168            ctx.needs_frame_paced_only(),
5169            "with_hero_registry bubbles the paced class up"
5170        );
5171
5172        // A Transition inside the closure dominates the outer aggregate too.
5173        let mut ctx2 = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
5174        ctx2.request_frame_paced();
5175        ctx2.with_hero_registry(&registry, |child| {
5176            child.request_frame();
5177        });
5178        assert_eq!(ctx2.frame_class(), Some(TickClass::Transition));
5179        assert!(!ctx2.needs_frame_paced_only());
5180
5181        // The interval half of the absorb: a named interval inside surfaces
5182        // outside, and folds on the MIN-lattice with an outer request.
5183        let mut ctx3 = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
5184        ctx3.with_hero_registry(&registry, |child| {
5185            child.request_frame_paced_at(SLOW_PACE);
5186        });
5187        assert_eq!(ctx3.paced_interval(), Some(SLOW_PACE));
5188        ctx3.request_frame_paced();
5189        assert_eq!(
5190            ctx3.paced_interval(),
5191            Some(Duration::ZERO),
5192            "the outer theme-cap request tightens the folded aggregate"
5193        );
5194    }
5195
5196    #[test]
5197    fn child_pod_paint_seeds_has_focus_composed_with_ancestor() {
5198        use std::cell::Cell;
5199        use std::rc::Rc;
5200
5201        // A probe recording what `ctx.has_focus()` it observed during paint.
5202        struct FocusProbe(Rc<Cell<Option<bool>>>);
5203        impl Widget for FocusProbe {
5204            fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
5205                bc.constrain(Size::new(10.0, 10.0))
5206            }
5207            fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
5208                self.0.set(Some(ctx.has_focus()));
5209            }
5210        }
5211
5212        let seen = Rc::new(Cell::new(None));
5213        let mut pod = ChildPod::new(Box::new(FocusProbe(seen.clone())));
5214        let mut lctx = LayoutCtx::new();
5215        pod.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
5216        let mut scene = RecordingScene::default();
5217
5218        // Focused pod under a focused ancestor: the child observes focus.
5219        pod.set_focused(true);
5220        let mut focused_parent = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
5221        focused_parent.set_has_focus(true);
5222        pod.paint_child(&mut focused_parent, &mut scene);
5223        assert_eq!(seen.get(), Some(true));
5224
5225        // Focused pod under a BLURRED ancestor (the stale-deep-flag case): the
5226        // cleared ancestor link must force `has_focus == false` below it.
5227        let mut blurred_parent = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
5228        blurred_parent.set_has_focus(false);
5229        pod.paint_child(&mut blurred_parent, &mut scene);
5230        assert_eq!(seen.get(), Some(false));
5231
5232        // Unfocused pod under a focused ancestor stays unfocused.
5233        pod.set_focused(false);
5234        let mut focused_parent2 = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
5235        focused_parent2.set_has_focus(true);
5236        pod.paint_child(&mut focused_parent2, &mut scene);
5237        assert_eq!(seen.get(), Some(false));
5238    }
5239
5240    #[test]
5241    fn child_pod_stamps_a_hover_claim_and_seeds_it_by_epoch() {
5242        use std::cell::Cell;
5243        use std::rc::Rc;
5244
5245        /// A probe that claims hover on every event it receives and records what
5246        /// `is_hovered()` each pass reported.
5247        struct HoverProbe {
5248            event_seen: Rc<Cell<Option<bool>>>,
5249            paint_seen: Rc<Cell<Option<bool>>>,
5250        }
5251        impl Widget for HoverProbe {
5252            fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
5253                bc.constrain(Size::new(10.0, 10.0))
5254            }
5255            fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
5256                self.paint_seen.set(Some(ctx.is_hovered()));
5257            }
5258            fn event(&mut self, ctx: &mut EventCtx, _event: &InputEvent) -> EventResult {
5259                self.event_seen.set(Some(ctx.is_hovered()));
5260                ctx.claim_hover();
5261                EventResult::Ignored
5262            }
5263        }
5264
5265        let event_seen = Rc::new(Cell::new(None));
5266        let paint_seen = Rc::new(Cell::new(None));
5267        let mut pod = ChildPod::new(Box::new(HoverProbe {
5268            event_seen: event_seen.clone(),
5269            paint_seen: paint_seen.clone(),
5270        }));
5271        let mut lctx = LayoutCtx::new();
5272        pod.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
5273        let mut scene = RecordingScene::default();
5274        let mut state = ();
5275        let event = InputEvent::Pointer(PointerEvent {
5276            phase: PointerPhase::Move,
5277            position: Point::new(5.0, 5.0),
5278            button: PointerButton::Primary,
5279        });
5280
5281        // A never-claimed pod carries no stamp, and reads unhovered against any
5282        // live epoch (the root's starts past `0` for exactly this reason).
5283        assert_eq!(pod.hover_epoch(), 0);
5284
5285        // Dispatch on an INELIGIBLE pass (the default): the claim records nothing.
5286        {
5287            let mut ctx = EventCtx::new(&mut state as &mut dyn Any, Point::ZERO, pod.size());
5288            ctx.set_hover_epoch(4);
5289            pod.event_child(&mut ctx, &event);
5290            assert!(!ctx.is_hover_claimed(), "nothing bubbled up");
5291        }
5292        assert_eq!(pod.hover_epoch(), 0, "no stamp from an ineligible pass");
5293
5294        // Dispatch on an eligible pass: the pod is stamped with the *next* epoch
5295        // (the root advances its own when the pass ends) and the claim bubbles.
5296        {
5297            let mut ctx = EventCtx::new(&mut state as &mut dyn Any, Point::ZERO, pod.size());
5298            ctx.set_hover_epoch(4);
5299            ctx.set_hovered(true);
5300            ctx.set_hover_eligible(true);
5301            pod.event_child(&mut ctx, &event);
5302            assert!(ctx.is_hover_claimed());
5303            assert_eq!(
5304                event_seen.get(),
5305                Some(false),
5306                "the pod held no link going into this pass"
5307            );
5308        }
5309        assert_eq!(pod.hover_epoch(), 5);
5310
5311        // Paint against the epoch the claim recorded: hovered, and the ancestor
5312        // chain is ANDed in exactly like focus.
5313        let mut live = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
5314        live.set_hover_epoch(5);
5315        live.set_hovered(true);
5316        pod.paint_child(&mut live, &mut scene);
5317        assert_eq!(paint_seen.get(), Some(true));
5318
5319        // Same stamp under an UNHOVERED ancestor: the chain wins.
5320        let mut unhovered_parent = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
5321        unhovered_parent.set_hover_epoch(5);
5322        unhovered_parent.set_hovered(false);
5323        pod.paint_child(&mut unhovered_parent, &mut scene);
5324        assert_eq!(paint_seen.get(), Some(false));
5325
5326        // The epoch moves on (the pointer went elsewhere): the stamp is stranded
5327        // with nobody clearing it — the whole point of stamping rather than
5328        // flagging.
5329        let mut later = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
5330        later.set_hover_epoch(6);
5331        later.set_hovered(true);
5332        pod.paint_child(&mut later, &mut scene);
5333        assert_eq!(paint_seen.get(), Some(false));
5334
5335        // A pod holding the capture path refuses a claim outright, whatever the
5336        // pass says — a drag never paints hover under the pointer.
5337        pod.set_active(true);
5338        {
5339            let mut ctx = EventCtx::new(&mut state as &mut dyn Any, Point::ZERO, pod.size());
5340            ctx.set_hover_epoch(6);
5341            ctx.set_hover_eligible(true);
5342            pod.event_child(&mut ctx, &event);
5343            assert!(!ctx.is_hover_claimed());
5344        }
5345        assert_eq!(pod.hover_epoch(), 5, "the captured pod kept its old stamp");
5346    }
5347
5348    /// Two overlapping children can both hit-test a point (a Stack, or any
5349    /// container whose topmost child ignores the move). Only the first claim in
5350    /// **dispatch order** is recorded, which is the primitive; "the topmost
5351    /// claimant wins" is the consequence of dispatching topmost-first *and* of
5352    /// every container claiming after it routes (see `EventCtx::claim_hover`),
5353    /// not a rule this level enforces. Either way, no pass can leave two widgets
5354    /// hovered.
5355    #[test]
5356    fn only_the_first_hover_claim_in_a_pass_is_recorded() {
5357        struct Claimer;
5358        impl Widget for Claimer {
5359            fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
5360                bc.constrain(Size::new(10.0, 10.0))
5361            }
5362            fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
5363            fn event(&mut self, ctx: &mut EventCtx, _event: &InputEvent) -> EventResult {
5364                ctx.claim_hover();
5365                EventResult::Ignored
5366            }
5367        }
5368
5369        let mut first = ChildPod::new(Box::new(Claimer));
5370        let mut second = ChildPod::new(Box::new(Claimer));
5371        let mut state = ();
5372        let event = InputEvent::Pointer(PointerEvent {
5373            phase: PointerPhase::Move,
5374            position: Point::new(5.0, 5.0),
5375            button: PointerButton::Primary,
5376        });
5377        let mut ctx = EventCtx::new(&mut state as &mut dyn Any, Point::ZERO, Size::ZERO);
5378        ctx.set_hover_epoch(2);
5379        ctx.set_hover_eligible(true);
5380
5381        first.event_child(&mut ctx, &event);
5382        second.event_child(&mut ctx, &event);
5383        assert_eq!(first.hover_epoch(), 3, "the first claim is recorded");
5384        assert_eq!(
5385            second.hover_epoch(),
5386            0,
5387            "the pass was closed to further claims"
5388        );
5389    }
5390
5391    #[test]
5392    fn child_pod_contains_uses_container_space_bounds() {
5393        let mut pod = ChildPod::new(Box::new(FixedBox {
5394            intrinsic: Size::ZERO,
5395        }));
5396        pod.set_origin(Point::new(10.0, 10.0));
5397        let mut lctx = LayoutCtx::new();
5398        pod.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(20.0, 20.0)));
5399
5400        assert!(pod.contains(Point::new(10.0, 10.0))); // top-left inclusive
5401        assert!(pod.contains(Point::new(29.9, 29.9)));
5402        assert!(!pod.contains(Point::new(30.0, 30.0))); // bottom-right exclusive
5403        assert!(!pod.contains(Point::new(9.9, 15.0)));
5404    }
5405
5406    /// A scene recorder that overrides the shadow/layer `PaintScene` additions, to
5407    /// prove they reach an implementing scene that opts in.
5408    #[derive(Default)]
5409    struct ShadowLayerRecordingScene {
5410        shadows: Vec<(Point, Size, f64, f64, Color)>,
5411        layers: Vec<(Point, Size, f32)>,
5412        pops: u32,
5413    }
5414
5415    impl PaintScene for ShadowLayerRecordingScene {
5416        fn fill_rect(&mut self, _origin: Point, _size: Size, _color: Color) {}
5417        fn draw_text(&mut self, _origin: Point, _text: &str) {}
5418
5419        fn draw_shadow(
5420            &mut self,
5421            origin: Point,
5422            size: Size,
5423            radius: f64,
5424            std_dev: f64,
5425            color: Color,
5426        ) {
5427            self.shadows.push((origin, size, radius, std_dev, color));
5428        }
5429
5430        fn push_layer(&mut self, origin: Point, size: Size, alpha: f32) {
5431            self.layers.push((origin, size, alpha));
5432        }
5433
5434        fn pop_layer(&mut self) {
5435            self.pops += 1;
5436        }
5437    }
5438
5439    #[test]
5440    fn draw_shadow_and_push_layer_are_no_ops_when_not_overridden() {
5441        // `RecordingScene` (defined above) does not override the shadow/layer
5442        // additions — the trait's default no-op bodies must compile
5443        // unchanged and simply do nothing.
5444        let mut scene = RecordingScene::default();
5445        scene.draw_shadow(Point::ZERO, Size::new(10.0, 10.0), 4.0, 2.0, Color::BLACK);
5446        scene.push_layer(Point::ZERO, Size::new(10.0, 10.0), 0.5);
5447        scene.pop_layer();
5448        assert!(scene.rects.is_empty());
5449        assert!(scene.texts.is_empty());
5450    }
5451
5452    #[test]
5453    fn fill_path_and_stroke_path_are_no_ops_when_not_overridden() {
5454        // `RecordingScene` does not override the path additions
5455        // either — the trait's default no-op bodies must compile unchanged.
5456        let mut scene = RecordingScene::default();
5457        let mut path = BezPath::new();
5458        path.move_to((0.0, 0.0));
5459        path.line_to((1.0, 0.0));
5460        scene.fill_path(Point::ZERO, &path, &Brush::Solid(Color::BLACK));
5461        scene.stroke_path(Point::ZERO, &path, 2.0, &Brush::Solid(Color::BLACK));
5462        assert!(scene.rects.is_empty());
5463        assert!(scene.texts.is_empty());
5464    }
5465
5466    /// A scene recorder overriding the path additions, proving they
5467    /// reach an implementing scene that opts in.
5468    #[derive(Default)]
5469    struct PathRecordingScene {
5470        fills: Vec<(Point, BezPath)>,
5471        strokes: Vec<(Point, BezPath, f64)>,
5472    }
5473
5474    impl PaintScene for PathRecordingScene {
5475        fn fill_rect(&mut self, _origin: Point, _size: Size, _color: Color) {}
5476        fn draw_text(&mut self, _origin: Point, _text: &str) {}
5477
5478        fn fill_path(&mut self, origin: Point, path: &BezPath, _brush: &Brush) {
5479            self.fills.push((origin, path.clone()));
5480        }
5481
5482        fn stroke_path(&mut self, origin: Point, path: &BezPath, width: f64, _brush: &Brush) {
5483            self.strokes.push((origin, path.clone(), width));
5484        }
5485    }
5486
5487    #[test]
5488    fn fill_path_and_stroke_path_reach_an_overriding_implementor() {
5489        let mut scene = PathRecordingScene::default();
5490        let mut path = BezPath::new();
5491        path.move_to((0.0, 0.0));
5492        path.line_to((5.0, 0.0));
5493
5494        scene.fill_path(Point::new(1.0, 2.0), &path, &Brush::Solid(Color::BLACK));
5495        scene.stroke_path(
5496            Point::new(3.0, 4.0),
5497            &path,
5498            1.5,
5499            &Brush::Solid(Color::BLACK),
5500        );
5501
5502        assert_eq!(scene.fills.len(), 1);
5503        assert_eq!(scene.fills[0].0, Point::new(1.0, 2.0));
5504        assert_eq!(scene.fills[0].1, path);
5505
5506        assert_eq!(scene.strokes.len(), 1);
5507        assert_eq!(scene.strokes[0].0, Point::new(3.0, 4.0));
5508        assert_eq!(scene.strokes[0].1, path);
5509        assert_eq!(scene.strokes[0].2, 1.5);
5510    }
5511
5512    #[test]
5513    fn path_at_translates_path_points_by_origin() {
5514        let mut path = BezPath::new();
5515        path.move_to((0.0, 0.0));
5516        path.line_to((5.0, 0.0));
5517
5518        let translated = path_at(Point::new(10.0, 20.0), &path);
5519        let mut expected = BezPath::new();
5520        expected.move_to((10.0, 20.0));
5521        expected.line_to((15.0, 20.0));
5522        assert_eq!(translated, expected);
5523    }
5524
5525    /// A dummy theme type, standing in for `frust_theme::Theme` — proving the
5526    /// type-erased theme slot works for *any* `'static` type, not just the real
5527    /// theme (`frust-core` never names it).
5528    #[derive(Debug, PartialEq)]
5529    struct TestTheme {
5530        accent: u32,
5531    }
5532
5533    #[test]
5534    fn layout_ctx_theme_as_recovers_threaded_theme() {
5535        let theme = TestTheme { accent: 7 };
5536        let ctx = LayoutCtx::new().with_theme(&theme);
5537        assert_eq!(ctx.theme_as::<TestTheme>(), Some(&TestTheme { accent: 7 }));
5538    }
5539
5540    #[test]
5541    fn layout_ctx_theme_as_is_none_without_a_theme() {
5542        let ctx = LayoutCtx::new();
5543        assert!(ctx.theme_as::<TestTheme>().is_none());
5544    }
5545
5546    #[test]
5547    fn layout_ctx_theme_as_is_none_on_type_mismatch() {
5548        let theme = TestTheme { accent: 1 };
5549        let ctx = LayoutCtx::new().with_theme(&theme);
5550        // A downcast to the wrong type yields `None`, never a panic.
5551        assert!(ctx.theme_as::<u32>().is_none());
5552    }
5553
5554    #[test]
5555    fn paint_ctx_theme_as_recovers_threaded_theme() {
5556        let theme = TestTheme { accent: 9 };
5557        let mut ctx = PaintCtx::new(Point::ZERO, Size::new(1.0, 1.0));
5558        ctx.set_theme(Some(&theme));
5559        assert_eq!(ctx.theme_as::<TestTheme>(), Some(&TestTheme { accent: 9 }));
5560    }
5561
5562    #[test]
5563    fn paint_ctx_theme_as_is_none_without_a_theme() {
5564        let ctx = PaintCtx::new(Point::ZERO, Size::new(1.0, 1.0));
5565        assert!(ctx.theme_as::<TestTheme>().is_none());
5566    }
5567
5568    #[test]
5569    fn child_pod_paint_threads_theme_into_child() {
5570        use std::cell::Cell;
5571        use std::rc::Rc;
5572
5573        // A probe recording the accent it recovered from the paint context's
5574        // threaded theme (or `None` if no theme reached it).
5575        struct ThemeProbe(Rc<Cell<Option<u32>>>);
5576        impl Widget for ThemeProbe {
5577            fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
5578                bc.constrain(Size::new(10.0, 10.0))
5579            }
5580            fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
5581                self.0.set(ctx.theme_as::<TestTheme>().map(|t| t.accent));
5582            }
5583        }
5584
5585        let seen = Rc::new(Cell::new(None));
5586        let mut pod = ChildPod::new(Box::new(ThemeProbe(seen.clone())));
5587        let mut lctx = LayoutCtx::new();
5588        pod.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
5589        let mut scene = RecordingScene::default();
5590
5591        // A parent carrying a theme threads it into the child paint.
5592        let theme = TestTheme { accent: 42 };
5593        let mut parent = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
5594        parent.set_theme(Some(&theme));
5595        pod.paint_child(&mut parent, &mut scene);
5596        assert_eq!(seen.get(), Some(42));
5597
5598        // A parent with no theme leaves the child's accessor empty.
5599        let mut bare = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
5600        pod.paint_child(&mut bare, &mut scene);
5601        assert_eq!(seen.get(), None);
5602    }
5603
5604    /// A leaf widget whose visible appearance is driven purely by
5605    /// [`PaintCtx::frame_time`] — stands in for a caret-blink widget: it fills
5606    /// a rect only during the "on" half of a 1-second blink cycle, with no
5607    /// internal state of its own.
5608    struct BlinkBox;
5609
5610    impl Widget for BlinkBox {
5611        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
5612            bc.constrain(Size::new(4.0, 4.0))
5613        }
5614        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
5615            let millis = ctx.frame_time().as_nanos() / 1_000_000;
5616            if (millis / 500).is_multiple_of(2) {
5617                scene.fill_rect(ctx.origin(), ctx.size(), Color::BLACK);
5618            }
5619        }
5620    }
5621
5622    #[test]
5623    fn for_test_seeds_the_requested_frame_time() {
5624        let ctx = PaintCtx::for_test(Point::ZERO, Size::new(1.0, 1.0), FrameTime::from_nanos(42));
5625        assert_eq!(ctx.frame_time(), FrameTime::from_nanos(42));
5626    }
5627
5628    /// End-to-end proof the seam actually drives a clock-dependent widget:
5629    /// the same [`BlinkBox`] paints differently at two [`FrameTime`]s built
5630    /// via [`PaintCtx::for_test`] alone — no [`crate::app::RenderRoot`]
5631    /// involved, exactly how an app crate outside this workspace would use
5632    /// the seam.
5633    #[test]
5634    fn for_test_drives_a_clock_dependent_widget_to_two_appearances() {
5635        let mut widget = BlinkBox;
5636        let size = Size::new(4.0, 4.0);
5637
5638        let mut on_ctx = PaintCtx::for_test(Point::ZERO, size, FrameTime::from_nanos(0));
5639        let mut on_scene = RecordingScene::default();
5640        widget.paint(&mut on_ctx, &mut on_scene);
5641        assert_eq!(on_scene.rects.len(), 1, "expected the caret painted on");
5642
5643        let mut off_ctx = PaintCtx::for_test(Point::ZERO, size, FrameTime::from_nanos(500_000_000));
5644        let mut off_scene = RecordingScene::default();
5645        widget.paint(&mut off_ctx, &mut off_scene);
5646        assert!(
5647            off_scene.rects.is_empty(),
5648            "expected the caret painted off half a blink cycle later"
5649        );
5650    }
5651
5652    #[test]
5653    fn draw_shadow_and_push_layer_reach_an_overriding_implementor() {
5654        let mut scene = ShadowLayerRecordingScene::default();
5655        let origin = Point::new(3.0, 4.0);
5656        let size = Size::new(20.0, 12.0);
5657        scene.draw_shadow(origin, size, 6.0, 3.0, Color::BLACK);
5658        scene.push_layer(origin, size, 0.25);
5659        scene.pop_layer();
5660
5661        assert_eq!(scene.shadows, vec![(origin, size, 6.0, 3.0, Color::BLACK)]);
5662        assert_eq!(scene.layers, vec![(origin, size, 0.25)]);
5663        assert_eq!(scene.pops, 1);
5664    }
5665
5666    #[test]
5667    fn a_pods_type_name_resolves_through_every_box() {
5668        // A pod's widget is always erased, and the container plumbing stores it
5669        // double-boxed — the name must still be the widget's own, at any depth.
5670        let single = ChildPod::new(Box::new(Animator));
5671        assert!(
5672            single.type_name().ends_with("Animator"),
5673            "{}",
5674            single.type_name()
5675        );
5676
5677        let boxed: Box<dyn Widget> = Box::new(Animator);
5678        let double = ChildPod::new(Box::new(boxed));
5679        assert!(
5680            double.type_name().ends_with("Animator"),
5681            "{}",
5682            double.type_name()
5683        );
5684    }
5685
5686    #[test]
5687    fn a_pods_tooling_id_is_assigned_once_and_never_shared() {
5688        let a = ChildPod::new(Box::new(Animator));
5689        let b = ChildPod::new(Box::new(Animator));
5690        let first = a.inspect_id();
5691        assert_eq!(a.inspect_id(), first, "a pod keeps the id it was given");
5692        assert_ne!(b.inspect_id(), first, "two pods never share an id");
5693        assert!(
5694            first.0 >= ChildPod::INSPECT_ID_BASE,
5695            "pod ids stay in the range reserved against arena WidgetIds"
5696        );
5697    }
5698
5699    #[test]
5700    fn a_leaf_visits_no_children_and_a_container_visits_all_of_its_own() {
5701        struct Two {
5702            children: Vec<ChildPod>,
5703        }
5704        impl Widget for Two {
5705            fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
5706                bc.max()
5707            }
5708            fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
5709            fn visit_children(&self, visitor: &mut dyn FnMut(&ChildPod)) {
5710                for child in &self.children {
5711                    visitor(child);
5712                }
5713            }
5714        }
5715
5716        // The trait default: a widget that says nothing publishes nothing.
5717        let mut seen = 0;
5718        Animator.visit_children(&mut |_| seen += 1);
5719        assert_eq!(seen, 0);
5720
5721        let container = Two {
5722            children: vec![
5723                ChildPod::new(Box::new(Animator)),
5724                ChildPod::new(Box::new(Animator)),
5725            ],
5726        };
5727        let mut ids = Vec::new();
5728        container.visit_children(&mut |child| ids.push(child.inspect_id()));
5729        assert_eq!(ids.len(), 2);
5730        assert_ne!(ids[0], ids[1]);
5731    }
5732
5733    /// A recorder that implements only the *uniform* rounded methods and the
5734    /// solid stroke — i.e. every pre-per-corner recorder scene in the wild —
5735    /// so the per-corner/dashed trait defaults can be observed falling back.
5736    #[derive(Default)]
5737    struct UniformOnlyScene {
5738        fills: Vec<(Point, Size, f64)>,
5739        clips: Vec<(Point, Size, f64)>,
5740        strokes: Vec<(Point, f64)>,
5741        pops: usize,
5742    }
5743
5744    impl PaintScene for UniformOnlyScene {
5745        fn fill_rect(&mut self, _origin: Point, _size: Size, _color: Color) {}
5746        fn draw_text(&mut self, _origin: Point, _text: &str) {}
5747        fn fill_rounded_rect(&mut self, origin: Point, size: Size, radius: f64, _color: Color) {
5748            self.fills.push((origin, size, radius));
5749        }
5750        fn push_clip_rounded(&mut self, origin: Point, size: Size, radius: f64) {
5751            self.clips.push((origin, size, radius));
5752        }
5753        fn pop_clip(&mut self) {
5754            self.pops += 1;
5755        }
5756        fn stroke_path(&mut self, origin: Point, _path: &BezPath, width: f64, _brush: &Brush) {
5757            self.strokes.push((origin, width));
5758        }
5759    }
5760
5761    fn corner_probe_path() -> BezPath {
5762        let mut path = BezPath::new();
5763        path.move_to((0.0, 0.0));
5764        path.line_to((10.0, 0.0));
5765        path
5766    }
5767
5768    #[test]
5769    fn per_corner_paint_defaults_fall_back_to_the_uniform_methods() {
5770        // A scene that predates per-corner radii still paints something, at the
5771        // largest corner — and its clip stack stays balanced, which is why the
5772        // push delegates rather than no-op'ing against a real `pop_clip`.
5773        let mut scene = UniformOnlyScene::default();
5774        let origin = Point::new(2.0, 3.0);
5775        let size = Size::new(20.0, 10.0);
5776        let radii = CornerRadii::new(4.0, 12.0, 0.0, 1.0);
5777
5778        scene.fill_rounded_rect_radii(origin, size, radii, Color::BLACK);
5779        scene.push_clip_rounded_radii(origin, size, radii);
5780        scene.pop_clip();
5781
5782        assert_eq!(scene.fills, vec![(origin, size, 12.0)]);
5783        assert_eq!(scene.clips, vec![(origin, size, 12.0)]);
5784        assert_eq!(scene.pops, 1);
5785    }
5786
5787    /// A recorder that predates path clips: it knows only the rectangular
5788    /// push and the shared pop.
5789    #[derive(Default)]
5790    struct RectClipOnlyScene {
5791        clips: Vec<(Point, Size)>,
5792        pops: usize,
5793    }
5794
5795    impl PaintScene for RectClipOnlyScene {
5796        fn fill_rect(&mut self, _origin: Point, _size: Size, _color: Color) {}
5797        fn draw_text(&mut self, _origin: Point, _text: &str) {}
5798        fn push_clip(&mut self, origin: Point, size: Size) {
5799            self.clips.push((origin, size));
5800        }
5801        fn pop_clip(&mut self) {
5802            self.pops += 1;
5803        }
5804    }
5805
5806    fn triangle_probe_path() -> BezPath {
5807        let mut path = BezPath::new();
5808        path.move_to((4.0, 2.0));
5809        path.line_to((14.0, 2.0));
5810        path.line_to((9.0, 12.0));
5811        path.close_path();
5812        path
5813    }
5814
5815    #[test]
5816    fn path_clip_default_pushes_the_translated_bounding_box() {
5817        // A scene with no path-clip concept still clips — to the path's
5818        // bounding box, offset by `origin` — and its clip stack stays balanced
5819        // against the shared `pop_clip`.
5820        let mut scene = RectClipOnlyScene::default();
5821        scene.push_clip_path(Point::new(10.0, 20.0), &triangle_probe_path());
5822        scene.pop_clip();
5823
5824        assert_eq!(
5825            scene.clips,
5826            vec![(Point::new(14.0, 22.0), Size::new(10.0, 10.0))]
5827        );
5828        assert_eq!(scene.pops, 1);
5829    }
5830
5831    #[test]
5832    fn scene_builder_records_a_path_clip_translated_by_origin() {
5833        let mut scene = frust_scene::Scene::new();
5834        {
5835            let mut builder = frust_scene::SceneBuilder::new(&mut scene);
5836            let paint: &mut dyn PaintScene = &mut builder;
5837            paint.push_clip_path(Point::new(10.0, 20.0), &triangle_probe_path());
5838            paint.pop_clip();
5839        }
5840
5841        match &scene.commands()[0] {
5842            frust_scene::Command::PushClipPath { path, transform } => {
5843                let expected = Affine::translate((10.0, 20.0)) * triangle_probe_path();
5844                assert_eq!(*path, expected);
5845                assert_eq!(*transform, Affine::IDENTITY);
5846            }
5847            other => panic!("expected PushClipPath, got {other:?}"),
5848        }
5849        assert!(matches!(scene.commands()[1], frust_scene::Command::PopClip));
5850    }
5851
5852    #[test]
5853    fn dashed_stroke_default_falls_back_to_the_solid_stroke() {
5854        let mut scene = UniformOnlyScene::default();
5855        let origin = Point::new(5.0, 6.0);
5856        scene.stroke_path_dashed(
5857            origin,
5858            &corner_probe_path(),
5859            2.0,
5860            DashPattern::new(4.0, 2.0),
5861            &Brush::Solid(Color::BLACK),
5862        );
5863
5864        assert_eq!(scene.strokes, vec![(origin, 2.0)]);
5865    }
5866
5867    #[test]
5868    fn scene_builder_records_per_corner_radii_translated_by_origin() {
5869        // The `SceneBuilder` impl overrides the defaults above: every corner
5870        // reaches the command intact, under the same origin→rect convention the
5871        // uniform methods use.
5872        let mut scene = frust_scene::Scene::new();
5873        let radii = CornerRadii::new(4.0, 12.0, 0.0, 1.0);
5874        {
5875            let mut builder = frust_scene::SceneBuilder::new(&mut scene);
5876            let paint: &mut dyn PaintScene = &mut builder;
5877            paint.fill_rounded_rect_radii(
5878                Point::new(2.0, 3.0),
5879                Size::new(20.0, 10.0),
5880                radii,
5881                Color::BLACK,
5882            );
5883            paint.push_clip_rounded_radii(Point::new(0.0, 0.0), Size::new(5.0, 5.0), radii);
5884            paint.pop_clip();
5885        }
5886
5887        match &scene.commands()[0] {
5888            frust_scene::Command::RoundedRect {
5889                rect, radii: got, ..
5890            } => {
5891                assert_eq!(*rect, Rect::new(2.0, 3.0, 22.0, 13.0));
5892                assert_eq!(*got, radii);
5893            }
5894            other => panic!("expected RoundedRect, got {other:?}"),
5895        }
5896        match &scene.commands()[1] {
5897            frust_scene::Command::PushClipRounded { radii: got, .. } => assert_eq!(*got, radii),
5898            other => panic!("expected PushClipRounded, got {other:?}"),
5899        }
5900        assert!(matches!(scene.commands()[2], frust_scene::Command::PopClip));
5901    }
5902
5903    #[test]
5904    fn scene_builder_records_a_dashed_stroke_translated_by_origin() {
5905        let mut scene = frust_scene::Scene::new();
5906        let dash = DashPattern::new(4.0, 2.0).with_phase(1.0);
5907        {
5908            let mut builder = frust_scene::SceneBuilder::new(&mut scene);
5909            let paint: &mut dyn PaintScene = &mut builder;
5910            paint.stroke_path_dashed(
5911                Point::new(5.0, 0.0),
5912                &corner_probe_path(),
5913                2.0,
5914                dash,
5915                &Brush::Solid(Color::BLACK),
5916            );
5917        }
5918
5919        match &scene.commands()[0] {
5920            frust_scene::Command::Path { path, style, .. } => {
5921                assert_eq!(
5922                    *style,
5923                    frust_scene::PathStyle::Stroke {
5924                        width: 2.0,
5925                        dash: Some(dash)
5926                    }
5927                );
5928                // Same origin translation `stroke_path` applies.
5929                assert_eq!(path.elements().len(), 2);
5930                assert!(matches!(
5931                    path.elements()[0],
5932                    kurbo::PathEl::MoveTo(p) if p == Point::new(5.0, 0.0)
5933                ));
5934            }
5935            other => panic!("expected Path, got {other:?}"),
5936        }
5937    }
5938
5939    #[test]
5940    fn uniform_paint_calls_still_encode_the_commands_they_always_did() {
5941        // The additive contract: the uniform methods every existing call site
5942        // uses keep producing the same commands, now spelled as four equal
5943        // corners / an undashed stroke.
5944        let mut scene = frust_scene::Scene::new();
5945        {
5946            let mut builder = frust_scene::SceneBuilder::new(&mut scene);
5947            let paint: &mut dyn PaintScene = &mut builder;
5948            paint.fill_rounded_rect(
5949                Point::new(0.0, 0.0),
5950                Size::new(10.0, 10.0),
5951                4.0,
5952                Color::BLACK,
5953            );
5954            paint.push_clip_rounded(Point::new(0.0, 0.0), Size::new(10.0, 10.0), 4.0);
5955            paint.pop_clip();
5956            paint.draw_shadow(
5957                Point::new(0.0, 0.0),
5958                Size::new(10.0, 10.0),
5959                4.0,
5960                2.0,
5961                Color::BLACK,
5962            );
5963            paint.stroke_path(
5964                Point::new(0.0, 0.0),
5965                &corner_probe_path(),
5966                1.0,
5967                &Brush::Solid(Color::BLACK),
5968            );
5969        }
5970
5971        let uniform = CornerRadii::uniform(4.0);
5972        match &scene.commands()[0] {
5973            frust_scene::Command::RoundedRect { radii, .. } => assert_eq!(*radii, uniform),
5974            other => panic!("expected RoundedRect, got {other:?}"),
5975        }
5976        match &scene.commands()[1] {
5977            frust_scene::Command::PushClipRounded { radii, .. } => assert_eq!(*radii, uniform),
5978            other => panic!("expected PushClipRounded, got {other:?}"),
5979        }
5980        match &scene.commands()[3] {
5981            frust_scene::Command::BlurredRoundedRect { radii, .. } => assert_eq!(*radii, uniform),
5982            other => panic!("expected BlurredRoundedRect, got {other:?}"),
5983        }
5984        match &scene.commands()[4] {
5985            frust_scene::Command::Path { style, .. } => assert_eq!(
5986                *style,
5987                frust_scene::PathStyle::Stroke {
5988                    width: 1.0,
5989                    dash: None
5990                }
5991            ),
5992            other => panic!("expected Path, got {other:?}"),
5993        }
5994    }
5995
5996    #[test]
5997    fn every_paint_scene_method_terminates_on_discard_scene() {
5998        // Every `PaintScene` method — including the delegating defaults
5999        // (per-corner/dashed variants, the additive image/shader/shadow
6000        // surface) — is reachable and terminates on `DiscardScene` without
6001        // panicking; a future `todo!()` or an infinitely-recursive default
6002        // would fail here. The records-nothing guarantee itself is asserted
6003        // where a scene actually exists, at the navigator/switcher call
6004        // sites, not here.
6005        use frust_scene::{FontHandle, Glyph};
6006
6007        let mut scene = DiscardScene;
6008        let origin = Point::new(2.0, 3.0);
6009        let size = Size::new(20.0, 10.0);
6010        let color = Color::BLACK;
6011        let brush = Brush::Solid(color);
6012        let radii = CornerRadii::new(4.0, 12.0, 0.0, 1.0);
6013        let path = corner_probe_path();
6014
6015        scene.fill_rect(origin, size, color);
6016        scene.fill_rounded_rect(origin, size, 4.0, color);
6017        scene.fill_rounded_rect_radii(origin, size, radii, color);
6018        scene.stroke_line(origin, Point::new(10.0, 10.0), 1.0, color);
6019        scene.push_clip(origin, size);
6020        scene.push_clip_rounded(origin, size, 4.0);
6021        scene.push_clip_rounded_radii(origin, size, radii);
6022        scene.pop_clip();
6023        scene.draw_text(origin, "discarded");
6024
6025        let font = FontHandle::new(peniko::FontData::new(
6026            peniko::Blob::from(Vec::<u8>::new()),
6027            0,
6028        ));
6029        let run = GlyphRun {
6030            font,
6031            font_size: 16.0,
6032            brush: brush.clone(),
6033            transform: Affine::IDENTITY,
6034            glyphs: vec![Glyph {
6035                id: 1,
6036                x: 0.0,
6037                y: 0.0,
6038            }],
6039        };
6040        scene.draw_glyph_run(run);
6041
6042        let image = peniko::ImageData {
6043            data: peniko::Blob::from(vec![0u8; 2 * 2 * 4]),
6044            format: peniko::ImageFormat::Rgba8,
6045            alpha_type: peniko::ImageAlphaType::Alpha,
6046            width: 2,
6047            height: 2,
6048        };
6049        scene.draw_image(&image, Rect::new(0.0, 0.0, 20.0, 20.0));
6050
6051        let program = ShaderProgram::new("fn main() {}");
6052        scene.draw_shader(&program, Rect::new(0.0, 0.0, 10.0, 10.0), 1.5);
6053
6054        scene.draw_shadow(origin, size, 4.0, 2.0, color);
6055        scene.fill_rect_brush(origin, size, &brush);
6056        scene.fill_rounded_rect_brush(origin, size, 4.0, &brush);
6057        scene.push_layer(origin, size, 0.5);
6058        scene.pop_layer();
6059        scene.clear_rect(origin, size);
6060        scene.fill_path(origin, &path, &brush);
6061        scene.stroke_path(origin, &path, 1.0, &brush);
6062        scene.stroke_path_dashed(origin, &path, 1.0, DashPattern::new(4.0, 2.0), &brush);
6063        scene.push_transform(Affine::translate((1.0, 2.0)));
6064        scene.pop_transform();
6065    }
6066
6067    /// A minimal recorder that tracks only transform and layer stack operations
6068    /// to verify the default [`PaintScene::push_retained`]/[`PaintScene::pop_retained`]
6069    /// implementation emulates the presentation correctly.
6070    #[derive(Default)]
6071    struct MinimalRetainedRecorder {
6072        operations: Vec<String>,
6073    }
6074
6075    impl PaintScene for MinimalRetainedRecorder {
6076        fn fill_rect(&mut self, _origin: Point, _size: Size, _color: Color) {}
6077        fn draw_text(&mut self, _origin: Point, _text: &str) {}
6078        fn push_transform(&mut self, _transform: Affine) {
6079            self.operations.push("push_transform".to_string());
6080        }
6081        fn pop_transform(&mut self) {
6082            self.operations.push("pop_transform".to_string());
6083        }
6084        fn push_layer(&mut self, _origin: Point, _size: Size, _alpha: f32) {
6085            self.operations.push("push_layer".to_string());
6086        }
6087        fn pop_layer(&mut self) {
6088            self.operations.push("pop_layer".to_string());
6089        }
6090    }
6091
6092    #[test]
6093    fn default_push_retained_pop_retained_emulates_presentation_for_recorders() {
6094        // The default `push_retained` and `pop_retained` implementation
6095        // ensures recorders that override only the transform/layer methods
6096        // see a consistent sequence: push_transform, push_layer, paint body,
6097        // pop_layer, pop_transform — the reverse order matching the push ops.
6098        let mut recorder = MinimalRetainedRecorder::default();
6099        let origin = Point::new(10.0, 20.0);
6100        let size = Size::new(100.0, 50.0);
6101        let alpha = 0.8;
6102        let scale = 0.9;
6103
6104        // Emulate a body that doesn't call any paint methods (just has side effects).
6105        recorder.push_retained(1, origin, size, alpha, scale);
6106        // Body paint code would go here
6107        recorder.pop_retained();
6108
6109        // Verify the sequence: transform, layer, pop_layer, pop_transform.
6110        assert_eq!(
6111            recorder.operations,
6112            vec![
6113                "push_transform".to_string(),
6114                "push_layer".to_string(),
6115                "pop_layer".to_string(),
6116                "pop_transform".to_string(),
6117            ]
6118        );
6119    }
6120
6121    #[test]
6122    fn scene_builder_records_push_pop_retained_commands_directly_without_extra_transforms() {
6123        // The `SceneBuilder` impl overrides `push_retained`/`pop_retained` to
6124        // record the retained commands directly, without the emulating
6125        // transform/layer pairs. It records Command::PushRetained with the
6126        // key, rect (converted from origin/size), alpha, scale, and the current
6127        // transform, and Command::PopRetained with no extra layer/transform commands.
6128        let mut scene = frust_scene::Scene::new();
6129        {
6130            let mut builder = frust_scene::SceneBuilder::new(&mut scene);
6131            let paint: &mut dyn PaintScene = &mut builder;
6132
6133            paint.push_retained(7, Point::new(5.0, 10.0), Size::new(80.0, 40.0), 0.5, 1.2);
6134            // Paint some content inside the retained bracket.
6135            paint.fill_rect(Point::new(10.0, 15.0), Size::new(20.0, 25.0), Color::BLACK);
6136            paint.pop_retained();
6137        }
6138
6139        // Verify the command sequence: PushRetained, FillRect, PopRetained.
6140        // No PushLayer, PopLayer, PushTransform, or PopTransform commands.
6141        let commands = scene.commands();
6142        assert_eq!(commands.len(), 3);
6143
6144        match &commands[0] {
6145            frust_scene::Command::PushRetained {
6146                key,
6147                rect,
6148                alpha,
6149                scale,
6150                ..
6151            } => {
6152                assert_eq!(*key, 7);
6153                assert_eq!(*rect, Rect::new(5.0, 10.0, 85.0, 50.0));
6154                assert_eq!(*alpha, 0.5);
6155                assert_eq!(*scale, 1.2);
6156            }
6157            other => panic!("expected PushRetained, got {other:?}"),
6158        }
6159
6160        match &commands[1] {
6161            frust_scene::Command::FillRect { rect, .. } => {
6162                assert_eq!(*rect, Rect::new(10.0, 15.0, 30.0, 40.0));
6163            }
6164            other => panic!("expected FillRect, got {other:?}"),
6165        }
6166
6167        assert!(
6168            matches!(commands[2], frust_scene::Command::PopRetained),
6169            "expected PopRetained, got {:?}",
6170            commands[2]
6171        );
6172    }
6173
6174    #[test]
6175    fn current_transform_is_the_scene_builders_own_and_none_for_a_recorder() {
6176        assert_eq!(
6177            MinimalRetainedRecorder::default().current_transform(),
6178            None,
6179            "a recorder tracks no transform"
6180        );
6181        let mut scene = frust_scene::Scene::new();
6182        let mut builder = frust_scene::SceneBuilder::new(&mut scene);
6183        let paint: &mut dyn PaintScene = &mut builder;
6184        assert_eq!(paint.current_transform(), Some(Affine::IDENTITY));
6185        paint.push_transform(Affine::scale(2.0));
6186        paint.push_transform(Affine::translate((3.0, 4.0)));
6187        assert_eq!(
6188            paint.current_transform(),
6189            Some(Affine::scale(2.0) * Affine::translate((3.0, 4.0))),
6190            "every open transform, composed"
6191        );
6192    }
6193
6194    #[test]
6195    fn backdrop_blur_is_a_no_op_by_default_and_recorded_by_the_scene_builder() {
6196        // The default body: a recorder scene that never heard of a backdrop
6197        // takes the call and records nothing.
6198        let mut recorder = MinimalRetainedRecorder::default();
6199        recorder.backdrop_blur(Point::new(1.0, 2.0), Size::new(3.0, 4.0), 5.0);
6200
6201        let mut scene = frust_scene::Scene::new();
6202        {
6203            let mut builder = frust_scene::SceneBuilder::new(&mut scene);
6204            let paint: &mut dyn PaintScene = &mut builder;
6205            paint.push_transform(Affine::translate((10.0, 20.0)));
6206            paint.backdrop_blur(Point::new(1.0, 2.0), Size::new(3.0, 4.0), 5.0);
6207        }
6208        match scene.commands() {
6209            [
6210                frust_scene::Command::BackdropBlur {
6211                    rect,
6212                    radius,
6213                    transform,
6214                },
6215            ] => {
6216                assert_eq!(*rect, Rect::new(1.0, 2.0, 4.0, 6.0));
6217                assert_eq!(*radius, 5.0);
6218                assert_eq!(*transform, Affine::translate((10.0, 20.0)));
6219            }
6220            other => panic!("expected one BackdropBlur, got {other:?}"),
6221        }
6222    }
6223
6224    #[test]
6225    fn retained_enabled_needs_the_switch_and_a_threaded_ledger() {
6226        let ledger = crate::retained::RetainedLedger::empty();
6227        assert!(
6228            !PaintCtx::new(Point::ZERO, Size::ZERO).retained_enabled(),
6229            "no ledger threaded"
6230        );
6231        let mut ctx = PaintCtx::new(Point::ZERO, Size::ZERO).with_retained_ledger(&ledger);
6232        assert!(
6233            ctx.retained_enabled(),
6234            "an empty ledger still enables brackets"
6235        );
6236        ctx.set_paint_state(0, false, Some(&ledger), 0);
6237        assert!(!ctx.retained_enabled(), "the kill switch disables them");
6238    }
6239
6240    /// Publishes on every paint channel a boundary must not skip.
6241    struct Publisher;
6242
6243    impl Widget for Publisher {
6244        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
6245            bc.max()
6246        }
6247        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
6248            ctx.report_input_shield(Rect::new(0.0, 0.0, 1.0, 1.0));
6249            ctx.report_hero("tag", Rect::new(0.0, 0.0, 1.0, 1.0));
6250        }
6251    }
6252
6253    #[test]
6254    fn paint_publications_sum_up_through_paint_child_and_hero_registries() {
6255        let mut pod = ChildPod::new(Box::new(Publisher));
6256        pod.layout_child(
6257            &mut LayoutCtx::new(),
6258            &BoxConstraints::tight(Size::new(5.0, 5.0)),
6259        );
6260        let mut quiet = ChildPod::new(Box::new(FixedBox {
6261            intrinsic: Size::new(5.0, 5.0),
6262        }));
6263        quiet.layout_child(
6264            &mut LayoutCtx::new(),
6265            &BoxConstraints::tight(Size::new(5.0, 5.0)),
6266        );
6267
6268        let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
6269        quiet.paint_child(&mut ctx, &mut DiscardScene);
6270        assert_eq!(
6271            ctx.paint_publications(),
6272            0,
6273            "a plain body publishes nothing"
6274        );
6275        pod.paint_child(&mut ctx, &mut DiscardScene);
6276        assert_eq!(ctx.paint_publications(), 2, "a shield and a hero report");
6277
6278        let registry = RefCell::new(HeroFrames::new(Point::ZERO, HashMap::new()));
6279        ctx.with_hero_registry(&registry, |inner| {
6280            pod.paint_child(inner, &mut DiscardScene);
6281        });
6282        assert_eq!(
6283            ctx.paint_publications(),
6284            4,
6285            "a hero registry's subtree counts too"
6286        );
6287    }
6288}
6289
6290#[cfg(test)]
6291mod transform_tests {
6292    use std::f64::consts::FRAC_PI_4;
6293
6294    use kurbo::Vec2;
6295
6296    use super::*;
6297    use crate::event::{
6298        PointerButton, PointerEvent, PointerId, PointerPhase, ScaleEvent, ScalePhase, ScrollDelta,
6299    };
6300
6301    /// One recorded paint operation, in order.
6302    #[derive(Clone, Debug, PartialEq)]
6303    enum Op {
6304        Rect(Point, Size),
6305        Push(Affine),
6306        Pop,
6307    }
6308
6309    /// A scene recorder that keeps the transform stack interleaved with draws,
6310    /// so a test can see exactly what a pod pushed around its child.
6311    #[derive(Default)]
6312    struct OpLog(Vec<Op>);
6313
6314    impl PaintScene for OpLog {
6315        fn fill_rect(&mut self, origin: Point, size: Size, _color: Color) {
6316            self.0.push(Op::Rect(origin, size));
6317        }
6318        fn draw_text(&mut self, _origin: Point, _text: &str) {}
6319        fn push_transform(&mut self, transform: Affine) {
6320            self.0.push(Op::Push(transform));
6321        }
6322        fn pop_transform(&mut self) {
6323            self.0.push(Op::Pop);
6324        }
6325    }
6326
6327    /// A leaf that fills its whole box, records every event it receives (in
6328    /// local space), captures on `Down`, and contributes one semantics node.
6329    #[derive(Default)]
6330    struct Recorder {
6331        events: Vec<InputEvent>,
6332    }
6333
6334    impl Widget for Recorder {
6335        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
6336            bc.max()
6337        }
6338        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
6339            scene.fill_rect(ctx.origin(), ctx.size(), Color::BLACK);
6340        }
6341        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
6342            self.events.push(event.clone());
6343            if matches!(
6344                event,
6345                InputEvent::Pointer(PointerEvent {
6346                    phase: PointerPhase::Down,
6347                    ..
6348                })
6349            ) {
6350                ctx.capture_pointer();
6351            }
6352            EventResult::Handled
6353        }
6354        fn semantics(&self, ctx: &mut SemanticsCtx) {
6355            ctx.push_node(accesskit::Role::Label, |_| {});
6356        }
6357    }
6358
6359    /// A leaf that records the visible rect its paint context carries.
6360    struct VisibleProbe(std::rc::Rc<Cell<Option<Rect>>>);
6361
6362    impl Widget for VisibleProbe {
6363        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
6364            bc.max()
6365        }
6366        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
6367            self.0.set(ctx.visible_rect());
6368        }
6369    }
6370
6371    fn pointer(phase: PointerPhase, x: f64, y: f64) -> InputEvent {
6372        InputEvent::Pointer(PointerEvent {
6373            phase,
6374            position: Point::new(x, y),
6375            button: PointerButton::Primary,
6376        })
6377    }
6378
6379    /// A laid-out `Recorder` pod of `size` at `origin`.
6380    fn recorder_pod(origin: Point, size: Size) -> ChildPod {
6381        let mut pod = ChildPod::new(Box::new(Recorder::default()));
6382        pod.layout_child(&mut LayoutCtx::new(), &BoxConstraints::tight(size));
6383        pod.set_origin(origin);
6384        pod
6385    }
6386
6387    fn recorded(pod: &mut ChildPod) -> Vec<InputEvent> {
6388        pod.widget_mut()
6389            .downcast_mut::<Recorder>()
6390            .expect("recorder pod")
6391            .events
6392            .clone()
6393    }
6394
6395    fn paint(pod: &mut ChildPod, visible: Option<Rect>) -> (Vec<Op>, Option<Rect>) {
6396        let mut ctx = PaintCtx::new(Point::ZERO, Size::new(400.0, 400.0));
6397        ctx.set_visible_rect(visible);
6398        let mut scene = OpLog::default();
6399        pod.paint_child(&mut ctx, &mut scene);
6400        (scene.0, ctx.visible_rect_ref())
6401    }
6402
6403    fn dispatch(pod: &mut ChildPod, event: &InputEvent) -> EventResult {
6404        let mut state = 0u32;
6405        let mut ctx = EventCtx::new(&mut state, Point::ZERO, Size::new(400.0, 400.0));
6406        pod.event_child(&mut ctx, event)
6407    }
6408
6409    #[test]
6410    fn a_child_under_scale_and_translate_receives_down_at_its_local_point() {
6411        let mut pod = recorder_pod(Point::new(10.0, 20.0), Size::new(50.0, 50.0));
6412        // Local p lands at origin + translate(5, 5) * scale(2) * p.
6413        pod.set_transform(Some(
6414            Affine::translate(Vec2::new(5.0, 5.0)) * Affine::scale(2.0),
6415        ));
6416        // Local (7, 9) is drawn at (10 + 5 + 14, 20 + 5 + 18) = (29, 43).
6417        let at = Point::new(29.0, 43.0);
6418        assert!(pod.contains(at));
6419        assert_eq!(
6420            dispatch(&mut pod, &pointer(PointerPhase::Down, at.x, at.y)),
6421            EventResult::Handled
6422        );
6423        assert!(pod.is_active(), "capture bookkeeping is unchanged");
6424        // Scroll positions and scale focal points take the same mapping; their
6425        // magnitudes do not.
6426        dispatch(
6427            &mut pod,
6428            &InputEvent::Scroll {
6429                position: at,
6430                delta: ScrollDelta::Pixels(0.0, 12.0),
6431            },
6432        );
6433        dispatch(
6434            &mut pod,
6435            &InputEvent::Scale(ScaleEvent {
6436                phase: ScalePhase::Update,
6437                scale_delta: 1.1,
6438                focal: at,
6439                velocity: 0.0,
6440            }),
6441        );
6442        let local = Point::new(7.0, 9.0);
6443        assert_eq!(
6444            recorded(&mut pod),
6445            vec![
6446                pointer(PointerPhase::Down, local.x, local.y),
6447                InputEvent::Scroll {
6448                    position: local,
6449                    delta: ScrollDelta::Pixels(0.0, 12.0),
6450                },
6451                InputEvent::Scale(ScaleEvent {
6452                    phase: ScalePhase::Update,
6453                    scale_delta: 1.1,
6454                    focal: local,
6455                    velocity: 0.0,
6456                }),
6457            ]
6458        );
6459        // The child now covers (15..115, 25..125): the untransformed box's
6460        // corner misses, and a point past its untransformed extent hits.
6461        assert!(!pod.contains(Point::new(12.0, 22.0)));
6462        assert!(pod.contains(Point::new(100.0, 100.0)));
6463        assert!(!pod.contains(Point::new(116.0, 60.0)));
6464        // A contact's inner event is mapped the same way.
6465        let contact = InputEvent::PointerContact {
6466            pointer_id: PointerId::touch(1),
6467            event: PointerEvent {
6468                phase: PointerPhase::Move,
6469                position: at,
6470                button: PointerButton::Primary,
6471            },
6472        };
6473        dispatch(&mut pod, &contact);
6474        assert_eq!(
6475            recorded(&mut pod).last(),
6476            Some(&InputEvent::PointerContact {
6477                pointer_id: PointerId::touch(1),
6478                event: PointerEvent {
6479                    phase: PointerPhase::Move,
6480                    position: local,
6481                    button: PointerButton::Primary,
6482                },
6483            })
6484        );
6485    }
6486
6487    #[test]
6488    fn contains_agrees_with_paint_for_rotated_content_at_the_corners() {
6489        let size = Size::new(100.0, 100.0);
6490        let mut pod = recorder_pod(Point::new(40.0, 40.0), size);
6491        pod.set_transform(Some(Affine::rotate_about(
6492            FRAC_PI_4,
6493            Point::new(50.0, 50.0),
6494        )));
6495        let (ops, _) = paint(&mut pod, None);
6496        let [Op::Push(drawn), Op::Rect(rect_origin, rect_size), Op::Pop] = ops.as_slice() else {
6497            panic!("expected push/rect/pop, got {ops:?}");
6498        };
6499        assert_eq!((*rect_origin, *rect_size), (Point::new(40.0, 40.0), size));
6500        let rect = Rect::from_origin_size(*rect_origin, *rect_size);
6501        let center = *drawn * rect.center();
6502        for corner in [
6503            Point::new(rect.x0, rect.y0),
6504            Point::new(rect.x1, rect.y0),
6505            Point::new(rect.x1, rect.y1),
6506            Point::new(rect.x0, rect.y1),
6507        ] {
6508            // Where paint actually put this corner (the parent paints at the
6509            // absolute origin zero, so absolute space is the container's space).
6510            let painted = *drawn * corner;
6511            let inward = (center - painted).normalize();
6512            assert!(
6513                pod.contains(painted + inward),
6514                "just inside painted corner {painted:?}"
6515            );
6516            assert!(
6517                !pod.contains(painted - inward),
6518                "just outside painted corner {painted:?}"
6519            );
6520            // The untransformed box's own corner is outside the painted
6521            // diamond: the hit test is not an AABB test.
6522            let unrotated = corner + (rect.center() - corner) * 0.02;
6523            assert!(!pod.contains(unrotated), "AABB corner {unrotated:?}");
6524        }
6525    }
6526
6527    #[test]
6528    fn an_untransformed_pod_paints_and_routes_exactly_as_before() {
6529        let origin = Point::new(12.0, 34.0);
6530        let size = Size::new(60.0, 40.0);
6531        let events = [
6532            pointer(PointerPhase::Down, 20.0, 40.0),
6533            pointer(PointerPhase::Move, 90.0, 10.0),
6534            pointer(PointerPhase::Up, 30.0, 50.0),
6535            InputEvent::Scroll {
6536                position: Point::new(25.0, 45.0),
6537                delta: ScrollDelta::Lines(0.0, 1.0),
6538            },
6539            InputEvent::Scale(ScaleEvent {
6540                phase: ScalePhase::Begin,
6541                scale_delta: 1.0,
6542                focal: Point::new(15.0, 35.0),
6543                velocity: 0.0,
6544            }),
6545            InputEvent::Housekeeping,
6546        ];
6547        let visible = Some(Rect::new(0.0, 0.0, 50.0, 50.0));
6548
6549        // Baseline: a pod that never had a transform.
6550        let mut baseline = recorder_pod(origin, size);
6551        let baseline_paint = paint(&mut baseline, visible);
6552        for event in &events {
6553            dispatch(&mut baseline, event);
6554        }
6555        let baseline_events = recorded(&mut baseline);
6556        // The baseline is the pre-transform contract: no transform pushed, the
6557        // child drawn at its origin, the visible rect threaded unchanged, and
6558        // every event translated by `-origin` and nothing else.
6559        assert_eq!(baseline_paint.0, vec![Op::Rect(origin, size)]);
6560        assert_eq!(baseline_paint.1, visible);
6561        let translated: Vec<_> = events
6562            .iter()
6563            .map(|e| e.translated(-origin.to_vec2()))
6564            .collect();
6565        assert_eq!(baseline_events, translated);
6566
6567        // A pod whose transform was set and then cleared is indistinguishable.
6568        let mut cleared = recorder_pod(origin, size);
6569        cleared.set_transform(Some(Affine::rotate(1.0) * Affine::scale(3.0)));
6570        cleared.set_transform(None);
6571        assert_eq!(cleared.transform(), None);
6572        assert_eq!(paint(&mut cleared, visible), baseline_paint);
6573        for event in &events {
6574            dispatch(&mut cleared, event);
6575        }
6576        assert_eq!(recorded(&mut cleared), baseline_events);
6577        for point in [
6578            Point::new(12.0, 34.0),
6579            Point::new(71.9, 73.9),
6580            Point::new(72.0, 50.0),
6581            Point::new(11.9, 50.0),
6582        ] {
6583            assert_eq!(cleared.contains(point), baseline.contains(point));
6584        }
6585    }
6586
6587    #[test]
6588    fn a_singular_transform_hits_nothing_and_never_panics() {
6589        let origin = Point::new(10.0, 10.0);
6590        for singular in [
6591            Affine::scale(0.0),
6592            Affine::scale_non_uniform(2.0, 0.0),
6593            Affine::new([1.0, 2.0, 2.0, 4.0, 0.0, 0.0]),
6594            Affine::new([f64::NAN, 0.0, 0.0, 1.0, 0.0, 0.0]),
6595        ] {
6596            let mut pod = recorder_pod(origin, Size::new(50.0, 50.0));
6597            pod.set_transform(Some(singular));
6598            for point in [origin, Point::new(20.0, 20.0), Point::new(10.0, 30.0)] {
6599                assert!(!pod.contains(point), "{singular:?} hit {point:?}");
6600            }
6601            // Paint skips the collapsed subtree entirely.
6602            assert_eq!(
6603                paint(&mut pod, Some(Rect::new(0.0, 0.0, 99.0, 99.0))).0,
6604                vec![]
6605            );
6606            // An event reaching it through a recorded path still arrives,
6607            // translated as if untransformed, so a capture can end.
6608            dispatch(&mut pod, &pointer(PointerPhase::Up, 20.0, 25.0));
6609            assert_eq!(
6610                recorded(&mut pod),
6611                vec![pointer(PointerPhase::Up, 10.0, 15.0)]
6612            );
6613            // Semantics never reports NaN bounds.
6614            let mut sem = SemanticsCtx::new(Size::new(200.0, 200.0), 2);
6615            pod.semantics_child(&mut sem);
6616            let update = sem.finish(accesskit::NodeId(1));
6617            let bounds = update.nodes[0].1.bounds().expect("bounds");
6618            assert!(
6619                [bounds.x0, bounds.y0, bounds.x1, bounds.y1]
6620                    .iter()
6621                    .all(|v| v.is_finite()),
6622                "{singular:?} gave {bounds:?}"
6623            );
6624        }
6625    }
6626
6627    #[test]
6628    fn a_transformed_pod_maps_the_visible_rect_and_reports_its_bounding_box() {
6629        let mut pod = recorder_pod(Point::new(10.0, 20.0), Size::new(50.0, 50.0));
6630        pod.set_transform(Some(Affine::scale(2.0)));
6631        // Paint: the transform is conjugated by the absolute origin, and the
6632        // visible rect reaches the child as its preimage. The parent's own
6633        // visible rect is untouched.
6634        let visible = Some(Rect::new(10.0, 20.0, 110.0, 120.0));
6635        let (ops, parent_visible) = paint(&mut pod, visible);
6636        assert_eq!(parent_visible, visible);
6637        assert_eq!(
6638            ops,
6639            vec![
6640                Op::Push(
6641                    Affine::translate(Vec2::new(10.0, 20.0))
6642                        * Affine::scale(2.0)
6643                        * Affine::translate(Vec2::new(-10.0, -20.0))
6644                ),
6645                Op::Rect(Point::new(10.0, 20.0), Size::new(50.0, 50.0)),
6646                Op::Pop,
6647            ]
6648        );
6649        // What the child itself culls against: the visible rect mapped back
6650        // through the inverse — (10, 20) .. (60, 70) in its own absolute frame.
6651        let seen = std::rc::Rc::new(Cell::new(None));
6652        let mut probe = ChildPod::new(Box::new(VisibleProbe(seen.clone())));
6653        probe.layout_child(
6654            &mut LayoutCtx::new(),
6655            &BoxConstraints::tight(Size::new(50.0, 50.0)),
6656        );
6657        probe.set_origin(Point::new(10.0, 20.0));
6658        probe.set_transform(Some(Affine::scale(2.0)));
6659        paint(&mut probe, visible);
6660        assert_eq!(seen.get(), Some(Rect::new(10.0, 20.0, 60.0, 70.0)));
6661        // Semantics: the transformed box, (10, 20) .. (110, 120).
6662        let mut sem = SemanticsCtx::new(Size::new(400.0, 400.0), 2);
6663        pod.semantics_child(&mut sem);
6664        let update = sem.finish(accesskit::NodeId(1));
6665        let bounds = update.nodes[0].1.bounds().expect("bounds");
6666        assert_eq!(
6667            (bounds.x0, bounds.y0, bounds.x1, bounds.y1),
6668            (10.0, 20.0, 110.0, 120.0)
6669        );
6670    }
6671}
6672
6673#[cfg(test)]
6674mod contact_release_tests {
6675    use super::*;
6676    use crate::event::{ContactPass, PointerButton, PointerEvent, PointerId, PointerPhase};
6677
6678    /// A leaf that captures on `Down`, opting into the gesture's other contacts
6679    /// when `opt_in` is set.
6680    struct Grab {
6681        opt_in: bool,
6682    }
6683    impl Widget for Grab {
6684        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
6685            bc.constrain(Size::new(20.0, 20.0))
6686        }
6687        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
6688        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
6689            if let InputEvent::Pointer(p) = event
6690                && p.phase == PointerPhase::Down
6691            {
6692                ctx.capture_pointer();
6693                if self.opt_in {
6694                    ctx.capture_contacts();
6695                }
6696            }
6697            EventResult::Handled
6698        }
6699    }
6700
6701    /// A single-child container that takes the gesture over from its captured
6702    /// child on any `Move`.
6703    struct Taker {
6704        inner: ChildPod,
6705    }
6706    impl Widget for Taker {
6707        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
6708            self.inner.layout_child(ctx, bc);
6709            bc.constrain(Size::new(20.0, 20.0))
6710        }
6711        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
6712            self.inner.paint_child(ctx, scene);
6713        }
6714        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
6715            match event {
6716                InputEvent::Pointer(p)
6717                    if p.phase == PointerPhase::Move && self.inner.is_active() =>
6718                {
6719                    ctx.release_captured_child(&mut self.inner);
6720                    EventResult::Handled
6721                }
6722                _ => self.inner.event_child(ctx, event),
6723            }
6724        }
6725    }
6726
6727    /// An outer pod around a [`Taker`] around a [`Grab`].
6728    fn nested(opt_in: bool) -> ChildPod {
6729        let mut pod = ChildPod::new(Box::new(Taker {
6730            inner: ChildPod::new(Box::new(Grab { opt_in })),
6731        }));
6732        pod.layout_child(
6733            &mut LayoutCtx::new(),
6734            &BoxConstraints::tight(Size::new(20.0, 20.0)),
6735        );
6736        pod
6737    }
6738
6739    fn pointer(phase: PointerPhase) -> InputEvent {
6740        InputEvent::Pointer(PointerEvent {
6741            phase,
6742            position: Point::new(5.0, 5.0),
6743            button: PointerButton::Primary,
6744        })
6745    }
6746
6747    /// Dispatch into `pod` from a fresh context; whether a capture release
6748    /// bubbled out of it.
6749    fn released(pod: &mut ChildPod, phase: PointerPhase) -> bool {
6750        let mut unit = ();
6751        let mut ctx = EventCtx::new(&mut unit, Point::ZERO, Size::new(20.0, 20.0));
6752        pod.event_child(&mut ctx, &pointer(phase));
6753        ctx.is_capture_released()
6754    }
6755
6756    fn inner_active(pod: &mut ChildPod) -> bool {
6757        pod.widget_mut()
6758            .downcast_mut::<Taker>()
6759            .expect("a Taker")
6760            .inner
6761            .is_active()
6762    }
6763
6764    #[test]
6765    fn releasing_the_opted_in_child_signals_and_bubbles() {
6766        let mut pod = nested(true);
6767        assert!(!released(&mut pod, PointerPhase::Down));
6768        assert!(pod.holds_contact_opt_in(), "the opt-in below is recorded");
6769        assert!(
6770            released(&mut pod, PointerPhase::Move),
6771            "and its release bubbles"
6772        );
6773        assert!(!inner_active(&mut pod), "the child left the active path");
6774        assert!(pod.is_active(), "the container that took over keeps it");
6775    }
6776
6777    #[test]
6778    fn releasing_a_child_without_the_opt_in_clears_the_link_silently() {
6779        let mut pod = nested(false);
6780        released(&mut pod, PointerPhase::Down);
6781        assert!(!pod.holds_contact_opt_in());
6782        assert!(!released(&mut pod, PointerPhase::Move));
6783        assert!(!inner_active(&mut pod));
6784    }
6785
6786    #[test]
6787    fn a_non_claimant_contact_cannot_release_anything() {
6788        let mut pod = nested(true);
6789        released(&mut pod, PointerPhase::Down);
6790        let _pass = ContactPass::enter(PointerId::touch(1), true);
6791        assert!(!released(&mut pod, PointerPhase::Move));
6792        assert!(inner_active(&mut pod), "the claimant's link stands");
6793    }
6794}