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