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