Skip to main content

frus_widgets/
ui.rs

1//! The driver: walks the tree carrying a context (translation + clip), produces the
2//! [`Scene`] and the hit-test maps (click, focus, scroll), and makes it possible to find a
3//! widget by identity so that keyboard/edit events can be routed to it.
4
5use std::hash::{Hash, Hasher};
6
7use frus_core::{
8    Affine, ClipShape, Color, FontWeight, LayerFilter, LayerTransform, Path, Point, Primitive,
9    Rect, Scene, Size, TextStyle,
10};
11use frus_layout::{Layout, NodeId, Overflowing, Side};
12
13use crate::shortcuts::{Intent, KeyStroke};
14
15use crate::barrier::ModalBarrier;
16use crate::dismiss::{DismissPhase, Dismissable};
17use crate::dragdrop::{DragSource, DropZone};
18use crate::hero::{lerp_rect, HeroSpot};
19use crate::interaction::{Status, WidgetId};
20use crate::pageview::PageSnap;
21use crate::physics::{ScrollMetrics, ScrollPhysics};
22use crate::portal::Placement;
23use crate::positioned::Positioning;
24use crate::refresh::Refreshable;
25use crate::relayout::Constraints;
26use crate::runtime::Runtime;
27use crate::theme::Theme;
28use crate::widget::Widget;
29
30/// Parallax factor of the screen behind during a transition (0 = fixed, 1 = follows
31/// exactly). It is what gives a native navigation its depth.
32const NAV_PARALLAX: f32 = 0.3;
33
34/// Thickness of a scrollbar, in pixels (`scrollbar.dart:12`).
35const BAR_SIZE: f32 = 8.0;
36/// Minimum length of a thumb (`scrollbar.dart:15`). It is the tap target: a thumb has to
37/// be catchable by a pointer, whatever fraction of the content the viewport shows.
38const MIN_THUMB: f32 = 48.0;
39/// The thumb's opacity at rest, over `on_surface` (`scrollbar.dart:248`, `:242`). The two
40/// numbers are the reference's, and which one applies follows the surface's own
41/// brightness — this crate's scheme carries no brightness flag, so it is read off the
42/// surface rather than declared.
43/// The gap between the thumb and the edge of the viewport (`scrollbar.dart:14`).
44const BAR_MARGIN: f32 = 2.0;
45const THUMB_ON_DARK: f32 = 0.30;
46const THUMB_ON_LIGHT: f32 = 0.10;
47/// And under a pointer (`scrollbar.dart:245`, `:239`) — a five-fold change on a light
48/// surface, which is what makes a bar you can barely see worth reaching for.
49const THUMB_HOVER_ON_DARK: f32 = 0.65;
50const THUMB_HOVER_ON_LIGHT: f32 = 0.50;
51/// And while it is being dragged (`scrollbar.dart:244`, `:238`), which arrives at once:
52/// the hand is already on it, and a fade would only lag behind the grab.
53const THUMB_DRAG_ON_DARK: f32 = 0.75;
54const THUMB_DRAG_ON_LIGHT: f32 = 0.60;
55
56/// One **axis** of a scroll area, as a scrollbar needs to see it: which way it runs, where
57/// it has got to, how far it may go, whether its numbers run backwards, and what the area
58/// itself said about whether it wants a bar at all.
59#[derive(Copy, Clone)]
60struct Bar {
61    vertical: bool,
62    offset: f32,
63    max: f32,
64    reverse: bool,
65    /// The area's own answer, over the application's. See [`crate::Scrollbars`].
66    asked: Option<crate::physics::Scrollbars>,
67    /// Whether the area asked for its bar to stay put instead of fading.
68    visible: Option<bool>,
69}
70
71/// A scrollbar thumb (for hit-testing a drag).
72#[derive(Copy, Clone, Debug)]
73pub struct Scrollbar {
74    /// The scrollable area the bar belongs to.
75    pub id: WidgetId,
76    /// Whether the bar runs vertically; otherwise it is horizontal.
77    pub vertical: bool,
78    /// The thumb's rectangle, in absolute coordinates.
79    pub thumb: Rect,
80    /// Start and length of the track, along the axis.
81    pub track_start: f32,
82    /// The track's length along the axis.
83    pub track_len: f32,
84    /// The thumb's length along the axis.
85    pub thumb_len: f32,
86    /// Offset maximal correspondant.
87    pub max: f32,
88    /// The offsets of a **reversed** axis run the other way along the track: offset 0
89    /// is the far end, so the thumb rests there and a drag towards the start raises the
90    /// number. See [`Scrollable::reverse_y`].
91    pub reverse: bool,
92    /// **How present the bar is**, `0.0..=1.0`. A bar at zero is painted by nothing and
93    /// cannot be grabbed — but it is still here, because a mouse that comes near it
94    /// brings it back.
95    pub opacity: f32,
96    /// The rectangle a **mouse** has to be inside for the bar to notice it: the track,
97    /// widened to take in a tap target's worth of room around the thumb. The reference
98    /// reaches out like this on purpose (`scrollbar.dart:762`) — a bar 8 pixels wide,
99    /// resting at a tenth of an opacity, is not something a hand aims at precisely.
100    pub reach: Rect,
101}
102
103/// A scrollable area of the frame: where it is, how far it may scroll, and how it
104/// is meant to behave at its edges.
105///
106/// The registry the shell and the runtime both read. It carries the area's own
107/// [`ScrollPhysics`] only when the widget asked for one — resolving the default is
108/// the application's job, through [`Scrollable::physics_or`].
109#[derive(Copy, Clone, Debug)]
110pub struct Scrollable {
111    /// The scrollable area's identity.
112    pub id: WidgetId,
113    /// The viewport, in absolute coordinates.
114    pub viewport: Rect,
115    /// Largest horizontal offset the content may rest at (0 = nothing to scroll).
116    pub max_x: f32,
117    /// Largest vertical offset the content may rest at.
118    pub max_y: f32,
119    /// The area's own physics, when it asked for one.
120    pub physics: Option<ScrollPhysics>,
121    /// The [`crate::RefreshIndicator`] area this scrollable sits inside, when there is one.
122    /// Movement refused at its **top** edge feeds that area's pull instead of the
123    /// overscroll glow.
124    pub refresh: Option<WidgetId>,
125    /// Set when this area rests **only on pages** ([`crate::PageView`]): the release
126    /// springs to a page boundary instead of flinging.
127    pub page: Option<PageSnap>,
128    /// Whether the horizontal axis is **reversed** — offset 0 is the right-hand end of
129    /// the content rather than the left.
130    pub reverse_x: bool,
131    /// Whether the vertical axis is **reversed** — offset 0 is the **bottom** of the
132    /// content rather than the top.
133    ///
134    /// Offsets are measured from the end an axis starts at, which is what makes a
135    /// reversed list stay where it is when content arrives: a conversation resting at
136    /// offset 0 is resting at the newest message, and the newest message is wherever the
137    /// end now happens to be. Measuring from the top instead would leave the view
138    /// drifting away from the end every time something was appended, which is the one
139    /// thing this exists to prevent.
140    ///
141    /// Everything the *user* does is unchanged by it. A finger pushes the content the
142    /// way it moves, in either direction; only the arithmetic between that push and the
143    /// number changes sign, in one place.
144    pub reverse_y: bool,
145    /// What the content asked to be kept in view, in the frame's coordinates.
146    pub keep_visible: Option<KeepVisible>,
147    /// The scroll region this one sits **inside**, when there is one.
148    ///
149    /// Scroll areas nest — a sideways strip inside a page that scrolls down — and
150    /// bringing something into view means moving every one of them, from the nearest
151    /// outwards. Identities are hashes and carry no ancestry, so the walk records it.
152    pub host: Option<WidgetId>,
153}
154
155impl Scrollable {
156    /// This area's physics, falling back to the application's choice.
157    pub fn physics_or(&self, default: ScrollPhysics) -> ScrollPhysics {
158        self.physics.unwrap_or(default)
159    }
160
161    /// Whether a finger may move this area at all, resting at `offset`.
162    ///
163    /// The rule the reference states plainly: the user can change the offset **if, and
164    /// only if, there is content outside the viewport to reveal** — or the content is
165    /// already displaced, having shrunk under an offset it no longer reaches. An area that
166    /// fails this takes no drag at all: not a short one, not a refused one, and so no
167    /// end-of-content glow either. An edge effect where there is no edge to meet is a
168    /// statement about the content that is not true.
169    ///
170    /// A [`crate::RefreshIndicator`] listening above is the exception, and the same one the
171    /// reference makes: a list of two items must still pull down to reload.
172    pub fn accepts_user_offset(&self, offset: (f32, f32)) -> bool {
173        self.max_x > 0.0
174            || self.max_y > 0.0
175            || offset.0 != 0.0
176            || offset.1 != 0.0
177            || self.refresh.is_some()
178    }
179
180    /// The horizontal axis as the physics sees it, at `offset`.
181    pub fn metrics_x(&self, offset: f32) -> ScrollMetrics {
182        ScrollMetrics::new(offset, self.max_x, self.viewport.width)
183    }
184
185    /// The vertical axis as the physics sees it, at `offset`.
186    pub fn metrics_y(&self, offset: f32) -> ScrollMetrics {
187        ScrollMetrics::new(offset, self.max_y, self.viewport.height)
188    }
189
190    /// Turns a **screen** delta — how far the content was pushed, in pixels — into the
191    /// change it makes to this area's offsets.
192    ///
193    /// Two sign changes, and both have a reason. The content moves *opposite* the
194    /// number: dragging down reveals what is above, which is a smaller offset. And a
195    /// reversed axis counts from the other end, so the same drag raises it instead.
196    ///
197    /// One function rather than a minus sign at each of the five places a delta becomes
198    /// an offset — the wheel, the drag, the release fling, and both axes of each — so
199    /// that a reversed scroll is right in all of them or in none.
200    pub fn offset_delta(&self, screen: (f32, f32)) -> (f32, f32) {
201        (
202            if self.reverse_x { screen.0 } else { -screen.0 },
203            if self.reverse_y { screen.1 } else { -screen.1 },
204        )
205    }
206
207    /// The edge a refusal happened at, given the refused movement **in offset space**.
208    ///
209    /// A negative refusal means the content was pulled towards the axis's start, and
210    /// where that is on screen is exactly what reversing changes: the start of a
211    /// reversed vertical axis is the **bottom**. Without this the glow would flash at
212    /// the far end of a conversation the moment it refused to go further back.
213    pub fn refused_edge(&self, vertical: bool, refused: f32) -> crate::overscroll::GlowEdge {
214        let reversed = if vertical {
215            self.reverse_y
216        } else {
217            self.reverse_x
218        };
219        crate::overscroll::edge_for(vertical, if reversed { -refused } else { refused })
220    }
221
222    /// The edge an axis **starts** at, which is where a pull-to-refresh listens.
223    pub fn start_edge(&self, vertical: bool) -> crate::overscroll::GlowEdge {
224        self.refused_edge(vertical, -1.0)
225    }
226
227    /// The offset that brings `target` fully inside this viewport, and how far the
228    /// content moves to get there.
229    ///
230    /// The **least** movement that does it: a box already in view does not budge, one
231    /// off the near edge comes to that edge and no further. Centring it instead would
232    /// make Tab through a form scroll on every stop, which is motion nobody asked for.
233    ///
234    /// A target **too big** for the viewport gets its leading edge aligned, which falls
235    /// out of asking about that edge first: a tall field half off the top comes down to
236    /// the top rather than up to the bottom, because its start is what you read.
237    ///
238    /// Both halves are returned because they are not the same number. The offset is
239    /// clamped to what this region can actually do, and an outer region has to be told
240    /// how far the target really moved — not how far it was asked to.
241    pub fn reveal(&self, target: Rect, current: (f32, f32)) -> ((f32, f32), (f32, f32)) {
242        let shift = |lo: f32, hi: f32, near: f32, far: f32| {
243            if near < lo {
244                lo - near
245            } else if far > hi {
246                hi - far
247            } else {
248                0.0
249            }
250        };
251        let wanted = (
252            shift(
253                self.viewport.x,
254                self.viewport.x + self.viewport.width,
255                target.x,
256                target.x + target.width,
257            ),
258            shift(
259                self.viewport.y,
260                self.viewport.y + self.viewport.height,
261                target.y,
262                target.y + target.height,
263            ),
264        );
265        // `offset_delta` turns a movement of the content into a movement of the offset,
266        // and it is its own inverse — it negates or it does not — so the same function
267        // turns the offset that was allowed back into the content movement it buys.
268        let delta = self.offset_delta(wanted);
269        let next = (
270            (current.0 + delta.0).clamp(0.0, self.max_x),
271            (current.1 + delta.1).clamp(0.0, self.max_y),
272        );
273        let applied = (next.0 - current.0, next.1 - current.1);
274        (next, self.offset_delta(applied))
275    }
276
277    /// The offset that puts `target` in the **middle** of this viewport, and how far the
278    /// content moves to get there.
279    ///
280    /// The same pair as [`Scrollable::reveal`], and the same clamp — which is what makes
281    /// the first item of a row rest at the start rather than half off it. A target
282    /// bigger than the viewport centres too, which leaves it hanging over both edges,
283    /// and that is the honest answer: there is no offset that shows all of it.
284    pub fn centre(&self, target: Rect, current: (f32, f32)) -> ((f32, f32), (f32, f32)) {
285        let wanted = (
286            (self.viewport.x + self.viewport.width / 2.0) - (target.x + target.width / 2.0),
287            (self.viewport.y + self.viewport.height / 2.0) - (target.y + target.height / 2.0),
288        );
289        let delta = self.offset_delta(wanted);
290        let next = (
291            (current.0 + delta.0).clamp(0.0, self.max_x),
292            (current.1 + delta.1).clamp(0.0, self.max_y),
293        );
294        let applied = (next.0 - current.0, next.1 - current.1);
295        (next, self.offset_delta(applied))
296    }
297
298    /// The offset that brings `keep` into view under its own policy, and how far the
299    /// content moves from **rest** to get there.
300    ///
301    /// Absolute, because [`KeepVisible::rect`] is recorded at rest: the base is offset
302    /// zero, not wherever the region currently is.
303    pub fn keep_offset(&self, keep: KeepVisible) -> ((f32, f32), (f32, f32)) {
304        match keep.centre {
305            true => self.centre(keep.rect, (0.0, 0.0)),
306            false => self.reveal(keep.rect, (0.0, 0.0)),
307        }
308    }
309
310    /// Where the content's leading edge sits, relative to the viewport's, at `offset`.
311    ///
312    /// Normally minus the offset. On a reversed axis the content is anchored to the far
313    /// end, so offset 0 puts the content's *end* against the viewport's — which is also
314    /// the answer for content too short to scroll, where it sits at the bottom of the
315    /// box rather than the top.
316    pub fn content_origin(&self, offset: (f32, f32), content: Size) -> (f32, f32) {
317        let axis = |reverse: bool, offset: f32, viewport: f32, content: f32| {
318            if reverse {
319                viewport - content + offset
320            } else {
321                -offset
322            }
323        };
324        (
325            axis(self.reverse_x, offset.0, self.viewport.width, content.width),
326            axis(
327                self.reverse_y,
328                offset.1,
329                self.viewport.height,
330                content.height,
331            ),
332        )
333    }
334}
335
336/// A box a scroll region has been asked to **keep in view**, and which box it is.
337///
338/// A tab bar wider than its window is what this is for. Declared by the content through
339/// [`Widget::keep_visible`], carried on the region so it survives a repaint boundary
340/// like everything else about the region, and acted on by
341/// [`crate::Runtime::sync_visible`].
342#[derive(Copy, Clone, Debug, PartialEq)]
343pub struct KeepVisible {
344    /// Which box this is. The region acts when this **changes** and not otherwise: the
345    /// box moves as the region scrolls, so a region chasing the box itself would pin
346    /// the content in place and no finger could move it.
347    pub key: u64,
348    /// The box: in the declaring widget's own coordinates when it comes back from
349    /// [`Widget::keep_visible`], and in the frame's — **as it would sit at offset zero**
350    /// — when it reaches the region.
351    ///
352    /// At rest rather than where it is actually drawn, and that is the whole reason the
353    /// answer can be trusted. A box recorded where it is drawn is a box whose position
354    /// already contains the current offset, so the offset that would centre it can only
355    /// be worked out **relative** to wherever the region happens to be — and on the
356    /// first frame, before anything has been retained, there is no wherever to be
357    /// relative to. Recorded at rest, the answer is absolute: the same number on the
358    /// first frame and the hundredth.
359    pub rect: Rect,
360    /// Put it in the **middle** of the window rather than merely inside it. What a tab
361    /// bar wants: a selected tab flush against the edge reads as the end of the row.
362    /// Clamped either way, so the first tab still rests at the start.
363    pub centre: bool,
364}
365
366/// Direction of arrow-key focus navigation.
367#[derive(Clone, Copy, Debug, PartialEq, Eq)]
368pub enum FocusDirection {
369    /// The up arrow: focus moves to the widget above.
370    Up,
371    /// The down arrow: focus moves to the widget below.
372    Down,
373    /// The left arrow: focus moves to the widget on the left.
374    Left,
375    /// The right arrow: focus moves to the widget on the right.
376    Right,
377}
378
379#[derive(Clone)]
380struct Hit<Msg> {
381    id: WidgetId,
382    rect: Rect,
383    /// The message the target emits, or `None` for a target that **swallows** input
384    /// without doing anything — an [`crate::AbsorbPointer`]. Such a target still wins
385    /// the hit test, which is exactly what stops the click reaching what is behind it.
386    msg: Option<Msg>,
387    /// **Inverse** transform (screen → the flat frame) of a transformed subtree
388    /// (`Transform::scale`/`rotate`/a composition): the test point is passed through it to
389    /// get back to the untransformed frame where `rect` lives. `None` = an axis-aligned,
390    /// untransformed rect (the common case).
391    xform: Option<Affine>,
392}
393
394impl<Msg> Hit<Msg> {
395    /// `true` when `point` (in screen coordinates) falls inside the target, taking any
396    /// transform on the subtree into account.
397    fn contains(&self, point: Point) -> bool {
398        let p = self.xform.map_or(point, |inv| inv.apply(point));
399        self.rect.contains(p)
400    }
401}
402
403/// The painted output of a repaint-boundary subtree, cached (see `paintcache.rs`) and
404/// replayed as is on a *hit*. Erased behind a `Box<dyn Any>` in the cache; brought back down
405/// to `Msg` here (one `Msg` instance per app → the `downcast` always succeeds).
406#[derive(Clone)]
407struct BoundaryData<Msg> {
408    prims: Vec<frus_core::Primitive>,
409    hits: Vec<Hit<Msg>>,
410    long_presses: Vec<Hit<Msg>>,
411    dismisses: Vec<Msg>,
412    focusables: Vec<Focusable>,
413    scrollables: Vec<Scrollable>,
414    draggables: Vec<(WidgetId, Rect)>,
415    drag_sources: Vec<DragSource>,
416    drop_zones: Vec<DropZone>,
417    inks: Vec<(WidgetId, Rect)>,
418    semantics: Vec<(WidgetId, Rect, frus_core::SemanticsProperties)>,
419    system_ui: Vec<(Rect, crate::SystemUiOverlayStyle)>,
420}
421
422/// Lengths of the builder's collections on entering a boundary: the lower bounds of the
423/// slices to capture on the way out.
424struct Snapshot {
425    scene: usize,
426    hits: usize,
427    long_presses: usize,
428    dismisses: usize,
429    focusables: usize,
430    scrollables: usize,
431    draggables: usize,
432    drag_sources: usize,
433    drop_zones: usize,
434    inks: usize,
435    semantics: usize,
436    system_ui: usize,
437    overlays: usize,
438    focus_scope_start: Option<usize>,
439}
440
441/// Lower bounds of the **interaction** registries at the start of a transformed composition
442/// (a scale/rotation layer): only what the subtree has just added is re-mapped. Distinct from
443/// [`Snapshot`] (the boundary cache) because it **includes `reorderables`** — those are never
444/// cached but do have to be transformed. See [`transform_interaction_registries`].
445/// Lengths of the registries a [`ModalBarrier`] withholds from, on entering its subtree: the
446/// point each is truncated back to on the way out. Distinct from [`XformBase`] because a
447/// barrier also covers the **scene** and the **scrollbars**, and distinct from [`Snapshot`]
448/// because it covers the registries that are never cached.
449/// What is withdrawn from a layer of an [`crate::IndexedStack`] that is not the shown one:
450/// its paint, its input and its place in the accessibility tree. Not `absorb` — the layer
451/// is not a scrim over what is behind it, it is simply not there for the reader.
452const HIDDEN_LAYER: crate::barrier::ModalBarrier = crate::barrier::ModalBarrier {
453    pointer: true,
454    absorb: false,
455    paint: true,
456    semantics: true,
457};
458
459struct BarrierBase {
460    scene: usize,
461    hits: usize,
462    long_presses: usize,
463    focusables: usize,
464    scrollables: usize,
465    scrollbars: usize,
466    draggables: usize,
467    drag_sources: usize,
468    drop_zones: usize,
469    inks: usize,
470    reorderables: usize,
471    interactives: usize,
472    semantics: usize,
473}
474
475struct XformBase {
476    hits: usize,
477    long_presses: usize,
478    focusables: usize,
479    scrollables: usize,
480    draggables: usize,
481    drag_sources: usize,
482    drop_zones: usize,
483    inks: usize,
484    reorderables: usize,
485    semantics: usize,
486}
487
488/// Node count of the subtree **if** it is "plain" — that is, if the boundary and **all** its
489/// descendants take the default walk branch (children in prefix order): not scrollable, not a
490/// navigator, not a virtualised list, no `layout_builder`, not a stack, no overlay, no
491/// continuous animation. The count then follows the walk's rect order **exactly**, which is
492/// what makes the fingerprint and the bit-for-bit replay correct. `None` = a subtree that
493/// cannot be cached (it is repainted in full — the safe fallback).
494fn plain_subtree_len<Msg>(widget: &dyn Widget<Msg>) -> Option<usize> {
495    if widget.continuous()
496        || widget.scroll_content().is_some()
497        || widget.interactive().is_some()
498        || widget.fitted().is_some()
499        || widget.rotated_quarter_turns().is_some()
500        || widget.navigator().is_some()
501        // Only *whether*, never *what*: these two ask if the widget windows its
502        // children at all, and a size it will not read is the honest argument.
503        || widget.virtual_list(Size::ZERO).is_some()
504        || widget.page_view().is_some()
505        || widget.overflow_box().is_some()
506        || widget.constraints_transform().is_some()
507        || widget.layout_builder().is_some()
508        || widget.stack()
509        || widget.overlay().is_some()
510        // A reorderable (a Kanban card, a draggable header): its bounds feed the reorderables
511        // registry, which is not cached — so its subtree is not put in the paint cache.
512        || widget.reorder_index().is_some()
513    {
514        return None;
515    }
516    let mut n = 1;
517    for child in widget.children() {
518        n += plain_subtree_len(child.as_ref())?;
519    }
520    Some(n)
521}
522
523/// Quantises a `[0,1]` float for the fingerprint (independent of tiny binary differences: two
524/// visually identical progresses ⇒ the same fingerprint).
525fn quant(x: f32) -> i32 {
526    (x * 4096.0).round() as i32
527}
528
529/// Adds to `h` everything the paint reads from a `Status`, **except** the time (excluded: a
530/// cacheable boundary holds no `continuous` widget).
531fn hash_status<H: Hasher>(s: &Status, h: &mut H) {
532    (s.interaction as u8).hash(h);
533    s.focused.hash(h);
534    s.cursor.hash(h);
535    s.selection.hash(h);
536    s.composing.hash(h);
537    quant(s.hover_progress).hash(h);
538    quant(s.focus_progress).hash(h);
539    quant(s.press_progress).hash(h);
540    quant(s.opacity).hash(h);
541    quant(s.value).hash(h);
542    // Sub-region hover (milestone 208): a change of position repaints the highlight.
543    s.hover_cursor.map(|p| (quant(p.x), quant(p.y))).hash(h);
544}
545
546/// The result of building an interface for one frame.
547/// A **shortcut / action scope**: a subtree that binds keystrokes, answers intents, or
548/// both, together with the range of focus stops it contains.
549///
550/// The range is how "focus is inside this subtree" is answered without an ancestor test:
551/// the walk is depth-first, so a subtree's focus stops are contiguous, and the focused
552/// stop's index either falls inside the range or does not.
553struct Scope<Msg> {
554    strokes: Vec<(KeyStroke, Intent)>,
555    callbacks: Vec<(KeyStroke, Msg)>,
556    actions: Vec<(Intent, Msg)>,
557    listeners: Vec<(Intent, Msg)>,
558    /// The focus stops this subtree contains, as indices into `focusables`.
559    range: std::ops::Range<usize>,
560}
561
562/// A **focus stop**: somewhere the keyboard can land, and what the traversal should make
563/// of it.
564#[derive(Clone, Copy, Debug)]
565pub struct Focusable {
566    /// The widget's identity.
567    pub id: WidgetId,
568    /// Its **visible** box this frame: the box it was given, clipped by whatever
569    /// encloses it. Empty when it is scrolled out of sight, which is what a click test
570    /// wants — a tap on empty space must not land on something nobody can see.
571    pub rect: Rect,
572    /// Its box this frame, **unclipped**. What the geometry is really about: where the
573    /// widget is, whether or not any of it is on screen. Arrow navigation places
574    /// candidates by this, and [`Ui::reveal`] works out what to scroll from it.
575    pub bounds: Rect,
576    /// The [`crate::AutofillGroup`] this stop sits in — the form it is part of — when
577    /// there is one (milestone 512).
578    pub form: Option<WidgetId>,
579    /// The nearest enclosing scroll region, when there is one. What could bring this
580    /// stop into view, and the first link of the chain [`Ui::reveal`] walks.
581    pub scroll: Option<WidgetId>,
582    /// Focusable by a click, but **passed over by Tab** — the reference's
583    /// `ExcludeFocusTraversal`. The two are separate questions.
584    pub skip: bool,
585    /// An explicit traversal position, smallest first. `None` means tree order, which is
586    /// where everything sits until someone says otherwise.
587    pub order: Option<f32>,
588    /// The traversal group it belongs to, within which its order is resolved.
589    pub group: Option<WidgetId>,
590}
591
592/// The **interface** of one frame: what the walk over a widget tree produced.
593///
594/// Besides the [`Scene`] to paint it holds everything the shell asks of a frame afterwards —
595/// what a click, a long press or a dismissal answers, what can take the focus, which areas
596/// scroll, which widgets can be dragged or dropped on — so that events are routed against
597/// the frame the reader saw. It is made by [`build_ui`] and read until the next frame
598/// replaces it.
599pub struct Ui<Msg = crate::callback::Callback> {
600    scene: Scene,
601    hits: Vec<Hit<Msg>>,
602    /// Long-press targets (id, visible bounds, message).
603    long_presses: Vec<Hit<Msg>>,
604    /// Overlay dismissal messages, from the bottom to the **top**.
605    dismisses: Vec<Msg>,
606    focusables: Vec<Focusable>,
607    /// **Focus scope**: index of the topmost modal overlay's first focusable —
608    /// Tab/arrows/click-to-focus are trapped from there on (`None` = no modal, every
609    /// focusable takes part).
610    focus_scope_start: Option<usize>,
611    /// (id, viewport, offset max x, offset max y)
612    scrollables: Vec<Scrollable>,
613    scrollbars: Vec<Scrollbar>,
614    draggables: Vec<(WidgetId, Rect)>,
615    drag_sources: Vec<DragSource>,
616    drop_zones: Vec<DropZone>,
617    /// **Inked surfaces**: (id, the box the ink is clipped to). Tracked separately from
618    /// clicking because the splash needs the surface's **whole** box — where the finger
619    /// landed inside it, and how far the circle has to travel to cover it — which a
620    /// click target, recorded as its *visible* part, does not give.
621    inks: Vec<(WidgetId, Rect)>,
622    /// **Reorderables** (column headers, Kanban cards): (id, visible bounds). Tracked
623    /// independently of clicking — a card is not clickable but can still be picked up and
624    /// dropped onto.
625    reorderables: Vec<(WidgetId, Rect)>,
626    /// Interactive viewports (`InteractiveViewer`): (id, the screen viewport). The shell
627    /// routes panning (dragging) and zooming (wheel / pinch) to them.
628    interactives: Vec<(WidgetId, Rect)>,
629    /// Pull-to-refresh areas of the frame, with their configuration.
630    refreshes: Vec<Refreshable>,
631    /// Swipe-to-dismiss items of the frame, with their configuration.
632    dismissables: Vec<Dismissable>,
633    /// Draggable sheets of the frame, with their configuration and the scroll areas
634    /// walked inside each.
635    sheets: Vec<crate::sheet::SheetArea>,
636    wants_animation: bool,
637    /// The accessibility tree: semantic nodes (id, bounds, annotation), in paint order. The
638    /// shell maps it onto AccessKit.
639    semantics: Vec<(WidgetId, Rect, frus_core::SemanticsProperties)>,
640    /// The regions saying what the system bars over them should look like (paint order).
641    system_ui: Vec<(Rect, crate::SystemUiOverlayStyle)>,
642    /// Boxes whose children ran outside them, with the edge and the amount.
643    overflows: Vec<Overflowing>,
644    /// Shortcut and action scopes, in the order the walk closed them — innermost first
645    /// among any that overlap, which is the order the resolution wants.
646    scopes: Vec<Scope<Msg>>,
647    /// Keystroke listeners, with the focus stops they cover.
648    #[allow(clippy::type_complexity)]
649    listeners: Vec<(
650        std::ops::Range<usize>,
651        std::rc::Rc<dyn Fn(KeyStroke) -> Option<Msg>>,
652    )>,
653}
654
655impl<Msg: Clone> Ui<Msg> {
656    /// The scene to hand to the renderer.
657    /// Boxes in this frame whose children did not fit inside them.
658    ///
659    /// Empty is the normal answer. A non-empty one means content is being drawn outside
660    /// its parent — invisible where it is clipped, unhittable where it leaves the window,
661    /// and silent either way until something asks. That silence is what let a task row's
662    /// delete button sit off-screen through three milestones.
663    pub fn overflows(&self) -> &[Overflowing] {
664        &self.overflows
665    }
666
667    /// The scrollbars this frame drew, and therefore the ones a pointer may drag. Empty
668    /// where the platform draws none — see [`crate::Scrollbars`].
669    pub fn scrollbars(&self) -> &[Scrollbar] {
670        &self.scrollbars
671    }
672
673    /// The [`Scene`] this frame painted.
674    pub fn scene(&self) -> &Scene {
675        &self.scene
676    }
677
678    /// The frame's **accessibility tree**: every meaningful node with its bounds on screen and
679    /// its annotation ([`frus_core::SemanticsProperties`]). Paint order (= reading order). The shell
680    /// pushes it to AccessKit.
681    pub fn semantics(&self) -> &[(WidgetId, Rect, frus_core::SemanticsProperties)] {
682        &self.semantics
683    }
684
685    /// What the system bars should look like over this frame (#46): the regions under
686    /// `status_bar` answer for the status bar and those under `navigation_bar` for the
687    /// other, the one drawn last first and field by field. What none states is left
688    /// `None`, for [`crate::SystemUiOverlayStyle::resolve`] to take from the theme.
689    ///
690    /// The shell picks the two points: the first row of the screen's content under the
691    /// status bar, and the last one above the navigation bar.
692    pub fn system_ui_style(
693        &self,
694        status_bar: Point,
695        navigation_bar: Point,
696    ) -> crate::SystemUiOverlayStyle {
697        crate::system_ui::style_under(&self.system_ui, status_bar, navigation_bar)
698    }
699
700    /// `true` when a widget animates continuously (the framework must redraw).
701    /// Whether **this tree's widgets** want the next frame: a spinner turning, an ink
702    /// ripple spreading, a pull being held.
703    ///
704    /// It answers for the widgets and nothing else. A fetch still in flight also owes the
705    /// screen a frame — see [`frus_core::images_in_flight`] — and that is the *process's*
706    /// business, asked by whoever drives the loop. It used to be folded in here, which
707    /// made this method's answer depend on what an unrelated part of the program happened
708    /// to be doing: in the test suite, where everything shares one process, a refresh area
709    /// with nothing pulled asked for frames whenever another test was loading an image
710    /// (milestone 411).
711    pub fn wants_animation(&self) -> bool {
712        self.wants_animation
713    }
714
715    /// Identity of the topmost clickable widget containing `point`.
716    pub fn hit(&self, point: Point) -> Option<WidgetId> {
717        self.hits
718            .iter()
719            .rev()
720            .find(|hit| hit.contains(point))
721            .map(|hit| hit.id)
722    }
723
724    /// The message tied to a given clickable widget. `None` for a target that swallows
725    /// input without emitting anything.
726    ///
727    /// It is always paired with [`hit`](Self::hit), so the two have to agree on which
728    /// target an identity means: every registered target therefore has one of its own,
729    /// the dismissal barrier below an overlay included.
730    pub fn msg_for(&self, id: WidgetId) -> Option<Msg> {
731        self.hits
732            .iter()
733            .find(|hit| hit.id == id)
734            .and_then(|hit| hit.msg.clone())
735    }
736
737    /// Dismissal message of the **topmost** overlay (for Escape).
738    pub fn top_dismiss(&self) -> Option<Msg> {
739        self.dismisses.last().cloned()
740    }
741
742    /// **Long-press** message of the topmost target containing `point`.
743    pub fn long_press_at(&self, point: Point) -> Option<Msg> {
744        self.long_presses
745            .iter()
746            .rev()
747            .find(|hit| hit.contains(point))
748            .and_then(|hit| hit.msg.clone())
749    }
750
751    /// The frame's **pull-to-refresh areas**, with the configuration each was built
752    /// with. The shell steps them and reads their `refreshing` flag from here.
753    pub fn refresh_areas(&self) -> &[Refreshable] {
754        &self.refreshes
755    }
756
757    /// The frame's **dismissible items**, with the configuration each was built with.
758    pub fn dismissables(&self) -> &[Dismissable] {
759        &self.dismissables
760    }
761
762    /// The frame's **sheets**, with the configuration each was built with and the scroll
763    /// areas walked inside it.
764    pub fn sheets(&self) -> &[crate::sheet::SheetArea] {
765        &self.sheets
766    }
767
768    /// The sheet whose panel is under `point`, the last one walked winning — which is
769    /// the one painted on top.
770    pub fn sheet_at(&self, point: Point) -> Option<&crate::sheet::SheetArea> {
771        self.sheets.iter().rev().find(|sheet| sheet.contains(point))
772    }
773
774    /// The sheet `id`, when it is in the frame.
775    pub fn sheet(&self, id: WidgetId) -> Option<&crate::sheet::SheetArea> {
776        self.sheets.iter().find(|sheet| sheet.id == id)
777    }
778
779    /// The sheet the scroll area `area` was walked inside, when there is one — the
780    /// innermost, should sheets nest, since an outer one counts the inner one's areas too.
781    pub fn sheet_holding(&self, area: WidgetId) -> Option<&crate::sheet::SheetArea> {
782        self.sheets
783            .iter()
784            .filter(|sheet| sheet.areas.contains(&area))
785            .min_by_key(|sheet| sheet.areas.len())
786    }
787
788    /// The topmost dismissible item containing `point` — the candidate a press has to
789    /// weigh against the scrollable underneath it.
790    pub fn dismissable_at(&self, point: Point) -> Option<Dismissable> {
791        self.dismissables
792            .iter()
793            .rev()
794            .find(|item| item.rect.contains(point))
795            .copied()
796    }
797
798    /// Topmost focusable widget containing `point`: (id, its bounds).
799    pub fn focus_hit(&self, point: Point) -> Option<(WidgetId, Rect)> {
800        self.focus_pool()
801            .iter()
802            .rev()
803            .find(|f| f.rect.contains(point))
804            .map(|f| (f.id, f.rect))
805    }
806
807    /// The **participating** focusables: those of the topmost modal scope when there is one
808    /// (a focus trap), otherwise all of them.
809    fn focus_pool(&self) -> &[Focusable] {
810        match self.focus_scope_start {
811            Some(start) => &self.focusables[start.min(self.focusables.len())..],
812            None => &self.focusables,
813        }
814    }
815
816    /// Nearest focusable in the given **direction** (arrow navigation, a geometric policy):
817    /// among the focusables whose centre lies on the right side, it minimises the distance
818    /// along the main axis with a penalty on the cross-axis offset. `None` when there is
819    /// nothing in that direction.
820    pub fn focus_directional(
821        &self,
822        current: WidgetId,
823        direction: FocusDirection,
824    ) -> Option<WidgetId> {
825        // The starting point may be outside the scope (focus from before it opened); the
826        // **candidates**, though, are trapped inside it.
827        let from = self
828            .focusables
829            .iter()
830            .find(|f| f.id == current)
831            .map(|f| &f.rect)?;
832        let center = |r: &Rect| (r.x + r.width * 0.5, r.y + r.height * 0.5);
833        let (fx, fy) = center(from);
834
835        let mut best: Option<(WidgetId, f32)> = None;
836        for candidate in self.focus_pool() {
837            let (id, rect) = (candidate.id, &candidate.rect);
838            if id == current {
839                continue;
840            }
841            let (cx, cy) = center(rect);
842            // (how far ahead in the direction, the cross-axis offset)
843            let (ahead, cross) = match direction {
844                FocusDirection::Right => (cx - fx, (cy - fy).abs()),
845                FocusDirection::Left => (fx - cx, (cy - fy).abs()),
846                FocusDirection::Down => (cy - fy, (cx - fx).abs()),
847                FocusDirection::Up => (fy - cy, (cx - fx).abs()),
848            };
849            // Inside a **cone** around the direction (not a plain half-plane): a candidate
850            // almost aligned on the cross axis but barely "ahead" (slightly different widths)
851            // is not a directional target.
852            if ahead <= 0.5 || cross > ahead * 3.0 {
853                continue;
854            }
855            let score = ahead + cross * 3.0;
856            if best.map(|(_, s)| score < s).unwrap_or(true) {
857                best = Some((id, score));
858            }
859        }
860        best.map(|(id, _)| id)
861    }
862
863    /// Next/previous focus stop for Tab, wrapping. With no current focus, the first (or
864    /// the last).
865    ///
866    /// **Tree order, unless something said otherwise.** Stops carrying an explicit order
867    /// are sorted ahead of those that do not, smallest first, and the sort is applied
868    /// **within a traversal group** rather than across the whole frame — so a reordered
869    /// dialog does not reshuffle the page behind it. Stops marked `skip` are passed over
870    /// here while remaining reachable by a click: the keyboard's order and the pointer's
871    /// reach are two different questions.
872    pub fn focus_next(&self, current: Option<WidgetId>, forward: bool) -> Option<WidgetId> {
873        let order = self.traversal_order();
874        if order.is_empty() {
875            return None;
876        }
877        let n = order.len();
878        // A current focus **outside the scope** (taken before the modal opened) is treated
879        // as "none": Tab enters the trap.
880        match current.and_then(|c| order.iter().position(|id| *id == c)) {
881            Some(i) => {
882                let j = if forward {
883                    (i + 1) % n
884                } else {
885                    (i + n - 1) % n
886                };
887                Some(order[j])
888            }
889            None => Some(order[if forward { 0 } else { n - 1 }]),
890        }
891    }
892
893    /// The Tab order: the participating stops, minus those that asked to be skipped,
894    /// with each traversal group's members sorted among themselves.
895    ///
896    /// A **stable** sort on the order key, so everything without one keeps tree order and
897    /// ties do too — the property that makes an explicit order a local statement rather
898    /// than a rearrangement of the whole frame.
899    pub fn traversal_order(&self) -> Vec<WidgetId> {
900        let pool: Vec<&Focusable> = self.focus_pool().iter().filter(|f| !f.skip).collect();
901        if pool.iter().all(|f| f.order.is_none()) {
902            return pool.iter().map(|f| f.id).collect();
903        }
904        let mut out: Vec<WidgetId> = Vec::with_capacity(pool.len());
905        let mut i = 0;
906        while i < pool.len() {
907            // A group's members are contiguous: the walk is depth-first, so a subtree's
908            // stops arrive together.
909            let group = pool[i].group;
910            let mut j = i;
911            while j < pool.len() && pool[j].group == group {
912                j += 1;
913            }
914            let mut run: Vec<&Focusable> = pool[i..j].to_vec();
915            run.sort_by(|a, b| {
916                let key = |f: &Focusable| f.order.unwrap_or(f32::INFINITY);
917                key(a).total_cmp(&key(b))
918            });
919            out.extend(run.iter().map(|f| f.id));
920            i = j;
921        }
922        out
923    }
924
925    /// What a keystroke means here: the messages it sends, in the order they should be
926    /// applied.
927    ///
928    /// The resolution, in one place because it is the whole feature:
929    ///
930    /// 1. Only scopes **containing the focused stop** are candidates; with nothing
931    ///    focused, all of them are. Innermost first — `scopes` is filled on the way *out*
932    ///    of the walk, so that order is already the one wanted.
933    /// 2. A [`crate::KeyboardListener`] gets the first refusal, and a `None` from it lets
934    ///    the stroke carry on.
935    /// 3. A direct binding ([`crate::CallbackShortcuts`]) sends its message.
936    /// 4. Otherwise a [`crate::Shortcuts`] binding names an **intent**, and the innermost
937    ///    [`crate::Actions`] that answers it supplies the message. An intent nobody
938    ///    answers does nothing — deliberately: a key bound to a meaning the current screen
939    ///    has no answer for should be inert, not an error.
940    /// 5. Every [`crate::ActionListener`] watching that intent adds its message too.
941    ///
942    /// Empty means *nobody wanted it*, and the caller should carry on to whatever it would
943    /// have done with the key.
944    pub fn keystroke(&self, stroke: KeyStroke, focused: Option<WidgetId>) -> Vec<Msg> {
945        let at = focused.and_then(|id| self.focusables.iter().position(|f| f.id == id));
946        let covers = |range: &std::ops::Range<usize>| match at {
947            Some(i) => range.contains(&i),
948            None => true,
949        };
950
951        for (range, on_key) in self.listeners.iter().rev() {
952            if covers(range) {
953                if let Some(msg) = on_key(stroke) {
954                    return vec![msg];
955                }
956            }
957        }
958
959        let mut out = Vec::new();
960        for scope in self.scopes.iter().filter(|s| covers(&s.range)) {
961            if let Some((_, msg)) = scope.callbacks.iter().find(|(s, _)| stroke.matches(s)) {
962                out.push(msg.clone());
963                return out;
964            }
965            let Some((_, intent)) = scope.strokes.iter().find(|(s, _)| stroke.matches(s)) else {
966                continue;
967            };
968            let answer = self
969                .scopes
970                .iter()
971                .filter(|s| covers(&s.range))
972                .find_map(|s| {
973                    s.actions
974                        .iter()
975                        .find(|(i, _)| i == intent)
976                        .map(|(_, m)| m.clone())
977                });
978            if let Some(msg) = answer {
979                out.push(msg);
980                for watcher in self.scopes.iter().filter(|s| covers(&s.range)) {
981                    for (i, m) in &watcher.listeners {
982                        if i == intent {
983                            out.push(m.clone());
984                        }
985                    }
986                }
987            }
988            return out;
989        }
990        out
991    }
992
993    /// Topmost scrollable area containing `point`.
994    pub fn scroll_hit(&self, point: Point) -> Option<Scrollable> {
995        self.scrollables
996            .iter()
997            .rev()
998            .find(|area| area.viewport.contains(point))
999            .copied()
1000    }
1001
1002    /// **Every** scrollable area containing `point`, innermost first — the whole stack a
1003    /// finger has under it.
1004    ///
1005    /// [`Self::scroll_hit`] answers *which one is on top*, which is the right question for
1006    /// a wheel or a scrollbar. A finger asks a different one: a strip that only slides
1007    /// across, sitting in a page that only scrolls down, cannot take a drag downwards, and
1008    /// the page behind it can. Deciding that needs the ones behind as well.
1009    pub fn scroll_chain(&self, point: Point) -> impl Iterator<Item = Scrollable> + '_ {
1010        self.scrollables
1011            .iter()
1012            .rev()
1013            .filter(move |area| area.viewport.contains(point))
1014            .copied()
1015    }
1016
1017    /// Frame of a **focusable** widget `id`, when it is present this frame — so the shell can
1018    /// find the focused field's geometry (vertical caret movement).
1019    pub fn widget_rect(&self, id: WidgetId) -> Option<Rect> {
1020        self.focusables
1021            .iter()
1022            .find(|f| f.id == id)
1023            .map(|f| f.rect)
1024            // Falls back to the **reorderables** registry. Kanban cards (and headers that are
1025            // reorderable but **not** sortable) are not focusable: without this fallback the
1026            // shell cannot find their bounds, and the whole **vertical** drag preview (ghost,
1027            // insertion line, reflow) as well as the *insert-after* routing stay dead.
1028            .or_else(|| {
1029                self.reorderables
1030                    .iter()
1031                    .find(|(rid, _)| *rid == id)
1032                    .map(|(_, rect)| *rect)
1033            })
1034    }
1035
1036    /// The box of the inked surface `id`, when the frame has one — what the shell needs
1037    /// to start a splash: where inside it the finger landed, and how big it is.
1038    ///
1039    /// The **topmost** match wins, as the hit-test does: two inked surfaces can overlap,
1040    /// and the one that took the tap is the one drawn last.
1041    pub fn ink_box(&self, id: WidgetId) -> Option<Rect> {
1042        self.inks
1043            .iter()
1044            .rev()
1045            .find(|(iid, _)| *iid == id)
1046            .map(|(_, rect)| *rect)
1047    }
1048
1049    /// The identities of **every** focusable widget of the frame (the scope included) — so
1050    /// the shell can detect the focus **disappearing** (an overlay closed) and restore it.
1051    pub fn focusable_ids(&self) -> impl Iterator<Item = WidgetId> + '_ {
1052        self.focusables.iter().map(|f| f.id)
1053    }
1054
1055    /// The focus stops of the **form** `id` is part of, in tree order, each with its box
1056    /// this frame (unclipped: a field scrolled out of sight is still a field of the form).
1057    /// A stop in no [`crate::AutofillGroup`] is a form of one. Empty when `id` is not a
1058    /// focus stop this frame.
1059    ///
1060    /// Stops, not fields: which of them take part in autofill is a question for the
1061    /// widgets ([`crate::Widget::autofill_hints`]), which the shell has and this does not.
1062    pub fn form_of(&self, id: WidgetId) -> Vec<(WidgetId, Rect)> {
1063        let Some(stop) = self.focusables.iter().find(|f| f.id == id) else {
1064            return Vec::new();
1065        };
1066        match stop.form {
1067            Some(form) => self.group_stops(form),
1068            None => vec![(stop.id, stop.bounds)],
1069        }
1070    }
1071
1072    /// The [`crate::AutofillGroup`] focus stop `id` sits in, if it is a stop this frame
1073    /// and sits in one.
1074    pub fn form_group(&self, id: WidgetId) -> Option<WidgetId> {
1075        self.focusables.iter().find(|f| f.id == id)?.form
1076    }
1077
1078    /// Every focus stop inside the [`crate::AutofillGroup`] `group`, in tree order, with
1079    /// its unclipped box. Empty once the group has left the screen — which is the moment
1080    /// a form is finished with, and the platform may offer to save what was typed in it.
1081    pub fn group_stops(&self, group: WidgetId) -> Vec<(WidgetId, Rect)> {
1082        self.focusables
1083            .iter()
1084            .filter(|f| f.form == Some(group))
1085            .map(|f| (f.id, f.bounds))
1086            .collect()
1087    }
1088
1089    /// Frame (viewport) of the scrollable area `id`, when there is one — so the shell can find
1090    /// a multi-line field's width/height (caret following).
1091    pub fn scrollable_viewport(&self, id: WidgetId) -> Option<Rect> {
1092        self.scrollables
1093            .iter()
1094            .find(|area| area.id == id)
1095            .map(|area| area.viewport)
1096    }
1097
1098    /// The scroll offsets that would bring `id` **into view**, nearest region first.
1099    ///
1100    /// Empty when it is already in view, when the frame does not contain it, or when
1101    /// nothing around it scrolls. Nobody has to check first: an empty answer is the
1102    /// answer that nothing needs to move.
1103    ///
1104    /// Nested regions are walked from the inside out, and each is told where the target
1105    /// **ended up** rather than where it started. A sideways strip inside a page that
1106    /// scrolls down moves the target across, and the page then decides how far down it
1107    /// still is; asking both about the original box would have them fight over the same
1108    /// pixels and land somewhere neither wanted.
1109    pub fn reveal(&self, id: WidgetId, runtime: &crate::Runtime) -> Vec<(WidgetId, (f32, f32))> {
1110        let Some(stop) = self.focusables.iter().find(|f| f.id == id) else {
1111            return Vec::new();
1112        };
1113        let mut target = stop.bounds;
1114        let mut host = stop.scroll;
1115        let mut moves = Vec::new();
1116        // Regions nest a handful deep at most. The bound is a backstop against a chain
1117        // that loops, which cannot happen and would hang the frame if it did.
1118        for _ in 0..16 {
1119            let Some(hid) = host else { break };
1120            let Some(area) = self.scrollables.iter().find(|area| area.id == hid) else {
1121                break;
1122            };
1123            let current = runtime.scroll.get(&hid).copied().unwrap_or((0.0, 0.0));
1124            let (offset, shift) = area.reveal(target, current);
1125            if shift.0 != 0.0 || shift.1 != 0.0 {
1126                target = target.translate(shift.0, shift.1);
1127                moves.push((hid, offset));
1128            }
1129            host = area.host;
1130        }
1131        moves
1132    }
1133
1134    /// Every scrollable area of the frame — geometry, bounds and physics — which is
1135    /// what the runtime needs to drive momentum and edges.
1136    pub fn scroll_regions(&self) -> &[Scrollable] {
1137        &self.scrollables
1138    }
1139
1140    /// The area `id`, when it is present this frame.
1141    pub fn scroll_region(&self, id: WidgetId) -> Option<Scrollable> {
1142        self.scrollables.iter().find(|area| area.id == id).copied()
1143    }
1144
1145    /// SingleChildScrollView bounds `(id, max_x, max_y)` of every scrollable area.
1146    pub fn scrollable_maxes(&self) -> Vec<(WidgetId, f32, f32)> {
1147        self.scrollables
1148            .iter()
1149            .map(|area| (area.id, area.max_x, area.max_y))
1150            .collect()
1151    }
1152
1153    /// Scrollbar thumb under `point` (to start a drag).
1154    pub fn scrollbar_at(&self, point: Point) -> Option<Scrollbar> {
1155        self.scrollbars
1156            .iter()
1157            .rev()
1158            .find(|bar| bar.opacity > 0.0 && bar.thumb.contains(point))
1159            .copied()
1160    }
1161
1162    /// The scrollbar a **pointer is near enough to wake**: inside its track, or within a
1163    /// tap target's reach of its thumb.
1164    ///
1165    /// Wider than [`Ui::scrollbar_at`], and — unlike it — it answers for a bar that has
1166    /// faded out entirely, which is the whole point. A bar nobody can see is a bar nobody
1167    /// can grab, so the reference lets a mouse coming near one bring it back first
1168    /// (`scrollbar.dart:766`); only then is it something to take hold of.
1169    pub fn scrollbar_near(&self, point: Point) -> Option<Scrollbar> {
1170        self.scrollbars
1171            .iter()
1172            .rev()
1173            .find(|bar| bar.reach.contains(point))
1174            .copied()
1175    }
1176
1177    /// Topmost draggable widget under `point`: (id, its bounds).
1178    pub fn draggable_at(&self, point: Point) -> Option<(WidgetId, Rect)> {
1179        self.draggables
1180            .iter()
1181            .rev()
1182            .find(|(_, rect)| rect.contains(point))
1183            .map(|(id, rect)| (*id, *rect))
1184    }
1185
1186    /// Topmost **drag source** under `point`, with what it carries.
1187    pub fn drag_source_at(&self, point: Point) -> Option<DragSource> {
1188        self.drag_sources
1189            .iter()
1190            .rev()
1191            .find(|source| source.rect.contains(point))
1192            .copied()
1193    }
1194
1195    /// Topmost **drop target** under `point`.
1196    pub fn drop_zone_at(&self, point: Point) -> Option<DropZone> {
1197        self.drop_zones
1198            .iter()
1199            .rev()
1200            .find(|zone| zone.rect.contains(point))
1201            .copied()
1202    }
1203
1204    /// Topmost **reorderable** widget under `point`: its id. The basis of reordering
1205    /// drag-and-drop (the source on press, the target on drop) — independent of clickability.
1206    pub fn reorderable_at(&self, point: Point) -> Option<WidgetId> {
1207        self.reorderables
1208            .iter()
1209            .rev()
1210            .find(|(_, rect)| rect.contains(point))
1211            .map(|(id, _)| *id)
1212    }
1213
1214    /// **Every** reorderable under `point`, topmost first.
1215    ///
1216    /// [`Self::reorderable_at`] answers *which one is on top*, which is the right question
1217    /// for a press: whatever is nearest the finger is what the finger grabbed. A **drop**
1218    /// asks a different one, because the thing on top may be a grip that nothing can be
1219    /// dropped on — see [`Widget::reorder_droppable`] — and the row it belongs to, which
1220    /// can, is the one behind it.
1221    pub fn reorderables_at(&self, point: Point) -> impl Iterator<Item = WidgetId> + '_ {
1222        self.reorderables
1223            .iter()
1224            .rev()
1225            .filter(move |(_, rect)| rect.contains(point))
1226            .map(|(id, _)| *id)
1227    }
1228
1229    /// Every **reorderable** of the frame, with its box, in paint order.
1230    pub fn reorderables(&self) -> &[(WidgetId, Rect)] {
1231        &self.reorderables
1232    }
1233
1234    /// Topmost **interactive** viewport (`InteractiveViewer`) under `point`: (id, its screen
1235    /// viewport). The shell routes panning and zooming to it.
1236    pub fn interactive_at(&self, point: Point) -> Option<(WidgetId, Rect)> {
1237        self.interactives
1238            .iter()
1239            .rev()
1240            .find(|(_, rect)| rect.contains(point))
1241            .map(|(id, rect)| (*id, *rect))
1242    }
1243
1244    /// Interactive viewports `(id, screen viewport)`, to drive the inertia (fling) and the
1245    /// clamping of the pan on the framework's side.
1246    pub fn interactive_bounds(&self) -> Vec<(WidgetId, Rect)> {
1247        self.interactives.clone()
1248    }
1249}
1250
1251/// Paints a **striped band** along the edge of every box whose children ran past it.
1252///
1253/// Milestone 335 taught the layout to notice an overflow and the shell to say so in the
1254/// console. That is the half a developer reads; this is the half a *screenshot* shows —
1255/// and a screenshot is what a bug report is made of. Three of this framework's own
1256/// milestones went looking for defects that a band would have pointed at: a delete button
1257/// laid out past the card (333, 334), a segmented control 221 px outside its parent (335).
1258///
1259/// The look is the reference's, because the point of it is to be recognised on sight:
1260/// black and yellow diagonal stripes, three quarters opaque, over a tenth of the box along
1261/// the offending edge, with the edge and the number written across it. Not the mechanism,
1262/// though — the reference paints one repeating gradient and this paints the stripes,
1263/// because a repeating diagonal gradient is a shader feature and a parallelogram is four
1264/// points.
1265///
1266/// **Debug builds only.** A band is a message to whoever is building the application, and
1267/// in a release build there is nobody to read it — the reference draws the same line.
1268fn paint_overflow_bands(scene: &mut Scene, overflows: &[Overflowing]) {
1269    if !cfg!(debug_assertions) || overflows.is_empty() {
1270        return;
1271    }
1272    // The reference's two colours and its 10 % band, exactly.
1273    const BLACK: Color = Color::rgba(0.0, 0.0, 0.0, 0.75);
1274    const YELLOW: Color = Color::rgba(1.0, 1.0, 0.0, 0.75);
1275    const FRACTION: f32 = 0.1;
1276    /// The stripes run at 45°; this is their period measured along the x axis.
1277    const PERIOD: f32 = 14.0;
1278
1279    let (outer, outer_bounds) = (scene.current_clip(), scene.current_bounds());
1280    scene.set_clip(Rect::UNBOUNDED);
1281    for over in overflows {
1282        let r = over.rect;
1283        let band = match over.side {
1284            Side::Left => Rect::new(r.x, r.y, r.width * FRACTION, r.height),
1285            Side::Right => Rect::new(
1286                r.x + r.width * (1.0 - FRACTION),
1287                r.y,
1288                r.width * FRACTION,
1289                r.height,
1290            ),
1291            Side::Top => Rect::new(r.x, r.y, r.width, r.height * FRACTION),
1292            Side::Bottom => Rect::new(
1293                r.x,
1294                r.y + r.height * (1.0 - FRACTION),
1295                r.width,
1296                r.height * FRACTION,
1297            ),
1298        };
1299        if band.width <= 0.0 || band.height <= 0.0 {
1300            continue;
1301        }
1302        scene.set_clip(band);
1303        scene.fill_rect(band, BLACK);
1304        // One parallelogram per stripe, sheared by the band's height so that every one
1305        // of them crosses it at 45° whatever shape the band is.
1306        let shear = band.height;
1307        let mut x = band.x - shear;
1308        while x < band.x + band.width {
1309            let stripe = Path::new()
1310                .move_to(Point::new(x, band.y))
1311                .line_to(Point::new(x + PERIOD / 2.0, band.y))
1312                .line_to(Point::new(x + PERIOD / 2.0 + shear, band.y + band.height))
1313                .line_to(Point::new(x + shear, band.y + band.height))
1314                .close();
1315            scene.fill_path(&stripe, YELLOW);
1316            x += PERIOD;
1317        }
1318        // The label is **not** clipped to the band: a vertical one is usually longer than
1319        // the box is tall, and half a sentence is worse than a sentence written over
1320        // whatever is beside it. The reference does not clip it either.
1321        scene.set_clip(Rect::UNBOUNDED);
1322        scene.set_bounds(Rect::UNBOUNDED);
1323        paint_overflow_label(scene, band, over);
1324    }
1325    scene.set_clip(outer);
1326    scene.set_bounds(outer_bounds);
1327}
1328
1329/// Writes the reference's sentence across a band: which edge overflowed, and by how much.
1330///
1331/// The console already says it — but the console is on the developer's machine and the
1332/// band is on the device, and a photograph of a phone is what half the bug reports in the
1333/// world are made of. A striped edge says *something* is too big; `RIGHT OVERFLOWED BY 86
1334/// PIXELS` says which edge, and by enough that you can tell a forgotten padding from a
1335/// missing wrap without running anything.
1336///
1337/// The words are the reference's, exactly, so that searching for them finds the same
1338/// answers. So are the numbers: 7.5 px, the heaviest weight there is, dark red on an
1339/// opaque white plate — a label that must stay readable over stripes cannot be subtle —
1340/// centred on the band's outer edge and turned a quarter turn on the vertical ones.
1341fn paint_overflow_label(scene: &mut Scene, band: Rect, over: &Overflowing) {
1342    /// The reference's label metrics.
1343    const SIZE: f32 = 7.5;
1344    /// The gap between the label and the outer edge of the band.
1345    const PAD: f32 = 1.0;
1346    /// `0xFF900000`.
1347    const INK: Color = Color::rgb(0.5647059, 0.0, 0.0);
1348
1349    let side = match over.side {
1350        Side::Left => "LEFT",
1351        Side::Right => "RIGHT",
1352        Side::Top => "TOP",
1353        Side::Bottom => "BOTTOM",
1354    };
1355    let text = format!("{side} OVERFLOWED BY {} PIXELS", format_pixels(over.amount));
1356    // The reference asks for an 800 weight; the heaviest this framework has is bold.
1357    let style = TextStyle::new(SIZE).weight(FontWeight::Bold);
1358    let size = frus_text::measure_styled(&text, SIZE, FontWeight::Bold, false);
1359
1360    // Where the middle of the label's leading edge goes, before any rotation, and by how
1361    // much the whole thing then turns about that point.
1362    let (anchor, angle) = match over.side {
1363        Side::Left => (
1364            Point::new(band.x + SIZE + PAD, band.y + band.height / 2.0),
1365            std::f32::consts::FRAC_PI_2,
1366        ),
1367        Side::Right => (
1368            Point::new(band.x + band.width - SIZE - PAD, band.y + band.height / 2.0),
1369            -std::f32::consts::FRAC_PI_2,
1370        ),
1371        Side::Top => (Point::new(band.x + band.width / 2.0, band.y + PAD), 0.0),
1372        Side::Bottom => (
1373            Point::new(band.x + band.width / 2.0, band.y + band.height - SIZE - PAD),
1374            0.0,
1375        ),
1376    };
1377    if angle == 0.0 {
1378        let at = Point::new(anchor.x - size.width / 2.0, anchor.y);
1379        scene.fill_rect(Rect::from_point_size(at, size), Color::WHITE);
1380        scene.text(at, text, &style.resolved(), INK);
1381        return;
1382    }
1383    // A rotation of a group, not of a glyph: the plate and the sentence turn together and
1384    // neither has to know it is turning.
1385    //
1386    // The group is painted **at the origin** rather than where it lands, because a layer
1387    // is rendered flat into a window-sized texture before it is composited: a vertical
1388    // label is written across a box near the right edge, so laid out flat it would run
1389    // off the texture and lose its last word before the rotation ever brought it back
1390    // inside. The transform is the one that carries the origin to where the label goes —
1391    // shift it half its width to the left, turn it, and land it on the anchor.
1392    let flat = Rect::from_point_size(Point::ZERO, size);
1393    let matrix = Affine::translation(-size.width / 2.0, 0.0)
1394        .then(Affine::rotation(angle))
1395        .then(Affine::translation(anchor.x, anchor.y));
1396    scene.transformed(LayerTransform::new(matrix), move |scene: &mut Scene| {
1397        scene.fill_rect(flat, Color::WHITE);
1398        scene.text(flat.origin(), text, &style.resolved(), INK);
1399    });
1400}
1401
1402/// How the reference writes an overflow: whole pixels past ten, one decimal past one, and
1403/// three significant figures below that — because the difference between 0.5 px and
1404/// 0.0001 px is the difference between a layout bug and a rounding error.
1405fn format_pixels(value: f32) -> String {
1406    if value > 10.0 {
1407        format!("{value:.0}")
1408    } else if value > 1.0 {
1409        format!("{value:.1}")
1410    } else {
1411        // Three significant figures: the first one sits at the first non-zero decimal.
1412        let leading = if value > 0.0 {
1413            (-value.abs().log10().floor()) as i32
1414        } else {
1415            0
1416        };
1417        format!("{value:.*}", (leading + 2).clamp(0, 12) as usize)
1418    }
1419}
1420
1421/// Identity of the `index`-th child: **by key** when the child declares one (stable whatever
1422/// its position), **positional** otherwise. It must be used everywhere a child identity is
1423/// derived (render, collection, lookup, animations) so they all stay consistent.
1424pub(crate) fn child_id<Msg>(parent: WidgetId, index: usize, child: &dyn Widget<Msg>) -> WidgetId {
1425    match child.key() {
1426        Some(key) => parent.keyed(key),
1427        None => parent.child(index),
1428    }
1429}
1430
1431/// A widget's **effective** style for layout: its `style()`, whose size is **replaced by the
1432/// runtime's interpolated size** when the widget is animated (`Widget::anim_size` +
1433/// `Container::animated_size`). This is the only place where an animated size enters layout —
1434/// used **identically** by [`build_layout`] and by the cache fingerprint (`hash_node`), which
1435/// is what keeps them consistent (the cache stays invalid for as long as the size moves).
1436pub(crate) fn effective_style<Msg>(
1437    widget: &dyn Widget<Msg>,
1438    id: WidgetId,
1439    runtime: &Runtime,
1440    theme: &Theme,
1441) -> frus_layout::Style {
1442    // The theme's say on size and spacing (milestone 309), before the runtime's
1443    // animated overrides below — an animation in flight wins over a resting default.
1444    let mut style = widget.style_themed(theme);
1445    if widget.anim_size().is_some() {
1446        if let Some(size) = runtime.anim_size(id) {
1447            style.width = frus_layout::Dimension::Length(size.width);
1448            style.height = frus_layout::Dimension::Length(size.height);
1449        }
1450    }
1451    if widget.anim_padding().is_some() {
1452        if let Some(padding) = runtime.anim_padding(id) {
1453            style.padding = padding;
1454        }
1455    }
1456    // A share of the parent, growing or shrinking. `Percent` and not `Length`, because
1457    // the whole point of the quantity is that it does not know what it will come to — and
1458    // an axis whose factor is unset goes back to following its content rather than to
1459    // nought, which is what "unset" has always meant here.
1460    if widget.anim_fractions().is_some() {
1461        if let Some((width, height)) = runtime.anim_fractions(id) {
1462            let dim = |factor: Option<f32>| match factor {
1463                Some(f) => frus_layout::Dimension::Percent(f),
1464                None => frus_layout::Dimension::Auto,
1465            };
1466            style.width = dim(width);
1467            style.height = dim(height);
1468        }
1469    }
1470    // A dismissed item closing its gap. It collapses along the axis it was **not**
1471    // swiped along — a row swiped sideways loses its height — which is what makes the
1472    // neighbours slide up rather than the row narrow to a sliver. Only an explicit
1473    // length can shrink: an `Auto` box has no number to scale, which is why
1474    // `Dismissible` asks for a size (see its docs).
1475    // A sheet's panel, as tall as the retained share of its box. The same `Percent` as a
1476    // fraction above, for the same reason: the share is the quantity, and the box it is a
1477    // share of is the layout's to know. Hashed through here as well, so the cache misses
1478    // on every frame the sheet moves.
1479    if let Some(spec) = widget.sheet() {
1480        style.height = frus_layout::Dimension::Percent(runtime.sheet_size(id, spec));
1481    }
1482    if let Some(spec) = widget.dismissible() {
1483        if let Some(factor) = runtime.dismiss_extent_factor(id) {
1484            let axis = if spec.axis.is_horizontal() {
1485                &mut style.height
1486            } else {
1487                &mut style.width
1488            };
1489            if let frus_layout::Dimension::Length(extent) = *axis {
1490                *axis = frus_layout::Dimension::Length(extent * factor);
1491            }
1492        }
1493    }
1494    style
1495}
1496
1497/// Builds the main layout tree (a scrollable is a **leaf**). `id`/`runtime` are what inject
1498/// the animated size through [`effective_style`], following the walk's identity scheme
1499/// (`child_id`) **exactly**.
1500pub(crate) fn build_layout<'a, Msg>(
1501    widget: &'a dyn Widget<Msg>,
1502    id: WidgetId,
1503    runtime: &'a Runtime,
1504    theme: &Theme,
1505    layout: &mut Layout<'a, BaselineData>,
1506) -> NodeId {
1507    let (node, fills) = build_layout_scoped(widget, id, runtime, theme, layout, true);
1508    // A root that wants to fill has no parent to fill: the flex machinery below needs one
1509    // on each side of the question. A percentage of the room the layout is being computed
1510    // in is the same answer without a parent, and it degrades to hugging the content when
1511    // that room is unbounded — a scrollable measuring its content, a natural size.
1512    if fills.horizontal {
1513        layout.fill_root(node, true);
1514    }
1515    if fills.vertical {
1516        layout.fill_root(node, false);
1517    }
1518    node
1519}
1520
1521/// The axes a subtree asks to **fill** rather than shrink-wrap.
1522///
1523/// It is not a property of one widget. A column whose row fills the width is itself as
1524/// wide as the room it was given, because the row inside it took that room — so the
1525/// request travels up as the layout is built, and each container answers it with the one
1526/// thing a container can do about it: grow along its own axis, or stretch across it.
1527#[derive(Clone, Copy, Default)]
1528pub(crate) struct Fills {
1529    horizontal: bool,
1530    vertical: bool,
1531}
1532
1533impl Fills {
1534    /// What a widget asks for on its own account.
1535    ///
1536    /// A straight copy since milestone 405: the widget's answer already has a flag per
1537    /// axis, where it used to be one direction and a container that wanted both had no way
1538    /// to say so.
1539    fn own<Msg>(widget: &dyn Widget<Msg>, theme: &Theme) -> Self {
1540        let asked = widget.fill_axes(theme);
1541        Fills {
1542            horizontal: asked.horizontal,
1543            vertical: asked.vertical,
1544        }
1545    }
1546
1547    fn merge(self, other: Fills) -> Self {
1548        Fills {
1549            horizontal: self.horizontal || other.horizontal,
1550            vertical: self.vertical || other.vertical,
1551        }
1552    }
1553
1554    /// The half of a child's request a container passes on to **its** parent, given
1555    /// which way the container runs. Along its own axis a container divides the room up
1556    /// and the request stops there; across it, the container's size *is* the child's, so
1557    /// the request has to keep going.
1558    /// Whether one axis is asked for.
1559    fn wants(self, horizontal: bool) -> bool {
1560        if horizontal {
1561            self.horizontal
1562        } else {
1563            self.vertical
1564        }
1565    }
1566
1567    /// Drops any axis this box was **given a size on**: it has already been told how big
1568    /// to be, and a request to fill cannot reach past that answer.
1569    fn bounded_by(self, style: &frus_layout::Style) -> Self {
1570        let auto = |d| matches!(d, frus_layout::Dimension::Auto);
1571        Fills {
1572            horizontal: self.horizontal && auto(style.width),
1573            vertical: self.vertical && auto(style.height),
1574        }
1575    }
1576
1577    fn across(self, direction: frus_layout::FlexDirection) -> Self {
1578        if direction.is_horizontal() {
1579            Fills {
1580                horizontal: false,
1581                vertical: self.vertical,
1582            }
1583        } else {
1584            Fills {
1585                horizontal: self.horizontal,
1586                vertical: false,
1587            }
1588        }
1589    }
1590}
1591
1592/// A layout leaf's **text baseline**, when it has one: the distance from the top of its
1593/// box down to the line its letters sit on.
1594///
1595/// It rides on the layout tree rather than being asked of the widgets afterwards because
1596/// only the layout knows where a node ended up, and only the walk that built it knows
1597/// which branch it took — a scrollable, a stack and a page view are all *leaves* here,
1598/// with their contents laid out elsewhere, and a second walk guessing at that would be a
1599/// copy of this one waiting to drift out of step.
1600pub(crate) type BaselineData = Option<f32>;
1601
1602/// An owned copy of a theme, **made in a frame of its own**.
1603///
1604/// `#[inline(never)]` is the whole point: inlined, the eleven-kilobyte temporary is built
1605/// in the caller's frame, and the caller here is the recursive walk that pays for its
1606/// frame once per level of the tree. Behind the call it is built, copied into the
1607/// allocation, and gone before the walk recurses.
1608#[inline(never)]
1609fn owned_theme(theme: &Theme) -> std::rc::Rc<Theme> {
1610    std::rc::Rc::new(theme.clone())
1611}
1612
1613/// The body of [`build_layout`], carrying whether baselines are still being collected.
1614/// An [`crate::IgnoreBaseline`] turns them off for its subtree — the widgets inside still
1615/// have baselines, and the point is that nothing above may see them.
1616fn build_layout_scoped<'a, Msg>(
1617    widget: &'a dyn Widget<Msg>,
1618    id: WidgetId,
1619    runtime: &'a Runtime,
1620    theme: &Theme,
1621    layout: &mut Layout<'a, BaselineData>,
1622    baselines: bool,
1623) -> (NodeId, Fills) {
1624    // A component is built into what it stands for before anything is asked of the node,
1625    // the scope it introduces included — see `Widget::expand`.
1626    widget.expand(id, runtime, theme);
1627    // A themed subtree lays out under **its** theme, not the frame's: a theme reaches
1628    // sizes and spacing (milestone 309), so this has to happen here and not only at paint
1629    // time. `hash_node`, which fingerprints this same walk for the relayout cache, makes
1630    // the same swap — the two staying in step is what keeps the cache honest.
1631    let scoped = scoped_theme(widget, id, runtime, theme);
1632    let theme = scoped.as_deref().unwrap_or(theme);
1633    // And a **scoped surface**, the same idea one milestone later: a shell that hands a slot
1634    // a narrowed description narrows it for everything the slot builds, the deferred build
1635    // below included. Held for this subtree and put back on the way out.
1636    let _surface = widget
1637        .media_override(crate::MediaQuery::of())
1638        .map(crate::MediaQuery::install);
1639    // And the **shell**, the third inherited thing (milestone 422): a slot told what shell
1640    // it stands in composes differently — a bar grows a menu button — so it has to be in
1641    // force before `build_themed` below, exactly as the surface is.
1642    let _shell = widget.scaffold_override().map(crate::ScaffoldInfo::install);
1643    // A subtree that could not be composed until the theme was known (`ThemeBuilder`).
1644    // It has to happen **before** anything reads `children()`, and under the subtree's
1645    // own theme, which is why it sits after the swap above rather than at the call site.
1646    widget.build_in(id, runtime, theme);
1647    // Everything below an `IgnoreBaseline` keeps its baseline and loses the right to be
1648    // seen: the flag is narrowed once, here, so no branch below has to remember.
1649    let baselines = baselines && !widget.ignores_baseline();
1650    // A leaf's own baseline, or nothing when this subtree is being ignored.
1651    let own_baseline = if baselines {
1652        widget.text_baseline(theme)
1653    } else {
1654        None
1655    };
1656    // `Baseline`: the child is pushed down until **its** baseline lands `target` pixels
1657    // from the top of this box, and this box grows to contain it. If the child's own
1658    // baseline is already lower than that, there is nowhere to push it up to and the
1659    // child is top-aligned instead — the same answer the reference gives.
1660    if let Some(target) = widget.baseline_target() {
1661        let mut style = effective_style(widget, id, runtime, theme);
1662        if let Some(child) = widget.children().first() {
1663            let child = child.as_ref();
1664            let cid = child_id(id, 0, child);
1665            // A child with no baseline of its own is treated as if its baseline were its
1666            // bottom edge, which is what a box with no text amounts to.
1667            let child_baseline = natural_baseline(child, cid, runtime, theme)
1668                .unwrap_or_else(|| natural_size(child, cid, runtime, theme).height);
1669            style.padding.top += (target - child_baseline).max(0.0);
1670            let (node, fills) = build_layout_scoped(child, cid, runtime, theme, layout, baselines);
1671            return (layout.container(style, &[node]), fills);
1672        }
1673        return (layout.leaf(style, None), Fills::default());
1674    }
1675    // `RotatedBox`: a leaf whose box is the child's **natural** size, with its dimensions
1676    // **swapped** for an odd quarter turn (the rotation does affect layout). The child itself
1677    // is laid out separately (at render time).
1678    if let Some(q) = widget.rotated_quarter_turns() {
1679        let mut style = effective_style(widget, id, runtime, theme);
1680        if let Some(child) = widget.children().first() {
1681            let nat = natural_size(
1682                child.as_ref(),
1683                child_id(id, 0, child.as_ref()),
1684                runtime,
1685                theme,
1686            );
1687            let (w, h) = if q.rem_euclid(4) % 2 != 0 {
1688                (nat.height, nat.width)
1689            } else {
1690                (nat.width, nat.height)
1691            };
1692            style.width = frus_layout::Dimension::Length(w);
1693            style.height = frus_layout::Dimension::Length(h);
1694        }
1695        return (layout.leaf(style, own_baseline), Fills::default());
1696    }
1697    // `Intrinsic`: one axis is taken from what the content would **like** to be, not from
1698    // the space on offer. The content is measured once, unconstrained, and the answer is
1699    // written into this node's style as a length — the same trick `RotatedBox` uses, except
1700    // that here the child stays inside, laid out normally within the size it asked for.
1701    if let Some((axis, step)) = widget.intrinsic() {
1702        let mut style = effective_style(widget, id, runtime, theme);
1703        if let Some(child) = widget.children().first() {
1704            let child = child.as_ref();
1705            let cid = child_id(id, 0, child);
1706            let nat = natural_size(child, cid, runtime, theme);
1707            let quantise = |extent: f32| match step {
1708                Some(step) if step > 0.0 => (extent / step).ceil() * step,
1709                _ => extent,
1710            };
1711            match axis {
1712                crate::constraints::IntrinsicAxis::Width => {
1713                    style.width = frus_layout::Dimension::Length(quantise(nat.width));
1714                }
1715                crate::constraints::IntrinsicAxis::Height => {
1716                    style.height = frus_layout::Dimension::Length(quantise(nat.height));
1717                }
1718            }
1719            let (node, fills) = build_layout_scoped(child, cid, runtime, theme, layout, baselines);
1720            return (layout.container(style, &[node]), fills);
1721        }
1722        return (layout.leaf(style, own_baseline), Fills::default());
1723    }
1724    // `OverflowBox`: a leaf, because its child is laid out **separately**, to constraints of
1725    // its own. That separation is the whole feature — a child sharing this node's layout
1726    // could never be bigger than it.
1727    if widget.overflow_box().is_some() {
1728        return (
1729            layout.leaf(effective_style(widget, id, runtime, theme), own_baseline),
1730            Fills::default(),
1731        );
1732    }
1733    // A box that **transforms the space on offer**: its child is laid out in a box derived
1734    // from this one's, in a tree of its own, and this box is then as big as what came back
1735    // — held to what it was itself offered, which is the reference's
1736    // `constraints.constrain(child.size)`. `UnconstrainedBox` is this branch with the
1737    // constraint taken away.
1738    if let Some(transform) = widget.constraints_transform() {
1739        let style = effective_style(widget, id, runtime, theme);
1740        let child = widget.children().first().map(|c| c.as_ref());
1741        let owned = owned_theme(theme);
1742        let measure: frus_layout::MeasureFn<'a> = Box::new(move |w, h| {
1743            let Some(child) = child else {
1744                return Size::new(0.0, 0.0);
1745            };
1746            let (aw, free_x) = transform.width.offered(w);
1747            let (ah, free_y) = transform.height.offered(h);
1748            let mut inner: Layout<BaselineData> = Layout::new();
1749            let node = build_layout(child, child_id(id, 0, child), runtime, &owned, &mut inner);
1750            // **Handed** every axis that has a number, **asked** about every axis that does
1751            // not. `compute_scroll` already fills a lone constrained axis, for the reason a
1752            // scrollable's cross axis needs it; with numbers on both, `compute_filled` is
1753            // that same rule. Merely constraining them instead would leave a child with no
1754            // size of its own — a plain container — hugging at nothing, and a box asked how
1755            // big its child wanted to be would come back empty.
1756            match (free_x, free_y) {
1757                (false, false) => inner.compute_filled(node, aw, ah),
1758                _ => inner.compute_scroll(node, aw, ah, free_x, free_y),
1759            }
1760            let size = inner.size_of(node);
1761            // Held to the offer: a child too big for the room spills, and the box stays the
1762            // size the room was.
1763            Size::new(
1764                size.width.min(w.unwrap_or(f32::INFINITY)),
1765                size.height.min(h.unwrap_or(f32::INFINITY)),
1766            )
1767        });
1768        return (
1769            layout.measured_leaf(style, own_baseline, measure),
1770            Fills::default(),
1771        );
1772    }
1773    // A `LayoutBuilder` **is measured**: it builds its content from the space offered and
1774    // is then as big as what it built, which is the reference's
1775    // `size = constraints.constrain(child.size)`. It cannot be a plain leaf, because a
1776    // leaf answers before it has been asked and there is nothing to answer with until the
1777    // box is known; and it cannot be a container, because its child does not exist yet.
1778    //
1779    // Taffy calls the closure during the computation with the space actually available —
1780    // the same moment the reference runs its layout callback — and the closure builds the
1781    // subtree, lays it out in a `Layout` of its own, and hands back the size. An axis the
1782    // style pins is still the style's: taffy does not ask about a dimension it already
1783    // knows, so `LayoutBuilder::height(200.0)` behaves exactly as it did.
1784    if let Some(build) = widget.layout_builder() {
1785        let style = effective_style(widget, id, runtime, theme);
1786        // The closure needs an **owned** theme: a themed subtree's theme is a local in
1787        // this function, so it cannot be borrowed for as long as the closure lives. The
1788        // runtime and the widget are borrowed, which is the whole reason `Layout` carries
1789        // a lifetime.
1790        //
1791        // This is a clone of eleven kilobytes, and it used to be a silent one — `Theme`
1792        // was `Copy` until milestone 448, so `*theme` read like a pointer copy and was
1793        // not. It happens once per `LayoutBuilder`, not once per node.
1794        //
1795        // **Behind an `Rc`, and made somewhere else**, because the cost that mattered was
1796        // not the copy. A `Theme` local here and a closure that captures one are two
1797        // slots of eleven kilobytes in *this* function's frame, and a frame is charged to
1798        // every level of the walk whether or not the branch is taken: 24 736 bytes a
1799        // level, which overflowed a Windows main thread's megabyte at about forty. The
1800        // `Rc` makes the capture a pointer, and `owned_theme` keeps the copy in a frame
1801        // that pops before the recursion goes any deeper.
1802        let owned = owned_theme(theme);
1803        let cid = id.child(0);
1804        let measure: frus_layout::MeasureFn<'a> = Box::new(move |w, h| {
1805            // What taffy offers. `None` is an *intrinsic* question — how big would you
1806            // be with no limit, or with none at all — and there is no honest answer to
1807            // it here: finding out means running the application's callback
1808            // speculatively. The reference refuses these outright (`computeDryLayout`
1809            // asserts); we build at the offered numbers, substituting zero for what is
1810            // not offered, and leave the unoffered axis free in the nested computation
1811            // so the content's own size comes back.
1812            let offered = Size::new(w.unwrap_or(0.0), h.unwrap_or(0.0));
1813            let child = build(offered);
1814            let mut inner: Layout<BaselineData> = Layout::new();
1815            let node = build_layout(child.as_ref(), cid, runtime, &owned, &mut inner);
1816            // The content is **handed** the offered box, not asked what it would like:
1817            // it was built from that box. An axis that was not offered is the one being
1818            // asked about, and stays free so the content's own size comes back.
1819            match (w, h) {
1820                (Some(w), Some(h)) => inner.compute_filled(node, w, h),
1821                _ => inner.compute_scroll(
1822                    node,
1823                    offered.width,
1824                    offered.height,
1825                    w.is_none(),
1826                    h.is_none(),
1827                ),
1828            }
1829            inner.size_of(node)
1830        });
1831        return (
1832            layout.measured_leaf(style, own_baseline, measure),
1833            Fills::own(widget, theme),
1834        );
1835    }
1836    // **A viewport is sized from its content on the axis it does not scroll.**
1837    //
1838    // A scrollable area declares a size on the axis it scrolls — that is what a viewport
1839    // is, a window shorter than what is behind it. On the *other* axis there is nothing to
1840    // choose: the content is as tall as it is, and the viewport is that tall. Until
1841    // milestone 485 a horizontal strip took the 200 px that belongs to a vertical one, so a
1842    // scrollable tab bar 48 px tall claimed 200 and pushed its panel a hundred and fifty
1843    // pixels into empty space (#65).
1844    //
1845    // It cannot be answered by the widget alone, and that is the whole reason this lives
1846    // here. A scroll host lays its content out **against** the viewport on the axis that
1847    // does not scroll — the content of a horizontal strip is given the viewport's height —
1848    // so a viewport that said `Auto` there would hand the content nothing and come back
1849    // nothing tall, the circle closing at zero. Breaking it takes a measurement, at the one
1850    // moment the runtime and the theme are both to hand.
1851    if let Some(content) = widget.scroll_content() {
1852        let style = effective_style(widget, id, runtime, theme);
1853        let axis = widget.scroll_axis();
1854        // Only an axis that **neither scrolls nor was given a size**. A caller's number is
1855        // a caller's number, and an axis that scrolls has already answered.
1856        let hug_x = !axis.free_x() && matches!(style.width, frus_layout::Dimension::Auto);
1857        let hug_y = !axis.free_y() && matches!(style.height, frus_layout::Dimension::Auto);
1858        if hug_x || hug_y {
1859            let owned = owned_theme(theme);
1860            let cid = child_id(id, 0, content);
1861            let measure: frus_layout::MeasureFn<'a> = Box::new(move |w, h| {
1862                let mut inner: Layout<BaselineData> = Layout::new();
1863                let node = build_layout(content, cid, runtime, &owned, &mut inner);
1864                // The content is unconstrained on every axis that scrolls — that is what
1865                // scrolling means — and on the one being measured. What is left is an axis
1866                // with a real number on it, and the content is held to it.
1867                inner.compute_scroll(
1868                    node,
1869                    w.unwrap_or(0.0),
1870                    h.unwrap_or(0.0),
1871                    hug_x || axis.free_x() || w.is_none(),
1872                    hug_y || axis.free_y() || h.is_none(),
1873                );
1874                let content = inner.size_of(node);
1875                let pad = widget.scroll_padding();
1876                // **Only the hugged axis is answered.** On the axis that scrolls, a
1877                // viewport is precisely *not* as big as its content — answering with the
1878                // content's size there would make a flexible viewport ask for the whole
1879                // length of what is behind it as its basis, and a column holding one would
1880                // overflow by however much there was to scroll. Nought is what a leaf
1881                // answered before this branch existed, and it is still the right answer.
1882                Size::new(
1883                    match hug_x {
1884                        true => content.width + pad.left + pad.right,
1885                        false => 0.0,
1886                    },
1887                    match hug_y {
1888                        true => content.height + pad.top + pad.bottom,
1889                        false => 0.0,
1890                    },
1891                )
1892            });
1893            return (
1894                layout.measured_leaf(style, own_baseline, measure),
1895                Fills::own(widget, theme),
1896            );
1897        }
1898    }
1899
1900    // Scrollables, interactive viewports, fitters (`FittedBox`), navigators, virtualised
1901    // lists and stacks: their content is laid out separately (independent layers / screens /
1902    // items, or a child laid out at its natural size).
1903    if widget.scroll_content().is_some()
1904        || widget.interactive().is_some()
1905        || widget.fitted().is_some()
1906        || widget.navigator().is_some()
1907        || widget.virtual_list(Size::ZERO).is_some()
1908        || widget.page_view().is_some()
1909        || widget.stack()
1910    {
1911        // Nothing bubbles out of these: what is inside is laid out somewhere else, to
1912        // constraints of its own, and cannot have an opinion about this box.
1913        return (
1914            layout.leaf(effective_style(widget, id, runtime, theme), own_baseline),
1915            Fills::own(widget, theme),
1916        );
1917    }
1918    // A portal only lays out its anchor (child 0); the overlay is deferred.
1919    if widget.overlay().is_some() {
1920        let anchor_w = widget.children()[0].as_ref();
1921        let (anchor, fills) = build_layout_scoped(
1922            anchor_w,
1923            child_id(id, 0, anchor_w),
1924            runtime,
1925            theme,
1926            layout,
1927            baselines,
1928        );
1929        return (
1930            layout.container(effective_style(widget, id, runtime, theme), &[anchor]),
1931            fills,
1932        );
1933    }
1934    let children = widget.children();
1935    if children.is_empty() {
1936        // A leaf measured under constraints (a paragraph that wraps…): taffy calls the
1937        // closure during the computation.
1938        if let Some(measure) = widget.measure(theme) {
1939            return (
1940                layout.measured_leaf(
1941                    effective_style(widget, id, runtime, theme),
1942                    own_baseline,
1943                    measure,
1944                ),
1945                Fills::own(widget, theme),
1946            );
1947        }
1948        (
1949            layout.leaf(effective_style(widget, id, runtime, theme), own_baseline),
1950            Fills::own(widget, theme),
1951        )
1952    } else {
1953        let style = effective_style(widget, id, runtime, theme);
1954        let built: Vec<(NodeId, Fills)> = children
1955            .iter()
1956            .enumerate()
1957            .map(|(i, child)| {
1958                build_layout_scoped(
1959                    child.as_ref(),
1960                    child_id(id, i, child.as_ref()),
1961                    runtime,
1962                    theme,
1963                    layout,
1964                    baselines,
1965                )
1966            })
1967            .collect();
1968        let child_ids: Vec<NodeId> = built.iter().map(|(node, _)| *node).collect();
1969        // `MainAxisSize::Max`: a run that fills the room its parent leaves it. Only the
1970        // parent knows what that means — grow along an axis it shares, stretch across
1971        // one it does not — which is why the child asks rather than saying.
1972        //
1973        // The request is the child's **and its subtree's**: a column whose row fills the
1974        // width is itself as wide as the room it was given.
1975        let alone = children.len() == 1;
1976        for (node, fills) in &built {
1977            for horizontal in [true, false] {
1978                if !fills.wants(horizontal) {
1979                    continue;
1980                }
1981                let parallel = horizontal == style.flex_direction.is_horizontal();
1982                // Along an axis the parent runs too, the reference leaves the main axis
1983                // **unbounded** and `Max` quietly degrades to `Min`: there is no maximum
1984                // to take. The exception is a parent with a single child — a padding, an
1985                // alignment, a decorated box — which passes its own constraints straight
1986                // down, and there the run does fill.
1987                if parallel && !alone {
1988                    continue;
1989                }
1990                layout.fill_parent(*node, horizontal, parallel);
1991            }
1992        }
1993        // A **lone** child gives way; several do not.
1994        //
1995        // The reference hands an inflexible child of a row or a column an unbounded main
1996        // axis: it is never squeezed, and a line that does not fit overflows and says so.
1997        // That is the default here now, because a framework that quietly crushes a button
1998        // instead of reporting a layout that does not fit is one whose bugs are invisible
1999        // — milestone 333 spent three milestones on exactly that crushed button.
2000        //
2001        // But a box with one child is not dividing a line up: it is handing its own
2002        // constraints down, and the reference bounds a lone child by them. The same
2003        // exception the fill request and the floor below both make, for the same reason.
2004        if alone {
2005            for node in &child_ids {
2006                layout.allow_shrink(*node);
2007            }
2008        }
2009        // A grid's tiles are all the same shape. The ratio is the *container's*, since a
2010        // tile cannot know how wide its column came out, and the reference says the same:
2011        // a grid's delegate hands every child a tight box derived from the track, square
2012        // unless the application asked for another ratio.
2013        if let Some(ratio) = widget.tile_shape() {
2014            for node in &child_ids {
2015                layout.set_tile_shape(*node, ratio);
2016            }
2017        }
2018        // A child that refuses to be squeezed along the row. Only a row of **several**
2019        // children may say so. Across a column the same floor would refuse a width the
2020        // column was handing it, which is how a paragraph is told how wide to be; and a
2021        // box with a single child — a padding, an alignment, a decorated box — is handing
2022        // one down rather than dividing a line up, whichever way it nominally runs.
2023        if style.flex_direction.is_horizontal() && !alone {
2024            for (child, node) in children.iter().zip(&child_ids) {
2025                if let Some(floor) = child.main_axis_floor(theme) {
2026                    layout.set_min_width(*node, floor);
2027                }
2028            }
2029        }
2030        // **Baseline** cross-alignment: the children are pushed down until their
2031        // baselines meet, which is the only alignment that makes two runs of different
2032        // sizes read as one line rather than as one row.
2033        //
2034        // It is resolved here rather than by taffy, which cannot: taffy asks a leaf for
2035        // a size and a leaf can only answer with one. The measurement a baseline needs
2036        // is the font's, so the answer has to come from up here, where the widgets are.
2037        //
2038        // The **natural** baseline is the right one to measure even though the children
2039        // will be laid out at some other width: the first line of a piece of text sits
2040        // in the same place however narrow the box gets, since narrowing it adds lines
2041        // below rather than moving the first one.
2042        if style.align == frus_layout::Align::Baseline && style.flex_direction.is_horizontal() {
2043            let own: Vec<Option<f32>> = children
2044                .iter()
2045                .enumerate()
2046                .map(|(i, child)| {
2047                    natural_baseline(
2048                        child.as_ref(),
2049                        child_id(id, i, child.as_ref()),
2050                        runtime,
2051                        theme,
2052                    )
2053                })
2054                .collect();
2055            if let Some(deepest) = own.iter().flatten().copied().reduce(f32::max) {
2056                for (node, b) in child_ids.iter().zip(&own) {
2057                    // A child with no baseline keeps its place: it is a box, and a box
2058                    // has nothing to line up.
2059                    if let Some(b) = b {
2060                        layout.add_margin_top(*node, deepest - b);
2061                    }
2062                }
2063            }
2064        }
2065        // What this container passes on to **its** parent. Across its own axis the
2066        // container's size is its child's, so the request keeps going; along it the
2067        // container divides the room up and the request stops. A container with a single
2068        // child divides nothing, and passes the whole request on.
2069        let mut fills = Fills::own(widget, theme);
2070        for (_, child) in &built {
2071            fills = fills.merge(if alone {
2072                *child
2073            } else {
2074                child.across(style.flex_direction)
2075            });
2076        }
2077        (
2078            layout.container(style, &child_ids),
2079            fills.bounded_by(&style),
2080        )
2081    }
2082}
2083
2084/// The distance from the top of `widget`'s **natural** box to the first baseline inside
2085/// it, or `None` when there is no text in there to have one.
2086///
2087/// It lays the subtree out on its own, exactly as [`natural_size`] does, and takes the
2088/// first baseline the layout collected. First in *layout* order, which is the order a
2089/// reader would find it in — and it is the leaves that carry them, so an intervening
2090/// padding or alignment is already counted in the rectangle it comes with.
2091pub(crate) fn natural_baseline<Msg>(
2092    widget: &dyn Widget<Msg>,
2093    id: WidgetId,
2094    runtime: &Runtime,
2095    theme: &Theme,
2096) -> Option<f32> {
2097    let mut layout = Layout::new();
2098    let node = build_layout(widget, id, runtime, theme, &mut layout);
2099    layout.compute_scroll(node, 0.0, 0.0, true, true);
2100    let rects = layout.absolute_rects(node);
2101    let top = rects.first()?.0.y;
2102    rects
2103        .iter()
2104        .find_map(|(rect, data)| data.copied().flatten().map(|b| rect.y + b - top))
2105}
2106
2107/// **Natural** (intrinsic) size of a subtree: laid out under free axes (`MaxContent`), with
2108/// no imposed constraint. Used by `RotatedBox` (the dimensions to swap) and — at render time
2109/// — by `FittedBox` (the fit factor).
2110pub(crate) fn natural_size<Msg>(
2111    widget: &dyn Widget<Msg>,
2112    id: WidgetId,
2113    runtime: &Runtime,
2114    theme: &Theme,
2115) -> Size {
2116    let mut layout = Layout::new();
2117    let node = build_layout(widget, id, runtime, theme, &mut layout);
2118    layout.compute_scroll(node, 0.0, 0.0, true, true);
2119    layout
2120        .absolute_rects(node)
2121        .first()
2122        .map(|(r, _)| Size::new(r.width, r.height))
2123        .unwrap_or(Size::new(0.0, 0.0))
2124}
2125
2126/// A deferred overlay: `(content, id, the anchor's bounds, placement, dismissal,
2127/// progress 0..=1, whether it takes the pointer, the scrim's colour, the theme it was
2128/// declared under)`. The progress animates the appearance — a drawer sliding in, a
2129/// scrim fading — and is `1.0` for overlays that are not animated.
2130///
2131/// The **theme travels with it** because an overlay is painted long after the walk has
2132/// left the node that declared it: a dialog opened from inside a [`crate::Themed`]
2133/// subtree would otherwise come out in the root's theme, which is the surprise the
2134/// reference had to grow a whole mechanism to avoid. The scrim's colour travels for the
2135/// same reason — by the time it is painted, the widget that asked for it is long
2136/// behind.
2137type Overlay<'a, Msg> = (
2138    &'a dyn Widget<Msg>,
2139    WidgetId,
2140    Rect,
2141    Placement,
2142    Option<Msg>,
2143    f32,
2144    bool,
2145    Option<frus_core::Color>,
2146    std::rc::Rc<Theme>,
2147);
2148
2149struct Builder<'a, Msg> {
2150    scene: Scene,
2151    hits: Vec<Hit<Msg>>,
2152    long_presses: Vec<Hit<Msg>>,
2153    dismisses: Vec<Msg>,
2154    focusables: Vec<Focusable>,
2155    /// Start of the topmost modal overlay's focus scope.
2156    focus_scope_start: Option<usize>,
2157    /// Set while inside an `ExcludeFocus`: nothing in here registers a focus stop.
2158    focus_excluded: bool,
2159    /// The identity of the nearest enclosing backdrop group — the key a backdrop
2160    /// asking to be shared takes. Pushed and popped by the walk, like the focus flags.
2161    backdrop_group: Option<u64>,
2162    /// Set while inside an `ExcludeFocusTraversal`: stops register, Tab passes them by.
2163    focus_skipped: bool,
2164    /// The traversal order in force, from the nearest enclosing `FocusTraversalOrder`.
2165    focus_order: Option<f32>,
2166    /// The nearest enclosing `FocusTraversalGroup`, within which an order is resolved.
2167    focus_group: Option<WidgetId>,
2168    /// The nearest enclosing [`crate::AutofillGroup`]: the form a field is part of.
2169    autofill_group: Option<WidgetId>,
2170    /// Shortcut and action scopes closed so far this frame.
2171    scopes: Vec<Scope<Msg>>,
2172    /// Keystroke listeners, with the focus stops they cover.
2173    #[allow(clippy::type_complexity)]
2174    listeners: Vec<(
2175        std::ops::Range<usize>,
2176        std::rc::Rc<dyn Fn(KeyStroke) -> Option<Msg>>,
2177    )>,
2178    scrollables: Vec<Scrollable>,
2179    scrollbars: Vec<Scrollbar>,
2180    draggables: Vec<(WidgetId, Rect)>,
2181    drag_sources: Vec<DragSource>,
2182    drop_zones: Vec<DropZone>,
2183    inks: Vec<(WidgetId, Rect)>,
2184    reorderables: Vec<(WidgetId, Rect)>,
2185    interactives: Vec<(WidgetId, Rect)>,
2186    /// Deferred overlays: (content, id, the anchor's bounds, placement, dismissal, progress
2187    /// `0..=1`). The progress animates the appearance (a drawer sliding in, a scrim fading);
2188    /// it is `1.0` for overlays that are not animated.
2189    overlays: Vec<Overlay<'a, Msg>>,
2190    /// A widget is asking for a continuous, time-driven animation.
2191    wants_animation: bool,
2192    available: Size,
2193    runtime: &'a Runtime,
2194    /// The theme **in force at this point of the walk** — the root's, unless a
2195    /// [`crate::Themed`] ancestor replaced it. Owned rather than borrowed because a
2196    /// subtree's theme is derived from the one above it and outlives nothing.
2197    ///
2198    /// Owned **behind an `Rc`**: a `Theme` is eleven kilobytes, and this field is swapped
2199    /// on the way into a themed subtree and swapped back on the way out. By value that is
2200    /// two copies of eleven kilobytes standing in the walk's frame — one being moved in,
2201    /// one held across the recursion waiting to be put back — on every level of the tree,
2202    /// taken or not. Behind a pointer the swap is a pointer's width, and the walk's frame
2203    /// went from 28 144 bytes a level to 5 264.
2204    theme: std::rc::Rc<Theme>,
2205    /// The inspector's collection (`Some` only while it is on): one node per painted widget,
2206    /// in paint order.
2207    inspector: Option<Vec<crate::inspector::InspectorNode>>,
2208    /// Current depth of the walk (for the dump's indentation and the palette of the
2209    /// inspector's outlines).
2210    depth: usize,
2211    /// The [`crate::RefreshIndicator`] area currently being walked, if any: every scrollable
2212    /// registered under it records it, so the shell knows where to send the movement
2213    /// its physics refuses. Saved and restored around the subtree, so sibling areas do
2214    /// not inherit one another's.
2215    refresh_host: Option<WidgetId>,
2216    /// The scroll region currently being walked, if any: the nearest one enclosing
2217    /// whatever the walk is looking at. Focus stops record it so the shell can reveal
2218    /// them, and nested regions record one another. Saved and restored around the
2219    /// subtree, like [`Builder::refresh_host`].
2220    scroll_host: Option<WidgetId>,
2221    /// Which screen of a route transition the walk is inside (`0` = leaving, `1` =
2222    /// entering); `None` outside one. Recorded on each hero so the two sides of a
2223    /// flight are told apart rather than guessed from paint order.
2224    hero_screen: Option<u8>,
2225    /// The shared elements seen so far, each with the widget that declared it (needed
2226    /// to lift its subtree's painting) and the transition screen it belongs to.
2227    heroes: Vec<(HeroSpot, &'a dyn Widget<Msg>)>,
2228    /// The frame's refresh areas, in paint order.
2229    refreshes: Vec<Refreshable>,
2230    /// The frame's dismissible items, in paint order.
2231    dismissables: Vec<Dismissable>,
2232    /// The frame's sheets, in the order their subtrees finished walking.
2233    sheets: Vec<crate::sheet::SheetArea>,
2234    /// Accessibility nodes collected during the walk (paint order).
2235    semantics: Vec<(WidgetId, Rect, frus_core::SemanticsProperties)>,
2236    /// The parts of the screen saying what the system bars over them should look like,
2237    /// with their boxes, in paint order.
2238    system_ui: Vec<(Rect, crate::SystemUiOverlayStyle)>,
2239    /// Boxes whose children did not fit, screen-positioned, from every sub-root walked
2240    /// this frame.
2241    overflows: std::cell::RefCell<Vec<Overflowing>>,
2242    /// The same, still in their sub-root's own coordinates, waiting for the walk to reach
2243    /// that sub-root and say where on screen it ended up.
2244    pending_overflows: std::cell::RefCell<std::collections::HashMap<WidgetId, Vec<Overflowing>>>,
2245}
2246
2247impl<'a, Msg: Clone + 'static> Builder<'a, Msg> {
2248    /// A layout root's rects, through the relayout cache retained in the runtime (it only
2249    /// recomputes through taffy when the style/structure/constraints have changed). A brief
2250    /// mutable borrow: the `Vec` returned is owned.
2251    ///
2252    /// In **RTL**, taffy computes in LTR (canonical, and cached), then each rect is
2253    /// **mirrored** about the root's width (the 1st rect): the rows reverse, the alignment
2254    /// and the margins flip — without touching the widgets. The text itself is drawn
2255    /// normally inside its moved box (the *internal* bidi is handled by cosmic-text).
2256    fn cached_rects(&self, key: WidgetId, root: &dyn Widget<Msg>, c: Constraints) -> Vec<Rect> {
2257        let (mut rects, overflows) =
2258            self.runtime
2259                .layout_cache
2260                .borrow_mut()
2261                .rects(key, root, self.runtime, &self.theme, c);
2262        self.mirror(&mut rects);
2263        self.record_overflows(key, &rects, overflows);
2264        rects
2265    }
2266
2267    /// Files this sub-root's overflowing boxes under its identity, mirrored with
2268    /// everything else in RTL — where an edge changes name as well as position, since the
2269    /// layout is the mirror image and "ran past the right" becomes "ran past the left".
2270    ///
2271    /// They wait here rather than going straight out because a sub-root's rectangles are
2272    /// in its **own** coordinates: a scrollable's content, a page, a list item and a
2273    /// stack's layer each get their own taffy pass, and where that pass lands on screen is
2274    /// only known when the walk reaches it. [`Self::claim_overflows`] is the other half.
2275    /// Records a child that came out **bigger than the box holding it**, one entry per
2276    /// edge it ran past — the same yellow and black band a row too full of children gets,
2277    /// from a box that laid its child out itself and so was never measured against it.
2278    ///
2279    /// The rectangle is already on the screen, this being the walk rather than a sub-root's
2280    /// own pass, so it goes straight out rather than through [`Self::record_overflows`].
2281    fn report_spill(&self, own: Rect, child: Rect) {
2282        // Per edge, from where the child actually landed: a centred child that is too wide
2283        // runs past **both** sides, and a band on one of them would be a half-truth.
2284        let over = [
2285            (Side::Left, own.x - child.x),
2286            (Side::Top, own.y - child.y),
2287            (Side::Right, (child.x + child.width) - (own.x + own.width)),
2288            (
2289                Side::Bottom,
2290                (child.y + child.height) - (own.y + own.height),
2291            ),
2292        ];
2293        let spills: Vec<Overflowing> = over
2294            .into_iter()
2295            .filter(|(_, amount)| *amount > 0.5)
2296            .map(|(side, amount)| Overflowing {
2297                rect: own,
2298                side,
2299                amount,
2300            })
2301            .collect();
2302        self.overflows.borrow_mut().extend(spills);
2303    }
2304
2305    fn record_overflows(&self, key: WidgetId, mirrored: &[Rect], overflows: Vec<Overflowing>) {
2306        if overflows.is_empty() {
2307            return;
2308        }
2309        let rtl = self.rtl();
2310        let root = mirrored.first().copied();
2311        let list: Vec<Overflowing> = overflows
2312            .into_iter()
2313            .map(|mut o| {
2314                if let (true, Some(root)) = (rtl, root) {
2315                    o.rect.x = root.x + (root.width - (o.rect.x - root.x) - o.rect.width);
2316                    o.side = match o.side {
2317                        Side::Left => Side::Right,
2318                        Side::Right => Side::Left,
2319                        other => other,
2320                    };
2321                }
2322                o
2323            })
2324            .collect();
2325        self.pending_overflows.borrow_mut().insert(key, list);
2326    }
2327
2328    /// The walk has reached sub-root `id` and knows where it sits: its overflowing boxes
2329    /// become screen rectangles.
2330    fn claim_overflows(&self, id: WidgetId, translation: (f32, f32)) {
2331        let taken = self.pending_overflows.borrow_mut().remove(&id);
2332        if let Some(list) = taken {
2333            self.overflows
2334                .borrow_mut()
2335                .extend(list.into_iter().map(|o| Overflowing {
2336                    rect: o.rect.translate(translation.0, translation.1),
2337                    ..o
2338                }));
2339        }
2340    }
2341
2342    /// The layout direction **in force here** — read from the ambient theme rather than
2343    /// from a field set once at the root, so a [`crate::Themed`] subtree carries its own.
2344    ///
2345    /// What that reaches is everything decided during the walk: which edge a drawer
2346    /// slides from, which way an anchored overlay opens, the sign of a rotation, the
2347    /// mirroring of any **layout root** inside the subtree (a scrollable, an overlay).
2348    /// What it does not reach is ordinary flow inside the subtree: the frame's rects are
2349    /// computed once at the root and mirrored there as a whole, so a direction set part
2350    /// way down does not reverse the rows around it. Setting the direction on the theme
2351    /// handed to `build_ui` is still the way to flip an application.
2352    fn rtl(&self) -> bool {
2353        self.theme.direction.is_rtl()
2354    }
2355
2356    /// Mirrors a root's rects horizontally (RTL). The first rect is the root itself: its
2357    /// width is the axis of symmetry.
2358    fn mirror(&self, rects: &mut [Rect]) {
2359        if !self.rtl() {
2360            return;
2361        }
2362        let Some(root) = rects.first().copied() else {
2363            return;
2364        };
2365        // The axis: the rects are relative to the root (origin at root.x).
2366        for r in rects.iter_mut() {
2367            r.x = root.x + (root.width - (r.x - root.x) - r.width);
2368        }
2369    }
2370
2371    /// 64-bit fingerprint of everything painting a boundary subtree reads **without**
2372    /// rebuilding the `view`: each descendant's `Status` (in walk order) and the subtree's
2373    /// absolute rects. An unchanged fingerprint + generation ⇒ a bit-for-bit identical paint.
2374    /// The time is excluded (a cacheable boundary holds no `continuous` widget).
2375    fn boundary_fingerprint(
2376        &self,
2377        widget: &dyn Widget<Msg>,
2378        id: WidgetId,
2379        translation: (f32, f32),
2380        sub: &[Rect],
2381    ) -> u64 {
2382        let mut h = std::collections::hash_map::DefaultHasher::new();
2383        self.rtl().hash(&mut h);
2384        translation.0.to_bits().hash(&mut h);
2385        translation.1.to_bits().hash(&mut h);
2386        for r in sub {
2387            r.x.to_bits().hash(&mut h);
2388            r.y.to_bits().hash(&mut h);
2389            r.width.to_bits().hash(&mut h);
2390            r.height.to_bits().hash(&mut h);
2391        }
2392        self.hash_statuses(widget, id, &mut h);
2393        h.finish()
2394    }
2395
2396    /// Adds `widget`'s `Status` to `h`, then its children's, recursively — following the
2397    /// walk's identity scheme (`child_id`) **exactly**, so it lines up with the order in which
2398    /// `walk_node` paints them.
2399    fn hash_statuses<H: Hasher>(&self, widget: &dyn Widget<Msg>, id: WidgetId, h: &mut H) {
2400        hash_status(&self.full_status(widget, id), h);
2401        // Live ink is part of what the boundary paints, and it moves every frame while a
2402        // splash is alive. Without it here the cached primitives would be replayed and
2403        // the ripple would stand still — and once the ink dries the hash goes back to
2404        // what it was, so the boundary starts hitting again.
2405        if let Some(ripples) = self.runtime.ink.get(&id) {
2406            ripples.hash_state(h);
2407        }
2408        for (i, child) in widget.children().iter().enumerate() {
2409            self.hash_statuses(child.as_ref(), child_id(id, i, child.as_ref()), h);
2410        }
2411    }
2412
2413    /// Current lengths of the builder's collections: the lower bound of the slices to capture
2414    /// for a boundary (see [`capture_since`](Self::capture_since)).
2415    fn snapshot(&self) -> Snapshot {
2416        Snapshot {
2417            scene: self.scene.primitives().len(),
2418            hits: self.hits.len(),
2419            long_presses: self.long_presses.len(),
2420            dismisses: self.dismisses.len(),
2421            focusables: self.focusables.len(),
2422            scrollables: self.scrollables.len(),
2423            draggables: self.draggables.len(),
2424            drag_sources: self.drag_sources.len(),
2425            drop_zones: self.drop_zones.len(),
2426            inks: self.inks.len(),
2427            semantics: self.semantics.len(),
2428            system_ui: self.system_ui.len(),
2429            overlays: self.overlays.len(),
2430            focus_scope_start: self.focus_scope_start,
2431        }
2432    }
2433
2434    /// Captures the output produced since `snap` (the collections' *tail slices*) into a
2435    /// [`BoundaryData`]. Returns `None` — hence **not cacheable** — when the subtree pushed an
2436    /// overlay or touched the modal focus scope (global state that cannot be captured here).
2437    fn capture_since(&self, snap: &Snapshot) -> Option<BoundaryData<Msg>> {
2438        if self.overlays.len() != snap.overlays || self.focus_scope_start != snap.focus_scope_start
2439        {
2440            return None;
2441        }
2442        Some(BoundaryData {
2443            prims: self.scene.primitives()[snap.scene..].to_vec(),
2444            hits: self.hits[snap.hits..].to_vec(),
2445            long_presses: self.long_presses[snap.long_presses..].to_vec(),
2446            dismisses: self.dismisses[snap.dismisses..].to_vec(),
2447            focusables: self.focusables[snap.focusables..].to_vec(),
2448            scrollables: self.scrollables[snap.scrollables..].to_vec(),
2449            draggables: self.draggables[snap.draggables..].to_vec(),
2450            drag_sources: self.drag_sources[snap.drag_sources..].to_vec(),
2451            drop_zones: self.drop_zones[snap.drop_zones..].to_vec(),
2452            inks: self.inks[snap.inks..].to_vec(),
2453            semantics: self.semantics[snap.semantics..].to_vec(),
2454            system_ui: self.system_ui[snap.system_ui..].to_vec(),
2455        })
2456    }
2457
2458    /// Replays a boundary from the cache: primitives already formed (clip/owner baked in) and
2459    /// interaction maps, appended as is.
2460    fn splice_boundary(&mut self, data: BoundaryData<Msg>) {
2461        for p in data.prims {
2462            self.scene.push_primitive(p);
2463        }
2464        self.hits.extend(data.hits);
2465        self.long_presses.extend(data.long_presses);
2466        self.dismisses.extend(data.dismisses);
2467        self.focusables.extend(data.focusables);
2468        self.scrollables.extend(data.scrollables);
2469        self.draggables.extend(data.draggables);
2470        self.drag_sources.extend(data.drag_sources);
2471        self.drop_zones.extend(data.drop_zones);
2472        self.inks.extend(data.inks);
2473        self.semantics.extend(data.semantics);
2474        self.system_ui.extend(data.system_ui);
2475    }
2476
2477    /// Lengths of every registry a [`ModalBarrier`] can withhold from, taken before its subtree
2478    /// is walked.
2479    fn barrier_base(&self) -> BarrierBase {
2480        BarrierBase {
2481            scene: self.scene.primitives().len(),
2482            hits: self.hits.len(),
2483            long_presses: self.long_presses.len(),
2484            focusables: self.focusables.len(),
2485            scrollables: self.scrollables.len(),
2486            scrollbars: self.scrollbars.len(),
2487            draggables: self.draggables.len(),
2488            drag_sources: self.drag_sources.len(),
2489            drop_zones: self.drop_zones.len(),
2490            inks: self.inks.len(),
2491            reorderables: self.reorderables.len(),
2492            interactives: self.interactives.len(),
2493            semantics: self.semantics.len(),
2494        }
2495    }
2496
2497    /// Drops what the subtree added since `base`, according to `barrier`, and — for an
2498    /// absorbing barrier — puts a message-less hit target in its place so that input stops
2499    /// at the barrier instead of reaching whatever is painted behind it.
2500    /// Resolves the **shared elements** of a route transition: every tag that appears
2501    /// on both screens flies from where it was to where it is going.
2502    ///
2503    /// Called once, after both screens have been drawn, because that is the first
2504    /// moment both boxes are known. `hero_base` and `scene_base` mark where those two
2505    /// screens started.
2506    ///
2507    /// What flies is the **destination**'s own painting, mapped onto the box it is
2508    /// travelling through. Both originals are taken out of the frame: a thing that is
2509    /// flying is not also sitting at either end, and leaving them in is the difference
2510    /// between a shared element and three copies of one.
2511    fn fly_heroes(&mut self, hero_base: usize, scene_base: usize, progress: f32) {
2512        let spots: Vec<(HeroSpot, &'a dyn Widget<Msg>)> = self.heroes.split_off(hero_base);
2513        if spots.len() < 2 {
2514            return;
2515        }
2516        // The pairs: one tag, one hero on each side. A tag on one side only has nothing
2517        // to fly to, and a tag used more than once per side is ambiguous — both are
2518        // left alone rather than guessed at.
2519        let mut flights: Vec<(Rect, Rect, &'a dyn Widget<Msg>, WidgetId, WidgetId)> = Vec::new();
2520        for (from, _) in spots.iter().filter(|(s, _)| s.screen == 0) {
2521            let mut matching = spots
2522                .iter()
2523                .filter(|(s, _)| s.screen == 1 && s.tag == from.tag);
2524            let Some((to, widget)) = matching.next() else {
2525                continue;
2526            };
2527            if matching.next().is_some() {
2528                continue;
2529            }
2530            flights.push((from.rect, to.rect, *widget, from.id, to.id));
2531        }
2532        if flights.is_empty() {
2533            return;
2534        }
2535
2536        // Everything the two screens painted, so the originals can be taken out and the
2537        // travelling copies laid on top.
2538        let painted = self.scene.split_off(scene_base);
2539        let mut hidden: std::collections::HashSet<u64> = std::collections::HashSet::new();
2540        for (_, _, widget, from_id, to_id) in &flights {
2541            for id in subtree_ids(*widget, *to_id) {
2542                hidden.insert(id.as_u64());
2543            }
2544            // The source's subtree is the *other* screen's widget, whose identity is
2545            // rooted at `from_id`; the same relative shape, so the ids follow.
2546            for id in subtree_ids(*widget, *from_id) {
2547                hidden.insert(id.as_u64());
2548            }
2549        }
2550        for primitive in &painted {
2551            if !hidden.contains(&primitive.owner()) {
2552                self.scene.push_primitive(primitive.clone());
2553            }
2554        }
2555
2556        self.scene.set_clip(Rect::UNBOUNDED);
2557        for (from, to, _, _, to_id) in &flights {
2558            let travelling = lerp_rect(*from, *to, progress);
2559            if to.width <= 0.0 || to.height <= 0.0 {
2560                continue;
2561            }
2562            let (sx, sy) = (travelling.width / to.width, travelling.height / to.height);
2563            let pivot = Point::new(to.x, to.y);
2564            let owners: std::collections::HashSet<u64> = subtree_ids(
2565                flights
2566                    .iter()
2567                    .find(|(_, _, _, _, id)| id == to_id)
2568                    .map(|(_, _, w, _, _)| *w)
2569                    .expect("the flight just matched"),
2570                *to_id,
2571            )
2572            .iter()
2573            .map(|id| id.as_u64())
2574            .collect();
2575            for primitive in painted.iter().filter(|p| owners.contains(&p.owner())) {
2576                let flown = primitive
2577                    .scaled_about_xy(pivot, sx, sy)
2578                    .translated(travelling.x - to.x, travelling.y - to.y)
2579                    .with_clip(Rect::UNBOUNDED);
2580                self.scene.push_primitive(flown);
2581            }
2582        }
2583    }
2584
2585    /// Reconciles what a [`crate::Semantics`] wrapper's subtree announced with what the
2586    /// wrapper says about it.
2587    ///
2588    /// The wrapper's own answers win, and the subtree's show through wherever it said
2589    /// nothing — a container that knows a child is a *heading* rarely knows what the words
2590    /// are, and dropping them would replace a spoken title with silence.
2591    ///
2592    /// The merged node goes in **where the subtree's first one was**, not at the end. The
2593    /// accessibility tree is flat and its order is reading order, so a wrapper appended
2594    /// after its own contents would be read after them.
2595    fn apply_description(
2596        &mut self,
2597        described: crate::semantics::Description,
2598        base: usize,
2599        id: WidgetId,
2600        own: Rect,
2601    ) {
2602        let mut props = described.props;
2603        // Merging swallows every child's own entry, so **something** has to answer for
2604        // this node when it is activated — the wrapper itself never does (see
2605        // `Semantics::on_click`'s own reasoning: an annotation that swallowed a click
2606        // would put a hit target over the widget it describes). Found testing #18 in a
2607        // real browser and real screen reader: a `Checkbox::new(..).on_toggle(..)`
2608        // merged with its row's caption read correctly and did **nothing** on
2609        // activation, on every platform, because the merged entry carried the row's own
2610        // id, which nothing routes a click through. When exactly one child in the
2611        // swallowed subtree was clickable, its id is who a click or a keyboard
2612        // activation has to reach, so the merged entry keys on that id instead — the
2613        // wrapper's id is kept only when there is no such child, or more than one and
2614        // "the one that gets clicked" has no single answer.
2615        let mut operable = None;
2616        if described.merging {
2617            let mut clickable_count = 0;
2618            for (child_id, _, child) in self.semantics.drain(base..).collect::<Vec<_>>() {
2619                if child.clickable {
2620                    clickable_count += 1;
2621                    operable = Some(child_id);
2622                }
2623                props = props.over(&child);
2624            }
2625            if clickable_count != 1 {
2626                operable = None;
2627            }
2628        }
2629        let id = operable.unwrap_or(id);
2630        if props.is_meaningful() && own.width > 0.0 && own.height > 0.0 {
2631            self.semantics.insert(base, (id, own, props));
2632        }
2633    }
2634
2635    fn apply_barrier(
2636        &mut self,
2637        barrier: ModalBarrier,
2638        base: &BarrierBase,
2639        id: WidgetId,
2640        own: Rect,
2641    ) {
2642        if barrier.pointer {
2643            self.hits.truncate(base.hits);
2644            self.long_presses.truncate(base.long_presses);
2645            self.focusables.truncate(base.focusables);
2646            self.scrollables.truncate(base.scrollables);
2647            self.scrollbars.truncate(base.scrollbars);
2648            self.draggables.truncate(base.draggables);
2649            self.drag_sources.truncate(base.drag_sources);
2650            self.drop_zones.truncate(base.drop_zones);
2651            self.inks.truncate(base.inks);
2652            self.reorderables.truncate(base.reorderables);
2653            self.interactives.truncate(base.interactives);
2654            // The modal focus scope is an index **into** `focusables`. A barrier that cut
2655            // away the focusable it pointed at would leave it dangling past the end, and
2656            // every later slice of the pool would then be empty — no focusable at all,
2657            // rather than the ones the barrier meant to spare.
2658            if let Some(start) = self.focus_scope_start {
2659                self.focus_scope_start = Some(start.min(self.focusables.len()));
2660            }
2661        }
2662        if barrier.absorb && own.width > 0.0 && own.height > 0.0 {
2663            self.hits.push(Hit {
2664                id,
2665                rect: own,
2666                msg: None,
2667                xform: None,
2668            });
2669        }
2670        if barrier.paint {
2671            // The clip is baked into each primitive when it is pushed, so dropping the tail
2672            // leaves nothing for the next sibling to repair.
2673            let _hidden = self.scene.split_off(base.scene);
2674        }
2675        if barrier.semantics {
2676            self.semantics.truncate(base.semantics);
2677        }
2678    }
2679
2680    /// Lower bounds of the interaction registries **before** a transformed composited subtree.
2681    fn xform_base(&self) -> XformBase {
2682        XformBase {
2683            hits: self.hits.len(),
2684            long_presses: self.long_presses.len(),
2685            focusables: self.focusables.len(),
2686            scrollables: self.scrollables.len(),
2687            draggables: self.draggables.len(),
2688            drag_sources: self.drag_sources.len(),
2689            drop_zones: self.drop_zones.len(),
2690            inks: self.inks.len(),
2691            reorderables: self.reorderables.len(),
2692            semantics: self.semantics.len(),
2693        }
2694    }
2695
2696    /// Applies the layer transform `matrix` to the registry entries **added since** `base`:
2697    /// the test point of click / long-press targets goes through `M⁻¹` (without overwriting an
2698    /// inner transform already set); and when `matrix` preserves axis alignment
2699    /// (scale/translation, no rotation), the **rects** of focus / scrolling / dragging /
2700    /// **reordering** / accessibility are mapped exactly by `M` (otherwise they are left as
2701    /// they are — the click stays correct through `M⁻¹`). The **shared factor** of the two
2702    /// transformed-composition sites (`walk`'s boundary and `emit_transformed_child`): one
2703    /// place to keep up to date, which forgets no registry (milestone 250 had forgotten
2704    /// `reorderables` in exactly one of the two).
2705    fn transform_interaction_registries(&mut self, base: &XformBase, matrix: Affine) {
2706        let inverse = matrix.inverse();
2707        for h in &mut self.hits[base.hits..] {
2708            h.xform.get_or_insert(inverse);
2709        }
2710        for h in &mut self.long_presses[base.long_presses..] {
2711            h.xform.get_or_insert(inverse);
2712        }
2713        if matrix.is_axis_aligned() {
2714            for f in &mut self.focusables[base.focusables..] {
2715                f.rect = matrix.apply_rect(f.rect);
2716                f.bounds = matrix.apply_rect(f.bounds);
2717            }
2718            for area in &mut self.scrollables[base.scrollables..] {
2719                area.viewport = matrix.apply_rect(area.viewport);
2720            }
2721            for source in &mut self.drag_sources[base.drag_sources..] {
2722                source.rect = matrix.apply_rect(source.rect);
2723            }
2724            for zone in &mut self.drop_zones[base.drop_zones..] {
2725                zone.rect = matrix.apply_rect(zone.rect);
2726            }
2727            for (_, r) in &mut self.inks[base.inks..] {
2728                *r = matrix.apply_rect(*r);
2729            }
2730            for (_, r) in &mut self.draggables[base.draggables..] {
2731                *r = matrix.apply_rect(*r);
2732            }
2733            for (_, r) in &mut self.reorderables[base.reorderables..] {
2734                *r = matrix.apply_rect(*r);
2735            }
2736            for (_, r, _) in &mut self.semantics[base.semantics..] {
2737                *r = matrix.apply_rect(*r);
2738            }
2739        }
2740    }
2741
2742    /// Entry point for walking a node. When the node is a cacheable **repaint boundary**, it
2743    /// tries to replay its subtree from the paint cache; otherwise (or on a *miss*) it
2744    /// delegates to the full walk [`walk_node`](Self::walk_node), capturing its output for the
2745    /// next frame. Every recursion goes through here → nested boundaries get cached too.
2746    /// The focus flags a subtree carries — exclusion, Tab-skipping, an order, a group —
2747    /// are **subtree-scoped**, so they are pushed here and popped on the way out rather
2748    /// than asked of each widget at the moment it registers. The body of the walk has
2749    /// early returns in a dozen branches; wrapping it is the only way to be sure the
2750    /// scope is closed on every one of them.
2751    fn walk(
2752        &mut self,
2753        widget: &'a dyn Widget<Msg>,
2754        id: WidgetId,
2755        translation: (f32, f32),
2756        clip: Rect,
2757        rects: &[Rect],
2758        index: &mut usize,
2759    ) {
2760        let outer = (
2761            self.focus_excluded,
2762            self.focus_skipped,
2763            self.focus_order,
2764            self.focus_group,
2765        );
2766        if !widget.descendants_focusable() {
2767            self.focus_excluded = true;
2768        }
2769        if widget.focus_skip_traversal() {
2770            self.focus_skipped = true;
2771        }
2772        if let Some(order) = widget.focus_order() {
2773            self.focus_order = Some(order);
2774        }
2775        if widget.focus_group() {
2776            self.focus_group = Some(id);
2777        }
2778        // The backdrop group a subtree sits in, scoped the same way and for the same
2779        // reason. Its own identity is the key: stable across frames, and unique
2780        // without anything having to hand one out.
2781        let outer_group = self.backdrop_group;
2782        if widget.backdrop_group() {
2783            self.backdrop_group = Some(id.as_u64());
2784        }
2785        // The form a field belongs to, scoped the same way (milestone 512).
2786        let outer_form = self.autofill_group;
2787        if widget.autofill_group() {
2788            self.autofill_group = Some(id);
2789        }
2790        // The focus stops this subtree contains start here; the walk is depth-first, so
2791        // they are contiguous and the range closes below.
2792        let stops_before = self.focusables.len();
2793        self.walk_scoped(widget, id, translation, clip, rects, index);
2794        (
2795            self.focus_excluded,
2796            self.focus_skipped,
2797            self.focus_order,
2798            self.focus_group,
2799        ) = outer;
2800        self.backdrop_group = outer_group;
2801        self.autofill_group = outer_form;
2802        self.close_scope(widget, stops_before);
2803    }
2804
2805    /// Files whatever tables this subtree carried, with the focus stops it turned out to
2806    /// contain. Recorded **on the way out**, because the range is not known on the way in;
2807    /// so `scopes` ends up innermost-first among any that overlap, which is the order the
2808    /// resolution wants and the reason it is not sorted afterwards.
2809    fn close_scope(&mut self, widget: &'a dyn Widget<Msg>, stops_before: usize) {
2810        let range = stops_before..self.focusables.len();
2811        if let Some(on_key) = widget.on_keystroke() {
2812            self.listeners.push((range.clone(), on_key));
2813        }
2814        let (strokes, callbacks) = (widget.shortcut_bindings(), widget.shortcut_callbacks());
2815        let (actions, listeners) = (widget.action_bindings(), widget.action_listeners());
2816        if strokes.is_empty() && callbacks.is_empty() && actions.is_empty() && listeners.is_empty()
2817        {
2818            return;
2819        }
2820        self.scopes.push(Scope {
2821            strokes: strokes.to_vec(),
2822            callbacks: callbacks.to_vec(),
2823            actions: actions.to_vec(),
2824            listeners: listeners.to_vec(),
2825            range,
2826        });
2827    }
2828
2829    fn walk_scoped(
2830        &mut self,
2831        widget: &'a dyn Widget<Msg>,
2832        id: WidgetId,
2833        translation: (f32, f32),
2834        clip: Rect,
2835        rects: &[Rect],
2836        index: &mut usize,
2837    ) {
2838        self.claim_overflows(id, translation);
2839        // A **sheet's panel**: the subtree is walked as usual, and every scroll area it
2840        // registered is recorded against the sheet, so the shell knows which lists share a
2841        // finger with it. By range, since a subtree's registrations are contiguous —
2842        // a replayed boundary splices its own in the same place.
2843        if let Some(spec) = widget.sheet() {
2844            let panel = rects[*index].translate(translation.0, translation.1);
2845            let first = self.scrollables.len();
2846            self.walk_node(widget, id, translation, clip, rects, index);
2847            let areas = self.scrollables[first..]
2848                .iter()
2849                .map(|area| area.id)
2850                .collect();
2851            // What the share is a share of. At nought there is nothing to divide by, so the
2852            // height last measured stands in: a finger that lowered the panel to nothing
2853            // may still be holding it.
2854            let size = self.runtime.sheet_size(id, spec);
2855            let available = if size > 0.0 {
2856                panel.height / size
2857            } else {
2858                self.runtime
2859                    .sheets
2860                    .get(&id)
2861                    .map_or(0.0, |sheet| sheet.available())
2862            };
2863            self.sheets.push(crate::sheet::SheetArea {
2864                id,
2865                panel,
2866                available,
2867                spec: spec.clone(),
2868                areas,
2869            });
2870            return;
2871        }
2872
2873        // A **refresh area**: the subtree is walked with this widget named as the host, so
2874        // every scrollable inside records where to send the movement its physics refuses at
2875        // the top edge. The indicator is then painted **over** the subtree — painting it in
2876        // the widget's own `paint` would put it under the list it belongs to.
2877        if let Some(spec) = widget.refresh() {
2878            let viewport = rects[*index]
2879                .translate(translation.0, translation.1)
2880                .intersect(clip);
2881            let outer = self.refresh_host.replace(id);
2882            self.walk_node(widget, id, translation, clip, rects, index);
2883            self.refresh_host = outer;
2884
2885            let area = Refreshable { id, viewport, spec };
2886            if let Some(pull) = self.runtime.refresh.get(&id) {
2887                crate::refresh::paint_refresh(&mut self.scene, &area, pull, &self.theme, clip);
2888                // A pull that is settling, spinning or fading away drives itself, so the
2889                // frame after this one has to happen.
2890                self.wants_animation = true;
2891            }
2892            self.refreshes.push(area);
2893            return;
2894        }
2895
2896        // A **barrier** (`IgnorePointer`, `AbsorbPointer`, a hidden `Visibility`,
2897        // `ExcludeSemantics`): the subtree is walked exactly as usual, and then whatever it
2898        // added to the withheld registries is dropped again.
2899        //
2900        // Removing afterwards rather than skipping the walk is what makes it exact. A widget
2901        // deep inside registers its click target, focus stop, scrollable area or accessibility
2902        // node without knowing that something above is holding the subtree out of the frame;
2903        // truncating at the barrier catches every one of them, including those added by
2904        // widgets written after this code. It also keeps the walk's rect indexing untouched,
2905        // which a skipped subtree would break.
2906        // A **description** (`Semantics`): something above states what the subtree *is*,
2907        // which the subtree cannot state for itself. Same shape as the barrier below and for
2908        // the same reason — walk first, reconcile after, so a widget deep inside annotates
2909        // itself without knowing anything is speaking for it.
2910        if let Some(described) = widget.describes() {
2911            let base = self.semantics.len();
2912            let own = rects[*index]
2913                .translate(translation.0, translation.1)
2914                .intersect(clip);
2915            self.walk_node(widget, id, translation, clip, rects, index);
2916            self.apply_description(described, base, id, own);
2917            return;
2918        }
2919
2920        if let Some(barrier) = widget.barrier().filter(|b| !b.is_none()) {
2921            let base = self.barrier_base();
2922            // The barrier's own box, before `walk_node` advances the index past it.
2923            let own = rects[*index]
2924                .translate(translation.0, translation.1)
2925                .intersect(clip);
2926            self.walk_node(widget, id, translation, clip, rects, index);
2927            self.apply_barrier(barrier, &base, id, own);
2928            return;
2929        }
2930
2931        // An opacity group: the subtree is painted normally, then its range of primitives is
2932        // **drained** into a composited layer at the group's opacity — so overlaps do not
2933        // double-blend. The animated opacity is the value the runtime tweened; otherwise the
2934        // fixed target. Opaque (≈1): no layer at all (zero cost).
2935        if let Some(target) = widget.opacity_group() {
2936            let opacity = self.runtime.value_or(id, target).clamp(0.0, 1.0);
2937            if opacity < 0.999 {
2938                let start = self.scene.primitives().len();
2939                self.walk_node(widget, id, translation, clip, rects, index);
2940                let group = self.scene.split_off(start);
2941                self.scene.push_primitive(Primitive::Layer {
2942                    primitives: group,
2943                    opacity,
2944                    clip,
2945                    clip_shape: ClipShape::Rect,
2946                    transform: None,
2947                    filter: LayerFilter::NONE,
2948                    owner: id.as_u64(),
2949                });
2950                return;
2951            }
2952            // Fully opaque: an ordinary render (no pointless layer).
2953        }
2954
2955        // A **pixel effect** over the subtree (`ColorFiltered`, `ImageFiltered`,
2956        // `ShaderMask`): painted normally, then drained into a composited layer that
2957        // carries the effect. The renderer applies it when it composites — the image
2958        // filter as a pre-pass over the layer texture, the colour filter and the mask
2959        // in the compositing fragment.
2960        //
2961        // The box is passed in because a mask is written in fractions of the box it
2962        // covers, and this is the first point at which that box is a place on screen.
2963        if let Some(filter) = widget.layer_filter(crate::widget::FilterContext {
2964            box_rect: rects[*index].translate(translation.0, translation.1),
2965            backdrop_group: self.backdrop_group,
2966        }) {
2967            // The layer keeps the **ambient** clip rather than its own box: an image
2968            // filter reaches past the pixels it came from, and cutting it at the box
2969            // would shave the edge off every blur.
2970            let start = self.scene.primitives().len();
2971            self.walk_node(widget, id, translation, clip, rects, index);
2972            let group = self.scene.split_off(start);
2973            let (group, filter, clip_box) = fold_filter(group, filter, clip);
2974            self.scene.push_primitive(Primitive::Layer {
2975                primitives: group,
2976                opacity: 1.0,
2977                clip: clip_box,
2978                clip_shape: ClipShape::Rect,
2979                transform: None,
2980                filter,
2981                owner: id.as_u64(),
2982            });
2983            return;
2984        }
2985
2986        // **Shape** clipping: the subtree is painted normally, bounded to the widget's box,
2987        // then its primitives are **drained** into a composited layer whose shape (rounded
2988        // corners / ellipse) modulates the alpha — the overflowing corners are erased at
2989        // compositing time. The shape's box is the layer's clip rect. Two sources: `ClipPath`
2990        // (an arbitrary path, taking priority) or `ClipRRect`/`ClipOval` (an analytic shape).
2991        // The local path is **offset to the box's screen position**.
2992        if widget.clip_path().is_some() || widget.clip_shape().is_some() {
2993            let box_rect = rects[*index].translate(translation.0, translation.1);
2994            let shape = widget
2995                .clip_path()
2996                .map(|p| ClipShape::Path(p.translated(box_rect.x, box_rect.y)))
2997                .or_else(|| widget.clip_shape())
2998                .expect("clip_path or clip_shape is Some");
2999            let clip_box = clip.intersect(box_rect);
3000            let start = self.scene.primitives().len();
3001            self.walk_node(widget, id, translation, clip_box, rects, index);
3002            let group = self.scene.split_off(start);
3003            // A clip around a filter is **one** layer, not two. It matters most for a
3004            // backdrop: the reference's own advice is to wrap one in a clip to give it
3005            // its shape, and a backdrop pushed a level down would be filtering the
3006            // clip's contents rather than the frame.
3007            let (group, filter, clip_box) = fold_filter(group, LayerFilter::NONE, clip_box);
3008            self.scene.push_primitive(Primitive::Layer {
3009                primitives: group,
3010                opacity: 1.0,
3011                clip: clip_box,
3012                clip_shape: shape,
3013                transform: None,
3014                filter,
3015                owner: id.as_u64(),
3016            });
3017            return;
3018        }
3019
3020        // Composable paint transforms (`Transform`: scale and/or rotation; translation goes
3021        // through the child offset instead). Scale and rotation are melted into **a single
3022        // affine matrix** `M`, the subtree is painted **flat**, then wrapped in a composited
3023        // layer transformed by `M`. The hit-test applies `M⁻¹` to the point. The translation,
3024        // applied upstream through `child_offset`, is the innermost one.
3025        //
3026        // A transform that was told to **animate** hands over its numbers, and only its
3027        // numbers: the pivots stay the widget's, because a pivot is a choice of origin
3028        // rather than a quantity and interpolating it would slide the subtree across the
3029        // screen with nothing describing it having changed. Where the runtime has never
3030        // heard of this widget the static value stands, which is the same rule every
3031        // other animated property follows on mount.
3032        let moving = self.runtime.anim_transform(id);
3033        let scale = widget
3034            .transform_scale()
3035            .map(|(sx, sy, pivot)| {
3036                moving.map_or((sx, sy, pivot), |m| (m.scale_x, m.scale_y, pivot))
3037            })
3038            .filter(|(sx, sy, _)| (sx - 1.0).abs() > 1e-4 || (sy - 1.0).abs() > 1e-4);
3039        let rotate = widget
3040            .transform_rotate()
3041            .map(|(angle, pivot)| moving.map_or((angle, pivot), |m| (m.rotation, pivot)))
3042            .filter(|(a, _)| a.abs() > 1e-4);
3043        if scale.is_some() || rotate.is_some() {
3044            // The pivots are taken on the **child's** box (the next node in prefix order): it
3045            // is the child that gets transformed, and its box hugs the content even when the
3046            // Transform's own box is stretched by the parent.
3047            let basis = rects.get(*index + 1).copied().unwrap_or(rects[*index]);
3048            let box_rect = basis.translate(translation.0, translation.1);
3049            let pivot_of = |align: frus_core::Alignment| {
3050                Point::new(
3051                    box_rect.x + box_rect.width * align.fraction_x(),
3052                    box_rect.y + box_rect.height * align.fraction_y(),
3053                )
3054            };
3055            // The composition: scale (about its own pivot) **then** rotation (about its own).
3056            // In RTL the world is mirrored → the rotation direction is inverted.
3057            let mut matrix = Affine::IDENTITY;
3058            if let Some((sx, sy, pivot_align)) = scale {
3059                matrix = matrix.then(Affine::scale(sx, sy).about(pivot_of(pivot_align)));
3060            }
3061            if let Some((angle, pivot_align)) = rotate {
3062                let angle = if self.rtl() { -angle } else { angle };
3063                matrix = matrix.then(Affine::rotation(angle).about(pivot_of(pivot_align)));
3064            }
3065
3066            let p0 = self.scene.primitives().len();
3067            let base = self.xform_base();
3068            self.walk_node(widget, id, translation, clip, rects, index);
3069            // Wraps the range of primitives — painted flat — in a layer transformed by `M`
3070            // (scale/rotation applied at compositing time).
3071            let group = self.scene.split_off(p0);
3072            self.scene.push_primitive(Primitive::Layer {
3073                primitives: group,
3074                opacity: 1.0,
3075                clip,
3076                clip_shape: ClipShape::Rect,
3077                transform: Some(LayerTransform::new(matrix)),
3078                filter: LayerFilter::NONE,
3079                owner: id.as_u64(),
3080            });
3081            // Counter-transforms the hit-test and maps the subtree's interaction rects.
3082            self.transform_interaction_registries(&base, matrix);
3083            return;
3084        }
3085
3086        // The inspector wants a full walk (it collects every node); caching only happens
3087        // outside inspection.
3088        if self.inspector.is_none() && widget.repaint_boundary() {
3089            if let Some(count) = plain_subtree_len(widget) {
3090                let start = *index;
3091                let sub = &rects[start..start + count];
3092                let fp = self.boundary_fingerprint(widget, id, translation, sub);
3093
3094                // Hit: the same generation (config) + the same fingerprint (state+geometry)
3095                // ⇒ the paint would be identical. The output is cloned and replayed.
3096                let hit = {
3097                    let mut pc = self.runtime.paint_cache.borrow_mut();
3098                    pc.get(id, fp).and_then(|(rc, any)| {
3099                        any.downcast_ref::<BoundaryData<Msg>>()
3100                            .map(|d| (rc, d.clone()))
3101                    })
3102                };
3103                if let Some((rc, data)) = hit {
3104                    self.runtime.paint_cache.borrow_mut().note_hit();
3105                    self.splice_boundary(data);
3106                    *index += rc;
3107                    return;
3108                }
3109
3110                // Miss: paints normally, capturing the subtree's output (primitives +
3111                // interaction maps) for the next frame.
3112                let snap = self.snapshot();
3113                self.walk_node(widget, id, translation, clip, rects, index);
3114                self.runtime.paint_cache.borrow_mut().note_miss();
3115                if let Some(data) = self.capture_since(&snap) {
3116                    self.runtime
3117                        .paint_cache
3118                        .borrow_mut()
3119                        .put(id, fp, count, Box::new(data));
3120                }
3121                return;
3122            }
3123        }
3124        self.walk_node(widget, id, translation, clip, rects, index);
3125    }
3126
3127    /// Paints a child **flat** (laid out separately, at `translation`, with its own rects)
3128    /// then wraps it in a composited layer transformed by `matrix` and clipped to `clip`. The
3129    /// hit-test counter-transforms the point (`M⁻¹`); when `matrix` stays axis-aligned
3130    /// (scale/translation), the focus / scroll / drag / accessibility bounds are transformed
3131    /// too. The shared factor of `InteractiveViewer`, `RotatedBox` and `FittedBox`.
3132    // The child, its identity, and the four things that place it. Splitting them would
3133    // only mean passing the same values in two hops.
3134    #[allow(clippy::too_many_arguments)]
3135    fn emit_transformed_child(
3136        &mut self,
3137        child: &'a dyn Widget<Msg>,
3138        cid: WidgetId,
3139        translation: (f32, f32),
3140        clip: Rect,
3141        child_rects: &[Rect],
3142        matrix: Affine,
3143        owner: WidgetId,
3144    ) {
3145        let p0 = self.scene.primitives().len();
3146        let base = self.xform_base();
3147        let mut child_index = 0;
3148        self.walk(child, cid, translation, clip, child_rects, &mut child_index);
3149        let group = self.scene.split_off(p0);
3150        self.scene.push_primitive(Primitive::Layer {
3151            primitives: group,
3152            opacity: 1.0,
3153            clip,
3154            clip_shape: ClipShape::Rect,
3155            transform: Some(LayerTransform::new(matrix)),
3156            filter: LayerFilter::NONE,
3157            owner: owner.as_u64(),
3158        });
3159        self.transform_interaction_registries(&base, matrix);
3160    }
3161
3162    /// Paints one node, **under the theme it asks for**. A [`crate::Themed`] replaces the
3163    /// ambient theme for itself and everything below it; it is put back on the way out, so
3164    /// the sibling that follows is untouched. Everything the subtree reads — its paint,
3165    /// its ink, its focus ring, its layout through `cached_rects`, and any overlay it
3166    /// defers — goes through this field, which is what makes one swap enough.
3167    fn walk_node(
3168        &mut self,
3169        widget: &'a dyn Widget<Msg>,
3170        id: WidgetId,
3171        translation: (f32, f32),
3172        clip: Rect,
3173        rects: &[Rect],
3174        index: &mut usize,
3175    ) {
3176        // The box this node is about to be given, read **before** the walk consumes the
3177        // index: a decoration painted over the children needs it once they are done, and
3178        // by then `*index` has moved past the whole subtree.
3179        let over = rects
3180            .get(*index)
3181            .copied()
3182            .map(|r| r.translate(translation.0, translation.1));
3183        let outer = scoped_theme(widget, id, self.runtime, &self.theme)
3184            .map(|theme| std::mem::replace(&mut self.theme, std::rc::Rc::from(theme)));
3185        // The scoped surface, held for this subtree exactly as the layout walk holds it —
3186        // a widget that paints from `MediaQuery::of()` must see the same description it
3187        // was measured against, or the two disagree by whatever the scope removed.
3188        let _surface = widget
3189            .media_override(crate::MediaQuery::of())
3190            .map(crate::MediaQuery::install);
3191        let _shell = widget.scaffold_override().map(crate::ScaffoldInfo::install);
3192        self.walk_node_themed(widget, id, translation, clip, rects, index);
3193        // Over its own children: the reference's `foregroundDecoration`, and the only
3194        // point in the walk where a widget paints after its subtree. Still under this
3195        // node's own theme, since it is this node's decoration.
3196        if let Some(bounds) = over {
3197            if let Some(decoration) = widget.foreground(&self.theme) {
3198                let status = self.full_status(widget, id);
3199                self.scene.set_clip(clip);
3200                self.scene.set_owner(id.as_u64());
3201                self.scene.set_bounds(bounds);
3202                decoration.paint_into(&mut self.scene, bounds, status.opacity);
3203                self.scene.set_clip(clip);
3204            }
3205        }
3206        if let Some(outer) = outer {
3207            self.theme = outer;
3208        }
3209    }
3210
3211    /// The walk proper, with the theme already in place — split out so that no early
3212    /// return inside it can skip putting the outer theme back.
3213    fn walk_node_themed(
3214        &mut self,
3215        widget: &'a dyn Widget<Msg>,
3216        id: WidgetId,
3217        translation: (f32, f32),
3218        clip: Rect,
3219        rects: &[Rect],
3220        index: &mut usize,
3221    ) {
3222        let rect = rects[*index];
3223        *index += 1;
3224        let draw_rect = rect.translate(translation.0, translation.1);
3225        self.inspect_enter(widget, id, draw_rect);
3226
3227        let status = self.full_status(widget, id);
3228        if widget.continuous() {
3229            self.wants_animation = true;
3230        }
3231
3232        self.scene.set_clip(clip);
3233        self.scene.set_owner(id.as_u64());
3234        // The box this widget was given. Text primitives record it: a line of text
3235        // says only where it starts, and the renderer has to know what it covers to
3236        // order it against anything else (milestone 295).
3237        self.scene.set_bounds(draw_rect);
3238        widget.paint(draw_rect, status, &self.theme, &mut self.scene);
3239        // A widget may have tightened the clip (TextField, for one): it is restored here.
3240        self.scene.set_clip(clip);
3241        // A part of the screen saying what the system bars over it should look like, kept
3242        // with its box: the shell asks which ones lie under each bar (#46).
3243        if let Some(style) = widget.system_ui_style() {
3244            self.system_ui.push((draw_rect, style));
3245        }
3246        // The ink a tap left on this surface: over the surface's own paint, under its
3247        // children — where a material surface puts it. The box is recorded too, so the
3248        // shell can start the next splash at the right place, in the right size.
3249        if let Some(style) = widget.ink(&self.theme) {
3250            self.inks.push((id, draw_rect));
3251            if let Some(ripples) = self.runtime.ink.get(&id) {
3252                ripples.paint(
3253                    id.as_u64(),
3254                    draw_rect,
3255                    style.radius,
3256                    style.color,
3257                    &mut self.scene,
3258                );
3259                self.scene.set_clip(clip);
3260            }
3261        }
3262        self.draw_focus_ring(draw_rect, &status, widget);
3263
3264        // A shared element is recorded **whether or not it is on screen**: half way
3265        // through a transition the one being left behind has usually slid off the edge,
3266        // and its box is exactly what the flight starts from. Every other registry here
3267        // is about what a pointer can reach, which is why they are inside the guard.
3268        if let Some(screen) = self.hero_screen {
3269            if let Some(tag) = widget.hero_tag() {
3270                self.heroes.push((
3271                    HeroSpot {
3272                        tag,
3273                        screen,
3274                        id,
3275                        rect: draw_rect,
3276                    },
3277                    widget,
3278                ));
3279            }
3280        }
3281
3282        let visible = draw_rect.intersect(clip);
3283        // A focus stop clipped entirely away by a **scroll** is still a focus stop.
3284        // Tab has to reach the field below the fold — the shell brings it into view
3285        // when it lands there — and registering only what the eye can see is how a long
3286        // form came to have half its fields unreachable from the keyboard. Clipped away
3287        // by anything else it is gone: nothing could reveal it, so focus would land
3288        // where the eye cannot follow.
3289        //
3290        // Only the focus registry does this. A click still needs the visible box, or a
3291        // tap on empty space would land on something scrolled out of sight.
3292        if widget.focusable()
3293            && !self.focus_excluded
3294            && (self.scroll_host.is_some() || (visible.width > 0.0 && visible.height > 0.0))
3295        {
3296            self.focusables.push(Focusable {
3297                id,
3298                rect: visible,
3299                bounds: draw_rect,
3300                form: self.autofill_group,
3301                scroll: self.scroll_host,
3302                skip: self.focus_skipped || widget.focus_skip_traversal(),
3303                order: widget.focus_order().or(self.focus_order),
3304                group: self.focus_group,
3305            });
3306        }
3307        if visible.width > 0.0 && visible.height > 0.0 {
3308            // A press that lands on a **surface** and on nothing inside it stops there.
3309            // Registered before the children, so anything inside still wins on the way
3310            // back out — see [`Widget::opaque`].
3311            if widget.opaque() {
3312                self.hits.push(Hit {
3313                    id,
3314                    rect: visible,
3315                    msg: None,
3316                    xform: None,
3317                });
3318            }
3319            if let Some(msg) = widget.on_click() {
3320                self.hits.push(Hit {
3321                    id,
3322                    rect: visible,
3323                    msg: Some(msg),
3324                    xform: None,
3325                });
3326            }
3327            if let Some(msg) = widget.on_long_press() {
3328                self.long_presses.push(Hit {
3329                    id,
3330                    rect: visible,
3331                    msg: Some(msg),
3332                    xform: None,
3333                });
3334            }
3335            if widget.draggable() {
3336                self.draggables.push((id, visible));
3337            }
3338            if let Some(payload) = widget.drag_payload() {
3339                self.drag_sources.push(DragSource {
3340                    id,
3341                    rect: visible,
3342                    payload,
3343                });
3344            }
3345            if widget.drop_zone() {
3346                self.drop_zones.push(DropZone { id, rect: visible });
3347            }
3348            if widget.reorder_index().is_some() {
3349                self.reorderables.push((id, visible));
3350            }
3351            // The accessibility tree: nodes that carry meaning (a role or a label).
3352            if let Some(sem) = widget.semantics().filter(|s| s.is_meaningful()) {
3353                self.semantics.push((id, visible, sem));
3354            }
3355            // A **multi-line** field whose content overflows: a scrollable region (wheel and
3356            // inertia through the generic machinery; the shell follows the caret) plus a
3357            // draggable scrollbar (mouse and touch).
3358            if let Some(vp) = widget.text_viewport(draw_rect) {
3359                if let Some((content_h, visible_h, _, _)) = widget.text_metrics(draw_rect.width, 0)
3360                {
3361                    let max_y = (content_h - visible_h).max(0.0);
3362                    if max_y > 0.0 {
3363                        self.scrollables.push(Scrollable {
3364                            id,
3365                            viewport: vp,
3366                            max_x: 0.0,
3367                            max_y,
3368                            physics: widget.scroll_physics(),
3369                            refresh: self.refresh_host,
3370                            page: None,
3371                            reverse_x: false,
3372                            reverse_y: false,
3373                            host: self.scroll_host,
3374                            keep_visible: None,
3375                        });
3376                        let offset_y = self
3377                            .runtime
3378                            .scroll
3379                            .get(&id)
3380                            .map(|s| s.1)
3381                            .unwrap_or(0.0)
3382                            .clamp(0.0, max_y);
3383                        self.add_scrollbar(
3384                            id,
3385                            vp,
3386                            Bar {
3387                                vertical: true,
3388                                offset: offset_y,
3389                                max: max_y,
3390                                reverse: false,
3391                                asked: widget.scrollbars(),
3392                                visible: widget.thumb_visibility(),
3393                            },
3394                        );
3395                    }
3396                }
3397            }
3398        }
3399
3400        if let Some((progress, forward)) = widget.navigator() {
3401            let bounds = draw_rect;
3402            let children = widget.children();
3403            let w = bounds.width;
3404            // **The pages stop at the navigator's edge.** A screen sliding in comes from
3405            // outside the box, and one sliding out goes outside it; before milestone 398
3406            // the only thing stopping them was the window, so a navigator that was not the
3407            // whole window painted its pages over its siblings — and even a full-window
3408            // one spent the frame drawing a screen nobody could see. The reference clips
3409            // by default (`Clip.hardEdge`) for the same reason.
3410            let clip = if widget.navigator_clips() {
3411                clip.intersect(bounds)
3412            } else {
3413                clip
3414            };
3415            // The pages under the one on show are kept in the tree and stay out of the picture.
3416            let hidden = widget.navigator_retained().min(children.len());
3417            let shown = &children[hidden..];
3418            if shown.len() >= 2 {
3419                // A transition: two offset screens. The screen "behind" (a negative offset)
3420                // moves less (parallax) → the sense of depth.
3421                let dir = if forward { 1.0 } else { -1.0 };
3422                let raw = [-progress * w * dir, (1.0 - progress) * w * dir];
3423                let off = [
3424                    if raw[0] < 0.0 {
3425                        raw[0] * NAV_PARALLAX
3426                    } else {
3427                        raw[0]
3428                    },
3429                    if raw[1] < 0.0 {
3430                        raw[1] * NAV_PARALLAX
3431                    } else {
3432                        raw[1]
3433                    },
3434                ];
3435                // Depth order: the one offset furthest left (the back one) goes first.
3436                let (back, front) = if off[0] <= off[1] { (0, 1) } else { (1, 0) };
3437                // Where the shared elements of this transition start, in both the
3438                // registry and the scene: everything after this point belongs to the
3439                // two screens, and the flight is resolved from it once both are drawn.
3440                let hero_base = self.heroes.len();
3441                let scene_base = self.scene.primitives().len();
3442                let outer_screen = self.hero_screen.replace(back as u8);
3443                // A screen being left keeps its floating layers to itself.
3444                // `process_overlays` draws every overlay above the **whole window**, after
3445                // both screens, so a menu left open on the departing screen is painted on
3446                // top of the screen that replaced it — opaque, and anchored to a bar the
3447                // window no longer shows. A device found it; the parallax is why it is not
3448                // self-correcting, since the outgoing screen travels only 30 % of the width
3449                // and its anchor never actually leaves.
3450                //
3451                // `Navigator::from` inserts the screen being left at index 0, so
3452                // `children[1]` is always the destination — on a push, on a pop, and under
3453                // a back gesture alike. Whatever the other one defers is dropped.
3454                let overlay_base = self.overlays.len();
3455                self.render_screen(
3456                    shown[back].as_ref(),
3457                    child_id(id, hidden + back, shown[back].as_ref()),
3458                    bounds,
3459                    off[back],
3460                    clip,
3461                );
3462                if back != 1 {
3463                    self.overlays.truncate(overlay_base);
3464                }
3465                // Darkens the screen behind in proportion to how far it is covered.
3466                let coverage = (off[back].abs() / (w * NAV_PARALLAX)).min(1.0);
3467                if coverage > 0.0 {
3468                    let scrim =
3469                        Rect::new(bounds.x + off[back], bounds.y, bounds.width, bounds.height);
3470                    self.scene.set_owner(0);
3471                    self.scene.set_clip(clip);
3472                    self.scene
3473                        .fill_rect(scrim, self.theme.scheme.scrim.with_alpha(0.22 * coverage));
3474                }
3475                self.hero_screen = Some(front as u8);
3476                let overlay_base = self.overlays.len();
3477                self.render_screen(
3478                    shown[front].as_ref(),
3479                    child_id(id, hidden + front, shown[front].as_ref()),
3480                    bounds,
3481                    off[front],
3482                    clip,
3483                );
3484                if front != 1 {
3485                    self.overlays.truncate(overlay_base);
3486                }
3487                self.hero_screen = outer_screen;
3488                self.fly_heroes(hero_base, scene_base, progress);
3489            } else if let Some(screen) = shown.first() {
3490                self.render_screen(
3491                    screen.as_ref(),
3492                    child_id(id, hidden, screen.as_ref()),
3493                    bounds,
3494                    0.0,
3495                    clip,
3496                );
3497            }
3498        } else if widget.interactive().is_some() {
3499            // An **interactive** viewport (`InteractiveViewer`): the child fills the viewport
3500            // at scale 1, then the retained transform (scale + translation, from the shell's
3501            // gestures) is applied to it through **a single layer** carrying both the matrix
3502            // `M` and the clip to the viewport.
3503            let viewport = draw_rect;
3504            let content_clip = clip.intersect(viewport);
3505            self.interactives.push((id, viewport));
3506            if let Some(content) = widget.children().first() {
3507                let content = content.as_ref();
3508                let cid = child_id(id, 0, content);
3509                let content_rects = self.cached_rects(
3510                    cid,
3511                    content,
3512                    Constraints::definite(Size::new(viewport.width, viewport.height)),
3513                );
3514                let matrix = self
3515                    .runtime
3516                    .interactive
3517                    .get(&id)
3518                    .copied()
3519                    .unwrap_or_default()
3520                    .matrix();
3521                self.emit_transformed_child(
3522                    content,
3523                    cid,
3524                    (viewport.x, viewport.y),
3525                    content_clip,
3526                    &content_rects,
3527                    matrix,
3528                    id,
3529                );
3530            }
3531        } else if let Some(q) = widget.rotated_quarter_turns() {
3532            // `RotatedBox`: the child, measured at its **natural** size, is centred in the box
3533            // (with the dimensions swapped for an odd quarter, see `build_layout`) then rotated
3534            // about the centre. The rotation, applied at compositing time, does not stay
3535            // axis-aligned for an odd quarter (the focus bounds are left as they are — the
3536            // click itself stays correct through `M⁻¹`).
3537            let box_rect = draw_rect;
3538            if let Some(child) = widget.children().first() {
3539                let child = child.as_ref();
3540                let cid = child_id(id, 0, child);
3541                let child_rects =
3542                    self.cached_rects(cid, child, Constraints::scroll(0.0, 0.0, true, true));
3543                let nat = child_rects
3544                    .first()
3545                    .copied()
3546                    .unwrap_or(Rect::new(0.0, 0.0, 0.0, 0.0));
3547                let center = Point::new(
3548                    box_rect.x + box_rect.width / 2.0,
3549                    box_rect.y + box_rect.height / 2.0,
3550                );
3551                let translation = (center.x - nat.width / 2.0, center.y - nat.height / 2.0);
3552                let angle = q.rem_euclid(4) as f32 * std::f32::consts::FRAC_PI_2;
3553                let angle = if self.rtl() { -angle } else { angle };
3554                let matrix = Affine::rotation(angle).about(center);
3555                self.emit_transformed_child(
3556                    child,
3557                    cid,
3558                    translation,
3559                    clip,
3560                    &child_rects,
3561                    matrix,
3562                    id,
3563                );
3564            }
3565        } else if let Some(fit) = widget.fitted() {
3566            // `FittedBox`: the child, measured at its **natural** size, is scaled according to
3567            // the `BoxFit` (uniformly, except for `Fill`), centred and clipped to the box. The
3568            // scale stays axis-aligned → the focus bounds follow.
3569            let box_rect = draw_rect;
3570            let content_clip = clip.intersect(box_rect);
3571            if let Some(child) = widget.children().first() {
3572                let child = child.as_ref();
3573                let cid = child_id(id, 0, child);
3574                let child_rects =
3575                    self.cached_rects(cid, child, Constraints::scroll(0.0, 0.0, true, true));
3576                let nat = child_rects
3577                    .first()
3578                    .copied()
3579                    .unwrap_or(Rect::new(0.0, 0.0, 0.0, 0.0));
3580                let (sx, sy) = fit.scale(
3581                    Size::new(nat.width, nat.height),
3582                    Size::new(box_rect.width, box_rect.height),
3583                );
3584                let center = Point::new(
3585                    box_rect.x + box_rect.width / 2.0,
3586                    box_rect.y + box_rect.height / 2.0,
3587                );
3588                let translation = (center.x - nat.width / 2.0, center.y - nat.height / 2.0);
3589                let matrix = Affine::scale(sx, sy).about(center);
3590                self.emit_transformed_child(
3591                    child,
3592                    cid,
3593                    translation,
3594                    content_clip,
3595                    &child_rects,
3596                    matrix,
3597                    id,
3598                );
3599            }
3600        } else if let Some(content) = widget.scroll_content() {
3601            let axis = widget.scroll_axis();
3602            let viewport = draw_rect;
3603            let content_clip = clip.intersect(viewport);
3604            // `reverse` applies to the axis this scrolls along; a two-dimensional scroll
3605            // takes it on the vertical, which is the one a reversed view is ever about.
3606            let reverse = if widget.scroll_reverse() {
3607                (axis.free_x() && !axis.free_y(), axis.free_y())
3608            } else {
3609                (false, false)
3610            };
3611
3612            // The padding is **inside** the viewport and scrolls with the content:
3613            // room at the end of a feed is reachable by scrolling to it, room taken out
3614            // of the window would sit there for ever. The reference's `SliverPadding`.
3615            let pad = widget.scroll_padding();
3616            let content_rects = self.cached_rects(
3617                child_id(id, 0, content),
3618                content,
3619                Constraints::scroll(
3620                    (viewport.width - pad.left - pad.right).max(0.0),
3621                    (viewport.height - pad.top - pad.bottom).max(0.0),
3622                    axis.free_x(),
3623                    axis.free_y(),
3624                ),
3625            );
3626
3627            let content_size = content_rects
3628                .first()
3629                .copied()
3630                .unwrap_or(Rect::new(0.0, 0.0, 0.0, 0.0));
3631            // What scrolls is the content **and** its room, which is what makes the far
3632            // inset reachable rather than decorative.
3633            let scrolled = Size::new(
3634                content_size.width + pad.left + pad.right,
3635                content_size.height + pad.top + pad.bottom,
3636            );
3637            let max_x = (scrolled.width - viewport.width).max(0.0);
3638            let max_y = (scrolled.height - viewport.height).max(0.0);
3639
3640            // The geometry without the identity: enough to ask where the content sits at
3641            // an offset, and what offset would put a box where it is wanted.
3642            let frame = Scrollable {
3643                id,
3644                viewport,
3645                max_x,
3646                max_y,
3647                physics: None,
3648                refresh: None,
3649                page: None,
3650                reverse_x: reverse.0,
3651                reverse_y: reverse.1,
3652                host: None,
3653                keep_visible: None,
3654            };
3655            // What the content asks to be kept in view — a tab bar's selected tab — in
3656            // the content's own coordinates, which is the only frame it can speak in.
3657            let asked = content.keep_visible(
3658                Size::new(content_size.width, content_size.height),
3659                &self.theme,
3660            );
3661            // Where that box would sit if nobody had scrolled: the frame's coordinates,
3662            // with the offset taken out. See [`KeepVisible::rect`] for why it is stored
3663            // this way rather than where it is drawn.
3664            let keep = asked.map(|keep| {
3665                let rest = frame.content_origin((0.0, 0.0), scrolled);
3666                KeepVisible {
3667                    rect: keep.rect.translate(
3668                        viewport.x + rest.0 + pad.left,
3669                        viewport.y + rest.1 + pad.top,
3670                    ),
3671                    ..keep
3672                }
3673            });
3674            // Inside the extent this frame has just measured, unless something owns the
3675            // offset: content that shrank is drawn where it rests (milestone 533).
3676            let (offset_x, offset_y) = match self.runtime.scroll_offset_within(id, (max_x, max_y)) {
3677                Some(offset) => offset,
3678                // Never scrolled, and something inside wants to be seen: **open** there
3679                // rather than sliding across a frame later. A tab bar restored on its
3680                // eighth tab should show the eighth tab, not show the first and then
3681                // travel. The same answer a paged view gives its initial page, and the
3682                // runtime retains the offset on that first frame so it stays.
3683                None => match keep {
3684                    Some(keep) => frame.keep_offset(keep).0,
3685                    None => (0.0, 0.0),
3686                },
3687            };
3688
3689            let origin = frame.content_origin((offset_x, offset_y), scrolled);
3690            let content_translation = (
3691                viewport.x + origin.0 + pad.left,
3692                viewport.y + origin.1 + pad.top,
3693            );
3694            self.scrollables.push(Scrollable {
3695                id,
3696                viewport,
3697                max_x,
3698                max_y,
3699                physics: widget.scroll_physics(),
3700                refresh: self.refresh_host,
3701                page: None,
3702                reverse_x: reverse.0,
3703                reverse_y: reverse.1,
3704                host: self.scroll_host,
3705                keep_visible: keep,
3706            });
3707            let mut content_index = 0;
3708            let outer = self.scroll_host.replace(id);
3709            self.walk(
3710                content,
3711                child_id(id, 0, content),
3712                content_translation,
3713                content_clip,
3714                &content_rects,
3715                &mut content_index,
3716            );
3717            self.scroll_host = outer;
3718
3719            // The overscroll glow, then the scrollbars, over the content (not
3720            // clipped by it).
3721            self.scene.set_clip(clip);
3722            self.add_overscroll_glow(id, viewport);
3723            if max_y > 0.0 {
3724                self.add_scrollbar(
3725                    id,
3726                    viewport,
3727                    Bar {
3728                        vertical: true,
3729                        offset: offset_y,
3730                        max: max_y,
3731                        reverse: reverse.1,
3732                        asked: widget.scrollbars(),
3733                        visible: widget.thumb_visibility(),
3734                    },
3735                );
3736            }
3737            if max_x > 0.0 {
3738                self.add_scrollbar(
3739                    id,
3740                    viewport,
3741                    Bar {
3742                        vertical: false,
3743                        offset: offset_x,
3744                        max: max_x,
3745                        reverse: reverse.0,
3746                        asked: widget.scrollbars(),
3747                        visible: widget.thumb_visibility(),
3748                    },
3749                );
3750            }
3751        } else if let Some(vlist) =
3752            widget.virtual_list(Size::new(draw_rect.width, draw_rect.height))
3753        {
3754            // A virtualised list: only build/lay out/paint the visible window.
3755            let viewport = draw_rect;
3756            let content_clip = clip.intersect(viewport);
3757            let reverse = widget.scroll_reverse();
3758            // Down the screen or across it. A list virtualises along **one** axis, which
3759            // is what lets it place item `n` without building the ones before it, so
3760            // `Axis::Both` has no meaning here and reads as vertical.
3761            let across = matches!(vlist.axis, crate::scroll::Axis::Horizontal);
3762            // Room around the items, inside the viewport and scrolling with them. The
3763            // **leading** inset is the one at the end the items start from, so a
3764            // reversed list clears the far end first and both keep their sides.
3765            let pad = widget.scroll_padding();
3766            let (lead, trail, cross_near, cross_far) = match (across, reverse) {
3767                (true, false) => (pad.left, pad.right, pad.top, pad.bottom),
3768                (true, true) => (pad.right, pad.left, pad.top, pad.bottom),
3769                (false, false) => (pad.top, pad.bottom, pad.left, pad.right),
3770                (false, true) => (pad.bottom, pad.top, pad.left, pad.right),
3771            };
3772            // From here down it is a main axis and a cross axis, and only the two
3773            // branches at the end have to know which is which. The alternative was a
3774            // second copy of the window arithmetic below, which is where a reversed
3775            // list gets its signs right -- and a fix applied to one copy and not the
3776            // other is exactly how the two would drift apart.
3777            let (extent, cross_extent) = match across {
3778                true => (viewport.width, viewport.height),
3779                false => (viewport.height, viewport.width),
3780            };
3781            let content = lead + trail + vlist.count as f32 * vlist.item_extent;
3782            let max = (content - extent).max(0.0);
3783            let bounds = if across { (max, 0.0) } else { (0.0, max) };
3784            let offset = self
3785                .runtime
3786                .scroll_offset_within(id, bounds)
3787                .map_or(0.0, |(x, y)| if across { x } else { y });
3788            self.scrollables.push(Scrollable {
3789                id,
3790                viewport,
3791                max_x: if across { max } else { 0.0 },
3792                max_y: if across { 0.0 } else { max },
3793                physics: widget.scroll_physics(),
3794                refresh: self.refresh_host,
3795                page: None,
3796                reverse_x: across && reverse,
3797                reverse_y: !across && reverse,
3798                host: self.scroll_host,
3799                keep_visible: None,
3800            });
3801
3802            let outer = self.scroll_host.replace(id);
3803            if vlist.item_extent > 0.0 && vlist.count > 0 {
3804                // The **window** is the same arithmetic either way, and that is not a
3805                // coincidence: a reversed list counts its indices from the end, and a
3806                // reversed offset counts its pixels from the end, so index and offset
3807                // agree about which way is forward. Only where an item lands differs.
3808                let first = ((offset - lead) / vlist.item_extent).floor().max(0.0) as usize;
3809                let last = ((((offset + extent - lead) / vlist.item_extent).ceil()).max(0.0)
3810                    as usize)
3811                    .min(vlist.count);
3812                let origin = if across { viewport.x } else { viewport.y };
3813                for i in first..last {
3814                    let item = (vlist.build)(i);
3815                    let along = if reverse {
3816                        origin + extent - lead - (i + 1) as f32 * vlist.item_extent + offset
3817                    } else {
3818                        origin + lead + i as f32 * vlist.item_extent - offset
3819                    };
3820                    let (x, y) = match across {
3821                        true => (along, viewport.y + cross_near),
3822                        false => (viewport.x + cross_near, along),
3823                    };
3824
3825                    // **Filled**, not merely constrained: a list hands its children a
3826                    // box rather than asking them how big they would like to be. The
3827                    // reference gives a list's children a *tight* cross-axis extent, and
3828                    // for a fixed-extent list a tight main-axis one too — which is
3829                    // exactly the pair of numbers here.
3830                    //
3831                    // Constrained-but-not-filled, a row whose width nobody set hugged
3832                    // its own text: a list of coloured rows painted a column of chips
3833                    // down the left instead of rows across the list. Found on a device
3834                    // in milestone 349.
3835                    let free_cross = (cross_extent - cross_near - cross_far).max(0.0);
3836                    let item_rects = self.cached_rects(
3837                        id.child(i),
3838                        item.as_ref(),
3839                        Constraints::filled(match across {
3840                            true => Size::new(vlist.item_extent, free_cross),
3841                            false => Size::new(free_cross, vlist.item_extent),
3842                        }),
3843                    );
3844
3845                    let mut item_index = 0;
3846                    self.render_item(
3847                        item.as_ref(),
3848                        id.child(i),
3849                        (x, y),
3850                        content_clip,
3851                        &item_rects,
3852                        &mut item_index,
3853                    );
3854                }
3855            }
3856
3857            self.scroll_host = outer;
3858            self.scene.set_clip(clip);
3859            self.add_overscroll_glow(id, viewport);
3860            if max > 0.0 {
3861                self.add_scrollbar(
3862                    id,
3863                    viewport,
3864                    Bar {
3865                        vertical: !across,
3866                        offset,
3867                        max,
3868                        reverse,
3869                        asked: widget.scrollbars(),
3870                        visible: widget.thumb_visibility(),
3871                    },
3872                );
3873            }
3874        } else if let Some(pages) = widget.page_view() {
3875            // A paged view: like a virtualised list turned on its side, and with the
3876            // page extent — the whole geometry — derived from the viewport rather than
3877            // given. Only the pages the viewport touches are built.
3878            let viewport = draw_rect;
3879            let content_clip = clip.intersect(viewport);
3880            let snap = pages.snap(viewport);
3881            let (viewport_along, page_across) = if snap.horizontal {
3882                (viewport.width, viewport.height)
3883            } else {
3884                (viewport.height, viewport.width)
3885            };
3886            let reverse = widget.scroll_reverse();
3887            // Room at both ends, so a page narrower than the viewport comes to rest
3888            // **centred** in it rather than jammed against the leading edge — and so
3889            // the last one can be reached at all: unpadded, the travel stops with the
3890            // last page's trailing edge on the viewport's, which is short of where that
3891            // page rests. Padded, the total works out to `extent × (count - 1)`,
3892            // the last page's own offset, exactly.
3893            let pad = pages.end_padding(viewport);
3894            let total = pages.count as f32 * snap.extent + 2.0 * pad;
3895            let max = (total - viewport_along).max(0.0);
3896            // With no retained offset this is the view's **first** frame, and it opens
3897            // on the page it was asked for. Reading the initial page here rather than
3898            // correcting it a frame later is what keeps page 0 from flashing past on
3899            // the way to page 3.
3900            let bounds = if snap.horizontal {
3901                (max, 0.0)
3902            } else {
3903                (0.0, max)
3904            };
3905            let along = match self.runtime.scroll_offset_within(id, bounds) {
3906                Some((x, y)) => {
3907                    if snap.horizontal {
3908                        x
3909                    } else {
3910                        y
3911                    }
3912                }
3913                None => snap
3914                    .offset_of(snap.requested.min(pages.count.saturating_sub(1)))
3915                    .clamp(0.0, max),
3916            };
3917            self.scrollables.push(Scrollable {
3918                id,
3919                viewport,
3920                max_x: if snap.horizontal { max } else { 0.0 },
3921                max_y: if snap.horizontal { 0.0 } else { max },
3922                physics: widget.scroll_physics(),
3923                refresh: self.refresh_host,
3924                page: Some(snap),
3925                reverse_x: snap.horizontal && reverse,
3926                reverse_y: !snap.horizontal && reverse,
3927                host: self.scroll_host,
3928                keep_visible: None,
3929            });
3930
3931            let outer = self.scroll_host.replace(id);
3932            if pages.count > 0 {
3933                // The window is the same arithmetic whichever end index 0 is at: a
3934                // reversed view counts its indices from the far end and a reversed
3935                // offset counts its pixels from the same one, so the two agree about
3936                // which way forward is. Only where a page **lands** differs.
3937                let first = ((along - pad) / snap.extent).floor().max(0.0) as usize;
3938                let last = ((((along + viewport_along - pad) / snap.extent).ceil()).max(0.0)
3939                    as usize)
3940                    .min(pages.count)
3941                    .max(first + 1);
3942                // A wheel shows **more** rows than a flat strip would: the ones a quarter
3943                // turn away are compressed into the last few pixels at either end, and
3944                // their flat positions are a whole quarter-circumference outside the
3945                // viewport. Widening the window in both directions keeps the reversed
3946                // arithmetic above untouched, and the rows that turn out to have gone
3947                // over the horizon are dropped below — before they are built.
3948                let (first, last) = match pages.wheel {
3949                    Some(wheel) => {
3950                        let span = wheel.radius(viewport_along) * std::f32::consts::FRAC_PI_2;
3951                        let extra = (span / snap.extent).ceil().max(0.0) as usize + 1;
3952                        (first.saturating_sub(extra), (last + extra).min(pages.count))
3953                    }
3954                    None => (first, last),
3955                };
3956                for index in first..last.min(pages.count) {
3957                    let start = match reverse {
3958                        true => viewport_along - pad - (index + 1) as f32 * snap.extent + along,
3959                        false => pad + index as f32 * snap.extent - along,
3960                    };
3961                    // Where this row's centre would sit on the unrolled strip, measured
3962                    // from the middle of the viewport — which is the whole of what the
3963                    // cylinder needs to know about it.
3964                    let on_wheel = match pages.wheel {
3965                        Some(wheel) => {
3966                            let flat = start + snap.extent / 2.0 - viewport_along / 2.0;
3967                            match wheel.row(viewport_along, flat) {
3968                                // Past the horizon: not drawn, and not built either.
3969                                None => continue,
3970                                Some(row) => Some((row, flat)),
3971                            }
3972                        }
3973                        None => None,
3974                    };
3975                    let page = (pages.build)(index);
3976                    let (size, origin) = if snap.horizontal {
3977                        (
3978                            Size::new(snap.extent, page_across),
3979                            (viewport.x + start, viewport.y),
3980                        )
3981                    } else {
3982                        (
3983                            Size::new(page_across, snap.extent),
3984                            (viewport.x, viewport.y + start),
3985                        )
3986                    };
3987
3988                    // A page is **given** its box, not asked for one: it fills the
3989                    // panel even when its content is a single centred line.
3990                    let page_rects = self.cached_rects(
3991                        id.child(index),
3992                        page.as_ref(),
3993                        Constraints::filled(size),
3994                    );
3995
3996                    let mut page_index = 0;
3997                    // The row is drawn **flat**, then the whole of it is composited on
3998                    // the cylinder — one layer per row, the way `Transform` does it, so
3999                    // the text, the rules and the backgrounds all turn together instead
4000                    // of each primitive having to know it is on a wheel.
4001                    let before = self.scene.primitives().len();
4002                    let base = self.xform_base();
4003                    self.render_item(
4004                        page.as_ref(),
4005                        id.child(index),
4006                        origin,
4007                        content_clip,
4008                        &page_rects,
4009                        &mut page_index,
4010                    );
4011                    if let Some((row, flat)) = on_wheel {
4012                        // The row's own centre on screen: the scale is about it, so a row
4013                        // shrinks in place rather than creeping towards a corner.
4014                        let centre = Point::new(
4015                            viewport.x + viewport.width / 2.0,
4016                            viewport.y + viewport_along / 2.0 + flat,
4017                        );
4018                        let matrix = Affine::scale(row.scale_x, row.scale_y)
4019                            .about(centre)
4020                            // And then up or down to where the cylinder actually put it.
4021                            .then(Affine::translation(0.0, row.offset - flat));
4022                        let group = self.scene.split_off(before);
4023                        self.scene.push_primitive(Primitive::Layer {
4024                            primitives: group,
4025                            opacity: row.opacity.clamp(0.0, 1.0),
4026                            clip: content_clip,
4027                            clip_shape: ClipShape::Rect,
4028                            transform: Some(LayerTransform::new(matrix)),
4029                            filter: LayerFilter::NONE,
4030                            owner: id.child(index).as_u64(),
4031                        });
4032                        self.transform_interaction_registries(&base, matrix);
4033                    }
4034                }
4035            }
4036
4037            self.scroll_host = outer;
4038            self.scene.set_clip(clip);
4039            self.add_overscroll_glow(id, viewport);
4040        } else if let Some(overflow) = widget.overflow_box() {
4041            // The child is laid out to constraints of its own — which is why it can
4042            // come out bigger than the box holding it — then anchored inside it and
4043            // rendered **without** the box's clip. A spill that got clipped here would
4044            // be no spill at all; a `ClipRect` above is how a caller asks for one.
4045            let own = draw_rect;
4046            if let Some(child) = widget.children().first() {
4047                let child = child.as_ref();
4048                let cid = child_id(id, 0, child);
4049                let asked = overflow.child_size(Size::new(own.width, own.height));
4050                let child_rects = if overflow.unconstrained {
4051                    // Unconstrained: the child is asked how big it wants to be, and
4052                    // gets exactly that.
4053                    self.cached_rects(cid, child, Constraints::scroll(0.0, 0.0, true, true))
4054                } else {
4055                    self.cached_rects(cid, child, Constraints::filled(asked))
4056                };
4057                let size = child_rects
4058                    .first()
4059                    .map(|r| Size::new(r.width, r.height))
4060                    .unwrap_or(asked);
4061                let origin = overflow.origin(own, size);
4062
4063                let mut child_index = 0;
4064                self.render_item(child, cid, origin, clip, &child_rects, &mut child_index);
4065            }
4066        } else if let Some(transform) = widget.constraints_transform() {
4067            // The child is laid out at **the same box the measurement gave it**, worked out
4068            // again from what is left of that measurement — the box it produced. Each of
4069            // the three things an axis can say survives the round trip; see
4070            // `AxisConstraint::at`.
4071            let own = draw_rect;
4072            if let Some(child) = widget.children().first() {
4073                let child = child.as_ref();
4074                let cid = child_id(id, 0, child);
4075                let (aw, free_x) = transform.width.at(own.width);
4076                let (ah, free_y) = transform.height.at(own.height);
4077                let constraints = match (free_x, free_y) {
4078                    (false, false) => Constraints::filled(Size::new(aw, ah)),
4079                    _ => Constraints::scroll(aw, ah, free_x, free_y),
4080                };
4081                let child_rects = self.cached_rects(cid, child, constraints);
4082                let size = child_rects
4083                    .first()
4084                    .map(|r| Size::new(r.width, r.height))
4085                    .unwrap_or(Size::new(0.0, 0.0));
4086                let align = transform.alignment;
4087                let origin = (
4088                    own.x + (own.width - size.width) * align.fraction_x(),
4089                    own.y + (own.height - size.height) * align.fraction_y(),
4090                );
4091                if transform.report {
4092                    self.report_spill(own, Rect::new(origin.0, origin.1, size.width, size.height));
4093                }
4094                let mut child_index = 0;
4095                self.render_item(child, cid, origin, clip, &child_rects, &mut child_index);
4096            }
4097        } else if let Some(build) = widget.layout_builder() {
4098            // Builds the content from the actual box, then lays it out and renders it inside
4099            // (like a list item: with no retained state).
4100            let bounds = draw_rect;
4101            let content_clip = clip.intersect(bounds);
4102            let child = build(Size::new(bounds.width, bounds.height));
4103
4104            // **Filled**, not merely constrained: the content is handed this box, and it
4105            // was built from this box in the first place. Constraining it instead let a
4106            // root with no width of its own hug its content — a grid built here laid its
4107            // columns out at nothing, which is what `GridView::extent`'s first tests found
4108            // (milestone 356). The same distinction a list's items and a paged view's
4109            // pages already draw.
4110            let child_rects = self.cached_rects(
4111                id.child(0),
4112                child.as_ref(),
4113                Constraints::filled(Size::new(bounds.width, bounds.height)),
4114            );
4115
4116            let mut child_index = 0;
4117            self.render_item(
4118                child.as_ref(),
4119                id.child(0),
4120                (bounds.x, bounds.y),
4121                content_clip,
4122                &child_rects,
4123                &mut child_index,
4124            );
4125        } else if let Some(build) = widget.scroll_overlay() {
4126            // The **body first**, exactly as an ordinary single child: it keeps its own
4127            // identity, registers itself as a scroll region in its own right, and consumes
4128            // its rects from this array the way the generic walk would. Nothing about it
4129            // changes for being watched.
4130            let children = widget.children();
4131            for (child_index, child) in children.iter().enumerate() {
4132                self.walk(
4133                    child.as_ref(),
4134                    child_id(id, child_index, child.as_ref()),
4135                    translation,
4136                    clip,
4137                    rects,
4138                    index,
4139                );
4140            }
4141            // Then the overlay, built from **where the body has got to this frame**. Read
4142            // from the retained offset rather than from the registry the walk is still
4143            // filling: the offset is written before the frame is drawn, so it is current,
4144            // where the extents beside it are not yet known — which is why the builder is
4145            // handed the one and not the other.
4146            let offset = children
4147                .first()
4148                .map(|body| child_id(id, 0, body.as_ref()))
4149                .and_then(|body| self.runtime.scroll.get(&body).copied())
4150                .unwrap_or((0.0, 0.0));
4151            let bounds = draw_rect;
4152            let over = build(offset, Size::new(bounds.width, bounds.height));
4153            // `child(1)`, beside the body's `child(0)` — or beside its key, which cannot
4154            // collide with a positional identity. The overlay is laid out in a tree of its
4155            // own, so it needs an identity of its own for hover, ink and the paint cache.
4156            let over_id = id.child(1);
4157            let over_rects = self.cached_rects(
4158                over_id,
4159                over.as_ref(),
4160                // **Filled**, not constrained: an overlay covers the region it is over, and
4161                // where inside that box its content sits is the caller's business — a
4162                // column pins it to the top, an `Align` puts it anywhere else. The same
4163                // rule a `layout_builder`'s content is given.
4164                Constraints::filled(Size::new(bounds.width, bounds.height)),
4165            );
4166            let mut over_index = 0;
4167            self.render_item(
4168                over.as_ref(),
4169                over_id,
4170                (bounds.x, bounds.y),
4171                clip.intersect(bounds),
4172                &over_rects,
4173                &mut over_index,
4174            );
4175        } else if let Some(spec) = widget.dismissible() {
4176            // A dismissible item is a stack whose **last** layer — the item itself — is
4177            // offset by however far it has been swiped, and whose earlier layers, the
4178            // backgrounds, show only through the strip the item has uncovered. Clipping
4179            // them is what makes a background read as something the item is sliding
4180            // *off*, rather than a block that was always there.
4181            let bounds = draw_rect;
4182            let state = self.runtime.dismiss.get(&id).copied();
4183            let progress = state.map(|s| s.progress()).unwrap_or(0.0);
4184            let layers = widget.children();
4185            let last = layers.len().saturating_sub(1);
4186            // Which background: the first covers a swipe towards the end, the second —
4187            // when there is one — the other way. With only one, it serves both.
4188            let shown_background = if progress < 0.0 && last >= 2 { 1 } else { 0 };
4189            let strip = crate::dismiss::revealed_strip(bounds, progress);
4190
4191            for (i, layer) in layers.iter().enumerate() {
4192                let is_item = i == last;
4193                if !is_item && (i != shown_background || progress == 0.0) {
4194                    // Not the background this direction shows: skip it, and skip the
4195                    // rects it would have consumed — it is laid out separately, so
4196                    // there is no index to keep in step.
4197                    continue;
4198                }
4199                let offset = if is_item {
4200                    if spec.axis.is_horizontal() {
4201                        (bounds.width * progress, 0.0)
4202                    } else {
4203                        (0.0, bounds.height * progress)
4204                    }
4205                } else {
4206                    (0.0, 0.0)
4207                };
4208                let layer_clip = if is_item {
4209                    clip.intersect(bounds)
4210                } else {
4211                    clip.intersect(strip)
4212                };
4213                if layer_clip.width <= 0.0 || layer_clip.height <= 0.0 {
4214                    continue;
4215                }
4216                let cid = child_id(id, i, layer.as_ref());
4217                let layer_rects = self.cached_rects(
4218                    cid,
4219                    layer.as_ref(),
4220                    Constraints::filled(Size::new(bounds.width, bounds.height)),
4221                );
4222                let mut layer_index = 0;
4223                self.walk(
4224                    layer.as_ref(),
4225                    cid,
4226                    (bounds.x + offset.0, bounds.y + offset.1),
4227                    layer_clip,
4228                    &layer_rects,
4229                    &mut layer_index,
4230                );
4231            }
4232
4233            self.dismissables.push(Dismissable {
4234                id,
4235                rect: bounds,
4236                spec,
4237            });
4238            // A swipe that is settling, flying or collapsing drives itself.
4239            if state.is_some_and(|s| s.phase() != DismissPhase::Drag) {
4240                self.wants_animation = true;
4241            }
4242        } else if widget.stack() {
4243            // A stack: the layers are rendered in order, bottom first.
4244            let bounds = draw_rect;
4245            let layer_clip = clip.intersect(bounds);
4246            let loose = widget.stack_loose();
4247            // An **indexed** stack shows one layer and lays out the rest. Which is a
4248            // question for here rather than for the widget: a layer that wrapped itself in
4249            // something to keep quiet would no longer be laid out the way a bare layer is,
4250            // and the layout is the whole promise.
4251            let shown = widget.stack_visible();
4252            // Where a layer smaller than the stack sits. Resolved once, against the
4253            // reading direction, so a start-anchored badge follows the script.
4254            let align = widget
4255                .alignment_geometry()
4256                .unwrap_or(frus_core::AlignmentGeometry::Physical(
4257                    frus_core::Alignment::TOP_LEFT,
4258                ))
4259                .resolve(if self.rtl() {
4260                    frus_core::TextDirection::Rtl
4261                } else {
4262                    frus_core::TextDirection::Ltr
4263                });
4264            for (i, layer) in widget.children().iter().enumerate() {
4265                let cid = child_id(id, i, layer.as_ref());
4266                // What this layer asks for on each axis: a pinned pair gives a number, a
4267                // lone pin plus an explicit extent gives one, and neither leaves the axis
4268                // to the layer itself — or, under `Expand`, to the stack.
4269                // A layer moving between two sets of pins is read from the runtime rather
4270                // than from the widget: the widget carries where it is **going**, and a
4271                // layer that jumped there would be the whole bug this animates away.
4272                // Both the size and the place come from the one value, so a box cannot be
4273                // measured against one frame's pins and placed against another's.
4274                let pins = match layer.positioned() {
4275                    Some(spec) if layer.anim_pins().is_some() => {
4276                        Some(self.runtime.anim_pins(cid).unwrap_or(spec))
4277                    }
4278                    other => other,
4279                };
4280                let (want_w, want_h) = match pins {
4281                    Some(p) => (
4282                        p.resolved_width(bounds.width),
4283                        p.resolved_height(bounds.height),
4284                    ),
4285                    None if loose => (None, None),
4286                    // The historical behaviour, and still the default: a layer is
4287                    // **given** the box rather than asked what size it would like. An
4288                    // unsized layer that hugged its content would collapse to nothing —
4289                    // invisibly, since a stack draws no box of its own.
4290                    None => (Some(bounds.width), Some(bounds.height)),
4291                };
4292                // A pinned layer is **forced** into what its edges say: two opposite
4293                // pins are not a suggestion, and a width of its own is a contradiction
4294                // rather than a second opinion. An unpinned layer under `Expand` is
4295                // merely handed the box, which leaves a layer that chose a size alone —
4296                // that is what a badge sitting in a filled stack has always relied on.
4297                // An axis nobody pinned is left free, so the layer's own size comes back.
4298                let layer_rects = if pins.is_some() {
4299                    self.cached_rects(
4300                        cid,
4301                        layer.as_ref(),
4302                        Constraints::pinned(want_w, want_h, Size::new(bounds.width, bounds.height)),
4303                    )
4304                } else {
4305                    match (want_w, want_h) {
4306                        (Some(w), Some(h)) => self.cached_rects(
4307                            cid,
4308                            layer.as_ref(),
4309                            Constraints::filled(Size::new(w, h)),
4310                        ),
4311                        _ => self.cached_rects(
4312                            cid,
4313                            layer.as_ref(),
4314                            Constraints::scroll(bounds.width, bounds.height, true, true),
4315                        ),
4316                    }
4317                };
4318                let own = layer_rects
4319                    .first()
4320                    .copied()
4321                    .unwrap_or(Rect::new(0.0, 0.0, 0.0, 0.0));
4322                let (dx, dy) = match pins {
4323                    Some(p) => (
4324                        Positioning::place(
4325                            p.left,
4326                            p.right,
4327                            bounds.width,
4328                            own.width,
4329                            align.fraction_x(),
4330                        ),
4331                        Positioning::place(
4332                            p.top,
4333                            p.bottom,
4334                            bounds.height,
4335                            own.height,
4336                            align.fraction_y(),
4337                        ),
4338                    ),
4339                    None => (
4340                        (bounds.width - own.width).max(0.0) * align.fraction_x(),
4341                        (bounds.height - own.height).max(0.0) * align.fraction_y(),
4342                    ),
4343                };
4344                let mut layer_index = 0;
4345                // A layer that is not the shown one is walked in full — so it is measured,
4346                // and so the runtime keeps its scroll offset, its caret and its focus — and
4347                // then everything it added is dropped again. The same withdrawal a
4348                // `Visibility` that maintains its size performs, applied from outside.
4349                let hidden = shown.is_some_and(|which| which != i);
4350                let base = hidden.then(|| self.barrier_base());
4351                self.walk(
4352                    layer.as_ref(),
4353                    cid,
4354                    (bounds.x + dx, bounds.y + dy),
4355                    layer_clip,
4356                    &layer_rects,
4357                    &mut layer_index,
4358                );
4359                if let Some(base) = base {
4360                    self.apply_barrier(HIDDEN_LAYER, &base, cid, Rect::new(0.0, 0.0, 0.0, 0.0));
4361                }
4362            }
4363        } else if let Some((content, placement)) = widget.overlay() {
4364            // The anchor (child 0) is rendered inline; the overlay (child 1) is deferred.
4365            self.walk(
4366                widget.children()[0].as_ref(),
4367                child_id(id, 0, widget.children()[0].as_ref()),
4368                translation,
4369                clip,
4370                rects,
4371                index,
4372            );
4373            // The appearance progress: for an animated overlay (a drawer), the value the
4374            // runtime interpolated; otherwise `1.0` (shown at once). With no value recorded
4375            // (an isolated render), the target is adopted immediately.
4376            let target = widget.anim_target().unwrap_or(1.0);
4377            let progress = self.runtime.value_or(id, target);
4378            // A tooltip only shows while the anchor is hovered; an animated overlay disappears
4379            // once its progress has fallen back to zero.
4380            let visible = match placement {
4381                Placement::Tooltip => self.runtime.input.hovered == Some(id.child(0)),
4382                _ => true,
4383            };
4384            if visible && progress > 0.001 {
4385                self.overlays.push((
4386                    content,
4387                    child_id(id, 1, content),
4388                    draw_rect,
4389                    placement,
4390                    widget.overlay_dismiss(),
4391                    progress,
4392                    widget.overlay_traps_focus(),
4393                    widget.overlay_scrim(&self.theme),
4394                    self.theme.clone(),
4395                ));
4396            }
4397        } else {
4398            let children = widget.children();
4399            // Fractional alignment (`Container::alignment`) + paint offset
4400            // (`Transform::translate`): both offset the subtree. `(0, 0)` otherwise → the
4401            // ordinary flex walk.
4402            let extra = self.child_offset(widget, id, rect, rects, *index, children);
4403            for (child_index, child) in children.iter().enumerate() {
4404                self.walk(
4405                    child.as_ref(),
4406                    child_id(id, child_index, child.as_ref()),
4407                    (translation.0 + extra.0, translation.1 + extra.1),
4408                    clip,
4409                    rects,
4410                    index,
4411                );
4412            }
4413        }
4414        self.depth -= 1;
4415    }
4416
4417    /// Offset to apply to a child's subtree: the sum of the **fractional alignment**
4418    /// (`Widget::alignment`, a single child slid through the free space) and the **paint
4419    /// offset** (`Transform::translate`, the whole subtree offset without touching layout).
4420    /// `(0, 0)` by default. Since it is added to the walk's translation, this offset follows
4421    /// both the primitives **and** the hit-test.
4422    fn child_offset(
4423        &self,
4424        widget: &dyn Widget<Msg>,
4425        id: WidgetId,
4426        container: Rect,
4427        rects: &[Rect],
4428        child_index: usize,
4429        children: &[Box<dyn Widget<Msg>>],
4430    ) -> (f32, f32) {
4431        let mut off = (0.0, 0.0);
4432
4433        // Fractional alignment: it targets a single child. taffy laid the child out at the
4434        // top left (Start/Start) at its natural size; it is then slid by `free × fraction`.
4435        // A **stack** answers this question too, and means something else by it: where a
4436        // layer smaller than the box sits, which the stack branch applies per layer. It
4437        // must not also be applied to the stack itself, or a stack with exactly one layer
4438        // would shift twice.
4439        if let (Some(geo), 1, false) = (widget.alignment_geometry(), children.len(), widget.stack())
4440        {
4441            // An **animated** anchor moves through the runtime's pair rather than the
4442            // widget's own numbers, in the anchor's own coordinates — so the reading
4443            // direction is applied to the interpolated anchor below, and not to the two
4444            // it came from.
4445            let geo = match self.runtime.anim_offset(id) {
4446                Some((x, y)) => geo.with_fractions(x, y),
4447                None => geo,
4448            };
4449            // Resolves the alignment (physical or directional) against the reading direction;
4450            // `resolve` produces a physical `Alignment` that the rest (with its RTL correction)
4451            // handles uniformly.
4452            let direction = if self.rtl() {
4453                frus_core::TextDirection::Rtl
4454            } else {
4455                frus_core::TextDirection::Ltr
4456            };
4457            let align = geo.resolve(direction);
4458            let child = rects[child_index];
4459            let pad = effective_style(widget, id, self.runtime, &self.theme).padding;
4460            let free_w = (container.width - pad.left - pad.right - child.width).max(0.0);
4461            let free_h = (container.height - pad.top - pad.bottom - child.height).max(0.0);
4462            // In RTL, taffy lays the child out on the left and `mirror` has sent it back to
4463            // the right: the baseline is therefore already right-aligned. 1 is subtracted so
4464            // the fraction stays **physical** (x = +1 ⇒ the right in both directions).
4465            let fx = align.fraction_x() - if self.rtl() { 1.0 } else { 0.0 };
4466            off.0 += free_w * fx;
4467            off.1 += free_h * align.fraction_y();
4468        }
4469
4470        // Paint offset (`Transform::translate`): in RTL the world's x axis is mirrored, so a
4471        // logical +x offset ("towards the end") points left — the sign is inverted to stay
4472        // consistent with the reading direction.
4473        if let Some((tx, ty)) = widget.transform_translate() {
4474            off.0 += if self.rtl() { -tx } else { tx };
4475            off.1 += ty;
4476        }
4477
4478        // The same offset, given as a **fraction of the child's own box**
4479        // (`FractionalTranslation`). The child's rectangle is the one thing this function
4480        // has that the widget did not: a widget cannot multiply by a width it was never
4481        // told.
4482        if let Some((fx, fy)) = widget.translate_fraction() {
4483            // The same tween, on the other offset rule: a slide that animates reads the
4484            // runtime's pair, and one that does not reads its own.
4485            let (fx, fy) = self.runtime.anim_offset(id).unwrap_or((fx, fy));
4486            let child = rects.get(child_index).copied().unwrap_or(container);
4487            let tx = child.width * fx;
4488            off.0 += if self.rtl() { -tx } else { tx };
4489            off.1 += child.height * fy;
4490        }
4491
4492        off
4493    }
4494
4495    /// Records the widget in the inspector's collection (when it is on) and opens a depth
4496    /// level — every render path closes it again at the end of the function
4497    /// (`self.depth -= 1`).
4498    fn inspect_enter(&mut self, widget: &dyn Widget<Msg>, id: WidgetId, draw_rect: Rect) {
4499        if let Some(nodes) = &mut self.inspector {
4500            nodes.push(crate::inspector::InspectorNode {
4501                id,
4502                rect: draw_rect,
4503                name: widget.debug_name(),
4504                depth: self.depth,
4505            });
4506        }
4507        self.depth += 1;
4508    }
4509
4510    /// A widget's full status: pointer interaction + focus + animation progresses + the
4511    /// cursor/selection when there is one.
4512    fn full_status(&self, widget: &dyn Widget<Msg>, id: WidgetId) -> crate::interaction::Status {
4513        let mut status = self.runtime.input.status_for(id);
4514        status.hover_progress = self.runtime.hover_progress(id);
4515        status.focus_progress = self.runtime.focus_progress(id);
4516        // A press is a progression now, and where the runtime has never heard of this
4517        // widget the flag is adopted whole — the rule `value` follows just below. Without
4518        // it a frame built outside the loop would draw a held widget as untouched.
4519        status.press_progress = self.runtime.press_progress_or(
4520            id,
4521            if status.interaction == crate::interaction::Interaction::Pressed {
4522                1.0
4523            } else {
4524                0.0
4525            },
4526        );
4527        status.opacity = self.runtime.opacity(id);
4528        // A widget's own animated value, or **its target** where the runtime has never
4529        // heard of it: the same rule the runtime applies on mount (adopt, do not animate
4530        // in from zero). Without the fallback an isolated render — a test, a frame built
4531        // before the loop has advanced anything — draws every such widget at zero: a
4532        // switch that is on drawn off, an indicator under the wrong tab.
4533        status.value = match widget.anim_target() {
4534            Some(target) => self.runtime.value_or(id, target),
4535            None => self.runtime.value(id),
4536        };
4537        status.anim_color = self.runtime.anim_color(id);
4538        status.anim_radius = self.runtime.anim_radius(id);
4539        status.time = self.runtime.time;
4540        status.drag_over = self.runtime.drag_over == Some(id);
4541        status.scroll_y = self.runtime.scroll.get(&id).map(|s| s.1).unwrap_or(0.0);
4542        if status.focused {
4543            if let Some(edit) = self.runtime.edits.get(&id) {
4544                status.cursor = Some(edit.cursor);
4545                status.selection = edit.selection_range();
4546                status.composing = edit.composing;
4547            }
4548            status.handles = self.runtime.selection_handles == Some(id);
4549            status.caret_hidden = self.runtime.caret_hidden;
4550        }
4551        status
4552    }
4553
4554    /// The generic focus ring (for widgets that do not draw their own).
4555    fn draw_focus_ring(
4556        &mut self,
4557        draw_rect: Rect,
4558        status: &crate::interaction::Status,
4559        widget: &dyn Widget<Msg>,
4560    ) {
4561        // The generic ring only appears when the last interaction was a **keyboard** one
4562        // (`focus_visible`) — a click must not flash a ring.
4563        if status.focused
4564            && self.runtime.focus_visible
4565            && widget.focusable()
4566            && !widget.draws_own_focus()
4567        {
4568            let ring = Rect::new(
4569                draw_rect.x - 2.0,
4570                draw_rect.y - 2.0,
4571                draw_rect.width + 4.0,
4572                draw_rect.height + 4.0,
4573            );
4574            let alpha = 0.4 + 0.6 * status.focus_progress.clamp(0.0, 1.0);
4575            self.scene.draw_rect(
4576                ring,
4577                Color::TRANSPARENT,
4578                self.theme.radius + 2.0,
4579                2.0,
4580                self.theme.focus.fade(alpha),
4581            );
4582        }
4583    }
4584
4585    /// Renders a **virtualised list item**: built on the fly, it cannot defer an overlay
4586    /// (hence a render of its own, without the special branches).
4587    fn render_item(
4588        &mut self,
4589        widget: &dyn Widget<Msg>,
4590        id: WidgetId,
4591        translation: (f32, f32),
4592        clip: Rect,
4593        rects: &[Rect],
4594        index: &mut usize,
4595    ) {
4596        let rect = rects[*index];
4597        *index += 1;
4598        let draw_rect = rect.translate(translation.0, translation.1);
4599        self.inspect_enter(widget, id, draw_rect);
4600
4601        let status = self.full_status(widget, id);
4602        if widget.continuous() {
4603            self.wants_animation = true;
4604        }
4605        self.scene.set_clip(clip);
4606        self.scene.set_owner(id.as_u64());
4607        // The box this widget was given. Text primitives record it: a line of text
4608        // says only where it starts, and the renderer has to know what it covers to
4609        // order it against anything else (milestone 295).
4610        self.scene.set_bounds(draw_rect);
4611        widget.paint(draw_rect, status, &self.theme, &mut self.scene);
4612        self.scene.set_clip(clip);
4613        self.draw_focus_ring(draw_rect, &status, widget);
4614
4615        let visible = draw_rect.intersect(clip);
4616        // A focus stop clipped entirely away by a **scroll** is still a focus stop.
4617        // Tab has to reach the field below the fold — the shell brings it into view
4618        // when it lands there — and registering only what the eye can see is how a long
4619        // form came to have half its fields unreachable from the keyboard. Clipped away
4620        // by anything else it is gone: nothing could reveal it, so focus would land
4621        // where the eye cannot follow.
4622        //
4623        // Only the focus registry does this. A click still needs the visible box, or a
4624        // tap on empty space would land on something scrolled out of sight.
4625        if widget.focusable()
4626            && !self.focus_excluded
4627            && (self.scroll_host.is_some() || (visible.width > 0.0 && visible.height > 0.0))
4628        {
4629            self.focusables.push(Focusable {
4630                id,
4631                rect: visible,
4632                bounds: draw_rect,
4633                form: self.autofill_group,
4634                scroll: self.scroll_host,
4635                skip: self.focus_skipped || widget.focus_skip_traversal(),
4636                order: widget.focus_order().or(self.focus_order),
4637                group: self.focus_group,
4638            });
4639        }
4640        if visible.width > 0.0 && visible.height > 0.0 {
4641            // A press that lands on a **surface** and on nothing inside it stops there.
4642            // Registered before the children, so anything inside still wins on the way
4643            // back out — see [`Widget::opaque`].
4644            if widget.opaque() {
4645                self.hits.push(Hit {
4646                    id,
4647                    rect: visible,
4648                    msg: None,
4649                    xform: None,
4650                });
4651            }
4652            if let Some(msg) = widget.on_click() {
4653                self.hits.push(Hit {
4654                    id,
4655                    rect: visible,
4656                    msg: Some(msg),
4657                    xform: None,
4658                });
4659            }
4660            if let Some(msg) = widget.on_long_press() {
4661                self.long_presses.push(Hit {
4662                    id,
4663                    rect: visible,
4664                    msg: Some(msg),
4665                    xform: None,
4666                });
4667            }
4668            if widget.draggable() {
4669                self.draggables.push((id, visible));
4670            }
4671            if widget.reorder_index().is_some() {
4672                self.reorderables.push((id, visible));
4673            }
4674            // The accessibility tree: nodes that carry meaning (a role or a label).
4675            if let Some(sem) = widget.semantics().filter(|s| s.is_meaningful()) {
4676                self.semantics.push((id, visible, sem));
4677            }
4678        }
4679
4680        let children = widget.children();
4681        // Fractional alignment + paint offset, as in the main walk (a virtualised-list /
4682        // `layout_builder` child may itself be an aligned or transformed container).
4683        let extra = self.child_offset(widget, id, rect, rects, *index, children);
4684        for (child_index, child) in children.iter().enumerate() {
4685            self.render_item(
4686                child.as_ref(),
4687                child_id(id, child_index, child.as_ref()),
4688                (translation.0 + extra.0, translation.1 + extra.1),
4689                clip,
4690                rects,
4691                index,
4692            );
4693        }
4694        self.depth -= 1;
4695    }
4696
4697    /// Lays out a full-window screen and renders it offset by `off_x`.
4698    fn render_screen(
4699        &mut self,
4700        screen: &'a dyn Widget<Msg>,
4701        id: WidgetId,
4702        bounds: Rect,
4703        off_x: f32,
4704        clip: Rect,
4705    ) {
4706        let rects = self.cached_rects(
4707            id,
4708            screen,
4709            Constraints::definite(Size::new(bounds.width, bounds.height)),
4710        );
4711        let screen_clip = clip.intersect(bounds);
4712        let mut index = 0;
4713        self.walk(
4714            screen,
4715            id,
4716            (bounds.x + off_x, bounds.y),
4717            screen_clip,
4718            &rects,
4719            &mut index,
4720        );
4721    }
4722
4723    /// Processes the deferred overlays: sub-layout, positioning and rendering **above**
4724    /// everything (their clickable areas win). May spawn further overlays (nested portals).
4725    fn process_overlays(&mut self) {
4726        let window = Rect::new(0.0, 0.0, self.available.width, self.available.height);
4727        while let Some((content, oid, anchor, placement, dismiss, progress, traps, scrim, theme)) =
4728            self.overlays.pop()
4729        {
4730            // The theme this overlay was declared under, for the whole of its layout and
4731            // painting; whatever was in force when the walk ended is put back after it,
4732            // so two overlays declared under two themes do not bleed into each other.
4733            let outer = std::mem::replace(&mut self.theme, theme);
4734            // Drawers slide along a **spring curve** (a gentle arrival), not linearly; the
4735            // other overlays keep their raw progress.
4736            let progress = if matches!(
4737                placement,
4738                Placement::Left | Placement::Right | Placement::Bottom
4739            ) {
4740                crate::runtime::spring_ease(progress)
4741            } else {
4742                progress
4743            };
4744            // The content's natural size. A drawer (`Left`) is constrained in height to the
4745            // window (so its `Percent(1.0)` panel unfolds) with a free width; the other
4746            // overlays take their natural size.
4747            let (free_x, free_y) = match placement {
4748                Placement::Left | Placement::Right => (true, false),
4749                // The sheet is full-width (constrained to the window) with its natural height:
4750                // its `Percent(1.0)` panel unfolds across the width.
4751                Placement::Bottom => (false, true),
4752                _ => (true, true),
4753            };
4754            let mut rects = self.cached_rects(
4755                oid,
4756                content,
4757                Constraints::scroll(self.available.width, self.available.height, free_x, free_y),
4758            );
4759            let mut size = rects
4760                .first()
4761                .copied()
4762                .unwrap_or(Rect::new(0.0, 0.0, 0.0, 0.0));
4763            // **Nothing wider than the window.** A free axis asks the content how big it
4764            // would like to be, and a dialog answers with the width of its widest line —
4765            // which, at a reader's font size, is wider than the screen. Centred, that
4766            // spills off *both* edges, and a button drawn at a negative x is a button
4767            // nobody can press. So the answer is taken as a wish rather than a size: past
4768            // the room there is, the content is laid out again at that room, and whatever
4769            // can fold — a paragraph, a bar of actions — folds.
4770            //
4771            // The room is the window less the content's **own margin**, which is how an
4772            // overlay says how far off the edges it wants to be held: a dialog's inset
4773            // padding is exactly that, and a clamp that ignored it would push the surface
4774            // flat against both sides of the screen.
4775            //
4776            // The height is deliberately left alone. A dialog too tall for the screen
4777            // wants its content to scroll, which is a question of its own; squashing it
4778            // here would only move the spill inside the surface.
4779            let inset = content.style_themed(&self.theme).margin;
4780            let room = (self.available.width - inset.left - inset.right).max(0.0);
4781            if free_x && size.width > room {
4782                rects = self.cached_rects(
4783                    oid,
4784                    content,
4785                    Constraints::scroll(room, self.available.height, false, free_y),
4786                );
4787                size = rects
4788                    .first()
4789                    .copied()
4790                    .unwrap_or(Rect::new(0.0, 0.0, 0.0, 0.0));
4791            }
4792
4793            // A drawer's slide-in from the left / right edge.
4794            let from_left = -(1.0 - progress) * size.width;
4795            let from_right = self.available.width - progress * size.width;
4796            // An anchored menu/tooltip: aligned to the anchor's **start** edge — in RTL that
4797            // edge is the right one (the menu opens leftwards).
4798            let anchor_x = if self.rtl() {
4799                anchor.x + anchor.width - size.width
4800            } else {
4801                anchor.x
4802            };
4803            let mut pos = match placement {
4804                Placement::Below => (anchor_x, anchor.y + anchor.height + 4.0),
4805                Placement::Center => (
4806                    (self.available.width - size.width) * 0.5,
4807                    (self.available.height - size.height) * 0.5,
4808                ),
4809                Placement::Tooltip => (anchor_x, anchor.y - size.height - 6.0),
4810                // `Left` = a drawer on the **start** side; in RTL, start = the right.
4811                Placement::Left if self.rtl() => (from_right, 0.0),
4812                Placement::Left => (from_left, 0.0),
4813                // `Right` = the **end** side; in RTL, end = the left.
4814                Placement::Right if self.rtl() => (from_left, 0.0),
4815                Placement::Right => (from_right, 0.0),
4816                // The sheet slides up from the bottom: its bottom edge stays flush with the
4817                // window, offset downwards by `(1-progress)·height`.
4818                Placement::Bottom => (0.0, self.available.height - progress * size.height),
4819            };
4820
4821            // An **anchored** overlay follows a widget; the others (a modal, a drawer, a
4822            // sheet) are positioned against the window and have no anchor worth the name.
4823            // Only the first kind can find its anchor gone from the window: a screen
4824            // sliding out under a `Navigator`, a row scrolled sideways past the edge.
4825            let anchored = matches!(placement, Placement::Below | Placement::Tooltip);
4826            let anchor_on_screen = !anchored
4827                || (anchor.x < self.available.width
4828                    && anchor.x + anchor.width > 0.0
4829                    && anchor.y < self.available.height
4830                    && anchor.y + anchor.height > 0.0);
4831
4832            // Auto-flip: when an anchored overlay overflows an edge, it is flipped / nudged
4833            // back inside the window.
4834            //
4835            // This is for a menu opened near the right margin, and it assumes the anchor is
4836            // something the window is showing. When the anchor has **left** the window the
4837            // nudge does the opposite of its job: it drags a departing screen's menu back
4838            // into view, fully opaque, over the screen that replaced it. An overlay whose
4839            // anchor is off screen goes off screen with it.
4840            if anchored && anchor_on_screen {
4841                // A vertical overflow → flip to the other side of the anchor.
4842                if placement == Placement::Below
4843                    && pos.1 + size.height > self.available.height
4844                    && anchor.y - size.height - 4.0 >= 0.0
4845                {
4846                    pos.1 = anchor.y - size.height - 4.0;
4847                } else if placement == Placement::Tooltip
4848                    && pos.1 < 0.0
4849                    && anchor.y + anchor.height + size.height + 6.0 <= self.available.height
4850                {
4851                    pos.1 = anchor.y + anchor.height + 6.0;
4852                }
4853                // A horizontal overflow → nudge back inside the window.
4854                if pos.0 + size.width > self.available.width {
4855                    pos.0 = (self.available.width - size.width).max(0.0);
4856                }
4857                if pos.0 < 0.0 {
4858                    pos.0 = 0.0;
4859                }
4860            }
4861
4862            if matches!(
4863                placement,
4864                Placement::Center | Placement::Left | Placement::Right | Placement::Bottom
4865            ) {
4866                // The scrim behind the modal / the drawer, modulated by the progress (a fade
4867                // synchronised with the slide). The colour is the overlay's own when it asked
4868                // for one — alpha included, since a scrim's transparency *is* the colour —
4869                // and the `scrim` role at half opacity otherwise.
4870                let base = scrim.unwrap_or_else(|| self.theme.scheme.scrim.with_alpha(0.5));
4871                self.scene.set_owner(0);
4872                self.scene.set_clip(window);
4873                self.scene
4874                    .fill_rect(window, base.with_alpha(base.a * progress));
4875            }
4876
4877            // Dismissal on a click **outside** the content (a modal, a menu…): a full-screen
4878            // hit added **before** the content, so the content beats it where they overlap.
4879            // The dismissal is also remembered for Escape (the last one rendered = the
4880            // topmost).
4881            //
4882            // Gated on the same condition as the nudge, and for the same reason: a
4883            // window-wide barrier belonging to an overlay nobody can see would swallow the
4884            // next press anywhere on the screen that replaced it.
4885            if let Some(msg) = dismiss.filter(|_| anchor_on_screen) {
4886                self.dismisses.push(msg.clone());
4887                self.hits.push(Hit {
4888                    id: oid.barrier(),
4889                    rect: window,
4890                    msg: Some(msg),
4891                    xform: None,
4892                });
4893            }
4894
4895            // A **modal** (scrimmed) or **trapping anchored** (menu) overlay: its focusables
4896            // form a **scope** that traps Tab/arrows/click-to-focus. The last one rendered (the
4897            // topmost) wins; anchored overlays that do not trap (a tooltip, an autocomplete
4898            // list) leave the focus alone.
4899            let modal = matches!(
4900                placement,
4901                Placement::Center | Placement::Left | Placement::Right | Placement::Bottom
4902            );
4903            if modal || traps {
4904                self.focus_scope_start = Some(self.focusables.len());
4905            }
4906
4907            let mut index = 0;
4908            self.walk(content, oid, pos, window, &rects, &mut index);
4909            self.theme = outer;
4910        }
4911    }
4912}
4913
4914impl<Msg: Clone> Builder<'_, Msg> {
4915    /// Draws a scrollbar (track + thumb) and registers it for hit-testing a drag.
4916    /// Paints the region's overscroll glows over its viewport.
4917    ///
4918    /// The colour is the scheme's **secondary**: the edge feedback is an
4919    /// acknowledgement, not a call to action, so it must read as part of the surface
4920    /// rather than compete with whatever primary-coloured control sits nearby.
4921    fn add_overscroll_glow(&mut self, id: WidgetId, viewport: Rect) {
4922        if let Some(glows) = self.runtime.scroll_glow.get(&id) {
4923            glows.paint(viewport, self.theme.scheme.secondary, &mut self.scene);
4924        }
4925    }
4926
4927    fn add_scrollbar(&mut self, id: WidgetId, viewport: Rect, axis: Bar) {
4928        let Bar {
4929            vertical,
4930            offset,
4931            max,
4932            reverse,
4933            asked,
4934            visible,
4935        } = axis;
4936        // **Along the horizontal axis, never** (`app.dart:861`) — on any platform, under
4937        // any setting. A sideways strip is scrolled by the thing that made it sideways,
4938        // and a bar under it takes room from content that is short of it already.
4939        if !vertical {
4940            return;
4941        }
4942        // And down the vertical one, only where the platform's own scroll views have a
4943        // bar (`app.dart:865` against `:870`). A finger already knows where it is on the
4944        // page; a permanent bar over the content is an affordance for a pointer that
4945        // cannot feel the edges. This drew one everywhere, on both axes, always.
4946        if asked.unwrap_or(self.runtime.scrollbars) == crate::physics::Scrollbars::Never {
4947            return;
4948        }
4949        let (track_start, track_len, content_len) = if vertical {
4950            (viewport.y, viewport.height, viewport.height + max)
4951        } else {
4952            (viewport.x, viewport.width, viewport.width + max)
4953        };
4954        // Every metric below is `theme ?? framework`. There is no per-call override for a
4955        // scrollbar — it is drawn by the walk, not by a widget a caller configured — so
4956        // the chain is two terms here rather than three.
4957        let bar = &self.theme.widgets.scrollbar;
4958        let bar_size = bar.thickness.unwrap_or(BAR_SIZE);
4959        let bar_margin = bar.margin.unwrap_or(BAR_MARGIN);
4960        let thumb_len = (track_len * track_len / content_len)
4961            .max(bar.min_thumb_length.unwrap_or(MIN_THUMB))
4962            .min(track_len);
4963        let travel = track_len - thumb_len;
4964        // Along a reversed axis the numbers run the other way, so the thumb does too:
4965        // offset 0 is the far end, and that is where it rests.
4966        let fraction = if max > 0.0 { offset / max } else { 0.0 };
4967        let fraction = if reverse { 1.0 - fraction } else { fraction };
4968        let thumb_pos = track_start + fraction * travel;
4969
4970        // The track's rectangle is still worked out — it is the room a pointer has to be
4971        // in, and the geometry the thumb sits in — but nothing is painted for it.
4972        //
4973        // `BAR_SIZE` is the **thumb's** thickness, not a slot's: the reference keeps the
4974        // thumb clear of the edge with `crossAxisMargin` (`scrollbar.dart:357`) rather
4975        // than shrinking it to fit. This drew a 6-pixel thumb inside an 8-pixel slot,
4976        // which is the same arithmetic with the wrong number left over.
4977        let (track, thumb) = if vertical {
4978            let x = viewport.x + viewport.width - bar_margin - bar_size;
4979            (
4980                Rect::new(
4981                    x - bar_margin,
4982                    viewport.y,
4983                    bar_size + bar_margin * 2.0,
4984                    viewport.height,
4985                ),
4986                Rect::new(x, thumb_pos, bar_size, thumb_len),
4987            )
4988        } else {
4989            let y = viewport.y + viewport.height - bar_margin - bar_size;
4990            (
4991                Rect::new(
4992                    viewport.x,
4993                    y - bar_margin,
4994                    viewport.width,
4995                    bar_size + bar_margin * 2.0,
4996                ),
4997                Rect::new(thumb_pos, y, thumb_len, bar_size),
4998            )
4999        };
5000        // What a mouse has to be inside for the bar to notice it: the track, widened to
5001        // take in a tap target's worth of room around the thumb (`scrollbar.dart:762`).
5002        let reach = {
5003            let (cx, cy) = (thumb.x + thumb.width * 0.5, thumb.y + thumb.height * 0.5);
5004            let r = crate::theme::MIN_TAP_TARGET * 0.5;
5005            let x0 = track.x.min(cx - r);
5006            let y0 = track.y.min(cy - r);
5007            let x1 = (track.x + track.width).max(cx + r);
5008            let y1 = (track.y + track.height).max(cy + r);
5009            Rect::new(x0, y0, x1 - x0, y1 - y0)
5010        };
5011
5012        // **How present the bar is**, this frame. It arrives when the area moves and goes
5013        // again once the area has been still long enough; an area nobody has scrolled has
5014        // none at all. Unless it was asked to stay (`scrollbar.dart:214`).
5015        let fade = self.runtime.scrollbar_fade_of(id);
5016        let opacity = if visible.unwrap_or(false) {
5017            1.0
5018        } else {
5019            fade.opacity.clamp(0.0, 1.0)
5020        };
5021
5022        // **No track.** The reference's is transparent unless a caller asks for one
5023        // (`scrollbar.dart:281`), and even then it is 3 to 5 %.
5024        //
5025        // The thumb takes the reference's opacity for the surface's brightness: at rest
5026        // (`scrollbar.dart:242`, `:248`), warmed towards its hovered value while a
5027        // pointer is near it (`:239`, `:245`), and at its dragged value the moment the
5028        // thumb is held (`:238`, `:244`) — that one does not fade in, because the hand is
5029        // already on it and a fade would only lag behind the grab.
5030        // The scheme's own answer, which is what the reference reads here
5031        // (`scrollbar.dart:232`). This used to be `surface.compute_luminance() < 0.5`:
5032        // right on both built-in schemes and a guess on any other.
5033        let dark = self.theme.brightness().is_dark();
5034        let (rest, warm, grabbed) = if dark {
5035            (THUMB_ON_DARK, THUMB_HOVER_ON_DARK, THUMB_DRAG_ON_DARK)
5036        } else {
5037            (THUMB_ON_LIGHT, THUMB_HOVER_ON_LIGHT, THUMB_DRAG_ON_LIGHT)
5038        };
5039        // A theme's levels replace the scheme's, one at a time: naming the resting
5040        // opacity says nothing about the held one.
5041        let rest = bar.opacity.unwrap_or(rest);
5042        let warm = bar.hover_opacity.unwrap_or(warm);
5043        let grabbed = bar.drag_opacity.unwrap_or(grabbed);
5044        let level = if self.runtime.scrollbar_dragged == Some(id) {
5045            grabbed
5046        } else {
5047            rest + (warm - rest) * fade.hover.clamp(0.0, 1.0)
5048        };
5049        if opacity > 0.0 {
5050            let base = bar.thumb_color.unwrap_or(self.theme.scheme.on_surface);
5051            self.scene.draw_rect(
5052                thumb,
5053                base.fade(level * opacity),
5054                bar.radius.unwrap_or(bar_size * 0.5),
5055                0.0,
5056                Color::TRANSPARENT,
5057            );
5058        }
5059        // Registered even when nothing was painted for it — unlike a bar the platform
5060        // does not draw at all, which is not registered because it is not there. A mouse
5061        // coming near a faded bar is what brings it back (`scrollbar.dart:2132`), so
5062        // something has to be here for the mouse to find. What a bar at zero cannot be is
5063        // **grabbed**; see `Ui::scrollbar_at`.
5064        self.scrollbars.push(Scrollbar {
5065            id,
5066            vertical,
5067            thumb,
5068            track_start,
5069            track_len,
5070            thumb_len,
5071            reverse,
5072            max,
5073            opacity,
5074            reach,
5075        });
5076    }
5077}
5078
5079/// Turns a widget tree into a [`Ui`] for a given size, runtime state and theme.
5080pub fn build_ui<'a, Msg: Clone + 'static>(
5081    root: &'a dyn Widget<Msg>,
5082    available: Size,
5083    runtime: &'a Runtime,
5084    theme: &'a Theme,
5085) -> Ui<Msg> {
5086    build_ui_impl(root, available, runtime, theme, false).0
5087}
5088
5089/// Like [`build_ui`], but it also collects the **inspection nodes** (one per painted widget:
5090/// name, box, depth) — the raw material of the runtime inspector and of the diagnostic dump.
5091/// Reserve it for the frames where the inspector is on.
5092pub fn build_ui_inspected<'a, Msg: Clone + 'static>(
5093    root: &'a dyn Widget<Msg>,
5094    available: Size,
5095    runtime: &'a Runtime,
5096    theme: &'a Theme,
5097) -> (Ui<Msg>, Vec<crate::inspector::InspectorNode>) {
5098    let (ui, nodes) = build_ui_impl(root, available, runtime, theme, true);
5099    (ui, nodes.unwrap_or_default())
5100}
5101
5102/// Panics, **in debug builds only**, when the reader's font size is installed and the
5103/// surface it belongs to is not.
5104///
5105/// The two travel together — [`MediaQuery::install`] sets both — so a scale away from 1
5106/// with no description means somebody installed half a surface, and the half they left out
5107/// is about to be missing for the layout that follows. That is not a theory: for four
5108/// milestones the shell scoped only the build, the scale was 1 while text was measured and
5109/// painted, and the reader's setting reached a device without moving a pixel. Nothing said
5110/// a word — not the 91 goldens, not clippy, not the strict rustdoc.
5111///
5112/// Debug only, because it is a wiring mistake rather than a state a running application can
5113/// reach: it should stop a test, not a user's frame. A scale of exactly 1 is the neutral
5114/// value and says nothing either way, so it is never a failure.
5115#[cfg(debug_assertions)]
5116fn assert_surface_is_whole() {
5117    let scale = frus_core::text_scale();
5118    if (scale - 1.0).abs() > f32::EPSILON && !crate::MediaQuery::of().is_described() {
5119        panic!(
5120            "a text scale of {scale} is installed with no surface described: something \
5121             installed half a surface. Use `MediaQuery::install` (or `scope`), which sets \
5122             the description and the reader's font size together \u{2014} see milestone 408."
5123        );
5124    }
5125}
5126
5127fn build_ui_impl<'a, Msg: Clone + 'static>(
5128    root: &'a dyn Widget<Msg>,
5129    available: Size,
5130    runtime: &'a Runtime,
5131    theme: &'a Theme,
5132    inspect: bool,
5133) -> (Ui<Msg>, Option<Vec<crate::inspector::InspectorNode>>) {
5134    #[cfg(debug_assertions)]
5135    assert_surface_is_whole();
5136
5137    let (mut rects, overflows) = runtime.layout_cache.borrow_mut().rects(
5138        WidgetId::ROOT,
5139        root,
5140        runtime,
5141        theme,
5142        Constraints::definite(available),
5143    );
5144
5145    let mut builder = Builder {
5146        scene: Scene::new(),
5147        hits: Vec::new(),
5148        long_presses: Vec::new(),
5149        dismisses: Vec::new(),
5150        focusables: Vec::new(),
5151        focus_scope_start: None,
5152        scrollables: Vec::new(),
5153        scrollbars: Vec::new(),
5154        draggables: Vec::new(),
5155        drag_sources: Vec::new(),
5156        drop_zones: Vec::new(),
5157        inks: Vec::new(),
5158        reorderables: Vec::new(),
5159        interactives: Vec::new(),
5160        overlays: Vec::new(),
5161        wants_animation: false,
5162        available,
5163        runtime,
5164        theme: std::rc::Rc::new(theme.clone()),
5165        inspector: inspect.then(Vec::new),
5166        depth: 0,
5167        refresh_host: None,
5168        scroll_host: None,
5169        hero_screen: None,
5170        heroes: Vec::new(),
5171        refreshes: Vec::new(),
5172        dismissables: Vec::new(),
5173        sheets: Vec::new(),
5174        semantics: Vec::new(),
5175        system_ui: Vec::new(),
5176        focus_excluded: false,
5177        backdrop_group: None,
5178        focus_skipped: false,
5179        focus_order: None,
5180        focus_group: None,
5181        autofill_group: None,
5182        scopes: Vec::new(),
5183        listeners: Vec::new(),
5184        overflows: std::cell::RefCell::new(Vec::new()),
5185        pending_overflows: std::cell::RefCell::new(std::collections::HashMap::new()),
5186    };
5187    // The root is mirrored in RTL (like every layout root).
5188    builder.mirror(&mut rects);
5189    builder.record_overflows(WidgetId::ROOT, &rects, overflows);
5190    let mut index = 0;
5191    builder.walk(
5192        root,
5193        WidgetId::ROOT,
5194        (0.0, 0.0),
5195        Rect::UNBOUNDED,
5196        &rects,
5197        &mut index,
5198    );
5199
5200    // Overlays (floating menus, modals, tooltips) above everything else. (Their walk restarts
5201    // from depth 0: roots as far as the inspector is concerned.)
5202    builder.process_overlays();
5203
5204    // End of frame: forget the layout roots and repaint boundaries of widgets that have gone,
5205    // and freeze the caches' diagnostic counters.
5206    runtime.layout_cache.borrow_mut().end_frame();
5207    runtime.paint_cache.borrow_mut().end_frame();
5208
5209    // Replays the outgoing subtrees, fading out, over the current scene.
5210    builder.scene.set_clip(Rect::UNBOUNDED);
5211    for (primitives, opacity) in runtime.leaving.values() {
5212        for primitive in primitives {
5213            builder.scene.push_faded(primitive, *opacity);
5214        }
5215    }
5216
5217    // A box whose children ran past it now **says so on the screen**, not only in the
5218    // console — see `paint_overflow_bands`. Last, so nothing paints over it.
5219    paint_overflow_bands(&mut builder.scene, &builder.overflows.borrow());
5220
5221    let ui = Ui {
5222        scene: builder.scene,
5223        hits: builder.hits,
5224        long_presses: builder.long_presses,
5225        dismisses: builder.dismisses,
5226        focusables: builder.focusables,
5227        focus_scope_start: builder.focus_scope_start,
5228        scrollables: builder.scrollables,
5229        scrollbars: builder.scrollbars,
5230        draggables: builder.draggables,
5231        drag_sources: builder.drag_sources,
5232        drop_zones: builder.drop_zones,
5233        inks: builder.inks,
5234        reorderables: builder.reorderables,
5235        interactives: builder.interactives,
5236        refreshes: builder.refreshes,
5237        dismissables: builder.dismissables,
5238        sheets: builder.sheets,
5239        wants_animation: builder.wants_animation,
5240        semantics: builder.semantics,
5241        system_ui: builder.system_ui,
5242        overflows: builder.overflows.into_inner(),
5243        scopes: builder.scopes,
5244        listeners: builder.listeners,
5245    };
5246    (ui, builder.inspector)
5247}
5248
5249/// Collects the identities of every widget in the tree (prefix order), following the same
5250/// positional scheme as [`build_ui`]. Used to detect mounts/unmounts between two frames.
5251pub fn collect_ids<Msg>(root: &dyn Widget<Msg>) -> Vec<WidgetId> {
5252    fn walk<Msg>(widget: &dyn Widget<Msg>, id: WidgetId, out: &mut Vec<WidgetId>) {
5253        out.push(id);
5254        for (index, child) in widget.children().iter().enumerate() {
5255            walk(child.as_ref(), child_id(id, index, child.as_ref()), out);
5256        }
5257    }
5258    let mut out = Vec::new();
5259    walk(root, WidgetId::ROOT, &mut out);
5260    out
5261}
5262
5263/// Identities of the **subtree** rooted at `root_id` — `widget` being the widget with that
5264/// identity — that is `[root_id, …descendants]`, derived by the same positional scheme as
5265/// [`collect_ids`].
5266///
5267/// Used by the **drag ghost**: a **rich** card's primitives are painted by its children (owners
5268/// other than the card itself). To capture all of its visuals, the shell gathers the primitives
5269/// of the **whole** subtree, not only the card's own.
5270pub fn subtree_ids<Msg>(widget: &dyn Widget<Msg>, root_id: WidgetId) -> Vec<WidgetId> {
5271    fn walk<Msg>(widget: &dyn Widget<Msg>, id: WidgetId, out: &mut Vec<WidgetId>) {
5272        out.push(id);
5273        for (index, child) in widget.children().iter().enumerate() {
5274            walk(child.as_ref(), child_id(id, index, child.as_ref()), out);
5275        }
5276    }
5277    let mut out = Vec::new();
5278    walk(widget, root_id, &mut out);
5279    out
5280}
5281
5282/// The owners of everything a carried row, card or item may be **moved past** this frame:
5283/// every reorderable that can be dropped on, and all it paints.
5284///
5285/// What a reorder preview slides out of the way is decided by geometry — the band the
5286/// source sits in — and geometry alone moves whatever shares that band: a button floating
5287/// over a list, the navigation bar under it. This is the other half of the question, *is
5288/// it part of something that can be reordered*, asked of the tree. A grip is inside its
5289/// row, so it moves with it without being a target itself.
5290pub fn reorderable_owners<Msg>(
5291    ui: &Ui<Msg>,
5292    root: &dyn Widget<Msg>,
5293) -> std::collections::HashSet<u64> {
5294    ui.reorderables
5295        .iter()
5296        .filter_map(|(id, _)| {
5297            find_widget(root, *id)
5298                .filter(|widget| widget.reorder_droppable())
5299                .map(|widget| subtree_ids(widget, *id))
5300        })
5301        .flatten()
5302        .map(|id| id.as_u64())
5303        .collect()
5304}
5305
5306/// The slots a reorderable `id` can be dropped among **in its own list**: its siblings — the
5307/// children of its parent — that can be dropped on, with their boxes this frame, in order.
5308///
5309/// A list's rows are one widget's children, and so are a board column's cards and the drop
5310/// zone that ends it. Nothing else on the page is, which is what lets a drop that landed on
5311/// no row at all still mean *somewhere in this list* rather than anywhere reorderable.
5312/// Siblings the frame registered no box for are left out.
5313pub fn reorder_siblings<Msg>(
5314    ui: &Ui<Msg>,
5315    root: &dyn Widget<Msg>,
5316    id: WidgetId,
5317) -> Vec<(WidgetId, Rect)> {
5318    fn parent_of<Msg>(
5319        widget: &dyn Widget<Msg>,
5320        own: WidgetId,
5321        target: WidgetId,
5322    ) -> Option<(&dyn Widget<Msg>, WidgetId)> {
5323        for (index, child) in widget.children().iter().enumerate() {
5324            let id = child_id(own, index, child.as_ref());
5325            if id == target {
5326                return Some((widget, own));
5327            }
5328            if let Some(found) = parent_of(child.as_ref(), id, target) {
5329                return Some(found);
5330            }
5331        }
5332        None
5333    }
5334    let Some((parent, parent_id)) = parent_of(root, WidgetId::ROOT, id) else {
5335        return Vec::new();
5336    };
5337    parent
5338        .children()
5339        .iter()
5340        .enumerate()
5341        .filter(|(_, child)| child.reorder_index().is_some() && child.reorder_droppable())
5342        .filter_map(|(index, child)| {
5343            let id = child_id(parent_id, index, child.as_ref());
5344            ui.reorderables
5345                .iter()
5346                .find(|(registered, _)| *registered == id)
5347                .map(|(_, rect)| (id, *rect))
5348        })
5349        .collect()
5350}
5351
5352/// Identity of the **first** widget declaring the key `key` (a hash), or `None`. It is how the
5353/// shell resolves a focus-by-key request (`Command::focus`): the application wraps a field in
5354/// `keyed(k, …)`, then focuses by `k`.
5355pub fn find_by_key<Msg>(root: &dyn Widget<Msg>, key: u64) -> Option<WidgetId> {
5356    fn walk<Msg>(widget: &dyn Widget<Msg>, id: WidgetId, key: u64) -> Option<WidgetId> {
5357        if widget.key() == Some(key) {
5358            return Some(id);
5359        }
5360        for (index, child) in widget.children().iter().enumerate() {
5361            if let Some(found) = walk(child.as_ref(), child_id(id, index, child.as_ref()), key) {
5362                return Some(found);
5363            }
5364        }
5365        None
5366    }
5367    walk(root, WidgetId::ROOT, key)
5368}
5369
5370/// Identity of the **sheet** the key `key` names: the first sheet's panel at or under the
5371/// widget declaring it, which is the identity the sheet's state is kept under and the one the
5372/// frame's sheets are reported by. `None` when nothing declares the key, or nothing under it
5373/// is a sheet.
5374///
5375/// A sheet's state belongs to its panel, which the application never builds itself, so a
5376/// request by key (`Command::sheet`) needs this one step more than a focus request does.
5377pub fn find_sheet_by_key<Msg>(root: &dyn Widget<Msg>, key: u64) -> Option<WidgetId> {
5378    fn first_sheet<Msg>(widget: &dyn Widget<Msg>, id: WidgetId) -> Option<WidgetId> {
5379        if widget.sheet().is_some() {
5380            return Some(id);
5381        }
5382        widget
5383            .children()
5384            .iter()
5385            .enumerate()
5386            .find_map(|(index, child)| {
5387                first_sheet(child.as_ref(), child_id(id, index, child.as_ref()))
5388            })
5389    }
5390    fn walk<Msg>(widget: &dyn Widget<Msg>, id: WidgetId, key: u64) -> Option<WidgetId> {
5391        if widget.key() == Some(key) {
5392            return first_sheet(widget, id);
5393        }
5394        widget
5395            .children()
5396            .iter()
5397            .enumerate()
5398            .find_map(|(index, child)| {
5399                walk(child.as_ref(), child_id(id, index, child.as_ref()), key)
5400            })
5401    }
5402    walk(root, WidgetId::ROOT, key)
5403}
5404
5405/// Path from the **root down to the widget** with identity `target` (`[root, …, target]`), for
5406/// the leaf→root bubbling of keys. Empty when it cannot be found.
5407pub fn find_path<Msg>(root: &dyn Widget<Msg>, target: WidgetId) -> Vec<&dyn Widget<Msg>> {
5408    fn walk<'a, Msg>(
5409        widget: &'a dyn Widget<Msg>,
5410        id: WidgetId,
5411        target: WidgetId,
5412        path: &mut Vec<&'a dyn Widget<Msg>>,
5413    ) -> bool {
5414        path.push(widget);
5415        if id == target {
5416            return true;
5417        }
5418        for (index, child) in widget.children().iter().enumerate() {
5419            if walk(
5420                child.as_ref(),
5421                child_id(id, index, child.as_ref()),
5422                target,
5423                path,
5424            ) {
5425                return true;
5426            }
5427        }
5428        path.pop();
5429        false
5430    }
5431    let mut path = Vec::new();
5432    if !walk(root, WidgetId::ROOT, target, &mut path) {
5433        path.clear();
5434    }
5435    path
5436}
5437
5438/// The theme a subtree inherits at this node — the widget's own override, with any
5439/// **animated text style** the runtime is holding for it laid on top.
5440///
5441/// The one place the theme swap happens, and it is one place on purpose. The swap is made
5442/// four times over a frame — by the layout pass, by the relayout fingerprint that has to
5443/// agree with it or the cache lies, by the paint walk, and by [`build_deferred`] before
5444/// any of them — and every animated value in this framework is read from the runtime by
5445/// whoever consumes it. This one's consumer is the swap itself, so a copy of the rule at
5446/// each of the four sites is four chances to drift. `Responsive` cost three silent bugs
5447/// that way (milestone 495); this is the same lesson written down as a function.
5448///
5449/// Cheap where nothing is animated: no `anim_text_style`, no clone, and the widget's own
5450/// `theme_override` is returned untouched — which is every node but the rare one.
5451pub(crate) fn scoped_theme<Msg>(
5452    widget: &dyn Widget<Msg>,
5453    id: WidgetId,
5454    runtime: &Runtime,
5455    inherited: &Theme,
5456) -> Option<Box<Theme>> {
5457    let own = widget.theme_override(inherited);
5458    let Some(target) = widget.anim_text_style() else {
5459        return own;
5460    };
5461    // Mid-flight if the runtime has a value for this node, and the target otherwise —
5462    // which is the first frame, before the family has been advanced over this tree.
5463    let style = runtime.anim_text_style(id).unwrap_or(target);
5464    let mut theme = own.map_or_else(|| inherited.clone(), |boxed| *boxed);
5465    // **Merged over**, not assigned: the node's own `theme_override` has already put the
5466    // target style and the four unanimated answers (alignment, wrapping, overflow, line
5467    // count) in place, and this replaces field by field exactly what is moving.
5468    theme.widgets.text.style = theme.widgets.text.style.merge(style);
5469    Some(Box::new(theme))
5470}
5471
5472/// Runs the **deferred builds** over a tree, the way the layout pass does on its way down.
5473///
5474/// A [`ThemeBuilder`](crate::ThemeBuilder) — and everything built on one, an
5475/// [`AppBar`](crate::AppBar) included — has no children at all until something calls
5476/// [`Widget::build_themed`] on it. The layout pass does that first thing, so within a frame
5477/// nothing ever meets an unbuilt subtree.
5478///
5479/// **A tree built outside a frame does.** The shell rebuilds from `view` alone on a burst of
5480/// keystrokes, faster than a frame, so the next key sees the current value rather than the
5481/// retained tree's — and then reads that tree straight away. Every traversal that arrives
5482/// there ([`find_widget`], focus resolution, a caret being revealed) walks a tree whose
5483/// deferred subtrees are empty, and silently finds nothing.
5484///
5485/// This is that half of the walk and nothing else: the theme swap
5486/// ([`Widget::theme_override`]) then the build, top-down. The swap is not optional — a
5487/// builder inside a [`Themed`](crate::Themed) has to see its own subtree's theme, and a
5488/// preparation that is *nearly* the walk is worse than none, because what it builds is wrong
5489/// rather than absent.
5490///
5491/// **Which is why it takes a runtime.** A subtree under an animated text style
5492/// (`AnimatedDefaultTextStyle`) inherits a theme that is moving, and a builder inside one
5493/// composes against whatever theme reaches it *first* — this walk. Preparing it against
5494/// the target instead of the value in flight is the same failure as skipping the swap: what
5495/// it builds is wrong rather than absent, and only for as long as the movement lasts, which
5496/// is the hardest kind to see. The runtime is one frame behind here, because this runs
5497/// before the frame advances its animations, and one frame behind is the whole tree agreeing
5498/// with itself rather than a builder disagreeing with the layout around it.
5499pub fn build_deferred<Msg>(root: &dyn Widget<Msg>, theme: &Theme, runtime: &Runtime) {
5500    fn walk<Msg>(widget: &dyn Widget<Msg>, id: WidgetId, runtime: &Runtime, theme: &Theme) {
5501        widget.expand(id, runtime, theme);
5502        let scoped = scoped_theme(widget, id, runtime, theme);
5503        let theme = scoped.as_deref().unwrap_or(theme);
5504        let _surface = widget
5505            .media_override(crate::MediaQuery::of())
5506            .map(crate::MediaQuery::install);
5507        let _shell = widget.scaffold_override().map(crate::ScaffoldInfo::install);
5508        widget.build_in(id, runtime, theme);
5509        for (index, child) in widget.children().iter().enumerate() {
5510            walk(
5511                child.as_ref(),
5512                child_id(id, index, child.as_ref()),
5513                runtime,
5514                theme,
5515            );
5516        }
5517    }
5518    walk(root, WidgetId::ROOT, runtime, theme);
5519}
5520
5521/// The widget of the tree under `root` that has the identity `target`, or `None` if none
5522/// does. Identities are the ones the walk that built the frame gave, so an id read from a
5523/// [`Ui`] finds its widget here.
5524pub fn find_widget<Msg>(root: &dyn Widget<Msg>, target: WidgetId) -> Option<&dyn Widget<Msg>> {
5525    fn walk<Msg>(
5526        widget: &dyn Widget<Msg>,
5527        id: WidgetId,
5528        target: WidgetId,
5529    ) -> Option<&dyn Widget<Msg>> {
5530        if id == target {
5531            return Some(widget);
5532        }
5533        for (index, child) in widget.children().iter().enumerate() {
5534            if let Some(found) = walk(child.as_ref(), child_id(id, index, child.as_ref()), target) {
5535                return Some(found);
5536            }
5537        }
5538        None
5539    }
5540    walk(root, WidgetId::ROOT, target)
5541}
5542
5543/// Folds a filter into the single filtered layer its subtree turned out to be, so two
5544/// filter widgets wrapped one inside the other cost one layer rather than two.
5545///
5546/// It is not only a saving. Compositing renders a layer into a texture and composites
5547/// that texture; a layer *inside* that texture is not composited again, so a nested
5548/// filter would simply not be applied. Folding is what makes `ColorFiltered` around
5549/// `ImageFiltered` mean what it reads as.
5550///
5551/// Everything about the inner layer must be neutral — full opacity, a plain
5552/// rectangular clip, no transform — because those are applied at compositing time and
5553/// there is only one composite to apply them at. And the two filters must not want
5554/// the same slot: greyscale of an inverted picture is not the inversion of a
5555/// greyscale one, so there is no single layer that means both, and the two nest.
5556fn fold_filter(
5557    group: Vec<Primitive>,
5558    filter: LayerFilter,
5559    clip: Rect,
5560) -> (Vec<Primitive>, LayerFilter, Rect) {
5561    if group.len() != 1 {
5562        return (group, filter, clip);
5563    }
5564    let Primitive::Layer {
5565        opacity,
5566        clip: inner_clip,
5567        clip_shape,
5568        transform,
5569        filter: inner,
5570        ..
5571    } = &group[0]
5572    else {
5573        return (group, filter, clip);
5574    };
5575    if *opacity < 0.999 || !matches!(clip_shape, ClipShape::Rect) || transform.is_some() {
5576        return (group, filter, clip);
5577    }
5578    let (inner, inner_clip) = (*inner, *inner_clip);
5579    let Some(merged) = filter.merge(inner) else {
5580        return (group, filter, clip);
5581    };
5582    let Some(Primitive::Layer { primitives, .. }) = group.into_iter().next() else {
5583        unreachable!("checked just above")
5584    };
5585    (primitives, merged, clip.intersect(inner_clip))
5586}
5587
5588#[cfg(test)]
5589mod tests {
5590    use super::*;
5591    use crate::physics::Scrollbars;
5592    use crate::runtime::Edit;
5593    use crate::{
5594        Button, Container, Flex, Key, Keyed, OverlayPortal, Placement, SingleChildScrollView,
5595        TextField,
5596    };
5597    use frus_core::{Color, Point, Primitive, Rect, Size};
5598
5599    /// **A deep tree on the smallest stack any platform gives a frame.**
5600    ///
5601    /// Building a tree recurses twice over it — the layout walk and the scene walk — and
5602    /// what each level costs is a property of those two functions, not of the widgets.
5603    /// Two `Theme` copies in each frame made that cost 53 kilobytes a level, so a tree
5604    /// forty deep exhausted the megabyte a Windows main thread gets and the process died
5605    /// with no panic and no message. A real screen nests further than forty.
5606    ///
5607    /// One megabyte and 64 levels is what this holds to: the platform's smallest stack,
5608    /// against a tree deeper than a screen builds and half again the depth that used to
5609    /// die. It is deliberately not the deepest that fits — around 128 now, at about seven
5610    /// kilobytes a level — because a tripwire wants margin over the number it guards. An
5611    /// overflow aborts rather than fails, so this one kills the run outright, which is the
5612    /// right noise for what it watches.
5613    #[test]
5614    fn a_deep_tree_builds_on_a_platform_s_smallest_stack() {
5615        let built = std::thread::Builder::new()
5616            .stack_size(1024 * 1024)
5617            .spawn(|| {
5618                let mut node: Box<dyn Widget<Msg>> = Box::new(crate::Text::new("the bottom"));
5619                for _ in 0..64 {
5620                    node = Box::new(Container::new().padding(1.0).child(node));
5621                }
5622                let ui = build_ui(
5623                    node.as_ref(),
5624                    Size::new(800.0, 600.0),
5625                    &Runtime::default(),
5626                    &Theme::default(),
5627                );
5628                ui.scene().primitives().len()
5629            })
5630            .expect("spawning the deep-tree thread")
5631            .join()
5632            .expect("the deep tree was built");
5633        assert!(built > 0, "and it painted something");
5634    }
5635
5636    #[derive(Clone, Debug, PartialEq)]
5637    enum Msg {
5638        A,
5639        B,
5640        C,
5641        D,
5642        Edited(String),
5643    }
5644
5645    #[test]
5646    fn semantics_tree_carries_roles_and_labels() {
5647        use crate::{Button, Checkbox, Role, Text};
5648        let rt = Runtime::default();
5649        let tree = crate::Flex::column()
5650            .child(Text::new("Titre"))
5651            .child(Button::new("Submit").on_press(Msg::A))
5652            .child(Checkbox::new(true).on_toggle(|_| Msg::B));
5653        let ui = build_ui(&tree, Size::new(300.0, 200.0), &rt, &Theme::default());
5654        let sem = ui.semantics();
5655        // One node per meaningful widget (the containing Flex is skipped).
5656        let roles: Vec<Role> = sem.iter().map(|(_, _, s)| s.role).collect();
5657        assert!(roles.contains(&Role::Label));
5658        assert!(roles.contains(&Role::Button));
5659        assert!(roles.contains(&Role::CheckBox));
5660        // The button carries its label and is actionable.
5661        let button = sem.iter().find(|(_, _, s)| s.role == Role::Button).unwrap();
5662        assert_eq!(button.2.label.as_deref(), Some("Submit"));
5663        assert!(button.2.clickable);
5664        // The checked box mirrors its state.
5665        let check = sem
5666            .iter()
5667            .find(|(_, _, s)| s.role == Role::CheckBox)
5668            .unwrap();
5669        assert_eq!(check.2.toggled, crate::Toggled::True);
5670    }
5671
5672    fn clickable_sample() -> Flex<Msg> {
5673        Flex::row()
5674            .width(400.0)
5675            .height(100.0)
5676            .padding(10.0)
5677            .gap(8.0)
5678            .child(
5679                Container::new()
5680                    .width(120.0)
5681                    .color(Color::rgb(1.0, 0.0, 0.0))
5682                    .hover_color(Color::rgb(0.0, 1.0, 0.0))
5683                    .on_click(Msg::A),
5684            )
5685            .child(
5686                Container::new()
5687                    .flex(1.0)
5688                    .color(Color::rgb(0.0, 0.0, 1.0))
5689                    .on_click(Msg::B),
5690            )
5691    }
5692
5693    /// A box whose children ran past it is **marked on the screen**, over a tenth of it,
5694    /// on the edge they ran past — and a box whose children fit is left alone.
5695    #[test]
5696    fn an_overflow_paints_a_band_on_the_edge_it_ran_past() {
5697        use crate::{Container, Flex};
5698        // Two children, because a row with one is not dividing a line up — it hands its
5699        // constraints down, and a lone child is bounded by them rather than overflowing.
5700        let row = |child_width: f32| {
5701            let root: Flex<()> = Flex::row()
5702                .width(100.0)
5703                .height(40.0)
5704                .child(
5705                    Container::new()
5706                        .width(child_width)
5707                        .height(20.0)
5708                        .color(Color::WHITE),
5709                )
5710                .child(Container::new().width(20.0).height(20.0));
5711            let rt = crate::runtime::Runtime::default();
5712            build_ui(
5713                &root,
5714                Size::new(200.0, 100.0),
5715                &rt,
5716                &crate::theme::Theme::dark(),
5717            )
5718        };
5719        // The stripes are paths; nothing else in this tree draws one.
5720        let stripes = |ui: &Ui<()>| {
5721            ui.scene()
5722                .primitives()
5723                .iter()
5724                .filter(|p| matches!(p, Primitive::Path { .. }))
5725                .count()
5726        };
5727        let over = row(300.0);
5728        assert!(!over.overflows().is_empty(), "the fixture has to overflow");
5729        assert!(stripes(&over) > 2, "striped: {}", stripes(&over));
5730        // The band is on the right-hand tenth: 90..100 of a 100-wide row.
5731        let band = over
5732            .scene()
5733            .primitives()
5734            .iter()
5735            .rev()
5736            .find_map(|p| match p {
5737                Primitive::Rect { rect, color, .. } if color.a > 0.7 && color.r == 0.0 => {
5738                    Some(*rect)
5739                }
5740                _ => None,
5741            })
5742            .expect("the band under the stripes");
5743        assert!((band.x - 90.0).abs() < 0.5, "at the right edge: {band:?}");
5744        assert!((band.width - 10.0).abs() < 0.5, "a tenth of it: {band:?}");
5745
5746        let fits = row(40.0);
5747        assert!(fits.overflows().is_empty(), "the fixture has to fit");
5748        assert!(fits.overflows().is_empty());
5749        assert_eq!(stripes(&fits), 0, "nothing to mark");
5750    }
5751
5752    /// The band says **which edge and by how much**, in the reference's words. A striped
5753    /// edge tells you something is too big; the sentence tells you what to look for, and
5754    /// it is the only half of the report that survives being photographed off a device.
5755    #[test]
5756    fn a_band_writes_which_edge_overflowed_and_by_how_much() {
5757        use crate::{Container, Flex};
5758        let root: Flex<()> = Flex::row()
5759            .width(100.0)
5760            .height(40.0)
5761            .child(
5762                Container::new()
5763                    .width(300.0)
5764                    .height(20.0)
5765                    .color(Color::WHITE),
5766            )
5767            .child(Container::new().width(20.0).height(20.0));
5768        let ui = build_ui(
5769            &root,
5770            Size::new(200.0, 100.0),
5771            &Runtime::default(),
5772            &Theme::dark(),
5773        );
5774        // The label is inside the rotated layer, which is why the search is recursive.
5775        fn labels(primitives: &[Primitive], out: &mut Vec<String>) {
5776            for p in primitives {
5777                match p {
5778                    Primitive::Text { text, .. } => out.push(text.clone()),
5779                    Primitive::Layer { primitives, .. } => labels(primitives, out),
5780                    _ => {}
5781                }
5782            }
5783        }
5784        let mut found = Vec::new();
5785        labels(ui.scene().primitives(), &mut found);
5786        assert!(
5787            found.iter().any(|t| t == "RIGHT OVERFLOWED BY 220 PIXELS"),
5788            "the sentence, in the reference's words: {found:?}"
5789        );
5790    }
5791
5792    /// The reference's precision rule: whole pixels when there are many, a decimal when
5793    /// there are few, three significant figures when there is almost nothing — which is
5794    /// the difference between a layout bug and a rounding error.
5795    #[test]
5796    fn an_overflow_is_written_to_the_precision_it_deserves() {
5797        assert_eq!(format_pixels(86.0), "86");
5798        // Past ten pixels the decimal is noise; at ten exactly it is not yet.
5799        assert_eq!(format_pixels(10.4), "10");
5800        assert_eq!(format_pixels(10.0), "10.0");
5801        assert_eq!(format_pixels(4.5), "4.5");
5802        assert_eq!(format_pixels(1.0), "1.00");
5803        assert_eq!(format_pixels(0.5), "0.500");
5804        assert_eq!(format_pixels(0.0123), "0.0123");
5805    }
5806
5807    #[test]
5808    fn rtl_mirrors_row_horizontally() {
5809        let size = Size::new(400.0, 100.0);
5810        // A = a fixed 120 px button (on the left in LTR), B = the flexible remainder.
5811        let ltr = build_ui(
5812            &clickable_sample(),
5813            size,
5814            &Runtime::default(),
5815            &Theme::default(),
5816        );
5817        let rtl = build_ui(
5818            &clickable_sample(),
5819            size,
5820            &Runtime::default(),
5821            &Theme::default().rtl(),
5822        );
5823        let hit = |ui: &Ui<Msg>, x: f32| ui.hit(Point::new(x, 50.0)).and_then(|id| ui.msg_for(id));
5824
5825        // LTR: button A takes the left edge.
5826        assert_eq!(hit(&ltr, 40.0), Some(Msg::A));
5827        assert_eq!(hit(&ltr, 360.0), Some(Msg::B));
5828        // RTL: everything is mirrored — A moves to the right, B takes the left.
5829        assert_eq!(hit(&rtl, 360.0), Some(Msg::A), "A on the right in RTL");
5830        assert_eq!(hit(&rtl, 40.0), Some(Msg::B), "B on the left in RTL");
5831    }
5832
5833    #[test]
5834    fn wrapped_text_wraps_in_layout_and_invalidates_the_cache() {
5835        let tree = |text: &str| {
5836            crate::Flex::column()
5837                .width(120.0)
5838                .child(crate::Text::new(text).wrap())
5839                .child(Container::new().height(10.0).on_click(Msg::A))
5840        };
5841        // Position of the clickable follower: the first y hit while sweeping.
5842        let follower_y = |ui: &Ui<Msg>| {
5843            (0..600)
5844                .map(|y| y as f32)
5845                .find(|&y| ui.hit(Point::new(60.0, y)).is_some())
5846                .expect("suiveur cliquable")
5847        };
5848
5849        let rt = Runtime::default();
5850        let long = "a rather long paragraph that will wrap onto several lines";
5851        let ui = build_ui(&tree(long), Size::new(120.0, 600.0), &rt, &Theme::default());
5852
5853        // The paragraph's render carries its wrap width (≤ the column).
5854        let max_w = ui
5855            .scene()
5856            .primitives()
5857            .iter()
5858            .find_map(|p| match p {
5859                Primitive::Text { max_width, .. } => *max_width,
5860                _ => None,
5861            })
5862            .expect("a wrapped paragraph");
5863        assert!(max_w <= 120.5, "wrapped to the column width: {max_w}");
5864
5865        // The wrapped text takes several lines: the follower is pushed down.
5866        let y_long = follower_y(&ui);
5867        assert!(
5868            y_long > 30.0,
5869            "the follower is pushed down by the wrap: {y_long}"
5870        );
5871
5872        // The SAME structure/styles, different content, the SAME runtime (a warm cache): the
5873        // measure key must invalidate the cache — otherwise the rects would be stale.
5874        let ui2 = build_ui(
5875            &tree("short"),
5876            Size::new(120.0, 600.0),
5877            &rt,
5878            &Theme::default(),
5879        );
5880        let y_short = follower_y(&ui2);
5881        assert!(
5882            y_short < y_long,
5883            "shorter content → a higher follower (the cache was invalidated): {y_short} vs {y_long}"
5884        );
5885    }
5886
5887    #[test]
5888    fn relayout_cache_reuses_the_root_layout_across_frames() {
5889        let rt = Runtime::default();
5890        let size = Size::new(400.0, 100.0);
5891        // Frame 1: nothing cached → a recomputation (at least the root).
5892        let _ = build_ui(&clickable_sample(), size, &rt, &Theme::default());
5893        let (hits1, misses1) = rt.layout_cache.borrow().last_frame_stats();
5894        assert_eq!(hits1, 0, "1st frame: nothing reused");
5895        assert!(misses1 >= 1, "1st frame: at least one computation");
5896
5897        // Frame 2: the same tree, the same constraints → the root is reused.
5898        let _ = build_ui(&clickable_sample(), size, &rt, &Theme::default());
5899        let (hits2, misses2) = rt.layout_cache.borrow().last_frame_stats();
5900        assert_eq!(hits2, 1, "2nd frame: the root is reused");
5901        assert_eq!(misses2, 0, "2nd frame: nothing recomputed");
5902
5903        // Frame 3: the window is resized → the constraints change → a recomputation.
5904        let _ = build_ui(
5905            &clickable_sample(),
5906            Size::new(500.0, 100.0),
5907            &rt,
5908            &Theme::default(),
5909        );
5910        let (hits3, misses3) = rt.layout_cache.borrow().last_frame_stats();
5911        assert_eq!((hits3, misses3), (0, 1), "redimensionnement → recalcul");
5912    }
5913
5914    #[test]
5915    fn modal_traps_tab_arrows_and_pointer_focus() {
5916        use crate::portal::{OverlayPortal, Placement};
5917        // The background: two focusable buttons; the open modal: two buttons as well.
5918        let dialog = Flex::<Msg>::row()
5919            .child(Button::new("ok").on_press(Msg::C))
5920            .child(Button::new("no").on_press(Msg::D));
5921        let tree: Flex<Msg> = Flex::column()
5922            .child(Button::new("bg1").on_press(Msg::A))
5923            .child(Button::new("bg2").on_press(Msg::B))
5924            .child(
5925                OverlayPortal::new(Container::new())
5926                    .overlay(dialog, Placement::Center)
5927                    .dismiss(Msg::A),
5928            );
5929        let ui = build_ui(
5930            &tree,
5931            Size::new(400.0, 300.0),
5932            &Runtime::default(),
5933            &Theme::default(),
5934        );
5935
5936        // Tab enters the trap (the modal's first focusable) and cycles inside it.
5937        let first = ui.focus_next(None, true).expect("the scope's first");
5938        assert_eq!(ui.msg_for(first), Some(Msg::C));
5939        let second = ui.focus_next(Some(first), true).expect("suivant");
5940        assert_eq!(ui.msg_for(second), Some(Msg::D));
5941        let wrapped = ui.focus_next(Some(second), true).expect("boucle");
5942        assert_eq!(
5943            ui.msg_for(wrapped),
5944            Some(Msg::C),
5945            "Tab cycles inside the modal"
5946        );
5947
5948        // The arrows stay inside the scope: there is nothing above the dialog.
5949        assert_eq!(ui.focus_directional(first, FocusDirection::Up), None);
5950        let right = ui
5951            .focus_directional(first, FocusDirection::Right)
5952            .expect("to the right");
5953        assert_eq!(ui.msg_for(right), Some(Msg::D));
5954
5955        // Click-to-focus ignores the background (the bg1 button is at the top left).
5956        assert_eq!(
5957            ui.focus_hit(Point::new(10.0, 10.0)),
5958            None,
5959            "fond hors scope"
5960        );
5961
5962        // Without a modal: no trap, Tab starts at the background.
5963        let open_less: Flex<Msg> = Flex::column()
5964            .child(Button::new("bg1").on_press(Msg::A))
5965            .child(Button::new("bg2").on_press(Msg::B));
5966        let ui2 = build_ui(
5967            &open_less,
5968            Size::new(400.0, 300.0),
5969            &Runtime::default(),
5970            &Theme::default(),
5971        );
5972        let f = ui2.focus_next(None, true).expect("premier");
5973        assert_eq!(ui2.msg_for(f), Some(Msg::A));
5974    }
5975
5976    #[test]
5977    fn open_menu_traps_focus_in_its_items() {
5978        use crate::PopupMenuButton;
5979        // A focusable background plus an **open** menu (anchor + two items).
5980        let menu = PopupMenuButton::new(Button::new("open").on_press(Msg::A), true, Msg::D)
5981            .item("one", Msg::B)
5982            .item("two", Msg::C);
5983        let tree: Flex<Msg> = Flex::column()
5984            .child(Button::new("bg").on_press(Msg::A))
5985            .child(menu);
5986        let ui = build_ui(
5987            &tree,
5988            Size::new(400.0, 300.0),
5989            &Runtime::default(),
5990            &Theme::default(),
5991        );
5992
5993        // Tab enters the menu's items and **cycles** inside (background and anchor are out of scope).
5994        let first = ui.focus_next(None, true).expect("the scope's first");
5995        assert_eq!(ui.msg_for(first), Some(Msg::B));
5996        let second = ui.focus_next(Some(first), true).expect("suivant");
5997        assert_eq!(ui.msg_for(second), Some(Msg::C));
5998        let wrapped = ui.focus_next(Some(second), true).expect("boucle");
5999        assert_eq!(
6000            ui.msg_for(wrapped),
6001            Some(Msg::B),
6002            "Tab cycles inside the open menu"
6003        );
6004        // The background (top left) is out of scope while the menu is open.
6005        assert_eq!(
6006            ui.focus_hit(Point::new(10.0, 10.0)),
6007            None,
6008            "fond hors scope"
6009        );
6010
6011        // The menu **closed**: no trap, Tab starts at the background.
6012        let closed: Flex<Msg> = Flex::column()
6013            .child(Button::new("bg").on_press(Msg::A))
6014            .child(PopupMenuButton::new(
6015                Button::new("open").on_press(Msg::B),
6016                false,
6017                Msg::D,
6018            ));
6019        let ui2 = build_ui(
6020            &closed,
6021            Size::new(400.0, 300.0),
6022            &Runtime::default(),
6023            &Theme::default(),
6024        );
6025        let f = ui2.focus_next(None, true).expect("premier");
6026        assert_eq!(ui2.msg_for(f), Some(Msg::A), "with no open menu, no trap");
6027    }
6028
6029    #[test]
6030    fn escape_infrastructure_finds_path_and_topmost_dismiss() {
6031        use crate::portal::{OverlayPortal, Placement};
6032        // An open portal (a modal with a dismissal) around a clickable anchor.
6033        let anchor = Container::<Msg>::new()
6034            .width(50.0)
6035            .height(30.0)
6036            .on_click(Msg::A);
6037        let content = Container::<Msg>::new()
6038            .width(80.0)
6039            .height(40.0)
6040            .on_click(Msg::B);
6041        let portal = OverlayPortal::new(anchor)
6042            .overlay(content, Placement::Center)
6043            .dismiss(Msg::C);
6044        let tree: Flex<Msg> = Flex::column().child(portal);
6045
6046        let ui = build_ui(
6047            &tree,
6048            Size::new(300.0, 200.0),
6049            &Runtime::default(),
6050            &Theme::default(),
6051        );
6052        // The topmost dismissal is the portal's.
6053        assert_eq!(ui.top_dismiss(), Some(Msg::C));
6054
6055        // Focus "inside the dialog": the root→content path goes through the portal, which
6056        // consumes Escape while bubbling. (The Center content, 80×40, is centred in 300×200 →
6057        // its centre is at (150, 100).)
6058        let inner = ui
6059            .hit(Point::new(150.0, 100.0))
6060            .expect("contenu de la modale");
6061        let path = find_path(&tree, inner);
6062        assert!(path.len() >= 3, "root, portal, content: {}", path.len());
6063        assert_eq!(
6064            path.last().unwrap().on_click(),
6065            Some(Msg::B),
6066            "the target closes the path"
6067        );
6068        let consumed = path
6069            .iter()
6070            .rev()
6071            .find_map(|w| match w.on_key(&crate::Key::Escape) {
6072                crate::KeyResponse::Handled(msg) => Some(msg),
6073                _ => None,
6074            });
6075        assert_eq!(
6076            consumed,
6077            Some(Some(Msg::C)),
6078            "the portal consumes Escape while bubbling"
6079        );
6080
6081        // Chemin introuvable → vide ; pas d'overlay → pas de fermeture.
6082        assert!(find_path(&tree, WidgetId::ROOT.child(99)).is_empty());
6083        let closed: Flex<Msg> = Flex::column().child(Container::new().on_click(Msg::A));
6084        let ui2 = build_ui(
6085            &closed,
6086            Size::new(300.0, 200.0),
6087            &Runtime::default(),
6088            &Theme::default(),
6089        );
6090        assert_eq!(ui2.top_dismiss(), None);
6091    }
6092
6093    #[test]
6094    fn long_press_targets_are_collected_topmost_first() {
6095        // A long-press container holding a long-press child: a point inside the child returns
6096        // the child's message (the topmost one).
6097        let tree: Container<Msg> = Container::new()
6098            .width(200.0)
6099            .height(100.0)
6100            .on_long_press(Msg::A)
6101            .child(
6102                Container::new()
6103                    .width(50.0)
6104                    .height(50.0)
6105                    .on_long_press(Msg::B),
6106            );
6107        let ui = build_ui(
6108            &tree,
6109            Size::new(200.0, 100.0),
6110            &Runtime::default(),
6111            &Theme::default(),
6112        );
6113        assert_eq!(ui.long_press_at(Point::new(25.0, 25.0)), Some(Msg::B));
6114        assert_eq!(ui.long_press_at(Point::new(150.0, 80.0)), Some(Msg::A));
6115        assert_eq!(ui.long_press_at(Point::new(500.0, 500.0)), None);
6116    }
6117
6118    #[test]
6119    fn hit_and_msg_for_route_correctly() {
6120        let rt = Runtime::default();
6121        let ui = build_ui(
6122            &clickable_sample(),
6123            Size::new(400.0, 100.0),
6124            &rt,
6125            &Theme::default(),
6126        );
6127        let id_a = ui.hit(Point::new(50.0, 50.0)).expect("A");
6128        let id_b = ui.hit(Point::new(300.0, 50.0)).expect("B");
6129        assert_ne!(id_a, id_b);
6130        assert_eq!(ui.msg_for(id_a), Some(Msg::A));
6131        assert_eq!(ui.msg_for(id_b), Some(Msg::B));
6132        assert_eq!(ui.hit(Point::new(3.0, 3.0)), None);
6133    }
6134
6135    /// Every rectangle a frame paints down the right-hand edge of a 200-wide viewport.
6136    /// The bar is drawn last, over the content, and it is the only thing over there.
6137    fn painted(ui: &Ui<()>) -> Vec<(Rect, Color)> {
6138        ui.scene()
6139            .primitives()
6140            .iter()
6141            .filter_map(|p| match p {
6142                Primitive::Rect { rect, color, .. }
6143                    if rect.x >= 200.0 - BAR_SIZE - BAR_MARGIN * 2.0 =>
6144                {
6145                    Some((*rect, *color))
6146                }
6147                _ => None,
6148            })
6149            .collect()
6150    }
6151
6152    /// A tall page in a short window, its bar **pinned**: whether a bar fades is the
6153    /// business of the tests below, and these are about whether there is one to fade.
6154    fn bars(area: SingleChildScrollView<()>, how: Scrollbars) -> Vec<(Rect, Color)> {
6155        let runtime = Runtime::with_scrollbars(how);
6156        let ui = build_ui(
6157            &area.thumb_visibility(true),
6158            Size::new(200.0, 100.0),
6159            &runtime,
6160            &Theme::dark(),
6161        );
6162        painted(&ui)
6163    }
6164
6165    /// A tall column in a short viewport: something to scroll.
6166    fn tall() -> SingleChildScrollView<()> {
6167        SingleChildScrollView::new()
6168            .width(200.0)
6169            .height(100.0)
6170            .child(Container::<()>::new().width(200.0).height(1000.0))
6171    }
6172
6173    /// **A touch screen gets no scrollbar** (`app.dart:870`).
6174    ///
6175    /// This drew one on every platform, on both axes, always — a permanent bar over the
6176    /// content of every scrolling page on a phone, where the reference draws nothing at
6177    /// all. A finger already knows where it is on the page.
6178    #[test]
6179    fn a_touch_screen_gets_no_scrollbar() {
6180        assert_eq!(bars(tall(), Scrollbars::Never), vec![]);
6181        assert_eq!(
6182            bars(tall(), Scrollbars::Always).len(),
6183            1,
6184            "and a desktop gets one"
6185        );
6186
6187        // Nothing to drag, either: a bar that is not drawn is not a target.
6188        let runtime = Runtime::with_scrollbars(Scrollbars::Never);
6189        let ui = build_ui(&tall(), Size::new(200.0, 100.0), &runtime, &Theme::dark());
6190        assert!(ui.scrollbars().is_empty());
6191    }
6192
6193    /// **A bar takes the scheme's word for the brightness, not the surface's luminance.**
6194    ///
6195    /// The opacities either side of this are three times apart at rest (`scrollbar.dart:242`,
6196    /// `:248`), so getting the question wrong is not a shade — it is a bar that reads as
6197    /// a smudge, or one that reads as a stripe.
6198    ///
6199    /// A **dimmed light** scheme is where the two answers part. The measurement it used
6200    /// to make says dark; the scheme says light, and the scheme is the one that knows.
6201    #[test]
6202    fn a_bar_reads_the_scheme_s_brightness_and_not_its_luminance() {
6203        use crate::media::Brightness;
6204        let dimmed = |brightness| {
6205            let mut theme = Theme::light();
6206            theme.scheme.surface = Color::rgb8(110, 110, 110);
6207            theme.scheme.brightness = brightness;
6208            theme
6209        };
6210        let thumb = |theme: &Theme| {
6211            let runtime = Runtime::with_scrollbars(Scrollbars::Always);
6212            let ui = build_ui(
6213                &tall().thumb_visibility(true),
6214                Size::new(200.0, 100.0),
6215                &runtime,
6216                theme,
6217            );
6218            painted(&ui)[0].1
6219        };
6220
6221        let light = dimmed(Brightness::Light);
6222        assert!(
6223            light.scheme.surface.compute_luminance() < 0.5,
6224            "the luminance the old line measured says dark"
6225        );
6226        assert_eq!(
6227            thumb(&light),
6228            light.scheme.on_surface.fade(THUMB_ON_LIGHT),
6229            "and the bar takes the scheme's word instead"
6230        );
6231
6232        let dark = dimmed(Brightness::Dark);
6233        assert_eq!(thumb(&dark), dark.scheme.on_surface.fade(THUMB_ON_DARK));
6234    }
6235
6236    /// **A theme reaches every number the bar is made of**, not only its colour. A
6237    /// scrollbar themed in its colour and sized from a constant is themed in the half
6238    /// that shows and unthemed in the half a pointer has to hit.
6239    ///
6240    /// There is no per-call override to outrank here — nobody configures a scrollbar at a
6241    /// call site, the walk draws it — so the chain is `theme ?? framework`, and the second
6242    /// half of this test is that the framework still answers where the theme says nothing.
6243    #[test]
6244    fn a_theme_reaches_every_number_the_scrollbar_is_made_of() {
6245        const THUMB: Color = Color::rgb(0.2, 0.7, 0.9);
6246        let mut theme = Theme::dark();
6247        theme.widgets.scrollbar.thickness = Some(14.0);
6248        theme.widgets.scrollbar.margin = Some(5.0);
6249        theme.widgets.scrollbar.min_thumb_length = Some(90.0);
6250        theme.widgets.scrollbar.radius = Some(2.0);
6251        theme.widgets.scrollbar.thumb_color = Some(THUMB);
6252        theme.widgets.scrollbar.opacity = Some(0.8);
6253
6254        let runtime = Runtime::with_scrollbars(Scrollbars::Always);
6255        let ui = build_ui(
6256            &tall().thumb_visibility(true),
6257            Size::new(200.0, 100.0),
6258            &runtime,
6259            &theme,
6260        );
6261        // `painted` filters on the strip a bar of the **default** thickness occupies, so
6262        // it cannot see a themed one — which is the point of the theme. This asks for the
6263        // strip the themed bar is in.
6264        let over_there: Vec<(Rect, Color)> = ui
6265            .scene()
6266            .primitives()
6267            .iter()
6268            .filter_map(|p| match p {
6269                Primitive::Rect { rect, color, .. } if rect.x >= 200.0 - 14.0 - 5.0 * 2.0 => {
6270                    Some((*rect, *color))
6271                }
6272                _ => None,
6273            })
6274            .collect();
6275        assert_eq!(over_there.len(), 1, "still one rectangle: the thumb");
6276        let (thumb, color) = over_there[0];
6277        assert!(
6278            (thumb.width - 14.0).abs() < 0.01,
6279            "the theme's thickness: {thumb:?}"
6280        );
6281        assert!(
6282            ((thumb.x + thumb.width) - (200.0 - 5.0)).abs() < 0.01,
6283            "held clear by the theme's margin, not shrunk to fit: {thumb:?}"
6284        );
6285        assert!(
6286            thumb.height >= 90.0,
6287            "and never shorter than the theme's floor: {thumb:?}"
6288        );
6289        assert_eq!(
6290            color,
6291            THUMB.fade(0.8),
6292            "the theme's colour, faded to the theme's resting level"
6293        );
6294
6295        // The radius is not in `painted`, which reads the rectangles' colours; take it
6296        // from the primitive itself. A pill is what an unthemed bar is, and this one is
6297        // not a pill.
6298        let radius = ui.scene().primitives().iter().find_map(|p| match p {
6299            Primitive::Rect { radius, .. } if radius.top_left > 0.0 => Some(radius.top_left),
6300            _ => None,
6301        });
6302        assert_eq!(radius, Some(2.0), "the theme's corner radius");
6303
6304        // And with a theme that says nothing, the framework still answers.
6305        let plain = bars(tall(), Scrollbars::Always);
6306        let (thumb, color) = plain[0];
6307        assert!((thumb.width - BAR_SIZE).abs() < 0.01);
6308        assert_eq!(color, Theme::dark().scheme.on_surface.fade(THUMB_ON_DARK));
6309    }
6310
6311    /// **And it is a thumb and nothing else** (`scrollbar.dart:281`): the reference's
6312    /// track is transparent unless a caller asks for one, and even then it is 3 to 5 %.
6313    /// This painted an 18 % stripe down every scrolling page, under a 55 % thumb.
6314    #[test]
6315    fn a_bar_is_a_thumb_and_no_track() {
6316        let painted = bars(tall(), Scrollbars::Always);
6317        assert_eq!(painted.len(), 1, "one rectangle: the thumb");
6318        let (thumb, color) = painted[0];
6319        assert!(
6320            (thumb.width - BAR_SIZE).abs() < 0.01,
6321            "at the reference's thickness: {thumb:?}"
6322        );
6323        assert!(
6324            ((thumb.x + thumb.width) - (200.0 - BAR_MARGIN)).abs() < 0.01,
6325            "and clear of the edge by `crossAxisMargin`, not shrunk to fit: {thumb:?}"
6326        );
6327        assert!(
6328            thumb.height >= MIN_THUMB,
6329            "and never shorter than a pointer can catch: {thumb:?}"
6330        );
6331        let theme = Theme::dark();
6332        assert_eq!(color, theme.scheme.on_surface.fade(THUMB_ON_DARK));
6333    }
6334
6335    /// **Along the horizontal axis, never** (`app.dart:861`) — on any platform, under any
6336    /// setting. A sideways strip is scrolled by whatever made it sideways, and a bar
6337    /// under it takes room from content that is short of it already.
6338    #[test]
6339    fn a_sideways_strip_never_gets_one() {
6340        let strip = SingleChildScrollView::new()
6341            .axis(crate::scroll::Axis::Horizontal)
6342            .width(200.0)
6343            .height(100.0)
6344            .child(Container::<()>::new().width(1000.0).height(100.0));
6345        let runtime = Runtime::with_scrollbars(Scrollbars::Always);
6346        let ui = build_ui(&strip, Size::new(200.0, 100.0), &runtime, &Theme::dark());
6347        assert!(
6348            ui.scrollbars().is_empty(),
6349            "a desktop draws none across, either"
6350        );
6351    }
6352
6353    /// And **one area may answer for itself**, over the application's answer — the
6354    /// per-area say the physics beside it already had.
6355    #[test]
6356    fn an_area_may_ask_for_its_own_answer() {
6357        assert_eq!(
6358            bars(tall().scrollbars(Scrollbars::Always), Scrollbars::Never).len(),
6359            1,
6360            "asked for one where the platform gives none"
6361        );
6362        assert_eq!(
6363            bars(tall().scrollbars(Scrollbars::Never), Scrollbars::Always),
6364            vec![],
6365            "and refused one where it does"
6366        );
6367    }
6368
6369    /// **A bar nobody has scrolled is not there at all.** The reference's fade starts
6370    /// closed and is opened only by movement (`scrollbar.dart:1960`); a list that has
6371    /// just appeared shows its content, not its furniture.
6372    ///
6373    /// It is **registered** even so, which a bar the platform does not draw is not. The
6374    /// difference is real: that one is not there, and this one is here and invisible —
6375    /// and a mouse coming near it is the only thing that can bring it back.
6376    #[test]
6377    fn an_untouched_area_shows_no_bar_but_can_still_be_reached_for() {
6378        let runtime = Runtime::with_scrollbars(Scrollbars::Always);
6379        let ui = build_ui(&tall(), Size::new(200.0, 100.0), &runtime, &Theme::dark());
6380        assert_eq!(painted(&ui), vec![], "nothing is painted for it");
6381
6382        let bar = *ui.scrollbars().first().expect("registered all the same");
6383        assert_eq!(bar.opacity, 0.0);
6384        let on_it = Point::new(194.0, 20.0);
6385        assert!(
6386            ui.scrollbar_at(on_it).is_none(),
6387            "and a bar at zero cannot be grabbed"
6388        );
6389        assert!(
6390            ui.scrollbar_near(on_it).is_some(),
6391            "but it can be reached for, which is what brings it back"
6392        );
6393        // The reach is wider than the bar: 8 pixels at a tenth of an opacity is not
6394        // something a hand aims at precisely.
6395        assert!(
6396            ui.scrollbar_near(Point::new(178.0, 20.0)).is_some(),
6397            "the reach takes in a tap target's room around the thumb: {:?}",
6398            bar.reach
6399        );
6400    }
6401
6402    /// **A pinned bar does not fade** (`scrollbar.dart:214`) — for an area whose content
6403    /// does not look scrollable, or one a reader should be able to aim at without
6404    /// scrolling first to make the bar appear.
6405    #[test]
6406    fn a_pinned_bar_does_not_fade() {
6407        let runtime = Runtime::with_scrollbars(Scrollbars::Always);
6408        let ui = build_ui(
6409            &tall().thumb_visibility(true),
6410            Size::new(200.0, 100.0),
6411            &runtime,
6412            &Theme::dark(),
6413        );
6414        assert_eq!(painted(&ui).len(), 1, "there without being scrolled to");
6415        assert_eq!(ui.scrollbars()[0].opacity, 1.0);
6416    }
6417
6418    /// **A thumb under a pointer, and a thumb in a hand**, are two different colours
6419    /// (`scrollbar.dart:245`, `:244`) — and neither is the resting one. The dragged
6420    /// value arrives at once: the hand is already on it.
6421    #[test]
6422    fn a_thumb_answers_the_pointer() {
6423        use crate::runtime::ScrollbarFade;
6424        let build = |prime: &dyn Fn(&mut Runtime)| {
6425            let mut runtime = Runtime::with_scrollbars(Scrollbars::Always);
6426            prime(&mut runtime);
6427            let ui = build_ui(
6428                &tall().thumb_visibility(true),
6429                Size::new(200.0, 100.0),
6430                &runtime,
6431                &Theme::dark(),
6432            );
6433            let id = ui.scrollbars()[0].id;
6434            (id, painted(&ui)[0].1)
6435        };
6436
6437        let theme = Theme::dark();
6438        let (id, resting) = build(&|_| {});
6439        assert_eq!(resting, theme.scheme.on_surface.fade(THUMB_ON_DARK));
6440
6441        let (_, warmed) = build(&|rt| {
6442            rt.scrollbar_fade.insert(
6443                id,
6444                ScrollbarFade {
6445                    opacity: 1.0,
6446                    hover: 1.0,
6447                    ..ScrollbarFade::default()
6448                },
6449            );
6450        });
6451        assert_eq!(warmed, theme.scheme.on_surface.fade(THUMB_HOVER_ON_DARK));
6452
6453        let (_, grabbed) = build(&|rt| rt.scrollbar_dragged = Some(id));
6454        assert_eq!(grabbed, theme.scheme.on_surface.fade(THUMB_DRAG_ON_DARK));
6455    }
6456
6457    #[test]
6458    fn hover_progress_interpolates_color() {
6459        let rt = Runtime::default();
6460        let base = build_ui(
6461            &clickable_sample(),
6462            Size::new(400.0, 100.0),
6463            &rt,
6464            &Theme::default(),
6465        );
6466        let id_a = base.hit(Point::new(50.0, 50.0)).unwrap();
6467
6468        // Without any progress: the base colour (red).
6469        if let Primitive::Rect { color, .. } = base.scene().primitives()[0] {
6470            assert_eq!(color, Color::rgb(1.0, 0.0, 0.0));
6471        } else {
6472            panic!("expected a rect");
6473        }
6474
6475        // Full progress: the hover colour (green).
6476        let mut rt = Runtime::default();
6477        rt.input.hovered = Some(id_a);
6478        rt.anims.insert(
6479            id_a,
6480            crate::Anim {
6481                hover: 1.0,
6482                ..Default::default()
6483            },
6484        );
6485        let ui = build_ui(
6486            &clickable_sample(),
6487            Size::new(400.0, 100.0),
6488            &rt,
6489            &Theme::default(),
6490        );
6491        if let Primitive::Rect { color, .. } = ui.scene().primitives()[0] {
6492            assert_eq!(color, Color::rgb(0.0, 1.0, 0.0));
6493        } else {
6494            panic!("expected a rect");
6495        }
6496    }
6497
6498    #[test]
6499    fn multiline_field_registers_as_scrollable_when_overflowing() {
6500        // A multi-line field whose content exceeds `rows` registers itself as a scrollable
6501        // area (with `max_y > 0`) — which is what the wheel and the scrollbar target. A short
6502        // field does not register.
6503        let tall = TextField::<Msg>::new("a\nb\nc\nd\ne\nf")
6504            .on_input(Msg::Edited)
6505            .rows(2)
6506            .width(200.0);
6507        let tree: Flex<Msg> = Flex::column().child(tall);
6508        let rt = Runtime::default();
6509        let ui = build_ui(&tree, Size::new(220.0, 240.0), &rt, &Theme::default());
6510        let maxes = ui.scrollable_maxes();
6511        assert_eq!(maxes.len(), 1, "the overflowing field registers");
6512        assert!(maxes[0].2 > 0.0, "max_y > 0 (overflowing content)");
6513
6514        let short = TextField::<Msg>::new("a\nb")
6515            .on_input(Msg::Edited)
6516            .rows(4)
6517            .width(200.0);
6518        let tree: Flex<Msg> = Flex::column().child(short);
6519        let ui = build_ui(&tree, Size::new(220.0, 240.0), &rt, &Theme::default());
6520        assert!(
6521            ui.scrollable_maxes().is_empty(),
6522            "a short field does not scroll"
6523        );
6524    }
6525
6526    #[test]
6527    fn only_text_inputs_place_a_cursor() {
6528        // The invariant of the click fix (milestone 39): a focusable button returns NO cursor
6529        // (`cursor_at` = None), so the shell does not start a text selection on it and does not
6530        // capture the click. Only text fields place one.
6531        let button = Button::<Msg>::new("x").on_press(Msg::A);
6532        assert_eq!(Widget::<Msg>::cursor_at(&button, 10.0, 5.0, 200.0, 0), None);
6533        let input = TextField::<Msg>::new("hi").on_input(Msg::Edited);
6534        assert!(Widget::<Msg>::cursor_at(&input, 10.0, 5.0, 200.0, 0).is_some());
6535    }
6536
6537    /// A form taller than its window: six buttons of 60 px in a 150 px scroll.
6538    fn long_form() -> SingleChildScrollView<Msg> {
6539        let mut column = Flex::<Msg>::column();
6540        for i in 0..6 {
6541            column = column.child(
6542                Button::new(format!("field {i}"))
6543                    .on_press(Msg::A)
6544                    .height(60.0),
6545            );
6546        }
6547        SingleChildScrollView::new()
6548            .width(200.0)
6549            .height(150.0)
6550            .child(column)
6551    }
6552
6553    fn form_ui(runtime: &Runtime) -> Ui<Msg> {
6554        build_ui(
6555            &long_form(),
6556            Size::new(200.0, 150.0),
6557            runtime,
6558            &Theme::default(),
6559        )
6560    }
6561
6562    /// **Every** field is a focus stop, not only the two and a half on screen.
6563    ///
6564    /// Registering what the eye can see is how a long form came to have most of its
6565    /// fields unreachable from the keyboard: Tab walked the visible ones and wrapped.
6566    #[test]
6567    fn tab_reaches_the_fields_below_the_fold() {
6568        let ui = form_ui(&Runtime::default());
6569        assert_eq!(ui.focusables.len(), 6, "all six, not the visible ones");
6570        // And the traversal is in tree order, so Tab walks down the form.
6571        let ids: Vec<_> = ui.focusables.iter().map(|f| f.id).collect();
6572        assert_eq!(ui.focus_next(Some(ids[2]), true), Some(ids[3]));
6573    }
6574
6575    /// A stop clipped away by a scroll keeps its **real** box, and an empty visible one.
6576    /// The two are different questions and the registry answers both.
6577    #[test]
6578    fn a_stop_out_of_sight_keeps_its_geometry() {
6579        let ui = form_ui(&Runtime::default());
6580        let fifth = ui.focusables[5];
6581        assert!((fifth.bounds.y - 300.0).abs() < 0.5, "{:?}", fifth.bounds);
6582        assert_eq!(fifth.rect.height, 0.0, "none of it is on screen");
6583        assert!(fifth.scroll.is_some(), "something around it scrolls");
6584    }
6585
6586    /// A click still lands only on what can be seen. The unclipped box of a field
6587    /// scrolled out of sight overlaps the window, and a tap there must not focus it.
6588    #[test]
6589    fn a_click_still_only_reaches_what_is_visible() {
6590        let ui = form_ui(&Runtime::default());
6591        // The second field, in the middle of its own visible box.
6592        let second = ui.focusables[1];
6593        let inside = Point::new(
6594            second.rect.x + second.rect.width / 2.0,
6595            second.rect.y + second.rect.height / 2.0,
6596        );
6597        assert_eq!(ui.focus_hit(inside).map(|(id, _)| id), Some(second.id));
6598        // The sixth field's **real** box overlaps nothing on screen, and a tap where it
6599        // would be if the form were not scrolled hits nothing at all.
6600        let sixth = ui.focusables[5];
6601        let past = Point::new(
6602            sixth.bounds.x + sixth.bounds.width / 2.0,
6603            sixth.bounds.y + sixth.bounds.height / 2.0,
6604        );
6605        assert!(past.y > 150.0, "past the fold: {past:?}");
6606        assert_eq!(ui.focus_hit(past), None);
6607    }
6608
6609    /// Focus lands below the fold and the view follows: the least movement that brings
6610    /// the field's foot to the window's.
6611    #[test]
6612    fn revealing_a_stop_below_the_fold_scrolls_to_it() {
6613        let runtime = Runtime::default();
6614        let ui = form_ui(&runtime);
6615        let area = ui.scroll_regions()[0];
6616        let fifth = ui.focusables[5].id;
6617        let moves = ui.reveal(fifth, &runtime);
6618        // Field 5 sits at 300..360 and the window is 150 tall: its foot reaches the
6619        // window's foot at 360 - 150 = 210.
6620        assert_eq!(moves, vec![(area.id, (0.0, 210.0))]);
6621    }
6622
6623    /// Already in view, nothing moves — the answer is the empty list rather than a
6624    /// zero, so no caller has to ask twice.
6625    #[test]
6626    fn revealing_something_already_in_view_moves_nothing() {
6627        let runtime = Runtime::default();
6628        let ui = form_ui(&runtime);
6629        assert!(ui.reveal(ui.focusables[0].id, &runtime).is_empty());
6630        assert!(ui.reveal(ui.focusables[1].id, &runtime).is_empty());
6631        // And a stop the frame does not contain at all is not an error.
6632        assert!(ui.reveal(WidgetId::from_u64(7), &runtime).is_empty());
6633    }
6634
6635    /// Coming back **up** the form scrolls the other way, and only as far as it must.
6636    #[test]
6637    fn revealing_a_stop_above_the_fold_scrolls_back() {
6638        let mut runtime = Runtime::default();
6639        let area_id = form_ui(&runtime).scroll_regions()[0].id;
6640        runtime.scroll.insert(area_id, (0.0, 210.0));
6641        let ui = form_ui(&runtime);
6642        let first = ui.focusables[0].id;
6643        // Field 0 sits at 0..60: its head reaches the window's head at offset 0.
6644        assert_eq!(ui.reveal(first, &runtime), vec![(area_id, (0.0, 0.0))]);
6645        // At offset 210 the window shows 210..360, so field 4 (240..300) is wholly in
6646        // it and does not move. Field 3 (180..240) hangs over the top edge and would.
6647        assert!(ui.reveal(ui.focusables[4].id, &runtime).is_empty());
6648        assert!(!ui.reveal(ui.focusables[3].id, &runtime).is_empty());
6649    }
6650
6651    /// The arithmetic on its own: the least movement, the leading edge for anything too
6652    /// big to fit, and the clamp at the ends of the travel.
6653    #[test]
6654    fn the_reveal_moves_as_little_as_it_can() {
6655        let area = Scrollable {
6656            id: WidgetId::ROOT,
6657            viewport: Rect::new(0.0, 0.0, 100.0, 100.0),
6658            max_x: 0.0,
6659            max_y: 400.0,
6660            physics: None,
6661            refresh: None,
6662            page: None,
6663            reverse_x: false,
6664            reverse_y: false,
6665            host: None,
6666            keep_visible: None,
6667        };
6668        // Inside: nothing.
6669        assert_eq!(
6670            area.reveal(Rect::new(0.0, 10.0, 50.0, 20.0), (0.0, 0.0)).1,
6671            (0.0, 0.0)
6672        );
6673        // Below: its foot comes to the window's foot, and no further.
6674        assert_eq!(
6675            area.reveal(Rect::new(0.0, 140.0, 50.0, 20.0), (0.0, 0.0)).0,
6676            (0.0, 60.0)
6677        );
6678        // Above: its head comes to the window's head.
6679        assert_eq!(
6680            area.reveal(Rect::new(0.0, -30.0, 50.0, 20.0), (0.0, 50.0))
6681                .0,
6682            (0.0, 20.0)
6683        );
6684        // Taller than the window: the **start** edge wins, because that is what is read.
6685        assert_eq!(
6686            area.reveal(Rect::new(0.0, -10.0, 50.0, 300.0), (0.0, 40.0))
6687                .0,
6688            (0.0, 30.0)
6689        );
6690        // And the travel is a wall: an offset past the end is not asked for.
6691        assert_eq!(
6692            area.reveal(Rect::new(0.0, 900.0, 50.0, 20.0), (0.0, 0.0)).0,
6693            (0.0, 400.0)
6694        );
6695    }
6696
6697    /// A reversed axis numbers its offsets from the far end, so the same movement of the
6698    /// content is the opposite movement of the offset. One function knows that, and the
6699    /// reveal asks it rather than keeping a second opinion.
6700    #[test]
6701    fn the_reveal_follows_a_reversed_axis() {
6702        let area = Scrollable {
6703            id: WidgetId::ROOT,
6704            viewport: Rect::new(0.0, 0.0, 100.0, 100.0),
6705            max_x: 0.0,
6706            max_y: 400.0,
6707            physics: None,
6708            refresh: None,
6709            page: None,
6710            reverse_x: false,
6711            reverse_y: true,
6712            host: None,
6713            keep_visible: None,
6714        };
6715        // A box below the window: the content must move **up** by 60 either way. On a
6716        // reversed axis a rising offset pushes the content down (`content_origin` is
6717        // `viewport - content + offset` there), so the offset comes **down** by 60 to do
6718        // what a plain axis would need it to go up by.
6719        let (offset, shift) = area.reveal(Rect::new(0.0, 140.0, 50.0, 20.0), (0.0, 100.0));
6720        assert_eq!(shift, (0.0, -60.0));
6721        assert_eq!(offset, (0.0, 40.0));
6722    }
6723
6724    /// Centring puts the box in the middle of the window, and the clamp is what keeps
6725    /// the first and last items of a row where they belong rather than half off it.
6726    #[test]
6727    fn centring_is_clamped_to_the_travel() {
6728        let area = Scrollable {
6729            id: WidgetId::ROOT,
6730            viewport: Rect::new(0.0, 0.0, 300.0, 50.0),
6731            max_x: 700.0,
6732            max_y: 0.0,
6733            physics: None,
6734            refresh: None,
6735            page: None,
6736            reverse_x: false,
6737            reverse_y: false,
6738            host: None,
6739            keep_visible: None,
6740        };
6741        // A box at 400..500 has its middle at 450; the window's is at 150.
6742        assert_eq!(
6743            area.centre(Rect::new(400.0, 0.0, 100.0, 50.0), (0.0, 0.0))
6744                .0,
6745            (300.0, 0.0)
6746        );
6747        // The first item cannot be centred without scrolling backwards, so it stays.
6748        assert_eq!(
6749            area.centre(Rect::new(0.0, 0.0, 100.0, 50.0), (0.0, 0.0)).0,
6750            (0.0, 0.0)
6751        );
6752        // Nor the last past the end of the travel.
6753        assert_eq!(
6754            area.centre(Rect::new(900.0, 0.0, 100.0, 50.0), (0.0, 0.0))
6755                .0,
6756            (700.0, 0.0)
6757        );
6758    }
6759
6760    /// **A lone child could widen the box it was given, and take the screen with it.**
6761    ///
6762    /// A flex item's automatic minimum is its **min-content** size, so agreeing to shrink
6763    /// buys nothing on its own: the item shrinks willingly and then refuses to go below
6764    /// the widest thing inside it. A column holding one over-wide row came out at that
6765    /// row's width; the padded box around it came out at the same; and every sibling,
6766    /// stretched across the column, came out at the same again — a whole screen laid out
6767    /// to the width of its widest line and then clipped by the window.
6768    ///
6769    /// Reported from a real application: a shop whose banner, search field, filter row
6770    /// and product grid were all cut off at the same edge, because one segmented control
6771    /// four segments wide had decided how wide the screen was.
6772    ///
6773    /// The over-wide child still overflows — that is the reference's behaviour and the
6774    /// honest one — but it no longer drags its parent and its siblings out with it.
6775    #[test]
6776    fn a_lone_child_cannot_widen_the_box_it_was_given() {
6777        let ordinary = Color::rgb(0.1, 0.2, 0.3);
6778        let overwide = Color::rgb(0.9, 0.8, 0.7);
6779        let screen: SingleChildScrollView<Msg> = SingleChildScrollView::new().width(300.0).child(
6780            Container::new().padding(24.0).child(
6781                Flex::column()
6782                    .align(frus_layout::Align::Stretch)
6783                    .child(Container::new().height(20.0).color(ordinary))
6784                    .child(Container::new().width(600.0).height(20.0).color(overwide)),
6785            ),
6786        );
6787        let ui = build_ui(
6788            &screen,
6789            Size::new(300.0, 800.0),
6790            &Runtime::default(),
6791            &Theme::default(),
6792        );
6793        let width_of = |wanted: Color| {
6794            ui.scene()
6795                .primitives()
6796                .iter()
6797                .find_map(|p| match p {
6798                    Primitive::Rect { color, rect, .. } if *color == wanted => Some(rect.width),
6799                    _ => None,
6800                })
6801                .expect("the box is drawn")
6802        };
6803        // 300 less the padding either side, and not 600.
6804        assert_eq!(width_of(ordinary), 252.0);
6805        // The one that asked to be too wide still is: it overflows rather than being
6806        // squeezed, and rather than resizing everything around it.
6807        assert_eq!(width_of(overwide), 600.0);
6808    }
6809
6810    #[test]
6811    fn tab_cycles_focusables_in_order() {
6812        let tree = Flex::<Msg>::column()
6813            .child(Button::new("un").on_press(Msg::A))
6814            .child(Button::new("deux").on_press(Msg::B));
6815        let ui = build_ui(
6816            &tree,
6817            Size::new(200.0, 200.0),
6818            &Runtime::default(),
6819            &Theme::default(),
6820        );
6821        assert_eq!(ui.focusables.len(), 2, "both buttons are focusable");
6822        let first = ui.focusables[0].id;
6823        let second = ui.focusables[1].id;
6824
6825        // With no focus: Tab -> first, Shift+Tab -> last.
6826        assert_eq!(ui.focus_next(None, true), Some(first));
6827        assert_eq!(ui.focus_next(None, false), Some(second));
6828        // Wrap-around.
6829        assert_eq!(ui.focus_next(Some(first), true), Some(second));
6830        assert_eq!(ui.focus_next(Some(second), true), Some(first));
6831        assert_eq!(ui.focus_next(Some(first), false), Some(second));
6832    }
6833
6834    #[test]
6835    fn focus_ring_for_button_not_for_textinput() {
6836        let theme = Theme::default();
6837        let ring = theme.focus.fade(0.4); // focus_progress = 0 → alpha 0.4
6838        let count_ring = |tree: &dyn Widget<Msg>, keyboard: bool| -> usize {
6839            let mut rt = Runtime::default();
6840            let probe = build_ui(tree, Size::new(200.0, 200.0), &rt, &theme);
6841            rt.input.focused = probe.focusables.first().map(|f| f.id);
6842            rt.focus_visible = keyboard;
6843            let ui = build_ui(tree, Size::new(200.0, 200.0), &rt, &theme);
6844            ui.scene()
6845                .primitives()
6846                .iter()
6847                .filter(
6848                    |p| matches!(p, Primitive::Rect { border_color, .. } if *border_color == ring),
6849                )
6850                .count()
6851        };
6852        let with_button = Flex::<Msg>::column().child(Button::new("x").on_press(Msg::A));
6853        assert!(
6854            count_ring(&with_button, true) >= 1,
6855            "a button focused with the keyboard has a generic ring"
6856        );
6857        // Focus taken with the pointer: no ring (FocusHighlightMode).
6858        assert_eq!(
6859            count_ring(&with_button, false),
6860            0,
6861            "a click does not flash a ring"
6862        );
6863
6864        let with_input = Flex::<Msg>::column().child(TextField::new("hi").on_input(Msg::Edited));
6865        assert_eq!(
6866            count_ring(&with_input, true),
6867            0,
6868            "the field draws its own focus"
6869        );
6870    }
6871
6872    #[test]
6873    fn arrow_focus_navigates_geometrically() {
6874        // A 2×2 grid of buttons; each target is identified by its message.
6875        let grid: Flex<Msg> = Flex::column()
6876            .child(
6877                Flex::row()
6878                    .child(Button::new("a").on_press(Msg::A))
6879                    .child(Button::new("b").on_press(Msg::B)),
6880            )
6881            .child(
6882                Flex::row()
6883                    .child(Button::new("c").on_press(Msg::C))
6884                    .child(Button::new("d").on_press(Msg::D)),
6885            );
6886        let ui = build_ui(
6887            &grid,
6888            Size::new(300.0, 200.0),
6889            &Runtime::default(),
6890            &Theme::default(),
6891        );
6892        let top_left = ui.focus_next(None, true).expect("premier focusable");
6893        assert_eq!(ui.msg_for(top_left), Some(Msg::A));
6894
6895        // Right: a → b; down: a → c; and nothing to the left of a.
6896        let right = ui
6897            .focus_directional(top_left, FocusDirection::Right)
6898            .expect("to the right");
6899        assert_eq!(ui.msg_for(right), Some(Msg::B));
6900        let down = ui
6901            .focus_directional(top_left, FocusDirection::Down)
6902            .expect("downwards");
6903        assert_eq!(ui.msg_for(down), Some(Msg::C));
6904        assert_eq!(ui.focus_directional(top_left, FocusDirection::Left), None);
6905        // The diagonal is kept in check: from b, down → d (aligned), not c.
6906        let down_right = ui
6907            .focus_directional(right, FocusDirection::Down)
6908            .expect("down from b");
6909        assert_eq!(ui.msg_for(down_right), Some(Msg::D));
6910    }
6911
6912    #[test]
6913    fn keyed_identity_survives_middle_removal() {
6914        let colored = |c: Color| Container::<Msg>::new().width(50.0).height(20.0).color(c);
6915        let red = Color::rgb(1.0, 0.0, 0.0);
6916        let green = Color::rgb(0.0, 1.0, 0.0);
6917        let blue = Color::rgb(0.0, 0.0, 1.0);
6918
6919        let owner_of = |ui: &Ui<Msg>, c: Color| -> u64 {
6920            ui.scene()
6921                .primitives()
6922                .iter()
6923                .find_map(|p| match p {
6924                    Primitive::Rect { color, owner, .. } if *color == c => Some(*owner),
6925                    _ => None,
6926                })
6927                .expect("the primitive is there")
6928        };
6929
6930        // The list [red(key 1), green(key 2), blue(key 3)].
6931        let full = Flex::<Msg>::column()
6932            .child(Keyed::new(1u64, colored(red)))
6933            .child(Keyed::new(2u64, colored(green)))
6934            .child(Keyed::new(3u64, colored(blue)));
6935        let ui_full = build_ui(
6936            &full,
6937            Size::new(200.0, 200.0),
6938            &Runtime::default(),
6939            &Theme::default(),
6940        );
6941
6942        // The list [red(1), blue(3)]: the green in the middle is removed → blue goes from index 2 to 1.
6943        let removed = Flex::<Msg>::column()
6944            .child(Keyed::new(1u64, colored(red)))
6945            .child(Keyed::new(3u64, colored(blue)));
6946        let ui_removed = build_ui(
6947            &removed,
6948            Size::new(200.0, 200.0),
6949            &Runtime::default(),
6950            &Theme::default(),
6951        );
6952
6953        // Blue's identity (owner, key 3) is UNCHANGED despite the shift in position.
6954        assert_eq!(owner_of(&ui_full, blue), owner_of(&ui_removed, blue));
6955
6956        // Without a key, the 2nd child's positional identity DOES change (index 2 vs 1).
6957        let unkeyed_full = Flex::<Msg>::column()
6958            .child(colored(red))
6959            .child(colored(green))
6960            .child(colored(blue));
6961        let unkeyed_removed = Flex::<Msg>::column()
6962            .child(colored(red))
6963            .child(colored(blue));
6964        let u1 = build_ui(
6965            &unkeyed_full,
6966            Size::new(200.0, 200.0),
6967            &Runtime::default(),
6968            &Theme::default(),
6969        );
6970        let u2 = build_ui(
6971            &unkeyed_removed,
6972            Size::new(200.0, 200.0),
6973            &Runtime::default(),
6974            &Theme::default(),
6975        );
6976        assert_ne!(owner_of(&u1, blue), owner_of(&u2, blue));
6977    }
6978
6979    #[test]
6980    fn center_overlay_scrim_click_dismisses() {
6981        // A Center modal with `.dismiss`: clicking the scrim (outside the content) returns the
6982        // dismissal message; clicking the content does not.
6983        let modal = Container::<Msg>::new()
6984            .width(100.0)
6985            .height(60.0)
6986            .color(Color::WHITE);
6987        let portal: OverlayPortal<Msg> =
6988            OverlayPortal::new(Container::<Msg>::new().width(20.0).height(20.0))
6989                .overlay(modal, Placement::Center)
6990                .dismiss(Msg::A);
6991        let ui = build_ui(
6992            &portal,
6993            Size::new(400.0, 300.0),
6994            &Runtime::default(),
6995            &Theme::default(),
6996        );
6997
6998        // The top-left corner: on the scrim → it dismisses.
6999        let corner = ui.hit(Point::new(5.0, 5.0)).expect("a clickable scrim");
7000        assert_eq!(ui.msg_for(corner), Some(Msg::A));
7001    }
7002
7003    /// **The shell's mark reaches the field** (milestone 511): a focused field whose
7004    /// selection the runtime marks as a touch one paints its two handles; the same
7005    /// selection unmarked paints none, and so does a marked field that lost the focus.
7006    #[test]
7007    fn a_touch_selection_reaches_the_field_as_handles() {
7008        let tree = Flex::column().width(300.0).height(80.0).child(
7009            TextField::new("hello world")
7010                .width(200.0)
7011                .on_input(Msg::Edited),
7012        );
7013        let theme = Theme::default();
7014        let size = Size::new(300.0, 80.0);
7015        let (id, _) = build_ui(&tree, size, &Runtime::default(), &theme)
7016            .focus_hit(Point::new(10.0, 10.0))
7017            .expect("the field");
7018        let handles = |focused: bool, marked: bool| {
7019            let mut rt = Runtime::default();
7020            rt.edits.insert(
7021                id,
7022                Edit {
7023                    cursor: 11,
7024                    anchor: Some(6),
7025                    composing: None,
7026                },
7027            );
7028            rt.input.focused = focused.then_some(id);
7029            rt.selection_handles = marked.then_some(id);
7030            let ui = build_ui(&tree, size, &rt, &theme);
7031            ui.scene()
7032                .primitives()
7033                .iter()
7034                .filter(|p| {
7035                    matches!(p, Primitive::Path { fill: Some(c), .. } if *c == theme.scheme.primary)
7036                })
7037                .count()
7038        };
7039        assert_eq!(handles(true, true), 2);
7040        assert_eq!(handles(true, false), 0, "a selection made with a mouse");
7041        assert_eq!(handles(false, true), 0, "a field left behind");
7042    }
7043
7044    /// A wrapper that fuses with its field shares its identity, so it is the wrapper the
7045    /// shell asks for the handles — and a wrapper that forgot to pass the question on
7046    /// would leave a finger nothing to take (the three silent bugs `Responsive` has
7047    /// already cost, milestones 477 to 495).
7048    #[test]
7049    fn the_wrappers_that_fuse_with_a_field_pass_its_handles_on() {
7050        let field = || TextField::<Msg>::new("hello world").width(200.0);
7051        let edit = Edit {
7052            cursor: 11,
7053            anchor: Some(6),
7054            composing: None,
7055        };
7056        let direct = Widget::<Msg>::selection_handles(&field(), 200.0, &edit, 0.0);
7057        assert!(direct.is_some());
7058        let keyed = crate::Keyed::new(1, field());
7059        let responsive = crate::Responsive::new(crate::SizeClass::Compact).compact(field());
7060        let boxed: Box<dyn Widget<Msg>> = Box::new(field());
7061        assert_eq!(keyed.selection_handles(200.0, &edit, 0.0), direct);
7062        assert_eq!(responsive.selection_handles(200.0, &edit, 0.0), direct);
7063        assert_eq!(boxed.selection_handles(200.0, &edit, 0.0), direct);
7064    }
7065
7066    /// **A field knows the form it is part of** (milestone 512): the stops inside one
7067    /// [`crate::AutofillGroup`] are that form, in tree order, whichever of them asks; two
7068    /// groups side by side are two forms; a field in no group is a form of one; and a
7069    /// group sits in the focus registry, so a cached subtree replays it like any stop.
7070    #[test]
7071    fn a_field_knows_the_form_it_is_part_of() {
7072        use crate::{AutofillGroup, AutofillHint};
7073        let field = |value: &str, hint: AutofillHint| {
7074            TextField::new(value)
7075                .width(200.0)
7076                .on_input(Msg::Edited)
7077                .autofill([hint])
7078        };
7079        let tree = Flex::column()
7080            .width(300.0)
7081            .height(400.0)
7082            .child(AutofillGroup::new(
7083                Flex::column()
7084                    .child(field("someone", AutofillHint::Username))
7085                    .child(field("secret", AutofillHint::Password)),
7086            ))
7087            .child(AutofillGroup::new(field(
7088                "123456",
7089                AutofillHint::OneTimeCode,
7090            )))
7091            .child(TextField::new("search").width(200.0).on_input(Msg::Edited));
7092        let ui = build_ui(
7093            &tree,
7094            Size::new(300.0, 400.0),
7095            &Runtime::default(),
7096            &Theme::default(),
7097        );
7098        let ids: Vec<WidgetId> = ui.focusable_ids().collect();
7099        assert_eq!(ids.len(), 4);
7100        let form = |id: WidgetId| ui.form_of(id).iter().map(|(f, _)| *f).collect::<Vec<_>>();
7101        assert_eq!(form(ids[0]), vec![ids[0], ids[1]], "the sign-in pair");
7102        assert_eq!(
7103            form(ids[1]),
7104            vec![ids[0], ids[1]],
7105            "whichever of the two asks"
7106        );
7107        assert_eq!(
7108            form(ids[2]),
7109            vec![ids[2]],
7110            "a second group is a second form"
7111        );
7112        assert_eq!(
7113            form(ids[3]),
7114            vec![ids[3]],
7115            "a field in no group is a form of one"
7116        );
7117        assert!(ui.form_of(WidgetId::from_u64(7)).is_empty());
7118        // The two groups are told apart by identity, and a lone field has none.
7119        let (first, second) = (ui.form_group(ids[0]), ui.form_group(ids[2]));
7120        assert!(first.is_some() && second.is_some() && first != second);
7121        assert_eq!(ui.form_group(ids[3]), None);
7122        assert_eq!(ui.group_stops(first.unwrap()).len(), 2);
7123        // Each stop carries its box, which is what the platform anchors its offer to.
7124        assert!(ui.form_of(ids[1]).iter().all(|(_, r)| r.height > 0.0));
7125    }
7126
7127    #[test]
7128    fn find_widget_and_edit_types() {
7129        let tree = Flex::column()
7130            .width(300.0)
7131            .height(80.0)
7132            .child(TextField::new("hi").width(200.0).on_input(Msg::Edited));
7133        let rt = Runtime::default();
7134        let ui = build_ui(&tree, Size::new(300.0, 80.0), &rt, &Theme::default());
7135        let (id, _rect) = ui.focus_hit(Point::new(10.0, 10.0)).expect("the field");
7136
7137        let widget = find_widget(&tree, id).expect("the widget was found");
7138        let mut edit = Edit {
7139            cursor: 2,
7140            anchor: None,
7141            composing: None,
7142        };
7143        assert_eq!(
7144            widget.on_edit(&mut edit, &Key::Text("!".to_string())),
7145            Some(Msg::Edited("hi!".to_string()))
7146        );
7147    }
7148
7149    #[test]
7150    fn a_traversal_can_arrive_before_the_layout_pass() {
7151        // Milestone 415. A `ThemeBuilder`'s subtree does not exist until something has
7152        // called `build_themed` on it, and `children()` says so in its own doc: "a
7153        // traversal that arrives first should call `build_themed`, not this".
7154        //
7155        // The shell has such a traversal. On a **burst of keystrokes** — a software
7156        // keyboard, `adb input text`, auto-repeat — it rebuilds the tree from `view` alone
7157        // and reads it immediately, with no layout pass in between, so every deferred
7158        // subtree in it is empty and every widget inside one is unreachable.
7159        let deferred = |value: &str| {
7160            let value = value.to_string();
7161            crate::ThemeBuilder::new(move |_: &Theme| {
7162                TextField::new(value.clone())
7163                    .width(200.0)
7164                    .on_input(Msg::Edited)
7165            })
7166        };
7167        let laid_out = Flex::column()
7168            .width(300.0)
7169            .height(80.0)
7170            .child(deferred("hi"));
7171        let rt = Runtime::default();
7172        let ui = build_ui(&laid_out, Size::new(300.0, 80.0), &rt, &Theme::default());
7173        let (id, _) = ui.focus_hit(Point::new(10.0, 10.0)).expect("the field");
7174        assert!(
7175            find_widget(&laid_out, id).is_some(),
7176            "after a layout pass the field is where the hit-test said it was"
7177        );
7178
7179        // The same tree, freshly built and not yet laid out. `build_deferred` is what the
7180        // layout pass does on its way down, and nothing else.
7181        let fresh = Flex::column()
7182            .width(300.0)
7183            .height(80.0)
7184            .child(deferred("hi"));
7185        build_deferred(&fresh, &Theme::default(), &Runtime::default());
7186        assert!(
7187            find_widget(&fresh, id).is_some(),
7188            "prepared the same way, the same traversal reaches the same widget"
7189        );
7190    }
7191
7192    #[test]
7193    #[should_panic(expected = "before it was built")]
7194    fn reading_an_unbuilt_deferred_subtree_trips_the_wire() {
7195        // Without the preparation the traversal used to return `None` and the caller
7196        // shrugged: no field found, no caret revealed, nothing said. The failure mode was
7197        // **silence**, so the guard is a panic in debug rather than a comment.
7198        let fresh =
7199            Flex::<Msg>::column()
7200                .width(300.0)
7201                .height(80.0)
7202                .child(crate::ThemeBuilder::new(|_: &Theme| {
7203                    Container::<Msg>::new().width(10.0).height(10.0)
7204                }));
7205        let _ = find_widget(&fresh, WidgetId::ROOT.child(9));
7206    }
7207
7208    #[test]
7209    fn a_deferred_subtree_is_built_under_its_own_theme() {
7210        // The layout pass swaps the theme before it builds (`theme_override`), so a
7211        // builder inside a `Themed` sees the subtree's theme and not the frame's.
7212        // `build_deferred` has to make the same swap or it would build the wrong subtree
7213        // — and being *nearly* the walk is worse than not being it at all.
7214        let seen = std::rc::Rc::new(std::cell::Cell::new(0.0f32));
7215        let recorder = {
7216            let seen = seen.clone();
7217            crate::ThemeBuilder::new(move |theme: &Theme| {
7218                seen.set(theme.radius);
7219                Container::<Msg>::new().width(10.0).height(10.0)
7220            })
7221        };
7222        let inner = Theme {
7223            radius: 99.0,
7224            ..Theme::default()
7225        };
7226        let tree = Flex::<Msg>::column().child(crate::Themed::new(inner, recorder));
7227        build_deferred(&tree, &Theme::default(), &Runtime::default());
7228        assert_eq!(seen.get(), 99.0, "the subtree's theme, not the frame's");
7229    }
7230
7231    #[test]
7232    fn find_by_key_resolves_a_keyed_field_to_its_focus_id() {
7233        // Two named fields: `find_by_key` finds each one's focus identity (the one the
7234        // hit-test would assign), and it tells the keys apart.
7235        fn hash(k: &str) -> u64 {
7236            use std::hash::{Hash, Hasher};
7237            let mut h = std::collections::hash_map::DefaultHasher::new();
7238            k.hash(&mut h);
7239            h.finish()
7240        }
7241        let tree: Flex<Msg> = Flex::column()
7242            .child(Keyed::new(
7243                "email",
7244                TextField::new("").on_input(Msg::Edited),
7245            ))
7246            .child(Keyed::new(
7247                "password",
7248                TextField::new("").on_input(Msg::Edited),
7249            ));
7250
7251        let email = find_by_key(&tree, hash("email")).expect("email was found");
7252        let password = find_by_key(&tree, hash("password")).expect("password was found");
7253        assert_ne!(email, password, "distinct keys → distinct identities");
7254        // The identity resolved is indeed the by-key identity (stable, independent of the
7255        // position).
7256        assert_eq!(email, WidgetId::ROOT.keyed(hash("email")));
7257        assert!(
7258            find_by_key(&tree, hash("absent")).is_none(),
7259            "an unknown key → None"
7260        );
7261
7262        // And above all: it is **exactly** the identity the focus hit-test would assign to
7263        // that field — so setting this focus really does route to it.
7264        let rt = Runtime::default();
7265        let ui = build_ui(&tree, Size::new(300.0, 200.0), &rt, &Theme::default());
7266        let (hit_id, _) = ui
7267            .focus_hit(Point::new(10.0, 10.0))
7268            .expect("the email field under the cursor");
7269        assert_eq!(email, hit_id, "find_by_key == the field's focus identity");
7270    }
7271
7272    #[test]
7273    fn ink_is_painted_over_the_surface_and_under_its_child() {
7274        use crate::ink::InkWell;
7275        let tree = Container::<Msg>::new().width(120.0).height(60.0).child(
7276            InkWell::<Msg>::new().radius(8.0).on_click(Msg::A).child(
7277                Container::<Msg>::new()
7278                    .width(120.0)
7279                    .height(60.0)
7280                    .color(frus_core::Color::WHITE),
7281            ),
7282        );
7283        let size = Size::new(120.0, 60.0);
7284
7285        // Dry: the well paints nothing of its own.
7286        let rt = Runtime::default();
7287        let dry = build_ui(&tree, size, &rt, &Theme::default());
7288        let plain = dry.scene.len();
7289        let id = dry.inks.first().expect("the well registered its box").0;
7290        assert_eq!(
7291            dry.ink_box(id).map(|r| (r.width, r.height)),
7292            Some((120.0, 60.0)),
7293            "the registry carries the **whole** box, which is what sizes the splash"
7294        );
7295
7296        // A finger lands in the top-left corner, and is still down.
7297        let mut rt = Runtime::default();
7298        rt.input.pressed = Some(id);
7299        rt.ink_press(id, Point::new(10.0, 10.0), Size::new(120.0, 60.0));
7300        rt.advance_ink(1.0 / 60.0);
7301        let ui = build_ui(&tree, size, &rt, &Theme::default());
7302        assert_eq!(ui.scene.len(), plain + 1, "the splash adds one layer");
7303
7304        // It is a shape-clipped layer, so the ink cannot escape the rounded corners…
7305        let (index, clip_shape) = ui
7306            .scene
7307            .primitives()
7308            .iter()
7309            .enumerate()
7310            .find_map(|(i, p)| match p {
7311                frus_core::Primitive::Layer { clip_shape, .. } => Some((i, clip_shape.clone())),
7312                _ => None,
7313            })
7314            .expect("the ink is a composited layer");
7315        assert_eq!(clip_shape, frus_core::ClipShape::RRect(8.0.into()));
7316
7317        // …and it is painted **before** the child that sits on top of it: the ink is
7318        // under the content, the way a material surface holds it.
7319        let child_index = ui
7320            .scene
7321            .primitives()
7322            .iter()
7323            .rposition(|p| matches!(p, frus_core::Primitive::Rect { .. }))
7324            .expect("the child painted a rectangle");
7325        assert!(
7326            index < child_index,
7327            "ink at {index} must come before the content at {child_index}"
7328        );
7329    }
7330
7331    #[test]
7332    fn a_splash_left_unconfirmed_does_not_wait_for_ever() {
7333        // The finger came down and never came back up on this widget — it slid off, or
7334        // the widget went away mid-press. Nothing will ever confirm the splash, and
7335        // without the sweep in `advance_ink` the ink would sit there for good.
7336        let mut rt = Runtime::default();
7337        let id = WidgetId::from_u64(42);
7338        rt.input.pressed = Some(id);
7339        rt.ink_press(id, Point::new(5.0, 5.0), Size::new(100.0, 40.0));
7340        // Held for a fifth of a second: the ink is up, and it stays up.
7341        for _ in 0..12 {
7342            rt.advance_ink(1.0 / 60.0);
7343        }
7344        assert!(rt.ink.contains_key(&id), "the splash waits for the finger");
7345
7346        // The finger is gone and nothing confirmed the tap.
7347        rt.input.pressed = None;
7348        let mut frames = 0;
7349        while rt.advance_ink(1.0 / 60.0) && frames < 200 {
7350            frames += 1;
7351        }
7352        assert!(rt.ink.is_empty(), "the ink is gone after {frames} frames");
7353        assert!(frames <= 6, "and quickly: a cancel is 75 ms, took {frames}");
7354    }
7355
7356    #[test]
7357    fn an_overscroll_glow_is_painted_over_its_scroll_area() {
7358        let tree = SingleChildScrollView::<Msg>::new()
7359            .width(200.0)
7360            .height(100.0)
7361            .child(Container::<Msg>::new().width(100.0).height(400.0));
7362        let size = Size::new(200.0, 100.0);
7363
7364        // Quiet: nothing extra is drawn.
7365        let rt = Runtime::default();
7366        let plain = build_ui(&tree, size, &rt, &Theme::default()).scene.len();
7367
7368        // A fling has just landed on the bottom edge.
7369        let mut rt = Runtime::default();
7370        let id = build_ui(&tree, size, &rt, &Theme::default()).scroll_regions()[0].id;
7371        rt.glow_absorb(id, crate::overscroll::GlowEdge::Bottom, 4000.0);
7372        rt.advance_glow(0.02);
7373        let ui = build_ui(&tree, size, &rt, &Theme::default());
7374        assert_eq!(
7375            ui.scene.len(),
7376            plain + 1,
7377            "the glow adds exactly one shape to the frame"
7378        );
7379        // The glow sits under the scrollbars, so it is not the last primitive: it is
7380        // the only filled path in the frame.
7381        let clip = ui
7382            .scene
7383            .primitives()
7384            .iter()
7385            .find_map(|p| match p {
7386                frus_core::Primitive::Path { clip, .. } => Some(*clip),
7387                _ => None,
7388            })
7389            .expect("the glow is a filled path");
7390        // Drawn against the bottom of the viewport, and inside it.
7391        assert!(
7392            (clip.y + clip.height - 100.0).abs() < 1.0,
7393            "clip = {clip:?}"
7394        );
7395        assert!(clip.width <= 200.0 + 1e-3);
7396    }
7397
7398    #[test]
7399    fn a_scroll_area_carries_its_physics_into_the_registry() {
7400        let content = Container::<Msg>::new().width(100.0).height(400.0);
7401        // Unset: the region says nothing and the application decides.
7402        let plain = SingleChildScrollView::new()
7403            .width(200.0)
7404            .height(100.0)
7405            .child(content);
7406        let rt = Runtime::default();
7407        let ui = build_ui(&plain, Size::new(200.0, 100.0), &rt, &Theme::default());
7408        assert_eq!(ui.scroll_regions()[0].physics, None);
7409        assert_eq!(
7410            ui.scroll_regions()[0].physics_or(ScrollPhysics::Clamping),
7411            ScrollPhysics::Clamping,
7412            "an area with no opinion follows the application"
7413        );
7414
7415        // Set: the area's own choice wins over the application's.
7416        let bouncy = SingleChildScrollView::new()
7417            .width(200.0)
7418            .height(100.0)
7419            .physics(ScrollPhysics::BOUNCING)
7420            .child(Container::<Msg>::new().width(100.0).height(400.0));
7421        let ui = build_ui(&bouncy, Size::new(200.0, 100.0), &rt, &Theme::default());
7422        let area = ui.scroll_regions()[0];
7423        assert_eq!(area.physics, Some(ScrollPhysics::BOUNCING));
7424        assert_eq!(
7425            area.physics_or(ScrollPhysics::Clamping),
7426            ScrollPhysics::BOUNCING
7427        );
7428        // And the metrics it hands the physics describe the right axis.
7429        let metrics = area.metrics_y(10.0);
7430        assert_eq!(metrics.pixels, 10.0);
7431        assert_eq!(metrics.max, 300.0); // 400 of content in a 100 viewport
7432        assert_eq!(metrics.viewport, 100.0);
7433    }
7434
7435    #[test]
7436    fn scroll_translates_and_clips_content() {
7437        let content = Flex::<Msg>::column()
7438            .gap(0.0)
7439            .child(
7440                Container::new()
7441                    .height(60.0)
7442                    .color(Color::rgb(1.0, 0.0, 0.0)),
7443            )
7444            .child(
7445                Container::new()
7446                    .height(60.0)
7447                    .color(Color::rgb(0.0, 1.0, 0.0)),
7448            )
7449            .child(
7450                Container::new()
7451                    .height(60.0)
7452                    .color(Color::rgb(0.0, 0.0, 1.0)),
7453            );
7454        let tree = SingleChildScrollView::new()
7455            .width(200.0)
7456            .height(100.0)
7457            .child(content);
7458
7459        let rt = Runtime::default();
7460        let ui = build_ui(&tree, Size::new(200.0, 100.0), &rt, &Theme::default());
7461        let area = ui.scrollables[0];
7462        let sid = area.id;
7463        assert_eq!(area.max_y, 80.0); // 180 - 100
7464        assert_eq!(area.max_x, 0.0);
7465        assert_eq!(ui.first_rect().0.y, 0.0);
7466        assert_eq!(ui.first_rect().1, Rect::new(0.0, 0.0, 200.0, 100.0));
7467
7468        let mut rt = Runtime::default();
7469        rt.scroll.insert(sid, (0.0, 50.0));
7470        let ui2 = build_ui(&tree, Size::new(200.0, 100.0), &rt, &Theme::default());
7471        assert_eq!(ui2.first_rect().0.y, -50.0);
7472    }
7473
7474    #[test]
7475    fn kanban_cards_are_reorderable_without_being_clickable() {
7476        use crate::Kanban;
7477        let board = Kanban::new(|_, _, _, _| Msg::A).column("To do", ["Card A"]);
7478        let rt = Runtime::default();
7479        let ui = build_ui(&board, Size::new(400.0, 300.0), &rt, &Theme::default());
7480        // The card **and** the drop zone are registered as reorderables.
7481        assert!(
7482            ui.reorderables.len() >= 2,
7483            "the card + the drop zone are in the reorderables registry"
7484        );
7485        // A point on the card can be **picked up** (it is reorderable) but is **not clickable**
7486        // — which is the whole point of the registry: without it, `ui.hit` alone would never
7487        // find the card.
7488        let (card_id, card) = ui.reorderables[0];
7489        let p = Point::new(card.x + 5.0, card.y + 5.0);
7490        assert!(
7491            ui.reorderable_at(p).is_some(),
7492            "the card can be picked up at that point"
7493        );
7494        assert!(
7495            ui.hit(p).is_none(),
7496            "…but it is not clickable (absent from the hit registry)"
7497        );
7498        // `widget_rect` must find the card **through the reorderable fallback** (it is not
7499        // focusable): otherwise the shell's vertical drag preview never starts.
7500        assert_eq!(
7501            ui.widget_rect(card_id),
7502            Some(card),
7503            "widget_rect falls back to the reorderables registry"
7504        );
7505    }
7506
7507    #[test]
7508    fn reorderables_inside_a_scroll_are_still_registered() {
7509        // The milestone 258/260 scenario: the board (with its reorderable cards) is **wrapped
7510        // in a `SingleChildScrollView`**. The reorderables must stay registered — otherwise dragging a card
7511        // stops engaging as soon as the board scrolls.
7512        use crate::{Axis, Kanban, SingleChildScrollView};
7513        let board = Kanban::new(|_, _, _, _| Msg::A).column("To do", ["Card A"]);
7514        let scrolled = SingleChildScrollView::new()
7515            .axis(Axis::Horizontal)
7516            .width(400.0)
7517            .height(300.0)
7518            .child(board);
7519        let rt = Runtime::default();
7520        let ui = build_ui(&scrolled, Size::new(400.0, 300.0), &rt, &Theme::default());
7521        assert!(
7522            ui.reorderables.len() >= 2,
7523            "the card + the drop zone stay reorderable even inside a SingleChildScrollView (got {})",
7524            ui.reorderables.len()
7525        );
7526    }
7527
7528    #[test]
7529    fn reorderables_inside_a_per_column_card_scroll_are_still_registered() {
7530        // Milestone 264: `card_area_height` puts each column's cards inside a **vertical
7531        // `SingleChildScrollView` with an explicit height**. This is exactly the case that *collapsed* at
7532        // milestone 263 (a flex scroll with no defined ancestor height → cards clipped to zero
7533        // and no longer reorderable). With a **defined** height the visible cards must stay
7534        // registered as reorderables — a guard against a regression of per-column dragging.
7535        use crate::Kanban;
7536        let board = Kanban::new(|_, _, _, _| Msg::A)
7537            .card_area_height(220.0)
7538            .column("To do", ["Card A", "Card B"]);
7539        let rt = Runtime::default();
7540        let ui = build_ui(&board, Size::new(400.0, 300.0), &rt, &Theme::default());
7541        assert!(
7542            ui.reorderables.len() >= 3,
7543            "2 cards + the drop zone stay reorderable inside a vertical scroll with a defined height \
7544             (got {})",
7545            ui.reorderables.len()
7546        );
7547    }
7548
7549    #[test]
7550    fn scrollable_columns_fill_the_board_height_then_scroll() {
7551        // Milestone 266: `Kanban::scrollable_columns()` makes the columns **fill** the board's
7552        // height (laid out in an ancestor with a defined height), and each column scrolls its
7553        // cards vertically **with no explicit height** (the flex does the arithmetic, so the
7554        // milestone 264 stopgap is gone). This checks that at least one column has a vertical
7555        // `SingleChildScrollView` whose viewport **fills** the height (far more than the default 200) and
7556        // **scrolls** (max_y > 0), which is the proof of fill-then-scroll.
7557        use crate::{Axis, Container, Flex, Kanban, SingleChildScrollView};
7558        // Enough cards to **overflow** the filled viewport (otherwise max_y = 0: nothing to scroll).
7559        let long: Vec<String> = (0..24).map(|i| format!("card {i}")).collect();
7560        let board = Kanban::new(|_, _, _, _| Msg::A)
7561            .scrollable_columns()
7562            .column("To do", long);
7563        // `board_screen`'s nesting: the board inside a **plain `Container` with padding** (the
7564        // visual margin), itself inside a horizontal `flex(1)` SingleChildScrollView, inside a screen (a Flex
7565        // column) of bounded height. That `Auto` `Container` **collapsed** at milestone 266
7566        // (hence the `Flex` `flex(1)` workaround); since `compute_scroll` **fills the
7567        // constrained axis**, it fills the viewport's height and the board follows — no filler
7568        // container needed any more.
7569        let padded = Container::<Msg>::new().padding(24.0).child(board);
7570        let scroll_h = SingleChildScrollView::new()
7571            .axis(Axis::Horizontal)
7572            .width(360.0)
7573            .flex(1.0)
7574            .child(padded);
7575        let root: Flex<Msg> = Flex::column().width(400.0).height(600.0).child(scroll_h);
7576        let ui = build_ui(
7577            &root,
7578            Size::new(400.0, 600.0),
7579            &Runtime::default(),
7580            &Theme::default(),
7581        );
7582        // A column's **vertical** scroll: a tall viewport (it fills) and it scrolls.
7583        let filled = ui
7584            .scrollables
7585            .iter()
7586            .any(|area| area.viewport.height > 300.0 && area.max_y > 0.0);
7587        assert!(
7588            filled,
7589            "a column fills the board's height then scrolls (scrollables: {:?})",
7590            ui.scrollables
7591        );
7592    }
7593
7594    #[test]
7595    fn subtree_ids_covers_a_widget_and_its_descendants() {
7596        use crate::Text;
7597        // Root (Container) > Flex(column) > [Text, Text].
7598        let tree: Container<Msg> = Container::new().child(
7599            Flex::<Msg>::column()
7600                .child(Text::new("a"))
7601                .child(Text::new("b")),
7602        );
7603        let all = collect_ids(&tree);
7604        // From the root: identical to `collect_ids` (the same positional walk).
7605        assert_eq!(subtree_ids(&tree, WidgetId::ROOT), all);
7606        // A child's subtree starts with its own identity and stays a **strict** subset of the
7607        // tree (which is how a rich card's ghost captures all of its content).
7608        let flex_id = all[1];
7609        let sub = subtree_ids(Widget::children(&tree)[0].as_ref(), flex_id);
7610        assert_eq!(
7611            sub[0], flex_id,
7612            "the subtree starts with the identity supplied"
7613        );
7614        assert!(
7615            sub.len() < all.len() && sub.iter().all(|i| all.contains(i)),
7616            "a subset of the tree including the descendants"
7617        );
7618    }
7619
7620    impl<Msg: Clone> Ui<Msg> {
7621        fn first_rect(&self) -> (Rect, Rect) {
7622            for primitive in self.scene.primitives() {
7623                if let Primitive::Rect { rect, clip, .. } = primitive {
7624                    return (*rect, *clip);
7625                }
7626            }
7627            panic!("no rect at all");
7628        }
7629    }
7630
7631    /// A static subtree under `RepaintBoundary` (mixed content: text + boxes) — enough to
7632    /// produce several primitives.
7633    fn boundary_tree() -> Container<Msg> {
7634        use crate::Text;
7635        Container::new().repaint_boundary().child(
7636            Flex::<Msg>::column()
7637                .child(Text::new("Statique"))
7638                .child(Container::new().width(30.0).height(20.0).on_click(Msg::A))
7639                .child(Button::new("ok").on_press(Msg::B)),
7640        )
7641    }
7642
7643    #[test]
7644    fn repaint_boundary_reuses_a_static_subtree_bit_identical() {
7645        let tree = boundary_tree();
7646        let size = Size::new(200.0, 200.0);
7647        let theme = Theme::default();
7648        let rt = Runtime::default();
7649
7650        // Frame 1: nothing cached → 1 miss (a full paint), the subtree is captured.
7651        let ui1 = build_ui(&tree, size, &rt, &theme);
7652        // The **primitives** are what is compared (the scene's transient ambient state — the
7653        // current clip/owner — is not rendered).
7654        let dbg1 = format!("{:?}", ui1.scene().primitives());
7655        assert_eq!(rt.paint_cache.borrow().last_frame_stats(), (0, 1));
7656
7657        // Frame 2: the same generation + the same state → the boundary is reused.
7658        let ui2 = build_ui(&tree, size, &rt, &theme);
7659        let dbg2 = format!("{:?}", ui2.scene().primitives());
7660        assert_eq!(
7661            rt.paint_cache.borrow().last_frame_stats(),
7662            (1, 0),
7663            "the boundary is reused"
7664        );
7665        assert_eq!(
7666            dbg1, dbg2,
7667            "the replayed scene is bit-for-bit identical to the full repaint"
7668        );
7669        // The interaction maps are replayed too (a click stays routable).
7670        assert_eq!(ui1.hits.len(), ui2.hits.len());
7671        assert!(!ui2.hits.is_empty(), "the subtree's hit is indeed replayed");
7672    }
7673
7674    #[test]
7675    fn repaint_boundary_invalidated_by_generation_bump() {
7676        let tree = boundary_tree();
7677        let size = Size::new(200.0, 200.0);
7678        let theme = Theme::default();
7679        let rt = Runtime::default();
7680
7681        build_ui(&tree, size, &rt, &theme); // frame 1 : miss + capture
7682
7683        // The `view` is rebuilt (the config may have changed).
7684        rt.paint_cache.borrow_mut().bump_generation();
7685        build_ui(&tree, size, &rt, &theme); // frame 2
7686        assert_eq!(
7687            rt.paint_cache.borrow().last_frame_stats(),
7688            (0, 1),
7689            "a stale generation → a full repaint"
7690        );
7691    }
7692
7693    #[test]
7694    fn repaint_boundary_invalidated_by_interaction_change() {
7695        let tree = boundary_tree();
7696        let size = Size::new(200.0, 200.0);
7697        let theme = Theme::default();
7698        let mut rt = Runtime::default();
7699
7700        build_ui(&tree, size, &rt, &theme); // frame 1 : miss + capture
7701
7702        // No rebuild, but a descendant's interaction state changes (the boundary's animated
7703        // hover) → the fingerprint differs → a repaint.
7704        rt.anims.insert(
7705            WidgetId::ROOT,
7706            crate::Anim {
7707                hover: 0.5,
7708                ..Default::default()
7709            },
7710        );
7711        build_ui(&tree, size, &rt, &theme); // frame 2
7712        assert_eq!(
7713            rt.paint_cache.borrow().last_frame_stats(),
7714            (0, 1),
7715            "the interaction state changed → a full repaint"
7716        );
7717
7718        // Once the state has settled, the boundary is reused again.
7719        build_ui(&tree, size, &rt, &theme); // frame 3
7720        assert_eq!(rt.paint_cache.borrow().last_frame_stats(), (1, 0));
7721    }
7722}