Skip to main content

frus_shell/
app.rs

1//! The generic driver: implements [`winit::application::ApplicationHandler`] for
2//! any [`Application`].
3//!
4//! The framework owns the window, the renderer, the [`Runtime`] — the retained
5//! interaction state: hover, focus, scroll, editing, animations — input routing by
6//! hit test, dragging (scrollbars, selection, handles, the back gesture) and the
7//! animation clock. The application supplies only `update`, `view` and friends.
8
9use std::collections::HashMap;
10use std::sync::Arc;
11
12use web_time::Instant;
13
14use frus_gpu::Renderer;
15use frus_widgets::{
16    build_deferred, build_ui, collect_ids, find_by_key, find_path, find_widget,
17    nearest_reorder_slot, reflow_reorder_cards, reflow_reorder_columns, reorder_drop_after,
18    reorder_siblings, reorderable_owners, subtree_ids, Accessibility, Brightness, Color,
19    Cursor as UiCursor, Edit, EditKind, EditSnapshot, FocusDirection, Insets, Key, KeyResponse,
20    KeyStroke, MediaQuery, Point, Primitive, Rect, 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    },
680    /// Reordering a **column**: a header is grabbed (`id`, column `from`) and dropped
681    /// onto another. `moved` tells a drag from a plain tap — which stays a sort —
682    /// by the `TOUCH_SLOP` threshold measured from `start`.
683    Reorder {
684        id: WidgetId,
685        from: usize,
686        start: Point,
687        moved: bool,
688        /// What is carried and where: the row the grab moves and its box, taken from the
689        /// frame the first time the drag is carried and moved with the content since, as a
690        /// lifted item's box is. The frame knows a reorderable's box only while it shows, and
691        /// only as much of it as shows — and carrying a row to an edge is exactly what
692        /// scrolls it out of sight (milestone 527, seen on a phone).
693        carried: Option<(WidgetId, Rect)>,
694    },
695    /// Panning an interactive viewport (`InteractiveViewer`): the pointer pushes the
696    /// content. `last` is the previous position, for the delta; `moved` tells a real
697    /// pan from a plain tap, by the `TOUCH_SLOP` threshold, so a click on a child can
698    /// still get through. `viewport` bounds the pan to the frame. The release
699    /// velocity comes from the shell's gesture tracker.
700    Pan {
701        id: WidgetId,
702        last: Point,
703        moved: bool,
704        viewport: frus_widgets::Rect,
705    },
706    /// Scrolling a scrollable area with a finger. `moved` tells a real scroll from a
707    /// plain tap, movement staying under the `TOUCH_SLOP` threshold.
708    Scroll {
709        id: WidgetId,
710        last: Point,
711        moved: bool,
712        /// The speed inherited from a fling this press interrupted, per axis, in
713        /// px/s — added to the release velocity so repeated swipes build momentum
714        /// where the platform does that. Zero when the content was already still.
715        carried: (f32, f32),
716        /// A dismissible item under the finger, still in the running. Both gestures
717        /// start the same way, so neither is chosen at the press: the first movement
718        /// past the threshold decides by **direction**, and the loser never sees the
719        /// gesture. Cleared once the scroll has won.
720        dismiss: Option<frus_widgets::Dismissable>,
721        /// A detector that takes a drag under the finger, still in the running, with the way
722        /// it drags (milestone 582). Decided at the threshold like `dismiss`: by direction.
723        pan: Option<(WidgetId, frus_widgets::PanAxis)>,
724        /// The axis this gesture was **claimed by**, decided once at the threshold and
725        /// held for the rest of the drag: `true` for vertical.
726        ///
727        /// A page that scrolls down must not drift sideways at the same time, and a
728        /// finger never travels in a straight line. The reference settles this in its
729        /// gesture arena — a vertical and a horizontal recogniser compete, the first
730        /// past its own slop wins, the loser is out of the gesture entirely — and this
731        /// is the same rule for an area that can go both ways. `None` until the
732        /// threshold is crossed.
733        axis: Option<bool>,
734    },
735    /// A drag on a [`GestureDetector`](frus_widgets::GestureDetector) (milestone 582). `start`
736    /// is where the pointer went down, `last` where it was at the last update; `moved` tells a
737    /// drag from a tap, by the slop, and the start is only reported once it is a drag.
738    Gesture {
739        id: WidgetId,
740        axis: frus_widgets::PanAxis,
741        start: Point,
742        last: Point,
743        moved: bool,
744    },
745    /// Swiping a [`frus_widgets::Dismissible`] item aside. `last` is the previous
746    /// position, for the delta; the item is already past the threshold by the time this
747    /// exists, since it is only ever reached by winning the direction test.
748    Dismiss {
749        item: frus_widgets::Dismissable,
750        last: Point,
751        /// `false` until the finger has passed the threshold — a press that never
752        /// travels is still a tap on the row.
753        moved: bool,
754    },
755    /// Carrying a [`frus_widgets::Draggable`] towards a [`frus_widgets::DragTarget`].
756    /// `over` is the target the pointer is on now, when it would accept the payload —
757    /// kept here so that entering and leaving one costs a comparison rather than a
758    /// second hit test.
759    Item {
760        source: frus_widgets::DragSource,
761        start: Point,
762        /// `false` until the finger has passed the threshold: a press that never
763        /// travels is a plain click on whatever is inside.
764        moved: bool,
765        over: Option<WidgetId>,
766    },
767    /// A [`frus_widgets::DraggableScrollableSheet`] moved by its panel, where nothing
768    /// under the finger scrolls — its grabber, its header, a list too short to move
769    /// (milestone 515). `available` is the height its shares are shares of, taken at
770    /// the press: the panel may shrink to nothing under the finger, and still come back.
771    Sheet {
772        id: WidgetId,
773        last: Point,
774        /// `false` until the finger has passed the threshold: a press that never
775        /// travels is a tap on whatever is in the sheet.
776        moved: bool,
777        available: f32,
778    },
779    /// The "back" gesture: the framework measures the finger's progress and velocity
780    /// and passes them to the application, which decides on the navigation.
781    Back { start_x: f32 },
782}
783
784/// The driver: an `event → frame` loop around an [`Application`].
785/// Turns an application's `view` into a tree that is **ready to be read**.
786///
787/// The one way this shell builds a tree, and it exists because there were two. A
788/// `ThemeBuilder` — and everything built on one, an `AppBar` included — has no children
789/// until [`build_deferred`] has been over it, so every traversal of a tree that has not
790/// been prepared finds nothing inside such a subtree and says nothing about it.
791///
792/// Both of the shell's build sites read the tree **before** the layout pass would have
793/// prepared it: the frame's own path calls `collect_ids` on it for the mount and leave
794/// bookkeeping, and the burst path hands it straight to `find_widget`. Neither of those is
795/// a mistake to be spotted at a call site; they are why this is one function.
796///
797/// The surface must already be described — the reader's font setting reaches anything a
798/// deferred subtree measures, and a build outside a surface measures at scale 1 while the
799/// frame lays it out at whatever the reader asked for (milestone 408).
800/// **The ambient answers an application gives**, read afresh every frame.
801///
802/// Both of these are settings rather than facts: a settings screen may turn scrollbars on
803/// or change the reader's language while the application is running, and neither should
804/// wait for a restart. Reading them once at start-up is the bug this shape avoids.
805///
806/// It is one function so that a test can drive it. The frame loop needs a window and an
807/// event-loop proxy, so nothing in this repo can call the loop — which is exactly how a
808/// setting that never reached production got shipped once already (milestone 408).
809fn install_ambient<A: Application>(app: &A, preferred: &[frus_widgets::Locale]) {
810    // **How the scrollables behave** (milestone 616): the application's behaviour, at the
811    // root of the tree, under every `ScrollConfiguration` the view installs.
812    frus_widgets::ScrollConfiguration::set_root(app.scroll_behavior());
813    // **Which language the interface is in**, resolved from what the platform reports
814    // against what the application has. Installed before the table, because an
815    // application choosing its table by language reads it from here.
816    frus_widgets::locale::install(app.resolved_locale(preferred));
817    if let Some(table) = app.localizations() {
818        frus_widgets::localizations::install(table);
819    }
820}
821
822fn build_view<A: Application>(
823    app: &A,
824    theme: &Theme,
825    runtime: &frus_widgets::Runtime,
826) -> Box<dyn Widget<A::Message>> {
827    debug_assert!(
828        frus_widgets::MediaQuery::of().is_described(),
829        "a view is being built with no surface described: install one first — milestone 416"
830    );
831    // Every state a component keeps is marked as reached while the tree is built, and
832    // what a build did not reach is let go when the frame that laid it out is done.
833    runtime.states.begin_build();
834    let tree = app.view(theme);
835    build_deferred(tree.as_ref(), theme, runtime);
836    tree
837}
838
839/// Where the messages an effect produces on another thread are sent: the event loop's proxy,
840/// or nowhere for a driver no event loop runs.
841struct Mailbox<M: 'static>(Option<EventLoopProxy<M>>);
842
843impl<M: 'static> Clone for Mailbox<M> {
844    fn clone(&self) -> Self {
845        Self(self.0.clone())
846    }
847}
848
849impl<M: 'static> Mailbox<M> {
850    /// Sends `message` to the loop. An error when the loop has closed, or when there is
851    /// none — either way nobody is left to tell.
852    fn send_event(&self, message: M) -> Result<(), ()> {
853        match &self.0 {
854            Some(proxy) => proxy.send_event(message).map_err(|_| ()),
855            None => Err(()),
856        }
857    }
858}
859
860/// The event handler that drives an [`Application`]: it owns the window, the renderer, the
861/// last interface built and the runtime, and turns each event of the loop into messages,
862/// frames and effects. [`run`](crate::run) and the other entry points build one; it is
863/// exported for a platform layer that supplies its own loop, not for an application to
864/// construct.
865pub struct App<A: Application> {
866    /// The application being driven: its state and logic.
867    app: A,
868    /// The channel that feeds back the messages effects produce, from their threads.
869    proxy: Mailbox<A::Message>,
870    window: Option<Arc<Window>>,
871    renderer: Option<Renderer>,
872    /// A renderer being initialised **asynchronously**, on the Web only: filled in by
873    /// the `spawn_local` future and picked up on the first frame, the Web being unable
874    /// to block on GPU init.
875    #[cfg(web)]
876    pending_renderer: std::rc::Rc<std::cell::RefCell<Option<Renderer>>>,
877    /// What this side knows of the browser's history — see [`crate::history`].
878    #[cfg(web)]
879    history: crate::history::History,
880    /// How the application's location is written in the address bar, and the document's base
881    /// path, which the `Path` way of writing needs. Read once, at launch.
882    #[cfg(web)]
883    address: (crate::LocationStrategy, String),
884    /// The window's accessibility bridge (AccessKit) — desktop only.
885    #[cfg(desktop)]
886    a11y: Option<crate::a11y::A11y>,
887    /// The window's accessibility bridge, projected into the canvas's DOM — web only.
888    #[cfg(web)]
889    a11y: Option<crate::a11y_web::A11y>,
890    /// The last interface built, used for hit testing, focus and scrolling.
891    ui: Option<Ui<A::Message>>,
892    /// The last widget tree built, used for keyboard and editing routing.
893    tree: Option<Box<dyn Widget<A::Message>>>,
894    /// The pointer's last known position, in **logical** pixels.
895    cursor: Point,
896    /// Where the pointer that **hovers** is: a mouse once it has moved, a finger only
897    /// while it touches. Kept as a place and asked of each frame — see [`crate::hover`].
898    hover: crate::hover::Hover,
899    /// The screen's DPI scale factor (physical = logical × scale × density).
900    scale: f32,
901    /// The last **logical** size handed to the app, to detect breakpoints.
902    last_size: Option<(f32, f32)>,
903    /// State retained between frames: hover and focus, scroll, caret and selection.
904    runtime: Runtime,
905    /// **The theme on display and the fade toward the one now asked for** — the
906    /// framework's, not the application's, since milestone 452. It resolves the
907    /// application's themes against the platform's brightness and the reader's contrast
908    /// setting once a frame, and crosses between two of them over time.
909    themes: crate::theming::ThemeFade,
910    /// Live-reload watching (development, `FRUS_WATCH=1`): relaunch on recompilation.
911    reload: Option<crate::reload::ReloadWatcher>,
912    /// Overflowing boxes already named on the console, so that a layout that does not
913    /// fit is reported once and not sixty times a second.
914    reported_overflows: std::cell::RefCell<std::collections::HashSet<u64>>,
915    /// Is the runtime inspector on? Toggled by F12, in debug builds only.
916    inspector: bool,
917    /// A tree dump to print on the next inspected frame.
918    inspector_dump: bool,
919    /// The current keyboard modifiers.
920    shift: bool,
921    ctrl: bool,
922    alt: bool,
923    meta: bool,
924    /// The remembered "goal" visual column for Up/Down/PgUp/PgDn: crossing shorter
925    /// lines keeps the original column, the way an editor does. Cleared as soon as any
926    /// other caret movement happens.
927    goal_x: Option<f32>,
928    /// Clipboard access: `arboard` on the desktop, the platform's on Android, the
929    /// browser's on the web, nothing yet on iOS.
930    clipboard: clip::Clipboard,
931    /// Has the startup effect (`init`) already run? This keeps it from being replayed
932    /// when the surface is recreated, as it is when Android returns from background.
933    started: bool,
934    /// The last frame's instant, for the animations' dt.
935    last_frame: Option<Instant>,
936    /// The **build** phase is dirty: the app's state, or the size, changed and the
937    /// `view` must be rebuilt. The view is a pure function of `(state, theme, size)` —
938    /// never of hover, scroll or focus, which live in the `Runtime` — so a frame that
939    /// only animates an interaction merely **repaints** the retained tree (§1: "a hover
940    /// touches paint and nothing else").
941    build_dirty: bool,
942    /// Whether the application's own animations were moving at the last frame. The frame
943    /// they stop in is built once more: the tick that ends a transition has changed the
944    /// state — the page it left is gone — and says it is no longer moving, so without it
945    /// the tree hit-tested from then on is the transition's last (milestone 531).
946    app_was_animating: bool,
947    /// The mouse drag under way.
948    drag: Option<Drag>,
949    /// Was the pointer that started the drag under way a finger? A mouse is
950    /// precise and needs almost no slop; a finger needs a lot.
951    pointer_touch: bool,
952    /// The pointer history of the drag under way, and the instant it began.
953    ///
954    /// One tracker for all of them: at most one drag is active at a time, and every
955    /// kind of drag asks the same question on release — how fast was the finger
956    /// going. Keeping it here rather than in each [`Drag`] variant means the
957    /// gesture's clock and its history start together, in one place.
958    gesture_velocity: VelocityTracker,
959    /// The test driver's clock for the velocity tracker, when there is one: seconds of
960    /// frames run, and when the gesture under way began on it. `None` in an application,
961    /// which stamps samples with the wall clock. A driver's events are microseconds apart on
962    /// the wall, and irregularly so on a loaded machine: a fling read from them came out at
963    /// any speed, in either direction, and a test that let go of a scroll saw it thrown back
964    /// to the top (milestone 584, seen in CI).
965    gesture_clock: Option<(f32, f32)>,
966    gesture_start: Instant,
967    /// The pointer's **smoothed** abscissa during a reorder: it springs toward the
968    /// real position, giving the columns' sliding a gentle inertia — the background
969    /// catches up with the ghost, which sticks to the pointer.
970    reorder_x: f32,
971    /// The **smoothed** ordinate of the **insertion line** during a **vertical**
972    /// reorder, that is, Kanban cards: it springs toward the **chosen** slot edge, the
973    /// hovered half, so that the line and the gap *slide* between cards instead of
974    /// jumping — the vertical counterpart of the horizontal `reorder_x` spring.
975    reorder_y: f32,
976    /// The last **announcement** pushed to the accessibility bridge's live region, for
977    /// the screen reader. It persists as is: it is re-spoken only on a change, so the
978    /// same text carried over every frame does not repeat. Desktop and web.
979    #[cfg(any(desktop, web))]
980    announce: String,
981    /// The tap-or-long-press recogniser (gesture tier 1).
982    press: PressRecognizer,
983    /// The pressed target's long-press message, captured on the press.
984    long_press_msg: Option<A::Message>,
985    /// A **text field** pressed with a finger: a hold there selects the word under it
986    /// (milestone 511), where on anything else it is the widget's long press.
987    pending_word: Option<WidgetId>,
988    /// A finger came down on words in a selection area: a hold there selects the word under it
989    /// and opens the bar (milestone 576).
990    pending_region_hold: bool,
991    /// The focused field's caret blink (milestone 513).
992    caret: crate::caret::CaretBlink,
993    /// The form last shown to the platform's autofill service, as it was shown
994    /// (milestone 512) — what a returned value is routed against, and what a value the
995    /// service has not heard is told apart from.
996    #[cfg(android)]
997    autofill_reported: Vec<crate::autofill::AutofillField>,
998    /// The field of that form the service was told has focus.
999    #[cfg(android)]
1000    autofill_focus: Option<WidgetId>,
1001    /// The [`frus_widgets::AutofillGroup`] that form is, while it is on screen.
1002    #[cfg(android)]
1003    autofill_group: Option<WidgetId>,
1004    /// When the last **recorded** edit happened, for the pause that breaks a run of
1005    /// typing into two steps of undo. One field and not one per text field, because only
1006    /// the focused one is being typed into, and leaving a field ends its run anyway.
1007    last_edit_at: Option<Instant>,
1008    /// A [`frus_widgets::Draggable`] that asked to be lifted by a **hold**, waiting
1009    /// for the long-press deadline. Inside a scrollable this is the only way up: the
1010    /// plain drag belongs to the scroll, and a hold is the one signal it cannot claim.
1011    pending_lift: Option<frus_widgets::DragSource>,
1012    /// A **reorderable row** that asked to be lifted by a hold, waiting for the same
1013    /// deadline: `(the row, its index, where the finger landed)`.
1014    ///
1015    /// Its own field rather than a second use of `pending_lift`, because what the two
1016    /// become at the deadline is different — one carries a payload to a drop target, the
1017    /// other carries itself to a slot — but the reason they wait is the same, and it is
1018    /// the reason a list can still be scrolled.
1019    pending_reorder: Option<(WidgetId, usize, Point)>,
1020    /// The last click's instant, for double-click detection.
1021    last_click_time: Option<Instant>,
1022    /// When and where the last press began a selection in a selection area: a second one soon
1023    /// after and close by is a double click.
1024    last_region_click: Option<(Instant, Point)>,
1025    /// A first tap waiting to see whether it is half of a double tap (milestone 581).
1026    pending_tap: Option<PendingTap<A::Message>>,
1027    /// How many values the work components started had brought when this shell last looked
1028    /// (milestone 585).
1029    task_arrivals: u64,
1030    /// The regions the pointer is in, the innermost first, each with its box when it was
1031    /// last seen (milestone 583).
1032    entered: Vec<(WidgetId, Rect)>,
1033    /// Whether a button or a finger is down: a region hears moves only without one.
1034    pointer_held: bool,
1035    /// The widgets hearing the pointer's raw events that it went down in, each with its box
1036    /// then: they hear it until it lifts, wherever it goes (milestone 586).
1037    captured: Vec<(WidgetId, Rect)>,
1038    /// A counter for the keys of leaving events, which fade out.
1039    leaving_counter: u64,
1040    /// The running subscriptions: id → cancellation handle, dropping which stops it.
1041    running_subs: HashMap<u64, SubHandle>,
1042    /// Pending focus requests — the keys `Command::focus` produced — resolved against
1043    /// the **freshly built** tree on the next frame.
1044    pending_focus: Vec<u64>,
1045    /// Pending **scroll** requests — the `(key, ScrollTo)` pairs `Command::scroll`
1046    /// produced — resolved against the frame that follows, and dropped whether or not
1047    /// the key named anything (see `Command::scroll`).
1048    pending_scroll: Vec<(u64, ScrollTo)>,
1049    /// The requests of the frame in progress that the **previous** frame's registry
1050    /// could not place — a region that has only just appeared. Tried once more against
1051    /// the registry this frame builds, and then gone: a request gets one frame.
1052    retry_scroll: Vec<(u64, ScrollTo)>,
1053    /// Pending **sheet** requests — the `(key, SheetTo)` pairs `Command::sheet` produced —
1054    /// on the terms of the scroll requests above.
1055    pending_sheet: Vec<(u64, SheetTo)>,
1056    /// The sheet requests of the frame in progress that the previous frame's sheets did
1057    /// not name: tried once more against this frame's.
1058    retry_sheet: Vec<(u64, SheetTo)>,
1059    /// The **focus history** of triggers, for returning focus when an overlay closes:
1060    /// on every focus change the old one, if still present, is pushed; when focus
1061    /// **vanishes** because a menu or modal closed, we go back to the most recent
1062    /// entry that is still present.
1063    focus_history: Vec<WidgetId>,
1064    /// The previous frame's focus, to detect the transitions worth pushing.
1065    prev_focus: Option<WidgetId>,
1066    /// The widgets that took the focus on appearing, as of the last frame: one that is new
1067    /// this frame takes it (milestone 605).
1068    autofocused: std::collections::HashSet<WidgetId>,
1069    /// How smooth the frames are, when asked (milestone 609).
1070    frame_stats: crate::frame_stats::FrameStats,
1071    /// What the frame being drawn has cost so far, while frames are measured.
1072    frame_costs: crate::frame_stats::FrameCosts,
1073    /// The window is occluded, so rendering is suspended.
1074    occluded: bool,
1075    /// Cumulative elapsed time, in seconds, for the continuous animations.
1076    elapsed: f32,
1077    /// The last window insets handed to the app — padding plus keyboard — in logical px.
1078    last_insets: WindowInsets,
1079    /// Whether the window's title bar line is shared with the content, and what the content
1080    /// is told about it (milestone 640).
1081    #[cfg_attr(not(windows), allow(dead_code))]
1082    title_bar_on: bool,
1083    title_bar: Option<frus_widgets::TitleBar>,
1084    /// The application's icon, kept for the life of the program, to paint on the line.
1085    #[cfg_attr(not(windows), allow(dead_code))]
1086    window_icon: Option<&'static [u8]>,
1087    /// The soft keyboard has just grown, so the focused widget is brought back into view
1088    /// once the frame that lays out the shortened window is built: the keyboard covers
1089    /// the bottom of the screen, and a field under it is a field the reader cannot see
1090    /// while typing in it.
1091    reveal_after_keyboard: bool,
1092    /// What the **platform** last said about the person using it: the font-size slider,
1093    /// the night setting, the accessibility switches.
1094    ///
1095    /// Cached rather than read per frame, because reading it is a walk across the JNI
1096    /// boundary and the answer changes about once a year. It is refreshed when the
1097    /// surface appears — which on Android is also when it *reappears*, the activity being
1098    /// recreated on a font-scale or night change since no `configChanges` is declared —
1099    /// and on a theme change where the platform sends one.
1100    platform: PlatformSettings,
1101    /// The **keyboard-free** inset baseline, along with the physical size it was
1102    /// taken at — a rotation resets it: whatever bottom inset exceeds it is credited
1103    /// to the software keyboard.
1104    inset_baseline: Option<(Insets, (u32, u32))>,
1105    /// The Android activity's handle, used to query the insets, the keyboard and so on.
1106    /// The current **lifecycle** state, so the app is notified only on real
1107    /// **transitions**.
1108    lifecycle: Lifecycle,
1109    #[cfg(android)]
1110    android_app: Option<winit::platform::android::activity::AndroidApp>,
1111    /// Is the software keyboard being asked for? It follows the text fields' focus.
1112    #[cfg(android)]
1113    soft_input_shown: bool,
1114    /// The system bars as the platform was last told them, so that it is told only of a
1115    /// change (#46).
1116    #[cfg(android)]
1117    system_bars: Option<frus_widgets::SystemBars>,
1118}
1119
1120impl<A: Application> App<A> {
1121    /// Creates the driver around an application and its message channel.
1122    pub fn new(app: A, proxy: EventLoopProxy<A::Message>) -> Self {
1123        Self::with_mailbox(app, Mailbox(Some(proxy)))
1124    }
1125
1126    /// A driver with no event loop behind it, for a test that feeds it input and frames
1127    /// by hand: whatever an effect sends back goes nowhere. An event loop cannot be built
1128    /// on a machine with no display, which is where the continuous integration runs.
1129    #[cfg(any(test, feature = "testing"))]
1130    fn detached(app: A) -> Self {
1131        let mut shell = Self::with_mailbox(app, Mailbox(None));
1132        // A shell with no window is a test's: it keeps a clipboard of its own rather than
1133        // the machine's, which tests running side by side would share — and which macOS
1134        // does not let several threads touch at once (a test run aborted on it).
1135        shell.clipboard = clip::Clipboard::in_memory();
1136        shell
1137    }
1138
1139    fn with_mailbox(app: A, proxy: Mailbox<A::Message>) -> Self {
1140        // The widget layer knows how to *want* an image over the network and has no way
1141        // to get one: no runtime, no socket, and no dependency on this crate, since the
1142        // dependency runs the other way. So the shell says how, on the way up, and the
1143        // widget layer only asks. Same shape as the image decoder a step earlier.
1144        #[cfg(feature = "net")]
1145        frus_widgets::set_image_fetcher(fetch_image_bytes);
1146        // And how to run the work a component starts (milestone 585): this shell's executor
1147        // natively, the browser on the Web.
1148        #[cfg(not(web))]
1149        frus_widgets::set_task_spawner(|task| crate::runtime::spawn(task).detach());
1150        #[cfg(web)]
1151        frus_widgets::set_task_spawner(|task| wasm_bindgen_futures::spawn_local(task));
1152        Self {
1153            app,
1154            proxy,
1155            window: None,
1156            renderer: None,
1157            #[cfg(web)]
1158            pending_renderer: std::rc::Rc::new(std::cell::RefCell::new(None)),
1159            #[cfg(web)]
1160            history: crate::history::History::new(),
1161            #[cfg(web)]
1162            address: (crate::LocationStrategy::Hash, "/".to_string()),
1163            #[cfg(desktop)]
1164            a11y: None,
1165            #[cfg(web)]
1166            a11y: None,
1167            ui: None,
1168            tree: None,
1169            cursor: Point::new(0.0, 0.0),
1170            hover: crate::hover::Hover::default(),
1171            scale: 1.0,
1172            last_size: None,
1173            runtime: Runtime::default(),
1174            themes: crate::theming::ThemeFade::default(),
1175            reload: crate::reload::ReloadWatcher::new(),
1176            reported_overflows: std::cell::RefCell::new(std::collections::HashSet::new()),
1177            inspector: false,
1178            inspector_dump: false,
1179            shift: false,
1180            ctrl: false,
1181            alt: false,
1182            meta: false,
1183            goal_x: None,
1184            clipboard: clip::Clipboard::new(),
1185            started: false,
1186            last_frame: None,
1187            build_dirty: true,
1188            app_was_animating: false,
1189            drag: None,
1190            pointer_touch: false,
1191            gesture_velocity: VelocityTracker::default(),
1192            gesture_clock: None,
1193            gesture_start: Instant::now(),
1194            reorder_x: 0.0,
1195            reorder_y: 0.0,
1196            #[cfg(any(desktop, web))]
1197            announce: String::new(),
1198            press: PressRecognizer::new(),
1199            long_press_msg: None,
1200            pending_word: None,
1201            pending_region_hold: false,
1202            caret: crate::caret::CaretBlink::new(),
1203            #[cfg(android)]
1204            autofill_reported: Vec::new(),
1205            #[cfg(android)]
1206            autofill_focus: None,
1207            #[cfg(android)]
1208            autofill_group: None,
1209            last_edit_at: None,
1210            pending_lift: None,
1211            pending_reorder: None,
1212            last_click_time: None,
1213            last_region_click: None,
1214            pending_tap: None,
1215            task_arrivals: 0,
1216            entered: Vec::new(),
1217            pointer_held: false,
1218            captured: Vec::new(),
1219            leaving_counter: 0,
1220            running_subs: HashMap::new(),
1221            pending_focus: Vec::new(),
1222            pending_scroll: Vec::new(),
1223            retry_scroll: Vec::new(),
1224            pending_sheet: Vec::new(),
1225            retry_sheet: Vec::new(),
1226            focus_history: Vec::new(),
1227            prev_focus: None,
1228            autofocused: std::collections::HashSet::new(),
1229            frame_stats: crate::frame_stats::FrameStats::new(),
1230            frame_costs: crate::frame_stats::FrameCosts::default(),
1231            occluded: false,
1232            elapsed: 0.0,
1233            last_insets: WindowInsets::ZERO,
1234            title_bar_on: false,
1235            title_bar: None,
1236            window_icon: None,
1237            reveal_after_keyboard: false,
1238            platform: PlatformSettings::default(),
1239            inset_baseline: None,
1240            // The app starts out detached; `resumed` will move it to `Resumed`.
1241            lifecycle: Lifecycle::Detached,
1242            #[cfg(android)]
1243            android_app: None,
1244            #[cfg(android)]
1245            soft_input_shown: false,
1246            #[cfg(android)]
1247            system_bars: None,
1248        }
1249    }
1250
1251    /// What an assistive technology's click on widget `id` does: what a pointer click on it
1252    /// would — its message, or, for a built-in button of the selection bar that carries none,
1253    /// its editing action (milestone 568), on the field the bar is open on.
1254    #[cfg(any(desktop, web))]
1255    fn activate_for_assistive_tech(&mut self, id: WidgetId) {
1256        if let Some(action) = self.ui.as_ref().and_then(|ui| ui.edit_action_for(id)) {
1257            self.perform_edit_action(action);
1258            return;
1259        }
1260        if let Some(msg) = self.ui.as_ref().and_then(|ui| ui.msg_for(id)) {
1261            // An item of the application's own on the bar closes it, as a press does.
1262            if self.ui.as_ref().is_some_and(|ui| ui.hit_is_on_toolbar(id)) {
1263                self.runtime.selection_handles = None;
1264                self.hide_selection_toolbar();
1265            }
1266            self.dispatch(msg);
1267            self.request_redraw();
1268        }
1269    }
1270
1271    /// Replays the actions an assistive technology asked for: an AT click activates
1272    /// the widget, exactly as a pointer click would, and an AT focus focuses it.
1273    #[cfg(desktop)]
1274    fn drain_a11y_actions(&mut self) {
1275        use crate::a11y::A11yAction;
1276        let actions = match self.a11y.as_ref() {
1277            Some(a11y) => a11y.take_actions(),
1278            None => return,
1279        };
1280        for action in actions {
1281            match action {
1282                A11yAction::Click(id) => self.activate_for_assistive_tech(id),
1283                A11yAction::Focus(id) => {
1284                    // A button of the selection bar takes no focus: the bar acts on the
1285                    // field, and a focus that left it would end the very selection the bar
1286                    // is open on before the click that follows the focus got to use it.
1287                    if self.ui.as_ref().is_some_and(|ui| ui.hit_is_on_toolbar(id)) {
1288                        continue;
1289                    }
1290                    self.runtime.input.focused = Some(id);
1291                    self.runtime.focus_visible = true;
1292                    self.request_redraw();
1293                }
1294            }
1295        }
1296    }
1297
1298    /// The web's counterpart to `drain_a11y_actions` above — same replay, the DOM
1299    /// bridge's own action type (#18).
1300    #[cfg(web)]
1301    fn drain_a11y_actions(&mut self) {
1302        use crate::a11y_web::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    /// Keeps the **system bars** in step with the frame (#46): their colour and their icons,
1326    /// from the regions under them and then the theme. The platform is told only when the
1327    /// answer changes, telling it being a crossing to the Java UI thread — and a request
1328    /// that did not arrive is asked again next frame rather than remembered as made.
1329    ///
1330    /// The two points are the first row of content under the status bar and the last one
1331    /// above the navigation bar: this window is not drawn behind the bars, so what a bar
1332    /// stands against is what lies at the edge of the content next to it.
1333    fn sync_system_bars(&mut self, theme: &Theme) {
1334        #[cfg(android)]
1335        {
1336            let Some(ui) = self.ui.as_ref() else {
1337                return;
1338            };
1339            let Some(window) = self.window.as_ref() else {
1340                return;
1341            };
1342            let scale = self.total_scale();
1343            let size = window.inner_size();
1344            let (width, height) = (size.width as f32 / scale, size.height as f32 / scale);
1345            let edges = self.last_insets.padding;
1346            let bars = ui
1347                .system_ui_style(
1348                    frus_widgets::Point::new(width / 2.0, edges.top + 0.5),
1349                    frus_widgets::Point::new(width / 2.0, height - edges.bottom - 0.5),
1350                )
1351                .resolve(theme);
1352            if self.system_bars != Some(bars) && crate::android_system_bars::apply(bars) {
1353                self.system_bars = Some(bars);
1354            }
1355        }
1356        #[cfg(not(android))]
1357        let _ = theme;
1358    }
1359
1360    /// Keeps the **software keyboard** in step with focus: asked for when focus is in
1361    /// a text field (`wants_keyboard`), closed otherwise. Called at the end of a
1362    /// frame, since any focus change already triggers a redraw.
1363    fn sync_soft_input(&mut self) {
1364        #[cfg(android)]
1365        {
1366            let editing = self
1367                .runtime
1368                .input
1369                .focused
1370                .and_then(|id| {
1371                    self.tree
1372                        .as_ref()
1373                        .and_then(|tree| find_widget(tree.as_ref(), id))
1374                })
1375                .is_some_and(wants_keyboard);
1376            if editing != self.soft_input_shown {
1377                self.soft_input_shown = editing;
1378                self.end_composition();
1379                if crate::android_ime::installed() {
1380                    // The InputConnection bridge: the Java view captures the IME.
1381                    if editing {
1382                        if let Some(id) = self.runtime.input.focused {
1383                            self.push_ime_context(id);
1384                        }
1385                        crate::android_ime::start_input(self.focused_ime());
1386                    } else {
1387                        crate::android_ime::clear_editor_state();
1388                        crate::android_ime::stop_input();
1389                    }
1390                } else if let Some(app) = &self.android_app {
1391                    // The TYPE_NULL fallback: a plain open, Latin keys only.
1392                    if editing {
1393                        app.show_soft_input(true);
1394                    } else {
1395                        app.hide_soft_input(false);
1396                    }
1397                }
1398            }
1399        }
1400    }
1401
1402    /// What keyboard the **focused field** asks for.
1403    ///
1404    /// Found the same way the caret is — the focused id, resolved against the last
1405    /// tree built — so the two answers cannot disagree about which field is being
1406    /// typed into. A focus that resolves to nothing falls back to ordinary text, which
1407    /// is what every field got before this was asked at all.
1408    #[cfg(android)]
1409    fn focused_ime(&self) -> frus_widgets::Ime {
1410        self.runtime
1411            .input
1412            .focused
1413            .and_then(|id| {
1414                self.tree
1415                    .as_ref()
1416                    .and_then(|tree| find_widget(tree.as_ref(), id))
1417            })
1418            .map(|widget| widget.ime())
1419            .unwrap_or_default()
1420    }
1421
1422    /// Notifies the app of a lifecycle **change**; never the same state twice in a row.
1423    fn set_lifecycle(&mut self, state: Lifecycle) {
1424        if self.lifecycle != state {
1425            self.lifecycle = state;
1426            self.app.on_lifecycle(state);
1427        }
1428    }
1429
1430    /// **Explicitly** asks for the software keyboard again, for the focused field —
1431    /// called when the user **taps in a text field**, even one already focused. This is
1432    /// indispensable because the keyboard may have been closed by the **system back
1433    /// button** without the app being told: `soft_input_shown` then stays `true` and
1434    /// focus does not change on the next tap, so
1435    /// [`sync_soft_input`](Self::sync_soft_input)'s diff would see no change and never
1436    /// reopen the keyboard. Here it is reopened unconditionally — the native behaviour:
1437    /// tapping in a field shows the keyboard.
1438    fn request_soft_input(&mut self) {
1439        #[cfg(android)]
1440        {
1441            // Only a field that takes typing: a tap in a paragraph made selectable is a tap
1442            // in words to select, not in a form.
1443            let typing = self
1444                .runtime
1445                .input
1446                .focused
1447                .and_then(|id| {
1448                    self.tree
1449                        .as_ref()
1450                        .and_then(|tree| find_widget(tree.as_ref(), id))
1451                })
1452                .is_some_and(wants_keyboard);
1453            if !typing {
1454                return;
1455            }
1456            self.soft_input_shown = true;
1457            self.end_composition();
1458            if crate::android_ime::installed() {
1459                if let Some(id) = self.runtime.input.focused {
1460                    self.push_ime_context(id);
1461                }
1462                crate::android_ime::start_input(self.focused_ime());
1463            } else if let Some(app) = &self.android_app {
1464                app.show_soft_input(true);
1465            }
1466        }
1467    }
1468
1469    /// Applies the pending IME operations to the focused field (the §6 bridge). The
1470    /// composition is materialised **in the field**: each update erases the previous
1471    /// one, the controlled model having no styled composition region yet — see
1472    /// docs/milestone-81.md.
1473    #[cfg(android)]
1474    fn drain_ime(&mut self) {
1475        use crate::ime::Step;
1476        let events = crate::android_ime::drain();
1477        if events.is_empty() {
1478            return;
1479        }
1480        let Some(focused) = self.runtime.input.focused else {
1481            return;
1482        };
1483        for event in events {
1484            let edit = self
1485                .runtime
1486                .edits
1487                .get(&focused)
1488                .copied()
1489                .unwrap_or_default();
1490            // Planned against the field as it stands before this operation: its text,
1491            // its caret and the composition it underlines (milestone 510).
1492            let text = self
1493                .tree
1494                .as_ref()
1495                .and_then(|tree| find_widget(tree.as_ref(), focused))
1496                .and_then(|widget| widget.text_value().map(str::to_owned))
1497                .unwrap_or_default();
1498            for step in crate::ime::plan(&event, &edit, &text) {
1499                match step {
1500                    Step::Place { cursor, anchor } => {
1501                        let edit = self.runtime.edits.entry(focused).or_default();
1502                        edit.cursor = cursor;
1503                        edit.anchor = anchor;
1504                    }
1505                    Step::Key(key) => self.apply_key(focused, key),
1506                    Step::Compose(region) => {
1507                        if let Some(edit) = self.runtime.edits.get_mut(&focused) {
1508                            edit.composing = region;
1509                        }
1510                    }
1511                    Step::ComposeTo(start) => {
1512                        if let Some(edit) = self.runtime.edits.get_mut(&focused) {
1513                            let end = edit.cursor;
1514                            edit.composing = (end > start).then_some((start, end));
1515                        }
1516                    }
1517                }
1518            }
1519        }
1520        // Refresh the input context, which the IME queries for its suggestions.
1521        self.push_ime_context(focused);
1522        self.request_redraw();
1523    }
1524
1525    /// Keeps the platform's **autofill service** in step with the form being edited
1526    /// (milestone 512). When a field that says what it is for takes focus, its form is
1527    /// shown to the service and the service is told which field it is in; while it stays,
1528    /// every value the service has not heard is reported, or what it saves is what the
1529    /// fields held when it first looked; and when the form's group has left the screen
1530    /// — submitted, or navigated away from — it is committed, which is the moment a
1531    /// service may offer to save what was typed.
1532    #[cfg(android)]
1533    fn sync_autofill(&mut self) {
1534        use crate::{android_autofill as platform, autofill};
1535        if !crate::android_ime::installed() {
1536            return;
1537        }
1538        let scale = self.total_scale();
1539        let (Some(ui), Some(tree)) = (self.ui.as_ref(), self.tree.as_ref()) else {
1540            return;
1541        };
1542        let fields_of = |id: WidgetId| {
1543            autofill::structure(&ui.form_of(id), scale, |widget| {
1544                find_widget(tree.as_ref(), widget).map(|w| {
1545                    (
1546                        w.autofill_hints(),
1547                        w.text_value().unwrap_or_default().to_owned(),
1548                    )
1549                })
1550            })
1551        };
1552        // The focused field, when it takes part: it says what it is for.
1553        let focus = self.runtime.input.focused.filter(|&id| {
1554            find_widget(tree.as_ref(), id).is_some_and(|w| !w.autofill_hints().is_empty())
1555        });
1556        if focus != self.autofill_focus {
1557            if let Some(left) = self.autofill_focus {
1558                platform::exit(autofill::virtual_id(left));
1559            }
1560            if let Some(entered) = focus {
1561                let group = ui.form_group(entered);
1562                // A field of the form already shown — the next step of a wizard — keeps
1563                // the fields shown before it; any other form starts afresh.
1564                let shown: &[autofill::AutofillField] =
1565                    if group.is_some() && group == self.autofill_group {
1566                        &self.autofill_reported
1567                    } else {
1568                        &[]
1569                    };
1570                let fields = autofill::with_gone(fields_of(entered), shown);
1571                platform::publish(&fields);
1572                platform::enter(autofill::virtual_id(entered));
1573                self.autofill_reported = fields;
1574                self.autofill_group = group;
1575            }
1576            self.autofill_focus = focus;
1577        } else if let Some(id) = focus {
1578            let fields = autofill::with_gone(fields_of(id), &self.autofill_reported);
1579            for (virtual_id, value) in autofill::changed(&self.autofill_reported, &fields) {
1580                platform::value_changed(virtual_id, &value);
1581            }
1582            self.autofill_reported = fields;
1583        }
1584        if let Some(group) = self.autofill_group {
1585            if ui.group_stops(group).is_empty() {
1586                platform::commit();
1587                self.autofill_group = None;
1588                self.autofill_reported.clear();
1589            }
1590        }
1591    }
1592
1593    /// Puts the values an autofill service chose into their fields (milestone 512), each
1594    /// through the message typing it would have produced — the path an undo takes, so a
1595    /// field that refuses typing refuses this too — with the caret after what arrived.
1596    #[cfg(android)]
1597    fn drain_autofill(&mut self) {
1598        let fills = crate::android_autofill::drain();
1599        if fills.is_empty() {
1600            return;
1601        }
1602        for (widget, value) in crate::autofill::route(&self.autofill_reported, fills) {
1603            let message = self
1604                .tree
1605                .as_ref()
1606                .and_then(|tree| find_widget(tree.as_ref(), widget))
1607                .and_then(|w| w.replace_value(value.clone()));
1608            if let Some(message) = message {
1609                self.runtime.edits.insert(
1610                    widget,
1611                    Edit {
1612                        cursor: value.chars().count(),
1613                        anchor: None,
1614                        composing: None,
1615                    },
1616                );
1617                self.dispatch_edit(message);
1618            }
1619        }
1620        if let Some(id) = self.runtime.input.focused {
1621            self.push_ime_context(id);
1622        }
1623        self.request_redraw();
1624    }
1625
1626    /// Publishes field `id`'s editing state to the bridge — text, caret and selection
1627    /// — the context the IME reads for its suggestions.
1628    #[cfg(android)]
1629    fn push_ime_context(&self, id: WidgetId) {
1630        let value = self
1631            .tree
1632            .as_ref()
1633            .and_then(|tree| find_widget(tree.as_ref(), id))
1634            .and_then(|widget| widget.text_value().map(|s| s.to_string()));
1635        if let Some(text) = value {
1636            let edit = self.runtime.edits.get(&id).copied().unwrap_or_default();
1637            crate::android_ime::set_editor_state(&text, edit.cursor, edit.selection_range());
1638            // And the keyboard is **told**, as an Android editor tells it on every change:
1639            // a keyboard that predicts keeps its own model of the field, and one never
1640            // told of a change drifts from it (milestone 510). In UTF-16 units.
1641            let len = text.chars().count();
1642            let at = |i: usize| crate::ime::utf16_index(&text, i.min(len)) as i32;
1643            let (start, end) = edit.selection_range().unwrap_or((edit.cursor, edit.cursor));
1644            let (cand_start, cand_end) = edit
1645                .composing
1646                .map(|(s, e)| (at(s), at(e)))
1647                .unwrap_or((-1, -1));
1648            crate::android_ime::update_selection(at(start), at(end), cand_start, cand_end);
1649        }
1650    }
1651
1652    /// Ends the focused field's composition where it stands: the keyboard is being
1653    /// started again, and a keyboard started again has forgotten it.
1654    #[cfg(android)]
1655    fn end_composition(&mut self) {
1656        if let Some(id) = self.runtime.input.focused {
1657            if let Some(edit) = self.runtime.edits.get_mut(&id) {
1658                edit.composing = None;
1659            }
1660        }
1661    }
1662
1663    /// Remembers the Android activity's handle, the source of the system insets.
1664    #[cfg(android)]
1665    pub(crate) fn set_android_app(
1666        &mut self,
1667        android_app: winit::platform::android::activity::AndroidApp,
1668    ) {
1669        self.android_app = Some(android_app);
1670    }
1671
1672    /// The system insets — the safe area — in **logical** px. On Android they are
1673    /// derived from the activity's content rect, outside the system bars; zero
1674    /// elsewhere.
1675    ///
1676    /// The content rect is what the system leaves the activity, so it excludes
1677    /// **any** decoration the theme asks for — a title bar included. An app whose
1678    /// manifest keeps the default theme therefore reports a top inset of the status
1679    /// bar *plus* 56dp of action bar that is never drawn, and the shell dutifully
1680    /// pads it away: a wide empty band above the app bar. The manifests here ask for
1681    /// `Theme.DeviceDefault.NoActionBar`, and so must any frus app.
1682    fn compute_insets(&self, phys_w: u32, phys_h: u32, scale: f32) -> Insets {
1683        #[cfg(android)]
1684        if let Some(app) = &self.android_app {
1685            let r = app.content_rect();
1686            // A degenerate rect, before the first layout, means no inset.
1687            if r.right > r.left && r.bottom > r.top {
1688                let left = r.left.max(0) as f32;
1689                let top = r.top.max(0) as f32;
1690                let right = (phys_w as i32 - r.right).max(0) as f32;
1691                let bottom = (phys_h as i32 - r.bottom).max(0) as f32;
1692                return Insets::new(top / scale, right / scale, bottom / scale, left / scale);
1693            }
1694        }
1695        let _ = (phys_w, phys_h, scale);
1696        Insets::ZERO
1697    }
1698
1699    /// Asks the platform what it says about its user, and remembers the answer.
1700    ///
1701    /// Marks the build dirty when something moved: the font size reaches the layout
1702    /// through [`MediaQuery::scope`], so a changed scaler is a changed geometry for every
1703    /// widget on the screen, and a frame drawn from the cache would keep the old one.
1704    fn refresh_platform_settings(&mut self) {
1705        #[cfg(android)]
1706        if let Some(app) = &self.android_app {
1707            let read = crate::android_settings::read(app);
1708            if read != self.platform {
1709                self.platform = read;
1710                self.build_dirty = true;
1711            }
1712            return;
1713        }
1714        // Desktop and web report the one thing their window system knows. A text scaler
1715        // is **not** among it: winit exposes no equivalent of Windows'
1716        // `UISettings.TextScaleFactor` or of GNOME's `text-scaling-factor`, so it stays
1717        // at 1 there and an application that knows better says so itself. That is a
1718        // missing wire and is written down rather than papered over.
1719        let brightness = self
1720            .window
1721            .as_ref()
1722            .and_then(|window| window.theme())
1723            .map(|theme| match theme {
1724                winit::window::Theme::Dark => frus_widgets::Brightness::Dark,
1725                winit::window::Theme::Light => frus_widgets::Brightness::Light,
1726            })
1727            .unwrap_or_default();
1728        if brightness != self.platform.brightness {
1729            self.platform.brightness = brightness;
1730            self.build_dirty = true;
1731        }
1732
1733        // **The reader's languages**, read once. Changing the display language on a
1734        // desktop means signing out and back in on Windows, and is a per-process
1735        // environment variable on Linux — so there is nothing to watch for, and re-reading
1736        // it every time the window comes back would allocate for an answer that cannot
1737        // have moved. Android is the platform where it *does* change under a running
1738        // application, and Android reads it on every walk.
1739        if self.platform.locales.is_empty() {
1740            let read = platform_locales();
1741            if !read.is_empty() {
1742                self.platform.locales = read;
1743                self.build_dirty = true;
1744            }
1745        }
1746    }
1747}
1748
1749/// **The reader's preferred languages**, best first, from whatever the platform keeps
1750/// them in. Empty when it says nothing, or when there is no way to ask.
1751///
1752/// Android does not come through here: its answer lives on the activity's
1753/// `Configuration`, which [`crate::android_settings`] already walks.
1754/// On **Android** there is nothing to ask here: the answer lives on the activity's
1755/// `Configuration`, and the branch above returns before reaching this. A build with no
1756/// activity at all has no reader to ask about.
1757#[cfg(android)]
1758fn platform_locales() -> Vec<frus_widgets::Locale> {
1759    Vec::new()
1760}
1761
1762#[cfg(not(android))]
1763fn platform_locales() -> Vec<frus_widgets::Locale> {
1764    #[cfg(not(web))]
1765    {
1766        sys_locale::get_locales()
1767            .filter_map(|tag| frus_widgets::Locale::parse(&tag))
1768            .collect()
1769    }
1770    #[cfg(web)]
1771    {
1772        // `navigator.languages` is the ordered list; `navigator.language` is its first
1773        // entry and the fallback for a browser that does not offer the list.
1774        let Some(navigator) = web_sys::window().map(|window| window.navigator()) else {
1775            return Vec::new();
1776        };
1777        let listed: Vec<frus_widgets::Locale> = navigator
1778            .languages()
1779            .iter()
1780            .filter_map(|value| value.as_string())
1781            .filter_map(|tag| frus_widgets::Locale::parse(&tag))
1782            .collect();
1783        if listed.is_empty() {
1784            return navigator
1785                .language()
1786                .and_then(|tag| frus_widgets::Locale::parse(&tag))
1787                .into_iter()
1788                .collect();
1789        }
1790        listed
1791    }
1792}
1793
1794impl<A: Application> ApplicationHandler<A::Message> for App<A> {
1795    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
1796        if self.window.is_some() {
1797            return;
1798        }
1799        // Back in the foreground, or starting up: the surface is (re)born below.
1800        self.set_lifecycle(Lifecycle::Resumed);
1801        // And so is what the platform says about its user — a trip to the system
1802        // settings and back is exactly this event.
1803        self.refresh_platform_settings();
1804
1805        let mut attributes = Window::default_attributes()
1806            .with_title(self.app.title())
1807            // A sensible minimum size, in logical px, to avoid an absurd UI.
1808            .with_min_inner_size(winit::dpi::LogicalSize::new(360.0, 280.0));
1809        // The AccessKit adapter must be created **before** the window is shown, so we
1810        // create it hidden and reveal it afterwards. Desktop only.
1811        #[cfg(desktop)]
1812        {
1813            attributes = attributes.with_visible(false);
1814        }
1815        // The application's icon, on the window (Windows and X11; Wayland and macOS take it
1816        // from the bundle and ignore this). Windows keeps a second, larger one for the taskbar
1817        // and Alt+Tab, set on its own.
1818        #[cfg(desktop)]
1819        {
1820            let icon = crate::icon::window_icon(&self.app.icon());
1821            #[cfg(windows)]
1822            {
1823                use winit::platform::windows::WindowAttributesExtWindows;
1824                attributes = attributes.with_taskbar_icon(icon.clone());
1825            }
1826            attributes = attributes.with_window_icon(icon);
1827        }
1828        // Not on the Web: there the page sizes the canvas, with its own CSS, and asking winit
1829        // for a size writes it **inline** on the canvas, where it outranks the stylesheet — an
1830        // application that named 900 by 680 then stayed 900 by 680 in a phone-sized window,
1831        // cropped, its drawer off the edge of what could be seen.
1832        #[cfg(not(web))]
1833        if let Some((w, h)) = self.app.window_size() {
1834            attributes = attributes.with_inner_size(winit::dpi::LogicalSize::new(w, h));
1835        }
1836        // Web: winit creates a `<canvas>` and **appends it** to the document's body.
1837        #[cfg(web)]
1838        {
1839            use winit::platform::web::WindowAttributesExtWebSys;
1840            attributes = attributes.with_append(true);
1841        }
1842        let window = match event_loop.create_window(attributes) {
1843            Ok(window) => Arc::new(window),
1844            Err(err) => {
1845                log::error!("failed to create the window: {err}");
1846                event_loop.exit();
1847                return;
1848            }
1849        };
1850
1851        // Web: GPU init is **asynchronous**, blocking being impossible. We start the
1852        // future, and the ready renderer is picked up on the first frame — see
1853        // `RedrawRequested`. `init` runs right away, since it does not touch the GPU.
1854        #[cfg(web)]
1855        {
1856            self.scale = window.scale_factor() as f32;
1857            self.window = Some(window.clone());
1858            // The accessibility bridge (#18): the canvas exists as soon as the window
1859            // does, so there is no "still hidden" step to wait for here.
1860            self.a11y = crate::a11y_web::A11y::new(&window);
1861            // The browser keeps reload, the developer tools and the rest of its own shortcuts
1862            // while the canvas has the focus, instead of winit cancelling them.
1863            crate::web_keys::let_the_browser_keep_its_shortcuts();
1864            // The tab's icon: the application's, or the frus logo where the page has none.
1865            crate::web_icon::apply(&self.app.icon());
1866            self.build_dirty = true;
1867            if !self.started {
1868                self.started = true;
1869                let command = self.app.init();
1870                self.run_command(command);
1871                self.sync_subscriptions();
1872                self.open_launch_address(&window);
1873            }
1874            let slot = self.pending_renderer.clone();
1875            let win = window.clone();
1876            let size = window.inner_size();
1877            wasm_bindgen_futures::spawn_local(async move {
1878                match Renderer::new(win.clone(), size.width.max(1), size.height.max(1)).await {
1879                    Ok(r) => {
1880                        *slot.borrow_mut() = Some(r);
1881                        win.request_redraw();
1882                    }
1883                    Err(err) => log::error!("failed to initialise the renderer (Web): {err:#}"),
1884                }
1885            });
1886        }
1887
1888        #[cfg(not(web))]
1889        {
1890            let size = window.inner_size();
1891            let (width, height) = (size.width.max(1), size.height.max(1));
1892            // On Windows, a renderer whose frames let the system's own title bar show where
1893            // the window's menu bar leaves the line transparent (milestone 643); the
1894            // ordinary one where that cannot be had.
1895            let see_through = if cfg!(windows) {
1896                match pollster::block_on(Renderer::see_through(window.clone(), width, height)) {
1897                    Ok(renderer) => Some(renderer),
1898                    Err(err) => {
1899                        log::info!("title bar painted by frus: {err:#}");
1900                        None
1901                    }
1902                }
1903            } else {
1904                None
1905            };
1906            let renderer = match see_through {
1907                Some(renderer) => Ok(renderer),
1908                None => pollster::block_on(Renderer::new(window.clone(), width, height)),
1909            };
1910
1911            match renderer {
1912                Ok(renderer) => {
1913                    self.scale = window.scale_factor() as f32;
1914                    // The accessibility bridge (AccessKit), created while the window is
1915                    // still hidden, after which we reveal it. Inert with no screen reader.
1916                    #[cfg(desktop)]
1917                    {
1918                        self.a11y = Some(crate::a11y::A11y::new(event_loop, &window));
1919                        window.set_visible(true);
1920                        // A second process's link brings this window forward and asks it for
1921                        // a frame.
1922                        crate::instance::set_window(window.clone());
1923                    }
1924                    self.window = Some(window.clone());
1925                    // What the input bridge's wakes ask a frame of (milestone 513).
1926                    #[cfg(android)]
1927                    crate::android_ime::set_window(Some(window.clone()));
1928                    self.renderer = Some(renderer);
1929                    // The surface was (re)created: force a full rebuild on the first frame.
1930                    self.build_dirty = true;
1931                    // The startup effect, an initial load and so on: once only, not on
1932                    // every surface recreation, such as returning from the background.
1933                    if !self.started {
1934                        self.started = true;
1935                        let command = self.app.init();
1936                        self.run_command(command);
1937                        self.sync_subscriptions();
1938                        #[cfg(any(android, desktop))]
1939                        self.open_launch_link();
1940                    }
1941                    window.request_redraw();
1942                }
1943                Err(err) => {
1944                    log::error!("failed to initialise the renderer: {err:#}");
1945                    event_loop.exit();
1946                }
1947            }
1948        }
1949    }
1950
1951    /// Going to the background, on Android: the native surface is destroyed. We
1952    /// release the renderer and the window, and `resumed` recreates them on the way
1953    /// back, without replaying `init` — see `started`. Harmless on desktop, where the
1954    /// event never fires.
1955    fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
1956        // Background: the surface is lost, so the app moves to `Paused`.
1957        self.set_lifecycle(Lifecycle::Paused);
1958        self.renderer = None;
1959        self.window = None;
1960        #[cfg(android)]
1961        crate::android_ime::set_window(None);
1962        self.last_frame = None;
1963    }
1964
1965    /// The event loop is ending, on close: a final `Detached` notification, where the
1966    /// app can persist its state before the process disappears.
1967    fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
1968        self.set_lifecycle(Lifecycle::Detached);
1969    }
1970
1971    /// A message produced by an effect, on a background thread: we apply it and ask
1972    /// for another frame.
1973    fn user_event(&mut self, _event_loop: &ActiveEventLoop, message: A::Message) {
1974        self.dispatch(message);
1975        self.request_redraw();
1976    }
1977
1978    /// The loop woke up: if the long-press deadline has been reached the recogniser
1979    /// **accepts eagerly** — the message is emitted and the release to come will be
1980    /// swallowed, the long press having evicted the tap.
1981    fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) {
1982        if matches!(cause, StartCause::ResumeTimeReached { .. }) && self.press.poll(Instant::now())
1983        {
1984            self.hold_deadline_reached();
1985        }
1986        if matches!(cause, StartCause::ResumeTimeReached { .. }) {
1987            self.poll_pending_tap(Instant::now());
1988        }
1989
1990        // The caret's turn is due: a frame to show it, or to hide it, in (milestone 513).
1991        if matches!(cause, StartCause::ResumeTimeReached { .. })
1992            && self.lifecycle == Lifecycle::Resumed
1993            && self.caret.next_toggle(Instant::now()).is_some()
1994        {
1995            self.request_redraw();
1996        }
1997
1998        // Pending IME operations, from the Android input bridge.
1999        #[cfg(android)]
2000        self.drain_ime();
2001        // And the values an autofill service chose, from the same bridge.
2002        #[cfg(android)]
2003        self.drain_autofill();
2004
2005        // Actions an assistive technology asked for, through AccessKit.
2006        #[cfg(desktop)]
2007        self.drain_a11y_actions();
2008        // Actions a screen reader asked for, through the DOM bridge (#18).
2009        #[cfg(web)]
2010        self.drain_a11y_actions();
2011
2012        // Live reload, in development: the binary was replaced by a recompilation, so
2013        // capture the state and relaunch the new binary. This does not return.
2014        if let Some(watcher) = self.reload.as_mut() {
2015            if watcher.binary_changed() {
2016                watcher.handoff(self.app.save_state());
2017            }
2018        }
2019        event_loop.set_control_flow(self.idle_control_flow());
2020    }
2021
2022    fn window_event(
2023        &mut self,
2024        event_loop: &ActiveEventLoop,
2025        _window_id: WindowId,
2026        event: WindowEvent,
2027    ) {
2028        // The AccessKit adapter observes the window events: focus, size and so on.
2029        #[cfg(desktop)]
2030        if let (Some(a11y), Some(window)) = (self.a11y.as_mut(), self.window.as_ref()) {
2031            a11y.process_event(window, &event);
2032        }
2033
2034        match event {
2035            WindowEvent::CloseRequested => event_loop.exit(),
2036
2037            // Gaining or losing focus **in the foreground**: `Resumed` ⇄ `Inactive`. We
2038            // leave `Paused`/`Detached` — background and closing — alone; those are
2039            // decided by `suspended` and `exiting`.
2040            WindowEvent::Focused(focused) => {
2041                if !matches!(self.lifecycle, Lifecycle::Paused | Lifecycle::Detached) {
2042                    self.set_lifecycle(if focused {
2043                        Lifecycle::Resumed
2044                    } else {
2045                        Lifecycle::Inactive
2046                    });
2047                }
2048                // The line the window shares with the system quietens with it, as the
2049                // system's own caption does: a frame reads the window's state again
2050                // (milestone 643).
2051                #[cfg(windows)]
2052                if self.title_bar_on {
2053                    self.request_redraw();
2054                }
2055            }
2056
2057            WindowEvent::Resized(size) => {
2058                if let Some(renderer) = self.renderer.as_mut() {
2059                    renderer.resize(size.width, size.height);
2060                }
2061                if let Some(window) = &self.window {
2062                    window.request_redraw();
2063                }
2064            }
2065
2066            WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
2067                self.scale = scale_factor as f32;
2068                // Reconfigure the surface to the current physical size.
2069                if let Some(window) = &self.window {
2070                    let size = window.inner_size();
2071                    if let Some(renderer) = self.renderer.as_mut() {
2072                        renderer.resize(size.width, size.height);
2073                    }
2074                }
2075                self.request_redraw();
2076            }
2077
2078            WindowEvent::Occluded(occluded) => {
2079                self.occluded = occluded;
2080                if !occluded {
2081                    self.request_redraw();
2082                }
2083            }
2084
2085            // Every pointer source, mouse and touch alike, converges on the
2086            // **normalised** `pointer()` input — gesture tier 0, with an explicit
2087            // `Cancel`. winit hands us physical px; we work in logical ones, the total
2088            // scale being DPI × density.
2089            WindowEvent::CursorMoved { position, .. } => {
2090                let scale = self.total_scale();
2091                let position = Point::new(position.x as f32 / scale, position.y as f32 / scale);
2092                self.pointer(
2093                    event_loop,
2094                    PointerEvent {
2095                        kind: PointerKind::Move,
2096                        position,
2097                        touch: false,
2098                    },
2099                );
2100            }
2101
2102            // The mouse left the window: nothing in it is under the pointer any more.
2103            WindowEvent::CursorLeft { .. } => {
2104                self.hover.left();
2105                self.sync_hover();
2106                self.sync_regions(false);
2107            }
2108
2109            WindowEvent::Touch(touch) => {
2110                let scale = self.total_scale();
2111                let position = Point::new(
2112                    touch.location.x as f32 / scale,
2113                    touch.location.y as f32 / scale,
2114                );
2115                let kind = match touch.phase {
2116                    TouchPhase::Started => PointerKind::Down,
2117                    TouchPhase::Moved => PointerKind::Move,
2118                    TouchPhase::Ended => PointerKind::Up,
2119                    TouchPhase::Cancelled => PointerKind::Cancel,
2120                };
2121                self.pointer(
2122                    event_loop,
2123                    PointerEvent {
2124                        kind,
2125                        position,
2126                        touch: true,
2127                    },
2128                );
2129            }
2130
2131            WindowEvent::ModifiersChanged(modifiers) => {
2132                let state = modifiers.state();
2133                self.shift = state.shift_key();
2134                self.ctrl = state.control_key();
2135                self.alt = state.alt_key();
2136                self.meta = state.super_key();
2137            }
2138
2139            WindowEvent::MouseInput {
2140                state: ElementState::Pressed,
2141                button: MouseButton::Left,
2142                ..
2143            } => self.pointer(
2144                event_loop,
2145                PointerEvent {
2146                    kind: PointerKind::Down,
2147                    position: self.cursor,
2148                    touch: false,
2149                },
2150            ),
2151
2152            WindowEvent::MouseInput {
2153                state: ElementState::Released,
2154                button: MouseButton::Left,
2155                ..
2156            } => self.pointer(
2157                event_loop,
2158                PointerEvent {
2159                    kind: PointerKind::Up,
2160                    position: self.cursor,
2161                    touch: false,
2162                },
2163            ),
2164
2165            // A right-click on a text field opens its selection bar (milestone 568), which is
2166            // the desktop's context menu for the four things it holds.
2167            WindowEvent::MouseInput {
2168                state: ElementState::Pressed,
2169                button: MouseButton::Right,
2170                ..
2171            } => self.context_click(),
2172
2173            WindowEvent::KeyboardInput { event, .. } if event.state == ElementState::Pressed => {
2174                self.key_down(&KeyDown::from(&event), Some(event_loop));
2175            }
2176
2177            WindowEvent::MouseWheel { delta, .. } => {
2178                let (mut dx, mut dy) = match delta {
2179                    MouseScrollDelta::LineDelta(x, y) => (x * SCROLL_SPEED, y * SCROLL_SPEED),
2180                    // Physical delta → logical.
2181                    MouseScrollDelta::PixelDelta(pos) => {
2182                        let scale = self.total_scale();
2183                        (pos.x as f32 / scale, pos.y as f32 / scale)
2184                    }
2185                };
2186                // With Shift, the wheel scrolls horizontally.
2187                if self.shift {
2188                    dx = dy;
2189                    dy = 0.0;
2190                }
2191                // Over an interactive viewport the wheel **zooms**, anchored at the
2192                // pointer and bounded by the widget's min and max scales.
2193                if let Some((id, viewport)) = self
2194                    .ui
2195                    .as_ref()
2196                    .and_then(|ui| ui.interactive_at(self.cursor))
2197                {
2198                    let (min, max) = self
2199                        .tree
2200                        .as_ref()
2201                        .and_then(|tree| find_widget(tree.as_ref(), id))
2202                        .and_then(|w| w.interactive())
2203                        .unwrap_or((0.5, 4.0));
2204                    // Wheel up (`dy > 0`) zooms in, in gentle steps of about 1.1× a notch.
2205                    let factor = (1.0 + dy * 0.1 / SCROLL_SPEED).clamp(0.2, 5.0);
2206                    // Zooming cuts off a fling in progress.
2207                    self.runtime.interactive_velocity.remove(&id);
2208                    let view = self.runtime.interactive.entry(id).or_default();
2209                    // Zoom anchored at the pointer, then bounded to the frame.
2210                    *view = view
2211                        .zoom_at(factor, self.cursor, min, max)
2212                        .clamped(viewport);
2213                    self.request_redraw();
2214                    return;
2215                }
2216                // A wheel asks the same question a finger does, and the reference gates it
2217                // the same way: an area with everything already in sight takes no offset,
2218                // and the notch belongs to whatever is behind it.
2219                let hit = {
2220                    let scroll = &self.runtime.scroll;
2221                    self.ui.as_ref().and_then(|ui| {
2222                        ui.scroll_chain(self.cursor).find(|area| {
2223                            area.accepts_user_offset(
2224                                scroll.get(&area.id).copied().unwrap_or((0.0, 0.0)),
2225                            )
2226                        })
2227                    })
2228                };
2229                if let Some(area) = hit {
2230                    // Scrolling with inertia: the wheel pushes the TARGET and the
2231                    // spring eases across to it. How far past the ends that target
2232                    // may go is the physics' call — a little elastic overshoot where
2233                    // the platform bounces, none at all where it does not.
2234                    let id = area.id;
2235                    let physics = area.physics;
2236                    let over = if physics.allows_overscroll() {
2237                        SCROLL_OVER
2238                    } else {
2239                        0.0
2240                    };
2241                    // A notch of the wheel is a new intent: it takes over from the
2242                    // momentum of the last gesture.
2243                    self.runtime.stop_scroll_fling(id);
2244                    let current = self.runtime.scroll.get(&id).copied().unwrap_or((0.0, 0.0));
2245                    // A screen delta into offsets: the content moves opposite the number,
2246                    // and a reversed axis counts from the other end. One function, so a
2247                    // reversed area is right at every one of the places this happens.
2248                    let (ddx, ddy) = area.offset_delta((dx, dy));
2249                    let target = self.runtime.scroll_target.entry(id).or_insert(current);
2250                    let (wanted_x, wanted_y) = (target.0 + ddx, target.1 + ddy);
2251                    target.0 = wanted_x.clamp(-over, area.max_x + over);
2252                    target.1 = wanted_y.clamp(-over, area.max_y + over);
2253                    let refused = (wanted_x - target.0, wanted_y - target.1);
2254                    self.runtime.scroll_velocity.entry(id).or_insert((0.0, 0.0));
2255                    // A notch that asks to go past the end deserves the same
2256                    // acknowledgement a finger gets: the wheel is a gesture too.
2257                    let cursor = self.cursor;
2258                    for (refused, vertical, extent) in [
2259                        (refused.0, false, area.viewport.width),
2260                        (refused.1, true, area.viewport.height),
2261                    ] {
2262                        if refused.abs() < 1e-3 {
2263                            continue;
2264                        }
2265                        let edge = area.refused_edge(vertical, refused);
2266                        let (offset, cross) =
2267                            frus_widgets::glow_cross_axis(area.viewport, edge, cursor);
2268                        self.runtime.glow_pull(
2269                            id,
2270                            area.overscroll,
2271                            edge,
2272                            refused,
2273                            extent,
2274                            offset,
2275                            cross,
2276                        );
2277                        // A wheel has no "lift off", so the pull is released at once
2278                        // and simply fades — otherwise it would hang until the hold
2279                        // timer expired.
2280                        self.runtime.glow_scroll_end(id);
2281                    }
2282                    self.request_redraw();
2283                }
2284            }
2285
2286            WindowEvent::RedrawRequested => {
2287                // Measured only when asked: a clock read per stage is cheap, but not free.
2288                let measuring = self.frame_stats.enabled();
2289                let frame_start = measuring.then(Instant::now);
2290                self.frame_costs = crate::frame_stats::FrameCosts::default();
2291                // Web: pick up the asynchronously initialised renderer as soon as it
2292                // is ready; until then, nothing is painted.
2293                #[cfg(web)]
2294                if self.renderer.is_none() {
2295                    match self.pending_renderer.borrow_mut().take() {
2296                        Some(mut renderer) => {
2297                            // The renderer was sized from whatever `inner_size()` read
2298                            // back when it was spawned — often 0×0, before the canvas
2299                            // had been laid out by CSS. A `Resized` event carrying the
2300                            // real size may have arrived and been dropped in the
2301                            // meantime, since `self.renderer` was still `None` then.
2302                            // Catching up here, against the size right now, is what
2303                            // makes that race harmless either way.
2304                            if let Some(window) = &self.window {
2305                                let size = window.inner_size();
2306                                renderer.resize(size.width.max(1), size.height.max(1));
2307                            }
2308                            self.renderer = Some(renderer);
2309                        }
2310                        None => return,
2311                    }
2312                }
2313                // A paste the browser has answered since the last frame goes in before
2314                // this one is built. Elsewhere a paste is answered on the spot and this
2315                // finds nothing.
2316                while let Some(pasted) = self.clipboard.take_answered() {
2317                    self.land_paste(pasted);
2318                }
2319                // A bar whose field is no longer the focused one is over: a field focused
2320                // again later must not find it open.
2321                if self
2322                    .runtime
2323                    .selection_toolbar
2324                    .is_some_and(|mark| self.runtime.input.focused != Some(mark.id))
2325                {
2326                    self.runtime.selection_toolbar = None;
2327                }
2328                // Occluded window: rendering is suspended, resuming on Occluded(false).
2329                if self.occluded {
2330                    return;
2331                }
2332                let size = self
2333                    .window
2334                    .as_ref()
2335                    .map(|w| w.inner_size())
2336                    .unwrap_or_default();
2337                // Minimised, or zero-sized: nothing to draw, which avoids GPU errors.
2338                if size.width == 0 || size.height == 0 {
2339                    self.last_frame = None; // no dt jump when it is restored
2340                    return;
2341                }
2342                // The interface is described in **logical** pixels; the GPU output is
2343                // scaled to physical ones (DPI × density) just before rendering.
2344                let scale = self.total_scale();
2345                let (width, height) = (size.width as f32 / scale, size.height as f32 / scale);
2346
2347                // A logical size change — a resize OR a density change — notifies the
2348                // app before the view, so it can react to the breakpoint in its logic.
2349                if self.last_size != Some((width, height)) {
2350                    self.last_size = Some((width, height));
2351                    self.build_dirty = true;
2352                    self.app.on_resize(width, height);
2353                }
2354
2355                // Window insets: separates the static **padding** — bars and notch —
2356                // from the **keyboard**, which is whatever bottom inset exceeds the
2357                // keyboard-free baseline. The baseline is taken at the first measurement
2358                // for this physical size (a rotation gives a new one), and corrects
2359                // downwards should a barer state appear: the keyboard open at startup,
2360                // the bars hidden, and so on.
2361                let raw = self.compute_insets(size.width, size.height, scale);
2362                let phys = (size.width, size.height);
2363                let mut baseline = match self.inset_baseline {
2364                    Some((b, s)) if s == phys => b,
2365                    _ => {
2366                        self.inset_baseline = Some((raw, phys));
2367                        raw
2368                    }
2369                };
2370                if raw.bottom < baseline.bottom {
2371                    baseline = raw;
2372                    self.inset_baseline = Some((raw, phys));
2373                }
2374                let insets = WindowInsets::from_baseline(baseline, raw);
2375                if self.last_insets != insets {
2376                    if insets.view_insets.bottom > self.last_insets.view_insets.bottom {
2377                        self.reveal_after_keyboard = true;
2378                    }
2379                    self.last_insets = insets;
2380                    self.build_dirty = true;
2381                    self.app.on_insets(insets);
2382                }
2383
2384                // The elapsed dt, clamped, for every animation.
2385                let now = Instant::now();
2386                let dt = self
2387                    .last_frame
2388                    .map(|prev| (now - prev).as_secs_f32().min(0.05))
2389                    .unwrap_or(0.0);
2390                self.last_frame = Some(now);
2391
2392                // A continuous clock, in seconds, for the time-driven animations.
2393                self.elapsed += dt;
2394                self.runtime.time = self.elapsed;
2395
2396                // The caret's blink (milestone 513), on the wall clock: the animation
2397                // clock above is clamped per frame and stands still between frames, and a
2398                // caret at rest is exactly when there are none. Solid while the window is
2399                // not the one in front, where nothing wakes the loop for it.
2400                let caret_now = Instant::now();
2401                let caret = if self.lifecycle == Lifecycle::Resumed {
2402                    self.caret_signature()
2403                } else {
2404                    None
2405                };
2406                self.caret.observe(caret_now, caret);
2407                self.runtime.caret_hidden = self.caret.hidden(caret_now);
2408
2409                // The application advances its own animations: navigation, gesture.
2410                let mut app_animating = self.app.tick(dt);
2411
2412                // And the framework advances the **theme**, which is its own work: it
2413                // resolves the application's themes against the brightness the platform
2414                // reports and the contrast the reader asked for, and crosses from the one
2415                // on screen to the one now wanted over `theme_animation_duration`.
2416                //
2417                // Both halves of that were the application's before milestone 452, and
2418                // both were being done by hand in this repo's own demonstration — which
2419                // is a fair sign of what every application was having to write.
2420                let settings = self
2421                    .platform
2422                    .accessibility
2423                    .with_overrides(self.app.accessibility());
2424
2425                // **The ambient scopes, before anything reads them** — which the theme
2426                // does: the direction of the layout follows the language, so an
2427                // application whose theme is right-to-left in Arabic needs the language
2428                // installed before its theme is asked for. This used to sit further down,
2429                // beside the build, where it was in time for the widgets and one frame
2430                // late for the theme.
2431                //
2432                // Read every frame, all of it: these are the application's answers and
2433                // they may change while it is running.
2434                install_ambient(&self.app, &self.platform.locales);
2435
2436                let theme_moved = self.themes.advance(
2437                    &self.app,
2438                    self.platform.brightness,
2439                    settings.high_contrast,
2440                    settings.disable_animations,
2441                    dt,
2442                );
2443                // A theme that moved is a rebuild — the view is a pure function of
2444                // `(state, theme, size)` — and a fade still running is another frame.
2445                app_animating |= theme_moved | self.themes.animating();
2446                let theme = self.themes.displayed(&self.app);
2447
2448                // **The surface, in force for the whole frame** — not just for `view`.
2449                // A size becomes a number in three places: while the widgets are built,
2450                // while they are measured and laid out, and while they are painted.
2451                // Scoping the build alone left the last two at scale 1, so the layout
2452                // measured one size and the renderer drew another, and the reader's
2453                // setting reached a device without moving a single pixel (milestone 407).
2454                //
2455                // One guard, not two: the description and the font size are installed by
2456                // the same call and released by the same drop, so they cannot be held for
2457                // different lengths of time. Holding them separately is the same bug with
2458                // an extra step (milestone 408). It lives to the end of this frame and
2459                // puts back what was there, panic or not.
2460                let _surface = self.media_query(width, height).install();
2461
2462                // === BUILD phase, conditional ===
2463                // The `view` is rebuilt only when the app's state or the size changed
2464                // (`build_dirty`), or when the app is animating, theme, navigation and
2465                // gesture all altering the state the view reads. A frame that animates
2466                // interaction alone — hover, scroll, focus, caret — **reuses the
2467                // retained tree** and merely repaints: the `view` is a pure function of
2468                // `(state, theme, size)` and never of the `Runtime`.
2469                // The user's motion setting reaches the runtime before anything is
2470                // advanced with it, and it is read every frame: a settings screen with a
2471                // *reduce motion* switch changes it while the application is running.
2472                self.runtime.still = settings.disable_animations;
2473                // And where the pointer stands in relation to a bar, from the previous
2474                // frame's registry — the only one there is at this point in the frame.
2475                //
2476                // Near a bar, it holds it open and warms the thumb; a bar that has faded
2477                // out entirely still answers here, and comes back for the reach. On a
2478                // touch platform nothing draws a bar in the first place, so the registry
2479                // is empty and a lingering finger position says nothing.
2480                self.runtime.scrollbar_hovered = self
2481                    .ui
2482                    .as_ref()
2483                    .and_then(|ui| ui.scrollbar_near(self.cursor))
2484                    .map(|bar| bar.id);
2485                self.runtime.scrollbar_dragged = match self.drag {
2486                    Some(Drag::Scrollbar { id, .. }) => Some(id),
2487                    _ => None,
2488                };
2489                // A switcher in flight carries each child's progress as a number built into
2490                // the tree, so it is rebuilt — not only repainted — until the switch settles.
2491                // Something a component keeps changed — a `set_state`, a hook, a controller —
2492                // and what it builds is built again.
2493                // What components asked of the shell since the last frame — a focus, a scroll,
2494                // work for another thread — is run first: it may ask for this very rebuild.
2495                let asked = self.app.effects();
2496                self.run_command(asked);
2497                #[cfg(web)]
2498                self.sync_address();
2499                #[cfg(desktop)]
2500                self.open_links_from_other_instances();
2501                // Work a component started brought something: build again to show it.
2502                let arrived = self.take_task_arrivals();
2503                if frus_widgets::take_rebuild_request() || arrived {
2504                    self.build_dirty = true;
2505                }
2506                let need_build = frame_needs_build(
2507                    self.build_dirty,
2508                    app_animating,
2509                    std::mem::replace(&mut self.app_was_animating, app_animating),
2510                    self.tree.is_none() || self.runtime.switching(),
2511                );
2512                if need_build {
2513                    // No scope of its own: the surface above is already installed, and
2514                    // covers the layout and the paint that follow as well.
2515                    //
2516                    // `build_view`, not `view`: `collect_ids` below reads this tree before
2517                    // the layout pass has been down it, so an unprepared tree would report
2518                    // no identities at all inside a deferred subtree — and everything in an
2519                    // `AppBar` would silently never mount, never fade in and never fade out.
2520                    let build_start = measuring.then(Instant::now);
2521                    let tree = build_view(&self.app, &theme, &self.runtime);
2522                    if let Some(at) = build_start {
2523                        self.frame_costs.build = Some(at.elapsed().as_secs_f32() * 1000.0);
2524                    }
2525                    let ids = collect_ids(tree.as_ref());
2526                    let present: std::collections::HashSet<_> = ids.iter().copied().collect();
2527
2528                    // Leaving: snapshot the widgets present at N-1 but absent at N,
2529                    // so they can be faded out.
2530                    let leaving: std::collections::HashSet<u64> = self
2531                        .runtime
2532                        .mounted
2533                        .iter()
2534                        .filter(|id| !present.contains(id))
2535                        .map(|id| id.as_u64())
2536                        .collect();
2537                    if !leaving.is_empty() {
2538                        if let Some(ui) = &self.ui {
2539                            let captured: Vec<_> = ui
2540                                .scene()
2541                                .primitives()
2542                                .iter()
2543                                .filter(|p| leaving.contains(&p.owner()))
2544                                .cloned()
2545                                .collect();
2546                            if !captured.is_empty() {
2547                                self.runtime
2548                                    .leaving
2549                                    .insert(self.leaving_counter, (captured, 1.0));
2550                                self.leaving_counter = self.leaving_counter.wrapping_add(1);
2551                            }
2552                        }
2553                    }
2554
2555                    // Mounting: new widgets start out fading in.
2556                    for &id in &ids {
2557                        if self.runtime.mounted.insert(id) {
2558                            self.runtime.anims.entry(id).or_default().opacity = 0.0;
2559                        }
2560                    }
2561                    self.runtime.mounted.retain(|id| present.contains(id));
2562
2563                    self.tree = Some(tree);
2564                    // What the build asked for may be a timer — a component's
2565                    // `use_interval` — and it is the build that says so, which is later than
2566                    // the message that changed it. Diffing here starts and stops it now, not
2567                    // on the next message.
2568                    self.sync_subscriptions();
2569
2570                    // The configuration may have changed, so invalidate the paint
2571                    // cache and its repaint boundaries. Entries from a stale generation
2572                    // no longer *hit*, giving a full repaint this frame and reuse on the
2573                    // interaction-only frames that follow.
2574                    self.runtime.paint_cache.borrow_mut().bump_generation();
2575                }
2576                self.build_dirty = false;
2577
2578                // Focus requests (`Command::focus`), resolved against the tree just
2579                // (re)built — the key names the field wrapped in `keyed(k, …)`. The most
2580                // recent one that resolves wins, and the focus ring becomes visible
2581                // again, since we jump to the field as the keyboard would.
2582                if !self.pending_focus.is_empty() {
2583                    let keys = std::mem::take(&mut self.pending_focus);
2584                    let ids: Vec<WidgetId> = self
2585                        .tree
2586                        .as_deref()
2587                        .map(|tree| keys.iter().filter_map(|&k| find_by_key(tree, k)).collect())
2588                        .unwrap_or_default();
2589                    if let Some(&id) = ids.last() {
2590                        self.runtime.input.focused = Some(id);
2591                        self.runtime.focus_visible = true;
2592                    }
2593                }
2594
2595                // === PAINT phase ===
2596                // The retained tree is (re)painted. Layout goes through the relayout
2597                // cache (milestone 55: taffy is called again only when the structure
2598                // changed); painting goes through the repaint cache (milestone 88: a
2599                // A row carried past the end of the list: the list comes to meet it.
2600                // Before the tree is borrowed for the rest of the frame, because it moves
2601                // an offset the build below is about to read — and this frame, not the
2602                // next one, or the content would lag a frame behind the finger.
2603                let autoscrolling = self.autoscroll_carried(dt);
2604
2605                // The reorder spring, per axis, with a time constant of about 70 ms:
2606                // - **horizontal** (`Table` columns): the smoothed `reorder_x` catches up
2607                //   with the pointer — the columns slide with inertia while the ghost
2608                //   sticks to the real pointer;
2609                // - **vertical** (Kanban cards): the smoothed `reorder_y` catches up with
2610                //   the **chosen** slot edge, the hovered half — the insertion line and
2611                //   the gap *slide* between cards instead of jumping, which is the
2612                //   vertical counterpart of `reorder_x`;
2613                // - **a horizontal list's rows**: `reorder_x` catches up with the chosen
2614                //   slot edge, the same line turned on its side.
2615                let reorder_animating = self.advance_reorder_springs(dt);
2616
2617                // static `RepaintBoundary` subtree is replayed without repainting while
2618                // its geometry and the interaction state hold still).
2619                let tree = self
2620                    .tree
2621                    .as_deref()
2622                    .expect("the view was built at least once");
2623
2624                // Scroll and pan inertia; the bounds and viewports come from the
2625                // previous frame.
2626                let scroll_regions = self
2627                    .ui
2628                    .as_ref()
2629                    .map(|ui| ui.scroll_regions().to_vec())
2630                    .unwrap_or_default();
2631                // The refresh areas of the frame, with the `refreshing` flag each was
2632                // built with: that flag is what tells a spinning indicator when to stop.
2633                let refresh_areas = self
2634                    .ui
2635                    .as_ref()
2636                    .map(|ui| ui.refresh_areas().to_vec())
2637                    .unwrap_or_default();
2638                let dismissables = self
2639                    .ui
2640                    .as_ref()
2641                    .map(|ui| ui.dismissables().to_vec())
2642                    .unwrap_or_default();
2643                // Filled by the dismissal step below, dispatched once the tree is no
2644                // longer borrowed.
2645                let mut dismissed: Vec<A::Message> = Vec::new();
2646                let sheet_areas = self
2647                    .ui
2648                    .as_ref()
2649                    .map(|ui| ui.sheets().to_vec())
2650                    .unwrap_or_default();
2651                let interactive_bounds = self
2652                    .ui
2653                    .as_ref()
2654                    .map(|ui| ui.interactive_bounds())
2655                    .unwrap_or_default();
2656
2657                // A paged view that has been asked for another page glides across to
2658                // it. Done before the springs are stepped, so the request is honoured
2659                // in the frame it arrives rather than the one after.
2660                self.runtime.sync_pages(&scroll_regions);
2661                // And a bar whose selected item has moved out of its window slides back
2662                // to it. Same moment, same reason: the request is honoured in the frame
2663                // it arrives rather than the one after.
2664                self.runtime.sync_visible(&scroll_regions);
2665                // And the scroll requests an application has just returned. Same moment
2666                // and the same reason, with one addition: a region that has only just
2667                // appeared is not in the registry above, which was built last frame, so
2668                // what does not resolve here is tried again below against this frame's.
2669                let requests = std::mem::take(&mut self.pending_scroll);
2670                let (retry, scrolled) =
2671                    apply_scroll_requests(&mut self.runtime, tree, requests, &scroll_regions);
2672                self.retry_scroll = retry;
2673                // And the sheets it has asked to move, on the same terms: before the sheets
2674                // are stepped, so an animated request starts in the frame it arrives in.
2675                let requests = std::mem::take(&mut self.pending_sheet);
2676                let (retry, sheeted) =
2677                    apply_sheet_requests(&mut self.runtime, tree, requests, &sheet_areas);
2678                self.retry_sheet = retry;
2679
2680                // Every family is stepped, in this order, whatever an earlier one answered; each
2681                // answer is kept by name, so that frame statistics can say which kept the frames
2682                // coming (milestone 609).
2683                let moving: [(&str, bool); 25] = [
2684                    ("scrolled", scrolled),
2685                    ("sheeted", sheeted),
2686                    ("anims", self.runtime.advance(dt)),
2687                    ("leaving", self.runtime.advance_leaving(dt)),
2688                    ("switchers", self.runtime.advance_switchers(tree, dt)),
2689                    ("values", self.runtime.advance_values(tree, dt)),
2690                    ("colors", self.runtime.advance_colors(tree, dt)),
2691                    ("sizes", self.runtime.advance_sizes(tree, dt)),
2692                    ("radii", self.runtime.advance_radii(tree, dt)),
2693                    ("paddings", self.runtime.advance_paddings(tree, dt)),
2694                    ("offsets", self.runtime.advance_offsets(tree, dt)),
2695                    ("pins", self.runtime.advance_pins(tree, dt)),
2696                    ("fractions", self.runtime.advance_fractions(tree, dt)),
2697                    ("text_styles", self.runtime.advance_text_styles(tree, dt)),
2698                    ("transforms", self.runtime.advance_transforms(tree, dt)),
2699                    ("scroll", self.runtime.advance_scroll(&scroll_regions, dt)),
2700                    ("glow", self.runtime.advance_glow(dt)),
2701                    ("refresh", self.runtime.advance_refresh(&refresh_areas, dt)),
2702                    ("dismiss", {
2703                        // A dismissed item announces itself only once its gap has
2704                        // finished closing. The messages are *collected* here and
2705                        // dispatched below: the retained tree is borrowed for the whole
2706                        // of this frame, and `dispatch` rebuilds it.
2707                        let (moving, done) = self.runtime.advance_dismiss(&dismissables, dt);
2708                        dismissed.extend(done.into_iter().filter_map(|(id, direction)| {
2709                            find_widget(tree, id).and_then(|widget| widget.on_dismissed(direction))
2710                        }));
2711                        moving
2712                    }),
2713                    ("sheets", {
2714                        // A sheet lowered to nothing says so once it has arrived there,
2715                        // collected like a dismissal and for the same reason.
2716                        let (moving, closed) = self.runtime.advance_sheets(&sheet_areas, dt);
2717                        dismissed.extend(closed.into_iter().filter_map(|id| {
2718                            find_widget(tree, id).and_then(|widget| widget.on_sheet_dismissed())
2719                        }));
2720                        // A throw that carried a sheet to full height goes on into its
2721                        // list, under the list's own physics (milestone 520).
2722                        let mut handed = false;
2723                        for (list, velocity) in self.runtime.take_sheet_handovers() {
2724                            if let Some(area) = scroll_regions.iter().find(|a| a.id == list) {
2725                                handed |=
2726                                    self.runtime
2727                                        .fling_scroll(*area, area.physics, (0.0, velocity));
2728                            }
2729                        }
2730                        moving | handed
2731                    }),
2732                    (
2733                        "interactive",
2734                        self.runtime.advance_interactive(&interactive_bounds, dt),
2735                    ),
2736                    ("ink", self.runtime.advance_ink(dt)),
2737                    ("reorder", reorder_animating),
2738                    ("autoscroll", autoscrolling),
2739                    ("app", app_animating),
2740                ];
2741                let animating = moving.iter().any(|(_, on)| *on);
2742                self.frame_stats.note_moving(&moving);
2743                // With the inspector on, the same build collects the observed nodes,
2744                // and the overlay — outlines plus a card for the hovered widget — is
2745                // painted on top of a copy of the scene.
2746                let layout_start = measuring.then(Instant::now);
2747                let (ui, scene) = if self.inspector {
2748                    let (ui, nodes) = frus_widgets::build_ui_inspected(
2749                        tree,
2750                        Size::new(width, height),
2751                        &self.runtime,
2752                        &theme,
2753                    );
2754                    if std::mem::take(&mut self.inspector_dump) {
2755                        // Through `log`, so it reaches logcat and the browser too.
2756                        log::info!(target: "frus::inspector", "
2757{}", frus_widgets::dump_tree(&nodes));
2758                    }
2759                    let mut scene = ui.scene().clone();
2760                    frus_widgets::paint_inspector_overlay(
2761                        &nodes,
2762                        Some(self.cursor),
2763                        Size::new(width, height),
2764                        &theme,
2765                        &mut scene,
2766                    );
2767                    // Scene: logical → physical (DPI × density).
2768                    (ui, scene.scaled(scale))
2769                } else {
2770                    let ui = build_ui(tree, Size::new(width, height), &self.runtime, &theme);
2771                    // The preview of a column reorder, or a lifted item, on top of the
2772                    // scene.
2773                    let scene = if matches!(self.drag, Some(Drag::Reorder { moved: true, .. })) {
2774                        let mut scene = ui.scene().clone();
2775                        self.paint_reorder_preview(&ui, &theme, &mut scene);
2776                        scene.scaled(scale)
2777                    } else if matches!(self.drag, Some(Drag::Item { moved: true, .. })) {
2778                        let mut scene = ui.scene().clone();
2779                        self.paint_drag_ghost(&ui, &theme, &mut scene);
2780                        scene.scaled(scale)
2781                    } else {
2782                        // Scene: logical → physical (DPI × density), for a crisp render.
2783                        ui.scene().scaled(scale)
2784                    };
2785                    (ui, scene)
2786                };
2787                if let Some(at) = layout_start {
2788                    self.frame_costs.layout_paint = at.elapsed().as_secs_f32() * 1000.0;
2789                    self.frame_costs.layers = count_layers(scene.primitives());
2790                }
2791                self.report_overflows(&ui);
2792                // Laid out: the components the build no longer reached are let go, and the
2793                // effects it asked for run. An effect that changed something asks for the
2794                // frame that shows it.
2795                self.runtime.states.end_frame();
2796                if frus_widgets::rebuild_requested() {
2797                    self.request_redraw();
2798                }
2799                // A see-through frame starts transparent: the window's background under
2800                // everything but the line the system paints (milestone 643).
2801                let scene = if self.renderer.as_ref().is_some_and(Renderer::sees_through) {
2802                    backdrop_under(scene, width * scale, height * scale, self.system_line())
2803                } else {
2804                    scene
2805                };
2806                if let Some(renderer) = self.renderer.as_mut() {
2807                    let render_start = measuring.then(Instant::now);
2808                    match renderer.render(&scene) {
2809                        frus_gpu::RenderOutcome::Presented => {}
2810                        frus_gpu::RenderOutcome::NeedsReconfigure => {
2811                            renderer.reconfigure();
2812                        }
2813                        frus_gpu::RenderOutcome::Skipped => {}
2814                    }
2815                    if let Some(at) = render_start {
2816                        self.frame_costs.render = at.elapsed().as_secs_f32() * 1000.0;
2817                        let timings = renderer.last_timings();
2818                        self.frame_costs.acquire = timings.acquire;
2819                        self.frame_costs.draw = timings.draw;
2820                        self.frame_costs.gpu = timings.gpu;
2821                    }
2822                }
2823
2824                // A continuously animating widget, a spinner say, forces a redraw. So
2825                // does an image still on its way: without a frame to draw it in, the
2826                // frame that would show it never happens.
2827                //
2828                // The fetch is asked about **here** rather than inside the tree, and as a
2829                // count rather than a flag on the widget: showing a placeholder means
2830                // taking the image out of the tree, so a hook read off `Image` would go
2831                // quiet at exactly the moment it is needed. Asking here also keeps `Ui`
2832                // answering for its own widgets alone (milestone 411).
2833                let widget_asks = ui.wants_animation();
2834                let loading =
2835                    frus_widgets::images_in_flight() > 0 || frus_widgets::tasks_in_flight() > 0;
2836                let wants_animation = widget_asks || loading;
2837
2838                // Keep the interface, for hit testing. The tree is already retained.
2839                self.ui = Some(ui);
2840                // The keyboard took room from the bottom of the window: what the reader is
2841                // typing in is brought back above it, by the least scroll that does it, and
2842                // by the same glide Tab uses. This frame's layout is the shortened one, so
2843                // the viewports asked are the ones left above the keyboard. A frame with
2844                // nothing focused, or no region around what is, moves nothing.
2845                let revealed = std::mem::take(&mut self.reveal_after_keyboard) && {
2846                    let moves = match (self.runtime.input.focused, self.ui.as_ref()) {
2847                        (Some(id), Some(ui)) => ui.reveal(id, &self.runtime),
2848                        _ => Vec::new(),
2849                    };
2850                    let moved = !moves.is_empty();
2851                    for (area, offset) in moves {
2852                        self.runtime.scroll_ballistic.remove(&area);
2853                        self.runtime.scroll_velocity.remove(&area);
2854                        self.runtime.scroll_target.insert(area, offset);
2855                    }
2856                    moved
2857                };
2858                // The tree may have changed under a still pointer — a tap that opened a
2859                // screen — so what it is over is asked of this frame, as the reference
2860                // does after every frame. Not during a drag: a slider dragged past its
2861                // end is still the one being pressed, and the move path leaves it so.
2862                //
2863                // Field by field rather than through `sync_hover`, which borrows the whole
2864                // shell: the frame still holds part of it here. A change is shown the way
2865                // the reference shows it, on the next frame — the tree is built again,
2866                // since a tooltip's bubble is decided while the tree is walked.
2867                let rehovered = self.drag.is_none() && {
2868                    let hovered = self.ui.as_ref().and_then(|ui| self.hover.target(ui));
2869                    let changed = hovered != self.runtime.input.hovered;
2870                    self.runtime.input.hovered = hovered;
2871                    changed
2872                };
2873                if rehovered {
2874                    self.build_dirty = true;
2875                }
2876                let wants_animation = wants_animation || rehovered || revealed;
2877
2878                // The paged views that have just turned a page, read off **this**
2879                // frame's regions: a page change is worth reporting the moment it
2880                // reads as one, not a frame later.
2881                let paged = self
2882                    .ui
2883                    .as_ref()
2884                    .map(|ui| ui.scroll_regions().to_vec())
2885                    .unwrap_or_default();
2886                // A region whose content shrank this frame under an offset nobody owns was
2887                // drawn where it now rests; the runtime keeps it there before anything reads
2888                // the offset back — the reports below, a press, the next frame's springs.
2889                // Without it the row was drawn right once and pressed wrong (milestone 533).
2890                self.runtime.keep_scroll_in_range(&paged);
2891                let turned: Vec<A::Message> = self
2892                    .runtime
2893                    .page_changes(&paged)
2894                    .into_iter()
2895                    .filter_map(|(id, page)| {
2896                        find_widget(tree, id).and_then(|widget| widget.on_page_changed(page))
2897                    })
2898                    .collect();
2899
2900                // The scroll requests the previous frame's registry could not place:
2901                // tried once against **this** frame's, which is where a region that has
2902                // only just appeared turns up. This is the last chance they get.
2903                let (_, moved_late) = apply_scroll_requests(
2904                    &mut self.runtime,
2905                    tree,
2906                    std::mem::take(&mut self.retry_scroll),
2907                    &paged,
2908                );
2909                // The same for the sheet requests: a sheet shown by the very message that
2910                // asked it to move is in this frame's sheets and not the last one's.
2911                let (_, sheeted_late) = {
2912                    let sheets = self
2913                        .ui
2914                        .as_ref()
2915                        .map(|ui| ui.sheets().to_vec())
2916                        .unwrap_or_default();
2917                    apply_sheet_requests(
2918                        &mut self.runtime,
2919                        tree,
2920                        std::mem::take(&mut self.retry_sheet),
2921                        &sheets,
2922                    )
2923                };
2924
2925                // The regions that have moved since they last said so, read off **this**
2926                // frame's registry so that the offset and the extents reported together
2927                // come from the same frame. A region nobody is listening to costs the
2928                // comparison and nothing else.
2929                let scrolled: Vec<A::Message> = {
2930                    let grain =
2931                        |id| find_widget(tree, id).map_or(0.0, |widget| widget.scroll_grain());
2932                    self.runtime
2933                        .scroll_changes(&paged, grain)
2934                        .into_iter()
2935                        .filter_map(|(id, position)| {
2936                            find_widget(tree, id).and_then(|widget| widget.on_scroll(position))
2937                        })
2938                        .collect()
2939                };
2940
2941                // The rows whose gap has just finished closing, the pages that have
2942                // turned and the regions that have moved: the tree is no longer
2943                // borrowed, so the application can be told — and rebuild.
2944                for message in dismissed.into_iter().chain(turned).chain(scrolled) {
2945                    self.dispatch(message);
2946                }
2947                // A screen that changed under a still pointer enters and leaves regions too,
2948                // as the reference's mouse tracker does after every frame (milestone 583).
2949                self.sync_regions(false);
2950                if moved_late || sheeted_late {
2951                    // Nothing else this frame knows the offset changed: the springs ran
2952                    // before the request was placed.
2953                    self.request_redraw();
2954                }
2955
2956                // Focus return: when the focused widget has **vanished**, an overlay
2957                // having closed, go back to the trigger. Done before the AccessKit
2958                // announcement, so the focus it publishes is current.
2959                self.reconcile_focus();
2960
2961                // Publish the frame's semantic tree to AccessKit.
2962                #[cfg(desktop)]
2963                if let Some(a11y) = self.a11y.as_mut() {
2964                    let focus = self.runtime.input.focused;
2965                    let title = self.app.title();
2966                    if let Some(ui) = self.ui.as_ref() {
2967                        a11y.update(ui.semantics(), focus, &title, &self.announce);
2968                    }
2969                }
2970                // Publish the frame's semantic tree to the DOM bridge (#18).
2971                #[cfg(web)]
2972                if let Some(a11y) = self.a11y.as_mut() {
2973                    let focus = self.runtime.input.focused;
2974                    let title = self.app.title();
2975                    if let Some(ui) = self.ui.as_ref() {
2976                        a11y.update(ui.semantics(), focus, &title, &self.announce);
2977                    }
2978                }
2979
2980                // The system bars follow the frame: the regions under them, then the theme.
2981                self.sync_system_bars(&theme);
2982
2983                // The Android software keyboard follows the text fields' focus.
2984                self.sync_soft_input();
2985                // And the autofill service follows the form being edited.
2986                #[cfg(android)]
2987                self.sync_autofill();
2988
2989                // While an animation is running, ask for another frame.
2990                if animating || wants_animation {
2991                    self.request_redraw();
2992                }
2993                if let Some(start) = frame_start {
2994                    use crate::frame_stats::Why;
2995                    let why = [
2996                        (app_animating, Why::APP),
2997                        (animating, Why::RUNTIME),
2998                        (widget_asks, Why::WIDGET),
2999                        (loading, Why::LOADING),
3000                        (rehovered || revealed, Why::POINTER),
3001                    ];
3002                    self.frame_costs.why = why
3003                        .into_iter()
3004                        .filter(|(on, _)| *on)
3005                        .fold(Why::NONE, |all, (_, why)| all | why);
3006                    self.frame_stats.frame(start, self.frame_costs);
3007                }
3008                // The title bar's line, shared with the content when it asks (milestone 640).
3009                #[cfg(windows)]
3010                self.sync_title_bar();
3011            }
3012
3013            _ => {}
3014        }
3015    }
3016}
3017
3018impl<A: Application> App<A> {
3019    /// How tall the title bar's line the system paints is, in physical pixels: `0` where
3020    /// it paints none (milestone 643).
3021    fn system_line(&self) -> f32 {
3022        // The line the content was told: the one its row covers.
3023        match self.title_bar {
3024            Some(line) if self.title_bar_on && line.system_paints => {
3025                line.height * self.total_scale()
3026            }
3027            _ => 0.0,
3028        }
3029    }
3030
3031    /// The total scale: system DPI × app density (physical = logical × this).
3032    fn total_scale(&self) -> f32 {
3033        (self.scale * self.app.density()).max(0.1)
3034    }
3035
3036    /// The surface description installed around every call to `view`, so that any
3037    /// widget built there can read it with `MediaQuery::of()` instead of having the
3038    /// application carry it down by hand.
3039    ///
3040    /// Everything in it is already known to the shell — the logical size it is about
3041    /// to lay out for, the DPI scale, the app's density, and the insets last reported
3042    /// by the platform. It is assembled here, in one place, rather than at each of the
3043    /// call sites.
3044    /// The **platform** answers, and the application overrides what it chose to speak for.
3045    ///
3046    /// That order is the one the reference uses and the only one that stays honest: the
3047    /// settings belong to the person using the device, so the framework asks them first,
3048    /// and an application with a *reduce motion* switch of its own says only that. An
3049    /// [`AccessibilityOverrides`] whose fields are all `None` — the default — leaves every
3050    /// answer to the user, which is what an application that has no settings screen means.
3051    fn media_query(&self, width: f32, height: f32) -> MediaQuery {
3052        MediaQuery::new(Size::new(width, height))
3053            .with_device_pixel_ratio(self.scale)
3054            .with_density(self.app.density())
3055            .with_text_scaler(self.platform.text_scaler)
3056            .with_platform_brightness(self.platform.brightness)
3057            .with_accessibility(
3058                self.platform
3059                    .accessibility
3060                    .with_overrides(self.app.accessibility()),
3061            )
3062            .with_insets(self.last_insets)
3063            .with_title_bar(self.title_bar)
3064    }
3065
3066    /// **Shares the window's title bar line with the content** when the frame asks for it,
3067    /// gives it back when it no longer does, and tells the content what the system keeps on
3068    /// it (milestone 640). See `title_bar.rs`.
3069    #[cfg(windows)]
3070    fn sync_title_bar(&mut self) {
3071        use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
3072        let Some(window) = self.window.clone() else {
3073            return;
3074        };
3075        let hwnd = match window.window_handle().map(|h| h.as_raw()) {
3076            Ok(RawWindowHandle::Win32(win)) => {
3077                win.hwnd.get() as windows_sys::Win32::Foundation::HWND
3078            }
3079            _ => return,
3080        };
3081        let wants = self.ui.as_ref().is_some_and(|ui| ui.wants_title_bar());
3082        if wants != self.title_bar_on {
3083            // SAFETY: the window is alive and was made on this thread.
3084            unsafe {
3085                if wants {
3086                    crate::title_bar::enable(hwnd);
3087                    let wake = window.clone();
3088                    crate::title_bar::on_change(move || wake.request_redraw());
3089                } else {
3090                    crate::title_bar::disable(hwnd);
3091                }
3092            }
3093            self.title_bar_on = wants;
3094        }
3095        let next = if self.title_bar_on {
3096            let scale = self.total_scale();
3097            let band = crate::title_bar::height() as f32 / scale;
3098            // What the content has on the line is its own; the rest moves the window.
3099            if let Some(ui) = &self.ui {
3100                let to_px = |r: frus_widgets::Rect| windows_sys::Win32::Foundation::RECT {
3101                    left: (r.x * scale).floor() as i32,
3102                    top: (r.y * scale).floor() as i32,
3103                    right: ((r.x + r.width) * scale).ceil() as i32,
3104                    bottom: ((r.y + r.height) * scale).ceil() as i32,
3105                };
3106                let interactive = ui
3107                    .hit_rects()
3108                    .filter(|r| r.y < band && r.width < self.logical_width())
3109                    .map(to_px)
3110                    .collect();
3111                let icon = ui
3112                    .title_bar_regions()
3113                    .iter()
3114                    .find(|(_, role)| *role == frus_widgets::TitleBarRole::Icon)
3115                    .map(|(r, _)| to_px(*r));
3116                crate::title_bar::publish(interactive, icon);
3117            }
3118            // SAFETY: as above.
3119            let buttons = unsafe { crate::title_bar::buttons(hwnd) }.map(|r| {
3120                frus_widgets::Rect::new(
3121                    r.left as f32 / scale,
3122                    r.top as f32 / scale,
3123                    (r.right - r.left) as f32 / scale,
3124                    (r.bottom - r.top) as f32 / scale,
3125                )
3126            });
3127            let button = |code: u32| {
3128                use windows_sys::Win32::UI::WindowsAndMessaging::{
3129                    HTCLOSE, HTMAXBUTTON, HTMINBUTTON,
3130                };
3131                match code {
3132                    HTMINBUTTON => Some(frus_widgets::CaptionButton::Minimize),
3133                    HTMAXBUTTON => Some(frus_widgets::CaptionButton::Maximize),
3134                    HTCLOSE => Some(frus_widgets::CaptionButton::Close),
3135                    _ => None,
3136                }
3137            };
3138            let (hovered, pressed) = crate::title_bar::buttons_state();
3139            if self.window_icon.is_none() {
3140                self.window_icon = self
3141                    .app
3142                    .icon()
3143                    .png()
3144                    .map(|png| &*Box::leak(png.to_vec().into_boxed_slice()));
3145            }
3146            // The caption the system would draw: dark as the window's own, which follows the
3147            // system's apps (milestone 642).
3148            let active = window.has_focus();
3149            let dark = window.theme() == Some(winit::window::Theme::Dark);
3150            let (background, foreground) = crate::title_bar::caption_colors(dark, active);
3151            Some(frus_widgets::TitleBar {
3152                // The line ends where the system's buttons do: maximized, the window's
3153                // resizing border is off the screen and the buttons sit higher, and the words
3154                // centred in the line stay level with them (milestone 643).
3155                height: buttons.map_or(band, |b| (b.y + b.height).clamp(1.0, band)),
3156                leading: 0.0,
3157                buttons,
3158                icon: self.window_icon,
3159                hovered: button(hovered),
3160                pressed: button(pressed),
3161                maximized: window.is_maximized(),
3162                active,
3163                background: Some(background),
3164                foreground: Some(foreground),
3165                // The system shows its own caption through a see-through frame, where it
3166                // paints a backdrop on the line (milestone 643).
3167                system_paints: self.renderer.as_ref().is_some_and(Renderer::sees_through)
3168                    // SAFETY: as above.
3169                    && unsafe { crate::title_bar::system_backdrop(hwnd, dark) },
3170            })
3171        } else {
3172            None
3173        };
3174        if next != self.title_bar {
3175            log::debug!(
3176                target: "frus::title_bar",
3177                "the line: {:?}",
3178                next.map(|line| (line.height, line.buttons, line.active, line.system_paints))
3179            );
3180            self.title_bar = next;
3181            self.build_dirty = true;
3182            self.request_redraw();
3183        }
3184    }
3185
3186    /// The current layout direction; RTL flips both the layout and the gestures.
3187    fn is_rtl(&self) -> bool {
3188        self.themes.displayed(&self.app).direction.is_rtl()
3189    }
3190
3191    /// The window's **logical** width, in px, for the edge thresholds.
3192    fn logical_width(&self) -> f32 {
3193        let scale = self.total_scale();
3194        self.window
3195            .as_ref()
3196            .map(|w| w.inner_size().width as f32 / scale)
3197            .unwrap_or(1.0)
3198            .max(1.0)
3199    }
3200
3201    fn request_redraw(&self) {
3202        if let Some(window) = &self.window {
3203            window.request_redraw();
3204        }
3205    }
3206
3207    /// The **normalised** pointer input (gesture tier 0): mouse and touch converge
3208    /// here, with an explicit `Cancel`. The long-press recogniser is fed along the way,
3209    /// and the loop is woken at its deadline.
3210    fn pointer(&mut self, event_loop: &ActiveEventLoop, event: PointerEvent) {
3211        self.pointer_event(event);
3212        // Wake the loop exactly at the next deadline, and rest otherwise.
3213        event_loop.set_control_flow(self.idle_control_flow());
3214    }
3215
3216    /// Everything [`pointer`](Self::pointer) does with an event but wake the loop: the one
3217    /// place a press, a movement and a release are routed, and so what a test drives. The
3218    /// regions the pointer is in are brought up to date after it (milestone 583).
3219    fn pointer_event(&mut self, event: PointerEvent) {
3220        let kind = event.kind;
3221        // The raw events first, against the frame the pointer is over: a listener hears the
3222        // press before anything it leads to rebuilds the tree (milestone 586).
3223        self.cursor = event.position;
3224        self.route_listeners(kind, event.touch);
3225        self.route_pointer_event(event);
3226        self.pointer_held = match kind {
3227            PointerKind::Down => true,
3228            PointerKind::Up | PointerKind::Cancel => false,
3229            PointerKind::Move => self.pointer_held,
3230        };
3231        self.sync_regions(kind == PointerKind::Move);
3232    }
3233
3234    /// Routes one pointer event: see [`pointer_event`](Self::pointer_event).
3235    fn route_pointer_event(&mut self, event: PointerEvent) {
3236        self.cursor = event.position;
3237        self.hover.event(event.kind, event.position, event.touch);
3238        // Whether the gesture under way could still end as a tap, before this event moves
3239        // it on: the press is shared with it until it cannot (milestone 534).
3240        let could_tap = gesture_was_a_tap(self.drag.as_ref());
3241        match event.kind {
3242            PointerKind::Down => {
3243                // What is under the pointer **now**: a finger has no move before its
3244                // press, and a press only shows while it is also the hovered widget.
3245                self.sync_hover();
3246                // A pointer interaction: the keyboard focus ring fades away.
3247                self.runtime.focus_visible = false;
3248                self.pointer_down(event.touch);
3249                let (long_press, lift) = hold_candidates(
3250                    self.drag.as_ref(),
3251                    self.ui.as_ref(),
3252                    self.tree.as_deref(),
3253                    self.cursor,
3254                );
3255                self.long_press_msg = long_press;
3256                self.pending_lift = lift;
3257                let interested = self.long_press_msg.is_some()
3258                    || self.pending_lift.is_some()
3259                    || self.pending_reorder.is_some()
3260                    || self.pending_word.is_some()
3261                    || self.pending_region_hold;
3262                self.press.down(self.cursor, Instant::now(), interested);
3263            }
3264            PointerKind::Move => {
3265                self.press.moved(self.cursor);
3266                self.pointer_move();
3267                // The inspector follows the pointer to highlight, so it redraws even
3268                // over inert widgets, which have no hover animation.
3269                if self.inspector {
3270                    self.request_redraw();
3271                }
3272            }
3273            PointerKind::Up => {
3274                // The recogniser is always told, but a lifted item owes a drop: the
3275                // long press that started it must not also eat its ending.
3276                let swallow = self.press.up();
3277                self.pending_lift = None;
3278                self.pending_reorder = None;
3279                self.pending_word = None;
3280                self.pending_region_hold = false;
3281                // A row lifted by the hold owes its drop for the same reason a lifted
3282                // item does: the release is what says where it goes.
3283                if swallow
3284                    && !matches!(
3285                        self.drag,
3286                        Some(Drag::Item { moved: true, .. } | Drag::Reorder { moved: true, .. })
3287                    )
3288                {
3289                    // The long press evicted the tap, so the release is swallowed.
3290                    self.drag = None;
3291                    self.runtime.input.pressed = None;
3292                    self.request_redraw();
3293                } else {
3294                    self.pointer_up();
3295                }
3296            }
3297            PointerKind::Cancel => {
3298                self.press.cancel();
3299                self.pending_lift = None;
3300                self.pending_reorder = None;
3301                self.pending_word = None;
3302                self.pending_region_hold = false;
3303                // A cancelled gesture still owes the offset back, or the region
3304                // would stay frozen under a finger that is no longer there.
3305                if let Some(Drag::Dismiss { item, .. }) = self.drag {
3306                    // A cancelled swipe asks for nothing: zero velocity puts it back.
3307                    self.runtime.dismiss_release(
3308                        item.id,
3309                        0.0,
3310                        0.0,
3311                        item.spec.axis,
3312                        // Unreachable threshold: a cancel never dismisses, however far
3313                        // the item had already travelled.
3314                        f32::INFINITY,
3315                    );
3316                }
3317                // A value a cancelled drag held settles back, and says nothing.
3318                if let Some(Drag::Gesture { id, .. }) = self.drag {
3319                    self.runtime.release_value(id);
3320                }
3321                if let Some(Drag::Scroll { id, .. }) = self.drag {
3322                    self.runtime.release_scroll(id);
3323                    self.runtime.glow_scroll_end(id);
3324                    // A cancelled gesture asks for nothing: the indicator slides back
3325                    // rather than promising a refresh nobody requested.
3326                    if let Some(host) = self.refresh_host_of(id) {
3327                        self.runtime.refresh_cancel(host);
3328                    }
3329                }
3330                // A sheet under a cancelled finger settles from where it was left.
3331                let sheet = match self.drag {
3332                    Some(Drag::Sheet { id, available, .. }) => self
3333                        .ui
3334                        .as_ref()
3335                        .and_then(|ui| ui.sheet(id))
3336                        .map(|sheet| (sheet.id, sheet.spec.clone(), available)),
3337                    Some(Drag::Scroll { id, .. }) => self
3338                        .ui
3339                        .as_ref()
3340                        .and_then(|ui| ui.sheet_holding(id))
3341                        .map(|sheet| (sheet.id, sheet.spec.clone(), sheet.available)),
3342                    _ => None,
3343                };
3344                if let Some((id, spec, available)) = sheet {
3345                    self.runtime.sheet_release(id, &spec, available, 0.0, None);
3346                }
3347                self.drag = None;
3348                self.runtime.input.pressed = None;
3349                self.request_redraw();
3350            }
3351        }
3352        // A press a gesture has taken, or one whose pointer has gone, is let go of — after
3353        // the routing, which is what reads it (milestone 534).
3354        if settle_press(
3355            &mut self.runtime.input,
3356            event.kind,
3357            could_tap,
3358            self.drag.as_ref(),
3359            event.touch,
3360        ) {
3361            self.request_redraw();
3362        }
3363        // After the release has been routed — it reads the press, not the hover — a
3364        // finger that has lifted stops hovering (milestone 505).
3365        if matches!(event.kind, PointerKind::Up | PointerKind::Cancel) {
3366            self.sync_hover();
3367        }
3368    }
3369
3370    /// A press held still for the long-press delay: what it was a candidate for is decided.
3371    fn hold_deadline_reached(&mut self) {
3372        // Two claims on one hold: a widget asking for a message, and an item asking
3373        // to be lifted. Serving both would do a discrete action *and* start a drag
3374        // from the same gesture, which is never what anyone meant. **The lift
3375        // wins** — it changes what the rest of the gesture means, and the message
3376        // would be acting on something the finger is still holding.
3377        let lifting = self.pending_lift.is_some() || self.pending_reorder.is_some();
3378        // A hold in a text field selects the word under the finger (milestone 511),
3379        // and is that — not also the long press of whatever the field sits in.
3380        let word = self.pending_word.take();
3381        // A hold on words in a selection area selects the word, unless something held asked
3382        // for the hold first: a lift, a field, a widget's own long press.
3383        let region = std::mem::take(&mut self.pending_region_hold)
3384            && !lifting
3385            && word.is_none()
3386            && self.long_press_msg.is_none();
3387        if let Some(message) = self.long_press_msg.take() {
3388            if !lifting && word.is_none() {
3389                self.dispatch(message);
3390            }
3391        }
3392        if let Some(id) = word {
3393            self.select_held_word(id);
3394        }
3395        if region && self.select_region_word_held() {
3396            // The page under the finger stops being scrolled by it: the hold was a selection.
3397            if let Some(Drag::Scroll { id, .. }) = self.drag {
3398                self.runtime.release_scroll(id);
3399            }
3400        }
3401        let lift = self.pending_lift.take();
3402        let reorder = self.pending_reorder.take();
3403        // The scroll hands the gesture over to what the hold lifted.
3404        if lift.is_some() || reorder.is_some() {
3405            if let Some(Drag::Scroll { id, .. }) = self.drag {
3406                self.runtime.release_scroll(id);
3407            }
3408        }
3409        self.drag = drag_after_hold(self.drag.take(), lift, reorder, self.cursor);
3410        if matches!(self.drag, Some(Drag::Reorder { .. })) {
3411            self.reorder_x = self.cursor.x;
3412            self.reorder_y = self.cursor.y;
3413        }
3414        self.request_redraw();
3415    }
3416
3417    /// The loop's idle policy: wake at the **nearest** deadline — the long press, the
3418    /// live-reload poll, the caret's next turn — and otherwise wait outright.
3419    fn idle_control_flow(&self) -> ControlFlow {
3420        let press = self.press.deadline();
3421        let reload = self.reload.as_ref().map(|w| w.deadline());
3422        // Twice a second while a field has the caret and the window is in front, and never
3423        // otherwise (milestone 513).
3424        let caret = (self.lifecycle == Lifecycle::Resumed)
3425            .then(|| self.caret.next_toggle(Instant::now()))
3426            .flatten();
3427        // A first tap that may still become a double tap is sent when its wait is over.
3428        let tap = self.pending_tap.as_ref().map(|p| p.at + DOUBLE_TAP_TIMEOUT);
3429        match [press, reload, caret, tap].into_iter().flatten().min() {
3430            Some(at) => ControlFlow::WaitUntil(at),
3431            None => ControlFlow::Wait,
3432        }
3433    }
3434
3435    /// The focused field's caret, as its blink sees it: which field, where its caret and
3436    /// selection are, and a digest of its value. `None` when what has focus takes no
3437    /// typing, which has no caret to blink.
3438    fn caret_signature(&self) -> Option<crate::caret::Signature> {
3439        use std::hash::{Hash, Hasher};
3440        let field = self.runtime.input.focused?;
3441        let widget = find_widget(self.tree.as_ref()?.as_ref(), field)?;
3442        let text = widget.text_value()?;
3443        let mut hasher = std::collections::hash_map::DefaultHasher::new();
3444        text.hash(&mut hasher);
3445        Some(crate::caret::Signature {
3446            field,
3447            edit: self.runtime.edits.get(&field).copied().unwrap_or_default(),
3448            value: hasher.finish(),
3449        })
3450    }
3451
3452    /// Pointer movement, mouse or finger: continues a drag under way, and otherwise
3453    /// updates the hover.
3454    fn pointer_move(&mut self) {
3455        if self.drag.is_some() {
3456            self.handle_drag();
3457            return;
3458        }
3459        let hovered = self.sync_hover();
3460        // The system cursor follows the hovered sub-region (milestone 205): a hand
3461        // over a clickable icon, and so on. Recomputed on every move, since the
3462        // sub-region can change without the hovered widget changing.
3463        self.update_cursor_icon(hovered);
3464    }
3465
3466    /// Asks the frame what is under the hovering pointer, and records it.
3467    ///
3468    /// The only way the hover changes, so it can never name a widget of another frame:
3469    /// an id is a position in the tree, and the same position on the next screen is a
3470    /// different widget (milestone 505).
3471    fn sync_hover(&mut self) -> Option<WidgetId> {
3472        let hovered = self.ui.as_ref().and_then(|ui| self.hover.target(ui));
3473        if hovered != self.runtime.input.hovered {
3474            self.runtime.input.hovered = hovered;
3475            self.request_redraw();
3476        }
3477        hovered
3478    }
3479
3480    /// Whether work a component started has brought something since this shell last looked
3481    /// (milestone 585).
3482    fn take_task_arrivals(&mut self) -> bool {
3483        let now = frus_widgets::task_arrivals();
3484        std::mem::replace(&mut self.task_arrivals, now) != now
3485    }
3486
3487    /// Hands one raw pointer event to the widgets that hear them (milestone 586): a press to
3488    /// every one under the pointer, the innermost first, which then hear that pointer until
3489    /// it lifts, wherever it goes; a mouse moving with nothing held, to those under it.
3490    fn route_listeners(&mut self, kind: PointerKind, touch: bool) {
3491        use frus_widgets::ListenerEventKind as Raw;
3492        let at = self.cursor;
3493        let heard: Vec<(WidgetId, Rect, Raw)> = match kind {
3494            PointerKind::Down => {
3495                self.captured = self
3496                    .ui
3497                    .as_ref()
3498                    .map(|ui| ui.pointer_listeners_at(at))
3499                    .unwrap_or_default();
3500                self.captured
3501                    .iter()
3502                    .map(|(id, rect)| (*id, *rect, Raw::Down))
3503                    .collect()
3504            }
3505            PointerKind::Move if self.pointer_held => self
3506                .captured
3507                .iter()
3508                .map(|(id, rect)| (*id, *rect, Raw::Move))
3509                .collect(),
3510            PointerKind::Move if !touch => self
3511                .ui
3512                .as_ref()
3513                .map(|ui| ui.pointer_listeners_at(at))
3514                .unwrap_or_default()
3515                .into_iter()
3516                .map(|(id, rect)| (id, rect, Raw::Hover))
3517                .collect(),
3518            PointerKind::Move => Vec::new(),
3519            PointerKind::Up | PointerKind::Cancel => {
3520                let raw = if kind == PointerKind::Up {
3521                    Raw::Up
3522                } else {
3523                    Raw::Cancel
3524                };
3525                std::mem::take(&mut self.captured)
3526                    .into_iter()
3527                    .map(|(id, rect)| (id, rect, raw))
3528                    .collect()
3529            }
3530        };
3531        let messages: Vec<A::Message> = heard
3532            .into_iter()
3533            .flat_map(|(id, rect, kind)| {
3534                let event = frus_widgets::ListenerEvent {
3535                    kind,
3536                    local: Point::new(at.x - rect.x, at.y - rect.y),
3537                    touch,
3538                };
3539                self.tree
3540                    .as_ref()
3541                    .and_then(|tree| find_widget(tree.as_ref(), id))
3542                    .map(|widget| widget.on_pointer_event(event))
3543                    .unwrap_or_default()
3544            })
3545            .collect();
3546        for message in messages {
3547            self.dispatch(message);
3548            self.request_redraw();
3549        }
3550    }
3551
3552    /// Brings the regions the pointer is in up to date (milestone 583): an exit for each it has
3553    /// left, innermost first, an enter for each it has come into, outermost first, and — when
3554    /// the pointer `moved` with nothing held — a move for each it is still in.
3555    fn sync_regions(&mut self, moved: bool) {
3556        let point = self.hover.at();
3557        let now: Vec<(WidgetId, Rect)> = match (point, self.ui.as_ref()) {
3558            (Some(point), Some(ui)) => ui
3559                .hover_regions_at(point)
3560                .into_iter()
3561                .map(|(id, rect, _)| (id, rect))
3562                .collect(),
3563            _ => Vec::new(),
3564        };
3565        let at = point.unwrap_or(self.cursor);
3566        let local = |rect: Rect| Point::new(at.x - rect.x, at.y - rect.y);
3567        let before = std::mem::replace(&mut self.entered, now.clone());
3568        let mut events = Vec::new();
3569        for (id, rect) in &before {
3570            if !now.iter().any(|(n, _)| n == id) {
3571                events.push((
3572                    *id,
3573                    frus_widgets::HoverEvent::Exit {
3574                        local: local(*rect),
3575                    },
3576                ));
3577            }
3578        }
3579        for (id, rect) in now.iter().rev() {
3580            if !before.iter().any(|(b, _)| b == id) {
3581                events.push((
3582                    *id,
3583                    frus_widgets::HoverEvent::Enter {
3584                        local: local(*rect),
3585                    },
3586                ));
3587            } else if moved && !self.pointer_held {
3588                events.push((
3589                    *id,
3590                    frus_widgets::HoverEvent::Move {
3591                        local: local(*rect),
3592                    },
3593                ));
3594            }
3595        }
3596        let messages: Vec<_> = events
3597            .into_iter()
3598            .filter_map(|(id, event)| {
3599                self.tree
3600                    .as_ref()
3601                    .and_then(|tree| find_widget(tree.as_ref(), id))
3602                    .and_then(|widget| widget.on_hover_event(event))
3603            })
3604            .collect();
3605        for message in messages {
3606            self.dispatch(message);
3607            self.request_redraw();
3608        }
3609    }
3610
3611    /// The cursor to show: what the widget under the pointer asks for, `requested`, first;
3612    /// then the text cursor over a field that can be focused, as a platform shows it; then
3613    /// the innermost region the pointer is in that asks for one (milestone 583); otherwise
3614    /// the default.
3615    fn region_cursor(&self, requested: Option<UiCursor>) -> UiCursor {
3616        requested
3617            .or_else(|| {
3618                let point = self.hover.at()?;
3619                let (id, _) = self.ui.as_ref()?.focus_hit(point)?;
3620                let widget = find_widget(self.tree.as_ref()?.as_ref(), id)?;
3621                is_text_field(widget).then_some(UiCursor::Text)
3622            })
3623            .or_else(|| {
3624                self.hover.at().and_then(|point| {
3625                    self.ui
3626                        .as_ref()?
3627                        .hover_regions_at(point)
3628                        .into_iter()
3629                        .find_map(|(_, _, region)| region.cursor)
3630                })
3631            })
3632            .unwrap_or(UiCursor::Default)
3633    }
3634
3635    /// Applies the cursor shape the hovered widget asks for at the pointer's local
3636    /// position (milestone 205), and the default cursor otherwise. Translates
3637    /// `frus_widgets::Cursor` into winit's.
3638    fn update_cursor_icon(&mut self, hovered: Option<WidgetId>) {
3639        let requested = hovered.and_then(|id| {
3640            let rect = self.ui.as_ref()?.widget_rect(id)?;
3641            let widget = find_widget(self.tree.as_ref()?.as_ref(), id)?;
3642            widget.cursor_icon(
3643                self.cursor.x - rect.x,
3644                self.cursor.y - rect.y,
3645                rect.width,
3646                rect.height,
3647            )
3648        });
3649        let shown = self.region_cursor(requested);
3650        if let Some(window) = &self.window {
3651            window.set_cursor_visible(shown != UiCursor::None);
3652            window.set_cursor(cursor_icon(shown));
3653        }
3654        // Sub-region highlighting (milestone 208): the pointer's position is retained
3655        // while it hovers an interactive sub-region, that is, while cursor_icon answered.
3656        // A change repaints — the status hash includes hover_cursor — so the halo
3657        // follows the pointer or goes away.
3658        let hover_cursor = requested.map(|_| self.cursor);
3659        if hover_cursor != self.runtime.input.hover_cursor {
3660            self.runtime.input.hover_cursor = hover_cursor;
3661            self.request_redraw();
3662        }
3663    }
3664
3665    /// A pointer press, mouse or finger, at `self.cursor`. `touch` enables finger
3666    /// scrolling when no other gesture captures the press.
3667    fn pointer_down(&mut self, touch: bool) {
3668        self.pointer_touch = touch;
3669        // A first tap still waiting for its second is a tap after all if this press is not
3670        // that second one: somewhere else, or too late.
3671        self.poll_pending_tap(Instant::now());
3672        if let Some(pending) = self.pending_tap.as_ref() {
3673            let here = self.ui.as_ref().and_then(|ui| ui.hit(self.cursor));
3674            let (dx, dy) = (
3675                self.cursor.x - pending.position.x,
3676                self.cursor.y - pending.position.y,
3677            );
3678            if here != Some(pending.id) || dx.hypot(dy) > DOUBLE_TAP_SLOP {
3679                self.flush_pending_tap();
3680            }
3681        }
3682        self.pending_word = None;
3683        self.pending_region_hold = false;
3684        // A press **on the bar** comes first: the bar is drawn over everything, the handles
3685        // included, and the desktop menu hangs from the selection's top, over its handles
3686        // (milestone 618). It acts on the focused field, so nothing here may touch the
3687        // focus, the selection or the handles: it is only recorded, and `pointer_up`
3688        // resolves it to the button's action.
3689        if self
3690            .ui
3691            .as_ref()
3692            .is_some_and(|ui| ui.toolbar_contains(self.cursor))
3693        {
3694            press_at(&mut self.runtime, self.ui.as_ref(), self.cursor);
3695            self.request_redraw();
3696            return;
3697        }
3698        // Then a selection handle. It hangs below its line, over whatever is drawn
3699        // there, so nothing else may claim the press before it; and any other press puts
3700        // the handles away (milestone 511).
3701        // A selection area's handle likewise: a press on one widens or narrows the selection
3702        // rather than putting it away.
3703        if let Some(drag) = self.grab_region_handle() {
3704            self.drag = Some(drag);
3705            if let Some(selection) = self.runtime.region.as_mut() {
3706                selection.bar = false;
3707            }
3708            self.request_redraw();
3709            return;
3710        }
3711        if let Some(drag) = self.grab_selection_handle() {
3712            self.drag = Some(drag);
3713            // The bar is put away while a handle is dragged, and comes back when it is let
3714            // go (`pointer_up`): a bar that followed a moving selection would flicker.
3715            self.runtime.selection_toolbar = None;
3716            self.request_redraw();
3717            return;
3718        }
3719        if self.runtime.selection_handles.take().is_some() {
3720            self.request_redraw();
3721        }
3722        self.hide_selection_toolbar();
3723        // A selection an area holds goes with any press: the one that begins another puts a
3724        // new one in its place, below.
3725        if self.runtime.region.take().is_some() {
3726            self.request_redraw();
3727        }
3728        // 0) The back gesture: a press on the **leading edge** — left under LTR,
3729        // right under RTL — if the app allows it. It takes the pages back, so it starts on
3730        // them and not on what is above them: the window's menu bar, whose first word sits
3731        // in the edge, or an overlay — the menu it opens, a dialog (milestone 642).
3732        let above_the_pages = self.ui.as_ref().is_some_and(|ui| {
3733            ui.over_overlay(self.cursor)
3734                || ui
3735                    .title_bar_row()
3736                    .is_some_and(|row| row.contains(self.cursor))
3737        });
3738        let on_back_edge = !above_the_pages
3739            && if self.is_rtl() {
3740                self.cursor.x > self.logical_width() - BACK_EDGE
3741            } else {
3742                self.cursor.x < BACK_EDGE
3743            };
3744        if on_back_edge && self.app.can_go_back() {
3745            self.drag = Some(Drag::Back {
3746                start_x: self.cursor.x,
3747            });
3748            self.begin_gesture();
3749            self.build_dirty = true;
3750            self.app.back_gesture(0.0);
3751            self.request_redraw();
3752            return;
3753        }
3754
3755        // 1) Is this a scrollbar drag?
3756        if let Some(bar) = self.ui.as_ref().and_then(|ui| ui.scrollbar_at(self.cursor)) {
3757            let (along, thumb_start) = if bar.vertical {
3758                (self.cursor.y, bar.thumb.y)
3759            } else {
3760                (self.cursor.x, bar.thumb.x)
3761            };
3762            self.drag = Some(Drag::Scrollbar {
3763                id: bar.id,
3764                vertical: bar.vertical,
3765                grab: along - thumb_start,
3766                track_start: bar.track_start,
3767                track_len: bar.track_len,
3768                thumb_len: bar.thumb_len,
3769                max: bar.max,
3770                reverse: bar.reverse,
3771            });
3772            self.request_redraw();
3773            return;
3774        }
3775
3776        // 1b) Is this a draggable widget, a Slider for instance?
3777        if let Some((id, rect)) = self.ui.as_ref().and_then(|ui| ui.draggable_at(self.cursor)) {
3778            self.drag = Some(Drag::Widget {
3779                id,
3780                rect,
3781                last_x: self.cursor.x,
3782            });
3783            // The bracket opens **before** the first value: an application that means
3784            // to hold the expensive work until the finger lifts has to know the finger
3785            // went down first, and a press that jumps the value straight away would
3786            // otherwise deliver the change before the start.
3787            self.dispatch_drag_edge(id, rect, Edge::Start);
3788            // A zero delta on press: only a slider, which takes a fraction, jumps.
3789            self.apply_widget_drag(id, rect, 0.0);
3790            self.request_redraw();
3791            return;
3792        }
3793
3794        // 1c) A reorder: a press on a reorderable — a table header, a Kanban card, a
3795        // list's grip. We do not `return` — focus and `pressed`, where a tap means sort,
3796        // are settled below; the drag engages only past the threshold, and otherwise the
3797        // release sorts.
3798        //
3799        // A row that asked for a **hold** takes nothing now. The press is left to
3800        // whatever else wants it, which inside a list is the scroll, and the deadline
3801        // below decides between them: a finger that stays put was never scrolling.
3802        if let Some((id, from, hold)) = self.reorderable_at(self.cursor) {
3803            if hold {
3804                self.pending_reorder = Some((id, from, self.cursor));
3805            } else {
3806                self.drag = Some(Drag::Reorder {
3807                    id,
3808                    from,
3809                    start: self.cursor,
3810                    moved: false,
3811                    carried: None,
3812                });
3813                self.reorder_x = self.cursor.x; // starts glued to the pointer, with no jerk
3814                self.reorder_y = self.cursor.y; // likewise for the vertical insertion line
3815            }
3816        }
3817
3818        if press_at(&mut self.runtime, self.ui.as_ref(), self.cursor) {
3819            self.request_redraw();
3820        }
3821        // 2) Focus and caret placement, and the start of a text selection.
3822        let previously_focused = self.runtime.input.focused;
3823        let focus = self.ui.as_ref().and_then(|ui| ui.focus_hit(self.cursor));
3824        self.runtime.input.focused = focus.map(|(id, _)| id);
3825        if let Some((id, rect)) = focus {
3826            let local_x = self.cursor.x - rect.x;
3827            // The retained vertical scroll, in a multi-line field, is part of the
3828            // content coordinate: we add it so the click lands on the right line.
3829            let local_y =
3830                self.cursor.y - rect.y + self.runtime.scroll.get(&id).map(|s| s.1).unwrap_or(0.0);
3831            // The scroll shown just before this click, computed from the current caret
3832            // when the field was already focused, and 0 otherwise.
3833            let scroll_cursor = if previously_focused == Some(id) {
3834                self.runtime.edits.get(&id).map(|e| e.cursor).unwrap_or(0)
3835            } else {
3836                0
3837            };
3838            // Only **text fields** (`cursor_at` → `Some`) start a selection; the other
3839            // focusables — buttons, checkboxes — keep focus but must NOT capture the
3840            // click, which would otherwise be swallowed on release as the end of a drag.
3841            let cursor = self
3842                .tree
3843                .as_ref()
3844                .and_then(|tree| find_widget(tree.as_ref(), id))
3845                .and_then(|widget| widget.cursor_at(local_x, local_y, rect.width, scroll_cursor));
3846            if let Some(cursor) = cursor {
3847                // Placing the caret with the mouse forgets the vertical goal column.
3848                self.goal_x = None;
3849                self.runtime.edits.insert(
3850                    id,
3851                    Edit {
3852                        cursor,
3853                        anchor: None,
3854                        composing: None,
3855                    },
3856                );
3857                self.drag = Some(Drag::TextSelect { id, rect });
3858                // A finger that stays put selects the word instead (milestone 511).
3859                if touch {
3860                    self.pending_word = Some(id);
3861                }
3862                // The caret moved, so the run of typing ends here: typing at one place,
3863                // then at another, then Ctrl+Z should take back only the second.
3864                self.runtime.close_edit_run(id);
3865                // Tapping in a field **reopens** the keyboard, even one the app already
3866                // considers shown but which the system back closed — see
3867                // `request_soft_input`.
3868                self.request_soft_input();
3869
3870                // A double click selects the word under the pointer.
3871                let now = Instant::now();
3872                let double = self
3873                    .last_click_time
3874                    .map(|t| (now - t).as_secs_f32() < 0.4)
3875                    .unwrap_or(false);
3876                self.last_click_time = Some(now);
3877                if double {
3878                    if let Some((start, end)) = self
3879                        .tree
3880                        .as_ref()
3881                        .and_then(|tree| find_widget(tree.as_ref(), id))
3882                        .and_then(|widget| widget.word_at(cursor))
3883                    {
3884                        self.runtime.edits.insert(
3885                            id,
3886                            Edit {
3887                                cursor: end,
3888                                anchor: Some(start),
3889                                composing: None,
3890                            },
3891                        );
3892                        self.drag = None;
3893                    }
3894                }
3895            }
3896        }
3897
3898        // 2b) Words in a selection area, with nothing else having a claim on the press: a
3899        // selection begins, and may be dragged across the texts of the area. A finger is
3900        // left to scroll, and a finger that stays put selects the word instead (milestone 576).
3901        if self.drag.is_none() && focus.is_none() {
3902            if touch {
3903                self.pending_region_hold = self.region_press_target().is_some();
3904            } else {
3905                self.begin_region_selection();
3906            }
3907        }
3908
3909        // 3) Touch: when nothing captured the gesture — no scrollbar, no widget, no
3910        // text selection — prepare a finger scroll on the area under the finger. A
3911        // release without movement, under TOUCH_SLOP, stays a tap.
3912        if touch && self.drag.is_none() {
3913            // The innermost area that would actually take a finger. An area whose content
3914            // fits refuses the offset outright — the reference takes its drag recognisers
3915            // away for exactly this — so it neither swallows the press nor lights an edge,
3916            // and the page behind it, or a dismissible row under it, gets its turn.
3917            let landed = {
3918                let scroll = &self.runtime.scroll;
3919                self.ui.as_ref().and_then(|ui| {
3920                    ui.scroll_chain(self.cursor).find(|area| {
3921                        area.accepts_user_offset(
3922                            scroll.get(&area.id).copied().unwrap_or((0.0, 0.0)),
3923                        )
3924                    })
3925                })
3926            };
3927            if let Some(area) = landed {
3928                // A finger back on the content catches it: the fling stops where it
3929                // is rather than sliding under the finger, and hands the next
3930                // release whatever momentum the platform lets a swipe build on.
3931                let physics = area.physics;
3932                let carried = self.runtime.catch_scroll_fling(area.id, physics);
3933                // The offset belongs to the finger until it lifts.
3934                self.runtime.hold_scroll(area.id);
3935                // And so does the sheet it sits in, if one is still settling: the finger
3936                // may be about to move it, and a sheet sliding away under a finger that
3937                // has caught its list is a sheet that ignored the catch.
3938                if let Some(sheet) = self
3939                    .ui
3940                    .as_ref()
3941                    .and_then(|ui| ui.sheet_holding(area.id))
3942                    .map(|sheet| sheet.id)
3943                {
3944                    self.runtime.sheet_hold(sheet);
3945                }
3946                self.drag = Some(Drag::Scroll {
3947                    id: area.id,
3948                    last: self.cursor,
3949                    moved: false,
3950                    carried,
3951                    dismiss: self
3952                        .ui
3953                        .as_ref()
3954                        .and_then(|ui| ui.dismissable_at(self.cursor)),
3955                    axis: None,
3956                    pan: self.pan_target(),
3957                });
3958                self.begin_gesture();
3959                // A fling on a scroll area is read the way its scroll behaviour reads one
3960                // (milestone 616), as the reference's scrollable builds its recognizer.
3961                self.gesture_velocity = area.velocity_tracker();
3962            }
3963        }
3964
3965        // 3a) A sheet's panel with nothing under the finger that scrolls: the finger moves
3966        // the sheet itself. A pointer too — on a desktop there is no other way to raise one.
3967        if self.drag.is_none() {
3968            if let Some((id, available)) = self
3969                .ui
3970                .as_ref()
3971                .and_then(|ui| ui.sheet_at(self.cursor))
3972                .map(|sheet| (sheet.id, sheet.available))
3973            {
3974                self.runtime.sheet_hold(id);
3975                self.drag = Some(Drag::Sheet {
3976                    id,
3977                    last: self.cursor,
3978                    moved: false,
3979                    available,
3980                });
3981                self.begin_gesture();
3982            }
3983        }
3984
3985        // 3b) A dismissible item with nothing scrollable under it: there is no gesture to
3986        // arbitrate against, so the swipe is prepared directly. It still waits for the
3987        // threshold, or a tap on the row would start sliding it.
3988        if self.drag.is_none() {
3989            if let Some(item) = self
3990                .ui
3991                .as_ref()
3992                .and_then(|ui| ui.dismissable_at(self.cursor))
3993            {
3994                self.drag = Some(Drag::Dismiss {
3995                    item,
3996                    last: self.cursor,
3997                    moved: false,
3998                });
3999                self.begin_gesture();
4000            }
4001        }
4002
4003        // 3c) A draggable item, when nothing above has taken the gesture. It comes
4004        // **after** the touch scroll on purpose: a widget that took every drag inside a
4005        // list would silently stop that list scrolling, and a list that does not scroll
4006        // is a worse bug than an item that does not lift. With a pointer there is no
4007        // touch scroll to lose to, so it lifts everywhere.
4008        if self.drag.is_none() {
4009            if let Some(source) = self
4010                .ui
4011                .as_ref()
4012                .and_then(|ui| ui.drag_source_at(self.cursor))
4013                .filter(|source| {
4014                    // One that asked for a hold waits for the deadline instead.
4015                    !self
4016                        .tree
4017                        .as_ref()
4018                        .and_then(|tree| find_widget(tree.as_ref(), source.id))
4019                        .is_some_and(|widget| widget.drag_needs_long_press())
4020                })
4021            {
4022                self.drag = Some(Drag::Item {
4023                    source,
4024                    start: self.cursor,
4025                    moved: false,
4026                    over: None,
4027                });
4028                self.begin_gesture();
4029            }
4030        }
4031
4032        // 3d) A detector that takes a drag, with no scroll under it to share the gesture with
4033        // (milestone 582): prepared, and engaged past the slop, so a tap still goes through.
4034        if self.drag.is_none() {
4035            if let Some((id, axis)) = self.pan_target() {
4036                self.drag = Some(Drag::Gesture {
4037                    id,
4038                    axis,
4039                    start: self.cursor,
4040                    last: self.cursor,
4041                    moved: false,
4042                });
4043                self.begin_gesture();
4044            }
4045        }
4046
4047        // 4) An interactive viewport (`InteractiveViewer`) under the pointer: prepare a
4048        // **pan**, with mouse or finger. Like the touch scroll it engages only past the
4049        // threshold, so a tap goes through to the child, a button for instance.
4050        if self.drag.is_none() {
4051            if let Some((id, viewport)) = self
4052                .ui
4053                .as_ref()
4054                .and_then(|ui| ui.interactive_at(self.cursor))
4055            {
4056                // The press stops a fling in progress: we take the content back in hand.
4057                self.runtime.interactive_velocity.remove(&id);
4058                self.drag = Some(Drag::Pan {
4059                    id,
4060                    last: self.cursor,
4061                    moved: false,
4062                    viewport,
4063                });
4064                self.begin_gesture();
4065            }
4066        }
4067        self.request_redraw();
4068    }
4069
4070    /// A pointer release, mouse or finger: it ends a drag, or commits a click or tap
4071    /// when the release lands back on the widget that was pressed.
4072    /// What widget `id` emits for a click at the pointer on one of its sub-regions — see
4073    /// [`Widget::positional_click`]. Local coordinates are the pointer minus the widget's
4074    /// corner.
4075    fn positional_click_on(&self, id: WidgetId) -> Option<A::Message> {
4076        let rect = self.ui.as_ref()?.widget_rect(id)?;
4077        let widget = find_widget(self.tree.as_ref()?.as_ref(), id)?;
4078        widget.positional_click(
4079            self.cursor.x - rect.x,
4080            self.cursor.y - rect.y,
4081            rect.width,
4082            rect.height,
4083        )
4084    }
4085
4086    fn pointer_up(&mut self) {
4087        let ended = self.drag.take();
4088        if let Some(Drag::Back { .. }) = ended {
4089            // The app decides — commit or cancel — from the velocity, which it wants
4090            // in **fractions of the screen** per second, not pixels.
4091            let sign = if self.is_rtl() { -1.0 } else { 1.0 };
4092            let velocity = sign * self.gesture_estimate().velocity.x / self.logical_width();
4093            self.build_dirty = true;
4094            self.app.back_gesture_end(velocity);
4095            self.request_redraw();
4096            return;
4097        }
4098        // A touch scroll, a pan or a swipe that never moved is a plain tap: we let it
4099        // follow the ordinary click path below.
4100        let was_tap = gesture_was_a_tap(ended.as_ref());
4101        // The bracket closes. A press that never moved still gets its end: it changed
4102        // the value once, and a caller that defers its expensive work until the release
4103        // would otherwise never be told the release happened.
4104        if let Some(Drag::Widget { id, rect, .. }) = ended {
4105            self.dispatch_drag_edge(id, rect, Edge::End);
4106        }
4107        // The keyboard is told where a dragged handle left the selection.
4108        #[cfg(android)]
4109        if let Some(Drag::SelectionHandle { id, .. }) = ended {
4110            self.push_ime_context(id);
4111        }
4112        if let Some(Drag::SelectionHandle { id, .. }) = ended {
4113            self.show_selection_toolbar(id);
4114        }
4115        // A handle of a selection area let go: its bar comes back over what is selected now.
4116        if let Some(Drag::RegionHandle { .. }) = ended {
4117            if let Some(selection) = self.runtime.region.as_mut() {
4118                selection.bar = true;
4119            }
4120        }
4121        // Reordering: on the drop, the target column is the reorderable header under
4122        // the pointer, and we route the grabbed header's `on_reorder(from, to)`.
4123        if let Some(Drag::Reorder {
4124            id,
4125            from,
4126            moved: true,
4127            carried,
4128            ..
4129        }) = &ended
4130        {
4131            let target = self.reorder_drop_target(*id, *from, *carried);
4132            let tree = self.tree.as_ref();
4133            let base = target
4134                .and_then(|tid| tree.and_then(|t| find_widget(t.as_ref(), tid)))
4135                .and_then(|widget| widget.reorder_index());
4136            // The **trailing** half of a hovered target that is inserted at — the lower
4137            // half down a list, the right half across one (the left, right to left) —
4138            // means inserting **after** it, index +1: the effective drop slot follows the
4139            // insertion line that was painted. No effect for `Table` columns, or off target.
4140            let to = match (base, target) {
4141                (Some(base), Some(tid)) => {
4142                    let after = self
4143                        .ui
4144                        .as_ref()
4145                        .and_then(|ui| ui.widget_rect(tid))
4146                        .map(|rect| self.reorder_insert_after(tid, rect))
4147                        .unwrap_or(false);
4148                    Some(base + after as usize)
4149                }
4150                _ => None,
4151            };
4152            // Dropping **onto itself** moves nothing, even in the lower half, where
4153            // `to = from + 1` would slip past the `to != from` guard — otherwise we would
4154            // emit a null move and announce it.
4155            let self_drop = target == Some(*id);
4156            let message = match to {
4157                Some(to) if to != *from && !self_drop => tree
4158                    .and_then(|t| find_widget(t.as_ref(), *id))
4159                    .and_then(|widget| widget.on_reorder(to)),
4160                _ => None,
4161            };
4162            if let Some(message) = message {
4163                let to = to.unwrap_or(*from);
4164                let widget = tree.and_then(|t| find_widget(t.as_ref(), *id));
4165                let axis = widget
4166                    .map(|w| w.reorder_axis())
4167                    .unwrap_or(ReorderAxis::Horizontal);
4168                // The move is spoken to the screen reader — the ghost's counterpart for
4169                // a blind user. A widget whose index **is** a position says so itself; a
4170                // `Kanban` card's is a flat `column × stride + position` that means
4171                // nothing read aloud, so the fallback speaks of the axis and gives no
4172                // number at all.
4173                let announcement = widget
4174                    .and_then(|w| w.reorder_announcement(to))
4175                    .unwrap_or_else(|| match axis {
4176                        ReorderAxis::Horizontal => format!("Column moved to position {}", to + 1),
4177                        ReorderAxis::Vertical => "Card moved".to_string(),
4178                    });
4179                self.dispatch(message);
4180                self.set_announcement(announcement);
4181            }
4182        }
4183        // Fling: the finger's momentum is handed to the area's physics, which
4184        // returns the motion that follows — a spline that stops at the edge, or
4185        // friction that hands over to a spring and bounces.
4186        // A swiped item: the release velocity decides between flying out and sliding
4187        // back, using the same fitted estimate a fling uses.
4188        if let Some(Drag::Dismiss { item, moved, .. }) = &ended {
4189            if *moved {
4190                let estimate = self.gesture_estimate();
4191                let horizontal = item.spec.axis.is_horizontal();
4192                let (along, across) = if horizontal {
4193                    (estimate.velocity.x, estimate.velocity.y)
4194                } else {
4195                    (estimate.velocity.y, estimate.velocity.x)
4196                };
4197                self.runtime.dismiss_release(
4198                    item.id,
4199                    along,
4200                    across,
4201                    item.spec.axis,
4202                    item.spec.threshold,
4203                );
4204                self.request_redraw();
4205                return;
4206            }
4207        }
4208        // A sheet let go of by its panel settles from wherever the finger left it — and
4209        // one only pressed settles too, since the press caught it if it was moving.
4210        if let Some(Drag::Sheet {
4211            id,
4212            moved,
4213            available,
4214            ..
4215        }) = &ended
4216        {
4217            let velocity = if *moved {
4218                -self.fling_velocity(self.gesture_estimate()).1
4219            } else {
4220                0.0
4221            };
4222            // Thrown by its handle, a sheet with one list in it carries that list on at the
4223            // top, as the reference's does: there its whole content is the list.
4224            let sheet = self.ui.as_ref().and_then(|ui| ui.sheet(*id)).map(|sheet| {
4225                let list = match sheet.areas.as_slice() {
4226                    [only] => Some(*only),
4227                    _ => None,
4228                };
4229                (sheet.spec.clone(), list)
4230            });
4231            if let Some((spec, list)) = sheet {
4232                self.runtime
4233                    .sheet_release(*id, &spec, *available, velocity, list);
4234            }
4235            if *moved {
4236                self.request_redraw();
4237                return;
4238            }
4239        }
4240        if let Some(Drag::Item {
4241            source,
4242            moved: true,
4243            over,
4244            ..
4245        }) = &ended
4246        {
4247            // The drop, then the source's own answer. In that order: an application
4248            // that reacts to both should see the thing arrive before it is told the
4249            // journey is over.
4250            self.runtime.drag_over = None;
4251            let accepted = over.is_some();
4252            let dropped = over.and_then(|id| {
4253                self.tree
4254                    .as_ref()
4255                    .and_then(|tree| find_widget(tree.as_ref(), id))
4256                    .and_then(|widget| widget.on_drop(source.payload))
4257            });
4258            let ended_msg = self
4259                .tree
4260                .as_ref()
4261                .and_then(|tree| find_widget(tree.as_ref(), source.id))
4262                .and_then(|widget| widget.on_dropped(accepted));
4263            for message in dropped.into_iter().chain(ended_msg) {
4264                self.dispatch(message);
4265            }
4266            self.request_redraw();
4267            return;
4268        }
4269        if let Some(Drag::Item { .. }) = &ended {
4270            // Lifted but never moved: nothing was dropped, and the click below still
4271            // has its say.
4272            self.runtime.drag_over = None;
4273        }
4274        // Set when a sheet takes the release of a list inside it, which then does not
4275        // fling as well (milestone 515).
4276        let mut sheet_took = false;
4277        if let Some(Drag::Scroll { id, .. }) = &ended {
4278            let sheet = self
4279                .ui
4280                .as_ref()
4281                .and_then(|ui| ui.sheet_holding(*id))
4282                .cloned();
4283            if let Some(sheet) = sheet {
4284                // In the sheet's terms, positive growing it: a list's offset grows the
4285                // same way, on an axis that is not reversed.
4286                let velocity = match &ended {
4287                    Some(Drag::Scroll {
4288                        moved: true,
4289                        axis: Some(true),
4290                        carried,
4291                        ..
4292                    }) => -self.fling_velocity(self.gesture_estimate()).1 + carried.1,
4293                    _ => 0.0,
4294                };
4295                let offset = self.runtime.scroll.get(id).map_or(0.0, |o| o.1);
4296                let size = self.runtime.sheet_size(sheet.id, &sheet.spec);
4297                sheet_took = velocity != 0.0
4298                    && frus_widgets::sheet_takes_release(
4299                        velocity,
4300                        offset,
4301                        size,
4302                        &sheet.spec,
4303                        sheet.available,
4304                    );
4305                // Whoever takes it, the sheet is let go of: settled by the throw, or
4306                // from where it is — which is a stop, whenever the list keeps the throw.
4307                self.runtime.sheet_release(
4308                    sheet.id,
4309                    &sheet.spec,
4310                    sheet.available,
4311                    if sheet_took { velocity } else { 0.0 },
4312                    Some(*id),
4313                );
4314            }
4315        }
4316        if let Some(Drag::Scroll { id, .. }) = &ended {
4317            // The finger gives the offset back before anything is flung at it.
4318            self.runtime.release_scroll(*id);
4319            // A pull held against an edge now fades at the "let go" rate.
4320            self.runtime.glow_scroll_end(*id);
4321            // And a pull-to-refresh gesture is answered: releasing it armed is the
4322            // whole point of the gesture, so the message goes out now rather than
4323            // after the indicator has finished settling.
4324            self.release_refresh(*id);
4325        }
4326        if let Some(Drag::Scroll {
4327            id,
4328            moved: true,
4329            carried,
4330            axis,
4331            ..
4332        }) = &ended
4333        {
4334            // In **scroll space**: the content moves opposite the finger — and along the
4335            // axis the gesture was claimed by, or the release would fling the way the drag
4336            // was not allowed to go.
4337            let estimate = self.gesture_estimate();
4338            let velocity = self.fling_velocity(estimate);
4339            // Through the same conversion the drag went through, so the release carries
4340            // on the way the finger was going — on a reversed axis too.
4341            let velocity = match self.ui.as_ref().and_then(|u| u.scroll_region(*id)) {
4342                Some(area) => area.offset_delta(velocity),
4343                None => (-velocity.0, -velocity.1),
4344            };
4345            // The momentum carried in from an interrupted fling is masked with the rest:
4346            // a gesture held to one axis must not launch along the other on the strength
4347            // of what the *previous* one was doing.
4348            let launch = (velocity.0 + carried.0, velocity.1 + carried.1);
4349            if sheet_took {
4350                self.request_redraw();
4351            } else {
4352                self.fling(
4353                    *id,
4354                    match axis {
4355                        Some(true) => (0.0, launch.1),
4356                        Some(false) => (launch.0, 0.0),
4357                        None => launch,
4358                    },
4359                );
4360            }
4361        }
4362        // A detector's drag let go: its end, with the velocity it was let go at (milestone
4363        // 582). One that never moved was a tap, and goes on to the click below.
4364        if let Some(Drag::Gesture {
4365            id,
4366            axis,
4367            moved: true,
4368            ..
4369        }) = &ended
4370        {
4371            let velocity = self.fling_velocity(self.gesture_estimate());
4372            let velocity = mask_pan(*axis, Point::new(velocity.0, velocity.1));
4373            // No faster than a finger can throw: the reference's `kMaxFlingVelocity`.
4374            let speed = velocity.x.hypot(velocity.y);
4375            let velocity = if speed > MAX_PAN_VELOCITY {
4376                let k = MAX_PAN_VELOCITY / speed;
4377                Point::new(velocity.x * k, velocity.y * k)
4378            } else {
4379                velocity
4380            };
4381            self.send_pan(*id, frus_widgets::PanEvent::End { velocity });
4382        }
4383        // A pan fling: the momentum launches the content, which `advance_interactive`
4384        // decelerates and bounds frame by frame.
4385        if let Some(Drag::Pan {
4386            id, moved: true, ..
4387        }) = &ended
4388        {
4389            let estimate = self.gesture_estimate();
4390            let velocity = self.fling_velocity(estimate);
4391            if velocity.0.hypot(velocity.1) > PAN_FLING_MIN {
4392                self.runtime.interactive_velocity.insert(*id, velocity);
4393            }
4394        }
4395        if ended.is_some() && !was_tap {
4396            self.request_redraw();
4397            return;
4398        }
4399        // A click only counts when press and release land on the same widget.
4400        let released = self.ui.as_ref().and_then(|ui| ui.hit(self.cursor));
4401        // A built-in button of the selection bar sends no message: the shell performs its
4402        // action on the field the bar is open on (milestone 568).
4403        let bar_action = match (self.runtime.input.pressed, released) {
4404            (Some(pressed), Some(released)) if pressed == released => {
4405                self.ui.as_ref().and_then(|ui| ui.edit_action_for(pressed))
4406            }
4407            _ => None,
4408        };
4409        let on_bar = self
4410            .ui
4411            .as_ref()
4412            .is_some_and(|ui| ui.toolbar_contains(self.cursor));
4413        // A field answers no tap of its own, so it is no hit target and `released` never
4414        // names it: its clickable suffix is asked through the focusable the press landed
4415        // on, when the release lands on it too. It wins over a tappable parent, the way
4416        // the inner target always does.
4417        let field_positional = if bar_action.is_some() {
4418            None
4419        } else {
4420            self.ui
4421                .as_ref()
4422                .and_then(|ui| ui.focus_hit(self.cursor))
4423                .map(|(id, _)| id)
4424                .filter(|id| self.runtime.input.focused == Some(*id))
4425                .and_then(|id| self.positional_click_on(id))
4426        };
4427        let (message, announce) = match (self.runtime.input.pressed, released) {
4428            (Some(_), Some(_)) if bar_action.is_some() => (None, None),
4429            _ if field_positional.is_some() => (field_positional, None),
4430            (Some(pressed), Some(released)) if pressed == released => {
4431                // A **positional** click, on a sub-region such as a chart's point, takes
4432                // priority over `on_click`.
4433                let positional = self.positional_click_on(released);
4434                let message =
4435                    positional.or_else(|| self.ui.as_ref().and_then(|ui| ui.msg_for(pressed)));
4436                // The spoken announcement of the effect — a sort, a selection — read off
4437                // the clicked widget before `dispatch` rebuilds the tree.
4438                let announce = self
4439                    .tree
4440                    .as_ref()
4441                    .and_then(|tree| find_widget(tree.as_ref(), pressed))
4442                    .and_then(|widget| widget.announce());
4443                (message, announce)
4444            }
4445            _ => (None, None),
4446        };
4447        // Something that takes a double tap: this tap is its second, or a first that waits.
4448        let message = match (self.runtime.input.pressed, released) {
4449            (Some(pressed), Some(released)) if pressed == released && bar_action.is_none() => {
4450                self.double_tap(pressed, message)
4451            }
4452            _ => message,
4453        };
4454        // The tap completed on the widget it started on: its ink finishes growing and
4455        // fades. Anything else leaves the splash unconfirmed, and `advance_ink` sweeps
4456        // it away quickly — the finger slid off, and the ink says so.
4457        if let (Some(pressed), Some(released)) = (self.runtime.input.pressed, released) {
4458            if pressed == released {
4459                self.runtime.ink_confirm(pressed);
4460            }
4461        }
4462        self.runtime.input.pressed = None;
4463        if let Some(action) = bar_action {
4464            self.perform_edit_action(action);
4465        } else if on_bar && message.is_some() {
4466            // An item of the application's own: it has sent its message, and the bar is
4467            // done with.
4468            self.runtime.selection_handles = None;
4469            self.hide_selection_toolbar();
4470        }
4471        if let Some(message) = message {
4472            self.dispatch(message);
4473            if let Some(announce) = announce {
4474                self.set_announcement(announce);
4475            }
4476        }
4477        self.request_redraw();
4478    }
4479
4480    /// A tap on `id`, which answered `message`. If `id` also takes a double tap, the tap is
4481    /// either the second of one, and the double tap's message is sent in its place, or a
4482    /// first, which waits for [`DOUBLE_TAP_TIMEOUT`] before it is a tap (milestone 581).
4483    fn double_tap(&mut self, id: WidgetId, message: Option<A::Message>) -> Option<A::Message> {
4484        let Some(double) = self
4485            .tree
4486            .as_ref()
4487            .and_then(|tree| find_widget(tree.as_ref(), id))
4488            .and_then(|widget| widget.on_double_tap())
4489        else {
4490            return message;
4491        };
4492        let now = Instant::now();
4493        let second = self.pending_tap.as_ref().is_some_and(|pending| {
4494            let (dx, dy) = (
4495                self.cursor.x - pending.position.x,
4496                self.cursor.y - pending.position.y,
4497            );
4498            pending.id == id
4499                && now.duration_since(pending.at) <= DOUBLE_TAP_TIMEOUT
4500                && dx.hypot(dy) <= DOUBLE_TAP_SLOP
4501        });
4502        if second {
4503            self.pending_tap = None;
4504            return Some(double);
4505        }
4506        self.flush_pending_tap();
4507        self.pending_tap = Some(PendingTap {
4508            id,
4509            at: now,
4510            position: self.cursor,
4511            single: message,
4512        });
4513        None
4514    }
4515
4516    /// Sends the waiting first tap's own message, if one is waiting.
4517    fn flush_pending_tap(&mut self) {
4518        if let Some(message) = self.pending_tap.take().and_then(|pending| pending.single) {
4519            self.dispatch(message);
4520            self.request_redraw();
4521        }
4522    }
4523
4524    /// The waiting first tap, sent if its wait is over by `now`. Whether one was.
4525    fn poll_pending_tap(&mut self, now: Instant) -> bool {
4526        let due = self
4527            .pending_tap
4528            .as_ref()
4529            .is_some_and(|pending| now.duration_since(pending.at) >= DOUBLE_TAP_TIMEOUT);
4530        if due {
4531            self.flush_pending_tap();
4532        }
4533        due
4534    }
4535
4536    /// Speaks a message aloud through the screen reader's **live region**, for a
4537    /// column reorder and the like. With no screen reader running it costs nothing. The
4538    /// text is re-spoken only on a change. Desktop and web; a no-op on Android and iOS,
4539    /// which have no bridge yet.
4540    #[cfg(any(desktop, web))]
4541    fn set_announcement(&mut self, message: String) {
4542        self.announce = message;
4543    }
4544    #[cfg(not(any(desktop, web)))]
4545    fn set_announcement(&mut self, _message: String) {}
4546
4547    /// Returning focus when an overlay closes: if the focused widget has **vanished**
4548    /// from the frame, a menu or modal having closed, focus goes back to the
4549    /// **trigger** — the most recent focusable in the history that is still present.
4550    /// Every transition is recorded along the way: the old focus, if still present,
4551    /// becomes a candidate trigger. The focus ring is drawn on the following frame
4552    /// (`request_redraw` when focus moved).
4553    fn reconcile_focus(&mut self) {
4554        let present: std::collections::HashSet<WidgetId> = match self.ui.as_ref() {
4555            Some(ui) => ui.focusable_ids().collect(),
4556            None => return,
4557        };
4558        // A widget that takes the focus on appearing, and has just appeared, takes it. The
4559        // focus it takes it from is recorded below as any move is, so it goes back there
4560        // when this one goes: a submenu closing hands it back to its row.
4561        let autofocus: Vec<WidgetId> = self
4562            .ui
4563            .as_ref()
4564            .map(|ui| ui.autofocus_ids().collect())
4565            .unwrap_or_default();
4566        let appeared = autofocus
4567            .iter()
4568            .find(|id| !self.autofocused.contains(id))
4569            .copied();
4570        self.autofocused = autofocus.into_iter().collect();
4571        if let Some(id) = appeared {
4572            self.runtime.input.focused = Some(id);
4573        }
4574        let before = self.runtime.input.focused;
4575        let after = resolve_focus(
4576            before,
4577            &present,
4578            &mut self.focus_history,
4579            &mut self.prev_focus,
4580        );
4581        if after != before || appeared.is_some() {
4582            self.runtime.input.focused = after;
4583            self.request_redraw();
4584        }
4585    }
4586
4587    /// Applies a message to the application, runs its effects, then re-evaluates the
4588    /// subscriptions, the state having possibly changed which ones should run.
4589    fn dispatch(&mut self, message: A::Message) {
4590        // The app may have changed state, so the `view` must be rebuilt.
4591        self.build_dirty = true;
4592        let command = self.app.update(message);
4593        self.run_command(command);
4594        let asked = self.app.effects();
4595        self.run_command(asked);
4596        #[cfg(web)]
4597        self.sync_address();
4598        self.sync_subscriptions();
4599    }
4600
4601    /// The link the program was started with — on Android the activity's intent, on a desktop an
4602    /// argument — handed to the application as a location. Once, before the first frame.
4603    #[cfg(any(android, desktop))]
4604    fn open_launch_link(&mut self) {
4605        #[cfg(android)]
4606        let link = self
4607            .android_app
4608            .as_ref()
4609            .and_then(crate::android_link::launch_link);
4610        #[cfg(desktop)]
4611        let link = crate::link::link_among(std::env::args());
4612        if let Some(location) = link.as_deref().and_then(crate::link::location_of_link) {
4613            let command = self.app.open_location(&location);
4614            self.run_command(command);
4615        }
4616    }
4617
4618    /// The links a second process handed to this one since the last frame.
4619    #[cfg(desktop)]
4620    fn open_links_from_other_instances(&mut self) {
4621        for link in crate::instance::take_links() {
4622            if let Some(location) = crate::link::location_of_link(&link) {
4623                let command = self.app.open_location(&location);
4624                self.run_command(command);
4625                self.build_dirty = true;
4626                self.request_redraw();
4627            }
4628        }
4629    }
4630
4631    /// The address the page was opened at, handed to the application, and the browser's own
4632    /// moves from here on listened to. Once, before the first frame.
4633    #[cfg(web)]
4634    fn open_launch_address(&mut self, window: &Arc<Window>) {
4635        let strategy = self.app.location_strategy();
4636        let base = crate::address::base();
4637        let (location, index) = crate::address::current(strategy, &base);
4638        self.history.opened(location.clone(), index);
4639        crate::address::listen(window.clone(), strategy, base.clone());
4640        self.address = (strategy, base);
4641        if let Some(location) = location {
4642            let command = self.app.open_location(&location);
4643            self.run_command(command);
4644            // Opened deep in the application: the pages beneath get entries of their own.
4645            let (strategy, base) = &self.address;
4646            for (step, index) in self.history.seed(&self.app.location_stack()) {
4647                crate::address::apply(step, *strategy, base, index);
4648            }
4649        }
4650    }
4651
4652    /// What the browser did since the last look is told to the application, and where the
4653    /// application is is shown in the address bar — the agreement of [`crate::history`].
4654    #[cfg(web)]
4655    fn sync_address(&mut self) {
4656        if !self.started {
4657            return;
4658        }
4659        for (location, state) in crate::address::take_popped() {
4660            if self.history.popped(location.clone(), state) {
4661                let command = self.app.open_location(&location);
4662                self.run_command(command);
4663                self.build_dirty = true;
4664                // The move it starts — a page sliding — is ticked by the frames after this
4665                // one, which was decided to be still before the address was read.
4666                self.request_redraw();
4667            }
4668        }
4669        if let Some(location) = self.app.location() {
4670            if let Some(step) = self
4671                .history
4672                .reflect(&location, self.app.location_replaces())
4673            {
4674                let (strategy, base) = &self.address;
4675                crate::address::apply(step, *strategy, base, self.history.index());
4676            }
4677        }
4678    }
4679
4680    /// Runs a command, and whatever message it produces comes back into the loop
4681    /// through the proxy. Focus requests are **queued**, then resolved against the
4682    /// freshly built tree on the next frame.
4683    ///
4684    /// Where each kind of effect goes:
4685    ///
4686    /// | | native | Web |
4687    /// |---|---|---|
4688    /// | synchronous task | its own thread | a `spawn_local` microtask |
4689    /// | asynchronous task | the shell's executor | the browser |
4690    /// | timer | a task on the executor's reactor | `setTimeout` |
4691    ///
4692    /// A **synchronous** task keeps its thread: it blocks by definition, and that is
4693    /// what a thread is for. An **asynchronous** one no longer gets one — it is a
4694    /// task among others on a pool of four, which is the whole point of milestone
4695    /// 303. See [`crate::runtime`].
4696    fn run_command(&mut self, command: crate::command::Command<A::Message>) {
4697        let parts = command.into_parts();
4698        self.pending_focus.extend(parts.focus);
4699        self.pending_scroll.extend(parts.scrolls);
4700        self.pending_sheet.extend(parts.sheets);
4701        for task in parts.tasks {
4702            let proxy = self.proxy.clone();
4703            #[cfg(not(web))]
4704            std::thread::spawn(move || {
4705                if let Some(message) = task() {
4706                    let _ = proxy.send_event(message);
4707                }
4708            });
4709            #[cfg(web)]
4710            wasm_bindgen_futures::spawn_local(async move {
4711                if let Some(message) = task() {
4712                    let _ = proxy.send_event(message);
4713                }
4714            });
4715        }
4716        for future in parts.async_tasks {
4717            let proxy = self.proxy.clone();
4718            // Native: a task on the shared executor. `detach` because an effect must
4719            // outlive the handle — nothing here wants to cancel it, unlike a
4720            // subscription.
4721            #[cfg(not(web))]
4722            crate::runtime::spawn(async move {
4723                if let Some(message) = future.await {
4724                    let _ = proxy.send_event(message);
4725                }
4726            })
4727            .detach();
4728            // Web: the browser drives the future, single-threaded — `fetch` and friends
4729            // genuinely `await`, without blocking the loop.
4730            #[cfg(web)]
4731            wasm_bindgen_futures::spawn_local(async move {
4732                if let Some(message) = future.await {
4733                    let _ = proxy.send_event(message);
4734                }
4735            });
4736        }
4737        for (delay, message) in parts.timers {
4738            let proxy = self.proxy.clone();
4739            #[cfg(not(web))]
4740            crate::runtime::spawn(async move {
4741                crate::runtime::sleep(delay).await;
4742                let _ = proxy.send_event(message);
4743            })
4744            .detach();
4745            #[cfg(web)]
4746            web_timer::after(delay.as_millis() as i32, move || {
4747                let _ = proxy.send_event(message);
4748            });
4749        }
4750    }
4751
4752    /// Diffs the subscriptions the app declares against those running: starts the new
4753    /// ones and stops those that vanished, by dropping their `Sender`.
4754    fn sync_subscriptions(&mut self) {
4755        let entries = self.app.subscription().into_entries();
4756        let declared: std::collections::HashSet<u64> = entries.iter().map(|e| e.id).collect();
4757
4758        // Stop the subscriptions that are no longer declared.
4759        self.running_subs.retain(|id, _| declared.contains(id));
4760
4761        // Start the new ones.
4762        for entry in entries {
4763            if self.running_subs.contains_key(&entry.id) {
4764                continue;
4765            }
4766            let handle = self.start_subscription(entry.kind);
4767            self.running_subs.insert(entry.id, handle);
4768        }
4769    }
4770
4771    /// Starts a subscription as a task on the shell's executor, and returns its
4772    /// cancellation handle — **dropping the handle cancels the task**, which is what
4773    /// `sync_subscriptions` above relies on when the application stops declaring it.
4774    ///
4775    /// This used to be a thread per subscription, parked in `recv_timeout` and woken
4776    /// by the timeout or by its `Sender` being dropped. A timer is the clearest case
4777    /// there is of work that only ever waits, so it is the clearest case for not
4778    /// spending a thread on it.
4779    #[cfg(not(web))]
4780    fn start_subscription(&self, kind: crate::subscription::Kind<A::Message>) -> SubHandle {
4781        let proxy = self.proxy.clone();
4782        match kind {
4783            crate::subscription::Kind::Every { interval, make } => {
4784                crate::runtime::spawn(async move {
4785                    loop {
4786                        crate::runtime::sleep(interval).await;
4787                        // The loop having closed is the other way this ends, and the
4788                        // application is gone by then, so there is nobody to tell.
4789                        if proxy.send_event(make(Instant::now())).is_err() {
4790                            break;
4791                        }
4792                    }
4793                })
4794            }
4795        }
4796    }
4797
4798    /// Starts a subscription on the **Web**: a browser `setInterval`, with no thread.
4799    /// Returns its cancellation handle, which calls `clearInterval` on drop. The proxy
4800    /// feeds the message back into the loop on every tick.
4801    #[cfg(web)]
4802    fn start_subscription(&self, kind: crate::subscription::Kind<A::Message>) -> SubHandle {
4803        let proxy = self.proxy.clone();
4804        match kind {
4805            crate::subscription::Kind::Every { interval, make } => {
4806                let ms = interval.as_millis() as i32;
4807                web_timer::Interval::new(ms, move || {
4808                    let _ = proxy.send_event(make(Instant::now()));
4809                })
4810            }
4811        }
4812    }
4813
4814    /// Applies the mouse drag under way.
4815    fn handle_drag(&mut self) {
4816        let Some(mut drag) = self.drag.take() else {
4817            return;
4818        };
4819        // How far this pointer has to travel before a press counts as a drag.
4820        let slop = self.hit_slop();
4821        match &mut drag {
4822            Drag::Scrollbar {
4823                id,
4824                vertical,
4825                grab,
4826                track_start,
4827                track_len,
4828                thumb_len,
4829                max,
4830                reverse,
4831            } => {
4832                let along = if *vertical {
4833                    self.cursor.y
4834                } else {
4835                    self.cursor.x
4836                };
4837                let travel = (*track_len - *thumb_len).max(1.0);
4838                let thumb_start = (along - *grab).clamp(*track_start, *track_start + travel);
4839                let fraction = (thumb_start - *track_start) / travel;
4840                // A reversed axis numbers its offsets from the far end, so the thumb's
4841                // position along the track reads backwards.
4842                let fraction = if *reverse { 1.0 - fraction } else { fraction };
4843                let offset = (fraction * *max).clamp(0.0, *max);
4844                let entry = self.runtime.scroll.entry(*id).or_insert((0.0, 0.0));
4845                if *vertical {
4846                    entry.1 = offset;
4847                } else {
4848                    entry.0 = offset;
4849                }
4850                // A precise drag: the target follows the offset and inertia is cut off.
4851                let synced = *entry;
4852                self.runtime.scroll_target.insert(*id, synced);
4853                self.runtime.scroll_velocity.remove(&*id);
4854            }
4855            Drag::RegionSelect { area } => {
4856                let area = *area;
4857                self.extend_region_selection(area);
4858            }
4859            Drag::RegionHandle { area, handle, grab } => {
4860                let (area, handle, grab) = (*area, *handle, *grab);
4861                self.drag_region_handle(area, handle, grab);
4862            }
4863            Drag::TextSelect { id, rect } => {
4864                let local_x = self.cursor.x - rect.x;
4865                let local_y = self.cursor.y - rect.y
4866                    + self.runtime.scroll.get(id).map(|s| s.1).unwrap_or(0.0);
4867                // The field is focused during the drag, so scroll from the current caret.
4868                let scroll_cursor = self.runtime.edits.get(id).map(|e| e.cursor).unwrap_or(0);
4869                let cursor = self
4870                    .tree
4871                    .as_ref()
4872                    .and_then(|tree| find_widget(tree.as_ref(), *id))
4873                    .and_then(|widget| {
4874                        widget.cursor_at(local_x, local_y, rect.width, scroll_cursor)
4875                    });
4876                if let Some(cursor) = cursor {
4877                    let edit = self.runtime.edits.entry(*id).or_default();
4878                    if edit.anchor.is_none() {
4879                        edit.anchor = Some(edit.cursor);
4880                    }
4881                    edit.cursor = cursor;
4882                }
4883            }
4884            Drag::SelectionHandle {
4885                id,
4886                rect,
4887                handle,
4888                grab,
4889            } => {
4890                // The text position the handle stands for, carried with the finger, and
4891                // the retained vertical scroll folded in as for a press.
4892                let local_x = self.cursor.x - rect.x + grab.x;
4893                let local_y = self.cursor.y - rect.y
4894                    + grab.y
4895                    + self.runtime.scroll.get(id).map(|s| s.1).unwrap_or(0.0);
4896                let edit = self.runtime.edits.get(id).copied().unwrap_or_default();
4897                let moved = self
4898                    .tree
4899                    .as_ref()
4900                    .and_then(|tree| find_widget(tree.as_ref(), *id))
4901                    .and_then(|widget| widget.cursor_at(local_x, local_y, rect.width, edit.cursor))
4902                    .and_then(|to| crate::selection::drag(edit, *handle, to));
4903                if let Some(moved) = moved {
4904                    self.runtime.edits.insert(*id, moved);
4905                }
4906            }
4907            Drag::Widget { id, rect, last_x } => {
4908                let dx = self.cursor.x - *last_x;
4909                *last_x = self.cursor.x;
4910                self.apply_widget_drag(*id, *rect, dx);
4911            }
4912            Drag::Reorder { start, moved, .. } => {
4913                // Past the threshold this is a real drag, and no longer a sort.
4914                let dx = self.cursor.x - start.x;
4915                let dy = self.cursor.y - start.y;
4916                if !*moved && (dx * dx + dy * dy) > slop * slop {
4917                    *moved = true;
4918                }
4919                // The ghost card follows the pointer, so redraw on every move.
4920                if *moved {
4921                    self.request_redraw();
4922                }
4923            }
4924            Drag::Gesture {
4925                id,
4926                axis,
4927                start,
4928                last,
4929                moved,
4930            } => {
4931                let (id, axis) = (*id, *axis);
4932                if !*moved {
4933                    // Past the slop it is a drag and no longer a tap. (The reference's pan
4934                    // waits for twice this, but its tap has given up at once: a movement in
4935                    // between is neither. One threshold keeps a press either a tap or a drag.)
4936                    let (tx, ty) = (self.cursor.x - start.x, self.cursor.y - start.y);
4937                    let travelled = match axis {
4938                        frus_widgets::PanAxis::Free => tx.hypot(ty),
4939                        frus_widgets::PanAxis::Horizontal => tx.abs(),
4940                        frus_widgets::PanAxis::Vertical => ty.abs(),
4941                    };
4942                    if travelled > slop {
4943                        *moved = true;
4944                        let local = self.pan_local(id, *start);
4945                        self.send_pan(id, frus_widgets::PanEvent::Start { local });
4946                    }
4947                }
4948                if *moved {
4949                    let delta = mask_pan(
4950                        axis,
4951                        Point::new(self.cursor.x - last.x, self.cursor.y - last.y),
4952                    );
4953                    *last = self.cursor;
4954                    self.track_gesture();
4955                    if delta.x != 0.0 || delta.y != 0.0 {
4956                        let local = self.pan_local(id, self.cursor);
4957                        self.send_pan(id, frus_widgets::PanEvent::Update { local, delta });
4958                    }
4959                }
4960            }
4961            Drag::Pan {
4962                id,
4963                last,
4964                moved,
4965                viewport,
4966            } => {
4967                let dx = self.cursor.x - last.x;
4968                let dy = self.cursor.y - last.y;
4969                if !*moved && (dx * dx + dy * dy) > slop * slop {
4970                    *moved = true;
4971                }
4972                if *moved {
4973                    let view = self.runtime.interactive.entry(*id).or_default();
4974                    // The finger pushes the content, bounded to the frame.
4975                    *view = view.pan(dx, dy).clamped(*viewport);
4976                    *last = self.cursor;
4977                    self.track_gesture();
4978                }
4979            }
4980            Drag::Item {
4981                source,
4982                start,
4983                moved,
4984                over,
4985            } => {
4986                let dx = self.cursor.x - start.x;
4987                let dy = self.cursor.y - start.y;
4988                // Under the threshold nothing is lifted: the press may still be a click
4989                // on whatever the draggable wraps.
4990                if !*moved && (dx * dx + dy * dy) > slop * slop {
4991                    *moved = true;
4992                }
4993                if !*moved {
4994                    return;
4995                }
4996                // The target under the pointer, but only if it would take this payload:
4997                // a target that refuses is not highlighted, so the answer is visible
4998                // before the finger lifts rather than after.
4999                let payload = source.payload;
5000                let candidate = self
5001                    .ui
5002                    .as_ref()
5003                    .and_then(|ui| ui.drop_zone_at(self.cursor))
5004                    .filter(|zone| {
5005                        self.tree
5006                            .as_ref()
5007                            .and_then(|tree| find_widget(tree.as_ref(), zone.id))
5008                            .is_some_and(|widget| widget.accepts_drag(payload))
5009                    })
5010                    .map(|zone| zone.id);
5011                if *over != candidate {
5012                    *over = candidate;
5013                    self.runtime.drag_over = candidate;
5014                }
5015                self.request_redraw();
5016            }
5017            Drag::Scroll {
5018                id,
5019                last,
5020                moved,
5021                carried,
5022                dismiss,
5023                axis,
5024                pan,
5025            } => {
5026                let dx = self.cursor.x - last.x;
5027                let dy = self.cursor.y - last.y;
5028                // Under the threshold we do not scroll yet; the gesture may be a tap.
5029                if !*moved && (dx * dx + dy * dy) > slop * slop {
5030                    *moved = true;
5031                    // The moment of arbitration. A scroll and a swipe start identically,
5032                    // so the question is not who is on top but **which way the finger
5033                    // went**: along the item's swipe axis it is a dismissal, across it a
5034                    // scroll. Deciding once, here, is what keeps the loser out of the
5035                    // gesture entirely — a swipe that also scrolled the list would be
5036                    // worse than either.
5037                    // A detector's drag, by the same rule: one kept to an axis takes a movement
5038                    // along it; a free one takes a movement no scroll under it runs along —
5039                    // the reference's pan, which loses a vertical movement to a vertical scroll
5040                    // because its slop is twice the scroll's (milestone 582).
5041                    if let Some((target, pan_axis)) = pan.take() {
5042                        let down = dy.abs() >= dx.abs();
5043                        let wins = match pan_axis {
5044                            frus_widgets::PanAxis::Horizontal => !down,
5045                            frus_widgets::PanAxis::Vertical => down,
5046                            frus_widgets::PanAxis::Free => {
5047                                let chain: Vec<_> = self
5048                                    .ui
5049                                    .as_ref()
5050                                    .map(|ui| ui.scroll_chain(*last).collect())
5051                                    .unwrap_or_default();
5052                                // Nothing under the finger scrolls that way (`claim_area`
5053                                // without its fallback to the area pressed on).
5054                                !chain.iter().any(|a| a.takes_user_scroll(down))
5055                            }
5056                        };
5057                        if wins {
5058                            // The scroll gives its offset back untouched: it never moved.
5059                            self.runtime.release_scroll(*id);
5060                            let start = *last;
5061                            self.drag = Some(Drag::Gesture {
5062                                id: target,
5063                                axis: pan_axis,
5064                                start,
5065                                last: start,
5066                                moved: false,
5067                            });
5068                            self.handle_drag();
5069                            return;
5070                        }
5071                    }
5072                    if let Some(item) = dismiss.take() {
5073                        let along = if item.spec.axis.is_horizontal() {
5074                            dx.abs() > dy.abs()
5075                        } else {
5076                            dy.abs() > dx.abs()
5077                        };
5078                        if along {
5079                            // The list gives the offset back untouched: it never moved.
5080                            self.runtime.release_scroll(*id);
5081                            self.drag = Some(Drag::Dismiss {
5082                                item,
5083                                last: *last,
5084                                moved: true,
5085                            });
5086                            self.handle_drag();
5087                            return;
5088                        }
5089                    }
5090                    // And the same arbitration between the two **axes** — which settles,
5091                    // with it, *which* of the scrollables under the finger the gesture
5092                    // belongs to. The reference does both at once in its arena: every
5093                    // scrollable in the stack enters a recogniser for its own axis, the
5094                    // one the finger matches wins, and the losers are out of the gesture
5095                    // entirely. Without the first half a page scrolling down drifts
5096                    // sideways the whole way, since no finger travels in a straight line;
5097                    // without the second, a strip of chips that only slides across stops
5098                    // the page behind it from scrolling at all.
5099                    let chain: Vec<_> = self
5100                        .ui
5101                        .as_ref()
5102                        .map(|ui| ui.scroll_chain(*last).collect())
5103                        .unwrap_or_default();
5104                    let down = dy.abs() >= dx.abs();
5105                    if let Some(area) = claim_area(&chain, *id, down) {
5106                        if area.id != *id {
5107                            // The area under the finger lost: it gives its offset back
5108                            // untouched, since it never moved. The winner is caught and
5109                            // held exactly as the press would have done to it.
5110                            self.runtime.release_scroll(*id);
5111                            let physics = area.physics;
5112                            *carried = self.runtime.catch_scroll_fling(area.id, physics);
5113                            self.runtime.hold_scroll(area.id);
5114                            *id = area.id;
5115                        }
5116                        *axis = Some(claim_axis(
5117                            dx,
5118                            dy,
5119                            area.takes_user_scroll(false),
5120                            area.takes_user_scroll(true),
5121                        ));
5122                    } else {
5123                        *axis = Some(down);
5124                    }
5125                }
5126                // Only the claimed axis moves. The other delta is not held back for later:
5127                // it is not part of this gesture at all.
5128                let (dx, mut dy) = match *axis {
5129                    Some(true) => (0.0, dy),
5130                    Some(false) => (dx, 0.0),
5131                    None => (dx, dy),
5132                };
5133                if *moved && *axis == Some(true) {
5134                    // A list inside a sheet shares the finger with it (milestone 515).
5135                    // Every movement is split — the sheet first going up, the list
5136                    // first going down — so the gesture changes hands in the middle of a
5137                    // drag, and what the list is left with goes through its physics below
5138                    // exactly as a movement of its own would.
5139                    let sheet = self
5140                        .ui
5141                        .as_ref()
5142                        .filter(|ui| ui.scroll_region(*id).is_some_and(|a| !a.reverse_y))
5143                        .and_then(|ui| ui.sheet_holding(*id))
5144                        .filter(|sheet| sheet.available > 0.0)
5145                        .cloned();
5146                    if let Some(sheet) = sheet {
5147                        let px = sheet.available;
5148                        let offset = self.runtime.scroll.get(id).map_or(0.0, |o| o.1);
5149                        let size = self.runtime.sheet_size(sheet.id, &sheet.spec) * px;
5150                        let (grown, listed) = frus_widgets::split_sheet_drag(
5151                            -dy,
5152                            offset,
5153                            size,
5154                            sheet.spec.floor() * px,
5155                            sheet.spec.max * px,
5156                        );
5157                        if grown != 0.0 {
5158                            self.runtime
5159                                .sheet_drag(sheet.id, &sheet.spec, grown / px, px);
5160                        }
5161                        dy = -listed;
5162                    }
5163                }
5164                if *moved {
5165                    let area = self.ui.as_ref().and_then(|u| u.scroll_region(*id));
5166                    // A region gone from the frame moves as the application's behaviour says.
5167                    let physics = area.map(|a| a.physics).unwrap_or_else(|| {
5168                        self.app
5169                            .scroll_behavior()
5170                            .scroll_physics(&self.themes.displayed(&self.app))
5171                    });
5172                    let cur = self.runtime.scroll.get(id).copied().unwrap_or((0.0, 0.0));
5173                    // The finger pushes the content, so we follow the delta at once —
5174                    // but only as far as the physics allows. Past an edge, bouncing
5175                    // physics resists more and more (the rubber band) while clamping
5176                    // physics refuses the move outright.
5177                    // What the physics refuses is exactly the distance the user asked
5178                    // for and did not get — which is what the glow acknowledges.
5179                    let axis = |metrics: frus_widgets::ScrollMetrics, delta: f32| {
5180                        let applied = physics.apply_user_offset(metrics, delta);
5181                        let proposed = metrics.pixels + applied;
5182                        let refused = physics.apply_boundary_conditions(metrics, proposed);
5183                        (proposed - refused, refused)
5184                    };
5185                    let (nx, ny, refused) = match area {
5186                        Some(area) => {
5187                            let (ddx, ddy) = area.offset_delta((dx, dy));
5188                            let (nx, rx) = axis(area.metrics_x(cur.0), ddx);
5189                            let (ny, ry) = axis(area.metrics_y(cur.1), ddy);
5190                            (nx, ny, Some((area, rx, ry)))
5191                        }
5192                        None => (cur.0 - dx, cur.1 - dy, None),
5193                    };
5194                    if let Some((area, rx, ry)) = refused {
5195                        // A pull-to-refresh area listening above this scrollable takes
5196                        // the **top** edge; the glow takes the other three. Both answer
5197                        // "there is nothing more that way", and giving both would say it
5198                        // twice — the indicator being the more useful of the two, since
5199                        // it leads somewhere.
5200                        if let Some(host) = area.refresh {
5201                            self.feed_refresh(host, physics, area.viewport.height, cur.1, ny, ry);
5202                        }
5203                        let cursor = self.cursor;
5204                        for (refused, vertical, extent) in [
5205                            (rx, false, area.viewport.width),
5206                            (ry, true, area.viewport.height),
5207                        ] {
5208                            if refused.abs() < 1e-3 {
5209                                continue;
5210                            }
5211                            let edge = area.refused_edge(vertical, refused);
5212                            // The refresh area takes the edge the axis **starts** at,
5213                            // which is the bottom of a reversed one.
5214                            if area.refresh.is_some() && edge == area.start_edge(vertical) {
5215                                continue;
5216                            }
5217                            let (offset, cross) =
5218                                frus_widgets::glow_cross_axis(area.viewport, edge, cursor);
5219                            self.runtime.glow_pull(
5220                                area.id,
5221                                area.overscroll,
5222                                edge,
5223                                refused,
5224                                extent,
5225                                offset,
5226                                cross,
5227                            );
5228                        }
5229                        // Content that moves again lets its stretch go, as the
5230                        // reference's does on any scroll that is not an overscroll.
5231                        if rx.abs() < 1e-3 && ry.abs() < 1e-3 && (nx, ny) != cur {
5232                            self.runtime.stretch_release(area.id);
5233                        }
5234                    }
5235                    self.runtime.scroll.insert(*id, (nx, ny));
5236                    self.runtime.scroll_target.insert(*id, (nx, ny));
5237                    self.runtime.scroll_velocity.remove(id);
5238                    *last = self.cursor;
5239                    self.track_gesture();
5240                }
5241            }
5242            Drag::Dismiss { item, last, moved } => {
5243                let dx = self.cursor.x - last.x;
5244                let dy = self.cursor.y - last.y;
5245                if !*moved && (dx * dx + dy * dy) > slop * slop {
5246                    *moved = true;
5247                }
5248                if *moved {
5249                    let delta = if item.spec.axis.is_horizontal() {
5250                        dx
5251                    } else {
5252                        dy
5253                    };
5254                    self.runtime
5255                        .dismiss_drag(item.id, delta, item.extent(), item.spec.axis);
5256                    *last = self.cursor;
5257                    self.track_gesture();
5258                }
5259            }
5260            Drag::Sheet {
5261                id,
5262                last,
5263                moved,
5264                available,
5265            } => {
5266                let dx = self.cursor.x - last.x;
5267                let dy = self.cursor.y - last.y;
5268                if !*moved && (dx * dx + dy * dy) > slop * slop {
5269                    *moved = true;
5270                }
5271                if *moved && *available > 0.0 {
5272                    let spec = self
5273                        .ui
5274                        .as_ref()
5275                        .and_then(|ui| ui.sheet(*id))
5276                        .map(|sheet| sheet.spec.clone());
5277                    if let Some(spec) = spec {
5278                        // Up grows it: the screen's y runs the other way.
5279                        self.runtime
5280                            .sheet_drag(*id, &spec, -dy / *available, *available);
5281                    }
5282                    *last = self.cursor;
5283                    self.track_gesture();
5284                }
5285            }
5286            Drag::Back { start_x } => {
5287                let width = self.logical_width();
5288                // Under RTL the finger slides **left** from the right edge, so progress
5289                // runs the other way.
5290                let sign = if self.is_rtl() { -1.0 } else { 1.0 };
5291                let progress = (sign * (self.cursor.x - *start_x) / width).clamp(0.0, 1.0);
5292                self.track_gesture();
5293                self.build_dirty = true;
5294                self.app.back_gesture(progress);
5295            }
5296        }
5297        self.drag = Some(drag);
5298        self.request_redraw();
5299    }
5300
5301    /// The refresh area a scrollable sits inside, if any.
5302    fn refresh_host_of(&self, scrollable: WidgetId) -> Option<WidgetId> {
5303        self.ui
5304            .as_ref()
5305            .and_then(|ui| ui.scroll_region(scrollable))
5306            .and_then(|area| area.refresh)
5307    }
5308
5309    /// Ends the pull of whichever refresh area holds `scrollable`, and dispatches its
5310    /// message when the pull was armed.
5311    fn release_refresh(&mut self, scrollable: WidgetId) {
5312        let Some(host) = self.refresh_host_of(scrollable) else {
5313            return;
5314        };
5315        if !self.runtime.refresh_release(host) {
5316            return;
5317        }
5318        let message = self
5319            .tree
5320            .as_ref()
5321            .and_then(|tree| find_widget(tree.as_ref(), host))
5322            .and_then(|widget| widget.on_refresh());
5323        if let Some(message) = message {
5324            self.dispatch(message);
5325        }
5326    }
5327
5328    /// Feeds one move of a drag into the pull of the refresh area `host`.
5329    ///
5330    /// The two physics put the overscroll in **different places**, so the signal is
5331    /// read differently:
5332    ///
5333    /// - **Clamping** refuses the movement and pins the offset at the edge, so the
5334    ///   refused amount is the only trace the gesture leaves. It is incremental, and
5335    ///   the physics returns nothing at all for a move back towards the content — so
5336    ///   an eased-off pull holds rather than retracting, which is right: the finger has
5337    ///   not let go.
5338    /// - **Bouncing** lets the offset go past the edge, so the *depth* it reached is
5339    ///   the signal, and the change in that depth is signed. The indicator therefore
5340    ///   follows the rubber band back in as the finger returns, which is also right:
5341    ///   there, the content itself is already saying so.
5342    ///
5343    /// Leaving the top edge at all ends the pull. The gesture has become an ordinary
5344    /// scroll, and an indicator still hanging there would be promising something the
5345    /// release is no longer going to deliver.
5346    fn feed_refresh(
5347        &mut self,
5348        host: WidgetId,
5349        physics: frus_widgets::ScrollPhysics,
5350        extent: f32,
5351        before: f32,
5352        after: f32,
5353        refused_y: f32,
5354    ) {
5355        if after > 0.5 {
5356            self.runtime.refresh_cancel(host);
5357            return;
5358        }
5359        let pulled = if physics.allows_overscroll() {
5360            (-after).max(0.0) - (-before).max(0.0)
5361        } else {
5362            -refused_y
5363        };
5364        if pulled.abs() > 1e-3 {
5365            self.runtime.refresh_pull(host, pulled, extent);
5366        }
5367    }
5368
5369    /// How far the pointer must travel before a press becomes a drag, given what
5370    /// kind of pointer it is.
5371    fn hit_slop(&self) -> f32 {
5372        if self.pointer_touch {
5373            TOUCH_SLOP
5374        } else {
5375            PRECISE_SLOP
5376        }
5377    }
5378
5379    /// Seconds since the drag under way began — the clock the velocity tracker's
5380    /// samples are stamped with.
5381    fn gesture_now(&self) -> f32 {
5382        match self.gesture_clock {
5383            // The test driver's clock: the frames it has run, not the wall's.
5384            Some((now, start)) => now - start,
5385            None => (Instant::now() - self.gesture_start).as_secs_f32(),
5386        }
5387    }
5388
5389    /// Starts a fresh gesture: the history of the previous one must not leak into
5390    /// the next, or a flick left then right would fling the wrong way.
5391    ///
5392    /// The tracker is the application's scroll behaviour's; a drag on a scroll area
5393    /// replaces it with the area's own.
5394    fn begin_gesture(&mut self) {
5395        self.gesture_velocity = self
5396            .app
5397            .scroll_behavior()
5398            .velocity_tracker(&self.themes.displayed(&self.app));
5399        self.gesture_start = Instant::now();
5400        if let Some((now, start)) = self.gesture_clock.as_mut() {
5401            *start = *now;
5402        }
5403        self.track_gesture();
5404    }
5405
5406    /// Records where the pointer is now.
5407    fn track_gesture(&mut self) {
5408        let now = self.gesture_now();
5409        self.gesture_velocity.add_position(now, self.cursor);
5410    }
5411
5412    /// What the gesture tracker makes of the drag as it stands.
5413    fn gesture_estimate(&self) -> VelocityEstimate {
5414        self.gesture_velocity
5415            .estimate(self.gesture_now())
5416            .unwrap_or(VelocityEstimate::STILL)
5417    }
5418
5419    /// [`fling_velocity`], gated on this pointer's slop.
5420    fn fling_velocity(&self, estimate: VelocityEstimate) -> (f32, f32) {
5421        fling_velocity(estimate, self.hit_slop())
5422    }
5423
5424    /// A scroll fling: projects each axis's ballistic destination, friction in closed
5425    /// form, bounds it with the elastic overshoot, and primes the scroll spring with
5426    /// the finger's momentum.
5427    fn fling(&mut self, id: WidgetId, velocity: (f32, f32)) {
5428        let Some(area) = self.ui.as_ref().and_then(|ui| ui.scroll_region(id)) else {
5429            return;
5430        };
5431        let physics = area.physics;
5432        // The physics decides everything from here: whether there is a fling at all,
5433        // how far it runs, and what happens at the edges. A release too slow to
5434        // fling still gets a chance to spring an overscroll back.
5435        if self.runtime.fling_scroll(area, physics, velocity) {
5436            self.request_redraw();
5437        }
5438    }
5439
5440    /// The topmost **reorderable** widget under `point`, as `(id, flat index)`. It
5441    /// uses the reorderables' registry, which is independent of clicking, and so covers
5442    /// the Kanban cards and drop zones, which are not clickable.
5443    fn reorderable_at(&self, point: Point) -> Option<(WidgetId, usize, bool)> {
5444        let id = self.ui.as_ref()?.reorderable_at(point)?;
5445        let widget = self
5446            .tree
5447            .as_ref()
5448            .and_then(|tree| find_widget(tree.as_ref(), id))?;
5449        // A target-**only** widget, a drop zone: reorderable but not grabbable, so no
5450        // drag starts on it. Dropping still aims at it, through `ui.reorderable_at`.
5451        if !widget.reorder_draggable() {
5452            return None;
5453        }
5454        let from = widget.reorder_index()?;
5455        Some((id, from, widget.drag_needs_long_press()))
5456    }
5457
5458    /// **What is actually moving**, given what was grabbed: its id and its box.
5459    ///
5460    /// Usually they are the same widget. They are not when what was grabbed is a **grip**
5461    /// — a source that is not a target — because a grip is a 40-pixel gutter inside a much
5462    /// larger row, and everything the preview is made of is the row's: the ghost is the
5463    /// row lifted out of the frame, the gap that closes behind it is the row's height, and
5464    /// the band the neighbours are matched against is the row's width. Taking the grip's
5465    /// box instead would lift an icon and open a slot two centimetres too narrow.
5466    ///
5467    /// The row is found by the index the two share, among the reorderables under the
5468    /// grip's own middle — which is inside its row by construction.
5469    fn reorder_source(&self, id: WidgetId, from: usize) -> Option<(WidgetId, Rect)> {
5470        let ui = self.ui.as_ref()?;
5471        let tree = self.tree.as_ref()?;
5472        let own = ui.widget_rect(id)?;
5473        let grabbed = find_widget(tree.as_ref(), id)?;
5474        if grabbed.reorder_droppable() {
5475            return Some((id, own));
5476        }
5477        let middle = Point::new(own.x + own.width * 0.5, own.y + own.height * 0.5);
5478        let slot = ui.reorderables_at(middle).find(|other| {
5479            find_widget(tree.as_ref(), *other)
5480                .is_some_and(|w| w.reorder_droppable() && w.reorder_index() == Some(from))
5481        })?;
5482        // A grip whose row is nowhere to be found still moves something: itself, which is
5483        // wrong-looking but not a crash, and cannot happen while the two are built
5484        // together.
5485        Some((slot, ui.widget_rect(slot).unwrap_or(own)))
5486    }
5487
5488    /// The row the reorder under way carries, and its box: the one taken when the drag was
5489    /// first carried, moved with the content since — or, before any frame has carried it,
5490    /// the frame's.
5491    fn reorder_carried(&self) -> Option<(WidgetId, Rect)> {
5492        match &self.drag {
5493            Some(Drag::Reorder {
5494                carried: Some(carried),
5495                ..
5496            }) => Some(*carried),
5497            Some(Drag::Reorder { id, from, .. }) => self.reorder_source(*id, *from),
5498            _ => None,
5499        }
5500    }
5501
5502    /// Takes the carried row's box from the frame, once: the first frame a reorder is carried
5503    /// in, before anything has scrolled it. From then on it goes with the content, and the
5504    /// frame is no longer asked — a row carried to an edge is clipped by it, and a row
5505    /// scrolled out of sight is not in the frame at all.
5506    fn keep_carried_box(&mut self) {
5507        let source = match &self.drag {
5508            Some(Drag::Reorder {
5509                id,
5510                from,
5511                moved: true,
5512                carried: None,
5513                ..
5514            }) => self.reorder_source(*id, *from),
5515            _ => return,
5516        };
5517        if let Some(Drag::Reorder { carried, .. }) = self.drag.as_mut() {
5518            *carried = source;
5519        }
5520    }
5521
5522    /// The topmost reorderable under `point` that something can actually be **dropped**
5523    /// on. It is not always the topmost reorderable: a list row's grip is a source and
5524    /// not a target, and what is under it is the row the drop is really aimed at.
5525    fn reorder_target_at(&self, point: Point) -> Option<WidgetId> {
5526        let ui = self.ui.as_ref()?;
5527        let tree = self.tree.as_ref()?;
5528        ui.reorderables_at(point)
5529            .find(|id| find_widget(tree.as_ref(), *id).is_some_and(|w| w.reorder_droppable()))
5530    }
5531
5532    /// Where the reorderable `id`, grabbed at index `from`, is **dropped** if released now:
5533    /// the target under the pointer, or — over no target at all — the nearest slot of its own
5534    /// list, for one inserted between its neighbours, along the axis it is reordered on.
5535    ///
5536    /// The insertion line and the release both ask this, so the line cannot promise a place
5537    /// the drop does not keep. The second half is milestone 518: a row carried to the bottom
5538    /// of a phone's list is over what follows the list, and the release used to put it back.
5539    /// Milestone 527 turned it on its side, for a list whose rows run across.
5540    fn reorder_drop_target(
5541        &self,
5542        id: WidgetId,
5543        from: usize,
5544        carried: Option<(WidgetId, Rect)>,
5545    ) -> Option<WidgetId> {
5546        if let Some(target) = self.reorder_target_at(self.cursor) {
5547            return Some(target);
5548        }
5549        let tree = self.tree.as_ref()?;
5550        let Some(ReorderMotion::Slots(axis)) = find_widget(tree.as_ref(), id).map(reorder_motion)
5551        else {
5552            return None;
5553        };
5554        // Among the slots of the row that moves, which is not the grip that was grabbed —
5555        // and which may have been scrolled out of the frame by the carry itself.
5556        let row = match carried {
5557            Some((row, _)) => row,
5558            None => self.reorder_source(id, from)?.0,
5559        };
5560        let slots = reorder_siblings(self.ui.as_ref()?, tree.as_ref(), row);
5561        let boxes: Vec<Rect> = slots.iter().map(|(_, rect)| *rect).collect();
5562        nearest_reorder_slot(self.cursor, &boxes, axis).map(|index| slots[index].0)
5563    }
5564
5565    /// What is being **carried** this frame, where it is now: the box the ghost is drawn
5566    /// at, for a reorder or for a lifted item. `None` when nothing is engaged.
5567    ///
5568    /// It is the ghost's box and not the pointer, because the reference scrolls when the
5569    /// *item* reaches the edge, and that is the honest moment: a row half off the bottom
5570    /// is already asking to go further, whatever the finger holding it is over.
5571    fn carried_rect(&self) -> Option<Rect> {
5572        match &self.drag {
5573            Some(Drag::Reorder {
5574                start, moved: true, ..
5575            }) => {
5576                let (_, src) = self.reorder_carried()?;
5577                // The same offsets the ghost is painted at, and for the same reason a
5578                // column's ghost only rises: it moves along its own axis.
5579                let (gx, gy) = ghost_offset(
5580                    self.dragged_reorder_motion()
5581                        .unwrap_or(ReorderMotion::Columns),
5582                    (self.cursor.x - start.x, self.cursor.y - start.y),
5583                );
5584                Some(src.translate(gx, gy))
5585            }
5586            Some(Drag::Item {
5587                source,
5588                start,
5589                moved: true,
5590                ..
5591            }) => Some(
5592                source
5593                    .rect
5594                    .translate(self.cursor.x - start.x, self.cursor.y - start.y),
5595            ),
5596            _ => None,
5597        }
5598    }
5599
5600    /// Scrolls the area under the pointer while what is being carried hangs past one of
5601    /// its edges — the gap `Draggable` has had since milestone 285, and what a list longer
5602    /// than a screen needs before it can be reordered at all.
5603    ///
5604    /// The area is the innermost one under the **pointer** that a user could move by
5605    /// hand: an area that refuses a finger refuses this too, so a list that fits its
5606    /// viewport never twitches.
5607    fn autoscroll_carried(&mut self, dt: f32) -> bool {
5608        self.keep_carried_box();
5609        let Some(carried) = self.carried_rect() else {
5610            return false;
5611        };
5612        let area = {
5613            let scroll = &self.runtime.scroll;
5614            self.ui.as_ref().and_then(|ui| {
5615                ui.scroll_chain(self.cursor).find(|area| {
5616                    area.accepts_user_offset(scroll.get(&area.id).copied().unwrap_or((0.0, 0.0)))
5617                })
5618            })
5619        };
5620        let Some(area) = area else {
5621            return false;
5622        };
5623        let offset = self
5624            .runtime
5625            .scroll
5626            .get(&area.id)
5627            .copied()
5628            .unwrap_or((0.0, 0.0));
5629        // What is still hidden on either side, in **screen** terms: on a reversed axis
5630        // the offset is measured from the other end, so the two swap over.
5631        let room = |offset: f32, max: f32, reverse: bool| {
5632            if reverse {
5633                (max - offset, offset)
5634            } else {
5635                (offset, max - offset)
5636            }
5637        };
5638        let view = area.viewport;
5639        let dx = edge_autoscroll(
5640            (carried.x, carried.x + carried.width),
5641            (view.x, view.x + view.width),
5642            room(offset.0, area.max_x, area.reverse_x),
5643            dt,
5644        )
5645        .unwrap_or(0.0);
5646        let dy = edge_autoscroll(
5647            (carried.y, carried.y + carried.height),
5648            (view.y, view.y + view.height),
5649            room(offset.1, area.max_y, area.reverse_y),
5650            dt,
5651        )
5652        .unwrap_or(0.0);
5653        if dx == 0.0 && dy == 0.0 {
5654            return false;
5655        }
5656        // A movement of the content becomes a change of offset the area's own way, which
5657        // is the one place the sign of a reversed axis is decided.
5658        let delta = area.offset_delta((dx, dy));
5659        let next = (
5660            (offset.0 + delta.0).clamp(0.0, area.max_x),
5661            (offset.1 + delta.1).clamp(0.0, area.max_y),
5662        );
5663        if next == offset {
5664            return false;
5665        }
5666        self.runtime.scroll.insert(area.id, next);
5667        // The target follows, or the inertia the area was resting at would spring the
5668        // list straight back out from under the row.
5669        self.runtime.scroll_target.insert(area.id, next);
5670        self.runtime.scroll_velocity.remove(&area.id);
5671        // What is carried stays under the finger. Its ghost is drawn at the box the frame
5672        // puts it at, offset by how far the finger has moved since the press; the frame now
5673        // puts that box where the content went, so the press goes there too. Otherwise the
5674        // ghost rides up with the list, stops hanging over the edge, and the scroll it
5675        // started starves itself — seen on a phone, milestone 517.
5676        if let Some(drag) = self.drag.as_mut() {
5677            follow_content(
5678                drag,
5679                area.offset_delta((next.0 - offset.0, next.1 - offset.1)),
5680            );
5681        }
5682        true
5683    }
5684
5685    /// How the reorderable currently **grabbed** moves, if a drag is under way. It is what
5686    /// keeps the **pointer spring** (`reorder_x` following the pointer) to `Table`'s columns;
5687    /// the Kanban cards and a list's rows spring their insertion line instead.
5688    fn dragged_reorder_motion(&self) -> Option<ReorderMotion> {
5689        let Some(Drag::Reorder { id, .. }) = self.drag else {
5690            return None;
5691        };
5692        self.tree
5693            .as_ref()
5694            .and_then(|t| find_widget(t.as_ref(), id))
5695            .map(reorder_motion)
5696    }
5697
5698    /// Steps the reorder springs one frame, per gesture — see the frame loop — and says
5699    /// whether they are still moving.
5700    fn advance_reorder_springs(&mut self, dt: f32) -> bool {
5701        let reorder_motion = matches!(self.drag, Some(Drag::Reorder { moved: true, .. }))
5702            .then(|| self.dragged_reorder_motion())
5703            .flatten();
5704        match reorder_motion {
5705            Some(ReorderMotion::Columns) => {
5706                self.reorder_x = spring_toward(self.reorder_x, self.cursor.x, dt, 0.07);
5707                (self.cursor.x - self.reorder_x).abs() > 0.5
5708            }
5709            Some(ReorderMotion::Slots(ReorderAxis::Vertical)) => {
5710                match self.reorder_drop_line(drag_preview::INSERT_THICKNESS) {
5711                    Some(target) => {
5712                        self.reorder_y = spring_toward(self.reorder_y, target.y, dt, 0.07);
5713                        (target.y - self.reorder_y).abs() > 0.5
5714                    }
5715                    None => false,
5716                }
5717            }
5718            Some(ReorderMotion::Slots(ReorderAxis::Horizontal)) => {
5719                match self.reorder_drop_line(drag_preview::INSERT_THICKNESS) {
5720                    Some(target) => {
5721                        self.reorder_x = spring_toward(self.reorder_x, target.x, dt, 0.07);
5722                        (target.x - self.reorder_x).abs() > 0.5
5723                    }
5724                    None => false,
5725                }
5726            }
5727            None => false,
5728        }
5729    }
5730
5731    /// Paints the **reorder preview** on top of the scene, unclipped: the source
5732    /// column dimmed, a **drop indicator** at the target column's insertion edge, and a
5733    /// **lifted card** — shadow plus a `primary` border — following the pointer. No
5734    /// effect outside an engaged header drag.
5735    /// Names, on the console, every box this frame whose children did not fit inside it.
5736    ///
5737    /// The reference treats an overflowing flex as an error condition, paints a striped
5738    /// band along the offending edge and writes to the console; nothing here said anything
5739    /// at all, which is how a task row's delete button came to be laid out past the window
5740    /// and stayed there for three milestones — drawn nowhere, hittable nowhere. Half of
5741    /// that is now here: the words, once per site, with the two ways out the reference
5742    /// suggests. The striped band is not.
5743    ///
5744    /// Once per site, because a layout that does not fit does not fit on every frame, and
5745    /// sixty lines a second is the same as silence.
5746    fn report_overflows(&self, ui: &Ui<A::Message>) {
5747        for o in ui.overflows() {
5748            // The site: a box, an edge, and the amount rounded to the pixel. Enough to
5749            // stay quiet while a window is resized past the same defect, and to speak up
5750            // when the defect changes.
5751            let mut key = std::collections::hash_map::DefaultHasher::new();
5752            use std::hash::{Hash, Hasher};
5753            (
5754                o.rect.width.to_bits(),
5755                o.rect.height.to_bits(),
5756                o.side as u8,
5757                o.amount.round() as i32,
5758            )
5759                .hash(&mut key);
5760            if !self.reported_overflows.borrow_mut().insert(key.finish()) {
5761                continue;
5762            }
5763            let side = match o.side {
5764                frus_widgets::Side::Left => "left",
5765                frus_widgets::Side::Right => "right",
5766                frus_widgets::Side::Top => "top",
5767                frus_widgets::Side::Bottom => "bottom",
5768            };
5769            log::warn!(
5770                "a box {:.0}x{:.0} is overflowed by {:.0} px on the {side}: its children do not \
5771                 fit inside it, and what runs past the edge is drawn outside its parent — \
5772                 invisible where something clips it, and untappable where it leaves the window. \
5773                 Give the child that should give way an `Expanded`, or put the content in a \
5774                 `SingleChildScrollView`.",
5775                o.rect.width,
5776                o.rect.height,
5777                o.amount,
5778            );
5779        }
5780    }
5781
5782    /// Winit's key event as a [`KeyStroke`] — the general vocabulary shortcuts are bound
5783    /// to, as opposed to [`Key`], which is the one text editing needs.
5784    ///
5785    /// `None` for anything with no place in a shortcut: a dead key, a compose sequence, a
5786    /// modifier pressed on its own.
5787    fn keystroke(&self, event: &KeyDown) -> Option<KeyStroke> {
5788        let key = match &event.logical_key {
5789            WinitKey::Named(NamedKey::Enter) => ShortcutKey::Enter,
5790            WinitKey::Named(NamedKey::Escape) => ShortcutKey::Escape,
5791            WinitKey::Named(NamedKey::Tab) => ShortcutKey::Tab,
5792            WinitKey::Named(NamedKey::Space) => ShortcutKey::Space,
5793            WinitKey::Named(NamedKey::Backspace) => ShortcutKey::Backspace,
5794            WinitKey::Named(NamedKey::Delete) => ShortcutKey::Delete,
5795            WinitKey::Named(NamedKey::ArrowUp) => ShortcutKey::Up,
5796            WinitKey::Named(NamedKey::ArrowDown) => ShortcutKey::Down,
5797            WinitKey::Named(NamedKey::ArrowLeft) => ShortcutKey::Left,
5798            WinitKey::Named(NamedKey::ArrowRight) => ShortcutKey::Right,
5799            WinitKey::Named(NamedKey::Home) => ShortcutKey::Home,
5800            WinitKey::Named(NamedKey::End) => ShortcutKey::End,
5801            WinitKey::Named(NamedKey::PageUp) => ShortcutKey::PageUp,
5802            WinitKey::Named(NamedKey::PageDown) => ShortcutKey::PageDown,
5803            WinitKey::Named(NamedKey::F1) => ShortcutKey::F(1),
5804            WinitKey::Named(NamedKey::F2) => ShortcutKey::F(2),
5805            WinitKey::Named(NamedKey::F3) => ShortcutKey::F(3),
5806            WinitKey::Named(NamedKey::F4) => ShortcutKey::F(4),
5807            WinitKey::Named(NamedKey::F5) => ShortcutKey::F(5),
5808            WinitKey::Named(NamedKey::F6) => ShortcutKey::F(6),
5809            WinitKey::Named(NamedKey::F7) => ShortcutKey::F(7),
5810            WinitKey::Named(NamedKey::F8) => ShortcutKey::F(8),
5811            WinitKey::Named(NamedKey::F9) => ShortcutKey::F(9),
5812            WinitKey::Named(NamedKey::F10) => ShortcutKey::F(10),
5813            WinitKey::Named(NamedKey::F11) => ShortcutKey::F(11),
5814            WinitKey::Character(text) => ShortcutKey::Char(text.chars().next()?),
5815            _ => return None,
5816        };
5817        Some(KeyStroke {
5818            key,
5819            ctrl: self.ctrl,
5820            shift: self.shift,
5821            alt: self.alt,
5822            meta: self.meta,
5823        })
5824    }
5825
5826    fn paint_reorder_preview(&self, ui: &Ui<A::Message>, theme: &Theme, scene: &mut Scene) {
5827        let Some(Drag::Reorder {
5828            start, moved: true, ..
5829        }) = self.drag
5830        else {
5831            return;
5832        };
5833        // What moves is the row, even when what was grabbed is the grip inside it — at the
5834        // box it was carried from, which has gone with the content.
5835        let Some((id, src)) = self.reorder_carried() else {
5836            return;
5837        };
5838        let motion = self
5839            .tree
5840            .as_ref()
5841            .and_then(|t| find_widget(t.as_ref(), id))
5842            .map(reorder_motion)
5843            .unwrap_or(ReorderMotion::Columns);
5844        let dx = self.cursor.x - start.x;
5845        let dy = self.cursor.y - start.y;
5846
5847        // The ghost's offset, per motion — see `ghost_offset`.
5848        let (gx, gy) = ghost_offset(motion, (dx, dy));
5849
5850        // The owners of the grabbed item's **subtree**: used by the ghost, to capture a
5851        // rich card's content, which its children paint (milestone 251), **and** by the
5852        // vertical reflow, to lift the card out of the preview. Fallback: the card alone.
5853        let owners: std::collections::HashSet<u64> = self
5854            .tree
5855            .as_ref()
5856            .and_then(|t| find_widget(t.as_ref(), id))
5857            .map(|w| subtree_ids(w, id).iter().map(|i| i.as_u64()).collect())
5858            .unwrap_or_else(|| std::iter::once(id.as_u64()).collect());
5859
5860        match motion {
5861            ReorderMotion::Columns => {
5862                // Reflow the neighbouring columns: the source's gap closes and the drop
5863                // slot opens, following the pointer's **smoothed** abscissa — a gentle
5864                // inertial slide, while the ghost sticks to the real pointer.
5865                let reflowed =
5866                    reflow_reorder_columns(scene.primitives(), src, self.reorder_x, id.as_u64());
5867                scene.clear();
5868                for primitive in reflowed {
5869                    scene.push_primitive(primitive);
5870                }
5871            }
5872            ReorderMotion::Slots(axis) => {
5873                // Reflow the **cards**: the lifted card's gap closes in the source
5874                // column and a slot opens under the **insertion line** in the target one.
5875                // Then the line is laid on top, at the chosen edge — the hovered half,
5876                // milestone 252.
5877                //
5878                // A **smoothed** line (milestone 265): the chosen slot's width, abscissa
5879                // and thickness are kept, but the **ordinate** is replaced by the
5880                // `reorder_y` spring — the line *and* the gap slide between cards, with
5881                // vertical inertia, instead of jumping a notch. A horizontal list's line
5882                // stands on its end, and its **abscissa** is the one that springs.
5883                let line =
5884                    self.reorder_drop_line(drag_preview::INSERT_THICKNESS)
5885                        .map(|r| match axis {
5886                            ReorderAxis::Vertical => Rect {
5887                                y: self.reorder_y,
5888                                ..r
5889                            },
5890                            ReorderAxis::Horizontal => Rect {
5891                                x: self.reorder_x,
5892                                ..r
5893                            },
5894                        });
5895                // Only what can be reordered makes room — a card, a row, a drop zone, and
5896                // what each of them paints. The reflow is geometric, and a button floating
5897                // over the list or the navigation bar under it shares the band without being
5898                // part of the list: moved with it, the first left its `+` a row above its own
5899                // disc on a phone (milestone 517).
5900                let movable = self
5901                    .tree
5902                    .as_ref()
5903                    .map(|tree| reorderable_owners(ui, tree.as_ref()))
5904                    .unwrap_or_default();
5905                let reflowed =
5906                    reflow_reorder_cards(scene.primitives(), src, line, &owners, &movable, axis);
5907                scene.clear();
5908                for primitive in reflowed {
5909                    scene.push_primitive(primitive);
5910                }
5911                if let Some(line) = line {
5912                    // Rounded across its thickness, whichever way it stands.
5913                    let thickness = match axis {
5914                        ReorderAxis::Vertical => line.height,
5915                        ReorderAxis::Horizontal => line.width,
5916                    };
5917                    scene.set_clip(Rect::UNBOUNDED);
5918                    scene.draw_rect(
5919                        line,
5920                        theme.primary,
5921                        theme.radius.min(thickness * 0.5),
5922                        0.0,
5923                        Color::TRANSPARENT,
5924                    );
5925                }
5926            }
5927        }
5928
5929        // A faithful ghost: the grabbed item's primitives, taken from the original
5930        // scene, translated and **un-clipped**, since they would otherwise be cropped at
5931        // the source. In a **rich card** the background is painted by the card but its
5932        // content — label, tags, the × button — by children, under other owners, so we
5933        // capture the primitives of the item's **whole subtree**; see `owners` above.
5934        let ghost: Vec<Primitive> = ui
5935            .scene()
5936            .primitives()
5937            .iter()
5938            .filter(|p| owners.contains(&p.owner()))
5939            .map(|p| p.translated(gx, gy).with_clip(Rect::UNBOUNDED))
5940            .collect();
5941        draw_ghost_card(scene, theme, src.translate(gx, gy), &ghost);
5942    }
5943
5944    /// The lifted item: its own primitives dimmed where it sits, and a copy of them
5945    /// floating under the pointer.
5946    ///
5947    /// The copy is taken from the frame rather than built again, so it cannot drift
5948    /// from what is on screen — a rebuilt "feedback" widget is a second definition of
5949    /// the same thing, and two definitions is one too many. The same reason the
5950    /// reorder ghost works this way.
5951    fn paint_drag_ghost(&self, ui: &Ui<A::Message>, theme: &Theme, scene: &mut Scene) {
5952        let Some(Drag::Item {
5953            source,
5954            start,
5955            moved: true,
5956            ..
5957        }) = &self.drag
5958        else {
5959            return;
5960        };
5961        let widget = self
5962            .tree
5963            .as_ref()
5964            .and_then(|tree| find_widget(tree.as_ref(), source.id));
5965        // A rich item paints its background itself and its content through children,
5966        // under other owners, so the whole subtree is captured — the same reason the
5967        // reorder ghost does.
5968        let owners: std::collections::HashSet<u64> = widget
5969            .map(|w| {
5970                subtree_ids(w, source.id)
5971                    .iter()
5972                    .map(|i| i.as_u64())
5973                    .collect()
5974            })
5975            .unwrap_or_else(|| std::iter::once(source.id.as_u64()).collect());
5976        let opacity = widget.map(|w| w.drag_ghost_opacity()).unwrap_or(1.0);
5977        let dx = self.cursor.x - start.x;
5978        let dy = self.cursor.y - start.y;
5979
5980        // What is left behind, faded in place: the item is being carried, not deleted.
5981        let original: Vec<Primitive> = scene.primitives().to_vec();
5982        scene.clear();
5983        for primitive in &original {
5984            if owners.contains(&primitive.owner()) {
5985                scene.push_faded(primitive, opacity);
5986            } else {
5987                scene.push_primitive(primitive.clone());
5988            }
5989        }
5990
5991        let ghost: Vec<Primitive> = ui
5992            .scene()
5993            .primitives()
5994            .iter()
5995            .filter(|p| owners.contains(&p.owner()))
5996            .map(|p| p.translated(dx, dy).with_clip(Rect::UNBOUNDED))
5997            .collect();
5998        draw_ghost_card(scene, theme, source.rect.translate(dx, dy), &ghost);
5999    }
6000
6001    /// The **insertion** line of the preview: a thin band at the edge of the hovered
6002    /// reorderable slot, a card, a row or a drop zone — the edge **before** it when the
6003    /// pointer is in its leading half, the edge **after** it in its trailing half. Across a
6004    /// vertical list; standing on its end in a horizontal one. `None` when the release would
6005    /// land nowhere.
6006    fn reorder_drop_line(&self, thickness: f32) -> Option<Rect> {
6007        let Some(Drag::Reorder {
6008            id, from, carried, ..
6009        }) = self.drag
6010        else {
6011            return None;
6012        };
6013        // The reorderable slot — card, row or drop zone — the release would land on: the
6014        // one under the pointer, or the nearest of the list's own.
6015        let target = self.reorder_drop_target(id, from, carried)?;
6016        let rect = self.ui.as_ref()?.widget_rect(target)?;
6017        let axis = match self
6018            .tree
6019            .as_ref()
6020            .and_then(|t| find_widget(t.as_ref(), target))
6021            .map(reorder_motion)
6022        {
6023            Some(ReorderMotion::Slots(axis)) => axis,
6024            _ => ReorderAxis::Vertical,
6025        };
6026        Some(drop_insertion_line(
6027            rect,
6028            thickness,
6029            self.reorder_insert_after(target, rect),
6030            axis,
6031            self.is_rtl(),
6032        ))
6033    }
6034
6035    /// For a slot a reorderable is **inserted** at, tells whether insertion happens
6036    /// **after** it (index +1, between it and the next) — the pointer in its trailing half,
6037    /// which `reorder_drop_after` works out along the list's axis and reading direction.
6038    /// Always `false` for `Table`'s columns, which keep their drop logic unchanged. This is
6039    /// the insertion line's counterpart on the **routing** side.
6040    fn reorder_insert_after(&self, target: WidgetId, rect: Rect) -> bool {
6041        match self
6042            .tree
6043            .as_ref()
6044            .and_then(|t| find_widget(t.as_ref(), target))
6045            .map(reorder_motion)
6046        {
6047            Some(ReorderMotion::Slots(axis)) => {
6048                reorder_drop_after(self.cursor, rect, axis, self.is_rtl())
6049            }
6050            _ => false,
6051        }
6052    }
6053
6054    /// The innermost detector that takes a drag under the pointer, with the way it drags.
6055    fn pan_target(&self) -> Option<(WidgetId, frus_widgets::PanAxis)> {
6056        let (id, _) = self.ui.as_ref()?.pan_at(self.cursor)?;
6057        let axis = find_widget(self.tree.as_ref()?.as_ref(), id)?.pan_axis()?;
6058        Some((id, axis))
6059    }
6060
6061    /// `at`, in the coordinates of the detector `id`.
6062    fn pan_local(&self, id: WidgetId, at: Point) -> Point {
6063        let origin = self
6064            .ui
6065            .as_ref()
6066            .and_then(|ui| ui.widget_rect(id))
6067            .map_or(Point::new(0.0, 0.0), |rect| Point::new(rect.x, rect.y));
6068        Point::new(at.x - origin.x, at.y - origin.y)
6069    }
6070
6071    /// Hands one moment of a drag to the detector `id`, and its message to the application.
6072    fn send_pan(&mut self, id: WidgetId, event: frus_widgets::PanEvent) {
6073        let rtl = self.is_rtl();
6074        let value = self.runtime.value(id);
6075        let (message, moved, released) = match self
6076            .tree
6077            .as_ref()
6078            .and_then(|tree| find_widget(tree.as_ref(), id))
6079        {
6080            Some(widget) => {
6081                // A widget whose own value the drag moves — a switch's thumb (milestone
6082                // 638): held where the finger puts it, and asked what letting go there
6083                // means.
6084                let moved = widget.pan_value(event, value, rtl);
6085                let released = match event {
6086                    frus_widgets::PanEvent::End { .. } if self.runtime.value_placed(id) => {
6087                        Some(widget.on_value_release(value))
6088                    }
6089                    _ => None,
6090                };
6091                (widget.on_pan(event), moved, released)
6092            }
6093            None => (None, None, None),
6094        };
6095        if let Some(v) = moved {
6096            self.runtime.hold_value(id, v);
6097        }
6098        if let Some(release) = released {
6099            self.runtime.release_value(id);
6100            if let Some(message) = release {
6101                self.dispatch(message);
6102            }
6103        }
6104        if let Some(message) = message {
6105            self.dispatch(message);
6106        }
6107        self.request_redraw();
6108    }
6109
6110    /// Sends a value drag's **start** or **end** to the widget being dragged.
6111    ///
6112    /// The fraction is worked out the same way [`apply_widget_drag`](Self::apply_widget_drag)
6113    /// works it out, from the same rectangle: an end that disagreed with the last
6114    /// `on_drag` about where the pointer finished would be a slider that settles
6115    /// somewhere its own final message never mentioned.
6116    fn dispatch_drag_edge(&mut self, id: WidgetId, rect: frus_widgets::Rect, edge: Edge) {
6117        let (x, rtl) = (self.cursor.x, self.is_rtl());
6118        let message = self
6119            .tree
6120            .as_ref()
6121            .and_then(|tree| find_widget(tree.as_ref(), id))
6122            .and_then(|widget| {
6123                let fraction = widget.drag_track().fraction(rect, x, rtl);
6124                match edge {
6125                    Edge::Start => widget.on_drag_start(fraction),
6126                    Edge::End => widget.on_drag_end(fraction),
6127                }
6128            });
6129        if let Some(message) = message {
6130            self.dispatch(message);
6131        }
6132    }
6133
6134    fn apply_widget_drag(&mut self, id: WidgetId, rect: frus_widgets::Rect, dx: f32) {
6135        let (x, rtl) = (self.cursor.x, self.is_rtl());
6136        // An accumulating handle, taking a delta, first; otherwise a slider's absolute
6137        // fraction, along the travel the widget names (milestone 648).
6138        let message = self
6139            .tree
6140            .as_ref()
6141            .and_then(|tree| find_widget(tree.as_ref(), id))
6142            .and_then(|widget| {
6143                widget
6144                    .on_drag_delta(dx)
6145                    .or_else(|| widget.on_drag(widget.drag_track().fraction(rect, x, rtl)))
6146            });
6147        if let Some(message) = message {
6148            self.dispatch(message);
6149        }
6150    }
6151
6152    /// **A key going down**: everything the shell does with one, from the system's back key
6153    /// and the inspector to a shortcut, Tab, the arrows, activation and typing. Called with
6154    /// winit's event in a window, and with one a test made in the driver (milestone 608).
6155    /// `event_loop` is what Back quits through; with none, Back that would quit does nothing.
6156    fn key_down(&mut self, event: &KeyDown, event_loop: Option<&ActiveEventLoop>) {
6157        // A keyboard interaction: the focus ring becomes visible again.
6158        if !self.runtime.focus_visible {
6159            self.runtime.focus_visible = true;
6160            self.request_redraw();
6161        }
6162
6163        // The **system** back — Android's button or gesture, the browser's back
6164        // key: it closes the topmost overlay, else pops a screen, else quits.
6165        if matches!(
6166            event.logical_key,
6167            WinitKey::Named(NamedKey::BrowserBack) | WinitKey::Named(NamedKey::GoBack)
6168        ) {
6169            if !event.repeat {
6170                if let Some(event_loop) = event_loop {
6171                    self.system_back(event_loop);
6172                }
6173            }
6174            return;
6175        }
6176
6177        // F12 toggles the **inspector**: outlines, a card for the hovered
6178        // widget, and a tree dump on stderr. Debug builds only.
6179        if matches!(event.logical_key, WinitKey::Named(NamedKey::F12)) {
6180            if cfg!(debug_assertions) && !event.repeat {
6181                self.inspector = !self.inspector;
6182                self.inspector_dump = self.inspector;
6183                self.request_redraw();
6184            }
6185            return;
6186        }
6187
6188        // **Shortcuts.** Placed after the system keys the shell owns outright
6189        // (back, F12) and before everything an application can bind, so that a
6190        // binding cannot take the inspector or the back gesture away.
6191        //
6192        // A stroke with no Ctrl, Alt or Meta goes to a focused field first: a
6193        // binding on a bare letter would otherwise make every field under it
6194        // impossible to type in.
6195        if let Some(stroke) = self.keystroke(event) {
6196            let typing = !stroke.is_command() && self.runtime.input.focused.is_some();
6197            if !typing {
6198                let msgs = self
6199                    .ui
6200                    .as_ref()
6201                    .map(|ui| ui.keystroke(stroke, self.runtime.input.focused))
6202                    .unwrap_or_default();
6203                if !msgs.is_empty() {
6204                    for msg in msgs {
6205                        self.dispatch(msg);
6206                    }
6207                    self.request_redraw();
6208                    return;
6209                }
6210            }
6211        }
6212
6213        // Tab and Shift+Tab move between focusables, even with nothing focused.
6214        if matches!(event.logical_key, WinitKey::Named(NamedKey::Tab)) {
6215            let forward = !self.shift;
6216            let next = self
6217                .ui
6218                .as_ref()
6219                .and_then(|ui| ui.focus_next(self.runtime.input.focused, forward));
6220            if next.is_some() {
6221                self.runtime.input.focused = next;
6222                self.runtime.region = None;
6223                self.reveal_focus();
6224                self.request_redraw();
6225            }
6226            return;
6227        }
6228
6229        // Escape walks **leaf to root** from the focused widget — an `OverlayPortal`
6230        // consumes it to close itself — and failing that closes the topmost
6231        // overlay, so Escape works with nothing focused.
6232        if matches!(event.logical_key, WinitKey::Named(NamedKey::Escape)) {
6233            // Auto-repeat does not trigger another close.
6234            if !event.repeat {
6235                self.escape();
6236            }
6237            return;
6238        }
6239
6240        let Some(focused) = self.runtime.input.focused else {
6241            // With nothing focused, a selection an area holds still answers to the
6242            // clipboard's Copy and to Select all.
6243            self.region_key(event);
6244            return;
6245        };
6246
6247        // Arrows navigate focus **geometrically** — except left and right in a
6248        // text field, where they move the caret; up and down navigate even out
6249        // of a single-line field.
6250        let arrow = match event.logical_key {
6251            WinitKey::Named(NamedKey::ArrowUp) => Some(FocusDirection::Up),
6252            WinitKey::Named(NamedKey::ArrowDown) => Some(FocusDirection::Down),
6253            WinitKey::Named(NamedKey::ArrowLeft) => Some(FocusDirection::Left),
6254            WinitKey::Named(NamedKey::ArrowRight) => Some(FocusDirection::Right),
6255            _ => None,
6256        };
6257        // PgUp and PgDn jump a **page** inside a multi-line field, bounded to
6258        // the field so they never leave it. No effect anywhere else.
6259        if matches!(
6260            event.logical_key,
6261            WinitKey::Named(NamedKey::PageUp) | WinitKey::Named(NamedKey::PageDown)
6262        ) {
6263            let down = matches!(event.logical_key, WinitKey::Named(NamedKey::PageDown));
6264            if self.move_caret_vertical(focused, down, true) {
6265                return;
6266            }
6267        }
6268
6269        if let Some(direction) = arrow {
6270            // In a **multi-line** field, Up and Down move the caret between
6271            // lines while it stays in the field; on the first or last line we
6272            // fall back to focus navigation and leave the field.
6273            if matches!(direction, FocusDirection::Up | FocusDirection::Down) {
6274                let down = matches!(direction, FocusDirection::Down);
6275                if self.move_caret_vertical(focused, down, false) {
6276                    return;
6277                }
6278            }
6279
6280            // Left and right arrows are offered to the focused widget first —
6281            // a range slider, say — through `on_key`. If it consumes them, focus
6282            // does not move.
6283            if matches!(direction, FocusDirection::Left | FocusDirection::Right) {
6284                let key = if matches!(direction, FocusDirection::Left) {
6285                    Key::Left {
6286                        shift: self.shift,
6287                        word: self.ctrl,
6288                    }
6289                } else {
6290                    Key::Right {
6291                        shift: self.shift,
6292                        word: self.ctrl,
6293                    }
6294                };
6295                let widget = self
6296                    .tree
6297                    .as_ref()
6298                    .and_then(|tree| find_widget(tree.as_ref(), focused));
6299                // A reorderable header moves its column by one; we capture its
6300                // position to announce it, the keyboard being how screen-reader
6301                // users reorder.
6302                let reorder_from = widget.and_then(|w| w.reorder_index());
6303                let handled = widget.map(|w| w.on_key(&key));
6304                if let Some(KeyResponse::Handled(message)) = handled {
6305                    if let Some(message) = message {
6306                        self.dispatch(message);
6307                    }
6308                    if let Some(from) = reorder_from {
6309                        let to = if matches!(direction, FocusDirection::Left) {
6310                            from.wrapping_sub(1)
6311                        } else {
6312                            from + 1
6313                        };
6314                        self.set_announcement(format!("Column moved to position {}", to + 1));
6315                    }
6316                    self.request_redraw();
6317                    return;
6318                }
6319            }
6320
6321            let is_text = self
6322                .tree
6323                .as_ref()
6324                .and_then(|tree| find_widget(tree.as_ref(), focused))
6325                .is_some_and(is_text_field);
6326            let navigates =
6327                !is_text || matches!(direction, FocusDirection::Up | FocusDirection::Down);
6328            if navigates {
6329                if let Some(next) = self
6330                    .ui
6331                    .as_ref()
6332                    .and_then(|ui| ui.focus_directional(focused, direction))
6333                {
6334                    self.runtime.input.focused = Some(next);
6335                    self.reveal_focus();
6336                    self.request_redraw();
6337                }
6338                return;
6339            }
6340        }
6341
6342        // Home and End are offered to the focused widget — a range slider maps
6343        // them to min and max — before the default action. A text field ignores
6344        // them here (`on_key` returns Ignored) and falls back to ordinary
6345        // editing further down.
6346        if matches!(
6347            event.logical_key,
6348            WinitKey::Named(NamedKey::Home) | WinitKey::Named(NamedKey::End)
6349        ) {
6350            let key = if matches!(event.logical_key, WinitKey::Named(NamedKey::Home)) {
6351                Key::Home {
6352                    shift: self.shift,
6353                    doc: self.ctrl,
6354                }
6355            } else {
6356                Key::End {
6357                    shift: self.shift,
6358                    doc: self.ctrl,
6359                }
6360            };
6361            let handled = self
6362                .tree
6363                .as_ref()
6364                .and_then(|tree| find_widget(tree.as_ref(), focused))
6365                .map(|widget| widget.on_key(&key));
6366            if let Some(KeyResponse::Handled(message)) = handled {
6367                if let Some(message) = message {
6368                    self.dispatch(message);
6369                }
6370                self.request_redraw();
6371                return;
6372            }
6373        }
6374
6375        // Keyboard activation, Enter or Space, of a clickable focusable: a
6376        // button, a checkbox, a switch. Text fields, which have no `on_click`,
6377        // fall back to ordinary editing — Enter submits, Space is a space.
6378        if matches!(
6379            event.logical_key,
6380            WinitKey::Named(NamedKey::Enter) | WinitKey::Named(NamedKey::Space)
6381        ) {
6382            let widget = self
6383                .tree
6384                .as_ref()
6385                .and_then(|tree| find_widget(tree.as_ref(), focused))
6386                .filter(|widget| widget.focusable());
6387            let message = widget.and_then(|widget| widget.on_click());
6388            if let Some(message) = message {
6389                // Auto-repeat does not machine-gun the activation: holding
6390                // Space on a button is one single click.
6391                if !event.repeat {
6392                    // The spoken announcement of the effect — a sort, a
6393                    // selection — captured before `dispatch` rebuilds the tree.
6394                    let announce = widget.and_then(|widget| widget.announce());
6395                    self.dispatch(message);
6396                    if let Some(announce) = announce {
6397                        self.set_announcement(announce);
6398                    }
6399                    self.request_redraw();
6400                }
6401                return;
6402            }
6403        }
6404
6405        // The clipboard: Ctrl+C/X/V, and a keyboard's own Copy, Cut and Paste.
6406        if let Some(command) = clipboard_command(&event.logical_key, event.physical_key, self.ctrl)
6407        {
6408            match command {
6409                ClipCommand::Copy => {
6410                    self.copy_selection(focused);
6411                }
6412                ClipCommand::Cut => self.cut_selection(focused),
6413                ClipCommand::Paste => {
6414                    // Answered now, or — on the Web — on a later frame.
6415                    if let Some(pasted) = self.clipboard.paste(focused, self.window.as_ref()) {
6416                        self.land_paste(pasted);
6417                    }
6418                }
6419            }
6420            return;
6421        }
6422
6423        // The context-menu key, or Shift+F10: the selection bar for someone with no
6424        // pointer to hold or right-click with (milestone 568). Only a text field has
6425        // one, and it is opened on the field that has the focus.
6426        if opens_selection_bar(&event.logical_key, self.shift)
6427            && self
6428                .tree
6429                .as_ref()
6430                .and_then(|tree| find_widget(tree.as_ref(), focused))
6431                .is_some_and(|widget| widget.text_value().is_some())
6432        {
6433            self.show_selection_toolbar(focused);
6434            return;
6435        }
6436
6437        // The other editing shortcuts, Ctrl+A/Z/Y.
6438        if self.ctrl {
6439            match &event.logical_key {
6440                WinitKey::Character(c) if c.eq_ignore_ascii_case("a") => {
6441                    self.select_all(focused);
6442                    return;
6443                }
6444                // Undo, and redo under both its spellings — Ctrl+Y on Windows,
6445                // Ctrl+Shift+Z everywhere else, and people bring the one their
6446                // hands already know.
6447                WinitKey::Character(c) if c.eq_ignore_ascii_case("z") && !self.shift => {
6448                    self.step_history(focused, false);
6449                    self.request_redraw();
6450                    return;
6451                }
6452                WinitKey::Character(c)
6453                    if c.eq_ignore_ascii_case("y")
6454                        || (c.eq_ignore_ascii_case("z") && self.shift) =>
6455                {
6456                    self.step_history(focused, true);
6457                    self.request_redraw();
6458                    return;
6459                }
6460                _ => {}
6461            }
6462        }
6463
6464        // With the input bridge active, editing goes EXCLUSIVELY through the
6465        // InputConnection, that is, the IME queue — the bridge view receives the
6466        // hardware keys too. Without this guard every keystroke would arrive
6467        // twice, once from winit's native queue and once from the bridge.
6468        #[cfg(android)]
6469        if crate::android_ime::installed() {
6470            return;
6471        }
6472
6473        let shift = self.shift;
6474        let key = match &event.logical_key {
6475            WinitKey::Named(NamedKey::Backspace) => Some(Key::Backspace),
6476            WinitKey::Named(NamedKey::Delete) => Some(Key::Delete),
6477            // Repeating Enter does not submit again; text and deletion do
6478            // repeat normally.
6479            WinitKey::Named(NamedKey::Enter) if !event.repeat => Some(Key::Enter),
6480            WinitKey::Named(NamedKey::Enter) => None,
6481            // With Ctrl, Left/Right jump a word and Home/End bound the field.
6482            WinitKey::Named(NamedKey::ArrowLeft) => Some(Key::Left {
6483                shift,
6484                word: self.ctrl,
6485            }),
6486            WinitKey::Named(NamedKey::ArrowRight) => Some(Key::Right {
6487                shift,
6488                word: self.ctrl,
6489            }),
6490            WinitKey::Named(NamedKey::Home) => Some(Key::Home {
6491                shift,
6492                doc: self.ctrl,
6493            }),
6494            WinitKey::Named(NamedKey::End) => Some(Key::End {
6495                shift,
6496                doc: self.ctrl,
6497            }),
6498            WinitKey::Named(NamedKey::Space) => Some(Key::Text(" ".to_string())),
6499            // Android delivers Enter as `Character("\n")`, through the
6500            // KeyCharacterMap, rather than `Named(Enter)`: the same submission,
6501            // without inserting a line break into the field.
6502            WinitKey::Character(c) if c == "\n" || c == "\r" => {
6503                if event.repeat {
6504                    None
6505                } else {
6506                    Some(Key::Enter)
6507                }
6508            }
6509            _ => event.text.as_ref().map(|text| Key::Text(text.to_string())),
6510        };
6511
6512        if let Some(key) = key {
6513            self.apply_key(focused, key);
6514            self.request_redraw();
6515        }
6516    }
6517
6518    /// The **system** back — Android's `KEYCODE_BACK`, the browser's back key — and
6519    /// its native chain: ① the topmost overlay closes, be it a sheet, a drawer, a modal
6520    /// or a menu; ② failing that a screen is popped, by replaying the **back gesture
6521    /// already committed**, through the same hooks as the swipe, so it settles with an
6522    /// animation; ③ failing that, at the root, back **quits the application**, as
6523    /// Android expects.
6524    fn system_back(&mut self, event_loop: &ActiveEventLoop) {
6525        if !self.system_back_step() {
6526            event_loop.exit();
6527        }
6528    }
6529
6530    /// What the system's Back does short of leaving the application, and whether it did
6531    /// anything: `false` means there was nothing left to go back from.
6532    fn system_back_step(&mut self) -> bool {
6533        // An open selection bar goes first, as a platform's own text selection does: Back
6534        // closes it before it closes anything behind it (milestone 568).
6535        if self.close_selection_toolbar() {
6536            return true;
6537        }
6538        // Then a selection an area holds with its bar, as for a field's.
6539        if self
6540            .runtime
6541            .region
6542            .as_ref()
6543            .is_some_and(|selection| selection.bar)
6544        {
6545            self.runtime.region = None;
6546            self.request_redraw();
6547            return true;
6548        }
6549        if let Some(message) = self.ui.as_ref().and_then(|ui| ui.top_dismiss()) {
6550            self.dispatch(message);
6551            self.request_redraw();
6552            return true;
6553        }
6554        if self.app.can_go_back() {
6555            self.build_dirty = true;
6556            // A pop, not a swipe: no finger is on the page (milestone 622).
6557            self.app.go_back();
6558            self.request_redraw();
6559            return true;
6560        }
6561        false
6562    }
6563
6564    /// Routes **Escape**: a leaf-to-root walk from the focused widget, with a
6565    /// three-state result — `Ignored` keeps walking up, `Handled` consumes, `Skip` stops
6566    /// with no fallback — then falls back to closing the topmost overlay when nobody
6567    /// answered, or when nothing is focused.
6568    fn escape(&mut self) {
6569        // The selection bar first, as for the system's Back (milestone 568).
6570        if self.close_selection_toolbar() {
6571            return;
6572        }
6573        // Then a selection an area holds.
6574        if self.runtime.region.take().is_some() {
6575            self.request_redraw();
6576            return;
6577        }
6578        // 1) Walk up the focus path. `Some(None)` means consumed with no message; an
6579        // outer `None` means the whole path ignored it, so we fall back.
6580        let outcome: Option<Option<A::Message>> = self.runtime.input.focused.and_then(|focused| {
6581            let tree = self.tree.as_ref()?;
6582            let path = find_path(tree.as_ref(), focused);
6583            for widget in path.iter().rev() {
6584                match widget.on_key(&Key::Escape) {
6585                    KeyResponse::Handled(message) => return Some(message),
6586                    KeyResponse::Skip => return Some(None),
6587                    KeyResponse::Ignored => {}
6588                }
6589            }
6590            None
6591        });
6592
6593        match outcome {
6594            Some(message) => {
6595                if let Some(message) = message {
6596                    self.dispatch(message);
6597                    self.request_redraw();
6598                }
6599            }
6600            // 2) Nobody on the path, or nothing focused: the topmost overlay.
6601            None => {
6602                if let Some(message) = self.ui.as_ref().and_then(|ui| ui.top_dismiss()) {
6603                    self.dispatch(message);
6604                    self.request_redraw();
6605                }
6606            }
6607        }
6608    }
6609
6610    /// Moves the caret vertically in the focused multi-line field — Up/Down when
6611    /// `page` is false, PgUp/PgDn otherwise — preserving the **remembered goal column**.
6612    /// Applies the selection under Shift and reveals the caret. Returns `true` when the
6613    /// caret moved within the field, `false` otherwise, in which case the shell
6614    /// navigates focus instead.
6615    fn move_caret_vertical(&mut self, id: WidgetId, down: bool, page: bool) -> bool {
6616        let width = self
6617            .ui
6618            .as_ref()
6619            .and_then(|ui| ui.widget_rect(id))
6620            .map(|r| r.width)
6621            .unwrap_or(0.0);
6622        let cursor = self.runtime.edits.get(&id).map(|e| e.cursor).unwrap_or(0);
6623        let moved = self
6624            .tree
6625            .as_ref()
6626            .and_then(|tree| find_widget(tree.as_ref(), id))
6627            .and_then(|widget| widget.caret_vertical(width, cursor, down, page, self.goal_x));
6628        let Some((new_cursor, goal)) = moved else {
6629            return false;
6630        };
6631        let shift = self.shift;
6632        let edit = self.runtime.edits.entry(id).or_default();
6633        if shift {
6634            if edit.anchor.is_none() {
6635                edit.anchor = Some(edit.cursor);
6636            }
6637        } else {
6638            edit.anchor = None;
6639        }
6640        edit.cursor = new_cursor;
6641        // Remember the goal column for the next vertical jump.
6642        self.goal_x = Some(goal);
6643        self.reveal_caret(id, new_cursor);
6644        self.request_redraw();
6645        true
6646    }
6647
6648    /// A hold in field `id` selects the word under the finger — the press has already
6649    /// put the caret there — and gives the selection its handles (milestone 511).
6650    fn select_held_word(&mut self, id: WidgetId) {
6651        let cursor = self.runtime.edits.get(&id).map(|e| e.cursor).unwrap_or(0);
6652        let word = self
6653            .tree
6654            .as_ref()
6655            .and_then(|tree| find_widget(tree.as_ref(), id))
6656            .and_then(|widget| widget.word_at(cursor))
6657            .filter(|(start, end)| start < end);
6658        let Some((start, end)) = word else {
6659            // Nothing to select — an empty field, a hold past the end of the text. The
6660            // bar still opens, at the caret, with Paste and whatever else applies.
6661            self.show_selection_toolbar(id);
6662            return;
6663        };
6664        self.runtime.edits.insert(
6665            id,
6666            Edit {
6667                cursor: end,
6668                anchor: Some(start),
6669                composing: None,
6670            },
6671        );
6672        self.runtime.selection_handles = Some(id);
6673        self.runtime.close_edit_run(id);
6674        self.show_selection_toolbar(id);
6675        #[cfg(android)]
6676        self.push_ime_context(id);
6677    }
6678
6679    /// The selection handle under the pointer, taken: a drag that moves its end of the
6680    /// focused field's selection. `None` when no handle is showing there.
6681    fn grab_selection_handle(&self) -> Option<Drag> {
6682        let id = self.runtime.selection_handles?;
6683        if self.runtime.input.focused != Some(id) {
6684            return None;
6685        }
6686        let rect = self.ui.as_ref()?.widget_rect(id)?;
6687        let edit = self.runtime.edits.get(&id).copied()?;
6688        let scroll_y = self.runtime.scroll.get(&id).map(|s| s.1).unwrap_or(0.0);
6689        let handles = find_widget(self.tree.as_ref()?.as_ref(), id)?
6690            .selection_handles(rect.width, &edit, scroll_y)?;
6691        let local = Point::new(self.cursor.x - rect.x, self.cursor.y - rect.y);
6692        let handle = crate::selection::grab(&handles, local)?;
6693        let at = handles[handle.index()].line_center;
6694        Some(Drag::SelectionHandle {
6695            id,
6696            rect,
6697            handle,
6698            grab: Point::new(at.x - local.x, at.y - local.y),
6699        })
6700    }
6701
6702    /// Routes a key to the focused field: updates the editing state and applies
6703    /// whatever message comes out, a value change or a submission.
6704    fn apply_key(&mut self, id: WidgetId, key: Key) {
6705        // Any horizontal move, or any keystroke, forgets the vertical goal column.
6706        self.goal_x = None;
6707        // Typing puts a touch selection's handles away, as a press does (milestone 511) —
6708        // and the bar over it (milestone 568).
6709        self.runtime.selection_handles = None;
6710        self.runtime.selection_toolbar = None;
6711        let was = self.runtime.edits.get(&id).copied().unwrap_or_default();
6712        let mut edit = was;
6713        let widget = self
6714            .tree
6715            .as_ref()
6716            .and_then(|tree| find_widget(tree.as_ref(), id));
6717        // What the field held before the key: half of an undo step, and the half the
6718        // application owns.
6719        let before = widget
6720            .and_then(|widget| widget.text_value())
6721            .map(str::to_owned);
6722        // A field's own announcement (#18) — read here for the same reason the click
6723        // and keyboard-activation paths read theirs off the widget **before**
6724        // `dispatch_edit` rebuilds the tree: a plain keystroke leaves it unused, since
6725        // `on_edit` answers `None` for one and the announcement below is never spoken.
6726        let announce = widget.and_then(|widget| widget.announce());
6727        let message = widget.and_then(|widget| widget.on_edit(&mut edit, &key));
6728        self.runtime.edits.insert(id, edit);
6729        // In a multi-line field, make the retained scroll follow the caret and reveal it.
6730        self.reveal_caret(id, edit.cursor);
6731        if let Some(message) = message {
6732            self.dispatch_edit(message);
6733            if let Some(announce) = announce {
6734                self.set_announcement(announce);
6735            }
6736        }
6737        // The history, recorded on the **evidence** of a changed value rather than on the
6738        // intent of a key that usually changes one: a filter may have refused the
6739        // character, a length limit may have swallowed it, a read-only field ignores it,
6740        // and an Enter submits instead of typing. The value is read again from the tree
6741        // the dispatch has just rebuilt, so what is recorded is what happened.
6742        let after = self
6743            .tree
6744            .as_ref()
6745            .and_then(|tree| find_widget(tree.as_ref(), id))
6746            .and_then(|widget| widget.text_value())
6747            .map(str::to_owned);
6748        match (before, after) {
6749            (Some(before), Some(after)) if before != after => {
6750                let kind = EditKind::of(&key, was.selection_range().is_some(), self.composing());
6751                let since = self.since_last_edit();
6752                self.runtime
6753                    .record_edit(id, EditSnapshot::new(before, was), kind, since);
6754            }
6755            // A key that moved only the caret ends the run: what is typed next is a step
6756            // of its own, or one undo would take back two visits to the field at once.
6757            (Some(_), _) => self.runtime.close_edit_run(id),
6758            _ => {}
6759        }
6760    }
6761
6762    /// Dispatches a message that changed a field's value, and refreshes the tree at once.
6763    ///
6764    /// Keys can arrive in **bursts**, faster than a frame — a software keyboard, `adb
6765    /// input text`, auto-repeat — and the next one must see the CURRENT value, not the
6766    /// retained tree's, or it would overwrite the previous keystroke. So the tree is
6767    /// refreshed right away; `build_dirty` stays raised and the next frame redoes the full
6768    /// pass: mounts, leaving fades and all.
6769    fn dispatch_edit(&mut self, message: A::Message) {
6770        self.dispatch(message);
6771        if let Some((width, height)) = self.last_size {
6772            let theme = self.themes.displayed(&self.app);
6773            // A surface of its own, because this build happens between frames rather than
6774            // inside one — and the same `build_view` as the frame path, because the next
6775            // key in the burst reads this tree straight away.
6776            let tree = self
6777                .media_query(width, height)
6778                .scope(|| build_view(&self.app, &theme, &self.runtime));
6779            self.tree = Some(tree);
6780        }
6781    }
6782
6783    /// Seconds since the last recorded edit, and stamps this one. The first edit is
6784    /// infinitely far from the one before it, which is to say it starts a run.
6785    fn since_last_edit(&mut self) -> f32 {
6786        let now = Instant::now();
6787        let since = self
6788            .last_edit_at
6789            .map(|then| now.duration_since(then).as_secs_f32())
6790            .unwrap_or(f32::INFINITY);
6791        self.last_edit_at = Some(now);
6792        since
6793    }
6794
6795    /// Whether an input method is in the middle of composing a word.
6796    fn composing(&self) -> bool {
6797        self.runtime
6798            .input
6799            .focused
6800            .and_then(|id| self.runtime.edits.get(&id))
6801            .is_some_and(|edit| edit.composing.is_some())
6802    }
6803
6804    /// Steps a text field's value back one change, or forward again. `true` when something
6805    /// moved — Ctrl+Z on anything that is not a field, or with nothing left to undo, moves
6806    /// nothing and says so.
6807    fn step_history(&mut self, id: WidgetId, forward: bool) -> bool {
6808        let value = self
6809            .tree
6810            .as_ref()
6811            .and_then(|tree| find_widget(tree.as_ref(), id))
6812            .and_then(|widget| widget.text_value())
6813            .map(str::to_owned);
6814        let Some(value) = value else {
6815            return false;
6816        };
6817        let edit = self.runtime.edits.get(&id).copied().unwrap_or_default();
6818        let current = EditSnapshot::new(value, edit);
6819        let target = if forward {
6820            self.runtime.redo_edit(id, current)
6821        } else {
6822            self.runtime.undo_edit(id, current)
6823        };
6824        let Some(target) = target else {
6825            return false;
6826        };
6827        let message = self
6828            .tree
6829            .as_ref()
6830            .and_then(|tree| find_widget(tree.as_ref(), id))
6831            .and_then(|widget| widget.replace_value(target.value.clone()));
6832        // The caret goes back with the text — an undo that restores the value and leaves
6833        // the caret at the end has done half the job.
6834        self.runtime.edits.insert(id, target.edit);
6835        self.reveal_caret(id, target.edit.cursor);
6836        if let Some(message) = message {
6837            self.dispatch_edit(message);
6838        }
6839        // An undo is not a keystroke: what is typed after it starts a run of its own,
6840        // however quickly it follows.
6841        self.last_edit_at = None;
6842        true
6843    }
6844
6845    /// Brings the focused widget **into view**, gliding every scroll region around it.
6846    ///
6847    /// Called wherever the keyboard moves the focus, and nowhere else. A click already
6848    /// landed on something the eye could see, and a focus restored because an overlay
6849    /// closed should put the page back where the reader left it, not chase whatever the
6850    /// restoration happened to pick.
6851    ///
6852    /// The glide is a **target**, not a jump: the same easing a scrollbar drag or a page
6853    /// request uses, so Tab through a form reads as one movement rather than a series of
6854    /// cuts. Anything already moving the offset is let go of — the keyboard has just
6855    /// overruled it.
6856    fn reveal_focus(&mut self) {
6857        let Some(id) = self.runtime.input.focused else {
6858            return;
6859        };
6860        let Some(moves) = self.ui.as_ref().map(|ui| ui.reveal(id, &self.runtime)) else {
6861            return;
6862        };
6863        for (area, offset) in moves {
6864            self.runtime.scroll_ballistic.remove(&area);
6865            self.runtime.scroll_velocity.remove(&area);
6866            self.runtime.scroll_target.insert(area, offset);
6867        }
6868    }
6869
6870    /// Makes a multi-line field's retained scroll **follow the caret**: adjusts
6871    /// `runtime.scroll[id]` just enough to keep the caret visible, the way an editor
6872    /// re-centres as you type. A no-op for a field that does not scroll.
6873    fn reveal_caret(&mut self, id: WidgetId, cursor: usize) {
6874        let Some(vp) = self.ui.as_ref().and_then(|ui| ui.scrollable_viewport(id)) else {
6875            return;
6876        };
6877        let metrics = self
6878            .tree
6879            .as_ref()
6880            .and_then(|tree| find_widget(tree.as_ref(), id))
6881            .and_then(|widget| widget.text_metrics(vp.width, cursor));
6882        let Some((content_h, visible_h, caret_top, caret_h)) = metrics else {
6883            return;
6884        };
6885        let max_y = (content_h - visible_h).max(0.0);
6886        let cur = self.runtime.scroll.get(&id).map(|s| s.1).unwrap_or(0.0);
6887        // The scroll window in which the caret stays visible, from "the caret's bottom
6888        // is visible" to "the caret's top is visible". We bring the current scroll into it.
6889        let lo = (caret_top + caret_h - visible_h).max(0.0);
6890        let hi = caret_top;
6891        let target = cur.clamp(lo.min(hi), lo.max(hi)).clamp(0.0, max_y);
6892        self.runtime.scroll.insert(id, (0.0, target));
6893        self.runtime.scroll_target.insert(id, (0.0, target));
6894        self.runtime.scroll_velocity.remove(&id);
6895    }
6896
6897    /// Copies field `id`'s selected text to the clipboard. `false` when there was nothing
6898    /// to copy — no selection, or a field that gives its text to nobody (a masked one).
6899    fn copy_selection(&mut self, id: WidgetId) -> bool {
6900        let edit = self.runtime.edits.get(&id).copied().unwrap_or_default();
6901        let text = self
6902            .tree
6903            .as_ref()
6904            .and_then(|tree| find_widget(tree.as_ref(), id))
6905            .and_then(|widget| widget.selected_text(&edit));
6906        match text {
6907            Some(text) => {
6908                self.clipboard.set_text(text);
6909                true
6910            }
6911            None => false,
6912        }
6913    }
6914
6915    /// The texts of `area` in the frame on screen, in reading order, each with its length in
6916    /// characters — what a selection over them is worked out from.
6917    fn region_stops(&self, area: WidgetId) -> Vec<(WidgetId, usize)> {
6918        let (Some(ui), Some(tree)) = (self.ui.as_ref(), self.tree.as_ref()) else {
6919            return Vec::new();
6920        };
6921        ui.text_stops_in(area)
6922            .filter_map(|stop| {
6923                let text = find_widget(tree.as_ref(), stop.id)?.selection_text()?;
6924                Some((stop.id, text.chars().count()))
6925            })
6926            .collect()
6927    }
6928
6929    /// The selection from `anchor` to `extent` in `area`, in the frame on screen.
6930    fn region_selection(
6931        &self,
6932        area: WidgetId,
6933        anchor: RegionPoint,
6934        extent: RegionPoint,
6935    ) -> RegionSelection {
6936        RegionSelection::new(area, anchor, extent, &self.region_stops(area))
6937    }
6938
6939    /// The character boundary of `stop`'s text nearest `at`, by the layout it was drawn with.
6940    fn region_index(&self, stop: &TextStop, at: Point) -> Option<usize> {
6941        find_widget(self.tree.as_ref()?.as_ref(), stop.id)?.selection_hit(
6942            at.x - stop.rect.x,
6943            at.y - stop.rect.y,
6944            stop.rect.width,
6945        )
6946    }
6947
6948    /// A press at the pointer over words in a selection area, if nothing else has a claim on
6949    /// it: the selection begins there, empty, and follows the pointer until it lifts. A double
6950    /// click selects the word.
6951    fn begin_region_selection(&mut self) {
6952        let Some(stop) = self.region_press_target() else {
6953            return;
6954        };
6955        let Some(index) = self.region_index(&stop, self.cursor) else {
6956            return;
6957        };
6958        let now = Instant::now();
6959        let here = self.cursor;
6960        let double = self.last_region_click.is_some_and(|(then, there)| {
6961            (now - then).as_secs_f32() < 0.4
6962                && (here.x - there.x).hypot(here.y - there.y) <= REGION_DOUBLE_CLICK_SLOP
6963        });
6964        self.last_region_click = Some((now, here));
6965        let word = double
6966            .then(|| {
6967                let tree = self.tree.as_ref()?;
6968                let text = find_widget(tree.as_ref(), stop.id)?.selection_text()?;
6969                Some(frus_widgets::word_bounds(text, index))
6970            })
6971            .flatten();
6972        let (anchor, extent) = match word {
6973            Some((start, end)) => (
6974                RegionPoint {
6975                    text: stop.id,
6976                    index: start,
6977                },
6978                RegionPoint {
6979                    text: stop.id,
6980                    index: end,
6981                },
6982            ),
6983            None => {
6984                let point = RegionPoint {
6985                    text: stop.id,
6986                    index,
6987                };
6988                (point, point)
6989            }
6990        };
6991        self.runtime.region = Some(self.region_selection(stop.area, anchor, extent));
6992        // A word is a whole selection: there is nothing to drag out of it.
6993        self.drag = word
6994            .is_none()
6995            .then_some(Drag::RegionSelect { area: stop.area });
6996        self.request_redraw();
6997    }
6998
6999    /// The text of a selection area under the pointer, if a press there is the area's to take:
7000    /// something that answers a press — a button, a tappable row, a link — keeps it.
7001    fn region_press_target(&self) -> Option<TextStop> {
7002        let ui = self.ui.as_ref()?;
7003        let claimed = self
7004            .runtime
7005            .input
7006            .pressed
7007            .is_some_and(|pressed| ui.msg_for(pressed).is_some());
7008        if claimed {
7009            return None;
7010        }
7011        ui.text_stop_at(self.cursor)
7012    }
7013
7014    /// A finger held on words in a selection area: the word under it is selected and the bar
7015    /// opens over it. Whether there was a word.
7016    fn select_region_word_held(&mut self) -> bool {
7017        let Some(stop) = self.ui.as_ref().and_then(|ui| ui.text_stop_at(self.cursor)) else {
7018            return false;
7019        };
7020        let Some(index) = self.region_index(&stop, self.cursor) else {
7021            return false;
7022        };
7023        let word = self
7024            .tree
7025            .as_ref()
7026            .and_then(|tree| find_widget(tree.as_ref(), stop.id))
7027            .and_then(|widget| widget.selection_text())
7028            .map(|text| frus_widgets::word_bounds(text, index))
7029            .filter(|(start, end)| start < end);
7030        let Some((start, end)) = word else {
7031            return false;
7032        };
7033        let anchor = RegionPoint {
7034            text: stop.id,
7035            index: start,
7036        };
7037        let extent = RegionPoint {
7038            text: stop.id,
7039            index: end,
7040        };
7041        self.runtime.region = Some(
7042            self.region_selection(stop.area, anchor, extent)
7043                .with_bar(true),
7044        );
7045        self.request_redraw();
7046        true
7047    }
7048
7049    /// The two handles of a selection area's touch selection, where they are on the surface.
7050    fn region_handles(&self) -> Option<(WidgetId, [frus_widgets::SelectionHandle; 2])> {
7051        let selection = self.runtime.region.as_ref().filter(|s| s.handles)?;
7052        let (ui, tree) = (self.ui.as_ref()?, self.tree.as_ref()?);
7053        let at = |point: RegionPoint, start: bool| {
7054            let stop = ui
7055                .text_stops_in(selection.area)
7056                .find(|s| s.id == point.text)?;
7057            let handle = find_widget(tree.as_ref(), point.text)?.selection_grip(
7058                stop.rect.width,
7059                point.index,
7060                start,
7061            )?;
7062            let (dx, dy) = (stop.rect.x, stop.rect.y);
7063            Some(frus_widgets::SelectionHandle {
7064                rect: handle.rect.translate(dx, dy),
7065                line_center: Point::new(handle.line_center.x + dx, handle.line_center.y + dy),
7066            })
7067        };
7068        Some((
7069            selection.area,
7070            [at(selection.start?, true)?, at(selection.end?, false)?],
7071        ))
7072    }
7073
7074    /// The handle of a selection area's touch selection under the pointer, taken.
7075    fn grab_region_handle(&self) -> Option<Drag> {
7076        let (area, handles) = self.region_handles()?;
7077        let handle = crate::selection::grab(&handles, self.cursor)?;
7078        let at = handles[handle.index()].line_center;
7079        Some(Drag::RegionHandle {
7080            area,
7081            handle,
7082            grab: Point::new(at.x - self.cursor.x, at.y - self.cursor.y),
7083        })
7084    }
7085
7086    /// A handle of a selection area moved with the finger: its end goes to the text position
7087    /// under it — carried by `grab`, so the handle's line and not the fingertip picks it — and
7088    /// the other end stays. The two never meet nor cross, as a field's do not.
7089    fn drag_region_handle(
7090        &mut self,
7091        area: WidgetId,
7092        handle: crate::selection::Handle,
7093        grab: Point,
7094    ) {
7095        let Some(selection) = self.runtime.region.as_ref() else {
7096            return;
7097        };
7098        let (Some(start), Some(end)) = (selection.start, selection.end) else {
7099            return;
7100        };
7101        let target = Point::new(self.cursor.x + grab.x, self.cursor.y + grab.y);
7102        let Some(stop) = self
7103            .ui
7104            .as_ref()
7105            .and_then(|ui| ui.nearest_text_stop(area, target))
7106        else {
7107            return;
7108        };
7109        let Some(index) = self.region_index(&stop, target) else {
7110            return;
7111        };
7112        let moved = RegionPoint {
7113            text: stop.id,
7114            index,
7115        };
7116        let stops = self.region_stops(area);
7117        let order = |point: RegionPoint| {
7118            stops
7119                .iter()
7120                .position(|(id, _)| *id == point.text)
7121                .map(|at| (at, point.index))
7122        };
7123        let (Some(to), Some(other)) = (
7124            order(moved),
7125            order(match handle {
7126                crate::selection::Handle::Start => end,
7127                crate::selection::Handle::End => start,
7128            }),
7129        ) else {
7130            return;
7131        };
7132        let (fixed, crosses) = match handle {
7133            crate::selection::Handle::Start => (end, to >= other),
7134            crate::selection::Handle::End => (start, to <= other),
7135        };
7136        if crosses {
7137            return;
7138        }
7139        let next = RegionSelection::new(area, fixed, moved, &stops).with_bar(true);
7140        // The bar stays put away until the handle is let go.
7141        let next = RegionSelection { bar: false, ..next };
7142        if self.runtime.region.as_ref() != Some(&next) {
7143            self.runtime.region = Some(next);
7144            self.request_redraw();
7145        }
7146    }
7147
7148    /// The pointer has moved during a drag in `area`: the far end of the selection goes to the
7149    /// text nearest it.
7150    fn extend_region_selection(&mut self, area: WidgetId) {
7151        let Some(anchor) = self.runtime.region.as_ref().map(|region| region.anchor) else {
7152            return;
7153        };
7154        let Some(stop) = self
7155            .ui
7156            .as_ref()
7157            .and_then(|ui| ui.nearest_text_stop(area, self.cursor))
7158        else {
7159            return;
7160        };
7161        let Some(index) = self.region_index(&stop, self.cursor) else {
7162            return;
7163        };
7164        let extent = RegionPoint {
7165            text: stop.id,
7166            index,
7167        };
7168        let selection = self.region_selection(area, anchor, extent);
7169        if self.runtime.region.as_ref() != Some(&selection) {
7170            self.runtime.region = Some(selection);
7171            self.request_redraw();
7172        }
7173    }
7174
7175    /// The words a selection area has selected, as they are copied: one line per text. `None`
7176    /// when nothing is selected.
7177    fn region_text(&self) -> Option<String> {
7178        let region = self.runtime.region.as_ref()?;
7179        let (ui, tree) = (self.ui.as_ref()?, self.tree.as_ref()?);
7180        let texts: Vec<(WidgetId, &str)> = ui
7181            .text_stops_in(region.area)
7182            .filter_map(|stop| {
7183                let text = find_widget(tree.as_ref(), stop.id)?.selection_text()?;
7184                Some((stop.id, text))
7185            })
7186            .collect();
7187        let copied = frus_widgets::region_copy(&texts, &region.ranges);
7188        (!copied.is_empty()).then_some(copied)
7189    }
7190
7191    /// Puts the words a selection area has selected on the clipboard. Whether there were any.
7192    fn copy_region(&mut self) -> bool {
7193        match self.region_text() {
7194            Some(copied) => {
7195                self.clipboard.set_text(copied);
7196                true
7197            }
7198            None => false,
7199        }
7200    }
7201
7202    /// Selects every text of the area that holds a selection.
7203    fn select_all_region(&mut self) {
7204        let Some(area) = self.runtime.region.as_ref().map(|region| region.area) else {
7205            return;
7206        };
7207        let stops = self.region_stops(area);
7208        let (Some(first), Some(last)) = (stops.first(), stops.last()) else {
7209            return;
7210        };
7211        let anchor = RegionPoint {
7212            text: first.0,
7213            index: 0,
7214        };
7215        let extent = RegionPoint {
7216            text: last.0,
7217            index: last.1,
7218        };
7219        let bar = self
7220            .runtime
7221            .region
7222            .as_ref()
7223            .is_some_and(|selection| selection.bar);
7224        self.runtime.region = Some(self.region_selection(area, anchor, extent).with_bar(bar));
7225        self.request_redraw();
7226    }
7227
7228    /// A key while nothing has the focus: Copy, and Select all, act on a selection an area
7229    /// holds. Whether the key was one of them.
7230    fn region_key(&mut self, event: &KeyDown) -> bool {
7231        if self.runtime.region.is_none() {
7232            return false;
7233        }
7234        if let Some(ClipCommand::Copy) =
7235            clipboard_command(&event.logical_key, event.physical_key, self.ctrl)
7236        {
7237            self.copy_region();
7238            return true;
7239        }
7240        let select_all = self.ctrl
7241            && matches!(&event.logical_key, WinitKey::Character(c) if c.eq_ignore_ascii_case("a"));
7242        if select_all {
7243            self.select_all_region();
7244            return true;
7245        }
7246        false
7247    }
7248
7249    /// Cuts field `id`'s selection: copies it, then deletes it — **only if it was copied**.
7250    /// A masked field refuses the copy, and a cut that deleted anyway would destroy the
7251    /// text while putting it nowhere.
7252    fn cut_selection(&mut self, id: WidgetId) {
7253        if self.copy_selection(id) {
7254            self.apply_key(id, Key::Backspace);
7255            self.request_redraw();
7256        }
7257    }
7258
7259    /// Selects all of field `id`'s text.
7260    fn select_all(&mut self, id: WidgetId) {
7261        self.runtime.edits.insert(
7262            id,
7263            Edit {
7264                cursor: usize::MAX,
7265                anchor: Some(0),
7266                composing: None,
7267            },
7268        );
7269        // A selection is a caret move: what is typed over it is a step of its own, not more
7270        // of whatever was being typed before.
7271        self.runtime.close_edit_run(id);
7272        self.request_redraw();
7273    }
7274
7275    /// Opens field `id`'s **selection bar** — Cut, Copy, Paste, Select all — over its
7276    /// selection (milestone 568). Whether the clipboard holds text is asked once, here, and
7277    /// kept for as long as the bar is open: it is what decides whether Paste is offered.
7278    fn show_selection_toolbar(&mut self, id: WidgetId) {
7279        self.show_selection_toolbar_at(id, None);
7280    }
7281
7282    /// The same, opened by a right-click at `at`: the bar goes there instead of to the
7283    /// selection (milestone 618).
7284    fn show_selection_toolbar_at(&mut self, id: WidgetId, at: Option<Point>) {
7285        let can_paste = self.clipboard.has_text();
7286        self.runtime.selection_toolbar = Some(ToolbarMark { id, can_paste, at });
7287        self.request_redraw();
7288    }
7289
7290    /// What Back and Escape do to an open selection bar: put it away, with the handles that
7291    /// went with it, and say so — `true` — so that the key goes no further.
7292    fn close_selection_toolbar(&mut self) -> bool {
7293        if self.runtime.selection_toolbar.is_none() {
7294            return false;
7295        }
7296        self.runtime.selection_handles = None;
7297        self.hide_selection_toolbar();
7298        true
7299    }
7300
7301    /// Puts the bar away, if one is open.
7302    fn hide_selection_toolbar(&mut self) {
7303        if self.runtime.selection_toolbar.take().is_some() {
7304            self.request_redraw();
7305        }
7306    }
7307
7308    /// What pressing a built-in button of the selection bar does, to the field the bar is
7309    /// open on. Copy, Cut and Paste close it and the handles with it; Select all leaves
7310    /// both open on a phone, on the selection it has just made larger, and closes both on a
7311    /// desktop (milestone 625).
7312    fn perform_edit_action(&mut self, action: frus_widgets::EditAction) {
7313        use frus_widgets::EditAction;
7314        // The bar of a selection area's selection, which acts on the area and not on a field.
7315        if self
7316            .runtime
7317            .region
7318            .as_ref()
7319            .is_some_and(|selection| selection.bar)
7320        {
7321            match action {
7322                EditAction::Copy => {
7323                    // As a platform's own selection does after a copy: it is put away.
7324                    self.copy_region();
7325                    self.runtime.region = None;
7326                }
7327                EditAction::SelectAll => self.select_all_region(),
7328                EditAction::Cut | EditAction::Paste => {}
7329            }
7330            self.request_redraw();
7331            return;
7332        }
7333        let Some(id) = self.runtime.selection_toolbar.map(|mark| mark.id) else {
7334            return;
7335        };
7336        match action {
7337            EditAction::Copy => {
7338                if self.copy_selection(id) {
7339                    // As the platforms' own text fields do after a copy: the selection
7340                    // collapses to its end, and what marked it is put away.
7341                    if let Some(edit) = self.runtime.edits.get_mut(&id) {
7342                        *edit = collapsed_to_end(*edit);
7343                    }
7344                    self.runtime.close_edit_run(id);
7345                }
7346                self.runtime.selection_handles = None;
7347                self.hide_selection_toolbar();
7348            }
7349            EditAction::Cut => {
7350                // `apply_key` puts the handles and the bar away, as any key does.
7351                self.cut_selection(id);
7352                self.hide_selection_toolbar();
7353            }
7354            EditAction::Paste => {
7355                self.runtime.selection_handles = None;
7356                self.hide_selection_toolbar();
7357                // Answered now, or — on the Web — on a later frame.
7358                if let Some(pasted) = self.clipboard.paste(id, self.window.as_ref()) {
7359                    self.land_paste(pasted);
7360                }
7361            }
7362            EditAction::SelectAll => {
7363                self.select_all(id);
7364                // Made with a finger or a right-click, the selection keeps whatever
7365                // marked it.
7366                if self.runtime.selection_handles.is_some() {
7367                    self.runtime.selection_handles = Some(id);
7368                }
7369                if select_all_hides_bar(frus_widgets::default_target_platform()) {
7370                    // On a desktop the menu has done its work, and goes with the handles,
7371                    // as the reference's `hideToolbar()` takes both (milestone 625).
7372                    self.runtime.selection_handles = None;
7373                    self.hide_selection_toolbar();
7374                } else {
7375                    // On a phone the bar stays, with the list its new state calls for,
7376                    // where it was: a menu a right-click opened stays at the pointer.
7377                    let at = self.runtime.selection_toolbar.and_then(|mark| mark.at);
7378                    self.show_selection_toolbar_at(id, at);
7379                }
7380            }
7381        }
7382        #[cfg(android)]
7383        self.push_ime_context(id);
7384    }
7385
7386    /// A right-click on a text field (a desktop's context menu, milestone 568): focuses it,
7387    /// puts the caret where the click landed unless it landed inside the selection there
7388    /// already is, and opens the bar. On anything else it puts an open bar away.
7389    fn context_click(&mut self) {
7390        // Something that answers a secondary tap takes it (milestone 581).
7391        let secondary = self
7392            .ui
7393            .as_ref()
7394            .and_then(|ui| ui.hit(self.cursor))
7395            .and_then(|id| {
7396                self.tree
7397                    .as_ref()
7398                    .and_then(|tree| find_widget(tree.as_ref(), id))
7399            })
7400            .and_then(|widget| widget.on_secondary_tap());
7401        if let Some(message) = secondary {
7402            self.hide_selection_toolbar();
7403            self.dispatch(message);
7404            self.request_redraw();
7405            return;
7406        }
7407        let hit = self.ui.as_ref().and_then(|ui| ui.focus_hit(self.cursor));
7408        let Some((id, rect)) = hit else {
7409            self.hide_selection_toolbar();
7410            return;
7411        };
7412        let was_focused = self.runtime.input.focused == Some(id);
7413        let scroll_y = self.runtime.scroll.get(&id).map(|s| s.1).unwrap_or(0.0);
7414        let scroll_cursor = if was_focused {
7415            self.runtime.edits.get(&id).map(|e| e.cursor).unwrap_or(0)
7416        } else {
7417            0
7418        };
7419        let cursor = self
7420            .tree
7421            .as_ref()
7422            .and_then(|tree| find_widget(tree.as_ref(), id))
7423            .and_then(|widget| {
7424                widget.cursor_at(
7425                    self.cursor.x - rect.x,
7426                    self.cursor.y - rect.y + scroll_y,
7427                    rect.width,
7428                    scroll_cursor,
7429                )
7430            });
7431        // Only a text field has a bar; a button under the pointer is not one.
7432        let Some(cursor) = cursor else {
7433            self.hide_selection_toolbar();
7434            return;
7435        };
7436        let inside = was_focused
7437            && self
7438                .runtime
7439                .edits
7440                .get(&id)
7441                .and_then(|edit| edit.selection_range())
7442                .is_some_and(|(start, end)| cursor >= start && cursor <= end);
7443        self.runtime.input.focused = Some(id);
7444        self.runtime.selection_handles = None;
7445        if !inside {
7446            self.goal_x = None;
7447            self.runtime.edits.insert(
7448                id,
7449                Edit {
7450                    cursor,
7451                    anchor: None,
7452                    composing: None,
7453                },
7454            );
7455            self.runtime.close_edit_run(id);
7456        }
7457        self.show_selection_toolbar_at(id, Some(self.cursor));
7458    }
7459
7460    /// Types what the clipboard answered into the field that asked for it, if the paste
7461    /// still lands there — see `clip::Pasted::lands`.
7462    fn land_paste(&mut self, pasted: clip::Pasted) {
7463        if pasted.lands(self.runtime.input.focused) {
7464            self.apply_key(pasted.into, Key::Text(pasted.text));
7465            self.request_redraw();
7466        }
7467    }
7468}
7469
7470/// The edit a **copy** leaves behind: the selection collapsed to its end, the caret where
7471/// the selection stopped. What the platforms' own text fields do after Copy from their bar,
7472/// and what tells the reader it happened — the highlight goes.
7473fn collapsed_to_end(edit: Edit) -> Edit {
7474    let cursor = edit.selection_range().map_or(edit.cursor, |(_, end)| end);
7475    Edit {
7476        cursor,
7477        anchor: None,
7478        composing: None,
7479    }
7480}
7481
7482/// Which end of a value drag is being announced.
7483#[derive(Clone, Copy, PartialEq, Eq)]
7484enum Edge {
7485    Start,
7486    End,
7487}
7488
7489/// Was the gesture that just ended **still a tap**?
7490///
7491/// Every drag here engages only past a threshold, so a press that never crossed it is
7492/// not a drag at all: it is a tap, and the release owes the widget underneath its
7493/// click. A drag that *did* move has already been answered — a fling, a drop, a
7494/// dismissal — and must not also click.
7495///
7496/// One list, because forgetting a variant is silent: the widget stays hittable, the
7497/// press still records it, and only the release quietly does nothing. That is how a
7498/// dismissible row came to swallow every tap on it (milestone 327).
7499/// The window's cursor for one the interface asks for. A hidden cursor is the default
7500/// shape, made invisible by the caller.
7501fn cursor_icon(cursor: UiCursor) -> CursorIcon {
7502    match cursor {
7503        UiCursor::Default | UiCursor::None => CursorIcon::Default,
7504        UiCursor::Pointer => CursorIcon::Pointer,
7505        UiCursor::Text => CursorIcon::Text,
7506        UiCursor::Forbidden => CursorIcon::NotAllowed,
7507        UiCursor::Wait => CursorIcon::Wait,
7508        UiCursor::Progress => CursorIcon::Progress,
7509        UiCursor::Help => CursorIcon::Help,
7510        UiCursor::Precise => CursorIcon::Crosshair,
7511        UiCursor::Move => CursorIcon::Move,
7512        UiCursor::Grab => CursorIcon::Grab,
7513        UiCursor::Grabbing => CursorIcon::Grabbing,
7514        UiCursor::ResizeColumn => CursorIcon::ColResize,
7515        UiCursor::ResizeRow => CursorIcon::RowResize,
7516        UiCursor::ZoomIn => CursorIcon::ZoomIn,
7517        UiCursor::ZoomOut => CursorIcon::ZoomOut,
7518        UiCursor::ContextMenu => CursorIcon::ContextMenu,
7519    }
7520}
7521
7522/// A movement, or a velocity, kept to the way a detector drags.
7523fn mask_pan(axis: frus_widgets::PanAxis, value: Point) -> Point {
7524    match axis {
7525        frus_widgets::PanAxis::Free => value,
7526        frus_widgets::PanAxis::Horizontal => Point::new(value.x, 0.0),
7527        frus_widgets::PanAxis::Vertical => Point::new(0.0, value.y),
7528    }
7529}
7530
7531fn gesture_was_a_tap(ended: Option<&Drag>) -> bool {
7532    matches!(
7533        ended,
7534        Some(
7535            Drag::Scroll { moved: false, .. }
7536                | Drag::Pan { moved: false, .. }
7537                | Drag::Gesture { moved: false, .. }
7538                | Drag::Reorder { moved: false, .. }
7539                | Drag::Item { moved: false, .. }
7540                | Drag::Dismiss { moved: false, .. }
7541                | Drag::Sheet { moved: false, .. }
7542        )
7543    )
7544}
7545
7546/// A press lands at `at`: what is under it is pressed, and a surface that takes ink
7547/// splashes from where it landed. The box comes from the frame that was on screen when the
7548/// pointer came down, which is the one the user aimed at. Returns whether a splash started.
7549fn press_at<Msg: Clone>(runtime: &mut Runtime, ui: Option<&Ui<Msg>>, at: Point) -> bool {
7550    runtime.input.pressed = ui.and_then(|ui| ui.hit(at));
7551    let Some(pressed) = runtime.input.pressed else {
7552        return false;
7553    };
7554    let Some(rect) = ui.and_then(|ui| ui.ink_box(pressed)) else {
7555        return false;
7556    };
7557    runtime.ink_press(
7558        pressed,
7559        Point::new(at.x - rect.x, at.y - rect.y),
7560        Size::new(rect.width, rect.height),
7561    );
7562    true
7563}
7564
7565/// What becomes of the press once a pointer event has been routed, `could_tap` being
7566/// whether the gesture under way could still have ended as a tap **before** the event, and
7567/// `drag` the gesture after it. Returns whether anything changed, which wants a frame.
7568///
7569/// A press on something inside a scrollable is shared with the scroll until the pointer
7570/// crosses the slop. From then on the gesture has it — a scroll, a swipe, a sheet, a
7571/// carried row — and, as in the reference, where the recogniser that loses the arena is
7572/// told so, the press is **let go of there and then**: its ink is swept, its press fades.
7573/// And a pointer that has lifted holds nothing, whatever its release did.
7574///
7575/// Before this, a release that ended a gesture which had moved returned before the press
7576/// was cleared, so the press outlived the finger; the ink, which waits for as long as its
7577/// widget is pressed, was held for good — the grey left on a segment a phone had swiped
7578/// a strip of filters by (milestone 534).
7579///
7580/// A finger hovers only so that its press can show (milestone 505), so a finger whose press
7581/// was taken stops hovering too. A mouse is still over whatever it is over.
7582fn settle_press(
7583    input: &mut frus_widgets::InputState,
7584    kind: PointerKind,
7585    could_tap: bool,
7586    drag: Option<&Drag>,
7587    touch: bool,
7588) -> bool {
7589    let taken = match kind {
7590        PointerKind::Down => false,
7591        PointerKind::Move => could_tap && !gesture_was_a_tap(drag),
7592        PointerKind::Up | PointerKind::Cancel => true,
7593    };
7594    if !taken {
7595        return false;
7596    }
7597    let mut changed = input.pressed.take().is_some();
7598    if touch {
7599        changed |= input.hovered.take().is_some();
7600    }
7601    changed
7602}
7603
7604/// How fast an area scrolls under a carried item, in pixels per second **per pixel** the
7605/// item hangs over the edge: the further out it is, the faster the list comes to meet it.
7606/// The reference's number, and the reference's law.
7607const AUTOSCROLL_VELOCITY: f32 = 50.0;
7608
7609/// The overhang that speed is worked out from is capped here, so that carrying a row far
7610/// past the edge — or off the window entirely — settles at a fast but usable speed instead
7611/// of one nobody can aim with.
7612const AUTOSCROLL_MAX_OVERHANG: f32 = 20.0;
7613
7614/// Places the scroll requests it can against `regions`, and hands back the ones whose
7615/// region that registry does not name, along with whether anything moved.
7616///
7617/// A request names its region by **key**, the way a focus request does: the application
7618/// wrapped it in `keyed(k, …)` and the tree is what turns that back into an identity.
7619/// The framework's own identities are hashes of a position in a tree — an application
7620/// cannot know one, and should not have to.
7621///
7622/// Nothing is *stored* here. The request is spent the moment its region is found, and
7623/// what it leaves behind is an offset in the runtime, indistinguishable from an offset a
7624/// finger left there. That is what makes it survive a rebuild: there is nothing to
7625/// survive.
7626fn apply_scroll_requests<Msg>(
7627    runtime: &mut Runtime,
7628    tree: &dyn Widget<Msg>,
7629    requests: Vec<(u64, ScrollTo)>,
7630    regions: &[Scrollable],
7631) -> (Vec<(u64, ScrollTo)>, bool) {
7632    let mut unplaced = Vec::new();
7633    let mut moved = false;
7634    for (key, to) in requests {
7635        let area = find_by_key(tree, key).and_then(|id| regions.iter().find(|a| a.id == id));
7636        match area {
7637            Some(area) => moved |= runtime.scroll_to(area, to),
7638            None => unplaced.push((key, to)),
7639        }
7640    }
7641    (unplaced, moved)
7642}
7643
7644/// Places the sheet requests it can against `sheets`, and hands back the ones whose sheet
7645/// that registry does not name, along with whether anything moved — the terms of
7646/// `apply_scroll_requests`, above.
7647///
7648/// A request names the key the application wrapped the sheet in; the state belongs to the
7649/// sheet's panel under that key, which is what the tree is asked for.
7650fn apply_sheet_requests<Msg>(
7651    runtime: &mut Runtime,
7652    tree: &dyn Widget<Msg>,
7653    requests: Vec<(u64, SheetTo)>,
7654    sheets: &[frus_widgets::SheetArea],
7655) -> (Vec<(u64, SheetTo)>, bool) {
7656    let mut unplaced = Vec::new();
7657    let mut moved = false;
7658    for (key, to) in requests {
7659        let sheet = frus_widgets::find_sheet_by_key(tree, key)
7660            .and_then(|id| sheets.iter().find(|sheet| sheet.id == id));
7661        match sheet {
7662            Some(sheet) => moved |= runtime.sheet_to(sheet.id, &sheet.spec, &to),
7663            None => unplaced.push((key, to)),
7664        }
7665    }
7666    (unplaced, moved)
7667}
7668
7669/// Moves a carried item's press by `shift` — how far its content moved on screen — so that
7670/// the ghost, drawn at the content's box plus the finger's travel since the press, stays
7671/// under the finger. A lifted item's box, recorded at the press, goes with it.
7672///
7673/// Anything else being dragged is not carried content, and is left alone.
7674fn follow_content(drag: &mut Drag, shift: (f32, f32)) {
7675    match drag {
7676        Drag::Reorder { start, carried, .. } => {
7677            start.x += shift.0;
7678            start.y += shift.1;
7679            if let Some((_, rect)) = carried {
7680                *rect = rect.translate(shift.0, shift.1);
7681            }
7682        }
7683        Drag::Item { source, start, .. } => {
7684            start.x += shift.0;
7685            start.y += shift.1;
7686            source.rect = source.rect.translate(shift.0, shift.1);
7687        }
7688        _ => {}
7689    }
7690}
7691
7692/// One axis of the auto-scroll: how far the **content** has to move this frame so that an
7693/// item held past a viewport's edge brings the rest of the list into view.
7694///
7695/// Everything is in screen coordinates and along one axis: `item` and `view` are
7696/// `(start, end)` pairs, `room` is what is left to scroll that way (nought at the end of
7697/// the content). The answer is a movement of the content, which the area then turns into
7698/// an offset of its own — signs and reversed axes belong to `Scrollable::offset_delta`,
7699/// not here.
7700///
7701/// `None` means *not now*: the item is inside, or the edge it hangs over is the end of the
7702/// content, where there is nothing left to reveal and the list should stay still rather
7703/// than fight.
7704fn edge_autoscroll(item: (f32, f32), view: (f32, f32), room: (f32, f32), dt: f32) -> Option<f32> {
7705    let before = view.0 - item.0; // above the top edge
7706    let after = item.1 - view.1; // below the bottom edge
7707                                 // The leading edge is asked first, and both are never obeyed at once: an item taller
7708                                 // than the viewport hangs over both, and an item pulled two ways scrolls nowhere.
7709    let overhang = if before > 0.0 && room.0 > 0.0 {
7710        before.min(AUTOSCROLL_MAX_OVERHANG)
7711    } else if after > 0.0 && room.1 > 0.0 {
7712        -after.min(AUTOSCROLL_MAX_OVERHANG)
7713    } else {
7714        return None;
7715    };
7716    let travel = overhang * AUTOSCROLL_VELOCITY * dt;
7717    (travel != 0.0).then_some(travel)
7718}
7719
7720/// Moves `current` toward `target` by one **exponential** spring step, with time
7721/// constant `tau` in seconds, over an interval `dt`: frame-rate independent and free
7722/// of overshoot. Used to smooth the abscissa during a reorder.
7723fn spring_toward(current: f32, target: f32, dt: f32, tau: f32) -> f32 {
7724    let k = 1.0 - (-dt / tau.max(1e-4)).exp();
7725    current + (target - current) * k
7726}
7727
7728/// The focus history's maximum depth, for nested overlays' triggers.
7729const FOCUS_HISTORY_MAX: usize = 8;
7730
7731/// Resolves focus after a (re)build, from the focusables **present** this frame. When
7732/// `current` has **vanished**, an overlay having closed, focus returns to the most
7733/// recent present **trigger** in `history`, which is popped until one is found. The
7734/// transition is recorded: the old focus (`prev`), if still present and different from
7735/// the new one, is pushed as a candidate trigger, bounded by [`FOCUS_HISTORY_MAX`].
7736/// Returns the resolved focus. A **pure** function apart from the mutated `history` and
7737/// `prev` — testable without a window.
7738fn resolve_focus(
7739    current: Option<WidgetId>,
7740    present: &std::collections::HashSet<WidgetId>,
7741    history: &mut Vec<WidgetId>,
7742    prev: &mut Option<WidgetId>,
7743) -> Option<WidgetId> {
7744    let mut cur = current;
7745    if let Some(c) = cur {
7746        if !present.contains(&c) {
7747            cur = None;
7748            while let Some(cand) = history.pop() {
7749                if present.contains(&cand) {
7750                    cur = Some(cand);
7751                    break;
7752                }
7753            }
7754        }
7755    }
7756    if *prev != cur {
7757        if let Some(old) = *prev {
7758            if present.contains(&old) && Some(old) != cur {
7759                history.push(old);
7760                if history.len() > FOCUS_HISTORY_MAX {
7761                    history.remove(0);
7762                }
7763            }
7764        }
7765        *prev = cur;
7766    }
7767    cur
7768}
7769
7770/// Paints the dragged header's **lifted card**, the ghost, into `scene` — unclipped —
7771/// at `card`: a drop shadow, a **faithful face** from the header's already-translated
7772/// primitives, or a solid one as a fallback when `ghost` is empty, then a `primary`
7773/// border. A pure function, testable without a GPU. The neighbours' sliding happens
7774/// upstream, in `reflow_reorder_columns`.
7775fn draw_ghost_card(scene: &mut Scene, theme: &Theme, card: Rect, ghost: &[Primitive]) {
7776    use drag_preview::{BORDER_ALPHA, BORDER_WIDTH, SHADOW_ALPHA, SHADOW_BLUR, SHADOW_OFFSET_Y};
7777    scene.set_clip(Rect::UNBOUNDED);
7778    // The shadow colour comes from **the theme** and can be overridden — the same role
7779    // as `Button`'s shadow; only the geometry, offset, blur and opacity, is a local
7780    // constant.
7781    let shadow = theme.scheme.shadow.with_alpha(SHADOW_ALPHA);
7782    scene.shadow(
7783        card.translate(0.0, SHADOW_OFFSET_Y),
7784        shadow,
7785        theme.radius,
7786        SHADOW_BLUR,
7787    );
7788    let border = theme.primary.fade(BORDER_ALPHA);
7789    if ghost.is_empty() {
7790        scene.draw_rect(card, theme.surface, theme.radius, BORDER_WIDTH, border);
7791    } else {
7792        scene.draw_rect(card, theme.surface, theme.radius, 0.0, Color::TRANSPARENT);
7793        for primitive in ghost {
7794            scene.push_primitive(primitive.clone());
7795        }
7796        scene.draw_rect(card, Color::TRANSPARENT, theme.radius, BORDER_WIDTH, border);
7797    }
7798}
7799
7800/// How a reorderable moves while it is carried: the two gestures the shell knows.
7801#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7802enum ReorderMotion {
7803    /// A table's column: a continuous slide along x following the pointer, dropped into
7804    /// the place of the column under it.
7805    Columns,
7806    /// A list's row or a board's card: inserted before or after a slot along the axis,
7807    /// with an insertion line, a gap opening there, and the nearest slot past either end.
7808    Slots(ReorderAxis),
7809}
7810
7811impl ReorderMotion {
7812    /// Which of the two a reorderable is, from its two answers: every vertical one inserts,
7813    /// and a horizontal one only when it says so — a list's row does, a table's column does
7814    /// not.
7815    fn of(axis: ReorderAxis, inserts: bool) -> Self {
7816        match axis {
7817            ReorderAxis::Vertical => Self::Slots(ReorderAxis::Vertical),
7818            ReorderAxis::Horizontal if inserts => Self::Slots(ReorderAxis::Horizontal),
7819            ReorderAxis::Horizontal => Self::Columns,
7820        }
7821    }
7822}
7823
7824/// How `widget` moves while it is carried — [`ReorderMotion::of`] its two answers.
7825fn reorder_motion<Msg>(widget: &dyn Widget<Msg>) -> ReorderMotion {
7826    ReorderMotion::of(widget.reorder_axis(), widget.reorder_inserts())
7827}
7828
7829/// Where the ghost is drawn relative to the box it was lifted from, given the finger's
7830/// `travel` since the press.
7831///
7832/// - A **column** follows the pointer along x, with a slight lift.
7833/// - A **card** follows it in both directions: a board carries it across columns.
7834/// - A **horizontal list's row** follows it along x only. The reference keeps a list's
7835///   proxy in its lane on either axis; a vertical list here shares the board's freedom
7836///   because the two are one path, and a horizontal list has no board to share it with.
7837fn ghost_offset(motion: ReorderMotion, travel: (f32, f32)) -> (f32, f32) {
7838    match motion {
7839        ReorderMotion::Columns => (travel.0, drag_preview::LIFT_Y),
7840        ReorderMotion::Slots(ReorderAxis::Vertical) => travel,
7841        ReorderMotion::Slots(ReorderAxis::Horizontal) => (travel.0, 0.0),
7842    }
7843}
7844
7845/// The geometry of a reorder preview's **insertion line**: a thin band of thickness
7846/// `thickness`, centred on the target edge **where the insertion will happen**.
7847///
7848/// Along a **vertical** list: the **top** edge (inserting before, the upper half hovered)
7849/// or the **bottom** one (inserting after, `after = true`, the lower half hovered),
7850/// spanning the full width. Along a **horizontal** list the band stands on its end,
7851/// spanning the full height at the left or right edge — and *after* is the right edge
7852/// only when the list reads left to right: under `rtl` what follows a row is on its left.
7853/// A pure function, testable without a GPU.
7854fn drop_insertion_line(
7855    target: Rect,
7856    thickness: f32,
7857    after: bool,
7858    axis: ReorderAxis,
7859    rtl: bool,
7860) -> Rect {
7861    match axis {
7862        ReorderAxis::Vertical => {
7863            let edge = if after {
7864                target.y + target.height
7865            } else {
7866                target.y
7867            };
7868            Rect::new(target.x, edge - thickness * 0.5, target.width, thickness)
7869        }
7870        ReorderAxis::Horizontal => {
7871            let edge = if after != rtl {
7872                target.x + target.width
7873            } else {
7874                target.x
7875            };
7876            Rect::new(edge - thickness * 0.5, target.y, thickness, target.height)
7877        }
7878    }
7879}
7880
7881/// Which axis a scroll gesture is **claimed by**, decided once when the finger passes the
7882/// threshold: `true` for vertical.
7883///
7884/// An area that can only go one way claims that one, whatever the finger did — a diagonal
7885/// flick down a list is a scroll down, not a refusal. An area that can go both ways takes
7886/// the direction the finger actually went in, ties going to vertical, which is the way a
7887/// page reads. The loser gets nothing for the rest of the gesture: this is the reference's
7888/// arena, where a vertical and a horizontal recogniser compete and only one wins.
7889fn claim_axis(dx: f32, dy: f32, can_x: bool, can_y: bool) -> bool {
7890    match (can_x, can_y) {
7891        (true, false) => false,
7892        (false, true) => true,
7893        _ => dy.abs() >= dx.abs(),
7894    }
7895}
7896
7897/// Which of the scrollables under the finger a gesture is **for**: the innermost one that
7898/// can go the way the finger went (`down` = along the vertical), given `chain` innermost
7899/// first and `under` the area the press landed on.
7900///
7901/// A strip that only slides across, sitting in a page that only scrolls down, is not what a
7902/// drag downwards is about — the page behind it is, and the reference's arena gives it to
7903/// the page for exactly that reason. Where nothing in the stack can go that way, the area
7904/// under the finger keeps the gesture and goes its own way: a lone recogniser in an arena
7905/// still wins.
7906fn claim_area(
7907    chain: &[frus_widgets::Scrollable],
7908    under: WidgetId,
7909    down: bool,
7910) -> Option<frus_widgets::Scrollable> {
7911    chain
7912        .iter()
7913        // A refresh area listening above takes a pull downwards even with nothing to
7914        // scroll, which is the reference's one exception to the same rule; the physics
7915        // may say never, or always (milestone 649).
7916        .find(|a| a.takes_user_scroll(down))
7917        .or_else(|| {
7918            chain
7919                .iter()
7920                .find(|a| a.id == under && a.physics.user() != frus_widgets::UserScroll::Never)
7921        })
7922        .copied()
7923}
7924
7925/// Gets the bytes at `url` and hands them to `deliver`, once.
7926///
7927/// Registered as the process's [`frus_widgets::ImageFetcher`] when the shell is built with
7928/// `net`. It spawns and forgets: nothing here waits for the answer, and the store the
7929/// callback writes into is what a later frame reads.
7930///
7931/// The two spawns are the two the rest of this file uses for every other effect — the
7932/// shared executor natively, the browser on the Web.
7933///
7934/// It carries a **deadline**, and that is not politeness. An image still in flight keeps
7935/// the interface redrawing — that is how the frame showing it ever happens — so a
7936/// request that never answers would leave the application repainting at full rate for
7937/// ever, on somebody's battery. A request without an answer has to become one with a
7938/// failure.
7939#[cfg(feature = "net")]
7940fn fetch_image_bytes(
7941    url: &str,
7942    deliver: Box<dyn FnOnce(Result<Vec<u8>, String>) + Send + 'static>,
7943) {
7944    /// Long enough for a photograph on a slow connection, short enough that a dead
7945    /// server does not pin a screen redrawing until the user closes the application.
7946    const DEADLINE: std::time::Duration = std::time::Duration::from_secs(30);
7947
7948    let url = url.to_string();
7949    let run = async move {
7950        let result = crate::net::Request::get(url)
7951            .timeout(DEADLINE)
7952            .send_bytes()
7953            .await;
7954        deliver(result.map_err(|e| e.to_string()));
7955    };
7956    #[cfg(not(web))]
7957    crate::runtime::spawn(run).detach();
7958    #[cfg(web)]
7959    wasm_bindgen_futures::spawn_local(run);
7960}
7961
7962#[cfg(test)]
7963mod tests {
7964    use super::{
7965        build_view, claim_area, claim_axis, draw_ghost_card, drop_insertion_line, fling_velocity,
7966        gesture_was_a_tap, ghost_offset, install_ambient, reorder_motion, resolve_focus,
7967        spring_toward, Drag, Point, Rect, ReorderAxis, ReorderMotion, Scene, Theme,
7968        VelocityEstimate, PRECISE_SLOP, TOUCH_SLOP,
7969    };
7970    use super::{clipboard_command, ClipCommand, KeyCode, PhysicalKey, WinitKey};
7971    use super::{collect_ids, find_widget, MediaQuery};
7972    use frus_widgets::Locale;
7973
7974    /// **Which systems put the bar away after its Select all** (milestone 625): the three
7975    /// desktops, as the reference's field does; on a phone the bar stays.
7976    #[test]
7977    fn select_all_hides_the_bar_on_the_desktops() {
7978        use frus_widgets::TargetPlatform as P;
7979        for p in [P::MacOs, P::Linux, P::Windows] {
7980            assert!(super::select_all_hides_bar(p), "{p:?}");
7981        }
7982        for p in [P::Android, P::Ios, P::Fuchsia] {
7983            assert!(!super::select_all_hides_bar(p), "{p:?}");
7984        }
7985    }
7986
7987    /// **The keys that open the selection bar** (milestone 568): the context-menu key, and
7988    /// F10 with Shift — F10 alone is the menu bar's key in other toolkits and asks for nothing
7989    /// here, and no ordinary key does.
7990    #[test]
7991    fn the_context_menu_key_and_shift_f10_open_the_bar() {
7992        use super::opens_selection_bar;
7993        assert!(opens_selection_bar(
7994            &WinitKey::Named(winit::keyboard::NamedKey::ContextMenu),
7995            false
7996        ));
7997        assert!(opens_selection_bar(
7998            &WinitKey::Named(winit::keyboard::NamedKey::F10),
7999            true
8000        ));
8001        assert!(!opens_selection_bar(
8002            &WinitKey::Named(winit::keyboard::NamedKey::F10),
8003            false
8004        ));
8005        assert!(!opens_selection_bar(
8006            &WinitKey::Named(winit::keyboard::NamedKey::Enter),
8007            true
8008        ));
8009        assert!(!opens_selection_bar(&character("m"), true));
8010    }
8011
8012    /// **What a Copy from the selection bar leaves behind** (milestone 568): the selection
8013    /// collapsed to its end, whichever way it was made — dragged backwards puts the caret at
8014    /// the far end, and Copy puts it at the *end* of the text, not where the caret was.
8015    #[test]
8016    fn a_copy_collapses_the_selection_to_its_end() {
8017        use frus_widgets::Edit;
8018        let forwards = Edit {
8019            cursor: 11,
8020            anchor: Some(6),
8021            composing: None,
8022        };
8023        let backwards = Edit {
8024            cursor: 6,
8025            anchor: Some(11),
8026            composing: None,
8027        };
8028        let expect = Edit {
8029            cursor: 11,
8030            anchor: None,
8031            composing: None,
8032        };
8033        assert_eq!(super::collapsed_to_end(forwards), expect);
8034        assert_eq!(super::collapsed_to_end(backwards), expect);
8035        // Nothing selected: the caret stays where it is.
8036        let caret = Edit {
8037            cursor: 3,
8038            anchor: None,
8039            composing: None,
8040        };
8041        assert_eq!(super::collapsed_to_end(caret), caret);
8042        // An anchor on the caret is no selection either.
8043        let empty = Edit {
8044            cursor: 3,
8045            anchor: Some(3),
8046            composing: Some((1, 2)),
8047        };
8048        assert_eq!(super::collapsed_to_end(empty).anchor, None);
8049        assert_eq!(super::collapsed_to_end(empty).cursor, 3);
8050    }
8051
8052    /// **A field with a clear button kept the keyboard only until the first letter**
8053    /// (milestone 510). Whether the focused widget takes typing was asked of a caret
8054    /// hit test at the corner of a field one pixel wide; a field whose clickable suffix
8055    /// covers that pixel — the × it shows once it holds text — answered no, and the
8056    /// keyboard was closed mid-word. Asked of the widget, the answer is the field's.
8057    #[test]
8058    fn a_field_with_a_clear_button_still_wants_the_keyboard() {
8059        use frus_widgets::{Container, Icons, TextField};
8060        let clearable = TextField::<()>::new("F")
8061            .suffix_icon(Icons::CLOSE)
8062            .on_suffix(());
8063        assert!(
8064            super::wants_keyboard(&clearable),
8065            "the field with its clear button"
8066        );
8067        assert!(
8068            !super::wants_keyboard(&TextField::<()>::new("F").enabled(false)),
8069            "a disabled field takes no typing"
8070        );
8071        assert!(
8072            !super::wants_keyboard(&Container::<()>::new()),
8073            "and what is not a field wants no keyboard"
8074        );
8075    }
8076
8077    /// **Left and right moved the focus, not the caret, in a field with text in it**
8078    /// (milestone 570). Whether the focused widget is a text field was asked of a caret probe
8079    /// at a corner of the field one pixel wide, and a field that holds text shows a clear
8080    /// button over that corner: the probe said no caret goes there, so the arrows navigated to
8081    /// the next control. In an empty field, which has no button, they worked — which is how it
8082    /// went unseen. The probe still answers no for such a field; the question is the widget's.
8083    #[test]
8084    fn a_field_with_a_clear_button_is_a_text_field_and_keeps_its_arrows() {
8085        use frus_widgets::{Container, Icons, TextField};
8086        let clearable = TextField::<()>::new("hello")
8087            .suffix_icon(Icons::CLOSE)
8088            .on_suffix(());
8089        assert!(
8090            frus_widgets::Widget::<()>::cursor_at(&clearable, 0.0, 0.0, 1.0, 0).is_none(),
8091            "the trap: the probe finds no caret at the corner a clear button covers"
8092        );
8093        assert!(
8094            super::is_text_field(&clearable),
8095            "and it is a text field all the same"
8096        );
8097        assert!(
8098            super::is_text_field(&TextField::<()>::new("")),
8099            "empty, too"
8100        );
8101        assert!(
8102            !super::is_text_field(&Container::<()>::new()),
8103            "what is not a field leaves its arrows to focus navigation"
8104        );
8105    }
8106
8107    /// **A text that can only be read does not raise the keyboard.** A read-only field and a
8108    /// selectable paragraph are both text fields — they have a caret, and the arrows are
8109    /// theirs — but the software keyboard is for typing, and sliding it up over the words a
8110    /// reader has come to select hides the words. Before milestone 573 a read-only field
8111    /// raised it all the same.
8112    #[test]
8113    fn a_text_that_can_only_be_read_wants_no_keyboard() {
8114        use frus_widgets::{Text, TextField};
8115        let read_only = TextField::<()>::new("REF-4417").read_only();
8116        assert!(super::is_text_field(&read_only), "it has a caret");
8117        assert!(
8118            !super::wants_keyboard(&read_only),
8119            "and no use for a keyboard"
8120        );
8121
8122        let selectable = Text::new("ORDER-4417").selectable();
8123        assert!(
8124            super::is_text_field::<()>(&selectable),
8125            "the arrows move its caret"
8126        );
8127        assert!(!super::wants_keyboard::<()>(&selectable));
8128
8129        assert!(
8130            super::wants_keyboard(&TextField::<()>::new("typed")),
8131            "and a field that can be typed into still does"
8132        );
8133        assert!(
8134            !super::is_text_field::<()>(&Text::new("ORDER-4417")),
8135            "an ordinary text leaves its arrows to focus navigation"
8136        );
8137    }
8138
8139    fn character(c: &str) -> WinitKey {
8140        WinitKey::Character(c.into())
8141    }
8142
8143    /// Ctrl+C/X/V are the clipboard, in either case — Caps Lock or Shift is still a
8144    /// copy — and the same letters without Ctrl are only letters.
8145    #[test]
8146    fn ctrl_c_x_v_are_the_clipboard_and_the_letters_alone_are_not() {
8147        let any = PhysicalKey::Code(KeyCode::KeyQ);
8148        for (letter, command) in [
8149            ("c", ClipCommand::Copy),
8150            ("X", ClipCommand::Cut),
8151            ("v", ClipCommand::Paste),
8152        ] {
8153            assert_eq!(
8154                clipboard_command(&character(letter), any, true),
8155                Some(command)
8156            );
8157            assert_eq!(
8158                clipboard_command(&character(letter), any, false),
8159                None,
8160                "{letter}"
8161            );
8162        }
8163        assert_eq!(clipboard_command(&character("a"), any, true), None);
8164    }
8165
8166    /// **Android's Copy, Cut and Paste keys arrive by physical code alone** — winit gives
8167    /// them no logical key — and they are the clipboard with no modifier held. A check
8168    /// of the logical key alone ignored them (milestone 509).
8169    #[test]
8170    fn a_keyboards_own_clipboard_keys_work_by_their_physical_code() {
8171        let unidentified = WinitKey::Unidentified(winit::keyboard::NativeKey::Unidentified);
8172        for (code, command) in [
8173            (KeyCode::Copy, ClipCommand::Copy),
8174            (KeyCode::Cut, ClipCommand::Cut),
8175            (KeyCode::Paste, ClipCommand::Paste),
8176        ] {
8177            assert_eq!(
8178                clipboard_command(&unidentified, PhysicalKey::Code(code), false),
8179                Some(command),
8180                "{code:?}"
8181            );
8182        }
8183        // And the named keys a desktop keyboard reports for the same thing.
8184        let named = WinitKey::Named(super::NamedKey::Paste);
8185        let any = PhysicalKey::Code(KeyCode::KeyQ);
8186        assert_eq!(
8187            clipboard_command(&named, any, false),
8188            Some(ClipCommand::Paste)
8189        );
8190    }
8191
8192    /// **A paste lands only in the field that asked, while that field still has the
8193    /// focus** — on the Web the clipboard answers after the key press, and by then the
8194    /// reader may have moved to another field or to none. And a clipboard holding no
8195    /// text pastes nothing, rather than typing an empty string over the selection
8196    /// (milestone 526).
8197    #[test]
8198    fn a_paste_lands_in_the_field_that_asked_only_while_it_has_the_focus() {
8199        use super::clip::Pasted;
8200        let field = WidgetId::from_u64(1);
8201        let other = WidgetId::from_u64(2);
8202        let pasted = Pasted {
8203            into: field,
8204            text: "copied".into(),
8205        };
8206        assert!(pasted.lands(Some(field)));
8207        assert!(
8208            !pasted.lands(Some(other)),
8209            "the focus moved to another field"
8210        );
8211        assert!(!pasted.lands(None), "the focus left every field");
8212        let nothing = Pasted {
8213            into: field,
8214            text: String::new(),
8215        };
8216        assert!(!nothing.lands(Some(field)), "an empty clipboard");
8217    }
8218
8219    /// **A paste answered later is queued for its field, and a frame is asked for.** Two
8220    /// pastes asked in two fields and answered in the other order come out in the order
8221    /// the answers came, each still naming the field that asked; each answer wakes the
8222    /// window once; and nothing is taken twice (milestone 526).
8223    #[test]
8224    fn a_paste_answered_later_waits_for_the_frame_that_takes_it() {
8225        use super::clip::{Answers, Pasted};
8226        use std::cell::Cell;
8227        use std::rc::Rc;
8228        let first = WidgetId::from_u64(1);
8229        let second = WidgetId::from_u64(2);
8230        let answers = Answers::default();
8231        let wakes = Rc::new(Cell::new(0));
8232        let wake = || {
8233            let wakes = Rc::clone(&wakes);
8234            move || wakes.set(wakes.get() + 1)
8235        };
8236        let to_first = answers.answer_for(first, wake());
8237        let to_second = answers.answer_for(second, wake());
8238        assert_eq!(answers.take(), None, "nothing is answered yet");
8239        assert_eq!(wakes.get(), 0, "asking is not answering");
8240
8241        to_second("later".into());
8242        assert_eq!(wakes.get(), 1);
8243        to_first("sooner".into());
8244        assert_eq!(wakes.get(), 2);
8245
8246        assert_eq!(
8247            answers.take(),
8248            Some(Pasted {
8249                into: second,
8250                text: "later".into()
8251            })
8252        );
8253        assert_eq!(
8254            answers.take(),
8255            Some(Pasted {
8256                into: first,
8257                text: "sooner".into()
8258            })
8259        );
8260        assert_eq!(answers.take(), None, "each answer is taken once");
8261    }
8262
8263    /// An application whose whole interface lives **inside a deferred subtree** — which is
8264    /// what any application with an `AppBar` is, since a bar defers its own composition
8265    /// until the theme is known.
8266    ///
8267    /// It exists because nothing else in this repo can stand in for one: the shell's own
8268    /// types need a window and an event-loop proxy, so the only piece of the frame a test
8269    /// can hold is the part that turns a `view` into a tree. That part is where two
8270    /// milestones' worth of bugs were.
8271    struct Deferred;
8272
8273    impl crate::Application for Deferred {
8274        type Message = ();
8275
8276        fn update(&mut self, _message: ()) -> crate::Command<()> {
8277            crate::Command::none()
8278        }
8279
8280        fn view(&self, _theme: &Theme) -> Box<dyn frus_widgets::Widget<()>> {
8281            Box::new(frus_widgets::ThemeBuilder::new(|_: &Theme| {
8282                frus_widgets::Flex::column()
8283                    .width(300.0)
8284                    .height(80.0)
8285                    .child(
8286                        frus_widgets::TextField::new("hi")
8287                            .width(200.0)
8288                            .on_input(|_| ()),
8289                    )
8290            }))
8291        }
8292    }
8293
8294    /// The surface every frame installs, so a build outside one measures the way a build
8295    /// inside one does.
8296    fn surfaced<R>(f: impl FnOnce() -> R) -> R {
8297        MediaQuery::new(frus_widgets::Size::new(300.0, 80.0)).scope(f)
8298    }
8299
8300    /// **A shell installs the reader's language** — the assertion that keeps milestone 454
8301    /// from being a resolution nobody ever runs.
8302    ///
8303    /// `locale::of()` answers `en` with nothing installed, exactly as
8304    /// `localizations::of()` answers English, and for the same reason: an application that
8305    /// says nothing must keep working. That default is also what would hide the wiring
8306    /// being missing, so this drives the shell's own reading of the trait rather than
8307    /// calling `resolve` directly — milestone 408's bug, and milestone 449's guard against
8308    /// it.
8309    #[test]
8310    fn a_shell_installs_the_reader_s_language() {
8311        /// An application with three languages, and a switch of its own it may or may not
8312        /// have been told to use.
8313        struct Speaks(Option<Locale>);
8314
8315        impl crate::Application for Speaks {
8316            type Message = ();
8317
8318            fn update(&mut self, _message: ()) -> crate::Command<()> {
8319                crate::Command::none()
8320            }
8321
8322            fn view(&self, _theme: &Theme) -> Box<dyn frus_widgets::Widget<()>> {
8323                Box::new(frus_widgets::Flex::<()>::column())
8324            }
8325
8326            fn supported_locales(&self) -> Vec<Locale> {
8327                vec![
8328                    Locale::new("en"),
8329                    Locale::new("fr"),
8330                    Locale::with_country("fr", "CA"),
8331                ]
8332            }
8333
8334            fn locale(&self) -> Option<Locale> {
8335                self.0.clone()
8336            }
8337        }
8338
8339        // The device speaks Belgian French; the application has French, so it wins.
8340        install_ambient(&Speaks(None), &[Locale::with_country("fr", "BE")]);
8341        assert_eq!(frus_widgets::locale::of(), Locale::new("fr"));
8342
8343        // A platform that reported nothing is not a failure: the application's own first
8344        // choice stands.
8345        install_ambient(&Speaks(None), &[]);
8346        assert_eq!(frus_widgets::locale::of(), Locale::new("en"));
8347
8348        // And an application with a language menu of its own outranks the device — still
8349        // resolved, so asking for one it does not have gives the nearest thing it does.
8350        install_ambient(
8351            &Speaks(Some(Locale::with_country("fr", "FR"))),
8352            &[Locale::new("en")],
8353        );
8354        assert_eq!(frus_widgets::locale::of(), Locale::new("fr"));
8355
8356        // An application that says nothing at all is where it was: English.
8357        install_ambient(&Deferred, &[Locale::new("fr")]);
8358        assert_eq!(
8359            frus_widgets::locale::of(),
8360            Locale::new("en"),
8361            "it supports only English, so French resolves to it"
8362        );
8363    }
8364
8365    /// **A shell installs the application's words** — which is the assertion that keeps
8366    /// milestone 449 from being a table nobody ever reads.
8367    ///
8368    /// `localizations::of` answers English with nothing installed, so every widget test
8369    /// and every golden would pass whether or not the shell ever called `install`. The
8370    /// default is what makes the feature safe to add; it is also what would hide the
8371    /// wiring being missing. So this drives the shell's own reading of the trait.
8372    #[test]
8373    fn a_shell_installs_the_application_s_words() {
8374        struct Fr;
8375
8376        impl frus_widgets::localizations::Localizations for Fr {
8377            fn back_button_label(&self) -> &str {
8378                "Retour"
8379            }
8380
8381            fn first_day_of_week_index(&self) -> usize {
8382                1
8383            }
8384        }
8385
8386        struct Speaks;
8387
8388        impl crate::Application for Speaks {
8389            type Message = ();
8390
8391            fn update(&mut self, _message: ()) -> crate::Command<()> {
8392                crate::Command::none()
8393            }
8394
8395            fn view(&self, _theme: &Theme) -> Box<dyn frus_widgets::Widget<()>> {
8396                Box::new(frus_widgets::Flex::<()>::column())
8397            }
8398
8399            fn localizations(&self) -> Option<std::rc::Rc<dyn frus_widgets::Localizations>> {
8400                Some(std::rc::Rc::new(Fr))
8401            }
8402        }
8403
8404        install_ambient(&Speaks, &[]);
8405        assert_eq!(
8406            frus_widgets::localizations::of().back_button_label(),
8407            "Retour",
8408            "the shell read the application's table and installed it"
8409        );
8410        assert_eq!(
8411            frus_widgets::localizations::of().first_day_of_week_index(),
8412            1
8413        );
8414
8415        // And an application that says nothing leaves whatever is in force alone rather
8416        // than forcing English back on top of a table installed elsewhere.
8417        install_ambient(&Deferred, &[]);
8418        assert_eq!(
8419            frus_widgets::localizations::of().back_button_label(),
8420            "Retour",
8421            "saying nothing is not the same as saying English"
8422        );
8423    }
8424
8425    /// **The application's scroll behaviour is the one at the root** (milestone 616), and
8426    /// one that says nothing gets the behaviour that follows the theme.
8427    #[test]
8428    fn the_scroll_behaviour_at_the_root_is_the_applications() {
8429        struct Quiet;
8430
8431        impl crate::Application for Quiet {
8432            type Message = ();
8433
8434            fn update(&mut self, _message: ()) -> crate::Command<()> {
8435                crate::Command::none()
8436            }
8437
8438            fn view(&self, _theme: &Theme) -> Box<dyn frus_widgets::Widget<()>> {
8439                Box::new(frus_widgets::Flex::<()>::column())
8440            }
8441
8442            fn scroll_behavior(&self) -> frus_widgets::ScrollBehavior {
8443                frus_widgets::ScrollBehavior::material().with_overscroll(false)
8444            }
8445        }
8446
8447        struct Silent;
8448
8449        impl crate::Application for Silent {
8450            type Message = ();
8451
8452            fn update(&mut self, _message: ()) -> crate::Command<()> {
8453                crate::Command::none()
8454            }
8455
8456            fn view(&self, _theme: &Theme) -> Box<dyn frus_widgets::Widget<()>> {
8457                Box::new(frus_widgets::Flex::<()>::column())
8458            }
8459        }
8460
8461        install_ambient(&Quiet, &[]);
8462        assert_eq!(
8463            frus_widgets::ScrollConfiguration::of(),
8464            frus_widgets::ScrollBehavior::material().with_overscroll(false)
8465        );
8466        install_ambient(&Silent, &[]);
8467        assert_eq!(
8468            frus_widgets::ScrollConfiguration::of(),
8469            frus_widgets::ScrollBehavior::material()
8470        );
8471    }
8472
8473    /// A tree the shell hands to a traversal is **ready to be traversed**.
8474    ///
8475    /// Milestones 415 and 416 were the same bug found twice, in the two places this shell
8476    /// turns a `view` into a tree. A deferred subtree has no children until something has
8477    /// built it, so an unprepared tree reports **one** identity — its root — and every
8478    /// widget in the application is invisible to `collect_ids`, to `find_widget`, and to
8479    /// everything downstream of them: the mount and leave fades, the focus, the caret.
8480    ///
8481    /// The failure was silence in both places, which is why it went unnoticed twice. This
8482    /// is the assertion that would have caught it, and it needs no window.
8483    #[test]
8484    fn a_view_is_built_ready_to_be_read() {
8485        let theme = Theme::default();
8486        let prepared =
8487            surfaced(|| build_view(&Deferred, &theme, &frus_widgets::Runtime::default()));
8488        let ids = collect_ids(prepared.as_ref());
8489        assert!(
8490            ids.len() > 1,
8491            "an application inside a deferred subtree has identities to mount: {ids:?}"
8492        );
8493        // And every one of them is reachable, which is what the burst path needs.
8494        for id in &ids {
8495            assert!(
8496                find_widget(prepared.as_ref(), *id).is_some(),
8497                "{id:?} was counted but cannot be found"
8498            );
8499        }
8500    }
8501
8502    /// The same view, unprepared, is the state both call sites used to read.
8503    #[test]
8504    #[should_panic(expected = "before it was built")]
8505    fn an_unprepared_view_is_the_state_that_broke() {
8506        let theme = Theme::default();
8507        let raw = surfaced(|| crate::Application::view(&Deferred, &theme));
8508        let _ = collect_ids(raw.as_ref());
8509    }
8510
8511    /// A press on a **dismissible** row that never moved is a tap, and the widget under
8512    /// the finger owes it a click (reported on a device: a task row's avatar opened
8513    /// nothing, while a control a few hundred pixels away in the same card worked).
8514    ///
8515    /// The row registers a swipe on the press, in case the finger is about to slide
8516    /// sideways. When it does not, the swipe has to stand down. Every other gesture
8517    /// here already did; this one was left out of the list.
8518    #[test]
8519    fn a_swipe_that_never_started_is_still_a_tap() {
8520        let item = frus_widgets::Dismissable {
8521            id: WidgetId::from_u64(1),
8522            rect: Rect::new(0.0, 0.0, 300.0, 62.0),
8523            spec: frus_widgets::DismissSpec::default(),
8524        };
8525        let dismiss = |moved| Drag::Dismiss {
8526            item,
8527            last: Point::new(0.0, 0.0),
8528            moved,
8529        };
8530        assert!(
8531            gesture_was_a_tap(Some(&dismiss(false))),
8532            "a swipe under the threshold leaves the click alone"
8533        );
8534        assert!(
8535            !gesture_was_a_tap(Some(&dismiss(true))),
8536            "a swipe that ran has already been answered and must not also click"
8537        );
8538        // A scroll, the variant that was already right — and the one that masked this
8539        // bug, since a list long enough to scroll claims the press before the swipe can.
8540        let scroll = Drag::Scroll {
8541            id: WidgetId::from_u64(2),
8542            last: Point::new(0.0, 0.0),
8543            moved: false,
8544            carried: (0.0, 0.0),
8545            dismiss: None,
8546            axis: None,
8547            pan: None,
8548        };
8549        assert!(gesture_was_a_tap(Some(&scroll)));
8550        // A press on a sheet's panel likewise: a button in the sheet still clicks, and a
8551        // sheet that was dragged does not also click what it was dragged by (515).
8552        let sheet = |moved| Drag::Sheet {
8553            id: WidgetId::from_u64(4),
8554            last: Point::new(0.0, 0.0),
8555            moved,
8556            available: 800.0,
8557        };
8558        assert!(gesture_was_a_tap(Some(&sheet(false))));
8559        assert!(!gesture_was_a_tap(Some(&sheet(true))));
8560        // And a gesture that captures on the press, rather than at a threshold, never
8561        // becomes a tap however still the finger was.
8562        let select = Drag::TextSelect {
8563            id: WidgetId::from_u64(3),
8564            rect: Rect::new(0.0, 0.0, 10.0, 10.0),
8565        };
8566        assert!(!gesture_was_a_tap(Some(&select)));
8567        assert!(!gesture_was_a_tap(None), "no gesture, no verdict to give");
8568    }
8569
8570    /// A page that scrolls down must not drift sideways while it does it (reported on a
8571    /// device, 2026-08-16). No finger travels in a straight line, so an area that can go
8572    /// both ways has to pick one and keep it.
8573    #[test]
8574    fn a_scroll_gesture_is_claimed_by_one_axis() {
8575        // Both axes available: the direction the finger went in wins, and a little wobble
8576        // across it changes nothing.
8577        assert!(claim_axis(6.0, 40.0, true, true), "down the page");
8578        assert!(!claim_axis(40.0, 6.0, true, true), "across it");
8579        // A tie reads as vertical, which is the way a page reads.
8580        assert!(claim_axis(10.0, 10.0, true, true));
8581        // One axis only: it takes the gesture whatever the finger did, or a diagonal flick
8582        // down a list would move nothing at all.
8583        assert!(claim_axis(40.0, 6.0, false, true), "a list only goes down");
8584        assert!(
8585            !claim_axis(6.0, 40.0, true, false),
8586            "a strip only goes across"
8587        );
8588    }
8589
8590    /// An area under the finger, `max_x` by `max_y` of content out of sight.
8591    fn area(id: u64, max_x: f32, max_y: f32) -> frus_widgets::Scrollable {
8592        frus_widgets::Scrollable {
8593            id: WidgetId::from_u64(id),
8594            viewport: Rect::new(0.0, 0.0, 100.0, 100.0),
8595            max_x,
8596            max_y,
8597            axis: frus_widgets::Axis::Vertical,
8598            physics: frus_widgets::ScrollPhysics::CLAMPING,
8599            overscroll: Some(frus_widgets::OverscrollIndicator::Glow),
8600            fling: frus_widgets::VelocityStrategy::Regression,
8601            refresh: None,
8602            page: None,
8603            reverse_x: false,
8604            reverse_y: false,
8605            host: None,
8606            keep_visible: None,
8607        }
8608    }
8609
8610    /// A strip that only slides sideways, sitting in a page that only scrolls down, must
8611    /// not swallow a drag downwards: the page behind it is what that drag is about
8612    /// (reported on a device, 2026-08-16).
8613    #[test]
8614    fn the_gesture_goes_to_the_area_that_can_take_it() {
8615        let strip = area(1, 400.0, 0.0);
8616        let page = area(2, 0.0, 900.0);
8617        let chain = [strip, page]; // innermost first, as the finger meets them
8618        assert_eq!(
8619            claim_area(&chain, strip.id, true).map(|a| a.id),
8620            Some(page.id),
8621            "down the page, over the strip"
8622        );
8623        assert_eq!(
8624            claim_area(&chain, strip.id, false).map(|a| a.id),
8625            Some(strip.id),
8626            "across the strip"
8627        );
8628        // Nothing behind it: the strip keeps the gesture and goes its own way, which is
8629        // what a lone recogniser in an arena does.
8630        assert_eq!(
8631            claim_area(&[strip], strip.id, true).map(|a| a.id),
8632            Some(strip.id)
8633        );
8634        // The innermost that can take it wins, not the outermost.
8635        let inner = area(3, 0.0, 200.0);
8636        assert_eq!(
8637            claim_area(&[inner, strip, page], inner.id, true).map(|a| a.id),
8638            Some(inner.id)
8639        );
8640    }
8641
8642    /// **The physics have the last word on a finger** (milestone 649): never scrollable
8643    /// takes none, even with content to reveal, and the area around it gets the drag;
8644    /// always scrollable takes one along its axis with nothing to reveal.
8645    #[test]
8646    fn the_physics_have_the_last_word_on_a_finger() {
8647        let mut inner = area(1, 0.0, 300.0);
8648        inner.physics = frus_widgets::ScrollPhysics::NEVER;
8649        assert!(!inner.accepts_user_offset((0.0, 0.0)));
8650        assert!(!inner.takes_user_scroll(true));
8651        let outer = area(2, 0.0, 600.0);
8652        let chain = [inner, outer];
8653        assert_eq!(
8654            claim_area(&chain, WidgetId::from_u64(1), true).map(|a| a.id),
8655            Some(WidgetId::from_u64(2)),
8656            "the page around it scrolls"
8657        );
8658        assert_eq!(
8659            claim_area(&[inner], WidgetId::from_u64(1), true).map(|a| a.id),
8660            None,
8661            "and alone it keeps nothing"
8662        );
8663
8664        let mut short = area(3, 0.0, 0.0);
8665        short.physics = frus_widgets::ScrollPhysics::ALWAYS;
8666        assert!(short.accepts_user_offset((0.0, 0.0)));
8667        assert!(short.takes_user_scroll(true), "along its axis");
8668        assert!(!short.takes_user_scroll(false), "and not across it");
8669        assert_eq!(
8670            claim_area(&[short, outer], WidgetId::from_u64(3), true).map(|a| a.id),
8671            Some(WidgetId::from_u64(3)),
8672            "a short list that always scrolls keeps the drag from the page"
8673        );
8674        let mut strip = short;
8675        strip.axis = frus_widgets::Axis::Horizontal;
8676        assert_eq!(
8677            claim_area(&[strip, outer], WidgetId::from_u64(3), true).map(|a| a.id),
8678            Some(WidgetId::from_u64(2)),
8679            "a strip that always scrolls across leaves a drag down to the page"
8680        );
8681        let mut both = short;
8682        both.axis = frus_widgets::Axis::Both;
8683        assert!(both.takes_user_scroll(false) && both.takes_user_scroll(true));
8684    }
8685
8686    /// An end-of-content glow on a page with nothing to scroll says something about the
8687    /// content that is not true (reported on a device, 2026-08-16). The reference takes the
8688    /// drag recognisers away outright when everything fits.
8689    #[test]
8690    fn an_area_whose_content_fits_takes_no_finger() {
8691        let fits = area(1, 0.0, 0.0);
8692        assert!(!fits.accepts_user_offset((0.0, 0.0)));
8693        // Unless it is already displaced — content that shrank under a scrolled offset has
8694        // to be draggable back into place.
8695        assert!(fits.accepts_user_offset((0.0, 12.0)));
8696        // Or a refresh area is listening above it: a list of two items still pulls down.
8697        let mut pullable = fits;
8698        pullable.refresh = Some(WidgetId::from_u64(9));
8699        assert!(pullable.accepts_user_offset((0.0, 0.0)));
8700        // A single pixel out of sight is content to reveal, and enough.
8701        assert!(area(1, 0.0, 1.0).accepts_user_offset((0.0, 0.0)));
8702        assert!(area(1, 1.0, 0.0).accepts_user_offset((0.0, 0.0)));
8703    }
8704    use frus_widgets::WidgetId;
8705    use std::collections::HashSet;
8706
8707    /// An estimate reading `velocity` px/s after travelling `offset` px.
8708    fn released(velocity: (f32, f32), offset: (f32, f32)) -> VelocityEstimate {
8709        VelocityEstimate {
8710            velocity: frus_widgets::Velocity::new(velocity.0, velocity.1),
8711            confidence: 1.0,
8712            duration: 0.05,
8713            offset,
8714        }
8715    }
8716
8717    #[test]
8718    fn a_fast_twitch_that_went_nowhere_is_not_a_fling() {
8719        // 2000 px/s, but the finger covered 3 px: a wobble on lift-off, not a throw.
8720        let (x, y) = fling_velocity(released((0.0, 2000.0), (0.0, 3.0)), TOUCH_SLOP);
8721        assert_eq!((x, y), (0.0, 0.0));
8722        // The same speed over a real distance is.
8723        let (_, y) = fling_velocity(released((0.0, 2000.0), (0.0, 60.0)), TOUCH_SLOP);
8724        assert_eq!(y, 2000.0);
8725    }
8726
8727    #[test]
8728    fn the_fling_gate_is_per_axis() {
8729        // A vertical swipe with the sideways wobble a thumb always adds: the wobble
8730        // must not fling the content horizontally.
8731        let (x, y) = fling_velocity(released((300.0, 1500.0), (5.0, 120.0)), TOUCH_SLOP);
8732        assert_eq!(x, 0.0, "the wobble is not a horizontal fling");
8733        assert_eq!(y, 1500.0, "the swipe still flings vertically");
8734    }
8735
8736    #[test]
8737    fn a_precise_pointer_needs_almost_no_travel() {
8738        // The same 3 px that a finger's slop rejects is a deliberate mouse drag.
8739        let estimate = released((0.0, 900.0), (0.0, 3.0));
8740        assert_eq!(fling_velocity(estimate, TOUCH_SLOP).1, 0.0);
8741        assert_eq!(fling_velocity(estimate, PRECISE_SLOP).1, 900.0);
8742    }
8743
8744    #[test]
8745    fn insertion_line_sits_on_the_target_top_edge() {
8746        // The upper half hovered means inserting **before**: a band of thickness 4
8747        // centred on the top edge (y=100) of a target 200 wide.
8748        let line = drop_insertion_line(
8749            Rect::new(20.0, 100.0, 200.0, 44.0),
8750            4.0,
8751            false,
8752            ReorderAxis::Vertical,
8753            false,
8754        );
8755        assert_eq!(line.x, 20.0, "aligned to the target's left");
8756        assert_eq!(line.width, 200.0, "the target's full width");
8757        assert_eq!(line.y, 98.0, "centred on the top edge (100 - 4/2)");
8758        assert_eq!(line.height, 4.0);
8759    }
8760
8761    #[test]
8762    fn insertion_line_sits_on_the_target_bottom_edge_when_inserting_after() {
8763        // The lower half hovered means inserting **after**: the band slides to the
8764        // **bottom** edge (y = 100 + 44 = 144, centred → 142). Same width, same thickness.
8765        let line = drop_insertion_line(
8766            Rect::new(20.0, 100.0, 200.0, 44.0),
8767            4.0,
8768            true,
8769            ReorderAxis::Vertical,
8770            false,
8771        );
8772        assert_eq!(line.x, 20.0, "still aligned to the left");
8773        assert_eq!(line.width, 200.0, "the target's full width");
8774        assert_eq!(line.y, 142.0, "centred on the bottom edge (144 - 4/2)");
8775        assert_eq!(line.height, 4.0);
8776        // A vertical list reads down whichever way its text runs.
8777        let rtl = drop_insertion_line(
8778            Rect::new(20.0, 100.0, 200.0, 44.0),
8779            4.0,
8780            true,
8781            ReorderAxis::Vertical,
8782            true,
8783        );
8784        assert_eq!(rtl, line, "right to left changes nothing down a list");
8785    }
8786
8787    /// **A horizontal list's line stands on its end**, on the edge the row goes to: the
8788    /// right one after a row that reads left to right, the left one after a row that reads
8789    /// right to left — where what follows it is.
8790    #[test]
8791    fn a_horizontal_insertion_line_stands_on_the_edge_the_row_goes_to() {
8792        let chip = Rect::new(100.0, 20.0, 80.0, 48.0);
8793        let line = |after, rtl| drop_insertion_line(chip, 4.0, after, ReorderAxis::Horizontal, rtl);
8794        let before = line(false, false);
8795        assert_eq!(
8796            (before.x, before.y, before.width, before.height),
8797            (98.0, 20.0, 4.0, 48.0),
8798            "before a row: its left edge, the full height, 4 wide"
8799        );
8800        assert_eq!(
8801            line(true, false).x,
8802            178.0,
8803            "after it: its right edge (180 - 4/2)"
8804        );
8805        // Right to left, the two edges trade places.
8806        assert_eq!(
8807            line(true, true).x,
8808            98.0,
8809            "after, right to left: the left edge"
8810        );
8811        assert_eq!(
8812            line(false, true).x,
8813            178.0,
8814            "before, right to left: the right edge"
8815        );
8816    }
8817
8818    /// **Which gesture a reorderable gets.** A column slides; a row or a card is inserted
8819    /// between slots — and a horizontal reorderable is a row only when it says so, which is
8820    /// what keeps a table's columns exactly as they were.
8821    #[test]
8822    fn a_horizontal_reorderable_inserts_only_when_it_says_so() {
8823        assert_eq!(
8824            ReorderMotion::of(ReorderAxis::Horizontal, false),
8825            ReorderMotion::Columns,
8826            "a table's column"
8827        );
8828        assert_eq!(
8829            ReorderMotion::of(ReorderAxis::Horizontal, true),
8830            ReorderMotion::Slots(ReorderAxis::Horizontal),
8831            "a horizontal list's row"
8832        );
8833        // Every vertical reorderable inserts, whatever it answers.
8834        for inserts in [false, true] {
8835            assert_eq!(
8836                ReorderMotion::of(ReorderAxis::Vertical, inserts),
8837                ReorderMotion::Slots(ReorderAxis::Vertical)
8838            );
8839        }
8840        // A widget that answers nothing keeps the trait's defaults, which are the table's.
8841        assert_eq!(
8842            reorder_motion::<()>(&frus_widgets::Container::new()),
8843            ReorderMotion::Columns
8844        );
8845        // And the real rows say which they are, down and across.
8846        for (axis, expected) in [
8847            (
8848                ReorderAxis::Vertical,
8849                ReorderMotion::Slots(ReorderAxis::Vertical),
8850            ),
8851            (
8852                ReorderAxis::Horizontal,
8853                ReorderMotion::Slots(ReorderAxis::Horizontal),
8854            ),
8855        ] {
8856            let list = frus_widgets::ReorderableList::<()>::new(|_, _| ())
8857                .axis(axis)
8858                .row(frus_widgets::Container::new().width(60.0));
8859            let row = frus_widgets::Widget::children(&list)[0].as_ref();
8860            assert_eq!(reorder_motion(row), expected, "{axis:?}");
8861        }
8862    }
8863
8864    /// **The ghost stays in its lane.** A column rises and follows x; a card follows the
8865    /// finger anywhere; a horizontal list's row follows x and nothing else — so a finger
8866    /// that wanders below the strip does not drag the row off it.
8867    #[test]
8868    fn a_horizontal_rows_ghost_follows_the_finger_along_x_only() {
8869        let travel = (120.0, 35.0);
8870        assert_eq!(
8871            ghost_offset(ReorderMotion::Columns, travel),
8872            (120.0, super::drag_preview::LIFT_Y)
8873        );
8874        assert_eq!(
8875            ghost_offset(ReorderMotion::Slots(ReorderAxis::Vertical), travel),
8876            (120.0, 35.0)
8877        );
8878        assert_eq!(
8879            ghost_offset(ReorderMotion::Slots(ReorderAxis::Horizontal), travel),
8880            (120.0, 0.0)
8881        );
8882    }
8883
8884    #[test]
8885    fn focus_returns_to_trigger_when_overlay_closes() {
8886        let anchor = WidgetId::from_u64(1);
8887        let item = WidgetId::from_u64(2);
8888        let (mut history, mut prev) = (Vec::new(), None);
8889
8890        // Frame 1: focus on the anchor, which is present.
8891        let f = resolve_focus(
8892            Some(anchor),
8893            &HashSet::from([anchor]),
8894            &mut history,
8895            &mut prev,
8896        );
8897        assert_eq!(f, Some(anchor));
8898        assert!(history.is_empty());
8899
8900        // Frame 2: the menu is open and the item focused; the anchor is pushed as trigger.
8901        let present = HashSet::from([anchor, item]);
8902        let f = resolve_focus(Some(item), &present, &mut history, &mut prev);
8903        assert_eq!(f, Some(item));
8904        assert_eq!(history, vec![anchor]);
8905
8906        // Frame 3: the menu closed and the item vanished → back to the trigger, history spent.
8907        let f = resolve_focus(
8908            Some(item),
8909            &HashSet::from([anchor]),
8910            &mut history,
8911            &mut prev,
8912        );
8913        assert_eq!(f, Some(anchor), "focus returns to the trigger");
8914        assert!(history.is_empty());
8915    }
8916
8917    #[test]
8918    fn focus_falls_to_none_when_no_trigger_remains() {
8919        let a = WidgetId::from_u64(1);
8920        let (mut history, mut prev) = (Vec::new(), Some(a));
8921        // `a` vanishes and the history is empty → no focus at all.
8922        let f = resolve_focus(Some(a), &HashSet::new(), &mut history, &mut prev);
8923        assert_eq!(f, None);
8924        assert_eq!(prev, None);
8925    }
8926
8927    #[test]
8928    fn spring_approaches_target_monotonically_and_settles() {
8929        // From 0 toward 100: a monotonic approach with no overshoot, all but reached
8930        // after several 16 ms steps.
8931        let mut x = 0.0;
8932        let mut prev = -1.0;
8933        for _ in 0..30 {
8934            x = spring_toward(x, 100.0, 0.016, 0.07);
8935            assert!(x > prev && x <= 100.0, "monotonic and bounded: {x}");
8936            prev = x;
8937        }
8938        assert!(x > 99.0, "all but reached after ~0.5 s: {x}");
8939    }
8940
8941    #[test]
8942    fn ghost_card_shape() {
8943        let theme = Theme::default();
8944        let card = Rect::new(140.0, 0.0, 80.0, 34.0);
8945        // An empty ghost falls back to a shadow plus a solid bordered card: 2 primitives.
8946        let mut scene = Scene::new();
8947        draw_ghost_card(&mut scene, &theme, card, &[]);
8948        assert_eq!(scene.primitives().len(), 2, "shadow plus solid card");
8949    }
8950}
8951
8952/// The half of a scroll request that belongs to the shell: turning the **name** an
8953/// application wrote into the region a frame actually has.
8954#[cfg(test)]
8955mod sheet_request_tests {
8956    use super::{apply_sheet_requests, Runtime, Widget};
8957    use crate::command::Command;
8958    use frus_widgets::{
8959        build_ui, keyed, Container, DraggableScrollableSheet, SheetTo, Size, Theme,
8960    };
8961
8962    /// A page with one named sheet on it — the key on a wrapper the application wrote, the
8963    /// state on a panel it never sees.
8964    fn view() -> Box<dyn Widget<()>> {
8965        Box::new(
8966            Container::<()>::new()
8967                .width(400.0)
8968                .height(800.0)
8969                .child(keyed(
8970                    "places",
8971                    DraggableScrollableSheet::<()>::new(Container::new()),
8972                )),
8973        )
8974    }
8975
8976    /// **A request reaches the sheet the view named**, and nothing else: another key, or a
8977    /// frame without the sheet, hands it back unplaced and moves nothing.
8978    #[test]
8979    fn a_request_reaches_the_sheet_the_view_named() {
8980        let tree = view();
8981        let mut runtime = Runtime::default();
8982        let sheets = build_ui(
8983            tree.as_ref(),
8984            Size::new(400.0, 800.0),
8985            &runtime,
8986            &Theme::default(),
8987        )
8988        .sheets()
8989        .to_vec();
8990        let sheet = sheets.first().expect("the page has its sheet").clone();
8991
8992        let requests = Command::<()>::sheet("places", SheetTo::size(0.9))
8993            .into_parts()
8994            .sheets;
8995        let (unplaced, moved) =
8996            apply_sheet_requests(&mut runtime, tree.as_ref(), requests, &sheets);
8997        assert!(unplaced.is_empty());
8998        assert!(moved);
8999        assert_eq!(runtime.sheet_size(sheet.id, &sheet.spec), 0.9);
9000
9001        let elsewhere = Command::<()>::sheet("elsewhere", SheetTo::size(0.3))
9002            .into_parts()
9003            .sheets;
9004        let (unplaced, moved) =
9005            apply_sheet_requests(&mut runtime, tree.as_ref(), elsewhere, &sheets);
9006        assert_eq!(unplaced.len(), 1);
9007        assert!(!moved);
9008        let not_yet = Command::<()>::sheet("places", SheetTo::size(0.3))
9009            .into_parts()
9010            .sheets;
9011        let (unplaced, _) = apply_sheet_requests(&mut runtime, tree.as_ref(), not_yet, &[]);
9012        assert_eq!(unplaced.len(), 1, "kept for the frame that has the sheet");
9013        assert_eq!(
9014            runtime.sheet_size(sheet.id, &sheet.spec),
9015            0.9,
9016            "nothing moved"
9017        );
9018    }
9019}
9020
9021#[cfg(test)]
9022mod scroll_request_tests {
9023    use super::{apply_scroll_requests, Rect, Runtime, Scrollable, Widget, WidgetId};
9024    use crate::command::Command;
9025    use frus_widgets::{keyed, Container, ScrollTo, SingleChildScrollView};
9026
9027    /// A view with one named scroll region in it, and nothing else of interest.
9028    fn view() -> Box<dyn Widget<()>> {
9029        Box::new(Container::<()>::new().child(keyed(
9030            "log",
9031            SingleChildScrollView::<()>::new().height(200.0),
9032        )))
9033    }
9034
9035    /// The region as the frame would register it: a 200-tall window over 1 000 of content.
9036    fn region(id: WidgetId) -> Scrollable {
9037        Scrollable {
9038            id,
9039            viewport: Rect {
9040                x: 0.0,
9041                y: 0.0,
9042                width: 300.0,
9043                height: 200.0,
9044            },
9045            max_x: 0.0,
9046            max_y: 1000.0,
9047            axis: frus_widgets::Axis::Vertical,
9048            physics: frus_widgets::ScrollPhysics::CLAMPING,
9049            overscroll: Some(frus_widgets::OverscrollIndicator::Glow),
9050            fling: frus_widgets::VelocityStrategy::Regression,
9051            refresh: None,
9052            page: None,
9053            reverse_x: false,
9054            reverse_y: false,
9055            keep_visible: None,
9056            host: None,
9057        }
9058    }
9059
9060    /// The key a request carries and the key the view declared are the **same hash** —
9061    /// which is the whole of the identity design, and the one thing that cannot be
9062    /// checked on either side alone.
9063    #[test]
9064    fn a_request_reaches_the_region_the_view_named() {
9065        let tree = view();
9066        let requests = Command::<()>::scroll("log", ScrollTo::end().instant())
9067            .into_parts()
9068            .scrolls;
9069        let id = frus_widgets::find_by_key(tree.as_ref(), requests[0].0)
9070            .expect("the view named it, so the tree knows it");
9071        let mut runtime = Runtime::default();
9072        let (unplaced, moved) =
9073            apply_scroll_requests(&mut runtime, tree.as_ref(), requests, &[region(id)]);
9074        assert!(unplaced.is_empty());
9075        assert!(moved);
9076        assert_eq!(runtime.scroll.get(&id), Some(&(0.0, 1000.0)));
9077    }
9078
9079    /// A region that has only just appeared is not in the registry the request is first
9080    /// tried against — that one was built last frame — so it is handed back rather than
9081    /// thrown away. Coming back to a screen and being put where you left off depends on
9082    /// exactly this.
9083    #[test]
9084    fn a_region_this_registry_has_not_got_is_handed_back() {
9085        let tree = view();
9086        let requests = Command::<()>::scroll("log", ScrollTo::y(120.0))
9087            .into_parts()
9088            .scrolls;
9089        let mut runtime = Runtime::default();
9090        let (unplaced, moved) = apply_scroll_requests(&mut runtime, tree.as_ref(), requests, &[]);
9091        assert_eq!(unplaced.len(), 1, "kept for the second attempt");
9092        assert!(!moved);
9093        assert!(runtime.scroll_target.is_empty());
9094    }
9095
9096    /// A key naming nothing at all leaves no trace: the caller drops what comes back,
9097    /// so a typo costs one frame's lookup and never accumulates.
9098    #[test]
9099    fn a_name_the_view_does_not_use_moves_nothing() {
9100        let tree = view();
9101        let requests = Command::<()>::scroll("ledger", ScrollTo::start())
9102            .into_parts()
9103            .scrolls;
9104        let mut runtime = Runtime::default();
9105        let id = frus_widgets::find_by_key(
9106            tree.as_ref(),
9107            Command::<()>::scroll("log", ScrollTo::start())
9108                .into_parts()
9109                .scrolls[0]
9110                .0,
9111        )
9112        .expect("the real one is there");
9113        let (unplaced, moved) =
9114            apply_scroll_requests(&mut runtime, tree.as_ref(), requests, &[region(id)]);
9115        assert_eq!(unplaced.len(), 1);
9116        assert!(!moved);
9117        assert!(runtime.scroll.is_empty() && runtime.scroll_target.is_empty());
9118    }
9119
9120    /// Two requests in one batch, both placed, and the batch is what `Command::batch`
9121    /// produces — a screen that restores both axes writes two and means two.
9122    #[test]
9123    fn a_batch_places_every_request_it_carries() {
9124        let tree = view();
9125        let requests = Command::<()>::batch([
9126            Command::scroll("log", ScrollTo::y(400.0).instant()),
9127            Command::scroll("log", ScrollTo::y(600.0).instant()),
9128        ])
9129        .into_parts()
9130        .scrolls;
9131        assert_eq!(requests.len(), 2);
9132        let id = frus_widgets::find_by_key(tree.as_ref(), requests[0].0).expect("named");
9133        let mut runtime = Runtime::default();
9134        let (unplaced, moved) =
9135            apply_scroll_requests(&mut runtime, tree.as_ref(), requests, &[region(id)]);
9136        assert!(unplaced.is_empty() && moved);
9137        assert_eq!(
9138            runtime.scroll.get(&id),
9139            Some(&(0.0, 600.0)),
9140            "the last request is the current statement"
9141        );
9142    }
9143}
9144
9145#[cfg(test)]
9146mod autoscroll_tests {
9147    use super::{
9148        edge_autoscroll, follow_content, Drag, Point, Rect, WidgetId, AUTOSCROLL_MAX_OVERHANG,
9149        AUTOSCROLL_VELOCITY,
9150    };
9151
9152    /// A viewport from 100 to 500, with room to scroll either way.
9153    const VIEW: (f32, f32) = (100.0, 500.0);
9154    const ROOM: (f32, f32) = (1000.0, 1000.0);
9155    /// A sixtieth of a second, which is the frame this is asked on.
9156    const DT: f32 = 1.0 / 60.0;
9157
9158    #[test]
9159    fn a_row_inside_the_viewport_scrolls_nothing() {
9160        assert_eq!(edge_autoscroll((200.0, 260.0), VIEW, ROOM, DT), None);
9161        // Touching the edge exactly is still inside: the row has not asked for anything.
9162        assert_eq!(edge_autoscroll((440.0, 500.0), VIEW, ROOM, DT), None);
9163    }
9164
9165    /// The law: pixels per second per pixel of overhang, so ten pixels out is ten times
9166    /// the speed of one pixel out — the row is not dragged along by a fixed nudge.
9167    #[test]
9168    fn the_speed_follows_how_far_out_the_row_is() {
9169        let near = edge_autoscroll((460.0, 501.0), VIEW, ROOM, DT).expect("one pixel out");
9170        let far = edge_autoscroll((460.0, 510.0), VIEW, ROOM, DT).expect("ten pixels out");
9171        assert!(
9172            near < 0.0,
9173            "the content moves up to reveal what is below: {near}"
9174        );
9175        assert!(
9176            (far / near - 10.0).abs() < 0.01,
9177            "ten times as far, ten times as fast: {near} vs {far}"
9178        );
9179        assert!(
9180            (near + 1.0 * AUTOSCROLL_VELOCITY * DT).abs() < 1e-4,
9181            "one pixel out, at the stated velocity: {near}"
9182        );
9183    }
9184
9185    /// Carrying a row right off the window is not a request to scroll a thousand times
9186    /// faster; past the cap the speed stops growing.
9187    #[test]
9188    fn the_overhang_is_capped() {
9189        let far = edge_autoscroll((460.0, 520.0), VIEW, ROOM, DT).expect("twenty out");
9190        let absurd = edge_autoscroll((460.0, 5000.0), VIEW, ROOM, DT).expect("off the window");
9191        assert!((far - absurd).abs() < 1e-4, "{far} vs {absurd}");
9192        assert!((absurd + AUTOSCROLL_MAX_OVERHANG * AUTOSCROLL_VELOCITY * DT).abs() < 1e-4);
9193    }
9194
9195    /// Above the top the content moves the other way, and the sign is the whole of the
9196    /// difference between the two edges.
9197    #[test]
9198    fn above_the_top_the_content_comes_down() {
9199        let up = edge_autoscroll((90.0, 150.0), VIEW, ROOM, DT).expect("ten above");
9200        assert!(up > 0.0, "{up}");
9201    }
9202
9203    /// **What is carried stays under the finger while its list scrolls.**
9204    ///
9205    /// Seen on a phone: a row carried to the bottom edge started the list scrolling, and
9206    /// the ghost rode up with the content — it is drawn at the row's box, and the box had
9207    /// moved — until it no longer hung over the edge and the scroll stopped by itself,
9208    /// 95 px in. The press moves with the content instead.
9209    #[test]
9210    fn a_carried_item_follows_its_content() {
9211        // The list scrolled on by 95 px: its content went 95 px up the screen.
9212        let shift = (0.0, -95.0);
9213        let pressed = Point::new(900.0, 1330.0);
9214        let row_then = Rect::new(105.0, 1250.0, 700.0, 160.0);
9215        let row_now = row_then.translate(0.0, -95.0);
9216        let finger = Point::new(900.0, 2030.0);
9217
9218        let mut reorder = Drag::Reorder {
9219            id: WidgetId::from_u64(1),
9220            from: 0,
9221            start: pressed,
9222            moved: true,
9223            carried: Some((WidgetId::from_u64(1), row_then)),
9224        };
9225        follow_content(&mut reorder, shift);
9226        let Drag::Reorder { start, carried, .. } = reorder else {
9227            unreachable!("still a reorder");
9228        };
9229        // The box it was carried from went with the content, as the row did — and the frame's
9230        // own box is not asked again, since the frame clips a row carried to an edge and loses
9231        // one scrolled out of sight (milestone 527).
9232        let (_, carried) = carried.expect("the box it was carried from");
9233        assert_eq!(carried, row_now, "the carried box went with the content");
9234        // Drawn the way the shell draws it: the carried box, plus the travel.
9235        let ghost = carried.translate(finger.x - start.x, finger.y - start.y);
9236        assert_eq!(
9237            ghost,
9238            row_then.translate(finger.x - pressed.x, finger.y - pressed.y),
9239            "the ghost is where it would be had nothing scrolled"
9240        );
9241
9242        let mut item = Drag::Item {
9243            source: frus_widgets::DragSource {
9244                id: WidgetId::from_u64(1),
9245                rect: row_then,
9246                payload: 7,
9247            },
9248            start: pressed,
9249            moved: true,
9250            over: None,
9251        };
9252        follow_content(&mut item, shift);
9253        let Drag::Item { source, start, .. } = item else {
9254            unreachable!("still an item");
9255        };
9256        assert_eq!(
9257            source.rect, row_now,
9258            "the box taken at the press went with the content"
9259        );
9260        assert_eq!(start, Point::new(900.0, 1235.0), "and so did the press");
9261    }
9262
9263    /// At the end of the content there is nothing left to reveal, so the list stays where
9264    /// it is instead of straining against its own end.
9265    #[test]
9266    fn an_exhausted_edge_stays_still() {
9267        assert_eq!(
9268            edge_autoscroll((460.0, 520.0), VIEW, (1000.0, 0.0), DT),
9269            None
9270        );
9271        assert_eq!(
9272            edge_autoscroll((90.0, 150.0), VIEW, (0.0, 1000.0), DT),
9273            None
9274        );
9275    }
9276
9277    /// A row taller than the viewport hangs over both edges at once. Pulled two ways it
9278    /// would scroll nowhere, or jitter between them; the leading edge decides.
9279    #[test]
9280    fn a_row_taller_than_the_viewport_follows_its_leading_edge() {
9281        let both = edge_autoscroll((50.0, 900.0), VIEW, ROOM, DT).expect("over both edges");
9282        assert!(both > 0.0, "the top edge wins: {both}");
9283    }
9284}
9285
9286/// **A back gesture the page below cannot take** (milestone 531).
9287///
9288/// Seen on a phone: a back gesture from the left edge, at the height of a task row of the
9289/// page below, did not slide the page on top. Half a second in, the row was lifted instead —
9290/// drawn over the page on top in its green-outlined ghost, and following the finger — and it
9291/// went away when the finger did. At a height with no row the gesture worked.
9292///
9293/// The press that starts the gesture is tested against the frame on screen, and after a push
9294/// that frame is the push's last: an application's animation that has just settled asks for
9295/// no rebuild, so the page the push left is still in the frame, parallaxed to the left and
9296/// under the finger. The press found the row's hold, the long press armed, and its deadline
9297/// replaced the gesture with a lift.
9298#[cfg(test)]
9299mod back_gesture_tests {
9300    use super::{
9301        drag_after_hold, frame_needs_build, hold_candidates, Drag, Point, Theme, WidgetId,
9302    };
9303    use frus_widgets::{build_ui, column, Container, DragSource, Draggable, Navigator};
9304    use frus_widgets::{Rect, Runtime, Size, Ui, Widget};
9305
9306    /// A phone, in logical pixels.
9307    const W: f32 = 392.0;
9308    const H: f32 = 850.0;
9309    /// Where the row sits on the page below, and the finger's press on the edge at its
9310    /// height.
9311    const ROW_TOP: f32 = 480.0;
9312    const ROW_HEIGHT: f32 = 66.0;
9313    const ON_THE_ROW: Point = Point::new(3.0, 509.0);
9314    const ABOVE_THE_ROW: Point = Point::new(3.0, 200.0);
9315    /// The long press the row asks for.
9316    const HELD: u8 = 1;
9317
9318    /// A page with one row that lifts on a hold and says something when held, below a
9319    /// stretch with nothing to take.
9320    fn home() -> impl Widget<u8> {
9321        column![
9322            Container::<u8>::new().width(W).height(ROW_TOP),
9323            Draggable::new(
9324                Container::<u8>::new()
9325                    .width(W)
9326                    .height(ROW_HEIGHT)
9327                    .on_long_press(HELD)
9328            )
9329            .payload(7)
9330            .long_press(),
9331        ]
9332    }
9333
9334    /// The frame on screen after a push from `home` has settled: the push's last, with the
9335    /// page it left still in it.
9336    fn after_a_push() -> Navigator<u8> {
9337        Navigator::new("data", Container::<u8>::new().width(W).height(H))
9338            .size(W, H)
9339            .from("home", home(), 0.999, true)
9340    }
9341
9342    fn frame(root: &dyn Widget<u8>) -> Ui<u8> {
9343        build_ui(
9344            root,
9345            Size::new(W, H),
9346            &Runtime::default(),
9347            &Theme::default(),
9348        )
9349    }
9350
9351    fn back(at: Point) -> Drag {
9352        Drag::Back { start_x: at.x }
9353    }
9354
9355    fn still_scroll() -> Drag {
9356        Drag::Scroll {
9357            id: WidgetId::from_u64(9),
9358            last: ON_THE_ROW,
9359            moved: false,
9360            carried: (0.0, 0.0),
9361            dismiss: None,
9362            axis: None,
9363            pan: None,
9364        }
9365    }
9366
9367    fn row_source() -> DragSource {
9368        DragSource {
9369            id: WidgetId::from_u64(3),
9370            rect: Rect::new(-117.0, ROW_TOP, W, ROW_HEIGHT),
9371            payload: 7,
9372        }
9373    }
9374
9375    /// **Why the row was under the finger at all**: the frame a transition ends on is built
9376    /// again. The tick that ends a push takes the page it left out of the state and reports
9377    /// nothing moving, so a loop that builds only while something moves hit-tests the
9378    /// push's last frame from then on — the page left behind still in it, where a tap or a
9379    /// hold can reach what is no longer on screen. The demo's
9380    /// `the_last_frame_of_a_push_still_holds_the_page_it_left` shows that frame on its own
9381    /// page, at the phone's coordinates.
9382    #[test]
9383    fn the_frame_an_animation_settles_in_is_built_again() {
9384        assert!(
9385            frame_needs_build(false, false, true, false),
9386            "the frame the application's animation stops in builds the view again"
9387        );
9388        assert!(
9389            frame_needs_build(false, true, true, false),
9390            "as does every frame while it moves"
9391        );
9392        assert!(
9393            !frame_needs_build(false, false, false, false),
9394            "and a still frame after a still one repaints the tree it has"
9395        );
9396        assert!(frame_needs_build(true, false, false, false), "unless asked");
9397        assert!(
9398            frame_needs_build(false, false, false, true),
9399            "or the frame has its own reason"
9400        );
9401    }
9402
9403    /// The reproduction: the frame really has a row to take under the finger, and a back
9404    /// gesture's press takes nothing from it.
9405    #[test]
9406    fn a_back_gesture_claims_nothing_under_its_finger() {
9407        let root = after_a_push();
9408        let ui = frame(&root);
9409        let (held, lift) = hold_candidates(None, Some(&ui), Some(&root), ON_THE_ROW);
9410        assert!(
9411            lift.is_some() && held == Some(HELD),
9412            "the frame the phone had: a press there, with no gesture, finds the row's hold"
9413        );
9414
9415        let gesture = back(ON_THE_ROW);
9416        let (held, lift) = hold_candidates(Some(&gesture), Some(&ui), Some(&root), ON_THE_ROW);
9417        assert!(
9418            lift.is_none(),
9419            "a back gesture's press arms no lift of the row under it"
9420        );
9421        assert_eq!(held, None, "nor its long press");
9422    }
9423
9424    /// Whatever a press found, the deadline never takes the pointer from a back gesture:
9425    /// the gesture goes on, and keeps its start.
9426    #[test]
9427    fn a_hold_never_takes_the_pointer_from_a_back_gesture() {
9428        let reorder = Some((WidgetId::from_u64(4), 2, ON_THE_ROW));
9429        for (lift, reorder) in [
9430            (Some(row_source()), None),
9431            (None, reorder),
9432            (Some(row_source()), reorder),
9433            (None, None),
9434        ] {
9435            let after = drag_after_hold(Some(back(ON_THE_ROW)), lift, reorder, ON_THE_ROW);
9436            assert!(
9437                matches!(after, Some(Drag::Back { start_x }) if start_x == ON_THE_ROW.x),
9438                "the back gesture is still the drag (lift {}, reorder {})",
9439                lift.is_some(),
9440                reorder.is_some()
9441            );
9442        }
9443    }
9444
9445    /// At a height with nothing to take — where the gesture always worked — it still does.
9446    #[test]
9447    fn a_back_gesture_where_nothing_is_under_it_still_owns_the_pointer() {
9448        let root = after_a_push();
9449        let ui = frame(&root);
9450        let gesture = back(ABOVE_THE_ROW);
9451        let (held, lift) = hold_candidates(Some(&gesture), Some(&ui), Some(&root), ABOVE_THE_ROW);
9452        assert!(held.is_none() && lift.is_none());
9453        assert!(matches!(
9454            drag_after_hold(Some(gesture), None, None, ABOVE_THE_ROW),
9455            Some(Drag::Back { .. })
9456        ));
9457    }
9458
9459    /// No regression: a hold on the row with no back gesture — the list's scroll still
9460    /// waiting to see whether the finger moves — lifts it, or its reorder, and says what
9461    /// the row asks when held.
9462    #[test]
9463    fn a_hold_on_a_row_without_a_back_gesture_still_lifts_it() {
9464        let root = home();
9465        let ui = frame(&root);
9466        let at = Point::new(40.0, ROW_TOP + 10.0);
9467        let scroll = still_scroll();
9468        let (held, lift) = hold_candidates(Some(&scroll), Some(&ui), Some(&root), at);
9469        assert_eq!(
9470            held,
9471            Some(HELD),
9472            "the long press is armed under a still scroll"
9473        );
9474        assert!(lift.is_some(), "and so is the lift");
9475        let (held, lift) = hold_candidates(None, Some(&ui), Some(&root), at);
9476        assert!(held.is_some() && lift.is_some(), "and with no drag at all");
9477
9478        let cursor = Point::new(41.0, ROW_TOP + 11.0);
9479        let lifted = drag_after_hold(Some(still_scroll()), Some(row_source()), None, cursor);
9480        assert!(
9481            matches!(lifted, Some(Drag::Item { moved: true, start, .. }) if start == cursor),
9482            "the hold lifts the item where the finger is"
9483        );
9484        let reorder = Some((WidgetId::from_u64(4), 2, at));
9485        let reordering = drag_after_hold(Some(still_scroll()), None, reorder, cursor);
9486        assert!(
9487            matches!(reordering, Some(Drag::Reorder { moved: true, from: 2, start, .. }) if start == at),
9488            "or picks the row up for a reorder, from where it was pressed"
9489        );
9490        assert!(
9491            drag_after_hold(Some(still_scroll()), None, None, cursor).is_none(),
9492            "a hold with nothing to lift ends the still scroll: the long press had it"
9493        );
9494    }
9495}
9496
9497/// How many layers `primitives` hold, nested ones included (milestone 609).
9498fn count_layers(primitives: &[frus_widgets::Primitive]) -> u32 {
9499    primitives
9500        .iter()
9501        .map(|p| match p {
9502            frus_widgets::Primitive::Layer { primitives, .. } => 1 + count_layers(primitives),
9503            _ => 0,
9504        })
9505        .sum()
9506}
9507
9508/// **What the shell reads of a key going down** (milestone 608). Winit's own event cannot be
9509/// made outside winit, so the shell's handling takes this, made from it in a window and by
9510/// hand in a test.
9511pub(crate) struct KeyDown {
9512    /// The key the layout makes of it: a character, or a named key.
9513    logical_key: WinitKey,
9514    /// Where it is on the keyboard, whatever the layout.
9515    physical_key: PhysicalKey,
9516    /// The text it types, if any.
9517    text: Option<winit::keyboard::SmolStr>,
9518    /// Whether it is the key repeating, held down.
9519    repeat: bool,
9520}
9521
9522impl From<&winit::event::KeyEvent> for KeyDown {
9523    fn from(event: &winit::event::KeyEvent) -> Self {
9524        Self {
9525            logical_key: event.logical_key.clone(),
9526            physical_key: event.physical_key,
9527            text: event.text.clone(),
9528            repeat: event.repeat,
9529        }
9530    }
9531}
9532
9533/// `scene` over the window's background, for a see-through renderer, whose frames start
9534/// transparent (milestone 643): everywhere but the `line` pixels at the top the system paints,
9535/// which are left clear for its title bar to show through. In physical pixels, as the scene
9536/// is; the scene's own primitives follow, in their order.
9537fn backdrop_under(mut scene: Scene, width: f32, height: f32, line: f32) -> Scene {
9538    let mut framed = Scene::new();
9539    framed.fill_rect(
9540        frus_widgets::Rect::new(0.0, line, width, (height - line).max(0.0)),
9541        frus_gpu::backdrop(),
9542    );
9543    for primitive in scene.split_off(0) {
9544        framed.push_primitive(primitive);
9545    }
9546    framed
9547}
9548
9549#[cfg(test)]
9550mod backdrop_tests {
9551    use super::*;
9552
9553    /// **A see-through frame is the window's background under everything but the line the
9554    /// system paints**, and the scene over it, unchanged.
9555    #[test]
9556    fn the_background_leaves_the_system_s_line_clear() {
9557        let mut scene = Scene::new();
9558        scene.fill_rect(
9559            frus_widgets::Rect::new(10.0, 5.0, 20.0, 20.0),
9560            frus_widgets::Color::WHITE,
9561        );
9562        let framed = backdrop_under(scene.clone(), 800.0, 600.0, 48.0);
9563        let prims = framed.primitives();
9564        assert_eq!(prims.len(), 2);
9565        match &prims[0] {
9566            frus_widgets::Primitive::Rect { rect, color, .. } => {
9567                assert_eq!(*rect, frus_widgets::Rect::new(0.0, 48.0, 800.0, 552.0));
9568                assert_eq!(*color, frus_gpu::backdrop());
9569                assert_eq!(
9570                    color.a, 1.0,
9571                    "opaque: nothing behind the window shows there"
9572                );
9573            }
9574            other => panic!("the background first: {other:?}"),
9575        }
9576        assert_eq!(prims[1], scene.primitives()[0], "then the scene");
9577
9578        let whole = backdrop_under(Scene::new(), 800.0, 600.0, 0.0);
9579        assert!(
9580            matches!(&whole.primitives()[0],
9581            frus_widgets::Primitive::Rect { rect, .. } if *rect == frus_widgets::Rect::new(0.0, 0.0, 800.0, 600.0)),
9582            "no line: the whole window"
9583        );
9584    }
9585}
9586
9587/// A driver with no window and no event loop: input and frames fed by hand, through the
9588/// shell's own paths.
9589#[cfg(any(test, feature = "testing"))]
9590#[doc(hidden)]
9591pub mod testing {
9592    use super::*;
9593    use crate::gesture::{PointerEvent, PointerKind, LONG_PRESS_DELAY};
9594
9595    /// What the last frame left behind: the interface it laid out, and the tree it laid out from.
9596    pub type FrameParts<'a, Msg> = (&'a Ui<Msg>, &'a dyn Widget<Msg>);
9597
9598    /// The shell around an application, on a surface of a stated logical size.
9599    pub struct Driver<A: Application> {
9600        shell: App<A>,
9601        size: Size,
9602        /// Where the last frame drew a reorder's ghost, when it drew one.
9603        ghost: Option<Rect>,
9604    }
9605
9606    impl<A: Application> Driver<A> {
9607        /// A driver for `app` on a `width` by `height` logical surface.
9608        pub fn new(app: A, width: f32, height: f32) -> Self {
9609            let mut shell = App::detached(app);
9610            // Gestures are timed by the frames run, as everything else here is.
9611            shell.gesture_clock = Some((0.0, 0.0));
9612            Self {
9613                shell,
9614                size: Size::new(width, height),
9615                ghost: None,
9616            }
9617        }
9618
9619        /// The system's **Back** — Android's back button, the browser's Back — as the shell
9620        /// handles it, short of leaving: whether it went back from anything.
9621        pub fn system_back(&mut self) -> bool {
9622            self.shell.system_back_step()
9623        }
9624
9625        /// How the gesture under way will read its fling: its scroll area's behaviour's, on
9626        /// a drag that landed on one, else the application's (milestone 616).
9627        pub fn fling_strategy(&self) -> frus_widgets::VelocityStrategy {
9628            self.shell.gesture_velocity.strategy()
9629        }
9630
9631        /// Where what is carried is drawn this frame — a reorder's ghost or a lifted item —
9632        /// or `None` when nothing is.
9633        pub fn carried(&self) -> Option<Rect> {
9634            self.shell.carried_rect()
9635        }
9636
9637        /// Where the last frame drew a reorder's ghost — the outline around it — or `None`
9638        /// when it drew none.
9639        pub fn ghost(&self) -> Option<Rect> {
9640            self.ghost
9641        }
9642
9643        /// Every scroll region of the last frame, as its viewport and its offset.
9644        pub fn offsets(&self) -> Vec<(Rect, (f32, f32))> {
9645            let s = &self.shell;
9646            s.ui.as_ref()
9647                .map(|ui| {
9648                    ui.scroll_regions()
9649                        .iter()
9650                        .map(|a| {
9651                            (
9652                                a.viewport,
9653                                s.runtime.scroll.get(&a.id).copied().unwrap_or((0.0, 0.0)),
9654                            )
9655                        })
9656                        .collect()
9657                })
9658                .unwrap_or_default()
9659        }
9660
9661        /// The interface the last frame laid out — what a tap is asked about, what the registries
9662        /// hold — and the tree it was laid out from.
9663        pub fn frame_parts(&self) -> Option<FrameParts<'_, A::Message>> {
9664            let s = &self.shell;
9665            Some((s.ui.as_ref()?, s.tree.as_deref()?))
9666        }
9667
9668        /// The words the last frame drew and the box each was drawn in, in paint order.
9669        pub fn texts(&self) -> Vec<(String, Rect)> {
9670            self.shell
9671                .ui
9672                .as_ref()
9673                .map(|ui| {
9674                    ui.scene()
9675                        .primitives()
9676                        .iter()
9677                        .filter_map(|p| match p {
9678                            frus_widgets::Primitive::Text { text, .. } => {
9679                                Some((text.clone(), p.bounds()))
9680                            }
9681                            _ => None,
9682                        })
9683                        .collect()
9684                })
9685                .unwrap_or_default()
9686        }
9687
9688        /// A tap on the first word of the last frame that reads `label`, and a frame after it:
9689        /// how a test presses a button by what it says. Whether there was such a word.
9690        pub fn tap_text(&mut self, label: &str) -> bool {
9691            let Some((_, rect)) = self.texts().into_iter().find(|(text, _)| text == label) else {
9692                return false;
9693            };
9694            let at = Point::new(rect.x + rect.width / 2.0, rect.y + rect.height / 2.0);
9695            self.press(at);
9696            self.release(at);
9697            self.frame(1.0 / 60.0);
9698            true
9699        }
9700
9701        /// The application, as the shell holds it.
9702        pub fn app(&self) -> &A {
9703            &self.shell.app
9704        }
9705
9706        /// The widget with the keyboard focus, if one has it.
9707        pub fn focused(&self) -> Option<WidgetId> {
9708            self.shell.runtime.input.focused
9709        }
9710
9711        /// The modifiers the shell holds as down: Ctrl, Shift, Alt and Meta.
9712        pub fn shell_modifiers(&self) -> (bool, bool, bool, bool) {
9713            (
9714                self.shell.ctrl,
9715                self.shell.shift,
9716                self.shell.alt,
9717                self.shell.meta,
9718            )
9719        }
9720
9721        /// The widget the last frame would hand a press at `at`, if any.
9722        pub fn hit(&self, at: Point) -> Option<WidgetId> {
9723            self.shell.ui.as_ref().and_then(|ui| ui.hit(at))
9724        }
9725
9726        /// Hands the application a message, as the shell does.
9727        pub fn update(&mut self, message: A::Message) {
9728            self.shell.dispatch(message);
9729        }
9730
9731        /// Frames at sixty a second for `seconds`.
9732        pub fn run(&mut self, seconds: f32) {
9733            let n = (seconds * 60.0).round() as usize;
9734            for _ in 0..n.max(1) {
9735                self.frame(1.0 / 60.0);
9736            }
9737        }
9738
9739        /// A finger down at `at`.
9740        pub fn press(&mut self, at: Point) {
9741            self.pointer(PointerKind::Down, at);
9742        }
9743
9744        /// The finger moved to `at`.
9745        pub fn move_to(&mut self, at: Point) {
9746            self.pointer(PointerKind::Move, at);
9747        }
9748
9749        /// The finger lifted at `at`.
9750        pub fn release(&mut self, at: Point) {
9751            self.pointer(PointerKind::Up, at);
9752        }
9753
9754        /// The long-press deadline, delivered as the loop's wake delivers it. Whether it
9755        /// fired: a press that moved past the slop, or was never waiting, has none.
9756        pub fn hold_deadline(&mut self) -> bool {
9757            let fired = self.shell.press.poll(Instant::now() + LONG_PRESS_DELAY);
9758            if fired {
9759                self.shell.hold_deadline_reached();
9760            }
9761            fired
9762        }
9763
9764        /// The double tap's wait, over: a first tap still waiting is sent as a tap. Whether one
9765        /// was waiting.
9766        pub fn tap_deadline(&mut self) -> bool {
9767            self.shell
9768                .poll_pending_tap(Instant::now() + DOUBLE_TAP_TIMEOUT)
9769        }
9770
9771        /// The cursor the window would show where the pointer is now.
9772        pub fn cursor_shown(&self) -> frus_widgets::Cursor {
9773            let s = &self.shell;
9774            let requested = s.runtime.input.hovered.and_then(|id| {
9775                let rect = s.ui.as_ref()?.widget_rect(id)?;
9776                let widget = find_widget(s.tree.as_ref()?.as_ref(), id)?;
9777                widget.cursor_icon(
9778                    s.cursor.x - rect.x,
9779                    s.cursor.y - rect.y,
9780                    rect.width,
9781                    rect.height,
9782                )
9783            });
9784            s.region_cursor(requested)
9785        }
9786
9787        /// When the loop would wake next if it went idle now, if it would.
9788        pub fn next_wake(&self) -> Option<Instant> {
9789            match self.shell.idle_control_flow() {
9790                ControlFlow::WaitUntil(at) => Some(at),
9791                _ => None,
9792            }
9793        }
9794
9795        /// A click of the secondary mouse button at `at`.
9796        pub fn secondary_click(&mut self, at: Point) {
9797            self.pointer_of(PointerKind::Move, at, false);
9798            self.shell.context_click();
9799        }
9800
9801        /// A mouse button down at `at` — a pointer that hovers, which is not a finger: nothing is
9802        /// left to scroll, and a press on words in a selection area begins a selection.
9803        pub fn press_mouse(&mut self, at: Point) {
9804            self.pointer_of(PointerKind::Down, at, false);
9805        }
9806
9807        /// The mouse moved to `at`, its button down.
9808        pub fn move_mouse(&mut self, at: Point) {
9809            self.pointer_of(PointerKind::Move, at, false);
9810        }
9811
9812        /// The mouse button lifted at `at`.
9813        pub fn release_mouse(&mut self, at: Point) {
9814            self.pointer_of(PointerKind::Up, at, false);
9815        }
9816
9817        /// The words a selection area has selected, as Copy would put them on the clipboard.
9818        pub fn area_selection(&self) -> Option<String> {
9819            self.shell.region_text()
9820        }
9821
9822        /// Select all, as the keyboard asks it of a selection area that holds a selection.
9823        pub fn area_select_all(&mut self) {
9824            self.shell.select_all_region();
9825        }
9826
9827        /// Escape, as the keyboard delivers it.
9828        pub fn escape(&mut self) {
9829            self.shell.escape();
9830        }
9831
9832        /// **A key pressed**, with the modifiers the stroke names held down for it, through
9833        /// the shell's own handling of a key (milestone 608): the arrows move the focus or a
9834        /// caret or answer the widget with the focus, Enter and Space activate, Escape
9835        /// closes, Tab moves on, a bound shortcut fires, a character is typed.
9836        ///
9837        /// The system's back key is not one of them: what it does last is quit, and a
9838        /// driver has no loop to quit.
9839        pub fn key(&mut self, stroke: frus_widgets::KeyStroke) {
9840            use frus_widgets::ShortcutKey as K;
9841            let named = |key| (WinitKey::Named(key), None);
9842            let (logical_key, text) = match stroke.key {
9843                K::Char(c) => {
9844                    let typed: winit::keyboard::SmolStr = c.to_string().into();
9845                    (WinitKey::Character(typed.clone()), Some(typed))
9846                }
9847                K::Enter => named(NamedKey::Enter),
9848                K::Escape => named(NamedKey::Escape),
9849                K::Tab => named(NamedKey::Tab),
9850                K::Space => (WinitKey::Named(NamedKey::Space), Some(" ".into())),
9851                K::Backspace => named(NamedKey::Backspace),
9852                K::Delete => named(NamedKey::Delete),
9853                K::Up => named(NamedKey::ArrowUp),
9854                K::Down => named(NamedKey::ArrowDown),
9855                K::Left => named(NamedKey::ArrowLeft),
9856                K::Right => named(NamedKey::ArrowRight),
9857                K::Home => named(NamedKey::Home),
9858                K::End => named(NamedKey::End),
9859                K::PageUp => named(NamedKey::PageUp),
9860                K::PageDown => named(NamedKey::PageDown),
9861                K::F(n) => named(match n {
9862                    1 => NamedKey::F1,
9863                    2 => NamedKey::F2,
9864                    3 => NamedKey::F3,
9865                    4 => NamedKey::F4,
9866                    5 => NamedKey::F5,
9867                    6 => NamedKey::F6,
9868                    7 => NamedKey::F7,
9869                    8 => NamedKey::F8,
9870                    9 => NamedKey::F9,
9871                    10 => NamedKey::F10,
9872                    11 => NamedKey::F11,
9873                    _ => NamedKey::F12,
9874                }),
9875            };
9876            // Where a letter is on a QWERTY keyboard: the clipboard's shortcuts are read off
9877            // the key's place, so that they work on any layout.
9878            const LETTERS: [KeyCode; 26] = [
9879                KeyCode::KeyA,
9880                KeyCode::KeyB,
9881                KeyCode::KeyC,
9882                KeyCode::KeyD,
9883                KeyCode::KeyE,
9884                KeyCode::KeyF,
9885                KeyCode::KeyG,
9886                KeyCode::KeyH,
9887                KeyCode::KeyI,
9888                KeyCode::KeyJ,
9889                KeyCode::KeyK,
9890                KeyCode::KeyL,
9891                KeyCode::KeyM,
9892                KeyCode::KeyN,
9893                KeyCode::KeyO,
9894                KeyCode::KeyP,
9895                KeyCode::KeyQ,
9896                KeyCode::KeyR,
9897                KeyCode::KeyS,
9898                KeyCode::KeyT,
9899                KeyCode::KeyU,
9900                KeyCode::KeyV,
9901                KeyCode::KeyW,
9902                KeyCode::KeyX,
9903                KeyCode::KeyY,
9904                KeyCode::KeyZ,
9905            ];
9906            let physical_key = match stroke.key {
9907                K::Char(c) if c.is_ascii_alphabetic() => {
9908                    PhysicalKey::Code(LETTERS[(c.to_ascii_lowercase() as u8 - b'a') as usize])
9909                }
9910                _ => PhysicalKey::Unidentified(winit::keyboard::NativeKeyCode::Unidentified),
9911            };
9912            let held = (
9913                self.shell.ctrl,
9914                self.shell.shift,
9915                self.shell.alt,
9916                self.shell.meta,
9917            );
9918            (
9919                self.shell.ctrl,
9920                self.shell.shift,
9921                self.shell.alt,
9922                self.shell.meta,
9923            ) = (stroke.ctrl, stroke.shift, stroke.alt, stroke.meta);
9924            self.shell.key_down(
9925                &KeyDown {
9926                    logical_key,
9927                    physical_key,
9928                    text,
9929                    repeat: false,
9930                },
9931                None,
9932            );
9933            (
9934                self.shell.ctrl,
9935                self.shell.shift,
9936                self.shell.alt,
9937                self.shell.meta,
9938            ) = held;
9939        }
9940
9941        /// The two handles of a selection area's touch selection, on the surface, if it has them.
9942        pub fn area_handles(&self) -> Option<[frus_widgets::SelectionHandle; 2]> {
9943            self.shell.region_handles().map(|(_, handles)| handles)
9944        }
9945
9946        fn pointer(&mut self, kind: PointerKind, at: Point) {
9947            self.pointer_of(kind, at, true);
9948        }
9949
9950        fn pointer_of(&mut self, kind: PointerKind, at: Point, touch: bool) {
9951            self.shell.pointer_event(PointerEvent {
9952                kind,
9953                position: at,
9954                touch,
9955            });
9956        }
9957
9958        /// One frame, the real frame's steps in the real frame's order, less the GPU.
9959        pub fn frame(&mut self, dt: f32) {
9960            let (width, height) = (self.size.width, self.size.height);
9961            let s = &mut self.shell;
9962            if let Some((now, _)) = s.gesture_clock.as_mut() {
9963                *now += dt;
9964            }
9965            if s.last_size != Some((width, height)) {
9966                s.last_size = Some((width, height));
9967                s.build_dirty = true;
9968                s.app.on_resize(width, height);
9969            }
9970            let mut app_animating = s.app.tick(dt);
9971            let settings = s
9972                .platform
9973                .accessibility
9974                .with_overrides(s.app.accessibility());
9975            let theme_moved = s.themes.advance(
9976                &s.app,
9977                s.platform.brightness,
9978                settings.high_contrast,
9979                settings.disable_animations,
9980                dt,
9981            );
9982            app_animating |= theme_moved | s.themes.animating();
9983            let theme = s.themes.displayed(&s.app);
9984            install_ambient(&s.app, &s.platform.locales);
9985            let _surface = s.media_query(width, height).install();
9986            s.runtime.still = settings.disable_animations;
9987            let was = std::mem::replace(&mut s.app_was_animating, app_animating);
9988            let asked = s.app.effects();
9989            s.run_command(asked);
9990            let arrived = s.take_task_arrivals();
9991            if frus_widgets::take_rebuild_request() || arrived {
9992                s.build_dirty = true;
9993            }
9994            if frame_needs_build(
9995                s.build_dirty,
9996                app_animating,
9997                was,
9998                s.tree.is_none() || s.runtime.switching(),
9999            ) {
10000                s.tree = Some(build_view(&s.app, &theme, &s.runtime));
10001            }
10002            s.build_dirty = false;
10003            s.autoscroll_carried(dt);
10004            s.advance_reorder_springs(dt);
10005            let regions =
10006                s.ui.as_ref()
10007                    .map(|ui| ui.scroll_regions().to_vec())
10008                    .unwrap_or_default();
10009            s.runtime.sync_pages(&regions);
10010            s.runtime.sync_visible(&regions);
10011            s.runtime.advance(dt);
10012            s.runtime.advance_scroll(&regions, dt);
10013            let tree = s.tree.as_deref().expect("the view was built");
10014            // The widgets' own values, as the window's frame steps them: without it a value
10015            // a drag held would be held for ever here (milestone 638).
10016            s.runtime.advance_values(tree, dt);
10017            let ui = build_ui(tree, Size::new(width, height), &s.runtime, &theme);
10018            s.runtime.states.end_frame();
10019            let mut scene = ui.scene().clone();
10020            let ghost = if matches!(s.drag, Some(Drag::Reorder { moved: true, .. })) {
10021                s.paint_reorder_preview(&ui, &theme, &mut scene);
10022                // The ghost's outline is the last thing the preview draws, on the ghost's box.
10023                scene.primitives().last().map(|p| p.bounds())
10024            } else {
10025                None
10026            };
10027            let paged = ui.scroll_regions().to_vec();
10028            let scrolled: Vec<A::Message> = {
10029                let grain = |id| find_widget(tree, id).map_or(0.0, |w| w.scroll_grain());
10030                s.runtime
10031                    .scroll_changes(&paged, grain)
10032                    .into_iter()
10033                    .filter_map(|(id, position)| {
10034                        find_widget(tree, id).and_then(|w| w.on_scroll(position))
10035                    })
10036                    .collect()
10037            };
10038            s.ui = Some(ui);
10039            s.sync_regions(false);
10040            // As the shell does after every frame: focus that vanished goes back, and a
10041            // widget that takes the focus on appearing takes it (milestone 605).
10042            s.reconcile_focus();
10043            for message in scrolled {
10044                s.dispatch(message);
10045            }
10046            self.ghost = ghost;
10047        }
10048    }
10049}
10050
10051/// **What is carried stays carried, however far its list scrolls** (milestone 527, seen on
10052/// a phone).
10053///
10054/// A label held on the Kanban screen's strip and carried towards the strip's right edge was
10055/// not drawn lifted, the strip scrolled away under it, and the release put it somewhere the
10056/// finger had never been. The carried row's box came from the frame, and the frame keeps a
10057/// reorderable's box only while it is on screen, clipped to what shows: once auto-scroll had
10058/// pushed the row against the edge the box stopped moving with the content while the press
10059/// kept moving, the ghost ran ahead of the finger, the scroll fed on it, and a row scrolled
10060/// out of sight had no box at all.
10061///
10062/// These drive the shell's own input path and its frame, on the screen's shape: a bar, a strip
10063/// of ten 96-px labels that lift on a hold, and a board of cards, each in a horizontal scroll.
10064#[cfg(test)]
10065mod carried_row_tests {
10066    use super::testing::Driver;
10067    use crate::{Application, Command};
10068    use frus_widgets::{
10069        column, text, Axis, Button, CellFn, Container, Flex, Kanban, Point, Rect, ReorderAxis,
10070        ReorderGrab, ReorderableList, SingleChildScrollView, Theme, Widget,
10071    };
10072
10073    /// A phone, in logical pixels.
10074    const W: f32 = 392.7;
10075    const H: f32 = 850.9;
10076    /// The height of the labels' middle, under the 56-px bar and the strip's 12 px of padding.
10077    const STRIP_Y: f32 = 88.0;
10078    /// How wide the strip's content is: ten labels, nine gaps, and 24 px at either end.
10079    const STRIP_CONTENT: f32 = 24.0 + 10.0 * 96.0 + 9.0 * 8.0 + 24.0;
10080
10081    #[derive(Clone, Debug, PartialEq)]
10082    enum Board {
10083        Label(usize, usize),
10084        Card(usize, usize, usize, usize),
10085        Delete(usize, usize),
10086        Add(usize),
10087        Row(usize, usize),
10088    }
10089
10090    /// The Kanban screen, in the shape that matters.
10091    struct BoardApp {
10092        labels: Vec<usize>,
10093        cards: Vec<Vec<String>>,
10094        log: Vec<Board>,
10095    }
10096
10097    impl Default for BoardApp {
10098        fn default() -> Self {
10099            let cols: [&[&str]; 3] = [
10100                &["Design API", "Write spec", "Triage bugs"],
10101                &["Build widget"],
10102                &["Kickoff", "Research"],
10103            ];
10104            Self {
10105                labels: (0..10).collect(),
10106                cards: cols
10107                    .iter()
10108                    .map(|c| c.iter().map(|s| s.to_string()).collect())
10109                    .collect(),
10110                log: Vec::new(),
10111            }
10112        }
10113    }
10114
10115    impl Application for BoardApp {
10116        type Message = Board;
10117
10118        fn update(&mut self, message: Board) -> Command<Board> {
10119            self.log.push(message.clone());
10120            match message {
10121                Board::Label(from, to) => {
10122                    let label = self.labels.remove(from);
10123                    self.labels.insert(to, label);
10124                }
10125                Board::Card(from_col, from_pos, to_col, to_pos) => {
10126                    let card = self.cards[from_col].remove(from_pos);
10127                    let to_pos = to_pos.min(self.cards[to_col].len());
10128                    self.cards[to_col].insert(to_pos, card);
10129                }
10130                _ => {}
10131            }
10132            Command::none()
10133        }
10134
10135        fn view(&self, theme: &Theme) -> Box<dyn Widget<Board>> {
10136            let mut strip = ReorderableList::new(Board::Label)
10137                .axis(ReorderAxis::Horizontal)
10138                .grab(ReorderGrab::LongPress)
10139                .gap(8.0);
10140            for &label in &self.labels {
10141                strip = strip.keyed_row(
10142                    label as u64,
10143                    Container::new()
10144                        .width(96.0)
10145                        .padding(12.0)
10146                        .color(theme.surface)
10147                        .child(text(format!("label {label}")).size(14.0)),
10148                );
10149            }
10150            let strip = SingleChildScrollView::new()
10151                .axis(Axis::Horizontal)
10152                .width(W)
10153                .child(
10154                    Container::new()
10155                        .padding_each(12.0, 24.0, 0.0, 24.0)
10156                        .child(strip),
10157                );
10158            let mut board = Kanban::new(Board::Card)
10159                .on_add(Board::Add)
10160                .scrollable_columns();
10161            for (col, title) in ["To do", "Doing", "Done"].iter().enumerate() {
10162                let cards: Vec<CellFn<Board>> = self.cards[col]
10163                    .iter()
10164                    .enumerate()
10165                    .map(|(pos, label)| {
10166                        let label = label.clone();
10167                        Box::new(move || {
10168                            Box::new(
10169                                Flex::row()
10170                                    .child(text(label.clone()).size(14.0))
10171                                    .child(Flex::row().flex(1.0))
10172                                    .child(Button::new("x").on_press(Board::Delete(col, pos))),
10173                            ) as Box<dyn Widget<Board>>
10174                        }) as CellFn<Board>
10175                    })
10176                    .collect();
10177                board = board.column_widgets(*title, cards);
10178            }
10179            let board = SingleChildScrollView::new()
10180                .axis(Axis::Horizontal)
10181                .width(W)
10182                .flex(1.0)
10183                .child(Container::new().padding(24.0).child(board));
10184            Box::new(
10185                Container::new().width(W).height(H).child(
10186                    column![
10187                        Container::<Board>::new().width(W).height(56.0),
10188                        strip,
10189                        board
10190                    ]
10191                    .flex(1.0),
10192                ),
10193            )
10194        }
10195    }
10196
10197    /// Twenty 60-px rows that lift on a hold, in a list taller than the window.
10198    #[derive(Default)]
10199    struct ListApp {
10200        log: Vec<Board>,
10201    }
10202
10203    impl Application for ListApp {
10204        type Message = Board;
10205
10206        fn update(&mut self, message: Board) -> Command<Board> {
10207            self.log.push(message);
10208            Command::none()
10209        }
10210
10211        fn view(&self, _theme: &Theme) -> Box<dyn Widget<Board>> {
10212            let mut list = ReorderableList::new(Board::Row)
10213                .grab(ReorderGrab::LongPress)
10214                .width(W);
10215            for row in 0..20u64 {
10216                list = list.keyed_row(
10217                    row,
10218                    Container::new()
10219                        .height(60.0)
10220                        .child(text(format!("row {row}")).size(14.0)),
10221                );
10222            }
10223            Box::new(
10224                SingleChildScrollView::new()
10225                    .axis(Axis::Vertical)
10226                    .width(W)
10227                    .height(H)
10228                    .child(list),
10229            )
10230        }
10231    }
10232
10233    fn driver<A: Application>(app: A) -> Driver<A> {
10234        let mut driver = Driver::new(app, W, H);
10235        driver.run(0.2);
10236        driver
10237    }
10238
10239    /// A finger held still at `at` until the long press has lifted what is under it.
10240    fn hold<A: Application>(driver: &mut Driver<A>, at: Point) {
10241        driver.press(at);
10242        driver.run(0.5);
10243        assert!(
10244            driver.hold_deadline(),
10245            "the hold lifts what is under {at:?}"
10246        );
10247        driver.run(0.05);
10248    }
10249
10250    /// The finger through `points`, a tenth of a second apart, as the phone's were.
10251    fn carry<A: Application>(driver: &mut Driver<A>, points: impl IntoIterator<Item = Point>) {
10252        for at in points {
10253            driver.move_to(at);
10254            driver.run(0.1);
10255        }
10256    }
10257
10258    /// Frame by frame for `seconds` with the finger still at `finger`: the carried box keeps
10259    /// the place it had under the finger — `offset` from it and `size` — and the region
10260    /// `region` keeps scrolling the way it started. Hands back that region's last offset.
10261    fn held_under_the_finger<A: Application>(
10262        driver: &mut Driver<A>,
10263        seconds: f32,
10264        finger: Point,
10265        offset: (f32, f32),
10266        size: (f32, f32),
10267        region: usize,
10268    ) -> (f32, f32) {
10269        let mut last = driver.offsets()[region].1;
10270        for frame in 0..(seconds * 60.0) as usize {
10271            driver.run(1.0 / 60.0);
10272            let now = driver.offsets()[region].1;
10273            let carried: Rect = driver.carried().unwrap_or_else(|| {
10274                panic!("frame {frame}: nothing is carried any more, the list at {now:?}")
10275            });
10276            assert!(
10277                (carried.x - finger.x - offset.0).abs() < 0.5
10278                    && (carried.y - finger.y - offset.1).abs() < 0.5
10279                    && (carried.width - size.0).abs() < 0.5
10280                    && (carried.height - size.1).abs() < 0.5,
10281                "frame {frame}: carried at {carried:?}, the finger at {finger:?}, the list at {now:?}"
10282            );
10283            assert!(
10284                now.0 >= last.0 - 0.01 && now.1 >= last.1 - 0.01,
10285                "frame {frame}: the list went back, {last:?} then {now:?}"
10286            );
10287            // And it is drawn there: the ghost the finger sees is the box being carried.
10288            let ghost = driver
10289                .ghost()
10290                .unwrap_or_else(|| panic!("frame {frame}: no ghost is drawn"));
10291            assert!(
10292                (ghost.x - carried.x).abs() < 1.0
10293                    && (ghost.y - carried.y).abs() < 1.0
10294                    && (ghost.width - carried.width).abs() < 2.0,
10295                "frame {frame}: the ghost is drawn at {ghost:?}, what is carried is at {carried:?}"
10296            );
10297            last = now;
10298        }
10299        last
10300    }
10301
10302    /// **The reproduction.** A label held and carried past the strip's right edge stays under
10303    /// the finger for as long as it is held there, whole, while the strip scrolls on to its
10304    /// end; and the release drops it where the finger is, past the last label.
10305    #[test]
10306    fn a_label_carried_past_the_strips_edge_stays_under_the_finger() {
10307        let mut driver = driver(BoardApp::default());
10308        hold(&mut driver, Point::new(155.0, STRIP_Y));
10309        let lifted = driver.carried().expect("the label is carried");
10310        assert_eq!(
10311            lifted,
10312            Rect::new(128.0, 68.0, 96.0, 41.0),
10313            "Feature, lifted"
10314        );
10315        let finger = Point::new(370.0, STRIP_Y);
10316        carry(
10317            &mut driver,
10318            [200.0, 260.0, 320.0, finger.x].map(|x| Point::new(x, STRIP_Y)),
10319        );
10320        let strip = held_under_the_finger(
10321            &mut driver,
10322            1.5,
10323            finger,
10324            (lifted.x - 155.0, lifted.y - STRIP_Y),
10325            (96.0, 41.0),
10326            0,
10327        );
10328        assert!(
10329            (strip.0 - (STRIP_CONTENT - W)).abs() < 1.0,
10330            "the strip scrolled on to its end: {strip:?}"
10331        );
10332        driver.release(finger);
10333        driver.run(0.2);
10334        assert_eq!(
10335            driver.app().log,
10336            [Board::Label(1, 9)],
10337            "past the last label, it goes last; no card moves"
10338        );
10339    }
10340
10341    /// **The phone's own gesture**, where the label never reaches an edge: held on Feature,
10342    /// carried along x as the injected moves went, let go over Design's right half.
10343    #[test]
10344    fn a_hold_on_a_label_then_a_carry_along_x_reorders_the_labels_and_moves_no_card() {
10345        let mut driver = driver(BoardApp::default());
10346        hold(&mut driver, Point::new(155.0, STRIP_Y));
10347        carry(
10348            &mut driver,
10349            [167.0, 189.0, 218.0, 247.0, 276.0, 306.0].map(|x| Point::new(x, STRIP_Y)),
10350        );
10351        driver.run(0.4);
10352        assert!(driver.carried().is_some(), "still carried");
10353        driver.release(Point::new(306.0, STRIP_Y));
10354        driver.run(0.2);
10355        assert_eq!(driver.app().log, [Board::Label(1, 2)]);
10356        assert_eq!(driver.app().labels[..4], [0, 2, 1, 3]);
10357        assert_eq!(
10358            driver.app().cards,
10359            BoardApp::default().cards,
10360            "no card moved"
10361        );
10362        assert_eq!(driver.offsets()[0].1, (0.0, 0.0), "nothing scrolled");
10363        assert_eq!(driver.offsets()[1].1, (0.0, 0.0), "the board neither");
10364    }
10365
10366    /// **A card still crosses the board**: Design API, pressed and carried onto the upper
10367    /// half of Build widget, goes to the top of Doing; no label moves.
10368    #[test]
10369    fn a_card_carried_across_the_board_moves_the_card() {
10370        let mut driver = driver(BoardApp::default());
10371        driver.press(Point::new(100.0, 190.0));
10372        carry(
10373            &mut driver,
10374            [130.0, 180.0, 240.0, 300.0].map(|x| Point::new(x, 185.0)),
10375        );
10376        driver.release(Point::new(300.0, 185.0));
10377        driver.run(0.2);
10378        assert_eq!(driver.app().log, [Board::Card(0, 0, 1, 0)]);
10379        assert_eq!(driver.app().labels, (0..10).collect::<Vec<_>>());
10380    }
10381
10382    /// **The board has the same edge.** A card carried to the board's right edge scrolls
10383    /// the board and stays under the finger, although the column it came from scrolls away.
10384    #[test]
10385    fn a_card_carried_to_the_boards_edge_stays_under_the_finger() {
10386        let mut driver = driver(BoardApp::default());
10387        let press = Point::new(100.0, 190.0);
10388        driver.press(press);
10389        let finger = Point::new(385.0, 190.0);
10390        carry(
10391            &mut driver,
10392            [130.0, 200.0, 280.0, 340.0, finger.x].map(|x| Point::new(x, 190.0)),
10393        );
10394        let card = driver.carried().expect("the card is carried");
10395        let board = held_under_the_finger(
10396            &mut driver,
10397            1.0,
10398            finger,
10399            (card.x - finger.x, card.y - finger.y),
10400            (card.width, card.height),
10401            1,
10402        );
10403        assert!(
10404            board.0 > 250.0,
10405            "the board scrolled past the column the card came from: {board:?}"
10406        );
10407    }
10408
10409    /// **A list that runs down has the same edge**: row 0 held and carried to the bottom
10410    /// stays under the finger after its own place has scrolled off the top, and lands low in
10411    /// the list.
10412    #[test]
10413    fn a_row_carried_past_the_bottom_of_a_long_list_stays_under_the_finger() {
10414        let mut driver = driver(ListApp::default());
10415        hold(&mut driver, Point::new(100.0, 30.0));
10416        let lifted = driver.carried().expect("the row is carried");
10417        let finger = Point::new(100.0, 840.0);
10418        carry(
10419            &mut driver,
10420            [200.0, 400.0, 600.0, 800.0, finger.y].map(|y| Point::new(100.0, y)),
10421        );
10422        let list = held_under_the_finger(
10423            &mut driver,
10424            1.5,
10425            finger,
10426            (lifted.x - 100.0, lifted.y - 30.0),
10427            (lifted.width, lifted.height),
10428            0,
10429        );
10430        assert!(
10431            list.1 > 200.0,
10432            "the list scrolled past the row's own place: {list:?}"
10433        );
10434        driver.release(finger);
10435        driver.run(0.2);
10436        assert!(
10437            matches!(driver.app().log[..], [Board::Row(0, to)] if to > 15),
10438            "row 0 lands near the end: {:?}",
10439            driver.app().log
10440        );
10441    }
10442}
10443
10444/// **A press a scroll took is let go of** (milestone 534).
10445///
10446/// The shell's own types need a window and an event loop, so these drive a real frame
10447/// through the pieces `App::pointer` is made of, in the order it calls them: the hover, the
10448/// press where the pointer lands, the gesture the drag handler turns the press into, and
10449/// `settle_press` after every event. What is painted is then compared with a runtime
10450/// nobody pressed.
10451#[cfg(test)]
10452mod press_tests {
10453    use super::{gesture_was_a_tap, press_at, settle_press, Drag, Point, Runtime, Theme};
10454    use crate::gesture::PointerKind;
10455    use crate::hover::Hover;
10456    use frus_widgets::{
10457        build_ui, Axis, Button, Container, Flex, Primitive, SegmentedButton, SingleChildScrollView,
10458        Size, Ui, Widget,
10459    };
10460
10461    const SCREEN: Size = Size::new(300.0, 400.0);
10462    const DT: f32 = 1.0 / 60.0;
10463
10464    /// The phone's screen: a strip of segments too wide for it, in a horizontal scroll.
10465    fn filters() -> Box<dyn Widget<usize>> {
10466        let mut segments = SegmentedButton::new(0, |i| i);
10467        for label in ["All", "Active", "Done", "Archived", "Starred", "Shared"] {
10468            segments = segments.segment(label);
10469        }
10470        Box::new(
10471            Flex::column().width(300.0).height(400.0).child(
10472                SingleChildScrollView::new()
10473                    .axis(Axis::Horizontal)
10474                    .width(300.0)
10475                    .height(80.0)
10476                    .child(segments),
10477            ),
10478        )
10479    }
10480
10481    /// A plain button at the top of a page that scrolls down.
10482    fn page() -> Box<dyn Widget<usize>> {
10483        Box::new(
10484            Flex::column().width(300.0).height(400.0).child(
10485                SingleChildScrollView::new()
10486                    .width(300.0)
10487                    .height(400.0)
10488                    .child(
10489                        Flex::column()
10490                            .child(Button::new("Go").on_press(7usize))
10491                            .child(Container::new().width(300.0).height(1200.0)),
10492                    ),
10493            ),
10494        )
10495    }
10496
10497    fn frame(view: &dyn Widget<usize>, runtime: &Runtime) -> Ui<usize> {
10498        build_ui(view, SCREEN, runtime, &Theme::default())
10499    }
10500
10501    /// Frames go by with nothing else happening, as a phone left alone for `seconds`.
10502    fn idle(runtime: &mut Runtime, seconds: f32) {
10503        for _ in 0..(seconds / DT) as usize {
10504            runtime.advance(DT);
10505            runtime.advance_ink(DT);
10506        }
10507    }
10508
10509    /// What the screen shows.
10510    fn painted(view: &dyn Widget<usize>, runtime: &Runtime) -> Vec<Primitive> {
10511        frame(view, runtime).scene().primitives().to_vec()
10512    }
10513
10514    /// The same screen, scrolled the same, that no pointer has touched.
10515    fn untouched(view: &dyn Widget<usize>, runtime: &Runtime) -> Vec<Primitive> {
10516        let mut quiet = Runtime::default();
10517        quiet.scroll.clone_from(&runtime.scroll);
10518        quiet.scroll_target.clone_from(&runtime.scroll_target);
10519        idle(&mut quiet, 0.5);
10520        painted(view, &quiet)
10521    }
10522
10523    /// A pointer on a screen, routed as the shell routes one.
10524    struct Pointer<'a> {
10525        view: &'a dyn Widget<usize>,
10526        runtime: Runtime,
10527        hover: Hover,
10528        drag: Option<Drag>,
10529        touch: bool,
10530        at: Point,
10531    }
10532
10533    impl<'a> Pointer<'a> {
10534        fn new(view: &'a dyn Widget<usize>, touch: bool) -> Self {
10535            Self {
10536                view,
10537                runtime: Runtime::default(),
10538                hover: Hover::default(),
10539                drag: None,
10540                touch,
10541                at: Point::new(0.0, 0.0),
10542            }
10543        }
10544
10545        /// One event: `route` stands for what the shell's handler makes of it.
10546        fn event(&mut self, kind: PointerKind, at: Point, route: impl FnOnce(&mut Self)) {
10547            self.at = at;
10548            self.hover.event(kind, at, self.touch);
10549            let could_tap = gesture_was_a_tap(self.drag.as_ref());
10550            if kind == PointerKind::Down {
10551                let ui = frame(self.view, &self.runtime);
10552                self.runtime.input.hovered = self.hover.target(&ui);
10553                press_at(&mut self.runtime, Some(&ui), at);
10554            }
10555            route(self);
10556            settle_press(
10557                &mut self.runtime.input,
10558                kind,
10559                could_tap,
10560                self.drag.as_ref(),
10561                self.touch,
10562            );
10563            if matches!(kind, PointerKind::Up | PointerKind::Cancel) {
10564                let ui = frame(self.view, &self.runtime);
10565                self.runtime.input.hovered = self.hover.target(&ui);
10566            }
10567        }
10568
10569        /// A finger lands where a touch scroll takes it, as `pointer_down` prepares one.
10570        fn land(&mut self, at: Point) {
10571            self.event(PointerKind::Down, at, |p| {
10572                if !p.touch {
10573                    return;
10574                }
10575                let ui = frame(p.view, &p.runtime);
10576                let area = ui
10577                    .scroll_chain(at)
10578                    .find(|area| area.accepts_user_offset((0.0, 0.0)))
10579                    .expect("the region under the finger has somewhere to go");
10580                p.drag = Some(Drag::Scroll {
10581                    id: area.id,
10582                    last: at,
10583                    moved: false,
10584                    carried: (0.0, 0.0),
10585                    dismiss: None,
10586                    axis: None,
10587                    pan: None,
10588                });
10589            });
10590        }
10591
10592        /// The pointer moves by `by`; past the slop a touch scroll moves its content, as the
10593        /// drag handler moves it.
10594        fn slide(&mut self, by: (f32, f32)) {
10595            let to = Point::new(self.at.x + by.0, self.at.y + by.1);
10596            self.event(PointerKind::Move, to, |p| {
10597                let Some(Drag::Scroll {
10598                    id,
10599                    moved,
10600                    axis,
10601                    last,
10602                    ..
10603                }) = p.drag.as_mut()
10604                else {
10605                    return;
10606                };
10607                let (dx, dy) = (to.x - last.x, to.y - last.y);
10608                if dx.hypot(dy) <= super::TOUCH_SLOP && !*moved {
10609                    return;
10610                }
10611                *moved = true;
10612                *axis = Some(dy.abs() >= dx.abs());
10613                *last = to;
10614                let offset = p.runtime.scroll.get(id).copied().unwrap_or((0.0, 0.0));
10615                let moved_to = (offset.0 - by.0, offset.1 - by.1);
10616                p.runtime.scroll.insert(*id, moved_to);
10617                p.runtime.scroll_target.insert(*id, moved_to);
10618            });
10619        }
10620
10621        /// The pointer lifts; the release takes the gesture, as `pointer_up` does.
10622        fn lift(&mut self) -> Option<Drag> {
10623            let mut ended = None;
10624            self.event(PointerKind::Up, self.at, |p| ended = p.drag.take());
10625            ended
10626        }
10627    }
10628
10629    /// **Seen on a phone**: a swipe that started on "Done" scrolled the strip, selected
10630    /// nothing — and left "Done" grey, still there two seconds later. Afterwards the screen
10631    /// is the one nobody touched.
10632    #[test]
10633    fn a_segment_a_swipe_started_on_is_not_left_pressed() {
10634        let view = filters();
10635        let mut finger = Pointer::new(view.as_ref(), true);
10636        let done = Point::new(200.0, 16.0);
10637        finger.land(done);
10638        let segment = finger
10639            .runtime
10640            .input
10641            .pressed
10642            .expect("the segment is pressed");
10643        assert!(
10644            finger.runtime.ink.contains_key(&segment),
10645            "and it splashed: a segment takes ink"
10646        );
10647        assert!(
10648            frame(view.as_ref(), &finger.runtime)
10649                .scroll_chain(done)
10650                .any(|area| area.max_x > 0.0),
10651            "the strip is wider than the screen, as the phone's was"
10652        );
10653        for _ in 0..19 {
10654            finger.slide((-10.0, 0.0));
10655        }
10656        let ended = finger.lift();
10657        assert!(
10658            !gesture_was_a_tap(ended.as_ref()),
10659            "a gesture that scrolled clicks nothing"
10660        );
10661        idle(&mut finger.runtime, 2.0);
10662        // What the phone showed first, then why.
10663        let (shown, quiet) = (
10664            painted(view.as_ref(), &finger.runtime),
10665            untouched(view.as_ref(), &finger.runtime),
10666        );
10667        assert_eq!(
10668            shown.len(),
10669            quiet.len(),
10670            "two seconds on, the screen paints what nobody touched, and no layer more"
10671        );
10672        assert_eq!(shown, quiet, "nothing is highlighted");
10673        assert!(
10674            finger.runtime.ink.is_empty(),
10675            "no ink waits for a finger that has gone"
10676        );
10677        assert_eq!(finger.runtime.input.pressed, None, "the press went with it");
10678        assert_eq!(
10679            finger.runtime.input.hovered, None,
10680            "and a lifted finger hovers nothing"
10681        );
10682    }
10683
10684    /// The reference cancels the press **when the scroll wins**, not when the finger lifts:
10685    /// a strip being scrolled does not carry a pressed segment along with it.
10686    #[test]
10687    fn the_press_ends_when_the_scroll_takes_it() {
10688        let view = filters();
10689        let mut finger = Pointer::new(view.as_ref(), true);
10690        finger.land(Point::new(200.0, 16.0));
10691        finger.slide((-4.0, 0.0));
10692        assert!(
10693            finger.runtime.input.pressed.is_some(),
10694            "under the slop it may still be a tap"
10695        );
10696        finger.slide((-40.0, 0.0));
10697        assert_eq!(
10698            finger.runtime.input.pressed, None,
10699            "past it, the scroll has it"
10700        );
10701        assert_eq!(
10702            finger.runtime.input.hovered, None,
10703            "and the finger's hover with it"
10704        );
10705        idle(&mut finger.runtime, 0.5);
10706        assert_eq!(
10707            painted(view.as_ref(), &finger.runtime),
10708            untouched(view.as_ref(), &finger.runtime),
10709            "the finger is still down, and nothing under it is highlighted"
10710        );
10711    }
10712
10713    /// The same down a page: a plain button a scroll started on.
10714    #[test]
10715    fn a_button_a_page_scrolled_from_is_not_left_pressed() {
10716        let view = page();
10717        let mut finger = Pointer::new(view.as_ref(), true);
10718        let go = Point::new(20.0, 15.0);
10719        finger.land(go);
10720        let button = finger.runtime.input.pressed.expect("the button is pressed");
10721        assert_eq!(
10722            frame(view.as_ref(), &finger.runtime).msg_for(button),
10723            Some(7),
10724            "the press is on the button"
10725        );
10726        for _ in 0..10 {
10727            finger.slide((0.0, -30.0));
10728        }
10729        assert!(!gesture_was_a_tap(finger.lift().as_ref()));
10730        idle(&mut finger.runtime, 2.0);
10731        assert_eq!(finger.runtime.input.pressed, None);
10732        assert!(finger.runtime.ink.is_empty());
10733        assert_eq!(
10734            painted(view.as_ref(), &finger.runtime),
10735            untouched(view.as_ref(), &finger.runtime),
10736        );
10737    }
10738
10739    /// **A tap is still a tap**: a finger that stays inside the slop keeps its press to the
10740    /// release, which is what clicks; only then is it let go of.
10741    #[test]
10742    fn a_tap_still_presses_and_clicks() {
10743        let view = filters();
10744        let mut finger = Pointer::new(view.as_ref(), true);
10745        let segment = Point::new(200.0, 16.0);
10746        finger.land(segment);
10747        finger.slide((3.0, 2.0));
10748        let pressed = finger.runtime.input.pressed;
10749        assert!(pressed.is_some(), "a wobble under the slop takes nothing");
10750        assert_eq!(finger.runtime.input.hovered, pressed, "and the press shows");
10751        // What `pointer_up` reads, before the release lets it go.
10752        let ui = frame(view.as_ref(), &finger.runtime);
10753        assert_eq!(
10754            ui.hit(segment),
10755            pressed,
10756            "released on the widget it pressed"
10757        );
10758        assert!(
10759            pressed.and_then(|id| ui.msg_for(id)).is_some(),
10760            "and the segment it pressed has a choice to make"
10761        );
10762        let ended = finger.lift();
10763        assert!(
10764            gesture_was_a_tap(ended.as_ref()),
10765            "a scroll that never moved"
10766        );
10767        assert_eq!(
10768            finger.runtime.input.pressed, None,
10769            "let go of once released"
10770        );
10771    }
10772
10773    /// **A mouse is as it was**: it has no touch scroll, keeps its press while it moves, and
10774    /// still hovers where it is after its button comes up.
10775    #[test]
10776    fn a_mouse_keeps_its_press_while_it_moves_and_its_hover_after() {
10777        let view = filters();
10778        let mut mouse = Pointer::new(view.as_ref(), false);
10779        let done = Point::new(200.0, 16.0);
10780        mouse.land(done);
10781        assert!(mouse.drag.is_none(), "a mouse does not drag a scroll");
10782        let pressed = mouse.runtime.input.pressed.expect("pressed");
10783        mouse.slide((-60.0, 0.0));
10784        assert_eq!(mouse.runtime.input.pressed, Some(pressed), "still held");
10785        mouse.lift();
10786        assert_eq!(mouse.runtime.input.pressed, None);
10787        assert!(
10788            mouse.runtime.input.hovered.is_some(),
10789            "a released mouse is still over the strip"
10790        );
10791    }
10792
10793    /// A gesture a **mouse** moves past its slop takes the press as a finger's does — but
10794    /// not the hover, since the mouse is still there.
10795    #[test]
10796    fn a_gesture_takes_a_mouse_s_press_and_leaves_its_hover() {
10797        let id = frus_widgets::WidgetId::from_u64(3);
10798        let sheet = |moved| Drag::Sheet {
10799            id: frus_widgets::WidgetId::from_u64(9),
10800            last: Point::new(0.0, 0.0),
10801            moved,
10802            available: 800.0,
10803        };
10804        for touch in [false, true] {
10805            let mut input = frus_widgets::InputState {
10806                pressed: Some(id),
10807                hovered: Some(id),
10808                ..Default::default()
10809            };
10810            assert!(!settle_press(
10811                &mut input,
10812                PointerKind::Move,
10813                true,
10814                Some(&sheet(false)),
10815                touch
10816            ));
10817            assert_eq!(input.pressed, Some(id), "not yet past the slop");
10818            assert!(settle_press(
10819                &mut input,
10820                PointerKind::Move,
10821                true,
10822                Some(&sheet(true)),
10823                touch
10824            ));
10825            assert_eq!(input.pressed, None, "touch {touch}");
10826            assert_eq!(input.hovered, (!touch).then_some(id), "touch {touch}");
10827        }
10828        // A gesture that was never a tap — a slider, a selection — had no press to share,
10829        // and moving it lets go of nothing.
10830        let mut input = frus_widgets::InputState {
10831            pressed: Some(id),
10832            ..Default::default()
10833        };
10834        let select = Drag::TextSelect {
10835            id,
10836            rect: super::Rect::new(0.0, 0.0, 10.0, 10.0),
10837        };
10838        assert!(!settle_press(
10839            &mut input,
10840            PointerKind::Move,
10841            false,
10842            Some(&select),
10843            true
10844        ));
10845        assert_eq!(input.pressed, Some(id));
10846    }
10847}
10848
10849/// The mouse, the words and the selection a [`SelectionArea`](frus_widgets::SelectionArea) holds,
10850/// driven through the shell as a person drives it.
10851#[cfg(test)]
10852mod selection_area_tests {
10853    use super::testing::Driver;
10854    use crate::{Application, Command};
10855    use frus_widgets::{Button, Container, Flex, Point, SelectionArea, Text, Theme, Widget};
10856
10857    const W: f32 = 400.0;
10858    const H: f32 = 300.0;
10859
10860    /// A line outside the area, then an area of three lines with a button between two of them.
10861    #[derive(Default)]
10862    struct Doc {
10863        /// What the application says about the area's bar.
10864        bar: Bar,
10865        /// How many times the bar's own item was pressed.
10866        shared: std::rc::Rc<std::cell::Cell<u32>>,
10867        /// The theme's platform: Android unless a test says otherwise, because these are a
10868        /// finger's tests and Android's bar is the pill (milestone 618).
10869        platform: Option<frus_widgets::TargetPlatform>,
10870    }
10871
10872    /// The application's say over the area's bar (milestone 578).
10873    #[derive(Default, Clone, Copy)]
10874    enum Bar {
10875        /// Nothing: Copy and Select all.
10876        #[default]
10877        Default,
10878        /// Its own item, Share, after Copy, and no Select all.
10879        Share,
10880        /// No bar at all.
10881        Empty,
10882    }
10883
10884    impl Application for Doc {
10885        type Message = ();
10886
10887        fn update(&mut self, _message: ()) -> Command<()> {
10888            self.shared.set(self.shared.get() + 1);
10889            Command::none()
10890        }
10891
10892        fn theme(&self) -> Theme {
10893            Theme::default().with_platform(
10894                self.platform
10895                    .unwrap_or(frus_widgets::TargetPlatform::Android),
10896            )
10897        }
10898
10899        fn view(&self, _theme: &Theme) -> Box<dyn Widget<()>> {
10900            let area = SelectionArea::around(
10901                Flex::column()
10902                    .child(Text::new("first line of words").no_wrap())
10903                    .child(Text::new("second line").no_wrap())
10904                    .child(Button::new("Press").on_press(()))
10905                    .child(Text::new("third line ends here").no_wrap()),
10906            );
10907            let area = match self.bar {
10908                Bar::Default => area,
10909                Bar::Share => area.selection_toolbar(|_, items| {
10910                    let mut items: Vec<_> = items
10911                        .into_iter()
10912                        .filter(|item| item.label() == "Copy")
10913                        .collect();
10914                    items.push(frus_widgets::ToolbarItem::custom("Share", ()));
10915                    items
10916                }),
10917                Bar::Empty => area.selection_toolbar(|_, _| Vec::new()),
10918            };
10919            Box::new(
10920                Container::new().width(W).height(H).child(
10921                    Flex::column()
10922                        .child(Text::new("Outside the area").no_wrap())
10923                        .child(area),
10924                ),
10925            )
10926        }
10927    }
10928
10929    /// A row that answers a tap, with words in it, beside words that answer nothing.
10930    struct Tappable;
10931
10932    impl Application for Tappable {
10933        type Message = ();
10934
10935        fn update(&mut self, _message: ()) -> Command<()> {
10936            Command::none()
10937        }
10938
10939        fn view(&self, _theme: &Theme) -> Box<dyn Widget<()>> {
10940            Box::new(
10941                Container::new()
10942                    .width(W)
10943                    .height(H)
10944                    .child(SelectionArea::around(
10945                        Flex::column()
10946                            .child(
10947                                Container::new()
10948                                    .on_click(())
10949                                    .child(Text::new("tap this row").no_wrap()),
10950                            )
10951                            .child(Text::new("plain words here").no_wrap()),
10952                    )),
10953            )
10954        }
10955    }
10956
10957    /// **What answers a tap keeps it, even when it is not a focus stop**: words inside a row that
10958    /// takes a tap begin no selection, and words beside it do. (A button is a focus stop and is
10959    /// kept by that; a row that is only clickable is kept by nothing but this.)
10960    #[test]
10961    fn a_row_that_takes_a_tap_keeps_its_press() {
10962        let mut d = Driver::new(Tappable, W, H);
10963        d.run(0.2);
10964        let find = |d: &Driver<Tappable>, label: &str| {
10965            let (_, rect) = d
10966                .texts()
10967                .into_iter()
10968                .find(|(text, _)| text == label)
10969                .expect("on screen");
10970            Point::new(rect.x + 20.0, rect.y + 8.0)
10971        };
10972        let (tap, plain) = (find(&d, "tap this row"), find(&d, "plain words here"));
10973        let end = Point::new(plain.x + 60.0, plain.y);
10974        d.press_mouse(tap);
10975        d.run(0.05);
10976        d.move_mouse(end);
10977        d.run(0.05);
10978        d.release_mouse(end);
10979        d.run(0.05);
10980        assert_eq!(d.area_selection(), None, "the row's press stayed the row's");
10981
10982        d.press_mouse(plain);
10983        d.run(0.05);
10984        d.move_mouse(end);
10985        d.run(0.05);
10986        d.release_mouse(end);
10987        d.run(0.05);
10988        assert!(d.area_selection().is_some(), "plain words still select");
10989    }
10990
10991    fn driver() -> Driver<Doc> {
10992        driver_with(Bar::Default)
10993    }
10994
10995    fn driver_with(bar: Bar) -> Driver<Doc> {
10996        driver_on(bar, None)
10997    }
10998
10999    fn driver_on(bar: Bar, platform: Option<frus_widgets::TargetPlatform>) -> Driver<Doc> {
11000        let doc = Doc {
11001            bar,
11002            platform,
11003            ..Doc::default()
11004        };
11005        let mut driver = Driver::new(doc, W, H);
11006        driver.run(0.2);
11007        driver
11008    }
11009
11010    /// Where the word `label` starts, and a point on its line a fraction `across` of the way
11011    /// along it.
11012    fn on(driver: &Driver<Doc>, label: &str, across: f32) -> Point {
11013        let (_, rect) = driver
11014            .texts()
11015            .into_iter()
11016            .find(|(text, _)| text == label)
11017            .unwrap_or_else(|| panic!("{label:?} is on screen"));
11018        // A text primitive is a point at its top-left; the line is about 19 px tall and the
11019        // words a few px per character.
11020        Point::new(rect.x + across, rect.y + 8.0)
11021    }
11022
11023    fn drag(driver: &mut Driver<Doc>, from: Point, to: Point) {
11024        driver.press_mouse(from);
11025        driver.run(0.05);
11026        driver.move_mouse(to);
11027        driver.run(0.05);
11028        driver.release_mouse(to);
11029        driver.run(0.05);
11030    }
11031
11032    /// **A drag across texts selects across them**: the tail of the first, the whole of the one
11033    /// between, the head of the last — and what the button between them says is not among the
11034    /// words, because it is not a text of the area's.
11035    #[test]
11036    fn a_drag_selects_across_the_texts_of_an_area() {
11037        let mut d = driver();
11038        let (from, to) = (
11039            on(&d, "first line of words", 60.0),
11040            on(&d, "third line ends here", 40.0),
11041        );
11042        drag(&mut d, from, to);
11043        let copied = d.area_selection().expect("something is selected");
11044        let lines: Vec<&str> = copied.lines().collect();
11045        assert_eq!(lines.len(), 3, "one line per text: {copied:?}");
11046        assert!(
11047            !lines[0].is_empty() && "first line of words".ends_with(lines[0]),
11048            "the tail of the first: {:?}",
11049            lines[0]
11050        );
11051        assert!(lines[0].len() < "first line of words".len());
11052        assert_eq!(lines[1], "second line", "the whole of the middle one");
11053        assert!(
11054            !lines[2].is_empty() && "third line ends here".starts_with(lines[2]),
11055            "the head of the last: {:?}",
11056            lines[2]
11057        );
11058        assert!(lines[2].len() < "third line ends here".len());
11059        assert!(!copied.contains("Press"), "a button's label is not text");
11060    }
11061
11062    /// **Dragged upward it is the same words.**
11063    #[test]
11064    fn dragging_upward_selects_the_same_words() {
11065        let (down, up) = {
11066            let mut d = driver();
11067            let (a, b) = (
11068                on(&d, "first line of words", 60.0),
11069                on(&d, "third line ends here", 40.0),
11070            );
11071            drag(&mut d, a, b);
11072            let down = d.area_selection();
11073            drag(&mut d, b, a);
11074            (down, d.area_selection())
11075        };
11076        assert!(down.is_some());
11077        assert_eq!(down, up);
11078    }
11079
11080    /// **A drag that leaves the words still has an end**: past the right edge of the last line,
11081    /// the selection ends with it, and below everything it ends at the last text.
11082    #[test]
11083    fn a_drag_past_the_words_ends_at_the_last_of_them() {
11084        let mut d = driver();
11085        let from = on(&d, "second line", 5.0);
11086        drag(&mut d, from, Point::new(W - 2.0, H - 2.0));
11087        let copied = d.area_selection().expect("a selection");
11088        assert!(copied.ends_with("third line ends here"), "{copied:?}");
11089    }
11090
11091    /// **Words outside the area are not part of it**: a drag over them selects nothing, and
11092    /// one that starts inside never picks them up.
11093    #[test]
11094    fn words_outside_the_area_are_left_alone() {
11095        let mut d = driver();
11096        let outside = on(&d, "Outside the area", 30.0);
11097        let end = on(&d, "third line ends here", 40.0);
11098        drag(&mut d, outside, end);
11099        assert_eq!(d.area_selection(), None, "the press began outside");
11100
11101        let inside = on(&d, "first line of words", 30.0);
11102        drag(&mut d, inside, Point::new(20.0, 2.0));
11103        let copied = d.area_selection().expect("a selection");
11104        assert!(!copied.contains("Outside"), "{copied:?}");
11105    }
11106
11107    /// **A press on a button is the button's**: no selection begins under it.
11108    #[test]
11109    fn a_button_keeps_its_press() {
11110        let mut d = driver();
11111        let button = on(&d, "Press", 10.0);
11112        let end = on(&d, "third line ends here", 40.0);
11113        drag(&mut d, button, end);
11114        assert_eq!(d.area_selection(), None);
11115    }
11116
11117    /// **A press without a drag puts the selection away**, in the area or out of it.
11118    #[test]
11119    fn a_click_puts_the_selection_away() {
11120        let mut d = driver();
11121        let (from, to) = (
11122            on(&d, "first line of words", 60.0),
11123            on(&d, "third line ends here", 40.0),
11124        );
11125        drag(&mut d, from, to);
11126        assert!(d.area_selection().is_some());
11127        let elsewhere = on(&d, "second line", 30.0);
11128        drag(&mut d, elsewhere, elsewhere);
11129        assert_eq!(d.area_selection(), None, "a click in the area");
11130
11131        drag(&mut d, from, to);
11132        assert!(d.area_selection().is_some());
11133        let outside = on(&d, "Outside the area", 30.0);
11134        drag(&mut d, outside, outside);
11135        assert_eq!(d.area_selection(), None, "a click outside it");
11136    }
11137
11138    /// **A double click selects the word.**
11139    #[test]
11140    fn a_double_click_selects_the_word() {
11141        let mut d = driver();
11142        // "second" is the first word of the line: a few px in.
11143        let at = on(&d, "second line", 12.0);
11144        d.press_mouse(at);
11145        d.release_mouse(at);
11146        d.run(0.05);
11147        d.press_mouse(at);
11148        d.release_mouse(at);
11149        d.run(0.05);
11150        assert_eq!(d.area_selection().as_deref(), Some("second"));
11151    }
11152
11153    /// **Select all takes every text of the area** — and none of what is outside it.
11154    #[test]
11155    fn select_all_takes_the_whole_area() {
11156        let mut d = driver();
11157        let at = on(&d, "second line", 12.0);
11158        let end = on(&d, "second line", 40.0);
11159        drag(&mut d, at, end);
11160        assert!(d.area_selection().is_some());
11161        d.area_select_all();
11162        assert_eq!(
11163            d.area_selection().as_deref(),
11164            Some("first line of words\nsecond line\nthird line ends here")
11165        );
11166    }
11167
11168    /// **A finger scrolls, and does not select**: a touch drag over the words leaves them be.
11169    #[test]
11170    fn a_finger_does_not_select() {
11171        let mut d = driver();
11172        let (from, to) = (
11173            on(&d, "first line of words", 60.0),
11174            on(&d, "third line ends here", 40.0),
11175        );
11176        d.press(from);
11177        d.run(0.05);
11178        d.move_to(to);
11179        d.run(0.05);
11180        d.release(to);
11181        d.run(0.05);
11182        assert_eq!(d.area_selection(), None);
11183    }
11184
11185    /// A finger held still on `at` until the long press fires, then lifted.
11186    fn hold(d: &mut Driver<Doc>, at: Point) -> bool {
11187        d.press(at);
11188        d.run(0.1);
11189        let fired = d.hold_deadline();
11190        d.run(0.05);
11191        d.release(at);
11192        d.run(0.05);
11193        fired
11194    }
11195
11196    /// Where the bar's button reading `label` is, if the bar shows one.
11197    fn bar_button(d: &Driver<Doc>, label: &str) -> Option<Point> {
11198        let (ui, _) = d.frame_parts()?;
11199        d.texts()
11200            .into_iter()
11201            .filter(|(text, _)| text == label)
11202            .map(|(_, rect)| Point::new(rect.x + 4.0, rect.y + 6.0))
11203            .find(|at| ui.toolbar_contains(*at))
11204    }
11205
11206    /// **A finger held on words selects the word under it and opens the bar** — Copy and
11207    /// Select all — over it (milestone 576). A phone has no mouse to drag with.
11208    #[test]
11209    fn a_hold_selects_the_word_and_opens_the_bar() {
11210        let mut d = driver();
11211        let at = on(&d, "second line", 12.0);
11212        assert!(hold(&mut d, at), "the hold fired");
11213        assert_eq!(d.area_selection().as_deref(), Some("second"));
11214        assert!(bar_button(&d, "Copy").is_some(), "Copy is on the bar");
11215        assert!(bar_button(&d, "Select all").is_some(), "and Select all");
11216    }
11217
11218    /// **On a desktop theme the bar is the menu, and its rows act on the area as the pill's
11219    /// buttons do** (milestone 618).
11220    #[test]
11221    fn the_desktop_menus_rows_act_on_the_area() {
11222        let mut d = driver_on(Bar::Default, Some(frus_widgets::TargetPlatform::Linux));
11223        let at = on(&d, "second line", 12.0);
11224        hold(&mut d, at);
11225        let select_all = bar_button(&d, "Select all").expect("Select all");
11226        // The menu hangs from the selection's top, over the handles under it: the press is
11227        // the menu's, not a handle's.
11228        d.press(select_all);
11229        d.release(select_all);
11230        d.run(0.05);
11231        assert_eq!(
11232            d.area_selection().as_deref(),
11233            Some("first line of words\nsecond line\nthird line ends here")
11234        );
11235    }
11236
11237    /// **The bar's Select all takes the whole area and keeps the bar**, which no longer offers
11238    /// Select all; its Copy puts the selection away, as a platform's own does.
11239    #[test]
11240    fn the_bars_buttons_act_on_the_area() {
11241        let mut d = driver();
11242        let at = on(&d, "second line", 12.0);
11243        hold(&mut d, at);
11244        let select_all = bar_button(&d, "Select all").expect("Select all");
11245        d.press(select_all);
11246        d.release(select_all);
11247        d.run(0.05);
11248        assert_eq!(
11249            d.area_selection().as_deref(),
11250            Some("first line of words\nsecond line\nthird line ends here")
11251        );
11252        assert!(
11253            bar_button(&d, "Select all").is_none(),
11254            "nothing left to select"
11255        );
11256        let copy = bar_button(&d, "Copy").expect("Copy stays");
11257        d.press(copy);
11258        d.release(copy);
11259        d.run(0.05);
11260        assert_eq!(d.area_selection(), None, "copied, and put away");
11261        assert!(bar_button(&d, "Copy").is_none(), "the bar with it");
11262    }
11263
11264    /// **A hold on a button is the button's**, and a finger that moves is a scroll, not a hold.
11265    #[test]
11266    fn a_hold_on_a_button_selects_nothing() {
11267        let mut d = driver();
11268        let button = on(&d, "Press", 10.0);
11269        hold(&mut d, button);
11270        assert_eq!(d.area_selection(), None);
11271    }
11272
11273    /// **Escape, and a tap elsewhere, put the selection and its bar away.**
11274    #[test]
11275    fn escape_and_a_tap_put_the_bar_away() {
11276        let mut d = driver();
11277        let at = on(&d, "second line", 12.0);
11278        hold(&mut d, at);
11279        assert!(d.area_selection().is_some());
11280        d.escape();
11281        d.run(0.05);
11282        assert_eq!(d.area_selection(), None);
11283        assert!(bar_button(&d, "Copy").is_none());
11284
11285        hold(&mut d, at);
11286        assert!(d.area_selection().is_some());
11287        let elsewhere = on(&d, "Outside the area", 20.0);
11288        d.press(elsewhere);
11289        d.release(elsewhere);
11290        d.run(0.05);
11291        assert_eq!(d.area_selection(), None);
11292        assert!(bar_button(&d, "Copy").is_none());
11293    }
11294
11295    /// **A selection made with a mouse has no bar**: the bar is for a finger, which has no
11296    /// Ctrl+C.
11297    #[test]
11298    fn a_mouse_selection_has_no_bar() {
11299        let mut d = driver();
11300        let (from, to) = (
11301            on(&d, "first line of words", 60.0),
11302            on(&d, "third line ends here", 40.0),
11303        );
11304        drag(&mut d, from, to);
11305        assert!(d.area_selection().is_some());
11306        assert!(bar_button(&d, "Copy").is_none());
11307    }
11308
11309    /// **The application says what the bar holds** (milestone 578): its own item beside Copy,
11310    /// and no Select all — and pressing its item sends the application's message.
11311    #[test]
11312    fn the_application_says_what_the_bar_holds() {
11313        let mut d = driver_with(Bar::Share);
11314        let at = on(&d, "second line", 12.0);
11315        assert!(hold(&mut d, at));
11316        assert_eq!(d.area_selection().as_deref(), Some("second"));
11317        assert!(bar_button(&d, "Copy").is_some(), "Copy was kept");
11318        assert!(
11319            bar_button(&d, "Select all").is_none(),
11320            "Select all was taken off"
11321        );
11322        let share = bar_button(&d, "Share").expect("the application's own item");
11323        let shared = d.app().shared.clone();
11324        d.press(share);
11325        d.release(share);
11326        d.run(0.05);
11327        assert_eq!(shared.get(), 1, "its message was sent");
11328    }
11329
11330    /// **An empty list shows no bar**, and the selection is still made.
11331    #[test]
11332    fn an_empty_list_shows_no_bar() {
11333        let mut d = driver_with(Bar::Empty);
11334        let at = on(&d, "second line", 12.0);
11335        assert!(hold(&mut d, at));
11336        assert_eq!(d.area_selection().as_deref(), Some("second"));
11337        assert!(bar_button(&d, "Copy").is_none());
11338        assert!(d
11339            .frame_parts()
11340            .is_some_and(|(ui, _)| !ui.toolbar_contains(at)));
11341    }
11342
11343    /// A handle's middle — where a finger takes it.
11344    fn middle(rect: frus_widgets::Rect) -> Point {
11345        Point::new(rect.x + rect.width / 2.0, rect.y + rect.height / 2.0)
11346    }
11347
11348    /// A finger taking a handle at `from` and carrying it to `to`, a few steps at a time.
11349    fn carry_handle(d: &mut Driver<Doc>, from: Point, to: Point) {
11350        d.press(from);
11351        d.run(0.05);
11352        for step in 1..=4 {
11353            let f = step as f32 / 4.0;
11354            d.move_to(Point::new(
11355                from.x + (to.x - from.x) * f,
11356                from.y + (to.y - from.y) * f,
11357            ));
11358            d.run(0.05);
11359        }
11360        d.release(to);
11361        d.run(0.05);
11362    }
11363
11364    /// **A touch selection has two handles**, hanging below the ends of the word, the start one
11365    /// to the left of the end one — and a mouse selection has none.
11366    #[test]
11367    fn a_touch_selection_has_handles_and_a_mouse_one_does_not() {
11368        let mut d = driver();
11369        let paths = |d: &Driver<Doc>| {
11370            let (ui, _) = d.frame_parts().expect("a frame");
11371            ui.scene()
11372                .primitives()
11373                .iter()
11374                .filter(|p| matches!(p, frus_widgets::Primitive::Path { .. }))
11375                .count()
11376        };
11377        let before = paths(&d);
11378        let at = on(&d, "second line", 12.0);
11379        hold(&mut d, at);
11380        assert_eq!(paths(&d), before + 2, "the two handles are painted");
11381        let [start, end] = d.area_handles().expect("two handles");
11382        assert!(start.line_center.x < end.line_center.x, "{start:?} {end:?}");
11383        assert!(start.rect.y > at.y - 8.0, "they hang below the line");
11384
11385        let (from, to) = (
11386            on(&d, "first line of words", 60.0),
11387            on(&d, "third line ends here", 40.0),
11388        );
11389        drag(&mut d, from, to);
11390        assert!(d.area_selection().is_some());
11391        assert!(
11392            d.area_handles().is_none(),
11393            "a mouse selection has no handles"
11394        );
11395        d.area_select_all();
11396        assert!(
11397            d.area_handles().is_none(),
11398            "nor does it grow any when the keyboard selects all"
11399        );
11400    }
11401
11402    /// **The end handle carries the end of the selection to the next text**: dragged down to the
11403    /// third line, the selection runs from the word to it. The bar is put away while the handle
11404    /// moves, and back when it is let go.
11405    #[test]
11406    fn the_end_handle_widens_the_selection_across_texts() {
11407        let mut d = driver();
11408        let at = on(&d, "second line", 12.0);
11409        hold(&mut d, at);
11410        let [_, end] = d.area_handles().expect("handles");
11411        let third = on(&d, "third line ends here", 60.0);
11412        let from = middle(end.rect);
11413        // The handle hangs below its line: carried to where its line would meet the third.
11414        let to = Point::new(third.x, from.y + (third.y - at.y));
11415        d.press(from);
11416        d.run(0.05);
11417        d.move_to(Point::new(from.x, (from.y + to.y) / 2.0));
11418        d.run(0.05);
11419        d.move_to(to);
11420        d.run(0.05);
11421        assert!(
11422            bar_button(&d, "Copy").is_none(),
11423            "no bar while a handle moves"
11424        );
11425        d.release(to);
11426        d.run(0.05);
11427        let copied = d.area_selection().expect("a selection");
11428        let lines: Vec<&str> = copied.lines().collect();
11429        assert_eq!(lines.len(), 2, "{copied:?}");
11430        assert_eq!(lines[0], "second line", "the rest of the word's line");
11431        assert!("third line ends here".starts_with(lines[1]), "{copied:?}");
11432        assert!(!lines[1].is_empty());
11433        assert!(bar_button(&d, "Copy").is_some(), "the bar is back");
11434    }
11435
11436    /// **The start handle carries the start of it back up**: dragged to the first line, the
11437    /// selection begins there.
11438    #[test]
11439    fn the_start_handle_widens_it_upward() {
11440        let mut d = driver();
11441        let at = on(&d, "second line", 80.0);
11442        hold(&mut d, at);
11443        assert_eq!(d.area_selection().as_deref(), Some("line"));
11444        let [start, _] = d.area_handles().expect("handles");
11445        let first = on(&d, "first line of words", 30.0);
11446        let from = middle(start.rect);
11447        let to = Point::new(first.x, from.y + (first.y - at.y));
11448        carry_handle(&mut d, from, to);
11449        let copied = d.area_selection().expect("a selection");
11450        let lines: Vec<&str> = copied.lines().collect();
11451        assert_eq!(lines.len(), 2, "{copied:?}");
11452        assert!("first line of words".ends_with(lines[0]), "{copied:?}");
11453        assert_eq!(lines[1], "second line");
11454    }
11455
11456    /// **A handle cannot cross the other**: the end handle dragged back past the start leaves the
11457    /// selection as it was, rather than turning it inside out.
11458    #[test]
11459    fn a_handle_does_not_cross_the_other() {
11460        let mut d = driver();
11461        let at = on(&d, "second line", 80.0);
11462        hold(&mut d, at);
11463        let before = d.area_selection();
11464        assert!(before.is_some());
11465        let [_, end] = d.area_handles().expect("handles");
11466        let first = on(&d, "first line of words", 5.0);
11467        let from = middle(end.rect);
11468        // In one move, straight to a place before the start: refused, not turned inside out.
11469        let to = Point::new(first.x, from.y + (first.y - at.y));
11470        d.press(from);
11471        d.run(0.05);
11472        d.move_to(to);
11473        d.run(0.05);
11474        d.release(to);
11475        d.run(0.05);
11476        assert_eq!(d.area_selection(), before);
11477    }
11478
11479    /// **The selection is painted**: a highlight over each text it covers, in the theme's
11480    /// colour, and none over the one it does not.
11481    #[test]
11482    fn the_selection_is_highlighted_in_each_text() {
11483        let mut d = driver();
11484        let (from, to) = (
11485            on(&d, "first line of words", 60.0),
11486            on(&d, "third line ends here", 40.0),
11487        );
11488        let bare = rects(&d);
11489        drag(&mut d, from, to);
11490        // What the selection added to the frame: a box over each text it covers.
11491        let boxes: Vec<frus_widgets::Rect> = rects(&d)
11492            .into_iter()
11493            .filter(|rect| !bare.contains(rect))
11494            .collect();
11495        assert_eq!(boxes.len(), 3, "one per text covered: {boxes:?}");
11496        let outside = on(&d, "Outside the area", 0.0);
11497        assert!(
11498            boxes.iter().all(|b| b.y > outside.y + 10.0),
11499            "none over the line outside the area: {boxes:?}"
11500        );
11501    }
11502
11503    fn rects(d: &Driver<Doc>) -> Vec<frus_widgets::Rect> {
11504        let (ui, _) = d.frame_parts().expect("a frame");
11505        ui.scene()
11506            .primitives()
11507            .iter()
11508            .filter_map(|p| match p {
11509                frus_widgets::Primitive::Rect { rect, .. } => Some(*rect),
11510                _ => None,
11511            })
11512            .collect()
11513    }
11514}
11515
11516/// Taps, double taps, long presses and secondary clicks on a
11517/// [`GestureDetector`](frus_widgets::GestureDetector), driven through the shell (milestone 581).
11518#[cfg(test)]
11519mod gesture_detector_tests {
11520    use super::testing::Driver;
11521    use crate::{Application, Command};
11522    use frus_widgets::{Container, Flex, GestureDetector, Point, Text, Theme, Widget};
11523    use std::cell::RefCell;
11524    use std::rc::Rc;
11525
11526    const W: f32 = 400.0;
11527    const H: f32 = 400.0;
11528
11529    #[derive(Clone, Debug, PartialEq)]
11530    enum Msg {
11531        Tap,
11532        Double,
11533        Hold,
11534        Menu,
11535        Plain,
11536        OnlyDouble,
11537    }
11538
11539    /// Three detectors down a page: one that takes everything, one that takes only a tap, and
11540    /// one that takes only a double tap.
11541    #[derive(Default)]
11542    struct Page {
11543        heard: Rc<RefCell<Vec<Msg>>>,
11544    }
11545
11546    impl Application for Page {
11547        type Message = Msg;
11548
11549        fn update(&mut self, message: Msg) -> Command<Msg> {
11550            self.heard.borrow_mut().push(message);
11551            Command::none()
11552        }
11553
11554        fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
11555            let block = |label: &str| {
11556                Container::new()
11557                    .width(300.0)
11558                    .height(60.0)
11559                    .child(Text::new(label).no_wrap())
11560            };
11561            Box::new(
11562                Container::new().width(W).height(H).child(
11563                    Flex::column()
11564                        .gap(40.0)
11565                        .child(
11566                            GestureDetector::new(block("everything"))
11567                                .on_tap(Msg::Tap)
11568                                .on_double_tap(Msg::Double)
11569                                .on_long_press(Msg::Hold)
11570                                .on_secondary_tap(Msg::Menu),
11571                        )
11572                        .child(GestureDetector::new(block("tap only")).on_tap(Msg::Plain))
11573                        .child(
11574                            GestureDetector::new(block("double only"))
11575                                .on_double_tap(Msg::OnlyDouble),
11576                        ),
11577                ),
11578            )
11579        }
11580    }
11581
11582    fn driver() -> (Driver<Page>, Rc<RefCell<Vec<Msg>>>) {
11583        let page = Page::default();
11584        let heard = page.heard.clone();
11585        let mut driver = Driver::new(page, W, H);
11586        driver.run(0.2);
11587        (driver, heard)
11588    }
11589
11590    /// A point inside the block labelled `label`.
11591    fn on(d: &Driver<Page>, label: &str) -> Point {
11592        let (_, rect) = d
11593            .texts()
11594            .into_iter()
11595            .find(|(text, _)| text == label)
11596            .unwrap_or_else(|| panic!("{label:?} is on screen"));
11597        Point::new(rect.x + 20.0, rect.y + 8.0)
11598    }
11599
11600    fn tap(d: &mut Driver<Page>, at: Point) {
11601        d.press(at);
11602        d.run(0.02);
11603        d.release(at);
11604        d.run(0.02);
11605    }
11606
11607    /// **With a double tap possible, a tap waits**: nothing is sent at the release, and the
11608    /// tap is sent once the wait is over.
11609    #[test]
11610    fn a_tap_waits_while_a_double_tap_is_possible() {
11611        let (mut d, heard) = driver();
11612        let at = on(&d, "everything");
11613        tap(&mut d, at);
11614        assert!(heard.borrow().is_empty(), "not yet: {:?}", heard.borrow());
11615        assert!(d.tap_deadline(), "a tap was waiting");
11616        assert_eq!(*heard.borrow(), vec![Msg::Tap]);
11617    }
11618
11619    /// **The loop wakes when the wait is over**, so a waiting tap is sent without anything
11620    /// else happening; and nothing is scheduled once it is gone.
11621    #[test]
11622    fn the_loop_wakes_for_a_waiting_tap() {
11623        let (mut d, _) = driver();
11624        let before = std::time::Instant::now();
11625        let at = on(&d, "everything");
11626        tap(&mut d, at);
11627        let wake = d.next_wake().expect("a wake for the tap");
11628        let wait = wake.duration_since(before);
11629        assert!(
11630            wait <= std::time::Duration::from_millis(400),
11631            "about 300 ms: {wait:?}"
11632        );
11633        d.tap_deadline();
11634        assert_eq!(d.next_wake(), None);
11635    }
11636
11637    /// **Two taps in quick succession are a double tap**, and not also a tap.
11638    #[test]
11639    fn two_quick_taps_are_a_double_tap() {
11640        let (mut d, heard) = driver();
11641        let at = on(&d, "everything");
11642        tap(&mut d, at);
11643        tap(&mut d, Point::new(at.x + 6.0, at.y + 2.0));
11644        assert_eq!(*heard.borrow(), vec![Msg::Double]);
11645        assert!(!d.tap_deadline(), "nothing is left waiting");
11646        assert_eq!(*heard.borrow(), vec![Msg::Double]);
11647    }
11648
11649    /// **Without a double tap, a tap answers at once.**
11650    #[test]
11651    fn a_tap_alone_answers_at_once() {
11652        let (mut d, heard) = driver();
11653        let at = on(&d, "tap only");
11654        tap(&mut d, at);
11655        assert_eq!(*heard.borrow(), vec![Msg::Plain]);
11656        tap(&mut d, at);
11657        assert_eq!(
11658            *heard.borrow(),
11659            vec![Msg::Plain, Msg::Plain],
11660            "two taps are two taps"
11661        );
11662    }
11663
11664    /// **A press somewhere else ends the wait**: the first tap is a tap after all, sent before
11665    /// what the second press does.
11666    #[test]
11667    fn a_press_elsewhere_sends_the_waiting_tap() {
11668        let (mut d, heard) = driver();
11669        let (first, other) = (on(&d, "everything"), on(&d, "tap only"));
11670        tap(&mut d, first);
11671        tap(&mut d, other);
11672        assert_eq!(*heard.borrow(), vec![Msg::Tap, Msg::Plain]);
11673    }
11674
11675    /// **The secondary button** sends its own message, and no tap.
11676    #[test]
11677    fn a_secondary_click_sends_its_message() {
11678        let (mut d, heard) = driver();
11679        let at = on(&d, "everything");
11680        d.secondary_click(at);
11681        d.run(0.02);
11682        assert_eq!(*heard.borrow(), vec![Msg::Menu]);
11683        let plain = on(&d, "tap only");
11684        d.secondary_click(plain);
11685        assert_eq!(
11686            *heard.borrow(),
11687            vec![Msg::Menu],
11688            "a detector without one hears nothing"
11689        );
11690    }
11691
11692    /// **A long press** sends its message, and the release after it is not a tap.
11693    #[test]
11694    fn a_long_press_is_not_also_a_tap() {
11695        let (mut d, heard) = driver();
11696        let at = on(&d, "everything");
11697        d.press(at);
11698        d.run(0.1);
11699        assert!(d.hold_deadline(), "the hold fired");
11700        d.release(at);
11701        d.run(0.02);
11702        d.tap_deadline();
11703        assert_eq!(*heard.borrow(), vec![Msg::Hold]);
11704    }
11705
11706    /// **A detector with only a double tap** takes the press all the same: one tap sends
11707    /// nothing, two send the double tap.
11708    #[test]
11709    fn a_double_tap_alone_is_heard() {
11710        let (mut d, heard) = driver();
11711        let at = on(&d, "double only");
11712        tap(&mut d, at);
11713        d.tap_deadline();
11714        assert!(heard.borrow().is_empty(), "{:?}", heard.borrow());
11715        tap(&mut d, at);
11716        tap(&mut d, at);
11717        assert_eq!(*heard.borrow(), vec![Msg::OnlyDouble]);
11718    }
11719}
11720
11721/// Drags on a [`GestureDetector`](frus_widgets::GestureDetector), alone and inside a scroll,
11722/// driven through the shell (milestone 582).
11723#[cfg(test)]
11724mod gesture_pan_tests {
11725    use super::testing::Driver;
11726    use crate::{Application, Command};
11727    use frus_widgets::{
11728        Container, Flex, GestureDetector, PanAxis, Point, SingleChildScrollView, Text, Theme,
11729        Widget,
11730    };
11731    use std::cell::RefCell;
11732    use std::rc::Rc;
11733
11734    const W: f32 = 400.0;
11735    const H: f32 = 400.0;
11736
11737    #[derive(Clone, Debug, PartialEq)]
11738    enum Msg {
11739        Start(Point),
11740        Update(Point, Point),
11741        End(Point),
11742        Tap,
11743        Hold,
11744    }
11745
11746    /// A detector, either alone on the page or at the top of a tall vertical scroll.
11747    struct Page {
11748        axis: PanAxis,
11749        scrolled: bool,
11750        heard: Rc<RefCell<Vec<Msg>>>,
11751    }
11752
11753    impl Application for Page {
11754        type Message = Msg;
11755
11756        fn update(&mut self, message: Msg) -> Command<Msg> {
11757            self.heard.borrow_mut().push(message);
11758            Command::none()
11759        }
11760
11761        fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
11762            let detector = GestureDetector::new(
11763                Container::new()
11764                    .width(300.0)
11765                    .height(150.0)
11766                    .child(Text::new("drag me").no_wrap()),
11767            )
11768            .on_tap(Msg::Tap)
11769            .on_long_press(Msg::Hold)
11770            .on_pan_start(Msg::Start)
11771            .on_pan_update(Msg::Update)
11772            .on_pan_end(Msg::End)
11773            .pan_axis(self.axis);
11774            let content = Flex::column()
11775                .child(Container::new().width(300.0).height(30.0))
11776                .child(detector)
11777                .child(Container::new().width(300.0).height(1200.0));
11778            if self.scrolled {
11779                Box::new(
11780                    SingleChildScrollView::new()
11781                        .width(W)
11782                        .height(H)
11783                        .child(content),
11784                )
11785            } else {
11786                Box::new(Container::new().width(W).height(H).child(content))
11787            }
11788        }
11789    }
11790
11791    fn driver(axis: PanAxis, scrolled: bool) -> (Driver<Page>, Rc<RefCell<Vec<Msg>>>) {
11792        let heard = Rc::new(RefCell::new(Vec::new()));
11793        let page = Page {
11794            axis,
11795            scrolled,
11796            heard: heard.clone(),
11797        };
11798        let mut driver = Driver::new(page, W, H);
11799        driver.run(0.2);
11800        (driver, heard)
11801    }
11802
11803    /// Where the detector's words start.
11804    fn origin(d: &Driver<Page>) -> Point {
11805        let (_, rect) = d
11806            .texts()
11807            .into_iter()
11808            .find(|(text, _)| text == "drag me")
11809            .expect("on screen");
11810        Point::new(rect.x, rect.y)
11811    }
11812
11813    /// A finger from `from` by `(dx, dy)`, in eight steps.
11814    fn swipe(d: &mut Driver<Page>, from: Point, dx: f32, dy: f32) {
11815        d.press(from);
11816        d.run(0.02);
11817        for step in 1..=8 {
11818            let f = step as f32 / 8.0;
11819            d.move_to(Point::new(from.x + dx * f, from.y + dy * f));
11820            d.run(0.02);
11821        }
11822        d.release(Point::new(from.x + dx, from.y + dy));
11823        d.run(0.02);
11824    }
11825
11826    fn deltas(heard: &[Msg]) -> Point {
11827        heard.iter().fold(Point::new(0.0, 0.0), |sum, m| match m {
11828            Msg::Update(_, delta) => Point::new(sum.x + delta.x, sum.y + delta.y),
11829            _ => sum,
11830        })
11831    }
11832
11833    fn scroll_y(d: &Driver<Page>) -> f32 {
11834        d.offsets().first().map_or(0.0, |(_, offset)| offset.1)
11835    }
11836
11837    /// **A drag is a start, movements and an end**: the start where the finger went down in
11838    /// the detector's own coordinates, and movements that add up to the way it went once past
11839    /// the slop.
11840    #[test]
11841    fn a_drag_is_a_start_updates_and_an_end() {
11842        let (mut d, heard) = driver(PanAxis::Free, false);
11843        let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 40.0);
11844        swipe(&mut d, at, 120.0, 60.0);
11845        let heard = heard.borrow();
11846        assert!(matches!(heard.first(), Some(Msg::Start(_))), "{heard:?}");
11847        if let Some(Msg::Start(local)) = heard.first() {
11848            // The words sit at the detector's corner, 30 px down the page.
11849            let corner = origin(&d);
11850            assert!(corner.y >= 30.0, "{corner:?}");
11851            assert_eq!(*local, Point::new(at.x - corner.x, at.y - corner.y));
11852        }
11853        assert!(matches!(heard.last(), Some(Msg::End(_))), "{heard:?}");
11854        if let Some(Msg::End(velocity)) = heard.last() {
11855            assert!(velocity.x.hypot(velocity.y) <= 8000.0 + 0.5, "{velocity:?}");
11856        }
11857        let moved = deltas(&heard);
11858        assert!(moved.x > 60.0 && moved.y > 20.0, "{moved:?}");
11859        assert!(!heard.contains(&Msg::Tap), "a drag is not a tap");
11860        // The last movement is where the finger lifted: nothing was left behind on the way.
11861        let last = heard.iter().rev().find_map(|m| match m {
11862            Msg::Update(local, _) => Some(*local),
11863            _ => None,
11864        });
11865        let corner = origin(&d);
11866        assert_eq!(
11867            last,
11868            Some(Point::new(at.x + 120.0 - corner.x, at.y + 60.0 - corner.y))
11869        );
11870    }
11871
11872    /// **A press that barely moves is a tap**, and no drag.
11873    #[test]
11874    fn a_press_that_does_not_move_is_a_tap() {
11875        let (mut d, heard) = driver(PanAxis::Free, false);
11876        let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 40.0);
11877        swipe(&mut d, at, 3.0, 2.0);
11878        assert_eq!(*heard.borrow(), vec![Msg::Tap]);
11879    }
11880
11881    /// **A hold on a detector that also drags is a long press**: the drag waiting for the
11882    /// finger to move does not keep the hold from being one (seen on the phone).
11883    #[test]
11884    fn a_hold_on_a_dragging_detector_is_a_long_press() {
11885        let (mut d, heard) = driver(PanAxis::Free, false);
11886        let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 40.0);
11887        d.press(at);
11888        d.run(0.1);
11889        assert!(d.hold_deadline(), "the hold fired");
11890        d.release(at);
11891        d.run(0.02);
11892        assert_eq!(*heard.borrow(), vec![Msg::Hold]);
11893    }
11894
11895    /// **The mouse drags too**, with its own, finer slop.
11896    #[test]
11897    fn a_mouse_drags_it() {
11898        let (mut d, heard) = driver(PanAxis::Free, false);
11899        let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 40.0);
11900        d.press_mouse(at);
11901        d.run(0.02);
11902        d.move_mouse(Point::new(at.x + 30.0, at.y));
11903        d.run(0.02);
11904        d.release_mouse(Point::new(at.x + 30.0, at.y));
11905        d.run(0.02);
11906        let heard = heard.borrow();
11907        assert!(matches!(heard.first(), Some(Msg::Start(_))), "{heard:?}");
11908        assert!(matches!(heard.last(), Some(Msg::End(_))), "{heard:?}");
11909    }
11910
11911    /// **A horizontal drag in a vertical scroll**: across, the drag has it and the page stays;
11912    /// down, the page scrolls and the drag hears nothing. Its movements are only across.
11913    #[test]
11914    fn a_horizontal_drag_shares_a_vertical_scroll() {
11915        let (mut d, heard) = driver(PanAxis::Horizontal, true);
11916        let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 40.0);
11917        swipe(&mut d, at, 120.0, 10.0);
11918        assert!(
11919            matches!(heard.borrow().first(), Some(Msg::Start(_))),
11920            "{:?}",
11921            heard.borrow()
11922        );
11923        assert_eq!(deltas(&heard.borrow()).y, 0.0, "only across");
11924        assert_eq!(scroll_y(&d), 0.0, "the page did not move");
11925
11926        heard.borrow_mut().clear();
11927        let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 120.0);
11928        swipe(&mut d, at, 5.0, -120.0);
11929        assert!(heard.borrow().is_empty(), "{:?}", heard.borrow());
11930        assert!(scroll_y(&d) > 30.0, "the page scrolled: {}", scroll_y(&d));
11931    }
11932
11933    /// **A free drag in a vertical scroll** takes a movement across and leaves a movement
11934    /// down to the scroll, as the reference's pan does.
11935    #[test]
11936    fn a_free_drag_leaves_the_scroll_its_own_direction() {
11937        let (mut d, heard) = driver(PanAxis::Free, true);
11938        let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 40.0);
11939        swipe(&mut d, at, 150.0, 8.0);
11940        assert!(
11941            matches!(heard.borrow().first(), Some(Msg::Start(_))),
11942            "{:?}",
11943            heard.borrow()
11944        );
11945        assert_eq!(scroll_y(&d), 0.0);
11946
11947        heard.borrow_mut().clear();
11948        let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 120.0);
11949        swipe(&mut d, at, 4.0, -120.0);
11950        assert!(heard.borrow().is_empty(), "{:?}", heard.borrow());
11951        assert!(scroll_y(&d) > 30.0, "the page scrolled: {}", scroll_y(&d));
11952    }
11953
11954    /// **A machine that stalls in the middle of a drag does not change it** (milestone 584,
11955    /// seen in CI). The shell's gestures are timed by the driver's frames and not by the wall:
11956    /// with the wall's clock, twenty milliseconds lost between two movements a microsecond
11957    /// apart read as a fling in either direction, and a page scrolled down was thrown back to
11958    /// the top.
11959    #[test]
11960    fn a_stall_mid_drag_does_not_change_the_scroll() {
11961        let (mut d, heard) = driver(PanAxis::Free, true);
11962        let from = Point::new(origin(&d).x + 40.0, origin(&d).y + 120.0);
11963        d.press(from);
11964        d.run(0.02);
11965        for step in 1..=8 {
11966            let f = step as f32 / 8.0;
11967            d.move_to(Point::new(from.x + 4.0 * f, from.y - 120.0 * f));
11968            if step == 5 {
11969                std::thread::sleep(std::time::Duration::from_millis(20));
11970            }
11971            d.run(0.02);
11972        }
11973        d.release(Point::new(from.x + 4.0, from.y - 120.0));
11974        d.run(0.02);
11975        assert!(heard.borrow().is_empty(), "{:?}", heard.borrow());
11976        assert!(scroll_y(&d) > 30.0, "the page scrolled: {}", scroll_y(&d));
11977    }
11978
11979    /// **A vertical drag in a vertical scroll** is the detector's: it asked for that
11980    /// direction, and it is under the finger.
11981    #[test]
11982    fn a_vertical_drag_takes_it_from_the_scroll() {
11983        let (mut d, heard) = driver(PanAxis::Vertical, true);
11984        let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 120.0);
11985        swipe(&mut d, at, 6.0, -100.0);
11986        let heard = heard.borrow();
11987        assert!(matches!(heard.first(), Some(Msg::Start(_))), "{heard:?}");
11988        let moved = deltas(&heard);
11989        assert_eq!(moved.x, 0.0, "only down");
11990        assert!(moved.y < -40.0, "{moved:?}");
11991        assert_eq!(scroll_y(&d), 0.0, "the page did not move");
11992    }
11993}
11994
11995/// The pointer entering, moving over and leaving a
11996/// [`MouseRegion`](frus_widgets::MouseRegion), driven through the shell (milestone 583).
11997#[cfg(test)]
11998mod mouse_region_tests {
11999    use super::testing::Driver;
12000    use crate::{Application, Command};
12001    use frus_widgets::{
12002        Container, Cursor, Flex, MouseRegion, Point, Positioned, Stack, TextField, Theme, Widget,
12003    };
12004    use std::cell::RefCell;
12005    use std::rc::Rc;
12006
12007    const W: f32 = 400.0;
12008    const H: f32 = 600.0;
12009
12010    #[derive(Clone, Debug, PartialEq)]
12011    enum Msg {
12012        Enter(&'static str, Point),
12013        Move(&'static str),
12014        Exit(&'static str),
12015        Show,
12016    }
12017
12018    /// An outer region holding an inner one at its top; below them, a big region with a
12019    /// smaller one overlapping it — not inside it — in a stack; and a region that appears
12020    /// when told to.
12021    struct Page {
12022        front_opaque: bool,
12023        shown: bool,
12024        heard: Rc<RefCell<Vec<Msg>>>,
12025    }
12026
12027    fn region(name: &'static str, child: impl Widget<Msg> + 'static) -> MouseRegion<Msg> {
12028        MouseRegion::new(child)
12029            .on_enter(move |at| Msg::Enter(name, at))
12030            .on_hover(move |_| Msg::Move(name))
12031            .on_exit(move |_| Msg::Exit(name))
12032    }
12033
12034    impl Application for Page {
12035        type Message = Msg;
12036
12037        fn update(&mut self, message: Msg) -> Command<Msg> {
12038            if message == Msg::Show {
12039                self.shown = true;
12040            } else {
12041                self.heard.borrow_mut().push(message);
12042            }
12043            Command::none()
12044        }
12045
12046        fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
12047            // 0..200: outer (300 wide), with inner 100×50 at its top-left.
12048            let nested = region(
12049                "outer",
12050                Container::new().width(300.0).height(200.0).child(
12051                    region("inner", Container::new().width(100.0).height(50.0))
12052                        .cursor(Cursor::Pointer),
12053                ),
12054            )
12055            .cursor(Cursor::Grab);
12056            // 200..400: back fills a 300×200 stack; front sits at (50, 50), 100×100, over it.
12057            let stack = Stack::new()
12058                .width(300.0)
12059                .height(200.0)
12060                .layer(region("back", Container::new().width(300.0).height(200.0)))
12061                .layer(
12062                    Positioned::new(
12063                        region("front", Container::new().width(100.0).height(100.0))
12064                            .opaque(self.front_opaque),
12065                    )
12066                    .left(50.0)
12067                    .top(50.0),
12068                );
12069            // 400..500: a field inside a region that asks for a hand.
12070            let field = region("field", TextField::new("").width(300.0)).cursor(Cursor::Pointer);
12071            // 500..600: a region that appears.
12072            let appearing: Box<dyn Widget<Msg>> = if self.shown {
12073                Box::new(region("late", Container::new().width(300.0).height(100.0)))
12074            } else {
12075                Box::new(Container::new().width(300.0).height(100.0))
12076            };
12077            Box::new(
12078                Container::new().width(W).height(H).child(
12079                    Flex::column()
12080                        .child(nested)
12081                        .child(stack)
12082                        .child(Container::new().height(100.0).child(field))
12083                        .child(appearing),
12084                ),
12085            )
12086        }
12087    }
12088
12089    fn driver(front_opaque: bool) -> (Driver<Page>, Rc<RefCell<Vec<Msg>>>) {
12090        let heard = Rc::new(RefCell::new(Vec::new()));
12091        let page = Page {
12092            front_opaque,
12093            shown: false,
12094            heard: heard.clone(),
12095        };
12096        let mut driver = Driver::new(page, W, H);
12097        driver.run(0.2);
12098        (driver, heard)
12099    }
12100
12101    fn take(heard: &Rc<RefCell<Vec<Msg>>>) -> Vec<Msg> {
12102        std::mem::take(&mut *heard.borrow_mut())
12103    }
12104
12105    fn mouse(d: &mut Driver<Page>, x: f32, y: f32) {
12106        d.move_mouse(Point::new(x, y));
12107        d.run(0.02);
12108    }
12109
12110    /// **Into a region inside another, both are entered**, the outer first, each told where
12111    /// the pointer is in its own coordinates; a move inside is heard by both; out of the
12112    /// inner one only it is left, and out of both the outer one is too.
12113    #[test]
12114    fn nested_regions_are_entered_and_left_together() {
12115        let (mut d, heard) = driver(true);
12116        mouse(&mut d, 20.0, 10.0);
12117        assert_eq!(
12118            take(&heard),
12119            vec![
12120                Msg::Enter("outer", Point::new(20.0, 10.0)),
12121                Msg::Enter("inner", Point::new(20.0, 10.0)),
12122            ]
12123        );
12124        mouse(&mut d, 30.0, 12.0);
12125        let moved = take(&heard);
12126        assert!(moved.contains(&Msg::Move("inner")) && moved.contains(&Msg::Move("outer")));
12127        mouse(&mut d, 200.0, 150.0);
12128        assert_eq!(take(&heard), vec![Msg::Exit("inner"), Msg::Move("outer")]);
12129        mouse(&mut d, 350.0, 150.0);
12130        assert_eq!(take(&heard), vec![Msg::Exit("outer")]);
12131    }
12132
12133    /// **An opaque region hides the one behind it**: over the overlap, only the front one is
12134    /// in; one that is not opaque lets the back one in too.
12135    #[test]
12136    fn an_opaque_region_hides_the_one_behind() {
12137        let (mut d, heard) = driver(true);
12138        mouse(&mut d, 80.0, 280.0);
12139        let entered: Vec<_> = take(&heard);
12140        assert!(
12141            entered.iter().any(|m| matches!(m, Msg::Enter("front", _))),
12142            "{entered:?}"
12143        );
12144        assert!(
12145            !entered.iter().any(|m| matches!(m, Msg::Enter("back", _))),
12146            "{entered:?}"
12147        );
12148        // Out of the front one and still in the back: the back one is entered now.
12149        mouse(&mut d, 250.0, 280.0);
12150        let after = take(&heard);
12151        assert!(after.contains(&Msg::Exit("front")), "{after:?}");
12152        assert!(
12153            after.iter().any(|m| matches!(m, Msg::Enter("back", _))),
12154            "{after:?}"
12155        );
12156
12157        let (mut d, heard) = driver(false);
12158        mouse(&mut d, 80.0, 280.0);
12159        let entered = take(&heard);
12160        assert!(
12161            entered.iter().any(|m| matches!(m, Msg::Enter("front", _))),
12162            "{entered:?}"
12163        );
12164        assert!(
12165            entered.iter().any(|m| matches!(m, Msg::Enter("back", _))),
12166            "{entered:?}"
12167        );
12168    }
12169
12170    /// **With a button held, a region is entered and left but hears no moves**, as the
12171    /// reference's hover is only a pointer with nothing pressed.
12172    #[test]
12173    fn no_moves_while_a_button_is_held() {
12174        let (mut d, heard) = driver(true);
12175        mouse(&mut d, 350.0, 150.0);
12176        take(&heard);
12177        d.press_mouse(Point::new(350.0, 150.0));
12178        d.run(0.02);
12179        d.move_mouse(Point::new(40.0, 20.0));
12180        d.run(0.02);
12181        d.move_mouse(Point::new(45.0, 22.0));
12182        d.run(0.02);
12183        let heard_now = take(&heard);
12184        assert!(
12185            heard_now
12186                .iter()
12187                .any(|m| matches!(m, Msg::Enter("inner", _))),
12188            "{heard_now:?}"
12189        );
12190        assert!(
12191            !heard_now.iter().any(|m| matches!(m, Msg::Move(_))),
12192            "{heard_now:?}"
12193        );
12194        d.release_mouse(Point::new(45.0, 22.0));
12195        d.run(0.02);
12196    }
12197
12198    /// **A finger is in a region while it touches**: entered at the press, left at the lift.
12199    #[test]
12200    fn a_finger_enters_while_it_touches() {
12201        let (mut d, heard) = driver(true);
12202        d.press(Point::new(200.0, 150.0));
12203        d.run(0.02);
12204        assert_eq!(
12205            take(&heard),
12206            vec![Msg::Enter("outer", Point::new(200.0, 150.0))]
12207        );
12208        d.release(Point::new(200.0, 150.0));
12209        d.run(0.02);
12210        assert_eq!(take(&heard), vec![Msg::Exit("outer")]);
12211    }
12212
12213    /// **A region that comes under a still pointer is entered** at the next frame, with no
12214    /// movement at all.
12215    #[test]
12216    fn a_region_appearing_under_the_pointer_is_entered() {
12217        let (mut d, heard) = driver(true);
12218        mouse(&mut d, 150.0, 550.0);
12219        assert!(take(&heard).is_empty());
12220        d.update(Msg::Show);
12221        d.run(0.05);
12222        let heard_now = take(&heard);
12223        assert!(
12224            heard_now.iter().any(|m| matches!(m, Msg::Enter("late", _))),
12225            "{heard_now:?}"
12226        );
12227    }
12228
12229    /// **The cursor is the innermost region's**, unless what is under the pointer asks for
12230    /// one of its own: a field keeps its text cursor inside a region asking for a hand.
12231    #[test]
12232    fn the_innermost_region_sets_the_cursor() {
12233        let (mut d, _) = driver(true);
12234        mouse(&mut d, 20.0, 10.0);
12235        assert_eq!(d.cursor_shown(), Cursor::Pointer, "the inner region's");
12236        mouse(&mut d, 200.0, 150.0);
12237        assert_eq!(d.cursor_shown(), Cursor::Grab, "the outer region's");
12238        mouse(&mut d, 350.0, 150.0);
12239        assert_eq!(d.cursor_shown(), Cursor::Default, "no region");
12240        mouse(&mut d, 100.0, 420.0);
12241        assert_eq!(d.cursor_shown(), Cursor::Text, "the field's own");
12242    }
12243}
12244
12245/// The pointer's raw events on a [`Listener`](frus_widgets::Listener), driven through the
12246/// shell (milestone 586).
12247#[cfg(test)]
12248mod listener_tests {
12249    use super::testing::Driver;
12250    use crate::{Application, Command};
12251    use frus_widgets::{
12252        Button, Container, Flex, Listener, ListenerEvent, ListenerEventKind as Raw, Point,
12253        SingleChildScrollView, Theme, Widget,
12254    };
12255    use std::cell::RefCell;
12256    use std::rc::Rc;
12257
12258    const W: f32 = 400.0;
12259    const H: f32 = 400.0;
12260
12261    #[derive(Clone, Debug, PartialEq)]
12262    enum Msg {
12263        Heard(&'static str, Raw, Point),
12264        Pressed,
12265    }
12266
12267    /// In a scroll taller than the window: an outer listener (300×300 at the top, 40 px down)
12268    /// holding an inner one (100×100 at its top-left) with a button in it.
12269    struct Page {
12270        heard: Rc<RefCell<Vec<Msg>>>,
12271    }
12272
12273    fn listen(name: &'static str) -> impl Fn(ListenerEvent) -> Option<Msg> {
12274        move |event| Some(Msg::Heard(name, event.kind, event.local))
12275    }
12276
12277    impl Application for Page {
12278        type Message = Msg;
12279
12280        fn update(&mut self, message: Msg) -> Command<Msg> {
12281            self.heard.borrow_mut().push(message);
12282            Command::none()
12283        }
12284
12285        fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
12286            let inner = Listener::new(
12287                Container::new()
12288                    .width(100.0)
12289                    .height(100.0)
12290                    .child(Button::new("Press").on_press(Msg::Pressed)),
12291                listen("inner"),
12292            );
12293            let outer = Listener::new(
12294                Container::new().width(300.0).height(300.0).child(inner),
12295                listen("outer"),
12296            );
12297            Box::new(
12298                SingleChildScrollView::new().width(W).height(H).child(
12299                    Flex::column()
12300                        .child(Container::new().width(300.0).height(40.0))
12301                        .child(outer)
12302                        .child(Container::new().width(300.0).height(1000.0)),
12303                ),
12304            )
12305        }
12306    }
12307
12308    fn driver() -> (Driver<Page>, Rc<RefCell<Vec<Msg>>>) {
12309        let heard = Rc::new(RefCell::new(Vec::new()));
12310        let mut d = Driver::new(
12311            Page {
12312                heard: heard.clone(),
12313            },
12314            W,
12315            H,
12316        );
12317        d.run(0.2);
12318        (d, heard)
12319    }
12320
12321    fn take(heard: &Rc<RefCell<Vec<Msg>>>) -> Vec<Msg> {
12322        std::mem::take(&mut *heard.borrow_mut())
12323    }
12324
12325    /// The events `name` heard, in order.
12326    fn of(heard: &[Msg], name: &str) -> Vec<(Raw, Point)> {
12327        heard
12328            .iter()
12329            .filter_map(|m| match m {
12330                Msg::Heard(n, kind, at) if *n == name => Some((*kind, *at)),
12331                _ => None,
12332            })
12333            .collect()
12334    }
12335
12336    fn scroll_y(d: &Driver<Page>) -> f32 {
12337        d.offsets().first().map_or(0.0, |(_, offset)| offset.1)
12338    }
12339
12340    /// **A press is heard by every listener under it, the innermost first**, each in its own
12341    /// coordinates.
12342    #[test]
12343    fn a_press_is_heard_innermost_first() {
12344        let (mut d, heard) = driver();
12345        d.press(Point::new(250.0, 90.0));
12346        d.run(0.02);
12347        let h = take(&heard);
12348        assert_eq!(of(&h, "outer"), vec![(Raw::Down, Point::new(250.0, 50.0))]);
12349        assert!(of(&h, "inner").is_empty(), "outside the inner one: {h:?}");
12350        d.release(Point::new(250.0, 90.0));
12351        d.run(0.02);
12352        take(&heard);
12353
12354        d.press(Point::new(60.0, 80.0));
12355        d.run(0.02);
12356        let h = take(&heard);
12357        let order: Vec<_> = h
12358            .iter()
12359            .filter_map(|m| match m {
12360                Msg::Heard(n, Raw::Down, _) => Some(*n),
12361                _ => None,
12362            })
12363            .collect();
12364        assert_eq!(order, vec!["inner", "outer"], "{h:?}");
12365        assert_eq!(of(&h, "inner"), vec![(Raw::Down, Point::new(60.0, 40.0))]);
12366        d.release(Point::new(60.0, 80.0));
12367        d.run(0.02);
12368    }
12369
12370    /// **The pointer that went down inside is followed wherever it goes**, out of the box,
12371    /// until it lifts — and the listener does not keep the page from scrolling under it.
12372    #[test]
12373    fn the_pointer_is_followed_out_and_the_page_still_scrolls() {
12374        let (mut d, heard) = driver();
12375        let from = Point::new(250.0, 300.0);
12376        d.press(from);
12377        d.run(0.02);
12378        for step in 1..=8 {
12379            d.move_to(Point::new(250.0, 300.0 - 35.0 * step as f32));
12380            d.run(0.02);
12381        }
12382        d.release(Point::new(250.0, 20.0));
12383        d.run(0.02);
12384        let h = take(&heard);
12385        let outer = of(&h, "outer");
12386        assert_eq!(outer.first().map(|e| e.0), Some(Raw::Down), "{outer:?}");
12387        assert_eq!(outer.last().map(|e| e.0), Some(Raw::Up), "{outer:?}");
12388        assert!(
12389            outer.iter().filter(|e| e.0 == Raw::Move).count() >= 7,
12390            "{outer:?}"
12391        );
12392        assert!(scroll_y(&d) > 50.0, "the page scrolled: {}", scroll_y(&d));
12393    }
12394
12395    /// **A pointer that leaves the box is still heard**, outside it, until it lifts — a mouse,
12396    /// which scrolls nothing, carried right out of both listeners.
12397    #[test]
12398    fn a_pointer_that_leaves_is_still_heard() {
12399        let (mut d, heard) = driver();
12400        d.press_mouse(Point::new(60.0, 80.0));
12401        d.run(0.02);
12402        d.move_mouse(Point::new(390.0, 390.0));
12403        d.run(0.02);
12404        d.release_mouse(Point::new(390.0, 390.0));
12405        d.run(0.02);
12406        let h = take(&heard);
12407        let inner = of(&h, "inner");
12408        assert_eq!(
12409            inner,
12410            vec![
12411                (Raw::Down, Point::new(60.0, 40.0)),
12412                (Raw::Move, Point::new(390.0, 350.0)),
12413                (Raw::Up, Point::new(390.0, 350.0)),
12414            ],
12415            "{h:?}"
12416        );
12417        assert_eq!(of(&h, "outer").len(), 3, "{h:?}");
12418    }
12419
12420    /// **A button inside keeps its tap**, and the listeners hear the press and the release.
12421    #[test]
12422    fn a_button_inside_keeps_its_tap() {
12423        let (mut d, heard) = driver();
12424        let at = Point::new(30.0, 55.0);
12425        d.press(at);
12426        d.release(at);
12427        d.run(0.05);
12428        let h = take(&heard);
12429        assert!(h.contains(&Msg::Pressed), "{h:?}");
12430        let inner = of(&h, "inner");
12431        assert_eq!(
12432            inner.iter().map(|e| e.0).collect::<Vec<_>>(),
12433            vec![Raw::Down, Raw::Up]
12434        );
12435    }
12436
12437    /// **A mouse with nothing held is heard as a hover** by the listeners under it, and by no
12438    /// other.
12439    #[test]
12440    fn a_mouse_hovering_is_heard() {
12441        let (mut d, heard) = driver();
12442        d.move_mouse(Point::new(250.0, 90.0));
12443        d.run(0.02);
12444        let h = take(&heard);
12445        assert_eq!(of(&h, "outer"), vec![(Raw::Hover, Point::new(250.0, 50.0))]);
12446        assert!(of(&h, "inner").is_empty());
12447        d.move_mouse(Point::new(390.0, 390.0));
12448        d.run(0.02);
12449        assert!(take(&heard).is_empty(), "outside both");
12450    }
12451}
12452
12453#[cfg(test)]
12454mod menu_keyboard_tests {
12455    use super::testing::Driver;
12456    use crate::{Application, Command};
12457    use frus_widgets::{
12458        KeyStroke, MenuBar, MenuItem, MenuPath, Point, ShortcutKey, SubmenuButton, Theme, Widget,
12459    };
12460
12461    const W: f32 = 600.0;
12462    const H: f32 = 400.0;
12463
12464    #[derive(Clone, Debug, PartialEq)]
12465    enum Msg {
12466        Menu(MenuPath),
12467        Picked(&'static str),
12468    }
12469
12470    /// File: New, Open recent ▸ (notes.txt, todo.md), Quit.
12471    #[derive(Default)]
12472    struct Editor {
12473        menu: MenuPath,
12474        /// The last row picked.
12475        picked: Option<&'static str>,
12476    }
12477
12478    impl Application for Editor {
12479        type Message = Msg;
12480
12481        fn update(&mut self, message: Msg) -> Command<Msg> {
12482            match message {
12483                Msg::Menu(path) => self.menu = path,
12484                Msg::Picked(what) => {
12485                    self.picked = Some(what);
12486                    self.menu = MenuPath::closed();
12487                }
12488            }
12489            Command::none()
12490        }
12491
12492        fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
12493            Box::new(
12494                MenuBar::new(&self.menu, Msg::Menu).menu(
12495                    SubmenuButton::new("File")
12496                        .item(MenuItem::new("New", Msg::Picked("new")))
12497                        .submenu(
12498                            SubmenuButton::new("Open recent")
12499                                .item(MenuItem::new("notes.txt", Msg::Picked("notes")))
12500                                .item(MenuItem::new("todo.md", Msg::Picked("todo"))),
12501                        )
12502                        .item(MenuItem::new("Quit", Msg::Picked("quit"))),
12503                ),
12504            )
12505        }
12506    }
12507
12508    /// The identity of the row whose words are `label`, from what the last frame drew.
12509    fn row(d: &Driver<Editor>, label: &str) -> Option<frus_widgets::WidgetId> {
12510        let (_, rect) = d.texts().into_iter().find(|(text, _)| text == label)?;
12511        d.hit(Point::new(rect.x + 4.0, rect.y + 4.0))
12512    }
12513
12514    /// **A menu that opens gives its first row the focus**, a submenu that opens gives its
12515    /// own first row the focus, and Escape closes one level and hands the focus back.
12516    #[test]
12517    fn opening_a_menu_focuses_its_first_row_and_escape_goes_back_one_level() {
12518        let mut d = Driver::new(Editor::default(), W, H);
12519        d.run(0.1);
12520        assert_eq!(d.focused(), None);
12521
12522        d.update(Msg::Menu(MenuPath::from_indices(vec![0])));
12523        d.run(0.1);
12524        let new = row(&d, "New").expect("the menu is open");
12525        assert_eq!(d.focused(), Some(new), "the first row has the focus");
12526
12527        d.update(Msg::Menu(MenuPath::from_indices(vec![0, 1])));
12528        d.run(0.1);
12529        let notes = row(&d, "notes.txt").expect("the submenu is open");
12530        assert_eq!(d.focused(), Some(notes), "the submenu's first row has it");
12531
12532        d.escape();
12533        d.run(0.1);
12534        assert_eq!(
12535            d.app().menu,
12536            MenuPath::from_indices(vec![0]),
12537            "one level closed"
12538        );
12539        assert!(row(&d, "notes.txt").is_none());
12540        assert_eq!(d.focused(), Some(new), "and the focus is back in the menu");
12541
12542        d.escape();
12543        d.run(0.1);
12544        assert!(!d.app().menu.is_open(), "then the menu");
12545    }
12546
12547    /// **The whole menu from the keyboard**, through the shell's own handling of each key
12548    /// (milestone 608): down to the row that opens a submenu, right into it, down to its
12549    /// second row, left back out, up to the first row and Enter on it.
12550    #[test]
12551    fn the_menu_is_worked_from_the_keyboard() {
12552        let key = |d: &mut Driver<Editor>, k: ShortcutKey| {
12553            d.key(KeyStroke::new(k));
12554            d.run(0.1);
12555        };
12556        let mut d = Driver::new(Editor::default(), W, H);
12557        d.run(0.1);
12558        d.update(Msg::Menu(MenuPath::from_indices(vec![0])));
12559        d.run(0.1);
12560        let (new, recent) = (row(&d, "New").unwrap(), row(&d, "Open recent").unwrap());
12561        assert_eq!(d.focused(), Some(new));
12562
12563        key(&mut d, ShortcutKey::Down);
12564        assert_eq!(d.focused(), Some(recent), "down to the next row");
12565        key(&mut d, ShortcutKey::Right);
12566        assert_eq!(
12567            d.app().menu,
12568            MenuPath::from_indices(vec![0, 1]),
12569            "right opens it"
12570        );
12571        assert_eq!(
12572            d.focused(),
12573            row(&d, "notes.txt"),
12574            "and its first row has the focus"
12575        );
12576        key(&mut d, ShortcutKey::Down);
12577        assert_eq!(d.focused(), row(&d, "todo.md"));
12578        key(&mut d, ShortcutKey::Left);
12579        assert_eq!(
12580            d.app().menu,
12581            MenuPath::from_indices(vec![0]),
12582            "left closes it"
12583        );
12584        assert_eq!(
12585            d.focused(),
12586            Some(recent),
12587            "and the focus is back on its row"
12588        );
12589        key(&mut d, ShortcutKey::Up);
12590        assert_eq!(d.focused(), Some(new));
12591        key(&mut d, ShortcutKey::Enter);
12592        assert_eq!(d.app().picked, Some("new"), "Enter picks the row");
12593        assert!(!d.app().menu.is_open(), "and the menus close");
12594    }
12595
12596    /// **A key with modifiers held** reaches a shortcut bound to it, and the modifiers are
12597    /// let go after it: a bare key that follows is not taken for the shortcut.
12598    #[test]
12599    fn a_stroke_holds_its_modifiers_for_the_key_alone() {
12600        let mut d = Driver::new(Editor::default(), W, H);
12601        d.run(0.1);
12602        d.key(KeyStroke::new(ShortcutKey::Char('a')).ctrl());
12603        assert!(!d.shell_modifiers().0, "ctrl let go after the stroke");
12604    }
12605
12606    /// **The focus goes back to the word on the bar** when its menu closes: Tab onto File,
12607    /// Enter opens it and the focus goes into it, Escape closes it and the focus is on File
12608    /// again. A word whose wrappers changed as its menu opened was a different widget once
12609    /// it closed, and the focus had nowhere to go back to (milestone 608).
12610    #[test]
12611    fn closing_a_menu_hands_the_focus_back_to_its_word() {
12612        let key = |d: &mut Driver<Editor>, k: ShortcutKey| {
12613            d.key(KeyStroke::new(k));
12614            d.run(0.1);
12615        };
12616        let mut d = Driver::new(Editor::default(), W, H);
12617        d.run(0.1);
12618        key(&mut d, ShortcutKey::Tab);
12619        let file = d.focused().expect("Tab reached the bar");
12620        key(&mut d, ShortcutKey::Enter);
12621        assert!(d.app().menu.is_open(), "Enter opened the menu");
12622        assert_eq!(d.focused(), row(&d, "New"), "and the focus went into it");
12623        key(&mut d, ShortcutKey::Escape);
12624        assert!(!d.app().menu.is_open(), "Escape closed it");
12625        assert_eq!(d.focused(), Some(file), "and the focus is back on its word");
12626    }
12627
12628    /// **A row that is not the first gets the focus back too.** With the focus on Quit, the
12629    /// pointer opens the submenu beside Open recent, which takes the focus; Escape closes it,
12630    /// and the focus is on Quit again. The other rows of a menu gained a hover region when a
12631    /// submenu opened, which moved them, and the focus fell to the first row instead, the
12632    /// one that takes it when nothing else does (milestone 608).
12633    #[test]
12634    fn a_submenu_closing_hands_the_focus_back_to_any_row() {
12635        let key = |d: &mut Driver<Editor>, k: ShortcutKey| {
12636            d.key(KeyStroke::new(k));
12637            d.run(0.1);
12638        };
12639        let mut d = Driver::new(Editor::default(), W, H);
12640        d.run(0.1);
12641        d.update(Msg::Menu(MenuPath::from_indices(vec![0])));
12642        d.run(0.1);
12643        key(&mut d, ShortcutKey::Down);
12644        key(&mut d, ShortcutKey::Down);
12645        let quit = row(&d, "Quit");
12646        assert_eq!(d.focused(), quit, "down twice reaches Quit");
12647        // What the pointer moving onto Open recent sends.
12648        d.update(Msg::Menu(MenuPath::from_indices(vec![0, 1])));
12649        d.run(0.1);
12650        assert_eq!(d.focused(), row(&d, "notes.txt"));
12651        key(&mut d, ShortcutKey::Escape);
12652        assert_eq!(d.app().menu, MenuPath::from_indices(vec![0]));
12653        assert_eq!(d.focused(), quit, "the focus is back on Quit");
12654    }
12655}
12656
12657/// **A right-click on a field opens its selection bar**, the desktop's context menu for the
12658/// four things the bar holds (milestone 568), driven through the shell's own path: the
12659/// milestone had built it and not run it.
12660#[cfg(test)]
12661mod right_click_bar_tests {
12662    use super::testing::Driver;
12663    use crate::{Application, Command};
12664    use frus_widgets::{Point, TextField, Theme, Widget};
12665
12666    struct Form;
12667
12668    impl Application for Form {
12669        type Message = ();
12670
12671        fn update(&mut self, _message: ()) -> Command<()> {
12672            Command::none()
12673        }
12674
12675        fn view(&self, _theme: &Theme) -> Box<dyn Widget<()>> {
12676            Box::new(TextField::new("some words in a field"))
12677        }
12678    }
12679
12680    #[test]
12681    fn a_right_click_on_a_field_opens_its_bar() {
12682        let mut d = Driver::new(Form, 500.0, 300.0);
12683        d.run(0.1);
12684        let shown = |d: &Driver<Form>, label: &str| d.texts().iter().any(|(t, _)| t == label);
12685        assert!(!shown(&d, "Select all"), "no bar before the click");
12686        let (_, words) = d
12687            .texts()
12688            .into_iter()
12689            .find(|(t, _)| t.contains("some words"))
12690            .expect("the field's words");
12691        d.secondary_click(Point::new(words.x + 20.0, words.y + words.height * 0.5));
12692        d.run(0.1);
12693        // Only what applies: nothing is selected, so nothing to cut or copy; and the
12694        // driver's clipboard is its own and empty, so nothing to paste — it no longer
12695        // follows the machine's (a CI runner's had text on it).
12696        assert!(shown(&d, "Select all"), "the bar is open: {:?}", d.texts());
12697        assert!(!shown(&d, "Copy") && !shown(&d, "Cut"));
12698        assert!(
12699            !shown(&d, "Paste"),
12700            "an empty clipboard offers nothing to paste"
12701        );
12702    }
12703
12704    /// The same field, under a theme that follows Linux.
12705    struct DesktopForm;
12706
12707    impl Application for DesktopForm {
12708        type Message = ();
12709
12710        fn update(&mut self, _message: ()) -> Command<()> {
12711            Command::none()
12712        }
12713
12714        fn view(&self, _theme: &Theme) -> Box<dyn Widget<()>> {
12715            Box::new(TextField::new("some words in a field"))
12716        }
12717
12718        fn theme(&self) -> Theme {
12719            Theme::default().with_platform(frus_widgets::TargetPlatform::Linux)
12720        }
12721    }
12722
12723    /// **A right-click opens the menu where the pointer is** (milestone 618), as the
12724    /// reference's context menu goes to the last secondary tap: its first row's words sit
12725    /// 20 px after the pointer, on the pointer's line.
12726    #[test]
12727    fn a_right_click_opens_the_menu_at_the_pointer() {
12728        let mut d = Driver::new(DesktopForm, 500.0, 300.0);
12729        d.run(0.1);
12730        let (_, words) = d
12731            .texts()
12732            .into_iter()
12733            .find(|(t, _)| t.contains("some words"))
12734            .expect("the field's words");
12735        let click = Point::new(words.x + 20.0, words.y + words.height * 0.5);
12736        d.secondary_click(click);
12737        d.run(0.1);
12738        let (_, row) = d
12739            .texts()
12740            .into_iter()
12741            .find(|(t, _)| t == "Select all")
12742            .expect("the menu is open");
12743        assert_eq!(row.x, click.x + 20.0, "the menu starts at the pointer");
12744        assert!(
12745            row.y >= click.y && row.y < click.y + 36.0,
12746            "its first row is on the pointer's line: {row:?} {click:?}"
12747        );
12748    }
12749
12750    /// **On a desktop, the menu's Select all puts the menu away** (milestone 625), as the
12751    /// reference's field does on macOS, Linux and Windows. A test runs on one of the three,
12752    /// whose system — not the theme — decides. The selection stays: the menu opened again
12753    /// over it offers Copy.
12754    #[test]
12755    fn a_desktop_s_select_all_puts_the_menu_away() {
12756        assert!(frus_widgets::default_target_platform().is_desktop());
12757        let mut d = Driver::new(DesktopForm, 500.0, 300.0);
12758        d.run(0.1);
12759        let shown = |d: &Driver<DesktopForm>, label: &str| {
12760            d.texts()
12761                .into_iter()
12762                .find(|(t, _)| t == label)
12763                .map(|(_, r)| r)
12764        };
12765        let words = shown(&d, "some words in a field").expect("the field's words");
12766        let click = Point::new(words.x + 20.0, words.y + words.height * 0.5);
12767        d.secondary_click(click);
12768        d.run(0.1);
12769        let row = shown(&d, "Select all").expect("the menu is open");
12770        let row = Point::new(row.x + 10.0, row.y + row.height * 0.5);
12771        d.press(row);
12772        d.release(row);
12773        d.run(0.1);
12774        assert!(shown(&d, "Select all").is_none(), "the menu is gone");
12775        assert!(shown(&d, "Copy").is_none(), "all of it: {:?}", d.texts());
12776        d.secondary_click(click);
12777        d.run(0.1);
12778        assert!(shown(&d, "Copy").is_some(), "the text is still selected");
12779    }
12780}
12781
12782#[cfg(test)]
12783mod scroll_behavior_tests {
12784    use super::testing::Driver;
12785    use crate::{Application, Command};
12786    use frus_widgets::{
12787        Container, Point, ScrollBehavior, ScrollConfiguration, SingleChildScrollView,
12788        TargetPlatform, Theme, VelocityStrategy, Widget,
12789    };
12790
12791    /// A tall page in a scroll view that follows iOS, in an application whose theme says
12792    /// Android.
12793    struct Page;
12794
12795    impl Application for Page {
12796        type Message = ();
12797
12798        fn update(&mut self, _message: ()) -> Command<()> {
12799            Command::none()
12800        }
12801
12802        fn view(&self, _theme: &Theme) -> Box<dyn Widget<()>> {
12803            Box::new(ScrollConfiguration::new(
12804                ScrollBehavior::material().with_platform(TargetPlatform::Ios),
12805                SingleChildScrollView::new()
12806                    .width(300.0)
12807                    .height(400.0)
12808                    .child(Container::new().width(300.0).height(2000.0)),
12809            ))
12810        }
12811
12812        fn theme(&self) -> Theme {
12813            Theme::default().with_platform(TargetPlatform::Android)
12814        }
12815    }
12816
12817    /// **A drag on a scroll area reads its fling the way the area's behaviour does**
12818    /// (milestone 616), not the application's: the reference's scrollable builds its drag
12819    /// recognizer from its own configuration.
12820    #[test]
12821    fn a_drag_on_an_area_reads_its_fling_the_area_s_way() {
12822        let mut d = Driver::new(Page, 300.0, 400.0);
12823        d.run(0.2);
12824        d.press(Point::new(150.0, 200.0));
12825        assert_eq!(
12826            d.fling_strategy(),
12827            VelocityStrategy::RecentAverage(frus_widgets::BOUNCING_FLING_WEIGHTS)
12828        );
12829        d.release(Point::new(150.0, 200.0));
12830    }
12831}
12832
12833/// A switch's thumb dragged through the shell (milestone 638): it follows the finger, flips
12834/// when let go past half way and goes back when not, and a tap still flips it.
12835#[cfg(test)]
12836mod switch_drag_tests {
12837    use super::testing::Driver;
12838    use crate::{Application, Command};
12839    use frus_widgets::{Container, Point, Rect, Switch, Theme, Widget};
12840
12841    const W: f32 = 300.0;
12842    const H: f32 = 200.0;
12843
12844    #[derive(Clone, Debug, PartialEq)]
12845    enum Msg {
12846        Set(bool),
12847    }
12848
12849    #[derive(Default)]
12850    struct Page {
12851        on: bool,
12852        heard: Vec<Msg>,
12853    }
12854
12855    impl Application for Page {
12856        type Message = Msg;
12857
12858        fn update(&mut self, message: Msg) -> Command<Msg> {
12859            let Msg::Set(on) = message;
12860            self.on = on;
12861            self.heard.push(message);
12862            Command::none()
12863        }
12864
12865        fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
12866            Box::new(
12867                Container::new()
12868                    .width(W)
12869                    .height(H)
12870                    .child(Switch::new(self.on).on_toggle(Msg::Set)),
12871            )
12872        }
12873    }
12874
12875    fn driver() -> Driver<Page> {
12876        let mut d = Driver::new(Page::default(), W, H);
12877        d.run(0.2);
12878        d
12879    }
12880
12881    /// The track and the thumb the last frame drew: the 52 × 32 rectangle, and the last one.
12882    fn track_and_thumb(d: &Driver<Page>) -> (Rect, Rect) {
12883        let (ui, _) = d.frame_parts().expect("a frame");
12884        let rects: Vec<Rect> = ui
12885            .scene()
12886            .primitives()
12887            .iter()
12888            .filter_map(|p| match p {
12889                frus_widgets::Primitive::Rect { rect, .. } => Some(*rect),
12890                _ => None,
12891            })
12892            .collect();
12893        let track = *rects
12894            .iter()
12895            .find(|r| r.width == 52.0 && r.height == 32.0)
12896            .expect("the track");
12897        (track, *rects.last().expect("the thumb"))
12898    }
12899
12900    /// How far along its travel the thumb's centre is, 0 at the start and 1 at the end.
12901    fn along(d: &Driver<Page>) -> f32 {
12902        let (track, thumb) = track_and_thumb(d);
12903        (thumb.x + thumb.width * 0.5 - track.x - 16.0) / 20.0
12904    }
12905
12906    /// A finger on the thumb, moved by `dx` in eight steps; lifted when `lift`.
12907    fn drag(d: &mut Driver<Page>, dx: f32, lift: bool) {
12908        let (track, _) = track_and_thumb(d);
12909        let from = Point::new(track.x + 16.0, track.y + 16.0);
12910        d.press(from);
12911        d.run(0.02);
12912        for step in 1..=8 {
12913            d.move_to(Point::new(from.x + dx * step as f32 / 8.0, from.y));
12914            d.run(0.02);
12915        }
12916        if lift {
12917            d.release(Point::new(from.x + dx, from.y));
12918            d.run(0.02);
12919        }
12920    }
12921
12922    /// **The thumb is where the finger is** while it is down, past the slop: placed, not
12923    /// animated, and the switch has said nothing yet.
12924    #[test]
12925    fn the_thumb_follows_the_finger() {
12926        let mut d = driver();
12927        assert!(along(&d).abs() < 1e-3, "off at rest");
12928        drag(&mut d, 40.0, false);
12929        let now = along(&d);
12930        assert!(now > 0.3 && now <= 1.0, "moved with the finger: {now}");
12931        assert!(d.app().heard.is_empty(), "nothing said while held");
12932        // Held, it stays: frames pass and the thumb does not go back.
12933        d.run(0.5);
12934        assert!((along(&d) - now).abs() < 1e-3, "held where it was put");
12935    }
12936
12937    /// **Let go past half way, it flips** — and the release is not also a tap, which would
12938    /// flip it straight back.
12939    #[test]
12940    fn let_go_past_half_way_it_flips() {
12941        let mut d = driver();
12942        drag(&mut d, 60.0, true);
12943        assert_eq!(d.app().heard, vec![Msg::Set(true)]);
12944        d.run(0.5);
12945        assert!((along(&d) - 1.0).abs() < 1e-3, "settled on");
12946    }
12947
12948    /// **Short of half way, it goes back**, and says nothing.
12949    #[test]
12950    fn let_go_short_of_half_way_it_goes_back() {
12951        let mut d = driver();
12952        // Out past the slop, then most of the way back: a finger that thought better of it.
12953        drag(&mut d, 30.0, false);
12954        let (track, _) = track_and_thumb(&d);
12955        let back = Point::new(track.x + 16.0 + 15.0, track.y + 16.0);
12956        d.move_to(back);
12957        d.run(0.02);
12958        assert!(along(&d) > 0.0, "it had moved");
12959        assert!(along(&d) < 0.5, "but is short of half way: {}", along(&d));
12960        d.release(back);
12961        d.run(0.5);
12962        assert!(d.app().heard.is_empty(), "nothing said");
12963        assert!(along(&d).abs() < 1e-3, "back off: {}", along(&d));
12964    }
12965
12966    /// **A tap still flips it**: a drag begins only past the slop.
12967    #[test]
12968    fn a_tap_still_flips_it() {
12969        let mut d = driver();
12970        let (track, _) = track_and_thumb(&d);
12971        let at = Point::new(track.x + 16.0, track.y + 16.0);
12972        d.press(at);
12973        d.release(at);
12974        d.run(0.5);
12975        assert_eq!(d.app().heard, vec![Msg::Set(true)]);
12976        assert!((along(&d) - 1.0).abs() < 1e-3);
12977    }
12978}