Skip to main content

frus_shell/
app.rs

1//! The generic driver: implements [`winit::application::ApplicationHandler`] for
2//! any [`Application`].
3//!
4//! The framework owns the window, the renderer, the [`Runtime`] — the retained
5//! interaction state: hover, focus, scroll, editing, animations — input routing by
6//! hit test, dragging (scrollbars, selection, handles, the back gesture) and the
7//! animation clock. The application supplies only `update`, `view` and friends.
8
9use std::collections::HashMap;
10use std::sync::Arc;
11
12use web_time::Instant;
13
14use frus_gpu::Renderer;
15use frus_widgets::{
16    build_deferred, build_ui, collect_ids, find_by_key, find_path, find_widget,
17    nearest_reorder_slot, reflow_reorder_cards, reflow_reorder_columns, reorder_drop_after,
18    reorder_siblings, reorderable_owners, subtree_ids, Accessibility, Brightness, Color,
19    Cursor as UiCursor, Edit, EditKind, EditSnapshot, FocusDirection, Insets, Key, KeyResponse,
20    KeyStroke, MediaQuery, Point, Primitive, Rect, ReorderAxis, Runtime, Scene, ScrollTo,
21    Scrollable, SheetTo, ShortcutKey, Size, Theme, Ui, VelocityEstimate, VelocityTracker, Widget,
22    WidgetId, WindowInsets,
23};
24use winit::application::ApplicationHandler;
25use winit::event::{
26    ElementState, MouseButton, MouseScrollDelta, StartCause, TouchPhase, WindowEvent,
27};
28use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoopProxy};
29use winit::keyboard::{Key as WinitKey, KeyCode, NamedKey, PhysicalKey};
30use winit::window::{CursorIcon, Window, WindowId};
31
32use crate::application::{Application, Lifecycle};
33
34/// Everything the platform reports about its user, in one walk.
35#[derive(Clone, Debug, PartialEq)]
36pub(crate) struct PlatformSettings {
37    /// The system's *Font size* slider. `1.0` is normal; Android's own slider reaches
38    /// 1.3, and the accessibility sizes go further.
39    pub text_scaler: f32,
40    /// Whether the system is currently showing a dark interface.
41    pub brightness: Brightness,
42    /// The accessibility settings, as far as they could be read.
43    pub accessibility: Accessibility,
44    /// **The reader's preferred languages**, best first, as the platform reports them.
45    ///
46    /// Empty where the platform said nothing — which is a real answer and not a failure:
47    /// the resolution treats it as *no preference* and hands back the application's own
48    /// first choice, exactly as the reference does (`app.dart:153`).
49    pub locales: Vec<frus_widgets::Locale>,
50}
51
52impl Default for PlatformSettings {
53    /// What a platform that reports nothing looks like: a user who has changed nothing.
54    fn default() -> Self {
55        Self {
56            text_scaler: 1.0,
57            brightness: Brightness::Light,
58            accessibility: Accessibility::NONE,
59            locales: Vec::new(),
60        }
61    }
62}
63use crate::gesture::{PointerEvent, PointerKind, PressRecognizer};
64
65/// What a key asks of the clipboard.
66#[derive(Clone, Copy, Debug, PartialEq, Eq)]
67enum ClipCommand {
68    Copy,
69    Cut,
70    Paste,
71}
72
73/// The clipboard command a key press means, if any: Ctrl+C/X/V, and the Copy, Cut and
74/// Paste keys a keyboard may have for them (milestone 509).
75///
76/// Both halves of the key are asked. A desktop keyboard's Copy key arrives as a named
77/// logical key; Android's arrives by **physical code only** — winit gives it no logical
78/// key at all — so a check of the logical key alone would ignore it on the one platform
79/// that has it as a keycode of its own.
80fn clipboard_command(logical: &WinitKey, physical: PhysicalKey, ctrl: bool) -> Option<ClipCommand> {
81    match logical {
82        WinitKey::Named(NamedKey::Copy) => return Some(ClipCommand::Copy),
83        WinitKey::Named(NamedKey::Cut) => return Some(ClipCommand::Cut),
84        WinitKey::Named(NamedKey::Paste) => return Some(ClipCommand::Paste),
85        WinitKey::Character(c) if ctrl => {
86            if c.eq_ignore_ascii_case("c") {
87                return Some(ClipCommand::Copy);
88            }
89            if c.eq_ignore_ascii_case("x") {
90                return Some(ClipCommand::Cut);
91            }
92            if c.eq_ignore_ascii_case("v") {
93                return Some(ClipCommand::Paste);
94            }
95        }
96        _ => {}
97    }
98    match physical {
99        PhysicalKey::Code(KeyCode::Copy) => Some(ClipCommand::Copy),
100        PhysicalKey::Code(KeyCode::Cut) => Some(ClipCommand::Cut),
101        PhysicalKey::Code(KeyCode::Paste) => Some(ClipCommand::Paste),
102        _ => None,
103    }
104}
105
106/// Whether `widget` takes typing, and so wants the software keyboard while it has focus.
107///
108/// Asked of the widget (milestone 510). It used to be asked of a **caret hit test** at
109/// the corner of a field one pixel wide, and a field with a clickable suffix — the ×
110/// a field shows once it holds text — answered that no caret goes there, since that
111/// pixel is its clear button. So the first letter typed into such a field closed the
112/// keyboard and ended its composition, and the keyboard's next update was written
113/// beside the first: `F`, then `FFr`.
114#[cfg(any(android, test))]
115fn wants_keyboard<M>(widget: &dyn Widget<M>) -> bool {
116    widget.text_value().is_some() && widget.focusable()
117}
118
119/// The clipboard: `arboard` on the desktop platforms, the platform's own on Android
120/// (`ClipboardManager`, through the bundled dex — milestone 509, #22), the browser's
121/// asynchronous Clipboard API on the Web (milestone 526, #17), and a no-op on iOS
122/// (`arboard` does not compile there and is not a dependency).
123/// The stub is gated on what is *not* implemented rather than on a list of the other
124/// platforms: that is what makes adding a target never leave this type undefined.
125/// One uniform API, so the driver's body stays free of `cfg`.
126///
127/// **A paste is asked for, and answered.** The Web's read is a promise — it may prompt
128/// the reader, be refused, or take its time — and the other platforms' reads are
129/// immediate. So `Clipboard::paste` names the field that asked and either answers at
130/// once or answers later through `Clipboard::take_answered`, which the driver drains
131/// every frame; both answers land through the same rule, `Pasted::lands`. Nothing on a
132/// synchronous platform pretends to wait, and no widget knows which kind it is on: the
133/// shell, not the field, holds the clipboard.
134mod clip {
135    use frus_widgets::WidgetId;
136
137    /// What the clipboard answered to a paste: its text, and the field that asked.
138    #[derive(Clone, Debug, PartialEq, Eq)]
139    pub struct Pasted {
140        pub into: WidgetId,
141        pub text: String,
142    }
143
144    impl Pasted {
145        /// Whether this paste goes into its field: only while that field still has the
146        /// focus — on the Web the answer comes back after the key press, and the reader
147        /// may have moved on to another field, or to none — and only when there is text.
148        /// An empty clipboard is nothing to paste, not an instruction to delete the
149        /// selection; the browser answers a clipboard holding no text with `""`.
150        pub fn lands(&self, focused: Option<WidgetId>) -> bool {
151            focused == Some(self.into) && !self.text.is_empty()
152        }
153    }
154
155    /// The platforms whose read is immediate answer a paste on the spot.
156    #[cfg(not(web))]
157    impl Clipboard {
158        pub fn paste(
159            &mut self,
160            into: WidgetId,
161            _wake: Option<&std::sync::Arc<winit::window::Window>>,
162        ) -> Option<Pasted> {
163            self.get_text().map(|text| Pasted { into, text })
164        }
165        pub fn take_answered(&mut self) -> Option<Pasted> {
166            None
167        }
168    }
169
170    #[cfg(desktop)]
171    pub struct Clipboard(Option<arboard::Clipboard>);
172
173    #[cfg(desktop)]
174    impl Clipboard {
175        pub fn new() -> Self {
176            Self(arboard::Clipboard::new().ok())
177        }
178        pub fn get_text(&mut self) -> Option<String> {
179            self.0.as_mut().and_then(|c| c.get_text().ok())
180        }
181        pub fn set_text(&mut self, text: String) {
182            if let Some(c) = self.0.as_mut() {
183                let _ = c.set_text(text);
184            }
185        }
186    }
187
188    #[cfg(android)]
189    pub struct Clipboard;
190
191    #[cfg(android)]
192    impl Clipboard {
193        pub fn new() -> Self {
194            Self
195        }
196        pub fn get_text(&mut self) -> Option<String> {
197            crate::android_clipboard::get_text()
198        }
199        pub fn set_text(&mut self, text: String) {
200            crate::android_clipboard::set_text(&text);
201        }
202    }
203
204    /// The pastes a clipboard has answered later and the driver has not taken yet, in the
205    /// order the answers came — which need not be the order they were asked in.
206    ///
207    /// Held by the Web's clipboard; kept out of its `cfg` so that it is tested natively.
208    #[cfg(any(web, test))]
209    #[derive(Default)]
210    pub struct Answers(std::rc::Rc<std::cell::RefCell<std::collections::VecDeque<Pasted>>>);
211
212    #[cfg(any(web, test))]
213    impl Answers {
214        /// What answers a paste into `into` once the text is there: it queues the paste,
215        /// then calls `wake`, so that a frame comes to take it.
216        pub fn answer_for(
217            &self,
218            into: WidgetId,
219            wake: impl FnOnce() + 'static,
220        ) -> impl FnOnce(String) + 'static {
221            let queue = std::rc::Rc::clone(&self.0);
222            move |text| {
223                queue.borrow_mut().push_back(Pasted { into, text });
224                wake();
225            }
226        }
227
228        /// The oldest answer not taken yet.
229        pub fn take(&self) -> Option<Pasted> {
230            self.0.borrow_mut().pop_front()
231        }
232    }
233
234    /// The Web: a read is started by the key press and answered by the browser later,
235    /// into a queue the driver drains; the window is asked for a frame so that it does.
236    #[cfg(web)]
237    pub struct Clipboard {
238        answered: Answers,
239    }
240
241    #[cfg(web)]
242    impl Clipboard {
243        pub fn new() -> Self {
244            Self {
245                answered: Answers::default(),
246            }
247        }
248        pub fn set_text(&mut self, text: String) {
249            crate::web_clipboard::set_text(&text);
250        }
251        pub fn paste(
252            &mut self,
253            into: WidgetId,
254            wake: Option<&std::sync::Arc<winit::window::Window>>,
255        ) -> Option<Pasted> {
256            let wake = wake.cloned();
257            crate::web_clipboard::get_text(self.answered.answer_for(into, move || {
258                if let Some(window) = wake {
259                    window.request_redraw();
260                }
261            }));
262            None
263        }
264        pub fn take_answered(&mut self) -> Option<Pasted> {
265            self.answered.take()
266        }
267    }
268
269    #[cfg(not(any(desktop, android, web)))]
270    pub struct Clipboard;
271
272    #[cfg(not(any(desktop, android, web)))]
273    impl Clipboard {
274        pub fn new() -> Self {
275            Self
276        }
277        pub fn get_text(&mut self) -> Option<String> {
278            None
279        }
280        pub fn set_text(&mut self, _text: String) {}
281    }
282}
283
284/// Browser timers, for the Web: the counterpart of the native subscriptions'
285/// `recv_timeout` thread. A **retained** `setInterval`, whose **drop** calls
286/// `clearInterval` and releases the closure. That is how an `every` subscription
287/// ticks on the Web, where there is no background thread.
288#[cfg(web)]
289mod web_timer {
290    use wasm_bindgen::prelude::Closure;
291    use wasm_bindgen::JsCast;
292
293    /// An active `setInterval`: as long as this handle lives the callback is called
294    /// back every `ms`; dropping it cancels that.
295    pub(crate) struct Interval {
296        id: i32,
297        // The JS closure must live as long as the interval does.
298        _closure: Closure<dyn FnMut()>,
299    }
300
301    impl Interval {
302        /// Schedules `f` every `ms` milliseconds, 1 ms at the least. `None` when there
303        /// is no window, that is, in a DOM-less context.
304        pub(crate) fn new(ms: i32, f: impl FnMut() + 'static) -> Option<Self> {
305            let window = web_sys::window()?;
306            let closure = Closure::wrap(Box::new(f) as Box<dyn FnMut()>);
307            let id = window
308                .set_interval_with_callback_and_timeout_and_arguments_0(
309                    closure.as_ref().unchecked_ref(),
310                    ms.max(1),
311                )
312                .ok()?;
313            Some(Self {
314                id,
315                _closure: closure,
316            })
317        }
318    }
319
320    impl Drop for Interval {
321        fn drop(&mut self) {
322            if let Some(window) = web_sys::window() {
323                window.clear_interval_with_handle(self.id);
324            }
325        }
326    }
327
328    /// Calls `f` once, after `ms` milliseconds — the Web half of
329    /// [`Command::after`](crate::Command::after).
330    ///
331    /// There is no handle to keep, and that is the point. `Closure::once_into_js`
332    /// hands ownership of the closure to the JS side, which frees it after the call.
333    /// A `Closure` we owned instead would have to be either leaked — one leak per
334    /// timer — or dropped here, which would free the callback out from under a
335    /// `setTimeout` that has not fired yet.
336    pub(crate) fn after(ms: i32, f: impl FnOnce() + 'static) {
337        let Some(window) = web_sys::window() else {
338            return;
339        };
340        let callback = wasm_bindgen::closure::Closure::once_into_js(f);
341        let _ = window.set_timeout_with_callback_and_timeout_and_arguments_0(
342            callback.unchecked_ref(),
343            ms.max(0),
344        );
345    }
346}
347
348/// An active subscription's cancellation handle: **dropping** it stops the
349/// subscription.
350/// - Native: the executor task's handle — dropping it cancels the task.
351/// - Web: a retained `setInterval` (`None` when installing it failed).
352#[cfg(not(web))]
353type SubHandle = async_executor::Task<()>;
354#[cfg(web)]
355type SubHandle = Option<web_timer::Interval>;
356
357/// Scroll speed, in pixels per wheel notch.
358const SCROLL_SPEED: f32 = 40.0;
359
360/// The distance a **finger** must travel before the framework is confident the
361/// gesture is a drag rather than a tap, in logical px.
362///
363/// 18 px is large for a screen measurement, and deliberately so: a thumb covers a
364/// wide contact patch and rolls as it presses, so a smaller threshold turns taps
365/// near the edge of a button into aborted drags. It is the value the mature
366/// toolkits settled on after starting at 8 and hearing that targets were too hard
367/// to hit.
368const TOUCH_SLOP: f32 = 18.0;
369
370/// The same threshold for a **precise** pointer — a mouse or a trackpad. It knows
371/// exactly where it is, so almost any movement is intentional.
372const PRECISE_SLOP: f32 = 1.0;
373
374/// The velocity a release should be flung with, per axis — zero on an axis the
375/// gesture did not really travel along.
376///
377/// Speed alone is not a fling. A finger that twitches fast over three pixels
378/// produces a large velocity and no intent; requiring the gesture to have
379/// **covered ground** as well is what separates the two, and the distance that says
380/// "this was a drag" is the same `slop` that started it.
381///
382/// The gate is per axis because a scroll is two independent axes: a swipe running
383/// down the screen must not fling sideways on the little horizontal wobble a thumb
384/// always adds.
385fn fling_velocity(estimate: VelocityEstimate, slop: f32) -> (f32, f32) {
386    let gate = |velocity: f32, travelled: f32| {
387        if travelled.abs() > slop {
388            velocity
389        } else {
390            0.0
391        }
392    };
393    (
394        gate(estimate.velocity.x, estimate.offset.0),
395        gate(estimate.velocity.y, estimate.offset.1),
396    )
397}
398
399/// The elastic overshoot allowed past the scroll bounds, in px — the rubber band.
400const SCROLL_OVER: f32 = 48.0;
401
402/// The minimum velocity, in px/s, on releasing a pan that starts a fling.
403const PAN_FLING_MIN: f32 = 80.0;
404
405/// The width, in physical px, of the edge zone that arms the back gesture.
406const BACK_EDGE: f32 = 24.0;
407
408/// The **drag-and-drop** (reorder) preview — paint geometry only. The shadow's
409/// *colour* comes from the theme (`theme.scheme.shadow`, as `Button` does); only the
410/// geometry lives here, as named constants rather than magic numbers scattered about.
411mod drag_preview {
412    /// The vertical offset of the ghost's drop shadow, in px.
413    pub const SHADOW_OFFSET_Y: f32 = 4.0;
414    /// The blur of the ghost's drop shadow, in px.
415    pub const SHADOW_BLUR: f32 = 12.0;
416    /// The opacity of the ghost's shadow.
417    pub const SHADOW_ALPHA: f32 = 0.28;
418    /// The opacity of the ghost's `primary` border.
419    pub const BORDER_ALPHA: f32 = 0.9;
420    /// The thickness of the ghost's `primary` border, in px.
421    pub const BORDER_WIDTH: f32 = 1.5;
422    /// A slight vertical lift of the ghost during a **horizontal** drag, for columns.
423    pub const LIFT_Y: f32 = -2.0;
424    /// The thickness of the **insertion line** in the vertical preview, in px.
425    pub const INSERT_THICKNESS: f32 = 3.0;
426}
427
428/// Whether a frame builds the view again rather than repainting the tree it already has:
429/// when something asked for it (`dirty`), while the application's own animations are
430/// moving **and in the frame they stop in**, or for a reason of the frame's own
431/// (`otherwise`: no tree yet, a switcher in flight).
432///
433/// The frame they stop in, because the tick that ends a transition has changed the state
434/// — a push's page left behind is gone — and reports nothing moving. Without it the tree
435/// hit-tested from then on is the transition's last, the page left behind still in it
436/// under the finger (milestone 531).
437fn frame_needs_build(dirty: bool, animating: bool, was_animating: bool, otherwise: bool) -> bool {
438    dirty || animating || was_animating || otherwise
439}
440
441/// What a press is still a candidate for once it has chosen its drag: the long press a
442/// widget under the finger asks for, and the item it would lift on a hold.
443///
444/// A long press only while the press was not captured by a drag — a scrollbar, a handle,
445/// a selection; a touch scroll that has not moved yet stays a candidate. An item that
446/// lifts on a hold is a candidate under a scroll too: the scroll keeps the gesture for
447/// now, and the deadline decides, since a finger that stays put was never scrolling.
448fn hold_candidates<Msg: Clone>(
449    drag: Option<&Drag>,
450    ui: Option<&frus_widgets::Ui<Msg>>,
451    tree: Option<&dyn frus_widgets::Widget<Msg>>,
452    at: Point,
453) -> (Option<Msg>, Option<frus_widgets::DragSource>) {
454    // A back gesture owns its pointer from the press that starts it, as the reference's
455    // pop gesture does: nothing on either page takes a finger that is taking a page away
456    // (milestone 531). The page below is not even in the frame yet when the press lands —
457    // what is under the finger is whatever the frame on screen holds.
458    if matches!(drag, Some(Drag::Back { .. })) {
459        return (None, None);
460    }
461    let Some(ui) = ui else {
462        return (None, None);
463    };
464    let free = matches!(drag, None | Some(Drag::Scroll { moved: false, .. }));
465    let long_press = if free { ui.long_press_at(at) } else { None };
466    let lift = ui.drag_source_at(at).filter(|source| {
467        tree.and_then(|tree| find_widget(tree, source.id))
468            .is_some_and(|widget| widget.drag_needs_long_press())
469    });
470    (long_press, lift)
471}
472
473/// The drag a press is left with when its hold's deadline fires, from the drag it had
474/// and what the press found to lift.
475///
476/// A pending touch scroll no longer has any reason to exist — unless the hold was the
477/// lift of an item or of a row, in which case the scroll hands the gesture over and what
478/// was held is already up: `moved` is true because the hold *was* the threshold, and
479/// asking for a movement as well would mean a row that was held and then carried
480/// straight out of the list never engaged at all.
481fn drag_after_hold(
482    drag: Option<Drag>,
483    lift: Option<frus_widgets::DragSource>,
484    reorder: Option<(WidgetId, usize, Point)>,
485    cursor: Point,
486) -> Option<Drag> {
487    // Never a back gesture's: the hold was not what that finger was doing, and taking the
488    // drag from it would leave the application's gesture open with nothing to end it.
489    if let Some(Drag::Back { start_x }) = drag {
490        return Some(Drag::Back { start_x });
491    }
492    if let Some(source) = lift {
493        Some(Drag::Item {
494            source,
495            start: cursor,
496            moved: true,
497            over: None,
498        })
499    } else if let Some((id, from, start)) = reorder {
500        Some(Drag::Reorder {
501            id,
502            from,
503            start,
504            moved: true,
505            carried: None,
506        })
507    } else {
508        None
509    }
510}
511
512/// A drag currently under way with the mouse.
513enum Drag {
514    /// A scrollbar's thumb.
515    Scrollbar {
516        id: WidgetId,
517        vertical: bool,
518        grab: f32,
519        track_start: f32,
520        track_len: f32,
521        thumb_len: f32,
522        max: f32,
523        /// Whether this axis numbers its offsets from the far end, in which case the
524        /// thumb's position along the track reads backwards.
525        reverse: bool,
526    },
527    /// A text selection inside a field, with its bounds, for placement.
528    TextSelect {
529        id: WidgetId,
530        rect: frus_widgets::Rect,
531    },
532    /// A **selection handle**, held (milestone 511). `grab` is the offset from the finger
533    /// to the text position the handle stands for, so the selection follows the handle
534    /// rather than jumping to the fingertip the moment it moves.
535    SelectionHandle {
536        id: WidgetId,
537        rect: frus_widgets::Rect,
538        handle: crate::selection::Handle,
539        grab: Point,
540    },
541    /// A draggable widget — a slider or a handle — dragged along its horizontal axis.
542    /// `last_x` is the pointer's last abscissa, so the **delta** can be delivered to
543    /// the handles that accumulate, such as a column resize.
544    Widget {
545        id: WidgetId,
546        rect: frus_widgets::Rect,
547        last_x: f32,
548    },
549    /// Reordering a **column**: a header is grabbed (`id`, column `from`) and dropped
550    /// onto another. `moved` tells a drag from a plain tap — which stays a sort —
551    /// by the `TOUCH_SLOP` threshold measured from `start`.
552    Reorder {
553        id: WidgetId,
554        from: usize,
555        start: Point,
556        moved: bool,
557        /// What is carried and where: the row the grab moves and its box, taken from the
558        /// frame the first time the drag is carried and moved with the content since, as a
559        /// lifted item's box is. The frame knows a reorderable's box only while it shows, and
560        /// only as much of it as shows — and carrying a row to an edge is exactly what
561        /// scrolls it out of sight (milestone 527, seen on a phone).
562        carried: Option<(WidgetId, Rect)>,
563    },
564    /// Panning an interactive viewport (`InteractiveViewer`): the pointer pushes the
565    /// content. `last` is the previous position, for the delta; `moved` tells a real
566    /// pan from a plain tap, by the `TOUCH_SLOP` threshold, so a click on a child can
567    /// still get through. `viewport` bounds the pan to the frame. The release
568    /// velocity comes from the shell's gesture tracker.
569    Pan {
570        id: WidgetId,
571        last: Point,
572        moved: bool,
573        viewport: frus_widgets::Rect,
574    },
575    /// Scrolling a scrollable area with a finger. `moved` tells a real scroll from a
576    /// plain tap, movement staying under the `TOUCH_SLOP` threshold.
577    Scroll {
578        id: WidgetId,
579        last: Point,
580        moved: bool,
581        /// The speed inherited from a fling this press interrupted, per axis, in
582        /// px/s — added to the release velocity so repeated swipes build momentum
583        /// where the platform does that. Zero when the content was already still.
584        carried: (f32, f32),
585        /// A dismissible item under the finger, still in the running. Both gestures
586        /// start the same way, so neither is chosen at the press: the first movement
587        /// past the threshold decides by **direction**, and the loser never sees the
588        /// gesture. Cleared once the scroll has won.
589        dismiss: Option<frus_widgets::Dismissable>,
590        /// The axis this gesture was **claimed by**, decided once at the threshold and
591        /// held for the rest of the drag: `true` for vertical.
592        ///
593        /// A page that scrolls down must not drift sideways at the same time, and a
594        /// finger never travels in a straight line. The reference settles this in its
595        /// gesture arena — a vertical and a horizontal recogniser compete, the first
596        /// past its own slop wins, the loser is out of the gesture entirely — and this
597        /// is the same rule for an area that can go both ways. `None` until the
598        /// threshold is crossed.
599        axis: Option<bool>,
600    },
601    /// Swiping a [`frus_widgets::Dismissible`] item aside. `last` is the previous
602    /// position, for the delta; the item is already past the threshold by the time this
603    /// exists, since it is only ever reached by winning the direction test.
604    Dismiss {
605        item: frus_widgets::Dismissable,
606        last: Point,
607        /// `false` until the finger has passed the threshold — a press that never
608        /// travels is still a tap on the row.
609        moved: bool,
610    },
611    /// Carrying a [`frus_widgets::Draggable`] towards a [`frus_widgets::DragTarget`].
612    /// `over` is the target the pointer is on now, when it would accept the payload —
613    /// kept here so that entering and leaving one costs a comparison rather than a
614    /// second hit test.
615    Item {
616        source: frus_widgets::DragSource,
617        start: Point,
618        /// `false` until the finger has passed the threshold: a press that never
619        /// travels is a plain click on whatever is inside.
620        moved: bool,
621        over: Option<WidgetId>,
622    },
623    /// A [`frus_widgets::DraggableScrollableSheet`] moved by its panel, where nothing
624    /// under the finger scrolls — its grabber, its header, a list too short to move
625    /// (milestone 515). `available` is the height its shares are shares of, taken at
626    /// the press: the panel may shrink to nothing under the finger, and still come back.
627    Sheet {
628        id: WidgetId,
629        last: Point,
630        /// `false` until the finger has passed the threshold: a press that never
631        /// travels is a tap on whatever is in the sheet.
632        moved: bool,
633        available: f32,
634    },
635    /// The "back" gesture: the framework measures the finger's progress and velocity
636    /// and passes them to the application, which decides on the navigation.
637    Back { start_x: f32 },
638}
639
640/// The driver: an `event → frame` loop around an [`Application`].
641/// Turns an application's `view` into a tree that is **ready to be read**.
642///
643/// The one way this shell builds a tree, and it exists because there were two. A
644/// `ThemeBuilder` — and everything built on one, an `AppBar` included — has no children
645/// until [`build_deferred`] has been over it, so every traversal of a tree that has not
646/// been prepared finds nothing inside such a subtree and says nothing about it.
647///
648/// Both of the shell's build sites read the tree **before** the layout pass would have
649/// prepared it: the frame's own path calls `collect_ids` on it for the mount and leave
650/// bookkeeping, and the burst path hands it straight to `find_widget`. Neither of those is
651/// a mistake to be spotted at a call site; they are why this is one function.
652///
653/// The surface must already be described — the reader's font setting reaches anything a
654/// deferred subtree measures, and a build outside a surface measures at scale 1 while the
655/// frame lays it out at whatever the reader asked for (milestone 408).
656/// **The ambient answers an application gives**, read afresh every frame.
657///
658/// Both of these are settings rather than facts: a settings screen may turn scrollbars on
659/// or change the reader's language while the application is running, and neither should
660/// wait for a restart. Reading them once at start-up is the bug this shape avoids.
661///
662/// It is one function so that a test can drive it. The frame loop needs a window and an
663/// event-loop proxy, so nothing in this repo can call the loop — which is exactly how a
664/// setting that never reached production got shipped once already (milestone 408).
665fn install_ambient<A: Application>(
666    app: &A,
667    runtime: &mut frus_widgets::Runtime,
668    preferred: &[frus_widgets::Locale],
669) {
670    runtime.scrollbars = app.scrollbars();
671    // **Which language the interface is in**, resolved from what the platform reports
672    // against what the application has. Installed before the table, because an
673    // application choosing its table by language reads it from here.
674    frus_widgets::locale::install(app.resolved_locale(preferred));
675    if let Some(table) = app.localizations() {
676        frus_widgets::localizations::install(table);
677    }
678}
679
680fn build_view<A: Application>(
681    app: &A,
682    theme: &Theme,
683    runtime: &frus_widgets::Runtime,
684) -> Box<dyn Widget<A::Message>> {
685    debug_assert!(
686        frus_widgets::MediaQuery::of().is_described(),
687        "a view is being built with no surface described: install one first — milestone 416"
688    );
689    // Every state a component keeps is marked as reached while the tree is built, and
690    // what a build did not reach is let go when the frame that laid it out is done.
691    runtime.states.begin_build();
692    let tree = app.view(theme);
693    build_deferred(tree.as_ref(), theme, runtime);
694    tree
695}
696
697/// Where the messages an effect produces on another thread are sent: the event loop's proxy,
698/// or nowhere for a driver no event loop runs.
699struct Mailbox<M: 'static>(Option<EventLoopProxy<M>>);
700
701impl<M: 'static> Clone for Mailbox<M> {
702    fn clone(&self) -> Self {
703        Self(self.0.clone())
704    }
705}
706
707impl<M: 'static> Mailbox<M> {
708    /// Sends `message` to the loop. An error when the loop has closed, or when there is
709    /// none — either way nobody is left to tell.
710    fn send_event(&self, message: M) -> Result<(), ()> {
711        match &self.0 {
712            Some(proxy) => proxy.send_event(message).map_err(|_| ()),
713            None => Err(()),
714        }
715    }
716}
717
718/// The event handler that drives an [`Application`]: it owns the window, the renderer, the
719/// last interface built and the runtime, and turns each event of the loop into messages,
720/// frames and effects. [`run`](crate::run) and the other entry points build one; it is
721/// exported for a platform layer that supplies its own loop, not for an application to
722/// construct.
723pub struct App<A: Application> {
724    /// The application being driven: its state and logic.
725    app: A,
726    /// The channel that feeds back the messages effects produce, from their threads.
727    proxy: Mailbox<A::Message>,
728    window: Option<Arc<Window>>,
729    renderer: Option<Renderer>,
730    /// A renderer being initialised **asynchronously**, on the Web only: filled in by
731    /// the `spawn_local` future and picked up on the first frame, the Web being unable
732    /// to block on GPU init.
733    #[cfg(web)]
734    pending_renderer: std::rc::Rc<std::cell::RefCell<Option<Renderer>>>,
735    /// What this side knows of the browser's history — see [`crate::history`].
736    #[cfg(web)]
737    history: crate::history::History,
738    /// How the application's location is written in the address bar, and the document's base
739    /// path, which the `Path` way of writing needs. Read once, at launch.
740    #[cfg(web)]
741    address: (crate::LocationStrategy, String),
742    /// The window's accessibility bridge (AccessKit) — desktop only.
743    #[cfg(desktop)]
744    a11y: Option<crate::a11y::A11y>,
745    /// The window's accessibility bridge, projected into the canvas's DOM — web only.
746    #[cfg(web)]
747    a11y: Option<crate::a11y_web::A11y>,
748    /// The last interface built, used for hit testing, focus and scrolling.
749    ui: Option<Ui<A::Message>>,
750    /// The last widget tree built, used for keyboard and editing routing.
751    tree: Option<Box<dyn Widget<A::Message>>>,
752    /// The pointer's last known position, in **logical** pixels.
753    cursor: Point,
754    /// Where the pointer that **hovers** is: a mouse once it has moved, a finger only
755    /// while it touches. Kept as a place and asked of each frame — see [`crate::hover`].
756    hover: crate::hover::Hover,
757    /// The screen's DPI scale factor (physical = logical × scale × density).
758    scale: f32,
759    /// The last **logical** size handed to the app, to detect breakpoints.
760    last_size: Option<(f32, f32)>,
761    /// State retained between frames: hover and focus, scroll, caret and selection.
762    runtime: Runtime,
763    /// **The theme on display and the fade toward the one now asked for** — the
764    /// framework's, not the application's, since milestone 452. It resolves the
765    /// application's themes against the platform's brightness and the reader's contrast
766    /// setting once a frame, and crosses between two of them over time.
767    themes: crate::theming::ThemeFade,
768    /// Live-reload watching (development, `FRUS_WATCH=1`): relaunch on recompilation.
769    reload: Option<crate::reload::ReloadWatcher>,
770    /// Overflowing boxes already named on the console, so that a layout that does not
771    /// fit is reported once and not sixty times a second.
772    reported_overflows: std::cell::RefCell<std::collections::HashSet<u64>>,
773    /// Is the runtime inspector on? Toggled by F12, in debug builds only.
774    inspector: bool,
775    /// A tree dump to print on the next inspected frame.
776    inspector_dump: bool,
777    /// The current keyboard modifiers.
778    shift: bool,
779    ctrl: bool,
780    alt: bool,
781    meta: bool,
782    /// The remembered "goal" visual column for Up/Down/PgUp/PgDn: crossing shorter
783    /// lines keeps the original column, the way an editor does. Cleared as soon as any
784    /// other caret movement happens.
785    goal_x: Option<f32>,
786    /// Clipboard access: `arboard` on the desktop, the platform's on Android, the
787    /// browser's on the web, nothing yet on iOS.
788    clipboard: clip::Clipboard,
789    /// Has the startup effect (`init`) already run? This keeps it from being replayed
790    /// when the surface is recreated, as it is when Android returns from background.
791    started: bool,
792    /// The last frame's instant, for the animations' dt.
793    last_frame: Option<Instant>,
794    /// The **build** phase is dirty: the app's state, or the size, changed and the
795    /// `view` must be rebuilt. The view is a pure function of `(state, theme, size)` —
796    /// never of hover, scroll or focus, which live in the `Runtime` — so a frame that
797    /// only animates an interaction merely **repaints** the retained tree (§1: "a hover
798    /// touches paint and nothing else").
799    build_dirty: bool,
800    /// Whether the application's own animations were moving at the last frame. The frame
801    /// they stop in is built once more: the tick that ends a transition has changed the
802    /// state — the page it left is gone — and says it is no longer moving, so without it
803    /// the tree hit-tested from then on is the transition's last (milestone 531).
804    app_was_animating: bool,
805    /// The mouse drag under way.
806    drag: Option<Drag>,
807    /// Was the pointer that started the drag under way a finger? A mouse is
808    /// precise and needs almost no slop; a finger needs a lot.
809    pointer_touch: bool,
810    /// The pointer history of the drag under way, and the instant it began.
811    ///
812    /// One tracker for all of them: at most one drag is active at a time, and every
813    /// kind of drag asks the same question on release — how fast was the finger
814    /// going. Keeping it here rather than in each [`Drag`] variant means the
815    /// gesture's clock and its history start together, in one place.
816    gesture_velocity: VelocityTracker,
817    gesture_start: Instant,
818    /// The pointer's **smoothed** abscissa during a reorder: it springs toward the
819    /// real position, giving the columns' sliding a gentle inertia — the background
820    /// catches up with the ghost, which sticks to the pointer.
821    reorder_x: f32,
822    /// The **smoothed** ordinate of the **insertion line** during a **vertical**
823    /// reorder, that is, Kanban cards: it springs toward the **chosen** slot edge, the
824    /// hovered half, so that the line and the gap *slide* between cards instead of
825    /// jumping — the vertical counterpart of the horizontal `reorder_x` spring.
826    reorder_y: f32,
827    /// The last **announcement** pushed to the accessibility bridge's live region, for
828    /// the screen reader. It persists as is: it is re-spoken only on a change, so the
829    /// same text carried over every frame does not repeat. Desktop and web.
830    #[cfg(any(desktop, web))]
831    announce: String,
832    /// The tap-or-long-press recogniser (gesture tier 1).
833    press: PressRecognizer,
834    /// The pressed target's long-press message, captured on the press.
835    long_press_msg: Option<A::Message>,
836    /// A **text field** pressed with a finger: a hold there selects the word under it
837    /// (milestone 511), where on anything else it is the widget's long press.
838    pending_word: Option<WidgetId>,
839    /// The focused field's caret blink (milestone 513).
840    caret: crate::caret::CaretBlink,
841    /// The form last shown to the platform's autofill service, as it was shown
842    /// (milestone 512) — what a returned value is routed against, and what a value the
843    /// service has not heard is told apart from.
844    #[cfg(android)]
845    autofill_reported: Vec<crate::autofill::AutofillField>,
846    /// The field of that form the service was told has focus.
847    #[cfg(android)]
848    autofill_focus: Option<WidgetId>,
849    /// The [`frus_widgets::AutofillGroup`] that form is, while it is on screen.
850    #[cfg(android)]
851    autofill_group: Option<WidgetId>,
852    /// When the last **recorded** edit happened, for the pause that breaks a run of
853    /// typing into two steps of undo. One field and not one per text field, because only
854    /// the focused one is being typed into, and leaving a field ends its run anyway.
855    last_edit_at: Option<Instant>,
856    /// A [`frus_widgets::Draggable`] that asked to be lifted by a **hold**, waiting
857    /// for the long-press deadline. Inside a scrollable this is the only way up: the
858    /// plain drag belongs to the scroll, and a hold is the one signal it cannot claim.
859    pending_lift: Option<frus_widgets::DragSource>,
860    /// A **reorderable row** that asked to be lifted by a hold, waiting for the same
861    /// deadline: `(the row, its index, where the finger landed)`.
862    ///
863    /// Its own field rather than a second use of `pending_lift`, because what the two
864    /// become at the deadline is different — one carries a payload to a drop target, the
865    /// other carries itself to a slot — but the reason they wait is the same, and it is
866    /// the reason a list can still be scrolled.
867    pending_reorder: Option<(WidgetId, usize, Point)>,
868    /// The last click's instant, for double-click detection.
869    last_click_time: Option<Instant>,
870    /// A counter for the keys of leaving events, which fade out.
871    leaving_counter: u64,
872    /// The running subscriptions: id → cancellation handle, dropping which stops it.
873    running_subs: HashMap<u64, SubHandle>,
874    /// Pending focus requests — the keys `Command::focus` produced — resolved against
875    /// the **freshly built** tree on the next frame.
876    pending_focus: Vec<u64>,
877    /// Pending **scroll** requests — the `(key, ScrollTo)` pairs `Command::scroll`
878    /// produced — resolved against the frame that follows, and dropped whether or not
879    /// the key named anything (see `Command::scroll`).
880    pending_scroll: Vec<(u64, ScrollTo)>,
881    /// The requests of the frame in progress that the **previous** frame's registry
882    /// could not place — a region that has only just appeared. Tried once more against
883    /// the registry this frame builds, and then gone: a request gets one frame.
884    retry_scroll: Vec<(u64, ScrollTo)>,
885    /// Pending **sheet** requests — the `(key, SheetTo)` pairs `Command::sheet` produced —
886    /// on the terms of the scroll requests above.
887    pending_sheet: Vec<(u64, SheetTo)>,
888    /// The sheet requests of the frame in progress that the previous frame's sheets did
889    /// not name: tried once more against this frame's.
890    retry_sheet: Vec<(u64, SheetTo)>,
891    /// The **focus history** of triggers, for returning focus when an overlay closes:
892    /// on every focus change the old one, if still present, is pushed; when focus
893    /// **vanishes** because a menu or modal closed, we go back to the most recent
894    /// entry that is still present.
895    focus_history: Vec<WidgetId>,
896    /// The previous frame's focus, to detect the transitions worth pushing.
897    prev_focus: Option<WidgetId>,
898    /// The window is occluded, so rendering is suspended.
899    occluded: bool,
900    /// Cumulative elapsed time, in seconds, for the continuous animations.
901    elapsed: f32,
902    /// The last window insets handed to the app — padding plus keyboard — in logical px.
903    last_insets: WindowInsets,
904    /// The soft keyboard has just grown, so the focused widget is brought back into view
905    /// once the frame that lays out the shortened window is built: the keyboard covers
906    /// the bottom of the screen, and a field under it is a field the reader cannot see
907    /// while typing in it.
908    reveal_after_keyboard: bool,
909    /// What the **platform** last said about the person using it: the font-size slider,
910    /// the night setting, the accessibility switches.
911    ///
912    /// Cached rather than read per frame, because reading it is a walk across the JNI
913    /// boundary and the answer changes about once a year. It is refreshed when the
914    /// surface appears — which on Android is also when it *reappears*, the activity being
915    /// recreated on a font-scale or night change since no `configChanges` is declared —
916    /// and on a theme change where the platform sends one.
917    platform: PlatformSettings,
918    /// The **keyboard-free** inset baseline, along with the physical size it was
919    /// taken at — a rotation resets it: whatever bottom inset exceeds it is credited
920    /// to the software keyboard.
921    inset_baseline: Option<(Insets, (u32, u32))>,
922    /// The Android activity's handle, used to query the insets, the keyboard and so on.
923    /// The current **lifecycle** state, so the app is notified only on real
924    /// **transitions**.
925    lifecycle: Lifecycle,
926    #[cfg(android)]
927    android_app: Option<winit::platform::android::activity::AndroidApp>,
928    /// Is the software keyboard being asked for? It follows the text fields' focus.
929    #[cfg(android)]
930    soft_input_shown: bool,
931    /// The system bars as the platform was last told them, so that it is told only of a
932    /// change (#46).
933    #[cfg(android)]
934    system_bars: Option<frus_widgets::SystemBars>,
935}
936
937impl<A: Application> App<A> {
938    /// Creates the driver around an application and its message channel.
939    pub fn new(app: A, proxy: EventLoopProxy<A::Message>) -> Self {
940        Self::with_mailbox(app, Mailbox(Some(proxy)))
941    }
942
943    /// A driver with no event loop behind it, for a test that feeds it input and frames
944    /// by hand: whatever an effect sends back goes nowhere. An event loop cannot be built
945    /// on a machine with no display, which is where the continuous integration runs.
946    #[cfg(any(test, feature = "testing"))]
947    fn detached(app: A) -> Self {
948        Self::with_mailbox(app, Mailbox(None))
949    }
950
951    fn with_mailbox(app: A, proxy: Mailbox<A::Message>) -> Self {
952        // The widget layer knows how to *want* an image over the network and has no way
953        // to get one: no runtime, no socket, and no dependency on this crate, since the
954        // dependency runs the other way. So the shell says how, on the way up, and the
955        // widget layer only asks. Same shape as the image decoder a step earlier.
956        #[cfg(feature = "net")]
957        frus_widgets::set_image_fetcher(fetch_image_bytes);
958        Self {
959            app,
960            proxy,
961            window: None,
962            renderer: None,
963            #[cfg(web)]
964            pending_renderer: std::rc::Rc::new(std::cell::RefCell::new(None)),
965            #[cfg(web)]
966            history: crate::history::History::new(),
967            #[cfg(web)]
968            address: (crate::LocationStrategy::Hash, "/".to_string()),
969            #[cfg(desktop)]
970            a11y: None,
971            #[cfg(web)]
972            a11y: None,
973            ui: None,
974            tree: None,
975            cursor: Point::new(0.0, 0.0),
976            hover: crate::hover::Hover::default(),
977            scale: 1.0,
978            last_size: None,
979            runtime: Runtime::default(),
980            themes: crate::theming::ThemeFade::default(),
981            reload: crate::reload::ReloadWatcher::new(),
982            reported_overflows: std::cell::RefCell::new(std::collections::HashSet::new()),
983            inspector: false,
984            inspector_dump: false,
985            shift: false,
986            ctrl: false,
987            alt: false,
988            meta: false,
989            goal_x: None,
990            clipboard: clip::Clipboard::new(),
991            started: false,
992            last_frame: None,
993            build_dirty: true,
994            app_was_animating: false,
995            drag: None,
996            pointer_touch: false,
997            gesture_velocity: VelocityTracker::platform_default(),
998            gesture_start: Instant::now(),
999            reorder_x: 0.0,
1000            reorder_y: 0.0,
1001            #[cfg(any(desktop, web))]
1002            announce: String::new(),
1003            press: PressRecognizer::new(),
1004            long_press_msg: None,
1005            pending_word: None,
1006            caret: crate::caret::CaretBlink::new(),
1007            #[cfg(android)]
1008            autofill_reported: Vec::new(),
1009            #[cfg(android)]
1010            autofill_focus: None,
1011            #[cfg(android)]
1012            autofill_group: None,
1013            last_edit_at: None,
1014            pending_lift: None,
1015            pending_reorder: None,
1016            last_click_time: None,
1017            leaving_counter: 0,
1018            running_subs: HashMap::new(),
1019            pending_focus: Vec::new(),
1020            pending_scroll: Vec::new(),
1021            retry_scroll: Vec::new(),
1022            pending_sheet: Vec::new(),
1023            retry_sheet: Vec::new(),
1024            focus_history: Vec::new(),
1025            prev_focus: None,
1026            occluded: false,
1027            elapsed: 0.0,
1028            last_insets: WindowInsets::ZERO,
1029            reveal_after_keyboard: false,
1030            platform: PlatformSettings::default(),
1031            inset_baseline: None,
1032            // The app starts out detached; `resumed` will move it to `Resumed`.
1033            lifecycle: Lifecycle::Detached,
1034            #[cfg(android)]
1035            android_app: None,
1036            #[cfg(android)]
1037            soft_input_shown: false,
1038            #[cfg(android)]
1039            system_bars: None,
1040        }
1041    }
1042
1043    /// Replays the actions an assistive technology asked for: an AT click activates
1044    /// the widget, exactly as a pointer click would, and an AT focus focuses it.
1045    #[cfg(desktop)]
1046    fn drain_a11y_actions(&mut self) {
1047        use crate::a11y::A11yAction;
1048        let actions = match self.a11y.as_ref() {
1049            Some(a11y) => a11y.take_actions(),
1050            None => return,
1051        };
1052        for action in actions {
1053            match action {
1054                A11yAction::Click(id) => {
1055                    if let Some(msg) = self.ui.as_ref().and_then(|ui| ui.msg_for(id)) {
1056                        self.dispatch(msg);
1057                        self.request_redraw();
1058                    }
1059                }
1060                A11yAction::Focus(id) => {
1061                    self.runtime.input.focused = Some(id);
1062                    self.runtime.focus_visible = true;
1063                    self.request_redraw();
1064                }
1065            }
1066        }
1067    }
1068
1069    /// The web's counterpart to `drain_a11y_actions` above — same replay, the DOM
1070    /// bridge's own action type (#18).
1071    #[cfg(web)]
1072    fn drain_a11y_actions(&mut self) {
1073        use crate::a11y_web::A11yAction;
1074        let actions = match self.a11y.as_ref() {
1075            Some(a11y) => a11y.take_actions(),
1076            None => return,
1077        };
1078        for action in actions {
1079            match action {
1080                A11yAction::Click(id) => {
1081                    if let Some(msg) = self.ui.as_ref().and_then(|ui| ui.msg_for(id)) {
1082                        self.dispatch(msg);
1083                        self.request_redraw();
1084                    }
1085                }
1086                A11yAction::Focus(id) => {
1087                    self.runtime.input.focused = Some(id);
1088                    self.runtime.focus_visible = true;
1089                    self.request_redraw();
1090                }
1091            }
1092        }
1093    }
1094
1095    /// Keeps the **system bars** in step with the frame (#46): their colour and their icons,
1096    /// from the regions under them and then the theme. The platform is told only when the
1097    /// answer changes, telling it being a crossing to the Java UI thread — and a request
1098    /// that did not arrive is asked again next frame rather than remembered as made.
1099    ///
1100    /// The two points are the first row of content under the status bar and the last one
1101    /// above the navigation bar: this window is not drawn behind the bars, so what a bar
1102    /// stands against is what lies at the edge of the content next to it.
1103    fn sync_system_bars(&mut self, theme: &Theme) {
1104        #[cfg(android)]
1105        {
1106            let Some(ui) = self.ui.as_ref() else {
1107                return;
1108            };
1109            let Some(window) = self.window.as_ref() else {
1110                return;
1111            };
1112            let scale = self.total_scale();
1113            let size = window.inner_size();
1114            let (width, height) = (size.width as f32 / scale, size.height as f32 / scale);
1115            let edges = self.last_insets.padding;
1116            let bars = ui
1117                .system_ui_style(
1118                    frus_widgets::Point::new(width / 2.0, edges.top + 0.5),
1119                    frus_widgets::Point::new(width / 2.0, height - edges.bottom - 0.5),
1120                )
1121                .resolve(theme);
1122            if self.system_bars != Some(bars) && crate::android_system_bars::apply(bars) {
1123                self.system_bars = Some(bars);
1124            }
1125        }
1126        #[cfg(not(android))]
1127        let _ = theme;
1128    }
1129
1130    /// Keeps the **software keyboard** in step with focus: asked for when focus is in
1131    /// a text field (`wants_keyboard`), closed otherwise. Called at the end of a
1132    /// frame, since any focus change already triggers a redraw.
1133    fn sync_soft_input(&mut self) {
1134        #[cfg(android)]
1135        {
1136            let editing = self
1137                .runtime
1138                .input
1139                .focused
1140                .and_then(|id| {
1141                    self.tree
1142                        .as_ref()
1143                        .and_then(|tree| find_widget(tree.as_ref(), id))
1144                })
1145                .is_some_and(wants_keyboard);
1146            if editing != self.soft_input_shown {
1147                self.soft_input_shown = editing;
1148                self.end_composition();
1149                if crate::android_ime::installed() {
1150                    // The InputConnection bridge: the Java view captures the IME.
1151                    if editing {
1152                        if let Some(id) = self.runtime.input.focused {
1153                            self.push_ime_context(id);
1154                        }
1155                        crate::android_ime::start_input(self.focused_ime());
1156                    } else {
1157                        crate::android_ime::clear_editor_state();
1158                        crate::android_ime::stop_input();
1159                    }
1160                } else if let Some(app) = &self.android_app {
1161                    // The TYPE_NULL fallback: a plain open, Latin keys only.
1162                    if editing {
1163                        app.show_soft_input(true);
1164                    } else {
1165                        app.hide_soft_input(false);
1166                    }
1167                }
1168            }
1169        }
1170    }
1171
1172    /// What keyboard the **focused field** asks for.
1173    ///
1174    /// Found the same way the caret is — the focused id, resolved against the last
1175    /// tree built — so the two answers cannot disagree about which field is being
1176    /// typed into. A focus that resolves to nothing falls back to ordinary text, which
1177    /// is what every field got before this was asked at all.
1178    #[cfg(android)]
1179    fn focused_ime(&self) -> frus_widgets::Ime {
1180        self.runtime
1181            .input
1182            .focused
1183            .and_then(|id| {
1184                self.tree
1185                    .as_ref()
1186                    .and_then(|tree| find_widget(tree.as_ref(), id))
1187            })
1188            .map(|widget| widget.ime())
1189            .unwrap_or_default()
1190    }
1191
1192    /// Notifies the app of a lifecycle **change**; never the same state twice in a row.
1193    fn set_lifecycle(&mut self, state: Lifecycle) {
1194        if self.lifecycle != state {
1195            self.lifecycle = state;
1196            self.app.on_lifecycle(state);
1197        }
1198    }
1199
1200    /// **Explicitly** asks for the software keyboard again, for the focused field —
1201    /// called when the user **taps in a text field**, even one already focused. This is
1202    /// indispensable because the keyboard may have been closed by the **system back
1203    /// button** without the app being told: `soft_input_shown` then stays `true` and
1204    /// focus does not change on the next tap, so
1205    /// [`sync_soft_input`](Self::sync_soft_input)'s diff would see no change and never
1206    /// reopen the keyboard. Here it is reopened unconditionally — the native behaviour:
1207    /// tapping in a field shows the keyboard.
1208    fn request_soft_input(&mut self) {
1209        #[cfg(android)]
1210        {
1211            self.soft_input_shown = true;
1212            self.end_composition();
1213            if crate::android_ime::installed() {
1214                if let Some(id) = self.runtime.input.focused {
1215                    self.push_ime_context(id);
1216                }
1217                crate::android_ime::start_input(self.focused_ime());
1218            } else if let Some(app) = &self.android_app {
1219                app.show_soft_input(true);
1220            }
1221        }
1222    }
1223
1224    /// Applies the pending IME operations to the focused field (the §6 bridge). The
1225    /// composition is materialised **in the field**: each update erases the previous
1226    /// one, the controlled model having no styled composition region yet — see
1227    /// docs/milestone-81.md.
1228    #[cfg(android)]
1229    fn drain_ime(&mut self) {
1230        use crate::ime::Step;
1231        let events = crate::android_ime::drain();
1232        if events.is_empty() {
1233            return;
1234        }
1235        let Some(focused) = self.runtime.input.focused else {
1236            return;
1237        };
1238        for event in events {
1239            let edit = self
1240                .runtime
1241                .edits
1242                .get(&focused)
1243                .copied()
1244                .unwrap_or_default();
1245            // Planned against the field as it stands before this operation: its text,
1246            // its caret and the composition it underlines (milestone 510).
1247            let text = self
1248                .tree
1249                .as_ref()
1250                .and_then(|tree| find_widget(tree.as_ref(), focused))
1251                .and_then(|widget| widget.text_value().map(str::to_owned))
1252                .unwrap_or_default();
1253            for step in crate::ime::plan(&event, &edit, &text) {
1254                match step {
1255                    Step::Place { cursor, anchor } => {
1256                        let edit = self.runtime.edits.entry(focused).or_default();
1257                        edit.cursor = cursor;
1258                        edit.anchor = anchor;
1259                    }
1260                    Step::Key(key) => self.apply_key(focused, key),
1261                    Step::Compose(region) => {
1262                        if let Some(edit) = self.runtime.edits.get_mut(&focused) {
1263                            edit.composing = region;
1264                        }
1265                    }
1266                    Step::ComposeTo(start) => {
1267                        if let Some(edit) = self.runtime.edits.get_mut(&focused) {
1268                            let end = edit.cursor;
1269                            edit.composing = (end > start).then_some((start, end));
1270                        }
1271                    }
1272                }
1273            }
1274        }
1275        // Refresh the input context, which the IME queries for its suggestions.
1276        self.push_ime_context(focused);
1277        self.request_redraw();
1278    }
1279
1280    /// Keeps the platform's **autofill service** in step with the form being edited
1281    /// (milestone 512). When a field that says what it is for takes focus, its form is
1282    /// shown to the service and the service is told which field it is in; while it stays,
1283    /// every value the service has not heard is reported, or what it saves is what the
1284    /// fields held when it first looked; and when the form's group has left the screen
1285    /// — submitted, or navigated away from — it is committed, which is the moment a
1286    /// service may offer to save what was typed.
1287    #[cfg(android)]
1288    fn sync_autofill(&mut self) {
1289        use crate::{android_autofill as platform, autofill};
1290        if !crate::android_ime::installed() {
1291            return;
1292        }
1293        let scale = self.total_scale();
1294        let (Some(ui), Some(tree)) = (self.ui.as_ref(), self.tree.as_ref()) else {
1295            return;
1296        };
1297        let fields_of = |id: WidgetId| {
1298            autofill::structure(&ui.form_of(id), scale, |widget| {
1299                find_widget(tree.as_ref(), widget).map(|w| {
1300                    (
1301                        w.autofill_hints(),
1302                        w.text_value().unwrap_or_default().to_owned(),
1303                    )
1304                })
1305            })
1306        };
1307        // The focused field, when it takes part: it says what it is for.
1308        let focus = self.runtime.input.focused.filter(|&id| {
1309            find_widget(tree.as_ref(), id).is_some_and(|w| !w.autofill_hints().is_empty())
1310        });
1311        if focus != self.autofill_focus {
1312            if let Some(left) = self.autofill_focus {
1313                platform::exit(autofill::virtual_id(left));
1314            }
1315            if let Some(entered) = focus {
1316                let group = ui.form_group(entered);
1317                // A field of the form already shown — the next step of a wizard — keeps
1318                // the fields shown before it; any other form starts afresh.
1319                let shown: &[autofill::AutofillField] =
1320                    if group.is_some() && group == self.autofill_group {
1321                        &self.autofill_reported
1322                    } else {
1323                        &[]
1324                    };
1325                let fields = autofill::with_gone(fields_of(entered), shown);
1326                platform::publish(&fields);
1327                platform::enter(autofill::virtual_id(entered));
1328                self.autofill_reported = fields;
1329                self.autofill_group = group;
1330            }
1331            self.autofill_focus = focus;
1332        } else if let Some(id) = focus {
1333            let fields = autofill::with_gone(fields_of(id), &self.autofill_reported);
1334            for (virtual_id, value) in autofill::changed(&self.autofill_reported, &fields) {
1335                platform::value_changed(virtual_id, &value);
1336            }
1337            self.autofill_reported = fields;
1338        }
1339        if let Some(group) = self.autofill_group {
1340            if ui.group_stops(group).is_empty() {
1341                platform::commit();
1342                self.autofill_group = None;
1343                self.autofill_reported.clear();
1344            }
1345        }
1346    }
1347
1348    /// Puts the values an autofill service chose into their fields (milestone 512), each
1349    /// through the message typing it would have produced — the path an undo takes, so a
1350    /// field that refuses typing refuses this too — with the caret after what arrived.
1351    #[cfg(android)]
1352    fn drain_autofill(&mut self) {
1353        let fills = crate::android_autofill::drain();
1354        if fills.is_empty() {
1355            return;
1356        }
1357        for (widget, value) in crate::autofill::route(&self.autofill_reported, fills) {
1358            let message = self
1359                .tree
1360                .as_ref()
1361                .and_then(|tree| find_widget(tree.as_ref(), widget))
1362                .and_then(|w| w.replace_value(value.clone()));
1363            if let Some(message) = message {
1364                self.runtime.edits.insert(
1365                    widget,
1366                    Edit {
1367                        cursor: value.chars().count(),
1368                        anchor: None,
1369                        composing: None,
1370                    },
1371                );
1372                self.dispatch_edit(message);
1373            }
1374        }
1375        if let Some(id) = self.runtime.input.focused {
1376            self.push_ime_context(id);
1377        }
1378        self.request_redraw();
1379    }
1380
1381    /// Publishes field `id`'s editing state to the bridge — text, caret and selection
1382    /// — the context the IME reads for its suggestions.
1383    #[cfg(android)]
1384    fn push_ime_context(&self, id: WidgetId) {
1385        let value = self
1386            .tree
1387            .as_ref()
1388            .and_then(|tree| find_widget(tree.as_ref(), id))
1389            .and_then(|widget| widget.text_value().map(|s| s.to_string()));
1390        if let Some(text) = value {
1391            let edit = self.runtime.edits.get(&id).copied().unwrap_or_default();
1392            crate::android_ime::set_editor_state(&text, edit.cursor, edit.selection_range());
1393            // And the keyboard is **told**, as an Android editor tells it on every change:
1394            // a keyboard that predicts keeps its own model of the field, and one never
1395            // told of a change drifts from it (milestone 510). In UTF-16 units.
1396            let len = text.chars().count();
1397            let at = |i: usize| crate::ime::utf16_index(&text, i.min(len)) as i32;
1398            let (start, end) = edit.selection_range().unwrap_or((edit.cursor, edit.cursor));
1399            let (cand_start, cand_end) = edit
1400                .composing
1401                .map(|(s, e)| (at(s), at(e)))
1402                .unwrap_or((-1, -1));
1403            crate::android_ime::update_selection(at(start), at(end), cand_start, cand_end);
1404        }
1405    }
1406
1407    /// Ends the focused field's composition where it stands: the keyboard is being
1408    /// started again, and a keyboard started again has forgotten it.
1409    #[cfg(android)]
1410    fn end_composition(&mut self) {
1411        if let Some(id) = self.runtime.input.focused {
1412            if let Some(edit) = self.runtime.edits.get_mut(&id) {
1413                edit.composing = None;
1414            }
1415        }
1416    }
1417
1418    /// Remembers the Android activity's handle, the source of the system insets.
1419    #[cfg(android)]
1420    pub(crate) fn set_android_app(
1421        &mut self,
1422        android_app: winit::platform::android::activity::AndroidApp,
1423    ) {
1424        self.android_app = Some(android_app);
1425    }
1426
1427    /// The system insets — the safe area — in **logical** px. On Android they are
1428    /// derived from the activity's content rect, outside the system bars; zero
1429    /// elsewhere.
1430    ///
1431    /// The content rect is what the system leaves the activity, so it excludes
1432    /// **any** decoration the theme asks for — a title bar included. An app whose
1433    /// manifest keeps the default theme therefore reports a top inset of the status
1434    /// bar *plus* 56dp of action bar that is never drawn, and the shell dutifully
1435    /// pads it away: a wide empty band above the app bar. The manifests here ask for
1436    /// `Theme.DeviceDefault.NoActionBar`, and so must any frus app.
1437    fn compute_insets(&self, phys_w: u32, phys_h: u32, scale: f32) -> Insets {
1438        #[cfg(android)]
1439        if let Some(app) = &self.android_app {
1440            let r = app.content_rect();
1441            // A degenerate rect, before the first layout, means no inset.
1442            if r.right > r.left && r.bottom > r.top {
1443                let left = r.left.max(0) as f32;
1444                let top = r.top.max(0) as f32;
1445                let right = (phys_w as i32 - r.right).max(0) as f32;
1446                let bottom = (phys_h as i32 - r.bottom).max(0) as f32;
1447                return Insets::new(top / scale, right / scale, bottom / scale, left / scale);
1448            }
1449        }
1450        let _ = (phys_w, phys_h, scale);
1451        Insets::ZERO
1452    }
1453
1454    /// Asks the platform what it says about its user, and remembers the answer.
1455    ///
1456    /// Marks the build dirty when something moved: the font size reaches the layout
1457    /// through [`MediaQuery::scope`], so a changed scaler is a changed geometry for every
1458    /// widget on the screen, and a frame drawn from the cache would keep the old one.
1459    fn refresh_platform_settings(&mut self) {
1460        #[cfg(android)]
1461        if let Some(app) = &self.android_app {
1462            let read = crate::android_settings::read(app);
1463            if read != self.platform {
1464                self.platform = read;
1465                self.build_dirty = true;
1466            }
1467            return;
1468        }
1469        // Desktop and web report the one thing their window system knows. A text scaler
1470        // is **not** among it: winit exposes no equivalent of Windows'
1471        // `UISettings.TextScaleFactor` or of GNOME's `text-scaling-factor`, so it stays
1472        // at 1 there and an application that knows better says so itself. That is a
1473        // missing wire and is written down rather than papered over.
1474        let brightness = self
1475            .window
1476            .as_ref()
1477            .and_then(|window| window.theme())
1478            .map(|theme| match theme {
1479                winit::window::Theme::Dark => frus_widgets::Brightness::Dark,
1480                winit::window::Theme::Light => frus_widgets::Brightness::Light,
1481            })
1482            .unwrap_or_default();
1483        if brightness != self.platform.brightness {
1484            self.platform.brightness = brightness;
1485            self.build_dirty = true;
1486        }
1487
1488        // **The reader's languages**, read once. Changing the display language on a
1489        // desktop means signing out and back in on Windows, and is a per-process
1490        // environment variable on Linux — so there is nothing to watch for, and re-reading
1491        // it every time the window comes back would allocate for an answer that cannot
1492        // have moved. Android is the platform where it *does* change under a running
1493        // application, and Android reads it on every walk.
1494        if self.platform.locales.is_empty() {
1495            let read = platform_locales();
1496            if !read.is_empty() {
1497                self.platform.locales = read;
1498                self.build_dirty = true;
1499            }
1500        }
1501    }
1502}
1503
1504/// **The reader's preferred languages**, best first, from whatever the platform keeps
1505/// them in. Empty when it says nothing, or when there is no way to ask.
1506///
1507/// Android does not come through here: its answer lives on the activity's
1508/// `Configuration`, which [`crate::android_settings`] already walks.
1509/// On **Android** there is nothing to ask here: the answer lives on the activity's
1510/// `Configuration`, and the branch above returns before reaching this. A build with no
1511/// activity at all has no reader to ask about.
1512#[cfg(android)]
1513fn platform_locales() -> Vec<frus_widgets::Locale> {
1514    Vec::new()
1515}
1516
1517#[cfg(not(android))]
1518fn platform_locales() -> Vec<frus_widgets::Locale> {
1519    #[cfg(not(web))]
1520    {
1521        sys_locale::get_locales()
1522            .filter_map(|tag| frus_widgets::Locale::parse(&tag))
1523            .collect()
1524    }
1525    #[cfg(web)]
1526    {
1527        // `navigator.languages` is the ordered list; `navigator.language` is its first
1528        // entry and the fallback for a browser that does not offer the list.
1529        let Some(navigator) = web_sys::window().map(|window| window.navigator()) else {
1530            return Vec::new();
1531        };
1532        let listed: Vec<frus_widgets::Locale> = navigator
1533            .languages()
1534            .iter()
1535            .filter_map(|value| value.as_string())
1536            .filter_map(|tag| frus_widgets::Locale::parse(&tag))
1537            .collect();
1538        if listed.is_empty() {
1539            return navigator
1540                .language()
1541                .and_then(|tag| frus_widgets::Locale::parse(&tag))
1542                .into_iter()
1543                .collect();
1544        }
1545        listed
1546    }
1547}
1548
1549impl<A: Application> ApplicationHandler<A::Message> for App<A> {
1550    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
1551        if self.window.is_some() {
1552            return;
1553        }
1554        // Back in the foreground, or starting up: the surface is (re)born below.
1555        self.set_lifecycle(Lifecycle::Resumed);
1556        // And so is what the platform says about its user — a trip to the system
1557        // settings and back is exactly this event.
1558        self.refresh_platform_settings();
1559
1560        let mut attributes = Window::default_attributes()
1561            .with_title(self.app.title())
1562            // A sensible minimum size, in logical px, to avoid an absurd UI.
1563            .with_min_inner_size(winit::dpi::LogicalSize::new(360.0, 280.0));
1564        // The AccessKit adapter must be created **before** the window is shown, so we
1565        // create it hidden and reveal it afterwards. Desktop only.
1566        #[cfg(desktop)]
1567        {
1568            attributes = attributes.with_visible(false);
1569        }
1570        if let Some((w, h)) = self.app.window_size() {
1571            attributes = attributes.with_inner_size(winit::dpi::LogicalSize::new(w, h));
1572        }
1573        // Web: winit creates a `<canvas>` and **appends it** to the document's body.
1574        #[cfg(web)]
1575        {
1576            use winit::platform::web::WindowAttributesExtWebSys;
1577            attributes = attributes.with_append(true);
1578        }
1579        let window = match event_loop.create_window(attributes) {
1580            Ok(window) => Arc::new(window),
1581            Err(err) => {
1582                log::error!("failed to create the window: {err}");
1583                event_loop.exit();
1584                return;
1585            }
1586        };
1587
1588        // Web: GPU init is **asynchronous**, blocking being impossible. We start the
1589        // future, and the ready renderer is picked up on the first frame — see
1590        // `RedrawRequested`. `init` runs right away, since it does not touch the GPU.
1591        #[cfg(web)]
1592        {
1593            self.scale = window.scale_factor() as f32;
1594            self.window = Some(window.clone());
1595            // The accessibility bridge (#18): the canvas exists as soon as the window
1596            // does, so there is no "still hidden" step to wait for here.
1597            self.a11y = crate::a11y_web::A11y::new(&window);
1598            self.build_dirty = true;
1599            if !self.started {
1600                self.started = true;
1601                let command = self.app.init();
1602                self.run_command(command);
1603                self.sync_subscriptions();
1604                self.open_launch_address(&window);
1605            }
1606            let slot = self.pending_renderer.clone();
1607            let win = window.clone();
1608            let size = window.inner_size();
1609            wasm_bindgen_futures::spawn_local(async move {
1610                match Renderer::new(win.clone(), size.width.max(1), size.height.max(1)).await {
1611                    Ok(r) => {
1612                        *slot.borrow_mut() = Some(r);
1613                        win.request_redraw();
1614                    }
1615                    Err(err) => log::error!("failed to initialise the renderer (Web): {err:#}"),
1616                }
1617            });
1618            return;
1619        }
1620
1621        #[cfg(not(web))]
1622        {
1623            let size = window.inner_size();
1624            let renderer = pollster::block_on(Renderer::new(
1625                window.clone(),
1626                size.width.max(1),
1627                size.height.max(1),
1628            ));
1629
1630            match renderer {
1631                Ok(renderer) => {
1632                    self.scale = window.scale_factor() as f32;
1633                    // The accessibility bridge (AccessKit), created while the window is
1634                    // still hidden, after which we reveal it. Inert with no screen reader.
1635                    #[cfg(desktop)]
1636                    {
1637                        self.a11y = Some(crate::a11y::A11y::new(event_loop, &window));
1638                        window.set_visible(true);
1639                        // A second process's link brings this window forward and asks it for
1640                        // a frame.
1641                        crate::instance::set_window(window.clone());
1642                    }
1643                    self.window = Some(window.clone());
1644                    // What the input bridge's wakes ask a frame of (milestone 513).
1645                    #[cfg(android)]
1646                    crate::android_ime::set_window(Some(window.clone()));
1647                    self.renderer = Some(renderer);
1648                    // The surface was (re)created: force a full rebuild on the first frame.
1649                    self.build_dirty = true;
1650                    // The startup effect, an initial load and so on: once only, not on
1651                    // every surface recreation, such as returning from the background.
1652                    if !self.started {
1653                        self.started = true;
1654                        let command = self.app.init();
1655                        self.run_command(command);
1656                        self.sync_subscriptions();
1657                        #[cfg(any(android, desktop))]
1658                        self.open_launch_link();
1659                    }
1660                    window.request_redraw();
1661                }
1662                Err(err) => {
1663                    log::error!("failed to initialise the renderer: {err:#}");
1664                    event_loop.exit();
1665                }
1666            }
1667        }
1668    }
1669
1670    /// Going to the background, on Android: the native surface is destroyed. We
1671    /// release the renderer and the window, and `resumed` recreates them on the way
1672    /// back, without replaying `init` — see `started`. Harmless on desktop, where the
1673    /// event never fires.
1674    fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
1675        // Background: the surface is lost, so the app moves to `Paused`.
1676        self.set_lifecycle(Lifecycle::Paused);
1677        self.renderer = None;
1678        self.window = None;
1679        #[cfg(android)]
1680        crate::android_ime::set_window(None);
1681        self.last_frame = None;
1682    }
1683
1684    /// The event loop is ending, on close: a final `Detached` notification, where the
1685    /// app can persist its state before the process disappears.
1686    fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
1687        self.set_lifecycle(Lifecycle::Detached);
1688    }
1689
1690    /// A message produced by an effect, on a background thread: we apply it and ask
1691    /// for another frame.
1692    fn user_event(&mut self, _event_loop: &ActiveEventLoop, message: A::Message) {
1693        self.dispatch(message);
1694        self.request_redraw();
1695    }
1696
1697    /// The loop woke up: if the long-press deadline has been reached the recogniser
1698    /// **accepts eagerly** — the message is emitted and the release to come will be
1699    /// swallowed, the long press having evicted the tap.
1700    fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) {
1701        if matches!(cause, StartCause::ResumeTimeReached { .. }) && self.press.poll(Instant::now())
1702        {
1703            self.hold_deadline_reached();
1704        }
1705
1706        // The caret's turn is due: a frame to show it, or to hide it, in (milestone 513).
1707        if matches!(cause, StartCause::ResumeTimeReached { .. })
1708            && self.lifecycle == Lifecycle::Resumed
1709            && self.caret.next_toggle(Instant::now()).is_some()
1710        {
1711            self.request_redraw();
1712        }
1713
1714        // Pending IME operations, from the Android input bridge.
1715        #[cfg(android)]
1716        self.drain_ime();
1717        // And the values an autofill service chose, from the same bridge.
1718        #[cfg(android)]
1719        self.drain_autofill();
1720
1721        // Actions an assistive technology asked for, through AccessKit.
1722        #[cfg(desktop)]
1723        self.drain_a11y_actions();
1724        // Actions a screen reader asked for, through the DOM bridge (#18).
1725        #[cfg(web)]
1726        self.drain_a11y_actions();
1727
1728        // Live reload, in development: the binary was replaced by a recompilation, so
1729        // capture the state and relaunch the new binary. This does not return.
1730        if let Some(watcher) = self.reload.as_mut() {
1731            if watcher.binary_changed() {
1732                watcher.handoff(self.app.save_state());
1733            }
1734        }
1735        event_loop.set_control_flow(self.idle_control_flow());
1736    }
1737
1738    fn window_event(
1739        &mut self,
1740        event_loop: &ActiveEventLoop,
1741        _window_id: WindowId,
1742        event: WindowEvent,
1743    ) {
1744        // The AccessKit adapter observes the window events: focus, size and so on.
1745        #[cfg(desktop)]
1746        if let (Some(a11y), Some(window)) = (self.a11y.as_mut(), self.window.as_ref()) {
1747            a11y.process_event(window, &event);
1748        }
1749
1750        match event {
1751            WindowEvent::CloseRequested => event_loop.exit(),
1752
1753            // Gaining or losing focus **in the foreground**: `Resumed` ⇄ `Inactive`. We
1754            // leave `Paused`/`Detached` — background and closing — alone; those are
1755            // decided by `suspended` and `exiting`.
1756            WindowEvent::Focused(focused) => {
1757                if !matches!(self.lifecycle, Lifecycle::Paused | Lifecycle::Detached) {
1758                    self.set_lifecycle(if focused {
1759                        Lifecycle::Resumed
1760                    } else {
1761                        Lifecycle::Inactive
1762                    });
1763                }
1764            }
1765
1766            WindowEvent::Resized(size) => {
1767                if let Some(renderer) = self.renderer.as_mut() {
1768                    renderer.resize(size.width, size.height);
1769                }
1770                if let Some(window) = &self.window {
1771                    window.request_redraw();
1772                }
1773            }
1774
1775            WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
1776                self.scale = scale_factor as f32;
1777                // Reconfigure the surface to the current physical size.
1778                if let Some(window) = &self.window {
1779                    let size = window.inner_size();
1780                    if let Some(renderer) = self.renderer.as_mut() {
1781                        renderer.resize(size.width, size.height);
1782                    }
1783                }
1784                self.request_redraw();
1785            }
1786
1787            WindowEvent::Occluded(occluded) => {
1788                self.occluded = occluded;
1789                if !occluded {
1790                    self.request_redraw();
1791                }
1792            }
1793
1794            // Every pointer source, mouse and touch alike, converges on the
1795            // **normalised** `pointer()` input — gesture tier 0, with an explicit
1796            // `Cancel`. winit hands us physical px; we work in logical ones, the total
1797            // scale being DPI × density.
1798            WindowEvent::CursorMoved { position, .. } => {
1799                let scale = self.total_scale();
1800                let position = Point::new(position.x as f32 / scale, position.y as f32 / scale);
1801                self.pointer(
1802                    event_loop,
1803                    PointerEvent {
1804                        kind: PointerKind::Move,
1805                        position,
1806                        touch: false,
1807                    },
1808                );
1809            }
1810
1811            // The mouse left the window: nothing in it is under the pointer any more.
1812            WindowEvent::CursorLeft { .. } => {
1813                self.hover.left();
1814                self.sync_hover();
1815            }
1816
1817            WindowEvent::Touch(touch) => {
1818                let scale = self.total_scale();
1819                let position = Point::new(
1820                    touch.location.x as f32 / scale,
1821                    touch.location.y as f32 / scale,
1822                );
1823                let kind = match touch.phase {
1824                    TouchPhase::Started => PointerKind::Down,
1825                    TouchPhase::Moved => PointerKind::Move,
1826                    TouchPhase::Ended => PointerKind::Up,
1827                    TouchPhase::Cancelled => PointerKind::Cancel,
1828                };
1829                self.pointer(
1830                    event_loop,
1831                    PointerEvent {
1832                        kind,
1833                        position,
1834                        touch: true,
1835                    },
1836                );
1837            }
1838
1839            WindowEvent::ModifiersChanged(modifiers) => {
1840                let state = modifiers.state();
1841                self.shift = state.shift_key();
1842                self.ctrl = state.control_key();
1843                self.alt = state.alt_key();
1844                self.meta = state.super_key();
1845            }
1846
1847            WindowEvent::MouseInput {
1848                state: ElementState::Pressed,
1849                button: MouseButton::Left,
1850                ..
1851            } => self.pointer(
1852                event_loop,
1853                PointerEvent {
1854                    kind: PointerKind::Down,
1855                    position: self.cursor,
1856                    touch: false,
1857                },
1858            ),
1859
1860            WindowEvent::MouseInput {
1861                state: ElementState::Released,
1862                button: MouseButton::Left,
1863                ..
1864            } => self.pointer(
1865                event_loop,
1866                PointerEvent {
1867                    kind: PointerKind::Up,
1868                    position: self.cursor,
1869                    touch: false,
1870                },
1871            ),
1872
1873            WindowEvent::KeyboardInput { event, .. } if event.state == ElementState::Pressed => {
1874                // A keyboard interaction: the focus ring becomes visible again.
1875                if !self.runtime.focus_visible {
1876                    self.runtime.focus_visible = true;
1877                    self.request_redraw();
1878                }
1879
1880                // The **system** back — Android's button or gesture, the browser's back
1881                // key: it closes the topmost overlay, else pops a screen, else quits.
1882                if matches!(
1883                    event.logical_key,
1884                    WinitKey::Named(NamedKey::BrowserBack) | WinitKey::Named(NamedKey::GoBack)
1885                ) {
1886                    if !event.repeat {
1887                        self.system_back(event_loop);
1888                    }
1889                    return;
1890                }
1891
1892                // F12 toggles the **inspector**: outlines, a card for the hovered
1893                // widget, and a tree dump on stderr. Debug builds only.
1894                if matches!(event.logical_key, WinitKey::Named(NamedKey::F12)) {
1895                    if cfg!(debug_assertions) && !event.repeat {
1896                        self.inspector = !self.inspector;
1897                        self.inspector_dump = self.inspector;
1898                        self.request_redraw();
1899                    }
1900                    return;
1901                }
1902
1903                // **Shortcuts.** Placed after the system keys the shell owns outright
1904                // (back, F12) and before everything an application can bind, so that a
1905                // binding cannot take the inspector or the back gesture away.
1906                //
1907                // A stroke with no Ctrl, Alt or Meta goes to a focused field first: a
1908                // binding on a bare letter would otherwise make every field under it
1909                // impossible to type in.
1910                if let Some(stroke) = self.keystroke(&event) {
1911                    let typing = !stroke.is_command() && self.runtime.input.focused.is_some();
1912                    if !typing {
1913                        let msgs = self
1914                            .ui
1915                            .as_ref()
1916                            .map(|ui| ui.keystroke(stroke, self.runtime.input.focused))
1917                            .unwrap_or_default();
1918                        if !msgs.is_empty() {
1919                            for msg in msgs {
1920                                self.dispatch(msg);
1921                            }
1922                            self.request_redraw();
1923                            return;
1924                        }
1925                    }
1926                }
1927
1928                // Tab and Shift+Tab move between focusables, even with nothing focused.
1929                if matches!(event.logical_key, WinitKey::Named(NamedKey::Tab)) {
1930                    let forward = !self.shift;
1931                    let next = self
1932                        .ui
1933                        .as_ref()
1934                        .and_then(|ui| ui.focus_next(self.runtime.input.focused, forward));
1935                    if next.is_some() {
1936                        self.runtime.input.focused = next;
1937                        self.reveal_focus();
1938                        self.request_redraw();
1939                    }
1940                    return;
1941                }
1942
1943                // Escape walks **leaf to root** from the focused widget — an `OverlayPortal`
1944                // consumes it to close itself — and failing that closes the topmost
1945                // overlay, so Escape works with nothing focused.
1946                if matches!(event.logical_key, WinitKey::Named(NamedKey::Escape)) {
1947                    // Auto-repeat does not trigger another close.
1948                    if !event.repeat {
1949                        self.escape();
1950                    }
1951                    return;
1952                }
1953
1954                let Some(focused) = self.runtime.input.focused else {
1955                    return;
1956                };
1957
1958                // Arrows navigate focus **geometrically** — except left and right in a
1959                // text field, where they move the caret; up and down navigate even out
1960                // of a single-line field.
1961                let arrow = match event.logical_key {
1962                    WinitKey::Named(NamedKey::ArrowUp) => Some(FocusDirection::Up),
1963                    WinitKey::Named(NamedKey::ArrowDown) => Some(FocusDirection::Down),
1964                    WinitKey::Named(NamedKey::ArrowLeft) => Some(FocusDirection::Left),
1965                    WinitKey::Named(NamedKey::ArrowRight) => Some(FocusDirection::Right),
1966                    _ => None,
1967                };
1968                // PgUp and PgDn jump a **page** inside a multi-line field, bounded to
1969                // the field so they never leave it. No effect anywhere else.
1970                if matches!(
1971                    event.logical_key,
1972                    WinitKey::Named(NamedKey::PageUp) | WinitKey::Named(NamedKey::PageDown)
1973                ) {
1974                    let down = matches!(event.logical_key, WinitKey::Named(NamedKey::PageDown));
1975                    if self.move_caret_vertical(focused, down, true) {
1976                        return;
1977                    }
1978                }
1979
1980                if let Some(direction) = arrow {
1981                    // In a **multi-line** field, Up and Down move the caret between
1982                    // lines while it stays in the field; on the first or last line we
1983                    // fall back to focus navigation and leave the field.
1984                    if matches!(direction, FocusDirection::Up | FocusDirection::Down) {
1985                        let down = matches!(direction, FocusDirection::Down);
1986                        if self.move_caret_vertical(focused, down, false) {
1987                            return;
1988                        }
1989                    }
1990
1991                    // Left and right arrows are offered to the focused widget first —
1992                    // a range slider, say — through `on_key`. If it consumes them, focus
1993                    // does not move.
1994                    if matches!(direction, FocusDirection::Left | FocusDirection::Right) {
1995                        let key = if matches!(direction, FocusDirection::Left) {
1996                            Key::Left {
1997                                shift: self.shift,
1998                                word: self.ctrl,
1999                            }
2000                        } else {
2001                            Key::Right {
2002                                shift: self.shift,
2003                                word: self.ctrl,
2004                            }
2005                        };
2006                        let widget = self
2007                            .tree
2008                            .as_ref()
2009                            .and_then(|tree| find_widget(tree.as_ref(), focused));
2010                        // A reorderable header moves its column by one; we capture its
2011                        // position to announce it, the keyboard being how screen-reader
2012                        // users reorder.
2013                        let reorder_from = widget.and_then(|w| w.reorder_index());
2014                        let handled = widget.map(|w| w.on_key(&key));
2015                        if let Some(KeyResponse::Handled(message)) = handled {
2016                            if let Some(message) = message {
2017                                self.dispatch(message);
2018                            }
2019                            if let Some(from) = reorder_from {
2020                                let to = if matches!(direction, FocusDirection::Left) {
2021                                    from.wrapping_sub(1)
2022                                } else {
2023                                    from + 1
2024                                };
2025                                self.set_announcement(format!(
2026                                    "Column moved to position {}",
2027                                    to + 1
2028                                ));
2029                            }
2030                            self.request_redraw();
2031                            return;
2032                        }
2033                    }
2034
2035                    let is_text = self
2036                        .tree
2037                        .as_ref()
2038                        .and_then(|tree| find_widget(tree.as_ref(), focused))
2039                        .and_then(|widget| widget.cursor_at(0.0, 0.0, 1.0, 0))
2040                        .is_some();
2041                    let navigates =
2042                        !is_text || matches!(direction, FocusDirection::Up | FocusDirection::Down);
2043                    if navigates {
2044                        if let Some(next) = self
2045                            .ui
2046                            .as_ref()
2047                            .and_then(|ui| ui.focus_directional(focused, direction))
2048                        {
2049                            self.runtime.input.focused = Some(next);
2050                            self.reveal_focus();
2051                            self.request_redraw();
2052                        }
2053                        return;
2054                    }
2055                }
2056
2057                // Home and End are offered to the focused widget — a range slider maps
2058                // them to min and max — before the default action. A text field ignores
2059                // them here (`on_key` returns Ignored) and falls back to ordinary
2060                // editing further down.
2061                if matches!(
2062                    event.logical_key,
2063                    WinitKey::Named(NamedKey::Home) | WinitKey::Named(NamedKey::End)
2064                ) {
2065                    let key = if matches!(event.logical_key, WinitKey::Named(NamedKey::Home)) {
2066                        Key::Home {
2067                            shift: self.shift,
2068                            doc: self.ctrl,
2069                        }
2070                    } else {
2071                        Key::End {
2072                            shift: self.shift,
2073                            doc: self.ctrl,
2074                        }
2075                    };
2076                    let handled = self
2077                        .tree
2078                        .as_ref()
2079                        .and_then(|tree| find_widget(tree.as_ref(), focused))
2080                        .map(|widget| widget.on_key(&key));
2081                    if let Some(KeyResponse::Handled(message)) = handled {
2082                        if let Some(message) = message {
2083                            self.dispatch(message);
2084                        }
2085                        self.request_redraw();
2086                        return;
2087                    }
2088                }
2089
2090                // Keyboard activation, Enter or Space, of a clickable focusable: a
2091                // button, a checkbox, a switch. Text fields, which have no `on_click`,
2092                // fall back to ordinary editing — Enter submits, Space is a space.
2093                if matches!(
2094                    event.logical_key,
2095                    WinitKey::Named(NamedKey::Enter) | WinitKey::Named(NamedKey::Space)
2096                ) {
2097                    let widget = self
2098                        .tree
2099                        .as_ref()
2100                        .and_then(|tree| find_widget(tree.as_ref(), focused))
2101                        .filter(|widget| widget.focusable());
2102                    let message = widget.and_then(|widget| widget.on_click());
2103                    if let Some(message) = message {
2104                        // Auto-repeat does not machine-gun the activation: holding
2105                        // Space on a button is one single click.
2106                        if !event.repeat {
2107                            // The spoken announcement of the effect — a sort, a
2108                            // selection — captured before `dispatch` rebuilds the tree.
2109                            let announce = widget.and_then(|widget| widget.announce());
2110                            self.dispatch(message);
2111                            if let Some(announce) = announce {
2112                                self.set_announcement(announce);
2113                            }
2114                            self.request_redraw();
2115                        }
2116                        return;
2117                    }
2118                }
2119
2120                // The clipboard: Ctrl+C/X/V, and a keyboard's own Copy, Cut and Paste.
2121                if let Some(command) =
2122                    clipboard_command(&event.logical_key, event.physical_key, self.ctrl)
2123                {
2124                    match command {
2125                        ClipCommand::Copy => self.copy_selection(focused),
2126                        ClipCommand::Cut => {
2127                            self.copy_selection(focused);
2128                            self.apply_key(focused, Key::Backspace);
2129                            self.request_redraw();
2130                        }
2131                        ClipCommand::Paste => {
2132                            // Answered now, or — on the Web — on a later frame.
2133                            if let Some(pasted) =
2134                                self.clipboard.paste(focused, self.window.as_ref())
2135                            {
2136                                self.land_paste(pasted);
2137                            }
2138                        }
2139                    }
2140                    return;
2141                }
2142
2143                // The other editing shortcuts, Ctrl+A/Z/Y.
2144                if self.ctrl {
2145                    match &event.logical_key {
2146                        WinitKey::Character(c) if c.eq_ignore_ascii_case("a") => {
2147                            self.runtime.edits.insert(
2148                                focused,
2149                                Edit {
2150                                    cursor: usize::MAX,
2151                                    anchor: Some(0),
2152                                    composing: None,
2153                                },
2154                            );
2155                            // A selection is a caret move: what is typed over it is a step
2156                            // of its own, not more of whatever was being typed before.
2157                            self.runtime.close_edit_run(focused);
2158                            self.request_redraw();
2159                            return;
2160                        }
2161                        // Undo, and redo under both its spellings — Ctrl+Y on Windows,
2162                        // Ctrl+Shift+Z everywhere else, and people bring the one their
2163                        // hands already know.
2164                        WinitKey::Character(c) if c.eq_ignore_ascii_case("z") && !self.shift => {
2165                            self.step_history(focused, false);
2166                            self.request_redraw();
2167                            return;
2168                        }
2169                        WinitKey::Character(c)
2170                            if c.eq_ignore_ascii_case("y")
2171                                || (c.eq_ignore_ascii_case("z") && self.shift) =>
2172                        {
2173                            self.step_history(focused, true);
2174                            self.request_redraw();
2175                            return;
2176                        }
2177                        _ => {}
2178                    }
2179                }
2180
2181                // With the input bridge active, editing goes EXCLUSIVELY through the
2182                // InputConnection, that is, the IME queue — the bridge view receives the
2183                // hardware keys too. Without this guard every keystroke would arrive
2184                // twice, once from winit's native queue and once from the bridge.
2185                #[cfg(android)]
2186                if crate::android_ime::installed() {
2187                    return;
2188                }
2189
2190                let shift = self.shift;
2191                let key = match &event.logical_key {
2192                    WinitKey::Named(NamedKey::Backspace) => Some(Key::Backspace),
2193                    WinitKey::Named(NamedKey::Delete) => Some(Key::Delete),
2194                    // Repeating Enter does not submit again; text and deletion do
2195                    // repeat normally.
2196                    WinitKey::Named(NamedKey::Enter) if !event.repeat => Some(Key::Enter),
2197                    WinitKey::Named(NamedKey::Enter) => None,
2198                    // With Ctrl, Left/Right jump a word and Home/End bound the field.
2199                    WinitKey::Named(NamedKey::ArrowLeft) => Some(Key::Left {
2200                        shift,
2201                        word: self.ctrl,
2202                    }),
2203                    WinitKey::Named(NamedKey::ArrowRight) => Some(Key::Right {
2204                        shift,
2205                        word: self.ctrl,
2206                    }),
2207                    WinitKey::Named(NamedKey::Home) => Some(Key::Home {
2208                        shift,
2209                        doc: self.ctrl,
2210                    }),
2211                    WinitKey::Named(NamedKey::End) => Some(Key::End {
2212                        shift,
2213                        doc: self.ctrl,
2214                    }),
2215                    WinitKey::Named(NamedKey::Space) => Some(Key::Text(" ".to_string())),
2216                    // Android delivers Enter as `Character("\n")`, through the
2217                    // KeyCharacterMap, rather than `Named(Enter)`: the same submission,
2218                    // without inserting a line break into the field.
2219                    WinitKey::Character(c) if c == "\n" || c == "\r" => {
2220                        if event.repeat {
2221                            None
2222                        } else {
2223                            Some(Key::Enter)
2224                        }
2225                    }
2226                    _ => event.text.as_ref().map(|text| Key::Text(text.to_string())),
2227                };
2228
2229                if let Some(key) = key {
2230                    self.apply_key(focused, key);
2231                    self.request_redraw();
2232                }
2233            }
2234
2235            WindowEvent::MouseWheel { delta, .. } => {
2236                let (mut dx, mut dy) = match delta {
2237                    MouseScrollDelta::LineDelta(x, y) => (x * SCROLL_SPEED, y * SCROLL_SPEED),
2238                    // Physical delta → logical.
2239                    MouseScrollDelta::PixelDelta(pos) => {
2240                        let scale = self.total_scale();
2241                        (pos.x as f32 / scale, pos.y as f32 / scale)
2242                    }
2243                };
2244                // With Shift, the wheel scrolls horizontally.
2245                if self.shift {
2246                    dx = dy;
2247                    dy = 0.0;
2248                }
2249                // Over an interactive viewport the wheel **zooms**, anchored at the
2250                // pointer and bounded by the widget's min and max scales.
2251                if let Some((id, viewport)) = self
2252                    .ui
2253                    .as_ref()
2254                    .and_then(|ui| ui.interactive_at(self.cursor))
2255                {
2256                    let (min, max) = self
2257                        .tree
2258                        .as_ref()
2259                        .and_then(|tree| find_widget(tree.as_ref(), id))
2260                        .and_then(|w| w.interactive())
2261                        .unwrap_or((0.5, 4.0));
2262                    // Wheel up (`dy > 0`) zooms in, in gentle steps of about 1.1× a notch.
2263                    let factor = (1.0 + dy * 0.1 / SCROLL_SPEED).clamp(0.2, 5.0);
2264                    // Zooming cuts off a fling in progress.
2265                    self.runtime.interactive_velocity.remove(&id);
2266                    let view = self.runtime.interactive.entry(id).or_default();
2267                    // Zoom anchored at the pointer, then bounded to the frame.
2268                    *view = view
2269                        .zoom_at(factor, self.cursor, min, max)
2270                        .clamped(viewport);
2271                    self.request_redraw();
2272                    return;
2273                }
2274                // A wheel asks the same question a finger does, and the reference gates it
2275                // the same way: an area with everything already in sight takes no offset,
2276                // and the notch belongs to whatever is behind it.
2277                let hit = {
2278                    let scroll = &self.runtime.scroll;
2279                    self.ui.as_ref().and_then(|ui| {
2280                        ui.scroll_chain(self.cursor).find(|area| {
2281                            area.accepts_user_offset(
2282                                scroll.get(&area.id).copied().unwrap_or((0.0, 0.0)),
2283                            )
2284                        })
2285                    })
2286                };
2287                if let Some(area) = hit {
2288                    // Scrolling with inertia: the wheel pushes the TARGET and the
2289                    // spring eases across to it. How far past the ends that target
2290                    // may go is the physics' call — a little elastic overshoot where
2291                    // the platform bounces, none at all where it does not.
2292                    let id = area.id;
2293                    let physics = area.physics_or(self.app.scroll_physics());
2294                    let over = if physics.allows_overscroll() {
2295                        SCROLL_OVER
2296                    } else {
2297                        0.0
2298                    };
2299                    // A notch of the wheel is a new intent: it takes over from the
2300                    // momentum of the last gesture.
2301                    self.runtime.stop_scroll_fling(id);
2302                    let current = self.runtime.scroll.get(&id).copied().unwrap_or((0.0, 0.0));
2303                    // A screen delta into offsets: the content moves opposite the number,
2304                    // and a reversed axis counts from the other end. One function, so a
2305                    // reversed area is right at every one of the places this happens.
2306                    let (ddx, ddy) = area.offset_delta((dx, dy));
2307                    let target = self.runtime.scroll_target.entry(id).or_insert(current);
2308                    let (wanted_x, wanted_y) = (target.0 + ddx, target.1 + ddy);
2309                    target.0 = wanted_x.clamp(-over, area.max_x + over);
2310                    target.1 = wanted_y.clamp(-over, area.max_y + over);
2311                    let refused = (wanted_x - target.0, wanted_y - target.1);
2312                    self.runtime.scroll_velocity.entry(id).or_insert((0.0, 0.0));
2313                    // A notch that asks to go past the end deserves the same
2314                    // acknowledgement a finger gets: the wheel is a gesture too.
2315                    let cursor = self.cursor;
2316                    for (refused, vertical, extent) in [
2317                        (refused.0, false, area.viewport.width),
2318                        (refused.1, true, area.viewport.height),
2319                    ] {
2320                        if refused.abs() < 1e-3 {
2321                            continue;
2322                        }
2323                        let edge = area.refused_edge(vertical, refused);
2324                        let (offset, cross) =
2325                            frus_widgets::glow_cross_axis(area.viewport, edge, cursor);
2326                        self.runtime
2327                            .glow_pull(id, edge, refused, extent, offset, cross);
2328                        // A wheel has no "lift off", so the pull is released at once
2329                        // and simply fades — otherwise it would hang until the hold
2330                        // timer expired.
2331                        self.runtime.glow_scroll_end(id);
2332                    }
2333                    self.request_redraw();
2334                }
2335            }
2336
2337            WindowEvent::RedrawRequested => {
2338                // Web: pick up the asynchronously initialised renderer as soon as it
2339                // is ready; until then, nothing is painted.
2340                #[cfg(web)]
2341                if self.renderer.is_none() {
2342                    match self.pending_renderer.borrow_mut().take() {
2343                        Some(mut renderer) => {
2344                            // The renderer was sized from whatever `inner_size()` read
2345                            // back when it was spawned — often 0×0, before the canvas
2346                            // had been laid out by CSS. A `Resized` event carrying the
2347                            // real size may have arrived and been dropped in the
2348                            // meantime, since `self.renderer` was still `None` then.
2349                            // Catching up here, against the size right now, is what
2350                            // makes that race harmless either way.
2351                            if let Some(window) = &self.window {
2352                                let size = window.inner_size();
2353                                renderer.resize(size.width.max(1), size.height.max(1));
2354                            }
2355                            self.renderer = Some(renderer);
2356                        }
2357                        None => return,
2358                    }
2359                }
2360                // A paste the browser has answered since the last frame goes in before
2361                // this one is built. Elsewhere a paste is answered on the spot and this
2362                // finds nothing.
2363                while let Some(pasted) = self.clipboard.take_answered() {
2364                    self.land_paste(pasted);
2365                }
2366                // Occluded window: rendering is suspended, resuming on Occluded(false).
2367                if self.occluded {
2368                    return;
2369                }
2370                let size = self
2371                    .window
2372                    .as_ref()
2373                    .map(|w| w.inner_size())
2374                    .unwrap_or_default();
2375                // Minimised, or zero-sized: nothing to draw, which avoids GPU errors.
2376                if size.width == 0 || size.height == 0 {
2377                    self.last_frame = None; // no dt jump when it is restored
2378                    return;
2379                }
2380                // The interface is described in **logical** pixels; the GPU output is
2381                // scaled to physical ones (DPI × density) just before rendering.
2382                let scale = self.total_scale();
2383                let (width, height) = (size.width as f32 / scale, size.height as f32 / scale);
2384
2385                // A logical size change — a resize OR a density change — notifies the
2386                // app before the view, so it can react to the breakpoint in its logic.
2387                if self.last_size != Some((width, height)) {
2388                    self.last_size = Some((width, height));
2389                    self.build_dirty = true;
2390                    self.app.on_resize(width, height);
2391                }
2392
2393                // Window insets: separates the static **padding** — bars and notch —
2394                // from the **keyboard**, which is whatever bottom inset exceeds the
2395                // keyboard-free baseline. The baseline is taken at the first measurement
2396                // for this physical size (a rotation gives a new one), and corrects
2397                // downwards should a barer state appear: the keyboard open at startup,
2398                // the bars hidden, and so on.
2399                let raw = self.compute_insets(size.width, size.height, scale);
2400                let phys = (size.width, size.height);
2401                let mut baseline = match self.inset_baseline {
2402                    Some((b, s)) if s == phys => b,
2403                    _ => {
2404                        self.inset_baseline = Some((raw, phys));
2405                        raw
2406                    }
2407                };
2408                if raw.bottom < baseline.bottom {
2409                    baseline = raw;
2410                    self.inset_baseline = Some((raw, phys));
2411                }
2412                let insets = WindowInsets::from_baseline(baseline, raw);
2413                if self.last_insets != insets {
2414                    if insets.view_insets.bottom > self.last_insets.view_insets.bottom {
2415                        self.reveal_after_keyboard = true;
2416                    }
2417                    self.last_insets = insets;
2418                    self.build_dirty = true;
2419                    self.app.on_insets(insets);
2420                }
2421
2422                // The elapsed dt, clamped, for every animation.
2423                let now = Instant::now();
2424                let dt = self
2425                    .last_frame
2426                    .map(|prev| (now - prev).as_secs_f32().min(0.05))
2427                    .unwrap_or(0.0);
2428                self.last_frame = Some(now);
2429
2430                // A continuous clock, in seconds, for the time-driven animations.
2431                self.elapsed += dt;
2432                self.runtime.time = self.elapsed;
2433
2434                // The caret's blink (milestone 513), on the wall clock: the animation
2435                // clock above is clamped per frame and stands still between frames, and a
2436                // caret at rest is exactly when there are none. Solid while the window is
2437                // not the one in front, where nothing wakes the loop for it.
2438                let caret_now = Instant::now();
2439                let caret = if self.lifecycle == Lifecycle::Resumed {
2440                    self.caret_signature()
2441                } else {
2442                    None
2443                };
2444                self.caret.observe(caret_now, caret);
2445                self.runtime.caret_hidden = self.caret.hidden(caret_now);
2446
2447                // The application advances its own animations: navigation, gesture.
2448                let mut app_animating = self.app.tick(dt);
2449
2450                // And the framework advances the **theme**, which is its own work: it
2451                // resolves the application's themes against the brightness the platform
2452                // reports and the contrast the reader asked for, and crosses from the one
2453                // on screen to the one now wanted over `theme_animation_duration`.
2454                //
2455                // Both halves of that were the application's before milestone 452, and
2456                // both were being done by hand in this repo's own demonstration — which
2457                // is a fair sign of what every application was having to write.
2458                let settings = self
2459                    .platform
2460                    .accessibility
2461                    .with_overrides(self.app.accessibility());
2462
2463                // **The ambient scopes, before anything reads them** — which the theme
2464                // does: the direction of the layout follows the language, so an
2465                // application whose theme is right-to-left in Arabic needs the language
2466                // installed before its theme is asked for. This used to sit further down,
2467                // beside the build, where it was in time for the widgets and one frame
2468                // late for the theme.
2469                //
2470                // Read every frame, all of it: these are the application's answers and
2471                // they may change while it is running.
2472                install_ambient(&self.app, &mut self.runtime, &self.platform.locales);
2473
2474                let theme_moved = self.themes.advance(
2475                    &self.app,
2476                    self.platform.brightness,
2477                    settings.high_contrast,
2478                    settings.disable_animations,
2479                    dt,
2480                );
2481                // A theme that moved is a rebuild — the view is a pure function of
2482                // `(state, theme, size)` — and a fade still running is another frame.
2483                app_animating |= theme_moved | self.themes.animating();
2484                let theme = self.themes.displayed(&self.app);
2485
2486                // **The surface, in force for the whole frame** — not just for `view`.
2487                // A size becomes a number in three places: while the widgets are built,
2488                // while they are measured and laid out, and while they are painted.
2489                // Scoping the build alone left the last two at scale 1, so the layout
2490                // measured one size and the renderer drew another, and the reader's
2491                // setting reached a device without moving a single pixel (milestone 407).
2492                //
2493                // One guard, not two: the description and the font size are installed by
2494                // the same call and released by the same drop, so they cannot be held for
2495                // different lengths of time. Holding them separately is the same bug with
2496                // an extra step (milestone 408). It lives to the end of this frame and
2497                // puts back what was there, panic or not.
2498                let _surface = self.media_query(width, height).install();
2499
2500                // === BUILD phase, conditional ===
2501                // The `view` is rebuilt only when the app's state or the size changed
2502                // (`build_dirty`), or when the app is animating, theme, navigation and
2503                // gesture all altering the state the view reads. A frame that animates
2504                // interaction alone — hover, scroll, focus, caret — **reuses the
2505                // retained tree** and merely repaints: the `view` is a pure function of
2506                // `(state, theme, size)` and never of the `Runtime`.
2507                // The user's motion setting reaches the runtime before anything is
2508                // advanced with it, and it is read every frame: a settings screen with a
2509                // *reduce motion* switch changes it while the application is running.
2510                self.runtime.still = settings.disable_animations;
2511                // And where the pointer stands in relation to a bar, from the previous
2512                // frame's registry — the only one there is at this point in the frame.
2513                //
2514                // Near a bar, it holds it open and warms the thumb; a bar that has faded
2515                // out entirely still answers here, and comes back for the reach. On a
2516                // touch platform nothing draws a bar in the first place, so the registry
2517                // is empty and a lingering finger position says nothing.
2518                self.runtime.scrollbar_hovered = self
2519                    .ui
2520                    .as_ref()
2521                    .and_then(|ui| ui.scrollbar_near(self.cursor))
2522                    .map(|bar| bar.id);
2523                self.runtime.scrollbar_dragged = match self.drag {
2524                    Some(Drag::Scrollbar { id, .. }) => Some(id),
2525                    _ => None,
2526                };
2527                // A switcher in flight carries each child's progress as a number built into
2528                // the tree, so it is rebuilt — not only repainted — until the switch settles.
2529                // Something a component keeps changed — a `set_state`, a hook, a controller —
2530                // and what it builds is built again.
2531                // What components asked of the shell since the last frame — a focus, a scroll,
2532                // work for another thread — is run first: it may ask for this very rebuild.
2533                let asked = self.app.effects();
2534                self.run_command(asked);
2535                #[cfg(web)]
2536                self.sync_address();
2537                #[cfg(desktop)]
2538                self.open_links_from_other_instances();
2539                if frus_widgets::take_rebuild_request() {
2540                    self.build_dirty = true;
2541                }
2542                let need_build = frame_needs_build(
2543                    self.build_dirty,
2544                    app_animating,
2545                    std::mem::replace(&mut self.app_was_animating, app_animating),
2546                    self.tree.is_none() || self.runtime.switching(),
2547                );
2548                if need_build {
2549                    // No scope of its own: the surface above is already installed, and
2550                    // covers the layout and the paint that follow as well.
2551                    //
2552                    // `build_view`, not `view`: `collect_ids` below reads this tree before
2553                    // the layout pass has been down it, so an unprepared tree would report
2554                    // no identities at all inside a deferred subtree — and everything in an
2555                    // `AppBar` would silently never mount, never fade in and never fade out.
2556                    let tree = build_view(&self.app, &theme, &self.runtime);
2557                    let ids = collect_ids(tree.as_ref());
2558                    let present: std::collections::HashSet<_> = ids.iter().copied().collect();
2559
2560                    // Leaving: snapshot the widgets present at N-1 but absent at N,
2561                    // so they can be faded out.
2562                    let leaving: std::collections::HashSet<u64> = self
2563                        .runtime
2564                        .mounted
2565                        .iter()
2566                        .filter(|id| !present.contains(id))
2567                        .map(|id| id.as_u64())
2568                        .collect();
2569                    if !leaving.is_empty() {
2570                        if let Some(ui) = &self.ui {
2571                            let captured: Vec<_> = ui
2572                                .scene()
2573                                .primitives()
2574                                .iter()
2575                                .filter(|p| leaving.contains(&p.owner()))
2576                                .cloned()
2577                                .collect();
2578                            if !captured.is_empty() {
2579                                self.runtime
2580                                    .leaving
2581                                    .insert(self.leaving_counter, (captured, 1.0));
2582                                self.leaving_counter = self.leaving_counter.wrapping_add(1);
2583                            }
2584                        }
2585                    }
2586
2587                    // Mounting: new widgets start out fading in.
2588                    for &id in &ids {
2589                        if self.runtime.mounted.insert(id) {
2590                            self.runtime.anims.entry(id).or_default().opacity = 0.0;
2591                        }
2592                    }
2593                    self.runtime.mounted.retain(|id| present.contains(id));
2594
2595                    self.tree = Some(tree);
2596                    // What the build asked for may be a timer — a component's
2597                    // `use_interval` — and it is the build that says so, which is later than
2598                    // the message that changed it. Diffing here starts and stops it now, not
2599                    // on the next message.
2600                    self.sync_subscriptions();
2601
2602                    // The configuration may have changed, so invalidate the paint
2603                    // cache and its repaint boundaries. Entries from a stale generation
2604                    // no longer *hit*, giving a full repaint this frame and reuse on the
2605                    // interaction-only frames that follow.
2606                    self.runtime.paint_cache.borrow_mut().bump_generation();
2607                }
2608                self.build_dirty = false;
2609
2610                // Focus requests (`Command::focus`), resolved against the tree just
2611                // (re)built — the key names the field wrapped in `keyed(k, …)`. The most
2612                // recent one that resolves wins, and the focus ring becomes visible
2613                // again, since we jump to the field as the keyboard would.
2614                if !self.pending_focus.is_empty() {
2615                    let keys = std::mem::take(&mut self.pending_focus);
2616                    let ids: Vec<WidgetId> = self
2617                        .tree
2618                        .as_deref()
2619                        .map(|tree| keys.iter().filter_map(|&k| find_by_key(tree, k)).collect())
2620                        .unwrap_or_default();
2621                    if let Some(&id) = ids.last() {
2622                        self.runtime.input.focused = Some(id);
2623                        self.runtime.focus_visible = true;
2624                    }
2625                }
2626
2627                // === PAINT phase ===
2628                // The retained tree is (re)painted. Layout goes through the relayout
2629                // cache (milestone 55: taffy is called again only when the structure
2630                // changed); painting goes through the repaint cache (milestone 88: a
2631                // A row carried past the end of the list: the list comes to meet it.
2632                // Before the tree is borrowed for the rest of the frame, because it moves
2633                // an offset the build below is about to read — and this frame, not the
2634                // next one, or the content would lag a frame behind the finger.
2635                let autoscrolling = self.autoscroll_carried(dt);
2636
2637                // The reorder spring, per axis, with a time constant of about 70 ms:
2638                // - **horizontal** (`Table` columns): the smoothed `reorder_x` catches up
2639                //   with the pointer — the columns slide with inertia while the ghost
2640                //   sticks to the real pointer;
2641                // - **vertical** (Kanban cards): the smoothed `reorder_y` catches up with
2642                //   the **chosen** slot edge, the hovered half — the insertion line and
2643                //   the gap *slide* between cards instead of jumping, which is the
2644                //   vertical counterpart of `reorder_x`;
2645                // - **a horizontal list's rows**: `reorder_x` catches up with the chosen
2646                //   slot edge, the same line turned on its side.
2647                let reorder_animating = self.advance_reorder_springs(dt);
2648
2649                // static `RepaintBoundary` subtree is replayed without repainting while
2650                // its geometry and the interaction state hold still).
2651                let tree = self
2652                    .tree
2653                    .as_deref()
2654                    .expect("the view was built at least once");
2655
2656                // Scroll and pan inertia; the bounds and viewports come from the
2657                // previous frame.
2658                let scroll_regions = self
2659                    .ui
2660                    .as_ref()
2661                    .map(|ui| ui.scroll_regions().to_vec())
2662                    .unwrap_or_default();
2663                let scroll_physics = self.app.scroll_physics();
2664                // The refresh areas of the frame, with the `refreshing` flag each was
2665                // built with: that flag is what tells a spinning indicator when to stop.
2666                let refresh_areas = self
2667                    .ui
2668                    .as_ref()
2669                    .map(|ui| ui.refresh_areas().to_vec())
2670                    .unwrap_or_default();
2671                let dismissables = self
2672                    .ui
2673                    .as_ref()
2674                    .map(|ui| ui.dismissables().to_vec())
2675                    .unwrap_or_default();
2676                // Filled by the dismissal step below, dispatched once the tree is no
2677                // longer borrowed.
2678                let mut dismissed: Vec<A::Message> = Vec::new();
2679                let sheet_areas = self
2680                    .ui
2681                    .as_ref()
2682                    .map(|ui| ui.sheets().to_vec())
2683                    .unwrap_or_default();
2684                let interactive_bounds = self
2685                    .ui
2686                    .as_ref()
2687                    .map(|ui| ui.interactive_bounds())
2688                    .unwrap_or_default();
2689
2690                // A paged view that has been asked for another page glides across to
2691                // it. Done before the springs are stepped, so the request is honoured
2692                // in the frame it arrives rather than the one after.
2693                self.runtime.sync_pages(&scroll_regions);
2694                // And a bar whose selected item has moved out of its window slides back
2695                // to it. Same moment, same reason: the request is honoured in the frame
2696                // it arrives rather than the one after.
2697                self.runtime.sync_visible(&scroll_regions);
2698                // And the scroll requests an application has just returned. Same moment
2699                // and the same reason, with one addition: a region that has only just
2700                // appeared is not in the registry above, which was built last frame, so
2701                // what does not resolve here is tried again below against this frame's.
2702                let requests = std::mem::take(&mut self.pending_scroll);
2703                let (retry, scrolled) =
2704                    apply_scroll_requests(&mut self.runtime, tree, requests, &scroll_regions);
2705                self.retry_scroll = retry;
2706                // And the sheets it has asked to move, on the same terms: before the sheets
2707                // are stepped, so an animated request starts in the frame it arrives in.
2708                let requests = std::mem::take(&mut self.pending_sheet);
2709                let (retry, sheeted) =
2710                    apply_sheet_requests(&mut self.runtime, tree, requests, &sheet_areas);
2711                self.retry_sheet = retry;
2712
2713                let animating = scrolled
2714                    | sheeted
2715                    | self.runtime.advance(dt)
2716                    | self.runtime.advance_leaving(dt)
2717                    | self.runtime.advance_switchers(tree, dt)
2718                    | self.runtime.advance_values(tree, dt)
2719                    | self.runtime.advance_colors(tree, dt)
2720                    | self.runtime.advance_sizes(tree, dt)
2721                    | self.runtime.advance_radii(tree, dt)
2722                    | self.runtime.advance_paddings(tree, dt)
2723                    | self.runtime.advance_offsets(tree, dt)
2724                    | self.runtime.advance_pins(tree, dt)
2725                    | self.runtime.advance_fractions(tree, dt)
2726                    | self.runtime.advance_text_styles(tree, dt)
2727                    | self.runtime.advance_transforms(tree, dt)
2728                    | self
2729                        .runtime
2730                        .advance_scroll(&scroll_regions, scroll_physics, dt)
2731                    | self.runtime.advance_glow(dt)
2732                    | self.runtime.advance_refresh(&refresh_areas, dt)
2733                    | {
2734                        // A dismissed item announces itself only once its gap has
2735                        // finished closing. The messages are *collected* here and
2736                        // dispatched below: the retained tree is borrowed for the whole
2737                        // of this frame, and `dispatch` rebuilds it.
2738                        let (moving, done) = self.runtime.advance_dismiss(&dismissables, dt);
2739                        dismissed.extend(done.into_iter().filter_map(|(id, direction)| {
2740                            find_widget(tree, id).and_then(|widget| widget.on_dismissed(direction))
2741                        }));
2742                        moving
2743                    }
2744                    | {
2745                        // A sheet lowered to nothing says so once it has arrived there,
2746                        // collected like a dismissal and for the same reason.
2747                        let (moving, closed) = self.runtime.advance_sheets(&sheet_areas, dt);
2748                        dismissed.extend(closed.into_iter().filter_map(|id| {
2749                            find_widget(tree, id).and_then(|widget| widget.on_sheet_dismissed())
2750                        }));
2751                        // A throw that carried a sheet to full height goes on into its
2752                        // list, under the list's own physics (milestone 520).
2753                        let mut handed = false;
2754                        for (list, velocity) in self.runtime.take_sheet_handovers() {
2755                            if let Some(area) = scroll_regions.iter().find(|a| a.id == list) {
2756                                let physics = area.physics_or(scroll_physics);
2757                                handed |=
2758                                    self.runtime.fling_scroll(*area, physics, (0.0, velocity));
2759                            }
2760                        }
2761                        moving | handed
2762                    }
2763                    | self.runtime.advance_interactive(&interactive_bounds, dt)
2764                    | self.runtime.advance_ink(dt)
2765                    | reorder_animating
2766                    | autoscrolling
2767                    | app_animating;
2768                // With the inspector on, the same build collects the observed nodes,
2769                // and the overlay — outlines plus a card for the hovered widget — is
2770                // painted on top of a copy of the scene.
2771                let (ui, scene) = if self.inspector {
2772                    let (ui, nodes) = frus_widgets::build_ui_inspected(
2773                        tree,
2774                        Size::new(width, height),
2775                        &self.runtime,
2776                        &theme,
2777                    );
2778                    if std::mem::take(&mut self.inspector_dump) {
2779                        eprintln!("{}", frus_widgets::dump_tree(&nodes));
2780                    }
2781                    let mut scene = ui.scene().clone();
2782                    frus_widgets::paint_inspector_overlay(
2783                        &nodes,
2784                        Some(self.cursor),
2785                        Size::new(width, height),
2786                        &theme,
2787                        &mut scene,
2788                    );
2789                    // Scene: logical → physical (DPI × density).
2790                    (ui, scene.scaled(scale))
2791                } else {
2792                    let ui = build_ui(tree, Size::new(width, height), &self.runtime, &theme);
2793                    // The preview of a column reorder, or a lifted item, on top of the
2794                    // scene.
2795                    let scene = if matches!(self.drag, Some(Drag::Reorder { moved: true, .. })) {
2796                        let mut scene = ui.scene().clone();
2797                        self.paint_reorder_preview(&ui, &theme, &mut scene);
2798                        scene.scaled(scale)
2799                    } else if matches!(self.drag, Some(Drag::Item { moved: true, .. })) {
2800                        let mut scene = ui.scene().clone();
2801                        self.paint_drag_ghost(&ui, &theme, &mut scene);
2802                        scene.scaled(scale)
2803                    } else {
2804                        // Scene: logical → physical (DPI × density), for a crisp render.
2805                        ui.scene().scaled(scale)
2806                    };
2807                    (ui, scene)
2808                };
2809                self.report_overflows(&ui);
2810                // Laid out: the components the build no longer reached are let go, and the
2811                // effects it asked for run. An effect that changed something asks for the
2812                // frame that shows it.
2813                self.runtime.states.end_frame();
2814                if frus_widgets::rebuild_requested() {
2815                    self.request_redraw();
2816                }
2817                if let Some(renderer) = self.renderer.as_mut() {
2818                    match renderer.render(&scene) {
2819                        frus_gpu::RenderOutcome::Presented => {}
2820                        frus_gpu::RenderOutcome::NeedsReconfigure => {
2821                            renderer.reconfigure();
2822                        }
2823                        frus_gpu::RenderOutcome::Skipped => {}
2824                    }
2825                }
2826
2827                // A continuously animating widget, a spinner say, forces a redraw. So
2828                // does an image still on its way: without a frame to draw it in, the
2829                // frame that would show it never happens.
2830                //
2831                // The fetch is asked about **here** rather than inside the tree, and as a
2832                // count rather than a flag on the widget: showing a placeholder means
2833                // taking the image out of the tree, so a hook read off `Image` would go
2834                // quiet at exactly the moment it is needed. Asking here also keeps `Ui`
2835                // answering for its own widgets alone (milestone 411).
2836                let wants_animation = ui.wants_animation() || frus_widgets::images_in_flight() > 0;
2837
2838                // Keep the interface, for hit testing. The tree is already retained.
2839                self.ui = Some(ui);
2840                // The keyboard took room from the bottom of the window: what the reader is
2841                // typing in is brought back above it, by the least scroll that does it, and
2842                // by the same glide Tab uses. This frame's layout is the shortened one, so
2843                // the viewports asked are the ones left above the keyboard. A frame with
2844                // nothing focused, or no region around what is, moves nothing.
2845                let revealed = std::mem::take(&mut self.reveal_after_keyboard) && {
2846                    let moves = match (self.runtime.input.focused, self.ui.as_ref()) {
2847                        (Some(id), Some(ui)) => ui.reveal(id, &self.runtime),
2848                        _ => Vec::new(),
2849                    };
2850                    let moved = !moves.is_empty();
2851                    for (area, offset) in moves {
2852                        self.runtime.scroll_ballistic.remove(&area);
2853                        self.runtime.scroll_velocity.remove(&area);
2854                        self.runtime.scroll_target.insert(area, offset);
2855                    }
2856                    moved
2857                };
2858                // The tree may have changed under a still pointer — a tap that opened a
2859                // screen — so what it is over is asked of this frame, as the reference
2860                // does after every frame. Not during a drag: a slider dragged past its
2861                // end is still the one being pressed, and the move path leaves it so.
2862                //
2863                // Field by field rather than through `sync_hover`, which borrows the whole
2864                // shell: the frame still holds part of it here. A change is shown the way
2865                // the reference shows it, on the next frame — the tree is built again,
2866                // since a tooltip's bubble is decided while the tree is walked.
2867                let rehovered = self.drag.is_none() && {
2868                    let hovered = self.ui.as_ref().and_then(|ui| self.hover.target(ui));
2869                    let changed = hovered != self.runtime.input.hovered;
2870                    self.runtime.input.hovered = hovered;
2871                    changed
2872                };
2873                if rehovered {
2874                    self.build_dirty = true;
2875                }
2876                let wants_animation = wants_animation || rehovered || revealed;
2877
2878                // The paged views that have just turned a page, read off **this**
2879                // frame's regions: a page change is worth reporting the moment it
2880                // reads as one, not a frame later.
2881                let paged = self
2882                    .ui
2883                    .as_ref()
2884                    .map(|ui| ui.scroll_regions().to_vec())
2885                    .unwrap_or_default();
2886                // A region whose content shrank this frame under an offset nobody owns was
2887                // drawn where it now rests; the runtime keeps it there before anything reads
2888                // the offset back — the reports below, a press, the next frame's springs.
2889                // Without it the row was drawn right once and pressed wrong (milestone 533).
2890                self.runtime.keep_scroll_in_range(&paged);
2891                let turned: Vec<A::Message> = self
2892                    .runtime
2893                    .page_changes(&paged)
2894                    .into_iter()
2895                    .filter_map(|(id, page)| {
2896                        find_widget(tree, id).and_then(|widget| widget.on_page_changed(page))
2897                    })
2898                    .collect();
2899
2900                // The scroll requests the previous frame's registry could not place:
2901                // tried once against **this** frame's, which is where a region that has
2902                // only just appeared turns up. This is the last chance they get.
2903                let (_, moved_late) = apply_scroll_requests(
2904                    &mut self.runtime,
2905                    tree,
2906                    std::mem::take(&mut self.retry_scroll),
2907                    &paged,
2908                );
2909                // The same for the sheet requests: a sheet shown by the very message that
2910                // asked it to move is in this frame's sheets and not the last one's.
2911                let (_, sheeted_late) = {
2912                    let sheets = self
2913                        .ui
2914                        .as_ref()
2915                        .map(|ui| ui.sheets().to_vec())
2916                        .unwrap_or_default();
2917                    apply_sheet_requests(
2918                        &mut self.runtime,
2919                        tree,
2920                        std::mem::take(&mut self.retry_sheet),
2921                        &sheets,
2922                    )
2923                };
2924
2925                // The regions that have moved since they last said so, read off **this**
2926                // frame's registry so that the offset and the extents reported together
2927                // come from the same frame. A region nobody is listening to costs the
2928                // comparison and nothing else.
2929                let scrolled: Vec<A::Message> = {
2930                    let grain =
2931                        |id| find_widget(tree, id).map_or(0.0, |widget| widget.scroll_grain());
2932                    self.runtime
2933                        .scroll_changes(&paged, grain)
2934                        .into_iter()
2935                        .filter_map(|(id, position)| {
2936                            find_widget(tree, id).and_then(|widget| widget.on_scroll(position))
2937                        })
2938                        .collect()
2939                };
2940
2941                // The rows whose gap has just finished closing, the pages that have
2942                // turned and the regions that have moved: the tree is no longer
2943                // borrowed, so the application can be told — and rebuild.
2944                for message in dismissed.into_iter().chain(turned).chain(scrolled) {
2945                    self.dispatch(message);
2946                }
2947                if moved_late || sheeted_late {
2948                    // Nothing else this frame knows the offset changed: the springs ran
2949                    // before the request was placed.
2950                    self.request_redraw();
2951                }
2952
2953                // Focus return: when the focused widget has **vanished**, an overlay
2954                // having closed, go back to the trigger. Done before the AccessKit
2955                // announcement, so the focus it publishes is current.
2956                self.reconcile_focus();
2957
2958                // Publish the frame's semantic tree to AccessKit.
2959                #[cfg(desktop)]
2960                if let Some(a11y) = self.a11y.as_mut() {
2961                    let focus = self.runtime.input.focused;
2962                    let title = self.app.title();
2963                    if let Some(ui) = self.ui.as_ref() {
2964                        a11y.update(ui.semantics(), focus, &title, &self.announce);
2965                    }
2966                }
2967                // Publish the frame's semantic tree to the DOM bridge (#18).
2968                #[cfg(web)]
2969                if let Some(a11y) = self.a11y.as_mut() {
2970                    let focus = self.runtime.input.focused;
2971                    let title = self.app.title();
2972                    if let Some(ui) = self.ui.as_ref() {
2973                        a11y.update(ui.semantics(), focus, &title, &self.announce);
2974                    }
2975                }
2976
2977                // The system bars follow the frame: the regions under them, then the theme.
2978                self.sync_system_bars(&theme);
2979
2980                // The Android software keyboard follows the text fields' focus.
2981                self.sync_soft_input();
2982                // And the autofill service follows the form being edited.
2983                #[cfg(android)]
2984                self.sync_autofill();
2985
2986                // While an animation is running, ask for another frame.
2987                if animating || wants_animation {
2988                    self.request_redraw();
2989                }
2990            }
2991
2992            _ => {}
2993        }
2994    }
2995}
2996
2997impl<A: Application> App<A> {
2998    /// The total scale: system DPI × app density (physical = logical × this).
2999    fn total_scale(&self) -> f32 {
3000        (self.scale * self.app.density()).max(0.1)
3001    }
3002
3003    /// The surface description installed around every call to `view`, so that any
3004    /// widget built there can read it with `MediaQuery::of()` instead of having the
3005    /// application carry it down by hand.
3006    ///
3007    /// Everything in it is already known to the shell — the logical size it is about
3008    /// to lay out for, the DPI scale, the app's density, and the insets last reported
3009    /// by the platform. It is assembled here, in one place, rather than at each of the
3010    /// call sites.
3011    /// The **platform** answers, and the application overrides what it chose to speak for.
3012    ///
3013    /// That order is the one the reference uses and the only one that stays honest: the
3014    /// settings belong to the person using the device, so the framework asks them first,
3015    /// and an application with a *reduce motion* switch of its own says only that. An
3016    /// [`AccessibilityOverrides`] whose fields are all `None` — the default — leaves every
3017    /// answer to the user, which is what an application that has no settings screen means.
3018    fn media_query(&self, width: f32, height: f32) -> MediaQuery {
3019        MediaQuery::new(Size::new(width, height))
3020            .with_device_pixel_ratio(self.scale)
3021            .with_density(self.app.density())
3022            .with_text_scaler(self.platform.text_scaler)
3023            .with_platform_brightness(self.platform.brightness)
3024            .with_accessibility(
3025                self.platform
3026                    .accessibility
3027                    .with_overrides(self.app.accessibility()),
3028            )
3029            .with_insets(self.last_insets)
3030    }
3031
3032    /// The current layout direction; RTL flips both the layout and the gestures.
3033    fn is_rtl(&self) -> bool {
3034        self.themes.displayed(&self.app).direction.is_rtl()
3035    }
3036
3037    /// The window's **logical** width, in px, for the edge thresholds.
3038    fn logical_width(&self) -> f32 {
3039        let scale = self.total_scale();
3040        self.window
3041            .as_ref()
3042            .map(|w| w.inner_size().width as f32 / scale)
3043            .unwrap_or(1.0)
3044            .max(1.0)
3045    }
3046
3047    fn request_redraw(&self) {
3048        if let Some(window) = &self.window {
3049            window.request_redraw();
3050        }
3051    }
3052
3053    /// The **normalised** pointer input (gesture tier 0): mouse and touch converge
3054    /// here, with an explicit `Cancel`. The long-press recogniser is fed along the way,
3055    /// and the loop is woken at its deadline.
3056    fn pointer(&mut self, event_loop: &ActiveEventLoop, event: PointerEvent) {
3057        self.pointer_event(event);
3058        // Wake the loop exactly at the next deadline, and rest otherwise.
3059        event_loop.set_control_flow(self.idle_control_flow());
3060    }
3061
3062    /// Everything [`pointer`](Self::pointer) does with an event but wake the loop: the one
3063    /// place a press, a movement and a release are routed, and so what a test drives.
3064    fn pointer_event(&mut self, event: PointerEvent) {
3065        self.cursor = event.position;
3066        self.hover.event(event.kind, event.position, event.touch);
3067        // Whether the gesture under way could still end as a tap, before this event moves
3068        // it on: the press is shared with it until it cannot (milestone 534).
3069        let could_tap = gesture_was_a_tap(self.drag.as_ref());
3070        match event.kind {
3071            PointerKind::Down => {
3072                // What is under the pointer **now**: a finger has no move before its
3073                // press, and a press only shows while it is also the hovered widget.
3074                self.sync_hover();
3075                // A pointer interaction: the keyboard focus ring fades away.
3076                self.runtime.focus_visible = false;
3077                self.pointer_down(event.touch);
3078                let (long_press, lift) = hold_candidates(
3079                    self.drag.as_ref(),
3080                    self.ui.as_ref(),
3081                    self.tree.as_deref(),
3082                    self.cursor,
3083                );
3084                self.long_press_msg = long_press;
3085                self.pending_lift = lift;
3086                let interested = self.long_press_msg.is_some()
3087                    || self.pending_lift.is_some()
3088                    || self.pending_reorder.is_some()
3089                    || self.pending_word.is_some();
3090                self.press.down(self.cursor, Instant::now(), interested);
3091            }
3092            PointerKind::Move => {
3093                self.press.moved(self.cursor);
3094                self.pointer_move();
3095                // The inspector follows the pointer to highlight, so it redraws even
3096                // over inert widgets, which have no hover animation.
3097                if self.inspector {
3098                    self.request_redraw();
3099                }
3100            }
3101            PointerKind::Up => {
3102                // The recogniser is always told, but a lifted item owes a drop: the
3103                // long press that started it must not also eat its ending.
3104                let swallow = self.press.up();
3105                self.pending_lift = None;
3106                self.pending_reorder = None;
3107                self.pending_word = None;
3108                // A row lifted by the hold owes its drop for the same reason a lifted
3109                // item does: the release is what says where it goes.
3110                if swallow
3111                    && !matches!(
3112                        self.drag,
3113                        Some(Drag::Item { moved: true, .. } | Drag::Reorder { moved: true, .. })
3114                    )
3115                {
3116                    // The long press evicted the tap, so the release is swallowed.
3117                    self.drag = None;
3118                    self.runtime.input.pressed = None;
3119                    self.request_redraw();
3120                } else {
3121                    self.pointer_up();
3122                }
3123            }
3124            PointerKind::Cancel => {
3125                self.press.cancel();
3126                self.pending_lift = None;
3127                self.pending_reorder = None;
3128                self.pending_word = None;
3129                // A cancelled gesture still owes the offset back, or the region
3130                // would stay frozen under a finger that is no longer there.
3131                if let Some(Drag::Dismiss { item, .. }) = self.drag {
3132                    // A cancelled swipe asks for nothing: zero velocity puts it back.
3133                    self.runtime.dismiss_release(
3134                        item.id,
3135                        0.0,
3136                        0.0,
3137                        item.spec.axis,
3138                        // Unreachable threshold: a cancel never dismisses, however far
3139                        // the item had already travelled.
3140                        f32::INFINITY,
3141                    );
3142                }
3143                if let Some(Drag::Scroll { id, .. }) = self.drag {
3144                    self.runtime.release_scroll(id);
3145                    self.runtime.glow_scroll_end(id);
3146                    // A cancelled gesture asks for nothing: the indicator slides back
3147                    // rather than promising a refresh nobody requested.
3148                    if let Some(host) = self.refresh_host_of(id) {
3149                        self.runtime.refresh_cancel(host);
3150                    }
3151                }
3152                // A sheet under a cancelled finger settles from where it was left.
3153                let sheet = match self.drag {
3154                    Some(Drag::Sheet { id, available, .. }) => self
3155                        .ui
3156                        .as_ref()
3157                        .and_then(|ui| ui.sheet(id))
3158                        .map(|sheet| (sheet.id, sheet.spec.clone(), available)),
3159                    Some(Drag::Scroll { id, .. }) => self
3160                        .ui
3161                        .as_ref()
3162                        .and_then(|ui| ui.sheet_holding(id))
3163                        .map(|sheet| (sheet.id, sheet.spec.clone(), sheet.available)),
3164                    _ => None,
3165                };
3166                if let Some((id, spec, available)) = sheet {
3167                    self.runtime.sheet_release(id, &spec, available, 0.0, None);
3168                }
3169                self.drag = None;
3170                self.runtime.input.pressed = None;
3171                self.request_redraw();
3172            }
3173        }
3174        // A press a gesture has taken, or one whose pointer has gone, is let go of — after
3175        // the routing, which is what reads it (milestone 534).
3176        if settle_press(
3177            &mut self.runtime.input,
3178            event.kind,
3179            could_tap,
3180            self.drag.as_ref(),
3181            event.touch,
3182        ) {
3183            self.request_redraw();
3184        }
3185        // After the release has been routed — it reads the press, not the hover — a
3186        // finger that has lifted stops hovering (milestone 505).
3187        if matches!(event.kind, PointerKind::Up | PointerKind::Cancel) {
3188            self.sync_hover();
3189        }
3190    }
3191
3192    /// A press held still for the long-press delay: what it was a candidate for is decided.
3193    fn hold_deadline_reached(&mut self) {
3194        // Two claims on one hold: a widget asking for a message, and an item asking
3195        // to be lifted. Serving both would do a discrete action *and* start a drag
3196        // from the same gesture, which is never what anyone meant. **The lift
3197        // wins** — it changes what the rest of the gesture means, and the message
3198        // would be acting on something the finger is still holding.
3199        let lifting = self.pending_lift.is_some() || self.pending_reorder.is_some();
3200        // A hold in a text field selects the word under the finger (milestone 511),
3201        // and is that — not also the long press of whatever the field sits in.
3202        let word = self.pending_word.take();
3203        if let Some(message) = self.long_press_msg.take() {
3204            if !lifting && word.is_none() {
3205                self.dispatch(message);
3206            }
3207        }
3208        if let Some(id) = word {
3209            self.select_held_word(id);
3210        }
3211        let lift = self.pending_lift.take();
3212        let reorder = self.pending_reorder.take();
3213        // The scroll hands the gesture over to what the hold lifted.
3214        if lift.is_some() || reorder.is_some() {
3215            if let Some(Drag::Scroll { id, .. }) = self.drag {
3216                self.runtime.release_scroll(id);
3217            }
3218        }
3219        self.drag = drag_after_hold(self.drag.take(), lift, reorder, self.cursor);
3220        if matches!(self.drag, Some(Drag::Reorder { .. })) {
3221            self.reorder_x = self.cursor.x;
3222            self.reorder_y = self.cursor.y;
3223        }
3224        self.request_redraw();
3225    }
3226
3227    /// The loop's idle policy: wake at the **nearest** deadline — the long press, the
3228    /// live-reload poll, the caret's next turn — and otherwise wait outright.
3229    fn idle_control_flow(&self) -> ControlFlow {
3230        let press = self.press.deadline();
3231        let reload = self.reload.as_ref().map(|w| w.deadline());
3232        // Twice a second while a field has the caret and the window is in front, and never
3233        // otherwise (milestone 513).
3234        let caret = (self.lifecycle == Lifecycle::Resumed)
3235            .then(|| self.caret.next_toggle(Instant::now()))
3236            .flatten();
3237        match [press, reload, caret].into_iter().flatten().min() {
3238            Some(at) => ControlFlow::WaitUntil(at),
3239            None => ControlFlow::Wait,
3240        }
3241    }
3242
3243    /// The focused field's caret, as its blink sees it: which field, where its caret and
3244    /// selection are, and a digest of its value. `None` when what has focus takes no
3245    /// typing, which has no caret to blink.
3246    fn caret_signature(&self) -> Option<crate::caret::Signature> {
3247        use std::hash::{Hash, Hasher};
3248        let field = self.runtime.input.focused?;
3249        let widget = find_widget(self.tree.as_ref()?.as_ref(), field)?;
3250        let text = widget.text_value()?;
3251        let mut hasher = std::collections::hash_map::DefaultHasher::new();
3252        text.hash(&mut hasher);
3253        Some(crate::caret::Signature {
3254            field,
3255            edit: self.runtime.edits.get(&field).copied().unwrap_or_default(),
3256            value: hasher.finish(),
3257        })
3258    }
3259
3260    /// Pointer movement, mouse or finger: continues a drag under way, and otherwise
3261    /// updates the hover.
3262    fn pointer_move(&mut self) {
3263        if self.drag.is_some() {
3264            self.handle_drag();
3265            return;
3266        }
3267        let hovered = self.sync_hover();
3268        // The system cursor follows the hovered sub-region (milestone 205): a hand
3269        // over a clickable icon, and so on. Recomputed on every move, since the
3270        // sub-region can change without the hovered widget changing.
3271        self.update_cursor_icon(hovered);
3272    }
3273
3274    /// Asks the frame what is under the hovering pointer, and records it.
3275    ///
3276    /// The only way the hover changes, so it can never name a widget of another frame:
3277    /// an id is a position in the tree, and the same position on the next screen is a
3278    /// different widget (milestone 505).
3279    fn sync_hover(&mut self) -> Option<WidgetId> {
3280        let hovered = self.ui.as_ref().and_then(|ui| self.hover.target(ui));
3281        if hovered != self.runtime.input.hovered {
3282            self.runtime.input.hovered = hovered;
3283            self.request_redraw();
3284        }
3285        hovered
3286    }
3287
3288    /// Applies the cursor shape the hovered widget asks for at the pointer's local
3289    /// position (milestone 205), and the default cursor otherwise. Translates
3290    /// `frus_widgets::Cursor` into winit's.
3291    fn update_cursor_icon(&mut self, hovered: Option<WidgetId>) {
3292        let requested = hovered.and_then(|id| {
3293            let rect = self.ui.as_ref()?.widget_rect(id)?;
3294            let widget = find_widget(self.tree.as_ref()?.as_ref(), id)?;
3295            widget.cursor_icon(
3296                self.cursor.x - rect.x,
3297                self.cursor.y - rect.y,
3298                rect.width,
3299                rect.height,
3300            )
3301        });
3302        let icon = match requested.unwrap_or(UiCursor::Default) {
3303            UiCursor::Default => CursorIcon::Default,
3304            UiCursor::Pointer => CursorIcon::Pointer,
3305            UiCursor::Text => CursorIcon::Text,
3306        };
3307        if let Some(window) = &self.window {
3308            window.set_cursor(icon);
3309        }
3310        // Sub-region highlighting (milestone 208): the pointer's position is retained
3311        // while it hovers an interactive sub-region, that is, while cursor_icon answered.
3312        // A change repaints — the status hash includes hover_cursor — so the halo
3313        // follows the pointer or goes away.
3314        let hover_cursor = requested.map(|_| self.cursor);
3315        if hover_cursor != self.runtime.input.hover_cursor {
3316            self.runtime.input.hover_cursor = hover_cursor;
3317            self.request_redraw();
3318        }
3319    }
3320
3321    /// A pointer press, mouse or finger, at `self.cursor`. `touch` enables finger
3322    /// scrolling when no other gesture captures the press.
3323    fn pointer_down(&mut self, touch: bool) {
3324        self.pointer_touch = touch;
3325        self.pending_word = None;
3326        // A selection handle first. It hangs below its line, over whatever is drawn
3327        // there, so nothing else may claim the press before it; and any other press puts
3328        // the handles away (milestone 511).
3329        if let Some(drag) = self.grab_selection_handle() {
3330            self.drag = Some(drag);
3331            self.request_redraw();
3332            return;
3333        }
3334        if self.runtime.selection_handles.take().is_some() {
3335            self.request_redraw();
3336        }
3337        // 0) The back gesture: a press on the **leading edge** — left under LTR,
3338        // right under RTL — if the app allows it.
3339        let on_back_edge = if self.is_rtl() {
3340            self.cursor.x > self.logical_width() - BACK_EDGE
3341        } else {
3342            self.cursor.x < BACK_EDGE
3343        };
3344        if on_back_edge && self.app.can_go_back() {
3345            self.drag = Some(Drag::Back {
3346                start_x: self.cursor.x,
3347            });
3348            self.begin_gesture();
3349            self.build_dirty = true;
3350            self.app.back_gesture(0.0);
3351            self.request_redraw();
3352            return;
3353        }
3354
3355        // 1) Is this a scrollbar drag?
3356        if let Some(bar) = self.ui.as_ref().and_then(|ui| ui.scrollbar_at(self.cursor)) {
3357            let (along, thumb_start) = if bar.vertical {
3358                (self.cursor.y, bar.thumb.y)
3359            } else {
3360                (self.cursor.x, bar.thumb.x)
3361            };
3362            self.drag = Some(Drag::Scrollbar {
3363                id: bar.id,
3364                vertical: bar.vertical,
3365                grab: along - thumb_start,
3366                track_start: bar.track_start,
3367                track_len: bar.track_len,
3368                thumb_len: bar.thumb_len,
3369                max: bar.max,
3370                reverse: bar.reverse,
3371            });
3372            self.request_redraw();
3373            return;
3374        }
3375
3376        // 1b) Is this a draggable widget, a Slider for instance?
3377        if let Some((id, rect)) = self.ui.as_ref().and_then(|ui| ui.draggable_at(self.cursor)) {
3378            self.drag = Some(Drag::Widget {
3379                id,
3380                rect,
3381                last_x: self.cursor.x,
3382            });
3383            // The bracket opens **before** the first value: an application that means
3384            // to hold the expensive work until the finger lifts has to know the finger
3385            // went down first, and a press that jumps the value straight away would
3386            // otherwise deliver the change before the start.
3387            self.dispatch_drag_edge(id, rect, Edge::Start);
3388            // A zero delta on press: only a slider, which takes a fraction, jumps.
3389            self.apply_widget_drag(id, rect, 0.0);
3390            self.request_redraw();
3391            return;
3392        }
3393
3394        // 1c) A reorder: a press on a reorderable — a table header, a Kanban card, a
3395        // list's grip. We do not `return` — focus and `pressed`, where a tap means sort,
3396        // are settled below; the drag engages only past the threshold, and otherwise the
3397        // release sorts.
3398        //
3399        // A row that asked for a **hold** takes nothing now. The press is left to
3400        // whatever else wants it, which inside a list is the scroll, and the deadline
3401        // below decides between them: a finger that stays put was never scrolling.
3402        if let Some((id, from, hold)) = self.reorderable_at(self.cursor) {
3403            if hold {
3404                self.pending_reorder = Some((id, from, self.cursor));
3405            } else {
3406                self.drag = Some(Drag::Reorder {
3407                    id,
3408                    from,
3409                    start: self.cursor,
3410                    moved: false,
3411                    carried: None,
3412                });
3413                self.reorder_x = self.cursor.x; // starts glued to the pointer, with no jerk
3414                self.reorder_y = self.cursor.y; // likewise for the vertical insertion line
3415            }
3416        }
3417
3418        if press_at(&mut self.runtime, self.ui.as_ref(), self.cursor) {
3419            self.request_redraw();
3420        }
3421        // 2) Focus and caret placement, and the start of a text selection.
3422        let previously_focused = self.runtime.input.focused;
3423        let focus = self.ui.as_ref().and_then(|ui| ui.focus_hit(self.cursor));
3424        self.runtime.input.focused = focus.map(|(id, _)| id);
3425        if let Some((id, rect)) = focus {
3426            let local_x = self.cursor.x - rect.x;
3427            // The retained vertical scroll, in a multi-line field, is part of the
3428            // content coordinate: we add it so the click lands on the right line.
3429            let local_y =
3430                self.cursor.y - rect.y + self.runtime.scroll.get(&id).map(|s| s.1).unwrap_or(0.0);
3431            // The scroll shown just before this click, computed from the current caret
3432            // when the field was already focused, and 0 otherwise.
3433            let scroll_cursor = if previously_focused == Some(id) {
3434                self.runtime.edits.get(&id).map(|e| e.cursor).unwrap_or(0)
3435            } else {
3436                0
3437            };
3438            // Only **text fields** (`cursor_at` → `Some`) start a selection; the other
3439            // focusables — buttons, checkboxes — keep focus but must NOT capture the
3440            // click, which would otherwise be swallowed on release as the end of a drag.
3441            let cursor = self
3442                .tree
3443                .as_ref()
3444                .and_then(|tree| find_widget(tree.as_ref(), id))
3445                .and_then(|widget| widget.cursor_at(local_x, local_y, rect.width, scroll_cursor));
3446            if let Some(cursor) = cursor {
3447                // Placing the caret with the mouse forgets the vertical goal column.
3448                self.goal_x = None;
3449                self.runtime.edits.insert(
3450                    id,
3451                    Edit {
3452                        cursor,
3453                        anchor: None,
3454                        composing: None,
3455                    },
3456                );
3457                self.drag = Some(Drag::TextSelect { id, rect });
3458                // A finger that stays put selects the word instead (milestone 511).
3459                if touch {
3460                    self.pending_word = Some(id);
3461                }
3462                // The caret moved, so the run of typing ends here: typing at one place,
3463                // then at another, then Ctrl+Z should take back only the second.
3464                self.runtime.close_edit_run(id);
3465                // Tapping in a field **reopens** the keyboard, even one the app already
3466                // considers shown but which the system back closed — see
3467                // `request_soft_input`.
3468                self.request_soft_input();
3469
3470                // A double click selects the word under the pointer.
3471                let now = Instant::now();
3472                let double = self
3473                    .last_click_time
3474                    .map(|t| (now - t).as_secs_f32() < 0.4)
3475                    .unwrap_or(false);
3476                self.last_click_time = Some(now);
3477                if double {
3478                    if let Some((start, end)) = self
3479                        .tree
3480                        .as_ref()
3481                        .and_then(|tree| find_widget(tree.as_ref(), id))
3482                        .and_then(|widget| widget.word_at(cursor))
3483                    {
3484                        self.runtime.edits.insert(
3485                            id,
3486                            Edit {
3487                                cursor: end,
3488                                anchor: Some(start),
3489                                composing: None,
3490                            },
3491                        );
3492                        self.drag = None;
3493                    }
3494                }
3495            }
3496        }
3497
3498        // 3) Touch: when nothing captured the gesture — no scrollbar, no widget, no
3499        // text selection — prepare a finger scroll on the area under the finger. A
3500        // release without movement, under TOUCH_SLOP, stays a tap.
3501        if touch && self.drag.is_none() {
3502            // The innermost area that would actually take a finger. An area whose content
3503            // fits refuses the offset outright — the reference takes its drag recognisers
3504            // away for exactly this — so it neither swallows the press nor lights an edge,
3505            // and the page behind it, or a dismissible row under it, gets its turn.
3506            let landed = {
3507                let scroll = &self.runtime.scroll;
3508                self.ui.as_ref().and_then(|ui| {
3509                    ui.scroll_chain(self.cursor).find(|area| {
3510                        area.accepts_user_offset(
3511                            scroll.get(&area.id).copied().unwrap_or((0.0, 0.0)),
3512                        )
3513                    })
3514                })
3515            };
3516            if let Some(area) = landed {
3517                // A finger back on the content catches it: the fling stops where it
3518                // is rather than sliding under the finger, and hands the next
3519                // release whatever momentum the platform lets a swipe build on.
3520                let physics = area.physics_or(self.app.scroll_physics());
3521                let carried = self.runtime.catch_scroll_fling(area.id, physics);
3522                // The offset belongs to the finger until it lifts.
3523                self.runtime.hold_scroll(area.id);
3524                // And so does the sheet it sits in, if one is still settling: the finger
3525                // may be about to move it, and a sheet sliding away under a finger that
3526                // has caught its list is a sheet that ignored the catch.
3527                if let Some(sheet) = self
3528                    .ui
3529                    .as_ref()
3530                    .and_then(|ui| ui.sheet_holding(area.id))
3531                    .map(|sheet| sheet.id)
3532                {
3533                    self.runtime.sheet_hold(sheet);
3534                }
3535                self.drag = Some(Drag::Scroll {
3536                    id: area.id,
3537                    last: self.cursor,
3538                    moved: false,
3539                    carried,
3540                    dismiss: self
3541                        .ui
3542                        .as_ref()
3543                        .and_then(|ui| ui.dismissable_at(self.cursor)),
3544                    axis: None,
3545                });
3546                self.begin_gesture();
3547            }
3548        }
3549
3550        // 3a) A sheet's panel with nothing under the finger that scrolls: the finger moves
3551        // the sheet itself. A pointer too — on a desktop there is no other way to raise one.
3552        if self.drag.is_none() {
3553            if let Some((id, available)) = self
3554                .ui
3555                .as_ref()
3556                .and_then(|ui| ui.sheet_at(self.cursor))
3557                .map(|sheet| (sheet.id, sheet.available))
3558            {
3559                self.runtime.sheet_hold(id);
3560                self.drag = Some(Drag::Sheet {
3561                    id,
3562                    last: self.cursor,
3563                    moved: false,
3564                    available,
3565                });
3566                self.begin_gesture();
3567            }
3568        }
3569
3570        // 3b) A dismissible item with nothing scrollable under it: there is no gesture to
3571        // arbitrate against, so the swipe is prepared directly. It still waits for the
3572        // threshold, or a tap on the row would start sliding it.
3573        if self.drag.is_none() {
3574            if let Some(item) = self
3575                .ui
3576                .as_ref()
3577                .and_then(|ui| ui.dismissable_at(self.cursor))
3578            {
3579                self.drag = Some(Drag::Dismiss {
3580                    item,
3581                    last: self.cursor,
3582                    moved: false,
3583                });
3584                self.begin_gesture();
3585            }
3586        }
3587
3588        // 3c) A draggable item, when nothing above has taken the gesture. It comes
3589        // **after** the touch scroll on purpose: a widget that took every drag inside a
3590        // list would silently stop that list scrolling, and a list that does not scroll
3591        // is a worse bug than an item that does not lift. With a pointer there is no
3592        // touch scroll to lose to, so it lifts everywhere.
3593        if self.drag.is_none() {
3594            if let Some(source) = self
3595                .ui
3596                .as_ref()
3597                .and_then(|ui| ui.drag_source_at(self.cursor))
3598                .filter(|source| {
3599                    // One that asked for a hold waits for the deadline instead.
3600                    !self
3601                        .tree
3602                        .as_ref()
3603                        .and_then(|tree| find_widget(tree.as_ref(), source.id))
3604                        .is_some_and(|widget| widget.drag_needs_long_press())
3605                })
3606            {
3607                self.drag = Some(Drag::Item {
3608                    source,
3609                    start: self.cursor,
3610                    moved: false,
3611                    over: None,
3612                });
3613                self.begin_gesture();
3614            }
3615        }
3616
3617        // 4) An interactive viewport (`InteractiveViewer`) under the pointer: prepare a
3618        // **pan**, with mouse or finger. Like the touch scroll it engages only past the
3619        // threshold, so a tap goes through to the child, a button for instance.
3620        if self.drag.is_none() {
3621            if let Some((id, viewport)) = self
3622                .ui
3623                .as_ref()
3624                .and_then(|ui| ui.interactive_at(self.cursor))
3625            {
3626                // The press stops a fling in progress: we take the content back in hand.
3627                self.runtime.interactive_velocity.remove(&id);
3628                self.drag = Some(Drag::Pan {
3629                    id,
3630                    last: self.cursor,
3631                    moved: false,
3632                    viewport,
3633                });
3634                self.begin_gesture();
3635            }
3636        }
3637        self.request_redraw();
3638    }
3639
3640    /// A pointer release, mouse or finger: it ends a drag, or commits a click or tap
3641    /// when the release lands back on the widget that was pressed.
3642    fn pointer_up(&mut self) {
3643        let ended = self.drag.take();
3644        if let Some(Drag::Back { .. }) = ended {
3645            // The app decides — commit or cancel — from the velocity, which it wants
3646            // in **fractions of the screen** per second, not pixels.
3647            let sign = if self.is_rtl() { -1.0 } else { 1.0 };
3648            let velocity = sign * self.gesture_estimate().velocity.x / self.logical_width();
3649            self.build_dirty = true;
3650            self.app.back_gesture_end(velocity);
3651            self.request_redraw();
3652            return;
3653        }
3654        // A touch scroll, a pan or a swipe that never moved is a plain tap: we let it
3655        // follow the ordinary click path below.
3656        let was_tap = gesture_was_a_tap(ended.as_ref());
3657        // The bracket closes. A press that never moved still gets its end: it changed
3658        // the value once, and a caller that defers its expensive work until the release
3659        // would otherwise never be told the release happened.
3660        if let Some(Drag::Widget { id, rect, .. }) = ended {
3661            self.dispatch_drag_edge(id, rect, Edge::End);
3662        }
3663        // The keyboard is told where a dragged handle left the selection.
3664        #[cfg(android)]
3665        if let Some(Drag::SelectionHandle { id, .. }) = ended {
3666            self.push_ime_context(id);
3667        }
3668        // Reordering: on the drop, the target column is the reorderable header under
3669        // the pointer, and we route the grabbed header's `on_reorder(from, to)`.
3670        if let Some(Drag::Reorder {
3671            id,
3672            from,
3673            moved: true,
3674            carried,
3675            ..
3676        }) = &ended
3677        {
3678            let target = self.reorder_drop_target(*id, *from, *carried);
3679            let tree = self.tree.as_ref();
3680            let base = target
3681                .and_then(|tid| tree.and_then(|t| find_widget(t.as_ref(), tid)))
3682                .and_then(|widget| widget.reorder_index());
3683            // The **trailing** half of a hovered target that is inserted at — the lower
3684            // half down a list, the right half across one (the left, right to left) —
3685            // means inserting **after** it, index +1: the effective drop slot follows the
3686            // insertion line that was painted. No effect for `Table` columns, or off target.
3687            let to = match (base, target) {
3688                (Some(base), Some(tid)) => {
3689                    let after = self
3690                        .ui
3691                        .as_ref()
3692                        .and_then(|ui| ui.widget_rect(tid))
3693                        .map(|rect| self.reorder_insert_after(tid, rect))
3694                        .unwrap_or(false);
3695                    Some(base + after as usize)
3696                }
3697                _ => None,
3698            };
3699            // Dropping **onto itself** moves nothing, even in the lower half, where
3700            // `to = from + 1` would slip past the `to != from` guard — otherwise we would
3701            // emit a null move and announce it.
3702            let self_drop = target == Some(*id);
3703            let message = match to {
3704                Some(to) if to != *from && !self_drop => tree
3705                    .and_then(|t| find_widget(t.as_ref(), *id))
3706                    .and_then(|widget| widget.on_reorder(to)),
3707                _ => None,
3708            };
3709            if let Some(message) = message {
3710                let to = to.unwrap_or(*from);
3711                let widget = tree.and_then(|t| find_widget(t.as_ref(), *id));
3712                let axis = widget
3713                    .map(|w| w.reorder_axis())
3714                    .unwrap_or(ReorderAxis::Horizontal);
3715                // The move is spoken to the screen reader — the ghost's counterpart for
3716                // a blind user. A widget whose index **is** a position says so itself; a
3717                // `Kanban` card's is a flat `column × stride + position` that means
3718                // nothing read aloud, so the fallback speaks of the axis and gives no
3719                // number at all.
3720                let announcement = widget
3721                    .and_then(|w| w.reorder_announcement(to))
3722                    .unwrap_or_else(|| match axis {
3723                        ReorderAxis::Horizontal => format!("Column moved to position {}", to + 1),
3724                        ReorderAxis::Vertical => "Card moved".to_string(),
3725                    });
3726                self.dispatch(message);
3727                self.set_announcement(announcement);
3728            }
3729        }
3730        // Fling: the finger's momentum is handed to the area's physics, which
3731        // returns the motion that follows — a spline that stops at the edge, or
3732        // friction that hands over to a spring and bounces.
3733        // A swiped item: the release velocity decides between flying out and sliding
3734        // back, using the same fitted estimate a fling uses.
3735        if let Some(Drag::Dismiss { item, moved, .. }) = &ended {
3736            if *moved {
3737                let estimate = self.gesture_estimate();
3738                let horizontal = item.spec.axis.is_horizontal();
3739                let (along, across) = if horizontal {
3740                    (estimate.velocity.x, estimate.velocity.y)
3741                } else {
3742                    (estimate.velocity.y, estimate.velocity.x)
3743                };
3744                self.runtime.dismiss_release(
3745                    item.id,
3746                    along,
3747                    across,
3748                    item.spec.axis,
3749                    item.spec.threshold,
3750                );
3751                self.request_redraw();
3752                return;
3753            }
3754        }
3755        // A sheet let go of by its panel settles from wherever the finger left it — and
3756        // one only pressed settles too, since the press caught it if it was moving.
3757        if let Some(Drag::Sheet {
3758            id,
3759            moved,
3760            available,
3761            ..
3762        }) = &ended
3763        {
3764            let velocity = if *moved {
3765                -self.fling_velocity(self.gesture_estimate()).1
3766            } else {
3767                0.0
3768            };
3769            // Thrown by its handle, a sheet with one list in it carries that list on at the
3770            // top, as the reference's does: there its whole content is the list.
3771            let sheet = self.ui.as_ref().and_then(|ui| ui.sheet(*id)).map(|sheet| {
3772                let list = match sheet.areas.as_slice() {
3773                    [only] => Some(*only),
3774                    _ => None,
3775                };
3776                (sheet.spec.clone(), list)
3777            });
3778            if let Some((spec, list)) = sheet {
3779                self.runtime
3780                    .sheet_release(*id, &spec, *available, velocity, list);
3781            }
3782            if *moved {
3783                self.request_redraw();
3784                return;
3785            }
3786        }
3787        if let Some(Drag::Item {
3788            source,
3789            moved: true,
3790            over,
3791            ..
3792        }) = &ended
3793        {
3794            // The drop, then the source's own answer. In that order: an application
3795            // that reacts to both should see the thing arrive before it is told the
3796            // journey is over.
3797            self.runtime.drag_over = None;
3798            let accepted = over.is_some();
3799            let dropped = over.and_then(|id| {
3800                self.tree
3801                    .as_ref()
3802                    .and_then(|tree| find_widget(tree.as_ref(), id))
3803                    .and_then(|widget| widget.on_drop(source.payload))
3804            });
3805            let ended_msg = self
3806                .tree
3807                .as_ref()
3808                .and_then(|tree| find_widget(tree.as_ref(), source.id))
3809                .and_then(|widget| widget.on_dropped(accepted));
3810            for message in dropped.into_iter().chain(ended_msg) {
3811                self.dispatch(message);
3812            }
3813            self.request_redraw();
3814            return;
3815        }
3816        if let Some(Drag::Item { .. }) = &ended {
3817            // Lifted but never moved: nothing was dropped, and the click below still
3818            // has its say.
3819            self.runtime.drag_over = None;
3820        }
3821        // Set when a sheet takes the release of a list inside it, which then does not
3822        // fling as well (milestone 515).
3823        let mut sheet_took = false;
3824        if let Some(Drag::Scroll { id, .. }) = &ended {
3825            let sheet = self
3826                .ui
3827                .as_ref()
3828                .and_then(|ui| ui.sheet_holding(*id))
3829                .cloned();
3830            if let Some(sheet) = sheet {
3831                // In the sheet's terms, positive growing it: a list's offset grows the
3832                // same way, on an axis that is not reversed.
3833                let velocity = match &ended {
3834                    Some(Drag::Scroll {
3835                        moved: true,
3836                        axis: Some(true),
3837                        carried,
3838                        ..
3839                    }) => -self.fling_velocity(self.gesture_estimate()).1 + carried.1,
3840                    _ => 0.0,
3841                };
3842                let offset = self.runtime.scroll.get(id).map_or(0.0, |o| o.1);
3843                let size = self.runtime.sheet_size(sheet.id, &sheet.spec);
3844                sheet_took = velocity != 0.0
3845                    && frus_widgets::sheet_takes_release(
3846                        velocity,
3847                        offset,
3848                        size,
3849                        &sheet.spec,
3850                        sheet.available,
3851                    );
3852                // Whoever takes it, the sheet is let go of: settled by the throw, or
3853                // from where it is — which is a stop, whenever the list keeps the throw.
3854                self.runtime.sheet_release(
3855                    sheet.id,
3856                    &sheet.spec,
3857                    sheet.available,
3858                    if sheet_took { velocity } else { 0.0 },
3859                    Some(*id),
3860                );
3861            }
3862        }
3863        if let Some(Drag::Scroll { id, .. }) = &ended {
3864            // The finger gives the offset back before anything is flung at it.
3865            self.runtime.release_scroll(*id);
3866            // A pull held against an edge now fades at the "let go" rate.
3867            self.runtime.glow_scroll_end(*id);
3868            // And a pull-to-refresh gesture is answered: releasing it armed is the
3869            // whole point of the gesture, so the message goes out now rather than
3870            // after the indicator has finished settling.
3871            self.release_refresh(*id);
3872        }
3873        if let Some(Drag::Scroll {
3874            id,
3875            moved: true,
3876            carried,
3877            axis,
3878            ..
3879        }) = &ended
3880        {
3881            // In **scroll space**: the content moves opposite the finger — and along the
3882            // axis the gesture was claimed by, or the release would fling the way the drag
3883            // was not allowed to go.
3884            let estimate = self.gesture_estimate();
3885            let velocity = self.fling_velocity(estimate);
3886            // Through the same conversion the drag went through, so the release carries
3887            // on the way the finger was going — on a reversed axis too.
3888            let velocity = match self.ui.as_ref().and_then(|u| u.scroll_region(*id)) {
3889                Some(area) => area.offset_delta(velocity),
3890                None => (-velocity.0, -velocity.1),
3891            };
3892            // The momentum carried in from an interrupted fling is masked with the rest:
3893            // a gesture held to one axis must not launch along the other on the strength
3894            // of what the *previous* one was doing.
3895            let launch = (velocity.0 + carried.0, velocity.1 + carried.1);
3896            if sheet_took {
3897                self.request_redraw();
3898            } else {
3899                self.fling(
3900                    *id,
3901                    match axis {
3902                        Some(true) => (0.0, launch.1),
3903                        Some(false) => (launch.0, 0.0),
3904                        None => launch,
3905                    },
3906                );
3907            }
3908        }
3909        // A pan fling: the momentum launches the content, which `advance_interactive`
3910        // decelerates and bounds frame by frame.
3911        if let Some(Drag::Pan {
3912            id, moved: true, ..
3913        }) = &ended
3914        {
3915            let estimate = self.gesture_estimate();
3916            let velocity = self.fling_velocity(estimate);
3917            if velocity.0.hypot(velocity.1) > PAN_FLING_MIN {
3918                self.runtime.interactive_velocity.insert(*id, velocity);
3919            }
3920        }
3921        if ended.is_some() && !was_tap {
3922            self.request_redraw();
3923            return;
3924        }
3925        // A click only counts when press and release land on the same widget.
3926        let released = self.ui.as_ref().and_then(|ui| ui.hit(self.cursor));
3927        let (message, announce) = match (self.runtime.input.pressed, released) {
3928            (Some(pressed), Some(released)) if pressed == released => {
3929                // A **positional** click, on a sub-region such as a field's clickable
3930                // suffix, takes priority over `on_click`. Local coordinates are the
3931                // pointer minus the widget's corner.
3932                let positional = self
3933                    .ui
3934                    .as_ref()
3935                    .and_then(|ui| ui.widget_rect(released))
3936                    .and_then(|rect| {
3937                        self.tree
3938                            .as_ref()
3939                            .and_then(|tree| find_widget(tree.as_ref(), released))
3940                            .and_then(|widget| {
3941                                widget.positional_click(
3942                                    self.cursor.x - rect.x,
3943                                    self.cursor.y - rect.y,
3944                                    rect.width,
3945                                    rect.height,
3946                                )
3947                            })
3948                    });
3949                let message =
3950                    positional.or_else(|| self.ui.as_ref().and_then(|ui| ui.msg_for(pressed)));
3951                // The spoken announcement of the effect — a sort, a selection — read off
3952                // the clicked widget before `dispatch` rebuilds the tree.
3953                let announce = self
3954                    .tree
3955                    .as_ref()
3956                    .and_then(|tree| find_widget(tree.as_ref(), pressed))
3957                    .and_then(|widget| widget.announce());
3958                (message, announce)
3959            }
3960            _ => (None, None),
3961        };
3962        // The tap completed on the widget it started on: its ink finishes growing and
3963        // fades. Anything else leaves the splash unconfirmed, and `advance_ink` sweeps
3964        // it away quickly — the finger slid off, and the ink says so.
3965        if let (Some(pressed), Some(released)) = (self.runtime.input.pressed, released) {
3966            if pressed == released {
3967                self.runtime.ink_confirm(pressed);
3968            }
3969        }
3970        self.runtime.input.pressed = None;
3971        if let Some(message) = message {
3972            self.dispatch(message);
3973            if let Some(announce) = announce {
3974                self.set_announcement(announce);
3975            }
3976        }
3977        self.request_redraw();
3978    }
3979
3980    /// Speaks a message aloud through the screen reader's **live region**, for a
3981    /// column reorder and the like. With no screen reader running it costs nothing. The
3982    /// text is re-spoken only on a change. Desktop and web; a no-op on Android and iOS,
3983    /// which have no bridge yet.
3984    #[cfg(any(desktop, web))]
3985    fn set_announcement(&mut self, message: String) {
3986        self.announce = message;
3987    }
3988    #[cfg(not(any(desktop, web)))]
3989    fn set_announcement(&mut self, _message: String) {}
3990
3991    /// Returning focus when an overlay closes: if the focused widget has **vanished**
3992    /// from the frame, a menu or modal having closed, focus goes back to the
3993    /// **trigger** — the most recent focusable in the history that is still present.
3994    /// Every transition is recorded along the way: the old focus, if still present,
3995    /// becomes a candidate trigger. The focus ring is drawn on the following frame
3996    /// (`request_redraw` when focus moved).
3997    fn reconcile_focus(&mut self) {
3998        let present: std::collections::HashSet<WidgetId> = match self.ui.as_ref() {
3999            Some(ui) => ui.focusable_ids().collect(),
4000            None => return,
4001        };
4002        let before = self.runtime.input.focused;
4003        let after = resolve_focus(
4004            before,
4005            &present,
4006            &mut self.focus_history,
4007            &mut self.prev_focus,
4008        );
4009        if after != before {
4010            self.runtime.input.focused = after;
4011            self.request_redraw();
4012        }
4013    }
4014
4015    /// Applies a message to the application, runs its effects, then re-evaluates the
4016    /// subscriptions, the state having possibly changed which ones should run.
4017    fn dispatch(&mut self, message: A::Message) {
4018        // The app may have changed state, so the `view` must be rebuilt.
4019        self.build_dirty = true;
4020        let command = self.app.update(message);
4021        self.run_command(command);
4022        let asked = self.app.effects();
4023        self.run_command(asked);
4024        #[cfg(web)]
4025        self.sync_address();
4026        self.sync_subscriptions();
4027    }
4028
4029    /// The link the program was started with — on Android the activity's intent, on a desktop an
4030    /// argument — handed to the application as a location. Once, before the first frame.
4031    #[cfg(any(android, desktop))]
4032    fn open_launch_link(&mut self) {
4033        #[cfg(android)]
4034        let link = self
4035            .android_app
4036            .as_ref()
4037            .and_then(crate::android_link::launch_link);
4038        #[cfg(desktop)]
4039        let link = crate::link::link_among(std::env::args());
4040        if let Some(location) = link.as_deref().and_then(crate::link::location_of_link) {
4041            let command = self.app.open_location(&location);
4042            self.run_command(command);
4043        }
4044    }
4045
4046    /// The links a second process handed to this one since the last frame.
4047    #[cfg(desktop)]
4048    fn open_links_from_other_instances(&mut self) {
4049        for link in crate::instance::take_links() {
4050            if let Some(location) = crate::link::location_of_link(&link) {
4051                let command = self.app.open_location(&location);
4052                self.run_command(command);
4053                self.build_dirty = true;
4054                self.request_redraw();
4055            }
4056        }
4057    }
4058
4059    /// The address the page was opened at, handed to the application, and the browser's own
4060    /// moves from here on listened to. Once, before the first frame.
4061    #[cfg(web)]
4062    fn open_launch_address(&mut self, window: &Arc<Window>) {
4063        let strategy = self.app.location_strategy();
4064        let base = crate::address::base();
4065        let (location, index) = crate::address::current(strategy, &base);
4066        self.history.opened(location.clone(), index);
4067        crate::address::listen(window.clone(), strategy, base.clone());
4068        self.address = (strategy, base);
4069        if let Some(location) = location {
4070            let command = self.app.open_location(&location);
4071            self.run_command(command);
4072            // Opened deep in the application: the pages beneath get entries of their own.
4073            let (strategy, base) = &self.address;
4074            for (step, index) in self.history.seed(&self.app.location_stack()) {
4075                crate::address::apply(step, *strategy, base, index);
4076            }
4077        }
4078    }
4079
4080    /// What the browser did since the last look is told to the application, and where the
4081    /// application is is shown in the address bar — the agreement of [`crate::history`].
4082    #[cfg(web)]
4083    fn sync_address(&mut self) {
4084        if !self.started {
4085            return;
4086        }
4087        for (location, state) in crate::address::take_popped() {
4088            if self.history.popped(location.clone(), state) {
4089                let command = self.app.open_location(&location);
4090                self.run_command(command);
4091                self.build_dirty = true;
4092                // The move it starts — a page sliding — is ticked by the frames after this
4093                // one, which was decided to be still before the address was read.
4094                self.request_redraw();
4095            }
4096        }
4097        if let Some(location) = self.app.location() {
4098            if let Some(step) = self
4099                .history
4100                .reflect(&location, self.app.location_replaces())
4101            {
4102                let (strategy, base) = &self.address;
4103                crate::address::apply(step, *strategy, base, self.history.index());
4104            }
4105        }
4106    }
4107
4108    /// Runs a command, and whatever message it produces comes back into the loop
4109    /// through the proxy. Focus requests are **queued**, then resolved against the
4110    /// freshly built tree on the next frame.
4111    ///
4112    /// Where each kind of effect goes:
4113    ///
4114    /// | | native | Web |
4115    /// |---|---|---|
4116    /// | synchronous task | its own thread | a `spawn_local` microtask |
4117    /// | asynchronous task | the shell's executor | the browser |
4118    /// | timer | a task on the executor's reactor | `setTimeout` |
4119    ///
4120    /// A **synchronous** task keeps its thread: it blocks by definition, and that is
4121    /// what a thread is for. An **asynchronous** one no longer gets one — it is a
4122    /// task among others on a pool of four, which is the whole point of milestone
4123    /// 303. See [`crate::runtime`].
4124    fn run_command(&mut self, command: crate::command::Command<A::Message>) {
4125        let parts = command.into_parts();
4126        self.pending_focus.extend(parts.focus);
4127        self.pending_scroll.extend(parts.scrolls);
4128        self.pending_sheet.extend(parts.sheets);
4129        for task in parts.tasks {
4130            let proxy = self.proxy.clone();
4131            #[cfg(not(web))]
4132            std::thread::spawn(move || {
4133                if let Some(message) = task() {
4134                    let _ = proxy.send_event(message);
4135                }
4136            });
4137            #[cfg(web)]
4138            wasm_bindgen_futures::spawn_local(async move {
4139                if let Some(message) = task() {
4140                    let _ = proxy.send_event(message);
4141                }
4142            });
4143        }
4144        for future in parts.async_tasks {
4145            let proxy = self.proxy.clone();
4146            // Native: a task on the shared executor. `detach` because an effect must
4147            // outlive the handle — nothing here wants to cancel it, unlike a
4148            // subscription.
4149            #[cfg(not(web))]
4150            crate::runtime::spawn(async move {
4151                if let Some(message) = future.await {
4152                    let _ = proxy.send_event(message);
4153                }
4154            })
4155            .detach();
4156            // Web: the browser drives the future, single-threaded — `fetch` and friends
4157            // genuinely `await`, without blocking the loop.
4158            #[cfg(web)]
4159            wasm_bindgen_futures::spawn_local(async move {
4160                if let Some(message) = future.await {
4161                    let _ = proxy.send_event(message);
4162                }
4163            });
4164        }
4165        for (delay, message) in parts.timers {
4166            let proxy = self.proxy.clone();
4167            #[cfg(not(web))]
4168            crate::runtime::spawn(async move {
4169                crate::runtime::sleep(delay).await;
4170                let _ = proxy.send_event(message);
4171            })
4172            .detach();
4173            #[cfg(web)]
4174            web_timer::after(delay.as_millis() as i32, move || {
4175                let _ = proxy.send_event(message);
4176            });
4177        }
4178    }
4179
4180    /// Diffs the subscriptions the app declares against those running: starts the new
4181    /// ones and stops those that vanished, by dropping their `Sender`.
4182    fn sync_subscriptions(&mut self) {
4183        let entries = self.app.subscription().into_entries();
4184        let declared: std::collections::HashSet<u64> = entries.iter().map(|e| e.id).collect();
4185
4186        // Stop the subscriptions that are no longer declared.
4187        self.running_subs.retain(|id, _| declared.contains(id));
4188
4189        // Start the new ones.
4190        for entry in entries {
4191            if self.running_subs.contains_key(&entry.id) {
4192                continue;
4193            }
4194            let handle = self.start_subscription(entry.kind);
4195            self.running_subs.insert(entry.id, handle);
4196        }
4197    }
4198
4199    /// Starts a subscription as a task on the shell's executor, and returns its
4200    /// cancellation handle — **dropping the handle cancels the task**, which is what
4201    /// `sync_subscriptions` above relies on when the application stops declaring it.
4202    ///
4203    /// This used to be a thread per subscription, parked in `recv_timeout` and woken
4204    /// by the timeout or by its `Sender` being dropped. A timer is the clearest case
4205    /// there is of work that only ever waits, so it is the clearest case for not
4206    /// spending a thread on it.
4207    #[cfg(not(web))]
4208    fn start_subscription(&self, kind: crate::subscription::Kind<A::Message>) -> SubHandle {
4209        let proxy = self.proxy.clone();
4210        match kind {
4211            crate::subscription::Kind::Every { interval, make } => {
4212                crate::runtime::spawn(async move {
4213                    loop {
4214                        crate::runtime::sleep(interval).await;
4215                        // The loop having closed is the other way this ends, and the
4216                        // application is gone by then, so there is nobody to tell.
4217                        if proxy.send_event(make(Instant::now())).is_err() {
4218                            break;
4219                        }
4220                    }
4221                })
4222            }
4223        }
4224    }
4225
4226    /// Starts a subscription on the **Web**: a browser `setInterval`, with no thread.
4227    /// Returns its cancellation handle, which calls `clearInterval` on drop. The proxy
4228    /// feeds the message back into the loop on every tick.
4229    #[cfg(web)]
4230    fn start_subscription(&self, kind: crate::subscription::Kind<A::Message>) -> SubHandle {
4231        let proxy = self.proxy.clone();
4232        match kind {
4233            crate::subscription::Kind::Every { interval, make } => {
4234                let ms = interval.as_millis() as i32;
4235                web_timer::Interval::new(ms, move || {
4236                    let _ = proxy.send_event(make(Instant::now()));
4237                })
4238            }
4239        }
4240    }
4241
4242    /// Applies the mouse drag under way.
4243    fn handle_drag(&mut self) {
4244        let Some(mut drag) = self.drag.take() else {
4245            return;
4246        };
4247        // How far this pointer has to travel before a press counts as a drag.
4248        let slop = self.hit_slop();
4249        match &mut drag {
4250            Drag::Scrollbar {
4251                id,
4252                vertical,
4253                grab,
4254                track_start,
4255                track_len,
4256                thumb_len,
4257                max,
4258                reverse,
4259            } => {
4260                let along = if *vertical {
4261                    self.cursor.y
4262                } else {
4263                    self.cursor.x
4264                };
4265                let travel = (*track_len - *thumb_len).max(1.0);
4266                let thumb_start = (along - *grab).clamp(*track_start, *track_start + travel);
4267                let fraction = (thumb_start - *track_start) / travel;
4268                // A reversed axis numbers its offsets from the far end, so the thumb's
4269                // position along the track reads backwards.
4270                let fraction = if *reverse { 1.0 - fraction } else { fraction };
4271                let offset = (fraction * *max).clamp(0.0, *max);
4272                let entry = self.runtime.scroll.entry(*id).or_insert((0.0, 0.0));
4273                if *vertical {
4274                    entry.1 = offset;
4275                } else {
4276                    entry.0 = offset;
4277                }
4278                // A precise drag: the target follows the offset and inertia is cut off.
4279                let synced = *entry;
4280                self.runtime.scroll_target.insert(*id, synced);
4281                self.runtime.scroll_velocity.remove(&*id);
4282            }
4283            Drag::TextSelect { id, rect } => {
4284                let local_x = self.cursor.x - rect.x;
4285                let local_y = self.cursor.y - rect.y
4286                    + self.runtime.scroll.get(id).map(|s| s.1).unwrap_or(0.0);
4287                // The field is focused during the drag, so scroll from the current caret.
4288                let scroll_cursor = self.runtime.edits.get(id).map(|e| e.cursor).unwrap_or(0);
4289                let cursor = self
4290                    .tree
4291                    .as_ref()
4292                    .and_then(|tree| find_widget(tree.as_ref(), *id))
4293                    .and_then(|widget| {
4294                        widget.cursor_at(local_x, local_y, rect.width, scroll_cursor)
4295                    });
4296                if let Some(cursor) = cursor {
4297                    let edit = self.runtime.edits.entry(*id).or_default();
4298                    if edit.anchor.is_none() {
4299                        edit.anchor = Some(edit.cursor);
4300                    }
4301                    edit.cursor = cursor;
4302                }
4303            }
4304            Drag::SelectionHandle {
4305                id,
4306                rect,
4307                handle,
4308                grab,
4309            } => {
4310                // The text position the handle stands for, carried with the finger, and
4311                // the retained vertical scroll folded in as for a press.
4312                let local_x = self.cursor.x - rect.x + grab.x;
4313                let local_y = self.cursor.y - rect.y
4314                    + grab.y
4315                    + self.runtime.scroll.get(id).map(|s| s.1).unwrap_or(0.0);
4316                let edit = self.runtime.edits.get(id).copied().unwrap_or_default();
4317                let moved = self
4318                    .tree
4319                    .as_ref()
4320                    .and_then(|tree| find_widget(tree.as_ref(), *id))
4321                    .and_then(|widget| widget.cursor_at(local_x, local_y, rect.width, edit.cursor))
4322                    .and_then(|to| crate::selection::drag(edit, *handle, to));
4323                if let Some(moved) = moved {
4324                    self.runtime.edits.insert(*id, moved);
4325                }
4326            }
4327            Drag::Widget { id, rect, last_x } => {
4328                let dx = self.cursor.x - *last_x;
4329                *last_x = self.cursor.x;
4330                self.apply_widget_drag(*id, *rect, dx);
4331            }
4332            Drag::Reorder { start, moved, .. } => {
4333                // Past the threshold this is a real drag, and no longer a sort.
4334                let dx = self.cursor.x - start.x;
4335                let dy = self.cursor.y - start.y;
4336                if !*moved && (dx * dx + dy * dy) > slop * slop {
4337                    *moved = true;
4338                }
4339                // The ghost card follows the pointer, so redraw on every move.
4340                if *moved {
4341                    self.request_redraw();
4342                }
4343            }
4344            Drag::Pan {
4345                id,
4346                last,
4347                moved,
4348                viewport,
4349            } => {
4350                let dx = self.cursor.x - last.x;
4351                let dy = self.cursor.y - last.y;
4352                if !*moved && (dx * dx + dy * dy) > slop * slop {
4353                    *moved = true;
4354                }
4355                if *moved {
4356                    let view = self.runtime.interactive.entry(*id).or_default();
4357                    // The finger pushes the content, bounded to the frame.
4358                    *view = view.pan(dx, dy).clamped(*viewport);
4359                    *last = self.cursor;
4360                    self.track_gesture();
4361                }
4362            }
4363            Drag::Item {
4364                source,
4365                start,
4366                moved,
4367                over,
4368            } => {
4369                let dx = self.cursor.x - start.x;
4370                let dy = self.cursor.y - start.y;
4371                // Under the threshold nothing is lifted: the press may still be a click
4372                // on whatever the draggable wraps.
4373                if !*moved && (dx * dx + dy * dy) > slop * slop {
4374                    *moved = true;
4375                }
4376                if !*moved {
4377                    return;
4378                }
4379                // The target under the pointer, but only if it would take this payload:
4380                // a target that refuses is not highlighted, so the answer is visible
4381                // before the finger lifts rather than after.
4382                let payload = source.payload;
4383                let candidate = self
4384                    .ui
4385                    .as_ref()
4386                    .and_then(|ui| ui.drop_zone_at(self.cursor))
4387                    .filter(|zone| {
4388                        self.tree
4389                            .as_ref()
4390                            .and_then(|tree| find_widget(tree.as_ref(), zone.id))
4391                            .is_some_and(|widget| widget.accepts_drag(payload))
4392                    })
4393                    .map(|zone| zone.id);
4394                if *over != candidate {
4395                    *over = candidate;
4396                    self.runtime.drag_over = candidate;
4397                }
4398                self.request_redraw();
4399            }
4400            Drag::Scroll {
4401                id,
4402                last,
4403                moved,
4404                carried,
4405                dismiss,
4406                axis,
4407            } => {
4408                let dx = self.cursor.x - last.x;
4409                let dy = self.cursor.y - last.y;
4410                // Under the threshold we do not scroll yet; the gesture may be a tap.
4411                if !*moved && (dx * dx + dy * dy) > slop * slop {
4412                    *moved = true;
4413                    // The moment of arbitration. A scroll and a swipe start identically,
4414                    // so the question is not who is on top but **which way the finger
4415                    // went**: along the item's swipe axis it is a dismissal, across it a
4416                    // scroll. Deciding once, here, is what keeps the loser out of the
4417                    // gesture entirely — a swipe that also scrolled the list would be
4418                    // worse than either.
4419                    if let Some(item) = dismiss.take() {
4420                        let along = if item.spec.axis.is_horizontal() {
4421                            dx.abs() > dy.abs()
4422                        } else {
4423                            dy.abs() > dx.abs()
4424                        };
4425                        if along {
4426                            // The list gives the offset back untouched: it never moved.
4427                            self.runtime.release_scroll(*id);
4428                            self.drag = Some(Drag::Dismiss {
4429                                item,
4430                                last: *last,
4431                                moved: true,
4432                            });
4433                            self.handle_drag();
4434                            return;
4435                        }
4436                    }
4437                    // And the same arbitration between the two **axes** — which settles,
4438                    // with it, *which* of the scrollables under the finger the gesture
4439                    // belongs to. The reference does both at once in its arena: every
4440                    // scrollable in the stack enters a recogniser for its own axis, the
4441                    // one the finger matches wins, and the losers are out of the gesture
4442                    // entirely. Without the first half a page scrolling down drifts
4443                    // sideways the whole way, since no finger travels in a straight line;
4444                    // without the second, a strip of chips that only slides across stops
4445                    // the page behind it from scrolling at all.
4446                    let chain: Vec<_> = self
4447                        .ui
4448                        .as_ref()
4449                        .map(|ui| ui.scroll_chain(*last).collect())
4450                        .unwrap_or_default();
4451                    let down = dy.abs() >= dx.abs();
4452                    if let Some(area) = claim_area(&chain, *id, down) {
4453                        if area.id != *id {
4454                            // The area under the finger lost: it gives its offset back
4455                            // untouched, since it never moved. The winner is caught and
4456                            // held exactly as the press would have done to it.
4457                            self.runtime.release_scroll(*id);
4458                            let physics = area.physics_or(self.app.scroll_physics());
4459                            *carried = self.runtime.catch_scroll_fling(area.id, physics);
4460                            self.runtime.hold_scroll(area.id);
4461                            *id = area.id;
4462                        }
4463                        *axis = Some(claim_axis(dx, dy, area.max_x > 0.0, area.max_y > 0.0));
4464                    } else {
4465                        *axis = Some(down);
4466                    }
4467                }
4468                // Only the claimed axis moves. The other delta is not held back for later:
4469                // it is not part of this gesture at all.
4470                let (dx, mut dy) = match *axis {
4471                    Some(true) => (0.0, dy),
4472                    Some(false) => (dx, 0.0),
4473                    None => (dx, dy),
4474                };
4475                if *moved && *axis == Some(true) {
4476                    // A list inside a sheet shares the finger with it (milestone 515).
4477                    // Every movement is split — the sheet first going up, the list
4478                    // first going down — so the gesture changes hands in the middle of a
4479                    // drag, and what the list is left with goes through its physics below
4480                    // exactly as a movement of its own would.
4481                    let sheet = self
4482                        .ui
4483                        .as_ref()
4484                        .filter(|ui| ui.scroll_region(*id).is_some_and(|a| !a.reverse_y))
4485                        .and_then(|ui| ui.sheet_holding(*id))
4486                        .filter(|sheet| sheet.available > 0.0)
4487                        .cloned();
4488                    if let Some(sheet) = sheet {
4489                        let px = sheet.available;
4490                        let offset = self.runtime.scroll.get(id).map_or(0.0, |o| o.1);
4491                        let size = self.runtime.sheet_size(sheet.id, &sheet.spec) * px;
4492                        let (grown, listed) = frus_widgets::split_sheet_drag(
4493                            -dy,
4494                            offset,
4495                            size,
4496                            sheet.spec.floor() * px,
4497                            sheet.spec.max * px,
4498                        );
4499                        if grown != 0.0 {
4500                            self.runtime
4501                                .sheet_drag(sheet.id, &sheet.spec, grown / px, px);
4502                        }
4503                        dy = -listed;
4504                    }
4505                }
4506                if *moved {
4507                    let area = self.ui.as_ref().and_then(|u| u.scroll_region(*id));
4508                    let physics = area
4509                        .map(|a| a.physics_or(self.app.scroll_physics()))
4510                        .unwrap_or_else(|| self.app.scroll_physics());
4511                    let cur = self.runtime.scroll.get(id).copied().unwrap_or((0.0, 0.0));
4512                    // The finger pushes the content, so we follow the delta at once —
4513                    // but only as far as the physics allows. Past an edge, bouncing
4514                    // physics resists more and more (the rubber band) while clamping
4515                    // physics refuses the move outright.
4516                    // What the physics refuses is exactly the distance the user asked
4517                    // for and did not get — which is what the glow acknowledges.
4518                    let axis = |metrics: frus_widgets::ScrollMetrics, delta: f32| {
4519                        let applied = physics.apply_user_offset(metrics, delta);
4520                        let proposed = metrics.pixels + applied;
4521                        let refused = physics.apply_boundary_conditions(metrics, proposed);
4522                        (proposed - refused, refused)
4523                    };
4524                    let (nx, ny, refused) = match area {
4525                        Some(area) => {
4526                            let (ddx, ddy) = area.offset_delta((dx, dy));
4527                            let (nx, rx) = axis(area.metrics_x(cur.0), ddx);
4528                            let (ny, ry) = axis(area.metrics_y(cur.1), ddy);
4529                            (nx, ny, Some((area, rx, ry)))
4530                        }
4531                        None => (cur.0 - dx, cur.1 - dy, None),
4532                    };
4533                    if let Some((area, rx, ry)) = refused {
4534                        // A pull-to-refresh area listening above this scrollable takes
4535                        // the **top** edge; the glow takes the other three. Both answer
4536                        // "there is nothing more that way", and giving both would say it
4537                        // twice — the indicator being the more useful of the two, since
4538                        // it leads somewhere.
4539                        if let Some(host) = area.refresh {
4540                            self.feed_refresh(host, physics, area.viewport.height, cur.1, ny, ry);
4541                        }
4542                        let cursor = self.cursor;
4543                        for (refused, vertical, extent) in [
4544                            (rx, false, area.viewport.width),
4545                            (ry, true, area.viewport.height),
4546                        ] {
4547                            if refused.abs() < 1e-3 {
4548                                continue;
4549                            }
4550                            let edge = area.refused_edge(vertical, refused);
4551                            // The refresh area takes the edge the axis **starts** at,
4552                            // which is the bottom of a reversed one.
4553                            if area.refresh.is_some() && edge == area.start_edge(vertical) {
4554                                continue;
4555                            }
4556                            let (offset, cross) =
4557                                frus_widgets::glow_cross_axis(area.viewport, edge, cursor);
4558                            self.runtime
4559                                .glow_pull(area.id, edge, refused, extent, offset, cross);
4560                        }
4561                    }
4562                    self.runtime.scroll.insert(*id, (nx, ny));
4563                    self.runtime.scroll_target.insert(*id, (nx, ny));
4564                    self.runtime.scroll_velocity.remove(id);
4565                    *last = self.cursor;
4566                    self.track_gesture();
4567                }
4568            }
4569            Drag::Dismiss { item, last, moved } => {
4570                let dx = self.cursor.x - last.x;
4571                let dy = self.cursor.y - last.y;
4572                if !*moved && (dx * dx + dy * dy) > slop * slop {
4573                    *moved = true;
4574                }
4575                if *moved {
4576                    let delta = if item.spec.axis.is_horizontal() {
4577                        dx
4578                    } else {
4579                        dy
4580                    };
4581                    self.runtime
4582                        .dismiss_drag(item.id, delta, item.extent(), item.spec.axis);
4583                    *last = self.cursor;
4584                    self.track_gesture();
4585                }
4586            }
4587            Drag::Sheet {
4588                id,
4589                last,
4590                moved,
4591                available,
4592            } => {
4593                let dx = self.cursor.x - last.x;
4594                let dy = self.cursor.y - last.y;
4595                if !*moved && (dx * dx + dy * dy) > slop * slop {
4596                    *moved = true;
4597                }
4598                if *moved && *available > 0.0 {
4599                    let spec = self
4600                        .ui
4601                        .as_ref()
4602                        .and_then(|ui| ui.sheet(*id))
4603                        .map(|sheet| sheet.spec.clone());
4604                    if let Some(spec) = spec {
4605                        // Up grows it: the screen's y runs the other way.
4606                        self.runtime
4607                            .sheet_drag(*id, &spec, -dy / *available, *available);
4608                    }
4609                    *last = self.cursor;
4610                    self.track_gesture();
4611                }
4612            }
4613            Drag::Back { start_x } => {
4614                let width = self.logical_width();
4615                // Under RTL the finger slides **left** from the right edge, so progress
4616                // runs the other way.
4617                let sign = if self.is_rtl() { -1.0 } else { 1.0 };
4618                let progress = (sign * (self.cursor.x - *start_x) / width).clamp(0.0, 1.0);
4619                self.track_gesture();
4620                self.build_dirty = true;
4621                self.app.back_gesture(progress);
4622            }
4623        }
4624        self.drag = Some(drag);
4625        self.request_redraw();
4626    }
4627
4628    /// The refresh area a scrollable sits inside, if any.
4629    fn refresh_host_of(&self, scrollable: WidgetId) -> Option<WidgetId> {
4630        self.ui
4631            .as_ref()
4632            .and_then(|ui| ui.scroll_region(scrollable))
4633            .and_then(|area| area.refresh)
4634    }
4635
4636    /// Ends the pull of whichever refresh area holds `scrollable`, and dispatches its
4637    /// message when the pull was armed.
4638    fn release_refresh(&mut self, scrollable: WidgetId) {
4639        let Some(host) = self.refresh_host_of(scrollable) else {
4640            return;
4641        };
4642        if !self.runtime.refresh_release(host) {
4643            return;
4644        }
4645        let message = self
4646            .tree
4647            .as_ref()
4648            .and_then(|tree| find_widget(tree.as_ref(), host))
4649            .and_then(|widget| widget.on_refresh());
4650        if let Some(message) = message {
4651            self.dispatch(message);
4652        }
4653    }
4654
4655    /// Feeds one move of a drag into the pull of the refresh area `host`.
4656    ///
4657    /// The two physics put the overscroll in **different places**, so the signal is
4658    /// read differently:
4659    ///
4660    /// - **Clamping** refuses the movement and pins the offset at the edge, so the
4661    ///   refused amount is the only trace the gesture leaves. It is incremental, and
4662    ///   the physics returns nothing at all for a move back towards the content — so
4663    ///   an eased-off pull holds rather than retracting, which is right: the finger has
4664    ///   not let go.
4665    /// - **Bouncing** lets the offset go past the edge, so the *depth* it reached is
4666    ///   the signal, and the change in that depth is signed. The indicator therefore
4667    ///   follows the rubber band back in as the finger returns, which is also right:
4668    ///   there, the content itself is already saying so.
4669    ///
4670    /// Leaving the top edge at all ends the pull. The gesture has become an ordinary
4671    /// scroll, and an indicator still hanging there would be promising something the
4672    /// release is no longer going to deliver.
4673    fn feed_refresh(
4674        &mut self,
4675        host: WidgetId,
4676        physics: frus_widgets::ScrollPhysics,
4677        extent: f32,
4678        before: f32,
4679        after: f32,
4680        refused_y: f32,
4681    ) {
4682        if after > 0.5 {
4683            self.runtime.refresh_cancel(host);
4684            return;
4685        }
4686        let pulled = if physics.allows_overscroll() {
4687            (-after).max(0.0) - (-before).max(0.0)
4688        } else {
4689            -refused_y
4690        };
4691        if pulled.abs() > 1e-3 {
4692            self.runtime.refresh_pull(host, pulled, extent);
4693        }
4694    }
4695
4696    /// How far the pointer must travel before a press becomes a drag, given what
4697    /// kind of pointer it is.
4698    fn hit_slop(&self) -> f32 {
4699        if self.pointer_touch {
4700            TOUCH_SLOP
4701        } else {
4702            PRECISE_SLOP
4703        }
4704    }
4705
4706    /// Seconds since the drag under way began — the clock the velocity tracker's
4707    /// samples are stamped with.
4708    fn gesture_now(&self) -> f32 {
4709        (Instant::now() - self.gesture_start).as_secs_f32()
4710    }
4711
4712    /// Starts a fresh gesture: the history of the previous one must not leak into
4713    /// the next, or a flick left then right would fling the wrong way.
4714    fn begin_gesture(&mut self) {
4715        self.gesture_velocity = VelocityTracker::platform_default();
4716        self.gesture_start = Instant::now();
4717        self.track_gesture();
4718    }
4719
4720    /// Records where the pointer is now.
4721    fn track_gesture(&mut self) {
4722        let now = self.gesture_now();
4723        self.gesture_velocity.add_position(now, self.cursor);
4724    }
4725
4726    /// What the gesture tracker makes of the drag as it stands.
4727    fn gesture_estimate(&self) -> VelocityEstimate {
4728        self.gesture_velocity
4729            .estimate(self.gesture_now())
4730            .unwrap_or(VelocityEstimate::STILL)
4731    }
4732
4733    /// [`fling_velocity`], gated on this pointer's slop.
4734    fn fling_velocity(&self, estimate: VelocityEstimate) -> (f32, f32) {
4735        fling_velocity(estimate, self.hit_slop())
4736    }
4737
4738    /// A scroll fling: projects each axis's ballistic destination, friction in closed
4739    /// form, bounds it with the elastic overshoot, and primes the scroll spring with
4740    /// the finger's momentum.
4741    fn fling(&mut self, id: WidgetId, velocity: (f32, f32)) {
4742        let Some(area) = self.ui.as_ref().and_then(|ui| ui.scroll_region(id)) else {
4743            return;
4744        };
4745        let physics = area.physics_or(self.app.scroll_physics());
4746        // The physics decides everything from here: whether there is a fling at all,
4747        // how far it runs, and what happens at the edges. A release too slow to
4748        // fling still gets a chance to spring an overscroll back.
4749        if self.runtime.fling_scroll(area, physics, velocity) {
4750            self.request_redraw();
4751        }
4752    }
4753
4754    /// The topmost **reorderable** widget under `point`, as `(id, flat index)`. It
4755    /// uses the reorderables' registry, which is independent of clicking, and so covers
4756    /// the Kanban cards and drop zones, which are not clickable.
4757    fn reorderable_at(&self, point: Point) -> Option<(WidgetId, usize, bool)> {
4758        let id = self.ui.as_ref()?.reorderable_at(point)?;
4759        let widget = self
4760            .tree
4761            .as_ref()
4762            .and_then(|tree| find_widget(tree.as_ref(), id))?;
4763        // A target-**only** widget, a drop zone: reorderable but not grabbable, so no
4764        // drag starts on it. Dropping still aims at it, through `ui.reorderable_at`.
4765        if !widget.reorder_draggable() {
4766            return None;
4767        }
4768        let from = widget.reorder_index()?;
4769        Some((id, from, widget.drag_needs_long_press()))
4770    }
4771
4772    /// **What is actually moving**, given what was grabbed: its id and its box.
4773    ///
4774    /// Usually they are the same widget. They are not when what was grabbed is a **grip**
4775    /// — a source that is not a target — because a grip is a 40-pixel gutter inside a much
4776    /// larger row, and everything the preview is made of is the row's: the ghost is the
4777    /// row lifted out of the frame, the gap that closes behind it is the row's height, and
4778    /// the band the neighbours are matched against is the row's width. Taking the grip's
4779    /// box instead would lift an icon and open a slot two centimetres too narrow.
4780    ///
4781    /// The row is found by the index the two share, among the reorderables under the
4782    /// grip's own middle — which is inside its row by construction.
4783    fn reorder_source(&self, id: WidgetId, from: usize) -> Option<(WidgetId, Rect)> {
4784        let ui = self.ui.as_ref()?;
4785        let tree = self.tree.as_ref()?;
4786        let own = ui.widget_rect(id)?;
4787        let grabbed = find_widget(tree.as_ref(), id)?;
4788        if grabbed.reorder_droppable() {
4789            return Some((id, own));
4790        }
4791        let middle = Point::new(own.x + own.width * 0.5, own.y + own.height * 0.5);
4792        let slot = ui.reorderables_at(middle).find(|other| {
4793            find_widget(tree.as_ref(), *other)
4794                .is_some_and(|w| w.reorder_droppable() && w.reorder_index() == Some(from))
4795        })?;
4796        // A grip whose row is nowhere to be found still moves something: itself, which is
4797        // wrong-looking but not a crash, and cannot happen while the two are built
4798        // together.
4799        Some((slot, ui.widget_rect(slot).unwrap_or(own)))
4800    }
4801
4802    /// The row the reorder under way carries, and its box: the one taken when the drag was
4803    /// first carried, moved with the content since — or, before any frame has carried it,
4804    /// the frame's.
4805    fn reorder_carried(&self) -> Option<(WidgetId, Rect)> {
4806        match &self.drag {
4807            Some(Drag::Reorder {
4808                carried: Some(carried),
4809                ..
4810            }) => Some(*carried),
4811            Some(Drag::Reorder { id, from, .. }) => self.reorder_source(*id, *from),
4812            _ => None,
4813        }
4814    }
4815
4816    /// Takes the carried row's box from the frame, once: the first frame a reorder is carried
4817    /// in, before anything has scrolled it. From then on it goes with the content, and the
4818    /// frame is no longer asked — a row carried to an edge is clipped by it, and a row
4819    /// scrolled out of sight is not in the frame at all.
4820    fn keep_carried_box(&mut self) {
4821        let source = match &self.drag {
4822            Some(Drag::Reorder {
4823                id,
4824                from,
4825                moved: true,
4826                carried: None,
4827                ..
4828            }) => self.reorder_source(*id, *from),
4829            _ => return,
4830        };
4831        if let Some(Drag::Reorder { carried, .. }) = self.drag.as_mut() {
4832            *carried = source;
4833        }
4834    }
4835
4836    /// The topmost reorderable under `point` that something can actually be **dropped**
4837    /// on. It is not always the topmost reorderable: a list row's grip is a source and
4838    /// not a target, and what is under it is the row the drop is really aimed at.
4839    fn reorder_target_at(&self, point: Point) -> Option<WidgetId> {
4840        let ui = self.ui.as_ref()?;
4841        let tree = self.tree.as_ref()?;
4842        ui.reorderables_at(point)
4843            .find(|id| find_widget(tree.as_ref(), *id).is_some_and(|w| w.reorder_droppable()))
4844    }
4845
4846    /// Where the reorderable `id`, grabbed at index `from`, is **dropped** if released now:
4847    /// the target under the pointer, or — over no target at all — the nearest slot of its own
4848    /// list, for one inserted between its neighbours, along the axis it is reordered on.
4849    ///
4850    /// The insertion line and the release both ask this, so the line cannot promise a place
4851    /// the drop does not keep. The second half is milestone 518: a row carried to the bottom
4852    /// of a phone's list is over what follows the list, and the release used to put it back.
4853    /// Milestone 527 turned it on its side, for a list whose rows run across.
4854    fn reorder_drop_target(
4855        &self,
4856        id: WidgetId,
4857        from: usize,
4858        carried: Option<(WidgetId, Rect)>,
4859    ) -> Option<WidgetId> {
4860        if let Some(target) = self.reorder_target_at(self.cursor) {
4861            return Some(target);
4862        }
4863        let tree = self.tree.as_ref()?;
4864        let Some(ReorderMotion::Slots(axis)) = find_widget(tree.as_ref(), id).map(reorder_motion)
4865        else {
4866            return None;
4867        };
4868        // Among the slots of the row that moves, which is not the grip that was grabbed —
4869        // and which may have been scrolled out of the frame by the carry itself.
4870        let row = match carried {
4871            Some((row, _)) => row,
4872            None => self.reorder_source(id, from)?.0,
4873        };
4874        let slots = reorder_siblings(self.ui.as_ref()?, tree.as_ref(), row);
4875        let boxes: Vec<Rect> = slots.iter().map(|(_, rect)| *rect).collect();
4876        nearest_reorder_slot(self.cursor, &boxes, axis).map(|index| slots[index].0)
4877    }
4878
4879    /// What is being **carried** this frame, where it is now: the box the ghost is drawn
4880    /// at, for a reorder or for a lifted item. `None` when nothing is engaged.
4881    ///
4882    /// It is the ghost's box and not the pointer, because the reference scrolls when the
4883    /// *item* reaches the edge, and that is the honest moment: a row half off the bottom
4884    /// is already asking to go further, whatever the finger holding it is over.
4885    fn carried_rect(&self) -> Option<Rect> {
4886        match &self.drag {
4887            Some(Drag::Reorder {
4888                start, moved: true, ..
4889            }) => {
4890                let (_, src) = self.reorder_carried()?;
4891                // The same offsets the ghost is painted at, and for the same reason a
4892                // column's ghost only rises: it moves along its own axis.
4893                let (gx, gy) = ghost_offset(
4894                    self.dragged_reorder_motion()
4895                        .unwrap_or(ReorderMotion::Columns),
4896                    (self.cursor.x - start.x, self.cursor.y - start.y),
4897                );
4898                Some(src.translate(gx, gy))
4899            }
4900            Some(Drag::Item {
4901                source,
4902                start,
4903                moved: true,
4904                ..
4905            }) => Some(
4906                source
4907                    .rect
4908                    .translate(self.cursor.x - start.x, self.cursor.y - start.y),
4909            ),
4910            _ => None,
4911        }
4912    }
4913
4914    /// Scrolls the area under the pointer while what is being carried hangs past one of
4915    /// its edges — the gap `Draggable` has had since milestone 285, and what a list longer
4916    /// than a screen needs before it can be reordered at all.
4917    ///
4918    /// The area is the innermost one under the **pointer** that a user could move by
4919    /// hand: an area that refuses a finger refuses this too, so a list that fits its
4920    /// viewport never twitches.
4921    fn autoscroll_carried(&mut self, dt: f32) -> bool {
4922        self.keep_carried_box();
4923        let Some(carried) = self.carried_rect() else {
4924            return false;
4925        };
4926        let area = {
4927            let scroll = &self.runtime.scroll;
4928            self.ui.as_ref().and_then(|ui| {
4929                ui.scroll_chain(self.cursor).find(|area| {
4930                    area.accepts_user_offset(scroll.get(&area.id).copied().unwrap_or((0.0, 0.0)))
4931                })
4932            })
4933        };
4934        let Some(area) = area else {
4935            return false;
4936        };
4937        let offset = self
4938            .runtime
4939            .scroll
4940            .get(&area.id)
4941            .copied()
4942            .unwrap_or((0.0, 0.0));
4943        // What is still hidden on either side, in **screen** terms: on a reversed axis
4944        // the offset is measured from the other end, so the two swap over.
4945        let room = |offset: f32, max: f32, reverse: bool| {
4946            if reverse {
4947                (max - offset, offset)
4948            } else {
4949                (offset, max - offset)
4950            }
4951        };
4952        let view = area.viewport;
4953        let dx = edge_autoscroll(
4954            (carried.x, carried.x + carried.width),
4955            (view.x, view.x + view.width),
4956            room(offset.0, area.max_x, area.reverse_x),
4957            dt,
4958        )
4959        .unwrap_or(0.0);
4960        let dy = edge_autoscroll(
4961            (carried.y, carried.y + carried.height),
4962            (view.y, view.y + view.height),
4963            room(offset.1, area.max_y, area.reverse_y),
4964            dt,
4965        )
4966        .unwrap_or(0.0);
4967        if dx == 0.0 && dy == 0.0 {
4968            return false;
4969        }
4970        // A movement of the content becomes a change of offset the area's own way, which
4971        // is the one place the sign of a reversed axis is decided.
4972        let delta = area.offset_delta((dx, dy));
4973        let next = (
4974            (offset.0 + delta.0).clamp(0.0, area.max_x),
4975            (offset.1 + delta.1).clamp(0.0, area.max_y),
4976        );
4977        if next == offset {
4978            return false;
4979        }
4980        self.runtime.scroll.insert(area.id, next);
4981        // The target follows, or the inertia the area was resting at would spring the
4982        // list straight back out from under the row.
4983        self.runtime.scroll_target.insert(area.id, next);
4984        self.runtime.scroll_velocity.remove(&area.id);
4985        // What is carried stays under the finger. Its ghost is drawn at the box the frame
4986        // puts it at, offset by how far the finger has moved since the press; the frame now
4987        // puts that box where the content went, so the press goes there too. Otherwise the
4988        // ghost rides up with the list, stops hanging over the edge, and the scroll it
4989        // started starves itself — seen on a phone, milestone 517.
4990        if let Some(drag) = self.drag.as_mut() {
4991            follow_content(
4992                drag,
4993                area.offset_delta((next.0 - offset.0, next.1 - offset.1)),
4994            );
4995        }
4996        true
4997    }
4998
4999    /// How the reorderable currently **grabbed** moves, if a drag is under way. It is what
5000    /// keeps the **pointer spring** (`reorder_x` following the pointer) to `Table`'s columns;
5001    /// the Kanban cards and a list's rows spring their insertion line instead.
5002    fn dragged_reorder_motion(&self) -> Option<ReorderMotion> {
5003        let Some(Drag::Reorder { id, .. }) = self.drag else {
5004            return None;
5005        };
5006        self.tree
5007            .as_ref()
5008            .and_then(|t| find_widget(t.as_ref(), id))
5009            .map(reorder_motion)
5010    }
5011
5012    /// Steps the reorder springs one frame, per gesture — see the frame loop — and says
5013    /// whether they are still moving.
5014    fn advance_reorder_springs(&mut self, dt: f32) -> bool {
5015        let reorder_motion = matches!(self.drag, Some(Drag::Reorder { moved: true, .. }))
5016            .then(|| self.dragged_reorder_motion())
5017            .flatten();
5018        match reorder_motion {
5019            Some(ReorderMotion::Columns) => {
5020                self.reorder_x = spring_toward(self.reorder_x, self.cursor.x, dt, 0.07);
5021                (self.cursor.x - self.reorder_x).abs() > 0.5
5022            }
5023            Some(ReorderMotion::Slots(ReorderAxis::Vertical)) => {
5024                match self.reorder_drop_line(drag_preview::INSERT_THICKNESS) {
5025                    Some(target) => {
5026                        self.reorder_y = spring_toward(self.reorder_y, target.y, dt, 0.07);
5027                        (target.y - self.reorder_y).abs() > 0.5
5028                    }
5029                    None => false,
5030                }
5031            }
5032            Some(ReorderMotion::Slots(ReorderAxis::Horizontal)) => {
5033                match self.reorder_drop_line(drag_preview::INSERT_THICKNESS) {
5034                    Some(target) => {
5035                        self.reorder_x = spring_toward(self.reorder_x, target.x, dt, 0.07);
5036                        (target.x - self.reorder_x).abs() > 0.5
5037                    }
5038                    None => false,
5039                }
5040            }
5041            None => false,
5042        }
5043    }
5044
5045    /// Paints the **reorder preview** on top of the scene, unclipped: the source
5046    /// column dimmed, a **drop indicator** at the target column's insertion edge, and a
5047    /// **lifted card** — shadow plus a `primary` border — following the pointer. No
5048    /// effect outside an engaged header drag.
5049    /// Names, on the console, every box this frame whose children did not fit inside it.
5050    ///
5051    /// The reference treats an overflowing flex as an error condition, paints a striped
5052    /// band along the offending edge and writes to the console; nothing here said anything
5053    /// at all, which is how a task row's delete button came to be laid out past the window
5054    /// and stayed there for three milestones — drawn nowhere, hittable nowhere. Half of
5055    /// that is now here: the words, once per site, with the two ways out the reference
5056    /// suggests. The striped band is not.
5057    ///
5058    /// Once per site, because a layout that does not fit does not fit on every frame, and
5059    /// sixty lines a second is the same as silence.
5060    fn report_overflows(&self, ui: &Ui<A::Message>) {
5061        for o in ui.overflows() {
5062            // The site: a box, an edge, and the amount rounded to the pixel. Enough to
5063            // stay quiet while a window is resized past the same defect, and to speak up
5064            // when the defect changes.
5065            let mut key = std::collections::hash_map::DefaultHasher::new();
5066            use std::hash::{Hash, Hasher};
5067            (
5068                o.rect.width.to_bits(),
5069                o.rect.height.to_bits(),
5070                o.side as u8,
5071                o.amount.round() as i32,
5072            )
5073                .hash(&mut key);
5074            if !self.reported_overflows.borrow_mut().insert(key.finish()) {
5075                continue;
5076            }
5077            let side = match o.side {
5078                frus_widgets::Side::Left => "left",
5079                frus_widgets::Side::Right => "right",
5080                frus_widgets::Side::Top => "top",
5081                frus_widgets::Side::Bottom => "bottom",
5082            };
5083            log::warn!(
5084                "a box {:.0}x{:.0} is overflowed by {:.0} px on the {side}: its children                  do not fit inside it, and what runs past the edge is drawn outside its                  parent — invisible where something clips it, and untappable where it                  leaves the window. Give the child that should give way an `Expanded`,                  or put the content in a `SingleChildScrollView`.",
5085                o.rect.width,
5086                o.rect.height,
5087                o.amount,
5088            );
5089        }
5090    }
5091
5092    /// Winit's key event as a [`KeyStroke`] — the general vocabulary shortcuts are bound
5093    /// to, as opposed to [`Key`], which is the one text editing needs.
5094    ///
5095    /// `None` for anything with no place in a shortcut: a dead key, a compose sequence, a
5096    /// modifier pressed on its own.
5097    fn keystroke(&self, event: &winit::event::KeyEvent) -> Option<KeyStroke> {
5098        let key = match &event.logical_key {
5099            WinitKey::Named(NamedKey::Enter) => ShortcutKey::Enter,
5100            WinitKey::Named(NamedKey::Escape) => ShortcutKey::Escape,
5101            WinitKey::Named(NamedKey::Tab) => ShortcutKey::Tab,
5102            WinitKey::Named(NamedKey::Space) => ShortcutKey::Space,
5103            WinitKey::Named(NamedKey::Backspace) => ShortcutKey::Backspace,
5104            WinitKey::Named(NamedKey::Delete) => ShortcutKey::Delete,
5105            WinitKey::Named(NamedKey::ArrowUp) => ShortcutKey::Up,
5106            WinitKey::Named(NamedKey::ArrowDown) => ShortcutKey::Down,
5107            WinitKey::Named(NamedKey::ArrowLeft) => ShortcutKey::Left,
5108            WinitKey::Named(NamedKey::ArrowRight) => ShortcutKey::Right,
5109            WinitKey::Named(NamedKey::Home) => ShortcutKey::Home,
5110            WinitKey::Named(NamedKey::End) => ShortcutKey::End,
5111            WinitKey::Named(NamedKey::PageUp) => ShortcutKey::PageUp,
5112            WinitKey::Named(NamedKey::PageDown) => ShortcutKey::PageDown,
5113            WinitKey::Named(NamedKey::F1) => ShortcutKey::F(1),
5114            WinitKey::Named(NamedKey::F2) => ShortcutKey::F(2),
5115            WinitKey::Named(NamedKey::F3) => ShortcutKey::F(3),
5116            WinitKey::Named(NamedKey::F4) => ShortcutKey::F(4),
5117            WinitKey::Named(NamedKey::F5) => ShortcutKey::F(5),
5118            WinitKey::Named(NamedKey::F6) => ShortcutKey::F(6),
5119            WinitKey::Named(NamedKey::F7) => ShortcutKey::F(7),
5120            WinitKey::Named(NamedKey::F8) => ShortcutKey::F(8),
5121            WinitKey::Named(NamedKey::F9) => ShortcutKey::F(9),
5122            WinitKey::Named(NamedKey::F10) => ShortcutKey::F(10),
5123            WinitKey::Named(NamedKey::F11) => ShortcutKey::F(11),
5124            WinitKey::Character(text) => ShortcutKey::Char(text.chars().next()?),
5125            _ => return None,
5126        };
5127        Some(KeyStroke {
5128            key,
5129            ctrl: self.ctrl,
5130            shift: self.shift,
5131            alt: self.alt,
5132            meta: self.meta,
5133        })
5134    }
5135
5136    fn paint_reorder_preview(&self, ui: &Ui<A::Message>, theme: &Theme, scene: &mut Scene) {
5137        let Some(Drag::Reorder {
5138            start, moved: true, ..
5139        }) = self.drag
5140        else {
5141            return;
5142        };
5143        // What moves is the row, even when what was grabbed is the grip inside it — at the
5144        // box it was carried from, which has gone with the content.
5145        let Some((id, src)) = self.reorder_carried() else {
5146            return;
5147        };
5148        let motion = self
5149            .tree
5150            .as_ref()
5151            .and_then(|t| find_widget(t.as_ref(), id))
5152            .map(reorder_motion)
5153            .unwrap_or(ReorderMotion::Columns);
5154        let dx = self.cursor.x - start.x;
5155        let dy = self.cursor.y - start.y;
5156
5157        // The ghost's offset, per motion — see `ghost_offset`.
5158        let (gx, gy) = ghost_offset(motion, (dx, dy));
5159
5160        // The owners of the grabbed item's **subtree**: used by the ghost, to capture a
5161        // rich card's content, which its children paint (milestone 251), **and** by the
5162        // vertical reflow, to lift the card out of the preview. Fallback: the card alone.
5163        let owners: std::collections::HashSet<u64> = self
5164            .tree
5165            .as_ref()
5166            .and_then(|t| find_widget(t.as_ref(), id))
5167            .map(|w| subtree_ids(w, id).iter().map(|i| i.as_u64()).collect())
5168            .unwrap_or_else(|| std::iter::once(id.as_u64()).collect());
5169
5170        match motion {
5171            ReorderMotion::Columns => {
5172                // Reflow the neighbouring columns: the source's gap closes and the drop
5173                // slot opens, following the pointer's **smoothed** abscissa — a gentle
5174                // inertial slide, while the ghost sticks to the real pointer.
5175                let reflowed =
5176                    reflow_reorder_columns(scene.primitives(), src, self.reorder_x, id.as_u64());
5177                scene.clear();
5178                for primitive in reflowed {
5179                    scene.push_primitive(primitive);
5180                }
5181            }
5182            ReorderMotion::Slots(axis) => {
5183                // Reflow the **cards**: the lifted card's gap closes in the source
5184                // column and a slot opens under the **insertion line** in the target one.
5185                // Then the line is laid on top, at the chosen edge — the hovered half,
5186                // milestone 252.
5187                //
5188                // A **smoothed** line (milestone 265): the chosen slot's width, abscissa
5189                // and thickness are kept, but the **ordinate** is replaced by the
5190                // `reorder_y` spring — the line *and* the gap slide between cards, with
5191                // vertical inertia, instead of jumping a notch. A horizontal list's line
5192                // stands on its end, and its **abscissa** is the one that springs.
5193                let line =
5194                    self.reorder_drop_line(drag_preview::INSERT_THICKNESS)
5195                        .map(|r| match axis {
5196                            ReorderAxis::Vertical => Rect {
5197                                y: self.reorder_y,
5198                                ..r
5199                            },
5200                            ReorderAxis::Horizontal => Rect {
5201                                x: self.reorder_x,
5202                                ..r
5203                            },
5204                        });
5205                // Only what can be reordered makes room — a card, a row, a drop zone, and
5206                // what each of them paints. The reflow is geometric, and a button floating
5207                // over the list or the navigation bar under it shares the band without being
5208                // part of the list: moved with it, the first left its `+` a row above its own
5209                // disc on a phone (milestone 517).
5210                let movable = self
5211                    .tree
5212                    .as_ref()
5213                    .map(|tree| reorderable_owners(ui, tree.as_ref()))
5214                    .unwrap_or_default();
5215                let reflowed =
5216                    reflow_reorder_cards(scene.primitives(), src, line, &owners, &movable, axis);
5217                scene.clear();
5218                for primitive in reflowed {
5219                    scene.push_primitive(primitive);
5220                }
5221                if let Some(line) = line {
5222                    // Rounded across its thickness, whichever way it stands.
5223                    let thickness = match axis {
5224                        ReorderAxis::Vertical => line.height,
5225                        ReorderAxis::Horizontal => line.width,
5226                    };
5227                    scene.set_clip(Rect::UNBOUNDED);
5228                    scene.draw_rect(
5229                        line,
5230                        theme.primary,
5231                        theme.radius.min(thickness * 0.5),
5232                        0.0,
5233                        Color::TRANSPARENT,
5234                    );
5235                }
5236            }
5237        }
5238
5239        // A faithful ghost: the grabbed item's primitives, taken from the original
5240        // scene, translated and **un-clipped**, since they would otherwise be cropped at
5241        // the source. In a **rich card** the background is painted by the card but its
5242        // content — label, tags, the × button — by children, under other owners, so we
5243        // capture the primitives of the item's **whole subtree**; see `owners` above.
5244        let ghost: Vec<Primitive> = ui
5245            .scene()
5246            .primitives()
5247            .iter()
5248            .filter(|p| owners.contains(&p.owner()))
5249            .map(|p| p.translated(gx, gy).with_clip(Rect::UNBOUNDED))
5250            .collect();
5251        draw_ghost_card(scene, theme, src.translate(gx, gy), &ghost);
5252    }
5253
5254    /// The lifted item: its own primitives dimmed where it sits, and a copy of them
5255    /// floating under the pointer.
5256    ///
5257    /// The copy is taken from the frame rather than built again, so it cannot drift
5258    /// from what is on screen — a rebuilt "feedback" widget is a second definition of
5259    /// the same thing, and two definitions is one too many. The same reason the
5260    /// reorder ghost works this way.
5261    fn paint_drag_ghost(&self, ui: &Ui<A::Message>, theme: &Theme, scene: &mut Scene) {
5262        let Some(Drag::Item {
5263            source,
5264            start,
5265            moved: true,
5266            ..
5267        }) = &self.drag
5268        else {
5269            return;
5270        };
5271        let widget = self
5272            .tree
5273            .as_ref()
5274            .and_then(|tree| find_widget(tree.as_ref(), source.id));
5275        // A rich item paints its background itself and its content through children,
5276        // under other owners, so the whole subtree is captured — the same reason the
5277        // reorder ghost does.
5278        let owners: std::collections::HashSet<u64> = widget
5279            .map(|w| {
5280                subtree_ids(w, source.id)
5281                    .iter()
5282                    .map(|i| i.as_u64())
5283                    .collect()
5284            })
5285            .unwrap_or_else(|| std::iter::once(source.id.as_u64()).collect());
5286        let opacity = widget.map(|w| w.drag_ghost_opacity()).unwrap_or(1.0);
5287        let dx = self.cursor.x - start.x;
5288        let dy = self.cursor.y - start.y;
5289
5290        // What is left behind, faded in place: the item is being carried, not deleted.
5291        let original: Vec<Primitive> = scene.primitives().to_vec();
5292        scene.clear();
5293        for primitive in &original {
5294            if owners.contains(&primitive.owner()) {
5295                scene.push_faded(primitive, opacity);
5296            } else {
5297                scene.push_primitive(primitive.clone());
5298            }
5299        }
5300
5301        let ghost: Vec<Primitive> = ui
5302            .scene()
5303            .primitives()
5304            .iter()
5305            .filter(|p| owners.contains(&p.owner()))
5306            .map(|p| p.translated(dx, dy).with_clip(Rect::UNBOUNDED))
5307            .collect();
5308        draw_ghost_card(scene, theme, source.rect.translate(dx, dy), &ghost);
5309    }
5310
5311    /// The **insertion** line of the preview: a thin band at the edge of the hovered
5312    /// reorderable slot, a card, a row or a drop zone — the edge **before** it when the
5313    /// pointer is in its leading half, the edge **after** it in its trailing half. Across a
5314    /// vertical list; standing on its end in a horizontal one. `None` when the release would
5315    /// land nowhere.
5316    fn reorder_drop_line(&self, thickness: f32) -> Option<Rect> {
5317        let Some(Drag::Reorder {
5318            id, from, carried, ..
5319        }) = self.drag
5320        else {
5321            return None;
5322        };
5323        // The reorderable slot — card, row or drop zone — the release would land on: the
5324        // one under the pointer, or the nearest of the list's own.
5325        let target = self.reorder_drop_target(id, from, carried)?;
5326        let rect = self.ui.as_ref()?.widget_rect(target)?;
5327        let axis = match self
5328            .tree
5329            .as_ref()
5330            .and_then(|t| find_widget(t.as_ref(), target))
5331            .map(reorder_motion)
5332        {
5333            Some(ReorderMotion::Slots(axis)) => axis,
5334            _ => ReorderAxis::Vertical,
5335        };
5336        Some(drop_insertion_line(
5337            rect,
5338            thickness,
5339            self.reorder_insert_after(target, rect),
5340            axis,
5341            self.is_rtl(),
5342        ))
5343    }
5344
5345    /// For a slot a reorderable is **inserted** at, tells whether insertion happens
5346    /// **after** it (index +1, between it and the next) — the pointer in its trailing half,
5347    /// which `reorder_drop_after` works out along the list's axis and reading direction.
5348    /// Always `false` for `Table`'s columns, which keep their drop logic unchanged. This is
5349    /// the insertion line's counterpart on the **routing** side.
5350    fn reorder_insert_after(&self, target: WidgetId, rect: Rect) -> bool {
5351        match self
5352            .tree
5353            .as_ref()
5354            .and_then(|t| find_widget(t.as_ref(), target))
5355            .map(reorder_motion)
5356        {
5357            Some(ReorderMotion::Slots(axis)) => {
5358                reorder_drop_after(self.cursor, rect, axis, self.is_rtl())
5359            }
5360            _ => false,
5361        }
5362    }
5363
5364    /// Sends a value drag's **start** or **end** to the widget being dragged.
5365    ///
5366    /// The fraction is worked out the same way [`apply_widget_drag`](Self::apply_widget_drag)
5367    /// works it out, from the same rectangle: an end that disagreed with the last
5368    /// `on_drag` about where the pointer finished would be a slider that settles
5369    /// somewhere its own final message never mentioned.
5370    fn dispatch_drag_edge(&mut self, id: WidgetId, rect: frus_widgets::Rect, edge: Edge) {
5371        let fraction = drag_fraction(rect, self.cursor.x);
5372        let message = self
5373            .tree
5374            .as_ref()
5375            .and_then(|tree| find_widget(tree.as_ref(), id))
5376            .and_then(|widget| match edge {
5377                Edge::Start => widget.on_drag_start(fraction),
5378                Edge::End => widget.on_drag_end(fraction),
5379            });
5380        if let Some(message) = message {
5381            self.dispatch(message);
5382        }
5383    }
5384
5385    fn apply_widget_drag(&mut self, id: WidgetId, rect: frus_widgets::Rect, dx: f32) {
5386        let fraction = drag_fraction(rect, self.cursor.x);
5387        // An accumulating handle, taking a delta, first; otherwise a slider's absolute
5388        // fraction.
5389        let message = self
5390            .tree
5391            .as_ref()
5392            .and_then(|tree| find_widget(tree.as_ref(), id))
5393            .and_then(|widget| {
5394                widget
5395                    .on_drag_delta(dx)
5396                    .or_else(|| widget.on_drag(fraction))
5397            });
5398        if let Some(message) = message {
5399            self.dispatch(message);
5400        }
5401    }
5402
5403    /// The **system** back — Android's `KEYCODE_BACK`, the browser's back key — and
5404    /// its native chain: ① the topmost overlay closes, be it a sheet, a drawer, a modal
5405    /// or a menu; ② failing that a screen is popped, by replaying the **back gesture
5406    /// already committed**, through the same hooks as the swipe, so it settles with an
5407    /// animation; ③ failing that, at the root, back **quits the application**, as
5408    /// Android expects.
5409    fn system_back(&mut self, event_loop: &ActiveEventLoop) {
5410        if let Some(message) = self.ui.as_ref().and_then(|ui| ui.top_dismiss()) {
5411            self.dispatch(message);
5412            self.request_redraw();
5413            return;
5414        }
5415        if self.app.can_go_back() {
5416            self.build_dirty = true;
5417            self.app.back_gesture(0.0);
5418            // A "committed" momentum: the projection passes the commit threshold and
5419            // the settle animates the pop.
5420            self.app.back_gesture_end(5.0);
5421            self.request_redraw();
5422            return;
5423        }
5424        event_loop.exit();
5425    }
5426
5427    /// Routes **Escape**: a leaf-to-root walk from the focused widget, with a
5428    /// three-state result — `Ignored` keeps walking up, `Handled` consumes, `Skip` stops
5429    /// with no fallback — then falls back to closing the topmost overlay when nobody
5430    /// answered, or when nothing is focused.
5431    fn escape(&mut self) {
5432        // 1) Walk up the focus path. `Some(None)` means consumed with no message; an
5433        // outer `None` means the whole path ignored it, so we fall back.
5434        let outcome: Option<Option<A::Message>> = self.runtime.input.focused.and_then(|focused| {
5435            let tree = self.tree.as_ref()?;
5436            let path = find_path(tree.as_ref(), focused);
5437            for widget in path.iter().rev() {
5438                match widget.on_key(&Key::Escape) {
5439                    KeyResponse::Handled(message) => return Some(message),
5440                    KeyResponse::Skip => return Some(None),
5441                    KeyResponse::Ignored => {}
5442                }
5443            }
5444            None
5445        });
5446
5447        match outcome {
5448            Some(message) => {
5449                if let Some(message) = message {
5450                    self.dispatch(message);
5451                    self.request_redraw();
5452                }
5453            }
5454            // 2) Nobody on the path, or nothing focused: the topmost overlay.
5455            None => {
5456                if let Some(message) = self.ui.as_ref().and_then(|ui| ui.top_dismiss()) {
5457                    self.dispatch(message);
5458                    self.request_redraw();
5459                }
5460            }
5461        }
5462    }
5463
5464    /// Moves the caret vertically in the focused multi-line field — Up/Down when
5465    /// `page` is false, PgUp/PgDn otherwise — preserving the **remembered goal column**.
5466    /// Applies the selection under Shift and reveals the caret. Returns `true` when the
5467    /// caret moved within the field, `false` otherwise, in which case the shell
5468    /// navigates focus instead.
5469    fn move_caret_vertical(&mut self, id: WidgetId, down: bool, page: bool) -> bool {
5470        let width = self
5471            .ui
5472            .as_ref()
5473            .and_then(|ui| ui.widget_rect(id))
5474            .map(|r| r.width)
5475            .unwrap_or(0.0);
5476        let cursor = self.runtime.edits.get(&id).map(|e| e.cursor).unwrap_or(0);
5477        let moved = self
5478            .tree
5479            .as_ref()
5480            .and_then(|tree| find_widget(tree.as_ref(), id))
5481            .and_then(|widget| widget.caret_vertical(width, cursor, down, page, self.goal_x));
5482        let Some((new_cursor, goal)) = moved else {
5483            return false;
5484        };
5485        let shift = self.shift;
5486        let edit = self.runtime.edits.entry(id).or_default();
5487        if shift {
5488            if edit.anchor.is_none() {
5489                edit.anchor = Some(edit.cursor);
5490            }
5491        } else {
5492            edit.anchor = None;
5493        }
5494        edit.cursor = new_cursor;
5495        // Remember the goal column for the next vertical jump.
5496        self.goal_x = Some(goal);
5497        self.reveal_caret(id, new_cursor);
5498        self.request_redraw();
5499        true
5500    }
5501
5502    /// A hold in field `id` selects the word under the finger — the press has already
5503    /// put the caret there — and gives the selection its handles (milestone 511).
5504    fn select_held_word(&mut self, id: WidgetId) {
5505        let cursor = self.runtime.edits.get(&id).map(|e| e.cursor).unwrap_or(0);
5506        let word = self
5507            .tree
5508            .as_ref()
5509            .and_then(|tree| find_widget(tree.as_ref(), id))
5510            .and_then(|widget| widget.word_at(cursor))
5511            .filter(|(start, end)| start < end);
5512        let Some((start, end)) = word else {
5513            return;
5514        };
5515        self.runtime.edits.insert(
5516            id,
5517            Edit {
5518                cursor: end,
5519                anchor: Some(start),
5520                composing: None,
5521            },
5522        );
5523        self.runtime.selection_handles = Some(id);
5524        self.runtime.close_edit_run(id);
5525        #[cfg(android)]
5526        self.push_ime_context(id);
5527    }
5528
5529    /// The selection handle under the pointer, taken: a drag that moves its end of the
5530    /// focused field's selection. `None` when no handle is showing there.
5531    fn grab_selection_handle(&self) -> Option<Drag> {
5532        let id = self.runtime.selection_handles?;
5533        if self.runtime.input.focused != Some(id) {
5534            return None;
5535        }
5536        let rect = self.ui.as_ref()?.widget_rect(id)?;
5537        let edit = self.runtime.edits.get(&id).copied()?;
5538        let scroll_y = self.runtime.scroll.get(&id).map(|s| s.1).unwrap_or(0.0);
5539        let handles = find_widget(self.tree.as_ref()?.as_ref(), id)?
5540            .selection_handles(rect.width, &edit, scroll_y)?;
5541        let local = Point::new(self.cursor.x - rect.x, self.cursor.y - rect.y);
5542        let handle = crate::selection::grab(&handles, local)?;
5543        let at = handles[handle.index()].line_center;
5544        Some(Drag::SelectionHandle {
5545            id,
5546            rect,
5547            handle,
5548            grab: Point::new(at.x - local.x, at.y - local.y),
5549        })
5550    }
5551
5552    /// Routes a key to the focused field: updates the editing state and applies
5553    /// whatever message comes out, a value change or a submission.
5554    fn apply_key(&mut self, id: WidgetId, key: Key) {
5555        // Any horizontal move, or any keystroke, forgets the vertical goal column.
5556        self.goal_x = None;
5557        // Typing puts a touch selection's handles away, as a press does (milestone 511).
5558        self.runtime.selection_handles = None;
5559        let was = self.runtime.edits.get(&id).copied().unwrap_or_default();
5560        let mut edit = was;
5561        let widget = self
5562            .tree
5563            .as_ref()
5564            .and_then(|tree| find_widget(tree.as_ref(), id));
5565        // What the field held before the key: half of an undo step, and the half the
5566        // application owns.
5567        let before = widget
5568            .and_then(|widget| widget.text_value())
5569            .map(str::to_owned);
5570        // A field's own announcement (#18) — read here for the same reason the click
5571        // and keyboard-activation paths read theirs off the widget **before**
5572        // `dispatch_edit` rebuilds the tree: a plain keystroke leaves it unused, since
5573        // `on_edit` answers `None` for one and the announcement below is never spoken.
5574        let announce = widget.and_then(|widget| widget.announce());
5575        let message = widget.and_then(|widget| widget.on_edit(&mut edit, &key));
5576        self.runtime.edits.insert(id, edit);
5577        // In a multi-line field, make the retained scroll follow the caret and reveal it.
5578        self.reveal_caret(id, edit.cursor);
5579        if let Some(message) = message {
5580            self.dispatch_edit(message);
5581            if let Some(announce) = announce {
5582                self.set_announcement(announce);
5583            }
5584        }
5585        // The history, recorded on the **evidence** of a changed value rather than on the
5586        // intent of a key that usually changes one: a filter may have refused the
5587        // character, a length limit may have swallowed it, a read-only field ignores it,
5588        // and an Enter submits instead of typing. The value is read again from the tree
5589        // the dispatch has just rebuilt, so what is recorded is what happened.
5590        let after = self
5591            .tree
5592            .as_ref()
5593            .and_then(|tree| find_widget(tree.as_ref(), id))
5594            .and_then(|widget| widget.text_value())
5595            .map(str::to_owned);
5596        match (before, after) {
5597            (Some(before), Some(after)) if before != after => {
5598                let kind = EditKind::of(&key, was.selection_range().is_some(), self.composing());
5599                let since = self.since_last_edit();
5600                self.runtime
5601                    .record_edit(id, EditSnapshot::new(before, was), kind, since);
5602            }
5603            // A key that moved only the caret ends the run: what is typed next is a step
5604            // of its own, or one undo would take back two visits to the field at once.
5605            (Some(_), _) => self.runtime.close_edit_run(id),
5606            _ => {}
5607        }
5608    }
5609
5610    /// Dispatches a message that changed a field's value, and refreshes the tree at once.
5611    ///
5612    /// Keys can arrive in **bursts**, faster than a frame — a software keyboard, `adb
5613    /// input text`, auto-repeat — and the next one must see the CURRENT value, not the
5614    /// retained tree's, or it would overwrite the previous keystroke. So the tree is
5615    /// refreshed right away; `build_dirty` stays raised and the next frame redoes the full
5616    /// pass: mounts, leaving fades and all.
5617    fn dispatch_edit(&mut self, message: A::Message) {
5618        self.dispatch(message);
5619        if let Some((width, height)) = self.last_size {
5620            let theme = self.themes.displayed(&self.app);
5621            // A surface of its own, because this build happens between frames rather than
5622            // inside one — and the same `build_view` as the frame path, because the next
5623            // key in the burst reads this tree straight away.
5624            let tree = self
5625                .media_query(width, height)
5626                .scope(|| build_view(&self.app, &theme, &self.runtime));
5627            self.tree = Some(tree);
5628        }
5629    }
5630
5631    /// Seconds since the last recorded edit, and stamps this one. The first edit is
5632    /// infinitely far from the one before it, which is to say it starts a run.
5633    fn since_last_edit(&mut self) -> f32 {
5634        let now = Instant::now();
5635        let since = self
5636            .last_edit_at
5637            .map(|then| now.duration_since(then).as_secs_f32())
5638            .unwrap_or(f32::INFINITY);
5639        self.last_edit_at = Some(now);
5640        since
5641    }
5642
5643    /// Whether an input method is in the middle of composing a word.
5644    fn composing(&self) -> bool {
5645        self.runtime
5646            .input
5647            .focused
5648            .and_then(|id| self.runtime.edits.get(&id))
5649            .is_some_and(|edit| edit.composing.is_some())
5650    }
5651
5652    /// Steps a text field's value back one change, or forward again. `true` when something
5653    /// moved — Ctrl+Z on anything that is not a field, or with nothing left to undo, moves
5654    /// nothing and says so.
5655    fn step_history(&mut self, id: WidgetId, forward: bool) -> bool {
5656        let value = self
5657            .tree
5658            .as_ref()
5659            .and_then(|tree| find_widget(tree.as_ref(), id))
5660            .and_then(|widget| widget.text_value())
5661            .map(str::to_owned);
5662        let Some(value) = value else {
5663            return false;
5664        };
5665        let edit = self.runtime.edits.get(&id).copied().unwrap_or_default();
5666        let current = EditSnapshot::new(value, edit);
5667        let target = if forward {
5668            self.runtime.redo_edit(id, current)
5669        } else {
5670            self.runtime.undo_edit(id, current)
5671        };
5672        let Some(target) = target else {
5673            return false;
5674        };
5675        let message = self
5676            .tree
5677            .as_ref()
5678            .and_then(|tree| find_widget(tree.as_ref(), id))
5679            .and_then(|widget| widget.replace_value(target.value.clone()));
5680        // The caret goes back with the text — an undo that restores the value and leaves
5681        // the caret at the end has done half the job.
5682        self.runtime.edits.insert(id, target.edit);
5683        self.reveal_caret(id, target.edit.cursor);
5684        if let Some(message) = message {
5685            self.dispatch_edit(message);
5686        }
5687        // An undo is not a keystroke: what is typed after it starts a run of its own,
5688        // however quickly it follows.
5689        self.last_edit_at = None;
5690        true
5691    }
5692
5693    /// Brings the focused widget **into view**, gliding every scroll region around it.
5694    ///
5695    /// Called wherever the keyboard moves the focus, and nowhere else. A click already
5696    /// landed on something the eye could see, and a focus restored because an overlay
5697    /// closed should put the page back where the reader left it, not chase whatever the
5698    /// restoration happened to pick.
5699    ///
5700    /// The glide is a **target**, not a jump: the same easing a scrollbar drag or a page
5701    /// request uses, so Tab through a form reads as one movement rather than a series of
5702    /// cuts. Anything already moving the offset is let go of — the keyboard has just
5703    /// overruled it.
5704    fn reveal_focus(&mut self) {
5705        let Some(id) = self.runtime.input.focused else {
5706            return;
5707        };
5708        let Some(moves) = self.ui.as_ref().map(|ui| ui.reveal(id, &self.runtime)) else {
5709            return;
5710        };
5711        for (area, offset) in moves {
5712            self.runtime.scroll_ballistic.remove(&area);
5713            self.runtime.scroll_velocity.remove(&area);
5714            self.runtime.scroll_target.insert(area, offset);
5715        }
5716    }
5717
5718    /// Makes a multi-line field's retained scroll **follow the caret**: adjusts
5719    /// `runtime.scroll[id]` just enough to keep the caret visible, the way an editor
5720    /// re-centres as you type. A no-op for a field that does not scroll.
5721    fn reveal_caret(&mut self, id: WidgetId, cursor: usize) {
5722        let Some(vp) = self.ui.as_ref().and_then(|ui| ui.scrollable_viewport(id)) else {
5723            return;
5724        };
5725        let metrics = self
5726            .tree
5727            .as_ref()
5728            .and_then(|tree| find_widget(tree.as_ref(), id))
5729            .and_then(|widget| widget.text_metrics(vp.width, cursor));
5730        let Some((content_h, visible_h, caret_top, caret_h)) = metrics else {
5731            return;
5732        };
5733        let max_y = (content_h - visible_h).max(0.0);
5734        let cur = self.runtime.scroll.get(&id).map(|s| s.1).unwrap_or(0.0);
5735        // The scroll window in which the caret stays visible, from "the caret's bottom
5736        // is visible" to "the caret's top is visible". We bring the current scroll into it.
5737        let lo = (caret_top + caret_h - visible_h).max(0.0);
5738        let hi = caret_top;
5739        let target = cur.clamp(lo.min(hi), lo.max(hi)).clamp(0.0, max_y);
5740        self.runtime.scroll.insert(id, (0.0, target));
5741        self.runtime.scroll_target.insert(id, (0.0, target));
5742        self.runtime.scroll_velocity.remove(&id);
5743    }
5744
5745    /// Copies field `id`'s selected text to the clipboard.
5746    fn copy_selection(&mut self, id: WidgetId) {
5747        let edit = self.runtime.edits.get(&id).copied().unwrap_or_default();
5748        let text = self
5749            .tree
5750            .as_ref()
5751            .and_then(|tree| find_widget(tree.as_ref(), id))
5752            .and_then(|widget| widget.selected_text(&edit));
5753        if let Some(text) = text {
5754            self.clipboard.set_text(text);
5755        }
5756    }
5757
5758    /// Types what the clipboard answered into the field that asked for it, if the paste
5759    /// still lands there — see `clip::Pasted::lands`.
5760    fn land_paste(&mut self, pasted: clip::Pasted) {
5761        if pasted.lands(self.runtime.input.focused) {
5762            self.apply_key(pasted.into, Key::Text(pasted.text));
5763            self.request_redraw();
5764        }
5765    }
5766}
5767
5768/// Which end of a value drag is being announced.
5769#[derive(Clone, Copy, PartialEq, Eq)]
5770enum Edge {
5771    Start,
5772    End,
5773}
5774
5775/// Where along `rect` the pointer sits, as a 0..=1 fraction.
5776///
5777/// One function, used by the start, every move and the end alike: three copies of this
5778/// arithmetic would agree on every slider until one of them did not.
5779fn drag_fraction(rect: frus_widgets::Rect, x: f32) -> f32 {
5780    if rect.width > 0.0 {
5781        ((x - rect.x) / rect.width).clamp(0.0, 1.0)
5782    } else {
5783        0.0
5784    }
5785}
5786
5787/// Was the gesture that just ended **still a tap**?
5788///
5789/// Every drag here engages only past a threshold, so a press that never crossed it is
5790/// not a drag at all: it is a tap, and the release owes the widget underneath its
5791/// click. A drag that *did* move has already been answered — a fling, a drop, a
5792/// dismissal — and must not also click.
5793///
5794/// One list, because forgetting a variant is silent: the widget stays hittable, the
5795/// press still records it, and only the release quietly does nothing. That is how a
5796/// dismissible row came to swallow every tap on it (milestone 327).
5797fn gesture_was_a_tap(ended: Option<&Drag>) -> bool {
5798    matches!(
5799        ended,
5800        Some(
5801            Drag::Scroll { moved: false, .. }
5802                | Drag::Pan { moved: false, .. }
5803                | Drag::Reorder { moved: false, .. }
5804                | Drag::Item { moved: false, .. }
5805                | Drag::Dismiss { moved: false, .. }
5806                | Drag::Sheet { moved: false, .. }
5807        )
5808    )
5809}
5810
5811/// A press lands at `at`: what is under it is pressed, and a surface that takes ink
5812/// splashes from where it landed. The box comes from the frame that was on screen when the
5813/// pointer came down, which is the one the user aimed at. Returns whether a splash started.
5814fn press_at<Msg: Clone>(runtime: &mut Runtime, ui: Option<&Ui<Msg>>, at: Point) -> bool {
5815    runtime.input.pressed = ui.and_then(|ui| ui.hit(at));
5816    let Some(pressed) = runtime.input.pressed else {
5817        return false;
5818    };
5819    let Some(rect) = ui.and_then(|ui| ui.ink_box(pressed)) else {
5820        return false;
5821    };
5822    runtime.ink_press(
5823        pressed,
5824        Point::new(at.x - rect.x, at.y - rect.y),
5825        Size::new(rect.width, rect.height),
5826    );
5827    true
5828}
5829
5830/// What becomes of the press once a pointer event has been routed, `could_tap` being
5831/// whether the gesture under way could still have ended as a tap **before** the event, and
5832/// `drag` the gesture after it. Returns whether anything changed, which wants a frame.
5833///
5834/// A press on something inside a scrollable is shared with the scroll until the pointer
5835/// crosses the slop. From then on the gesture has it — a scroll, a swipe, a sheet, a
5836/// carried row — and, as in the reference, where the recogniser that loses the arena is
5837/// told so, the press is **let go of there and then**: its ink is swept, its press fades.
5838/// And a pointer that has lifted holds nothing, whatever its release did.
5839///
5840/// Before this, a release that ended a gesture which had moved returned before the press
5841/// was cleared, so the press outlived the finger; the ink, which waits for as long as its
5842/// widget is pressed, was held for good — the grey left on a segment a phone had swiped
5843/// a strip of filters by (milestone 534).
5844///
5845/// A finger hovers only so that its press can show (milestone 505), so a finger whose press
5846/// was taken stops hovering too. A mouse is still over whatever it is over.
5847fn settle_press(
5848    input: &mut frus_widgets::InputState,
5849    kind: PointerKind,
5850    could_tap: bool,
5851    drag: Option<&Drag>,
5852    touch: bool,
5853) -> bool {
5854    let taken = match kind {
5855        PointerKind::Down => false,
5856        PointerKind::Move => could_tap && !gesture_was_a_tap(drag),
5857        PointerKind::Up | PointerKind::Cancel => true,
5858    };
5859    if !taken {
5860        return false;
5861    }
5862    let mut changed = input.pressed.take().is_some();
5863    if touch {
5864        changed |= input.hovered.take().is_some();
5865    }
5866    changed
5867}
5868
5869/// How fast an area scrolls under a carried item, in pixels per second **per pixel** the
5870/// item hangs over the edge: the further out it is, the faster the list comes to meet it.
5871/// The reference's number, and the reference's law.
5872const AUTOSCROLL_VELOCITY: f32 = 50.0;
5873
5874/// The overhang that speed is worked out from is capped here, so that carrying a row far
5875/// past the edge — or off the window entirely — settles at a fast but usable speed instead
5876/// of one nobody can aim with.
5877const AUTOSCROLL_MAX_OVERHANG: f32 = 20.0;
5878
5879/// Places the scroll requests it can against `regions`, and hands back the ones whose
5880/// region that registry does not name, along with whether anything moved.
5881///
5882/// A request names its region by **key**, the way a focus request does: the application
5883/// wrapped it in `keyed(k, …)` and the tree is what turns that back into an identity.
5884/// The framework's own identities are hashes of a position in a tree — an application
5885/// cannot know one, and should not have to.
5886///
5887/// Nothing is *stored* here. The request is spent the moment its region is found, and
5888/// what it leaves behind is an offset in the runtime, indistinguishable from an offset a
5889/// finger left there. That is what makes it survive a rebuild: there is nothing to
5890/// survive.
5891fn apply_scroll_requests<Msg>(
5892    runtime: &mut Runtime,
5893    tree: &dyn Widget<Msg>,
5894    requests: Vec<(u64, ScrollTo)>,
5895    regions: &[Scrollable],
5896) -> (Vec<(u64, ScrollTo)>, bool) {
5897    let mut unplaced = Vec::new();
5898    let mut moved = false;
5899    for (key, to) in requests {
5900        let area = find_by_key(tree, key).and_then(|id| regions.iter().find(|a| a.id == id));
5901        match area {
5902            Some(area) => moved |= runtime.scroll_to(area, to),
5903            None => unplaced.push((key, to)),
5904        }
5905    }
5906    (unplaced, moved)
5907}
5908
5909/// Places the sheet requests it can against `sheets`, and hands back the ones whose sheet
5910/// that registry does not name, along with whether anything moved — the terms of
5911/// `apply_scroll_requests`, above.
5912///
5913/// A request names the key the application wrapped the sheet in; the state belongs to the
5914/// sheet's panel under that key, which is what the tree is asked for.
5915fn apply_sheet_requests<Msg>(
5916    runtime: &mut Runtime,
5917    tree: &dyn Widget<Msg>,
5918    requests: Vec<(u64, SheetTo)>,
5919    sheets: &[frus_widgets::SheetArea],
5920) -> (Vec<(u64, SheetTo)>, bool) {
5921    let mut unplaced = Vec::new();
5922    let mut moved = false;
5923    for (key, to) in requests {
5924        let sheet = frus_widgets::find_sheet_by_key(tree, key)
5925            .and_then(|id| sheets.iter().find(|sheet| sheet.id == id));
5926        match sheet {
5927            Some(sheet) => moved |= runtime.sheet_to(sheet.id, &sheet.spec, &to),
5928            None => unplaced.push((key, to)),
5929        }
5930    }
5931    (unplaced, moved)
5932}
5933
5934/// Moves a carried item's press by `shift` — how far its content moved on screen — so that
5935/// the ghost, drawn at the content's box plus the finger's travel since the press, stays
5936/// under the finger. A lifted item's box, recorded at the press, goes with it.
5937///
5938/// Anything else being dragged is not carried content, and is left alone.
5939fn follow_content(drag: &mut Drag, shift: (f32, f32)) {
5940    match drag {
5941        Drag::Reorder { start, carried, .. } => {
5942            start.x += shift.0;
5943            start.y += shift.1;
5944            if let Some((_, rect)) = carried {
5945                *rect = rect.translate(shift.0, shift.1);
5946            }
5947        }
5948        Drag::Item { source, start, .. } => {
5949            start.x += shift.0;
5950            start.y += shift.1;
5951            source.rect = source.rect.translate(shift.0, shift.1);
5952        }
5953        _ => {}
5954    }
5955}
5956
5957/// One axis of the auto-scroll: how far the **content** has to move this frame so that an
5958/// item held past a viewport's edge brings the rest of the list into view.
5959///
5960/// Everything is in screen coordinates and along one axis: `item` and `view` are
5961/// `(start, end)` pairs, `room` is what is left to scroll that way (nought at the end of
5962/// the content). The answer is a movement of the content, which the area then turns into
5963/// an offset of its own — signs and reversed axes belong to `Scrollable::offset_delta`,
5964/// not here.
5965///
5966/// `None` means *not now*: the item is inside, or the edge it hangs over is the end of the
5967/// content, where there is nothing left to reveal and the list should stay still rather
5968/// than fight.
5969fn edge_autoscroll(item: (f32, f32), view: (f32, f32), room: (f32, f32), dt: f32) -> Option<f32> {
5970    let before = view.0 - item.0; // above the top edge
5971    let after = item.1 - view.1; // below the bottom edge
5972                                 // The leading edge is asked first, and both are never obeyed at once: an item taller
5973                                 // than the viewport hangs over both, and an item pulled two ways scrolls nowhere.
5974    let overhang = if before > 0.0 && room.0 > 0.0 {
5975        before.min(AUTOSCROLL_MAX_OVERHANG)
5976    } else if after > 0.0 && room.1 > 0.0 {
5977        -after.min(AUTOSCROLL_MAX_OVERHANG)
5978    } else {
5979        return None;
5980    };
5981    let travel = overhang * AUTOSCROLL_VELOCITY * dt;
5982    (travel != 0.0).then_some(travel)
5983}
5984
5985/// Moves `current` toward `target` by one **exponential** spring step, with time
5986/// constant `tau` in seconds, over an interval `dt`: frame-rate independent and free
5987/// of overshoot. Used to smooth the abscissa during a reorder.
5988fn spring_toward(current: f32, target: f32, dt: f32, tau: f32) -> f32 {
5989    let k = 1.0 - (-dt / tau.max(1e-4)).exp();
5990    current + (target - current) * k
5991}
5992
5993/// The focus history's maximum depth, for nested overlays' triggers.
5994const FOCUS_HISTORY_MAX: usize = 8;
5995
5996/// Resolves focus after a (re)build, from the focusables **present** this frame. When
5997/// `current` has **vanished**, an overlay having closed, focus returns to the most
5998/// recent present **trigger** in `history`, which is popped until one is found. The
5999/// transition is recorded: the old focus (`prev`), if still present and different from
6000/// the new one, is pushed as a candidate trigger, bounded by [`FOCUS_HISTORY_MAX`].
6001/// Returns the resolved focus. A **pure** function apart from the mutated `history` and
6002/// `prev` — testable without a window.
6003fn resolve_focus(
6004    current: Option<WidgetId>,
6005    present: &std::collections::HashSet<WidgetId>,
6006    history: &mut Vec<WidgetId>,
6007    prev: &mut Option<WidgetId>,
6008) -> Option<WidgetId> {
6009    let mut cur = current;
6010    if let Some(c) = cur {
6011        if !present.contains(&c) {
6012            cur = None;
6013            while let Some(cand) = history.pop() {
6014                if present.contains(&cand) {
6015                    cur = Some(cand);
6016                    break;
6017                }
6018            }
6019        }
6020    }
6021    if *prev != cur {
6022        if let Some(old) = *prev {
6023            if present.contains(&old) && Some(old) != cur {
6024                history.push(old);
6025                if history.len() > FOCUS_HISTORY_MAX {
6026                    history.remove(0);
6027                }
6028            }
6029        }
6030        *prev = cur;
6031    }
6032    cur
6033}
6034
6035/// Paints the dragged header's **lifted card**, the ghost, into `scene` — unclipped —
6036/// at `card`: a drop shadow, a **faithful face** from the header's already-translated
6037/// primitives, or a solid one as a fallback when `ghost` is empty, then a `primary`
6038/// border. A pure function, testable without a GPU. The neighbours' sliding happens
6039/// upstream, in `reflow_reorder_columns`.
6040fn draw_ghost_card(scene: &mut Scene, theme: &Theme, card: Rect, ghost: &[Primitive]) {
6041    use drag_preview::{BORDER_ALPHA, BORDER_WIDTH, SHADOW_ALPHA, SHADOW_BLUR, SHADOW_OFFSET_Y};
6042    scene.set_clip(Rect::UNBOUNDED);
6043    // The shadow colour comes from **the theme** and can be overridden — the same role
6044    // as `Button`'s shadow; only the geometry, offset, blur and opacity, is a local
6045    // constant.
6046    let shadow = theme.scheme.shadow.with_alpha(SHADOW_ALPHA);
6047    scene.shadow(
6048        card.translate(0.0, SHADOW_OFFSET_Y),
6049        shadow,
6050        theme.radius,
6051        SHADOW_BLUR,
6052    );
6053    let border = theme.primary.fade(BORDER_ALPHA);
6054    if ghost.is_empty() {
6055        scene.draw_rect(card, theme.surface, theme.radius, BORDER_WIDTH, border);
6056    } else {
6057        scene.draw_rect(card, theme.surface, theme.radius, 0.0, Color::TRANSPARENT);
6058        for primitive in ghost {
6059            scene.push_primitive(primitive.clone());
6060        }
6061        scene.draw_rect(card, Color::TRANSPARENT, theme.radius, BORDER_WIDTH, border);
6062    }
6063}
6064
6065/// How a reorderable moves while it is carried: the two gestures the shell knows.
6066#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6067enum ReorderMotion {
6068    /// A table's column: a continuous slide along x following the pointer, dropped into
6069    /// the place of the column under it.
6070    Columns,
6071    /// A list's row or a board's card: inserted before or after a slot along the axis,
6072    /// with an insertion line, a gap opening there, and the nearest slot past either end.
6073    Slots(ReorderAxis),
6074}
6075
6076impl ReorderMotion {
6077    /// Which of the two a reorderable is, from its two answers: every vertical one inserts,
6078    /// and a horizontal one only when it says so — a list's row does, a table's column does
6079    /// not.
6080    fn of(axis: ReorderAxis, inserts: bool) -> Self {
6081        match axis {
6082            ReorderAxis::Vertical => Self::Slots(ReorderAxis::Vertical),
6083            ReorderAxis::Horizontal if inserts => Self::Slots(ReorderAxis::Horizontal),
6084            ReorderAxis::Horizontal => Self::Columns,
6085        }
6086    }
6087}
6088
6089/// How `widget` moves while it is carried — [`ReorderMotion::of`] its two answers.
6090fn reorder_motion<Msg>(widget: &dyn Widget<Msg>) -> ReorderMotion {
6091    ReorderMotion::of(widget.reorder_axis(), widget.reorder_inserts())
6092}
6093
6094/// Where the ghost is drawn relative to the box it was lifted from, given the finger's
6095/// `travel` since the press.
6096///
6097/// - A **column** follows the pointer along x, with a slight lift.
6098/// - A **card** follows it in both directions: a board carries it across columns.
6099/// - A **horizontal list's row** follows it along x only. The reference keeps a list's
6100///   proxy in its lane on either axis; a vertical list here shares the board's freedom
6101///   because the two are one path, and a horizontal list has no board to share it with.
6102fn ghost_offset(motion: ReorderMotion, travel: (f32, f32)) -> (f32, f32) {
6103    match motion {
6104        ReorderMotion::Columns => (travel.0, drag_preview::LIFT_Y),
6105        ReorderMotion::Slots(ReorderAxis::Vertical) => travel,
6106        ReorderMotion::Slots(ReorderAxis::Horizontal) => (travel.0, 0.0),
6107    }
6108}
6109
6110/// The geometry of a reorder preview's **insertion line**: a thin band of thickness
6111/// `thickness`, centred on the target edge **where the insertion will happen**.
6112///
6113/// Along a **vertical** list: the **top** edge (inserting before, the upper half hovered)
6114/// or the **bottom** one (inserting after, `after = true`, the lower half hovered),
6115/// spanning the full width. Along a **horizontal** list the band stands on its end,
6116/// spanning the full height at the left or right edge — and *after* is the right edge
6117/// only when the list reads left to right: under `rtl` what follows a row is on its left.
6118/// A pure function, testable without a GPU.
6119fn drop_insertion_line(
6120    target: Rect,
6121    thickness: f32,
6122    after: bool,
6123    axis: ReorderAxis,
6124    rtl: bool,
6125) -> Rect {
6126    match axis {
6127        ReorderAxis::Vertical => {
6128            let edge = if after {
6129                target.y + target.height
6130            } else {
6131                target.y
6132            };
6133            Rect::new(target.x, edge - thickness * 0.5, target.width, thickness)
6134        }
6135        ReorderAxis::Horizontal => {
6136            let edge = if after != rtl {
6137                target.x + target.width
6138            } else {
6139                target.x
6140            };
6141            Rect::new(edge - thickness * 0.5, target.y, thickness, target.height)
6142        }
6143    }
6144}
6145
6146/// Which axis a scroll gesture is **claimed by**, decided once when the finger passes the
6147/// threshold: `true` for vertical.
6148///
6149/// An area that can only go one way claims that one, whatever the finger did — a diagonal
6150/// flick down a list is a scroll down, not a refusal. An area that can go both ways takes
6151/// the direction the finger actually went in, ties going to vertical, which is the way a
6152/// page reads. The loser gets nothing for the rest of the gesture: this is the reference's
6153/// arena, where a vertical and a horizontal recogniser compete and only one wins.
6154fn claim_axis(dx: f32, dy: f32, can_x: bool, can_y: bool) -> bool {
6155    match (can_x, can_y) {
6156        (true, false) => false,
6157        (false, true) => true,
6158        _ => dy.abs() >= dx.abs(),
6159    }
6160}
6161
6162/// Which of the scrollables under the finger a gesture is **for**: the innermost one that
6163/// can go the way the finger went (`down` = along the vertical), given `chain` innermost
6164/// first and `under` the area the press landed on.
6165///
6166/// A strip that only slides across, sitting in a page that only scrolls down, is not what a
6167/// drag downwards is about — the page behind it is, and the reference's arena gives it to
6168/// the page for exactly that reason. Where nothing in the stack can go that way, the area
6169/// under the finger keeps the gesture and goes its own way: a lone recogniser in an arena
6170/// still wins.
6171fn claim_area(
6172    chain: &[frus_widgets::Scrollable],
6173    under: WidgetId,
6174    down: bool,
6175) -> Option<frus_widgets::Scrollable> {
6176    chain
6177        .iter()
6178        .find(|a| {
6179            if down {
6180                // A refresh area listening above takes a pull downwards even with nothing
6181                // to scroll, which is the reference's one exception to the same rule.
6182                a.max_y > 0.0 || a.refresh.is_some()
6183            } else {
6184                a.max_x > 0.0
6185            }
6186        })
6187        .or_else(|| chain.iter().find(|a| a.id == under))
6188        .copied()
6189}
6190
6191/// Gets the bytes at `url` and hands them to `deliver`, once.
6192///
6193/// Registered as the process's [`frus_widgets::ImageFetcher`] when the shell is built with
6194/// `net`. It spawns and forgets: nothing here waits for the answer, and the store the
6195/// callback writes into is what a later frame reads.
6196///
6197/// The two spawns are the two the rest of this file uses for every other effect — the
6198/// shared executor natively, the browser on the Web.
6199///
6200/// It carries a **deadline**, and that is not politeness. An image still in flight keeps
6201/// the interface redrawing — that is how the frame showing it ever happens — so a
6202/// request that never answers would leave the application repainting at full rate for
6203/// ever, on somebody's battery. A request without an answer has to become one with a
6204/// failure.
6205#[cfg(feature = "net")]
6206fn fetch_image_bytes(
6207    url: &str,
6208    deliver: Box<dyn FnOnce(Result<Vec<u8>, String>) + Send + 'static>,
6209) {
6210    /// Long enough for a photograph on a slow connection, short enough that a dead
6211    /// server does not pin a screen redrawing until the user closes the application.
6212    const DEADLINE: std::time::Duration = std::time::Duration::from_secs(30);
6213
6214    let url = url.to_string();
6215    let run = async move {
6216        let result = crate::net::Request::get(url)
6217            .timeout(DEADLINE)
6218            .send_bytes()
6219            .await;
6220        deliver(result.map_err(|e| e.to_string()));
6221    };
6222    #[cfg(not(web))]
6223    crate::runtime::spawn(run).detach();
6224    #[cfg(web)]
6225    wasm_bindgen_futures::spawn_local(run);
6226}
6227
6228#[cfg(test)]
6229mod tests {
6230    use super::{
6231        build_view, claim_area, claim_axis, draw_ghost_card, drop_insertion_line, fling_velocity,
6232        gesture_was_a_tap, ghost_offset, install_ambient, reorder_motion, resolve_focus,
6233        spring_toward, Drag, Point, Rect, ReorderAxis, ReorderMotion, Scene, Theme,
6234        VelocityEstimate, PRECISE_SLOP, TOUCH_SLOP,
6235    };
6236    use super::{clipboard_command, ClipCommand, KeyCode, PhysicalKey, WinitKey};
6237    use super::{collect_ids, find_widget, MediaQuery};
6238    use frus_widgets::Locale;
6239
6240    /// **A field with a clear button kept the keyboard only until the first letter**
6241    /// (milestone 510). Whether the focused widget takes typing was asked of a caret
6242    /// hit test at the corner of a field one pixel wide; a field whose clickable suffix
6243    /// covers that pixel — the × it shows once it holds text — answered no, and the
6244    /// keyboard was closed mid-word. Asked of the widget, the answer is the field's.
6245    #[test]
6246    fn a_field_with_a_clear_button_still_wants_the_keyboard() {
6247        use frus_widgets::{Container, Icons, TextField};
6248        let clearable = TextField::<()>::new("F")
6249            .suffix_icon(Icons::CLOSE)
6250            .on_suffix(());
6251        assert!(
6252            super::wants_keyboard(&clearable),
6253            "the field with its clear button"
6254        );
6255        assert!(
6256            !super::wants_keyboard(&TextField::<()>::new("F").enabled(false)),
6257            "a disabled field takes no typing"
6258        );
6259        assert!(
6260            !super::wants_keyboard(&Container::<()>::new()),
6261            "and what is not a field wants no keyboard"
6262        );
6263    }
6264
6265    fn character(c: &str) -> WinitKey {
6266        WinitKey::Character(c.into())
6267    }
6268
6269    /// Ctrl+C/X/V are the clipboard, in either case — Caps Lock or Shift is still a
6270    /// copy — and the same letters without Ctrl are only letters.
6271    #[test]
6272    fn ctrl_c_x_v_are_the_clipboard_and_the_letters_alone_are_not() {
6273        let any = PhysicalKey::Code(KeyCode::KeyQ);
6274        for (letter, command) in [
6275            ("c", ClipCommand::Copy),
6276            ("X", ClipCommand::Cut),
6277            ("v", ClipCommand::Paste),
6278        ] {
6279            assert_eq!(
6280                clipboard_command(&character(letter), any, true),
6281                Some(command)
6282            );
6283            assert_eq!(
6284                clipboard_command(&character(letter), any, false),
6285                None,
6286                "{letter}"
6287            );
6288        }
6289        assert_eq!(clipboard_command(&character("a"), any, true), None);
6290    }
6291
6292    /// **Android's Copy, Cut and Paste keys arrive by physical code alone** — winit gives
6293    /// them no logical key — and they are the clipboard with no modifier held. A check
6294    /// of the logical key alone ignored them (milestone 509).
6295    #[test]
6296    fn a_keyboards_own_clipboard_keys_work_by_their_physical_code() {
6297        let unidentified = WinitKey::Unidentified(winit::keyboard::NativeKey::Unidentified);
6298        for (code, command) in [
6299            (KeyCode::Copy, ClipCommand::Copy),
6300            (KeyCode::Cut, ClipCommand::Cut),
6301            (KeyCode::Paste, ClipCommand::Paste),
6302        ] {
6303            assert_eq!(
6304                clipboard_command(&unidentified, PhysicalKey::Code(code), false),
6305                Some(command),
6306                "{code:?}"
6307            );
6308        }
6309        // And the named keys a desktop keyboard reports for the same thing.
6310        let named = WinitKey::Named(super::NamedKey::Paste);
6311        let any = PhysicalKey::Code(KeyCode::KeyQ);
6312        assert_eq!(
6313            clipboard_command(&named, any, false),
6314            Some(ClipCommand::Paste)
6315        );
6316    }
6317
6318    /// **A paste lands only in the field that asked, while that field still has the
6319    /// focus** — on the Web the clipboard answers after the key press, and by then the
6320    /// reader may have moved to another field or to none. And a clipboard holding no
6321    /// text pastes nothing, rather than typing an empty string over the selection
6322    /// (milestone 526).
6323    #[test]
6324    fn a_paste_lands_in_the_field_that_asked_only_while_it_has_the_focus() {
6325        use super::clip::Pasted;
6326        let field = WidgetId::from_u64(1);
6327        let other = WidgetId::from_u64(2);
6328        let pasted = Pasted {
6329            into: field,
6330            text: "copied".into(),
6331        };
6332        assert!(pasted.lands(Some(field)));
6333        assert!(
6334            !pasted.lands(Some(other)),
6335            "the focus moved to another field"
6336        );
6337        assert!(!pasted.lands(None), "the focus left every field");
6338        let nothing = Pasted {
6339            into: field,
6340            text: String::new(),
6341        };
6342        assert!(!nothing.lands(Some(field)), "an empty clipboard");
6343    }
6344
6345    /// **A paste answered later is queued for its field, and a frame is asked for.** Two
6346    /// pastes asked in two fields and answered in the other order come out in the order
6347    /// the answers came, each still naming the field that asked; each answer wakes the
6348    /// window once; and nothing is taken twice (milestone 526).
6349    #[test]
6350    fn a_paste_answered_later_waits_for_the_frame_that_takes_it() {
6351        use super::clip::{Answers, Pasted};
6352        use std::cell::Cell;
6353        use std::rc::Rc;
6354        let first = WidgetId::from_u64(1);
6355        let second = WidgetId::from_u64(2);
6356        let answers = Answers::default();
6357        let wakes = Rc::new(Cell::new(0));
6358        let wake = || {
6359            let wakes = Rc::clone(&wakes);
6360            move || wakes.set(wakes.get() + 1)
6361        };
6362        let to_first = answers.answer_for(first, wake());
6363        let to_second = answers.answer_for(second, wake());
6364        assert_eq!(answers.take(), None, "nothing is answered yet");
6365        assert_eq!(wakes.get(), 0, "asking is not answering");
6366
6367        to_second("later".into());
6368        assert_eq!(wakes.get(), 1);
6369        to_first("sooner".into());
6370        assert_eq!(wakes.get(), 2);
6371
6372        assert_eq!(
6373            answers.take(),
6374            Some(Pasted {
6375                into: second,
6376                text: "later".into()
6377            })
6378        );
6379        assert_eq!(
6380            answers.take(),
6381            Some(Pasted {
6382                into: first,
6383                text: "sooner".into()
6384            })
6385        );
6386        assert_eq!(answers.take(), None, "each answer is taken once");
6387    }
6388
6389    /// An application whose whole interface lives **inside a deferred subtree** — which is
6390    /// what any application with an `AppBar` is, since a bar defers its own composition
6391    /// until the theme is known.
6392    ///
6393    /// It exists because nothing else in this repo can stand in for one: the shell's own
6394    /// types need a window and an event-loop proxy, so the only piece of the frame a test
6395    /// can hold is the part that turns a `view` into a tree. That part is where two
6396    /// milestones' worth of bugs were.
6397    struct Deferred;
6398
6399    impl crate::Application for Deferred {
6400        type Message = ();
6401
6402        fn update(&mut self, _message: ()) -> crate::Command<()> {
6403            crate::Command::none()
6404        }
6405
6406        fn view(&self, _theme: &Theme) -> Box<dyn frus_widgets::Widget<()>> {
6407            Box::new(frus_widgets::ThemeBuilder::new(|_: &Theme| {
6408                frus_widgets::Flex::column()
6409                    .width(300.0)
6410                    .height(80.0)
6411                    .child(
6412                        frus_widgets::TextField::new("hi")
6413                            .width(200.0)
6414                            .on_input(|_| ()),
6415                    )
6416            }))
6417        }
6418    }
6419
6420    /// The surface every frame installs, so a build outside one measures the way a build
6421    /// inside one does.
6422    fn surfaced<R>(f: impl FnOnce() -> R) -> R {
6423        MediaQuery::new(frus_widgets::Size::new(300.0, 80.0)).scope(f)
6424    }
6425
6426    /// **A shell installs the reader's language** — the assertion that keeps milestone 454
6427    /// from being a resolution nobody ever runs.
6428    ///
6429    /// `locale::of()` answers `en` with nothing installed, exactly as
6430    /// `localizations::of()` answers English, and for the same reason: an application that
6431    /// says nothing must keep working. That default is also what would hide the wiring
6432    /// being missing, so this drives the shell's own reading of the trait rather than
6433    /// calling `resolve` directly — milestone 408's bug, and milestone 449's guard against
6434    /// it.
6435    #[test]
6436    fn a_shell_installs_the_reader_s_language() {
6437        /// An application with three languages, and a switch of its own it may or may not
6438        /// have been told to use.
6439        struct Speaks(Option<Locale>);
6440
6441        impl crate::Application for Speaks {
6442            type Message = ();
6443
6444            fn update(&mut self, _message: ()) -> crate::Command<()> {
6445                crate::Command::none()
6446            }
6447
6448            fn view(&self, _theme: &Theme) -> Box<dyn frus_widgets::Widget<()>> {
6449                Box::new(frus_widgets::Flex::<()>::column())
6450            }
6451
6452            fn supported_locales(&self) -> Vec<Locale> {
6453                vec![
6454                    Locale::new("en"),
6455                    Locale::new("fr"),
6456                    Locale::with_country("fr", "CA"),
6457                ]
6458            }
6459
6460            fn locale(&self) -> Option<Locale> {
6461                self.0.clone()
6462            }
6463        }
6464
6465        let mut runtime = frus_widgets::Runtime::default();
6466
6467        // The device speaks Belgian French; the application has French, so it wins.
6468        install_ambient(
6469            &Speaks(None),
6470            &mut runtime,
6471            &[Locale::with_country("fr", "BE")],
6472        );
6473        assert_eq!(frus_widgets::locale::of(), Locale::new("fr"));
6474
6475        // A platform that reported nothing is not a failure: the application's own first
6476        // choice stands.
6477        install_ambient(&Speaks(None), &mut runtime, &[]);
6478        assert_eq!(frus_widgets::locale::of(), Locale::new("en"));
6479
6480        // And an application with a language menu of its own outranks the device — still
6481        // resolved, so asking for one it does not have gives the nearest thing it does.
6482        install_ambient(
6483            &Speaks(Some(Locale::with_country("fr", "FR"))),
6484            &mut runtime,
6485            &[Locale::new("en")],
6486        );
6487        assert_eq!(frus_widgets::locale::of(), Locale::new("fr"));
6488
6489        // An application that says nothing at all is where it was: English.
6490        install_ambient(&Deferred, &mut runtime, &[Locale::new("fr")]);
6491        assert_eq!(
6492            frus_widgets::locale::of(),
6493            Locale::new("en"),
6494            "it supports only English, so French resolves to it"
6495        );
6496    }
6497
6498    /// **A shell installs the application's words** — which is the assertion that keeps
6499    /// milestone 449 from being a table nobody ever reads.
6500    ///
6501    /// `localizations::of` answers English with nothing installed, so every widget test
6502    /// and every golden would pass whether or not the shell ever called `install`. The
6503    /// default is what makes the feature safe to add; it is also what would hide the
6504    /// wiring being missing. So this drives the shell's own reading of the trait.
6505    #[test]
6506    fn a_shell_installs_the_application_s_words() {
6507        struct Fr;
6508
6509        impl frus_widgets::localizations::Localizations for Fr {
6510            fn back_button_label(&self) -> &str {
6511                "Retour"
6512            }
6513
6514            fn first_day_of_week_index(&self) -> usize {
6515                1
6516            }
6517        }
6518
6519        struct Speaks;
6520
6521        impl crate::Application for Speaks {
6522            type Message = ();
6523
6524            fn update(&mut self, _message: ()) -> crate::Command<()> {
6525                crate::Command::none()
6526            }
6527
6528            fn view(&self, _theme: &Theme) -> Box<dyn frus_widgets::Widget<()>> {
6529                Box::new(frus_widgets::Flex::<()>::column())
6530            }
6531
6532            fn localizations(&self) -> Option<std::rc::Rc<dyn frus_widgets::Localizations>> {
6533                Some(std::rc::Rc::new(Fr))
6534            }
6535        }
6536
6537        let mut runtime = frus_widgets::Runtime::default();
6538        install_ambient(&Speaks, &mut runtime, &[]);
6539        assert_eq!(
6540            frus_widgets::localizations::of().back_button_label(),
6541            "Retour",
6542            "the shell read the application's table and installed it"
6543        );
6544        assert_eq!(
6545            frus_widgets::localizations::of().first_day_of_week_index(),
6546            1
6547        );
6548
6549        // And an application that says nothing leaves whatever is in force alone rather
6550        // than forcing English back on top of a table installed elsewhere.
6551        install_ambient(&Deferred, &mut runtime, &[]);
6552        assert_eq!(
6553            frus_widgets::localizations::of().back_button_label(),
6554            "Retour",
6555            "saying nothing is not the same as saying English"
6556        );
6557    }
6558
6559    /// A tree the shell hands to a traversal is **ready to be traversed**.
6560    ///
6561    /// Milestones 415 and 416 were the same bug found twice, in the two places this shell
6562    /// turns a `view` into a tree. A deferred subtree has no children until something has
6563    /// built it, so an unprepared tree reports **one** identity — its root — and every
6564    /// widget in the application is invisible to `collect_ids`, to `find_widget`, and to
6565    /// everything downstream of them: the mount and leave fades, the focus, the caret.
6566    ///
6567    /// The failure was silence in both places, which is why it went unnoticed twice. This
6568    /// is the assertion that would have caught it, and it needs no window.
6569    #[test]
6570    fn a_view_is_built_ready_to_be_read() {
6571        let theme = Theme::default();
6572        let prepared =
6573            surfaced(|| build_view(&Deferred, &theme, &frus_widgets::Runtime::default()));
6574        let ids = collect_ids(prepared.as_ref());
6575        assert!(
6576            ids.len() > 1,
6577            "an application inside a deferred subtree has identities to mount: {ids:?}"
6578        );
6579        // And every one of them is reachable, which is what the burst path needs.
6580        for id in &ids {
6581            assert!(
6582                find_widget(prepared.as_ref(), *id).is_some(),
6583                "{id:?} was counted but cannot be found"
6584            );
6585        }
6586    }
6587
6588    /// The same view, unprepared, is the state both call sites used to read.
6589    #[test]
6590    #[should_panic(expected = "before it was built")]
6591    fn an_unprepared_view_is_the_state_that_broke() {
6592        let theme = Theme::default();
6593        let raw = surfaced(|| crate::Application::view(&Deferred, &theme));
6594        let _ = collect_ids(raw.as_ref());
6595    }
6596
6597    /// A press on a **dismissible** row that never moved is a tap, and the widget under
6598    /// the finger owes it a click (reported on a device: a task row's avatar opened
6599    /// nothing, while a control a few hundred pixels away in the same card worked).
6600    ///
6601    /// The row registers a swipe on the press, in case the finger is about to slide
6602    /// sideways. When it does not, the swipe has to stand down. Every other gesture
6603    /// here already did; this one was left out of the list.
6604    #[test]
6605    fn a_swipe_that_never_started_is_still_a_tap() {
6606        let item = frus_widgets::Dismissable {
6607            id: WidgetId::from_u64(1),
6608            rect: Rect::new(0.0, 0.0, 300.0, 62.0),
6609            spec: frus_widgets::DismissSpec::default(),
6610        };
6611        let dismiss = |moved| Drag::Dismiss {
6612            item,
6613            last: Point::new(0.0, 0.0),
6614            moved,
6615        };
6616        assert!(
6617            gesture_was_a_tap(Some(&dismiss(false))),
6618            "a swipe under the threshold leaves the click alone"
6619        );
6620        assert!(
6621            !gesture_was_a_tap(Some(&dismiss(true))),
6622            "a swipe that ran has already been answered and must not also click"
6623        );
6624        // A scroll, the variant that was already right — and the one that masked this
6625        // bug, since a list long enough to scroll claims the press before the swipe can.
6626        let scroll = Drag::Scroll {
6627            id: WidgetId::from_u64(2),
6628            last: Point::new(0.0, 0.0),
6629            moved: false,
6630            carried: (0.0, 0.0),
6631            dismiss: None,
6632            axis: None,
6633        };
6634        assert!(gesture_was_a_tap(Some(&scroll)));
6635        // A press on a sheet's panel likewise: a button in the sheet still clicks, and a
6636        // sheet that was dragged does not also click what it was dragged by (515).
6637        let sheet = |moved| Drag::Sheet {
6638            id: WidgetId::from_u64(4),
6639            last: Point::new(0.0, 0.0),
6640            moved,
6641            available: 800.0,
6642        };
6643        assert!(gesture_was_a_tap(Some(&sheet(false))));
6644        assert!(!gesture_was_a_tap(Some(&sheet(true))));
6645        // And a gesture that captures on the press, rather than at a threshold, never
6646        // becomes a tap however still the finger was.
6647        let select = Drag::TextSelect {
6648            id: WidgetId::from_u64(3),
6649            rect: Rect::new(0.0, 0.0, 10.0, 10.0),
6650        };
6651        assert!(!gesture_was_a_tap(Some(&select)));
6652        assert!(!gesture_was_a_tap(None), "no gesture, no verdict to give");
6653    }
6654
6655    /// A page that scrolls down must not drift sideways while it does it (reported on a
6656    /// device, 2026-08-16). No finger travels in a straight line, so an area that can go
6657    /// both ways has to pick one and keep it.
6658    #[test]
6659    fn a_scroll_gesture_is_claimed_by_one_axis() {
6660        // Both axes available: the direction the finger went in wins, and a little wobble
6661        // across it changes nothing.
6662        assert!(claim_axis(6.0, 40.0, true, true), "down the page");
6663        assert!(!claim_axis(40.0, 6.0, true, true), "across it");
6664        // A tie reads as vertical, which is the way a page reads.
6665        assert!(claim_axis(10.0, 10.0, true, true));
6666        // One axis only: it takes the gesture whatever the finger did, or a diagonal flick
6667        // down a list would move nothing at all.
6668        assert!(claim_axis(40.0, 6.0, false, true), "a list only goes down");
6669        assert!(
6670            !claim_axis(6.0, 40.0, true, false),
6671            "a strip only goes across"
6672        );
6673    }
6674
6675    /// An area under the finger, `max_x` by `max_y` of content out of sight.
6676    fn area(id: u64, max_x: f32, max_y: f32) -> frus_widgets::Scrollable {
6677        frus_widgets::Scrollable {
6678            id: WidgetId::from_u64(id),
6679            viewport: Rect::new(0.0, 0.0, 100.0, 100.0),
6680            max_x,
6681            max_y,
6682            physics: None,
6683            refresh: None,
6684            page: None,
6685            reverse_x: false,
6686            reverse_y: false,
6687            host: None,
6688            keep_visible: None,
6689        }
6690    }
6691
6692    /// A strip that only slides sideways, sitting in a page that only scrolls down, must
6693    /// not swallow a drag downwards: the page behind it is what that drag is about
6694    /// (reported on a device, 2026-08-16).
6695    #[test]
6696    fn the_gesture_goes_to_the_area_that_can_take_it() {
6697        let strip = area(1, 400.0, 0.0);
6698        let page = area(2, 0.0, 900.0);
6699        let chain = [strip, page]; // innermost first, as the finger meets them
6700        assert_eq!(
6701            claim_area(&chain, strip.id, true).map(|a| a.id),
6702            Some(page.id),
6703            "down the page, over the strip"
6704        );
6705        assert_eq!(
6706            claim_area(&chain, strip.id, false).map(|a| a.id),
6707            Some(strip.id),
6708            "across the strip"
6709        );
6710        // Nothing behind it: the strip keeps the gesture and goes its own way, which is
6711        // what a lone recogniser in an arena does.
6712        assert_eq!(
6713            claim_area(&[strip], strip.id, true).map(|a| a.id),
6714            Some(strip.id)
6715        );
6716        // The innermost that can take it wins, not the outermost.
6717        let inner = area(3, 0.0, 200.0);
6718        assert_eq!(
6719            claim_area(&[inner, strip, page], inner.id, true).map(|a| a.id),
6720            Some(inner.id)
6721        );
6722    }
6723
6724    /// An end-of-content glow on a page with nothing to scroll says something about the
6725    /// content that is not true (reported on a device, 2026-08-16). The reference takes the
6726    /// drag recognisers away outright when everything fits.
6727    #[test]
6728    fn an_area_whose_content_fits_takes_no_finger() {
6729        let fits = area(1, 0.0, 0.0);
6730        assert!(!fits.accepts_user_offset((0.0, 0.0)));
6731        // Unless it is already displaced — content that shrank under a scrolled offset has
6732        // to be draggable back into place.
6733        assert!(fits.accepts_user_offset((0.0, 12.0)));
6734        // Or a refresh area is listening above it: a list of two items still pulls down.
6735        let mut pullable = fits;
6736        pullable.refresh = Some(WidgetId::from_u64(9));
6737        assert!(pullable.accepts_user_offset((0.0, 0.0)));
6738        // A single pixel out of sight is content to reveal, and enough.
6739        assert!(area(1, 0.0, 1.0).accepts_user_offset((0.0, 0.0)));
6740        assert!(area(1, 1.0, 0.0).accepts_user_offset((0.0, 0.0)));
6741    }
6742    use frus_widgets::WidgetId;
6743    use std::collections::HashSet;
6744
6745    /// An estimate reading `velocity` px/s after travelling `offset` px.
6746    fn released(velocity: (f32, f32), offset: (f32, f32)) -> VelocityEstimate {
6747        VelocityEstimate {
6748            velocity: frus_widgets::Velocity::new(velocity.0, velocity.1),
6749            confidence: 1.0,
6750            duration: 0.05,
6751            offset,
6752        }
6753    }
6754
6755    #[test]
6756    fn a_fast_twitch_that_went_nowhere_is_not_a_fling() {
6757        // 2000 px/s, but the finger covered 3 px: a wobble on lift-off, not a throw.
6758        let (x, y) = fling_velocity(released((0.0, 2000.0), (0.0, 3.0)), TOUCH_SLOP);
6759        assert_eq!((x, y), (0.0, 0.0));
6760        // The same speed over a real distance is.
6761        let (_, y) = fling_velocity(released((0.0, 2000.0), (0.0, 60.0)), TOUCH_SLOP);
6762        assert_eq!(y, 2000.0);
6763    }
6764
6765    #[test]
6766    fn the_fling_gate_is_per_axis() {
6767        // A vertical swipe with the sideways wobble a thumb always adds: the wobble
6768        // must not fling the content horizontally.
6769        let (x, y) = fling_velocity(released((300.0, 1500.0), (5.0, 120.0)), TOUCH_SLOP);
6770        assert_eq!(x, 0.0, "the wobble is not a horizontal fling");
6771        assert_eq!(y, 1500.0, "the swipe still flings vertically");
6772    }
6773
6774    #[test]
6775    fn a_precise_pointer_needs_almost_no_travel() {
6776        // The same 3 px that a finger's slop rejects is a deliberate mouse drag.
6777        let estimate = released((0.0, 900.0), (0.0, 3.0));
6778        assert_eq!(fling_velocity(estimate, TOUCH_SLOP).1, 0.0);
6779        assert_eq!(fling_velocity(estimate, PRECISE_SLOP).1, 900.0);
6780    }
6781
6782    #[test]
6783    fn insertion_line_sits_on_the_target_top_edge() {
6784        // The upper half hovered means inserting **before**: a band of thickness 4
6785        // centred on the top edge (y=100) of a target 200 wide.
6786        let line = drop_insertion_line(
6787            Rect::new(20.0, 100.0, 200.0, 44.0),
6788            4.0,
6789            false,
6790            ReorderAxis::Vertical,
6791            false,
6792        );
6793        assert_eq!(line.x, 20.0, "aligned to the target's left");
6794        assert_eq!(line.width, 200.0, "the target's full width");
6795        assert_eq!(line.y, 98.0, "centred on the top edge (100 - 4/2)");
6796        assert_eq!(line.height, 4.0);
6797    }
6798
6799    #[test]
6800    fn insertion_line_sits_on_the_target_bottom_edge_when_inserting_after() {
6801        // The lower half hovered means inserting **after**: the band slides to the
6802        // **bottom** edge (y = 100 + 44 = 144, centred → 142). Same width, same thickness.
6803        let line = drop_insertion_line(
6804            Rect::new(20.0, 100.0, 200.0, 44.0),
6805            4.0,
6806            true,
6807            ReorderAxis::Vertical,
6808            false,
6809        );
6810        assert_eq!(line.x, 20.0, "still aligned to the left");
6811        assert_eq!(line.width, 200.0, "the target's full width");
6812        assert_eq!(line.y, 142.0, "centred on the bottom edge (144 - 4/2)");
6813        assert_eq!(line.height, 4.0);
6814        // A vertical list reads down whichever way its text runs.
6815        let rtl = drop_insertion_line(
6816            Rect::new(20.0, 100.0, 200.0, 44.0),
6817            4.0,
6818            true,
6819            ReorderAxis::Vertical,
6820            true,
6821        );
6822        assert_eq!(rtl, line, "right to left changes nothing down a list");
6823    }
6824
6825    /// **A horizontal list's line stands on its end**, on the edge the row goes to: the
6826    /// right one after a row that reads left to right, the left one after a row that reads
6827    /// right to left — where what follows it is.
6828    #[test]
6829    fn a_horizontal_insertion_line_stands_on_the_edge_the_row_goes_to() {
6830        let chip = Rect::new(100.0, 20.0, 80.0, 48.0);
6831        let line = |after, rtl| drop_insertion_line(chip, 4.0, after, ReorderAxis::Horizontal, rtl);
6832        let before = line(false, false);
6833        assert_eq!(
6834            (before.x, before.y, before.width, before.height),
6835            (98.0, 20.0, 4.0, 48.0),
6836            "before a row: its left edge, the full height, 4 wide"
6837        );
6838        assert_eq!(
6839            line(true, false).x,
6840            178.0,
6841            "after it: its right edge (180 - 4/2)"
6842        );
6843        // Right to left, the two edges trade places.
6844        assert_eq!(
6845            line(true, true).x,
6846            98.0,
6847            "after, right to left: the left edge"
6848        );
6849        assert_eq!(
6850            line(false, true).x,
6851            178.0,
6852            "before, right to left: the right edge"
6853        );
6854    }
6855
6856    /// **Which gesture a reorderable gets.** A column slides; a row or a card is inserted
6857    /// between slots — and a horizontal reorderable is a row only when it says so, which is
6858    /// what keeps a table's columns exactly as they were.
6859    #[test]
6860    fn a_horizontal_reorderable_inserts_only_when_it_says_so() {
6861        assert_eq!(
6862            ReorderMotion::of(ReorderAxis::Horizontal, false),
6863            ReorderMotion::Columns,
6864            "a table's column"
6865        );
6866        assert_eq!(
6867            ReorderMotion::of(ReorderAxis::Horizontal, true),
6868            ReorderMotion::Slots(ReorderAxis::Horizontal),
6869            "a horizontal list's row"
6870        );
6871        // Every vertical reorderable inserts, whatever it answers.
6872        for inserts in [false, true] {
6873            assert_eq!(
6874                ReorderMotion::of(ReorderAxis::Vertical, inserts),
6875                ReorderMotion::Slots(ReorderAxis::Vertical)
6876            );
6877        }
6878        // A widget that answers nothing keeps the trait's defaults, which are the table's.
6879        assert_eq!(
6880            reorder_motion::<()>(&frus_widgets::Container::new()),
6881            ReorderMotion::Columns
6882        );
6883        // And the real rows say which they are, down and across.
6884        for (axis, expected) in [
6885            (
6886                ReorderAxis::Vertical,
6887                ReorderMotion::Slots(ReorderAxis::Vertical),
6888            ),
6889            (
6890                ReorderAxis::Horizontal,
6891                ReorderMotion::Slots(ReorderAxis::Horizontal),
6892            ),
6893        ] {
6894            let list = frus_widgets::ReorderableList::<()>::new(|_, _| ())
6895                .axis(axis)
6896                .row(frus_widgets::Container::new().width(60.0));
6897            let row = frus_widgets::Widget::children(&list)[0].as_ref();
6898            assert_eq!(reorder_motion(row), expected, "{axis:?}");
6899        }
6900    }
6901
6902    /// **The ghost stays in its lane.** A column rises and follows x; a card follows the
6903    /// finger anywhere; a horizontal list's row follows x and nothing else — so a finger
6904    /// that wanders below the strip does not drag the row off it.
6905    #[test]
6906    fn a_horizontal_rows_ghost_follows_the_finger_along_x_only() {
6907        let travel = (120.0, 35.0);
6908        assert_eq!(
6909            ghost_offset(ReorderMotion::Columns, travel),
6910            (120.0, super::drag_preview::LIFT_Y)
6911        );
6912        assert_eq!(
6913            ghost_offset(ReorderMotion::Slots(ReorderAxis::Vertical), travel),
6914            (120.0, 35.0)
6915        );
6916        assert_eq!(
6917            ghost_offset(ReorderMotion::Slots(ReorderAxis::Horizontal), travel),
6918            (120.0, 0.0)
6919        );
6920    }
6921
6922    #[test]
6923    fn focus_returns_to_trigger_when_overlay_closes() {
6924        let anchor = WidgetId::from_u64(1);
6925        let item = WidgetId::from_u64(2);
6926        let (mut history, mut prev) = (Vec::new(), None);
6927
6928        // Frame 1: focus on the anchor, which is present.
6929        let f = resolve_focus(
6930            Some(anchor),
6931            &HashSet::from([anchor]),
6932            &mut history,
6933            &mut prev,
6934        );
6935        assert_eq!(f, Some(anchor));
6936        assert!(history.is_empty());
6937
6938        // Frame 2: the menu is open and the item focused; the anchor is pushed as trigger.
6939        let present = HashSet::from([anchor, item]);
6940        let f = resolve_focus(Some(item), &present, &mut history, &mut prev);
6941        assert_eq!(f, Some(item));
6942        assert_eq!(history, vec![anchor]);
6943
6944        // Frame 3: the menu closed and the item vanished → back to the trigger, history spent.
6945        let f = resolve_focus(
6946            Some(item),
6947            &HashSet::from([anchor]),
6948            &mut history,
6949            &mut prev,
6950        );
6951        assert_eq!(f, Some(anchor), "focus returns to the trigger");
6952        assert!(history.is_empty());
6953    }
6954
6955    #[test]
6956    fn focus_falls_to_none_when_no_trigger_remains() {
6957        let a = WidgetId::from_u64(1);
6958        let (mut history, mut prev) = (Vec::new(), Some(a));
6959        // `a` vanishes and the history is empty → no focus at all.
6960        let f = resolve_focus(Some(a), &HashSet::new(), &mut history, &mut prev);
6961        assert_eq!(f, None);
6962        assert_eq!(prev, None);
6963    }
6964
6965    #[test]
6966    fn spring_approaches_target_monotonically_and_settles() {
6967        // From 0 toward 100: a monotonic approach with no overshoot, all but reached
6968        // after several 16 ms steps.
6969        let mut x = 0.0;
6970        let mut prev = -1.0;
6971        for _ in 0..30 {
6972            x = spring_toward(x, 100.0, 0.016, 0.07);
6973            assert!(x > prev && x <= 100.0, "monotonic and bounded: {x}");
6974            prev = x;
6975        }
6976        assert!(x > 99.0, "all but reached after ~0.5 s: {x}");
6977    }
6978
6979    #[test]
6980    fn ghost_card_shape() {
6981        let theme = Theme::default();
6982        let card = Rect::new(140.0, 0.0, 80.0, 34.0);
6983        // An empty ghost falls back to a shadow plus a solid bordered card: 2 primitives.
6984        let mut scene = Scene::new();
6985        draw_ghost_card(&mut scene, &theme, card, &[]);
6986        assert_eq!(scene.primitives().len(), 2, "shadow plus solid card");
6987    }
6988}
6989
6990/// The half of a scroll request that belongs to the shell: turning the **name** an
6991/// application wrote into the region a frame actually has.
6992#[cfg(test)]
6993mod sheet_request_tests {
6994    use super::{apply_sheet_requests, Runtime, Widget};
6995    use crate::command::Command;
6996    use frus_widgets::{
6997        build_ui, keyed, Container, DraggableScrollableSheet, SheetTo, Size, Theme,
6998    };
6999
7000    /// A page with one named sheet on it — the key on a wrapper the application wrote, the
7001    /// state on a panel it never sees.
7002    fn view() -> Box<dyn Widget<()>> {
7003        Box::new(
7004            Container::<()>::new()
7005                .width(400.0)
7006                .height(800.0)
7007                .child(keyed(
7008                    "places",
7009                    DraggableScrollableSheet::<()>::new(Container::new()),
7010                )),
7011        )
7012    }
7013
7014    /// **A request reaches the sheet the view named**, and nothing else: another key, or a
7015    /// frame without the sheet, hands it back unplaced and moves nothing.
7016    #[test]
7017    fn a_request_reaches_the_sheet_the_view_named() {
7018        let tree = view();
7019        let mut runtime = Runtime::default();
7020        let sheets = build_ui(
7021            tree.as_ref(),
7022            Size::new(400.0, 800.0),
7023            &runtime,
7024            &Theme::default(),
7025        )
7026        .sheets()
7027        .to_vec();
7028        let sheet = sheets.first().expect("the page has its sheet").clone();
7029
7030        let requests = Command::<()>::sheet("places", SheetTo::size(0.9))
7031            .into_parts()
7032            .sheets;
7033        let (unplaced, moved) =
7034            apply_sheet_requests(&mut runtime, tree.as_ref(), requests, &sheets);
7035        assert!(unplaced.is_empty());
7036        assert!(moved);
7037        assert_eq!(runtime.sheet_size(sheet.id, &sheet.spec), 0.9);
7038
7039        let elsewhere = Command::<()>::sheet("elsewhere", SheetTo::size(0.3))
7040            .into_parts()
7041            .sheets;
7042        let (unplaced, moved) =
7043            apply_sheet_requests(&mut runtime, tree.as_ref(), elsewhere, &sheets);
7044        assert_eq!(unplaced.len(), 1);
7045        assert!(!moved);
7046        let not_yet = Command::<()>::sheet("places", SheetTo::size(0.3))
7047            .into_parts()
7048            .sheets;
7049        let (unplaced, _) = apply_sheet_requests(&mut runtime, tree.as_ref(), not_yet, &[]);
7050        assert_eq!(unplaced.len(), 1, "kept for the frame that has the sheet");
7051        assert_eq!(
7052            runtime.sheet_size(sheet.id, &sheet.spec),
7053            0.9,
7054            "nothing moved"
7055        );
7056    }
7057}
7058
7059#[cfg(test)]
7060mod scroll_request_tests {
7061    use super::{apply_scroll_requests, Rect, Runtime, Scrollable, Widget, WidgetId};
7062    use crate::command::Command;
7063    use frus_widgets::{keyed, Container, ScrollTo, SingleChildScrollView};
7064
7065    /// A view with one named scroll region in it, and nothing else of interest.
7066    fn view() -> Box<dyn Widget<()>> {
7067        Box::new(Container::<()>::new().child(keyed(
7068            "log",
7069            SingleChildScrollView::<()>::new().height(200.0),
7070        )))
7071    }
7072
7073    /// The region as the frame would register it: a 200-tall window over 1 000 of content.
7074    fn region(id: WidgetId) -> Scrollable {
7075        Scrollable {
7076            id,
7077            viewport: Rect {
7078                x: 0.0,
7079                y: 0.0,
7080                width: 300.0,
7081                height: 200.0,
7082            },
7083            max_x: 0.0,
7084            max_y: 1000.0,
7085            physics: None,
7086            refresh: None,
7087            page: None,
7088            reverse_x: false,
7089            reverse_y: false,
7090            keep_visible: None,
7091            host: None,
7092        }
7093    }
7094
7095    /// The key a request carries and the key the view declared are the **same hash** —
7096    /// which is the whole of the identity design, and the one thing that cannot be
7097    /// checked on either side alone.
7098    #[test]
7099    fn a_request_reaches_the_region_the_view_named() {
7100        let tree = view();
7101        let requests = Command::<()>::scroll("log", ScrollTo::end().instant())
7102            .into_parts()
7103            .scrolls;
7104        let id = frus_widgets::find_by_key(tree.as_ref(), requests[0].0)
7105            .expect("the view named it, so the tree knows it");
7106        let mut runtime = Runtime::default();
7107        let (unplaced, moved) =
7108            apply_scroll_requests(&mut runtime, tree.as_ref(), requests, &[region(id)]);
7109        assert!(unplaced.is_empty());
7110        assert!(moved);
7111        assert_eq!(runtime.scroll.get(&id), Some(&(0.0, 1000.0)));
7112    }
7113
7114    /// A region that has only just appeared is not in the registry the request is first
7115    /// tried against — that one was built last frame — so it is handed back rather than
7116    /// thrown away. Coming back to a screen and being put where you left off depends on
7117    /// exactly this.
7118    #[test]
7119    fn a_region_this_registry_has_not_got_is_handed_back() {
7120        let tree = view();
7121        let requests = Command::<()>::scroll("log", ScrollTo::y(120.0))
7122            .into_parts()
7123            .scrolls;
7124        let mut runtime = Runtime::default();
7125        let (unplaced, moved) = apply_scroll_requests(&mut runtime, tree.as_ref(), requests, &[]);
7126        assert_eq!(unplaced.len(), 1, "kept for the second attempt");
7127        assert!(!moved);
7128        assert!(runtime.scroll_target.is_empty());
7129    }
7130
7131    /// A key naming nothing at all leaves no trace: the caller drops what comes back,
7132    /// so a typo costs one frame's lookup and never accumulates.
7133    #[test]
7134    fn a_name_the_view_does_not_use_moves_nothing() {
7135        let tree = view();
7136        let requests = Command::<()>::scroll("ledger", ScrollTo::start())
7137            .into_parts()
7138            .scrolls;
7139        let mut runtime = Runtime::default();
7140        let id = frus_widgets::find_by_key(
7141            tree.as_ref(),
7142            Command::<()>::scroll("log", ScrollTo::start())
7143                .into_parts()
7144                .scrolls[0]
7145                .0,
7146        )
7147        .expect("the real one is there");
7148        let (unplaced, moved) =
7149            apply_scroll_requests(&mut runtime, tree.as_ref(), requests, &[region(id)]);
7150        assert_eq!(unplaced.len(), 1);
7151        assert!(!moved);
7152        assert!(runtime.scroll.is_empty() && runtime.scroll_target.is_empty());
7153    }
7154
7155    /// Two requests in one batch, both placed, and the batch is what `Command::batch`
7156    /// produces — a screen that restores both axes writes two and means two.
7157    #[test]
7158    fn a_batch_places_every_request_it_carries() {
7159        let tree = view();
7160        let requests = Command::<()>::batch([
7161            Command::scroll("log", ScrollTo::y(400.0).instant()),
7162            Command::scroll("log", ScrollTo::y(600.0).instant()),
7163        ])
7164        .into_parts()
7165        .scrolls;
7166        assert_eq!(requests.len(), 2);
7167        let id = frus_widgets::find_by_key(tree.as_ref(), requests[0].0).expect("named");
7168        let mut runtime = Runtime::default();
7169        let (unplaced, moved) =
7170            apply_scroll_requests(&mut runtime, tree.as_ref(), requests, &[region(id)]);
7171        assert!(unplaced.is_empty() && moved);
7172        assert_eq!(
7173            runtime.scroll.get(&id),
7174            Some(&(0.0, 600.0)),
7175            "the last request is the current statement"
7176        );
7177    }
7178}
7179
7180#[cfg(test)]
7181mod autoscroll_tests {
7182    use super::{
7183        edge_autoscroll, follow_content, Drag, Point, Rect, WidgetId, AUTOSCROLL_MAX_OVERHANG,
7184        AUTOSCROLL_VELOCITY,
7185    };
7186
7187    /// A viewport from 100 to 500, with room to scroll either way.
7188    const VIEW: (f32, f32) = (100.0, 500.0);
7189    const ROOM: (f32, f32) = (1000.0, 1000.0);
7190    /// A sixtieth of a second, which is the frame this is asked on.
7191    const DT: f32 = 1.0 / 60.0;
7192
7193    #[test]
7194    fn a_row_inside_the_viewport_scrolls_nothing() {
7195        assert_eq!(edge_autoscroll((200.0, 260.0), VIEW, ROOM, DT), None);
7196        // Touching the edge exactly is still inside: the row has not asked for anything.
7197        assert_eq!(edge_autoscroll((440.0, 500.0), VIEW, ROOM, DT), None);
7198    }
7199
7200    /// The law: pixels per second per pixel of overhang, so ten pixels out is ten times
7201    /// the speed of one pixel out — the row is not dragged along by a fixed nudge.
7202    #[test]
7203    fn the_speed_follows_how_far_out_the_row_is() {
7204        let near = edge_autoscroll((460.0, 501.0), VIEW, ROOM, DT).expect("one pixel out");
7205        let far = edge_autoscroll((460.0, 510.0), VIEW, ROOM, DT).expect("ten pixels out");
7206        assert!(
7207            near < 0.0,
7208            "the content moves up to reveal what is below: {near}"
7209        );
7210        assert!(
7211            (far / near - 10.0).abs() < 0.01,
7212            "ten times as far, ten times as fast: {near} vs {far}"
7213        );
7214        assert!(
7215            (near + 1.0 * AUTOSCROLL_VELOCITY * DT).abs() < 1e-4,
7216            "one pixel out, at the stated velocity: {near}"
7217        );
7218    }
7219
7220    /// Carrying a row right off the window is not a request to scroll a thousand times
7221    /// faster; past the cap the speed stops growing.
7222    #[test]
7223    fn the_overhang_is_capped() {
7224        let far = edge_autoscroll((460.0, 520.0), VIEW, ROOM, DT).expect("twenty out");
7225        let absurd = edge_autoscroll((460.0, 5000.0), VIEW, ROOM, DT).expect("off the window");
7226        assert!((far - absurd).abs() < 1e-4, "{far} vs {absurd}");
7227        assert!((absurd + AUTOSCROLL_MAX_OVERHANG * AUTOSCROLL_VELOCITY * DT).abs() < 1e-4);
7228    }
7229
7230    /// Above the top the content moves the other way, and the sign is the whole of the
7231    /// difference between the two edges.
7232    #[test]
7233    fn above_the_top_the_content_comes_down() {
7234        let up = edge_autoscroll((90.0, 150.0), VIEW, ROOM, DT).expect("ten above");
7235        assert!(up > 0.0, "{up}");
7236    }
7237
7238    /// **What is carried stays under the finger while its list scrolls.**
7239    ///
7240    /// Seen on a phone: a row carried to the bottom edge started the list scrolling, and
7241    /// the ghost rode up with the content — it is drawn at the row's box, and the box had
7242    /// moved — until it no longer hung over the edge and the scroll stopped by itself,
7243    /// 95 px in. The press moves with the content instead.
7244    #[test]
7245    fn a_carried_item_follows_its_content() {
7246        // The list scrolled on by 95 px: its content went 95 px up the screen.
7247        let shift = (0.0, -95.0);
7248        let pressed = Point::new(900.0, 1330.0);
7249        let row_then = Rect::new(105.0, 1250.0, 700.0, 160.0);
7250        let row_now = row_then.translate(0.0, -95.0);
7251        let finger = Point::new(900.0, 2030.0);
7252
7253        let mut reorder = Drag::Reorder {
7254            id: WidgetId::from_u64(1),
7255            from: 0,
7256            start: pressed,
7257            moved: true,
7258            carried: Some((WidgetId::from_u64(1), row_then)),
7259        };
7260        follow_content(&mut reorder, shift);
7261        let Drag::Reorder { start, carried, .. } = reorder else {
7262            unreachable!("still a reorder");
7263        };
7264        // The box it was carried from went with the content, as the row did — and the frame's
7265        // own box is not asked again, since the frame clips a row carried to an edge and loses
7266        // one scrolled out of sight (milestone 527).
7267        let (_, carried) = carried.expect("the box it was carried from");
7268        assert_eq!(carried, row_now, "the carried box went with the content");
7269        // Drawn the way the shell draws it: the carried box, plus the travel.
7270        let ghost = carried.translate(finger.x - start.x, finger.y - start.y);
7271        assert_eq!(
7272            ghost,
7273            row_then.translate(finger.x - pressed.x, finger.y - pressed.y),
7274            "the ghost is where it would be had nothing scrolled"
7275        );
7276
7277        let mut item = Drag::Item {
7278            source: frus_widgets::DragSource {
7279                id: WidgetId::from_u64(1),
7280                rect: row_then,
7281                payload: 7,
7282            },
7283            start: pressed,
7284            moved: true,
7285            over: None,
7286        };
7287        follow_content(&mut item, shift);
7288        let Drag::Item { source, start, .. } = item else {
7289            unreachable!("still an item");
7290        };
7291        assert_eq!(
7292            source.rect, row_now,
7293            "the box taken at the press went with the content"
7294        );
7295        assert_eq!(start, Point::new(900.0, 1235.0), "and so did the press");
7296    }
7297
7298    /// At the end of the content there is nothing left to reveal, so the list stays where
7299    /// it is instead of straining against its own end.
7300    #[test]
7301    fn an_exhausted_edge_stays_still() {
7302        assert_eq!(
7303            edge_autoscroll((460.0, 520.0), VIEW, (1000.0, 0.0), DT),
7304            None
7305        );
7306        assert_eq!(
7307            edge_autoscroll((90.0, 150.0), VIEW, (0.0, 1000.0), DT),
7308            None
7309        );
7310    }
7311
7312    /// A row taller than the viewport hangs over both edges at once. Pulled two ways it
7313    /// would scroll nowhere, or jitter between them; the leading edge decides.
7314    #[test]
7315    fn a_row_taller_than_the_viewport_follows_its_leading_edge() {
7316        let both = edge_autoscroll((50.0, 900.0), VIEW, ROOM, DT).expect("over both edges");
7317        assert!(both > 0.0, "the top edge wins: {both}");
7318    }
7319}
7320
7321/// **A back gesture the page below cannot take** (milestone 531).
7322///
7323/// Seen on a phone: a back gesture from the left edge, at the height of a task row of the
7324/// page below, did not slide the page on top. Half a second in, the row was lifted instead —
7325/// drawn over the page on top in its green-outlined ghost, and following the finger — and it
7326/// went away when the finger did. At a height with no row the gesture worked.
7327///
7328/// The press that starts the gesture is tested against the frame on screen, and after a push
7329/// that frame is the push's last: an application's animation that has just settled asks for
7330/// no rebuild, so the page the push left is still in the frame, parallaxed to the left and
7331/// under the finger. The press found the row's hold, the long press armed, and its deadline
7332/// replaced the gesture with a lift.
7333#[cfg(test)]
7334mod back_gesture_tests {
7335    use super::{
7336        drag_after_hold, frame_needs_build, hold_candidates, Drag, Point, Theme, WidgetId,
7337    };
7338    use frus_widgets::{build_ui, column, Container, DragSource, Draggable, Navigator};
7339    use frus_widgets::{Rect, Runtime, Size, Ui, Widget};
7340
7341    /// A phone, in logical pixels.
7342    const W: f32 = 392.0;
7343    const H: f32 = 850.0;
7344    /// Where the row sits on the page below, and the finger's press on the edge at its
7345    /// height.
7346    const ROW_TOP: f32 = 480.0;
7347    const ROW_HEIGHT: f32 = 66.0;
7348    const ON_THE_ROW: Point = Point::new(3.0, 509.0);
7349    const ABOVE_THE_ROW: Point = Point::new(3.0, 200.0);
7350    /// The long press the row asks for.
7351    const HELD: u8 = 1;
7352
7353    /// A page with one row that lifts on a hold and says something when held, below a
7354    /// stretch with nothing to take.
7355    fn home() -> impl Widget<u8> {
7356        column![
7357            Container::<u8>::new().width(W).height(ROW_TOP),
7358            Draggable::new(
7359                Container::<u8>::new()
7360                    .width(W)
7361                    .height(ROW_HEIGHT)
7362                    .on_long_press(HELD)
7363            )
7364            .payload(7)
7365            .long_press(),
7366        ]
7367    }
7368
7369    /// The frame on screen after a push from `home` has settled: the push's last, with the
7370    /// page it left still in it.
7371    fn after_a_push() -> Navigator<u8> {
7372        Navigator::new("data", Container::<u8>::new().width(W).height(H))
7373            .size(W, H)
7374            .from("home", home(), 0.999, true)
7375    }
7376
7377    fn frame(root: &dyn Widget<u8>) -> Ui<u8> {
7378        build_ui(
7379            root,
7380            Size::new(W, H),
7381            &Runtime::default(),
7382            &Theme::default(),
7383        )
7384    }
7385
7386    fn back(at: Point) -> Drag {
7387        Drag::Back { start_x: at.x }
7388    }
7389
7390    fn still_scroll() -> Drag {
7391        Drag::Scroll {
7392            id: WidgetId::from_u64(9),
7393            last: ON_THE_ROW,
7394            moved: false,
7395            carried: (0.0, 0.0),
7396            dismiss: None,
7397            axis: None,
7398        }
7399    }
7400
7401    fn row_source() -> DragSource {
7402        DragSource {
7403            id: WidgetId::from_u64(3),
7404            rect: Rect::new(-117.0, ROW_TOP, W, ROW_HEIGHT),
7405            payload: 7,
7406        }
7407    }
7408
7409    /// **Why the row was under the finger at all**: the frame a transition ends on is built
7410    /// again. The tick that ends a push takes the page it left out of the state and reports
7411    /// nothing moving, so a loop that builds only while something moves hit-tests the
7412    /// push's last frame from then on — the page left behind still in it, where a tap or a
7413    /// hold can reach what is no longer on screen. The demo's
7414    /// `the_last_frame_of_a_push_still_holds_the_page_it_left` shows that frame on its own
7415    /// page, at the phone's coordinates.
7416    #[test]
7417    fn the_frame_an_animation_settles_in_is_built_again() {
7418        assert!(
7419            frame_needs_build(false, false, true, false),
7420            "the frame the application's animation stops in builds the view again"
7421        );
7422        assert!(
7423            frame_needs_build(false, true, true, false),
7424            "as does every frame while it moves"
7425        );
7426        assert!(
7427            !frame_needs_build(false, false, false, false),
7428            "and a still frame after a still one repaints the tree it has"
7429        );
7430        assert!(frame_needs_build(true, false, false, false), "unless asked");
7431        assert!(
7432            frame_needs_build(false, false, false, true),
7433            "or the frame has its own reason"
7434        );
7435    }
7436
7437    /// The reproduction: the frame really has a row to take under the finger, and a back
7438    /// gesture's press takes nothing from it.
7439    #[test]
7440    fn a_back_gesture_claims_nothing_under_its_finger() {
7441        let root = after_a_push();
7442        let ui = frame(&root);
7443        let (held, lift) = hold_candidates(None, Some(&ui), Some(&root), ON_THE_ROW);
7444        assert!(
7445            lift.is_some() && held == Some(HELD),
7446            "the frame the phone had: a press there, with no gesture, finds the row's hold"
7447        );
7448
7449        let gesture = back(ON_THE_ROW);
7450        let (held, lift) = hold_candidates(Some(&gesture), Some(&ui), Some(&root), ON_THE_ROW);
7451        assert!(
7452            lift.is_none(),
7453            "a back gesture's press arms no lift of the row under it"
7454        );
7455        assert_eq!(held, None, "nor its long press");
7456    }
7457
7458    /// Whatever a press found, the deadline never takes the pointer from a back gesture:
7459    /// the gesture goes on, and keeps its start.
7460    #[test]
7461    fn a_hold_never_takes_the_pointer_from_a_back_gesture() {
7462        let reorder = Some((WidgetId::from_u64(4), 2, ON_THE_ROW));
7463        for (lift, reorder) in [
7464            (Some(row_source()), None),
7465            (None, reorder),
7466            (Some(row_source()), reorder),
7467            (None, None),
7468        ] {
7469            let after = drag_after_hold(Some(back(ON_THE_ROW)), lift, reorder, ON_THE_ROW);
7470            assert!(
7471                matches!(after, Some(Drag::Back { start_x }) if start_x == ON_THE_ROW.x),
7472                "the back gesture is still the drag (lift {}, reorder {})",
7473                lift.is_some(),
7474                reorder.is_some()
7475            );
7476        }
7477    }
7478
7479    /// At a height with nothing to take — where the gesture always worked — it still does.
7480    #[test]
7481    fn a_back_gesture_where_nothing_is_under_it_still_owns_the_pointer() {
7482        let root = after_a_push();
7483        let ui = frame(&root);
7484        let gesture = back(ABOVE_THE_ROW);
7485        let (held, lift) = hold_candidates(Some(&gesture), Some(&ui), Some(&root), ABOVE_THE_ROW);
7486        assert!(held.is_none() && lift.is_none());
7487        assert!(matches!(
7488            drag_after_hold(Some(gesture), None, None, ABOVE_THE_ROW),
7489            Some(Drag::Back { .. })
7490        ));
7491    }
7492
7493    /// No regression: a hold on the row with no back gesture — the list's scroll still
7494    /// waiting to see whether the finger moves — lifts it, or its reorder, and says what
7495    /// the row asks when held.
7496    #[test]
7497    fn a_hold_on_a_row_without_a_back_gesture_still_lifts_it() {
7498        let root = home();
7499        let ui = frame(&root);
7500        let at = Point::new(40.0, ROW_TOP + 10.0);
7501        let scroll = still_scroll();
7502        let (held, lift) = hold_candidates(Some(&scroll), Some(&ui), Some(&root), at);
7503        assert_eq!(
7504            held,
7505            Some(HELD),
7506            "the long press is armed under a still scroll"
7507        );
7508        assert!(lift.is_some(), "and so is the lift");
7509        let (held, lift) = hold_candidates(None, Some(&ui), Some(&root), at);
7510        assert!(held.is_some() && lift.is_some(), "and with no drag at all");
7511
7512        let cursor = Point::new(41.0, ROW_TOP + 11.0);
7513        let lifted = drag_after_hold(Some(still_scroll()), Some(row_source()), None, cursor);
7514        assert!(
7515            matches!(lifted, Some(Drag::Item { moved: true, start, .. }) if start == cursor),
7516            "the hold lifts the item where the finger is"
7517        );
7518        let reorder = Some((WidgetId::from_u64(4), 2, at));
7519        let reordering = drag_after_hold(Some(still_scroll()), None, reorder, cursor);
7520        assert!(
7521            matches!(reordering, Some(Drag::Reorder { moved: true, from: 2, start, .. }) if start == at),
7522            "or picks the row up for a reorder, from where it was pressed"
7523        );
7524        assert!(
7525            drag_after_hold(Some(still_scroll()), None, None, cursor).is_none(),
7526            "a hold with nothing to lift ends the still scroll: the long press had it"
7527        );
7528    }
7529}
7530
7531/// A driver with no window and no event loop: input and frames fed by hand, through the
7532/// shell's own paths.
7533#[cfg(any(test, feature = "testing"))]
7534#[doc(hidden)]
7535pub mod testing {
7536    use super::*;
7537    use crate::gesture::{PointerEvent, PointerKind, LONG_PRESS_DELAY};
7538
7539    /// What the last frame left behind: the interface it laid out, and the tree it laid out from.
7540    pub type FrameParts<'a, Msg> = (&'a Ui<Msg>, &'a dyn Widget<Msg>);
7541
7542    /// The shell around an application, on a surface of a stated logical size.
7543    pub struct Driver<A: Application> {
7544        shell: App<A>,
7545        size: Size,
7546        /// Where the last frame drew a reorder's ghost, when it drew one.
7547        ghost: Option<Rect>,
7548    }
7549
7550    impl<A: Application> Driver<A> {
7551        /// A driver for `app` on a `width` by `height` logical surface.
7552        pub fn new(app: A, width: f32, height: f32) -> Self {
7553            Self {
7554                shell: App::detached(app),
7555                size: Size::new(width, height),
7556                ghost: None,
7557            }
7558        }
7559
7560        /// Where what is carried is drawn this frame — a reorder's ghost or a lifted item —
7561        /// or `None` when nothing is.
7562        pub fn carried(&self) -> Option<Rect> {
7563            self.shell.carried_rect()
7564        }
7565
7566        /// Where the last frame drew a reorder's ghost — the outline around it — or `None`
7567        /// when it drew none.
7568        pub fn ghost(&self) -> Option<Rect> {
7569            self.ghost
7570        }
7571
7572        /// Every scroll region of the last frame, as its viewport and its offset.
7573        pub fn offsets(&self) -> Vec<(Rect, (f32, f32))> {
7574            let s = &self.shell;
7575            s.ui.as_ref()
7576                .map(|ui| {
7577                    ui.scroll_regions()
7578                        .iter()
7579                        .map(|a| {
7580                            (
7581                                a.viewport,
7582                                s.runtime.scroll.get(&a.id).copied().unwrap_or((0.0, 0.0)),
7583                            )
7584                        })
7585                        .collect()
7586                })
7587                .unwrap_or_default()
7588        }
7589
7590        /// The interface the last frame laid out — what a tap is asked about, what the registries
7591        /// hold — and the tree it was laid out from.
7592        pub fn frame_parts(&self) -> Option<FrameParts<'_, A::Message>> {
7593            let s = &self.shell;
7594            Some((s.ui.as_ref()?, s.tree.as_deref()?))
7595        }
7596
7597        /// The words the last frame drew and the box each was drawn in, in paint order.
7598        pub fn texts(&self) -> Vec<(String, Rect)> {
7599            self.shell
7600                .ui
7601                .as_ref()
7602                .map(|ui| {
7603                    ui.scene()
7604                        .primitives()
7605                        .iter()
7606                        .filter_map(|p| match p {
7607                            frus_widgets::Primitive::Text { text, .. } => {
7608                                Some((text.clone(), p.bounds()))
7609                            }
7610                            _ => None,
7611                        })
7612                        .collect()
7613                })
7614                .unwrap_or_default()
7615        }
7616
7617        /// A tap on the first word of the last frame that reads `label`, and a frame after it:
7618        /// how a test presses a button by what it says. Whether there was such a word.
7619        pub fn tap_text(&mut self, label: &str) -> bool {
7620            let Some((_, rect)) = self.texts().into_iter().find(|(text, _)| text == label) else {
7621                return false;
7622            };
7623            let at = Point::new(rect.x + rect.width / 2.0, rect.y + rect.height / 2.0);
7624            self.press(at);
7625            self.release(at);
7626            self.frame(1.0 / 60.0);
7627            true
7628        }
7629
7630        /// The application, as the shell holds it.
7631        pub fn app(&self) -> &A {
7632            &self.shell.app
7633        }
7634
7635        /// Hands the application a message, as the shell does.
7636        pub fn update(&mut self, message: A::Message) {
7637            self.shell.dispatch(message);
7638        }
7639
7640        /// Frames at sixty a second for `seconds`.
7641        pub fn run(&mut self, seconds: f32) {
7642            let n = (seconds * 60.0).round() as usize;
7643            for _ in 0..n.max(1) {
7644                self.frame(1.0 / 60.0);
7645            }
7646        }
7647
7648        /// A finger down at `at`.
7649        pub fn press(&mut self, at: Point) {
7650            self.pointer(PointerKind::Down, at);
7651        }
7652
7653        /// The finger moved to `at`.
7654        pub fn move_to(&mut self, at: Point) {
7655            self.pointer(PointerKind::Move, at);
7656        }
7657
7658        /// The finger lifted at `at`.
7659        pub fn release(&mut self, at: Point) {
7660            self.pointer(PointerKind::Up, at);
7661        }
7662
7663        /// The long-press deadline, delivered as the loop's wake delivers it. Whether it
7664        /// fired: a press that moved past the slop, or was never waiting, has none.
7665        pub fn hold_deadline(&mut self) -> bool {
7666            let fired = self.shell.press.poll(Instant::now() + LONG_PRESS_DELAY);
7667            if fired {
7668                self.shell.hold_deadline_reached();
7669            }
7670            fired
7671        }
7672
7673        fn pointer(&mut self, kind: PointerKind, at: Point) {
7674            self.shell.pointer_event(PointerEvent {
7675                kind,
7676                position: at,
7677                touch: true,
7678            });
7679        }
7680
7681        /// One frame, the real frame's steps in the real frame's order, less the GPU.
7682        pub fn frame(&mut self, dt: f32) {
7683            let (width, height) = (self.size.width, self.size.height);
7684            let s = &mut self.shell;
7685            if s.last_size != Some((width, height)) {
7686                s.last_size = Some((width, height));
7687                s.build_dirty = true;
7688                s.app.on_resize(width, height);
7689            }
7690            let mut app_animating = s.app.tick(dt);
7691            let settings = s
7692                .platform
7693                .accessibility
7694                .with_overrides(s.app.accessibility());
7695            let theme_moved = s.themes.advance(
7696                &s.app,
7697                s.platform.brightness,
7698                settings.high_contrast,
7699                settings.disable_animations,
7700                dt,
7701            );
7702            app_animating |= theme_moved | s.themes.animating();
7703            let theme = s.themes.displayed(&s.app);
7704            install_ambient(&s.app, &mut s.runtime, &s.platform.locales);
7705            let _surface = s.media_query(width, height).install();
7706            s.runtime.still = settings.disable_animations;
7707            let was = std::mem::replace(&mut s.app_was_animating, app_animating);
7708            let asked = s.app.effects();
7709            s.run_command(asked);
7710            if frus_widgets::take_rebuild_request() {
7711                s.build_dirty = true;
7712            }
7713            if frame_needs_build(
7714                s.build_dirty,
7715                app_animating,
7716                was,
7717                s.tree.is_none() || s.runtime.switching(),
7718            ) {
7719                s.tree = Some(build_view(&s.app, &theme, &s.runtime));
7720            }
7721            s.build_dirty = false;
7722            s.autoscroll_carried(dt);
7723            s.advance_reorder_springs(dt);
7724            let regions =
7725                s.ui.as_ref()
7726                    .map(|ui| ui.scroll_regions().to_vec())
7727                    .unwrap_or_default();
7728            let physics = s.app.scroll_physics();
7729            s.runtime.sync_pages(&regions);
7730            s.runtime.sync_visible(&regions);
7731            s.runtime.advance(dt);
7732            s.runtime.advance_scroll(&regions, physics, dt);
7733            let tree = s.tree.as_deref().expect("the view was built");
7734            let ui = build_ui(tree, Size::new(width, height), &s.runtime, &theme);
7735            s.runtime.states.end_frame();
7736            let mut scene = ui.scene().clone();
7737            let ghost = if matches!(s.drag, Some(Drag::Reorder { moved: true, .. })) {
7738                s.paint_reorder_preview(&ui, &theme, &mut scene);
7739                // The ghost's outline is the last thing the preview draws, on the ghost's box.
7740                scene.primitives().last().map(|p| p.bounds())
7741            } else {
7742                None
7743            };
7744            let paged = ui.scroll_regions().to_vec();
7745            let scrolled: Vec<A::Message> = {
7746                let grain = |id| find_widget(tree, id).map_or(0.0, |w| w.scroll_grain());
7747                s.runtime
7748                    .scroll_changes(&paged, grain)
7749                    .into_iter()
7750                    .filter_map(|(id, position)| {
7751                        find_widget(tree, id).and_then(|w| w.on_scroll(position))
7752                    })
7753                    .collect()
7754            };
7755            s.ui = Some(ui);
7756            for message in scrolled {
7757                s.dispatch(message);
7758            }
7759            self.ghost = ghost;
7760        }
7761    }
7762}
7763
7764/// **What is carried stays carried, however far its list scrolls** (milestone 527, seen on
7765/// a phone).
7766///
7767/// A label held on the Kanban screen's strip and carried towards the strip's right edge was
7768/// not drawn lifted, the strip scrolled away under it, and the release put it somewhere the
7769/// finger had never been. The carried row's box came from the frame, and the frame keeps a
7770/// reorderable's box only while it is on screen, clipped to what shows: once auto-scroll had
7771/// pushed the row against the edge the box stopped moving with the content while the press
7772/// kept moving, the ghost ran ahead of the finger, the scroll fed on it, and a row scrolled
7773/// out of sight had no box at all.
7774///
7775/// These drive the shell's own input path and its frame, on the screen's shape: a bar, a strip
7776/// of ten 96-px labels that lift on a hold, and a board of cards, each in a horizontal scroll.
7777#[cfg(test)]
7778mod carried_row_tests {
7779    use super::testing::Driver;
7780    use crate::{Application, Command};
7781    use frus_widgets::{
7782        column, text, Axis, Button, CellFn, Container, Flex, Kanban, Point, Rect, ReorderAxis,
7783        ReorderGrab, ReorderableList, SingleChildScrollView, Theme, Widget,
7784    };
7785
7786    /// A phone, in logical pixels.
7787    const W: f32 = 392.7;
7788    const H: f32 = 850.9;
7789    /// The height of the labels' middle, under the 56-px bar and the strip's 12 px of padding.
7790    const STRIP_Y: f32 = 88.0;
7791    /// How wide the strip's content is: ten labels, nine gaps, and 24 px at either end.
7792    const STRIP_CONTENT: f32 = 24.0 + 10.0 * 96.0 + 9.0 * 8.0 + 24.0;
7793
7794    #[derive(Clone, Debug, PartialEq)]
7795    enum Board {
7796        Label(usize, usize),
7797        Card(usize, usize, usize, usize),
7798        Delete(usize, usize),
7799        Add(usize),
7800        Row(usize, usize),
7801    }
7802
7803    /// The Kanban screen, in the shape that matters.
7804    struct BoardApp {
7805        labels: Vec<usize>,
7806        cards: Vec<Vec<String>>,
7807        log: Vec<Board>,
7808    }
7809
7810    impl Default for BoardApp {
7811        fn default() -> Self {
7812            let cols: [&[&str]; 3] = [
7813                &["Design API", "Write spec", "Triage bugs"],
7814                &["Build widget"],
7815                &["Kickoff", "Research"],
7816            ];
7817            Self {
7818                labels: (0..10).collect(),
7819                cards: cols
7820                    .iter()
7821                    .map(|c| c.iter().map(|s| s.to_string()).collect())
7822                    .collect(),
7823                log: Vec::new(),
7824            }
7825        }
7826    }
7827
7828    impl Application for BoardApp {
7829        type Message = Board;
7830
7831        fn update(&mut self, message: Board) -> Command<Board> {
7832            self.log.push(message.clone());
7833            match message {
7834                Board::Label(from, to) => {
7835                    let label = self.labels.remove(from);
7836                    self.labels.insert(to, label);
7837                }
7838                Board::Card(from_col, from_pos, to_col, to_pos) => {
7839                    let card = self.cards[from_col].remove(from_pos);
7840                    let to_pos = to_pos.min(self.cards[to_col].len());
7841                    self.cards[to_col].insert(to_pos, card);
7842                }
7843                _ => {}
7844            }
7845            Command::none()
7846        }
7847
7848        fn view(&self, theme: &Theme) -> Box<dyn Widget<Board>> {
7849            let mut strip = ReorderableList::new(Board::Label)
7850                .axis(ReorderAxis::Horizontal)
7851                .grab(ReorderGrab::LongPress)
7852                .gap(8.0);
7853            for &label in &self.labels {
7854                strip = strip.keyed_row(
7855                    label as u64,
7856                    Container::new()
7857                        .width(96.0)
7858                        .padding(12.0)
7859                        .color(theme.surface)
7860                        .child(text(format!("label {label}")).size(14.0)),
7861                );
7862            }
7863            let strip = SingleChildScrollView::new()
7864                .axis(Axis::Horizontal)
7865                .width(W)
7866                .child(
7867                    Container::new()
7868                        .padding_each(12.0, 24.0, 0.0, 24.0)
7869                        .child(strip),
7870                );
7871            let mut board = Kanban::new(Board::Card)
7872                .on_add(Board::Add)
7873                .scrollable_columns();
7874            for (col, title) in ["To do", "Doing", "Done"].iter().enumerate() {
7875                let cards: Vec<CellFn<Board>> = self.cards[col]
7876                    .iter()
7877                    .enumerate()
7878                    .map(|(pos, label)| {
7879                        let label = label.clone();
7880                        Box::new(move || {
7881                            Box::new(
7882                                Flex::row()
7883                                    .child(text(label.clone()).size(14.0))
7884                                    .child(Flex::row().flex(1.0))
7885                                    .child(Button::new("x").on_press(Board::Delete(col, pos))),
7886                            ) as Box<dyn Widget<Board>>
7887                        }) as CellFn<Board>
7888                    })
7889                    .collect();
7890                board = board.column_widgets(*title, cards);
7891            }
7892            let board = SingleChildScrollView::new()
7893                .axis(Axis::Horizontal)
7894                .width(W)
7895                .flex(1.0)
7896                .child(Container::new().padding(24.0).child(board));
7897            Box::new(
7898                Container::new().width(W).height(H).child(
7899                    column![
7900                        Container::<Board>::new().width(W).height(56.0),
7901                        strip,
7902                        board
7903                    ]
7904                    .flex(1.0),
7905                ),
7906            )
7907        }
7908    }
7909
7910    /// Twenty 60-px rows that lift on a hold, in a list taller than the window.
7911    #[derive(Default)]
7912    struct ListApp {
7913        log: Vec<Board>,
7914    }
7915
7916    impl Application for ListApp {
7917        type Message = Board;
7918
7919        fn update(&mut self, message: Board) -> Command<Board> {
7920            self.log.push(message);
7921            Command::none()
7922        }
7923
7924        fn view(&self, _theme: &Theme) -> Box<dyn Widget<Board>> {
7925            let mut list = ReorderableList::new(Board::Row)
7926                .grab(ReorderGrab::LongPress)
7927                .width(W);
7928            for row in 0..20u64 {
7929                list = list.keyed_row(
7930                    row,
7931                    Container::new()
7932                        .height(60.0)
7933                        .child(text(format!("row {row}")).size(14.0)),
7934                );
7935            }
7936            Box::new(
7937                SingleChildScrollView::new()
7938                    .axis(Axis::Vertical)
7939                    .width(W)
7940                    .height(H)
7941                    .child(list),
7942            )
7943        }
7944    }
7945
7946    fn driver<A: Application>(app: A) -> Driver<A> {
7947        let mut driver = Driver::new(app, W, H);
7948        driver.run(0.2);
7949        driver
7950    }
7951
7952    /// A finger held still at `at` until the long press has lifted what is under it.
7953    fn hold<A: Application>(driver: &mut Driver<A>, at: Point) {
7954        driver.press(at);
7955        driver.run(0.5);
7956        assert!(
7957            driver.hold_deadline(),
7958            "the hold lifts what is under {at:?}"
7959        );
7960        driver.run(0.05);
7961    }
7962
7963    /// The finger through `points`, a tenth of a second apart, as the phone's were.
7964    fn carry<A: Application>(driver: &mut Driver<A>, points: impl IntoIterator<Item = Point>) {
7965        for at in points {
7966            driver.move_to(at);
7967            driver.run(0.1);
7968        }
7969    }
7970
7971    /// Frame by frame for `seconds` with the finger still at `finger`: the carried box keeps
7972    /// the place it had under the finger — `offset` from it and `size` — and the region
7973    /// `region` keeps scrolling the way it started. Hands back that region's last offset.
7974    fn held_under_the_finger<A: Application>(
7975        driver: &mut Driver<A>,
7976        seconds: f32,
7977        finger: Point,
7978        offset: (f32, f32),
7979        size: (f32, f32),
7980        region: usize,
7981    ) -> (f32, f32) {
7982        let mut last = driver.offsets()[region].1;
7983        for frame in 0..(seconds * 60.0) as usize {
7984            driver.run(1.0 / 60.0);
7985            let now = driver.offsets()[region].1;
7986            let carried: Rect = driver.carried().unwrap_or_else(|| {
7987                panic!("frame {frame}: nothing is carried any more, the list at {now:?}")
7988            });
7989            assert!(
7990                (carried.x - finger.x - offset.0).abs() < 0.5
7991                    && (carried.y - finger.y - offset.1).abs() < 0.5
7992                    && (carried.width - size.0).abs() < 0.5
7993                    && (carried.height - size.1).abs() < 0.5,
7994                "frame {frame}: carried at {carried:?}, the finger at {finger:?}, the list at {now:?}"
7995            );
7996            assert!(
7997                now.0 >= last.0 - 0.01 && now.1 >= last.1 - 0.01,
7998                "frame {frame}: the list went back, {last:?} then {now:?}"
7999            );
8000            // And it is drawn there: the ghost the finger sees is the box being carried.
8001            let ghost = driver
8002                .ghost()
8003                .unwrap_or_else(|| panic!("frame {frame}: no ghost is drawn"));
8004            assert!(
8005                (ghost.x - carried.x).abs() < 1.0
8006                    && (ghost.y - carried.y).abs() < 1.0
8007                    && (ghost.width - carried.width).abs() < 2.0,
8008                "frame {frame}: the ghost is drawn at {ghost:?}, what is carried is at {carried:?}"
8009            );
8010            last = now;
8011        }
8012        last
8013    }
8014
8015    /// **The reproduction.** A label held and carried past the strip's right edge stays under
8016    /// the finger for as long as it is held there, whole, while the strip scrolls on to its
8017    /// end; and the release drops it where the finger is, past the last label.
8018    #[test]
8019    fn a_label_carried_past_the_strips_edge_stays_under_the_finger() {
8020        let mut driver = driver(BoardApp::default());
8021        hold(&mut driver, Point::new(155.0, STRIP_Y));
8022        let lifted = driver.carried().expect("the label is carried");
8023        assert_eq!(
8024            lifted,
8025            Rect::new(128.0, 68.0, 96.0, 41.0),
8026            "Feature, lifted"
8027        );
8028        let finger = Point::new(370.0, STRIP_Y);
8029        carry(
8030            &mut driver,
8031            [200.0, 260.0, 320.0, finger.x].map(|x| Point::new(x, STRIP_Y)),
8032        );
8033        let strip = held_under_the_finger(
8034            &mut driver,
8035            1.5,
8036            finger,
8037            (lifted.x - 155.0, lifted.y - STRIP_Y),
8038            (96.0, 41.0),
8039            0,
8040        );
8041        assert!(
8042            (strip.0 - (STRIP_CONTENT - W)).abs() < 1.0,
8043            "the strip scrolled on to its end: {strip:?}"
8044        );
8045        driver.release(finger);
8046        driver.run(0.2);
8047        assert_eq!(
8048            driver.app().log,
8049            [Board::Label(1, 9)],
8050            "past the last label, it goes last; no card moves"
8051        );
8052    }
8053
8054    /// **The phone's own gesture**, where the label never reaches an edge: held on Feature,
8055    /// carried along x as the injected moves went, let go over Design's right half.
8056    #[test]
8057    fn a_hold_on_a_label_then_a_carry_along_x_reorders_the_labels_and_moves_no_card() {
8058        let mut driver = driver(BoardApp::default());
8059        hold(&mut driver, Point::new(155.0, STRIP_Y));
8060        carry(
8061            &mut driver,
8062            [167.0, 189.0, 218.0, 247.0, 276.0, 306.0].map(|x| Point::new(x, STRIP_Y)),
8063        );
8064        driver.run(0.4);
8065        assert!(driver.carried().is_some(), "still carried");
8066        driver.release(Point::new(306.0, STRIP_Y));
8067        driver.run(0.2);
8068        assert_eq!(driver.app().log, [Board::Label(1, 2)]);
8069        assert_eq!(driver.app().labels[..4], [0, 2, 1, 3]);
8070        assert_eq!(
8071            driver.app().cards,
8072            BoardApp::default().cards,
8073            "no card moved"
8074        );
8075        assert_eq!(driver.offsets()[0].1, (0.0, 0.0), "nothing scrolled");
8076        assert_eq!(driver.offsets()[1].1, (0.0, 0.0), "the board neither");
8077    }
8078
8079    /// **A card still crosses the board**: Design API, pressed and carried onto the upper
8080    /// half of Build widget, goes to the top of Doing; no label moves.
8081    #[test]
8082    fn a_card_carried_across_the_board_moves_the_card() {
8083        let mut driver = driver(BoardApp::default());
8084        driver.press(Point::new(100.0, 190.0));
8085        carry(
8086            &mut driver,
8087            [130.0, 180.0, 240.0, 300.0].map(|x| Point::new(x, 185.0)),
8088        );
8089        driver.release(Point::new(300.0, 185.0));
8090        driver.run(0.2);
8091        assert_eq!(driver.app().log, [Board::Card(0, 0, 1, 0)]);
8092        assert_eq!(driver.app().labels, (0..10).collect::<Vec<_>>());
8093    }
8094
8095    /// **The board has the same edge.** A card carried to the board's right edge scrolls
8096    /// the board and stays under the finger, although the column it came from scrolls away.
8097    #[test]
8098    fn a_card_carried_to_the_boards_edge_stays_under_the_finger() {
8099        let mut driver = driver(BoardApp::default());
8100        let press = Point::new(100.0, 190.0);
8101        driver.press(press);
8102        let finger = Point::new(385.0, 190.0);
8103        carry(
8104            &mut driver,
8105            [130.0, 200.0, 280.0, 340.0, finger.x].map(|x| Point::new(x, 190.0)),
8106        );
8107        let card = driver.carried().expect("the card is carried");
8108        let board = held_under_the_finger(
8109            &mut driver,
8110            1.0,
8111            finger,
8112            (card.x - finger.x, card.y - finger.y),
8113            (card.width, card.height),
8114            1,
8115        );
8116        assert!(
8117            board.0 > 250.0,
8118            "the board scrolled past the column the card came from: {board:?}"
8119        );
8120    }
8121
8122    /// **A list that runs down has the same edge**: row 0 held and carried to the bottom
8123    /// stays under the finger after its own place has scrolled off the top, and lands low in
8124    /// the list.
8125    #[test]
8126    fn a_row_carried_past_the_bottom_of_a_long_list_stays_under_the_finger() {
8127        let mut driver = driver(ListApp::default());
8128        hold(&mut driver, Point::new(100.0, 30.0));
8129        let lifted = driver.carried().expect("the row is carried");
8130        let finger = Point::new(100.0, 840.0);
8131        carry(
8132            &mut driver,
8133            [200.0, 400.0, 600.0, 800.0, finger.y].map(|y| Point::new(100.0, y)),
8134        );
8135        let list = held_under_the_finger(
8136            &mut driver,
8137            1.5,
8138            finger,
8139            (lifted.x - 100.0, lifted.y - 30.0),
8140            (lifted.width, lifted.height),
8141            0,
8142        );
8143        assert!(
8144            list.1 > 200.0,
8145            "the list scrolled past the row's own place: {list:?}"
8146        );
8147        driver.release(finger);
8148        driver.run(0.2);
8149        assert!(
8150            matches!(driver.app().log[..], [Board::Row(0, to)] if to > 15),
8151            "row 0 lands near the end: {:?}",
8152            driver.app().log
8153        );
8154    }
8155}
8156
8157/// **A press a scroll took is let go of** (milestone 534).
8158///
8159/// The shell's own types need a window and an event loop, so these drive a real frame
8160/// through the pieces `App::pointer` is made of, in the order it calls them: the hover, the
8161/// press where the pointer lands, the gesture the drag handler turns the press into, and
8162/// `settle_press` after every event. What is painted is then compared with a runtime
8163/// nobody pressed.
8164#[cfg(test)]
8165mod press_tests {
8166    use super::{gesture_was_a_tap, press_at, settle_press, Drag, Point, Runtime, Theme};
8167    use crate::gesture::PointerKind;
8168    use crate::hover::Hover;
8169    use frus_widgets::{
8170        build_ui, Axis, Button, Container, Flex, Primitive, SegmentedButton, SingleChildScrollView,
8171        Size, Ui, Widget,
8172    };
8173
8174    const SCREEN: Size = Size::new(300.0, 400.0);
8175    const DT: f32 = 1.0 / 60.0;
8176
8177    /// The phone's screen: a strip of segments too wide for it, in a horizontal scroll.
8178    fn filters() -> Box<dyn Widget<usize>> {
8179        let mut segments = SegmentedButton::new(0, |i| i);
8180        for label in ["All", "Active", "Done", "Archived", "Starred", "Shared"] {
8181            segments = segments.segment(label);
8182        }
8183        Box::new(
8184            Flex::column().width(300.0).height(400.0).child(
8185                SingleChildScrollView::new()
8186                    .axis(Axis::Horizontal)
8187                    .width(300.0)
8188                    .height(80.0)
8189                    .child(segments),
8190            ),
8191        )
8192    }
8193
8194    /// A plain button at the top of a page that scrolls down.
8195    fn page() -> Box<dyn Widget<usize>> {
8196        Box::new(
8197            Flex::column().width(300.0).height(400.0).child(
8198                SingleChildScrollView::new()
8199                    .width(300.0)
8200                    .height(400.0)
8201                    .child(
8202                        Flex::column()
8203                            .child(Button::new("Go").on_press(7usize))
8204                            .child(Container::new().width(300.0).height(1200.0)),
8205                    ),
8206            ),
8207        )
8208    }
8209
8210    fn frame(view: &dyn Widget<usize>, runtime: &Runtime) -> Ui<usize> {
8211        build_ui(view, SCREEN, runtime, &Theme::default())
8212    }
8213
8214    /// Frames go by with nothing else happening, as a phone left alone for `seconds`.
8215    fn idle(runtime: &mut Runtime, seconds: f32) {
8216        for _ in 0..(seconds / DT) as usize {
8217            runtime.advance(DT);
8218            runtime.advance_ink(DT);
8219        }
8220    }
8221
8222    /// What the screen shows.
8223    fn painted(view: &dyn Widget<usize>, runtime: &Runtime) -> Vec<Primitive> {
8224        frame(view, runtime).scene().primitives().to_vec()
8225    }
8226
8227    /// The same screen, scrolled the same, that no pointer has touched.
8228    fn untouched(view: &dyn Widget<usize>, runtime: &Runtime) -> Vec<Primitive> {
8229        let mut quiet = Runtime::default();
8230        quiet.scroll.clone_from(&runtime.scroll);
8231        quiet.scroll_target.clone_from(&runtime.scroll_target);
8232        idle(&mut quiet, 0.5);
8233        painted(view, &quiet)
8234    }
8235
8236    /// A pointer on a screen, routed as the shell routes one.
8237    struct Pointer<'a> {
8238        view: &'a dyn Widget<usize>,
8239        runtime: Runtime,
8240        hover: Hover,
8241        drag: Option<Drag>,
8242        touch: bool,
8243        at: Point,
8244    }
8245
8246    impl<'a> Pointer<'a> {
8247        fn new(view: &'a dyn Widget<usize>, touch: bool) -> Self {
8248            Self {
8249                view,
8250                runtime: Runtime::default(),
8251                hover: Hover::default(),
8252                drag: None,
8253                touch,
8254                at: Point::new(0.0, 0.0),
8255            }
8256        }
8257
8258        /// One event: `route` stands for what the shell's handler makes of it.
8259        fn event(&mut self, kind: PointerKind, at: Point, route: impl FnOnce(&mut Self)) {
8260            self.at = at;
8261            self.hover.event(kind, at, self.touch);
8262            let could_tap = gesture_was_a_tap(self.drag.as_ref());
8263            if kind == PointerKind::Down {
8264                let ui = frame(self.view, &self.runtime);
8265                self.runtime.input.hovered = self.hover.target(&ui);
8266                press_at(&mut self.runtime, Some(&ui), at);
8267            }
8268            route(self);
8269            settle_press(
8270                &mut self.runtime.input,
8271                kind,
8272                could_tap,
8273                self.drag.as_ref(),
8274                self.touch,
8275            );
8276            if matches!(kind, PointerKind::Up | PointerKind::Cancel) {
8277                let ui = frame(self.view, &self.runtime);
8278                self.runtime.input.hovered = self.hover.target(&ui);
8279            }
8280        }
8281
8282        /// A finger lands where a touch scroll takes it, as `pointer_down` prepares one.
8283        fn land(&mut self, at: Point) {
8284            self.event(PointerKind::Down, at, |p| {
8285                if !p.touch {
8286                    return;
8287                }
8288                let ui = frame(p.view, &p.runtime);
8289                let area = ui
8290                    .scroll_chain(at)
8291                    .find(|area| area.accepts_user_offset((0.0, 0.0)))
8292                    .expect("the region under the finger has somewhere to go");
8293                p.drag = Some(Drag::Scroll {
8294                    id: area.id,
8295                    last: at,
8296                    moved: false,
8297                    carried: (0.0, 0.0),
8298                    dismiss: None,
8299                    axis: None,
8300                });
8301            });
8302        }
8303
8304        /// The pointer moves by `by`; past the slop a touch scroll moves its content, as the
8305        /// drag handler moves it.
8306        fn slide(&mut self, by: (f32, f32)) {
8307            let to = Point::new(self.at.x + by.0, self.at.y + by.1);
8308            self.event(PointerKind::Move, to, |p| {
8309                let Some(Drag::Scroll {
8310                    id,
8311                    moved,
8312                    axis,
8313                    last,
8314                    ..
8315                }) = p.drag.as_mut()
8316                else {
8317                    return;
8318                };
8319                let (dx, dy) = (to.x - last.x, to.y - last.y);
8320                if dx.hypot(dy) <= super::TOUCH_SLOP && !*moved {
8321                    return;
8322                }
8323                *moved = true;
8324                *axis = Some(dy.abs() >= dx.abs());
8325                *last = to;
8326                let offset = p.runtime.scroll.get(id).copied().unwrap_or((0.0, 0.0));
8327                let moved_to = (offset.0 - by.0, offset.1 - by.1);
8328                p.runtime.scroll.insert(*id, moved_to);
8329                p.runtime.scroll_target.insert(*id, moved_to);
8330            });
8331        }
8332
8333        /// The pointer lifts; the release takes the gesture, as `pointer_up` does.
8334        fn lift(&mut self) -> Option<Drag> {
8335            let mut ended = None;
8336            self.event(PointerKind::Up, self.at, |p| ended = p.drag.take());
8337            ended
8338        }
8339    }
8340
8341    /// **Seen on a phone**: a swipe that started on "Done" scrolled the strip, selected
8342    /// nothing — and left "Done" grey, still there two seconds later. Afterwards the screen
8343    /// is the one nobody touched.
8344    #[test]
8345    fn a_segment_a_swipe_started_on_is_not_left_pressed() {
8346        let view = filters();
8347        let mut finger = Pointer::new(view.as_ref(), true);
8348        let done = Point::new(200.0, 16.0);
8349        finger.land(done);
8350        let segment = finger
8351            .runtime
8352            .input
8353            .pressed
8354            .expect("the segment is pressed");
8355        assert!(
8356            finger.runtime.ink.contains_key(&segment),
8357            "and it splashed: a segment takes ink"
8358        );
8359        assert!(
8360            frame(view.as_ref(), &finger.runtime)
8361                .scroll_chain(done)
8362                .any(|area| area.max_x > 0.0),
8363            "the strip is wider than the screen, as the phone's was"
8364        );
8365        for _ in 0..19 {
8366            finger.slide((-10.0, 0.0));
8367        }
8368        let ended = finger.lift();
8369        assert!(
8370            !gesture_was_a_tap(ended.as_ref()),
8371            "a gesture that scrolled clicks nothing"
8372        );
8373        idle(&mut finger.runtime, 2.0);
8374        // What the phone showed first, then why.
8375        let (shown, quiet) = (
8376            painted(view.as_ref(), &finger.runtime),
8377            untouched(view.as_ref(), &finger.runtime),
8378        );
8379        assert_eq!(
8380            shown.len(),
8381            quiet.len(),
8382            "two seconds on, the screen paints what nobody touched, and no layer more"
8383        );
8384        assert_eq!(shown, quiet, "nothing is highlighted");
8385        assert!(
8386            finger.runtime.ink.is_empty(),
8387            "no ink waits for a finger that has gone"
8388        );
8389        assert_eq!(finger.runtime.input.pressed, None, "the press went with it");
8390        assert_eq!(
8391            finger.runtime.input.hovered, None,
8392            "and a lifted finger hovers nothing"
8393        );
8394    }
8395
8396    /// The reference cancels the press **when the scroll wins**, not when the finger lifts:
8397    /// a strip being scrolled does not carry a pressed segment along with it.
8398    #[test]
8399    fn the_press_ends_when_the_scroll_takes_it() {
8400        let view = filters();
8401        let mut finger = Pointer::new(view.as_ref(), true);
8402        finger.land(Point::new(200.0, 16.0));
8403        finger.slide((-4.0, 0.0));
8404        assert!(
8405            finger.runtime.input.pressed.is_some(),
8406            "under the slop it may still be a tap"
8407        );
8408        finger.slide((-40.0, 0.0));
8409        assert_eq!(
8410            finger.runtime.input.pressed, None,
8411            "past it, the scroll has it"
8412        );
8413        assert_eq!(
8414            finger.runtime.input.hovered, None,
8415            "and the finger's hover with it"
8416        );
8417        idle(&mut finger.runtime, 0.5);
8418        assert_eq!(
8419            painted(view.as_ref(), &finger.runtime),
8420            untouched(view.as_ref(), &finger.runtime),
8421            "the finger is still down, and nothing under it is highlighted"
8422        );
8423    }
8424
8425    /// The same down a page: a plain button a scroll started on.
8426    #[test]
8427    fn a_button_a_page_scrolled_from_is_not_left_pressed() {
8428        let view = page();
8429        let mut finger = Pointer::new(view.as_ref(), true);
8430        let go = Point::new(20.0, 15.0);
8431        finger.land(go);
8432        let button = finger.runtime.input.pressed.expect("the button is pressed");
8433        assert_eq!(
8434            frame(view.as_ref(), &finger.runtime).msg_for(button),
8435            Some(7),
8436            "the press is on the button"
8437        );
8438        for _ in 0..10 {
8439            finger.slide((0.0, -30.0));
8440        }
8441        assert!(!gesture_was_a_tap(finger.lift().as_ref()));
8442        idle(&mut finger.runtime, 2.0);
8443        assert_eq!(finger.runtime.input.pressed, None);
8444        assert!(finger.runtime.ink.is_empty());
8445        assert_eq!(
8446            painted(view.as_ref(), &finger.runtime),
8447            untouched(view.as_ref(), &finger.runtime),
8448        );
8449    }
8450
8451    /// **A tap is still a tap**: a finger that stays inside the slop keeps its press to the
8452    /// release, which is what clicks; only then is it let go of.
8453    #[test]
8454    fn a_tap_still_presses_and_clicks() {
8455        let view = filters();
8456        let mut finger = Pointer::new(view.as_ref(), true);
8457        let segment = Point::new(200.0, 16.0);
8458        finger.land(segment);
8459        finger.slide((3.0, 2.0));
8460        let pressed = finger.runtime.input.pressed;
8461        assert!(pressed.is_some(), "a wobble under the slop takes nothing");
8462        assert_eq!(finger.runtime.input.hovered, pressed, "and the press shows");
8463        // What `pointer_up` reads, before the release lets it go.
8464        let ui = frame(view.as_ref(), &finger.runtime);
8465        assert_eq!(
8466            ui.hit(segment),
8467            pressed,
8468            "released on the widget it pressed"
8469        );
8470        assert!(
8471            pressed.and_then(|id| ui.msg_for(id)).is_some(),
8472            "and the segment it pressed has a choice to make"
8473        );
8474        let ended = finger.lift();
8475        assert!(
8476            gesture_was_a_tap(ended.as_ref()),
8477            "a scroll that never moved"
8478        );
8479        assert_eq!(
8480            finger.runtime.input.pressed, None,
8481            "let go of once released"
8482        );
8483    }
8484
8485    /// **A mouse is as it was**: it has no touch scroll, keeps its press while it moves, and
8486    /// still hovers where it is after its button comes up.
8487    #[test]
8488    fn a_mouse_keeps_its_press_while_it_moves_and_its_hover_after() {
8489        let view = filters();
8490        let mut mouse = Pointer::new(view.as_ref(), false);
8491        let done = Point::new(200.0, 16.0);
8492        mouse.land(done);
8493        assert!(mouse.drag.is_none(), "a mouse does not drag a scroll");
8494        let pressed = mouse.runtime.input.pressed.expect("pressed");
8495        mouse.slide((-60.0, 0.0));
8496        assert_eq!(mouse.runtime.input.pressed, Some(pressed), "still held");
8497        mouse.lift();
8498        assert_eq!(mouse.runtime.input.pressed, None);
8499        assert!(
8500            mouse.runtime.input.hovered.is_some(),
8501            "a released mouse is still over the strip"
8502        );
8503    }
8504
8505    /// A gesture a **mouse** moves past its slop takes the press as a finger's does — but
8506    /// not the hover, since the mouse is still there.
8507    #[test]
8508    fn a_gesture_takes_a_mouse_s_press_and_leaves_its_hover() {
8509        let id = frus_widgets::WidgetId::from_u64(3);
8510        let sheet = |moved| Drag::Sheet {
8511            id: frus_widgets::WidgetId::from_u64(9),
8512            last: Point::new(0.0, 0.0),
8513            moved,
8514            available: 800.0,
8515        };
8516        for touch in [false, true] {
8517            let mut input = frus_widgets::InputState {
8518                pressed: Some(id),
8519                hovered: Some(id),
8520                ..Default::default()
8521            };
8522            assert!(!settle_press(
8523                &mut input,
8524                PointerKind::Move,
8525                true,
8526                Some(&sheet(false)),
8527                touch
8528            ));
8529            assert_eq!(input.pressed, Some(id), "not yet past the slop");
8530            assert!(settle_press(
8531                &mut input,
8532                PointerKind::Move,
8533                true,
8534                Some(&sheet(true)),
8535                touch
8536            ));
8537            assert_eq!(input.pressed, None, "touch {touch}");
8538            assert_eq!(input.hovered, (!touch).then_some(id), "touch {touch}");
8539        }
8540        // A gesture that was never a tap — a slider, a selection — had no press to share,
8541        // and moving it lets go of nothing.
8542        let mut input = frus_widgets::InputState {
8543            pressed: Some(id),
8544            ..Default::default()
8545        };
8546        let select = Drag::TextSelect {
8547            id,
8548            rect: super::Rect::new(0.0, 0.0, 10.0, 10.0),
8549        };
8550        assert!(!settle_press(
8551            &mut input,
8552            PointerKind::Move,
8553            false,
8554            Some(&select),
8555            true
8556        ));
8557        assert_eq!(input.pressed, Some(id));
8558    }
8559}