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/// Thickness of a scrollbar, in pixels (`scrollbar.dart:12`).
31const BAR_SIZE: f32 = 8.0;
32/// Minimum length of a thumb (`scrollbar.dart:15`). It is the tap target: a thumb has to
33/// be catchable by a pointer, whatever fraction of the content the viewport shows.
34const MIN_THUMB: f32 = 48.0;
35/// The thumb's opacity at rest, over `on_surface` (`scrollbar.dart:248`, `:242`). The two
36/// numbers are the reference's, and which one applies follows the surface's own
37/// brightness — this crate's scheme carries no brightness flag, so it is read off the
38/// surface rather than declared.
39/// The gap between the thumb and the edge of the viewport (`scrollbar.dart:14`).
40const BAR_MARGIN: f32 = 2.0;
41const THUMB_ON_DARK: f32 = 0.30;
42const THUMB_ON_LIGHT: f32 = 0.10;
43/// And under a pointer (`scrollbar.dart:245`, `:239`) — a five-fold change on a light
44/// surface, which is what makes a bar you can barely see worth reaching for.
45const THUMB_HOVER_ON_DARK: f32 = 0.65;
46const THUMB_HOVER_ON_LIGHT: f32 = 0.50;
47/// And while it is being dragged (`scrollbar.dart:244`, `:238`), which arrives at once:
48/// the hand is already on it, and a fade would only lag behind the grab.
49const THUMB_DRAG_ON_DARK: f32 = 0.75;
50const THUMB_DRAG_ON_LIGHT: f32 = 0.60;
51/// An Android thumb at rest: the reference's highlight colours, made opaque
52/// (`theme_data.dart:501`, `scrollbar.dart:240`).
53const ANDROID_THUMB_ON_DARK: Color = Color::rgba8(0xCC, 0xCC, 0xCC, 0xFF);
54const ANDROID_THUMB_ON_LIGHT: Color = Color::rgba8(0xBC, 0xBC, 0xBC, 0xFF);
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    /// Whether a pointer can wake and drag it; not under an Android theme, where the bar
102    /// only shows where the page is (milestone 617).
103    pub interactive: bool,
104}
105
106/// The physics an area scrolls with: its own laid over its scroll behaviour's, as the
107/// reference applies a widget's physics to the scroll configuration's (milestone 649).
108fn region_physics<Msg>(widget: &dyn Widget<Msg>, theme: &Theme) -> ScrollPhysics {
109    let platform = crate::ScrollConfiguration::of().scroll_physics(theme);
110    widget
111        .scroll_physics()
112        .map_or(platform, |own| own.apply_to(platform))
113}
114
115/// A scrollable area of the frame: where it is, how far it may scroll, and how it
116/// is meant to behave at its edges.
117///
118/// The registry the shell and the runtime both read. What the area does when it is
119/// dragged, flung or pulled past an edge is settled when it is registered: its own
120/// physics if it asked for some, else what the [`ScrollBehavior`](crate::ScrollBehavior)
121/// in force there says, under the theme it is laid out in (milestone 616).
122#[derive(Copy, Clone, Debug)]
123pub struct Scrollable {
124    /// The scrollable area's identity.
125    pub id: WidgetId,
126    /// The viewport, in absolute coordinates.
127    pub viewport: Rect,
128    /// Largest horizontal offset the content may rest at (0 = nothing to scroll).
129    pub max_x: f32,
130    /// Largest vertical offset the content may rest at.
131    pub max_y: f32,
132    /// The axis the area scrolls along (milestone 649): what an area that is always
133    /// scrollable takes a drag along when nothing overflows.
134    pub axis: crate::scroll::Axis,
135    /// How the area moves at its edges and after a fling: its own, when it asked for
136    /// some, else its scroll behaviour's.
137    pub physics: ScrollPhysics,
138    /// What the area shows when pulled past an edge it will not move beyond, from its
139    /// scroll behaviour; `None` for nothing.
140    pub overscroll: Option<crate::physics::OverscrollIndicator>,
141    /// How a fling on this area is read from the finger's last moves, from its scroll
142    /// behaviour.
143    pub fling: frus_core::VelocityStrategy,
144    /// The [`crate::RefreshIndicator`] area this scrollable sits inside, when there is one.
145    /// Movement refused at its **top** edge feeds that area's pull instead of the
146    /// overscroll glow.
147    pub refresh: Option<WidgetId>,
148    /// Set when this area rests **only on pages** ([`crate::PageView`]): the release
149    /// springs to a page boundary instead of flinging.
150    pub page: Option<PageSnap>,
151    /// Whether the horizontal axis is **reversed** — offset 0 is the right-hand end of
152    /// the content rather than the left.
153    pub reverse_x: bool,
154    /// Whether the vertical axis is **reversed** — offset 0 is the **bottom** of the
155    /// content rather than the top.
156    ///
157    /// Offsets are measured from the end an axis starts at, which is what makes a
158    /// reversed list stay where it is when content arrives: a conversation resting at
159    /// offset 0 is resting at the newest message, and the newest message is wherever the
160    /// end now happens to be. Measuring from the top instead would leave the view
161    /// drifting away from the end every time something was appended, which is the one
162    /// thing this exists to prevent.
163    ///
164    /// Everything the *user* does is unchanged by it. A finger pushes the content the
165    /// way it moves, in either direction; only the arithmetic between that push and the
166    /// number changes sign, in one place.
167    pub reverse_y: bool,
168    /// What the content asked to be kept in view, in the frame's coordinates.
169    pub keep_visible: Option<KeepVisible>,
170    /// The scroll region this one sits **inside**, when there is one.
171    ///
172    /// Scroll areas nest — a sideways strip inside a page that scrolls down — and
173    /// bringing something into view means moving every one of them, from the nearest
174    /// outwards. Identities are hashes and carry no ancestry, so the walk records it.
175    pub host: Option<WidgetId>,
176}
177
178impl Scrollable {
179    /// The tracker a drag on this area reads its fling with.
180    pub fn velocity_tracker(&self) -> frus_core::VelocityTracker {
181        frus_core::VelocityTracker::new(self.fling)
182    }
183
184    /// Whether a finger may move this area at all, resting at `offset`.
185    ///
186    /// The rule the reference states plainly: the user can change the offset **if, and
187    /// only if, there is content outside the viewport to reveal** — or the content is
188    /// already displaced, having shrunk under an offset it no longer reaches. An area that
189    /// fails this takes no drag at all: not a short one, not a refused one, and so no
190    /// end-of-content glow either. An edge effect where there is no edge to meet is a
191    /// statement about the content that is not true.
192    ///
193    /// A [`crate::RefreshIndicator`] listening above is the exception, and the same one the
194    /// reference makes: a list of two items must still pull down to reload.
195    ///
196    /// The physics have the last word (milestone 649): an area the user may never scroll
197    /// takes nothing, and one the user may always scroll takes everything.
198    pub fn accepts_user_offset(&self, offset: (f32, f32)) -> bool {
199        match self.physics.user() {
200            crate::physics::UserScroll::Never => false,
201            crate::physics::UserScroll::Always => true,
202            crate::physics::UserScroll::WhenOverflowing => {
203                self.max_x > 0.0
204                    || self.max_y > 0.0
205                    || offset.0 != 0.0
206                    || offset.1 != 0.0
207                    || self.refresh.is_some()
208            }
209        }
210    }
211
212    /// Whether a movement of the user's along the vertical (`down`) or across is this
213    /// area's to take: there is content that way to reveal, or a pull to refresh above
214    /// for a movement down — unless the physics say never, or always along its axis.
215    pub fn takes_user_scroll(&self, down: bool) -> bool {
216        let overflowing = if down {
217            self.max_y > 0.0 || self.refresh.is_some()
218        } else {
219            self.max_x > 0.0
220        };
221        let along = match self.axis {
222            crate::scroll::Axis::Vertical => down,
223            crate::scroll::Axis::Horizontal => !down,
224            crate::scroll::Axis::Both => true,
225        };
226        match self.physics.user() {
227            crate::physics::UserScroll::Never => false,
228            crate::physics::UserScroll::Always => overflowing || along,
229            crate::physics::UserScroll::WhenOverflowing => overflowing,
230        }
231    }
232
233    /// The horizontal axis as the physics sees it, at `offset`.
234    pub fn metrics_x(&self, offset: f32) -> ScrollMetrics {
235        ScrollMetrics::new(offset, self.max_x, self.viewport.width)
236    }
237
238    /// The vertical axis as the physics sees it, at `offset`.
239    pub fn metrics_y(&self, offset: f32) -> ScrollMetrics {
240        ScrollMetrics::new(offset, self.max_y, self.viewport.height)
241    }
242
243    /// Turns a **screen** delta — how far the content was pushed, in pixels — into the
244    /// change it makes to this area's offsets.
245    ///
246    /// Two sign changes, and both have a reason. The content moves *opposite* the
247    /// number: dragging down reveals what is above, which is a smaller offset. And a
248    /// reversed axis counts from the other end, so the same drag raises it instead.
249    ///
250    /// One function rather than a minus sign at each of the five places a delta becomes
251    /// an offset — the wheel, the drag, the release fling, and both axes of each — so
252    /// that a reversed scroll is right in all of them or in none.
253    pub fn offset_delta(&self, screen: (f32, f32)) -> (f32, f32) {
254        (
255            if self.reverse_x { screen.0 } else { -screen.0 },
256            if self.reverse_y { screen.1 } else { -screen.1 },
257        )
258    }
259
260    /// The edge a refusal happened at, given the refused movement **in offset space**.
261    ///
262    /// A negative refusal means the content was pulled towards the axis's start, and
263    /// where that is on screen is exactly what reversing changes: the start of a
264    /// reversed vertical axis is the **bottom**. Without this the glow would flash at
265    /// the far end of a conversation the moment it refused to go further back.
266    pub fn refused_edge(&self, vertical: bool, refused: f32) -> crate::overscroll::GlowEdge {
267        let reversed = if vertical {
268            self.reverse_y
269        } else {
270            self.reverse_x
271        };
272        crate::overscroll::edge_for(vertical, if reversed { -refused } else { refused })
273    }
274
275    /// The edge an axis **starts** at, which is where a pull-to-refresh listens.
276    pub fn start_edge(&self, vertical: bool) -> crate::overscroll::GlowEdge {
277        self.refused_edge(vertical, -1.0)
278    }
279
280    /// The offset that brings `target` fully inside this viewport, and how far the
281    /// content moves to get there.
282    ///
283    /// The **least** movement that does it: a box already in view does not budge, one
284    /// off the near edge comes to that edge and no further. Centring it instead would
285    /// make Tab through a form scroll on every stop, which is motion nobody asked for.
286    ///
287    /// A target **too big** for the viewport gets its leading edge aligned, which falls
288    /// out of asking about that edge first: a tall field half off the top comes down to
289    /// the top rather than up to the bottom, because its start is what you read.
290    ///
291    /// Both halves are returned because they are not the same number. The offset is
292    /// clamped to what this region can actually do, and an outer region has to be told
293    /// how far the target really moved — not how far it was asked to.
294    pub fn reveal(&self, target: Rect, current: (f32, f32)) -> ((f32, f32), (f32, f32)) {
295        let shift = |lo: f32, hi: f32, near: f32, far: f32| {
296            if near < lo {
297                lo - near
298            } else if far > hi {
299                hi - far
300            } else {
301                0.0
302            }
303        };
304        let wanted = (
305            shift(
306                self.viewport.x,
307                self.viewport.x + self.viewport.width,
308                target.x,
309                target.x + target.width,
310            ),
311            shift(
312                self.viewport.y,
313                self.viewport.y + self.viewport.height,
314                target.y,
315                target.y + target.height,
316            ),
317        );
318        // `offset_delta` turns a movement of the content into a movement of the offset,
319        // and it is its own inverse — it negates or it does not — so the same function
320        // turns the offset that was allowed back into the content movement it buys.
321        let delta = self.offset_delta(wanted);
322        let next = (
323            (current.0 + delta.0).clamp(0.0, self.max_x),
324            (current.1 + delta.1).clamp(0.0, self.max_y),
325        );
326        let applied = (next.0 - current.0, next.1 - current.1);
327        (next, self.offset_delta(applied))
328    }
329
330    /// The offset that puts `target` in the **middle** of this viewport, and how far the
331    /// content moves to get there.
332    ///
333    /// The same pair as [`Scrollable::reveal`], and the same clamp — which is what makes
334    /// the first item of a row rest at the start rather than half off it. A target
335    /// bigger than the viewport centres too, which leaves it hanging over both edges,
336    /// and that is the honest answer: there is no offset that shows all of it.
337    pub fn centre(&self, target: Rect, current: (f32, f32)) -> ((f32, f32), (f32, f32)) {
338        let wanted = (
339            (self.viewport.x + self.viewport.width / 2.0) - (target.x + target.width / 2.0),
340            (self.viewport.y + self.viewport.height / 2.0) - (target.y + target.height / 2.0),
341        );
342        let delta = self.offset_delta(wanted);
343        let next = (
344            (current.0 + delta.0).clamp(0.0, self.max_x),
345            (current.1 + delta.1).clamp(0.0, self.max_y),
346        );
347        let applied = (next.0 - current.0, next.1 - current.1);
348        (next, self.offset_delta(applied))
349    }
350
351    /// The offset that brings `keep` into view under its own policy, and how far the
352    /// content moves from **rest** to get there.
353    ///
354    /// Absolute, because [`KeepVisible::rect`] is recorded at rest: the base is offset
355    /// zero, not wherever the region currently is.
356    pub fn keep_offset(&self, keep: KeepVisible) -> ((f32, f32), (f32, f32)) {
357        match keep.centre {
358            true => self.centre(keep.rect, (0.0, 0.0)),
359            false => self.reveal(keep.rect, (0.0, 0.0)),
360        }
361    }
362
363    /// Where the content's leading edge sits, relative to the viewport's, at `offset`.
364    ///
365    /// Normally minus the offset. On a reversed axis the content is anchored to the far
366    /// end, so offset 0 puts the content's *end* against the viewport's — which is also
367    /// the answer for content too short to scroll, where it sits at the bottom of the
368    /// box rather than the top.
369    pub fn content_origin(&self, offset: (f32, f32), content: Size) -> (f32, f32) {
370        let axis = |reverse: bool, offset: f32, viewport: f32, content: f32| {
371            if reverse {
372                viewport - content + offset
373            } else {
374                -offset
375            }
376        };
377        (
378            axis(self.reverse_x, offset.0, self.viewport.width, content.width),
379            axis(
380                self.reverse_y,
381                offset.1,
382                self.viewport.height,
383                content.height,
384            ),
385        )
386    }
387}
388
389/// A box a scroll region has been asked to **keep in view**, and which box it is.
390///
391/// A tab bar wider than its window is what this is for. Declared by the content through
392/// [`Widget::keep_visible`], carried on the region so it survives a repaint boundary
393/// like everything else about the region, and acted on by
394/// [`crate::Runtime::sync_visible`].
395#[derive(Copy, Clone, Debug, PartialEq)]
396pub struct KeepVisible {
397    /// Which box this is. The region acts when this **changes** and not otherwise: the
398    /// box moves as the region scrolls, so a region chasing the box itself would pin
399    /// the content in place and no finger could move it.
400    pub key: u64,
401    /// The box: in the declaring widget's own coordinates when it comes back from
402    /// [`Widget::keep_visible`], and in the frame's — **as it would sit at offset zero**
403    /// — when it reaches the region.
404    ///
405    /// At rest rather than where it is actually drawn, and that is the whole reason the
406    /// answer can be trusted. A box recorded where it is drawn is a box whose position
407    /// already contains the current offset, so the offset that would centre it can only
408    /// be worked out **relative** to wherever the region happens to be — and on the
409    /// first frame, before anything has been retained, there is no wherever to be
410    /// relative to. Recorded at rest, the answer is absolute: the same number on the
411    /// first frame and the hundredth.
412    pub rect: Rect,
413    /// Put it in the **middle** of the window rather than merely inside it. What a tab
414    /// bar wants: a selected tab flush against the edge reads as the end of the row.
415    /// Clamped either way, so the first tab still rests at the start.
416    pub centre: bool,
417}
418
419/// Direction of arrow-key focus navigation.
420#[derive(Clone, Copy, Debug, PartialEq, Eq)]
421pub enum FocusDirection {
422    /// The up arrow: focus moves to the widget above.
423    Up,
424    /// The down arrow: focus moves to the widget below.
425    Down,
426    /// The left arrow: focus moves to the widget on the left.
427    Left,
428    /// The right arrow: focus moves to the widget on the right.
429    Right,
430}
431
432/// A region the mouse enters and leaves, as a frame registered it (milestone 583).
433#[derive(Clone, Copy, Debug)]
434struct HoverEntry {
435    id: WidgetId,
436    /// The part that shows, which the pointer can be over.
437    seen: Rect,
438    /// The whole box, which positions are measured from.
439    whole: Rect,
440    /// The region it is inside, if any.
441    parent: Option<WidgetId>,
442    spec: crate::HoverRegion,
443}
444
445#[derive(Clone)]
446struct Hit<Msg> {
447    id: WidgetId,
448    rect: Rect,
449    /// The message the target emits, or `None` for a target that **swallows** input
450    /// without doing anything — an [`crate::AbsorbPointer`]. Such a target still wins
451    /// the hit test, which is exactly what stops the click reaching what is behind it.
452    msg: Option<Msg>,
453    /// **Inverse** transform (screen → the flat frame) of a transformed subtree
454    /// (`Transform::scale`/`rotate`/a composition): the test point is passed through it to
455    /// get back to the untransformed frame where `rect` lives. `None` = an axis-aligned,
456    /// untransformed rect (the common case).
457    xform: Option<Affine>,
458}
459
460impl<Msg> Hit<Msg> {
461    /// `true` when `point` (in screen coordinates) falls inside the target, taking any
462    /// transform on the subtree into account.
463    fn contains(&self, point: Point) -> bool {
464        let p = self.xform.map_or(point, |inv| inv.apply(point));
465        self.rect.contains(p)
466    }
467}
468
469/// The painted output of a repaint-boundary subtree, cached (see `paintcache.rs`) and
470/// replayed as is on a *hit*. Erased behind a `Box<dyn Any>` in the cache; brought back down
471/// to `Msg` here (one `Msg` instance per app → the `downcast` always succeeds).
472#[derive(Clone)]
473struct BoundaryData<Msg> {
474    prims: Vec<frus_core::Primitive>,
475    hits: Vec<Hit<Msg>>,
476    long_presses: Vec<Hit<Msg>>,
477    dismisses: Vec<Msg>,
478    focusables: Vec<Focusable>,
479    scrollables: Vec<Scrollable>,
480    draggables: Vec<(WidgetId, Rect)>,
481    drag_sources: Vec<DragSource>,
482    drop_zones: Vec<DropZone>,
483    inks: Vec<(WidgetId, Rect)>,
484    /// The detectors that take a drag, in painted order, with the box a press can land in
485    /// (clipped) and the whole box (milestone 582).
486    pans: Vec<(WidgetId, Rect, Rect)>,
487    /// The widgets that hear the pointer's raw events, likewise (milestone 586).
488    pointer_listeners: Vec<(WidgetId, Rect, Rect)>,
489    /// The regions the mouse enters and leaves, in painted order (milestone 583).
490    hover_regions: Vec<HoverEntry>,
491    text_stops: Vec<crate::TextStop>,
492    semantics: Vec<(WidgetId, Rect, frus_core::SemanticsProperties)>,
493    system_ui: Vec<(Rect, crate::SystemUiOverlayStyle)>,
494}
495
496/// Lengths of the builder's collections on entering a boundary: the lower bounds of the
497/// slices to capture on the way out.
498struct Snapshot {
499    scene: usize,
500    hits: usize,
501    long_presses: usize,
502    dismisses: usize,
503    focusables: usize,
504    scrollables: usize,
505    draggables: usize,
506    drag_sources: usize,
507    drop_zones: usize,
508    inks: usize,
509    pans: usize,
510    pointer_listeners: usize,
511    hover_regions: usize,
512    text_stops: usize,
513    semantics: usize,
514    system_ui: usize,
515    overlays: usize,
516    focus_scope_start: Option<usize>,
517}
518
519/// Lower bounds of the **interaction** registries at the start of a transformed composition
520/// (a scale/rotation layer): only what the subtree has just added is re-mapped. Distinct from
521/// [`Snapshot`] (the boundary cache) because it **includes `reorderables`** — those are never
522/// cached but do have to be transformed. See [`transform_interaction_registries`].
523/// Lengths of the registries a [`ModalBarrier`] withholds from, on entering its subtree: the
524/// point each is truncated back to on the way out. Distinct from [`XformBase`] because a
525/// barrier also covers the **scene** and the **scrollbars**, and distinct from [`Snapshot`]
526/// because it covers the registries that are never cached.
527/// What is withdrawn from a layer of an [`crate::IndexedStack`] that is not the shown one:
528/// its paint, its input and its place in the accessibility tree. Not `absorb` — the layer
529/// is not a scrim over what is behind it, it is simply not there for the reader.
530const HIDDEN_LAYER: crate::barrier::ModalBarrier = crate::barrier::ModalBarrier {
531    pointer: true,
532    absorb: false,
533    paint: true,
534    semantics: true,
535};
536
537struct BarrierBase {
538    scene: usize,
539    hits: usize,
540    long_presses: usize,
541    focusables: usize,
542    scrollables: usize,
543    scrollbars: usize,
544    draggables: usize,
545    drag_sources: usize,
546    drop_zones: usize,
547    inks: usize,
548    pans: usize,
549    pointer_listeners: usize,
550    hover_regions: usize,
551    text_stops: usize,
552    reorderables: usize,
553    interactives: usize,
554    semantics: usize,
555}
556
557struct XformBase {
558    hits: usize,
559    long_presses: usize,
560    focusables: usize,
561    scrollables: usize,
562    draggables: usize,
563    drag_sources: usize,
564    drop_zones: usize,
565    inks: usize,
566    pans: usize,
567    pointer_listeners: usize,
568    hover_regions: usize,
569    text_stops: usize,
570    reorderables: usize,
571    semantics: usize,
572}
573
574/// Node count of the subtree **if** it is "plain" — that is, if the boundary and **all** its
575/// descendants take the default walk branch (children in prefix order): not scrollable, not a
576/// navigator, not a virtualised list, no `layout_builder`, not a stack, no overlay, no
577/// continuous animation. The count then follows the walk's rect order **exactly**, which is
578/// what makes the fingerprint and the bit-for-bit replay correct. `None` = a subtree that
579/// cannot be cached (it is repainted in full — the safe fallback).
580fn plain_subtree_len<Msg>(widget: &dyn Widget<Msg>) -> Option<usize> {
581    if widget.continuous()
582        || widget.scroll_content().is_some()
583        || widget.interactive().is_some()
584        || widget.fitted().is_some()
585        || widget.rotated_quarter_turns().is_some()
586        || widget.navigator().is_some()
587        // Only *whether*, never *what*: these two ask if the widget windows its
588        // children at all, and a size it will not read is the honest argument.
589        || widget.virtual_list(Size::ZERO).is_some()
590        || widget.page_view().is_some()
591        || widget.overflow_box().is_some()
592        || widget.constraints_transform().is_some()
593        || widget.layout_builder().is_some()
594        || widget.stack()
595        // Painted where the scroll has got to, not where it was laid out.
596        || widget.sticky_header()
597        || widget.custom_layout().is_some()
598        || widget.overlay().is_some()
599        // A reorderable (a Kanban card, a draggable header): its bounds feed the reorderables
600        // registry, which is not cached — so its subtree is not put in the paint cache.
601        || widget.reorder_index().is_some()
602        // Read by the shell every frame, from the walk (milestone 640).
603        || widget.wants_title_bar()
604        || widget.title_bar_role().is_some()
605    {
606        return None;
607    }
608    let mut n = 1;
609    for child in widget.children() {
610        n += plain_subtree_len(child.as_ref())?;
611    }
612    Some(n)
613}
614
615/// Moves `v[from..to]` after the rest of `v[from..]`: what a sticky header added, after what
616/// its section added (milestone 602).
617fn after<T>(v: &mut [T], from: usize, to: usize) {
618    if to > from && from <= v.len() {
619        v[from..].rotate_left(to - from);
620    }
621}
622
623/// The walk's constraints for a child a [`crate::CustomMultiChildLayout`] lays out. An
624/// exact extent is **forced** when nothing else is only allowed, as a pinned stack layer's
625/// is; beside an axis that is only allowed, it is handed, and a child with a size of its own
626/// there keeps it.
627fn custom_constraints(c: crate::ChildConstraints) -> Constraints {
628    use crate::ChildAxis::{AtMost, Exactly, Free};
629    let axis = |a| match a {
630        Free => (0.0, true, false, false),
631        AtMost(v) => (f32::max(v, 0.0), false, false, true),
632        Exactly(v) => (f32::max(v, 0.0), false, true, false),
633    };
634    let (w, free_x, exact_x, loose_x) = axis(c.width);
635    let (h, free_y, exact_y, loose_y) = axis(c.height);
636    if (exact_x || exact_y) && !loose_x && !loose_y {
637        Constraints::pinned(
638            (!free_x).then_some(w),
639            (!free_y).then_some(h),
640            Size::new(w, h),
641        )
642    } else {
643        Constraints::axes(w, h, [free_x, free_y], [exact_x, exact_y])
644    }
645}
646
647/// Quantises a `[0,1]` float for the fingerprint (independent of tiny binary differences: two
648/// visually identical progresses ⇒ the same fingerprint).
649fn quant(x: f32) -> i32 {
650    (x * 4096.0).round() as i32
651}
652
653/// Adds to `h` everything the paint reads from a `Status`, **except** the time (excluded: a
654/// cacheable boundary holds no `continuous` widget).
655fn hash_status<H: Hasher>(s: &Status, h: &mut H) {
656    (s.interaction as u8).hash(h);
657    s.focused.hash(h);
658    s.cursor.hash(h);
659    s.selection.hash(h);
660    s.composing.hash(h);
661    // Both change what a field floats over itself without touching its selection: a bar
662    // that opens on a selection already made, handles put away by a press.
663    s.handles.hash(h);
664    s.toolbar.hash(h);
665    // What a selection area has selected in this text: the highlight is part of its paint,
666    // and the bar floats from it.
667    s.region.hash(h);
668    s.region_bar.hash(h);
669    s.region_grips.hash(h);
670    quant(s.hover_progress).hash(h);
671    quant(s.focus_progress).hash(h);
672    quant(s.press_progress).hash(h);
673    quant(s.opacity).hash(h);
674    quant(s.value).hash(h);
675    // Sub-region hover (milestone 208): a change of position repaints the highlight.
676    s.hover_cursor.map(|p| (quant(p.x), quant(p.y))).hash(h);
677    // The thumb a drag took is the one that rises and is painted on top.
678    s.thumb.hash(h);
679}
680
681/// The result of building an interface for one frame.
682/// A **shortcut / action scope**: a subtree that binds keystrokes, answers intents, or
683/// both, together with the range of focus stops it contains.
684///
685/// The range is how "focus is inside this subtree" is answered without an ancestor test:
686/// the walk is depth-first, so a subtree's focus stops are contiguous, and the focused
687/// stop's index either falls inside the range or does not.
688struct Scope<Msg> {
689    strokes: Vec<(KeyStroke, Intent)>,
690    callbacks: Vec<(KeyStroke, Msg)>,
691    actions: Vec<(Intent, Msg)>,
692    listeners: Vec<(Intent, Msg)>,
693    /// The focus stops this subtree contains, as indices into `focusables`.
694    range: std::ops::Range<usize>,
695}
696
697/// A **focus stop**: somewhere the keyboard can land, and what the traversal should make
698/// of it.
699#[derive(Clone, Copy, Debug)]
700pub struct Focusable {
701    /// The widget's identity.
702    pub id: WidgetId,
703    /// Its **visible** box this frame: the box it was given, clipped by whatever
704    /// encloses it. Empty when it is scrolled out of sight, which is what a click test
705    /// wants — a tap on empty space must not land on something nobody can see.
706    pub rect: Rect,
707    /// Its box this frame, **unclipped**. What the geometry is really about: where the
708    /// widget is, whether or not any of it is on screen. Arrow navigation places
709    /// candidates by this, and [`Ui::reveal`] works out what to scroll from it.
710    pub bounds: Rect,
711    /// The [`crate::AutofillGroup`] this stop sits in — the form it is part of — when
712    /// there is one (milestone 512).
713    pub form: Option<WidgetId>,
714    /// The nearest enclosing scroll region, when there is one. What could bring this
715    /// stop into view, and the first link of the chain [`Ui::reveal`] walks.
716    pub scroll: Option<WidgetId>,
717    /// Focusable by a click, but **passed over by Tab** — the reference's
718    /// `ExcludeFocusTraversal`. The two are separate questions.
719    pub skip: bool,
720    /// An explicit traversal position, smallest first. `None` means tree order, which is
721    /// where everything sits until someone says otherwise.
722    pub order: Option<f32>,
723    /// The traversal group it belongs to, within which its order is resolved.
724    pub group: Option<WidgetId>,
725    /// Whether it takes the focus when it appears ([`Widget::autofocus`], milestone 605).
726    pub autofocus: bool,
727}
728
729/// The **interface** of one frame: what the walk over a widget tree produced.
730///
731/// Besides the [`Scene`] to paint it holds everything the shell asks of a frame afterwards —
732/// what a click, a long press or a dismissal answers, what can take the focus, which areas
733/// scroll, which widgets can be dragged or dropped on — so that events are routed against
734/// the frame the reader saw. It is made by [`build_ui`] and read until the next frame
735/// replaces it.
736pub struct Ui<Msg = crate::callback::Callback> {
737    scene: Scene,
738    hits: Vec<Hit<Msg>>,
739    /// Long-press targets (id, visible bounds, message).
740    long_presses: Vec<Hit<Msg>>,
741    /// Overlay dismissal messages, from the bottom to the **top**.
742    dismisses: Vec<Msg>,
743    /// The buttons of a selection bar that carry no message, and the editing action each
744    /// performs (milestone 568) — what the shell resolves a press on one to.
745    edit_actions: Vec<(WidgetId, crate::EditAction)>,
746    /// The boxes selection bars were placed in this frame: a press inside one is a press
747    /// **on the bar**, which must not put the selection it acts on away.
748    toolbars: Vec<Rect>,
749    /// Where a press lands **on an overlay** rather than on the pages under it: an overlay's
750    /// box, and the whole window under one that a press outside dismisses (milestone 642).
751    overlay_cover: Vec<Rect>,
752    focusables: Vec<Focusable>,
753    /// **Focus scope**: index of the topmost modal overlay's first focusable —
754    /// Tab/arrows/click-to-focus are trapped from there on (`None` = no modal, every
755    /// focusable takes part).
756    focus_scope_start: Option<usize>,
757    /// (id, viewport, offset max x, offset max y)
758    scrollables: Vec<Scrollable>,
759    scrollbars: Vec<Scrollbar>,
760    draggables: Vec<(WidgetId, Rect)>,
761    drag_sources: Vec<DragSource>,
762    drop_zones: Vec<DropZone>,
763    /// **Inked surfaces**: (id, the box the ink is clipped to). Tracked separately from
764    /// clicking because the splash needs the surface's **whole** box — where the finger
765    /// landed inside it, and how far the circle has to travel to cover it — which a
766    /// click target, recorded as its *visible* part, does not give.
767    inks: Vec<(WidgetId, Rect)>,
768    /// The detectors that take a drag, in painted order, with the box a press can land in
769    /// (clipped) and the whole box (milestone 582).
770    /// The row that wants the title bar's line, where it was laid out, and what the system
771    /// acts on there (milestones 640, 642).
772    title_bar_row: Option<Rect>,
773    title_bar_regions: Vec<(Rect, crate::TitleBarRole)>,
774    pans: Vec<(WidgetId, Rect, Rect)>,
775    /// The widgets that hear the pointer's raw events, likewise (milestone 586).
776    pointer_listeners: Vec<(WidgetId, Rect, Rect)>,
777    /// The regions the mouse enters and leaves, in painted order (milestone 583).
778    hover_regions: Vec<HoverEntry>,
779    /// The texts inside a [`SelectionArea`](crate::SelectionArea), in the order they were
780    /// painted — reading order — each with the area it is in.
781    text_stops: Vec<crate::TextStop>,
782    /// **Reorderables** (column headers, Kanban cards): (id, visible bounds). Tracked
783    /// independently of clicking — a card is not clickable but can still be picked up and
784    /// dropped onto.
785    reorderables: Vec<(WidgetId, Rect)>,
786    /// Interactive viewports (`InteractiveViewer`): (id, the screen viewport). The shell
787    /// routes panning (dragging) and zooming (wheel / pinch) to them.
788    interactives: Vec<(WidgetId, Rect)>,
789    /// Pull-to-refresh areas of the frame, with their configuration.
790    refreshes: Vec<Refreshable>,
791    /// Swipe-to-dismiss items of the frame, with their configuration.
792    dismissables: Vec<Dismissable>,
793    /// Draggable sheets of the frame, with their configuration and the scroll areas
794    /// walked inside each.
795    sheets: Vec<crate::sheet::SheetArea>,
796    wants_animation: bool,
797    /// The accessibility tree: semantic nodes (id, bounds, annotation), in paint order. The
798    /// shell maps it onto AccessKit.
799    semantics: Vec<(WidgetId, Rect, frus_core::SemanticsProperties)>,
800    /// The regions saying what the system bars over them should look like (paint order).
801    system_ui: Vec<(Rect, crate::SystemUiOverlayStyle)>,
802    /// Boxes whose children ran outside them, with the edge and the amount.
803    overflows: Vec<Overflowing>,
804    /// Shortcut and action scopes, in the order the walk closed them — innermost first
805    /// among any that overlap, which is the order the resolution wants.
806    scopes: Vec<Scope<Msg>>,
807    /// Keystroke listeners, with the focus stops they cover.
808    #[allow(clippy::type_complexity)]
809    listeners: Vec<(
810        std::ops::Range<usize>,
811        std::rc::Rc<dyn Fn(KeyStroke) -> Option<Msg>>,
812    )>,
813}
814
815impl<Msg: Clone> Ui<Msg> {
816    /// The scene to hand to the renderer.
817    /// Boxes in this frame whose children did not fit inside them.
818    ///
819    /// Empty is the normal answer. A non-empty one means content is being drawn outside
820    /// its parent — invisible where it is clipped, unhittable where it leaves the window,
821    /// and silent either way until something asks. That silence is what let a task row's
822    /// delete button sit off-screen through three milestones.
823    pub fn overflows(&self) -> &[Overflowing] {
824        &self.overflows
825    }
826
827    /// The scrollbars this frame drew, and therefore the ones a pointer may drag. Empty
828    /// where the platform draws none — see [`crate::Scrollbars`].
829    pub fn scrollbars(&self) -> &[Scrollbar] {
830        &self.scrollbars
831    }
832
833    /// The [`Scene`] this frame painted.
834    pub fn scene(&self) -> &Scene {
835        &self.scene
836    }
837
838    /// The frame's **accessibility tree**: every meaningful node with its bounds on screen and
839    /// its annotation ([`frus_core::SemanticsProperties`]). Paint order (= reading order). The shell
840    /// pushes it to AccessKit.
841    pub fn semantics(&self) -> &[(WidgetId, Rect, frus_core::SemanticsProperties)] {
842        &self.semantics
843    }
844
845    /// What the system bars should look like over this frame (#46): the regions under
846    /// `status_bar` answer for the status bar and those under `navigation_bar` for the
847    /// other, the one drawn last first and field by field. What none states is left
848    /// `None`, for [`crate::SystemUiOverlayStyle::resolve`] to take from the theme.
849    ///
850    /// The shell picks the two points: the first row of the screen's content under the
851    /// status bar, and the last one above the navigation bar.
852    pub fn system_ui_style(
853        &self,
854        status_bar: Point,
855        navigation_bar: Point,
856    ) -> crate::SystemUiOverlayStyle {
857        crate::system_ui::style_under(&self.system_ui, status_bar, navigation_bar)
858    }
859
860    /// `true` when a widget animates continuously (the framework must redraw).
861    /// Whether **this tree's widgets** want the next frame: a spinner turning, an ink
862    /// ripple spreading, a pull being held.
863    ///
864    /// It answers for the widgets and nothing else. A fetch still in flight also owes the
865    /// screen a frame — see [`frus_core::images_in_flight`] — and that is the *process's*
866    /// business, asked by whoever drives the loop. It used to be folded in here, which
867    /// made this method's answer depend on what an unrelated part of the program happened
868    /// to be doing: in the test suite, where everything shares one process, a refresh area
869    /// with nothing pulled asked for frames whenever another test was loading an image
870    /// (milestone 411).
871    pub fn wants_animation(&self) -> bool {
872        self.wants_animation
873    }
874
875    /// Whether something in this frame wants the title bar's line (milestone 640).
876    pub fn wants_title_bar(&self) -> bool {
877        self.title_bar_row.is_some()
878    }
879
880    /// Where the row that wants the title bar's line was laid out — a
881    /// [`WindowMenuBar`](crate::WindowMenuBar)'s, the window's and not a page's (milestone
882    /// 642). What the shell does to the pages, the back gesture at the window's edge, does
883    /// not start on it.
884    pub fn title_bar_row(&self) -> Option<Rect> {
885        self.title_bar_row
886    }
887
888    /// What the system acts on on the title bar's line: the window's icon and its buttons,
889    /// where they were painted (milestone 640).
890    pub fn title_bar_regions(&self) -> &[(Rect, crate::TitleBarRole)] {
891        &self.title_bar_regions
892    }
893
894    /// Every box a press can land on, in painted order: what on the title bar's line is the
895    /// application's, rather than the system's to move the window by (milestone 640).
896    pub fn hit_rects(&self) -> impl Iterator<Item = Rect> + '_ {
897        self.hits.iter().map(|hit| hit.rect)
898    }
899
900    /// Identity of the topmost clickable widget containing `point`.
901    pub fn hit(&self, point: Point) -> Option<WidgetId> {
902        self.hits
903            .iter()
904            .rev()
905            .find(|hit| hit.contains(point))
906            .map(|hit| hit.id)
907    }
908
909    /// The message tied to a given clickable widget. `None` for a target that swallows
910    /// input without emitting anything.
911    ///
912    /// It is always paired with [`hit`](Self::hit), so the two have to agree on which
913    /// target an identity means: every registered target therefore has one of its own,
914    /// the dismissal barrier below an overlay included.
915    pub fn msg_for(&self, id: WidgetId) -> Option<Msg> {
916        self.hits
917            .iter()
918            .find(|hit| hit.id == id)
919            .and_then(|hit| hit.msg.clone())
920    }
921
922    /// The editing action the button `id` of a selection bar performs, for a button that
923    /// carries no message (Cut, Copy, Paste, Select all — see [`crate::EditAction`]). The
924    /// shell holds the clipboard and the editing state, so it is the shell that performs it.
925    pub fn edit_action_for(&self, id: WidgetId) -> Option<crate::EditAction> {
926        self.edit_actions
927            .iter()
928            .find(|(button, _)| *button == id)
929            .map(|(_, action)| *action)
930    }
931
932    /// Whether the target `id` is one **on** a selection bar shown this frame — the click of
933    /// an assistive technology names its target by identity, not by where a pointer is, so
934    /// this is the identity-side counterpart of [`toolbar_contains`](Self::toolbar_contains).
935    pub fn hit_is_on_toolbar(&self, id: WidgetId) -> bool {
936        self.hits
937            .iter()
938            .rev()
939            .find(|hit| hit.id == id)
940            .is_some_and(|hit| {
941                self.toolbars.iter().any(|bar| {
942                    bar.contains(Point::new(
943                        hit.rect.x + hit.rect.width * 0.5,
944                        hit.rect.y + hit.rect.height * 0.5,
945                    ))
946                })
947            })
948    }
949
950    /// Whether `point` is inside a selection bar shown this frame — which a press must not
951    /// treat as a press elsewhere.
952    pub fn toolbar_contains(&self, point: Point) -> bool {
953        self.toolbars.iter().any(|bar| bar.contains(point))
954    }
955
956    /// Whether a press at `point` lands **on an overlay** — a menu, a dialog, a drawer, or
957    /// the window under one that a press outside dismisses — rather than on the pages under
958    /// it (milestone 642). What the shell does to the pages, the back gesture at the
959    /// window's edge, does not start there: in the reference that gesture is the page's, and
960    /// an overlay is above the page.
961    pub fn over_overlay(&self, point: Point) -> bool {
962        self.overlay_cover.iter().any(|r| r.contains(point))
963    }
964
965    /// Dismissal message of the **topmost** overlay (for Escape).
966    pub fn top_dismiss(&self) -> Option<Msg> {
967        self.dismisses.last().cloned()
968    }
969
970    /// **Long-press** message of the topmost target containing `point`.
971    pub fn long_press_at(&self, point: Point) -> Option<Msg> {
972        self.long_presses
973            .iter()
974            .rev()
975            .find(|hit| hit.contains(point))
976            .and_then(|hit| hit.msg.clone())
977    }
978
979    /// The frame's **pull-to-refresh areas**, with the configuration each was built
980    /// with. The shell steps them and reads their `refreshing` flag from here.
981    pub fn refresh_areas(&self) -> &[Refreshable] {
982        &self.refreshes
983    }
984
985    /// The frame's **dismissible items**, with the configuration each was built with.
986    pub fn dismissables(&self) -> &[Dismissable] {
987        &self.dismissables
988    }
989
990    /// The frame's **sheets**, with the configuration each was built with and the scroll
991    /// areas walked inside it.
992    pub fn sheets(&self) -> &[crate::sheet::SheetArea] {
993        &self.sheets
994    }
995
996    /// The sheet whose panel is under `point`, the last one walked winning — which is
997    /// the one painted on top.
998    pub fn sheet_at(&self, point: Point) -> Option<&crate::sheet::SheetArea> {
999        self.sheets.iter().rev().find(|sheet| sheet.contains(point))
1000    }
1001
1002    /// The sheet `id`, when it is in the frame.
1003    pub fn sheet(&self, id: WidgetId) -> Option<&crate::sheet::SheetArea> {
1004        self.sheets.iter().find(|sheet| sheet.id == id)
1005    }
1006
1007    /// The sheet the scroll area `area` was walked inside, when there is one — the
1008    /// innermost, should sheets nest, since an outer one counts the inner one's areas too.
1009    pub fn sheet_holding(&self, area: WidgetId) -> Option<&crate::sheet::SheetArea> {
1010        self.sheets
1011            .iter()
1012            .filter(|sheet| sheet.areas.contains(&area))
1013            .min_by_key(|sheet| sheet.areas.len())
1014    }
1015
1016    /// The topmost dismissible item containing `point` — the candidate a press has to
1017    /// weigh against the scrollable underneath it.
1018    pub fn dismissable_at(&self, point: Point) -> Option<Dismissable> {
1019        self.dismissables
1020            .iter()
1021            .rev()
1022            .find(|item| item.rect.contains(point))
1023            .copied()
1024    }
1025
1026    /// Every text that took part in a selection area this frame, in the order it was painted.
1027    pub fn text_stops(&self) -> &[crate::TextStop] {
1028        &self.text_stops
1029    }
1030
1031    /// The texts a [`SelectionArea`](crate::SelectionArea) holds, in reading order.
1032    pub fn text_stops_in(&self, area: WidgetId) -> impl Iterator<Item = &crate::TextStop> {
1033        self.text_stops.iter().filter(move |stop| stop.area == area)
1034    }
1035
1036    /// The topmost text in a selection area whose box contains `point`.
1037    pub fn text_stop_at(&self, point: Point) -> Option<crate::TextStop> {
1038        self.text_stops
1039            .iter()
1040            .rev()
1041            .find(|stop| stop.rect.contains(point))
1042            .copied()
1043    }
1044
1045    /// The text of `area` **nearest** `point`: the one under it, or the one closest to it —
1046    /// which is what a drag that has left the words still points at. Distance across lines
1047    /// counts four times what it does along one, so a point beside a line is nearest that
1048    /// line and not the one above it; ties go to the later text, so that a drag past the end
1049    /// of a paragraph ends at its last line.
1050    pub fn nearest_text_stop(&self, area: WidgetId, point: Point) -> Option<crate::TextStop> {
1051        let distance = |rect: Rect| {
1052            let dx = (rect.x - point.x)
1053                .max(point.x - (rect.x + rect.width))
1054                .max(0.0);
1055            let dy = (rect.y - point.y)
1056                .max(point.y - (rect.y + rect.height))
1057                .max(0.0);
1058            dy * 4.0 + dx
1059        };
1060        let mut best: Option<(f32, crate::TextStop)> = None;
1061        for stop in self.text_stops_in(area) {
1062            let d = distance(stop.rect);
1063            if best.is_none_or(|(so_far, _)| d <= so_far) {
1064                best = Some((d, *stop));
1065            }
1066        }
1067        best.map(|(_, stop)| stop)
1068    }
1069
1070    /// Topmost focusable widget containing `point`: (id, its bounds).
1071    pub fn focus_hit(&self, point: Point) -> Option<(WidgetId, Rect)> {
1072        self.focus_pool()
1073            .iter()
1074            .rev()
1075            .find(|f| f.rect.contains(point))
1076            .map(|f| (f.id, f.rect))
1077    }
1078
1079    /// The **participating** focusables: those of the topmost modal scope when there is one
1080    /// (a focus trap), otherwise all of them.
1081    fn focus_pool(&self) -> &[Focusable] {
1082        match self.focus_scope_start {
1083            Some(start) => &self.focusables[start.min(self.focusables.len())..],
1084            None => &self.focusables,
1085        }
1086    }
1087
1088    /// Nearest focusable in the given **direction** (arrow navigation, a geometric policy):
1089    /// among the focusables whose centre lies on the right side, it minimises the distance
1090    /// along the main axis with a penalty on the cross-axis offset. `None` when there is
1091    /// nothing in that direction.
1092    pub fn focus_directional(
1093        &self,
1094        current: WidgetId,
1095        direction: FocusDirection,
1096    ) -> Option<WidgetId> {
1097        // The starting point may be outside the scope (focus from before it opened); the
1098        // **candidates**, though, are trapped inside it.
1099        let from = self
1100            .focusables
1101            .iter()
1102            .find(|f| f.id == current)
1103            .map(|f| &f.rect)?;
1104        let center = |r: &Rect| (r.x + r.width * 0.5, r.y + r.height * 0.5);
1105        let (fx, fy) = center(from);
1106
1107        let mut best: Option<(WidgetId, f32)> = None;
1108        for candidate in self.focus_pool() {
1109            let (id, rect) = (candidate.id, &candidate.rect);
1110            if id == current {
1111                continue;
1112            }
1113            let (cx, cy) = center(rect);
1114            // (how far ahead in the direction, the cross-axis offset)
1115            let (ahead, cross) = match direction {
1116                FocusDirection::Right => (cx - fx, (cy - fy).abs()),
1117                FocusDirection::Left => (fx - cx, (cy - fy).abs()),
1118                FocusDirection::Down => (cy - fy, (cx - fx).abs()),
1119                FocusDirection::Up => (fy - cy, (cx - fx).abs()),
1120            };
1121            // Inside a **cone** around the direction (not a plain half-plane): a candidate
1122            // almost aligned on the cross axis but barely "ahead" (slightly different widths)
1123            // is not a directional target.
1124            if ahead <= 0.5 || cross > ahead * 3.0 {
1125                continue;
1126            }
1127            let score = ahead + cross * 3.0;
1128            if best.map(|(_, s)| score < s).unwrap_or(true) {
1129                best = Some((id, score));
1130            }
1131        }
1132        best.map(|(id, _)| id)
1133    }
1134
1135    /// Next/previous focus stop for Tab, wrapping. With no current focus, the first (or
1136    /// the last).
1137    ///
1138    /// **Tree order, unless something said otherwise.** Stops carrying an explicit order
1139    /// are sorted ahead of those that do not, smallest first, and the sort is applied
1140    /// **within a traversal group** rather than across the whole frame — so a reordered
1141    /// dialog does not reshuffle the page behind it. Stops marked `skip` are passed over
1142    /// here while remaining reachable by a click: the keyboard's order and the pointer's
1143    /// reach are two different questions.
1144    pub fn focus_next(&self, current: Option<WidgetId>, forward: bool) -> Option<WidgetId> {
1145        let order = self.traversal_order();
1146        if order.is_empty() {
1147            return None;
1148        }
1149        let n = order.len();
1150        // A current focus **outside the scope** (taken before the modal opened) is treated
1151        // as "none": Tab enters the trap.
1152        match current.and_then(|c| order.iter().position(|id| *id == c)) {
1153            Some(i) => {
1154                let j = if forward {
1155                    (i + 1) % n
1156                } else {
1157                    (i + n - 1) % n
1158                };
1159                Some(order[j])
1160            }
1161            None => Some(order[if forward { 0 } else { n - 1 }]),
1162        }
1163    }
1164
1165    /// The Tab order: the participating stops, minus those that asked to be skipped,
1166    /// with each traversal group's members sorted among themselves.
1167    ///
1168    /// A **stable** sort on the order key, so everything without one keeps tree order and
1169    /// ties do too — the property that makes an explicit order a local statement rather
1170    /// than a rearrangement of the whole frame.
1171    pub fn traversal_order(&self) -> Vec<WidgetId> {
1172        let pool: Vec<&Focusable> = self.focus_pool().iter().filter(|f| !f.skip).collect();
1173        if pool.iter().all(|f| f.order.is_none()) {
1174            return pool.iter().map(|f| f.id).collect();
1175        }
1176        let mut out: Vec<WidgetId> = Vec::with_capacity(pool.len());
1177        let mut i = 0;
1178        while i < pool.len() {
1179            // A group's members are contiguous: the walk is depth-first, so a subtree's
1180            // stops arrive together.
1181            let group = pool[i].group;
1182            let mut j = i;
1183            while j < pool.len() && pool[j].group == group {
1184                j += 1;
1185            }
1186            let mut run: Vec<&Focusable> = pool[i..j].to_vec();
1187            run.sort_by(|a, b| {
1188                let key = |f: &Focusable| f.order.unwrap_or(f32::INFINITY);
1189                key(a).total_cmp(&key(b))
1190            });
1191            out.extend(run.iter().map(|f| f.id));
1192            i = j;
1193        }
1194        out
1195    }
1196
1197    /// What a keystroke means here: the messages it sends, in the order they should be
1198    /// applied.
1199    ///
1200    /// The resolution, in one place because it is the whole feature:
1201    ///
1202    /// 1. Only scopes **containing the focused stop** are candidates; with nothing
1203    ///    focused, all of them are. Innermost first — `scopes` is filled on the way *out*
1204    ///    of the walk, so that order is already the one wanted.
1205    /// 2. A [`crate::KeyboardListener`] gets the first refusal, and a `None` from it lets
1206    ///    the stroke carry on.
1207    /// 3. A direct binding ([`crate::CallbackShortcuts`]) sends its message.
1208    /// 4. Otherwise a [`crate::Shortcuts`] binding names an **intent**, and the innermost
1209    ///    [`crate::Actions`] that answers it supplies the message. An intent nobody
1210    ///    answers does nothing — deliberately: a key bound to a meaning the current screen
1211    ///    has no answer for should be inert, not an error.
1212    /// 5. Every [`crate::ActionListener`] watching that intent adds its message too.
1213    ///
1214    /// Empty means *nobody wanted it*, and the caller should carry on to whatever it would
1215    /// have done with the key.
1216    pub fn keystroke(&self, stroke: KeyStroke, focused: Option<WidgetId>) -> Vec<Msg> {
1217        let at = focused.and_then(|id| self.focusables.iter().position(|f| f.id == id));
1218        let covers = |range: &std::ops::Range<usize>| match at {
1219            Some(i) => range.contains(&i),
1220            None => true,
1221        };
1222
1223        for (range, on_key) in self.listeners.iter().rev() {
1224            if covers(range) {
1225                if let Some(msg) = on_key(stroke) {
1226                    return vec![msg];
1227                }
1228            }
1229        }
1230
1231        let mut out = Vec::new();
1232        for scope in self.scopes.iter().filter(|s| covers(&s.range)) {
1233            if let Some((_, msg)) = scope.callbacks.iter().find(|(s, _)| stroke.matches(s)) {
1234                out.push(msg.clone());
1235                return out;
1236            }
1237            let Some((_, intent)) = scope.strokes.iter().find(|(s, _)| stroke.matches(s)) else {
1238                continue;
1239            };
1240            let answer = self
1241                .scopes
1242                .iter()
1243                .filter(|s| covers(&s.range))
1244                .find_map(|s| {
1245                    s.actions
1246                        .iter()
1247                        .find(|(i, _)| i == intent)
1248                        .map(|(_, m)| m.clone())
1249                });
1250            if let Some(msg) = answer {
1251                out.push(msg);
1252                for watcher in self.scopes.iter().filter(|s| covers(&s.range)) {
1253                    for (i, m) in &watcher.listeners {
1254                        if i == intent {
1255                            out.push(m.clone());
1256                        }
1257                    }
1258                }
1259            }
1260            return out;
1261        }
1262        out
1263    }
1264
1265    /// Topmost scrollable area containing `point`.
1266    pub fn scroll_hit(&self, point: Point) -> Option<Scrollable> {
1267        self.scrollables
1268            .iter()
1269            .rev()
1270            .find(|area| area.viewport.contains(point))
1271            .copied()
1272    }
1273
1274    /// **Every** scrollable area containing `point`, innermost first — the whole stack a
1275    /// finger has under it.
1276    ///
1277    /// [`Self::scroll_hit`] answers *which one is on top*, which is the right question for
1278    /// a wheel or a scrollbar. A finger asks a different one: a strip that only slides
1279    /// across, sitting in a page that only scrolls down, cannot take a drag downwards, and
1280    /// the page behind it can. Deciding that needs the ones behind as well.
1281    pub fn scroll_chain(&self, point: Point) -> impl Iterator<Item = Scrollable> + '_ {
1282        self.scrollables
1283            .iter()
1284            .rev()
1285            .filter(move |area| area.viewport.contains(point))
1286            .copied()
1287    }
1288
1289    /// Frame of a **focusable** widget `id`, when it is present this frame — so the shell can
1290    /// find the focused field's geometry (vertical caret movement).
1291    pub fn widget_rect(&self, id: WidgetId) -> Option<Rect> {
1292        self.focusables
1293            .iter()
1294            .find(|f| f.id == id)
1295            .map(|f| f.rect)
1296            // Falls back to the **reorderables** registry. Kanban cards (and headers that are
1297            // reorderable but **not** sortable) are not focusable: without this fallback the
1298            // shell cannot find their bounds, and the whole **vertical** drag preview (ghost,
1299            // insertion line, reflow) as well as the *insert-after* routing stay dead.
1300            .or_else(|| {
1301                self.reorderables
1302                    .iter()
1303                    .find(|(rid, _)| *rid == id)
1304                    .map(|(_, rect)| *rect)
1305            })
1306            // And to the detectors that take a drag, which report where a drag is in their
1307            // own coordinates (milestone 582): the whole box, whatever a clip hides of it.
1308            .or_else(|| {
1309                self.pans
1310                    .iter()
1311                    .rev()
1312                    .find(|(pid, _, _)| *pid == id)
1313                    .map(|(_, _, whole)| *whole)
1314            })
1315    }
1316
1317    /// The box of the inked surface `id`, when the frame has one — what the shell needs
1318    /// to start a splash: where inside it the finger landed, and how big it is.
1319    ///
1320    /// The **topmost** match wins, as the hit-test does: two inked surfaces can overlap,
1321    /// and the one that took the tap is the one drawn last.
1322    pub fn ink_box(&self, id: WidgetId) -> Option<Rect> {
1323        self.inks
1324            .iter()
1325            .rev()
1326            .find(|(iid, _)| *iid == id)
1327            .map(|(_, rect)| *rect)
1328    }
1329
1330    /// The identities of **every** focusable widget of the frame (the scope included) — so
1331    /// the shell can detect the focus **disappearing** (an overlay closed) and restore it.
1332    pub fn focusable_ids(&self) -> impl Iterator<Item = WidgetId> + '_ {
1333        self.focusables.iter().map(|f| f.id)
1334    }
1335
1336    /// The focusable widgets of the frame that take the focus when they appear
1337    /// ([`Widget::autofocus`]), in tree order.
1338    pub fn autofocus_ids(&self) -> impl Iterator<Item = WidgetId> + '_ {
1339        self.focusables.iter().filter(|f| f.autofocus).map(|f| f.id)
1340    }
1341
1342    /// The focus stops of the **form** `id` is part of, in tree order, each with its box
1343    /// this frame (unclipped: a field scrolled out of sight is still a field of the form).
1344    /// A stop in no [`crate::AutofillGroup`] is a form of one. Empty when `id` is not a
1345    /// focus stop this frame.
1346    ///
1347    /// Stops, not fields: which of them take part in autofill is a question for the
1348    /// widgets ([`crate::Widget::autofill_hints`]), which the shell has and this does not.
1349    pub fn form_of(&self, id: WidgetId) -> Vec<(WidgetId, Rect)> {
1350        let Some(stop) = self.focusables.iter().find(|f| f.id == id) else {
1351            return Vec::new();
1352        };
1353        match stop.form {
1354            Some(form) => self.group_stops(form),
1355            None => vec![(stop.id, stop.bounds)],
1356        }
1357    }
1358
1359    /// The [`crate::AutofillGroup`] focus stop `id` sits in, if it is a stop this frame
1360    /// and sits in one.
1361    pub fn form_group(&self, id: WidgetId) -> Option<WidgetId> {
1362        self.focusables.iter().find(|f| f.id == id)?.form
1363    }
1364
1365    /// Every focus stop inside the [`crate::AutofillGroup`] `group`, in tree order, with
1366    /// its unclipped box. Empty once the group has left the screen — which is the moment
1367    /// a form is finished with, and the platform may offer to save what was typed in it.
1368    pub fn group_stops(&self, group: WidgetId) -> Vec<(WidgetId, Rect)> {
1369        self.focusables
1370            .iter()
1371            .filter(|f| f.form == Some(group))
1372            .map(|f| (f.id, f.bounds))
1373            .collect()
1374    }
1375
1376    /// Frame (viewport) of the scrollable area `id`, when there is one — so the shell can find
1377    /// a multi-line field's width/height (caret following).
1378    pub fn scrollable_viewport(&self, id: WidgetId) -> Option<Rect> {
1379        self.scrollables
1380            .iter()
1381            .find(|area| area.id == id)
1382            .map(|area| area.viewport)
1383    }
1384
1385    /// The scroll offsets that would bring `id` **into view**, nearest region first.
1386    ///
1387    /// Empty when it is already in view, when the frame does not contain it, or when
1388    /// nothing around it scrolls. Nobody has to check first: an empty answer is the
1389    /// answer that nothing needs to move.
1390    ///
1391    /// Nested regions are walked from the inside out, and each is told where the target
1392    /// **ended up** rather than where it started. A sideways strip inside a page that
1393    /// scrolls down moves the target across, and the page then decides how far down it
1394    /// still is; asking both about the original box would have them fight over the same
1395    /// pixels and land somewhere neither wanted.
1396    pub fn reveal(&self, id: WidgetId, runtime: &crate::Runtime) -> Vec<(WidgetId, (f32, f32))> {
1397        let Some(stop) = self.focusables.iter().find(|f| f.id == id) else {
1398            return Vec::new();
1399        };
1400        let mut target = stop.bounds;
1401        let mut host = stop.scroll;
1402        let mut moves = Vec::new();
1403        // Regions nest a handful deep at most. The bound is a backstop against a chain
1404        // that loops, which cannot happen and would hang the frame if it did.
1405        for _ in 0..16 {
1406            let Some(hid) = host else { break };
1407            let Some(area) = self.scrollables.iter().find(|area| area.id == hid) else {
1408                break;
1409            };
1410            let current = runtime.scroll.get(&hid).copied().unwrap_or((0.0, 0.0));
1411            let (offset, shift) = area.reveal(target, current);
1412            if shift.0 != 0.0 || shift.1 != 0.0 {
1413                target = target.translate(shift.0, shift.1);
1414                moves.push((hid, offset));
1415            }
1416            host = area.host;
1417        }
1418        moves
1419    }
1420
1421    /// Every scrollable area of the frame — geometry, bounds and physics — which is
1422    /// what the runtime needs to drive momentum and edges.
1423    pub fn scroll_regions(&self) -> &[Scrollable] {
1424        &self.scrollables
1425    }
1426
1427    /// The area `id`, when it is present this frame.
1428    pub fn scroll_region(&self, id: WidgetId) -> Option<Scrollable> {
1429        self.scrollables.iter().find(|area| area.id == id).copied()
1430    }
1431
1432    /// SingleChildScrollView bounds `(id, max_x, max_y)` of every scrollable area.
1433    pub fn scrollable_maxes(&self) -> Vec<(WidgetId, f32, f32)> {
1434        self.scrollables
1435            .iter()
1436            .map(|area| (area.id, area.max_x, area.max_y))
1437            .collect()
1438    }
1439
1440    /// Scrollbar thumb under `point` (to start a drag).
1441    pub fn scrollbar_at(&self, point: Point) -> Option<Scrollbar> {
1442        self.scrollbars
1443            .iter()
1444            .rev()
1445            .find(|bar| bar.interactive && bar.opacity > 0.0 && bar.thumb.contains(point))
1446            .copied()
1447    }
1448
1449    /// The scrollbar a **pointer is near enough to wake**: inside its track, or within a
1450    /// tap target's reach of its thumb.
1451    ///
1452    /// Wider than [`Ui::scrollbar_at`], and — unlike it — it answers for a bar that has
1453    /// faded out entirely, which is the whole point. A bar nobody can see is a bar nobody
1454    /// can grab, so the reference lets a mouse coming near one bring it back first
1455    /// (`scrollbar.dart:766`); only then is it something to take hold of.
1456    pub fn scrollbar_near(&self, point: Point) -> Option<Scrollbar> {
1457        self.scrollbars
1458            .iter()
1459            .rev()
1460            .find(|bar| bar.interactive && bar.reach.contains(point))
1461            .copied()
1462    }
1463
1464    /// Topmost draggable widget under `point`: (id, its bounds).
1465    pub fn draggable_at(&self, point: Point) -> Option<(WidgetId, Rect)> {
1466        self.draggables
1467            .iter()
1468            .rev()
1469            .find(|(_, rect)| rect.contains(point))
1470            .map(|(id, rect)| (*id, *rect))
1471    }
1472
1473    /// Topmost **drag source** under `point`, with what it carries.
1474    pub fn drag_source_at(&self, point: Point) -> Option<DragSource> {
1475        self.drag_sources
1476            .iter()
1477            .rev()
1478            .find(|source| source.rect.contains(point))
1479            .copied()
1480    }
1481
1482    /// Every **drop target** of the frame, in paint order: the last one is on top.
1483    pub fn drop_zones(&self) -> &[DropZone] {
1484        &self.drop_zones
1485    }
1486
1487    /// Topmost **drop target** under `point`.
1488    pub fn drop_zone_at(&self, point: Point) -> Option<DropZone> {
1489        self.drop_zones
1490            .iter()
1491            .rev()
1492            .find(|zone| zone.rect.contains(point))
1493            .copied()
1494    }
1495
1496    /// Topmost **reorderable** widget under `point`: its id. The basis of reordering
1497    /// drag-and-drop (the source on press, the target on drop) — independent of clickability.
1498    pub fn reorderable_at(&self, point: Point) -> Option<WidgetId> {
1499        self.reorderables
1500            .iter()
1501            .rev()
1502            .find(|(_, rect)| rect.contains(point))
1503            .map(|(id, _)| *id)
1504    }
1505
1506    /// **Every** reorderable under `point`, topmost first.
1507    ///
1508    /// [`Self::reorderable_at`] answers *which one is on top*, which is the right question
1509    /// for a press: whatever is nearest the finger is what the finger grabbed. A **drop**
1510    /// asks a different one, because the thing on top may be a grip that nothing can be
1511    /// dropped on — see [`Widget::reorder_droppable`] — and the row it belongs to, which
1512    /// can, is the one behind it.
1513    pub fn reorderables_at(&self, point: Point) -> impl Iterator<Item = WidgetId> + '_ {
1514        self.reorderables
1515            .iter()
1516            .rev()
1517            .filter(move |(_, rect)| rect.contains(point))
1518            .map(|(id, _)| *id)
1519    }
1520
1521    /// Every **reorderable** of the frame, with its box, in paint order.
1522    pub fn reorderables(&self) -> &[(WidgetId, Rect)] {
1523        &self.reorderables
1524    }
1525
1526    /// Topmost **interactive** viewport (`InteractiveViewer`) under `point`: (id, its screen
1527    /// viewport). The shell routes panning and zooming to it.
1528    pub fn interactive_at(&self, point: Point) -> Option<(WidgetId, Rect)> {
1529        self.interactives
1530            .iter()
1531            .rev()
1532            .find(|(_, rect)| rect.contains(point))
1533            .map(|(id, rect)| (*id, *rect))
1534    }
1535
1536    /// The regions the mouse is in at `point`, the innermost first, each with its whole box
1537    /// (milestone 583).
1538    ///
1539    /// Every region under the pointer is in — a region inside another is in both — except
1540    /// those an **opaque** region hides: once one is found, only the regions it is inside
1541    /// are still in, and a region behind it that it is not inside is not.
1542    pub fn hover_regions_at(&self, point: Point) -> Vec<(WidgetId, Rect, crate::HoverRegion)> {
1543        let mut found: Vec<&HoverEntry> = Vec::new();
1544        // The regions an opaque one found so far is inside: its parent chain.
1545        let mut hidden_behind: Option<Vec<WidgetId>> = None;
1546        for region in self.hover_regions.iter().rev() {
1547            if !region.seen.contains(point) {
1548                continue;
1549            }
1550            if let Some(chain) = &hidden_behind {
1551                if !chain.contains(&region.id) {
1552                    continue;
1553                }
1554            }
1555            found.push(region);
1556            if region.spec.opaque && hidden_behind.is_none() {
1557                let mut chain = Vec::new();
1558                let mut parent = region.parent;
1559                while let Some(id) = parent {
1560                    chain.push(id);
1561                    parent = self
1562                        .hover_regions
1563                        .iter()
1564                        .rev()
1565                        .find(|r| r.id == id)
1566                        .and_then(|r| r.parent);
1567                }
1568                hidden_behind = Some(chain);
1569            }
1570        }
1571        found
1572            .into_iter()
1573            .map(|region| (region.id, region.whole, region.spec))
1574            .collect()
1575    }
1576
1577    /// The whole box of the region `id` in this frame, if it is in it.
1578    pub fn hover_region_rect(&self, id: WidgetId) -> Option<Rect> {
1579        self.hover_regions
1580            .iter()
1581            .rev()
1582            .find(|region| region.id == id)
1583            .map(|region| region.whole)
1584    }
1585
1586    /// The widgets hearing the pointer's raw events under `point`, the innermost first, each
1587    /// with its whole box (milestone 586).
1588    pub fn pointer_listeners_at(&self, point: Point) -> Vec<(WidgetId, Rect)> {
1589        self.pointer_listeners
1590            .iter()
1591            .rev()
1592            .filter(|(_, seen, _)| seen.contains(point))
1593            .map(|(id, _, whole)| (*id, *whole))
1594            .collect()
1595    }
1596
1597    /// The innermost detector that takes a drag under `point`, and its box (milestone 582):
1598    /// the one drawn last, as the hit-test chooses.
1599    pub fn pan_at(&self, point: Point) -> Option<(WidgetId, Rect)> {
1600        self.pans
1601            .iter()
1602            .rev()
1603            .find(|(_, seen, _)| seen.contains(point))
1604            .map(|(id, seen, _)| (*id, *seen))
1605    }
1606
1607    /// Interactive viewports `(id, screen viewport)`, to drive the inertia (fling) and the
1608    /// clamping of the pan on the framework's side.
1609    pub fn interactive_bounds(&self) -> Vec<(WidgetId, Rect)> {
1610        self.interactives.clone()
1611    }
1612}
1613
1614/// Paints a **striped band** along the edge of every box whose children ran past it.
1615///
1616/// Milestone 335 taught the layout to notice an overflow and the shell to say so in the
1617/// console. That is the half a developer reads; this is the half a *screenshot* shows —
1618/// and a screenshot is what a bug report is made of. Three of this framework's own
1619/// milestones went looking for defects that a band would have pointed at: a delete button
1620/// laid out past the card (333, 334), a segmented control 221 px outside its parent (335).
1621///
1622/// The look is the reference's, because the point of it is to be recognised on sight:
1623/// black and yellow diagonal stripes, three quarters opaque, over a tenth of the box along
1624/// the offending edge, with the edge and the number written across it. Not the mechanism,
1625/// though — the reference paints one repeating gradient and this paints the stripes,
1626/// because a repeating diagonal gradient is a shader feature and a parallelogram is four
1627/// points.
1628///
1629/// **Debug builds only.** A band is a message to whoever is building the application, and
1630/// in a release build there is nobody to read it — the reference draws the same line.
1631fn paint_overflow_bands(scene: &mut Scene, overflows: &[Overflowing]) {
1632    if !cfg!(debug_assertions) || overflows.is_empty() {
1633        return;
1634    }
1635    // The reference's two colours and its 10 % band, exactly.
1636    const BLACK: Color = Color::rgba(0.0, 0.0, 0.0, 0.75);
1637    const YELLOW: Color = Color::rgba(1.0, 1.0, 0.0, 0.75);
1638    const FRACTION: f32 = 0.1;
1639    /// The stripes run at 45°; this is their period measured along the x axis.
1640    const PERIOD: f32 = 14.0;
1641
1642    let (outer, outer_bounds) = (scene.current_clip(), scene.current_bounds());
1643    scene.set_clip(Rect::UNBOUNDED);
1644    for over in overflows {
1645        let r = over.rect;
1646        let band = match over.side {
1647            Side::Left => Rect::new(r.x, r.y, r.width * FRACTION, r.height),
1648            Side::Right => Rect::new(
1649                r.x + r.width * (1.0 - FRACTION),
1650                r.y,
1651                r.width * FRACTION,
1652                r.height,
1653            ),
1654            Side::Top => Rect::new(r.x, r.y, r.width, r.height * FRACTION),
1655            Side::Bottom => Rect::new(
1656                r.x,
1657                r.y + r.height * (1.0 - FRACTION),
1658                r.width,
1659                r.height * FRACTION,
1660            ),
1661        };
1662        if band.width <= 0.0 || band.height <= 0.0 {
1663            continue;
1664        }
1665        scene.set_clip(band);
1666        scene.fill_rect(band, BLACK);
1667        // One parallelogram per stripe, sheared by the band's height so that every one
1668        // of them crosses it at 45° whatever shape the band is.
1669        let shear = band.height;
1670        let mut x = band.x - shear;
1671        while x < band.x + band.width {
1672            let stripe = Path::new()
1673                .move_to(Point::new(x, band.y))
1674                .line_to(Point::new(x + PERIOD / 2.0, band.y))
1675                .line_to(Point::new(x + PERIOD / 2.0 + shear, band.y + band.height))
1676                .line_to(Point::new(x + shear, band.y + band.height))
1677                .close();
1678            scene.fill_path(&stripe, YELLOW);
1679            x += PERIOD;
1680        }
1681        // The label is **not** clipped to the band: a vertical one is usually longer than
1682        // the box is tall, and half a sentence is worse than a sentence written over
1683        // whatever is beside it. The reference does not clip it either.
1684        scene.set_clip(Rect::UNBOUNDED);
1685        scene.set_bounds(Rect::UNBOUNDED);
1686        paint_overflow_label(scene, band, over);
1687    }
1688    scene.set_clip(outer);
1689    scene.set_bounds(outer_bounds);
1690}
1691
1692/// Writes the reference's sentence across a band: which edge overflowed, and by how much.
1693///
1694/// The console already says it — but the console is on the developer's machine and the
1695/// band is on the device, and a photograph of a phone is what half the bug reports in the
1696/// world are made of. A striped edge says *something* is too big; `RIGHT OVERFLOWED BY 86
1697/// PIXELS` says which edge, and by enough that you can tell a forgotten padding from a
1698/// missing wrap without running anything.
1699///
1700/// The words are the reference's, exactly, so that searching for them finds the same
1701/// answers. So are the numbers: 7.5 px, the heaviest weight there is, dark red on an
1702/// opaque white plate — a label that must stay readable over stripes cannot be subtle —
1703/// centred on the band's outer edge and turned a quarter turn on the vertical ones.
1704fn paint_overflow_label(scene: &mut Scene, band: Rect, over: &Overflowing) {
1705    /// The reference's label metrics.
1706    const SIZE: f32 = 7.5;
1707    /// The gap between the label and the outer edge of the band.
1708    const PAD: f32 = 1.0;
1709    /// `0xFF900000`.
1710    const INK: Color = Color::rgb(0.5647059, 0.0, 0.0);
1711
1712    let side = match over.side {
1713        Side::Left => "LEFT",
1714        Side::Right => "RIGHT",
1715        Side::Top => "TOP",
1716        Side::Bottom => "BOTTOM",
1717    };
1718    let text = format!("{side} OVERFLOWED BY {} PIXELS", format_pixels(over.amount));
1719    // The reference asks for an 800 weight; the heaviest this framework has is bold.
1720    let style = TextStyle::new(SIZE).weight(FontWeight::Bold);
1721    let size = frus_text::measure_styled(&text, SIZE, FontWeight::Bold, false);
1722
1723    // Where the middle of the label's leading edge goes, before any rotation, and by how
1724    // much the whole thing then turns about that point.
1725    let (anchor, angle) = match over.side {
1726        Side::Left => (
1727            Point::new(band.x + SIZE + PAD, band.y + band.height / 2.0),
1728            std::f32::consts::FRAC_PI_2,
1729        ),
1730        Side::Right => (
1731            Point::new(band.x + band.width - SIZE - PAD, band.y + band.height / 2.0),
1732            -std::f32::consts::FRAC_PI_2,
1733        ),
1734        Side::Top => (Point::new(band.x + band.width / 2.0, band.y + PAD), 0.0),
1735        Side::Bottom => (
1736            Point::new(band.x + band.width / 2.0, band.y + band.height - SIZE - PAD),
1737            0.0,
1738        ),
1739    };
1740    if angle == 0.0 {
1741        let at = Point::new(anchor.x - size.width / 2.0, anchor.y);
1742        scene.fill_rect(Rect::from_point_size(at, size), Color::WHITE);
1743        scene.text(at, text, &style.resolved(), INK);
1744        return;
1745    }
1746    // A rotation of a group, not of a glyph: the plate and the sentence turn together and
1747    // neither has to know it is turning.
1748    //
1749    // The group is painted **at the origin** rather than where it lands, because a layer
1750    // is rendered flat into a window-sized texture before it is composited: a vertical
1751    // label is written across a box near the right edge, so laid out flat it would run
1752    // off the texture and lose its last word before the rotation ever brought it back
1753    // inside. The transform is the one that carries the origin to where the label goes —
1754    // shift it half its width to the left, turn it, and land it on the anchor.
1755    let flat = Rect::from_point_size(Point::ZERO, size);
1756    let matrix = Affine::translation(-size.width / 2.0, 0.0)
1757        .then(Affine::rotation(angle))
1758        .then(Affine::translation(anchor.x, anchor.y));
1759    scene.transformed(LayerTransform::new(matrix), move |scene: &mut Scene| {
1760        scene.fill_rect(flat, Color::WHITE);
1761        scene.text(flat.origin(), text, &style.resolved(), INK);
1762    });
1763}
1764
1765/// How the reference writes an overflow: whole pixels past ten, one decimal past one, and
1766/// three significant figures below that — because the difference between 0.5 px and
1767/// 0.0001 px is the difference between a layout bug and a rounding error.
1768fn format_pixels(value: f32) -> String {
1769    if value > 10.0 {
1770        format!("{value:.0}")
1771    } else if value > 1.0 {
1772        format!("{value:.1}")
1773    } else {
1774        // Three significant figures: the first one sits at the first non-zero decimal.
1775        let leading = if value > 0.0 {
1776            (-value.abs().log10().floor()) as i32
1777        } else {
1778            0
1779        };
1780        format!("{value:.*}", (leading + 2).clamp(0, 12) as usize)
1781    }
1782}
1783
1784/// Identity of the `index`-th child: **by key** when the child declares one (stable whatever
1785/// its position), **positional** otherwise. It must be used everywhere a child identity is
1786/// derived (render, collection, lookup, animations) so they all stay consistent.
1787pub(crate) fn child_id<Msg>(parent: WidgetId, index: usize, child: &dyn Widget<Msg>) -> WidgetId {
1788    match child.key() {
1789        Some(key) => parent.keyed(key),
1790        None => parent.child(index),
1791    }
1792}
1793
1794/// A widget's **effective** style for layout: its `style()`, whose size is **replaced by the
1795/// runtime's interpolated size** when the widget is animated (`Widget::anim_size` +
1796/// `Container::animated_size`). This is the only place where an animated size enters layout —
1797/// used **identically** by [`build_layout`] and by the cache fingerprint (`hash_node`), which
1798/// is what keeps them consistent (the cache stays invalid for as long as the size moves).
1799pub(crate) fn effective_style<Msg>(
1800    widget: &dyn Widget<Msg>,
1801    id: WidgetId,
1802    runtime: &Runtime,
1803    theme: &Theme,
1804) -> frus_layout::Style {
1805    // The theme's say on size and spacing (milestone 309), before the runtime's
1806    // animated overrides below — an animation in flight wins over a resting default.
1807    let mut style = widget.style_themed(theme);
1808    if widget.anim_size().is_some() {
1809        if let Some(size) = runtime.anim_size(id) {
1810            style.width = frus_layout::Dimension::Length(size.width);
1811            style.height = frus_layout::Dimension::Length(size.height);
1812        }
1813    }
1814    if widget.anim_padding().is_some() {
1815        if let Some(padding) = runtime.anim_padding(id) {
1816            style.padding = padding;
1817        }
1818    }
1819    // A share of the parent, growing or shrinking. `Percent` and not `Length`, because
1820    // the whole point of the quantity is that it does not know what it will come to — and
1821    // an axis whose factor is unset goes back to following its content rather than to
1822    // nought, which is what "unset" has always meant here.
1823    if widget.anim_fractions().is_some() {
1824        if let Some((width, height)) = runtime.anim_fractions(id) {
1825            let dim = |factor: Option<f32>| match factor {
1826                Some(f) => frus_layout::Dimension::Percent(f),
1827                None => frus_layout::Dimension::Auto,
1828            };
1829            style.width = dim(width);
1830            style.height = dim(height);
1831        }
1832    }
1833    // A dismissed item closing its gap. It collapses along the axis it was **not**
1834    // swiped along — a row swiped sideways loses its height — which is what makes the
1835    // neighbours slide up rather than the row narrow to a sliver. Only an explicit
1836    // length can shrink: an `Auto` box has no number to scale, which is why
1837    // `Dismissible` asks for a size (see its docs).
1838    // A sheet's panel, as tall as the retained share of its box. The same `Percent` as a
1839    // fraction above, for the same reason: the share is the quantity, and the box it is a
1840    // share of is the layout's to know. Hashed through here as well, so the cache misses
1841    // on every frame the sheet moves.
1842    if let Some(spec) = widget.sheet() {
1843        style.height = frus_layout::Dimension::Percent(runtime.sheet_size(id, spec));
1844    }
1845    if let Some(spec) = widget.dismissible() {
1846        if let Some(factor) = runtime.dismiss_extent_factor(id) {
1847            let axis = if spec.axis.is_horizontal() {
1848                &mut style.height
1849            } else {
1850                &mut style.width
1851            };
1852            if let frus_layout::Dimension::Length(extent) = *axis {
1853                *axis = frus_layout::Dimension::Length(extent * factor);
1854            }
1855        }
1856    }
1857    style
1858}
1859
1860/// Builds the main layout tree (a scrollable is a **leaf**). `id`/`runtime` are what inject
1861/// the animated size through [`effective_style`], following the walk's identity scheme
1862/// (`child_id`) **exactly**.
1863pub(crate) fn build_layout<'a, Msg>(
1864    widget: &'a dyn Widget<Msg>,
1865    id: WidgetId,
1866    runtime: &'a Runtime,
1867    theme: &Theme,
1868    layout: &mut Layout<'a, BaselineData>,
1869) -> NodeId {
1870    let (node, fills) = build_layout_scoped(widget, id, runtime, theme, layout, true);
1871    // A root that wants to fill has no parent to fill: the flex machinery below needs one
1872    // on each side of the question. A percentage of the room the layout is being computed
1873    // in is the same answer without a parent, and it degrades to hugging the content when
1874    // that room is unbounded — a scrollable measuring its content, a natural size.
1875    if fills.horizontal {
1876        layout.fill_root(node, true);
1877    }
1878    if fills.vertical {
1879        layout.fill_root(node, false);
1880    }
1881    node
1882}
1883
1884/// The axes a subtree asks to **fill** rather than shrink-wrap.
1885///
1886/// It is not a property of one widget. A column whose row fills the width is itself as
1887/// wide as the room it was given, because the row inside it took that room — so the
1888/// request travels up as the layout is built, and each container answers it with the one
1889/// thing a container can do about it: grow along its own axis, or stretch across it.
1890#[derive(Clone, Copy, Default)]
1891pub(crate) struct Fills {
1892    horizontal: bool,
1893    vertical: bool,
1894    /// The axes on which **this** node's size is only a default, replaced by the room on
1895    /// offer when the fill is granted (milestone 590). Never passed up: it is about this
1896    /// node's own style, not the subtree's request.
1897    soft_horizontal: bool,
1898    soft_vertical: bool,
1899}
1900
1901impl Fills {
1902    /// What a widget asks for on its own account.
1903    ///
1904    /// A straight copy since milestone 405: the widget's answer already has a flag per
1905    /// axis, where it used to be one direction and a container that wanted both had no way
1906    /// to say so.
1907    fn own<Msg>(widget: &dyn Widget<Msg>, theme: &Theme) -> Self {
1908        let asked = widget.fill_axes(theme);
1909        let soft = widget.soft_extent();
1910        Fills {
1911            horizontal: asked.horizontal,
1912            vertical: asked.vertical,
1913            soft_horizontal: soft.horizontal && asked.horizontal,
1914            soft_vertical: soft.vertical && asked.vertical,
1915        }
1916    }
1917
1918    fn merge(self, other: Fills) -> Self {
1919        Fills {
1920            horizontal: self.horizontal || other.horizontal,
1921            vertical: self.vertical || other.vertical,
1922            // This node's own; a child's default is not this node's.
1923            ..self
1924        }
1925    }
1926
1927    /// Whether this node's size on one axis is only a default.
1928    fn soft(self, horizontal: bool) -> bool {
1929        if horizontal {
1930            self.soft_horizontal
1931        } else {
1932            self.soft_vertical
1933        }
1934    }
1935
1936    /// The half of a child's request a container passes on to **its** parent, given
1937    /// which way the container runs. Along its own axis a container divides the room up
1938    /// and the request stops there; across it, the container's size *is* the child's, so
1939    /// the request has to keep going.
1940    /// Whether one axis is asked for.
1941    fn wants(self, horizontal: bool) -> bool {
1942        if horizontal {
1943            self.horizontal
1944        } else {
1945            self.vertical
1946        }
1947    }
1948
1949    /// Drops any axis this box was **given a size on**: it has already been told how big
1950    /// to be, and a request to fill cannot reach past that answer.
1951    fn bounded_by(self, style: &frus_layout::Style) -> Self {
1952        let auto = |d| matches!(d, frus_layout::Dimension::Auto);
1953        // A box with an aspect ratio decides its children's size from its own, as the
1954        // reference's `AspectRatio` hands its child tight constraints: nothing inside it
1955        // reaches past it (milestone 633).
1956        if style.aspect_ratio.is_some() {
1957            return Fills {
1958                horizontal: false,
1959                vertical: false,
1960                ..self
1961            };
1962        }
1963        Fills {
1964            horizontal: self.horizontal && auto(style.width),
1965            vertical: self.vertical && auto(style.height),
1966            ..self
1967        }
1968    }
1969
1970    fn across(self, direction: frus_layout::FlexDirection) -> Self {
1971        if direction.is_horizontal() {
1972            Fills {
1973                horizontal: false,
1974                ..self
1975            }
1976        } else {
1977            Fills {
1978                vertical: false,
1979                ..self
1980            }
1981        }
1982    }
1983}
1984
1985/// A layout leaf's **text baseline**, when it has one: the distance from the top of its
1986/// box down to the line its letters sit on.
1987///
1988/// It rides on the layout tree rather than being asked of the widgets afterwards because
1989/// only the layout knows where a node ended up, and only the walk that built it knows
1990/// which branch it took — a scrollable, a stack and a page view are all *leaves* here,
1991/// with their contents laid out elsewhere, and a second walk guessing at that would be a
1992/// copy of this one waiting to drift out of step.
1993pub(crate) type BaselineData = Option<f32>;
1994
1995/// An owned copy of a theme, **made in a frame of its own**.
1996///
1997/// `#[inline(never)]` is the whole point: inlined, the eleven-kilobyte temporary is built
1998/// in the caller's frame, and the caller here is the recursive walk that pays for its
1999/// frame once per level of the tree. Behind the call it is built, copied into the
2000/// allocation, and gone before the walk recurses.
2001#[inline(never)]
2002fn owned_theme(theme: &Theme) -> std::rc::Rc<Theme> {
2003    std::rc::Rc::new(theme.clone())
2004}
2005
2006/// The body of [`build_layout`], carrying whether baselines are still being collected.
2007/// An [`crate::IgnoreBaseline`] turns them off for its subtree — the widgets inside still
2008/// have baselines, and the point is that nothing above may see them.
2009fn build_layout_scoped<'a, Msg>(
2010    widget: &'a dyn Widget<Msg>,
2011    id: WidgetId,
2012    runtime: &'a Runtime,
2013    theme: &Theme,
2014    layout: &mut Layout<'a, BaselineData>,
2015    baselines: bool,
2016) -> (NodeId, Fills) {
2017    // A component is built into what it stands for before anything is asked of the node,
2018    // the scope it introduces included — see `Widget::expand` — under the scope a wrapper
2019    // around it introduces (milestone 642).
2020    expand_scoped(widget, id, runtime, theme);
2021    // A themed subtree lays out under **its** theme, not the frame's: a theme reaches
2022    // sizes and spacing (milestone 309), so this has to happen here and not only at paint
2023    // time. `hash_node`, which fingerprints this same walk for the relayout cache, makes
2024    // the same swap — the two staying in step is what keeps the cache honest.
2025    let scoped = scoped_theme(widget, id, runtime, theme);
2026    let theme = scoped.as_deref().unwrap_or(theme);
2027    // And a **scoped surface**, the same idea one milestone later: a shell that hands a slot
2028    // a narrowed description narrows it for everything the slot builds, the deferred build
2029    // below included. Held for this subtree and put back on the way out.
2030    let _surface = widget
2031        .media_override(crate::MediaQuery::of())
2032        .map(crate::MediaQuery::install);
2033    // And the **shell**, the third inherited thing (milestone 422): a slot told what shell
2034    // it stands in composes differently — a bar grows a menu button — so it has to be in
2035    // force before `build_themed` below, exactly as the surface is.
2036    let _shell = widget.scaffold_override().map(crate::ScaffoldInfo::install);
2037    let _scrolling = widget
2038        .scroll_behavior_override()
2039        .map(crate::ScrollConfiguration::install);
2040    // A subtree that could not be composed until the theme was known (`ThemeBuilder`).
2041    // It has to happen **before** anything reads `children()`, and under the subtree's
2042    // own theme, which is why it sits after the swap above rather than at the call site.
2043    widget.build_in(id, runtime, theme);
2044    // Everything below an `IgnoreBaseline` keeps its baseline and loses the right to be
2045    // seen: the flag is narrowed once, here, so no branch below has to remember.
2046    let baselines = baselines && !widget.ignores_baseline();
2047    // A leaf's own baseline, or nothing when this subtree is being ignored.
2048    let own_baseline = if baselines {
2049        widget.text_baseline(theme)
2050    } else {
2051        None
2052    };
2053    // `Baseline`: the child is pushed down until **its** baseline lands `target` pixels
2054    // from the top of this box, and this box grows to contain it. If the child's own
2055    // baseline is already lower than that, there is nowhere to push it up to and the
2056    // child is top-aligned instead — the same answer the reference gives.
2057    if let Some(target) = widget.baseline_target() {
2058        let mut style = effective_style(widget, id, runtime, theme);
2059        if let Some(child) = widget.children().first() {
2060            let child = child.as_ref();
2061            let cid = child_id(id, 0, child);
2062            // A child with no baseline of its own is treated as if its baseline were its
2063            // bottom edge, which is what a box with no text amounts to.
2064            let child_baseline = natural_baseline(child, cid, runtime, theme)
2065                .unwrap_or_else(|| natural_size(child, cid, runtime, theme).height);
2066            style.padding.top += (target - child_baseline).max(0.0);
2067            let (node, fills) = build_layout_scoped(child, cid, runtime, theme, layout, baselines);
2068            return (layout.container(style, &[node]), fills);
2069        }
2070        return (layout.leaf(style, None), Fills::default());
2071    }
2072    // `RotatedBox`: a leaf whose box is the child's **natural** size, with its dimensions
2073    // **swapped** for an odd quarter turn (the rotation does affect layout). The child itself
2074    // is laid out separately (at render time).
2075    if let Some(q) = widget.rotated_quarter_turns() {
2076        let mut style = effective_style(widget, id, runtime, theme);
2077        if let Some(child) = widget.children().first() {
2078            let nat = natural_size(
2079                child.as_ref(),
2080                child_id(id, 0, child.as_ref()),
2081                runtime,
2082                theme,
2083            );
2084            let (w, h) = if q.rem_euclid(4) % 2 != 0 {
2085                (nat.height, nat.width)
2086            } else {
2087                (nat.width, nat.height)
2088            };
2089            style.width = frus_layout::Dimension::Length(w);
2090            style.height = frus_layout::Dimension::Length(h);
2091        }
2092        return (layout.leaf(style, own_baseline), Fills::default());
2093    }
2094    // `Intrinsic`: one axis is taken from what the content would **like** to be, not from
2095    // the space on offer. The content is measured once, unconstrained, and the answer is
2096    // written into this node's style as a length — the same trick `RotatedBox` uses, except
2097    // that here the child stays inside, laid out normally within the size it asked for.
2098    if let Some((axis, step)) = widget.intrinsic() {
2099        let mut style = effective_style(widget, id, runtime, theme);
2100        if let Some(child) = widget.children().first() {
2101            let child = child.as_ref();
2102            let cid = child_id(id, 0, child);
2103            let nat = natural_size(child, cid, runtime, theme);
2104            let quantise = |extent: f32| match step {
2105                Some(step) if step > 0.0 => (extent / step).ceil() * step,
2106                _ => extent,
2107            };
2108            match axis {
2109                crate::constraints::IntrinsicAxis::Width => {
2110                    style.width = frus_layout::Dimension::Length(quantise(nat.width));
2111                }
2112                crate::constraints::IntrinsicAxis::Height => {
2113                    style.height = frus_layout::Dimension::Length(quantise(nat.height));
2114                }
2115            }
2116            let (node, fills) = build_layout_scoped(child, cid, runtime, theme, layout, baselines);
2117            return (layout.container(style, &[node]), fills);
2118        }
2119        return (layout.leaf(style, own_baseline), Fills::default());
2120    }
2121    // `OverflowBox`: a leaf, because its child is laid out **separately**, to constraints of
2122    // its own. That separation is the whole feature — a child sharing this node's layout
2123    // could never be bigger than it.
2124    if widget.overflow_box().is_some() {
2125        return (
2126            layout.leaf(effective_style(widget, id, runtime, theme), own_baseline),
2127            Fills::default(),
2128        );
2129    }
2130    // A box that **transforms the space on offer**: its child is laid out in a box derived
2131    // from this one's, in a tree of its own, and this box is then as big as what came back
2132    // — held to what it was itself offered, which is the reference's
2133    // `constraints.constrain(child.size)`. `UnconstrainedBox` is this branch with the
2134    // constraint taken away.
2135    if let Some(transform) = widget.constraints_transform() {
2136        let style = effective_style(widget, id, runtime, theme);
2137        let child = widget.children().first().map(|c| c.as_ref());
2138        let owned = owned_theme(theme);
2139        let animated = widget.animated_size();
2140        let factor = widget.size_factor();
2141        let measure: frus_layout::MeasureFn<'a> = Box::new(move |w, h| {
2142            let Some(child) = child else {
2143                return Size::new(0.0, 0.0);
2144            };
2145            let (aw, free_x, fill_x) = transform.width.offered(w);
2146            let (ah, free_y, fill_y) = transform.height.offered(h);
2147            let mut inner: Layout<BaselineData> = Layout::new();
2148            let node = build_layout(child, child_id(id, 0, child), runtime, &owned, &mut inner);
2149            // **Handed** every axis that has a number, **asked** about every axis that does
2150            // not. Merely constraining them instead would leave a child with no size of its
2151            // own — a plain container — hugging at nothing, and a box asked how big its child
2152            // wanted to be would come back empty. A loose axis is the exception, and says so:
2153            // there the number is only the most the child may take (milestone 598).
2154            inner.compute_axes(node, aw, ah, [free_x, free_y], [fill_x, fill_y]);
2155            let mut size = inner.size_of(node);
2156            // A box following its child's size answers with the size it has got to, and is
2157            // told when the child's has changed (milestone 594). An intrinsic question is
2158            // answered and not recorded: on an axis the child is **given**, the layout
2159            // offering nothing (how big with no limit) or zero (how small could it be). Either
2160            // makes a paragraph a size it never ends up at — one line, or one word a line.
2161            // On an axis the child decides, the offer is not read, so it asks nothing.
2162            if let Some(spec) = &animated {
2163                let asks = |rule, offer: Option<f32>| {
2164                    rule == crate::constraints::AxisConstraint::AsGiven
2165                        && offer.is_none_or(|v| v <= 0.0)
2166                };
2167                let probe = asks(transform.width, w) || asks(transform.height, h);
2168                let shown = runtime
2169                    .size_anims
2170                    .borrow_mut()
2171                    .observe(id, size, spec, !probe);
2172                // Whole pixels, rounded up and then held to the offer below: a size on its
2173                // way between two others is a fraction, and a box rounded down from one is
2174                // one its content no longer fits in (milestone 289).
2175                size = Size::new(frus_core::fits(shown.width), frus_core::fits(shown.height));
2176            }
2177            // A fraction of the child, on each axis, in whole pixels (milestone 596).
2178            if let Some((across, down)) = factor {
2179                size = Size::new(
2180                    frus_core::fits(size.width * across.max(0.0)),
2181                    frus_core::fits(size.height * down.max(0.0)),
2182                );
2183            }
2184            // Held to the offer: a child too big for the room spills, and the box stays the
2185            // size the room was.
2186            Size::new(
2187                size.width.min(w.unwrap_or(f32::INFINITY)),
2188                size.height.min(h.unwrap_or(f32::INFINITY)),
2189            )
2190        });
2191        return (
2192            layout.measured_leaf(style, own_baseline, measure),
2193            // An animated box asks for the room on the axes its child is given
2194            // (milestone 594); a plain transform box asks for nothing.
2195            Fills::own(widget, theme),
2196        );
2197    }
2198    // A `LayoutBuilder` **is measured**: it builds its content from the space offered and
2199    // is then as big as what it built, which is the reference's
2200    // `size = constraints.constrain(child.size)`. It cannot be a plain leaf, because a
2201    // leaf answers before it has been asked and there is nothing to answer with until the
2202    // box is known; and it cannot be a container, because its child does not exist yet.
2203    //
2204    // Taffy calls the closure during the computation with the space actually available —
2205    // the same moment the reference runs its layout callback — and the closure builds the
2206    // subtree, lays it out in a `Layout` of its own, and hands back the size. An axis the
2207    // style pins is still the style's: taffy does not ask about a dimension it already
2208    // knows, so `LayoutBuilder::height(200.0)` behaves exactly as it did.
2209    if let Some(build) = widget.layout_builder() {
2210        let style = effective_style(widget, id, runtime, theme);
2211        // The closure needs an **owned** theme: a themed subtree's theme is a local in
2212        // this function, so it cannot be borrowed for as long as the closure lives. The
2213        // runtime and the widget are borrowed, which is the whole reason `Layout` carries
2214        // a lifetime.
2215        //
2216        // This is a clone of eleven kilobytes, and it used to be a silent one — `Theme`
2217        // was `Copy` until milestone 448, so `*theme` read like a pointer copy and was
2218        // not. It happens once per `LayoutBuilder`, not once per node.
2219        //
2220        // **Behind an `Rc`, and made somewhere else**, because the cost that mattered was
2221        // not the copy. A `Theme` local here and a closure that captures one are two
2222        // slots of eleven kilobytes in *this* function's frame, and a frame is charged to
2223        // every level of the walk whether or not the branch is taken: 24 736 bytes a
2224        // level, which overflowed a Windows main thread's megabyte at about forty. The
2225        // `Rc` makes the capture a pointer, and `owned_theme` keeps the copy in a frame
2226        // that pops before the recursion goes any deeper.
2227        let owned = owned_theme(theme);
2228        let cid = id.child(0);
2229        let measure: frus_layout::MeasureFn<'a> = Box::new(move |w, h| {
2230            // What taffy offers. `None` is an *intrinsic* question — how big would you
2231            // be with no limit, or with none at all — and there is no honest answer to
2232            // it here: finding out means running the application's callback
2233            // speculatively. The reference refuses these outright (`computeDryLayout`
2234            // asserts); we build at the offered numbers, substituting zero for what is
2235            // not offered, and leave the unoffered axis free in the nested computation
2236            // so the content's own size comes back.
2237            let offered = Size::new(w.unwrap_or(0.0), h.unwrap_or(0.0));
2238            let child = build(offered);
2239            let mut inner: Layout<BaselineData> = Layout::new();
2240            let node = build_layout(child.as_ref(), cid, runtime, &owned, &mut inner);
2241            // The content is **handed** the offered box, not asked what it would like:
2242            // it was built from that box. An axis that was not offered is the one being
2243            // asked about, and stays free so the content's own size comes back.
2244            match (w, h) {
2245                (Some(w), Some(h)) => inner.compute_filled(node, w, h),
2246                _ => inner.compute_scroll(
2247                    node,
2248                    offered.width,
2249                    offered.height,
2250                    w.is_none(),
2251                    h.is_none(),
2252                ),
2253            }
2254            inner.size_of(node)
2255        });
2256        return (
2257            layout.measured_leaf(style, own_baseline, measure),
2258            Fills::own(widget, theme),
2259        );
2260    }
2261    // **A viewport is sized from its content on the axis it does not scroll.**
2262    //
2263    // A scrollable area declares a size on the axis it scrolls — that is what a viewport
2264    // is, a window shorter than what is behind it. On the *other* axis there is nothing to
2265    // choose: the content is as tall as it is, and the viewport is that tall. Until
2266    // milestone 485 a horizontal strip took the 200 px that belongs to a vertical one, so a
2267    // scrollable tab bar 48 px tall claimed 200 and pushed its panel a hundred and fifty
2268    // pixels into empty space (#65).
2269    //
2270    // It cannot be answered by the widget alone, and that is the whole reason this lives
2271    // here. A scroll host lays its content out **against** the viewport on the axis that
2272    // does not scroll — the content of a horizontal strip is given the viewport's height —
2273    // so a viewport that said `Auto` there would hand the content nothing and come back
2274    // nothing tall, the circle closing at zero. Breaking it takes a measurement, at the one
2275    // moment the runtime and the theme are both to hand.
2276    if let Some(content) = widget.scroll_content() {
2277        let style = effective_style(widget, id, runtime, theme);
2278        let axis = widget.scroll_axis();
2279        // Only an axis that **neither scrolls nor was given a size**. A caller's number is
2280        // a caller's number, and an axis that scrolls has already answered.
2281        let hug_x = !axis.free_x() && matches!(style.width, frus_layout::Dimension::Auto);
2282        let hug_y = !axis.free_y() && matches!(style.height, frus_layout::Dimension::Auto);
2283        // And an axis that **scrolls with no size given and no growing asked**: as big as
2284        // the content, up to the room on offer — the reference's viewport,
2285        // `constraints.constrain(child.size)` (milestone 590). It used to be 200 px.
2286        let free_to_measure =
2287            |d| matches!(d, frus_layout::Dimension::Auto) && style.flex_grow == 0.0;
2288        let clamp_x = axis.free_x() && free_to_measure(style.width);
2289        let clamp_y = axis.free_y() && free_to_measure(style.height);
2290        if hug_x || hug_y || clamp_x || clamp_y {
2291            let owned = owned_theme(theme);
2292            let cid = child_id(id, 0, content);
2293            let measure: frus_layout::MeasureFn<'a> = Box::new(move |w, h| {
2294                let mut inner: Layout<BaselineData> = Layout::new();
2295                let node = build_layout(content, cid, runtime, &owned, &mut inner);
2296                // The content is unconstrained on every axis that scrolls — that is what
2297                // scrolling means — and on the one being measured. What is left is an axis
2298                // with a real number on it, and the content is held to it.
2299                inner.compute_scroll(
2300                    node,
2301                    w.unwrap_or(0.0),
2302                    h.unwrap_or(0.0),
2303                    hug_x || axis.free_x() || w.is_none(),
2304                    hug_y || axis.free_y() || h.is_none(),
2305                );
2306                let content = inner.size_of(node);
2307                let pad = widget.scroll_padding();
2308                // **Only the hugged axis is answered.** On the axis that scrolls, a
2309                // viewport is precisely *not* as big as its content — answering with the
2310                // content's size there would make a flexible viewport ask for the whole
2311                // length of what is behind it as its basis, and a column holding one would
2312                // overflow by however much there was to scroll. Nought is what a leaf
2313                // answered before this branch existed, and it is still the right answer.
2314                let whole_x = content.width + pad.left + pad.right;
2315                let whole_y = content.height + pad.top + pad.bottom;
2316                Size::new(
2317                    if hug_x {
2318                        whole_x
2319                    } else if clamp_x {
2320                        whole_x.min(w.unwrap_or(f32::INFINITY))
2321                    } else {
2322                        0.0
2323                    },
2324                    if hug_y {
2325                        whole_y
2326                    } else if clamp_y {
2327                        whole_y.min(h.unwrap_or(f32::INFINITY))
2328                    } else {
2329                        0.0
2330                    },
2331                )
2332            });
2333            return (
2334                layout.measured_leaf(style, own_baseline, measure),
2335                Fills::own(widget, theme),
2336            );
2337        }
2338    }
2339
2340    // **A stack that is as big as its largest unpinned layer** (`stack.dart:625`): each such
2341    // layer is laid out in a tree of its own under the stack's fit, and the stack is the
2342    // largest of them, held to the room it is offered. With none, or under `Expand`, it is
2343    // the room itself — which its fill request asks for.
2344    if widget.stack() && widget.stack_measured() {
2345        let style = effective_style(widget, id, runtime, theme);
2346        let owned = owned_theme(theme);
2347        let fit = widget.stack_fit();
2348        let layers = widget.children();
2349        let measure: frus_layout::MeasureFn<'a> = Box::new(move |w, h| {
2350            let mut size = Size::new(0.0, 0.0);
2351            // Under `Expand` the stack is the room; otherwise its largest unpinned layer.
2352            // With none, it is nothing here, and its fill request asks for the room.
2353            let sized_by_layers = fit != crate::StackFit::Expand;
2354            for (i, layer) in layers.iter().enumerate() {
2355                if layer.positioned().is_some() {
2356                    continue;
2357                }
2358                let mut inner: Layout<BaselineData> = Layout::new();
2359                let node = build_layout(
2360                    layer.as_ref(),
2361                    child_id(id, i, layer.as_ref()),
2362                    runtime,
2363                    &owned,
2364                    &mut inner,
2365                );
2366                // Forced or handed the room, a layer fills it; allowed it, it is asked —
2367                // and one that asks for the room takes it on its own, as a root.
2368                let fill = fit != crate::StackFit::Loose;
2369                let (fill_x, fill_y) = (fill, fill);
2370                inner.compute_axes(
2371                    node,
2372                    w.unwrap_or(0.0),
2373                    h.unwrap_or(0.0),
2374                    [w.is_none(), h.is_none()],
2375                    [fill_x && w.is_some(), fill_y && h.is_some()],
2376                );
2377                let own = inner.size_of(node);
2378                size = Size::new(size.width.max(own.width), size.height.max(own.height));
2379            }
2380            // The room, on each axis that was offered one, when the layers do not decide.
2381            if !sized_by_layers {
2382                size = Size::new(w.unwrap_or(size.width), h.unwrap_or(size.height));
2383            }
2384            Size::new(
2385                frus_core::fits(size.width).min(w.unwrap_or(f32::INFINITY)),
2386                frus_core::fits(size.height).min(h.unwrap_or(f32::INFINITY)),
2387            )
2388        });
2389        return (
2390            layout.measured_leaf(style, own_baseline, measure),
2391            Fills::own(widget, theme),
2392        );
2393    }
2394
2395    // Scrollables, interactive viewports, fitters (`FittedBox`), navigators, virtualised
2396    // lists and stacks: their content is laid out separately (independent layers / screens /
2397    // items, or a child laid out at its natural size).
2398    if widget.scroll_content().is_some()
2399        || widget.interactive().is_some()
2400        || widget.fitted().is_some()
2401        || widget.navigator().is_some()
2402        || widget.virtual_list(Size::ZERO).is_some()
2403        || widget.page_view().is_some()
2404        || widget.stack()
2405        || widget.custom_layout().is_some()
2406    {
2407        // Nothing bubbles out of these: what is inside is laid out somewhere else, to
2408        // constraints of its own, and cannot have an opinion about this box.
2409        return (
2410            layout.leaf(effective_style(widget, id, runtime, theme), own_baseline),
2411            Fills::own(widget, theme),
2412        );
2413    }
2414    // A portal only lays out its anchor (child 0); the overlay is deferred.
2415    if widget.overlay().is_some() {
2416        let anchor_w = widget.children()[0].as_ref();
2417        let (anchor, fills) = build_layout_scoped(
2418            anchor_w,
2419            child_id(id, 0, anchor_w),
2420            runtime,
2421            theme,
2422            layout,
2423            baselines,
2424        );
2425        // **The anchor takes the room the portal is given**, across the axis it fills. Laid
2426        // in a row, an anchor stretched to the portal's height but kept its own width, so a
2427        // menu row with a submenu open, which is a portal, shrank to its label while every
2428        // other row was as wide as the menu, and its chevron was drawn off the menu's start
2429        // edge (milestone 607). A column stretches it across instead; an anchor that fills
2430        // only the height keeps the row.
2431        let mut style = effective_style(widget, id, runtime, theme);
2432        style.flex_direction = if fills.vertical && !fills.horizontal {
2433            frus_layout::FlexDirection::Row
2434        } else {
2435            frus_layout::FlexDirection::Column
2436        };
2437        return (layout.container(style, &[anchor]), fills);
2438    }
2439    let children = widget.children();
2440    if children.is_empty() {
2441        // A leaf measured under constraints (a paragraph that wraps…): taffy calls the
2442        // closure during the computation.
2443        if let Some(measure) = widget.measure(theme) {
2444            return (
2445                layout.measured_leaf(
2446                    effective_style(widget, id, runtime, theme),
2447                    own_baseline,
2448                    measure,
2449                ),
2450                Fills::own(widget, theme),
2451            );
2452        }
2453        (
2454            layout.leaf(effective_style(widget, id, runtime, theme), own_baseline),
2455            Fills::own(widget, theme),
2456        )
2457    } else {
2458        let style = effective_style(widget, id, runtime, theme);
2459        let built: Vec<(NodeId, Fills)> = children
2460            .iter()
2461            .enumerate()
2462            .map(|(i, child)| {
2463                build_layout_scoped(
2464                    child.as_ref(),
2465                    child_id(id, i, child.as_ref()),
2466                    runtime,
2467                    theme,
2468                    layout,
2469                    baselines,
2470                )
2471            })
2472            .collect();
2473        let child_ids: Vec<NodeId> = built.iter().map(|(node, _)| *node).collect();
2474        // `MainAxisSize::Max`: a run that fills the room its parent leaves it. Only the
2475        // parent knows what that means — grow along an axis it shares, stretch across
2476        // one it does not — which is why the child asks rather than saying.
2477        //
2478        // The request is the child's **and its subtree's**: a column whose row fills the
2479        // width is itself as wide as the room it was given.
2480        let alone = children.len() == 1;
2481        for (node, fills) in &built {
2482            for horizontal in [true, false] {
2483                if !fills.wants(horizontal) {
2484                    continue;
2485                }
2486                let parallel = horizontal == style.flex_direction.is_horizontal();
2487                // Along an axis the parent runs too, the reference leaves the main axis
2488                // **unbounded** and `Max` quietly degrades to `Min`: there is no maximum
2489                // to take. The exception is a parent with a single child — a padding, an
2490                // alignment, a decorated box — which passes its own constraints straight
2491                // down, and there the run does fill.
2492                if parallel && !alone {
2493                    continue;
2494                }
2495                // A size that was only a default gives way to the room (milestone 590).
2496                if fills.soft(horizontal) {
2497                    layout.clear_size(*node, horizontal);
2498                }
2499                layout.fill_parent(*node, horizontal, parallel);
2500            }
2501        }
2502        // A **lone** child gives way; several do not.
2503        //
2504        // The reference hands an inflexible child of a row or a column an unbounded main
2505        // axis: it is never squeezed, and a line that does not fit overflows and says so.
2506        // That is the default here now, because a framework that quietly crushes a button
2507        // instead of reporting a layout that does not fit is one whose bugs are invisible
2508        // — milestone 333 spent three milestones on exactly that crushed button.
2509        //
2510        // But a box with one child is not dividing a line up: it is handing its own
2511        // constraints down, and the reference bounds a lone child by them. The same
2512        // exception the fill request and the floor below both make, for the same reason.
2513        if alone {
2514            for node in &child_ids {
2515                layout.allow_shrink(*node);
2516            }
2517        }
2518        // A grid's tiles are all the same shape. The ratio is the *container's*, since a
2519        // tile cannot know how wide its column came out, and the reference says the same:
2520        // a grid's delegate hands every child a tight box derived from the track, square
2521        // unless the application asked for another ratio.
2522        if let Some(ratio) = widget.tile_shape() {
2523            for node in &child_ids {
2524                layout.set_tile_shape(*node, ratio);
2525            }
2526        }
2527        // A child that refuses to be squeezed along the row. Only a row of **several**
2528        // children may say so. Across a column the same floor would refuse a width the
2529        // column was handing it, which is how a paragraph is told how wide to be; and a
2530        // box with a single child — a padding, an alignment, a decorated box — is handing
2531        // one down rather than dividing a line up, whichever way it nominally runs.
2532        if style.flex_direction.is_horizontal() && !alone {
2533            for (child, node) in children.iter().zip(&child_ids) {
2534                if let Some(floor) = child.main_axis_floor(theme) {
2535                    layout.set_min_width(*node, floor);
2536                }
2537            }
2538        }
2539        // **Baseline** cross-alignment: the children are pushed down until their
2540        // baselines meet, which is the only alignment that makes two runs of different
2541        // sizes read as one line rather than as one row.
2542        //
2543        // It is resolved here rather than by taffy, which cannot: taffy asks a leaf for
2544        // a size and a leaf can only answer with one. The measurement a baseline needs
2545        // is the font's, so the answer has to come from up here, where the widgets are.
2546        //
2547        // The **natural** baseline is the right one to measure even though the children
2548        // will be laid out at some other width: the first line of a piece of text sits
2549        // in the same place however narrow the box gets, since narrowing it adds lines
2550        // below rather than moving the first one.
2551        if style.align == frus_layout::Align::Baseline && style.flex_direction.is_horizontal() {
2552            let own: Vec<Option<f32>> = children
2553                .iter()
2554                .enumerate()
2555                .map(|(i, child)| {
2556                    natural_baseline(
2557                        child.as_ref(),
2558                        child_id(id, i, child.as_ref()),
2559                        runtime,
2560                        theme,
2561                    )
2562                })
2563                .collect();
2564            if let Some(deepest) = own.iter().flatten().copied().reduce(f32::max) {
2565                for (node, b) in child_ids.iter().zip(&own) {
2566                    // A child with no baseline keeps its place: it is a box, and a box
2567                    // has nothing to line up.
2568                    if let Some(b) = b {
2569                        layout.add_margin_top(*node, deepest - b);
2570                    }
2571                }
2572            }
2573        }
2574        // What this container passes on to **its** parent. Across its own axis the
2575        // container's size is its child's, so the request keeps going; along it the
2576        // container divides the room up and the request stops. A container with a single
2577        // child divides nothing, and passes the whole request on.
2578        let mut fills = Fills::own(widget, theme);
2579        // A grid divides its room on both axes and sizes its cells itself — the
2580        // reference's grid hands its tiles tight constraints — so a cell's request stops at
2581        // it (milestone 633).
2582        let divides_both = style.grid_columns.is_some();
2583        for (_, child) in &built {
2584            if divides_both {
2585                continue;
2586            }
2587            fills = fills.merge(if alone {
2588                *child
2589            } else {
2590                child.across(style.flex_direction)
2591            });
2592        }
2593        (
2594            layout.container(style, &child_ids),
2595            fills.bounded_by(&style),
2596        )
2597    }
2598}
2599
2600/// The distance from the top of `widget`'s **natural** box to the first baseline inside
2601/// it, or `None` when there is no text in there to have one.
2602///
2603/// It lays the subtree out on its own, exactly as [`natural_size`] does, and takes the
2604/// first baseline the layout collected. First in *layout* order, which is the order a
2605/// reader would find it in — and it is the leaves that carry them, so an intervening
2606/// padding or alignment is already counted in the rectangle it comes with.
2607pub(crate) fn natural_baseline<Msg>(
2608    widget: &dyn Widget<Msg>,
2609    id: WidgetId,
2610    runtime: &Runtime,
2611    theme: &Theme,
2612) -> Option<f32> {
2613    let mut layout = Layout::new();
2614    let node = build_layout(widget, id, runtime, theme, &mut layout);
2615    layout.compute_scroll(node, 0.0, 0.0, true, true);
2616    let rects = layout.absolute_rects(node);
2617    let top = rects.first()?.0.y;
2618    rects
2619        .iter()
2620        .find_map(|(rect, data)| data.copied().flatten().map(|b| rect.y + b - top))
2621}
2622
2623/// **Natural** (intrinsic) size of a subtree: laid out under free axes (`MaxContent`), with
2624/// no imposed constraint. Used by `RotatedBox` (the dimensions to swap) and — at render time
2625/// — by `FittedBox` (the fit factor).
2626pub(crate) fn natural_size<Msg>(
2627    widget: &dyn Widget<Msg>,
2628    id: WidgetId,
2629    runtime: &Runtime,
2630    theme: &Theme,
2631) -> Size {
2632    let mut layout = Layout::new();
2633    let node = build_layout(widget, id, runtime, theme, &mut layout);
2634    layout.compute_scroll(node, 0.0, 0.0, true, true);
2635    layout
2636        .absolute_rects(node)
2637        .first()
2638        .map(|(r, _)| Size::new(r.width, r.height))
2639        .unwrap_or(Size::new(0.0, 0.0))
2640}
2641
2642/// A deferred overlay: `(content, id, the anchor's bounds, placement, dismissal,
2643/// progress 0..=1, whether it takes the pointer, the scrim's colour, the theme it was
2644/// declared under)`. The progress animates the appearance — a drawer sliding in, a
2645/// scrim fading — and is `1.0` for overlays that are not animated.
2646///
2647/// The **theme travels with it** because an overlay is painted long after the walk has
2648/// left the node that declared it: a dialog opened from inside a [`crate::Themed`]
2649/// subtree would otherwise come out in the root's theme, which is the surprise the
2650/// reference had to grow a whole mechanism to avoid. The scrim's colour travels for the
2651/// same reason — by the time it is painted, the widget that asked for it is long
2652/// behind.
2653type Overlay<'a, Msg> = (
2654    &'a dyn Widget<Msg>,
2655    WidgetId,
2656    Rect,
2657    Placement,
2658    Option<Msg>,
2659    f32,
2660    bool,
2661    Option<frus_core::Color>,
2662    std::rc::Rc<Theme>,
2663);
2664
2665struct Builder<'a, Msg> {
2666    scene: Scene,
2667    hits: Vec<Hit<Msg>>,
2668    long_presses: Vec<Hit<Msg>>,
2669    dismisses: Vec<Msg>,
2670    edit_actions: Vec<(WidgetId, crate::EditAction)>,
2671    toolbars: Vec<Rect>,
2672    overlay_cover: Vec<Rect>,
2673    focusables: Vec<Focusable>,
2674    /// Start of the topmost modal overlay's focus scope.
2675    focus_scope_start: Option<usize>,
2676    /// Set while inside an `ExcludeFocus`: nothing in here registers a focus stop.
2677    focus_excluded: bool,
2678    /// Where a press or a drag can land, when that is less than what is drawn: inside a
2679    /// stack that lets its layers hang over its edges, its own box (milestone 639).
2680    touch_clip: Option<Rect>,
2681    /// The identity of the nearest enclosing backdrop group — the key a backdrop
2682    /// asking to be shared takes. Pushed and popped by the walk, like the focus flags.
2683    backdrop_group: Option<u64>,
2684    /// Set while inside an `ExcludeFocusTraversal`: stops register, Tab passes them by.
2685    focus_skipped: bool,
2686    /// The traversal order in force, from the nearest enclosing `FocusTraversalOrder`.
2687    focus_order: Option<f32>,
2688    /// The nearest enclosing `FocusTraversalGroup`, within which an order is resolved.
2689    focus_group: Option<WidgetId>,
2690    /// The nearest enclosing [`crate::AutofillGroup`]: the form a field is part of.
2691    autofill_group: Option<WidgetId>,
2692    /// Shortcut and action scopes closed so far this frame.
2693    scopes: Vec<Scope<Msg>>,
2694    /// Keystroke listeners, with the focus stops they cover.
2695    #[allow(clippy::type_complexity)]
2696    listeners: Vec<(
2697        std::ops::Range<usize>,
2698        std::rc::Rc<dyn Fn(KeyStroke) -> Option<Msg>>,
2699    )>,
2700    scrollables: Vec<Scrollable>,
2701    scrollbars: Vec<Scrollbar>,
2702    draggables: Vec<(WidgetId, Rect)>,
2703    drag_sources: Vec<DragSource>,
2704    drop_zones: Vec<DropZone>,
2705    inks: Vec<(WidgetId, Rect)>,
2706    /// The detectors that take a drag, in painted order, with the box a press can land in
2707    /// (clipped) and the whole box (milestone 582).
2708    /// The row that wants the title bar's line, where it was laid out, and what the system
2709    /// acts on there (milestones 640, 642).
2710    title_bar_row: Option<Rect>,
2711    title_bar_regions: Vec<(Rect, crate::TitleBarRole)>,
2712    pans: Vec<(WidgetId, Rect, Rect)>,
2713    /// The widgets that hear the pointer's raw events, likewise (milestone 586).
2714    pointer_listeners: Vec<(WidgetId, Rect, Rect)>,
2715    /// The regions the mouse enters and leaves, in painted order (milestone 583).
2716    hover_regions: Vec<HoverEntry>,
2717    text_stops: Vec<crate::TextStop>,
2718    /// The region the mouse enters and leaves that the walk is inside, if any: the parent of
2719    /// the next one registered (milestone 583).
2720    hover_parent: Option<WidgetId>,
2721    /// The [`SelectionArea`](crate::SelectionArea) the walk is inside, if it is inside one, and
2722    /// its widget — which may have a say over the bar.
2723    area: Option<WidgetId>,
2724    area_widget: Option<&'a dyn Widget<Msg>>,
2725    /// Whether the bar over a selection area's selection has been placed this frame: it goes
2726    /// against the first text the selection covers, and only that one.
2727    region_bar_placed: bool,
2728    reorderables: Vec<(WidgetId, Rect)>,
2729    interactives: Vec<(WidgetId, Rect)>,
2730    /// Deferred overlays: (content, id, the anchor's bounds, placement, dismissal, progress
2731    /// `0..=1`). The progress animates the appearance (a drawer sliding in, a scrim fading);
2732    /// it is `1.0` for overlays that are not animated.
2733    overlays: Vec<Overlay<'a, Msg>>,
2734    /// A widget is asking for a continuous, time-driven animation.
2735    wants_animation: bool,
2736    available: Size,
2737    runtime: &'a Runtime,
2738    /// The theme **in force at this point of the walk** — the root's, unless a
2739    /// [`crate::Themed`] ancestor replaced it. Owned rather than borrowed because a
2740    /// subtree's theme is derived from the one above it and outlives nothing.
2741    ///
2742    /// Owned **behind an `Rc`**: a `Theme` is eleven kilobytes, and this field is swapped
2743    /// on the way into a themed subtree and swapped back on the way out. By value that is
2744    /// two copies of eleven kilobytes standing in the walk's frame — one being moved in,
2745    /// one held across the recursion waiting to be put back — on every level of the tree,
2746    /// taken or not. Behind a pointer the swap is a pointer's width, and the walk's frame
2747    /// went from 28 144 bytes a level to 5 264.
2748    theme: std::rc::Rc<Theme>,
2749    /// The inspector's collection (`Some` only while it is on): one node per painted widget,
2750    /// in paint order.
2751    inspector: Option<Vec<crate::inspector::InspectorNode>>,
2752    /// **The widgets this frame builds as it goes** — a list's rows, a paged view's pages,
2753    /// what a builder makes from its box — kept for the rest of the walk, so that they go
2754    /// through the same walk as the tree they sit in (milestone 592). They used to go through
2755    /// a reduced copy of it that knew no overlay, no transform and no nested scroll, and an
2756    /// open menu in a list's row crashed the frame.
2757    built: &'a Built<Msg>,
2758    /// Current depth of the walk (for the dump's indentation and the palette of the
2759    /// inspector's outlines).
2760    depth: usize,
2761    /// The [`crate::RefreshIndicator`] area currently being walked, if any: every scrollable
2762    /// registered under it records it, so the shell knows where to send the movement
2763    /// its physics refuses. Saved and restored around the subtree, so sibling areas do
2764    /// not inherit one another's.
2765    refresh_host: Option<WidgetId>,
2766    /// The scroll region currently being walked, if any: the nearest one enclosing
2767    /// whatever the walk is looking at. Focus stops record it so the shell can reveal
2768    /// them, and nested regions record one another. Saved and restored around the
2769    /// subtree, like [`Builder::refresh_host`].
2770    scroll_host: Option<WidgetId>,
2771    /// Which screen of a route transition the walk is inside (`0` = leaving, `1` =
2772    /// entering); `None` outside one. Recorded on each hero so the two sides of a
2773    /// flight are told apart rather than guessed from paint order.
2774    hero_screen: Option<u8>,
2775    /// The shared elements seen so far, each with the widget that declared it (needed
2776    /// to lift its subtree's painting) and the transition screen it belongs to.
2777    heroes: Vec<(HeroSpot, &'a dyn Widget<Msg>)>,
2778    /// The frame's refresh areas, in paint order.
2779    refreshes: Vec<Refreshable>,
2780    /// The frame's dismissible items, in paint order.
2781    dismissables: Vec<Dismissable>,
2782    /// The frame's sheets, in the order their subtrees finished walking.
2783    sheets: Vec<crate::sheet::SheetArea>,
2784    /// Accessibility nodes collected during the walk (paint order).
2785    semantics: Vec<(WidgetId, Rect, frus_core::SemanticsProperties)>,
2786    /// The parts of the screen saying what the system bars over them should look like,
2787    /// with their boxes, in paint order.
2788    system_ui: Vec<(Rect, crate::SystemUiOverlayStyle)>,
2789    /// Boxes whose children did not fit, screen-positioned, from every sub-root walked
2790    /// this frame.
2791    overflows: std::cell::RefCell<Vec<Overflowing>>,
2792    /// The same, still in their sub-root's own coordinates, waiting for the walk to reach
2793    /// that sub-root and say where on screen it ended up.
2794    pending_overflows: std::cell::RefCell<std::collections::HashMap<WidgetId, Vec<Overflowing>>>,
2795}
2796
2797impl<'a, Msg: Clone + 'static> Builder<'a, Msg> {
2798    /// A layout root's rects, through the relayout cache retained in the runtime (it only
2799    /// recomputes through taffy when the style/structure/constraints have changed). A brief
2800    /// mutable borrow: the `Vec` returned is owned.
2801    ///
2802    /// In **RTL**, taffy computes in LTR (canonical, and cached), then each rect is
2803    /// **mirrored** about the root's width (the 1st rect): the rows reverse, the alignment
2804    /// and the margins flip — without touching the widgets. The text itself is drawn
2805    /// normally inside its moved box (the *internal* bidi is handled by cosmic-text).
2806    fn cached_rects(&self, key: WidgetId, root: &dyn Widget<Msg>, c: Constraints) -> Vec<Rect> {
2807        let (mut rects, overflows) =
2808            self.runtime
2809                .layout_cache
2810                .borrow_mut()
2811                .rects(key, root, self.runtime, &self.theme, c);
2812        self.mirror(&mut rects);
2813        self.record_overflows(key, &rects, overflows);
2814        rects
2815    }
2816
2817    /// Files this sub-root's overflowing boxes under its identity, mirrored with
2818    /// everything else in RTL — where an edge changes name as well as position, since the
2819    /// layout is the mirror image and "ran past the right" becomes "ran past the left".
2820    ///
2821    /// They wait here rather than going straight out because a sub-root's rectangles are
2822    /// in its **own** coordinates: a scrollable's content, a page, a list item and a
2823    /// stack's layer each get their own taffy pass, and where that pass lands on screen is
2824    /// only known when the walk reaches it. [`Self::claim_overflows`] is the other half.
2825    /// Records a child that came out **bigger than the box holding it**, one entry per
2826    /// edge it ran past — the same yellow and black band a row too full of children gets,
2827    /// from a box that laid its child out itself and so was never measured against it.
2828    ///
2829    /// The rectangle is already on the screen, this being the walk rather than a sub-root's
2830    /// own pass, so it goes straight out rather than through [`Self::record_overflows`].
2831    fn report_spill(&self, own: Rect, child: Rect) {
2832        // Per edge, from where the child actually landed: a centred child that is too wide
2833        // runs past **both** sides, and a band on one of them would be a half-truth.
2834        let over = [
2835            (Side::Left, own.x - child.x),
2836            (Side::Top, own.y - child.y),
2837            (Side::Right, (child.x + child.width) - (own.x + own.width)),
2838            (
2839                Side::Bottom,
2840                (child.y + child.height) - (own.y + own.height),
2841            ),
2842        ];
2843        let spills: Vec<Overflowing> = over
2844            .into_iter()
2845            .filter(|(_, amount)| *amount > 0.5)
2846            .map(|(side, amount)| Overflowing {
2847                rect: own,
2848                side,
2849                amount,
2850            })
2851            .collect();
2852        self.overflows.borrow_mut().extend(spills);
2853    }
2854
2855    fn record_overflows(&self, key: WidgetId, mirrored: &[Rect], overflows: Vec<Overflowing>) {
2856        if overflows.is_empty() {
2857            return;
2858        }
2859        let rtl = self.rtl();
2860        let root = mirrored.first().copied();
2861        let list: Vec<Overflowing> = overflows
2862            .into_iter()
2863            .map(|mut o| {
2864                if let (true, Some(root)) = (rtl, root) {
2865                    o.rect.x = root.x + (root.width - (o.rect.x - root.x) - o.rect.width);
2866                    o.side = match o.side {
2867                        Side::Left => Side::Right,
2868                        Side::Right => Side::Left,
2869                        other => other,
2870                    };
2871                }
2872                o
2873            })
2874            .collect();
2875        self.pending_overflows.borrow_mut().insert(key, list);
2876    }
2877
2878    /// The walk has reached sub-root `id` and knows where it sits: its overflowing boxes
2879    /// become screen rectangles.
2880    fn claim_overflows(&self, id: WidgetId, translation: (f32, f32)) {
2881        let taken = self.pending_overflows.borrow_mut().remove(&id);
2882        if let Some(list) = taken {
2883            self.overflows
2884                .borrow_mut()
2885                .extend(list.into_iter().map(|o| Overflowing {
2886                    rect: o.rect.translate(translation.0, translation.1),
2887                    ..o
2888                }));
2889        }
2890    }
2891
2892    /// The layout direction **in force here** — read from the ambient theme rather than
2893    /// from a field set once at the root, so a [`crate::Themed`] subtree carries its own.
2894    ///
2895    /// What that reaches is everything decided during the walk: which edge a drawer
2896    /// slides from, which way an anchored overlay opens, the sign of a rotation, the
2897    /// mirroring of any **layout root** inside the subtree (a scrollable, an overlay).
2898    /// What it does not reach is ordinary flow inside the subtree: the frame's rects are
2899    /// computed once at the root and mirrored there as a whole, so a direction set part
2900    /// way down does not reverse the rows around it. Setting the direction on the theme
2901    /// handed to `build_ui` is still the way to flip an application.
2902    fn rtl(&self) -> bool {
2903        self.theme.direction.is_rtl()
2904    }
2905
2906    /// Mirrors a root's rects horizontally (RTL). The first rect is the root itself: its
2907    /// width is the axis of symmetry.
2908    fn mirror(&self, rects: &mut [Rect]) {
2909        if !self.rtl() {
2910            return;
2911        }
2912        let Some(root) = rects.first().copied() else {
2913            return;
2914        };
2915        // The axis: the rects are relative to the root (origin at root.x).
2916        for r in rects.iter_mut() {
2917            r.x = root.x + (root.width - (r.x - root.x) - r.width);
2918        }
2919    }
2920
2921    /// 64-bit fingerprint of everything painting a boundary subtree reads **without**
2922    /// rebuilding the `view`: each descendant's `Status` (in walk order) and the subtree's
2923    /// absolute rects. An unchanged fingerprint + generation ⇒ a bit-for-bit identical paint.
2924    /// The time is excluded (a cacheable boundary holds no `continuous` widget).
2925    fn boundary_fingerprint(
2926        &self,
2927        widget: &dyn Widget<Msg>,
2928        id: WidgetId,
2929        translation: (f32, f32),
2930        sub: &[Rect],
2931    ) -> u64 {
2932        let mut h = std::collections::hash_map::DefaultHasher::new();
2933        self.rtl().hash(&mut h);
2934        translation.0.to_bits().hash(&mut h);
2935        translation.1.to_bits().hash(&mut h);
2936        for r in sub {
2937            r.x.to_bits().hash(&mut h);
2938            r.y.to_bits().hash(&mut h);
2939            r.width.to_bits().hash(&mut h);
2940            r.height.to_bits().hash(&mut h);
2941        }
2942        self.hash_statuses(widget, id, &mut h);
2943        h.finish()
2944    }
2945
2946    /// Adds `widget`'s `Status` to `h`, then its children's, recursively — following the
2947    /// walk's identity scheme (`child_id`) **exactly**, so it lines up with the order in which
2948    /// `walk_node` paints them.
2949    fn hash_statuses<H: Hasher>(&self, widget: &dyn Widget<Msg>, id: WidgetId, h: &mut H) {
2950        hash_status(&self.full_status(widget, id), h);
2951        // Live ink is part of what the boundary paints, and it moves every frame while a
2952        // splash is alive. Without it here the cached primitives would be replayed and
2953        // the ripple would stand still — and once the ink dries the hash goes back to
2954        // what it was, so the boundary starts hitting again.
2955        if let Some(ripples) = self.runtime.ink.get(&id) {
2956            ripples.hash_state(h);
2957        }
2958        for (i, child) in widget.children().iter().enumerate() {
2959            self.hash_statuses(child.as_ref(), child_id(id, i, child.as_ref()), h);
2960        }
2961    }
2962
2963    /// Current lengths of the builder's collections: the lower bound of the slices to capture
2964    /// for a boundary (see [`capture_since`](Self::capture_since)).
2965    fn snapshot(&self) -> Snapshot {
2966        Snapshot {
2967            scene: self.scene.primitives().len(),
2968            hits: self.hits.len(),
2969            long_presses: self.long_presses.len(),
2970            dismisses: self.dismisses.len(),
2971            focusables: self.focusables.len(),
2972            scrollables: self.scrollables.len(),
2973            draggables: self.draggables.len(),
2974            drag_sources: self.drag_sources.len(),
2975            drop_zones: self.drop_zones.len(),
2976            inks: self.inks.len(),
2977            pans: self.pans.len(),
2978            pointer_listeners: self.pointer_listeners.len(),
2979            hover_regions: self.hover_regions.len(),
2980            text_stops: self.text_stops.len(),
2981            semantics: self.semantics.len(),
2982            system_ui: self.system_ui.len(),
2983            overlays: self.overlays.len(),
2984            focus_scope_start: self.focus_scope_start,
2985        }
2986    }
2987
2988    /// Captures the output produced since `snap` (the collections' *tail slices*) into a
2989    /// [`BoundaryData`]. Returns `None` — hence **not cacheable** — when the subtree pushed an
2990    /// overlay or touched the modal focus scope (global state that cannot be captured here).
2991    fn capture_since(&self, snap: &Snapshot) -> Option<BoundaryData<Msg>> {
2992        if self.overlays.len() != snap.overlays || self.focus_scope_start != snap.focus_scope_start
2993        {
2994            return None;
2995        }
2996        Some(BoundaryData {
2997            prims: self.scene.primitives()[snap.scene..].to_vec(),
2998            hits: self.hits[snap.hits..].to_vec(),
2999            long_presses: self.long_presses[snap.long_presses..].to_vec(),
3000            dismisses: self.dismisses[snap.dismisses..].to_vec(),
3001            focusables: self.focusables[snap.focusables..].to_vec(),
3002            scrollables: self.scrollables[snap.scrollables..].to_vec(),
3003            draggables: self.draggables[snap.draggables..].to_vec(),
3004            drag_sources: self.drag_sources[snap.drag_sources..].to_vec(),
3005            drop_zones: self.drop_zones[snap.drop_zones..].to_vec(),
3006            inks: self.inks[snap.inks..].to_vec(),
3007            pans: self.pans[snap.pans..].to_vec(),
3008            pointer_listeners: self.pointer_listeners[snap.pointer_listeners..].to_vec(),
3009            hover_regions: self.hover_regions[snap.hover_regions..].to_vec(),
3010            text_stops: self.text_stops[snap.text_stops..].to_vec(),
3011            semantics: self.semantics[snap.semantics..].to_vec(),
3012            system_ui: self.system_ui[snap.system_ui..].to_vec(),
3013        })
3014    }
3015
3016    /// Replays a boundary from the cache: primitives already formed (clip/owner baked in) and
3017    /// interaction maps, appended as is.
3018    fn splice_boundary(&mut self, data: BoundaryData<Msg>) {
3019        for p in data.prims {
3020            self.scene.push_primitive(p);
3021        }
3022        self.hits.extend(data.hits);
3023        self.long_presses.extend(data.long_presses);
3024        self.dismisses.extend(data.dismisses);
3025        self.focusables.extend(data.focusables);
3026        self.scrollables.extend(data.scrollables);
3027        self.draggables.extend(data.draggables);
3028        self.drag_sources.extend(data.drag_sources);
3029        self.drop_zones.extend(data.drop_zones);
3030        self.inks.extend(data.inks);
3031        self.pans.extend(data.pans);
3032        self.pointer_listeners.extend(data.pointer_listeners);
3033        self.hover_regions.extend(data.hover_regions);
3034        self.text_stops.extend(data.text_stops);
3035        self.semantics.extend(data.semantics);
3036        self.system_ui.extend(data.system_ui);
3037    }
3038
3039    /// Lengths of every registry a [`ModalBarrier`] can withhold from, taken before its subtree
3040    /// is walked.
3041    fn barrier_base(&self) -> BarrierBase {
3042        BarrierBase {
3043            scene: self.scene.primitives().len(),
3044            hits: self.hits.len(),
3045            long_presses: self.long_presses.len(),
3046            focusables: self.focusables.len(),
3047            scrollables: self.scrollables.len(),
3048            scrollbars: self.scrollbars.len(),
3049            draggables: self.draggables.len(),
3050            drag_sources: self.drag_sources.len(),
3051            drop_zones: self.drop_zones.len(),
3052            inks: self.inks.len(),
3053            pans: self.pans.len(),
3054            pointer_listeners: self.pointer_listeners.len(),
3055            hover_regions: self.hover_regions.len(),
3056            text_stops: self.text_stops.len(),
3057            reorderables: self.reorderables.len(),
3058            interactives: self.interactives.len(),
3059            semantics: self.semantics.len(),
3060        }
3061    }
3062
3063    /// Drops what the subtree added since `base`, according to `barrier`, and — for an
3064    /// absorbing barrier — puts a message-less hit target in its place so that input stops
3065    /// at the barrier instead of reaching whatever is painted behind it.
3066    /// Resolves the **shared elements** of a route transition: every tag that appears
3067    /// on both screens flies from where it was to where it is going.
3068    ///
3069    /// Called once, after both screens have been drawn, because that is the first
3070    /// moment both boxes are known. `hero_base` and `scene_base` mark where those two
3071    /// screens started.
3072    ///
3073    /// What flies is the **destination**'s own painting, mapped onto the box it is
3074    /// travelling through. Both originals are taken out of the frame: a thing that is
3075    /// flying is not also sitting at either end, and leaving them in is the difference
3076    /// between a shared element and three copies of one.
3077    fn fly_heroes(&mut self, hero_base: usize, scene_base: usize, progress: f32) {
3078        let spots: Vec<(HeroSpot, &'a dyn Widget<Msg>)> = self.heroes.split_off(hero_base);
3079        if spots.len() < 2 {
3080            return;
3081        }
3082        // The pairs: one tag, one hero on each side. A tag on one side only has nothing
3083        // to fly to, and a tag used more than once per side is ambiguous — both are
3084        // left alone rather than guessed at.
3085        let mut flights: Vec<(Rect, Rect, &'a dyn Widget<Msg>, WidgetId, WidgetId)> = Vec::new();
3086        for (from, _) in spots.iter().filter(|(s, _)| s.screen == 0) {
3087            let mut matching = spots
3088                .iter()
3089                .filter(|(s, _)| s.screen == 1 && s.tag == from.tag);
3090            let Some((to, widget)) = matching.next() else {
3091                continue;
3092            };
3093            if matching.next().is_some() {
3094                continue;
3095            }
3096            flights.push((from.rect, to.rect, *widget, from.id, to.id));
3097        }
3098        if flights.is_empty() {
3099            return;
3100        }
3101
3102        // Everything the two screens painted, so the originals can be taken out and the
3103        // travelling copies laid on top.
3104        let painted = self.scene.split_off(scene_base);
3105        let mut hidden: std::collections::HashSet<u64> = std::collections::HashSet::new();
3106        for (_, _, widget, from_id, to_id) in &flights {
3107            for id in subtree_ids(*widget, *to_id) {
3108                hidden.insert(id.as_u64());
3109            }
3110            // The source's subtree is the *other* screen's widget, whose identity is
3111            // rooted at `from_id`; the same relative shape, so the ids follow.
3112            for id in subtree_ids(*widget, *from_id) {
3113                hidden.insert(id.as_u64());
3114            }
3115        }
3116        // The destinations' own painting, wherever it was drawn: a page in a transition may
3117        // be inside a layer that zooms or fades it (milestone 620), and a hero in flight is
3118        // above both pages, untouched by either — as the reference's flies in the overlay.
3119        let destinations: std::collections::HashSet<u64> = flights
3120            .iter()
3121            .flat_map(|(_, _, widget, _, to_id)| subtree_ids(*widget, *to_id))
3122            .map(|id| id.as_u64())
3123            .collect();
3124        let mut taken = Vec::new();
3125        for primitive in take_owned(painted, &hidden, &destinations, &mut taken) {
3126            self.scene.push_primitive(primitive);
3127        }
3128        let painted = taken;
3129
3130        self.scene.set_clip(Rect::UNBOUNDED);
3131        for (from, to, _, _, to_id) in &flights {
3132            let travelling = lerp_rect(*from, *to, progress);
3133            if to.width <= 0.0 || to.height <= 0.0 {
3134                continue;
3135            }
3136            let (sx, sy) = (travelling.width / to.width, travelling.height / to.height);
3137            let pivot = Point::new(to.x, to.y);
3138            let owners: std::collections::HashSet<u64> = subtree_ids(
3139                flights
3140                    .iter()
3141                    .find(|(_, _, _, _, id)| id == to_id)
3142                    .map(|(_, _, w, _, _)| *w)
3143                    .expect("the flight just matched"),
3144                *to_id,
3145            )
3146            .iter()
3147            .map(|id| id.as_u64())
3148            .collect();
3149            for primitive in painted.iter().filter(|p| owners.contains(&p.owner())) {
3150                let flown = primitive
3151                    .scaled_about_xy(pivot, sx, sy)
3152                    .translated(travelling.x - to.x, travelling.y - to.y)
3153                    .with_clip(Rect::UNBOUNDED);
3154                self.scene.push_primitive(flown);
3155            }
3156        }
3157    }
3158
3159    /// Reconciles what a [`crate::Semantics`] wrapper's subtree announced with what the
3160    /// wrapper says about it.
3161    ///
3162    /// The wrapper's own answers win, and the subtree's show through wherever it said
3163    /// nothing — a container that knows a child is a *heading* rarely knows what the words
3164    /// are, and dropping them would replace a spoken title with silence.
3165    ///
3166    /// The merged node goes in **where the subtree's first one was**, not at the end. The
3167    /// accessibility tree is flat and its order is reading order, so a wrapper appended
3168    /// after its own contents would be read after them.
3169    fn apply_description(
3170        &mut self,
3171        described: crate::semantics::Description,
3172        base: usize,
3173        id: WidgetId,
3174        own: Rect,
3175    ) {
3176        let mut props = described.props;
3177        // Merging swallows every child's own entry, so **something** has to answer for
3178        // this node when it is activated — the wrapper itself never does (see
3179        // `Semantics::on_click`'s own reasoning: an annotation that swallowed a click
3180        // would put a hit target over the widget it describes). Found testing #18 in a
3181        // real browser and real screen reader: a `Checkbox::new(..).on_toggle(..)`
3182        // merged with its row's caption read correctly and did **nothing** on
3183        // activation, on every platform, because the merged entry carried the row's own
3184        // id, which nothing routes a click through. When exactly one child in the
3185        // swallowed subtree was clickable, its id is who a click or a keyboard
3186        // activation has to reach, so the merged entry keys on that id instead — the
3187        // wrapper's id is kept only when there is no such child, or more than one and
3188        // "the one that gets clicked" has no single answer.
3189        let mut operable = None;
3190        if described.merging {
3191            let mut clickable_count = 0;
3192            for (child_id, _, child) in self.semantics.drain(base..).collect::<Vec<_>>() {
3193                if child.clickable {
3194                    clickable_count += 1;
3195                    operable = Some(child_id);
3196                }
3197                props = props.over(&child);
3198            }
3199            if clickable_count != 1 {
3200                operable = None;
3201            }
3202        }
3203        let id = operable.unwrap_or(id);
3204        if props.is_meaningful() && own.width > 0.0 && own.height > 0.0 {
3205            self.semantics.insert(base, (id, own, props));
3206        }
3207    }
3208
3209    /// Gives a **tappable area** its accessibility node, from the entries its subtree added
3210    /// since `base` (milestone 700).
3211    ///
3212    /// The reference gives anything with a tap handler a node carrying the tap, and a screen
3213    /// reader names it from the words inside. The tree here is flat, so the words are taken
3214    /// in: the plain texts, headings and described images under the area — the entries that
3215    /// only *say* something — become its label, one line each, and leave the tree, so they
3216    /// are not read a second time. What can be operated on its own (a checkbox, a field, a
3217    /// button, another tappable area) keeps its entry, after this one.
3218    ///
3219    /// A widget that describes itself without a name — a `Button::with_child(..)` — keeps its
3220    /// own entry and role and takes the words as its label, as the reference's button reads
3221    /// the text inside it. One with no entry at all gets a button node: the role that says
3222    /// "this can be pressed" on every platform, where a generic container with a click action
3223    /// is dropped from the tree by some of them.
3224    ///
3225    /// An area with no entry of its own and no words, whose subtree holds exactly one
3226    /// clickable entry, gets nothing: that entry already answers for the press, and a second,
3227    /// unnamed "button" around it would only be noise.
3228    fn apply_tap_area(&mut self, base: usize, id: WidgetId, own: Rect) {
3229        let mut words: Option<String> = None;
3230        let mut kept = Vec::new();
3231        let mut clickables = 0;
3232        let mut entries = self.semantics.drain(base..).collect::<Vec<_>>();
3233        // The widget's own entry, when it has one, is the first: registered before its
3234        // children are walked.
3235        let described =
3236            (entries.first().is_some_and(|(first, _, _)| *first == id)).then(|| entries.remove(0));
3237        for entry in entries {
3238            let (_, _, ref sem) = entry;
3239            let says_only = !sem.clickable
3240                && matches!(
3241                    sem.role,
3242                    frus_core::Role::Label
3243                        | frus_core::Role::Heading
3244                        | frus_core::Role::Image
3245                        | frus_core::Role::None
3246                );
3247            match (&sem.label, says_only) {
3248                (Some(label), true) => {
3249                    words = Some(match words {
3250                        Some(before) => format!("{before}\n{label}"),
3251                        None => label.clone(),
3252                    });
3253                }
3254                _ => {
3255                    clickables += usize::from(sem.clickable);
3256                    kept.push(entry);
3257                }
3258            }
3259        }
3260        if let Some((own_id, rect, mut props)) = described {
3261            if props.label.is_none() {
3262                props.label = words;
3263            }
3264            self.semantics.push((own_id, rect, props));
3265            self.semantics.extend(kept);
3266            return;
3267        }
3268        let silent_wrapper = words.is_none() && clickables == 1;
3269        if !silent_wrapper && own.width > 0.0 && own.height > 0.0 {
3270            let mut props = frus_core::SemanticsProperties::new(frus_core::Role::Button);
3271            props.label = words;
3272            props.clickable = true;
3273            self.semantics.push((id, own, props));
3274        }
3275        self.semantics.extend(kept);
3276    }
3277
3278    fn apply_barrier(
3279        &mut self,
3280        barrier: ModalBarrier,
3281        base: &BarrierBase,
3282        id: WidgetId,
3283        own: Rect,
3284    ) {
3285        if barrier.pointer {
3286            self.hits.truncate(base.hits);
3287            self.long_presses.truncate(base.long_presses);
3288            self.focusables.truncate(base.focusables);
3289            self.scrollables.truncate(base.scrollables);
3290            self.scrollbars.truncate(base.scrollbars);
3291            self.draggables.truncate(base.draggables);
3292            self.drag_sources.truncate(base.drag_sources);
3293            self.drop_zones.truncate(base.drop_zones);
3294            self.inks.truncate(base.inks);
3295            self.pans.truncate(base.pans);
3296            self.pointer_listeners.truncate(base.pointer_listeners);
3297            self.hover_regions.truncate(base.hover_regions);
3298            self.text_stops.truncate(base.text_stops);
3299            self.reorderables.truncate(base.reorderables);
3300            self.interactives.truncate(base.interactives);
3301            // The modal focus scope is an index **into** `focusables`. A barrier that cut
3302            // away the focusable it pointed at would leave it dangling past the end, and
3303            // every later slice of the pool would then be empty — no focusable at all,
3304            // rather than the ones the barrier meant to spare.
3305            if let Some(start) = self.focus_scope_start {
3306                self.focus_scope_start = Some(start.min(self.focusables.len()));
3307            }
3308        }
3309        if barrier.absorb && own.width > 0.0 && own.height > 0.0 {
3310            self.hits.push(Hit {
3311                id,
3312                rect: own,
3313                msg: None,
3314                xform: None,
3315            });
3316        }
3317        if barrier.paint {
3318            // The clip is baked into each primitive when it is pushed, so dropping the tail
3319            // leaves nothing for the next sibling to repair.
3320            let _hidden = self.scene.split_off(base.scene);
3321        }
3322        if barrier.semantics {
3323            self.semantics.truncate(base.semantics);
3324        }
3325    }
3326
3327    /// Lower bounds of the interaction registries **before** a transformed composited subtree.
3328    fn xform_base(&self) -> XformBase {
3329        XformBase {
3330            hits: self.hits.len(),
3331            long_presses: self.long_presses.len(),
3332            focusables: self.focusables.len(),
3333            scrollables: self.scrollables.len(),
3334            draggables: self.draggables.len(),
3335            drag_sources: self.drag_sources.len(),
3336            drop_zones: self.drop_zones.len(),
3337            inks: self.inks.len(),
3338            pans: self.pans.len(),
3339            pointer_listeners: self.pointer_listeners.len(),
3340            hover_regions: self.hover_regions.len(),
3341            text_stops: self.text_stops.len(),
3342            reorderables: self.reorderables.len(),
3343            semantics: self.semantics.len(),
3344        }
3345    }
3346
3347    /// Applies the layer transform `matrix` to the registry entries **added since** `base`:
3348    /// the test point of click / long-press targets goes through `M⁻¹` (without overwriting an
3349    /// inner transform already set); and when `matrix` preserves axis alignment
3350    /// (scale/translation, no rotation), the **rects** of focus / scrolling / dragging /
3351    /// **reordering** / accessibility are mapped exactly by `M` (otherwise they are left as
3352    /// they are — the click stays correct through `M⁻¹`). The **shared factor** of the two
3353    /// transformed-composition sites (`walk`'s boundary and `emit_transformed_child`): one
3354    /// place to keep up to date, which forgets no registry (milestone 250 had forgotten
3355    /// `reorderables` in exactly one of the two).
3356    fn transform_interaction_registries(&mut self, base: &XformBase, matrix: Affine) {
3357        let inverse = matrix.inverse();
3358        for h in &mut self.hits[base.hits..] {
3359            h.xform.get_or_insert(inverse);
3360        }
3361        for h in &mut self.long_presses[base.long_presses..] {
3362            h.xform.get_or_insert(inverse);
3363        }
3364        if matrix.is_axis_aligned() {
3365            for f in &mut self.focusables[base.focusables..] {
3366                f.rect = matrix.apply_rect(f.rect);
3367                f.bounds = matrix.apply_rect(f.bounds);
3368            }
3369            for area in &mut self.scrollables[base.scrollables..] {
3370                area.viewport = matrix.apply_rect(area.viewport);
3371            }
3372            for source in &mut self.drag_sources[base.drag_sources..] {
3373                source.rect = matrix.apply_rect(source.rect);
3374            }
3375            for zone in &mut self.drop_zones[base.drop_zones..] {
3376                zone.rect = matrix.apply_rect(zone.rect);
3377            }
3378            for (_, r) in &mut self.inks[base.inks..] {
3379                *r = matrix.apply_rect(*r);
3380            }
3381            for (_, seen, whole) in &mut self.pans[base.pans..] {
3382                *seen = matrix.apply_rect(*seen);
3383                *whole = matrix.apply_rect(*whole);
3384            }
3385            for (_, seen, whole) in &mut self.pointer_listeners[base.pointer_listeners..] {
3386                *seen = matrix.apply_rect(*seen);
3387                *whole = matrix.apply_rect(*whole);
3388            }
3389            for region in &mut self.hover_regions[base.hover_regions..] {
3390                region.seen = matrix.apply_rect(region.seen);
3391                region.whole = matrix.apply_rect(region.whole);
3392            }
3393            for stop in &mut self.text_stops[base.text_stops..] {
3394                stop.rect = matrix.apply_rect(stop.rect);
3395            }
3396            for (_, r) in &mut self.draggables[base.draggables..] {
3397                *r = matrix.apply_rect(*r);
3398            }
3399            for (_, r) in &mut self.reorderables[base.reorderables..] {
3400                *r = matrix.apply_rect(*r);
3401            }
3402            for (_, r, _) in &mut self.semantics[base.semantics..] {
3403                *r = matrix.apply_rect(*r);
3404            }
3405        }
3406    }
3407
3408    /// Entry point for walking a node. When the node is a cacheable **repaint boundary**, it
3409    /// tries to replay its subtree from the paint cache; otherwise (or on a *miss*) it
3410    /// delegates to the full walk [`walk_node`](Self::walk_node), capturing its output for the
3411    /// next frame. Every recursion goes through here → nested boundaries get cached too.
3412    /// The focus flags a subtree carries — exclusion, Tab-skipping, an order, a group —
3413    /// are **subtree-scoped**, so they are pushed here and popped on the way out rather
3414    /// than asked of each widget at the moment it registers. The body of the walk has
3415    /// early returns in a dozen branches; wrapping it is the only way to be sure the
3416    /// scope is closed on every one of them.
3417    fn walk(
3418        &mut self,
3419        widget: &'a dyn Widget<Msg>,
3420        id: WidgetId,
3421        translation: (f32, f32),
3422        clip: Rect,
3423        rects: &[Rect],
3424        index: &mut usize,
3425    ) {
3426        let outer = (
3427            self.focus_excluded,
3428            self.focus_skipped,
3429            self.focus_order,
3430            self.focus_group,
3431        );
3432        if !widget.descendants_focusable() {
3433            self.focus_excluded = true;
3434        }
3435        if widget.focus_skip_traversal() {
3436            self.focus_skipped = true;
3437        }
3438        if let Some(order) = widget.focus_order() {
3439            self.focus_order = Some(order);
3440        }
3441        if widget.focus_group() {
3442            self.focus_group = Some(id);
3443        }
3444        // The backdrop group a subtree sits in, scoped the same way and for the same
3445        // reason. Its own identity is the key: stable across frames, and unique
3446        // without anything having to hand one out.
3447        let outer_group = self.backdrop_group;
3448        if widget.backdrop_group() {
3449            self.backdrop_group = Some(id.as_u64());
3450        }
3451        // The form a field belongs to, scoped the same way (milestone 512).
3452        let outer_form = self.autofill_group;
3453        if widget.autofill_group() {
3454            self.autofill_group = Some(id);
3455        }
3456        // The focus stops this subtree contains start here; the walk is depth-first, so
3457        // they are contiguous and the range closes below.
3458        let stops_before = self.focusables.len();
3459        self.walk_scoped(widget, id, translation, clip, rects, index);
3460        (
3461            self.focus_excluded,
3462            self.focus_skipped,
3463            self.focus_order,
3464            self.focus_group,
3465        ) = outer;
3466        self.backdrop_group = outer_group;
3467        self.autofill_group = outer_form;
3468        self.close_scope(widget, stops_before);
3469    }
3470
3471    /// Files whatever tables this subtree carried, with the focus stops it turned out to
3472    /// contain. Recorded **on the way out**, because the range is not known on the way in;
3473    /// so `scopes` ends up innermost-first among any that overlap, which is the order the
3474    /// resolution wants and the reason it is not sorted afterwards.
3475    fn close_scope(&mut self, widget: &'a dyn Widget<Msg>, stops_before: usize) {
3476        let range = stops_before..self.focusables.len();
3477        if let Some(on_key) = widget.on_keystroke() {
3478            self.listeners.push((range.clone(), on_key));
3479        }
3480        let (strokes, callbacks) = (widget.shortcut_bindings(), widget.shortcut_callbacks());
3481        let (actions, listeners) = (widget.action_bindings(), widget.action_listeners());
3482        if strokes.is_empty() && callbacks.is_empty() && actions.is_empty() && listeners.is_empty()
3483        {
3484            return;
3485        }
3486        self.scopes.push(Scope {
3487            strokes: strokes.to_vec(),
3488            callbacks: callbacks.to_vec(),
3489            actions: actions.to_vec(),
3490            listeners: listeners.to_vec(),
3491            range,
3492        });
3493    }
3494
3495    fn walk_scoped(
3496        &mut self,
3497        widget: &'a dyn Widget<Msg>,
3498        id: WidgetId,
3499        translation: (f32, f32),
3500        clip: Rect,
3501        rects: &[Rect],
3502        index: &mut usize,
3503    ) {
3504        self.claim_overflows(id, translation);
3505        // A **sheet's panel**: the subtree is walked as usual, and every scroll area it
3506        // registered is recorded against the sheet, so the shell knows which lists share a
3507        // finger with it. By range, since a subtree's registrations are contiguous —
3508        // a replayed boundary splices its own in the same place.
3509        if let Some(spec) = widget.sheet() {
3510            let panel = rects[*index].translate(translation.0, translation.1);
3511            let first = self.scrollables.len();
3512            self.walk_node(widget, id, translation, clip, rects, index);
3513            let areas = self.scrollables[first..]
3514                .iter()
3515                .map(|area| area.id)
3516                .collect();
3517            // What the share is a share of. At nought there is nothing to divide by, so the
3518            // height last measured stands in: a finger that lowered the panel to nothing
3519            // may still be holding it.
3520            let size = self.runtime.sheet_size(id, spec);
3521            let available = if size > 0.0 {
3522                panel.height / size
3523            } else {
3524                self.runtime
3525                    .sheets
3526                    .get(&id)
3527                    .map_or(0.0, |sheet| sheet.available())
3528            };
3529            self.sheets.push(crate::sheet::SheetArea {
3530                id,
3531                panel,
3532                available,
3533                spec: spec.clone(),
3534                areas,
3535            });
3536            return;
3537        }
3538
3539        // A **refresh area**: the subtree is walked with this widget named as the host, so
3540        // every scrollable inside records where to send the movement its physics refuses at
3541        // the top edge. The indicator is then painted **over** the subtree — painting it in
3542        // the widget's own `paint` would put it under the list it belongs to.
3543        if let Some(spec) = widget.refresh() {
3544            let viewport = rects[*index]
3545                .translate(translation.0, translation.1)
3546                .intersect(clip);
3547            let outer = self.refresh_host.replace(id);
3548            self.walk_node(widget, id, translation, clip, rects, index);
3549            self.refresh_host = outer;
3550
3551            let area = Refreshable { id, viewport, spec };
3552            if let Some(pull) = self.runtime.refresh.get(&id) {
3553                crate::refresh::paint_refresh(&mut self.scene, &area, pull, &self.theme, clip);
3554                // A pull that is settling, spinning or fading away drives itself, so the
3555                // frame after this one has to happen.
3556                self.wants_animation = true;
3557            }
3558            self.refreshes.push(area);
3559            return;
3560        }
3561
3562        // A **barrier** (`IgnorePointer`, `AbsorbPointer`, a hidden `Visibility`,
3563        // `ExcludeSemantics`): the subtree is walked exactly as usual, and then whatever it
3564        // added to the withheld registries is dropped again.
3565        //
3566        // Removing afterwards rather than skipping the walk is what makes it exact. A widget
3567        // deep inside registers its click target, focus stop, scrollable area or accessibility
3568        // node without knowing that something above is holding the subtree out of the frame;
3569        // truncating at the barrier catches every one of them, including those added by
3570        // widgets written after this code. It also keeps the walk's rect indexing untouched,
3571        // which a skipped subtree would break.
3572        // A **description** (`Semantics`): something above states what the subtree *is*,
3573        // which the subtree cannot state for itself. Same shape as the barrier below and for
3574        // the same reason — walk first, reconcile after, so a widget deep inside annotates
3575        // itself without knowing anything is speaking for it.
3576        if let Some(described) = widget.describes() {
3577            let base = self.semantics.len();
3578            let own = rects[*index]
3579                .translate(translation.0, translation.1)
3580                .intersect(clip);
3581            self.walk_node(widget, id, translation, clip, rects, index);
3582            self.apply_description(described, base, id, own);
3583            return;
3584        }
3585
3586        if let Some(barrier) = widget.barrier().filter(|b| !b.is_none()) {
3587            let base = self.barrier_base();
3588            // The barrier's own box, before `walk_node` advances the index past it.
3589            let own = rects[*index]
3590                .translate(translation.0, translation.1)
3591                .intersect(clip);
3592            self.walk_node(widget, id, translation, clip, rects, index);
3593            self.apply_barrier(barrier, &base, id, own);
3594            return;
3595        }
3596
3597        // An opacity group: the subtree is painted normally, then its range of primitives is
3598        // **drained** into a composited layer at the group's opacity — so overlaps do not
3599        // double-blend. The animated opacity is the value the runtime tweened; otherwise the
3600        // fixed target. Opaque (≈1): no layer at all (zero cost).
3601        if let Some(target) = widget.opacity_group() {
3602            let opacity = self.runtime.value_or(id, target).clamp(0.0, 1.0);
3603            // Transparent (≈0): walked, so that what it registers is still there, and not
3604            // painted at all — the reference's faded-out child costs nothing to draw either
3605            // (milestone 595). A layer of nothing would still be a primitive in the frame.
3606            if opacity < 0.001 {
3607                let start = self.scene.primitives().len();
3608                self.walk_node(widget, id, translation, clip, rects, index);
3609                let _unseen = self.scene.split_off(start);
3610                return;
3611            }
3612            if opacity < 0.999 {
3613                let start = self.scene.primitives().len();
3614                self.walk_node(widget, id, translation, clip, rects, index);
3615                let group = self.scene.split_off(start);
3616                self.scene.push_primitive(Primitive::Layer {
3617                    primitives: group,
3618                    opacity,
3619                    clip,
3620                    clip_shape: ClipShape::Rect,
3621                    transform: None,
3622                    filter: LayerFilter::NONE,
3623                    owner: id.as_u64(),
3624                });
3625                return;
3626            }
3627            // Fully opaque: an ordinary render (no pointless layer).
3628        }
3629
3630        // A **pixel effect** over the subtree (`ColorFiltered`, `ImageFiltered`,
3631        // `ShaderMask`): painted normally, then drained into a composited layer that
3632        // carries the effect. The renderer applies it when it composites — the image
3633        // filter as a pre-pass over the layer texture, the colour filter and the mask
3634        // in the compositing fragment.
3635        //
3636        // The box is passed in because a mask is written in fractions of the box it
3637        // covers, and this is the first point at which that box is a place on screen.
3638        if let Some(filter) = widget.layer_filter(crate::widget::FilterContext {
3639            box_rect: rects[*index].translate(translation.0, translation.1),
3640            backdrop_group: self.backdrop_group,
3641        }) {
3642            // The layer keeps the **ambient** clip rather than its own box: an image
3643            // filter reaches past the pixels it came from, and cutting it at the box
3644            // would shave the edge off every blur.
3645            let start = self.scene.primitives().len();
3646            self.walk_node(widget, id, translation, clip, rects, index);
3647            let group = self.scene.split_off(start);
3648            let (group, filter, clip_box) = fold_filter(group, filter, clip);
3649            self.scene.push_primitive(Primitive::Layer {
3650                primitives: group,
3651                opacity: 1.0,
3652                clip: clip_box,
3653                clip_shape: ClipShape::Rect,
3654                transform: None,
3655                filter,
3656                owner: id.as_u64(),
3657            });
3658            return;
3659        }
3660
3661        // **Shape** clipping: the subtree is painted normally, bounded to the widget's box,
3662        // then its primitives are **drained** into a composited layer whose shape (rounded
3663        // corners / ellipse) modulates the alpha — the overflowing corners are erased at
3664        // compositing time. The shape's box is the layer's clip rect. Two sources: `ClipPath`
3665        // (an arbitrary path, taking priority) or `ClipRRect`/`ClipOval` (an analytic shape).
3666        // The local path is **offset to the box's screen position**.
3667        if widget.clip_path().is_some() || widget.clip_shape().is_some() {
3668            let box_rect = rects[*index].translate(translation.0, translation.1);
3669            let shape = widget
3670                .clip_path()
3671                .map(|p| ClipShape::Path(p.translated(box_rect.x, box_rect.y)))
3672                .or_else(|| widget.clip_shape())
3673                .expect("clip_path or clip_shape is Some");
3674            let clip_box = clip.intersect(box_rect);
3675            let start = self.scene.primitives().len();
3676            self.walk_node(widget, id, translation, clip_box, rects, index);
3677            let group = self.scene.split_off(start);
3678            // A clip around a filter is **one** layer, not two. It matters most for a
3679            // backdrop: the reference's own advice is to wrap one in a clip to give it
3680            // its shape, and a backdrop pushed a level down would be filtering the
3681            // clip's contents rather than the frame.
3682            let (group, filter, clip_box) = fold_filter(group, LayerFilter::NONE, clip_box);
3683            self.scene.push_primitive(Primitive::Layer {
3684                primitives: group,
3685                opacity: 1.0,
3686                clip: clip_box,
3687                clip_shape: shape,
3688                transform: None,
3689                filter,
3690                owner: id.as_u64(),
3691            });
3692            return;
3693        }
3694
3695        // Composable paint transforms (`Transform`: scale and/or rotation; translation goes
3696        // through the child offset instead). Scale and rotation are melted into **a single
3697        // affine matrix** `M`, the subtree is painted **flat**, then wrapped in a composited
3698        // layer transformed by `M`. The hit-test applies `M⁻¹` to the point. The translation,
3699        // applied upstream through `child_offset`, is the innermost one.
3700        //
3701        // A transform that was told to **animate** hands over its numbers, and only its
3702        // numbers: the pivots stay the widget's, because a pivot is a choice of origin
3703        // rather than a quantity and interpolating it would slide the subtree across the
3704        // screen with nothing describing it having changed. Where the runtime has never
3705        // heard of this widget the static value stands, which is the same rule every
3706        // other animated property follows on mount.
3707        let moving = self.runtime.anim_transform(id);
3708        let scale = widget
3709            .transform_scale()
3710            .map(|(sx, sy, pivot)| {
3711                moving.map_or((sx, sy, pivot), |m| (m.scale_x, m.scale_y, pivot))
3712            })
3713            .filter(|(sx, sy, _)| (sx - 1.0).abs() > 1e-4 || (sy - 1.0).abs() > 1e-4);
3714        let rotate = widget
3715            .transform_rotate()
3716            .map(|(angle, pivot)| moving.map_or((angle, pivot), |m| (m.rotation, pivot)))
3717            .filter(|(a, _)| a.abs() > 1e-4);
3718        if scale.is_some() || rotate.is_some() {
3719            // The pivots are taken on the **child's** box (the next node in prefix order): it
3720            // is the child that gets transformed, and its box hugs the content even when the
3721            // Transform's own box is stretched by the parent.
3722            let basis = rects.get(*index + 1).copied().unwrap_or(rects[*index]);
3723            let box_rect = basis.translate(translation.0, translation.1);
3724            let pivot_of = |align: frus_core::Alignment| {
3725                Point::new(
3726                    box_rect.x + box_rect.width * align.fraction_x(),
3727                    box_rect.y + box_rect.height * align.fraction_y(),
3728                )
3729            };
3730            // The composition: scale (about its own pivot) **then** rotation (about its own).
3731            // In RTL the world is mirrored → the rotation direction is inverted.
3732            let mut matrix = Affine::IDENTITY;
3733            if let Some((sx, sy, pivot_align)) = scale {
3734                matrix = matrix.then(Affine::scale(sx, sy).about(pivot_of(pivot_align)));
3735            }
3736            if let Some((angle, pivot_align)) = rotate {
3737                let angle = if self.rtl() { -angle } else { angle };
3738                matrix = matrix.then(Affine::rotation(angle).about(pivot_of(pivot_align)));
3739            }
3740
3741            let p0 = self.scene.primitives().len();
3742            let base = self.xform_base();
3743            self.walk_node(widget, id, translation, clip, rects, index);
3744            // Wraps the range of primitives — painted flat — in a layer transformed by `M`
3745            // (scale/rotation applied at compositing time).
3746            let group = self.scene.split_off(p0);
3747            self.scene.push_primitive(Primitive::Layer {
3748                primitives: group,
3749                opacity: 1.0,
3750                clip,
3751                clip_shape: ClipShape::Rect,
3752                transform: Some(LayerTransform::new(matrix)),
3753                filter: LayerFilter::NONE,
3754                owner: id.as_u64(),
3755            });
3756            // Counter-transforms the hit-test and maps the subtree's interaction rects.
3757            self.transform_interaction_registries(&base, matrix);
3758            return;
3759        }
3760
3761        // The inspector wants a full walk (it collects every node); caching only happens
3762        // outside inspection.
3763        if self.inspector.is_none() && widget.repaint_boundary() {
3764            if let Some(count) = plain_subtree_len(widget) {
3765                let start = *index;
3766                let sub = &rects[start..start + count];
3767                let fp = self.boundary_fingerprint(widget, id, translation, sub);
3768
3769                // Hit: the same generation (config) + the same fingerprint (state+geometry)
3770                // ⇒ the paint would be identical. The output is cloned and replayed.
3771                let hit = {
3772                    let mut pc = self.runtime.paint_cache.borrow_mut();
3773                    pc.get(id, fp).and_then(|(rc, any)| {
3774                        any.downcast_ref::<BoundaryData<Msg>>()
3775                            .map(|d| (rc, d.clone()))
3776                    })
3777                };
3778                if let Some((rc, data)) = hit {
3779                    self.runtime.paint_cache.borrow_mut().note_hit();
3780                    self.splice_boundary(data);
3781                    *index += rc;
3782                    return;
3783                }
3784
3785                // Miss: paints normally, capturing the subtree's output (primitives +
3786                // interaction maps) for the next frame.
3787                let snap = self.snapshot();
3788                self.walk_node(widget, id, translation, clip, rects, index);
3789                self.runtime.paint_cache.borrow_mut().note_miss();
3790                if let Some(data) = self.capture_since(&snap) {
3791                    self.runtime
3792                        .paint_cache
3793                        .borrow_mut()
3794                        .put(id, fp, count, Box::new(data));
3795                }
3796                return;
3797            }
3798        }
3799        self.walk_node(widget, id, translation, clip, rects, index);
3800    }
3801
3802    /// Deforms the region's content by its overscroll **stretch**, if it has one
3803    /// (milestone 591): everything painted since `mark` goes into one layer, scaled along
3804    /// the axis from the edge pulled, and what can be touched in it moves with it.
3805    fn add_overscroll_stretch(
3806        &mut self,
3807        id: WidgetId,
3808        viewport: Rect,
3809        clip: Rect,
3810        mark: (usize, XformBase),
3811    ) {
3812        let Some(matrix) = self
3813            .runtime
3814            .scroll_stretch
3815            .get(&id)
3816            .and_then(|stretch| stretch.transform(viewport))
3817        else {
3818            return;
3819        };
3820        let (p0, base) = mark;
3821        let group = self.scene.split_off(p0);
3822        self.scene.push_primitive(Primitive::Layer {
3823            primitives: group,
3824            opacity: 1.0,
3825            clip,
3826            clip_shape: ClipShape::Rect,
3827            transform: Some(LayerTransform::new(matrix)),
3828            filter: LayerFilter::NONE,
3829            owner: id.as_u64(),
3830        });
3831        self.transform_interaction_registries(&base, matrix);
3832    }
3833
3834    /// Paints a child **flat** (laid out separately, at `translation`, with its own rects)
3835    /// then wraps it in a composited layer transformed by `matrix` and clipped to `clip`. The
3836    /// hit-test counter-transforms the point (`M⁻¹`); when `matrix` stays axis-aligned
3837    /// (scale/translation), the focus / scroll / drag / accessibility bounds are transformed
3838    /// too. The shared factor of `InteractiveViewer`, `RotatedBox` and `FittedBox`.
3839    // The child, its identity, and the four things that place it. Splitting them would
3840    // only mean passing the same values in two hops.
3841    #[allow(clippy::too_many_arguments)]
3842    fn emit_transformed_child(
3843        &mut self,
3844        child: &'a dyn Widget<Msg>,
3845        cid: WidgetId,
3846        translation: (f32, f32),
3847        clip: Rect,
3848        child_rects: &[Rect],
3849        matrix: Affine,
3850        owner: WidgetId,
3851    ) {
3852        let p0 = self.scene.primitives().len();
3853        let base = self.xform_base();
3854        let mut child_index = 0;
3855        self.walk(child, cid, translation, clip, child_rects, &mut child_index);
3856        let group = self.scene.split_off(p0);
3857        self.scene.push_primitive(Primitive::Layer {
3858            primitives: group,
3859            opacity: 1.0,
3860            clip,
3861            clip_shape: ClipShape::Rect,
3862            transform: Some(LayerTransform::new(matrix)),
3863            filter: LayerFilter::NONE,
3864            owner: owner.as_u64(),
3865        });
3866        self.transform_interaction_registries(&base, matrix);
3867    }
3868
3869    /// Paints one node, **under the theme it asks for**. A [`crate::Themed`] replaces the
3870    /// ambient theme for itself and everything below it; it is put back on the way out, so
3871    /// the sibling that follows is untouched. Everything the subtree reads — its paint,
3872    /// its ink, its focus ring, its layout through `cached_rects`, and any overlay it
3873    /// defers — goes through this field, which is what makes one swap enough.
3874    fn walk_node(
3875        &mut self,
3876        widget: &'a dyn Widget<Msg>,
3877        id: WidgetId,
3878        translation: (f32, f32),
3879        clip: Rect,
3880        rects: &[Rect],
3881        index: &mut usize,
3882    ) {
3883        // The box this node is about to be given, read **before** the walk consumes the
3884        // index: a decoration painted over the children needs it once they are done, and
3885        // by then `*index` has moved past the whole subtree.
3886        let over = rects
3887            .get(*index)
3888            .copied()
3889            .map(|r| r.translate(translation.0, translation.1));
3890        let outer = scoped_theme(widget, id, self.runtime, &self.theme)
3891            .map(|theme| std::mem::replace(&mut self.theme, std::rc::Rc::from(theme)));
3892        // The node that turns the texts under it selectable together is a selection area:
3893        // the texts it registers below belong to it, until the walk leaves it.
3894        let outer_area = outer
3895            .as_ref()
3896            .filter(|before| !before.widgets.text.selectable && self.theme.widgets.text.selectable)
3897            .map(|_| (self.area.replace(id), self.area_widget.replace(widget)));
3898        // The scoped surface, held for this subtree exactly as the layout walk holds it —
3899        // a widget that paints from `MediaQuery::of()` must see the same description it
3900        // was measured against, or the two disagree by whatever the scope removed.
3901        let _surface = widget
3902            .media_override(crate::MediaQuery::of())
3903            .map(crate::MediaQuery::install);
3904        let _shell = widget.scaffold_override().map(crate::ScaffoldInfo::install);
3905        let _scrolling = widget
3906            .scroll_behavior_override()
3907            .map(crate::ScrollConfiguration::install);
3908        // A region the mouse enters and leaves: registered before its subtree, so that the
3909        // regions inside it name it as their parent (milestone 583).
3910        let outer_hover = match (widget.hover_region(), over) {
3911            (Some(spec), Some(whole)) => {
3912                self.register_hover_region(id, spec, whole, clip);
3913                Some(self.hover_parent.replace(id))
3914            }
3915            _ => None,
3916        };
3917        // A **tappable area** that does not name itself — a `Container` or a detector with
3918        // `on_click`, a button holding a widget — is named by the words inside it once its
3919        // subtree is walked (milestone 700): see `apply_tap_area`.
3920        let tap_area = (widget.on_click().is_some()
3921            && widget.semantics().is_none_or(|own| own.label.is_none()))
3922        .then_some(self.semantics.len());
3923        self.walk_node_themed(widget, id, translation, clip, rects, index);
3924        if let (Some(base), Some(whole)) = (tap_area, over) {
3925            self.apply_tap_area(base, id, whole.intersect(clip));
3926        }
3927        if let Some(parent) = outer_hover {
3928            self.hover_parent = parent;
3929        }
3930        if let Some((area, area_widget)) = outer_area {
3931            self.area = area;
3932            self.area_widget = area_widget;
3933        }
3934        // Over its own children: the reference's `foregroundDecoration`, and the only
3935        // point in the walk where a widget paints after its subtree. Still under this
3936        // node's own theme, since it is this node's decoration.
3937        if let Some(bounds) = over {
3938            if let Some(decoration) = widget.foreground(&self.theme) {
3939                let status = self.full_status(widget, id);
3940                self.scene.set_clip(clip);
3941                self.scene.set_owner(id.as_u64());
3942                self.scene.set_bounds(bounds);
3943                decoration.paint_into_in(
3944                    &mut self.scene,
3945                    bounds,
3946                    status.opacity,
3947                    self.theme.direction,
3948                );
3949                self.scene.set_clip(clip);
3950            }
3951            // And what the widget paints over its children itself (milestone 652).
3952            if widget.paints_over() {
3953                let status = self.full_status(widget, id);
3954                self.scene.set_clip(clip);
3955                self.scene.set_owner(id.as_u64());
3956                self.scene.set_bounds(bounds);
3957                widget.paint_over(bounds, status, &self.theme, &mut self.scene);
3958                self.scene.set_clip(clip);
3959            }
3960        }
3961        if let Some(outer) = outer {
3962            self.theme = outer;
3963        }
3964    }
3965
3966    /// The walk proper, with the theme already in place — split out so that no early
3967    /// return inside it can skip putting the outer theme back.
3968    fn walk_node_themed(
3969        &mut self,
3970        widget: &'a dyn Widget<Msg>,
3971        id: WidgetId,
3972        translation: (f32, f32),
3973        clip: Rect,
3974        rects: &[Rect],
3975        index: &mut usize,
3976    ) {
3977        let rect = rects[*index];
3978        *index += 1;
3979        let draw_rect = rect.translate(translation.0, translation.1);
3980        self.inspect_enter(widget, id, draw_rect);
3981
3982        let status = self.full_status(widget, id);
3983        if widget.continuous() {
3984            self.wants_animation = true;
3985        }
3986
3987        self.scene.set_clip(clip);
3988        self.scene.set_owner(id.as_u64());
3989        // The box this widget was given. Text primitives record it: a line of text
3990        // says only where it starts, and the renderer has to know what it covers to
3991        // order it against anything else (milestone 295).
3992        self.scene.set_bounds(draw_rect);
3993        widget.paint(draw_rect, status, &self.theme, &mut self.scene);
3994        // A widget may have tightened the clip (TextField, for one): it is restored here.
3995        self.scene.set_clip(clip);
3996        // A part of the screen saying what the system bars over it should look like, kept
3997        // with its box: the shell asks which ones lie under each bar (#46).
3998        if let Some(style) = widget.system_ui_style() {
3999            self.system_ui.push((draw_rect, style));
4000        }
4001        // The ink a tap left on this surface: over the surface's own paint, under its
4002        // children — where a material surface puts it. The box is recorded too, so the
4003        // shell can start the next splash at the right place, in the right size.
4004        if let Some(style) = widget.ink(&self.theme) {
4005            self.inks.push((id, draw_rect));
4006            if let Some(ripples) = self.runtime.ink.get(&id) {
4007                ripples.paint(
4008                    id.as_u64(),
4009                    draw_rect,
4010                    style.radius,
4011                    style.color,
4012                    &mut self.scene,
4013                );
4014                self.scene.set_clip(clip);
4015            }
4016        }
4017        self.draw_focus_ring(draw_rect, &status, widget);
4018        if let Some(can_paste) = status.toolbar {
4019            self.push_selection_toolbar(widget, id, draw_rect, clip, &status, can_paste);
4020        }
4021        if status.region_bar && !self.region_bar_placed {
4022            if let Some(range) = status.region {
4023                self.push_region_toolbar(widget, id, draw_rect, clip, range);
4024            }
4025        }
4026
4027        // A shared element is recorded **whether or not it is on screen**: half way
4028        // through a transition the one being left behind has usually slid off the edge,
4029        // and its box is exactly what the flight starts from. Every other registry here
4030        // is about what a pointer can reach, which is why they are inside the guard.
4031        if let Some(screen) = self.hero_screen {
4032            if let Some(tag) = widget.hero_tag() {
4033                self.heroes.push((
4034                    HeroSpot {
4035                        tag,
4036                        screen,
4037                        id,
4038                        rect: draw_rect,
4039                    },
4040                    widget,
4041                ));
4042            }
4043        }
4044
4045        let visible = draw_rect.intersect(clip);
4046        // A text in a selection area: a place a selection can begin, end or pass through,
4047        // registered as far as it is visible, in the order it is painted.
4048        if self.theme.widgets.text.selectable && visible.width > 0.0 && visible.height > 0.0 {
4049            if let (Some(area), Some(_)) = (self.area, widget.selection_text()) {
4050                self.text_stops.push(crate::TextStop {
4051                    id,
4052                    rect: draw_rect,
4053                    area,
4054                });
4055            }
4056        }
4057        // A focus stop clipped entirely away by a **scroll** is still a focus stop.
4058        // Tab has to reach the field below the fold — the shell brings it into view
4059        // when it lands there — and registering only what the eye can see is how a long
4060        // form came to have half its fields unreachable from the keyboard. Clipped away
4061        // by anything else it is gone: nothing could reveal it, so focus would land
4062        // where the eye cannot follow.
4063        //
4064        // Only the focus registry does this. A click still needs the visible box, or a
4065        // tap on empty space would land on something scrolled out of sight.
4066        if widget.focusable()
4067            && !self.focus_excluded
4068            && (self.scroll_host.is_some() || (visible.width > 0.0 && visible.height > 0.0))
4069        {
4070            self.focusables.push(Focusable {
4071                id,
4072                rect: visible,
4073                bounds: draw_rect,
4074                form: self.autofill_group,
4075                scroll: self.scroll_host,
4076                skip: self.focus_skipped || widget.focus_skip_traversal(),
4077                order: widget.focus_order().or(self.focus_order),
4078                group: self.focus_group,
4079                autofocus: widget.autofocus(),
4080            });
4081        }
4082        if visible.width > 0.0 && visible.height > 0.0 {
4083            // What a press or a drag can reach: the visible box, cut at a hanging stack's.
4084            let touch = self.touch_clip.map_or(visible, |t| visible.intersect(t));
4085            // A press that lands on a **surface** and on nothing inside it stops there.
4086            // Registered before the children, so anything inside still wins on the way
4087            // back out — see [`Widget::opaque`].
4088            if widget.opaque() {
4089                self.hits.push(Hit {
4090                    id,
4091                    rect: touch,
4092                    msg: None,
4093                    xform: None,
4094                });
4095            }
4096            // A target for a press: something that answers a tap, or a double or a secondary
4097            // tap without one — the shell asks it for those once the press lands on it.
4098            let tap = widget.on_click();
4099            if tap.is_some()
4100                || widget.on_double_tap().is_some()
4101                || widget.on_secondary_tap().is_some()
4102            {
4103                self.hits.push(Hit {
4104                    id,
4105                    rect: touch,
4106                    msg: tap,
4107                    xform: None,
4108                });
4109            }
4110            // A detector that takes a drag (milestone 582).
4111            if widget.pan_axis().is_some() {
4112                self.pans.push((id, touch, draw_rect));
4113            }
4114            // The title bar's line (milestone 640).
4115            if widget.wants_title_bar() {
4116                self.title_bar_row = Some(draw_rect);
4117            }
4118            if let Some(role) = widget.title_bar_role() {
4119                self.title_bar_regions.push((draw_rect, role));
4120            }
4121            // A widget that hears the pointer's raw events (milestone 586).
4122            if widget.pointer_listener() {
4123                self.pointer_listeners.push((id, touch, draw_rect));
4124            }
4125            if let Some(msg) = widget.on_long_press() {
4126                self.long_presses.push(Hit {
4127                    id,
4128                    rect: touch,
4129                    msg: Some(msg),
4130                    xform: None,
4131                });
4132            }
4133            if let Some(action) = widget.edit_action() {
4134                self.edit_actions.push((id, action));
4135            }
4136            if widget.draggable() {
4137                self.draggables.push((id, touch));
4138            }
4139            if let Some(payload) = widget.drag_payload() {
4140                self.drag_sources.push(DragSource {
4141                    id,
4142                    rect: touch,
4143                    payload,
4144                });
4145            }
4146            if widget.drop_zone() {
4147                self.drop_zones.push(DropZone { id, rect: touch });
4148            }
4149            if widget.reorder_index().is_some() {
4150                self.reorderables.push((id, touch));
4151            }
4152            // The accessibility tree: nodes that carry meaning (a role or a label).
4153            if let Some(sem) = widget.semantics().filter(|s| s.is_meaningful()) {
4154                self.semantics.push((id, visible, sem));
4155            }
4156            // A **multi-line** field whose content overflows: a scrollable region (wheel and
4157            // inertia through the generic machinery; the shell follows the caret) plus a
4158            // draggable scrollbar (mouse and touch).
4159            if let Some(vp) = widget.text_viewport(draw_rect) {
4160                if let Some((content_h, visible_h, _, _)) = widget.text_metrics(draw_rect.width, 0)
4161                {
4162                    let max_y = (content_h - visible_h).max(0.0);
4163                    if max_y > 0.0 {
4164                        self.scrollables.push(Scrollable {
4165                            id,
4166                            viewport: vp,
4167                            max_x: 0.0,
4168                            max_y,
4169                            axis: crate::scroll::Axis::Vertical,
4170                            physics: region_physics(widget, &self.theme),
4171                            overscroll: crate::ScrollConfiguration::of()
4172                                .overscroll_indicator(&self.theme),
4173                            fling: crate::ScrollConfiguration::of()
4174                                .velocity_tracker(&self.theme)
4175                                .strategy(),
4176                            refresh: self.refresh_host,
4177                            page: None,
4178                            reverse_x: false,
4179                            reverse_y: false,
4180                            host: self.scroll_host,
4181                            keep_visible: None,
4182                        });
4183                        let offset_y = self
4184                            .runtime
4185                            .scroll
4186                            .get(&id)
4187                            .map(|s| s.1)
4188                            .unwrap_or(0.0)
4189                            .clamp(0.0, max_y);
4190                        self.add_scrollbar(
4191                            id,
4192                            vp,
4193                            Bar {
4194                                vertical: true,
4195                                offset: offset_y,
4196                                max: max_y,
4197                                reverse: false,
4198                                asked: widget.scrollbars(),
4199                                visible: widget.thumb_visibility(),
4200                            },
4201                        );
4202                    }
4203                }
4204            }
4205        }
4206
4207        if let Some((progress, forward)) = widget.navigator() {
4208            let bounds = draw_rect;
4209            let children = widget.children();
4210            // **The pages stop at the navigator's edge.** A screen sliding in comes from
4211            // outside the box, and one sliding out goes outside it; before milestone 398
4212            // the only thing stopping them was the window, so a navigator that was not the
4213            // whole window painted its pages over its siblings — and even a full-window
4214            // one spent the frame drawing a screen nobody could see. The reference clips
4215            // by default (`Clip.hardEdge`) for the same reason.
4216            let clip = if widget.navigator_clips() {
4217                clip.intersect(bounds)
4218            } else {
4219                clip
4220            };
4221            // The pages under the one on show are kept in the tree and stay out of the picture.
4222            let hidden = widget.navigator_retained().min(children.len());
4223            let shown = &children[hidden..];
4224            if shown.len() >= 2 {
4225                // **A transition, as the theme's platform draws it** (milestone 620): the
4226                // builder the theme maps the platform to says where each page is, how big,
4227                // how opaque, and what lies between them, `progress` of the way through.
4228                let builder = self.theme.page_transitions.builder_for(self.theme.platform);
4229                let frame = builder.frame(progress, forward, widget.navigator_gesture());
4230                // `Navigator::from` inserts the screen being left at index 0, so
4231                // `shown[1]` is always the destination: the page on top on a push, the one
4232                // underneath on a pop.
4233                let (top, below) = if forward { (1, 0) } else { (0, 1) };
4234                let mirror = if frame.mirrored && self.rtl() {
4235                    -1.0
4236                } else {
4237                    1.0
4238                };
4239                // Where the shared elements of this transition start, in both the
4240                // registry and the scene: everything after this point belongs to the
4241                // two screens, and the flight is resolved from it once both are drawn.
4242                let hero_base = self.heroes.len();
4243                let scene_base = self.scene.primitives().len();
4244                let surface = self.theme.scheme.surface;
4245                let fill = |builder: &mut Self, color: Color| {
4246                    builder.scene.set_owner(0);
4247                    builder.scene.set_clip(clip);
4248                    builder.scene.fill_rect(bounds, color);
4249                };
4250                if frame.backdrop > 0.0 {
4251                    fill(self, surface.fade(frame.backdrop));
4252                }
4253                let outer_screen = self.hero_screen.replace(below as u8);
4254                // A screen being left keeps its floating layers to itself.
4255                // `process_overlays` draws every overlay above the **whole window**, after
4256                // both screens, so a menu left open on the departing screen would be
4257                // painted on top of the screen that replaced it. Whatever the departing one
4258                // defers is dropped.
4259                let overlay_base = self.overlays.len();
4260                self.render_posed(
4261                    shown[below].as_ref(),
4262                    child_id(id, hidden + below, shown[below].as_ref()),
4263                    bounds,
4264                    frame.below,
4265                    mirror,
4266                    clip,
4267                );
4268                if below != 1 {
4269                    self.overlays.truncate(overlay_base);
4270                }
4271                if frame.barrier > 0.0 {
4272                    fill(self, Color::BLACK.fade(frame.barrier));
4273                }
4274                if frame.scrim > 0.0 {
4275                    fill(self, surface.fade(frame.scrim));
4276                }
4277                if frame.edge_shadow > 0.0 {
4278                    self.paint_edge_shadow(bounds, frame, mirror, clip);
4279                }
4280                self.hero_screen = Some(top as u8);
4281                let overlay_base = self.overlays.len();
4282                self.render_posed(
4283                    shown[top].as_ref(),
4284                    child_id(id, hidden + top, shown[top].as_ref()),
4285                    bounds,
4286                    frame.top,
4287                    mirror,
4288                    clip,
4289                );
4290                if top != 1 {
4291                    self.overlays.truncate(overlay_base);
4292                }
4293                self.hero_screen = outer_screen;
4294                self.fly_heroes(hero_base, scene_base, progress);
4295            } else if let Some(screen) = shown.first() {
4296                self.render_screen(
4297                    screen.as_ref(),
4298                    child_id(id, hidden, screen.as_ref()),
4299                    bounds,
4300                    0.0,
4301                    clip,
4302                );
4303            }
4304        } else if widget.interactive().is_some() {
4305            // An **interactive** viewport (`InteractiveViewer`): the child fills the viewport
4306            // at scale 1, then the retained transform (scale + translation, from the shell's
4307            // gestures) is applied to it through **a single layer** carrying both the matrix
4308            // `M` and the clip to the viewport.
4309            let viewport = draw_rect;
4310            let content_clip = clip.intersect(viewport);
4311            self.interactives.push((id, viewport));
4312            if let Some(content) = widget.children().first() {
4313                let content = content.as_ref();
4314                let cid = child_id(id, 0, content);
4315                let content_rects = self.cached_rects(
4316                    cid,
4317                    content,
4318                    Constraints::definite(Size::new(viewport.width, viewport.height)),
4319                );
4320                let matrix = self
4321                    .runtime
4322                    .interactive
4323                    .get(&id)
4324                    .copied()
4325                    .unwrap_or_default()
4326                    .matrix();
4327                self.emit_transformed_child(
4328                    content,
4329                    cid,
4330                    (viewport.x, viewport.y),
4331                    content_clip,
4332                    &content_rects,
4333                    matrix,
4334                    id,
4335                );
4336            }
4337        } else if let Some(q) = widget.rotated_quarter_turns() {
4338            // `RotatedBox`: the child, measured at its **natural** size, is centred in the box
4339            // (with the dimensions swapped for an odd quarter, see `build_layout`) then rotated
4340            // about the centre. The rotation, applied at compositing time, does not stay
4341            // axis-aligned for an odd quarter (the focus bounds are left as they are — the
4342            // click itself stays correct through `M⁻¹`).
4343            let box_rect = draw_rect;
4344            if let Some(child) = widget.children().first() {
4345                let child = child.as_ref();
4346                let cid = child_id(id, 0, child);
4347                let child_rects =
4348                    self.cached_rects(cid, child, Constraints::scroll(0.0, 0.0, true, true));
4349                let nat = child_rects
4350                    .first()
4351                    .copied()
4352                    .unwrap_or(Rect::new(0.0, 0.0, 0.0, 0.0));
4353                let center = Point::new(
4354                    box_rect.x + box_rect.width / 2.0,
4355                    box_rect.y + box_rect.height / 2.0,
4356                );
4357                let translation = (center.x - nat.width / 2.0, center.y - nat.height / 2.0);
4358                let angle = q.rem_euclid(4) as f32 * std::f32::consts::FRAC_PI_2;
4359                let angle = if self.rtl() { -angle } else { angle };
4360                let matrix = Affine::rotation(angle).about(center);
4361                self.emit_transformed_child(
4362                    child,
4363                    cid,
4364                    translation,
4365                    clip,
4366                    &child_rects,
4367                    matrix,
4368                    id,
4369                );
4370            }
4371        } else if let Some(fit) = widget.fitted() {
4372            // `FittedBox`: the child, measured at its **natural** size, is scaled according to
4373            // the `BoxFit` (uniformly, except for `Fill`), centred and clipped to the box. The
4374            // scale stays axis-aligned → the focus bounds follow.
4375            let box_rect = draw_rect;
4376            let content_clip = clip.intersect(box_rect);
4377            if let Some(child) = widget.children().first() {
4378                let child = child.as_ref();
4379                let cid = child_id(id, 0, child);
4380                let child_rects =
4381                    self.cached_rects(cid, child, Constraints::scroll(0.0, 0.0, true, true));
4382                let nat = child_rects
4383                    .first()
4384                    .copied()
4385                    .unwrap_or(Rect::new(0.0, 0.0, 0.0, 0.0));
4386                let (sx, sy) = fit.scale(
4387                    Size::new(nat.width, nat.height),
4388                    Size::new(box_rect.width, box_rect.height),
4389                );
4390                let center = Point::new(
4391                    box_rect.x + box_rect.width / 2.0,
4392                    box_rect.y + box_rect.height / 2.0,
4393                );
4394                let translation = (center.x - nat.width / 2.0, center.y - nat.height / 2.0);
4395                let matrix = Affine::scale(sx, sy).about(center);
4396                self.emit_transformed_child(
4397                    child,
4398                    cid,
4399                    translation,
4400                    content_clip,
4401                    &child_rects,
4402                    matrix,
4403                    id,
4404                );
4405            }
4406        } else if let Some(content) = widget.scroll_content() {
4407            let axis = widget.scroll_axis();
4408            let viewport = draw_rect;
4409            let content_clip = clip.intersect(viewport);
4410            // `reverse` applies to the axis this scrolls along; a two-dimensional scroll
4411            // takes it on the vertical, which is the one a reversed view is ever about.
4412            let reverse = if widget.scroll_reverse() {
4413                (axis.free_x() && !axis.free_y(), axis.free_y())
4414            } else {
4415                (false, false)
4416            };
4417
4418            // The padding is **inside** the viewport and scrolls with the content:
4419            // room at the end of a feed is reachable by scrolling to it, room taken out
4420            // of the window would sit there for ever. The reference's `SliverPadding`.
4421            let pad = widget.scroll_padding();
4422            let content_rects = self.cached_rects(
4423                child_id(id, 0, content),
4424                content,
4425                Constraints::scroll(
4426                    (viewport.width - pad.left - pad.right).max(0.0),
4427                    (viewport.height - pad.top - pad.bottom).max(0.0),
4428                    axis.free_x(),
4429                    axis.free_y(),
4430                ),
4431            );
4432
4433            let content_size = content_rects
4434                .first()
4435                .copied()
4436                .unwrap_or(Rect::new(0.0, 0.0, 0.0, 0.0));
4437            // What scrolls is the content **and** its room, which is what makes the far
4438            // inset reachable rather than decorative.
4439            let scrolled = Size::new(
4440                content_size.width + pad.left + pad.right,
4441                content_size.height + pad.top + pad.bottom,
4442            );
4443            let max_x = (scrolled.width - viewport.width).max(0.0);
4444            let max_y = (scrolled.height - viewport.height).max(0.0);
4445
4446            // The geometry without the identity: enough to ask where the content sits at
4447            // an offset, and what offset would put a box where it is wanted.
4448            let frame = Scrollable {
4449                id,
4450                viewport,
4451                max_x,
4452                max_y,
4453                axis,
4454                physics: crate::ScrollPhysics::CLAMPING,
4455                overscroll: None,
4456                fling: frus_core::VelocityStrategy::Regression,
4457                refresh: None,
4458                page: None,
4459                reverse_x: reverse.0,
4460                reverse_y: reverse.1,
4461                host: None,
4462                keep_visible: None,
4463            };
4464            // What the content asks to be kept in view — a tab bar's selected tab — in
4465            // the content's own coordinates, which is the only frame it can speak in.
4466            let asked = content.keep_visible(
4467                Size::new(content_size.width, content_size.height),
4468                &self.theme,
4469            );
4470            // Where that box would sit if nobody had scrolled: the frame's coordinates,
4471            // with the offset taken out. See [`KeepVisible::rect`] for why it is stored
4472            // this way rather than where it is drawn.
4473            let keep = asked.map(|keep| {
4474                let rest = frame.content_origin((0.0, 0.0), scrolled);
4475                KeepVisible {
4476                    rect: keep.rect.translate(
4477                        viewport.x + rest.0 + pad.left,
4478                        viewport.y + rest.1 + pad.top,
4479                    ),
4480                    ..keep
4481                }
4482            });
4483            // Inside the extent this frame has just measured, unless something owns the
4484            // offset: content that shrank is drawn where it rests (milestone 533).
4485            let (offset_x, offset_y) = match self.runtime.scroll_offset_within(id, (max_x, max_y)) {
4486                Some(offset) => offset,
4487                // Never scrolled, and something inside wants to be seen: **open** there
4488                // rather than sliding across a frame later. A tab bar restored on its
4489                // eighth tab should show the eighth tab, not show the first and then
4490                // travel. The same answer a paged view gives its initial page, and the
4491                // runtime retains the offset on that first frame so it stays.
4492                None => match keep {
4493                    Some(keep) => frame.keep_offset(keep).0,
4494                    None => (0.0, 0.0),
4495                },
4496            };
4497
4498            let origin = frame.content_origin((offset_x, offset_y), scrolled);
4499            let content_translation = (
4500                viewport.x + origin.0 + pad.left,
4501                viewport.y + origin.1 + pad.top,
4502            );
4503            self.scrollables.push(Scrollable {
4504                id,
4505                viewport,
4506                max_x,
4507                max_y,
4508                axis,
4509                physics: region_physics(widget, &self.theme),
4510                overscroll: crate::ScrollConfiguration::of().overscroll_indicator(&self.theme),
4511                fling: crate::ScrollConfiguration::of()
4512                    .velocity_tracker(&self.theme)
4513                    .strategy(),
4514                refresh: self.refresh_host,
4515                page: None,
4516                reverse_x: reverse.0,
4517                reverse_y: reverse.1,
4518                host: self.scroll_host,
4519                keep_visible: keep,
4520            });
4521            let mut content_index = 0;
4522            // Where the content starts, so that a stretch can take all of it (milestone 591).
4523            let stretch_mark = (self.scene.primitives().len(), self.xform_base());
4524            let outer = self.scroll_host.replace(id);
4525            self.walk(
4526                content,
4527                child_id(id, 0, content),
4528                content_translation,
4529                content_clip,
4530                &content_rects,
4531                &mut content_index,
4532            );
4533            self.scroll_host = outer;
4534            self.add_overscroll_stretch(id, viewport, content_clip, stretch_mark);
4535
4536            // The overscroll glow, then the scrollbars, over the content (not
4537            // clipped by it).
4538            self.scene.set_clip(clip);
4539            self.add_overscroll_glow(id, viewport);
4540            if max_y > 0.0 {
4541                self.add_scrollbar(
4542                    id,
4543                    viewport,
4544                    Bar {
4545                        vertical: true,
4546                        offset: offset_y,
4547                        max: max_y,
4548                        reverse: reverse.1,
4549                        asked: widget.scrollbars(),
4550                        visible: widget.thumb_visibility(),
4551                    },
4552                );
4553            }
4554            if max_x > 0.0 {
4555                self.add_scrollbar(
4556                    id,
4557                    viewport,
4558                    Bar {
4559                        vertical: false,
4560                        offset: offset_x,
4561                        max: max_x,
4562                        reverse: reverse.0,
4563                        asked: widget.scrollbars(),
4564                        visible: widget.thumb_visibility(),
4565                    },
4566                );
4567            }
4568        } else if let Some(vlist) =
4569            widget.virtual_list(Size::new(draw_rect.width, draw_rect.height))
4570        {
4571            // A virtualised list: only build/lay out/paint the visible window.
4572            let viewport = draw_rect;
4573            let content_clip = clip.intersect(viewport);
4574            let reverse = widget.scroll_reverse();
4575            // Down the screen or across it. A list virtualises along **one** axis, which
4576            // is what lets it place item `n` without building the ones before it, so
4577            // `Axis::Both` has no meaning here and reads as vertical.
4578            let across = matches!(vlist.axis, crate::scroll::Axis::Horizontal);
4579            // Room around the items, inside the viewport and scrolling with them. The
4580            // **leading** inset is the one at the end the items start from, so a
4581            // reversed list clears the far end first and both keep their sides.
4582            let pad = widget.scroll_padding();
4583            let (lead, trail, cross_near, cross_far) = match (across, reverse) {
4584                (true, false) => (pad.left, pad.right, pad.top, pad.bottom),
4585                (true, true) => (pad.right, pad.left, pad.top, pad.bottom),
4586                (false, false) => (pad.top, pad.bottom, pad.left, pad.right),
4587                (false, true) => (pad.bottom, pad.top, pad.left, pad.right),
4588            };
4589            // From here down it is a main axis and a cross axis, and only the two
4590            // branches at the end have to know which is which. The alternative was a
4591            // second copy of the window arithmetic below, which is where a reversed
4592            // list gets its signs right -- and a fix applied to one copy and not the
4593            // other is exactly how the two would drift apart.
4594            let (extent, cross_extent) = match across {
4595                true => (viewport.width, viewport.height),
4596                false => (viewport.height, viewport.width),
4597            };
4598            let content = lead + trail + vlist.count as f32 * vlist.item_extent;
4599            let max = (content - extent).max(0.0);
4600            let bounds = if across { (max, 0.0) } else { (0.0, max) };
4601            let offset = self
4602                .runtime
4603                .scroll_offset_within(id, bounds)
4604                .map_or(0.0, |(x, y)| if across { x } else { y });
4605            self.scrollables.push(Scrollable {
4606                id,
4607                viewport,
4608                max_x: if across { max } else { 0.0 },
4609                max_y: if across { 0.0 } else { max },
4610                axis: if across {
4611                    crate::scroll::Axis::Horizontal
4612                } else {
4613                    crate::scroll::Axis::Vertical
4614                },
4615                physics: region_physics(widget, &self.theme),
4616                overscroll: crate::ScrollConfiguration::of().overscroll_indicator(&self.theme),
4617                fling: crate::ScrollConfiguration::of()
4618                    .velocity_tracker(&self.theme)
4619                    .strategy(),
4620                refresh: self.refresh_host,
4621                page: None,
4622                reverse_x: across && reverse,
4623                reverse_y: !across && reverse,
4624                host: self.scroll_host,
4625                keep_visible: None,
4626            });
4627
4628            // Where the content starts, so that a stretch can take all of it (milestone 591).
4629            let stretch_mark = (self.scene.primitives().len(), self.xform_base());
4630            let outer = self.scroll_host.replace(id);
4631            if vlist.item_extent > 0.0 && vlist.count > 0 {
4632                // The **window** is the same arithmetic either way, and that is not a
4633                // coincidence: a reversed list counts its indices from the end, and a
4634                // reversed offset counts its pixels from the end, so index and offset
4635                // agree about which way is forward. Only where an item lands differs.
4636                let first = ((offset - lead) / vlist.item_extent).floor().max(0.0) as usize;
4637                let last = ((((offset + extent - lead) / vlist.item_extent).ceil()).max(0.0)
4638                    as usize)
4639                    .min(vlist.count);
4640                let origin = if across { viewport.x } else { viewport.y };
4641                for i in first..last {
4642                    let item = (vlist.build)(i);
4643                    let along = if reverse {
4644                        origin + extent - lead - (i + 1) as f32 * vlist.item_extent + offset
4645                    } else {
4646                        origin + lead + i as f32 * vlist.item_extent - offset
4647                    };
4648                    let (x, y) = match across {
4649                        true => (along, viewport.y + cross_near),
4650                        false => (viewport.x + cross_near, along),
4651                    };
4652
4653                    // **Filled**, not merely constrained: a list hands its children a
4654                    // box rather than asking them how big they would like to be. The
4655                    // reference gives a list's children a *tight* cross-axis extent, and
4656                    // for a fixed-extent list a tight main-axis one too — which is
4657                    // exactly the pair of numbers here.
4658                    //
4659                    // Constrained-but-not-filled, a row whose width nobody set hugged
4660                    // its own text: a list of coloured rows painted a column of chips
4661                    // down the left instead of rows across the list. Found on a device
4662                    // in milestone 349.
4663                    let free_cross = (cross_extent - cross_near - cross_far).max(0.0);
4664                    let item_rects = self.cached_rects(
4665                        id.child(i),
4666                        item.as_ref(),
4667                        Constraints::filled(match across {
4668                            true => Size::new(vlist.item_extent, free_cross),
4669                            false => Size::new(free_cross, vlist.item_extent),
4670                        }),
4671                    );
4672
4673                    let item = self.keep(item);
4674                    let mut item_index = 0;
4675                    self.walk(
4676                        item,
4677                        id.child(i),
4678                        (x, y),
4679                        content_clip,
4680                        &item_rects,
4681                        &mut item_index,
4682                    );
4683                }
4684            }
4685
4686            self.scroll_host = outer;
4687            self.add_overscroll_stretch(id, viewport, content_clip, stretch_mark);
4688            self.scene.set_clip(clip);
4689            self.add_overscroll_glow(id, viewport);
4690            if max > 0.0 {
4691                self.add_scrollbar(
4692                    id,
4693                    viewport,
4694                    Bar {
4695                        vertical: !across,
4696                        offset,
4697                        max,
4698                        reverse,
4699                        asked: widget.scrollbars(),
4700                        visible: widget.thumb_visibility(),
4701                    },
4702                );
4703            }
4704        } else if let Some(pages) = widget.page_view() {
4705            // A paged view: like a virtualised list turned on its side, and with the
4706            // page extent — the whole geometry — derived from the viewport rather than
4707            // given. Only the pages the viewport touches are built.
4708            let viewport = draw_rect;
4709            let content_clip = clip.intersect(viewport);
4710            let snap = pages.snap(viewport);
4711            let (viewport_along, page_across) = if snap.horizontal {
4712                (viewport.width, viewport.height)
4713            } else {
4714                (viewport.height, viewport.width)
4715            };
4716            let reverse = widget.scroll_reverse();
4717            // Room at both ends, so a page narrower than the viewport comes to rest
4718            // **centred** in it rather than jammed against the leading edge — and so
4719            // the last one can be reached at all: unpadded, the travel stops with the
4720            // last page's trailing edge on the viewport's, which is short of where that
4721            // page rests. Padded, the total works out to `extent × (count - 1)`,
4722            // the last page's own offset, exactly.
4723            let pad = pages.end_padding(viewport);
4724            let total = pages.count as f32 * snap.extent + 2.0 * pad;
4725            let max = (total - viewport_along).max(0.0);
4726            // With no retained offset this is the view's **first** frame, and it opens
4727            // on the page it was asked for. Reading the initial page here rather than
4728            // correcting it a frame later is what keeps page 0 from flashing past on
4729            // the way to page 3.
4730            let bounds = if snap.horizontal {
4731                (max, 0.0)
4732            } else {
4733                (0.0, max)
4734            };
4735            let along = match self.runtime.scroll_offset_within(id, bounds) {
4736                Some((x, y)) => {
4737                    if snap.horizontal {
4738                        x
4739                    } else {
4740                        y
4741                    }
4742                }
4743                None => snap
4744                    .offset_of(snap.requested.min(pages.count.saturating_sub(1)))
4745                    .clamp(0.0, max),
4746            };
4747            self.scrollables.push(Scrollable {
4748                id,
4749                viewport,
4750                max_x: if snap.horizontal { max } else { 0.0 },
4751                max_y: if snap.horizontal { 0.0 } else { max },
4752                axis: if snap.horizontal {
4753                    crate::scroll::Axis::Horizontal
4754                } else {
4755                    crate::scroll::Axis::Vertical
4756                },
4757                physics: region_physics(widget, &self.theme),
4758                overscroll: crate::ScrollConfiguration::of().overscroll_indicator(&self.theme),
4759                fling: crate::ScrollConfiguration::of()
4760                    .velocity_tracker(&self.theme)
4761                    .strategy(),
4762                refresh: self.refresh_host,
4763                page: Some(snap),
4764                reverse_x: snap.horizontal && reverse,
4765                reverse_y: !snap.horizontal && reverse,
4766                host: self.scroll_host,
4767                keep_visible: None,
4768            });
4769
4770            // Where the content starts, so that a stretch can take all of it (milestone 591).
4771            let stretch_mark = (self.scene.primitives().len(), self.xform_base());
4772            let outer = self.scroll_host.replace(id);
4773            if pages.count > 0 {
4774                // The window is the same arithmetic whichever end index 0 is at: a
4775                // reversed view counts its indices from the far end and a reversed
4776                // offset counts its pixels from the same one, so the two agree about
4777                // which way forward is. Only where a page **lands** differs.
4778                let first = ((along - pad) / snap.extent).floor().max(0.0) as usize;
4779                let last = ((((along + viewport_along - pad) / snap.extent).ceil()).max(0.0)
4780                    as usize)
4781                    .min(pages.count)
4782                    .max(first + 1);
4783                // A wheel shows **more** rows than a flat strip would: the ones a quarter
4784                // turn away are compressed into the last few pixels at either end, and
4785                // their flat positions are a whole quarter-circumference outside the
4786                // viewport. Widening the window in both directions keeps the reversed
4787                // arithmetic above untouched, and the rows that turn out to have gone
4788                // over the horizon are dropped below — before they are built.
4789                let (first, last) = match pages.wheel {
4790                    Some(wheel) => {
4791                        let span = wheel.radius(viewport_along) * std::f32::consts::FRAC_PI_2;
4792                        let extra = (span / snap.extent).ceil().max(0.0) as usize + 1;
4793                        (first.saturating_sub(extra), (last + extra).min(pages.count))
4794                    }
4795                    None => (first, last),
4796                };
4797                for index in first..last.min(pages.count) {
4798                    let start = match reverse {
4799                        true => viewport_along - pad - (index + 1) as f32 * snap.extent + along,
4800                        false => pad + index as f32 * snap.extent - along,
4801                    };
4802                    // Where this row's centre would sit on the unrolled strip, measured
4803                    // from the middle of the viewport — which is the whole of what the
4804                    // cylinder needs to know about it.
4805                    let on_wheel = match pages.wheel {
4806                        Some(wheel) => {
4807                            let flat = start + snap.extent / 2.0 - viewport_along / 2.0;
4808                            match wheel.row(viewport_along, flat) {
4809                                // Past the horizon: not drawn, and not built either.
4810                                None => continue,
4811                                Some(row) => Some((row, flat)),
4812                            }
4813                        }
4814                        None => None,
4815                    };
4816                    let page = (pages.build)(index);
4817                    let (size, origin) = if snap.horizontal {
4818                        (
4819                            Size::new(snap.extent, page_across),
4820                            (viewport.x + start, viewport.y),
4821                        )
4822                    } else {
4823                        (
4824                            Size::new(page_across, snap.extent),
4825                            (viewport.x, viewport.y + start),
4826                        )
4827                    };
4828
4829                    // A page is **given** its box, not asked for one: it fills the
4830                    // panel even when its content is a single centred line.
4831                    let page_rects = self.cached_rects(
4832                        id.child(index),
4833                        page.as_ref(),
4834                        Constraints::filled(size),
4835                    );
4836
4837                    let mut page_index = 0;
4838                    // The row is drawn **flat**, then the whole of it is composited on
4839                    // the cylinder — one layer per row, the way `Transform` does it, so
4840                    // the text, the rules and the backgrounds all turn together instead
4841                    // of each primitive having to know it is on a wheel.
4842                    let before = self.scene.primitives().len();
4843                    let base = self.xform_base();
4844                    let page = self.keep(page);
4845                    self.walk(
4846                        page,
4847                        id.child(index),
4848                        origin,
4849                        content_clip,
4850                        &page_rects,
4851                        &mut page_index,
4852                    );
4853                    if let Some((row, flat)) = on_wheel {
4854                        // The row's own centre on screen: the scale is about it, so a row
4855                        // shrinks in place rather than creeping towards a corner.
4856                        let centre = Point::new(
4857                            viewport.x + viewport.width / 2.0,
4858                            viewport.y + viewport_along / 2.0 + flat,
4859                        );
4860                        let matrix = Affine::scale(row.scale_x, row.scale_y)
4861                            .about(centre)
4862                            // And then up or down to where the cylinder actually put it.
4863                            .then(Affine::translation(0.0, row.offset - flat));
4864                        let group = self.scene.split_off(before);
4865                        self.scene.push_primitive(Primitive::Layer {
4866                            primitives: group,
4867                            opacity: row.opacity.clamp(0.0, 1.0),
4868                            clip: content_clip,
4869                            clip_shape: ClipShape::Rect,
4870                            transform: Some(LayerTransform::new(matrix)),
4871                            filter: LayerFilter::NONE,
4872                            owner: id.child(index).as_u64(),
4873                        });
4874                        self.transform_interaction_registries(&base, matrix);
4875                    }
4876                }
4877            }
4878
4879            self.scroll_host = outer;
4880            self.add_overscroll_stretch(id, viewport, content_clip, stretch_mark);
4881            self.scene.set_clip(clip);
4882            self.add_overscroll_glow(id, viewport);
4883        } else if let Some(overflow) = widget.overflow_box() {
4884            // The child is laid out to constraints of its own — which is why it can
4885            // come out bigger than the box holding it — then anchored inside it and
4886            // rendered **without** the box's clip. A spill that got clipped here would
4887            // be no spill at all; a `ClipRect` above is how a caller asks for one.
4888            let own = draw_rect;
4889            if let Some(child) = widget.children().first() {
4890                let child = child.as_ref();
4891                let cid = child_id(id, 0, child);
4892                let asked = overflow.child_size(Size::new(own.width, own.height));
4893                let child_rects = if overflow.unconstrained {
4894                    // Unconstrained: the child is asked how big it wants to be, and
4895                    // gets exactly that.
4896                    self.cached_rects(cid, child, Constraints::scroll(0.0, 0.0, true, true))
4897                } else if overflow.natural_height {
4898                    // The box's width, and the height the child asks for at it.
4899                    self.cached_rects(
4900                        cid,
4901                        child,
4902                        Constraints::scroll(asked.width, 0.0, false, true),
4903                    )
4904                } else {
4905                    self.cached_rects(cid, child, Constraints::filled(asked))
4906                };
4907                let size = child_rects
4908                    .first()
4909                    .map(|r| Size::new(r.width, r.height))
4910                    .unwrap_or(asked);
4911                let origin = overflow.origin(own, size);
4912
4913                let mut child_index = 0;
4914                self.walk(child, cid, origin, clip, &child_rects, &mut child_index);
4915            }
4916        } else if let Some(transform) = widget.constraints_transform() {
4917            // The child is laid out at **the same box the measurement gave it**, worked out
4918            // again from what is left of that measurement — the box it produced. Each of
4919            // the three things an axis can say survives the round trip; see
4920            // `AxisConstraint::at`.
4921            let own = draw_rect;
4922            if let Some(child) = widget.children().first() {
4923                let child = child.as_ref();
4924                let cid = child_id(id, 0, child);
4925                // A box a multiple of its child below one is smaller than the child: on a
4926                // loose axis, the child is allowed what it had, the box divided back by the
4927                // factor, and at zero there is nothing to divide, so it is asked. At one or
4928                // more the box already holds it (milestone 598).
4929                let factor = widget.size_factor().unwrap_or((1.0, 1.0));
4930                let back = |rule, own: f32, factor: f32| match rule {
4931                    crate::constraints::AxisConstraint::Loose if factor <= 0.0 => {
4932                        crate::constraints::AxisConstraint::Unbounded.at(own)
4933                    }
4934                    crate::constraints::AxisConstraint::Loose if factor < 1.0 => {
4935                        rule.at(own / factor)
4936                    }
4937                    _ => rule.at(own),
4938                };
4939                let (aw, free_x, fill_x) = back(transform.width, own.width, factor.0);
4940                let (ah, free_y, fill_y) = back(transform.height, own.height, factor.1);
4941                // A box on its way to its child's size is not that size yet, and the child is
4942                // laid out as ever (milestone 594). On the axes the child decides, it comes
4943                // back at its own size, which is where the box is going; an axis it is given
4944                // as it came is not animated at all, the box filling the room there.
4945                let animated = widget.animated_size();
4946                let constraints = Constraints::axes(aw, ah, [free_x, free_y], [fill_x, fill_y]);
4947                let child_rects = self.cached_rects(cid, child, constraints);
4948                let size = child_rects
4949                    .first()
4950                    .map(|r| Size::new(r.width, r.height))
4951                    .unwrap_or(Size::new(0.0, 0.0));
4952                // An anchor that may be directional is resolved here, where the reading
4953                // direction is known (milestone 597).
4954                // An anchor on its way to another is where the runtime has it (milestone 600).
4955                let geometry = widget.alignment_geometry().map(|geometry| {
4956                    match self.runtime.anim_offset(id) {
4957                        Some((x, y)) => geometry.with_fractions(x, y),
4958                        None => geometry,
4959                    }
4960                });
4961                let align = match geometry {
4962                    Some(geometry) => geometry.resolve(if self.rtl() {
4963                        frus_core::TextDirection::Rtl
4964                    } else {
4965                        frus_core::TextDirection::Ltr
4966                    }),
4967                    None => transform.alignment,
4968                };
4969                let origin = (
4970                    own.x + (own.width - size.width) * align.fraction_x(),
4971                    own.y + (own.height - size.height) * align.fraction_y(),
4972                );
4973                if transform.report {
4974                    self.report_spill(own, Rect::new(origin.0, origin.1, size.width, size.height));
4975                }
4976                let clip = match &animated {
4977                    Some(spec) if spec.clip => clip.intersect(own),
4978                    // A reveal is the child cut to it (milestone 596).
4979                    _ if widget.clips_to_box() => clip.intersect(own),
4980                    _ => clip,
4981                };
4982                let mut child_index = 0;
4983                self.walk(child, cid, origin, clip, &child_rects, &mut child_index);
4984            }
4985        } else if let Some(build) = widget.layout_builder() {
4986            // Builds the content from the actual box, then lays it out and renders it inside
4987            // (like a list item: with no retained state).
4988            let bounds = draw_rect;
4989            let content_clip = clip.intersect(bounds);
4990            let child = build(Size::new(bounds.width, bounds.height));
4991
4992            // **Filled**, not merely constrained: the content is handed this box, and it
4993            // was built from this box in the first place. Constraining it instead let a
4994            // root with no width of its own hug its content — a grid built here laid its
4995            // columns out at nothing, which is what `GridView::extent`'s first tests found
4996            // (milestone 356). The same distinction a list's items and a paged view's
4997            // pages already draw.
4998            let child_rects = self.cached_rects(
4999                id.child(0),
5000                child.as_ref(),
5001                Constraints::filled(Size::new(bounds.width, bounds.height)),
5002            );
5003
5004            let child = self.keep(child);
5005            let mut child_index = 0;
5006            self.walk(
5007                child,
5008                id.child(0),
5009                (bounds.x, bounds.y),
5010                content_clip,
5011                &child_rects,
5012                &mut child_index,
5013            );
5014        } else if let Some(build) = widget.scroll_overlay() {
5015            // The **body first**, exactly as an ordinary single child: it keeps its own
5016            // identity, registers itself as a scroll region in its own right, and consumes
5017            // its rects from this array the way the generic walk would. Nothing about it
5018            // changes for being watched.
5019            let children = widget.children();
5020            for (child_index, child) in children.iter().enumerate() {
5021                self.walk(
5022                    child.as_ref(),
5023                    child_id(id, child_index, child.as_ref()),
5024                    translation,
5025                    clip,
5026                    rects,
5027                    index,
5028                );
5029            }
5030            // Then the overlay, built from **where the body has got to this frame**. Read
5031            // from the retained offset rather than from the registry the walk is still
5032            // filling: the offset is written before the frame is drawn, so it is current,
5033            // where the extents beside it are not yet known — which is why the builder is
5034            // handed the one and not the other.
5035            let offset = children
5036                .first()
5037                .map(|body| child_id(id, 0, body.as_ref()))
5038                .and_then(|body| self.runtime.scroll.get(&body).copied())
5039                .unwrap_or((0.0, 0.0));
5040            let bounds = draw_rect;
5041            let over = build(offset, Size::new(bounds.width, bounds.height));
5042            // `child(1)`, beside the body's `child(0)` — or beside its key, which cannot
5043            // collide with a positional identity. The overlay is laid out in a tree of its
5044            // own, so it needs an identity of its own for hover, ink and the paint cache.
5045            let over_id = id.child(1);
5046            let over_rects = self.cached_rects(
5047                over_id,
5048                over.as_ref(),
5049                // **Filled**, not constrained: an overlay covers the region it is over, and
5050                // where inside that box its content sits is the caller's business — a
5051                // column pins it to the top, an `Align` puts it anywhere else. The same
5052                // rule a `layout_builder`'s content is given.
5053                Constraints::filled(Size::new(bounds.width, bounds.height)),
5054            );
5055            let over = self.keep(over);
5056            let mut over_index = 0;
5057            self.walk(
5058                over,
5059                over_id,
5060                (bounds.x, bounds.y),
5061                clip.intersect(bounds),
5062                &over_rects,
5063                &mut over_index,
5064            );
5065        } else if let Some(spec) = widget.dismissible() {
5066            // A dismissible item is a stack whose **last** layer — the item itself — is
5067            // offset by however far it has been swiped, and whose earlier layers, the
5068            // backgrounds, show only through the strip the item has uncovered. Clipping
5069            // them is what makes a background read as something the item is sliding
5070            // *off*, rather than a block that was always there.
5071            let bounds = draw_rect;
5072            let state = self.runtime.dismiss.get(&id).copied();
5073            let progress = state.map(|s| s.progress()).unwrap_or(0.0);
5074            let layers = widget.children();
5075            let last = layers.len().saturating_sub(1);
5076            // Which background: the first covers a swipe towards the end, the second —
5077            // when there is one — the other way. With only one, it serves both.
5078            let shown_background = if progress < 0.0 && last >= 2 { 1 } else { 0 };
5079            let strip = crate::dismiss::revealed_strip(bounds, progress);
5080
5081            for (i, layer) in layers.iter().enumerate() {
5082                let is_item = i == last;
5083                if !is_item && (i != shown_background || progress == 0.0) {
5084                    // Not the background this direction shows: skip it, and skip the
5085                    // rects it would have consumed — it is laid out separately, so
5086                    // there is no index to keep in step.
5087                    continue;
5088                }
5089                let offset = if is_item {
5090                    if spec.axis.is_horizontal() {
5091                        (bounds.width * progress, 0.0)
5092                    } else {
5093                        (0.0, bounds.height * progress)
5094                    }
5095                } else {
5096                    (0.0, 0.0)
5097                };
5098                let layer_clip = if is_item {
5099                    clip.intersect(bounds)
5100                } else {
5101                    clip.intersect(strip)
5102                };
5103                if layer_clip.width <= 0.0 || layer_clip.height <= 0.0 {
5104                    continue;
5105                }
5106                let cid = child_id(id, i, layer.as_ref());
5107                let layer_rects = self.cached_rects(
5108                    cid,
5109                    layer.as_ref(),
5110                    Constraints::filled(Size::new(bounds.width, bounds.height)),
5111                );
5112                let mut layer_index = 0;
5113                self.walk(
5114                    layer.as_ref(),
5115                    cid,
5116                    (bounds.x + offset.0, bounds.y + offset.1),
5117                    layer_clip,
5118                    &layer_rects,
5119                    &mut layer_index,
5120                );
5121            }
5122
5123            self.dismissables.push(Dismissable {
5124                id,
5125                rect: bounds,
5126                spec,
5127            });
5128            // A swipe that is settling, flying or collapsing drives itself.
5129            if state.is_some_and(|s| s.phase() != DismissPhase::Drag) {
5130                self.wants_animation = true;
5131            }
5132        } else if let Some(custom) = widget.custom_layout() {
5133            // Children placed by a function (milestone 601): it is handed this box, measures
5134            // each child in a layout of its own under the constraints it chooses, and says
5135            // where it goes. The function runs here, where the box is known, and nowhere
5136            // else, which is why the box's size cannot depend on its children.
5137            let own = draw_rect;
5138            let children = widget.children();
5139            let measure = |i: usize, c: crate::ChildConstraints| -> Vec<Rect> {
5140                let child = children[i].as_ref();
5141                self.cached_rects(child_id(id, i, child), child, custom_constraints(c))
5142            };
5143            let mut layout = crate::ChildLayout::new(custom.keys, &measure);
5144            (custom.delegate)(Size::new(own.width, own.height), &mut layout);
5145            let (laid, at) = (layout.laid, layout.at);
5146            for (i, child) in children.iter().enumerate() {
5147                let Some(child_rects) = &laid[i] else {
5148                    continue;
5149                };
5150                let mut child_index = 0;
5151                self.walk(
5152                    child.as_ref(),
5153                    child_id(id, i, child.as_ref()),
5154                    (own.x + at[i].x, own.y + at[i].y),
5155                    clip,
5156                    child_rects,
5157                    &mut child_index,
5158                );
5159            }
5160        } else if widget.stack() {
5161            // A stack: the layers are rendered in order, bottom first.
5162            let bounds = draw_rect;
5163            // Cut at the stack's edges unless it says otherwise (`stack.dart:718`).
5164            let layer_clip = if widget.stack_clips() {
5165                clip.intersect(bounds)
5166            } else {
5167                clip
5168            };
5169            // Drawn past its edges or not, a stack is only touched inside them: the
5170            // reference asks a box whether a point is in it before its children.
5171            let outer_touch = self.touch_clip;
5172            if !widget.stack_clips() {
5173                self.touch_clip = Some(outer_touch.map_or(bounds, |t| t.intersect(bounds)));
5174            }
5175            let fit = widget.stack_fit();
5176            // The stack's own reading direction, or the theme's.
5177            let rtl = widget
5178                .stack_direction()
5179                .map_or(self.rtl(), |d| d == frus_core::TextDirection::Rtl);
5180            // An **indexed** stack shows one layer and lays out the rest. Which is a
5181            // question for here rather than for the widget: a layer that wrapped itself in
5182            // something to keep quiet would no longer be laid out the way a bare layer is,
5183            // and the layout is the whole promise.
5184            let shown = widget.stack_visible();
5185            // Where a layer smaller than the stack sits. Resolved once, against the
5186            // reading direction, so a start-anchored badge follows the script.
5187            let align = widget
5188                .alignment_geometry()
5189                .unwrap_or(frus_core::AlignmentGeometry::Physical(
5190                    frus_core::Alignment::TOP_LEFT,
5191                ))
5192                .resolve(if rtl {
5193                    frus_core::TextDirection::Rtl
5194                } else {
5195                    frus_core::TextDirection::Ltr
5196                });
5197            for (i, layer) in widget.children().iter().enumerate() {
5198                let cid = child_id(id, i, layer.as_ref());
5199                // What this layer asks for on each axis: a pinned pair gives a number, a
5200                // lone pin plus an explicit extent gives one, and neither leaves the axis
5201                // to the layer itself — or, under `Expand`, to the stack.
5202                // A layer moving between two sets of pins is read from the runtime rather
5203                // than from the widget: the widget carries where it is **going**, and a
5204                // layer that jumped there would be the whole bug this animates away.
5205                // Both the size and the place come from the one value, so a box cannot be
5206                // measured against one frame's pins and placed against another's.
5207                let pins = match layer.positioned() {
5208                    Some(spec) if layer.anim_pins().is_some() => {
5209                        Some(self.runtime.anim_pins(cid).unwrap_or(spec))
5210                    }
5211                    other => other,
5212                };
5213                // A start or an end pin is a left or a right once the direction is known
5214                // (milestone 589). The stack's layers are placed on the screen as they are,
5215                // not mirrored, so this is the one place it is decided.
5216                let pins = pins.map(|p| p.for_direction(rtl));
5217                let (want_w, want_h) = match pins {
5218                    Some(p) => (
5219                        p.resolved_width(bounds.width),
5220                        p.resolved_height(bounds.height),
5221                    ),
5222                    None => (Some(bounds.width), Some(bounds.height)),
5223                };
5224                // A pinned layer is **forced** into what its edges say: two opposite
5225                // pins are not a suggestion, and a width of its own is a contradiction
5226                // rather than a second opinion. An unpinned layer under `Expand` is
5227                // merely handed the box, which leaves a layer that chose a size alone —
5228                // that is what a badge sitting in a filled stack has always relied on.
5229                // An axis nobody pinned is left free, so the layer's own size comes back.
5230                let layer_rects = if pins.is_some() {
5231                    self.cached_rects(
5232                        cid,
5233                        layer.as_ref(),
5234                        Constraints::pinned(want_w, want_h, Size::new(bounds.width, bounds.height)),
5235                    )
5236                } else {
5237                    let room = Size::new(
5238                        want_w.unwrap_or(bounds.width),
5239                        want_h.unwrap_or(bounds.height),
5240                    );
5241                    // The stack's fit (`stack.dart:634`): allowed up to the room, forced
5242                    // into it, or handed it.
5243                    let c = match fit {
5244                        // A layer that asks for the room takes it here unasked: laid out on
5245                        // its own, its fill request is the root's.
5246                        crate::StackFit::Loose => Constraints::axes(
5247                            room.width,
5248                            room.height,
5249                            [false, false],
5250                            [false, false],
5251                        ),
5252                        crate::StackFit::Expand => {
5253                            Constraints::pinned(Some(room.width), Some(room.height), room)
5254                        }
5255                        crate::StackFit::Passthrough => Constraints::filled(room),
5256                    };
5257                    self.cached_rects(cid, layer.as_ref(), c)
5258                };
5259                let own = layer_rects
5260                    .first()
5261                    .copied()
5262                    .unwrap_or(Rect::new(0.0, 0.0, 0.0, 0.0));
5263                let (dx, dy) = match pins {
5264                    Some(p) => (
5265                        Positioning::place(
5266                            p.left,
5267                            p.right,
5268                            bounds.width,
5269                            own.width,
5270                            align.fraction_x(),
5271                        ),
5272                        Positioning::place(
5273                            p.top,
5274                            p.bottom,
5275                            bounds.height,
5276                            own.height,
5277                            align.fraction_y(),
5278                        ),
5279                    ),
5280                    None => (
5281                        (bounds.width - own.width).max(0.0) * align.fraction_x(),
5282                        (bounds.height - own.height).max(0.0) * align.fraction_y(),
5283                    ),
5284                };
5285                let mut layer_index = 0;
5286                // A layer that is not the shown one is walked in full — so it is measured,
5287                // and so the runtime keeps its scroll offset, its caret and its focus — and
5288                // then everything it added is dropped again. The same withdrawal a
5289                // `Visibility` that maintains its size performs, applied from outside.
5290                let hidden = shown.is_some_and(|which| which != i);
5291                let base = hidden.then(|| self.barrier_base());
5292                self.walk(
5293                    layer.as_ref(),
5294                    cid,
5295                    (bounds.x + dx, bounds.y + dy),
5296                    layer_clip,
5297                    &layer_rects,
5298                    &mut layer_index,
5299                );
5300                if let Some(base) = base {
5301                    self.apply_barrier(HIDDEN_LAYER, &base, cid, Rect::new(0.0, 0.0, 0.0, 0.0));
5302                }
5303            }
5304            self.touch_clip = outer_touch;
5305        } else if let Some((content, placement)) = widget.overlay() {
5306            // The anchor (child 0) is rendered inline; the overlay (child 1) is deferred.
5307            self.walk(
5308                widget.children()[0].as_ref(),
5309                child_id(id, 0, widget.children()[0].as_ref()),
5310                translation,
5311                clip,
5312                rects,
5313                index,
5314            );
5315            // The appearance progress: for an animated overlay (a drawer), the value the
5316            // runtime interpolated; otherwise `1.0` (shown at once). With no value recorded
5317            // (an isolated render), the target is adopted immediately.
5318            let target = widget.anim_target().unwrap_or(1.0);
5319            let progress = self.runtime.value_or(id, target);
5320            // A tooltip only shows while the anchor is hovered; an animated overlay disappears
5321            // once its progress has fallen back to zero.
5322            let visible = match placement {
5323                Placement::Tooltip => self.runtime.input.hovered == Some(id.child(0)),
5324                _ => true,
5325            };
5326            if visible && progress > 0.001 {
5327                self.overlays.push((
5328                    content,
5329                    child_id(id, 1, content),
5330                    draw_rect,
5331                    placement,
5332                    widget.overlay_dismiss(),
5333                    progress,
5334                    widget.overlay_traps_focus(),
5335                    widget.overlay_scrim(&self.theme),
5336                    self.theme.clone(),
5337                ));
5338            }
5339        } else {
5340            let children = widget.children();
5341            // Fractional alignment (`Container::alignment`) + paint offset
5342            // (`Transform::translate`): both offset the subtree. `(0, 0)` otherwise → the
5343            // ordinary flex walk.
5344            let extra = self.child_offset(widget, id, rect, rects, *index, children);
5345            if widget.sticky_header() && children.len() == 2 {
5346                self.walk_sticky(
5347                    widget,
5348                    id,
5349                    draw_rect,
5350                    (translation.0 + extra.0, translation.1 + extra.1),
5351                    clip,
5352                    rects,
5353                    index,
5354                );
5355                self.depth -= 1;
5356                return;
5357            }
5358            for (child_index, child) in children.iter().enumerate() {
5359                self.walk(
5360                    child.as_ref(),
5361                    child_id(id, child_index, child.as_ref()),
5362                    (translation.0 + extra.0, translation.1 + extra.1),
5363                    clip,
5364                    rects,
5365                    index,
5366                );
5367            }
5368        }
5369        self.depth -= 1;
5370    }
5371
5372    /// A section with a sticky header (milestone 602). The header is moved down to the top of
5373    /// what is visible — the clip — but never above the section's top nor so far that it
5374    /// leaves the section's bottom, which is how the next section's header pushes it off.
5375    ///
5376    /// It is drawn **over** the section, which goes under it, so what the header adds is
5377    /// moved after what the section adds: its primitives, and the registries a pointer is
5378    /// matched against last-first. Focus and semantics keep the reading order, header first.
5379    #[allow(clippy::too_many_arguments)]
5380    fn walk_sticky(
5381        &mut self,
5382        widget: &'a dyn Widget<Msg>,
5383        id: WidgetId,
5384        section: Rect,
5385        translation: (f32, f32),
5386        clip: Rect,
5387        rects: &[Rect],
5388        index: &mut usize,
5389    ) {
5390        let children = widget.children();
5391        let header = rects
5392            .get(*index)
5393            .copied()
5394            .unwrap_or(Rect::new(0.0, 0.0, 0.0, 0.0))
5395            .translate(translation.0, translation.1);
5396        let room = (section.y + section.height - (header.y + header.height)).max(0.0);
5397        let down = (clip.y - header.y).clamp(0.0, room);
5398        let start = self.barrier_base();
5399        self.walk(
5400            children[0].as_ref(),
5401            child_id(id, 0, children[0].as_ref()),
5402            (translation.0, translation.1 + down),
5403            clip,
5404            rects,
5405            index,
5406        );
5407        let mid = self.barrier_base();
5408        self.walk(
5409            children[1].as_ref(),
5410            child_id(id, 1, children[1].as_ref()),
5411            translation,
5412            clip,
5413            rects,
5414            index,
5415        );
5416        let mut primitives = self.scene.split_off(start.scene);
5417        after(&mut primitives, 0, mid.scene - start.scene);
5418        for primitive in primitives {
5419            self.scene.push_primitive(primitive);
5420        }
5421        after(&mut self.hits, start.hits, mid.hits);
5422        after(&mut self.long_presses, start.long_presses, mid.long_presses);
5423        after(&mut self.scrollables, start.scrollables, mid.scrollables);
5424        after(&mut self.draggables, start.draggables, mid.draggables);
5425        after(&mut self.drag_sources, start.drag_sources, mid.drag_sources);
5426        after(&mut self.drop_zones, start.drop_zones, mid.drop_zones);
5427        after(&mut self.inks, start.inks, mid.inks);
5428        after(&mut self.pans, start.pans, mid.pans);
5429        after(
5430            &mut self.pointer_listeners,
5431            start.pointer_listeners,
5432            mid.pointer_listeners,
5433        );
5434        after(
5435            &mut self.hover_regions,
5436            start.hover_regions,
5437            mid.hover_regions,
5438        );
5439        after(&mut self.interactives, start.interactives, mid.interactives);
5440    }
5441
5442    /// Offset to apply to a child's subtree: the sum of the **fractional alignment**
5443    /// (`Widget::alignment`, a single child slid through the free space) and the **paint
5444    /// offset** (`Transform::translate`, the whole subtree offset without touching layout).
5445    /// `(0, 0)` by default. Since it is added to the walk's translation, this offset follows
5446    /// both the primitives **and** the hit-test.
5447    fn child_offset(
5448        &self,
5449        widget: &dyn Widget<Msg>,
5450        id: WidgetId,
5451        container: Rect,
5452        rects: &[Rect],
5453        child_index: usize,
5454        children: &[Box<dyn Widget<Msg>>],
5455    ) -> (f32, f32) {
5456        let mut off = (0.0, 0.0);
5457
5458        // Fractional alignment: it targets a single child. taffy laid the child out at the
5459        // top left (Start/Start) at its natural size; it is then slid by `free × fraction`.
5460        // A **stack** answers this question too, and means something else by it: where a
5461        // layer smaller than the box sits, which the stack branch applies per layer. It
5462        // must not also be applied to the stack itself, or a stack with exactly one layer
5463        // would shift twice.
5464        if let (Some(geo), 1, false) = (widget.alignment_geometry(), children.len(), widget.stack())
5465        {
5466            // An **animated** anchor moves through the runtime's pair rather than the
5467            // widget's own numbers, in the anchor's own coordinates — so the reading
5468            // direction is applied to the interpolated anchor below, and not to the two
5469            // it came from.
5470            let geo = match self.runtime.anim_offset(id) {
5471                Some((x, y)) => geo.with_fractions(x, y),
5472                None => geo,
5473            };
5474            // Resolves the alignment (physical or directional) against the reading direction;
5475            // `resolve` produces a physical `Alignment` that the rest (with its RTL correction)
5476            // handles uniformly.
5477            let direction = if self.rtl() {
5478                frus_core::TextDirection::Rtl
5479            } else {
5480                frus_core::TextDirection::Ltr
5481            };
5482            let align = geo.resolve(direction);
5483            let child = rects[child_index];
5484            let pad = effective_style(widget, id, self.runtime, &self.theme).padding;
5485            let free_w = (container.width - pad.left - pad.right - child.width).max(0.0);
5486            let free_h = (container.height - pad.top - pad.bottom - child.height).max(0.0);
5487            // In RTL, taffy lays the child out on the left and `mirror` has sent it back to
5488            // the right: the baseline is therefore already right-aligned. 1 is subtracted so
5489            // the fraction stays **physical** (x = +1 ⇒ the right in both directions).
5490            let fx = align.fraction_x() - if self.rtl() { 1.0 } else { 0.0 };
5491            off.0 += free_w * fx;
5492            off.1 += free_h * align.fraction_y();
5493        }
5494
5495        // Paint offset (`Transform::translate`): in RTL the world's x axis is mirrored, so a
5496        // logical +x offset ("towards the end") points left — the sign is inverted to stay
5497        // consistent with the reading direction.
5498        if let Some((tx, ty)) = widget.transform_translate() {
5499            off.0 += if self.rtl() { -tx } else { tx };
5500            off.1 += ty;
5501        }
5502
5503        // The same offset, given as a **fraction of the child's own box**
5504        // (`FractionalTranslation`). The child's rectangle is the one thing this function
5505        // has that the widget did not: a widget cannot multiply by a width it was never
5506        // told.
5507        if let Some((fx, fy)) = widget.translate_fraction() {
5508            // The same tween, on the other offset rule: a slide that animates reads the
5509            // runtime's pair, and one that does not reads its own.
5510            let (fx, fy) = self.runtime.anim_offset(id).unwrap_or((fx, fy));
5511            let child = rects.get(child_index).copied().unwrap_or(container);
5512            let tx = child.width * fx;
5513            off.0 += if self.rtl() { -tx } else { tx };
5514            off.1 += child.height * fy;
5515        }
5516
5517        off
5518    }
5519
5520    /// Records the widget in the inspector's collection (when it is on) and opens a depth
5521    /// level — every render path closes it again at the end of the function
5522    /// (`self.depth -= 1`).
5523    fn inspect_enter(&mut self, widget: &dyn Widget<Msg>, id: WidgetId, draw_rect: Rect) {
5524        if let Some(nodes) = &mut self.inspector {
5525            nodes.push(crate::inspector::InspectorNode {
5526                id,
5527                rect: draw_rect,
5528                name: widget.debug_name(),
5529                depth: self.depth,
5530            });
5531        }
5532        self.depth += 1;
5533    }
5534
5535    /// A widget's full status: pointer interaction + focus + animation progresses + the
5536    /// cursor/selection when there is one.
5537    fn full_status(&self, widget: &dyn Widget<Msg>, id: WidgetId) -> crate::interaction::Status {
5538        let mut status = self.runtime.input.status_for(id);
5539        status.hover_progress = self.runtime.hover_progress(id);
5540        status.focus_progress = self.runtime.focus_progress(id);
5541        // A press is a progression now, and where the runtime has never heard of this
5542        // widget the flag is adopted whole — the rule `value` follows just below. Without
5543        // it a frame built outside the loop would draw a held widget as untouched.
5544        status.press_progress = self.runtime.press_progress_or(
5545            id,
5546            if status.interaction == crate::interaction::Interaction::Pressed {
5547                1.0
5548            } else {
5549                0.0
5550            },
5551        );
5552        status.opacity = self.runtime.opacity(id);
5553        // A widget's own animated value, or **its target** where the runtime has never
5554        // heard of it: the same rule the runtime applies on mount (adopt, do not animate
5555        // in from zero). Without the fallback an isolated render — a test, a frame built
5556        // before the loop has advanced anything — draws every such widget at zero: a
5557        // switch that is on drawn off, an indicator under the wrong tab.
5558        status.value = match widget.anim_target() {
5559            Some(target) => self.runtime.value_or(id, target),
5560            None => self.runtime.value(id),
5561        };
5562        status.value_placed = self.runtime.value_placed(id);
5563        status.anim_color = self.runtime.anim_color(id);
5564        status.anim_radius = self.runtime.anim_radius(id);
5565        status.time = self.runtime.time;
5566        status.drag_over = self.runtime.drag_over == Some(id);
5567        status.scroll_y = self.runtime.scroll.get(&id).map(|s| s.1).unwrap_or(0.0);
5568        // What a selection area has selected in this text, focused or not.
5569        status.region = self
5570            .runtime
5571            .region
5572            .as_ref()
5573            .and_then(|selection| selection.ranges.get(&id).copied());
5574        status.region_bar = status.region.is_some()
5575            && self
5576                .runtime
5577                .region
5578                .as_ref()
5579                .is_some_and(|selection| selection.bar);
5580        if let Some(selection) = self.runtime.region.as_ref().filter(|s| s.handles) {
5581            let here =
5582                |point: Option<crate::RegionPoint>| point.filter(|p| p.text == id).map(|p| p.index);
5583            status.region_grips = (here(selection.start), here(selection.end));
5584        }
5585        if status.focused {
5586            if let Some(edit) = self.runtime.edits.get(&id) {
5587                status.cursor = Some(edit.cursor);
5588                status.selection = edit.selection_range();
5589                status.composing = edit.composing;
5590            }
5591            status.handles = self.runtime.selection_handles == Some(id);
5592            status.toolbar = self
5593                .runtime
5594                .selection_toolbar
5595                .filter(|mark| mark.id == id)
5596                .map(|mark| mark.can_paste);
5597            status.caret_hidden = self.runtime.caret_hidden;
5598        }
5599        status
5600    }
5601
5602    /// The generic focus ring (for widgets that do not draw their own).
5603    fn draw_focus_ring(
5604        &mut self,
5605        draw_rect: Rect,
5606        status: &crate::interaction::Status,
5607        widget: &dyn Widget<Msg>,
5608    ) {
5609        // The generic ring only appears when the last interaction was a **keyboard** one
5610        // (`focus_visible`) — a click must not flash a ring.
5611        if status.focused
5612            && self.runtime.focus_visible
5613            && widget.focusable()
5614            && !widget.draws_own_focus()
5615        {
5616            let ring = Rect::new(
5617                draw_rect.x - 2.0,
5618                draw_rect.y - 2.0,
5619                draw_rect.width + 4.0,
5620                draw_rect.height + 4.0,
5621            );
5622            let alpha = 0.4 + 0.6 * status.focus_progress.clamp(0.0, 1.0);
5623            self.scene.draw_rect(
5624                ring,
5625                Color::TRANSPARENT,
5626                self.theme.radius + 2.0,
5627                2.0,
5628                self.theme.focus.fade(alpha),
5629            );
5630        }
5631    }
5632
5633    /// Keeps a widget this frame has just built for the rest of the walk, and hands back
5634    /// the reference the walk takes (milestone 592).
5635    fn keep(&self, widget: Box<dyn Widget<Msg>>) -> &'a dyn Widget<Msg> {
5636        let built: &'a Built<Msg> = self.built;
5637        let kept: &'a mut Box<dyn Widget<Msg>> = built.alloc(widget);
5638        &**kept
5639    }
5640
5641    /// Lays out a full-window screen and renders it offset by `off_x`.
5642    /// Renders a page in a transition, as `pose` puts it: moved along the reading direction
5643    /// (`mirror` turns that round), scaled about its centre and faded. A page at its
5644    /// natural size and opacity is drawn as it always was; otherwise it goes into one
5645    /// layer, and what can be touched in it moves with it.
5646    fn render_posed(
5647        &mut self,
5648        screen: &'a dyn Widget<Msg>,
5649        id: WidgetId,
5650        bounds: Rect,
5651        pose: crate::PagePose,
5652        mirror: f32,
5653        clip: Rect,
5654    ) {
5655        let off_x = pose.dx * bounds.width * mirror;
5656        let plain = (pose.scale - 1.0).abs() < 1e-4 && pose.opacity > 0.999;
5657        if plain {
5658            self.render_screen(screen, id, bounds, off_x, clip);
5659            return;
5660        }
5661        let mark = (self.scene.primitives().len(), self.xform_base());
5662        self.render_screen(screen, id, bounds, off_x, clip);
5663        let (p0, base) = mark;
5664        let group = self.scene.split_off(p0);
5665        // A page not yet faded in is still a layer, at nothing: a hero on it is about to
5666        // fly from it, and the flight takes it out of here.
5667        let centre = Point::new(
5668            bounds.x + off_x + bounds.width * 0.5,
5669            bounds.y + bounds.height * 0.5,
5670        );
5671        let matrix = Affine::scale(pose.scale, pose.scale).about(centre);
5672        self.scene.push_primitive(Primitive::Layer {
5673            primitives: group,
5674            opacity: pose.opacity.clamp(0.0, 1.0),
5675            clip,
5676            clip_shape: ClipShape::Rect,
5677            transform: Some(LayerTransform::new(matrix)),
5678            filter: LayerFilter::NONE,
5679            owner: id.as_u64(),
5680        });
5681        self.transform_interaction_registries(&base, matrix);
5682    }
5683
5684    /// The iOS page's shadow along its start edge (`route.dart:1065`): black at `0x04`
5685    /// fading to nothing over 5 % of the page's width, outside the page, at `frame`'s
5686    /// strength.
5687    fn paint_edge_shadow(
5688        &mut self,
5689        bounds: Rect,
5690        frame: crate::TransitionFrame,
5691        mirror: f32,
5692        clip: Rect,
5693    ) {
5694        let width = bounds.width * 0.05;
5695        let edge = if mirror < 0.0 {
5696            bounds.x + bounds.width + frame.top.dx * bounds.width * mirror
5697        } else {
5698            bounds.x + frame.top.dx * bounds.width
5699        };
5700        let shade = Color::rgba8(0, 0, 0, 0x04).fade(frame.edge_shadow);
5701        // Darkest at the page's edge, nothing at the far side: left to right, the colours
5702        // are ordered by which side the page is on.
5703        let (rect, from, to) = if mirror < 0.0 {
5704            (
5705                Rect::new(edge, bounds.y, width, bounds.height),
5706                shade,
5707                Color::TRANSPARENT,
5708            )
5709        } else {
5710            (
5711                Rect::new(edge - width, bounds.y, width, bounds.height),
5712                Color::TRANSPARENT,
5713                shade,
5714            )
5715        };
5716        self.scene.set_owner(0);
5717        self.scene.set_clip(clip);
5718        self.scene
5719            .gradient_rect(rect, from, to, [1.0, 0.0], 0.0, 0.0, Color::TRANSPARENT);
5720    }
5721
5722    fn render_screen(
5723        &mut self,
5724        screen: &'a dyn Widget<Msg>,
5725        id: WidgetId,
5726        bounds: Rect,
5727        off_x: f32,
5728        clip: Rect,
5729    ) {
5730        let rects = self.cached_rects(
5731            id,
5732            screen,
5733            Constraints::definite(Size::new(bounds.width, bounds.height)),
5734        );
5735        let screen_clip = clip.intersect(bounds);
5736        let mut index = 0;
5737        self.walk(
5738            screen,
5739            id,
5740            (bounds.x + off_x, bounds.y),
5741            screen_clip,
5742            &rects,
5743            &mut index,
5744        );
5745    }
5746
5747    /// Floats field `id`'s **selection bar** over everything, when the shell has marked it as
5748    /// showing (milestone 568): asks the field for the bar its situation calls for, and for
5749    /// the box to place it against, and defers it like any other overlay.
5750    ///
5751    /// The three conditions are read where they are true — the selection from the retained
5752    /// edit, the text from the widget, the clipboard from the mark the shell set when the
5753    /// bar opened — so they are the same ones the field's default list is decided by.
5754    fn push_selection_toolbar(
5755        &mut self,
5756        widget: &'a dyn Widget<Msg>,
5757        id: WidgetId,
5758        draw_rect: Rect,
5759        clip: Rect,
5760        status: &Status,
5761        can_paste: bool,
5762    ) {
5763        let Some(edit) = self.runtime.edits.get(&id).copied() else {
5764            return;
5765        };
5766        let Some(local) = widget.selection_anchor(draw_rect.width, &edit, status.scroll_y) else {
5767            return;
5768        };
5769        let anchor = local.translate(draw_rect.x, draw_rect.y);
5770        // Opened by a right-click, the bar goes where the pointer was (milestone 618), as
5771        // the reference's context menu does (`editable_text.dart:3211`); the selection
5772        // still decides whether it is shown at all.
5773        let pointer = self
5774            .runtime
5775            .selection_toolbar
5776            .filter(|mark| mark.id == id)
5777            .and_then(|mark| mark.at)
5778            .map(|at| Rect::new(at.x, at.y, 0.0, 0.0));
5779        // The bar goes with what it points at: a selection scrolled out of the region it is
5780        // drawn in takes its bar with it, rather than leaving it floating over nothing.
5781        let seen = anchor.x + anchor.width >= clip.x
5782            && anchor.x <= clip.x + clip.width
5783            && anchor.y + anchor.height > clip.y
5784            && anchor.y < clip.y + clip.height;
5785        if !seen {
5786            return;
5787        }
5788        let len = widget.text_value().map_or(0, |text| text.chars().count());
5789        let selection = edit
5790            .selection_range()
5791            .map(|(start, end)| (start.min(len), end.min(len)))
5792            .filter(|(start, end)| start < end);
5793        let context = crate::ToolbarContext {
5794            has_selection: selection.is_some(),
5795            can_paste,
5796            all_selected: len == 0 || selection == Some((0, len)),
5797        };
5798        let Some(content) = widget.selection_toolbar(context) else {
5799            return;
5800        };
5801        self.overlays.push((
5802            content,
5803            id.toolbar(context.variant()),
5804            pointer.unwrap_or(anchor),
5805            Placement::Selection,
5806            None,
5807            1.0,
5808            false,
5809            None,
5810            self.theme.clone(),
5811        ));
5812    }
5813
5814    /// Registers a region the mouse enters and leaves, as far as it shows (milestone 583).
5815    fn register_hover_region(
5816        &mut self,
5817        id: WidgetId,
5818        spec: crate::HoverRegion,
5819        whole: Rect,
5820        clip: Rect,
5821    ) {
5822        let seen = whole.intersect(clip);
5823        if seen.width > 0.0 && seen.height > 0.0 {
5824            self.hover_regions.push(HoverEntry {
5825                id,
5826                seen,
5827                whole,
5828                parent: self.hover_parent,
5829                spec,
5830            });
5831        }
5832    }
5833
5834    /// Floats the bar of a selection area's selection over `range` of this text — the first
5835    /// the selection covers, in reading order, since the walk meets them in that order.
5836    fn push_region_toolbar(
5837        &mut self,
5838        widget: &'a dyn Widget<Msg>,
5839        id: WidgetId,
5840        draw_rect: Rect,
5841        clip: Rect,
5842        range: (usize, usize),
5843    ) {
5844        let all_selected = self
5845            .runtime
5846            .region
5847            .as_ref()
5848            .is_some_and(|selection| selection.all);
5849        let context = crate::ToolbarContext {
5850            has_selection: true,
5851            can_paste: false,
5852            all_selected,
5853        };
5854        let Some((local, own)) = widget.region_toolbar(context, draw_rect.width, range) else {
5855            return;
5856        };
5857        // The area's bar, if the application said what it holds; the text's otherwise.
5858        let content = match self.area_widget.and_then(|area| area.area_toolbar(context)) {
5859            Some(Some(bar)) => bar,
5860            Some(None) => return,
5861            None => own,
5862        };
5863        let anchor = local.translate(draw_rect.x, draw_rect.y);
5864        let seen = anchor.x + anchor.width >= clip.x
5865            && anchor.x <= clip.x + clip.width
5866            && anchor.y + anchor.height > clip.y
5867            && anchor.y < clip.y + clip.height;
5868        if !seen {
5869            return;
5870        }
5871        self.region_bar_placed = true;
5872        self.overlays.push((
5873            content,
5874            id.toolbar(context.variant()),
5875            anchor,
5876            Placement::Selection,
5877            None,
5878            1.0,
5879            false,
5880            None,
5881            self.theme.clone(),
5882        ));
5883    }
5884
5885    /// Processes the deferred overlays: sub-layout, positioning and rendering **above**
5886    /// everything (their clickable areas win). May spawn further overlays (nested portals).
5887    fn process_overlays(&mut self) {
5888        let window = Rect::new(0.0, 0.0, self.available.width, self.available.height);
5889        while let Some((content, oid, anchor, placement, dismiss, progress, traps, scrim, theme)) =
5890            self.overlays.pop()
5891        {
5892            // The theme this overlay was declared under, for the whole of its layout and
5893            // painting; whatever was in force when the walk ended is put back after it,
5894            // so two overlays declared under two themes do not bleed into each other.
5895            let outer = std::mem::replace(&mut self.theme, theme);
5896            // Drawers slide along a **spring curve** (a gentle arrival), not linearly; the
5897            // other overlays keep their raw progress.
5898            let progress = if matches!(
5899                placement,
5900                Placement::Left | Placement::Right | Placement::Bottom
5901            ) {
5902                crate::runtime::spring_ease(progress)
5903            } else {
5904                progress
5905            };
5906            // The content's natural size. A drawer (`Left`) is constrained in height to the
5907            // window (so its `Percent(1.0)` panel unfolds) with a free width; the other
5908            // overlays take their natural size.
5909            let (free_x, free_y) = match placement {
5910                Placement::Left | Placement::Right => (true, false),
5911                // The sheet is full-width (constrained to the window) with its natural height:
5912                // its `Percent(1.0)` panel unfolds across the width.
5913                Placement::Bottom => (false, true),
5914                _ => (true, true),
5915            };
5916            let mut rects = self.cached_rects(
5917                oid,
5918                content,
5919                Constraints::scroll(self.available.width, self.available.height, free_x, free_y),
5920            );
5921            let mut size = rects
5922                .first()
5923                .copied()
5924                .unwrap_or(Rect::new(0.0, 0.0, 0.0, 0.0));
5925            // **Nothing wider than the window.** A free axis asks the content how big it
5926            // would like to be, and a dialog answers with the width of its widest line —
5927            // which, at a reader's font size, is wider than the screen. Centred, that
5928            // spills off *both* edges, and a button drawn at a negative x is a button
5929            // nobody can press. So the answer is taken as a wish rather than a size: past
5930            // the room there is, the content is laid out again at that room, and whatever
5931            // can fold — a paragraph, a bar of actions — folds.
5932            //
5933            // The room is the window less the content's **own margin**, which is how an
5934            // overlay says how far off the edges it wants to be held: a dialog's inset
5935            // padding is exactly that, and a clamp that ignored it would push the surface
5936            // flat against both sides of the screen.
5937            //
5938            // The height is deliberately left alone. A dialog too tall for the screen
5939            // wants its content to scroll, which is a question of its own; squashing it
5940            // here would only move the spill inside the surface.
5941            let inset = content.style_themed(&self.theme).margin;
5942            let room = (self.available.width - inset.left - inset.right).max(0.0);
5943            if free_x && size.width > room {
5944                rects = self.cached_rects(
5945                    oid,
5946                    content,
5947                    Constraints::scroll(room, self.available.height, false, free_y),
5948                );
5949                size = rects
5950                    .first()
5951                    .copied()
5952                    .unwrap_or(Rect::new(0.0, 0.0, 0.0, 0.0));
5953            }
5954
5955            // A drawer's slide-in from the left / right edge.
5956            let from_left = -(1.0 - progress) * size.width;
5957            let from_right = self.available.width - progress * size.width;
5958            // An anchored menu/tooltip: aligned to the anchor's **start** edge — in RTL that
5959            // edge is the right one (the menu opens leftwards).
5960            let anchor_x = if self.rtl() {
5961                anchor.x + anchor.width - size.width
5962            } else {
5963                anchor.x
5964            };
5965            let mut pos = match placement {
5966                Placement::Below => (anchor_x, anchor.y + anchor.height + 4.0),
5967                // A submenu: past the row's end edge, its first row level with the row —
5968                // the panel's own room above its rows taken back (milestone 603).
5969                Placement::Beside if self.rtl() => (anchor.x - size.width, anchor.y - 8.0),
5970                Placement::Beside => (anchor.x + anchor.width, anchor.y - 8.0),
5971                Placement::Center => (
5972                    (self.available.width - size.width) * 0.5,
5973                    (self.available.height - size.height) * 0.5,
5974                ),
5975                Placement::Tooltip => (anchor_x, anchor.y - size.height - 6.0),
5976                // The bar over a selection: on the desktops, a menu hanging from the pointer
5977                // or the selection's top (milestone 618); elsewhere centred on it, above or
5978                // below, inside the window.
5979                Placement::Selection if crate::selectiontoolbar::uses_menu(self.theme.platform) => {
5980                    crate::selectiontoolbar::place_menu(
5981                        crate::selectiontoolbar::primary_anchor(anchor),
5982                        Size::new(size.width, size.height),
5983                        window,
5984                    )
5985                }
5986                Placement::Selection => crate::selectiontoolbar::place(
5987                    anchor,
5988                    Size::new(size.width, size.height),
5989                    window,
5990                    crate::textinput::HANDLE_SIZE,
5991                ),
5992                // `Left` = a drawer on the **start** side; in RTL, start = the right.
5993                Placement::Left if self.rtl() => (from_right, 0.0),
5994                Placement::Left => (from_left, 0.0),
5995                // `Right` = the **end** side; in RTL, end = the left.
5996                Placement::Right if self.rtl() => (from_left, 0.0),
5997                Placement::Right => (from_right, 0.0),
5998                // The sheet slides up from the bottom: its bottom edge stays flush with the
5999                // window, offset downwards by `(1-progress)·height`.
6000                Placement::Bottom => (0.0, self.available.height - progress * size.height),
6001            };
6002
6003            // An **anchored** overlay follows a widget; the others (a modal, a drawer, a
6004            // sheet) are positioned against the window and have no anchor worth the name.
6005            // Only the first kind can find its anchor gone from the window: a screen
6006            // sliding out under a `Navigator`, a row scrolled sideways past the edge.
6007            let anchored = matches!(
6008                placement,
6009                Placement::Below | Placement::Tooltip | Placement::Beside
6010            );
6011            let anchor_on_screen = !anchored
6012                || (anchor.x < self.available.width
6013                    && anchor.x + anchor.width > 0.0
6014                    && anchor.y < self.available.height
6015                    && anchor.y + anchor.height > 0.0);
6016
6017            // Auto-flip: when an anchored overlay overflows an edge, it is flipped / nudged
6018            // back inside the window.
6019            //
6020            // This is for a menu opened near the right margin, and it assumes the anchor is
6021            // something the window is showing. When the anchor has **left** the window the
6022            // nudge does the opposite of its job: it drags a departing screen's menu back
6023            // into view, fully opaque, over the screen that replaced it. An overlay whose
6024            // anchor is off screen goes off screen with it.
6025            if anchored && anchor_on_screen {
6026                // A vertical overflow → flip to the other side of the anchor.
6027                if placement == Placement::Below
6028                    && pos.1 + size.height > self.available.height
6029                    && anchor.y - size.height - 4.0 >= 0.0
6030                {
6031                    pos.1 = anchor.y - size.height - 4.0;
6032                } else if placement == Placement::Tooltip
6033                    && pos.1 < 0.0
6034                    && anchor.y + anchor.height + size.height + 6.0 <= self.available.height
6035                {
6036                    pos.1 = anchor.y + anchor.height + 6.0;
6037                }
6038                // A submenu with no room on its end side opens on the other side of its row,
6039                // rather than being pushed back over the row it came from.
6040                if placement == Placement::Beside {
6041                    let (end, start) = if self.rtl() {
6042                        (anchor.x - size.width, anchor.x + anchor.width)
6043                    } else {
6044                        (anchor.x + anchor.width, anchor.x - size.width)
6045                    };
6046                    let fits = |x: f32| x >= 0.0 && x + size.width <= self.available.width;
6047                    if !fits(end) && fits(start) {
6048                        pos.0 = start;
6049                    }
6050                    if pos.1 + size.height > self.available.height {
6051                        pos.1 = (self.available.height - size.height).max(0.0);
6052                    }
6053                }
6054                // A horizontal overflow → nudge back inside the window.
6055                if pos.0 + size.width > self.available.width {
6056                    pos.0 = (self.available.width - size.width).max(0.0);
6057                }
6058                if pos.0 < 0.0 {
6059                    pos.0 = 0.0;
6060                }
6061            }
6062
6063            if matches!(
6064                placement,
6065                Placement::Center | Placement::Left | Placement::Right | Placement::Bottom
6066            ) {
6067                // The scrim behind the modal / the drawer, modulated by the progress (a fade
6068                // synchronised with the slide). The colour is the overlay's own when it asked
6069                // for one — alpha included, since a scrim's transparency *is* the colour —
6070                // and the `scrim` role at half opacity otherwise.
6071                let base = scrim.unwrap_or_else(|| self.theme.scheme.scrim.with_alpha(0.5));
6072                self.scene.set_owner(0);
6073                self.scene.set_clip(window);
6074                self.scene
6075                    .fill_rect(window, base.with_alpha(base.a * progress));
6076            }
6077
6078            // Dismissal on a click **outside** the content (a modal, a menu…): a full-screen
6079            // hit added **before** the content, so the content beats it where they overlap.
6080            // The dismissal is also remembered for Escape (the last one rendered = the
6081            // topmost).
6082            //
6083            // Gated on the same condition as the nudge, and for the same reason: a
6084            // window-wide barrier belonging to an overlay nobody can see would swallow the
6085            // next press anywhere on the screen that replaced it.
6086            if let Some(msg) = dismiss.filter(|_| anchor_on_screen) {
6087                self.overlay_cover.push(window);
6088                self.dismisses.push(msg.clone());
6089                self.hits.push(Hit {
6090                    id: oid.barrier(),
6091                    rect: window,
6092                    msg: Some(msg),
6093                    xform: None,
6094                });
6095            }
6096
6097            // A **modal** (scrimmed) or **trapping anchored** (menu) overlay: its focusables
6098            // form a **scope** that traps Tab/arrows/click-to-focus. The last one rendered (the
6099            // topmost) wins; anchored overlays that do not trap (a tooltip, an autocomplete
6100            // list) leave the focus alone.
6101            let modal = matches!(
6102                placement,
6103                Placement::Center | Placement::Left | Placement::Right | Placement::Bottom
6104            );
6105            if modal || traps {
6106                self.focus_scope_start = Some(self.focusables.len());
6107            }
6108
6109            // Where a press is the overlay's and not the pages' — a tooltip takes none.
6110            if placement != Placement::Tooltip && anchor_on_screen {
6111                self.overlay_cover
6112                    .push(Rect::new(pos.0, pos.1, size.width, size.height));
6113            }
6114
6115            // A bar over a selection: where it ended up, so that a press on it is known for
6116            // one — see [`Ui::toolbar_contains`].
6117            if placement == Placement::Selection {
6118                self.toolbars
6119                    .push(Rect::new(pos.0, pos.1, size.width, size.height));
6120            }
6121
6122            let mut index = 0;
6123            self.walk(content, oid, pos, window, &rects, &mut index);
6124            self.theme = outer;
6125        }
6126    }
6127}
6128
6129impl<Msg: Clone> Builder<'_, Msg> {
6130    /// Draws a scrollbar (track + thumb) and registers it for hit-testing a drag.
6131    /// Paints the region's overscroll glows over its viewport.
6132    ///
6133    /// The colour is the scheme's **secondary**: the edge feedback is an
6134    /// acknowledgement, not a call to action, so it must read as part of the surface
6135    /// rather than compete with whatever primary-coloured control sits nearby.
6136    fn add_overscroll_glow(&mut self, id: WidgetId, viewport: Rect) {
6137        if let Some(glows) = self.runtime.scroll_glow.get(&id) {
6138            glows.paint(viewport, self.theme.scheme.secondary, &mut self.scene);
6139        }
6140    }
6141
6142    fn add_scrollbar(&mut self, id: WidgetId, viewport: Rect, axis: Bar) {
6143        let Bar {
6144            vertical,
6145            offset,
6146            max,
6147            reverse,
6148            asked,
6149            visible,
6150        } = axis;
6151        // **Along the horizontal axis, never** (`app.dart:861`) — on any platform, under
6152        // any setting. A sideways strip is scrolled by the thing that made it sideways,
6153        // and a bar under it takes room from content that is short of it already.
6154        if !vertical {
6155            return;
6156        }
6157        // And down the vertical one, only where the platform's own scroll views have a
6158        // bar (`app.dart:865` against `:870`). A finger already knows where it is on the
6159        // page; a permanent bar over the content is an affordance for a pointer that
6160        // cannot feel the edges. This drew one everywhere, on both axes, always.
6161        let behavior = crate::ScrollConfiguration::of().scrollbars(&self.theme);
6162        if asked.unwrap_or(behavior) == crate::physics::Scrollbars::Never {
6163            return;
6164        }
6165        let (track_start, track_len, content_len) = if vertical {
6166            (viewport.y, viewport.height, viewport.height + max)
6167        } else {
6168            (viewport.x, viewport.width, viewport.width + max)
6169        };
6170        // Every metric below is `theme ?? framework`. There is no per-call override for a
6171        // scrollbar — it is drawn by the walk, not by a widget a caller configured — so
6172        // the chain is two terms here rather than three.
6173        let bar = &self.theme.widgets.scrollbar;
6174        // **Under an Android theme, the platform's own bar** (`scrollbar.dart:331`): half
6175        // as thick, flush with the edge, square-ended, an opaque grey at rest, and not
6176        // something a pointer takes hold of. The theme's platform decides it, as the
6177        // reference's does, so a theme set to Android draws it on any device.
6178        let android = self.theme.platform == frus_core::TargetPlatform::Android;
6179        let bar_size = bar
6180            .thickness
6181            .unwrap_or(if android { BAR_SIZE / 2.0 } else { BAR_SIZE });
6182        let bar_margin = bar.margin.unwrap_or(if android { 0.0 } else { BAR_MARGIN });
6183        let thumb_len = (track_len * track_len / content_len)
6184            .max(bar.min_thumb_length.unwrap_or(MIN_THUMB))
6185            .min(track_len);
6186        let travel = track_len - thumb_len;
6187        // Along a reversed axis the numbers run the other way, so the thumb does too:
6188        // offset 0 is the far end, and that is where it rests.
6189        let fraction = if max > 0.0 { offset / max } else { 0.0 };
6190        let fraction = if reverse { 1.0 - fraction } else { fraction };
6191        let thumb_pos = track_start + fraction * travel;
6192
6193        // The track's rectangle is still worked out — it is the room a pointer has to be
6194        // in, and the geometry the thumb sits in — but nothing is painted for it.
6195        //
6196        // `BAR_SIZE` is the **thumb's** thickness, not a slot's: the reference keeps the
6197        // thumb clear of the edge with `crossAxisMargin` (`scrollbar.dart:357`) rather
6198        // than shrinking it to fit. This drew a 6-pixel thumb inside an 8-pixel slot,
6199        // which is the same arithmetic with the wrong number left over.
6200        let (track, thumb) = if vertical {
6201            let x = viewport.x + viewport.width - bar_margin - bar_size;
6202            (
6203                Rect::new(
6204                    x - bar_margin,
6205                    viewport.y,
6206                    bar_size + bar_margin * 2.0,
6207                    viewport.height,
6208                ),
6209                Rect::new(x, thumb_pos, bar_size, thumb_len),
6210            )
6211        } else {
6212            let y = viewport.y + viewport.height - bar_margin - bar_size;
6213            (
6214                Rect::new(
6215                    viewport.x,
6216                    y - bar_margin,
6217                    viewport.width,
6218                    bar_size + bar_margin * 2.0,
6219                ),
6220                Rect::new(thumb_pos, y, thumb_len, bar_size),
6221            )
6222        };
6223        // What a mouse has to be inside for the bar to notice it: the track, widened to
6224        // take in a tap target's worth of room around the thumb (`scrollbar.dart:762`).
6225        let reach = {
6226            let (cx, cy) = (thumb.x + thumb.width * 0.5, thumb.y + thumb.height * 0.5);
6227            let r = crate::theme::MIN_TAP_TARGET * 0.5;
6228            let x0 = track.x.min(cx - r);
6229            let y0 = track.y.min(cy - r);
6230            let x1 = (track.x + track.width).max(cx + r);
6231            let y1 = (track.y + track.height).max(cy + r);
6232            Rect::new(x0, y0, x1 - x0, y1 - y0)
6233        };
6234
6235        // **How present the bar is**, this frame. It arrives when the area moves and goes
6236        // again once the area has been still long enough; an area nobody has scrolled has
6237        // none at all. Unless it was asked to stay (`scrollbar.dart:214`).
6238        let fade = self.runtime.scrollbar_fade_of(id);
6239        let opacity = if visible.unwrap_or(false) {
6240            1.0
6241        } else {
6242            fade.opacity.clamp(0.0, 1.0)
6243        };
6244
6245        // **No track.** The reference's is transparent unless a caller asks for one
6246        // (`scrollbar.dart:281`), and even then it is 3 to 5 %.
6247        //
6248        // The thumb takes the reference's opacity for the surface's brightness: at rest
6249        // (`scrollbar.dart:242`, `:248`), warmed towards its hovered value while a
6250        // pointer is near it (`:239`, `:245`), and at its dragged value the moment the
6251        // thumb is held (`:238`, `:244`) — that one does not fade in, because the hand is
6252        // already on it and a fade would only lag behind the grab.
6253        // The scheme's own answer, which is what the reference reads here
6254        // (`scrollbar.dart:232`). This used to be `surface.compute_luminance() < 0.5`:
6255        // right on both built-in schemes and a guess on any other.
6256        let dark = self.theme.brightness().is_dark();
6257        let (rest, warm, grabbed) = if dark {
6258            (THUMB_ON_DARK, THUMB_HOVER_ON_DARK, THUMB_DRAG_ON_DARK)
6259        } else {
6260            (THUMB_ON_LIGHT, THUMB_HOVER_ON_LIGHT, THUMB_DRAG_ON_LIGHT)
6261        };
6262        // A theme's levels replace the scheme's, one at a time: naming the resting
6263        // opacity says nothing about the held one.
6264        let rest = bar.opacity.unwrap_or(rest);
6265        let warm = bar.hover_opacity.unwrap_or(warm);
6266        let grabbed = bar.drag_opacity.unwrap_or(grabbed);
6267        let base = bar.thumb_color.unwrap_or(self.theme.scheme.on_surface);
6268        // At rest under an Android theme, the reference's thumb is the theme's highlight
6269        // made opaque (`scrollbar.dart:240`, `:246`), not a fade of the content colour —
6270        // unless the theme chose a colour or a resting level of its own.
6271        let idle = if android && bar.thumb_color.is_none() && bar.opacity.is_none() {
6272            if dark {
6273                ANDROID_THUMB_ON_DARK
6274            } else {
6275                ANDROID_THUMB_ON_LIGHT
6276            }
6277        } else {
6278            base.fade(rest)
6279        };
6280        let color = if self.runtime.scrollbar_dragged == Some(id) {
6281            base.fade(grabbed)
6282        } else {
6283            idle.lerp(base.fade(warm), fade.hover.clamp(0.0, 1.0))
6284        };
6285        if opacity > 0.0 {
6286            self.scene.draw_rect(
6287                thumb,
6288                color.fade(opacity),
6289                bar.radius
6290                    .unwrap_or(if android { 0.0 } else { bar_size * 0.5 }),
6291                0.0,
6292                Color::TRANSPARENT,
6293            );
6294        }
6295        // Registered even when nothing was painted for it — unlike a bar the platform
6296        // does not draw at all, which is not registered because it is not there. A mouse
6297        // coming near a faded bar is what brings it back (`scrollbar.dart:2132`), so
6298        // something has to be here for the mouse to find. What a bar at zero cannot be is
6299        // **grabbed**; see `Ui::scrollbar_at`.
6300        self.scrollbars.push(Scrollbar {
6301            id,
6302            vertical,
6303            thumb,
6304            track_start,
6305            track_len,
6306            thumb_len,
6307            reverse,
6308            max,
6309            opacity,
6310            reach,
6311            interactive: bar.interactive.unwrap_or(!android),
6312        });
6313    }
6314}
6315
6316/// `primitives` without those owned by `hidden`, looking inside layers too; the ones owned
6317/// by `keep` among them are moved into `taken`, as painted, outside any layer. A layer left
6318/// with nothing in it is dropped.
6319fn take_owned(
6320    primitives: Vec<Primitive>,
6321    hidden: &std::collections::HashSet<u64>,
6322    keep: &std::collections::HashSet<u64>,
6323    taken: &mut Vec<Primitive>,
6324) -> Vec<Primitive> {
6325    let mut out = Vec::with_capacity(primitives.len());
6326    for primitive in primitives {
6327        match primitive {
6328            Primitive::Layer {
6329                primitives: inner,
6330                opacity,
6331                clip,
6332                clip_shape,
6333                transform,
6334                filter,
6335                owner,
6336            } => {
6337                let inner = take_owned(inner, hidden, keep, taken);
6338                if !inner.is_empty() {
6339                    out.push(Primitive::Layer {
6340                        primitives: inner,
6341                        opacity,
6342                        clip,
6343                        clip_shape,
6344                        transform,
6345                        filter,
6346                        owner,
6347                    });
6348                }
6349            }
6350            other if hidden.contains(&other.owner()) => {
6351                if keep.contains(&other.owner()) {
6352                    taken.push(other);
6353                }
6354            }
6355            other => out.push(other),
6356        }
6357    }
6358    out
6359}
6360
6361/// Turns a widget tree into a [`Ui`] for a given size, runtime state and theme.
6362pub fn build_ui<'a, Msg: Clone + 'static>(
6363    root: &'a dyn Widget<Msg>,
6364    available: Size,
6365    runtime: &'a Runtime,
6366    theme: &'a Theme,
6367) -> Ui<Msg> {
6368    build_ui_impl(root, available, runtime, theme, false).0
6369}
6370
6371/// Like [`build_ui`], but it also collects the **inspection nodes** (one per painted widget:
6372/// name, box, depth) — the raw material of the runtime inspector and of the diagnostic dump.
6373/// Reserve it for the frames where the inspector is on.
6374pub fn build_ui_inspected<'a, Msg: Clone + 'static>(
6375    root: &'a dyn Widget<Msg>,
6376    available: Size,
6377    runtime: &'a Runtime,
6378    theme: &'a Theme,
6379) -> (Ui<Msg>, Vec<crate::inspector::InspectorNode>) {
6380    let (ui, nodes) = build_ui_impl(root, available, runtime, theme, true);
6381    (ui, nodes.unwrap_or_default())
6382}
6383
6384/// Panics, **in debug builds only**, when the reader's font size is installed and the
6385/// surface it belongs to is not.
6386///
6387/// The two travel together — [`MediaQuery::install`] sets both — so a scale away from 1
6388/// with no description means somebody installed half a surface, and the half they left out
6389/// is about to be missing for the layout that follows. That is not a theory: for four
6390/// milestones the shell scoped only the build, the scale was 1 while text was measured and
6391/// painted, and the reader's setting reached a device without moving a pixel. Nothing said
6392/// a word — not the 91 goldens, not clippy, not the strict rustdoc.
6393///
6394/// Debug only, because it is a wiring mistake rather than a state a running application can
6395/// reach: it should stop a test, not a user's frame. A scale of exactly 1 is the neutral
6396/// value and says nothing either way, so it is never a failure.
6397#[cfg(debug_assertions)]
6398fn assert_surface_is_whole() {
6399    let scale = frus_core::text_scale();
6400    if (scale - 1.0).abs() > f32::EPSILON && !crate::MediaQuery::of().is_described() {
6401        panic!(
6402            "a text scale of {scale} is installed with no surface described: something \
6403             installed half a surface. Use `MediaQuery::install` (or `scope`), which sets \
6404             the description and the reader's font size together \u{2014} see milestone 408."
6405        );
6406    }
6407}
6408
6409/// Where a frame keeps the widgets it builds as it walks (see `Builder::built`).
6410type Built<Msg> = typed_arena::Arena<Box<dyn Widget<Msg>>>;
6411
6412fn build_ui_impl<Msg: Clone + 'static>(
6413    root: &dyn Widget<Msg>,
6414    available: Size,
6415    runtime: &Runtime,
6416    theme: &Theme,
6417    inspect: bool,
6418) -> (Ui<Msg>, Option<Vec<crate::inspector::InspectorNode>>) {
6419    // Made first, so that it outlives the walk that borrows from it.
6420    let built = Built::new();
6421    build_ui_walk(root, available, runtime, theme, inspect, &built)
6422}
6423
6424fn build_ui_walk<'a, Msg: Clone + 'static>(
6425    root: &'a dyn Widget<Msg>,
6426    available: Size,
6427    runtime: &'a Runtime,
6428    theme: &'a Theme,
6429    inspect: bool,
6430    built: &'a Built<Msg>,
6431) -> (Ui<Msg>, Option<Vec<crate::inspector::InspectorNode>>) {
6432    #[cfg(debug_assertions)]
6433    assert_surface_is_whole();
6434
6435    let (mut rects, overflows) = runtime.layout_cache.borrow_mut().rects(
6436        WidgetId::ROOT,
6437        root,
6438        runtime,
6439        theme,
6440        Constraints::definite(available),
6441    );
6442
6443    let mut builder = Builder {
6444        scene: Scene::new(),
6445        hits: Vec::new(),
6446        long_presses: Vec::new(),
6447        dismisses: Vec::new(),
6448        edit_actions: Vec::new(),
6449        toolbars: Vec::new(),
6450        overlay_cover: Vec::new(),
6451        focusables: Vec::new(),
6452        focus_scope_start: None,
6453        scrollables: Vec::new(),
6454        scrollbars: Vec::new(),
6455        draggables: Vec::new(),
6456        drag_sources: Vec::new(),
6457        drop_zones: Vec::new(),
6458        inks: Vec::new(),
6459        pans: Vec::new(),
6460        title_bar_row: None,
6461        title_bar_regions: Vec::new(),
6462        pointer_listeners: Vec::new(),
6463        hover_regions: Vec::new(),
6464        hover_parent: None,
6465        text_stops: Vec::new(),
6466        area: None,
6467        area_widget: None,
6468        region_bar_placed: false,
6469        reorderables: Vec::new(),
6470        interactives: Vec::new(),
6471        overlays: Vec::new(),
6472        wants_animation: false,
6473        available,
6474        runtime,
6475        theme: std::rc::Rc::new(theme.clone()),
6476        inspector: inspect.then(Vec::new),
6477        depth: 0,
6478        refresh_host: None,
6479        scroll_host: None,
6480        hero_screen: None,
6481        heroes: Vec::new(),
6482        refreshes: Vec::new(),
6483        dismissables: Vec::new(),
6484        sheets: Vec::new(),
6485        semantics: Vec::new(),
6486        system_ui: Vec::new(),
6487        focus_excluded: false,
6488        touch_clip: None,
6489        backdrop_group: None,
6490        focus_skipped: false,
6491        focus_order: None,
6492        focus_group: None,
6493        autofill_group: None,
6494        scopes: Vec::new(),
6495        listeners: Vec::new(),
6496        overflows: std::cell::RefCell::new(Vec::new()),
6497        pending_overflows: std::cell::RefCell::new(std::collections::HashMap::new()),
6498        built,
6499    };
6500    // The root is mirrored in RTL (like every layout root).
6501    builder.mirror(&mut rects);
6502    builder.record_overflows(WidgetId::ROOT, &rects, overflows);
6503    let mut index = 0;
6504    builder.walk(
6505        root,
6506        WidgetId::ROOT,
6507        (0.0, 0.0),
6508        Rect::UNBOUNDED,
6509        &rects,
6510        &mut index,
6511    );
6512
6513    // Overlays (floating menus, modals, tooltips) above everything else. (Their walk restarts
6514    // from depth 0: roots as far as the inspector is concerned.)
6515    builder.process_overlays();
6516
6517    // End of frame: forget the layout roots and repaint boundaries of widgets that have gone,
6518    // and freeze the caches' diagnostic counters.
6519    runtime.layout_cache.borrow_mut().end_frame();
6520    runtime.paint_cache.borrow_mut().end_frame();
6521
6522    // Replays the outgoing subtrees, fading out, over the current scene.
6523    builder.scene.set_clip(Rect::UNBOUNDED);
6524    for (primitives, opacity) in runtime.leaving.values() {
6525        for primitive in primitives {
6526            builder.scene.push_faded(primitive, *opacity);
6527        }
6528    }
6529
6530    // A box whose children ran past it now **says so on the screen**, not only in the
6531    // console — see `paint_overflow_bands`. Last, so nothing paints over it.
6532    paint_overflow_bands(&mut builder.scene, &builder.overflows.borrow());
6533
6534    let ui = Ui {
6535        scene: builder.scene,
6536        hits: builder.hits,
6537        long_presses: builder.long_presses,
6538        dismisses: builder.dismisses,
6539        edit_actions: builder.edit_actions,
6540        toolbars: builder.toolbars,
6541        overlay_cover: builder.overlay_cover,
6542        focusables: builder.focusables,
6543        focus_scope_start: builder.focus_scope_start,
6544        scrollables: builder.scrollables,
6545        scrollbars: builder.scrollbars,
6546        draggables: builder.draggables,
6547        drag_sources: builder.drag_sources,
6548        drop_zones: builder.drop_zones,
6549        inks: builder.inks,
6550        pans: builder.pans,
6551        title_bar_row: builder.title_bar_row,
6552        title_bar_regions: builder.title_bar_regions,
6553        pointer_listeners: builder.pointer_listeners,
6554        hover_regions: builder.hover_regions,
6555        text_stops: builder.text_stops,
6556        reorderables: builder.reorderables,
6557        interactives: builder.interactives,
6558        refreshes: builder.refreshes,
6559        dismissables: builder.dismissables,
6560        sheets: builder.sheets,
6561        wants_animation: builder.wants_animation,
6562        semantics: builder.semantics,
6563        system_ui: builder.system_ui,
6564        overflows: builder.overflows.into_inner(),
6565        scopes: builder.scopes,
6566        listeners: builder.listeners,
6567    };
6568    (ui, builder.inspector)
6569}
6570
6571/// Collects the identities of every widget in the tree (prefix order), following the same
6572/// positional scheme as [`build_ui`]. Used to detect mounts/unmounts between two frames.
6573pub fn collect_ids<Msg>(root: &dyn Widget<Msg>) -> Vec<WidgetId> {
6574    fn walk<Msg>(widget: &dyn Widget<Msg>, id: WidgetId, out: &mut Vec<WidgetId>) {
6575        out.push(id);
6576        for (index, child) in widget.children().iter().enumerate() {
6577            walk(child.as_ref(), child_id(id, index, child.as_ref()), out);
6578        }
6579    }
6580    let mut out = Vec::new();
6581    walk(root, WidgetId::ROOT, &mut out);
6582    out
6583}
6584
6585/// Identities of the **subtree** rooted at `root_id` — `widget` being the widget with that
6586/// identity — that is `[root_id, …descendants]`, derived by the same positional scheme as
6587/// [`collect_ids`].
6588///
6589/// Used by the **drag ghost**: a **rich** card's primitives are painted by its children (owners
6590/// other than the card itself). To capture all of its visuals, the shell gathers the primitives
6591/// of the **whole** subtree, not only the card's own.
6592pub fn subtree_ids<Msg>(widget: &dyn Widget<Msg>, root_id: WidgetId) -> Vec<WidgetId> {
6593    fn walk<Msg>(widget: &dyn Widget<Msg>, id: WidgetId, out: &mut Vec<WidgetId>) {
6594        out.push(id);
6595        for (index, child) in widget.children().iter().enumerate() {
6596            walk(child.as_ref(), child_id(id, index, child.as_ref()), out);
6597        }
6598    }
6599    let mut out = Vec::new();
6600    walk(widget, root_id, &mut out);
6601    out
6602}
6603
6604/// The owners of everything a carried row, card or item may be **moved past** this frame:
6605/// every reorderable that can be dropped on, and all it paints.
6606///
6607/// What a reorder preview slides out of the way is decided by geometry — the band the
6608/// source sits in — and geometry alone moves whatever shares that band: a button floating
6609/// over a list, the navigation bar under it. This is the other half of the question, *is
6610/// it part of something that can be reordered*, asked of the tree. A grip is inside its
6611/// row, so it moves with it without being a target itself.
6612pub fn reorderable_owners<Msg>(
6613    ui: &Ui<Msg>,
6614    root: &dyn Widget<Msg>,
6615) -> std::collections::HashSet<u64> {
6616    ui.reorderables
6617        .iter()
6618        .filter_map(|(id, _)| {
6619            find_widget(root, *id)
6620                .filter(|widget| widget.reorder_droppable())
6621                .map(|widget| subtree_ids(widget, *id))
6622        })
6623        .flatten()
6624        .map(|id| id.as_u64())
6625        .collect()
6626}
6627
6628/// The slots a reorderable `id` can be dropped among **in its own list**: its siblings — the
6629/// children of its parent — that can be dropped on, with their boxes this frame, in order.
6630///
6631/// A list's rows are one widget's children, and so are a board column's cards and the drop
6632/// zone that ends it. Nothing else on the page is, which is what lets a drop that landed on
6633/// no row at all still mean *somewhere in this list* rather than anywhere reorderable.
6634/// Siblings the frame registered no box for are left out.
6635pub fn reorder_siblings<Msg>(
6636    ui: &Ui<Msg>,
6637    root: &dyn Widget<Msg>,
6638    id: WidgetId,
6639) -> Vec<(WidgetId, Rect)> {
6640    fn parent_of<Msg>(
6641        widget: &dyn Widget<Msg>,
6642        own: WidgetId,
6643        target: WidgetId,
6644    ) -> Option<(&dyn Widget<Msg>, WidgetId)> {
6645        for (index, child) in widget.children().iter().enumerate() {
6646            let id = child_id(own, index, child.as_ref());
6647            if id == target {
6648                return Some((widget, own));
6649            }
6650            if let Some(found) = parent_of(child.as_ref(), id, target) {
6651                return Some(found);
6652            }
6653        }
6654        None
6655    }
6656    let Some((parent, parent_id)) = parent_of(root, WidgetId::ROOT, id) else {
6657        return Vec::new();
6658    };
6659    parent
6660        .children()
6661        .iter()
6662        .enumerate()
6663        .filter(|(_, child)| child.reorder_index().is_some() && child.reorder_droppable())
6664        .filter_map(|(index, child)| {
6665            let id = child_id(parent_id, index, child.as_ref());
6666            ui.reorderables
6667                .iter()
6668                .find(|(registered, _)| *registered == id)
6669                .map(|(_, rect)| (id, *rect))
6670        })
6671        .collect()
6672}
6673
6674/// Identity of the **first** widget declaring the key `key` (a hash), or `None`. It is how the
6675/// shell resolves a focus-by-key request (`Command::focus`): the application wraps a field in
6676/// `keyed(k, …)`, then focuses by `k`.
6677pub fn find_by_key<Msg>(root: &dyn Widget<Msg>, key: u64) -> Option<WidgetId> {
6678    fn walk<Msg>(widget: &dyn Widget<Msg>, id: WidgetId, key: u64) -> Option<WidgetId> {
6679        if widget.key() == Some(key) {
6680            return Some(id);
6681        }
6682        for (index, child) in widget.children().iter().enumerate() {
6683            if let Some(found) = walk(child.as_ref(), child_id(id, index, child.as_ref()), key) {
6684                return Some(found);
6685            }
6686        }
6687        None
6688    }
6689    walk(root, WidgetId::ROOT, key)
6690}
6691
6692/// Identity of the **sheet** the key `key` names: the first sheet's panel at or under the
6693/// widget declaring it, which is the identity the sheet's state is kept under and the one the
6694/// frame's sheets are reported by. `None` when nothing declares the key, or nothing under it
6695/// is a sheet.
6696///
6697/// A sheet's state belongs to its panel, which the application never builds itself, so a
6698/// request by key (`Command::sheet`) needs this one step more than a focus request does.
6699pub fn find_sheet_by_key<Msg>(root: &dyn Widget<Msg>, key: u64) -> Option<WidgetId> {
6700    fn first_sheet<Msg>(widget: &dyn Widget<Msg>, id: WidgetId) -> Option<WidgetId> {
6701        if widget.sheet().is_some() {
6702            return Some(id);
6703        }
6704        widget
6705            .children()
6706            .iter()
6707            .enumerate()
6708            .find_map(|(index, child)| {
6709                first_sheet(child.as_ref(), child_id(id, index, child.as_ref()))
6710            })
6711    }
6712    fn walk<Msg>(widget: &dyn Widget<Msg>, id: WidgetId, key: u64) -> Option<WidgetId> {
6713        if widget.key() == Some(key) {
6714            return first_sheet(widget, id);
6715        }
6716        widget
6717            .children()
6718            .iter()
6719            .enumerate()
6720            .find_map(|(index, child)| {
6721                walk(child.as_ref(), child_id(id, index, child.as_ref()), key)
6722            })
6723    }
6724    walk(root, WidgetId::ROOT, key)
6725}
6726
6727/// The messages a **move of the focus** from `before` to `after` sends (milestone 660): each
6728/// widget whose subtree the focus left hears `false`, deepest first, then each whose subtree
6729/// it entered hears `true`, deepest first. The widgets around both — the paths' shared start
6730/// — hear nothing; a widget no longer in the tree is not heard from.
6731pub fn focus_changes<Msg>(
6732    root: &dyn Widget<Msg>,
6733    before: Option<WidgetId>,
6734    after: Option<WidgetId>,
6735) -> Vec<Msg> {
6736    let path = |id: Option<WidgetId>| id.map_or_else(Vec::new, |id| find_path(root, id));
6737    let (left, entered) = (path(before), path(after));
6738    let shared = left
6739        .iter()
6740        .zip(&entered)
6741        .take_while(|(a, b)| std::ptr::addr_eq(**a, **b))
6742        .count();
6743    let lost = left[shared..]
6744        .iter()
6745        .rev()
6746        .filter_map(|widget| widget.on_focus_change(false));
6747    let gained = entered[shared..]
6748        .iter()
6749        .rev()
6750        .filter_map(|widget| widget.on_focus_change(true));
6751    lost.chain(gained).collect()
6752}
6753
6754/// Path from the **root down to the widget** with identity `target` (`[root, …, target]`), for
6755/// the leaf→root bubbling of keys. Empty when it cannot be found.
6756pub fn find_path<Msg>(root: &dyn Widget<Msg>, target: WidgetId) -> Vec<&dyn Widget<Msg>> {
6757    fn walk<'a, Msg>(
6758        widget: &'a dyn Widget<Msg>,
6759        id: WidgetId,
6760        target: WidgetId,
6761        path: &mut Vec<&'a dyn Widget<Msg>>,
6762    ) -> bool {
6763        path.push(widget);
6764        if id == target {
6765            return true;
6766        }
6767        for (index, child) in widget.children().iter().enumerate() {
6768            if walk(
6769                child.as_ref(),
6770                child_id(id, index, child.as_ref()),
6771                target,
6772                path,
6773            ) {
6774                return true;
6775            }
6776        }
6777        path.pop();
6778        false
6779    }
6780    let mut path = Vec::new();
6781    if !walk(root, WidgetId::ROOT, target, &mut path) {
6782        path.clear();
6783    }
6784    path
6785}
6786
6787/// The theme a subtree inherits at this node — the widget's own override, with any
6788/// **animated text style** the runtime is holding for it laid on top.
6789///
6790/// The one place the theme swap happens, and it is one place on purpose. The swap is made
6791/// four times over a frame — by the layout pass, by the relayout fingerprint that has to
6792/// agree with it or the cache lies, by the paint walk, and by [`build_deferred`] before
6793/// any of them — and every animated value in this framework is read from the runtime by
6794/// whoever consumes it. This one's consumer is the swap itself, so a copy of the rule at
6795/// each of the four sites is four chances to drift. `Responsive` cost three silent bugs
6796/// that way (milestone 495); this is the same lesson written down as a function.
6797///
6798/// Cheap where nothing is animated: no `anim_text_style`, no clone, and the widget's own
6799/// `theme_override` is returned untouched — which is every node but the rare one.
6800pub(crate) fn scoped_theme<Msg>(
6801    widget: &dyn Widget<Msg>,
6802    id: WidgetId,
6803    runtime: &Runtime,
6804    inherited: &Theme,
6805) -> Option<Box<Theme>> {
6806    let own = widget.theme_override(inherited);
6807    let Some(target) = widget.anim_text_style() else {
6808        return own;
6809    };
6810    // Mid-flight if the runtime has a value for this node, and the target otherwise —
6811    // which is the first frame, before the family has been advanced over this tree.
6812    let style = runtime.anim_text_style(id).unwrap_or(target);
6813    let mut theme = own.map_or_else(|| inherited.clone(), |boxed| *boxed);
6814    // **Merged over**, not assigned: the node's own `theme_override` has already put the
6815    // target style and the four unanimated answers (alignment, wrapping, overflow, line
6816    // count) in place, and this replaces field by field exactly what is moving.
6817    theme.widgets.text.style = theme.widgets.text.style.merge(style);
6818    Some(Box::new(theme))
6819}
6820
6821/// Builds what `widget` stands for ([`Widget::expand`]) **under the scopes the node says it
6822/// introduces before it is built** — the surface, the theme, the shell and the scrolling a
6823/// [`MediaScope`](crate::MediaScope) or a [`Themed`](crate::Themed) around a component
6824/// imposes (milestone 642).
6825///
6826/// A wrapper and the component inside it are one node, and the walks ask a node for its
6827/// scopes after building it, since what a component builds may introduce one. That left
6828/// the wrapper's own scope out of the build: `MediaScope::tweak(.., page)` built `page` for
6829/// the whole window, and a scaffold in it read a size it was not laid out in. An unbuilt
6830/// component answers for no scope, so what is asked here is exactly the wrapper's; the walk
6831/// asks again once the component is built, and gets the whole chain.
6832///
6833/// Out of line, so that the walks' recursive frames do not carry what is held here.
6834#[inline(never)]
6835fn expand_scoped<Msg>(widget: &dyn Widget<Msg>, id: WidgetId, runtime: &Runtime, theme: &Theme) {
6836    let _surface = widget
6837        .media_override(crate::MediaQuery::of())
6838        .map(crate::MediaQuery::install);
6839    let _shell = widget.scaffold_override().map(crate::ScaffoldInfo::install);
6840    let _scrolling = widget
6841        .scroll_behavior_override()
6842        .map(crate::ScrollConfiguration::install);
6843    let scoped = scoped_theme(widget, id, runtime, theme);
6844    widget.expand(id, runtime, scoped.as_deref().unwrap_or(theme));
6845}
6846
6847/// Runs the **deferred builds** over a tree, the way the layout pass does on its way down.
6848///
6849/// A [`ThemeBuilder`](crate::ThemeBuilder) — and everything built on one, an
6850/// [`AppBar`](crate::AppBar) included — has no children at all until something calls
6851/// [`Widget::build_themed`] on it. The layout pass does that first thing, so within a frame
6852/// nothing ever meets an unbuilt subtree.
6853///
6854/// **A tree built outside a frame does.** The shell rebuilds from `view` alone on a burst of
6855/// keystrokes, faster than a frame, so the next key sees the current value rather than the
6856/// retained tree's — and then reads that tree straight away. Every traversal that arrives
6857/// there ([`find_widget`], focus resolution, a caret being revealed) walks a tree whose
6858/// deferred subtrees are empty, and silently finds nothing.
6859///
6860/// This is that half of the walk and nothing else: the theme swap
6861/// ([`Widget::theme_override`]) then the build, top-down. The swap is not optional — a
6862/// builder inside a [`Themed`](crate::Themed) has to see its own subtree's theme, and a
6863/// preparation that is *nearly* the walk is worse than none, because what it builds is wrong
6864/// rather than absent.
6865///
6866/// **Which is why it takes a runtime.** A subtree under an animated text style
6867/// (`AnimatedDefaultTextStyle`) inherits a theme that is moving, and a builder inside one
6868/// composes against whatever theme reaches it *first* — this walk. Preparing it against
6869/// the target instead of the value in flight is the same failure as skipping the swap: what
6870/// it builds is wrong rather than absent, and only for as long as the movement lasts, which
6871/// is the hardest kind to see. The runtime is one frame behind here, because this runs
6872/// before the frame advances its animations, and one frame behind is the whole tree agreeing
6873/// with itself rather than a builder disagreeing with the layout around it.
6874pub fn build_deferred<Msg>(root: &dyn Widget<Msg>, theme: &Theme, runtime: &Runtime) {
6875    fn walk<Msg>(widget: &dyn Widget<Msg>, id: WidgetId, runtime: &Runtime, theme: &Theme) {
6876        expand_scoped(widget, id, runtime, theme);
6877        let scoped = scoped_theme(widget, id, runtime, theme);
6878        let theme = scoped.as_deref().unwrap_or(theme);
6879        let _surface = widget
6880            .media_override(crate::MediaQuery::of())
6881            .map(crate::MediaQuery::install);
6882        let _shell = widget.scaffold_override().map(crate::ScaffoldInfo::install);
6883        let _scrolling = widget
6884            .scroll_behavior_override()
6885            .map(crate::ScrollConfiguration::install);
6886        widget.build_in(id, runtime, theme);
6887        for (index, child) in widget.children().iter().enumerate() {
6888            walk(
6889                child.as_ref(),
6890                child_id(id, index, child.as_ref()),
6891                runtime,
6892                theme,
6893            );
6894        }
6895    }
6896    walk(root, WidgetId::ROOT, runtime, theme);
6897}
6898
6899/// The widget of the tree under `root` that has the identity `target`, or `None` if none
6900/// does. Identities are the ones the walk that built the frame gave, so an id read from a
6901/// [`Ui`] finds its widget here.
6902pub fn find_widget<Msg>(root: &dyn Widget<Msg>, target: WidgetId) -> Option<&dyn Widget<Msg>> {
6903    fn walk<Msg>(
6904        widget: &dyn Widget<Msg>,
6905        id: WidgetId,
6906        target: WidgetId,
6907    ) -> Option<&dyn Widget<Msg>> {
6908        if id == target {
6909            return Some(widget);
6910        }
6911        for (index, child) in widget.children().iter().enumerate() {
6912            if let Some(found) = walk(child.as_ref(), child_id(id, index, child.as_ref()), target) {
6913                return Some(found);
6914            }
6915        }
6916        None
6917    }
6918    walk(root, WidgetId::ROOT, target)
6919}
6920
6921/// Folds a filter into the single filtered layer its subtree turned out to be, so two
6922/// filter widgets wrapped one inside the other cost one layer rather than two.
6923///
6924/// It is not only a saving. Compositing renders a layer into a texture and composites
6925/// that texture; a layer *inside* that texture is not composited again, so a nested
6926/// filter would simply not be applied. Folding is what makes `ColorFiltered` around
6927/// `ImageFiltered` mean what it reads as.
6928///
6929/// Everything about the inner layer must be neutral — full opacity, a plain
6930/// rectangular clip, no transform — because those are applied at compositing time and
6931/// there is only one composite to apply them at. And the two filters must not want
6932/// the same slot: greyscale of an inverted picture is not the inversion of a
6933/// greyscale one, so there is no single layer that means both, and the two nest.
6934fn fold_filter(
6935    group: Vec<Primitive>,
6936    filter: LayerFilter,
6937    clip: Rect,
6938) -> (Vec<Primitive>, LayerFilter, Rect) {
6939    if group.len() != 1 {
6940        return (group, filter, clip);
6941    }
6942    let Primitive::Layer {
6943        opacity,
6944        clip: inner_clip,
6945        clip_shape,
6946        transform,
6947        filter: inner,
6948        ..
6949    } = &group[0]
6950    else {
6951        return (group, filter, clip);
6952    };
6953    if *opacity < 0.999 || !matches!(clip_shape, ClipShape::Rect) || transform.is_some() {
6954        return (group, filter, clip);
6955    }
6956    let (inner, inner_clip) = (*inner, *inner_clip);
6957    let Some(merged) = filter.merge(inner) else {
6958        return (group, filter, clip);
6959    };
6960    let Some(Primitive::Layer { primitives, .. }) = group.into_iter().next() else {
6961        unreachable!("checked just above")
6962    };
6963    (primitives, merged, clip.intersect(inner_clip))
6964}
6965
6966#[cfg(test)]
6967mod tests {
6968    use super::*;
6969    use crate::physics::Scrollbars;
6970    use crate::runtime::Edit;
6971    use crate::{
6972        Button, Container, Flex, Key, Keyed, OverlayPortal, Placement, SingleChildScrollView,
6973        TextField,
6974    };
6975    use frus_core::{Color, Point, Primitive, Rect, Size};
6976
6977    /// **A deep tree on the smallest stack any platform gives a frame.**
6978    ///
6979    /// Building a tree recurses twice over it — the layout walk and the scene walk — and
6980    /// what each level costs is a property of those two functions, not of the widgets.
6981    /// Two `Theme` copies in each frame made that cost 53 kilobytes a level, so a tree
6982    /// forty deep exhausted the megabyte a Windows main thread gets and the process died
6983    /// with no panic and no message. A real screen nests further than forty.
6984    ///
6985    /// One megabyte and 64 levels is what this holds to: the platform's smallest stack,
6986    /// against a tree deeper than a screen builds and half again the depth that used to
6987    /// die. It is deliberately not the deepest that fits — around 128 now, at about seven
6988    /// kilobytes a level — because a tripwire wants margin over the number it guards. An
6989    /// overflow aborts rather than fails, so this one kills the run outright, which is the
6990    /// right noise for what it watches.
6991    #[test]
6992    fn a_deep_tree_builds_on_a_platform_s_smallest_stack() {
6993        let built = std::thread::Builder::new()
6994            .stack_size(1024 * 1024)
6995            .spawn(|| {
6996                let mut node: Box<dyn Widget<Msg>> = Box::new(crate::Text::new("the bottom"));
6997                for _ in 0..64 {
6998                    node = Box::new(Container::new().padding(1.0).child(node));
6999                }
7000                let ui = build_ui(
7001                    node.as_ref(),
7002                    Size::new(800.0, 600.0),
7003                    &Runtime::default(),
7004                    &Theme::default(),
7005                );
7006                ui.scene().primitives().len()
7007            })
7008            .expect("spawning the deep-tree thread")
7009            .join()
7010            .expect("the deep tree was built");
7011        assert!(built > 0, "and it painted something");
7012    }
7013
7014    #[derive(Clone, Debug, PartialEq)]
7015    enum Msg {
7016        A,
7017        B,
7018        C,
7019        D,
7020        Edited(String),
7021    }
7022
7023    #[test]
7024    fn semantics_tree_carries_roles_and_labels() {
7025        use crate::{Button, Checkbox, Role, Text};
7026        let rt = Runtime::default();
7027        let tree = crate::Flex::column()
7028            .child(Text::new("Titre"))
7029            .child(Button::new("Submit").on_press(Msg::A))
7030            .child(Checkbox::new(true).on_toggle(|_| Msg::B));
7031        let ui = build_ui(&tree, Size::new(300.0, 200.0), &rt, &Theme::default());
7032        let sem = ui.semantics();
7033        // One node per meaningful widget (the containing Flex is skipped).
7034        let roles: Vec<Role> = sem.iter().map(|(_, _, s)| s.role).collect();
7035        assert!(roles.contains(&Role::Label));
7036        assert!(roles.contains(&Role::Button));
7037        assert!(roles.contains(&Role::CheckBox));
7038        // The button carries its label and is actionable.
7039        let button = sem.iter().find(|(_, _, s)| s.role == Role::Button).unwrap();
7040        assert_eq!(button.2.label.as_deref(), Some("Submit"));
7041        assert!(button.2.clickable);
7042        // The checked box mirrors its state.
7043        let check = sem
7044            .iter()
7045            .find(|(_, _, s)| s.role == Role::CheckBox)
7046            .unwrap();
7047        assert_eq!(check.2.toggled, crate::Toggled::True);
7048    }
7049
7050    /// **A tappable area is something to press** (milestone 700): a `Container` with
7051    /// `on_click` gets a button node named by the words inside it, which leave the tree so
7052    /// they are not read twice, and an assistive technology's "activate" reaches its message.
7053    #[test]
7054    fn a_tappable_area_is_a_button_named_by_its_words() {
7055        use crate::{Role, Text};
7056        let rt = Runtime::default();
7057        let tree = Flex::<Msg>::column().child(Text::new("Plain")).child(
7058            Container::<Msg>::new()
7059                .on_click(Msg::A)
7060                .padding(8.0)
7061                .child(Text::new("Attach…")),
7062        );
7063        let ui = build_ui(&tree, Size::new(300.0, 200.0), &rt, &Theme::default());
7064        let sem = ui.semantics();
7065        let found: Vec<(Role, Option<&str>, bool)> = sem
7066            .iter()
7067            .map(|(_, _, s)| (s.role, s.label.as_deref(), s.clickable))
7068            .collect();
7069        assert_eq!(
7070            found,
7071            [
7072                (Role::Label, Some("Plain"), false),
7073                (Role::Button, Some("Attach…"), true),
7074            ]
7075        );
7076        let (id, rect, _) = sem[1];
7077        assert!(rect.width > 0.0 && rect.height > 0.0);
7078        assert_eq!(ui.msg_for(id), Some(Msg::A), "activation reaches the area");
7079    }
7080
7081    /// **What can be operated on its own keeps its node** inside a tappable area, after the
7082    /// area's; several words are joined one per line.
7083    #[test]
7084    fn a_tappable_area_keeps_its_controls() {
7085        use crate::{Checkbox, Role, Text};
7086        let rt = Runtime::default();
7087        let tree = Container::<Msg>::new().on_click(Msg::A).child(
7088            Flex::<Msg>::column()
7089                .child(Text::new("Notifications"))
7090                .child(Text::new("Daily summary"))
7091                .child(Checkbox::new(true).on_toggle(|_| Msg::B)),
7092        );
7093        let ui = build_ui(&tree, Size::new(300.0, 200.0), &rt, &Theme::default());
7094        let found: Vec<(Role, Option<&str>)> = ui
7095            .semantics()
7096            .iter()
7097            .map(|(_, _, s)| (s.role, s.label.as_deref()))
7098            .collect();
7099        assert_eq!(
7100            found[0],
7101            (Role::Button, Some("Notifications\nDaily summary"))
7102        );
7103        assert_eq!(found[1].0, Role::CheckBox);
7104        assert_eq!(found.len(), 2);
7105    }
7106
7107    /// **A wordless area around one control adds nothing**: the control already answers for
7108    /// the press, and nested areas each name themselves from their own words.
7109    #[test]
7110    fn a_wordless_area_around_one_control_adds_nothing() {
7111        use crate::{Button, Role, Text};
7112        let rt = Runtime::default();
7113        let around_button = Container::<Msg>::new()
7114            .on_click(Msg::A)
7115            .child(Button::new("Submit").on_press(Msg::B));
7116        let ui = build_ui(
7117            &around_button,
7118            Size::new(300.0, 200.0),
7119            &rt,
7120            &Theme::default(),
7121        );
7122        let roles: Vec<(Role, Option<&str>)> = ui
7123            .semantics()
7124            .iter()
7125            .map(|(_, _, s)| (s.role, s.label.as_deref()))
7126            .collect();
7127        assert_eq!(roles, [(Role::Button, Some("Submit"))]);
7128
7129        let nested = Container::<Msg>::new().on_click(Msg::A).child(
7130            Flex::<Msg>::column().child(Text::new("Card")).child(
7131                Container::<Msg>::new()
7132                    .on_click(Msg::B)
7133                    .child(Text::new("Open")),
7134            ),
7135        );
7136        let ui = build_ui(&nested, Size::new(300.0, 200.0), &rt, &Theme::default());
7137        let found: Vec<(Option<&str>, Option<Msg>)> = ui
7138            .semantics()
7139            .iter()
7140            .map(|(id, _, s)| (s.label.as_deref(), ui.msg_for(*id)))
7141            .collect();
7142        assert_eq!(
7143            found,
7144            [(Some("Card"), Some(Msg::A)), (Some("Open"), Some(Msg::B))]
7145        );
7146    }
7147
7148    /// **A button holding a widget is named by the words in it**, in its own entry: one node,
7149    /// the button's role, the text's words — where it used to be an unnamed button beside a
7150    /// separate text. A named button keeps its name.
7151    #[test]
7152    fn a_button_holding_a_text_is_named_by_it() {
7153        use crate::{Button, Role, Text};
7154        let rt = Runtime::default();
7155        let tree = Flex::<Msg>::column()
7156            .child(Button::with_child(Text::new("Save")).on_press(Msg::A))
7157            .child(Button::new("Submit").on_press(Msg::B));
7158        let ui = build_ui(&tree, Size::new(300.0, 200.0), &rt, &Theme::default());
7159        let found: Vec<(Role, Option<&str>, Option<Msg>)> = ui
7160            .semantics()
7161            .iter()
7162            .map(|(id, _, s)| (s.role, s.label.as_deref(), ui.msg_for(*id)))
7163            .collect();
7164        assert_eq!(
7165            found,
7166            [
7167                (Role::Button, Some("Save"), Some(Msg::A)),
7168                (Role::Button, Some("Submit"), Some(Msg::B)),
7169            ]
7170        );
7171    }
7172
7173    /// **Something clickable inside an area keeps its entry**, whatever its role: only what
7174    /// merely says something is taken into the area's name.
7175    #[test]
7176    fn a_clickable_entry_inside_an_area_is_not_taken_as_words() {
7177        use crate::{Role, Semantics, Text};
7178        let rt = Runtime::default();
7179        let inner = Semantics::new(
7180            frus_core::SemanticsProperties::default(),
7181            Container::<Msg>::new().width(20.0).height(20.0),
7182        )
7183        .label("More")
7184        .clickable(true);
7185        let tree = Container::<Msg>::new()
7186            .on_click(Msg::A)
7187            .child(Flex::<Msg>::column().child(Text::new("Card")).child(inner));
7188        let ui = build_ui(&tree, Size::new(300.0, 200.0), &rt, &Theme::default());
7189        let found: Vec<(Role, Option<&str>)> = ui
7190            .semantics()
7191            .iter()
7192            .map(|(_, _, s)| (s.role, s.label.as_deref()))
7193            .collect();
7194        assert_eq!(
7195            found,
7196            [(Role::Button, Some("Card")), (Role::None, Some("More"))]
7197        );
7198    }
7199
7200    fn clickable_sample() -> Flex<Msg> {
7201        Flex::row()
7202            .width(400.0)
7203            .height(100.0)
7204            .padding(10.0)
7205            .gap(8.0)
7206            .child(
7207                Container::new()
7208                    .width(120.0)
7209                    .color(Color::rgb(1.0, 0.0, 0.0))
7210                    .hover_color(Color::rgb(0.0, 1.0, 0.0))
7211                    .on_click(Msg::A),
7212            )
7213            .child(
7214                Container::new()
7215                    .flex(1.0)
7216                    .color(Color::rgb(0.0, 0.0, 1.0))
7217                    .on_click(Msg::B),
7218            )
7219    }
7220
7221    /// A box whose children ran past it is **marked on the screen**, over a tenth of it,
7222    /// on the edge they ran past — and a box whose children fit is left alone.
7223    #[test]
7224    fn an_overflow_paints_a_band_on_the_edge_it_ran_past() {
7225        use crate::{Container, Flex};
7226        // Two children, because a row with one is not dividing a line up — it hands its
7227        // constraints down, and a lone child is bounded by them rather than overflowing.
7228        let row = |child_width: f32| {
7229            let root: Flex<()> = Flex::row()
7230                .width(100.0)
7231                .height(40.0)
7232                .child(
7233                    Container::new()
7234                        .width(child_width)
7235                        .height(20.0)
7236                        .color(Color::WHITE),
7237                )
7238                .child(Container::new().width(20.0).height(20.0));
7239            let rt = crate::runtime::Runtime::default();
7240            build_ui(
7241                &root,
7242                Size::new(200.0, 100.0),
7243                &rt,
7244                &crate::theme::Theme::dark(),
7245            )
7246        };
7247        // The stripes are paths; nothing else in this tree draws one.
7248        let stripes = |ui: &Ui<()>| {
7249            ui.scene()
7250                .primitives()
7251                .iter()
7252                .filter(|p| matches!(p, Primitive::Path { .. }))
7253                .count()
7254        };
7255        let over = row(300.0);
7256        assert!(!over.overflows().is_empty(), "the fixture has to overflow");
7257        assert!(stripes(&over) > 2, "striped: {}", stripes(&over));
7258        // The band is on the right-hand tenth: 90..100 of a 100-wide row.
7259        let band = over
7260            .scene()
7261            .primitives()
7262            .iter()
7263            .rev()
7264            .find_map(|p| match p {
7265                Primitive::Rect { rect, color, .. } if color.a > 0.7 && color.r == 0.0 => {
7266                    Some(*rect)
7267                }
7268                _ => None,
7269            })
7270            .expect("the band under the stripes");
7271        assert!((band.x - 90.0).abs() < 0.5, "at the right edge: {band:?}");
7272        assert!((band.width - 10.0).abs() < 0.5, "a tenth of it: {band:?}");
7273
7274        let fits = row(40.0);
7275        assert!(fits.overflows().is_empty(), "the fixture has to fit");
7276        assert!(fits.overflows().is_empty());
7277        assert_eq!(stripes(&fits), 0, "nothing to mark");
7278    }
7279
7280    /// The band says **which edge and by how much**, in the reference's words. A striped
7281    /// edge tells you something is too big; the sentence tells you what to look for, and
7282    /// it is the only half of the report that survives being photographed off a device.
7283    #[test]
7284    fn a_band_writes_which_edge_overflowed_and_by_how_much() {
7285        use crate::{Container, Flex};
7286        let root: Flex<()> = Flex::row()
7287            .width(100.0)
7288            .height(40.0)
7289            .child(
7290                Container::new()
7291                    .width(300.0)
7292                    .height(20.0)
7293                    .color(Color::WHITE),
7294            )
7295            .child(Container::new().width(20.0).height(20.0));
7296        let ui = build_ui(
7297            &root,
7298            Size::new(200.0, 100.0),
7299            &Runtime::default(),
7300            &Theme::dark(),
7301        );
7302        // The label is inside the rotated layer, which is why the search is recursive.
7303        fn labels(primitives: &[Primitive], out: &mut Vec<String>) {
7304            for p in primitives {
7305                match p {
7306                    Primitive::Text { text, .. } => out.push(text.clone()),
7307                    Primitive::Layer { primitives, .. } => labels(primitives, out),
7308                    _ => {}
7309                }
7310            }
7311        }
7312        let mut found = Vec::new();
7313        labels(ui.scene().primitives(), &mut found);
7314        assert!(
7315            found.iter().any(|t| t == "RIGHT OVERFLOWED BY 220 PIXELS"),
7316            "the sentence, in the reference's words: {found:?}"
7317        );
7318    }
7319
7320    /// The reference's precision rule: whole pixels when there are many, a decimal when
7321    /// there are few, three significant figures when there is almost nothing — which is
7322    /// the difference between a layout bug and a rounding error.
7323    #[test]
7324    fn an_overflow_is_written_to_the_precision_it_deserves() {
7325        assert_eq!(format_pixels(86.0), "86");
7326        // Past ten pixels the decimal is noise; at ten exactly it is not yet.
7327        assert_eq!(format_pixels(10.4), "10");
7328        assert_eq!(format_pixels(10.0), "10.0");
7329        assert_eq!(format_pixels(4.5), "4.5");
7330        assert_eq!(format_pixels(1.0), "1.00");
7331        assert_eq!(format_pixels(0.5), "0.500");
7332        assert_eq!(format_pixels(0.0123), "0.0123");
7333    }
7334
7335    #[test]
7336    fn rtl_mirrors_row_horizontally() {
7337        let size = Size::new(400.0, 100.0);
7338        // A = a fixed 120 px button (on the left in LTR), B = the flexible remainder.
7339        let ltr = build_ui(
7340            &clickable_sample(),
7341            size,
7342            &Runtime::default(),
7343            &Theme::default(),
7344        );
7345        let rtl = build_ui(
7346            &clickable_sample(),
7347            size,
7348            &Runtime::default(),
7349            &Theme::default().rtl(),
7350        );
7351        let hit = |ui: &Ui<Msg>, x: f32| ui.hit(Point::new(x, 50.0)).and_then(|id| ui.msg_for(id));
7352
7353        // LTR: button A takes the left edge.
7354        assert_eq!(hit(&ltr, 40.0), Some(Msg::A));
7355        assert_eq!(hit(&ltr, 360.0), Some(Msg::B));
7356        // RTL: everything is mirrored — A moves to the right, B takes the left.
7357        assert_eq!(hit(&rtl, 360.0), Some(Msg::A), "A on the right in RTL");
7358        assert_eq!(hit(&rtl, 40.0), Some(Msg::B), "B on the left in RTL");
7359    }
7360
7361    #[test]
7362    fn wrapped_text_wraps_in_layout_and_invalidates_the_cache() {
7363        let tree = |text: &str| {
7364            crate::Flex::column()
7365                .width(120.0)
7366                .child(crate::Text::new(text).wrap())
7367                .child(Container::new().height(10.0).on_click(Msg::A))
7368        };
7369        // Position of the clickable follower: the first y hit while sweeping.
7370        let follower_y = |ui: &Ui<Msg>| {
7371            (0..600)
7372                .map(|y| y as f32)
7373                .find(|&y| ui.hit(Point::new(60.0, y)).is_some())
7374                .expect("suiveur cliquable")
7375        };
7376
7377        let rt = Runtime::default();
7378        let long = "a rather long paragraph that will wrap onto several lines";
7379        let ui = build_ui(&tree(long), Size::new(120.0, 600.0), &rt, &Theme::default());
7380
7381        // The paragraph's render carries its wrap width (≤ the column).
7382        let max_w = ui
7383            .scene()
7384            .primitives()
7385            .iter()
7386            .find_map(|p| match p {
7387                Primitive::Text { max_width, .. } => *max_width,
7388                _ => None,
7389            })
7390            .expect("a wrapped paragraph");
7391        assert!(max_w <= 120.5, "wrapped to the column width: {max_w}");
7392
7393        // The wrapped text takes several lines: the follower is pushed down.
7394        let y_long = follower_y(&ui);
7395        assert!(
7396            y_long > 30.0,
7397            "the follower is pushed down by the wrap: {y_long}"
7398        );
7399
7400        // The SAME structure/styles, different content, the SAME runtime (a warm cache): the
7401        // measure key must invalidate the cache — otherwise the rects would be stale.
7402        let ui2 = build_ui(
7403            &tree("short"),
7404            Size::new(120.0, 600.0),
7405            &rt,
7406            &Theme::default(),
7407        );
7408        let y_short = follower_y(&ui2);
7409        assert!(
7410            y_short < y_long,
7411            "shorter content → a higher follower (the cache was invalidated): {y_short} vs {y_long}"
7412        );
7413    }
7414
7415    #[test]
7416    fn relayout_cache_reuses_the_root_layout_across_frames() {
7417        let rt = Runtime::default();
7418        let size = Size::new(400.0, 100.0);
7419        // Frame 1: nothing cached → a recomputation (at least the root).
7420        let _ = build_ui(&clickable_sample(), size, &rt, &Theme::default());
7421        let (hits1, misses1) = rt.layout_cache.borrow().last_frame_stats();
7422        assert_eq!(hits1, 0, "1st frame: nothing reused");
7423        assert!(misses1 >= 1, "1st frame: at least one computation");
7424
7425        // Frame 2: the same tree, the same constraints → the root is reused.
7426        let _ = build_ui(&clickable_sample(), size, &rt, &Theme::default());
7427        let (hits2, misses2) = rt.layout_cache.borrow().last_frame_stats();
7428        assert_eq!(hits2, 1, "2nd frame: the root is reused");
7429        assert_eq!(misses2, 0, "2nd frame: nothing recomputed");
7430
7431        // Frame 3: the window is resized → the constraints change → a recomputation.
7432        let _ = build_ui(
7433            &clickable_sample(),
7434            Size::new(500.0, 100.0),
7435            &rt,
7436            &Theme::default(),
7437        );
7438        let (hits3, misses3) = rt.layout_cache.borrow().last_frame_stats();
7439        assert_eq!((hits3, misses3), (0, 1), "redimensionnement → recalcul");
7440    }
7441
7442    #[test]
7443    fn modal_traps_tab_arrows_and_pointer_focus() {
7444        use crate::portal::{OverlayPortal, Placement};
7445        // The background: two focusable buttons; the open modal: two buttons as well.
7446        let dialog = Flex::<Msg>::row()
7447            .child(Button::new("ok").on_press(Msg::C))
7448            .child(Button::new("no").on_press(Msg::D));
7449        let tree: Flex<Msg> = Flex::column()
7450            .child(Button::new("bg1").on_press(Msg::A))
7451            .child(Button::new("bg2").on_press(Msg::B))
7452            .child(
7453                OverlayPortal::new(Container::new())
7454                    .overlay(dialog, Placement::Center)
7455                    .dismiss(Msg::A),
7456            );
7457        let ui = build_ui(
7458            &tree,
7459            Size::new(400.0, 300.0),
7460            &Runtime::default(),
7461            &Theme::default(),
7462        );
7463
7464        // Tab enters the trap (the modal's first focusable) and cycles inside it.
7465        let first = ui.focus_next(None, true).expect("the scope's first");
7466        assert_eq!(ui.msg_for(first), Some(Msg::C));
7467        let second = ui.focus_next(Some(first), true).expect("suivant");
7468        assert_eq!(ui.msg_for(second), Some(Msg::D));
7469        let wrapped = ui.focus_next(Some(second), true).expect("boucle");
7470        assert_eq!(
7471            ui.msg_for(wrapped),
7472            Some(Msg::C),
7473            "Tab cycles inside the modal"
7474        );
7475
7476        // The arrows stay inside the scope: there is nothing above the dialog.
7477        assert_eq!(ui.focus_directional(first, FocusDirection::Up), None);
7478        let right = ui
7479            .focus_directional(first, FocusDirection::Right)
7480            .expect("to the right");
7481        assert_eq!(ui.msg_for(right), Some(Msg::D));
7482
7483        // Click-to-focus ignores the background (the bg1 button is at the top left).
7484        assert_eq!(
7485            ui.focus_hit(Point::new(10.0, 10.0)),
7486            None,
7487            "fond hors scope"
7488        );
7489
7490        // Without a modal: no trap, Tab starts at the background.
7491        let open_less: Flex<Msg> = Flex::column()
7492            .child(Button::new("bg1").on_press(Msg::A))
7493            .child(Button::new("bg2").on_press(Msg::B));
7494        let ui2 = build_ui(
7495            &open_less,
7496            Size::new(400.0, 300.0),
7497            &Runtime::default(),
7498            &Theme::default(),
7499        );
7500        let f = ui2.focus_next(None, true).expect("premier");
7501        assert_eq!(ui2.msg_for(f), Some(Msg::A));
7502    }
7503
7504    #[test]
7505    fn open_menu_traps_focus_in_its_items() {
7506        use crate::PopupMenuButton;
7507        // A focusable background plus an **open** menu (anchor + two items).
7508        let menu = PopupMenuButton::new(Button::new("open").on_press(Msg::A), true, Msg::D)
7509            .item("one", Msg::B)
7510            .item("two", Msg::C);
7511        let tree: Flex<Msg> = Flex::column()
7512            .child(Button::new("bg").on_press(Msg::A))
7513            .child(menu);
7514        let ui = build_ui(
7515            &tree,
7516            Size::new(400.0, 300.0),
7517            &Runtime::default(),
7518            &Theme::default(),
7519        );
7520
7521        // Tab enters the menu's items and **cycles** inside (background and anchor are out of scope).
7522        let first = ui.focus_next(None, true).expect("the scope's first");
7523        assert_eq!(ui.msg_for(first), Some(Msg::B));
7524        let second = ui.focus_next(Some(first), true).expect("suivant");
7525        assert_eq!(ui.msg_for(second), Some(Msg::C));
7526        let wrapped = ui.focus_next(Some(second), true).expect("boucle");
7527        assert_eq!(
7528            ui.msg_for(wrapped),
7529            Some(Msg::B),
7530            "Tab cycles inside the open menu"
7531        );
7532        // The background (top left) is out of scope while the menu is open.
7533        assert_eq!(
7534            ui.focus_hit(Point::new(10.0, 10.0)),
7535            None,
7536            "fond hors scope"
7537        );
7538
7539        // The menu **closed**: no trap, Tab starts at the background.
7540        let closed: Flex<Msg> = Flex::column()
7541            .child(Button::new("bg").on_press(Msg::A))
7542            .child(PopupMenuButton::new(
7543                Button::new("open").on_press(Msg::B),
7544                false,
7545                Msg::D,
7546            ));
7547        let ui2 = build_ui(
7548            &closed,
7549            Size::new(400.0, 300.0),
7550            &Runtime::default(),
7551            &Theme::default(),
7552        );
7553        let f = ui2.focus_next(None, true).expect("premier");
7554        assert_eq!(ui2.msg_for(f), Some(Msg::A), "with no open menu, no trap");
7555    }
7556
7557    #[test]
7558    fn escape_infrastructure_finds_path_and_topmost_dismiss() {
7559        use crate::portal::{OverlayPortal, Placement};
7560        // An open portal (a modal with a dismissal) around a clickable anchor.
7561        let anchor = Container::<Msg>::new()
7562            .width(50.0)
7563            .height(30.0)
7564            .on_click(Msg::A);
7565        let content = Container::<Msg>::new()
7566            .width(80.0)
7567            .height(40.0)
7568            .on_click(Msg::B);
7569        let portal = OverlayPortal::new(anchor)
7570            .overlay(content, Placement::Center)
7571            .dismiss(Msg::C);
7572        let tree: Flex<Msg> = Flex::column().child(portal);
7573
7574        let ui = build_ui(
7575            &tree,
7576            Size::new(300.0, 200.0),
7577            &Runtime::default(),
7578            &Theme::default(),
7579        );
7580        // The topmost dismissal is the portal's.
7581        assert_eq!(ui.top_dismiss(), Some(Msg::C));
7582
7583        // Focus "inside the dialog": the root→content path goes through the portal, which
7584        // consumes Escape while bubbling. (The Center content, 80×40, is centred in 300×200 →
7585        // its centre is at (150, 100).)
7586        let inner = ui
7587            .hit(Point::new(150.0, 100.0))
7588            .expect("contenu de la modale");
7589        let path = find_path(&tree, inner);
7590        assert!(path.len() >= 3, "root, portal, content: {}", path.len());
7591        assert_eq!(
7592            path.last().unwrap().on_click(),
7593            Some(Msg::B),
7594            "the target closes the path"
7595        );
7596        let consumed = path
7597            .iter()
7598            .rev()
7599            .find_map(|w| match w.on_key(&crate::Key::Escape) {
7600                crate::KeyResponse::Handled(msg) => Some(msg),
7601                _ => None,
7602            });
7603        assert_eq!(
7604            consumed,
7605            Some(Some(Msg::C)),
7606            "the portal consumes Escape while bubbling"
7607        );
7608
7609        // Chemin introuvable → vide ; pas d'overlay → pas de fermeture.
7610        assert!(find_path(&tree, WidgetId::ROOT.child(99)).is_empty());
7611        let closed: Flex<Msg> = Flex::column().child(Container::new().on_click(Msg::A));
7612        let ui2 = build_ui(
7613            &closed,
7614            Size::new(300.0, 200.0),
7615            &Runtime::default(),
7616            &Theme::default(),
7617        );
7618        assert_eq!(ui2.top_dismiss(), None);
7619    }
7620
7621    #[test]
7622    fn long_press_targets_are_collected_topmost_first() {
7623        // A long-press container holding a long-press child: a point inside the child returns
7624        // the child's message (the topmost one).
7625        let tree: Container<Msg> = Container::new()
7626            .width(200.0)
7627            .height(100.0)
7628            .on_long_press(Msg::A)
7629            .child(
7630                Container::new()
7631                    .width(50.0)
7632                    .height(50.0)
7633                    .on_long_press(Msg::B),
7634            );
7635        let ui = build_ui(
7636            &tree,
7637            Size::new(200.0, 100.0),
7638            &Runtime::default(),
7639            &Theme::default(),
7640        );
7641        assert_eq!(ui.long_press_at(Point::new(25.0, 25.0)), Some(Msg::B));
7642        assert_eq!(ui.long_press_at(Point::new(150.0, 80.0)), Some(Msg::A));
7643        assert_eq!(ui.long_press_at(Point::new(500.0, 500.0)), None);
7644    }
7645
7646    #[test]
7647    fn hit_and_msg_for_route_correctly() {
7648        let rt = Runtime::default();
7649        let ui = build_ui(
7650            &clickable_sample(),
7651            Size::new(400.0, 100.0),
7652            &rt,
7653            &Theme::default(),
7654        );
7655        let id_a = ui.hit(Point::new(50.0, 50.0)).expect("A");
7656        let id_b = ui.hit(Point::new(300.0, 50.0)).expect("B");
7657        assert_ne!(id_a, id_b);
7658        assert_eq!(ui.msg_for(id_a), Some(Msg::A));
7659        assert_eq!(ui.msg_for(id_b), Some(Msg::B));
7660        assert_eq!(ui.hit(Point::new(3.0, 3.0)), None);
7661    }
7662
7663    /// Every rectangle a frame paints down the right-hand edge of a 200-wide viewport.
7664    /// The bar is drawn last, over the content, and it is the only thing over there.
7665    fn painted(ui: &Ui<()>) -> Vec<(Rect, Color)> {
7666        ui.scene()
7667            .primitives()
7668            .iter()
7669            .filter_map(|p| match p {
7670                Primitive::Rect { rect, color, .. }
7671                    if rect.x >= 200.0 - BAR_SIZE - BAR_MARGIN * 2.0 =>
7672                {
7673                    Some((*rect, *color))
7674                }
7675                _ => None,
7676            })
7677            .collect()
7678    }
7679
7680    /// A runtime, and a scroll behaviour in force for as long as the guard lives, that
7681    /// draws scrollbars — one that follows a desktop — or does not.
7682    fn bars_runtime(how: Scrollbars) -> (Runtime, crate::ScrollBehaviorGuard) {
7683        let behavior = match how {
7684            Scrollbars::Always => {
7685                crate::ScrollBehavior::new().with_platform(frus_core::TargetPlatform::Linux)
7686            }
7687            Scrollbars::Never => crate::ScrollBehavior::new().with_scrollbars(false),
7688        };
7689        (
7690            Runtime::default(),
7691            crate::ScrollConfiguration::install(behavior),
7692        )
7693    }
7694
7695    /// A tall page in a short window, its bar **pinned**: whether a bar fades is the
7696    /// business of the tests below, and these are about whether there is one to fade.
7697    fn bars(area: SingleChildScrollView<()>, how: Scrollbars) -> Vec<(Rect, Color)> {
7698        let (runtime, _bars) = bars_runtime(how);
7699        let ui = build_ui(
7700            &area.thumb_visibility(true),
7701            Size::new(200.0, 100.0),
7702            &runtime,
7703            &Theme::dark(),
7704        );
7705        painted(&ui)
7706    }
7707
7708    /// **Under an Android theme, the platform's own bar** (`scrollbar.dart:331`, milestone
7709    /// 617): 4 px instead of 8, flush with the edge instead of 2 px in, square instead of
7710    /// a pill, an opaque grey at rest instead of a fade of the content colour, and nothing
7711    /// a pointer can take hold of — whatever this test runs on.
7712    #[test]
7713    fn an_android_theme_draws_the_android_bar() {
7714        let (runtime, _bars) = bars_runtime(Scrollbars::Always);
7715        let laid = |platform: frus_core::TargetPlatform| {
7716            let ui = build_ui(
7717                &tall().thumb_visibility(true),
7718                Size::new(200.0, 100.0),
7719                &runtime,
7720                &Theme::dark().with_platform(platform),
7721            );
7722            let thumb = ui
7723                .scene()
7724                .primitives()
7725                .iter()
7726                .find_map(|p| match p {
7727                    Primitive::Rect {
7728                        rect,
7729                        color,
7730                        radius,
7731                        ..
7732                    } if rect.x >= 200.0 - BAR_SIZE - BAR_MARGIN * 2.0 => {
7733                        Some((*rect, *color, radius.top_left))
7734                    }
7735                    _ => None,
7736                })
7737                .expect("a thumb");
7738            let bar = ui.scrollbars()[0];
7739            let center = Point::new(
7740                bar.thumb.x + bar.thumb.width / 2.0,
7741                bar.thumb.y + bar.thumb.height / 2.0,
7742            );
7743            (
7744                thumb,
7745                bar.interactive,
7746                ui.scrollbar_at(center).is_some(),
7747                ui.scrollbar_near(center).is_some(),
7748            )
7749        };
7750        let ((rect, color, radius), interactive, grabbed, woken) =
7751            laid(frus_core::TargetPlatform::Android);
7752        assert_eq!(
7753            (rect.width, rect.x + rect.width),
7754            (4.0, 200.0),
7755            "4 px, flush"
7756        );
7757        assert_eq!(radius, 0.0, "square ends");
7758        assert_eq!(color, ANDROID_THUMB_ON_DARK, "an opaque grey at rest");
7759        assert!(
7760            !interactive && !grabbed && !woken,
7761            "nothing to take hold of"
7762        );
7763
7764        let ((rect, color, radius), interactive, grabbed, woken) =
7765            laid(frus_core::TargetPlatform::Linux);
7766        assert_eq!(
7767            (rect.width, rect.x + rect.width),
7768            (8.0, 198.0),
7769            "8 px, 2 px in"
7770        );
7771        assert_eq!(radius, 4.0, "a pill");
7772        assert!(color.a < 1.0, "a fade of the content colour");
7773        assert!(
7774            interactive && grabbed && woken,
7775            "a pointer takes hold of it"
7776        );
7777    }
7778
7779    /// **A theme's word outranks the platform's** on an Android theme too: a bar it makes
7780    /// interactive is one, and a colour it chose is the one at rest.
7781    #[test]
7782    fn an_android_bar_takes_the_theme_s_word() {
7783        let (runtime, _bars) = bars_runtime(Scrollbars::Always);
7784        let mut theme = Theme::dark().with_platform(frus_core::TargetPlatform::Android);
7785        theme.widgets.scrollbar.interactive = Some(true);
7786        theme.widgets.scrollbar.thumb_color = Some(Color::rgb(1.0, 0.0, 0.0));
7787        let ui = build_ui(
7788            &tall().thumb_visibility(true),
7789            Size::new(200.0, 100.0),
7790            &runtime,
7791            &theme,
7792        );
7793        let bar = ui.scrollbars()[0];
7794        assert!(bar.interactive);
7795        let color = painted(&ui)[0].1;
7796        assert_eq!((color.r, color.g, color.b), (1.0, 0.0, 0.0));
7797    }
7798
7799    /// A tall column in a short viewport: something to scroll.
7800    fn tall() -> SingleChildScrollView<()> {
7801        SingleChildScrollView::new()
7802            .width(200.0)
7803            .height(100.0)
7804            .child(Container::<()>::new().width(200.0).height(1000.0))
7805    }
7806
7807    /// **A touch screen gets no scrollbar** (`app.dart:870`).
7808    ///
7809    /// This drew one on every platform, on both axes, always — a permanent bar over the
7810    /// content of every scrolling page on a phone, where the reference draws nothing at
7811    /// all. A finger already knows where it is on the page.
7812    #[test]
7813    fn a_touch_screen_gets_no_scrollbar() {
7814        assert_eq!(bars(tall(), Scrollbars::Never), vec![]);
7815        assert_eq!(
7816            bars(tall(), Scrollbars::Always).len(),
7817            1,
7818            "and a desktop gets one"
7819        );
7820
7821        // Nothing to drag, either: a bar that is not drawn is not a target.
7822        let (runtime, _bars) = bars_runtime(Scrollbars::Never);
7823        let ui = build_ui(&tall(), Size::new(200.0, 100.0), &runtime, &Theme::dark());
7824        assert!(ui.scrollbars().is_empty());
7825    }
7826
7827    /// **A bar takes the scheme's word for the brightness, not the surface's luminance.**
7828    ///
7829    /// The opacities either side of this are three times apart at rest (`scrollbar.dart:242`,
7830    /// `:248`), so getting the question wrong is not a shade — it is a bar that reads as
7831    /// a smudge, or one that reads as a stripe.
7832    ///
7833    /// A **dimmed light** scheme is where the two answers part. The measurement it used
7834    /// to make says dark; the scheme says light, and the scheme is the one that knows.
7835    #[test]
7836    fn a_bar_reads_the_scheme_s_brightness_and_not_its_luminance() {
7837        use crate::media::Brightness;
7838        let dimmed = |brightness| {
7839            let mut theme = Theme::light();
7840            theme.scheme.surface = Color::rgb8(110, 110, 110);
7841            theme.scheme.brightness = brightness;
7842            theme
7843        };
7844        let thumb = |theme: &Theme| {
7845            let (runtime, _bars) = bars_runtime(Scrollbars::Always);
7846            let ui = build_ui(
7847                &tall().thumb_visibility(true),
7848                Size::new(200.0, 100.0),
7849                &runtime,
7850                theme,
7851            );
7852            painted(&ui)[0].1
7853        };
7854
7855        let light = dimmed(Brightness::Light);
7856        assert!(
7857            light.scheme.surface.compute_luminance() < 0.5,
7858            "the luminance the old line measured says dark"
7859        );
7860        assert_eq!(
7861            thumb(&light),
7862            light.scheme.on_surface.fade(THUMB_ON_LIGHT),
7863            "and the bar takes the scheme's word instead"
7864        );
7865
7866        let dark = dimmed(Brightness::Dark);
7867        assert_eq!(thumb(&dark), dark.scheme.on_surface.fade(THUMB_ON_DARK));
7868    }
7869
7870    /// **A theme reaches every number the bar is made of**, not only its colour. A
7871    /// scrollbar themed in its colour and sized from a constant is themed in the half
7872    /// that shows and unthemed in the half a pointer has to hit.
7873    ///
7874    /// There is no per-call override to outrank here — nobody configures a scrollbar at a
7875    /// call site, the walk draws it — so the chain is `theme ?? framework`, and the second
7876    /// half of this test is that the framework still answers where the theme says nothing.
7877    #[test]
7878    fn a_theme_reaches_every_number_the_scrollbar_is_made_of() {
7879        const THUMB: Color = Color::rgb(0.2, 0.7, 0.9);
7880        let mut theme = Theme::dark();
7881        theme.widgets.scrollbar.thickness = Some(14.0);
7882        theme.widgets.scrollbar.margin = Some(5.0);
7883        theme.widgets.scrollbar.min_thumb_length = Some(90.0);
7884        theme.widgets.scrollbar.radius = Some(2.0);
7885        theme.widgets.scrollbar.thumb_color = Some(THUMB);
7886        theme.widgets.scrollbar.opacity = Some(0.8);
7887
7888        let (runtime, _bars) = bars_runtime(Scrollbars::Always);
7889        let ui = build_ui(
7890            &tall().thumb_visibility(true),
7891            Size::new(200.0, 100.0),
7892            &runtime,
7893            &theme,
7894        );
7895        // `painted` filters on the strip a bar of the **default** thickness occupies, so
7896        // it cannot see a themed one — which is the point of the theme. This asks for the
7897        // strip the themed bar is in.
7898        let over_there: Vec<(Rect, Color)> = ui
7899            .scene()
7900            .primitives()
7901            .iter()
7902            .filter_map(|p| match p {
7903                Primitive::Rect { rect, color, .. } if rect.x >= 200.0 - 14.0 - 5.0 * 2.0 => {
7904                    Some((*rect, *color))
7905                }
7906                _ => None,
7907            })
7908            .collect();
7909        assert_eq!(over_there.len(), 1, "still one rectangle: the thumb");
7910        let (thumb, color) = over_there[0];
7911        assert!(
7912            (thumb.width - 14.0).abs() < 0.01,
7913            "the theme's thickness: {thumb:?}"
7914        );
7915        assert!(
7916            ((thumb.x + thumb.width) - (200.0 - 5.0)).abs() < 0.01,
7917            "held clear by the theme's margin, not shrunk to fit: {thumb:?}"
7918        );
7919        assert!(
7920            thumb.height >= 90.0,
7921            "and never shorter than the theme's floor: {thumb:?}"
7922        );
7923        assert_eq!(
7924            color,
7925            THUMB.fade(0.8),
7926            "the theme's colour, faded to the theme's resting level"
7927        );
7928
7929        // The radius is not in `painted`, which reads the rectangles' colours; take it
7930        // from the primitive itself. A pill is what an unthemed bar is, and this one is
7931        // not a pill.
7932        let radius = ui.scene().primitives().iter().find_map(|p| match p {
7933            Primitive::Rect { radius, .. } if radius.top_left > 0.0 => Some(radius.top_left),
7934            _ => None,
7935        });
7936        assert_eq!(radius, Some(2.0), "the theme's corner radius");
7937
7938        // And with a theme that says nothing, the framework still answers.
7939        let plain = bars(tall(), Scrollbars::Always);
7940        let (thumb, color) = plain[0];
7941        assert!((thumb.width - BAR_SIZE).abs() < 0.01);
7942        assert_eq!(color, Theme::dark().scheme.on_surface.fade(THUMB_ON_DARK));
7943    }
7944
7945    /// **And it is a thumb and nothing else** (`scrollbar.dart:281`): the reference's
7946    /// track is transparent unless a caller asks for one, and even then it is 3 to 5 %.
7947    /// This painted an 18 % stripe down every scrolling page, under a 55 % thumb.
7948    #[test]
7949    fn a_bar_is_a_thumb_and_no_track() {
7950        let painted = bars(tall(), Scrollbars::Always);
7951        assert_eq!(painted.len(), 1, "one rectangle: the thumb");
7952        let (thumb, color) = painted[0];
7953        assert!(
7954            (thumb.width - BAR_SIZE).abs() < 0.01,
7955            "at the reference's thickness: {thumb:?}"
7956        );
7957        assert!(
7958            ((thumb.x + thumb.width) - (200.0 - BAR_MARGIN)).abs() < 0.01,
7959            "and clear of the edge by `crossAxisMargin`, not shrunk to fit: {thumb:?}"
7960        );
7961        assert!(
7962            thumb.height >= MIN_THUMB,
7963            "and never shorter than a pointer can catch: {thumb:?}"
7964        );
7965        let theme = Theme::dark();
7966        assert_eq!(color, theme.scheme.on_surface.fade(THUMB_ON_DARK));
7967    }
7968
7969    /// **Along the horizontal axis, never** (`app.dart:861`) — on any platform, under any
7970    /// setting. A sideways strip is scrolled by whatever made it sideways, and a bar
7971    /// under it takes room from content that is short of it already.
7972    #[test]
7973    fn a_sideways_strip_never_gets_one() {
7974        let strip = SingleChildScrollView::new()
7975            .axis(crate::scroll::Axis::Horizontal)
7976            .width(200.0)
7977            .height(100.0)
7978            .child(Container::<()>::new().width(1000.0).height(100.0));
7979        let (runtime, _bars) = bars_runtime(Scrollbars::Always);
7980        let ui = build_ui(&strip, Size::new(200.0, 100.0), &runtime, &Theme::dark());
7981        assert!(
7982            ui.scrollbars().is_empty(),
7983            "a desktop draws none across, either"
7984        );
7985    }
7986
7987    /// And **one area may answer for itself**, over the application's answer — the
7988    /// per-area say the physics beside it already had.
7989    #[test]
7990    fn an_area_may_ask_for_its_own_answer() {
7991        assert_eq!(
7992            bars(tall().scrollbars(Scrollbars::Always), Scrollbars::Never).len(),
7993            1,
7994            "asked for one where the platform gives none"
7995        );
7996        assert_eq!(
7997            bars(tall().scrollbars(Scrollbars::Never), Scrollbars::Always),
7998            vec![],
7999            "and refused one where it does"
8000        );
8001    }
8002
8003    /// **A bar nobody has scrolled is not there at all.** The reference's fade starts
8004    /// closed and is opened only by movement (`scrollbar.dart:1960`); a list that has
8005    /// just appeared shows its content, not its furniture.
8006    ///
8007    /// It is **registered** even so, which a bar the platform does not draw is not. The
8008    /// difference is real: that one is not there, and this one is here and invisible —
8009    /// and a mouse coming near it is the only thing that can bring it back.
8010    #[test]
8011    fn an_untouched_area_shows_no_bar_but_can_still_be_reached_for() {
8012        let (runtime, _bars) = bars_runtime(Scrollbars::Always);
8013        let ui = build_ui(&tall(), Size::new(200.0, 100.0), &runtime, &Theme::dark());
8014        assert_eq!(painted(&ui), vec![], "nothing is painted for it");
8015
8016        let bar = *ui.scrollbars().first().expect("registered all the same");
8017        assert_eq!(bar.opacity, 0.0);
8018        let on_it = Point::new(194.0, 20.0);
8019        assert!(
8020            ui.scrollbar_at(on_it).is_none(),
8021            "and a bar at zero cannot be grabbed"
8022        );
8023        assert!(
8024            ui.scrollbar_near(on_it).is_some(),
8025            "but it can be reached for, which is what brings it back"
8026        );
8027        // The reach is wider than the bar: 8 pixels at a tenth of an opacity is not
8028        // something a hand aims at precisely.
8029        assert!(
8030            ui.scrollbar_near(Point::new(178.0, 20.0)).is_some(),
8031            "the reach takes in a tap target's room around the thumb: {:?}",
8032            bar.reach
8033        );
8034    }
8035
8036    /// **A pinned bar does not fade** (`scrollbar.dart:214`) — for an area whose content
8037    /// does not look scrollable, or one a reader should be able to aim at without
8038    /// scrolling first to make the bar appear.
8039    #[test]
8040    fn a_pinned_bar_does_not_fade() {
8041        let (runtime, _bars) = bars_runtime(Scrollbars::Always);
8042        let ui = build_ui(
8043            &tall().thumb_visibility(true),
8044            Size::new(200.0, 100.0),
8045            &runtime,
8046            &Theme::dark(),
8047        );
8048        assert_eq!(painted(&ui).len(), 1, "there without being scrolled to");
8049        assert_eq!(ui.scrollbars()[0].opacity, 1.0);
8050    }
8051
8052    /// **A thumb under a pointer, and a thumb in a hand**, are two different colours
8053    /// (`scrollbar.dart:245`, `:244`) — and neither is the resting one. The dragged
8054    /// value arrives at once: the hand is already on it.
8055    #[test]
8056    fn a_thumb_answers_the_pointer() {
8057        use crate::runtime::ScrollbarFade;
8058        let build = |prime: &dyn Fn(&mut Runtime)| {
8059            let (mut runtime, _bars) = bars_runtime(Scrollbars::Always);
8060            prime(&mut runtime);
8061            let ui = build_ui(
8062                &tall().thumb_visibility(true),
8063                Size::new(200.0, 100.0),
8064                &runtime,
8065                &Theme::dark(),
8066            );
8067            let id = ui.scrollbars()[0].id;
8068            (id, painted(&ui)[0].1)
8069        };
8070
8071        let theme = Theme::dark();
8072        let (id, resting) = build(&|_| {});
8073        assert_eq!(resting, theme.scheme.on_surface.fade(THUMB_ON_DARK));
8074
8075        let (_, warmed) = build(&|rt| {
8076            rt.scrollbar_fade.insert(
8077                id,
8078                ScrollbarFade {
8079                    opacity: 1.0,
8080                    hover: 1.0,
8081                    ..ScrollbarFade::default()
8082                },
8083            );
8084        });
8085        assert_eq!(warmed, theme.scheme.on_surface.fade(THUMB_HOVER_ON_DARK));
8086
8087        let (_, grabbed) = build(&|rt| rt.scrollbar_dragged = Some(id));
8088        assert_eq!(grabbed, theme.scheme.on_surface.fade(THUMB_DRAG_ON_DARK));
8089    }
8090
8091    #[test]
8092    fn hover_progress_interpolates_color() {
8093        let rt = Runtime::default();
8094        let base = build_ui(
8095            &clickable_sample(),
8096            Size::new(400.0, 100.0),
8097            &rt,
8098            &Theme::default(),
8099        );
8100        let id_a = base.hit(Point::new(50.0, 50.0)).unwrap();
8101
8102        // Without any progress: the base colour (red).
8103        if let Primitive::Rect { color, .. } = base.scene().primitives()[0] {
8104            assert_eq!(color, Color::rgb(1.0, 0.0, 0.0));
8105        } else {
8106            panic!("expected a rect");
8107        }
8108
8109        // Full progress: the hover colour (green).
8110        let mut rt = Runtime::default();
8111        rt.input.hovered = Some(id_a);
8112        rt.anims.insert(
8113            id_a,
8114            crate::Anim {
8115                hover: 1.0,
8116                ..Default::default()
8117            },
8118        );
8119        let ui = build_ui(
8120            &clickable_sample(),
8121            Size::new(400.0, 100.0),
8122            &rt,
8123            &Theme::default(),
8124        );
8125        if let Primitive::Rect { color, .. } = ui.scene().primitives()[0] {
8126            assert_eq!(color, Color::rgb(0.0, 1.0, 0.0));
8127        } else {
8128            panic!("expected a rect");
8129        }
8130    }
8131
8132    #[test]
8133    fn multiline_field_registers_as_scrollable_when_overflowing() {
8134        // A multi-line field whose content exceeds `rows` registers itself as a scrollable
8135        // area (with `max_y > 0`) — which is what the wheel and the scrollbar target. A short
8136        // field does not register.
8137        let tall = TextField::<Msg>::new("a\nb\nc\nd\ne\nf")
8138            .on_input(Msg::Edited)
8139            .rows(2)
8140            .width(200.0);
8141        let tree: Flex<Msg> = Flex::column().child(tall);
8142        let rt = Runtime::default();
8143        let ui = build_ui(&tree, Size::new(220.0, 240.0), &rt, &Theme::default());
8144        let maxes = ui.scrollable_maxes();
8145        assert_eq!(maxes.len(), 1, "the overflowing field registers");
8146        assert!(maxes[0].2 > 0.0, "max_y > 0 (overflowing content)");
8147
8148        let short = TextField::<Msg>::new("a\nb")
8149            .on_input(Msg::Edited)
8150            .rows(4)
8151            .width(200.0);
8152        let tree: Flex<Msg> = Flex::column().child(short);
8153        let ui = build_ui(&tree, Size::new(220.0, 240.0), &rt, &Theme::default());
8154        assert!(
8155            ui.scrollable_maxes().is_empty(),
8156            "a short field does not scroll"
8157        );
8158    }
8159
8160    #[test]
8161    fn only_text_inputs_place_a_cursor() {
8162        // The invariant of the click fix (milestone 39): a focusable button returns NO cursor
8163        // (`cursor_at` = None), so the shell does not start a text selection on it and does not
8164        // capture the click. Only text fields place one.
8165        let button = Button::<Msg>::new("x").on_press(Msg::A);
8166        assert_eq!(Widget::<Msg>::cursor_at(&button, 10.0, 5.0, 200.0, 0), None);
8167        let input = TextField::<Msg>::new("hi").on_input(Msg::Edited);
8168        assert!(Widget::<Msg>::cursor_at(&input, 10.0, 5.0, 200.0, 0).is_some());
8169    }
8170
8171    /// A form taller than its window: six buttons of 60 px in a 150 px scroll.
8172    fn long_form() -> SingleChildScrollView<Msg> {
8173        let mut column = Flex::<Msg>::column();
8174        for i in 0..6 {
8175            column = column.child(
8176                Button::new(format!("field {i}"))
8177                    .on_press(Msg::A)
8178                    .height(60.0),
8179            );
8180        }
8181        SingleChildScrollView::new()
8182            .width(200.0)
8183            .height(150.0)
8184            .child(column)
8185    }
8186
8187    fn form_ui(runtime: &Runtime) -> Ui<Msg> {
8188        build_ui(
8189            &long_form(),
8190            Size::new(200.0, 150.0),
8191            runtime,
8192            &Theme::default(),
8193        )
8194    }
8195
8196    /// **Every** field is a focus stop, not only the two and a half on screen.
8197    ///
8198    /// Registering what the eye can see is how a long form came to have most of its
8199    /// fields unreachable from the keyboard: Tab walked the visible ones and wrapped.
8200    #[test]
8201    fn tab_reaches_the_fields_below_the_fold() {
8202        let ui = form_ui(&Runtime::default());
8203        assert_eq!(ui.focusables.len(), 6, "all six, not the visible ones");
8204        // And the traversal is in tree order, so Tab walks down the form.
8205        let ids: Vec<_> = ui.focusables.iter().map(|f| f.id).collect();
8206        assert_eq!(ui.focus_next(Some(ids[2]), true), Some(ids[3]));
8207    }
8208
8209    /// A stop clipped away by a scroll keeps its **real** box, and an empty visible one.
8210    /// The two are different questions and the registry answers both.
8211    #[test]
8212    fn a_stop_out_of_sight_keeps_its_geometry() {
8213        let ui = form_ui(&Runtime::default());
8214        let fifth = ui.focusables[5];
8215        assert!((fifth.bounds.y - 300.0).abs() < 0.5, "{:?}", fifth.bounds);
8216        assert_eq!(fifth.rect.height, 0.0, "none of it is on screen");
8217        assert!(fifth.scroll.is_some(), "something around it scrolls");
8218    }
8219
8220    /// A click still lands only on what can be seen. The unclipped box of a field
8221    /// scrolled out of sight overlaps the window, and a tap there must not focus it.
8222    #[test]
8223    fn a_click_still_only_reaches_what_is_visible() {
8224        let ui = form_ui(&Runtime::default());
8225        // The second field, in the middle of its own visible box.
8226        let second = ui.focusables[1];
8227        let inside = Point::new(
8228            second.rect.x + second.rect.width / 2.0,
8229            second.rect.y + second.rect.height / 2.0,
8230        );
8231        assert_eq!(ui.focus_hit(inside).map(|(id, _)| id), Some(second.id));
8232        // The sixth field's **real** box overlaps nothing on screen, and a tap where it
8233        // would be if the form were not scrolled hits nothing at all.
8234        let sixth = ui.focusables[5];
8235        let past = Point::new(
8236            sixth.bounds.x + sixth.bounds.width / 2.0,
8237            sixth.bounds.y + sixth.bounds.height / 2.0,
8238        );
8239        assert!(past.y > 150.0, "past the fold: {past:?}");
8240        assert_eq!(ui.focus_hit(past), None);
8241    }
8242
8243    /// Focus lands below the fold and the view follows: the least movement that brings
8244    /// the field's foot to the window's.
8245    #[test]
8246    fn revealing_a_stop_below_the_fold_scrolls_to_it() {
8247        let runtime = Runtime::default();
8248        let ui = form_ui(&runtime);
8249        let area = ui.scroll_regions()[0];
8250        let fifth = ui.focusables[5].id;
8251        let moves = ui.reveal(fifth, &runtime);
8252        // Field 5 sits at 300..360 and the window is 150 tall: its foot reaches the
8253        // window's foot at 360 - 150 = 210.
8254        assert_eq!(moves, vec![(area.id, (0.0, 210.0))]);
8255    }
8256
8257    /// Already in view, nothing moves — the answer is the empty list rather than a
8258    /// zero, so no caller has to ask twice.
8259    #[test]
8260    fn revealing_something_already_in_view_moves_nothing() {
8261        let runtime = Runtime::default();
8262        let ui = form_ui(&runtime);
8263        assert!(ui.reveal(ui.focusables[0].id, &runtime).is_empty());
8264        assert!(ui.reveal(ui.focusables[1].id, &runtime).is_empty());
8265        // And a stop the frame does not contain at all is not an error.
8266        assert!(ui.reveal(WidgetId::from_u64(7), &runtime).is_empty());
8267    }
8268
8269    /// Coming back **up** the form scrolls the other way, and only as far as it must.
8270    #[test]
8271    fn revealing_a_stop_above_the_fold_scrolls_back() {
8272        let mut runtime = Runtime::default();
8273        let area_id = form_ui(&runtime).scroll_regions()[0].id;
8274        runtime.scroll.insert(area_id, (0.0, 210.0));
8275        let ui = form_ui(&runtime);
8276        let first = ui.focusables[0].id;
8277        // Field 0 sits at 0..60: its head reaches the window's head at offset 0.
8278        assert_eq!(ui.reveal(first, &runtime), vec![(area_id, (0.0, 0.0))]);
8279        // At offset 210 the window shows 210..360, so field 4 (240..300) is wholly in
8280        // it and does not move. Field 3 (180..240) hangs over the top edge and would.
8281        assert!(ui.reveal(ui.focusables[4].id, &runtime).is_empty());
8282        assert!(!ui.reveal(ui.focusables[3].id, &runtime).is_empty());
8283    }
8284
8285    /// The arithmetic on its own: the least movement, the leading edge for anything too
8286    /// big to fit, and the clamp at the ends of the travel.
8287    #[test]
8288    fn the_reveal_moves_as_little_as_it_can() {
8289        let area = Scrollable {
8290            id: WidgetId::ROOT,
8291            viewport: Rect::new(0.0, 0.0, 100.0, 100.0),
8292            max_x: 0.0,
8293            max_y: 400.0,
8294            axis: crate::scroll::Axis::Vertical,
8295            physics: crate::ScrollPhysics::CLAMPING,
8296            overscroll: Some(crate::OverscrollIndicator::Glow),
8297            fling: frus_core::VelocityStrategy::Regression,
8298            refresh: None,
8299            page: None,
8300            reverse_x: false,
8301            reverse_y: false,
8302            host: None,
8303            keep_visible: None,
8304        };
8305        // Inside: nothing.
8306        assert_eq!(
8307            area.reveal(Rect::new(0.0, 10.0, 50.0, 20.0), (0.0, 0.0)).1,
8308            (0.0, 0.0)
8309        );
8310        // Below: its foot comes to the window's foot, and no further.
8311        assert_eq!(
8312            area.reveal(Rect::new(0.0, 140.0, 50.0, 20.0), (0.0, 0.0)).0,
8313            (0.0, 60.0)
8314        );
8315        // Above: its head comes to the window's head.
8316        assert_eq!(
8317            area.reveal(Rect::new(0.0, -30.0, 50.0, 20.0), (0.0, 50.0))
8318                .0,
8319            (0.0, 20.0)
8320        );
8321        // Taller than the window: the **start** edge wins, because that is what is read.
8322        assert_eq!(
8323            area.reveal(Rect::new(0.0, -10.0, 50.0, 300.0), (0.0, 40.0))
8324                .0,
8325            (0.0, 30.0)
8326        );
8327        // And the travel is a wall: an offset past the end is not asked for.
8328        assert_eq!(
8329            area.reveal(Rect::new(0.0, 900.0, 50.0, 20.0), (0.0, 0.0)).0,
8330            (0.0, 400.0)
8331        );
8332    }
8333
8334    /// A reversed axis numbers its offsets from the far end, so the same movement of the
8335    /// content is the opposite movement of the offset. One function knows that, and the
8336    /// reveal asks it rather than keeping a second opinion.
8337    #[test]
8338    fn the_reveal_follows_a_reversed_axis() {
8339        let area = Scrollable {
8340            id: WidgetId::ROOT,
8341            viewport: Rect::new(0.0, 0.0, 100.0, 100.0),
8342            max_x: 0.0,
8343            max_y: 400.0,
8344            axis: crate::scroll::Axis::Vertical,
8345            physics: crate::ScrollPhysics::CLAMPING,
8346            overscroll: Some(crate::OverscrollIndicator::Glow),
8347            fling: frus_core::VelocityStrategy::Regression,
8348            refresh: None,
8349            page: None,
8350            reverse_x: false,
8351            reverse_y: true,
8352            host: None,
8353            keep_visible: None,
8354        };
8355        // A box below the window: the content must move **up** by 60 either way. On a
8356        // reversed axis a rising offset pushes the content down (`content_origin` is
8357        // `viewport - content + offset` there), so the offset comes **down** by 60 to do
8358        // what a plain axis would need it to go up by.
8359        let (offset, shift) = area.reveal(Rect::new(0.0, 140.0, 50.0, 20.0), (0.0, 100.0));
8360        assert_eq!(shift, (0.0, -60.0));
8361        assert_eq!(offset, (0.0, 40.0));
8362    }
8363
8364    /// Centring puts the box in the middle of the window, and the clamp is what keeps
8365    /// the first and last items of a row where they belong rather than half off it.
8366    #[test]
8367    fn centring_is_clamped_to_the_travel() {
8368        let area = Scrollable {
8369            id: WidgetId::ROOT,
8370            viewport: Rect::new(0.0, 0.0, 300.0, 50.0),
8371            max_x: 700.0,
8372            max_y: 0.0,
8373            axis: crate::scroll::Axis::Vertical,
8374            physics: crate::ScrollPhysics::CLAMPING,
8375            overscroll: Some(crate::OverscrollIndicator::Glow),
8376            fling: frus_core::VelocityStrategy::Regression,
8377            refresh: None,
8378            page: None,
8379            reverse_x: false,
8380            reverse_y: false,
8381            host: None,
8382            keep_visible: None,
8383        };
8384        // A box at 400..500 has its middle at 450; the window's is at 150.
8385        assert_eq!(
8386            area.centre(Rect::new(400.0, 0.0, 100.0, 50.0), (0.0, 0.0))
8387                .0,
8388            (300.0, 0.0)
8389        );
8390        // The first item cannot be centred without scrolling backwards, so it stays.
8391        assert_eq!(
8392            area.centre(Rect::new(0.0, 0.0, 100.0, 50.0), (0.0, 0.0)).0,
8393            (0.0, 0.0)
8394        );
8395        // Nor the last past the end of the travel.
8396        assert_eq!(
8397            area.centre(Rect::new(900.0, 0.0, 100.0, 50.0), (0.0, 0.0))
8398                .0,
8399            (700.0, 0.0)
8400        );
8401    }
8402
8403    /// **A lone child could widen the box it was given, and take the screen with it.**
8404    ///
8405    /// A flex item's automatic minimum is its **min-content** size, so agreeing to shrink
8406    /// buys nothing on its own: the item shrinks willingly and then refuses to go below
8407    /// the widest thing inside it. A column holding one over-wide row came out at that
8408    /// row's width; the padded box around it came out at the same; and every sibling,
8409    /// stretched across the column, came out at the same again — a whole screen laid out
8410    /// to the width of its widest line and then clipped by the window.
8411    ///
8412    /// Reported from a real application: a shop whose banner, search field, filter row
8413    /// and product grid were all cut off at the same edge, because one segmented control
8414    /// four segments wide had decided how wide the screen was.
8415    ///
8416    /// The over-wide child still overflows — that is the reference's behaviour and the
8417    /// honest one — but it no longer drags its parent and its siblings out with it.
8418    #[test]
8419    fn a_lone_child_cannot_widen_the_box_it_was_given() {
8420        let ordinary = Color::rgb(0.1, 0.2, 0.3);
8421        let overwide = Color::rgb(0.9, 0.8, 0.7);
8422        let screen: SingleChildScrollView<Msg> = SingleChildScrollView::new().width(300.0).child(
8423            Container::new().padding(24.0).child(
8424                Flex::column()
8425                    .align(frus_layout::Align::Stretch)
8426                    .child(Container::new().height(20.0).color(ordinary))
8427                    .child(Container::new().width(600.0).height(20.0).color(overwide)),
8428            ),
8429        );
8430        let ui = build_ui(
8431            &screen,
8432            Size::new(300.0, 800.0),
8433            &Runtime::default(),
8434            &Theme::default(),
8435        );
8436        let width_of = |wanted: Color| {
8437            ui.scene()
8438                .primitives()
8439                .iter()
8440                .find_map(|p| match p {
8441                    Primitive::Rect { color, rect, .. } if *color == wanted => Some(rect.width),
8442                    _ => None,
8443                })
8444                .expect("the box is drawn")
8445        };
8446        // 300 less the padding either side, and not 600.
8447        assert_eq!(width_of(ordinary), 252.0);
8448        // The one that asked to be too wide still is: it overflows rather than being
8449        // squeezed, and rather than resizing everything around it.
8450        assert_eq!(width_of(overwide), 600.0);
8451    }
8452
8453    #[test]
8454    fn tab_cycles_focusables_in_order() {
8455        let tree = Flex::<Msg>::column()
8456            .child(Button::new("un").on_press(Msg::A))
8457            .child(Button::new("deux").on_press(Msg::B));
8458        let ui = build_ui(
8459            &tree,
8460            Size::new(200.0, 200.0),
8461            &Runtime::default(),
8462            &Theme::default(),
8463        );
8464        assert_eq!(ui.focusables.len(), 2, "both buttons are focusable");
8465        let first = ui.focusables[0].id;
8466        let second = ui.focusables[1].id;
8467
8468        // With no focus: Tab -> first, Shift+Tab -> last.
8469        assert_eq!(ui.focus_next(None, true), Some(first));
8470        assert_eq!(ui.focus_next(None, false), Some(second));
8471        // Wrap-around.
8472        assert_eq!(ui.focus_next(Some(first), true), Some(second));
8473        assert_eq!(ui.focus_next(Some(second), true), Some(first));
8474        assert_eq!(ui.focus_next(Some(first), false), Some(second));
8475    }
8476
8477    #[test]
8478    fn focus_ring_for_button_not_for_textinput() {
8479        let theme = Theme::default();
8480        let ring = theme.focus.fade(0.4); // focus_progress = 0 → alpha 0.4
8481        let count_ring = |tree: &dyn Widget<Msg>, keyboard: bool| -> usize {
8482            let mut rt = Runtime::default();
8483            let probe = build_ui(tree, Size::new(200.0, 200.0), &rt, &theme);
8484            rt.input.focused = probe.focusables.first().map(|f| f.id);
8485            rt.focus_visible = keyboard;
8486            let ui = build_ui(tree, Size::new(200.0, 200.0), &rt, &theme);
8487            ui.scene()
8488                .primitives()
8489                .iter()
8490                .filter(
8491                    |p| matches!(p, Primitive::Rect { border_color, .. } if *border_color == ring),
8492                )
8493                .count()
8494        };
8495        let with_button = Flex::<Msg>::column().child(Button::new("x").on_press(Msg::A));
8496        assert!(
8497            count_ring(&with_button, true) >= 1,
8498            "a button focused with the keyboard has a generic ring"
8499        );
8500        // Focus taken with the pointer: no ring (FocusHighlightMode).
8501        assert_eq!(
8502            count_ring(&with_button, false),
8503            0,
8504            "a click does not flash a ring"
8505        );
8506
8507        let with_input = Flex::<Msg>::column().child(TextField::new("hi").on_input(Msg::Edited));
8508        assert_eq!(
8509            count_ring(&with_input, true),
8510            0,
8511            "the field draws its own focus"
8512        );
8513    }
8514
8515    #[test]
8516    fn arrow_focus_navigates_geometrically() {
8517        // A 2×2 grid of buttons; each target is identified by its message.
8518        let grid: Flex<Msg> = Flex::column()
8519            .child(
8520                Flex::row()
8521                    .child(Button::new("a").on_press(Msg::A))
8522                    .child(Button::new("b").on_press(Msg::B)),
8523            )
8524            .child(
8525                Flex::row()
8526                    .child(Button::new("c").on_press(Msg::C))
8527                    .child(Button::new("d").on_press(Msg::D)),
8528            );
8529        let ui = build_ui(
8530            &grid,
8531            Size::new(300.0, 200.0),
8532            &Runtime::default(),
8533            &Theme::default(),
8534        );
8535        let top_left = ui.focus_next(None, true).expect("premier focusable");
8536        assert_eq!(ui.msg_for(top_left), Some(Msg::A));
8537
8538        // Right: a → b; down: a → c; and nothing to the left of a.
8539        let right = ui
8540            .focus_directional(top_left, FocusDirection::Right)
8541            .expect("to the right");
8542        assert_eq!(ui.msg_for(right), Some(Msg::B));
8543        let down = ui
8544            .focus_directional(top_left, FocusDirection::Down)
8545            .expect("downwards");
8546        assert_eq!(ui.msg_for(down), Some(Msg::C));
8547        assert_eq!(ui.focus_directional(top_left, FocusDirection::Left), None);
8548        // The diagonal is kept in check: from b, down → d (aligned), not c.
8549        let down_right = ui
8550            .focus_directional(right, FocusDirection::Down)
8551            .expect("down from b");
8552        assert_eq!(ui.msg_for(down_right), Some(Msg::D));
8553    }
8554
8555    #[test]
8556    fn keyed_identity_survives_middle_removal() {
8557        let colored = |c: Color| Container::<Msg>::new().width(50.0).height(20.0).color(c);
8558        let red = Color::rgb(1.0, 0.0, 0.0);
8559        let green = Color::rgb(0.0, 1.0, 0.0);
8560        let blue = Color::rgb(0.0, 0.0, 1.0);
8561
8562        let owner_of = |ui: &Ui<Msg>, c: Color| -> u64 {
8563            ui.scene()
8564                .primitives()
8565                .iter()
8566                .find_map(|p| match p {
8567                    Primitive::Rect { color, owner, .. } if *color == c => Some(*owner),
8568                    _ => None,
8569                })
8570                .expect("the primitive is there")
8571        };
8572
8573        // The list [red(key 1), green(key 2), blue(key 3)].
8574        let full = Flex::<Msg>::column()
8575            .child(Keyed::new(1u64, colored(red)))
8576            .child(Keyed::new(2u64, colored(green)))
8577            .child(Keyed::new(3u64, colored(blue)));
8578        let ui_full = build_ui(
8579            &full,
8580            Size::new(200.0, 200.0),
8581            &Runtime::default(),
8582            &Theme::default(),
8583        );
8584
8585        // The list [red(1), blue(3)]: the green in the middle is removed → blue goes from index 2 to 1.
8586        let removed = Flex::<Msg>::column()
8587            .child(Keyed::new(1u64, colored(red)))
8588            .child(Keyed::new(3u64, colored(blue)));
8589        let ui_removed = build_ui(
8590            &removed,
8591            Size::new(200.0, 200.0),
8592            &Runtime::default(),
8593            &Theme::default(),
8594        );
8595
8596        // Blue's identity (owner, key 3) is UNCHANGED despite the shift in position.
8597        assert_eq!(owner_of(&ui_full, blue), owner_of(&ui_removed, blue));
8598
8599        // Without a key, the 2nd child's positional identity DOES change (index 2 vs 1).
8600        let unkeyed_full = Flex::<Msg>::column()
8601            .child(colored(red))
8602            .child(colored(green))
8603            .child(colored(blue));
8604        let unkeyed_removed = Flex::<Msg>::column()
8605            .child(colored(red))
8606            .child(colored(blue));
8607        let u1 = build_ui(
8608            &unkeyed_full,
8609            Size::new(200.0, 200.0),
8610            &Runtime::default(),
8611            &Theme::default(),
8612        );
8613        let u2 = build_ui(
8614            &unkeyed_removed,
8615            Size::new(200.0, 200.0),
8616            &Runtime::default(),
8617            &Theme::default(),
8618        );
8619        assert_ne!(owner_of(&u1, blue), owner_of(&u2, blue));
8620    }
8621
8622    #[test]
8623    fn center_overlay_scrim_click_dismisses() {
8624        // A Center modal with `.dismiss`: clicking the scrim (outside the content) returns the
8625        // dismissal message; clicking the content does not.
8626        let modal = Container::<Msg>::new()
8627            .width(100.0)
8628            .height(60.0)
8629            .color(Color::WHITE);
8630        let portal: OverlayPortal<Msg> =
8631            OverlayPortal::new(Container::<Msg>::new().width(20.0).height(20.0))
8632                .overlay(modal, Placement::Center)
8633                .dismiss(Msg::A);
8634        let ui = build_ui(
8635            &portal,
8636            Size::new(400.0, 300.0),
8637            &Runtime::default(),
8638            &Theme::default(),
8639        );
8640
8641        // The top-left corner: on the scrim → it dismisses.
8642        let corner = ui.hit(Point::new(5.0, 5.0)).expect("a clickable scrim");
8643        assert_eq!(ui.msg_for(corner), Some(Msg::A));
8644    }
8645
8646    /// **The shell's mark reaches the field** (milestone 511): a focused field whose
8647    /// selection the runtime marks as a touch one paints its two handles; the same
8648    /// selection unmarked paints none, and so does a marked field that lost the focus.
8649    #[test]
8650    fn a_touch_selection_reaches_the_field_as_handles() {
8651        let tree = Flex::column().width(300.0).height(80.0).child(
8652            TextField::new("hello world")
8653                .width(200.0)
8654                .on_input(Msg::Edited),
8655        );
8656        let theme = Theme::default();
8657        let size = Size::new(300.0, 80.0);
8658        let (id, _) = build_ui(&tree, size, &Runtime::default(), &theme)
8659            .focus_hit(Point::new(10.0, 10.0))
8660            .expect("the field");
8661        let handles = |focused: bool, marked: bool| {
8662            let mut rt = Runtime::default();
8663            rt.edits.insert(
8664                id,
8665                Edit {
8666                    cursor: 11,
8667                    anchor: Some(6),
8668                    composing: None,
8669                },
8670            );
8671            rt.input.focused = focused.then_some(id);
8672            rt.selection_handles = marked.then_some(id);
8673            let ui = build_ui(&tree, size, &rt, &theme);
8674            ui.scene()
8675                .primitives()
8676                .iter()
8677                .filter(|p| {
8678                    matches!(p, Primitive::Path { fill: Some(c), .. } if *c == theme.scheme.primary)
8679                })
8680                .count()
8681        };
8682        assert_eq!(handles(true, true), 2);
8683        assert_eq!(handles(true, false), 0, "a selection made with a mouse");
8684        assert_eq!(handles(false, true), 0, "a field left behind");
8685    }
8686
8687    /// A wrapper that fuses with its field shares its identity, so it is the wrapper the
8688    /// shell asks for the handles — and a wrapper that forgot to pass the question on
8689    /// would leave a finger nothing to take (the three silent bugs `Responsive` has
8690    /// already cost, milestones 477 to 495).
8691    #[test]
8692    fn the_wrappers_that_fuse_with_a_field_pass_its_handles_on() {
8693        let field = || TextField::<Msg>::new("hello world").width(200.0);
8694        let edit = Edit {
8695            cursor: 11,
8696            anchor: Some(6),
8697            composing: None,
8698        };
8699        let direct = Widget::<Msg>::selection_handles(&field(), 200.0, &edit, 0.0);
8700        assert!(direct.is_some());
8701        let keyed = crate::Keyed::new(1, field());
8702        let responsive = crate::Responsive::new(crate::SizeClass::Compact).compact(field());
8703        let boxed: Box<dyn Widget<Msg>> = Box::new(field());
8704        assert_eq!(keyed.selection_handles(200.0, &edit, 0.0), direct);
8705        assert_eq!(responsive.selection_handles(200.0, &edit, 0.0), direct);
8706        assert_eq!(boxed.selection_handles(200.0, &edit, 0.0), direct);
8707    }
8708
8709    /// The tree the selection-bar tests share: a field with `spacer` px of room above it, so
8710    /// the bar has somewhere to go above the selection (or not, at 0).
8711    fn bar_tree(spacer: f32, value: &str) -> impl Widget<Msg> {
8712        Flex::column()
8713            .width(300.0)
8714            .height(400.0)
8715            .child(Container::new().width(300.0).height(spacer))
8716            .child(
8717                TextField::new(value.to_string())
8718                    .width(200.0)
8719                    .on_input(Msg::Edited),
8720            )
8721    }
8722
8723    /// The identity of the field in [`bar_tree`].
8724    fn bar_field_id(tree: &impl Widget<Msg>) -> WidgetId {
8725        let ui = build_ui(
8726            tree,
8727            Size::new(300.0, 400.0),
8728            &Runtime::default(),
8729            &Theme::default(),
8730        );
8731        let id = ui.focusable_ids().next().expect("the field");
8732        id
8733    }
8734
8735    /// A frame with the field's bar open: `edit` selected, the runtime's mark saying
8736    /// `can_paste`, and the field focused and marked as the two flags say.
8737    fn bar_frame(
8738        tree: &impl Widget<Msg>,
8739        edit: Edit,
8740        can_paste: bool,
8741        focused: bool,
8742        marked: bool,
8743    ) -> Ui<Msg> {
8744        let id = bar_field_id(tree);
8745        let mut rt = Runtime::default();
8746        rt.edits.insert(id, edit);
8747        rt.input.focused = focused.then_some(id);
8748        rt.selection_toolbar = marked.then_some(crate::runtime::ToolbarMark {
8749            id,
8750            can_paste,
8751            at: None,
8752        });
8753        // The pill's tests: under a theme that follows Android, where the bar is a pill
8754        // (milestone 618), whatever this test runs on.
8755        build_ui(tree, Size::new(300.0, 400.0), &rt, &pill_theme())
8756    }
8757
8758    /// **On the desktops the bar is a menu, and a right-click puts it at the pointer**
8759    /// (milestone 618; `editable_text.dart:3211`): opened any other way it hangs from the
8760    /// top of the selection, at its middle; opened by a right-click it hangs from where the
8761    /// pointer was; either way it is pushed back inside the window.
8762    #[test]
8763    fn a_desktop_menu_hangs_from_the_pointer_or_the_selection() {
8764        let tree = bar_tree(150.0, "hello world");
8765        let id = bar_field_id(&tree);
8766        let theme = Theme::default().with_platform(frus_core::TargetPlatform::Linux);
8767        let frame = |at: Option<Point>| {
8768            let mut rt = Runtime::default();
8769            rt.edits.insert(id, selected(5, Some(0)));
8770            rt.input.focused = Some(id);
8771            rt.selection_toolbar = Some(crate::runtime::ToolbarMark {
8772                id,
8773                can_paste: false,
8774                at,
8775            });
8776            build_ui(&tree, Size::new(300.0, 400.0), &rt, &theme)
8777        };
8778        let window = Rect::new(0.0, 0.0, 300.0, 400.0);
8779
8780        let ui = frame(Some(Point::new(40.0, 60.0)));
8781        let menu = ui.toolbars[0];
8782        assert_eq!(menu.width, 222.0, "the menu, not the pill");
8783        assert_eq!((menu.x, menu.y), (40.0, 60.0), "at the pointer");
8784
8785        let ui = frame(Some(Point::new(250.0, 380.0)));
8786        let menu = ui.toolbars[0];
8787        let expected = crate::selectiontoolbar::place_menu(
8788            Point::new(250.0, 380.0),
8789            Size::new(menu.width, menu.height),
8790            window,
8791        );
8792        assert_eq!((menu.x, menu.y), expected, "pushed back inside");
8793        assert!(menu.x + menu.width <= 292.0 && menu.y + menu.height <= 392.0);
8794
8795        let ui = frame(None);
8796        let menu = ui.toolbars[0];
8797        let field = ui.widget_rect(id).expect("the field");
8798        assert!(
8799            (menu.y - field.y).abs() < field.height,
8800            "hangs from the selection's top, not above it: {menu:?} {field:?}"
8801        );
8802    }
8803
8804    /// A theme whose selection bar is the pill.
8805    fn pill_theme() -> Theme {
8806        Theme::default().with_platform(frus_core::TargetPlatform::Android)
8807    }
8808
8809    fn selected(cursor: usize, anchor: Option<usize>) -> Edit {
8810        Edit {
8811            cursor,
8812            anchor,
8813            composing: None,
8814        }
8815    }
8816
8817    /// The editing actions a finger can reach on the bar, found the way a finger finds
8818    /// them: pressing along a line through the bar and asking what is there.
8819    fn reachable_actions(ui: &Ui<Msg>) -> Vec<crate::EditAction> {
8820        let Some(bar) = ui.toolbars.first().copied() else {
8821            return Vec::new();
8822        };
8823        let mut found = Vec::new();
8824        let mut x = bar.x + 1.0;
8825        while x < bar.x + bar.width {
8826            let at = Point::new(x, bar.y + bar.height * 0.5);
8827            if let Some(action) = ui.hit(at).and_then(|id| ui.edit_action_for(id)) {
8828                if !found.contains(&action) {
8829                    found.push(action);
8830                }
8831            }
8832            x += 2.0;
8833        }
8834        found
8835    }
8836
8837    fn sorted_actions(mut actions: Vec<crate::EditAction>) -> Vec<crate::EditAction> {
8838        actions.sort_by_key(|action| format!("{action:?}"));
8839        actions
8840    }
8841
8842    /// **The bar floats above a selection** (milestone 568): a focused field the shell has
8843    /// marked, with a word selected and something on the clipboard, gets Cut, Copy, Paste and
8844    /// Select all — in a box **above** the field, and each one a target a press can find.
8845    #[test]
8846    fn a_marked_field_floats_its_selection_bar_above_it() {
8847        use crate::EditAction::{Copy, Cut, Paste, SelectAll};
8848        let tree = bar_tree(200.0, "hello world");
8849        let ui = bar_frame(&tree, selected(11, Some(6)), true, true, true);
8850        assert_eq!(ui.toolbars.len(), 1, "one bar");
8851        let bar = ui.toolbars[0];
8852        // Above the **selection**, a gap clear of it: not above the field, whose top edge a
8853        // bar over a line of text a few px down may overlap.
8854        let field = ui.widget_rect(bar_field_id(&tree)).expect("the field");
8855        let selection = Widget::<Msg>::selection_anchor(
8856            &TextField::<Msg>::new("hello world").width(200.0),
8857            field.width,
8858            &selected(11, Some(6)),
8859            0.0,
8860        )
8861        .expect("a selection box");
8862        let gap = field.y + selection.y - (bar.y + bar.height);
8863        assert!(
8864            (gap - crate::selectiontoolbar::GAP).abs() < 0.01,
8865            "the bar ends {gap} px above the selection: {bar:?}, selection at {selection:?} in {field:?}"
8866        );
8867        assert!(
8868            bar.x >= 0.0 && bar.x + bar.width <= 300.0,
8869            "inside the window"
8870        );
8871        assert_eq!(
8872            sorted_actions(reachable_actions(&ui)),
8873            sorted_actions(vec![Cut, Copy, Paste, SelectAll])
8874        );
8875    }
8876
8877    /// With no room above the field, the bar goes **below** the selection — clear of the
8878    /// handles that hang there.
8879    #[test]
8880    fn with_no_room_above_the_bar_goes_below_the_field() {
8881        let tree = bar_tree(0.0, "hello world");
8882        let ui = bar_frame(&tree, selected(11, Some(6)), true, true, true);
8883        let bar = *ui.toolbars.first().expect("a bar");
8884        let field = ui.widget_rect(bar_field_id(&tree)).expect("the field");
8885        assert!(
8886            bar.y >= field.y + field.height * 0.5,
8887            "below the selection's line: bar {bar:?}, field {field:?}"
8888        );
8889    }
8890
8891    /// **Only a marked, focused field shows its bar**: the same selection unmarked (made
8892    /// with a mouse or the keyboard) shows none, and neither does a marked field that lost
8893    /// the focus.
8894    #[test]
8895    fn the_bar_shows_only_for_the_marked_focused_field() {
8896        let tree = bar_tree(200.0, "hello world");
8897        let edit = selected(11, Some(6));
8898        let shown = |focused, marked| bar_frame(&tree, edit, true, focused, marked).toolbars.len();
8899        assert_eq!(shown(true, true), 1);
8900        assert_eq!(shown(true, false), 0, "not marked");
8901        assert_eq!(shown(false, true), 0, "a field left behind");
8902    }
8903
8904    /// **The list follows the three conditions**: nothing selected offers Paste and Select
8905    /// all; everything selected offers Cut and Copy; and a field with nothing in it and
8906    /// nothing to paste has no bar at all.
8907    #[test]
8908    fn the_bar_offers_what_the_situation_allows() {
8909        use crate::EditAction::{Copy, Cut, Paste, SelectAll};
8910        let tree = bar_tree(200.0, "hello world");
8911        // A caret, no selection, something to paste.
8912        let caret = bar_frame(&tree, selected(3, None), true, true, true);
8913        assert_eq!(
8914            sorted_actions(reachable_actions(&caret)),
8915            sorted_actions(vec![Paste, SelectAll])
8916        );
8917        // A caret and an empty clipboard: only Select all.
8918        let bare = bar_frame(&tree, selected(3, None), false, true, true);
8919        assert_eq!(reachable_actions(&bare), vec![SelectAll]);
8920        // Everything selected (`usize::MAX` is "the end", as Select all leaves it).
8921        let all = bar_frame(&tree, selected(usize::MAX, Some(0)), false, true, true);
8922        assert_eq!(
8923            sorted_actions(reachable_actions(&all)),
8924            sorted_actions(vec![Cut, Copy])
8925        );
8926        // An empty field with nothing to paste has nothing to offer.
8927        let empty = bar_tree(200.0, "");
8928        let nothing = bar_frame(&empty, selected(0, None), false, true, true);
8929        assert!(nothing.toolbars.is_empty());
8930    }
8931
8932    /// A press on the bar is known for one — the shell must not treat it as a press
8933    /// elsewhere, which puts the selection away.
8934    #[test]
8935    fn a_press_on_the_bar_is_a_press_on_the_bar() {
8936        let tree = bar_tree(200.0, "hello world");
8937        let ui = bar_frame(&tree, selected(11, Some(6)), true, true, true);
8938        let bar = ui.toolbars[0];
8939        assert!(ui.toolbar_contains(Point::new(bar.x + 3.0, bar.y + 3.0)));
8940        assert!(!ui.toolbar_contains(Point::new(bar.x - 5.0, bar.y - 5.0)));
8941        let unmarked = bar_frame(&tree, selected(11, Some(6)), true, true, false);
8942        assert!(!unmarked.toolbar_contains(Point::new(bar.x + 3.0, bar.y + 3.0)));
8943    }
8944
8945    /// The same, **by identity** — how an assistive technology names what it clicks: a button
8946    /// of the bar is on the bar, the field it acts on is not.
8947    #[test]
8948    fn a_target_named_by_identity_is_on_the_bar_or_not() {
8949        let tree = bar_tree(200.0, "hello world");
8950        let ui = bar_frame(&tree, selected(11, Some(6)), true, true, true);
8951        let bar = ui.toolbars[0];
8952        let button = ui
8953            .hit(Point::new(
8954                bar.x + bar.width * 0.5,
8955                bar.y + bar.height * 0.5,
8956            ))
8957            .filter(|id| ui.edit_action_for(*id).is_some())
8958            .or_else(|| {
8959                let mut x = bar.x + 1.0;
8960                let mut found = None;
8961                while x < bar.x + bar.width && found.is_none() {
8962                    found = ui
8963                        .hit(Point::new(x, bar.y + bar.height * 0.5))
8964                        .filter(|id| ui.edit_action_for(*id).is_some());
8965                    x += 2.0;
8966                }
8967                found
8968            })
8969            .expect("a button of the bar");
8970        assert!(ui.hit_is_on_toolbar(button));
8971        assert!(!ui.hit_is_on_toolbar(bar_field_id(&tree)));
8972    }
8973
8974    /// **The hooks pass through the wrappers that fuse with a field** — the same three silent
8975    /// bugs `Responsive` has already cost (milestones 477 to 495), for the bar's hooks.
8976    #[test]
8977    fn the_wrappers_that_fuse_with_a_field_pass_its_bar_on() {
8978        use crate::ToolbarContext;
8979        let field = || TextField::<Msg>::new("hello world").width(200.0);
8980        let context = ToolbarContext {
8981            has_selection: true,
8982            can_paste: true,
8983            all_selected: false,
8984        };
8985        let edit = selected(11, Some(6));
8986        let count = |widget: &dyn Widget<Msg>| {
8987            widget
8988                .selection_toolbar(context)
8989                .map(|bar| bar.children().len())
8990        };
8991        let direct = count(&field());
8992        assert_eq!(direct, Some(4));
8993        let anchor = Widget::<Msg>::selection_anchor(&field(), 200.0, &edit, 0.0);
8994        assert!(anchor.is_some());
8995        let keyed = crate::Keyed::new(1, field());
8996        let responsive = crate::Responsive::new(crate::SizeClass::Compact).compact(field());
8997        let boxed: Box<dyn Widget<Msg>> = Box::new(field());
8998        assert_eq!(count(&keyed), direct);
8999        assert_eq!(count(&responsive), direct);
9000        assert_eq!(count(&boxed), direct);
9001        assert_eq!(keyed.selection_anchor(200.0, &edit, 0.0), anchor);
9002        assert_eq!(responsive.selection_anchor(200.0, &edit, 0.0), anchor);
9003        assert_eq!(boxed.selection_anchor(200.0, &edit, 0.0), anchor);
9004    }
9005
9006    /// Two fields, one marked: **one bar**, over the marked one — not one over each field
9007    /// that happens to hold a selection.
9008    #[test]
9009    fn of_two_fields_only_the_marked_one_shows_a_bar() {
9010        let tree = Flex::column()
9011            .width(300.0)
9012            .height(400.0)
9013            .child(Container::new().width(300.0).height(150.0))
9014            .child(
9015                TextField::new("first field")
9016                    .width(200.0)
9017                    .on_input(Msg::Edited),
9018            )
9019            .child(
9020                TextField::new("second field")
9021                    .width(200.0)
9022                    .on_input(Msg::Edited),
9023            );
9024        let theme = pill_theme();
9025        let size = Size::new(300.0, 400.0);
9026        let ids: Vec<WidgetId> = build_ui(&tree, size, &Runtime::default(), &theme)
9027            .focusable_ids()
9028            .collect();
9029        assert_eq!(ids.len(), 2);
9030        let mut rt = Runtime::default();
9031        for id in &ids {
9032            rt.edits.insert(*id, selected(5, Some(0)));
9033        }
9034        // Focus follows the marked field; the other holds a selection too, unfocused.
9035        rt.input.focused = Some(ids[1]);
9036        rt.selection_toolbar = Some(crate::runtime::ToolbarMark {
9037            id: ids[1],
9038            can_paste: true,
9039            at: None,
9040        });
9041        let ui = build_ui(&tree, size, &rt, &theme);
9042        assert_eq!(ui.toolbars.len(), 1);
9043        let second = ui.widget_rect(ids[1]).expect("the second field");
9044        let bar = ui.toolbars[0];
9045        // Above the second field's line and below the first: it belongs to the second.
9046        assert!(
9047            bar.y + bar.height <= second.y + second.height,
9048            "{bar:?} vs {second:?}"
9049        );
9050        assert!(
9051            bar.y > 100.0,
9052            "not over the first field's row, which is at 150: {bar:?}"
9053        );
9054        // The bar is the *marked* field's: focus on the other one opens nothing.
9055        rt.input.focused = Some(ids[0]);
9056        let elsewhere = build_ui(&tree, size, &rt, &theme);
9057        assert!(elsewhere.toolbars.is_empty());
9058    }
9059
9060    /// **The paint cache tells a field with its bar open from one without** (milestone 568).
9061    /// The fingerprint of a cached subtree is the statuses inside it, so two statuses that
9062    /// differ only in the bar — or in the handles, which it had never held — must hash apart,
9063    /// or the cache would replay the frame from before the bar opened.
9064    #[test]
9065    fn the_bar_and_the_handles_are_part_of_a_fields_fingerprint() {
9066        let hashed = |status: &Status| {
9067            let mut hasher = std::collections::hash_map::DefaultHasher::new();
9068            hash_status(status, &mut hasher);
9069            hasher.finish()
9070        };
9071        let plain = Status::default();
9072        let open = Status {
9073            toolbar: Some(true),
9074            ..Status::default()
9075        };
9076        let open_without_paste = Status {
9077            toolbar: Some(false),
9078            ..Status::default()
9079        };
9080        let handled = Status {
9081            handles: true,
9082            ..Status::default()
9083        };
9084        assert_ne!(hashed(&plain), hashed(&open), "the bar opening");
9085        assert_ne!(
9086            hashed(&open),
9087            hashed(&open_without_paste),
9088            "the clipboard emptying"
9089        );
9090        assert_ne!(hashed(&plain), hashed(&handled), "the handles appearing");
9091    }
9092
9093    /// **A field knows the form it is part of** (milestone 512): the stops inside one
9094    /// [`crate::AutofillGroup`] are that form, in tree order, whichever of them asks; two
9095    /// groups side by side are two forms; a field in no group is a form of one; and a
9096    /// group sits in the focus registry, so a cached subtree replays it like any stop.
9097    #[test]
9098    fn a_field_knows_the_form_it_is_part_of() {
9099        use crate::{AutofillGroup, AutofillHint};
9100        let field = |value: &str, hint: AutofillHint| {
9101            TextField::new(value)
9102                .width(200.0)
9103                .on_input(Msg::Edited)
9104                .autofill([hint])
9105        };
9106        let tree = Flex::column()
9107            .width(300.0)
9108            .height(400.0)
9109            .child(AutofillGroup::new(
9110                Flex::column()
9111                    .child(field("someone", AutofillHint::Username))
9112                    .child(field("secret", AutofillHint::Password)),
9113            ))
9114            .child(AutofillGroup::new(field(
9115                "123456",
9116                AutofillHint::OneTimeCode,
9117            )))
9118            .child(TextField::new("search").width(200.0).on_input(Msg::Edited));
9119        let ui = build_ui(
9120            &tree,
9121            Size::new(300.0, 400.0),
9122            &Runtime::default(),
9123            &Theme::default(),
9124        );
9125        let ids: Vec<WidgetId> = ui.focusable_ids().collect();
9126        assert_eq!(ids.len(), 4);
9127        let form = |id: WidgetId| ui.form_of(id).iter().map(|(f, _)| *f).collect::<Vec<_>>();
9128        assert_eq!(form(ids[0]), vec![ids[0], ids[1]], "the sign-in pair");
9129        assert_eq!(
9130            form(ids[1]),
9131            vec![ids[0], ids[1]],
9132            "whichever of the two asks"
9133        );
9134        assert_eq!(
9135            form(ids[2]),
9136            vec![ids[2]],
9137            "a second group is a second form"
9138        );
9139        assert_eq!(
9140            form(ids[3]),
9141            vec![ids[3]],
9142            "a field in no group is a form of one"
9143        );
9144        assert!(ui.form_of(WidgetId::from_u64(7)).is_empty());
9145        // The two groups are told apart by identity, and a lone field has none.
9146        let (first, second) = (ui.form_group(ids[0]), ui.form_group(ids[2]));
9147        assert!(first.is_some() && second.is_some() && first != second);
9148        assert_eq!(ui.form_group(ids[3]), None);
9149        assert_eq!(ui.group_stops(first.unwrap()).len(), 2);
9150        // Each stop carries its box, which is what the platform anchors its offer to.
9151        assert!(ui.form_of(ids[1]).iter().all(|(_, r)| r.height > 0.0));
9152    }
9153
9154    #[test]
9155    fn find_widget_and_edit_types() {
9156        let tree = Flex::column()
9157            .width(300.0)
9158            .height(80.0)
9159            .child(TextField::new("hi").width(200.0).on_input(Msg::Edited));
9160        let rt = Runtime::default();
9161        let ui = build_ui(&tree, Size::new(300.0, 80.0), &rt, &Theme::default());
9162        let (id, _rect) = ui.focus_hit(Point::new(10.0, 10.0)).expect("the field");
9163
9164        let widget = find_widget(&tree, id).expect("the widget was found");
9165        let mut edit = Edit {
9166            cursor: 2,
9167            anchor: None,
9168            composing: None,
9169        };
9170        assert_eq!(
9171            widget.on_edit(&mut edit, &Key::Text("!".to_string())),
9172            Some(Msg::Edited("hi!".to_string()))
9173        );
9174    }
9175
9176    #[test]
9177    fn a_traversal_can_arrive_before_the_layout_pass() {
9178        // Milestone 415. A `ThemeBuilder`'s subtree does not exist until something has
9179        // called `build_themed` on it, and `children()` says so in its own doc: "a
9180        // traversal that arrives first should call `build_themed`, not this".
9181        //
9182        // The shell has such a traversal. On a **burst of keystrokes** — a software
9183        // keyboard, `adb input text`, auto-repeat — it rebuilds the tree from `view` alone
9184        // and reads it immediately, with no layout pass in between, so every deferred
9185        // subtree in it is empty and every widget inside one is unreachable.
9186        let deferred = |value: &str| {
9187            let value = value.to_string();
9188            crate::ThemeBuilder::new(move |_: &Theme| {
9189                TextField::new(value.clone())
9190                    .width(200.0)
9191                    .on_input(Msg::Edited)
9192            })
9193        };
9194        let laid_out = Flex::column()
9195            .width(300.0)
9196            .height(80.0)
9197            .child(deferred("hi"));
9198        let rt = Runtime::default();
9199        let ui = build_ui(&laid_out, Size::new(300.0, 80.0), &rt, &Theme::default());
9200        let (id, _) = ui.focus_hit(Point::new(10.0, 10.0)).expect("the field");
9201        assert!(
9202            find_widget(&laid_out, id).is_some(),
9203            "after a layout pass the field is where the hit-test said it was"
9204        );
9205
9206        // The same tree, freshly built and not yet laid out. `build_deferred` is what the
9207        // layout pass does on its way down, and nothing else.
9208        let fresh = Flex::column()
9209            .width(300.0)
9210            .height(80.0)
9211            .child(deferred("hi"));
9212        build_deferred(&fresh, &Theme::default(), &Runtime::default());
9213        assert!(
9214            find_widget(&fresh, id).is_some(),
9215            "prepared the same way, the same traversal reaches the same widget"
9216        );
9217    }
9218
9219    #[test]
9220    #[should_panic(expected = "before it was built")]
9221    fn reading_an_unbuilt_deferred_subtree_trips_the_wire() {
9222        // Without the preparation the traversal used to return `None` and the caller
9223        // shrugged: no field found, no caret revealed, nothing said. The failure mode was
9224        // **silence**, so the guard is a panic in debug rather than a comment.
9225        let fresh =
9226            Flex::<Msg>::column()
9227                .width(300.0)
9228                .height(80.0)
9229                .child(crate::ThemeBuilder::new(|_: &Theme| {
9230                    Container::<Msg>::new().width(10.0).height(10.0)
9231                }));
9232        let _ = find_widget(&fresh, WidgetId::ROOT.child(9));
9233    }
9234
9235    #[test]
9236    fn a_deferred_subtree_is_built_under_its_own_theme() {
9237        // The layout pass swaps the theme before it builds (`theme_override`), so a
9238        // builder inside a `Themed` sees the subtree's theme and not the frame's.
9239        // `build_deferred` has to make the same swap or it would build the wrong subtree
9240        // — and being *nearly* the walk is worse than not being it at all.
9241        let seen = std::rc::Rc::new(std::cell::Cell::new(0.0f32));
9242        let recorder = {
9243            let seen = seen.clone();
9244            crate::ThemeBuilder::new(move |theme: &Theme| {
9245                seen.set(theme.radius);
9246                Container::<Msg>::new().width(10.0).height(10.0)
9247            })
9248        };
9249        let inner = Theme {
9250            radius: 99.0,
9251            ..Theme::default()
9252        };
9253        let tree = Flex::<Msg>::column().child(crate::Themed::new(inner, recorder));
9254        build_deferred(&tree, &Theme::default(), &Runtime::default());
9255        assert_eq!(seen.get(), 99.0, "the subtree's theme, not the frame's");
9256    }
9257
9258    #[test]
9259    fn find_by_key_resolves_a_keyed_field_to_its_focus_id() {
9260        // Two named fields: `find_by_key` finds each one's focus identity (the one the
9261        // hit-test would assign), and it tells the keys apart.
9262        fn hash(k: &str) -> u64 {
9263            use std::hash::{Hash, Hasher};
9264            let mut h = std::collections::hash_map::DefaultHasher::new();
9265            k.hash(&mut h);
9266            h.finish()
9267        }
9268        let tree: Flex<Msg> = Flex::column()
9269            .child(Keyed::new(
9270                "email",
9271                TextField::new("").on_input(Msg::Edited),
9272            ))
9273            .child(Keyed::new(
9274                "password",
9275                TextField::new("").on_input(Msg::Edited),
9276            ));
9277
9278        let email = find_by_key(&tree, hash("email")).expect("email was found");
9279        let password = find_by_key(&tree, hash("password")).expect("password was found");
9280        assert_ne!(email, password, "distinct keys → distinct identities");
9281        // The identity resolved is indeed the by-key identity (stable, independent of the
9282        // position).
9283        assert_eq!(email, WidgetId::ROOT.keyed(hash("email")));
9284        assert!(
9285            find_by_key(&tree, hash("absent")).is_none(),
9286            "an unknown key → None"
9287        );
9288
9289        // And above all: it is **exactly** the identity the focus hit-test would assign to
9290        // that field — so setting this focus really does route to it.
9291        let rt = Runtime::default();
9292        let ui = build_ui(&tree, Size::new(300.0, 200.0), &rt, &Theme::default());
9293        let (hit_id, _) = ui
9294            .focus_hit(Point::new(10.0, 10.0))
9295            .expect("the email field under the cursor");
9296        assert_eq!(email, hit_id, "find_by_key == the field's focus identity");
9297    }
9298
9299    #[test]
9300    fn ink_is_painted_over_the_surface_and_under_its_child() {
9301        use crate::ink::InkWell;
9302        let tree = Container::<Msg>::new().width(120.0).height(60.0).child(
9303            InkWell::<Msg>::new().radius(8.0).on_click(Msg::A).child(
9304                Container::<Msg>::new()
9305                    .width(120.0)
9306                    .height(60.0)
9307                    .color(frus_core::Color::WHITE),
9308            ),
9309        );
9310        let size = Size::new(120.0, 60.0);
9311
9312        // Dry: the well paints nothing of its own.
9313        let rt = Runtime::default();
9314        let dry = build_ui(&tree, size, &rt, &Theme::default());
9315        let plain = dry.scene.len();
9316        let id = dry.inks.first().expect("the well registered its box").0;
9317        assert_eq!(
9318            dry.ink_box(id).map(|r| (r.width, r.height)),
9319            Some((120.0, 60.0)),
9320            "the registry carries the **whole** box, which is what sizes the splash"
9321        );
9322
9323        // A finger lands in the top-left corner, and is still down.
9324        let mut rt = Runtime::default();
9325        rt.input.pressed = Some(id);
9326        rt.ink_press(id, Point::new(10.0, 10.0), Size::new(120.0, 60.0));
9327        rt.advance_ink(1.0 / 60.0);
9328        let ui = build_ui(&tree, size, &rt, &Theme::default());
9329        assert_eq!(ui.scene.len(), plain + 1, "the splash adds one layer");
9330
9331        // It is a shape-clipped layer, so the ink cannot escape the rounded corners…
9332        let (index, clip_shape) = ui
9333            .scene
9334            .primitives()
9335            .iter()
9336            .enumerate()
9337            .find_map(|(i, p)| match p {
9338                frus_core::Primitive::Layer { clip_shape, .. } => Some((i, clip_shape.clone())),
9339                _ => None,
9340            })
9341            .expect("the ink is a composited layer");
9342        assert_eq!(clip_shape, frus_core::ClipShape::RRect(8.0.into()));
9343
9344        // …and it is painted **before** the child that sits on top of it: the ink is
9345        // under the content, the way a material surface holds it.
9346        let child_index = ui
9347            .scene
9348            .primitives()
9349            .iter()
9350            .rposition(|p| matches!(p, frus_core::Primitive::Rect { .. }))
9351            .expect("the child painted a rectangle");
9352        assert!(
9353            index < child_index,
9354            "ink at {index} must come before the content at {child_index}"
9355        );
9356    }
9357
9358    #[test]
9359    fn a_splash_left_unconfirmed_does_not_wait_for_ever() {
9360        // The finger came down and never came back up on this widget — it slid off, or
9361        // the widget went away mid-press. Nothing will ever confirm the splash, and
9362        // without the sweep in `advance_ink` the ink would sit there for good.
9363        let mut rt = Runtime::default();
9364        let id = WidgetId::from_u64(42);
9365        rt.input.pressed = Some(id);
9366        rt.ink_press(id, Point::new(5.0, 5.0), Size::new(100.0, 40.0));
9367        // Held for a fifth of a second: the ink is up, and it stays up.
9368        for _ in 0..12 {
9369            rt.advance_ink(1.0 / 60.0);
9370        }
9371        assert!(rt.ink.contains_key(&id), "the splash waits for the finger");
9372
9373        // The finger is gone and nothing confirmed the tap.
9374        rt.input.pressed = None;
9375        let mut frames = 0;
9376        while rt.advance_ink(1.0 / 60.0) && frames < 200 {
9377            frames += 1;
9378        }
9379        assert!(rt.ink.is_empty(), "the ink is gone after {frames} frames");
9380        assert!(frames <= 6, "and quickly: a cancel is 75 ms, took {frames}");
9381    }
9382
9383    #[test]
9384    fn an_overscroll_glow_is_painted_over_its_scroll_area() {
9385        let tree = SingleChildScrollView::<Msg>::new()
9386            .width(200.0)
9387            .height(100.0)
9388            .child(Container::<Msg>::new().width(100.0).height(400.0));
9389        let size = Size::new(200.0, 100.0);
9390
9391        // Quiet: nothing extra is drawn.
9392        let rt = Runtime::default();
9393        let plain = build_ui(&tree, size, &rt, &Theme::default()).scene.len();
9394
9395        // A fling has just landed on the bottom edge.
9396        let mut rt = Runtime::default();
9397        let id = build_ui(&tree, size, &rt, &Theme::default()).scroll_regions()[0].id;
9398        rt.glow_absorb(
9399            id,
9400            Some(crate::physics::OverscrollIndicator::Glow),
9401            crate::overscroll::GlowEdge::Bottom,
9402            4000.0,
9403        );
9404        rt.advance_glow(0.02);
9405        let ui = build_ui(&tree, size, &rt, &Theme::default());
9406        assert_eq!(
9407            ui.scene.len(),
9408            plain + 1,
9409            "the glow adds exactly one shape to the frame"
9410        );
9411        // The glow sits under the scrollbars, so it is not the last primitive: it is
9412        // the only filled path in the frame.
9413        let clip = ui
9414            .scene
9415            .primitives()
9416            .iter()
9417            .find_map(|p| match p {
9418                frus_core::Primitive::Path { clip, .. } => Some(*clip),
9419                _ => None,
9420            })
9421            .expect("the glow is a filled path");
9422        // Drawn against the bottom of the viewport, and inside it.
9423        assert!(
9424            (clip.y + clip.height - 100.0).abs() < 1.0,
9425            "clip = {clip:?}"
9426        );
9427        assert!(clip.width <= 200.0 + 1e-3);
9428    }
9429
9430    /// **A list's rows go through the same walk as the rest of the tree** (milestone 592): a
9431    /// carousel in each row is a scroll area of its own. The reduced walk the rows used to take
9432    /// knew no scroll area.
9433    #[test]
9434    fn a_list_row_is_walked_like_the_rest_of_the_tree() {
9435        let tree = crate::ListView::<Msg>::new(20, 60.0, |_| {
9436            SingleChildScrollView::<Msg>::new()
9437                .axis(crate::scroll::Axis::Horizontal)
9438                .height(60.0)
9439                .child(Container::<Msg>::new().width(900.0).height(60.0))
9440        })
9441        .width(300.0)
9442        .height(200.0);
9443        let ui = build_ui(
9444            &tree,
9445            Size::new(300.0, 200.0),
9446            &Runtime::default(),
9447            &Theme::default(),
9448        );
9449        let regions = ui.scroll_regions();
9450        assert!(
9451            regions.len() >= 4,
9452            "the list and a carousel per visible row: {}",
9453            regions.len()
9454        );
9455        assert!(
9456            regions.iter().any(|r| r.max_x > 0.0),
9457            "a row scrolls across: {regions:?}"
9458        );
9459    }
9460
9461    /// The stretch layers of a frame: their owner and their transform.
9462    fn stretch_layers(ui: &Ui<Msg>) -> Vec<(u64, frus_core::Affine)> {
9463        ui.scene
9464            .primitives()
9465            .iter()
9466            .filter_map(|p| match p {
9467                frus_core::Primitive::Layer {
9468                    transform: Some(transform),
9469                    owner,
9470                    ..
9471                } => Some((*owner, transform.affine)),
9472                _ => None,
9473            })
9474            .collect()
9475    }
9476
9477    /// **Where the indicator is the stretch, a pull deforms the content and draws no
9478    /// glow** (milestone 591): one layer holds the whole content, scaled down the axis
9479    /// from the top it is pulled past, and a button in it is touched where it is drawn.
9480    #[test]
9481    fn a_stretch_scales_the_content_from_the_edge_pulled() {
9482        let tree = SingleChildScrollView::<Msg>::new()
9483            .width(200.0)
9484            .height(100.0)
9485            .child(
9486                Flex::<Msg>::column()
9487                    .child(Container::<Msg>::new().width(100.0).height(50.0))
9488                    .child(crate::Button::new("Go").on_press(Msg::A))
9489                    .child(Container::<Msg>::new().width(100.0).height(400.0)),
9490            );
9491        let size = Size::new(200.0, 100.0);
9492        let mut rt = Runtime::default();
9493        let quiet = build_ui(&tree, size, &rt, &Theme::default());
9494        let id = quiet.scroll_regions()[0].id;
9495        assert!(
9496            stretch_layers(&quiet).is_empty(),
9497            "at rest, nothing is deformed"
9498        );
9499        let button = quiet
9500            .focusable_ids()
9501            .find_map(|b| quiet.widget_rect(b))
9502            .expect("the button");
9503
9504        rt.glow_pull(
9505            id,
9506            Some(crate::physics::OverscrollIndicator::Stretch),
9507            crate::overscroll::GlowEdge::Top,
9508            100.0,
9509            100.0,
9510            0.0,
9511            200.0,
9512        );
9513        assert!(
9514            rt.scroll_glow.is_empty(),
9515            "no glow where the content stretches"
9516        );
9517        let ui = build_ui(&tree, size, &rt, &Theme::default());
9518        let layers = stretch_layers(&ui);
9519        assert_eq!(layers.len(), 1, "{layers:?}");
9520        let (owner, m) = layers[0];
9521        assert_eq!(owner, id.as_u64());
9522        let top = m.apply(Point::new(0.0, 0.0));
9523        let low = m.apply(Point::new(200.0, 100.0));
9524        assert!(top.y.abs() < 1e-3, "the top holds: {top:?}");
9525        assert!((low.x - 200.0).abs() < 1e-3, "not across: {low:?}");
9526        assert!(low.y > 100.5, "down the axis: {low:?}");
9527        let moved = ui
9528            .focusable_ids()
9529            .find_map(|b| ui.widget_rect(b))
9530            .expect("the button");
9531        assert!(
9532            moved.y > button.y && moved.height > button.height,
9533            "touched where drawn: {button:?} -> {moved:?}"
9534        );
9535
9536        // Letting go springs it back, and the layer goes with it.
9537        rt.glow_scroll_end(id);
9538        while rt.advance_glow(1.0 / 60.0) {}
9539        assert!(rt.scroll_stretch.is_empty());
9540        assert!(stretch_layers(&build_ui(&tree, size, &rt, &Theme::default())).is_empty());
9541    }
9542
9543    /// A list stretches the same way, from the bottom when pulled past its end.
9544    #[test]
9545    fn a_list_stretches_from_its_end() {
9546        let tree = crate::ListView::<Msg>::new(50, 20.0, |i| crate::Text::new(format!("{i}")))
9547            .width(200.0)
9548            .height(100.0);
9549        let size = Size::new(200.0, 100.0);
9550        let mut rt = Runtime::default();
9551        let id = build_ui(&tree, size, &rt, &Theme::default()).scroll_regions()[0].id;
9552        rt.glow_absorb(
9553            id,
9554            Some(crate::physics::OverscrollIndicator::Stretch),
9555            crate::overscroll::GlowEdge::Bottom,
9556            4000.0,
9557        );
9558        rt.advance_glow(0.02);
9559        let ui = build_ui(&tree, size, &rt, &Theme::default());
9560        let layers = stretch_layers(&ui);
9561        assert_eq!(layers.len(), 1);
9562        let m = layers[0].1;
9563        assert!((m.apply(Point::new(0.0, 100.0)).y - 100.0).abs() < 1e-3);
9564        assert!(m.apply(Point::new(0.0, 0.0)).y < -0.5);
9565    }
9566
9567    /// **An area's own physics lie over its behaviour's** (milestone 649): never or always
9568    /// scrollable keeps the platform's feel, and a feel given keeps the platform's rule.
9569    #[test]
9570    fn an_area_s_physics_lie_over_its_behaviour_s() {
9571        use crate::physics::{ScrollFeel, UserScroll};
9572        use frus_core::TargetPlatform as P;
9573        let scroll = |physics: ScrollPhysics, height: f32| {
9574            SingleChildScrollView::new()
9575                .width(200.0)
9576                .height(100.0)
9577                .physics(physics)
9578                .child(Container::<Msg>::new().width(100.0).height(height))
9579        };
9580        let rt = Runtime::default();
9581        let _root = crate::ScrollConfiguration::install(crate::ScrollBehavior::material());
9582        let region = |tree: &dyn Widget<Msg>, platform: P| {
9583            let theme = Theme::default().with_platform(platform);
9584            build_ui(tree, Size::new(200.0, 100.0), &rt, &theme).scroll_regions()[0]
9585        };
9586        let never = region(&scroll(ScrollPhysics::NEVER, 400.0), P::Ios);
9587        assert_eq!(never.physics.user(), UserScroll::Never);
9588        assert_eq!(
9589            never.physics.feel(),
9590            ScrollFeel::Bouncing(crate::physics::ScrollDecelerationRate::Normal),
9591            "the platform's feel"
9592        );
9593        assert!(
9594            !never.accepts_user_offset((0.0, 0.0)),
9595            "content to reveal, and no finger"
9596        );
9597        let always = region(&scroll(ScrollPhysics::ALWAYS, 50.0), P::Android);
9598        assert_eq!(always.physics.feel(), ScrollFeel::Clamping);
9599        assert!(
9600            always.accepts_user_offset((0.0, 0.0)),
9601            "nothing to reveal, and a finger"
9602        );
9603        assert_eq!(always.axis, crate::scroll::Axis::Vertical);
9604        let bouncing = region(&scroll(ScrollPhysics::BOUNCING, 400.0), P::Android);
9605        assert_eq!(
9606            bouncing.physics,
9607            ScrollPhysics::BOUNCING,
9608            "a feel given, the default rule"
9609        );
9610    }
9611
9612    /// **What an area does is its scroll behaviour's, settled when it is registered**
9613    /// (milestone 616): under the application's behaviour it follows the theme's
9614    /// platform, a `ScrollConfiguration` changes it for its subtree, and the area's own
9615    /// physics win over both.
9616    #[test]
9617    fn a_scroll_area_carries_its_physics_into_the_registry() {
9618        use frus_core::TargetPlatform as P;
9619        let tall = || Container::<Msg>::new().width(100.0).height(400.0);
9620        let plain = || {
9621            SingleChildScrollView::new()
9622                .width(200.0)
9623                .height(100.0)
9624                .child(tall())
9625        };
9626        let rt = Runtime::default();
9627        let _root = crate::ScrollConfiguration::install(crate::ScrollBehavior::material());
9628        let region = |tree: &dyn Widget<Msg>, platform: P| {
9629            let theme = Theme::default().with_platform(platform);
9630            build_ui(tree, Size::new(200.0, 100.0), &rt, &theme).scroll_regions()[0]
9631        };
9632
9633        // Untold: the theme's platform decides, through the behaviour.
9634        let ios = region(&plain(), P::Ios);
9635        assert_eq!(ios.physics, ScrollPhysics::BOUNCING);
9636        assert_eq!(ios.overscroll, None, "nothing past the edge on iOS");
9637        assert_eq!(
9638            ios.fling,
9639            frus_core::VelocityStrategy::RecentAverage(frus_core::BOUNCING_FLING_WEIGHTS)
9640        );
9641        let android = region(&plain(), P::Android);
9642        assert_eq!(android.physics, ScrollPhysics::CLAMPING);
9643        assert_eq!(
9644            android.overscroll,
9645            Some(crate::OverscrollIndicator::Stretch)
9646        );
9647        assert_eq!(android.fling, frus_core::VelocityStrategy::Regression);
9648
9649        // A subtree under another behaviour scrolls as that one says.
9650        let configured = crate::ScrollConfiguration::new(
9651            crate::ScrollBehavior::material()
9652                .with_physics(ScrollPhysics::BOUNCING)
9653                .with_overscroll(false),
9654            plain(),
9655        );
9656        let inside = region(&configured, P::Android);
9657        assert_eq!(inside.physics, ScrollPhysics::BOUNCING);
9658        assert_eq!(inside.overscroll, None);
9659        // And only that subtree: the behaviour is put back after it.
9660        assert_eq!(
9661            crate::ScrollConfiguration::of(),
9662            crate::ScrollBehavior::material()
9663        );
9664
9665        // Told: the area's own choice wins over the behaviour's.
9666        let bouncy = SingleChildScrollView::new()
9667            .width(200.0)
9668            .height(100.0)
9669            .physics(ScrollPhysics::BOUNCING)
9670            .child(tall());
9671        let area = region(&bouncy, P::Android);
9672        assert_eq!(area.physics, ScrollPhysics::BOUNCING);
9673        // And the metrics it hands the physics describe the right axis.
9674        let metrics = area.metrics_y(10.0);
9675        assert_eq!(metrics.pixels, 10.0);
9676        assert_eq!(metrics.max, 300.0); // 400 of content in a 100 viewport
9677        assert_eq!(metrics.viewport, 100.0);
9678    }
9679
9680    #[test]
9681    fn scroll_translates_and_clips_content() {
9682        let content = Flex::<Msg>::column()
9683            .gap(0.0)
9684            .child(
9685                Container::new()
9686                    .height(60.0)
9687                    .color(Color::rgb(1.0, 0.0, 0.0)),
9688            )
9689            .child(
9690                Container::new()
9691                    .height(60.0)
9692                    .color(Color::rgb(0.0, 1.0, 0.0)),
9693            )
9694            .child(
9695                Container::new()
9696                    .height(60.0)
9697                    .color(Color::rgb(0.0, 0.0, 1.0)),
9698            );
9699        let tree = SingleChildScrollView::new()
9700            .width(200.0)
9701            .height(100.0)
9702            .child(content);
9703
9704        let rt = Runtime::default();
9705        let ui = build_ui(&tree, Size::new(200.0, 100.0), &rt, &Theme::default());
9706        let area = ui.scrollables[0];
9707        let sid = area.id;
9708        assert_eq!(area.max_y, 80.0); // 180 - 100
9709        assert_eq!(area.max_x, 0.0);
9710        assert_eq!(ui.first_rect().0.y, 0.0);
9711        assert_eq!(ui.first_rect().1, Rect::new(0.0, 0.0, 200.0, 100.0));
9712
9713        let mut rt = Runtime::default();
9714        rt.scroll.insert(sid, (0.0, 50.0));
9715        let ui2 = build_ui(&tree, Size::new(200.0, 100.0), &rt, &Theme::default());
9716        assert_eq!(ui2.first_rect().0.y, -50.0);
9717    }
9718
9719    #[test]
9720    fn kanban_cards_are_reorderable_without_being_clickable() {
9721        use crate::Kanban;
9722        let board = Kanban::new(|_, _, _, _| Msg::A).column("To do", ["Card A"]);
9723        let rt = Runtime::default();
9724        let ui = build_ui(&board, Size::new(400.0, 300.0), &rt, &Theme::default());
9725        // The card **and** the drop zone are registered as reorderables.
9726        assert!(
9727            ui.reorderables.len() >= 2,
9728            "the card + the drop zone are in the reorderables registry"
9729        );
9730        // A point on the card can be **picked up** (it is reorderable) but is **not clickable**
9731        // — which is the whole point of the registry: without it, `ui.hit` alone would never
9732        // find the card.
9733        let (card_id, card) = ui.reorderables[0];
9734        let p = Point::new(card.x + 5.0, card.y + 5.0);
9735        assert!(
9736            ui.reorderable_at(p).is_some(),
9737            "the card can be picked up at that point"
9738        );
9739        assert!(
9740            ui.hit(p).is_none(),
9741            "…but it is not clickable (absent from the hit registry)"
9742        );
9743        // `widget_rect` must find the card **through the reorderable fallback** (it is not
9744        // focusable): otherwise the shell's vertical drag preview never starts.
9745        assert_eq!(
9746            ui.widget_rect(card_id),
9747            Some(card),
9748            "widget_rect falls back to the reorderables registry"
9749        );
9750    }
9751
9752    #[test]
9753    fn reorderables_inside_a_scroll_are_still_registered() {
9754        // The milestone 258/260 scenario: the board (with its reorderable cards) is **wrapped
9755        // in a `SingleChildScrollView`**. The reorderables must stay registered — otherwise dragging a card
9756        // stops engaging as soon as the board scrolls.
9757        use crate::{Axis, Kanban, SingleChildScrollView};
9758        let board = Kanban::new(|_, _, _, _| Msg::A).column("To do", ["Card A"]);
9759        let scrolled = SingleChildScrollView::new()
9760            .axis(Axis::Horizontal)
9761            .width(400.0)
9762            .height(300.0)
9763            .child(board);
9764        let rt = Runtime::default();
9765        let ui = build_ui(&scrolled, Size::new(400.0, 300.0), &rt, &Theme::default());
9766        assert!(
9767            ui.reorderables.len() >= 2,
9768            "the card + the drop zone stay reorderable even inside a SingleChildScrollView (got {})",
9769            ui.reorderables.len()
9770        );
9771    }
9772
9773    #[test]
9774    fn reorderables_inside_a_per_column_card_scroll_are_still_registered() {
9775        // Milestone 264: `card_area_height` puts each column's cards inside a **vertical
9776        // `SingleChildScrollView` with an explicit height**. This is exactly the case that *collapsed* at
9777        // milestone 263 (a flex scroll with no defined ancestor height → cards clipped to zero
9778        // and no longer reorderable). With a **defined** height the visible cards must stay
9779        // registered as reorderables — a guard against a regression of per-column dragging.
9780        use crate::Kanban;
9781        let board = Kanban::new(|_, _, _, _| Msg::A)
9782            .card_area_height(220.0)
9783            .column("To do", ["Card A", "Card B"]);
9784        let rt = Runtime::default();
9785        let ui = build_ui(&board, Size::new(400.0, 300.0), &rt, &Theme::default());
9786        assert!(
9787            ui.reorderables.len() >= 3,
9788            "2 cards + the drop zone stay reorderable inside a vertical scroll with a defined height \
9789             (got {})",
9790            ui.reorderables.len()
9791        );
9792    }
9793
9794    #[test]
9795    fn scrollable_columns_fill_the_board_height_then_scroll() {
9796        // Milestone 266: `Kanban::scrollable_columns()` makes the columns **fill** the board's
9797        // height (laid out in an ancestor with a defined height), and each column scrolls its
9798        // cards vertically **with no explicit height** (the flex does the arithmetic, so the
9799        // milestone 264 stopgap is gone). This checks that at least one column has a vertical
9800        // `SingleChildScrollView` whose viewport **fills** the height (far more than the default 200) and
9801        // **scrolls** (max_y > 0), which is the proof of fill-then-scroll.
9802        use crate::{Axis, Container, Flex, Kanban, SingleChildScrollView};
9803        // Enough cards to **overflow** the filled viewport (otherwise max_y = 0: nothing to scroll).
9804        let long: Vec<String> = (0..24).map(|i| format!("card {i}")).collect();
9805        let board = Kanban::new(|_, _, _, _| Msg::A)
9806            .scrollable_columns()
9807            .column("To do", long);
9808        // `board_screen`'s nesting: the board inside a **plain `Container` with padding** (the
9809        // visual margin), itself inside a horizontal `flex(1)` SingleChildScrollView, inside a screen (a Flex
9810        // column) of bounded height. That `Auto` `Container` **collapsed** at milestone 266
9811        // (hence the `Flex` `flex(1)` workaround); since `compute_scroll` **fills the
9812        // constrained axis**, it fills the viewport's height and the board follows — no filler
9813        // container needed any more.
9814        let padded = Container::<Msg>::new().padding(24.0).child(board);
9815        let scroll_h = SingleChildScrollView::new()
9816            .axis(Axis::Horizontal)
9817            .width(360.0)
9818            .flex(1.0)
9819            .child(padded);
9820        let root: Flex<Msg> = Flex::column().width(400.0).height(600.0).child(scroll_h);
9821        let ui = build_ui(
9822            &root,
9823            Size::new(400.0, 600.0),
9824            &Runtime::default(),
9825            &Theme::default(),
9826        );
9827        // A column's **vertical** scroll: a tall viewport (it fills) and it scrolls.
9828        let filled = ui
9829            .scrollables
9830            .iter()
9831            .any(|area| area.viewport.height > 300.0 && area.max_y > 0.0);
9832        assert!(
9833            filled,
9834            "a column fills the board's height then scrolls (scrollables: {:?})",
9835            ui.scrollables
9836        );
9837    }
9838
9839    #[test]
9840    fn subtree_ids_covers_a_widget_and_its_descendants() {
9841        use crate::Text;
9842        // Root (Container) > Flex(column) > [Text, Text].
9843        let tree: Container<Msg> = Container::new().child(
9844            Flex::<Msg>::column()
9845                .child(Text::new("a"))
9846                .child(Text::new("b")),
9847        );
9848        let all = collect_ids(&tree);
9849        // From the root: identical to `collect_ids` (the same positional walk).
9850        assert_eq!(subtree_ids(&tree, WidgetId::ROOT), all);
9851        // A child's subtree starts with its own identity and stays a **strict** subset of the
9852        // tree (which is how a rich card's ghost captures all of its content).
9853        let flex_id = all[1];
9854        let sub = subtree_ids(Widget::children(&tree)[0].as_ref(), flex_id);
9855        assert_eq!(
9856            sub[0], flex_id,
9857            "the subtree starts with the identity supplied"
9858        );
9859        assert!(
9860            sub.len() < all.len() && sub.iter().all(|i| all.contains(i)),
9861            "a subset of the tree including the descendants"
9862        );
9863    }
9864
9865    impl<Msg: Clone> Ui<Msg> {
9866        fn first_rect(&self) -> (Rect, Rect) {
9867            for primitive in self.scene.primitives() {
9868                if let Primitive::Rect { rect, clip, .. } = primitive {
9869                    return (*rect, *clip);
9870                }
9871            }
9872            panic!("no rect at all");
9873        }
9874    }
9875
9876    /// A static subtree under `RepaintBoundary` (mixed content: text + boxes) — enough to
9877    /// produce several primitives.
9878    fn boundary_tree() -> Container<Msg> {
9879        use crate::Text;
9880        Container::new().repaint_boundary().child(
9881            Flex::<Msg>::column()
9882                .child(Text::new("Statique"))
9883                .child(Container::new().width(30.0).height(20.0).on_click(Msg::A))
9884                .child(Button::new("ok").on_press(Msg::B)),
9885        )
9886    }
9887
9888    #[test]
9889    fn repaint_boundary_reuses_a_static_subtree_bit_identical() {
9890        let tree = boundary_tree();
9891        let size = Size::new(200.0, 200.0);
9892        let theme = Theme::default();
9893        let rt = Runtime::default();
9894
9895        // Frame 1: nothing cached → 1 miss (a full paint), the subtree is captured.
9896        let ui1 = build_ui(&tree, size, &rt, &theme);
9897        // The **primitives** are what is compared (the scene's transient ambient state — the
9898        // current clip/owner — is not rendered).
9899        let dbg1 = format!("{:?}", ui1.scene().primitives());
9900        assert_eq!(rt.paint_cache.borrow().last_frame_stats(), (0, 1));
9901
9902        // Frame 2: the same generation + the same state → the boundary is reused.
9903        let ui2 = build_ui(&tree, size, &rt, &theme);
9904        let dbg2 = format!("{:?}", ui2.scene().primitives());
9905        assert_eq!(
9906            rt.paint_cache.borrow().last_frame_stats(),
9907            (1, 0),
9908            "the boundary is reused"
9909        );
9910        assert_eq!(
9911            dbg1, dbg2,
9912            "the replayed scene is bit-for-bit identical to the full repaint"
9913        );
9914        // The interaction maps are replayed too (a click stays routable).
9915        assert_eq!(ui1.hits.len(), ui2.hits.len());
9916        assert!(!ui2.hits.is_empty(), "the subtree's hit is indeed replayed");
9917    }
9918
9919    #[test]
9920    fn repaint_boundary_invalidated_by_generation_bump() {
9921        let tree = boundary_tree();
9922        let size = Size::new(200.0, 200.0);
9923        let theme = Theme::default();
9924        let rt = Runtime::default();
9925
9926        build_ui(&tree, size, &rt, &theme); // frame 1 : miss + capture
9927
9928        // The `view` is rebuilt (the config may have changed).
9929        rt.paint_cache.borrow_mut().bump_generation();
9930        build_ui(&tree, size, &rt, &theme); // frame 2
9931        assert_eq!(
9932            rt.paint_cache.borrow().last_frame_stats(),
9933            (0, 1),
9934            "a stale generation → a full repaint"
9935        );
9936    }
9937
9938    #[test]
9939    fn repaint_boundary_invalidated_by_interaction_change() {
9940        let tree = boundary_tree();
9941        let size = Size::new(200.0, 200.0);
9942        let theme = Theme::default();
9943        let mut rt = Runtime::default();
9944
9945        build_ui(&tree, size, &rt, &theme); // frame 1 : miss + capture
9946
9947        // No rebuild, but a descendant's interaction state changes (the boundary's animated
9948        // hover) → the fingerprint differs → a repaint.
9949        rt.anims.insert(
9950            WidgetId::ROOT,
9951            crate::Anim {
9952                hover: 0.5,
9953                ..Default::default()
9954            },
9955        );
9956        build_ui(&tree, size, &rt, &theme); // frame 2
9957        assert_eq!(
9958            rt.paint_cache.borrow().last_frame_stats(),
9959            (0, 1),
9960            "the interaction state changed → a full repaint"
9961        );
9962
9963        // Once the state has settled, the boundary is reused again.
9964        build_ui(&tree, size, &rt, &theme); // frame 3
9965        assert_eq!(rt.paint_cache.borrow().last_frame_stats(), (1, 0));
9966    }
9967}