Skip to main content

frus_widgets/
runtime.rs

1//! Runtime state retained between frames, **keyed by widget identity**.
2//!
3//! A field's *value* stays controlled (application state); what lives here is the
4//! widgets' own **interaction/edit** state: hover/focus, scroll offsets, and the
5//! cursor/selection position of fields. This is the foundation of reconciliation
6//! by identity (laid down at Milestone 6).
7
8use std::cell::RefCell;
9use std::collections::HashMap;
10
11use frus_core::{
12    BorderRadius, Color, Curve, Insets, Point, Primitive, Rect, Simulation, Size, TextStyle,
13};
14
15use crate::interaction::{InputState, WidgetId};
16use crate::overscroll::{edge_for, GlowEdge, ScrollGlows};
17use crate::physics::{Ballistic, ScrollPhysics};
18use crate::relayout::LayoutCache;
19use crate::ui::Scrollable;
20
21/// SingleChildScrollView offsets `(x, y)`, per scrollable region.
22pub type ScrollState = HashMap<WidgetId, (f32, f32)>;
23
24/// Edit state of an input field: cursor + selection anchor.
25///
26/// Indices are in **characters**. They may exceed the value's length (e.g.
27/// `usize::MAX` for "the end"): widgets clamp them at use.
28#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
29pub struct Edit {
30    /// Cursor position.
31    pub cursor: usize,
32    /// Selection anchor (`None` = no selection).
33    pub anchor: Option<usize>,
34    /// Range `(start, end)` **being composed** by the IME (provisional text,
35    /// underlined on screen); `None` outside composition. In character indices.
36    pub composing: Option<(usize, usize)>,
37}
38
39impl Edit {
40    /// Selected range `(start, end)`, non-empty, otherwise `None`.
41    pub fn selection_range(&self) -> Option<(usize, usize)> {
42        self.anchor
43            .map(|anchor| (anchor.min(self.cursor), anchor.max(self.cursor)))
44            .filter(|(start, end)| start < end)
45    }
46}
47
48/// Which field shows its selection bar, and what the clipboard held when it opened: the
49/// shell asks the platform once, on opening, not on every frame.
50#[derive(Copy, Clone, Debug, PartialEq)]
51pub struct ToolbarMark {
52    /// The field the bar acts on.
53    pub id: WidgetId,
54    /// Whether the clipboard held text when the bar opened, so whether Paste is offered.
55    pub can_paste: bool,
56    /// Where a right-click opened it, in the frame's coordinates: the bar goes there
57    /// rather than to the selection, as the reference's context menu goes to the last
58    /// secondary tap (milestone 618). `None` when it opened any other way.
59    pub at: Option<frus_core::Point>,
60}
61
62/// One of the two handles under a touch selection, in the field's **local** coordinates
63/// (px from its top-left corner, as on screen: any scroll of its content already applied).
64///
65/// The paint and the shell's hit test read the same answer, so a handle is taken
66/// exactly where it is drawn.
67#[derive(Copy, Clone, Debug, PartialEq)]
68pub struct SelectionHandle {
69    /// The box the handle is drawn in, hanging below its end of the selection.
70    pub rect: Rect,
71    /// The text position the handle stands for: its end of the selection, halfway down
72    /// the line. A handle hangs below its line, so a finger dragging it is aimed at the
73    /// line through this point rather than at where the finger is.
74    pub line_center: Point,
75}
76
77/// **Default** transition duration, in seconds. A widget can set its own through
78/// [`crate::widget::Widget::anim_duration`].
79pub(crate) const ANIM_DURATION: f32 = 0.12;
80
81/// The **press** transition's duration, in seconds (`ink_well.dart:998`).
82///
83/// Its own, and longer than [`ANIM_DURATION`], because the reference gives the press
84/// highlight 200 ms against the 50 ms it gives hover and focus (`ink_well.dart:995`).
85/// A press is the one interaction the pointer commits to, and it is drawn slowly enough
86/// to be seen arriving and leaving.
87pub(crate) const PRESS_DURATION: f32 = 0.2;
88
89/// The fling in flight on one scroll region: one simulation per axis, and the time
90/// elapsed since the finger let go.
91///
92/// The two axes share a clock — they were launched by the same gesture — and either
93/// may be `None`, since a fling usually runs on one axis only. The whole thing is
94/// `Copy`, so sampling it costs no allocation.
95#[derive(Clone, Copy, Debug, Default)]
96pub struct ScrollBallistic {
97    /// The fling along the x axis, if there is one.
98    pub x: Option<Ballistic>,
99    /// The fling along the y axis, if there is one.
100    pub y: Option<Ballistic>,
101    /// Seconds since the release.
102    pub elapsed: f32,
103}
104
105impl ScrollBallistic {
106    /// A fling on both axes at once (either may be `None`), starting now.
107    pub fn new(x: Option<Ballistic>, y: Option<Ballistic>) -> Self {
108        Self { x, y, elapsed: 0.0 }
109    }
110
111    /// Is there anything left to run?
112    pub fn is_empty(&self) -> bool {
113        self.x.is_none() && self.y.is_none()
114    }
115}
116
117/// How long a scrollbar waits, after its area stops moving, before it starts to go
118/// (`scrollbar.dart:18`).
119const BAR_TIME_TO_FADE: f32 = 0.6;
120/// How long a scrollbar takes to arrive, and to leave (`scrollbar.dart:17`).
121const BAR_FADE: f32 = 0.3;
122/// How long the thumb takes to reach its hovered colour (`scrollbar.dart:319`).
123const BAR_HOVER: f32 = 0.2;
124
125/// **The life of one area's scrollbar over time**: whether it is on screen at all, and
126/// how far the thumb has gone towards the colour it takes under a pointer.
127///
128/// A bar is not a permanent fixture. It arrives when the area moves, waits, and goes
129/// again — which is the whole of what makes it bearable to have one at all.
130#[derive(Copy, Clone, Debug, Default)]
131pub struct ScrollbarFade {
132    /// The offset this area was last seen at. The bar answers **movement**, and the
133    /// offset is written from the drag, the fling and the spring alike; watching the
134    /// value catches all three, and anything added later, without asking any of them
135    /// to remember to say so.
136    pub(crate) seen: (f32, f32),
137    /// Seconds since it last moved.
138    pub(crate) since: f32,
139    /// `0.0..=1.0` — how present the bar is.
140    pub opacity: f32,
141    /// `0.0..=1.0` — how far the thumb has gone towards its hovered colour.
142    pub hover: f32,
143}
144
145/// Stiffness of the scroll spring (px·s⁻²).
146const SCROLL_K: f32 = 200.0;
147/// Damping of the scroll spring.
148const SCROLL_C: f32 = 28.0;
149/// Elastic pull of the target back towards the valid bounds (per second) — the bounce.
150const SCROLL_RETRACT: f32 = 14.0;
151
152/// One scroll axis: elastic pull of the target back into `[0, max]`, then a spring
153/// from the current offset towards that target. Returns
154/// `(offset, velocity, target, moving)`.
155fn scroll_axis(current: f32, vel: f32, target: f32, max: f32, dt: f32) -> (f32, f32, f32, bool) {
156    let clamp_t = target.clamp(0.0, max);
157    // The target is pulled back towards the valid bound (overshoot → bounce).
158    let target = target + (clamp_t - target) * (1.0 - (-SCROLL_RETRACT * dt).exp());
159    let (offset, vel, _) = spring_step(current, vel, target, dt, SCROLL_K, SCROLL_C);
160    // Thresholds in pixels (spring_step is calibrated in fractions).
161    let moving = (offset - target).abs() > 0.5 || vel.abs() > 2.0 || (target - clamp_t).abs() > 0.5;
162    if moving {
163        (offset, vel, target, true)
164    } else {
165        (clamp_t, 0.0, clamp_t, false)
166    }
167}
168
169/// `offset` brought inside `[0, max]` on each axis.
170fn inside_extent(offset: (f32, f32), max: (f32, f32)) -> (f32, f32) {
171    (
172        offset.0.clamp(0.0, max.0.max(0.0)),
173        offset.1.clamp(0.0, max.1.max(0.0)),
174    )
175}
176
177/// A widget's animation progresses (`0.0..=1.0`).
178#[derive(Copy, Clone, Debug, PartialEq)]
179pub struct Anim {
180    /// The hover transition's progress.
181    pub hover: f32,
182    /// The focus transition's progress.
183    pub focus: f32,
184    /// The press (0 at rest), on its own clock: 200 ms against the 120 the other two
185    /// run on, because the reference gives the press highlight four times the fade it
186    /// gives hover and focus (`ink_well.dart:995`).
187    pub press: f32,
188    /// Opacity (1 at rest; started at 0 on mount for the fade-in).
189    pub opacity: f32,
190}
191
192impl Default for Anim {
193    fn default() -> Self {
194        Self {
195            hover: 0.0,
196            focus: 0.0,
197            press: 0.0,
198            opacity: 1.0,
199        }
200    }
201}
202
203/// **Timeline** of an implicitly animated value (`Widget::anim_target`):
204/// interpolates `from → to` according to the widget's curve and duration.
205/// `current` is the value handed to the paint. A change of target **rebases** the
206/// timeline from the current value (a clean, continuous restart).
207#[derive(Copy, Clone, Debug, PartialEq)]
208pub struct ValueAnim {
209    /// Current interpolated value (what the paint reads).
210    pub current: f32,
211    /// Starting value of the transition in progress.
212    from: f32,
213    /// Target of the transition in progress.
214    to: f32,
215    /// Time elapsed (s) since the transition started.
216    elapsed: f32,
217}
218
219impl ValueAnim {
220    /// A value **at rest** at `v` (no transition in progress).
221    fn settled(v: f32) -> Self {
222        Self {
223            current: v,
224            from: v,
225            to: v,
226            elapsed: 0.0,
227        }
228    }
229}
230
231/// Timeline of an animated **colour** (`Container::animated_color`): interpolates
232/// `from → to` per channel, according to the widget's curve and duration. Same
233/// rebase model as [`ValueAnim`], applied to a colour.
234#[derive(Copy, Clone, Debug, PartialEq)]
235struct ColorAnim {
236    current: Color,
237    from: Color,
238    to: Color,
239    elapsed: f32,
240}
241
242impl ColorAnim {
243    fn settled(c: Color) -> Self {
244        Self {
245            current: c,
246            from: c,
247            to: c,
248            elapsed: 0.0,
249        }
250    }
251}
252
253/// Timeline of an animated **text style** (`AnimatedDefaultTextStyle`): interpolates
254/// `from → to` field by field, according to the widget's curve and duration.
255///
256/// Unlike every other family here, what it drives is not the node's own box or paint but
257/// **the theme its subtree inherits** — so it is consumed by the theme swap, at every
258/// point of the walk that makes one. See `crate::ui::scoped_theme`.
259#[derive(Copy, Clone, Debug, PartialEq)]
260struct TextStyleAnim {
261    current: TextStyle,
262    from: TextStyle,
263    to: TextStyle,
264    elapsed: f32,
265}
266
267impl TextStyleAnim {
268    fn settled(style: TextStyle) -> Self {
269        Self {
270            current: style,
271            from: style,
272            to: style,
273            elapsed: 0.0,
274        }
275    }
276}
277
278/// Timeline of an animated **size** (`Container::animated_size`): interpolates
279/// `from → to` (width/height) according to the widget's curve and duration. The
280/// interpolated size is injected **at layout** (not at paint) through `effective_style`.
281#[derive(Copy, Clone, Debug, PartialEq)]
282struct SizeAnim {
283    current: Size,
284    from: Size,
285    to: Size,
286    elapsed: f32,
287}
288
289impl SizeAnim {
290    fn settled(s: Size) -> Self {
291        Self {
292            current: s,
293            from: s,
294            to: s,
295            elapsed: 0.0,
296        }
297    }
298}
299
300/// Linear interpolation of two sizes (component-wise).
301fn lerp_size(a: Size, b: Size, t: f32) -> Size {
302    Size::new(
303        a.width + (b.width - a.width) * t,
304        a.height + (b.height - a.height) * t,
305    )
306}
307
308/// Timeline of an animated **corner radius** (`Container::animated_radius`):
309/// interpolates `from → to` (all four corners) according to the widget's curve and
310/// duration. A **paint** property: delivered to the paint through `Status::anim_radius`.
311#[derive(Copy, Clone, Debug, PartialEq)]
312struct RadiusAnim {
313    current: BorderRadius,
314    from: BorderRadius,
315    to: BorderRadius,
316    elapsed: f32,
317}
318
319impl RadiusAnim {
320    fn settled(r: BorderRadius) -> Self {
321        Self {
322            current: r,
323            from: r,
324            to: r,
325            elapsed: 0.0,
326        }
327    }
328}
329
330/// Timeline of an animated **padding** (`Container::animated_padding`): interpolates
331/// `from → to` (all four sides) according to the widget's curve and duration. The
332/// interpolated padding is injected **at layout** (`effective_style`), like the size.
333#[derive(Copy, Clone, Debug, PartialEq)]
334struct PaddingAnim {
335    current: Insets,
336    from: Insets,
337    to: Insets,
338    elapsed: f32,
339}
340
341impl PaddingAnim {
342    fn settled(p: Insets) -> Self {
343        Self {
344            current: p,
345            from: p,
346            to: p,
347            elapsed: 0.0,
348        }
349    }
350}
351
352/// Timeline of an animated **offset pair** (`Container::animated_alignment`,
353/// `FractionalTranslation::animated`): the two fractions a node's offset rule is made of,
354/// interpolated together so a child moving diagonally arrives on both axes at once.
355#[derive(Copy, Clone, Debug, PartialEq)]
356struct OffsetAnim {
357    current: (f32, f32),
358    from: (f32, f32),
359    to: (f32, f32),
360    elapsed: f32,
361}
362
363impl OffsetAnim {
364    fn settled(o: (f32, f32)) -> Self {
365        Self {
366            current: o,
367            from: o,
368            to: o,
369            elapsed: 0.0,
370        }
371    }
372}
373
374/// Timeline of a bundle of **optional** numbers a node hands to whatever lays it out —
375/// the four edges and two extents of a [`crate::Positioned`], the two factors of a
376/// [`crate::FractionallySizedBox`]. One timeline for the whole bundle, so a box that
377/// changes two of its edges arrives on both at once, the way a scale and a turn do.
378///
379/// **A slot that is unset is not a number, and so is not animated.** `None` on an edge
380/// does not mean nought there — it means the box is not pinned on that side at all, and
381/// the two are different arrangements rather than two values of one. So a slot arriving
382/// takes effect at once, a slot leaving goes at once, and only a slot set at both ends
383/// travels. The reference does exactly this and for the same reason: its tween for such
384/// a value is dropped the moment the value is null.
385#[derive(Copy, Clone, Debug, PartialEq)]
386struct SlotsAnim<const N: usize> {
387    current: [Option<f32>; N],
388    from: [Option<f32>; N],
389    to: [Option<f32>; N],
390    elapsed: f32,
391}
392
393impl<const N: usize> SlotsAnim<N> {
394    fn settled(v: [Option<f32>; N]) -> Self {
395        Self {
396            current: v,
397            from: v,
398            to: v,
399            elapsed: 0.0,
400        }
401    }
402}
403
404/// Walks the tree collecting one family of slot bundles, following the walk's own
405/// identity scheme (`child_id`) exactly — an id computed any other way is an animation
406/// the paint never finds.
407fn collect_slots<Msg, const N: usize>(
408    widget: &dyn crate::widget::Widget<Msg>,
409    id: WidgetId,
410    still: bool,
411    out: &mut Vec<(WidgetId, [Option<f32>; N], f32, Curve)>,
412    read: impl Fn(&dyn crate::widget::Widget<Msg>) -> Option<[Option<f32>; N]> + Copy,
413) {
414    if let Some(target) = read(widget) {
415        out.push((
416            id,
417            target,
418            if still {
419                0.0
420            } else {
421                widget.anim_duration().max(0.0)
422            },
423            widget.anim_curve(),
424        ));
425    }
426    for (index, child) in widget.children().iter().enumerate() {
427        collect_slots(
428            child.as_ref(),
429            crate::ui::child_id(id, index, child.as_ref()),
430            still,
431            out,
432            read,
433        );
434    }
435}
436
437/// Steps one family of slot bundles towards the targets their widgets declare. Shared by
438/// every quantity made of optional numbers, so the rule about an unset slot is written
439/// once and cannot come out differently in two places.
440fn advance_slot_family<const N: usize>(
441    map: &mut HashMap<WidgetId, SlotsAnim<N>>,
442    targets: Vec<(WidgetId, [Option<f32>; N], f32, Curve)>,
443    dt: f32,
444) -> bool {
445    let present: std::collections::HashSet<WidgetId> = targets.iter().map(|(id, ..)| *id).collect();
446    map.retain(|id, _| present.contains(id));
447
448    let mut animating = false;
449    for (id, target, duration, curve) in targets {
450        match map.entry(id) {
451            std::collections::hash_map::Entry::Occupied(mut e) => {
452                let s = e.get_mut();
453                if s.to != target {
454                    s.from = s.current;
455                    s.to = target;
456                    s.elapsed = 0.0;
457                    // A slot with nothing at one end has nowhere to travel from or to, so
458                    // it is already where it is going.
459                    for i in 0..N {
460                        if s.from[i].is_none() || s.to[i].is_none() {
461                            s.from[i] = s.to[i];
462                        }
463                    }
464                }
465                if s.from == s.to {
466                    s.current = s.to;
467                } else {
468                    s.elapsed += dt;
469                    let t = if duration > 0.0 {
470                        (s.elapsed / duration).clamp(0.0, 1.0)
471                    } else {
472                        1.0
473                    };
474                    let e = curve.transform(t);
475                    for i in 0..N {
476                        s.current[i] = match (s.from[i], s.to[i]) {
477                            (Some(a), Some(b)) => Some(a + (b - a) * e),
478                            _ => s.to[i],
479                        };
480                    }
481                    if t < 1.0 {
482                        animating = true;
483                    }
484                }
485            }
486            std::collections::hash_map::Entry::Vacant(e) => {
487                e.insert(SlotsAnim::settled(target));
488            }
489        }
490    }
491    animating
492}
493
494/// **The numbers a paint-time transform is made of** that can sensibly be interpolated:
495/// a scale on each axis and a turn.
496///
497/// The **pivot** is not here, and that is the decision worth stating: it is a choice of
498/// origin, not a quantity. Interpolating it would move a shape across the screen while
499/// every number describing the shape stayed still, which is a thing no caller ever asks
500/// for and a thing nobody would be able to debug. A widget that wants the pivot to change
501/// has changed what it is doing, not how much of it.
502#[derive(Copy, Clone, Debug, PartialEq)]
503pub struct TransformValues {
504    /// Horizontal scale; `1.0` is neutral.
505    pub scale_x: f32,
506    /// Vertical scale; `1.0` is neutral.
507    pub scale_y: f32,
508    /// Clockwise turn, in radians.
509    pub rotation: f32,
510}
511
512impl Default for TransformValues {
513    /// The identity — **not** zeroes. A scale of nought is a shape with no area, so a
514    /// transform mounting at `Default` would flash the subtree out of existence.
515    fn default() -> Self {
516        Self {
517            scale_x: 1.0,
518            scale_y: 1.0,
519            rotation: 0.0,
520        }
521    }
522}
523
524/// Timeline of an animated **transform** (`Transform::animated`): interpolates
525/// `from → to` on both scales and the turn at once, so a widget that scales and rotates
526/// together arrives on both at the same moment.
527#[derive(Copy, Clone, Debug, PartialEq)]
528struct TransformAnim {
529    current: TransformValues,
530    from: TransformValues,
531    to: TransformValues,
532    elapsed: f32,
533}
534
535impl TransformAnim {
536    fn settled(v: TransformValues) -> Self {
537        Self {
538            current: v,
539            from: v,
540            to: v,
541            elapsed: 0.0,
542        }
543    }
544}
545
546/// Linear interpolation of two transforms.
547///
548/// A scale is interpolated **linearly**, not geometrically. Halfway between 1× and 4× is
549/// 2.5× here rather than 2×, which is what the reference's own `Tween<double>` gives and
550/// what a caller reading the two numbers expects; a geometric mean would be defensible
551/// and would disagree with every other implicit animation in the framework.
552fn lerp_transform(a: TransformValues, b: TransformValues, t: f32) -> TransformValues {
553    let mix = |x: f32, y: f32| x + (y - x) * t;
554    TransformValues {
555        scale_x: mix(a.scale_x, b.scale_x),
556        scale_y: mix(a.scale_y, b.scale_y),
557        rotation: mix(a.rotation, b.rotation),
558    }
559}
560
561/// Linear interpolation of two insets (per side).
562fn lerp_insets(a: Insets, b: Insets, t: f32) -> Insets {
563    let mix = |x: f32, y: f32| x + (y - x) * t;
564    Insets::new(
565        mix(a.top, b.top),
566        mix(a.right, b.right),
567        mix(a.bottom, b.bottom),
568        mix(a.left, b.left),
569    )
570}
571
572/// Linear interpolation of two radii (per corner) — the one rule, `BorderRadius::lerp`,
573/// so that an animated container and a decoration transition cannot round differently.
574fn lerp_radius(a: BorderRadius, b: BorderRadius, t: f32) -> BorderRadius {
575    a.lerp(b, t)
576}
577
578/// One damped-spring step (semi-implicit Euler) driving `progress` towards
579/// `target`, primed by `velocity`. `stiffness`/`damping` set the stiffness and the
580/// damping (≈ `2·√stiffness` = critical damping, no overshoot).
581/// Returns `(progress, velocity, at_rest)`.
582///
583/// A general-purpose motion helper: used by screen transitions and by gestures
584/// (a settle primed by the finger's velocity).
585pub fn spring_step(
586    progress: f32,
587    velocity: f32,
588    target: f32,
589    dt: f32,
590    stiffness: f32,
591    damping: f32,
592) -> (f32, f32, bool) {
593    let accel = stiffness * (target - progress) - damping * velocity;
594    let velocity = velocity + accel * dt;
595    let progress = progress + velocity * dt;
596    let at_rest = (progress - target).abs() < 0.004 && velocity.abs() < 0.06;
597    (progress, velocity, at_rest)
598}
599
600/// A **spring** curve (the step response of a **critically** damped spring)
601/// remapping a linear progress `t ∈ [0,1]`: starts at rest (zero slope), rises
602/// briskly, arrives gently **without overshoot** — the same feel as the screen
603/// transitions, but in closed form (no velocity state). `f(0) = 0`, `f(1) = 1`,
604/// monotonically increasing.
605pub fn spring_ease(t: f32) -> f32 {
606    // The critical response (`omega = 8`), now provided by `frus-core`'s shared
607    // animation layer: a single source of truth for this curve.
608    frus_core::Curve::critical_spring().transform(t)
609}
610
611/// Drives `value` towards `target` in steps of `step`; records whether it is still moving.
612fn approach(value: &mut f32, target: f32, step: f32, animating: &mut bool) {
613    if *value < target {
614        *value = (*value + step).min(target);
615    } else if *value > target {
616        *value = (*value - step).max(target);
617    }
618    if (*value - target).abs() > 1e-3 {
619        *animating = true;
620    }
621}
622
623/// Runtime context handed to `build_ui`: all the state retained between frames.
624#[derive(Default)]
625pub struct Runtime {
626    /// What every [`Component`](crate::Component) keeps between rebuilds: the state of a
627    /// [`StatefulWidget`](crate::StatefulWidget), and the values a component's hooks hold.
628    pub states: crate::component::StateStore,
629    /// **Motion is off**: the user has asked for animations to be reduced or removed,
630    /// and the shell has passed that on from the platform.
631    ///
632    /// The **implicit** animations — the ones a widget starts by itself when a value it
633    /// was given changes — complete at once instead of over time. The change still
634    /// happens; it stops moving, which is what the setting asks for. Skipping the change
635    /// as well would leave the interface **wrong** rather than still.
636    ///
637    /// It does not touch scrolling. A fling is physics answering a finger, not a
638    /// decoration, and a list that jumped to a stop would be harder to use rather than
639    /// calmer — the reference draws the same line.
640    pub still: bool,
641    /// **How present each area's scrollbar is**, and how close a pointer has come to
642    /// it. Advanced by [`Runtime::advance_scroll`], which is where movement is seen.
643    pub scrollbar_fade: HashMap<WidgetId, ScrollbarFade>,
644    /// The area whose scrollbar a **mouse is near**, set by the shell from the previous
645    /// frame's registry. A bar that has gone comes back for a pointer that reaches for
646    /// it — otherwise a faded bar could never be grabbed again (`scrollbar.dart:2132`).
647    pub scrollbar_hovered: Option<WidgetId>,
648    /// The area whose scrollbar is **being dragged**. It stays while the thumb is held,
649    /// however long the hand pauses (`scrollbar.dart:1970`).
650    pub scrollbar_dragged: Option<WidgetId>,
651    /// Hover / press / focus.
652    pub input: InputState,
653    /// **Current** scroll offsets (the rendered ones), per region.
654    pub scroll: ScrollState,
655    /// **Target** scroll offsets (what the spring drives towards), per region.
656    pub scroll_target: ScrollState,
657    /// SingleChildScrollView velocity (for the spring), per region.
658    pub scroll_velocity: ScrollState,
659    /// The **ballistic** motion still running after a fling, per region — the
660    /// simulation the platform's physics handed us, sampled frame by frame by
661    /// [`Runtime::advance_scroll`]. Absent = no fling in flight.
662    pub scroll_ballistic: HashMap<WidgetId, ScrollBallistic>,
663    /// The overscroll glows of each region — the edge feedback a platform that
664    /// clamps needs, since it has no bounce to speak with. Absent = all quiet.
665    pub scroll_glow: HashMap<WidgetId, ScrollGlows>,
666    /// The overscroll **stretch** of each region, where the indicator is the stretch
667    /// rather than the glow (milestone 591). Absent = at rest.
668    pub scroll_stretch: HashMap<WidgetId, crate::stretch::ScrollStretch>,
669    /// The **ink** splashed on each surface that takes it, keyed by the surface. A
670    /// ripple outlives the frame that started it — a tap is 450 ms of motion — so it
671    /// is retained here rather than in the widget, which is rebuilt every frame.
672    /// Absent = dry.
673    pub ink: HashMap<WidgetId, crate::ink::Ripples>,
674    /// The retained pull of each [`crate::RefreshIndicator`] area, keyed by the area. Absent =
675    /// nothing pulled and nothing spinning.
676    pub refresh: HashMap<WidgetId, crate::refresh::RefreshPull>,
677    /// The retained swipe of each [`crate::Dismissible`] item. Absent = at rest.
678    pub dismiss: HashMap<WidgetId, crate::dismiss::DismissState>,
679    /// The retained height of each [`crate::DraggableScrollableSheet`], keyed by its
680    /// panel. Absent = never moved, and at its initial height.
681    pub sheets: HashMap<WidgetId, crate::sheet::SheetState>,
682    /// Throws that carried a sheet to full height since they were last taken: the list
683    /// each goes on in and the velocity it goes on at, in px/s. See
684    /// [`Runtime::take_sheet_handovers`].
685    pub sheet_handovers: Vec<(WidgetId, f32)>,
686    /// The page each [`crate::PageView`] was last **told to show**, so that a request
687    /// is acted on when it *changes* rather than re-asserted every frame — which
688    /// would leave the offset unswipeable. Absent = never seen, so the next request
689    /// is the initial page and arrives without an animation.
690    pub page_requested: HashMap<WidgetId, usize>,
691    /// The box each scroll region was last asked to **keep in view**, by the key the
692    /// content named it with, so that the request is acted on when it *changes* rather
693    /// than re-asserted every frame — which would pin the content and leave it
694    /// undraggable. Absent = never seen, and the walk has already opened there.
695    pub kept_visible: HashMap<WidgetId, u64>,
696    /// The page each paged view was last **reported as showing**, so that
697    /// `on_page_changed` fires on a change and not on every frame of the motion.
698    pub page_shown: HashMap<WidgetId, usize>,
699    /// The offset each scroll region was last **reported at**, so that
700    /// [`crate::Widget::on_scroll`] fires on a change and not on every frame, and so
701    /// that a region's grain is measured from what the application was actually told
702    /// rather than from the previous frame — otherwise a slow drag would never travel
703    /// a whole grain in one frame and would never be reported at all.
704    pub scroll_reported: HashMap<WidgetId, (f32, f32)>,
705    /// The drop target a drag is currently **over**, when it would accept it. The
706    /// target paints its own "drop it here" state from this, through
707    /// [`crate::interaction::Status::drag_over`] — the shell decides *which* target,
708    /// the widget decides what that looks like.
709    pub drag_over: Option<WidgetId>,
710    /// The region a finger is currently holding, if any.
711    ///
712    /// A scroll offset has **one owner at a time**. While a drag owns it, nothing
713    /// else may move it: without this, the edge spring keeps retracting the offset
714    /// between two moves of the finger, and a rubber band pulled against a bouncing
715    /// edge is dragged back as fast as it is stretched — it never appears at all.
716    /// At most one, because at most one gesture is live.
717    pub scroll_held: Option<WidgetId>,
718    /// Retained transform (scale + translation) of each
719    /// [`InteractiveViewer`](crate::InteractiveViewer), per viewport. Absent =
720    /// identity (scale 1, no translation).
721    pub interactive: HashMap<WidgetId, crate::interactive::InteractiveView>,
722    /// Pan velocity (px/s) of a *fling* still running after release, per viewport —
723    /// decelerated every frame by [`Runtime::advance_interactive`]. Absent = at rest.
724    pub interactive_velocity: HashMap<WidgetId, (f32, f32)>,
725    /// Edit state, per input field.
726    pub edits: HashMap<WidgetId, Edit>,
727    /// The field whose selection shows **handles** — one made with a finger (milestone
728    /// 511). Put away by the next press or key; shown only while that field is focused.
729    pub selection_handles: Option<WidgetId>,
730    /// The field whose **bar** — Cut, Copy, Paste, Select all — is showing over its
731    /// selection (milestone 568), with whether the clipboard held text when it opened.
732    /// Put away by the next press elsewhere or key; shown only while that field is focused.
733    pub selection_toolbar: Option<ToolbarMark>,
734    /// The selection a [`SelectionArea`](crate::SelectionArea) holds, if it holds one: kept
735    /// by the shell, read by the walk to paint the highlight. At most one — a press outside
736    /// the area, or in another, puts it away.
737    pub region: Option<crate::RegionSelection>,
738    /// The focused field's caret is in the **hidden** half of its blink (milestone 513).
739    /// Kept by the shell, on the wall clock; `false` — shown — wherever nothing blinks it,
740    /// which is every test and every picture.
741    pub caret_hidden: bool,
742    /// **Undo history**, per input field: what it was, and what it was about to be.
743    ///
744    /// Beside `edits` rather than inside it, because they are different things kept for
745    /// different reasons — a caret is where the field is now, a history is everywhere it
746    /// has been — and because `Edit` is `Copy` and a history is not.
747    pub undo: HashMap<WidgetId, crate::undo::UndoHistory>,
748    /// Animation progresses (hover/focus/opacity), per widget.
749    pub anims: HashMap<WidgetId, Anim>,
750    /// The widgets' own animated values (`Widget::anim_target`), per widget — each
751    /// one a curved **timeline** (see [`ValueAnim`]).
752    pub values: HashMap<WidgetId, ValueAnim>,
753    /// Values a drag is **holding** where the finger put them (milestone 638): not advanced
754    /// towards their target while the finger is down.
755    held_values: std::collections::HashSet<WidgetId>,
756    /// Values a drag **placed**: held, or settling from where the finger left them. They
757    /// travel linearly, in the time the distance left takes, and the paint is told so.
758    placed_values: std::collections::HashSet<WidgetId>,
759    /// Animated background colours (`Container::animated_color`), per widget.
760    colors: HashMap<WidgetId, ColorAnim>,
761    /// Animated inherited text styles (`AnimatedDefaultTextStyle`), per widget — read by
762    /// the **theme swap**, which is why this one is consumed nowhere near the others.
763    text_styles: HashMap<WidgetId, TextStyleAnim>,
764    /// Animated sizes (`Container::animated_size`), per widget — injected at layout.
765    sizes: HashMap<WidgetId, SizeAnim>,
766    /// Animated corner radii (`Container::animated_radius`), per widget.
767    radii: HashMap<WidgetId, RadiusAnim>,
768    /// Animated paddings (`Container::animated_padding`), per widget — injected at layout.
769    paddings: HashMap<WidgetId, PaddingAnim>,
770    /// Animated offset pairs (an alignment, a slide), per widget — read at paint.
771    offsets: HashMap<WidgetId, OffsetAnim>,
772    /// Animated transforms (`Transform::animated`), per widget — read at paint.
773    transforms: HashMap<WidgetId, TransformAnim>,
774    /// Animated stack pins (`AnimatedPositioned`), per widget — read where the stack
775    /// lays its layers out. Six slots: left, top, right, bottom, width, height.
776    pins: HashMap<WidgetId, SlotsAnim<8>>,
777    /// Animated fractions of the parent (`FractionallySizedBox::animated`), per widget —
778    /// injected at layout. Two slots: the width factor and the height factor.
779    fractions: HashMap<WidgetId, SlotsAnim<2>>,
780    /// Widgets present at the previous frame (to detect mounts).
781    pub mounted: std::collections::HashSet<WidgetId>,
782    /// Snapshots of outgoing subtrees, fading out: event key → (captured
783    /// primitives, remaining opacity `1 → 0`).
784    pub leaving: HashMap<u64, (Vec<Primitive>, f32)>,
785    /// What each [`AnimatedSwitcher`](crate::AnimatedSwitcher) is showing and letting go
786    /// of, per widget. Interior mutability, like the caches below: a switcher meets a new
787    /// value while the tree is being **built**, which is the only time it has one.
788    pub(crate) switchers: RefCell<HashMap<WidgetId, crate::switcher::Switch>>,
789    /// Time elapsed (seconds) since start-up, for continuous animations.
790    pub time: f32,
791    /// Was the last interaction a **keyboard** one? The generic focus ring is only
792    /// painted in that case (`FocusHighlightMode`: a click must not flash a ring).
793    /// The focus itself stays active.
794    pub focus_visible: bool,
795    /// Relayout-boundary cache (rects retained per layout root, from one frame to
796    /// the next). Interior mutability: `build_ui` updates it while holding only a
797    /// shared reference to the `Runtime`.
798    pub layout_cache: RefCell<LayoutCache>,
799    /// **Repaint**-boundary cache (primitives + interactions retained per boundary,
800    /// from one frame to the next). Same interior mutability.
801    pub paint_cache: RefCell<crate::paintcache::PaintCache>,
802    /// Where each [`crate::AnimatedSize`] box is on its way from one size to the next
803    /// (milestone 594). Behind a `RefCell` for the same reason as the layout cache: the
804    /// layout sees a child's new size, and it holds only a shared reference.
805    pub(crate) size_anims: RefCell<crate::animatedsize::SizeAnims>,
806}
807
808impl Runtime {
809    /// A widget's animated hover progress.
810    pub fn hover_progress(&self, id: WidgetId) -> f32 {
811        self.anims.get(&id).map(|a| a.hover).unwrap_or(0.0)
812    }
813
814    /// A widget's animated focus progress.
815    pub fn focus_progress(&self, id: WidgetId) -> f32 {
816        self.anims.get(&id).map(|a| a.focus).unwrap_or(0.0)
817    }
818
819    /// A widget's animated **press** progress, or `default` where the runtime has never
820    /// heard of it.
821    ///
822    /// The fallback is the rule [`Runtime::value_or`] applies: a frame built before the
823    /// loop has advanced anything — a test, an isolated render — must draw a widget that
824    /// *is* being held as held, not as untouched on its way there.
825    pub fn press_progress_or(&self, id: WidgetId, default: f32) -> f32 {
826        self.anims.get(&id).map(|a| a.press).unwrap_or(default)
827    }
828
829    /// A widget's animated press progress (0 by default).
830    pub fn press_progress(&self, id: WidgetId) -> f32 {
831        self.press_progress_or(id, 0.0)
832    }
833
834    /// A widget's animated opacity (1 by default).
835    pub fn opacity(&self, id: WidgetId) -> f32 {
836        self.anims.get(&id).map(|a| a.opacity).unwrap_or(1.0)
837    }
838
839    /// A widget's animated value (0 by default).
840    pub fn value(&self, id: WidgetId) -> f32 {
841        self.values.get(&id).map(|v| v.current).unwrap_or(0.0)
842    }
843
844    /// A widget's animated value, or `default` if **no** value has been recorded yet
845    /// (a widget never animated — e.g. an isolated render with no loop). Lets the
846    /// target be adopted immediately, as on mount.
847    pub fn value_or(&self, id: WidgetId, default: f32) -> f32 {
848        self.values.get(&id).map(|v| v.current).unwrap_or(default)
849    }
850
851    /// Sets a widget's animated value to `v` (at rest, no transition in progress) —
852    /// for isolated renders/tests that want a precise progress without running the
853    /// animation.
854    pub fn set_value(&mut self, id: WidgetId, v: f32) {
855        self.values.insert(id, ValueAnim::settled(v));
856    }
857
858    /// **Holds** a widget's value at `v` under a finger (milestone 638): it stays there,
859    /// whatever its target, until [`release_value`](Self::release_value).
860    pub fn hold_value(&mut self, id: WidgetId, v: f32) {
861        self.values.insert(id, ValueAnim::settled(v));
862        self.held_values.insert(id);
863        self.placed_values.insert(id);
864    }
865
866    /// Lets go of a held value: it settles from where it was left to its target, linearly
867    /// and in the share of the widget's duration the distance left is — as a controller
868    /// let go part way finishes the way it was going.
869    pub fn release_value(&mut self, id: WidgetId) {
870        self.held_values.remove(&id);
871    }
872
873    /// Whether a drag placed this value and it has not settled yet.
874    pub fn value_placed(&self, id: WidgetId) -> bool {
875        self.placed_values.contains(&id)
876    }
877
878    /// Drives every animated value towards the target its widget declares
879    /// (`Widget::anim_target`). A widget seen for the **first** time adopts its
880    /// target with no transition (no animation on mount). Returns `true` if a value
881    /// is still moving.
882    pub fn advance_values<Msg>(&mut self, root: &dyn crate::widget::Widget<Msg>, dt: f32) -> bool {
883        fn collect<Msg>(
884            widget: &dyn crate::widget::Widget<Msg>,
885            id: WidgetId,
886            still: bool,
887            out: &mut Vec<(WidgetId, f32, f32, Curve, Option<f32>)>,
888        ) {
889            if let Some(target) = widget.anim_target() {
890                out.push((
891                    id,
892                    target,
893                    if still {
894                        0.0
895                    } else {
896                        widget.anim_duration().max(0.0)
897                    },
898                    widget.anim_curve(),
899                    widget.anim_initial(),
900                ));
901            }
902            for (index, child) in widget.children().iter().enumerate() {
903                collect(
904                    child.as_ref(),
905                    crate::ui::child_id(id, index, child.as_ref()),
906                    still,
907                    out,
908                );
909            }
910        }
911        let mut targets: Vec<(WidgetId, f32, f32, Curve, Option<f32>)> = Vec::new();
912        collect(root, WidgetId::ROOT, self.still, &mut targets);
913
914        // Forget the values of widgets that have gone.
915        let present: std::collections::HashSet<WidgetId> =
916            targets.iter().map(|(id, ..)| *id).collect();
917        self.values.retain(|id, _| present.contains(id));
918        self.held_values.retain(|id| present.contains(id));
919        self.placed_values.retain(|id| present.contains(id));
920
921        let mut animating = false;
922        for (id, target, duration, curve, initial) in targets {
923            // A finger holds it: nothing moves it until it lets go.
924            if self.held_values.contains(&id) {
925                continue;
926            }
927            let placed = self.placed_values.contains(&id);
928            match self.values.entry(id) {
929                std::collections::hash_map::Entry::Occupied(mut e) => {
930                    let v = e.get_mut();
931                    // Placed by a finger: straight there, in the time the distance takes.
932                    let (duration, curve) = if placed {
933                        let distance = if v.to != target {
934                            (target - v.current).abs()
935                        } else {
936                            (v.to - v.from).abs()
937                        };
938                        (duration * distance.min(1.0), Curve::Linear)
939                    } else {
940                        (duration, curve)
941                    };
942                    // New target: rebase the timeline from the current value.
943                    if v.to != target {
944                        v.from = v.current;
945                        v.to = target;
946                        v.elapsed = 0.0;
947                    }
948                    if v.from == v.to {
949                        v.current = v.to;
950                    } else {
951                        v.elapsed += dt;
952                        let t = if duration > 0.0 {
953                            (v.elapsed / duration).clamp(0.0, 1.0)
954                        } else {
955                            1.0
956                        };
957                        v.current = v.from + (v.to - v.from) * curve.transform(t);
958                        if t < 1.0 {
959                            animating = true;
960                        }
961                    }
962                    if placed && v.current == v.to {
963                        self.placed_values.remove(&id);
964                    }
965                }
966                std::collections::hash_map::Entry::Vacant(e) => match initial {
967                    // Mount, for a widget that plays from a start (`anim_initial`): the
968                    // value sets off from there, this frame counted as the first.
969                    Some(start) if start != target && duration > 0.0 => {
970                        let t = (dt / duration).clamp(0.0, 1.0);
971                        e.insert(ValueAnim {
972                            current: start + (target - start) * curve.transform(t),
973                            from: start,
974                            to: target,
975                            elapsed: dt,
976                        });
977                        animating |= t < 1.0;
978                    }
979                    // Mount: adopts the target with no transition.
980                    _ => {
981                        e.insert(ValueAnim::settled(target));
982                    }
983                },
984            }
985        }
986        animating
987    }
988
989    /// A widget's animated background colour, if in transition (`None` otherwise).
990    pub fn anim_color(&self, id: WidgetId) -> Option<Color> {
991        self.colors.get(&id).map(|c| c.current)
992    }
993
994    /// Drives every animated background colour towards the target its widget
995    /// declares (`Widget::anim_color`), following its duration/curve
996    /// (`anim_duration`/`anim_curve`). On mount: adopts the target with no
997    /// transition. Returns `true` if a colour is still moving. Same model as
998    /// [`Self::advance_values`].
999    pub fn advance_colors<Msg>(&mut self, root: &dyn crate::widget::Widget<Msg>, dt: f32) -> bool {
1000        fn collect<Msg>(
1001            widget: &dyn crate::widget::Widget<Msg>,
1002            id: WidgetId,
1003            still: bool,
1004            out: &mut Vec<(WidgetId, Color, f32, Curve)>,
1005        ) {
1006            if let Some(target) = widget.anim_color() {
1007                out.push((
1008                    id,
1009                    target,
1010                    if still {
1011                        0.0
1012                    } else {
1013                        widget.anim_duration().max(0.0)
1014                    },
1015                    widget.anim_curve(),
1016                ));
1017            }
1018            for (index, child) in widget.children().iter().enumerate() {
1019                collect(
1020                    child.as_ref(),
1021                    crate::ui::child_id(id, index, child.as_ref()),
1022                    still,
1023                    out,
1024                );
1025            }
1026        }
1027        let mut targets: Vec<(WidgetId, Color, f32, Curve)> = Vec::new();
1028        collect(root, WidgetId::ROOT, self.still, &mut targets);
1029
1030        let present: std::collections::HashSet<WidgetId> =
1031            targets.iter().map(|(id, ..)| *id).collect();
1032        self.colors.retain(|id, _| present.contains(id));
1033
1034        let mut animating = false;
1035        for (id, target, duration, curve) in targets {
1036            match self.colors.entry(id) {
1037                std::collections::hash_map::Entry::Occupied(mut e) => {
1038                    let c = e.get_mut();
1039                    if c.to != target {
1040                        c.from = c.current;
1041                        c.to = target;
1042                        c.elapsed = 0.0;
1043                    }
1044                    if c.from == c.to {
1045                        c.current = c.to;
1046                    } else {
1047                        c.elapsed += dt;
1048                        let t = if duration > 0.0 {
1049                            (c.elapsed / duration).clamp(0.0, 1.0)
1050                        } else {
1051                            1.0
1052                        };
1053                        c.current = c.from.lerp(c.to, curve.transform(t));
1054                        if t < 1.0 {
1055                            animating = true;
1056                        }
1057                    }
1058                }
1059                std::collections::hash_map::Entry::Vacant(e) => {
1060                    e.insert(ColorAnim::settled(target));
1061                }
1062            }
1063        }
1064        animating
1065    }
1066
1067    /// A widget's animated inherited text style, if in transition (`None` otherwise).
1068    ///
1069    /// Read by the theme swap rather than by the node itself: see
1070    /// `crate::ui::scoped_theme`, which is the one place the value is consumed and the
1071    /// reason there is one place at all.
1072    pub fn anim_text_style(&self, id: WidgetId) -> Option<TextStyle> {
1073        self.text_styles.get(&id).map(|s| s.current)
1074    }
1075
1076    /// Drives every animated inherited text style towards the target its widget declares
1077    /// (`Widget::anim_text_style`), following its duration/curve. On mount: adopts the
1078    /// target with no transition. Returns `true` if a style is still moving.
1079    ///
1080    /// Same model as [`Self::advance_colors`], over [`TextStyle::lerp`] — which is where
1081    /// the rules live for what travels, what holds and what swaps at the halfway point.
1082    pub fn advance_text_styles<Msg>(
1083        &mut self,
1084        root: &dyn crate::widget::Widget<Msg>,
1085        dt: f32,
1086    ) -> bool {
1087        fn collect<Msg>(
1088            widget: &dyn crate::widget::Widget<Msg>,
1089            id: WidgetId,
1090            still: bool,
1091            out: &mut Vec<(WidgetId, TextStyle, f32, Curve)>,
1092        ) {
1093            if let Some(target) = widget.anim_text_style() {
1094                out.push((
1095                    id,
1096                    target,
1097                    if still {
1098                        0.0
1099                    } else {
1100                        widget.anim_duration().max(0.0)
1101                    },
1102                    widget.anim_curve(),
1103                ));
1104            }
1105            for (index, child) in widget.children().iter().enumerate() {
1106                collect(
1107                    child.as_ref(),
1108                    crate::ui::child_id(id, index, child.as_ref()),
1109                    still,
1110                    out,
1111                );
1112            }
1113        }
1114        let mut targets: Vec<(WidgetId, TextStyle, f32, Curve)> = Vec::new();
1115        collect(root, WidgetId::ROOT, self.still, &mut targets);
1116
1117        let present: std::collections::HashSet<WidgetId> =
1118            targets.iter().map(|(id, ..)| *id).collect();
1119        self.text_styles.retain(|id, _| present.contains(id));
1120
1121        let mut animating = false;
1122        for (id, target, duration, curve) in targets {
1123            match self.text_styles.entry(id) {
1124                std::collections::hash_map::Entry::Occupied(mut e) => {
1125                    let s = e.get_mut();
1126                    if s.to != target {
1127                        s.from = s.current;
1128                        s.to = target;
1129                        s.elapsed = 0.0;
1130                    }
1131                    if s.from == s.to {
1132                        s.current = s.to;
1133                    } else {
1134                        s.elapsed += dt;
1135                        let t = if duration > 0.0 {
1136                            (s.elapsed / duration).clamp(0.0, 1.0)
1137                        } else {
1138                            1.0
1139                        };
1140                        s.current = s.from.lerp(s.to, curve.transform(t));
1141                        if t < 1.0 {
1142                            animating = true;
1143                        }
1144                    }
1145                }
1146                std::collections::hash_map::Entry::Vacant(e) => {
1147                    e.insert(TextStyleAnim::settled(target));
1148                }
1149            }
1150        }
1151        animating
1152    }
1153
1154    /// A widget's animated size, if in transition (`None` otherwise).
1155    pub fn anim_size(&self, id: WidgetId) -> Option<Size> {
1156        self.sizes.get(&id).map(|s| s.current)
1157    }
1158
1159    /// Drives every animated size towards the target its widget declares
1160    /// (`Widget::anim_size`), following its duration/curve. On mount: adopts the
1161    /// target with no transition. Returns `true` if a size is still moving. Same
1162    /// model as [`Self::advance_values`], except the output is **consumed at
1163    /// layout** (`effective_style`), not at paint.
1164    pub fn advance_sizes<Msg>(&mut self, root: &dyn crate::widget::Widget<Msg>, dt: f32) -> bool {
1165        fn collect<Msg>(
1166            widget: &dyn crate::widget::Widget<Msg>,
1167            id: WidgetId,
1168            still: bool,
1169            out: &mut Vec<(WidgetId, Size, f32, Curve)>,
1170        ) {
1171            if let Some(target) = widget.anim_size() {
1172                out.push((
1173                    id,
1174                    target,
1175                    if still {
1176                        0.0
1177                    } else {
1178                        widget.anim_duration().max(0.0)
1179                    },
1180                    widget.anim_curve(),
1181                ));
1182            }
1183            for (index, child) in widget.children().iter().enumerate() {
1184                collect(
1185                    child.as_ref(),
1186                    crate::ui::child_id(id, index, child.as_ref()),
1187                    still,
1188                    out,
1189                );
1190            }
1191        }
1192        let mut targets: Vec<(WidgetId, Size, f32, Curve)> = Vec::new();
1193        collect(root, WidgetId::ROOT, self.still, &mut targets);
1194
1195        let present: std::collections::HashSet<WidgetId> =
1196            targets.iter().map(|(id, ..)| *id).collect();
1197        self.sizes.retain(|id, _| present.contains(id));
1198
1199        let mut animating = false;
1200        for (id, target, duration, curve) in targets {
1201            match self.sizes.entry(id) {
1202                std::collections::hash_map::Entry::Occupied(mut e) => {
1203                    let s = e.get_mut();
1204                    if s.to != target {
1205                        s.from = s.current;
1206                        s.to = target;
1207                        s.elapsed = 0.0;
1208                    }
1209                    if s.from == s.to {
1210                        s.current = s.to;
1211                    } else {
1212                        s.elapsed += dt;
1213                        let t = if duration > 0.0 {
1214                            (s.elapsed / duration).clamp(0.0, 1.0)
1215                        } else {
1216                            1.0
1217                        };
1218                        s.current = lerp_size(s.from, s.to, curve.transform(t));
1219                        if t < 1.0 {
1220                            animating = true;
1221                        }
1222                    }
1223                }
1224                std::collections::hash_map::Entry::Vacant(e) => {
1225                    e.insert(SizeAnim::settled(target));
1226                }
1227            }
1228        }
1229        animating
1230    }
1231
1232    /// A widget's animated corner radius, if in transition (`None` otherwise).
1233    pub fn anim_radius(&self, id: WidgetId) -> Option<BorderRadius> {
1234        self.radii.get(&id).map(|r| r.current)
1235    }
1236
1237    /// Drives every animated corner radius towards the target its widget declares
1238    /// (`Widget::anim_radius`), following its duration/curve. On mount: adopts the
1239    /// target with no transition. Returns `true` if a radius is still moving. Same
1240    /// model as [`Self::advance_colors`], applied to a [`BorderRadius`].
1241    pub fn advance_radii<Msg>(&mut self, root: &dyn crate::widget::Widget<Msg>, dt: f32) -> bool {
1242        fn collect<Msg>(
1243            widget: &dyn crate::widget::Widget<Msg>,
1244            id: WidgetId,
1245            still: bool,
1246            out: &mut Vec<(WidgetId, BorderRadius, f32, Curve)>,
1247        ) {
1248            if let Some(target) = widget.anim_radius() {
1249                out.push((
1250                    id,
1251                    target,
1252                    if still {
1253                        0.0
1254                    } else {
1255                        widget.anim_duration().max(0.0)
1256                    },
1257                    widget.anim_curve(),
1258                ));
1259            }
1260            for (index, child) in widget.children().iter().enumerate() {
1261                collect(
1262                    child.as_ref(),
1263                    crate::ui::child_id(id, index, child.as_ref()),
1264                    still,
1265                    out,
1266                );
1267            }
1268        }
1269        let mut targets: Vec<(WidgetId, BorderRadius, f32, Curve)> = Vec::new();
1270        collect(root, WidgetId::ROOT, self.still, &mut targets);
1271
1272        let present: std::collections::HashSet<WidgetId> =
1273            targets.iter().map(|(id, ..)| *id).collect();
1274        self.radii.retain(|id, _| present.contains(id));
1275
1276        let mut animating = false;
1277        for (id, target, duration, curve) in targets {
1278            match self.radii.entry(id) {
1279                std::collections::hash_map::Entry::Occupied(mut e) => {
1280                    let r = e.get_mut();
1281                    if r.to != target {
1282                        r.from = r.current;
1283                        r.to = target;
1284                        r.elapsed = 0.0;
1285                    }
1286                    if r.from == r.to {
1287                        r.current = r.to;
1288                    } else {
1289                        r.elapsed += dt;
1290                        let t = if duration > 0.0 {
1291                            (r.elapsed / duration).clamp(0.0, 1.0)
1292                        } else {
1293                            1.0
1294                        };
1295                        r.current = lerp_radius(r.from, r.to, curve.transform(t));
1296                        if t < 1.0 {
1297                            animating = true;
1298                        }
1299                    }
1300                }
1301                std::collections::hash_map::Entry::Vacant(e) => {
1302                    e.insert(RadiusAnim::settled(target));
1303                }
1304            }
1305        }
1306        animating
1307    }
1308
1309    /// A widget's animated transform, if in transition (`None` otherwise).
1310    pub fn anim_transform(&self, id: WidgetId) -> Option<TransformValues> {
1311        self.transforms.get(&id).map(|t| t.current)
1312    }
1313
1314    /// Drives every animated transform towards the target its widget declares
1315    /// (`Widget::anim_transform`), following its duration/curve. On mount: adopts the
1316    /// target with no transition — a widget that appears already scaled does not scale
1317    /// in from nothing. Returns `true` if a transform is still moving. The output is
1318    /// consumed **at paint**, unlike the size and the padding.
1319    pub fn advance_transforms<Msg>(
1320        &mut self,
1321        root: &dyn crate::widget::Widget<Msg>,
1322        dt: f32,
1323    ) -> bool {
1324        fn collect<Msg>(
1325            widget: &dyn crate::widget::Widget<Msg>,
1326            id: WidgetId,
1327            still: bool,
1328            out: &mut Vec<(WidgetId, TransformValues, f32, Curve)>,
1329        ) {
1330            if let Some(target) = widget.anim_transform() {
1331                out.push((
1332                    id,
1333                    target,
1334                    if still {
1335                        0.0
1336                    } else {
1337                        widget.anim_duration().max(0.0)
1338                    },
1339                    widget.anim_curve(),
1340                ));
1341            }
1342            for (index, child) in widget.children().iter().enumerate() {
1343                collect(
1344                    child.as_ref(),
1345                    crate::ui::child_id(id, index, child.as_ref()),
1346                    still,
1347                    out,
1348                );
1349            }
1350        }
1351        let mut targets: Vec<(WidgetId, TransformValues, f32, Curve)> = Vec::new();
1352        collect(root, WidgetId::ROOT, self.still, &mut targets);
1353
1354        let present: std::collections::HashSet<WidgetId> =
1355            targets.iter().map(|(id, ..)| *id).collect();
1356        self.transforms.retain(|id, _| present.contains(id));
1357
1358        let mut animating = false;
1359        for (id, target, duration, curve) in targets {
1360            match self.transforms.entry(id) {
1361                std::collections::hash_map::Entry::Occupied(mut e) => {
1362                    let v = e.get_mut();
1363                    if v.to != target {
1364                        v.from = v.current;
1365                        v.to = target;
1366                        v.elapsed = 0.0;
1367                    }
1368                    if v.from == v.to {
1369                        v.current = v.to;
1370                    } else {
1371                        v.elapsed += dt;
1372                        let t = if duration > 0.0 {
1373                            (v.elapsed / duration).clamp(0.0, 1.0)
1374                        } else {
1375                            1.0
1376                        };
1377                        v.current = lerp_transform(v.from, v.to, curve.transform(t));
1378                        if t < 1.0 {
1379                            animating = true;
1380                        }
1381                    }
1382                }
1383                std::collections::hash_map::Entry::Vacant(e) => {
1384                    e.insert(TransformAnim::settled(target));
1385                }
1386            }
1387        }
1388        animating
1389    }
1390
1391    /// A widget's animated padding, if in transition (`None` otherwise).
1392    pub fn anim_padding(&self, id: WidgetId) -> Option<Insets> {
1393        self.paddings.get(&id).map(|p| p.current)
1394    }
1395
1396    /// A widget's animated offset pair, if in transition (`None` otherwise).
1397    pub fn anim_offset(&self, id: WidgetId) -> Option<(f32, f32)> {
1398        self.offsets.get(&id).map(|o| o.current)
1399    }
1400
1401    /// Drives every animated offset pair towards the target its widget declares
1402    /// (`Widget::anim_offset`), following its duration/curve. On mount: adopts the target
1403    /// with no transition. Returns `true` if one is still moving.
1404    ///
1405    /// The pair belongs to whichever **offset rule** the node declares — its alignment,
1406    /// or its slide — which is why one quantity serves both: a node has one of those
1407    /// rules, and the wrappers in `animated.rs` each give it a node of its own.
1408    pub fn advance_offsets<Msg>(&mut self, root: &dyn crate::widget::Widget<Msg>, dt: f32) -> bool {
1409        fn collect<Msg>(
1410            widget: &dyn crate::widget::Widget<Msg>,
1411            id: WidgetId,
1412            still: bool,
1413            out: &mut Vec<(WidgetId, (f32, f32), f32, Curve)>,
1414        ) {
1415            if let Some(target) = widget.anim_offset() {
1416                out.push((
1417                    id,
1418                    target,
1419                    if still {
1420                        0.0
1421                    } else {
1422                        widget.anim_duration().max(0.0)
1423                    },
1424                    widget.anim_curve(),
1425                ));
1426            }
1427            for (index, child) in widget.children().iter().enumerate() {
1428                collect(
1429                    child.as_ref(),
1430                    crate::ui::child_id(id, index, child.as_ref()),
1431                    still,
1432                    out,
1433                );
1434            }
1435        }
1436        let mut targets: Vec<(WidgetId, (f32, f32), f32, Curve)> = Vec::new();
1437        collect(root, WidgetId::ROOT, self.still, &mut targets);
1438
1439        let present: std::collections::HashSet<WidgetId> =
1440            targets.iter().map(|(id, ..)| *id).collect();
1441        self.offsets.retain(|id, _| present.contains(id));
1442
1443        let mut animating = false;
1444        for (id, target, duration, curve) in targets {
1445            match self.offsets.entry(id) {
1446                std::collections::hash_map::Entry::Occupied(mut e) => {
1447                    let o = e.get_mut();
1448                    if o.to != target {
1449                        o.from = o.current;
1450                        o.to = target;
1451                        o.elapsed = 0.0;
1452                    }
1453                    if o.from == o.to {
1454                        o.current = o.to;
1455                    } else {
1456                        o.elapsed += dt;
1457                        let t = if duration > 0.0 {
1458                            (o.elapsed / duration).clamp(0.0, 1.0)
1459                        } else {
1460                            1.0
1461                        };
1462                        let e = curve.transform(t);
1463                        o.current = (
1464                            o.from.0 + (o.to.0 - o.from.0) * e,
1465                            o.from.1 + (o.to.1 - o.from.1) * e,
1466                        );
1467                        if t < 1.0 {
1468                            animating = true;
1469                        }
1470                    }
1471                }
1472                std::collections::hash_map::Entry::Vacant(e) => {
1473                    e.insert(OffsetAnim::settled(target));
1474                }
1475            }
1476        }
1477        animating
1478    }
1479
1480    /// Drives every animated padding towards the target its widget declares
1481    /// (`Widget::anim_padding`), following its duration/curve. On mount: adopts the
1482    /// target with no transition. Returns `true` if a padding is still moving. Like
1483    /// the size, the output is **consumed at layout** (`effective_style`).
1484    pub fn advance_paddings<Msg>(
1485        &mut self,
1486        root: &dyn crate::widget::Widget<Msg>,
1487        dt: f32,
1488    ) -> bool {
1489        fn collect<Msg>(
1490            widget: &dyn crate::widget::Widget<Msg>,
1491            id: WidgetId,
1492            still: bool,
1493            out: &mut Vec<(WidgetId, Insets, f32, Curve)>,
1494        ) {
1495            if let Some(target) = widget.anim_padding() {
1496                out.push((
1497                    id,
1498                    target,
1499                    if still {
1500                        0.0
1501                    } else {
1502                        widget.anim_duration().max(0.0)
1503                    },
1504                    widget.anim_curve(),
1505                ));
1506            }
1507            for (index, child) in widget.children().iter().enumerate() {
1508                collect(
1509                    child.as_ref(),
1510                    crate::ui::child_id(id, index, child.as_ref()),
1511                    still,
1512                    out,
1513                );
1514            }
1515        }
1516        let mut targets: Vec<(WidgetId, Insets, f32, Curve)> = Vec::new();
1517        collect(root, WidgetId::ROOT, self.still, &mut targets);
1518
1519        let present: std::collections::HashSet<WidgetId> =
1520            targets.iter().map(|(id, ..)| *id).collect();
1521        self.paddings.retain(|id, _| present.contains(id));
1522
1523        let mut animating = false;
1524        for (id, target, duration, curve) in targets {
1525            match self.paddings.entry(id) {
1526                std::collections::hash_map::Entry::Occupied(mut e) => {
1527                    let p = e.get_mut();
1528                    if p.to != target {
1529                        p.from = p.current;
1530                        p.to = target;
1531                        p.elapsed = 0.0;
1532                    }
1533                    if p.from == p.to {
1534                        p.current = p.to;
1535                    } else {
1536                        p.elapsed += dt;
1537                        let t = if duration > 0.0 {
1538                            (p.elapsed / duration).clamp(0.0, 1.0)
1539                        } else {
1540                            1.0
1541                        };
1542                        p.current = lerp_insets(p.from, p.to, curve.transform(t));
1543                        if t < 1.0 {
1544                            animating = true;
1545                        }
1546                    }
1547                }
1548                std::collections::hash_map::Entry::Vacant(e) => {
1549                    e.insert(PaddingAnim::settled(target));
1550                }
1551            }
1552        }
1553        animating
1554    }
1555
1556    /// A widget's animated stack pins, if it has any (`None` otherwise).
1557    pub fn anim_pins(&self, id: WidgetId) -> Option<crate::positioned::Positioning> {
1558        self.pins.get(&id).map(|p| {
1559            let [left, top, right, bottom, width, height, start, end] = p.current;
1560            crate::positioned::Positioning {
1561                left,
1562                top,
1563                right,
1564                bottom,
1565                width,
1566                height,
1567                start,
1568                end,
1569            }
1570        })
1571    }
1572
1573    /// A widget's animated fractions of its parent, if it has any (`None` otherwise), as
1574    /// `(width factor, height factor)`.
1575    pub fn anim_fractions(&self, id: WidgetId) -> Option<(Option<f32>, Option<f32>)> {
1576        self.fractions
1577            .get(&id)
1578            .map(|f| (f.current[0], f.current[1]))
1579    }
1580
1581    /// Drives every animated set of stack pins towards the target its widget declares
1582    /// (`Widget::anim_pins`), following its duration/curve. On mount: adopts the target
1583    /// with no transition. Returns `true` if a layer is still moving.
1584    ///
1585    /// The output is consumed where the **stack** lays its layers out, which is the only
1586    /// place pins mean anything — a pin is a request to a parent of one particular kind,
1587    /// not a property of the box.
1588    pub fn advance_pins<Msg>(&mut self, root: &dyn crate::widget::Widget<Msg>, dt: f32) -> bool {
1589        let mut targets = Vec::new();
1590        collect_slots(root, WidgetId::ROOT, self.still, &mut targets, |w| {
1591            w.anim_pins().map(|p| {
1592                [
1593                    p.left, p.top, p.right, p.bottom, p.width, p.height, p.start, p.end,
1594                ]
1595            })
1596        });
1597        advance_slot_family(&mut self.pins, targets, dt)
1598    }
1599
1600    /// Drives every animated pair of fractions towards the target its widget declares
1601    /// (`Widget::anim_fractions`), following its duration/curve. On mount: adopts the
1602    /// target with no transition. Returns `true` if a box is still growing or shrinking.
1603    ///
1604    /// Like the size and the padding, the output is **consumed at layout**
1605    /// (`effective_style`): a share of the parent is a claim on room, and a claim on room
1606    /// is settled before anything is drawn.
1607    pub fn advance_fractions<Msg>(
1608        &mut self,
1609        root: &dyn crate::widget::Widget<Msg>,
1610        dt: f32,
1611    ) -> bool {
1612        let mut targets = Vec::new();
1613        collect_slots(root, WidgetId::ROOT, self.still, &mut targets, |w| {
1614            w.anim_fractions().map(|(width, height)| [width, height])
1615        });
1616        advance_slot_family(&mut self.fractions, targets, dt)
1617    }
1618
1619    /// Advances the transitions (hover/focus/press) by `dt` seconds towards their
1620    /// targets. Returns `true` if at least one animation is still running.
1621    pub fn advance(&mut self, dt: f32) -> bool {
1622        let hovered = self.input.hovered;
1623        let focused = self.input.focused;
1624        // Held **and** still under the pointer, which is what makes it a press: the same
1625        // rule `InputState::status_for` applies, so the progression and the flag can
1626        // never disagree about what is happening.
1627        let held = self.input.pressed.filter(|id| Some(*id) == hovered);
1628        if let Some(id) = hovered {
1629            self.anims.entry(id).or_default();
1630        }
1631        if let Some(id) = focused {
1632            self.anims.entry(id).or_default();
1633        }
1634        if let Some(id) = held {
1635            self.anims.entry(id).or_default();
1636        }
1637
1638        let step = if ANIM_DURATION > 0.0 {
1639            dt / ANIM_DURATION
1640        } else {
1641            1.0
1642        };
1643        // The press runs on its own, slower clock (`ink_well.dart:995`).
1644        let press_step = if PRESS_DURATION > 0.0 {
1645            dt / PRESS_DURATION
1646        } else {
1647            1.0
1648        };
1649        let mut animating = false;
1650
1651        self.anims.retain(|id, anim| {
1652            let hover_target = if Some(*id) == hovered { 1.0 } else { 0.0 };
1653            let focus_target = if Some(*id) == focused { 1.0 } else { 0.0 };
1654            let press_target = if Some(*id) == held { 1.0 } else { 0.0 };
1655            approach(&mut anim.hover, hover_target, step, &mut animating);
1656            approach(&mut anim.focus, focus_target, step, &mut animating);
1657            approach(&mut anim.press, press_target, press_step, &mut animating);
1658            // Opacity always tends towards 1 (the fade-in).
1659            approach(&mut anim.opacity, 1.0, step, &mut animating);
1660            // Entries that are entirely at rest are forgotten (nothing to animate). A
1661            // press still fading **out** is not at rest: forgetting it here would be the
1662            // jump this progression exists to remove, on the way back.
1663            !(hover_target == 0.0
1664                && focus_target == 0.0
1665                && press_target == 0.0
1666                && anim.hover <= 0.0
1667                && anim.focus <= 0.0
1668                && anim.press <= 0.0
1669                && anim.opacity >= 1.0)
1670        });
1671        // The boxes following their child's size (milestone 594), here so that everything
1672        // that advances the runtime's transitions advances them too.
1673        animating |= self.size_anims.get_mut().advance(dt, self.still);
1674
1675        animating
1676    }
1677
1678    /// Advances every scroll region by `dt`, and returns `true` if any is still
1679    /// moving.
1680    ///
1681    /// Two motions live here, and they are not the same thing:
1682    ///
1683    /// - a **fling**, when the finger let go with speed: the region's physics built
1684    ///   a simulation ([`ScrollBallistic`]), and we sample it. This is where the
1685    ///   platform's feel lives — spline deceleration that stops at the edge, or
1686    ///   friction that hands over to a spring and bounces.
1687    /// - a **glide to a target**, for the discrete inputs (the wheel, a programmatic
1688    ///   scroll): a spring eases the offset across to where it was asked to go.
1689    ///
1690    /// A fling wins while it runs: it drives the offset directly and keeps the
1691    /// target in step, so the spring has nothing to pull against.
1692    ///
1693    /// `regions` describes the scrollables of the last frame, each with the physics its
1694    /// scroll behaviour settled on when it was registered (milestone 616).
1695    pub fn advance_scroll(&mut self, regions: &[Scrollable], dt: f32) -> bool {
1696        // Content that shrank under an offset nobody owns is brought back first, so that
1697        // everything below starts from where the content now is (milestone 533).
1698        self.keep_scroll_in_range(regions);
1699        let mut animating = false;
1700        let ballistic_ids: Vec<WidgetId> = self.scroll_ballistic.keys().copied().collect();
1701        for id in ballistic_ids {
1702            if self.advance_ballistic(id, regions, dt) {
1703                animating = true;
1704            }
1705        }
1706
1707        let ids: Vec<WidgetId> = self.scroll_target.keys().copied().collect();
1708        for id in ids {
1709            // Under a fling this region's offset is already settled for the frame.
1710            if self.scroll_ballistic.contains_key(&id) {
1711                continue;
1712            }
1713            // Under a finger it belongs to the finger, full stop.
1714            if self.scroll_held == Some(id) {
1715                continue;
1716            }
1717            let area = regions.iter().find(|area| area.id == id);
1718            let (max_x, max_y) = area.map(|a| (a.max_x, a.max_y)).unwrap_or((0.0, 0.0));
1719            let current = self.scroll.get(&id).copied().unwrap_or((0.0, 0.0));
1720            let target = self.scroll_target.get(&id).copied().unwrap_or(current);
1721            let vel = self.scroll_velocity.get(&id).copied().unwrap_or((0.0, 0.0));
1722
1723            let (cx, vx, tx, ax) = scroll_axis(current.0, vel.0, target.0, max_x, dt);
1724            let (cy, vy, ty, ay) = scroll_axis(current.1, vel.1, target.1, max_y, dt);
1725
1726            self.scroll.insert(id, (cx, cy));
1727            if ax || ay {
1728                self.scroll_target.insert(id, (tx, ty));
1729                self.scroll_velocity.insert(id, (vx, vy));
1730                animating = true;
1731            } else {
1732                // At rest: the animation state is cleared (the current offset stays).
1733                self.scroll_target.remove(&id);
1734                self.scroll_velocity.remove(&id);
1735            }
1736        }
1737        animating | self.advance_scrollbars(regions, dt)
1738    }
1739
1740    /// Advances every area's **scrollbar fade** by `dt`, and returns `true` while any of
1741    /// them has something left to do — including simply *waiting* to start going.
1742    ///
1743    /// The bar arrives on movement and leaves `BAR_TIME_TO_FADE` after it stops. Nothing
1744    /// reports that movement: the offset is written from the drag handler, from the
1745    /// fling and from the spring, and none of the three leaves word. So this compares
1746    /// the offset against the one it saw last frame, which catches every writer there
1747    /// is, and every writer there will be.
1748    fn advance_scrollbars(&mut self, regions: &[Scrollable], dt: f32) -> bool {
1749        self.scrollbar_fade
1750            .retain(|id, _| regions.iter().any(|area| area.id == *id));
1751        let mut animating = false;
1752        for area in regions {
1753            let now = self.scroll.get(&area.id).copied().unwrap_or((0.0, 0.0));
1754            let hovered = self.scrollbar_hovered == Some(area.id);
1755            let held = hovered || self.scrollbar_dragged == Some(area.id);
1756            let fade = self.scrollbar_fade.entry(area.id).or_insert(ScrollbarFade {
1757                seen: now,
1758                // An area **nobody has moved yet** has no bar: the reference's fade
1759                // starts closed and is only ever opened by a scroll notification. A
1760                // list that has just appeared shows its content, not its furniture.
1761                since: BAR_TIME_TO_FADE,
1762                ..ScrollbarFade::default()
1763            });
1764            if fade.seen != now {
1765                fade.seen = now;
1766                fade.since = 0.0;
1767            } else if held {
1768                fade.since = 0.0;
1769            } else {
1770                fade.since += dt;
1771            }
1772            let waiting = fade.since < BAR_TIME_TO_FADE;
1773            let target = if waiting { 1.0 } else { 0.0 };
1774            approach(&mut fade.opacity, target, dt / BAR_FADE, &mut animating);
1775            let warm = if hovered { 1.0 } else { 0.0 };
1776            approach(&mut fade.hover, warm, dt / BAR_HOVER, &mut animating);
1777            // The wait is not a movement, but it still needs a frame at the end of it,
1778            // or the bar would sit there until something else happened to ask for one.
1779            if waiting && !held {
1780                animating = true;
1781            }
1782        }
1783        animating
1784    }
1785
1786    /// What `id`'s scrollbar looks like this frame. An area nothing has touched, or one
1787    /// rendered in isolation with no runtime behind it, answers a closed fade.
1788    pub fn scrollbar_fade_of(&self, id: WidgetId) -> ScrollbarFade {
1789        self.scrollbar_fade.get(&id).copied().unwrap_or_default()
1790    }
1791
1792    /// Samples one region's fling for this frame. Returns `true` while it runs.
1793    ///
1794    /// An axis ends either because its simulation says so, or because it hit an edge
1795    /// the physics does not let it cross — under clamping physics the simulation
1796    /// knows nothing of the bounds, so reaching one **stops that axis dead**, which
1797    /// is exactly the behaviour a platform without a bounce wants.
1798    fn advance_ballistic(&mut self, id: WidgetId, regions: &[Scrollable], dt: f32) -> bool {
1799        let Some(mut fling) = self.scroll_ballistic.get(&id).copied() else {
1800            return false;
1801        };
1802        // A region that vanished from the frame (a route changed under the fling)
1803        // has nothing left to move.
1804        let Some(area) = regions.iter().find(|area| area.id == id).copied() else {
1805            self.scroll_ballistic.remove(&id);
1806            return false;
1807        };
1808        let physics = area.physics;
1809        fling.elapsed += dt;
1810        let t = fling.elapsed;
1811        let current = self.scroll.get(&id).copied().unwrap_or((0.0, 0.0));
1812
1813        // An axis that slams into an edge reports the speed it lost there, so the
1814        // glow can show what the clamp swallowed.
1815        let mut absorbed: Vec<(GlowEdge, f32)> = Vec::new();
1816        let mut axis = |sim: &mut Option<Ballistic>, previous: f32, max: f32, vertical: bool| {
1817            let Some(active) = sim.as_ref() else {
1818                return previous;
1819            };
1820            let mut position = active.x(t);
1821            let mut finished = active.is_done(t);
1822            if !physics.allows_overscroll() {
1823                let pinned = position.clamp(0.0, max);
1824                if pinned != position {
1825                    // Hitting the edge is the end of the motion, not a pause at it.
1826                    finished = true;
1827                    absorbed.push((edge_for(vertical, position - pinned), active.dx(t)));
1828                }
1829                position = pinned;
1830            }
1831            if finished {
1832                *sim = None;
1833            }
1834            position
1835        };
1836
1837        let x = axis(&mut fling.x, current.0, area.max_x, false);
1838        let y = axis(&mut fling.y, current.1, area.max_y, true);
1839        for (edge, velocity) in absorbed {
1840            self.glow_absorb(id, area.overscroll, edge, velocity);
1841        }
1842        self.scroll.insert(id, (x, y));
1843        // The glide-to-target path must not fight the fling, nor yank the offset
1844        // back when the fling ends.
1845        self.scroll_target.insert(id, (x, y));
1846        self.scroll_velocity.remove(&id);
1847
1848        if fling.is_empty() {
1849            self.scroll_ballistic.remove(&id);
1850            self.scroll_target.remove(&id);
1851            // Settled on an end the content no longer reaches — it shrank under the fling,
1852            // whose simulation was built for the old one. Spring on to the end there is now:
1853            // left to the correction at rest, the offset would jump there in one frame. The
1854            // reference starts its ballistic again on new dimensions for the same reason.
1855            let inside = inside_extent((x, y), (area.max_x, area.max_y));
1856            let slack = physics.tolerance().distance;
1857            if (x - inside.0).abs() > slack || (y - inside.1).abs() > slack {
1858                return self.fling_scroll(area, physics, (0.0, 0.0));
1859            }
1860            false
1861        } else {
1862            self.scroll_ballistic.insert(id, fling);
1863            true
1864        }
1865    }
1866
1867    /// Launches a fling on `area` from the release `velocity` (px/s, in scroll
1868    /// space), under `physics`. A release too slow to fling leaves any overscroll to
1869    /// be sprung back, and otherwise does nothing.
1870    pub fn fling_scroll(
1871        &mut self,
1872        area: Scrollable,
1873        physics: ScrollPhysics,
1874        velocity: (f32, f32),
1875    ) -> bool {
1876        let current = self.scroll.get(&area.id).copied().unwrap_or((0.0, 0.0));
1877        // A paged area does not fling: it goes to **a** page, and the release speed
1878        // only says which one. The raw velocity is passed on rather than the
1879        // fling-filtered one, because the question a page view asks of a release is
1880        // "which way did it go", to which 60 px/s is as clear an answer as 2000.
1881        //
1882        // Unless it has been told not to snap, in which case it is a scrollable that
1883        // happens to know where its pages are: it still reports them and still glides
1884        // to the one it is asked for, and only the release falls through to the
1885        // ordinary fling below.
1886        if let Some(snap) = area.page.filter(|snap| snap.snapping) {
1887            let (metrics, velocity) = if snap.horizontal {
1888                (area.metrics_x(current.0), velocity.0)
1889            } else {
1890                (area.metrics_y(current.1), velocity.1)
1891            };
1892            let motion = physics.page_ballistic(metrics, velocity, snap.extent);
1893            if motion.is_none() {
1894                return false;
1895            }
1896            let ballistic = if snap.horizontal {
1897                ScrollBallistic::new(motion, None)
1898            } else {
1899                ScrollBallistic::new(None, motion)
1900            };
1901            self.scroll_ballistic.insert(area.id, ballistic);
1902            self.scroll_velocity.remove(&area.id);
1903            return true;
1904        }
1905        let min_velocity = physics.min_fling_velocity();
1906        // Below the threshold the gesture was not a fling — but an offset left out
1907        // of range still has to come home, so the physics is asked either way, with
1908        // a velocity of zero.
1909        let vx = if velocity.0.abs() >= min_velocity {
1910            velocity.0
1911        } else {
1912            0.0
1913        };
1914        let vy = if velocity.1.abs() >= min_velocity {
1915            velocity.1
1916        } else {
1917            0.0
1918        };
1919        let x = physics.ballistic(area.metrics_x(current.0), vx);
1920        let y = physics.ballistic(area.metrics_y(current.1), vy);
1921        if x.is_none() && y.is_none() {
1922            return false;
1923        }
1924        self.scroll_ballistic
1925            .insert(area.id, ScrollBallistic::new(x, y));
1926        self.scroll_velocity.remove(&area.id);
1927        true
1928    }
1929
1930    /// Acts on the page each paged view is **asking** for.
1931    ///
1932    /// A request is honoured when it *changes*, never re-asserted: the widget is
1933    /// rebuilt every frame and carries the same number each time, so a view that
1934    /// obeyed it on every frame could not be swiped at all — the finger would move
1935    /// the offset and the next frame would put it straight back.
1936    ///
1937    /// The **first** sighting of a region is its initial page, and arrives without
1938    /// an animation: an application opening on page 3 wants to be on page 3, not to
1939    /// watch it fly there.
1940    pub fn sync_pages(&mut self, regions: &[Scrollable]) {
1941        for area in regions {
1942            let Some(snap) = area.page else { continue };
1943            let previous = self.page_requested.insert(area.id, snap.requested);
1944            if previous == Some(snap.requested) {
1945                continue;
1946            }
1947            let max = if snap.horizontal {
1948                area.max_x
1949            } else {
1950                area.max_y
1951            };
1952            let offset = snap
1953                .offset_of(snap.requested.min(snap.count.saturating_sub(1)))
1954                .clamp(0.0, max);
1955            let current = self.scroll.get(&area.id).copied().unwrap_or((0.0, 0.0));
1956            let target = if snap.horizontal {
1957                (offset, current.1)
1958            } else {
1959                (current.0, offset)
1960            };
1961            match previous {
1962                // Never seen: this is where the view opens.
1963                None => {
1964                    self.scroll.insert(area.id, target);
1965                    self.scroll_target.remove(&area.id);
1966                    self.scroll_velocity.remove(&area.id);
1967                }
1968                // A change under way: glide, and let go of anything already moving
1969                // the offset — the application has just overruled it.
1970                Some(_) => {
1971                    self.scroll_ballistic.remove(&area.id);
1972                    self.scroll_target.insert(area.id, target);
1973                }
1974            }
1975        }
1976    }
1977
1978    /// Brings each region's **kept** box into view when it has changed.
1979    ///
1980    /// A tab bar wider than its window is what this is for: the application selects the
1981    /// eighth tab and the bar is still showing the first three, with nothing to say the
1982    /// selection went anywhere.
1983    ///
1984    /// Acted on **when the key changes**, never re-asserted. The box moves as the region
1985    /// scrolls, so a region that chased the box itself would pin the strip in place and
1986    /// no finger could move it — the same trap [`Runtime::sync_pages`] avoids, and the
1987    /// same answer.
1988    ///
1989    /// The **first** sighting arrives without an animation — a bar restored on its
1990    /// eighth tab wants to be on the eighth tab, not to watch it slide there — and the
1991    /// walk has already drawn that first frame at the same offset, so the jump is
1992    /// invisible. It is retained here all the same: without it the region would have no
1993    /// offset of its own, and the frame after would find nothing to keep it there.
1994    pub fn sync_visible(&mut self, regions: &[Scrollable]) {
1995        for area in regions {
1996            let Some(keep) = area.keep_visible else {
1997                continue;
1998            };
1999            let previous = self.kept_visible.insert(area.id, keep.key);
2000            if previous == Some(keep.key) {
2001                continue;
2002            }
2003            let (offset, _) = area.keep_offset(keep);
2004            match previous {
2005                // Never seen: this is where the region opens.
2006                None => {
2007                    self.scroll.insert(area.id, offset);
2008                    self.scroll_target.remove(&area.id);
2009                    self.scroll_velocity.remove(&area.id);
2010                }
2011                // A change: glide, and let go of anything already moving the offset,
2012                // since the selection has just overruled it.
2013                Some(_) => {
2014                    self.scroll_ballistic.remove(&area.id);
2015                    self.scroll_target.insert(area.id, offset);
2016                }
2017            }
2018        }
2019    }
2020
2021    /// The paged views whose page **on screen** has just changed, and the page each
2022    /// now shows.
2023    ///
2024    /// Reported as soon as the rounding tips — mid-drag, not once the spring has
2025    /// settled — because that is when a reader would say they are on the next page.
2026    /// A caller turns each into a message; the runtime holds no messages of its own.
2027    ///
2028    /// A view **appearing** is not a page change: the first sighting of a region is
2029    /// recorded silently, whatever page it opens on. An application that opens on
2030    /// page 3 already knows it is on page 3, and being told so on the first frame
2031    /// would only invite it to answer.
2032    pub fn page_changes(&mut self, regions: &[Scrollable]) -> Vec<(WidgetId, usize)> {
2033        let mut changed = Vec::new();
2034        for area in regions {
2035            let Some(snap) = area.page else { continue };
2036            let offset = self.scroll.get(&area.id).copied().unwrap_or_else(|| {
2037                let start = snap.offset_of(snap.requested);
2038                if snap.horizontal {
2039                    (start, 0.0)
2040                } else {
2041                    (0.0, start)
2042                }
2043            });
2044            let page = snap.page_at(if snap.horizontal { offset.0 } else { offset.1 });
2045            match self.page_shown.insert(area.id, page) {
2046                Some(previous) if previous != page => changed.push((area.id, page)),
2047                _ => {}
2048            }
2049        }
2050        changed
2051    }
2052
2053    /// Is region `id` **still**? Nothing is driving it: no finger, no fling, and no
2054    /// spring left to run.
2055    ///
2056    /// The three writers of an offset, asked in one place. A caller that asked only two
2057    /// of them would call a region at rest in the middle of a fling.
2058    pub fn scroll_at_rest(&self, id: WidgetId) -> bool {
2059        self.scroll_held != Some(id)
2060            && !self.scroll_ballistic.contains_key(&id)
2061            && !self.scroll_target.contains_key(&id)
2062    }
2063
2064    /// Where region `id` is **drawn**, its content now reaching no further than `max`: the
2065    /// retained offset, brought inside `[0, max]` when nothing owns it. `None` when it has
2066    /// never been scrolled.
2067    ///
2068    /// The walk reads this rather than the map because the content can shrink in the very
2069    /// frame being built — a row of labels at a smaller text size — and that frame should
2070    /// already show the content where it rests, as the reference corrects its position
2071    /// during layout. [`Runtime::keep_scroll_in_range`] retains the same answer afterwards.
2072    /// A finger, a fling or a glide keeps what it has: each of them owns the offset, and
2073    /// each already brings it home when it lets go.
2074    pub fn scroll_offset_within(&self, id: WidgetId, max: (f32, f32)) -> Option<(f32, f32)> {
2075        let offset = self.scroll.get(&id).copied()?;
2076        Some(if self.scroll_at_rest(id) {
2077            inside_extent(offset, max)
2078        } else {
2079            offset
2080        })
2081    }
2082
2083    /// Brings every region **nobody owns** back inside its content, and answers whether any
2084    /// had strayed.
2085    ///
2086    /// An offset at rest can only be out of range because the content or the viewport
2087    /// changed under it: a drag, a fling and a glide all settle inside. Found on a phone in
2088    /// milestone 533 — a row of filters swiped to its end at a large text size stayed
2089    /// swiped once the text was small again and the row fitted, its first label cut at the
2090    /// left and empty room at the right.
2091    ///
2092    /// Corrected at once rather than sprung, as the reference's default physics does for a
2093    /// position that was in range and is not animating. The scrollbar is not summoned for
2094    /// it: nothing scrolled, the content changed.
2095    pub fn keep_scroll_in_range(&mut self, regions: &[Scrollable]) -> bool {
2096        let mut corrected = false;
2097        for area in regions {
2098            let Some(offset) = self.scroll.get(&area.id).copied() else {
2099                continue;
2100            };
2101            let inside = inside_extent(offset, (area.max_x, area.max_y));
2102            if inside == offset || !self.scroll_at_rest(area.id) {
2103                continue;
2104            }
2105            self.scroll.insert(area.id, inside);
2106            if let Some(fade) = self
2107                .scrollbar_fade
2108                .get_mut(&area.id)
2109                .filter(|fade| fade.seen == offset)
2110            {
2111                fade.seen = inside;
2112            }
2113            corrected = true;
2114        }
2115        corrected
2116    }
2117
2118    /// The scroll regions whose offset is worth reporting this frame, and where each is
2119    /// — the regions' half of [`crate::Widget::on_scroll`]. `grain` answers
2120    /// [`crate::Widget::scroll_grain`] for a region; a caller turns each result into a
2121    /// message, the runtime holding none of its own.
2122    ///
2123    /// Three rules, and each of them is a decision:
2124    ///
2125    /// - **A region appearing is not a movement** — as long as it appears where an
2126    ///   application would assume it is. A region seen for the first time at rest at
2127    ///   `(0, 0)` is recorded silently: telling an application on the first frame of
2128    ///   every screen that its list is at the top would only invite it to answer, and it
2129    ///   already knew. A region that appears **somewhere else** — restored, or opened at
2130    ///   a box it was asked to keep in view — is reported, because that one is news.
2131    /// - **A grain suppresses the reports in between, never the last one.** While the
2132    ///   region is moving, nothing under `grain` pixels from the last report is sent; the
2133    ///   moment it comes to rest, a position different from the last reported one is sent
2134    ///   whatever the distance. A coarse grain therefore costs frames, never accuracy.
2135    /// - **The baseline is what was reported**, not the previous frame. Measuring
2136    ///   against the previous frame would let a slow drag creep the whole way down a
2137    ///   list, a fraction of a grain at a time, without ever saying so.
2138    pub fn scroll_changes(
2139        &mut self,
2140        regions: &[Scrollable],
2141        grain: impl Fn(WidgetId) -> f32,
2142    ) -> Vec<(WidgetId, crate::scrollposition::ScrollPosition)> {
2143        let mut changed = Vec::new();
2144        for area in regions {
2145            let offset = self.scroll.get(&area.id).copied().unwrap_or((0.0, 0.0));
2146            let previous = self.scroll_reported.get(&area.id).copied();
2147            let report = match previous {
2148                None => offset != (0.0, 0.0),
2149                Some(previous) if previous == offset => false,
2150                Some(previous) => {
2151                    let grain = grain(area.id).max(0.0);
2152                    let moved = (offset.0 - previous.0)
2153                        .abs()
2154                        .max((offset.1 - previous.1).abs());
2155                    moved >= grain || self.scroll_at_rest(area.id)
2156                }
2157            };
2158            if report {
2159                changed.push((
2160                    area.id,
2161                    crate::scrollposition::ScrollPosition::of(area, offset),
2162                ));
2163            }
2164            if report || previous.is_none() {
2165                self.scroll_reported.insert(area.id, offset);
2166            }
2167        }
2168        self.scroll_reported
2169            .retain(|id, _| regions.iter().any(|area| area.id == *id));
2170        changed
2171    }
2172
2173    /// Moves region `area` where `request` asks. `true` when it was acted on.
2174    ///
2175    /// **A finger refuses it.** An offset has one owner at a time, and while a hand is
2176    /// on the content that owner is the hand — a list that jumped out from under a
2177    /// finger because a timer fired would be the framework overruling the reader.
2178    pub fn scroll_to(
2179        &mut self,
2180        area: &Scrollable,
2181        request: crate::scrollposition::ScrollTo,
2182    ) -> bool {
2183        if request.is_empty() || self.scroll_held == Some(area.id) {
2184            return false;
2185        }
2186        let current = self.scroll.get(&area.id).copied().unwrap_or((0.0, 0.0));
2187        let to = request.resolve(area, current);
2188        // A fling in flight has just been overruled, animated or not: the request is
2189        // the more recent statement of where this list should be.
2190        self.scroll_ballistic.remove(&area.id);
2191        if request.animate {
2192            self.scroll_target.insert(area.id, to);
2193        } else {
2194            self.scroll.insert(area.id, to);
2195            self.scroll_target.remove(&area.id);
2196            self.scroll_velocity.remove(&area.id);
2197        }
2198        true
2199    }
2200
2201    /// A finger takes hold of `id`: from now until [`Runtime::release_scroll`],
2202    /// this region's offset moves only when the finger says so. Any fling in
2203    /// flight is caught, since the finger has just overruled it.
2204    /// Records a change to field `id`, which **was** `before`, `since` seconds after the
2205    /// last one — see [`crate::UndoHistory::record`] for what counts as one step.
2206    pub fn record_edit(
2207        &mut self,
2208        id: WidgetId,
2209        before: crate::undo::EditSnapshot,
2210        kind: crate::undo::EditKind,
2211        since: f32,
2212    ) {
2213        self.undo.entry(id).or_default().record(before, kind, since);
2214    }
2215
2216    /// Ends the run in progress in field `id`: the caret moved, so what is typed next is a
2217    /// step of its own.
2218    pub fn close_edit_run(&mut self, id: WidgetId) {
2219        if let Some(history) = self.undo.get_mut(&id) {
2220            history.close_run();
2221        }
2222    }
2223
2224    /// Steps field `id` back one change, `current` being where it is now. `None` when
2225    /// there is nothing to undo.
2226    pub fn undo_edit(
2227        &mut self,
2228        id: WidgetId,
2229        current: crate::undo::EditSnapshot,
2230    ) -> Option<crate::undo::EditSnapshot> {
2231        self.undo.get_mut(&id)?.undo(current)
2232    }
2233
2234    /// Steps field `id` forward again. `None` when nothing was undone.
2235    pub fn redo_edit(
2236        &mut self,
2237        id: WidgetId,
2238        current: crate::undo::EditSnapshot,
2239    ) -> Option<crate::undo::EditSnapshot> {
2240        self.undo.get_mut(&id)?.redo(current)
2241    }
2242
2243    /// A finger takes hold of the region `id`. A scroll offset has one owner at a time, so
2244    /// nothing else moves it until [`release_scroll`](Self::release_scroll); see
2245    /// [`scroll_held`](Self::scroll_held).
2246    pub fn hold_scroll(&mut self, id: WidgetId) {
2247        self.scroll_held = Some(id);
2248    }
2249
2250    /// The finger lets go: the offset is up for grabs again — a fling, a spring
2251    /// back from an overscroll, or nothing.
2252    pub fn release_scroll(&mut self, id: WidgetId) {
2253        if self.scroll_held == Some(id) {
2254            self.scroll_held = None;
2255        }
2256    }
2257
2258    /// Feeds `overscroll` px of refused movement into the pull of the refresh area
2259    /// `id`, over a scrollable of `extent` px.
2260    ///
2261    /// This is the same measurement the glow is fed — what
2262    /// `apply_boundary_conditions` refused — routed to a different consumer. Where a
2263    /// refresh area is listening, the glow on that edge stands down: two answers to
2264    /// one gesture would say the same thing twice.
2265    pub fn refresh_pull(&mut self, id: WidgetId, overscroll: f32, extent: f32) {
2266        crate::refresh::pull_into(&mut self.refresh, id, overscroll, extent);
2267    }
2268
2269    /// Moves the dismissible `id` by `delta` px along its axis, over an item of
2270    /// `extent` px.
2271    pub fn dismiss_drag(
2272        &mut self,
2273        id: WidgetId,
2274        delta: f32,
2275        extent: f32,
2276        axis: crate::dismiss::DismissAxis,
2277    ) {
2278        crate::dismiss::drag_into(&mut self.dismiss, id, delta, extent, axis);
2279    }
2280
2281    /// The finger lets go of the dismissible `id`. Returns the direction it is being
2282    /// dismissed in, or `None` when it slides back.
2283    pub fn dismiss_release(
2284        &mut self,
2285        id: WidgetId,
2286        velocity: f32,
2287        cross: f32,
2288        axis: crate::dismiss::DismissAxis,
2289        threshold: f32,
2290    ) -> Option<crate::dismiss::DismissDirection> {
2291        crate::dismiss::release_of(&mut self.dismiss, id, velocity, cross, axis, threshold)
2292    }
2293
2294    /// How much of a dismissed item's box is left, while its gap closes. `None` = not
2295    /// collapsing, so the item keeps the size its style asks for.
2296    pub fn dismiss_extent_factor(&self, id: WidgetId) -> Option<f32> {
2297        self.dismiss
2298            .get(&id)
2299            .filter(|s| s.phase() == crate::dismiss::DismissPhase::Collapse)
2300            .map(|s| s.extent_factor())
2301    }
2302
2303    /// Advances every dismissible of the frame by `dt`. Returns `(still animating, the
2304    /// items whose gap has just closed)` — the second being what the shell turns into
2305    /// messages.
2306    pub fn advance_dismiss(
2307        &mut self,
2308        items: &[crate::dismiss::Dismissable],
2309        dt: f32,
2310    ) -> (bool, Vec<(WidgetId, crate::dismiss::DismissDirection)>) {
2311        crate::dismiss::advance_all(&mut self.dismiss, items, dt)
2312    }
2313
2314    /// The height of the sheet `id`, as a share of its box: the retained one, or its
2315    /// initial one while nothing has moved it.
2316    pub fn sheet_size(&self, id: WidgetId, spec: &crate::sheet::SheetSpec) -> f32 {
2317        crate::sheet::size_of(&self.sheets, id, spec)
2318    }
2319
2320    /// Moves the sheet `id` by `delta`, a share of its box — held by the finger until
2321    /// [`Runtime::sheet_release`].
2322    pub fn sheet_drag(
2323        &mut self,
2324        id: WidgetId,
2325        spec: &crate::sheet::SheetSpec,
2326        delta: f32,
2327        available: f32,
2328    ) {
2329        crate::sheet::drag_into(&mut self.sheets, id, spec, delta, available);
2330    }
2331
2332    /// A finger lands on the sheet `id`: a settle in progress stops where it is.
2333    pub fn sheet_hold(&mut self, id: WidgetId) {
2334        crate::sheet::hold_of(&mut self.sheets, id);
2335    }
2336
2337    /// The finger lets go of the sheet `id` at `velocity` px/s, positive growing it, over
2338    /// a box `available` px tall — from `list`, when the finger was on the list inside it,
2339    /// which is then handed whatever is left of a throw that reaches full height.
2340    pub fn sheet_release(
2341        &mut self,
2342        id: WidgetId,
2343        spec: &crate::sheet::SheetSpec,
2344        available: f32,
2345        velocity: f32,
2346        list: Option<WidgetId>,
2347    ) {
2348        crate::sheet::release_of(&mut self.sheets, id, spec, available, velocity, list);
2349    }
2350
2351    /// Asks the sheet `id` for `to` — an application's request, by way of `Command::sheet`.
2352    /// `false` when a finger is on it, which refuses it, as a finger refuses a scroll
2353    /// request.
2354    pub fn sheet_to(
2355        &mut self,
2356        id: WidgetId,
2357        spec: &crate::sheet::SheetSpec,
2358        to: &crate::sheet::SheetTo,
2359    ) -> bool {
2360        crate::sheet::go_to(&mut self.sheets, id, spec, to)
2361    }
2362
2363    /// Advances every sheet of the frame by `dt`. Returns `(still moving, the sheets
2364    /// lowered to nothing on this frame)` — the second being what the shell turns into
2365    /// messages. A throw that reached full height is kept for
2366    /// [`Runtime::take_sheet_handovers`].
2367    pub fn advance_sheets(
2368        &mut self,
2369        areas: &[crate::sheet::SheetArea],
2370        dt: f32,
2371    ) -> (bool, Vec<WidgetId>) {
2372        let step = crate::sheet::advance_all(&mut self.sheets, areas, dt);
2373        self.sheet_handovers.extend(step.handovers);
2374        (step.moving, step.dismissed)
2375    }
2376
2377    /// The throws that carried a sheet to full height since the last call: the list each
2378    /// goes on in, and the velocity to fling it at, in px/s growing its offset.
2379    ///
2380    /// The sheet does not fling the list itself, because a fling is started under the
2381    /// list's physics, and those are the application's to choose.
2382    pub fn take_sheet_handovers(&mut self) -> Vec<(WidgetId, f32)> {
2383        std::mem::take(&mut self.sheet_handovers)
2384    }
2385
2386    /// Calls off the pull of `id` without asking for anything — the list scrolled away
2387    /// from its top edge, or the gesture was cancelled.
2388    pub fn refresh_cancel(&mut self, id: WidgetId) {
2389        crate::refresh::cancel_of(&mut self.refresh, id);
2390    }
2391
2392    /// Ends the pull of `id`. Returns `true` when it was armed, and so when the
2393    /// application should be asked to refresh.
2394    pub fn refresh_release(&mut self, id: WidgetId) -> bool {
2395        crate::refresh::release_of(&mut self.refresh, id)
2396    }
2397
2398    /// Advances every refresh area of the frame by `dt`, reading each one's current
2399    /// `refreshing` flag from `areas`, and drops those that have gone quiet. Returns
2400    /// `true` while any is still moving.
2401    pub fn advance_refresh(&mut self, areas: &[crate::refresh::Refreshable], dt: f32) -> bool {
2402        crate::refresh::advance_all(&mut self.refresh, areas, dt)
2403    }
2404
2405    /// Tells the glow on one edge of `id` that a finger is dragging past it.
2406    ///
2407    /// `overscroll` is the movement the physics **refused** — which is exactly the
2408    /// distance the user asked for and did not get, and so exactly what the glow is
2409    /// there to acknowledge.
2410    ///
2411    /// `indicator` is what the area's scroll behaviour shows past an edge; `None` shows
2412    /// nothing, as on iOS and the desktops (milestone 616).
2413    #[allow(clippy::too_many_arguments)]
2414    pub fn glow_pull(
2415        &mut self,
2416        id: WidgetId,
2417        indicator: Option<crate::physics::OverscrollIndicator>,
2418        edge: GlowEdge,
2419        overscroll: f32,
2420        extent: f32,
2421        cross_offset: f32,
2422        cross_extent: f32,
2423    ) {
2424        let Some(indicator) = indicator else {
2425            return;
2426        };
2427        if overscroll.abs() < 1e-3 {
2428            return;
2429        }
2430        if indicator == crate::physics::OverscrollIndicator::Stretch {
2431            self.scroll_stretch
2432                .entry(id)
2433                .or_default()
2434                .axis_mut(edge)
2435                .pull(overscroll, crate::stretch::is_start(edge), extent);
2436            return;
2437        }
2438        self.scroll_glow.entry(id).or_default().edge_mut(edge).pull(
2439            overscroll,
2440            extent,
2441            cross_offset,
2442            cross_extent,
2443        );
2444    }
2445
2446    /// Tells the glow on one edge of `id` that a fling just landed on it.
2447    pub fn glow_absorb(
2448        &mut self,
2449        id: WidgetId,
2450        indicator: Option<crate::physics::OverscrollIndicator>,
2451        edge: GlowEdge,
2452        velocity: f32,
2453    ) {
2454        let Some(indicator) = indicator else {
2455            return;
2456        };
2457        if indicator == crate::physics::OverscrollIndicator::Stretch {
2458            self.scroll_stretch
2459                .entry(id)
2460                .or_default()
2461                .axis_mut(edge)
2462                .absorb_impact(velocity, crate::stretch::is_start(edge));
2463            return;
2464        }
2465        self.scroll_glow
2466            .entry(id)
2467            .or_default()
2468            .edge_mut(edge)
2469            .absorb_impact(velocity);
2470    }
2471
2472    /// Tells every glow of `id` that the gesture is over, so a held pull can fade.
2473    pub fn glow_scroll_end(&mut self, id: WidgetId) {
2474        if let Some(glows) = self.scroll_glow.get_mut(&id) {
2475            glows.scroll_end();
2476        }
2477        self.stretch_release(id);
2478    }
2479
2480    /// Lets the stretch of `id` go: the content moved again, and the reference releases a
2481    /// stretch as soon as it scrolls (milestone 591).
2482    pub fn stretch_release(&mut self, id: WidgetId) {
2483        if let Some(stretch) = self.scroll_stretch.get_mut(&id) {
2484            stretch.release();
2485        }
2486    }
2487
2488    /// Advances every glow by `dt`, dropping those that have gone quiet. Returns
2489    /// `true` while any is still animating.
2490    pub fn advance_glow(&mut self, dt: f32) -> bool {
2491        let mut animating = false;
2492        self.scroll_stretch.retain(|_, stretch| {
2493            animating |= stretch.advance(dt);
2494            !stretch.is_idle()
2495        });
2496        self.scroll_glow.retain(|_, glows| {
2497            animating |= glows.advance(dt);
2498            !glows.is_idle()
2499        });
2500        animating
2501    }
2502
2503    /// A finger landed on the inked surface `id`, at `origin` in the surface's own
2504    /// coordinates, on a box of `size`. Starts a splash.
2505    pub fn ink_press(&mut self, id: WidgetId, origin: Point, size: Size) {
2506        self.ink.entry(id).or_default().press(origin, size);
2507    }
2508
2509    /// The tap on `id` completed: its splash finishes growing and begins to fade.
2510    pub fn ink_confirm(&mut self, id: WidgetId) {
2511        if let Some(ripples) = self.ink.get_mut(&id) {
2512            ripples.confirm();
2513        }
2514    }
2515
2516    /// Advances every splash by `dt`, dropping those that have gone. Returns `true`
2517    /// while any is still moving.
2518    ///
2519    /// A splash still waiting for a finger that is no longer down is cancelled here
2520    /// rather than left hanging. A release that lands on the widget confirms it first
2521    /// (the shell calls [`Runtime::ink_confirm`] before the next frame); anything else
2522    /// — the finger slid off, the widget was unmounted mid-press, a gesture was taken
2523    /// over by a scroll — leaves nothing to confirm it, and without this sweep the ink
2524    /// would sit there for the life of the application.
2525    pub fn advance_ink(&mut self, dt: f32) -> bool {
2526        if self.ink.is_empty() {
2527            return false;
2528        }
2529        let pressed = self.input.pressed;
2530        let mut animating = false;
2531        self.ink.retain(|id, ripples| {
2532            if Some(*id) != pressed {
2533                ripples.cancel();
2534            }
2535            animating |= ripples.advance(dt);
2536            !ripples.is_idle()
2537        });
2538        animating
2539    }
2540
2541    /// Cancels any fling on `id` — a new gesture, a wheel notch or a programmatic
2542    /// scroll takes precedence over momentum from the last one.
2543    pub fn stop_scroll_fling(&mut self, id: WidgetId) -> Option<ScrollBallistic> {
2544        self.scroll_ballistic.remove(&id)
2545    }
2546    /// Catches a fling under a new press: stops it, and returns the momentum the
2547    /// next release inherits from it, per axis, in px/s.
2548    ///
2549    /// It is what makes repeated swipes accelerate on the platforms that do that;
2550    /// where they do not, [`ScrollPhysics::carried_momentum`] returns zero and this
2551    /// is simply a stop.
2552    pub fn catch_scroll_fling(&mut self, id: WidgetId, physics: ScrollPhysics) -> (f32, f32) {
2553        let Some(fling) = self.scroll_ballistic.remove(&id) else {
2554            return (0.0, 0.0);
2555        };
2556        let carried = |sim: Option<Ballistic>| {
2557            sim.map(|s| physics.carried_momentum(s.dx(fling.elapsed)))
2558                .unwrap_or(0.0)
2559        };
2560        (carried(fling.x), carried(fling.y))
2561    }
2562
2563    /// Advances the pan *fling* of the interactive viewports: each velocity moves the
2564    /// translation (decelerated by exponential friction) and is then **clamped** to its
2565    /// viewport — hitting an edge cancels the velocity on that axis. `viewports` supplies
2566    /// the viewport of each interactive viewer (from the current frame). Returns `true`
2567    /// if a fling is still running.
2568    pub fn advance_interactive(&mut self, viewports: &[(WidgetId, Rect)], dt: f32) -> bool {
2569        use crate::interactive::{PAN_FRICTION, PAN_MIN_VELOCITY};
2570        if self.interactive_velocity.is_empty() {
2571            return false;
2572        }
2573        let ids: Vec<WidgetId> = self.interactive_velocity.keys().copied().collect();
2574        let decay = (-PAN_FRICTION * dt).exp();
2575        let mut animating = false;
2576        for id in ids {
2577            let mut v = self
2578                .interactive_velocity
2579                .get(&id)
2580                .copied()
2581                .unwrap_or((0.0, 0.0));
2582            let mut view = self.interactive.get(&id).copied().unwrap_or_default();
2583            let moved = view.pan(v.0 * dt, v.1 * dt);
2584            // Clamping: an axis that hits a bound cancels its velocity (no bounce).
2585            if let Some((_, vp)) = viewports.iter().find(|(i, _)| *i == id) {
2586                let clamped = moved.clamped(*vp);
2587                if (clamped.tx - moved.tx).abs() > 1e-3 {
2588                    v.0 = 0.0;
2589                }
2590                if (clamped.ty - moved.ty).abs() > 1e-3 {
2591                    v.1 = 0.0;
2592                }
2593                view = clamped;
2594            } else {
2595                view = moved;
2596            }
2597            self.interactive.insert(id, view);
2598            v.0 *= decay;
2599            v.1 *= decay;
2600            if v.0.hypot(v.1) < PAN_MIN_VELOCITY {
2601                self.interactive_velocity.remove(&id);
2602            } else {
2603                self.interactive_velocity.insert(id, v);
2604                animating = true;
2605            }
2606        }
2607        animating
2608    }
2609
2610    /// Fades out the opacity of outgoing subtrees; forgets those that have reached
2611    /// 0. Returns `true` if an exit is still running.
2612    pub fn advance_leaving(&mut self, dt: f32) -> bool {
2613        let step = if ANIM_DURATION > 0.0 {
2614            dt / ANIM_DURATION
2615        } else {
2616            1.0
2617        };
2618        let mut animating = false;
2619        self.leaving.retain(|_, (_, opacity)| {
2620            *opacity -= step;
2621            if *opacity > 0.0 {
2622                animating = true;
2623                true
2624            } else {
2625                false
2626            }
2627        });
2628        animating
2629    }
2630}
2631
2632#[cfg(test)]
2633mod tests {
2634    use super::*;
2635
2636    /// A vertical scroll region `max` px long, in a 400 px viewport, with no
2637    /// physics of its own.
2638    fn region(id: WidgetId, max: f32) -> Scrollable {
2639        Scrollable {
2640            id,
2641            viewport: Rect::new(0.0, 0.0, 300.0, 400.0),
2642            max_x: 0.0,
2643            max_y: max,
2644            axis: crate::scroll::Axis::Vertical,
2645            physics: crate::ScrollPhysics::CLAMPING,
2646            overscroll: Some(crate::OverscrollIndicator::Glow),
2647            fling: frus_core::VelocityStrategy::Regression,
2648            refresh: None,
2649            page: None,
2650            reverse_x: false,
2651            reverse_y: false,
2652            host: None,
2653            keep_visible: None,
2654        }
2655    }
2656
2657    /// The same region, with these physics settled on it, as its scroll behaviour would.
2658    fn region_with(physics: ScrollPhysics, id: WidgetId, max: f32) -> Scrollable {
2659        Scrollable {
2660            physics,
2661            ..region(id, max)
2662        }
2663    }
2664
2665    /// A three-page horizontal view, 300 px wide, asking for `requested`.
2666    fn paged_region(id: WidgetId, requested: usize) -> Scrollable {
2667        Scrollable {
2668            id,
2669            viewport: Rect::new(0.0, 0.0, 300.0, 400.0),
2670            max_x: 600.0,
2671            max_y: 0.0,
2672            axis: crate::scroll::Axis::Vertical,
2673            physics: crate::ScrollPhysics::CLAMPING,
2674            overscroll: Some(crate::OverscrollIndicator::Glow),
2675            fling: frus_core::VelocityStrategy::Regression,
2676            refresh: None,
2677            page: Some(crate::PageSnap {
2678                snapping: true,
2679                extent: 300.0,
2680                count: 3,
2681                requested,
2682                horizontal: true,
2683            }),
2684            reverse_x: false,
2685            reverse_y: false,
2686            host: None,
2687            keep_visible: None,
2688        }
2689    }
2690
2691    #[test]
2692    fn a_paged_release_springs_to_a_page_and_never_coasts() {
2693        let id = WidgetId::ROOT.child(0);
2694        let mut rt = Runtime::default();
2695        let area = paged_region(id, 0);
2696        rt.scroll.insert(id, (40.0, 0.0));
2697        // A flick far too slow to fling an ordinary list still turns the page.
2698        assert!(rt.fling_scroll(area, ScrollPhysics::CLAMPING, (60.0, 0.0)));
2699        for _ in 0..200 {
2700            rt.advance_scroll(&[area], 1.0 / 60.0);
2701        }
2702        let (x, _) = rt.scroll[&id];
2703        assert!((x - 300.0).abs() < 1.0, "settled at {x}");
2704    }
2705
2706    #[test]
2707    fn a_hard_fling_on_a_paged_view_crosses_one_page() {
2708        let id = WidgetId::ROOT.child(0);
2709        let mut rt = Runtime::default();
2710        let area = paged_region(id, 0);
2711        rt.scroll.insert(id, (10.0, 0.0));
2712        rt.fling_scroll(area, ScrollPhysics::CLAMPING, (7000.0, 0.0));
2713        for _ in 0..300 {
2714            rt.advance_scroll(&[area], 1.0 / 60.0);
2715        }
2716        let (x, _) = rt.scroll[&id];
2717        assert!((x - 300.0).abs() < 1.0, "a fling must not skip pages: {x}");
2718    }
2719
2720    #[test]
2721    fn the_page_asked_for_is_taken_on_the_first_sighting_and_on_a_change() {
2722        let id = WidgetId::ROOT.child(0);
2723        let mut rt = Runtime::default();
2724        // First sighting: the view opens there, with no animation to watch.
2725        rt.sync_pages(&[paged_region(id, 2)]);
2726        assert_eq!(rt.scroll[&id], (600.0, 0.0));
2727        assert!(!rt.scroll_target.contains_key(&id));
2728
2729        // Asked again for the same page while the finger has moved it: left alone,
2730        // or the view could not be swiped at all.
2731        rt.scroll.insert(id, (450.0, 0.0));
2732        rt.sync_pages(&[paged_region(id, 2)]);
2733        assert_eq!(rt.scroll[&id], (450.0, 0.0));
2734
2735        // A new page: a glide, not a jump.
2736        rt.sync_pages(&[paged_region(id, 0)]);
2737        assert_eq!(rt.scroll[&id], (450.0, 0.0));
2738        assert_eq!(rt.scroll_target[&id], (0.0, 0.0));
2739    }
2740
2741    #[test]
2742    fn a_page_change_is_reported_once_and_never_on_arrival() {
2743        let id = WidgetId::ROOT.child(0);
2744        let mut rt = Runtime::default();
2745        let area = paged_region(id, 1);
2746        // The view appearing is not a page change, whatever page it opens on.
2747        assert!(rt.page_changes(&[area]).is_empty());
2748
2749        // Half a page across: the rounding has not tipped yet.
2750        rt.scroll.insert(id, (440.0, 0.0));
2751        assert!(rt.page_changes(&[area]).is_empty());
2752        // Past the middle: reported, and only once.
2753        rt.scroll.insert(id, (460.0, 0.0));
2754        assert_eq!(rt.page_changes(&[area]), vec![(id, 2)]);
2755        rt.scroll.insert(id, (600.0, 0.0));
2756        assert!(rt.page_changes(&[area]).is_empty());
2757    }
2758
2759    #[test]
2760    fn hover_rises_then_falls_and_clears() {
2761        let id = WidgetId::ROOT.child(0);
2762        let mut rt = Runtime::default();
2763        rt.input.hovered = Some(id);
2764
2765        // Hovered: small steps → the progress rises without reaching 1.
2766        assert!(rt.advance(0.03)); // ~0.25, still running
2767        assert!(rt.advance(0.03)); // ~0.5, still running
2768        let p = rt.hover_progress(id);
2769        assert!(p > 0.4 && p < 0.6, "progress = {p}");
2770
2771        // A large step: reaches 1.0 and stays there (no more animation).
2772        rt.advance(1.0);
2773        assert_eq!(rt.hover_progress(id), 1.0);
2774        assert!(!rt.advance(0.03));
2775
2776        // End of hover: falls back (still running), then reaches 0 and the entry disappears.
2777        rt.input.hovered = None;
2778        assert!(rt.advance(0.03));
2779        rt.advance(1.0);
2780        assert_eq!(rt.hover_progress(id), 0.0);
2781        assert!(rt.anims.is_empty());
2782    }
2783
2784    /// **A press rises and falls** (milestone 441).
2785    ///
2786    /// It was a flag and nothing else, so a state layer reached full the instant a finger
2787    /// landed and vanished the instant it left. The reference fades its press highlight
2788    /// in and out instead (`ink_highlight.dart:62`), and this is that fade in the shape
2789    /// hover and focus already had here.
2790    #[test]
2791    fn a_press_rises_then_falls_back() {
2792        let id = WidgetId::ROOT.child(3);
2793        let mut rt = Runtime::default();
2794        rt.input.hovered = Some(id);
2795        rt.input.pressed = Some(id);
2796
2797        // Under the finger: part way there, still running.
2798        assert!(rt.advance(0.05));
2799        let p = rt.press_progress(id);
2800        assert!(p > 0.0 && p < 1.0, "on the way in: {p}");
2801        rt.advance(1.0);
2802        assert_eq!(rt.press_progress(id), 1.0);
2803
2804        // Let go: it comes back down rather than disappearing, which is the same jump
2805        // the other way round.
2806        rt.input.pressed = None;
2807        assert!(rt.advance(0.05));
2808        let p = rt.press_progress(id);
2809        assert!(p > 0.0 && p < 1.0, "on the way back: {p}");
2810        rt.advance(1.0);
2811        assert_eq!(rt.press_progress(id), 0.0);
2812    }
2813
2814    /// And it is the **slower** of the two clocks: the reference gives the press 200 ms
2815    /// against the 50 it gives hover and focus (`ink_well.dart:995`). A press is the one
2816    /// interaction the pointer commits to, and it is drawn slowly enough to be seen.
2817    #[test]
2818    fn a_press_arrives_more_slowly_than_a_hover() {
2819        let id = WidgetId::ROOT.child(4);
2820        let mut rt = Runtime::default();
2821        rt.input.hovered = Some(id);
2822        rt.input.pressed = Some(id);
2823        rt.advance(0.06);
2824        let (press, hover) = (rt.press_progress(id), rt.hover_progress(id));
2825        assert!(press < hover, "press {press} should trail hover {hover}");
2826        assert!(press > 0.0, "but it is under way");
2827    }
2828
2829    /// A press counts only while the pointer is **still on the widget** — the rule
2830    /// `InputState::status_for` applies, so the progression and the flag can never
2831    /// disagree about what is happening.
2832    #[test]
2833    fn a_press_dragged_off_the_widget_comes_back_down() {
2834        let id = WidgetId::ROOT.child(5);
2835        let elsewhere = WidgetId::ROOT.child(6);
2836        let mut rt = Runtime::default();
2837        rt.input.hovered = Some(id);
2838        rt.input.pressed = Some(id);
2839        rt.advance(1.0);
2840        assert_eq!(rt.press_progress(id), 1.0);
2841
2842        // Still held, but the finger has slid away: not a press any more. Long enough
2843        // for the hover to have finished falling and the press not to have — which is
2844        // the case the forget-predicate has to survive, since an entry with nothing left
2845        // *but* a press is exactly what a release looks like on the way out.
2846        rt.input.hovered = Some(elsewhere);
2847        assert!(rt.advance(0.13));
2848        assert_eq!(rt.hover_progress(id), 0.0, "the hover is done");
2849        let p = rt.press_progress(id);
2850        assert!(
2851            p > 0.0 && p < 1.0,
2852            "and the press is still coming down: {p}"
2853        );
2854
2855        rt.advance(1.0);
2856        assert_eq!(rt.press_progress(id), 0.0);
2857    }
2858
2859    #[test]
2860    fn focus_animates_independently() {
2861        let id = WidgetId::ROOT.child(1);
2862        let mut rt = Runtime::default();
2863        rt.input.focused = Some(id);
2864        rt.advance(1.0);
2865        assert_eq!(rt.focus_progress(id), 1.0);
2866        assert_eq!(rt.hover_progress(id), 0.0);
2867    }
2868
2869    #[test]
2870    fn opacity_rises_to_one() {
2871        let id = WidgetId::ROOT.child(2);
2872        let mut rt = Runtime::default();
2873        // Mount: starts transparent.
2874        rt.anims.insert(
2875            id,
2876            Anim {
2877                opacity: 0.0,
2878                ..Default::default()
2879            },
2880        );
2881        assert!(rt.advance(0.03));
2882        let o = rt.opacity(id);
2883        assert!(o > 0.0 && o < 1.0, "opacity = {o}");
2884        rt.advance(1.0);
2885        assert_eq!(rt.opacity(id), 1.0);
2886        // Default without an entry: opaque.
2887        assert_eq!(rt.opacity(WidgetId::ROOT), 1.0);
2888    }
2889
2890    /// **Motion off means the change lands at once, not that it does not land.**
2891    ///
2892    /// The user asked for animations to be reduced or removed, so an implicit animation
2893    /// completes in the frame it starts rather than over its duration. Skipping the
2894    /// change instead would leave the interface *wrong* rather than *still*, which is
2895    /// the failure this setting is most often given.
2896    ///
2897    /// The check is both halves: the value has arrived, **and** nothing is still asking
2898    /// for another frame.
2899    #[test]
2900    fn motion_off_lands_the_change_at_once() {
2901        // A switch mounts at its target, so the animation is what happens on the
2902        // **change** — which is exactly what this setting is about.
2903        let off: crate::Switch<()> = crate::Switch::new(false);
2904        let on: crate::Switch<()> = crate::Switch::new(true);
2905
2906        let mut moving = Runtime::default();
2907        assert!(!moving.advance_values(&off, 1.0));
2908        assert!(
2909            moving.advance_values(&on, 0.03),
2910            "a live animation owes the next frame to itself"
2911        );
2912        let part_way = moving.value(WidgetId::ROOT);
2913        assert!(part_way > 0.0 && part_way < 1.0, "part way: {part_way}");
2914
2915        let mut stopped = Runtime {
2916            still: true,
2917            ..Default::default()
2918        };
2919        assert!(!stopped.advance_values(&off, 1.0));
2920        let more = stopped.advance_values(&on, 0.03);
2921        assert!(!more, "motion off: nothing is left to animate");
2922        assert_eq!(
2923            stopped.value(WidgetId::ROOT),
2924            1.0,
2925            "and the value the widget asked for has actually arrived"
2926        );
2927    }
2928
2929    /// **A value a finger holds stays where it was put** (milestone 638), whatever its
2930    /// target; let go, it travels straight to the target in the share of the duration the
2931    /// distance left is, and is no longer *placed* once it arrives.
2932    #[test]
2933    fn a_held_value_stays_and_settles_from_where_it_was_left() {
2934        let off: crate::Switch<()> = crate::Switch::new(false);
2935        let mut rt = Runtime::default();
2936        rt.advance_values(&off, 1.0);
2937        rt.hold_value(WidgetId::ROOT, 0.25);
2938        assert!(rt.value_placed(WidgetId::ROOT));
2939        assert!(!rt.advance_values(&off, 1.0), "held: nothing moves");
2940        assert_eq!(rt.value(WidgetId::ROOT), 0.25);
2941
2942        rt.release_value(WidgetId::ROOT);
2943        // A quarter of the way to go is a quarter of the 300 ms: half of it is 37.5 ms.
2944        assert!(rt.advance_values(&off, 0.0375));
2945        let half = rt.value(WidgetId::ROOT);
2946        assert!((half - 0.125).abs() < 1e-4, "linear, half way: {half}");
2947        assert!(rt.value_placed(WidgetId::ROOT), "still settling");
2948        rt.advance_values(&off, 0.0375);
2949        assert_eq!(rt.value(WidgetId::ROOT), 0.0);
2950        rt.advance_values(&off, 0.0);
2951        assert!(
2952            !rt.value_placed(WidgetId::ROOT),
2953            "arrived: no longer placed"
2954        );
2955    }
2956
2957    #[test]
2958    fn value_snaps_on_mount_then_animates() {
2959        let mut rt = Runtime::default();
2960        // Mounting a switch that is off: adopts the target (0) with no animation.
2961        let off: crate::Switch<()> = crate::Switch::new(false);
2962        assert!(!rt.advance_values(&off, 1.0));
2963        assert_eq!(rt.value(WidgetId::ROOT), 0.0);
2964
2965        // Toggled on: the value rises towards 1 in small steps.
2966        let on: crate::Switch<()> = crate::Switch::new(true);
2967        assert!(rt.advance_values(&on, 0.03));
2968        let v = rt.value(WidgetId::ROOT);
2969        assert!(v > 0.0 && v < 1.0, "value = {v}");
2970        rt.advance_values(&on, 1.0);
2971        assert_eq!(rt.value(WidgetId::ROOT), 1.0);
2972
2973        // Widget gone: the value is forgotten.
2974        let empty: crate::Container<()> = crate::Container::new();
2975        rt.advance_values(&empty, 1.0);
2976        assert!(rt.values.is_empty());
2977    }
2978
2979    /// Minimal widget exposing a tunable animated value (target, duration, curve) —
2980    /// to test the timeline without depending on a concrete widget.
2981    struct Mock {
2982        target: f32,
2983        duration: f32,
2984        curve: Curve,
2985    }
2986
2987    impl crate::widget::Widget<()> for Mock {
2988        fn style(&self) -> frus_layout::Style {
2989            frus_layout::Style::default()
2990        }
2991        fn children(&self) -> &[Box<dyn crate::widget::Widget<()>>] {
2992            &[]
2993        }
2994        fn paint(
2995            &self,
2996            _bounds: frus_core::Rect,
2997            _status: crate::interaction::Status,
2998            _theme: &crate::theme::Theme,
2999            _scene: &mut frus_core::Scene,
3000        ) {
3001        }
3002        fn on_click(&self) -> Option<()> {
3003            None
3004        }
3005        fn anim_target(&self) -> Option<f32> {
3006            Some(self.target)
3007        }
3008        fn anim_duration(&self) -> f32 {
3009            self.duration
3010        }
3011        fn anim_curve(&self) -> Curve {
3012            self.curve.clone()
3013        }
3014    }
3015
3016    /// The **curve** shapes the trajectory: at t=0.25 an *ease-in* lags behind the
3017    /// linear progress and an *ease-out* runs ahead; all of them converge on the
3018    /// target.
3019    #[test]
3020    fn curve_shapes_the_value_timeline() {
3021        let id = WidgetId::ROOT;
3022        let dt = 0.03; // t = 0.25 over a duration of 0.12
3023        let dur = 0.12;
3024        let sample = |curve: Curve| {
3025            let mut rt = Runtime::default();
3026            rt.set_value(id, 0.0);
3027            rt.advance_values(
3028                &Mock {
3029                    target: 1.0,
3030                    duration: dur,
3031                    curve,
3032                },
3033                dt,
3034            );
3035            (rt.value(id), rt)
3036        };
3037        let (ein, mut rt_in) = sample(Curve::ease_in());
3038        let (eout, mut rt_out) = sample(Curve::ease_out());
3039        let (lin, mut rt_lin) = sample(Curve::Linear);
3040
3041        assert!((lin - 0.25).abs() < 1e-3, "linear = t: {lin}");
3042        assert!(ein < 0.25, "ease-in lags: {ein}");
3043        assert!(eout > 0.25, "ease-out leads: {eout}");
3044
3045        // A large step: all of them reach the target (the curves end at 1).
3046        for rt in [&mut rt_in, &mut rt_out, &mut rt_lin] {
3047            rt.advance_values(
3048                &Mock {
3049                    target: 1.0,
3050                    duration: dur,
3051                    curve: Curve::Linear,
3052                },
3053                1.0,
3054            );
3055        }
3056        assert_eq!(rt_in.value(id), 1.0);
3057        assert_eq!(rt_out.value(id), 1.0);
3058        assert_eq!(rt_lin.value(id), 1.0);
3059    }
3060
3061    /// The animated colour **snaps** on mount then **tweens** on a change of target,
3062    /// channel by channel; a widget that has gone is forgotten.
3063    #[test]
3064    fn animated_color_tweens_between_frames() {
3065        let id = WidgetId::ROOT;
3066        let red = Color::rgb(1.0, 0.0, 0.0);
3067        let blue = Color::rgb(0.0, 0.0, 1.0);
3068        let mut rt = Runtime::default();
3069
3070        // Mounting on red: adopts the target with no transition.
3071        let start: crate::Container<()> =
3072            crate::Container::new().animated_color(red, 0.10, Curve::Linear);
3073        assert!(!rt.advance_colors(&start, 1.0));
3074        assert_eq!(rt.anim_color(id), Some(red));
3075
3076        // Blue target: linear tween, halfway ≈ (0.5, 0, 0.5).
3077        let to_blue: crate::Container<()> =
3078            crate::Container::new().animated_color(blue, 0.10, Curve::Linear);
3079        assert!(rt.advance_colors(&to_blue, 0.05));
3080        let mid = rt.anim_color(id).unwrap();
3081        assert!(
3082            (mid.r - 0.5).abs() < 0.05 && (mid.b - 0.5).abs() < 0.05,
3083            "mi-parcours = {mid:?}"
3084        );
3085
3086        // The end: blue reached.
3087        rt.advance_colors(&to_blue, 1.0);
3088        assert_eq!(rt.anim_color(id), Some(blue));
3089
3090        // Widget gone: the colour is forgotten.
3091        let empty: crate::Container<()> = crate::Container::new();
3092        rt.advance_colors(&empty, 1.0);
3093        assert_eq!(rt.anim_color(id), None);
3094    }
3095
3096    /// The animated size **snaps** on mount then **tweens** on a change of target
3097    /// (width/height); a widget that has gone is forgotten.
3098    #[test]
3099    fn animated_size_tweens_between_frames() {
3100        let id = WidgetId::ROOT;
3101        let mut rt = Runtime::default();
3102
3103        let small: crate::Container<()> =
3104            crate::Container::new().animated_size(20.0, 20.0, 0.10, Curve::Linear);
3105        assert!(!rt.advance_sizes(&small, 1.0));
3106        assert_eq!(rt.anim_size(id), Some(Size::new(20.0, 20.0)));
3107
3108        // 40×40 target: halfway through a linear tween ≈ 30×30.
3109        let big: crate::Container<()> =
3110            crate::Container::new().animated_size(40.0, 40.0, 0.10, Curve::Linear);
3111        assert!(rt.advance_sizes(&big, 0.05));
3112        let mid = rt.anim_size(id).unwrap();
3113        assert!(
3114            (mid.width - 30.0).abs() < 0.5 && (mid.height - 30.0).abs() < 0.5,
3115            "mi-parcours = {mid:?}"
3116        );
3117
3118        rt.advance_sizes(&big, 1.0);
3119        assert_eq!(rt.anim_size(id), Some(Size::new(40.0, 40.0)));
3120
3121        let empty: crate::Container<()> = crate::Container::new();
3122        rt.advance_sizes(&empty, 1.0);
3123        assert_eq!(rt.anim_size(id), None);
3124    }
3125
3126    /// The animated corner radius **snaps** on mount then **tweens** on a change of
3127    /// target (per corner); a widget that has gone is forgotten.
3128    #[test]
3129    fn animated_radius_tweens_between_frames() {
3130        let id = WidgetId::ROOT;
3131        let mut rt = Runtime::default();
3132
3133        let sharp: crate::Container<()> =
3134            crate::Container::new().animated_radius(0.0, 0.10, Curve::Linear);
3135        assert!(!rt.advance_radii(&sharp, 1.0));
3136        assert_eq!(rt.anim_radius(id), Some(BorderRadius::from(0.0)));
3137
3138        // Target 20: halfway through a linear tween ≈ 10.
3139        let round: crate::Container<()> =
3140            crate::Container::new().animated_radius(20.0, 0.10, Curve::Linear);
3141        assert!(rt.advance_radii(&round, 0.05));
3142        let mid = rt.anim_radius(id).unwrap();
3143        assert!(
3144            (mid.top_left - 10.0).abs() < 0.5,
3145            "mi-parcours = {}",
3146            mid.top_left
3147        );
3148
3149        rt.advance_radii(&round, 1.0);
3150        assert_eq!(rt.anim_radius(id), Some(BorderRadius::from(20.0)));
3151
3152        let empty: crate::Container<()> = crate::Container::new();
3153        rt.advance_radii(&empty, 1.0);
3154        assert_eq!(rt.anim_radius(id), None);
3155    }
3156
3157    /// The animated padding **snaps** on mount then **tweens** on a change of target
3158    /// (per side); a widget that has gone is forgotten.
3159    #[test]
3160    fn animated_padding_tweens_between_frames() {
3161        let id = WidgetId::ROOT;
3162        let mut rt = Runtime::default();
3163
3164        let p0: crate::Container<()> =
3165            crate::Container::new().animated_padding(0.0, 0.10, Curve::Linear);
3166        assert!(!rt.advance_paddings(&p0, 1.0));
3167        assert_eq!(rt.anim_padding(id), Some(Insets::uniform(0.0)));
3168
3169        let p20: crate::Container<()> =
3170            crate::Container::new().animated_padding(20.0, 0.10, Curve::Linear);
3171        assert!(rt.advance_paddings(&p20, 0.05)); // t = 0.5 → 10
3172        let mid = rt.anim_padding(id).unwrap();
3173        assert!((mid.left - 10.0).abs() < 0.5, "mi-parcours = {}", mid.left);
3174
3175        rt.advance_paddings(&p20, 1.0);
3176        assert_eq!(rt.anim_padding(id), Some(Insets::uniform(20.0)));
3177
3178        let empty: crate::Container<()> = crate::Container::new();
3179        rt.advance_paddings(&empty, 1.0);
3180        assert_eq!(rt.anim_padding(id), None);
3181    }
3182
3183    /// The **duration** sets the speed: for the same `dt`, a shorter transition is
3184    /// further along.
3185    #[test]
3186    fn shorter_duration_animates_faster() {
3187        let id = WidgetId::ROOT;
3188        let advance = |duration: f32| {
3189            let mut rt = Runtime::default();
3190            rt.set_value(id, 0.0);
3191            rt.advance_values(
3192                &Mock {
3193                    target: 1.0,
3194                    duration,
3195                    curve: Curve::Linear,
3196                },
3197                0.025,
3198            );
3199            rt.value(id)
3200        };
3201        let fast = advance(0.05); // t = 0.5
3202        let slow = advance(0.20); // t = 0.125
3203        assert!(
3204            fast > slow,
3205            "shorter duration further along: {fast} vs {slow}"
3206        );
3207        assert!((fast - 0.5).abs() < 1e-3, "fast = {fast}");
3208        assert!((slow - 0.125).abs() < 1e-3, "slow = {slow}");
3209    }
3210
3211    #[test]
3212    fn spring_ease_is_monotonic_no_overshoot() {
3213        assert!((spring_ease(0.0) - 0.0).abs() < 1e-6);
3214        assert!((spring_ease(1.0) - 1.0).abs() < 1e-6);
3215        // Increasing and bounded to [0,1] (no overshoot beyond 1).
3216        let mut prev = 0.0;
3217        for i in 0..=100 {
3218            let v = spring_ease(i as f32 / 100.0);
3219            assert!(v >= prev - 1e-6, "decreases at {i}");
3220            assert!(v <= 1.0 + 1e-6, "exceeds 1 at {i}");
3221            prev = v;
3222        }
3223        // Already well advanced at halfway (a gentle arrival at the end).
3224        assert!(spring_ease(0.5) > 0.7);
3225        // Clamped outside the domain.
3226        assert_eq!(spring_ease(-1.0), 0.0);
3227        assert_eq!(spring_ease(2.0), 1.0);
3228    }
3229
3230    #[test]
3231    fn scroll_springs_to_target_and_settles() {
3232        let id = WidgetId::ROOT;
3233        let mut rt = Runtime::default();
3234        rt.scroll_target.insert(id, (0.0, 100.0));
3235        rt.scroll_velocity.insert(id, (0.0, 0.0));
3236        let regions = [region(id, 200.0)];
3237        for _ in 0..600 {
3238            if !rt.advance_scroll(&regions, 0.016) {
3239                break;
3240            }
3241        }
3242        let (_, y) = rt.scroll.get(&id).copied().unwrap();
3243        assert!((y - 100.0).abs() < 1.0, "reached the target: {y}");
3244        assert!(
3245            !rt.scroll_target.contains_key(&id),
3246            "animation state cleared at rest"
3247        );
3248    }
3249
3250    /// **A scrollbar arrives when its area moves, and leaves once it has been still**
3251    /// (`scrollbar.dart:17`, `:18`): 600 ms of stillness, then 300 ms of going.
3252    ///
3253    /// Nothing announces that movement. The offset is written by the drag handler, by
3254    /// the fling and by the spring, and none of the three leaves word — so this watches
3255    /// the value itself, which catches all three and whatever is added next.
3256    #[test]
3257    fn a_bar_arrives_on_movement_and_goes_again() {
3258        let id = WidgetId::ROOT;
3259        let mut rt = Runtime::default();
3260        let regions = [region(id, 200.0)];
3261        let run = |rt: &mut Runtime, frames: usize| {
3262            for _ in 0..frames {
3263                rt.advance_scroll(&regions, 0.016);
3264            }
3265        };
3266
3267        // An area nobody has scrolled has no bar at all.
3268        run(&mut rt, 10);
3269        assert_eq!(
3270            rt.scrollbar_fade_of(id).opacity,
3271            0.0,
3272            "untouched: no bar to see"
3273        );
3274
3275        // Moved by something — anything — and it comes, over `BAR_FADE`.
3276        rt.scroll.insert(id, (0.0, 40.0));
3277        run(&mut rt, 1);
3278        let arriving = rt.scrollbar_fade_of(id).opacity;
3279        assert!(
3280            arriving > 0.0 && arriving < 1.0,
3281            "on its way in, not snapped: {arriving}"
3282        );
3283        run(&mut rt, 20);
3284        assert!(rt.scrollbar_fade_of(id).opacity > 0.99, "there");
3285
3286        // Still for less than `BAR_TIME_TO_FADE` all told: it stays put.
3287        run(&mut rt, 10);
3288        assert!(
3289            rt.scrollbar_fade_of(id).opacity > 0.99,
3290            "half a second of stillness is not enough to send it away"
3291        );
3292
3293        // And then it goes.
3294        run(&mut rt, 80);
3295        assert_eq!(rt.scrollbar_fade_of(id).opacity, 0.0, "gone");
3296    }
3297
3298    /// **A held thumb keeps its bar**, however long the hand pauses on it
3299    /// (`scrollbar.dart:1970`), and so does a bar a pointer is resting near — which is
3300    /// what lets a faded bar be found again at all (`scrollbar.dart:2132`).
3301    #[test]
3302    fn a_bar_stays_for_the_pointer_that_wants_it() {
3303        let id = WidgetId::ROOT;
3304        let regions = [region(id, 200.0)];
3305        let run = |rt: &mut Runtime, frames: usize| {
3306            for _ in 0..frames {
3307                rt.advance_scroll(&regions, 0.016);
3308            }
3309        };
3310
3311        let mut held = Runtime::default();
3312        held.scroll.insert(id, (0.0, 40.0));
3313        held.scrollbar_dragged = Some(id);
3314        run(&mut held, 140); // well past 600 ms of stillness
3315        assert!(
3316            held.scrollbar_fade_of(id).opacity > 0.99,
3317            "a thumb in a hand does not fade out from under it"
3318        );
3319
3320        // A bar that has gone entirely comes back for a pointer that reaches for it,
3321        // and the thumb warms towards its hovered colour while it is there.
3322        let mut faded = Runtime::default();
3323        run(&mut faded, 10);
3324        assert_eq!(faded.scrollbar_fade_of(id).opacity, 0.0);
3325        faded.scrollbar_hovered = Some(id);
3326        run(&mut faded, 25);
3327        assert!(faded.scrollbar_fade_of(id).opacity > 0.99, "back");
3328        assert!(faded.scrollbar_fade_of(id).hover > 0.99, "and warmed");
3329    }
3330
3331    #[test]
3332    fn interactive_fling_decelerates_settles_and_stays_bounded() {
3333        use crate::interactive::InteractiveView;
3334        let id = WidgetId::ROOT;
3335        let vp = Rect::new(0.0, 0.0, 200.0, 200.0);
3336        let mut rt = Runtime::default();
3337        // Content zoomed ×2, flung leftwards at high speed.
3338        rt.interactive.insert(
3339            id,
3340            InteractiveView {
3341                scale: 2.0,
3342                tx: 0.0,
3343                ty: 0.0,
3344            },
3345        );
3346        rt.interactive_velocity.insert(id, (-2000.0, 0.0));
3347        let viewports = [(id, vp)];
3348        let mut frames = 0;
3349        while rt.advance_interactive(&viewports, 0.016) {
3350            frames += 1;
3351            assert!(frames < 1000, "the fling must eventually stop");
3352        }
3353        let view = rt.interactive.get(&id).copied().unwrap();
3354        // Stays clamped (content ×2 covers the viewport → tx ∈ [-200, 0]).
3355        assert!(
3356            view.tx >= -200.0 - 1e-3 && view.tx <= 0.0 + 1e-3,
3357            "clamped: {}",
3358            view.tx
3359        );
3360        assert!(
3361            !rt.interactive_velocity.contains_key(&id),
3362            "velocity cleared at rest"
3363        );
3364    }
3365
3366    #[test]
3367    fn scroll_overshoot_rubber_bands_back_to_max() {
3368        let id = WidgetId::ROOT;
3369        let mut rt = Runtime::default();
3370        // Target beyond the bound (overshoot) → must come back to max.
3371        rt.scroll_target.insert(id, (0.0, 240.0));
3372        rt.scroll_velocity.insert(id, (0.0, 0.0));
3373        let regions = [region(id, 200.0)];
3374        for _ in 0..1000 {
3375            if !rt.advance_scroll(&regions, 0.016) {
3376                break;
3377            }
3378        }
3379        let (_, y) = rt.scroll.get(&id).copied().unwrap();
3380        assert!((y - 200.0).abs() < 1.0, "back at the max bound: {y}");
3381    }
3382
3383    /// Runs a fling to completion and answers where the content came to rest.
3384    fn settle(physics: ScrollPhysics, max: f32, from: f32, velocity: f32) -> f32 {
3385        let id = WidgetId::ROOT;
3386        let mut rt = Runtime::default();
3387        let area = region_with(physics, id, max);
3388        rt.scroll.insert(id, (0.0, from));
3389        rt.fling_scroll(area, physics, (0.0, velocity));
3390        for _ in 0..1200 {
3391            if !rt.advance_scroll(&[area], 1.0 / 60.0) {
3392                break;
3393            }
3394        }
3395        assert!(
3396            rt.scroll_ballistic.is_empty(),
3397            "the fling should have finished"
3398        );
3399        rt.scroll.get(&id).copied().unwrap().1
3400    }
3401
3402    #[test]
3403    fn a_clamping_fling_stops_at_the_edge() {
3404        // Far more momentum than there is content: it must stop exactly at the end,
3405        // with no overshoot to spring back from.
3406        let rest = settle(ScrollPhysics::CLAMPING, 400.0, 0.0, 6000.0);
3407        assert_eq!(rest, 400.0, "stopped dead at the end, at {rest}");
3408        // And symmetrically at the start.
3409        let rest = settle(ScrollPhysics::CLAMPING, 400.0, 400.0, -6000.0);
3410        assert_eq!(rest, 0.0, "stopped dead at the start, at {rest}");
3411    }
3412
3413    #[test]
3414    fn a_clamping_fling_that_fits_lands_where_the_platform_says() {
3415        // 1000 px/s covers about 194 px under the platform's spline.
3416        let rest = settle(ScrollPhysics::CLAMPING, 4000.0, 0.0, 1000.0);
3417        assert!((rest - 194.0).abs() < 3.0, "landed at {rest}");
3418    }
3419
3420    #[test]
3421    fn a_bouncing_fling_overshoots_then_settles_back_on_the_edge() {
3422        let id = WidgetId::ROOT;
3423        let physics = ScrollPhysics::BOUNCING;
3424        let mut rt = Runtime::default();
3425        let area = region_with(physics, id, 400.0);
3426        rt.scroll.insert(id, (0.0, 300.0));
3427        rt.fling_scroll(area, physics, (0.0, 4000.0));
3428        let mut peak: f32 = 0.0;
3429        for _ in 0..1200 {
3430            let moving = rt.advance_scroll(&[area], 1.0 / 60.0);
3431            peak = peak.max(rt.scroll.get(&id).copied().unwrap().1);
3432            if !moving {
3433                break;
3434            }
3435        }
3436        assert!(
3437            peak > 400.0,
3438            "it should have gone past the end, peak {peak}"
3439        );
3440        let rest = rt.scroll.get(&id).copied().unwrap().1;
3441        assert!(
3442            (rest - 400.0).abs() < 1.0,
3443            "came back to the end, at {rest}"
3444        );
3445    }
3446
3447    #[test]
3448    fn an_overscrolled_offset_comes_home_even_without_a_fling() {
3449        // A release too slow to fling still owes the content its edge back.
3450        for physics in [ScrollPhysics::BOUNCING, ScrollPhysics::CLAMPING] {
3451            let rest = settle(physics, 400.0, 460.0, 5.0);
3452            assert!(
3453                (rest - 400.0).abs() < 1.0,
3454                "{physics:?} left the content at {rest}"
3455            );
3456        }
3457    }
3458
3459    #[test]
3460    fn a_release_with_nothing_to_do_starts_no_fling() {
3461        let id = WidgetId::ROOT;
3462        let mut rt = Runtime::default();
3463        rt.scroll.insert(id, (0.0, 100.0));
3464        assert!(!rt.fling_scroll(region(id, 400.0), ScrollPhysics::CLAMPING, (0.0, 5.0)));
3465        assert!(rt.scroll_ballistic.is_empty());
3466    }
3467
3468    #[test]
3469    fn a_held_region_is_not_dragged_back_by_the_spring() {
3470        // The bug the device found: while a finger holds an overscrolled offset,
3471        // the edge spring kept retracting it between two moves, so a rubber band
3472        // was pulled back as fast as it was stretched and never appeared.
3473        let id = WidgetId::ROOT;
3474        let physics = ScrollPhysics::BOUNCING;
3475        let area = region_with(physics, id, 400.0);
3476        let pulled = -60.0;
3477
3478        let mut held = Runtime::default();
3479        held.hold_scroll(id);
3480        held.scroll.insert(id, (0.0, pulled));
3481        held.scroll_target.insert(id, (0.0, pulled));
3482        for _ in 0..10 {
3483            held.advance_scroll(&[area], 1.0 / 60.0);
3484        }
3485        assert_eq!(
3486            held.scroll.get(&id).copied().unwrap().1,
3487            pulled,
3488            "a held offset must not move on its own"
3489        );
3490
3491        // The same offset, let go: now it must come home.
3492        let mut free = Runtime::default();
3493        free.scroll.insert(id, (0.0, pulled));
3494        free.scroll_target.insert(id, (0.0, pulled));
3495        while free.advance_scroll(&[area], 1.0 / 60.0) {}
3496        assert!(
3497            free.scroll.get(&id).copied().unwrap().1.abs() < 1.0,
3498            "a released overscroll springs back"
3499        );
3500    }
3501
3502    /// Milestone 533. A fling owns its offset as a finger does: content that shrinks under
3503    /// a bounce does not snap it to the new end in one frame, and when the bounce settles
3504    /// on the end it was aimed at — the old one, now past the content — it springs on to the
3505    /// new end rather than jumping there.
3506    #[test]
3507    fn a_fling_the_content_shrank_under_springs_on_to_the_new_end() {
3508        let id = WidgetId::ROOT;
3509        let physics = ScrollPhysics::BOUNCING;
3510        let mut rt = Runtime::default();
3511        rt.scroll.insert(id, (0.0, 300.0));
3512        assert!(rt.fling_scroll(region_with(physics, id, 400.0), physics, (0.0, 3000.0)));
3513
3514        let shrunk = [region_with(physics, id, 100.0)];
3515        let mut previous = 300.0;
3516        let mut biggest_step: f32 = 0.0;
3517        let mut frames = 0;
3518        while rt.advance_scroll(&shrunk, 1.0 / 60.0) {
3519            let y = rt.scroll.get(&id).copied().unwrap().1;
3520            biggest_step = biggest_step.max((y - previous).abs());
3521            previous = y;
3522            frames += 1;
3523            assert!(frames < 2000, "the motion must end");
3524        }
3525        let y = rt.scroll.get(&id).copied().unwrap().1;
3526        biggest_step = biggest_step.max((y - previous).abs());
3527        assert!((y - 100.0).abs() < 1.0, "home at the new end, at {y}");
3528        assert!(
3529            biggest_step < 120.0,
3530            "carried there, not jumped: {biggest_step} px in one frame"
3531        );
3532        assert!(rt.scroll_ballistic.is_empty() && rt.scroll_target.is_empty());
3533    }
3534
3535    /// Milestone 533. A glide still under way — a wheel's, or a slow release's — is left to
3536    /// its own spring, which already pulls a target past the end back inside it.
3537    #[test]
3538    fn a_glide_the_content_shrank_under_is_not_snapped() {
3539        let id = WidgetId::ROOT;
3540        let mut rt = Runtime::default();
3541        rt.scroll.insert(id, (0.0, 400.0));
3542        rt.scroll_target.insert(id, (0.0, 400.0));
3543        let shrunk = [region(id, 100.0)];
3544        rt.advance_scroll(&shrunk, 1.0 / 60.0);
3545        let first = rt.scroll.get(&id).copied().unwrap().1;
3546        assert!(first > 300.0, "one frame in, still gliding: {first}");
3547        while rt.advance_scroll(&shrunk, 1.0 / 60.0) {}
3548        let rest = rt.scroll.get(&id).copied().unwrap().1;
3549        assert!((rest - 100.0).abs() < 1.0, "and it glides home: {rest}");
3550    }
3551
3552    /// Milestone 533. Nothing owns the offset, the content shrank: it is corrected at once,
3553    /// both axes, both ends of the range.
3554    #[test]
3555    fn an_offset_nobody_owns_is_kept_inside_the_content() {
3556        let id = WidgetId::ROOT;
3557        let mut rt = Runtime::default();
3558        let area = Scrollable {
3559            max_x: 30.0,
3560            ..region(id, 100.0)
3561        };
3562        rt.scroll.insert(id, (80.0, 400.0));
3563        assert!(rt.keep_scroll_in_range(&[area]), "something was corrected");
3564        assert_eq!(rt.scroll.get(&id), Some(&(30.0, 100.0)));
3565        assert!(
3566            !rt.keep_scroll_in_range(&[area]),
3567            "and nothing is left to correct"
3568        );
3569        rt.scroll.insert(id, (-5.0, -20.0));
3570        rt.keep_scroll_in_range(&[area]);
3571        assert_eq!(rt.scroll.get(&id), Some(&(0.0, 0.0)));
3572        // A region nobody has scrolled gains no entry.
3573        let other = region(id.child(1), 0.0);
3574        assert!(!rt.keep_scroll_in_range(&[other]));
3575        assert!(!rt.scroll.contains_key(&other.id));
3576    }
3577
3578    #[test]
3579    fn releasing_a_region_that_was_never_held_changes_nothing() {
3580        let mut rt = Runtime::default();
3581        rt.hold_scroll(WidgetId::ROOT);
3582        rt.release_scroll(WidgetId::ROOT.child(1));
3583        assert_eq!(
3584            rt.scroll_held,
3585            Some(WidgetId::ROOT),
3586            "another region's release must not steal the hold"
3587        );
3588        rt.release_scroll(WidgetId::ROOT);
3589        assert_eq!(rt.scroll_held, None);
3590    }
3591
3592    #[test]
3593    fn a_clamping_fling_lights_the_edge_it_slams_into() {
3594        let id = WidgetId::ROOT;
3595        let physics = ScrollPhysics::CLAMPING;
3596        let mut rt = Runtime::default();
3597        let area = region_with(physics, id, 400.0);
3598        // Far more momentum than there is content: it will reach the end.
3599        rt.fling_scroll(area, physics, (0.0, 6000.0));
3600        while rt.advance_scroll(&[area], 1.0 / 60.0) {}
3601        let glows = rt
3602            .scroll_glow
3603            .get(&id)
3604            .expect("the edge should have lit up");
3605        assert!(!glows.edge(GlowEdge::Bottom).is_idle(), "the end glows");
3606        assert!(
3607            glows.edge(GlowEdge::Top).is_idle(),
3608            "and only the end — the fling never touched the start"
3609        );
3610        // It is a flash, not a permanent mark.
3611        while rt.advance_glow(1.0 / 60.0) {}
3612        assert!(rt.scroll_glow.is_empty(), "the glow is dropped once quiet");
3613    }
3614
3615    #[test]
3616    fn a_bouncing_fling_lights_nothing() {
3617        // The bounce *is* the feedback: a glow on top of it would say the same
3618        // thing twice.
3619        let id = WidgetId::ROOT;
3620        let physics = ScrollPhysics::BOUNCING;
3621        let mut rt = Runtime::default();
3622        let area = region_with(physics, id, 400.0);
3623        rt.fling_scroll(area, physics, (0.0, 6000.0));
3624        while rt.advance_scroll(&[area], 1.0 / 60.0) {}
3625        assert!(rt.scroll_glow.is_empty());
3626    }
3627
3628    #[test]
3629    fn a_pull_that_was_refused_lights_the_edge_and_fades() {
3630        let id = WidgetId::ROOT;
3631        let mut rt = Runtime::default();
3632        rt.glow_pull(
3633            id,
3634            Some(crate::physics::OverscrollIndicator::Glow),
3635            GlowEdge::Top,
3636            -30.0,
3637            600.0,
3638            150.0,
3639            300.0,
3640        );
3641        assert!(!rt.scroll_glow.get(&id).unwrap().is_idle());
3642        rt.glow_scroll_end(id);
3643        let mut frames = 0;
3644        while rt.advance_glow(1.0 / 60.0) {
3645            frames += 1;
3646            assert!(frames < 200, "the glow never faded");
3647        }
3648        assert!(rt.scroll_glow.is_empty());
3649    }
3650
3651    #[test]
3652    fn a_refusal_of_nothing_lights_nothing() {
3653        let mut rt = Runtime::default();
3654        rt.glow_pull(
3655            WidgetId::ROOT,
3656            Some(crate::physics::OverscrollIndicator::Glow),
3657            GlowEdge::Top,
3658            0.0,
3659            600.0,
3660            150.0,
3661            300.0,
3662        );
3663        assert!(
3664            rt.scroll_glow.is_empty(),
3665            "a zero pull is not an overscroll"
3666        );
3667        assert!(!rt.advance_glow(1.0 / 60.0));
3668    }
3669
3670    /// **An area whose behaviour shows nothing past an edge shows nothing** (milestone
3671    /// 616): neither a pull nor a fling landing on the edge lights or stretches anything,
3672    /// as on iOS and the desktops.
3673    #[test]
3674    fn an_area_with_no_indicator_shows_nothing_past_its_edge() {
3675        let mut rt = Runtime::default();
3676        rt.glow_pull(
3677            WidgetId::ROOT,
3678            None,
3679            GlowEdge::Top,
3680            80.0,
3681            600.0,
3682            150.0,
3683            300.0,
3684        );
3685        rt.glow_absorb(WidgetId::ROOT, None, GlowEdge::Bottom, 4000.0);
3686        assert!(rt.scroll_glow.is_empty() && rt.scroll_stretch.is_empty());
3687        // And a fling that slams into the edge of such an area leaves nothing either.
3688        let id = WidgetId::ROOT;
3689        let quiet = Scrollable {
3690            overscroll: None,
3691            ..region(id, 400.0)
3692        };
3693        rt.fling_scroll(quiet, ScrollPhysics::CLAMPING, (0.0, 6000.0));
3694        while rt.advance_scroll(&[quiet], 1.0 / 60.0) {}
3695        assert!(rt.scroll_glow.is_empty() && rt.scroll_stretch.is_empty());
3696        // Where the area glows, the same fling does light the edge: the instrument works.
3697        let lit = region(id, 400.0);
3698        rt.scroll.clear();
3699        rt.fling_scroll(lit, ScrollPhysics::CLAMPING, (0.0, 6000.0));
3700        while rt.advance_scroll(&[lit], 1.0 / 60.0) {}
3701        assert!(!rt.scroll_glow.is_empty());
3702    }
3703
3704    #[test]
3705    fn a_fling_on_a_region_that_vanished_is_dropped() {
3706        let id = WidgetId::ROOT;
3707        let mut rt = Runtime::default();
3708        rt.fling_scroll(region(id, 400.0), ScrollPhysics::CLAMPING, (0.0, 2000.0));
3709        assert!(!rt.scroll_ballistic.is_empty());
3710        // The route changed: the scrollable is no longer part of the frame.
3711        assert!(!rt.advance_scroll(&[], 1.0 / 60.0));
3712        assert!(rt.scroll_ballistic.is_empty());
3713    }
3714
3715    #[test]
3716    fn leaving_fades_out_and_clears() {
3717        let mut rt = Runtime::default();
3718        rt.leaving.insert(0, (Vec::new(), 1.0));
3719        assert!(rt.advance_leaving(0.06)); // ~0.5, still running
3720        assert!(!rt.advance_leaving(0.06)); // reaches 0 → cleared
3721        assert!(rt.leaving.is_empty());
3722    }
3723
3724    /// **A widget that plays from a start sets off from it when it appears** (milestone 691):
3725    /// `anim_initial` is where the value is on the first frame, and that frame counts.
3726    /// Without it, a widget appears at its target, as a switch does.
3727    #[test]
3728    fn a_value_with_a_start_plays_from_it_on_mount() {
3729        struct Playing(Option<f32>);
3730        impl crate::widget::Widget<()> for Playing {
3731            fn style(&self) -> frus_layout::Style {
3732                frus_layout::Style::default()
3733            }
3734            fn children(&self) -> &[Box<dyn crate::widget::Widget<()>>] {
3735                &[]
3736            }
3737            fn paint(
3738                &self,
3739                _: frus_core::Rect,
3740                _: crate::interaction::Status,
3741                _: &crate::theme::Theme,
3742                _: &mut frus_core::Scene,
3743            ) {
3744            }
3745            fn on_click(&self) -> Option<()> {
3746                None
3747            }
3748            fn anim_target(&self) -> Option<f32> {
3749                Some(1.0)
3750            }
3751            fn anim_initial(&self) -> Option<f32> {
3752                self.0
3753            }
3754            fn anim_duration(&self) -> f32 {
3755                2.0
3756            }
3757        }
3758        let mut rt = Runtime::default();
3759        assert!(rt.advance_values(&Playing(Some(0.0)), 0.5), "still playing");
3760        assert!(
3761            (rt.value(WidgetId::ROOT) - 0.25).abs() < 1e-6,
3762            "a quarter in"
3763        );
3764        assert!(rt.advance_values(&Playing(Some(0.0)), 0.5));
3765        assert!((rt.value(WidgetId::ROOT) - 0.5).abs() < 1e-6, "half way");
3766
3767        let mut plain = Runtime::default();
3768        assert!(!plain.advance_values(&Playing(None), 0.5));
3769        assert_eq!(
3770            plain.value(WidgetId::ROOT),
3771            1.0,
3772            "no start: at the target at once"
3773        );
3774
3775        let mut still = Runtime {
3776            still: true,
3777            ..Default::default()
3778        };
3779        assert!(!still.advance_values(&Playing(Some(0.0)), 0.5));
3780        assert_eq!(
3781            still.value(WidgetId::ROOT),
3782            1.0,
3783            "motion off: at the end at once"
3784        );
3785    }
3786}