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, focus_changes,
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, RegionPoint, RegionSelection, ReorderAxis,
21    Runtime, Scene, ScrollTo, Scrollable, SheetTo, ShortcutKey, Size, TextStop, Theme, ToolbarMark,
22    Ui, VelocityEstimate, VelocityTracker, Widget, 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 a key press asks for the **selection bar**: the context-menu key, which a
107/// keyboard may have between Alt Gr and Ctrl, or Shift+F10, which every desktop toolkit reads
108/// the same way (milestone 568). What lets someone with no pointer — and so no hold or
109/// right-click — open it.
110fn opens_selection_bar(logical: &WinitKey, shift: bool) -> bool {
111    match logical {
112        WinitKey::Named(NamedKey::ContextMenu) => true,
113        WinitKey::Named(NamedKey::F10) => shift,
114        _ => false,
115    }
116}
117
118/// Whether `widget` takes typing, and so wants the software keyboard while it has focus.
119///
120/// Asked of the widget (milestone 510). It used to be asked of a **caret hit test** at
121/// the corner of a field one pixel wide, and a field with a clickable suffix — the ×
122/// a field shows once it holds text — answered that no caret goes there, since that
123/// pixel is its clear button. So the first letter typed into such a field closed the
124/// keyboard and ended its composition, and the keyboard's next update was written
125/// beside the first: `F`, then `FFr`.
126///
127/// A field that can only be **read** — a read-only one, a selectable paragraph — is a text field
128/// with a caret and a selection and no use for a keyboard, which would slide up over the very
129/// words its reader has come to select.
130#[cfg(any(android, test))]
131fn wants_keyboard<M>(widget: &dyn Widget<M>) -> bool {
132    is_text_field(widget) && widget.focusable() && widget.takes_typing()
133}
134
135/// Whether `widget` is a **text field**: what takes the left and right arrows for its caret
136/// instead of leaving them to focus navigation.
137///
138/// Asked of the widget, and not of a caret hit test at the corner of a field one pixel wide —
139/// which is how this was decided until milestone 570. A field that holds text shows a clear
140/// button, and the button covers that pixel, so the probe answered "no caret goes here" and the
141/// **left and right arrows moved the focus to the next control** instead of the caret: in any
142/// field with something in it, on every platform, and never in an empty one. It is the same
143/// trap milestone 510 had already got the keyboard out of, in the one place it was not looked for.
144fn is_text_field<M>(widget: &dyn Widget<M>) -> bool {
145    widget.text_value().is_some()
146}
147
148/// How far apart two presses may be and still be a double click on words in a selection area, in
149/// logical pixels.
150const REGION_DOUBLE_CLICK_SLOP: f32 = 8.0;
151
152/// How long a first tap waits for a second one to make a double tap: the reference's
153/// `kDoubleTapTimeout`.
154const DOUBLE_TAP_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(300);
155
156/// How far apart two taps may be and still be a double tap, in logical pixels: the reference's
157/// `kDoubleTapSlop`.
158const DOUBLE_TAP_SLOP: f32 = 100.0;
159
160/// The fastest a drag's end velocity is reported, in logical pixels per second: the
161/// reference's `kMaxFlingVelocity`.
162const MAX_PAN_VELOCITY: f32 = 8000.0;
163
164/// A first tap on something that also takes a double tap, waiting to see whether a second
165/// one follows (milestone 581).
166struct PendingTap<M> {
167    /// What was tapped.
168    id: WidgetId,
169    /// When it was tapped.
170    at: Instant,
171    /// Where it was tapped.
172    position: Point,
173    /// The tap's own message, sent if no second tap comes in time.
174    single: Option<M>,
175}
176
177/// Whether a field's **Select all**, pressed on its bar, puts the bar away: on macOS, Linux
178/// and Windows it does, its work done; on Android, iOS and Fuchsia the bar stays, offering
179/// what can be done with everything selected (`editable_text.dart:2914`, milestone 625).
180///
181/// The **system's** platform decides, not the theme's, as in the reference: it is how the
182/// system's own menus behave, not how the application looks.
183fn select_all_hides_bar(platform: frus_widgets::TargetPlatform) -> bool {
184    platform.is_desktop()
185}
186
187/// The clipboard: `arboard` on the desktop platforms, the platform's own on Android
188/// (`ClipboardManager`, through the bundled dex — milestone 509, #22), the browser's
189/// asynchronous Clipboard API on the Web (milestone 526, #17), and a no-op on iOS
190/// (`arboard` does not compile there and is not a dependency).
191/// The stub is gated on what is *not* implemented rather than on a list of the other
192/// platforms: that is what makes adding a target never leave this type undefined.
193/// One uniform API, so the driver's body stays free of `cfg`.
194///
195/// **A paste is asked for, and answered.** The Web's read is a promise — it may prompt
196/// the reader, be refused, or take its time — and the other platforms' reads are
197/// immediate. So `Clipboard::paste` names the field that asked and either answers at
198/// once or answers later through `Clipboard::take_answered`, which the driver drains
199/// every frame; both answers land through the same rule, `Pasted::lands`. Nothing on a
200/// synchronous platform pretends to wait, and no widget knows which kind it is on: the
201/// shell, not the field, holds the clipboard.
202mod clip {
203    use frus_widgets::WidgetId;
204
205    /// What the clipboard answered to a paste: its text, and the field that asked.
206    #[derive(Clone, Debug, PartialEq, Eq)]
207    pub struct Pasted {
208        pub into: WidgetId,
209        pub text: String,
210    }
211
212    impl Pasted {
213        /// Whether this paste goes into its field: only while that field still has the
214        /// focus — on the Web the answer comes back after the key press, and the reader
215        /// may have moved on to another field, or to none — and only when there is text.
216        /// An empty clipboard is nothing to paste, not an instruction to delete the
217        /// selection; the browser answers a clipboard holding no text with `""`.
218        pub fn lands(&self, focused: Option<WidgetId>) -> bool {
219            focused == Some(self.into) && !self.text.is_empty()
220        }
221    }
222
223    /// The platforms whose read is immediate answer a paste on the spot.
224    #[cfg(not(web))]
225    impl Clipboard {
226        pub fn paste(
227            &mut self,
228            into: WidgetId,
229            _wake: Option<&std::sync::Arc<winit::window::Window>>,
230        ) -> Option<Pasted> {
231            self.get_text().map(|text| Pasted { into, text })
232        }
233        pub fn take_answered(&mut self) -> Option<Pasted> {
234            None
235        }
236    }
237
238    #[cfg(desktop)]
239    pub enum Clipboard {
240        /// The system's, through `arboard`.
241        System(Option<arboard::Clipboard>),
242        /// One of its own, in memory: what a shell with no window holds (the test
243        /// driver's). The system's is shared by everything on the machine and, on macOS,
244        /// is not to be touched from several threads at once — and tests run on several.
245        /// Only there, so a build without it has no variant nothing makes.
246        #[cfg(any(test, feature = "testing"))]
247        Memory(Option<String>),
248    }
249
250    #[cfg(desktop)]
251    impl Clipboard {
252        pub fn new() -> Self {
253            Self::System(arboard::Clipboard::new().ok())
254        }
255        /// Only a shell with no window — the test driver — holds one.
256        #[cfg(any(test, feature = "testing"))]
257        pub fn in_memory() -> Self {
258            Self::Memory(None)
259        }
260        pub fn get_text(&mut self) -> Option<String> {
261            match self {
262                Self::System(system) => system.as_mut().and_then(|c| c.get_text().ok()),
263                #[cfg(any(test, feature = "testing"))]
264                Self::Memory(text) => text.clone(),
265            }
266        }
267        pub fn has_text(&mut self) -> bool {
268            self.get_text().is_some_and(|text| !text.is_empty())
269        }
270        pub fn set_text(&mut self, text: String) {
271            match self {
272                Self::System(system) => {
273                    if let Some(c) = system.as_mut() {
274                        let _ = c.set_text(text);
275                    }
276                }
277                #[cfg(any(test, feature = "testing"))]
278                Self::Memory(held) => *held = Some(text),
279            }
280        }
281    }
282
283    /// Elsewhere a shell with no window keeps the platform's clipboard: no test runs there.
284    #[cfg(all(not(desktop), any(test, feature = "testing")))]
285    impl Clipboard {
286        pub fn in_memory() -> Self {
287            Self::new()
288        }
289    }
290
291    #[cfg(android)]
292    pub struct Clipboard;
293
294    #[cfg(android)]
295    impl Clipboard {
296        pub fn new() -> Self {
297            Self
298        }
299        pub fn get_text(&mut self) -> Option<String> {
300            crate::android_clipboard::get_text()
301        }
302        pub fn has_text(&mut self) -> bool {
303            self.get_text().is_some_and(|text| !text.is_empty())
304        }
305        pub fn set_text(&mut self, text: String) {
306            crate::android_clipboard::set_text(&text);
307        }
308    }
309
310    /// The pastes a clipboard has answered later and the driver has not taken yet, in the
311    /// order the answers came — which need not be the order they were asked in.
312    ///
313    /// Held by the Web's clipboard; kept out of its `cfg` so that it is tested natively.
314    #[cfg(any(web, test))]
315    #[derive(Default)]
316    pub struct Answers(std::rc::Rc<std::cell::RefCell<std::collections::VecDeque<Pasted>>>);
317
318    #[cfg(any(web, test))]
319    impl Answers {
320        /// What answers a paste into `into` once the text is there: it queues the paste,
321        /// then calls `wake`, so that a frame comes to take it.
322        pub fn answer_for(
323            &self,
324            into: WidgetId,
325            wake: impl FnOnce() + 'static,
326        ) -> impl FnOnce(String) + 'static {
327            let queue = std::rc::Rc::clone(&self.0);
328            move |text| {
329                queue.borrow_mut().push_back(Pasted { into, text });
330                wake();
331            }
332        }
333
334        /// The oldest answer not taken yet.
335        pub fn take(&self) -> Option<Pasted> {
336            self.0.borrow_mut().pop_front()
337        }
338    }
339
340    /// The Web: a read is started by the key press and answered by the browser later,
341    /// into a queue the driver drains; the window is asked for a frame so that it does.
342    #[cfg(web)]
343    pub struct Clipboard {
344        answered: Answers,
345    }
346
347    #[cfg(web)]
348    impl Clipboard {
349        pub fn new() -> Self {
350            Self {
351                answered: Answers::default(),
352            }
353        }
354        pub fn set_text(&mut self, text: String) {
355            crate::web_clipboard::set_text(&text);
356        }
357        /// The browser's clipboard cannot be read without asking, and asking is a promise
358        /// that may prompt the reader: so a bar opening cannot know, and offers Paste.
359        pub fn has_text(&mut self) -> bool {
360            true
361        }
362        pub fn paste(
363            &mut self,
364            into: WidgetId,
365            wake: Option<&std::sync::Arc<winit::window::Window>>,
366        ) -> Option<Pasted> {
367            let wake = wake.cloned();
368            crate::web_clipboard::get_text(self.answered.answer_for(into, move || {
369                if let Some(window) = wake {
370                    window.request_redraw();
371                }
372            }));
373            None
374        }
375        pub fn take_answered(&mut self) -> Option<Pasted> {
376            self.answered.take()
377        }
378    }
379
380    #[cfg(not(any(desktop, android, web)))]
381    pub struct Clipboard;
382
383    #[cfg(not(any(desktop, android, web)))]
384    impl Clipboard {
385        pub fn new() -> Self {
386            Self
387        }
388        pub fn get_text(&mut self) -> Option<String> {
389            None
390        }
391        pub fn has_text(&mut self) -> bool {
392            false
393        }
394        pub fn set_text(&mut self, _text: String) {}
395    }
396}
397
398/// Browser timers, for the Web: the counterpart of the native subscriptions'
399/// `recv_timeout` thread. A **retained** `setInterval`, whose **drop** calls
400/// `clearInterval` and releases the closure. That is how an `every` subscription
401/// ticks on the Web, where there is no background thread.
402#[cfg(web)]
403mod web_timer {
404    use wasm_bindgen::prelude::Closure;
405    use wasm_bindgen::JsCast;
406
407    /// An active `setInterval`: as long as this handle lives the callback is called
408    /// back every `ms`; dropping it cancels that.
409    pub(crate) struct Interval {
410        id: i32,
411        // The JS closure must live as long as the interval does.
412        _closure: Closure<dyn FnMut()>,
413    }
414
415    impl Interval {
416        /// Schedules `f` every `ms` milliseconds, 1 ms at the least. `None` when there
417        /// is no window, that is, in a DOM-less context.
418        pub(crate) fn new(ms: i32, f: impl FnMut() + 'static) -> Option<Self> {
419            let window = web_sys::window()?;
420            let closure = Closure::wrap(Box::new(f) as Box<dyn FnMut()>);
421            let id = window
422                .set_interval_with_callback_and_timeout_and_arguments_0(
423                    closure.as_ref().unchecked_ref(),
424                    ms.max(1),
425                )
426                .ok()?;
427            Some(Self {
428                id,
429                _closure: closure,
430            })
431        }
432    }
433
434    impl Drop for Interval {
435        fn drop(&mut self) {
436            if let Some(window) = web_sys::window() {
437                window.clear_interval_with_handle(self.id);
438            }
439        }
440    }
441
442    /// Calls `f` once, after `ms` milliseconds — the Web half of
443    /// [`Command::after`](crate::Command::after).
444    ///
445    /// There is no handle to keep, and that is the point. `Closure::once_into_js`
446    /// hands ownership of the closure to the JS side, which frees it after the call.
447    /// A `Closure` we owned instead would have to be either leaked — one leak per
448    /// timer — or dropped here, which would free the callback out from under a
449    /// `setTimeout` that has not fired yet.
450    pub(crate) fn after(ms: i32, f: impl FnOnce() + 'static) {
451        let Some(window) = web_sys::window() else {
452            return;
453        };
454        let callback = wasm_bindgen::closure::Closure::once_into_js(f);
455        let _ = window.set_timeout_with_callback_and_timeout_and_arguments_0(
456            callback.unchecked_ref(),
457            ms.max(0),
458        );
459    }
460}
461
462/// An active subscription's cancellation handle: **dropping** it stops the
463/// subscription.
464/// - Native: the executor task's handle — dropping it cancels the task.
465/// - Web: a retained `setInterval` (`None` when installing it failed).
466#[cfg(not(web))]
467type SubHandle = async_executor::Task<()>;
468#[cfg(web)]
469type SubHandle = Option<web_timer::Interval>;
470
471/// Scroll speed, in pixels per wheel notch.
472const SCROLL_SPEED: f32 = 40.0;
473
474/// The distance a **finger** must travel before the framework is confident the
475/// gesture is a drag rather than a tap, in logical px.
476///
477/// 18 px is large for a screen measurement, and deliberately so: a thumb covers a
478/// wide contact patch and rolls as it presses, so a smaller threshold turns taps
479/// near the edge of a button into aborted drags. It is the value the mature
480/// toolkits settled on after starting at 8 and hearing that targets were too hard
481/// to hit.
482const TOUCH_SLOP: f32 = 18.0;
483
484/// The same threshold for a **precise** pointer — a mouse or a trackpad. It knows
485/// exactly where it is, so almost any movement is intentional.
486const PRECISE_SLOP: f32 = 1.0;
487
488/// The velocity a release should be flung with, per axis — zero on an axis the
489/// gesture did not really travel along.
490///
491/// Speed alone is not a fling. A finger that twitches fast over three pixels
492/// produces a large velocity and no intent; requiring the gesture to have
493/// **covered ground** as well is what separates the two, and the distance that says
494/// "this was a drag" is the same `slop` that started it.
495///
496/// The gate is per axis because a scroll is two independent axes: a swipe running
497/// down the screen must not fling sideways on the little horizontal wobble a thumb
498/// always adds.
499fn fling_velocity(estimate: VelocityEstimate, slop: f32) -> (f32, f32) {
500    let gate = |velocity: f32, travelled: f32| {
501        if travelled.abs() > slop {
502            velocity
503        } else {
504            0.0
505        }
506    };
507    (
508        gate(estimate.velocity.x, estimate.offset.0),
509        gate(estimate.velocity.y, estimate.offset.1),
510    )
511}
512
513/// The elastic overshoot allowed past the scroll bounds, in px — the rubber band.
514const SCROLL_OVER: f32 = 48.0;
515
516/// The minimum velocity, in px/s, on releasing a pan that starts a fling.
517const PAN_FLING_MIN: f32 = 80.0;
518
519/// The width, in physical px, of the edge zone that arms the back gesture.
520const BACK_EDGE: f32 = 24.0;
521
522/// The **drag-and-drop** (reorder) preview — paint geometry only. The shadow's
523/// *colour* comes from the theme (`theme.scheme.shadow`, as `Button` does); only the
524/// geometry lives here, as named constants rather than magic numbers scattered about.
525mod drag_preview {
526    /// The vertical offset of the ghost's drop shadow, in px.
527    pub const SHADOW_OFFSET_Y: f32 = 4.0;
528    /// The blur of the ghost's drop shadow, in px.
529    pub const SHADOW_BLUR: f32 = 12.0;
530    /// The opacity of the ghost's shadow.
531    pub const SHADOW_ALPHA: f32 = 0.28;
532    /// The opacity of the ghost's `primary` border.
533    pub const BORDER_ALPHA: f32 = 0.9;
534    /// The thickness of the ghost's `primary` border, in px.
535    pub const BORDER_WIDTH: f32 = 1.5;
536    /// A slight vertical lift of the ghost during a **horizontal** drag, for columns.
537    pub const LIFT_Y: f32 = -2.0;
538    /// The thickness of the **insertion line** in the vertical preview, in px.
539    pub const INSERT_THICKNESS: f32 = 3.0;
540}
541
542/// Whether a frame builds the view again rather than repainting the tree it already has:
543/// when something asked for it (`dirty`), while the application's own animations are
544/// moving **and in the frame they stop in**, or for a reason of the frame's own
545/// (`otherwise`: no tree yet, a switcher in flight).
546///
547/// The frame they stop in, because the tick that ends a transition has changed the state
548/// — a push's page left behind is gone — and reports nothing moving. Without it the tree
549/// hit-tested from then on is the transition's last, the page left behind still in it
550/// under the finger (milestone 531).
551fn frame_needs_build(dirty: bool, animating: bool, was_animating: bool, otherwise: bool) -> bool {
552    dirty || animating || was_animating || otherwise
553}
554
555/// What a press is still a candidate for once it has chosen its drag: the long press a
556/// widget under the finger asks for, and the item it would lift on a hold.
557///
558/// A long press only while the press was not captured by a drag — a scrollbar, a handle,
559/// a selection; a touch scroll that has not moved yet stays a candidate. An item that
560/// lifts on a hold is a candidate under a scroll too: the scroll keeps the gesture for
561/// now, and the deadline decides, since a finger that stays put was never scrolling.
562fn hold_candidates<Msg: Clone>(
563    drag: Option<&Drag>,
564    ui: Option<&frus_widgets::Ui<Msg>>,
565    tree: Option<&dyn frus_widgets::Widget<Msg>>,
566    at: Point,
567) -> (Option<Msg>, Option<frus_widgets::DragSource>) {
568    // A back gesture owns its pointer from the press that starts it, as the reference's
569    // pop gesture does: nothing on either page takes a finger that is taking a page away
570    // (milestone 531). The page below is not even in the frame yet when the press lands —
571    // what is under the finger is whatever the frame on screen holds.
572    if matches!(drag, Some(Drag::Back { .. })) {
573        return (None, None);
574    }
575    let Some(ui) = ui else {
576        return (None, None);
577    };
578    // A drag that has not moved yet may still be a hold: a scroll's, or a detector's that
579    // also takes a long press (milestone 582, seen on the phone).
580    let free = matches!(
581        drag,
582        None | Some(Drag::Scroll { moved: false, .. }) | Some(Drag::Gesture { moved: false, .. })
583    );
584    let long_press = if free { ui.long_press_at(at) } else { None };
585    let lift = ui.drag_source_at(at).filter(|source| {
586        tree.and_then(|tree| find_widget(tree, source.id))
587            .is_some_and(|widget| widget.drag_needs_long_press())
588    });
589    (long_press, lift)
590}
591
592/// The drag a press is left with when its hold's deadline fires, from the drag it had
593/// and what the press found to lift.
594///
595/// A pending touch scroll no longer has any reason to exist — unless the hold was the
596/// lift of an item or of a row, in which case the scroll hands the gesture over and what
597/// was held is already up: `moved` is true because the hold *was* the threshold, and
598/// asking for a movement as well would mean a row that was held and then carried
599/// straight out of the list never engaged at all.
600fn drag_after_hold(
601    drag: Option<Drag>,
602    lift: Option<frus_widgets::DragSource>,
603    reorder: Option<(WidgetId, usize, Point)>,
604    cursor: Point,
605) -> Option<Drag> {
606    // Never a back gesture's: the hold was not what that finger was doing, and taking the
607    // drag from it would leave the application's gesture open with nothing to end it.
608    if let Some(Drag::Back { start_x }) = drag {
609        return Some(Drag::Back { start_x });
610    }
611    if let Some(source) = lift {
612        Some(Drag::Item {
613            source,
614            start: cursor,
615            moved: true,
616            over: None,
617        })
618    } else if let Some((id, from, start)) = reorder {
619        Some(Drag::Reorder {
620            id,
621            from,
622            start,
623            moved: true,
624            carried: None,
625        })
626    } else {
627        None
628    }
629}
630
631/// A drag currently under way with the mouse.
632enum Drag {
633    /// A scrollbar's thumb.
634    Scrollbar {
635        id: WidgetId,
636        vertical: bool,
637        grab: f32,
638        track_start: f32,
639        track_len: f32,
640        thumb_len: f32,
641        max: f32,
642        /// Whether this axis numbers its offsets from the far end, in which case the
643        /// thumb's position along the track reads backwards.
644        reverse: bool,
645    },
646    /// A selection dragged out over the texts of a
647    /// [`SelectionArea`](frus_widgets::SelectionArea): it may leave the text it began in, and
648    /// never leaves the area.
649    RegionSelect { area: WidgetId },
650    /// A handle of a selection area's touch selection, held (milestone 577): it carries its
651    /// end of the selection to the text under the finger, the other end staying put. `grab` is
652    /// the offset from the finger to the text position the handle stands for.
653    RegionHandle {
654        area: WidgetId,
655        handle: crate::selection::Handle,
656        grab: Point,
657    },
658    /// A text selection inside a field, with its bounds, for placement.
659    TextSelect {
660        id: WidgetId,
661        rect: frus_widgets::Rect,
662    },
663    /// A **selection handle**, held (milestone 511). `grab` is the offset from the finger
664    /// to the text position the handle stands for, so the selection follows the handle
665    /// rather than jumping to the fingertip the moment it moves.
666    SelectionHandle {
667        id: WidgetId,
668        rect: frus_widgets::Rect,
669        handle: crate::selection::Handle,
670        grab: Point,
671    },
672    /// A draggable widget — a slider or a handle — dragged along its horizontal axis.
673    /// `last_x` is the pointer's last abscissa, so the **delta** can be delivered to
674    /// the handles that accumulate, such as a column resize.
675    Widget {
676        id: WidgetId,
677        rect: frus_widgets::Rect,
678        last_x: f32,
679        /// Where the press landed, and the widget's track as it stood then: a slide that
680        /// moves the value from where it was measures from both (milestone 650).
681        press_x: f32,
682        track: frus_widgets::DragTrack,
683        /// Whether the press started anything: a slider that only takes a slide on its
684        /// thumb, pressed elsewhere, takes none of the gesture.
685        active: bool,
686        /// On a track with two thumbs, the one the press took — `None` until a press on
687        /// both has moved far enough to say which (milestone 651).
688        thumb: Option<usize>,
689    },
690    /// Reordering a **column**: a header is grabbed (`id`, column `from`) and dropped
691    /// onto another. `moved` tells a drag from a plain tap — which stays a sort —
692    /// by the `TOUCH_SLOP` threshold measured from `start`.
693    Reorder {
694        id: WidgetId,
695        from: usize,
696        start: Point,
697        moved: bool,
698        /// What is carried and where: the row the grab moves and its box, taken from the
699        /// frame the first time the drag is carried and moved with the content since, as a
700        /// lifted item's box is. The frame knows a reorderable's box only while it shows, and
701        /// only as much of it as shows — and carrying a row to an edge is exactly what
702        /// scrolls it out of sight (milestone 527, seen on a phone).
703        carried: Option<(WidgetId, Rect)>,
704    },
705    /// Panning an interactive viewport (`InteractiveViewer`): the pointer pushes the
706    /// content. `last` is the previous position, for the delta; `moved` tells a real
707    /// pan from a plain tap, by the `TOUCH_SLOP` threshold, so a click on a child can
708    /// still get through. `viewport` bounds the pan to the frame. The release
709    /// velocity comes from the shell's gesture tracker.
710    Pan {
711        id: WidgetId,
712        last: Point,
713        moved: bool,
714        viewport: frus_widgets::Rect,
715    },
716    /// Scrolling a scrollable area with a finger. `moved` tells a real scroll from a
717    /// plain tap, movement staying under the `TOUCH_SLOP` threshold.
718    Scroll {
719        id: WidgetId,
720        last: Point,
721        moved: bool,
722        /// The speed inherited from a fling this press interrupted, per axis, in
723        /// px/s — added to the release velocity so repeated swipes build momentum
724        /// where the platform does that. Zero when the content was already still.
725        carried: (f32, f32),
726        /// A dismissible item under the finger, still in the running. Both gestures
727        /// start the same way, so neither is chosen at the press: the first movement
728        /// past the threshold decides by **direction**, and the loser never sees the
729        /// gesture. Cleared once the scroll has won.
730        dismiss: Option<frus_widgets::Dismissable>,
731        /// A detector that takes a drag under the finger, still in the running, with the way
732        /// it drags (milestone 582). Decided at the threshold like `dismiss`: by direction.
733        pan: Option<(WidgetId, frus_widgets::PanAxis)>,
734        /// The axis this gesture was **claimed by**, decided once at the threshold and
735        /// held for the rest of the drag: `true` for vertical.
736        ///
737        /// A page that scrolls down must not drift sideways at the same time, and a
738        /// finger never travels in a straight line. The reference settles this in its
739        /// gesture arena — a vertical and a horizontal recogniser compete, the first
740        /// past its own slop wins, the loser is out of the gesture entirely — and this
741        /// is the same rule for an area that can go both ways. `None` until the
742        /// threshold is crossed.
743        axis: Option<bool>,
744    },
745    /// A drag on a [`GestureDetector`](frus_widgets::GestureDetector) (milestone 582). `start`
746    /// is where the pointer went down, `last` where it was at the last update; `moved` tells a
747    /// drag from a tap, by the slop, and the start is only reported once it is a drag.
748    Gesture {
749        id: WidgetId,
750        axis: frus_widgets::PanAxis,
751        start: Point,
752        last: Point,
753        moved: bool,
754    },
755    /// Swiping a [`frus_widgets::Dismissible`] item aside. `last` is the previous
756    /// position, for the delta; the item is already past the threshold by the time this
757    /// exists, since it is only ever reached by winning the direction test.
758    Dismiss {
759        item: frus_widgets::Dismissable,
760        last: Point,
761        /// `false` until the finger has passed the threshold — a press that never
762        /// travels is still a tap on the row.
763        moved: bool,
764    },
765    /// Carrying a [`frus_widgets::Draggable`] towards a [`frus_widgets::DragTarget`].
766    /// `over` is the target the pointer is on now, when it would accept the payload —
767    /// kept here so that entering and leaving one costs a comparison rather than a
768    /// second hit test.
769    Item {
770        source: frus_widgets::DragSource,
771        start: Point,
772        /// `false` until the finger has passed the threshold: a press that never
773        /// travels is a plain click on whatever is inside.
774        moved: bool,
775        over: Option<WidgetId>,
776    },
777    /// A [`frus_widgets::DraggableScrollableSheet`] moved by its panel, where nothing
778    /// under the finger scrolls — its grabber, its header, a list too short to move
779    /// (milestone 515). `available` is the height its shares are shares of, taken at
780    /// the press: the panel may shrink to nothing under the finger, and still come back.
781    Sheet {
782        id: WidgetId,
783        last: Point,
784        /// `false` until the finger has passed the threshold: a press that never
785        /// travels is a tap on whatever is in the sheet.
786        moved: bool,
787        available: f32,
788    },
789    /// The "back" gesture: the framework measures the finger's progress and velocity
790    /// and passes them to the application, which decides on the navigation.
791    Back { start_x: f32 },
792}
793
794/// The driver: an `event → frame` loop around an [`Application`].
795/// Turns an application's `view` into a tree that is **ready to be read**.
796///
797/// The one way this shell builds a tree, and it exists because there were two. A
798/// `ThemeBuilder` — and everything built on one, an `AppBar` included — has no children
799/// until [`build_deferred`] has been over it, so every traversal of a tree that has not
800/// been prepared finds nothing inside such a subtree and says nothing about it.
801///
802/// Both of the shell's build sites read the tree **before** the layout pass would have
803/// prepared it: the frame's own path calls `collect_ids` on it for the mount and leave
804/// bookkeeping, and the burst path hands it straight to `find_widget`. Neither of those is
805/// a mistake to be spotted at a call site; they are why this is one function.
806///
807/// The surface must already be described — the reader's font setting reaches anything a
808/// deferred subtree measures, and a build outside a surface measures at scale 1 while the
809/// frame lays it out at whatever the reader asked for (milestone 408).
810/// **The ambient answers an application gives**, read afresh every frame.
811///
812/// Both of these are settings rather than facts: a settings screen may turn scrollbars on
813/// or change the reader's language while the application is running, and neither should
814/// wait for a restart. Reading them once at start-up is the bug this shape avoids.
815///
816/// It is one function so that a test can drive it. The frame loop needs a window and an
817/// event-loop proxy, so nothing in this repo can call the loop — which is exactly how a
818/// setting that never reached production got shipped once already (milestone 408).
819fn install_ambient<A: Application>(app: &A, preferred: &[frus_widgets::Locale]) {
820    // **How the scrollables behave** (milestone 616): the application's behaviour, at the
821    // root of the tree, under every `ScrollConfiguration` the view installs.
822    frus_widgets::ScrollConfiguration::set_root(app.scroll_behavior());
823    // **Which language the interface is in**, resolved from what the platform reports
824    // against what the application has. Installed before the table, because an
825    // application choosing its table by language reads it from here.
826    frus_widgets::locale::install(app.resolved_locale(preferred));
827    if let Some(table) = app.localizations() {
828        frus_widgets::localizations::install(table);
829    }
830}
831
832fn build_view<A: Application>(
833    app: &A,
834    theme: &Theme,
835    runtime: &frus_widgets::Runtime,
836) -> Box<dyn Widget<A::Message>> {
837    debug_assert!(
838        frus_widgets::MediaQuery::of().is_described(),
839        "a view is being built with no surface described: install one first — milestone 416"
840    );
841    // Every state a component keeps is marked as reached while the tree is built, and
842    // what a build did not reach is let go when the frame that laid it out is done.
843    runtime.states.begin_build();
844    let tree = app.view(theme);
845    build_deferred(tree.as_ref(), theme, runtime);
846    tree
847}
848
849/// Where the messages an effect produces on another thread are sent: the event loop's proxy,
850/// or nowhere for a driver no event loop runs.
851struct Mailbox<M: 'static>(Option<EventLoopProxy<M>>);
852
853impl<M: 'static> Clone for Mailbox<M> {
854    fn clone(&self) -> Self {
855        Self(self.0.clone())
856    }
857}
858
859impl<M: 'static> Mailbox<M> {
860    /// Sends `message` to the loop. An error when the loop has closed, or when there is
861    /// none — either way nobody is left to tell.
862    fn send_event(&self, message: M) -> Result<(), ()> {
863        match &self.0 {
864            Some(proxy) => proxy.send_event(message).map_err(|_| ()),
865            None => Err(()),
866        }
867    }
868}
869
870/// The event handler that drives an [`Application`]: it owns the window, the renderer, the
871/// last interface built and the runtime, and turns each event of the loop into messages,
872/// frames and effects. [`run`](crate::run) and the other entry points build one; it is
873/// exported for a platform layer that supplies its own loop, not for an application to
874/// construct.
875pub struct App<A: Application> {
876    /// The application being driven: its state and logic.
877    app: A,
878    /// The channel that feeds back the messages effects produce, from their threads.
879    proxy: Mailbox<A::Message>,
880    window: Option<Arc<Window>>,
881    renderer: Option<Renderer>,
882    /// The renderer's GPU device was lost: a new renderer is made at the next frame's start
883    /// (milestone 686).
884    renderer_lost: bool,
885    /// A renderer being initialised **asynchronously**, on the Web only: filled in by
886    /// the `spawn_local` future and picked up on the first frame, the Web being unable
887    /// to block on GPU init.
888    #[cfg(web)]
889    pending_renderer: std::rc::Rc<std::cell::RefCell<Option<Renderer>>>,
890    /// What this side knows of the browser's history — see [`crate::history`].
891    #[cfg(web)]
892    history: crate::history::History,
893    /// How the application's location is written in the address bar, and the document's base
894    /// path, which the `Path` way of writing needs. Read once, at launch.
895    #[cfg(web)]
896    address: (crate::LocationStrategy, String),
897    /// The window's accessibility bridge (AccessKit) — desktop only.
898    #[cfg(desktop)]
899    a11y: Option<crate::a11y::A11y>,
900    /// The window's accessibility bridge, projected into the canvas's DOM — web only.
901    #[cfg(web)]
902    a11y: Option<crate::a11y_web::A11y>,
903    /// The last interface built, used for hit testing, focus and scrolling.
904    ui: Option<Ui<A::Message>>,
905    /// The last widget tree built, used for keyboard and editing routing.
906    tree: Option<Box<dyn Widget<A::Message>>>,
907    /// The pointer's last known position, in **logical** pixels.
908    cursor: Point,
909    /// Where the pointer that **hovers** is: a mouse once it has moved, a finger only
910    /// while it touches. Kept as a place and asked of each frame — see [`crate::hover`].
911    hover: crate::hover::Hover,
912    /// The screen's DPI scale factor (physical = logical × scale × density).
913    scale: f32,
914    /// The last **logical** size handed to the app, to detect breakpoints.
915    last_size: Option<(f32, f32)>,
916    /// State retained between frames: hover and focus, scroll, caret and selection.
917    runtime: Runtime,
918    /// **The theme on display and the fade toward the one now asked for** — the
919    /// framework's, not the application's, since milestone 452. It resolves the
920    /// application's themes against the platform's brightness and the reader's contrast
921    /// setting once a frame, and crosses between two of them over time.
922    themes: crate::theming::ThemeFade,
923    /// Live-reload watching (development, `FRUS_WATCH=1`): relaunch on recompilation.
924    reload: Option<crate::reload::ReloadWatcher>,
925    /// The channel `frus run` listens on, when it started this application — see [`crate::dev`].
926    #[cfg(any(desktop, android))]
927    dev: Option<crate::dev::DevChannel>,
928    /// Overflowing boxes already named on the console, so that a layout that does not
929    /// fit is reported once and not sixty times a second.
930    reported_overflows: std::cell::RefCell<std::collections::HashSet<u64>>,
931    /// Is the runtime inspector on? Toggled by F12, in debug builds only.
932    inspector: bool,
933    /// A tree dump to print on the next inspected frame.
934    inspector_dump: bool,
935    /// What the keys of `frus run` turned on or asked for (milestone 668); see
936    /// [`App::take_dev_commands`].
937    debug: DebugAsks,
938    /// The current keyboard modifiers.
939    shift: bool,
940    ctrl: bool,
941    alt: bool,
942    meta: bool,
943    /// The remembered "goal" visual column for Up/Down/PgUp/PgDn: crossing shorter
944    /// lines keeps the original column, the way an editor does. Cleared as soon as any
945    /// other caret movement happens.
946    goal_x: Option<f32>,
947    /// Clipboard access: `arboard` on the desktop, the platform's on Android, the
948    /// browser's on the web, nothing yet on iOS.
949    clipboard: clip::Clipboard,
950    /// Has the startup effect (`init`) already run? This keeps it from being replayed
951    /// when the surface is recreated, as it is when Android returns from background.
952    started: bool,
953    /// The last frame's instant, for the animations' dt.
954    last_frame: Option<Instant>,
955    /// The **build** phase is dirty: the app's state, or the size, changed and the
956    /// `view` must be rebuilt. The view is a pure function of `(state, theme, size)` —
957    /// never of hover, scroll or focus, which live in the `Runtime` — so a frame that
958    /// only animates an interaction merely **repaints** the retained tree (§1: "a hover
959    /// touches paint and nothing else").
960    build_dirty: bool,
961    /// Whether the application's own animations were moving at the last frame. The frame
962    /// they stop in is built once more: the tick that ends a transition has changed the
963    /// state — the page it left is gone — and says it is no longer moving, so without it
964    /// the tree hit-tested from then on is the transition's last (milestone 531).
965    app_was_animating: bool,
966    /// The mouse drag under way.
967    drag: Option<Drag>,
968    /// Was the pointer that started the drag under way a finger? A mouse is
969    /// precise and needs almost no slop; a finger needs a lot.
970    pointer_touch: bool,
971    /// The pointer history of the drag under way, and the instant it began.
972    ///
973    /// One tracker for all of them: at most one drag is active at a time, and every
974    /// kind of drag asks the same question on release — how fast was the finger
975    /// going. Keeping it here rather than in each [`Drag`] variant means the
976    /// gesture's clock and its history start together, in one place.
977    gesture_velocity: VelocityTracker,
978    /// The test driver's clock for the velocity tracker, when there is one: seconds of
979    /// frames run, and when the gesture under way began on it. `None` in an application,
980    /// which stamps samples with the wall clock. A driver's events are microseconds apart on
981    /// the wall, and irregularly so on a loaded machine: a fling read from them came out at
982    /// any speed, in either direction, and a test that let go of a scroll saw it thrown back
983    /// to the top (milestone 584, seen in CI).
984    gesture_clock: Option<(f32, f32)>,
985    gesture_start: Instant,
986    /// The pointer's **smoothed** abscissa during a reorder: it springs toward the
987    /// real position, giving the columns' sliding a gentle inertia — the background
988    /// catches up with the ghost, which sticks to the pointer.
989    reorder_x: f32,
990    /// The **smoothed** ordinate of the **insertion line** during a **vertical**
991    /// reorder, that is, Kanban cards: it springs toward the **chosen** slot edge, the
992    /// hovered half, so that the line and the gap *slide* between cards instead of
993    /// jumping — the vertical counterpart of the horizontal `reorder_x` spring.
994    reorder_y: f32,
995    /// The last **announcement** pushed to the accessibility bridge's live region, for
996    /// the screen reader. It persists as is: it is re-spoken only on a change, so the
997    /// same text carried over every frame does not repeat. Desktop and web.
998    #[cfg(any(desktop, web))]
999    announce: String,
1000    /// The tap-or-long-press recogniser (gesture tier 1).
1001    press: PressRecognizer,
1002    /// The pressed target's long-press message, captured on the press.
1003    long_press_msg: Option<A::Message>,
1004    /// A **text field** pressed with a finger: a hold there selects the word under it
1005    /// (milestone 511), where on anything else it is the widget's long press.
1006    pending_word: Option<WidgetId>,
1007    /// A finger came down on words in a selection area: a hold there selects the word under it
1008    /// and opens the bar (milestone 576).
1009    pending_region_hold: bool,
1010    /// The focused field's caret blink (milestone 513).
1011    caret: crate::caret::CaretBlink,
1012    /// The form last shown to the platform's autofill service, as it was shown
1013    /// (milestone 512) — what a returned value is routed against, and what a value the
1014    /// service has not heard is told apart from.
1015    #[cfg(android)]
1016    autofill_reported: Vec<crate::autofill::AutofillField>,
1017    /// The field of that form the service was told has focus.
1018    #[cfg(android)]
1019    autofill_focus: Option<WidgetId>,
1020    /// The [`frus_widgets::AutofillGroup`] that form is, while it is on screen.
1021    #[cfg(android)]
1022    autofill_group: Option<WidgetId>,
1023    /// When the last **recorded** edit happened, for the pause that breaks a run of
1024    /// typing into two steps of undo. One field and not one per text field, because only
1025    /// the focused one is being typed into, and leaving a field ends its run anyway.
1026    last_edit_at: Option<Instant>,
1027    /// A [`frus_widgets::Draggable`] that asked to be lifted by a **hold**, waiting
1028    /// for the long-press deadline. Inside a scrollable this is the only way up: the
1029    /// plain drag belongs to the scroll, and a hold is the one signal it cannot claim.
1030    pending_lift: Option<frus_widgets::DragSource>,
1031    /// A **reorderable row** that asked to be lifted by a hold, waiting for the same
1032    /// deadline: `(the row, its index, where the finger landed)`.
1033    ///
1034    /// Its own field rather than a second use of `pending_lift`, because what the two
1035    /// become at the deadline is different — one carries a payload to a drop target, the
1036    /// other carries itself to a slot — but the reason they wait is the same, and it is
1037    /// the reason a list can still be scrolled.
1038    pending_reorder: Option<(WidgetId, usize, Point)>,
1039    /// The last click's instant, for double-click detection.
1040    last_click_time: Option<Instant>,
1041    /// When and where the last press began a selection in a selection area: a second one soon
1042    /// after and close by is a double click.
1043    last_region_click: Option<(Instant, Point)>,
1044    /// A first tap waiting to see whether it is half of a double tap (milestone 581).
1045    pending_tap: Option<PendingTap<A::Message>>,
1046    /// How many values the work components started had brought when this shell last looked
1047    /// (milestone 585).
1048    task_arrivals: u64,
1049    /// The regions the pointer is in, the innermost first, each with its box when it was
1050    /// last seen (milestone 583).
1051    entered: Vec<(WidgetId, Rect)>,
1052    /// Whether a button or a finger is down: a region hears moves only without one.
1053    pointer_held: bool,
1054    /// The widgets hearing the pointer's raw events that it went down in, each with its box
1055    /// then: they hear it until it lifts, wherever it goes (milestone 586).
1056    captured: Vec<(WidgetId, Rect)>,
1057    /// A counter for the keys of leaving events, which fade out.
1058    leaving_counter: u64,
1059    /// The running subscriptions: id → cancellation handle, dropping which stops it.
1060    running_subs: HashMap<u64, SubHandle>,
1061    /// Pending focus requests — the keys `Command::focus` produced — resolved against
1062    /// the **freshly built** tree on the next frame.
1063    pending_focus: Vec<u64>,
1064    /// Pending **scroll** requests — the `(key, ScrollTo)` pairs `Command::scroll`
1065    /// produced — resolved against the frame that follows, and dropped whether or not
1066    /// the key named anything (see `Command::scroll`).
1067    pending_scroll: Vec<(u64, ScrollTo)>,
1068    /// The requests of the frame in progress that the **previous** frame's registry
1069    /// could not place — a region that has only just appeared. Tried once more against
1070    /// the registry this frame builds, and then gone: a request gets one frame.
1071    retry_scroll: Vec<(u64, ScrollTo)>,
1072    /// Pending **sheet** requests — the `(key, SheetTo)` pairs `Command::sheet` produced —
1073    /// on the terms of the scroll requests above.
1074    pending_sheet: Vec<(u64, SheetTo)>,
1075    /// The sheet requests of the frame in progress that the previous frame's sheets did
1076    /// not name: tried once more against this frame's.
1077    retry_sheet: Vec<(u64, SheetTo)>,
1078    /// The **focus history** of triggers, for returning focus when an overlay closes:
1079    /// on every focus change the old one, if still present, is pushed; when focus
1080    /// **vanishes** because a menu or modal closed, we go back to the most recent
1081    /// entry that is still present.
1082    focus_history: Vec<WidgetId>,
1083    /// The previous frame's focus, to detect the transitions worth pushing.
1084    prev_focus: Option<WidgetId>,
1085    /// The focus the widgets were last told about (milestone 660); see
1086    /// `report_focus`.
1087    focus_reported: Option<WidgetId>,
1088    /// The widgets that took the focus on appearing, as of the last frame: one that is new
1089    /// this frame takes it (milestone 605).
1090    autofocused: std::collections::HashSet<WidgetId>,
1091    /// How smooth the frames are, when asked (milestone 609).
1092    frame_stats: crate::frame_stats::FrameStats,
1093    /// What the frame being drawn has cost so far, while frames are measured.
1094    frame_costs: crate::frame_stats::FrameCosts,
1095    /// The window is occluded, so rendering is suspended.
1096    occluded: bool,
1097    /// Cumulative elapsed time, in seconds, for the continuous animations.
1098    elapsed: f32,
1099    /// The last window insets handed to the app — padding plus keyboard — in logical px.
1100    last_insets: WindowInsets,
1101    /// Whether the window's title bar line is shared with the content, and what the content
1102    /// is told about it (milestone 640).
1103    #[cfg_attr(not(windows), allow(dead_code))]
1104    title_bar_on: bool,
1105    title_bar: Option<frus_widgets::TitleBar>,
1106    /// The application's icon, kept for the life of the program, to paint on the line.
1107    #[cfg_attr(not(windows), allow(dead_code))]
1108    window_icon: Option<&'static [u8]>,
1109    /// The soft keyboard has just grown, so the focused widget is brought back into view
1110    /// once the frame that lays out the shortened window is built: the keyboard covers
1111    /// the bottom of the screen, and a field under it is a field the reader cannot see
1112    /// while typing in it.
1113    reveal_after_keyboard: bool,
1114    /// What the **platform** last said about the person using it: the font-size slider,
1115    /// the night setting, the accessibility switches.
1116    ///
1117    /// Cached rather than read per frame, because reading it is a walk across the JNI
1118    /// boundary and the answer changes about once a year. It is refreshed when the
1119    /// surface appears — which on Android is also when it *reappears*, the activity being
1120    /// recreated on a font-scale or night change since no `configChanges` is declared —
1121    /// and on a theme change where the platform sends one.
1122    platform: PlatformSettings,
1123    /// The **keyboard-free** inset baseline, along with the physical size it was
1124    /// taken at — a rotation resets it: whatever bottom inset exceeds it is credited
1125    /// to the software keyboard.
1126    inset_baseline: Option<(Insets, (u32, u32))>,
1127    /// The Android activity's handle, used to query the insets, the keyboard and so on.
1128    /// The current **lifecycle** state, so the app is notified only on real
1129    /// **transitions**.
1130    lifecycle: Lifecycle,
1131    #[cfg(android)]
1132    android_app: Option<winit::platform::android::activity::AndroidApp>,
1133    /// Is the software keyboard being asked for? It follows the text fields' focus.
1134    #[cfg(android)]
1135    soft_input_shown: bool,
1136    /// The system bars as the platform was last told them, so that it is told only of a
1137    /// change (#46).
1138    #[cfg(android)]
1139    system_bars: Option<frus_widgets::SystemBars>,
1140}
1141
1142impl<A: Application> App<A> {
1143    /// Creates the driver around an application and its message channel.
1144    pub fn new(app: A, proxy: EventLoopProxy<A::Message>) -> Self {
1145        Self::with_mailbox(app, Mailbox(Some(proxy)))
1146    }
1147
1148    /// A driver with no event loop behind it, for a test that feeds it input and frames
1149    /// by hand: whatever an effect sends back goes nowhere. An event loop cannot be built
1150    /// on a machine with no display, which is where the continuous integration runs.
1151    #[cfg(any(test, feature = "testing"))]
1152    fn detached(app: A) -> Self {
1153        let mut shell = Self::with_mailbox(app, Mailbox(None));
1154        // A shell with no window is a test's: it keeps a clipboard of its own rather than
1155        // the machine's, which tests running side by side would share — and which macOS
1156        // does not let several threads touch at once (a test run aborted on it).
1157        shell.clipboard = clip::Clipboard::in_memory();
1158        shell
1159    }
1160
1161    fn with_mailbox(app: A, proxy: Mailbox<A::Message>) -> Self {
1162        // The widget layer knows how to *want* an image over the network and has no way
1163        // to get one: no runtime, no socket, and no dependency on this crate, since the
1164        // dependency runs the other way. So the shell says how, on the way up, and the
1165        // widget layer only asks. Same shape as the image decoder a step earlier.
1166        #[cfg(feature = "net")]
1167        frus_widgets::set_image_fetcher(fetch_image_bytes);
1168        // And how to run the work a component starts (milestone 585): this shell's executor
1169        // natively, the browser on the Web.
1170        #[cfg(not(web))]
1171        frus_widgets::set_task_spawner(|task| crate::runtime::spawn(task).detach());
1172        #[cfg(web)]
1173        frus_widgets::set_task_spawner(wasm_bindgen_futures::spawn_local);
1174        Self {
1175            app,
1176            proxy,
1177            window: None,
1178            renderer: None,
1179            renderer_lost: false,
1180            #[cfg(web)]
1181            pending_renderer: std::rc::Rc::new(std::cell::RefCell::new(None)),
1182            #[cfg(web)]
1183            history: crate::history::History::new(),
1184            #[cfg(web)]
1185            address: (crate::LocationStrategy::Hash, "/".to_string()),
1186            #[cfg(desktop)]
1187            a11y: None,
1188            #[cfg(web)]
1189            a11y: None,
1190            ui: None,
1191            tree: None,
1192            cursor: Point::new(0.0, 0.0),
1193            hover: crate::hover::Hover::default(),
1194            scale: 1.0,
1195            last_size: None,
1196            runtime: Runtime::default(),
1197            themes: crate::theming::ThemeFade::default(),
1198            reload: crate::reload::ReloadWatcher::new(),
1199            #[cfg(any(desktop, android))]
1200            dev: None,
1201            reported_overflows: std::cell::RefCell::new(std::collections::HashSet::new()),
1202            inspector: false,
1203            inspector_dump: false,
1204            debug: DebugAsks::default(),
1205            shift: false,
1206            ctrl: false,
1207            alt: false,
1208            meta: false,
1209            goal_x: None,
1210            clipboard: clip::Clipboard::new(),
1211            started: false,
1212            last_frame: None,
1213            build_dirty: true,
1214            app_was_animating: false,
1215            drag: None,
1216            pointer_touch: false,
1217            gesture_velocity: VelocityTracker::default(),
1218            gesture_clock: None,
1219            gesture_start: Instant::now(),
1220            reorder_x: 0.0,
1221            reorder_y: 0.0,
1222            #[cfg(any(desktop, web))]
1223            announce: String::new(),
1224            press: PressRecognizer::new(),
1225            long_press_msg: None,
1226            pending_word: None,
1227            pending_region_hold: false,
1228            caret: crate::caret::CaretBlink::new(),
1229            #[cfg(android)]
1230            autofill_reported: Vec::new(),
1231            #[cfg(android)]
1232            autofill_focus: None,
1233            #[cfg(android)]
1234            autofill_group: None,
1235            last_edit_at: None,
1236            pending_lift: None,
1237            pending_reorder: None,
1238            last_click_time: None,
1239            last_region_click: None,
1240            pending_tap: None,
1241            task_arrivals: 0,
1242            entered: Vec::new(),
1243            pointer_held: false,
1244            captured: Vec::new(),
1245            leaving_counter: 0,
1246            running_subs: HashMap::new(),
1247            pending_focus: Vec::new(),
1248            pending_scroll: Vec::new(),
1249            retry_scroll: Vec::new(),
1250            pending_sheet: Vec::new(),
1251            retry_sheet: Vec::new(),
1252            focus_history: Vec::new(),
1253            prev_focus: None,
1254            focus_reported: None,
1255            autofocused: std::collections::HashSet::new(),
1256            frame_stats: crate::frame_stats::FrameStats::new(),
1257            frame_costs: crate::frame_stats::FrameCosts::default(),
1258            occluded: false,
1259            elapsed: 0.0,
1260            last_insets: WindowInsets::ZERO,
1261            title_bar_on: false,
1262            title_bar: None,
1263            window_icon: None,
1264            reveal_after_keyboard: false,
1265            platform: PlatformSettings::default(),
1266            inset_baseline: None,
1267            // The app starts out detached; `resumed` will move it to `Resumed`.
1268            lifecycle: Lifecycle::Detached,
1269            #[cfg(android)]
1270            android_app: None,
1271            #[cfg(android)]
1272            soft_input_shown: false,
1273            #[cfg(android)]
1274            system_bars: None,
1275        }
1276    }
1277
1278    /// What an assistive technology's click on widget `id` does: what a pointer click on it
1279    /// would — its message, or, for a built-in button of the selection bar that carries none,
1280    /// its editing action (milestone 568), on the field the bar is open on.
1281    #[cfg(any(desktop, web))]
1282    fn activate_for_assistive_tech(&mut self, id: WidgetId) {
1283        if let Some(action) = self.ui.as_ref().and_then(|ui| ui.edit_action_for(id)) {
1284            self.perform_edit_action(action);
1285            return;
1286        }
1287        if let Some(msg) = self.ui.as_ref().and_then(|ui| ui.msg_for(id)) {
1288            // An item of the application's own on the bar closes it, as a press does.
1289            if self.ui.as_ref().is_some_and(|ui| ui.hit_is_on_toolbar(id)) {
1290                self.runtime.selection_handles = None;
1291                self.hide_selection_toolbar();
1292            }
1293            self.dispatch(msg);
1294            self.request_redraw();
1295        }
1296    }
1297
1298    /// Replays the actions an assistive technology asked for: an AT click activates
1299    /// the widget, exactly as a pointer click would, and an AT focus focuses it.
1300    #[cfg(desktop)]
1301    fn drain_a11y_actions(&mut self) {
1302        use crate::a11y::A11yAction;
1303        let actions = match self.a11y.as_ref() {
1304            Some(a11y) => a11y.take_actions(),
1305            None => return,
1306        };
1307        for action in actions {
1308            match action {
1309                A11yAction::Click(id) => self.activate_for_assistive_tech(id),
1310                A11yAction::Focus(id) => {
1311                    // A button of the selection bar takes no focus: the bar acts on the
1312                    // field, and a focus that left it would end the very selection the bar
1313                    // is open on before the click that follows the focus got to use it.
1314                    if self.ui.as_ref().is_some_and(|ui| ui.hit_is_on_toolbar(id)) {
1315                        continue;
1316                    }
1317                    self.runtime.input.focused = Some(id);
1318                    self.runtime.focus_visible = true;
1319                    self.request_redraw();
1320                }
1321            }
1322        }
1323    }
1324
1325    /// The web's counterpart to `drain_a11y_actions` above — same replay, the DOM
1326    /// bridge's own action type (#18).
1327    #[cfg(web)]
1328    fn drain_a11y_actions(&mut self) {
1329        use crate::a11y_web::A11yAction;
1330        let actions = match self.a11y.as_ref() {
1331            Some(a11y) => a11y.take_actions(),
1332            None => return,
1333        };
1334        for action in actions {
1335            match action {
1336                A11yAction::Click(id) => self.activate_for_assistive_tech(id),
1337                A11yAction::Focus(id) => {
1338                    // A button of the selection bar takes no focus: the bar acts on the
1339                    // field, and a focus that left it would end the very selection the bar
1340                    // is open on before the click that follows the focus got to use it.
1341                    if self.ui.as_ref().is_some_and(|ui| ui.hit_is_on_toolbar(id)) {
1342                        continue;
1343                    }
1344                    self.runtime.input.focused = Some(id);
1345                    self.runtime.focus_visible = true;
1346                    self.request_redraw();
1347                }
1348            }
1349        }
1350    }
1351
1352    /// Keeps the **system bars** in step with the frame (#46): their colour and their icons,
1353    /// from the regions under them and then the theme. The platform is told only when the
1354    /// answer changes, telling it being a crossing to the Java UI thread — and a request
1355    /// that did not arrive is asked again next frame rather than remembered as made.
1356    ///
1357    /// The two points are the first row of content under the status bar and the last one
1358    /// above the navigation bar: this window is not drawn behind the bars, so what a bar
1359    /// stands against is what lies at the edge of the content next to it.
1360    fn sync_system_bars(&mut self, theme: &Theme) {
1361        #[cfg(android)]
1362        {
1363            let Some(ui) = self.ui.as_ref() else {
1364                return;
1365            };
1366            let Some(window) = self.window.as_ref() else {
1367                return;
1368            };
1369            let scale = self.total_scale();
1370            let size = window.inner_size();
1371            let (width, height) = (size.width as f32 / scale, size.height as f32 / scale);
1372            let edges = self.last_insets.padding;
1373            let bars = ui
1374                .system_ui_style(
1375                    frus_widgets::Point::new(width / 2.0, edges.top + 0.5),
1376                    frus_widgets::Point::new(width / 2.0, height - edges.bottom - 0.5),
1377                )
1378                .resolve(theme);
1379            if self.system_bars != Some(bars) && crate::android_system_bars::apply(bars) {
1380                self.system_bars = Some(bars);
1381            }
1382        }
1383        #[cfg(not(android))]
1384        let _ = theme;
1385    }
1386
1387    /// Keeps the **software keyboard** in step with focus: asked for when focus is in
1388    /// a text field (`wants_keyboard`), closed otherwise. Called at the end of a
1389    /// frame, since any focus change already triggers a redraw.
1390    fn sync_soft_input(&mut self) {
1391        #[cfg(android)]
1392        {
1393            let editing = self
1394                .runtime
1395                .input
1396                .focused
1397                .and_then(|id| {
1398                    self.tree
1399                        .as_ref()
1400                        .and_then(|tree| find_widget(tree.as_ref(), id))
1401                })
1402                .is_some_and(wants_keyboard);
1403            if editing != self.soft_input_shown {
1404                self.soft_input_shown = editing;
1405                self.end_composition();
1406                if crate::android_ime::installed() {
1407                    // The InputConnection bridge: the Java view captures the IME.
1408                    if editing {
1409                        if let Some(id) = self.runtime.input.focused {
1410                            self.push_ime_context(id);
1411                        }
1412                        crate::android_ime::start_input(self.focused_ime());
1413                    } else {
1414                        crate::android_ime::clear_editor_state();
1415                        crate::android_ime::stop_input();
1416                    }
1417                } else if let Some(app) = &self.android_app {
1418                    // The TYPE_NULL fallback: a plain open, Latin keys only.
1419                    if editing {
1420                        app.show_soft_input(true);
1421                    } else {
1422                        app.hide_soft_input(false);
1423                    }
1424                }
1425            }
1426        }
1427    }
1428
1429    /// What keyboard the **focused field** asks for.
1430    ///
1431    /// Found the same way the caret is — the focused id, resolved against the last
1432    /// tree built — so the two answers cannot disagree about which field is being
1433    /// typed into. A focus that resolves to nothing falls back to ordinary text, which
1434    /// is what every field got before this was asked at all.
1435    #[cfg(android)]
1436    fn focused_ime(&self) -> frus_widgets::Ime {
1437        self.runtime
1438            .input
1439            .focused
1440            .and_then(|id| {
1441                self.tree
1442                    .as_ref()
1443                    .and_then(|tree| find_widget(tree.as_ref(), id))
1444            })
1445            .map(|widget| widget.ime())
1446            .unwrap_or_default()
1447    }
1448
1449    /// Notifies the app of a lifecycle **change**; never the same state twice in a row.
1450    fn set_lifecycle(&mut self, state: Lifecycle) {
1451        if self.lifecycle != state {
1452            self.lifecycle = state;
1453            self.app.on_lifecycle(state);
1454        }
1455    }
1456
1457    /// **Explicitly** asks for the software keyboard again, for the focused field —
1458    /// called when the user **taps in a text field**, even one already focused. This is
1459    /// indispensable because the keyboard may have been closed by the **system back
1460    /// button** without the app being told: `soft_input_shown` then stays `true` and
1461    /// focus does not change on the next tap, so
1462    /// [`sync_soft_input`](Self::sync_soft_input)'s diff would see no change and never
1463    /// reopen the keyboard. Here it is reopened unconditionally — the native behaviour:
1464    /// tapping in a field shows the keyboard.
1465    fn request_soft_input(&mut self) {
1466        #[cfg(android)]
1467        {
1468            // Only a field that takes typing: a tap in a paragraph made selectable is a tap
1469            // in words to select, not in a form.
1470            let typing = self
1471                .runtime
1472                .input
1473                .focused
1474                .and_then(|id| {
1475                    self.tree
1476                        .as_ref()
1477                        .and_then(|tree| find_widget(tree.as_ref(), id))
1478                })
1479                .is_some_and(wants_keyboard);
1480            if !typing {
1481                return;
1482            }
1483            self.soft_input_shown = true;
1484            self.end_composition();
1485            if crate::android_ime::installed() {
1486                if let Some(id) = self.runtime.input.focused {
1487                    self.push_ime_context(id);
1488                }
1489                crate::android_ime::start_input(self.focused_ime());
1490            } else if let Some(app) = &self.android_app {
1491                app.show_soft_input(true);
1492            }
1493        }
1494    }
1495
1496    /// Applies the pending IME operations to the focused field (the §6 bridge). The
1497    /// composition is materialised **in the field**: each update erases the previous
1498    /// one, the controlled model having no styled composition region yet — see
1499    /// docs/milestone-81.md.
1500    #[cfg(android)]
1501    fn drain_ime(&mut self) {
1502        use crate::ime::Step;
1503        let events = crate::android_ime::drain();
1504        if events.is_empty() {
1505            return;
1506        }
1507        let Some(focused) = self.runtime.input.focused else {
1508            return;
1509        };
1510        for event in events {
1511            let edit = self
1512                .runtime
1513                .edits
1514                .get(&focused)
1515                .copied()
1516                .unwrap_or_default();
1517            // Planned against the field as it stands before this operation: its text,
1518            // its caret and the composition it underlines (milestone 510).
1519            let text = self
1520                .tree
1521                .as_ref()
1522                .and_then(|tree| find_widget(tree.as_ref(), focused))
1523                .and_then(|widget| widget.text_value().map(str::to_owned))
1524                .unwrap_or_default();
1525            for step in crate::ime::plan(&event, &edit, &text) {
1526                match step {
1527                    Step::Place { cursor, anchor } => {
1528                        let edit = self.runtime.edits.entry(focused).or_default();
1529                        edit.cursor = cursor;
1530                        edit.anchor = anchor;
1531                    }
1532                    Step::Key(key) => self.apply_key(focused, key),
1533                    Step::Compose(region) => {
1534                        if let Some(edit) = self.runtime.edits.get_mut(&focused) {
1535                            edit.composing = region;
1536                        }
1537                    }
1538                    Step::ComposeTo(start) => {
1539                        if let Some(edit) = self.runtime.edits.get_mut(&focused) {
1540                            let end = edit.cursor;
1541                            edit.composing = (end > start).then_some((start, end));
1542                        }
1543                    }
1544                }
1545            }
1546        }
1547        // Refresh the input context, which the IME queries for its suggestions.
1548        self.push_ime_context(focused);
1549        self.request_redraw();
1550    }
1551
1552    /// Keeps the platform's **autofill service** in step with the form being edited
1553    /// (milestone 512). When a field that says what it is for takes focus, its form is
1554    /// shown to the service and the service is told which field it is in; while it stays,
1555    /// every value the service has not heard is reported, or what it saves is what the
1556    /// fields held when it first looked; and when the form's group has left the screen
1557    /// — submitted, or navigated away from — it is committed, which is the moment a
1558    /// service may offer to save what was typed.
1559    #[cfg(android)]
1560    fn sync_autofill(&mut self) {
1561        use crate::{android_autofill as platform, autofill};
1562        if !crate::android_ime::installed() {
1563            return;
1564        }
1565        let scale = self.total_scale();
1566        let (Some(ui), Some(tree)) = (self.ui.as_ref(), self.tree.as_ref()) else {
1567            return;
1568        };
1569        let fields_of = |id: WidgetId| {
1570            autofill::structure(&ui.form_of(id), scale, |widget| {
1571                find_widget(tree.as_ref(), widget).map(|w| {
1572                    (
1573                        w.autofill_hints(),
1574                        w.text_value().unwrap_or_default().to_owned(),
1575                    )
1576                })
1577            })
1578        };
1579        // The focused field, when it takes part: it says what it is for.
1580        let focus = self.runtime.input.focused.filter(|&id| {
1581            find_widget(tree.as_ref(), id).is_some_and(|w| !w.autofill_hints().is_empty())
1582        });
1583        if focus != self.autofill_focus {
1584            if let Some(left) = self.autofill_focus {
1585                platform::exit(autofill::virtual_id(left));
1586            }
1587            if let Some(entered) = focus {
1588                let group = ui.form_group(entered);
1589                // A field of the form already shown — the next step of a wizard — keeps
1590                // the fields shown before it; any other form starts afresh.
1591                let shown: &[autofill::AutofillField] =
1592                    if group.is_some() && group == self.autofill_group {
1593                        &self.autofill_reported
1594                    } else {
1595                        &[]
1596                    };
1597                let fields = autofill::with_gone(fields_of(entered), shown);
1598                platform::publish(&fields);
1599                platform::enter(autofill::virtual_id(entered));
1600                self.autofill_reported = fields;
1601                self.autofill_group = group;
1602            }
1603            self.autofill_focus = focus;
1604        } else if let Some(id) = focus {
1605            let fields = autofill::with_gone(fields_of(id), &self.autofill_reported);
1606            for (virtual_id, value) in autofill::changed(&self.autofill_reported, &fields) {
1607                platform::value_changed(virtual_id, &value);
1608            }
1609            self.autofill_reported = fields;
1610        }
1611        if let Some(group) = self.autofill_group {
1612            if ui.group_stops(group).is_empty() {
1613                platform::commit();
1614                self.autofill_group = None;
1615                self.autofill_reported.clear();
1616            }
1617        }
1618    }
1619
1620    /// Puts the values an autofill service chose into their fields (milestone 512), each
1621    /// through the message typing it would have produced — the path an undo takes, so a
1622    /// field that refuses typing refuses this too — with the caret after what arrived.
1623    #[cfg(android)]
1624    fn drain_autofill(&mut self) {
1625        let fills = crate::android_autofill::drain();
1626        if fills.is_empty() {
1627            return;
1628        }
1629        for (widget, value) in crate::autofill::route(&self.autofill_reported, fills) {
1630            let message = self
1631                .tree
1632                .as_ref()
1633                .and_then(|tree| find_widget(tree.as_ref(), widget))
1634                .and_then(|w| w.replace_value(value.clone()));
1635            if let Some(message) = message {
1636                self.runtime.edits.insert(
1637                    widget,
1638                    Edit {
1639                        cursor: value.chars().count(),
1640                        anchor: None,
1641                        composing: None,
1642                    },
1643                );
1644                self.dispatch_edit(message);
1645            }
1646        }
1647        if let Some(id) = self.runtime.input.focused {
1648            self.push_ime_context(id);
1649        }
1650        self.request_redraw();
1651    }
1652
1653    /// Publishes field `id`'s editing state to the bridge — text, caret and selection
1654    /// — the context the IME reads for its suggestions.
1655    #[cfg(android)]
1656    fn push_ime_context(&self, id: WidgetId) {
1657        let value = self
1658            .tree
1659            .as_ref()
1660            .and_then(|tree| find_widget(tree.as_ref(), id))
1661            .and_then(|widget| widget.text_value().map(|s| s.to_string()));
1662        if let Some(text) = value {
1663            let edit = self.runtime.edits.get(&id).copied().unwrap_or_default();
1664            crate::android_ime::set_editor_state(&text, edit.cursor, edit.selection_range());
1665            // And the keyboard is **told**, as an Android editor tells it on every change:
1666            // a keyboard that predicts keeps its own model of the field, and one never
1667            // told of a change drifts from it (milestone 510). In UTF-16 units.
1668            let len = text.chars().count();
1669            let at = |i: usize| crate::ime::utf16_index(&text, i.min(len)) as i32;
1670            let (start, end) = edit.selection_range().unwrap_or((edit.cursor, edit.cursor));
1671            let (cand_start, cand_end) = edit
1672                .composing
1673                .map(|(s, e)| (at(s), at(e)))
1674                .unwrap_or((-1, -1));
1675            crate::android_ime::update_selection(at(start), at(end), cand_start, cand_end);
1676        }
1677    }
1678
1679    /// Ends the focused field's composition where it stands: the keyboard is being
1680    /// started again, and a keyboard started again has forgotten it.
1681    #[cfg(android)]
1682    fn end_composition(&mut self) {
1683        if let Some(id) = self.runtime.input.focused {
1684            if let Some(edit) = self.runtime.edits.get_mut(&id) {
1685                edit.composing = None;
1686            }
1687        }
1688    }
1689
1690    /// Remembers the Android activity's handle, the source of the system insets.
1691    #[cfg(android)]
1692    pub(crate) fn set_android_app(
1693        &mut self,
1694        android_app: winit::platform::android::activity::AndroidApp,
1695    ) {
1696        self.android_app = Some(android_app);
1697    }
1698
1699    /// The system insets — the safe area — in **logical** px. On Android they are
1700    /// derived from the activity's content rect, outside the system bars; zero
1701    /// elsewhere.
1702    ///
1703    /// The content rect is what the system leaves the activity, so it excludes
1704    /// **any** decoration the theme asks for — a title bar included. An app whose
1705    /// manifest keeps the default theme therefore reports a top inset of the status
1706    /// bar *plus* 56dp of action bar that is never drawn, and the shell dutifully
1707    /// pads it away: a wide empty band above the app bar. The manifests here ask for
1708    /// `Theme.DeviceDefault.NoActionBar`, and so must any frus app.
1709    fn compute_insets(&self, phys_w: u32, phys_h: u32, scale: f32) -> Insets {
1710        #[cfg(android)]
1711        if let Some(app) = &self.android_app {
1712            let r = app.content_rect();
1713            // A degenerate rect, before the first layout, means no inset.
1714            if r.right > r.left && r.bottom > r.top {
1715                let left = r.left.max(0) as f32;
1716                let top = r.top.max(0) as f32;
1717                let right = (phys_w as i32 - r.right).max(0) as f32;
1718                let bottom = (phys_h as i32 - r.bottom).max(0) as f32;
1719                return Insets::new(top / scale, right / scale, bottom / scale, left / scale);
1720            }
1721        }
1722        let _ = (phys_w, phys_h, scale);
1723        Insets::ZERO
1724    }
1725
1726    /// Asks the platform what it says about its user, and remembers the answer.
1727    ///
1728    /// Marks the build dirty when something moved: the font size reaches the layout
1729    /// through [`MediaQuery::scope`], so a changed scaler is a changed geometry for every
1730    /// widget on the screen, and a frame drawn from the cache would keep the old one.
1731    fn refresh_platform_settings(&mut self) {
1732        #[cfg(android)]
1733        if let Some(app) = &self.android_app {
1734            let read = crate::android_settings::read(app);
1735            if read != self.platform {
1736                self.platform = read;
1737                self.build_dirty = true;
1738            }
1739            return;
1740        }
1741        // Desktop and web report the one thing their window system knows. A text scaler
1742        // is **not** among it: winit exposes no equivalent of Windows'
1743        // `UISettings.TextScaleFactor` or of GNOME's `text-scaling-factor`, so it stays
1744        // at 1 there and an application that knows better says so itself. That is a
1745        // missing wire and is written down rather than papered over.
1746        let brightness = self
1747            .window
1748            .as_ref()
1749            .and_then(|window| window.theme())
1750            .map(|theme| match theme {
1751                winit::window::Theme::Dark => frus_widgets::Brightness::Dark,
1752                winit::window::Theme::Light => frus_widgets::Brightness::Light,
1753            })
1754            .unwrap_or_default();
1755        if brightness != self.platform.brightness {
1756            self.platform.brightness = brightness;
1757            self.build_dirty = true;
1758        }
1759
1760        // **The reader's languages**, read once. Changing the display language on a
1761        // desktop means signing out and back in on Windows, and is a per-process
1762        // environment variable on Linux — so there is nothing to watch for, and re-reading
1763        // it every time the window comes back would allocate for an answer that cannot
1764        // have moved. Android is the platform where it *does* change under a running
1765        // application, and Android reads it on every walk.
1766        if self.platform.locales.is_empty() {
1767            let read = platform_locales();
1768            if !read.is_empty() {
1769                self.platform.locales = read;
1770                self.build_dirty = true;
1771            }
1772        }
1773    }
1774}
1775
1776/// **The reader's preferred languages**, best first, from whatever the platform keeps
1777/// them in. Empty when it says nothing, or when there is no way to ask.
1778///
1779/// Android does not come through here: its answer lives on the activity's
1780/// `Configuration`, which [`crate::android_settings`] already walks.
1781/// On **Android** there is nothing to ask here: the answer lives on the activity's
1782/// `Configuration`, and the branch above returns before reaching this. A build with no
1783/// activity at all has no reader to ask about.
1784#[cfg(android)]
1785fn platform_locales() -> Vec<frus_widgets::Locale> {
1786    Vec::new()
1787}
1788
1789#[cfg(not(android))]
1790fn platform_locales() -> Vec<frus_widgets::Locale> {
1791    #[cfg(not(web))]
1792    {
1793        sys_locale::get_locales()
1794            .filter_map(|tag| frus_widgets::Locale::parse(&tag))
1795            .collect()
1796    }
1797    #[cfg(web)]
1798    {
1799        // `navigator.languages` is the ordered list; `navigator.language` is its first
1800        // entry and the fallback for a browser that does not offer the list.
1801        let Some(navigator) = web_sys::window().map(|window| window.navigator()) else {
1802            return Vec::new();
1803        };
1804        let listed: Vec<frus_widgets::Locale> = navigator
1805            .languages()
1806            .iter()
1807            .filter_map(|value| value.as_string())
1808            .filter_map(|tag| frus_widgets::Locale::parse(&tag))
1809            .collect();
1810        if listed.is_empty() {
1811            return navigator
1812                .language()
1813                .and_then(|tag| frus_widgets::Locale::parse(&tag))
1814                .into_iter()
1815                .collect();
1816        }
1817        listed
1818    }
1819}
1820
1821/// Gives back the decoded images nobody is showing, and says how much went.
1822fn memory_trimmed() {
1823    let before = frus_widgets::image_cache_bytes();
1824    frus_widgets::trim_image_caches();
1825    log::info!(
1826        "memory is low: decoded network images trimmed from {} to {} KB",
1827        before / 1024,
1828        frus_widgets::image_cache_bytes() / 1024
1829    );
1830}
1831
1832impl<A: Application> ApplicationHandler<A::Message> for App<A> {
1833    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
1834        if self.window.is_some() {
1835            return;
1836        }
1837        // Back in the foreground, or starting up: the surface is (re)born below.
1838        self.set_lifecycle(Lifecycle::Resumed);
1839        // And so is what the platform says about its user — a trip to the system
1840        // settings and back is exactly this event.
1841        self.refresh_platform_settings();
1842
1843        let mut attributes = Window::default_attributes()
1844            .with_title(self.app.title())
1845            // A sensible minimum size, in logical px, to avoid an absurd UI.
1846            .with_min_inner_size(winit::dpi::LogicalSize::new(360.0, 280.0));
1847        // The AccessKit adapter must be created **before** the window is shown, so we
1848        // create it hidden and reveal it afterwards. Desktop only.
1849        #[cfg(desktop)]
1850        {
1851            attributes = attributes.with_visible(false);
1852        }
1853        // The application's icon, on the window (Windows and X11; Wayland and macOS take it
1854        // from the bundle and ignore this). Windows keeps a second, larger one for the taskbar
1855        // and Alt+Tab, set on its own.
1856        #[cfg(desktop)]
1857        {
1858            let icon = crate::icon::window_icon(&self.app.icon());
1859            #[cfg(windows)]
1860            {
1861                use winit::platform::windows::WindowAttributesExtWindows;
1862                attributes = attributes.with_taskbar_icon(icon.clone());
1863            }
1864            attributes = attributes.with_window_icon(icon);
1865        }
1866        // Not on the Web: there the page sizes the canvas, with its own CSS, and asking winit
1867        // for a size writes it **inline** on the canvas, where it outranks the stylesheet — an
1868        // application that named 900 by 680 then stayed 900 by 680 in a phone-sized window,
1869        // cropped, its drawer off the edge of what could be seen.
1870        #[cfg(not(web))]
1871        if let Some((w, h)) = self.app.window_size() {
1872            attributes = attributes.with_inner_size(winit::dpi::LogicalSize::new(w, h));
1873        }
1874        // Restarted by `frus run`: where the window was, as big as it was.
1875        #[cfg(desktop)]
1876        if let Some([x, y, w, h]) = crate::dev::restore_window() {
1877            attributes = attributes
1878                .with_position(winit::dpi::PhysicalPosition::new(x, y))
1879                .with_inner_size(winit::dpi::PhysicalSize::new(w as u32, h as u32));
1880        }
1881        // Web: winit creates a `<canvas>` and **appends it** to the document's body.
1882        #[cfg(web)]
1883        {
1884            use winit::platform::web::WindowAttributesExtWebSys;
1885            attributes = attributes.with_append(true);
1886        }
1887        let window = match event_loop.create_window(attributes) {
1888            Ok(window) => {
1889                crate::startup::window_made();
1890                Arc::new(window)
1891            }
1892            Err(err) => {
1893                log::error!("failed to create the window: {err}");
1894                event_loop.exit();
1895                return;
1896            }
1897        };
1898
1899        // Web: GPU init is **asynchronous**, blocking being impossible. We start the
1900        // future, and the ready renderer is picked up on the first frame — see
1901        // `RedrawRequested`. `init` runs right away, since it does not touch the GPU.
1902        #[cfg(web)]
1903        {
1904            self.scale = window.scale_factor() as f32;
1905            self.window = Some(window.clone());
1906            // The accessibility bridge (#18): the canvas exists as soon as the window
1907            // does, so there is no "still hidden" step to wait for here.
1908            self.a11y = crate::a11y_web::A11y::new(&window);
1909            // The browser keeps reload, the developer tools and the rest of its own shortcuts
1910            // while the canvas has the focus, instead of winit cancelling them.
1911            crate::web_keys::let_the_browser_keep_its_shortcuts();
1912            // The tab's icon: the application's, or the frus logo where the page has none.
1913            crate::web_icon::apply(&self.app.icon());
1914            self.build_dirty = true;
1915            if !self.started {
1916                self.started = true;
1917                let command = self.app.init();
1918                self.run_command(command);
1919                self.sync_subscriptions();
1920                self.open_launch_address(&window);
1921                crate::dev_commands::set_window(Some(window.clone()));
1922                if let Some(states) = crate::web_dev::start() {
1923                    self.runtime.states.restore_from(&states);
1924                }
1925                #[cfg(feature = "hot-reload")]
1926                {
1927                    crate::hot::set_window(window.clone());
1928                    crate::hot::connect();
1929                }
1930            }
1931            let slot = self.pending_renderer.clone();
1932            let win = window.clone();
1933            let size = window.inner_size();
1934            wasm_bindgen_futures::spawn_local(async move {
1935                match Renderer::new(win.clone(), size.width.max(1), size.height.max(1)).await {
1936                    Ok(r) => {
1937                        crate::startup::renderer_made(r.startup_times());
1938                        *slot.borrow_mut() = Some(r);
1939                        win.request_redraw();
1940                    }
1941                    Err(err) => log::error!("failed to initialise the renderer (Web): {err:#}"),
1942                }
1943            });
1944        }
1945
1946        #[cfg(not(web))]
1947        {
1948            let size = window.inner_size();
1949            let (width, height) = (size.width.max(1), size.height.max(1));
1950            // On Windows, a renderer whose frames let the system's own title bar show where
1951            // the window's menu bar leaves the line transparent (milestone 643); the
1952            // ordinary one where that cannot be had.
1953            let see_through = if cfg!(windows) {
1954                match pollster::block_on(Renderer::see_through(window.clone(), width, height)) {
1955                    Ok(renderer) => Some(renderer),
1956                    Err(err) => {
1957                        log::info!("title bar painted by frus: {err:#}");
1958                        None
1959                    }
1960                }
1961            } else {
1962                None
1963            };
1964            let renderer = match see_through {
1965                Some(renderer) => Ok(renderer),
1966                None => pollster::block_on(Renderer::new(window.clone(), width, height)),
1967            };
1968
1969            match renderer {
1970                Ok(renderer) => {
1971                    crate::startup::renderer_made(renderer.startup_times());
1972                    self.scale = window.scale_factor() as f32;
1973                    // The accessibility bridge (AccessKit), created while the window is
1974                    // still hidden, after which we reveal it. Inert with no screen reader.
1975                    #[cfg(desktop)]
1976                    {
1977                        self.a11y = Some(crate::a11y::A11y::new(event_loop, &window));
1978                        window.set_visible(true);
1979                        // A second process's link brings this window forward and asks it for
1980                        // a frame.
1981                        crate::instance::set_window(window.clone());
1982                    }
1983                    self.window = Some(window.clone());
1984                    // What the input bridge's wakes ask a frame of (milestone 513).
1985                    #[cfg(android)]
1986                    crate::android_ime::set_window(Some(window.clone()));
1987                    #[cfg(android)]
1988                    crate::dev_commands::set_window(Some(window.clone()));
1989                    self.renderer = Some(renderer);
1990                    // The surface was (re)created: force a full rebuild on the first frame.
1991                    self.build_dirty = true;
1992                    // The startup effect, an initial load and so on: once only, not on
1993                    // every surface recreation, such as returning from the background.
1994                    if !self.started {
1995                        self.started = true;
1996                        let command = self.app.init();
1997                        self.run_command(command);
1998                        self.sync_subscriptions();
1999                        #[cfg(any(android, desktop))]
2000                        self.open_launch_link();
2001                        #[cfg(any(android, desktop))]
2002                        self.connect_dev();
2003                        #[cfg(feature = "hot-reload")]
2004                        {
2005                            crate::hot::set_window(window.clone());
2006                            crate::hot::connect();
2007                        }
2008                    }
2009                    window.request_redraw();
2010                }
2011                Err(err) => {
2012                    log::error!("failed to initialise the renderer: {err:#}");
2013                    event_loop.exit();
2014                }
2015            }
2016        }
2017    }
2018
2019    /// Going to the background, on Android: the native surface is destroyed. We
2020    /// release the renderer and the window, and `resumed` recreates them on the way
2021    /// back, without replaying `init` — see `started`. Harmless on desktop, where the
2022    /// event never fires.
2023    fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
2024        // Background: the surface is lost, so the app moves to `Paused`.
2025        self.set_lifecycle(Lifecycle::Paused);
2026        // Under `frus run`, this is how a restart begins on Android: the states are kept.
2027        #[cfg(android)]
2028        if self.dev.is_some() {
2029            if let Some(storage) = self
2030                .android_app
2031                .as_ref()
2032                .and_then(|a| a.internal_data_path())
2033            {
2034                crate::dev::keep_states(&storage, &self.runtime.states.snapshot());
2035            }
2036        }
2037        self.renderer = None;
2038        self.window = None;
2039        #[cfg(android)]
2040        crate::android_ime::set_window(None);
2041        #[cfg(android)]
2042        crate::dev_commands::set_window(None);
2043        self.last_frame = None;
2044    }
2045
2046    /// The event loop is ending, on close: a final `Detached` notification, where the
2047    /// app can persist its state before the process disappears.
2048    fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
2049        self.set_lifecycle(Lifecycle::Detached);
2050    }
2051
2052    /// The system is short of memory (Android's low-memory callback, iOS's memory
2053    /// warning): the decoded images nobody is showing are given back, rather than left
2054    /// for the system to reclaim by ending the process.
2055    fn memory_warning(&mut self, _event_loop: &ActiveEventLoop) {
2056        memory_trimmed();
2057    }
2058
2059    /// A message produced by an effect, on a background thread: we apply it and ask
2060    /// for another frame.
2061    fn user_event(&mut self, _event_loop: &ActiveEventLoop, message: A::Message) {
2062        self.dispatch(message);
2063        self.request_redraw();
2064    }
2065
2066    /// The loop woke up: if the long-press deadline has been reached the recogniser
2067    /// **accepts eagerly** — the message is emitted and the release to come will be
2068    /// swallowed, the long press having evicted the tap.
2069    fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) {
2070        if matches!(cause, StartCause::ResumeTimeReached { .. }) && self.press.poll(Instant::now())
2071        {
2072            self.hold_deadline_reached();
2073        }
2074        if matches!(cause, StartCause::ResumeTimeReached { .. }) {
2075            self.poll_pending_tap(Instant::now());
2076        }
2077
2078        // The caret's turn is due: a frame to show it, or to hide it, in (milestone 513).
2079        if matches!(cause, StartCause::ResumeTimeReached { .. })
2080            && self.lifecycle == Lifecycle::Resumed
2081            && self.caret.next_toggle(Instant::now()).is_some()
2082        {
2083            self.request_redraw();
2084        }
2085
2086        // Pending IME operations, from the Android input bridge.
2087        #[cfg(android)]
2088        self.drain_ime();
2089        // And the values an autofill service chose, from the same bridge.
2090        #[cfg(android)]
2091        self.drain_autofill();
2092
2093        // Actions an assistive technology asked for, through AccessKit.
2094        #[cfg(desktop)]
2095        self.drain_a11y_actions();
2096        // Actions a screen reader asked for, through the DOM bridge (#18).
2097        #[cfg(web)]
2098        self.drain_a11y_actions();
2099
2100        // Live reload, in development: the binary was replaced by a recompilation, so
2101        // capture the state and relaunch the new binary. This does not return.
2102        if let Some(watcher) = self.reload.as_mut() {
2103            if watcher.binary_changed() {
2104                watcher.handoff(self.app.save_state());
2105            }
2106        }
2107        event_loop.set_control_flow(self.idle_control_flow());
2108    }
2109
2110    fn window_event(
2111        &mut self,
2112        event_loop: &ActiveEventLoop,
2113        _window_id: WindowId,
2114        event: WindowEvent,
2115    ) {
2116        // The AccessKit adapter observes the window events: focus, size and so on.
2117        #[cfg(desktop)]
2118        if let (Some(a11y), Some(window)) = (self.a11y.as_mut(), self.window.as_ref()) {
2119            a11y.process_event(window, &event);
2120        }
2121
2122        match event {
2123            WindowEvent::CloseRequested => event_loop.exit(),
2124
2125            // Gaining or losing focus **in the foreground**: `Resumed` ⇄ `Inactive`. We
2126            // leave `Paused`/`Detached` — background and closing — alone; those are
2127            // decided by `suspended` and `exiting`.
2128            WindowEvent::Focused(focused) => {
2129                if !matches!(self.lifecycle, Lifecycle::Paused | Lifecycle::Detached) {
2130                    self.set_lifecycle(if focused {
2131                        Lifecycle::Resumed
2132                    } else {
2133                        Lifecycle::Inactive
2134                    });
2135                }
2136                // The line the window shares with the system quietens with it, as the
2137                // system's own caption does: a frame reads the window's state again
2138                // (milestone 643).
2139                #[cfg(windows)]
2140                if self.title_bar_on {
2141                    self.request_redraw();
2142                }
2143            }
2144
2145            WindowEvent::Resized(size) => {
2146                if let Some(renderer) = self.renderer.as_mut() {
2147                    renderer.resize(size.width, size.height);
2148                }
2149                if let Some(window) = &self.window {
2150                    window.request_redraw();
2151                }
2152            }
2153
2154            WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
2155                self.scale = scale_factor as f32;
2156                // Reconfigure the surface to the current physical size.
2157                if let Some(window) = &self.window {
2158                    let size = window.inner_size();
2159                    if let Some(renderer) = self.renderer.as_mut() {
2160                        renderer.resize(size.width, size.height);
2161                    }
2162                }
2163                self.request_redraw();
2164            }
2165
2166            WindowEvent::Occluded(occluded) => {
2167                self.occluded = occluded;
2168                if !occluded {
2169                    self.request_redraw();
2170                }
2171            }
2172
2173            // Every pointer source, mouse and touch alike, converges on the
2174            // **normalised** `pointer()` input — gesture tier 0, with an explicit
2175            // `Cancel`. winit hands us physical px; we work in logical ones, the total
2176            // scale being DPI × density.
2177            WindowEvent::CursorMoved { position, .. } => {
2178                let scale = self.total_scale();
2179                let position = Point::new(position.x as f32 / scale, position.y as f32 / scale);
2180                self.pointer(
2181                    event_loop,
2182                    PointerEvent {
2183                        kind: PointerKind::Move,
2184                        position,
2185                        touch: false,
2186                    },
2187                );
2188            }
2189
2190            // The mouse left the window: nothing in it is under the pointer any more.
2191            WindowEvent::CursorLeft { .. } => {
2192                self.hover.left();
2193                self.sync_hover();
2194                self.sync_regions(false);
2195            }
2196
2197            WindowEvent::Touch(touch) => {
2198                let scale = self.total_scale();
2199                let position = Point::new(
2200                    touch.location.x as f32 / scale,
2201                    touch.location.y as f32 / scale,
2202                );
2203                let kind = match touch.phase {
2204                    TouchPhase::Started => PointerKind::Down,
2205                    TouchPhase::Moved => PointerKind::Move,
2206                    TouchPhase::Ended => PointerKind::Up,
2207                    TouchPhase::Cancelled => PointerKind::Cancel,
2208                };
2209                self.pointer(
2210                    event_loop,
2211                    PointerEvent {
2212                        kind,
2213                        position,
2214                        touch: true,
2215                    },
2216                );
2217            }
2218
2219            WindowEvent::ModifiersChanged(modifiers) => {
2220                let state = modifiers.state();
2221                self.shift = state.shift_key();
2222                self.ctrl = state.control_key();
2223                self.alt = state.alt_key();
2224                self.meta = state.super_key();
2225            }
2226
2227            WindowEvent::MouseInput {
2228                state: ElementState::Pressed,
2229                button: MouseButton::Left,
2230                ..
2231            } => self.pointer(
2232                event_loop,
2233                PointerEvent {
2234                    kind: PointerKind::Down,
2235                    position: self.cursor,
2236                    touch: false,
2237                },
2238            ),
2239
2240            WindowEvent::MouseInput {
2241                state: ElementState::Released,
2242                button: MouseButton::Left,
2243                ..
2244            } => self.pointer(
2245                event_loop,
2246                PointerEvent {
2247                    kind: PointerKind::Up,
2248                    position: self.cursor,
2249                    touch: false,
2250                },
2251            ),
2252
2253            // A right-click on a text field opens its selection bar (milestone 568), which is
2254            // the desktop's context menu for the four things it holds.
2255            WindowEvent::MouseInput {
2256                state: ElementState::Pressed,
2257                button: MouseButton::Right,
2258                ..
2259            } => self.context_click(),
2260
2261            WindowEvent::KeyboardInput { event, .. } if event.state == ElementState::Pressed => {
2262                self.key_down(&KeyDown::from(&event), Some(event_loop));
2263            }
2264
2265            WindowEvent::MouseWheel { delta, .. } => {
2266                let (mut dx, mut dy) = match delta {
2267                    MouseScrollDelta::LineDelta(x, y) => (x * SCROLL_SPEED, y * SCROLL_SPEED),
2268                    // Physical delta → logical.
2269                    MouseScrollDelta::PixelDelta(pos) => {
2270                        let scale = self.total_scale();
2271                        (pos.x as f32 / scale, pos.y as f32 / scale)
2272                    }
2273                };
2274                // With Shift, the wheel scrolls horizontally.
2275                if self.shift {
2276                    dx = dy;
2277                    dy = 0.0;
2278                }
2279                // Over an interactive viewport the wheel **zooms**, anchored at the
2280                // pointer and bounded by the widget's min and max scales.
2281                if let Some((id, viewport)) = self
2282                    .ui
2283                    .as_ref()
2284                    .and_then(|ui| ui.interactive_at(self.cursor))
2285                {
2286                    let (min, max) = self
2287                        .tree
2288                        .as_ref()
2289                        .and_then(|tree| find_widget(tree.as_ref(), id))
2290                        .and_then(|w| w.interactive())
2291                        .unwrap_or((0.5, 4.0));
2292                    // Wheel up (`dy > 0`) zooms in, in gentle steps of about 1.1× a notch.
2293                    let factor = (1.0 + dy * 0.1 / SCROLL_SPEED).clamp(0.2, 5.0);
2294                    // Zooming cuts off a fling in progress.
2295                    self.runtime.interactive_velocity.remove(&id);
2296                    let view = self.runtime.interactive.entry(id).or_default();
2297                    // Zoom anchored at the pointer, then bounded to the frame.
2298                    *view = view
2299                        .zoom_at(factor, self.cursor, min, max)
2300                        .clamped(viewport);
2301                    self.request_redraw();
2302                    return;
2303                }
2304                // A wheel asks the same question a finger does, and the reference gates it
2305                // the same way: an area with everything already in sight takes no offset,
2306                // and the notch belongs to whatever is behind it.
2307                let hit = {
2308                    let scroll = &self.runtime.scroll;
2309                    self.ui.as_ref().and_then(|ui| {
2310                        ui.scroll_chain(self.cursor).find(|area| {
2311                            area.accepts_user_offset(
2312                                scroll.get(&area.id).copied().unwrap_or((0.0, 0.0)),
2313                            )
2314                        })
2315                    })
2316                };
2317                if let Some(area) = hit {
2318                    // Scrolling with inertia: the wheel pushes the TARGET and the
2319                    // spring eases across to it. How far past the ends that target
2320                    // may go is the physics' call — a little elastic overshoot where
2321                    // the platform bounces, none at all where it does not.
2322                    let id = area.id;
2323                    let physics = area.physics;
2324                    let over = if physics.allows_overscroll() {
2325                        SCROLL_OVER
2326                    } else {
2327                        0.0
2328                    };
2329                    // A notch of the wheel is a new intent: it takes over from the
2330                    // momentum of the last gesture.
2331                    self.runtime.stop_scroll_fling(id);
2332                    let current = self.runtime.scroll.get(&id).copied().unwrap_or((0.0, 0.0));
2333                    // A screen delta into offsets: the content moves opposite the number,
2334                    // and a reversed axis counts from the other end. One function, so a
2335                    // reversed area is right at every one of the places this happens.
2336                    let (ddx, ddy) = area.offset_delta((dx, dy));
2337                    let target = self.runtime.scroll_target.entry(id).or_insert(current);
2338                    let (wanted_x, wanted_y) = (target.0 + ddx, target.1 + ddy);
2339                    target.0 = wanted_x.clamp(-over, area.max_x + over);
2340                    target.1 = wanted_y.clamp(-over, area.max_y + over);
2341                    let refused = (wanted_x - target.0, wanted_y - target.1);
2342                    self.runtime.scroll_velocity.entry(id).or_insert((0.0, 0.0));
2343                    // A notch that asks to go past the end deserves the same
2344                    // acknowledgement a finger gets: the wheel is a gesture too.
2345                    let cursor = self.cursor;
2346                    for (refused, vertical, extent) in [
2347                        (refused.0, false, area.viewport.width),
2348                        (refused.1, true, area.viewport.height),
2349                    ] {
2350                        if refused.abs() < 1e-3 {
2351                            continue;
2352                        }
2353                        let edge = area.refused_edge(vertical, refused);
2354                        let (offset, cross) =
2355                            frus_widgets::glow_cross_axis(area.viewport, edge, cursor);
2356                        self.runtime.glow_pull(
2357                            id,
2358                            area.overscroll,
2359                            edge,
2360                            refused,
2361                            extent,
2362                            offset,
2363                            cross,
2364                        );
2365                        // A wheel has no "lift off", so the pull is released at once
2366                        // and simply fades — otherwise it would hang until the hold
2367                        // timer expired.
2368                        self.runtime.glow_scroll_end(id);
2369                    }
2370                    self.request_redraw();
2371                }
2372            }
2373
2374            WindowEvent::RedrawRequested => {
2375                // The GPU device was lost on the last frame: a new renderer first.
2376                if std::mem::take(&mut self.renderer_lost) {
2377                    self.rebuild_renderer();
2378                }
2379                // Measured only when asked: a clock read per stage is cheap, but not free.
2380                let measuring = self.frame_stats.enabled();
2381                let frame_start = measuring.then(Instant::now);
2382                self.frame_costs = crate::frame_stats::FrameCosts::default();
2383                // Web: pick up the asynchronously initialised renderer as soon as it
2384                // is ready; until then, nothing is painted.
2385                #[cfg(web)]
2386                if self.renderer.is_none() {
2387                    match self.pending_renderer.borrow_mut().take() {
2388                        Some(mut renderer) => {
2389                            // The renderer was sized from whatever `inner_size()` read
2390                            // back when it was spawned — often 0×0, before the canvas
2391                            // had been laid out by CSS. A `Resized` event carrying the
2392                            // real size may have arrived and been dropped in the
2393                            // meantime, since `self.renderer` was still `None` then.
2394                            // Catching up here, against the size right now, is what
2395                            // makes that race harmless either way.
2396                            if let Some(window) = &self.window {
2397                                let size = window.inner_size();
2398                                renderer.resize(size.width.max(1), size.height.max(1));
2399                            }
2400                            self.renderer = Some(renderer);
2401                        }
2402                        None => return,
2403                    }
2404                }
2405                // A paste the browser has answered since the last frame goes in before
2406                // this one is built. Elsewhere a paste is answered on the spot and this
2407                // finds nothing.
2408                while let Some(pasted) = self.clipboard.take_answered() {
2409                    self.land_paste(pasted);
2410                }
2411                // The widgets hear where the focus went since the last frame, before this
2412                // one is built from what they answer (milestone 660).
2413                self.report_focus();
2414                // A bar whose field is no longer the focused one is over: a field focused
2415                // again later must not find it open.
2416                if self
2417                    .runtime
2418                    .selection_toolbar
2419                    .is_some_and(|mark| self.runtime.input.focused != Some(mark.id))
2420                {
2421                    self.runtime.selection_toolbar = None;
2422                }
2423                // Occluded window: rendering is suspended, resuming on Occluded(false).
2424                if self.occluded {
2425                    return;
2426                }
2427                let size = self
2428                    .window
2429                    .as_ref()
2430                    .map(|w| w.inner_size())
2431                    .unwrap_or_default();
2432                // Minimised, or zero-sized: nothing to draw, which avoids GPU errors.
2433                if size.width == 0 || size.height == 0 {
2434                    self.last_frame = None; // no dt jump when it is restored
2435                    return;
2436                }
2437                // The interface is described in **logical** pixels; the GPU output is
2438                // scaled to physical ones (DPI × density) just before rendering.
2439                let scale = self.total_scale();
2440                let (width, height) = (size.width as f32 / scale, size.height as f32 / scale);
2441
2442                // A logical size change — a resize OR a density change — notifies the
2443                // app before the view, so it can react to the breakpoint in its logic.
2444                if self.last_size != Some((width, height)) {
2445                    self.last_size = Some((width, height));
2446                    self.build_dirty = true;
2447                    self.app.on_resize(width, height);
2448                }
2449
2450                // Window insets: separates the static **padding** — bars and notch —
2451                // from the **keyboard**, which is whatever bottom inset exceeds the
2452                // keyboard-free baseline. The baseline is taken at the first measurement
2453                // for this physical size (a rotation gives a new one), and corrects
2454                // downwards should a barer state appear: the keyboard open at startup,
2455                // the bars hidden, and so on.
2456                let raw = self.compute_insets(size.width, size.height, scale);
2457                let phys = (size.width, size.height);
2458                let mut baseline = match self.inset_baseline {
2459                    Some((b, s)) if s == phys => b,
2460                    _ => {
2461                        self.inset_baseline = Some((raw, phys));
2462                        raw
2463                    }
2464                };
2465                if raw.bottom < baseline.bottom {
2466                    baseline = raw;
2467                    self.inset_baseline = Some((raw, phys));
2468                }
2469                let insets = WindowInsets::from_baseline(baseline, raw);
2470                if self.last_insets != insets {
2471                    if insets.view_insets.bottom > self.last_insets.view_insets.bottom {
2472                        self.reveal_after_keyboard = true;
2473                    }
2474                    self.last_insets = insets;
2475                    self.build_dirty = true;
2476                    self.app.on_insets(insets);
2477                }
2478
2479                // The elapsed dt, clamped, for every animation.
2480                let now = Instant::now();
2481                let dt = self
2482                    .last_frame
2483                    .map(|prev| (now - prev).as_secs_f32().min(0.05))
2484                    .unwrap_or(0.0);
2485                self.last_frame = Some(now);
2486
2487                // A continuous clock, in seconds, for the time-driven animations.
2488                self.elapsed += dt;
2489                self.runtime.time = self.elapsed;
2490
2491                // The caret's blink (milestone 513), on the wall clock: the animation
2492                // clock above is clamped per frame and stands still between frames, and a
2493                // caret at rest is exactly when there are none. Solid while the window is
2494                // not the one in front, where nothing wakes the loop for it.
2495                let caret_now = Instant::now();
2496                let caret = if self.lifecycle == Lifecycle::Resumed {
2497                    self.caret_signature()
2498                } else {
2499                    None
2500                };
2501                self.caret.observe(caret_now, caret);
2502                self.runtime.caret_hidden = self.caret.hidden(caret_now);
2503
2504                // The application advances its own animations: navigation, gesture.
2505                let mut app_animating = self.app.tick(dt);
2506
2507                // And the framework advances the **theme**, which is its own work: it
2508                // resolves the application's themes against the brightness the platform
2509                // reports and the contrast the reader asked for, and crosses from the one
2510                // on screen to the one now wanted over `theme_animation_duration`.
2511                //
2512                // Both halves of that were the application's before milestone 452, and
2513                // both were being done by hand in this repo's own demonstration — which
2514                // is a fair sign of what every application was having to write.
2515                let settings = self
2516                    .platform
2517                    .accessibility
2518                    .with_overrides(self.app.accessibility());
2519
2520                // **The ambient scopes, before anything reads them** — which the theme
2521                // does: the direction of the layout follows the language, so an
2522                // application whose theme is right-to-left in Arabic needs the language
2523                // installed before its theme is asked for. This used to sit further down,
2524                // beside the build, where it was in time for the widgets and one frame
2525                // late for the theme.
2526                //
2527                // Read every frame, all of it: these are the application's answers and
2528                // they may change while it is running.
2529                install_ambient(&self.app, &self.platform.locales);
2530
2531                let theme_moved = self.themes.advance(
2532                    &self.app,
2533                    self.brightness(),
2534                    settings.high_contrast,
2535                    settings.disable_animations,
2536                    dt,
2537                );
2538                // A theme that moved is a rebuild — the view is a pure function of
2539                // `(state, theme, size)` — and a fade still running is another frame.
2540                app_animating |= theme_moved | self.themes.animating();
2541                let theme = self.themes.displayed(&self.app);
2542
2543                // **The surface, in force for the whole frame** — not just for `view`.
2544                // A size becomes a number in three places: while the widgets are built,
2545                // while they are measured and laid out, and while they are painted.
2546                // Scoping the build alone left the last two at scale 1, so the layout
2547                // measured one size and the renderer drew another, and the reader's
2548                // setting reached a device without moving a single pixel (milestone 407).
2549                //
2550                // One guard, not two: the description and the font size are installed by
2551                // the same call and released by the same drop, so they cannot be held for
2552                // different lengths of time. Holding them separately is the same bug with
2553                // an extra step (milestone 408). It lives to the end of this frame and
2554                // puts back what was there, panic or not.
2555                let _surface = self.media_query(width, height).install();
2556
2557                // === BUILD phase, conditional ===
2558                // The `view` is rebuilt only when the app's state or the size changed
2559                // (`build_dirty`), or when the app is animating, theme, navigation and
2560                // gesture all altering the state the view reads. A frame that animates
2561                // interaction alone — hover, scroll, focus, caret — **reuses the
2562                // retained tree** and merely repaints: the `view` is a pure function of
2563                // `(state, theme, size)` and never of the `Runtime`.
2564                // The user's motion setting reaches the runtime before anything is
2565                // advanced with it, and it is read every frame: a settings screen with a
2566                // *reduce motion* switch changes it while the application is running.
2567                self.runtime.still = settings.disable_animations;
2568                // And where the pointer stands in relation to a bar, from the previous
2569                // frame's registry — the only one there is at this point in the frame.
2570                //
2571                // Near a bar, it holds it open and warms the thumb; a bar that has faded
2572                // out entirely still answers here, and comes back for the reach. On a
2573                // touch platform nothing draws a bar in the first place, so the registry
2574                // is empty and a lingering finger position says nothing.
2575                self.runtime.scrollbar_hovered = self
2576                    .ui
2577                    .as_ref()
2578                    .and_then(|ui| ui.scrollbar_near(self.cursor))
2579                    .map(|bar| bar.id);
2580                self.runtime.scrollbar_dragged = match self.drag {
2581                    Some(Drag::Scrollbar { id, .. }) => Some(id),
2582                    _ => None,
2583                };
2584                // A switcher in flight carries each child's progress as a number built into
2585                // the tree, so it is rebuilt — not only repainted — until the switch settles.
2586                // Something a component keeps changed — a `set_state`, a hook, a controller —
2587                // and what it builds is built again.
2588                // What components asked of the shell since the last frame — a focus, a scroll,
2589                // work for another thread — is run first: it may ask for this very rebuild.
2590                let asked = self.app.effects();
2591                self.run_command(asked);
2592                self.take_dev_commands();
2593                #[cfg(web)]
2594                self.sync_address();
2595                #[cfg(any(desktop, android))]
2596                self.report_dev();
2597                #[cfg(desktop)]
2598                self.open_links_from_other_instances();
2599                // Work a component started brought something: build again to show it.
2600                let arrived = self.take_task_arrivals();
2601                // A hot-reload patch changed what the components build.
2602                #[cfg(feature = "hot-reload")]
2603                let arrived = crate::hot::take_patched() || arrived;
2604                if frus_widgets::take_rebuild_request() || arrived {
2605                    self.build_dirty = true;
2606                }
2607                let need_build = frame_needs_build(
2608                    self.build_dirty,
2609                    app_animating,
2610                    std::mem::replace(&mut self.app_was_animating, app_animating),
2611                    self.tree.is_none() || self.runtime.switching(),
2612                );
2613                if need_build {
2614                    // No scope of its own: the surface above is already installed, and
2615                    // covers the layout and the paint that follow as well.
2616                    //
2617                    // `build_view`, not `view`: `collect_ids` below reads this tree before
2618                    // the layout pass has been down it, so an unprepared tree would report
2619                    // no identities at all inside a deferred subtree — and everything in an
2620                    // `AppBar` would silently never mount, never fade in and never fade out.
2621                    let build_start = measuring.then(Instant::now);
2622                    let tree = build_view(&self.app, &theme, &self.runtime);
2623                    if let Some(at) = build_start {
2624                        self.frame_costs.build = Some(at.elapsed().as_secs_f32() * 1000.0);
2625                    }
2626                    let ids = collect_ids(tree.as_ref());
2627                    let present: std::collections::HashSet<_> = ids.iter().copied().collect();
2628
2629                    // Leaving: snapshot the widgets present at N-1 but absent at N,
2630                    // so they can be faded out.
2631                    let leaving: std::collections::HashSet<u64> = self
2632                        .runtime
2633                        .mounted
2634                        .iter()
2635                        .filter(|id| !present.contains(id))
2636                        .map(|id| id.as_u64())
2637                        .collect();
2638                    if !leaving.is_empty() {
2639                        if let Some(ui) = &self.ui {
2640                            let captured: Vec<_> = ui
2641                                .scene()
2642                                .primitives()
2643                                .iter()
2644                                .filter(|p| leaving.contains(&p.owner()))
2645                                .cloned()
2646                                .collect();
2647                            if !captured.is_empty() {
2648                                self.runtime
2649                                    .leaving
2650                                    .insert(self.leaving_counter, (captured, 1.0));
2651                                self.leaving_counter = self.leaving_counter.wrapping_add(1);
2652                            }
2653                        }
2654                    }
2655
2656                    // Mounting: new widgets start out fading in.
2657                    for &id in &ids {
2658                        if self.runtime.mounted.insert(id) {
2659                            self.runtime.anims.entry(id).or_default().opacity = 0.0;
2660                        }
2661                    }
2662                    self.runtime.mounted.retain(|id| present.contains(id));
2663
2664                    self.tree = Some(tree);
2665                    // What the build asked for may be a timer — a component's
2666                    // `use_interval` — and it is the build that says so, which is later than
2667                    // the message that changed it. Diffing here starts and stops it now, not
2668                    // on the next message.
2669                    self.sync_subscriptions();
2670
2671                    // The configuration may have changed, so invalidate the paint
2672                    // cache and its repaint boundaries. Entries from a stale generation
2673                    // no longer *hit*, giving a full repaint this frame and reuse on the
2674                    // interaction-only frames that follow.
2675                    self.runtime.paint_cache.borrow_mut().bump_generation();
2676                }
2677                self.build_dirty = false;
2678
2679                // Focus requests (`Command::focus`), resolved against the tree just
2680                // (re)built — the key names the field wrapped in `keyed(k, …)`. The most
2681                // recent one that resolves wins, and the focus ring becomes visible
2682                // again, since we jump to the field as the keyboard would.
2683                if !self.pending_focus.is_empty() {
2684                    let keys = std::mem::take(&mut self.pending_focus);
2685                    let ids: Vec<WidgetId> = self
2686                        .tree
2687                        .as_deref()
2688                        .map(|tree| keys.iter().filter_map(|&k| find_by_key(tree, k)).collect())
2689                        .unwrap_or_default();
2690                    if let Some(&id) = ids.last() {
2691                        self.runtime.input.focused = Some(id);
2692                        self.runtime.focus_visible = true;
2693                    }
2694                }
2695
2696                // === PAINT phase ===
2697                // The retained tree is (re)painted. Layout goes through the relayout
2698                // cache (milestone 55: taffy is called again only when the structure
2699                // changed); painting goes through the repaint cache (milestone 88: a
2700                // A row carried past the end of the list: the list comes to meet it.
2701                // Before the tree is borrowed for the rest of the frame, because it moves
2702                // an offset the build below is about to read — and this frame, not the
2703                // next one, or the content would lag a frame behind the finger.
2704                let autoscrolling = self.autoscroll_carried(dt);
2705
2706                // The reorder spring, per axis, with a time constant of about 70 ms:
2707                // - **horizontal** (`Table` columns): the smoothed `reorder_x` catches up
2708                //   with the pointer — the columns slide with inertia while the ghost
2709                //   sticks to the real pointer;
2710                // - **vertical** (Kanban cards): the smoothed `reorder_y` catches up with
2711                //   the **chosen** slot edge, the hovered half — the insertion line and
2712                //   the gap *slide* between cards instead of jumping, which is the
2713                //   vertical counterpart of `reorder_x`;
2714                // - **a horizontal list's rows**: `reorder_x` catches up with the chosen
2715                //   slot edge, the same line turned on its side.
2716                let reorder_animating = self.advance_reorder_springs(dt);
2717
2718                // static `RepaintBoundary` subtree is replayed without repainting while
2719                // its geometry and the interaction state hold still).
2720                let tree = self
2721                    .tree
2722                    .as_deref()
2723                    .expect("the view was built at least once");
2724
2725                // Scroll and pan inertia; the bounds and viewports come from the
2726                // previous frame.
2727                let scroll_regions = self
2728                    .ui
2729                    .as_ref()
2730                    .map(|ui| ui.scroll_regions().to_vec())
2731                    .unwrap_or_default();
2732                // The refresh areas of the frame, with the `refreshing` flag each was
2733                // built with: that flag is what tells a spinning indicator when to stop.
2734                let refresh_areas = self
2735                    .ui
2736                    .as_ref()
2737                    .map(|ui| ui.refresh_areas().to_vec())
2738                    .unwrap_or_default();
2739                let dismissables = self
2740                    .ui
2741                    .as_ref()
2742                    .map(|ui| ui.dismissables().to_vec())
2743                    .unwrap_or_default();
2744                // Filled by the dismissal step below, dispatched once the tree is no
2745                // longer borrowed.
2746                let mut dismissed: Vec<A::Message> = Vec::new();
2747                let sheet_areas = self
2748                    .ui
2749                    .as_ref()
2750                    .map(|ui| ui.sheets().to_vec())
2751                    .unwrap_or_default();
2752                let interactive_bounds = self
2753                    .ui
2754                    .as_ref()
2755                    .map(|ui| ui.interactive_bounds())
2756                    .unwrap_or_default();
2757
2758                // A paged view that has been asked for another page glides across to
2759                // it. Done before the springs are stepped, so the request is honoured
2760                // in the frame it arrives rather than the one after.
2761                self.runtime.sync_pages(&scroll_regions);
2762                // And a bar whose selected item has moved out of its window slides back
2763                // to it. Same moment, same reason: the request is honoured in the frame
2764                // it arrives rather than the one after.
2765                self.runtime.sync_visible(&scroll_regions);
2766                // And the scroll requests an application has just returned. Same moment
2767                // and the same reason, with one addition: a region that has only just
2768                // appeared is not in the registry above, which was built last frame, so
2769                // what does not resolve here is tried again below against this frame's.
2770                let requests = std::mem::take(&mut self.pending_scroll);
2771                let (retry, scrolled) =
2772                    apply_scroll_requests(&mut self.runtime, tree, requests, &scroll_regions);
2773                self.retry_scroll = retry;
2774                // And the sheets it has asked to move, on the same terms: before the sheets
2775                // are stepped, so an animated request starts in the frame it arrives in.
2776                let requests = std::mem::take(&mut self.pending_sheet);
2777                let (retry, sheeted) =
2778                    apply_sheet_requests(&mut self.runtime, tree, requests, &sheet_areas);
2779                self.retry_sheet = retry;
2780
2781                // Every family is stepped, in this order, whatever an earlier one answered; each
2782                // answer is kept by name, so that frame statistics can say which kept the frames
2783                // coming (milestone 609).
2784                let moving: [(&str, bool); 25] = [
2785                    ("scrolled", scrolled),
2786                    ("sheeted", sheeted),
2787                    ("anims", self.runtime.advance(dt)),
2788                    ("leaving", self.runtime.advance_leaving(dt)),
2789                    ("switchers", self.runtime.advance_switchers(tree, dt)),
2790                    ("values", self.runtime.advance_values(tree, dt)),
2791                    ("colors", self.runtime.advance_colors(tree, dt)),
2792                    ("sizes", self.runtime.advance_sizes(tree, dt)),
2793                    ("radii", self.runtime.advance_radii(tree, dt)),
2794                    ("paddings", self.runtime.advance_paddings(tree, dt)),
2795                    ("offsets", self.runtime.advance_offsets(tree, dt)),
2796                    ("pins", self.runtime.advance_pins(tree, dt)),
2797                    ("fractions", self.runtime.advance_fractions(tree, dt)),
2798                    ("text_styles", self.runtime.advance_text_styles(tree, dt)),
2799                    ("transforms", self.runtime.advance_transforms(tree, dt)),
2800                    ("scroll", self.runtime.advance_scroll(&scroll_regions, dt)),
2801                    ("glow", self.runtime.advance_glow(dt)),
2802                    ("refresh", self.runtime.advance_refresh(&refresh_areas, dt)),
2803                    ("dismiss", {
2804                        // A dismissed item announces itself only once its gap has
2805                        // finished closing. The messages are *collected* here and
2806                        // dispatched below: the retained tree is borrowed for the whole
2807                        // of this frame, and `dispatch` rebuilds it.
2808                        let (moving, done) = self.runtime.advance_dismiss(&dismissables, dt);
2809                        dismissed.extend(done.into_iter().filter_map(|(id, direction)| {
2810                            find_widget(tree, id).and_then(|widget| widget.on_dismissed(direction))
2811                        }));
2812                        moving
2813                    }),
2814                    ("sheets", {
2815                        // A sheet lowered to nothing says so once it has arrived there,
2816                        // collected like a dismissal and for the same reason.
2817                        let (moving, closed) = self.runtime.advance_sheets(&sheet_areas, dt);
2818                        dismissed.extend(closed.into_iter().filter_map(|id| {
2819                            find_widget(tree, id).and_then(|widget| widget.on_sheet_dismissed())
2820                        }));
2821                        // A throw that carried a sheet to full height goes on into its
2822                        // list, under the list's own physics (milestone 520).
2823                        let mut handed = false;
2824                        for (list, velocity) in self.runtime.take_sheet_handovers() {
2825                            if let Some(area) = scroll_regions.iter().find(|a| a.id == list) {
2826                                handed |=
2827                                    self.runtime
2828                                        .fling_scroll(*area, area.physics, (0.0, velocity));
2829                            }
2830                        }
2831                        moving | handed
2832                    }),
2833                    (
2834                        "interactive",
2835                        self.runtime.advance_interactive(&interactive_bounds, dt),
2836                    ),
2837                    ("ink", self.runtime.advance_ink(dt)),
2838                    ("reorder", reorder_animating),
2839                    ("autoscroll", autoscrolling),
2840                    ("app", app_animating),
2841                ];
2842                let animating = moving.iter().any(|(_, on)| *on);
2843                self.frame_stats.note_moving(&moving);
2844                // With the inspector on, the same build collects the observed nodes,
2845                // and the overlay — outlines plus a card for the hovered widget — is
2846                // painted on top of a copy of the scene.
2847                let layout_start = measuring.then(Instant::now);
2848                // So do debug paint (outlines alone, no card) and the dumps `frus run` asks
2849                // for (milestone 668).
2850                let inspected = self.inspector
2851                    || self.debug.outlines
2852                    || self.inspector_dump
2853                    || self.debug.focus_dump;
2854                let (ui, scene) = if inspected {
2855                    let (ui, nodes) = frus_widgets::build_ui_inspected(
2856                        tree,
2857                        Size::new(width, height),
2858                        &self.runtime,
2859                        &theme,
2860                    );
2861                    if std::mem::take(&mut self.inspector_dump) {
2862                        // Through `log`, so it reaches logcat and the browser too.
2863                        log::info!(target: "frus::inspector", "
2864{}", frus_widgets::dump_tree(&nodes));
2865                    }
2866                    if std::mem::take(&mut self.debug.focus_dump) {
2867                        let focusable: Vec<WidgetId> = ui.focusable_ids().collect();
2868                        log::info!(target: "frus::inspector", "focus, in tab order:
2869{}", dump_focus(&nodes, &focusable, self.runtime.input.focused));
2870                    }
2871                    let mut scene = if self.inspector {
2872                        // The card goes over the application's text, which a scene draws after
2873                        // its shapes: the application's frame as one layer, under it.
2874                        on_top_of(ui.scene())
2875                    } else {
2876                        ui.scene().clone()
2877                    };
2878                    if self.inspector || self.debug.outlines {
2879                        frus_widgets::paint_inspector_overlay(
2880                            &nodes,
2881                            self.inspector.then_some(self.cursor),
2882                            Size::new(width, height),
2883                            &theme,
2884                            &mut scene,
2885                        );
2886                    }
2887                    // Scene: logical → physical (DPI × density).
2888                    (ui, scene.scaled(scale))
2889                } else {
2890                    let ui = build_ui(tree, Size::new(width, height), &self.runtime, &theme);
2891                    // The preview of a column reorder, or a lifted item, on top of the
2892                    // scene.
2893                    let scene = if matches!(self.drag, Some(Drag::Reorder { moved: true, .. })) {
2894                        let mut scene = ui.scene().clone();
2895                        self.paint_reorder_preview(&ui, &theme, &mut scene);
2896                        scene.scaled(scale)
2897                    } else if matches!(self.drag, Some(Drag::Item { moved: true, .. })) {
2898                        let mut scene = ui.scene().clone();
2899                        self.paint_drag_ghost(&ui, &theme, &mut scene);
2900                        scene.scaled(scale)
2901                    } else {
2902                        // Scene: logical → physical (DPI × density), for a crisp render.
2903                        ui.scene().scaled(scale)
2904                    };
2905                    (ui, scene)
2906                };
2907                if let Some(at) = layout_start {
2908                    self.frame_costs.layout_paint = at.elapsed().as_secs_f32() * 1000.0;
2909                    self.frame_costs.layers = count_layers(scene.primitives());
2910                }
2911                self.report_overflows(&ui);
2912                // Laid out: the components the build no longer reached are let go, and the
2913                // effects it asked for run. An effect that changed something asks for the
2914                // frame that shows it.
2915                self.runtime.states.end_frame();
2916                crate::startup::laid_out();
2917                #[cfg(web)]
2918                crate::web_dev::keep(|| self.runtime.states.snapshot());
2919                if frus_widgets::rebuild_requested() {
2920                    self.request_redraw();
2921                }
2922                // A see-through frame starts transparent: the window's background under
2923                // everything but the line the system paints (milestone 643).
2924                let scene = if self.renderer.as_ref().is_some_and(Renderer::sees_through) {
2925                    backdrop_under(scene, width * scale, height * scale, self.system_line())
2926                } else {
2927                    scene
2928                };
2929                // What the keys of `frus run` asked of this frame (milestone 668).
2930                if std::mem::take(&mut self.debug.semantics_dump) {
2931                    log::info!(target: "frus::inspector", "semantics, in reading order:
2932{}", dump_semantics(ui.semantics()));
2933                }
2934                let scene = if self.debug.performance {
2935                    // The application's frame as one layer, the panel over it: in a scene of
2936                    // its own, the application's text would be drawn over the panel's
2937                    // rectangle — the renderer draws a scene's shapes, then its text.
2938                    let mut scene = on_top_of(&scene);
2939                    let costs = self.debug.last_costs.as_ref().map(|(costs, _)| costs);
2940                    // Below the status bar, where the system draws its own.
2941                    let top = self.last_insets.padding.top * scale;
2942                    paint_performance(
2943                        &mut scene,
2944                        costs,
2945                        &self.debug.history,
2946                        self.debug.fps,
2947                        width * scale,
2948                        top,
2949                        scale,
2950                    );
2951                    scene
2952                } else {
2953                    scene
2954                };
2955                #[cfg(desktop)]
2956                if let Some(path) = self.debug.screenshot.take() {
2957                    write_screenshot(&scene, width * scale, height * scale, &path);
2958                }
2959                let mut device_lost = false;
2960                if let Some(renderer) = self.renderer.as_mut() {
2961                    let render_start = measuring.then(Instant::now);
2962                    match renderer.render(&scene) {
2963                        frus_gpu::RenderOutcome::Presented => crate::startup::presented(),
2964                        frus_gpu::RenderOutcome::NeedsReconfigure => {
2965                            renderer.reconfigure();
2966                        }
2967                        frus_gpu::RenderOutcome::DeviceLost => device_lost = true,
2968                        frus_gpu::RenderOutcome::Skipped => {}
2969                    }
2970                    // The page's picture, read in the task that drew it: a WebGPU canvas keeps
2971                    // what was drawn only until the task ends.
2972                    #[cfg(web)]
2973                    if std::mem::take(&mut self.debug.page_screenshot) {
2974                        if let Some(window) = self.window.as_ref() {
2975                            crate::web_dev::send_screenshot(window);
2976                        }
2977                    }
2978                    if let Some(at) = render_start {
2979                        self.frame_costs.render = at.elapsed().as_secs_f32() * 1000.0;
2980                        let timings = renderer.last_timings();
2981                        self.frame_costs.acquire = timings.acquire;
2982                        self.frame_costs.draw = timings.draw;
2983                        self.frame_costs.gpu = timings.gpu;
2984                    }
2985                }
2986                // The GPU device is gone: a new renderer on the same window, at the next frame's
2987                // start, which draws the interface again (milestone 686, #217).
2988                if device_lost {
2989                    self.renderer_lost = true;
2990                    if let Some(window) = self.window.as_ref() {
2991                        window.request_redraw();
2992                    }
2993                }
2994                // A continuously animating widget, a spinner say, forces a redraw. So
2995                // does an image still on its way: without a frame to draw it in, the
2996                // frame that would show it never happens.
2997                //
2998                // The fetch is asked about **here** rather than inside the tree, and as a
2999                // count rather than a flag on the widget: showing a placeholder means
3000                // taking the image out of the tree, so a hook read off `Image` would go
3001                // quiet at exactly the moment it is needed. Asking here also keeps `Ui`
3002                // answering for its own widgets alone (milestone 411).
3003                let widget_asks = ui.wants_animation();
3004                let loading =
3005                    frus_widgets::images_in_flight() > 0 || frus_widgets::tasks_in_flight() > 0;
3006                let wants_animation = widget_asks || loading;
3007
3008                // Keep the interface, for hit testing. The tree is already retained.
3009                self.ui = Some(ui);
3010                // The keyboard took room from the bottom of the window: what the reader is
3011                // typing in is brought back above it, by the least scroll that does it, and
3012                // by the same glide Tab uses. This frame's layout is the shortened one, so
3013                // the viewports asked are the ones left above the keyboard. A frame with
3014                // nothing focused, or no region around what is, moves nothing.
3015                let revealed = std::mem::take(&mut self.reveal_after_keyboard) && {
3016                    let moves = match (self.runtime.input.focused, self.ui.as_ref()) {
3017                        (Some(id), Some(ui)) => ui.reveal(id, &self.runtime),
3018                        _ => Vec::new(),
3019                    };
3020                    let moved = !moves.is_empty();
3021                    for (area, offset) in moves {
3022                        self.runtime.scroll_ballistic.remove(&area);
3023                        self.runtime.scroll_velocity.remove(&area);
3024                        self.runtime.scroll_target.insert(area, offset);
3025                    }
3026                    moved
3027                };
3028                // The tree may have changed under a still pointer — a tap that opened a
3029                // screen — so what it is over is asked of this frame, as the reference
3030                // does after every frame. Not during a drag: a slider dragged past its
3031                // end is still the one being pressed, and the move path leaves it so.
3032                //
3033                // Field by field rather than through `sync_hover`, which borrows the whole
3034                // shell: the frame still holds part of it here. A change is shown the way
3035                // the reference shows it, on the next frame — the tree is built again,
3036                // since a tooltip's bubble is decided while the tree is walked.
3037                let rehovered = self.drag.is_none() && {
3038                    let hovered = self.ui.as_ref().and_then(|ui| self.hover.target(ui));
3039                    let changed = hovered != self.runtime.input.hovered;
3040                    self.runtime.input.hovered = hovered;
3041                    changed
3042                };
3043                if rehovered {
3044                    self.build_dirty = true;
3045                }
3046                let wants_animation = wants_animation || rehovered || revealed;
3047
3048                // The paged views that have just turned a page, read off **this**
3049                // frame's regions: a page change is worth reporting the moment it
3050                // reads as one, not a frame later.
3051                let paged = self
3052                    .ui
3053                    .as_ref()
3054                    .map(|ui| ui.scroll_regions().to_vec())
3055                    .unwrap_or_default();
3056                // A region whose content shrank this frame under an offset nobody owns was
3057                // drawn where it now rests; the runtime keeps it there before anything reads
3058                // the offset back — the reports below, a press, the next frame's springs.
3059                // Without it the row was drawn right once and pressed wrong (milestone 533).
3060                self.runtime.keep_scroll_in_range(&paged);
3061                let turned: Vec<A::Message> = self
3062                    .runtime
3063                    .page_changes(&paged)
3064                    .into_iter()
3065                    .filter_map(|(id, page)| {
3066                        find_widget(tree, id).and_then(|widget| widget.on_page_changed(page))
3067                    })
3068                    .collect();
3069
3070                // The scroll requests the previous frame's registry could not place:
3071                // tried once against **this** frame's, which is where a region that has
3072                // only just appeared turns up. This is the last chance they get.
3073                let (_, moved_late) = apply_scroll_requests(
3074                    &mut self.runtime,
3075                    tree,
3076                    std::mem::take(&mut self.retry_scroll),
3077                    &paged,
3078                );
3079                // The same for the sheet requests: a sheet shown by the very message that
3080                // asked it to move is in this frame's sheets and not the last one's.
3081                let (_, sheeted_late) = {
3082                    let sheets = self
3083                        .ui
3084                        .as_ref()
3085                        .map(|ui| ui.sheets().to_vec())
3086                        .unwrap_or_default();
3087                    apply_sheet_requests(
3088                        &mut self.runtime,
3089                        tree,
3090                        std::mem::take(&mut self.retry_sheet),
3091                        &sheets,
3092                    )
3093                };
3094
3095                // The regions that have moved since they last said so, read off **this**
3096                // frame's registry so that the offset and the extents reported together
3097                // come from the same frame. A region nobody is listening to costs the
3098                // comparison and nothing else.
3099                let scrolled: Vec<A::Message> = {
3100                    let grain =
3101                        |id| find_widget(tree, id).map_or(0.0, |widget| widget.scroll_grain());
3102                    self.runtime
3103                        .scroll_changes(&paged, grain)
3104                        .into_iter()
3105                        .filter_map(|(id, position)| {
3106                            find_widget(tree, id).and_then(|widget| widget.on_scroll(position))
3107                        })
3108                        .collect()
3109                };
3110
3111                // The rows whose gap has just finished closing, the pages that have
3112                // turned and the regions that have moved: the tree is no longer
3113                // borrowed, so the application can be told — and rebuild.
3114                for message in dismissed.into_iter().chain(turned).chain(scrolled) {
3115                    self.dispatch(message);
3116                }
3117                // A screen that changed under a still pointer enters and leaves regions too,
3118                // as the reference's mouse tracker does after every frame (milestone 583).
3119                self.sync_regions(false);
3120                if moved_late || sheeted_late {
3121                    // Nothing else this frame knows the offset changed: the springs ran
3122                    // before the request was placed.
3123                    self.request_redraw();
3124                }
3125
3126                // Focus return: when the focused widget has **vanished**, an overlay
3127                // having closed, go back to the trigger. Done before the AccessKit
3128                // announcement, so the focus it publishes is current.
3129                self.reconcile_focus();
3130
3131                // Publish the frame's semantic tree to AccessKit.
3132                #[cfg(desktop)]
3133                if let Some(a11y) = self.a11y.as_mut() {
3134                    let focus = self.runtime.input.focused;
3135                    let title = self.app.title();
3136                    if let Some(ui) = self.ui.as_ref() {
3137                        a11y.update(ui.semantics(), focus, &title, &self.announce);
3138                    }
3139                }
3140                // Publish the frame's semantic tree to the DOM bridge (#18).
3141                #[cfg(web)]
3142                if let Some(a11y) = self.a11y.as_mut() {
3143                    let focus = self.runtime.input.focused;
3144                    let title = self.app.title();
3145                    if let Some(ui) = self.ui.as_ref() {
3146                        a11y.update(ui.semantics(), focus, &title, &self.announce);
3147                    }
3148                }
3149
3150                // The system bars follow the frame: the regions under them, then the theme.
3151                self.sync_system_bars(&theme);
3152
3153                // The Android software keyboard follows the text fields' focus.
3154                self.sync_soft_input();
3155                // And the autofill service follows the form being edited.
3156                #[cfg(android)]
3157                self.sync_autofill();
3158
3159                // While an animation is running, ask for another frame.
3160                if animating || wants_animation {
3161                    self.request_redraw();
3162                }
3163                if let Some(start) = frame_start {
3164                    use crate::frame_stats::Why;
3165                    let why = [
3166                        (app_animating, Why::APP),
3167                        (animating, Why::RUNTIME),
3168                        (widget_asks, Why::WIDGET),
3169                        (loading, Why::LOADING),
3170                        (rehovered || revealed, Why::POINTER),
3171                    ];
3172                    self.frame_costs.why = why
3173                        .into_iter()
3174                        .filter(|(on, _)| *on)
3175                        .fold(Why::NONE, |all, (_, why)| all | why);
3176                    self.frame_stats.frame(start, self.frame_costs);
3177                    if self.debug.performance {
3178                        if let Some((_, before)) = self.debug.last_costs {
3179                            let gap = start.saturating_duration_since(before).as_secs_f32();
3180                            if gap > 0.0 {
3181                                self.debug.fps = 1.0 / gap;
3182                            }
3183                        }
3184                        // The first frame measured: one more shows what it cost.
3185                        if self.debug.last_costs.is_none() {
3186                            self.request_redraw();
3187                        }
3188                        self.debug.last_costs = Some((self.frame_costs, start));
3189                        self.debug.history.push(&self.frame_costs);
3190                    }
3191                }
3192                // The title bar's line, shared with the content when it asks (milestone 640).
3193                #[cfg(windows)]
3194                self.sync_title_bar();
3195            }
3196
3197            _ => {}
3198        }
3199    }
3200}
3201
3202/// What the keys of `frus run` turned on or asked for (milestone 668).
3203#[derive(Default)]
3204struct DebugAsks {
3205    /// Outlines around every widget, without the inspector's card: debug paint (`p`).
3206    outlines: bool,
3207    /// The focusable widgets to print on the next frame, the focused one marked (`f`).
3208    focus_dump: bool,
3209    /// The accessibility tree to print on the next frame (`S`).
3210    semantics_dump: bool,
3211    /// The brightness the theme is resolved with instead of the platform's (`b`).
3212    brightness: Option<frus_widgets::Brightness>,
3213    /// The performance panel: the last frame's costs, over the interface (`P`).
3214    performance: bool,
3215    /// The last frame's costs and when it began, for the panel.
3216    last_costs: Option<(crate::frame_stats::FrameCosts, Instant)>,
3217    /// The latest frames' costs, oldest first, for the panel's graphs (milestone 676).
3218    history: FrameHistory,
3219    /// Frames per second, from the gap between the last two.
3220    fps: f32,
3221    /// Where to write the next frame as a PNG (`s`).
3222    #[cfg_attr(not(desktop), allow(dead_code))]
3223    screenshot: Option<std::path::PathBuf>,
3224    /// The page's next frame to send to `frus run` as a PNG (`s` on the web, milestone 675).
3225    #[cfg_attr(not(web), allow(dead_code))]
3226    page_screenshot: bool,
3227}
3228
3229/// The order `o` goes through the platforms in.
3230const PLATFORM_CYCLE: [frus_widgets::TargetPlatform; 6] = [
3231    frus_widgets::TargetPlatform::Android,
3232    frus_widgets::TargetPlatform::Ios,
3233    frus_widgets::TargetPlatform::Windows,
3234    frus_widgets::TargetPlatform::MacOs,
3235    frus_widgets::TargetPlatform::Linux,
3236    frus_widgets::TargetPlatform::Fuchsia,
3237];
3238
3239/// The platform after `current` in [`PLATFORM_CYCLE`].
3240fn next_platform(current: frus_widgets::TargetPlatform) -> frus_widgets::TargetPlatform {
3241    let at = PLATFORM_CYCLE
3242        .iter()
3243        .position(|p| *p == current)
3244        .unwrap_or(0);
3245    PLATFORM_CYCLE[(at + 1) % PLATFORM_CYCLE.len()]
3246}
3247
3248/// The focusable widgets of a frame, in tab order, the focused one marked.
3249fn dump_focus(
3250    nodes: &[frus_widgets::InspectorNode],
3251    focusable: &[WidgetId],
3252    focused: Option<WidgetId>,
3253) -> String {
3254    let mut out = String::new();
3255    for id in focusable {
3256        let node = nodes.iter().rev().find(|n| n.id == *id);
3257        let mark = if Some(*id) == focused { "▶ " } else { "  " };
3258        match node {
3259            Some(n) => out.push_str(&format!(
3260                "{mark}{}  {:.0},{:.0}  {:.0}×{:.0}  #{:08x}\n",
3261                n.name,
3262                n.rect.x,
3263                n.rect.y,
3264                n.rect.width,
3265                n.rect.height,
3266                id.as_u64() as u32
3267            )),
3268            None => out.push_str(&format!("{mark}#{:08x}\n", id.as_u64() as u32)),
3269        }
3270    }
3271    if out.is_empty() {
3272        out.push_str("  (nothing focusable)\n");
3273    }
3274    out
3275}
3276
3277/// The accessibility tree of a frame, one node a line, in reading order.
3278fn dump_semantics(nodes: &[(WidgetId, Rect, frus_widgets::SemanticsProperties)]) -> String {
3279    let mut out = String::new();
3280    for (_, rect, props) in nodes {
3281        out.push_str(&format!("  {:?}", props.role));
3282        if let Some(label) = &props.label {
3283            out.push_str(&format!(" \"{label}\""));
3284        }
3285        if let Some(value) = &props.value {
3286            out.push_str(&format!(" = {value}"));
3287        }
3288        if props.clickable {
3289            out.push_str(" clickable");
3290        }
3291        if props.disabled {
3292            out.push_str(" disabled");
3293        }
3294        out.push_str(&format!(
3295            "  {:.0},{:.0}  {:.0}×{:.0}\n",
3296            rect.x, rect.y, rect.width, rect.height
3297        ));
3298    }
3299    if out.is_empty() {
3300        out.push_str("  (empty)\n");
3301    }
3302    out
3303}
3304
3305/// The performance panel, in the top right corner of a `width`×`height` frame drawn at
3306/// `scale`: the last frame's costs and the rate of frames.
3307#[allow(clippy::too_many_arguments)]
3308fn paint_performance(
3309    scene: &mut frus_widgets::Scene,
3310    costs: Option<&crate::frame_stats::FrameCosts>,
3311    history: &FrameHistory,
3312    fps: f32,
3313    width: f32,
3314    top: f32,
3315    scale: f32,
3316) {
3317    let lines = match costs {
3318        Some(costs) => performance_lines(costs, fps),
3319        None => vec!["measuring…".to_string()],
3320    };
3321    paint_panel(scene, lines, history, width, top, scale);
3322}
3323
3324/// How many frames the graphs show.
3325const HISTORY: usize = 90;
3326/// A frame's budget at 60 Hz, in milliseconds: the line the bars are measured against.
3327const BUDGET_MS: f32 = 1000.0 / 60.0;
3328/// The height of a graph, in milliseconds: two budgets.
3329const GRAPH_MS: f32 = BUDGET_MS * 2.0;
3330
3331/// The latest frames' costs for the performance panel: what the interface took (build, layout,
3332/// paint) and what drawing took (encoding, submitting, waiting for the GPU and the display).
3333#[derive(Default)]
3334struct FrameHistory {
3335    ui: std::collections::VecDeque<f32>,
3336    render: std::collections::VecDeque<f32>,
3337}
3338
3339impl FrameHistory {
3340    fn push(&mut self, costs: &crate::frame_stats::FrameCosts) {
3341        let ui = costs.build.unwrap_or(0.0) + costs.layout_paint;
3342        for (list, value) in [(&mut self.ui, ui), (&mut self.render, costs.render)] {
3343            if list.len() == HISTORY {
3344                list.pop_front();
3345            }
3346            list.push_back(value);
3347        }
3348    }
3349
3350    /// The bars of one graph, `height` high: for each frame its height, and whether it was
3351    /// over budget. A frame over the graph's top fills it.
3352    fn bars(values: &std::collections::VecDeque<f32>, height: f32) -> Vec<(f32, bool)> {
3353        values
3354            .iter()
3355            .map(|ms| ((ms / GRAPH_MS).min(1.0) * height, *ms > BUDGET_MS))
3356            .collect()
3357    }
3358}
3359
3360/// A scene holding `scene` as one layer, for something to be painted over all of it: within a
3361/// scene, the renderer draws the shapes first and the text after, so a panel's rectangle in the
3362/// same scene would sit under the application's text.
3363fn on_top_of(scene: &Scene) -> Scene {
3364    let mut over = Scene::new();
3365    over.layer(1.0, |inner| {
3366        for primitive in scene.primitives() {
3367            inner.push_primitive(primitive.clone());
3368        }
3369    });
3370    over
3371}
3372
3373/// The panel's lines for a frame that cost `costs`.
3374fn performance_lines(costs: &crate::frame_stats::FrameCosts, fps: f32) -> Vec<String> {
3375    let build = costs
3376        .build
3377        .map_or_else(|| "kept".to_string(), |ms| format!("{ms:.1} ms"));
3378    let mut lines = vec![
3379        // One frame alone has no rate: the interface is idle.
3380        if fps > 0.0 {
3381            format!("{fps:.0} fps")
3382        } else {
3383            "idle".to_string()
3384        },
3385        format!("build {build}  layout+paint {:.1} ms", costs.layout_paint),
3386        format!(
3387            "render {:.1} ms (acquire {:.1}, draw {:.1})",
3388            costs.render, costs.acquire, costs.draw
3389        ),
3390    ];
3391    if let Some(gpu) = costs.gpu {
3392        lines.push(format!("GPU {gpu:.1} ms"));
3393    }
3394    lines
3395}
3396
3397/// `lines` in a dark panel in the top right corner of a frame `width` physical pixels wide,
3398/// `top` pixels down.
3399fn paint_panel(
3400    scene: &mut frus_widgets::Scene,
3401    lines: Vec<String>,
3402    history: &FrameHistory,
3403    width: f32,
3404    top: f32,
3405    scale: f32,
3406) {
3407    let size = 12.0 * scale;
3408    let style = frus_widgets::TextStyle::new(size).resolved();
3409    let pad = 6.0 * scale;
3410    let line_h = size * 1.4;
3411    // Estimated rather than measured: the panel is a tool's, and a little room to spare is
3412    // all an estimate costs.
3413    let longest = lines
3414        .iter()
3415        .map(|line| line.chars().count())
3416        .max()
3417        .unwrap_or(0);
3418    // Two graphs under the text, one bar a frame.
3419    let bar_w = 2.0 * scale;
3420    let graph_w = HISTORY as f32 * bar_w;
3421    let graph_h = 36.0 * scale;
3422    let graphs_h = if history.ui.is_empty() {
3423        0.0
3424    } else {
3425        2.0 * (graph_h + line_h) + pad
3426    };
3427    let text_w = (longest as f32 * size * 0.6).max(graph_w);
3428    let panel = Rect::new(
3429        (width - text_w - pad * 3.0).max(0.0),
3430        top + pad,
3431        text_w + pad * 2.0,
3432        line_h * lines.len() as f32 + pad * 2.0 + graphs_h,
3433    );
3434    scene.set_clip(Rect::UNBOUNDED);
3435    scene.set_owner(0);
3436    // Opaque: a translucent one blends in linear light and paints as a grey.
3437    scene.draw_rect(
3438        panel,
3439        frus_widgets::Color::rgb(0.05, 0.05, 0.05),
3440        4.0 * scale,
3441        0.0,
3442        frus_widgets::Color::TRANSPARENT,
3443    );
3444    let count = lines.len();
3445    for (i, line) in lines.into_iter().enumerate() {
3446        scene.text(
3447            frus_widgets::Point::new(panel.x + pad, panel.y + pad + line_h * i as f32),
3448            line,
3449            &style,
3450            frus_widgets::Color::rgb(0.55, 1.0, 0.55),
3451        );
3452    }
3453    if history.ui.is_empty() {
3454        return;
3455    }
3456    let mut y = panel.y + pad + line_h * count as f32 + pad;
3457    for (title, values) in [
3458        ("interface (build, layout, paint)", &history.ui),
3459        ("drawing (encode, submit, present)", &history.render),
3460    ] {
3461        scene.text(
3462            frus_widgets::Point::new(panel.x + pad, y),
3463            title.to_string(),
3464            &style,
3465            frus_widgets::Color::rgb(0.75, 0.75, 0.75),
3466        );
3467        y += line_h;
3468        let area = Rect::new(panel.x + pad, y, graph_w, graph_h);
3469        scene.draw_rect(
3470            area,
3471            frus_widgets::Color::rgb(0.12, 0.12, 0.12),
3472            0.0,
3473            0.0,
3474            frus_widgets::Color::TRANSPARENT,
3475        );
3476        // The bars, the newest on the right.
3477        let bars = FrameHistory::bars(values, graph_h);
3478        let start = area.x + area.width - bars.len() as f32 * bar_w;
3479        for (i, (h, over)) in bars.into_iter().enumerate() {
3480            if h <= 0.0 {
3481                continue;
3482            }
3483            let color = if over {
3484                frus_widgets::Color::rgb(1.0, 0.35, 0.3)
3485            } else {
3486                frus_widgets::Color::rgb(0.4, 0.85, 0.45)
3487            };
3488            scene.fill_rect(
3489                Rect::new(
3490                    start + i as f32 * bar_w,
3491                    area.y + area.height - h,
3492                    bar_w * 0.8,
3493                    h,
3494                ),
3495                color,
3496            );
3497        }
3498        // The budget: a frame drawn at 60 Hz stays under this line.
3499        let budget_y = area.y + area.height * (1.0 - BUDGET_MS / GRAPH_MS);
3500        scene.fill_rect(
3501            Rect::new(area.x, budget_y, area.width, scale.max(1.0)),
3502            frus_widgets::Color::rgb(0.95, 0.95, 0.95),
3503        );
3504        y += graph_h + pad / 2.0;
3505    }
3506}
3507
3508/// Writes `scene`, a frame `width`×`height` physical pixels, to `path` as a PNG, drawn again
3509/// offscreen: the window's own image cannot be read back. Says where in the log, or why not.
3510#[cfg(desktop)]
3511fn write_screenshot(scene: &Scene, width: f32, height: f32, path: &std::path::Path) {
3512    let (w, h) = (
3513        width.round().max(1.0) as u32,
3514        height.round().max(1.0) as u32,
3515    );
3516    let Some(frame) = frus_gpu::render_offscreen(scene, w, h, frus_gpu::backdrop()) else {
3517        log::warn!(target: "frus::dev", "screenshot: no GPU to draw it with");
3518        return;
3519    };
3520    let png = crate::dev_commands::png(frame.width, frame.height, &frame.rgba);
3521    match std::fs::write(path, png) {
3522        Ok(()) => log::debug!(
3523            target: "frus::dev",
3524            "screenshot {}×{} written to {}",
3525            frame.width,
3526            frame.height,
3527            path.display()
3528        ),
3529        Err(error) => log::warn!(target: "frus::dev", "screenshot: {}: {error}", path.display()),
3530    }
3531}
3532
3533impl<A: Application> App<A> {
3534    /// Carries out what the keys of `frus run` asked since the last frame (milestone 668).
3535    /// Each says what it did in the log, which `frus run` shows.
3536    /// A new renderer on the window, after its GPU device was lost — a driver reset, a GPU
3537    /// removed or restarted —, of the same kind as the old one: the application goes on, its
3538    /// interface drawn again on the next frame (milestone 686, #217).
3539    fn rebuild_renderer(&mut self) {
3540        let see_through = self.renderer.as_ref().is_some_and(Renderer::sees_through);
3541        // The old renderer — its surface on this window among it — goes first.
3542        self.renderer = None;
3543        let Some(window) = self.window.clone() else {
3544            return;
3545        };
3546        let size = window.inner_size();
3547        let (width, height) = (size.width.max(1), size.height.max(1));
3548        log::warn!("the GPU device was lost: drawing again with a new one");
3549        #[cfg(not(web))]
3550        {
3551            let made = if see_through {
3552                pollster::block_on(Renderer::see_through(window.clone(), width, height))
3553            } else {
3554                pollster::block_on(Renderer::new(window.clone(), width, height))
3555            };
3556            match made {
3557                Ok(renderer) => self.renderer = Some(renderer),
3558                Err(err) => {
3559                    log::error!("the GPU device was lost, and no new one could be made: {err:#}")
3560                }
3561            }
3562        }
3563        #[cfg(web)]
3564        {
3565            let _ = see_through;
3566            let slot = self.pending_renderer.clone();
3567            let win = window.clone();
3568            wasm_bindgen_futures::spawn_local(async move {
3569                match Renderer::new(win.clone(), width, height).await {
3570                    Ok(renderer) => {
3571                        *slot.borrow_mut() = Some(renderer);
3572                        win.request_redraw();
3573                    }
3574                    Err(err) => {
3575                        log::error!(
3576                            "the GPU device was lost, and no new one could be made: {err:#}"
3577                        )
3578                    }
3579                }
3580            });
3581        }
3582        self.build_dirty = true;
3583        window.request_redraw();
3584    }
3585
3586    fn take_dev_commands(&mut self) {
3587        for command in crate::dev_commands::take_commands() {
3588            let (name, argument) = command
3589                .split_once(' ')
3590                .map_or((command.as_str(), ""), |(n, a)| (n, a.trim()));
3591            let on_off = |on: bool| if on { "on" } else { "off" };
3592            match name {
3593                "inspector" => {
3594                    self.inspector = !self.inspector;
3595                    self.inspector_dump = self.inspector;
3596                    log::info!(target: "frus::dev", "inspector {}", on_off(self.inspector));
3597                }
3598                "outlines" => {
3599                    self.debug.outlines = !self.debug.outlines;
3600                    log::info!(target: "frus::dev", "debug paint {}", on_off(self.debug.outlines));
3601                }
3602                "widgets" => self.inspector_dump = true,
3603                "focus" => self.debug.focus_dump = true,
3604                "semantics" => self.debug.semantics_dump = true,
3605                "platform" => {
3606                    let next = next_platform(frus_widgets::default_target_platform());
3607                    if cfg!(debug_assertions) {
3608                        frus_widgets::set_debug_default_target_platform_override(Some(next));
3609                        log::info!(target: "frus::dev", "target platform: {next:?}");
3610                    } else {
3611                        log::warn!(target: "frus::dev", "the platform can be switched in a debug build only");
3612                    }
3613                }
3614                "brightness" => {
3615                    let now = self.debug.brightness.unwrap_or(self.platform.brightness);
3616                    let next = match now {
3617                        frus_widgets::Brightness::Light => frus_widgets::Brightness::Dark,
3618                        frus_widgets::Brightness::Dark => frus_widgets::Brightness::Light,
3619                    };
3620                    self.debug.brightness = Some(next);
3621                    log::info!(
3622                        target: "frus::dev",
3623                        "brightness: {next:?} (an application without a theme for it stays as it is)"
3624                    );
3625                }
3626                "performance" => {
3627                    self.debug.performance = !self.debug.performance;
3628                    self.debug.last_costs = None;
3629                    self.debug.history = FrameHistory::default();
3630                    self.frame_stats.set_enabled(self.debug.performance);
3631                    // This frame began unmeasured: the next one is, and shows its figures.
3632                    self.request_redraw();
3633                    log::info!(target: "frus::dev", "performance panel {}", on_off(self.debug.performance));
3634                }
3635                // The system's low-memory call, on demand (milestone 687).
3636                "trim-memory" => memory_trimmed(),
3637                // A driver reset, on purpose: what follows is the recovery (milestone 686).
3638                "lose-gpu" => {
3639                    if let Some(renderer) = self.renderer.as_ref() {
3640                        log::warn!(target: "frus::dev", "losing the GPU device, as a driver reset would");
3641                        renderer.lose_device_for_testing();
3642                        self.request_redraw();
3643                    }
3644                }
3645                "screenshot" if !argument.is_empty() => {
3646                    #[cfg(desktop)]
3647                    {
3648                        self.debug.screenshot = Some(std::path::PathBuf::from(argument));
3649                    }
3650                    #[cfg(web)]
3651                    {
3652                        self.debug.page_screenshot = true;
3653                    }
3654                    #[cfg(android)]
3655                    log::warn!(target: "frus::dev", "the application takes no screenshot of itself here");
3656                }
3657                _ => {
3658                    log::warn!(target: "frus::dev", "frus run asked for `{command}`, which this frus does not know")
3659                }
3660            }
3661            self.build_dirty = true;
3662        }
3663    }
3664
3665    /// The brightness the theme is resolved with: the platform's, unless `b` said otherwise.
3666    fn brightness(&self) -> frus_widgets::Brightness {
3667        self.debug.brightness.unwrap_or(self.platform.brightness)
3668    }
3669
3670    /// How tall the title bar's line the system paints is, in physical pixels: `0` where
3671    /// it paints none (milestone 643).
3672    fn system_line(&self) -> f32 {
3673        // The line the content was told: the one its row covers.
3674        match self.title_bar {
3675            Some(line) if self.title_bar_on && line.system_paints => {
3676                line.height * self.total_scale()
3677            }
3678            _ => 0.0,
3679        }
3680    }
3681
3682    /// The total scale: system DPI × app density (physical = logical × this).
3683    fn total_scale(&self) -> f32 {
3684        (self.scale * self.app.density()).max(0.1)
3685    }
3686
3687    /// The surface description installed around every call to `view`, so that any
3688    /// widget built there can read it with `MediaQuery::of()` instead of having the
3689    /// application carry it down by hand.
3690    ///
3691    /// Everything in it is already known to the shell — the logical size it is about
3692    /// to lay out for, the DPI scale, the app's density, and the insets last reported
3693    /// by the platform. It is assembled here, in one place, rather than at each of the
3694    /// call sites.
3695    /// The **platform** answers, and the application overrides what it chose to speak for.
3696    ///
3697    /// That order is the one the reference uses and the only one that stays honest: the
3698    /// settings belong to the person using the device, so the framework asks them first,
3699    /// and an application with a *reduce motion* switch of its own says only that. An
3700    /// [`AccessibilityOverrides`] whose fields are all `None` — the default — leaves every
3701    /// answer to the user, which is what an application that has no settings screen means.
3702    fn media_query(&self, width: f32, height: f32) -> MediaQuery {
3703        MediaQuery::new(Size::new(width, height))
3704            .with_device_pixel_ratio(self.scale)
3705            .with_density(self.app.density())
3706            .with_text_scaler(self.platform.text_scaler)
3707            .with_platform_brightness(self.brightness())
3708            .with_accessibility(
3709                self.platform
3710                    .accessibility
3711                    .with_overrides(self.app.accessibility()),
3712            )
3713            .with_insets(self.last_insets)
3714            .with_title_bar(self.title_bar)
3715    }
3716
3717    /// **Shares the window's title bar line with the content** when the frame asks for it,
3718    /// gives it back when it no longer does, and tells the content what the system keeps on
3719    /// it (milestone 640). See `title_bar.rs`.
3720    #[cfg(windows)]
3721    fn sync_title_bar(&mut self) {
3722        use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
3723        let Some(window) = self.window.clone() else {
3724            return;
3725        };
3726        let hwnd = match window.window_handle().map(|h| h.as_raw()) {
3727            Ok(RawWindowHandle::Win32(win)) => {
3728                win.hwnd.get() as windows_sys::Win32::Foundation::HWND
3729            }
3730            _ => return,
3731        };
3732        let wants = self.ui.as_ref().is_some_and(|ui| ui.wants_title_bar());
3733        if wants != self.title_bar_on {
3734            // SAFETY: the window is alive and was made on this thread.
3735            unsafe {
3736                if wants {
3737                    crate::title_bar::enable(hwnd);
3738                    let wake = window.clone();
3739                    crate::title_bar::on_change(move || wake.request_redraw());
3740                } else {
3741                    crate::title_bar::disable(hwnd);
3742                }
3743            }
3744            self.title_bar_on = wants;
3745        }
3746        let next = if self.title_bar_on {
3747            let scale = self.total_scale();
3748            let band = crate::title_bar::height() as f32 / scale;
3749            // What the content has on the line is its own; the rest moves the window.
3750            if let Some(ui) = &self.ui {
3751                let to_px = |r: frus_widgets::Rect| windows_sys::Win32::Foundation::RECT {
3752                    left: (r.x * scale).floor() as i32,
3753                    top: (r.y * scale).floor() as i32,
3754                    right: ((r.x + r.width) * scale).ceil() as i32,
3755                    bottom: ((r.y + r.height) * scale).ceil() as i32,
3756                };
3757                let interactive = ui
3758                    .hit_rects()
3759                    .filter(|r| r.y < band && r.width < self.logical_width())
3760                    .map(to_px)
3761                    .collect();
3762                let icon = ui
3763                    .title_bar_regions()
3764                    .iter()
3765                    .find(|(_, role)| *role == frus_widgets::TitleBarRole::Icon)
3766                    .map(|(r, _)| to_px(*r));
3767                crate::title_bar::publish(interactive, icon);
3768            }
3769            // SAFETY: as above.
3770            let buttons = unsafe { crate::title_bar::buttons(hwnd) }.map(|r| {
3771                frus_widgets::Rect::new(
3772                    r.left as f32 / scale,
3773                    r.top as f32 / scale,
3774                    (r.right - r.left) as f32 / scale,
3775                    (r.bottom - r.top) as f32 / scale,
3776                )
3777            });
3778            let button = |code: u32| {
3779                use windows_sys::Win32::UI::WindowsAndMessaging::{
3780                    HTCLOSE, HTMAXBUTTON, HTMINBUTTON,
3781                };
3782                match code {
3783                    HTMINBUTTON => Some(frus_widgets::CaptionButton::Minimize),
3784                    HTMAXBUTTON => Some(frus_widgets::CaptionButton::Maximize),
3785                    HTCLOSE => Some(frus_widgets::CaptionButton::Close),
3786                    _ => None,
3787                }
3788            };
3789            let (hovered, pressed) = crate::title_bar::buttons_state();
3790            if self.window_icon.is_none() {
3791                self.window_icon = self
3792                    .app
3793                    .icon()
3794                    .png()
3795                    .map(|png| &*Box::leak(png.to_vec().into_boxed_slice()));
3796            }
3797            // The caption the system would draw: dark as the window's own, which follows the
3798            // system's apps (milestone 642).
3799            let active = window.has_focus();
3800            let dark = window.theme() == Some(winit::window::Theme::Dark);
3801            let (background, foreground) = crate::title_bar::caption_colors(dark, active);
3802            Some(frus_widgets::TitleBar {
3803                // The line ends where the system's buttons do: maximized, the window's
3804                // resizing border is off the screen and the buttons sit higher, and the words
3805                // centred in the line stay level with them (milestone 643).
3806                height: buttons.map_or(band, |b| (b.y + b.height).clamp(1.0, band)),
3807                leading: 0.0,
3808                buttons,
3809                icon: self.window_icon,
3810                hovered: button(hovered),
3811                pressed: button(pressed),
3812                maximized: window.is_maximized(),
3813                active,
3814                background: Some(background),
3815                foreground: Some(foreground),
3816                // The system shows its own caption through a see-through frame, where it
3817                // paints a backdrop on the line (milestone 643).
3818                system_paints: self.renderer.as_ref().is_some_and(Renderer::sees_through)
3819                    // SAFETY: as above.
3820                    && unsafe { crate::title_bar::system_backdrop(hwnd, dark) },
3821            })
3822        } else {
3823            None
3824        };
3825        if next != self.title_bar {
3826            log::debug!(
3827                target: "frus::title_bar",
3828                "the line: {:?}",
3829                next.map(|line| (line.height, line.buttons, line.active, line.system_paints))
3830            );
3831            self.title_bar = next;
3832            self.build_dirty = true;
3833            self.request_redraw();
3834        }
3835    }
3836
3837    /// The current layout direction; RTL flips both the layout and the gestures.
3838    fn is_rtl(&self) -> bool {
3839        self.themes.displayed(&self.app).direction.is_rtl()
3840    }
3841
3842    /// The window's **logical** width, in px, for the edge thresholds.
3843    fn logical_width(&self) -> f32 {
3844        let scale = self.total_scale();
3845        self.window
3846            .as_ref()
3847            .map(|w| w.inner_size().width as f32 / scale)
3848            .unwrap_or(1.0)
3849            .max(1.0)
3850    }
3851
3852    fn request_redraw(&self) {
3853        if let Some(window) = &self.window {
3854            window.request_redraw();
3855        }
3856    }
3857
3858    /// The **normalised** pointer input (gesture tier 0): mouse and touch converge
3859    /// here, with an explicit `Cancel`. The long-press recogniser is fed along the way,
3860    /// and the loop is woken at its deadline.
3861    fn pointer(&mut self, event_loop: &ActiveEventLoop, event: PointerEvent) {
3862        self.pointer_event(event);
3863        // Wake the loop exactly at the next deadline, and rest otherwise.
3864        event_loop.set_control_flow(self.idle_control_flow());
3865    }
3866
3867    /// Everything [`pointer`](Self::pointer) does with an event but wake the loop: the one
3868    /// place a press, a movement and a release are routed, and so what a test drives. The
3869    /// regions the pointer is in are brought up to date after it (milestone 583).
3870    fn pointer_event(&mut self, event: PointerEvent) {
3871        let kind = event.kind;
3872        // The raw events first, against the frame the pointer is over: a listener hears the
3873        // press before anything it leads to rebuilds the tree (milestone 586).
3874        self.cursor = event.position;
3875        self.route_listeners(kind, event.touch);
3876        self.route_pointer_event(event);
3877        self.pointer_held = match kind {
3878            PointerKind::Down => true,
3879            PointerKind::Up | PointerKind::Cancel => false,
3880            PointerKind::Move => self.pointer_held,
3881        };
3882        self.sync_regions(kind == PointerKind::Move);
3883    }
3884
3885    /// Routes one pointer event: see [`pointer_event`](Self::pointer_event).
3886    fn route_pointer_event(&mut self, event: PointerEvent) {
3887        self.cursor = event.position;
3888        self.hover.event(event.kind, event.position, event.touch);
3889        // Whether the gesture under way could still end as a tap, before this event moves
3890        // it on: the press is shared with it until it cannot (milestone 534).
3891        let could_tap = gesture_was_a_tap(self.drag.as_ref());
3892        match event.kind {
3893            PointerKind::Down => {
3894                // What is under the pointer **now**: a finger has no move before its
3895                // press, and a press only shows while it is also the hovered widget.
3896                self.sync_hover();
3897                // A pointer interaction: the keyboard focus ring fades away.
3898                self.runtime.focus_visible = false;
3899                self.pointer_down(event.touch);
3900                let (long_press, lift) = hold_candidates(
3901                    self.drag.as_ref(),
3902                    self.ui.as_ref(),
3903                    self.tree.as_deref(),
3904                    self.cursor,
3905                );
3906                self.long_press_msg = long_press;
3907                self.pending_lift = lift;
3908                let interested = self.long_press_msg.is_some()
3909                    || self.pending_lift.is_some()
3910                    || self.pending_reorder.is_some()
3911                    || self.pending_word.is_some()
3912                    || self.pending_region_hold;
3913                self.press.down(self.cursor, Instant::now(), interested);
3914            }
3915            PointerKind::Move => {
3916                self.press.moved(self.cursor);
3917                self.pointer_move();
3918                // The inspector follows the pointer to highlight, so it redraws even
3919                // over inert widgets, which have no hover animation.
3920                if self.inspector {
3921                    self.request_redraw();
3922                }
3923            }
3924            PointerKind::Up => {
3925                // The recogniser is always told, but a lifted item owes a drop: the
3926                // long press that started it must not also eat its ending.
3927                let swallow = self.press.up();
3928                self.pending_lift = None;
3929                self.pending_reorder = None;
3930                self.pending_word = None;
3931                self.pending_region_hold = false;
3932                // A row lifted by the hold owes its drop for the same reason a lifted
3933                // item does: the release is what says where it goes.
3934                if swallow
3935                    && !matches!(
3936                        self.drag,
3937                        Some(Drag::Item { moved: true, .. } | Drag::Reorder { moved: true, .. })
3938                    )
3939                {
3940                    // The long press evicted the tap, so the release is swallowed.
3941                    self.drag = None;
3942                    self.runtime.input.pressed = None;
3943                    self.request_redraw();
3944                } else {
3945                    self.pointer_up();
3946                }
3947            }
3948            PointerKind::Cancel => {
3949                self.press.cancel();
3950                self.pending_lift = None;
3951                self.pending_reorder = None;
3952                self.pending_word = None;
3953                self.pending_region_hold = false;
3954                // A cancelled gesture still owes the offset back, or the region
3955                // would stay frozen under a finger that is no longer there.
3956                if let Some(Drag::Dismiss { item, .. }) = self.drag {
3957                    // A cancelled swipe asks for nothing: zero velocity puts it back.
3958                    self.runtime.dismiss_release(
3959                        item.id,
3960                        0.0,
3961                        0.0,
3962                        item.spec.axis,
3963                        // Unreachable threshold: a cancel never dismisses, however far
3964                        // the item had already travelled.
3965                        f32::INFINITY,
3966                    );
3967                }
3968                // A value a cancelled drag held settles back, and says nothing.
3969                if let Some(Drag::Gesture { id, .. }) = self.drag {
3970                    self.runtime.release_value(id);
3971                }
3972                if let Some(Drag::Scroll { id, .. }) = self.drag {
3973                    self.runtime.release_scroll(id);
3974                    self.runtime.glow_scroll_end(id);
3975                    // A cancelled gesture asks for nothing: the indicator slides back
3976                    // rather than promising a refresh nobody requested.
3977                    if let Some(host) = self.refresh_host_of(id) {
3978                        self.runtime.refresh_cancel(host);
3979                    }
3980                }
3981                // A sheet under a cancelled finger settles from where it was left.
3982                let sheet = match self.drag {
3983                    Some(Drag::Sheet { id, available, .. }) => self
3984                        .ui
3985                        .as_ref()
3986                        .and_then(|ui| ui.sheet(id))
3987                        .map(|sheet| (sheet.id, sheet.spec.clone(), available)),
3988                    Some(Drag::Scroll { id, .. }) => self
3989                        .ui
3990                        .as_ref()
3991                        .and_then(|ui| ui.sheet_holding(id))
3992                        .map(|sheet| (sheet.id, sheet.spec.clone(), sheet.available)),
3993                    _ => None,
3994                };
3995                if let Some((id, spec, available)) = sheet {
3996                    self.runtime.sheet_release(id, &spec, available, 0.0, None);
3997                }
3998                self.drag = None;
3999                self.runtime.input.pressed = None;
4000                self.request_redraw();
4001            }
4002        }
4003        // A press a gesture has taken, or one whose pointer has gone, is let go of — after
4004        // the routing, which is what reads it (milestone 534).
4005        if settle_press(
4006            &mut self.runtime.input,
4007            event.kind,
4008            could_tap,
4009            self.drag.as_ref(),
4010            event.touch,
4011        ) {
4012            self.request_redraw();
4013        }
4014        // After the release has been routed — it reads the press, not the hover — a
4015        // finger that has lifted stops hovering (milestone 505).
4016        if matches!(event.kind, PointerKind::Up | PointerKind::Cancel) {
4017            self.sync_hover();
4018        }
4019    }
4020
4021    /// A press held still for the long-press delay: what it was a candidate for is decided.
4022    fn hold_deadline_reached(&mut self) {
4023        // Two claims on one hold: a widget asking for a message, and an item asking
4024        // to be lifted. Serving both would do a discrete action *and* start a drag
4025        // from the same gesture, which is never what anyone meant. **The lift
4026        // wins** — it changes what the rest of the gesture means, and the message
4027        // would be acting on something the finger is still holding.
4028        let lifting = self.pending_lift.is_some() || self.pending_reorder.is_some();
4029        // A hold in a text field selects the word under the finger (milestone 511),
4030        // and is that — not also the long press of whatever the field sits in.
4031        let word = self.pending_word.take();
4032        // A hold on words in a selection area selects the word, unless something held asked
4033        // for the hold first: a lift, a field, a widget's own long press.
4034        let region = std::mem::take(&mut self.pending_region_hold)
4035            && !lifting
4036            && word.is_none()
4037            && self.long_press_msg.is_none();
4038        if let Some(message) = self.long_press_msg.take() {
4039            if !lifting && word.is_none() {
4040                self.dispatch(message);
4041            }
4042        }
4043        if let Some(id) = word {
4044            self.select_held_word(id);
4045        }
4046        if region && self.select_region_word_held() {
4047            // The page under the finger stops being scrolled by it: the hold was a selection.
4048            if let Some(Drag::Scroll { id, .. }) = self.drag {
4049                self.runtime.release_scroll(id);
4050            }
4051        }
4052        let lift = self.pending_lift.take();
4053        let reorder = self.pending_reorder.take();
4054        // The scroll hands the gesture over to what the hold lifted.
4055        if lift.is_some() || reorder.is_some() {
4056            if let Some(Drag::Scroll { id, .. }) = self.drag {
4057                self.runtime.release_scroll(id);
4058            }
4059        }
4060        self.drag = drag_after_hold(self.drag.take(), lift, reorder, self.cursor);
4061        if matches!(self.drag, Some(Drag::Reorder { .. })) {
4062            self.reorder_x = self.cursor.x;
4063            self.reorder_y = self.cursor.y;
4064        }
4065        self.request_redraw();
4066    }
4067
4068    /// The loop's idle policy: wake at the **nearest** deadline — the long press, the
4069    /// live-reload poll, the caret's next turn — and otherwise wait outright.
4070    fn idle_control_flow(&self) -> ControlFlow {
4071        let press = self.press.deadline();
4072        let reload = self.reload.as_ref().map(|w| w.deadline());
4073        // Twice a second while a field has the caret and the window is in front, and never
4074        // otherwise (milestone 513).
4075        let caret = (self.lifecycle == Lifecycle::Resumed)
4076            .then(|| self.caret.next_toggle(Instant::now()))
4077            .flatten();
4078        // A first tap that may still become a double tap is sent when its wait is over.
4079        let tap = self.pending_tap.as_ref().map(|p| p.at + DOUBLE_TAP_TIMEOUT);
4080        match [press, reload, caret, tap].into_iter().flatten().min() {
4081            Some(at) => ControlFlow::WaitUntil(at),
4082            None => ControlFlow::Wait,
4083        }
4084    }
4085
4086    /// The focused field's caret, as its blink sees it: which field, where its caret and
4087    /// selection are, and a digest of its value. `None` when what has focus takes no
4088    /// typing, which has no caret to blink.
4089    fn caret_signature(&self) -> Option<crate::caret::Signature> {
4090        use std::hash::{Hash, Hasher};
4091        let field = self.runtime.input.focused?;
4092        let widget = find_widget(self.tree.as_ref()?.as_ref(), field)?;
4093        let text = widget.text_value()?;
4094        let mut hasher = std::collections::hash_map::DefaultHasher::new();
4095        text.hash(&mut hasher);
4096        Some(crate::caret::Signature {
4097            field,
4098            edit: self.runtime.edits.get(&field).copied().unwrap_or_default(),
4099            value: hasher.finish(),
4100        })
4101    }
4102
4103    /// Pointer movement, mouse or finger: continues a drag under way, and otherwise
4104    /// updates the hover.
4105    fn pointer_move(&mut self) {
4106        if self.drag.is_some() {
4107            self.handle_drag();
4108            return;
4109        }
4110        let hovered = self.sync_hover();
4111        // The system cursor follows the hovered sub-region (milestone 205): a hand
4112        // over a clickable icon, and so on. Recomputed on every move, since the
4113        // sub-region can change without the hovered widget changing.
4114        self.update_cursor_icon(hovered);
4115    }
4116
4117    /// Asks the frame what is under the hovering pointer, and records it.
4118    ///
4119    /// The only way the hover changes, so it can never name a widget of another frame:
4120    /// an id is a position in the tree, and the same position on the next screen is a
4121    /// different widget (milestone 505).
4122    fn sync_hover(&mut self) -> Option<WidgetId> {
4123        let hovered = self.ui.as_ref().and_then(|ui| self.hover.target(ui));
4124        if hovered != self.runtime.input.hovered {
4125            self.runtime.input.hovered = hovered;
4126            self.request_redraw();
4127        }
4128        hovered
4129    }
4130
4131    /// Whether work a component started has brought something since this shell last looked
4132    /// (milestone 585).
4133    fn take_task_arrivals(&mut self) -> bool {
4134        let now = frus_widgets::task_arrivals();
4135        std::mem::replace(&mut self.task_arrivals, now) != now
4136    }
4137
4138    /// Hands one raw pointer event to the widgets that hear them (milestone 586): a press to
4139    /// every one under the pointer, the innermost first, which then hear that pointer until
4140    /// it lifts, wherever it goes; a mouse moving with nothing held, to those under it.
4141    fn route_listeners(&mut self, kind: PointerKind, touch: bool) {
4142        use frus_widgets::ListenerEventKind as Raw;
4143        let at = self.cursor;
4144        let heard: Vec<(WidgetId, Rect, Raw)> = match kind {
4145            PointerKind::Down => {
4146                self.captured = self
4147                    .ui
4148                    .as_ref()
4149                    .map(|ui| ui.pointer_listeners_at(at))
4150                    .unwrap_or_default();
4151                self.captured
4152                    .iter()
4153                    .map(|(id, rect)| (*id, *rect, Raw::Down))
4154                    .collect()
4155            }
4156            PointerKind::Move if self.pointer_held => self
4157                .captured
4158                .iter()
4159                .map(|(id, rect)| (*id, *rect, Raw::Move))
4160                .collect(),
4161            PointerKind::Move if !touch => self
4162                .ui
4163                .as_ref()
4164                .map(|ui| ui.pointer_listeners_at(at))
4165                .unwrap_or_default()
4166                .into_iter()
4167                .map(|(id, rect)| (id, rect, Raw::Hover))
4168                .collect(),
4169            PointerKind::Move => Vec::new(),
4170            PointerKind::Up | PointerKind::Cancel => {
4171                let raw = if kind == PointerKind::Up {
4172                    Raw::Up
4173                } else {
4174                    Raw::Cancel
4175                };
4176                std::mem::take(&mut self.captured)
4177                    .into_iter()
4178                    .map(|(id, rect)| (id, rect, raw))
4179                    .collect()
4180            }
4181        };
4182        let messages: Vec<A::Message> = heard
4183            .into_iter()
4184            .flat_map(|(id, rect, kind)| {
4185                let event = frus_widgets::ListenerEvent {
4186                    kind,
4187                    local: Point::new(at.x - rect.x, at.y - rect.y),
4188                    touch,
4189                };
4190                self.tree
4191                    .as_ref()
4192                    .and_then(|tree| find_widget(tree.as_ref(), id))
4193                    .map(|widget| widget.on_pointer_event(event))
4194                    .unwrap_or_default()
4195            })
4196            .collect();
4197        for message in messages {
4198            self.dispatch(message);
4199            self.request_redraw();
4200        }
4201    }
4202
4203    /// Brings the regions the pointer is in up to date (milestone 583): an exit for each it has
4204    /// left, innermost first, an enter for each it has come into, outermost first, and — when
4205    /// the pointer `moved` with nothing held — a move for each it is still in.
4206    fn sync_regions(&mut self, moved: bool) {
4207        let point = self.hover.at();
4208        let now: Vec<(WidgetId, Rect)> = match (point, self.ui.as_ref()) {
4209            (Some(point), Some(ui)) => ui
4210                .hover_regions_at(point)
4211                .into_iter()
4212                .map(|(id, rect, _)| (id, rect))
4213                .collect(),
4214            _ => Vec::new(),
4215        };
4216        let at = point.unwrap_or(self.cursor);
4217        let local = |rect: Rect| Point::new(at.x - rect.x, at.y - rect.y);
4218        let before = std::mem::replace(&mut self.entered, now.clone());
4219        let mut events = Vec::new();
4220        for (id, rect) in &before {
4221            if !now.iter().any(|(n, _)| n == id) {
4222                events.push((
4223                    *id,
4224                    frus_widgets::HoverEvent::Exit {
4225                        local: local(*rect),
4226                    },
4227                ));
4228            }
4229        }
4230        for (id, rect) in now.iter().rev() {
4231            if !before.iter().any(|(b, _)| b == id) {
4232                events.push((
4233                    *id,
4234                    frus_widgets::HoverEvent::Enter {
4235                        local: local(*rect),
4236                    },
4237                ));
4238            } else if moved && !self.pointer_held {
4239                events.push((
4240                    *id,
4241                    frus_widgets::HoverEvent::Move {
4242                        local: local(*rect),
4243                    },
4244                ));
4245            }
4246        }
4247        let messages: Vec<_> = events
4248            .into_iter()
4249            .filter_map(|(id, event)| {
4250                self.tree
4251                    .as_ref()
4252                    .and_then(|tree| find_widget(tree.as_ref(), id))
4253                    .and_then(|widget| widget.on_hover_event(event))
4254            })
4255            .collect();
4256        for message in messages {
4257            self.dispatch(message);
4258            self.request_redraw();
4259        }
4260    }
4261
4262    /// The cursor to show: what the widget under the pointer asks for, `requested`, first;
4263    /// then the text cursor over a field that can be focused, as a platform shows it; then
4264    /// the innermost region the pointer is in that asks for one (milestone 583); otherwise
4265    /// the default.
4266    fn region_cursor(&self, requested: Option<UiCursor>) -> UiCursor {
4267        requested
4268            .or_else(|| {
4269                let point = self.hover.at()?;
4270                let (id, _) = self.ui.as_ref()?.focus_hit(point)?;
4271                let widget = find_widget(self.tree.as_ref()?.as_ref(), id)?;
4272                is_text_field(widget).then_some(UiCursor::Text)
4273            })
4274            .or_else(|| {
4275                self.hover.at().and_then(|point| {
4276                    self.ui
4277                        .as_ref()?
4278                        .hover_regions_at(point)
4279                        .into_iter()
4280                        .find_map(|(_, _, region)| region.cursor)
4281                })
4282            })
4283            .unwrap_or(UiCursor::Default)
4284    }
4285
4286    /// Applies the cursor shape the hovered widget asks for at the pointer's local
4287    /// position (milestone 205), and the default cursor otherwise. Translates
4288    /// `frus_widgets::Cursor` into winit's.
4289    fn update_cursor_icon(&mut self, hovered: Option<WidgetId>) {
4290        let requested = hovered.and_then(|id| {
4291            let rect = self.ui.as_ref()?.widget_rect(id)?;
4292            let widget = find_widget(self.tree.as_ref()?.as_ref(), id)?;
4293            widget.cursor_icon(
4294                self.cursor.x - rect.x,
4295                self.cursor.y - rect.y,
4296                rect.width,
4297                rect.height,
4298            )
4299        });
4300        let shown = self.region_cursor(requested);
4301        if let Some(window) = &self.window {
4302            window.set_cursor_visible(shown != UiCursor::None);
4303            window.set_cursor(cursor_icon(shown));
4304        }
4305        // Sub-region highlighting (milestone 208): the pointer's position is retained
4306        // while it hovers an interactive sub-region, that is, while cursor_icon answered.
4307        // A change repaints — the status hash includes hover_cursor — so the halo
4308        // follows the pointer or goes away.
4309        let hover_cursor = requested.map(|_| self.cursor);
4310        if hover_cursor != self.runtime.input.hover_cursor {
4311            self.runtime.input.hover_cursor = hover_cursor;
4312            self.request_redraw();
4313        }
4314    }
4315
4316    /// A pointer press, mouse or finger, at `self.cursor`. `touch` enables finger
4317    /// scrolling when no other gesture captures the press.
4318    fn pointer_down(&mut self, touch: bool) {
4319        self.pointer_touch = touch;
4320        // A first tap still waiting for its second is a tap after all if this press is not
4321        // that second one: somewhere else, or too late.
4322        self.poll_pending_tap(Instant::now());
4323        if let Some(pending) = self.pending_tap.as_ref() {
4324            let here = self.ui.as_ref().and_then(|ui| ui.hit(self.cursor));
4325            let (dx, dy) = (
4326                self.cursor.x - pending.position.x,
4327                self.cursor.y - pending.position.y,
4328            );
4329            if here != Some(pending.id) || dx.hypot(dy) > DOUBLE_TAP_SLOP {
4330                self.flush_pending_tap();
4331            }
4332        }
4333        self.pending_word = None;
4334        self.pending_region_hold = false;
4335        // A press **on the bar** comes first: the bar is drawn over everything, the handles
4336        // included, and the desktop menu hangs from the selection's top, over its handles
4337        // (milestone 618). It acts on the focused field, so nothing here may touch the
4338        // focus, the selection or the handles: it is only recorded, and `pointer_up`
4339        // resolves it to the button's action.
4340        if self
4341            .ui
4342            .as_ref()
4343            .is_some_and(|ui| ui.toolbar_contains(self.cursor))
4344        {
4345            press_at(&mut self.runtime, self.ui.as_ref(), self.cursor);
4346            self.request_redraw();
4347            return;
4348        }
4349        // Then a selection handle. It hangs below its line, over whatever is drawn
4350        // there, so nothing else may claim the press before it; and any other press puts
4351        // the handles away (milestone 511).
4352        // A selection area's handle likewise: a press on one widens or narrows the selection
4353        // rather than putting it away.
4354        if let Some(drag) = self.grab_region_handle() {
4355            self.drag = Some(drag);
4356            if let Some(selection) = self.runtime.region.as_mut() {
4357                selection.bar = false;
4358            }
4359            self.request_redraw();
4360            return;
4361        }
4362        if let Some(drag) = self.grab_selection_handle() {
4363            self.drag = Some(drag);
4364            // The bar is put away while a handle is dragged, and comes back when it is let
4365            // go (`pointer_up`): a bar that followed a moving selection would flicker.
4366            self.runtime.selection_toolbar = None;
4367            self.request_redraw();
4368            return;
4369        }
4370        if self.runtime.selection_handles.take().is_some() {
4371            self.request_redraw();
4372        }
4373        self.hide_selection_toolbar();
4374        // A selection an area holds goes with any press: the one that begins another puts a
4375        // new one in its place, below.
4376        if self.runtime.region.take().is_some() {
4377            self.request_redraw();
4378        }
4379        // 0) The back gesture: a press on the **leading edge** — left under LTR,
4380        // right under RTL — if the app allows it. It takes the pages back, so it starts on
4381        // them and not on what is above them: the window's menu bar, whose first word sits
4382        // in the edge, or an overlay — the menu it opens, a dialog (milestone 642).
4383        let above_the_pages = self.ui.as_ref().is_some_and(|ui| {
4384            ui.over_overlay(self.cursor)
4385                || ui
4386                    .title_bar_row()
4387                    .is_some_and(|row| row.contains(self.cursor))
4388        });
4389        let on_back_edge = !above_the_pages
4390            && if self.is_rtl() {
4391                self.cursor.x > self.logical_width() - BACK_EDGE
4392            } else {
4393                self.cursor.x < BACK_EDGE
4394            };
4395        if on_back_edge && self.app.can_go_back() {
4396            self.drag = Some(Drag::Back {
4397                start_x: self.cursor.x,
4398            });
4399            self.begin_gesture();
4400            self.build_dirty = true;
4401            self.app.back_gesture(0.0);
4402            self.request_redraw();
4403            return;
4404        }
4405
4406        // 1) Is this a scrollbar drag?
4407        if let Some(bar) = self.ui.as_ref().and_then(|ui| ui.scrollbar_at(self.cursor)) {
4408            let (along, thumb_start) = if bar.vertical {
4409                (self.cursor.y, bar.thumb.y)
4410            } else {
4411                (self.cursor.x, bar.thumb.x)
4412            };
4413            self.drag = Some(Drag::Scrollbar {
4414                id: bar.id,
4415                vertical: bar.vertical,
4416                grab: along - thumb_start,
4417                track_start: bar.track_start,
4418                track_len: bar.track_len,
4419                thumb_len: bar.thumb_len,
4420                max: bar.max,
4421                reverse: bar.reverse,
4422            });
4423            self.request_redraw();
4424            return;
4425        }
4426
4427        // 1b) Is this a draggable widget, a Slider for instance?
4428        if let Some((id, rect)) = self.ui.as_ref().and_then(|ui| ui.draggable_at(self.cursor)) {
4429            let (x, rtl) = (self.cursor.x, self.is_rtl());
4430            let track = self
4431                .tree
4432                .as_ref()
4433                .and_then(|tree| find_widget(tree.as_ref(), id))
4434                .map_or(frus_widgets::DragTrack::WHOLE, |widget| widget.drag_track());
4435            let active = track.engages(rect, x, rtl);
4436            let thumbs = matches!(track.mode, frus_widgets::DragMode::Thumbs { .. });
4437            let thumb = track.pick_thumb(rect, x, 0.0, rtl);
4438            self.drag = Some(Drag::Widget {
4439                id,
4440                rect,
4441                last_x: x,
4442                press_x: x,
4443                track,
4444                active,
4445                thumb,
4446            });
4447            if thumbs {
4448                // Two thumbs: the one the press took jumps to it, as one alone would.
4449                if let Some(thumb) = thumb {
4450                    let at = track.fraction(rect, x, rtl);
4451                    self.thumb_drag(id, thumb, frus_widgets::ThumbDrag::Start, at);
4452                    self.thumb_drag(id, thumb, frus_widgets::ThumbDrag::Move, at);
4453                }
4454            } else if active {
4455                // The bracket opens **before** the first value: an application that means
4456                // to hold the expensive work until the finger lifts has to know the finger
4457                // went down first, and a press that jumps the value straight away would
4458                // otherwise deliver the change before the start.
4459                self.dispatch_drag_edge(id, Edge::Start, 0.0);
4460                // A zero delta on press: only a slider whose press moves it jumps.
4461                self.apply_widget_drag(id, 0.0, track.value_at(rect, x, x, false, rtl));
4462            }
4463            self.request_redraw();
4464            return;
4465        }
4466
4467        // 1c) A reorder: a press on a reorderable — a table header, a Kanban card, a
4468        // list's grip. We do not `return` — focus and `pressed`, where a tap means sort,
4469        // are settled below; the drag engages only past the threshold, and otherwise the
4470        // release sorts.
4471        //
4472        // A row that asked for a **hold** takes nothing now. The press is left to
4473        // whatever else wants it, which inside a list is the scroll, and the deadline
4474        // below decides between them: a finger that stays put was never scrolling.
4475        if let Some((id, from, hold)) = self.reorderable_at(self.cursor) {
4476            if hold {
4477                self.pending_reorder = Some((id, from, self.cursor));
4478            } else {
4479                self.drag = Some(Drag::Reorder {
4480                    id,
4481                    from,
4482                    start: self.cursor,
4483                    moved: false,
4484                    carried: None,
4485                });
4486                self.reorder_x = self.cursor.x; // starts glued to the pointer, with no jerk
4487                self.reorder_y = self.cursor.y; // likewise for the vertical insertion line
4488            }
4489        }
4490
4491        if press_at(&mut self.runtime, self.ui.as_ref(), self.cursor) {
4492            self.request_redraw();
4493        }
4494        // 2) Focus and caret placement, and the start of a text selection.
4495        let previously_focused = self.runtime.input.focused;
4496        let focus = self.ui.as_ref().and_then(|ui| ui.focus_hit(self.cursor));
4497        // A press outside the focused widget tells it so, before the focus moves
4498        // (milestone 660).
4499        if let Some(held) = previously_focused {
4500            let inside = focus.is_some_and(|(id, _)| id == held);
4501            let outside = (!inside)
4502                .then(|| {
4503                    self.tree
4504                        .as_ref()
4505                        .and_then(|tree| find_widget(tree.as_ref(), held))
4506                        .and_then(|widget| widget.on_tap_outside())
4507                })
4508                .flatten();
4509            if let Some(message) = outside {
4510                self.dispatch(message);
4511            }
4512        }
4513        self.runtime.input.focused = focus.map(|(id, _)| id);
4514        if let Some((id, rect)) = focus {
4515            let local_x = self.cursor.x - rect.x;
4516            // The retained vertical scroll, in a multi-line field, is part of the
4517            // content coordinate: we add it so the click lands on the right line.
4518            let local_y =
4519                self.cursor.y - rect.y + self.runtime.scroll.get(&id).map(|s| s.1).unwrap_or(0.0);
4520            // The scroll shown just before this click, computed from the current caret
4521            // when the field was already focused, and 0 otherwise.
4522            let scroll_cursor = if previously_focused == Some(id) {
4523                self.runtime.edits.get(&id).map(|e| e.cursor).unwrap_or(0)
4524            } else {
4525                0
4526            };
4527            // Only **text fields** (`cursor_at` → `Some`) start a selection; the other
4528            // focusables — buttons, checkboxes — keep focus but must NOT capture the
4529            // click, which would otherwise be swallowed on release as the end of a drag.
4530            let cursor = self
4531                .tree
4532                .as_ref()
4533                .and_then(|tree| find_widget(tree.as_ref(), id))
4534                .and_then(|widget| widget.cursor_at(local_x, local_y, rect.width, scroll_cursor));
4535            if let Some(cursor) = cursor {
4536                // Placing the caret with the mouse forgets the vertical goal column.
4537                self.goal_x = None;
4538                self.runtime.edits.insert(
4539                    id,
4540                    Edit {
4541                        cursor,
4542                        anchor: None,
4543                        composing: None,
4544                    },
4545                );
4546                self.drag = Some(Drag::TextSelect { id, rect });
4547                // A finger that stays put selects the word instead (milestone 511).
4548                if touch {
4549                    self.pending_word = Some(id);
4550                }
4551                // The caret moved, so the run of typing ends here: typing at one place,
4552                // then at another, then Ctrl+Z should take back only the second.
4553                self.runtime.close_edit_run(id);
4554                // Tapping in a field **reopens** the keyboard, even one the app already
4555                // considers shown but which the system back closed — see
4556                // `request_soft_input`.
4557                self.request_soft_input();
4558
4559                // A double click selects the word under the pointer.
4560                let now = Instant::now();
4561                let double = self
4562                    .last_click_time
4563                    .map(|t| (now - t).as_secs_f32() < 0.4)
4564                    .unwrap_or(false);
4565                self.last_click_time = Some(now);
4566                if double {
4567                    if let Some((start, end)) = self
4568                        .tree
4569                        .as_ref()
4570                        .and_then(|tree| find_widget(tree.as_ref(), id))
4571                        .and_then(|widget| widget.word_at(cursor))
4572                    {
4573                        self.runtime.edits.insert(
4574                            id,
4575                            Edit {
4576                                cursor: end,
4577                                anchor: Some(start),
4578                                composing: None,
4579                            },
4580                        );
4581                        self.drag = None;
4582                    }
4583                }
4584            }
4585        }
4586
4587        // 2b) Words in a selection area, with nothing else having a claim on the press: a
4588        // selection begins, and may be dragged across the texts of the area. A finger is
4589        // left to scroll, and a finger that stays put selects the word instead (milestone 576).
4590        if self.drag.is_none() && focus.is_none() {
4591            if touch {
4592                self.pending_region_hold = self.region_press_target().is_some();
4593            } else {
4594                self.begin_region_selection();
4595            }
4596        }
4597
4598        // 3) Touch: when nothing captured the gesture — no scrollbar, no widget, no
4599        // text selection — prepare a finger scroll on the area under the finger. A
4600        // release without movement, under TOUCH_SLOP, stays a tap.
4601        if touch && self.drag.is_none() {
4602            // The innermost area that would actually take a finger. An area whose content
4603            // fits refuses the offset outright — the reference takes its drag recognisers
4604            // away for exactly this — so it neither swallows the press nor lights an edge,
4605            // and the page behind it, or a dismissible row under it, gets its turn.
4606            let landed = {
4607                let scroll = &self.runtime.scroll;
4608                self.ui.as_ref().and_then(|ui| {
4609                    ui.scroll_chain(self.cursor).find(|area| {
4610                        area.accepts_user_offset(
4611                            scroll.get(&area.id).copied().unwrap_or((0.0, 0.0)),
4612                        )
4613                    })
4614                })
4615            };
4616            if let Some(area) = landed {
4617                // A finger back on the content catches it: the fling stops where it
4618                // is rather than sliding under the finger, and hands the next
4619                // release whatever momentum the platform lets a swipe build on.
4620                let physics = area.physics;
4621                let carried = self.runtime.catch_scroll_fling(area.id, physics);
4622                // The offset belongs to the finger until it lifts.
4623                self.runtime.hold_scroll(area.id);
4624                // And so does the sheet it sits in, if one is still settling: the finger
4625                // may be about to move it, and a sheet sliding away under a finger that
4626                // has caught its list is a sheet that ignored the catch.
4627                if let Some(sheet) = self
4628                    .ui
4629                    .as_ref()
4630                    .and_then(|ui| ui.sheet_holding(area.id))
4631                    .map(|sheet| sheet.id)
4632                {
4633                    self.runtime.sheet_hold(sheet);
4634                }
4635                self.drag = Some(Drag::Scroll {
4636                    id: area.id,
4637                    last: self.cursor,
4638                    moved: false,
4639                    carried,
4640                    dismiss: self
4641                        .ui
4642                        .as_ref()
4643                        .and_then(|ui| ui.dismissable_at(self.cursor)),
4644                    axis: None,
4645                    pan: self.pan_target(),
4646                });
4647                self.begin_gesture();
4648                // A fling on a scroll area is read the way its scroll behaviour reads one
4649                // (milestone 616), as the reference's scrollable builds its recognizer.
4650                self.gesture_velocity = area.velocity_tracker();
4651            }
4652        }
4653
4654        // 3a) A sheet's panel with nothing under the finger that scrolls: the finger moves
4655        // the sheet itself. A pointer too — on a desktop there is no other way to raise one.
4656        if self.drag.is_none() {
4657            if let Some((id, available)) = self
4658                .ui
4659                .as_ref()
4660                .and_then(|ui| ui.sheet_at(self.cursor))
4661                .map(|sheet| (sheet.id, sheet.available))
4662            {
4663                self.runtime.sheet_hold(id);
4664                self.drag = Some(Drag::Sheet {
4665                    id,
4666                    last: self.cursor,
4667                    moved: false,
4668                    available,
4669                });
4670                self.begin_gesture();
4671            }
4672        }
4673
4674        // 3b) A dismissible item with nothing scrollable under it: there is no gesture to
4675        // arbitrate against, so the swipe is prepared directly. It still waits for the
4676        // threshold, or a tap on the row would start sliding it.
4677        if self.drag.is_none() {
4678            if let Some(item) = self
4679                .ui
4680                .as_ref()
4681                .and_then(|ui| ui.dismissable_at(self.cursor))
4682            {
4683                self.drag = Some(Drag::Dismiss {
4684                    item,
4685                    last: self.cursor,
4686                    moved: false,
4687                });
4688                self.begin_gesture();
4689            }
4690        }
4691
4692        // 3c) A draggable item, when nothing above has taken the gesture. It comes
4693        // **after** the touch scroll on purpose: a widget that took every drag inside a
4694        // list would silently stop that list scrolling, and a list that does not scroll
4695        // is a worse bug than an item that does not lift. With a pointer there is no
4696        // touch scroll to lose to, so it lifts everywhere.
4697        if self.drag.is_none() {
4698            if let Some(source) = self
4699                .ui
4700                .as_ref()
4701                .and_then(|ui| ui.drag_source_at(self.cursor))
4702                .filter(|source| {
4703                    // One that asked for a hold waits for the deadline instead.
4704                    !self
4705                        .tree
4706                        .as_ref()
4707                        .and_then(|tree| find_widget(tree.as_ref(), source.id))
4708                        .is_some_and(|widget| widget.drag_needs_long_press())
4709                })
4710            {
4711                self.drag = Some(Drag::Item {
4712                    source,
4713                    start: self.cursor,
4714                    moved: false,
4715                    over: None,
4716                });
4717                self.begin_gesture();
4718            }
4719        }
4720
4721        // 3d) A detector that takes a drag, with no scroll under it to share the gesture with
4722        // (milestone 582): prepared, and engaged past the slop, so a tap still goes through.
4723        if self.drag.is_none() {
4724            if let Some((id, axis)) = self.pan_target() {
4725                self.drag = Some(Drag::Gesture {
4726                    id,
4727                    axis,
4728                    start: self.cursor,
4729                    last: self.cursor,
4730                    moved: false,
4731                });
4732                self.begin_gesture();
4733            }
4734        }
4735
4736        // 4) An interactive viewport (`InteractiveViewer`) under the pointer: prepare a
4737        // **pan**, with mouse or finger. Like the touch scroll it engages only past the
4738        // threshold, so a tap goes through to the child, a button for instance.
4739        if self.drag.is_none() {
4740            if let Some((id, viewport)) = self
4741                .ui
4742                .as_ref()
4743                .and_then(|ui| ui.interactive_at(self.cursor))
4744            {
4745                // The press stops a fling in progress: we take the content back in hand.
4746                self.runtime.interactive_velocity.remove(&id);
4747                self.drag = Some(Drag::Pan {
4748                    id,
4749                    last: self.cursor,
4750                    moved: false,
4751                    viewport,
4752                });
4753                self.begin_gesture();
4754            }
4755        }
4756        self.request_redraw();
4757    }
4758
4759    /// A pointer release, mouse or finger: it ends a drag, or commits a click or tap
4760    /// when the release lands back on the widget that was pressed.
4761    /// What widget `id` emits for a click at the pointer on one of its sub-regions — see
4762    /// [`Widget::positional_click`]. Local coordinates are the pointer minus the widget's
4763    /// corner.
4764    fn positional_click_on(&self, id: WidgetId) -> Option<A::Message> {
4765        let rect = self.ui.as_ref()?.widget_rect(id)?;
4766        let widget = find_widget(self.tree.as_ref()?.as_ref(), id)?;
4767        widget.positional_click(
4768            self.cursor.x - rect.x,
4769            self.cursor.y - rect.y,
4770            rect.width,
4771            rect.height,
4772        )
4773    }
4774
4775    fn pointer_up(&mut self) {
4776        let ended = self.drag.take();
4777        if let Some(Drag::Back { .. }) = ended {
4778            // The app decides — commit or cancel — from the velocity, which it wants
4779            // in **fractions of the screen** per second, not pixels.
4780            let sign = if self.is_rtl() { -1.0 } else { 1.0 };
4781            let velocity = sign * self.gesture_estimate().velocity.x / self.logical_width();
4782            self.build_dirty = true;
4783            self.app.back_gesture_end(velocity);
4784            self.request_redraw();
4785            return;
4786        }
4787        // A touch scroll, a pan or a swipe that never moved is a plain tap: we let it
4788        // follow the ordinary click path below.
4789        let was_tap = gesture_was_a_tap(ended.as_ref());
4790        // The bracket closes. A press that never moved still gets its end: it changed
4791        // the value once, and a caller that defers its expensive work until the release
4792        // would otherwise never be told the release happened.
4793        if let Some(Drag::Widget {
4794            id,
4795            rect,
4796            press_x,
4797            track,
4798            active: true,
4799            thumb,
4800            ..
4801        }) = ended
4802        {
4803            let rtl = self.is_rtl();
4804            if matches!(track.mode, frus_widgets::DragMode::Thumbs { .. }) {
4805                if let Some(thumb) = thumb {
4806                    let at = track.fraction(rect, self.cursor.x, rtl);
4807                    self.thumb_drag(id, thumb, frus_widgets::ThumbDrag::End, at);
4808                }
4809            } else {
4810                let released = track.released_at(rect, press_x, self.cursor.x, rtl);
4811                self.dispatch_drag_edge(id, Edge::End, released);
4812            }
4813        }
4814        // The keyboard is told where a dragged handle left the selection.
4815        #[cfg(android)]
4816        if let Some(Drag::SelectionHandle { id, .. }) = ended {
4817            self.push_ime_context(id);
4818        }
4819        if let Some(Drag::SelectionHandle { id, .. }) = ended {
4820            self.show_selection_toolbar(id);
4821        }
4822        // A handle of a selection area let go: its bar comes back over what is selected now.
4823        if let Some(Drag::RegionHandle { .. }) = ended {
4824            if let Some(selection) = self.runtime.region.as_mut() {
4825                selection.bar = true;
4826            }
4827        }
4828        // Reordering: on the drop, the target column is the reorderable header under
4829        // the pointer, and we route the grabbed header's `on_reorder(from, to)`.
4830        if let Some(Drag::Reorder {
4831            id,
4832            from,
4833            moved: true,
4834            carried,
4835            ..
4836        }) = &ended
4837        {
4838            let target = self.reorder_drop_target(*id, *from, *carried);
4839            let tree = self.tree.as_ref();
4840            let base = target
4841                .and_then(|tid| tree.and_then(|t| find_widget(t.as_ref(), tid)))
4842                .and_then(|widget| widget.reorder_index());
4843            // The **trailing** half of a hovered target that is inserted at — the lower
4844            // half down a list, the right half across one (the left, right to left) —
4845            // means inserting **after** it, index +1: the effective drop slot follows the
4846            // insertion line that was painted. No effect for `Table` columns, or off target.
4847            let to = match (base, target) {
4848                (Some(base), Some(tid)) => {
4849                    let after = self
4850                        .ui
4851                        .as_ref()
4852                        .and_then(|ui| ui.widget_rect(tid))
4853                        .map(|rect| self.reorder_insert_after(tid, rect))
4854                        .unwrap_or(false);
4855                    Some(base + after as usize)
4856                }
4857                _ => None,
4858            };
4859            // Dropping **onto itself** moves nothing, even in the lower half, where
4860            // `to = from + 1` would slip past the `to != from` guard — otherwise we would
4861            // emit a null move and announce it.
4862            let self_drop = target == Some(*id);
4863            let message = match to {
4864                Some(to) if to != *from && !self_drop => tree
4865                    .and_then(|t| find_widget(t.as_ref(), *id))
4866                    .and_then(|widget| widget.on_reorder(to)),
4867                _ => None,
4868            };
4869            if let Some(message) = message {
4870                let to = to.unwrap_or(*from);
4871                let widget = tree.and_then(|t| find_widget(t.as_ref(), *id));
4872                let axis = widget
4873                    .map(|w| w.reorder_axis())
4874                    .unwrap_or(ReorderAxis::Horizontal);
4875                // The move is spoken to the screen reader — the ghost's counterpart for
4876                // a blind user. A widget whose index **is** a position says so itself; a
4877                // `Kanban` card's is a flat `column × stride + position` that means
4878                // nothing read aloud, so the fallback speaks of the axis and gives no
4879                // number at all.
4880                let announcement = widget
4881                    .and_then(|w| w.reorder_announcement(to))
4882                    .unwrap_or_else(|| match axis {
4883                        ReorderAxis::Horizontal => format!("Column moved to position {}", to + 1),
4884                        ReorderAxis::Vertical => "Card moved".to_string(),
4885                    });
4886                self.dispatch(message);
4887                self.set_announcement(announcement);
4888            }
4889        }
4890        // Fling: the finger's momentum is handed to the area's physics, which
4891        // returns the motion that follows — a spline that stops at the edge, or
4892        // friction that hands over to a spring and bounces.
4893        // A swiped item: the release velocity decides between flying out and sliding
4894        // back, using the same fitted estimate a fling uses.
4895        if let Some(Drag::Dismiss { item, moved, .. }) = &ended {
4896            if *moved {
4897                let estimate = self.gesture_estimate();
4898                let horizontal = item.spec.axis.is_horizontal();
4899                let (along, across) = if horizontal {
4900                    (estimate.velocity.x, estimate.velocity.y)
4901                } else {
4902                    (estimate.velocity.y, estimate.velocity.x)
4903                };
4904                self.runtime.dismiss_release(
4905                    item.id,
4906                    along,
4907                    across,
4908                    item.spec.axis,
4909                    item.spec.threshold,
4910                );
4911                self.request_redraw();
4912                return;
4913            }
4914        }
4915        // A sheet let go of by its panel settles from wherever the finger left it — and
4916        // one only pressed settles too, since the press caught it if it was moving.
4917        if let Some(Drag::Sheet {
4918            id,
4919            moved,
4920            available,
4921            ..
4922        }) = &ended
4923        {
4924            let velocity = if *moved {
4925                -self.fling_velocity(self.gesture_estimate()).1
4926            } else {
4927                0.0
4928            };
4929            // Thrown by its handle, a sheet with one list in it carries that list on at the
4930            // top, as the reference's does: there its whole content is the list.
4931            let sheet = self.ui.as_ref().and_then(|ui| ui.sheet(*id)).map(|sheet| {
4932                let list = match sheet.areas.as_slice() {
4933                    [only] => Some(*only),
4934                    _ => None,
4935                };
4936                (sheet.spec.clone(), list)
4937            });
4938            if let Some((spec, list)) = sheet {
4939                self.runtime
4940                    .sheet_release(*id, &spec, *available, velocity, list);
4941            }
4942            if *moved {
4943                self.request_redraw();
4944                return;
4945            }
4946        }
4947        if let Some(Drag::Item {
4948            source,
4949            moved: true,
4950            over,
4951            ..
4952        }) = &ended
4953        {
4954            // The drop, then the source's own answer. In that order: an application
4955            // that reacts to both should see the thing arrive before it is told the
4956            // journey is over.
4957            self.runtime.drag_over = None;
4958            let accepted = over.is_some();
4959            let dropped = over.and_then(|id| {
4960                self.tree
4961                    .as_ref()
4962                    .and_then(|tree| find_widget(tree.as_ref(), id))
4963                    .and_then(|widget| widget.on_drop(source.payload))
4964            });
4965            let ended_msg = self
4966                .tree
4967                .as_ref()
4968                .and_then(|tree| find_widget(tree.as_ref(), source.id))
4969                .and_then(|widget| widget.on_dropped(accepted));
4970            for message in dropped.into_iter().chain(ended_msg) {
4971                self.dispatch(message);
4972            }
4973            self.request_redraw();
4974            return;
4975        }
4976        if let Some(Drag::Item { .. }) = &ended {
4977            // Lifted but never moved: nothing was dropped, and the click below still
4978            // has its say.
4979            self.runtime.drag_over = None;
4980        }
4981        // Set when a sheet takes the release of a list inside it, which then does not
4982        // fling as well (milestone 515).
4983        let mut sheet_took = false;
4984        if let Some(Drag::Scroll { id, .. }) = &ended {
4985            let sheet = self
4986                .ui
4987                .as_ref()
4988                .and_then(|ui| ui.sheet_holding(*id))
4989                .cloned();
4990            if let Some(sheet) = sheet {
4991                // In the sheet's terms, positive growing it: a list's offset grows the
4992                // same way, on an axis that is not reversed.
4993                let velocity = match &ended {
4994                    Some(Drag::Scroll {
4995                        moved: true,
4996                        axis: Some(true),
4997                        carried,
4998                        ..
4999                    }) => -self.fling_velocity(self.gesture_estimate()).1 + carried.1,
5000                    _ => 0.0,
5001                };
5002                let offset = self.runtime.scroll.get(id).map_or(0.0, |o| o.1);
5003                let size = self.runtime.sheet_size(sheet.id, &sheet.spec);
5004                sheet_took = velocity != 0.0
5005                    && frus_widgets::sheet_takes_release(
5006                        velocity,
5007                        offset,
5008                        size,
5009                        &sheet.spec,
5010                        sheet.available,
5011                    );
5012                // Whoever takes it, the sheet is let go of: settled by the throw, or
5013                // from where it is — which is a stop, whenever the list keeps the throw.
5014                self.runtime.sheet_release(
5015                    sheet.id,
5016                    &sheet.spec,
5017                    sheet.available,
5018                    if sheet_took { velocity } else { 0.0 },
5019                    Some(*id),
5020                );
5021            }
5022        }
5023        if let Some(Drag::Scroll { id, .. }) = &ended {
5024            // The finger gives the offset back before anything is flung at it.
5025            self.runtime.release_scroll(*id);
5026            // A pull held against an edge now fades at the "let go" rate.
5027            self.runtime.glow_scroll_end(*id);
5028            // And a pull-to-refresh gesture is answered: releasing it armed is the
5029            // whole point of the gesture, so the message goes out now rather than
5030            // after the indicator has finished settling.
5031            self.release_refresh(*id);
5032        }
5033        if let Some(Drag::Scroll {
5034            id,
5035            moved: true,
5036            carried,
5037            axis,
5038            ..
5039        }) = &ended
5040        {
5041            // In **scroll space**: the content moves opposite the finger — and along the
5042            // axis the gesture was claimed by, or the release would fling the way the drag
5043            // was not allowed to go.
5044            let estimate = self.gesture_estimate();
5045            let velocity = self.fling_velocity(estimate);
5046            // Through the same conversion the drag went through, so the release carries
5047            // on the way the finger was going — on a reversed axis too.
5048            let velocity = match self.ui.as_ref().and_then(|u| u.scroll_region(*id)) {
5049                Some(area) => area.offset_delta(velocity),
5050                None => (-velocity.0, -velocity.1),
5051            };
5052            // The momentum carried in from an interrupted fling is masked with the rest:
5053            // a gesture held to one axis must not launch along the other on the strength
5054            // of what the *previous* one was doing.
5055            let launch = (velocity.0 + carried.0, velocity.1 + carried.1);
5056            if sheet_took {
5057                self.request_redraw();
5058            } else {
5059                self.fling(
5060                    *id,
5061                    match axis {
5062                        Some(true) => (0.0, launch.1),
5063                        Some(false) => (launch.0, 0.0),
5064                        None => launch,
5065                    },
5066                );
5067            }
5068        }
5069        // A detector's drag let go: its end, with the velocity it was let go at (milestone
5070        // 582). One that never moved was a tap, and goes on to the click below.
5071        if let Some(Drag::Gesture {
5072            id,
5073            axis,
5074            moved: true,
5075            ..
5076        }) = &ended
5077        {
5078            let velocity = self.fling_velocity(self.gesture_estimate());
5079            let velocity = mask_pan(*axis, Point::new(velocity.0, velocity.1));
5080            // No faster than a finger can throw: the reference's `kMaxFlingVelocity`.
5081            let speed = velocity.x.hypot(velocity.y);
5082            let velocity = if speed > MAX_PAN_VELOCITY {
5083                let k = MAX_PAN_VELOCITY / speed;
5084                Point::new(velocity.x * k, velocity.y * k)
5085            } else {
5086                velocity
5087            };
5088            self.send_pan(*id, frus_widgets::PanEvent::End { velocity });
5089        }
5090        // A pan fling: the momentum launches the content, which `advance_interactive`
5091        // decelerates and bounds frame by frame.
5092        if let Some(Drag::Pan {
5093            id, moved: true, ..
5094        }) = &ended
5095        {
5096            let estimate = self.gesture_estimate();
5097            let velocity = self.fling_velocity(estimate);
5098            if velocity.0.hypot(velocity.1) > PAN_FLING_MIN {
5099                self.runtime.interactive_velocity.insert(*id, velocity);
5100            }
5101        }
5102        if ended.is_some() && !was_tap {
5103            self.request_redraw();
5104            return;
5105        }
5106        // A click only counts when press and release land on the same widget.
5107        let released = self.ui.as_ref().and_then(|ui| ui.hit(self.cursor));
5108        // A built-in button of the selection bar sends no message: the shell performs its
5109        // action on the field the bar is open on (milestone 568).
5110        let bar_action = match (self.runtime.input.pressed, released) {
5111            (Some(pressed), Some(released)) if pressed == released => {
5112                self.ui.as_ref().and_then(|ui| ui.edit_action_for(pressed))
5113            }
5114            _ => None,
5115        };
5116        let on_bar = self
5117            .ui
5118            .as_ref()
5119            .is_some_and(|ui| ui.toolbar_contains(self.cursor));
5120        // A field answers no tap of its own, so it is no hit target and `released` never
5121        // names it: its clickable suffix is asked through the focusable the press landed
5122        // on, when the release lands on it too. It wins over a tappable parent, the way
5123        // the inner target always does.
5124        let field_positional = if bar_action.is_some() {
5125            None
5126        } else {
5127            self.ui
5128                .as_ref()
5129                .and_then(|ui| ui.focus_hit(self.cursor))
5130                .map(|(id, _)| id)
5131                .filter(|id| self.runtime.input.focused == Some(*id))
5132                .and_then(|id| self.positional_click_on(id))
5133        };
5134        let (message, announce) = match (self.runtime.input.pressed, released) {
5135            (Some(_), Some(_)) if bar_action.is_some() => (None, None),
5136            _ if field_positional.is_some() => (field_positional, None),
5137            (Some(pressed), Some(released)) if pressed == released => {
5138                // A **positional** click, on a sub-region such as a chart's point, takes
5139                // priority over `on_click`.
5140                let positional = self.positional_click_on(released);
5141                let message =
5142                    positional.or_else(|| self.ui.as_ref().and_then(|ui| ui.msg_for(pressed)));
5143                // The spoken announcement of the effect — a sort, a selection — read off
5144                // the clicked widget before `dispatch` rebuilds the tree.
5145                let announce = self
5146                    .tree
5147                    .as_ref()
5148                    .and_then(|tree| find_widget(tree.as_ref(), pressed))
5149                    .and_then(|widget| widget.announce());
5150                (message, announce)
5151            }
5152            _ => (None, None),
5153        };
5154        // Something that takes a double tap: this tap is its second, or a first that waits.
5155        let message = match (self.runtime.input.pressed, released) {
5156            (Some(pressed), Some(released)) if pressed == released && bar_action.is_none() => {
5157                self.double_tap(pressed, message)
5158            }
5159            _ => message,
5160        };
5161        // The tap completed on the widget it started on: its ink finishes growing and
5162        // fades. Anything else leaves the splash unconfirmed, and `advance_ink` sweeps
5163        // it away quickly — the finger slid off, and the ink says so.
5164        if let (Some(pressed), Some(released)) = (self.runtime.input.pressed, released) {
5165            if pressed == released {
5166                self.runtime.ink_confirm(pressed);
5167            }
5168        }
5169        self.runtime.input.pressed = None;
5170        if let Some(action) = bar_action {
5171            self.perform_edit_action(action);
5172        } else if on_bar && message.is_some() {
5173            // An item of the application's own: it has sent its message, and the bar is
5174            // done with.
5175            self.runtime.selection_handles = None;
5176            self.hide_selection_toolbar();
5177        }
5178        if let Some(message) = message {
5179            self.dispatch(message);
5180            if let Some(announce) = announce {
5181                self.set_announcement(announce);
5182            }
5183        }
5184        self.request_redraw();
5185    }
5186
5187    /// A tap on `id`, which answered `message`. If `id` also takes a double tap, the tap is
5188    /// either the second of one, and the double tap's message is sent in its place, or a
5189    /// first, which waits for [`DOUBLE_TAP_TIMEOUT`] before it is a tap (milestone 581).
5190    fn double_tap(&mut self, id: WidgetId, message: Option<A::Message>) -> Option<A::Message> {
5191        let Some(double) = self
5192            .tree
5193            .as_ref()
5194            .and_then(|tree| find_widget(tree.as_ref(), id))
5195            .and_then(|widget| widget.on_double_tap())
5196        else {
5197            return message;
5198        };
5199        let now = Instant::now();
5200        let second = self.pending_tap.as_ref().is_some_and(|pending| {
5201            let (dx, dy) = (
5202                self.cursor.x - pending.position.x,
5203                self.cursor.y - pending.position.y,
5204            );
5205            pending.id == id
5206                && now.duration_since(pending.at) <= DOUBLE_TAP_TIMEOUT
5207                && dx.hypot(dy) <= DOUBLE_TAP_SLOP
5208        });
5209        if second {
5210            self.pending_tap = None;
5211            return Some(double);
5212        }
5213        self.flush_pending_tap();
5214        self.pending_tap = Some(PendingTap {
5215            id,
5216            at: now,
5217            position: self.cursor,
5218            single: message,
5219        });
5220        None
5221    }
5222
5223    /// Sends the waiting first tap's own message, if one is waiting.
5224    fn flush_pending_tap(&mut self) {
5225        if let Some(message) = self.pending_tap.take().and_then(|pending| pending.single) {
5226            self.dispatch(message);
5227            self.request_redraw();
5228        }
5229    }
5230
5231    /// The waiting first tap, sent if its wait is over by `now`. Whether one was.
5232    fn poll_pending_tap(&mut self, now: Instant) -> bool {
5233        let due = self
5234            .pending_tap
5235            .as_ref()
5236            .is_some_and(|pending| now.duration_since(pending.at) >= DOUBLE_TAP_TIMEOUT);
5237        if due {
5238            self.flush_pending_tap();
5239        }
5240        due
5241    }
5242
5243    /// Speaks a message aloud through the screen reader's **live region**, for a
5244    /// column reorder and the like. With no screen reader running it costs nothing. The
5245    /// text is re-spoken only on a change. Desktop and web; a no-op on Android and iOS,
5246    /// which have no bridge yet.
5247    #[cfg(any(desktop, web))]
5248    fn set_announcement(&mut self, message: String) {
5249        self.announce = message;
5250    }
5251    #[cfg(not(any(desktop, web)))]
5252    fn set_announcement(&mut self, _message: String) {}
5253
5254    /// Returning focus when an overlay closes: if the focused widget has **vanished**
5255    /// from the frame, a menu or modal having closed, focus goes back to the
5256    /// **trigger** — the most recent focusable in the history that is still present.
5257    /// Every transition is recorded along the way: the old focus, if still present,
5258    /// becomes a candidate trigger. The focus ring is drawn on the following frame
5259    /// (`request_redraw` when focus moved).
5260    fn reconcile_focus(&mut self) {
5261        let present: std::collections::HashSet<WidgetId> = match self.ui.as_ref() {
5262            Some(ui) => ui.focusable_ids().collect(),
5263            None => return,
5264        };
5265        // A widget that takes the focus on appearing, and has just appeared, takes it. The
5266        // focus it takes it from is recorded below as any move is, so it goes back there
5267        // when this one goes: a submenu closing hands it back to its row.
5268        let autofocus: Vec<WidgetId> = self
5269            .ui
5270            .as_ref()
5271            .map(|ui| ui.autofocus_ids().collect())
5272            .unwrap_or_default();
5273        let appeared = autofocus
5274            .iter()
5275            .find(|id| !self.autofocused.contains(id))
5276            .copied();
5277        self.autofocused = autofocus.into_iter().collect();
5278        if let Some(id) = appeared {
5279            self.runtime.input.focused = Some(id);
5280        }
5281        let before = self.runtime.input.focused;
5282        let after = resolve_focus(
5283            before,
5284            &present,
5285            &mut self.focus_history,
5286            &mut self.prev_focus,
5287        );
5288        if after != before || appeared.is_some() {
5289            self.runtime.input.focused = after;
5290            self.request_redraw();
5291        }
5292    }
5293
5294    /// Tells the widgets **where the focus went** since they were last told (milestone 660):
5295    /// each one whose subtree it left or entered, through `Widget::on_focus_change`.
5296    fn report_focus(&mut self) {
5297        let now = self.runtime.input.focused;
5298        // Nothing moved, nothing to say — and no walk of the tree, which is the point: the
5299        // answer would be empty anyway.
5300        if now == self.focus_reported {
5301            return;
5302        }
5303        let before = std::mem::replace(&mut self.focus_reported, now);
5304        let messages = self
5305            .tree
5306            .as_ref()
5307            .map(|tree| focus_changes(tree.as_ref(), before, now))
5308            .unwrap_or_default();
5309        for message in messages {
5310            self.dispatch(message);
5311        }
5312    }
5313
5314    /// Applies a message to the application, runs its effects, then re-evaluates the
5315    /// subscriptions, the state having possibly changed which ones should run.
5316    fn dispatch(&mut self, message: A::Message) {
5317        // The app may have changed state, so the `view` must be rebuilt.
5318        self.build_dirty = true;
5319        let command = self.app.update(message);
5320        self.run_command(command);
5321        let asked = self.app.effects();
5322        self.run_command(asked);
5323        #[cfg(web)]
5324        self.sync_address();
5325        #[cfg(any(desktop, android))]
5326        self.report_dev();
5327        self.sync_subscriptions();
5328    }
5329
5330    /// The link the program was started with — on Android the activity's intent, on a desktop an
5331    /// argument — handed to the application as a location. Once, before the first frame.
5332    #[cfg(any(android, desktop))]
5333    fn open_launch_link(&mut self) {
5334        #[cfg(android)]
5335        let link = self
5336            .android_app
5337            .as_ref()
5338            .and_then(crate::android_link::launch_link);
5339        // A link on the command line, or where `frus run` restarted the application from.
5340        #[cfg(desktop)]
5341        let link = crate::link::link_among(std::env::args()).or_else(crate::dev::restore_link);
5342        if let Some(location) = link.as_deref().and_then(crate::link::location_of_link) {
5343            let command = self.app.open_location(&location);
5344            self.run_command(command);
5345        }
5346    }
5347
5348    /// Opens the channel to `frus run`, when it started this application with one.
5349    #[cfg(any(android, desktop))]
5350    fn connect_dev(&mut self) {
5351        #[cfg(desktop)]
5352        {
5353            self.dev = crate::dev::address()
5354                .as_deref()
5355                .and_then(crate::dev::DevChannel::connect);
5356            crate::dev_commands::set_window(self.window.clone());
5357            if let Some(states) = crate::dev::restored_states() {
5358                self.runtime.states.restore_from(&states);
5359            }
5360        }
5361        // On Android the extra only says that `frus run` is listening, and it listens to the log.
5362        #[cfg(android)]
5363        {
5364            let asked = self
5365                .android_app
5366                .as_ref()
5367                .and_then(|app| crate::android_link::launch_extra(app, crate::dev::DEV_EXTRA));
5368            // Started by `frus run --hot`'s engine, which cannot add the extra (milestone 680).
5369            #[cfg(feature = "hot-reload")]
5370            let asked = asked.or_else(|| crate::hot::under_engine().then(String::new));
5371            self.dev = asked.map(|_| crate::dev::DevChannel::through_log());
5372            if self.dev.is_some() {
5373                let storage = self
5374                    .android_app
5375                    .as_ref()
5376                    .and_then(|app| app.internal_data_path());
5377                if let Some(states) = storage.as_deref().and_then(crate::dev::restored_states) {
5378                    self.runtime.states.restore_from(&states);
5379                }
5380                if let Some(dir) = storage {
5381                    crate::dev_commands::poll_commands(dir);
5382                }
5383            }
5384        }
5385    }
5386
5387    /// Tells `frus run` where the application is and where its window sits, when either
5388    /// changed; a channel that can no longer be written to is closed.
5389    #[cfg(any(android, desktop))]
5390    fn report_dev(&mut self) {
5391        let Some(dev) = self.dev.as_mut() else {
5392            return;
5393        };
5394        #[cfg(desktop)]
5395        let window = self.window.as_ref().and_then(|window| {
5396            let at = window.outer_position().ok()?;
5397            let size = window.inner_size();
5398            Some([at.x, at.y, size.width as i32, size.height as i32])
5399        });
5400        #[cfg(android)]
5401        let window = None;
5402        if !dev.report(self.app.location(), window) {
5403            self.dev = None;
5404            return;
5405        }
5406        let states = &self.runtime.states;
5407        dev.save_if_asked(|| states.snapshot());
5408    }
5409
5410    /// The links a second process handed to this one since the last frame.
5411    #[cfg(desktop)]
5412    fn open_links_from_other_instances(&mut self) {
5413        for link in crate::instance::take_links() {
5414            if let Some(location) = crate::link::location_of_link(&link) {
5415                let command = self.app.open_location(&location);
5416                self.run_command(command);
5417                self.build_dirty = true;
5418                self.request_redraw();
5419            }
5420        }
5421    }
5422
5423    /// The address the page was opened at, handed to the application, and the browser's own
5424    /// moves from here on listened to. Once, before the first frame.
5425    #[cfg(web)]
5426    fn open_launch_address(&mut self, window: &Arc<Window>) {
5427        let strategy = self.app.location_strategy();
5428        let base = crate::address::base();
5429        let (location, index) = crate::address::current(strategy, &base);
5430        self.history.opened(location.clone(), index);
5431        crate::address::listen(window.clone(), strategy, base.clone());
5432        self.address = (strategy, base);
5433        if let Some(location) = location {
5434            let command = self.app.open_location(&location);
5435            self.run_command(command);
5436            // Opened deep in the application: the pages beneath get entries of their own.
5437            let (strategy, base) = &self.address;
5438            for (step, index) in self.history.seed(&self.app.location_stack()) {
5439                crate::address::apply(step, *strategy, base, index);
5440            }
5441        }
5442    }
5443
5444    /// What the browser did since the last look is told to the application, and where the
5445    /// application is is shown in the address bar — the agreement of [`crate::history`].
5446    #[cfg(web)]
5447    fn sync_address(&mut self) {
5448        if !self.started {
5449            return;
5450        }
5451        for (location, state) in crate::address::take_popped() {
5452            if self.history.popped(location.clone(), state) {
5453                let command = self.app.open_location(&location);
5454                self.run_command(command);
5455                self.build_dirty = true;
5456                // The move it starts — a page sliding — is ticked by the frames after this
5457                // one, which was decided to be still before the address was read.
5458                self.request_redraw();
5459            }
5460        }
5461        if let Some(location) = self.app.location() {
5462            if let Some(step) = self
5463                .history
5464                .reflect(&location, self.app.location_replaces())
5465            {
5466                let (strategy, base) = &self.address;
5467                crate::address::apply(step, *strategy, base, self.history.index());
5468            }
5469        }
5470    }
5471
5472    /// Runs a command, and whatever message it produces comes back into the loop
5473    /// through the proxy. Focus requests are **queued**, then resolved against the
5474    /// freshly built tree on the next frame.
5475    ///
5476    /// Where each kind of effect goes:
5477    ///
5478    /// | | native | Web |
5479    /// |---|---|---|
5480    /// | synchronous task | its own thread | a `spawn_local` microtask |
5481    /// | asynchronous task | the shell's executor | the browser |
5482    /// | timer | a task on the executor's reactor | `setTimeout` |
5483    ///
5484    /// A **synchronous** task keeps its thread: it blocks by definition, and that is
5485    /// what a thread is for. An **asynchronous** one no longer gets one — it is a
5486    /// task among others on a pool of four, which is the whole point of milestone
5487    /// 303. See [`crate::runtime`].
5488    fn run_command(&mut self, command: crate::command::Command<A::Message>) {
5489        let parts = command.into_parts();
5490        self.pending_focus.extend(parts.focus);
5491        self.pending_scroll.extend(parts.scrolls);
5492        self.pending_sheet.extend(parts.sheets);
5493        for task in parts.tasks {
5494            let proxy = self.proxy.clone();
5495            #[cfg(not(web))]
5496            std::thread::spawn(move || {
5497                if let Some(message) = task() {
5498                    let _ = proxy.send_event(message);
5499                }
5500            });
5501            #[cfg(web)]
5502            wasm_bindgen_futures::spawn_local(async move {
5503                if let Some(message) = task() {
5504                    let _ = proxy.send_event(message);
5505                }
5506            });
5507        }
5508        for future in parts.async_tasks {
5509            let proxy = self.proxy.clone();
5510            // Native: a task on the shared executor. `detach` because an effect must
5511            // outlive the handle — nothing here wants to cancel it, unlike a
5512            // subscription.
5513            #[cfg(not(web))]
5514            crate::runtime::spawn(async move {
5515                if let Some(message) = future.await {
5516                    let _ = proxy.send_event(message);
5517                }
5518            })
5519            .detach();
5520            // Web: the browser drives the future, single-threaded — `fetch` and friends
5521            // genuinely `await`, without blocking the loop.
5522            #[cfg(web)]
5523            wasm_bindgen_futures::spawn_local(async move {
5524                if let Some(message) = future.await {
5525                    let _ = proxy.send_event(message);
5526                }
5527            });
5528        }
5529        for (delay, message) in parts.timers {
5530            let proxy = self.proxy.clone();
5531            #[cfg(not(web))]
5532            crate::runtime::spawn(async move {
5533                crate::runtime::sleep(delay).await;
5534                let _ = proxy.send_event(message);
5535            })
5536            .detach();
5537            #[cfg(web)]
5538            web_timer::after(delay.as_millis() as i32, move || {
5539                let _ = proxy.send_event(message);
5540            });
5541        }
5542    }
5543
5544    /// Diffs the subscriptions the app declares against those running: starts the new
5545    /// ones and stops those that vanished, by dropping their `Sender`.
5546    fn sync_subscriptions(&mut self) {
5547        let entries = self.app.subscription().into_entries();
5548        let declared: std::collections::HashSet<u64> = entries.iter().map(|e| e.id).collect();
5549
5550        // Stop the subscriptions that are no longer declared.
5551        self.running_subs.retain(|id, _| declared.contains(id));
5552
5553        // Start the new ones.
5554        for entry in entries {
5555            if self.running_subs.contains_key(&entry.id) {
5556                continue;
5557            }
5558            let handle = self.start_subscription(entry.kind);
5559            self.running_subs.insert(entry.id, handle);
5560        }
5561    }
5562
5563    /// Starts a subscription as a task on the shell's executor, and returns its
5564    /// cancellation handle — **dropping the handle cancels the task**, which is what
5565    /// `sync_subscriptions` above relies on when the application stops declaring it.
5566    ///
5567    /// This used to be a thread per subscription, parked in `recv_timeout` and woken
5568    /// by the timeout or by its `Sender` being dropped. A timer is the clearest case
5569    /// there is of work that only ever waits, so it is the clearest case for not
5570    /// spending a thread on it.
5571    #[cfg(not(web))]
5572    fn start_subscription(&self, kind: crate::subscription::Kind<A::Message>) -> SubHandle {
5573        let proxy = self.proxy.clone();
5574        match kind {
5575            crate::subscription::Kind::Every { interval, make } => {
5576                crate::runtime::spawn(async move {
5577                    loop {
5578                        crate::runtime::sleep(interval).await;
5579                        // The loop having closed is the other way this ends, and the
5580                        // application is gone by then, so there is nobody to tell.
5581                        if proxy.send_event(make(Instant::now())).is_err() {
5582                            break;
5583                        }
5584                    }
5585                })
5586            }
5587        }
5588    }
5589
5590    /// Starts a subscription on the **Web**: a browser `setInterval`, with no thread.
5591    /// Returns its cancellation handle, which calls `clearInterval` on drop. The proxy
5592    /// feeds the message back into the loop on every tick.
5593    #[cfg(web)]
5594    fn start_subscription(&self, kind: crate::subscription::Kind<A::Message>) -> SubHandle {
5595        let proxy = self.proxy.clone();
5596        match kind {
5597            crate::subscription::Kind::Every { interval, make } => {
5598                let ms = interval.as_millis() as i32;
5599                web_timer::Interval::new(ms, move || {
5600                    let _ = proxy.send_event(make(Instant::now()));
5601                })
5602            }
5603        }
5604    }
5605
5606    /// Applies the mouse drag under way.
5607    fn handle_drag(&mut self) {
5608        let Some(mut drag) = self.drag.take() else {
5609            return;
5610        };
5611        // How far this pointer has to travel before a press counts as a drag.
5612        let slop = self.hit_slop();
5613        match &mut drag {
5614            Drag::Scrollbar {
5615                id,
5616                vertical,
5617                grab,
5618                track_start,
5619                track_len,
5620                thumb_len,
5621                max,
5622                reverse,
5623            } => {
5624                let along = if *vertical {
5625                    self.cursor.y
5626                } else {
5627                    self.cursor.x
5628                };
5629                let travel = (*track_len - *thumb_len).max(1.0);
5630                let thumb_start = (along - *grab).clamp(*track_start, *track_start + travel);
5631                let fraction = (thumb_start - *track_start) / travel;
5632                // A reversed axis numbers its offsets from the far end, so the thumb's
5633                // position along the track reads backwards.
5634                let fraction = if *reverse { 1.0 - fraction } else { fraction };
5635                let offset = (fraction * *max).clamp(0.0, *max);
5636                let entry = self.runtime.scroll.entry(*id).or_insert((0.0, 0.0));
5637                if *vertical {
5638                    entry.1 = offset;
5639                } else {
5640                    entry.0 = offset;
5641                }
5642                // A precise drag: the target follows the offset and inertia is cut off.
5643                let synced = *entry;
5644                self.runtime.scroll_target.insert(*id, synced);
5645                self.runtime.scroll_velocity.remove(&*id);
5646            }
5647            Drag::RegionSelect { area } => {
5648                let area = *area;
5649                self.extend_region_selection(area);
5650            }
5651            Drag::RegionHandle { area, handle, grab } => {
5652                let (area, handle, grab) = (*area, *handle, *grab);
5653                self.drag_region_handle(area, handle, grab);
5654            }
5655            Drag::TextSelect { id, rect } => {
5656                let local_x = self.cursor.x - rect.x;
5657                let local_y = self.cursor.y - rect.y
5658                    + self.runtime.scroll.get(id).map(|s| s.1).unwrap_or(0.0);
5659                // The field is focused during the drag, so scroll from the current caret.
5660                let scroll_cursor = self.runtime.edits.get(id).map(|e| e.cursor).unwrap_or(0);
5661                let cursor = self
5662                    .tree
5663                    .as_ref()
5664                    .and_then(|tree| find_widget(tree.as_ref(), *id))
5665                    .and_then(|widget| {
5666                        widget.cursor_at(local_x, local_y, rect.width, scroll_cursor)
5667                    });
5668                if let Some(cursor) = cursor {
5669                    let edit = self.runtime.edits.entry(*id).or_default();
5670                    if edit.anchor.is_none() {
5671                        edit.anchor = Some(edit.cursor);
5672                    }
5673                    edit.cursor = cursor;
5674                }
5675            }
5676            Drag::SelectionHandle {
5677                id,
5678                rect,
5679                handle,
5680                grab,
5681            } => {
5682                // The text position the handle stands for, carried with the finger, and
5683                // the retained vertical scroll folded in as for a press.
5684                let local_x = self.cursor.x - rect.x + grab.x;
5685                let local_y = self.cursor.y - rect.y
5686                    + grab.y
5687                    + self.runtime.scroll.get(id).map(|s| s.1).unwrap_or(0.0);
5688                let edit = self.runtime.edits.get(id).copied().unwrap_or_default();
5689                let moved = self
5690                    .tree
5691                    .as_ref()
5692                    .and_then(|tree| find_widget(tree.as_ref(), *id))
5693                    .and_then(|widget| widget.cursor_at(local_x, local_y, rect.width, edit.cursor))
5694                    .and_then(|to| crate::selection::drag(edit, *handle, to));
5695                if let Some(moved) = moved {
5696                    self.runtime.edits.insert(*id, moved);
5697                }
5698            }
5699            Drag::Widget {
5700                id,
5701                rect,
5702                last_x,
5703                press_x,
5704                track,
5705                active,
5706                thumb,
5707            } => {
5708                let dx = self.cursor.x - *last_x;
5709                *last_x = self.cursor.x;
5710                if matches!(track.mode, frus_widgets::DragMode::Thumbs { .. }) {
5711                    let rtl = self.is_rtl();
5712                    let at = track.fraction(*rect, self.cursor.x, rtl);
5713                    match *thumb {
5714                        Some(picked) => {
5715                            self.thumb_drag(*id, picked, frus_widgets::ThumbDrag::Move, at);
5716                        }
5717                        // A press on both thumbs: the first movement says which.
5718                        None => {
5719                            let moved = self.cursor.x - *press_x;
5720                            if let Some(picked) = track.pick_thumb(*rect, *press_x, moved, rtl) {
5721                                *thumb = Some(picked);
5722                                self.thumb_drag(*id, picked, frus_widgets::ThumbDrag::Start, at);
5723                                self.thumb_drag(*id, picked, frus_widgets::ThumbDrag::Move, at);
5724                            }
5725                        }
5726                    }
5727                } else if *active {
5728                    let value = track.value_at(*rect, *press_x, self.cursor.x, true, self.is_rtl());
5729                    self.apply_widget_drag(*id, dx, value);
5730                }
5731            }
5732            Drag::Reorder { start, moved, .. } => {
5733                // Past the threshold this is a real drag, and no longer a sort.
5734                let dx = self.cursor.x - start.x;
5735                let dy = self.cursor.y - start.y;
5736                if !*moved && (dx * dx + dy * dy) > slop * slop {
5737                    *moved = true;
5738                }
5739                // The ghost card follows the pointer, so redraw on every move.
5740                if *moved {
5741                    self.request_redraw();
5742                }
5743            }
5744            Drag::Gesture {
5745                id,
5746                axis,
5747                start,
5748                last,
5749                moved,
5750            } => {
5751                let (id, axis) = (*id, *axis);
5752                if !*moved {
5753                    // Past the slop it is a drag and no longer a tap. (The reference's pan
5754                    // waits for twice this, but its tap has given up at once: a movement in
5755                    // between is neither. One threshold keeps a press either a tap or a drag.)
5756                    let (tx, ty) = (self.cursor.x - start.x, self.cursor.y - start.y);
5757                    let travelled = match axis {
5758                        frus_widgets::PanAxis::Free => tx.hypot(ty),
5759                        frus_widgets::PanAxis::Horizontal => tx.abs(),
5760                        frus_widgets::PanAxis::Vertical => ty.abs(),
5761                    };
5762                    if travelled > slop {
5763                        *moved = true;
5764                        let local = self.pan_local(id, *start);
5765                        self.send_pan(id, frus_widgets::PanEvent::Start { local });
5766                    }
5767                }
5768                if *moved {
5769                    let delta = mask_pan(
5770                        axis,
5771                        Point::new(self.cursor.x - last.x, self.cursor.y - last.y),
5772                    );
5773                    *last = self.cursor;
5774                    self.track_gesture();
5775                    if delta.x != 0.0 || delta.y != 0.0 {
5776                        let local = self.pan_local(id, self.cursor);
5777                        self.send_pan(id, frus_widgets::PanEvent::Update { local, delta });
5778                    }
5779                }
5780            }
5781            Drag::Pan {
5782                id,
5783                last,
5784                moved,
5785                viewport,
5786            } => {
5787                let dx = self.cursor.x - last.x;
5788                let dy = self.cursor.y - last.y;
5789                if !*moved && (dx * dx + dy * dy) > slop * slop {
5790                    *moved = true;
5791                }
5792                if *moved {
5793                    let view = self.runtime.interactive.entry(*id).or_default();
5794                    // The finger pushes the content, bounded to the frame.
5795                    *view = view.pan(dx, dy).clamped(*viewport);
5796                    *last = self.cursor;
5797                    self.track_gesture();
5798                }
5799            }
5800            Drag::Item {
5801                source,
5802                start,
5803                moved,
5804                over,
5805            } => {
5806                let dx = self.cursor.x - start.x;
5807                let dy = self.cursor.y - start.y;
5808                // Under the threshold nothing is lifted: the press may still be a click
5809                // on whatever the draggable wraps.
5810                if !*moved && (dx * dx + dy * dy) > slop * slop {
5811                    *moved = true;
5812                }
5813                if !*moved {
5814                    return;
5815                }
5816                // The target under the pointer, but only if it would take this payload:
5817                // a target that refuses is not highlighted, so the answer is visible
5818                // before the finger lifts rather than after.
5819                let payload = source.payload;
5820                let candidate = self
5821                    .ui
5822                    .as_ref()
5823                    .and_then(|ui| ui.drop_zone_at(self.cursor))
5824                    .filter(|zone| {
5825                        self.tree
5826                            .as_ref()
5827                            .and_then(|tree| find_widget(tree.as_ref(), zone.id))
5828                            .is_some_and(|widget| widget.accepts_drag(payload))
5829                    })
5830                    .map(|zone| zone.id);
5831                if *over != candidate {
5832                    *over = candidate;
5833                    self.runtime.drag_over = candidate;
5834                }
5835                self.request_redraw();
5836            }
5837            Drag::Scroll {
5838                id,
5839                last,
5840                moved,
5841                carried,
5842                dismiss,
5843                axis,
5844                pan,
5845            } => {
5846                let dx = self.cursor.x - last.x;
5847                let dy = self.cursor.y - last.y;
5848                // Under the threshold we do not scroll yet; the gesture may be a tap.
5849                if !*moved && (dx * dx + dy * dy) > slop * slop {
5850                    *moved = true;
5851                    // The moment of arbitration. A scroll and a swipe start identically,
5852                    // so the question is not who is on top but **which way the finger
5853                    // went**: along the item's swipe axis it is a dismissal, across it a
5854                    // scroll. Deciding once, here, is what keeps the loser out of the
5855                    // gesture entirely — a swipe that also scrolled the list would be
5856                    // worse than either.
5857                    // A detector's drag, by the same rule: one kept to an axis takes a movement
5858                    // along it; a free one takes a movement no scroll under it runs along —
5859                    // the reference's pan, which loses a vertical movement to a vertical scroll
5860                    // because its slop is twice the scroll's (milestone 582).
5861                    if let Some((target, pan_axis)) = pan.take() {
5862                        let down = dy.abs() >= dx.abs();
5863                        let wins = match pan_axis {
5864                            frus_widgets::PanAxis::Horizontal => !down,
5865                            frus_widgets::PanAxis::Vertical => down,
5866                            frus_widgets::PanAxis::Free => {
5867                                let chain: Vec<_> = self
5868                                    .ui
5869                                    .as_ref()
5870                                    .map(|ui| ui.scroll_chain(*last).collect())
5871                                    .unwrap_or_default();
5872                                // Nothing under the finger scrolls that way (`claim_area`
5873                                // without its fallback to the area pressed on).
5874                                !chain.iter().any(|a| a.takes_user_scroll(down))
5875                            }
5876                        };
5877                        if wins {
5878                            // The scroll gives its offset back untouched: it never moved.
5879                            self.runtime.release_scroll(*id);
5880                            let start = *last;
5881                            self.drag = Some(Drag::Gesture {
5882                                id: target,
5883                                axis: pan_axis,
5884                                start,
5885                                last: start,
5886                                moved: false,
5887                            });
5888                            self.handle_drag();
5889                            return;
5890                        }
5891                    }
5892                    if let Some(item) = dismiss.take() {
5893                        let along = if item.spec.axis.is_horizontal() {
5894                            dx.abs() > dy.abs()
5895                        } else {
5896                            dy.abs() > dx.abs()
5897                        };
5898                        if along {
5899                            // The list gives the offset back untouched: it never moved.
5900                            self.runtime.release_scroll(*id);
5901                            self.drag = Some(Drag::Dismiss {
5902                                item,
5903                                last: *last,
5904                                moved: true,
5905                            });
5906                            self.handle_drag();
5907                            return;
5908                        }
5909                    }
5910                    // And the same arbitration between the two **axes** — which settles,
5911                    // with it, *which* of the scrollables under the finger the gesture
5912                    // belongs to. The reference does both at once in its arena: every
5913                    // scrollable in the stack enters a recogniser for its own axis, the
5914                    // one the finger matches wins, and the losers are out of the gesture
5915                    // entirely. Without the first half a page scrolling down drifts
5916                    // sideways the whole way, since no finger travels in a straight line;
5917                    // without the second, a strip of chips that only slides across stops
5918                    // the page behind it from scrolling at all.
5919                    let chain: Vec<_> = self
5920                        .ui
5921                        .as_ref()
5922                        .map(|ui| ui.scroll_chain(*last).collect())
5923                        .unwrap_or_default();
5924                    let down = dy.abs() >= dx.abs();
5925                    if let Some(area) = claim_area(&chain, *id, down) {
5926                        if area.id != *id {
5927                            // The area under the finger lost: it gives its offset back
5928                            // untouched, since it never moved. The winner is caught and
5929                            // held exactly as the press would have done to it.
5930                            self.runtime.release_scroll(*id);
5931                            let physics = area.physics;
5932                            *carried = self.runtime.catch_scroll_fling(area.id, physics);
5933                            self.runtime.hold_scroll(area.id);
5934                            *id = area.id;
5935                        }
5936                        *axis = Some(claim_axis(
5937                            dx,
5938                            dy,
5939                            area.takes_user_scroll(false),
5940                            area.takes_user_scroll(true),
5941                        ));
5942                    } else {
5943                        *axis = Some(down);
5944                    }
5945                }
5946                // Only the claimed axis moves. The other delta is not held back for later:
5947                // it is not part of this gesture at all.
5948                let (dx, mut dy) = match *axis {
5949                    Some(true) => (0.0, dy),
5950                    Some(false) => (dx, 0.0),
5951                    None => (dx, dy),
5952                };
5953                if *moved && *axis == Some(true) {
5954                    // A list inside a sheet shares the finger with it (milestone 515).
5955                    // Every movement is split — the sheet first going up, the list
5956                    // first going down — so the gesture changes hands in the middle of a
5957                    // drag, and what the list is left with goes through its physics below
5958                    // exactly as a movement of its own would.
5959                    let sheet = self
5960                        .ui
5961                        .as_ref()
5962                        .filter(|ui| ui.scroll_region(*id).is_some_and(|a| !a.reverse_y))
5963                        .and_then(|ui| ui.sheet_holding(*id))
5964                        .filter(|sheet| sheet.available > 0.0)
5965                        .cloned();
5966                    if let Some(sheet) = sheet {
5967                        let px = sheet.available;
5968                        let offset = self.runtime.scroll.get(id).map_or(0.0, |o| o.1);
5969                        let size = self.runtime.sheet_size(sheet.id, &sheet.spec) * px;
5970                        let (grown, listed) = frus_widgets::split_sheet_drag(
5971                            -dy,
5972                            offset,
5973                            size,
5974                            sheet.spec.floor() * px,
5975                            sheet.spec.max * px,
5976                        );
5977                        if grown != 0.0 {
5978                            self.runtime
5979                                .sheet_drag(sheet.id, &sheet.spec, grown / px, px);
5980                        }
5981                        dy = -listed;
5982                    }
5983                }
5984                if *moved {
5985                    let area = self.ui.as_ref().and_then(|u| u.scroll_region(*id));
5986                    // A region gone from the frame moves as the application's behaviour says.
5987                    let physics = area.map(|a| a.physics).unwrap_or_else(|| {
5988                        self.app
5989                            .scroll_behavior()
5990                            .scroll_physics(&self.themes.displayed(&self.app))
5991                    });
5992                    let cur = self.runtime.scroll.get(id).copied().unwrap_or((0.0, 0.0));
5993                    // The finger pushes the content, so we follow the delta at once —
5994                    // but only as far as the physics allows. Past an edge, bouncing
5995                    // physics resists more and more (the rubber band) while clamping
5996                    // physics refuses the move outright.
5997                    // What the physics refuses is exactly the distance the user asked
5998                    // for and did not get — which is what the glow acknowledges.
5999                    let axis = |metrics: frus_widgets::ScrollMetrics, delta: f32| {
6000                        let applied = physics.apply_user_offset(metrics, delta);
6001                        let proposed = metrics.pixels + applied;
6002                        let refused = physics.apply_boundary_conditions(metrics, proposed);
6003                        (proposed - refused, refused)
6004                    };
6005                    let (nx, ny, refused) = match area {
6006                        Some(area) => {
6007                            let (ddx, ddy) = area.offset_delta((dx, dy));
6008                            let (nx, rx) = axis(area.metrics_x(cur.0), ddx);
6009                            let (ny, ry) = axis(area.metrics_y(cur.1), ddy);
6010                            (nx, ny, Some((area, rx, ry)))
6011                        }
6012                        None => (cur.0 - dx, cur.1 - dy, None),
6013                    };
6014                    if let Some((area, rx, ry)) = refused {
6015                        // A pull-to-refresh area listening above this scrollable takes
6016                        // the **top** edge; the glow takes the other three. Both answer
6017                        // "there is nothing more that way", and giving both would say it
6018                        // twice — the indicator being the more useful of the two, since
6019                        // it leads somewhere.
6020                        if let Some(host) = area.refresh {
6021                            self.feed_refresh(host, physics, area.viewport.height, cur.1, ny, ry);
6022                        }
6023                        let cursor = self.cursor;
6024                        for (refused, vertical, extent) in [
6025                            (rx, false, area.viewport.width),
6026                            (ry, true, area.viewport.height),
6027                        ] {
6028                            if refused.abs() < 1e-3 {
6029                                continue;
6030                            }
6031                            let edge = area.refused_edge(vertical, refused);
6032                            // The refresh area takes the edge the axis **starts** at,
6033                            // which is the bottom of a reversed one.
6034                            if area.refresh.is_some() && edge == area.start_edge(vertical) {
6035                                continue;
6036                            }
6037                            let (offset, cross) =
6038                                frus_widgets::glow_cross_axis(area.viewport, edge, cursor);
6039                            self.runtime.glow_pull(
6040                                area.id,
6041                                area.overscroll,
6042                                edge,
6043                                refused,
6044                                extent,
6045                                offset,
6046                                cross,
6047                            );
6048                        }
6049                        // Content that moves again lets its stretch go, as the
6050                        // reference's does on any scroll that is not an overscroll.
6051                        if rx.abs() < 1e-3 && ry.abs() < 1e-3 && (nx, ny) != cur {
6052                            self.runtime.stretch_release(area.id);
6053                        }
6054                    }
6055                    self.runtime.scroll.insert(*id, (nx, ny));
6056                    self.runtime.scroll_target.insert(*id, (nx, ny));
6057                    self.runtime.scroll_velocity.remove(id);
6058                    *last = self.cursor;
6059                    self.track_gesture();
6060                }
6061            }
6062            Drag::Dismiss { item, last, moved } => {
6063                let dx = self.cursor.x - last.x;
6064                let dy = self.cursor.y - last.y;
6065                if !*moved && (dx * dx + dy * dy) > slop * slop {
6066                    *moved = true;
6067                }
6068                if *moved {
6069                    let delta = if item.spec.axis.is_horizontal() {
6070                        dx
6071                    } else {
6072                        dy
6073                    };
6074                    self.runtime
6075                        .dismiss_drag(item.id, delta, item.extent(), item.spec.axis);
6076                    *last = self.cursor;
6077                    self.track_gesture();
6078                }
6079            }
6080            Drag::Sheet {
6081                id,
6082                last,
6083                moved,
6084                available,
6085            } => {
6086                let dx = self.cursor.x - last.x;
6087                let dy = self.cursor.y - last.y;
6088                if !*moved && (dx * dx + dy * dy) > slop * slop {
6089                    *moved = true;
6090                }
6091                if *moved && *available > 0.0 {
6092                    let spec = self
6093                        .ui
6094                        .as_ref()
6095                        .and_then(|ui| ui.sheet(*id))
6096                        .map(|sheet| sheet.spec.clone());
6097                    if let Some(spec) = spec {
6098                        // Up grows it: the screen's y runs the other way.
6099                        self.runtime
6100                            .sheet_drag(*id, &spec, -dy / *available, *available);
6101                    }
6102                    *last = self.cursor;
6103                    self.track_gesture();
6104                }
6105            }
6106            Drag::Back { start_x } => {
6107                let width = self.logical_width();
6108                // Under RTL the finger slides **left** from the right edge, so progress
6109                // runs the other way.
6110                let sign = if self.is_rtl() { -1.0 } else { 1.0 };
6111                let progress = (sign * (self.cursor.x - *start_x) / width).clamp(0.0, 1.0);
6112                self.track_gesture();
6113                self.build_dirty = true;
6114                self.app.back_gesture(progress);
6115            }
6116        }
6117        self.drag = Some(drag);
6118        self.request_redraw();
6119    }
6120
6121    /// The refresh area a scrollable sits inside, if any.
6122    fn refresh_host_of(&self, scrollable: WidgetId) -> Option<WidgetId> {
6123        self.ui
6124            .as_ref()
6125            .and_then(|ui| ui.scroll_region(scrollable))
6126            .and_then(|area| area.refresh)
6127    }
6128
6129    /// Ends the pull of whichever refresh area holds `scrollable`, and dispatches its
6130    /// message when the pull was armed.
6131    fn release_refresh(&mut self, scrollable: WidgetId) {
6132        let Some(host) = self.refresh_host_of(scrollable) else {
6133            return;
6134        };
6135        if !self.runtime.refresh_release(host) {
6136            return;
6137        }
6138        let message = self
6139            .tree
6140            .as_ref()
6141            .and_then(|tree| find_widget(tree.as_ref(), host))
6142            .and_then(|widget| widget.on_refresh());
6143        if let Some(message) = message {
6144            self.dispatch(message);
6145        }
6146    }
6147
6148    /// Feeds one move of a drag into the pull of the refresh area `host`.
6149    ///
6150    /// The two physics put the overscroll in **different places**, so the signal is
6151    /// read differently:
6152    ///
6153    /// - **Clamping** refuses the movement and pins the offset at the edge, so the
6154    ///   refused amount is the only trace the gesture leaves. It is incremental, and
6155    ///   the physics returns nothing at all for a move back towards the content — so
6156    ///   an eased-off pull holds rather than retracting, which is right: the finger has
6157    ///   not let go.
6158    /// - **Bouncing** lets the offset go past the edge, so the *depth* it reached is
6159    ///   the signal, and the change in that depth is signed. The indicator therefore
6160    ///   follows the rubber band back in as the finger returns, which is also right:
6161    ///   there, the content itself is already saying so.
6162    ///
6163    /// Leaving the top edge at all ends the pull. The gesture has become an ordinary
6164    /// scroll, and an indicator still hanging there would be promising something the
6165    /// release is no longer going to deliver.
6166    fn feed_refresh(
6167        &mut self,
6168        host: WidgetId,
6169        physics: frus_widgets::ScrollPhysics,
6170        extent: f32,
6171        before: f32,
6172        after: f32,
6173        refused_y: f32,
6174    ) {
6175        if after > 0.5 {
6176            self.runtime.refresh_cancel(host);
6177            return;
6178        }
6179        let pulled = if physics.allows_overscroll() {
6180            (-after).max(0.0) - (-before).max(0.0)
6181        } else {
6182            -refused_y
6183        };
6184        if pulled.abs() > 1e-3 {
6185            self.runtime.refresh_pull(host, pulled, extent);
6186        }
6187    }
6188
6189    /// How far the pointer must travel before a press becomes a drag, given what
6190    /// kind of pointer it is.
6191    fn hit_slop(&self) -> f32 {
6192        if self.pointer_touch {
6193            TOUCH_SLOP
6194        } else {
6195            PRECISE_SLOP
6196        }
6197    }
6198
6199    /// Seconds since the drag under way began — the clock the velocity tracker's
6200    /// samples are stamped with.
6201    fn gesture_now(&self) -> f32 {
6202        match self.gesture_clock {
6203            // The test driver's clock: the frames it has run, not the wall's.
6204            Some((now, start)) => now - start,
6205            None => (Instant::now() - self.gesture_start).as_secs_f32(),
6206        }
6207    }
6208
6209    /// Starts a fresh gesture: the history of the previous one must not leak into
6210    /// the next, or a flick left then right would fling the wrong way.
6211    ///
6212    /// The tracker is the application's scroll behaviour's; a drag on a scroll area
6213    /// replaces it with the area's own.
6214    fn begin_gesture(&mut self) {
6215        self.gesture_velocity = self
6216            .app
6217            .scroll_behavior()
6218            .velocity_tracker(&self.themes.displayed(&self.app));
6219        self.gesture_start = Instant::now();
6220        if let Some((now, start)) = self.gesture_clock.as_mut() {
6221            *start = *now;
6222        }
6223        self.track_gesture();
6224    }
6225
6226    /// Records where the pointer is now.
6227    fn track_gesture(&mut self) {
6228        let now = self.gesture_now();
6229        self.gesture_velocity.add_position(now, self.cursor);
6230    }
6231
6232    /// What the gesture tracker makes of the drag as it stands.
6233    fn gesture_estimate(&self) -> VelocityEstimate {
6234        self.gesture_velocity
6235            .estimate(self.gesture_now())
6236            .unwrap_or(VelocityEstimate::STILL)
6237    }
6238
6239    /// [`fling_velocity`], gated on this pointer's slop.
6240    fn fling_velocity(&self, estimate: VelocityEstimate) -> (f32, f32) {
6241        fling_velocity(estimate, self.hit_slop())
6242    }
6243
6244    /// A scroll fling: projects each axis's ballistic destination, friction in closed
6245    /// form, bounds it with the elastic overshoot, and primes the scroll spring with
6246    /// the finger's momentum.
6247    fn fling(&mut self, id: WidgetId, velocity: (f32, f32)) {
6248        let Some(area) = self.ui.as_ref().and_then(|ui| ui.scroll_region(id)) else {
6249            return;
6250        };
6251        let physics = area.physics;
6252        // The physics decides everything from here: whether there is a fling at all,
6253        // how far it runs, and what happens at the edges. A release too slow to
6254        // fling still gets a chance to spring an overscroll back.
6255        if self.runtime.fling_scroll(area, physics, velocity) {
6256            self.request_redraw();
6257        }
6258    }
6259
6260    /// The topmost **reorderable** widget under `point`, as `(id, flat index)`. It
6261    /// uses the reorderables' registry, which is independent of clicking, and so covers
6262    /// the Kanban cards and drop zones, which are not clickable.
6263    fn reorderable_at(&self, point: Point) -> Option<(WidgetId, usize, bool)> {
6264        let id = self.ui.as_ref()?.reorderable_at(point)?;
6265        let widget = self
6266            .tree
6267            .as_ref()
6268            .and_then(|tree| find_widget(tree.as_ref(), id))?;
6269        // A target-**only** widget, a drop zone: reorderable but not grabbable, so no
6270        // drag starts on it. Dropping still aims at it, through `ui.reorderable_at`.
6271        if !widget.reorder_draggable() {
6272            return None;
6273        }
6274        let from = widget.reorder_index()?;
6275        Some((id, from, widget.drag_needs_long_press()))
6276    }
6277
6278    /// **What is actually moving**, given what was grabbed: its id and its box.
6279    ///
6280    /// Usually they are the same widget. They are not when what was grabbed is a **grip**
6281    /// — a source that is not a target — because a grip is a 40-pixel gutter inside a much
6282    /// larger row, and everything the preview is made of is the row's: the ghost is the
6283    /// row lifted out of the frame, the gap that closes behind it is the row's height, and
6284    /// the band the neighbours are matched against is the row's width. Taking the grip's
6285    /// box instead would lift an icon and open a slot two centimetres too narrow.
6286    ///
6287    /// The row is found by the index the two share, among the reorderables under the
6288    /// grip's own middle — which is inside its row by construction.
6289    fn reorder_source(&self, id: WidgetId, from: usize) -> Option<(WidgetId, Rect)> {
6290        let ui = self.ui.as_ref()?;
6291        let tree = self.tree.as_ref()?;
6292        let own = ui.widget_rect(id)?;
6293        let grabbed = find_widget(tree.as_ref(), id)?;
6294        if grabbed.reorder_droppable() {
6295            return Some((id, own));
6296        }
6297        let middle = Point::new(own.x + own.width * 0.5, own.y + own.height * 0.5);
6298        let slot = ui.reorderables_at(middle).find(|other| {
6299            find_widget(tree.as_ref(), *other)
6300                .is_some_and(|w| w.reorder_droppable() && w.reorder_index() == Some(from))
6301        })?;
6302        // A grip whose row is nowhere to be found still moves something: itself, which is
6303        // wrong-looking but not a crash, and cannot happen while the two are built
6304        // together.
6305        Some((slot, ui.widget_rect(slot).unwrap_or(own)))
6306    }
6307
6308    /// The row the reorder under way carries, and its box: the one taken when the drag was
6309    /// first carried, moved with the content since — or, before any frame has carried it,
6310    /// the frame's.
6311    fn reorder_carried(&self) -> Option<(WidgetId, Rect)> {
6312        match &self.drag {
6313            Some(Drag::Reorder {
6314                carried: Some(carried),
6315                ..
6316            }) => Some(*carried),
6317            Some(Drag::Reorder { id, from, .. }) => self.reorder_source(*id, *from),
6318            _ => None,
6319        }
6320    }
6321
6322    /// Takes the carried row's box from the frame, once: the first frame a reorder is carried
6323    /// in, before anything has scrolled it. From then on it goes with the content, and the
6324    /// frame is no longer asked — a row carried to an edge is clipped by it, and a row
6325    /// scrolled out of sight is not in the frame at all.
6326    fn keep_carried_box(&mut self) {
6327        let source = match &self.drag {
6328            Some(Drag::Reorder {
6329                id,
6330                from,
6331                moved: true,
6332                carried: None,
6333                ..
6334            }) => self.reorder_source(*id, *from),
6335            _ => return,
6336        };
6337        if let Some(Drag::Reorder { carried, .. }) = self.drag.as_mut() {
6338            *carried = source;
6339        }
6340    }
6341
6342    /// The topmost reorderable under `point` that something can actually be **dropped**
6343    /// on. It is not always the topmost reorderable: a list row's grip is a source and
6344    /// not a target, and what is under it is the row the drop is really aimed at.
6345    fn reorder_target_at(&self, point: Point) -> Option<WidgetId> {
6346        let ui = self.ui.as_ref()?;
6347        let tree = self.tree.as_ref()?;
6348        ui.reorderables_at(point)
6349            .find(|id| find_widget(tree.as_ref(), *id).is_some_and(|w| w.reorder_droppable()))
6350    }
6351
6352    /// Where the reorderable `id`, grabbed at index `from`, is **dropped** if released now:
6353    /// the target under the pointer, or — over no target at all — the nearest slot of its own
6354    /// list, for one inserted between its neighbours, along the axis it is reordered on.
6355    ///
6356    /// The insertion line and the release both ask this, so the line cannot promise a place
6357    /// the drop does not keep. The second half is milestone 518: a row carried to the bottom
6358    /// of a phone's list is over what follows the list, and the release used to put it back.
6359    /// Milestone 527 turned it on its side, for a list whose rows run across.
6360    fn reorder_drop_target(
6361        &self,
6362        id: WidgetId,
6363        from: usize,
6364        carried: Option<(WidgetId, Rect)>,
6365    ) -> Option<WidgetId> {
6366        if let Some(target) = self.reorder_target_at(self.cursor) {
6367            return Some(target);
6368        }
6369        let tree = self.tree.as_ref()?;
6370        let Some(ReorderMotion::Slots(axis)) = find_widget(tree.as_ref(), id).map(reorder_motion)
6371        else {
6372            return None;
6373        };
6374        // Among the slots of the row that moves, which is not the grip that was grabbed —
6375        // and which may have been scrolled out of the frame by the carry itself.
6376        let row = match carried {
6377            Some((row, _)) => row,
6378            None => self.reorder_source(id, from)?.0,
6379        };
6380        let slots = reorder_siblings(self.ui.as_ref()?, tree.as_ref(), row);
6381        let boxes: Vec<Rect> = slots.iter().map(|(_, rect)| *rect).collect();
6382        nearest_reorder_slot(self.cursor, &boxes, axis).map(|index| slots[index].0)
6383    }
6384
6385    /// What is being **carried** this frame, where it is now: the box the ghost is drawn
6386    /// at, for a reorder or for a lifted item. `None` when nothing is engaged.
6387    ///
6388    /// It is the ghost's box and not the pointer, because the reference scrolls when the
6389    /// *item* reaches the edge, and that is the honest moment: a row half off the bottom
6390    /// is already asking to go further, whatever the finger holding it is over.
6391    fn carried_rect(&self) -> Option<Rect> {
6392        match &self.drag {
6393            Some(Drag::Reorder {
6394                start, moved: true, ..
6395            }) => {
6396                let (_, src) = self.reorder_carried()?;
6397                // The same offsets the ghost is painted at, and for the same reason a
6398                // column's ghost only rises: it moves along its own axis.
6399                let (gx, gy) = ghost_offset(
6400                    self.dragged_reorder_motion()
6401                        .unwrap_or(ReorderMotion::Columns),
6402                    (self.cursor.x - start.x, self.cursor.y - start.y),
6403                );
6404                Some(src.translate(gx, gy))
6405            }
6406            Some(Drag::Item {
6407                source,
6408                start,
6409                moved: true,
6410                ..
6411            }) => Some(
6412                source
6413                    .rect
6414                    .translate(self.cursor.x - start.x, self.cursor.y - start.y),
6415            ),
6416            _ => None,
6417        }
6418    }
6419
6420    /// Scrolls the area under the pointer while what is being carried hangs past one of
6421    /// its edges — the gap `Draggable` has had since milestone 285, and what a list longer
6422    /// than a screen needs before it can be reordered at all.
6423    ///
6424    /// The area is the innermost one under the **pointer** that a user could move by
6425    /// hand: an area that refuses a finger refuses this too, so a list that fits its
6426    /// viewport never twitches.
6427    fn autoscroll_carried(&mut self, dt: f32) -> bool {
6428        self.keep_carried_box();
6429        let Some(carried) = self.carried_rect() else {
6430            return false;
6431        };
6432        let area = {
6433            let scroll = &self.runtime.scroll;
6434            self.ui.as_ref().and_then(|ui| {
6435                ui.scroll_chain(self.cursor).find(|area| {
6436                    area.accepts_user_offset(scroll.get(&area.id).copied().unwrap_or((0.0, 0.0)))
6437                })
6438            })
6439        };
6440        let Some(area) = area else {
6441            return false;
6442        };
6443        let offset = self
6444            .runtime
6445            .scroll
6446            .get(&area.id)
6447            .copied()
6448            .unwrap_or((0.0, 0.0));
6449        // What is still hidden on either side, in **screen** terms: on a reversed axis
6450        // the offset is measured from the other end, so the two swap over.
6451        let room = |offset: f32, max: f32, reverse: bool| {
6452            if reverse {
6453                (max - offset, offset)
6454            } else {
6455                (offset, max - offset)
6456            }
6457        };
6458        let view = area.viewport;
6459        let dx = edge_autoscroll(
6460            (carried.x, carried.x + carried.width),
6461            (view.x, view.x + view.width),
6462            room(offset.0, area.max_x, area.reverse_x),
6463            dt,
6464        )
6465        .unwrap_or(0.0);
6466        let dy = edge_autoscroll(
6467            (carried.y, carried.y + carried.height),
6468            (view.y, view.y + view.height),
6469            room(offset.1, area.max_y, area.reverse_y),
6470            dt,
6471        )
6472        .unwrap_or(0.0);
6473        if dx == 0.0 && dy == 0.0 {
6474            return false;
6475        }
6476        // A movement of the content becomes a change of offset the area's own way, which
6477        // is the one place the sign of a reversed axis is decided.
6478        let delta = area.offset_delta((dx, dy));
6479        let next = (
6480            (offset.0 + delta.0).clamp(0.0, area.max_x),
6481            (offset.1 + delta.1).clamp(0.0, area.max_y),
6482        );
6483        if next == offset {
6484            return false;
6485        }
6486        self.runtime.scroll.insert(area.id, next);
6487        // The target follows, or the inertia the area was resting at would spring the
6488        // list straight back out from under the row.
6489        self.runtime.scroll_target.insert(area.id, next);
6490        self.runtime.scroll_velocity.remove(&area.id);
6491        // What is carried stays under the finger. Its ghost is drawn at the box the frame
6492        // puts it at, offset by how far the finger has moved since the press; the frame now
6493        // puts that box where the content went, so the press goes there too. Otherwise the
6494        // ghost rides up with the list, stops hanging over the edge, and the scroll it
6495        // started starves itself — seen on a phone, milestone 517.
6496        if let Some(drag) = self.drag.as_mut() {
6497            follow_content(
6498                drag,
6499                area.offset_delta((next.0 - offset.0, next.1 - offset.1)),
6500            );
6501        }
6502        true
6503    }
6504
6505    /// How the reorderable currently **grabbed** moves, if a drag is under way. It is what
6506    /// keeps the **pointer spring** (`reorder_x` following the pointer) to `Table`'s columns;
6507    /// the Kanban cards and a list's rows spring their insertion line instead.
6508    fn dragged_reorder_motion(&self) -> Option<ReorderMotion> {
6509        let Some(Drag::Reorder { id, .. }) = self.drag else {
6510            return None;
6511        };
6512        self.tree
6513            .as_ref()
6514            .and_then(|t| find_widget(t.as_ref(), id))
6515            .map(reorder_motion)
6516    }
6517
6518    /// Steps the reorder springs one frame, per gesture — see the frame loop — and says
6519    /// whether they are still moving.
6520    fn advance_reorder_springs(&mut self, dt: f32) -> bool {
6521        let reorder_motion = matches!(self.drag, Some(Drag::Reorder { moved: true, .. }))
6522            .then(|| self.dragged_reorder_motion())
6523            .flatten();
6524        match reorder_motion {
6525            Some(ReorderMotion::Columns) => {
6526                self.reorder_x = spring_toward(self.reorder_x, self.cursor.x, dt, 0.07);
6527                (self.cursor.x - self.reorder_x).abs() > 0.5
6528            }
6529            Some(ReorderMotion::Slots(ReorderAxis::Vertical)) => {
6530                match self.reorder_drop_line(drag_preview::INSERT_THICKNESS) {
6531                    Some(target) => {
6532                        self.reorder_y = spring_toward(self.reorder_y, target.y, dt, 0.07);
6533                        (target.y - self.reorder_y).abs() > 0.5
6534                    }
6535                    None => false,
6536                }
6537            }
6538            Some(ReorderMotion::Slots(ReorderAxis::Horizontal)) => {
6539                match self.reorder_drop_line(drag_preview::INSERT_THICKNESS) {
6540                    Some(target) => {
6541                        self.reorder_x = spring_toward(self.reorder_x, target.x, dt, 0.07);
6542                        (target.x - self.reorder_x).abs() > 0.5
6543                    }
6544                    None => false,
6545                }
6546            }
6547            None => false,
6548        }
6549    }
6550
6551    /// Paints the **reorder preview** on top of the scene, unclipped: the source
6552    /// column dimmed, a **drop indicator** at the target column's insertion edge, and a
6553    /// **lifted card** — shadow plus a `primary` border — following the pointer. No
6554    /// effect outside an engaged header drag.
6555    /// Names, on the console, every box this frame whose children did not fit inside it.
6556    ///
6557    /// The reference treats an overflowing flex as an error condition, paints a striped
6558    /// band along the offending edge and writes to the console; nothing here said anything
6559    /// at all, which is how a task row's delete button came to be laid out past the window
6560    /// and stayed there for three milestones — drawn nowhere, hittable nowhere. Half of
6561    /// that is now here: the words, once per site, with the two ways out the reference
6562    /// suggests. The striped band is not.
6563    ///
6564    /// Once per site, because a layout that does not fit does not fit on every frame, and
6565    /// sixty lines a second is the same as silence.
6566    fn report_overflows(&self, ui: &Ui<A::Message>) {
6567        for o in ui.overflows() {
6568            // The site: a box, an edge, and the amount rounded to the pixel. Enough to
6569            // stay quiet while a window is resized past the same defect, and to speak up
6570            // when the defect changes.
6571            let mut key = std::collections::hash_map::DefaultHasher::new();
6572            use std::hash::{Hash, Hasher};
6573            (
6574                o.rect.width.to_bits(),
6575                o.rect.height.to_bits(),
6576                o.side as u8,
6577                o.amount.round() as i32,
6578            )
6579                .hash(&mut key);
6580            if !self.reported_overflows.borrow_mut().insert(key.finish()) {
6581                continue;
6582            }
6583            let side = match o.side {
6584                frus_widgets::Side::Left => "left",
6585                frus_widgets::Side::Right => "right",
6586                frus_widgets::Side::Top => "top",
6587                frus_widgets::Side::Bottom => "bottom",
6588            };
6589            log::warn!(
6590                "a box {:.0}x{:.0} is overflowed by {:.0} px on the {side}: its children do not \
6591                 fit inside it, and what runs past the edge is drawn outside its parent — \
6592                 invisible where something clips it, and untappable where it leaves the window. \
6593                 Give the child that should give way an `Expanded`, or put the content in a \
6594                 `SingleChildScrollView`.",
6595                o.rect.width,
6596                o.rect.height,
6597                o.amount,
6598            );
6599        }
6600    }
6601
6602    /// Winit's key event as a [`KeyStroke`] — the general vocabulary shortcuts are bound
6603    /// to, as opposed to [`Key`], which is the one text editing needs.
6604    ///
6605    /// `None` for anything with no place in a shortcut: a dead key, a compose sequence, a
6606    /// modifier pressed on its own.
6607    fn keystroke(&self, event: &KeyDown) -> Option<KeyStroke> {
6608        let key = match &event.logical_key {
6609            WinitKey::Named(NamedKey::Enter) => ShortcutKey::Enter,
6610            WinitKey::Named(NamedKey::Escape) => ShortcutKey::Escape,
6611            WinitKey::Named(NamedKey::Tab) => ShortcutKey::Tab,
6612            WinitKey::Named(NamedKey::Space) => ShortcutKey::Space,
6613            WinitKey::Named(NamedKey::Backspace) => ShortcutKey::Backspace,
6614            WinitKey::Named(NamedKey::Delete) => ShortcutKey::Delete,
6615            WinitKey::Named(NamedKey::ArrowUp) => ShortcutKey::Up,
6616            WinitKey::Named(NamedKey::ArrowDown) => ShortcutKey::Down,
6617            WinitKey::Named(NamedKey::ArrowLeft) => ShortcutKey::Left,
6618            WinitKey::Named(NamedKey::ArrowRight) => ShortcutKey::Right,
6619            WinitKey::Named(NamedKey::Home) => ShortcutKey::Home,
6620            WinitKey::Named(NamedKey::End) => ShortcutKey::End,
6621            WinitKey::Named(NamedKey::PageUp) => ShortcutKey::PageUp,
6622            WinitKey::Named(NamedKey::PageDown) => ShortcutKey::PageDown,
6623            WinitKey::Named(NamedKey::F1) => ShortcutKey::F(1),
6624            WinitKey::Named(NamedKey::F2) => ShortcutKey::F(2),
6625            WinitKey::Named(NamedKey::F3) => ShortcutKey::F(3),
6626            WinitKey::Named(NamedKey::F4) => ShortcutKey::F(4),
6627            WinitKey::Named(NamedKey::F5) => ShortcutKey::F(5),
6628            WinitKey::Named(NamedKey::F6) => ShortcutKey::F(6),
6629            WinitKey::Named(NamedKey::F7) => ShortcutKey::F(7),
6630            WinitKey::Named(NamedKey::F8) => ShortcutKey::F(8),
6631            WinitKey::Named(NamedKey::F9) => ShortcutKey::F(9),
6632            WinitKey::Named(NamedKey::F10) => ShortcutKey::F(10),
6633            WinitKey::Named(NamedKey::F11) => ShortcutKey::F(11),
6634            WinitKey::Character(text) => ShortcutKey::Char(text.chars().next()?),
6635            _ => return None,
6636        };
6637        Some(KeyStroke {
6638            key,
6639            ctrl: self.ctrl,
6640            shift: self.shift,
6641            alt: self.alt,
6642            meta: self.meta,
6643        })
6644    }
6645
6646    fn paint_reorder_preview(&self, ui: &Ui<A::Message>, theme: &Theme, scene: &mut Scene) {
6647        let Some(Drag::Reorder {
6648            start, moved: true, ..
6649        }) = self.drag
6650        else {
6651            return;
6652        };
6653        // What moves is the row, even when what was grabbed is the grip inside it — at the
6654        // box it was carried from, which has gone with the content.
6655        let Some((id, src)) = self.reorder_carried() else {
6656            return;
6657        };
6658        let motion = self
6659            .tree
6660            .as_ref()
6661            .and_then(|t| find_widget(t.as_ref(), id))
6662            .map(reorder_motion)
6663            .unwrap_or(ReorderMotion::Columns);
6664        let dx = self.cursor.x - start.x;
6665        let dy = self.cursor.y - start.y;
6666
6667        // The ghost's offset, per motion — see `ghost_offset`.
6668        let (gx, gy) = ghost_offset(motion, (dx, dy));
6669
6670        // The owners of the grabbed item's **subtree**: used by the ghost, to capture a
6671        // rich card's content, which its children paint (milestone 251), **and** by the
6672        // vertical reflow, to lift the card out of the preview. Fallback: the card alone.
6673        let owners: std::collections::HashSet<u64> = self
6674            .tree
6675            .as_ref()
6676            .and_then(|t| find_widget(t.as_ref(), id))
6677            .map(|w| subtree_ids(w, id).iter().map(|i| i.as_u64()).collect())
6678            .unwrap_or_else(|| std::iter::once(id.as_u64()).collect());
6679
6680        match motion {
6681            ReorderMotion::Columns => {
6682                // Reflow the neighbouring columns: the source's gap closes and the drop
6683                // slot opens, following the pointer's **smoothed** abscissa — a gentle
6684                // inertial slide, while the ghost sticks to the real pointer.
6685                let reflowed =
6686                    reflow_reorder_columns(scene.primitives(), src, self.reorder_x, id.as_u64());
6687                scene.clear();
6688                for primitive in reflowed {
6689                    scene.push_primitive(primitive);
6690                }
6691            }
6692            ReorderMotion::Slots(axis) => {
6693                // Reflow the **cards**: the lifted card's gap closes in the source
6694                // column and a slot opens under the **insertion line** in the target one.
6695                // Then the line is laid on top, at the chosen edge — the hovered half,
6696                // milestone 252.
6697                //
6698                // A **smoothed** line (milestone 265): the chosen slot's width, abscissa
6699                // and thickness are kept, but the **ordinate** is replaced by the
6700                // `reorder_y` spring — the line *and* the gap slide between cards, with
6701                // vertical inertia, instead of jumping a notch. A horizontal list's line
6702                // stands on its end, and its **abscissa** is the one that springs.
6703                let line =
6704                    self.reorder_drop_line(drag_preview::INSERT_THICKNESS)
6705                        .map(|r| match axis {
6706                            ReorderAxis::Vertical => Rect {
6707                                y: self.reorder_y,
6708                                ..r
6709                            },
6710                            ReorderAxis::Horizontal => Rect {
6711                                x: self.reorder_x,
6712                                ..r
6713                            },
6714                        });
6715                // Only what can be reordered makes room — a card, a row, a drop zone, and
6716                // what each of them paints. The reflow is geometric, and a button floating
6717                // over the list or the navigation bar under it shares the band without being
6718                // part of the list: moved with it, the first left its `+` a row above its own
6719                // disc on a phone (milestone 517).
6720                let movable = self
6721                    .tree
6722                    .as_ref()
6723                    .map(|tree| reorderable_owners(ui, tree.as_ref()))
6724                    .unwrap_or_default();
6725                let reflowed =
6726                    reflow_reorder_cards(scene.primitives(), src, line, &owners, &movable, axis);
6727                scene.clear();
6728                for primitive in reflowed {
6729                    scene.push_primitive(primitive);
6730                }
6731                if let Some(line) = line {
6732                    // Rounded across its thickness, whichever way it stands.
6733                    let thickness = match axis {
6734                        ReorderAxis::Vertical => line.height,
6735                        ReorderAxis::Horizontal => line.width,
6736                    };
6737                    scene.set_clip(Rect::UNBOUNDED);
6738                    scene.draw_rect(
6739                        line,
6740                        theme.primary,
6741                        theme.radius.min(thickness * 0.5),
6742                        0.0,
6743                        Color::TRANSPARENT,
6744                    );
6745                }
6746            }
6747        }
6748
6749        // A faithful ghost: the grabbed item's primitives, taken from the original
6750        // scene, translated and **un-clipped**, since they would otherwise be cropped at
6751        // the source. In a **rich card** the background is painted by the card but its
6752        // content — label, tags, the × button — by children, under other owners, so we
6753        // capture the primitives of the item's **whole subtree**; see `owners` above.
6754        let ghost: Vec<Primitive> = ui
6755            .scene()
6756            .primitives()
6757            .iter()
6758            .filter(|p| owners.contains(&p.owner()))
6759            .map(|p| p.translated(gx, gy).with_clip(Rect::UNBOUNDED))
6760            .collect();
6761        draw_ghost_card(scene, theme, src.translate(gx, gy), &ghost);
6762    }
6763
6764    /// The lifted item: its own primitives dimmed where it sits, and a copy of them
6765    /// floating under the pointer.
6766    ///
6767    /// The copy is taken from the frame rather than built again, so it cannot drift
6768    /// from what is on screen — a rebuilt "feedback" widget is a second definition of
6769    /// the same thing, and two definitions is one too many. The same reason the
6770    /// reorder ghost works this way.
6771    fn paint_drag_ghost(&self, ui: &Ui<A::Message>, theme: &Theme, scene: &mut Scene) {
6772        let Some(Drag::Item {
6773            source,
6774            start,
6775            moved: true,
6776            ..
6777        }) = &self.drag
6778        else {
6779            return;
6780        };
6781        let widget = self
6782            .tree
6783            .as_ref()
6784            .and_then(|tree| find_widget(tree.as_ref(), source.id));
6785        // A rich item paints its background itself and its content through children,
6786        // under other owners, so the whole subtree is captured — the same reason the
6787        // reorder ghost does.
6788        let owners: std::collections::HashSet<u64> = widget
6789            .map(|w| {
6790                subtree_ids(w, source.id)
6791                    .iter()
6792                    .map(|i| i.as_u64())
6793                    .collect()
6794            })
6795            .unwrap_or_else(|| std::iter::once(source.id.as_u64()).collect());
6796        let opacity = widget.map(|w| w.drag_ghost_opacity()).unwrap_or(1.0);
6797        let dx = self.cursor.x - start.x;
6798        let dy = self.cursor.y - start.y;
6799
6800        // What is left behind, faded in place: the item is being carried, not deleted.
6801        let original: Vec<Primitive> = scene.primitives().to_vec();
6802        scene.clear();
6803        for primitive in &original {
6804            if owners.contains(&primitive.owner()) {
6805                scene.push_faded(primitive, opacity);
6806            } else {
6807                scene.push_primitive(primitive.clone());
6808            }
6809        }
6810
6811        let ghost: Vec<Primitive> = ui
6812            .scene()
6813            .primitives()
6814            .iter()
6815            .filter(|p| owners.contains(&p.owner()))
6816            .map(|p| p.translated(dx, dy).with_clip(Rect::UNBOUNDED))
6817            .collect();
6818        draw_ghost_card(scene, theme, source.rect.translate(dx, dy), &ghost);
6819    }
6820
6821    /// The **insertion** line of the preview: a thin band at the edge of the hovered
6822    /// reorderable slot, a card, a row or a drop zone — the edge **before** it when the
6823    /// pointer is in its leading half, the edge **after** it in its trailing half. Across a
6824    /// vertical list; standing on its end in a horizontal one. `None` when the release would
6825    /// land nowhere.
6826    fn reorder_drop_line(&self, thickness: f32) -> Option<Rect> {
6827        let Some(Drag::Reorder {
6828            id, from, carried, ..
6829        }) = self.drag
6830        else {
6831            return None;
6832        };
6833        // The reorderable slot — card, row or drop zone — the release would land on: the
6834        // one under the pointer, or the nearest of the list's own.
6835        let target = self.reorder_drop_target(id, from, carried)?;
6836        let rect = self.ui.as_ref()?.widget_rect(target)?;
6837        let axis = match self
6838            .tree
6839            .as_ref()
6840            .and_then(|t| find_widget(t.as_ref(), target))
6841            .map(reorder_motion)
6842        {
6843            Some(ReorderMotion::Slots(axis)) => axis,
6844            _ => ReorderAxis::Vertical,
6845        };
6846        Some(drop_insertion_line(
6847            rect,
6848            thickness,
6849            self.reorder_insert_after(target, rect),
6850            axis,
6851            self.is_rtl(),
6852        ))
6853    }
6854
6855    /// For a slot a reorderable is **inserted** at, tells whether insertion happens
6856    /// **after** it (index +1, between it and the next) — the pointer in its trailing half,
6857    /// which `reorder_drop_after` works out along the list's axis and reading direction.
6858    /// Always `false` for `Table`'s columns, which keep their drop logic unchanged. This is
6859    /// the insertion line's counterpart on the **routing** side.
6860    fn reorder_insert_after(&self, target: WidgetId, rect: Rect) -> bool {
6861        match self
6862            .tree
6863            .as_ref()
6864            .and_then(|t| find_widget(t.as_ref(), target))
6865            .map(reorder_motion)
6866        {
6867            Some(ReorderMotion::Slots(axis)) => {
6868                reorder_drop_after(self.cursor, rect, axis, self.is_rtl())
6869            }
6870            _ => false,
6871        }
6872    }
6873
6874    /// The innermost detector that takes a drag under the pointer, with the way it drags.
6875    fn pan_target(&self) -> Option<(WidgetId, frus_widgets::PanAxis)> {
6876        let (id, _) = self.ui.as_ref()?.pan_at(self.cursor)?;
6877        let axis = find_widget(self.tree.as_ref()?.as_ref(), id)?.pan_axis()?;
6878        Some((id, axis))
6879    }
6880
6881    /// `at`, in the coordinates of the detector `id`.
6882    fn pan_local(&self, id: WidgetId, at: Point) -> Point {
6883        let origin = self
6884            .ui
6885            .as_ref()
6886            .and_then(|ui| ui.widget_rect(id))
6887            .map_or(Point::new(0.0, 0.0), |rect| Point::new(rect.x, rect.y));
6888        Point::new(at.x - origin.x, at.y - origin.y)
6889    }
6890
6891    /// Hands one moment of a drag to the detector `id`, and its message to the application.
6892    fn send_pan(&mut self, id: WidgetId, event: frus_widgets::PanEvent) {
6893        let rtl = self.is_rtl();
6894        let value = self.runtime.value(id);
6895        let (message, moved, released) = match self
6896            .tree
6897            .as_ref()
6898            .and_then(|tree| find_widget(tree.as_ref(), id))
6899        {
6900            Some(widget) => {
6901                // A widget whose own value the drag moves — a switch's thumb (milestone
6902                // 638): held where the finger puts it, and asked what letting go there
6903                // means.
6904                let moved = widget.pan_value(event, value, rtl);
6905                let released = match event {
6906                    frus_widgets::PanEvent::End { .. } if self.runtime.value_placed(id) => {
6907                        Some(widget.on_value_release(value))
6908                    }
6909                    _ => None,
6910                };
6911                (widget.on_pan(event), moved, released)
6912            }
6913            None => (None, None, None),
6914        };
6915        if let Some(v) = moved {
6916            self.runtime.hold_value(id, v);
6917        }
6918        if let Some(release) = released {
6919            self.runtime.release_value(id);
6920            if let Some(message) = release {
6921                self.dispatch(message);
6922            }
6923        }
6924        if let Some(message) = message {
6925            self.dispatch(message);
6926        }
6927        self.request_redraw();
6928    }
6929
6930    /// Sends a value drag's **start** or **end** to the widget being dragged, at
6931    /// `fraction` along its travel.
6932    ///
6933    /// The fraction is the one [`frus_widgets::DragTrack`] works out for every moment of
6934    /// the drag, from the same press and the same track: an end that disagreed with the
6935    /// last `on_drag` about where the value finished would be a slider that settles
6936    /// somewhere its own final message never mentioned.
6937    fn dispatch_drag_edge(&mut self, id: WidgetId, edge: Edge, fraction: f32) {
6938        let message = self
6939            .tree
6940            .as_ref()
6941            .and_then(|tree| find_widget(tree.as_ref(), id))
6942            .and_then(|widget| match edge {
6943                Edge::Start => widget.on_drag_start(fraction),
6944                Edge::End => widget.on_drag_end(fraction),
6945            });
6946        if let Some(message) = message {
6947            self.dispatch(message);
6948        }
6949    }
6950
6951    /// A moment of a drag on one of a widget's thumbs (milestone 651).
6952    fn thumb_drag(
6953        &mut self,
6954        id: WidgetId,
6955        thumb: usize,
6956        moment: frus_widgets::ThumbDrag,
6957        fraction: f32,
6958    ) {
6959        if moment == frus_widgets::ThumbDrag::Start {
6960            // The widget paints the thumb it took raised and on top.
6961            self.runtime.input.thumb = Some((id, thumb));
6962        }
6963        let message = self
6964            .tree
6965            .as_ref()
6966            .and_then(|tree| find_widget(tree.as_ref(), id))
6967            .and_then(|widget| widget.on_thumb_drag(thumb, moment, fraction));
6968        if let Some(message) = message {
6969            self.dispatch(message);
6970        }
6971    }
6972
6973    /// One moment of a value drag: `dx` for a handle that accumulates, else the `value`
6974    /// the drag's track says this moment stands for — `None` when it changes nothing.
6975    fn apply_widget_drag(&mut self, id: WidgetId, dx: f32, value: Option<f32>) {
6976        let message = self
6977            .tree
6978            .as_ref()
6979            .and_then(|tree| find_widget(tree.as_ref(), id))
6980            .and_then(|widget| {
6981                widget
6982                    .on_drag_delta(dx)
6983                    .or_else(|| value.and_then(|v| widget.on_drag(v)))
6984            });
6985        if let Some(message) = message {
6986            self.dispatch(message);
6987        }
6988    }
6989
6990    /// **A key going down**: everything the shell does with one, from the system's back key
6991    /// and the inspector to a shortcut, Tab, the arrows, activation and typing. Called with
6992    /// winit's event in a window, and with one a test made in the driver (milestone 608).
6993    /// `event_loop` is what Back quits through; with none, Back that would quit does nothing.
6994    fn key_down(&mut self, event: &KeyDown, event_loop: Option<&ActiveEventLoop>) {
6995        // A keyboard interaction: the focus ring becomes visible again.
6996        if !self.runtime.focus_visible {
6997            self.runtime.focus_visible = true;
6998            self.request_redraw();
6999        }
7000
7001        // The **system** back — Android's button or gesture, the browser's back
7002        // key: it closes the topmost overlay, else pops a screen, else quits.
7003        if matches!(
7004            event.logical_key,
7005            WinitKey::Named(NamedKey::BrowserBack) | WinitKey::Named(NamedKey::GoBack)
7006        ) {
7007            if !event.repeat {
7008                if let Some(event_loop) = event_loop {
7009                    self.system_back(event_loop);
7010                }
7011            }
7012            return;
7013        }
7014
7015        // F12 toggles the **inspector**: outlines, a card for the hovered
7016        // widget, and a tree dump on stderr. Debug builds only.
7017        if matches!(event.logical_key, WinitKey::Named(NamedKey::F12)) {
7018            if cfg!(debug_assertions) && !event.repeat {
7019                self.inspector = !self.inspector;
7020                self.inspector_dump = self.inspector;
7021                self.request_redraw();
7022            }
7023            return;
7024        }
7025
7026        // **Shortcuts.** Placed after the system keys the shell owns outright
7027        // (back, F12) and before everything an application can bind, so that a
7028        // binding cannot take the inspector or the back gesture away.
7029        //
7030        // A stroke with no Ctrl, Alt or Meta goes to a focused field first: a
7031        // binding on a bare letter would otherwise make every field under it
7032        // impossible to type in.
7033        if let Some(stroke) = self.keystroke(event) {
7034            let typing = !stroke.is_command() && self.runtime.input.focused.is_some();
7035            if !typing {
7036                let msgs = self
7037                    .ui
7038                    .as_ref()
7039                    .map(|ui| ui.keystroke(stroke, self.runtime.input.focused))
7040                    .unwrap_or_default();
7041                if !msgs.is_empty() {
7042                    for msg in msgs {
7043                        self.dispatch(msg);
7044                    }
7045                    self.request_redraw();
7046                    return;
7047                }
7048            }
7049        }
7050
7051        // Tab and Shift+Tab move between focusables, even with nothing focused.
7052        if matches!(event.logical_key, WinitKey::Named(NamedKey::Tab)) {
7053            let forward = !self.shift;
7054            let next = self
7055                .ui
7056                .as_ref()
7057                .and_then(|ui| ui.focus_next(self.runtime.input.focused, forward));
7058            if next.is_some() {
7059                self.runtime.input.focused = next;
7060                self.runtime.region = None;
7061                self.reveal_focus();
7062                self.request_redraw();
7063            }
7064            return;
7065        }
7066
7067        // Escape walks **leaf to root** from the focused widget — an `OverlayPortal`
7068        // consumes it to close itself — and failing that closes the topmost
7069        // overlay, so Escape works with nothing focused.
7070        if matches!(event.logical_key, WinitKey::Named(NamedKey::Escape)) {
7071            // Auto-repeat does not trigger another close.
7072            if !event.repeat {
7073                self.escape();
7074            }
7075            return;
7076        }
7077
7078        let Some(focused) = self.runtime.input.focused else {
7079            // With nothing focused, a selection an area holds still answers to the
7080            // clipboard's Copy and to Select all.
7081            self.region_key(event);
7082            return;
7083        };
7084
7085        // Arrows navigate focus **geometrically** — except left and right in a
7086        // text field, where they move the caret; up and down navigate even out
7087        // of a single-line field.
7088        let arrow = match event.logical_key {
7089            WinitKey::Named(NamedKey::ArrowUp) => Some(FocusDirection::Up),
7090            WinitKey::Named(NamedKey::ArrowDown) => Some(FocusDirection::Down),
7091            WinitKey::Named(NamedKey::ArrowLeft) => Some(FocusDirection::Left),
7092            WinitKey::Named(NamedKey::ArrowRight) => Some(FocusDirection::Right),
7093            _ => None,
7094        };
7095        // PgUp and PgDn jump a **page** inside a multi-line field, bounded to
7096        // the field so they never leave it. No effect anywhere else.
7097        if matches!(
7098            event.logical_key,
7099            WinitKey::Named(NamedKey::PageUp) | WinitKey::Named(NamedKey::PageDown)
7100        ) {
7101            let down = matches!(event.logical_key, WinitKey::Named(NamedKey::PageDown));
7102            if self.move_caret_vertical(focused, down, true) {
7103                return;
7104            }
7105        }
7106
7107        if let Some(direction) = arrow {
7108            // In a **multi-line** field, Up and Down move the caret between
7109            // lines while it stays in the field; on the first or last line we
7110            // fall back to focus navigation and leave the field.
7111            if matches!(direction, FocusDirection::Up | FocusDirection::Down) {
7112                let down = matches!(direction, FocusDirection::Down);
7113                if self.move_caret_vertical(focused, down, false) {
7114                    return;
7115                }
7116            }
7117
7118            // Left and right arrows are offered to the focused widget first —
7119            // a range slider, say — through `on_key`. If it consumes them, focus
7120            // does not move.
7121            if matches!(direction, FocusDirection::Left | FocusDirection::Right) {
7122                let key = if matches!(direction, FocusDirection::Left) {
7123                    Key::Left {
7124                        shift: self.shift,
7125                        word: self.ctrl,
7126                    }
7127                } else {
7128                    Key::Right {
7129                        shift: self.shift,
7130                        word: self.ctrl,
7131                    }
7132                };
7133                let widget = self
7134                    .tree
7135                    .as_ref()
7136                    .and_then(|tree| find_widget(tree.as_ref(), focused));
7137                // A reorderable header moves its column by one; we capture its
7138                // position to announce it, the keyboard being how screen-reader
7139                // users reorder.
7140                let reorder_from = widget.and_then(|w| w.reorder_index());
7141                let handled = widget.map(|w| w.on_key(&key));
7142                if let Some(KeyResponse::Handled(message)) = handled {
7143                    if let Some(message) = message {
7144                        self.dispatch(message);
7145                    }
7146                    if let Some(from) = reorder_from {
7147                        let to = if matches!(direction, FocusDirection::Left) {
7148                            from.wrapping_sub(1)
7149                        } else {
7150                            from + 1
7151                        };
7152                        self.set_announcement(format!("Column moved to position {}", to + 1));
7153                    }
7154                    self.request_redraw();
7155                    return;
7156                }
7157            }
7158
7159            let is_text = self
7160                .tree
7161                .as_ref()
7162                .and_then(|tree| find_widget(tree.as_ref(), focused))
7163                .is_some_and(is_text_field);
7164            let navigates =
7165                !is_text || matches!(direction, FocusDirection::Up | FocusDirection::Down);
7166            if navigates {
7167                if let Some(next) = self
7168                    .ui
7169                    .as_ref()
7170                    .and_then(|ui| ui.focus_directional(focused, direction))
7171                {
7172                    self.runtime.input.focused = Some(next);
7173                    self.reveal_focus();
7174                    self.request_redraw();
7175                }
7176                return;
7177            }
7178        }
7179
7180        // Home and End are offered to the focused widget — a range slider maps
7181        // them to min and max — before the default action. A text field ignores
7182        // them here (`on_key` returns Ignored) and falls back to ordinary
7183        // editing further down.
7184        if matches!(
7185            event.logical_key,
7186            WinitKey::Named(NamedKey::Home) | WinitKey::Named(NamedKey::End)
7187        ) {
7188            let key = if matches!(event.logical_key, WinitKey::Named(NamedKey::Home)) {
7189                Key::Home {
7190                    shift: self.shift,
7191                    doc: self.ctrl,
7192                }
7193            } else {
7194                Key::End {
7195                    shift: self.shift,
7196                    doc: self.ctrl,
7197                }
7198            };
7199            let handled = self
7200                .tree
7201                .as_ref()
7202                .and_then(|tree| find_widget(tree.as_ref(), focused))
7203                .map(|widget| widget.on_key(&key));
7204            if let Some(KeyResponse::Handled(message)) = handled {
7205                if let Some(message) = message {
7206                    self.dispatch(message);
7207                }
7208                self.request_redraw();
7209                return;
7210            }
7211        }
7212
7213        // Keyboard activation, Enter or Space, of a clickable focusable: a
7214        // button, a checkbox, a switch. Text fields, which have no `on_click`,
7215        // fall back to ordinary editing — Enter submits, Space is a space.
7216        if matches!(
7217            event.logical_key,
7218            WinitKey::Named(NamedKey::Enter) | WinitKey::Named(NamedKey::Space)
7219        ) {
7220            let widget = self
7221                .tree
7222                .as_ref()
7223                .and_then(|tree| find_widget(tree.as_ref(), focused))
7224                .filter(|widget| widget.focusable());
7225            let message = widget.and_then(|widget| widget.on_click());
7226            if let Some(message) = message {
7227                // Auto-repeat does not machine-gun the activation: holding
7228                // Space on a button is one single click.
7229                if !event.repeat {
7230                    // The spoken announcement of the effect — a sort, a
7231                    // selection — captured before `dispatch` rebuilds the tree.
7232                    let announce = widget.and_then(|widget| widget.announce());
7233                    self.dispatch(message);
7234                    if let Some(announce) = announce {
7235                        self.set_announcement(announce);
7236                    }
7237                    self.request_redraw();
7238                }
7239                return;
7240            }
7241        }
7242
7243        // The clipboard: Ctrl+C/X/V, and a keyboard's own Copy, Cut and Paste.
7244        if let Some(command) = clipboard_command(&event.logical_key, event.physical_key, self.ctrl)
7245        {
7246            match command {
7247                ClipCommand::Copy => {
7248                    self.copy_selection(focused);
7249                }
7250                ClipCommand::Cut => self.cut_selection(focused),
7251                ClipCommand::Paste => {
7252                    // Answered now, or — on the Web — on a later frame.
7253                    if let Some(pasted) = self.clipboard.paste(focused, self.window.as_ref()) {
7254                        self.land_paste(pasted);
7255                    }
7256                }
7257            }
7258            return;
7259        }
7260
7261        // The context-menu key, or Shift+F10: the selection bar for someone with no
7262        // pointer to hold or right-click with (milestone 568). Only a text field has
7263        // one, and it is opened on the field that has the focus.
7264        if opens_selection_bar(&event.logical_key, self.shift)
7265            && self
7266                .tree
7267                .as_ref()
7268                .and_then(|tree| find_widget(tree.as_ref(), focused))
7269                .is_some_and(|widget| widget.text_value().is_some())
7270        {
7271            self.show_selection_toolbar(focused);
7272            return;
7273        }
7274
7275        // The other editing shortcuts, Ctrl+A/Z/Y.
7276        if self.ctrl {
7277            match &event.logical_key {
7278                WinitKey::Character(c) if c.eq_ignore_ascii_case("a") => {
7279                    self.select_all(focused);
7280                    return;
7281                }
7282                // Undo, and redo under both its spellings — Ctrl+Y on Windows,
7283                // Ctrl+Shift+Z everywhere else, and people bring the one their
7284                // hands already know.
7285                WinitKey::Character(c) if c.eq_ignore_ascii_case("z") && !self.shift => {
7286                    self.step_history(focused, false);
7287                    self.request_redraw();
7288                    return;
7289                }
7290                WinitKey::Character(c)
7291                    if c.eq_ignore_ascii_case("y")
7292                        || (c.eq_ignore_ascii_case("z") && self.shift) =>
7293                {
7294                    self.step_history(focused, true);
7295                    self.request_redraw();
7296                    return;
7297                }
7298                _ => {}
7299            }
7300        }
7301
7302        // With the input bridge active, editing goes EXCLUSIVELY through the
7303        // InputConnection, that is, the IME queue — the bridge view receives the
7304        // hardware keys too. Without this guard every keystroke would arrive
7305        // twice, once from winit's native queue and once from the bridge.
7306        #[cfg(android)]
7307        if crate::android_ime::installed() {
7308            return;
7309        }
7310
7311        let shift = self.shift;
7312        let key = match &event.logical_key {
7313            WinitKey::Named(NamedKey::Backspace) => Some(Key::Backspace),
7314            WinitKey::Named(NamedKey::Delete) => Some(Key::Delete),
7315            // Repeating Enter does not submit again; text and deletion do
7316            // repeat normally.
7317            WinitKey::Named(NamedKey::Enter) if !event.repeat => Some(Key::Enter),
7318            WinitKey::Named(NamedKey::Enter) => None,
7319            // With Ctrl, Left/Right jump a word and Home/End bound the field.
7320            WinitKey::Named(NamedKey::ArrowLeft) => Some(Key::Left {
7321                shift,
7322                word: self.ctrl,
7323            }),
7324            WinitKey::Named(NamedKey::ArrowRight) => Some(Key::Right {
7325                shift,
7326                word: self.ctrl,
7327            }),
7328            WinitKey::Named(NamedKey::Home) => Some(Key::Home {
7329                shift,
7330                doc: self.ctrl,
7331            }),
7332            WinitKey::Named(NamedKey::End) => Some(Key::End {
7333                shift,
7334                doc: self.ctrl,
7335            }),
7336            WinitKey::Named(NamedKey::Space) => Some(Key::Text(" ".to_string())),
7337            // Android delivers Enter as `Character("\n")`, through the
7338            // KeyCharacterMap, rather than `Named(Enter)`: the same submission,
7339            // without inserting a line break into the field.
7340            WinitKey::Character(c) if c == "\n" || c == "\r" => {
7341                if event.repeat {
7342                    None
7343                } else {
7344                    Some(Key::Enter)
7345                }
7346            }
7347            _ => event.text.as_ref().map(|text| Key::Text(text.to_string())),
7348        };
7349
7350        if let Some(key) = key {
7351            self.apply_key(focused, key);
7352            self.request_redraw();
7353        }
7354    }
7355
7356    /// The **system** back — Android's `KEYCODE_BACK`, the browser's back key — and
7357    /// its native chain: ① the topmost overlay closes, be it a sheet, a drawer, a modal
7358    /// or a menu; ② failing that a screen is popped, by replaying the **back gesture
7359    /// already committed**, through the same hooks as the swipe, so it settles with an
7360    /// animation; ③ failing that, at the root, back **quits the application**, as
7361    /// Android expects.
7362    fn system_back(&mut self, event_loop: &ActiveEventLoop) {
7363        if !self.system_back_step() {
7364            event_loop.exit();
7365        }
7366    }
7367
7368    /// What the system's Back does short of leaving the application, and whether it did
7369    /// anything: `false` means there was nothing left to go back from.
7370    fn system_back_step(&mut self) -> bool {
7371        // An open selection bar goes first, as a platform's own text selection does: Back
7372        // closes it before it closes anything behind it (milestone 568).
7373        if self.close_selection_toolbar() {
7374            return true;
7375        }
7376        // Then a selection an area holds with its bar, as for a field's.
7377        if self
7378            .runtime
7379            .region
7380            .as_ref()
7381            .is_some_and(|selection| selection.bar)
7382        {
7383            self.runtime.region = None;
7384            self.request_redraw();
7385            return true;
7386        }
7387        if let Some(message) = self.ui.as_ref().and_then(|ui| ui.top_dismiss()) {
7388            self.dispatch(message);
7389            self.request_redraw();
7390            return true;
7391        }
7392        if self.app.can_go_back() {
7393            self.build_dirty = true;
7394            // A pop, not a swipe: no finger is on the page (milestone 622).
7395            self.app.go_back();
7396            self.request_redraw();
7397            return true;
7398        }
7399        false
7400    }
7401
7402    /// Routes **Escape**: a leaf-to-root walk from the focused widget, with a
7403    /// three-state result — `Ignored` keeps walking up, `Handled` consumes, `Skip` stops
7404    /// with no fallback — then falls back to closing the topmost overlay when nobody
7405    /// answered, or when nothing is focused.
7406    fn escape(&mut self) {
7407        // The selection bar first, as for the system's Back (milestone 568).
7408        if self.close_selection_toolbar() {
7409            return;
7410        }
7411        // Then a selection an area holds.
7412        if self.runtime.region.take().is_some() {
7413            self.request_redraw();
7414            return;
7415        }
7416        // 1) Walk up the focus path. `Some(None)` means consumed with no message; an
7417        // outer `None` means the whole path ignored it, so we fall back.
7418        let outcome: Option<Option<A::Message>> = self.runtime.input.focused.and_then(|focused| {
7419            let tree = self.tree.as_ref()?;
7420            let path = find_path(tree.as_ref(), focused);
7421            for widget in path.iter().rev() {
7422                match widget.on_key(&Key::Escape) {
7423                    KeyResponse::Handled(message) => return Some(message),
7424                    KeyResponse::Skip => return Some(None),
7425                    KeyResponse::Ignored => {}
7426                }
7427            }
7428            None
7429        });
7430
7431        match outcome {
7432            Some(message) => {
7433                if let Some(message) = message {
7434                    self.dispatch(message);
7435                    self.request_redraw();
7436                }
7437            }
7438            // 2) Nobody on the path, or nothing focused: the topmost overlay.
7439            None => {
7440                if let Some(message) = self.ui.as_ref().and_then(|ui| ui.top_dismiss()) {
7441                    self.dispatch(message);
7442                    self.request_redraw();
7443                }
7444            }
7445        }
7446    }
7447
7448    /// Moves the caret vertically in the focused multi-line field — Up/Down when
7449    /// `page` is false, PgUp/PgDn otherwise — preserving the **remembered goal column**.
7450    /// Applies the selection under Shift and reveals the caret. Returns `true` when the
7451    /// caret moved within the field, `false` otherwise, in which case the shell
7452    /// navigates focus instead.
7453    fn move_caret_vertical(&mut self, id: WidgetId, down: bool, page: bool) -> bool {
7454        let width = self
7455            .ui
7456            .as_ref()
7457            .and_then(|ui| ui.widget_rect(id))
7458            .map(|r| r.width)
7459            .unwrap_or(0.0);
7460        let cursor = self.runtime.edits.get(&id).map(|e| e.cursor).unwrap_or(0);
7461        let moved = self
7462            .tree
7463            .as_ref()
7464            .and_then(|tree| find_widget(tree.as_ref(), id))
7465            .and_then(|widget| widget.caret_vertical(width, cursor, down, page, self.goal_x));
7466        let Some((new_cursor, goal)) = moved else {
7467            return false;
7468        };
7469        let shift = self.shift;
7470        let edit = self.runtime.edits.entry(id).or_default();
7471        if shift {
7472            if edit.anchor.is_none() {
7473                edit.anchor = Some(edit.cursor);
7474            }
7475        } else {
7476            edit.anchor = None;
7477        }
7478        edit.cursor = new_cursor;
7479        // Remember the goal column for the next vertical jump.
7480        self.goal_x = Some(goal);
7481        self.reveal_caret(id, new_cursor);
7482        self.request_redraw();
7483        true
7484    }
7485
7486    /// A hold in field `id` selects the word under the finger — the press has already
7487    /// put the caret there — and gives the selection its handles (milestone 511).
7488    fn select_held_word(&mut self, id: WidgetId) {
7489        let cursor = self.runtime.edits.get(&id).map(|e| e.cursor).unwrap_or(0);
7490        let word = self
7491            .tree
7492            .as_ref()
7493            .and_then(|tree| find_widget(tree.as_ref(), id))
7494            .and_then(|widget| widget.word_at(cursor))
7495            .filter(|(start, end)| start < end);
7496        let Some((start, end)) = word else {
7497            // Nothing to select — an empty field, a hold past the end of the text. The
7498            // bar still opens, at the caret, with Paste and whatever else applies.
7499            self.show_selection_toolbar(id);
7500            return;
7501        };
7502        self.runtime.edits.insert(
7503            id,
7504            Edit {
7505                cursor: end,
7506                anchor: Some(start),
7507                composing: None,
7508            },
7509        );
7510        self.runtime.selection_handles = Some(id);
7511        self.runtime.close_edit_run(id);
7512        self.show_selection_toolbar(id);
7513        #[cfg(android)]
7514        self.push_ime_context(id);
7515    }
7516
7517    /// The selection handle under the pointer, taken: a drag that moves its end of the
7518    /// focused field's selection. `None` when no handle is showing there.
7519    fn grab_selection_handle(&self) -> Option<Drag> {
7520        let id = self.runtime.selection_handles?;
7521        if self.runtime.input.focused != Some(id) {
7522            return None;
7523        }
7524        let rect = self.ui.as_ref()?.widget_rect(id)?;
7525        let edit = self.runtime.edits.get(&id).copied()?;
7526        let scroll_y = self.runtime.scroll.get(&id).map(|s| s.1).unwrap_or(0.0);
7527        let handles = find_widget(self.tree.as_ref()?.as_ref(), id)?
7528            .selection_handles(rect.width, &edit, scroll_y)?;
7529        let local = Point::new(self.cursor.x - rect.x, self.cursor.y - rect.y);
7530        let handle = crate::selection::grab(&handles, local)?;
7531        let at = handles[handle.index()].line_center;
7532        Some(Drag::SelectionHandle {
7533            id,
7534            rect,
7535            handle,
7536            grab: Point::new(at.x - local.x, at.y - local.y),
7537        })
7538    }
7539
7540    /// Routes a key to the focused field: updates the editing state and applies
7541    /// whatever message comes out, a value change or a submission.
7542    fn apply_key(&mut self, id: WidgetId, key: Key) {
7543        // Any horizontal move, or any keystroke, forgets the vertical goal column.
7544        self.goal_x = None;
7545        // Typing puts a touch selection's handles away, as a press does (milestone 511) —
7546        // and the bar over it (milestone 568).
7547        self.runtime.selection_handles = None;
7548        self.runtime.selection_toolbar = None;
7549        let was = self.runtime.edits.get(&id).copied().unwrap_or_default();
7550        let mut edit = was;
7551        let widget = self
7552            .tree
7553            .as_ref()
7554            .and_then(|tree| find_widget(tree.as_ref(), id));
7555        // What the field held before the key: half of an undo step, and the half the
7556        // application owns.
7557        let before = widget
7558            .and_then(|widget| widget.text_value())
7559            .map(str::to_owned);
7560        // A field's own announcement (#18) — read here for the same reason the click
7561        // and keyboard-activation paths read theirs off the widget **before**
7562        // `dispatch_edit` rebuilds the tree: a plain keystroke leaves it unused, since
7563        // `on_edit` answers `None` for one and the announcement below is never spoken.
7564        let announce = widget.and_then(|widget| widget.announce());
7565        let message = widget.and_then(|widget| widget.on_edit(&mut edit, &key));
7566        self.runtime.edits.insert(id, edit);
7567        // In a multi-line field, make the retained scroll follow the caret and reveal it.
7568        self.reveal_caret(id, edit.cursor);
7569        if let Some(message) = message {
7570            self.dispatch_edit(message);
7571            if let Some(announce) = announce {
7572                self.set_announcement(announce);
7573            }
7574        }
7575        // The history, recorded on the **evidence** of a changed value rather than on the
7576        // intent of a key that usually changes one: a filter may have refused the
7577        // character, a length limit may have swallowed it, a read-only field ignores it,
7578        // and an Enter submits instead of typing. The value is read again from the tree
7579        // the dispatch has just rebuilt, so what is recorded is what happened.
7580        let after = self
7581            .tree
7582            .as_ref()
7583            .and_then(|tree| find_widget(tree.as_ref(), id))
7584            .and_then(|widget| widget.text_value())
7585            .map(str::to_owned);
7586        match (before, after) {
7587            (Some(before), Some(after)) if before != after => {
7588                let kind = EditKind::of(&key, was.selection_range().is_some(), self.composing());
7589                let since = self.since_last_edit();
7590                self.runtime
7591                    .record_edit(id, EditSnapshot::new(before, was), kind, since);
7592            }
7593            // A key that moved only the caret ends the run: what is typed next is a step
7594            // of its own, or one undo would take back two visits to the field at once.
7595            (Some(_), _) => self.runtime.close_edit_run(id),
7596            _ => {}
7597        }
7598    }
7599
7600    /// Dispatches a message that changed a field's value, and refreshes the tree at once.
7601    ///
7602    /// Keys can arrive in **bursts**, faster than a frame — a software keyboard, `adb
7603    /// input text`, auto-repeat — and the next one must see the CURRENT value, not the
7604    /// retained tree's, or it would overwrite the previous keystroke. So the tree is
7605    /// refreshed right away; `build_dirty` stays raised and the next frame redoes the full
7606    /// pass: mounts, leaving fades and all.
7607    fn dispatch_edit(&mut self, message: A::Message) {
7608        self.dispatch(message);
7609        if let Some((width, height)) = self.last_size {
7610            let theme = self.themes.displayed(&self.app);
7611            // A surface of its own, because this build happens between frames rather than
7612            // inside one — and the same `build_view` as the frame path, because the next
7613            // key in the burst reads this tree straight away.
7614            let tree = self
7615                .media_query(width, height)
7616                .scope(|| build_view(&self.app, &theme, &self.runtime));
7617            self.tree = Some(tree);
7618        }
7619    }
7620
7621    /// Seconds since the last recorded edit, and stamps this one. The first edit is
7622    /// infinitely far from the one before it, which is to say it starts a run.
7623    fn since_last_edit(&mut self) -> f32 {
7624        let now = Instant::now();
7625        let since = self
7626            .last_edit_at
7627            .map(|then| now.duration_since(then).as_secs_f32())
7628            .unwrap_or(f32::INFINITY);
7629        self.last_edit_at = Some(now);
7630        since
7631    }
7632
7633    /// Whether an input method is in the middle of composing a word.
7634    fn composing(&self) -> bool {
7635        self.runtime
7636            .input
7637            .focused
7638            .and_then(|id| self.runtime.edits.get(&id))
7639            .is_some_and(|edit| edit.composing.is_some())
7640    }
7641
7642    /// Steps a text field's value back one change, or forward again. `true` when something
7643    /// moved — Ctrl+Z on anything that is not a field, or with nothing left to undo, moves
7644    /// nothing and says so.
7645    fn step_history(&mut self, id: WidgetId, forward: bool) -> bool {
7646        let value = self
7647            .tree
7648            .as_ref()
7649            .and_then(|tree| find_widget(tree.as_ref(), id))
7650            .and_then(|widget| widget.text_value())
7651            .map(str::to_owned);
7652        let Some(value) = value else {
7653            return false;
7654        };
7655        let edit = self.runtime.edits.get(&id).copied().unwrap_or_default();
7656        let current = EditSnapshot::new(value, edit);
7657        let target = if forward {
7658            self.runtime.redo_edit(id, current)
7659        } else {
7660            self.runtime.undo_edit(id, current)
7661        };
7662        let Some(target) = target else {
7663            return false;
7664        };
7665        let message = self
7666            .tree
7667            .as_ref()
7668            .and_then(|tree| find_widget(tree.as_ref(), id))
7669            .and_then(|widget| widget.replace_value(target.value.clone()));
7670        // The caret goes back with the text — an undo that restores the value and leaves
7671        // the caret at the end has done half the job.
7672        self.runtime.edits.insert(id, target.edit);
7673        self.reveal_caret(id, target.edit.cursor);
7674        if let Some(message) = message {
7675            self.dispatch_edit(message);
7676        }
7677        // An undo is not a keystroke: what is typed after it starts a run of its own,
7678        // however quickly it follows.
7679        self.last_edit_at = None;
7680        true
7681    }
7682
7683    /// Brings the focused widget **into view**, gliding every scroll region around it.
7684    ///
7685    /// Called wherever the keyboard moves the focus, and nowhere else. A click already
7686    /// landed on something the eye could see, and a focus restored because an overlay
7687    /// closed should put the page back where the reader left it, not chase whatever the
7688    /// restoration happened to pick.
7689    ///
7690    /// The glide is a **target**, not a jump: the same easing a scrollbar drag or a page
7691    /// request uses, so Tab through a form reads as one movement rather than a series of
7692    /// cuts. Anything already moving the offset is let go of — the keyboard has just
7693    /// overruled it.
7694    fn reveal_focus(&mut self) {
7695        let Some(id) = self.runtime.input.focused else {
7696            return;
7697        };
7698        let Some(moves) = self.ui.as_ref().map(|ui| ui.reveal(id, &self.runtime)) else {
7699            return;
7700        };
7701        for (area, offset) in moves {
7702            self.runtime.scroll_ballistic.remove(&area);
7703            self.runtime.scroll_velocity.remove(&area);
7704            self.runtime.scroll_target.insert(area, offset);
7705        }
7706    }
7707
7708    /// Makes a multi-line field's retained scroll **follow the caret**: adjusts
7709    /// `runtime.scroll[id]` just enough to keep the caret visible, the way an editor
7710    /// re-centres as you type. A no-op for a field that does not scroll.
7711    fn reveal_caret(&mut self, id: WidgetId, cursor: usize) {
7712        let Some(vp) = self.ui.as_ref().and_then(|ui| ui.scrollable_viewport(id)) else {
7713            return;
7714        };
7715        let metrics = self
7716            .tree
7717            .as_ref()
7718            .and_then(|tree| find_widget(tree.as_ref(), id))
7719            .and_then(|widget| widget.text_metrics(vp.width, cursor));
7720        let Some((content_h, visible_h, caret_top, caret_h)) = metrics else {
7721            return;
7722        };
7723        let max_y = (content_h - visible_h).max(0.0);
7724        let cur = self.runtime.scroll.get(&id).map(|s| s.1).unwrap_or(0.0);
7725        // The scroll window in which the caret stays visible, from "the caret's bottom
7726        // is visible" to "the caret's top is visible". We bring the current scroll into it.
7727        let lo = (caret_top + caret_h - visible_h).max(0.0);
7728        let hi = caret_top;
7729        let target = cur.clamp(lo.min(hi), lo.max(hi)).clamp(0.0, max_y);
7730        self.runtime.scroll.insert(id, (0.0, target));
7731        self.runtime.scroll_target.insert(id, (0.0, target));
7732        self.runtime.scroll_velocity.remove(&id);
7733    }
7734
7735    /// Copies field `id`'s selected text to the clipboard. `false` when there was nothing
7736    /// to copy — no selection, or a field that gives its text to nobody (a masked one).
7737    fn copy_selection(&mut self, id: WidgetId) -> bool {
7738        let edit = self.runtime.edits.get(&id).copied().unwrap_or_default();
7739        let text = self
7740            .tree
7741            .as_ref()
7742            .and_then(|tree| find_widget(tree.as_ref(), id))
7743            .and_then(|widget| widget.selected_text(&edit));
7744        match text {
7745            Some(text) => {
7746                self.clipboard.set_text(text);
7747                true
7748            }
7749            None => false,
7750        }
7751    }
7752
7753    /// The texts of `area` in the frame on screen, in reading order, each with its length in
7754    /// characters — what a selection over them is worked out from.
7755    fn region_stops(&self, area: WidgetId) -> Vec<(WidgetId, usize)> {
7756        let (Some(ui), Some(tree)) = (self.ui.as_ref(), self.tree.as_ref()) else {
7757            return Vec::new();
7758        };
7759        ui.text_stops_in(area)
7760            .filter_map(|stop| {
7761                let text = find_widget(tree.as_ref(), stop.id)?.selection_text()?;
7762                Some((stop.id, text.chars().count()))
7763            })
7764            .collect()
7765    }
7766
7767    /// The selection from `anchor` to `extent` in `area`, in the frame on screen.
7768    fn region_selection(
7769        &self,
7770        area: WidgetId,
7771        anchor: RegionPoint,
7772        extent: RegionPoint,
7773    ) -> RegionSelection {
7774        RegionSelection::new(area, anchor, extent, &self.region_stops(area))
7775    }
7776
7777    /// The character boundary of `stop`'s text nearest `at`, by the layout it was drawn with.
7778    fn region_index(&self, stop: &TextStop, at: Point) -> Option<usize> {
7779        find_widget(self.tree.as_ref()?.as_ref(), stop.id)?.selection_hit(
7780            at.x - stop.rect.x,
7781            at.y - stop.rect.y,
7782            stop.rect.width,
7783        )
7784    }
7785
7786    /// A press at the pointer over words in a selection area, if nothing else has a claim on
7787    /// it: the selection begins there, empty, and follows the pointer until it lifts. A double
7788    /// click selects the word.
7789    fn begin_region_selection(&mut self) {
7790        let Some(stop) = self.region_press_target() else {
7791            return;
7792        };
7793        let Some(index) = self.region_index(&stop, self.cursor) else {
7794            return;
7795        };
7796        let now = Instant::now();
7797        let here = self.cursor;
7798        let double = self.last_region_click.is_some_and(|(then, there)| {
7799            (now - then).as_secs_f32() < 0.4
7800                && (here.x - there.x).hypot(here.y - there.y) <= REGION_DOUBLE_CLICK_SLOP
7801        });
7802        self.last_region_click = Some((now, here));
7803        let word = double
7804            .then(|| {
7805                let tree = self.tree.as_ref()?;
7806                let text = find_widget(tree.as_ref(), stop.id)?.selection_text()?;
7807                Some(frus_widgets::word_bounds(text, index))
7808            })
7809            .flatten();
7810        let (anchor, extent) = match word {
7811            Some((start, end)) => (
7812                RegionPoint {
7813                    text: stop.id,
7814                    index: start,
7815                },
7816                RegionPoint {
7817                    text: stop.id,
7818                    index: end,
7819                },
7820            ),
7821            None => {
7822                let point = RegionPoint {
7823                    text: stop.id,
7824                    index,
7825                };
7826                (point, point)
7827            }
7828        };
7829        self.runtime.region = Some(self.region_selection(stop.area, anchor, extent));
7830        // A word is a whole selection: there is nothing to drag out of it.
7831        self.drag = word
7832            .is_none()
7833            .then_some(Drag::RegionSelect { area: stop.area });
7834        self.request_redraw();
7835    }
7836
7837    /// The text of a selection area under the pointer, if a press there is the area's to take:
7838    /// something that answers a press — a button, a tappable row, a link — keeps it.
7839    fn region_press_target(&self) -> Option<TextStop> {
7840        let ui = self.ui.as_ref()?;
7841        let claimed = self
7842            .runtime
7843            .input
7844            .pressed
7845            .is_some_and(|pressed| ui.msg_for(pressed).is_some());
7846        if claimed {
7847            return None;
7848        }
7849        ui.text_stop_at(self.cursor)
7850    }
7851
7852    /// A finger held on words in a selection area: the word under it is selected and the bar
7853    /// opens over it. Whether there was a word.
7854    fn select_region_word_held(&mut self) -> bool {
7855        let Some(stop) = self.ui.as_ref().and_then(|ui| ui.text_stop_at(self.cursor)) else {
7856            return false;
7857        };
7858        let Some(index) = self.region_index(&stop, self.cursor) else {
7859            return false;
7860        };
7861        let word = self
7862            .tree
7863            .as_ref()
7864            .and_then(|tree| find_widget(tree.as_ref(), stop.id))
7865            .and_then(|widget| widget.selection_text())
7866            .map(|text| frus_widgets::word_bounds(text, index))
7867            .filter(|(start, end)| start < end);
7868        let Some((start, end)) = word else {
7869            return false;
7870        };
7871        let anchor = RegionPoint {
7872            text: stop.id,
7873            index: start,
7874        };
7875        let extent = RegionPoint {
7876            text: stop.id,
7877            index: end,
7878        };
7879        self.runtime.region = Some(
7880            self.region_selection(stop.area, anchor, extent)
7881                .with_bar(true),
7882        );
7883        self.request_redraw();
7884        true
7885    }
7886
7887    /// The two handles of a selection area's touch selection, where they are on the surface.
7888    fn region_handles(&self) -> Option<(WidgetId, [frus_widgets::SelectionHandle; 2])> {
7889        let selection = self.runtime.region.as_ref().filter(|s| s.handles)?;
7890        let (ui, tree) = (self.ui.as_ref()?, self.tree.as_ref()?);
7891        let at = |point: RegionPoint, start: bool| {
7892            let stop = ui
7893                .text_stops_in(selection.area)
7894                .find(|s| s.id == point.text)?;
7895            let handle = find_widget(tree.as_ref(), point.text)?.selection_grip(
7896                stop.rect.width,
7897                point.index,
7898                start,
7899            )?;
7900            let (dx, dy) = (stop.rect.x, stop.rect.y);
7901            Some(frus_widgets::SelectionHandle {
7902                rect: handle.rect.translate(dx, dy),
7903                line_center: Point::new(handle.line_center.x + dx, handle.line_center.y + dy),
7904            })
7905        };
7906        Some((
7907            selection.area,
7908            [at(selection.start?, true)?, at(selection.end?, false)?],
7909        ))
7910    }
7911
7912    /// The handle of a selection area's touch selection under the pointer, taken.
7913    fn grab_region_handle(&self) -> Option<Drag> {
7914        let (area, handles) = self.region_handles()?;
7915        let handle = crate::selection::grab(&handles, self.cursor)?;
7916        let at = handles[handle.index()].line_center;
7917        Some(Drag::RegionHandle {
7918            area,
7919            handle,
7920            grab: Point::new(at.x - self.cursor.x, at.y - self.cursor.y),
7921        })
7922    }
7923
7924    /// A handle of a selection area moved with the finger: its end goes to the text position
7925    /// under it — carried by `grab`, so the handle's line and not the fingertip picks it — and
7926    /// the other end stays. The two never meet nor cross, as a field's do not.
7927    fn drag_region_handle(
7928        &mut self,
7929        area: WidgetId,
7930        handle: crate::selection::Handle,
7931        grab: Point,
7932    ) {
7933        let Some(selection) = self.runtime.region.as_ref() else {
7934            return;
7935        };
7936        let (Some(start), Some(end)) = (selection.start, selection.end) else {
7937            return;
7938        };
7939        let target = Point::new(self.cursor.x + grab.x, self.cursor.y + grab.y);
7940        let Some(stop) = self
7941            .ui
7942            .as_ref()
7943            .and_then(|ui| ui.nearest_text_stop(area, target))
7944        else {
7945            return;
7946        };
7947        let Some(index) = self.region_index(&stop, target) else {
7948            return;
7949        };
7950        let moved = RegionPoint {
7951            text: stop.id,
7952            index,
7953        };
7954        let stops = self.region_stops(area);
7955        let order = |point: RegionPoint| {
7956            stops
7957                .iter()
7958                .position(|(id, _)| *id == point.text)
7959                .map(|at| (at, point.index))
7960        };
7961        let (Some(to), Some(other)) = (
7962            order(moved),
7963            order(match handle {
7964                crate::selection::Handle::Start => end,
7965                crate::selection::Handle::End => start,
7966            }),
7967        ) else {
7968            return;
7969        };
7970        let (fixed, crosses) = match handle {
7971            crate::selection::Handle::Start => (end, to >= other),
7972            crate::selection::Handle::End => (start, to <= other),
7973        };
7974        if crosses {
7975            return;
7976        }
7977        let next = RegionSelection::new(area, fixed, moved, &stops).with_bar(true);
7978        // The bar stays put away until the handle is let go.
7979        let next = RegionSelection { bar: false, ..next };
7980        if self.runtime.region.as_ref() != Some(&next) {
7981            self.runtime.region = Some(next);
7982            self.request_redraw();
7983        }
7984    }
7985
7986    /// The pointer has moved during a drag in `area`: the far end of the selection goes to the
7987    /// text nearest it.
7988    fn extend_region_selection(&mut self, area: WidgetId) {
7989        let Some(anchor) = self.runtime.region.as_ref().map(|region| region.anchor) else {
7990            return;
7991        };
7992        let Some(stop) = self
7993            .ui
7994            .as_ref()
7995            .and_then(|ui| ui.nearest_text_stop(area, self.cursor))
7996        else {
7997            return;
7998        };
7999        let Some(index) = self.region_index(&stop, self.cursor) else {
8000            return;
8001        };
8002        let extent = RegionPoint {
8003            text: stop.id,
8004            index,
8005        };
8006        let selection = self.region_selection(area, anchor, extent);
8007        if self.runtime.region.as_ref() != Some(&selection) {
8008            self.runtime.region = Some(selection);
8009            self.request_redraw();
8010        }
8011    }
8012
8013    /// The words a selection area has selected, as they are copied: one line per text. `None`
8014    /// when nothing is selected.
8015    fn region_text(&self) -> Option<String> {
8016        let region = self.runtime.region.as_ref()?;
8017        let (ui, tree) = (self.ui.as_ref()?, self.tree.as_ref()?);
8018        let texts: Vec<(WidgetId, &str)> = ui
8019            .text_stops_in(region.area)
8020            .filter_map(|stop| {
8021                let text = find_widget(tree.as_ref(), stop.id)?.selection_text()?;
8022                Some((stop.id, text))
8023            })
8024            .collect();
8025        let copied = frus_widgets::region_copy(&texts, &region.ranges);
8026        (!copied.is_empty()).then_some(copied)
8027    }
8028
8029    /// Puts the words a selection area has selected on the clipboard. Whether there were any.
8030    fn copy_region(&mut self) -> bool {
8031        match self.region_text() {
8032            Some(copied) => {
8033                self.clipboard.set_text(copied);
8034                true
8035            }
8036            None => false,
8037        }
8038    }
8039
8040    /// Selects every text of the area that holds a selection.
8041    fn select_all_region(&mut self) {
8042        let Some(area) = self.runtime.region.as_ref().map(|region| region.area) else {
8043            return;
8044        };
8045        let stops = self.region_stops(area);
8046        let (Some(first), Some(last)) = (stops.first(), stops.last()) else {
8047            return;
8048        };
8049        let anchor = RegionPoint {
8050            text: first.0,
8051            index: 0,
8052        };
8053        let extent = RegionPoint {
8054            text: last.0,
8055            index: last.1,
8056        };
8057        let bar = self
8058            .runtime
8059            .region
8060            .as_ref()
8061            .is_some_and(|selection| selection.bar);
8062        self.runtime.region = Some(self.region_selection(area, anchor, extent).with_bar(bar));
8063        self.request_redraw();
8064    }
8065
8066    /// A key while nothing has the focus: Copy, and Select all, act on a selection an area
8067    /// holds. Whether the key was one of them.
8068    fn region_key(&mut self, event: &KeyDown) -> bool {
8069        if self.runtime.region.is_none() {
8070            return false;
8071        }
8072        if let Some(ClipCommand::Copy) =
8073            clipboard_command(&event.logical_key, event.physical_key, self.ctrl)
8074        {
8075            self.copy_region();
8076            return true;
8077        }
8078        let select_all = self.ctrl
8079            && matches!(&event.logical_key, WinitKey::Character(c) if c.eq_ignore_ascii_case("a"));
8080        if select_all {
8081            self.select_all_region();
8082            return true;
8083        }
8084        false
8085    }
8086
8087    /// Cuts field `id`'s selection: copies it, then deletes it — **only if it was copied**.
8088    /// A masked field refuses the copy, and a cut that deleted anyway would destroy the
8089    /// text while putting it nowhere.
8090    fn cut_selection(&mut self, id: WidgetId) {
8091        if self.copy_selection(id) {
8092            self.apply_key(id, Key::Backspace);
8093            self.request_redraw();
8094        }
8095    }
8096
8097    /// Selects all of field `id`'s text.
8098    fn select_all(&mut self, id: WidgetId) {
8099        self.runtime.edits.insert(
8100            id,
8101            Edit {
8102                cursor: usize::MAX,
8103                anchor: Some(0),
8104                composing: None,
8105            },
8106        );
8107        // A selection is a caret move: what is typed over it is a step of its own, not more
8108        // of whatever was being typed before.
8109        self.runtime.close_edit_run(id);
8110        self.request_redraw();
8111    }
8112
8113    /// Opens field `id`'s **selection bar** — Cut, Copy, Paste, Select all — over its
8114    /// selection (milestone 568). Whether the clipboard holds text is asked once, here, and
8115    /// kept for as long as the bar is open: it is what decides whether Paste is offered.
8116    fn show_selection_toolbar(&mut self, id: WidgetId) {
8117        self.show_selection_toolbar_at(id, None);
8118    }
8119
8120    /// The same, opened by a right-click at `at`: the bar goes there instead of to the
8121    /// selection (milestone 618).
8122    fn show_selection_toolbar_at(&mut self, id: WidgetId, at: Option<Point>) {
8123        let can_paste = self.clipboard.has_text();
8124        self.runtime.selection_toolbar = Some(ToolbarMark { id, can_paste, at });
8125        self.request_redraw();
8126    }
8127
8128    /// What Back and Escape do to an open selection bar: put it away, with the handles that
8129    /// went with it, and say so — `true` — so that the key goes no further.
8130    fn close_selection_toolbar(&mut self) -> bool {
8131        if self.runtime.selection_toolbar.is_none() {
8132            return false;
8133        }
8134        self.runtime.selection_handles = None;
8135        self.hide_selection_toolbar();
8136        true
8137    }
8138
8139    /// Puts the bar away, if one is open.
8140    fn hide_selection_toolbar(&mut self) {
8141        if self.runtime.selection_toolbar.take().is_some() {
8142            self.request_redraw();
8143        }
8144    }
8145
8146    /// What pressing a built-in button of the selection bar does, to the field the bar is
8147    /// open on. Copy, Cut and Paste close it and the handles with it; Select all leaves
8148    /// both open on a phone, on the selection it has just made larger, and closes both on a
8149    /// desktop (milestone 625).
8150    fn perform_edit_action(&mut self, action: frus_widgets::EditAction) {
8151        use frus_widgets::EditAction;
8152        // The bar of a selection area's selection, which acts on the area and not on a field.
8153        if self
8154            .runtime
8155            .region
8156            .as_ref()
8157            .is_some_and(|selection| selection.bar)
8158        {
8159            match action {
8160                EditAction::Copy => {
8161                    // As a platform's own selection does after a copy: it is put away.
8162                    self.copy_region();
8163                    self.runtime.region = None;
8164                }
8165                EditAction::SelectAll => self.select_all_region(),
8166                EditAction::Cut | EditAction::Paste => {}
8167            }
8168            self.request_redraw();
8169            return;
8170        }
8171        let Some(id) = self.runtime.selection_toolbar.map(|mark| mark.id) else {
8172            return;
8173        };
8174        match action {
8175            EditAction::Copy => {
8176                if self.copy_selection(id) {
8177                    // As the platforms' own text fields do after a copy: the selection
8178                    // collapses to its end, and what marked it is put away.
8179                    if let Some(edit) = self.runtime.edits.get_mut(&id) {
8180                        *edit = collapsed_to_end(*edit);
8181                    }
8182                    self.runtime.close_edit_run(id);
8183                }
8184                self.runtime.selection_handles = None;
8185                self.hide_selection_toolbar();
8186            }
8187            EditAction::Cut => {
8188                // `apply_key` puts the handles and the bar away, as any key does.
8189                self.cut_selection(id);
8190                self.hide_selection_toolbar();
8191            }
8192            EditAction::Paste => {
8193                self.runtime.selection_handles = None;
8194                self.hide_selection_toolbar();
8195                // Answered now, or — on the Web — on a later frame.
8196                if let Some(pasted) = self.clipboard.paste(id, self.window.as_ref()) {
8197                    self.land_paste(pasted);
8198                }
8199            }
8200            EditAction::SelectAll => {
8201                self.select_all(id);
8202                // Made with a finger or a right-click, the selection keeps whatever
8203                // marked it.
8204                if self.runtime.selection_handles.is_some() {
8205                    self.runtime.selection_handles = Some(id);
8206                }
8207                if select_all_hides_bar(frus_widgets::default_target_platform()) {
8208                    // On a desktop the menu has done its work, and goes with the handles,
8209                    // as the reference's `hideToolbar()` takes both (milestone 625).
8210                    self.runtime.selection_handles = None;
8211                    self.hide_selection_toolbar();
8212                } else {
8213                    // On a phone the bar stays, with the list its new state calls for,
8214                    // where it was: a menu a right-click opened stays at the pointer.
8215                    let at = self.runtime.selection_toolbar.and_then(|mark| mark.at);
8216                    self.show_selection_toolbar_at(id, at);
8217                }
8218            }
8219        }
8220        #[cfg(android)]
8221        self.push_ime_context(id);
8222    }
8223
8224    /// A right-click on a text field (a desktop's context menu, milestone 568): focuses it,
8225    /// puts the caret where the click landed unless it landed inside the selection there
8226    /// already is, and opens the bar. On anything else it puts an open bar away.
8227    fn context_click(&mut self) {
8228        // Something that answers a secondary tap takes it (milestone 581).
8229        let secondary = self
8230            .ui
8231            .as_ref()
8232            .and_then(|ui| ui.hit(self.cursor))
8233            .and_then(|id| {
8234                self.tree
8235                    .as_ref()
8236                    .and_then(|tree| find_widget(tree.as_ref(), id))
8237            })
8238            .and_then(|widget| widget.on_secondary_tap());
8239        if let Some(message) = secondary {
8240            self.hide_selection_toolbar();
8241            self.dispatch(message);
8242            self.request_redraw();
8243            return;
8244        }
8245        let hit = self.ui.as_ref().and_then(|ui| ui.focus_hit(self.cursor));
8246        let Some((id, rect)) = hit else {
8247            self.hide_selection_toolbar();
8248            return;
8249        };
8250        let was_focused = self.runtime.input.focused == Some(id);
8251        let scroll_y = self.runtime.scroll.get(&id).map(|s| s.1).unwrap_or(0.0);
8252        let scroll_cursor = if was_focused {
8253            self.runtime.edits.get(&id).map(|e| e.cursor).unwrap_or(0)
8254        } else {
8255            0
8256        };
8257        let cursor = self
8258            .tree
8259            .as_ref()
8260            .and_then(|tree| find_widget(tree.as_ref(), id))
8261            .and_then(|widget| {
8262                widget.cursor_at(
8263                    self.cursor.x - rect.x,
8264                    self.cursor.y - rect.y + scroll_y,
8265                    rect.width,
8266                    scroll_cursor,
8267                )
8268            });
8269        // Only a text field has a bar; a button under the pointer is not one.
8270        let Some(cursor) = cursor else {
8271            self.hide_selection_toolbar();
8272            return;
8273        };
8274        let inside = was_focused
8275            && self
8276                .runtime
8277                .edits
8278                .get(&id)
8279                .and_then(|edit| edit.selection_range())
8280                .is_some_and(|(start, end)| cursor >= start && cursor <= end);
8281        self.runtime.input.focused = Some(id);
8282        self.runtime.selection_handles = None;
8283        if !inside {
8284            self.goal_x = None;
8285            self.runtime.edits.insert(
8286                id,
8287                Edit {
8288                    cursor,
8289                    anchor: None,
8290                    composing: None,
8291                },
8292            );
8293            self.runtime.close_edit_run(id);
8294        }
8295        self.show_selection_toolbar_at(id, Some(self.cursor));
8296    }
8297
8298    /// Types what the clipboard answered into the field that asked for it, if the paste
8299    /// still lands there — see `clip::Pasted::lands`.
8300    fn land_paste(&mut self, pasted: clip::Pasted) {
8301        if pasted.lands(self.runtime.input.focused) {
8302            self.apply_key(pasted.into, Key::Text(pasted.text));
8303            self.request_redraw();
8304        }
8305    }
8306}
8307
8308/// The edit a **copy** leaves behind: the selection collapsed to its end, the caret where
8309/// the selection stopped. What the platforms' own text fields do after Copy from their bar,
8310/// and what tells the reader it happened — the highlight goes.
8311fn collapsed_to_end(edit: Edit) -> Edit {
8312    let cursor = edit.selection_range().map_or(edit.cursor, |(_, end)| end);
8313    Edit {
8314        cursor,
8315        anchor: None,
8316        composing: None,
8317    }
8318}
8319
8320/// Which end of a value drag is being announced.
8321#[derive(Clone, Copy, PartialEq, Eq)]
8322enum Edge {
8323    Start,
8324    End,
8325}
8326
8327/// Was the gesture that just ended **still a tap**?
8328///
8329/// Every drag here engages only past a threshold, so a press that never crossed it is
8330/// not a drag at all: it is a tap, and the release owes the widget underneath its
8331/// click. A drag that *did* move has already been answered — a fling, a drop, a
8332/// dismissal — and must not also click.
8333///
8334/// One list, because forgetting a variant is silent: the widget stays hittable, the
8335/// press still records it, and only the release quietly does nothing. That is how a
8336/// dismissible row came to swallow every tap on it (milestone 327).
8337/// The window's cursor for one the interface asks for. A hidden cursor is the default
8338/// shape, made invisible by the caller.
8339fn cursor_icon(cursor: UiCursor) -> CursorIcon {
8340    match cursor {
8341        UiCursor::Default | UiCursor::None => CursorIcon::Default,
8342        UiCursor::Pointer => CursorIcon::Pointer,
8343        UiCursor::Text => CursorIcon::Text,
8344        UiCursor::Forbidden => CursorIcon::NotAllowed,
8345        UiCursor::Wait => CursorIcon::Wait,
8346        UiCursor::Progress => CursorIcon::Progress,
8347        UiCursor::Help => CursorIcon::Help,
8348        UiCursor::Precise => CursorIcon::Crosshair,
8349        UiCursor::Move => CursorIcon::Move,
8350        UiCursor::Grab => CursorIcon::Grab,
8351        UiCursor::Grabbing => CursorIcon::Grabbing,
8352        UiCursor::ResizeColumn => CursorIcon::ColResize,
8353        UiCursor::ResizeRow => CursorIcon::RowResize,
8354        UiCursor::ZoomIn => CursorIcon::ZoomIn,
8355        UiCursor::ZoomOut => CursorIcon::ZoomOut,
8356        UiCursor::ContextMenu => CursorIcon::ContextMenu,
8357    }
8358}
8359
8360/// A movement, or a velocity, kept to the way a detector drags.
8361fn mask_pan(axis: frus_widgets::PanAxis, value: Point) -> Point {
8362    match axis {
8363        frus_widgets::PanAxis::Free => value,
8364        frus_widgets::PanAxis::Horizontal => Point::new(value.x, 0.0),
8365        frus_widgets::PanAxis::Vertical => Point::new(0.0, value.y),
8366    }
8367}
8368
8369fn gesture_was_a_tap(ended: Option<&Drag>) -> bool {
8370    matches!(
8371        ended,
8372        Some(
8373            Drag::Scroll { moved: false, .. }
8374                | Drag::Pan { moved: false, .. }
8375                | Drag::Gesture { moved: false, .. }
8376                | Drag::Reorder { moved: false, .. }
8377                | Drag::Item { moved: false, .. }
8378                | Drag::Dismiss { moved: false, .. }
8379                | Drag::Sheet { moved: false, .. }
8380        )
8381    )
8382}
8383
8384/// A press lands at `at`: what is under it is pressed, and a surface that takes ink
8385/// splashes from where it landed. The box comes from the frame that was on screen when the
8386/// pointer came down, which is the one the user aimed at. Returns whether a splash started.
8387fn press_at<Msg: Clone>(runtime: &mut Runtime, ui: Option<&Ui<Msg>>, at: Point) -> bool {
8388    runtime.input.pressed = ui.and_then(|ui| ui.hit(at));
8389    let Some(pressed) = runtime.input.pressed else {
8390        return false;
8391    };
8392    let Some(rect) = ui.and_then(|ui| ui.ink_box(pressed)) else {
8393        return false;
8394    };
8395    runtime.ink_press(
8396        pressed,
8397        Point::new(at.x - rect.x, at.y - rect.y),
8398        Size::new(rect.width, rect.height),
8399    );
8400    true
8401}
8402
8403/// What becomes of the press once a pointer event has been routed, `could_tap` being
8404/// whether the gesture under way could still have ended as a tap **before** the event, and
8405/// `drag` the gesture after it. Returns whether anything changed, which wants a frame.
8406///
8407/// A press on something inside a scrollable is shared with the scroll until the pointer
8408/// crosses the slop. From then on the gesture has it — a scroll, a swipe, a sheet, a
8409/// carried row — and, as in the reference, where the recogniser that loses the arena is
8410/// told so, the press is **let go of there and then**: its ink is swept, its press fades.
8411/// And a pointer that has lifted holds nothing, whatever its release did.
8412///
8413/// Before this, a release that ended a gesture which had moved returned before the press
8414/// was cleared, so the press outlived the finger; the ink, which waits for as long as its
8415/// widget is pressed, was held for good — the grey left on a segment a phone had swiped
8416/// a strip of filters by (milestone 534).
8417///
8418/// A finger hovers only so that its press can show (milestone 505), so a finger whose press
8419/// was taken stops hovering too. A mouse is still over whatever it is over.
8420fn settle_press(
8421    input: &mut frus_widgets::InputState,
8422    kind: PointerKind,
8423    could_tap: bool,
8424    drag: Option<&Drag>,
8425    touch: bool,
8426) -> bool {
8427    let taken = match kind {
8428        PointerKind::Down => false,
8429        PointerKind::Move => could_tap && !gesture_was_a_tap(drag),
8430        PointerKind::Up | PointerKind::Cancel => true,
8431    };
8432    if !taken {
8433        return false;
8434    }
8435    let mut changed = input.pressed.take().is_some();
8436    if touch {
8437        changed |= input.hovered.take().is_some();
8438    }
8439    changed
8440}
8441
8442/// How fast an area scrolls under a carried item, in pixels per second **per pixel** the
8443/// item hangs over the edge: the further out it is, the faster the list comes to meet it.
8444/// The reference's number, and the reference's law.
8445const AUTOSCROLL_VELOCITY: f32 = 50.0;
8446
8447/// The overhang that speed is worked out from is capped here, so that carrying a row far
8448/// past the edge — or off the window entirely — settles at a fast but usable speed instead
8449/// of one nobody can aim with.
8450const AUTOSCROLL_MAX_OVERHANG: f32 = 20.0;
8451
8452/// Places the scroll requests it can against `regions`, and hands back the ones whose
8453/// region that registry does not name, along with whether anything moved.
8454///
8455/// A request names its region by **key**, the way a focus request does: the application
8456/// wrapped it in `keyed(k, …)` and the tree is what turns that back into an identity.
8457/// The framework's own identities are hashes of a position in a tree — an application
8458/// cannot know one, and should not have to.
8459///
8460/// Nothing is *stored* here. The request is spent the moment its region is found, and
8461/// what it leaves behind is an offset in the runtime, indistinguishable from an offset a
8462/// finger left there. That is what makes it survive a rebuild: there is nothing to
8463/// survive.
8464fn apply_scroll_requests<Msg>(
8465    runtime: &mut Runtime,
8466    tree: &dyn Widget<Msg>,
8467    requests: Vec<(u64, ScrollTo)>,
8468    regions: &[Scrollable],
8469) -> (Vec<(u64, ScrollTo)>, bool) {
8470    let mut unplaced = Vec::new();
8471    let mut moved = false;
8472    for (key, to) in requests {
8473        let area = find_by_key(tree, key).and_then(|id| regions.iter().find(|a| a.id == id));
8474        match area {
8475            Some(area) => moved |= runtime.scroll_to(area, to),
8476            None => unplaced.push((key, to)),
8477        }
8478    }
8479    (unplaced, moved)
8480}
8481
8482/// Places the sheet requests it can against `sheets`, and hands back the ones whose sheet
8483/// that registry does not name, along with whether anything moved — the terms of
8484/// `apply_scroll_requests`, above.
8485///
8486/// A request names the key the application wrapped the sheet in; the state belongs to the
8487/// sheet's panel under that key, which is what the tree is asked for.
8488fn apply_sheet_requests<Msg>(
8489    runtime: &mut Runtime,
8490    tree: &dyn Widget<Msg>,
8491    requests: Vec<(u64, SheetTo)>,
8492    sheets: &[frus_widgets::SheetArea],
8493) -> (Vec<(u64, SheetTo)>, bool) {
8494    let mut unplaced = Vec::new();
8495    let mut moved = false;
8496    for (key, to) in requests {
8497        let sheet = frus_widgets::find_sheet_by_key(tree, key)
8498            .and_then(|id| sheets.iter().find(|sheet| sheet.id == id));
8499        match sheet {
8500            Some(sheet) => moved |= runtime.sheet_to(sheet.id, &sheet.spec, &to),
8501            None => unplaced.push((key, to)),
8502        }
8503    }
8504    (unplaced, moved)
8505}
8506
8507/// Moves a carried item's press by `shift` — how far its content moved on screen — so that
8508/// the ghost, drawn at the content's box plus the finger's travel since the press, stays
8509/// under the finger. A lifted item's box, recorded at the press, goes with it.
8510///
8511/// Anything else being dragged is not carried content, and is left alone.
8512fn follow_content(drag: &mut Drag, shift: (f32, f32)) {
8513    match drag {
8514        Drag::Reorder { start, carried, .. } => {
8515            start.x += shift.0;
8516            start.y += shift.1;
8517            if let Some((_, rect)) = carried {
8518                *rect = rect.translate(shift.0, shift.1);
8519            }
8520        }
8521        Drag::Item { source, start, .. } => {
8522            start.x += shift.0;
8523            start.y += shift.1;
8524            source.rect = source.rect.translate(shift.0, shift.1);
8525        }
8526        _ => {}
8527    }
8528}
8529
8530/// One axis of the auto-scroll: how far the **content** has to move this frame so that an
8531/// item held past a viewport's edge brings the rest of the list into view.
8532///
8533/// Everything is in screen coordinates and along one axis: `item` and `view` are
8534/// `(start, end)` pairs, `room` is what is left to scroll that way (nought at the end of
8535/// the content). The answer is a movement of the content, which the area then turns into
8536/// an offset of its own — signs and reversed axes belong to `Scrollable::offset_delta`,
8537/// not here.
8538///
8539/// `None` means *not now*: the item is inside, or the edge it hangs over is the end of the
8540/// content, where there is nothing left to reveal and the list should stay still rather
8541/// than fight.
8542fn edge_autoscroll(item: (f32, f32), view: (f32, f32), room: (f32, f32), dt: f32) -> Option<f32> {
8543    let before = view.0 - item.0; // above the top edge
8544    let after = item.1 - view.1; // below the bottom edge
8545                                 // The leading edge is asked first, and both are never obeyed at once: an item taller
8546                                 // than the viewport hangs over both, and an item pulled two ways scrolls nowhere.
8547    let overhang = if before > 0.0 && room.0 > 0.0 {
8548        before.min(AUTOSCROLL_MAX_OVERHANG)
8549    } else if after > 0.0 && room.1 > 0.0 {
8550        -after.min(AUTOSCROLL_MAX_OVERHANG)
8551    } else {
8552        return None;
8553    };
8554    let travel = overhang * AUTOSCROLL_VELOCITY * dt;
8555    (travel != 0.0).then_some(travel)
8556}
8557
8558/// Moves `current` toward `target` by one **exponential** spring step, with time
8559/// constant `tau` in seconds, over an interval `dt`: frame-rate independent and free
8560/// of overshoot. Used to smooth the abscissa during a reorder.
8561fn spring_toward(current: f32, target: f32, dt: f32, tau: f32) -> f32 {
8562    let k = 1.0 - (-dt / tau.max(1e-4)).exp();
8563    current + (target - current) * k
8564}
8565
8566/// The focus history's maximum depth, for nested overlays' triggers.
8567const FOCUS_HISTORY_MAX: usize = 8;
8568
8569/// Resolves focus after a (re)build, from the focusables **present** this frame. When
8570/// `current` has **vanished**, an overlay having closed, focus returns to the most
8571/// recent present **trigger** in `history`, which is popped until one is found. The
8572/// transition is recorded: the old focus (`prev`), if still present and different from
8573/// the new one, is pushed as a candidate trigger, bounded by [`FOCUS_HISTORY_MAX`].
8574/// Returns the resolved focus. A **pure** function apart from the mutated `history` and
8575/// `prev` — testable without a window.
8576fn resolve_focus(
8577    current: Option<WidgetId>,
8578    present: &std::collections::HashSet<WidgetId>,
8579    history: &mut Vec<WidgetId>,
8580    prev: &mut Option<WidgetId>,
8581) -> Option<WidgetId> {
8582    let mut cur = current;
8583    if let Some(c) = cur {
8584        if !present.contains(&c) {
8585            cur = None;
8586            while let Some(cand) = history.pop() {
8587                if present.contains(&cand) {
8588                    cur = Some(cand);
8589                    break;
8590                }
8591            }
8592        }
8593    }
8594    if *prev != cur {
8595        if let Some(old) = *prev {
8596            if present.contains(&old) && Some(old) != cur {
8597                history.push(old);
8598                if history.len() > FOCUS_HISTORY_MAX {
8599                    history.remove(0);
8600                }
8601            }
8602        }
8603        *prev = cur;
8604    }
8605    cur
8606}
8607
8608/// Paints the dragged header's **lifted card**, the ghost, into `scene` — unclipped —
8609/// at `card`: a drop shadow, a **faithful face** from the header's already-translated
8610/// primitives, or a solid one as a fallback when `ghost` is empty, then a `primary`
8611/// border. A pure function, testable without a GPU. The neighbours' sliding happens
8612/// upstream, in `reflow_reorder_columns`.
8613fn draw_ghost_card(scene: &mut Scene, theme: &Theme, card: Rect, ghost: &[Primitive]) {
8614    use drag_preview::{BORDER_ALPHA, BORDER_WIDTH, SHADOW_ALPHA, SHADOW_BLUR, SHADOW_OFFSET_Y};
8615    scene.set_clip(Rect::UNBOUNDED);
8616    // The shadow colour comes from **the theme** and can be overridden — the same role
8617    // as `Button`'s shadow; only the geometry, offset, blur and opacity, is a local
8618    // constant.
8619    let shadow = theme.scheme.shadow.with_alpha(SHADOW_ALPHA);
8620    scene.shadow(
8621        card.translate(0.0, SHADOW_OFFSET_Y),
8622        shadow,
8623        theme.radius,
8624        SHADOW_BLUR,
8625    );
8626    let border = theme.primary.fade(BORDER_ALPHA);
8627    if ghost.is_empty() {
8628        scene.draw_rect(card, theme.surface, theme.radius, BORDER_WIDTH, border);
8629    } else {
8630        scene.draw_rect(card, theme.surface, theme.radius, 0.0, Color::TRANSPARENT);
8631        for primitive in ghost {
8632            scene.push_primitive(primitive.clone());
8633        }
8634        scene.draw_rect(card, Color::TRANSPARENT, theme.radius, BORDER_WIDTH, border);
8635    }
8636}
8637
8638/// How a reorderable moves while it is carried: the two gestures the shell knows.
8639#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8640enum ReorderMotion {
8641    /// A table's column: a continuous slide along x following the pointer, dropped into
8642    /// the place of the column under it.
8643    Columns,
8644    /// A list's row or a board's card: inserted before or after a slot along the axis,
8645    /// with an insertion line, a gap opening there, and the nearest slot past either end.
8646    Slots(ReorderAxis),
8647}
8648
8649impl ReorderMotion {
8650    /// Which of the two a reorderable is, from its two answers: every vertical one inserts,
8651    /// and a horizontal one only when it says so — a list's row does, a table's column does
8652    /// not.
8653    fn of(axis: ReorderAxis, inserts: bool) -> Self {
8654        match axis {
8655            ReorderAxis::Vertical => Self::Slots(ReorderAxis::Vertical),
8656            ReorderAxis::Horizontal if inserts => Self::Slots(ReorderAxis::Horizontal),
8657            ReorderAxis::Horizontal => Self::Columns,
8658        }
8659    }
8660}
8661
8662/// How `widget` moves while it is carried — [`ReorderMotion::of`] its two answers.
8663fn reorder_motion<Msg>(widget: &dyn Widget<Msg>) -> ReorderMotion {
8664    ReorderMotion::of(widget.reorder_axis(), widget.reorder_inserts())
8665}
8666
8667/// Where the ghost is drawn relative to the box it was lifted from, given the finger's
8668/// `travel` since the press.
8669///
8670/// - A **column** follows the pointer along x, with a slight lift.
8671/// - A **card** follows it in both directions: a board carries it across columns.
8672/// - A **horizontal list's row** follows it along x only. The reference keeps a list's
8673///   proxy in its lane on either axis; a vertical list here shares the board's freedom
8674///   because the two are one path, and a horizontal list has no board to share it with.
8675fn ghost_offset(motion: ReorderMotion, travel: (f32, f32)) -> (f32, f32) {
8676    match motion {
8677        ReorderMotion::Columns => (travel.0, drag_preview::LIFT_Y),
8678        ReorderMotion::Slots(ReorderAxis::Vertical) => travel,
8679        ReorderMotion::Slots(ReorderAxis::Horizontal) => (travel.0, 0.0),
8680    }
8681}
8682
8683/// The geometry of a reorder preview's **insertion line**: a thin band of thickness
8684/// `thickness`, centred on the target edge **where the insertion will happen**.
8685///
8686/// Along a **vertical** list: the **top** edge (inserting before, the upper half hovered)
8687/// or the **bottom** one (inserting after, `after = true`, the lower half hovered),
8688/// spanning the full width. Along a **horizontal** list the band stands on its end,
8689/// spanning the full height at the left or right edge — and *after* is the right edge
8690/// only when the list reads left to right: under `rtl` what follows a row is on its left.
8691/// A pure function, testable without a GPU.
8692fn drop_insertion_line(
8693    target: Rect,
8694    thickness: f32,
8695    after: bool,
8696    axis: ReorderAxis,
8697    rtl: bool,
8698) -> Rect {
8699    match axis {
8700        ReorderAxis::Vertical => {
8701            let edge = if after {
8702                target.y + target.height
8703            } else {
8704                target.y
8705            };
8706            Rect::new(target.x, edge - thickness * 0.5, target.width, thickness)
8707        }
8708        ReorderAxis::Horizontal => {
8709            let edge = if after != rtl {
8710                target.x + target.width
8711            } else {
8712                target.x
8713            };
8714            Rect::new(edge - thickness * 0.5, target.y, thickness, target.height)
8715        }
8716    }
8717}
8718
8719/// Which axis a scroll gesture is **claimed by**, decided once when the finger passes the
8720/// threshold: `true` for vertical.
8721///
8722/// An area that can only go one way claims that one, whatever the finger did — a diagonal
8723/// flick down a list is a scroll down, not a refusal. An area that can go both ways takes
8724/// the direction the finger actually went in, ties going to vertical, which is the way a
8725/// page reads. The loser gets nothing for the rest of the gesture: this is the reference's
8726/// arena, where a vertical and a horizontal recogniser compete and only one wins.
8727fn claim_axis(dx: f32, dy: f32, can_x: bool, can_y: bool) -> bool {
8728    match (can_x, can_y) {
8729        (true, false) => false,
8730        (false, true) => true,
8731        _ => dy.abs() >= dx.abs(),
8732    }
8733}
8734
8735/// Which of the scrollables under the finger a gesture is **for**: the innermost one that
8736/// can go the way the finger went (`down` = along the vertical), given `chain` innermost
8737/// first and `under` the area the press landed on.
8738///
8739/// A strip that only slides across, sitting in a page that only scrolls down, is not what a
8740/// drag downwards is about — the page behind it is, and the reference's arena gives it to
8741/// the page for exactly that reason. Where nothing in the stack can go that way, the area
8742/// under the finger keeps the gesture and goes its own way: a lone recogniser in an arena
8743/// still wins.
8744fn claim_area(
8745    chain: &[frus_widgets::Scrollable],
8746    under: WidgetId,
8747    down: bool,
8748) -> Option<frus_widgets::Scrollable> {
8749    chain
8750        .iter()
8751        // A refresh area listening above takes a pull downwards even with nothing to
8752        // scroll, which is the reference's one exception to the same rule; the physics
8753        // may say never, or always (milestone 649).
8754        .find(|a| a.takes_user_scroll(down))
8755        .or_else(|| {
8756            chain
8757                .iter()
8758                .find(|a| a.id == under && a.physics.user() != frus_widgets::UserScroll::Never)
8759        })
8760        .copied()
8761}
8762
8763/// Gets the bytes at `url` and hands them to `deliver`, once.
8764///
8765/// Registered as the process's [`frus_widgets::ImageFetcher`] when the shell is built with
8766/// `net`. It spawns and forgets: nothing here waits for the answer, and the store the
8767/// callback writes into is what a later frame reads.
8768///
8769/// The two spawns are the two the rest of this file uses for every other effect — the
8770/// shared executor natively, the browser on the Web.
8771///
8772/// It carries a **deadline**, and that is not politeness. An image still in flight keeps
8773/// the interface redrawing — that is how the frame showing it ever happens — so a
8774/// request that never answers would leave the application repainting at full rate for
8775/// ever, on somebody's battery. A request without an answer has to become one with a
8776/// failure.
8777#[cfg(feature = "net")]
8778fn fetch_image_bytes(
8779    url: &str,
8780    deliver: Box<dyn FnOnce(Result<Vec<u8>, String>) + Send + 'static>,
8781) {
8782    /// Long enough for a photograph on a slow connection, short enough that a dead
8783    /// server does not pin a screen redrawing until the user closes the application.
8784    const DEADLINE: std::time::Duration = std::time::Duration::from_secs(30);
8785
8786    let url = url.to_string();
8787    let run = async move {
8788        let result = crate::net::Request::get(url)
8789            .timeout(DEADLINE)
8790            .send_bytes()
8791            .await;
8792        deliver(result.map_err(|e| e.to_string()));
8793    };
8794    #[cfg(not(web))]
8795    crate::runtime::spawn(run).detach();
8796    #[cfg(web)]
8797    wasm_bindgen_futures::spawn_local(run);
8798}
8799
8800#[cfg(test)]
8801mod debug_keys_tests {
8802    use super::*;
8803
8804    #[test]
8805    fn the_graphs_keep_the_latest_frames_and_mark_the_late_ones() {
8806        let mut history = FrameHistory::default();
8807        for i in 0..100 {
8808            history.push(&crate::frame_stats::FrameCosts {
8809                build: Some(1.0),
8810                layout_paint: i as f32,
8811                render: 5.0,
8812                ..Default::default()
8813            });
8814        }
8815        assert_eq!(history.ui.len(), HISTORY, "the oldest frames go");
8816        assert_eq!(
8817            history.ui.back().copied(),
8818            Some(100.0),
8819            "the newest is last"
8820        );
8821        let bars = FrameHistory::bars(&history.render, 36.0);
8822        assert!(bars.iter().all(|(_, over)| !over), "5 ms is within budget");
8823        let ui = FrameHistory::bars(&history.ui, 36.0);
8824        assert_eq!(
8825            ui.last().copied(),
8826            Some((36.0, true)),
8827            "over the top fills the graph"
8828        );
8829        let half = FrameHistory::bars(&[BUDGET_MS].into_iter().collect(), 36.0);
8830        assert!((half[0].0 - 18.0).abs() < 0.01 && !half[0].1, "{half:?}");
8831    }
8832
8833    #[test]
8834    fn the_platform_goes_round_as_the_reference_does() {
8835        use frus_widgets::TargetPlatform as P;
8836        let mut seen = vec![P::Android];
8837        for _ in 0..6 {
8838            seen.push(next_platform(*seen.last().unwrap()));
8839        }
8840        assert_eq!(
8841            seen,
8842            [
8843                P::Android,
8844                P::Ios,
8845                P::Windows,
8846                P::MacOs,
8847                P::Linux,
8848                P::Fuchsia,
8849                P::Android
8850            ]
8851        );
8852    }
8853
8854    #[test]
8855    fn the_dumps_say_what_there_is() {
8856        let id = WidgetId::from_u64(0x1234_5678);
8857        let other = WidgetId::from_u64(0x9);
8858        let nodes = [frus_widgets::InspectorNode {
8859            id,
8860            rect: Rect::new(10.0, 20.0, 100.0, 40.0),
8861            name: "Button",
8862            depth: 3,
8863        }];
8864        let focus = dump_focus(&nodes, &[id, other], Some(id));
8865        assert_eq!(focus, "▶ Button  10,20  100×40  #12345678\n  #00000009\n");
8866        assert_eq!(dump_focus(&nodes, &[], None), "  (nothing focusable)\n");
8867
8868        let semantics = [(
8869            id,
8870            Rect::new(0.0, 0.0, 50.0, 20.0),
8871            frus_widgets::SemanticsProperties::new(frus_widgets::Role::Button).label("Save"),
8872        )];
8873        let dump = dump_semantics(&semantics);
8874        assert!(dump.starts_with("  Button \"Save\""), "{dump}");
8875        assert!(dump.ends_with("0,0  50×20\n"), "{dump}");
8876    }
8877}
8878
8879#[cfg(test)]
8880mod tests {
8881    use super::{
8882        build_view, claim_area, claim_axis, draw_ghost_card, drop_insertion_line, fling_velocity,
8883        gesture_was_a_tap, ghost_offset, install_ambient, reorder_motion, resolve_focus,
8884        spring_toward, Drag, Point, Rect, ReorderAxis, ReorderMotion, Scene, Theme,
8885        VelocityEstimate, PRECISE_SLOP, TOUCH_SLOP,
8886    };
8887    use super::{clipboard_command, ClipCommand, KeyCode, PhysicalKey, WinitKey};
8888    use super::{collect_ids, find_widget, MediaQuery};
8889    use frus_widgets::Locale;
8890
8891    /// **Which systems put the bar away after its Select all** (milestone 625): the three
8892    /// desktops, as the reference's field does; on a phone the bar stays.
8893    #[test]
8894    fn select_all_hides_the_bar_on_the_desktops() {
8895        use frus_widgets::TargetPlatform as P;
8896        for p in [P::MacOs, P::Linux, P::Windows] {
8897            assert!(super::select_all_hides_bar(p), "{p:?}");
8898        }
8899        for p in [P::Android, P::Ios, P::Fuchsia] {
8900            assert!(!super::select_all_hides_bar(p), "{p:?}");
8901        }
8902    }
8903
8904    /// **The keys that open the selection bar** (milestone 568): the context-menu key, and
8905    /// F10 with Shift — F10 alone is the menu bar's key in other toolkits and asks for nothing
8906    /// here, and no ordinary key does.
8907    #[test]
8908    fn the_context_menu_key_and_shift_f10_open_the_bar() {
8909        use super::opens_selection_bar;
8910        assert!(opens_selection_bar(
8911            &WinitKey::Named(winit::keyboard::NamedKey::ContextMenu),
8912            false
8913        ));
8914        assert!(opens_selection_bar(
8915            &WinitKey::Named(winit::keyboard::NamedKey::F10),
8916            true
8917        ));
8918        assert!(!opens_selection_bar(
8919            &WinitKey::Named(winit::keyboard::NamedKey::F10),
8920            false
8921        ));
8922        assert!(!opens_selection_bar(
8923            &WinitKey::Named(winit::keyboard::NamedKey::Enter),
8924            true
8925        ));
8926        assert!(!opens_selection_bar(&character("m"), true));
8927    }
8928
8929    /// **What a Copy from the selection bar leaves behind** (milestone 568): the selection
8930    /// collapsed to its end, whichever way it was made — dragged backwards puts the caret at
8931    /// the far end, and Copy puts it at the *end* of the text, not where the caret was.
8932    #[test]
8933    fn a_copy_collapses_the_selection_to_its_end() {
8934        use frus_widgets::Edit;
8935        let forwards = Edit {
8936            cursor: 11,
8937            anchor: Some(6),
8938            composing: None,
8939        };
8940        let backwards = Edit {
8941            cursor: 6,
8942            anchor: Some(11),
8943            composing: None,
8944        };
8945        let expect = Edit {
8946            cursor: 11,
8947            anchor: None,
8948            composing: None,
8949        };
8950        assert_eq!(super::collapsed_to_end(forwards), expect);
8951        assert_eq!(super::collapsed_to_end(backwards), expect);
8952        // Nothing selected: the caret stays where it is.
8953        let caret = Edit {
8954            cursor: 3,
8955            anchor: None,
8956            composing: None,
8957        };
8958        assert_eq!(super::collapsed_to_end(caret), caret);
8959        // An anchor on the caret is no selection either.
8960        let empty = Edit {
8961            cursor: 3,
8962            anchor: Some(3),
8963            composing: Some((1, 2)),
8964        };
8965        assert_eq!(super::collapsed_to_end(empty).anchor, None);
8966        assert_eq!(super::collapsed_to_end(empty).cursor, 3);
8967    }
8968
8969    /// **A field with a clear button kept the keyboard only until the first letter**
8970    /// (milestone 510). Whether the focused widget takes typing was asked of a caret
8971    /// hit test at the corner of a field one pixel wide; a field whose clickable suffix
8972    /// covers that pixel — the × it shows once it holds text — answered no, and the
8973    /// keyboard was closed mid-word. Asked of the widget, the answer is the field's.
8974    #[test]
8975    fn a_field_with_a_clear_button_still_wants_the_keyboard() {
8976        use frus_widgets::{Container, Icons, TextField};
8977        let clearable = TextField::<()>::new("F")
8978            .suffix_icon(Icons::CLOSE)
8979            .on_suffix(());
8980        assert!(
8981            super::wants_keyboard(&clearable),
8982            "the field with its clear button"
8983        );
8984        assert!(
8985            !super::wants_keyboard(&TextField::<()>::new("F").enabled(false)),
8986            "a disabled field takes no typing"
8987        );
8988        assert!(
8989            !super::wants_keyboard(&Container::<()>::new()),
8990            "and what is not a field wants no keyboard"
8991        );
8992    }
8993
8994    /// **Left and right moved the focus, not the caret, in a field with text in it**
8995    /// (milestone 570). Whether the focused widget is a text field was asked of a caret probe
8996    /// at a corner of the field one pixel wide, and a field that holds text shows a clear
8997    /// button over that corner: the probe said no caret goes there, so the arrows navigated to
8998    /// the next control. In an empty field, which has no button, they worked — which is how it
8999    /// went unseen. The probe still answers no for such a field; the question is the widget's.
9000    #[test]
9001    fn a_field_with_a_clear_button_is_a_text_field_and_keeps_its_arrows() {
9002        use frus_widgets::{Container, Icons, TextField};
9003        let clearable = TextField::<()>::new("hello")
9004            .suffix_icon(Icons::CLOSE)
9005            .on_suffix(());
9006        assert!(
9007            frus_widgets::Widget::<()>::cursor_at(&clearable, 0.0, 0.0, 1.0, 0).is_none(),
9008            "the trap: the probe finds no caret at the corner a clear button covers"
9009        );
9010        assert!(
9011            super::is_text_field(&clearable),
9012            "and it is a text field all the same"
9013        );
9014        assert!(
9015            super::is_text_field(&TextField::<()>::new("")),
9016            "empty, too"
9017        );
9018        assert!(
9019            !super::is_text_field(&Container::<()>::new()),
9020            "what is not a field leaves its arrows to focus navigation"
9021        );
9022    }
9023
9024    /// **A text that can only be read does not raise the keyboard.** A read-only field and a
9025    /// selectable paragraph are both text fields — they have a caret, and the arrows are
9026    /// theirs — but the software keyboard is for typing, and sliding it up over the words a
9027    /// reader has come to select hides the words. Before milestone 573 a read-only field
9028    /// raised it all the same.
9029    #[test]
9030    fn a_text_that_can_only_be_read_wants_no_keyboard() {
9031        use frus_widgets::{Text, TextField};
9032        let read_only = TextField::<()>::new("REF-4417").read_only();
9033        assert!(super::is_text_field(&read_only), "it has a caret");
9034        assert!(
9035            !super::wants_keyboard(&read_only),
9036            "and no use for a keyboard"
9037        );
9038
9039        let selectable = Text::new("ORDER-4417").selectable();
9040        assert!(
9041            super::is_text_field::<()>(&selectable),
9042            "the arrows move its caret"
9043        );
9044        assert!(!super::wants_keyboard::<()>(&selectable));
9045
9046        assert!(
9047            super::wants_keyboard(&TextField::<()>::new("typed")),
9048            "and a field that can be typed into still does"
9049        );
9050        assert!(
9051            !super::is_text_field::<()>(&Text::new("ORDER-4417")),
9052            "an ordinary text leaves its arrows to focus navigation"
9053        );
9054    }
9055
9056    fn character(c: &str) -> WinitKey {
9057        WinitKey::Character(c.into())
9058    }
9059
9060    /// Ctrl+C/X/V are the clipboard, in either case — Caps Lock or Shift is still a
9061    /// copy — and the same letters without Ctrl are only letters.
9062    #[test]
9063    fn ctrl_c_x_v_are_the_clipboard_and_the_letters_alone_are_not() {
9064        let any = PhysicalKey::Code(KeyCode::KeyQ);
9065        for (letter, command) in [
9066            ("c", ClipCommand::Copy),
9067            ("X", ClipCommand::Cut),
9068            ("v", ClipCommand::Paste),
9069        ] {
9070            assert_eq!(
9071                clipboard_command(&character(letter), any, true),
9072                Some(command)
9073            );
9074            assert_eq!(
9075                clipboard_command(&character(letter), any, false),
9076                None,
9077                "{letter}"
9078            );
9079        }
9080        assert_eq!(clipboard_command(&character("a"), any, true), None);
9081    }
9082
9083    /// **Android's Copy, Cut and Paste keys arrive by physical code alone** — winit gives
9084    /// them no logical key — and they are the clipboard with no modifier held. A check
9085    /// of the logical key alone ignored them (milestone 509).
9086    #[test]
9087    fn a_keyboards_own_clipboard_keys_work_by_their_physical_code() {
9088        let unidentified = WinitKey::Unidentified(winit::keyboard::NativeKey::Unidentified);
9089        for (code, command) in [
9090            (KeyCode::Copy, ClipCommand::Copy),
9091            (KeyCode::Cut, ClipCommand::Cut),
9092            (KeyCode::Paste, ClipCommand::Paste),
9093        ] {
9094            assert_eq!(
9095                clipboard_command(&unidentified, PhysicalKey::Code(code), false),
9096                Some(command),
9097                "{code:?}"
9098            );
9099        }
9100        // And the named keys a desktop keyboard reports for the same thing.
9101        let named = WinitKey::Named(super::NamedKey::Paste);
9102        let any = PhysicalKey::Code(KeyCode::KeyQ);
9103        assert_eq!(
9104            clipboard_command(&named, any, false),
9105            Some(ClipCommand::Paste)
9106        );
9107    }
9108
9109    /// **A paste lands only in the field that asked, while that field still has the
9110    /// focus** — on the Web the clipboard answers after the key press, and by then the
9111    /// reader may have moved to another field or to none. And a clipboard holding no
9112    /// text pastes nothing, rather than typing an empty string over the selection
9113    /// (milestone 526).
9114    #[test]
9115    fn a_paste_lands_in_the_field_that_asked_only_while_it_has_the_focus() {
9116        use super::clip::Pasted;
9117        let field = WidgetId::from_u64(1);
9118        let other = WidgetId::from_u64(2);
9119        let pasted = Pasted {
9120            into: field,
9121            text: "copied".into(),
9122        };
9123        assert!(pasted.lands(Some(field)));
9124        assert!(
9125            !pasted.lands(Some(other)),
9126            "the focus moved to another field"
9127        );
9128        assert!(!pasted.lands(None), "the focus left every field");
9129        let nothing = Pasted {
9130            into: field,
9131            text: String::new(),
9132        };
9133        assert!(!nothing.lands(Some(field)), "an empty clipboard");
9134    }
9135
9136    /// **A paste answered later is queued for its field, and a frame is asked for.** Two
9137    /// pastes asked in two fields and answered in the other order come out in the order
9138    /// the answers came, each still naming the field that asked; each answer wakes the
9139    /// window once; and nothing is taken twice (milestone 526).
9140    #[test]
9141    fn a_paste_answered_later_waits_for_the_frame_that_takes_it() {
9142        use super::clip::{Answers, Pasted};
9143        use std::cell::Cell;
9144        use std::rc::Rc;
9145        let first = WidgetId::from_u64(1);
9146        let second = WidgetId::from_u64(2);
9147        let answers = Answers::default();
9148        let wakes = Rc::new(Cell::new(0));
9149        let wake = || {
9150            let wakes = Rc::clone(&wakes);
9151            move || wakes.set(wakes.get() + 1)
9152        };
9153        let to_first = answers.answer_for(first, wake());
9154        let to_second = answers.answer_for(second, wake());
9155        assert_eq!(answers.take(), None, "nothing is answered yet");
9156        assert_eq!(wakes.get(), 0, "asking is not answering");
9157
9158        to_second("later".into());
9159        assert_eq!(wakes.get(), 1);
9160        to_first("sooner".into());
9161        assert_eq!(wakes.get(), 2);
9162
9163        assert_eq!(
9164            answers.take(),
9165            Some(Pasted {
9166                into: second,
9167                text: "later".into()
9168            })
9169        );
9170        assert_eq!(
9171            answers.take(),
9172            Some(Pasted {
9173                into: first,
9174                text: "sooner".into()
9175            })
9176        );
9177        assert_eq!(answers.take(), None, "each answer is taken once");
9178    }
9179
9180    /// An application whose whole interface lives **inside a deferred subtree** — which is
9181    /// what any application with an `AppBar` is, since a bar defers its own composition
9182    /// until the theme is known.
9183    ///
9184    /// It exists because nothing else in this repo can stand in for one: the shell's own
9185    /// types need a window and an event-loop proxy, so the only piece of the frame a test
9186    /// can hold is the part that turns a `view` into a tree. That part is where two
9187    /// milestones' worth of bugs were.
9188    struct Deferred;
9189
9190    impl crate::Application for Deferred {
9191        type Message = ();
9192
9193        fn update(&mut self, _message: ()) -> crate::Command<()> {
9194            crate::Command::none()
9195        }
9196
9197        fn view(&self, _theme: &Theme) -> Box<dyn frus_widgets::Widget<()>> {
9198            Box::new(frus_widgets::ThemeBuilder::new(|_: &Theme| {
9199                frus_widgets::Flex::column()
9200                    .width(300.0)
9201                    .height(80.0)
9202                    .child(
9203                        frus_widgets::TextField::new("hi")
9204                            .width(200.0)
9205                            .on_input(|_| ()),
9206                    )
9207            }))
9208        }
9209    }
9210
9211    /// The surface every frame installs, so a build outside one measures the way a build
9212    /// inside one does.
9213    fn surfaced<R>(f: impl FnOnce() -> R) -> R {
9214        MediaQuery::new(frus_widgets::Size::new(300.0, 80.0)).scope(f)
9215    }
9216
9217    /// **A shell installs the reader's language** — the assertion that keeps milestone 454
9218    /// from being a resolution nobody ever runs.
9219    ///
9220    /// `locale::of()` answers `en` with nothing installed, exactly as
9221    /// `localizations::of()` answers English, and for the same reason: an application that
9222    /// says nothing must keep working. That default is also what would hide the wiring
9223    /// being missing, so this drives the shell's own reading of the trait rather than
9224    /// calling `resolve` directly — milestone 408's bug, and milestone 449's guard against
9225    /// it.
9226    #[test]
9227    fn a_shell_installs_the_reader_s_language() {
9228        /// An application with three languages, and a switch of its own it may or may not
9229        /// have been told to use.
9230        struct Speaks(Option<Locale>);
9231
9232        impl crate::Application for Speaks {
9233            type Message = ();
9234
9235            fn update(&mut self, _message: ()) -> crate::Command<()> {
9236                crate::Command::none()
9237            }
9238
9239            fn view(&self, _theme: &Theme) -> Box<dyn frus_widgets::Widget<()>> {
9240                Box::new(frus_widgets::Flex::<()>::column())
9241            }
9242
9243            fn supported_locales(&self) -> Vec<Locale> {
9244                vec![
9245                    Locale::new("en"),
9246                    Locale::new("fr"),
9247                    Locale::with_country("fr", "CA"),
9248                ]
9249            }
9250
9251            fn locale(&self) -> Option<Locale> {
9252                self.0.clone()
9253            }
9254        }
9255
9256        // The device speaks Belgian French; the application has French, so it wins.
9257        install_ambient(&Speaks(None), &[Locale::with_country("fr", "BE")]);
9258        assert_eq!(frus_widgets::locale::of(), Locale::new("fr"));
9259
9260        // A platform that reported nothing is not a failure: the application's own first
9261        // choice stands.
9262        install_ambient(&Speaks(None), &[]);
9263        assert_eq!(frus_widgets::locale::of(), Locale::new("en"));
9264
9265        // And an application with a language menu of its own outranks the device — still
9266        // resolved, so asking for one it does not have gives the nearest thing it does.
9267        install_ambient(
9268            &Speaks(Some(Locale::with_country("fr", "FR"))),
9269            &[Locale::new("en")],
9270        );
9271        assert_eq!(frus_widgets::locale::of(), Locale::new("fr"));
9272
9273        // An application that says nothing at all is where it was: English.
9274        install_ambient(&Deferred, &[Locale::new("fr")]);
9275        assert_eq!(
9276            frus_widgets::locale::of(),
9277            Locale::new("en"),
9278            "it supports only English, so French resolves to it"
9279        );
9280    }
9281
9282    /// **A shell installs the application's words** — which is the assertion that keeps
9283    /// milestone 449 from being a table nobody ever reads.
9284    ///
9285    /// `localizations::of` answers English with nothing installed, so every widget test
9286    /// and every golden would pass whether or not the shell ever called `install`. The
9287    /// default is what makes the feature safe to add; it is also what would hide the
9288    /// wiring being missing. So this drives the shell's own reading of the trait.
9289    #[test]
9290    fn a_shell_installs_the_application_s_words() {
9291        struct Fr;
9292
9293        impl frus_widgets::localizations::Localizations for Fr {
9294            fn back_button_label(&self) -> &str {
9295                "Retour"
9296            }
9297
9298            fn first_day_of_week_index(&self) -> usize {
9299                1
9300            }
9301        }
9302
9303        struct Speaks;
9304
9305        impl crate::Application for Speaks {
9306            type Message = ();
9307
9308            fn update(&mut self, _message: ()) -> crate::Command<()> {
9309                crate::Command::none()
9310            }
9311
9312            fn view(&self, _theme: &Theme) -> Box<dyn frus_widgets::Widget<()>> {
9313                Box::new(frus_widgets::Flex::<()>::column())
9314            }
9315
9316            fn localizations(&self) -> Option<std::rc::Rc<dyn frus_widgets::Localizations>> {
9317                Some(std::rc::Rc::new(Fr))
9318            }
9319        }
9320
9321        install_ambient(&Speaks, &[]);
9322        assert_eq!(
9323            frus_widgets::localizations::of().back_button_label(),
9324            "Retour",
9325            "the shell read the application's table and installed it"
9326        );
9327        assert_eq!(
9328            frus_widgets::localizations::of().first_day_of_week_index(),
9329            1
9330        );
9331
9332        // And an application that says nothing leaves whatever is in force alone rather
9333        // than forcing English back on top of a table installed elsewhere.
9334        install_ambient(&Deferred, &[]);
9335        assert_eq!(
9336            frus_widgets::localizations::of().back_button_label(),
9337            "Retour",
9338            "saying nothing is not the same as saying English"
9339        );
9340    }
9341
9342    /// **The application's scroll behaviour is the one at the root** (milestone 616), and
9343    /// one that says nothing gets the behaviour that follows the theme.
9344    #[test]
9345    fn the_scroll_behaviour_at_the_root_is_the_applications() {
9346        struct Quiet;
9347
9348        impl crate::Application for Quiet {
9349            type Message = ();
9350
9351            fn update(&mut self, _message: ()) -> crate::Command<()> {
9352                crate::Command::none()
9353            }
9354
9355            fn view(&self, _theme: &Theme) -> Box<dyn frus_widgets::Widget<()>> {
9356                Box::new(frus_widgets::Flex::<()>::column())
9357            }
9358
9359            fn scroll_behavior(&self) -> frus_widgets::ScrollBehavior {
9360                frus_widgets::ScrollBehavior::material().with_overscroll(false)
9361            }
9362        }
9363
9364        struct Silent;
9365
9366        impl crate::Application for Silent {
9367            type Message = ();
9368
9369            fn update(&mut self, _message: ()) -> crate::Command<()> {
9370                crate::Command::none()
9371            }
9372
9373            fn view(&self, _theme: &Theme) -> Box<dyn frus_widgets::Widget<()>> {
9374                Box::new(frus_widgets::Flex::<()>::column())
9375            }
9376        }
9377
9378        install_ambient(&Quiet, &[]);
9379        assert_eq!(
9380            frus_widgets::ScrollConfiguration::of(),
9381            frus_widgets::ScrollBehavior::material().with_overscroll(false)
9382        );
9383        install_ambient(&Silent, &[]);
9384        assert_eq!(
9385            frus_widgets::ScrollConfiguration::of(),
9386            frus_widgets::ScrollBehavior::material()
9387        );
9388    }
9389
9390    /// A tree the shell hands to a traversal is **ready to be traversed**.
9391    ///
9392    /// Milestones 415 and 416 were the same bug found twice, in the two places this shell
9393    /// turns a `view` into a tree. A deferred subtree has no children until something has
9394    /// built it, so an unprepared tree reports **one** identity — its root — and every
9395    /// widget in the application is invisible to `collect_ids`, to `find_widget`, and to
9396    /// everything downstream of them: the mount and leave fades, the focus, the caret.
9397    ///
9398    /// The failure was silence in both places, which is why it went unnoticed twice. This
9399    /// is the assertion that would have caught it, and it needs no window.
9400    #[test]
9401    fn a_view_is_built_ready_to_be_read() {
9402        let theme = Theme::default();
9403        let prepared =
9404            surfaced(|| build_view(&Deferred, &theme, &frus_widgets::Runtime::default()));
9405        let ids = collect_ids(prepared.as_ref());
9406        assert!(
9407            ids.len() > 1,
9408            "an application inside a deferred subtree has identities to mount: {ids:?}"
9409        );
9410        // And every one of them is reachable, which is what the burst path needs.
9411        for id in &ids {
9412            assert!(
9413                find_widget(prepared.as_ref(), *id).is_some(),
9414                "{id:?} was counted but cannot be found"
9415            );
9416        }
9417    }
9418
9419    /// The same view, unprepared, is the state both call sites used to read.
9420    #[test]
9421    #[should_panic(expected = "before it was built")]
9422    fn an_unprepared_view_is_the_state_that_broke() {
9423        let theme = Theme::default();
9424        let raw = surfaced(|| crate::Application::view(&Deferred, &theme));
9425        let _ = collect_ids(raw.as_ref());
9426    }
9427
9428    /// A press on a **dismissible** row that never moved is a tap, and the widget under
9429    /// the finger owes it a click (reported on a device: a task row's avatar opened
9430    /// nothing, while a control a few hundred pixels away in the same card worked).
9431    ///
9432    /// The row registers a swipe on the press, in case the finger is about to slide
9433    /// sideways. When it does not, the swipe has to stand down. Every other gesture
9434    /// here already did; this one was left out of the list.
9435    #[test]
9436    fn a_swipe_that_never_started_is_still_a_tap() {
9437        let item = frus_widgets::Dismissable {
9438            id: WidgetId::from_u64(1),
9439            rect: Rect::new(0.0, 0.0, 300.0, 62.0),
9440            spec: frus_widgets::DismissSpec::default(),
9441        };
9442        let dismiss = |moved| Drag::Dismiss {
9443            item,
9444            last: Point::new(0.0, 0.0),
9445            moved,
9446        };
9447        assert!(
9448            gesture_was_a_tap(Some(&dismiss(false))),
9449            "a swipe under the threshold leaves the click alone"
9450        );
9451        assert!(
9452            !gesture_was_a_tap(Some(&dismiss(true))),
9453            "a swipe that ran has already been answered and must not also click"
9454        );
9455        // A scroll, the variant that was already right — and the one that masked this
9456        // bug, since a list long enough to scroll claims the press before the swipe can.
9457        let scroll = Drag::Scroll {
9458            id: WidgetId::from_u64(2),
9459            last: Point::new(0.0, 0.0),
9460            moved: false,
9461            carried: (0.0, 0.0),
9462            dismiss: None,
9463            axis: None,
9464            pan: None,
9465        };
9466        assert!(gesture_was_a_tap(Some(&scroll)));
9467        // A press on a sheet's panel likewise: a button in the sheet still clicks, and a
9468        // sheet that was dragged does not also click what it was dragged by (515).
9469        let sheet = |moved| Drag::Sheet {
9470            id: WidgetId::from_u64(4),
9471            last: Point::new(0.0, 0.0),
9472            moved,
9473            available: 800.0,
9474        };
9475        assert!(gesture_was_a_tap(Some(&sheet(false))));
9476        assert!(!gesture_was_a_tap(Some(&sheet(true))));
9477        // And a gesture that captures on the press, rather than at a threshold, never
9478        // becomes a tap however still the finger was.
9479        let select = Drag::TextSelect {
9480            id: WidgetId::from_u64(3),
9481            rect: Rect::new(0.0, 0.0, 10.0, 10.0),
9482        };
9483        assert!(!gesture_was_a_tap(Some(&select)));
9484        assert!(!gesture_was_a_tap(None), "no gesture, no verdict to give");
9485    }
9486
9487    /// A page that scrolls down must not drift sideways while it does it (reported on a
9488    /// device, 2026-08-16). No finger travels in a straight line, so an area that can go
9489    /// both ways has to pick one and keep it.
9490    #[test]
9491    fn a_scroll_gesture_is_claimed_by_one_axis() {
9492        // Both axes available: the direction the finger went in wins, and a little wobble
9493        // across it changes nothing.
9494        assert!(claim_axis(6.0, 40.0, true, true), "down the page");
9495        assert!(!claim_axis(40.0, 6.0, true, true), "across it");
9496        // A tie reads as vertical, which is the way a page reads.
9497        assert!(claim_axis(10.0, 10.0, true, true));
9498        // One axis only: it takes the gesture whatever the finger did, or a diagonal flick
9499        // down a list would move nothing at all.
9500        assert!(claim_axis(40.0, 6.0, false, true), "a list only goes down");
9501        assert!(
9502            !claim_axis(6.0, 40.0, true, false),
9503            "a strip only goes across"
9504        );
9505    }
9506
9507    /// An area under the finger, `max_x` by `max_y` of content out of sight.
9508    fn area(id: u64, max_x: f32, max_y: f32) -> frus_widgets::Scrollable {
9509        frus_widgets::Scrollable {
9510            id: WidgetId::from_u64(id),
9511            viewport: Rect::new(0.0, 0.0, 100.0, 100.0),
9512            max_x,
9513            max_y,
9514            axis: frus_widgets::Axis::Vertical,
9515            physics: frus_widgets::ScrollPhysics::CLAMPING,
9516            overscroll: Some(frus_widgets::OverscrollIndicator::Glow),
9517            fling: frus_widgets::VelocityStrategy::Regression,
9518            refresh: None,
9519            page: None,
9520            reverse_x: false,
9521            reverse_y: false,
9522            host: None,
9523            keep_visible: None,
9524        }
9525    }
9526
9527    /// A strip that only slides sideways, sitting in a page that only scrolls down, must
9528    /// not swallow a drag downwards: the page behind it is what that drag is about
9529    /// (reported on a device, 2026-08-16).
9530    #[test]
9531    fn the_gesture_goes_to_the_area_that_can_take_it() {
9532        let strip = area(1, 400.0, 0.0);
9533        let page = area(2, 0.0, 900.0);
9534        let chain = [strip, page]; // innermost first, as the finger meets them
9535        assert_eq!(
9536            claim_area(&chain, strip.id, true).map(|a| a.id),
9537            Some(page.id),
9538            "down the page, over the strip"
9539        );
9540        assert_eq!(
9541            claim_area(&chain, strip.id, false).map(|a| a.id),
9542            Some(strip.id),
9543            "across the strip"
9544        );
9545        // Nothing behind it: the strip keeps the gesture and goes its own way, which is
9546        // what a lone recogniser in an arena does.
9547        assert_eq!(
9548            claim_area(&[strip], strip.id, true).map(|a| a.id),
9549            Some(strip.id)
9550        );
9551        // The innermost that can take it wins, not the outermost.
9552        let inner = area(3, 0.0, 200.0);
9553        assert_eq!(
9554            claim_area(&[inner, strip, page], inner.id, true).map(|a| a.id),
9555            Some(inner.id)
9556        );
9557    }
9558
9559    /// **The physics have the last word on a finger** (milestone 649): never scrollable
9560    /// takes none, even with content to reveal, and the area around it gets the drag;
9561    /// always scrollable takes one along its axis with nothing to reveal.
9562    #[test]
9563    fn the_physics_have_the_last_word_on_a_finger() {
9564        let mut inner = area(1, 0.0, 300.0);
9565        inner.physics = frus_widgets::ScrollPhysics::NEVER;
9566        assert!(!inner.accepts_user_offset((0.0, 0.0)));
9567        assert!(!inner.takes_user_scroll(true));
9568        let outer = area(2, 0.0, 600.0);
9569        let chain = [inner, outer];
9570        assert_eq!(
9571            claim_area(&chain, WidgetId::from_u64(1), true).map(|a| a.id),
9572            Some(WidgetId::from_u64(2)),
9573            "the page around it scrolls"
9574        );
9575        assert_eq!(
9576            claim_area(&[inner], WidgetId::from_u64(1), true).map(|a| a.id),
9577            None,
9578            "and alone it keeps nothing"
9579        );
9580
9581        let mut short = area(3, 0.0, 0.0);
9582        short.physics = frus_widgets::ScrollPhysics::ALWAYS;
9583        assert!(short.accepts_user_offset((0.0, 0.0)));
9584        assert!(short.takes_user_scroll(true), "along its axis");
9585        assert!(!short.takes_user_scroll(false), "and not across it");
9586        assert_eq!(
9587            claim_area(&[short, outer], WidgetId::from_u64(3), true).map(|a| a.id),
9588            Some(WidgetId::from_u64(3)),
9589            "a short list that always scrolls keeps the drag from the page"
9590        );
9591        let mut strip = short;
9592        strip.axis = frus_widgets::Axis::Horizontal;
9593        assert_eq!(
9594            claim_area(&[strip, outer], WidgetId::from_u64(3), true).map(|a| a.id),
9595            Some(WidgetId::from_u64(2)),
9596            "a strip that always scrolls across leaves a drag down to the page"
9597        );
9598        let mut both = short;
9599        both.axis = frus_widgets::Axis::Both;
9600        assert!(both.takes_user_scroll(false) && both.takes_user_scroll(true));
9601    }
9602
9603    /// An end-of-content glow on a page with nothing to scroll says something about the
9604    /// content that is not true (reported on a device, 2026-08-16). The reference takes the
9605    /// drag recognisers away outright when everything fits.
9606    #[test]
9607    fn an_area_whose_content_fits_takes_no_finger() {
9608        let fits = area(1, 0.0, 0.0);
9609        assert!(!fits.accepts_user_offset((0.0, 0.0)));
9610        // Unless it is already displaced — content that shrank under a scrolled offset has
9611        // to be draggable back into place.
9612        assert!(fits.accepts_user_offset((0.0, 12.0)));
9613        // Or a refresh area is listening above it: a list of two items still pulls down.
9614        let mut pullable = fits;
9615        pullable.refresh = Some(WidgetId::from_u64(9));
9616        assert!(pullable.accepts_user_offset((0.0, 0.0)));
9617        // A single pixel out of sight is content to reveal, and enough.
9618        assert!(area(1, 0.0, 1.0).accepts_user_offset((0.0, 0.0)));
9619        assert!(area(1, 1.0, 0.0).accepts_user_offset((0.0, 0.0)));
9620    }
9621    use frus_widgets::WidgetId;
9622    use std::collections::HashSet;
9623
9624    /// An estimate reading `velocity` px/s after travelling `offset` px.
9625    fn released(velocity: (f32, f32), offset: (f32, f32)) -> VelocityEstimate {
9626        VelocityEstimate {
9627            velocity: frus_widgets::Velocity::new(velocity.0, velocity.1),
9628            confidence: 1.0,
9629            duration: 0.05,
9630            offset,
9631        }
9632    }
9633
9634    #[test]
9635    fn a_fast_twitch_that_went_nowhere_is_not_a_fling() {
9636        // 2000 px/s, but the finger covered 3 px: a wobble on lift-off, not a throw.
9637        let (x, y) = fling_velocity(released((0.0, 2000.0), (0.0, 3.0)), TOUCH_SLOP);
9638        assert_eq!((x, y), (0.0, 0.0));
9639        // The same speed over a real distance is.
9640        let (_, y) = fling_velocity(released((0.0, 2000.0), (0.0, 60.0)), TOUCH_SLOP);
9641        assert_eq!(y, 2000.0);
9642    }
9643
9644    #[test]
9645    fn the_fling_gate_is_per_axis() {
9646        // A vertical swipe with the sideways wobble a thumb always adds: the wobble
9647        // must not fling the content horizontally.
9648        let (x, y) = fling_velocity(released((300.0, 1500.0), (5.0, 120.0)), TOUCH_SLOP);
9649        assert_eq!(x, 0.0, "the wobble is not a horizontal fling");
9650        assert_eq!(y, 1500.0, "the swipe still flings vertically");
9651    }
9652
9653    #[test]
9654    fn a_precise_pointer_needs_almost_no_travel() {
9655        // The same 3 px that a finger's slop rejects is a deliberate mouse drag.
9656        let estimate = released((0.0, 900.0), (0.0, 3.0));
9657        assert_eq!(fling_velocity(estimate, TOUCH_SLOP).1, 0.0);
9658        assert_eq!(fling_velocity(estimate, PRECISE_SLOP).1, 900.0);
9659    }
9660
9661    #[test]
9662    fn insertion_line_sits_on_the_target_top_edge() {
9663        // The upper half hovered means inserting **before**: a band of thickness 4
9664        // centred on the top edge (y=100) of a target 200 wide.
9665        let line = drop_insertion_line(
9666            Rect::new(20.0, 100.0, 200.0, 44.0),
9667            4.0,
9668            false,
9669            ReorderAxis::Vertical,
9670            false,
9671        );
9672        assert_eq!(line.x, 20.0, "aligned to the target's left");
9673        assert_eq!(line.width, 200.0, "the target's full width");
9674        assert_eq!(line.y, 98.0, "centred on the top edge (100 - 4/2)");
9675        assert_eq!(line.height, 4.0);
9676    }
9677
9678    #[test]
9679    fn insertion_line_sits_on_the_target_bottom_edge_when_inserting_after() {
9680        // The lower half hovered means inserting **after**: the band slides to the
9681        // **bottom** edge (y = 100 + 44 = 144, centred → 142). Same width, same thickness.
9682        let line = drop_insertion_line(
9683            Rect::new(20.0, 100.0, 200.0, 44.0),
9684            4.0,
9685            true,
9686            ReorderAxis::Vertical,
9687            false,
9688        );
9689        assert_eq!(line.x, 20.0, "still aligned to the left");
9690        assert_eq!(line.width, 200.0, "the target's full width");
9691        assert_eq!(line.y, 142.0, "centred on the bottom edge (144 - 4/2)");
9692        assert_eq!(line.height, 4.0);
9693        // A vertical list reads down whichever way its text runs.
9694        let rtl = drop_insertion_line(
9695            Rect::new(20.0, 100.0, 200.0, 44.0),
9696            4.0,
9697            true,
9698            ReorderAxis::Vertical,
9699            true,
9700        );
9701        assert_eq!(rtl, line, "right to left changes nothing down a list");
9702    }
9703
9704    /// **A horizontal list's line stands on its end**, on the edge the row goes to: the
9705    /// right one after a row that reads left to right, the left one after a row that reads
9706    /// right to left — where what follows it is.
9707    #[test]
9708    fn a_horizontal_insertion_line_stands_on_the_edge_the_row_goes_to() {
9709        let chip = Rect::new(100.0, 20.0, 80.0, 48.0);
9710        let line = |after, rtl| drop_insertion_line(chip, 4.0, after, ReorderAxis::Horizontal, rtl);
9711        let before = line(false, false);
9712        assert_eq!(
9713            (before.x, before.y, before.width, before.height),
9714            (98.0, 20.0, 4.0, 48.0),
9715            "before a row: its left edge, the full height, 4 wide"
9716        );
9717        assert_eq!(
9718            line(true, false).x,
9719            178.0,
9720            "after it: its right edge (180 - 4/2)"
9721        );
9722        // Right to left, the two edges trade places.
9723        assert_eq!(
9724            line(true, true).x,
9725            98.0,
9726            "after, right to left: the left edge"
9727        );
9728        assert_eq!(
9729            line(false, true).x,
9730            178.0,
9731            "before, right to left: the right edge"
9732        );
9733    }
9734
9735    /// **Which gesture a reorderable gets.** A column slides; a row or a card is inserted
9736    /// between slots — and a horizontal reorderable is a row only when it says so, which is
9737    /// what keeps a table's columns exactly as they were.
9738    #[test]
9739    fn a_horizontal_reorderable_inserts_only_when_it_says_so() {
9740        assert_eq!(
9741            ReorderMotion::of(ReorderAxis::Horizontal, false),
9742            ReorderMotion::Columns,
9743            "a table's column"
9744        );
9745        assert_eq!(
9746            ReorderMotion::of(ReorderAxis::Horizontal, true),
9747            ReorderMotion::Slots(ReorderAxis::Horizontal),
9748            "a horizontal list's row"
9749        );
9750        // Every vertical reorderable inserts, whatever it answers.
9751        for inserts in [false, true] {
9752            assert_eq!(
9753                ReorderMotion::of(ReorderAxis::Vertical, inserts),
9754                ReorderMotion::Slots(ReorderAxis::Vertical)
9755            );
9756        }
9757        // A widget that answers nothing keeps the trait's defaults, which are the table's.
9758        assert_eq!(
9759            reorder_motion::<()>(&frus_widgets::Container::new()),
9760            ReorderMotion::Columns
9761        );
9762        // And the real rows say which they are, down and across.
9763        for (axis, expected) in [
9764            (
9765                ReorderAxis::Vertical,
9766                ReorderMotion::Slots(ReorderAxis::Vertical),
9767            ),
9768            (
9769                ReorderAxis::Horizontal,
9770                ReorderMotion::Slots(ReorderAxis::Horizontal),
9771            ),
9772        ] {
9773            let list = frus_widgets::ReorderableList::<()>::new(|_, _| ())
9774                .axis(axis)
9775                .row(frus_widgets::Container::new().width(60.0));
9776            let row = frus_widgets::Widget::children(&list)[0].as_ref();
9777            assert_eq!(reorder_motion(row), expected, "{axis:?}");
9778        }
9779    }
9780
9781    /// **The ghost stays in its lane.** A column rises and follows x; a card follows the
9782    /// finger anywhere; a horizontal list's row follows x and nothing else — so a finger
9783    /// that wanders below the strip does not drag the row off it.
9784    #[test]
9785    fn a_horizontal_rows_ghost_follows_the_finger_along_x_only() {
9786        let travel = (120.0, 35.0);
9787        assert_eq!(
9788            ghost_offset(ReorderMotion::Columns, travel),
9789            (120.0, super::drag_preview::LIFT_Y)
9790        );
9791        assert_eq!(
9792            ghost_offset(ReorderMotion::Slots(ReorderAxis::Vertical), travel),
9793            (120.0, 35.0)
9794        );
9795        assert_eq!(
9796            ghost_offset(ReorderMotion::Slots(ReorderAxis::Horizontal), travel),
9797            (120.0, 0.0)
9798        );
9799    }
9800
9801    #[test]
9802    fn focus_returns_to_trigger_when_overlay_closes() {
9803        let anchor = WidgetId::from_u64(1);
9804        let item = WidgetId::from_u64(2);
9805        let (mut history, mut prev) = (Vec::new(), None);
9806
9807        // Frame 1: focus on the anchor, which is present.
9808        let f = resolve_focus(
9809            Some(anchor),
9810            &HashSet::from([anchor]),
9811            &mut history,
9812            &mut prev,
9813        );
9814        assert_eq!(f, Some(anchor));
9815        assert!(history.is_empty());
9816
9817        // Frame 2: the menu is open and the item focused; the anchor is pushed as trigger.
9818        let present = HashSet::from([anchor, item]);
9819        let f = resolve_focus(Some(item), &present, &mut history, &mut prev);
9820        assert_eq!(f, Some(item));
9821        assert_eq!(history, vec![anchor]);
9822
9823        // Frame 3: the menu closed and the item vanished → back to the trigger, history spent.
9824        let f = resolve_focus(
9825            Some(item),
9826            &HashSet::from([anchor]),
9827            &mut history,
9828            &mut prev,
9829        );
9830        assert_eq!(f, Some(anchor), "focus returns to the trigger");
9831        assert!(history.is_empty());
9832    }
9833
9834    #[test]
9835    fn focus_falls_to_none_when_no_trigger_remains() {
9836        let a = WidgetId::from_u64(1);
9837        let (mut history, mut prev) = (Vec::new(), Some(a));
9838        // `a` vanishes and the history is empty → no focus at all.
9839        let f = resolve_focus(Some(a), &HashSet::new(), &mut history, &mut prev);
9840        assert_eq!(f, None);
9841        assert_eq!(prev, None);
9842    }
9843
9844    #[test]
9845    fn spring_approaches_target_monotonically_and_settles() {
9846        // From 0 toward 100: a monotonic approach with no overshoot, all but reached
9847        // after several 16 ms steps.
9848        let mut x = 0.0;
9849        let mut prev = -1.0;
9850        for _ in 0..30 {
9851            x = spring_toward(x, 100.0, 0.016, 0.07);
9852            assert!(x > prev && x <= 100.0, "monotonic and bounded: {x}");
9853            prev = x;
9854        }
9855        assert!(x > 99.0, "all but reached after ~0.5 s: {x}");
9856    }
9857
9858    #[test]
9859    fn ghost_card_shape() {
9860        let theme = Theme::default();
9861        let card = Rect::new(140.0, 0.0, 80.0, 34.0);
9862        // An empty ghost falls back to a shadow plus a solid bordered card: 2 primitives.
9863        let mut scene = Scene::new();
9864        draw_ghost_card(&mut scene, &theme, card, &[]);
9865        assert_eq!(scene.primitives().len(), 2, "shadow plus solid card");
9866    }
9867}
9868
9869/// The half of a scroll request that belongs to the shell: turning the **name** an
9870/// application wrote into the region a frame actually has.
9871#[cfg(test)]
9872mod sheet_request_tests {
9873    use super::{apply_sheet_requests, Runtime, Widget};
9874    use crate::command::Command;
9875    use frus_widgets::{
9876        build_ui, keyed, Container, DraggableScrollableSheet, SheetTo, Size, Theme,
9877    };
9878
9879    /// A page with one named sheet on it — the key on a wrapper the application wrote, the
9880    /// state on a panel it never sees.
9881    fn view() -> Box<dyn Widget<()>> {
9882        Box::new(
9883            Container::<()>::new()
9884                .width(400.0)
9885                .height(800.0)
9886                .child(keyed(
9887                    "places",
9888                    DraggableScrollableSheet::<()>::new(Container::new()),
9889                )),
9890        )
9891    }
9892
9893    /// **A request reaches the sheet the view named**, and nothing else: another key, or a
9894    /// frame without the sheet, hands it back unplaced and moves nothing.
9895    #[test]
9896    fn a_request_reaches_the_sheet_the_view_named() {
9897        let tree = view();
9898        let mut runtime = Runtime::default();
9899        let sheets = build_ui(
9900            tree.as_ref(),
9901            Size::new(400.0, 800.0),
9902            &runtime,
9903            &Theme::default(),
9904        )
9905        .sheets()
9906        .to_vec();
9907        let sheet = sheets.first().expect("the page has its sheet").clone();
9908
9909        let requests = Command::<()>::sheet("places", SheetTo::size(0.9))
9910            .into_parts()
9911            .sheets;
9912        let (unplaced, moved) =
9913            apply_sheet_requests(&mut runtime, tree.as_ref(), requests, &sheets);
9914        assert!(unplaced.is_empty());
9915        assert!(moved);
9916        assert_eq!(runtime.sheet_size(sheet.id, &sheet.spec), 0.9);
9917
9918        let elsewhere = Command::<()>::sheet("elsewhere", SheetTo::size(0.3))
9919            .into_parts()
9920            .sheets;
9921        let (unplaced, moved) =
9922            apply_sheet_requests(&mut runtime, tree.as_ref(), elsewhere, &sheets);
9923        assert_eq!(unplaced.len(), 1);
9924        assert!(!moved);
9925        let not_yet = Command::<()>::sheet("places", SheetTo::size(0.3))
9926            .into_parts()
9927            .sheets;
9928        let (unplaced, _) = apply_sheet_requests(&mut runtime, tree.as_ref(), not_yet, &[]);
9929        assert_eq!(unplaced.len(), 1, "kept for the frame that has the sheet");
9930        assert_eq!(
9931            runtime.sheet_size(sheet.id, &sheet.spec),
9932            0.9,
9933            "nothing moved"
9934        );
9935    }
9936}
9937
9938#[cfg(test)]
9939mod scroll_request_tests {
9940    use super::{apply_scroll_requests, Rect, Runtime, Scrollable, Widget, WidgetId};
9941    use crate::command::Command;
9942    use frus_widgets::{keyed, Container, ScrollTo, SingleChildScrollView};
9943
9944    /// A view with one named scroll region in it, and nothing else of interest.
9945    fn view() -> Box<dyn Widget<()>> {
9946        Box::new(Container::<()>::new().child(keyed(
9947            "log",
9948            SingleChildScrollView::<()>::new().height(200.0),
9949        )))
9950    }
9951
9952    /// The region as the frame would register it: a 200-tall window over 1 000 of content.
9953    fn region(id: WidgetId) -> Scrollable {
9954        Scrollable {
9955            id,
9956            viewport: Rect {
9957                x: 0.0,
9958                y: 0.0,
9959                width: 300.0,
9960                height: 200.0,
9961            },
9962            max_x: 0.0,
9963            max_y: 1000.0,
9964            axis: frus_widgets::Axis::Vertical,
9965            physics: frus_widgets::ScrollPhysics::CLAMPING,
9966            overscroll: Some(frus_widgets::OverscrollIndicator::Glow),
9967            fling: frus_widgets::VelocityStrategy::Regression,
9968            refresh: None,
9969            page: None,
9970            reverse_x: false,
9971            reverse_y: false,
9972            keep_visible: None,
9973            host: None,
9974        }
9975    }
9976
9977    /// The key a request carries and the key the view declared are the **same hash** —
9978    /// which is the whole of the identity design, and the one thing that cannot be
9979    /// checked on either side alone.
9980    #[test]
9981    fn a_request_reaches_the_region_the_view_named() {
9982        let tree = view();
9983        let requests = Command::<()>::scroll("log", ScrollTo::end().instant())
9984            .into_parts()
9985            .scrolls;
9986        let id = frus_widgets::find_by_key(tree.as_ref(), requests[0].0)
9987            .expect("the view named it, so the tree knows it");
9988        let mut runtime = Runtime::default();
9989        let (unplaced, moved) =
9990            apply_scroll_requests(&mut runtime, tree.as_ref(), requests, &[region(id)]);
9991        assert!(unplaced.is_empty());
9992        assert!(moved);
9993        assert_eq!(runtime.scroll.get(&id), Some(&(0.0, 1000.0)));
9994    }
9995
9996    /// A region that has only just appeared is not in the registry the request is first
9997    /// tried against — that one was built last frame — so it is handed back rather than
9998    /// thrown away. Coming back to a screen and being put where you left off depends on
9999    /// exactly this.
10000    #[test]
10001    fn a_region_this_registry_has_not_got_is_handed_back() {
10002        let tree = view();
10003        let requests = Command::<()>::scroll("log", ScrollTo::y(120.0))
10004            .into_parts()
10005            .scrolls;
10006        let mut runtime = Runtime::default();
10007        let (unplaced, moved) = apply_scroll_requests(&mut runtime, tree.as_ref(), requests, &[]);
10008        assert_eq!(unplaced.len(), 1, "kept for the second attempt");
10009        assert!(!moved);
10010        assert!(runtime.scroll_target.is_empty());
10011    }
10012
10013    /// A key naming nothing at all leaves no trace: the caller drops what comes back,
10014    /// so a typo costs one frame's lookup and never accumulates.
10015    #[test]
10016    fn a_name_the_view_does_not_use_moves_nothing() {
10017        let tree = view();
10018        let requests = Command::<()>::scroll("ledger", ScrollTo::start())
10019            .into_parts()
10020            .scrolls;
10021        let mut runtime = Runtime::default();
10022        let id = frus_widgets::find_by_key(
10023            tree.as_ref(),
10024            Command::<()>::scroll("log", ScrollTo::start())
10025                .into_parts()
10026                .scrolls[0]
10027                .0,
10028        )
10029        .expect("the real one is there");
10030        let (unplaced, moved) =
10031            apply_scroll_requests(&mut runtime, tree.as_ref(), requests, &[region(id)]);
10032        assert_eq!(unplaced.len(), 1);
10033        assert!(!moved);
10034        assert!(runtime.scroll.is_empty() && runtime.scroll_target.is_empty());
10035    }
10036
10037    /// Two requests in one batch, both placed, and the batch is what `Command::batch`
10038    /// produces — a screen that restores both axes writes two and means two.
10039    #[test]
10040    fn a_batch_places_every_request_it_carries() {
10041        let tree = view();
10042        let requests = Command::<()>::batch([
10043            Command::scroll("log", ScrollTo::y(400.0).instant()),
10044            Command::scroll("log", ScrollTo::y(600.0).instant()),
10045        ])
10046        .into_parts()
10047        .scrolls;
10048        assert_eq!(requests.len(), 2);
10049        let id = frus_widgets::find_by_key(tree.as_ref(), requests[0].0).expect("named");
10050        let mut runtime = Runtime::default();
10051        let (unplaced, moved) =
10052            apply_scroll_requests(&mut runtime, tree.as_ref(), requests, &[region(id)]);
10053        assert!(unplaced.is_empty() && moved);
10054        assert_eq!(
10055            runtime.scroll.get(&id),
10056            Some(&(0.0, 600.0)),
10057            "the last request is the current statement"
10058        );
10059    }
10060}
10061
10062#[cfg(test)]
10063mod autoscroll_tests {
10064    use super::{
10065        edge_autoscroll, follow_content, Drag, Point, Rect, WidgetId, AUTOSCROLL_MAX_OVERHANG,
10066        AUTOSCROLL_VELOCITY,
10067    };
10068
10069    /// A viewport from 100 to 500, with room to scroll either way.
10070    const VIEW: (f32, f32) = (100.0, 500.0);
10071    const ROOM: (f32, f32) = (1000.0, 1000.0);
10072    /// A sixtieth of a second, which is the frame this is asked on.
10073    const DT: f32 = 1.0 / 60.0;
10074
10075    #[test]
10076    fn a_row_inside_the_viewport_scrolls_nothing() {
10077        assert_eq!(edge_autoscroll((200.0, 260.0), VIEW, ROOM, DT), None);
10078        // Touching the edge exactly is still inside: the row has not asked for anything.
10079        assert_eq!(edge_autoscroll((440.0, 500.0), VIEW, ROOM, DT), None);
10080    }
10081
10082    /// The law: pixels per second per pixel of overhang, so ten pixels out is ten times
10083    /// the speed of one pixel out — the row is not dragged along by a fixed nudge.
10084    #[test]
10085    fn the_speed_follows_how_far_out_the_row_is() {
10086        let near = edge_autoscroll((460.0, 501.0), VIEW, ROOM, DT).expect("one pixel out");
10087        let far = edge_autoscroll((460.0, 510.0), VIEW, ROOM, DT).expect("ten pixels out");
10088        assert!(
10089            near < 0.0,
10090            "the content moves up to reveal what is below: {near}"
10091        );
10092        assert!(
10093            (far / near - 10.0).abs() < 0.01,
10094            "ten times as far, ten times as fast: {near} vs {far}"
10095        );
10096        assert!(
10097            (near + 1.0 * AUTOSCROLL_VELOCITY * DT).abs() < 1e-4,
10098            "one pixel out, at the stated velocity: {near}"
10099        );
10100    }
10101
10102    /// Carrying a row right off the window is not a request to scroll a thousand times
10103    /// faster; past the cap the speed stops growing.
10104    #[test]
10105    fn the_overhang_is_capped() {
10106        let far = edge_autoscroll((460.0, 520.0), VIEW, ROOM, DT).expect("twenty out");
10107        let absurd = edge_autoscroll((460.0, 5000.0), VIEW, ROOM, DT).expect("off the window");
10108        assert!((far - absurd).abs() < 1e-4, "{far} vs {absurd}");
10109        assert!((absurd + AUTOSCROLL_MAX_OVERHANG * AUTOSCROLL_VELOCITY * DT).abs() < 1e-4);
10110    }
10111
10112    /// Above the top the content moves the other way, and the sign is the whole of the
10113    /// difference between the two edges.
10114    #[test]
10115    fn above_the_top_the_content_comes_down() {
10116        let up = edge_autoscroll((90.0, 150.0), VIEW, ROOM, DT).expect("ten above");
10117        assert!(up > 0.0, "{up}");
10118    }
10119
10120    /// **What is carried stays under the finger while its list scrolls.**
10121    ///
10122    /// Seen on a phone: a row carried to the bottom edge started the list scrolling, and
10123    /// the ghost rode up with the content — it is drawn at the row's box, and the box had
10124    /// moved — until it no longer hung over the edge and the scroll stopped by itself,
10125    /// 95 px in. The press moves with the content instead.
10126    #[test]
10127    fn a_carried_item_follows_its_content() {
10128        // The list scrolled on by 95 px: its content went 95 px up the screen.
10129        let shift = (0.0, -95.0);
10130        let pressed = Point::new(900.0, 1330.0);
10131        let row_then = Rect::new(105.0, 1250.0, 700.0, 160.0);
10132        let row_now = row_then.translate(0.0, -95.0);
10133        let finger = Point::new(900.0, 2030.0);
10134
10135        let mut reorder = Drag::Reorder {
10136            id: WidgetId::from_u64(1),
10137            from: 0,
10138            start: pressed,
10139            moved: true,
10140            carried: Some((WidgetId::from_u64(1), row_then)),
10141        };
10142        follow_content(&mut reorder, shift);
10143        let Drag::Reorder { start, carried, .. } = reorder else {
10144            unreachable!("still a reorder");
10145        };
10146        // The box it was carried from went with the content, as the row did — and the frame's
10147        // own box is not asked again, since the frame clips a row carried to an edge and loses
10148        // one scrolled out of sight (milestone 527).
10149        let (_, carried) = carried.expect("the box it was carried from");
10150        assert_eq!(carried, row_now, "the carried box went with the content");
10151        // Drawn the way the shell draws it: the carried box, plus the travel.
10152        let ghost = carried.translate(finger.x - start.x, finger.y - start.y);
10153        assert_eq!(
10154            ghost,
10155            row_then.translate(finger.x - pressed.x, finger.y - pressed.y),
10156            "the ghost is where it would be had nothing scrolled"
10157        );
10158
10159        let mut item = Drag::Item {
10160            source: frus_widgets::DragSource {
10161                id: WidgetId::from_u64(1),
10162                rect: row_then,
10163                payload: 7,
10164            },
10165            start: pressed,
10166            moved: true,
10167            over: None,
10168        };
10169        follow_content(&mut item, shift);
10170        let Drag::Item { source, start, .. } = item else {
10171            unreachable!("still an item");
10172        };
10173        assert_eq!(
10174            source.rect, row_now,
10175            "the box taken at the press went with the content"
10176        );
10177        assert_eq!(start, Point::new(900.0, 1235.0), "and so did the press");
10178    }
10179
10180    /// At the end of the content there is nothing left to reveal, so the list stays where
10181    /// it is instead of straining against its own end.
10182    #[test]
10183    fn an_exhausted_edge_stays_still() {
10184        assert_eq!(
10185            edge_autoscroll((460.0, 520.0), VIEW, (1000.0, 0.0), DT),
10186            None
10187        );
10188        assert_eq!(
10189            edge_autoscroll((90.0, 150.0), VIEW, (0.0, 1000.0), DT),
10190            None
10191        );
10192    }
10193
10194    /// A row taller than the viewport hangs over both edges at once. Pulled two ways it
10195    /// would scroll nowhere, or jitter between them; the leading edge decides.
10196    #[test]
10197    fn a_row_taller_than_the_viewport_follows_its_leading_edge() {
10198        let both = edge_autoscroll((50.0, 900.0), VIEW, ROOM, DT).expect("over both edges");
10199        assert!(both > 0.0, "the top edge wins: {both}");
10200    }
10201}
10202
10203/// **A back gesture the page below cannot take** (milestone 531).
10204///
10205/// Seen on a phone: a back gesture from the left edge, at the height of a task row of the
10206/// page below, did not slide the page on top. Half a second in, the row was lifted instead —
10207/// drawn over the page on top in its green-outlined ghost, and following the finger — and it
10208/// went away when the finger did. At a height with no row the gesture worked.
10209///
10210/// The press that starts the gesture is tested against the frame on screen, and after a push
10211/// that frame is the push's last: an application's animation that has just settled asks for
10212/// no rebuild, so the page the push left is still in the frame, parallaxed to the left and
10213/// under the finger. The press found the row's hold, the long press armed, and its deadline
10214/// replaced the gesture with a lift.
10215#[cfg(test)]
10216mod back_gesture_tests {
10217    use super::{
10218        drag_after_hold, frame_needs_build, hold_candidates, Drag, Point, Theme, WidgetId,
10219    };
10220    use frus_widgets::{build_ui, column, Container, DragSource, Draggable, Navigator};
10221    use frus_widgets::{Rect, Runtime, Size, Ui, Widget};
10222
10223    /// A phone, in logical pixels.
10224    const W: f32 = 392.0;
10225    const H: f32 = 850.0;
10226    /// Where the row sits on the page below, and the finger's press on the edge at its
10227    /// height.
10228    const ROW_TOP: f32 = 480.0;
10229    const ROW_HEIGHT: f32 = 66.0;
10230    const ON_THE_ROW: Point = Point::new(3.0, 509.0);
10231    const ABOVE_THE_ROW: Point = Point::new(3.0, 200.0);
10232    /// The long press the row asks for.
10233    const HELD: u8 = 1;
10234
10235    /// A page with one row that lifts on a hold and says something when held, below a
10236    /// stretch with nothing to take.
10237    fn home() -> impl Widget<u8> {
10238        column![
10239            Container::<u8>::new().width(W).height(ROW_TOP),
10240            Draggable::new(
10241                Container::<u8>::new()
10242                    .width(W)
10243                    .height(ROW_HEIGHT)
10244                    .on_long_press(HELD)
10245            )
10246            .payload(7)
10247            .long_press(),
10248        ]
10249    }
10250
10251    /// The frame on screen after a push from `home` has settled: the push's last, with the
10252    /// page it left still in it.
10253    fn after_a_push() -> Navigator<u8> {
10254        Navigator::new("data", Container::<u8>::new().width(W).height(H))
10255            .size(W, H)
10256            .from("home", home(), 0.999, true)
10257    }
10258
10259    fn frame(root: &dyn Widget<u8>) -> Ui<u8> {
10260        build_ui(
10261            root,
10262            Size::new(W, H),
10263            &Runtime::default(),
10264            &Theme::default(),
10265        )
10266    }
10267
10268    fn back(at: Point) -> Drag {
10269        Drag::Back { start_x: at.x }
10270    }
10271
10272    fn still_scroll() -> Drag {
10273        Drag::Scroll {
10274            id: WidgetId::from_u64(9),
10275            last: ON_THE_ROW,
10276            moved: false,
10277            carried: (0.0, 0.0),
10278            dismiss: None,
10279            axis: None,
10280            pan: None,
10281        }
10282    }
10283
10284    fn row_source() -> DragSource {
10285        DragSource {
10286            id: WidgetId::from_u64(3),
10287            rect: Rect::new(-117.0, ROW_TOP, W, ROW_HEIGHT),
10288            payload: 7,
10289        }
10290    }
10291
10292    /// **Why the row was under the finger at all**: the frame a transition ends on is built
10293    /// again. The tick that ends a push takes the page it left out of the state and reports
10294    /// nothing moving, so a loop that builds only while something moves hit-tests the
10295    /// push's last frame from then on — the page left behind still in it, where a tap or a
10296    /// hold can reach what is no longer on screen. The demo's
10297    /// `the_last_frame_of_a_push_still_holds_the_page_it_left` shows that frame on its own
10298    /// page, at the phone's coordinates.
10299    #[test]
10300    fn the_frame_an_animation_settles_in_is_built_again() {
10301        assert!(
10302            frame_needs_build(false, false, true, false),
10303            "the frame the application's animation stops in builds the view again"
10304        );
10305        assert!(
10306            frame_needs_build(false, true, true, false),
10307            "as does every frame while it moves"
10308        );
10309        assert!(
10310            !frame_needs_build(false, false, false, false),
10311            "and a still frame after a still one repaints the tree it has"
10312        );
10313        assert!(frame_needs_build(true, false, false, false), "unless asked");
10314        assert!(
10315            frame_needs_build(false, false, false, true),
10316            "or the frame has its own reason"
10317        );
10318    }
10319
10320    /// The reproduction: the frame really has a row to take under the finger, and a back
10321    /// gesture's press takes nothing from it.
10322    #[test]
10323    fn a_back_gesture_claims_nothing_under_its_finger() {
10324        let root = after_a_push();
10325        let ui = frame(&root);
10326        let (held, lift) = hold_candidates(None, Some(&ui), Some(&root), ON_THE_ROW);
10327        assert!(
10328            lift.is_some() && held == Some(HELD),
10329            "the frame the phone had: a press there, with no gesture, finds the row's hold"
10330        );
10331
10332        let gesture = back(ON_THE_ROW);
10333        let (held, lift) = hold_candidates(Some(&gesture), Some(&ui), Some(&root), ON_THE_ROW);
10334        assert!(
10335            lift.is_none(),
10336            "a back gesture's press arms no lift of the row under it"
10337        );
10338        assert_eq!(held, None, "nor its long press");
10339    }
10340
10341    /// Whatever a press found, the deadline never takes the pointer from a back gesture:
10342    /// the gesture goes on, and keeps its start.
10343    #[test]
10344    fn a_hold_never_takes_the_pointer_from_a_back_gesture() {
10345        let reorder = Some((WidgetId::from_u64(4), 2, ON_THE_ROW));
10346        for (lift, reorder) in [
10347            (Some(row_source()), None),
10348            (None, reorder),
10349            (Some(row_source()), reorder),
10350            (None, None),
10351        ] {
10352            let after = drag_after_hold(Some(back(ON_THE_ROW)), lift, reorder, ON_THE_ROW);
10353            assert!(
10354                matches!(after, Some(Drag::Back { start_x }) if start_x == ON_THE_ROW.x),
10355                "the back gesture is still the drag (lift {}, reorder {})",
10356                lift.is_some(),
10357                reorder.is_some()
10358            );
10359        }
10360    }
10361
10362    /// At a height with nothing to take — where the gesture always worked — it still does.
10363    #[test]
10364    fn a_back_gesture_where_nothing_is_under_it_still_owns_the_pointer() {
10365        let root = after_a_push();
10366        let ui = frame(&root);
10367        let gesture = back(ABOVE_THE_ROW);
10368        let (held, lift) = hold_candidates(Some(&gesture), Some(&ui), Some(&root), ABOVE_THE_ROW);
10369        assert!(held.is_none() && lift.is_none());
10370        assert!(matches!(
10371            drag_after_hold(Some(gesture), None, None, ABOVE_THE_ROW),
10372            Some(Drag::Back { .. })
10373        ));
10374    }
10375
10376    /// No regression: a hold on the row with no back gesture — the list's scroll still
10377    /// waiting to see whether the finger moves — lifts it, or its reorder, and says what
10378    /// the row asks when held.
10379    #[test]
10380    fn a_hold_on_a_row_without_a_back_gesture_still_lifts_it() {
10381        let root = home();
10382        let ui = frame(&root);
10383        let at = Point::new(40.0, ROW_TOP + 10.0);
10384        let scroll = still_scroll();
10385        let (held, lift) = hold_candidates(Some(&scroll), Some(&ui), Some(&root), at);
10386        assert_eq!(
10387            held,
10388            Some(HELD),
10389            "the long press is armed under a still scroll"
10390        );
10391        assert!(lift.is_some(), "and so is the lift");
10392        let (held, lift) = hold_candidates(None, Some(&ui), Some(&root), at);
10393        assert!(held.is_some() && lift.is_some(), "and with no drag at all");
10394
10395        let cursor = Point::new(41.0, ROW_TOP + 11.0);
10396        let lifted = drag_after_hold(Some(still_scroll()), Some(row_source()), None, cursor);
10397        assert!(
10398            matches!(lifted, Some(Drag::Item { moved: true, start, .. }) if start == cursor),
10399            "the hold lifts the item where the finger is"
10400        );
10401        let reorder = Some((WidgetId::from_u64(4), 2, at));
10402        let reordering = drag_after_hold(Some(still_scroll()), None, reorder, cursor);
10403        assert!(
10404            matches!(reordering, Some(Drag::Reorder { moved: true, from: 2, start, .. }) if start == at),
10405            "or picks the row up for a reorder, from where it was pressed"
10406        );
10407        assert!(
10408            drag_after_hold(Some(still_scroll()), None, None, cursor).is_none(),
10409            "a hold with nothing to lift ends the still scroll: the long press had it"
10410        );
10411    }
10412}
10413
10414/// How many layers `primitives` hold, nested ones included (milestone 609).
10415fn count_layers(primitives: &[frus_widgets::Primitive]) -> u32 {
10416    primitives
10417        .iter()
10418        .map(|p| match p {
10419            frus_widgets::Primitive::Layer { primitives, .. } => 1 + count_layers(primitives),
10420            _ => 0,
10421        })
10422        .sum()
10423}
10424
10425/// **What the shell reads of a key going down** (milestone 608). Winit's own event cannot be
10426/// made outside winit, so the shell's handling takes this, made from it in a window and by
10427/// hand in a test.
10428pub(crate) struct KeyDown {
10429    /// The key the layout makes of it: a character, or a named key.
10430    logical_key: WinitKey,
10431    /// Where it is on the keyboard, whatever the layout.
10432    physical_key: PhysicalKey,
10433    /// The text it types, if any.
10434    text: Option<winit::keyboard::SmolStr>,
10435    /// Whether it is the key repeating, held down.
10436    repeat: bool,
10437}
10438
10439impl From<&winit::event::KeyEvent> for KeyDown {
10440    fn from(event: &winit::event::KeyEvent) -> Self {
10441        Self {
10442            logical_key: event.logical_key.clone(),
10443            physical_key: event.physical_key,
10444            text: event.text.clone(),
10445            repeat: event.repeat,
10446        }
10447    }
10448}
10449
10450/// `scene` over the window's background, for a see-through renderer, whose frames start
10451/// transparent (milestone 643): everywhere but the `line` pixels at the top the system paints,
10452/// which are left clear for its title bar to show through. In physical pixels, as the scene
10453/// is; the scene's own primitives follow, in their order.
10454fn backdrop_under(mut scene: Scene, width: f32, height: f32, line: f32) -> Scene {
10455    let mut framed = Scene::new();
10456    framed.fill_rect(
10457        frus_widgets::Rect::new(0.0, line, width, (height - line).max(0.0)),
10458        frus_gpu::backdrop(),
10459    );
10460    for primitive in scene.split_off(0) {
10461        framed.push_primitive(primitive);
10462    }
10463    framed
10464}
10465
10466#[cfg(test)]
10467mod backdrop_tests {
10468    use super::*;
10469
10470    /// **A see-through frame is the window's background under everything but the line the
10471    /// system paints**, and the scene over it, unchanged.
10472    #[test]
10473    fn the_background_leaves_the_system_s_line_clear() {
10474        let mut scene = Scene::new();
10475        scene.fill_rect(
10476            frus_widgets::Rect::new(10.0, 5.0, 20.0, 20.0),
10477            frus_widgets::Color::WHITE,
10478        );
10479        let framed = backdrop_under(scene.clone(), 800.0, 600.0, 48.0);
10480        let prims = framed.primitives();
10481        assert_eq!(prims.len(), 2);
10482        match &prims[0] {
10483            frus_widgets::Primitive::Rect { rect, color, .. } => {
10484                assert_eq!(*rect, frus_widgets::Rect::new(0.0, 48.0, 800.0, 552.0));
10485                assert_eq!(*color, frus_gpu::backdrop());
10486                assert_eq!(
10487                    color.a, 1.0,
10488                    "opaque: nothing behind the window shows there"
10489                );
10490            }
10491            other => panic!("the background first: {other:?}"),
10492        }
10493        assert_eq!(prims[1], scene.primitives()[0], "then the scene");
10494
10495        let whole = backdrop_under(Scene::new(), 800.0, 600.0, 0.0);
10496        assert!(
10497            matches!(&whole.primitives()[0],
10498            frus_widgets::Primitive::Rect { rect, .. } if *rect == frus_widgets::Rect::new(0.0, 0.0, 800.0, 600.0)),
10499            "no line: the whole window"
10500        );
10501    }
10502}
10503
10504/// A driver with no window and no event loop: input and frames fed by hand, through the
10505/// shell's own paths.
10506#[cfg(any(test, feature = "testing"))]
10507#[doc(hidden)]
10508pub mod testing {
10509    use super::*;
10510    use crate::gesture::{PointerEvent, PointerKind, LONG_PRESS_DELAY};
10511
10512    /// What the last frame left behind: the interface it laid out, and the tree it laid out from.
10513    pub type FrameParts<'a, Msg> = (&'a Ui<Msg>, &'a dyn Widget<Msg>);
10514
10515    /// The shell around an application, on a surface of a stated logical size.
10516    pub struct Driver<A: Application> {
10517        shell: App<A>,
10518        size: Size,
10519        /// Where the last frame drew a reorder's ghost, when it drew one.
10520        ghost: Option<Rect>,
10521    }
10522
10523    impl<A: Application> Driver<A> {
10524        /// A driver for `app` on a `width` by `height` logical surface.
10525        pub fn new(app: A, width: f32, height: f32) -> Self {
10526            let mut shell = App::detached(app);
10527            // Gestures are timed by the frames run, as everything else here is.
10528            shell.gesture_clock = Some((0.0, 0.0));
10529            Self {
10530                shell,
10531                size: Size::new(width, height),
10532                ghost: None,
10533            }
10534        }
10535
10536        /// The system's **Back** — Android's back button, the browser's Back — as the shell
10537        /// handles it, short of leaving: whether it went back from anything.
10538        pub fn system_back(&mut self) -> bool {
10539            self.shell.system_back_step()
10540        }
10541
10542        /// How the gesture under way will read its fling: its scroll area's behaviour's, on
10543        /// a drag that landed on one, else the application's (milestone 616).
10544        pub fn fling_strategy(&self) -> frus_widgets::VelocityStrategy {
10545            self.shell.gesture_velocity.strategy()
10546        }
10547
10548        /// Where what is carried is drawn this frame — a reorder's ghost or a lifted item —
10549        /// or `None` when nothing is.
10550        pub fn carried(&self) -> Option<Rect> {
10551            self.shell.carried_rect()
10552        }
10553
10554        /// Where the last frame drew a reorder's ghost — the outline around it — or `None`
10555        /// when it drew none.
10556        pub fn ghost(&self) -> Option<Rect> {
10557            self.ghost
10558        }
10559
10560        /// Every scroll region of the last frame, as its viewport and its offset.
10561        pub fn offsets(&self) -> Vec<(Rect, (f32, f32))> {
10562            let s = &self.shell;
10563            s.ui.as_ref()
10564                .map(|ui| {
10565                    ui.scroll_regions()
10566                        .iter()
10567                        .map(|a| {
10568                            (
10569                                a.viewport,
10570                                s.runtime.scroll.get(&a.id).copied().unwrap_or((0.0, 0.0)),
10571                            )
10572                        })
10573                        .collect()
10574                })
10575                .unwrap_or_default()
10576        }
10577
10578        /// The interface the last frame laid out — what a tap is asked about, what the registries
10579        /// hold — and the tree it was laid out from.
10580        pub fn frame_parts(&self) -> Option<FrameParts<'_, A::Message>> {
10581            let s = &self.shell;
10582            Some((s.ui.as_ref()?, s.tree.as_deref()?))
10583        }
10584
10585        /// The words the last frame drew and the box each was drawn in, in paint order.
10586        pub fn texts(&self) -> Vec<(String, Rect)> {
10587            self.shell
10588                .ui
10589                .as_ref()
10590                .map(|ui| {
10591                    ui.scene()
10592                        .primitives()
10593                        .iter()
10594                        .filter_map(|p| match p {
10595                            frus_widgets::Primitive::Text { text, .. } => {
10596                                Some((text.clone(), p.bounds()))
10597                            }
10598                            _ => None,
10599                        })
10600                        .collect()
10601                })
10602                .unwrap_or_default()
10603        }
10604
10605        /// A tap on the first word of the last frame that reads `label`, and a frame after it:
10606        /// how a test presses a button by what it says. Whether there was such a word.
10607        pub fn tap_text(&mut self, label: &str) -> bool {
10608            let Some((_, rect)) = self.texts().into_iter().find(|(text, _)| text == label) else {
10609                return false;
10610            };
10611            let at = Point::new(rect.x + rect.width / 2.0, rect.y + rect.height / 2.0);
10612            self.press(at);
10613            self.release(at);
10614            self.frame(1.0 / 60.0);
10615            true
10616        }
10617
10618        /// The application, as the shell holds it.
10619        pub fn app(&self) -> &A {
10620            &self.shell.app
10621        }
10622
10623        /// The widget with the keyboard focus, if one has it.
10624        pub fn focused(&self) -> Option<WidgetId> {
10625            self.shell.runtime.input.focused
10626        }
10627
10628        /// The modifiers the shell holds as down: Ctrl, Shift, Alt and Meta.
10629        pub fn shell_modifiers(&self) -> (bool, bool, bool, bool) {
10630            (
10631                self.shell.ctrl,
10632                self.shell.shift,
10633                self.shell.alt,
10634                self.shell.meta,
10635            )
10636        }
10637
10638        /// The widget the last frame would hand a press at `at`, if any.
10639        pub fn hit(&self, at: Point) -> Option<WidgetId> {
10640            self.shell.ui.as_ref().and_then(|ui| ui.hit(at))
10641        }
10642
10643        /// Hands the application a message, as the shell does.
10644        pub fn update(&mut self, message: A::Message) {
10645            self.shell.dispatch(message);
10646        }
10647
10648        /// Frames at sixty a second for `seconds`.
10649        pub fn run(&mut self, seconds: f32) {
10650            let n = (seconds * 60.0).round() as usize;
10651            for _ in 0..n.max(1) {
10652                self.frame(1.0 / 60.0);
10653            }
10654        }
10655
10656        /// A finger down at `at`.
10657        pub fn press(&mut self, at: Point) {
10658            self.pointer(PointerKind::Down, at);
10659        }
10660
10661        /// The finger moved to `at`.
10662        pub fn move_to(&mut self, at: Point) {
10663            self.pointer(PointerKind::Move, at);
10664        }
10665
10666        /// The finger lifted at `at`.
10667        pub fn release(&mut self, at: Point) {
10668            self.pointer(PointerKind::Up, at);
10669        }
10670
10671        /// The long-press deadline, delivered as the loop's wake delivers it. Whether it
10672        /// fired: a press that moved past the slop, or was never waiting, has none.
10673        pub fn hold_deadline(&mut self) -> bool {
10674            let fired = self.shell.press.poll(Instant::now() + LONG_PRESS_DELAY);
10675            if fired {
10676                self.shell.hold_deadline_reached();
10677            }
10678            fired
10679        }
10680
10681        /// The double tap's wait, over: a first tap still waiting is sent as a tap. Whether one
10682        /// was waiting.
10683        pub fn tap_deadline(&mut self) -> bool {
10684            self.shell
10685                .poll_pending_tap(Instant::now() + DOUBLE_TAP_TIMEOUT)
10686        }
10687
10688        /// The cursor the window would show where the pointer is now.
10689        pub fn cursor_shown(&self) -> frus_widgets::Cursor {
10690            let s = &self.shell;
10691            let requested = s.runtime.input.hovered.and_then(|id| {
10692                let rect = s.ui.as_ref()?.widget_rect(id)?;
10693                let widget = find_widget(s.tree.as_ref()?.as_ref(), id)?;
10694                widget.cursor_icon(
10695                    s.cursor.x - rect.x,
10696                    s.cursor.y - rect.y,
10697                    rect.width,
10698                    rect.height,
10699                )
10700            });
10701            s.region_cursor(requested)
10702        }
10703
10704        /// When the loop would wake next if it went idle now, if it would.
10705        pub fn next_wake(&self) -> Option<Instant> {
10706            match self.shell.idle_control_flow() {
10707                ControlFlow::WaitUntil(at) => Some(at),
10708                _ => None,
10709            }
10710        }
10711
10712        /// A click of the secondary mouse button at `at`.
10713        pub fn secondary_click(&mut self, at: Point) {
10714            self.pointer_of(PointerKind::Move, at, false);
10715            self.shell.context_click();
10716        }
10717
10718        /// A mouse button down at `at` — a pointer that hovers, which is not a finger: nothing is
10719        /// left to scroll, and a press on words in a selection area begins a selection.
10720        pub fn press_mouse(&mut self, at: Point) {
10721            self.pointer_of(PointerKind::Down, at, false);
10722        }
10723
10724        /// The mouse moved to `at`, its button down.
10725        pub fn move_mouse(&mut self, at: Point) {
10726            self.pointer_of(PointerKind::Move, at, false);
10727        }
10728
10729        /// The mouse button lifted at `at`.
10730        pub fn release_mouse(&mut self, at: Point) {
10731            self.pointer_of(PointerKind::Up, at, false);
10732        }
10733
10734        /// The words a selection area has selected, as Copy would put them on the clipboard.
10735        pub fn area_selection(&self) -> Option<String> {
10736            self.shell.region_text()
10737        }
10738
10739        /// Select all, as the keyboard asks it of a selection area that holds a selection.
10740        pub fn area_select_all(&mut self) {
10741            self.shell.select_all_region();
10742        }
10743
10744        /// Escape, as the keyboard delivers it.
10745        pub fn escape(&mut self) {
10746            self.shell.escape();
10747        }
10748
10749        /// **A key pressed**, with the modifiers the stroke names held down for it, through
10750        /// the shell's own handling of a key (milestone 608): the arrows move the focus or a
10751        /// caret or answer the widget with the focus, Enter and Space activate, Escape
10752        /// closes, Tab moves on, a bound shortcut fires, a character is typed.
10753        ///
10754        /// The system's back key is not one of them: what it does last is quit, and a
10755        /// driver has no loop to quit.
10756        pub fn key(&mut self, stroke: frus_widgets::KeyStroke) {
10757            use frus_widgets::ShortcutKey as K;
10758            let named = |key| (WinitKey::Named(key), None);
10759            let (logical_key, text) = match stroke.key {
10760                K::Char(c) => {
10761                    let typed: winit::keyboard::SmolStr = c.to_string().into();
10762                    (WinitKey::Character(typed.clone()), Some(typed))
10763                }
10764                K::Enter => named(NamedKey::Enter),
10765                K::Escape => named(NamedKey::Escape),
10766                K::Tab => named(NamedKey::Tab),
10767                K::Space => (WinitKey::Named(NamedKey::Space), Some(" ".into())),
10768                K::Backspace => named(NamedKey::Backspace),
10769                K::Delete => named(NamedKey::Delete),
10770                K::Up => named(NamedKey::ArrowUp),
10771                K::Down => named(NamedKey::ArrowDown),
10772                K::Left => named(NamedKey::ArrowLeft),
10773                K::Right => named(NamedKey::ArrowRight),
10774                K::Home => named(NamedKey::Home),
10775                K::End => named(NamedKey::End),
10776                K::PageUp => named(NamedKey::PageUp),
10777                K::PageDown => named(NamedKey::PageDown),
10778                K::F(n) => named(match n {
10779                    1 => NamedKey::F1,
10780                    2 => NamedKey::F2,
10781                    3 => NamedKey::F3,
10782                    4 => NamedKey::F4,
10783                    5 => NamedKey::F5,
10784                    6 => NamedKey::F6,
10785                    7 => NamedKey::F7,
10786                    8 => NamedKey::F8,
10787                    9 => NamedKey::F9,
10788                    10 => NamedKey::F10,
10789                    11 => NamedKey::F11,
10790                    _ => NamedKey::F12,
10791                }),
10792            };
10793            // Where a letter is on a QWERTY keyboard: the clipboard's shortcuts are read off
10794            // the key's place, so that they work on any layout.
10795            const LETTERS: [KeyCode; 26] = [
10796                KeyCode::KeyA,
10797                KeyCode::KeyB,
10798                KeyCode::KeyC,
10799                KeyCode::KeyD,
10800                KeyCode::KeyE,
10801                KeyCode::KeyF,
10802                KeyCode::KeyG,
10803                KeyCode::KeyH,
10804                KeyCode::KeyI,
10805                KeyCode::KeyJ,
10806                KeyCode::KeyK,
10807                KeyCode::KeyL,
10808                KeyCode::KeyM,
10809                KeyCode::KeyN,
10810                KeyCode::KeyO,
10811                KeyCode::KeyP,
10812                KeyCode::KeyQ,
10813                KeyCode::KeyR,
10814                KeyCode::KeyS,
10815                KeyCode::KeyT,
10816                KeyCode::KeyU,
10817                KeyCode::KeyV,
10818                KeyCode::KeyW,
10819                KeyCode::KeyX,
10820                KeyCode::KeyY,
10821                KeyCode::KeyZ,
10822            ];
10823            let physical_key = match stroke.key {
10824                K::Char(c) if c.is_ascii_alphabetic() => {
10825                    PhysicalKey::Code(LETTERS[(c.to_ascii_lowercase() as u8 - b'a') as usize])
10826                }
10827                _ => PhysicalKey::Unidentified(winit::keyboard::NativeKeyCode::Unidentified),
10828            };
10829            let held = (
10830                self.shell.ctrl,
10831                self.shell.shift,
10832                self.shell.alt,
10833                self.shell.meta,
10834            );
10835            (
10836                self.shell.ctrl,
10837                self.shell.shift,
10838                self.shell.alt,
10839                self.shell.meta,
10840            ) = (stroke.ctrl, stroke.shift, stroke.alt, stroke.meta);
10841            self.shell.key_down(
10842                &KeyDown {
10843                    logical_key,
10844                    physical_key,
10845                    text,
10846                    repeat: false,
10847                },
10848                None,
10849            );
10850            (
10851                self.shell.ctrl,
10852                self.shell.shift,
10853                self.shell.alt,
10854                self.shell.meta,
10855            ) = held;
10856        }
10857
10858        /// The two handles of a selection area's touch selection, on the surface, if it has them.
10859        pub fn area_handles(&self) -> Option<[frus_widgets::SelectionHandle; 2]> {
10860            self.shell.region_handles().map(|(_, handles)| handles)
10861        }
10862
10863        fn pointer(&mut self, kind: PointerKind, at: Point) {
10864            self.pointer_of(kind, at, true);
10865        }
10866
10867        fn pointer_of(&mut self, kind: PointerKind, at: Point, touch: bool) {
10868            self.shell.pointer_event(PointerEvent {
10869                kind,
10870                position: at,
10871                touch,
10872            });
10873        }
10874
10875        /// One frame, the real frame's steps in the real frame's order, less the GPU.
10876        pub fn frame(&mut self, dt: f32) {
10877            let (width, height) = (self.size.width, self.size.height);
10878            let s = &mut self.shell;
10879            if let Some((now, _)) = s.gesture_clock.as_mut() {
10880                *now += dt;
10881            }
10882            if s.last_size != Some((width, height)) {
10883                s.last_size = Some((width, height));
10884                s.build_dirty = true;
10885                s.app.on_resize(width, height);
10886            }
10887            let mut app_animating = s.app.tick(dt);
10888            let settings = s
10889                .platform
10890                .accessibility
10891                .with_overrides(s.app.accessibility());
10892            let theme_moved = s.themes.advance(
10893                &s.app,
10894                s.platform.brightness,
10895                settings.high_contrast,
10896                settings.disable_animations,
10897                dt,
10898            );
10899            app_animating |= theme_moved | s.themes.animating();
10900            let theme = s.themes.displayed(&s.app);
10901            install_ambient(&s.app, &s.platform.locales);
10902            let _surface = s.media_query(width, height).install();
10903            s.runtime.still = settings.disable_animations;
10904            let was = std::mem::replace(&mut s.app_was_animating, app_animating);
10905            let asked = s.app.effects();
10906            s.run_command(asked);
10907            let arrived = s.take_task_arrivals();
10908            if frus_widgets::take_rebuild_request() || arrived {
10909                s.build_dirty = true;
10910            }
10911            s.report_focus();
10912            if frame_needs_build(
10913                s.build_dirty,
10914                app_animating,
10915                was,
10916                s.tree.is_none() || s.runtime.switching(),
10917            ) {
10918                s.tree = Some(build_view(&s.app, &theme, &s.runtime));
10919            }
10920            s.build_dirty = false;
10921            s.autoscroll_carried(dt);
10922            s.advance_reorder_springs(dt);
10923            let regions =
10924                s.ui.as_ref()
10925                    .map(|ui| ui.scroll_regions().to_vec())
10926                    .unwrap_or_default();
10927            s.runtime.sync_pages(&regions);
10928            s.runtime.sync_visible(&regions);
10929            s.runtime.advance(dt);
10930            s.runtime.advance_scroll(&regions, dt);
10931            let tree = s.tree.as_deref().expect("the view was built");
10932            // The widgets' own values, as the window's frame steps them: without it a value
10933            // a drag held would be held for ever here (milestone 638).
10934            s.runtime.advance_values(tree, dt);
10935            let ui = build_ui(tree, Size::new(width, height), &s.runtime, &theme);
10936            s.runtime.states.end_frame();
10937            let mut scene = ui.scene().clone();
10938            let ghost = if matches!(s.drag, Some(Drag::Reorder { moved: true, .. })) {
10939                s.paint_reorder_preview(&ui, &theme, &mut scene);
10940                // The ghost's outline is the last thing the preview draws, on the ghost's box.
10941                scene.primitives().last().map(|p| p.bounds())
10942            } else {
10943                None
10944            };
10945            let paged = ui.scroll_regions().to_vec();
10946            let scrolled: Vec<A::Message> = {
10947                let grain = |id| find_widget(tree, id).map_or(0.0, |w| w.scroll_grain());
10948                s.runtime
10949                    .scroll_changes(&paged, grain)
10950                    .into_iter()
10951                    .filter_map(|(id, position)| {
10952                        find_widget(tree, id).and_then(|w| w.on_scroll(position))
10953                    })
10954                    .collect()
10955            };
10956            s.ui = Some(ui);
10957            s.sync_regions(false);
10958            // As the shell does after every frame: focus that vanished goes back, and a
10959            // widget that takes the focus on appearing takes it (milestone 605).
10960            s.reconcile_focus();
10961            for message in scrolled {
10962                s.dispatch(message);
10963            }
10964            self.ghost = ghost;
10965        }
10966    }
10967}
10968
10969/// **What is carried stays carried, however far its list scrolls** (milestone 527, seen on
10970/// a phone).
10971///
10972/// A label held on the Kanban screen's strip and carried towards the strip's right edge was
10973/// not drawn lifted, the strip scrolled away under it, and the release put it somewhere the
10974/// finger had never been. The carried row's box came from the frame, and the frame keeps a
10975/// reorderable's box only while it is on screen, clipped to what shows: once auto-scroll had
10976/// pushed the row against the edge the box stopped moving with the content while the press
10977/// kept moving, the ghost ran ahead of the finger, the scroll fed on it, and a row scrolled
10978/// out of sight had no box at all.
10979///
10980/// These drive the shell's own input path and its frame, on the screen's shape: a bar, a strip
10981/// of ten 96-px labels that lift on a hold, and a board of cards, each in a horizontal scroll.
10982#[cfg(test)]
10983mod carried_row_tests {
10984    use super::testing::Driver;
10985    use crate::{Application, Command};
10986    use frus_widgets::{
10987        column, text, Axis, Button, CellFn, Container, Flex, Kanban, Point, Rect, ReorderAxis,
10988        ReorderGrab, ReorderableList, SingleChildScrollView, Theme, Widget,
10989    };
10990
10991    /// A phone, in logical pixels.
10992    const W: f32 = 392.7;
10993    const H: f32 = 850.9;
10994    /// The height of the labels' middle, under the 56-px bar and the strip's 12 px of padding.
10995    const STRIP_Y: f32 = 88.0;
10996    /// How wide the strip's content is: ten labels, nine gaps, and 24 px at either end.
10997    const STRIP_CONTENT: f32 = 24.0 + 10.0 * 96.0 + 9.0 * 8.0 + 24.0;
10998
10999    #[derive(Clone, Debug, PartialEq)]
11000    enum Board {
11001        Label(usize, usize),
11002        Card(usize, usize, usize, usize),
11003        Delete(usize, usize),
11004        Add(usize),
11005        Row(usize, usize),
11006    }
11007
11008    /// The Kanban screen, in the shape that matters.
11009    struct BoardApp {
11010        labels: Vec<usize>,
11011        cards: Vec<Vec<String>>,
11012        log: Vec<Board>,
11013    }
11014
11015    impl Default for BoardApp {
11016        fn default() -> Self {
11017            let cols: [&[&str]; 3] = [
11018                &["Design API", "Write spec", "Triage bugs"],
11019                &["Build widget"],
11020                &["Kickoff", "Research"],
11021            ];
11022            Self {
11023                labels: (0..10).collect(),
11024                cards: cols
11025                    .iter()
11026                    .map(|c| c.iter().map(|s| s.to_string()).collect())
11027                    .collect(),
11028                log: Vec::new(),
11029            }
11030        }
11031    }
11032
11033    impl Application for BoardApp {
11034        type Message = Board;
11035
11036        fn update(&mut self, message: Board) -> Command<Board> {
11037            self.log.push(message.clone());
11038            match message {
11039                Board::Label(from, to) => {
11040                    let label = self.labels.remove(from);
11041                    self.labels.insert(to, label);
11042                }
11043                Board::Card(from_col, from_pos, to_col, to_pos) => {
11044                    let card = self.cards[from_col].remove(from_pos);
11045                    let to_pos = to_pos.min(self.cards[to_col].len());
11046                    self.cards[to_col].insert(to_pos, card);
11047                }
11048                _ => {}
11049            }
11050            Command::none()
11051        }
11052
11053        fn view(&self, theme: &Theme) -> Box<dyn Widget<Board>> {
11054            let mut strip = ReorderableList::new(Board::Label)
11055                .axis(ReorderAxis::Horizontal)
11056                .grab(ReorderGrab::LongPress)
11057                .gap(8.0);
11058            for &label in &self.labels {
11059                strip = strip.keyed_row(
11060                    label as u64,
11061                    Container::new()
11062                        .width(96.0)
11063                        .padding(12.0)
11064                        .color(theme.surface)
11065                        .child(text(format!("label {label}")).size(14.0)),
11066                );
11067            }
11068            let strip = SingleChildScrollView::new()
11069                .axis(Axis::Horizontal)
11070                .width(W)
11071                .child(
11072                    Container::new()
11073                        .padding_each(12.0, 24.0, 0.0, 24.0)
11074                        .child(strip),
11075                );
11076            let mut board = Kanban::new(Board::Card)
11077                .on_add(Board::Add)
11078                .scrollable_columns();
11079            for (col, title) in ["To do", "Doing", "Done"].iter().enumerate() {
11080                let cards: Vec<CellFn<Board>> = self.cards[col]
11081                    .iter()
11082                    .enumerate()
11083                    .map(|(pos, label)| {
11084                        let label = label.clone();
11085                        Box::new(move || {
11086                            Box::new(
11087                                Flex::row()
11088                                    .child(text(label.clone()).size(14.0))
11089                                    .child(Flex::row().flex(1.0))
11090                                    .child(Button::new("x").on_press(Board::Delete(col, pos))),
11091                            ) as Box<dyn Widget<Board>>
11092                        }) as CellFn<Board>
11093                    })
11094                    .collect();
11095                board = board.column_widgets(*title, cards);
11096            }
11097            let board = SingleChildScrollView::new()
11098                .axis(Axis::Horizontal)
11099                .width(W)
11100                .flex(1.0)
11101                .child(Container::new().padding(24.0).child(board));
11102            Box::new(
11103                Container::new().width(W).height(H).child(
11104                    column![
11105                        Container::<Board>::new().width(W).height(56.0),
11106                        strip,
11107                        board
11108                    ]
11109                    .flex(1.0),
11110                ),
11111            )
11112        }
11113    }
11114
11115    /// Twenty 60-px rows that lift on a hold, in a list taller than the window.
11116    #[derive(Default)]
11117    struct ListApp {
11118        log: Vec<Board>,
11119    }
11120
11121    impl Application for ListApp {
11122        type Message = Board;
11123
11124        fn update(&mut self, message: Board) -> Command<Board> {
11125            self.log.push(message);
11126            Command::none()
11127        }
11128
11129        fn view(&self, _theme: &Theme) -> Box<dyn Widget<Board>> {
11130            let mut list = ReorderableList::new(Board::Row)
11131                .grab(ReorderGrab::LongPress)
11132                .width(W);
11133            for row in 0..20u64 {
11134                list = list.keyed_row(
11135                    row,
11136                    Container::new()
11137                        .height(60.0)
11138                        .child(text(format!("row {row}")).size(14.0)),
11139                );
11140            }
11141            Box::new(
11142                SingleChildScrollView::new()
11143                    .axis(Axis::Vertical)
11144                    .width(W)
11145                    .height(H)
11146                    .child(list),
11147            )
11148        }
11149    }
11150
11151    fn driver<A: Application>(app: A) -> Driver<A> {
11152        let mut driver = Driver::new(app, W, H);
11153        driver.run(0.2);
11154        driver
11155    }
11156
11157    /// A finger held still at `at` until the long press has lifted what is under it.
11158    fn hold<A: Application>(driver: &mut Driver<A>, at: Point) {
11159        driver.press(at);
11160        driver.run(0.5);
11161        assert!(
11162            driver.hold_deadline(),
11163            "the hold lifts what is under {at:?}"
11164        );
11165        driver.run(0.05);
11166    }
11167
11168    /// The finger through `points`, a tenth of a second apart, as the phone's were.
11169    fn carry<A: Application>(driver: &mut Driver<A>, points: impl IntoIterator<Item = Point>) {
11170        for at in points {
11171            driver.move_to(at);
11172            driver.run(0.1);
11173        }
11174    }
11175
11176    /// Frame by frame for `seconds` with the finger still at `finger`: the carried box keeps
11177    /// the place it had under the finger — `offset` from it and `size` — and the region
11178    /// `region` keeps scrolling the way it started. Hands back that region's last offset.
11179    fn held_under_the_finger<A: Application>(
11180        driver: &mut Driver<A>,
11181        seconds: f32,
11182        finger: Point,
11183        offset: (f32, f32),
11184        size: (f32, f32),
11185        region: usize,
11186    ) -> (f32, f32) {
11187        let mut last = driver.offsets()[region].1;
11188        for frame in 0..(seconds * 60.0) as usize {
11189            driver.run(1.0 / 60.0);
11190            let now = driver.offsets()[region].1;
11191            let carried: Rect = driver.carried().unwrap_or_else(|| {
11192                panic!("frame {frame}: nothing is carried any more, the list at {now:?}")
11193            });
11194            assert!(
11195                (carried.x - finger.x - offset.0).abs() < 0.5
11196                    && (carried.y - finger.y - offset.1).abs() < 0.5
11197                    && (carried.width - size.0).abs() < 0.5
11198                    && (carried.height - size.1).abs() < 0.5,
11199                "frame {frame}: carried at {carried:?}, the finger at {finger:?}, the list at {now:?}"
11200            );
11201            assert!(
11202                now.0 >= last.0 - 0.01 && now.1 >= last.1 - 0.01,
11203                "frame {frame}: the list went back, {last:?} then {now:?}"
11204            );
11205            // And it is drawn there: the ghost the finger sees is the box being carried.
11206            let ghost = driver
11207                .ghost()
11208                .unwrap_or_else(|| panic!("frame {frame}: no ghost is drawn"));
11209            assert!(
11210                (ghost.x - carried.x).abs() < 1.0
11211                    && (ghost.y - carried.y).abs() < 1.0
11212                    && (ghost.width - carried.width).abs() < 2.0,
11213                "frame {frame}: the ghost is drawn at {ghost:?}, what is carried is at {carried:?}"
11214            );
11215            last = now;
11216        }
11217        last
11218    }
11219
11220    /// **The reproduction.** A label held and carried past the strip's right edge stays under
11221    /// the finger for as long as it is held there, whole, while the strip scrolls on to its
11222    /// end; and the release drops it where the finger is, past the last label.
11223    #[test]
11224    fn a_label_carried_past_the_strips_edge_stays_under_the_finger() {
11225        let mut driver = driver(BoardApp::default());
11226        hold(&mut driver, Point::new(155.0, STRIP_Y));
11227        let lifted = driver.carried().expect("the label is carried");
11228        assert_eq!(
11229            lifted,
11230            Rect::new(128.0, 68.0, 96.0, 41.0),
11231            "Feature, lifted"
11232        );
11233        let finger = Point::new(370.0, STRIP_Y);
11234        carry(
11235            &mut driver,
11236            [200.0, 260.0, 320.0, finger.x].map(|x| Point::new(x, STRIP_Y)),
11237        );
11238        let strip = held_under_the_finger(
11239            &mut driver,
11240            1.5,
11241            finger,
11242            (lifted.x - 155.0, lifted.y - STRIP_Y),
11243            (96.0, 41.0),
11244            0,
11245        );
11246        assert!(
11247            (strip.0 - (STRIP_CONTENT - W)).abs() < 1.0,
11248            "the strip scrolled on to its end: {strip:?}"
11249        );
11250        driver.release(finger);
11251        driver.run(0.2);
11252        assert_eq!(
11253            driver.app().log,
11254            [Board::Label(1, 9)],
11255            "past the last label, it goes last; no card moves"
11256        );
11257    }
11258
11259    /// **The phone's own gesture**, where the label never reaches an edge: held on Feature,
11260    /// carried along x as the injected moves went, let go over Design's right half.
11261    #[test]
11262    fn a_hold_on_a_label_then_a_carry_along_x_reorders_the_labels_and_moves_no_card() {
11263        let mut driver = driver(BoardApp::default());
11264        hold(&mut driver, Point::new(155.0, STRIP_Y));
11265        carry(
11266            &mut driver,
11267            [167.0, 189.0, 218.0, 247.0, 276.0, 306.0].map(|x| Point::new(x, STRIP_Y)),
11268        );
11269        driver.run(0.4);
11270        assert!(driver.carried().is_some(), "still carried");
11271        driver.release(Point::new(306.0, STRIP_Y));
11272        driver.run(0.2);
11273        assert_eq!(driver.app().log, [Board::Label(1, 2)]);
11274        assert_eq!(driver.app().labels[..4], [0, 2, 1, 3]);
11275        assert_eq!(
11276            driver.app().cards,
11277            BoardApp::default().cards,
11278            "no card moved"
11279        );
11280        assert_eq!(driver.offsets()[0].1, (0.0, 0.0), "nothing scrolled");
11281        assert_eq!(driver.offsets()[1].1, (0.0, 0.0), "the board neither");
11282    }
11283
11284    /// **A card still crosses the board**: Design API, pressed and carried onto the upper
11285    /// half of Build widget, goes to the top of Doing; no label moves.
11286    #[test]
11287    fn a_card_carried_across_the_board_moves_the_card() {
11288        let mut driver = driver(BoardApp::default());
11289        driver.press(Point::new(100.0, 190.0));
11290        carry(
11291            &mut driver,
11292            [130.0, 180.0, 240.0, 300.0].map(|x| Point::new(x, 185.0)),
11293        );
11294        driver.release(Point::new(300.0, 185.0));
11295        driver.run(0.2);
11296        assert_eq!(driver.app().log, [Board::Card(0, 0, 1, 0)]);
11297        assert_eq!(driver.app().labels, (0..10).collect::<Vec<_>>());
11298    }
11299
11300    /// **The board has the same edge.** A card carried to the board's right edge scrolls
11301    /// the board and stays under the finger, although the column it came from scrolls away.
11302    #[test]
11303    fn a_card_carried_to_the_boards_edge_stays_under_the_finger() {
11304        let mut driver = driver(BoardApp::default());
11305        let press = Point::new(100.0, 190.0);
11306        driver.press(press);
11307        let finger = Point::new(385.0, 190.0);
11308        carry(
11309            &mut driver,
11310            [130.0, 200.0, 280.0, 340.0, finger.x].map(|x| Point::new(x, 190.0)),
11311        );
11312        let card = driver.carried().expect("the card is carried");
11313        let board = held_under_the_finger(
11314            &mut driver,
11315            1.0,
11316            finger,
11317            (card.x - finger.x, card.y - finger.y),
11318            (card.width, card.height),
11319            1,
11320        );
11321        assert!(
11322            board.0 > 250.0,
11323            "the board scrolled past the column the card came from: {board:?}"
11324        );
11325    }
11326
11327    /// **A list that runs down has the same edge**: row 0 held and carried to the bottom
11328    /// stays under the finger after its own place has scrolled off the top, and lands low in
11329    /// the list.
11330    #[test]
11331    fn a_row_carried_past_the_bottom_of_a_long_list_stays_under_the_finger() {
11332        let mut driver = driver(ListApp::default());
11333        hold(&mut driver, Point::new(100.0, 30.0));
11334        let lifted = driver.carried().expect("the row is carried");
11335        let finger = Point::new(100.0, 840.0);
11336        carry(
11337            &mut driver,
11338            [200.0, 400.0, 600.0, 800.0, finger.y].map(|y| Point::new(100.0, y)),
11339        );
11340        let list = held_under_the_finger(
11341            &mut driver,
11342            1.5,
11343            finger,
11344            (lifted.x - 100.0, lifted.y - 30.0),
11345            (lifted.width, lifted.height),
11346            0,
11347        );
11348        assert!(
11349            list.1 > 200.0,
11350            "the list scrolled past the row's own place: {list:?}"
11351        );
11352        driver.release(finger);
11353        driver.run(0.2);
11354        assert!(
11355            matches!(driver.app().log[..], [Board::Row(0, to)] if to > 15),
11356            "row 0 lands near the end: {:?}",
11357            driver.app().log
11358        );
11359    }
11360}
11361
11362/// **A press a scroll took is let go of** (milestone 534).
11363///
11364/// The shell's own types need a window and an event loop, so these drive a real frame
11365/// through the pieces `App::pointer` is made of, in the order it calls them: the hover, the
11366/// press where the pointer lands, the gesture the drag handler turns the press into, and
11367/// `settle_press` after every event. What is painted is then compared with a runtime
11368/// nobody pressed.
11369#[cfg(test)]
11370mod press_tests {
11371    use super::{gesture_was_a_tap, press_at, settle_press, Drag, Point, Runtime, Theme};
11372    use crate::gesture::PointerKind;
11373    use crate::hover::Hover;
11374    use frus_widgets::{
11375        build_ui, Axis, Button, Container, Flex, Primitive, SegmentedButton, SingleChildScrollView,
11376        Size, Ui, Widget,
11377    };
11378
11379    const SCREEN: Size = Size::new(300.0, 400.0);
11380    const DT: f32 = 1.0 / 60.0;
11381
11382    /// The phone's screen: a strip of segments too wide for it, in a horizontal scroll.
11383    fn filters() -> Box<dyn Widget<usize>> {
11384        let mut segments = SegmentedButton::new(0, |i| i);
11385        for label in ["All", "Active", "Done", "Archived", "Starred", "Shared"] {
11386            segments = segments.segment(label);
11387        }
11388        Box::new(
11389            Flex::column().width(300.0).height(400.0).child(
11390                SingleChildScrollView::new()
11391                    .axis(Axis::Horizontal)
11392                    .width(300.0)
11393                    .height(80.0)
11394                    .child(segments),
11395            ),
11396        )
11397    }
11398
11399    /// A plain button at the top of a page that scrolls down.
11400    fn page() -> Box<dyn Widget<usize>> {
11401        Box::new(
11402            Flex::column().width(300.0).height(400.0).child(
11403                SingleChildScrollView::new()
11404                    .width(300.0)
11405                    .height(400.0)
11406                    .child(
11407                        Flex::column()
11408                            .child(Button::new("Go").on_press(7usize))
11409                            .child(Container::new().width(300.0).height(1200.0)),
11410                    ),
11411            ),
11412        )
11413    }
11414
11415    fn frame(view: &dyn Widget<usize>, runtime: &Runtime) -> Ui<usize> {
11416        build_ui(view, SCREEN, runtime, &Theme::default())
11417    }
11418
11419    /// Frames go by with nothing else happening, as a phone left alone for `seconds`.
11420    fn idle(runtime: &mut Runtime, seconds: f32) {
11421        for _ in 0..(seconds / DT) as usize {
11422            runtime.advance(DT);
11423            runtime.advance_ink(DT);
11424        }
11425    }
11426
11427    /// What the screen shows.
11428    fn painted(view: &dyn Widget<usize>, runtime: &Runtime) -> Vec<Primitive> {
11429        frame(view, runtime).scene().primitives().to_vec()
11430    }
11431
11432    /// The same screen, scrolled the same, that no pointer has touched.
11433    fn untouched(view: &dyn Widget<usize>, runtime: &Runtime) -> Vec<Primitive> {
11434        let mut quiet = Runtime::default();
11435        quiet.scroll.clone_from(&runtime.scroll);
11436        quiet.scroll_target.clone_from(&runtime.scroll_target);
11437        idle(&mut quiet, 0.5);
11438        painted(view, &quiet)
11439    }
11440
11441    /// A pointer on a screen, routed as the shell routes one.
11442    struct Pointer<'a> {
11443        view: &'a dyn Widget<usize>,
11444        runtime: Runtime,
11445        hover: Hover,
11446        drag: Option<Drag>,
11447        touch: bool,
11448        at: Point,
11449    }
11450
11451    impl<'a> Pointer<'a> {
11452        fn new(view: &'a dyn Widget<usize>, touch: bool) -> Self {
11453            Self {
11454                view,
11455                runtime: Runtime::default(),
11456                hover: Hover::default(),
11457                drag: None,
11458                touch,
11459                at: Point::new(0.0, 0.0),
11460            }
11461        }
11462
11463        /// One event: `route` stands for what the shell's handler makes of it.
11464        fn event(&mut self, kind: PointerKind, at: Point, route: impl FnOnce(&mut Self)) {
11465            self.at = at;
11466            self.hover.event(kind, at, self.touch);
11467            let could_tap = gesture_was_a_tap(self.drag.as_ref());
11468            if kind == PointerKind::Down {
11469                let ui = frame(self.view, &self.runtime);
11470                self.runtime.input.hovered = self.hover.target(&ui);
11471                press_at(&mut self.runtime, Some(&ui), at);
11472            }
11473            route(self);
11474            settle_press(
11475                &mut self.runtime.input,
11476                kind,
11477                could_tap,
11478                self.drag.as_ref(),
11479                self.touch,
11480            );
11481            if matches!(kind, PointerKind::Up | PointerKind::Cancel) {
11482                let ui = frame(self.view, &self.runtime);
11483                self.runtime.input.hovered = self.hover.target(&ui);
11484            }
11485        }
11486
11487        /// A finger lands where a touch scroll takes it, as `pointer_down` prepares one.
11488        fn land(&mut self, at: Point) {
11489            self.event(PointerKind::Down, at, |p| {
11490                if !p.touch {
11491                    return;
11492                }
11493                let ui = frame(p.view, &p.runtime);
11494                let area = ui
11495                    .scroll_chain(at)
11496                    .find(|area| area.accepts_user_offset((0.0, 0.0)))
11497                    .expect("the region under the finger has somewhere to go");
11498                p.drag = Some(Drag::Scroll {
11499                    id: area.id,
11500                    last: at,
11501                    moved: false,
11502                    carried: (0.0, 0.0),
11503                    dismiss: None,
11504                    axis: None,
11505                    pan: None,
11506                });
11507            });
11508        }
11509
11510        /// The pointer moves by `by`; past the slop a touch scroll moves its content, as the
11511        /// drag handler moves it.
11512        fn slide(&mut self, by: (f32, f32)) {
11513            let to = Point::new(self.at.x + by.0, self.at.y + by.1);
11514            self.event(PointerKind::Move, to, |p| {
11515                let Some(Drag::Scroll {
11516                    id,
11517                    moved,
11518                    axis,
11519                    last,
11520                    ..
11521                }) = p.drag.as_mut()
11522                else {
11523                    return;
11524                };
11525                let (dx, dy) = (to.x - last.x, to.y - last.y);
11526                if dx.hypot(dy) <= super::TOUCH_SLOP && !*moved {
11527                    return;
11528                }
11529                *moved = true;
11530                *axis = Some(dy.abs() >= dx.abs());
11531                *last = to;
11532                let offset = p.runtime.scroll.get(id).copied().unwrap_or((0.0, 0.0));
11533                let moved_to = (offset.0 - by.0, offset.1 - by.1);
11534                p.runtime.scroll.insert(*id, moved_to);
11535                p.runtime.scroll_target.insert(*id, moved_to);
11536            });
11537        }
11538
11539        /// The pointer lifts; the release takes the gesture, as `pointer_up` does.
11540        fn lift(&mut self) -> Option<Drag> {
11541            let mut ended = None;
11542            self.event(PointerKind::Up, self.at, |p| ended = p.drag.take());
11543            ended
11544        }
11545    }
11546
11547    /// **Seen on a phone**: a swipe that started on "Done" scrolled the strip, selected
11548    /// nothing — and left "Done" grey, still there two seconds later. Afterwards the screen
11549    /// is the one nobody touched.
11550    #[test]
11551    fn a_segment_a_swipe_started_on_is_not_left_pressed() {
11552        let view = filters();
11553        let mut finger = Pointer::new(view.as_ref(), true);
11554        let done = Point::new(200.0, 16.0);
11555        finger.land(done);
11556        let segment = finger
11557            .runtime
11558            .input
11559            .pressed
11560            .expect("the segment is pressed");
11561        assert!(
11562            finger.runtime.ink.contains_key(&segment),
11563            "and it splashed: a segment takes ink"
11564        );
11565        assert!(
11566            frame(view.as_ref(), &finger.runtime)
11567                .scroll_chain(done)
11568                .any(|area| area.max_x > 0.0),
11569            "the strip is wider than the screen, as the phone's was"
11570        );
11571        for _ in 0..19 {
11572            finger.slide((-10.0, 0.0));
11573        }
11574        let ended = finger.lift();
11575        assert!(
11576            !gesture_was_a_tap(ended.as_ref()),
11577            "a gesture that scrolled clicks nothing"
11578        );
11579        idle(&mut finger.runtime, 2.0);
11580        // What the phone showed first, then why.
11581        let (shown, quiet) = (
11582            painted(view.as_ref(), &finger.runtime),
11583            untouched(view.as_ref(), &finger.runtime),
11584        );
11585        assert_eq!(
11586            shown.len(),
11587            quiet.len(),
11588            "two seconds on, the screen paints what nobody touched, and no layer more"
11589        );
11590        assert_eq!(shown, quiet, "nothing is highlighted");
11591        assert!(
11592            finger.runtime.ink.is_empty(),
11593            "no ink waits for a finger that has gone"
11594        );
11595        assert_eq!(finger.runtime.input.pressed, None, "the press went with it");
11596        assert_eq!(
11597            finger.runtime.input.hovered, None,
11598            "and a lifted finger hovers nothing"
11599        );
11600    }
11601
11602    /// The reference cancels the press **when the scroll wins**, not when the finger lifts:
11603    /// a strip being scrolled does not carry a pressed segment along with it.
11604    #[test]
11605    fn the_press_ends_when_the_scroll_takes_it() {
11606        let view = filters();
11607        let mut finger = Pointer::new(view.as_ref(), true);
11608        finger.land(Point::new(200.0, 16.0));
11609        finger.slide((-4.0, 0.0));
11610        assert!(
11611            finger.runtime.input.pressed.is_some(),
11612            "under the slop it may still be a tap"
11613        );
11614        finger.slide((-40.0, 0.0));
11615        assert_eq!(
11616            finger.runtime.input.pressed, None,
11617            "past it, the scroll has it"
11618        );
11619        assert_eq!(
11620            finger.runtime.input.hovered, None,
11621            "and the finger's hover with it"
11622        );
11623        idle(&mut finger.runtime, 0.5);
11624        assert_eq!(
11625            painted(view.as_ref(), &finger.runtime),
11626            untouched(view.as_ref(), &finger.runtime),
11627            "the finger is still down, and nothing under it is highlighted"
11628        );
11629    }
11630
11631    /// The same down a page: a plain button a scroll started on.
11632    #[test]
11633    fn a_button_a_page_scrolled_from_is_not_left_pressed() {
11634        let view = page();
11635        let mut finger = Pointer::new(view.as_ref(), true);
11636        let go = Point::new(20.0, 15.0);
11637        finger.land(go);
11638        let button = finger.runtime.input.pressed.expect("the button is pressed");
11639        assert_eq!(
11640            frame(view.as_ref(), &finger.runtime).msg_for(button),
11641            Some(7),
11642            "the press is on the button"
11643        );
11644        for _ in 0..10 {
11645            finger.slide((0.0, -30.0));
11646        }
11647        assert!(!gesture_was_a_tap(finger.lift().as_ref()));
11648        idle(&mut finger.runtime, 2.0);
11649        assert_eq!(finger.runtime.input.pressed, None);
11650        assert!(finger.runtime.ink.is_empty());
11651        assert_eq!(
11652            painted(view.as_ref(), &finger.runtime),
11653            untouched(view.as_ref(), &finger.runtime),
11654        );
11655    }
11656
11657    /// **A tap is still a tap**: a finger that stays inside the slop keeps its press to the
11658    /// release, which is what clicks; only then is it let go of.
11659    #[test]
11660    fn a_tap_still_presses_and_clicks() {
11661        let view = filters();
11662        let mut finger = Pointer::new(view.as_ref(), true);
11663        let segment = Point::new(200.0, 16.0);
11664        finger.land(segment);
11665        finger.slide((3.0, 2.0));
11666        let pressed = finger.runtime.input.pressed;
11667        assert!(pressed.is_some(), "a wobble under the slop takes nothing");
11668        assert_eq!(finger.runtime.input.hovered, pressed, "and the press shows");
11669        // What `pointer_up` reads, before the release lets it go.
11670        let ui = frame(view.as_ref(), &finger.runtime);
11671        assert_eq!(
11672            ui.hit(segment),
11673            pressed,
11674            "released on the widget it pressed"
11675        );
11676        assert!(
11677            pressed.and_then(|id| ui.msg_for(id)).is_some(),
11678            "and the segment it pressed has a choice to make"
11679        );
11680        let ended = finger.lift();
11681        assert!(
11682            gesture_was_a_tap(ended.as_ref()),
11683            "a scroll that never moved"
11684        );
11685        assert_eq!(
11686            finger.runtime.input.pressed, None,
11687            "let go of once released"
11688        );
11689    }
11690
11691    /// **A mouse is as it was**: it has no touch scroll, keeps its press while it moves, and
11692    /// still hovers where it is after its button comes up.
11693    #[test]
11694    fn a_mouse_keeps_its_press_while_it_moves_and_its_hover_after() {
11695        let view = filters();
11696        let mut mouse = Pointer::new(view.as_ref(), false);
11697        let done = Point::new(200.0, 16.0);
11698        mouse.land(done);
11699        assert!(mouse.drag.is_none(), "a mouse does not drag a scroll");
11700        let pressed = mouse.runtime.input.pressed.expect("pressed");
11701        mouse.slide((-60.0, 0.0));
11702        assert_eq!(mouse.runtime.input.pressed, Some(pressed), "still held");
11703        mouse.lift();
11704        assert_eq!(mouse.runtime.input.pressed, None);
11705        assert!(
11706            mouse.runtime.input.hovered.is_some(),
11707            "a released mouse is still over the strip"
11708        );
11709    }
11710
11711    /// A gesture a **mouse** moves past its slop takes the press as a finger's does — but
11712    /// not the hover, since the mouse is still there.
11713    #[test]
11714    fn a_gesture_takes_a_mouse_s_press_and_leaves_its_hover() {
11715        let id = frus_widgets::WidgetId::from_u64(3);
11716        let sheet = |moved| Drag::Sheet {
11717            id: frus_widgets::WidgetId::from_u64(9),
11718            last: Point::new(0.0, 0.0),
11719            moved,
11720            available: 800.0,
11721        };
11722        for touch in [false, true] {
11723            let mut input = frus_widgets::InputState {
11724                pressed: Some(id),
11725                hovered: Some(id),
11726                ..Default::default()
11727            };
11728            assert!(!settle_press(
11729                &mut input,
11730                PointerKind::Move,
11731                true,
11732                Some(&sheet(false)),
11733                touch
11734            ));
11735            assert_eq!(input.pressed, Some(id), "not yet past the slop");
11736            assert!(settle_press(
11737                &mut input,
11738                PointerKind::Move,
11739                true,
11740                Some(&sheet(true)),
11741                touch
11742            ));
11743            assert_eq!(input.pressed, None, "touch {touch}");
11744            assert_eq!(input.hovered, (!touch).then_some(id), "touch {touch}");
11745        }
11746        // A gesture that was never a tap — a slider, a selection — had no press to share,
11747        // and moving it lets go of nothing.
11748        let mut input = frus_widgets::InputState {
11749            pressed: Some(id),
11750            ..Default::default()
11751        };
11752        let select = Drag::TextSelect {
11753            id,
11754            rect: super::Rect::new(0.0, 0.0, 10.0, 10.0),
11755        };
11756        assert!(!settle_press(
11757            &mut input,
11758            PointerKind::Move,
11759            false,
11760            Some(&select),
11761            true
11762        ));
11763        assert_eq!(input.pressed, Some(id));
11764    }
11765}
11766
11767/// The mouse, the words and the selection a [`SelectionArea`](frus_widgets::SelectionArea) holds,
11768/// driven through the shell as a person drives it.
11769#[cfg(test)]
11770mod selection_area_tests {
11771    use super::testing::Driver;
11772    use crate::{Application, Command};
11773    use frus_widgets::{Button, Container, Flex, Point, SelectionArea, Text, Theme, Widget};
11774
11775    const W: f32 = 400.0;
11776    const H: f32 = 300.0;
11777
11778    /// A line outside the area, then an area of three lines with a button between two of them.
11779    #[derive(Default)]
11780    struct Doc {
11781        /// What the application says about the area's bar.
11782        bar: Bar,
11783        /// How many times the bar's own item was pressed.
11784        shared: std::rc::Rc<std::cell::Cell<u32>>,
11785        /// The theme's platform: Android unless a test says otherwise, because these are a
11786        /// finger's tests and Android's bar is the pill (milestone 618).
11787        platform: Option<frus_widgets::TargetPlatform>,
11788    }
11789
11790    /// The application's say over the area's bar (milestone 578).
11791    #[derive(Default, Clone, Copy)]
11792    enum Bar {
11793        /// Nothing: Copy and Select all.
11794        #[default]
11795        Default,
11796        /// Its own item, Share, after Copy, and no Select all.
11797        Share,
11798        /// No bar at all.
11799        Empty,
11800    }
11801
11802    impl Application for Doc {
11803        type Message = ();
11804
11805        fn update(&mut self, _message: ()) -> Command<()> {
11806            self.shared.set(self.shared.get() + 1);
11807            Command::none()
11808        }
11809
11810        fn theme(&self) -> Theme {
11811            Theme::default().with_platform(
11812                self.platform
11813                    .unwrap_or(frus_widgets::TargetPlatform::Android),
11814            )
11815        }
11816
11817        fn view(&self, _theme: &Theme) -> Box<dyn Widget<()>> {
11818            let area = SelectionArea::around(
11819                Flex::column()
11820                    .child(Text::new("first line of words").no_wrap())
11821                    .child(Text::new("second line").no_wrap())
11822                    .child(Button::new("Press").on_press(()))
11823                    .child(Text::new("third line ends here").no_wrap()),
11824            );
11825            let area = match self.bar {
11826                Bar::Default => area,
11827                Bar::Share => area.selection_toolbar(|_, items| {
11828                    let mut items: Vec<_> = items
11829                        .into_iter()
11830                        .filter(|item| item.label() == "Copy")
11831                        .collect();
11832                    items.push(frus_widgets::ToolbarItem::custom("Share", ()));
11833                    items
11834                }),
11835                Bar::Empty => area.selection_toolbar(|_, _| Vec::new()),
11836            };
11837            Box::new(
11838                Container::new().width(W).height(H).child(
11839                    Flex::column()
11840                        .child(Text::new("Outside the area").no_wrap())
11841                        .child(area),
11842                ),
11843            )
11844        }
11845    }
11846
11847    /// A row that answers a tap, with words in it, beside words that answer nothing.
11848    struct Tappable;
11849
11850    impl Application for Tappable {
11851        type Message = ();
11852
11853        fn update(&mut self, _message: ()) -> Command<()> {
11854            Command::none()
11855        }
11856
11857        fn view(&self, _theme: &Theme) -> Box<dyn Widget<()>> {
11858            Box::new(
11859                Container::new()
11860                    .width(W)
11861                    .height(H)
11862                    .child(SelectionArea::around(
11863                        Flex::column()
11864                            .child(
11865                                Container::new()
11866                                    .on_click(())
11867                                    .child(Text::new("tap this row").no_wrap()),
11868                            )
11869                            .child(Text::new("plain words here").no_wrap()),
11870                    )),
11871            )
11872        }
11873    }
11874
11875    /// **What answers a tap keeps it, even when it is not a focus stop**: words inside a row that
11876    /// takes a tap begin no selection, and words beside it do. (A button is a focus stop and is
11877    /// kept by that; a row that is only clickable is kept by nothing but this.)
11878    #[test]
11879    fn a_row_that_takes_a_tap_keeps_its_press() {
11880        let mut d = Driver::new(Tappable, W, H);
11881        d.run(0.2);
11882        let find = |d: &Driver<Tappable>, label: &str| {
11883            let (_, rect) = d
11884                .texts()
11885                .into_iter()
11886                .find(|(text, _)| text == label)
11887                .expect("on screen");
11888            Point::new(rect.x + 20.0, rect.y + 8.0)
11889        };
11890        let (tap, plain) = (find(&d, "tap this row"), find(&d, "plain words here"));
11891        let end = Point::new(plain.x + 60.0, plain.y);
11892        d.press_mouse(tap);
11893        d.run(0.05);
11894        d.move_mouse(end);
11895        d.run(0.05);
11896        d.release_mouse(end);
11897        d.run(0.05);
11898        assert_eq!(d.area_selection(), None, "the row's press stayed the row's");
11899
11900        d.press_mouse(plain);
11901        d.run(0.05);
11902        d.move_mouse(end);
11903        d.run(0.05);
11904        d.release_mouse(end);
11905        d.run(0.05);
11906        assert!(d.area_selection().is_some(), "plain words still select");
11907    }
11908
11909    fn driver() -> Driver<Doc> {
11910        driver_with(Bar::Default)
11911    }
11912
11913    fn driver_with(bar: Bar) -> Driver<Doc> {
11914        driver_on(bar, None)
11915    }
11916
11917    fn driver_on(bar: Bar, platform: Option<frus_widgets::TargetPlatform>) -> Driver<Doc> {
11918        let doc = Doc {
11919            bar,
11920            platform,
11921            ..Doc::default()
11922        };
11923        let mut driver = Driver::new(doc, W, H);
11924        driver.run(0.2);
11925        driver
11926    }
11927
11928    /// Where the word `label` starts, and a point on its line a fraction `across` of the way
11929    /// along it.
11930    fn on(driver: &Driver<Doc>, label: &str, across: f32) -> Point {
11931        let (_, rect) = driver
11932            .texts()
11933            .into_iter()
11934            .find(|(text, _)| text == label)
11935            .unwrap_or_else(|| panic!("{label:?} is on screen"));
11936        // A text primitive is a point at its top-left; the line is about 19 px tall and the
11937        // words a few px per character.
11938        Point::new(rect.x + across, rect.y + 8.0)
11939    }
11940
11941    fn drag(driver: &mut Driver<Doc>, from: Point, to: Point) {
11942        driver.press_mouse(from);
11943        driver.run(0.05);
11944        driver.move_mouse(to);
11945        driver.run(0.05);
11946        driver.release_mouse(to);
11947        driver.run(0.05);
11948    }
11949
11950    /// **A drag across texts selects across them**: the tail of the first, the whole of the one
11951    /// between, the head of the last — and what the button between them says is not among the
11952    /// words, because it is not a text of the area's.
11953    #[test]
11954    fn a_drag_selects_across_the_texts_of_an_area() {
11955        let mut d = driver();
11956        let (from, to) = (
11957            on(&d, "first line of words", 60.0),
11958            on(&d, "third line ends here", 40.0),
11959        );
11960        drag(&mut d, from, to);
11961        let copied = d.area_selection().expect("something is selected");
11962        let lines: Vec<&str> = copied.lines().collect();
11963        assert_eq!(lines.len(), 3, "one line per text: {copied:?}");
11964        assert!(
11965            !lines[0].is_empty() && "first line of words".ends_with(lines[0]),
11966            "the tail of the first: {:?}",
11967            lines[0]
11968        );
11969        assert!(lines[0].len() < "first line of words".len());
11970        assert_eq!(lines[1], "second line", "the whole of the middle one");
11971        assert!(
11972            !lines[2].is_empty() && "third line ends here".starts_with(lines[2]),
11973            "the head of the last: {:?}",
11974            lines[2]
11975        );
11976        assert!(lines[2].len() < "third line ends here".len());
11977        assert!(!copied.contains("Press"), "a button's label is not text");
11978    }
11979
11980    /// **Dragged upward it is the same words.**
11981    #[test]
11982    fn dragging_upward_selects_the_same_words() {
11983        let (down, up) = {
11984            let mut d = driver();
11985            let (a, b) = (
11986                on(&d, "first line of words", 60.0),
11987                on(&d, "third line ends here", 40.0),
11988            );
11989            drag(&mut d, a, b);
11990            let down = d.area_selection();
11991            drag(&mut d, b, a);
11992            (down, d.area_selection())
11993        };
11994        assert!(down.is_some());
11995        assert_eq!(down, up);
11996    }
11997
11998    /// **A drag that leaves the words still has an end**: past the right edge of the last line,
11999    /// the selection ends with it, and below everything it ends at the last text.
12000    #[test]
12001    fn a_drag_past_the_words_ends_at_the_last_of_them() {
12002        let mut d = driver();
12003        let from = on(&d, "second line", 5.0);
12004        drag(&mut d, from, Point::new(W - 2.0, H - 2.0));
12005        let copied = d.area_selection().expect("a selection");
12006        assert!(copied.ends_with("third line ends here"), "{copied:?}");
12007    }
12008
12009    /// **Words outside the area are not part of it**: a drag over them selects nothing, and
12010    /// one that starts inside never picks them up.
12011    #[test]
12012    fn words_outside_the_area_are_left_alone() {
12013        let mut d = driver();
12014        let outside = on(&d, "Outside the area", 30.0);
12015        let end = on(&d, "third line ends here", 40.0);
12016        drag(&mut d, outside, end);
12017        assert_eq!(d.area_selection(), None, "the press began outside");
12018
12019        let inside = on(&d, "first line of words", 30.0);
12020        drag(&mut d, inside, Point::new(20.0, 2.0));
12021        let copied = d.area_selection().expect("a selection");
12022        assert!(!copied.contains("Outside"), "{copied:?}");
12023    }
12024
12025    /// **A press on a button is the button's**: no selection begins under it.
12026    #[test]
12027    fn a_button_keeps_its_press() {
12028        let mut d = driver();
12029        let button = on(&d, "Press", 10.0);
12030        let end = on(&d, "third line ends here", 40.0);
12031        drag(&mut d, button, end);
12032        assert_eq!(d.area_selection(), None);
12033    }
12034
12035    /// **A press without a drag puts the selection away**, in the area or out of it.
12036    #[test]
12037    fn a_click_puts_the_selection_away() {
12038        let mut d = driver();
12039        let (from, to) = (
12040            on(&d, "first line of words", 60.0),
12041            on(&d, "third line ends here", 40.0),
12042        );
12043        drag(&mut d, from, to);
12044        assert!(d.area_selection().is_some());
12045        let elsewhere = on(&d, "second line", 30.0);
12046        drag(&mut d, elsewhere, elsewhere);
12047        assert_eq!(d.area_selection(), None, "a click in the area");
12048
12049        drag(&mut d, from, to);
12050        assert!(d.area_selection().is_some());
12051        let outside = on(&d, "Outside the area", 30.0);
12052        drag(&mut d, outside, outside);
12053        assert_eq!(d.area_selection(), None, "a click outside it");
12054    }
12055
12056    /// **A double click selects the word.**
12057    #[test]
12058    fn a_double_click_selects_the_word() {
12059        let mut d = driver();
12060        // "second" is the first word of the line: a few px in.
12061        let at = on(&d, "second line", 12.0);
12062        d.press_mouse(at);
12063        d.release_mouse(at);
12064        d.run(0.05);
12065        d.press_mouse(at);
12066        d.release_mouse(at);
12067        d.run(0.05);
12068        assert_eq!(d.area_selection().as_deref(), Some("second"));
12069    }
12070
12071    /// **Select all takes every text of the area** — and none of what is outside it.
12072    #[test]
12073    fn select_all_takes_the_whole_area() {
12074        let mut d = driver();
12075        let at = on(&d, "second line", 12.0);
12076        let end = on(&d, "second line", 40.0);
12077        drag(&mut d, at, end);
12078        assert!(d.area_selection().is_some());
12079        d.area_select_all();
12080        assert_eq!(
12081            d.area_selection().as_deref(),
12082            Some("first line of words\nsecond line\nthird line ends here")
12083        );
12084    }
12085
12086    /// **A finger scrolls, and does not select**: a touch drag over the words leaves them be.
12087    #[test]
12088    fn a_finger_does_not_select() {
12089        let mut d = driver();
12090        let (from, to) = (
12091            on(&d, "first line of words", 60.0),
12092            on(&d, "third line ends here", 40.0),
12093        );
12094        d.press(from);
12095        d.run(0.05);
12096        d.move_to(to);
12097        d.run(0.05);
12098        d.release(to);
12099        d.run(0.05);
12100        assert_eq!(d.area_selection(), None);
12101    }
12102
12103    /// A finger held still on `at` until the long press fires, then lifted.
12104    fn hold(d: &mut Driver<Doc>, at: Point) -> bool {
12105        d.press(at);
12106        d.run(0.1);
12107        let fired = d.hold_deadline();
12108        d.run(0.05);
12109        d.release(at);
12110        d.run(0.05);
12111        fired
12112    }
12113
12114    /// Where the bar's button reading `label` is, if the bar shows one.
12115    fn bar_button(d: &Driver<Doc>, label: &str) -> Option<Point> {
12116        let (ui, _) = d.frame_parts()?;
12117        d.texts()
12118            .into_iter()
12119            .filter(|(text, _)| text == label)
12120            .map(|(_, rect)| Point::new(rect.x + 4.0, rect.y + 6.0))
12121            .find(|at| ui.toolbar_contains(*at))
12122    }
12123
12124    /// **A finger held on words selects the word under it and opens the bar** — Copy and
12125    /// Select all — over it (milestone 576). A phone has no mouse to drag with.
12126    #[test]
12127    fn a_hold_selects_the_word_and_opens_the_bar() {
12128        let mut d = driver();
12129        let at = on(&d, "second line", 12.0);
12130        assert!(hold(&mut d, at), "the hold fired");
12131        assert_eq!(d.area_selection().as_deref(), Some("second"));
12132        assert!(bar_button(&d, "Copy").is_some(), "Copy is on the bar");
12133        assert!(bar_button(&d, "Select all").is_some(), "and Select all");
12134    }
12135
12136    /// **On a desktop theme the bar is the menu, and its rows act on the area as the pill's
12137    /// buttons do** (milestone 618).
12138    #[test]
12139    fn the_desktop_menus_rows_act_on_the_area() {
12140        let mut d = driver_on(Bar::Default, Some(frus_widgets::TargetPlatform::Linux));
12141        let at = on(&d, "second line", 12.0);
12142        hold(&mut d, at);
12143        let select_all = bar_button(&d, "Select all").expect("Select all");
12144        // The menu hangs from the selection's top, over the handles under it: the press is
12145        // the menu's, not a handle's.
12146        d.press(select_all);
12147        d.release(select_all);
12148        d.run(0.05);
12149        assert_eq!(
12150            d.area_selection().as_deref(),
12151            Some("first line of words\nsecond line\nthird line ends here")
12152        );
12153    }
12154
12155    /// **The bar's Select all takes the whole area and keeps the bar**, which no longer offers
12156    /// Select all; its Copy puts the selection away, as a platform's own does.
12157    #[test]
12158    fn the_bars_buttons_act_on_the_area() {
12159        let mut d = driver();
12160        let at = on(&d, "second line", 12.0);
12161        hold(&mut d, at);
12162        let select_all = bar_button(&d, "Select all").expect("Select all");
12163        d.press(select_all);
12164        d.release(select_all);
12165        d.run(0.05);
12166        assert_eq!(
12167            d.area_selection().as_deref(),
12168            Some("first line of words\nsecond line\nthird line ends here")
12169        );
12170        assert!(
12171            bar_button(&d, "Select all").is_none(),
12172            "nothing left to select"
12173        );
12174        let copy = bar_button(&d, "Copy").expect("Copy stays");
12175        d.press(copy);
12176        d.release(copy);
12177        d.run(0.05);
12178        assert_eq!(d.area_selection(), None, "copied, and put away");
12179        assert!(bar_button(&d, "Copy").is_none(), "the bar with it");
12180    }
12181
12182    /// **A hold on a button is the button's**, and a finger that moves is a scroll, not a hold.
12183    #[test]
12184    fn a_hold_on_a_button_selects_nothing() {
12185        let mut d = driver();
12186        let button = on(&d, "Press", 10.0);
12187        hold(&mut d, button);
12188        assert_eq!(d.area_selection(), None);
12189    }
12190
12191    /// **Escape, and a tap elsewhere, put the selection and its bar away.**
12192    #[test]
12193    fn escape_and_a_tap_put_the_bar_away() {
12194        let mut d = driver();
12195        let at = on(&d, "second line", 12.0);
12196        hold(&mut d, at);
12197        assert!(d.area_selection().is_some());
12198        d.escape();
12199        d.run(0.05);
12200        assert_eq!(d.area_selection(), None);
12201        assert!(bar_button(&d, "Copy").is_none());
12202
12203        hold(&mut d, at);
12204        assert!(d.area_selection().is_some());
12205        let elsewhere = on(&d, "Outside the area", 20.0);
12206        d.press(elsewhere);
12207        d.release(elsewhere);
12208        d.run(0.05);
12209        assert_eq!(d.area_selection(), None);
12210        assert!(bar_button(&d, "Copy").is_none());
12211    }
12212
12213    /// **A selection made with a mouse has no bar**: the bar is for a finger, which has no
12214    /// Ctrl+C.
12215    #[test]
12216    fn a_mouse_selection_has_no_bar() {
12217        let mut d = driver();
12218        let (from, to) = (
12219            on(&d, "first line of words", 60.0),
12220            on(&d, "third line ends here", 40.0),
12221        );
12222        drag(&mut d, from, to);
12223        assert!(d.area_selection().is_some());
12224        assert!(bar_button(&d, "Copy").is_none());
12225    }
12226
12227    /// **The application says what the bar holds** (milestone 578): its own item beside Copy,
12228    /// and no Select all — and pressing its item sends the application's message.
12229    #[test]
12230    fn the_application_says_what_the_bar_holds() {
12231        let mut d = driver_with(Bar::Share);
12232        let at = on(&d, "second line", 12.0);
12233        assert!(hold(&mut d, at));
12234        assert_eq!(d.area_selection().as_deref(), Some("second"));
12235        assert!(bar_button(&d, "Copy").is_some(), "Copy was kept");
12236        assert!(
12237            bar_button(&d, "Select all").is_none(),
12238            "Select all was taken off"
12239        );
12240        let share = bar_button(&d, "Share").expect("the application's own item");
12241        let shared = d.app().shared.clone();
12242        d.press(share);
12243        d.release(share);
12244        d.run(0.05);
12245        assert_eq!(shared.get(), 1, "its message was sent");
12246    }
12247
12248    /// **An empty list shows no bar**, and the selection is still made.
12249    #[test]
12250    fn an_empty_list_shows_no_bar() {
12251        let mut d = driver_with(Bar::Empty);
12252        let at = on(&d, "second line", 12.0);
12253        assert!(hold(&mut d, at));
12254        assert_eq!(d.area_selection().as_deref(), Some("second"));
12255        assert!(bar_button(&d, "Copy").is_none());
12256        assert!(d
12257            .frame_parts()
12258            .is_some_and(|(ui, _)| !ui.toolbar_contains(at)));
12259    }
12260
12261    /// A handle's middle — where a finger takes it.
12262    fn middle(rect: frus_widgets::Rect) -> Point {
12263        Point::new(rect.x + rect.width / 2.0, rect.y + rect.height / 2.0)
12264    }
12265
12266    /// A finger taking a handle at `from` and carrying it to `to`, a few steps at a time.
12267    fn carry_handle(d: &mut Driver<Doc>, from: Point, to: Point) {
12268        d.press(from);
12269        d.run(0.05);
12270        for step in 1..=4 {
12271            let f = step as f32 / 4.0;
12272            d.move_to(Point::new(
12273                from.x + (to.x - from.x) * f,
12274                from.y + (to.y - from.y) * f,
12275            ));
12276            d.run(0.05);
12277        }
12278        d.release(to);
12279        d.run(0.05);
12280    }
12281
12282    /// **A touch selection has two handles**, hanging below the ends of the word, the start one
12283    /// to the left of the end one — and a mouse selection has none.
12284    #[test]
12285    fn a_touch_selection_has_handles_and_a_mouse_one_does_not() {
12286        let mut d = driver();
12287        let paths = |d: &Driver<Doc>| {
12288            let (ui, _) = d.frame_parts().expect("a frame");
12289            ui.scene()
12290                .primitives()
12291                .iter()
12292                .filter(|p| matches!(p, frus_widgets::Primitive::Path { .. }))
12293                .count()
12294        };
12295        let before = paths(&d);
12296        let at = on(&d, "second line", 12.0);
12297        hold(&mut d, at);
12298        assert_eq!(paths(&d), before + 2, "the two handles are painted");
12299        let [start, end] = d.area_handles().expect("two handles");
12300        assert!(start.line_center.x < end.line_center.x, "{start:?} {end:?}");
12301        assert!(start.rect.y > at.y - 8.0, "they hang below the line");
12302
12303        let (from, to) = (
12304            on(&d, "first line of words", 60.0),
12305            on(&d, "third line ends here", 40.0),
12306        );
12307        drag(&mut d, from, to);
12308        assert!(d.area_selection().is_some());
12309        assert!(
12310            d.area_handles().is_none(),
12311            "a mouse selection has no handles"
12312        );
12313        d.area_select_all();
12314        assert!(
12315            d.area_handles().is_none(),
12316            "nor does it grow any when the keyboard selects all"
12317        );
12318    }
12319
12320    /// **The end handle carries the end of the selection to the next text**: dragged down to the
12321    /// third line, the selection runs from the word to it. The bar is put away while the handle
12322    /// moves, and back when it is let go.
12323    #[test]
12324    fn the_end_handle_widens_the_selection_across_texts() {
12325        let mut d = driver();
12326        let at = on(&d, "second line", 12.0);
12327        hold(&mut d, at);
12328        let [_, end] = d.area_handles().expect("handles");
12329        let third = on(&d, "third line ends here", 60.0);
12330        let from = middle(end.rect);
12331        // The handle hangs below its line: carried to where its line would meet the third.
12332        let to = Point::new(third.x, from.y + (third.y - at.y));
12333        d.press(from);
12334        d.run(0.05);
12335        d.move_to(Point::new(from.x, (from.y + to.y) / 2.0));
12336        d.run(0.05);
12337        d.move_to(to);
12338        d.run(0.05);
12339        assert!(
12340            bar_button(&d, "Copy").is_none(),
12341            "no bar while a handle moves"
12342        );
12343        d.release(to);
12344        d.run(0.05);
12345        let copied = d.area_selection().expect("a selection");
12346        let lines: Vec<&str> = copied.lines().collect();
12347        assert_eq!(lines.len(), 2, "{copied:?}");
12348        assert_eq!(lines[0], "second line", "the rest of the word's line");
12349        assert!("third line ends here".starts_with(lines[1]), "{copied:?}");
12350        assert!(!lines[1].is_empty());
12351        assert!(bar_button(&d, "Copy").is_some(), "the bar is back");
12352    }
12353
12354    /// **The start handle carries the start of it back up**: dragged to the first line, the
12355    /// selection begins there.
12356    #[test]
12357    fn the_start_handle_widens_it_upward() {
12358        let mut d = driver();
12359        let at = on(&d, "second line", 80.0);
12360        hold(&mut d, at);
12361        assert_eq!(d.area_selection().as_deref(), Some("line"));
12362        let [start, _] = d.area_handles().expect("handles");
12363        let first = on(&d, "first line of words", 30.0);
12364        let from = middle(start.rect);
12365        let to = Point::new(first.x, from.y + (first.y - at.y));
12366        carry_handle(&mut d, from, to);
12367        let copied = d.area_selection().expect("a selection");
12368        let lines: Vec<&str> = copied.lines().collect();
12369        assert_eq!(lines.len(), 2, "{copied:?}");
12370        assert!("first line of words".ends_with(lines[0]), "{copied:?}");
12371        assert_eq!(lines[1], "second line");
12372    }
12373
12374    /// **A handle cannot cross the other**: the end handle dragged back past the start leaves the
12375    /// selection as it was, rather than turning it inside out.
12376    #[test]
12377    fn a_handle_does_not_cross_the_other() {
12378        let mut d = driver();
12379        let at = on(&d, "second line", 80.0);
12380        hold(&mut d, at);
12381        let before = d.area_selection();
12382        assert!(before.is_some());
12383        let [_, end] = d.area_handles().expect("handles");
12384        let first = on(&d, "first line of words", 5.0);
12385        let from = middle(end.rect);
12386        // In one move, straight to a place before the start: refused, not turned inside out.
12387        let to = Point::new(first.x, from.y + (first.y - at.y));
12388        d.press(from);
12389        d.run(0.05);
12390        d.move_to(to);
12391        d.run(0.05);
12392        d.release(to);
12393        d.run(0.05);
12394        assert_eq!(d.area_selection(), before);
12395    }
12396
12397    /// **The selection is painted**: a highlight over each text it covers, in the theme's
12398    /// colour, and none over the one it does not.
12399    #[test]
12400    fn the_selection_is_highlighted_in_each_text() {
12401        let mut d = driver();
12402        let (from, to) = (
12403            on(&d, "first line of words", 60.0),
12404            on(&d, "third line ends here", 40.0),
12405        );
12406        let bare = rects(&d);
12407        drag(&mut d, from, to);
12408        // What the selection added to the frame: a box over each text it covers.
12409        let boxes: Vec<frus_widgets::Rect> = rects(&d)
12410            .into_iter()
12411            .filter(|rect| !bare.contains(rect))
12412            .collect();
12413        assert_eq!(boxes.len(), 3, "one per text covered: {boxes:?}");
12414        let outside = on(&d, "Outside the area", 0.0);
12415        assert!(
12416            boxes.iter().all(|b| b.y > outside.y + 10.0),
12417            "none over the line outside the area: {boxes:?}"
12418        );
12419    }
12420
12421    fn rects(d: &Driver<Doc>) -> Vec<frus_widgets::Rect> {
12422        let (ui, _) = d.frame_parts().expect("a frame");
12423        ui.scene()
12424            .primitives()
12425            .iter()
12426            .filter_map(|p| match p {
12427                frus_widgets::Primitive::Rect { rect, .. } => Some(*rect),
12428                _ => None,
12429            })
12430            .collect()
12431    }
12432}
12433
12434/// Taps, double taps, long presses and secondary clicks on a
12435/// [`GestureDetector`](frus_widgets::GestureDetector), driven through the shell (milestone 581).
12436#[cfg(test)]
12437mod gesture_detector_tests {
12438    use super::testing::Driver;
12439    use crate::{Application, Command};
12440    use frus_widgets::{Container, Flex, GestureDetector, Point, Text, Theme, Widget};
12441    use std::cell::RefCell;
12442    use std::rc::Rc;
12443
12444    const W: f32 = 400.0;
12445    const H: f32 = 400.0;
12446
12447    #[derive(Clone, Debug, PartialEq)]
12448    enum Msg {
12449        Tap,
12450        Double,
12451        Hold,
12452        Menu,
12453        Plain,
12454        OnlyDouble,
12455    }
12456
12457    /// Three detectors down a page: one that takes everything, one that takes only a tap, and
12458    /// one that takes only a double tap.
12459    #[derive(Default)]
12460    struct Page {
12461        heard: Rc<RefCell<Vec<Msg>>>,
12462    }
12463
12464    impl Application for Page {
12465        type Message = Msg;
12466
12467        fn update(&mut self, message: Msg) -> Command<Msg> {
12468            self.heard.borrow_mut().push(message);
12469            Command::none()
12470        }
12471
12472        fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
12473            let block = |label: &str| {
12474                Container::new()
12475                    .width(300.0)
12476                    .height(60.0)
12477                    .child(Text::new(label).no_wrap())
12478            };
12479            Box::new(
12480                Container::new().width(W).height(H).child(
12481                    Flex::column()
12482                        .gap(40.0)
12483                        .child(
12484                            GestureDetector::new(block("everything"))
12485                                .on_tap(Msg::Tap)
12486                                .on_double_tap(Msg::Double)
12487                                .on_long_press(Msg::Hold)
12488                                .on_secondary_tap(Msg::Menu),
12489                        )
12490                        .child(GestureDetector::new(block("tap only")).on_tap(Msg::Plain))
12491                        .child(
12492                            GestureDetector::new(block("double only"))
12493                                .on_double_tap(Msg::OnlyDouble),
12494                        ),
12495                ),
12496            )
12497        }
12498    }
12499
12500    fn driver() -> (Driver<Page>, Rc<RefCell<Vec<Msg>>>) {
12501        let page = Page::default();
12502        let heard = page.heard.clone();
12503        let mut driver = Driver::new(page, W, H);
12504        driver.run(0.2);
12505        (driver, heard)
12506    }
12507
12508    /// A point inside the block labelled `label`.
12509    fn on(d: &Driver<Page>, label: &str) -> Point {
12510        let (_, rect) = d
12511            .texts()
12512            .into_iter()
12513            .find(|(text, _)| text == label)
12514            .unwrap_or_else(|| panic!("{label:?} is on screen"));
12515        Point::new(rect.x + 20.0, rect.y + 8.0)
12516    }
12517
12518    fn tap(d: &mut Driver<Page>, at: Point) {
12519        d.press(at);
12520        d.run(0.02);
12521        d.release(at);
12522        d.run(0.02);
12523    }
12524
12525    /// **With a double tap possible, a tap waits**: nothing is sent at the release, and the
12526    /// tap is sent once the wait is over.
12527    #[test]
12528    fn a_tap_waits_while_a_double_tap_is_possible() {
12529        let (mut d, heard) = driver();
12530        let at = on(&d, "everything");
12531        tap(&mut d, at);
12532        assert!(heard.borrow().is_empty(), "not yet: {:?}", heard.borrow());
12533        assert!(d.tap_deadline(), "a tap was waiting");
12534        assert_eq!(*heard.borrow(), vec![Msg::Tap]);
12535    }
12536
12537    /// **The loop wakes when the wait is over**, so a waiting tap is sent without anything
12538    /// else happening; and nothing is scheduled once it is gone.
12539    #[test]
12540    fn the_loop_wakes_for_a_waiting_tap() {
12541        let (mut d, _) = driver();
12542        let before = std::time::Instant::now();
12543        let at = on(&d, "everything");
12544        tap(&mut d, at);
12545        let wake = d.next_wake().expect("a wake for the tap");
12546        let wait = wake.duration_since(before);
12547        assert!(
12548            wait <= std::time::Duration::from_millis(400),
12549            "about 300 ms: {wait:?}"
12550        );
12551        d.tap_deadline();
12552        assert_eq!(d.next_wake(), None);
12553    }
12554
12555    /// **Two taps in quick succession are a double tap**, and not also a tap.
12556    #[test]
12557    fn two_quick_taps_are_a_double_tap() {
12558        let (mut d, heard) = driver();
12559        let at = on(&d, "everything");
12560        tap(&mut d, at);
12561        tap(&mut d, Point::new(at.x + 6.0, at.y + 2.0));
12562        assert_eq!(*heard.borrow(), vec![Msg::Double]);
12563        assert!(!d.tap_deadline(), "nothing is left waiting");
12564        assert_eq!(*heard.borrow(), vec![Msg::Double]);
12565    }
12566
12567    /// **Without a double tap, a tap answers at once.**
12568    #[test]
12569    fn a_tap_alone_answers_at_once() {
12570        let (mut d, heard) = driver();
12571        let at = on(&d, "tap only");
12572        tap(&mut d, at);
12573        assert_eq!(*heard.borrow(), vec![Msg::Plain]);
12574        tap(&mut d, at);
12575        assert_eq!(
12576            *heard.borrow(),
12577            vec![Msg::Plain, Msg::Plain],
12578            "two taps are two taps"
12579        );
12580    }
12581
12582    /// **A press somewhere else ends the wait**: the first tap is a tap after all, sent before
12583    /// what the second press does.
12584    #[test]
12585    fn a_press_elsewhere_sends_the_waiting_tap() {
12586        let (mut d, heard) = driver();
12587        let (first, other) = (on(&d, "everything"), on(&d, "tap only"));
12588        tap(&mut d, first);
12589        tap(&mut d, other);
12590        assert_eq!(*heard.borrow(), vec![Msg::Tap, Msg::Plain]);
12591    }
12592
12593    /// **The secondary button** sends its own message, and no tap.
12594    #[test]
12595    fn a_secondary_click_sends_its_message() {
12596        let (mut d, heard) = driver();
12597        let at = on(&d, "everything");
12598        d.secondary_click(at);
12599        d.run(0.02);
12600        assert_eq!(*heard.borrow(), vec![Msg::Menu]);
12601        let plain = on(&d, "tap only");
12602        d.secondary_click(plain);
12603        assert_eq!(
12604            *heard.borrow(),
12605            vec![Msg::Menu],
12606            "a detector without one hears nothing"
12607        );
12608    }
12609
12610    /// **A long press** sends its message, and the release after it is not a tap.
12611    #[test]
12612    fn a_long_press_is_not_also_a_tap() {
12613        let (mut d, heard) = driver();
12614        let at = on(&d, "everything");
12615        d.press(at);
12616        d.run(0.1);
12617        assert!(d.hold_deadline(), "the hold fired");
12618        d.release(at);
12619        d.run(0.02);
12620        d.tap_deadline();
12621        assert_eq!(*heard.borrow(), vec![Msg::Hold]);
12622    }
12623
12624    /// **A detector with only a double tap** takes the press all the same: one tap sends
12625    /// nothing, two send the double tap.
12626    #[test]
12627    fn a_double_tap_alone_is_heard() {
12628        let (mut d, heard) = driver();
12629        let at = on(&d, "double only");
12630        tap(&mut d, at);
12631        d.tap_deadline();
12632        assert!(heard.borrow().is_empty(), "{:?}", heard.borrow());
12633        tap(&mut d, at);
12634        tap(&mut d, at);
12635        assert_eq!(*heard.borrow(), vec![Msg::OnlyDouble]);
12636    }
12637}
12638
12639/// Drags on a [`GestureDetector`](frus_widgets::GestureDetector), alone and inside a scroll,
12640/// driven through the shell (milestone 582).
12641#[cfg(test)]
12642mod gesture_pan_tests {
12643    use super::testing::Driver;
12644    use crate::{Application, Command};
12645    use frus_widgets::{
12646        Container, Flex, GestureDetector, PanAxis, Point, SingleChildScrollView, Text, Theme,
12647        Widget,
12648    };
12649    use std::cell::RefCell;
12650    use std::rc::Rc;
12651
12652    const W: f32 = 400.0;
12653    const H: f32 = 400.0;
12654
12655    #[derive(Clone, Debug, PartialEq)]
12656    enum Msg {
12657        Start(Point),
12658        Update(Point, Point),
12659        End(Point),
12660        Tap,
12661        Hold,
12662    }
12663
12664    /// A detector, either alone on the page or at the top of a tall vertical scroll.
12665    struct Page {
12666        axis: PanAxis,
12667        scrolled: bool,
12668        heard: Rc<RefCell<Vec<Msg>>>,
12669    }
12670
12671    impl Application for Page {
12672        type Message = Msg;
12673
12674        fn update(&mut self, message: Msg) -> Command<Msg> {
12675            self.heard.borrow_mut().push(message);
12676            Command::none()
12677        }
12678
12679        fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
12680            let detector = GestureDetector::new(
12681                Container::new()
12682                    .width(300.0)
12683                    .height(150.0)
12684                    .child(Text::new("drag me").no_wrap()),
12685            )
12686            .on_tap(Msg::Tap)
12687            .on_long_press(Msg::Hold)
12688            .on_pan_start(Msg::Start)
12689            .on_pan_update(Msg::Update)
12690            .on_pan_end(Msg::End)
12691            .pan_axis(self.axis);
12692            let content = Flex::column()
12693                .child(Container::new().width(300.0).height(30.0))
12694                .child(detector)
12695                .child(Container::new().width(300.0).height(1200.0));
12696            if self.scrolled {
12697                Box::new(
12698                    SingleChildScrollView::new()
12699                        .width(W)
12700                        .height(H)
12701                        .child(content),
12702                )
12703            } else {
12704                Box::new(Container::new().width(W).height(H).child(content))
12705            }
12706        }
12707    }
12708
12709    fn driver(axis: PanAxis, scrolled: bool) -> (Driver<Page>, Rc<RefCell<Vec<Msg>>>) {
12710        let heard = Rc::new(RefCell::new(Vec::new()));
12711        let page = Page {
12712            axis,
12713            scrolled,
12714            heard: heard.clone(),
12715        };
12716        let mut driver = Driver::new(page, W, H);
12717        driver.run(0.2);
12718        (driver, heard)
12719    }
12720
12721    /// Where the detector's words start.
12722    fn origin(d: &Driver<Page>) -> Point {
12723        let (_, rect) = d
12724            .texts()
12725            .into_iter()
12726            .find(|(text, _)| text == "drag me")
12727            .expect("on screen");
12728        Point::new(rect.x, rect.y)
12729    }
12730
12731    /// A finger from `from` by `(dx, dy)`, in eight steps.
12732    fn swipe(d: &mut Driver<Page>, from: Point, dx: f32, dy: f32) {
12733        d.press(from);
12734        d.run(0.02);
12735        for step in 1..=8 {
12736            let f = step as f32 / 8.0;
12737            d.move_to(Point::new(from.x + dx * f, from.y + dy * f));
12738            d.run(0.02);
12739        }
12740        d.release(Point::new(from.x + dx, from.y + dy));
12741        d.run(0.02);
12742    }
12743
12744    fn deltas(heard: &[Msg]) -> Point {
12745        heard.iter().fold(Point::new(0.0, 0.0), |sum, m| match m {
12746            Msg::Update(_, delta) => Point::new(sum.x + delta.x, sum.y + delta.y),
12747            _ => sum,
12748        })
12749    }
12750
12751    fn scroll_y(d: &Driver<Page>) -> f32 {
12752        d.offsets().first().map_or(0.0, |(_, offset)| offset.1)
12753    }
12754
12755    /// **A drag is a start, movements and an end**: the start where the finger went down in
12756    /// the detector's own coordinates, and movements that add up to the way it went once past
12757    /// the slop.
12758    #[test]
12759    fn a_drag_is_a_start_updates_and_an_end() {
12760        let (mut d, heard) = driver(PanAxis::Free, false);
12761        let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 40.0);
12762        swipe(&mut d, at, 120.0, 60.0);
12763        let heard = heard.borrow();
12764        assert!(matches!(heard.first(), Some(Msg::Start(_))), "{heard:?}");
12765        if let Some(Msg::Start(local)) = heard.first() {
12766            // The words sit at the detector's corner, 30 px down the page.
12767            let corner = origin(&d);
12768            assert!(corner.y >= 30.0, "{corner:?}");
12769            assert_eq!(*local, Point::new(at.x - corner.x, at.y - corner.y));
12770        }
12771        assert!(matches!(heard.last(), Some(Msg::End(_))), "{heard:?}");
12772        if let Some(Msg::End(velocity)) = heard.last() {
12773            assert!(velocity.x.hypot(velocity.y) <= 8000.0 + 0.5, "{velocity:?}");
12774        }
12775        let moved = deltas(&heard);
12776        assert!(moved.x > 60.0 && moved.y > 20.0, "{moved:?}");
12777        assert!(!heard.contains(&Msg::Tap), "a drag is not a tap");
12778        // The last movement is where the finger lifted: nothing was left behind on the way.
12779        let last = heard.iter().rev().find_map(|m| match m {
12780            Msg::Update(local, _) => Some(*local),
12781            _ => None,
12782        });
12783        let corner = origin(&d);
12784        assert_eq!(
12785            last,
12786            Some(Point::new(at.x + 120.0 - corner.x, at.y + 60.0 - corner.y))
12787        );
12788    }
12789
12790    /// **A press that barely moves is a tap**, and no drag.
12791    #[test]
12792    fn a_press_that_does_not_move_is_a_tap() {
12793        let (mut d, heard) = driver(PanAxis::Free, false);
12794        let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 40.0);
12795        swipe(&mut d, at, 3.0, 2.0);
12796        assert_eq!(*heard.borrow(), vec![Msg::Tap]);
12797    }
12798
12799    /// **A hold on a detector that also drags is a long press**: the drag waiting for the
12800    /// finger to move does not keep the hold from being one (seen on the phone).
12801    #[test]
12802    fn a_hold_on_a_dragging_detector_is_a_long_press() {
12803        let (mut d, heard) = driver(PanAxis::Free, false);
12804        let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 40.0);
12805        d.press(at);
12806        d.run(0.1);
12807        assert!(d.hold_deadline(), "the hold fired");
12808        d.release(at);
12809        d.run(0.02);
12810        assert_eq!(*heard.borrow(), vec![Msg::Hold]);
12811    }
12812
12813    /// **The mouse drags too**, with its own, finer slop.
12814    #[test]
12815    fn a_mouse_drags_it() {
12816        let (mut d, heard) = driver(PanAxis::Free, false);
12817        let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 40.0);
12818        d.press_mouse(at);
12819        d.run(0.02);
12820        d.move_mouse(Point::new(at.x + 30.0, at.y));
12821        d.run(0.02);
12822        d.release_mouse(Point::new(at.x + 30.0, at.y));
12823        d.run(0.02);
12824        let heard = heard.borrow();
12825        assert!(matches!(heard.first(), Some(Msg::Start(_))), "{heard:?}");
12826        assert!(matches!(heard.last(), Some(Msg::End(_))), "{heard:?}");
12827    }
12828
12829    /// **A horizontal drag in a vertical scroll**: across, the drag has it and the page stays;
12830    /// down, the page scrolls and the drag hears nothing. Its movements are only across.
12831    #[test]
12832    fn a_horizontal_drag_shares_a_vertical_scroll() {
12833        let (mut d, heard) = driver(PanAxis::Horizontal, true);
12834        let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 40.0);
12835        swipe(&mut d, at, 120.0, 10.0);
12836        assert!(
12837            matches!(heard.borrow().first(), Some(Msg::Start(_))),
12838            "{:?}",
12839            heard.borrow()
12840        );
12841        assert_eq!(deltas(&heard.borrow()).y, 0.0, "only across");
12842        assert_eq!(scroll_y(&d), 0.0, "the page did not move");
12843
12844        heard.borrow_mut().clear();
12845        let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 120.0);
12846        swipe(&mut d, at, 5.0, -120.0);
12847        assert!(heard.borrow().is_empty(), "{:?}", heard.borrow());
12848        assert!(scroll_y(&d) > 30.0, "the page scrolled: {}", scroll_y(&d));
12849    }
12850
12851    /// **A free drag in a vertical scroll** takes a movement across and leaves a movement
12852    /// down to the scroll, as the reference's pan does.
12853    #[test]
12854    fn a_free_drag_leaves_the_scroll_its_own_direction() {
12855        let (mut d, heard) = driver(PanAxis::Free, true);
12856        let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 40.0);
12857        swipe(&mut d, at, 150.0, 8.0);
12858        assert!(
12859            matches!(heard.borrow().first(), Some(Msg::Start(_))),
12860            "{:?}",
12861            heard.borrow()
12862        );
12863        assert_eq!(scroll_y(&d), 0.0);
12864
12865        heard.borrow_mut().clear();
12866        let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 120.0);
12867        swipe(&mut d, at, 4.0, -120.0);
12868        assert!(heard.borrow().is_empty(), "{:?}", heard.borrow());
12869        assert!(scroll_y(&d) > 30.0, "the page scrolled: {}", scroll_y(&d));
12870    }
12871
12872    /// **A machine that stalls in the middle of a drag does not change it** (milestone 584,
12873    /// seen in CI). The shell's gestures are timed by the driver's frames and not by the wall:
12874    /// with the wall's clock, twenty milliseconds lost between two movements a microsecond
12875    /// apart read as a fling in either direction, and a page scrolled down was thrown back to
12876    /// the top.
12877    #[test]
12878    fn a_stall_mid_drag_does_not_change_the_scroll() {
12879        let (mut d, heard) = driver(PanAxis::Free, true);
12880        let from = Point::new(origin(&d).x + 40.0, origin(&d).y + 120.0);
12881        d.press(from);
12882        d.run(0.02);
12883        for step in 1..=8 {
12884            let f = step as f32 / 8.0;
12885            d.move_to(Point::new(from.x + 4.0 * f, from.y - 120.0 * f));
12886            if step == 5 {
12887                std::thread::sleep(std::time::Duration::from_millis(20));
12888            }
12889            d.run(0.02);
12890        }
12891        d.release(Point::new(from.x + 4.0, from.y - 120.0));
12892        d.run(0.02);
12893        assert!(heard.borrow().is_empty(), "{:?}", heard.borrow());
12894        assert!(scroll_y(&d) > 30.0, "the page scrolled: {}", scroll_y(&d));
12895    }
12896
12897    /// **A vertical drag in a vertical scroll** is the detector's: it asked for that
12898    /// direction, and it is under the finger.
12899    #[test]
12900    fn a_vertical_drag_takes_it_from_the_scroll() {
12901        let (mut d, heard) = driver(PanAxis::Vertical, true);
12902        let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 120.0);
12903        swipe(&mut d, at, 6.0, -100.0);
12904        let heard = heard.borrow();
12905        assert!(matches!(heard.first(), Some(Msg::Start(_))), "{heard:?}");
12906        let moved = deltas(&heard);
12907        assert_eq!(moved.x, 0.0, "only down");
12908        assert!(moved.y < -40.0, "{moved:?}");
12909        assert_eq!(scroll_y(&d), 0.0, "the page did not move");
12910    }
12911}
12912
12913/// The pointer entering, moving over and leaving a
12914/// [`MouseRegion`](frus_widgets::MouseRegion), driven through the shell (milestone 583).
12915#[cfg(test)]
12916mod mouse_region_tests {
12917    use super::testing::Driver;
12918    use crate::{Application, Command};
12919    use frus_widgets::{
12920        Container, Cursor, Flex, MouseRegion, Point, Positioned, Stack, TextField, Theme, Widget,
12921    };
12922    use std::cell::RefCell;
12923    use std::rc::Rc;
12924
12925    const W: f32 = 400.0;
12926    const H: f32 = 600.0;
12927
12928    #[derive(Clone, Debug, PartialEq)]
12929    enum Msg {
12930        Enter(&'static str, Point),
12931        Move(&'static str),
12932        Exit(&'static str),
12933        Show,
12934    }
12935
12936    /// An outer region holding an inner one at its top; below them, a big region with a
12937    /// smaller one overlapping it — not inside it — in a stack; and a region that appears
12938    /// when told to.
12939    struct Page {
12940        front_opaque: bool,
12941        shown: bool,
12942        heard: Rc<RefCell<Vec<Msg>>>,
12943    }
12944
12945    fn region(name: &'static str, child: impl Widget<Msg> + 'static) -> MouseRegion<Msg> {
12946        MouseRegion::new(child)
12947            .on_enter(move |at| Msg::Enter(name, at))
12948            .on_hover(move |_| Msg::Move(name))
12949            .on_exit(move |_| Msg::Exit(name))
12950    }
12951
12952    impl Application for Page {
12953        type Message = Msg;
12954
12955        fn update(&mut self, message: Msg) -> Command<Msg> {
12956            if message == Msg::Show {
12957                self.shown = true;
12958            } else {
12959                self.heard.borrow_mut().push(message);
12960            }
12961            Command::none()
12962        }
12963
12964        fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
12965            // 0..200: outer (300 wide), with inner 100×50 at its top-left.
12966            let nested = region(
12967                "outer",
12968                Container::new().width(300.0).height(200.0).child(
12969                    region("inner", Container::new().width(100.0).height(50.0))
12970                        .cursor(Cursor::Pointer),
12971                ),
12972            )
12973            .cursor(Cursor::Grab);
12974            // 200..400: back fills a 300×200 stack; front sits at (50, 50), 100×100, over it.
12975            let stack = Stack::new()
12976                .width(300.0)
12977                .height(200.0)
12978                .layer(region("back", Container::new().width(300.0).height(200.0)))
12979                .layer(
12980                    Positioned::new(
12981                        region("front", Container::new().width(100.0).height(100.0))
12982                            .opaque(self.front_opaque),
12983                    )
12984                    .left(50.0)
12985                    .top(50.0),
12986                );
12987            // 400..500: a field inside a region that asks for a hand.
12988            let field = region("field", TextField::new("").width(300.0)).cursor(Cursor::Pointer);
12989            // 500..600: a region that appears.
12990            let appearing: Box<dyn Widget<Msg>> = if self.shown {
12991                Box::new(region("late", Container::new().width(300.0).height(100.0)))
12992            } else {
12993                Box::new(Container::new().width(300.0).height(100.0))
12994            };
12995            Box::new(
12996                Container::new().width(W).height(H).child(
12997                    Flex::column()
12998                        .child(nested)
12999                        .child(stack)
13000                        .child(Container::new().height(100.0).child(field))
13001                        .child(appearing),
13002                ),
13003            )
13004        }
13005    }
13006
13007    fn driver(front_opaque: bool) -> (Driver<Page>, Rc<RefCell<Vec<Msg>>>) {
13008        let heard = Rc::new(RefCell::new(Vec::new()));
13009        let page = Page {
13010            front_opaque,
13011            shown: false,
13012            heard: heard.clone(),
13013        };
13014        let mut driver = Driver::new(page, W, H);
13015        driver.run(0.2);
13016        (driver, heard)
13017    }
13018
13019    fn take(heard: &Rc<RefCell<Vec<Msg>>>) -> Vec<Msg> {
13020        std::mem::take(&mut *heard.borrow_mut())
13021    }
13022
13023    fn mouse(d: &mut Driver<Page>, x: f32, y: f32) {
13024        d.move_mouse(Point::new(x, y));
13025        d.run(0.02);
13026    }
13027
13028    /// **Into a region inside another, both are entered**, the outer first, each told where
13029    /// the pointer is in its own coordinates; a move inside is heard by both; out of the
13030    /// inner one only it is left, and out of both the outer one is too.
13031    #[test]
13032    fn nested_regions_are_entered_and_left_together() {
13033        let (mut d, heard) = driver(true);
13034        mouse(&mut d, 20.0, 10.0);
13035        assert_eq!(
13036            take(&heard),
13037            vec![
13038                Msg::Enter("outer", Point::new(20.0, 10.0)),
13039                Msg::Enter("inner", Point::new(20.0, 10.0)),
13040            ]
13041        );
13042        mouse(&mut d, 30.0, 12.0);
13043        let moved = take(&heard);
13044        assert!(moved.contains(&Msg::Move("inner")) && moved.contains(&Msg::Move("outer")));
13045        mouse(&mut d, 200.0, 150.0);
13046        assert_eq!(take(&heard), vec![Msg::Exit("inner"), Msg::Move("outer")]);
13047        mouse(&mut d, 350.0, 150.0);
13048        assert_eq!(take(&heard), vec![Msg::Exit("outer")]);
13049    }
13050
13051    /// **An opaque region hides the one behind it**: over the overlap, only the front one is
13052    /// in; one that is not opaque lets the back one in too.
13053    #[test]
13054    fn an_opaque_region_hides_the_one_behind() {
13055        let (mut d, heard) = driver(true);
13056        mouse(&mut d, 80.0, 280.0);
13057        let entered: Vec<_> = take(&heard);
13058        assert!(
13059            entered.iter().any(|m| matches!(m, Msg::Enter("front", _))),
13060            "{entered:?}"
13061        );
13062        assert!(
13063            !entered.iter().any(|m| matches!(m, Msg::Enter("back", _))),
13064            "{entered:?}"
13065        );
13066        // Out of the front one and still in the back: the back one is entered now.
13067        mouse(&mut d, 250.0, 280.0);
13068        let after = take(&heard);
13069        assert!(after.contains(&Msg::Exit("front")), "{after:?}");
13070        assert!(
13071            after.iter().any(|m| matches!(m, Msg::Enter("back", _))),
13072            "{after:?}"
13073        );
13074
13075        let (mut d, heard) = driver(false);
13076        mouse(&mut d, 80.0, 280.0);
13077        let entered = take(&heard);
13078        assert!(
13079            entered.iter().any(|m| matches!(m, Msg::Enter("front", _))),
13080            "{entered:?}"
13081        );
13082        assert!(
13083            entered.iter().any(|m| matches!(m, Msg::Enter("back", _))),
13084            "{entered:?}"
13085        );
13086    }
13087
13088    /// **With a button held, a region is entered and left but hears no moves**, as the
13089    /// reference's hover is only a pointer with nothing pressed.
13090    #[test]
13091    fn no_moves_while_a_button_is_held() {
13092        let (mut d, heard) = driver(true);
13093        mouse(&mut d, 350.0, 150.0);
13094        take(&heard);
13095        d.press_mouse(Point::new(350.0, 150.0));
13096        d.run(0.02);
13097        d.move_mouse(Point::new(40.0, 20.0));
13098        d.run(0.02);
13099        d.move_mouse(Point::new(45.0, 22.0));
13100        d.run(0.02);
13101        let heard_now = take(&heard);
13102        assert!(
13103            heard_now
13104                .iter()
13105                .any(|m| matches!(m, Msg::Enter("inner", _))),
13106            "{heard_now:?}"
13107        );
13108        assert!(
13109            !heard_now.iter().any(|m| matches!(m, Msg::Move(_))),
13110            "{heard_now:?}"
13111        );
13112        d.release_mouse(Point::new(45.0, 22.0));
13113        d.run(0.02);
13114    }
13115
13116    /// **A finger is in a region while it touches**: entered at the press, left at the lift.
13117    #[test]
13118    fn a_finger_enters_while_it_touches() {
13119        let (mut d, heard) = driver(true);
13120        d.press(Point::new(200.0, 150.0));
13121        d.run(0.02);
13122        assert_eq!(
13123            take(&heard),
13124            vec![Msg::Enter("outer", Point::new(200.0, 150.0))]
13125        );
13126        d.release(Point::new(200.0, 150.0));
13127        d.run(0.02);
13128        assert_eq!(take(&heard), vec![Msg::Exit("outer")]);
13129    }
13130
13131    /// **A region that comes under a still pointer is entered** at the next frame, with no
13132    /// movement at all.
13133    #[test]
13134    fn a_region_appearing_under_the_pointer_is_entered() {
13135        let (mut d, heard) = driver(true);
13136        mouse(&mut d, 150.0, 550.0);
13137        assert!(take(&heard).is_empty());
13138        d.update(Msg::Show);
13139        d.run(0.05);
13140        let heard_now = take(&heard);
13141        assert!(
13142            heard_now.iter().any(|m| matches!(m, Msg::Enter("late", _))),
13143            "{heard_now:?}"
13144        );
13145    }
13146
13147    /// **The cursor is the innermost region's**, unless what is under the pointer asks for
13148    /// one of its own: a field keeps its text cursor inside a region asking for a hand.
13149    #[test]
13150    fn the_innermost_region_sets_the_cursor() {
13151        let (mut d, _) = driver(true);
13152        mouse(&mut d, 20.0, 10.0);
13153        assert_eq!(d.cursor_shown(), Cursor::Pointer, "the inner region's");
13154        mouse(&mut d, 200.0, 150.0);
13155        assert_eq!(d.cursor_shown(), Cursor::Grab, "the outer region's");
13156        mouse(&mut d, 350.0, 150.0);
13157        assert_eq!(d.cursor_shown(), Cursor::Default, "no region");
13158        mouse(&mut d, 100.0, 420.0);
13159        assert_eq!(d.cursor_shown(), Cursor::Text, "the field's own");
13160    }
13161}
13162
13163/// The pointer's raw events on a [`Listener`](frus_widgets::Listener), driven through the
13164/// shell (milestone 586).
13165#[cfg(test)]
13166mod listener_tests {
13167    use super::testing::Driver;
13168    use crate::{Application, Command};
13169    use frus_widgets::{
13170        Button, Container, Flex, Listener, ListenerEvent, ListenerEventKind as Raw, Point,
13171        SingleChildScrollView, Theme, Widget,
13172    };
13173    use std::cell::RefCell;
13174    use std::rc::Rc;
13175
13176    const W: f32 = 400.0;
13177    const H: f32 = 400.0;
13178
13179    #[derive(Clone, Debug, PartialEq)]
13180    enum Msg {
13181        Heard(&'static str, Raw, Point),
13182        Pressed,
13183    }
13184
13185    /// In a scroll taller than the window: an outer listener (300×300 at the top, 40 px down)
13186    /// holding an inner one (100×100 at its top-left) with a button in it.
13187    struct Page {
13188        heard: Rc<RefCell<Vec<Msg>>>,
13189    }
13190
13191    fn listen(name: &'static str) -> impl Fn(ListenerEvent) -> Option<Msg> {
13192        move |event| Some(Msg::Heard(name, event.kind, event.local))
13193    }
13194
13195    impl Application for Page {
13196        type Message = Msg;
13197
13198        fn update(&mut self, message: Msg) -> Command<Msg> {
13199            self.heard.borrow_mut().push(message);
13200            Command::none()
13201        }
13202
13203        fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
13204            let inner = Listener::new(
13205                Container::new()
13206                    .width(100.0)
13207                    .height(100.0)
13208                    .child(Button::new("Press").on_press(Msg::Pressed)),
13209                listen("inner"),
13210            );
13211            let outer = Listener::new(
13212                Container::new().width(300.0).height(300.0).child(inner),
13213                listen("outer"),
13214            );
13215            Box::new(
13216                SingleChildScrollView::new().width(W).height(H).child(
13217                    Flex::column()
13218                        .child(Container::new().width(300.0).height(40.0))
13219                        .child(outer)
13220                        .child(Container::new().width(300.0).height(1000.0)),
13221                ),
13222            )
13223        }
13224    }
13225
13226    fn driver() -> (Driver<Page>, Rc<RefCell<Vec<Msg>>>) {
13227        let heard = Rc::new(RefCell::new(Vec::new()));
13228        let mut d = Driver::new(
13229            Page {
13230                heard: heard.clone(),
13231            },
13232            W,
13233            H,
13234        );
13235        d.run(0.2);
13236        (d, heard)
13237    }
13238
13239    fn take(heard: &Rc<RefCell<Vec<Msg>>>) -> Vec<Msg> {
13240        std::mem::take(&mut *heard.borrow_mut())
13241    }
13242
13243    /// The events `name` heard, in order.
13244    fn of(heard: &[Msg], name: &str) -> Vec<(Raw, Point)> {
13245        heard
13246            .iter()
13247            .filter_map(|m| match m {
13248                Msg::Heard(n, kind, at) if *n == name => Some((*kind, *at)),
13249                _ => None,
13250            })
13251            .collect()
13252    }
13253
13254    fn scroll_y(d: &Driver<Page>) -> f32 {
13255        d.offsets().first().map_or(0.0, |(_, offset)| offset.1)
13256    }
13257
13258    /// **A press is heard by every listener under it, the innermost first**, each in its own
13259    /// coordinates.
13260    #[test]
13261    fn a_press_is_heard_innermost_first() {
13262        let (mut d, heard) = driver();
13263        d.press(Point::new(250.0, 90.0));
13264        d.run(0.02);
13265        let h = take(&heard);
13266        assert_eq!(of(&h, "outer"), vec![(Raw::Down, Point::new(250.0, 50.0))]);
13267        assert!(of(&h, "inner").is_empty(), "outside the inner one: {h:?}");
13268        d.release(Point::new(250.0, 90.0));
13269        d.run(0.02);
13270        take(&heard);
13271
13272        d.press(Point::new(60.0, 80.0));
13273        d.run(0.02);
13274        let h = take(&heard);
13275        let order: Vec<_> = h
13276            .iter()
13277            .filter_map(|m| match m {
13278                Msg::Heard(n, Raw::Down, _) => Some(*n),
13279                _ => None,
13280            })
13281            .collect();
13282        assert_eq!(order, vec!["inner", "outer"], "{h:?}");
13283        assert_eq!(of(&h, "inner"), vec![(Raw::Down, Point::new(60.0, 40.0))]);
13284        d.release(Point::new(60.0, 80.0));
13285        d.run(0.02);
13286    }
13287
13288    /// **The pointer that went down inside is followed wherever it goes**, out of the box,
13289    /// until it lifts — and the listener does not keep the page from scrolling under it.
13290    #[test]
13291    fn the_pointer_is_followed_out_and_the_page_still_scrolls() {
13292        let (mut d, heard) = driver();
13293        let from = Point::new(250.0, 300.0);
13294        d.press(from);
13295        d.run(0.02);
13296        for step in 1..=8 {
13297            d.move_to(Point::new(250.0, 300.0 - 35.0 * step as f32));
13298            d.run(0.02);
13299        }
13300        d.release(Point::new(250.0, 20.0));
13301        d.run(0.02);
13302        let h = take(&heard);
13303        let outer = of(&h, "outer");
13304        assert_eq!(outer.first().map(|e| e.0), Some(Raw::Down), "{outer:?}");
13305        assert_eq!(outer.last().map(|e| e.0), Some(Raw::Up), "{outer:?}");
13306        assert!(
13307            outer.iter().filter(|e| e.0 == Raw::Move).count() >= 7,
13308            "{outer:?}"
13309        );
13310        assert!(scroll_y(&d) > 50.0, "the page scrolled: {}", scroll_y(&d));
13311    }
13312
13313    /// **A pointer that leaves the box is still heard**, outside it, until it lifts — a mouse,
13314    /// which scrolls nothing, carried right out of both listeners.
13315    #[test]
13316    fn a_pointer_that_leaves_is_still_heard() {
13317        let (mut d, heard) = driver();
13318        d.press_mouse(Point::new(60.0, 80.0));
13319        d.run(0.02);
13320        d.move_mouse(Point::new(390.0, 390.0));
13321        d.run(0.02);
13322        d.release_mouse(Point::new(390.0, 390.0));
13323        d.run(0.02);
13324        let h = take(&heard);
13325        let inner = of(&h, "inner");
13326        assert_eq!(
13327            inner,
13328            vec![
13329                (Raw::Down, Point::new(60.0, 40.0)),
13330                (Raw::Move, Point::new(390.0, 350.0)),
13331                (Raw::Up, Point::new(390.0, 350.0)),
13332            ],
13333            "{h:?}"
13334        );
13335        assert_eq!(of(&h, "outer").len(), 3, "{h:?}");
13336    }
13337
13338    /// **A button inside keeps its tap**, and the listeners hear the press and the release.
13339    #[test]
13340    fn a_button_inside_keeps_its_tap() {
13341        let (mut d, heard) = driver();
13342        let at = Point::new(30.0, 55.0);
13343        d.press(at);
13344        d.release(at);
13345        d.run(0.05);
13346        let h = take(&heard);
13347        assert!(h.contains(&Msg::Pressed), "{h:?}");
13348        let inner = of(&h, "inner");
13349        assert_eq!(
13350            inner.iter().map(|e| e.0).collect::<Vec<_>>(),
13351            vec![Raw::Down, Raw::Up]
13352        );
13353    }
13354
13355    /// **A mouse with nothing held is heard as a hover** by the listeners under it, and by no
13356    /// other.
13357    #[test]
13358    fn a_mouse_hovering_is_heard() {
13359        let (mut d, heard) = driver();
13360        d.move_mouse(Point::new(250.0, 90.0));
13361        d.run(0.02);
13362        let h = take(&heard);
13363        assert_eq!(of(&h, "outer"), vec![(Raw::Hover, Point::new(250.0, 50.0))]);
13364        assert!(of(&h, "inner").is_empty());
13365        d.move_mouse(Point::new(390.0, 390.0));
13366        d.run(0.02);
13367        assert!(take(&heard).is_empty(), "outside both");
13368    }
13369}
13370
13371#[cfg(test)]
13372mod menu_keyboard_tests {
13373    use super::testing::Driver;
13374    use crate::{Application, Command};
13375    use frus_widgets::{
13376        KeyStroke, MenuBar, MenuItem, MenuPath, Point, ShortcutKey, SubmenuButton, Theme, Widget,
13377    };
13378
13379    const W: f32 = 600.0;
13380    const H: f32 = 400.0;
13381
13382    #[derive(Clone, Debug, PartialEq)]
13383    enum Msg {
13384        Menu(MenuPath),
13385        Picked(&'static str),
13386    }
13387
13388    /// File: New, Open recent ▸ (notes.txt, todo.md), Quit.
13389    #[derive(Default)]
13390    struct Editor {
13391        menu: MenuPath,
13392        /// The last row picked.
13393        picked: Option<&'static str>,
13394    }
13395
13396    impl Application for Editor {
13397        type Message = Msg;
13398
13399        fn update(&mut self, message: Msg) -> Command<Msg> {
13400            match message {
13401                Msg::Menu(path) => self.menu = path,
13402                Msg::Picked(what) => {
13403                    self.picked = Some(what);
13404                    self.menu = MenuPath::closed();
13405                }
13406            }
13407            Command::none()
13408        }
13409
13410        fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
13411            Box::new(
13412                MenuBar::new(&self.menu, Msg::Menu).menu(
13413                    SubmenuButton::new("File")
13414                        .item(MenuItem::new("New", Msg::Picked("new")))
13415                        .submenu(
13416                            SubmenuButton::new("Open recent")
13417                                .item(MenuItem::new("notes.txt", Msg::Picked("notes")))
13418                                .item(MenuItem::new("todo.md", Msg::Picked("todo"))),
13419                        )
13420                        .item(MenuItem::new("Quit", Msg::Picked("quit"))),
13421                ),
13422            )
13423        }
13424    }
13425
13426    /// The identity of the row whose words are `label`, from what the last frame drew.
13427    fn row(d: &Driver<Editor>, label: &str) -> Option<frus_widgets::WidgetId> {
13428        let (_, rect) = d.texts().into_iter().find(|(text, _)| text == label)?;
13429        d.hit(Point::new(rect.x + 4.0, rect.y + 4.0))
13430    }
13431
13432    /// **A menu that opens gives its first row the focus**, a submenu that opens gives its
13433    /// own first row the focus, and Escape closes one level and hands the focus back.
13434    #[test]
13435    fn opening_a_menu_focuses_its_first_row_and_escape_goes_back_one_level() {
13436        let mut d = Driver::new(Editor::default(), W, H);
13437        d.run(0.1);
13438        assert_eq!(d.focused(), None);
13439
13440        d.update(Msg::Menu(MenuPath::from_indices(vec![0])));
13441        d.run(0.1);
13442        let new = row(&d, "New").expect("the menu is open");
13443        assert_eq!(d.focused(), Some(new), "the first row has the focus");
13444
13445        d.update(Msg::Menu(MenuPath::from_indices(vec![0, 1])));
13446        d.run(0.1);
13447        let notes = row(&d, "notes.txt").expect("the submenu is open");
13448        assert_eq!(d.focused(), Some(notes), "the submenu's first row has it");
13449
13450        d.escape();
13451        d.run(0.1);
13452        assert_eq!(
13453            d.app().menu,
13454            MenuPath::from_indices(vec![0]),
13455            "one level closed"
13456        );
13457        assert!(row(&d, "notes.txt").is_none());
13458        assert_eq!(d.focused(), Some(new), "and the focus is back in the menu");
13459
13460        d.escape();
13461        d.run(0.1);
13462        assert!(!d.app().menu.is_open(), "then the menu");
13463    }
13464
13465    /// **The whole menu from the keyboard**, through the shell's own handling of each key
13466    /// (milestone 608): down to the row that opens a submenu, right into it, down to its
13467    /// second row, left back out, up to the first row and Enter on it.
13468    #[test]
13469    fn the_menu_is_worked_from_the_keyboard() {
13470        let key = |d: &mut Driver<Editor>, k: ShortcutKey| {
13471            d.key(KeyStroke::new(k));
13472            d.run(0.1);
13473        };
13474        let mut d = Driver::new(Editor::default(), W, H);
13475        d.run(0.1);
13476        d.update(Msg::Menu(MenuPath::from_indices(vec![0])));
13477        d.run(0.1);
13478        let (new, recent) = (row(&d, "New").unwrap(), row(&d, "Open recent").unwrap());
13479        assert_eq!(d.focused(), Some(new));
13480
13481        key(&mut d, ShortcutKey::Down);
13482        assert_eq!(d.focused(), Some(recent), "down to the next row");
13483        key(&mut d, ShortcutKey::Right);
13484        assert_eq!(
13485            d.app().menu,
13486            MenuPath::from_indices(vec![0, 1]),
13487            "right opens it"
13488        );
13489        assert_eq!(
13490            d.focused(),
13491            row(&d, "notes.txt"),
13492            "and its first row has the focus"
13493        );
13494        key(&mut d, ShortcutKey::Down);
13495        assert_eq!(d.focused(), row(&d, "todo.md"));
13496        key(&mut d, ShortcutKey::Left);
13497        assert_eq!(
13498            d.app().menu,
13499            MenuPath::from_indices(vec![0]),
13500            "left closes it"
13501        );
13502        assert_eq!(
13503            d.focused(),
13504            Some(recent),
13505            "and the focus is back on its row"
13506        );
13507        key(&mut d, ShortcutKey::Up);
13508        assert_eq!(d.focused(), Some(new));
13509        key(&mut d, ShortcutKey::Enter);
13510        assert_eq!(d.app().picked, Some("new"), "Enter picks the row");
13511        assert!(!d.app().menu.is_open(), "and the menus close");
13512    }
13513
13514    /// **A key with modifiers held** reaches a shortcut bound to it, and the modifiers are
13515    /// let go after it: a bare key that follows is not taken for the shortcut.
13516    #[test]
13517    fn a_stroke_holds_its_modifiers_for_the_key_alone() {
13518        let mut d = Driver::new(Editor::default(), W, H);
13519        d.run(0.1);
13520        d.key(KeyStroke::new(ShortcutKey::Char('a')).ctrl());
13521        assert!(!d.shell_modifiers().0, "ctrl let go after the stroke");
13522    }
13523
13524    /// **The focus goes back to the word on the bar** when its menu closes: Tab onto File,
13525    /// Enter opens it and the focus goes into it, Escape closes it and the focus is on File
13526    /// again. A word whose wrappers changed as its menu opened was a different widget once
13527    /// it closed, and the focus had nowhere to go back to (milestone 608).
13528    #[test]
13529    fn closing_a_menu_hands_the_focus_back_to_its_word() {
13530        let key = |d: &mut Driver<Editor>, k: ShortcutKey| {
13531            d.key(KeyStroke::new(k));
13532            d.run(0.1);
13533        };
13534        let mut d = Driver::new(Editor::default(), W, H);
13535        d.run(0.1);
13536        key(&mut d, ShortcutKey::Tab);
13537        let file = d.focused().expect("Tab reached the bar");
13538        key(&mut d, ShortcutKey::Enter);
13539        assert!(d.app().menu.is_open(), "Enter opened the menu");
13540        assert_eq!(d.focused(), row(&d, "New"), "and the focus went into it");
13541        key(&mut d, ShortcutKey::Escape);
13542        assert!(!d.app().menu.is_open(), "Escape closed it");
13543        assert_eq!(d.focused(), Some(file), "and the focus is back on its word");
13544    }
13545
13546    /// **A row that is not the first gets the focus back too.** With the focus on Quit, the
13547    /// pointer opens the submenu beside Open recent, which takes the focus; Escape closes it,
13548    /// and the focus is on Quit again. The other rows of a menu gained a hover region when a
13549    /// submenu opened, which moved them, and the focus fell to the first row instead, the
13550    /// one that takes it when nothing else does (milestone 608).
13551    #[test]
13552    fn a_submenu_closing_hands_the_focus_back_to_any_row() {
13553        let key = |d: &mut Driver<Editor>, k: ShortcutKey| {
13554            d.key(KeyStroke::new(k));
13555            d.run(0.1);
13556        };
13557        let mut d = Driver::new(Editor::default(), W, H);
13558        d.run(0.1);
13559        d.update(Msg::Menu(MenuPath::from_indices(vec![0])));
13560        d.run(0.1);
13561        key(&mut d, ShortcutKey::Down);
13562        key(&mut d, ShortcutKey::Down);
13563        let quit = row(&d, "Quit");
13564        assert_eq!(d.focused(), quit, "down twice reaches Quit");
13565        // What the pointer moving onto Open recent sends.
13566        d.update(Msg::Menu(MenuPath::from_indices(vec![0, 1])));
13567        d.run(0.1);
13568        assert_eq!(d.focused(), row(&d, "notes.txt"));
13569        key(&mut d, ShortcutKey::Escape);
13570        assert_eq!(d.app().menu, MenuPath::from_indices(vec![0]));
13571        assert_eq!(d.focused(), quit, "the focus is back on Quit");
13572    }
13573}
13574
13575/// **A right-click on a field opens its selection bar**, the desktop's context menu for the
13576/// four things the bar holds (milestone 568), driven through the shell's own path: the
13577/// milestone had built it and not run it.
13578#[cfg(test)]
13579mod right_click_bar_tests {
13580    use super::testing::Driver;
13581    use crate::{Application, Command};
13582    use frus_widgets::{Point, TextField, Theme, Widget};
13583
13584    struct Form;
13585
13586    impl Application for Form {
13587        type Message = ();
13588
13589        fn update(&mut self, _message: ()) -> Command<()> {
13590            Command::none()
13591        }
13592
13593        fn view(&self, _theme: &Theme) -> Box<dyn Widget<()>> {
13594            Box::new(TextField::new("some words in a field"))
13595        }
13596    }
13597
13598    #[test]
13599    fn a_right_click_on_a_field_opens_its_bar() {
13600        let mut d = Driver::new(Form, 500.0, 300.0);
13601        d.run(0.1);
13602        let shown = |d: &Driver<Form>, label: &str| d.texts().iter().any(|(t, _)| t == label);
13603        assert!(!shown(&d, "Select all"), "no bar before the click");
13604        let (_, words) = d
13605            .texts()
13606            .into_iter()
13607            .find(|(t, _)| t.contains("some words"))
13608            .expect("the field's words");
13609        d.secondary_click(Point::new(words.x + 20.0, words.y + words.height * 0.5));
13610        d.run(0.1);
13611        // Only what applies: nothing is selected, so nothing to cut or copy; and the
13612        // driver's clipboard is its own and empty, so nothing to paste — it no longer
13613        // follows the machine's (a CI runner's had text on it).
13614        assert!(shown(&d, "Select all"), "the bar is open: {:?}", d.texts());
13615        assert!(!shown(&d, "Copy") && !shown(&d, "Cut"));
13616        assert!(
13617            !shown(&d, "Paste"),
13618            "an empty clipboard offers nothing to paste"
13619        );
13620    }
13621
13622    /// The same field, under a theme that follows Linux.
13623    struct DesktopForm;
13624
13625    impl Application for DesktopForm {
13626        type Message = ();
13627
13628        fn update(&mut self, _message: ()) -> Command<()> {
13629            Command::none()
13630        }
13631
13632        fn view(&self, _theme: &Theme) -> Box<dyn Widget<()>> {
13633            Box::new(TextField::new("some words in a field"))
13634        }
13635
13636        fn theme(&self) -> Theme {
13637            Theme::default().with_platform(frus_widgets::TargetPlatform::Linux)
13638        }
13639    }
13640
13641    /// **A right-click opens the menu where the pointer is** (milestone 618), as the
13642    /// reference's context menu goes to the last secondary tap: its first row's words sit
13643    /// 20 px after the pointer, on the pointer's line.
13644    #[test]
13645    fn a_right_click_opens_the_menu_at_the_pointer() {
13646        let mut d = Driver::new(DesktopForm, 500.0, 300.0);
13647        d.run(0.1);
13648        let (_, words) = d
13649            .texts()
13650            .into_iter()
13651            .find(|(t, _)| t.contains("some words"))
13652            .expect("the field's words");
13653        let click = Point::new(words.x + 20.0, words.y + words.height * 0.5);
13654        d.secondary_click(click);
13655        d.run(0.1);
13656        let (_, row) = d
13657            .texts()
13658            .into_iter()
13659            .find(|(t, _)| t == "Select all")
13660            .expect("the menu is open");
13661        assert_eq!(row.x, click.x + 20.0, "the menu starts at the pointer");
13662        assert!(
13663            row.y >= click.y && row.y < click.y + 36.0,
13664            "its first row is on the pointer's line: {row:?} {click:?}"
13665        );
13666    }
13667
13668    /// **On a desktop, the menu's Select all puts the menu away** (milestone 625), as the
13669    /// reference's field does on macOS, Linux and Windows. A test runs on one of the three,
13670    /// whose system — not the theme — decides. The selection stays: the menu opened again
13671    /// over it offers Copy.
13672    #[test]
13673    fn a_desktop_s_select_all_puts_the_menu_away() {
13674        assert!(frus_widgets::default_target_platform().is_desktop());
13675        let mut d = Driver::new(DesktopForm, 500.0, 300.0);
13676        d.run(0.1);
13677        let shown = |d: &Driver<DesktopForm>, label: &str| {
13678            d.texts()
13679                .into_iter()
13680                .find(|(t, _)| t == label)
13681                .map(|(_, r)| r)
13682        };
13683        let words = shown(&d, "some words in a field").expect("the field's words");
13684        let click = Point::new(words.x + 20.0, words.y + words.height * 0.5);
13685        d.secondary_click(click);
13686        d.run(0.1);
13687        let row = shown(&d, "Select all").expect("the menu is open");
13688        let row = Point::new(row.x + 10.0, row.y + row.height * 0.5);
13689        d.press(row);
13690        d.release(row);
13691        d.run(0.1);
13692        assert!(shown(&d, "Select all").is_none(), "the menu is gone");
13693        assert!(shown(&d, "Copy").is_none(), "all of it: {:?}", d.texts());
13694        d.secondary_click(click);
13695        d.run(0.1);
13696        assert!(shown(&d, "Copy").is_some(), "the text is still selected");
13697    }
13698}
13699
13700#[cfg(test)]
13701mod scroll_behavior_tests {
13702    use super::testing::Driver;
13703    use crate::{Application, Command};
13704    use frus_widgets::{
13705        Container, Point, ScrollBehavior, ScrollConfiguration, SingleChildScrollView,
13706        TargetPlatform, Theme, VelocityStrategy, Widget,
13707    };
13708
13709    /// A tall page in a scroll view that follows iOS, in an application whose theme says
13710    /// Android.
13711    struct Page;
13712
13713    impl Application for Page {
13714        type Message = ();
13715
13716        fn update(&mut self, _message: ()) -> Command<()> {
13717            Command::none()
13718        }
13719
13720        fn view(&self, _theme: &Theme) -> Box<dyn Widget<()>> {
13721            Box::new(ScrollConfiguration::new(
13722                ScrollBehavior::material().with_platform(TargetPlatform::Ios),
13723                SingleChildScrollView::new()
13724                    .width(300.0)
13725                    .height(400.0)
13726                    .child(Container::new().width(300.0).height(2000.0)),
13727            ))
13728        }
13729
13730        fn theme(&self) -> Theme {
13731            Theme::default().with_platform(TargetPlatform::Android)
13732        }
13733    }
13734
13735    /// **A drag on a scroll area reads its fling the way the area's behaviour does**
13736    /// (milestone 616), not the application's: the reference's scrollable builds its drag
13737    /// recognizer from its own configuration.
13738    #[test]
13739    fn a_drag_on_an_area_reads_its_fling_the_area_s_way() {
13740        let mut d = Driver::new(Page, 300.0, 400.0);
13741        d.run(0.2);
13742        d.press(Point::new(150.0, 200.0));
13743        assert_eq!(
13744            d.fling_strategy(),
13745            VelocityStrategy::RecentAverage(frus_widgets::BOUNCING_FLING_WEIGHTS)
13746        );
13747        d.release(Point::new(150.0, 200.0));
13748    }
13749}
13750
13751/// A switch's thumb dragged through the shell (milestone 638): it follows the finger, flips
13752/// when let go past half way and goes back when not, and a tap still flips it.
13753#[cfg(test)]
13754mod switch_drag_tests {
13755    use super::testing::Driver;
13756    use crate::{Application, Command};
13757    use frus_widgets::{Container, Point, Rect, Switch, Theme, Widget};
13758
13759    const W: f32 = 300.0;
13760    const H: f32 = 200.0;
13761
13762    #[derive(Clone, Debug, PartialEq)]
13763    enum Msg {
13764        Set(bool),
13765    }
13766
13767    #[derive(Default)]
13768    struct Page {
13769        on: bool,
13770        heard: Vec<Msg>,
13771    }
13772
13773    impl Application for Page {
13774        type Message = Msg;
13775
13776        fn update(&mut self, message: Msg) -> Command<Msg> {
13777            let Msg::Set(on) = message;
13778            self.on = on;
13779            self.heard.push(message);
13780            Command::none()
13781        }
13782
13783        fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
13784            Box::new(
13785                Container::new()
13786                    .width(W)
13787                    .height(H)
13788                    .child(Switch::new(self.on).on_toggle(Msg::Set)),
13789            )
13790        }
13791    }
13792
13793    fn driver() -> Driver<Page> {
13794        let mut d = Driver::new(Page::default(), W, H);
13795        d.run(0.2);
13796        d
13797    }
13798
13799    /// The track and the thumb the last frame drew: the 52 × 32 rectangle, and the last one.
13800    fn track_and_thumb(d: &Driver<Page>) -> (Rect, Rect) {
13801        let (ui, _) = d.frame_parts().expect("a frame");
13802        let rects: Vec<Rect> = ui
13803            .scene()
13804            .primitives()
13805            .iter()
13806            .filter_map(|p| match p {
13807                frus_widgets::Primitive::Rect { rect, .. } => Some(*rect),
13808                _ => None,
13809            })
13810            .collect();
13811        let track = *rects
13812            .iter()
13813            .find(|r| r.width == 52.0 && r.height == 32.0)
13814            .expect("the track");
13815        (track, *rects.last().expect("the thumb"))
13816    }
13817
13818    /// How far along its travel the thumb's centre is, 0 at the start and 1 at the end.
13819    fn along(d: &Driver<Page>) -> f32 {
13820        let (track, thumb) = track_and_thumb(d);
13821        (thumb.x + thumb.width * 0.5 - track.x - 16.0) / 20.0
13822    }
13823
13824    /// A finger on the thumb, moved by `dx` in eight steps; lifted when `lift`.
13825    fn drag(d: &mut Driver<Page>, dx: f32, lift: bool) {
13826        let (track, _) = track_and_thumb(d);
13827        let from = Point::new(track.x + 16.0, track.y + 16.0);
13828        d.press(from);
13829        d.run(0.02);
13830        for step in 1..=8 {
13831            d.move_to(Point::new(from.x + dx * step as f32 / 8.0, from.y));
13832            d.run(0.02);
13833        }
13834        if lift {
13835            d.release(Point::new(from.x + dx, from.y));
13836            d.run(0.02);
13837        }
13838    }
13839
13840    /// **The thumb is where the finger is** while it is down, past the slop: placed, not
13841    /// animated, and the switch has said nothing yet.
13842    #[test]
13843    fn the_thumb_follows_the_finger() {
13844        let mut d = driver();
13845        assert!(along(&d).abs() < 1e-3, "off at rest");
13846        drag(&mut d, 40.0, false);
13847        let now = along(&d);
13848        assert!(now > 0.3 && now <= 1.0, "moved with the finger: {now}");
13849        assert!(d.app().heard.is_empty(), "nothing said while held");
13850        // Held, it stays: frames pass and the thumb does not go back.
13851        d.run(0.5);
13852        assert!((along(&d) - now).abs() < 1e-3, "held where it was put");
13853    }
13854
13855    /// **Let go past half way, it flips** — and the release is not also a tap, which would
13856    /// flip it straight back.
13857    #[test]
13858    fn let_go_past_half_way_it_flips() {
13859        let mut d = driver();
13860        drag(&mut d, 60.0, true);
13861        assert_eq!(d.app().heard, vec![Msg::Set(true)]);
13862        d.run(0.5);
13863        assert!((along(&d) - 1.0).abs() < 1e-3, "settled on");
13864    }
13865
13866    /// **Short of half way, it goes back**, and says nothing.
13867    #[test]
13868    fn let_go_short_of_half_way_it_goes_back() {
13869        let mut d = driver();
13870        // Out past the slop, then most of the way back: a finger that thought better of it.
13871        drag(&mut d, 30.0, false);
13872        let (track, _) = track_and_thumb(&d);
13873        let back = Point::new(track.x + 16.0 + 15.0, track.y + 16.0);
13874        d.move_to(back);
13875        d.run(0.02);
13876        assert!(along(&d) > 0.0, "it had moved");
13877        assert!(along(&d) < 0.5, "but is short of half way: {}", along(&d));
13878        d.release(back);
13879        d.run(0.5);
13880        assert!(d.app().heard.is_empty(), "nothing said");
13881        assert!(along(&d).abs() < 1e-3, "back off: {}", along(&d));
13882    }
13883
13884    /// **A tap still flips it**: a drag begins only past the slop.
13885    #[test]
13886    fn a_tap_still_flips_it() {
13887        let mut d = driver();
13888        let (track, _) = track_and_thumb(&d);
13889        let at = Point::new(track.x + 16.0, track.y + 16.0);
13890        d.press(at);
13891        d.release(at);
13892        d.run(0.5);
13893        assert_eq!(d.app().heard, vec![Msg::Set(true)]);
13894        assert!((along(&d) - 1.0).abs() < 1e-3);
13895    }
13896}
13897
13898/// A range slider dragged through the shell (milestone 651): the press picks the nearer
13899/// thumb and the drag carries it; a press where the thumbs meet waits for the movement; the
13900/// thumb last taken is painted on top.
13901#[cfg(test)]
13902mod range_slider_drag_tests {
13903    use super::testing::Driver;
13904    use crate::{Application, Command};
13905    use frus_widgets::{Container, Point, RangeSlider, Rect, Theme, Widget};
13906
13907    #[derive(Clone, Debug, PartialEq)]
13908    enum Msg {
13909        Range(f32, f32),
13910        Start(f32, f32),
13911        End(f32, f32),
13912    }
13913
13914    struct Page {
13915        values: (f32, f32),
13916        heard: Vec<Msg>,
13917    }
13918
13919    impl Application for Page {
13920        type Message = Msg;
13921
13922        fn update(&mut self, message: Msg) -> Command<Msg> {
13923            if let Msg::Range(s, e) | Msg::End(s, e) = message {
13924                self.values = (s, e);
13925            }
13926            self.heard.push(message);
13927            Command::none()
13928        }
13929
13930        fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
13931            let (start, end) = self.values;
13932            Box::new(
13933                Container::new().width(300.0).height(200.0).child(
13934                    RangeSlider::new(start, end)
13935                        .width(248.0)
13936                        .divisions(20)
13937                        .on_change(Msg::Range)
13938                        .on_change_start(Msg::Start)
13939                        .on_change_end(Msg::End),
13940                ),
13941            )
13942        }
13943    }
13944
13945    fn driver(start: f32, end: f32) -> Driver<Page> {
13946        let page = Page {
13947            values: (start, end),
13948            heard: Vec::new(),
13949        };
13950        let mut d = Driver::new(page, 300.0, 200.0);
13951        d.run(0.2);
13952        d
13953    }
13954
13955    /// The thumbs the last frame drew, in the order they were painted.
13956    fn thumbs(d: &Driver<Page>) -> Vec<Rect> {
13957        let (ui, _) = d.frame_parts().expect("a frame");
13958        ui.scene()
13959            .primitives()
13960            .iter()
13961            .filter_map(|p| match p {
13962                frus_widgets::Primitive::Rect { rect, blur, .. }
13963                    if *blur == 0.0 && rect.width == 20.0 && rect.height == 20.0 =>
13964                {
13965                    Some(*rect)
13966                }
13967                _ => None,
13968            })
13969            .collect()
13970    }
13971
13972    fn centre(r: Rect) -> Point {
13973        Point::new(r.x + r.width * 0.5, r.y + r.height * 0.5)
13974    }
13975
13976    /// A finger pressed at `from`, moved by `dx` in four steps, and lifted.
13977    fn drag(d: &mut Driver<Page>, from: Point, dx: f32) {
13978        d.press(from);
13979        d.run(0.02);
13980        for step in 1..=4 {
13981            d.move_to(Point::new(from.x + dx * step as f32 / 4.0, from.y));
13982            d.run(0.02);
13983        }
13984        d.release(Point::new(from.x + dx, from.y));
13985        d.run(0.02);
13986    }
13987
13988    fn close(a: (f32, f32), b: (f32, f32)) -> bool {
13989        (a.0 - b.0).abs() < 1e-3 && (a.1 - b.1).abs() < 1e-3
13990    }
13991
13992    /// **The press takes the nearer thumb and the drag carries it**: the bracket opens on
13993    /// the interval as it was and closes on the one the drag settled on; the other thumb
13994    /// stays.
13995    #[test]
13996    fn a_drag_carries_the_nearer_thumb() {
13997        let mut d = driver(0.25, 0.75);
13998        let t = thumbs(&d);
13999        assert_eq!(t.len(), 2);
14000        let start = centre(t[0]).x.min(centre(t[1]).x);
14001        // Just right of the start thumb, nearer it than the end: it is taken, and carried
14002        // to 0.445 of the 200-pixel track, which snaps to 0.45.
14003        drag(&mut d, Point::new(start + 4.0, centre(t[0]).y), 34.0);
14004        let heard = &d.app().heard;
14005        assert_eq!(heard.first(), Some(&Msg::Start(0.25, 0.75)), "{heard:?}");
14006        let Some(Msg::End(s, e)) = heard.last().cloned() else {
14007            panic!("no end: {heard:?}");
14008        };
14009        assert!(close((s, e), (0.45, 0.75)), "({s}, {e})");
14010        assert!(close(d.app().values, (0.45, 0.75)));
14011        // The start, taken last, is now the one on top: painted last.
14012        let order = thumbs(&d);
14013        assert!(order[1].x < order[0].x, "the start on top: {order:?}");
14014    }
14015
14016    /// **Where the thumbs meet, the movement decides**: leftwards takes the start, and a
14017    /// thumb never passes the other.
14018    #[test]
14019    fn where_the_thumbs_meet_the_movement_decides() {
14020        let mut d = driver(0.5, 0.5);
14021        let at = centre(thumbs(&d)[0]);
14022        drag(&mut d, at, -40.0);
14023        assert!(close(d.app().values, (0.3, 0.5)), "{:?}", d.app().values);
14024
14025        let mut d = driver(0.5, 0.5);
14026        drag(&mut d, at, 40.0);
14027        assert!(close(d.app().values, (0.5, 0.7)), "{:?}", d.app().values);
14028
14029        // The start dragged past the end stops at it.
14030        let mut d = driver(0.3, 0.5);
14031        let start = centre(thumbs(&d)[0]);
14032        drag(&mut d, start, 120.0);
14033        assert!(close(d.app().values, (0.5, 0.5)), "{:?}", d.app().values);
14034    }
14035}
14036
14037/// The focus moving, through the shell (milestone 660): a group told when it arrives and
14038/// leaves, and a field told of a press outside it.
14039#[cfg(test)]
14040mod focus_change_tests {
14041    use super::testing::Driver;
14042    use crate::{Application, Command};
14043    use frus_widgets::{Flex, Focus, Point, Rect, TextField, Theme, Widget};
14044
14045    #[derive(Clone, Debug, PartialEq)]
14046    enum Msg {
14047        Group(bool),
14048        Outside,
14049        Typed,
14050    }
14051
14052    #[derive(Default)]
14053    struct Page {
14054        heard: Vec<Msg>,
14055    }
14056
14057    impl Application for Page {
14058        type Message = Msg;
14059
14060        fn update(&mut self, message: Msg) -> Command<Msg> {
14061            if message != Msg::Typed {
14062                self.heard.push(message);
14063            }
14064            Command::none()
14065        }
14066
14067        fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
14068            Box::new(
14069                Flex::column()
14070                    .width(300.0)
14071                    .height(400.0)
14072                    .gap(40.0)
14073                    .child(
14074                        Focus::new(
14075                            TextField::new("a")
14076                                .on_input(|_| Msg::Typed)
14077                                .on_tap_outside(Msg::Outside),
14078                        )
14079                        .skip_traversal()
14080                        .on_focus_change(Msg::Group),
14081                    )
14082                    .child(TextField::new("b").on_input(|_| Msg::Typed)),
14083            )
14084        }
14085    }
14086
14087    /// The two fields' boxes, `a` then `b`.
14088    fn fields(d: &Driver<Page>) -> (Rect, Rect) {
14089        let (ui, _) = d.frame_parts().expect("a frame");
14090        let rects: Vec<Rect> = ui
14091            .focusable_ids()
14092            .filter_map(|id| ui.widget_rect(id))
14093            .filter(|r| r.height < 100.0)
14094            .collect();
14095        (rects[0], rects[rects.len() - 1])
14096    }
14097
14098    fn centre(r: Rect) -> Point {
14099        Point::new(r.x + r.width / 2.0, r.y + r.height / 2.0)
14100    }
14101
14102    fn tap(d: &mut Driver<Page>, at: Point) {
14103        d.press(at);
14104        d.release(at);
14105        d.run(0.05);
14106    }
14107
14108    /// **A press on the field brings the focus into the group; a press elsewhere tells the
14109    /// field it was outside, then the group that the focus left.**
14110    #[test]
14111    fn the_shell_reports_the_focus_moving() {
14112        let mut d = Driver::new(Page::default(), 300.0, 400.0);
14113        d.run(0.2);
14114        let (a, b) = fields(&d);
14115        tap(&mut d, centre(a));
14116        assert_eq!(d.app().heard, vec![Msg::Group(true)], "arrived");
14117        tap(&mut d, centre(a));
14118        assert_eq!(
14119            d.app().heard,
14120            vec![Msg::Group(true)],
14121            "a press inside says nothing"
14122        );
14123        tap(&mut d, centre(b));
14124        assert_eq!(
14125            d.app().heard,
14126            vec![Msg::Group(true), Msg::Outside, Msg::Group(false)],
14127            "outside, then left"
14128        );
14129        tap(&mut d, Point::new(150.0, 390.0));
14130        assert_eq!(
14131            d.app().heard.len(),
14132            3,
14133            "the other field holds no `on_tap_outside`: {:?}",
14134            d.app().heard
14135        );
14136    }
14137}