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, while its widget persists and is mutated in place.
5//! Widgets take part in layout (constraints in, [`Size`] out) and paint.
6
7use std::any::Any;
8use std::borrow::Cow;
9use std::cell::{Cell, RefCell};
10use std::collections::HashMap;
11use std::num::NonZeroU64;
12use std::sync::Mutex;
13use std::sync::atomic::{AtomicU64, Ordering};
14use std::time::Duration;
15
16/// Re-exported so widgets can name [`PaintScene`]'s argument types.
17pub use frust_scene::{CornerRadii, DashPattern};
18use frust_scene::{GlyphRun, SceneBuilder, ShaderProgram};
19use kurbo::{Affine, BezPath, Point, Rect, Size};
20use peniko::{Brush, Color};
21
22use crate::anim::FrameTime;
23use crate::event::{EventCtx, EventResult, ImeState, InputEvent};
24use crate::insets::WindowInsets;
25use crate::layout::BoxConstraints;
26use crate::semantics::SemanticsCtx;
27use crate::shortcuts::ShortcutId;
28use crate::sliver::{SliverConstraints, SliverGeometry, SliverWidget};
29
30/// The renderer-agnostic paint target a widget draws into; `SceneBuilder`
31/// implements it, and defaulted methods keep recorder test scenes valid.
32/// A renderer that never publishes a ledger paints every retained body inline;
33/// one that does must serve every key it lists or refuse the frame.
34pub trait PaintScene {
35    /// Fill an axis-aligned rectangle at `origin`/`size` with solid `color`.
36    fn fill_rect(&mut self, origin: Point, size: Size, color: Color);
37
38    /// Fill a rectangle with uniformly rounded corners.
39    fn fill_rounded_rect(&mut self, _origin: Point, _size: Size, _radius: f64, _color: Color) {}
40
41    /// Fill a rectangle with per-corner `radii` (defaults to the largest corner).
42    fn fill_rounded_rect_radii(
43        &mut self,
44        origin: Point,
45        size: Size,
46        radii: CornerRadii,
47        color: Color,
48    ) {
49        self.fill_rounded_rect(origin, size, radii.largest(), color);
50    }
51
52    /// Stroke a straight line from `p0` to `p1` with `width` and solid `color`.
53    fn stroke_line(&mut self, _p0: Point, _p1: Point, _width: f64, _color: Color) {}
54
55    /// Push a rectangular clip until the matching [`PaintScene::pop_clip`].
56    fn push_clip(&mut self, _origin: Point, _size: Size) {}
57
58    /// Push a clip with uniformly rounded corners; one clip stack for all kinds.
59    fn push_clip_rounded(&mut self, _origin: Point, _size: Size, _radius: f64) {}
60
61    /// Push a clip with per-corner `radii`. Defaults to the largest corner, not a
62    /// no-op, so a recorder's clip stack stays balanced.
63    fn push_clip_rounded_radii(&mut self, origin: Point, size: Size, radii: CornerRadii) {
64        self.push_clip_rounded(origin, size, radii.largest());
65    }
66
67    /// Push a clip to `path`'s interior (nonzero, local space plus `origin`); a
68    /// retained body under it paints inline. Defaults to the bounding-box clip.
69    fn push_clip_path(&mut self, origin: Point, path: &BezPath) {
70        let bounds = kurbo::Shape::bounding_box(path);
71        self.push_clip(origin + bounds.origin().to_vec2(), bounds.size());
72    }
73
74    /// Pop the most recently pushed clip, rectangular, rounded or path.
75    fn pop_clip(&mut self) {}
76
77    /// Emit unshaped text (recorder intent only); `SceneBuilder` ignores it.
78    fn draw_text(&mut self, origin: Point, text: &str);
79
80    /// Emit a run of already-shaped glyphs.
81    fn draw_glyph_run(&mut self, _run: GlyphRun) {}
82
83    /// Draw a decoded image scaled from its natural size to fill `dest`.
84    fn draw_image(&mut self, _data: &peniko::ImageData, _dest: Rect) {}
85
86    /// Draw a fragment-shader-filled rect into `dest`; `time` is app seconds
87    /// (no CPU golden: `docs/LIMITATIONS.md` `engine-shader-quad-goldens-uncomparable`).
88    /// `program` must be a retained handle — minting one per paint or `build`
89    /// misses the pipeline cache every frame.
90    fn draw_shader(&mut self, _program: &ShaderProgram, _dest: Rect, _time: f32) {}
91
92    /// Composite the scene texture `id` (bound by `frust_gpu::ExternalPass::record`)
93    /// into `dest`, blended; an unbound id draws nothing and warns once.
94    fn draw_scene_texture(&mut self, _id: u64, _dest: Rect) {}
95
96    /// Draw a Gaussian-blurred rounded-rect elevation shadow at `origin`/`size`.
97    fn draw_shadow(
98        &mut self,
99        _origin: Point,
100        _size: Size,
101        _radius: f64,
102        _std_dev: f64,
103        _color: Color,
104    ) {
105    }
106
107    /// Fill a rectangle with `brush`; defaults to `fill_rect` with its solid
108    /// color (transparent for a gradient).
109    fn fill_rect_brush(&mut self, origin: Point, size: Size, brush: &Brush) {
110        let color = match brush {
111            Brush::Solid(color) => *color,
112            _ => Color::TRANSPARENT,
113        };
114        self.fill_rect(origin, size, color);
115    }
116
117    /// Fill a rounded rectangle with an arbitrary `brush`.
118    fn fill_rounded_rect_brush(
119        &mut self,
120        _origin: Point,
121        _size: Size,
122        _radius: f64,
123        _brush: &Brush,
124    ) {
125    }
126
127    /// Push a layer composited at `alpha` until [`PaintScene::pop_layer`].
128    fn push_layer(&mut self, _origin: Point, _size: Size, _alpha: f32) {}
129
130    /// Pop the most recently pushed layer.
131    fn pop_layer(&mut self) {}
132
133    /// Clear a rect to alpha 0, erasing what lies below — the platform-view
134    /// hole-punch on a translucent surface; opaque surfaces disregard it.
135    fn clear_rect(&mut self, _origin: Point, _size: Size) {}
136
137    /// Fill a vector `path` (local space plus `origin`) with `brush`, nonzero.
138    fn fill_path(&mut self, _origin: Point, _path: &BezPath, _brush: &Brush) {}
139
140    /// Stroke a vector `path` with `width`, round caps/joins and `brush`.
141    fn stroke_path(&mut self, _origin: Point, _path: &BezPath, _width: f64, _brush: &Brush) {}
142
143    /// Stroke `path` dashed by `dash`; defaults to a solid stroke.
144    fn stroke_path_dashed(
145        &mut self,
146        origin: Point,
147        path: &BezPath,
148        width: f64,
149        _dash: DashPattern,
150        brush: &Brush,
151    ) {
152        self.stroke_path(origin, path, width, brush);
153    }
154
155    /// Push an affine `transform` (the one seam carrying scale/rotation) until
156    /// [`PaintScene::pop_transform`].
157    fn push_transform(&mut self, _transform: Affine) {}
158
159    /// Pop the most recently pushed transform.
160    fn pop_transform(&mut self) {}
161
162    /// Push a retained bracket cached by `key`, presented at `alpha` and uniform
163    /// `scale`; the default emulates it with a transform then a layer.
164    fn push_retained(&mut self, _key: u64, origin: Point, size: Size, alpha: f32, scale: f64) {
165        let center = Point::new(origin.x + size.width / 2.0, origin.y + size.height / 2.0);
166        self.push_transform(Affine::scale_about(scale, center));
167        self.push_layer(origin, size, alpha);
168    }
169
170    /// Pop a retained bracket: by default the layer, then the transform.
171    fn pop_retained(&mut self) {
172        self.pop_layer();
173        self.pop_transform();
174    }
175
176    /// Draw the body cached under `key`, sent only for keys the ledger listed.
177    fn draw_retained(&mut self, _key: u64, _origin: Point, _size: Size, _alpha: f32, _scale: f64) {}
178
179    /// The transform the next command records under (device scale included),
180    /// or `None` (identity); a retained boundary requests pages at this scale.
181    fn current_transform(&self) -> Option<Affine> {
182        None
183    }
184
185    /// Blur what is already painted under `origin`/`size` with a Gaussian of
186    /// standard deviation `radius` (frosted glass).
187    fn backdrop_blur(&mut self, _origin: Point, _size: Size, _radius: f64) {}
188
189    /// The painter-order index of the next command (`0` for a recorder), which
190    /// a platform-view slot stamps on [`PlatformViewFrame::command_index`].
191    fn command_index(&self) -> usize {
192        0
193    }
194}
195
196/// A [`PaintScene`] that records nothing, for painting a subtree (an alpha-0
197/// page) only for its side effects — hero rects, animating children.
198pub struct DiscardScene;
199
200impl PaintScene for DiscardScene {
201    fn fill_rect(&mut self, _origin: Point, _size: Size, _color: Color) {}
202
203    fn draw_text(&mut self, _origin: Point, _text: &str) {}
204}
205
206/// Bridges [`PaintScene`] onto `SceneBuilder`; unshaped text is dropped.
207impl PaintScene for SceneBuilder<'_> {
208    fn fill_rect(&mut self, origin: Point, size: Size, color: Color) {
209        SceneBuilder::fill_rect(self, rect_at(origin, size), Brush::Solid(color));
210    }
211
212    fn fill_rounded_rect(&mut self, origin: Point, size: Size, radius: f64, color: Color) {
213        SceneBuilder::fill_rounded_rect(self, rect_at(origin, size), radius, Brush::Solid(color));
214    }
215
216    fn fill_rounded_rect_radii(
217        &mut self,
218        origin: Point,
219        size: Size,
220        radii: CornerRadii,
221        color: Color,
222    ) {
223        SceneBuilder::fill_rounded_rect_radii(
224            self,
225            rect_at(origin, size),
226            radii,
227            Brush::Solid(color),
228        );
229    }
230
231    fn stroke_line(&mut self, p0: Point, p1: Point, width: f64, color: Color) {
232        SceneBuilder::stroke_line(self, p0, p1, width, Brush::Solid(color));
233    }
234
235    fn push_clip(&mut self, origin: Point, size: Size) {
236        SceneBuilder::push_clip(self, rect_at(origin, size));
237    }
238
239    fn push_clip_rounded(&mut self, origin: Point, size: Size, radius: f64) {
240        SceneBuilder::push_clip_rounded(self, rect_at(origin, size), radius);
241    }
242
243    fn push_clip_rounded_radii(&mut self, origin: Point, size: Size, radii: CornerRadii) {
244        SceneBuilder::push_clip_rounded_radii(self, rect_at(origin, size), radii);
245    }
246
247    fn push_clip_path(&mut self, origin: Point, path: &BezPath) {
248        SceneBuilder::push_clip_path(self, path_at(origin, path));
249    }
250
251    fn pop_clip(&mut self) {
252        SceneBuilder::pop_clip(self);
253    }
254
255    fn draw_text(&mut self, _origin: Point, _text: &str) {
256        // Unshaped text is not renderable.
257    }
258
259    fn draw_glyph_run(&mut self, run: GlyphRun) {
260        SceneBuilder::draw_glyph_run(self, run);
261    }
262
263    fn draw_image(&mut self, data: &peniko::ImageData, dest: Rect) {
264        SceneBuilder::draw_image(self, data, dest);
265    }
266
267    fn draw_shader(&mut self, program: &ShaderProgram, dest: Rect, time: f32) {
268        SceneBuilder::draw_shader(self, program, dest, time);
269    }
270
271    fn draw_scene_texture(&mut self, id: u64, dest: Rect) {
272        SceneBuilder::scene_texture(self, id, dest);
273    }
274
275    fn draw_shadow(&mut self, origin: Point, size: Size, radius: f64, std_dev: f64, color: Color) {
276        SceneBuilder::draw_blurred_rounded_rect(
277            self,
278            rect_at(origin, size),
279            radius,
280            std_dev,
281            color,
282        );
283    }
284
285    fn fill_rect_brush(&mut self, origin: Point, size: Size, brush: &Brush) {
286        SceneBuilder::fill_rect(self, rect_at(origin, size), brush.clone());
287    }
288
289    fn fill_rounded_rect_brush(&mut self, origin: Point, size: Size, radius: f64, brush: &Brush) {
290        SceneBuilder::fill_rounded_rect(self, rect_at(origin, size), radius, brush.clone());
291    }
292
293    fn push_layer(&mut self, origin: Point, size: Size, alpha: f32) {
294        SceneBuilder::push_layer(self, rect_at(origin, size), alpha);
295    }
296
297    fn pop_layer(&mut self) {
298        SceneBuilder::pop_layer(self);
299    }
300
301    fn clear_rect(&mut self, origin: Point, size: Size) {
302        SceneBuilder::clear_rect(self, rect_at(origin, size));
303    }
304
305    fn fill_path(&mut self, origin: Point, path: &BezPath, brush: &Brush) {
306        SceneBuilder::fill_path(self, path_at(origin, path), brush.clone());
307    }
308
309    fn stroke_path(&mut self, origin: Point, path: &BezPath, width: f64, brush: &Brush) {
310        SceneBuilder::stroke_path(self, path_at(origin, path), width, brush.clone());
311    }
312
313    fn stroke_path_dashed(
314        &mut self,
315        origin: Point,
316        path: &BezPath,
317        width: f64,
318        dash: DashPattern,
319        brush: &Brush,
320    ) {
321        SceneBuilder::stroke_path_dashed(self, path_at(origin, path), width, dash, brush.clone());
322    }
323
324    fn push_transform(&mut self, transform: Affine) {
325        SceneBuilder::push_transform(self, transform);
326    }
327
328    fn pop_transform(&mut self) {
329        SceneBuilder::pop_transform(self);
330    }
331
332    fn push_retained(&mut self, key: u64, origin: Point, size: Size, alpha: f32, scale: f64) {
333        SceneBuilder::push_retained(self, key, rect_at(origin, size), alpha, scale);
334    }
335
336    fn pop_retained(&mut self) {
337        SceneBuilder::pop_retained(self);
338    }
339
340    fn draw_retained(&mut self, key: u64, origin: Point, size: Size, alpha: f32, scale: f64) {
341        SceneBuilder::draw_retained(self, key, rect_at(origin, size), alpha, scale);
342    }
343
344    fn current_transform(&self) -> Option<Affine> {
345        Some(SceneBuilder::current_transform(self))
346    }
347
348    fn backdrop_blur(&mut self, origin: Point, size: Size, radius: f64) {
349        SceneBuilder::backdrop_blur(self, rect_at(origin, size), radius);
350    }
351
352    fn command_index(&self) -> usize {
353        SceneBuilder::command_count(self)
354    }
355}
356
357/// Build an origin/size pair into the `kurbo::Rect` the scene builder speaks.
358fn rect_at(origin: Point, size: Size) -> Rect {
359    Rect::new(
360        origin.x,
361        origin.y,
362        origin.x + size.width,
363        origin.y + size.height,
364    )
365}
366
367/// Translate `path` from local space by `origin`, as [`rect_at`] does for rects.
368fn path_at(origin: Point, path: &BezPath) -> BezPath {
369    Affine::translate((origin.x, origin.y)) * path.clone()
370}
371
372/// Context passed to [`Widget::layout`], carrying the text-shaping context and
373/// theme type-erased so `frust-core` depends on neither `frust-text` nor
374/// `frust-theme`.
375pub struct LayoutCtx<'a> {
376    text_ctx: Option<&'a mut dyn Any>,
377    /// The active theme; `None` in bare-core tests (a supported state).
378    theme: Option<&'a dyn Any>,
379    /// The window insets, global except inside a `with_window_insets` scope.
380    window_insets: WindowInsets,
381    /// The window's logical size, for laying out overlay pods.
382    window_size: Size,
383    /// The root's layout epoch, bumped for global inputs that bypass constraints.
384    epoch: u64,
385    /// The layout-skip kill switch: `true` lays out every pod.
386    force: bool,
387}
388
389impl<'a> LayoutCtx<'a> {
390    /// Create a layout context with no shared resources (leaf unit tests).
391    pub fn new() -> LayoutCtx<'static> {
392        LayoutCtx {
393            text_ctx: None,
394            theme: None,
395            window_insets: WindowInsets::default(),
396            window_size: Size::ZERO,
397            epoch: 0,
398            force: true,
399        }
400    }
401
402    /// Create a layout context carrying the shared text-shaping context.
403    pub fn with_text_context(text_ctx: &'a mut dyn Any) -> Self {
404        LayoutCtx {
405            text_ctx: Some(text_ctx),
406            theme: None,
407            window_insets: WindowInsets::default(),
408            window_size: Size::ZERO,
409            epoch: 0,
410            force: true,
411        }
412    }
413
414    /// Create a layout context carrying an optional text context and theme.
415    pub fn with_resources(text_ctx: Option<&'a mut dyn Any>, theme: Option<&'a dyn Any>) -> Self {
416        LayoutCtx {
417            text_ctx,
418            theme,
419            window_insets: WindowInsets::default(),
420            window_size: Size::ZERO,
421            epoch: 0,
422            force: true,
423        }
424    }
425
426    /// Attach the app's active theme, type-erased.
427    pub fn with_theme(mut self, theme: &'a dyn Any) -> Self {
428        self.theme = Some(theme);
429        self
430    }
431
432    /// Recover the text-shaping context as `&mut T`; panics if absent or of
433    /// another type (a wiring bug).
434    pub fn text_context<T: Any>(&mut self) -> &mut T {
435        self.text_ctx
436            .as_deref_mut()
437            .expect("no text context threaded into this layout pass")
438            .downcast_mut::<T>()
439            .expect("threaded layout resource is not the expected text-context type")
440    }
441
442    /// Recover the theme as `&T`, or `None` if absent or of another type.
443    pub fn theme_as<T: Any>(&self) -> Option<&T> {
444        self.theme?.downcast_ref::<T>()
445    }
446
447    /// The window insets for this pass, possibly narrowed by an ancestor.
448    pub fn window_insets(&self) -> WindowInsets {
449        self.window_insets
450    }
451
452    /// Seed the window insets lent by the render root.
453    pub(crate) fn set_window_insets(&mut self, insets: WindowInsets) {
454        self.window_insets = insets;
455    }
456
457    /// Run `f` with `insets` as the window insets, then restore them (how
458    /// `SafeArea` removes consumed edges); a panic in `f` skips the restore.
459    pub fn with_window_insets<R>(
460        &mut self,
461        insets: WindowInsets,
462        f: impl FnOnce(&mut Self) -> R,
463    ) -> R {
464        let saved = std::mem::replace(&mut self.window_insets, insets);
465        let result = f(self);
466        self.window_insets = saved;
467        result
468    }
469
470    /// The window's logical size ([`Size::ZERO`] before the first root layout);
471    /// overlay pods are laid out against it, not their owner's box.
472    pub fn window_size(&self) -> Size {
473        self.window_size
474    }
475
476    /// Seed the window size lent by the render root.
477    pub(crate) fn set_window_size(&mut self, size: Size) {
478        self.window_size = size;
479    }
480
481    /// The root's global layout epoch for this pass; `0` outside a root.
482    pub fn layout_epoch(&self) -> u64 {
483        self.epoch
484    }
485
486    /// Seed the layout epoch lent by the render root.
487    pub(crate) fn set_layout_epoch(&mut self, epoch: u64) {
488        self.epoch = epoch;
489    }
490
491    /// Whether this pass ignores layout caches (`true` without a render root).
492    pub fn is_forced(&self) -> bool {
493        self.force
494    }
495
496    /// Seed the force flag for this pass.
497    pub(crate) fn set_layout_force(&mut self, force: bool) {
498        self.force = force;
499    }
500}
501
502impl Default for LayoutCtx<'static> {
503    fn default() -> Self {
504        Self::new()
505    }
506}
507
508/// The class of a paint-time frame request: must it land on the next vsync, or
509/// may the frame gate pace it? Any `Transition` makes the frame unpaced.
510#[derive(Clone, Copy, Debug, PartialEq, Eq)]
511pub enum TickClass {
512    /// A pacable decorative loop (shimmer, pulse), paced only when alone.
513    CosmeticLoop,
514    /// User-visible motion that must run every vsync; the dominant class.
515    Transition,
516}
517
518/// Context passed to [`Widget::paint`]: resolved geometry, frame-request
519/// signals that bubble into a [`PaintOutcome`], and root-threaded globals.
520pub struct PaintCtx<'a> {
521    origin: Point,
522    size: Size,
523    needs_frame: bool,
524    /// Whether a layout-changing animation asked for layout next frame.
525    needs_layout: bool,
526    /// The paint pass's layout-path bit: set when a pod below asked for layout
527    /// or an unpaced frame; a paced-only request leaves it clear.
528    layout_path_dirty: bool,
529    /// Whether any frame request this (sub)paint was unpaced.
530    frame_unpaced: bool,
531    /// The min over paced intervals; a bare paced request contributes zero.
532    paced_interval: Option<Duration>,
533    ime_state: Option<ImeState>,
534    /// Whether this widget holds the focus path on the live session.
535    has_focus: bool,
536    /// Whether the root's [`FocusRingPolicy`](crate::app::FocusRingPolicy) shows a ring now.
537    focus_ring: bool,
538    /// Whether this widget is on the recorded hover path.
539    hovered: bool,
540    hover_epoch: u64,
541    /// The live focus session epoch (see [`set_live_focus_session`]).
542    focus_epoch: u64,
543    frame_time: FrameTime,
544    theme: Option<&'a dyn Any>,
545    window_insets: WindowInsets,
546    /// The presented-frame count, stored without dirtying change flags.
547    presented_frames: Option<u64>,
548    /// The hero reporter, `None` outside a shared-element transition.
549    hero: Option<&'a RefCell<HeroFrames>>,
550    /// The absolute rect a scroll ancestor shows, for culling; only narrows.
551    visible_rect: Option<Rect>,
552    /// Platform-view frames published this (sub)paint; extended, never replaced.
553    platform_views: Vec<PlatformViewFrame>,
554    /// Z-shield rects reported this (sub)paint; extended like `platform_views`.
555    input_shields: Vec<Rect>,
556    translucent: bool,
557    /// The root's paint epoch: a pod painted under another one is not clean.
558    /// Covers translucency and every layout-epoch bump.
559    paint_epoch: u64,
560    /// Whether retained layers are enabled.
561    retained_layers: bool,
562    /// The renderer's resident-key ledger; `None` reads as empty.
563    retained_ledger: Option<&'a crate::retained::RetainedLedger>,
564    /// The paint serial; `0` without a root.
565    paint_serial: u64,
566    /// Side-channel publications this (sub)paint made, summed over children.
567    paint_publications: u32,
568    /// The owning root's last paint snapshot, `None` on a bare context.
569    shortcut_hints: Option<std::rc::Rc<[crate::shortcuts::ShortcutHint]>>,
570}
571
572impl<'a> PaintCtx<'a> {
573    /// Create a paint context at `origin`/`size` with a zero clock and no theme.
574    pub fn new(origin: Point, size: Size) -> Self {
575        Self {
576            origin,
577            size,
578            needs_frame: false,
579            needs_layout: false,
580            layout_path_dirty: false,
581            frame_unpaced: false,
582            paced_interval: None,
583            ime_state: None,
584            has_focus: false,
585            focus_ring: false,
586            hovered: false,
587            hover_epoch: 0,
588            focus_epoch: 0,
589            frame_time: FrameTime::ZERO,
590            theme: None,
591            window_insets: WindowInsets::default(),
592            presented_frames: None,
593            hero: None,
594            visible_rect: None,
595            platform_views: Vec::new(),
596            input_shields: Vec::new(),
597            translucent: false,
598            paint_epoch: 0,
599            retained_layers: true,
600            retained_ledger: None,
601            paint_serial: 0,
602            paint_publications: 0,
603            shortcut_hints: None,
604        }
605    }
606
607    /// Hand the context its root's last paint snapshot ([`PaintCtx::shortcut_hints`]).
608    pub(crate) fn with_shortcut_hints(
609        mut self,
610        hints: std::rc::Rc<[crate::shortcuts::ShortcutHint]>,
611    ) -> Self {
612        self.shortcut_hints = Some(hints);
613        self
614    }
615
616    /// Attach the app's active theme, type-erased (widget tests).
617    pub fn with_theme(mut self, theme: &'a dyn Any) -> Self {
618        self.theme = Some(theme);
619        self
620    }
621
622    /// Mark the pass as painting a translucent surface (widget tests).
623    pub fn with_translucent(mut self, translucent: bool) -> Self {
624        self.translucent = translucent;
625        self
626    }
627
628    /// Thread a resident-key ledger into the pass (widget tests).
629    pub fn with_retained_ledger(mut self, ledger: &'a crate::retained::RetainedLedger) -> Self {
630        self.retained_ledger = Some(ledger);
631        self
632    }
633
634    /// Build a paint context seeded with `frame_time`, for clock-dependent tests
635    /// outside this crate (`cfg(test)` / `test-support` only).
636    #[cfg(any(test, feature = "test-support"))]
637    pub fn for_test(origin: Point, size: Size, frame_time: FrameTime) -> Self {
638        let mut ctx = Self::new(origin, size);
639        ctx.frame_time = frame_time;
640        ctx
641    }
642
643    /// Recover the theme as `&T`, or `None` if absent or of another type.
644    pub fn theme_as<T: Any>(&self) -> Option<&T> {
645        self.theme?.downcast_ref::<T>()
646    }
647
648    /// Seed the type-erased theme lent by the render root.
649    pub(crate) fn set_theme(&mut self, theme: Option<&'a dyn Any>) {
650        self.theme = theme;
651    }
652
653    /// The theme reference, copied for re-lending to a child context.
654    pub(crate) fn theme_ref(&self) -> Option<&'a dyn Any> {
655        self.theme
656    }
657
658    /// The window insets for this paint pass.
659    pub fn window_insets(&self) -> WindowInsets {
660        self.window_insets
661    }
662
663    /// Seed the window insets lent by the render root.
664    pub(crate) fn set_window_insets(&mut self, insets: WindowInsets) {
665        self.window_insets = insets;
666    }
667
668    /// The window insets, copied for re-lending to a child context.
669    pub(crate) fn window_insets_ref(&self) -> WindowInsets {
670        self.window_insets
671    }
672
673    /// Run `f` with `insets` as the window insets, then restore them; wrapping
674    /// `paint_child` hands the override to the whole subtree.
675    pub fn with_window_insets<R>(
676        &mut self,
677        insets: WindowInsets,
678        f: impl FnOnce(&mut Self) -> R,
679    ) -> R {
680        let saved = std::mem::replace(&mut self.window_insets, insets);
681        let result = f(self);
682        self.window_insets = saved;
683        result
684    }
685
686    /// The shell's count of frames actually presented, or `None`. An FPS meter
687    /// differences this (`wrapping_sub`), not its own paint count.
688    pub fn presented_frames(&self) -> Option<u64> {
689        self.presented_frames
690    }
691
692    /// Seed the shell's presented-frame count.
693    pub(crate) fn set_presented_frames(&mut self, presented: Option<u64>) {
694        self.presented_frames = presented;
695    }
696
697    /// The widget's absolute window-space origin — the one to anchor overlays
698    /// and reported rects from ([`ChildPod::origin`] is parent-relative).
699    pub fn origin(&self) -> Point {
700        self.origin
701    }
702
703    /// The widget's resolved size.
704    pub fn size(&self) -> Size {
705        self.size
706    }
707
708    /// Whether the widget holds the focus path — authoritative at paint, since
709    /// a container-routed blur never reaches the widget's `event()`.
710    pub fn has_focus(&self) -> bool {
711        self.has_focus
712    }
713
714    /// Seed whether the widget being painted holds focus.
715    pub(crate) fn set_has_focus(&mut self, has_focus: bool) {
716        self.has_focus = has_focus;
717    }
718
719    /// The previous paint's shortcut declarations, as `BuildCtx::shortcut_hints` returns them;
720    /// empty on a context its root did not seed.
721    pub fn shortcut_hints(&self) -> std::rc::Rc<[crate::shortcuts::ShortcutHint]> {
722        self.shortcut_hints
723            .clone()
724            .unwrap_or_else(|| std::rc::Rc::from(Vec::new()))
725    }
726
727    /// Whether to draw a focus ring: [`PaintCtx::has_focus`] and the root's
728    /// [`FocusRingPolicy`](crate::app::FocusRingPolicy) allowing one right now.
729    pub fn is_focused_for_ring(&self) -> bool {
730        self.has_focus && self.focus_ring
731    }
732
733    pub(crate) fn set_focus_ring(&mut self, visible: bool) {
734        self.focus_ring = visible;
735    }
736
737    /// Whether this widget or a descendant is on the hover path (CSS `:hover`).
738    /// Authoritative: the widget the pointer left gets no event, so a hover
739    /// consumer self-corrects from this each paint.
740    pub fn is_hovered(&self) -> bool {
741        self.hovered
742    }
743
744    /// Seed whether the widget being painted holds the hover link.
745    pub(crate) fn set_hovered(&mut self, hovered: bool) {
746        self.hovered = hovered;
747    }
748
749    /// Seed the live hover epoch.
750    pub(crate) fn set_hover_epoch(&mut self, epoch: u64) {
751        self.hover_epoch = epoch;
752    }
753
754    /// The live hover epoch a pod's recorded stamp is compared against.
755    pub(crate) fn hover_epoch(&self) -> u64 {
756        self.hover_epoch
757    }
758
759    /// Seed the live focus epoch.
760    pub(crate) fn set_focus_epoch(&mut self, epoch: u64) {
761        self.focus_epoch = epoch;
762    }
763
764    /// The live focus epoch a pod's recorded stamp is compared against.
765    pub(crate) fn focus_epoch(&self) -> u64 {
766        self.focus_epoch
767    }
768
769    /// The shell clock for this frame; only ever difference it against a
770    /// stored earlier value.
771    pub fn frame_time(&self) -> FrameTime {
772        self.frame_time
773    }
774
775    /// Seed the shell-provided frame time.
776    pub(crate) fn set_frame_time(&mut self, frame_time: FrameTime) {
777        self.frame_time = frame_time;
778    }
779
780    /// Request another frame because paint advanced animation state
781    /// ([`TickClass::Transition`]).
782    pub fn request_frame(&mut self) {
783        self.request_frame_class(TickClass::Transition);
784    }
785
786    /// Request a pacable ([`TickClass::CosmeticLoop`]) frame at the theme's
787    /// `cosmetic_loop_rate`.
788    pub fn request_frame_paced(&mut self) {
789        self.request_frame_paced_at(Duration::ZERO);
790    }
791
792    /// Request a pacable repaint at most once per `interval` (a caret blink).
793    /// Intervals fold to the minimum, so a slow request never starves a fast
794    /// one; capped at [`Self::MAX_PACED_INTERVAL`].
795    pub fn request_frame_paced_at(&mut self, interval: Duration) {
796        self.needs_frame = true;
797        self.merge_paced_interval(interval.min(Self::MAX_PACED_INTERVAL));
798    }
799
800    /// Ceiling on paced intervals: above every real cadence, far below where
801    /// the shells' pacing arithmetic (`interval * 2`, nanos `as u64`) overflows.
802    pub const MAX_PACED_INTERVAL: Duration = Duration::from_secs(10);
803
804    /// Request a frame of an explicit [`TickClass`].
805    pub fn request_frame_class(&mut self, class: TickClass) {
806        match class {
807            TickClass::Transition => {
808                self.needs_frame = true;
809                self.frame_unpaced = true;
810            }
811            TickClass::CosmeticLoop => self.request_frame_paced(),
812        }
813    }
814
815    /// Fold one paced interval into the min aggregate (sole mutation point).
816    fn merge_paced_interval(&mut self, interval: Duration) {
817        self.paced_interval = Some(match self.paced_interval {
818            Some(current) => current.min(interval),
819            None => interval,
820        });
821    }
822
823    /// Fold a bubbled child's interval in, skipping `None` (a zero default
824    /// would re-tighten to the theme cap though nothing asked).
825    fn absorb_paced_interval(&mut self, interval: Option<Duration>) {
826        if let Some(interval) = interval {
827            self.merge_paced_interval(interval);
828        }
829    }
830
831    /// Whether a continuation frame was requested during this (sub)paint.
832    pub fn needs_frame(&self) -> bool {
833        self.needs_frame
834    }
835
836    /// The aggregate [`TickClass`] requested, or `None` if no frame was.
837    pub fn frame_class(&self) -> Option<TickClass> {
838        if !self.needs_frame {
839            None
840        } else if self.frame_unpaced {
841            Some(TickClass::Transition)
842        } else {
843            Some(TickClass::CosmeticLoop)
844        }
845    }
846
847    /// Whether every frame request was [`TickClass::CosmeticLoop`] (paceable).
848    pub fn needs_frame_paced_only(&self) -> bool {
849        self.needs_frame && !self.frame_unpaced
850    }
851
852    /// The tightest paced interval named, or `None`; both mean the theme rate.
853    pub fn paced_interval(&self) -> Option<Duration> {
854        self.paced_interval
855    }
856
857    /// Request layout next frame because this paint's animation changes layout;
858    /// implies an unpaced [`Self::request_frame`].
859    pub fn request_layout(&mut self) {
860        self.needs_layout = true;
861        self.request_frame_class(TickClass::Transition);
862    }
863
864    /// Whether a layout re-run was requested during this (sub)paint.
865    pub fn needs_layout(&self) -> bool {
866        self.needs_layout
867    }
868
869    /// Whether this (sub)paint marked a pod at or below it layout-dirty.
870    pub(crate) fn is_layout_path_dirty(&self) -> bool {
871        self.layout_path_dirty
872    }
873
874    /// Publish the focused editable's IME surface, so an app-driven change that
875    /// never crosses the event pass still reaches the shell.
876    pub fn publish_ime_state(&mut self, state: ImeState) {
877        self.note_publication();
878        self.ime_state = Some(state);
879    }
880
881    /// Take the IME surface published during this (sub)paint, if any.
882    pub fn take_ime_state(&mut self) -> Option<ImeState> {
883        self.ime_state.take()
884    }
885
886    /// Publish a platform-view slot's frame (pushed; many slots per pass).
887    pub fn publish_platform_view(&mut self, frame: PlatformViewFrame) {
888        self.note_publication();
889        self.platform_views.push(frame);
890    }
891
892    /// Take (and clear) every platform-view frame published this (sub)paint.
893    pub fn take_platform_views(&mut self) -> Vec<PlatformViewFrame> {
894        std::mem::take(&mut self.platform_views)
895    }
896
897    /// Report an absolute rect where frust content over a platform-view slot
898    /// keeps winning input (a "z-shield"); the shell differ assigns it to slots.
899    pub fn report_input_shield(&mut self, rect: Rect) {
900        self.note_publication();
901        self.input_shields.push(rect);
902    }
903
904    /// Take (and clear) every z-shield rect reported this (sub)paint.
905    pub fn take_input_shields(&mut self) -> Vec<Rect> {
906        std::mem::take(&mut self.input_shields)
907    }
908
909    /// Float `entry`'s pod above the app this frame and register it for input
910    /// routing. The root paints it after the main tree, so the owner must not;
911    /// the registry clears each pass, so the owner re-registers every paint.
912    pub fn register_overlay(&mut self, entry: crate::overlay::OverlayEntry) {
913        self.note_publication();
914        crate::overlay::register(entry);
915    }
916
917    /// Declare a keyboard shortcut for this frame (see [`crate::shortcuts`]). The table clears
918    /// each paint, so the declarer re-declares every paint; it answers its own
919    /// `InputEvent::Shortcut` with `Handled`.
920    pub fn register_shortcut(&mut self, decl: crate::shortcuts::ShortcutDecl) {
921        self.note_publication();
922        crate::shortcuts::register(decl);
923    }
924
925    /// Publish this frame's focused-field verbs and menu anchor for the
926    /// platform edit menu. A focused field publishes every paint, so hardware
927    /// shortcuts stay answerable; no publish means nothing is focused.
928    pub fn publish_selection_toolbar(
929        &mut self,
930        request: crate::selection_toolbar::SelectionToolbarRequest,
931    ) {
932        self.note_publication();
933        crate::selection_toolbar::publish(request);
934    }
935
936    /// Report a hero's absolute `bounds` and read back its directive; a no-op
937    /// returning [`HeroDirective::Normal`] with no reporter installed.
938    pub fn report_hero(&mut self, tag: &str, bounds: Rect) -> HeroDirective {
939        self.note_publication();
940        match self.hero {
941            Some(cell) => {
942                let mut frames = cell.borrow_mut();
943                let local = Rect::from_origin_size(
944                    bounds.origin() - frames.reference.to_vec2(),
945                    bounds.size(),
946                );
947                frames.captured.insert(tag.to_string(), local);
948                frames
949                    .directives
950                    .get(tag)
951                    .copied()
952                    .unwrap_or(HeroDirective::Normal)
953            }
954            None => HeroDirective::Normal,
955        }
956    }
957
958    /// Whether a hero reporter is installed (culling containers stop culling).
959    pub fn hero_active(&self) -> bool {
960        self.hero.is_some()
961    }
962
963    /// Run `f` with `registry` as the hero reporter; requests and publications
964    /// inside bubble back onto `self`.
965    pub fn with_hero_registry(
966        &mut self,
967        registry: &RefCell<HeroFrames>,
968        f: impl FnOnce(&mut PaintCtx),
969    ) {
970        let mut child = PaintCtx {
971            origin: self.origin,
972            size: self.size,
973            needs_frame: false,
974            needs_layout: false,
975            layout_path_dirty: false,
976            frame_unpaced: false,
977            paced_interval: None,
978            ime_state: None,
979            has_focus: self.has_focus,
980            focus_ring: self.focus_ring,
981            hovered: self.hovered,
982            hover_epoch: self.hover_epoch,
983            focus_epoch: self.focus_epoch,
984            frame_time: self.frame_time,
985            theme: self.theme,
986            window_insets: self.window_insets,
987            presented_frames: self.presented_frames,
988            hero: Some(registry),
989            visible_rect: self.visible_rect,
990            platform_views: Vec::new(),
991            input_shields: Vec::new(),
992            translucent: self.translucent,
993            paint_epoch: self.paint_epoch,
994            retained_layers: self.retained_layers,
995            retained_ledger: self.retained_ledger,
996            paint_serial: self.paint_serial,
997            paint_publications: 0,
998            shortcut_hints: self.shortcut_hints.clone(),
999        };
1000        f(&mut child);
1001        self.paint_publications = self
1002            .paint_publications
1003            .saturating_add(child.paint_publications);
1004        if child.needs_frame {
1005            self.needs_frame = true;
1006        }
1007        if child.needs_layout {
1008            self.needs_layout = true;
1009        }
1010        if child.layout_path_dirty {
1011            self.layout_path_dirty = true;
1012        }
1013        // Same absorb rules as `ChildPod::paint_child`.
1014        if child.frame_unpaced {
1015            self.frame_unpaced = true;
1016        }
1017        self.absorb_paced_interval(child.paced_interval);
1018        if let Some(ime) = child.ime_state.take() {
1019            self.ime_state = Some(ime);
1020        }
1021        self.platform_views.extend(child.platform_views);
1022        self.input_shields.extend(child.input_shields);
1023    }
1024
1025    /// Seed the hero reporter lent by an ancestor.
1026    pub(crate) fn set_hero(&mut self, hero: Option<&'a RefCell<HeroFrames>>) {
1027        self.hero = hero;
1028    }
1029
1030    /// The hero reporter, copied for re-lending to a child context.
1031    pub(crate) fn hero_ref(&self) -> Option<&'a RefCell<HeroFrames>> {
1032        self.hero
1033    }
1034
1035    /// The absolute visible rect for culling, or `None` (paint everything).
1036    pub fn visible_rect(&self) -> Option<Rect> {
1037        self.visible_rect
1038    }
1039
1040    /// Constrain the visible rect to `rect` (absolute); nested rects intersect.
1041    pub fn constrain_visible_rect(&mut self, rect: Rect) {
1042        self.visible_rect = Some(match self.visible_rect {
1043            Some(existing) => existing.intersect(rect),
1044            None => rect,
1045        });
1046    }
1047
1048    /// Seed the visible rectangle lent by an ancestor.
1049    pub(crate) fn set_visible_rect(&mut self, rect: Option<Rect>) {
1050        self.visible_rect = rect;
1051    }
1052
1053    /// The visible rectangle, copied for re-lending to a child context.
1054    pub(crate) fn visible_rect_ref(&self) -> Option<Rect> {
1055        self.visible_rect
1056    }
1057
1058    /// Whether the shell surface is translucent ("Mode B"); the hole-punch
1059    /// clears only then.
1060    pub fn is_translucent(&self) -> bool {
1061        self.translucent
1062    }
1063
1064    /// Seed the shell's surface-translucency flag.
1065    pub(crate) fn set_translucent(&mut self, translucent: bool) {
1066        self.translucent = translucent;
1067    }
1068
1069    /// Seed the paint epoch, retained-layers switch, ledger and paint serial.
1070    pub(crate) fn set_paint_state(
1071        &mut self,
1072        paint_epoch: u64,
1073        retained_layers: bool,
1074        retained_ledger: Option<&'a crate::retained::RetainedLedger>,
1075        paint_serial: u64,
1076    ) {
1077        self.paint_epoch = paint_epoch;
1078        self.retained_layers = retained_layers;
1079        self.retained_ledger = retained_ledger;
1080        self.paint_serial = paint_serial;
1081    }
1082
1083    /// The serial the root minted for this pass (`0` with no root), echoed by
1084    /// the ledger's [`frame`](crate::retained::RetainedLedger::frame).
1085    pub fn paint_serial(&self) -> u64 {
1086        self.paint_serial
1087    }
1088
1089    /// Whether the renderer holds a page for `key` at `rect_px` and `scale`, per
1090    /// a ledger covering `recorded_serial` (guarding against a stale ledger).
1091    pub fn retained_available(
1092        &self,
1093        key: crate::retained::RetainedKey,
1094        rect_px: kurbo::Size,
1095        scale: f32,
1096        recorded_serial: u64,
1097    ) -> bool {
1098        let extent = (
1099            rect_px.width.round().max(0.0) as u32,
1100            rect_px.height.round().max(0.0) as u32,
1101        );
1102        self.retained_layers
1103            && self
1104                .retained_ledger
1105                .is_some_and(|l| l.covers(recorded_serial) && l.is_resident(key, extent, scale))
1106    }
1107
1108    /// Whether nothing under `pod` changed since its last paint (ask about the
1109    /// child pod); hover and focus moves bump the paint epoch.
1110    pub fn subtree_clean(&self, pod: &ChildPod) -> bool {
1111        self.retained_layers && !pod.paint_dirty && pod.painted_epoch == self.paint_epoch
1112    }
1113
1114    /// Whether a boundary may bracket its body (retained layers on, ledger set).
1115    pub fn retained_enabled(&self) -> bool {
1116        self.retained_layers && self.retained_ledger.is_some()
1117    }
1118
1119    /// Side-channel publications (IME, platform view, shield, overlay, toolbar,
1120    /// hero) this (sub)paint made; a body that published must not be replayed.
1121    pub fn paint_publications(&self) -> u32 {
1122        self.paint_publications
1123    }
1124
1125    /// Count one paint-time side-channel publication.
1126    fn note_publication(&mut self) {
1127        self.paint_publications = self.paint_publications.saturating_add(1);
1128    }
1129}
1130
1131/// A tagged-rect ("hero") reporter a container installs over a subtree with
1132/// [`PaintCtx::with_hero_registry`] to find where tagged descendants painted.
1133#[derive(Debug, Default)]
1134pub struct HeroFrames {
1135    /// The enclosing surface's absolute top-left, subtracted from each report.
1136    reference: Point,
1137    /// Surface-local rects reported by tagged descendants this paint.
1138    captured: HashMap<String, Rect>,
1139    /// Per-tag paint directive the installer set for this paint.
1140    directives: HashMap<String, HeroDirective>,
1141}
1142
1143impl HeroFrames {
1144    /// A reporter at `reference` (absolute top-left) with per-tag `directives`.
1145    pub fn new(reference: Point, directives: HashMap<String, HeroDirective>) -> Self {
1146        Self {
1147            reference,
1148            captured: HashMap::new(),
1149            directives,
1150        }
1151    }
1152
1153    /// The surface-local rects captured this paint, keyed by tag.
1154    pub fn captured(&self) -> &HashMap<String, Rect> {
1155        &self.captured
1156    }
1157
1158    /// Consume the reporter, returning the captured surface-local rects.
1159    pub fn into_captured(self) -> HashMap<String, Rect> {
1160        self.captured
1161    }
1162}
1163
1164/// What a reported hero should do this paint ([`PaintCtx::report_hero`]).
1165#[derive(Clone, Copy, Debug, PartialEq)]
1166pub enum HeroDirective {
1167    /// Paint normally — the default.
1168    Normal,
1169    /// Skip painting: the matching hero on the other surface morphs this one.
1170    Suppress,
1171    /// Paint under a transform mapping the hero's absolute bounds onto `dest`.
1172    Morph {
1173        /// The absolute destination rect the hero's bounds morph onto.
1174        dest: Rect,
1175    },
1176}
1177
1178static NEXT_SLOT_ID: AtomicU64 = AtomicU64::new(1);
1179
1180/// Allocate a fresh platform-view slot id once per widget instance — never
1181/// from tree position, so a reorder keeps identity.
1182pub fn next_slot_id() -> u64 {
1183    NEXT_SLOT_ID.fetch_add(1, Ordering::Relaxed)
1184}
1185
1186/// Cap on the undrained retire list (nothing drains on desktop). Past it the
1187/// oldest id drops; the differ's missing-streak backstop still disposes it.
1188const MAX_PENDING_RETIRED_SLOTS: usize = 256;
1189
1190/// Process-wide: a widget being torn down has no `RenderRoot` handle.
1191static RETIRED_SLOTS: Mutex<Vec<u64>> = Mutex::new(Vec::new());
1192
1193/// Record that the widget owning `slot_id` was torn down, so the shell disposes
1194/// the native view promptly (teardown runs mid-rebuild, outside paint).
1195pub fn report_retired_slot(slot_id: u64) {
1196    let mut pending = RETIRED_SLOTS.lock().unwrap_or_else(|e| e.into_inner());
1197    if pending.len() >= MAX_PENDING_RETIRED_SLOTS {
1198        pending.remove(0);
1199    }
1200    pending.push(slot_id);
1201}
1202
1203/// Take (and clear) every retired slot id since the last call, in teardown order.
1204pub fn take_retired_slots() -> Vec<u64> {
1205    let mut pending = RETIRED_SLOTS.lock().unwrap_or_else(|e| e.into_inner());
1206    std::mem::take(&mut *pending)
1207}
1208
1209/// A platform-view slot's paint-time frame, in logical px and absolute window
1210/// coordinates. Per pass: a slot that did not paint has none, and the shell
1211/// differ decides hide/dispose.
1212#[derive(Clone, Debug, PartialEq)]
1213pub struct PlatformViewFrame {
1214    /// Stable per-instance id from [`next_slot_id`].
1215    pub slot_id: u64,
1216    /// `"dev.frust.<Factory>"`: the native view factory creating this slot.
1217    pub view_type: String,
1218    /// Opaque creation params for the native factory (may be empty).
1219    pub params_json: String,
1220    /// Bumped by the widget whenever `params_json` changes.
1221    pub params_generation: u64,
1222    /// Absolute paint bounds.
1223    pub rect: Rect,
1224    /// `visible_rect` intersected with `rect`, or `None` if fully visible.
1225    pub clip: Option<Rect>,
1226    /// `false` when the widget itself hides it (distinct from being absent).
1227    pub visible: bool,
1228    /// Mode B input forwarding: a touch-DOWN in `rect` outside every shield
1229    /// goes to the native view.
1230    pub interactive: bool,
1231    /// Manually declared z-shields, used only when `interactive`; the shell
1232    /// differ merges in the auto-collected ones.
1233    pub shields: Vec<Rect>,
1234    /// Painter-order position ([`PaintScene::command_index`]) taken before the
1235    /// slot's own punch; later commands paint over the native view.
1236    pub command_index: usize,
1237}
1238
1239/// The result of a whole [`crate::app::RenderRoot::paint`] pass.
1240#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1241pub struct PaintOutcome {
1242    /// Whether the shell should schedule another frame.
1243    pub needs_frame: bool,
1244    /// Whether a widget called [`PaintCtx::request_layout`].
1245    pub needs_layout: bool,
1246    /// Whether every frame request was [`TickClass::CosmeticLoop`] (paceable).
1247    pub needs_frame_paced_only: bool,
1248    /// The tightest paced interval; `None` and `Some(ZERO)` both mean the
1249    /// theme rate.
1250    pub paced_interval: Option<Duration>,
1251    /// The paint serial, handed to the renderer with the scene.
1252    pub paint_serial: u64,
1253}
1254
1255/// A retained UI element living in the widget tree. `Widget: Any` so a boxed
1256/// widget can be downcast back to its concrete type during rebuild.
1257pub trait Widget: Any {
1258    /// Choose a size within `bc` and return it. Under a render root the call
1259    /// may be skipped (see [`ChildPod::layout_child`]), so the result must
1260    /// depend only on `bc`, the context's globals and state changed through a
1261    /// pod-dirtying channel: a `LAYOUT` rebuild, `request_layout`, an unpaced
1262    /// frame request, or a handled / redraw-requesting event.
1263    fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size;
1264
1265    /// Emit draw commands into `scene`. Paint may advance animation state; it
1266    /// must then call [`PaintCtx::request_frame`] until the animation rests.
1267    fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene);
1268
1269    /// Handle an input event and report whether it was consumed; defaults to
1270    /// [`EventResult::Ignored`]. Containers forward via [`ChildPod::event_child`].
1271    fn event(&mut self, _ctx: &mut EventCtx, _event: &InputEvent) -> EventResult {
1272        EventResult::Ignored
1273    }
1274
1275    /// Contribute accessibility node(s) after layout; defaults to none. A
1276    /// container forwards via [`ChildPod::semantics_child`].
1277    fn semantics(&self, _ctx: &mut SemanticsCtx) {}
1278
1279    /// This widget's concrete type name, for diagnostics only (`docs/LIMITATIONS.md`
1280    /// `focus-wrapper-erasure-swap-blind`); override only in a transparent wrapper.
1281    fn type_name(&self) -> &'static str {
1282        core::any::type_name::<Self>()
1283    }
1284
1285    /// Visit this widget's owned [`ChildPod`]s in paint order, for read-only
1286    /// tooling; defaults to none and must not mutate pass state.
1287    fn visit_children(&self, _visitor: &mut dyn FnMut(&ChildPod)) {}
1288
1289    /// Whether keyboard traversal (Tab / Shift+Tab at the root) may stop on this widget's pod.
1290    fn is_focusable(&self) -> bool {
1291        false
1292    }
1293
1294    /// Visit the children traversal may enter, in focus order; defaults to
1295    /// [`Widget::visit_children`]. A container showing only some children narrows it.
1296    fn visit_focus_children(&self, visitor: &mut dyn FnMut(&ChildPod)) {
1297        self.visit_children(visitor);
1298    }
1299
1300    /// This widget as a sliver ([`crate::sliver`]), if it speaks that protocol.
1301    /// Defaults to `None`; transparent wrappers forward it.
1302    fn as_sliver(&mut self) -> Option<&mut dyn SliverWidget> {
1303        None
1304    }
1305}
1306
1307impl dyn Widget {
1308    /// Attempt to downcast this trait object to a concrete widget type.
1309    pub fn downcast_mut<W: Widget>(&mut self) -> Option<&mut W> {
1310        let any: &mut dyn Any = self;
1311        any.downcast_mut::<W>()
1312    }
1313}
1314
1315/// A boxed widget is itself a [`Widget`], delegating every pass — what lets an
1316/// `AnyView`'s `Box<dyn Widget>` element sit in a [`ChildPod`].
1317impl Widget for Box<dyn Widget> {
1318    fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1319        (**self).layout(ctx, bc)
1320    }
1321
1322    fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
1323        (**self).paint(ctx, scene);
1324    }
1325
1326    fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1327        (**self).event(ctx, event)
1328    }
1329
1330    fn semantics(&self, ctx: &mut SemanticsCtx) {
1331        (**self).semantics(ctx);
1332    }
1333
1334    fn visit_children(&self, visitor: &mut dyn FnMut(&ChildPod)) {
1335        (**self).visit_children(visitor);
1336    }
1337
1338    fn is_focusable(&self) -> bool {
1339        (**self).is_focusable()
1340    }
1341
1342    fn visit_focus_children(&self, visitor: &mut dyn FnMut(&ChildPod)) {
1343        (**self).visit_focus_children(visitor);
1344    }
1345
1346    /// Forward the boxed widget's sliver face, so erasure never hides a sliver.
1347    fn as_sliver(&mut self) -> Option<&mut dyn SliverWidget> {
1348        (**self).as_sliver()
1349    }
1350
1351    /// Report the boxed widget's name, through any number of box layers.
1352    fn type_name(&self) -> &'static str {
1353        (**self).type_name()
1354    }
1355}
1356
1357/// A container's owned child: a boxed widget plus the geometry and
1358/// capture/focus/hover bookkeeping the container keeps for it. Children are
1359/// pods, not arena nodes; `origin`/`size` are in the container's space.
1360pub struct ChildPod {
1361    widget: Box<dyn Widget>,
1362    origin: Point,
1363    size: Size,
1364    /// An optional transform relative to `origin` ([`ChildPod::set_transform`]).
1365    transform: Option<Affine>,
1366    /// Set while the child holds a pointer capture.
1367    active: bool,
1368    /// Whether the child itself opted into contacts on this capture.
1369    contacts_captor: bool,
1370    /// Whether the child or a descendant opted into contacts on this capture.
1371    contacts_path: bool,
1372    /// Set when a focus claim passed through; says nothing about the live
1373    /// session on its own (read [`ChildPod::holds_live_focus`]).
1374    focused: bool,
1375    /// The focus session epoch this link was stamped with; never cleared, so a
1376    /// moved session strands older links by arithmetic.
1377    focus_epoch: u64,
1378    /// The root identity paired with `focus_epoch` (per-root epochs collide).
1379    focus_root: u64,
1380    /// The hover epoch this link was stamped with; never cleared. Hover has no
1381    /// leave event, so an advancing live epoch strands stale stamps.
1382    hover_epoch: u64,
1383    /// The root identity paired with `hover_epoch`, read only by `Drop`.
1384    hover_root: u64,
1385    /// Persistent semantics base id, assigned on the first semantics visit.
1386    semantics_id: Cell<Option<NonZeroU64>>,
1387    /// An optional human name for tooling.
1388    debug_label: Option<Cow<'static, str>>,
1389    /// Persistent tooling id, assigned on the first `inspect_id` call.
1390    inspect_id: Cell<Option<NonZeroU64>>,
1391    /// Whether the next layout must run whatever the constraints.
1392    layout_dirty: bool,
1393    /// Whether anything under this pod may have changed since its last paint.
1394    paint_dirty: bool,
1395    painted_epoch: u64,
1396    /// The skip key: constraints, epoch and insets of the last layout.
1397    last_constraints: Option<BoxConstraints>,
1398    last_epoch: u64,
1399    last_insets: WindowInsets,
1400    /// Whether the last paint requested a frame or layout (for the landing paint).
1401    painted_animating: bool,
1402    /// The sliver skip key; at most one of this and `last_constraints` is set.
1403    last_sliver_constraints: Option<SliverConstraints>,
1404    /// The geometry the last sliver layout reported.
1405    sliver_geometry: Option<SliverGeometry>,
1406}
1407
1408thread_local! {
1409    /// The next tooling id [`ChildPod::inspect_id`] hands out (UI-thread only).
1410    static NEXT_INSPECT_ID: Cell<u64> = const { Cell::new(ChildPod::INSPECT_ID_BASE) };
1411
1412    /// The inspect id of the pod that recorded this dispatch's hover claim.
1413    static HOVER_CLAIMANT: Cell<Option<u64>> = const { Cell::new(None) };
1414
1415    /// Whether the first pod to record this dispatch's focus claim already held a live link.
1416    static FOCUS_REQ_FROM_LIVE: Cell<Option<bool>> = const { Cell::new(None) };
1417
1418    /// The thread's focus session as `(root, live epoch, claim epoch)`; the
1419    /// claim epoch is a dispatch's candidate session. `(0, 0, 0)` until a root
1420    /// publishes, which a real root never does.
1421    static LIVE_FOCUS_SESSION: Cell<(u64, u64, u64)> = const { Cell::new((0, 0, 0)) };
1422
1423    /// The open root→pod focus grant as `(carrier, target inspect id, landed)`; lands once.
1424    static FOCUS_GRANT: Cell<Option<(ShortcutId, u64, bool)>> = const { Cell::new(None) };
1425}
1426
1427/// A root→pod focus grant ([`RenderRoot`](crate::app::RenderRoot)'s Tab traversal), open for one
1428/// delivery of `InputEvent::Shortcut(carrier)`: the pod whose [`ChildPod::inspect_id`] is the
1429/// target turns it into the state [`EventCtx::request_focus`] produces, so its own pod and every
1430/// ancestor's record the link as for a claim. Closed on drop.
1431pub(crate) struct FocusGrant;
1432
1433impl FocusGrant {
1434    pub(crate) fn enter(carrier: ShortcutId, target: u64) -> Self {
1435        FOCUS_GRANT.with(|grant| grant.set(Some((carrier, target, false))));
1436        FocusGrant
1437    }
1438}
1439
1440impl Drop for FocusGrant {
1441    fn drop(&mut self) {
1442        FOCUS_GRANT.with(|grant| grant.set(None));
1443    }
1444}
1445
1446/// Publish the focus session standing on this thread: `live` is the session a
1447/// link must name to count, `claim` the one a link recorded now takes. A
1448/// thread-local because pod destructors run with no context.
1449pub(crate) fn set_live_focus_session(root: u64, live: u64, claim: u64) {
1450    LIVE_FOCUS_SESSION.with(|slot| slot.set((root, live, claim)));
1451}
1452
1453/// The `(root, epoch)` a link recorded now is stamped with.
1454fn focus_claim_stamp() -> (u64, u64) {
1455    let (root, _live, claim) = LIVE_FOCUS_SESSION.with(|slot| slot.get());
1456    (root, claim)
1457}
1458
1459/// Whether `(root, epoch)` names the live session or the open candidate.
1460fn names_live_focus_session(root: u64, epoch: u64) -> bool {
1461    let (live_root, live, claim) = LIVE_FOCUS_SESSION.with(|slot| slot.get());
1462    root == live_root && (epoch == live || epoch == claim)
1463}
1464
1465impl Drop for ChildPod {
1466    /// Report a live hover link severed by this pod's removal, so
1467    /// [`RenderRoot::rebuild`](crate::app::RenderRoot::rebuild) ends the hover.
1468    fn drop(&mut self) {
1469        if crate::event::live_hover_link_is(self.hover_root, self.hover_epoch) {
1470            crate::event::mark_hover_orphaned(self.hover_root);
1471        }
1472        // Focus half: an overlay-floated pod is severed by no reconciler. A zero
1473        // epoch means the link was never held.
1474        if self.focused
1475            && self.focus_epoch != 0
1476            && names_live_focus_session(self.focus_root, self.focus_epoch)
1477        {
1478            crate::event::mark_focus_orphaned();
1479        }
1480    }
1481}
1482
1483/// Forget the recorded hover claimant (called before a dispatch).
1484pub(crate) fn clear_hover_claimant() {
1485    HOVER_CLAIMANT.with(|claimant| claimant.set(None));
1486}
1487
1488/// Take the inspect id of the pod that recorded the last hover claim.
1489pub(crate) fn take_hover_claimant() -> Option<u64> {
1490    HOVER_CLAIMANT.with(Cell::take)
1491}
1492
1493/// Forget the recorded claimant liveness (called before a dispatch).
1494pub(crate) fn clear_focus_req_from_live() {
1495    FOCUS_REQ_FROM_LIVE.with(|c| c.set(None));
1496}
1497
1498/// Take whether the claim's first pod already held a live link (`None`: no pod claimed).
1499pub(crate) fn take_focus_req_from_live() -> Option<bool> {
1500    FOCUS_REQ_FROM_LIVE.with(Cell::take)
1501}
1502
1503impl ChildPod {
1504    /// Where [`ChildPod::inspect_id`]'s allocator starts: 2^48, far from arena
1505    /// `WidgetId`s.
1506    pub const INSPECT_ID_BASE: u64 = 1 << 48;
1507
1508    /// Wrap a freshly built child widget at the origin with zero size.
1509    pub fn new(widget: Box<dyn Widget>) -> Self {
1510        Self {
1511            widget,
1512            origin: Point::ZERO,
1513            size: Size::ZERO,
1514            transform: None,
1515            active: false,
1516            contacts_captor: false,
1517            contacts_path: false,
1518            focused: false,
1519            focus_epoch: 0,
1520            focus_root: 0,
1521            hover_epoch: 0,
1522            hover_root: 0,
1523            semantics_id: Cell::new(None),
1524            debug_label: None,
1525            inspect_id: Cell::new(None),
1526            layout_dirty: true,
1527            paint_dirty: true,
1528            painted_epoch: 0,
1529            last_constraints: None,
1530            last_epoch: 0,
1531            last_insets: WindowInsets::default(),
1532            painted_animating: false,
1533            last_sliver_constraints: None,
1534            sliver_geometry: None,
1535        }
1536    }
1537
1538    /// The wrapped widget's concrete type name, asked of the live widget
1539    /// ([`Widget::type_name`]). Diagnostic only; never parse it.
1540    pub fn type_name(&self) -> &'static str {
1541        self.widget.type_name()
1542    }
1543
1544    /// The human name attached for tooling, if any. `None` by default.
1545    pub fn debug_label(&self) -> Option<&str> {
1546        self.debug_label.as_deref()
1547    }
1548
1549    /// Attach a human name for tooling; nothing in the passes reads it.
1550    pub fn set_debug_label(&mut self, label: impl Into<Cow<'static, str>>) {
1551        self.debug_label = Some(label.into());
1552    }
1553
1554    /// Drop any attached debug label.
1555    pub fn clear_debug_label(&mut self) {
1556        self.debug_label = None;
1557    }
1558
1559    /// The attached label as an owned [`Cow`], for an inspect snapshot.
1560    pub(crate) fn debug_label_cow(&self) -> Option<Cow<'static, str>> {
1561        self.debug_label.clone()
1562    }
1563
1564    /// This pod's tooling id, assigned on first call and stable thereafter.
1565    pub fn inspect_id(&self) -> crate::view::WidgetId {
1566        let id = match self.inspect_id.get() {
1567            Some(id) => id,
1568            None => {
1569                let id = NEXT_INSPECT_ID.with(|next| {
1570                    let id = next.get();
1571                    next.set(id + 1);
1572                    NonZeroU64::new(id).expect("the allocator starts at 2^48, never zero")
1573                });
1574                self.inspect_id.set(Some(id));
1575                id
1576            }
1577        };
1578        crate::view::WidgetId(id.get())
1579    }
1580
1581    /// Shared access to the boxed child widget.
1582    pub fn widget(&self) -> &dyn Widget {
1583        &*self.widget
1584    }
1585
1586    /// Mutable access to the boxed widget; marks the pod layout-dirty.
1587    pub fn widget_mut(&mut self) -> &mut dyn Widget {
1588        self.layout_dirty = true;
1589        self.paint_dirty = true;
1590        &mut *self.widget
1591    }
1592
1593    /// Mutable access without marking the pod dirty; the caller marks it.
1594    pub fn widget_mut_for_rebuild(&mut self) -> &mut dyn Widget {
1595        &mut *self.widget
1596    }
1597
1598    /// Replace the boxed child widget; marks the pod layout-dirty.
1599    pub fn set_widget(&mut self, widget: Box<dyn Widget>) {
1600        self.widget = widget;
1601        self.layout_dirty = true;
1602        self.paint_dirty = true;
1603    }
1604
1605    /// Whether the next layout must run (tooling and tests).
1606    pub fn is_layout_dirty(&self) -> bool {
1607        self.layout_dirty
1608    }
1609
1610    /// Mark this pod (only) layout-dirty; a caller outside a rebuild must also
1611    /// dirty its path with `request_layout`.
1612    pub fn mark_layout_dirty(&mut self) {
1613        self.layout_dirty = true;
1614        self.paint_dirty = true;
1615    }
1616
1617    /// Whether the subtree must be painted afresh (tooling and tests).
1618    pub fn is_paint_dirty(&self) -> bool {
1619        self.paint_dirty
1620    }
1621
1622    /// Mark this pod (only) paint-dirty.
1623    pub fn mark_paint_dirty(&mut self) {
1624        self.paint_dirty = true;
1625    }
1626
1627    /// The child's origin in the container's coordinate space.
1628    pub fn origin(&self) -> Point {
1629        self.origin
1630    }
1631
1632    /// The child's resolved size (valid after [`ChildPod::layout_child`]).
1633    pub fn size(&self) -> Size {
1634        self.size
1635    }
1636
1637    /// Place the child at `origin` within the container's coordinate space.
1638    pub fn set_origin(&mut self, origin: Point) {
1639        self.origin = origin;
1640    }
1641
1642    /// The child's transform, if any (see [`ChildPod::set_transform`]).
1643    pub fn transform(&self) -> Option<Affine> {
1644        self.transform
1645    }
1646
1647    /// Place the child under `transform` (local space, before `origin`: `p`
1648    /// lands at `origin + transform * p`), or clear it with `None`. Paint, hit
1649    /// tests and events map through it; semantics reports its bounding box. A
1650    /// non-invertible transform draws and hits nothing. Window-space rects
1651    /// reported from `paint` are not mapped.
1652    pub fn set_transform(&mut self, transform: Option<Affine>) {
1653        self.transform = transform;
1654    }
1655
1656    /// The child's local→container mapping: `translate(origin) * transform`.
1657    fn local_to_container(&self, transform: Affine) -> Affine {
1658        Affine::translate(self.origin.to_vec2()) * transform
1659    }
1660
1661    /// Whether this child currently holds the recorded active (captured) path.
1662    pub fn is_active(&self) -> bool {
1663        self.active
1664    }
1665
1666    /// Set (or clear) the recorded active path. A clear is refused during a
1667    /// non-claimant contact's walk, so it cannot strand the claimant.
1668    pub fn set_active(&mut self, active: bool) {
1669        if !active && crate::event::in_secondary_contact_pass() {
1670            return;
1671        }
1672        self.active = active;
1673        if !active {
1674            self.contacts_captor = false;
1675            self.contacts_path = false;
1676        }
1677    }
1678
1679    /// Whether the active path leads to the widget that opted into the
1680    /// gesture's other contacts ([`EventCtx::capture_contacts`]).
1681    pub(crate) fn holds_contact_opt_in(&self) -> bool {
1682        self.active && self.contacts_path
1683    }
1684
1685    /// Whether this child has a recorded focus path at all; routing needs
1686    /// [`ChildPod::holds_live_focus`].
1687    pub fn is_focused(&self) -> bool {
1688        self.focused
1689    }
1690
1691    /// Record (or drop) this child's focus path, stamping the live session.
1692    pub fn set_focused(&mut self, focused: bool) {
1693        self.focused = focused;
1694        if focused {
1695            let (root, epoch) = focus_claim_stamp();
1696            self.focus_root = root;
1697            self.focus_epoch = epoch;
1698        }
1699    }
1700
1701    /// The focus epoch stamp (diagnostic; non-zero does not mean focused).
1702    pub fn focus_epoch(&self) -> u64 {
1703        self.focus_epoch
1704    }
1705
1706    /// Whether this child holds a focus link on the root's live session (flag
1707    /// set and stamp matching). Without a root both read `(0, 0)`.
1708    pub fn holds_live_focus(&self) -> bool {
1709        self.focused && names_live_focus_session(self.focus_root, self.focus_epoch)
1710    }
1711
1712    /// Drop a focus link the live session already stranded, reporting whether
1713    /// one was dropped; a live link is never retired.
1714    pub fn retire_stale_focus_link(&mut self) -> bool {
1715        if self.focused && !self.holds_live_focus() {
1716            self.focused = false;
1717            true
1718        } else {
1719            false
1720        }
1721    }
1722
1723    /// The hover epoch stamp (`0`: never claimed). Diagnostic only — read
1724    /// [`PaintCtx::is_hovered`] instead.
1725    pub fn hover_epoch(&self) -> u64 {
1726        self.hover_epoch
1727    }
1728
1729    /// Lay the child out under `bc` and return its size, or return the recorded
1730    /// size without calling [`Widget::layout`] when the pass is not forced, the
1731    /// pod is clean, and `bc`, the epoch and the insets match last time's.
1732    /// Dirty sources: a `LAYOUT` rebuild or swap, a frame or layout request
1733    /// below (plus the landing paint), a handled or redraw event,
1734    /// [`ChildPod::widget_mut`], a fresh build.
1735    pub fn layout_child(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1736        if !ctx.force
1737            && !self.layout_dirty
1738            && self.last_constraints == Some(*bc)
1739            && self.last_epoch == ctx.epoch
1740            && self.last_insets == ctx.window_insets
1741        {
1742            return self.size;
1743        }
1744        let size = self.widget.layout(ctx, bc);
1745        self.size = size;
1746        self.last_constraints = Some(*bc);
1747        self.last_sliver_constraints = None;
1748        self.sliver_geometry = None;
1749        self.last_epoch = ctx.epoch;
1750        self.last_insets = ctx.window_insets;
1751        self.layout_dirty = false;
1752        self.paint_dirty = true;
1753        size
1754    }
1755
1756    /// Lay the child out as a sliver under `sc`, or return `None` for a
1757    /// non-sliver child. Skips like [`ChildPod::layout_child`]; a geometry with
1758    /// a scroll-offset correction leaves the pod dirty so it is never replayed.
1759    pub fn layout_sliver_child(
1760        &mut self,
1761        ctx: &mut LayoutCtx,
1762        sc: &SliverConstraints,
1763    ) -> Option<SliverGeometry> {
1764        if !ctx.force
1765            && !self.layout_dirty
1766            && self.last_sliver_constraints == Some(*sc)
1767            && self.last_epoch == ctx.epoch
1768            && self.last_insets == ctx.window_insets
1769            && let Some(geometry) = self.sliver_geometry
1770        {
1771            return Some(geometry);
1772        }
1773        sc.debug_assert_valid();
1774        let geometry = self.widget.as_sliver()?.layout_sliver(ctx, sc);
1775        geometry.debug_assert_valid(sc);
1776        self.size = sc.axis.size(geometry.paint_extent, sc.cross_axis_extent);
1777        self.sliver_geometry = Some(geometry);
1778        self.last_sliver_constraints = Some(*sc);
1779        self.last_constraints = None;
1780        self.last_epoch = ctx.epoch;
1781        self.last_insets = ctx.window_insets;
1782        self.layout_dirty = geometry.scroll_offset_correction.is_some();
1783        self.paint_dirty = true;
1784        Some(geometry)
1785    }
1786
1787    /// The geometry of the last sliver layout, if it was one.
1788    pub fn sliver_geometry(&self) -> Option<SliverGeometry> {
1789        self.sliver_geometry
1790    }
1791
1792    /// Whether the child speaks the sliver protocol (does not dirty the pod).
1793    pub fn is_sliver(&mut self) -> bool {
1794        self.widget.as_sliver().is_some()
1795    }
1796
1797    /// Paint the child at its absolute position, bubbling its requests and
1798    /// publications into `ctx`; a non-invertible transform skips it.
1799    pub fn paint_child(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
1800        let Some(transform) = self.transform else {
1801            let visible_rect = ctx.visible_rect_ref();
1802            self.paint_child_in(ctx, scene, visible_rect);
1803            return;
1804        };
1805        // A singular transform leaves nothing to draw and no inverse.
1806        if crate::hit::checked_inverse(&transform).is_none() {
1807            return;
1808        }
1809        // Conjugate the local transform by the absolute origin: a local point
1810        // drawn at `abs + p` lands at `abs + transform * p`.
1811        let abs = (ctx.origin() + self.origin.to_vec2()).to_vec2();
1812        let around = Affine::translate(abs) * transform * Affine::translate(-abs);
1813        // Culling descendants get the visible rect's preimage bounding box.
1814        let visible_rect = match (ctx.visible_rect_ref(), crate::hit::checked_inverse(&around)) {
1815            (Some(rect), Some(inverse)) => Some(inverse.transform_rect_bbox(rect)),
1816            _ => None,
1817        };
1818        scene.push_transform(around);
1819        self.paint_child_in(ctx, scene, visible_rect);
1820        scene.pop_transform();
1821    }
1822
1823    /// [`ChildPod::paint_child`]'s body, with `visible_rect` as the culling rect.
1824    fn paint_child_in(
1825        &mut self,
1826        ctx: &mut PaintCtx,
1827        scene: &mut dyn PaintScene,
1828        visible_rect: Option<Rect>,
1829    ) {
1830        let child_origin = ctx.origin() + self.origin.to_vec2();
1831        let mut child_ctx = PaintCtx::new(child_origin, self.size);
1832        // Copy the root-threaded globals down unchanged.
1833        child_ctx.set_frame_time(ctx.frame_time());
1834        child_ctx.set_theme(ctx.theme_ref());
1835        child_ctx.set_window_insets(ctx.window_insets_ref());
1836        child_ctx.set_presented_frames(ctx.presented_frames());
1837        child_ctx.set_hero(ctx.hero_ref());
1838        child_ctx.set_visible_rect(visible_rect);
1839        child_ctx.set_translucent(ctx.is_translucent());
1840        child_ctx.shortcut_hints = ctx.shortcut_hints.clone();
1841        child_ctx.set_paint_state(
1842            ctx.paint_epoch,
1843            ctx.retained_layers,
1844            ctx.retained_ledger,
1845            ctx.paint_serial,
1846        );
1847        // Focus: a recorded link, on the live session (paint reaches branches no
1848        // pass clears), under a focused ancestor chain.
1849        child_ctx.set_focus_epoch(ctx.focus_epoch());
1850        child_ctx.set_has_focus(
1851            self.focused && self.focus_epoch == ctx.focus_epoch() && ctx.has_focus(),
1852        );
1853        child_ctx.set_focus_ring(ctx.focus_ring);
1854        // Hover likewise; the chain keeps a bare context's `0` from matching.
1855        child_ctx.set_hover_epoch(ctx.hover_epoch());
1856        child_ctx.set_hovered(self.hover_epoch == ctx.hover_epoch() && ctx.is_hovered());
1857        self.widget.paint(&mut child_ctx, scene);
1858        ctx.paint_publications = ctx
1859            .paint_publications
1860            .saturating_add(child_ctx.paint_publications);
1861        match child_ctx.frame_class() {
1862            Some(TickClass::Transition) => ctx.request_frame_class(TickClass::Transition),
1863            // Not `request_frame_class(CosmeticLoop)`: that folds in
1864            // `Duration::ZERO` and would re-tighten a slower child cadence.
1865            Some(TickClass::CosmeticLoop) => {
1866                ctx.needs_frame = true;
1867                debug_assert!(
1868                    child_ctx.paced_interval().is_some(),
1869                    "CosmeticLoop frame_class with no merged paced_interval — \
1870                     a new caller must be bypassing request_frame_paced_at"
1871                );
1872                ctx.absorb_paced_interval(child_ctx.paced_interval());
1873            }
1874            None => {}
1875        }
1876        if child_ctx.needs_layout() {
1877            ctx.request_layout();
1878        }
1879        // Layout-skip marks: requests and the landing paint dirty the pod; the
1880        // path bit goes up except for paced-only requests, preserving pacing.
1881        let animating = child_ctx.needs_layout() || child_ctx.needs_frame();
1882        let dirtied = animating || self.painted_animating || child_ctx.layout_path_dirty;
1883        if dirtied {
1884            self.layout_dirty = true;
1885        }
1886        // Painted this pass: paint-dirty only while still changing.
1887        self.paint_dirty = dirtied;
1888        self.painted_epoch = ctx.paint_epoch;
1889        let unpaced = child_ctx.needs_layout()
1890            || (child_ctx.needs_frame() && !child_ctx.needs_frame_paced_only());
1891        let landing = self.painted_animating && !animating;
1892        if unpaced || landing || child_ctx.layout_path_dirty {
1893            ctx.layout_path_dirty = true;
1894        }
1895        self.painted_animating = animating;
1896        if let Some(ime) = child_ctx.take_ime_state() {
1897            ctx.publish_ime_state(ime);
1898        }
1899        // Extend, never overwrite: sibling slots and shields must all survive.
1900        ctx.platform_views.extend(child_ctx.take_platform_views());
1901        ctx.input_shields.extend(child_ctx.take_input_shields());
1902    }
1903
1904    /// Collect the child's semantics at its absolute origin (a transformed pod
1905    /// reports its bounding box).
1906    pub fn semantics_child(&self, ctx: &mut SemanticsCtx) {
1907        let base = self.semantics_base(ctx);
1908        let (offset, size) = match self.transform {
1909            None => (self.origin.to_vec2(), self.size),
1910            Some(transform) => {
1911                let bounds = self
1912                    .local_to_container(transform)
1913                    .transform_rect_bbox(self.size.to_rect());
1914                if bounds.is_finite() {
1915                    (bounds.origin().to_vec2(), bounds.size())
1916                } else {
1917                    // A non-finite transform reports an empty frame, not NaN.
1918                    (self.origin.to_vec2(), Size::ZERO)
1919                }
1920            }
1921        };
1922        ctx.descend_into_pod(base, offset, size, |ctx| {
1923            self.widget.semantics(ctx);
1924        });
1925    }
1926
1927    /// This pod's stable semantics base id, assigned on first visit.
1928    fn semantics_base(&self, ctx: &mut SemanticsCtx) -> NonZeroU64 {
1929        match self.semantics_id.get() {
1930            Some(id) => id,
1931            None => {
1932                let id = ctx.alloc_base();
1933                self.semantics_id.set(Some(id));
1934                id
1935            }
1936        }
1937    }
1938
1939    /// Route an event into the child in its local space, folding its flags into
1940    /// `ctx` and recording a capture as the active path. Containers use
1941    /// `frust-widgets`' `route_event` helpers, not an ad-hoc `contains()` gate.
1942    ///
1943    /// A non-claimant contact walks forward-only: pods above the captor hand
1944    /// their child an inert carrier, so no container re-arms its gesture from
1945    /// the wrong finger; the captor receives the real event.
1946    pub fn event_child(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1947        if let Some(real) = crate::event::secondary_walk_event(event) {
1948            return self.walk_secondary(ctx, &real);
1949        }
1950        let local = self.localize(event);
1951        self.dispatch_local(ctx, &local)
1952    }
1953
1954    /// `event` mapped into the child's local space (`-origin` or the inverse).
1955    fn localize(&self, event: &InputEvent) -> InputEvent {
1956        match self.transform {
1957            None => event.translated(-self.origin.to_vec2()),
1958            Some(transform) => {
1959                match crate::hit::checked_inverse(&self.local_to_container(transform)) {
1960                    Some(inverse) => event.transformed(&inverse),
1961                    // No inverse: only a capture/focus path reaches here; deliver
1962                    // it untransformed so that path can still end.
1963                    None => event.translated(-self.origin.to_vec2()),
1964                }
1965            }
1966        }
1967    }
1968
1969    /// One step of a non-claimant contact's forward-only walk.
1970    fn walk_secondary(&mut self, ctx: &mut EventCtx, real: &InputEvent) -> EventResult {
1971        if !self.active {
1972            return EventResult::Ignored;
1973        }
1974        let local = self.localize(real);
1975        let result = if self.contacts_captor {
1976            crate::event::without_secondary_walk(|| self.dispatch_local(ctx, &local))
1977        } else {
1978            let carrier = crate::event::secondary_walk_carrier();
1979            let (_, delivered) = crate::event::run_secondary_walk(local.clone(), || {
1980                self.dispatch_local(ctx, &carrier)
1981            });
1982            match delivered {
1983                Some(result) => result,
1984                None => crate::event::without_secondary_walk(|| self.dispatch_local(ctx, &local)),
1985            }
1986        };
1987        crate::event::note_secondary_delivered(result);
1988        result
1989    }
1990
1991    /// Dispatch `local` to the child widget and fold what it bubbled into `ctx`.
1992    fn dispatch_local(&mut self, ctx: &mut EventCtx, local: &InputEvent) -> EventResult {
1993        let hovered = self.hover_epoch == ctx.hover_epoch() && ctx.is_hovered();
1994        // A capturing pod never lets a hover claim through, and the first
1995        // claim recorded (the topmost claimant's) closes the pass.
1996        let hover_eligible = ctx.is_hover_eligible() && !ctx.is_hover_claimed() && !self.active;
1997        let claim_epoch = ctx.hover_claim_epoch();
1998        let ((opted_in, opted_in_at_or_below), fold, (layout_requested, layout_path_dirty), result) = {
1999            let mut child_ctx = ctx.child_ctx(
2000                self.origin,
2001                self.size,
2002                self.focused,
2003                hovered,
2004                hover_eligible,
2005            );
2006            if let InputEvent::Shortcut(id) = local
2007                && self.takes_focus_grant(*id)
2008            {
2009                child_ctx.request_focus();
2010            }
2011            // Attributes a `capture_contacts` call to this child or below.
2012            let frame = crate::event::ContactFrame::enter(self.active && self.contacts_captor);
2013            let result = self.widget.event(&mut child_ctx, local);
2014            if matches!(local, InputEvent::Shortcut(_)) && result == EventResult::Handled {
2015                crate::shortcuts::note_handled();
2016            }
2017            let (opted_in, opted_in_at_or_below) = frame.close();
2018            (
2019                (opted_in, opted_in_at_or_below),
2020                child_ctx.fold(),
2021                (child_ctx.needs_layout(), child_ctx.is_layout_path_dirty()),
2022                result,
2023            )
2024        };
2025        let (captured, hover_claimed) = (fold.captured, fold.hover_claimed);
2026        let (focus_req, focus_rel, redraw) =
2027            (fold.focus_requested, fold.focus_released, fold.needs_redraw);
2028        // The event pass's layout-skip source, folded into `ctx` as the path
2029        // bit: a broadcast container returns `Ignored` whatever its child did.
2030        let path_dirty = layout_path_dirty
2031            || layout_requested
2032            || redraw
2033            || matches!(result, EventResult::Handled);
2034        if path_dirty {
2035            self.layout_dirty = true;
2036            self.paint_dirty = true;
2037        }
2038        ctx.absorb_layout(layout_requested, path_dirty);
2039        if captured {
2040            if !self.active {
2041                // A fresh capture drops the last gesture's opt-in.
2042                self.contacts_captor = false;
2043                self.contacts_path = false;
2044            }
2045            self.active = true;
2046            self.contacts_captor |= opted_in;
2047            self.contacts_path |= opted_in_at_or_below;
2048        }
2049        // Stamp the claim; a stale stamp is stranded by the next epoch.
2050        if hover_claimed {
2051            self.hover_epoch = claim_epoch;
2052            self.hover_root = ctx.hover_root();
2053            // Stamps land bottom-up, so the first is the claimant's own pod.
2054            if HOVER_CLAIMANT.with(Cell::get).is_none() {
2055                let id = self.inspect_id().0;
2056                HOVER_CLAIMANT.with(|claimant| claimant.set(Some(id)));
2057            }
2058        }
2059        // A focus request wins over a release in one dispatch.
2060        if focus_rel {
2061            self.set_focused(false);
2062        }
2063        if focus_req {
2064            // Bottom-up: the first pod here is the claimant's own.
2065            if FOCUS_REQ_FROM_LIVE.with(Cell::get).is_none() {
2066                let live = self.holds_live_focus();
2067                FOCUS_REQ_FROM_LIVE.with(|c| c.set(Some(live)));
2068            }
2069            self.set_focused(true);
2070        }
2071        ctx.absorb_child(fold);
2072        result
2073    }
2074
2075    /// Whether this pod is the open [`FocusGrant`]'s target for `carrier`; marks it landed.
2076    fn takes_focus_grant(&self, carrier: ShortcutId) -> bool {
2077        let Some(id) = self.inspect_id.get() else {
2078            return false;
2079        };
2080        FOCUS_GRANT.with(|grant| match grant.get() {
2081            Some((open, target, false)) if open == carrier && target == id.get() => {
2082                grant.set(Some((open, target, true)));
2083                true
2084            }
2085            _ => false,
2086        })
2087    }
2088
2089    /// Whether `point` (container space) lies in this child's bounds, half-open
2090    /// (`origin <= p < origin + size`). The initial hit test only.
2091    pub fn contains(&self, point: Point) -> bool {
2092        if let Some(transform) = self.transform {
2093            return crate::hit::point_in_transformed_rect(
2094                point,
2095                self.size.to_rect(),
2096                &self.local_to_container(transform),
2097            );
2098        }
2099        point.x >= self.origin.x
2100            && point.x < self.origin.x + self.size.width
2101            && point.y >= self.origin.y
2102            && point.y < self.origin.y + self.size.height
2103    }
2104}
2105
2106#[cfg(test)]
2107mod focus_link_tests {
2108    use super::*;
2109
2110    /// A guard restoring the focus channel this thread started with, so a test
2111    /// that publishes a session of its own cannot leak it into a neighbour.
2112    struct Session(u64, u64, u64);
2113
2114    impl Session {
2115        fn enter(root: u64, live: u64, claim: u64) -> Self {
2116            let previous = LIVE_FOCUS_SESSION.with(|slot| slot.get());
2117            set_live_focus_session(root, live, claim);
2118            Session(previous.0, previous.1, previous.2)
2119        }
2120    }
2121
2122    impl Drop for Session {
2123        fn drop(&mut self) {
2124            set_live_focus_session(self.0, self.1, self.2);
2125        }
2126    }
2127
2128    /// A do-nothing leaf: these tests exercise the pod's own bookkeeping, so
2129    /// the widget inside it never runs.
2130    struct Inert;
2131
2132    impl Widget for Inert {
2133        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2134            bc.constrain(Size::new(10.0, 10.0))
2135        }
2136        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
2137    }
2138
2139    fn pod() -> ChildPod {
2140        ChildPod::new(Box::new(Inert))
2141    }
2142
2143    #[test]
2144    fn a_recorded_link_counts_only_while_its_stamp_names_the_live_session() {
2145        let _session = Session::enter(7, 3, 3);
2146        let mut pod = pod();
2147        pod.set_focused(true);
2148        assert!(pod.holds_live_focus(), "recorded against the live session");
2149        assert_eq!(pod.focus_epoch(), 3);
2150
2151        // The session moves on. Nothing visits this pod; its flag is untouched.
2152        set_live_focus_session(7, 4, 4);
2153        assert!(
2154            pod.is_focused(),
2155            "the raw record survives, as it always did"
2156        );
2157        assert!(
2158            !pod.holds_live_focus(),
2159            "but it no longer names the session the root has"
2160        );
2161    }
2162
2163    #[test]
2164    fn a_link_of_another_root_never_counts_however_the_epochs_line_up() {
2165        let _session = Session::enter(7, 3, 3);
2166        let mut pod = pod();
2167        pod.set_focused(true);
2168
2169        // A second root on the same thread, with the identical epoch integer —
2170        // which two roots hold as a rule, not as a fluke.
2171        set_live_focus_session(8, 3, 3);
2172        assert!(!pod.holds_live_focus());
2173    }
2174
2175    #[test]
2176    fn a_claim_recorded_during_a_dispatch_counts_before_the_dispatch_commits_it() {
2177        // The root opened a candidate session: reads still answer against the
2178        // live one, and a link recorded now takes the candidate.
2179        let _session = Session::enter(7, 3, 4);
2180        let mut claimed = pod();
2181        claimed.set_focused(true);
2182        assert_eq!(claimed.focus_epoch(), 4, "stamped with the candidate");
2183        assert!(
2184            claimed.holds_live_focus(),
2185            "and observable to the container still unwinding around the claim"
2186        );
2187
2188        let mut standing = pod();
2189        standing.focused = true;
2190        standing.focus_root = 7;
2191        standing.focus_epoch = 3;
2192        assert!(
2193            standing.holds_live_focus(),
2194            "while a link recorded before this dispatch still reads live"
2195        );
2196
2197        // The dispatch commits the claim: the older link is stranded.
2198        set_live_focus_session(7, 4, 4);
2199        assert!(claimed.holds_live_focus());
2200        assert!(!standing.holds_live_focus());
2201    }
2202
2203    #[test]
2204    fn retiring_a_link_drops_only_one_the_live_session_has_already_stranded() {
2205        let _session = Session::enter(7, 3, 3);
2206        let mut pod = pod();
2207        pod.set_focused(true);
2208        assert!(
2209            !pod.retire_stale_focus_link(),
2210            "a live link is never retired by a pass that merely walked past it"
2211        );
2212        assert!(pod.is_focused());
2213
2214        set_live_focus_session(7, 4, 4);
2215        assert!(pod.retire_stale_focus_link(), "a stranded link is dropped");
2216        assert!(
2217            !pod.is_focused(),
2218            "so the raw flag and the stamp tell one story"
2219        );
2220        assert!(
2221            !pod.retire_stale_focus_link(),
2222            "and retiring is idempotent — there is nothing left to drop"
2223        );
2224    }
2225
2226    #[test]
2227    fn a_pod_that_never_held_a_link_reports_nothing_to_retire() {
2228        let _session = Session::enter(7, 3, 3);
2229        let mut pod = pod();
2230        assert!(!pod.holds_live_focus());
2231        assert!(!pod.retire_stale_focus_link());
2232    }
2233}
2234
2235#[cfg(test)]
2236mod tests {
2237    use super::*;
2238    use crate::event::{PointerButton, PointerEvent, PointerPhase};
2239
2240    /// A leaf widget that paints a filled box of a fixed intrinsic size.
2241    struct FixedBox {
2242        intrinsic: Size,
2243    }
2244
2245    impl Widget for FixedBox {
2246        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2247            bc.constrain(self.intrinsic)
2248        }
2249
2250        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
2251            scene.fill_rect(ctx.origin(), ctx.size(), Color::BLACK);
2252        }
2253    }
2254
2255    /// A leaf widget that advances no state but signals it wants another frame
2256    /// on every paint — stands in for an animating widget (e.g. a fling).
2257    struct Animator;
2258
2259    impl Widget for Animator {
2260        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2261            bc.max()
2262        }
2263
2264        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
2265            ctx.request_frame();
2266        }
2267    }
2268
2269    /// A leaf widget whose animation changes its layout: it signals
2270    /// [`PaintCtx::request_layout`] on every paint — stands in for an animating
2271    /// widget that resizes/repositions (e.g. an expanding accordion).
2272    struct LayoutAnimator;
2273
2274    impl Widget for LayoutAnimator {
2275        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2276            bc.max()
2277        }
2278
2279        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
2280            ctx.request_layout();
2281        }
2282    }
2283
2284    /// A leaf widget whose animation is a *pacable* decorative loop: it signals
2285    /// [`PaintCtx::request_frame_paced`] on every paint — stands in for a
2286    /// shimmer/idle-pulse whose cadence the frame gate may throttle.
2287    struct PacedAnimator;
2288
2289    impl Widget for PacedAnimator {
2290        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2291            bc.max()
2292        }
2293
2294        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
2295            ctx.request_frame_paced();
2296        }
2297    }
2298
2299    /// The interval a [`SlowPacedAnimator`] asks for — a ~2Hz caret blink,
2300    /// deliberately far slower than any theme's cosmetic-loop cap.
2301    const SLOW_PACE: Duration = Duration::from_millis(500);
2302
2303    /// A leaf widget whose decorative loop names its own (slow) cadence via
2304    /// [`PaintCtx::request_frame_paced_at`] — stands in for a blinking caret.
2305    struct SlowPacedAnimator;
2306
2307    impl Widget for SlowPacedAnimator {
2308        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2309            bc.max()
2310        }
2311
2312        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
2313            ctx.request_frame_paced_at(SLOW_PACE);
2314        }
2315    }
2316
2317    /// A scene recorder used to assert paint output without any GPU dependency.
2318    #[derive(Default)]
2319    struct RecordingScene {
2320        rects: Vec<(Point, Size)>,
2321        texts: Vec<(Point, String)>,
2322        shaders: Vec<(u64, Rect, f32)>,
2323        scene_textures: Vec<(u64, Rect)>,
2324    }
2325
2326    impl PaintScene for RecordingScene {
2327        fn fill_rect(&mut self, origin: Point, size: Size, _color: Color) {
2328            self.rects.push((origin, size));
2329        }
2330        fn draw_text(&mut self, origin: Point, text: &str) {
2331            self.texts.push((origin, text.to_string()));
2332        }
2333        fn draw_shader(&mut self, program: &ShaderProgram, dest: Rect, time: f32) {
2334            self.shaders.push((program.id(), dest, time));
2335        }
2336        fn draw_scene_texture(&mut self, id: u64, dest: Rect) {
2337            self.scene_textures.push((id, dest));
2338        }
2339    }
2340
2341    /// A leaf widget that records the local position of the last event it saw,
2342    /// mutates a `u32` app state, and optionally captures the pointer on `Down`.
2343    struct Probe {
2344        last_pos: Option<Point>,
2345        capture_on_down: bool,
2346    }
2347
2348    impl Widget for Probe {
2349        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2350            bc.max()
2351        }
2352        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
2353        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
2354            self.last_pos = Some(event.position());
2355            *ctx.state_mut::<u32>() += 1;
2356            ctx.request_redraw();
2357            if self.capture_on_down
2358                && matches!(
2359                    event,
2360                    InputEvent::Pointer(PointerEvent {
2361                        phase: PointerPhase::Down,
2362                        ..
2363                    })
2364                )
2365            {
2366                ctx.capture_pointer();
2367            }
2368            EventResult::Handled
2369        }
2370    }
2371
2372    fn down(x: f64, y: f64) -> InputEvent {
2373        InputEvent::Pointer(PointerEvent {
2374            phase: PointerPhase::Down,
2375            position: Point::new(x, y),
2376            button: PointerButton::Primary,
2377        })
2378    }
2379
2380    /// A leaf that requests focus on `Down`, releases it on Escape, records
2381    /// whether it saw a `Key`/`Ime` event, and reports whether it had focus when
2382    /// the last event arrived.
2383    #[derive(Default)]
2384    struct FocusProbe {
2385        saw_key: bool,
2386        had_focus_on_key: bool,
2387    }
2388
2389    impl Widget for FocusProbe {
2390        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2391            bc.max()
2392        }
2393        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
2394        fn event(&mut self, ctx: &mut EventCtx, event: &crate::event::InputEvent) -> EventResult {
2395            match event {
2396                InputEvent::Pointer(PointerEvent {
2397                    phase: PointerPhase::Down,
2398                    ..
2399                }) => {
2400                    ctx.request_focus();
2401                    EventResult::Handled
2402                }
2403                InputEvent::Key(_) | InputEvent::Ime(_) => {
2404                    self.saw_key = true;
2405                    self.had_focus_on_key = ctx.has_focus();
2406                    EventResult::Handled
2407                }
2408                _ => EventResult::Ignored,
2409            }
2410        }
2411    }
2412
2413    fn key_enter() -> InputEvent {
2414        InputEvent::Key(crate::event::KeyEvent {
2415            key: crate::event::Key::Named(crate::event::NamedKey::Enter),
2416            modifiers: crate::event::Modifiers::default(),
2417            repeat: false,
2418        })
2419    }
2420
2421    #[test]
2422    fn event_child_records_focus_on_request() {
2423        let mut pod = ChildPod::new(Box::new(FocusProbe::default()));
2424        pod.set_origin(Point::new(5.0, 5.0));
2425        assert!(!pod.is_focused());
2426
2427        let mut count = 0u32;
2428        let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::new(50.0, 50.0));
2429        pod.event_child(&mut ctx, &down(6.0, 6.0));
2430
2431        // The child requested focus → the pod records the focus path and it
2432        // bubbles up into the parent context.
2433        assert!(pod.is_focused());
2434        assert!(ctx.is_focus_requested());
2435    }
2436
2437    #[test]
2438    fn event_child_threads_has_focus_into_child() {
2439        // A focused pod seeds `has_focus` on the child dispatch; a Key event
2440        // reaching a focused child sees `has_focus() == true`.
2441        let mut pod = ChildPod::new(Box::new(FocusProbe::default()));
2442        pod.set_focused(true);
2443
2444        let mut count = 0u32;
2445        let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::new(10.0, 10.0));
2446        pod.event_child(&mut ctx, &key_enter());
2447
2448        let probe = pod.widget_mut().downcast_mut::<FocusProbe>().unwrap();
2449        assert!(probe.saw_key);
2450        assert!(probe.had_focus_on_key);
2451    }
2452
2453    #[test]
2454    fn widget_layout_respects_constraints() {
2455        let mut w = FixedBox {
2456            intrinsic: Size::new(1000.0, 1000.0),
2457        };
2458        let mut ctx = LayoutCtx::new();
2459        let bc = BoxConstraints::loose(Size::new(200.0, 100.0));
2460        assert_eq!(w.layout(&mut ctx, &bc), Size::new(200.0, 100.0));
2461    }
2462
2463    #[test]
2464    fn with_window_insets_scopes_and_restores_on_both_contexts() {
2465        use crate::insets::EdgeInsets;
2466        let root = WindowInsets::new(EdgeInsets::new(1.0, 2.0, 3.0, 4.0), EdgeInsets::ZERO);
2467        let scoped = root.consuming(true, true, true, true);
2468
2469        let mut lctx = LayoutCtx::new();
2470        lctx.set_window_insets(root);
2471        let inside = lctx.with_window_insets(scoped, |ctx| ctx.window_insets());
2472        assert_eq!(inside, scoped);
2473        assert_eq!(lctx.window_insets(), root, "layout override restored");
2474
2475        let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
2476        pctx.set_window_insets(root);
2477        let inside = pctx.with_window_insets(scoped, |ctx| ctx.window_insets());
2478        assert_eq!(inside, scoped);
2479        assert_eq!(pctx.window_insets(), root, "paint override restored");
2480    }
2481
2482    #[test]
2483    fn widget_paint_emits_into_scene() {
2484        let mut w = FixedBox {
2485            intrinsic: Size::new(50.0, 20.0),
2486        };
2487        let mut scene = RecordingScene::default();
2488        let mut ctx = PaintCtx::new(Point::new(5.0, 7.0), Size::new(50.0, 20.0));
2489        w.paint(&mut ctx, &mut scene);
2490        assert_eq!(
2491            scene.rects,
2492            vec![(Point::new(5.0, 7.0), Size::new(50.0, 20.0))]
2493        );
2494    }
2495
2496    #[test]
2497    fn widget_paint_emits_shader_into_scene() {
2498        /// A leaf widget that paints a shader quad.
2499        struct ShaderWidget {
2500            program: ShaderProgram,
2501            dest: Rect,
2502            time: f32,
2503        }
2504
2505        impl Widget for ShaderWidget {
2506            fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2507                bc.max()
2508            }
2509
2510            fn paint(&mut self, _ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
2511                scene.draw_shader(&self.program, self.dest, self.time);
2512            }
2513        }
2514
2515        let program = ShaderProgram::new("fn main() {}");
2516        let dest = Rect::new(10.0, 20.0, 100.0, 150.0);
2517        let time = 1.5;
2518        let mut w = ShaderWidget {
2519            program: program.clone(),
2520            dest,
2521            time,
2522        };
2523        let mut scene = RecordingScene::default();
2524        let mut ctx = PaintCtx::new(Point::ZERO, Size::new(200.0, 200.0));
2525        w.paint(&mut ctx, &mut scene);
2526        assert_eq!(scene.shaders.len(), 1);
2527        assert_eq!(scene.shaders[0].0, program.id());
2528        assert_eq!(scene.shaders[0].1, dest);
2529        assert_eq!(scene.shaders[0].2, time);
2530    }
2531
2532    #[test]
2533    fn widget_paint_emits_scene_texture_into_scene() {
2534        /// A leaf widget that paints a scene texture.
2535        struct SceneTextureWidget {
2536            id: u64,
2537            dest: Rect,
2538        }
2539
2540        impl Widget for SceneTextureWidget {
2541            fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2542                bc.max()
2543            }
2544
2545            fn paint(&mut self, _ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
2546                scene.draw_scene_texture(self.id, self.dest);
2547            }
2548        }
2549
2550        let id = 42u64;
2551        let dest = Rect::new(10.0, 20.0, 100.0, 150.0);
2552        let mut w = SceneTextureWidget { id, dest };
2553        let mut scene = RecordingScene::default();
2554        let mut ctx = PaintCtx::new(Point::ZERO, Size::new(200.0, 200.0));
2555        w.paint(&mut ctx, &mut scene);
2556        assert_eq!(scene.scene_textures.len(), 1);
2557        assert_eq!(scene.scene_textures[0].0, id);
2558        assert_eq!(scene.scene_textures[0].1, dest);
2559    }
2560
2561    #[test]
2562    fn downcast_recovers_concrete_widget() {
2563        let mut boxed: Box<dyn Widget> = Box::new(FixedBox {
2564            intrinsic: Size::new(3.0, 4.0),
2565        });
2566        let concrete = boxed.downcast_mut::<FixedBox>().expect("downcast");
2567        assert_eq!(concrete.intrinsic, Size::new(3.0, 4.0));
2568    }
2569
2570    #[test]
2571    fn boxed_widget_delegates_every_pass() {
2572        // The `Box<dyn Widget>: Widget` blanket impl must forward layout/paint/event.
2573        let mut boxed: Box<dyn Widget> = Box::new(Probe {
2574            last_pos: None,
2575            capture_on_down: false,
2576        });
2577        let mut ctx = LayoutCtx::new();
2578        assert_eq!(
2579            boxed.layout(&mut ctx, &BoxConstraints::tight(Size::new(4.0, 5.0))),
2580            Size::new(4.0, 5.0)
2581        );
2582        let mut count = 0u32;
2583        let mut ectx = EventCtx::new(&mut count, Point::ZERO, Size::new(4.0, 5.0));
2584        assert_eq!(
2585            boxed.event(&mut ectx, &down(1.0, 1.0)),
2586            EventResult::Handled
2587        );
2588        assert_eq!(count, 1);
2589    }
2590
2591    #[test]
2592    fn child_pod_layout_and_paint_offset_by_origin() {
2593        let mut pod = ChildPod::new(Box::new(FixedBox {
2594            intrinsic: Size::new(30.0, 10.0),
2595        }));
2596        pod.set_origin(Point::new(12.0, 8.0));
2597
2598        let mut lctx = LayoutCtx::new();
2599        let size = pod.layout_child(&mut lctx, &BoxConstraints::loose(Size::new(100.0, 100.0)));
2600        assert_eq!(size, Size::new(30.0, 10.0));
2601        assert_eq!(pod.size(), Size::new(30.0, 10.0));
2602
2603        // Paint under a container placed at (100, 200): child draws at the sum.
2604        let mut scene = RecordingScene::default();
2605        let mut pctx = PaintCtx::new(Point::new(100.0, 200.0), Size::new(300.0, 300.0));
2606        pod.paint_child(&mut pctx, &mut scene);
2607        assert_eq!(
2608            scene.rects,
2609            vec![(Point::new(112.0, 208.0), Size::new(30.0, 10.0))]
2610        );
2611    }
2612
2613    #[test]
2614    fn child_pod_translates_event_into_child_space() {
2615        let mut pod = ChildPod::new(Box::new(Probe {
2616            last_pos: None,
2617            capture_on_down: false,
2618        }));
2619        pod.set_origin(Point::new(10.0, 20.0));
2620
2621        let mut count = 0u32;
2622        let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::new(200.0, 200.0));
2623        // Event at container-space (25, 35) lands at child-local (15, 15).
2624        let result = pod.event_child(&mut ctx, &down(25.0, 35.0));
2625        assert_eq!(result, EventResult::Handled);
2626        assert_eq!(count, 1); // state mutated through the reborrowed context
2627        let probe = pod.widget_mut().downcast_mut::<Probe>().unwrap();
2628        assert_eq!(probe.last_pos, Some(Point::new(15.0, 15.0)));
2629    }
2630
2631    #[test]
2632    fn child_pod_propagates_capture_flag() {
2633        let mut pod = ChildPod::new(Box::new(Probe {
2634            last_pos: None,
2635            capture_on_down: true,
2636        }));
2637        pod.set_origin(Point::new(5.0, 5.0));
2638        assert!(!pod.is_active());
2639
2640        let mut count = 0u32;
2641        let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::new(50.0, 50.0));
2642        pod.event_child(&mut ctx, &down(6.0, 6.0));
2643
2644        // The child captured → the pod records the active path and the flag
2645        // bubbles up to the parent context.
2646        assert!(pod.is_active());
2647        assert!(ctx.is_pointer_captured());
2648        assert!(ctx.needs_redraw());
2649    }
2650
2651    #[test]
2652    fn child_pod_bubbles_needs_frame_from_child_paint() {
2653        // A non-animating child leaves the parent's frame flag clear.
2654        let mut still = ChildPod::new(Box::new(FixedBox {
2655            intrinsic: Size::new(10.0, 10.0),
2656        }));
2657        let mut lctx = LayoutCtx::new();
2658        still.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
2659        let mut scene = RecordingScene::default();
2660        let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
2661        still.paint_child(&mut pctx, &mut scene);
2662        assert!(!pctx.needs_frame());
2663
2664        // An animating child bubbles its request into the parent context.
2665        let mut anim = ChildPod::new(Box::new(Animator));
2666        anim.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
2667        let mut pctx2 = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
2668        assert!(!pctx2.needs_frame());
2669        anim.paint_child(&mut pctx2, &mut scene);
2670        assert!(pctx2.needs_frame());
2671    }
2672    /// A leaf widget that publishes a fixed [`PlatformViewFrame`] on every
2673    /// paint, unless `should_publish` is false — the `false` arm stands in for
2674    /// a slot that didn't paint this pass (culled subtree), exercising the
2675    /// "no publishers this pass" behavior at the `RenderRoot`
2676    /// level (see `app/tests/platform_views.rs`).
2677    struct PlatformViewProbe {
2678        slot_id: u64,
2679        should_publish: bool,
2680    }
2681
2682    impl Widget for PlatformViewProbe {
2683        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2684            bc.max()
2685        }
2686
2687        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
2688            if self.should_publish {
2689                ctx.publish_platform_view(PlatformViewFrame {
2690                    slot_id: self.slot_id,
2691                    view_type: "dev.frust.Probe".to_string(),
2692                    params_json: String::new(),
2693                    params_generation: 0,
2694                    rect: Rect::from_origin_size(ctx.origin(), ctx.size()),
2695                    clip: None,
2696                    visible: true,
2697                    interactive: false,
2698                    shields: Vec::new(),
2699                    command_index: 0,
2700                });
2701            }
2702        }
2703    }
2704
2705    #[test]
2706    fn scene_builder_command_index_reports_the_live_count() {
2707        let mut scene = frust_scene::Scene::new();
2708        {
2709            let mut builder = frust_scene::SceneBuilder::new(&mut scene);
2710            let paint: &mut dyn PaintScene = &mut builder;
2711            assert_eq!(paint.command_index(), 0);
2712
2713            paint.fill_rect(Point::ZERO, Size::new(4.0, 4.0), Color::BLACK);
2714            assert_eq!(paint.command_index(), 1);
2715
2716            // Structural commands count too: the index is a position in the
2717            // scene's command list, not a tally of what draws.
2718            paint.push_clip(Point::ZERO, Size::new(4.0, 4.0));
2719            paint.pop_clip();
2720            assert_eq!(paint.command_index(), 3);
2721
2722            // A transform push records nothing, so the index stays put.
2723            paint.push_transform(Affine::translate((1.0, 1.0)));
2724            assert_eq!(paint.command_index(), 3);
2725            paint.pop_transform();
2726        }
2727        assert_eq!(scene.commands().len(), 3);
2728    }
2729
2730    #[test]
2731    fn default_command_index_is_zero() {
2732        // A recorder with no command list keeps the defaulted 0, however much
2733        // it is asked to paint.
2734        let mut discard = DiscardScene;
2735        discard.fill_rect(Point::ZERO, Size::new(4.0, 4.0), Color::BLACK);
2736        assert_eq!(discard.command_index(), 0);
2737
2738        let mut recording = RecordingScene::default();
2739        recording.fill_rect(Point::ZERO, Size::new(4.0, 4.0), Color::BLACK);
2740        assert_eq!(recording.command_index(), 0);
2741    }
2742
2743    #[test]
2744    fn child_pod_extends_platform_views_never_overwrites() {
2745        // Two slots publishing across two `paint_child` calls in one pass must
2746        // BOTH survive, in paint order — the regression test for the
2747        // Option-overwrite hazard: an ime_state-shaped merge here would leave
2748        // only the second slot's frame (see `PaintCtx::publish_platform_view`'s
2749        // doc comment).
2750        let mut lctx = LayoutCtx::new();
2751
2752        let mut first = ChildPod::new(Box::new(PlatformViewProbe {
2753            slot_id: 1,
2754            should_publish: true,
2755        }));
2756        first.set_origin(Point::new(0.0, 0.0));
2757        first.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
2758
2759        let mut second = ChildPod::new(Box::new(PlatformViewProbe {
2760            slot_id: 2,
2761            should_publish: true,
2762        }));
2763        second.set_origin(Point::new(20.0, 0.0));
2764        second.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
2765
2766        let mut scene = RecordingScene::default();
2767        let mut pctx = PaintCtx::new(Point::ZERO, Size::new(100.0, 100.0));
2768        first.paint_child(&mut pctx, &mut scene);
2769        second.paint_child(&mut pctx, &mut scene);
2770
2771        let frames = pctx.take_platform_views();
2772        assert_eq!(
2773            frames.len(),
2774            2,
2775            "both slots' frames must survive, not just the last-painted one"
2776        );
2777        assert_eq!(frames[0].slot_id, 1);
2778        assert_eq!(frames[1].slot_id, 2);
2779    }
2780
2781    /// A container widget wrapping a single child pod at a fixed offset —
2782    /// stands in for `frust-widgets::Padding` to test that a published frame's
2783    /// rect compounds correctly under nesting rather than staying local to the
2784    /// innermost pod.
2785    struct TranslatingWrapper {
2786        child: ChildPod,
2787    }
2788
2789    impl Widget for TranslatingWrapper {
2790        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2791            self.child.layout_child(ctx, bc)
2792        }
2793
2794        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
2795            self.child.paint_child(ctx, scene);
2796        }
2797    }
2798
2799    #[test]
2800    fn platform_view_frame_rect_is_absolute_under_nested_translation() {
2801        // Wrap a publishing probe under two levels of translation — an inner
2802        // pod at (5, 7) inside a wrapper placed at (100, 200) — the published
2803        // rect must land in ABSOLUTE window coordinates (the same space
2804        // `PaintCtx::report_hero` callers use), not local to either level.
2805        let mut lctx = LayoutCtx::new();
2806
2807        let inner = ChildPod::new(Box::new(PlatformViewProbe {
2808            slot_id: 9,
2809            should_publish: true,
2810        }));
2811        let mut wrapper = TranslatingWrapper { child: inner };
2812        wrapper.child.set_origin(Point::new(5.0, 7.0));
2813        wrapper
2814            .child
2815            .layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
2816
2817        let mut outer = ChildPod::new(Box::new(wrapper));
2818        outer.set_origin(Point::new(100.0, 200.0));
2819        outer.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
2820
2821        let mut scene = RecordingScene::default();
2822        let mut pctx = PaintCtx::new(Point::ZERO, Size::new(400.0, 400.0));
2823        outer.paint_child(&mut pctx, &mut scene);
2824
2825        let frames = pctx.take_platform_views();
2826        assert_eq!(frames.len(), 1);
2827        assert_eq!(
2828            frames[0].rect,
2829            Rect::from_origin_size(Point::new(105.0, 207.0), Size::new(10.0, 10.0))
2830        );
2831    }
2832
2833    #[test]
2834    fn paint_ctx_origin_is_absolute_through_nested_offsets() {
2835        // Nest a leaf widget under two levels of offset containers: the leaf
2836        // must observe the summed absolute window-space origin, not a
2837        // parent-relative one. This pins the accumulation contract that external
2838        // design systems (anchored overlays) depend on.
2839        use std::cell::Cell;
2840
2841        struct OriginRecorder {
2842            recorded_origin: Cell<Option<Point>>,
2843        }
2844
2845        impl Widget for OriginRecorder {
2846            fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2847                bc.max()
2848            }
2849
2850            fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
2851                self.recorded_origin.set(Some(ctx.origin()));
2852            }
2853        }
2854
2855        let mut lctx = LayoutCtx::new();
2856
2857        // Inner recorder at (20, 30) relative to middle container.
2858        let recorder = OriginRecorder {
2859            recorded_origin: Cell::new(None),
2860        };
2861        let inner = ChildPod::new(Box::new(recorder));
2862        let mut middle = TranslatingWrapper { child: inner };
2863        middle.child.set_origin(Point::new(20.0, 30.0));
2864        middle
2865            .child
2866            .layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
2867
2868        // Middle container at (50, 70) relative to outer.
2869        let mut outer = ChildPod::new(Box::new(middle));
2870        outer.set_origin(Point::new(50.0, 70.0));
2871        outer.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
2872
2873        // Paint from root at (0, 0).
2874        let mut scene = RecordingScene::default();
2875        let mut pctx = PaintCtx::new(Point::ZERO, Size::new(400.0, 400.0));
2876        outer.paint_child(&mut pctx, &mut scene);
2877
2878        // Verify: leaf's origin must be sum of all offsets:
2879        // root(0,0) + outer(50,70) + middle(20,30) = (70, 100).
2880        let leaf = outer
2881            .widget_mut()
2882            .downcast_mut::<TranslatingWrapper>()
2883            .unwrap()
2884            .child
2885            .widget_mut()
2886            .downcast_mut::<OriginRecorder>()
2887            .unwrap();
2888        assert_eq!(
2889            leaf.recorded_origin.get(),
2890            Some(Point::new(70.0, 100.0)),
2891            "leaf must observe summed absolute origin"
2892        );
2893    }
2894
2895    #[test]
2896    fn request_frame_alone_does_not_set_needs_layout() {
2897        // A paint-only animation (request_frame, no request_layout) must leave
2898        // `needs_layout` clear — the mobile intra-frame layout-skip depends on this.
2899        let mut anim = ChildPod::new(Box::new(Animator));
2900        let mut lctx = LayoutCtx::new();
2901        anim.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
2902        let mut scene = RecordingScene::default();
2903        let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
2904        anim.paint_child(&mut pctx, &mut scene);
2905        assert!(pctx.needs_frame(), "request_frame sets needs_frame");
2906        assert!(
2907            !pctx.needs_layout(),
2908            "request_frame alone must NOT set needs_layout"
2909        );
2910    }
2911
2912    #[test]
2913    fn request_layout_implies_needs_frame() {
2914        // `request_layout` also sets `needs_frame` so one call per
2915        // animating-layout frame suffices.
2916        let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
2917        assert!(!ctx.needs_frame());
2918        assert!(!ctx.needs_layout());
2919        ctx.request_layout();
2920        assert!(ctx.needs_layout());
2921        assert!(ctx.needs_frame());
2922    }
2923
2924    #[test]
2925    fn child_pod_bubbles_needs_layout_from_child_paint() {
2926        // A non-layout-animating child leaves the parent's layout flag clear.
2927        let mut still = ChildPod::new(Box::new(FixedBox {
2928            intrinsic: Size::new(10.0, 10.0),
2929        }));
2930        let mut lctx = LayoutCtx::new();
2931        still.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
2932        let mut scene = RecordingScene::default();
2933        let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
2934        still.paint_child(&mut pctx, &mut scene);
2935        assert!(!pctx.needs_layout());
2936
2937        // A layout-animating child bubbles its request into the parent context.
2938        let mut anim = ChildPod::new(Box::new(LayoutAnimator));
2939        anim.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
2940        let mut pctx2 = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
2941        assert!(!pctx2.needs_layout());
2942        anim.paint_child(&mut pctx2, &mut scene);
2943        assert!(pctx2.needs_layout());
2944        // Bubbling `request_layout` also carries the implied `needs_frame`.
2945        assert!(pctx2.needs_frame());
2946    }
2947
2948    #[test]
2949    fn needs_layout_bubbles_through_nested_containers() {
2950        // A container holding a single `ChildPod` forwards paint via
2951        // `paint_child`; a layout-animating leaf two levels deep must still
2952        // surface `needs_layout` at the outermost paint context.
2953        struct SingleChildContainer {
2954            child: ChildPod,
2955        }
2956        impl Widget for SingleChildContainer {
2957            fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2958                self.child.layout_child(ctx, bc)
2959            }
2960            fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
2961                self.child.paint_child(ctx, scene);
2962            }
2963        }
2964
2965        let inner = SingleChildContainer {
2966            child: ChildPod::new(Box::new(LayoutAnimator)),
2967        };
2968        let mut outer = ChildPod::new(Box::new(SingleChildContainer {
2969            child: ChildPod::new(Box::new(inner)),
2970        }));
2971        let mut lctx = LayoutCtx::new();
2972        outer.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
2973        let mut scene = RecordingScene::default();
2974        let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
2975        outer.paint_child(&mut pctx, &mut scene);
2976        assert!(
2977            pctx.needs_layout(),
2978            "needs_layout bubbles up nested containers"
2979        );
2980        assert!(pctx.needs_frame());
2981    }
2982
2983    /// A single-`ChildPod` container that forwards paint via `paint_child`,
2984    /// reused by the tick-class bubbling tests below.
2985    struct SingleChildContainer {
2986        child: ChildPod,
2987    }
2988    impl Widget for SingleChildContainer {
2989        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
2990            self.child.layout_child(ctx, bc)
2991        }
2992        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
2993            self.child.paint_child(ctx, scene);
2994        }
2995    }
2996
2997    #[test]
2998    fn no_request_yields_no_frame_class() {
2999        // No frame requested at all → `frame_class()` is `None` and the frame is
3000        // not paced-only (as today: nothing to schedule).
3001        let ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3002        assert!(ctx.frame_class().is_none());
3003        assert!(!ctx.needs_frame());
3004        assert!(!ctx.needs_frame_paced_only());
3005    }
3006
3007    #[test]
3008    fn request_frame_is_transition_unpaced() {
3009        // The unchanged `request_frame` is a Transition request: it must NOT be
3010        // paced-only, preserving today's every-vsync behavior for existing callers.
3011        let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3012        ctx.request_frame();
3013        assert!(ctx.needs_frame());
3014        assert_eq!(ctx.frame_class(), Some(TickClass::Transition));
3015        assert!(
3016            !ctx.needs_frame_paced_only(),
3017            "request_frame stays unpaced (Transition), unchanged behavior"
3018        );
3019    }
3020
3021    #[test]
3022    fn request_frame_paced_is_cosmetic_loop() {
3023        let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3024        ctx.request_frame_paced();
3025        assert!(ctx.needs_frame());
3026        assert_eq!(ctx.frame_class(), Some(TickClass::CosmeticLoop));
3027        assert!(ctx.needs_frame_paced_only());
3028        // The bare form names no interval: `Duration::ZERO` is "at the theme's
3029        // own cosmetic rate" — unchanged behavior for all six shimmer widgets.
3030        assert_eq!(ctx.paced_interval(), Some(Duration::ZERO));
3031    }
3032
3033    #[test]
3034    fn request_frame_paced_at_carries_its_interval() {
3035        // The explicit form is the same class, only slower: a ~500ms caret.
3036        let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3037        ctx.request_frame_paced_at(Duration::from_millis(500));
3038        assert!(ctx.needs_frame());
3039        assert_eq!(ctx.frame_class(), Some(TickClass::CosmeticLoop));
3040        assert!(ctx.needs_frame_paced_only());
3041        assert_eq!(ctx.paced_interval(), Some(Duration::from_millis(500)));
3042    }
3043
3044    #[test]
3045    fn request_frame_paced_at_clamps_past_the_ten_second_ceiling() {
3046        // Past the ceiling: clamps DOWN to it rather than carrying the raw
3047        // caller value out to the shell's pacing arithmetic unbounded.
3048        let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3049        ctx.request_frame_paced_at(Duration::from_secs(10) + Duration::from_secs(1));
3050        assert_eq!(ctx.paced_interval(), Some(PaintCtx::MAX_PACED_INTERVAL));
3051    }
3052
3053    #[test]
3054    fn request_frame_paced_at_leaves_a_real_cadence_untouched() {
3055        // Every shipped cadence (the ~500ms caret above, muxr's ~550ms blink)
3056        // sits nowhere near the ceiling and must pass through byte-for-byte —
3057        // the clamp must never change behavior for any existing caller.
3058        let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3059        ctx.request_frame_paced_at(Duration::from_millis(550));
3060        assert_eq!(ctx.paced_interval(), Some(Duration::from_millis(550)));
3061    }
3062
3063    #[test]
3064    fn paced_intervals_aggregate_on_the_min_lattice() {
3065        // Two paced requests at different rates in one pass: the TIGHTEST wins,
3066        // so both are honored (the slower one is merely repainted more often
3067        // than it asked — see `request_frame_paced_at`'s contract).
3068        let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3069        ctx.request_frame_paced_at(Duration::from_millis(500));
3070        ctx.request_frame_paced_at(Duration::from_millis(33));
3071        assert_eq!(ctx.paced_interval(), Some(Duration::from_millis(33)));
3072        assert!(ctx.needs_frame_paced_only());
3073
3074        // Order-independent (a lattice fold, not a last-writer-wins slot).
3075        let mut reversed = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3076        reversed.request_frame_paced_at(Duration::from_millis(33));
3077        reversed.request_frame_paced_at(Duration::from_millis(500));
3078        assert_eq!(reversed.paced_interval(), Some(Duration::from_millis(33)));
3079
3080        // A bare `request_frame_paced` (the theme cap, `Duration::ZERO`) is the
3081        // absorbing element: a 30Hz shimmer beside a 2Hz caret paces at 30Hz.
3082        let mut shimmer_and_caret = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3083        shimmer_and_caret.request_frame_paced_at(Duration::from_millis(500));
3084        shimmer_and_caret.request_frame_paced();
3085        assert_eq!(shimmer_and_caret.paced_interval(), Some(Duration::ZERO));
3086    }
3087
3088    #[test]
3089    fn request_frame_class_cosmetic_matches_the_bare_paced_request() {
3090        // The general form must stay interchangeable with `request_frame_paced`
3091        // (the widgets' `cull_pacing`/`pacing_integration` suites drive it).
3092        let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3093        ctx.request_frame_class(TickClass::CosmeticLoop);
3094        assert_eq!(ctx.frame_class(), Some(TickClass::CosmeticLoop));
3095        assert_eq!(ctx.paced_interval(), Some(Duration::ZERO));
3096    }
3097
3098    #[test]
3099    fn no_paced_request_names_no_interval() {
3100        // Nothing requested, and a Transition-only request, both leave the
3101        // interval unset — there is no paced loop to pace.
3102        let ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3103        assert_eq!(ctx.paced_interval(), None);
3104        let mut transition = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3105        transition.request_frame();
3106        assert_eq!(transition.paced_interval(), None);
3107    }
3108
3109    #[test]
3110    fn transition_dominates_cosmetic_regardless_of_order() {
3111        // Paced then Transition → unpaced.
3112        let mut a = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3113        a.request_frame_paced();
3114        a.request_frame();
3115        assert_eq!(a.frame_class(), Some(TickClass::Transition));
3116        assert!(!a.needs_frame_paced_only());
3117
3118        // Transition then paced → still unpaced (a CosmeticLoop never clears it).
3119        let mut b = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3120        b.request_frame();
3121        b.request_frame_paced();
3122        assert_eq!(b.frame_class(), Some(TickClass::Transition));
3123        assert!(!b.needs_frame_paced_only());
3124    }
3125
3126    #[test]
3127    fn request_layout_implies_transition_class() {
3128        // `request_layout` is user-visible motion, so it re-forces the unpaced
3129        // Transition class even if a paced request preceded it.
3130        let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3131        ctx.request_frame_paced();
3132        assert!(ctx.needs_frame_paced_only());
3133        ctx.request_layout();
3134        assert_eq!(ctx.frame_class(), Some(TickClass::Transition));
3135        assert!(
3136            !ctx.needs_frame_paced_only(),
3137            "request_layout implies Transition, leaving the frame unpaced"
3138        );
3139    }
3140
3141    #[test]
3142    fn child_pod_bubbles_paced_class_from_child_paint() {
3143        // A purely-cosmetic child bubbles a paced request into the parent — the
3144        // parent's aggregate stays paced-only.
3145        let mut anim = ChildPod::new(Box::new(PacedAnimator));
3146        let mut lctx = LayoutCtx::new();
3147        anim.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
3148        let mut scene = RecordingScene::default();
3149        let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3150        anim.paint_child(&mut pctx, &mut scene);
3151        assert!(pctx.needs_frame());
3152        assert_eq!(pctx.frame_class(), Some(TickClass::CosmeticLoop));
3153        assert!(pctx.needs_frame_paced_only());
3154    }
3155
3156    #[test]
3157    fn paced_class_bubbles_through_nested_containers() {
3158        // A cosmetic-loop leaf two levels deep must still surface as paced-only
3159        // at the outermost paint context (the bubbling identity of the lattice).
3160        let inner = SingleChildContainer {
3161            child: ChildPod::new(Box::new(PacedAnimator)),
3162        };
3163        let mut outer = ChildPod::new(Box::new(SingleChildContainer {
3164            child: ChildPod::new(Box::new(inner)),
3165        }));
3166        let mut lctx = LayoutCtx::new();
3167        outer.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
3168        let mut scene = RecordingScene::default();
3169        let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3170        outer.paint_child(&mut pctx, &mut scene);
3171        assert!(pctx.needs_frame());
3172        assert!(
3173            pctx.needs_frame_paced_only(),
3174            "a nested cosmetic-loop leaf stays paced-only up the tree"
3175        );
3176    }
3177
3178    #[test]
3179    fn child_pod_bubbles_a_named_interval_unchanged() {
3180        // A slow caret nested under a container must reach the root with its own
3181        // interval intact — bubbling must never re-tighten it to the theme cap
3182        // (which a blanket `request_frame_class(CosmeticLoop)` forward would).
3183        let mut outer = ChildPod::new(Box::new(SingleChildContainer {
3184            child: ChildPod::new(Box::new(SlowPacedAnimator)),
3185        }));
3186        let mut lctx = LayoutCtx::new();
3187        outer.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
3188        let mut scene = RecordingScene::default();
3189        let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3190        outer.paint_child(&mut pctx, &mut scene);
3191        assert!(pctx.needs_frame_paced_only());
3192        assert_eq!(
3193            pctx.paced_interval(),
3194            Some(SLOW_PACE),
3195            "a nested slow paced loop keeps its own cadence up the tree"
3196        );
3197    }
3198
3199    #[test]
3200    fn absorb_paced_interval_skips_on_none_without_tightening_to_zero() {
3201        // The rule shared by both paced-interval absorb sites
3202        // (`ChildPod::paint_child`, `with_hero_registry`): a `None` interval
3203        // leaves the aggregate untouched rather than defaulting to
3204        // `Duration::ZERO`, the most-tightening value.
3205        //
3206        // Exercised directly against the fold: driving this state through
3207        // `paint_child` would trip its `debug_assert!` instead, since the
3208        // public `request_frame_paced*` API cannot produce it.
3209        let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3210        ctx.absorb_paced_interval(None);
3211        assert_eq!(
3212            ctx.paced_interval(),
3213            None,
3214            "a None interval must not tighten the aggregate to Duration::ZERO"
3215        );
3216
3217        // A pre-existing aggregate is likewise untouched by a `None` fold.
3218        let mut ctx2 = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3219        ctx2.request_frame_paced_at(Duration::from_millis(500));
3220        ctx2.absorb_paced_interval(None);
3221        assert_eq!(
3222            ctx2.paced_interval(),
3223            Some(Duration::from_millis(500)),
3224            "a None fold must not override an already-merged interval either"
3225        );
3226    }
3227
3228    #[test]
3229    fn sibling_paced_intervals_fold_to_the_tightest() {
3230        // A shimmer (theme cap) beside a slow caret under one container: the
3231        // container's aggregate paces at the shimmer's rate. The caret is then
3232        // repainted more often than it asked — no visual harm, by design.
3233        struct TwoChildContainer {
3234            a: ChildPod,
3235            b: ChildPod,
3236        }
3237        impl Widget for TwoChildContainer {
3238            fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3239                self.a.layout_child(ctx, bc);
3240                self.b.layout_child(ctx, bc)
3241            }
3242            fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
3243                self.a.paint_child(ctx, scene);
3244                self.b.paint_child(ctx, scene);
3245            }
3246        }
3247
3248        let mut outer = ChildPod::new(Box::new(TwoChildContainer {
3249            a: ChildPod::new(Box::new(SlowPacedAnimator)),
3250            b: ChildPod::new(Box::new(PacedAnimator)),
3251        }));
3252        let mut lctx = LayoutCtx::new();
3253        outer.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
3254        let mut scene = RecordingScene::default();
3255        let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3256        outer.paint_child(&mut pctx, &mut scene);
3257        assert!(pctx.needs_frame_paced_only());
3258        assert_eq!(
3259            pctx.paced_interval(),
3260            Some(Duration::ZERO),
3261            "the tightest sibling request (the theme cap) wins the fold"
3262        );
3263    }
3264
3265    #[test]
3266    fn mixed_sibling_requests_aggregate_to_unpaced() {
3267        // Two sibling children under one container: one paced, one Transition.
3268        // The container's aggregate must be unpaced (any Transition dominates).
3269        struct TwoChildContainer {
3270            a: ChildPod,
3271            b: ChildPod,
3272        }
3273        impl Widget for TwoChildContainer {
3274            fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3275                self.a.layout_child(ctx, bc);
3276                self.b.layout_child(ctx, bc)
3277            }
3278            fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
3279                self.a.paint_child(ctx, scene);
3280                self.b.paint_child(ctx, scene);
3281            }
3282        }
3283
3284        let mut outer = ChildPod::new(Box::new(TwoChildContainer {
3285            a: ChildPod::new(Box::new(PacedAnimator)),
3286            b: ChildPod::new(Box::new(Animator)),
3287        }));
3288        let mut lctx = LayoutCtx::new();
3289        outer.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
3290        let mut scene = RecordingScene::default();
3291        let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3292        outer.paint_child(&mut pctx, &mut scene);
3293        assert!(pctx.needs_frame());
3294        assert_eq!(pctx.frame_class(), Some(TickClass::Transition));
3295        assert!(
3296            !pctx.needs_frame_paced_only(),
3297            "a mixed paced+transition sibling set aggregates to unpaced"
3298        );
3299    }
3300
3301    #[test]
3302    fn with_hero_registry_bubbles_paced_class() {
3303        // The hero-reporter sub-context absorbs a paced request the same way it
3304        // absorbs `needs_frame`/`needs_layout`.
3305        let registry = RefCell::new(HeroFrames::default());
3306        let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3307        ctx.with_hero_registry(&registry, |child| {
3308            child.request_frame_paced();
3309        });
3310        assert!(ctx.needs_frame());
3311        assert!(
3312            ctx.needs_frame_paced_only(),
3313            "with_hero_registry bubbles the paced class up"
3314        );
3315
3316        // A Transition inside the closure dominates the outer aggregate too.
3317        let mut ctx2 = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3318        ctx2.request_frame_paced();
3319        ctx2.with_hero_registry(&registry, |child| {
3320            child.request_frame();
3321        });
3322        assert_eq!(ctx2.frame_class(), Some(TickClass::Transition));
3323        assert!(!ctx2.needs_frame_paced_only());
3324
3325        // The interval half of the absorb: a named interval inside surfaces
3326        // outside, and folds on the MIN-lattice with an outer request.
3327        let mut ctx3 = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3328        ctx3.with_hero_registry(&registry, |child| {
3329            child.request_frame_paced_at(SLOW_PACE);
3330        });
3331        assert_eq!(ctx3.paced_interval(), Some(SLOW_PACE));
3332        ctx3.request_frame_paced();
3333        assert_eq!(
3334            ctx3.paced_interval(),
3335            Some(Duration::ZERO),
3336            "the outer theme-cap request tightens the folded aggregate"
3337        );
3338    }
3339
3340    #[test]
3341    fn child_pod_paint_seeds_has_focus_composed_with_ancestor() {
3342        use std::cell::Cell;
3343        use std::rc::Rc;
3344
3345        // A probe recording what `ctx.has_focus()` it observed during paint.
3346        struct FocusProbe(Rc<Cell<Option<bool>>>);
3347        impl Widget for FocusProbe {
3348            fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3349                bc.constrain(Size::new(10.0, 10.0))
3350            }
3351            fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
3352                self.0.set(Some(ctx.has_focus()));
3353            }
3354        }
3355
3356        let seen = Rc::new(Cell::new(None));
3357        let mut pod = ChildPod::new(Box::new(FocusProbe(seen.clone())));
3358        let mut lctx = LayoutCtx::new();
3359        pod.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
3360        let mut scene = RecordingScene::default();
3361
3362        // Focused pod under a focused ancestor: the child observes focus.
3363        pod.set_focused(true);
3364        let mut focused_parent = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3365        focused_parent.set_has_focus(true);
3366        pod.paint_child(&mut focused_parent, &mut scene);
3367        assert_eq!(seen.get(), Some(true));
3368
3369        // Focused pod under a BLURRED ancestor (the stale-deep-flag case): the
3370        // cleared ancestor link must force `has_focus == false` below it.
3371        let mut blurred_parent = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3372        blurred_parent.set_has_focus(false);
3373        pod.paint_child(&mut blurred_parent, &mut scene);
3374        assert_eq!(seen.get(), Some(false));
3375
3376        // Unfocused pod under a focused ancestor stays unfocused.
3377        pod.set_focused(false);
3378        let mut focused_parent2 = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3379        focused_parent2.set_has_focus(true);
3380        pod.paint_child(&mut focused_parent2, &mut scene);
3381        assert_eq!(seen.get(), Some(false));
3382    }
3383
3384    #[test]
3385    fn child_pod_stamps_a_hover_claim_and_seeds_it_by_epoch() {
3386        use std::cell::Cell;
3387        use std::rc::Rc;
3388
3389        /// A probe that claims hover on every event it receives and records what
3390        /// `is_hovered()` each pass reported.
3391        struct HoverProbe {
3392            event_seen: Rc<Cell<Option<bool>>>,
3393            paint_seen: Rc<Cell<Option<bool>>>,
3394        }
3395        impl Widget for HoverProbe {
3396            fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3397                bc.constrain(Size::new(10.0, 10.0))
3398            }
3399            fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
3400                self.paint_seen.set(Some(ctx.is_hovered()));
3401            }
3402            fn event(&mut self, ctx: &mut EventCtx, _event: &InputEvent) -> EventResult {
3403                self.event_seen.set(Some(ctx.is_hovered()));
3404                ctx.claim_hover();
3405                EventResult::Ignored
3406            }
3407        }
3408
3409        let event_seen = Rc::new(Cell::new(None));
3410        let paint_seen = Rc::new(Cell::new(None));
3411        let mut pod = ChildPod::new(Box::new(HoverProbe {
3412            event_seen: event_seen.clone(),
3413            paint_seen: paint_seen.clone(),
3414        }));
3415        let mut lctx = LayoutCtx::new();
3416        pod.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
3417        let mut scene = RecordingScene::default();
3418        let mut state = ();
3419        let event = InputEvent::Pointer(PointerEvent {
3420            phase: PointerPhase::Move,
3421            position: Point::new(5.0, 5.0),
3422            button: PointerButton::Primary,
3423        });
3424
3425        // A never-claimed pod carries no stamp, and reads unhovered against any
3426        // live epoch (the root's starts past `0` for exactly this reason).
3427        assert_eq!(pod.hover_epoch(), 0);
3428
3429        // Dispatch on an INELIGIBLE pass (the default): the claim records nothing.
3430        {
3431            let mut ctx = EventCtx::new(&mut state as &mut dyn Any, Point::ZERO, pod.size());
3432            ctx.set_hover_epoch(4);
3433            pod.event_child(&mut ctx, &event);
3434            assert!(!ctx.is_hover_claimed(), "nothing bubbled up");
3435        }
3436        assert_eq!(pod.hover_epoch(), 0, "no stamp from an ineligible pass");
3437
3438        // Dispatch on an eligible pass: the pod is stamped with the *next* epoch
3439        // (the root advances its own when the pass ends) and the claim bubbles.
3440        {
3441            let mut ctx = EventCtx::new(&mut state as &mut dyn Any, Point::ZERO, pod.size());
3442            ctx.set_hover_epoch(4);
3443            ctx.set_hovered(true);
3444            ctx.set_hover_eligible(true);
3445            pod.event_child(&mut ctx, &event);
3446            assert!(ctx.is_hover_claimed());
3447            assert_eq!(
3448                event_seen.get(),
3449                Some(false),
3450                "the pod held no link going into this pass"
3451            );
3452        }
3453        assert_eq!(pod.hover_epoch(), 5);
3454
3455        // Paint against the epoch the claim recorded: hovered, and the ancestor
3456        // chain is ANDed in exactly like focus.
3457        let mut live = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3458        live.set_hover_epoch(5);
3459        live.set_hovered(true);
3460        pod.paint_child(&mut live, &mut scene);
3461        assert_eq!(paint_seen.get(), Some(true));
3462
3463        // Same stamp under an UNHOVERED ancestor: the chain wins.
3464        let mut unhovered_parent = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3465        unhovered_parent.set_hover_epoch(5);
3466        unhovered_parent.set_hovered(false);
3467        pod.paint_child(&mut unhovered_parent, &mut scene);
3468        assert_eq!(paint_seen.get(), Some(false));
3469
3470        // The epoch moves on (the pointer went elsewhere): the stamp is stranded
3471        // with nobody clearing it — the whole point of stamping rather than
3472        // flagging.
3473        let mut later = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3474        later.set_hover_epoch(6);
3475        later.set_hovered(true);
3476        pod.paint_child(&mut later, &mut scene);
3477        assert_eq!(paint_seen.get(), Some(false));
3478
3479        // A pod holding the capture path refuses a claim outright, whatever the
3480        // pass says — a drag never paints hover under the pointer.
3481        pod.set_active(true);
3482        {
3483            let mut ctx = EventCtx::new(&mut state as &mut dyn Any, Point::ZERO, pod.size());
3484            ctx.set_hover_epoch(6);
3485            ctx.set_hover_eligible(true);
3486            pod.event_child(&mut ctx, &event);
3487            assert!(!ctx.is_hover_claimed());
3488        }
3489        assert_eq!(pod.hover_epoch(), 5, "the captured pod kept its old stamp");
3490    }
3491
3492    /// Two overlapping children can both hit-test a point (a Stack, or any
3493    /// container whose topmost child ignores the move). Only the first claim in
3494    /// **dispatch order** is recorded, which is the primitive; "the topmost
3495    /// claimant wins" is the consequence of dispatching topmost-first *and* of
3496    /// every container claiming after it routes (see `EventCtx::claim_hover`),
3497    /// not a rule this level enforces. Either way, no pass can leave two widgets
3498    /// hovered.
3499    #[test]
3500    fn only_the_first_hover_claim_in_a_pass_is_recorded() {
3501        struct Claimer;
3502        impl Widget for Claimer {
3503            fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3504                bc.constrain(Size::new(10.0, 10.0))
3505            }
3506            fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
3507            fn event(&mut self, ctx: &mut EventCtx, _event: &InputEvent) -> EventResult {
3508                ctx.claim_hover();
3509                EventResult::Ignored
3510            }
3511        }
3512
3513        let mut first = ChildPod::new(Box::new(Claimer));
3514        let mut second = ChildPod::new(Box::new(Claimer));
3515        let mut state = ();
3516        let event = InputEvent::Pointer(PointerEvent {
3517            phase: PointerPhase::Move,
3518            position: Point::new(5.0, 5.0),
3519            button: PointerButton::Primary,
3520        });
3521        let mut ctx = EventCtx::new(&mut state as &mut dyn Any, Point::ZERO, Size::ZERO);
3522        ctx.set_hover_epoch(2);
3523        ctx.set_hover_eligible(true);
3524
3525        first.event_child(&mut ctx, &event);
3526        second.event_child(&mut ctx, &event);
3527        assert_eq!(first.hover_epoch(), 3, "the first claim is recorded");
3528        assert_eq!(
3529            second.hover_epoch(),
3530            0,
3531            "the pass was closed to further claims"
3532        );
3533    }
3534
3535    #[test]
3536    fn child_pod_contains_uses_container_space_bounds() {
3537        let mut pod = ChildPod::new(Box::new(FixedBox {
3538            intrinsic: Size::ZERO,
3539        }));
3540        pod.set_origin(Point::new(10.0, 10.0));
3541        let mut lctx = LayoutCtx::new();
3542        pod.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(20.0, 20.0)));
3543
3544        assert!(pod.contains(Point::new(10.0, 10.0))); // top-left inclusive
3545        assert!(pod.contains(Point::new(29.9, 29.9)));
3546        assert!(!pod.contains(Point::new(30.0, 30.0))); // bottom-right exclusive
3547        assert!(!pod.contains(Point::new(9.9, 15.0)));
3548    }
3549
3550    /// A scene recorder that overrides the shadow/layer `PaintScene` additions, to
3551    /// prove they reach an implementing scene that opts in.
3552    #[derive(Default)]
3553    struct ShadowLayerRecordingScene {
3554        shadows: Vec<(Point, Size, f64, f64, Color)>,
3555        layers: Vec<(Point, Size, f32)>,
3556        pops: u32,
3557    }
3558
3559    impl PaintScene for ShadowLayerRecordingScene {
3560        fn fill_rect(&mut self, _origin: Point, _size: Size, _color: Color) {}
3561        fn draw_text(&mut self, _origin: Point, _text: &str) {}
3562
3563        fn draw_shadow(
3564            &mut self,
3565            origin: Point,
3566            size: Size,
3567            radius: f64,
3568            std_dev: f64,
3569            color: Color,
3570        ) {
3571            self.shadows.push((origin, size, radius, std_dev, color));
3572        }
3573
3574        fn push_layer(&mut self, origin: Point, size: Size, alpha: f32) {
3575            self.layers.push((origin, size, alpha));
3576        }
3577
3578        fn pop_layer(&mut self) {
3579            self.pops += 1;
3580        }
3581    }
3582
3583    #[test]
3584    fn draw_shadow_and_push_layer_are_no_ops_when_not_overridden() {
3585        // `RecordingScene` (defined above) does not override the shadow/layer
3586        // additions — the trait's default no-op bodies must compile
3587        // unchanged and simply do nothing.
3588        let mut scene = RecordingScene::default();
3589        scene.draw_shadow(Point::ZERO, Size::new(10.0, 10.0), 4.0, 2.0, Color::BLACK);
3590        scene.push_layer(Point::ZERO, Size::new(10.0, 10.0), 0.5);
3591        scene.pop_layer();
3592        assert!(scene.rects.is_empty());
3593        assert!(scene.texts.is_empty());
3594    }
3595
3596    #[test]
3597    fn fill_path_and_stroke_path_are_no_ops_when_not_overridden() {
3598        // `RecordingScene` does not override the path additions
3599        // either — the trait's default no-op bodies must compile unchanged.
3600        let mut scene = RecordingScene::default();
3601        let mut path = BezPath::new();
3602        path.move_to((0.0, 0.0));
3603        path.line_to((1.0, 0.0));
3604        scene.fill_path(Point::ZERO, &path, &Brush::Solid(Color::BLACK));
3605        scene.stroke_path(Point::ZERO, &path, 2.0, &Brush::Solid(Color::BLACK));
3606        assert!(scene.rects.is_empty());
3607        assert!(scene.texts.is_empty());
3608    }
3609
3610    /// A scene recorder overriding the path additions, proving they
3611    /// reach an implementing scene that opts in.
3612    #[derive(Default)]
3613    struct PathRecordingScene {
3614        fills: Vec<(Point, BezPath)>,
3615        strokes: Vec<(Point, BezPath, f64)>,
3616    }
3617
3618    impl PaintScene for PathRecordingScene {
3619        fn fill_rect(&mut self, _origin: Point, _size: Size, _color: Color) {}
3620        fn draw_text(&mut self, _origin: Point, _text: &str) {}
3621
3622        fn fill_path(&mut self, origin: Point, path: &BezPath, _brush: &Brush) {
3623            self.fills.push((origin, path.clone()));
3624        }
3625
3626        fn stroke_path(&mut self, origin: Point, path: &BezPath, width: f64, _brush: &Brush) {
3627            self.strokes.push((origin, path.clone(), width));
3628        }
3629    }
3630
3631    #[test]
3632    fn fill_path_and_stroke_path_reach_an_overriding_implementor() {
3633        let mut scene = PathRecordingScene::default();
3634        let mut path = BezPath::new();
3635        path.move_to((0.0, 0.0));
3636        path.line_to((5.0, 0.0));
3637
3638        scene.fill_path(Point::new(1.0, 2.0), &path, &Brush::Solid(Color::BLACK));
3639        scene.stroke_path(
3640            Point::new(3.0, 4.0),
3641            &path,
3642            1.5,
3643            &Brush::Solid(Color::BLACK),
3644        );
3645
3646        assert_eq!(scene.fills.len(), 1);
3647        assert_eq!(scene.fills[0].0, Point::new(1.0, 2.0));
3648        assert_eq!(scene.fills[0].1, path);
3649
3650        assert_eq!(scene.strokes.len(), 1);
3651        assert_eq!(scene.strokes[0].0, Point::new(3.0, 4.0));
3652        assert_eq!(scene.strokes[0].1, path);
3653        assert_eq!(scene.strokes[0].2, 1.5);
3654    }
3655
3656    #[test]
3657    fn path_at_translates_path_points_by_origin() {
3658        let mut path = BezPath::new();
3659        path.move_to((0.0, 0.0));
3660        path.line_to((5.0, 0.0));
3661
3662        let translated = path_at(Point::new(10.0, 20.0), &path);
3663        let mut expected = BezPath::new();
3664        expected.move_to((10.0, 20.0));
3665        expected.line_to((15.0, 20.0));
3666        assert_eq!(translated, expected);
3667    }
3668
3669    /// A dummy theme type, standing in for `frust_theme::Theme` — proving the
3670    /// type-erased theme slot works for *any* `'static` type, not just the real
3671    /// theme (`frust-core` never names it).
3672    #[derive(Debug, PartialEq)]
3673    struct TestTheme {
3674        accent: u32,
3675    }
3676
3677    #[test]
3678    fn layout_ctx_theme_as_recovers_threaded_theme() {
3679        let theme = TestTheme { accent: 7 };
3680        let ctx = LayoutCtx::new().with_theme(&theme);
3681        assert_eq!(ctx.theme_as::<TestTheme>(), Some(&TestTheme { accent: 7 }));
3682    }
3683
3684    #[test]
3685    fn layout_ctx_theme_as_is_none_without_a_theme() {
3686        let ctx = LayoutCtx::new();
3687        assert!(ctx.theme_as::<TestTheme>().is_none());
3688    }
3689
3690    #[test]
3691    fn layout_ctx_theme_as_is_none_on_type_mismatch() {
3692        let theme = TestTheme { accent: 1 };
3693        let ctx = LayoutCtx::new().with_theme(&theme);
3694        // A downcast to the wrong type yields `None`, never a panic.
3695        assert!(ctx.theme_as::<u32>().is_none());
3696    }
3697
3698    #[test]
3699    fn paint_ctx_theme_as_recovers_threaded_theme() {
3700        let theme = TestTheme { accent: 9 };
3701        let mut ctx = PaintCtx::new(Point::ZERO, Size::new(1.0, 1.0));
3702        ctx.set_theme(Some(&theme));
3703        assert_eq!(ctx.theme_as::<TestTheme>(), Some(&TestTheme { accent: 9 }));
3704    }
3705
3706    #[test]
3707    fn paint_ctx_theme_as_is_none_without_a_theme() {
3708        let ctx = PaintCtx::new(Point::ZERO, Size::new(1.0, 1.0));
3709        assert!(ctx.theme_as::<TestTheme>().is_none());
3710    }
3711
3712    #[test]
3713    fn child_pod_paint_threads_theme_into_child() {
3714        use std::cell::Cell;
3715        use std::rc::Rc;
3716
3717        // A probe recording the accent it recovered from the paint context's
3718        // threaded theme (or `None` if no theme reached it).
3719        struct ThemeProbe(Rc<Cell<Option<u32>>>);
3720        impl Widget for ThemeProbe {
3721            fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3722                bc.constrain(Size::new(10.0, 10.0))
3723            }
3724            fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
3725                self.0.set(ctx.theme_as::<TestTheme>().map(|t| t.accent));
3726            }
3727        }
3728
3729        let seen = Rc::new(Cell::new(None));
3730        let mut pod = ChildPod::new(Box::new(ThemeProbe(seen.clone())));
3731        let mut lctx = LayoutCtx::new();
3732        pod.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
3733        let mut scene = RecordingScene::default();
3734
3735        // A parent carrying a theme threads it into the child paint.
3736        let theme = TestTheme { accent: 42 };
3737        let mut parent = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3738        parent.set_theme(Some(&theme));
3739        pod.paint_child(&mut parent, &mut scene);
3740        assert_eq!(seen.get(), Some(42));
3741
3742        // A parent with no theme leaves the child's accessor empty.
3743        let mut bare = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
3744        pod.paint_child(&mut bare, &mut scene);
3745        assert_eq!(seen.get(), None);
3746    }
3747
3748    /// A leaf widget whose visible appearance is driven purely by
3749    /// [`PaintCtx::frame_time`] — stands in for a caret-blink widget: it fills
3750    /// a rect only during the "on" half of a 1-second blink cycle, with no
3751    /// internal state of its own.
3752    struct BlinkBox;
3753
3754    impl Widget for BlinkBox {
3755        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3756            bc.constrain(Size::new(4.0, 4.0))
3757        }
3758        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
3759            let millis = ctx.frame_time().as_nanos() / 1_000_000;
3760            if (millis / 500).is_multiple_of(2) {
3761                scene.fill_rect(ctx.origin(), ctx.size(), Color::BLACK);
3762            }
3763        }
3764    }
3765
3766    #[test]
3767    fn for_test_seeds_the_requested_frame_time() {
3768        let ctx = PaintCtx::for_test(Point::ZERO, Size::new(1.0, 1.0), FrameTime::from_nanos(42));
3769        assert_eq!(ctx.frame_time(), FrameTime::from_nanos(42));
3770    }
3771
3772    /// End-to-end proof the seam actually drives a clock-dependent widget:
3773    /// the same [`BlinkBox`] paints differently at two [`FrameTime`]s built
3774    /// via [`PaintCtx::for_test`] alone — no [`crate::app::RenderRoot`]
3775    /// involved, exactly how an app crate outside this workspace would use
3776    /// the seam.
3777    #[test]
3778    fn for_test_drives_a_clock_dependent_widget_to_two_appearances() {
3779        let mut widget = BlinkBox;
3780        let size = Size::new(4.0, 4.0);
3781
3782        let mut on_ctx = PaintCtx::for_test(Point::ZERO, size, FrameTime::from_nanos(0));
3783        let mut on_scene = RecordingScene::default();
3784        widget.paint(&mut on_ctx, &mut on_scene);
3785        assert_eq!(on_scene.rects.len(), 1, "expected the caret painted on");
3786
3787        let mut off_ctx = PaintCtx::for_test(Point::ZERO, size, FrameTime::from_nanos(500_000_000));
3788        let mut off_scene = RecordingScene::default();
3789        widget.paint(&mut off_ctx, &mut off_scene);
3790        assert!(
3791            off_scene.rects.is_empty(),
3792            "expected the caret painted off half a blink cycle later"
3793        );
3794    }
3795
3796    #[test]
3797    fn draw_shadow_and_push_layer_reach_an_overriding_implementor() {
3798        let mut scene = ShadowLayerRecordingScene::default();
3799        let origin = Point::new(3.0, 4.0);
3800        let size = Size::new(20.0, 12.0);
3801        scene.draw_shadow(origin, size, 6.0, 3.0, Color::BLACK);
3802        scene.push_layer(origin, size, 0.25);
3803        scene.pop_layer();
3804
3805        assert_eq!(scene.shadows, vec![(origin, size, 6.0, 3.0, Color::BLACK)]);
3806        assert_eq!(scene.layers, vec![(origin, size, 0.25)]);
3807        assert_eq!(scene.pops, 1);
3808    }
3809
3810    #[test]
3811    fn a_pods_type_name_resolves_through_every_box() {
3812        // A pod's widget is always erased, and the container plumbing stores it
3813        // double-boxed — the name must still be the widget's own, at any depth.
3814        let single = ChildPod::new(Box::new(Animator));
3815        assert!(
3816            single.type_name().ends_with("Animator"),
3817            "{}",
3818            single.type_name()
3819        );
3820
3821        let boxed: Box<dyn Widget> = Box::new(Animator);
3822        let double = ChildPod::new(Box::new(boxed));
3823        assert!(
3824            double.type_name().ends_with("Animator"),
3825            "{}",
3826            double.type_name()
3827        );
3828    }
3829
3830    #[test]
3831    fn a_pods_tooling_id_is_assigned_once_and_never_shared() {
3832        let a = ChildPod::new(Box::new(Animator));
3833        let b = ChildPod::new(Box::new(Animator));
3834        let first = a.inspect_id();
3835        assert_eq!(a.inspect_id(), first, "a pod keeps the id it was given");
3836        assert_ne!(b.inspect_id(), first, "two pods never share an id");
3837        assert!(
3838            first.0 >= ChildPod::INSPECT_ID_BASE,
3839            "pod ids stay in the range reserved against arena WidgetIds"
3840        );
3841    }
3842
3843    #[test]
3844    fn a_leaf_visits_no_children_and_a_container_visits_all_of_its_own() {
3845        struct Two {
3846            children: Vec<ChildPod>,
3847        }
3848        impl Widget for Two {
3849            fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
3850                bc.max()
3851            }
3852            fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
3853            fn visit_children(&self, visitor: &mut dyn FnMut(&ChildPod)) {
3854                for child in &self.children {
3855                    visitor(child);
3856                }
3857            }
3858        }
3859
3860        // The trait default: a widget that says nothing publishes nothing.
3861        let mut seen = 0;
3862        Animator.visit_children(&mut |_| seen += 1);
3863        assert_eq!(seen, 0);
3864
3865        let container = Two {
3866            children: vec![
3867                ChildPod::new(Box::new(Animator)),
3868                ChildPod::new(Box::new(Animator)),
3869            ],
3870        };
3871        let mut ids = Vec::new();
3872        container.visit_children(&mut |child| ids.push(child.inspect_id()));
3873        assert_eq!(ids.len(), 2);
3874        assert_ne!(ids[0], ids[1]);
3875    }
3876
3877    /// A recorder that implements only the *uniform* rounded methods and the
3878    /// solid stroke — i.e. every pre-per-corner recorder scene in the wild —
3879    /// so the per-corner/dashed trait defaults can be observed falling back.
3880    #[derive(Default)]
3881    struct UniformOnlyScene {
3882        fills: Vec<(Point, Size, f64)>,
3883        clips: Vec<(Point, Size, f64)>,
3884        strokes: Vec<(Point, f64)>,
3885        pops: usize,
3886    }
3887
3888    impl PaintScene for UniformOnlyScene {
3889        fn fill_rect(&mut self, _origin: Point, _size: Size, _color: Color) {}
3890        fn draw_text(&mut self, _origin: Point, _text: &str) {}
3891        fn fill_rounded_rect(&mut self, origin: Point, size: Size, radius: f64, _color: Color) {
3892            self.fills.push((origin, size, radius));
3893        }
3894        fn push_clip_rounded(&mut self, origin: Point, size: Size, radius: f64) {
3895            self.clips.push((origin, size, radius));
3896        }
3897        fn pop_clip(&mut self) {
3898            self.pops += 1;
3899        }
3900        fn stroke_path(&mut self, origin: Point, _path: &BezPath, width: f64, _brush: &Brush) {
3901            self.strokes.push((origin, width));
3902        }
3903    }
3904
3905    fn corner_probe_path() -> BezPath {
3906        let mut path = BezPath::new();
3907        path.move_to((0.0, 0.0));
3908        path.line_to((10.0, 0.0));
3909        path
3910    }
3911
3912    #[test]
3913    fn per_corner_paint_defaults_fall_back_to_the_uniform_methods() {
3914        // A scene that predates per-corner radii still paints something, at the
3915        // largest corner — and its clip stack stays balanced, which is why the
3916        // push delegates rather than no-op'ing against a real `pop_clip`.
3917        let mut scene = UniformOnlyScene::default();
3918        let origin = Point::new(2.0, 3.0);
3919        let size = Size::new(20.0, 10.0);
3920        let radii = CornerRadii::new(4.0, 12.0, 0.0, 1.0);
3921
3922        scene.fill_rounded_rect_radii(origin, size, radii, Color::BLACK);
3923        scene.push_clip_rounded_radii(origin, size, radii);
3924        scene.pop_clip();
3925
3926        assert_eq!(scene.fills, vec![(origin, size, 12.0)]);
3927        assert_eq!(scene.clips, vec![(origin, size, 12.0)]);
3928        assert_eq!(scene.pops, 1);
3929    }
3930
3931    /// A recorder that predates path clips: it knows only the rectangular
3932    /// push and the shared pop.
3933    #[derive(Default)]
3934    struct RectClipOnlyScene {
3935        clips: Vec<(Point, Size)>,
3936        pops: usize,
3937    }
3938
3939    impl PaintScene for RectClipOnlyScene {
3940        fn fill_rect(&mut self, _origin: Point, _size: Size, _color: Color) {}
3941        fn draw_text(&mut self, _origin: Point, _text: &str) {}
3942        fn push_clip(&mut self, origin: Point, size: Size) {
3943            self.clips.push((origin, size));
3944        }
3945        fn pop_clip(&mut self) {
3946            self.pops += 1;
3947        }
3948    }
3949
3950    fn triangle_probe_path() -> BezPath {
3951        let mut path = BezPath::new();
3952        path.move_to((4.0, 2.0));
3953        path.line_to((14.0, 2.0));
3954        path.line_to((9.0, 12.0));
3955        path.close_path();
3956        path
3957    }
3958
3959    #[test]
3960    fn path_clip_default_pushes_the_translated_bounding_box() {
3961        // A scene with no path-clip concept still clips — to the path's
3962        // bounding box, offset by `origin` — and its clip stack stays balanced
3963        // against the shared `pop_clip`.
3964        let mut scene = RectClipOnlyScene::default();
3965        scene.push_clip_path(Point::new(10.0, 20.0), &triangle_probe_path());
3966        scene.pop_clip();
3967
3968        assert_eq!(
3969            scene.clips,
3970            vec![(Point::new(14.0, 22.0), Size::new(10.0, 10.0))]
3971        );
3972        assert_eq!(scene.pops, 1);
3973    }
3974
3975    #[test]
3976    fn scene_builder_records_a_path_clip_translated_by_origin() {
3977        let mut scene = frust_scene::Scene::new();
3978        {
3979            let mut builder = frust_scene::SceneBuilder::new(&mut scene);
3980            let paint: &mut dyn PaintScene = &mut builder;
3981            paint.push_clip_path(Point::new(10.0, 20.0), &triangle_probe_path());
3982            paint.pop_clip();
3983        }
3984
3985        match &scene.commands()[0] {
3986            frust_scene::Command::PushClipPath { path, transform } => {
3987                let expected = Affine::translate((10.0, 20.0)) * triangle_probe_path();
3988                assert_eq!(*path, expected);
3989                assert_eq!(*transform, Affine::IDENTITY);
3990            }
3991            other => panic!("expected PushClipPath, got {other:?}"),
3992        }
3993        assert!(matches!(scene.commands()[1], frust_scene::Command::PopClip));
3994    }
3995
3996    #[test]
3997    fn dashed_stroke_default_falls_back_to_the_solid_stroke() {
3998        let mut scene = UniformOnlyScene::default();
3999        let origin = Point::new(5.0, 6.0);
4000        scene.stroke_path_dashed(
4001            origin,
4002            &corner_probe_path(),
4003            2.0,
4004            DashPattern::new(4.0, 2.0),
4005            &Brush::Solid(Color::BLACK),
4006        );
4007
4008        assert_eq!(scene.strokes, vec![(origin, 2.0)]);
4009    }
4010
4011    #[test]
4012    fn scene_builder_records_per_corner_radii_translated_by_origin() {
4013        // The `SceneBuilder` impl overrides the defaults above: every corner
4014        // reaches the command intact, under the same origin→rect convention the
4015        // uniform methods use.
4016        let mut scene = frust_scene::Scene::new();
4017        let radii = CornerRadii::new(4.0, 12.0, 0.0, 1.0);
4018        {
4019            let mut builder = frust_scene::SceneBuilder::new(&mut scene);
4020            let paint: &mut dyn PaintScene = &mut builder;
4021            paint.fill_rounded_rect_radii(
4022                Point::new(2.0, 3.0),
4023                Size::new(20.0, 10.0),
4024                radii,
4025                Color::BLACK,
4026            );
4027            paint.push_clip_rounded_radii(Point::new(0.0, 0.0), Size::new(5.0, 5.0), radii);
4028            paint.pop_clip();
4029        }
4030
4031        match &scene.commands()[0] {
4032            frust_scene::Command::RoundedRect {
4033                rect, radii: got, ..
4034            } => {
4035                assert_eq!(*rect, Rect::new(2.0, 3.0, 22.0, 13.0));
4036                assert_eq!(*got, radii);
4037            }
4038            other => panic!("expected RoundedRect, got {other:?}"),
4039        }
4040        match &scene.commands()[1] {
4041            frust_scene::Command::PushClipRounded { radii: got, .. } => assert_eq!(*got, radii),
4042            other => panic!("expected PushClipRounded, got {other:?}"),
4043        }
4044        assert!(matches!(scene.commands()[2], frust_scene::Command::PopClip));
4045    }
4046
4047    #[test]
4048    fn scene_builder_records_a_dashed_stroke_translated_by_origin() {
4049        let mut scene = frust_scene::Scene::new();
4050        let dash = DashPattern::new(4.0, 2.0).with_phase(1.0);
4051        {
4052            let mut builder = frust_scene::SceneBuilder::new(&mut scene);
4053            let paint: &mut dyn PaintScene = &mut builder;
4054            paint.stroke_path_dashed(
4055                Point::new(5.0, 0.0),
4056                &corner_probe_path(),
4057                2.0,
4058                dash,
4059                &Brush::Solid(Color::BLACK),
4060            );
4061        }
4062
4063        match &scene.commands()[0] {
4064            frust_scene::Command::Path { path, style, .. } => {
4065                assert_eq!(
4066                    *style,
4067                    frust_scene::PathStyle::Stroke {
4068                        width: 2.0,
4069                        dash: Some(dash)
4070                    }
4071                );
4072                // Same origin translation `stroke_path` applies.
4073                assert_eq!(path.elements().len(), 2);
4074                assert!(matches!(
4075                    path.elements()[0],
4076                    kurbo::PathEl::MoveTo(p) if p == Point::new(5.0, 0.0)
4077                ));
4078            }
4079            other => panic!("expected Path, got {other:?}"),
4080        }
4081    }
4082
4083    #[test]
4084    fn uniform_paint_calls_still_encode_the_commands_they_always_did() {
4085        // The additive contract: the uniform methods every existing call site
4086        // uses keep producing the same commands, now spelled as four equal
4087        // corners / an undashed stroke.
4088        let mut scene = frust_scene::Scene::new();
4089        {
4090            let mut builder = frust_scene::SceneBuilder::new(&mut scene);
4091            let paint: &mut dyn PaintScene = &mut builder;
4092            paint.fill_rounded_rect(
4093                Point::new(0.0, 0.0),
4094                Size::new(10.0, 10.0),
4095                4.0,
4096                Color::BLACK,
4097            );
4098            paint.push_clip_rounded(Point::new(0.0, 0.0), Size::new(10.0, 10.0), 4.0);
4099            paint.pop_clip();
4100            paint.draw_shadow(
4101                Point::new(0.0, 0.0),
4102                Size::new(10.0, 10.0),
4103                4.0,
4104                2.0,
4105                Color::BLACK,
4106            );
4107            paint.stroke_path(
4108                Point::new(0.0, 0.0),
4109                &corner_probe_path(),
4110                1.0,
4111                &Brush::Solid(Color::BLACK),
4112            );
4113        }
4114
4115        let uniform = CornerRadii::uniform(4.0);
4116        match &scene.commands()[0] {
4117            frust_scene::Command::RoundedRect { radii, .. } => assert_eq!(*radii, uniform),
4118            other => panic!("expected RoundedRect, got {other:?}"),
4119        }
4120        match &scene.commands()[1] {
4121            frust_scene::Command::PushClipRounded { radii, .. } => assert_eq!(*radii, uniform),
4122            other => panic!("expected PushClipRounded, got {other:?}"),
4123        }
4124        match &scene.commands()[3] {
4125            frust_scene::Command::BlurredRoundedRect { radii, .. } => assert_eq!(*radii, uniform),
4126            other => panic!("expected BlurredRoundedRect, got {other:?}"),
4127        }
4128        match &scene.commands()[4] {
4129            frust_scene::Command::Path { style, .. } => assert_eq!(
4130                *style,
4131                frust_scene::PathStyle::Stroke {
4132                    width: 1.0,
4133                    dash: None
4134                }
4135            ),
4136            other => panic!("expected Path, got {other:?}"),
4137        }
4138    }
4139
4140    #[test]
4141    fn every_paint_scene_method_terminates_on_discard_scene() {
4142        // Every `PaintScene` method — including the delegating defaults
4143        // (per-corner/dashed variants, the additive image/shader/shadow
4144        // surface) — is reachable and terminates on `DiscardScene` without
4145        // panicking; a future `todo!()` or an infinitely-recursive default
4146        // would fail here. The records-nothing guarantee itself is asserted
4147        // where a scene actually exists, at the navigator/switcher call
4148        // sites, not here.
4149        use frust_scene::{FontHandle, Glyph};
4150
4151        let mut scene = DiscardScene;
4152        let origin = Point::new(2.0, 3.0);
4153        let size = Size::new(20.0, 10.0);
4154        let color = Color::BLACK;
4155        let brush = Brush::Solid(color);
4156        let radii = CornerRadii::new(4.0, 12.0, 0.0, 1.0);
4157        let path = corner_probe_path();
4158
4159        scene.fill_rect(origin, size, color);
4160        scene.fill_rounded_rect(origin, size, 4.0, color);
4161        scene.fill_rounded_rect_radii(origin, size, radii, color);
4162        scene.stroke_line(origin, Point::new(10.0, 10.0), 1.0, color);
4163        scene.push_clip(origin, size);
4164        scene.push_clip_rounded(origin, size, 4.0);
4165        scene.push_clip_rounded_radii(origin, size, radii);
4166        scene.pop_clip();
4167        scene.draw_text(origin, "discarded");
4168
4169        let font = FontHandle::new(peniko::FontData::new(
4170            peniko::Blob::from(Vec::<u8>::new()),
4171            0,
4172        ));
4173        let run = GlyphRun {
4174            font,
4175            font_size: 16.0,
4176            brush: brush.clone(),
4177            transform: Affine::IDENTITY,
4178            glyphs: vec![Glyph {
4179                id: 1,
4180                x: 0.0,
4181                y: 0.0,
4182            }],
4183        };
4184        scene.draw_glyph_run(run);
4185
4186        let image = peniko::ImageData {
4187            data: peniko::Blob::from(vec![0u8; 2 * 2 * 4]),
4188            format: peniko::ImageFormat::Rgba8,
4189            alpha_type: peniko::ImageAlphaType::Alpha,
4190            width: 2,
4191            height: 2,
4192        };
4193        scene.draw_image(&image, Rect::new(0.0, 0.0, 20.0, 20.0));
4194
4195        let program = ShaderProgram::new("fn main() {}");
4196        scene.draw_shader(&program, Rect::new(0.0, 0.0, 10.0, 10.0), 1.5);
4197
4198        scene.draw_shadow(origin, size, 4.0, 2.0, color);
4199        scene.fill_rect_brush(origin, size, &brush);
4200        scene.fill_rounded_rect_brush(origin, size, 4.0, &brush);
4201        scene.push_layer(origin, size, 0.5);
4202        scene.pop_layer();
4203        scene.clear_rect(origin, size);
4204        scene.fill_path(origin, &path, &brush);
4205        scene.stroke_path(origin, &path, 1.0, &brush);
4206        scene.stroke_path_dashed(origin, &path, 1.0, DashPattern::new(4.0, 2.0), &brush);
4207        scene.push_transform(Affine::translate((1.0, 2.0)));
4208        scene.pop_transform();
4209    }
4210
4211    /// A minimal recorder that tracks only transform and layer stack operations
4212    /// to verify the default [`PaintScene::push_retained`]/[`PaintScene::pop_retained`]
4213    /// implementation emulates the presentation correctly.
4214    #[derive(Default)]
4215    struct MinimalRetainedRecorder {
4216        operations: Vec<String>,
4217    }
4218
4219    impl PaintScene for MinimalRetainedRecorder {
4220        fn fill_rect(&mut self, _origin: Point, _size: Size, _color: Color) {}
4221        fn draw_text(&mut self, _origin: Point, _text: &str) {}
4222        fn push_transform(&mut self, _transform: Affine) {
4223            self.operations.push("push_transform".to_string());
4224        }
4225        fn pop_transform(&mut self) {
4226            self.operations.push("pop_transform".to_string());
4227        }
4228        fn push_layer(&mut self, _origin: Point, _size: Size, _alpha: f32) {
4229            self.operations.push("push_layer".to_string());
4230        }
4231        fn pop_layer(&mut self) {
4232            self.operations.push("pop_layer".to_string());
4233        }
4234    }
4235
4236    #[test]
4237    fn default_push_retained_pop_retained_emulates_presentation_for_recorders() {
4238        // The default `push_retained` and `pop_retained` implementation
4239        // ensures recorders that override only the transform/layer methods
4240        // see a consistent sequence: push_transform, push_layer, paint body,
4241        // pop_layer, pop_transform — the reverse order matching the push ops.
4242        let mut recorder = MinimalRetainedRecorder::default();
4243        let origin = Point::new(10.0, 20.0);
4244        let size = Size::new(100.0, 50.0);
4245        let alpha = 0.8;
4246        let scale = 0.9;
4247
4248        // Emulate a body that doesn't call any paint methods (just has side effects).
4249        recorder.push_retained(1, origin, size, alpha, scale);
4250        // Body paint code would go here
4251        recorder.pop_retained();
4252
4253        // Verify the sequence: transform, layer, pop_layer, pop_transform.
4254        assert_eq!(
4255            recorder.operations,
4256            vec![
4257                "push_transform".to_string(),
4258                "push_layer".to_string(),
4259                "pop_layer".to_string(),
4260                "pop_transform".to_string(),
4261            ]
4262        );
4263    }
4264
4265    #[test]
4266    fn scene_builder_records_push_pop_retained_commands_directly_without_extra_transforms() {
4267        // The `SceneBuilder` impl overrides `push_retained`/`pop_retained` to
4268        // record the retained commands directly, without the emulating
4269        // transform/layer pairs. It records Command::PushRetained with the
4270        // key, rect (converted from origin/size), alpha, scale, and the current
4271        // transform, and Command::PopRetained with no extra layer/transform commands.
4272        let mut scene = frust_scene::Scene::new();
4273        {
4274            let mut builder = frust_scene::SceneBuilder::new(&mut scene);
4275            let paint: &mut dyn PaintScene = &mut builder;
4276
4277            paint.push_retained(7, Point::new(5.0, 10.0), Size::new(80.0, 40.0), 0.5, 1.2);
4278            // Paint some content inside the retained bracket.
4279            paint.fill_rect(Point::new(10.0, 15.0), Size::new(20.0, 25.0), Color::BLACK);
4280            paint.pop_retained();
4281        }
4282
4283        // Verify the command sequence: PushRetained, FillRect, PopRetained.
4284        // No PushLayer, PopLayer, PushTransform, or PopTransform commands.
4285        let commands = scene.commands();
4286        assert_eq!(commands.len(), 3);
4287
4288        match &commands[0] {
4289            frust_scene::Command::PushRetained {
4290                key,
4291                rect,
4292                alpha,
4293                scale,
4294                ..
4295            } => {
4296                assert_eq!(*key, 7);
4297                assert_eq!(*rect, Rect::new(5.0, 10.0, 85.0, 50.0));
4298                assert_eq!(*alpha, 0.5);
4299                assert_eq!(*scale, 1.2);
4300            }
4301            other => panic!("expected PushRetained, got {other:?}"),
4302        }
4303
4304        match &commands[1] {
4305            frust_scene::Command::FillRect { rect, .. } => {
4306                assert_eq!(*rect, Rect::new(10.0, 15.0, 30.0, 40.0));
4307            }
4308            other => panic!("expected FillRect, got {other:?}"),
4309        }
4310
4311        assert!(
4312            matches!(commands[2], frust_scene::Command::PopRetained),
4313            "expected PopRetained, got {:?}",
4314            commands[2]
4315        );
4316    }
4317
4318    #[test]
4319    fn current_transform_is_the_scene_builders_own_and_none_for_a_recorder() {
4320        assert_eq!(
4321            MinimalRetainedRecorder::default().current_transform(),
4322            None,
4323            "a recorder tracks no transform"
4324        );
4325        let mut scene = frust_scene::Scene::new();
4326        let mut builder = frust_scene::SceneBuilder::new(&mut scene);
4327        let paint: &mut dyn PaintScene = &mut builder;
4328        assert_eq!(paint.current_transform(), Some(Affine::IDENTITY));
4329        paint.push_transform(Affine::scale(2.0));
4330        paint.push_transform(Affine::translate((3.0, 4.0)));
4331        assert_eq!(
4332            paint.current_transform(),
4333            Some(Affine::scale(2.0) * Affine::translate((3.0, 4.0))),
4334            "every open transform, composed"
4335        );
4336    }
4337
4338    #[test]
4339    fn backdrop_blur_is_a_no_op_by_default_and_recorded_by_the_scene_builder() {
4340        // The default body: a recorder scene that never heard of a backdrop
4341        // takes the call and records nothing.
4342        let mut recorder = MinimalRetainedRecorder::default();
4343        recorder.backdrop_blur(Point::new(1.0, 2.0), Size::new(3.0, 4.0), 5.0);
4344
4345        let mut scene = frust_scene::Scene::new();
4346        {
4347            let mut builder = frust_scene::SceneBuilder::new(&mut scene);
4348            let paint: &mut dyn PaintScene = &mut builder;
4349            paint.push_transform(Affine::translate((10.0, 20.0)));
4350            paint.backdrop_blur(Point::new(1.0, 2.0), Size::new(3.0, 4.0), 5.0);
4351        }
4352        match scene.commands() {
4353            [
4354                frust_scene::Command::BackdropBlur {
4355                    rect,
4356                    radius,
4357                    transform,
4358                },
4359            ] => {
4360                assert_eq!(*rect, Rect::new(1.0, 2.0, 4.0, 6.0));
4361                assert_eq!(*radius, 5.0);
4362                assert_eq!(*transform, Affine::translate((10.0, 20.0)));
4363            }
4364            other => panic!("expected one BackdropBlur, got {other:?}"),
4365        }
4366    }
4367
4368    #[test]
4369    fn retained_enabled_needs_the_switch_and_a_threaded_ledger() {
4370        let ledger = crate::retained::RetainedLedger::empty();
4371        assert!(
4372            !PaintCtx::new(Point::ZERO, Size::ZERO).retained_enabled(),
4373            "no ledger threaded"
4374        );
4375        let mut ctx = PaintCtx::new(Point::ZERO, Size::ZERO).with_retained_ledger(&ledger);
4376        assert!(
4377            ctx.retained_enabled(),
4378            "an empty ledger still enables brackets"
4379        );
4380        ctx.set_paint_state(0, false, Some(&ledger), 0);
4381        assert!(!ctx.retained_enabled(), "the kill switch disables them");
4382    }
4383
4384    /// Publishes on every paint channel a boundary must not skip.
4385    struct Publisher;
4386
4387    impl Widget for Publisher {
4388        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4389            bc.max()
4390        }
4391        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
4392            ctx.report_input_shield(Rect::new(0.0, 0.0, 1.0, 1.0));
4393            ctx.report_hero("tag", Rect::new(0.0, 0.0, 1.0, 1.0));
4394        }
4395    }
4396
4397    #[test]
4398    fn paint_publications_sum_up_through_paint_child_and_hero_registries() {
4399        let mut pod = ChildPod::new(Box::new(Publisher));
4400        pod.layout_child(
4401            &mut LayoutCtx::new(),
4402            &BoxConstraints::tight(Size::new(5.0, 5.0)),
4403        );
4404        let mut quiet = ChildPod::new(Box::new(FixedBox {
4405            intrinsic: Size::new(5.0, 5.0),
4406        }));
4407        quiet.layout_child(
4408            &mut LayoutCtx::new(),
4409            &BoxConstraints::tight(Size::new(5.0, 5.0)),
4410        );
4411
4412        let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
4413        quiet.paint_child(&mut ctx, &mut DiscardScene);
4414        assert_eq!(
4415            ctx.paint_publications(),
4416            0,
4417            "a plain body publishes nothing"
4418        );
4419        pod.paint_child(&mut ctx, &mut DiscardScene);
4420        assert_eq!(ctx.paint_publications(), 2, "a shield and a hero report");
4421
4422        let registry = RefCell::new(HeroFrames::new(Point::ZERO, HashMap::new()));
4423        ctx.with_hero_registry(&registry, |inner| {
4424            pod.paint_child(inner, &mut DiscardScene);
4425        });
4426        assert_eq!(
4427            ctx.paint_publications(),
4428            4,
4429            "a hero registry's subtree counts too"
4430        );
4431    }
4432}
4433
4434#[cfg(test)]
4435mod transform_tests {
4436    use std::f64::consts::FRAC_PI_4;
4437
4438    use kurbo::Vec2;
4439
4440    use super::*;
4441    use crate::event::{
4442        PointerButton, PointerEvent, PointerId, PointerPhase, ScaleEvent, ScalePhase, ScrollDelta,
4443    };
4444
4445    /// One recorded paint operation, in order.
4446    #[derive(Clone, Debug, PartialEq)]
4447    enum Op {
4448        Rect(Point, Size),
4449        Push(Affine),
4450        Pop,
4451    }
4452
4453    /// A scene recorder that keeps the transform stack interleaved with draws,
4454    /// so a test can see exactly what a pod pushed around its child.
4455    #[derive(Default)]
4456    struct OpLog(Vec<Op>);
4457
4458    impl PaintScene for OpLog {
4459        fn fill_rect(&mut self, origin: Point, size: Size, _color: Color) {
4460            self.0.push(Op::Rect(origin, size));
4461        }
4462        fn draw_text(&mut self, _origin: Point, _text: &str) {}
4463        fn push_transform(&mut self, transform: Affine) {
4464            self.0.push(Op::Push(transform));
4465        }
4466        fn pop_transform(&mut self) {
4467            self.0.push(Op::Pop);
4468        }
4469    }
4470
4471    /// A leaf that fills its whole box, records every event it receives (in
4472    /// local space), captures on `Down`, and contributes one semantics node.
4473    #[derive(Default)]
4474    struct Recorder {
4475        events: Vec<InputEvent>,
4476    }
4477
4478    impl Widget for Recorder {
4479        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4480            bc.max()
4481        }
4482        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
4483            scene.fill_rect(ctx.origin(), ctx.size(), Color::BLACK);
4484        }
4485        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
4486            self.events.push(event.clone());
4487            if matches!(
4488                event,
4489                InputEvent::Pointer(PointerEvent {
4490                    phase: PointerPhase::Down,
4491                    ..
4492                })
4493            ) {
4494                ctx.capture_pointer();
4495            }
4496            EventResult::Handled
4497        }
4498        fn semantics(&self, ctx: &mut SemanticsCtx) {
4499            ctx.push_node(accesskit::Role::Label, |_| {});
4500        }
4501    }
4502
4503    /// A leaf that records the visible rect its paint context carries.
4504    struct VisibleProbe(std::rc::Rc<Cell<Option<Rect>>>);
4505
4506    impl Widget for VisibleProbe {
4507        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4508            bc.max()
4509        }
4510        fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
4511            self.0.set(ctx.visible_rect());
4512        }
4513    }
4514
4515    fn pointer(phase: PointerPhase, x: f64, y: f64) -> InputEvent {
4516        InputEvent::Pointer(PointerEvent {
4517            phase,
4518            position: Point::new(x, y),
4519            button: PointerButton::Primary,
4520        })
4521    }
4522
4523    /// A laid-out `Recorder` pod of `size` at `origin`.
4524    fn recorder_pod(origin: Point, size: Size) -> ChildPod {
4525        let mut pod = ChildPod::new(Box::new(Recorder::default()));
4526        pod.layout_child(&mut LayoutCtx::new(), &BoxConstraints::tight(size));
4527        pod.set_origin(origin);
4528        pod
4529    }
4530
4531    fn recorded(pod: &mut ChildPod) -> Vec<InputEvent> {
4532        pod.widget_mut()
4533            .downcast_mut::<Recorder>()
4534            .expect("recorder pod")
4535            .events
4536            .clone()
4537    }
4538
4539    fn paint(pod: &mut ChildPod, visible: Option<Rect>) -> (Vec<Op>, Option<Rect>) {
4540        let mut ctx = PaintCtx::new(Point::ZERO, Size::new(400.0, 400.0));
4541        ctx.set_visible_rect(visible);
4542        let mut scene = OpLog::default();
4543        pod.paint_child(&mut ctx, &mut scene);
4544        (scene.0, ctx.visible_rect_ref())
4545    }
4546
4547    fn dispatch(pod: &mut ChildPod, event: &InputEvent) -> EventResult {
4548        let mut state = 0u32;
4549        let mut ctx = EventCtx::new(&mut state, Point::ZERO, Size::new(400.0, 400.0));
4550        pod.event_child(&mut ctx, event)
4551    }
4552
4553    #[test]
4554    fn a_child_under_scale_and_translate_receives_down_at_its_local_point() {
4555        let mut pod = recorder_pod(Point::new(10.0, 20.0), Size::new(50.0, 50.0));
4556        // Local p lands at origin + translate(5, 5) * scale(2) * p.
4557        pod.set_transform(Some(
4558            Affine::translate(Vec2::new(5.0, 5.0)) * Affine::scale(2.0),
4559        ));
4560        // Local (7, 9) is drawn at (10 + 5 + 14, 20 + 5 + 18) = (29, 43).
4561        let at = Point::new(29.0, 43.0);
4562        assert!(pod.contains(at));
4563        assert_eq!(
4564            dispatch(&mut pod, &pointer(PointerPhase::Down, at.x, at.y)),
4565            EventResult::Handled
4566        );
4567        assert!(pod.is_active(), "capture bookkeeping is unchanged");
4568        // Scroll positions and scale focal points take the same mapping; their
4569        // magnitudes do not.
4570        dispatch(
4571            &mut pod,
4572            &InputEvent::Scroll {
4573                position: at,
4574                delta: ScrollDelta::Pixels(0.0, 12.0),
4575            },
4576        );
4577        dispatch(
4578            &mut pod,
4579            &InputEvent::Scale(ScaleEvent {
4580                phase: ScalePhase::Update,
4581                scale_delta: 1.1,
4582                focal: at,
4583                velocity: 0.0,
4584            }),
4585        );
4586        let local = Point::new(7.0, 9.0);
4587        assert_eq!(
4588            recorded(&mut pod),
4589            vec![
4590                pointer(PointerPhase::Down, local.x, local.y),
4591                InputEvent::Scroll {
4592                    position: local,
4593                    delta: ScrollDelta::Pixels(0.0, 12.0),
4594                },
4595                InputEvent::Scale(ScaleEvent {
4596                    phase: ScalePhase::Update,
4597                    scale_delta: 1.1,
4598                    focal: local,
4599                    velocity: 0.0,
4600                }),
4601            ]
4602        );
4603        // The child now covers (15..115, 25..125): the untransformed box's
4604        // corner misses, and a point past its untransformed extent hits.
4605        assert!(!pod.contains(Point::new(12.0, 22.0)));
4606        assert!(pod.contains(Point::new(100.0, 100.0)));
4607        assert!(!pod.contains(Point::new(116.0, 60.0)));
4608        // A contact's inner event is mapped the same way.
4609        let contact = InputEvent::PointerContact {
4610            pointer_id: PointerId::touch(1),
4611            event: PointerEvent {
4612                phase: PointerPhase::Move,
4613                position: at,
4614                button: PointerButton::Primary,
4615            },
4616        };
4617        dispatch(&mut pod, &contact);
4618        assert_eq!(
4619            recorded(&mut pod).last(),
4620            Some(&InputEvent::PointerContact {
4621                pointer_id: PointerId::touch(1),
4622                event: PointerEvent {
4623                    phase: PointerPhase::Move,
4624                    position: local,
4625                    button: PointerButton::Primary,
4626                },
4627            })
4628        );
4629    }
4630
4631    #[test]
4632    fn contains_agrees_with_paint_for_rotated_content_at_the_corners() {
4633        let size = Size::new(100.0, 100.0);
4634        let mut pod = recorder_pod(Point::new(40.0, 40.0), size);
4635        pod.set_transform(Some(Affine::rotate_about(
4636            FRAC_PI_4,
4637            Point::new(50.0, 50.0),
4638        )));
4639        let (ops, _) = paint(&mut pod, None);
4640        let [Op::Push(drawn), Op::Rect(rect_origin, rect_size), Op::Pop] = ops.as_slice() else {
4641            panic!("expected push/rect/pop, got {ops:?}");
4642        };
4643        assert_eq!((*rect_origin, *rect_size), (Point::new(40.0, 40.0), size));
4644        let rect = Rect::from_origin_size(*rect_origin, *rect_size);
4645        let center = *drawn * rect.center();
4646        for corner in [
4647            Point::new(rect.x0, rect.y0),
4648            Point::new(rect.x1, rect.y0),
4649            Point::new(rect.x1, rect.y1),
4650            Point::new(rect.x0, rect.y1),
4651        ] {
4652            // Where paint actually put this corner (the parent paints at the
4653            // absolute origin zero, so absolute space is the container's space).
4654            let painted = *drawn * corner;
4655            let inward = (center - painted).normalize();
4656            assert!(
4657                pod.contains(painted + inward),
4658                "just inside painted corner {painted:?}"
4659            );
4660            assert!(
4661                !pod.contains(painted - inward),
4662                "just outside painted corner {painted:?}"
4663            );
4664            // The untransformed box's own corner is outside the painted
4665            // diamond: the hit test is not an AABB test.
4666            let unrotated = corner + (rect.center() - corner) * 0.02;
4667            assert!(!pod.contains(unrotated), "AABB corner {unrotated:?}");
4668        }
4669    }
4670
4671    #[test]
4672    fn an_untransformed_pod_paints_and_routes_exactly_as_before() {
4673        let origin = Point::new(12.0, 34.0);
4674        let size = Size::new(60.0, 40.0);
4675        let events = [
4676            pointer(PointerPhase::Down, 20.0, 40.0),
4677            pointer(PointerPhase::Move, 90.0, 10.0),
4678            pointer(PointerPhase::Up, 30.0, 50.0),
4679            InputEvent::Scroll {
4680                position: Point::new(25.0, 45.0),
4681                delta: ScrollDelta::Lines(0.0, 1.0),
4682            },
4683            InputEvent::Scale(ScaleEvent {
4684                phase: ScalePhase::Begin,
4685                scale_delta: 1.0,
4686                focal: Point::new(15.0, 35.0),
4687                velocity: 0.0,
4688            }),
4689            InputEvent::Housekeeping,
4690        ];
4691        let visible = Some(Rect::new(0.0, 0.0, 50.0, 50.0));
4692
4693        // Baseline: a pod that never had a transform.
4694        let mut baseline = recorder_pod(origin, size);
4695        let baseline_paint = paint(&mut baseline, visible);
4696        for event in &events {
4697            dispatch(&mut baseline, event);
4698        }
4699        let baseline_events = recorded(&mut baseline);
4700        // The baseline is the pre-transform contract: no transform pushed, the
4701        // child drawn at its origin, the visible rect threaded unchanged, and
4702        // every event translated by `-origin` and nothing else.
4703        assert_eq!(baseline_paint.0, vec![Op::Rect(origin, size)]);
4704        assert_eq!(baseline_paint.1, visible);
4705        let translated: Vec<_> = events
4706            .iter()
4707            .map(|e| e.translated(-origin.to_vec2()))
4708            .collect();
4709        assert_eq!(baseline_events, translated);
4710
4711        // A pod whose transform was set and then cleared is indistinguishable.
4712        let mut cleared = recorder_pod(origin, size);
4713        cleared.set_transform(Some(Affine::rotate(1.0) * Affine::scale(3.0)));
4714        cleared.set_transform(None);
4715        assert_eq!(cleared.transform(), None);
4716        assert_eq!(paint(&mut cleared, visible), baseline_paint);
4717        for event in &events {
4718            dispatch(&mut cleared, event);
4719        }
4720        assert_eq!(recorded(&mut cleared), baseline_events);
4721        for point in [
4722            Point::new(12.0, 34.0),
4723            Point::new(71.9, 73.9),
4724            Point::new(72.0, 50.0),
4725            Point::new(11.9, 50.0),
4726        ] {
4727            assert_eq!(cleared.contains(point), baseline.contains(point));
4728        }
4729    }
4730
4731    #[test]
4732    fn a_singular_transform_hits_nothing_and_never_panics() {
4733        let origin = Point::new(10.0, 10.0);
4734        for singular in [
4735            Affine::scale(0.0),
4736            Affine::scale_non_uniform(2.0, 0.0),
4737            Affine::new([1.0, 2.0, 2.0, 4.0, 0.0, 0.0]),
4738            Affine::new([f64::NAN, 0.0, 0.0, 1.0, 0.0, 0.0]),
4739        ] {
4740            let mut pod = recorder_pod(origin, Size::new(50.0, 50.0));
4741            pod.set_transform(Some(singular));
4742            for point in [origin, Point::new(20.0, 20.0), Point::new(10.0, 30.0)] {
4743                assert!(!pod.contains(point), "{singular:?} hit {point:?}");
4744            }
4745            // Paint skips the collapsed subtree entirely.
4746            assert_eq!(
4747                paint(&mut pod, Some(Rect::new(0.0, 0.0, 99.0, 99.0))).0,
4748                vec![]
4749            );
4750            // An event reaching it through a recorded path still arrives,
4751            // translated as if untransformed, so a capture can end.
4752            dispatch(&mut pod, &pointer(PointerPhase::Up, 20.0, 25.0));
4753            assert_eq!(
4754                recorded(&mut pod),
4755                vec![pointer(PointerPhase::Up, 10.0, 15.0)]
4756            );
4757            // Semantics never reports NaN bounds.
4758            let mut sem = SemanticsCtx::new(Size::new(200.0, 200.0), 2);
4759            pod.semantics_child(&mut sem);
4760            let update = sem.finish(accesskit::NodeId(1));
4761            let bounds = update.nodes[0].1.bounds().expect("bounds");
4762            assert!(
4763                [bounds.x0, bounds.y0, bounds.x1, bounds.y1]
4764                    .iter()
4765                    .all(|v| v.is_finite()),
4766                "{singular:?} gave {bounds:?}"
4767            );
4768        }
4769    }
4770
4771    #[test]
4772    fn a_transformed_pod_maps_the_visible_rect_and_reports_its_bounding_box() {
4773        let mut pod = recorder_pod(Point::new(10.0, 20.0), Size::new(50.0, 50.0));
4774        pod.set_transform(Some(Affine::scale(2.0)));
4775        // Paint: the transform is conjugated by the absolute origin, and the
4776        // visible rect reaches the child as its preimage. The parent's own
4777        // visible rect is untouched.
4778        let visible = Some(Rect::new(10.0, 20.0, 110.0, 120.0));
4779        let (ops, parent_visible) = paint(&mut pod, visible);
4780        assert_eq!(parent_visible, visible);
4781        assert_eq!(
4782            ops,
4783            vec![
4784                Op::Push(
4785                    Affine::translate(Vec2::new(10.0, 20.0))
4786                        * Affine::scale(2.0)
4787                        * Affine::translate(Vec2::new(-10.0, -20.0))
4788                ),
4789                Op::Rect(Point::new(10.0, 20.0), Size::new(50.0, 50.0)),
4790                Op::Pop,
4791            ]
4792        );
4793        // What the child itself culls against: the visible rect mapped back
4794        // through the inverse — (10, 20) .. (60, 70) in its own absolute frame.
4795        let seen = std::rc::Rc::new(Cell::new(None));
4796        let mut probe = ChildPod::new(Box::new(VisibleProbe(seen.clone())));
4797        probe.layout_child(
4798            &mut LayoutCtx::new(),
4799            &BoxConstraints::tight(Size::new(50.0, 50.0)),
4800        );
4801        probe.set_origin(Point::new(10.0, 20.0));
4802        probe.set_transform(Some(Affine::scale(2.0)));
4803        paint(&mut probe, visible);
4804        assert_eq!(seen.get(), Some(Rect::new(10.0, 20.0, 60.0, 70.0)));
4805        // Semantics: the transformed box, (10, 20) .. (110, 120).
4806        let mut sem = SemanticsCtx::new(Size::new(400.0, 400.0), 2);
4807        pod.semantics_child(&mut sem);
4808        let update = sem.finish(accesskit::NodeId(1));
4809        let bounds = update.nodes[0].1.bounds().expect("bounds");
4810        assert_eq!(
4811            (bounds.x0, bounds.y0, bounds.x1, bounds.y1),
4812            (10.0, 20.0, 110.0, 120.0)
4813        );
4814    }
4815}
4816
4817#[cfg(test)]
4818mod contact_release_tests {
4819    use super::*;
4820    use crate::event::{ContactPass, PointerButton, PointerEvent, PointerId, PointerPhase};
4821
4822    /// A leaf that captures on `Down`, opting into the gesture's other contacts
4823    /// when `opt_in` is set.
4824    struct Grab {
4825        opt_in: bool,
4826    }
4827    impl Widget for Grab {
4828        fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4829            bc.constrain(Size::new(20.0, 20.0))
4830        }
4831        fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
4832        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
4833            if let InputEvent::Pointer(p) = event
4834                && p.phase == PointerPhase::Down
4835            {
4836                ctx.capture_pointer();
4837                if self.opt_in {
4838                    ctx.capture_contacts();
4839                }
4840            }
4841            EventResult::Handled
4842        }
4843    }
4844
4845    /// A single-child container that takes the gesture over from its captured
4846    /// child on any `Move`.
4847    struct Taker {
4848        inner: ChildPod,
4849    }
4850    impl Widget for Taker {
4851        fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
4852            self.inner.layout_child(ctx, bc);
4853            bc.constrain(Size::new(20.0, 20.0))
4854        }
4855        fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
4856            self.inner.paint_child(ctx, scene);
4857        }
4858        fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
4859            match event {
4860                InputEvent::Pointer(p)
4861                    if p.phase == PointerPhase::Move && self.inner.is_active() =>
4862                {
4863                    ctx.release_captured_child(&mut self.inner);
4864                    EventResult::Handled
4865                }
4866                _ => self.inner.event_child(ctx, event),
4867            }
4868        }
4869    }
4870
4871    /// An outer pod around a [`Taker`] around a [`Grab`].
4872    fn nested(opt_in: bool) -> ChildPod {
4873        let mut pod = ChildPod::new(Box::new(Taker {
4874            inner: ChildPod::new(Box::new(Grab { opt_in })),
4875        }));
4876        pod.layout_child(
4877            &mut LayoutCtx::new(),
4878            &BoxConstraints::tight(Size::new(20.0, 20.0)),
4879        );
4880        pod
4881    }
4882
4883    fn pointer(phase: PointerPhase) -> InputEvent {
4884        InputEvent::Pointer(PointerEvent {
4885            phase,
4886            position: Point::new(5.0, 5.0),
4887            button: PointerButton::Primary,
4888        })
4889    }
4890
4891    /// Dispatch into `pod` from a fresh context; whether a capture release
4892    /// bubbled out of it.
4893    fn released(pod: &mut ChildPod, phase: PointerPhase) -> bool {
4894        let mut unit = ();
4895        let mut ctx = EventCtx::new(&mut unit, Point::ZERO, Size::new(20.0, 20.0));
4896        pod.event_child(&mut ctx, &pointer(phase));
4897        ctx.is_capture_released()
4898    }
4899
4900    fn inner_active(pod: &mut ChildPod) -> bool {
4901        pod.widget_mut()
4902            .downcast_mut::<Taker>()
4903            .expect("a Taker")
4904            .inner
4905            .is_active()
4906    }
4907
4908    #[test]
4909    fn releasing_the_opted_in_child_signals_and_bubbles() {
4910        let mut pod = nested(true);
4911        assert!(!released(&mut pod, PointerPhase::Down));
4912        assert!(pod.holds_contact_opt_in(), "the opt-in below is recorded");
4913        assert!(
4914            released(&mut pod, PointerPhase::Move),
4915            "and its release bubbles"
4916        );
4917        assert!(!inner_active(&mut pod), "the child left the active path");
4918        assert!(pod.is_active(), "the container that took over keeps it");
4919    }
4920
4921    #[test]
4922    fn releasing_a_child_without_the_opt_in_clears_the_link_silently() {
4923        let mut pod = nested(false);
4924        released(&mut pod, PointerPhase::Down);
4925        assert!(!pod.holds_contact_opt_in());
4926        assert!(!released(&mut pod, PointerPhase::Move));
4927        assert!(!inner_active(&mut pod));
4928    }
4929
4930    #[test]
4931    fn a_non_claimant_contact_cannot_release_anything() {
4932        let mut pod = nested(true);
4933        released(&mut pod, PointerPhase::Down);
4934        let _pass = ContactPass::enter(PointerId::touch(1), true);
4935        assert!(!released(&mut pod, PointerPhase::Move));
4936        assert!(inner_active(&mut pod), "the claimant's link stands");
4937    }
4938}