Skip to main content

frus_shell/
app.rs

1//! The generic driver: implements [`winit::application::ApplicationHandler`] for
2//! any [`Application`].
3//!
4//! The framework owns the window, the renderer, the [`Runtime`] — the retained
5//! interaction state: hover, focus, scroll, editing, animations — input routing by
6//! hit test, dragging (scrollbars, selection, handles, the back gesture) and the
7//! animation clock. The application supplies only `update`, `view` and friends.
8
9use std::collections::HashMap;
10use std::sync::Arc;
11
12use web_time::Instant;
13
14use frus_gpu::Renderer;
15use frus_widgets::{
16    build_deferred, build_ui, collect_ids, find_by_key, find_path, find_widget, focus_changes,
17    nearest_reorder_slot, reflow_reorder_cards, reflow_reorder_columns, reorder_drop_after,
18    reorder_siblings, reorderable_owners, subtree_ids, Accessibility, Brightness, Color,
19    Cursor as UiCursor, Edit, EditKind, EditSnapshot, FocusDirection, Insets, Key, KeyResponse,
20    KeyStroke, MediaQuery, Point, Primitive, Rect, RegionPoint, RegionSelection, ReorderAxis,
21    Runtime, Scene, ScrollTo, Scrollable, SheetTo, ShortcutKey, Size, TextStop, Theme, ToolbarMark,
22    Ui, VelocityEstimate, VelocityTracker, Widget, WidgetId, WindowInsets,
23};
24use winit::application::ApplicationHandler;
25use winit::event::{
26    ElementState, MouseButton, MouseScrollDelta, StartCause, TouchPhase, WindowEvent,
27};
28use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoopProxy};
29use winit::keyboard::{Key as WinitKey, KeyCode, NamedKey, PhysicalKey};
30use winit::window::{CursorIcon, Window, WindowId};
31
32use crate::application::{Application, Lifecycle};
33
34/// Everything the platform reports about its user, in one walk.
35#[derive(Clone, Debug, PartialEq)]
36pub(crate) struct PlatformSettings {
37    /// The system's *Font size* slider. `1.0` is normal; Android's own slider reaches
38    /// 1.3, and the accessibility sizes go further.
39    pub text_scaler: f32,
40    /// Whether the system is currently showing a dark interface.
41    pub brightness: Brightness,
42    /// The accessibility settings, as far as they could be read.
43    pub accessibility: Accessibility,
44    /// **The reader's preferred languages**, best first, as the platform reports them.
45    ///
46    /// Empty where the platform said nothing — which is a real answer and not a failure:
47    /// the resolution treats it as *no preference* and hands back the application's own
48    /// first choice, exactly as the reference does (`app.dart:153`).
49    pub locales: Vec<frus_widgets::Locale>,
50}
51
52impl Default for PlatformSettings {
53    /// What a platform that reports nothing looks like: a user who has changed nothing.
54    fn default() -> Self {
55        Self {
56            text_scaler: 1.0,
57            brightness: Brightness::Light,
58            accessibility: Accessibility::NONE,
59            locales: Vec::new(),
60        }
61    }
62}
63use crate::gesture::{PointerEvent, PointerKind, PressRecognizer};
64
65/// What a key asks of the clipboard.
66#[derive(Clone, Copy, Debug, PartialEq, Eq)]
67enum ClipCommand {
68    Copy,
69    Cut,
70    Paste,
71}
72
73/// The clipboard command a key press means, if any: Ctrl+C/X/V, and the Copy, Cut and
74/// Paste keys a keyboard may have for them (milestone 509).
75///
76/// Both halves of the key are asked. A desktop keyboard's Copy key arrives as a named
77/// logical key; Android's arrives by **physical code only** — winit gives it no logical
78/// key at all — so a check of the logical key alone would ignore it on the one platform
79/// that has it as a keycode of its own.
80fn clipboard_command(logical: &WinitKey, physical: PhysicalKey, ctrl: bool) -> Option<ClipCommand> {
81    match logical {
82        WinitKey::Named(NamedKey::Copy) => return Some(ClipCommand::Copy),
83        WinitKey::Named(NamedKey::Cut) => return Some(ClipCommand::Cut),
84        WinitKey::Named(NamedKey::Paste) => return Some(ClipCommand::Paste),
85        WinitKey::Character(c) if ctrl => {
86            if c.eq_ignore_ascii_case("c") {
87                return Some(ClipCommand::Copy);
88            }
89            if c.eq_ignore_ascii_case("x") {
90                return Some(ClipCommand::Cut);
91            }
92            if c.eq_ignore_ascii_case("v") {
93                return Some(ClipCommand::Paste);
94            }
95        }
96        _ => {}
97    }
98    match physical {
99        PhysicalKey::Code(KeyCode::Copy) => Some(ClipCommand::Copy),
100        PhysicalKey::Code(KeyCode::Cut) => Some(ClipCommand::Cut),
101        PhysicalKey::Code(KeyCode::Paste) => Some(ClipCommand::Paste),
102        _ => None,
103    }
104}
105
106/// Whether a key press asks for the **selection bar**: the context-menu key, which a
107/// keyboard may have between Alt Gr and Ctrl, or Shift+F10, which every desktop toolkit reads
108/// the same way (milestone 568). What lets someone with no pointer — and so no hold or
109/// right-click — open it.
110fn opens_selection_bar(logical: &WinitKey, shift: bool) -> bool {
111    match logical {
112        WinitKey::Named(NamedKey::ContextMenu) => true,
113        WinitKey::Named(NamedKey::F10) => shift,
114        _ => false,
115    }
116}
117
118/// Whether `widget` takes typing, and so wants the software keyboard while it has focus.
119///
120/// Asked of the widget (milestone 510). It used to be asked of a **caret hit test** at
121/// the corner of a field one pixel wide, and a field with a clickable suffix — the ×
122/// a field shows once it holds text — answered that no caret goes there, since that
123/// pixel is its clear button. So the first letter typed into such a field closed the
124/// keyboard and ended its composition, and the keyboard's next update was written
125/// beside the first: `F`, then `FFr`.
126///
127/// A field that can only be **read** — a read-only one, a selectable paragraph — is a text field
128/// with a caret and a selection and no use for a keyboard, which would slide up over the very
129/// words its reader has come to select.
130fn wants_keyboard<M>(widget: &dyn Widget<M>) -> bool {
131    is_text_field(widget) && widget.focusable() && widget.takes_typing()
132}
133
134/// Whether `widget` is a **text field**: what takes the left and right arrows for its caret
135/// instead of leaving them to focus navigation.
136///
137/// Asked of the widget, and not of a caret hit test at the corner of a field one pixel wide —
138/// which is how this was decided until milestone 570. A field that holds text shows a clear
139/// button, and the button covers that pixel, so the probe answered "no caret goes here" and the
140/// **left and right arrows moved the focus to the next control** instead of the caret: in any
141/// field with something in it, on every platform, and never in an empty one. It is the same
142/// trap milestone 510 had already got the keyboard out of, in the one place it was not looked for.
143fn is_text_field<M>(widget: &dyn Widget<M>) -> bool {
144    widget.text_value().is_some()
145}
146
147/// How far apart two presses may be and still be a double click on words in a selection area, in
148/// logical pixels.
149const REGION_DOUBLE_CLICK_SLOP: f32 = 8.0;
150
151/// How long a first tap waits for a second one to make a double tap: the reference's
152/// `kDoubleTapTimeout`.
153const DOUBLE_TAP_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(300);
154
155/// How often the system's cursor is read while files from outside the application are
156/// held over the window, where it can be read (milestone 695): the window hears nothing
157/// else of their movement. Thirty times a second keeps the highlight on the pointer.
158const FILE_DRAG_POLL: std::time::Duration = std::time::Duration::from_millis(33);
159
160/// How far apart two taps may be and still be a double tap, in logical pixels: the reference's
161/// `kDoubleTapSlop`.
162const DOUBLE_TAP_SLOP: f32 = 100.0;
163
164/// The fastest a drag's end velocity is reported, in logical pixels per second: the
165/// reference's `kMaxFlingVelocity`.
166const MAX_PAN_VELOCITY: f32 = 8000.0;
167
168/// A first tap on something that also takes a double tap, waiting to see whether a second
169/// one follows (milestone 581).
170struct PendingTap<M> {
171    /// What was tapped.
172    id: WidgetId,
173    /// When it was tapped.
174    at: Instant,
175    /// Where it was tapped.
176    position: Point,
177    /// The tap's own message, sent if no second tap comes in time.
178    single: Option<M>,
179}
180
181/// Whether a field's **Select all**, pressed on its bar, puts the bar away: on macOS, Linux
182/// and Windows it does, its work done; on Android, iOS and Fuchsia the bar stays, offering
183/// what can be done with everything selected (`editable_text.dart:2914`, milestone 625).
184///
185/// The **system's** platform decides, not the theme's, as in the reference: it is how the
186/// system's own menus behave, not how the application looks.
187fn select_all_hides_bar(platform: frus_widgets::TargetPlatform) -> bool {
188    platform.is_desktop()
189}
190
191/// The clipboard: `arboard` on the desktop platforms, the platform's own on Android
192/// (`ClipboardManager`, through the bundled dex — milestone 509, #22), the browser's
193/// asynchronous Clipboard API on the Web (milestone 526, #17), and a no-op on iOS
194/// (`arboard` does not compile there and is not a dependency).
195/// The stub is gated on what is *not* implemented rather than on a list of the other
196/// platforms: that is what makes adding a target never leave this type undefined.
197/// One uniform API, so the driver's body stays free of `cfg`.
198///
199/// **A paste is asked for, and answered.** The Web's read is a promise — it may prompt
200/// the reader, be refused, or take its time — and the other platforms' reads are
201/// immediate. So `Clipboard::paste` names the field that asked and either answers at
202/// once or answers later through `Clipboard::take_answered`, which the driver drains
203/// every frame; both answers land through the same rule, `Pasted::lands`. Nothing on a
204/// synchronous platform pretends to wait, and no widget knows which kind it is on: the
205/// shell, not the field, holds the clipboard.
206mod clip {
207    use frus_widgets::WidgetId;
208
209    /// What the clipboard answered to a paste: its text, and the field that asked.
210    #[derive(Clone, Debug, PartialEq, Eq)]
211    pub struct Pasted {
212        pub into: WidgetId,
213        pub text: String,
214    }
215
216    impl Pasted {
217        /// Whether this paste goes into its field: only while that field still has the
218        /// focus — on the Web the answer comes back after the key press, and the reader
219        /// may have moved on to another field, or to none — and only when there is text.
220        /// An empty clipboard is nothing to paste, not an instruction to delete the
221        /// selection; the browser answers a clipboard holding no text with `""`.
222        pub fn lands(&self, focused: Option<WidgetId>) -> bool {
223            focused == Some(self.into) && !self.text.is_empty()
224        }
225    }
226
227    /// The platforms whose read is immediate answer a paste on the spot.
228    #[cfg(not(web))]
229    impl Clipboard {
230        pub fn paste(
231            &mut self,
232            into: WidgetId,
233            _wake: Option<&std::sync::Arc<winit::window::Window>>,
234        ) -> Option<Pasted> {
235            self.get_text().map(|text| Pasted { into, text })
236        }
237        pub fn take_answered(&mut self) -> Option<Pasted> {
238            None
239        }
240    }
241
242    #[cfg(desktop)]
243    pub enum Clipboard {
244        /// The system's, through `arboard`.
245        System(Option<arboard::Clipboard>),
246        /// One of its own, in memory: what a shell with no window holds (the test
247        /// driver's). The system's is shared by everything on the machine and, on macOS,
248        /// is not to be touched from several threads at once — and tests run on several.
249        /// Only there, so a build without it has no variant nothing makes.
250        #[cfg(any(test, feature = "testing"))]
251        Memory(Option<String>),
252    }
253
254    #[cfg(desktop)]
255    impl Clipboard {
256        pub fn new() -> Self {
257            Self::System(arboard::Clipboard::new().ok())
258        }
259        /// Only a shell with no window — the test driver — holds one.
260        #[cfg(any(test, feature = "testing"))]
261        pub fn in_memory() -> Self {
262            Self::Memory(None)
263        }
264        pub fn get_text(&mut self) -> Option<String> {
265            match self {
266                Self::System(system) => system.as_mut().and_then(|c| c.get_text().ok()),
267                #[cfg(any(test, feature = "testing"))]
268                Self::Memory(text) => text.clone(),
269            }
270        }
271        pub fn has_text(&mut self) -> bool {
272            self.get_text().is_some_and(|text| !text.is_empty())
273        }
274        pub fn set_text(&mut self, text: String) {
275            match self {
276                Self::System(system) => {
277                    if let Some(c) = system.as_mut() {
278                        let _ = c.set_text(text);
279                    }
280                }
281                #[cfg(any(test, feature = "testing"))]
282                Self::Memory(held) => *held = Some(text),
283            }
284        }
285    }
286
287    /// Elsewhere a shell with no window keeps the platform's clipboard: no test runs there.
288    #[cfg(all(not(desktop), any(test, feature = "testing")))]
289    impl Clipboard {
290        pub fn in_memory() -> Self {
291            Self::new()
292        }
293    }
294
295    #[cfg(android)]
296    pub struct Clipboard;
297
298    #[cfg(android)]
299    impl Clipboard {
300        pub fn new() -> Self {
301            Self
302        }
303        pub fn get_text(&mut self) -> Option<String> {
304            crate::android_clipboard::get_text()
305        }
306        pub fn has_text(&mut self) -> bool {
307            self.get_text().is_some_and(|text| !text.is_empty())
308        }
309        pub fn set_text(&mut self, text: String) {
310            crate::android_clipboard::set_text(&text);
311        }
312    }
313
314    /// The pastes a clipboard has answered later and the driver has not taken yet, in the
315    /// order the answers came — which need not be the order they were asked in.
316    ///
317    /// Held by the Web's clipboard; kept out of its `cfg` so that it is tested natively.
318    #[cfg(any(web, test))]
319    #[derive(Default)]
320    pub struct Answers(std::rc::Rc<std::cell::RefCell<std::collections::VecDeque<Pasted>>>);
321
322    #[cfg(any(web, test))]
323    impl Answers {
324        /// What answers a paste into `into` once the text is there: it queues the paste,
325        /// then calls `wake`, so that a frame comes to take it.
326        pub fn answer_for(
327            &self,
328            into: WidgetId,
329            wake: impl FnOnce() + 'static,
330        ) -> impl FnOnce(String) + 'static {
331            let queue = std::rc::Rc::clone(&self.0);
332            move |text| {
333                queue.borrow_mut().push_back(Pasted { into, text });
334                wake();
335            }
336        }
337
338        /// The oldest answer not taken yet.
339        pub fn take(&self) -> Option<Pasted> {
340            self.0.borrow_mut().pop_front()
341        }
342    }
343
344    /// The Web: a read is started by the key press and answered by the browser later,
345    /// into a queue the driver drains; the window is asked for a frame so that it does.
346    #[cfg(web)]
347    pub struct Clipboard {
348        answered: Answers,
349    }
350
351    #[cfg(web)]
352    impl Clipboard {
353        pub fn new() -> Self {
354            Self {
355                answered: Answers::default(),
356            }
357        }
358        pub fn set_text(&mut self, text: String) {
359            crate::web_clipboard::set_text(&text);
360        }
361        /// The browser's clipboard cannot be read without asking, and asking is a promise
362        /// that may prompt the reader: so a bar opening cannot know, and offers Paste.
363        pub fn has_text(&mut self) -> bool {
364            true
365        }
366        pub fn paste(
367            &mut self,
368            into: WidgetId,
369            wake: Option<&std::sync::Arc<winit::window::Window>>,
370        ) -> Option<Pasted> {
371            let wake = wake.cloned();
372            crate::web_clipboard::get_text(self.answered.answer_for(into, move || {
373                if let Some(window) = wake {
374                    window.request_redraw();
375                }
376            }));
377            None
378        }
379        pub fn take_answered(&mut self) -> Option<Pasted> {
380            self.answered.take()
381        }
382    }
383
384    #[cfg(not(any(desktop, android, web)))]
385    pub struct Clipboard;
386
387    #[cfg(not(any(desktop, android, web)))]
388    impl Clipboard {
389        pub fn new() -> Self {
390            Self
391        }
392        pub fn get_text(&mut self) -> Option<String> {
393            None
394        }
395        pub fn has_text(&mut self) -> bool {
396            false
397        }
398        pub fn set_text(&mut self, _text: String) {}
399    }
400}
401
402/// Browser timers, for the Web: the counterpart of the native subscriptions'
403/// `recv_timeout` thread. A **retained** `setInterval`, whose **drop** calls
404/// `clearInterval` and releases the closure. That is how an `every` subscription
405/// ticks on the Web, where there is no background thread.
406#[cfg(web)]
407mod web_timer {
408    use wasm_bindgen::prelude::Closure;
409    use wasm_bindgen::JsCast;
410
411    /// An active `setInterval`: as long as this handle lives the callback is called
412    /// back every `ms`; dropping it cancels that.
413    pub(crate) struct Interval {
414        id: i32,
415        // The JS closure must live as long as the interval does.
416        _closure: Closure<dyn FnMut()>,
417    }
418
419    impl Interval {
420        /// Schedules `f` every `ms` milliseconds, 1 ms at the least. `None` when there
421        /// is no window, that is, in a DOM-less context.
422        pub(crate) fn new(ms: i32, f: impl FnMut() + 'static) -> Option<Self> {
423            let window = web_sys::window()?;
424            let closure = Closure::wrap(Box::new(f) as Box<dyn FnMut()>);
425            let id = window
426                .set_interval_with_callback_and_timeout_and_arguments_0(
427                    closure.as_ref().unchecked_ref(),
428                    ms.max(1),
429                )
430                .ok()?;
431            Some(Self {
432                id,
433                _closure: closure,
434            })
435        }
436    }
437
438    impl Drop for Interval {
439        fn drop(&mut self) {
440            if let Some(window) = web_sys::window() {
441                window.clear_interval_with_handle(self.id);
442            }
443        }
444    }
445
446    /// Calls `f` once, after `ms` milliseconds — the Web half of
447    /// [`Command::after`](crate::Command::after).
448    ///
449    /// There is no handle to keep, and that is the point. `Closure::once_into_js`
450    /// hands ownership of the closure to the JS side, which frees it after the call.
451    /// A `Closure` we owned instead would have to be either leaked — one leak per
452    /// timer — or dropped here, which would free the callback out from under a
453    /// `setTimeout` that has not fired yet.
454    pub(crate) fn after(ms: i32, f: impl FnOnce() + 'static) {
455        let Some(window) = web_sys::window() else {
456            return;
457        };
458        let callback = wasm_bindgen::closure::Closure::once_into_js(f);
459        let _ = window.set_timeout_with_callback_and_timeout_and_arguments_0(
460            callback.unchecked_ref(),
461            ms.max(0),
462        );
463    }
464}
465
466/// An active subscription's cancellation handle: **dropping** it stops the
467/// subscription.
468/// - Native: the executor task's handle — dropping it cancels the task.
469/// - Web: a retained `setInterval` (`None` when installing it failed).
470#[cfg(not(web))]
471type SubHandle = async_executor::Task<()>;
472#[cfg(web)]
473type SubHandle = Option<web_timer::Interval>;
474
475/// Scroll speed, in pixels per wheel notch.
476const SCROLL_SPEED: f32 = 40.0;
477
478/// The distance a **finger** must travel before the framework is confident the
479/// gesture is a drag rather than a tap, in logical px.
480///
481/// 18 px is large for a screen measurement, and deliberately so: a thumb covers a
482/// wide contact patch and rolls as it presses, so a smaller threshold turns taps
483/// near the edge of a button into aborted drags. It is the value the mature
484/// toolkits settled on after starting at 8 and hearing that targets were too hard
485/// to hit.
486const TOUCH_SLOP: f32 = 18.0;
487
488/// The same threshold for a **precise** pointer — a mouse or a trackpad. It knows
489/// exactly where it is, so almost any movement is intentional.
490const PRECISE_SLOP: f32 = 1.0;
491
492/// The velocity a release should be flung with, per axis — zero on an axis the
493/// gesture did not really travel along.
494///
495/// Speed alone is not a fling. A finger that twitches fast over three pixels
496/// produces a large velocity and no intent; requiring the gesture to have
497/// **covered ground** as well is what separates the two, and the distance that says
498/// "this was a drag" is the same `slop` that started it.
499///
500/// The gate is per axis because a scroll is two independent axes: a swipe running
501/// down the screen must not fling sideways on the little horizontal wobble a thumb
502/// always adds.
503fn fling_velocity(estimate: VelocityEstimate, slop: f32) -> (f32, f32) {
504    let gate = |velocity: f32, travelled: f32| {
505        if travelled.abs() > slop {
506            velocity
507        } else {
508            0.0
509        }
510    };
511    (
512        gate(estimate.velocity.x, estimate.offset.0),
513        gate(estimate.velocity.y, estimate.offset.1),
514    )
515}
516
517/// The elastic overshoot allowed past the scroll bounds, in px — the rubber band.
518const SCROLL_OVER: f32 = 48.0;
519
520/// The minimum velocity, in px/s, on releasing a pan that starts a fling.
521const PAN_FLING_MIN: f32 = 80.0;
522
523/// The width, in physical px, of the edge zone that arms the back gesture.
524const BACK_EDGE: f32 = 24.0;
525
526/// The **drag-and-drop** (reorder) preview — paint geometry only. The shadow's
527/// *colour* comes from the theme (`theme.scheme.shadow`, as `Button` does); only the
528/// geometry lives here, as named constants rather than magic numbers scattered about.
529mod drag_preview {
530    /// The vertical offset of the ghost's drop shadow, in px.
531    pub const SHADOW_OFFSET_Y: f32 = 4.0;
532    /// The blur of the ghost's drop shadow, in px.
533    pub const SHADOW_BLUR: f32 = 12.0;
534    /// The opacity of the ghost's shadow.
535    pub const SHADOW_ALPHA: f32 = 0.28;
536    /// The opacity of the ghost's `primary` border.
537    pub const BORDER_ALPHA: f32 = 0.9;
538    /// The thickness of the ghost's `primary` border, in px.
539    pub const BORDER_WIDTH: f32 = 1.5;
540    /// A slight vertical lift of the ghost during a **horizontal** drag, for columns.
541    pub const LIFT_Y: f32 = -2.0;
542    /// The thickness of the **insertion line** in the vertical preview, in px.
543    pub const INSERT_THICKNESS: f32 = 3.0;
544}
545
546/// Whether a frame builds the view again rather than repainting the tree it already has:
547/// when something asked for it (`dirty`), while the application's own animations are
548/// moving **and in the frame they stop in**, or for a reason of the frame's own
549/// (`otherwise`: no tree yet, a switcher in flight).
550///
551/// The frame they stop in, because the tick that ends a transition has changed the state
552/// — a push's page left behind is gone — and reports nothing moving. Without it the tree
553/// hit-tested from then on is the transition's last, the page left behind still in it
554/// under the finger (milestone 531).
555fn frame_needs_build(dirty: bool, animating: bool, was_animating: bool, otherwise: bool) -> bool {
556    dirty || animating || was_animating || otherwise
557}
558
559/// What a press is still a candidate for once it has chosen its drag: the long press a
560/// widget under the finger asks for, and the item it would lift on a hold.
561///
562/// A long press only while the press was not captured by a drag — a scrollbar, a handle,
563/// a selection; a touch scroll that has not moved yet stays a candidate. An item that
564/// lifts on a hold is a candidate under a scroll too: the scroll keeps the gesture for
565/// now, and the deadline decides, since a finger that stays put was never scrolling.
566fn hold_candidates<Msg: Clone>(
567    drag: Option<&Drag>,
568    ui: Option<&frus_widgets::Ui<Msg>>,
569    tree: Option<&dyn frus_widgets::Widget<Msg>>,
570    at: Point,
571) -> (Option<Msg>, Option<frus_widgets::DragSource>) {
572    // A back gesture owns its pointer from the press that starts it, as the reference's
573    // pop gesture does: nothing on either page takes a finger that is taking a page away
574    // (milestone 531). The page below is not even in the frame yet when the press lands —
575    // what is under the finger is whatever the frame on screen holds.
576    if matches!(drag, Some(Drag::Back { .. })) {
577        return (None, None);
578    }
579    let Some(ui) = ui else {
580        return (None, None);
581    };
582    // A drag that has not moved yet may still be a hold: a scroll's, or a detector's that
583    // also takes a long press (milestone 582, seen on the phone).
584    let free = matches!(
585        drag,
586        None | Some(Drag::Scroll { moved: false, .. }) | Some(Drag::Gesture { moved: false, .. })
587    );
588    let long_press = if free { ui.long_press_at(at) } else { None };
589    let lift = ui.drag_source_at(at).filter(|source| {
590        tree.and_then(|tree| find_widget(tree, source.id))
591            .is_some_and(|widget| widget.drag_needs_long_press())
592    });
593    (long_press, lift)
594}
595
596/// The drag a press is left with when its hold's deadline fires, from the drag it had
597/// and what the press found to lift.
598///
599/// A pending touch scroll no longer has any reason to exist — unless the hold was the
600/// lift of an item or of a row, in which case the scroll hands the gesture over and what
601/// was held is already up: `moved` is true because the hold *was* the threshold, and
602/// asking for a movement as well would mean a row that was held and then carried
603/// straight out of the list never engaged at all.
604fn drag_after_hold(
605    drag: Option<Drag>,
606    lift: Option<frus_widgets::DragSource>,
607    reorder: Option<(WidgetId, usize, Point)>,
608    cursor: Point,
609) -> Option<Drag> {
610    // Never a back gesture's: the hold was not what that finger was doing, and taking the
611    // drag from it would leave the application's gesture open with nothing to end it.
612    if let Some(Drag::Back { start_x }) = drag {
613        return Some(Drag::Back { start_x });
614    }
615    if let Some(source) = lift {
616        Some(Drag::Item {
617            source,
618            start: cursor,
619            moved: true,
620            over: None,
621        })
622    } else if let Some((id, from, start)) = reorder {
623        Some(Drag::Reorder {
624            id,
625            from,
626            start,
627            moved: true,
628            carried: None,
629        })
630    } else {
631        None
632    }
633}
634
635/// A drag currently under way with the mouse.
636enum Drag {
637    /// A scrollbar's thumb.
638    Scrollbar {
639        id: WidgetId,
640        vertical: bool,
641        grab: f32,
642        track_start: f32,
643        track_len: f32,
644        thumb_len: f32,
645        max: f32,
646        /// Whether this axis numbers its offsets from the far end, in which case the
647        /// thumb's position along the track reads backwards.
648        reverse: bool,
649    },
650    /// A selection dragged out over the texts of a
651    /// [`SelectionArea`](frus_widgets::SelectionArea): it may leave the text it began in, and
652    /// never leaves the area.
653    RegionSelect { area: WidgetId },
654    /// A handle of a selection area's touch selection, held (milestone 577): it carries its
655    /// end of the selection to the text under the finger, the other end staying put. `grab` is
656    /// the offset from the finger to the text position the handle stands for.
657    RegionHandle {
658        area: WidgetId,
659        handle: crate::selection::Handle,
660        grab: Point,
661    },
662    /// A text selection inside a field, with its bounds, for placement.
663    TextSelect {
664        id: WidgetId,
665        rect: frus_widgets::Rect,
666    },
667    /// A **selection handle**, held (milestone 511). `grab` is the offset from the finger
668    /// to the text position the handle stands for, so the selection follows the handle
669    /// rather than jumping to the fingertip the moment it moves.
670    SelectionHandle {
671        id: WidgetId,
672        rect: frus_widgets::Rect,
673        handle: crate::selection::Handle,
674        grab: Point,
675    },
676    /// A draggable widget — a slider or a handle — dragged along its horizontal axis.
677    /// `last_x` is the pointer's last abscissa, so the **delta** can be delivered to
678    /// the handles that accumulate, such as a column resize.
679    Widget {
680        id: WidgetId,
681        rect: frus_widgets::Rect,
682        last_x: f32,
683        /// Where the press landed, and the widget's track as it stood then: a slide that
684        /// moves the value from where it was measures from both (milestone 650).
685        press_x: f32,
686        track: frus_widgets::DragTrack,
687        /// Whether the press started anything: a slider that only takes a slide on its
688        /// thumb, pressed elsewhere, takes none of the gesture.
689        active: bool,
690        /// On a track with two thumbs, the one the press took — `None` until a press on
691        /// both has moved far enough to say which (milestone 651).
692        thumb: Option<usize>,
693    },
694    /// Reordering a **column**: a header is grabbed (`id`, column `from`) and dropped
695    /// onto another. `moved` tells a drag from a plain tap — which stays a sort —
696    /// by the `TOUCH_SLOP` threshold measured from `start`.
697    Reorder {
698        id: WidgetId,
699        from: usize,
700        start: Point,
701        moved: bool,
702        /// What is carried and where: the row the grab moves and its box, taken from the
703        /// frame the first time the drag is carried and moved with the content since, as a
704        /// lifted item's box is. The frame knows a reorderable's box only while it shows, and
705        /// only as much of it as shows — and carrying a row to an edge is exactly what
706        /// scrolls it out of sight (milestone 527, seen on a phone).
707        carried: Option<(WidgetId, Rect)>,
708    },
709    /// Panning an interactive viewport (`InteractiveViewer`): the pointer pushes the
710    /// content. `last` is the previous position, for the delta; `moved` tells a real
711    /// pan from a plain tap, by the `TOUCH_SLOP` threshold, so a click on a child can
712    /// still get through. `viewport` bounds the pan to the frame. The release
713    /// velocity comes from the shell's gesture tracker.
714    Pan {
715        id: WidgetId,
716        last: Point,
717        moved: bool,
718        viewport: frus_widgets::Rect,
719    },
720    /// Scrolling a scrollable area with a finger. `moved` tells a real scroll from a
721    /// plain tap, movement staying under the `TOUCH_SLOP` threshold.
722    Scroll {
723        id: WidgetId,
724        last: Point,
725        moved: bool,
726        /// The speed inherited from a fling this press interrupted, per axis, in
727        /// px/s — added to the release velocity so repeated swipes build momentum
728        /// where the platform does that. Zero when the content was already still.
729        carried: (f32, f32),
730        /// A dismissible item under the finger, still in the running. Both gestures
731        /// start the same way, so neither is chosen at the press: the first movement
732        /// past the threshold decides by **direction**, and the loser never sees the
733        /// gesture. Cleared once the scroll has won.
734        dismiss: Option<frus_widgets::Dismissable>,
735        /// A detector that takes a drag under the finger, still in the running, with the way
736        /// it drags (milestone 582). Decided at the threshold like `dismiss`: by direction.
737        pan: Option<(WidgetId, frus_widgets::PanAxis)>,
738        /// The axis this gesture was **claimed by**, decided once at the threshold and
739        /// held for the rest of the drag: `true` for vertical.
740        ///
741        /// A page that scrolls down must not drift sideways at the same time, and a
742        /// finger never travels in a straight line. The reference settles this in its
743        /// gesture arena — a vertical and a horizontal recogniser compete, the first
744        /// past its own slop wins, the loser is out of the gesture entirely — and this
745        /// is the same rule for an area that can go both ways. `None` until the
746        /// threshold is crossed.
747        axis: Option<bool>,
748    },
749    /// A drag on a [`GestureDetector`](frus_widgets::GestureDetector) (milestone 582). `start`
750    /// is where the pointer went down, `last` where it was at the last update; `moved` tells a
751    /// drag from a tap, by the slop, and the start is only reported once it is a drag.
752    Gesture {
753        id: WidgetId,
754        axis: frus_widgets::PanAxis,
755        start: Point,
756        last: Point,
757        moved: bool,
758    },
759    /// Swiping a [`frus_widgets::Dismissible`] item aside. `last` is the previous
760    /// position, for the delta; the item is already past the threshold by the time this
761    /// exists, since it is only ever reached by winning the direction test.
762    Dismiss {
763        item: frus_widgets::Dismissable,
764        last: Point,
765        /// `false` until the finger has passed the threshold — a press that never
766        /// travels is still a tap on the row.
767        moved: bool,
768    },
769    /// Carrying a [`frus_widgets::Draggable`] towards a [`frus_widgets::DragTarget`].
770    /// `over` is the target the pointer is on now, when it would accept the payload —
771    /// kept here so that entering and leaving one costs a comparison rather than a
772    /// second hit test.
773    Item {
774        source: frus_widgets::DragSource,
775        start: Point,
776        /// `false` until the finger has passed the threshold: a press that never
777        /// travels is a plain click on whatever is inside.
778        moved: bool,
779        over: Option<WidgetId>,
780    },
781    /// A [`frus_widgets::DraggableScrollableSheet`] moved by its panel, where nothing
782    /// under the finger scrolls — its grabber, its header, a list too short to move
783    /// (milestone 515). `available` is the height its shares are shares of, taken at
784    /// the press: the panel may shrink to nothing under the finger, and still come back.
785    Sheet {
786        id: WidgetId,
787        last: Point,
788        /// `false` until the finger has passed the threshold: a press that never
789        /// travels is a tap on whatever is in the sheet.
790        moved: bool,
791        available: f32,
792    },
793    /// The "back" gesture: the framework measures the finger's progress and velocity
794    /// and passes them to the application, which decides on the navigation.
795    Back { start_x: f32 },
796}
797
798/// The driver: an `event → frame` loop around an [`Application`].
799/// Turns an application's `view` into a tree that is **ready to be read**.
800///
801/// The one way this shell builds a tree, and it exists because there were two. A
802/// `ThemeBuilder` — and everything built on one, an `AppBar` included — has no children
803/// until [`build_deferred`] has been over it, so every traversal of a tree that has not
804/// been prepared finds nothing inside such a subtree and says nothing about it.
805///
806/// Both of the shell's build sites read the tree **before** the layout pass would have
807/// prepared it: the frame's own path calls `collect_ids` on it for the mount and leave
808/// bookkeeping, and the burst path hands it straight to `find_widget`. Neither of those is
809/// a mistake to be spotted at a call site; they are why this is one function.
810///
811/// The surface must already be described — the reader's font setting reaches anything a
812/// deferred subtree measures, and a build outside a surface measures at scale 1 while the
813/// frame lays it out at whatever the reader asked for (milestone 408).
814/// **The ambient answers an application gives**, read afresh every frame.
815///
816/// Both of these are settings rather than facts: a settings screen may turn scrollbars on
817/// or change the reader's language while the application is running, and neither should
818/// wait for a restart. Reading them once at start-up is the bug this shape avoids.
819///
820/// It is one function so that a test can drive it. The frame loop needs a window and an
821/// event-loop proxy, so nothing in this repo can call the loop — which is exactly how a
822/// setting that never reached production got shipped once already (milestone 408).
823fn install_ambient<A: Application>(app: &A, preferred: &[frus_widgets::Locale]) {
824    // **How the scrollables behave** (milestone 616): the application's behaviour, at the
825    // root of the tree, under every `ScrollConfiguration` the view installs.
826    frus_widgets::ScrollConfiguration::set_root(app.scroll_behavior());
827    // **Which language the interface is in**, resolved from what the platform reports
828    // against what the application has. Installed before the table, because an
829    // application choosing its table by language reads it from here.
830    frus_widgets::locale::install(app.resolved_locale(preferred));
831    if let Some(table) = app.localizations() {
832        frus_widgets::localizations::install(table);
833    }
834}
835
836fn build_view<A: Application>(
837    app: &A,
838    theme: &Theme,
839    runtime: &frus_widgets::Runtime,
840) -> Box<dyn Widget<A::Message>> {
841    debug_assert!(
842        frus_widgets::MediaQuery::of().is_described(),
843        "a view is being built with no surface described: install one first — milestone 416"
844    );
845    // Every state a component keeps is marked as reached while the tree is built, and
846    // what a build did not reach is let go when the frame that laid it out is done.
847    runtime.states.begin_build();
848    let tree = app.view(theme);
849    build_deferred(tree.as_ref(), theme, runtime);
850    tree
851}
852
853/// Where the messages an effect produces on another thread are sent: the event loop's proxy,
854/// or nowhere for a driver no event loop runs.
855struct Mailbox<M: 'static>(Option<EventLoopProxy<M>>);
856
857impl<M: 'static> Clone for Mailbox<M> {
858    fn clone(&self) -> Self {
859        Self(self.0.clone())
860    }
861}
862
863impl<M: 'static> Mailbox<M> {
864    /// Sends `message` to the loop. An error when the loop has closed, or when there is
865    /// none — either way nobody is left to tell.
866    fn send_event(&self, message: M) -> Result<(), ()> {
867        match &self.0 {
868            Some(proxy) => proxy.send_event(message).map_err(|_| ()),
869            None => Err(()),
870        }
871    }
872}
873
874/// The event handler that drives an [`Application`]: it owns the window, the renderer, the
875/// last interface built and the runtime, and turns each event of the loop into messages,
876/// frames and effects. [`run`](crate::run) and the other entry points build one; it is
877/// exported for a platform layer that supplies its own loop, not for an application to
878/// construct.
879pub struct App<A: Application> {
880    /// The application being driven: its state and logic.
881    app: A,
882    /// The channel that feeds back the messages effects produce, from their threads.
883    proxy: Mailbox<A::Message>,
884    window: Option<Arc<Window>>,
885    renderer: Option<Renderer>,
886    /// The renderer's GPU device was lost: a new renderer is made at the next frame's start
887    /// (milestone 686).
888    renderer_lost: bool,
889    /// A renderer being initialised **asynchronously**, on the Web only: filled in by
890    /// the `spawn_local` future and picked up on the first frame, the Web being unable
891    /// to block on GPU init.
892    #[cfg(web)]
893    pending_renderer: std::rc::Rc<std::cell::RefCell<Option<Renderer>>>,
894    /// What this side knows of the browser's history — see [`crate::history`].
895    #[cfg(web)]
896    history: crate::history::History,
897    /// How the application's location is written in the address bar, and the document's base
898    /// path, which the `Path` way of writing needs. Read once, at launch.
899    #[cfg(web)]
900    address: (crate::LocationStrategy, String),
901    /// The window's accessibility bridge (AccessKit) — desktop only.
902    #[cfg(desktop)]
903    a11y: Option<crate::a11y::A11y>,
904    /// The window's accessibility bridge, projected into the canvas's DOM — web only.
905    #[cfg(web)]
906    a11y: Option<crate::a11y_web::A11y>,
907    /// The element the focused text field is edited through, on the web (milestone 711).
908    #[cfg(web)]
909    web_editor: Option<crate::web_editing::Editor>,
910    /// The last interface built, used for hit testing, focus and scrolling.
911    ui: Option<Ui<A::Message>>,
912    /// The last widget tree built, used for keyboard and editing routing.
913    tree: Option<Box<dyn Widget<A::Message>>>,
914    /// The pointer's last known position, in **logical** pixels.
915    cursor: Point,
916    /// Where the pointer that **hovers** is: a mouse once it has moved, a finger only
917    /// while it touches. Kept as a place and asked of each frame — see [`crate::hover`].
918    hover: crate::hover::Hover,
919    /// The screen's DPI scale factor (physical = logical × scale × density).
920    scale: f32,
921    /// The last **logical** size handed to the app, to detect breakpoints.
922    last_size: Option<(f32, f32)>,
923    /// State retained between frames: hover and focus, scroll, caret and selection.
924    runtime: Runtime,
925    /// **The theme on display and the fade toward the one now asked for** — the
926    /// framework's, not the application's, since milestone 452. It resolves the
927    /// application's themes against the platform's brightness and the reader's contrast
928    /// setting once a frame, and crosses between two of them over time.
929    themes: crate::theming::ThemeFade,
930    /// Live-reload watching (development, `FRUS_WATCH=1`): relaunch on recompilation.
931    reload: Option<crate::reload::ReloadWatcher>,
932    /// The channel `frus run` listens on, when it started this application — see [`crate::dev`].
933    #[cfg(any(desktop, android))]
934    dev: Option<crate::dev::DevChannel>,
935    /// Overflowing boxes already named on the console, so that a layout that does not
936    /// fit is reported once and not sixty times a second.
937    reported_overflows: std::cell::RefCell<std::collections::HashSet<u64>>,
938    /// Is the runtime inspector on? Toggled by F12, in debug builds only.
939    inspector: bool,
940    /// A tree dump to print on the next inspected frame.
941    inspector_dump: bool,
942    /// What the keys of `frus run` turned on or asked for (milestone 668); see
943    /// [`App::take_dev_commands`].
944    debug: DebugAsks,
945    /// The current keyboard modifiers.
946    shift: bool,
947    ctrl: bool,
948    alt: bool,
949    meta: bool,
950    /// The remembered "goal" visual column for Up/Down/PgUp/PgDn: crossing shorter
951    /// lines keeps the original column, the way an editor does. Cleared as soon as any
952    /// other caret movement happens.
953    goal_x: Option<f32>,
954    /// Clipboard access: `arboard` on the desktop, the platform's on Android, the
955    /// browser's on the web, nothing yet on iOS.
956    clipboard: clip::Clipboard,
957    /// Has the startup effect (`init`) already run? This keeps it from being replayed
958    /// when the surface is recreated, as it is when Android returns from background.
959    started: bool,
960    /// The last frame's instant, for the animations' dt.
961    last_frame: Option<Instant>,
962    /// The **build** phase is dirty: the app's state, or the size, changed and the
963    /// `view` must be rebuilt. The view is a pure function of `(state, theme, size)` —
964    /// never of hover, scroll or focus, which live in the `Runtime` — so a frame that
965    /// only animates an interaction merely **repaints** the retained tree (§1: "a hover
966    /// touches paint and nothing else").
967    build_dirty: bool,
968    /// Whether the application's own animations were moving at the last frame. The frame
969    /// they stop in is built once more: the tick that ends a transition has changed the
970    /// state — the page it left is gone — and says it is no longer moving, so without it
971    /// the tree hit-tested from then on is the transition's last (milestone 531).
972    app_was_animating: bool,
973    /// The mouse drag under way.
974    drag: Option<Drag>,
975    /// Files from outside the application, held over the window or just dropped on it
976    /// (milestone 695). Apart from `drag`: the system runs that drag, not the pointer.
977    file_drag: Option<crate::file_drop::FileDrag>,
978    /// Was the pointer that started the drag under way a finger? A mouse is
979    /// precise and needs almost no slop; a finger needs a lot.
980    pointer_touch: bool,
981    /// The pointer history of the drag under way, and the instant it began.
982    ///
983    /// One tracker for all of them: at most one drag is active at a time, and every
984    /// kind of drag asks the same question on release — how fast was the finger
985    /// going. Keeping it here rather than in each [`Drag`] variant means the
986    /// gesture's clock and its history start together, in one place.
987    gesture_velocity: VelocityTracker,
988    /// The test driver's clock for the velocity tracker, when there is one: seconds of
989    /// frames run, and when the gesture under way began on it. `None` in an application,
990    /// which stamps samples with the wall clock. A driver's events are microseconds apart on
991    /// the wall, and irregularly so on a loaded machine: a fling read from them came out at
992    /// any speed, in either direction, and a test that let go of a scroll saw it thrown back
993    /// to the top (milestone 584, seen in CI).
994    gesture_clock: Option<(f32, f32)>,
995    gesture_start: Instant,
996    /// The pointer's **smoothed** abscissa during a reorder: it springs toward the
997    /// real position, giving the columns' sliding a gentle inertia — the background
998    /// catches up with the ghost, which sticks to the pointer.
999    reorder_x: f32,
1000    /// The **smoothed** ordinate of the **insertion line** during a **vertical**
1001    /// reorder, that is, Kanban cards: it springs toward the **chosen** slot edge, the
1002    /// hovered half, so that the line and the gap *slide* between cards instead of
1003    /// jumping — the vertical counterpart of the horizontal `reorder_x` spring.
1004    reorder_y: f32,
1005    /// The last **announcement** pushed to the accessibility bridge's live region, for
1006    /// the screen reader. It persists as is: it is re-spoken only on a change, so the
1007    /// same text carried over every frame does not repeat. Desktop and web.
1008    #[cfg(any(desktop, web))]
1009    announce: String,
1010    /// The tap-or-long-press recogniser (gesture tier 1).
1011    press: PressRecognizer,
1012    /// The pressed target's long-press message, captured on the press.
1013    long_press_msg: Option<A::Message>,
1014    /// A **text field** pressed with a finger: a hold there selects the word under it
1015    /// (milestone 511), where on anything else it is the widget's long press.
1016    pending_word: Option<WidgetId>,
1017    /// A finger came down on words in a selection area: a hold there selects the word under it
1018    /// and opens the bar (milestone 576).
1019    pending_region_hold: bool,
1020    /// The focused field's caret blink (milestone 513).
1021    caret: crate::caret::CaretBlink,
1022    /// The form last shown to the platform's autofill service, as it was shown
1023    /// (milestone 512) — what a returned value is routed against, and what a value the
1024    /// service has not heard is told apart from.
1025    #[cfg(android)]
1026    autofill_reported: Vec<crate::autofill::AutofillField>,
1027    /// The field of that form the service was told has focus.
1028    #[cfg(android)]
1029    autofill_focus: Option<WidgetId>,
1030    /// The [`frus_widgets::AutofillGroup`] that form is, while it is on screen.
1031    #[cfg(android)]
1032    autofill_group: Option<WidgetId>,
1033    /// When the last **recorded** edit happened, for the pause that breaks a run of
1034    /// typing into two steps of undo. One field and not one per text field, because only
1035    /// the focused one is being typed into, and leaving a field ends its run anyway.
1036    last_edit_at: Option<Instant>,
1037    /// A [`frus_widgets::Draggable`] that asked to be lifted by a **hold**, waiting
1038    /// for the long-press deadline. Inside a scrollable this is the only way up: the
1039    /// plain drag belongs to the scroll, and a hold is the one signal it cannot claim.
1040    pending_lift: Option<frus_widgets::DragSource>,
1041    /// A **reorderable row** that asked to be lifted by a hold, waiting for the same
1042    /// deadline: `(the row, its index, where the finger landed)`.
1043    ///
1044    /// Its own field rather than a second use of `pending_lift`, because what the two
1045    /// become at the deadline is different — one carries a payload to a drop target, the
1046    /// other carries itself to a slot — but the reason they wait is the same, and it is
1047    /// the reason a list can still be scrolled.
1048    pending_reorder: Option<(WidgetId, usize, Point)>,
1049    /// The last click's instant, for double-click detection.
1050    last_click_time: Option<Instant>,
1051    /// When and where the last press began a selection in a selection area: a second one soon
1052    /// after and close by is a double click.
1053    last_region_click: Option<(Instant, Point)>,
1054    /// A first tap waiting to see whether it is half of a double tap (milestone 581).
1055    pending_tap: Option<PendingTap<A::Message>>,
1056    /// How many values the work components started had brought when this shell last looked
1057    /// (milestone 585).
1058    task_arrivals: u64,
1059    /// The regions the pointer is in, the innermost first, each with its box when it was
1060    /// last seen (milestone 583).
1061    entered: Vec<(WidgetId, Rect)>,
1062    /// Whether a button or a finger is down: a region hears moves only without one.
1063    pointer_held: bool,
1064    /// The widgets hearing the pointer's raw events that it went down in, each with its box
1065    /// then: they hear it until it lifts, wherever it goes (milestone 586).
1066    captured: Vec<(WidgetId, Rect)>,
1067    /// A counter for the keys of leaving events, which fade out.
1068    leaving_counter: u64,
1069    /// The running subscriptions: id → cancellation handle, dropping which stops it.
1070    running_subs: HashMap<u64, SubHandle>,
1071    /// Pending focus requests — the keys `Command::focus` produced — resolved against
1072    /// the **freshly built** tree on the next frame.
1073    pending_focus: Vec<u64>,
1074    /// Pending **scroll** requests — the `(key, ScrollTo)` pairs `Command::scroll`
1075    /// produced — resolved against the frame that follows, and dropped whether or not
1076    /// the key named anything (see `Command::scroll`).
1077    pending_scroll: Vec<(u64, ScrollTo)>,
1078    /// The requests of the frame in progress that the **previous** frame's registry
1079    /// could not place — a region that has only just appeared. Tried once more against
1080    /// the registry this frame builds, and then gone: a request gets one frame.
1081    retry_scroll: Vec<(u64, ScrollTo)>,
1082    /// Pending **sheet** requests — the `(key, SheetTo)` pairs `Command::sheet` produced —
1083    /// on the terms of the scroll requests above.
1084    pending_sheet: Vec<(u64, SheetTo)>,
1085    /// The sheet requests of the frame in progress that the previous frame's sheets did
1086    /// not name: tried once more against this frame's.
1087    retry_sheet: Vec<(u64, SheetTo)>,
1088    /// The **focus history** of triggers, for returning focus when an overlay closes:
1089    /// on every focus change the old one, if still present, is pushed; when focus
1090    /// **vanishes** because a menu or modal closed, we go back to the most recent
1091    /// entry that is still present.
1092    focus_history: Vec<WidgetId>,
1093    /// The previous frame's focus, to detect the transitions worth pushing.
1094    prev_focus: Option<WidgetId>,
1095    /// The focus the widgets were last told about (milestone 660); see
1096    /// `report_focus`.
1097    focus_reported: Option<WidgetId>,
1098    /// The widgets that took the focus on appearing, as of the last frame: one that is new
1099    /// this frame takes it (milestone 605).
1100    autofocused: std::collections::HashSet<WidgetId>,
1101    /// How smooth the frames are, when asked (milestone 609).
1102    frame_stats: crate::frame_stats::FrameStats,
1103    /// What the frame being drawn has cost so far, while frames are measured.
1104    frame_costs: crate::frame_stats::FrameCosts,
1105    /// The window is occluded, so rendering is suspended.
1106    occluded: bool,
1107    /// Cumulative elapsed time, in seconds, for the continuous animations.
1108    elapsed: f32,
1109    /// The last window insets handed to the app — padding plus keyboard — in logical px.
1110    last_insets: WindowInsets,
1111    /// The device the window stands in for, when `FRUS_DEVICE` names one (milestone 721).
1112    simulated: Option<crate::simulated::Simulated>,
1113    /// Whether the window's title bar line is shared with the content, and what the content
1114    /// is told about it (milestone 640).
1115    #[cfg_attr(not(windows), allow(dead_code))]
1116    title_bar_on: bool,
1117    title_bar: Option<frus_widgets::TitleBar>,
1118    /// The tray icon as the system has it (milestone 706).
1119    #[cfg(all(desktop, feature = "tray"))]
1120    tray: Option<crate::tray::native::Live>,
1121    /// Whether closing the window leaves the application running in its tray.
1122    #[cfg(all(desktop, feature = "tray"))]
1123    tray_keeps_running: bool,
1124    /// What the taskbar button shows of the application's (milestone 708).
1125    #[cfg(all(desktop, feature = "taskbar"))]
1126    taskbar: crate::Taskbar,
1127    /// The global shortcuts the system holds for the application (milestone 709).
1128    #[cfg(all(desktop, feature = "global-shortcuts"))]
1129    shortcuts: crate::shortcuts::native::Registered,
1130    /// Combinations already refused for their spelling, so each is told once.
1131    #[cfg(all(desktop, feature = "global-shortcuts"))]
1132    shortcuts_refused: std::collections::HashSet<String>,
1133    /// The application's icon, kept for the life of the program, to paint on the line.
1134    #[cfg_attr(not(windows), allow(dead_code))]
1135    window_icon: Option<&'static [u8]>,
1136    /// The soft keyboard has just grown, so the focused widget is brought back into view
1137    /// once the frame that lays out the shortened window is built: the keyboard covers
1138    /// the bottom of the screen, and a field under it is a field the reader cannot see
1139    /// while typing in it.
1140    reveal_after_keyboard: bool,
1141    /// What the **platform** last said about the person using it: the font-size slider,
1142    /// the night setting, the accessibility switches.
1143    ///
1144    /// Cached rather than read per frame, because reading it is a walk across the JNI
1145    /// boundary and the answer changes about once a year. It is refreshed when the
1146    /// surface appears — which on Android is also when it *reappears*, the activity being
1147    /// recreated on a font-scale or night change since no `configChanges` is declared —
1148    /// and on a theme change where the platform sends one.
1149    platform: PlatformSettings,
1150    /// The **keyboard-free** inset baseline, along with the physical size it was
1151    /// taken at — a rotation resets it: whatever bottom inset exceeds it is credited
1152    /// to the software keyboard.
1153    inset_baseline: Option<(Insets, (u32, u32))>,
1154    /// The Android activity's handle, used to query the insets, the keyboard and so on.
1155    /// The current **lifecycle** state, so the app is notified only on real
1156    /// **transitions**.
1157    lifecycle: Lifecycle,
1158    #[cfg(android)]
1159    android_app: Option<winit::platform::android::activity::AndroidApp>,
1160    /// Is the software keyboard being asked for? It follows the text fields' focus.
1161    #[cfg(android)]
1162    soft_input_shown: bool,
1163    /// The system bars as the platform was last told them, so that it is told only of a
1164    /// change (#46).
1165    #[cfg(android)]
1166    system_bars: Option<frus_widgets::SystemBars>,
1167}
1168
1169impl<A: Application> App<A> {
1170    /// Creates the driver around an application and its message channel.
1171    pub fn new(app: A, proxy: EventLoopProxy<A::Message>) -> Self {
1172        let mut shell = Self::with_mailbox(app, Mailbox(Some(proxy)));
1173        // A window standing in for a device follows that device's platform (milestone 721).
1174        // Only a real window: a test's shell is never a simulation.
1175        shell.simulated = crate::simulated::Simulated::from_env();
1176        if let Some(simulated) = &shell.simulated {
1177            frus_widgets::set_debug_default_target_platform_override(Some(
1178                simulated.profile.platform,
1179            ));
1180        }
1181        shell
1182    }
1183
1184    /// A driver with no event loop behind it, for a test that feeds it input and frames
1185    /// by hand: whatever an effect sends back goes nowhere. An event loop cannot be built
1186    /// on a machine with no display, which is where the continuous integration runs.
1187    #[cfg(any(test, feature = "testing"))]
1188    fn detached(app: A) -> Self {
1189        let mut shell = Self::with_mailbox(app, Mailbox(None));
1190        // A shell with no window is a test's: it keeps a clipboard of its own rather than
1191        // the machine's, which tests running side by side would share — and which macOS
1192        // does not let several threads touch at once (a test run aborted on it).
1193        shell.clipboard = clip::Clipboard::in_memory();
1194        shell
1195    }
1196
1197    fn with_mailbox(app: A, proxy: Mailbox<A::Message>) -> Self {
1198        // The widget layer knows how to *want* an image over the network and has no way
1199        // to get one: no runtime, no socket, and no dependency on this crate, since the
1200        // dependency runs the other way. So the shell says how, on the way up, and the
1201        // widget layer only asks. Same shape as the image decoder a step earlier.
1202        #[cfg(feature = "net")]
1203        frus_widgets::set_image_fetcher(fetch_image_bytes);
1204        // And how to run the work a component starts (milestone 585): this shell's executor
1205        // natively, the browser on the Web.
1206        #[cfg(not(web))]
1207        frus_widgets::set_task_spawner(|task| crate::runtime::spawn(task).detach());
1208        #[cfg(web)]
1209        frus_widgets::set_task_spawner(wasm_bindgen_futures::spawn_local);
1210        Self {
1211            app,
1212            proxy,
1213            window: None,
1214            renderer: None,
1215            renderer_lost: false,
1216            #[cfg(web)]
1217            pending_renderer: std::rc::Rc::new(std::cell::RefCell::new(None)),
1218            #[cfg(web)]
1219            history: crate::history::History::new(),
1220            #[cfg(web)]
1221            address: (crate::LocationStrategy::Hash, "/".to_string()),
1222            #[cfg(desktop)]
1223            a11y: None,
1224            #[cfg(web)]
1225            a11y: None,
1226            #[cfg(web)]
1227            web_editor: None,
1228            ui: None,
1229            tree: None,
1230            cursor: Point::new(0.0, 0.0),
1231            hover: crate::hover::Hover::default(),
1232            scale: 1.0,
1233            last_size: None,
1234            runtime: Runtime::default(),
1235            themes: crate::theming::ThemeFade::default(),
1236            reload: crate::reload::ReloadWatcher::new(),
1237            #[cfg(any(desktop, android))]
1238            dev: None,
1239            reported_overflows: std::cell::RefCell::new(std::collections::HashSet::new()),
1240            inspector: false,
1241            inspector_dump: false,
1242            debug: DebugAsks::default(),
1243            shift: false,
1244            ctrl: false,
1245            alt: false,
1246            meta: false,
1247            goal_x: None,
1248            clipboard: clip::Clipboard::new(),
1249            started: false,
1250            last_frame: None,
1251            build_dirty: true,
1252            app_was_animating: false,
1253            drag: None,
1254            file_drag: None,
1255            pointer_touch: false,
1256            gesture_velocity: VelocityTracker::default(),
1257            gesture_clock: None,
1258            gesture_start: Instant::now(),
1259            reorder_x: 0.0,
1260            reorder_y: 0.0,
1261            #[cfg(any(desktop, web))]
1262            announce: String::new(),
1263            press: PressRecognizer::new(),
1264            long_press_msg: None,
1265            pending_word: None,
1266            pending_region_hold: false,
1267            caret: crate::caret::CaretBlink::new(),
1268            #[cfg(android)]
1269            autofill_reported: Vec::new(),
1270            #[cfg(android)]
1271            autofill_focus: None,
1272            #[cfg(android)]
1273            autofill_group: None,
1274            last_edit_at: None,
1275            pending_lift: None,
1276            pending_reorder: None,
1277            last_click_time: None,
1278            last_region_click: None,
1279            pending_tap: None,
1280            task_arrivals: 0,
1281            entered: Vec::new(),
1282            pointer_held: false,
1283            captured: Vec::new(),
1284            leaving_counter: 0,
1285            running_subs: HashMap::new(),
1286            pending_focus: Vec::new(),
1287            pending_scroll: Vec::new(),
1288            retry_scroll: Vec::new(),
1289            pending_sheet: Vec::new(),
1290            retry_sheet: Vec::new(),
1291            focus_history: Vec::new(),
1292            prev_focus: None,
1293            focus_reported: None,
1294            autofocused: std::collections::HashSet::new(),
1295            frame_stats: crate::frame_stats::FrameStats::new(),
1296            frame_costs: crate::frame_stats::FrameCosts::default(),
1297            occluded: false,
1298            elapsed: 0.0,
1299            last_insets: WindowInsets::ZERO,
1300            simulated: None,
1301            title_bar_on: false,
1302            title_bar: None,
1303            #[cfg(all(desktop, feature = "tray"))]
1304            tray: None,
1305            #[cfg(all(desktop, feature = "tray"))]
1306            tray_keeps_running: false,
1307            #[cfg(all(desktop, feature = "taskbar"))]
1308            taskbar: crate::Taskbar::new(),
1309            #[cfg(all(desktop, feature = "global-shortcuts"))]
1310            shortcuts: Default::default(),
1311            #[cfg(all(desktop, feature = "global-shortcuts"))]
1312            shortcuts_refused: Default::default(),
1313            window_icon: None,
1314            reveal_after_keyboard: false,
1315            platform: PlatformSettings::default(),
1316            inset_baseline: None,
1317            // The app starts out detached; `resumed` will move it to `Resumed`.
1318            lifecycle: Lifecycle::Detached,
1319            #[cfg(android)]
1320            android_app: None,
1321            #[cfg(android)]
1322            soft_input_shown: false,
1323            #[cfg(android)]
1324            system_bars: None,
1325        }
1326    }
1327
1328    /// What an assistive technology's click on widget `id` does: what a pointer click on it
1329    /// would — its message, or, for a built-in button of the selection bar that carries none,
1330    /// its editing action (milestone 568), on the field the bar is open on.
1331    #[cfg(any(desktop, web))]
1332    fn activate_for_assistive_tech(&mut self, id: WidgetId) {
1333        if let Some(action) = self.ui.as_ref().and_then(|ui| ui.edit_action_for(id)) {
1334            self.perform_edit_action(action);
1335            return;
1336        }
1337        if let Some(msg) = self.ui.as_ref().and_then(|ui| ui.msg_for(id)) {
1338            // An item of the application's own on the bar closes it, as a press does.
1339            if self.ui.as_ref().is_some_and(|ui| ui.hit_is_on_toolbar(id)) {
1340                self.runtime.selection_handles = None;
1341                self.hide_selection_toolbar();
1342            }
1343            self.dispatch(msg);
1344            self.request_redraw();
1345        }
1346    }
1347
1348    /// Puts `value` in the field `id`, for an assistive technology that set it (milestone 710):
1349    /// through the message typing it would have produced, as an autofill or an undo does — so
1350    /// a field that refuses typing refuses this too — with the caret after what arrived.
1351    #[cfg(any(desktop, test, feature = "testing"))]
1352    fn set_value_for_assistive_tech(&mut self, id: WidgetId, value: String) {
1353        let message = self
1354            .tree
1355            .as_ref()
1356            .and_then(|tree| find_widget(tree.as_ref(), id))
1357            .and_then(|widget| widget.replace_value(value.clone()));
1358        let Some(message) = message else {
1359            return;
1360        };
1361        self.runtime.edits.insert(
1362            id,
1363            Edit {
1364                cursor: value.chars().count(),
1365                anchor: None,
1366                composing: None,
1367            },
1368        );
1369        self.dispatch_edit(message);
1370        self.request_redraw();
1371    }
1372
1373    /// Replays the actions an assistive technology asked for: an AT click activates
1374    /// the widget, exactly as a pointer click would, and an AT focus focuses it.
1375    #[cfg(desktop)]
1376    fn drain_a11y_actions(&mut self) {
1377        use crate::a11y::A11yAction;
1378        let actions = match self.a11y.as_ref() {
1379            Some(a11y) => a11y.take_actions(),
1380            None => return,
1381        };
1382        for action in actions {
1383            match action {
1384                A11yAction::Click(id) => self.activate_for_assistive_tech(id),
1385                A11yAction::SetValue(id, value) => self.set_value_for_assistive_tech(id, value),
1386                A11yAction::Focus(id) => {
1387                    // A button of the selection bar takes no focus: the bar acts on the
1388                    // field, and a focus that left it would end the very selection the bar
1389                    // is open on before the click that follows the focus got to use it.
1390                    if self.ui.as_ref().is_some_and(|ui| ui.hit_is_on_toolbar(id)) {
1391                        continue;
1392                    }
1393                    self.runtime.input.focused = Some(id);
1394                    self.runtime.focus_visible = true;
1395                    self.request_redraw();
1396                }
1397            }
1398        }
1399    }
1400
1401    /// Keeps the element the focused field is edited through in step with the field
1402    /// (milestone 711): there while a field that takes typing has the focus, over it, holding
1403    /// its text and selection.
1404    #[cfg(web)]
1405    fn sync_web_editing(&mut self) {
1406        // A simulated device's field is in its screen, inside the canvas, and laid out in the
1407        // window's own pixels: one CSS pixel each.
1408        let placement = self.placement();
1409        let css = if self.simulated.is_some() {
1410            1.0
1411        } else {
1412            self.app.density()
1413        };
1414        let Some(editor) = self.web_editor.as_mut() else {
1415            return;
1416        };
1417        let field = self.runtime.input.focused.and_then(|id| {
1418            let widget = find_widget(self.tree.as_ref()?.as_ref(), id)?;
1419            if !wants_keyboard(widget) {
1420                return None;
1421            }
1422            let (_, rect, semantics) = self
1423                .ui
1424                .as_ref()?
1425                .semantics()
1426                .iter()
1427                .find(|(node, ..)| *node == id)?;
1428            Some(crate::web_editing::Field {
1429                id,
1430                rect: match placement {
1431                    Some(placement) => crate::simulated::Simulated::to_window(placement, *rect),
1432                    None => *rect,
1433                },
1434                web: widget.ime().web(),
1435                label: semantics.label.as_deref(),
1436                text: widget.text_value().unwrap_or_default(),
1437                edit: self.runtime.edits.get(&id).copied().unwrap_or_default(),
1438            })
1439        });
1440        editor.sync(field, css);
1441    }
1442
1443    /// Puts what the browser did to the element into its field (milestone 711): the text
1444    /// through the message typing would have produced — so a field that refuses typing
1445    /// refuses this too, and the next frame puts the field's own text back in the element —
1446    /// and the caret, the selection and the composition as the browser left them.
1447    #[cfg(web)]
1448    fn drain_web_editing(&mut self) {
1449        let changes = match self.web_editor.as_ref() {
1450            Some(editor) => editor.take_changes(),
1451            None => return,
1452        };
1453        if changes.is_empty() {
1454            return;
1455        }
1456        for (id, element) in changes {
1457            // A change to a field that has lost the focus since is not for it any more.
1458            if self.runtime.input.focused != Some(id) {
1459                continue;
1460            }
1461            let (value, edit) = crate::web_editing::read(&element);
1462            let message = self
1463                .tree
1464                .as_ref()
1465                .and_then(|tree| find_widget(tree.as_ref(), id))
1466                .filter(|widget| widget.text_value() != Some(value.as_str()))
1467                .and_then(|widget| widget.replace_value(value));
1468            self.runtime.edits.insert(id, edit);
1469            if let Some(message) = message {
1470                self.dispatch_edit(message);
1471            }
1472        }
1473        self.request_redraw();
1474    }
1475
1476    /// The web's counterpart to `drain_a11y_actions` above — same replay, the DOM
1477    /// bridge's own action type (#18).
1478    #[cfg(web)]
1479    fn drain_a11y_actions(&mut self) {
1480        use crate::a11y_web::A11yAction;
1481        let actions = match self.a11y.as_ref() {
1482            Some(a11y) => a11y.take_actions(),
1483            None => return,
1484        };
1485        for action in actions {
1486            match action {
1487                A11yAction::Click(id) => self.activate_for_assistive_tech(id),
1488                A11yAction::Focus(id) => {
1489                    // A button of the selection bar takes no focus: the bar acts on the
1490                    // field, and a focus that left it would end the very selection the bar
1491                    // is open on before the click that follows the focus got to use it.
1492                    if self.ui.as_ref().is_some_and(|ui| ui.hit_is_on_toolbar(id)) {
1493                        continue;
1494                    }
1495                    self.runtime.input.focused = Some(id);
1496                    self.runtime.focus_visible = true;
1497                    self.request_redraw();
1498                }
1499            }
1500        }
1501    }
1502
1503    /// Keeps the **system bars** in step with the frame (#46): their colour and their icons,
1504    /// from the regions under them and then the theme. The platform is told only when the
1505    /// answer changes, telling it being a crossing to the Java UI thread — and a request
1506    /// that did not arrive is asked again next frame rather than remembered as made.
1507    ///
1508    /// The two points are the first row of content under the status bar and the last one
1509    /// above the navigation bar: this window is not drawn behind the bars, so what a bar
1510    /// stands against is what lies at the edge of the content next to it.
1511    fn sync_system_bars(&mut self, theme: &Theme) {
1512        #[cfg(android)]
1513        {
1514            let Some(ui) = self.ui.as_ref() else {
1515                return;
1516            };
1517            let Some(window) = self.window.as_ref() else {
1518                return;
1519            };
1520            let scale = self.total_scale();
1521            let size = window.inner_size();
1522            let (width, height) = (size.width as f32 / scale, size.height as f32 / scale);
1523            let edges = self.last_insets.padding;
1524            let bars = ui
1525                .system_ui_style(
1526                    frus_widgets::Point::new(width / 2.0, edges.top + 0.5),
1527                    frus_widgets::Point::new(width / 2.0, height - edges.bottom - 0.5),
1528                )
1529                .resolve(theme);
1530            if self.system_bars != Some(bars) && crate::android_system_bars::apply(bars) {
1531                self.system_bars = Some(bars);
1532            }
1533        }
1534        #[cfg(not(android))]
1535        let _ = theme;
1536    }
1537
1538    /// Keeps the **software keyboard** in step with focus: asked for when focus is in
1539    /// a text field (`wants_keyboard`), closed otherwise. Called at the end of a
1540    /// frame, since any focus change already triggers a redraw.
1541    fn sync_soft_input(&mut self) {
1542        // A simulated device's keyboard comes up the same way, and is the shell's to draw
1543        // (milestone 722).
1544        if self.simulated.is_some() {
1545            let editing = self
1546                .runtime
1547                .input
1548                .focused
1549                .and_then(|id| {
1550                    self.tree
1551                        .as_ref()
1552                        .and_then(|tree| find_widget(tree.as_ref(), id))
1553                })
1554                .is_some_and(wants_keyboard);
1555            if let Some(simulated) = self.simulated.as_mut() {
1556                if simulated.keyboard != editing {
1557                    simulated.keyboard = editing;
1558                    self.build_dirty = true;
1559                    self.request_redraw();
1560                }
1561            }
1562        }
1563        // The device's own keyboard, when the window is not standing in for another.
1564        #[cfg(android)]
1565        if self.simulated.is_none() {
1566            let editing = self
1567                .runtime
1568                .input
1569                .focused
1570                .and_then(|id| {
1571                    self.tree
1572                        .as_ref()
1573                        .and_then(|tree| find_widget(tree.as_ref(), id))
1574                })
1575                .is_some_and(wants_keyboard);
1576            if editing != self.soft_input_shown {
1577                self.soft_input_shown = editing;
1578                self.end_composition();
1579                if crate::android_ime::installed() {
1580                    // The InputConnection bridge: the Java view captures the IME.
1581                    if editing {
1582                        if let Some(id) = self.runtime.input.focused {
1583                            self.push_ime_context(id);
1584                        }
1585                        crate::android_ime::start_input(self.focused_ime());
1586                    } else {
1587                        crate::android_ime::clear_editor_state();
1588                        crate::android_ime::stop_input();
1589                    }
1590                } else if let Some(app) = &self.android_app {
1591                    // The TYPE_NULL fallback: a plain open, Latin keys only.
1592                    if editing {
1593                        app.show_soft_input(true);
1594                    } else {
1595                        app.hide_soft_input(false);
1596                    }
1597                }
1598            }
1599        }
1600    }
1601
1602    /// What keyboard the **focused field** asks for.
1603    ///
1604    /// Found the same way the caret is — the focused id, resolved against the last
1605    /// tree built — so the two answers cannot disagree about which field is being
1606    /// typed into. A focus that resolves to nothing falls back to ordinary text, which
1607    /// is what every field got before this was asked at all.
1608    #[cfg(android)]
1609    fn focused_ime(&self) -> frus_widgets::Ime {
1610        self.runtime
1611            .input
1612            .focused
1613            .and_then(|id| {
1614                self.tree
1615                    .as_ref()
1616                    .and_then(|tree| find_widget(tree.as_ref(), id))
1617            })
1618            .map(|widget| widget.ime())
1619            .unwrap_or_default()
1620    }
1621
1622    /// Notifies the app of a lifecycle **change**; never the same state twice in a row.
1623    fn set_lifecycle(&mut self, state: Lifecycle) {
1624        if self.lifecycle != state {
1625            self.lifecycle = state;
1626            self.app.on_lifecycle(state);
1627        }
1628    }
1629
1630    /// **Explicitly** asks for the software keyboard again, for the focused field —
1631    /// called when the user **taps in a text field**, even one already focused. This is
1632    /// indispensable because the keyboard may have been closed by the **system back
1633    /// button** without the app being told: `soft_input_shown` then stays `true` and
1634    /// focus does not change on the next tap, so
1635    /// [`sync_soft_input`](Self::sync_soft_input)'s diff would see no change and never
1636    /// reopen the keyboard. Here it is reopened unconditionally — the native behaviour:
1637    /// tapping in a field shows the keyboard.
1638    fn request_soft_input(&mut self) {
1639        #[cfg(android)]
1640        {
1641            // Only a field that takes typing: a tap in a paragraph made selectable is a tap
1642            // in words to select, not in a form.
1643            let typing = self
1644                .runtime
1645                .input
1646                .focused
1647                .and_then(|id| {
1648                    self.tree
1649                        .as_ref()
1650                        .and_then(|tree| find_widget(tree.as_ref(), id))
1651                })
1652                .is_some_and(wants_keyboard);
1653            if !typing {
1654                return;
1655            }
1656            self.soft_input_shown = true;
1657            self.end_composition();
1658            if crate::android_ime::installed() {
1659                if let Some(id) = self.runtime.input.focused {
1660                    self.push_ime_context(id);
1661                }
1662                crate::android_ime::start_input(self.focused_ime());
1663            } else if let Some(app) = &self.android_app {
1664                app.show_soft_input(true);
1665            }
1666        }
1667    }
1668
1669    /// Applies the pending IME operations to the focused field (the §6 bridge). The
1670    /// composition is materialised **in the field**: each update erases the previous
1671    /// one, the controlled model having no styled composition region yet — see
1672    /// docs/milestone-81.md.
1673    #[cfg(android)]
1674    fn drain_ime(&mut self) {
1675        use crate::ime::Step;
1676        let events = crate::android_ime::drain();
1677        if events.is_empty() {
1678            return;
1679        }
1680        let Some(focused) = self.runtime.input.focused else {
1681            return;
1682        };
1683        for event in events {
1684            let edit = self
1685                .runtime
1686                .edits
1687                .get(&focused)
1688                .copied()
1689                .unwrap_or_default();
1690            // Planned against the field as it stands before this operation: its text,
1691            // its caret and the composition it underlines (milestone 510).
1692            let text = self
1693                .tree
1694                .as_ref()
1695                .and_then(|tree| find_widget(tree.as_ref(), focused))
1696                .and_then(|widget| widget.text_value().map(str::to_owned))
1697                .unwrap_or_default();
1698            for step in crate::ime::plan(&event, &edit, &text) {
1699                match step {
1700                    Step::Place { cursor, anchor } => {
1701                        let edit = self.runtime.edits.entry(focused).or_default();
1702                        edit.cursor = cursor;
1703                        edit.anchor = anchor;
1704                    }
1705                    Step::Key(key) => self.apply_key(focused, key),
1706                    Step::Compose(region) => {
1707                        if let Some(edit) = self.runtime.edits.get_mut(&focused) {
1708                            edit.composing = region;
1709                        }
1710                    }
1711                    Step::ComposeTo(start) => {
1712                        if let Some(edit) = self.runtime.edits.get_mut(&focused) {
1713                            let end = edit.cursor;
1714                            edit.composing = (end > start).then_some((start, end));
1715                        }
1716                    }
1717                }
1718            }
1719        }
1720        // Refresh the input context, which the IME queries for its suggestions.
1721        self.push_ime_context(focused);
1722        self.request_redraw();
1723    }
1724
1725    /// Keeps the platform's **autofill service** in step with the form being edited
1726    /// (milestone 512). When a field that says what it is for takes focus, its form is
1727    /// shown to the service and the service is told which field it is in; while it stays,
1728    /// every value the service has not heard is reported, or what it saves is what the
1729    /// fields held when it first looked; and when the form's group has left the screen
1730    /// — submitted, or navigated away from — it is committed, which is the moment a
1731    /// service may offer to save what was typed.
1732    #[cfg(android)]
1733    fn sync_autofill(&mut self) {
1734        use crate::{android_autofill as platform, autofill};
1735        if !crate::android_ime::installed() {
1736            return;
1737        }
1738        let scale = self.total_scale();
1739        let (Some(ui), Some(tree)) = (self.ui.as_ref(), self.tree.as_ref()) else {
1740            return;
1741        };
1742        let fields_of = |id: WidgetId| {
1743            autofill::structure(&ui.form_of(id), scale, |widget| {
1744                find_widget(tree.as_ref(), widget).map(|w| {
1745                    (
1746                        w.autofill_hints(),
1747                        w.text_value().unwrap_or_default().to_owned(),
1748                    )
1749                })
1750            })
1751        };
1752        // The focused field, when it takes part: it says what it is for.
1753        let focus = self.runtime.input.focused.filter(|&id| {
1754            find_widget(tree.as_ref(), id).is_some_and(|w| !w.autofill_hints().is_empty())
1755        });
1756        if focus != self.autofill_focus {
1757            if let Some(left) = self.autofill_focus {
1758                platform::exit(autofill::virtual_id(left));
1759            }
1760            if let Some(entered) = focus {
1761                let group = ui.form_group(entered);
1762                // A field of the form already shown — the next step of a wizard — keeps
1763                // the fields shown before it; any other form starts afresh.
1764                let shown: &[autofill::AutofillField] =
1765                    if group.is_some() && group == self.autofill_group {
1766                        &self.autofill_reported
1767                    } else {
1768                        &[]
1769                    };
1770                let fields = autofill::with_gone(fields_of(entered), shown);
1771                platform::publish(&fields);
1772                platform::enter(autofill::virtual_id(entered));
1773                self.autofill_reported = fields;
1774                self.autofill_group = group;
1775            }
1776            self.autofill_focus = focus;
1777        } else if let Some(id) = focus {
1778            let fields = autofill::with_gone(fields_of(id), &self.autofill_reported);
1779            for (virtual_id, value) in autofill::changed(&self.autofill_reported, &fields) {
1780                platform::value_changed(virtual_id, &value);
1781            }
1782            self.autofill_reported = fields;
1783        }
1784        if let Some(group) = self.autofill_group {
1785            if ui.group_stops(group).is_empty() {
1786                platform::commit();
1787                self.autofill_group = None;
1788                self.autofill_reported.clear();
1789            }
1790        }
1791    }
1792
1793    /// Puts the values an autofill service chose into their fields (milestone 512), each
1794    /// through the message typing it would have produced — the path an undo takes, so a
1795    /// field that refuses typing refuses this too — with the caret after what arrived.
1796    #[cfg(android)]
1797    fn drain_autofill(&mut self) {
1798        let fills = crate::android_autofill::drain();
1799        if fills.is_empty() {
1800            return;
1801        }
1802        for (widget, value) in crate::autofill::route(&self.autofill_reported, fills) {
1803            let message = self
1804                .tree
1805                .as_ref()
1806                .and_then(|tree| find_widget(tree.as_ref(), widget))
1807                .and_then(|w| w.replace_value(value.clone()));
1808            if let Some(message) = message {
1809                self.runtime.edits.insert(
1810                    widget,
1811                    Edit {
1812                        cursor: value.chars().count(),
1813                        anchor: None,
1814                        composing: None,
1815                    },
1816                );
1817                self.dispatch_edit(message);
1818            }
1819        }
1820        if let Some(id) = self.runtime.input.focused {
1821            self.push_ime_context(id);
1822        }
1823        self.request_redraw();
1824    }
1825
1826    /// Publishes field `id`'s editing state to the bridge — text, caret and selection
1827    /// — the context the IME reads for its suggestions.
1828    #[cfg(android)]
1829    fn push_ime_context(&self, id: WidgetId) {
1830        let value = self
1831            .tree
1832            .as_ref()
1833            .and_then(|tree| find_widget(tree.as_ref(), id))
1834            .and_then(|widget| widget.text_value().map(|s| s.to_string()));
1835        if let Some(text) = value {
1836            let edit = self.runtime.edits.get(&id).copied().unwrap_or_default();
1837            crate::android_ime::set_editor_state(&text, edit.cursor, edit.selection_range());
1838            // And the keyboard is **told**, as an Android editor tells it on every change:
1839            // a keyboard that predicts keeps its own model of the field, and one never
1840            // told of a change drifts from it (milestone 510). In UTF-16 units.
1841            let len = text.chars().count();
1842            let at = |i: usize| crate::ime::utf16_index(&text, i.min(len)) as i32;
1843            let (start, end) = edit.selection_range().unwrap_or((edit.cursor, edit.cursor));
1844            let (cand_start, cand_end) = edit
1845                .composing
1846                .map(|(s, e)| (at(s), at(e)))
1847                .unwrap_or((-1, -1));
1848            crate::android_ime::update_selection(at(start), at(end), cand_start, cand_end);
1849        }
1850    }
1851
1852    /// Ends the focused field's composition where it stands: the keyboard is being
1853    /// started again, and a keyboard started again has forgotten it.
1854    #[cfg(android)]
1855    fn end_composition(&mut self) {
1856        if let Some(id) = self.runtime.input.focused {
1857            if let Some(edit) = self.runtime.edits.get_mut(&id) {
1858                edit.composing = None;
1859            }
1860        }
1861    }
1862
1863    /// Remembers the Android activity's handle, the source of the system insets.
1864    #[cfg(android)]
1865    pub(crate) fn set_android_app(
1866        &mut self,
1867        android_app: winit::platform::android::activity::AndroidApp,
1868    ) {
1869        self.android_app = Some(android_app);
1870    }
1871
1872    /// The system insets — the safe area — in **logical** px. On Android they are
1873    /// derived from the activity's content rect, outside the system bars; zero
1874    /// elsewhere.
1875    ///
1876    /// The content rect is what the system leaves the activity, so it excludes
1877    /// **any** decoration the theme asks for — a title bar included. An app whose
1878    /// manifest keeps the default theme therefore reports a top inset of the status
1879    /// bar *plus* 56dp of action bar that is never drawn, and the shell dutifully
1880    /// pads it away: a wide empty band above the app bar. The manifests here ask for
1881    /// `Theme.DeviceDefault.NoActionBar`, and so must any frus app.
1882    fn compute_insets(&self, phys_w: u32, phys_h: u32, scale: f32) -> Insets {
1883        #[cfg(android)]
1884        if let Some(app) = &self.android_app {
1885            let r = app.content_rect();
1886            // A degenerate rect, before the first layout, means no inset.
1887            if r.right > r.left && r.bottom > r.top {
1888                let left = r.left.max(0) as f32;
1889                let top = r.top.max(0) as f32;
1890                let right = (phys_w as i32 - r.right).max(0) as f32;
1891                let bottom = (phys_h as i32 - r.bottom).max(0) as f32;
1892                return Insets::new(top / scale, right / scale, bottom / scale, left / scale);
1893            }
1894        }
1895        let _ = (phys_w, phys_h, scale);
1896        Insets::ZERO
1897    }
1898
1899    /// Asks the platform what it says about its user, and remembers the answer.
1900    ///
1901    /// Marks the build dirty when something moved: the font size reaches the layout
1902    /// through [`MediaQuery::scope`], so a changed scaler is a changed geometry for every
1903    /// widget on the screen, and a frame drawn from the cache would keep the old one.
1904    fn refresh_platform_settings(&mut self) {
1905        #[cfg(android)]
1906        if let Some(app) = &self.android_app {
1907            let read = crate::android_settings::read(app);
1908            if read != self.platform {
1909                self.platform = read;
1910                self.build_dirty = true;
1911            }
1912            return;
1913        }
1914        // Desktop and web report the one thing their window system knows. A text scaler
1915        // is **not** among it: winit exposes no equivalent of Windows'
1916        // `UISettings.TextScaleFactor` or of GNOME's `text-scaling-factor`, so it stays
1917        // at 1 there and an application that knows better says so itself. That is a
1918        // missing wire and is written down rather than papered over.
1919        let brightness = self
1920            .window
1921            .as_ref()
1922            .and_then(|window| window.theme())
1923            .map(|theme| match theme {
1924                winit::window::Theme::Dark => frus_widgets::Brightness::Dark,
1925                winit::window::Theme::Light => frus_widgets::Brightness::Light,
1926            })
1927            .unwrap_or_default();
1928        if brightness != self.platform.brightness {
1929            self.platform.brightness = brightness;
1930            self.build_dirty = true;
1931        }
1932
1933        // **The reader's languages**, read once. Changing the display language on a
1934        // desktop means signing out and back in on Windows, and is a per-process
1935        // environment variable on Linux — so there is nothing to watch for, and re-reading
1936        // it every time the window comes back would allocate for an answer that cannot
1937        // have moved. Android is the platform where it *does* change under a running
1938        // application, and Android reads it on every walk.
1939        if self.platform.locales.is_empty() {
1940            let read = platform_locales();
1941            if !read.is_empty() {
1942                self.platform.locales = read;
1943                self.build_dirty = true;
1944            }
1945        }
1946    }
1947}
1948
1949/// **The reader's preferred languages**, best first, from whatever the platform keeps
1950/// them in. Empty when it says nothing, or when there is no way to ask.
1951///
1952/// Android does not come through here: its answer lives on the activity's
1953/// `Configuration`, which [`crate::android_settings`] already walks.
1954/// On **Android** there is nothing to ask here: the answer lives on the activity's
1955/// `Configuration`, and the branch above returns before reaching this. A build with no
1956/// activity at all has no reader to ask about.
1957#[cfg(android)]
1958fn platform_locales() -> Vec<frus_widgets::Locale> {
1959    Vec::new()
1960}
1961
1962#[cfg(not(android))]
1963fn platform_locales() -> Vec<frus_widgets::Locale> {
1964    #[cfg(not(web))]
1965    {
1966        sys_locale::get_locales()
1967            .filter_map(|tag| frus_widgets::Locale::parse(&tag))
1968            .collect()
1969    }
1970    #[cfg(web)]
1971    {
1972        // `navigator.languages` is the ordered list; `navigator.language` is its first
1973        // entry and the fallback for a browser that does not offer the list.
1974        let Some(navigator) = web_sys::window().map(|window| window.navigator()) else {
1975            return Vec::new();
1976        };
1977        let listed: Vec<frus_widgets::Locale> = navigator
1978            .languages()
1979            .iter()
1980            .filter_map(|value| value.as_string())
1981            .filter_map(|tag| frus_widgets::Locale::parse(&tag))
1982            .collect();
1983        if listed.is_empty() {
1984            return navigator
1985                .language()
1986                .and_then(|tag| frus_widgets::Locale::parse(&tag))
1987                .into_iter()
1988                .collect();
1989        }
1990        listed
1991    }
1992}
1993
1994/// Gives back the decoded images nobody is showing, and says how much went.
1995fn memory_trimmed() {
1996    let before = frus_widgets::image_cache_bytes();
1997    frus_widgets::trim_image_caches();
1998    log::info!(
1999        "memory is low: decoded network images trimmed from {} to {} KB",
2000        before / 1024,
2001        frus_widgets::image_cache_bytes() / 1024
2002    );
2003}
2004
2005impl<A: Application> ApplicationHandler<A::Message> for App<A> {
2006    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
2007        if self.window.is_some() {
2008            return;
2009        }
2010        // Back in the foreground, or starting up: the surface is (re)born below.
2011        self.set_lifecycle(Lifecycle::Resumed);
2012        // And so is what the platform says about its user — a trip to the system
2013        // settings and back is exactly this event.
2014        self.refresh_platform_settings();
2015
2016        let mut attributes = Window::default_attributes()
2017            .with_title(self.app.title())
2018            // A sensible minimum size, in logical px, to avoid an absurd UI.
2019            .with_min_inner_size(winit::dpi::LogicalSize::new(360.0, 280.0));
2020        // The AccessKit adapter must be created **before** the window is shown, so we
2021        // create it hidden and reveal it afterwards. Desktop only.
2022        #[cfg(desktop)]
2023        {
2024            attributes = attributes.with_visible(false);
2025        }
2026        // The application's icon, on the window (Windows and X11; Wayland and macOS take it
2027        // from the bundle and ignore this). Windows keeps a second, larger one for the taskbar
2028        // and Alt+Tab, set on its own.
2029        #[cfg(desktop)]
2030        {
2031            let icon = crate::icon::window_icon(&self.app.icon());
2032            #[cfg(windows)]
2033            {
2034                use winit::platform::windows::WindowAttributesExtWindows;
2035                attributes = attributes.with_taskbar_icon(icon.clone());
2036            }
2037            attributes = attributes.with_window_icon(icon);
2038        }
2039        // Not on the Web: there the page sizes the canvas, with its own CSS, and asking winit
2040        // for a size writes it **inline** on the canvas, where it outranks the stylesheet — an
2041        // application that named 900 by 680 then stayed 900 by 680 in a phone-sized window,
2042        // cropped, its drawer off the edge of what could be seen.
2043        #[cfg(not(web))]
2044        if let Some((w, h)) = self.app.window_size() {
2045            attributes = attributes.with_inner_size(winit::dpi::LogicalSize::new(w, h));
2046        }
2047        // A simulated device: a window the device's shape, shrunk to fit the monitor
2048        // (milestone 721). What the application asked for is the desktop window's size.
2049        #[cfg(desktop)]
2050        if let Some(simulated) = &self.simulated {
2051            let room = simulated.window_size();
2052            let fit = event_loop
2053                .primary_monitor()
2054                .map(|monitor| {
2055                    let screen = monitor.size().to_logical::<f32>(monitor.scale_factor());
2056                    (0.9 * screen.width / room.width)
2057                        .min(0.9 * screen.height / room.height)
2058                        .min(1.0)
2059                })
2060                .unwrap_or(1.0);
2061            attributes = attributes.with_inner_size(winit::dpi::LogicalSize::new(
2062                room.width * fit,
2063                room.height * fit,
2064            ));
2065        }
2066        // Restarted by `frus run`: where the window was, as big as it was.
2067        #[cfg(desktop)]
2068        if let Some([x, y, w, h]) = crate::dev::restore_window() {
2069            attributes = attributes
2070                .with_position(winit::dpi::PhysicalPosition::new(x, y))
2071                .with_inner_size(winit::dpi::PhysicalSize::new(w as u32, h as u32));
2072        }
2073        // Web: winit creates a `<canvas>` and **appends it** to the document's body.
2074        #[cfg(web)]
2075        {
2076            use winit::platform::web::WindowAttributesExtWebSys;
2077            attributes = attributes.with_append(true);
2078        }
2079        let window = match event_loop.create_window(attributes) {
2080            Ok(window) => {
2081                crate::startup::window_made();
2082                #[cfg(any(
2083                    all(desktop, feature = "file-dialogs"),
2084                    all(any(desktop, android), feature = "notifications")
2085                ))]
2086                crate::interface_thread::window_made();
2087                #[cfg(all(desktop, feature = "file-dialogs"))]
2088                crate::file_dialog::window_made(&window);
2089                #[cfg(all(desktop, feature = "notifications"))]
2090                crate::notification::window_made(&self.app.title(), self.app.icon().png());
2091                Arc::new(window)
2092            }
2093            Err(err) => {
2094                log::error!("failed to create the window: {err}");
2095                event_loop.exit();
2096                return;
2097            }
2098        };
2099
2100        // Web: GPU init is **asynchronous**, blocking being impossible. We start the
2101        // future, and the ready renderer is picked up on the first frame — see
2102        // `RedrawRequested`. `init` runs right away, since it does not touch the GPU.
2103        #[cfg(web)]
2104        {
2105            self.scale = window.scale_factor() as f32;
2106            self.window = Some(window.clone());
2107            // The accessibility bridge (#18): the canvas exists as soon as the window
2108            // does, so there is no "still hidden" step to wait for here.
2109            self.a11y = crate::a11y_web::A11y::new(&window);
2110            // And the element a text field is edited through (milestone 711).
2111            self.web_editor = crate::web_editing::Editor::new(&window);
2112            // The browser keeps reload, the developer tools and the rest of its own shortcuts
2113            // while the canvas has the focus, instead of winit cancelling them.
2114            crate::web_keys::let_the_browser_keep_its_shortcuts();
2115            // The tab's icon: the application's, or the frus logo where the page has none.
2116            crate::web_icon::apply(&self.app.icon());
2117            self.build_dirty = true;
2118            if !self.started {
2119                self.started = true;
2120                let command = self.app.init();
2121                self.run_command(command);
2122                self.sync_subscriptions();
2123                self.open_launch_address(&window);
2124                crate::dev_commands::set_window(Some(window.clone()));
2125                if let Some(states) = crate::web_dev::start() {
2126                    self.runtime.states.restore_from(&states);
2127                }
2128                #[cfg(feature = "hot-reload")]
2129                {
2130                    crate::hot::set_window(window.clone());
2131                    crate::hot::connect();
2132                }
2133            }
2134            let slot = self.pending_renderer.clone();
2135            let win = window.clone();
2136            let size = window.inner_size();
2137            wasm_bindgen_futures::spawn_local(async move {
2138                match Renderer::new(win.clone(), size.width.max(1), size.height.max(1)).await {
2139                    Ok(r) => {
2140                        crate::startup::renderer_made(r.startup_times());
2141                        *slot.borrow_mut() = Some(r);
2142                        win.request_redraw();
2143                    }
2144                    Err(err) => log::error!("failed to initialise the renderer (Web): {err:#}"),
2145                }
2146            });
2147        }
2148
2149        #[cfg(not(web))]
2150        {
2151            let size = window.inner_size();
2152            let (width, height) = (size.width.max(1), size.height.max(1));
2153            // On Windows, a renderer whose frames let the system's own title bar show where
2154            // the window's menu bar leaves the line transparent (milestone 643); the
2155            // ordinary one where that cannot be had.
2156            let see_through = if cfg!(windows) {
2157                match pollster::block_on(Renderer::see_through(window.clone(), width, height)) {
2158                    Ok(renderer) => Some(renderer),
2159                    Err(err) => {
2160                        log::info!("title bar painted by frus: {err:#}");
2161                        None
2162                    }
2163                }
2164            } else {
2165                None
2166            };
2167            let renderer = match see_through {
2168                Some(renderer) => Ok(renderer),
2169                None => pollster::block_on(Renderer::new(window.clone(), width, height)),
2170            };
2171
2172            match renderer {
2173                Ok(renderer) => {
2174                    crate::startup::renderer_made(renderer.startup_times());
2175                    self.scale = window.scale_factor() as f32;
2176                    // The accessibility bridge (AccessKit), created while the window is
2177                    // still hidden, after which we reveal it. Inert with no screen reader.
2178                    #[cfg(desktop)]
2179                    {
2180                        self.a11y = Some(crate::a11y::A11y::new(event_loop, &window));
2181                        window.set_visible(true);
2182                        // A second process's link brings this window forward and asks it for
2183                        // a frame.
2184                        crate::instance::set_window(window.clone());
2185                    }
2186                    self.window = Some(window.clone());
2187                    // What the input bridge's wakes ask a frame of (milestone 513).
2188                    #[cfg(android)]
2189                    crate::android_ime::set_window(Some(window.clone()));
2190                    #[cfg(android)]
2191                    crate::dev_commands::set_window(Some(window.clone()));
2192                    self.renderer = Some(renderer);
2193                    // The surface was (re)created: force a full rebuild on the first frame.
2194                    self.build_dirty = true;
2195                    // The startup effect, an initial load and so on: once only, not on
2196                    // every surface recreation, such as returning from the background.
2197                    if !self.started {
2198                        self.started = true;
2199                        let command = self.app.init();
2200                        self.run_command(command);
2201                        self.sync_subscriptions();
2202                        #[cfg(any(android, desktop))]
2203                        self.open_launch_link();
2204                        #[cfg(any(android, desktop))]
2205                        self.connect_dev();
2206                        #[cfg(feature = "hot-reload")]
2207                        {
2208                            crate::hot::set_window(window.clone());
2209                            crate::hot::connect();
2210                        }
2211                    }
2212                    window.request_redraw();
2213                }
2214                Err(err) => {
2215                    log::error!("failed to initialise the renderer: {err:#}");
2216                    event_loop.exit();
2217                }
2218            }
2219        }
2220    }
2221
2222    /// Going to the background, on Android: the native surface is destroyed. We
2223    /// release the renderer and the window, and `resumed` recreates them on the way
2224    /// back, without replaying `init` — see `started`. Harmless on desktop, where the
2225    /// event never fires.
2226    fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
2227        // Background: the surface is lost, so the app moves to `Paused`.
2228        self.set_lifecycle(Lifecycle::Paused);
2229        // Under `frus run`, this is how a restart begins on Android: the states are kept.
2230        #[cfg(android)]
2231        if self.dev.is_some() {
2232            if let Some(storage) = self
2233                .android_app
2234                .as_ref()
2235                .and_then(|a| a.internal_data_path())
2236            {
2237                crate::dev::keep_states(&storage, &self.runtime.states.snapshot());
2238            }
2239        }
2240        self.renderer = None;
2241        self.window = None;
2242        #[cfg(android)]
2243        crate::android_ime::set_window(None);
2244        #[cfg(android)]
2245        crate::dev_commands::set_window(None);
2246        self.last_frame = None;
2247    }
2248
2249    /// The event loop is ending, on close: a final `Detached` notification, where the
2250    /// app can persist its state before the process disappears.
2251    fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
2252        self.set_lifecycle(Lifecycle::Detached);
2253    }
2254
2255    /// The system is short of memory (Android's low-memory callback, iOS's memory
2256    /// warning): the decoded images nobody is showing are given back, rather than left
2257    /// for the system to reclaim by ending the process.
2258    fn memory_warning(&mut self, _event_loop: &ActiveEventLoop) {
2259        memory_trimmed();
2260    }
2261
2262    /// A message produced by an effect, on a background thread: we apply it and ask
2263    /// for another frame.
2264    fn user_event(&mut self, _event_loop: &ActiveEventLoop, message: A::Message) {
2265        // A click on a notification brings the window forward with its message.
2266        #[cfg(all(desktop, feature = "notifications"))]
2267        if crate::notification::take_raise() {
2268            if let Some(window) = &self.window {
2269                window.set_minimized(false);
2270                window.focus_window();
2271            }
2272        }
2273        self.dispatch(message);
2274        self.request_redraw();
2275    }
2276
2277    /// The loop has run out of events: dropped files are handed over now that every one
2278    /// of them has arrived, and files still held over the window are looked for where
2279    /// the system can say (milestone 695).
2280    fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
2281        #[cfg(all(desktop, feature = "global-shortcuts"))]
2282        self.shortcut_presses();
2283        #[cfg(all(desktop, feature = "tray"))]
2284        self.tray_events(event_loop);
2285        if self.file_drag.is_none() {
2286            return;
2287        }
2288        self.deliver_dropped_files();
2289        if self.file_drag.is_some() {
2290            let at = self.system_cursor();
2291            self.files_moved(at);
2292        }
2293        event_loop.set_control_flow(self.idle_control_flow());
2294    }
2295
2296    /// The loop woke up: if the long-press deadline has been reached the recogniser
2297    /// **accepts eagerly** — the message is emitted and the release to come will be
2298    /// swallowed, the long press having evicted the tap.
2299    fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) {
2300        if matches!(cause, StartCause::ResumeTimeReached { .. }) && self.press.poll(Instant::now())
2301        {
2302            self.hold_deadline_reached();
2303        }
2304        if matches!(cause, StartCause::ResumeTimeReached { .. }) {
2305            self.poll_pending_tap(Instant::now());
2306        }
2307
2308        // The caret's turn is due: a frame to show it, or to hide it, in (milestone 513).
2309        if matches!(cause, StartCause::ResumeTimeReached { .. })
2310            && self.lifecycle == Lifecycle::Resumed
2311            && self.caret.next_toggle(Instant::now()).is_some()
2312        {
2313            self.request_redraw();
2314        }
2315
2316        // Pending IME operations, from the Android input bridge.
2317        #[cfg(android)]
2318        self.drain_ime();
2319        // And the values an autofill service chose, from the same bridge.
2320        #[cfg(android)]
2321        self.drain_autofill();
2322
2323        // Actions an assistive technology asked for, through AccessKit.
2324        #[cfg(desktop)]
2325        self.drain_a11y_actions();
2326        // Actions a screen reader asked for, through the DOM bridge (#18).
2327        #[cfg(web)]
2328        self.drain_a11y_actions();
2329        // And what the browser did to the field it edits for the application (milestone 711).
2330        #[cfg(web)]
2331        self.drain_web_editing();
2332
2333        // Live reload, in development: the binary was replaced by a recompilation, so
2334        // capture the state and relaunch the new binary. This does not return.
2335        if let Some(watcher) = self.reload.as_mut() {
2336            if watcher.binary_changed() {
2337                watcher.handoff(self.app.save_state());
2338            }
2339        }
2340        event_loop.set_control_flow(self.idle_control_flow());
2341    }
2342
2343    fn window_event(
2344        &mut self,
2345        event_loop: &ActiveEventLoop,
2346        _window_id: WindowId,
2347        event: WindowEvent,
2348    ) {
2349        // The AccessKit adapter observes the window events: focus, size and so on.
2350        #[cfg(desktop)]
2351        if let (Some(a11y), Some(window)) = (self.a11y.as_mut(), self.window.as_ref()) {
2352            a11y.process_event(window, &event);
2353        }
2354
2355        match event {
2356            WindowEvent::CloseRequested => {
2357                // A tray that keeps the application running: the window goes, the
2358                // application stays, until a quit item (milestone 706).
2359                #[cfg(all(desktop, feature = "tray"))]
2360                if self.tray.is_some() && self.tray_keeps_running {
2361                    if let Some(window) = &self.window {
2362                        window.set_visible(false);
2363                    }
2364                    return;
2365                }
2366                event_loop.exit()
2367            }
2368
2369            // Gaining or losing focus **in the foreground**: `Resumed` ⇄ `Inactive`. We
2370            // leave `Paused`/`Detached` — background and closing — alone; those are
2371            // decided by `suspended` and `exiting`.
2372            WindowEvent::Focused(focused) => {
2373                // On the web the "window" is the canvas, and it loses the focus to the page's
2374                // own elements — the accessibility bridge's, the field being edited — while
2375                // the page keeps it (milestone 711). Only the page losing it is the window's.
2376                #[cfg(web)]
2377                let focused = focused
2378                    || web_sys::window()
2379                        .and_then(|window| window.document())
2380                        .and_then(|document| document.has_focus().ok())
2381                        .unwrap_or(false);
2382                if !matches!(self.lifecycle, Lifecycle::Paused | Lifecycle::Detached) {
2383                    self.set_lifecycle(if focused {
2384                        Lifecycle::Resumed
2385                    } else {
2386                        Lifecycle::Inactive
2387                    });
2388                }
2389                // The line the window shares with the system quietens with it, as the
2390                // system's own caption does: a frame reads the window's state again
2391                // (milestone 643).
2392                #[cfg(windows)]
2393                if self.title_bar_on {
2394                    self.request_redraw();
2395                }
2396            }
2397
2398            WindowEvent::Resized(size) => {
2399                if let Some(renderer) = self.renderer.as_mut() {
2400                    renderer.resize(size.width, size.height);
2401                }
2402                if let Some(window) = &self.window {
2403                    window.request_redraw();
2404                }
2405            }
2406
2407            WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
2408                self.scale = scale_factor as f32;
2409                // Reconfigure the surface to the current physical size.
2410                if let Some(window) = &self.window {
2411                    let size = window.inner_size();
2412                    if let Some(renderer) = self.renderer.as_mut() {
2413                        renderer.resize(size.width, size.height);
2414                    }
2415                }
2416                self.request_redraw();
2417            }
2418
2419            WindowEvent::Occluded(occluded) => {
2420                self.occluded = occluded;
2421                if !occluded {
2422                    self.request_redraw();
2423                }
2424            }
2425
2426            // Every pointer source, mouse and touch alike, converges on the
2427            // **normalised** `pointer()` input — gesture tier 0, with an explicit
2428            // `Cancel`. winit hands us physical px; we work in logical ones, the total
2429            // scale being DPI × density.
2430            WindowEvent::CursorMoved { position, .. } => {
2431                let position = self.to_logical(position.x, position.y);
2432                self.pointer(
2433                    event_loop,
2434                    PointerEvent {
2435                        kind: PointerKind::Move,
2436                        position,
2437                        touch: false,
2438                    },
2439                );
2440            }
2441
2442            // Files dragged onto the window from outside the application, one event per
2443            // file, held or dropped — and taken away again (milestone 695). They land on a
2444            // `DragTarget`, like an item carried from inside.
2445            WindowEvent::HoveredFile(path) => {
2446                let at = self.system_cursor();
2447                self.file_event(path, false, at);
2448                event_loop.set_control_flow(self.idle_control_flow());
2449            }
2450            WindowEvent::DroppedFile(path) => {
2451                let at = self.system_cursor();
2452                self.file_event(path, true, at);
2453            }
2454            WindowEvent::HoveredFileCancelled => self.files_left(),
2455
2456            // The mouse left the window: nothing in it is under the pointer any more.
2457            WindowEvent::CursorLeft { .. } => {
2458                self.hover.left();
2459                self.sync_hover();
2460                self.sync_regions(false);
2461            }
2462
2463            WindowEvent::Touch(touch) => {
2464                let position = self.to_logical(touch.location.x, touch.location.y);
2465                let kind = match touch.phase {
2466                    TouchPhase::Started => PointerKind::Down,
2467                    TouchPhase::Moved => PointerKind::Move,
2468                    TouchPhase::Ended => PointerKind::Up,
2469                    TouchPhase::Cancelled => PointerKind::Cancel,
2470                };
2471                self.pointer(
2472                    event_loop,
2473                    PointerEvent {
2474                        kind,
2475                        position,
2476                        touch: true,
2477                    },
2478                );
2479            }
2480
2481            WindowEvent::ModifiersChanged(modifiers) => {
2482                let state = modifiers.state();
2483                self.shift = state.shift_key();
2484                self.ctrl = state.control_key();
2485                self.alt = state.alt_key();
2486                self.meta = state.super_key();
2487            }
2488
2489            WindowEvent::MouseInput {
2490                state: ElementState::Pressed,
2491                button: MouseButton::Left,
2492                ..
2493            } => self.pointer(
2494                event_loop,
2495                PointerEvent {
2496                    kind: PointerKind::Down,
2497                    position: self.cursor,
2498                    touch: false,
2499                },
2500            ),
2501
2502            WindowEvent::MouseInput {
2503                state: ElementState::Released,
2504                button: MouseButton::Left,
2505                ..
2506            } => self.pointer(
2507                event_loop,
2508                PointerEvent {
2509                    kind: PointerKind::Up,
2510                    position: self.cursor,
2511                    touch: false,
2512                },
2513            ),
2514
2515            // A right-click on a text field opens its selection bar (milestone 568), which is
2516            // the desktop's context menu for the four things it holds.
2517            WindowEvent::MouseInput {
2518                state: ElementState::Pressed,
2519                button: MouseButton::Right,
2520                ..
2521            } => self.context_click(),
2522
2523            WindowEvent::KeyboardInput { event, .. } if event.state == ElementState::Pressed => {
2524                self.key_down(&KeyDown::from(&event), Some(event_loop));
2525                // And the element hears at once where the key left the field: a letter the
2526                // browser types straight after an arrow goes in at the element's caret, and
2527                // that caret must not wait for the frame to have moved (milestone 711).
2528                #[cfg(web)]
2529                self.sync_web_editing();
2530            }
2531
2532            WindowEvent::MouseWheel { delta, .. } => {
2533                let (mut dx, mut dy) = match delta {
2534                    MouseScrollDelta::LineDelta(x, y) => (x * SCROLL_SPEED, y * SCROLL_SPEED),
2535                    // Physical delta → logical.
2536                    MouseScrollDelta::PixelDelta(pos) => {
2537                        let scale = self.total_scale();
2538                        (pos.x as f32 / scale, pos.y as f32 / scale)
2539                    }
2540                };
2541                // With Shift, the wheel scrolls horizontally.
2542                if self.shift {
2543                    dx = dy;
2544                    dy = 0.0;
2545                }
2546                // Over an interactive viewport the wheel **zooms**, anchored at the
2547                // pointer and bounded by the widget's min and max scales.
2548                if let Some((id, viewport)) = self
2549                    .ui
2550                    .as_ref()
2551                    .and_then(|ui| ui.interactive_at(self.cursor))
2552                {
2553                    let (min, max) = self
2554                        .tree
2555                        .as_ref()
2556                        .and_then(|tree| find_widget(tree.as_ref(), id))
2557                        .and_then(|w| w.interactive())
2558                        .unwrap_or((0.5, 4.0));
2559                    // Wheel up (`dy > 0`) zooms in, in gentle steps of about 1.1× a notch.
2560                    let factor = (1.0 + dy * 0.1 / SCROLL_SPEED).clamp(0.2, 5.0);
2561                    // Zooming cuts off a fling in progress.
2562                    self.runtime.interactive_velocity.remove(&id);
2563                    let view = self.runtime.interactive.entry(id).or_default();
2564                    // Zoom anchored at the pointer, then bounded to the frame.
2565                    *view = view
2566                        .zoom_at(factor, self.cursor, min, max)
2567                        .clamped(viewport);
2568                    self.request_redraw();
2569                    return;
2570                }
2571                // A wheel asks the same question a finger does, and the reference gates it
2572                // the same way: an area with everything already in sight takes no offset,
2573                // and the notch belongs to whatever is behind it.
2574                let hit = {
2575                    let scroll = &self.runtime.scroll;
2576                    self.ui.as_ref().and_then(|ui| {
2577                        ui.scroll_chain(self.cursor).find(|area| {
2578                            area.accepts_user_offset(
2579                                scroll.get(&area.id).copied().unwrap_or((0.0, 0.0)),
2580                            )
2581                        })
2582                    })
2583                };
2584                if let Some(area) = hit {
2585                    // Scrolling with inertia: the wheel pushes the TARGET and the
2586                    // spring eases across to it. How far past the ends that target
2587                    // may go is the physics' call — a little elastic overshoot where
2588                    // the platform bounces, none at all where it does not.
2589                    let id = area.id;
2590                    let physics = area.physics;
2591                    let over = if physics.allows_overscroll() {
2592                        SCROLL_OVER
2593                    } else {
2594                        0.0
2595                    };
2596                    // A notch of the wheel is a new intent: it takes over from the
2597                    // momentum of the last gesture.
2598                    self.runtime.stop_scroll_fling(id);
2599                    let current = self.runtime.scroll.get(&id).copied().unwrap_or((0.0, 0.0));
2600                    // A screen delta into offsets: the content moves opposite the number,
2601                    // and a reversed axis counts from the other end. One function, so a
2602                    // reversed area is right at every one of the places this happens.
2603                    let (ddx, ddy) = area.offset_delta((dx, dy));
2604                    let target = self.runtime.scroll_target.entry(id).or_insert(current);
2605                    let (wanted_x, wanted_y) = (target.0 + ddx, target.1 + ddy);
2606                    target.0 = wanted_x.clamp(-over, area.max_x + over);
2607                    target.1 = wanted_y.clamp(-over, area.max_y + over);
2608                    let refused = (wanted_x - target.0, wanted_y - target.1);
2609                    self.runtime.scroll_velocity.entry(id).or_insert((0.0, 0.0));
2610                    // A notch that asks to go past the end deserves the same
2611                    // acknowledgement a finger gets: the wheel is a gesture too.
2612                    let cursor = self.cursor;
2613                    for (refused, vertical, extent) in [
2614                        (refused.0, false, area.viewport.width),
2615                        (refused.1, true, area.viewport.height),
2616                    ] {
2617                        if refused.abs() < 1e-3 {
2618                            continue;
2619                        }
2620                        let edge = area.refused_edge(vertical, refused);
2621                        let (offset, cross) =
2622                            frus_widgets::glow_cross_axis(area.viewport, edge, cursor);
2623                        self.runtime.glow_pull(
2624                            id,
2625                            area.overscroll,
2626                            edge,
2627                            refused,
2628                            extent,
2629                            offset,
2630                            cross,
2631                        );
2632                        // A wheel has no "lift off", so the pull is released at once
2633                        // and simply fades — otherwise it would hang until the hold
2634                        // timer expired.
2635                        self.runtime.glow_scroll_end(id);
2636                    }
2637                    self.request_redraw();
2638                }
2639            }
2640
2641            WindowEvent::RedrawRequested => {
2642                // The GPU device was lost on the last frame: a new renderer first.
2643                if std::mem::take(&mut self.renderer_lost) {
2644                    self.rebuild_renderer();
2645                }
2646                // Measured only when asked: a clock read per stage is cheap, but not free.
2647                let measuring = self.frame_stats.enabled();
2648                let frame_start = measuring.then(Instant::now);
2649                // The stages are timed for the first frame too, whose line breaks the launch
2650                // down (milestone 696); after it, one atomic load.
2651                let timed = measuring || crate::startup::pending();
2652                self.frame_costs = crate::frame_stats::FrameCosts::default();
2653                // Web: pick up the asynchronously initialised renderer as soon as it
2654                // is ready; until then, nothing is painted.
2655                #[cfg(web)]
2656                if self.renderer.is_none() {
2657                    match self.pending_renderer.borrow_mut().take() {
2658                        Some(mut renderer) => {
2659                            // The renderer was sized from whatever `inner_size()` read
2660                            // back when it was spawned — often 0×0, before the canvas
2661                            // had been laid out by CSS. A `Resized` event carrying the
2662                            // real size may have arrived and been dropped in the
2663                            // meantime, since `self.renderer` was still `None` then.
2664                            // Catching up here, against the size right now, is what
2665                            // makes that race harmless either way.
2666                            if let Some(window) = &self.window {
2667                                let size = window.inner_size();
2668                                renderer.resize(size.width.max(1), size.height.max(1));
2669                            }
2670                            self.renderer = Some(renderer);
2671                        }
2672                        None => return,
2673                    }
2674                }
2675                // A paste the browser has answered since the last frame goes in before
2676                // this one is built. Elsewhere a paste is answered on the spot and this
2677                // finds nothing.
2678                while let Some(pasted) = self.clipboard.take_answered() {
2679                    self.land_paste(pasted);
2680                }
2681                // The widgets hear where the focus went since the last frame, before this
2682                // one is built from what they answer (milestone 660).
2683                self.report_focus();
2684                // A bar whose field is no longer the focused one is over: a field focused
2685                // again later must not find it open.
2686                if self
2687                    .runtime
2688                    .selection_toolbar
2689                    .is_some_and(|mark| self.runtime.input.focused != Some(mark.id))
2690                {
2691                    self.runtime.selection_toolbar = None;
2692                }
2693                // Occluded window: rendering is suspended, resuming on Occluded(false).
2694                if self.occluded {
2695                    return;
2696                }
2697                let size = self
2698                    .window
2699                    .as_ref()
2700                    .map(|w| w.inner_size())
2701                    .unwrap_or_default();
2702                // Minimised, or zero-sized: nothing to draw, which avoids GPU errors.
2703                if size.width == 0 || size.height == 0 {
2704                    self.last_frame = None; // no dt jump when it is restored
2705                    return;
2706                }
2707                // The interface is described in **logical** pixels; the GPU output is
2708                // scaled to physical ones (DPI × density) just before rendering.
2709                let scale = self.total_scale();
2710                let (width, height) = self.logical_size(size.width, size.height);
2711
2712                // A logical size change — a resize OR a density change — notifies the
2713                // app before the view, so it can react to the breakpoint in its logic.
2714                if self.last_size != Some((width, height)) {
2715                    self.last_size = Some((width, height));
2716                    self.build_dirty = true;
2717                    self.app.on_resize(width, height);
2718                }
2719
2720                // Window insets: separates the static **padding** — bars and notch —
2721                // from the **keyboard**, which is whatever bottom inset exceeds the
2722                // keyboard-free baseline. The baseline is taken at the first measurement
2723                // for this physical size (a rotation gives a new one), and corrects
2724                // downwards should a barer state appear: the keyboard open at startup,
2725                // the bars hidden, and so on.
2726                let raw = self.compute_insets(size.width, size.height, scale);
2727                let phys = (size.width, size.height);
2728                let mut baseline = match self.inset_baseline {
2729                    Some((b, s)) if s == phys => b,
2730                    _ => {
2731                        self.inset_baseline = Some((raw, phys));
2732                        raw
2733                    }
2734                };
2735                if raw.bottom < baseline.bottom {
2736                    baseline = raw;
2737                    self.inset_baseline = Some((raw, phys));
2738                }
2739                let insets = match &self.simulated {
2740                    // A simulated device's bars and keyboard are the profile's.
2741                    Some(simulated) => simulated.insets(),
2742                    None => WindowInsets::from_baseline(baseline, raw),
2743                };
2744                if self.last_insets != insets {
2745                    if insets.view_insets.bottom > self.last_insets.view_insets.bottom {
2746                        self.reveal_after_keyboard = true;
2747                    }
2748                    self.last_insets = insets;
2749                    self.build_dirty = true;
2750                    self.app.on_insets(insets);
2751                }
2752
2753                // The elapsed dt, clamped, for every animation.
2754                let now = Instant::now();
2755                let dt = self
2756                    .last_frame
2757                    .map(|prev| (now - prev).as_secs_f32().min(0.05))
2758                    .unwrap_or(0.0);
2759                self.last_frame = Some(now);
2760
2761                // A continuous clock, in seconds, for the time-driven animations.
2762                self.elapsed += dt;
2763                self.runtime.time = self.elapsed;
2764
2765                // The caret's blink (milestone 513), on the wall clock: the animation
2766                // clock above is clamped per frame and stands still between frames, and a
2767                // caret at rest is exactly when there are none. Solid while the window is
2768                // not the one in front, where nothing wakes the loop for it.
2769                let caret_now = Instant::now();
2770                let caret = if self.lifecycle == Lifecycle::Resumed {
2771                    self.caret_signature()
2772                } else {
2773                    None
2774                };
2775                self.caret.observe(caret_now, caret);
2776                self.runtime.caret_hidden = self.caret.hidden(caret_now);
2777
2778                // The application advances its own animations: navigation, gesture.
2779                let mut app_animating = self.app.tick(dt);
2780
2781                // And the framework advances the **theme**, which is its own work: it
2782                // resolves the application's themes against the brightness the platform
2783                // reports and the contrast the reader asked for, and crosses from the one
2784                // on screen to the one now wanted over `theme_animation_duration`.
2785                //
2786                // Both halves of that were the application's before milestone 452, and
2787                // both were being done by hand in this repo's own demonstration — which
2788                // is a fair sign of what every application was having to write.
2789                let settings = self
2790                    .platform
2791                    .accessibility
2792                    .with_overrides(self.app.accessibility());
2793
2794                // **The ambient scopes, before anything reads them** — which the theme
2795                // does: the direction of the layout follows the language, so an
2796                // application whose theme is right-to-left in Arabic needs the language
2797                // installed before its theme is asked for. This used to sit further down,
2798                // beside the build, where it was in time for the widgets and one frame
2799                // late for the theme.
2800                //
2801                // Read every frame, all of it: these are the application's answers and
2802                // they may change while it is running.
2803                install_ambient(&self.app, &self.platform.locales);
2804
2805                let theme_moved = self.themes.advance(
2806                    &self.app,
2807                    self.brightness(),
2808                    settings.high_contrast,
2809                    settings.disable_animations,
2810                    dt,
2811                );
2812                // A theme that moved is a rebuild — the view is a pure function of
2813                // `(state, theme, size)` — and a fade still running is another frame.
2814                app_animating |= theme_moved | self.themes.animating();
2815                let theme = self.themes.displayed(&self.app);
2816
2817                // **The surface, in force for the whole frame** — not just for `view`.
2818                // A size becomes a number in three places: while the widgets are built,
2819                // while they are measured and laid out, and while they are painted.
2820                // Scoping the build alone left the last two at scale 1, so the layout
2821                // measured one size and the renderer drew another, and the reader's
2822                // setting reached a device without moving a single pixel (milestone 407).
2823                //
2824                // One guard, not two: the description and the font size are installed by
2825                // the same call and released by the same drop, so they cannot be held for
2826                // different lengths of time. Holding them separately is the same bug with
2827                // an extra step (milestone 408). It lives to the end of this frame and
2828                // puts back what was there, panic or not.
2829                let _surface = self.media_query(width, height).install();
2830
2831                // === BUILD phase, conditional ===
2832                // The `view` is rebuilt only when the app's state or the size changed
2833                // (`build_dirty`), or when the app is animating, theme, navigation and
2834                // gesture all altering the state the view reads. A frame that animates
2835                // interaction alone — hover, scroll, focus, caret — **reuses the
2836                // retained tree** and merely repaints: the `view` is a pure function of
2837                // `(state, theme, size)` and never of the `Runtime`.
2838                // The user's motion setting reaches the runtime before anything is
2839                // advanced with it, and it is read every frame: a settings screen with a
2840                // *reduce motion* switch changes it while the application is running.
2841                self.runtime.still = settings.disable_animations;
2842                // And where the pointer stands in relation to a bar, from the previous
2843                // frame's registry — the only one there is at this point in the frame.
2844                //
2845                // Near a bar, it holds it open and warms the thumb; a bar that has faded
2846                // out entirely still answers here, and comes back for the reach. On a
2847                // touch platform nothing draws a bar in the first place, so the registry
2848                // is empty and a lingering finger position says nothing.
2849                self.runtime.scrollbar_hovered = self
2850                    .ui
2851                    .as_ref()
2852                    .and_then(|ui| ui.scrollbar_near(self.cursor))
2853                    .map(|bar| bar.id);
2854                self.runtime.scrollbar_dragged = match self.drag {
2855                    Some(Drag::Scrollbar { id, .. }) => Some(id),
2856                    _ => None,
2857                };
2858                // A switcher in flight carries each child's progress as a number built into
2859                // the tree, so it is rebuilt — not only repainted — until the switch settles.
2860                // Something a component keeps changed — a `set_state`, a hook, a controller —
2861                // and what it builds is built again.
2862                // What components asked of the shell since the last frame — a focus, a scroll,
2863                // work for another thread — is run first: it may ask for this very rebuild.
2864                let asked = self.app.effects();
2865                self.run_command(asked);
2866                self.take_dev_commands();
2867                #[cfg(web)]
2868                self.sync_address();
2869                #[cfg(any(desktop, android))]
2870                self.report_dev();
2871                #[cfg(desktop)]
2872                self.open_links_from_other_instances();
2873                // Work a component started brought something: build again to show it.
2874                let arrived = self.take_task_arrivals();
2875                // A hot-reload patch changed what the components build.
2876                #[cfg(feature = "hot-reload")]
2877                let arrived = crate::hot::take_patched() || arrived;
2878                if frus_widgets::take_rebuild_request() || arrived {
2879                    self.build_dirty = true;
2880                }
2881                let need_build = frame_needs_build(
2882                    self.build_dirty,
2883                    app_animating,
2884                    std::mem::replace(&mut self.app_was_animating, app_animating),
2885                    self.tree.is_none() || self.runtime.switching(),
2886                );
2887                if need_build {
2888                    // No scope of its own: the surface above is already installed, and
2889                    // covers the layout and the paint that follow as well.
2890                    //
2891                    // `build_view`, not `view`: `collect_ids` below reads this tree before
2892                    // the layout pass has been down it, so an unprepared tree would report
2893                    // no identities at all inside a deferred subtree — and everything in an
2894                    // `AppBar` would silently never mount, never fade in and never fade out.
2895                    let build_start = timed.then(Instant::now);
2896                    let tree = build_view(&self.app, &theme, &self.runtime);
2897                    if let Some(at) = build_start {
2898                        let took = at.elapsed();
2899                        self.frame_costs.build = Some(took.as_secs_f32() * 1000.0);
2900                        crate::startup::built(took);
2901                    }
2902                    let ids = collect_ids(tree.as_ref());
2903                    let present: std::collections::HashSet<_> = ids.iter().copied().collect();
2904
2905                    // Leaving: snapshot the widgets present at N-1 but absent at N,
2906                    // so they can be faded out.
2907                    let leaving: std::collections::HashSet<u64> = self
2908                        .runtime
2909                        .mounted
2910                        .iter()
2911                        .filter(|id| !present.contains(id))
2912                        .map(|id| id.as_u64())
2913                        .collect();
2914                    if !leaving.is_empty() {
2915                        if let Some(ui) = &self.ui {
2916                            let captured: Vec<_> = ui
2917                                .scene()
2918                                .primitives()
2919                                .iter()
2920                                .filter(|p| leaving.contains(&p.owner()))
2921                                .cloned()
2922                                .collect();
2923                            if !captured.is_empty() {
2924                                self.runtime
2925                                    .leaving
2926                                    .insert(self.leaving_counter, (captured, 1.0));
2927                                self.leaving_counter = self.leaving_counter.wrapping_add(1);
2928                            }
2929                        }
2930                    }
2931
2932                    // Mounting: new widgets start out fading in.
2933                    for &id in &ids {
2934                        if self.runtime.mounted.insert(id) {
2935                            self.runtime.anims.entry(id).or_default().opacity = 0.0;
2936                        }
2937                    }
2938                    self.runtime.mounted.retain(|id| present.contains(id));
2939
2940                    self.tree = Some(tree);
2941                    // What the build asked for may be a timer — a component's
2942                    // `use_interval` — and it is the build that says so, which is later than
2943                    // the message that changed it. Diffing here starts and stops it now, not
2944                    // on the next message.
2945                    self.sync_subscriptions();
2946
2947                    // The configuration may have changed, so invalidate the paint
2948                    // cache and its repaint boundaries. Entries from a stale generation
2949                    // no longer *hit*, giving a full repaint this frame and reuse on the
2950                    // interaction-only frames that follow.
2951                    self.runtime.paint_cache.borrow_mut().bump_generation();
2952                }
2953                self.build_dirty = false;
2954
2955                // Focus requests (`Command::focus`), resolved against the tree just
2956                // (re)built — the key names the field wrapped in `keyed(k, …)`. The most
2957                // recent one that resolves wins, and the focus ring becomes visible
2958                // again, since we jump to the field as the keyboard would.
2959                if !self.pending_focus.is_empty() {
2960                    let keys = std::mem::take(&mut self.pending_focus);
2961                    let ids: Vec<WidgetId> = self
2962                        .tree
2963                        .as_deref()
2964                        .map(|tree| keys.iter().filter_map(|&k| find_by_key(tree, k)).collect())
2965                        .unwrap_or_default();
2966                    if let Some(&id) = ids.last() {
2967                        self.runtime.input.focused = Some(id);
2968                        self.runtime.focus_visible = true;
2969                    }
2970                }
2971
2972                // === PAINT phase ===
2973                // The retained tree is (re)painted. Layout goes through the relayout
2974                // cache (milestone 55: taffy is called again only when the structure
2975                // changed); painting goes through the repaint cache (milestone 88: a
2976                // A row carried past the end of the list: the list comes to meet it.
2977                // Before the tree is borrowed for the rest of the frame, because it moves
2978                // an offset the build below is about to read — and this frame, not the
2979                // next one, or the content would lag a frame behind the finger.
2980                let autoscrolling = self.autoscroll_carried(dt);
2981
2982                // The reorder spring, per axis, with a time constant of about 70 ms:
2983                // - **horizontal** (`Table` columns): the smoothed `reorder_x` catches up
2984                //   with the pointer — the columns slide with inertia while the ghost
2985                //   sticks to the real pointer;
2986                // - **vertical** (Kanban cards): the smoothed `reorder_y` catches up with
2987                //   the **chosen** slot edge, the hovered half — the insertion line and
2988                //   the gap *slide* between cards instead of jumping, which is the
2989                //   vertical counterpart of `reorder_x`;
2990                // - **a horizontal list's rows**: `reorder_x` catches up with the chosen
2991                //   slot edge, the same line turned on its side.
2992                let reorder_animating = self.advance_reorder_springs(dt);
2993
2994                // static `RepaintBoundary` subtree is replayed without repainting while
2995                // its geometry and the interaction state hold still).
2996                let tree = self
2997                    .tree
2998                    .as_deref()
2999                    .expect("the view was built at least once");
3000
3001                // Scroll and pan inertia; the bounds and viewports come from the
3002                // previous frame.
3003                let scroll_regions = self
3004                    .ui
3005                    .as_ref()
3006                    .map(|ui| ui.scroll_regions().to_vec())
3007                    .unwrap_or_default();
3008                // The refresh areas of the frame, with the `refreshing` flag each was
3009                // built with: that flag is what tells a spinning indicator when to stop.
3010                let refresh_areas = self
3011                    .ui
3012                    .as_ref()
3013                    .map(|ui| ui.refresh_areas().to_vec())
3014                    .unwrap_or_default();
3015                let dismissables = self
3016                    .ui
3017                    .as_ref()
3018                    .map(|ui| ui.dismissables().to_vec())
3019                    .unwrap_or_default();
3020                // Filled by the dismissal step below, dispatched once the tree is no
3021                // longer borrowed.
3022                let mut dismissed: Vec<A::Message> = Vec::new();
3023                let sheet_areas = self
3024                    .ui
3025                    .as_ref()
3026                    .map(|ui| ui.sheets().to_vec())
3027                    .unwrap_or_default();
3028                let interactive_bounds = self
3029                    .ui
3030                    .as_ref()
3031                    .map(|ui| ui.interactive_bounds())
3032                    .unwrap_or_default();
3033
3034                // A paged view that has been asked for another page glides across to
3035                // it. Done before the springs are stepped, so the request is honoured
3036                // in the frame it arrives rather than the one after.
3037                self.runtime.sync_pages(&scroll_regions);
3038                // And a bar whose selected item has moved out of its window slides back
3039                // to it. Same moment, same reason: the request is honoured in the frame
3040                // it arrives rather than the one after.
3041                self.runtime.sync_visible(&scroll_regions);
3042                // And the scroll requests an application has just returned. Same moment
3043                // and the same reason, with one addition: a region that has only just
3044                // appeared is not in the registry above, which was built last frame, so
3045                // what does not resolve here is tried again below against this frame's.
3046                let requests = std::mem::take(&mut self.pending_scroll);
3047                let (retry, scrolled) =
3048                    apply_scroll_requests(&mut self.runtime, tree, requests, &scroll_regions);
3049                self.retry_scroll = retry;
3050                // And the sheets it has asked to move, on the same terms: before the sheets
3051                // are stepped, so an animated request starts in the frame it arrives in.
3052                let requests = std::mem::take(&mut self.pending_sheet);
3053                let (retry, sheeted) =
3054                    apply_sheet_requests(&mut self.runtime, tree, requests, &sheet_areas);
3055                self.retry_sheet = retry;
3056
3057                // Every family is stepped, in this order, whatever an earlier one answered; each
3058                // answer is kept by name, so that frame statistics can say which kept the frames
3059                // coming (milestone 609).
3060                let moving: [(&str, bool); 25] = [
3061                    ("scrolled", scrolled),
3062                    ("sheeted", sheeted),
3063                    ("anims", self.runtime.advance(dt)),
3064                    ("leaving", self.runtime.advance_leaving(dt)),
3065                    ("switchers", self.runtime.advance_switchers(tree, dt)),
3066                    ("values", self.runtime.advance_values(tree, dt)),
3067                    ("colors", self.runtime.advance_colors(tree, dt)),
3068                    ("sizes", self.runtime.advance_sizes(tree, dt)),
3069                    ("radii", self.runtime.advance_radii(tree, dt)),
3070                    ("paddings", self.runtime.advance_paddings(tree, dt)),
3071                    ("offsets", self.runtime.advance_offsets(tree, dt)),
3072                    ("pins", self.runtime.advance_pins(tree, dt)),
3073                    ("fractions", self.runtime.advance_fractions(tree, dt)),
3074                    ("text_styles", self.runtime.advance_text_styles(tree, dt)),
3075                    ("transforms", self.runtime.advance_transforms(tree, dt)),
3076                    ("scroll", self.runtime.advance_scroll(&scroll_regions, dt)),
3077                    ("glow", self.runtime.advance_glow(dt)),
3078                    ("refresh", self.runtime.advance_refresh(&refresh_areas, dt)),
3079                    ("dismiss", {
3080                        // A dismissed item announces itself only once its gap has
3081                        // finished closing. The messages are *collected* here and
3082                        // dispatched below: the retained tree is borrowed for the whole
3083                        // of this frame, and `dispatch` rebuilds it.
3084                        let (moving, done) = self.runtime.advance_dismiss(&dismissables, dt);
3085                        dismissed.extend(done.into_iter().filter_map(|(id, direction)| {
3086                            find_widget(tree, id).and_then(|widget| widget.on_dismissed(direction))
3087                        }));
3088                        moving
3089                    }),
3090                    ("sheets", {
3091                        // A sheet lowered to nothing says so once it has arrived there,
3092                        // collected like a dismissal and for the same reason.
3093                        let (moving, closed) = self.runtime.advance_sheets(&sheet_areas, dt);
3094                        dismissed.extend(closed.into_iter().filter_map(|id| {
3095                            find_widget(tree, id).and_then(|widget| widget.on_sheet_dismissed())
3096                        }));
3097                        // A throw that carried a sheet to full height goes on into its
3098                        // list, under the list's own physics (milestone 520).
3099                        let mut handed = false;
3100                        for (list, velocity) in self.runtime.take_sheet_handovers() {
3101                            if let Some(area) = scroll_regions.iter().find(|a| a.id == list) {
3102                                handed |=
3103                                    self.runtime
3104                                        .fling_scroll(*area, area.physics, (0.0, velocity));
3105                            }
3106                        }
3107                        moving | handed
3108                    }),
3109                    (
3110                        "interactive",
3111                        self.runtime.advance_interactive(&interactive_bounds, dt),
3112                    ),
3113                    ("ink", self.runtime.advance_ink(dt)),
3114                    ("reorder", reorder_animating),
3115                    ("autoscroll", autoscrolling),
3116                    ("app", app_animating),
3117                ];
3118                let animating = moving.iter().any(|(_, on)| *on);
3119                self.frame_stats.note_moving(&moving);
3120                // With the inspector on, the same build collects the observed nodes,
3121                // and the overlay — outlines plus a card for the hovered widget — is
3122                // painted on top of a copy of the scene.
3123                let layout_start = timed.then(Instant::now);
3124                // So do debug paint (outlines alone, no card) and the dumps `frus run` asks
3125                // for (milestone 668).
3126                let inspected = self.inspector
3127                    || self.debug.outlines
3128                    || self.inspector_dump
3129                    || self.debug.focus_dump;
3130                let (ui, scene) = if inspected {
3131                    let (ui, nodes) = frus_widgets::build_ui_inspected(
3132                        tree,
3133                        Size::new(width, height),
3134                        &self.runtime,
3135                        &theme,
3136                    );
3137                    if std::mem::take(&mut self.inspector_dump) {
3138                        // Through `log`, so it reaches logcat and the browser too.
3139                        log::info!(target: "frus::inspector", "
3140{}", frus_widgets::dump_tree(&nodes));
3141                    }
3142                    if std::mem::take(&mut self.debug.focus_dump) {
3143                        let focusable: Vec<WidgetId> = ui.focusable_ids().collect();
3144                        log::info!(target: "frus::inspector", "focus, in tab order:
3145{}", dump_focus(&nodes, &focusable, self.runtime.input.focused));
3146                    }
3147                    let mut scene = if self.inspector {
3148                        // The card goes over the application's text, which a scene draws after
3149                        // its shapes: the application's frame as one layer, under it.
3150                        on_top_of(ui.scene())
3151                    } else {
3152                        ui.scene().clone()
3153                    };
3154                    if self.inspector || self.debug.outlines {
3155                        frus_widgets::paint_inspector_overlay(
3156                            &nodes,
3157                            self.inspector.then_some(self.cursor),
3158                            Size::new(width, height),
3159                            &theme,
3160                            &mut scene,
3161                        );
3162                    }
3163                    // Scene: logical → physical (DPI × density).
3164                    (ui, scene.scaled(scale))
3165                } else {
3166                    let ui = build_ui(tree, Size::new(width, height), &self.runtime, &theme);
3167                    // The preview of a column reorder, or a lifted item, on top of the
3168                    // scene.
3169                    let scene = if matches!(self.drag, Some(Drag::Reorder { moved: true, .. })) {
3170                        let mut scene = ui.scene().clone();
3171                        self.paint_reorder_preview(&ui, &theme, &mut scene);
3172                        scene.scaled(scale)
3173                    } else if matches!(self.drag, Some(Drag::Item { moved: true, .. })) {
3174                        let mut scene = ui.scene().clone();
3175                        self.paint_drag_ghost(&ui, &theme, &mut scene);
3176                        scene.scaled(scale)
3177                    } else {
3178                        // Scene: logical → physical (DPI × density), for a crisp render.
3179                        ui.scene().scaled(scale)
3180                    };
3181                    (ui, scene)
3182                };
3183                let layout_took = layout_start.map(|at| at.elapsed());
3184                if let Some(took) = layout_took {
3185                    self.frame_costs.layout_paint = took.as_secs_f32() * 1000.0;
3186                    self.frame_costs.layers = count_layers(scene.primitives());
3187                }
3188                self.report_overflows(&ui);
3189                // Laid out: the components the build no longer reached are let go, and the
3190                // effects it asked for run. An effect that changed something asks for the
3191                // frame that shows it.
3192                self.runtime.states.end_frame();
3193                crate::startup::laid_out(layout_took);
3194                #[cfg(web)]
3195                crate::web_dev::keep(|| self.runtime.states.snapshot());
3196                if frus_widgets::rebuild_requested() {
3197                    self.request_redraw();
3198                }
3199                // A see-through frame starts transparent: the window's background under
3200                // everything but the line the system paints (milestone 643).
3201                let scene = if self.renderer.as_ref().is_some_and(Renderer::sees_through) {
3202                    backdrop_under(scene, width * scale, height * scale, self.system_line())
3203                } else {
3204                    scene
3205                };
3206                // What the keys of `frus run` asked of this frame (milestone 668).
3207                if std::mem::take(&mut self.debug.semantics_dump) {
3208                    log::info!(target: "frus::inspector", "semantics, in reading order:
3209{}", dump_semantics(ui.semantics()));
3210                }
3211                let scene = if self.debug.performance {
3212                    // The application's frame as one layer, the panel over it: in a scene of
3213                    // its own, the application's text would be drawn over the panel's
3214                    // rectangle — the renderer draws a scene's shapes, then its text.
3215                    let mut scene = on_top_of(&scene);
3216                    let costs = self.debug.last_costs.as_ref().map(|(costs, _)| costs);
3217                    // Below the status bar, where the system draws its own.
3218                    let top = self.last_insets.padding.top * scale;
3219                    paint_performance(
3220                        &mut scene,
3221                        costs,
3222                        &self.debug.history,
3223                        self.debug.fps,
3224                        width * scale,
3225                        top,
3226                        scale,
3227                    );
3228                    scene
3229                } else {
3230                    scene
3231                };
3232                // A simulated device: the application's frame inside the device's screen,
3233                // the device around it (milestone 721).
3234                let (scene, shot) = match (&self.simulated, self.placement()) {
3235                    (Some(simulated), Some(placement)) => {
3236                        let edges = self.last_insets.padding;
3237                        // The status bar's icons read against what the application has
3238                        // under them, as a device's system decides it.
3239                        let icons = ui
3240                            .system_ui_style(
3241                                Point::new(width / 2.0, edges.top + 0.5),
3242                                Point::new(width / 2.0, height - edges.bottom - 0.5),
3243                            )
3244                            .resolve(&theme)
3245                            .status_bar_icons;
3246                        let window = Size::new(
3247                            size.width as f32 / self.scale.max(0.1),
3248                            size.height as f32 / self.scale.max(0.1),
3249                        );
3250                        let framed = simulated.paint(
3251                            &scene,
3252                            placement,
3253                            self.scale,
3254                            window,
3255                            icons,
3256                            self.brightness(),
3257                        );
3258                        (framed, (size.width as f32, size.height as f32))
3259                    }
3260                    _ => (scene, (width * scale, height * scale)),
3261                };
3262                #[cfg(desktop)]
3263                if let Some(path) = self.debug.screenshot.take() {
3264                    write_screenshot(&scene, shot.0, shot.1, &path);
3265                }
3266                #[cfg(not(desktop))]
3267                let _ = shot;
3268                let mut device_lost = false;
3269                if let Some(renderer) = self.renderer.as_mut() {
3270                    let render_start = timed.then(Instant::now);
3271                    match renderer.render(&scene) {
3272                        frus_gpu::RenderOutcome::Presented => {
3273                            crate::startup::presented(render_start.map(|at| at.elapsed()))
3274                        }
3275                        frus_gpu::RenderOutcome::NeedsReconfigure => {
3276                            renderer.reconfigure();
3277                        }
3278                        frus_gpu::RenderOutcome::DeviceLost => device_lost = true,
3279                        frus_gpu::RenderOutcome::Skipped => {}
3280                    }
3281                    // The page's picture, read in the task that drew it: a WebGPU canvas keeps
3282                    // what was drawn only until the task ends.
3283                    #[cfg(web)]
3284                    if std::mem::take(&mut self.debug.page_screenshot) {
3285                        if let Some(window) = self.window.as_ref() {
3286                            crate::web_dev::send_screenshot(window);
3287                        }
3288                    }
3289                    if let Some(at) = render_start {
3290                        self.frame_costs.render = at.elapsed().as_secs_f32() * 1000.0;
3291                        let timings = renderer.last_timings();
3292                        self.frame_costs.acquire = timings.acquire;
3293                        self.frame_costs.draw = timings.draw;
3294                        self.frame_costs.gpu = timings.gpu;
3295                    }
3296                }
3297                // The GPU device is gone: a new renderer on the same window, at the next frame's
3298                // start, which draws the interface again (milestone 686, #217).
3299                if device_lost {
3300                    self.renderer_lost = true;
3301                    if let Some(window) = self.window.as_ref() {
3302                        window.request_redraw();
3303                    }
3304                }
3305                // A continuously animating widget, a spinner say, forces a redraw. So
3306                // does an image still on its way: without a frame to draw it in, the
3307                // frame that would show it never happens.
3308                //
3309                // The fetch is asked about **here** rather than inside the tree, and as a
3310                // count rather than a flag on the widget: showing a placeholder means
3311                // taking the image out of the tree, so a hook read off `Image` would go
3312                // quiet at exactly the moment it is needed. Asking here also keeps `Ui`
3313                // answering for its own widgets alone (milestone 411).
3314                let widget_asks = ui.wants_animation();
3315                let loading =
3316                    frus_widgets::images_in_flight() > 0 || frus_widgets::tasks_in_flight() > 0;
3317                let wants_animation = widget_asks || loading;
3318
3319                // Keep the interface, for hit testing. The tree is already retained.
3320                self.ui = Some(ui);
3321                // The keyboard took room from the bottom of the window: what the reader is
3322                // typing in is brought back above it, by the least scroll that does it, and
3323                // by the same glide Tab uses. This frame's layout is the shortened one, so
3324                // the viewports asked are the ones left above the keyboard. A frame with
3325                // nothing focused, or no region around what is, moves nothing.
3326                let revealed = std::mem::take(&mut self.reveal_after_keyboard) && {
3327                    let moves = match (self.runtime.input.focused, self.ui.as_ref()) {
3328                        (Some(id), Some(ui)) => ui.reveal(id, &self.runtime),
3329                        _ => Vec::new(),
3330                    };
3331                    let moved = !moves.is_empty();
3332                    for (area, offset) in moves {
3333                        self.runtime.scroll_ballistic.remove(&area);
3334                        self.runtime.scroll_velocity.remove(&area);
3335                        self.runtime.scroll_target.insert(area, offset);
3336                    }
3337                    moved
3338                };
3339                // The tree may have changed under a still pointer — a tap that opened a
3340                // screen — so what it is over is asked of this frame, as the reference
3341                // does after every frame. Not during a drag: a slider dragged past its
3342                // end is still the one being pressed, and the move path leaves it so.
3343                //
3344                // Field by field rather than through `sync_hover`, which borrows the whole
3345                // shell: the frame still holds part of it here. A change is shown the way
3346                // the reference shows it, on the next frame — the tree is built again,
3347                // since a tooltip's bubble is decided while the tree is walked.
3348                let rehovered = self.drag.is_none() && {
3349                    let hovered = self.ui.as_ref().and_then(|ui| self.hover.target(ui));
3350                    let changed = hovered != self.runtime.input.hovered;
3351                    self.runtime.input.hovered = hovered;
3352                    changed
3353                };
3354                if rehovered {
3355                    self.build_dirty = true;
3356                }
3357                let wants_animation = wants_animation || rehovered || revealed;
3358
3359                // The paged views that have just turned a page, read off **this**
3360                // frame's regions: a page change is worth reporting the moment it
3361                // reads as one, not a frame later.
3362                let paged = self
3363                    .ui
3364                    .as_ref()
3365                    .map(|ui| ui.scroll_regions().to_vec())
3366                    .unwrap_or_default();
3367                // A region whose content shrank this frame under an offset nobody owns was
3368                // drawn where it now rests; the runtime keeps it there before anything reads
3369                // the offset back — the reports below, a press, the next frame's springs.
3370                // Without it the row was drawn right once and pressed wrong (milestone 533).
3371                self.runtime.keep_scroll_in_range(&paged);
3372                let turned: Vec<A::Message> = self
3373                    .runtime
3374                    .page_changes(&paged)
3375                    .into_iter()
3376                    .filter_map(|(id, page)| {
3377                        find_widget(tree, id).and_then(|widget| widget.on_page_changed(page))
3378                    })
3379                    .collect();
3380
3381                // The scroll requests the previous frame's registry could not place:
3382                // tried once against **this** frame's, which is where a region that has
3383                // only just appeared turns up. This is the last chance they get.
3384                let (_, moved_late) = apply_scroll_requests(
3385                    &mut self.runtime,
3386                    tree,
3387                    std::mem::take(&mut self.retry_scroll),
3388                    &paged,
3389                );
3390                // The same for the sheet requests: a sheet shown by the very message that
3391                // asked it to move is in this frame's sheets and not the last one's.
3392                let (_, sheeted_late) = {
3393                    let sheets = self
3394                        .ui
3395                        .as_ref()
3396                        .map(|ui| ui.sheets().to_vec())
3397                        .unwrap_or_default();
3398                    apply_sheet_requests(
3399                        &mut self.runtime,
3400                        tree,
3401                        std::mem::take(&mut self.retry_sheet),
3402                        &sheets,
3403                    )
3404                };
3405
3406                // The regions that have moved since they last said so, read off **this**
3407                // frame's registry so that the offset and the extents reported together
3408                // come from the same frame. A region nobody is listening to costs the
3409                // comparison and nothing else.
3410                let scrolled: Vec<A::Message> = {
3411                    let grain =
3412                        |id| find_widget(tree, id).map_or(0.0, |widget| widget.scroll_grain());
3413                    self.runtime
3414                        .scroll_changes(&paged, grain)
3415                        .into_iter()
3416                        .filter_map(|(id, position)| {
3417                            find_widget(tree, id).and_then(|widget| widget.on_scroll(position))
3418                        })
3419                        .collect()
3420                };
3421
3422                // The rows whose gap has just finished closing, the pages that have
3423                // turned and the regions that have moved: the tree is no longer
3424                // borrowed, so the application can be told — and rebuild.
3425                for message in dismissed.into_iter().chain(turned).chain(scrolled) {
3426                    self.dispatch(message);
3427                }
3428                // A screen that changed under a still pointer enters and leaves regions too,
3429                // as the reference's mouse tracker does after every frame (milestone 583).
3430                self.sync_regions(false);
3431                if moved_late || sheeted_late {
3432                    // Nothing else this frame knows the offset changed: the springs ran
3433                    // before the request was placed.
3434                    self.request_redraw();
3435                }
3436
3437                // Focus return: when the focused widget has **vanished**, an overlay
3438                // having closed, go back to the trigger. Done before the AccessKit
3439                // announcement, so the focus it publishes is current.
3440                self.reconcile_focus();
3441
3442                // Publish the frame's semantic tree to AccessKit. The layout's pixels to the
3443                // window's: the total scale, the app's density included; a simulated device's
3444                // screen also sits at its own place in the window (milestone 726).
3445                #[cfg(desktop)]
3446                let (scale, origin) = (
3447                    f64::from(self.total_scale()),
3448                    self.placement().map_or((0.0, 0.0), |at| {
3449                        let os = f64::from(self.scale);
3450                        (f64::from(at.screen.x) * os, f64::from(at.screen.y) * os)
3451                    }),
3452                );
3453                #[cfg(desktop)]
3454                if let Some(a11y) = self.a11y.as_mut() {
3455                    let focus = self.runtime.input.focused;
3456                    let title = self.app.title();
3457                    if let Some(ui) = self.ui.as_ref() {
3458                        a11y.update(ui.semantics(), focus, &title, &self.announce, scale, origin);
3459                    }
3460                }
3461                // The field being edited gets its element (milestone 711) — before the
3462                // bridge, which hides that field's own node while it is (milestone 713).
3463                #[cfg(web)]
3464                self.sync_web_editing();
3465                // Publish the frame's semantic tree to the DOM bridge (#18). The page's
3466                // coordinates are CSS pixels, the layout's own: no scale to carry.
3467                #[cfg(web)]
3468                if let Some(a11y) = self.a11y.as_mut() {
3469                    let focus = self.runtime.input.focused;
3470                    let editing = self.web_editor.as_ref().and_then(|editor| editor.editing());
3471                    let title = self.app.title();
3472                    if let Some(ui) = self.ui.as_ref() {
3473                        a11y.update(ui.semantics(), focus, editing, &title, &self.announce);
3474                    }
3475                }
3476
3477                // The system bars follow the frame: the regions under them, then the theme.
3478                self.sync_system_bars(&theme);
3479
3480                // The Android software keyboard follows the text fields' focus.
3481                self.sync_soft_input();
3482                // And the autofill service follows the form being edited.
3483                #[cfg(android)]
3484                self.sync_autofill();
3485
3486                // While an animation is running, ask for another frame.
3487                if animating || wants_animation {
3488                    self.request_redraw();
3489                }
3490                if let Some(start) = frame_start {
3491                    use crate::frame_stats::Why;
3492                    let why = [
3493                        (app_animating, Why::APP),
3494                        (animating, Why::RUNTIME),
3495                        (widget_asks, Why::WIDGET),
3496                        (loading, Why::LOADING),
3497                        (rehovered || revealed, Why::POINTER),
3498                    ];
3499                    self.frame_costs.why = why
3500                        .into_iter()
3501                        .filter(|(on, _)| *on)
3502                        .fold(Why::NONE, |all, (_, why)| all | why);
3503                    self.frame_stats.frame(start, self.frame_costs);
3504                    if self.debug.performance {
3505                        if let Some((_, before)) = self.debug.last_costs {
3506                            let gap = start.saturating_duration_since(before).as_secs_f32();
3507                            if gap > 0.0 {
3508                                self.debug.fps = 1.0 / gap;
3509                            }
3510                        }
3511                        // The first frame measured: one more shows what it cost.
3512                        if self.debug.last_costs.is_none() {
3513                            self.request_redraw();
3514                        }
3515                        self.debug.last_costs = Some((self.frame_costs, start));
3516                        self.debug.history.push(&self.frame_costs);
3517                    }
3518                }
3519                // The title bar's line, shared with the content when it asks (milestone 640).
3520                #[cfg(windows)]
3521                self.sync_title_bar();
3522            }
3523
3524            _ => {}
3525        }
3526    }
3527}
3528
3529/// What the keys of `frus run` turned on or asked for (milestone 668).
3530#[derive(Default)]
3531struct DebugAsks {
3532    /// Outlines around every widget, without the inspector's card: debug paint (`p`).
3533    outlines: bool,
3534    /// The focusable widgets to print on the next frame, the focused one marked (`f`).
3535    focus_dump: bool,
3536    /// The accessibility tree to print on the next frame (`S`).
3537    semantics_dump: bool,
3538    /// The brightness the theme is resolved with instead of the platform's (`b`).
3539    brightness: Option<frus_widgets::Brightness>,
3540    /// The text size the layout is told instead of the platform's (`T`, milestone 722).
3541    text_scale: Option<f32>,
3542    /// The performance panel: the last frame's costs, over the interface (`P`).
3543    performance: bool,
3544    /// The last frame's costs and when it began, for the panel.
3545    last_costs: Option<(crate::frame_stats::FrameCosts, Instant)>,
3546    /// The latest frames' costs, oldest first, for the panel's graphs (milestone 676).
3547    history: FrameHistory,
3548    /// Frames per second, from the gap between the last two.
3549    fps: f32,
3550    /// Where to write the next frame as a PNG (`s`).
3551    #[cfg_attr(not(desktop), allow(dead_code))]
3552    screenshot: Option<std::path::PathBuf>,
3553    /// The page's next frame to send to `frus run` as a PNG (`s` on the web, milestone 675).
3554    #[cfg_attr(not(web), allow(dead_code))]
3555    page_screenshot: bool,
3556}
3557
3558/// The order `o` goes through the platforms in.
3559const PLATFORM_CYCLE: [frus_widgets::TargetPlatform; 6] = [
3560    frus_widgets::TargetPlatform::Android,
3561    frus_widgets::TargetPlatform::Ios,
3562    frus_widgets::TargetPlatform::Windows,
3563    frus_widgets::TargetPlatform::MacOs,
3564    frus_widgets::TargetPlatform::Linux,
3565    frus_widgets::TargetPlatform::Fuchsia,
3566];
3567
3568/// The text sizes `T` steps through: the usual one, two larger ones a reader with poor sight
3569/// picks, and one smaller.
3570const TEXT_SCALE_CYCLE: [f32; 4] = [1.0, 1.3, 2.0, 0.85];
3571
3572/// The text size after `current` in [`TEXT_SCALE_CYCLE`]; the usual one after any other.
3573fn next_text_scale(current: f32) -> f32 {
3574    TEXT_SCALE_CYCLE
3575        .iter()
3576        .position(|s| (s - current).abs() < 1e-3)
3577        .map_or(1.0, |i| TEXT_SCALE_CYCLE[(i + 1) % TEXT_SCALE_CYCLE.len()])
3578}
3579
3580/// The platform after `current` in [`PLATFORM_CYCLE`].
3581fn next_platform(current: frus_widgets::TargetPlatform) -> frus_widgets::TargetPlatform {
3582    let at = PLATFORM_CYCLE
3583        .iter()
3584        .position(|p| *p == current)
3585        .unwrap_or(0);
3586    PLATFORM_CYCLE[(at + 1) % PLATFORM_CYCLE.len()]
3587}
3588
3589/// The focusable widgets of a frame, in tab order, the focused one marked.
3590fn dump_focus(
3591    nodes: &[frus_widgets::InspectorNode],
3592    focusable: &[WidgetId],
3593    focused: Option<WidgetId>,
3594) -> String {
3595    let mut out = String::new();
3596    for id in focusable {
3597        let node = nodes.iter().rev().find(|n| n.id == *id);
3598        let mark = if Some(*id) == focused { "▶ " } else { "  " };
3599        match node {
3600            Some(n) => out.push_str(&format!(
3601                "{mark}{}  {:.0},{:.0}  {:.0}×{:.0}  #{:08x}\n",
3602                n.name,
3603                n.rect.x,
3604                n.rect.y,
3605                n.rect.width,
3606                n.rect.height,
3607                id.as_u64() as u32
3608            )),
3609            None => out.push_str(&format!("{mark}#{:08x}\n", id.as_u64() as u32)),
3610        }
3611    }
3612    if out.is_empty() {
3613        out.push_str("  (nothing focusable)\n");
3614    }
3615    out
3616}
3617
3618/// The accessibility tree of a frame, one node a line, in reading order.
3619fn dump_semantics(nodes: &[(WidgetId, Rect, frus_widgets::SemanticsProperties)]) -> String {
3620    let mut out = String::new();
3621    for (_, rect, props) in nodes {
3622        out.push_str(&format!("  {:?}", props.role));
3623        if let Some(label) = &props.label {
3624            out.push_str(&format!(" \"{label}\""));
3625        }
3626        if let Some(value) = &props.value {
3627            out.push_str(&format!(" = {value}"));
3628        }
3629        if props.clickable {
3630            out.push_str(" clickable");
3631        }
3632        if props.disabled {
3633            out.push_str(" disabled");
3634        }
3635        out.push_str(&format!(
3636            "  {:.0},{:.0}  {:.0}×{:.0}\n",
3637            rect.x, rect.y, rect.width, rect.height
3638        ));
3639    }
3640    if out.is_empty() {
3641        out.push_str("  (empty)\n");
3642    }
3643    out
3644}
3645
3646/// The performance panel, in the top right corner of a `width`×`height` frame drawn at
3647/// `scale`: the last frame's costs and the rate of frames.
3648#[allow(clippy::too_many_arguments)]
3649fn paint_performance(
3650    scene: &mut frus_widgets::Scene,
3651    costs: Option<&crate::frame_stats::FrameCosts>,
3652    history: &FrameHistory,
3653    fps: f32,
3654    width: f32,
3655    top: f32,
3656    scale: f32,
3657) {
3658    let lines = match costs {
3659        Some(costs) => performance_lines(costs, fps),
3660        None => vec!["measuring…".to_string()],
3661    };
3662    paint_panel(scene, lines, history, width, top, scale);
3663}
3664
3665/// How many frames the graphs show.
3666const HISTORY: usize = 90;
3667/// A frame's budget at 60 Hz, in milliseconds: the line the bars are measured against.
3668const BUDGET_MS: f32 = 1000.0 / 60.0;
3669/// The height of a graph, in milliseconds: two budgets.
3670const GRAPH_MS: f32 = BUDGET_MS * 2.0;
3671
3672/// The latest frames' costs for the performance panel: what the interface took (build, layout,
3673/// paint) and what drawing took (encoding, submitting, waiting for the GPU and the display).
3674#[derive(Default)]
3675struct FrameHistory {
3676    ui: std::collections::VecDeque<f32>,
3677    render: std::collections::VecDeque<f32>,
3678}
3679
3680impl FrameHistory {
3681    fn push(&mut self, costs: &crate::frame_stats::FrameCosts) {
3682        let ui = costs.build.unwrap_or(0.0) + costs.layout_paint;
3683        for (list, value) in [(&mut self.ui, ui), (&mut self.render, costs.render)] {
3684            if list.len() == HISTORY {
3685                list.pop_front();
3686            }
3687            list.push_back(value);
3688        }
3689    }
3690
3691    /// The bars of one graph, `height` high: for each frame its height, and whether it was
3692    /// over budget. A frame over the graph's top fills it.
3693    fn bars(values: &std::collections::VecDeque<f32>, height: f32) -> Vec<(f32, bool)> {
3694        values
3695            .iter()
3696            .map(|ms| ((ms / GRAPH_MS).min(1.0) * height, *ms > BUDGET_MS))
3697            .collect()
3698    }
3699}
3700
3701/// A scene holding `scene` as one layer, for something to be painted over all of it: within a
3702/// scene, the renderer draws the shapes first and the text after, so a panel's rectangle in the
3703/// same scene would sit under the application's text.
3704fn on_top_of(scene: &Scene) -> Scene {
3705    let mut over = Scene::new();
3706    over.layer(1.0, |inner| {
3707        for primitive in scene.primitives() {
3708            inner.push_primitive(primitive.clone());
3709        }
3710    });
3711    over
3712}
3713
3714/// The panel's lines for a frame that cost `costs`.
3715fn performance_lines(costs: &crate::frame_stats::FrameCosts, fps: f32) -> Vec<String> {
3716    let build = costs
3717        .build
3718        .map_or_else(|| "kept".to_string(), |ms| format!("{ms:.1} ms"));
3719    let mut lines = vec![
3720        // One frame alone has no rate: the interface is idle.
3721        if fps > 0.0 {
3722            format!("{fps:.0} fps")
3723        } else {
3724            "idle".to_string()
3725        },
3726        format!("build {build}  layout+paint {:.1} ms", costs.layout_paint),
3727        format!(
3728            "render {:.1} ms (acquire {:.1}, draw {:.1})",
3729            costs.render, costs.acquire, costs.draw
3730        ),
3731    ];
3732    if let Some(gpu) = costs.gpu {
3733        lines.push(format!("GPU {gpu:.1} ms"));
3734    }
3735    lines
3736}
3737
3738/// `lines` in a dark panel in the top right corner of a frame `width` physical pixels wide,
3739/// `top` pixels down.
3740fn paint_panel(
3741    scene: &mut frus_widgets::Scene,
3742    lines: Vec<String>,
3743    history: &FrameHistory,
3744    width: f32,
3745    top: f32,
3746    scale: f32,
3747) {
3748    let size = 12.0 * scale;
3749    let style = frus_widgets::TextStyle::new(size).resolved();
3750    let pad = 6.0 * scale;
3751    let line_h = size * 1.4;
3752    // Estimated rather than measured: the panel is a tool's, and a little room to spare is
3753    // all an estimate costs.
3754    let longest = lines
3755        .iter()
3756        .map(|line| line.chars().count())
3757        .max()
3758        .unwrap_or(0);
3759    // Two graphs under the text, one bar a frame.
3760    let bar_w = 2.0 * scale;
3761    let graph_w = HISTORY as f32 * bar_w;
3762    let graph_h = 36.0 * scale;
3763    let graphs_h = if history.ui.is_empty() {
3764        0.0
3765    } else {
3766        2.0 * (graph_h + line_h) + pad
3767    };
3768    let text_w = (longest as f32 * size * 0.6).max(graph_w);
3769    let panel = Rect::new(
3770        (width - text_w - pad * 3.0).max(0.0),
3771        top + pad,
3772        text_w + pad * 2.0,
3773        line_h * lines.len() as f32 + pad * 2.0 + graphs_h,
3774    );
3775    scene.set_clip(Rect::UNBOUNDED);
3776    scene.set_owner(0);
3777    // Opaque: a translucent one blends in linear light and paints as a grey.
3778    scene.draw_rect(
3779        panel,
3780        frus_widgets::Color::rgb(0.05, 0.05, 0.05),
3781        4.0 * scale,
3782        0.0,
3783        frus_widgets::Color::TRANSPARENT,
3784    );
3785    let count = lines.len();
3786    for (i, line) in lines.into_iter().enumerate() {
3787        scene.text(
3788            frus_widgets::Point::new(panel.x + pad, panel.y + pad + line_h * i as f32),
3789            line,
3790            &style,
3791            frus_widgets::Color::rgb(0.55, 1.0, 0.55),
3792        );
3793    }
3794    if history.ui.is_empty() {
3795        return;
3796    }
3797    let mut y = panel.y + pad + line_h * count as f32 + pad;
3798    for (title, values) in [
3799        ("interface (build, layout, paint)", &history.ui),
3800        ("drawing (encode, submit, present)", &history.render),
3801    ] {
3802        scene.text(
3803            frus_widgets::Point::new(panel.x + pad, y),
3804            title.to_string(),
3805            &style,
3806            frus_widgets::Color::rgb(0.75, 0.75, 0.75),
3807        );
3808        y += line_h;
3809        let area = Rect::new(panel.x + pad, y, graph_w, graph_h);
3810        scene.draw_rect(
3811            area,
3812            frus_widgets::Color::rgb(0.12, 0.12, 0.12),
3813            0.0,
3814            0.0,
3815            frus_widgets::Color::TRANSPARENT,
3816        );
3817        // The bars, the newest on the right.
3818        let bars = FrameHistory::bars(values, graph_h);
3819        let start = area.x + area.width - bars.len() as f32 * bar_w;
3820        for (i, (h, over)) in bars.into_iter().enumerate() {
3821            if h <= 0.0 {
3822                continue;
3823            }
3824            let color = if over {
3825                frus_widgets::Color::rgb(1.0, 0.35, 0.3)
3826            } else {
3827                frus_widgets::Color::rgb(0.4, 0.85, 0.45)
3828            };
3829            scene.fill_rect(
3830                Rect::new(
3831                    start + i as f32 * bar_w,
3832                    area.y + area.height - h,
3833                    bar_w * 0.8,
3834                    h,
3835                ),
3836                color,
3837            );
3838        }
3839        // The budget: a frame drawn at 60 Hz stays under this line.
3840        let budget_y = area.y + area.height * (1.0 - BUDGET_MS / GRAPH_MS);
3841        scene.fill_rect(
3842            Rect::new(area.x, budget_y, area.width, scale.max(1.0)),
3843            frus_widgets::Color::rgb(0.95, 0.95, 0.95),
3844        );
3845        y += graph_h + pad / 2.0;
3846    }
3847}
3848
3849/// Writes `scene`, a frame `width`×`height` physical pixels, to `path` as a PNG, drawn again
3850/// offscreen: the window's own image cannot be read back. Says where in the log, or why not.
3851#[cfg(desktop)]
3852fn write_screenshot(scene: &Scene, width: f32, height: f32, path: &std::path::Path) {
3853    let (w, h) = (
3854        width.round().max(1.0) as u32,
3855        height.round().max(1.0) as u32,
3856    );
3857    let Some(frame) = frus_gpu::render_offscreen(scene, w, h, frus_gpu::backdrop()) else {
3858        log::warn!(target: "frus::dev", "screenshot: no GPU to draw it with");
3859        return;
3860    };
3861    let png = crate::dev_commands::png(frame.width, frame.height, &frame.rgba);
3862    match std::fs::write(path, png) {
3863        Ok(()) => log::debug!(
3864            target: "frus::dev",
3865            "screenshot {}×{} written to {}",
3866            frame.width,
3867            frame.height,
3868            path.display()
3869        ),
3870        Err(error) => log::warn!(target: "frus::dev", "screenshot: {}: {error}", path.display()),
3871    }
3872}
3873
3874impl<A: Application> App<A> {
3875    /// Carries out what the keys of `frus run` asked since the last frame (milestone 668).
3876    /// Each says what it did in the log, which `frus run` shows.
3877    /// A new renderer on the window, after its GPU device was lost — a driver reset, a GPU
3878    /// removed or restarted —, of the same kind as the old one: the application goes on, its
3879    /// interface drawn again on the next frame (milestone 686, #217).
3880    fn rebuild_renderer(&mut self) {
3881        let see_through = self.renderer.as_ref().is_some_and(Renderer::sees_through);
3882        // The old renderer — its surface on this window among it — goes first.
3883        self.renderer = None;
3884        let Some(window) = self.window.clone() else {
3885            return;
3886        };
3887        let size = window.inner_size();
3888        let (width, height) = (size.width.max(1), size.height.max(1));
3889        log::warn!("the GPU device was lost: drawing again with a new one");
3890        #[cfg(not(web))]
3891        {
3892            let made = if see_through {
3893                pollster::block_on(Renderer::see_through(window.clone(), width, height))
3894            } else {
3895                pollster::block_on(Renderer::new(window.clone(), width, height))
3896            };
3897            match made {
3898                Ok(renderer) => self.renderer = Some(renderer),
3899                Err(err) => {
3900                    log::error!("the GPU device was lost, and no new one could be made: {err:#}")
3901                }
3902            }
3903        }
3904        #[cfg(web)]
3905        {
3906            let _ = see_through;
3907            let slot = self.pending_renderer.clone();
3908            let win = window.clone();
3909            wasm_bindgen_futures::spawn_local(async move {
3910                match Renderer::new(win.clone(), width, height).await {
3911                    Ok(renderer) => {
3912                        *slot.borrow_mut() = Some(renderer);
3913                        win.request_redraw();
3914                    }
3915                    Err(err) => {
3916                        log::error!(
3917                            "the GPU device was lost, and no new one could be made: {err:#}"
3918                        )
3919                    }
3920                }
3921            });
3922        }
3923        self.build_dirty = true;
3924        window.request_redraw();
3925    }
3926
3927    fn take_dev_commands(&mut self) {
3928        for command in crate::dev_commands::take_commands() {
3929            let (name, argument) = command
3930                .split_once(' ')
3931                .map_or((command.as_str(), ""), |(n, a)| (n, a.trim()));
3932            let on_off = |on: bool| if on { "on" } else { "off" };
3933            match name {
3934                "inspector" => {
3935                    self.inspector = !self.inspector;
3936                    self.inspector_dump = self.inspector;
3937                    log::info!(target: "frus::dev", "inspector {}", on_off(self.inspector));
3938                }
3939                "outlines" => {
3940                    self.debug.outlines = !self.debug.outlines;
3941                    log::info!(target: "frus::dev", "debug paint {}", on_off(self.debug.outlines));
3942                }
3943                "widgets" => self.inspector_dump = true,
3944                "focus" => self.debug.focus_dump = true,
3945                "semantics" => self.debug.semantics_dump = true,
3946                "platform" => {
3947                    let next = next_platform(frus_widgets::default_target_platform());
3948                    if cfg!(debug_assertions) {
3949                        frus_widgets::set_debug_default_target_platform_override(Some(next));
3950                        log::info!(target: "frus::dev", "target platform: {next:?}");
3951                    } else {
3952                        log::warn!(target: "frus::dev", "the platform can be switched in a debug build only");
3953                    }
3954                }
3955                "brightness" => {
3956                    let now = self.debug.brightness.unwrap_or(self.platform.brightness);
3957                    let next = match now {
3958                        frus_widgets::Brightness::Light => frus_widgets::Brightness::Dark,
3959                        frus_widgets::Brightness::Dark => frus_widgets::Brightness::Light,
3960                    };
3961                    self.debug.brightness = Some(next);
3962                    log::info!(
3963                        target: "frus::dev",
3964                        "brightness: {next:?} (an application without a theme for it stays as it is)"
3965                    );
3966                }
3967                "performance" => {
3968                    self.debug.performance = !self.debug.performance;
3969                    self.debug.last_costs = None;
3970                    self.debug.history = FrameHistory::default();
3971                    self.frame_stats.set_enabled(self.debug.performance);
3972                    // This frame began unmeasured: the next one is, and shows its figures.
3973                    self.request_redraw();
3974                    log::info!(target: "frus::dev", "performance panel {}", on_off(self.debug.performance));
3975                }
3976                // The reader's text size, the steps a phone's settings offer (milestone 722).
3977                "text-scale" => {
3978                    let now = self.debug.text_scale.unwrap_or(self.platform.text_scaler);
3979                    let next = next_text_scale(now);
3980                    self.debug.text_scale = Some(next);
3981                    log::info!(target: "frus::dev", "text size: {:.0}%", next * 100.0);
3982                }
3983                // A simulated device's keyboard held up, to see a screen under it without a
3984                // field to focus (milestone 722).
3985                "keyboard" => match self.simulated.as_mut() {
3986                    Some(simulated) => {
3987                        simulated.keyboard_held = !simulated.keyboard_held;
3988                        log::info!(
3989                            target: "frus::dev",
3990                            "keyboard held {}",
3991                            on_off(simulated.keyboard_held)
3992                        );
3993                        self.request_redraw();
3994                    }
3995                    None => log::info!(
3996                        target: "frus::dev",
3997                        "no keyboard to show: the window is not simulating a device (frus run -d)"
3998                    ),
3999                },
4000                // A simulated device turned on its side and back (milestone 722).
4001                "rotate" => match self.simulated.as_mut() {
4002                    Some(simulated) => {
4003                        simulated.orientation = match simulated.orientation {
4004                            frus_widgets::Orientation::Portrait => {
4005                                frus_widgets::Orientation::Landscape
4006                            }
4007                            frus_widgets::Orientation::Landscape => {
4008                                frus_widgets::Orientation::Portrait
4009                            }
4010                        };
4011                        log::info!(
4012                            target: "frus::dev",
4013                            "{} {:?}",
4014                            simulated.profile.name,
4015                            simulated.orientation
4016                        );
4017                        // The window turns with the device: its room is the same, transposed.
4018                        // Not a page's canvas, which the page sizes: asked for a size, winit
4019                        // writes it on the canvas and the page is turned on its side too
4020                        // (milestone 724).
4021                        #[cfg(desktop)]
4022                        if let Some(window) = &self.window {
4023                            let size = window.inner_size();
4024                            let _ = window.request_inner_size(winit::dpi::PhysicalSize::new(
4025                                size.height,
4026                                size.width,
4027                            ));
4028                        }
4029                        self.request_redraw();
4030                    }
4031                    None => log::info!(
4032                        target: "frus::dev",
4033                        "nothing to rotate: the window is not simulating a device (frus run -d)"
4034                    ),
4035                },
4036                // The system's low-memory call, on demand (milestone 687).
4037                "trim-memory" => memory_trimmed(),
4038                // A driver reset, on purpose: what follows is the recovery (milestone 686).
4039                "lose-gpu" => {
4040                    if let Some(renderer) = self.renderer.as_ref() {
4041                        log::warn!(target: "frus::dev", "losing the GPU device, as a driver reset would");
4042                        renderer.lose_device_for_testing();
4043                        self.request_redraw();
4044                    }
4045                }
4046                "screenshot" if !argument.is_empty() => {
4047                    #[cfg(desktop)]
4048                    {
4049                        self.debug.screenshot = Some(std::path::PathBuf::from(argument));
4050                    }
4051                    #[cfg(web)]
4052                    {
4053                        self.debug.page_screenshot = true;
4054                    }
4055                    #[cfg(android)]
4056                    log::warn!(target: "frus::dev", "the application takes no screenshot of itself here");
4057                }
4058                _ => {
4059                    log::warn!(target: "frus::dev", "frus run asked for `{command}`, which this frus does not know")
4060                }
4061            }
4062            self.build_dirty = true;
4063        }
4064    }
4065
4066    /// The brightness the theme is resolved with: the platform's, unless `b` said otherwise.
4067    fn brightness(&self) -> frus_widgets::Brightness {
4068        self.debug.brightness.unwrap_or(self.platform.brightness)
4069    }
4070
4071    /// How tall the title bar's line the system paints is, in physical pixels: `0` where
4072    /// it paints none (milestone 643).
4073    fn system_line(&self) -> f32 {
4074        // The line the content was told: the one its row covers.
4075        match self.title_bar {
4076            Some(line) if self.title_bar_on && line.system_paints => {
4077                line.height * self.total_scale()
4078            }
4079            _ => 0.0,
4080        }
4081    }
4082
4083    /// The total scale: system DPI × app density (physical = logical × this).
4084    ///
4085    /// For a simulated device, system DPI × the zoom that fits the device in the window:
4086    /// the device's own size is the application's, and its density is the device's.
4087    fn total_scale(&self) -> f32 {
4088        if let Some(placement) = self.placement() {
4089            return (self.scale * placement.zoom).max(0.01);
4090        }
4091        (self.scale * self.app.density()).max(0.1)
4092    }
4093
4094    /// Where the simulated device sits in the window; `None` when there is none.
4095    fn placement(&self) -> Option<crate::simulated::Placement> {
4096        let simulated = self.simulated.as_ref()?;
4097        let size = self.window.as_ref()?.inner_size();
4098        let scale = self.scale.max(0.1);
4099        Some(simulated.place(Size::new(
4100            size.width as f32 / scale,
4101            size.height as f32 / scale,
4102        )))
4103    }
4104
4105    /// The application's logical point under a physical position in the window.
4106    fn to_logical(&self, x: f64, y: f64) -> Point {
4107        if let Some(placement) = self.placement() {
4108            let scale = self.scale.max(0.1);
4109            return crate::simulated::Simulated::to_screen(
4110                placement,
4111                Point::new(x as f32 / scale, y as f32 / scale),
4112            );
4113        }
4114        let scale = self.total_scale();
4115        Point::new(x as f32 / scale, y as f32 / scale)
4116    }
4117
4118    /// The application's logical size for a window of `width` by `height` physical pixels:
4119    /// the simulated device's screen when there is one.
4120    fn logical_size(&self, width: u32, height: u32) -> (f32, f32) {
4121        if let Some(simulated) = &self.simulated {
4122            let screen = simulated.screen_size();
4123            return (screen.width, screen.height);
4124        }
4125        let scale = self.total_scale();
4126        (width as f32 / scale, height as f32 / scale)
4127    }
4128
4129    /// The surface description installed around every call to `view`, so that any
4130    /// widget built there can read it with `MediaQuery::of()` instead of having the
4131    /// application carry it down by hand.
4132    ///
4133    /// Everything in it is already known to the shell — the logical size it is about
4134    /// to lay out for, the DPI scale, the app's density, and the insets last reported
4135    /// by the platform. It is assembled here, in one place, rather than at each of the
4136    /// call sites.
4137    /// The **platform** answers, and the application overrides what it chose to speak for.
4138    ///
4139    /// That order is the one the reference uses and the only one that stays honest: the
4140    /// settings belong to the person using the device, so the framework asks them first,
4141    /// and an application with a *reduce motion* switch of its own says only that. An
4142    /// [`AccessibilityOverrides`] whose fields are all `None` — the default — leaves every
4143    /// answer to the user, which is what an application that has no settings screen means.
4144    fn media_query(&self, width: f32, height: f32) -> MediaQuery {
4145        // A simulated device reports its own density, as the device would.
4146        let (ratio, density) = match &self.simulated {
4147            Some(simulated) => (simulated.profile.pixel_ratio, 1.0),
4148            None => (self.scale, self.app.density()),
4149        };
4150        MediaQuery::new(Size::new(width, height))
4151            .with_device_pixel_ratio(ratio)
4152            .with_density(density)
4153            .with_text_scaler(self.debug.text_scale.unwrap_or(self.platform.text_scaler))
4154            .with_platform_brightness(self.brightness())
4155            .with_accessibility(
4156                self.platform
4157                    .accessibility
4158                    .with_overrides(self.app.accessibility()),
4159            )
4160            .with_insets(self.last_insets)
4161            .with_title_bar(self.title_bar)
4162    }
4163
4164    /// **Shares the window's title bar line with the content** when the frame asks for it,
4165    /// gives it back when it no longer does, and tells the content what the system keeps on
4166    /// it (milestone 640). See `title_bar.rs`.
4167    #[cfg(windows)]
4168    fn sync_title_bar(&mut self) {
4169        use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
4170        let Some(window) = self.window.clone() else {
4171            return;
4172        };
4173        let hwnd = match window.window_handle().map(|h| h.as_raw()) {
4174            Ok(RawWindowHandle::Win32(win)) => {
4175                win.hwnd.get() as windows_sys::Win32::Foundation::HWND
4176            }
4177            _ => return,
4178        };
4179        // A simulated device has no title bar to share: the window keeps the system's.
4180        let wants =
4181            self.simulated.is_none() && self.ui.as_ref().is_some_and(|ui| ui.wants_title_bar());
4182        if wants != self.title_bar_on {
4183            // SAFETY: the window is alive and was made on this thread.
4184            unsafe {
4185                if wants {
4186                    crate::title_bar::enable(hwnd);
4187                    let wake = window.clone();
4188                    crate::title_bar::on_change(move || wake.request_redraw());
4189                } else {
4190                    crate::title_bar::disable(hwnd);
4191                }
4192            }
4193            self.title_bar_on = wants;
4194        }
4195        let next = if self.title_bar_on {
4196            let scale = self.total_scale();
4197            let band = crate::title_bar::height() as f32 / scale;
4198            // What the content has on the line is its own; the rest moves the window.
4199            if let Some(ui) = &self.ui {
4200                let to_px = |r: frus_widgets::Rect| windows_sys::Win32::Foundation::RECT {
4201                    left: (r.x * scale).floor() as i32,
4202                    top: (r.y * scale).floor() as i32,
4203                    right: ((r.x + r.width) * scale).ceil() as i32,
4204                    bottom: ((r.y + r.height) * scale).ceil() as i32,
4205                };
4206                let interactive = ui
4207                    .hit_rects()
4208                    .filter(|r| r.y < band && r.width < self.logical_width())
4209                    .map(to_px)
4210                    .collect();
4211                let icon = ui
4212                    .title_bar_regions()
4213                    .iter()
4214                    .find(|(_, role)| *role == frus_widgets::TitleBarRole::Icon)
4215                    .map(|(r, _)| to_px(*r));
4216                crate::title_bar::publish(interactive, icon);
4217            }
4218            // SAFETY: as above.
4219            let buttons = unsafe { crate::title_bar::buttons(hwnd) }.map(|r| {
4220                frus_widgets::Rect::new(
4221                    r.left as f32 / scale,
4222                    r.top as f32 / scale,
4223                    (r.right - r.left) as f32 / scale,
4224                    (r.bottom - r.top) as f32 / scale,
4225                )
4226            });
4227            let button = |code: u32| {
4228                use windows_sys::Win32::UI::WindowsAndMessaging::{
4229                    HTCLOSE, HTMAXBUTTON, HTMINBUTTON,
4230                };
4231                match code {
4232                    HTMINBUTTON => Some(frus_widgets::CaptionButton::Minimize),
4233                    HTMAXBUTTON => Some(frus_widgets::CaptionButton::Maximize),
4234                    HTCLOSE => Some(frus_widgets::CaptionButton::Close),
4235                    _ => None,
4236                }
4237            };
4238            let (hovered, pressed) = crate::title_bar::buttons_state();
4239            if self.window_icon.is_none() {
4240                self.window_icon = self
4241                    .app
4242                    .icon()
4243                    .png()
4244                    .map(|png| &*Box::leak(png.to_vec().into_boxed_slice()));
4245            }
4246            // The caption the system would draw: dark as the window's own, which follows the
4247            // system's apps (milestone 642).
4248            let active = window.has_focus();
4249            let dark = window.theme() == Some(winit::window::Theme::Dark);
4250            let (background, foreground) = crate::title_bar::caption_colors(dark, active);
4251            Some(frus_widgets::TitleBar {
4252                // The line ends where the system's buttons do: maximized, the window's
4253                // resizing border is off the screen and the buttons sit higher, and the words
4254                // centred in the line stay level with them (milestone 643).
4255                height: buttons.map_or(band, |b| (b.y + b.height).clamp(1.0, band)),
4256                leading: 0.0,
4257                buttons,
4258                icon: self.window_icon,
4259                hovered: button(hovered),
4260                pressed: button(pressed),
4261                maximized: window.is_maximized(),
4262                active,
4263                background: Some(background),
4264                foreground: Some(foreground),
4265                // The system shows its own caption through a see-through frame, where it
4266                // paints a backdrop on the line (milestone 643).
4267                system_paints: self.renderer.as_ref().is_some_and(Renderer::sees_through)
4268                    // SAFETY: as above.
4269                    && unsafe { crate::title_bar::system_backdrop(hwnd, dark) },
4270            })
4271        } else {
4272            None
4273        };
4274        if next != self.title_bar {
4275            log::debug!(
4276                target: "frus::title_bar",
4277                "the line: {:?}",
4278                next.map(|line| (line.height, line.buttons, line.active, line.system_paints))
4279            );
4280            self.title_bar = next;
4281            self.build_dirty = true;
4282            self.request_redraw();
4283        }
4284    }
4285
4286    /// The current layout direction; RTL flips both the layout and the gestures.
4287    fn is_rtl(&self) -> bool {
4288        self.themes.displayed(&self.app).direction.is_rtl()
4289    }
4290
4291    /// The window's **logical** width, in px, for the edge thresholds.
4292    fn logical_width(&self) -> f32 {
4293        if let Some(simulated) = &self.simulated {
4294            return simulated.screen_size().width;
4295        }
4296        let scale = self.total_scale();
4297        self.window
4298            .as_ref()
4299            .map(|w| w.inner_size().width as f32 / scale)
4300            .unwrap_or(1.0)
4301            .max(1.0)
4302    }
4303
4304    fn request_redraw(&self) {
4305        if let Some(window) = &self.window {
4306            window.request_redraw();
4307        }
4308    }
4309
4310    /// Where the system's cursor is, read from the system: while it runs a drag of files
4311    /// the window hears no pointer movement. `None` where it cannot be read.
4312    fn system_cursor(&self) -> Option<Point> {
4313        let window = self.window.as_ref()?;
4314        let physical = crate::file_drop::cursor_in(window, 1.0)?;
4315        Some(self.to_logical(physical.x as f64, physical.y as f64))
4316    }
4317
4318    /// One file of a drag from outside the application, held over the window or dropped
4319    /// on it, the cursor at `at` when that is known.
4320    fn file_event(&mut self, path: std::path::PathBuf, dropping: bool, at: Option<Point>) {
4321        self.file_drag
4322            .get_or_insert_with(Default::default)
4323            .add(path, dropping);
4324        self.files_moved(at);
4325    }
4326
4327    /// Files held over the window, the cursor at `at` when that is known: the target
4328    /// they would land on lights up, as one does under an item carried from inside.
4329    fn files_moved(&mut self, at: Option<Point>) {
4330        let Some(drag) = self.file_drag.as_ref() else {
4331            return;
4332        };
4333        let over = match (self.ui.as_ref(), self.tree.as_ref()) {
4334            (Some(ui), Some(tree)) => {
4335                crate::file_drop::target(ui, tree.as_ref(), &drag.paths, at, self.cursor)
4336            }
4337            _ => None,
4338        };
4339        if drag.over != over {
4340            if let Some(drag) = self.file_drag.as_mut() {
4341                drag.over = over;
4342            }
4343            self.runtime.drag_over = over;
4344            self.request_redraw();
4345        }
4346    }
4347
4348    /// The files were taken away again without being dropped.
4349    fn files_left(&mut self) {
4350        if let Some(drag) = self.file_drag.take() {
4351            if drag.over.is_some() {
4352                self.runtime.drag_over = None;
4353                self.request_redraw();
4354            }
4355        }
4356    }
4357
4358    /// Hands dropped files to the target they landed on, all together: the system names
4359    /// them one event at a time, and this runs once the loop has had every one.
4360    fn deliver_dropped_files(&mut self) {
4361        if !self.file_drag.as_ref().is_some_and(|drag| drag.dropped) {
4362            return;
4363        }
4364        let Some(drag) = self.file_drag.take() else {
4365            return;
4366        };
4367        if drag.over.is_some() {
4368            self.runtime.drag_over = None;
4369        }
4370        let message = drag.over.and_then(|id| {
4371            self.tree
4372                .as_ref()
4373                .and_then(|tree| find_widget(tree.as_ref(), id))
4374                .and_then(|widget| widget.on_files_dropped(drag.paths.clone()))
4375        });
4376        if message.is_none() {
4377            log::debug!(
4378                "{} file(s) dropped where nothing takes them",
4379                drag.paths.len()
4380            );
4381        }
4382        if let Some(message) = message {
4383            self.dispatch(message);
4384        }
4385        self.request_redraw();
4386    }
4387
4388    /// The **normalised** pointer input (gesture tier 0): mouse and touch converge
4389    /// here, with an explicit `Cancel`. The long-press recogniser is fed along the way,
4390    /// and the loop is woken at its deadline.
4391    fn pointer(&mut self, event_loop: &ActiveEventLoop, event: PointerEvent) {
4392        self.pointer_event(event);
4393        // Wake the loop exactly at the next deadline, and rest otherwise.
4394        event_loop.set_control_flow(self.idle_control_flow());
4395    }
4396
4397    /// Everything [`pointer`](Self::pointer) does with an event but wake the loop: the one
4398    /// place a press, a movement and a release are routed, and so what a test drives. The
4399    /// regions the pointer is in are brought up to date after it (milestone 583).
4400    fn pointer_event(&mut self, event: PointerEvent) {
4401        let kind = event.kind;
4402        // The raw events first, against the frame the pointer is over: a listener hears the
4403        // press before anything it leads to rebuilds the tree (milestone 586).
4404        self.cursor = event.position;
4405        self.route_listeners(kind, event.touch);
4406        self.route_pointer_event(event);
4407        self.pointer_held = match kind {
4408            PointerKind::Down => true,
4409            PointerKind::Up | PointerKind::Cancel => false,
4410            PointerKind::Move => self.pointer_held,
4411        };
4412        self.sync_regions(kind == PointerKind::Move);
4413    }
4414
4415    /// Routes one pointer event: see [`pointer_event`](Self::pointer_event).
4416    fn route_pointer_event(&mut self, event: PointerEvent) {
4417        self.cursor = event.position;
4418        self.hover.event(event.kind, event.position, event.touch);
4419        // Whether the gesture under way could still end as a tap, before this event moves
4420        // it on: the press is shared with it until it cannot (milestone 534).
4421        let could_tap = gesture_was_a_tap(self.drag.as_ref());
4422        match event.kind {
4423            PointerKind::Down => {
4424                // What is under the pointer **now**: a finger has no move before its
4425                // press, and a press only shows while it is also the hovered widget.
4426                self.sync_hover();
4427                // A pointer interaction: the keyboard focus ring fades away.
4428                self.runtime.focus_visible = false;
4429                self.pointer_down(event.touch);
4430                let (long_press, lift) = hold_candidates(
4431                    self.drag.as_ref(),
4432                    self.ui.as_ref(),
4433                    self.tree.as_deref(),
4434                    self.cursor,
4435                );
4436                self.long_press_msg = long_press;
4437                self.pending_lift = lift;
4438                let interested = self.long_press_msg.is_some()
4439                    || self.pending_lift.is_some()
4440                    || self.pending_reorder.is_some()
4441                    || self.pending_word.is_some()
4442                    || self.pending_region_hold;
4443                self.press.down(self.cursor, Instant::now(), interested);
4444            }
4445            PointerKind::Move => {
4446                self.press.moved(self.cursor);
4447                self.pointer_move();
4448                // The inspector follows the pointer to highlight, so it redraws even
4449                // over inert widgets, which have no hover animation.
4450                if self.inspector {
4451                    self.request_redraw();
4452                }
4453            }
4454            PointerKind::Up => {
4455                // The recogniser is always told, but a lifted item owes a drop: the
4456                // long press that started it must not also eat its ending.
4457                let swallow = self.press.up();
4458                self.pending_lift = None;
4459                self.pending_reorder = None;
4460                self.pending_word = None;
4461                self.pending_region_hold = false;
4462                // A row lifted by the hold owes its drop for the same reason a lifted
4463                // item does: the release is what says where it goes.
4464                if swallow
4465                    && !matches!(
4466                        self.drag,
4467                        Some(Drag::Item { moved: true, .. } | Drag::Reorder { moved: true, .. })
4468                    )
4469                {
4470                    // The long press evicted the tap, so the release is swallowed.
4471                    self.drag = None;
4472                    self.runtime.input.pressed = None;
4473                    self.request_redraw();
4474                } else {
4475                    self.pointer_up();
4476                }
4477            }
4478            PointerKind::Cancel => {
4479                self.press.cancel();
4480                self.pending_lift = None;
4481                self.pending_reorder = None;
4482                self.pending_word = None;
4483                self.pending_region_hold = false;
4484                // A cancelled gesture still owes the offset back, or the region
4485                // would stay frozen under a finger that is no longer there.
4486                if let Some(Drag::Dismiss { item, .. }) = self.drag {
4487                    // A cancelled swipe asks for nothing: zero velocity puts it back.
4488                    self.runtime.dismiss_release(
4489                        item.id,
4490                        0.0,
4491                        0.0,
4492                        item.spec.axis,
4493                        // Unreachable threshold: a cancel never dismisses, however far
4494                        // the item had already travelled.
4495                        f32::INFINITY,
4496                    );
4497                }
4498                // A value a cancelled drag held settles back, and says nothing.
4499                if let Some(Drag::Gesture { id, .. }) = self.drag {
4500                    self.runtime.release_value(id);
4501                }
4502                if let Some(Drag::Scroll { id, .. }) = self.drag {
4503                    self.runtime.release_scroll(id);
4504                    self.runtime.glow_scroll_end(id);
4505                    // A cancelled gesture asks for nothing: the indicator slides back
4506                    // rather than promising a refresh nobody requested.
4507                    if let Some(host) = self.refresh_host_of(id) {
4508                        self.runtime.refresh_cancel(host);
4509                    }
4510                }
4511                // A sheet under a cancelled finger settles from where it was left.
4512                let sheet = match self.drag {
4513                    Some(Drag::Sheet { id, available, .. }) => self
4514                        .ui
4515                        .as_ref()
4516                        .and_then(|ui| ui.sheet(id))
4517                        .map(|sheet| (sheet.id, sheet.spec.clone(), available)),
4518                    Some(Drag::Scroll { id, .. }) => self
4519                        .ui
4520                        .as_ref()
4521                        .and_then(|ui| ui.sheet_holding(id))
4522                        .map(|sheet| (sheet.id, sheet.spec.clone(), sheet.available)),
4523                    _ => None,
4524                };
4525                if let Some((id, spec, available)) = sheet {
4526                    self.runtime.sheet_release(id, &spec, available, 0.0, None);
4527                }
4528                self.drag = None;
4529                self.runtime.input.pressed = None;
4530                self.request_redraw();
4531            }
4532        }
4533        // A press a gesture has taken, or one whose pointer has gone, is let go of — after
4534        // the routing, which is what reads it (milestone 534).
4535        if settle_press(
4536            &mut self.runtime.input,
4537            event.kind,
4538            could_tap,
4539            self.drag.as_ref(),
4540            event.touch,
4541        ) {
4542            self.request_redraw();
4543        }
4544        // After the release has been routed — it reads the press, not the hover — a
4545        // finger that has lifted stops hovering (milestone 505).
4546        if matches!(event.kind, PointerKind::Up | PointerKind::Cancel) {
4547            self.sync_hover();
4548        }
4549    }
4550
4551    /// A press held still for the long-press delay: what it was a candidate for is decided.
4552    fn hold_deadline_reached(&mut self) {
4553        // Two claims on one hold: a widget asking for a message, and an item asking
4554        // to be lifted. Serving both would do a discrete action *and* start a drag
4555        // from the same gesture, which is never what anyone meant. **The lift
4556        // wins** — it changes what the rest of the gesture means, and the message
4557        // would be acting on something the finger is still holding.
4558        let lifting = self.pending_lift.is_some() || self.pending_reorder.is_some();
4559        // A hold in a text field selects the word under the finger (milestone 511),
4560        // and is that — not also the long press of whatever the field sits in.
4561        let word = self.pending_word.take();
4562        // A hold on words in a selection area selects the word, unless something held asked
4563        // for the hold first: a lift, a field, a widget's own long press.
4564        let region = std::mem::take(&mut self.pending_region_hold)
4565            && !lifting
4566            && word.is_none()
4567            && self.long_press_msg.is_none();
4568        if let Some(message) = self.long_press_msg.take() {
4569            if !lifting && word.is_none() {
4570                self.dispatch(message);
4571            }
4572        }
4573        if let Some(id) = word {
4574            self.select_held_word(id);
4575        }
4576        if region && self.select_region_word_held() {
4577            // The page under the finger stops being scrolled by it: the hold was a selection.
4578            if let Some(Drag::Scroll { id, .. }) = self.drag {
4579                self.runtime.release_scroll(id);
4580            }
4581        }
4582        let lift = self.pending_lift.take();
4583        let reorder = self.pending_reorder.take();
4584        // The scroll hands the gesture over to what the hold lifted.
4585        if lift.is_some() || reorder.is_some() {
4586            if let Some(Drag::Scroll { id, .. }) = self.drag {
4587                self.runtime.release_scroll(id);
4588            }
4589        }
4590        self.drag = drag_after_hold(self.drag.take(), lift, reorder, self.cursor);
4591        if matches!(self.drag, Some(Drag::Reorder { .. })) {
4592            self.reorder_x = self.cursor.x;
4593            self.reorder_y = self.cursor.y;
4594        }
4595        self.request_redraw();
4596    }
4597
4598    /// The loop's idle policy: wake at the **nearest** deadline — the long press, the
4599    /// live-reload poll, the caret's next turn, the next look at where files being
4600    /// dragged over the window are — and otherwise wait outright.
4601    fn idle_control_flow(&self) -> ControlFlow {
4602        let press = self.press.deadline();
4603        let reload = self.reload.as_ref().map(|w| w.deadline());
4604        // Twice a second while a field has the caret and the window is in front, and never
4605        // otherwise (milestone 513).
4606        let caret = (self.lifecycle == Lifecycle::Resumed)
4607            .then(|| self.caret.next_toggle(Instant::now()))
4608            .flatten();
4609        // A first tap that may still become a double tap is sent when its wait is over.
4610        let tap = self.pending_tap.as_ref().map(|p| p.at + DOUBLE_TAP_TIMEOUT);
4611        // Files held over the window, where the system's cursor can be read: nothing else
4612        // says when they move from one target to another (milestone 695).
4613        let files =
4614            (cfg!(windows) && self.file_drag.is_some()).then(|| Instant::now() + FILE_DRAG_POLL);
4615        match [press, reload, caret, tap, files]
4616            .into_iter()
4617            .flatten()
4618            .min()
4619        {
4620            Some(at) => ControlFlow::WaitUntil(at),
4621            None => ControlFlow::Wait,
4622        }
4623    }
4624
4625    /// The focused field's caret, as its blink sees it: which field, where its caret and
4626    /// selection are, and a digest of its value. `None` when what has focus takes no
4627    /// typing, which has no caret to blink.
4628    fn caret_signature(&self) -> Option<crate::caret::Signature> {
4629        use std::hash::{Hash, Hasher};
4630        let field = self.runtime.input.focused?;
4631        let widget = find_widget(self.tree.as_ref()?.as_ref(), field)?;
4632        let text = widget.text_value()?;
4633        let mut hasher = std::collections::hash_map::DefaultHasher::new();
4634        text.hash(&mut hasher);
4635        Some(crate::caret::Signature {
4636            field,
4637            edit: self.runtime.edits.get(&field).copied().unwrap_or_default(),
4638            value: hasher.finish(),
4639        })
4640    }
4641
4642    /// Pointer movement, mouse or finger: continues a drag under way, and otherwise
4643    /// updates the hover.
4644    fn pointer_move(&mut self) {
4645        if self.drag.is_some() {
4646            self.handle_drag();
4647            return;
4648        }
4649        let hovered = self.sync_hover();
4650        // The system cursor follows the hovered sub-region (milestone 205): a hand
4651        // over a clickable icon, and so on. Recomputed on every move, since the
4652        // sub-region can change without the hovered widget changing.
4653        self.update_cursor_icon(hovered);
4654    }
4655
4656    /// Asks the frame what is under the hovering pointer, and records it.
4657    ///
4658    /// The only way the hover changes, so it can never name a widget of another frame:
4659    /// an id is a position in the tree, and the same position on the next screen is a
4660    /// different widget (milestone 505).
4661    fn sync_hover(&mut self) -> Option<WidgetId> {
4662        let hovered = self.ui.as_ref().and_then(|ui| self.hover.target(ui));
4663        if hovered != self.runtime.input.hovered {
4664            self.runtime.input.hovered = hovered;
4665            self.request_redraw();
4666        }
4667        hovered
4668    }
4669
4670    /// Whether work a component started has brought something since this shell last looked
4671    /// (milestone 585).
4672    fn take_task_arrivals(&mut self) -> bool {
4673        let now = frus_widgets::task_arrivals();
4674        std::mem::replace(&mut self.task_arrivals, now) != now
4675    }
4676
4677    /// Hands one raw pointer event to the widgets that hear them (milestone 586): a press to
4678    /// every one under the pointer, the innermost first, which then hear that pointer until
4679    /// it lifts, wherever it goes; a mouse moving with nothing held, to those under it.
4680    fn route_listeners(&mut self, kind: PointerKind, touch: bool) {
4681        use frus_widgets::ListenerEventKind as Raw;
4682        let at = self.cursor;
4683        let heard: Vec<(WidgetId, Rect, Raw)> = match kind {
4684            PointerKind::Down => {
4685                self.captured = self
4686                    .ui
4687                    .as_ref()
4688                    .map(|ui| ui.pointer_listeners_at(at))
4689                    .unwrap_or_default();
4690                self.captured
4691                    .iter()
4692                    .map(|(id, rect)| (*id, *rect, Raw::Down))
4693                    .collect()
4694            }
4695            PointerKind::Move if self.pointer_held => self
4696                .captured
4697                .iter()
4698                .map(|(id, rect)| (*id, *rect, Raw::Move))
4699                .collect(),
4700            PointerKind::Move if !touch => self
4701                .ui
4702                .as_ref()
4703                .map(|ui| ui.pointer_listeners_at(at))
4704                .unwrap_or_default()
4705                .into_iter()
4706                .map(|(id, rect)| (id, rect, Raw::Hover))
4707                .collect(),
4708            PointerKind::Move => Vec::new(),
4709            PointerKind::Up | PointerKind::Cancel => {
4710                let raw = if kind == PointerKind::Up {
4711                    Raw::Up
4712                } else {
4713                    Raw::Cancel
4714                };
4715                std::mem::take(&mut self.captured)
4716                    .into_iter()
4717                    .map(|(id, rect)| (id, rect, raw))
4718                    .collect()
4719            }
4720        };
4721        let messages: Vec<A::Message> = heard
4722            .into_iter()
4723            .flat_map(|(id, rect, kind)| {
4724                let event = frus_widgets::ListenerEvent {
4725                    kind,
4726                    local: Point::new(at.x - rect.x, at.y - rect.y),
4727                    touch,
4728                };
4729                self.tree
4730                    .as_ref()
4731                    .and_then(|tree| find_widget(tree.as_ref(), id))
4732                    .map(|widget| widget.on_pointer_event(event))
4733                    .unwrap_or_default()
4734            })
4735            .collect();
4736        for message in messages {
4737            self.dispatch(message);
4738            self.request_redraw();
4739        }
4740    }
4741
4742    /// Brings the regions the pointer is in up to date (milestone 583): an exit for each it has
4743    /// left, innermost first, an enter for each it has come into, outermost first, and — when
4744    /// the pointer `moved` with nothing held — a move for each it is still in.
4745    fn sync_regions(&mut self, moved: bool) {
4746        let point = self.hover.at();
4747        let now: Vec<(WidgetId, Rect)> = match (point, self.ui.as_ref()) {
4748            (Some(point), Some(ui)) => ui
4749                .hover_regions_at(point)
4750                .into_iter()
4751                .map(|(id, rect, _)| (id, rect))
4752                .collect(),
4753            _ => Vec::new(),
4754        };
4755        let at = point.unwrap_or(self.cursor);
4756        let local = |rect: Rect| Point::new(at.x - rect.x, at.y - rect.y);
4757        let before = std::mem::replace(&mut self.entered, now.clone());
4758        let mut events = Vec::new();
4759        for (id, rect) in &before {
4760            if !now.iter().any(|(n, _)| n == id) {
4761                events.push((
4762                    *id,
4763                    frus_widgets::HoverEvent::Exit {
4764                        local: local(*rect),
4765                    },
4766                ));
4767            }
4768        }
4769        for (id, rect) in now.iter().rev() {
4770            if !before.iter().any(|(b, _)| b == id) {
4771                events.push((
4772                    *id,
4773                    frus_widgets::HoverEvent::Enter {
4774                        local: local(*rect),
4775                    },
4776                ));
4777            } else if moved && !self.pointer_held {
4778                events.push((
4779                    *id,
4780                    frus_widgets::HoverEvent::Move {
4781                        local: local(*rect),
4782                    },
4783                ));
4784            }
4785        }
4786        let messages: Vec<_> = events
4787            .into_iter()
4788            .filter_map(|(id, event)| {
4789                self.tree
4790                    .as_ref()
4791                    .and_then(|tree| find_widget(tree.as_ref(), id))
4792                    .and_then(|widget| widget.on_hover_event(event))
4793            })
4794            .collect();
4795        for message in messages {
4796            self.dispatch(message);
4797            self.request_redraw();
4798        }
4799    }
4800
4801    /// The cursor to show: what the widget under the pointer asks for, `requested`, first;
4802    /// then the text cursor over a field that can be focused, as a platform shows it; then
4803    /// the innermost region the pointer is in that asks for one (milestone 583); otherwise
4804    /// the default.
4805    fn region_cursor(&self, requested: Option<UiCursor>) -> UiCursor {
4806        requested
4807            .or_else(|| {
4808                let point = self.hover.at()?;
4809                let (id, _) = self.ui.as_ref()?.focus_hit(point)?;
4810                let widget = find_widget(self.tree.as_ref()?.as_ref(), id)?;
4811                is_text_field(widget).then_some(UiCursor::Text)
4812            })
4813            .or_else(|| {
4814                self.hover.at().and_then(|point| {
4815                    self.ui
4816                        .as_ref()?
4817                        .hover_regions_at(point)
4818                        .into_iter()
4819                        .find_map(|(_, _, region)| region.cursor)
4820                })
4821            })
4822            .unwrap_or(UiCursor::Default)
4823    }
4824
4825    /// Applies the cursor shape the hovered widget asks for at the pointer's local
4826    /// position (milestone 205), and the default cursor otherwise. Translates
4827    /// `frus_widgets::Cursor` into winit's.
4828    fn update_cursor_icon(&mut self, hovered: Option<WidgetId>) {
4829        let requested = hovered.and_then(|id| {
4830            let rect = self.ui.as_ref()?.widget_rect(id)?;
4831            let widget = find_widget(self.tree.as_ref()?.as_ref(), id)?;
4832            widget.cursor_icon(
4833                self.cursor.x - rect.x,
4834                self.cursor.y - rect.y,
4835                rect.width,
4836                rect.height,
4837            )
4838        });
4839        let shown = self.region_cursor(requested);
4840        if let Some(window) = &self.window {
4841            window.set_cursor_visible(shown != UiCursor::None);
4842            window.set_cursor(cursor_icon(shown));
4843        }
4844        // Sub-region highlighting (milestone 208): the pointer's position is retained
4845        // while it hovers an interactive sub-region, that is, while cursor_icon answered.
4846        // A change repaints — the status hash includes hover_cursor — so the halo
4847        // follows the pointer or goes away.
4848        let hover_cursor = requested.map(|_| self.cursor);
4849        if hover_cursor != self.runtime.input.hover_cursor {
4850            self.runtime.input.hover_cursor = hover_cursor;
4851            self.request_redraw();
4852        }
4853    }
4854
4855    /// A pointer press, mouse or finger, at `self.cursor`. `touch` enables finger
4856    /// scrolling when no other gesture captures the press.
4857    fn pointer_down(&mut self, touch: bool) {
4858        self.pointer_touch = touch;
4859        // A first tap still waiting for its second is a tap after all if this press is not
4860        // that second one: somewhere else, or too late.
4861        self.poll_pending_tap(Instant::now());
4862        if let Some(pending) = self.pending_tap.as_ref() {
4863            let here = self.ui.as_ref().and_then(|ui| ui.hit(self.cursor));
4864            let (dx, dy) = (
4865                self.cursor.x - pending.position.x,
4866                self.cursor.y - pending.position.y,
4867            );
4868            if here != Some(pending.id) || dx.hypot(dy) > DOUBLE_TAP_SLOP {
4869                self.flush_pending_tap();
4870            }
4871        }
4872        self.pending_word = None;
4873        self.pending_region_hold = false;
4874        // A press **on the bar** comes first: the bar is drawn over everything, the handles
4875        // included, and the desktop menu hangs from the selection's top, over its handles
4876        // (milestone 618). It acts on the focused field, so nothing here may touch the
4877        // focus, the selection or the handles: it is only recorded, and `pointer_up`
4878        // resolves it to the button's action.
4879        if self
4880            .ui
4881            .as_ref()
4882            .is_some_and(|ui| ui.toolbar_contains(self.cursor))
4883        {
4884            press_at(&mut self.runtime, self.ui.as_ref(), self.cursor);
4885            self.request_redraw();
4886            return;
4887        }
4888        // Then a selection handle. It hangs below its line, over whatever is drawn
4889        // there, so nothing else may claim the press before it; and any other press puts
4890        // the handles away (milestone 511).
4891        // A selection area's handle likewise: a press on one widens or narrows the selection
4892        // rather than putting it away.
4893        if let Some(drag) = self.grab_region_handle() {
4894            self.drag = Some(drag);
4895            if let Some(selection) = self.runtime.region.as_mut() {
4896                selection.bar = false;
4897            }
4898            self.request_redraw();
4899            return;
4900        }
4901        if let Some(drag) = self.grab_selection_handle() {
4902            self.drag = Some(drag);
4903            // The bar is put away while a handle is dragged, and comes back when it is let
4904            // go (`pointer_up`): a bar that followed a moving selection would flicker.
4905            self.runtime.selection_toolbar = None;
4906            self.request_redraw();
4907            return;
4908        }
4909        if self.runtime.selection_handles.take().is_some() {
4910            self.request_redraw();
4911        }
4912        self.hide_selection_toolbar();
4913        // A selection an area holds goes with any press: the one that begins another puts a
4914        // new one in its place, below.
4915        if self.runtime.region.take().is_some() {
4916            self.request_redraw();
4917        }
4918        // 0) The back gesture: a press on the **leading edge** — left under LTR,
4919        // right under RTL — if the app allows it. It takes the pages back, so it starts on
4920        // them and not on what is above them: the window's menu bar, whose first word sits
4921        // in the edge, or an overlay — the menu it opens, a dialog (milestone 642).
4922        let above_the_pages = self.ui.as_ref().is_some_and(|ui| {
4923            ui.over_overlay(self.cursor)
4924                || ui
4925                    .title_bar_row()
4926                    .is_some_and(|row| row.contains(self.cursor))
4927        });
4928        let on_back_edge = !above_the_pages
4929            && if self.is_rtl() {
4930                self.cursor.x > self.logical_width() - BACK_EDGE
4931            } else {
4932                self.cursor.x < BACK_EDGE
4933            };
4934        if on_back_edge && self.app.can_go_back() {
4935            self.drag = Some(Drag::Back {
4936                start_x: self.cursor.x,
4937            });
4938            self.begin_gesture();
4939            self.build_dirty = true;
4940            self.app.back_gesture(0.0);
4941            self.request_redraw();
4942            return;
4943        }
4944
4945        // 1) Is this a scrollbar drag?
4946        if let Some(bar) = self.ui.as_ref().and_then(|ui| ui.scrollbar_at(self.cursor)) {
4947            let (along, thumb_start) = if bar.vertical {
4948                (self.cursor.y, bar.thumb.y)
4949            } else {
4950                (self.cursor.x, bar.thumb.x)
4951            };
4952            self.drag = Some(Drag::Scrollbar {
4953                id: bar.id,
4954                vertical: bar.vertical,
4955                grab: along - thumb_start,
4956                track_start: bar.track_start,
4957                track_len: bar.track_len,
4958                thumb_len: bar.thumb_len,
4959                max: bar.max,
4960                reverse: bar.reverse,
4961            });
4962            self.request_redraw();
4963            return;
4964        }
4965
4966        // 1b) Is this a draggable widget, a Slider for instance?
4967        if let Some((id, rect)) = self.ui.as_ref().and_then(|ui| ui.draggable_at(self.cursor)) {
4968            let (x, rtl) = (self.cursor.x, self.is_rtl());
4969            let track = self
4970                .tree
4971                .as_ref()
4972                .and_then(|tree| find_widget(tree.as_ref(), id))
4973                .map_or(frus_widgets::DragTrack::WHOLE, |widget| widget.drag_track());
4974            let active = track.engages(rect, x, rtl);
4975            let thumbs = matches!(track.mode, frus_widgets::DragMode::Thumbs { .. });
4976            let thumb = track.pick_thumb(rect, x, 0.0, rtl);
4977            self.drag = Some(Drag::Widget {
4978                id,
4979                rect,
4980                last_x: x,
4981                press_x: x,
4982                track,
4983                active,
4984                thumb,
4985            });
4986            if thumbs {
4987                // Two thumbs: the one the press took jumps to it, as one alone would.
4988                if let Some(thumb) = thumb {
4989                    let at = track.fraction(rect, x, rtl);
4990                    self.thumb_drag(id, thumb, frus_widgets::ThumbDrag::Start, at);
4991                    self.thumb_drag(id, thumb, frus_widgets::ThumbDrag::Move, at);
4992                }
4993            } else if active {
4994                // The bracket opens **before** the first value: an application that means
4995                // to hold the expensive work until the finger lifts has to know the finger
4996                // went down first, and a press that jumps the value straight away would
4997                // otherwise deliver the change before the start.
4998                self.dispatch_drag_edge(id, Edge::Start, 0.0);
4999                // A zero delta on press: only a slider whose press moves it jumps.
5000                self.apply_widget_drag(id, 0.0, track.value_at(rect, x, x, false, rtl));
5001            }
5002            self.request_redraw();
5003            return;
5004        }
5005
5006        // 1c) A reorder: a press on a reorderable — a table header, a Kanban card, a
5007        // list's grip. We do not `return` — focus and `pressed`, where a tap means sort,
5008        // are settled below; the drag engages only past the threshold, and otherwise the
5009        // release sorts.
5010        //
5011        // A row that asked for a **hold** takes nothing now. The press is left to
5012        // whatever else wants it, which inside a list is the scroll, and the deadline
5013        // below decides between them: a finger that stays put was never scrolling.
5014        if let Some((id, from, hold)) = self.reorderable_at(self.cursor) {
5015            if hold {
5016                self.pending_reorder = Some((id, from, self.cursor));
5017            } else {
5018                self.drag = Some(Drag::Reorder {
5019                    id,
5020                    from,
5021                    start: self.cursor,
5022                    moved: false,
5023                    carried: None,
5024                });
5025                self.reorder_x = self.cursor.x; // starts glued to the pointer, with no jerk
5026                self.reorder_y = self.cursor.y; // likewise for the vertical insertion line
5027            }
5028        }
5029
5030        if press_at(&mut self.runtime, self.ui.as_ref(), self.cursor) {
5031            self.request_redraw();
5032        }
5033        // 2) Focus and caret placement, and the start of a text selection.
5034        let previously_focused = self.runtime.input.focused;
5035        let focus = self.ui.as_ref().and_then(|ui| ui.focus_hit(self.cursor));
5036        // A press outside the focused widget tells it so, before the focus moves
5037        // (milestone 660).
5038        if let Some(held) = previously_focused {
5039            let inside = focus.is_some_and(|(id, _)| id == held);
5040            let outside = (!inside)
5041                .then(|| {
5042                    self.tree
5043                        .as_ref()
5044                        .and_then(|tree| find_widget(tree.as_ref(), held))
5045                        .and_then(|widget| widget.on_tap_outside())
5046                })
5047                .flatten();
5048            if let Some(message) = outside {
5049                self.dispatch(message);
5050            }
5051        }
5052        self.runtime.input.focused = focus.map(|(id, _)| id);
5053        if let Some((id, rect)) = focus {
5054            let local_x = self.cursor.x - rect.x;
5055            // The retained vertical scroll, in a multi-line field, is part of the
5056            // content coordinate: we add it so the click lands on the right line.
5057            let local_y =
5058                self.cursor.y - rect.y + self.runtime.scroll.get(&id).map(|s| s.1).unwrap_or(0.0);
5059            // The scroll shown just before this click, computed from the current caret
5060            // when the field was already focused, and 0 otherwise.
5061            let scroll_cursor = if previously_focused == Some(id) {
5062                self.runtime.edits.get(&id).map(|e| e.cursor).unwrap_or(0)
5063            } else {
5064                0
5065            };
5066            // Only **text fields** (`cursor_at` → `Some`) start a selection; the other
5067            // focusables — buttons, checkboxes — keep focus but must NOT capture the
5068            // click, which would otherwise be swallowed on release as the end of a drag.
5069            let cursor = self
5070                .tree
5071                .as_ref()
5072                .and_then(|tree| find_widget(tree.as_ref(), id))
5073                .and_then(|widget| widget.cursor_at(local_x, local_y, rect.width, scroll_cursor));
5074            if let Some(cursor) = cursor {
5075                // Placing the caret with the mouse forgets the vertical goal column.
5076                self.goal_x = None;
5077                self.runtime.edits.insert(
5078                    id,
5079                    Edit {
5080                        cursor,
5081                        anchor: None,
5082                        composing: None,
5083                    },
5084                );
5085                self.drag = Some(Drag::TextSelect { id, rect });
5086                // A finger that stays put selects the word instead (milestone 511).
5087                if touch {
5088                    self.pending_word = Some(id);
5089                }
5090                // The caret moved, so the run of typing ends here: typing at one place,
5091                // then at another, then Ctrl+Z should take back only the second.
5092                self.runtime.close_edit_run(id);
5093                // Tapping in a field **reopens** the keyboard, even one the app already
5094                // considers shown but which the system back closed — see
5095                // `request_soft_input`.
5096                self.request_soft_input();
5097
5098                // A double click selects the word under the pointer.
5099                let now = Instant::now();
5100                let double = self
5101                    .last_click_time
5102                    .map(|t| (now - t).as_secs_f32() < 0.4)
5103                    .unwrap_or(false);
5104                self.last_click_time = Some(now);
5105                if double {
5106                    if let Some((start, end)) = self
5107                        .tree
5108                        .as_ref()
5109                        .and_then(|tree| find_widget(tree.as_ref(), id))
5110                        .and_then(|widget| widget.word_at(cursor))
5111                    {
5112                        self.runtime.edits.insert(
5113                            id,
5114                            Edit {
5115                                cursor: end,
5116                                anchor: Some(start),
5117                                composing: None,
5118                            },
5119                        );
5120                        self.drag = None;
5121                    }
5122                }
5123            }
5124        }
5125
5126        // 2b) Words in a selection area, with nothing else having a claim on the press: a
5127        // selection begins, and may be dragged across the texts of the area. A finger is
5128        // left to scroll, and a finger that stays put selects the word instead (milestone 576).
5129        if self.drag.is_none() && focus.is_none() {
5130            if touch {
5131                self.pending_region_hold = self.region_press_target().is_some();
5132            } else {
5133                self.begin_region_selection();
5134            }
5135        }
5136
5137        // 3) Touch: when nothing captured the gesture — no scrollbar, no widget, no
5138        // text selection — prepare a finger scroll on the area under the finger. A
5139        // release without movement, under TOUCH_SLOP, stays a tap.
5140        if touch && self.drag.is_none() {
5141            // The innermost area that would actually take a finger. An area whose content
5142            // fits refuses the offset outright — the reference takes its drag recognisers
5143            // away for exactly this — so it neither swallows the press nor lights an edge,
5144            // and the page behind it, or a dismissible row under it, gets its turn.
5145            let landed = {
5146                let scroll = &self.runtime.scroll;
5147                self.ui.as_ref().and_then(|ui| {
5148                    ui.scroll_chain(self.cursor).find(|area| {
5149                        area.accepts_user_offset(
5150                            scroll.get(&area.id).copied().unwrap_or((0.0, 0.0)),
5151                        )
5152                    })
5153                })
5154            };
5155            if let Some(area) = landed {
5156                // A finger back on the content catches it: the fling stops where it
5157                // is rather than sliding under the finger, and hands the next
5158                // release whatever momentum the platform lets a swipe build on.
5159                let physics = area.physics;
5160                let carried = self.runtime.catch_scroll_fling(area.id, physics);
5161                // The offset belongs to the finger until it lifts.
5162                self.runtime.hold_scroll(area.id);
5163                // And so does the sheet it sits in, if one is still settling: the finger
5164                // may be about to move it, and a sheet sliding away under a finger that
5165                // has caught its list is a sheet that ignored the catch.
5166                if let Some(sheet) = self
5167                    .ui
5168                    .as_ref()
5169                    .and_then(|ui| ui.sheet_holding(area.id))
5170                    .map(|sheet| sheet.id)
5171                {
5172                    self.runtime.sheet_hold(sheet);
5173                }
5174                self.drag = Some(Drag::Scroll {
5175                    id: area.id,
5176                    last: self.cursor,
5177                    moved: false,
5178                    carried,
5179                    dismiss: self
5180                        .ui
5181                        .as_ref()
5182                        .and_then(|ui| ui.dismissable_at(self.cursor)),
5183                    axis: None,
5184                    pan: self.pan_target(),
5185                });
5186                self.begin_gesture();
5187                // A fling on a scroll area is read the way its scroll behaviour reads one
5188                // (milestone 616), as the reference's scrollable builds its recognizer.
5189                self.gesture_velocity = area.velocity_tracker();
5190            }
5191        }
5192
5193        // 3a) A sheet's panel with nothing under the finger that scrolls: the finger moves
5194        // the sheet itself. A pointer too — on a desktop there is no other way to raise one.
5195        if self.drag.is_none() {
5196            if let Some((id, available)) = self
5197                .ui
5198                .as_ref()
5199                .and_then(|ui| ui.sheet_at(self.cursor))
5200                .map(|sheet| (sheet.id, sheet.available))
5201            {
5202                self.runtime.sheet_hold(id);
5203                self.drag = Some(Drag::Sheet {
5204                    id,
5205                    last: self.cursor,
5206                    moved: false,
5207                    available,
5208                });
5209                self.begin_gesture();
5210            }
5211        }
5212
5213        // 3b) A dismissible item with nothing scrollable under it: there is no gesture to
5214        // arbitrate against, so the swipe is prepared directly. It still waits for the
5215        // threshold, or a tap on the row would start sliding it.
5216        if self.drag.is_none() {
5217            if let Some(item) = self
5218                .ui
5219                .as_ref()
5220                .and_then(|ui| ui.dismissable_at(self.cursor))
5221            {
5222                self.drag = Some(Drag::Dismiss {
5223                    item,
5224                    last: self.cursor,
5225                    moved: false,
5226                });
5227                self.begin_gesture();
5228            }
5229        }
5230
5231        // 3c) A draggable item, when nothing above has taken the gesture. It comes
5232        // **after** the touch scroll on purpose: a widget that took every drag inside a
5233        // list would silently stop that list scrolling, and a list that does not scroll
5234        // is a worse bug than an item that does not lift. With a pointer there is no
5235        // touch scroll to lose to, so it lifts everywhere.
5236        if self.drag.is_none() {
5237            if let Some(source) = self
5238                .ui
5239                .as_ref()
5240                .and_then(|ui| ui.drag_source_at(self.cursor))
5241                .filter(|source| {
5242                    // One that asked for a hold waits for the deadline instead.
5243                    !self
5244                        .tree
5245                        .as_ref()
5246                        .and_then(|tree| find_widget(tree.as_ref(), source.id))
5247                        .is_some_and(|widget| widget.drag_needs_long_press())
5248                })
5249            {
5250                self.drag = Some(Drag::Item {
5251                    source,
5252                    start: self.cursor,
5253                    moved: false,
5254                    over: None,
5255                });
5256                self.begin_gesture();
5257            }
5258        }
5259
5260        // 3d) A detector that takes a drag, with no scroll under it to share the gesture with
5261        // (milestone 582): prepared, and engaged past the slop, so a tap still goes through.
5262        if self.drag.is_none() {
5263            if let Some((id, axis)) = self.pan_target() {
5264                self.drag = Some(Drag::Gesture {
5265                    id,
5266                    axis,
5267                    start: self.cursor,
5268                    last: self.cursor,
5269                    moved: false,
5270                });
5271                self.begin_gesture();
5272            }
5273        }
5274
5275        // 4) An interactive viewport (`InteractiveViewer`) under the pointer: prepare a
5276        // **pan**, with mouse or finger. Like the touch scroll it engages only past the
5277        // threshold, so a tap goes through to the child, a button for instance.
5278        if self.drag.is_none() {
5279            if let Some((id, viewport)) = self
5280                .ui
5281                .as_ref()
5282                .and_then(|ui| ui.interactive_at(self.cursor))
5283            {
5284                // The press stops a fling in progress: we take the content back in hand.
5285                self.runtime.interactive_velocity.remove(&id);
5286                self.drag = Some(Drag::Pan {
5287                    id,
5288                    last: self.cursor,
5289                    moved: false,
5290                    viewport,
5291                });
5292                self.begin_gesture();
5293            }
5294        }
5295        self.request_redraw();
5296    }
5297
5298    /// A pointer release, mouse or finger: it ends a drag, or commits a click or tap
5299    /// when the release lands back on the widget that was pressed.
5300    /// What widget `id` emits for a click at the pointer on one of its sub-regions — see
5301    /// [`Widget::positional_click`]. Local coordinates are the pointer minus the widget's
5302    /// corner.
5303    fn positional_click_on(&self, id: WidgetId) -> Option<A::Message> {
5304        let rect = self.ui.as_ref()?.widget_rect(id)?;
5305        let widget = find_widget(self.tree.as_ref()?.as_ref(), id)?;
5306        widget.positional_click(
5307            self.cursor.x - rect.x,
5308            self.cursor.y - rect.y,
5309            rect.width,
5310            rect.height,
5311        )
5312    }
5313
5314    fn pointer_up(&mut self) {
5315        let ended = self.drag.take();
5316        if let Some(Drag::Back { .. }) = ended {
5317            // The app decides — commit or cancel — from the velocity, which it wants
5318            // in **fractions of the screen** per second, not pixels.
5319            let sign = if self.is_rtl() { -1.0 } else { 1.0 };
5320            let velocity = sign * self.gesture_estimate().velocity.x / self.logical_width();
5321            self.build_dirty = true;
5322            self.app.back_gesture_end(velocity);
5323            self.request_redraw();
5324            return;
5325        }
5326        // A touch scroll, a pan or a swipe that never moved is a plain tap: we let it
5327        // follow the ordinary click path below.
5328        let was_tap = gesture_was_a_tap(ended.as_ref());
5329        // The bracket closes. A press that never moved still gets its end: it changed
5330        // the value once, and a caller that defers its expensive work until the release
5331        // would otherwise never be told the release happened.
5332        if let Some(Drag::Widget {
5333            id,
5334            rect,
5335            press_x,
5336            track,
5337            active: true,
5338            thumb,
5339            ..
5340        }) = ended
5341        {
5342            let rtl = self.is_rtl();
5343            if matches!(track.mode, frus_widgets::DragMode::Thumbs { .. }) {
5344                if let Some(thumb) = thumb {
5345                    let at = track.fraction(rect, self.cursor.x, rtl);
5346                    self.thumb_drag(id, thumb, frus_widgets::ThumbDrag::End, at);
5347                }
5348            } else {
5349                let released = track.released_at(rect, press_x, self.cursor.x, rtl);
5350                self.dispatch_drag_edge(id, Edge::End, released);
5351            }
5352        }
5353        // The keyboard is told where a dragged handle left the selection.
5354        #[cfg(android)]
5355        if let Some(Drag::SelectionHandle { id, .. }) = ended {
5356            self.push_ime_context(id);
5357        }
5358        if let Some(Drag::SelectionHandle { id, .. }) = ended {
5359            self.show_selection_toolbar(id);
5360        }
5361        // A handle of a selection area let go: its bar comes back over what is selected now.
5362        if let Some(Drag::RegionHandle { .. }) = ended {
5363            if let Some(selection) = self.runtime.region.as_mut() {
5364                selection.bar = true;
5365            }
5366        }
5367        // Reordering: on the drop, the target column is the reorderable header under
5368        // the pointer, and we route the grabbed header's `on_reorder(from, to)`.
5369        if let Some(Drag::Reorder {
5370            id,
5371            from,
5372            moved: true,
5373            carried,
5374            ..
5375        }) = &ended
5376        {
5377            let target = self.reorder_drop_target(*id, *from, *carried);
5378            let tree = self.tree.as_ref();
5379            let base = target
5380                .and_then(|tid| tree.and_then(|t| find_widget(t.as_ref(), tid)))
5381                .and_then(|widget| widget.reorder_index());
5382            // The **trailing** half of a hovered target that is inserted at — the lower
5383            // half down a list, the right half across one (the left, right to left) —
5384            // means inserting **after** it, index +1: the effective drop slot follows the
5385            // insertion line that was painted. No effect for `Table` columns, or off target.
5386            let to = match (base, target) {
5387                (Some(base), Some(tid)) => {
5388                    let after = self
5389                        .ui
5390                        .as_ref()
5391                        .and_then(|ui| ui.widget_rect(tid))
5392                        .map(|rect| self.reorder_insert_after(tid, rect))
5393                        .unwrap_or(false);
5394                    Some(base + after as usize)
5395                }
5396                _ => None,
5397            };
5398            // Dropping **onto itself** moves nothing, even in the lower half, where
5399            // `to = from + 1` would slip past the `to != from` guard — otherwise we would
5400            // emit a null move and announce it.
5401            let self_drop = target == Some(*id);
5402            let message = match to {
5403                Some(to) if to != *from && !self_drop => tree
5404                    .and_then(|t| find_widget(t.as_ref(), *id))
5405                    .and_then(|widget| widget.on_reorder(to)),
5406                _ => None,
5407            };
5408            if let Some(message) = message {
5409                let to = to.unwrap_or(*from);
5410                let widget = tree.and_then(|t| find_widget(t.as_ref(), *id));
5411                let axis = widget
5412                    .map(|w| w.reorder_axis())
5413                    .unwrap_or(ReorderAxis::Horizontal);
5414                // The move is spoken to the screen reader — the ghost's counterpart for
5415                // a blind user. A widget whose index **is** a position says so itself; a
5416                // `Kanban` card's is a flat `column × stride + position` that means
5417                // nothing read aloud, so the fallback speaks of the axis and gives no
5418                // number at all.
5419                let announcement = widget
5420                    .and_then(|w| w.reorder_announcement(to))
5421                    .unwrap_or_else(|| match axis {
5422                        ReorderAxis::Horizontal => format!("Column moved to position {}", to + 1),
5423                        ReorderAxis::Vertical => "Card moved".to_string(),
5424                    });
5425                self.dispatch(message);
5426                self.set_announcement(announcement);
5427            }
5428        }
5429        // Fling: the finger's momentum is handed to the area's physics, which
5430        // returns the motion that follows — a spline that stops at the edge, or
5431        // friction that hands over to a spring and bounces.
5432        // A swiped item: the release velocity decides between flying out and sliding
5433        // back, using the same fitted estimate a fling uses.
5434        if let Some(Drag::Dismiss { item, moved, .. }) = &ended {
5435            if *moved {
5436                let estimate = self.gesture_estimate();
5437                let horizontal = item.spec.axis.is_horizontal();
5438                let (along, across) = if horizontal {
5439                    (estimate.velocity.x, estimate.velocity.y)
5440                } else {
5441                    (estimate.velocity.y, estimate.velocity.x)
5442                };
5443                self.runtime.dismiss_release(
5444                    item.id,
5445                    along,
5446                    across,
5447                    item.spec.axis,
5448                    item.spec.threshold,
5449                );
5450                self.request_redraw();
5451                return;
5452            }
5453        }
5454        // A sheet let go of by its panel settles from wherever the finger left it — and
5455        // one only pressed settles too, since the press caught it if it was moving.
5456        if let Some(Drag::Sheet {
5457            id,
5458            moved,
5459            available,
5460            ..
5461        }) = &ended
5462        {
5463            let velocity = if *moved {
5464                -self.fling_velocity(self.gesture_estimate()).1
5465            } else {
5466                0.0
5467            };
5468            // Thrown by its handle, a sheet with one list in it carries that list on at the
5469            // top, as the reference's does: there its whole content is the list.
5470            let sheet = self.ui.as_ref().and_then(|ui| ui.sheet(*id)).map(|sheet| {
5471                let list = match sheet.areas.as_slice() {
5472                    [only] => Some(*only),
5473                    _ => None,
5474                };
5475                (sheet.spec.clone(), list)
5476            });
5477            if let Some((spec, list)) = sheet {
5478                self.runtime
5479                    .sheet_release(*id, &spec, *available, velocity, list);
5480            }
5481            if *moved {
5482                self.request_redraw();
5483                return;
5484            }
5485        }
5486        if let Some(Drag::Item {
5487            source,
5488            moved: true,
5489            over,
5490            ..
5491        }) = &ended
5492        {
5493            // The drop, then the source's own answer. In that order: an application
5494            // that reacts to both should see the thing arrive before it is told the
5495            // journey is over.
5496            self.runtime.drag_over = None;
5497            let accepted = over.is_some();
5498            let dropped = over.and_then(|id| {
5499                self.tree
5500                    .as_ref()
5501                    .and_then(|tree| find_widget(tree.as_ref(), id))
5502                    .and_then(|widget| widget.on_drop(source.payload))
5503            });
5504            let ended_msg = self
5505                .tree
5506                .as_ref()
5507                .and_then(|tree| find_widget(tree.as_ref(), source.id))
5508                .and_then(|widget| widget.on_dropped(accepted));
5509            for message in dropped.into_iter().chain(ended_msg) {
5510                self.dispatch(message);
5511            }
5512            self.request_redraw();
5513            return;
5514        }
5515        if let Some(Drag::Item { .. }) = &ended {
5516            // Lifted but never moved: nothing was dropped, and the click below still
5517            // has its say.
5518            self.runtime.drag_over = None;
5519        }
5520        // Set when a sheet takes the release of a list inside it, which then does not
5521        // fling as well (milestone 515).
5522        let mut sheet_took = false;
5523        if let Some(Drag::Scroll { id, .. }) = &ended {
5524            let sheet = self
5525                .ui
5526                .as_ref()
5527                .and_then(|ui| ui.sheet_holding(*id))
5528                .cloned();
5529            if let Some(sheet) = sheet {
5530                // In the sheet's terms, positive growing it: a list's offset grows the
5531                // same way, on an axis that is not reversed.
5532                let velocity = match &ended {
5533                    Some(Drag::Scroll {
5534                        moved: true,
5535                        axis: Some(true),
5536                        carried,
5537                        ..
5538                    }) => -self.fling_velocity(self.gesture_estimate()).1 + carried.1,
5539                    _ => 0.0,
5540                };
5541                let offset = self.runtime.scroll.get(id).map_or(0.0, |o| o.1);
5542                let size = self.runtime.sheet_size(sheet.id, &sheet.spec);
5543                sheet_took = velocity != 0.0
5544                    && frus_widgets::sheet_takes_release(
5545                        velocity,
5546                        offset,
5547                        size,
5548                        &sheet.spec,
5549                        sheet.available,
5550                    );
5551                // Whoever takes it, the sheet is let go of: settled by the throw, or
5552                // from where it is — which is a stop, whenever the list keeps the throw.
5553                self.runtime.sheet_release(
5554                    sheet.id,
5555                    &sheet.spec,
5556                    sheet.available,
5557                    if sheet_took { velocity } else { 0.0 },
5558                    Some(*id),
5559                );
5560            }
5561        }
5562        if let Some(Drag::Scroll { id, .. }) = &ended {
5563            // The finger gives the offset back before anything is flung at it.
5564            self.runtime.release_scroll(*id);
5565            // A pull held against an edge now fades at the "let go" rate.
5566            self.runtime.glow_scroll_end(*id);
5567            // And a pull-to-refresh gesture is answered: releasing it armed is the
5568            // whole point of the gesture, so the message goes out now rather than
5569            // after the indicator has finished settling.
5570            self.release_refresh(*id);
5571        }
5572        if let Some(Drag::Scroll {
5573            id,
5574            moved: true,
5575            carried,
5576            axis,
5577            ..
5578        }) = &ended
5579        {
5580            // In **scroll space**: the content moves opposite the finger — and along the
5581            // axis the gesture was claimed by, or the release would fling the way the drag
5582            // was not allowed to go.
5583            let estimate = self.gesture_estimate();
5584            let velocity = self.fling_velocity(estimate);
5585            // Through the same conversion the drag went through, so the release carries
5586            // on the way the finger was going — on a reversed axis too.
5587            let velocity = match self.ui.as_ref().and_then(|u| u.scroll_region(*id)) {
5588                Some(area) => area.offset_delta(velocity),
5589                None => (-velocity.0, -velocity.1),
5590            };
5591            // The momentum carried in from an interrupted fling is masked with the rest:
5592            // a gesture held to one axis must not launch along the other on the strength
5593            // of what the *previous* one was doing.
5594            let launch = (velocity.0 + carried.0, velocity.1 + carried.1);
5595            if sheet_took {
5596                self.request_redraw();
5597            } else {
5598                self.fling(
5599                    *id,
5600                    match axis {
5601                        Some(true) => (0.0, launch.1),
5602                        Some(false) => (launch.0, 0.0),
5603                        None => launch,
5604                    },
5605                );
5606            }
5607        }
5608        // A detector's drag let go: its end, with the velocity it was let go at (milestone
5609        // 582). One that never moved was a tap, and goes on to the click below.
5610        if let Some(Drag::Gesture {
5611            id,
5612            axis,
5613            moved: true,
5614            ..
5615        }) = &ended
5616        {
5617            let velocity = self.fling_velocity(self.gesture_estimate());
5618            let velocity = mask_pan(*axis, Point::new(velocity.0, velocity.1));
5619            // No faster than a finger can throw: the reference's `kMaxFlingVelocity`.
5620            let speed = velocity.x.hypot(velocity.y);
5621            let velocity = if speed > MAX_PAN_VELOCITY {
5622                let k = MAX_PAN_VELOCITY / speed;
5623                Point::new(velocity.x * k, velocity.y * k)
5624            } else {
5625                velocity
5626            };
5627            self.send_pan(*id, frus_widgets::PanEvent::End { velocity });
5628        }
5629        // A pan fling: the momentum launches the content, which `advance_interactive`
5630        // decelerates and bounds frame by frame.
5631        if let Some(Drag::Pan {
5632            id, moved: true, ..
5633        }) = &ended
5634        {
5635            let estimate = self.gesture_estimate();
5636            let velocity = self.fling_velocity(estimate);
5637            if velocity.0.hypot(velocity.1) > PAN_FLING_MIN {
5638                self.runtime.interactive_velocity.insert(*id, velocity);
5639            }
5640        }
5641        if ended.is_some() && !was_tap {
5642            self.request_redraw();
5643            return;
5644        }
5645        // A click only counts when press and release land on the same widget.
5646        let released = self.ui.as_ref().and_then(|ui| ui.hit(self.cursor));
5647        // A built-in button of the selection bar sends no message: the shell performs its
5648        // action on the field the bar is open on (milestone 568).
5649        let bar_action = match (self.runtime.input.pressed, released) {
5650            (Some(pressed), Some(released)) if pressed == released => {
5651                self.ui.as_ref().and_then(|ui| ui.edit_action_for(pressed))
5652            }
5653            _ => None,
5654        };
5655        let on_bar = self
5656            .ui
5657            .as_ref()
5658            .is_some_and(|ui| ui.toolbar_contains(self.cursor));
5659        // A field answers no tap of its own, so it is no hit target and `released` never
5660        // names it: its clickable suffix is asked through the focusable the press landed
5661        // on, when the release lands on it too. It wins over a tappable parent, the way
5662        // the inner target always does.
5663        let field_positional = if bar_action.is_some() {
5664            None
5665        } else {
5666            self.ui
5667                .as_ref()
5668                .and_then(|ui| ui.focus_hit(self.cursor))
5669                .map(|(id, _)| id)
5670                .filter(|id| self.runtime.input.focused == Some(*id))
5671                .and_then(|id| self.positional_click_on(id))
5672        };
5673        let (message, announce) = match (self.runtime.input.pressed, released) {
5674            (Some(_), Some(_)) if bar_action.is_some() => (None, None),
5675            _ if field_positional.is_some() => (field_positional, None),
5676            (Some(pressed), Some(released)) if pressed == released => {
5677                // A **positional** click, on a sub-region such as a chart's point, takes
5678                // priority over `on_click`.
5679                let positional = self.positional_click_on(released);
5680                let message =
5681                    positional.or_else(|| self.ui.as_ref().and_then(|ui| ui.msg_for(pressed)));
5682                // The spoken announcement of the effect — a sort, a selection — read off
5683                // the clicked widget before `dispatch` rebuilds the tree.
5684                let announce = self
5685                    .tree
5686                    .as_ref()
5687                    .and_then(|tree| find_widget(tree.as_ref(), pressed))
5688                    .and_then(|widget| widget.announce());
5689                (message, announce)
5690            }
5691            _ => (None, None),
5692        };
5693        // Something that takes a double tap: this tap is its second, or a first that waits.
5694        let message = match (self.runtime.input.pressed, released) {
5695            (Some(pressed), Some(released)) if pressed == released && bar_action.is_none() => {
5696                self.double_tap(pressed, message)
5697            }
5698            _ => message,
5699        };
5700        // The tap completed on the widget it started on: its ink finishes growing and
5701        // fades. Anything else leaves the splash unconfirmed, and `advance_ink` sweeps
5702        // it away quickly — the finger slid off, and the ink says so.
5703        if let (Some(pressed), Some(released)) = (self.runtime.input.pressed, released) {
5704            if pressed == released {
5705                self.runtime.ink_confirm(pressed);
5706            }
5707        }
5708        self.runtime.input.pressed = None;
5709        if let Some(action) = bar_action {
5710            self.perform_edit_action(action);
5711        } else if on_bar && message.is_some() {
5712            // An item of the application's own: it has sent its message, and the bar is
5713            // done with.
5714            self.runtime.selection_handles = None;
5715            self.hide_selection_toolbar();
5716        }
5717        if let Some(message) = message {
5718            self.dispatch(message);
5719            if let Some(announce) = announce {
5720                self.set_announcement(announce);
5721            }
5722        }
5723        self.request_redraw();
5724    }
5725
5726    /// A tap on `id`, which answered `message`. If `id` also takes a double tap, the tap is
5727    /// either the second of one, and the double tap's message is sent in its place, or a
5728    /// first, which waits for [`DOUBLE_TAP_TIMEOUT`] before it is a tap (milestone 581).
5729    fn double_tap(&mut self, id: WidgetId, message: Option<A::Message>) -> Option<A::Message> {
5730        let Some(double) = self
5731            .tree
5732            .as_ref()
5733            .and_then(|tree| find_widget(tree.as_ref(), id))
5734            .and_then(|widget| widget.on_double_tap())
5735        else {
5736            return message;
5737        };
5738        let now = Instant::now();
5739        let second = self.pending_tap.as_ref().is_some_and(|pending| {
5740            let (dx, dy) = (
5741                self.cursor.x - pending.position.x,
5742                self.cursor.y - pending.position.y,
5743            );
5744            pending.id == id
5745                && now.duration_since(pending.at) <= DOUBLE_TAP_TIMEOUT
5746                && dx.hypot(dy) <= DOUBLE_TAP_SLOP
5747        });
5748        if second {
5749            self.pending_tap = None;
5750            return Some(double);
5751        }
5752        self.flush_pending_tap();
5753        self.pending_tap = Some(PendingTap {
5754            id,
5755            at: now,
5756            position: self.cursor,
5757            single: message,
5758        });
5759        None
5760    }
5761
5762    /// Sends the waiting first tap's own message, if one is waiting.
5763    fn flush_pending_tap(&mut self) {
5764        if let Some(message) = self.pending_tap.take().and_then(|pending| pending.single) {
5765            self.dispatch(message);
5766            self.request_redraw();
5767        }
5768    }
5769
5770    /// The waiting first tap, sent if its wait is over by `now`. Whether one was.
5771    fn poll_pending_tap(&mut self, now: Instant) -> bool {
5772        let due = self
5773            .pending_tap
5774            .as_ref()
5775            .is_some_and(|pending| now.duration_since(pending.at) >= DOUBLE_TAP_TIMEOUT);
5776        if due {
5777            self.flush_pending_tap();
5778        }
5779        due
5780    }
5781
5782    /// Speaks a message aloud through the screen reader's **live region**, for a
5783    /// column reorder and the like. With no screen reader running it costs nothing. The
5784    /// text is re-spoken only on a change. Desktop and web; a no-op on Android and iOS,
5785    /// which have no bridge yet.
5786    #[cfg(any(desktop, web))]
5787    fn set_announcement(&mut self, message: String) {
5788        self.announce = message;
5789    }
5790    #[cfg(not(any(desktop, web)))]
5791    fn set_announcement(&mut self, _message: String) {}
5792
5793    /// Returning focus when an overlay closes: if the focused widget has **vanished**
5794    /// from the frame, a menu or modal having closed, focus goes back to the
5795    /// **trigger** — the most recent focusable in the history that is still present.
5796    /// Every transition is recorded along the way: the old focus, if still present,
5797    /// becomes a candidate trigger. The focus ring is drawn on the following frame
5798    /// (`request_redraw` when focus moved).
5799    fn reconcile_focus(&mut self) {
5800        let present: std::collections::HashSet<WidgetId> = match self.ui.as_ref() {
5801            Some(ui) => ui.focusable_ids().collect(),
5802            None => return,
5803        };
5804        // A widget that takes the focus on appearing, and has just appeared, takes it. The
5805        // focus it takes it from is recorded below as any move is, so it goes back there
5806        // when this one goes: a submenu closing hands it back to its row.
5807        let autofocus: Vec<WidgetId> = self
5808            .ui
5809            .as_ref()
5810            .map(|ui| ui.autofocus_ids().collect())
5811            .unwrap_or_default();
5812        let appeared = autofocus
5813            .iter()
5814            .find(|id| !self.autofocused.contains(id))
5815            .copied();
5816        self.autofocused = autofocus.into_iter().collect();
5817        if let Some(id) = appeared {
5818            self.runtime.input.focused = Some(id);
5819        }
5820        let before = self.runtime.input.focused;
5821        let after = resolve_focus(
5822            before,
5823            &present,
5824            &mut self.focus_history,
5825            &mut self.prev_focus,
5826        );
5827        if after != before || appeared.is_some() {
5828            self.runtime.input.focused = after;
5829            self.request_redraw();
5830        }
5831    }
5832
5833    /// Tells the widgets **where the focus went** since they were last told (milestone 660):
5834    /// each one whose subtree it left or entered, through `Widget::on_focus_change`.
5835    fn report_focus(&mut self) {
5836        let now = self.runtime.input.focused;
5837        // Nothing moved, nothing to say — and no walk of the tree, which is the point: the
5838        // answer would be empty anyway.
5839        if now == self.focus_reported {
5840            return;
5841        }
5842        let before = std::mem::replace(&mut self.focus_reported, now);
5843        let messages = self
5844            .tree
5845            .as_ref()
5846            .map(|tree| focus_changes(tree.as_ref(), before, now))
5847            .unwrap_or_default();
5848        for message in messages {
5849            self.dispatch(message);
5850        }
5851    }
5852
5853    /// Applies a message to the application, runs its effects, then re-evaluates the
5854    /// subscriptions, the state having possibly changed which ones should run.
5855    fn dispatch(&mut self, message: A::Message) {
5856        // The app may have changed state, so the `view` must be rebuilt.
5857        self.build_dirty = true;
5858        let command = self.app.update(message);
5859        self.run_command(command);
5860        let asked = self.app.effects();
5861        self.run_command(asked);
5862        #[cfg(web)]
5863        self.sync_address();
5864        #[cfg(any(desktop, android))]
5865        self.report_dev();
5866        self.sync_subscriptions();
5867    }
5868
5869    /// The link the program was started with — on Android the activity's intent, on a desktop an
5870    /// argument — handed to the application as a location. Once, before the first frame.
5871    #[cfg(any(android, desktop))]
5872    fn open_launch_link(&mut self) {
5873        #[cfg(android)]
5874        let link = self
5875            .android_app
5876            .as_ref()
5877            .and_then(crate::android_link::launch_link);
5878        // A link on the command line, or where `frus run` restarted the application from.
5879        #[cfg(desktop)]
5880        let link = crate::link::link_among(std::env::args()).or_else(crate::dev::restore_link);
5881        if let Some(location) = link.as_deref().and_then(crate::link::location_of_link) {
5882            let command = self.app.open_location(&location);
5883            self.run_command(command);
5884        }
5885    }
5886
5887    /// Opens the channel to `frus run`, when it started this application with one.
5888    #[cfg(any(android, desktop))]
5889    fn connect_dev(&mut self) {
5890        #[cfg(desktop)]
5891        {
5892            self.dev = crate::dev::address()
5893                .as_deref()
5894                .and_then(crate::dev::DevChannel::connect);
5895            crate::dev_commands::set_window(self.window.clone());
5896            if let Some(states) = crate::dev::restored_states() {
5897                self.runtime.states.restore_from(&states);
5898            }
5899        }
5900        // On Android the extra only says that `frus run` is listening, and it listens to the log.
5901        #[cfg(android)]
5902        {
5903            let asked = self
5904                .android_app
5905                .as_ref()
5906                .and_then(|app| crate::android_link::launch_extra(app, crate::dev::DEV_EXTRA));
5907            // Started by `frus run --hot`'s engine, which cannot add the extra (milestone 680).
5908            #[cfg(feature = "hot-reload")]
5909            let asked = asked.or_else(|| crate::hot::under_engine().then(String::new));
5910            self.dev = asked.map(|_| crate::dev::DevChannel::through_log());
5911            if self.dev.is_some() {
5912                let storage = self
5913                    .android_app
5914                    .as_ref()
5915                    .and_then(|app| app.internal_data_path());
5916                if let Some(states) = storage.as_deref().and_then(crate::dev::restored_states) {
5917                    self.runtime.states.restore_from(&states);
5918                }
5919                if let Some(dir) = storage {
5920                    crate::dev_commands::poll_commands(dir);
5921                }
5922            }
5923        }
5924    }
5925
5926    /// Tells `frus run` where the application is and where its window sits, when either
5927    /// changed; a channel that can no longer be written to is closed.
5928    #[cfg(any(android, desktop))]
5929    fn report_dev(&mut self) {
5930        let Some(dev) = self.dev.as_mut() else {
5931            return;
5932        };
5933        #[cfg(desktop)]
5934        let window = self.window.as_ref().and_then(|window| {
5935            let at = window.outer_position().ok()?;
5936            let size = window.inner_size();
5937            Some([at.x, at.y, size.width as i32, size.height as i32])
5938        });
5939        #[cfg(android)]
5940        let window = None;
5941        if !dev.report(self.app.location(), window) {
5942            self.dev = None;
5943            return;
5944        }
5945        let states = &self.runtime.states;
5946        dev.save_if_asked(|| states.snapshot());
5947    }
5948
5949    /// The links a second process handed to this one since the last frame.
5950    #[cfg(desktop)]
5951    fn open_links_from_other_instances(&mut self) {
5952        for link in crate::instance::take_links() {
5953            if let Some(location) = crate::link::location_of_link(&link) {
5954                let command = self.app.open_location(&location);
5955                self.run_command(command);
5956                self.build_dirty = true;
5957                self.request_redraw();
5958            }
5959        }
5960    }
5961
5962    /// The address the page was opened at, handed to the application, and the browser's own
5963    /// moves from here on listened to. Once, before the first frame.
5964    #[cfg(web)]
5965    fn open_launch_address(&mut self, window: &Arc<Window>) {
5966        let strategy = self.app.location_strategy();
5967        let base = crate::address::base();
5968        let (location, index) = crate::address::current(strategy, &base);
5969        self.history.opened(location.clone(), index);
5970        crate::address::listen(window.clone(), strategy, base.clone());
5971        self.address = (strategy, base);
5972        if let Some(location) = location {
5973            let command = self.app.open_location(&location);
5974            self.run_command(command);
5975            // Opened deep in the application: the pages beneath get entries of their own.
5976            let (strategy, base) = &self.address;
5977            for (step, index) in self.history.seed(&self.app.location_stack()) {
5978                crate::address::apply(step, *strategy, base, index);
5979            }
5980        }
5981    }
5982
5983    /// What the browser did since the last look is told to the application, and where the
5984    /// application is is shown in the address bar — the agreement of [`crate::history`].
5985    #[cfg(web)]
5986    fn sync_address(&mut self) {
5987        if !self.started {
5988            return;
5989        }
5990        for (location, state) in crate::address::take_popped() {
5991            if self.history.popped(location.clone(), state) {
5992                let command = self.app.open_location(&location);
5993                self.run_command(command);
5994                self.build_dirty = true;
5995                // The move it starts — a page sliding — is ticked by the frames after this
5996                // one, which was decided to be still before the address was read.
5997                self.request_redraw();
5998            }
5999        }
6000        if let Some(location) = self.app.location() {
6001            if let Some(step) = self
6002                .history
6003                .reflect(&location, self.app.location_replaces())
6004            {
6005                let (strategy, base) = &self.address;
6006                crate::address::apply(step, *strategy, base, self.history.index());
6007            }
6008        }
6009    }
6010
6011    /// Runs a command, and whatever message it produces comes back into the loop
6012    /// through the proxy. Focus requests are **queued**, then resolved against the
6013    /// freshly built tree on the next frame.
6014    ///
6015    /// Where each kind of effect goes:
6016    ///
6017    /// | | native | Web |
6018    /// |---|---|---|
6019    /// | synchronous task | its own thread | a `spawn_local` microtask |
6020    /// | asynchronous task | the shell's executor | the browser |
6021    /// | timer | a task on the executor's reactor | `setTimeout` |
6022    ///
6023    /// A **synchronous** task keeps its thread: it blocks by definition, and that is
6024    /// what a thread is for. An **asynchronous** one no longer gets one — it is a
6025    /// task among others on a pool of four, which is the whole point of milestone
6026    /// 303. See [`crate::runtime`].
6027    fn run_command(&mut self, command: crate::command::Command<A::Message>) {
6028        let parts = command.into_parts();
6029        self.pending_focus.extend(parts.focus);
6030        self.pending_scroll.extend(parts.scrolls);
6031        self.pending_sheet.extend(parts.sheets);
6032        for task in parts.tasks {
6033            let proxy = self.proxy.clone();
6034            #[cfg(not(web))]
6035            std::thread::spawn(move || {
6036                if let Some(message) = task() {
6037                    let _ = proxy.send_event(message);
6038                }
6039            });
6040            #[cfg(web)]
6041            wasm_bindgen_futures::spawn_local(async move {
6042                if let Some(message) = task() {
6043                    let _ = proxy.send_event(message);
6044                }
6045            });
6046        }
6047        for future in parts.async_tasks {
6048            let proxy = self.proxy.clone();
6049            // Native: a task on the shared executor. `detach` because an effect must
6050            // outlive the handle — nothing here wants to cancel it, unlike a
6051            // subscription.
6052            #[cfg(not(web))]
6053            crate::runtime::spawn(async move {
6054                if let Some(message) = future.await {
6055                    let _ = proxy.send_event(message);
6056                }
6057            })
6058            .detach();
6059            // Web: the browser drives the future, single-threaded — `fetch` and friends
6060            // genuinely `await`, without blocking the loop.
6061            #[cfg(web)]
6062            wasm_bindgen_futures::spawn_local(async move {
6063                if let Some(message) = future.await {
6064                    let _ = proxy.send_event(message);
6065                }
6066            });
6067        }
6068        for (delay, message) in parts.timers {
6069            let proxy = self.proxy.clone();
6070            #[cfg(not(web))]
6071            crate::runtime::spawn(async move {
6072                crate::runtime::sleep(delay).await;
6073                let _ = proxy.send_event(message);
6074            })
6075            .detach();
6076            #[cfg(web)]
6077            web_timer::after(delay.as_millis() as i32, move || {
6078                let _ = proxy.send_event(message);
6079            });
6080        }
6081    }
6082
6083    /// Diffs the subscriptions the app declares against those running: starts the new
6084    /// ones and stops those that vanished, by dropping their `Sender`.
6085    fn sync_subscriptions(&mut self) {
6086        let entries = self.app.subscription().into_entries();
6087        let declared: std::collections::HashSet<u64> = entries.iter().map(|e| e.id).collect();
6088
6089        // Stop the subscriptions that are no longer declared.
6090        self.running_subs.retain(|id, _| declared.contains(id));
6091
6092        // Start the new ones.
6093        for entry in entries {
6094            if self.running_subs.contains_key(&entry.id) {
6095                continue;
6096            }
6097            let handle = self.start_subscription(entry.kind);
6098            self.running_subs.insert(entry.id, handle);
6099        }
6100        // The tray is declared from the state too, and follows it at the same moment.
6101        #[cfg(all(desktop, feature = "tray"))]
6102        self.sync_tray();
6103        // And the taskbar button (milestone 708) — only a real window has one.
6104        #[cfg(all(desktop, feature = "taskbar"))]
6105        if let Some(window) = &self.window {
6106            let wanted = self.app.taskbar();
6107            self.taskbar = crate::taskbar::native::apply(window, &self.taskbar, &wanted);
6108        }
6109        // And the global shortcuts (milestone 709).
6110        #[cfg(all(desktop, feature = "global-shortcuts"))]
6111        self.sync_shortcuts();
6112    }
6113
6114    /// The canonical spellings of the global shortcuts the application declares; a refused one
6115    /// is told once (milestone 709).
6116    #[cfg(all(desktop, feature = "global-shortcuts"))]
6117    fn declared_shortcuts(&mut self) -> Vec<(String, crate::GlobalShortcut<A::Message>)> {
6118        let mut declared = Vec::new();
6119        for shortcut in self.app.global_shortcuts() {
6120            match crate::shortcuts::canonical(&shortcut.keys, cfg!(target_os = "macos")) {
6121                Ok(keys) => declared.push((keys, shortcut)),
6122                Err(why) => {
6123                    if self.shortcuts_refused.insert(shortcut.keys.clone()) {
6124                        log::warn!("global shortcuts: refused {why}");
6125                    }
6126                }
6127            }
6128        }
6129        declared
6130    }
6131
6132    /// Makes the system hold the global shortcuts the application declares (milestone 709) —
6133    /// only with a real window, as the tray.
6134    #[cfg(all(desktop, feature = "global-shortcuts"))]
6135    fn sync_shortcuts(&mut self) {
6136        let Some(window) = self.window.clone() else {
6137            return;
6138        };
6139        let wanted: Vec<String> = self
6140            .declared_shortcuts()
6141            .into_iter()
6142            .map(|(keys, _)| keys)
6143            .collect();
6144        crate::shortcuts::native::sync(&mut self.shortcuts, &wanted, &window);
6145    }
6146
6147    /// Acts on the global shortcuts pressed since the last look (milestone 709): the
6148    /// declaration is asked for again, and the shortcut with those keys does what it says now.
6149    #[cfg(all(desktop, feature = "global-shortcuts"))]
6150    fn shortcut_presses(&mut self) {
6151        for keys in crate::shortcuts::native::take_pressed(&self.shortcuts) {
6152            let Some((_, shortcut)) = self
6153                .declared_shortcuts()
6154                .into_iter()
6155                .find(|(declared, _)| *declared == keys)
6156            else {
6157                continue;
6158            };
6159            match shortcut.action {
6160                crate::shortcuts::ShortcutAction::Message(message) => {
6161                    self.dispatch(message);
6162                    self.request_redraw();
6163                }
6164                crate::shortcuts::ShortcutAction::ShowWindow => {
6165                    if let Some(window) = &self.window {
6166                        window.set_visible(true);
6167                        window.set_minimized(false);
6168                        window.focus_window();
6169                        window.request_redraw();
6170                    }
6171                }
6172            }
6173        }
6174    }
6175
6176    /// Makes the system's tray follow what the application declares (milestone 706).
6177    #[cfg(all(desktop, feature = "tray"))]
6178    fn sync_tray(&mut self) {
6179        match self.app.tray() {
6180            None => {
6181                self.tray = None;
6182                self.tray_keeps_running = false;
6183            }
6184            // The system's tray belongs to a real window, on the thread it was made on: the
6185            // windowless driver tests run through has neither, and on macOS the menus may
6186            // only be made on the main thread, which a test's is not.
6187            Some(_) if self.window.is_none() => {}
6188            Some(tray) => {
6189                self.tray_keeps_running = tray.keep_running;
6190                let shape = tray.shape(&self.app.icon());
6191                crate::tray::native::sync(&mut self.tray, shape, self.window.as_ref());
6192            }
6193        }
6194    }
6195
6196    /// Acts on what the person did with the tray since the last look (milestone 706): the
6197    /// description is asked for again, and the item at that place does what it says now.
6198    #[cfg(all(desktop, feature = "tray"))]
6199    fn tray_events(&mut self, event_loop: &ActiveEventLoop) {
6200        for event in crate::tray::native::take_events() {
6201            let Some(action) = self.app.tray().and_then(|tray| tray.action(event)) else {
6202                continue;
6203            };
6204            match action {
6205                crate::tray::TrayAction::Message(message) => {
6206                    self.dispatch(message);
6207                    self.request_redraw();
6208                }
6209                crate::tray::TrayAction::ShowWindow => {
6210                    if let Some(window) = &self.window {
6211                        window.set_visible(true);
6212                        window.set_minimized(false);
6213                        window.focus_window();
6214                        window.request_redraw();
6215                    }
6216                }
6217                crate::tray::TrayAction::Quit => event_loop.exit(),
6218            }
6219        }
6220    }
6221
6222    /// Starts a subscription as a task on the shell's executor, and returns its
6223    /// cancellation handle — **dropping the handle cancels the task**, which is what
6224    /// `sync_subscriptions` above relies on when the application stops declaring it.
6225    ///
6226    /// This used to be a thread per subscription, parked in `recv_timeout` and woken
6227    /// by the timeout or by its `Sender` being dropped. A timer is the clearest case
6228    /// there is of work that only ever waits, so it is the clearest case for not
6229    /// spending a thread on it.
6230    #[cfg(not(web))]
6231    fn start_subscription(&self, kind: crate::subscription::Kind<A::Message>) -> SubHandle {
6232        let proxy = self.proxy.clone();
6233        match kind {
6234            crate::subscription::Kind::Every { interval, make } => {
6235                crate::runtime::spawn(async move {
6236                    loop {
6237                        crate::runtime::sleep(interval).await;
6238                        // The loop having closed is the other way this ends, and the
6239                        // application is gone by then, so there is nobody to tell.
6240                        if proxy.send_event(make(Instant::now())).is_err() {
6241                            break;
6242                        }
6243                    }
6244                })
6245            }
6246        }
6247    }
6248
6249    /// Starts a subscription on the **Web**: a browser `setInterval`, with no thread.
6250    /// Returns its cancellation handle, which calls `clearInterval` on drop. The proxy
6251    /// feeds the message back into the loop on every tick.
6252    #[cfg(web)]
6253    fn start_subscription(&self, kind: crate::subscription::Kind<A::Message>) -> SubHandle {
6254        let proxy = self.proxy.clone();
6255        match kind {
6256            crate::subscription::Kind::Every { interval, make } => {
6257                let ms = interval.as_millis() as i32;
6258                web_timer::Interval::new(ms, move || {
6259                    let _ = proxy.send_event(make(Instant::now()));
6260                })
6261            }
6262        }
6263    }
6264
6265    /// Applies the mouse drag under way.
6266    fn handle_drag(&mut self) {
6267        let Some(mut drag) = self.drag.take() else {
6268            return;
6269        };
6270        // How far this pointer has to travel before a press counts as a drag.
6271        let slop = self.hit_slop();
6272        match &mut drag {
6273            Drag::Scrollbar {
6274                id,
6275                vertical,
6276                grab,
6277                track_start,
6278                track_len,
6279                thumb_len,
6280                max,
6281                reverse,
6282            } => {
6283                let along = if *vertical {
6284                    self.cursor.y
6285                } else {
6286                    self.cursor.x
6287                };
6288                let travel = (*track_len - *thumb_len).max(1.0);
6289                let thumb_start = (along - *grab).clamp(*track_start, *track_start + travel);
6290                let fraction = (thumb_start - *track_start) / travel;
6291                // A reversed axis numbers its offsets from the far end, so the thumb's
6292                // position along the track reads backwards.
6293                let fraction = if *reverse { 1.0 - fraction } else { fraction };
6294                let offset = (fraction * *max).clamp(0.0, *max);
6295                let entry = self.runtime.scroll.entry(*id).or_insert((0.0, 0.0));
6296                if *vertical {
6297                    entry.1 = offset;
6298                } else {
6299                    entry.0 = offset;
6300                }
6301                // A precise drag: the target follows the offset and inertia is cut off.
6302                let synced = *entry;
6303                self.runtime.scroll_target.insert(*id, synced);
6304                self.runtime.scroll_velocity.remove(&*id);
6305            }
6306            Drag::RegionSelect { area } => {
6307                let area = *area;
6308                self.extend_region_selection(area);
6309            }
6310            Drag::RegionHandle { area, handle, grab } => {
6311                let (area, handle, grab) = (*area, *handle, *grab);
6312                self.drag_region_handle(area, handle, grab);
6313            }
6314            Drag::TextSelect { id, rect } => {
6315                let local_x = self.cursor.x - rect.x;
6316                let local_y = self.cursor.y - rect.y
6317                    + self.runtime.scroll.get(id).map(|s| s.1).unwrap_or(0.0);
6318                // The field is focused during the drag, so scroll from the current caret.
6319                let scroll_cursor = self.runtime.edits.get(id).map(|e| e.cursor).unwrap_or(0);
6320                let cursor = self
6321                    .tree
6322                    .as_ref()
6323                    .and_then(|tree| find_widget(tree.as_ref(), *id))
6324                    .and_then(|widget| {
6325                        widget.cursor_at(local_x, local_y, rect.width, scroll_cursor)
6326                    });
6327                if let Some(cursor) = cursor {
6328                    let edit = self.runtime.edits.entry(*id).or_default();
6329                    if edit.anchor.is_none() {
6330                        edit.anchor = Some(edit.cursor);
6331                    }
6332                    edit.cursor = cursor;
6333                }
6334            }
6335            Drag::SelectionHandle {
6336                id,
6337                rect,
6338                handle,
6339                grab,
6340            } => {
6341                // The text position the handle stands for, carried with the finger, and
6342                // the retained vertical scroll folded in as for a press.
6343                let local_x = self.cursor.x - rect.x + grab.x;
6344                let local_y = self.cursor.y - rect.y
6345                    + grab.y
6346                    + self.runtime.scroll.get(id).map(|s| s.1).unwrap_or(0.0);
6347                let edit = self.runtime.edits.get(id).copied().unwrap_or_default();
6348                let moved = self
6349                    .tree
6350                    .as_ref()
6351                    .and_then(|tree| find_widget(tree.as_ref(), *id))
6352                    .and_then(|widget| widget.cursor_at(local_x, local_y, rect.width, edit.cursor))
6353                    .and_then(|to| crate::selection::drag(edit, *handle, to));
6354                if let Some(moved) = moved {
6355                    self.runtime.edits.insert(*id, moved);
6356                }
6357            }
6358            Drag::Widget {
6359                id,
6360                rect,
6361                last_x,
6362                press_x,
6363                track,
6364                active,
6365                thumb,
6366            } => {
6367                let dx = self.cursor.x - *last_x;
6368                *last_x = self.cursor.x;
6369                if matches!(track.mode, frus_widgets::DragMode::Thumbs { .. }) {
6370                    let rtl = self.is_rtl();
6371                    let at = track.fraction(*rect, self.cursor.x, rtl);
6372                    match *thumb {
6373                        Some(picked) => {
6374                            self.thumb_drag(*id, picked, frus_widgets::ThumbDrag::Move, at);
6375                        }
6376                        // A press on both thumbs: the first movement says which.
6377                        None => {
6378                            let moved = self.cursor.x - *press_x;
6379                            if let Some(picked) = track.pick_thumb(*rect, *press_x, moved, rtl) {
6380                                *thumb = Some(picked);
6381                                self.thumb_drag(*id, picked, frus_widgets::ThumbDrag::Start, at);
6382                                self.thumb_drag(*id, picked, frus_widgets::ThumbDrag::Move, at);
6383                            }
6384                        }
6385                    }
6386                } else if *active {
6387                    let value = track.value_at(*rect, *press_x, self.cursor.x, true, self.is_rtl());
6388                    self.apply_widget_drag(*id, dx, value);
6389                }
6390            }
6391            Drag::Reorder { start, moved, .. } => {
6392                // Past the threshold this is a real drag, and no longer a sort.
6393                let dx = self.cursor.x - start.x;
6394                let dy = self.cursor.y - start.y;
6395                if !*moved && (dx * dx + dy * dy) > slop * slop {
6396                    *moved = true;
6397                }
6398                // The ghost card follows the pointer, so redraw on every move.
6399                if *moved {
6400                    self.request_redraw();
6401                }
6402            }
6403            Drag::Gesture {
6404                id,
6405                axis,
6406                start,
6407                last,
6408                moved,
6409            } => {
6410                let (id, axis) = (*id, *axis);
6411                if !*moved {
6412                    // Past the slop it is a drag and no longer a tap. (The reference's pan
6413                    // waits for twice this, but its tap has given up at once: a movement in
6414                    // between is neither. One threshold keeps a press either a tap or a drag.)
6415                    let (tx, ty) = (self.cursor.x - start.x, self.cursor.y - start.y);
6416                    let travelled = match axis {
6417                        frus_widgets::PanAxis::Free => tx.hypot(ty),
6418                        frus_widgets::PanAxis::Horizontal => tx.abs(),
6419                        frus_widgets::PanAxis::Vertical => ty.abs(),
6420                    };
6421                    if travelled > slop {
6422                        *moved = true;
6423                        let local = self.pan_local(id, *start);
6424                        self.send_pan(id, frus_widgets::PanEvent::Start { local });
6425                    }
6426                }
6427                if *moved {
6428                    let delta = mask_pan(
6429                        axis,
6430                        Point::new(self.cursor.x - last.x, self.cursor.y - last.y),
6431                    );
6432                    *last = self.cursor;
6433                    self.track_gesture();
6434                    if delta.x != 0.0 || delta.y != 0.0 {
6435                        let local = self.pan_local(id, self.cursor);
6436                        self.send_pan(id, frus_widgets::PanEvent::Update { local, delta });
6437                    }
6438                }
6439            }
6440            Drag::Pan {
6441                id,
6442                last,
6443                moved,
6444                viewport,
6445            } => {
6446                let dx = self.cursor.x - last.x;
6447                let dy = self.cursor.y - last.y;
6448                if !*moved && (dx * dx + dy * dy) > slop * slop {
6449                    *moved = true;
6450                }
6451                if *moved {
6452                    let view = self.runtime.interactive.entry(*id).or_default();
6453                    // The finger pushes the content, bounded to the frame.
6454                    *view = view.pan(dx, dy).clamped(*viewport);
6455                    *last = self.cursor;
6456                    self.track_gesture();
6457                }
6458            }
6459            Drag::Item {
6460                source,
6461                start,
6462                moved,
6463                over,
6464            } => {
6465                let dx = self.cursor.x - start.x;
6466                let dy = self.cursor.y - start.y;
6467                // Under the threshold nothing is lifted: the press may still be a click
6468                // on whatever the draggable wraps.
6469                if !*moved && (dx * dx + dy * dy) > slop * slop {
6470                    *moved = true;
6471                }
6472                if !*moved {
6473                    return;
6474                }
6475                // The target under the pointer, but only if it would take this payload:
6476                // a target that refuses is not highlighted, so the answer is visible
6477                // before the finger lifts rather than after.
6478                let payload = source.payload;
6479                let candidate = self
6480                    .ui
6481                    .as_ref()
6482                    .and_then(|ui| ui.drop_zone_at(self.cursor))
6483                    .filter(|zone| {
6484                        self.tree
6485                            .as_ref()
6486                            .and_then(|tree| find_widget(tree.as_ref(), zone.id))
6487                            .is_some_and(|widget| widget.accepts_drag(payload))
6488                    })
6489                    .map(|zone| zone.id);
6490                if *over != candidate {
6491                    *over = candidate;
6492                    self.runtime.drag_over = candidate;
6493                }
6494                self.request_redraw();
6495            }
6496            Drag::Scroll {
6497                id,
6498                last,
6499                moved,
6500                carried,
6501                dismiss,
6502                axis,
6503                pan,
6504            } => {
6505                let dx = self.cursor.x - last.x;
6506                let dy = self.cursor.y - last.y;
6507                // Under the threshold we do not scroll yet; the gesture may be a tap.
6508                if !*moved && (dx * dx + dy * dy) > slop * slop {
6509                    *moved = true;
6510                    // The moment of arbitration. A scroll and a swipe start identically,
6511                    // so the question is not who is on top but **which way the finger
6512                    // went**: along the item's swipe axis it is a dismissal, across it a
6513                    // scroll. Deciding once, here, is what keeps the loser out of the
6514                    // gesture entirely — a swipe that also scrolled the list would be
6515                    // worse than either.
6516                    // A detector's drag, by the same rule: one kept to an axis takes a movement
6517                    // along it; a free one takes a movement no scroll under it runs along —
6518                    // the reference's pan, which loses a vertical movement to a vertical scroll
6519                    // because its slop is twice the scroll's (milestone 582).
6520                    if let Some((target, pan_axis)) = pan.take() {
6521                        let down = dy.abs() >= dx.abs();
6522                        let wins = match pan_axis {
6523                            frus_widgets::PanAxis::Horizontal => !down,
6524                            frus_widgets::PanAxis::Vertical => down,
6525                            frus_widgets::PanAxis::Free => {
6526                                let chain: Vec<_> = self
6527                                    .ui
6528                                    .as_ref()
6529                                    .map(|ui| ui.scroll_chain(*last).collect())
6530                                    .unwrap_or_default();
6531                                // Nothing under the finger scrolls that way (`claim_area`
6532                                // without its fallback to the area pressed on).
6533                                !chain.iter().any(|a| a.takes_user_scroll(down))
6534                            }
6535                        };
6536                        if wins {
6537                            // The scroll gives its offset back untouched: it never moved.
6538                            self.runtime.release_scroll(*id);
6539                            let start = *last;
6540                            self.drag = Some(Drag::Gesture {
6541                                id: target,
6542                                axis: pan_axis,
6543                                start,
6544                                last: start,
6545                                moved: false,
6546                            });
6547                            self.handle_drag();
6548                            return;
6549                        }
6550                    }
6551                    if let Some(item) = dismiss.take() {
6552                        let along = if item.spec.axis.is_horizontal() {
6553                            dx.abs() > dy.abs()
6554                        } else {
6555                            dy.abs() > dx.abs()
6556                        };
6557                        if along {
6558                            // The list gives the offset back untouched: it never moved.
6559                            self.runtime.release_scroll(*id);
6560                            self.drag = Some(Drag::Dismiss {
6561                                item,
6562                                last: *last,
6563                                moved: true,
6564                            });
6565                            self.handle_drag();
6566                            return;
6567                        }
6568                    }
6569                    // And the same arbitration between the two **axes** — which settles,
6570                    // with it, *which* of the scrollables under the finger the gesture
6571                    // belongs to. The reference does both at once in its arena: every
6572                    // scrollable in the stack enters a recogniser for its own axis, the
6573                    // one the finger matches wins, and the losers are out of the gesture
6574                    // entirely. Without the first half a page scrolling down drifts
6575                    // sideways the whole way, since no finger travels in a straight line;
6576                    // without the second, a strip of chips that only slides across stops
6577                    // the page behind it from scrolling at all.
6578                    let chain: Vec<_> = self
6579                        .ui
6580                        .as_ref()
6581                        .map(|ui| ui.scroll_chain(*last).collect())
6582                        .unwrap_or_default();
6583                    let down = dy.abs() >= dx.abs();
6584                    if let Some(area) = claim_area(&chain, *id, down) {
6585                        if area.id != *id {
6586                            // The area under the finger lost: it gives its offset back
6587                            // untouched, since it never moved. The winner is caught and
6588                            // held exactly as the press would have done to it.
6589                            self.runtime.release_scroll(*id);
6590                            let physics = area.physics;
6591                            *carried = self.runtime.catch_scroll_fling(area.id, physics);
6592                            self.runtime.hold_scroll(area.id);
6593                            *id = area.id;
6594                        }
6595                        *axis = Some(claim_axis(
6596                            dx,
6597                            dy,
6598                            area.takes_user_scroll(false),
6599                            area.takes_user_scroll(true),
6600                        ));
6601                    } else {
6602                        *axis = Some(down);
6603                    }
6604                }
6605                // Only the claimed axis moves. The other delta is not held back for later:
6606                // it is not part of this gesture at all.
6607                let (dx, mut dy) = match *axis {
6608                    Some(true) => (0.0, dy),
6609                    Some(false) => (dx, 0.0),
6610                    None => (dx, dy),
6611                };
6612                if *moved && *axis == Some(true) {
6613                    // A list inside a sheet shares the finger with it (milestone 515).
6614                    // Every movement is split — the sheet first going up, the list
6615                    // first going down — so the gesture changes hands in the middle of a
6616                    // drag, and what the list is left with goes through its physics below
6617                    // exactly as a movement of its own would.
6618                    let sheet = self
6619                        .ui
6620                        .as_ref()
6621                        .filter(|ui| ui.scroll_region(*id).is_some_and(|a| !a.reverse_y))
6622                        .and_then(|ui| ui.sheet_holding(*id))
6623                        .filter(|sheet| sheet.available > 0.0)
6624                        .cloned();
6625                    if let Some(sheet) = sheet {
6626                        let px = sheet.available;
6627                        let offset = self.runtime.scroll.get(id).map_or(0.0, |o| o.1);
6628                        let size = self.runtime.sheet_size(sheet.id, &sheet.spec) * px;
6629                        let (grown, listed) = frus_widgets::split_sheet_drag(
6630                            -dy,
6631                            offset,
6632                            size,
6633                            sheet.spec.floor() * px,
6634                            sheet.spec.max * px,
6635                        );
6636                        if grown != 0.0 {
6637                            self.runtime
6638                                .sheet_drag(sheet.id, &sheet.spec, grown / px, px);
6639                        }
6640                        dy = -listed;
6641                    }
6642                }
6643                if *moved {
6644                    let area = self.ui.as_ref().and_then(|u| u.scroll_region(*id));
6645                    // A region gone from the frame moves as the application's behaviour says.
6646                    let physics = area.map(|a| a.physics).unwrap_or_else(|| {
6647                        self.app
6648                            .scroll_behavior()
6649                            .scroll_physics(&self.themes.displayed(&self.app))
6650                    });
6651                    let cur = self.runtime.scroll.get(id).copied().unwrap_or((0.0, 0.0));
6652                    // The finger pushes the content, so we follow the delta at once —
6653                    // but only as far as the physics allows. Past an edge, bouncing
6654                    // physics resists more and more (the rubber band) while clamping
6655                    // physics refuses the move outright.
6656                    // What the physics refuses is exactly the distance the user asked
6657                    // for and did not get — which is what the glow acknowledges.
6658                    let axis = |metrics: frus_widgets::ScrollMetrics, delta: f32| {
6659                        let applied = physics.apply_user_offset(metrics, delta);
6660                        let proposed = metrics.pixels + applied;
6661                        let refused = physics.apply_boundary_conditions(metrics, proposed);
6662                        (proposed - refused, refused)
6663                    };
6664                    let (nx, ny, refused) = match area {
6665                        Some(area) => {
6666                            let (ddx, ddy) = area.offset_delta((dx, dy));
6667                            let (nx, rx) = axis(area.metrics_x(cur.0), ddx);
6668                            let (ny, ry) = axis(area.metrics_y(cur.1), ddy);
6669                            (nx, ny, Some((area, rx, ry)))
6670                        }
6671                        None => (cur.0 - dx, cur.1 - dy, None),
6672                    };
6673                    if let Some((area, rx, ry)) = refused {
6674                        // A pull-to-refresh area listening above this scrollable takes
6675                        // the **top** edge; the glow takes the other three. Both answer
6676                        // "there is nothing more that way", and giving both would say it
6677                        // twice — the indicator being the more useful of the two, since
6678                        // it leads somewhere.
6679                        if let Some(host) = area.refresh {
6680                            self.feed_refresh(host, physics, area.viewport.height, cur.1, ny, ry);
6681                        }
6682                        let cursor = self.cursor;
6683                        for (refused, vertical, extent) in [
6684                            (rx, false, area.viewport.width),
6685                            (ry, true, area.viewport.height),
6686                        ] {
6687                            if refused.abs() < 1e-3 {
6688                                continue;
6689                            }
6690                            let edge = area.refused_edge(vertical, refused);
6691                            // The refresh area takes the edge the axis **starts** at,
6692                            // which is the bottom of a reversed one.
6693                            if area.refresh.is_some() && edge == area.start_edge(vertical) {
6694                                continue;
6695                            }
6696                            let (offset, cross) =
6697                                frus_widgets::glow_cross_axis(area.viewport, edge, cursor);
6698                            self.runtime.glow_pull(
6699                                area.id,
6700                                area.overscroll,
6701                                edge,
6702                                refused,
6703                                extent,
6704                                offset,
6705                                cross,
6706                            );
6707                        }
6708                        // Content that moves again lets its stretch go, as the
6709                        // reference's does on any scroll that is not an overscroll.
6710                        if rx.abs() < 1e-3 && ry.abs() < 1e-3 && (nx, ny) != cur {
6711                            self.runtime.stretch_release(area.id);
6712                        }
6713                    }
6714                    self.runtime.scroll.insert(*id, (nx, ny));
6715                    self.runtime.scroll_target.insert(*id, (nx, ny));
6716                    self.runtime.scroll_velocity.remove(id);
6717                    *last = self.cursor;
6718                    self.track_gesture();
6719                }
6720            }
6721            Drag::Dismiss { item, last, moved } => {
6722                let dx = self.cursor.x - last.x;
6723                let dy = self.cursor.y - last.y;
6724                if !*moved && (dx * dx + dy * dy) > slop * slop {
6725                    *moved = true;
6726                }
6727                if *moved {
6728                    let delta = if item.spec.axis.is_horizontal() {
6729                        dx
6730                    } else {
6731                        dy
6732                    };
6733                    self.runtime
6734                        .dismiss_drag(item.id, delta, item.extent(), item.spec.axis);
6735                    *last = self.cursor;
6736                    self.track_gesture();
6737                }
6738            }
6739            Drag::Sheet {
6740                id,
6741                last,
6742                moved,
6743                available,
6744            } => {
6745                let dx = self.cursor.x - last.x;
6746                let dy = self.cursor.y - last.y;
6747                if !*moved && (dx * dx + dy * dy) > slop * slop {
6748                    *moved = true;
6749                }
6750                if *moved && *available > 0.0 {
6751                    let spec = self
6752                        .ui
6753                        .as_ref()
6754                        .and_then(|ui| ui.sheet(*id))
6755                        .map(|sheet| sheet.spec.clone());
6756                    if let Some(spec) = spec {
6757                        // Up grows it: the screen's y runs the other way.
6758                        self.runtime
6759                            .sheet_drag(*id, &spec, -dy / *available, *available);
6760                    }
6761                    *last = self.cursor;
6762                    self.track_gesture();
6763                }
6764            }
6765            Drag::Back { start_x } => {
6766                let width = self.logical_width();
6767                // Under RTL the finger slides **left** from the right edge, so progress
6768                // runs the other way.
6769                let sign = if self.is_rtl() { -1.0 } else { 1.0 };
6770                let progress = (sign * (self.cursor.x - *start_x) / width).clamp(0.0, 1.0);
6771                self.track_gesture();
6772                self.build_dirty = true;
6773                self.app.back_gesture(progress);
6774            }
6775        }
6776        self.drag = Some(drag);
6777        self.request_redraw();
6778    }
6779
6780    /// The refresh area a scrollable sits inside, if any.
6781    fn refresh_host_of(&self, scrollable: WidgetId) -> Option<WidgetId> {
6782        self.ui
6783            .as_ref()
6784            .and_then(|ui| ui.scroll_region(scrollable))
6785            .and_then(|area| area.refresh)
6786    }
6787
6788    /// Ends the pull of whichever refresh area holds `scrollable`, and dispatches its
6789    /// message when the pull was armed.
6790    fn release_refresh(&mut self, scrollable: WidgetId) {
6791        let Some(host) = self.refresh_host_of(scrollable) else {
6792            return;
6793        };
6794        if !self.runtime.refresh_release(host) {
6795            return;
6796        }
6797        let message = self
6798            .tree
6799            .as_ref()
6800            .and_then(|tree| find_widget(tree.as_ref(), host))
6801            .and_then(|widget| widget.on_refresh());
6802        if let Some(message) = message {
6803            self.dispatch(message);
6804        }
6805    }
6806
6807    /// Feeds one move of a drag into the pull of the refresh area `host`.
6808    ///
6809    /// The two physics put the overscroll in **different places**, so the signal is
6810    /// read differently:
6811    ///
6812    /// - **Clamping** refuses the movement and pins the offset at the edge, so the
6813    ///   refused amount is the only trace the gesture leaves. It is incremental, and
6814    ///   the physics returns nothing at all for a move back towards the content — so
6815    ///   an eased-off pull holds rather than retracting, which is right: the finger has
6816    ///   not let go.
6817    /// - **Bouncing** lets the offset go past the edge, so the *depth* it reached is
6818    ///   the signal, and the change in that depth is signed. The indicator therefore
6819    ///   follows the rubber band back in as the finger returns, which is also right:
6820    ///   there, the content itself is already saying so.
6821    ///
6822    /// Leaving the top edge at all ends the pull. The gesture has become an ordinary
6823    /// scroll, and an indicator still hanging there would be promising something the
6824    /// release is no longer going to deliver.
6825    fn feed_refresh(
6826        &mut self,
6827        host: WidgetId,
6828        physics: frus_widgets::ScrollPhysics,
6829        extent: f32,
6830        before: f32,
6831        after: f32,
6832        refused_y: f32,
6833    ) {
6834        if after > 0.5 {
6835            self.runtime.refresh_cancel(host);
6836            return;
6837        }
6838        let pulled = if physics.allows_overscroll() {
6839            (-after).max(0.0) - (-before).max(0.0)
6840        } else {
6841            -refused_y
6842        };
6843        if pulled.abs() > 1e-3 {
6844            self.runtime.refresh_pull(host, pulled, extent);
6845        }
6846    }
6847
6848    /// How far the pointer must travel before a press becomes a drag, given what
6849    /// kind of pointer it is.
6850    fn hit_slop(&self) -> f32 {
6851        if self.pointer_touch {
6852            TOUCH_SLOP
6853        } else {
6854            PRECISE_SLOP
6855        }
6856    }
6857
6858    /// Seconds since the drag under way began — the clock the velocity tracker's
6859    /// samples are stamped with.
6860    fn gesture_now(&self) -> f32 {
6861        match self.gesture_clock {
6862            // The test driver's clock: the frames it has run, not the wall's.
6863            Some((now, start)) => now - start,
6864            None => (Instant::now() - self.gesture_start).as_secs_f32(),
6865        }
6866    }
6867
6868    /// Starts a fresh gesture: the history of the previous one must not leak into
6869    /// the next, or a flick left then right would fling the wrong way.
6870    ///
6871    /// The tracker is the application's scroll behaviour's; a drag on a scroll area
6872    /// replaces it with the area's own.
6873    fn begin_gesture(&mut self) {
6874        self.gesture_velocity = self
6875            .app
6876            .scroll_behavior()
6877            .velocity_tracker(&self.themes.displayed(&self.app));
6878        self.gesture_start = Instant::now();
6879        if let Some((now, start)) = self.gesture_clock.as_mut() {
6880            *start = *now;
6881        }
6882        self.track_gesture();
6883    }
6884
6885    /// Records where the pointer is now.
6886    fn track_gesture(&mut self) {
6887        let now = self.gesture_now();
6888        self.gesture_velocity.add_position(now, self.cursor);
6889    }
6890
6891    /// What the gesture tracker makes of the drag as it stands.
6892    fn gesture_estimate(&self) -> VelocityEstimate {
6893        self.gesture_velocity
6894            .estimate(self.gesture_now())
6895            .unwrap_or(VelocityEstimate::STILL)
6896    }
6897
6898    /// [`fling_velocity`], gated on this pointer's slop.
6899    fn fling_velocity(&self, estimate: VelocityEstimate) -> (f32, f32) {
6900        fling_velocity(estimate, self.hit_slop())
6901    }
6902
6903    /// A scroll fling: projects each axis's ballistic destination, friction in closed
6904    /// form, bounds it with the elastic overshoot, and primes the scroll spring with
6905    /// the finger's momentum.
6906    fn fling(&mut self, id: WidgetId, velocity: (f32, f32)) {
6907        let Some(area) = self.ui.as_ref().and_then(|ui| ui.scroll_region(id)) else {
6908            return;
6909        };
6910        let physics = area.physics;
6911        // The physics decides everything from here: whether there is a fling at all,
6912        // how far it runs, and what happens at the edges. A release too slow to
6913        // fling still gets a chance to spring an overscroll back.
6914        if self.runtime.fling_scroll(area, physics, velocity) {
6915            self.request_redraw();
6916        }
6917    }
6918
6919    /// The topmost **reorderable** widget under `point`, as `(id, flat index)`. It
6920    /// uses the reorderables' registry, which is independent of clicking, and so covers
6921    /// the Kanban cards and drop zones, which are not clickable.
6922    fn reorderable_at(&self, point: Point) -> Option<(WidgetId, usize, bool)> {
6923        let id = self.ui.as_ref()?.reorderable_at(point)?;
6924        let widget = self
6925            .tree
6926            .as_ref()
6927            .and_then(|tree| find_widget(tree.as_ref(), id))?;
6928        // A target-**only** widget, a drop zone: reorderable but not grabbable, so no
6929        // drag starts on it. Dropping still aims at it, through `ui.reorderable_at`.
6930        if !widget.reorder_draggable() {
6931            return None;
6932        }
6933        let from = widget.reorder_index()?;
6934        Some((id, from, widget.drag_needs_long_press()))
6935    }
6936
6937    /// **What is actually moving**, given what was grabbed: its id and its box.
6938    ///
6939    /// Usually they are the same widget. They are not when what was grabbed is a **grip**
6940    /// — a source that is not a target — because a grip is a 40-pixel gutter inside a much
6941    /// larger row, and everything the preview is made of is the row's: the ghost is the
6942    /// row lifted out of the frame, the gap that closes behind it is the row's height, and
6943    /// the band the neighbours are matched against is the row's width. Taking the grip's
6944    /// box instead would lift an icon and open a slot two centimetres too narrow.
6945    ///
6946    /// The row is found by the index the two share, among the reorderables under the
6947    /// grip's own middle — which is inside its row by construction.
6948    fn reorder_source(&self, id: WidgetId, from: usize) -> Option<(WidgetId, Rect)> {
6949        let ui = self.ui.as_ref()?;
6950        let tree = self.tree.as_ref()?;
6951        let own = ui.widget_rect(id)?;
6952        let grabbed = find_widget(tree.as_ref(), id)?;
6953        if grabbed.reorder_droppable() {
6954            return Some((id, own));
6955        }
6956        let middle = Point::new(own.x + own.width * 0.5, own.y + own.height * 0.5);
6957        let slot = ui.reorderables_at(middle).find(|other| {
6958            find_widget(tree.as_ref(), *other)
6959                .is_some_and(|w| w.reorder_droppable() && w.reorder_index() == Some(from))
6960        })?;
6961        // A grip whose row is nowhere to be found still moves something: itself, which is
6962        // wrong-looking but not a crash, and cannot happen while the two are built
6963        // together.
6964        Some((slot, ui.widget_rect(slot).unwrap_or(own)))
6965    }
6966
6967    /// The row the reorder under way carries, and its box: the one taken when the drag was
6968    /// first carried, moved with the content since — or, before any frame has carried it,
6969    /// the frame's.
6970    fn reorder_carried(&self) -> Option<(WidgetId, Rect)> {
6971        match &self.drag {
6972            Some(Drag::Reorder {
6973                carried: Some(carried),
6974                ..
6975            }) => Some(*carried),
6976            Some(Drag::Reorder { id, from, .. }) => self.reorder_source(*id, *from),
6977            _ => None,
6978        }
6979    }
6980
6981    /// Takes the carried row's box from the frame, once: the first frame a reorder is carried
6982    /// in, before anything has scrolled it. From then on it goes with the content, and the
6983    /// frame is no longer asked — a row carried to an edge is clipped by it, and a row
6984    /// scrolled out of sight is not in the frame at all.
6985    fn keep_carried_box(&mut self) {
6986        let source = match &self.drag {
6987            Some(Drag::Reorder {
6988                id,
6989                from,
6990                moved: true,
6991                carried: None,
6992                ..
6993            }) => self.reorder_source(*id, *from),
6994            _ => return,
6995        };
6996        if let Some(Drag::Reorder { carried, .. }) = self.drag.as_mut() {
6997            *carried = source;
6998        }
6999    }
7000
7001    /// The topmost reorderable under `point` that something can actually be **dropped**
7002    /// on. It is not always the topmost reorderable: a list row's grip is a source and
7003    /// not a target, and what is under it is the row the drop is really aimed at.
7004    fn reorder_target_at(&self, point: Point) -> Option<WidgetId> {
7005        let ui = self.ui.as_ref()?;
7006        let tree = self.tree.as_ref()?;
7007        ui.reorderables_at(point)
7008            .find(|id| find_widget(tree.as_ref(), *id).is_some_and(|w| w.reorder_droppable()))
7009    }
7010
7011    /// Where the reorderable `id`, grabbed at index `from`, is **dropped** if released now:
7012    /// the target under the pointer, or — over no target at all — the nearest slot of its own
7013    /// list, for one inserted between its neighbours, along the axis it is reordered on.
7014    ///
7015    /// The insertion line and the release both ask this, so the line cannot promise a place
7016    /// the drop does not keep. The second half is milestone 518: a row carried to the bottom
7017    /// of a phone's list is over what follows the list, and the release used to put it back.
7018    /// Milestone 527 turned it on its side, for a list whose rows run across.
7019    fn reorder_drop_target(
7020        &self,
7021        id: WidgetId,
7022        from: usize,
7023        carried: Option<(WidgetId, Rect)>,
7024    ) -> Option<WidgetId> {
7025        if let Some(target) = self.reorder_target_at(self.cursor) {
7026            return Some(target);
7027        }
7028        let tree = self.tree.as_ref()?;
7029        let Some(ReorderMotion::Slots(axis)) = find_widget(tree.as_ref(), id).map(reorder_motion)
7030        else {
7031            return None;
7032        };
7033        // Among the slots of the row that moves, which is not the grip that was grabbed —
7034        // and which may have been scrolled out of the frame by the carry itself.
7035        let row = match carried {
7036            Some((row, _)) => row,
7037            None => self.reorder_source(id, from)?.0,
7038        };
7039        let slots = reorder_siblings(self.ui.as_ref()?, tree.as_ref(), row);
7040        let boxes: Vec<Rect> = slots.iter().map(|(_, rect)| *rect).collect();
7041        nearest_reorder_slot(self.cursor, &boxes, axis).map(|index| slots[index].0)
7042    }
7043
7044    /// What is being **carried** this frame, where it is now: the box the ghost is drawn
7045    /// at, for a reorder or for a lifted item. `None` when nothing is engaged.
7046    ///
7047    /// It is the ghost's box and not the pointer, because the reference scrolls when the
7048    /// *item* reaches the edge, and that is the honest moment: a row half off the bottom
7049    /// is already asking to go further, whatever the finger holding it is over.
7050    fn carried_rect(&self) -> Option<Rect> {
7051        match &self.drag {
7052            Some(Drag::Reorder {
7053                start, moved: true, ..
7054            }) => {
7055                let (_, src) = self.reorder_carried()?;
7056                // The same offsets the ghost is painted at, and for the same reason a
7057                // column's ghost only rises: it moves along its own axis.
7058                let (gx, gy) = ghost_offset(
7059                    self.dragged_reorder_motion()
7060                        .unwrap_or(ReorderMotion::Columns),
7061                    (self.cursor.x - start.x, self.cursor.y - start.y),
7062                );
7063                Some(src.translate(gx, gy))
7064            }
7065            Some(Drag::Item {
7066                source,
7067                start,
7068                moved: true,
7069                ..
7070            }) => Some(
7071                source
7072                    .rect
7073                    .translate(self.cursor.x - start.x, self.cursor.y - start.y),
7074            ),
7075            _ => None,
7076        }
7077    }
7078
7079    /// Scrolls the area under the pointer while what is being carried hangs past one of
7080    /// its edges — the gap `Draggable` has had since milestone 285, and what a list longer
7081    /// than a screen needs before it can be reordered at all.
7082    ///
7083    /// The area is the innermost one under the **pointer** that a user could move by
7084    /// hand: an area that refuses a finger refuses this too, so a list that fits its
7085    /// viewport never twitches.
7086    fn autoscroll_carried(&mut self, dt: f32) -> bool {
7087        self.keep_carried_box();
7088        let Some(carried) = self.carried_rect() else {
7089            return false;
7090        };
7091        let area = {
7092            let scroll = &self.runtime.scroll;
7093            self.ui.as_ref().and_then(|ui| {
7094                ui.scroll_chain(self.cursor).find(|area| {
7095                    area.accepts_user_offset(scroll.get(&area.id).copied().unwrap_or((0.0, 0.0)))
7096                })
7097            })
7098        };
7099        let Some(area) = area else {
7100            return false;
7101        };
7102        let offset = self
7103            .runtime
7104            .scroll
7105            .get(&area.id)
7106            .copied()
7107            .unwrap_or((0.0, 0.0));
7108        // What is still hidden on either side, in **screen** terms: on a reversed axis
7109        // the offset is measured from the other end, so the two swap over.
7110        let room = |offset: f32, max: f32, reverse: bool| {
7111            if reverse {
7112                (max - offset, offset)
7113            } else {
7114                (offset, max - offset)
7115            }
7116        };
7117        let view = area.viewport;
7118        let dx = edge_autoscroll(
7119            (carried.x, carried.x + carried.width),
7120            (view.x, view.x + view.width),
7121            room(offset.0, area.max_x, area.reverse_x),
7122            dt,
7123        )
7124        .unwrap_or(0.0);
7125        let dy = edge_autoscroll(
7126            (carried.y, carried.y + carried.height),
7127            (view.y, view.y + view.height),
7128            room(offset.1, area.max_y, area.reverse_y),
7129            dt,
7130        )
7131        .unwrap_or(0.0);
7132        if dx == 0.0 && dy == 0.0 {
7133            return false;
7134        }
7135        // A movement of the content becomes a change of offset the area's own way, which
7136        // is the one place the sign of a reversed axis is decided.
7137        let delta = area.offset_delta((dx, dy));
7138        let next = (
7139            (offset.0 + delta.0).clamp(0.0, area.max_x),
7140            (offset.1 + delta.1).clamp(0.0, area.max_y),
7141        );
7142        if next == offset {
7143            return false;
7144        }
7145        self.runtime.scroll.insert(area.id, next);
7146        // The target follows, or the inertia the area was resting at would spring the
7147        // list straight back out from under the row.
7148        self.runtime.scroll_target.insert(area.id, next);
7149        self.runtime.scroll_velocity.remove(&area.id);
7150        // What is carried stays under the finger. Its ghost is drawn at the box the frame
7151        // puts it at, offset by how far the finger has moved since the press; the frame now
7152        // puts that box where the content went, so the press goes there too. Otherwise the
7153        // ghost rides up with the list, stops hanging over the edge, and the scroll it
7154        // started starves itself — seen on a phone, milestone 517.
7155        if let Some(drag) = self.drag.as_mut() {
7156            follow_content(
7157                drag,
7158                area.offset_delta((next.0 - offset.0, next.1 - offset.1)),
7159            );
7160        }
7161        true
7162    }
7163
7164    /// How the reorderable currently **grabbed** moves, if a drag is under way. It is what
7165    /// keeps the **pointer spring** (`reorder_x` following the pointer) to `Table`'s columns;
7166    /// the Kanban cards and a list's rows spring their insertion line instead.
7167    fn dragged_reorder_motion(&self) -> Option<ReorderMotion> {
7168        let Some(Drag::Reorder { id, .. }) = self.drag else {
7169            return None;
7170        };
7171        self.tree
7172            .as_ref()
7173            .and_then(|t| find_widget(t.as_ref(), id))
7174            .map(reorder_motion)
7175    }
7176
7177    /// Steps the reorder springs one frame, per gesture — see the frame loop — and says
7178    /// whether they are still moving.
7179    fn advance_reorder_springs(&mut self, dt: f32) -> bool {
7180        let reorder_motion = matches!(self.drag, Some(Drag::Reorder { moved: true, .. }))
7181            .then(|| self.dragged_reorder_motion())
7182            .flatten();
7183        match reorder_motion {
7184            Some(ReorderMotion::Columns) => {
7185                self.reorder_x = spring_toward(self.reorder_x, self.cursor.x, dt, 0.07);
7186                (self.cursor.x - self.reorder_x).abs() > 0.5
7187            }
7188            Some(ReorderMotion::Slots(ReorderAxis::Vertical)) => {
7189                match self.reorder_drop_line(drag_preview::INSERT_THICKNESS) {
7190                    Some(target) => {
7191                        self.reorder_y = spring_toward(self.reorder_y, target.y, dt, 0.07);
7192                        (target.y - self.reorder_y).abs() > 0.5
7193                    }
7194                    None => false,
7195                }
7196            }
7197            Some(ReorderMotion::Slots(ReorderAxis::Horizontal)) => {
7198                match self.reorder_drop_line(drag_preview::INSERT_THICKNESS) {
7199                    Some(target) => {
7200                        self.reorder_x = spring_toward(self.reorder_x, target.x, dt, 0.07);
7201                        (target.x - self.reorder_x).abs() > 0.5
7202                    }
7203                    None => false,
7204                }
7205            }
7206            None => false,
7207        }
7208    }
7209
7210    /// Paints the **reorder preview** on top of the scene, unclipped: the source
7211    /// column dimmed, a **drop indicator** at the target column's insertion edge, and a
7212    /// **lifted card** — shadow plus a `primary` border — following the pointer. No
7213    /// effect outside an engaged header drag.
7214    /// Names, on the console, every box this frame whose children did not fit inside it.
7215    ///
7216    /// The reference treats an overflowing flex as an error condition, paints a striped
7217    /// band along the offending edge and writes to the console; nothing here said anything
7218    /// at all, which is how a task row's delete button came to be laid out past the window
7219    /// and stayed there for three milestones — drawn nowhere, hittable nowhere. Half of
7220    /// that is now here: the words, once per site, with the two ways out the reference
7221    /// suggests. The striped band is not.
7222    ///
7223    /// Once per site, because a layout that does not fit does not fit on every frame, and
7224    /// sixty lines a second is the same as silence.
7225    fn report_overflows(&self, ui: &Ui<A::Message>) {
7226        for o in ui.overflows() {
7227            // The site: a box, an edge, and the amount rounded to the pixel. Enough to
7228            // stay quiet while a window is resized past the same defect, and to speak up
7229            // when the defect changes.
7230            let mut key = std::collections::hash_map::DefaultHasher::new();
7231            use std::hash::{Hash, Hasher};
7232            (
7233                o.rect.width.to_bits(),
7234                o.rect.height.to_bits(),
7235                o.side as u8,
7236                o.amount.round() as i32,
7237            )
7238                .hash(&mut key);
7239            if !self.reported_overflows.borrow_mut().insert(key.finish()) {
7240                continue;
7241            }
7242            let side = match o.side {
7243                frus_widgets::Side::Left => "left",
7244                frus_widgets::Side::Right => "right",
7245                frus_widgets::Side::Top => "top",
7246                frus_widgets::Side::Bottom => "bottom",
7247            };
7248            log::warn!(
7249                "a box {:.0}x{:.0} is overflowed by {:.0} px on the {side}: its children do not \
7250                 fit inside it, and what runs past the edge is drawn outside its parent — \
7251                 invisible where something clips it, and untappable where it leaves the window. \
7252                 Give the child that should give way an `Expanded`, or put the content in a \
7253                 `SingleChildScrollView`.",
7254                o.rect.width,
7255                o.rect.height,
7256                o.amount,
7257            );
7258        }
7259    }
7260
7261    /// Winit's key event as a [`KeyStroke`] — the general vocabulary shortcuts are bound
7262    /// to, as opposed to [`Key`], which is the one text editing needs.
7263    ///
7264    /// `None` for anything with no place in a shortcut: a dead key, a compose sequence, a
7265    /// modifier pressed on its own.
7266    fn keystroke(&self, event: &KeyDown) -> Option<KeyStroke> {
7267        let key = match &event.logical_key {
7268            WinitKey::Named(NamedKey::Enter) => ShortcutKey::Enter,
7269            WinitKey::Named(NamedKey::Escape) => ShortcutKey::Escape,
7270            WinitKey::Named(NamedKey::Tab) => ShortcutKey::Tab,
7271            WinitKey::Named(NamedKey::Space) => ShortcutKey::Space,
7272            WinitKey::Named(NamedKey::Backspace) => ShortcutKey::Backspace,
7273            WinitKey::Named(NamedKey::Delete) => ShortcutKey::Delete,
7274            WinitKey::Named(NamedKey::ArrowUp) => ShortcutKey::Up,
7275            WinitKey::Named(NamedKey::ArrowDown) => ShortcutKey::Down,
7276            WinitKey::Named(NamedKey::ArrowLeft) => ShortcutKey::Left,
7277            WinitKey::Named(NamedKey::ArrowRight) => ShortcutKey::Right,
7278            WinitKey::Named(NamedKey::Home) => ShortcutKey::Home,
7279            WinitKey::Named(NamedKey::End) => ShortcutKey::End,
7280            WinitKey::Named(NamedKey::PageUp) => ShortcutKey::PageUp,
7281            WinitKey::Named(NamedKey::PageDown) => ShortcutKey::PageDown,
7282            WinitKey::Named(NamedKey::F1) => ShortcutKey::F(1),
7283            WinitKey::Named(NamedKey::F2) => ShortcutKey::F(2),
7284            WinitKey::Named(NamedKey::F3) => ShortcutKey::F(3),
7285            WinitKey::Named(NamedKey::F4) => ShortcutKey::F(4),
7286            WinitKey::Named(NamedKey::F5) => ShortcutKey::F(5),
7287            WinitKey::Named(NamedKey::F6) => ShortcutKey::F(6),
7288            WinitKey::Named(NamedKey::F7) => ShortcutKey::F(7),
7289            WinitKey::Named(NamedKey::F8) => ShortcutKey::F(8),
7290            WinitKey::Named(NamedKey::F9) => ShortcutKey::F(9),
7291            WinitKey::Named(NamedKey::F10) => ShortcutKey::F(10),
7292            WinitKey::Named(NamedKey::F11) => ShortcutKey::F(11),
7293            WinitKey::Character(text) => ShortcutKey::Char(text.chars().next()?),
7294            _ => return None,
7295        };
7296        Some(KeyStroke {
7297            key,
7298            ctrl: self.ctrl,
7299            shift: self.shift,
7300            alt: self.alt,
7301            meta: self.meta,
7302        })
7303    }
7304
7305    fn paint_reorder_preview(&self, ui: &Ui<A::Message>, theme: &Theme, scene: &mut Scene) {
7306        let Some(Drag::Reorder {
7307            start, moved: true, ..
7308        }) = self.drag
7309        else {
7310            return;
7311        };
7312        // What moves is the row, even when what was grabbed is the grip inside it — at the
7313        // box it was carried from, which has gone with the content.
7314        let Some((id, src)) = self.reorder_carried() else {
7315            return;
7316        };
7317        let motion = self
7318            .tree
7319            .as_ref()
7320            .and_then(|t| find_widget(t.as_ref(), id))
7321            .map(reorder_motion)
7322            .unwrap_or(ReorderMotion::Columns);
7323        let dx = self.cursor.x - start.x;
7324        let dy = self.cursor.y - start.y;
7325
7326        // The ghost's offset, per motion — see `ghost_offset`.
7327        let (gx, gy) = ghost_offset(motion, (dx, dy));
7328
7329        // The owners of the grabbed item's **subtree**: used by the ghost, to capture a
7330        // rich card's content, which its children paint (milestone 251), **and** by the
7331        // vertical reflow, to lift the card out of the preview. Fallback: the card alone.
7332        let owners: std::collections::HashSet<u64> = self
7333            .tree
7334            .as_ref()
7335            .and_then(|t| find_widget(t.as_ref(), id))
7336            .map(|w| subtree_ids(w, id).iter().map(|i| i.as_u64()).collect())
7337            .unwrap_or_else(|| std::iter::once(id.as_u64()).collect());
7338
7339        match motion {
7340            ReorderMotion::Columns => {
7341                // Reflow the neighbouring columns: the source's gap closes and the drop
7342                // slot opens, following the pointer's **smoothed** abscissa — a gentle
7343                // inertial slide, while the ghost sticks to the real pointer.
7344                let reflowed =
7345                    reflow_reorder_columns(scene.primitives(), src, self.reorder_x, id.as_u64());
7346                scene.clear();
7347                for primitive in reflowed {
7348                    scene.push_primitive(primitive);
7349                }
7350            }
7351            ReorderMotion::Slots(axis) => {
7352                // Reflow the **cards**: the lifted card's gap closes in the source
7353                // column and a slot opens under the **insertion line** in the target one.
7354                // Then the line is laid on top, at the chosen edge — the hovered half,
7355                // milestone 252.
7356                //
7357                // A **smoothed** line (milestone 265): the chosen slot's width, abscissa
7358                // and thickness are kept, but the **ordinate** is replaced by the
7359                // `reorder_y` spring — the line *and* the gap slide between cards, with
7360                // vertical inertia, instead of jumping a notch. A horizontal list's line
7361                // stands on its end, and its **abscissa** is the one that springs.
7362                let line =
7363                    self.reorder_drop_line(drag_preview::INSERT_THICKNESS)
7364                        .map(|r| match axis {
7365                            ReorderAxis::Vertical => Rect {
7366                                y: self.reorder_y,
7367                                ..r
7368                            },
7369                            ReorderAxis::Horizontal => Rect {
7370                                x: self.reorder_x,
7371                                ..r
7372                            },
7373                        });
7374                // Only what can be reordered makes room — a card, a row, a drop zone, and
7375                // what each of them paints. The reflow is geometric, and a button floating
7376                // over the list or the navigation bar under it shares the band without being
7377                // part of the list: moved with it, the first left its `+` a row above its own
7378                // disc on a phone (milestone 517).
7379                let movable = self
7380                    .tree
7381                    .as_ref()
7382                    .map(|tree| reorderable_owners(ui, tree.as_ref()))
7383                    .unwrap_or_default();
7384                let reflowed =
7385                    reflow_reorder_cards(scene.primitives(), src, line, &owners, &movable, axis);
7386                scene.clear();
7387                for primitive in reflowed {
7388                    scene.push_primitive(primitive);
7389                }
7390                if let Some(line) = line {
7391                    // Rounded across its thickness, whichever way it stands.
7392                    let thickness = match axis {
7393                        ReorderAxis::Vertical => line.height,
7394                        ReorderAxis::Horizontal => line.width,
7395                    };
7396                    scene.set_clip(Rect::UNBOUNDED);
7397                    scene.draw_rect(
7398                        line,
7399                        theme.primary,
7400                        theme.radius.min(thickness * 0.5),
7401                        0.0,
7402                        Color::TRANSPARENT,
7403                    );
7404                }
7405            }
7406        }
7407
7408        // A faithful ghost: the grabbed item's primitives, taken from the original
7409        // scene, translated and **un-clipped**, since they would otherwise be cropped at
7410        // the source. In a **rich card** the background is painted by the card but its
7411        // content — label, tags, the × button — by children, under other owners, so we
7412        // capture the primitives of the item's **whole subtree**; see `owners` above.
7413        let ghost: Vec<Primitive> = ui
7414            .scene()
7415            .primitives()
7416            .iter()
7417            .filter(|p| owners.contains(&p.owner()))
7418            .map(|p| p.translated(gx, gy).with_clip(Rect::UNBOUNDED))
7419            .collect();
7420        draw_ghost_card(scene, theme, src.translate(gx, gy), &ghost);
7421    }
7422
7423    /// The lifted item: its own primitives dimmed where it sits, and a copy of them
7424    /// floating under the pointer.
7425    ///
7426    /// The copy is taken from the frame rather than built again, so it cannot drift
7427    /// from what is on screen — a rebuilt "feedback" widget is a second definition of
7428    /// the same thing, and two definitions is one too many. The same reason the
7429    /// reorder ghost works this way.
7430    fn paint_drag_ghost(&self, ui: &Ui<A::Message>, theme: &Theme, scene: &mut Scene) {
7431        let Some(Drag::Item {
7432            source,
7433            start,
7434            moved: true,
7435            ..
7436        }) = &self.drag
7437        else {
7438            return;
7439        };
7440        let widget = self
7441            .tree
7442            .as_ref()
7443            .and_then(|tree| find_widget(tree.as_ref(), source.id));
7444        // A rich item paints its background itself and its content through children,
7445        // under other owners, so the whole subtree is captured — the same reason the
7446        // reorder ghost does.
7447        let owners: std::collections::HashSet<u64> = widget
7448            .map(|w| {
7449                subtree_ids(w, source.id)
7450                    .iter()
7451                    .map(|i| i.as_u64())
7452                    .collect()
7453            })
7454            .unwrap_or_else(|| std::iter::once(source.id.as_u64()).collect());
7455        let opacity = widget.map(|w| w.drag_ghost_opacity()).unwrap_or(1.0);
7456        let dx = self.cursor.x - start.x;
7457        let dy = self.cursor.y - start.y;
7458
7459        // What is left behind, faded in place: the item is being carried, not deleted.
7460        let original: Vec<Primitive> = scene.primitives().to_vec();
7461        scene.clear();
7462        for primitive in &original {
7463            if owners.contains(&primitive.owner()) {
7464                scene.push_faded(primitive, opacity);
7465            } else {
7466                scene.push_primitive(primitive.clone());
7467            }
7468        }
7469
7470        let ghost: Vec<Primitive> = ui
7471            .scene()
7472            .primitives()
7473            .iter()
7474            .filter(|p| owners.contains(&p.owner()))
7475            .map(|p| p.translated(dx, dy).with_clip(Rect::UNBOUNDED))
7476            .collect();
7477        draw_ghost_card(scene, theme, source.rect.translate(dx, dy), &ghost);
7478    }
7479
7480    /// The **insertion** line of the preview: a thin band at the edge of the hovered
7481    /// reorderable slot, a card, a row or a drop zone — the edge **before** it when the
7482    /// pointer is in its leading half, the edge **after** it in its trailing half. Across a
7483    /// vertical list; standing on its end in a horizontal one. `None` when the release would
7484    /// land nowhere.
7485    fn reorder_drop_line(&self, thickness: f32) -> Option<Rect> {
7486        let Some(Drag::Reorder {
7487            id, from, carried, ..
7488        }) = self.drag
7489        else {
7490            return None;
7491        };
7492        // The reorderable slot — card, row or drop zone — the release would land on: the
7493        // one under the pointer, or the nearest of the list's own.
7494        let target = self.reorder_drop_target(id, from, carried)?;
7495        let rect = self.ui.as_ref()?.widget_rect(target)?;
7496        let axis = match self
7497            .tree
7498            .as_ref()
7499            .and_then(|t| find_widget(t.as_ref(), target))
7500            .map(reorder_motion)
7501        {
7502            Some(ReorderMotion::Slots(axis)) => axis,
7503            _ => ReorderAxis::Vertical,
7504        };
7505        Some(drop_insertion_line(
7506            rect,
7507            thickness,
7508            self.reorder_insert_after(target, rect),
7509            axis,
7510            self.is_rtl(),
7511        ))
7512    }
7513
7514    /// For a slot a reorderable is **inserted** at, tells whether insertion happens
7515    /// **after** it (index +1, between it and the next) — the pointer in its trailing half,
7516    /// which `reorder_drop_after` works out along the list's axis and reading direction.
7517    /// Always `false` for `Table`'s columns, which keep their drop logic unchanged. This is
7518    /// the insertion line's counterpart on the **routing** side.
7519    fn reorder_insert_after(&self, target: WidgetId, rect: Rect) -> bool {
7520        match self
7521            .tree
7522            .as_ref()
7523            .and_then(|t| find_widget(t.as_ref(), target))
7524            .map(reorder_motion)
7525        {
7526            Some(ReorderMotion::Slots(axis)) => {
7527                reorder_drop_after(self.cursor, rect, axis, self.is_rtl())
7528            }
7529            _ => false,
7530        }
7531    }
7532
7533    /// The innermost detector that takes a drag under the pointer, with the way it drags.
7534    fn pan_target(&self) -> Option<(WidgetId, frus_widgets::PanAxis)> {
7535        let (id, _) = self.ui.as_ref()?.pan_at(self.cursor)?;
7536        let axis = find_widget(self.tree.as_ref()?.as_ref(), id)?.pan_axis()?;
7537        Some((id, axis))
7538    }
7539
7540    /// `at`, in the coordinates of the detector `id`.
7541    fn pan_local(&self, id: WidgetId, at: Point) -> Point {
7542        let origin = self
7543            .ui
7544            .as_ref()
7545            .and_then(|ui| ui.widget_rect(id))
7546            .map_or(Point::new(0.0, 0.0), |rect| Point::new(rect.x, rect.y));
7547        Point::new(at.x - origin.x, at.y - origin.y)
7548    }
7549
7550    /// Hands one moment of a drag to the detector `id`, and its message to the application.
7551    fn send_pan(&mut self, id: WidgetId, event: frus_widgets::PanEvent) {
7552        let rtl = self.is_rtl();
7553        let value = self.runtime.value(id);
7554        let (message, moved, released) = match self
7555            .tree
7556            .as_ref()
7557            .and_then(|tree| find_widget(tree.as_ref(), id))
7558        {
7559            Some(widget) => {
7560                // A widget whose own value the drag moves — a switch's thumb (milestone
7561                // 638): held where the finger puts it, and asked what letting go there
7562                // means.
7563                let moved = widget.pan_value(event, value, rtl);
7564                let released = match event {
7565                    frus_widgets::PanEvent::End { .. } if self.runtime.value_placed(id) => {
7566                        Some(widget.on_value_release(value))
7567                    }
7568                    _ => None,
7569                };
7570                (widget.on_pan(event), moved, released)
7571            }
7572            None => (None, None, None),
7573        };
7574        if let Some(v) = moved {
7575            self.runtime.hold_value(id, v);
7576        }
7577        if let Some(release) = released {
7578            self.runtime.release_value(id);
7579            if let Some(message) = release {
7580                self.dispatch(message);
7581            }
7582        }
7583        if let Some(message) = message {
7584            self.dispatch(message);
7585        }
7586        self.request_redraw();
7587    }
7588
7589    /// Sends a value drag's **start** or **end** to the widget being dragged, at
7590    /// `fraction` along its travel.
7591    ///
7592    /// The fraction is the one [`frus_widgets::DragTrack`] works out for every moment of
7593    /// the drag, from the same press and the same track: an end that disagreed with the
7594    /// last `on_drag` about where the value finished would be a slider that settles
7595    /// somewhere its own final message never mentioned.
7596    fn dispatch_drag_edge(&mut self, id: WidgetId, edge: Edge, fraction: f32) {
7597        let message = self
7598            .tree
7599            .as_ref()
7600            .and_then(|tree| find_widget(tree.as_ref(), id))
7601            .and_then(|widget| match edge {
7602                Edge::Start => widget.on_drag_start(fraction),
7603                Edge::End => widget.on_drag_end(fraction),
7604            });
7605        if let Some(message) = message {
7606            self.dispatch(message);
7607        }
7608    }
7609
7610    /// A moment of a drag on one of a widget's thumbs (milestone 651).
7611    fn thumb_drag(
7612        &mut self,
7613        id: WidgetId,
7614        thumb: usize,
7615        moment: frus_widgets::ThumbDrag,
7616        fraction: f32,
7617    ) {
7618        if moment == frus_widgets::ThumbDrag::Start {
7619            // The widget paints the thumb it took raised and on top.
7620            self.runtime.input.thumb = Some((id, thumb));
7621        }
7622        let message = self
7623            .tree
7624            .as_ref()
7625            .and_then(|tree| find_widget(tree.as_ref(), id))
7626            .and_then(|widget| widget.on_thumb_drag(thumb, moment, fraction));
7627        if let Some(message) = message {
7628            self.dispatch(message);
7629        }
7630    }
7631
7632    /// One moment of a value drag: `dx` for a handle that accumulates, else the `value`
7633    /// the drag's track says this moment stands for — `None` when it changes nothing.
7634    fn apply_widget_drag(&mut self, id: WidgetId, dx: f32, value: Option<f32>) {
7635        let message = self
7636            .tree
7637            .as_ref()
7638            .and_then(|tree| find_widget(tree.as_ref(), id))
7639            .and_then(|widget| {
7640                widget
7641                    .on_drag_delta(dx)
7642                    .or_else(|| value.and_then(|v| widget.on_drag(v)))
7643            });
7644        if let Some(message) = message {
7645            self.dispatch(message);
7646        }
7647    }
7648
7649    /// **A key going down**: everything the shell does with one, from the system's back key
7650    /// and the inspector to a shortcut, Tab, the arrows, activation and typing. Called with
7651    /// winit's event in a window, and with one a test made in the driver (milestone 608).
7652    /// `event_loop` is what Back quits through; with none, Back that would quit does nothing.
7653    fn key_down(&mut self, event: &KeyDown, event_loop: Option<&ActiveEventLoop>) {
7654        // What the browser typed into the field just before this key comes first: a key it
7655        // hands on reaches the shell at once, and the letters before it are still waiting for
7656        // the end of the turn — an arrow would move a caret that has not had them yet, and be
7657        // undone when they arrived (milestone 711).
7658        #[cfg(web)]
7659        self.drain_web_editing();
7660        // A keyboard interaction: the focus ring becomes visible again.
7661        if !self.runtime.focus_visible {
7662            self.runtime.focus_visible = true;
7663            self.request_redraw();
7664        }
7665
7666        // The **system** back — Android's button or gesture, the browser's back
7667        // key: it closes the topmost overlay, else pops a screen, else quits.
7668        if matches!(
7669            event.logical_key,
7670            WinitKey::Named(NamedKey::BrowserBack) | WinitKey::Named(NamedKey::GoBack)
7671        ) {
7672            if !event.repeat {
7673                if let Some(event_loop) = event_loop {
7674                    self.system_back(event_loop);
7675                }
7676            }
7677            return;
7678        }
7679
7680        // F12 toggles the **inspector**: outlines, a card for the hovered
7681        // widget, and a tree dump on stderr. Debug builds only.
7682        if matches!(event.logical_key, WinitKey::Named(NamedKey::F12)) {
7683            if cfg!(debug_assertions) && !event.repeat {
7684                self.inspector = !self.inspector;
7685                self.inspector_dump = self.inspector;
7686                self.request_redraw();
7687            }
7688            return;
7689        }
7690
7691        // **Shortcuts.** Placed after the system keys the shell owns outright
7692        // (back, F12) and before everything an application can bind, so that a
7693        // binding cannot take the inspector or the back gesture away.
7694        //
7695        // A stroke with no Ctrl, Alt or Meta goes to a focused field first: a
7696        // binding on a bare letter would otherwise make every field under it
7697        // impossible to type in.
7698        if let Some(stroke) = self.keystroke(event) {
7699            let typing = !stroke.is_command() && self.runtime.input.focused.is_some();
7700            if !typing {
7701                let msgs = self
7702                    .ui
7703                    .as_ref()
7704                    .map(|ui| ui.keystroke(stroke, self.runtime.input.focused))
7705                    .unwrap_or_default();
7706                if !msgs.is_empty() {
7707                    for msg in msgs {
7708                        self.dispatch(msg);
7709                    }
7710                    self.request_redraw();
7711                    return;
7712                }
7713            }
7714        }
7715
7716        // Tab and Shift+Tab move between focusables, even with nothing focused.
7717        if matches!(event.logical_key, WinitKey::Named(NamedKey::Tab)) {
7718            let forward = !self.shift;
7719            let next = self
7720                .ui
7721                .as_ref()
7722                .and_then(|ui| ui.focus_next(self.runtime.input.focused, forward));
7723            if next.is_some() {
7724                self.runtime.input.focused = next;
7725                self.runtime.region = None;
7726                self.reveal_focus();
7727                self.request_redraw();
7728            }
7729            return;
7730        }
7731
7732        // Escape walks **leaf to root** from the focused widget — an `OverlayPortal`
7733        // consumes it to close itself — and failing that closes the topmost
7734        // overlay, so Escape works with nothing focused.
7735        if matches!(event.logical_key, WinitKey::Named(NamedKey::Escape)) {
7736            // Auto-repeat does not trigger another close.
7737            if !event.repeat {
7738                self.escape();
7739            }
7740            return;
7741        }
7742
7743        let Some(focused) = self.runtime.input.focused else {
7744            // With nothing focused, a selection an area holds still answers to the
7745            // clipboard's Copy and to Select all.
7746            self.region_key(event);
7747            return;
7748        };
7749
7750        // Arrows navigate focus **geometrically** — except left and right in a
7751        // text field, where they move the caret; up and down navigate even out
7752        // of a single-line field.
7753        let arrow = match event.logical_key {
7754            WinitKey::Named(NamedKey::ArrowUp) => Some(FocusDirection::Up),
7755            WinitKey::Named(NamedKey::ArrowDown) => Some(FocusDirection::Down),
7756            WinitKey::Named(NamedKey::ArrowLeft) => Some(FocusDirection::Left),
7757            WinitKey::Named(NamedKey::ArrowRight) => Some(FocusDirection::Right),
7758            _ => None,
7759        };
7760        // PgUp and PgDn jump a **page** inside a multi-line field, bounded to
7761        // the field so they never leave it. No effect anywhere else.
7762        if matches!(
7763            event.logical_key,
7764            WinitKey::Named(NamedKey::PageUp) | WinitKey::Named(NamedKey::PageDown)
7765        ) {
7766            let down = matches!(event.logical_key, WinitKey::Named(NamedKey::PageDown));
7767            if self.move_caret_vertical(focused, down, true) {
7768                return;
7769            }
7770        }
7771
7772        if let Some(direction) = arrow {
7773            // In a **multi-line** field, Up and Down move the caret between
7774            // lines while it stays in the field; on the first or last line we
7775            // fall back to focus navigation and leave the field.
7776            if matches!(direction, FocusDirection::Up | FocusDirection::Down) {
7777                let down = matches!(direction, FocusDirection::Down);
7778                if self.move_caret_vertical(focused, down, false) {
7779                    return;
7780                }
7781            }
7782
7783            // Left and right arrows are offered to the focused widget first —
7784            // a range slider, say — through `on_key`. If it consumes them, focus
7785            // does not move.
7786            if matches!(direction, FocusDirection::Left | FocusDirection::Right) {
7787                let key = if matches!(direction, FocusDirection::Left) {
7788                    Key::Left {
7789                        shift: self.shift,
7790                        word: self.ctrl,
7791                    }
7792                } else {
7793                    Key::Right {
7794                        shift: self.shift,
7795                        word: self.ctrl,
7796                    }
7797                };
7798                let widget = self
7799                    .tree
7800                    .as_ref()
7801                    .and_then(|tree| find_widget(tree.as_ref(), focused));
7802                // A reorderable header moves its column by one; we capture its
7803                // position to announce it, the keyboard being how screen-reader
7804                // users reorder.
7805                let reorder_from = widget.and_then(|w| w.reorder_index());
7806                let handled = widget.map(|w| w.on_key(&key));
7807                if let Some(KeyResponse::Handled(message)) = handled {
7808                    if let Some(message) = message {
7809                        self.dispatch(message);
7810                    }
7811                    if let Some(from) = reorder_from {
7812                        let to = if matches!(direction, FocusDirection::Left) {
7813                            from.wrapping_sub(1)
7814                        } else {
7815                            from + 1
7816                        };
7817                        self.set_announcement(format!("Column moved to position {}", to + 1));
7818                    }
7819                    self.request_redraw();
7820                    return;
7821                }
7822            }
7823
7824            let is_text = self
7825                .tree
7826                .as_ref()
7827                .and_then(|tree| find_widget(tree.as_ref(), focused))
7828                .is_some_and(is_text_field);
7829            let navigates =
7830                !is_text || matches!(direction, FocusDirection::Up | FocusDirection::Down);
7831            if navigates {
7832                if let Some(next) = self
7833                    .ui
7834                    .as_ref()
7835                    .and_then(|ui| ui.focus_directional(focused, direction))
7836                {
7837                    self.runtime.input.focused = Some(next);
7838                    self.reveal_focus();
7839                    self.request_redraw();
7840                }
7841                return;
7842            }
7843        }
7844
7845        // Home and End are offered to the focused widget — a range slider maps
7846        // them to min and max — before the default action. A text field ignores
7847        // them here (`on_key` returns Ignored) and falls back to ordinary
7848        // editing further down.
7849        if matches!(
7850            event.logical_key,
7851            WinitKey::Named(NamedKey::Home) | WinitKey::Named(NamedKey::End)
7852        ) {
7853            let key = if matches!(event.logical_key, WinitKey::Named(NamedKey::Home)) {
7854                Key::Home {
7855                    shift: self.shift,
7856                    doc: self.ctrl,
7857                }
7858            } else {
7859                Key::End {
7860                    shift: self.shift,
7861                    doc: self.ctrl,
7862                }
7863            };
7864            let handled = self
7865                .tree
7866                .as_ref()
7867                .and_then(|tree| find_widget(tree.as_ref(), focused))
7868                .map(|widget| widget.on_key(&key));
7869            if let Some(KeyResponse::Handled(message)) = handled {
7870                if let Some(message) = message {
7871                    self.dispatch(message);
7872                }
7873                self.request_redraw();
7874                return;
7875            }
7876        }
7877
7878        // Keyboard activation, Enter or Space, of a clickable focusable: a
7879        // button, a checkbox, a switch. Text fields, which have no `on_click`,
7880        // fall back to ordinary editing — Enter submits, Space is a space.
7881        if matches!(
7882            event.logical_key,
7883            WinitKey::Named(NamedKey::Enter) | WinitKey::Named(NamedKey::Space)
7884        ) {
7885            let widget = self
7886                .tree
7887                .as_ref()
7888                .and_then(|tree| find_widget(tree.as_ref(), focused))
7889                .filter(|widget| widget.focusable());
7890            let message = widget.and_then(|widget| widget.on_click());
7891            if let Some(message) = message {
7892                // Auto-repeat does not machine-gun the activation: holding
7893                // Space on a button is one single click.
7894                if !event.repeat {
7895                    // The spoken announcement of the effect — a sort, a
7896                    // selection — captured before `dispatch` rebuilds the tree.
7897                    let announce = widget.and_then(|widget| widget.announce());
7898                    self.dispatch(message);
7899                    if let Some(announce) = announce {
7900                        self.set_announcement(announce);
7901                    }
7902                    self.request_redraw();
7903                }
7904                return;
7905            }
7906        }
7907
7908        // The clipboard: Ctrl+C/X/V, and a keyboard's own Copy, Cut and Paste.
7909        if let Some(command) = clipboard_command(&event.logical_key, event.physical_key, self.ctrl)
7910        {
7911            match command {
7912                ClipCommand::Copy => {
7913                    self.copy_selection(focused);
7914                }
7915                ClipCommand::Cut => self.cut_selection(focused),
7916                ClipCommand::Paste => {
7917                    // Answered now, or — on the Web — on a later frame.
7918                    if let Some(pasted) = self.clipboard.paste(focused, self.window.as_ref()) {
7919                        self.land_paste(pasted);
7920                    }
7921                }
7922            }
7923            return;
7924        }
7925
7926        // The context-menu key, or Shift+F10: the selection bar for someone with no
7927        // pointer to hold or right-click with (milestone 568). Only a text field has
7928        // one, and it is opened on the field that has the focus.
7929        if opens_selection_bar(&event.logical_key, self.shift)
7930            && self
7931                .tree
7932                .as_ref()
7933                .and_then(|tree| find_widget(tree.as_ref(), focused))
7934                .is_some_and(|widget| widget.text_value().is_some())
7935        {
7936            self.show_selection_toolbar(focused);
7937            return;
7938        }
7939
7940        // The other editing shortcuts, Ctrl+A/Z/Y.
7941        if self.ctrl {
7942            match &event.logical_key {
7943                WinitKey::Character(c) if c.eq_ignore_ascii_case("a") => {
7944                    self.select_all(focused);
7945                    return;
7946                }
7947                // Undo, and redo under both its spellings — Ctrl+Y on Windows,
7948                // Ctrl+Shift+Z everywhere else, and people bring the one their
7949                // hands already know.
7950                WinitKey::Character(c) if c.eq_ignore_ascii_case("z") && !self.shift => {
7951                    self.step_history(focused, false);
7952                    self.request_redraw();
7953                    return;
7954                }
7955                WinitKey::Character(c)
7956                    if c.eq_ignore_ascii_case("y")
7957                        || (c.eq_ignore_ascii_case("z") && self.shift) =>
7958                {
7959                    self.step_history(focused, true);
7960                    self.request_redraw();
7961                    return;
7962                }
7963                _ => {}
7964            }
7965        }
7966
7967        // With the input bridge active, editing goes EXCLUSIVELY through the
7968        // InputConnection, that is, the IME queue — the bridge view receives the
7969        // hardware keys too. Without this guard every keystroke would arrive
7970        // twice, once from winit's native queue and once from the bridge.
7971        #[cfg(android)]
7972        if crate::android_ime::installed() {
7973            return;
7974        }
7975
7976        let shift = self.shift;
7977        let key = match &event.logical_key {
7978            WinitKey::Named(NamedKey::Backspace) => Some(Key::Backspace),
7979            WinitKey::Named(NamedKey::Delete) => Some(Key::Delete),
7980            // Repeating Enter does not submit again; text and deletion do
7981            // repeat normally.
7982            WinitKey::Named(NamedKey::Enter) if !event.repeat => Some(Key::Enter),
7983            WinitKey::Named(NamedKey::Enter) => None,
7984            // With Ctrl, Left/Right jump a word and Home/End bound the field.
7985            WinitKey::Named(NamedKey::ArrowLeft) => Some(Key::Left {
7986                shift,
7987                word: self.ctrl,
7988            }),
7989            WinitKey::Named(NamedKey::ArrowRight) => Some(Key::Right {
7990                shift,
7991                word: self.ctrl,
7992            }),
7993            WinitKey::Named(NamedKey::Home) => Some(Key::Home {
7994                shift,
7995                doc: self.ctrl,
7996            }),
7997            WinitKey::Named(NamedKey::End) => Some(Key::End {
7998                shift,
7999                doc: self.ctrl,
8000            }),
8001            WinitKey::Named(NamedKey::Space) => Some(Key::Text(" ".to_string())),
8002            // Android delivers Enter as `Character("\n")`, through the
8003            // KeyCharacterMap, rather than `Named(Enter)`: the same submission,
8004            // without inserting a line break into the field.
8005            WinitKey::Character(c) if c == "\n" || c == "\r" => {
8006                if event.repeat {
8007                    None
8008                } else {
8009                    Some(Key::Enter)
8010                }
8011            }
8012            _ => event.text.as_ref().map(|text| Key::Text(text.to_string())),
8013        };
8014
8015        if let Some(key) = key {
8016            self.apply_key(focused, key);
8017            self.request_redraw();
8018        }
8019    }
8020
8021    /// The **system** back — Android's `KEYCODE_BACK`, the browser's back key — and
8022    /// its native chain: ① the topmost overlay closes, be it a sheet, a drawer, a modal
8023    /// or a menu; ② failing that a screen is popped, by replaying the **back gesture
8024    /// already committed**, through the same hooks as the swipe, so it settles with an
8025    /// animation; ③ failing that, at the root, back **quits the application**, as
8026    /// Android expects.
8027    fn system_back(&mut self, event_loop: &ActiveEventLoop) {
8028        if !self.system_back_step() {
8029            event_loop.exit();
8030        }
8031    }
8032
8033    /// What the system's Back does short of leaving the application, and whether it did
8034    /// anything: `false` means there was nothing left to go back from.
8035    fn system_back_step(&mut self) -> bool {
8036        // An open selection bar goes first, as a platform's own text selection does: Back
8037        // closes it before it closes anything behind it (milestone 568).
8038        if self.close_selection_toolbar() {
8039            return true;
8040        }
8041        // Then a selection an area holds with its bar, as for a field's.
8042        if self
8043            .runtime
8044            .region
8045            .as_ref()
8046            .is_some_and(|selection| selection.bar)
8047        {
8048            self.runtime.region = None;
8049            self.request_redraw();
8050            return true;
8051        }
8052        if let Some(message) = self.ui.as_ref().and_then(|ui| ui.top_dismiss()) {
8053            self.dispatch(message);
8054            self.request_redraw();
8055            return true;
8056        }
8057        if self.app.can_go_back() {
8058            self.build_dirty = true;
8059            // A pop, not a swipe: no finger is on the page (milestone 622).
8060            self.app.go_back();
8061            self.request_redraw();
8062            return true;
8063        }
8064        false
8065    }
8066
8067    /// Routes **Escape**: a leaf-to-root walk from the focused widget, with a
8068    /// three-state result — `Ignored` keeps walking up, `Handled` consumes, `Skip` stops
8069    /// with no fallback — then falls back to closing the topmost overlay when nobody
8070    /// answered, or when nothing is focused.
8071    fn escape(&mut self) {
8072        // The selection bar first, as for the system's Back (milestone 568).
8073        if self.close_selection_toolbar() {
8074            return;
8075        }
8076        // Then a selection an area holds.
8077        if self.runtime.region.take().is_some() {
8078            self.request_redraw();
8079            return;
8080        }
8081        // 1) Walk up the focus path. `Some(None)` means consumed with no message; an
8082        // outer `None` means the whole path ignored it, so we fall back.
8083        let outcome: Option<Option<A::Message>> = self.runtime.input.focused.and_then(|focused| {
8084            let tree = self.tree.as_ref()?;
8085            let path = find_path(tree.as_ref(), focused);
8086            for widget in path.iter().rev() {
8087                match widget.on_key(&Key::Escape) {
8088                    KeyResponse::Handled(message) => return Some(message),
8089                    KeyResponse::Skip => return Some(None),
8090                    KeyResponse::Ignored => {}
8091                }
8092            }
8093            None
8094        });
8095
8096        match outcome {
8097            Some(message) => {
8098                if let Some(message) = message {
8099                    self.dispatch(message);
8100                    self.request_redraw();
8101                }
8102            }
8103            // 2) Nobody on the path, or nothing focused: the topmost overlay.
8104            None => {
8105                if let Some(message) = self.ui.as_ref().and_then(|ui| ui.top_dismiss()) {
8106                    self.dispatch(message);
8107                    self.request_redraw();
8108                }
8109            }
8110        }
8111    }
8112
8113    /// Moves the caret vertically in the focused multi-line field — Up/Down when
8114    /// `page` is false, PgUp/PgDn otherwise — preserving the **remembered goal column**.
8115    /// Applies the selection under Shift and reveals the caret. Returns `true` when the
8116    /// caret moved within the field, `false` otherwise, in which case the shell
8117    /// navigates focus instead.
8118    fn move_caret_vertical(&mut self, id: WidgetId, down: bool, page: bool) -> bool {
8119        let width = self
8120            .ui
8121            .as_ref()
8122            .and_then(|ui| ui.widget_rect(id))
8123            .map(|r| r.width)
8124            .unwrap_or(0.0);
8125        let cursor = self.runtime.edits.get(&id).map(|e| e.cursor).unwrap_or(0);
8126        let moved = self
8127            .tree
8128            .as_ref()
8129            .and_then(|tree| find_widget(tree.as_ref(), id))
8130            .and_then(|widget| widget.caret_vertical(width, cursor, down, page, self.goal_x));
8131        let Some((new_cursor, goal)) = moved else {
8132            return false;
8133        };
8134        let shift = self.shift;
8135        let edit = self.runtime.edits.entry(id).or_default();
8136        if shift {
8137            if edit.anchor.is_none() {
8138                edit.anchor = Some(edit.cursor);
8139            }
8140        } else {
8141            edit.anchor = None;
8142        }
8143        edit.cursor = new_cursor;
8144        // Remember the goal column for the next vertical jump.
8145        self.goal_x = Some(goal);
8146        self.reveal_caret(id, new_cursor);
8147        self.request_redraw();
8148        true
8149    }
8150
8151    /// A hold in field `id` selects the word under the finger — the press has already
8152    /// put the caret there — and gives the selection its handles (milestone 511).
8153    fn select_held_word(&mut self, id: WidgetId) {
8154        let cursor = self.runtime.edits.get(&id).map(|e| e.cursor).unwrap_or(0);
8155        let word = self
8156            .tree
8157            .as_ref()
8158            .and_then(|tree| find_widget(tree.as_ref(), id))
8159            .and_then(|widget| widget.word_at(cursor))
8160            .filter(|(start, end)| start < end);
8161        let Some((start, end)) = word else {
8162            // Nothing to select — an empty field, a hold past the end of the text. The
8163            // bar still opens, at the caret, with Paste and whatever else applies.
8164            self.show_selection_toolbar(id);
8165            return;
8166        };
8167        self.runtime.edits.insert(
8168            id,
8169            Edit {
8170                cursor: end,
8171                anchor: Some(start),
8172                composing: None,
8173            },
8174        );
8175        self.runtime.selection_handles = Some(id);
8176        self.runtime.close_edit_run(id);
8177        self.show_selection_toolbar(id);
8178        #[cfg(android)]
8179        self.push_ime_context(id);
8180    }
8181
8182    /// The selection handle under the pointer, taken: a drag that moves its end of the
8183    /// focused field's selection. `None` when no handle is showing there.
8184    fn grab_selection_handle(&self) -> Option<Drag> {
8185        let id = self.runtime.selection_handles?;
8186        if self.runtime.input.focused != Some(id) {
8187            return None;
8188        }
8189        let rect = self.ui.as_ref()?.widget_rect(id)?;
8190        let edit = self.runtime.edits.get(&id).copied()?;
8191        let scroll_y = self.runtime.scroll.get(&id).map(|s| s.1).unwrap_or(0.0);
8192        let handles = find_widget(self.tree.as_ref()?.as_ref(), id)?
8193            .selection_handles(rect.width, &edit, scroll_y)?;
8194        let local = Point::new(self.cursor.x - rect.x, self.cursor.y - rect.y);
8195        let handle = crate::selection::grab(&handles, local)?;
8196        let at = handles[handle.index()].line_center;
8197        Some(Drag::SelectionHandle {
8198            id,
8199            rect,
8200            handle,
8201            grab: Point::new(at.x - local.x, at.y - local.y),
8202        })
8203    }
8204
8205    /// Routes a key to the focused field: updates the editing state and applies
8206    /// whatever message comes out, a value change or a submission.
8207    fn apply_key(&mut self, id: WidgetId, key: Key) {
8208        // Any horizontal move, or any keystroke, forgets the vertical goal column.
8209        self.goal_x = None;
8210        // Typing puts a touch selection's handles away, as a press does (milestone 511) —
8211        // and the bar over it (milestone 568).
8212        self.runtime.selection_handles = None;
8213        self.runtime.selection_toolbar = None;
8214        let was = self.runtime.edits.get(&id).copied().unwrap_or_default();
8215        let mut edit = was;
8216        let widget = self
8217            .tree
8218            .as_ref()
8219            .and_then(|tree| find_widget(tree.as_ref(), id));
8220        // What the field held before the key: half of an undo step, and the half the
8221        // application owns.
8222        let before = widget
8223            .and_then(|widget| widget.text_value())
8224            .map(str::to_owned);
8225        // A field's own announcement (#18) — read here for the same reason the click
8226        // and keyboard-activation paths read theirs off the widget **before**
8227        // `dispatch_edit` rebuilds the tree: a plain keystroke leaves it unused, since
8228        // `on_edit` answers `None` for one and the announcement below is never spoken.
8229        let announce = widget.and_then(|widget| widget.announce());
8230        let message = widget.and_then(|widget| widget.on_edit(&mut edit, &key));
8231        self.runtime.edits.insert(id, edit);
8232        // In a multi-line field, make the retained scroll follow the caret and reveal it.
8233        self.reveal_caret(id, edit.cursor);
8234        if let Some(message) = message {
8235            self.dispatch_edit(message);
8236            if let Some(announce) = announce {
8237                self.set_announcement(announce);
8238            }
8239        }
8240        // The history, recorded on the **evidence** of a changed value rather than on the
8241        // intent of a key that usually changes one: a filter may have refused the
8242        // character, a length limit may have swallowed it, a read-only field ignores it,
8243        // and an Enter submits instead of typing. The value is read again from the tree
8244        // the dispatch has just rebuilt, so what is recorded is what happened.
8245        let after = self
8246            .tree
8247            .as_ref()
8248            .and_then(|tree| find_widget(tree.as_ref(), id))
8249            .and_then(|widget| widget.text_value())
8250            .map(str::to_owned);
8251        match (before, after) {
8252            (Some(before), Some(after)) if before != after => {
8253                let kind = EditKind::of(&key, was.selection_range().is_some(), self.composing());
8254                let since = self.since_last_edit();
8255                self.runtime
8256                    .record_edit(id, EditSnapshot::new(before, was), kind, since);
8257            }
8258            // A key that moved only the caret ends the run: what is typed next is a step
8259            // of its own, or one undo would take back two visits to the field at once.
8260            (Some(_), _) => self.runtime.close_edit_run(id),
8261            _ => {}
8262        }
8263    }
8264
8265    /// Dispatches a message that changed a field's value, and refreshes the tree at once.
8266    ///
8267    /// Keys can arrive in **bursts**, faster than a frame — a software keyboard, `adb
8268    /// input text`, auto-repeat — and the next one must see the CURRENT value, not the
8269    /// retained tree's, or it would overwrite the previous keystroke. So the tree is
8270    /// refreshed right away; `build_dirty` stays raised and the next frame redoes the full
8271    /// pass: mounts, leaving fades and all.
8272    fn dispatch_edit(&mut self, message: A::Message) {
8273        self.dispatch(message);
8274        if let Some((width, height)) = self.last_size {
8275            let theme = self.themes.displayed(&self.app);
8276            // A surface of its own, because this build happens between frames rather than
8277            // inside one — and the same `build_view` as the frame path, because the next
8278            // key in the burst reads this tree straight away.
8279            let tree = self
8280                .media_query(width, height)
8281                .scope(|| build_view(&self.app, &theme, &self.runtime));
8282            self.tree = Some(tree);
8283        }
8284    }
8285
8286    /// Seconds since the last recorded edit, and stamps this one. The first edit is
8287    /// infinitely far from the one before it, which is to say it starts a run.
8288    fn since_last_edit(&mut self) -> f32 {
8289        let now = Instant::now();
8290        let since = self
8291            .last_edit_at
8292            .map(|then| now.duration_since(then).as_secs_f32())
8293            .unwrap_or(f32::INFINITY);
8294        self.last_edit_at = Some(now);
8295        since
8296    }
8297
8298    /// Whether an input method is in the middle of composing a word.
8299    fn composing(&self) -> bool {
8300        self.runtime
8301            .input
8302            .focused
8303            .and_then(|id| self.runtime.edits.get(&id))
8304            .is_some_and(|edit| edit.composing.is_some())
8305    }
8306
8307    /// Steps a text field's value back one change, or forward again. `true` when something
8308    /// moved — Ctrl+Z on anything that is not a field, or with nothing left to undo, moves
8309    /// nothing and says so.
8310    fn step_history(&mut self, id: WidgetId, forward: bool) -> bool {
8311        let value = self
8312            .tree
8313            .as_ref()
8314            .and_then(|tree| find_widget(tree.as_ref(), id))
8315            .and_then(|widget| widget.text_value())
8316            .map(str::to_owned);
8317        let Some(value) = value else {
8318            return false;
8319        };
8320        let edit = self.runtime.edits.get(&id).copied().unwrap_or_default();
8321        let current = EditSnapshot::new(value, edit);
8322        let target = if forward {
8323            self.runtime.redo_edit(id, current)
8324        } else {
8325            self.runtime.undo_edit(id, current)
8326        };
8327        let Some(target) = target else {
8328            return false;
8329        };
8330        let message = self
8331            .tree
8332            .as_ref()
8333            .and_then(|tree| find_widget(tree.as_ref(), id))
8334            .and_then(|widget| widget.replace_value(target.value.clone()));
8335        // The caret goes back with the text — an undo that restores the value and leaves
8336        // the caret at the end has done half the job.
8337        self.runtime.edits.insert(id, target.edit);
8338        self.reveal_caret(id, target.edit.cursor);
8339        if let Some(message) = message {
8340            self.dispatch_edit(message);
8341        }
8342        // An undo is not a keystroke: what is typed after it starts a run of its own,
8343        // however quickly it follows.
8344        self.last_edit_at = None;
8345        true
8346    }
8347
8348    /// Brings the focused widget **into view**, gliding every scroll region around it.
8349    ///
8350    /// Called wherever the keyboard moves the focus, and nowhere else. A click already
8351    /// landed on something the eye could see, and a focus restored because an overlay
8352    /// closed should put the page back where the reader left it, not chase whatever the
8353    /// restoration happened to pick.
8354    ///
8355    /// The glide is a **target**, not a jump: the same easing a scrollbar drag or a page
8356    /// request uses, so Tab through a form reads as one movement rather than a series of
8357    /// cuts. Anything already moving the offset is let go of — the keyboard has just
8358    /// overruled it.
8359    fn reveal_focus(&mut self) {
8360        let Some(id) = self.runtime.input.focused else {
8361            return;
8362        };
8363        let Some(moves) = self.ui.as_ref().map(|ui| ui.reveal(id, &self.runtime)) else {
8364            return;
8365        };
8366        for (area, offset) in moves {
8367            self.runtime.scroll_ballistic.remove(&area);
8368            self.runtime.scroll_velocity.remove(&area);
8369            self.runtime.scroll_target.insert(area, offset);
8370        }
8371    }
8372
8373    /// Makes a multi-line field's retained scroll **follow the caret**: adjusts
8374    /// `runtime.scroll[id]` just enough to keep the caret visible, the way an editor
8375    /// re-centres as you type. A no-op for a field that does not scroll.
8376    fn reveal_caret(&mut self, id: WidgetId, cursor: usize) {
8377        let Some(vp) = self.ui.as_ref().and_then(|ui| ui.scrollable_viewport(id)) else {
8378            return;
8379        };
8380        let metrics = self
8381            .tree
8382            .as_ref()
8383            .and_then(|tree| find_widget(tree.as_ref(), id))
8384            .and_then(|widget| widget.text_metrics(vp.width, cursor));
8385        let Some((content_h, visible_h, caret_top, caret_h)) = metrics else {
8386            return;
8387        };
8388        let max_y = (content_h - visible_h).max(0.0);
8389        let cur = self.runtime.scroll.get(&id).map(|s| s.1).unwrap_or(0.0);
8390        // The scroll window in which the caret stays visible, from "the caret's bottom
8391        // is visible" to "the caret's top is visible". We bring the current scroll into it.
8392        let lo = (caret_top + caret_h - visible_h).max(0.0);
8393        let hi = caret_top;
8394        let target = cur.clamp(lo.min(hi), lo.max(hi)).clamp(0.0, max_y);
8395        self.runtime.scroll.insert(id, (0.0, target));
8396        self.runtime.scroll_target.insert(id, (0.0, target));
8397        self.runtime.scroll_velocity.remove(&id);
8398    }
8399
8400    /// Copies field `id`'s selected text to the clipboard. `false` when there was nothing
8401    /// to copy — no selection, or a field that gives its text to nobody (a masked one).
8402    fn copy_selection(&mut self, id: WidgetId) -> bool {
8403        let edit = self.runtime.edits.get(&id).copied().unwrap_or_default();
8404        let text = self
8405            .tree
8406            .as_ref()
8407            .and_then(|tree| find_widget(tree.as_ref(), id))
8408            .and_then(|widget| widget.selected_text(&edit));
8409        match text {
8410            Some(text) => {
8411                self.clipboard.set_text(text);
8412                true
8413            }
8414            None => false,
8415        }
8416    }
8417
8418    /// The texts of `area` in the frame on screen, in reading order, each with its length in
8419    /// characters — what a selection over them is worked out from.
8420    fn region_stops(&self, area: WidgetId) -> Vec<(WidgetId, usize)> {
8421        let (Some(ui), Some(tree)) = (self.ui.as_ref(), self.tree.as_ref()) else {
8422            return Vec::new();
8423        };
8424        ui.text_stops_in(area)
8425            .filter_map(|stop| {
8426                let text = find_widget(tree.as_ref(), stop.id)?.selection_text()?;
8427                Some((stop.id, text.chars().count()))
8428            })
8429            .collect()
8430    }
8431
8432    /// The selection from `anchor` to `extent` in `area`, in the frame on screen.
8433    fn region_selection(
8434        &self,
8435        area: WidgetId,
8436        anchor: RegionPoint,
8437        extent: RegionPoint,
8438    ) -> RegionSelection {
8439        RegionSelection::new(area, anchor, extent, &self.region_stops(area))
8440    }
8441
8442    /// The character boundary of `stop`'s text nearest `at`, by the layout it was drawn with.
8443    fn region_index(&self, stop: &TextStop, at: Point) -> Option<usize> {
8444        find_widget(self.tree.as_ref()?.as_ref(), stop.id)?.selection_hit(
8445            at.x - stop.rect.x,
8446            at.y - stop.rect.y,
8447            stop.rect.width,
8448        )
8449    }
8450
8451    /// A press at the pointer over words in a selection area, if nothing else has a claim on
8452    /// it: the selection begins there, empty, and follows the pointer until it lifts. A double
8453    /// click selects the word.
8454    fn begin_region_selection(&mut self) {
8455        let Some(stop) = self.region_press_target() else {
8456            return;
8457        };
8458        let Some(index) = self.region_index(&stop, self.cursor) else {
8459            return;
8460        };
8461        let now = Instant::now();
8462        let here = self.cursor;
8463        let double = self.last_region_click.is_some_and(|(then, there)| {
8464            (now - then).as_secs_f32() < 0.4
8465                && (here.x - there.x).hypot(here.y - there.y) <= REGION_DOUBLE_CLICK_SLOP
8466        });
8467        self.last_region_click = Some((now, here));
8468        let word = double
8469            .then(|| {
8470                let tree = self.tree.as_ref()?;
8471                let text = find_widget(tree.as_ref(), stop.id)?.selection_text()?;
8472                Some(frus_widgets::word_bounds(text, index))
8473            })
8474            .flatten();
8475        let (anchor, extent) = match word {
8476            Some((start, end)) => (
8477                RegionPoint {
8478                    text: stop.id,
8479                    index: start,
8480                },
8481                RegionPoint {
8482                    text: stop.id,
8483                    index: end,
8484                },
8485            ),
8486            None => {
8487                let point = RegionPoint {
8488                    text: stop.id,
8489                    index,
8490                };
8491                (point, point)
8492            }
8493        };
8494        self.runtime.region = Some(self.region_selection(stop.area, anchor, extent));
8495        // A word is a whole selection: there is nothing to drag out of it.
8496        self.drag = word
8497            .is_none()
8498            .then_some(Drag::RegionSelect { area: stop.area });
8499        self.request_redraw();
8500    }
8501
8502    /// The text of a selection area under the pointer, if a press there is the area's to take:
8503    /// something that answers a press — a button, a tappable row, a link — keeps it.
8504    fn region_press_target(&self) -> Option<TextStop> {
8505        let ui = self.ui.as_ref()?;
8506        let claimed = self
8507            .runtime
8508            .input
8509            .pressed
8510            .is_some_and(|pressed| ui.msg_for(pressed).is_some());
8511        if claimed {
8512            return None;
8513        }
8514        ui.text_stop_at(self.cursor)
8515    }
8516
8517    /// A finger held on words in a selection area: the word under it is selected and the bar
8518    /// opens over it. Whether there was a word.
8519    fn select_region_word_held(&mut self) -> bool {
8520        let Some(stop) = self.ui.as_ref().and_then(|ui| ui.text_stop_at(self.cursor)) else {
8521            return false;
8522        };
8523        let Some(index) = self.region_index(&stop, self.cursor) else {
8524            return false;
8525        };
8526        let word = self
8527            .tree
8528            .as_ref()
8529            .and_then(|tree| find_widget(tree.as_ref(), stop.id))
8530            .and_then(|widget| widget.selection_text())
8531            .map(|text| frus_widgets::word_bounds(text, index))
8532            .filter(|(start, end)| start < end);
8533        let Some((start, end)) = word else {
8534            return false;
8535        };
8536        let anchor = RegionPoint {
8537            text: stop.id,
8538            index: start,
8539        };
8540        let extent = RegionPoint {
8541            text: stop.id,
8542            index: end,
8543        };
8544        self.runtime.region = Some(
8545            self.region_selection(stop.area, anchor, extent)
8546                .with_bar(true),
8547        );
8548        self.request_redraw();
8549        true
8550    }
8551
8552    /// The two handles of a selection area's touch selection, where they are on the surface.
8553    fn region_handles(&self) -> Option<(WidgetId, [frus_widgets::SelectionHandle; 2])> {
8554        let selection = self.runtime.region.as_ref().filter(|s| s.handles)?;
8555        let (ui, tree) = (self.ui.as_ref()?, self.tree.as_ref()?);
8556        let at = |point: RegionPoint, start: bool| {
8557            let stop = ui
8558                .text_stops_in(selection.area)
8559                .find(|s| s.id == point.text)?;
8560            let handle = find_widget(tree.as_ref(), point.text)?.selection_grip(
8561                stop.rect.width,
8562                point.index,
8563                start,
8564            )?;
8565            let (dx, dy) = (stop.rect.x, stop.rect.y);
8566            Some(frus_widgets::SelectionHandle {
8567                rect: handle.rect.translate(dx, dy),
8568                line_center: Point::new(handle.line_center.x + dx, handle.line_center.y + dy),
8569            })
8570        };
8571        Some((
8572            selection.area,
8573            [at(selection.start?, true)?, at(selection.end?, false)?],
8574        ))
8575    }
8576
8577    /// The handle of a selection area's touch selection under the pointer, taken.
8578    fn grab_region_handle(&self) -> Option<Drag> {
8579        let (area, handles) = self.region_handles()?;
8580        let handle = crate::selection::grab(&handles, self.cursor)?;
8581        let at = handles[handle.index()].line_center;
8582        Some(Drag::RegionHandle {
8583            area,
8584            handle,
8585            grab: Point::new(at.x - self.cursor.x, at.y - self.cursor.y),
8586        })
8587    }
8588
8589    /// A handle of a selection area moved with the finger: its end goes to the text position
8590    /// under it — carried by `grab`, so the handle's line and not the fingertip picks it — and
8591    /// the other end stays. The two never meet nor cross, as a field's do not.
8592    fn drag_region_handle(
8593        &mut self,
8594        area: WidgetId,
8595        handle: crate::selection::Handle,
8596        grab: Point,
8597    ) {
8598        let Some(selection) = self.runtime.region.as_ref() else {
8599            return;
8600        };
8601        let (Some(start), Some(end)) = (selection.start, selection.end) else {
8602            return;
8603        };
8604        let target = Point::new(self.cursor.x + grab.x, self.cursor.y + grab.y);
8605        let Some(stop) = self
8606            .ui
8607            .as_ref()
8608            .and_then(|ui| ui.nearest_text_stop(area, target))
8609        else {
8610            return;
8611        };
8612        let Some(index) = self.region_index(&stop, target) else {
8613            return;
8614        };
8615        let moved = RegionPoint {
8616            text: stop.id,
8617            index,
8618        };
8619        let stops = self.region_stops(area);
8620        let order = |point: RegionPoint| {
8621            stops
8622                .iter()
8623                .position(|(id, _)| *id == point.text)
8624                .map(|at| (at, point.index))
8625        };
8626        let (Some(to), Some(other)) = (
8627            order(moved),
8628            order(match handle {
8629                crate::selection::Handle::Start => end,
8630                crate::selection::Handle::End => start,
8631            }),
8632        ) else {
8633            return;
8634        };
8635        let (fixed, crosses) = match handle {
8636            crate::selection::Handle::Start => (end, to >= other),
8637            crate::selection::Handle::End => (start, to <= other),
8638        };
8639        if crosses {
8640            return;
8641        }
8642        let next = RegionSelection::new(area, fixed, moved, &stops).with_bar(true);
8643        // The bar stays put away until the handle is let go.
8644        let next = RegionSelection { bar: false, ..next };
8645        if self.runtime.region.as_ref() != Some(&next) {
8646            self.runtime.region = Some(next);
8647            self.request_redraw();
8648        }
8649    }
8650
8651    /// The pointer has moved during a drag in `area`: the far end of the selection goes to the
8652    /// text nearest it.
8653    fn extend_region_selection(&mut self, area: WidgetId) {
8654        let Some(anchor) = self.runtime.region.as_ref().map(|region| region.anchor) else {
8655            return;
8656        };
8657        let Some(stop) = self
8658            .ui
8659            .as_ref()
8660            .and_then(|ui| ui.nearest_text_stop(area, self.cursor))
8661        else {
8662            return;
8663        };
8664        let Some(index) = self.region_index(&stop, self.cursor) else {
8665            return;
8666        };
8667        let extent = RegionPoint {
8668            text: stop.id,
8669            index,
8670        };
8671        let selection = self.region_selection(area, anchor, extent);
8672        if self.runtime.region.as_ref() != Some(&selection) {
8673            self.runtime.region = Some(selection);
8674            self.request_redraw();
8675        }
8676    }
8677
8678    /// The words a selection area has selected, as they are copied: one line per text. `None`
8679    /// when nothing is selected.
8680    fn region_text(&self) -> Option<String> {
8681        let region = self.runtime.region.as_ref()?;
8682        let (ui, tree) = (self.ui.as_ref()?, self.tree.as_ref()?);
8683        let texts: Vec<(WidgetId, &str)> = ui
8684            .text_stops_in(region.area)
8685            .filter_map(|stop| {
8686                let text = find_widget(tree.as_ref(), stop.id)?.selection_text()?;
8687                Some((stop.id, text))
8688            })
8689            .collect();
8690        let copied = frus_widgets::region_copy(&texts, &region.ranges);
8691        (!copied.is_empty()).then_some(copied)
8692    }
8693
8694    /// Puts the words a selection area has selected on the clipboard. Whether there were any.
8695    fn copy_region(&mut self) -> bool {
8696        match self.region_text() {
8697            Some(copied) => {
8698                self.clipboard.set_text(copied);
8699                true
8700            }
8701            None => false,
8702        }
8703    }
8704
8705    /// Selects every text of the area that holds a selection.
8706    fn select_all_region(&mut self) {
8707        let Some(area) = self.runtime.region.as_ref().map(|region| region.area) else {
8708            return;
8709        };
8710        let stops = self.region_stops(area);
8711        let (Some(first), Some(last)) = (stops.first(), stops.last()) else {
8712            return;
8713        };
8714        let anchor = RegionPoint {
8715            text: first.0,
8716            index: 0,
8717        };
8718        let extent = RegionPoint {
8719            text: last.0,
8720            index: last.1,
8721        };
8722        let bar = self
8723            .runtime
8724            .region
8725            .as_ref()
8726            .is_some_and(|selection| selection.bar);
8727        self.runtime.region = Some(self.region_selection(area, anchor, extent).with_bar(bar));
8728        self.request_redraw();
8729    }
8730
8731    /// A key while nothing has the focus: Copy, and Select all, act on a selection an area
8732    /// holds. Whether the key was one of them.
8733    fn region_key(&mut self, event: &KeyDown) -> bool {
8734        if self.runtime.region.is_none() {
8735            return false;
8736        }
8737        if let Some(ClipCommand::Copy) =
8738            clipboard_command(&event.logical_key, event.physical_key, self.ctrl)
8739        {
8740            self.copy_region();
8741            return true;
8742        }
8743        let select_all = self.ctrl
8744            && matches!(&event.logical_key, WinitKey::Character(c) if c.eq_ignore_ascii_case("a"));
8745        if select_all {
8746            self.select_all_region();
8747            return true;
8748        }
8749        false
8750    }
8751
8752    /// Cuts field `id`'s selection: copies it, then deletes it — **only if it was copied**.
8753    /// A masked field refuses the copy, and a cut that deleted anyway would destroy the
8754    /// text while putting it nowhere.
8755    fn cut_selection(&mut self, id: WidgetId) {
8756        if self.copy_selection(id) {
8757            self.apply_key(id, Key::Backspace);
8758            self.request_redraw();
8759        }
8760    }
8761
8762    /// Selects all of field `id`'s text.
8763    fn select_all(&mut self, id: WidgetId) {
8764        self.runtime.edits.insert(
8765            id,
8766            Edit {
8767                cursor: usize::MAX,
8768                anchor: Some(0),
8769                composing: None,
8770            },
8771        );
8772        // A selection is a caret move: what is typed over it is a step of its own, not more
8773        // of whatever was being typed before.
8774        self.runtime.close_edit_run(id);
8775        self.request_redraw();
8776    }
8777
8778    /// Opens field `id`'s **selection bar** — Cut, Copy, Paste, Select all — over its
8779    /// selection (milestone 568). Whether the clipboard holds text is asked once, here, and
8780    /// kept for as long as the bar is open: it is what decides whether Paste is offered.
8781    fn show_selection_toolbar(&mut self, id: WidgetId) {
8782        self.show_selection_toolbar_at(id, None);
8783    }
8784
8785    /// The same, opened by a right-click at `at`: the bar goes there instead of to the
8786    /// selection (milestone 618).
8787    fn show_selection_toolbar_at(&mut self, id: WidgetId, at: Option<Point>) {
8788        let can_paste = self.clipboard.has_text();
8789        self.runtime.selection_toolbar = Some(ToolbarMark { id, can_paste, at });
8790        self.request_redraw();
8791    }
8792
8793    /// What Back and Escape do to an open selection bar: put it away, with the handles that
8794    /// went with it, and say so — `true` — so that the key goes no further.
8795    fn close_selection_toolbar(&mut self) -> bool {
8796        if self.runtime.selection_toolbar.is_none() {
8797            return false;
8798        }
8799        self.runtime.selection_handles = None;
8800        self.hide_selection_toolbar();
8801        true
8802    }
8803
8804    /// Puts the bar away, if one is open.
8805    fn hide_selection_toolbar(&mut self) {
8806        if self.runtime.selection_toolbar.take().is_some() {
8807            self.request_redraw();
8808        }
8809    }
8810
8811    /// What pressing a built-in button of the selection bar does, to the field the bar is
8812    /// open on. Copy, Cut and Paste close it and the handles with it; Select all leaves
8813    /// both open on a phone, on the selection it has just made larger, and closes both on a
8814    /// desktop (milestone 625).
8815    fn perform_edit_action(&mut self, action: frus_widgets::EditAction) {
8816        use frus_widgets::EditAction;
8817        // The bar of a selection area's selection, which acts on the area and not on a field.
8818        if self
8819            .runtime
8820            .region
8821            .as_ref()
8822            .is_some_and(|selection| selection.bar)
8823        {
8824            match action {
8825                EditAction::Copy => {
8826                    // As a platform's own selection does after a copy: it is put away.
8827                    self.copy_region();
8828                    self.runtime.region = None;
8829                }
8830                EditAction::SelectAll => self.select_all_region(),
8831                EditAction::Cut | EditAction::Paste => {}
8832            }
8833            self.request_redraw();
8834            return;
8835        }
8836        let Some(id) = self.runtime.selection_toolbar.map(|mark| mark.id) else {
8837            return;
8838        };
8839        match action {
8840            EditAction::Copy => {
8841                if self.copy_selection(id) {
8842                    // As the platforms' own text fields do after a copy: the selection
8843                    // collapses to its end, and what marked it is put away.
8844                    if let Some(edit) = self.runtime.edits.get_mut(&id) {
8845                        *edit = collapsed_to_end(*edit);
8846                    }
8847                    self.runtime.close_edit_run(id);
8848                }
8849                self.runtime.selection_handles = None;
8850                self.hide_selection_toolbar();
8851            }
8852            EditAction::Cut => {
8853                // `apply_key` puts the handles and the bar away, as any key does.
8854                self.cut_selection(id);
8855                self.hide_selection_toolbar();
8856            }
8857            EditAction::Paste => {
8858                self.runtime.selection_handles = None;
8859                self.hide_selection_toolbar();
8860                // Answered now, or — on the Web — on a later frame.
8861                if let Some(pasted) = self.clipboard.paste(id, self.window.as_ref()) {
8862                    self.land_paste(pasted);
8863                }
8864            }
8865            EditAction::SelectAll => {
8866                self.select_all(id);
8867                // Made with a finger or a right-click, the selection keeps whatever
8868                // marked it.
8869                if self.runtime.selection_handles.is_some() {
8870                    self.runtime.selection_handles = Some(id);
8871                }
8872                if select_all_hides_bar(frus_widgets::default_target_platform()) {
8873                    // On a desktop the menu has done its work, and goes with the handles,
8874                    // as the reference's `hideToolbar()` takes both (milestone 625).
8875                    self.runtime.selection_handles = None;
8876                    self.hide_selection_toolbar();
8877                } else {
8878                    // On a phone the bar stays, with the list its new state calls for,
8879                    // where it was: a menu a right-click opened stays at the pointer.
8880                    let at = self.runtime.selection_toolbar.and_then(|mark| mark.at);
8881                    self.show_selection_toolbar_at(id, at);
8882                }
8883            }
8884        }
8885        #[cfg(android)]
8886        self.push_ime_context(id);
8887    }
8888
8889    /// A right-click on a text field (a desktop's context menu, milestone 568): focuses it,
8890    /// puts the caret where the click landed unless it landed inside the selection there
8891    /// already is, and opens the bar. On anything else it puts an open bar away.
8892    fn context_click(&mut self) {
8893        // Something that answers a secondary tap takes it (milestone 581).
8894        let secondary = self
8895            .ui
8896            .as_ref()
8897            .and_then(|ui| ui.hit(self.cursor))
8898            .and_then(|id| {
8899                self.tree
8900                    .as_ref()
8901                    .and_then(|tree| find_widget(tree.as_ref(), id))
8902            })
8903            .and_then(|widget| widget.on_secondary_tap());
8904        if let Some(message) = secondary {
8905            self.hide_selection_toolbar();
8906            self.dispatch(message);
8907            self.request_redraw();
8908            return;
8909        }
8910        let hit = self.ui.as_ref().and_then(|ui| ui.focus_hit(self.cursor));
8911        let Some((id, rect)) = hit else {
8912            self.hide_selection_toolbar();
8913            return;
8914        };
8915        let was_focused = self.runtime.input.focused == Some(id);
8916        let scroll_y = self.runtime.scroll.get(&id).map(|s| s.1).unwrap_or(0.0);
8917        let scroll_cursor = if was_focused {
8918            self.runtime.edits.get(&id).map(|e| e.cursor).unwrap_or(0)
8919        } else {
8920            0
8921        };
8922        let cursor = self
8923            .tree
8924            .as_ref()
8925            .and_then(|tree| find_widget(tree.as_ref(), id))
8926            .and_then(|widget| {
8927                widget.cursor_at(
8928                    self.cursor.x - rect.x,
8929                    self.cursor.y - rect.y + scroll_y,
8930                    rect.width,
8931                    scroll_cursor,
8932                )
8933            });
8934        // Only a text field has a bar; a button under the pointer is not one.
8935        let Some(cursor) = cursor else {
8936            self.hide_selection_toolbar();
8937            return;
8938        };
8939        let inside = was_focused
8940            && self
8941                .runtime
8942                .edits
8943                .get(&id)
8944                .and_then(|edit| edit.selection_range())
8945                .is_some_and(|(start, end)| cursor >= start && cursor <= end);
8946        self.runtime.input.focused = Some(id);
8947        self.runtime.selection_handles = None;
8948        if !inside {
8949            self.goal_x = None;
8950            self.runtime.edits.insert(
8951                id,
8952                Edit {
8953                    cursor,
8954                    anchor: None,
8955                    composing: None,
8956                },
8957            );
8958            self.runtime.close_edit_run(id);
8959        }
8960        self.show_selection_toolbar_at(id, Some(self.cursor));
8961    }
8962
8963    /// Types what the clipboard answered into the field that asked for it, if the paste
8964    /// still lands there — see `clip::Pasted::lands`.
8965    fn land_paste(&mut self, pasted: clip::Pasted) {
8966        if pasted.lands(self.runtime.input.focused) {
8967            self.apply_key(pasted.into, Key::Text(pasted.text));
8968            self.request_redraw();
8969        }
8970    }
8971}
8972
8973/// The edit a **copy** leaves behind: the selection collapsed to its end, the caret where
8974/// the selection stopped. What the platforms' own text fields do after Copy from their bar,
8975/// and what tells the reader it happened — the highlight goes.
8976fn collapsed_to_end(edit: Edit) -> Edit {
8977    let cursor = edit.selection_range().map_or(edit.cursor, |(_, end)| end);
8978    Edit {
8979        cursor,
8980        anchor: None,
8981        composing: None,
8982    }
8983}
8984
8985/// Which end of a value drag is being announced.
8986#[derive(Clone, Copy, PartialEq, Eq)]
8987enum Edge {
8988    Start,
8989    End,
8990}
8991
8992/// Was the gesture that just ended **still a tap**?
8993///
8994/// Every drag here engages only past a threshold, so a press that never crossed it is
8995/// not a drag at all: it is a tap, and the release owes the widget underneath its
8996/// click. A drag that *did* move has already been answered — a fling, a drop, a
8997/// dismissal — and must not also click.
8998///
8999/// One list, because forgetting a variant is silent: the widget stays hittable, the
9000/// press still records it, and only the release quietly does nothing. That is how a
9001/// dismissible row came to swallow every tap on it (milestone 327).
9002/// The window's cursor for one the interface asks for. A hidden cursor is the default
9003/// shape, made invisible by the caller.
9004fn cursor_icon(cursor: UiCursor) -> CursorIcon {
9005    match cursor {
9006        UiCursor::Default | UiCursor::None => CursorIcon::Default,
9007        UiCursor::Pointer => CursorIcon::Pointer,
9008        UiCursor::Text => CursorIcon::Text,
9009        UiCursor::Forbidden => CursorIcon::NotAllowed,
9010        UiCursor::Wait => CursorIcon::Wait,
9011        UiCursor::Progress => CursorIcon::Progress,
9012        UiCursor::Help => CursorIcon::Help,
9013        UiCursor::Precise => CursorIcon::Crosshair,
9014        UiCursor::Move => CursorIcon::Move,
9015        UiCursor::Grab => CursorIcon::Grab,
9016        UiCursor::Grabbing => CursorIcon::Grabbing,
9017        UiCursor::ResizeColumn => CursorIcon::ColResize,
9018        UiCursor::ResizeRow => CursorIcon::RowResize,
9019        UiCursor::ZoomIn => CursorIcon::ZoomIn,
9020        UiCursor::ZoomOut => CursorIcon::ZoomOut,
9021        UiCursor::ContextMenu => CursorIcon::ContextMenu,
9022    }
9023}
9024
9025/// A movement, or a velocity, kept to the way a detector drags.
9026fn mask_pan(axis: frus_widgets::PanAxis, value: Point) -> Point {
9027    match axis {
9028        frus_widgets::PanAxis::Free => value,
9029        frus_widgets::PanAxis::Horizontal => Point::new(value.x, 0.0),
9030        frus_widgets::PanAxis::Vertical => Point::new(0.0, value.y),
9031    }
9032}
9033
9034fn gesture_was_a_tap(ended: Option<&Drag>) -> bool {
9035    matches!(
9036        ended,
9037        Some(
9038            Drag::Scroll { moved: false, .. }
9039                | Drag::Pan { moved: false, .. }
9040                | Drag::Gesture { moved: false, .. }
9041                | Drag::Reorder { moved: false, .. }
9042                | Drag::Item { moved: false, .. }
9043                | Drag::Dismiss { moved: false, .. }
9044                | Drag::Sheet { moved: false, .. }
9045        )
9046    )
9047}
9048
9049/// A press lands at `at`: what is under it is pressed, and a surface that takes ink
9050/// splashes from where it landed. The box comes from the frame that was on screen when the
9051/// pointer came down, which is the one the user aimed at. Returns whether a splash started.
9052fn press_at<Msg: Clone>(runtime: &mut Runtime, ui: Option<&Ui<Msg>>, at: Point) -> bool {
9053    runtime.input.pressed = ui.and_then(|ui| ui.hit(at));
9054    let Some(pressed) = runtime.input.pressed else {
9055        return false;
9056    };
9057    let Some(rect) = ui.and_then(|ui| ui.ink_box(pressed)) else {
9058        return false;
9059    };
9060    runtime.ink_press(
9061        pressed,
9062        Point::new(at.x - rect.x, at.y - rect.y),
9063        Size::new(rect.width, rect.height),
9064    );
9065    true
9066}
9067
9068/// What becomes of the press once a pointer event has been routed, `could_tap` being
9069/// whether the gesture under way could still have ended as a tap **before** the event, and
9070/// `drag` the gesture after it. Returns whether anything changed, which wants a frame.
9071///
9072/// A press on something inside a scrollable is shared with the scroll until the pointer
9073/// crosses the slop. From then on the gesture has it — a scroll, a swipe, a sheet, a
9074/// carried row — and, as in the reference, where the recogniser that loses the arena is
9075/// told so, the press is **let go of there and then**: its ink is swept, its press fades.
9076/// And a pointer that has lifted holds nothing, whatever its release did.
9077///
9078/// Before this, a release that ended a gesture which had moved returned before the press
9079/// was cleared, so the press outlived the finger; the ink, which waits for as long as its
9080/// widget is pressed, was held for good — the grey left on a segment a phone had swiped
9081/// a strip of filters by (milestone 534).
9082///
9083/// A finger hovers only so that its press can show (milestone 505), so a finger whose press
9084/// was taken stops hovering too. A mouse is still over whatever it is over.
9085fn settle_press(
9086    input: &mut frus_widgets::InputState,
9087    kind: PointerKind,
9088    could_tap: bool,
9089    drag: Option<&Drag>,
9090    touch: bool,
9091) -> bool {
9092    let taken = match kind {
9093        PointerKind::Down => false,
9094        PointerKind::Move => could_tap && !gesture_was_a_tap(drag),
9095        PointerKind::Up | PointerKind::Cancel => true,
9096    };
9097    if !taken {
9098        return false;
9099    }
9100    let mut changed = input.pressed.take().is_some();
9101    if touch {
9102        changed |= input.hovered.take().is_some();
9103    }
9104    changed
9105}
9106
9107/// How fast an area scrolls under a carried item, in pixels per second **per pixel** the
9108/// item hangs over the edge: the further out it is, the faster the list comes to meet it.
9109/// The reference's number, and the reference's law.
9110const AUTOSCROLL_VELOCITY: f32 = 50.0;
9111
9112/// The overhang that speed is worked out from is capped here, so that carrying a row far
9113/// past the edge — or off the window entirely — settles at a fast but usable speed instead
9114/// of one nobody can aim with.
9115const AUTOSCROLL_MAX_OVERHANG: f32 = 20.0;
9116
9117/// Places the scroll requests it can against `regions`, and hands back the ones whose
9118/// region that registry does not name, along with whether anything moved.
9119///
9120/// A request names its region by **key**, the way a focus request does: the application
9121/// wrapped it in `keyed(k, …)` and the tree is what turns that back into an identity.
9122/// The framework's own identities are hashes of a position in a tree — an application
9123/// cannot know one, and should not have to.
9124///
9125/// Nothing is *stored* here. The request is spent the moment its region is found, and
9126/// what it leaves behind is an offset in the runtime, indistinguishable from an offset a
9127/// finger left there. That is what makes it survive a rebuild: there is nothing to
9128/// survive.
9129fn apply_scroll_requests<Msg>(
9130    runtime: &mut Runtime,
9131    tree: &dyn Widget<Msg>,
9132    requests: Vec<(u64, ScrollTo)>,
9133    regions: &[Scrollable],
9134) -> (Vec<(u64, ScrollTo)>, bool) {
9135    let mut unplaced = Vec::new();
9136    let mut moved = false;
9137    for (key, to) in requests {
9138        let area = find_by_key(tree, key).and_then(|id| regions.iter().find(|a| a.id == id));
9139        match area {
9140            Some(area) => moved |= runtime.scroll_to(area, to),
9141            None => unplaced.push((key, to)),
9142        }
9143    }
9144    (unplaced, moved)
9145}
9146
9147/// Places the sheet requests it can against `sheets`, and hands back the ones whose sheet
9148/// that registry does not name, along with whether anything moved — the terms of
9149/// `apply_scroll_requests`, above.
9150///
9151/// A request names the key the application wrapped the sheet in; the state belongs to the
9152/// sheet's panel under that key, which is what the tree is asked for.
9153fn apply_sheet_requests<Msg>(
9154    runtime: &mut Runtime,
9155    tree: &dyn Widget<Msg>,
9156    requests: Vec<(u64, SheetTo)>,
9157    sheets: &[frus_widgets::SheetArea],
9158) -> (Vec<(u64, SheetTo)>, bool) {
9159    let mut unplaced = Vec::new();
9160    let mut moved = false;
9161    for (key, to) in requests {
9162        let sheet = frus_widgets::find_sheet_by_key(tree, key)
9163            .and_then(|id| sheets.iter().find(|sheet| sheet.id == id));
9164        match sheet {
9165            Some(sheet) => moved |= runtime.sheet_to(sheet.id, &sheet.spec, &to),
9166            None => unplaced.push((key, to)),
9167        }
9168    }
9169    (unplaced, moved)
9170}
9171
9172/// Moves a carried item's press by `shift` — how far its content moved on screen — so that
9173/// the ghost, drawn at the content's box plus the finger's travel since the press, stays
9174/// under the finger. A lifted item's box, recorded at the press, goes with it.
9175///
9176/// Anything else being dragged is not carried content, and is left alone.
9177fn follow_content(drag: &mut Drag, shift: (f32, f32)) {
9178    match drag {
9179        Drag::Reorder { start, carried, .. } => {
9180            start.x += shift.0;
9181            start.y += shift.1;
9182            if let Some((_, rect)) = carried {
9183                *rect = rect.translate(shift.0, shift.1);
9184            }
9185        }
9186        Drag::Item { source, start, .. } => {
9187            start.x += shift.0;
9188            start.y += shift.1;
9189            source.rect = source.rect.translate(shift.0, shift.1);
9190        }
9191        _ => {}
9192    }
9193}
9194
9195/// One axis of the auto-scroll: how far the **content** has to move this frame so that an
9196/// item held past a viewport's edge brings the rest of the list into view.
9197///
9198/// Everything is in screen coordinates and along one axis: `item` and `view` are
9199/// `(start, end)` pairs, `room` is what is left to scroll that way (nought at the end of
9200/// the content). The answer is a movement of the content, which the area then turns into
9201/// an offset of its own — signs and reversed axes belong to `Scrollable::offset_delta`,
9202/// not here.
9203///
9204/// `None` means *not now*: the item is inside, or the edge it hangs over is the end of the
9205/// content, where there is nothing left to reveal and the list should stay still rather
9206/// than fight.
9207fn edge_autoscroll(item: (f32, f32), view: (f32, f32), room: (f32, f32), dt: f32) -> Option<f32> {
9208    let before = view.0 - item.0; // above the top edge
9209    let after = item.1 - view.1; // below the bottom edge
9210                                 // The leading edge is asked first, and both are never obeyed at once: an item taller
9211                                 // than the viewport hangs over both, and an item pulled two ways scrolls nowhere.
9212    let overhang = if before > 0.0 && room.0 > 0.0 {
9213        before.min(AUTOSCROLL_MAX_OVERHANG)
9214    } else if after > 0.0 && room.1 > 0.0 {
9215        -after.min(AUTOSCROLL_MAX_OVERHANG)
9216    } else {
9217        return None;
9218    };
9219    let travel = overhang * AUTOSCROLL_VELOCITY * dt;
9220    (travel != 0.0).then_some(travel)
9221}
9222
9223/// Moves `current` toward `target` by one **exponential** spring step, with time
9224/// constant `tau` in seconds, over an interval `dt`: frame-rate independent and free
9225/// of overshoot. Used to smooth the abscissa during a reorder.
9226fn spring_toward(current: f32, target: f32, dt: f32, tau: f32) -> f32 {
9227    let k = 1.0 - (-dt / tau.max(1e-4)).exp();
9228    current + (target - current) * k
9229}
9230
9231/// The focus history's maximum depth, for nested overlays' triggers.
9232const FOCUS_HISTORY_MAX: usize = 8;
9233
9234/// Resolves focus after a (re)build, from the focusables **present** this frame. When
9235/// `current` has **vanished**, an overlay having closed, focus returns to the most
9236/// recent present **trigger** in `history`, which is popped until one is found. The
9237/// transition is recorded: the old focus (`prev`), if still present and different from
9238/// the new one, is pushed as a candidate trigger, bounded by [`FOCUS_HISTORY_MAX`].
9239/// Returns the resolved focus. A **pure** function apart from the mutated `history` and
9240/// `prev` — testable without a window.
9241fn resolve_focus(
9242    current: Option<WidgetId>,
9243    present: &std::collections::HashSet<WidgetId>,
9244    history: &mut Vec<WidgetId>,
9245    prev: &mut Option<WidgetId>,
9246) -> Option<WidgetId> {
9247    let mut cur = current;
9248    if let Some(c) = cur {
9249        if !present.contains(&c) {
9250            cur = None;
9251            while let Some(cand) = history.pop() {
9252                if present.contains(&cand) {
9253                    cur = Some(cand);
9254                    break;
9255                }
9256            }
9257        }
9258    }
9259    if *prev != cur {
9260        if let Some(old) = *prev {
9261            if present.contains(&old) && Some(old) != cur {
9262                history.push(old);
9263                if history.len() > FOCUS_HISTORY_MAX {
9264                    history.remove(0);
9265                }
9266            }
9267        }
9268        *prev = cur;
9269    }
9270    cur
9271}
9272
9273/// Paints the dragged header's **lifted card**, the ghost, into `scene` — unclipped —
9274/// at `card`: a drop shadow, a **faithful face** from the header's already-translated
9275/// primitives, or a solid one as a fallback when `ghost` is empty, then a `primary`
9276/// border. A pure function, testable without a GPU. The neighbours' sliding happens
9277/// upstream, in `reflow_reorder_columns`.
9278fn draw_ghost_card(scene: &mut Scene, theme: &Theme, card: Rect, ghost: &[Primitive]) {
9279    use drag_preview::{BORDER_ALPHA, BORDER_WIDTH, SHADOW_ALPHA, SHADOW_BLUR, SHADOW_OFFSET_Y};
9280    scene.set_clip(Rect::UNBOUNDED);
9281    // The shadow colour comes from **the theme** and can be overridden — the same role
9282    // as `Button`'s shadow; only the geometry, offset, blur and opacity, is a local
9283    // constant.
9284    let shadow = theme.scheme.shadow.with_alpha(SHADOW_ALPHA);
9285    scene.shadow(
9286        card.translate(0.0, SHADOW_OFFSET_Y),
9287        shadow,
9288        theme.radius,
9289        SHADOW_BLUR,
9290    );
9291    let border = theme.primary.fade(BORDER_ALPHA);
9292    if ghost.is_empty() {
9293        scene.draw_rect(card, theme.surface, theme.radius, BORDER_WIDTH, border);
9294    } else {
9295        scene.draw_rect(card, theme.surface, theme.radius, 0.0, Color::TRANSPARENT);
9296        for primitive in ghost {
9297            scene.push_primitive(primitive.clone());
9298        }
9299        scene.draw_rect(card, Color::TRANSPARENT, theme.radius, BORDER_WIDTH, border);
9300    }
9301}
9302
9303/// How a reorderable moves while it is carried: the two gestures the shell knows.
9304#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9305enum ReorderMotion {
9306    /// A table's column: a continuous slide along x following the pointer, dropped into
9307    /// the place of the column under it.
9308    Columns,
9309    /// A list's row or a board's card: inserted before or after a slot along the axis,
9310    /// with an insertion line, a gap opening there, and the nearest slot past either end.
9311    Slots(ReorderAxis),
9312}
9313
9314impl ReorderMotion {
9315    /// Which of the two a reorderable is, from its two answers: every vertical one inserts,
9316    /// and a horizontal one only when it says so — a list's row does, a table's column does
9317    /// not.
9318    fn of(axis: ReorderAxis, inserts: bool) -> Self {
9319        match axis {
9320            ReorderAxis::Vertical => Self::Slots(ReorderAxis::Vertical),
9321            ReorderAxis::Horizontal if inserts => Self::Slots(ReorderAxis::Horizontal),
9322            ReorderAxis::Horizontal => Self::Columns,
9323        }
9324    }
9325}
9326
9327/// How `widget` moves while it is carried — [`ReorderMotion::of`] its two answers.
9328fn reorder_motion<Msg>(widget: &dyn Widget<Msg>) -> ReorderMotion {
9329    ReorderMotion::of(widget.reorder_axis(), widget.reorder_inserts())
9330}
9331
9332/// Where the ghost is drawn relative to the box it was lifted from, given the finger's
9333/// `travel` since the press.
9334///
9335/// - A **column** follows the pointer along x, with a slight lift.
9336/// - A **card** follows it in both directions: a board carries it across columns.
9337/// - A **horizontal list's row** follows it along x only. The reference keeps a list's
9338///   proxy in its lane on either axis; a vertical list here shares the board's freedom
9339///   because the two are one path, and a horizontal list has no board to share it with.
9340fn ghost_offset(motion: ReorderMotion, travel: (f32, f32)) -> (f32, f32) {
9341    match motion {
9342        ReorderMotion::Columns => (travel.0, drag_preview::LIFT_Y),
9343        ReorderMotion::Slots(ReorderAxis::Vertical) => travel,
9344        ReorderMotion::Slots(ReorderAxis::Horizontal) => (travel.0, 0.0),
9345    }
9346}
9347
9348/// The geometry of a reorder preview's **insertion line**: a thin band of thickness
9349/// `thickness`, centred on the target edge **where the insertion will happen**.
9350///
9351/// Along a **vertical** list: the **top** edge (inserting before, the upper half hovered)
9352/// or the **bottom** one (inserting after, `after = true`, the lower half hovered),
9353/// spanning the full width. Along a **horizontal** list the band stands on its end,
9354/// spanning the full height at the left or right edge — and *after* is the right edge
9355/// only when the list reads left to right: under `rtl` what follows a row is on its left.
9356/// A pure function, testable without a GPU.
9357fn drop_insertion_line(
9358    target: Rect,
9359    thickness: f32,
9360    after: bool,
9361    axis: ReorderAxis,
9362    rtl: bool,
9363) -> Rect {
9364    match axis {
9365        ReorderAxis::Vertical => {
9366            let edge = if after {
9367                target.y + target.height
9368            } else {
9369                target.y
9370            };
9371            Rect::new(target.x, edge - thickness * 0.5, target.width, thickness)
9372        }
9373        ReorderAxis::Horizontal => {
9374            let edge = if after != rtl {
9375                target.x + target.width
9376            } else {
9377                target.x
9378            };
9379            Rect::new(edge - thickness * 0.5, target.y, thickness, target.height)
9380        }
9381    }
9382}
9383
9384/// Which axis a scroll gesture is **claimed by**, decided once when the finger passes the
9385/// threshold: `true` for vertical.
9386///
9387/// An area that can only go one way claims that one, whatever the finger did — a diagonal
9388/// flick down a list is a scroll down, not a refusal. An area that can go both ways takes
9389/// the direction the finger actually went in, ties going to vertical, which is the way a
9390/// page reads. The loser gets nothing for the rest of the gesture: this is the reference's
9391/// arena, where a vertical and a horizontal recogniser compete and only one wins.
9392fn claim_axis(dx: f32, dy: f32, can_x: bool, can_y: bool) -> bool {
9393    match (can_x, can_y) {
9394        (true, false) => false,
9395        (false, true) => true,
9396        _ => dy.abs() >= dx.abs(),
9397    }
9398}
9399
9400/// Which of the scrollables under the finger a gesture is **for**: the innermost one that
9401/// can go the way the finger went (`down` = along the vertical), given `chain` innermost
9402/// first and `under` the area the press landed on.
9403///
9404/// A strip that only slides across, sitting in a page that only scrolls down, is not what a
9405/// drag downwards is about — the page behind it is, and the reference's arena gives it to
9406/// the page for exactly that reason. Where nothing in the stack can go that way, the area
9407/// under the finger keeps the gesture and goes its own way: a lone recogniser in an arena
9408/// still wins.
9409fn claim_area(
9410    chain: &[frus_widgets::Scrollable],
9411    under: WidgetId,
9412    down: bool,
9413) -> Option<frus_widgets::Scrollable> {
9414    chain
9415        .iter()
9416        // A refresh area listening above takes a pull downwards even with nothing to
9417        // scroll, which is the reference's one exception to the same rule; the physics
9418        // may say never, or always (milestone 649).
9419        .find(|a| a.takes_user_scroll(down))
9420        .or_else(|| {
9421            chain
9422                .iter()
9423                .find(|a| a.id == under && a.physics.user() != frus_widgets::UserScroll::Never)
9424        })
9425        .copied()
9426}
9427
9428/// Gets the bytes at `url` and hands them to `deliver`, once.
9429///
9430/// Registered as the process's [`frus_widgets::ImageFetcher`] when the shell is built with
9431/// `net`. It spawns and forgets: nothing here waits for the answer, and the store the
9432/// callback writes into is what a later frame reads.
9433///
9434/// The two spawns are the two the rest of this file uses for every other effect — the
9435/// shared executor natively, the browser on the Web.
9436///
9437/// It carries a **deadline**, and that is not politeness. An image still in flight keeps
9438/// the interface redrawing — that is how the frame showing it ever happens — so a
9439/// request that never answers would leave the application repainting at full rate for
9440/// ever, on somebody's battery. A request without an answer has to become one with a
9441/// failure.
9442#[cfg(feature = "net")]
9443fn fetch_image_bytes(
9444    url: &str,
9445    deliver: Box<dyn FnOnce(Result<Vec<u8>, String>) + Send + 'static>,
9446) {
9447    /// Long enough for a photograph on a slow connection, short enough that a dead
9448    /// server does not pin a screen redrawing until the user closes the application.
9449    const DEADLINE: std::time::Duration = std::time::Duration::from_secs(30);
9450
9451    let url = url.to_string();
9452    let run = async move {
9453        let result = crate::net::Request::get(url)
9454            .timeout(DEADLINE)
9455            .send_bytes()
9456            .await;
9457        deliver(result.map_err(|e| e.to_string()));
9458    };
9459    #[cfg(not(web))]
9460    crate::runtime::spawn(run).detach();
9461    #[cfg(web)]
9462    wasm_bindgen_futures::spawn_local(run);
9463}
9464
9465#[cfg(test)]
9466mod debug_keys_tests {
9467    use super::*;
9468
9469    #[test]
9470    fn the_graphs_keep_the_latest_frames_and_mark_the_late_ones() {
9471        let mut history = FrameHistory::default();
9472        for i in 0..100 {
9473            history.push(&crate::frame_stats::FrameCosts {
9474                build: Some(1.0),
9475                layout_paint: i as f32,
9476                render: 5.0,
9477                ..Default::default()
9478            });
9479        }
9480        assert_eq!(history.ui.len(), HISTORY, "the oldest frames go");
9481        assert_eq!(
9482            history.ui.back().copied(),
9483            Some(100.0),
9484            "the newest is last"
9485        );
9486        let bars = FrameHistory::bars(&history.render, 36.0);
9487        assert!(bars.iter().all(|(_, over)| !over), "5 ms is within budget");
9488        let ui = FrameHistory::bars(&history.ui, 36.0);
9489        assert_eq!(
9490            ui.last().copied(),
9491            Some((36.0, true)),
9492            "over the top fills the graph"
9493        );
9494        let half = FrameHistory::bars(&[BUDGET_MS].into_iter().collect(), 36.0);
9495        assert!((half[0].0 - 18.0).abs() < 0.01 && !half[0].1, "{half:?}");
9496    }
9497
9498    #[test]
9499    fn the_platform_goes_round_as_the_reference_does() {
9500        use frus_widgets::TargetPlatform as P;
9501        let mut seen = vec![P::Android];
9502        for _ in 0..6 {
9503            seen.push(next_platform(*seen.last().unwrap()));
9504        }
9505        assert_eq!(
9506            seen,
9507            [
9508                P::Android,
9509                P::Ios,
9510                P::Windows,
9511                P::MacOs,
9512                P::Linux,
9513                P::Fuchsia,
9514                P::Android
9515            ]
9516        );
9517    }
9518
9519    #[test]
9520    fn the_dumps_say_what_there_is() {
9521        let id = WidgetId::from_u64(0x1234_5678);
9522        let other = WidgetId::from_u64(0x9);
9523        let nodes = [frus_widgets::InspectorNode {
9524            id,
9525            rect: Rect::new(10.0, 20.0, 100.0, 40.0),
9526            name: "Button",
9527            depth: 3,
9528        }];
9529        let focus = dump_focus(&nodes, &[id, other], Some(id));
9530        assert_eq!(focus, "▶ Button  10,20  100×40  #12345678\n  #00000009\n");
9531        assert_eq!(dump_focus(&nodes, &[], None), "  (nothing focusable)\n");
9532
9533        let semantics = [(
9534            id,
9535            Rect::new(0.0, 0.0, 50.0, 20.0),
9536            frus_widgets::SemanticsProperties::new(frus_widgets::Role::Button).label("Save"),
9537        )];
9538        let dump = dump_semantics(&semantics);
9539        assert!(dump.starts_with("  Button \"Save\""), "{dump}");
9540        assert!(dump.ends_with("0,0  50×20\n"), "{dump}");
9541    }
9542}
9543
9544#[cfg(test)]
9545mod tests {
9546    use super::{
9547        build_view, claim_area, claim_axis, draw_ghost_card, drop_insertion_line, fling_velocity,
9548        gesture_was_a_tap, ghost_offset, install_ambient, reorder_motion, resolve_focus,
9549        spring_toward, Drag, Point, Rect, ReorderAxis, ReorderMotion, Scene, Theme,
9550        VelocityEstimate, PRECISE_SLOP, TOUCH_SLOP,
9551    };
9552    use super::{clipboard_command, ClipCommand, KeyCode, PhysicalKey, WinitKey};
9553    use super::{collect_ids, find_widget, MediaQuery};
9554    use frus_widgets::Locale;
9555
9556    /// **Which systems put the bar away after its Select all** (milestone 625): the three
9557    /// desktops, as the reference's field does; on a phone the bar stays.
9558    #[test]
9559    fn select_all_hides_the_bar_on_the_desktops() {
9560        use frus_widgets::TargetPlatform as P;
9561        for p in [P::MacOs, P::Linux, P::Windows] {
9562            assert!(super::select_all_hides_bar(p), "{p:?}");
9563        }
9564        for p in [P::Android, P::Ios, P::Fuchsia] {
9565            assert!(!super::select_all_hides_bar(p), "{p:?}");
9566        }
9567    }
9568
9569    /// **The keys that open the selection bar** (milestone 568): the context-menu key, and
9570    /// F10 with Shift — F10 alone is the menu bar's key in other toolkits and asks for nothing
9571    /// here, and no ordinary key does.
9572    #[test]
9573    fn the_context_menu_key_and_shift_f10_open_the_bar() {
9574        use super::opens_selection_bar;
9575        assert!(opens_selection_bar(
9576            &WinitKey::Named(winit::keyboard::NamedKey::ContextMenu),
9577            false
9578        ));
9579        assert!(opens_selection_bar(
9580            &WinitKey::Named(winit::keyboard::NamedKey::F10),
9581            true
9582        ));
9583        assert!(!opens_selection_bar(
9584            &WinitKey::Named(winit::keyboard::NamedKey::F10),
9585            false
9586        ));
9587        assert!(!opens_selection_bar(
9588            &WinitKey::Named(winit::keyboard::NamedKey::Enter),
9589            true
9590        ));
9591        assert!(!opens_selection_bar(&character("m"), true));
9592    }
9593
9594    /// **What a Copy from the selection bar leaves behind** (milestone 568): the selection
9595    /// collapsed to its end, whichever way it was made — dragged backwards puts the caret at
9596    /// the far end, and Copy puts it at the *end* of the text, not where the caret was.
9597    #[test]
9598    fn a_copy_collapses_the_selection_to_its_end() {
9599        use frus_widgets::Edit;
9600        let forwards = Edit {
9601            cursor: 11,
9602            anchor: Some(6),
9603            composing: None,
9604        };
9605        let backwards = Edit {
9606            cursor: 6,
9607            anchor: Some(11),
9608            composing: None,
9609        };
9610        let expect = Edit {
9611            cursor: 11,
9612            anchor: None,
9613            composing: None,
9614        };
9615        assert_eq!(super::collapsed_to_end(forwards), expect);
9616        assert_eq!(super::collapsed_to_end(backwards), expect);
9617        // Nothing selected: the caret stays where it is.
9618        let caret = Edit {
9619            cursor: 3,
9620            anchor: None,
9621            composing: None,
9622        };
9623        assert_eq!(super::collapsed_to_end(caret), caret);
9624        // An anchor on the caret is no selection either.
9625        let empty = Edit {
9626            cursor: 3,
9627            anchor: Some(3),
9628            composing: Some((1, 2)),
9629        };
9630        assert_eq!(super::collapsed_to_end(empty).anchor, None);
9631        assert_eq!(super::collapsed_to_end(empty).cursor, 3);
9632    }
9633
9634    /// **A field with a clear button kept the keyboard only until the first letter**
9635    /// (milestone 510). Whether the focused widget takes typing was asked of a caret
9636    /// hit test at the corner of a field one pixel wide; a field whose clickable suffix
9637    /// covers that pixel — the × it shows once it holds text — answered no, and the
9638    /// keyboard was closed mid-word. Asked of the widget, the answer is the field's.
9639    #[test]
9640    fn a_field_with_a_clear_button_still_wants_the_keyboard() {
9641        use frus_widgets::{Container, Icons, TextField};
9642        let clearable = TextField::<()>::new("F")
9643            .suffix_icon(Icons::CLOSE)
9644            .on_suffix(());
9645        assert!(
9646            super::wants_keyboard(&clearable),
9647            "the field with its clear button"
9648        );
9649        assert!(
9650            !super::wants_keyboard(&TextField::<()>::new("F").enabled(false)),
9651            "a disabled field takes no typing"
9652        );
9653        assert!(
9654            !super::wants_keyboard(&Container::<()>::new()),
9655            "and what is not a field wants no keyboard"
9656        );
9657    }
9658
9659    /// **Left and right moved the focus, not the caret, in a field with text in it**
9660    /// (milestone 570). Whether the focused widget is a text field was asked of a caret probe
9661    /// at a corner of the field one pixel wide, and a field that holds text shows a clear
9662    /// button over that corner: the probe said no caret goes there, so the arrows navigated to
9663    /// the next control. In an empty field, which has no button, they worked — which is how it
9664    /// went unseen. The probe still answers no for such a field; the question is the widget's.
9665    #[test]
9666    fn a_field_with_a_clear_button_is_a_text_field_and_keeps_its_arrows() {
9667        use frus_widgets::{Container, Icons, TextField};
9668        let clearable = TextField::<()>::new("hello")
9669            .suffix_icon(Icons::CLOSE)
9670            .on_suffix(());
9671        assert!(
9672            frus_widgets::Widget::<()>::cursor_at(&clearable, 0.0, 0.0, 1.0, 0).is_none(),
9673            "the trap: the probe finds no caret at the corner a clear button covers"
9674        );
9675        assert!(
9676            super::is_text_field(&clearable),
9677            "and it is a text field all the same"
9678        );
9679        assert!(
9680            super::is_text_field(&TextField::<()>::new("")),
9681            "empty, too"
9682        );
9683        assert!(
9684            !super::is_text_field(&Container::<()>::new()),
9685            "what is not a field leaves its arrows to focus navigation"
9686        );
9687    }
9688
9689    /// **A text that can only be read does not raise the keyboard.** A read-only field and a
9690    /// selectable paragraph are both text fields — they have a caret, and the arrows are
9691    /// theirs — but the software keyboard is for typing, and sliding it up over the words a
9692    /// reader has come to select hides the words. Before milestone 573 a read-only field
9693    /// raised it all the same.
9694    #[test]
9695    fn a_text_that_can_only_be_read_wants_no_keyboard() {
9696        use frus_widgets::{Text, TextField};
9697        let read_only = TextField::<()>::new("REF-4417").read_only();
9698        assert!(super::is_text_field(&read_only), "it has a caret");
9699        assert!(
9700            !super::wants_keyboard(&read_only),
9701            "and no use for a keyboard"
9702        );
9703
9704        let selectable = Text::new("ORDER-4417").selectable();
9705        assert!(
9706            super::is_text_field::<()>(&selectable),
9707            "the arrows move its caret"
9708        );
9709        assert!(!super::wants_keyboard::<()>(&selectable));
9710
9711        assert!(
9712            super::wants_keyboard(&TextField::<()>::new("typed")),
9713            "and a field that can be typed into still does"
9714        );
9715        assert!(
9716            !super::is_text_field::<()>(&Text::new("ORDER-4417")),
9717            "an ordinary text leaves its arrows to focus navigation"
9718        );
9719    }
9720
9721    fn character(c: &str) -> WinitKey {
9722        WinitKey::Character(c.into())
9723    }
9724
9725    /// Ctrl+C/X/V are the clipboard, in either case — Caps Lock or Shift is still a
9726    /// copy — and the same letters without Ctrl are only letters.
9727    #[test]
9728    fn ctrl_c_x_v_are_the_clipboard_and_the_letters_alone_are_not() {
9729        let any = PhysicalKey::Code(KeyCode::KeyQ);
9730        for (letter, command) in [
9731            ("c", ClipCommand::Copy),
9732            ("X", ClipCommand::Cut),
9733            ("v", ClipCommand::Paste),
9734        ] {
9735            assert_eq!(
9736                clipboard_command(&character(letter), any, true),
9737                Some(command)
9738            );
9739            assert_eq!(
9740                clipboard_command(&character(letter), any, false),
9741                None,
9742                "{letter}"
9743            );
9744        }
9745        assert_eq!(clipboard_command(&character("a"), any, true), None);
9746    }
9747
9748    /// **Android's Copy, Cut and Paste keys arrive by physical code alone** — winit gives
9749    /// them no logical key — and they are the clipboard with no modifier held. A check
9750    /// of the logical key alone ignored them (milestone 509).
9751    #[test]
9752    fn a_keyboards_own_clipboard_keys_work_by_their_physical_code() {
9753        let unidentified = WinitKey::Unidentified(winit::keyboard::NativeKey::Unidentified);
9754        for (code, command) in [
9755            (KeyCode::Copy, ClipCommand::Copy),
9756            (KeyCode::Cut, ClipCommand::Cut),
9757            (KeyCode::Paste, ClipCommand::Paste),
9758        ] {
9759            assert_eq!(
9760                clipboard_command(&unidentified, PhysicalKey::Code(code), false),
9761                Some(command),
9762                "{code:?}"
9763            );
9764        }
9765        // And the named keys a desktop keyboard reports for the same thing.
9766        let named = WinitKey::Named(super::NamedKey::Paste);
9767        let any = PhysicalKey::Code(KeyCode::KeyQ);
9768        assert_eq!(
9769            clipboard_command(&named, any, false),
9770            Some(ClipCommand::Paste)
9771        );
9772    }
9773
9774    /// **A paste lands only in the field that asked, while that field still has the
9775    /// focus** — on the Web the clipboard answers after the key press, and by then the
9776    /// reader may have moved to another field or to none. And a clipboard holding no
9777    /// text pastes nothing, rather than typing an empty string over the selection
9778    /// (milestone 526).
9779    #[test]
9780    fn a_paste_lands_in_the_field_that_asked_only_while_it_has_the_focus() {
9781        use super::clip::Pasted;
9782        let field = WidgetId::from_u64(1);
9783        let other = WidgetId::from_u64(2);
9784        let pasted = Pasted {
9785            into: field,
9786            text: "copied".into(),
9787        };
9788        assert!(pasted.lands(Some(field)));
9789        assert!(
9790            !pasted.lands(Some(other)),
9791            "the focus moved to another field"
9792        );
9793        assert!(!pasted.lands(None), "the focus left every field");
9794        let nothing = Pasted {
9795            into: field,
9796            text: String::new(),
9797        };
9798        assert!(!nothing.lands(Some(field)), "an empty clipboard");
9799    }
9800
9801    /// **A paste answered later is queued for its field, and a frame is asked for.** Two
9802    /// pastes asked in two fields and answered in the other order come out in the order
9803    /// the answers came, each still naming the field that asked; each answer wakes the
9804    /// window once; and nothing is taken twice (milestone 526).
9805    #[test]
9806    fn a_paste_answered_later_waits_for_the_frame_that_takes_it() {
9807        use super::clip::{Answers, Pasted};
9808        use std::cell::Cell;
9809        use std::rc::Rc;
9810        let first = WidgetId::from_u64(1);
9811        let second = WidgetId::from_u64(2);
9812        let answers = Answers::default();
9813        let wakes = Rc::new(Cell::new(0));
9814        let wake = || {
9815            let wakes = Rc::clone(&wakes);
9816            move || wakes.set(wakes.get() + 1)
9817        };
9818        let to_first = answers.answer_for(first, wake());
9819        let to_second = answers.answer_for(second, wake());
9820        assert_eq!(answers.take(), None, "nothing is answered yet");
9821        assert_eq!(wakes.get(), 0, "asking is not answering");
9822
9823        to_second("later".into());
9824        assert_eq!(wakes.get(), 1);
9825        to_first("sooner".into());
9826        assert_eq!(wakes.get(), 2);
9827
9828        assert_eq!(
9829            answers.take(),
9830            Some(Pasted {
9831                into: second,
9832                text: "later".into()
9833            })
9834        );
9835        assert_eq!(
9836            answers.take(),
9837            Some(Pasted {
9838                into: first,
9839                text: "sooner".into()
9840            })
9841        );
9842        assert_eq!(answers.take(), None, "each answer is taken once");
9843    }
9844
9845    /// An application whose whole interface lives **inside a deferred subtree** — which is
9846    /// what any application with an `AppBar` is, since a bar defers its own composition
9847    /// until the theme is known.
9848    ///
9849    /// It exists because nothing else in this repo can stand in for one: the shell's own
9850    /// types need a window and an event-loop proxy, so the only piece of the frame a test
9851    /// can hold is the part that turns a `view` into a tree. That part is where two
9852    /// milestones' worth of bugs were.
9853    struct Deferred;
9854
9855    impl crate::Application for Deferred {
9856        type Message = ();
9857
9858        fn update(&mut self, _message: ()) -> crate::Command<()> {
9859            crate::Command::none()
9860        }
9861
9862        fn view(&self, _theme: &Theme) -> Box<dyn frus_widgets::Widget<()>> {
9863            Box::new(frus_widgets::ThemeBuilder::new(|_: &Theme| {
9864                frus_widgets::Flex::column()
9865                    .width(300.0)
9866                    .height(80.0)
9867                    .child(
9868                        frus_widgets::TextField::new("hi")
9869                            .width(200.0)
9870                            .on_input(|_| ()),
9871                    )
9872            }))
9873        }
9874    }
9875
9876    /// The surface every frame installs, so a build outside one measures the way a build
9877    /// inside one does.
9878    fn surfaced<R>(f: impl FnOnce() -> R) -> R {
9879        MediaQuery::new(frus_widgets::Size::new(300.0, 80.0)).scope(f)
9880    }
9881
9882    /// **A shell installs the reader's language** — the assertion that keeps milestone 454
9883    /// from being a resolution nobody ever runs.
9884    ///
9885    /// `locale::of()` answers `en` with nothing installed, exactly as
9886    /// `localizations::of()` answers English, and for the same reason: an application that
9887    /// says nothing must keep working. That default is also what would hide the wiring
9888    /// being missing, so this drives the shell's own reading of the trait rather than
9889    /// calling `resolve` directly — milestone 408's bug, and milestone 449's guard against
9890    /// it.
9891    #[test]
9892    fn a_shell_installs_the_reader_s_language() {
9893        /// An application with three languages, and a switch of its own it may or may not
9894        /// have been told to use.
9895        struct Speaks(Option<Locale>);
9896
9897        impl crate::Application for Speaks {
9898            type Message = ();
9899
9900            fn update(&mut self, _message: ()) -> crate::Command<()> {
9901                crate::Command::none()
9902            }
9903
9904            fn view(&self, _theme: &Theme) -> Box<dyn frus_widgets::Widget<()>> {
9905                Box::new(frus_widgets::Flex::<()>::column())
9906            }
9907
9908            fn supported_locales(&self) -> Vec<Locale> {
9909                vec![
9910                    Locale::new("en"),
9911                    Locale::new("fr"),
9912                    Locale::with_country("fr", "CA"),
9913                ]
9914            }
9915
9916            fn locale(&self) -> Option<Locale> {
9917                self.0.clone()
9918            }
9919        }
9920
9921        // The device speaks Belgian French; the application has French, so it wins.
9922        install_ambient(&Speaks(None), &[Locale::with_country("fr", "BE")]);
9923        assert_eq!(frus_widgets::locale::of(), Locale::new("fr"));
9924
9925        // A platform that reported nothing is not a failure: the application's own first
9926        // choice stands.
9927        install_ambient(&Speaks(None), &[]);
9928        assert_eq!(frus_widgets::locale::of(), Locale::new("en"));
9929
9930        // And an application with a language menu of its own outranks the device — still
9931        // resolved, so asking for one it does not have gives the nearest thing it does.
9932        install_ambient(
9933            &Speaks(Some(Locale::with_country("fr", "FR"))),
9934            &[Locale::new("en")],
9935        );
9936        assert_eq!(frus_widgets::locale::of(), Locale::new("fr"));
9937
9938        // An application that says nothing at all is where it was: English.
9939        install_ambient(&Deferred, &[Locale::new("fr")]);
9940        assert_eq!(
9941            frus_widgets::locale::of(),
9942            Locale::new("en"),
9943            "it supports only English, so French resolves to it"
9944        );
9945    }
9946
9947    /// **A shell installs the application's words** — which is the assertion that keeps
9948    /// milestone 449 from being a table nobody ever reads.
9949    ///
9950    /// `localizations::of` answers English with nothing installed, so every widget test
9951    /// and every golden would pass whether or not the shell ever called `install`. The
9952    /// default is what makes the feature safe to add; it is also what would hide the
9953    /// wiring being missing. So this drives the shell's own reading of the trait.
9954    #[test]
9955    fn a_shell_installs_the_application_s_words() {
9956        struct Fr;
9957
9958        impl frus_widgets::localizations::Localizations for Fr {
9959            fn back_button_label(&self) -> &str {
9960                "Retour"
9961            }
9962
9963            fn first_day_of_week_index(&self) -> usize {
9964                1
9965            }
9966        }
9967
9968        struct Speaks;
9969
9970        impl crate::Application for Speaks {
9971            type Message = ();
9972
9973            fn update(&mut self, _message: ()) -> crate::Command<()> {
9974                crate::Command::none()
9975            }
9976
9977            fn view(&self, _theme: &Theme) -> Box<dyn frus_widgets::Widget<()>> {
9978                Box::new(frus_widgets::Flex::<()>::column())
9979            }
9980
9981            fn localizations(&self) -> Option<std::rc::Rc<dyn frus_widgets::Localizations>> {
9982                Some(std::rc::Rc::new(Fr))
9983            }
9984        }
9985
9986        install_ambient(&Speaks, &[]);
9987        assert_eq!(
9988            frus_widgets::localizations::of().back_button_label(),
9989            "Retour",
9990            "the shell read the application's table and installed it"
9991        );
9992        assert_eq!(
9993            frus_widgets::localizations::of().first_day_of_week_index(),
9994            1
9995        );
9996
9997        // And an application that says nothing leaves whatever is in force alone rather
9998        // than forcing English back on top of a table installed elsewhere.
9999        install_ambient(&Deferred, &[]);
10000        assert_eq!(
10001            frus_widgets::localizations::of().back_button_label(),
10002            "Retour",
10003            "saying nothing is not the same as saying English"
10004        );
10005    }
10006
10007    /// **The application's scroll behaviour is the one at the root** (milestone 616), and
10008    /// one that says nothing gets the behaviour that follows the theme.
10009    #[test]
10010    fn the_scroll_behaviour_at_the_root_is_the_applications() {
10011        struct Quiet;
10012
10013        impl crate::Application for Quiet {
10014            type Message = ();
10015
10016            fn update(&mut self, _message: ()) -> crate::Command<()> {
10017                crate::Command::none()
10018            }
10019
10020            fn view(&self, _theme: &Theme) -> Box<dyn frus_widgets::Widget<()>> {
10021                Box::new(frus_widgets::Flex::<()>::column())
10022            }
10023
10024            fn scroll_behavior(&self) -> frus_widgets::ScrollBehavior {
10025                frus_widgets::ScrollBehavior::material().with_overscroll(false)
10026            }
10027        }
10028
10029        struct Silent;
10030
10031        impl crate::Application for Silent {
10032            type Message = ();
10033
10034            fn update(&mut self, _message: ()) -> crate::Command<()> {
10035                crate::Command::none()
10036            }
10037
10038            fn view(&self, _theme: &Theme) -> Box<dyn frus_widgets::Widget<()>> {
10039                Box::new(frus_widgets::Flex::<()>::column())
10040            }
10041        }
10042
10043        install_ambient(&Quiet, &[]);
10044        assert_eq!(
10045            frus_widgets::ScrollConfiguration::of(),
10046            frus_widgets::ScrollBehavior::material().with_overscroll(false)
10047        );
10048        install_ambient(&Silent, &[]);
10049        assert_eq!(
10050            frus_widgets::ScrollConfiguration::of(),
10051            frus_widgets::ScrollBehavior::material()
10052        );
10053    }
10054
10055    /// A tree the shell hands to a traversal is **ready to be traversed**.
10056    ///
10057    /// Milestones 415 and 416 were the same bug found twice, in the two places this shell
10058    /// turns a `view` into a tree. A deferred subtree has no children until something has
10059    /// built it, so an unprepared tree reports **one** identity — its root — and every
10060    /// widget in the application is invisible to `collect_ids`, to `find_widget`, and to
10061    /// everything downstream of them: the mount and leave fades, the focus, the caret.
10062    ///
10063    /// The failure was silence in both places, which is why it went unnoticed twice. This
10064    /// is the assertion that would have caught it, and it needs no window.
10065    #[test]
10066    fn a_view_is_built_ready_to_be_read() {
10067        let theme = Theme::default();
10068        let prepared =
10069            surfaced(|| build_view(&Deferred, &theme, &frus_widgets::Runtime::default()));
10070        let ids = collect_ids(prepared.as_ref());
10071        assert!(
10072            ids.len() > 1,
10073            "an application inside a deferred subtree has identities to mount: {ids:?}"
10074        );
10075        // And every one of them is reachable, which is what the burst path needs.
10076        for id in &ids {
10077            assert!(
10078                find_widget(prepared.as_ref(), *id).is_some(),
10079                "{id:?} was counted but cannot be found"
10080            );
10081        }
10082    }
10083
10084    /// The same view, unprepared, is the state both call sites used to read.
10085    #[test]
10086    #[should_panic(expected = "before it was built")]
10087    fn an_unprepared_view_is_the_state_that_broke() {
10088        let theme = Theme::default();
10089        let raw = surfaced(|| crate::Application::view(&Deferred, &theme));
10090        let _ = collect_ids(raw.as_ref());
10091    }
10092
10093    /// A press on a **dismissible** row that never moved is a tap, and the widget under
10094    /// the finger owes it a click (reported on a device: a task row's avatar opened
10095    /// nothing, while a control a few hundred pixels away in the same card worked).
10096    ///
10097    /// The row registers a swipe on the press, in case the finger is about to slide
10098    /// sideways. When it does not, the swipe has to stand down. Every other gesture
10099    /// here already did; this one was left out of the list.
10100    #[test]
10101    fn a_swipe_that_never_started_is_still_a_tap() {
10102        let item = frus_widgets::Dismissable {
10103            id: WidgetId::from_u64(1),
10104            rect: Rect::new(0.0, 0.0, 300.0, 62.0),
10105            spec: frus_widgets::DismissSpec::default(),
10106        };
10107        let dismiss = |moved| Drag::Dismiss {
10108            item,
10109            last: Point::new(0.0, 0.0),
10110            moved,
10111        };
10112        assert!(
10113            gesture_was_a_tap(Some(&dismiss(false))),
10114            "a swipe under the threshold leaves the click alone"
10115        );
10116        assert!(
10117            !gesture_was_a_tap(Some(&dismiss(true))),
10118            "a swipe that ran has already been answered and must not also click"
10119        );
10120        // A scroll, the variant that was already right — and the one that masked this
10121        // bug, since a list long enough to scroll claims the press before the swipe can.
10122        let scroll = Drag::Scroll {
10123            id: WidgetId::from_u64(2),
10124            last: Point::new(0.0, 0.0),
10125            moved: false,
10126            carried: (0.0, 0.0),
10127            dismiss: None,
10128            axis: None,
10129            pan: None,
10130        };
10131        assert!(gesture_was_a_tap(Some(&scroll)));
10132        // A press on a sheet's panel likewise: a button in the sheet still clicks, and a
10133        // sheet that was dragged does not also click what it was dragged by (515).
10134        let sheet = |moved| Drag::Sheet {
10135            id: WidgetId::from_u64(4),
10136            last: Point::new(0.0, 0.0),
10137            moved,
10138            available: 800.0,
10139        };
10140        assert!(gesture_was_a_tap(Some(&sheet(false))));
10141        assert!(!gesture_was_a_tap(Some(&sheet(true))));
10142        // And a gesture that captures on the press, rather than at a threshold, never
10143        // becomes a tap however still the finger was.
10144        let select = Drag::TextSelect {
10145            id: WidgetId::from_u64(3),
10146            rect: Rect::new(0.0, 0.0, 10.0, 10.0),
10147        };
10148        assert!(!gesture_was_a_tap(Some(&select)));
10149        assert!(!gesture_was_a_tap(None), "no gesture, no verdict to give");
10150    }
10151
10152    /// A page that scrolls down must not drift sideways while it does it (reported on a
10153    /// device, 2026-08-16). No finger travels in a straight line, so an area that can go
10154    /// both ways has to pick one and keep it.
10155    #[test]
10156    fn a_scroll_gesture_is_claimed_by_one_axis() {
10157        // Both axes available: the direction the finger went in wins, and a little wobble
10158        // across it changes nothing.
10159        assert!(claim_axis(6.0, 40.0, true, true), "down the page");
10160        assert!(!claim_axis(40.0, 6.0, true, true), "across it");
10161        // A tie reads as vertical, which is the way a page reads.
10162        assert!(claim_axis(10.0, 10.0, true, true));
10163        // One axis only: it takes the gesture whatever the finger did, or a diagonal flick
10164        // down a list would move nothing at all.
10165        assert!(claim_axis(40.0, 6.0, false, true), "a list only goes down");
10166        assert!(
10167            !claim_axis(6.0, 40.0, true, false),
10168            "a strip only goes across"
10169        );
10170    }
10171
10172    /// An area under the finger, `max_x` by `max_y` of content out of sight.
10173    fn area(id: u64, max_x: f32, max_y: f32) -> frus_widgets::Scrollable {
10174        frus_widgets::Scrollable {
10175            id: WidgetId::from_u64(id),
10176            viewport: Rect::new(0.0, 0.0, 100.0, 100.0),
10177            max_x,
10178            max_y,
10179            axis: frus_widgets::Axis::Vertical,
10180            physics: frus_widgets::ScrollPhysics::CLAMPING,
10181            overscroll: Some(frus_widgets::OverscrollIndicator::Glow),
10182            fling: frus_widgets::VelocityStrategy::Regression,
10183            refresh: None,
10184            page: None,
10185            reverse_x: false,
10186            reverse_y: false,
10187            host: None,
10188            keep_visible: None,
10189        }
10190    }
10191
10192    /// A strip that only slides sideways, sitting in a page that only scrolls down, must
10193    /// not swallow a drag downwards: the page behind it is what that drag is about
10194    /// (reported on a device, 2026-08-16).
10195    #[test]
10196    fn the_gesture_goes_to_the_area_that_can_take_it() {
10197        let strip = area(1, 400.0, 0.0);
10198        let page = area(2, 0.0, 900.0);
10199        let chain = [strip, page]; // innermost first, as the finger meets them
10200        assert_eq!(
10201            claim_area(&chain, strip.id, true).map(|a| a.id),
10202            Some(page.id),
10203            "down the page, over the strip"
10204        );
10205        assert_eq!(
10206            claim_area(&chain, strip.id, false).map(|a| a.id),
10207            Some(strip.id),
10208            "across the strip"
10209        );
10210        // Nothing behind it: the strip keeps the gesture and goes its own way, which is
10211        // what a lone recogniser in an arena does.
10212        assert_eq!(
10213            claim_area(&[strip], strip.id, true).map(|a| a.id),
10214            Some(strip.id)
10215        );
10216        // The innermost that can take it wins, not the outermost.
10217        let inner = area(3, 0.0, 200.0);
10218        assert_eq!(
10219            claim_area(&[inner, strip, page], inner.id, true).map(|a| a.id),
10220            Some(inner.id)
10221        );
10222    }
10223
10224    /// **The physics have the last word on a finger** (milestone 649): never scrollable
10225    /// takes none, even with content to reveal, and the area around it gets the drag;
10226    /// always scrollable takes one along its axis with nothing to reveal.
10227    #[test]
10228    fn the_physics_have_the_last_word_on_a_finger() {
10229        let mut inner = area(1, 0.0, 300.0);
10230        inner.physics = frus_widgets::ScrollPhysics::NEVER;
10231        assert!(!inner.accepts_user_offset((0.0, 0.0)));
10232        assert!(!inner.takes_user_scroll(true));
10233        let outer = area(2, 0.0, 600.0);
10234        let chain = [inner, outer];
10235        assert_eq!(
10236            claim_area(&chain, WidgetId::from_u64(1), true).map(|a| a.id),
10237            Some(WidgetId::from_u64(2)),
10238            "the page around it scrolls"
10239        );
10240        assert_eq!(
10241            claim_area(&[inner], WidgetId::from_u64(1), true).map(|a| a.id),
10242            None,
10243            "and alone it keeps nothing"
10244        );
10245
10246        let mut short = area(3, 0.0, 0.0);
10247        short.physics = frus_widgets::ScrollPhysics::ALWAYS;
10248        assert!(short.accepts_user_offset((0.0, 0.0)));
10249        assert!(short.takes_user_scroll(true), "along its axis");
10250        assert!(!short.takes_user_scroll(false), "and not across it");
10251        assert_eq!(
10252            claim_area(&[short, outer], WidgetId::from_u64(3), true).map(|a| a.id),
10253            Some(WidgetId::from_u64(3)),
10254            "a short list that always scrolls keeps the drag from the page"
10255        );
10256        let mut strip = short;
10257        strip.axis = frus_widgets::Axis::Horizontal;
10258        assert_eq!(
10259            claim_area(&[strip, outer], WidgetId::from_u64(3), true).map(|a| a.id),
10260            Some(WidgetId::from_u64(2)),
10261            "a strip that always scrolls across leaves a drag down to the page"
10262        );
10263        let mut both = short;
10264        both.axis = frus_widgets::Axis::Both;
10265        assert!(both.takes_user_scroll(false) && both.takes_user_scroll(true));
10266    }
10267
10268    /// An end-of-content glow on a page with nothing to scroll says something about the
10269    /// content that is not true (reported on a device, 2026-08-16). The reference takes the
10270    /// drag recognisers away outright when everything fits.
10271    #[test]
10272    fn an_area_whose_content_fits_takes_no_finger() {
10273        let fits = area(1, 0.0, 0.0);
10274        assert!(!fits.accepts_user_offset((0.0, 0.0)));
10275        // Unless it is already displaced — content that shrank under a scrolled offset has
10276        // to be draggable back into place.
10277        assert!(fits.accepts_user_offset((0.0, 12.0)));
10278        // Or a refresh area is listening above it: a list of two items still pulls down.
10279        let mut pullable = fits;
10280        pullable.refresh = Some(WidgetId::from_u64(9));
10281        assert!(pullable.accepts_user_offset((0.0, 0.0)));
10282        // A single pixel out of sight is content to reveal, and enough.
10283        assert!(area(1, 0.0, 1.0).accepts_user_offset((0.0, 0.0)));
10284        assert!(area(1, 1.0, 0.0).accepts_user_offset((0.0, 0.0)));
10285    }
10286    use frus_widgets::WidgetId;
10287    use std::collections::HashSet;
10288
10289    /// An estimate reading `velocity` px/s after travelling `offset` px.
10290    fn released(velocity: (f32, f32), offset: (f32, f32)) -> VelocityEstimate {
10291        VelocityEstimate {
10292            velocity: frus_widgets::Velocity::new(velocity.0, velocity.1),
10293            confidence: 1.0,
10294            duration: 0.05,
10295            offset,
10296        }
10297    }
10298
10299    #[test]
10300    fn a_fast_twitch_that_went_nowhere_is_not_a_fling() {
10301        // 2000 px/s, but the finger covered 3 px: a wobble on lift-off, not a throw.
10302        let (x, y) = fling_velocity(released((0.0, 2000.0), (0.0, 3.0)), TOUCH_SLOP);
10303        assert_eq!((x, y), (0.0, 0.0));
10304        // The same speed over a real distance is.
10305        let (_, y) = fling_velocity(released((0.0, 2000.0), (0.0, 60.0)), TOUCH_SLOP);
10306        assert_eq!(y, 2000.0);
10307    }
10308
10309    #[test]
10310    fn the_fling_gate_is_per_axis() {
10311        // A vertical swipe with the sideways wobble a thumb always adds: the wobble
10312        // must not fling the content horizontally.
10313        let (x, y) = fling_velocity(released((300.0, 1500.0), (5.0, 120.0)), TOUCH_SLOP);
10314        assert_eq!(x, 0.0, "the wobble is not a horizontal fling");
10315        assert_eq!(y, 1500.0, "the swipe still flings vertically");
10316    }
10317
10318    #[test]
10319    fn a_precise_pointer_needs_almost_no_travel() {
10320        // The same 3 px that a finger's slop rejects is a deliberate mouse drag.
10321        let estimate = released((0.0, 900.0), (0.0, 3.0));
10322        assert_eq!(fling_velocity(estimate, TOUCH_SLOP).1, 0.0);
10323        assert_eq!(fling_velocity(estimate, PRECISE_SLOP).1, 900.0);
10324    }
10325
10326    #[test]
10327    fn insertion_line_sits_on_the_target_top_edge() {
10328        // The upper half hovered means inserting **before**: a band of thickness 4
10329        // centred on the top edge (y=100) of a target 200 wide.
10330        let line = drop_insertion_line(
10331            Rect::new(20.0, 100.0, 200.0, 44.0),
10332            4.0,
10333            false,
10334            ReorderAxis::Vertical,
10335            false,
10336        );
10337        assert_eq!(line.x, 20.0, "aligned to the target's left");
10338        assert_eq!(line.width, 200.0, "the target's full width");
10339        assert_eq!(line.y, 98.0, "centred on the top edge (100 - 4/2)");
10340        assert_eq!(line.height, 4.0);
10341    }
10342
10343    #[test]
10344    fn insertion_line_sits_on_the_target_bottom_edge_when_inserting_after() {
10345        // The lower half hovered means inserting **after**: the band slides to the
10346        // **bottom** edge (y = 100 + 44 = 144, centred → 142). Same width, same thickness.
10347        let line = drop_insertion_line(
10348            Rect::new(20.0, 100.0, 200.0, 44.0),
10349            4.0,
10350            true,
10351            ReorderAxis::Vertical,
10352            false,
10353        );
10354        assert_eq!(line.x, 20.0, "still aligned to the left");
10355        assert_eq!(line.width, 200.0, "the target's full width");
10356        assert_eq!(line.y, 142.0, "centred on the bottom edge (144 - 4/2)");
10357        assert_eq!(line.height, 4.0);
10358        // A vertical list reads down whichever way its text runs.
10359        let rtl = drop_insertion_line(
10360            Rect::new(20.0, 100.0, 200.0, 44.0),
10361            4.0,
10362            true,
10363            ReorderAxis::Vertical,
10364            true,
10365        );
10366        assert_eq!(rtl, line, "right to left changes nothing down a list");
10367    }
10368
10369    /// **A horizontal list's line stands on its end**, on the edge the row goes to: the
10370    /// right one after a row that reads left to right, the left one after a row that reads
10371    /// right to left — where what follows it is.
10372    #[test]
10373    fn a_horizontal_insertion_line_stands_on_the_edge_the_row_goes_to() {
10374        let chip = Rect::new(100.0, 20.0, 80.0, 48.0);
10375        let line = |after, rtl| drop_insertion_line(chip, 4.0, after, ReorderAxis::Horizontal, rtl);
10376        let before = line(false, false);
10377        assert_eq!(
10378            (before.x, before.y, before.width, before.height),
10379            (98.0, 20.0, 4.0, 48.0),
10380            "before a row: its left edge, the full height, 4 wide"
10381        );
10382        assert_eq!(
10383            line(true, false).x,
10384            178.0,
10385            "after it: its right edge (180 - 4/2)"
10386        );
10387        // Right to left, the two edges trade places.
10388        assert_eq!(
10389            line(true, true).x,
10390            98.0,
10391            "after, right to left: the left edge"
10392        );
10393        assert_eq!(
10394            line(false, true).x,
10395            178.0,
10396            "before, right to left: the right edge"
10397        );
10398    }
10399
10400    /// **Which gesture a reorderable gets.** A column slides; a row or a card is inserted
10401    /// between slots — and a horizontal reorderable is a row only when it says so, which is
10402    /// what keeps a table's columns exactly as they were.
10403    #[test]
10404    fn a_horizontal_reorderable_inserts_only_when_it_says_so() {
10405        assert_eq!(
10406            ReorderMotion::of(ReorderAxis::Horizontal, false),
10407            ReorderMotion::Columns,
10408            "a table's column"
10409        );
10410        assert_eq!(
10411            ReorderMotion::of(ReorderAxis::Horizontal, true),
10412            ReorderMotion::Slots(ReorderAxis::Horizontal),
10413            "a horizontal list's row"
10414        );
10415        // Every vertical reorderable inserts, whatever it answers.
10416        for inserts in [false, true] {
10417            assert_eq!(
10418                ReorderMotion::of(ReorderAxis::Vertical, inserts),
10419                ReorderMotion::Slots(ReorderAxis::Vertical)
10420            );
10421        }
10422        // A widget that answers nothing keeps the trait's defaults, which are the table's.
10423        assert_eq!(
10424            reorder_motion::<()>(&frus_widgets::Container::new()),
10425            ReorderMotion::Columns
10426        );
10427        // And the real rows say which they are, down and across.
10428        for (axis, expected) in [
10429            (
10430                ReorderAxis::Vertical,
10431                ReorderMotion::Slots(ReorderAxis::Vertical),
10432            ),
10433            (
10434                ReorderAxis::Horizontal,
10435                ReorderMotion::Slots(ReorderAxis::Horizontal),
10436            ),
10437        ] {
10438            let list = frus_widgets::ReorderableList::<()>::new(|_, _| ())
10439                .axis(axis)
10440                .row(frus_widgets::Container::new().width(60.0));
10441            let row = frus_widgets::Widget::children(&list)[0].as_ref();
10442            assert_eq!(reorder_motion(row), expected, "{axis:?}");
10443        }
10444    }
10445
10446    /// **The ghost stays in its lane.** A column rises and follows x; a card follows the
10447    /// finger anywhere; a horizontal list's row follows x and nothing else — so a finger
10448    /// that wanders below the strip does not drag the row off it.
10449    #[test]
10450    fn a_horizontal_rows_ghost_follows_the_finger_along_x_only() {
10451        let travel = (120.0, 35.0);
10452        assert_eq!(
10453            ghost_offset(ReorderMotion::Columns, travel),
10454            (120.0, super::drag_preview::LIFT_Y)
10455        );
10456        assert_eq!(
10457            ghost_offset(ReorderMotion::Slots(ReorderAxis::Vertical), travel),
10458            (120.0, 35.0)
10459        );
10460        assert_eq!(
10461            ghost_offset(ReorderMotion::Slots(ReorderAxis::Horizontal), travel),
10462            (120.0, 0.0)
10463        );
10464    }
10465
10466    #[test]
10467    fn focus_returns_to_trigger_when_overlay_closes() {
10468        let anchor = WidgetId::from_u64(1);
10469        let item = WidgetId::from_u64(2);
10470        let (mut history, mut prev) = (Vec::new(), None);
10471
10472        // Frame 1: focus on the anchor, which is present.
10473        let f = resolve_focus(
10474            Some(anchor),
10475            &HashSet::from([anchor]),
10476            &mut history,
10477            &mut prev,
10478        );
10479        assert_eq!(f, Some(anchor));
10480        assert!(history.is_empty());
10481
10482        // Frame 2: the menu is open and the item focused; the anchor is pushed as trigger.
10483        let present = HashSet::from([anchor, item]);
10484        let f = resolve_focus(Some(item), &present, &mut history, &mut prev);
10485        assert_eq!(f, Some(item));
10486        assert_eq!(history, vec![anchor]);
10487
10488        // Frame 3: the menu closed and the item vanished → back to the trigger, history spent.
10489        let f = resolve_focus(
10490            Some(item),
10491            &HashSet::from([anchor]),
10492            &mut history,
10493            &mut prev,
10494        );
10495        assert_eq!(f, Some(anchor), "focus returns to the trigger");
10496        assert!(history.is_empty());
10497    }
10498
10499    #[test]
10500    fn focus_falls_to_none_when_no_trigger_remains() {
10501        let a = WidgetId::from_u64(1);
10502        let (mut history, mut prev) = (Vec::new(), Some(a));
10503        // `a` vanishes and the history is empty → no focus at all.
10504        let f = resolve_focus(Some(a), &HashSet::new(), &mut history, &mut prev);
10505        assert_eq!(f, None);
10506        assert_eq!(prev, None);
10507    }
10508
10509    #[test]
10510    fn spring_approaches_target_monotonically_and_settles() {
10511        // From 0 toward 100: a monotonic approach with no overshoot, all but reached
10512        // after several 16 ms steps.
10513        let mut x = 0.0;
10514        let mut prev = -1.0;
10515        for _ in 0..30 {
10516            x = spring_toward(x, 100.0, 0.016, 0.07);
10517            assert!(x > prev && x <= 100.0, "monotonic and bounded: {x}");
10518            prev = x;
10519        }
10520        assert!(x > 99.0, "all but reached after ~0.5 s: {x}");
10521    }
10522
10523    #[test]
10524    fn ghost_card_shape() {
10525        let theme = Theme::default();
10526        let card = Rect::new(140.0, 0.0, 80.0, 34.0);
10527        // An empty ghost falls back to a shadow plus a solid bordered card: 2 primitives.
10528        let mut scene = Scene::new();
10529        draw_ghost_card(&mut scene, &theme, card, &[]);
10530        assert_eq!(scene.primitives().len(), 2, "shadow plus solid card");
10531    }
10532}
10533
10534/// The half of a scroll request that belongs to the shell: turning the **name** an
10535/// application wrote into the region a frame actually has.
10536#[cfg(test)]
10537mod sheet_request_tests {
10538    use super::{apply_sheet_requests, Runtime, Widget};
10539    use crate::command::Command;
10540    use frus_widgets::{
10541        build_ui, keyed, Container, DraggableScrollableSheet, SheetTo, Size, Theme,
10542    };
10543
10544    /// A page with one named sheet on it — the key on a wrapper the application wrote, the
10545    /// state on a panel it never sees.
10546    fn view() -> Box<dyn Widget<()>> {
10547        Box::new(
10548            Container::<()>::new()
10549                .width(400.0)
10550                .height(800.0)
10551                .child(keyed(
10552                    "places",
10553                    DraggableScrollableSheet::<()>::new(Container::new()),
10554                )),
10555        )
10556    }
10557
10558    /// **A request reaches the sheet the view named**, and nothing else: another key, or a
10559    /// frame without the sheet, hands it back unplaced and moves nothing.
10560    #[test]
10561    fn a_request_reaches_the_sheet_the_view_named() {
10562        let tree = view();
10563        let mut runtime = Runtime::default();
10564        let sheets = build_ui(
10565            tree.as_ref(),
10566            Size::new(400.0, 800.0),
10567            &runtime,
10568            &Theme::default(),
10569        )
10570        .sheets()
10571        .to_vec();
10572        let sheet = sheets.first().expect("the page has its sheet").clone();
10573
10574        let requests = Command::<()>::sheet("places", SheetTo::size(0.9))
10575            .into_parts()
10576            .sheets;
10577        let (unplaced, moved) =
10578            apply_sheet_requests(&mut runtime, tree.as_ref(), requests, &sheets);
10579        assert!(unplaced.is_empty());
10580        assert!(moved);
10581        assert_eq!(runtime.sheet_size(sheet.id, &sheet.spec), 0.9);
10582
10583        let elsewhere = Command::<()>::sheet("elsewhere", SheetTo::size(0.3))
10584            .into_parts()
10585            .sheets;
10586        let (unplaced, moved) =
10587            apply_sheet_requests(&mut runtime, tree.as_ref(), elsewhere, &sheets);
10588        assert_eq!(unplaced.len(), 1);
10589        assert!(!moved);
10590        let not_yet = Command::<()>::sheet("places", SheetTo::size(0.3))
10591            .into_parts()
10592            .sheets;
10593        let (unplaced, _) = apply_sheet_requests(&mut runtime, tree.as_ref(), not_yet, &[]);
10594        assert_eq!(unplaced.len(), 1, "kept for the frame that has the sheet");
10595        assert_eq!(
10596            runtime.sheet_size(sheet.id, &sheet.spec),
10597            0.9,
10598            "nothing moved"
10599        );
10600    }
10601}
10602
10603#[cfg(test)]
10604mod scroll_request_tests {
10605    use super::{apply_scroll_requests, Rect, Runtime, Scrollable, Widget, WidgetId};
10606    use crate::command::Command;
10607    use frus_widgets::{keyed, Container, ScrollTo, SingleChildScrollView};
10608
10609    /// A view with one named scroll region in it, and nothing else of interest.
10610    fn view() -> Box<dyn Widget<()>> {
10611        Box::new(Container::<()>::new().child(keyed(
10612            "log",
10613            SingleChildScrollView::<()>::new().height(200.0),
10614        )))
10615    }
10616
10617    /// The region as the frame would register it: a 200-tall window over 1 000 of content.
10618    fn region(id: WidgetId) -> Scrollable {
10619        Scrollable {
10620            id,
10621            viewport: Rect {
10622                x: 0.0,
10623                y: 0.0,
10624                width: 300.0,
10625                height: 200.0,
10626            },
10627            max_x: 0.0,
10628            max_y: 1000.0,
10629            axis: frus_widgets::Axis::Vertical,
10630            physics: frus_widgets::ScrollPhysics::CLAMPING,
10631            overscroll: Some(frus_widgets::OverscrollIndicator::Glow),
10632            fling: frus_widgets::VelocityStrategy::Regression,
10633            refresh: None,
10634            page: None,
10635            reverse_x: false,
10636            reverse_y: false,
10637            keep_visible: None,
10638            host: None,
10639        }
10640    }
10641
10642    /// The key a request carries and the key the view declared are the **same hash** —
10643    /// which is the whole of the identity design, and the one thing that cannot be
10644    /// checked on either side alone.
10645    #[test]
10646    fn a_request_reaches_the_region_the_view_named() {
10647        let tree = view();
10648        let requests = Command::<()>::scroll("log", ScrollTo::end().instant())
10649            .into_parts()
10650            .scrolls;
10651        let id = frus_widgets::find_by_key(tree.as_ref(), requests[0].0)
10652            .expect("the view named it, so the tree knows it");
10653        let mut runtime = Runtime::default();
10654        let (unplaced, moved) =
10655            apply_scroll_requests(&mut runtime, tree.as_ref(), requests, &[region(id)]);
10656        assert!(unplaced.is_empty());
10657        assert!(moved);
10658        assert_eq!(runtime.scroll.get(&id), Some(&(0.0, 1000.0)));
10659    }
10660
10661    /// A region that has only just appeared is not in the registry the request is first
10662    /// tried against — that one was built last frame — so it is handed back rather than
10663    /// thrown away. Coming back to a screen and being put where you left off depends on
10664    /// exactly this.
10665    #[test]
10666    fn a_region_this_registry_has_not_got_is_handed_back() {
10667        let tree = view();
10668        let requests = Command::<()>::scroll("log", ScrollTo::y(120.0))
10669            .into_parts()
10670            .scrolls;
10671        let mut runtime = Runtime::default();
10672        let (unplaced, moved) = apply_scroll_requests(&mut runtime, tree.as_ref(), requests, &[]);
10673        assert_eq!(unplaced.len(), 1, "kept for the second attempt");
10674        assert!(!moved);
10675        assert!(runtime.scroll_target.is_empty());
10676    }
10677
10678    /// A key naming nothing at all leaves no trace: the caller drops what comes back,
10679    /// so a typo costs one frame's lookup and never accumulates.
10680    #[test]
10681    fn a_name_the_view_does_not_use_moves_nothing() {
10682        let tree = view();
10683        let requests = Command::<()>::scroll("ledger", ScrollTo::start())
10684            .into_parts()
10685            .scrolls;
10686        let mut runtime = Runtime::default();
10687        let id = frus_widgets::find_by_key(
10688            tree.as_ref(),
10689            Command::<()>::scroll("log", ScrollTo::start())
10690                .into_parts()
10691                .scrolls[0]
10692                .0,
10693        )
10694        .expect("the real one is there");
10695        let (unplaced, moved) =
10696            apply_scroll_requests(&mut runtime, tree.as_ref(), requests, &[region(id)]);
10697        assert_eq!(unplaced.len(), 1);
10698        assert!(!moved);
10699        assert!(runtime.scroll.is_empty() && runtime.scroll_target.is_empty());
10700    }
10701
10702    /// Two requests in one batch, both placed, and the batch is what `Command::batch`
10703    /// produces — a screen that restores both axes writes two and means two.
10704    #[test]
10705    fn a_batch_places_every_request_it_carries() {
10706        let tree = view();
10707        let requests = Command::<()>::batch([
10708            Command::scroll("log", ScrollTo::y(400.0).instant()),
10709            Command::scroll("log", ScrollTo::y(600.0).instant()),
10710        ])
10711        .into_parts()
10712        .scrolls;
10713        assert_eq!(requests.len(), 2);
10714        let id = frus_widgets::find_by_key(tree.as_ref(), requests[0].0).expect("named");
10715        let mut runtime = Runtime::default();
10716        let (unplaced, moved) =
10717            apply_scroll_requests(&mut runtime, tree.as_ref(), requests, &[region(id)]);
10718        assert!(unplaced.is_empty() && moved);
10719        assert_eq!(
10720            runtime.scroll.get(&id),
10721            Some(&(0.0, 600.0)),
10722            "the last request is the current statement"
10723        );
10724    }
10725}
10726
10727#[cfg(test)]
10728mod autoscroll_tests {
10729    use super::{
10730        edge_autoscroll, follow_content, Drag, Point, Rect, WidgetId, AUTOSCROLL_MAX_OVERHANG,
10731        AUTOSCROLL_VELOCITY,
10732    };
10733
10734    /// A viewport from 100 to 500, with room to scroll either way.
10735    const VIEW: (f32, f32) = (100.0, 500.0);
10736    const ROOM: (f32, f32) = (1000.0, 1000.0);
10737    /// A sixtieth of a second, which is the frame this is asked on.
10738    const DT: f32 = 1.0 / 60.0;
10739
10740    #[test]
10741    fn a_row_inside_the_viewport_scrolls_nothing() {
10742        assert_eq!(edge_autoscroll((200.0, 260.0), VIEW, ROOM, DT), None);
10743        // Touching the edge exactly is still inside: the row has not asked for anything.
10744        assert_eq!(edge_autoscroll((440.0, 500.0), VIEW, ROOM, DT), None);
10745    }
10746
10747    /// The law: pixels per second per pixel of overhang, so ten pixels out is ten times
10748    /// the speed of one pixel out — the row is not dragged along by a fixed nudge.
10749    #[test]
10750    fn the_speed_follows_how_far_out_the_row_is() {
10751        let near = edge_autoscroll((460.0, 501.0), VIEW, ROOM, DT).expect("one pixel out");
10752        let far = edge_autoscroll((460.0, 510.0), VIEW, ROOM, DT).expect("ten pixels out");
10753        assert!(
10754            near < 0.0,
10755            "the content moves up to reveal what is below: {near}"
10756        );
10757        assert!(
10758            (far / near - 10.0).abs() < 0.01,
10759            "ten times as far, ten times as fast: {near} vs {far}"
10760        );
10761        assert!(
10762            (near + 1.0 * AUTOSCROLL_VELOCITY * DT).abs() < 1e-4,
10763            "one pixel out, at the stated velocity: {near}"
10764        );
10765    }
10766
10767    /// Carrying a row right off the window is not a request to scroll a thousand times
10768    /// faster; past the cap the speed stops growing.
10769    #[test]
10770    fn the_overhang_is_capped() {
10771        let far = edge_autoscroll((460.0, 520.0), VIEW, ROOM, DT).expect("twenty out");
10772        let absurd = edge_autoscroll((460.0, 5000.0), VIEW, ROOM, DT).expect("off the window");
10773        assert!((far - absurd).abs() < 1e-4, "{far} vs {absurd}");
10774        assert!((absurd + AUTOSCROLL_MAX_OVERHANG * AUTOSCROLL_VELOCITY * DT).abs() < 1e-4);
10775    }
10776
10777    /// Above the top the content moves the other way, and the sign is the whole of the
10778    /// difference between the two edges.
10779    #[test]
10780    fn above_the_top_the_content_comes_down() {
10781        let up = edge_autoscroll((90.0, 150.0), VIEW, ROOM, DT).expect("ten above");
10782        assert!(up > 0.0, "{up}");
10783    }
10784
10785    /// **What is carried stays under the finger while its list scrolls.**
10786    ///
10787    /// Seen on a phone: a row carried to the bottom edge started the list scrolling, and
10788    /// the ghost rode up with the content — it is drawn at the row's box, and the box had
10789    /// moved — until it no longer hung over the edge and the scroll stopped by itself,
10790    /// 95 px in. The press moves with the content instead.
10791    #[test]
10792    fn a_carried_item_follows_its_content() {
10793        // The list scrolled on by 95 px: its content went 95 px up the screen.
10794        let shift = (0.0, -95.0);
10795        let pressed = Point::new(900.0, 1330.0);
10796        let row_then = Rect::new(105.0, 1250.0, 700.0, 160.0);
10797        let row_now = row_then.translate(0.0, -95.0);
10798        let finger = Point::new(900.0, 2030.0);
10799
10800        let mut reorder = Drag::Reorder {
10801            id: WidgetId::from_u64(1),
10802            from: 0,
10803            start: pressed,
10804            moved: true,
10805            carried: Some((WidgetId::from_u64(1), row_then)),
10806        };
10807        follow_content(&mut reorder, shift);
10808        let Drag::Reorder { start, carried, .. } = reorder else {
10809            unreachable!("still a reorder");
10810        };
10811        // The box it was carried from went with the content, as the row did — and the frame's
10812        // own box is not asked again, since the frame clips a row carried to an edge and loses
10813        // one scrolled out of sight (milestone 527).
10814        let (_, carried) = carried.expect("the box it was carried from");
10815        assert_eq!(carried, row_now, "the carried box went with the content");
10816        // Drawn the way the shell draws it: the carried box, plus the travel.
10817        let ghost = carried.translate(finger.x - start.x, finger.y - start.y);
10818        assert_eq!(
10819            ghost,
10820            row_then.translate(finger.x - pressed.x, finger.y - pressed.y),
10821            "the ghost is where it would be had nothing scrolled"
10822        );
10823
10824        let mut item = Drag::Item {
10825            source: frus_widgets::DragSource {
10826                id: WidgetId::from_u64(1),
10827                rect: row_then,
10828                payload: 7,
10829            },
10830            start: pressed,
10831            moved: true,
10832            over: None,
10833        };
10834        follow_content(&mut item, shift);
10835        let Drag::Item { source, start, .. } = item else {
10836            unreachable!("still an item");
10837        };
10838        assert_eq!(
10839            source.rect, row_now,
10840            "the box taken at the press went with the content"
10841        );
10842        assert_eq!(start, Point::new(900.0, 1235.0), "and so did the press");
10843    }
10844
10845    /// At the end of the content there is nothing left to reveal, so the list stays where
10846    /// it is instead of straining against its own end.
10847    #[test]
10848    fn an_exhausted_edge_stays_still() {
10849        assert_eq!(
10850            edge_autoscroll((460.0, 520.0), VIEW, (1000.0, 0.0), DT),
10851            None
10852        );
10853        assert_eq!(
10854            edge_autoscroll((90.0, 150.0), VIEW, (0.0, 1000.0), DT),
10855            None
10856        );
10857    }
10858
10859    /// A row taller than the viewport hangs over both edges at once. Pulled two ways it
10860    /// would scroll nowhere, or jitter between them; the leading edge decides.
10861    #[test]
10862    fn a_row_taller_than_the_viewport_follows_its_leading_edge() {
10863        let both = edge_autoscroll((50.0, 900.0), VIEW, ROOM, DT).expect("over both edges");
10864        assert!(both > 0.0, "the top edge wins: {both}");
10865    }
10866}
10867
10868/// **A back gesture the page below cannot take** (milestone 531).
10869///
10870/// Seen on a phone: a back gesture from the left edge, at the height of a task row of the
10871/// page below, did not slide the page on top. Half a second in, the row was lifted instead —
10872/// drawn over the page on top in its green-outlined ghost, and following the finger — and it
10873/// went away when the finger did. At a height with no row the gesture worked.
10874///
10875/// The press that starts the gesture is tested against the frame on screen, and after a push
10876/// that frame is the push's last: an application's animation that has just settled asks for
10877/// no rebuild, so the page the push left is still in the frame, parallaxed to the left and
10878/// under the finger. The press found the row's hold, the long press armed, and its deadline
10879/// replaced the gesture with a lift.
10880#[cfg(test)]
10881mod back_gesture_tests {
10882    use super::{
10883        drag_after_hold, frame_needs_build, hold_candidates, Drag, Point, Theme, WidgetId,
10884    };
10885    use frus_widgets::{build_ui, column, Container, DragSource, Draggable, Navigator};
10886    use frus_widgets::{Rect, Runtime, Size, Ui, Widget};
10887
10888    /// A phone, in logical pixels.
10889    const W: f32 = 392.0;
10890    const H: f32 = 850.0;
10891    /// Where the row sits on the page below, and the finger's press on the edge at its
10892    /// height.
10893    const ROW_TOP: f32 = 480.0;
10894    const ROW_HEIGHT: f32 = 66.0;
10895    const ON_THE_ROW: Point = Point::new(3.0, 509.0);
10896    const ABOVE_THE_ROW: Point = Point::new(3.0, 200.0);
10897    /// The long press the row asks for.
10898    const HELD: u8 = 1;
10899
10900    /// A page with one row that lifts on a hold and says something when held, below a
10901    /// stretch with nothing to take.
10902    fn home() -> impl Widget<u8> {
10903        column![
10904            Container::<u8>::new().width(W).height(ROW_TOP),
10905            Draggable::new(
10906                Container::<u8>::new()
10907                    .width(W)
10908                    .height(ROW_HEIGHT)
10909                    .on_long_press(HELD)
10910            )
10911            .payload(7)
10912            .long_press(),
10913        ]
10914    }
10915
10916    /// The frame on screen after a push from `home` has settled: the push's last, with the
10917    /// page it left still in it.
10918    fn after_a_push() -> Navigator<u8> {
10919        Navigator::new("data", Container::<u8>::new().width(W).height(H))
10920            .size(W, H)
10921            .from("home", home(), 0.999, true)
10922    }
10923
10924    fn frame(root: &dyn Widget<u8>) -> Ui<u8> {
10925        build_ui(
10926            root,
10927            Size::new(W, H),
10928            &Runtime::default(),
10929            &Theme::default(),
10930        )
10931    }
10932
10933    fn back(at: Point) -> Drag {
10934        Drag::Back { start_x: at.x }
10935    }
10936
10937    fn still_scroll() -> Drag {
10938        Drag::Scroll {
10939            id: WidgetId::from_u64(9),
10940            last: ON_THE_ROW,
10941            moved: false,
10942            carried: (0.0, 0.0),
10943            dismiss: None,
10944            axis: None,
10945            pan: None,
10946        }
10947    }
10948
10949    fn row_source() -> DragSource {
10950        DragSource {
10951            id: WidgetId::from_u64(3),
10952            rect: Rect::new(-117.0, ROW_TOP, W, ROW_HEIGHT),
10953            payload: 7,
10954        }
10955    }
10956
10957    /// **Why the row was under the finger at all**: the frame a transition ends on is built
10958    /// again. The tick that ends a push takes the page it left out of the state and reports
10959    /// nothing moving, so a loop that builds only while something moves hit-tests the
10960    /// push's last frame from then on — the page left behind still in it, where a tap or a
10961    /// hold can reach what is no longer on screen. The demo's
10962    /// `the_last_frame_of_a_push_still_holds_the_page_it_left` shows that frame on its own
10963    /// page, at the phone's coordinates.
10964    #[test]
10965    fn the_frame_an_animation_settles_in_is_built_again() {
10966        assert!(
10967            frame_needs_build(false, false, true, false),
10968            "the frame the application's animation stops in builds the view again"
10969        );
10970        assert!(
10971            frame_needs_build(false, true, true, false),
10972            "as does every frame while it moves"
10973        );
10974        assert!(
10975            !frame_needs_build(false, false, false, false),
10976            "and a still frame after a still one repaints the tree it has"
10977        );
10978        assert!(frame_needs_build(true, false, false, false), "unless asked");
10979        assert!(
10980            frame_needs_build(false, false, false, true),
10981            "or the frame has its own reason"
10982        );
10983    }
10984
10985    /// The reproduction: the frame really has a row to take under the finger, and a back
10986    /// gesture's press takes nothing from it.
10987    #[test]
10988    fn a_back_gesture_claims_nothing_under_its_finger() {
10989        let root = after_a_push();
10990        let ui = frame(&root);
10991        let (held, lift) = hold_candidates(None, Some(&ui), Some(&root), ON_THE_ROW);
10992        assert!(
10993            lift.is_some() && held == Some(HELD),
10994            "the frame the phone had: a press there, with no gesture, finds the row's hold"
10995        );
10996
10997        let gesture = back(ON_THE_ROW);
10998        let (held, lift) = hold_candidates(Some(&gesture), Some(&ui), Some(&root), ON_THE_ROW);
10999        assert!(
11000            lift.is_none(),
11001            "a back gesture's press arms no lift of the row under it"
11002        );
11003        assert_eq!(held, None, "nor its long press");
11004    }
11005
11006    /// Whatever a press found, the deadline never takes the pointer from a back gesture:
11007    /// the gesture goes on, and keeps its start.
11008    #[test]
11009    fn a_hold_never_takes_the_pointer_from_a_back_gesture() {
11010        let reorder = Some((WidgetId::from_u64(4), 2, ON_THE_ROW));
11011        for (lift, reorder) in [
11012            (Some(row_source()), None),
11013            (None, reorder),
11014            (Some(row_source()), reorder),
11015            (None, None),
11016        ] {
11017            let after = drag_after_hold(Some(back(ON_THE_ROW)), lift, reorder, ON_THE_ROW);
11018            assert!(
11019                matches!(after, Some(Drag::Back { start_x }) if start_x == ON_THE_ROW.x),
11020                "the back gesture is still the drag (lift {}, reorder {})",
11021                lift.is_some(),
11022                reorder.is_some()
11023            );
11024        }
11025    }
11026
11027    /// At a height with nothing to take — where the gesture always worked — it still does.
11028    #[test]
11029    fn a_back_gesture_where_nothing_is_under_it_still_owns_the_pointer() {
11030        let root = after_a_push();
11031        let ui = frame(&root);
11032        let gesture = back(ABOVE_THE_ROW);
11033        let (held, lift) = hold_candidates(Some(&gesture), Some(&ui), Some(&root), ABOVE_THE_ROW);
11034        assert!(held.is_none() && lift.is_none());
11035        assert!(matches!(
11036            drag_after_hold(Some(gesture), None, None, ABOVE_THE_ROW),
11037            Some(Drag::Back { .. })
11038        ));
11039    }
11040
11041    /// No regression: a hold on the row with no back gesture — the list's scroll still
11042    /// waiting to see whether the finger moves — lifts it, or its reorder, and says what
11043    /// the row asks when held.
11044    #[test]
11045    fn a_hold_on_a_row_without_a_back_gesture_still_lifts_it() {
11046        let root = home();
11047        let ui = frame(&root);
11048        let at = Point::new(40.0, ROW_TOP + 10.0);
11049        let scroll = still_scroll();
11050        let (held, lift) = hold_candidates(Some(&scroll), Some(&ui), Some(&root), at);
11051        assert_eq!(
11052            held,
11053            Some(HELD),
11054            "the long press is armed under a still scroll"
11055        );
11056        assert!(lift.is_some(), "and so is the lift");
11057        let (held, lift) = hold_candidates(None, Some(&ui), Some(&root), at);
11058        assert!(held.is_some() && lift.is_some(), "and with no drag at all");
11059
11060        let cursor = Point::new(41.0, ROW_TOP + 11.0);
11061        let lifted = drag_after_hold(Some(still_scroll()), Some(row_source()), None, cursor);
11062        assert!(
11063            matches!(lifted, Some(Drag::Item { moved: true, start, .. }) if start == cursor),
11064            "the hold lifts the item where the finger is"
11065        );
11066        let reorder = Some((WidgetId::from_u64(4), 2, at));
11067        let reordering = drag_after_hold(Some(still_scroll()), None, reorder, cursor);
11068        assert!(
11069            matches!(reordering, Some(Drag::Reorder { moved: true, from: 2, start, .. }) if start == at),
11070            "or picks the row up for a reorder, from where it was pressed"
11071        );
11072        assert!(
11073            drag_after_hold(Some(still_scroll()), None, None, cursor).is_none(),
11074            "a hold with nothing to lift ends the still scroll: the long press had it"
11075        );
11076    }
11077}
11078
11079/// How many layers `primitives` hold, nested ones included (milestone 609).
11080fn count_layers(primitives: &[frus_widgets::Primitive]) -> u32 {
11081    primitives
11082        .iter()
11083        .map(|p| match p {
11084            frus_widgets::Primitive::Layer { primitives, .. } => 1 + count_layers(primitives),
11085            _ => 0,
11086        })
11087        .sum()
11088}
11089
11090/// **What the shell reads of a key going down** (milestone 608). Winit's own event cannot be
11091/// made outside winit, so the shell's handling takes this, made from it in a window and by
11092/// hand in a test.
11093pub(crate) struct KeyDown {
11094    /// The key the layout makes of it: a character, or a named key.
11095    logical_key: WinitKey,
11096    /// Where it is on the keyboard, whatever the layout.
11097    physical_key: PhysicalKey,
11098    /// The text it types, if any.
11099    text: Option<winit::keyboard::SmolStr>,
11100    /// Whether it is the key repeating, held down.
11101    repeat: bool,
11102}
11103
11104impl From<&winit::event::KeyEvent> for KeyDown {
11105    fn from(event: &winit::event::KeyEvent) -> Self {
11106        Self {
11107            logical_key: event.logical_key.clone(),
11108            physical_key: event.physical_key,
11109            text: event.text.clone(),
11110            repeat: event.repeat,
11111        }
11112    }
11113}
11114
11115/// `scene` over the window's background, for a see-through renderer, whose frames start
11116/// transparent (milestone 643): everywhere but the `line` pixels at the top the system paints,
11117/// which are left clear for its title bar to show through. In physical pixels, as the scene
11118/// is; the scene's own primitives follow, in their order.
11119fn backdrop_under(mut scene: Scene, width: f32, height: f32, line: f32) -> Scene {
11120    let mut framed = Scene::new();
11121    framed.fill_rect(
11122        frus_widgets::Rect::new(0.0, line, width, (height - line).max(0.0)),
11123        frus_gpu::backdrop(),
11124    );
11125    for primitive in scene.split_off(0) {
11126        framed.push_primitive(primitive);
11127    }
11128    framed
11129}
11130
11131#[cfg(test)]
11132mod backdrop_tests {
11133    use super::*;
11134
11135    /// **A see-through frame is the window's background under everything but the line the
11136    /// system paints**, and the scene over it, unchanged.
11137    #[test]
11138    fn the_background_leaves_the_system_s_line_clear() {
11139        let mut scene = Scene::new();
11140        scene.fill_rect(
11141            frus_widgets::Rect::new(10.0, 5.0, 20.0, 20.0),
11142            frus_widgets::Color::WHITE,
11143        );
11144        let framed = backdrop_under(scene.clone(), 800.0, 600.0, 48.0);
11145        let prims = framed.primitives();
11146        assert_eq!(prims.len(), 2);
11147        match &prims[0] {
11148            frus_widgets::Primitive::Rect { rect, color, .. } => {
11149                assert_eq!(*rect, frus_widgets::Rect::new(0.0, 48.0, 800.0, 552.0));
11150                assert_eq!(*color, frus_gpu::backdrop());
11151                assert_eq!(
11152                    color.a, 1.0,
11153                    "opaque: nothing behind the window shows there"
11154                );
11155            }
11156            other => panic!("the background first: {other:?}"),
11157        }
11158        assert_eq!(prims[1], scene.primitives()[0], "then the scene");
11159
11160        let whole = backdrop_under(Scene::new(), 800.0, 600.0, 0.0);
11161        assert!(
11162            matches!(&whole.primitives()[0],
11163            frus_widgets::Primitive::Rect { rect, .. } if *rect == frus_widgets::Rect::new(0.0, 0.0, 800.0, 600.0)),
11164            "no line: the whole window"
11165        );
11166    }
11167}
11168
11169/// A driver with no window and no event loop: input and frames fed by hand, through the
11170/// shell's own paths.
11171#[cfg(any(test, feature = "testing"))]
11172#[doc(hidden)]
11173pub mod testing {
11174    use super::*;
11175    use crate::gesture::{PointerEvent, PointerKind, LONG_PRESS_DELAY};
11176
11177    /// What the last frame left behind: the interface it laid out, and the tree it laid out from.
11178    pub type FrameParts<'a, Msg> = (&'a Ui<Msg>, &'a dyn Widget<Msg>);
11179
11180    /// The shell around an application, on a surface of a stated logical size.
11181    pub struct Driver<A: Application> {
11182        shell: App<A>,
11183        size: Size,
11184        /// Where the last frame drew a reorder's ghost, when it drew one.
11185        ghost: Option<Rect>,
11186    }
11187
11188    impl<A: Application> Driver<A> {
11189        /// A driver for `app` on a `width` by `height` logical surface.
11190        pub fn new(app: A, width: f32, height: f32) -> Self {
11191            let mut shell = App::detached(app);
11192            // Gestures are timed by the frames run, as everything else here is.
11193            shell.gesture_clock = Some((0.0, 0.0));
11194            Self {
11195                shell,
11196                size: Size::new(width, height),
11197                ghost: None,
11198            }
11199        }
11200
11201        /// The system's **Back** — Android's back button, the browser's Back — as the shell
11202        /// handles it, short of leaving: whether it went back from anything.
11203        pub fn system_back(&mut self) -> bool {
11204            self.shell.system_back_step()
11205        }
11206
11207        /// How the gesture under way will read its fling: its scroll area's behaviour's, on
11208        /// a drag that landed on one, else the application's (milestone 616).
11209        pub fn fling_strategy(&self) -> frus_widgets::VelocityStrategy {
11210            self.shell.gesture_velocity.strategy()
11211        }
11212
11213        /// Where what is carried is drawn this frame — a reorder's ghost or a lifted item —
11214        /// or `None` when nothing is.
11215        pub fn carried(&self) -> Option<Rect> {
11216            self.shell.carried_rect()
11217        }
11218
11219        /// Where the last frame drew a reorder's ghost — the outline around it — or `None`
11220        /// when it drew none.
11221        pub fn ghost(&self) -> Option<Rect> {
11222            self.ghost
11223        }
11224
11225        /// Every scroll region of the last frame, as its viewport and its offset.
11226        pub fn offsets(&self) -> Vec<(Rect, (f32, f32))> {
11227            let s = &self.shell;
11228            s.ui.as_ref()
11229                .map(|ui| {
11230                    ui.scroll_regions()
11231                        .iter()
11232                        .map(|a| {
11233                            (
11234                                a.viewport,
11235                                s.runtime.scroll.get(&a.id).copied().unwrap_or((0.0, 0.0)),
11236                            )
11237                        })
11238                        .collect()
11239                })
11240                .unwrap_or_default()
11241        }
11242
11243        /// The interface the last frame laid out — what a tap is asked about, what the registries
11244        /// hold — and the tree it was laid out from.
11245        /// Whether a system tray icon was made (milestone 706): never, with no window.
11246        #[cfg(all(test, desktop, feature = "tray"))]
11247        pub(crate) fn has_system_tray(&self) -> bool {
11248            self.shell.tray.is_some()
11249        }
11250
11251        /// What an assistive technology's **activate** does on the node labelled `label` —
11252        /// the path a screen reader's press takes (milestone 710). Whether there was one.
11253        pub fn assistive_click(&mut self, label: &str) -> bool {
11254            let Some(id) = self.node_labelled(label) else {
11255                return false;
11256            };
11257            self.shell.activate_for_assistive_tech(id);
11258            true
11259        }
11260
11261        /// What an assistive technology's **set value** does on the node labelled `label`
11262        /// (milestone 710). Whether there was one.
11263        pub fn assistive_set_value(&mut self, label: &str, value: &str) -> bool {
11264            let Some(id) = self.node_labelled(label) else {
11265                return false;
11266            };
11267            self.shell
11268                .set_value_for_assistive_tech(id, value.to_owned());
11269            true
11270        }
11271
11272        /// The node an assistive technology knows by `label`.
11273        fn node_labelled(&self, label: &str) -> Option<WidgetId> {
11274            self.shell
11275                .ui
11276                .as_ref()?
11277                .semantics()
11278                .iter()
11279                .find(|(_, _, sem)| sem.label.as_deref() == Some(label))
11280                .map(|(id, _, _)| *id)
11281        }
11282
11283        pub fn frame_parts(&self) -> Option<FrameParts<'_, A::Message>> {
11284            let s = &self.shell;
11285            Some((s.ui.as_ref()?, s.tree.as_deref()?))
11286        }
11287
11288        /// The words the last frame drew and the box each was drawn in, in paint order.
11289        pub fn texts(&self) -> Vec<(String, Rect)> {
11290            self.shell
11291                .ui
11292                .as_ref()
11293                .map(|ui| {
11294                    ui.scene()
11295                        .primitives()
11296                        .iter()
11297                        .filter_map(|p| match p {
11298                            frus_widgets::Primitive::Text { text, .. } => {
11299                                Some((text.clone(), p.bounds()))
11300                            }
11301                            _ => None,
11302                        })
11303                        .collect()
11304                })
11305                .unwrap_or_default()
11306        }
11307
11308        /// A tap on the first word of the last frame that reads `label`, and a frame after it:
11309        /// how a test presses a button by what it says. Whether there was such a word.
11310        pub fn tap_text(&mut self, label: &str) -> bool {
11311            let Some((_, rect)) = self.texts().into_iter().find(|(text, _)| text == label) else {
11312                return false;
11313            };
11314            let at = Point::new(rect.x + rect.width / 2.0, rect.y + rect.height / 2.0);
11315            self.press(at);
11316            self.release(at);
11317            self.frame(1.0 / 60.0);
11318            true
11319        }
11320
11321        /// The application, as the shell holds it.
11322        pub fn app(&self) -> &A {
11323            &self.shell.app
11324        }
11325
11326        /// The widget with the keyboard focus, if one has it.
11327        pub fn focused(&self) -> Option<WidgetId> {
11328            self.shell.runtime.input.focused
11329        }
11330
11331        /// Makes the shell stand in for a built-in device, as `FRUS_DEVICE` does
11332        /// (milestone 722).
11333        #[cfg(test)]
11334        pub(crate) fn simulate(&mut self, device: &str) {
11335            self.shell.simulated = Some(crate::simulated::Simulated::named(device).unwrap());
11336        }
11337
11338        /// Whether the simulated device's keyboard is up, once the end of a frame has
11339        /// kept it in step with the focus.
11340        #[cfg(test)]
11341        pub(crate) fn simulated_keyboard(&mut self) -> bool {
11342            self.shell.sync_soft_input();
11343            self.shell.simulated.as_ref().is_some_and(|s| s.keyboard)
11344        }
11345
11346        /// The modifiers the shell holds as down: Ctrl, Shift, Alt and Meta.
11347        pub fn shell_modifiers(&self) -> (bool, bool, bool, bool) {
11348            (
11349                self.shell.ctrl,
11350                self.shell.shift,
11351                self.shell.alt,
11352                self.shell.meta,
11353            )
11354        }
11355
11356        /// The widget the last frame would hand a press at `at`, if any.
11357        pub fn hit(&self, at: Point) -> Option<WidgetId> {
11358            self.shell.ui.as_ref().and_then(|ui| ui.hit(at))
11359        }
11360
11361        /// Hands the application a message, as the shell does.
11362        pub fn update(&mut self, message: A::Message) {
11363            self.shell.dispatch(message);
11364        }
11365
11366        /// Frames at sixty a second for `seconds`.
11367        pub fn run(&mut self, seconds: f32) {
11368            let n = (seconds * 60.0).round() as usize;
11369            for _ in 0..n.max(1) {
11370                self.frame(1.0 / 60.0);
11371            }
11372        }
11373
11374        /// Files from outside the application held over the window, as the system names
11375        /// them, the cursor at `at` — or nowhere known, as on the systems that do not say.
11376        pub fn hover_files(&mut self, paths: &[std::path::PathBuf], at: Option<Point>) {
11377            for path in paths {
11378                self.shell.file_event(path.clone(), false, at);
11379            }
11380        }
11381
11382        /// The files held over the window moved, to `at`.
11383        pub fn move_files(&mut self, at: Option<Point>) {
11384            self.shell.files_moved(at);
11385        }
11386
11387        /// The files dropped at `at`, then handed over as the loop does once it has had
11388        /// every one of them.
11389        pub fn drop_files(&mut self, paths: &[std::path::PathBuf], at: Option<Point>) {
11390            for path in paths {
11391                self.shell.file_event(path.clone(), true, at);
11392            }
11393            self.shell.deliver_dropped_files();
11394        }
11395
11396        /// The files were taken away again.
11397        pub fn cancel_files(&mut self) {
11398            self.shell.files_left();
11399        }
11400
11401        /// The target something carried — an item or files — would land on now.
11402        pub fn drag_over(&self) -> Option<WidgetId> {
11403            self.shell.runtime.drag_over
11404        }
11405
11406        /// A finger down at `at`.
11407        pub fn press(&mut self, at: Point) {
11408            self.pointer(PointerKind::Down, at);
11409        }
11410
11411        /// The finger moved to `at`.
11412        pub fn move_to(&mut self, at: Point) {
11413            self.pointer(PointerKind::Move, at);
11414        }
11415
11416        /// The finger lifted at `at`.
11417        pub fn release(&mut self, at: Point) {
11418            self.pointer(PointerKind::Up, at);
11419        }
11420
11421        /// The long-press deadline, delivered as the loop's wake delivers it. Whether it
11422        /// fired: a press that moved past the slop, or was never waiting, has none.
11423        pub fn hold_deadline(&mut self) -> bool {
11424            let fired = self.shell.press.poll(Instant::now() + LONG_PRESS_DELAY);
11425            if fired {
11426                self.shell.hold_deadline_reached();
11427            }
11428            fired
11429        }
11430
11431        /// The double tap's wait, over: a first tap still waiting is sent as a tap. Whether one
11432        /// was waiting.
11433        pub fn tap_deadline(&mut self) -> bool {
11434            self.shell
11435                .poll_pending_tap(Instant::now() + DOUBLE_TAP_TIMEOUT)
11436        }
11437
11438        /// The cursor the window would show where the pointer is now.
11439        pub fn cursor_shown(&self) -> frus_widgets::Cursor {
11440            let s = &self.shell;
11441            let requested = s.runtime.input.hovered.and_then(|id| {
11442                let rect = s.ui.as_ref()?.widget_rect(id)?;
11443                let widget = find_widget(s.tree.as_ref()?.as_ref(), id)?;
11444                widget.cursor_icon(
11445                    s.cursor.x - rect.x,
11446                    s.cursor.y - rect.y,
11447                    rect.width,
11448                    rect.height,
11449                )
11450            });
11451            s.region_cursor(requested)
11452        }
11453
11454        /// When the loop would wake next if it went idle now, if it would.
11455        pub fn next_wake(&self) -> Option<Instant> {
11456            match self.shell.idle_control_flow() {
11457                ControlFlow::WaitUntil(at) => Some(at),
11458                _ => None,
11459            }
11460        }
11461
11462        /// A click of the secondary mouse button at `at`.
11463        pub fn secondary_click(&mut self, at: Point) {
11464            self.pointer_of(PointerKind::Move, at, false);
11465            self.shell.context_click();
11466        }
11467
11468        /// A mouse button down at `at` — a pointer that hovers, which is not a finger: nothing is
11469        /// left to scroll, and a press on words in a selection area begins a selection.
11470        pub fn press_mouse(&mut self, at: Point) {
11471            self.pointer_of(PointerKind::Down, at, false);
11472        }
11473
11474        /// The mouse moved to `at`, its button down.
11475        pub fn move_mouse(&mut self, at: Point) {
11476            self.pointer_of(PointerKind::Move, at, false);
11477        }
11478
11479        /// The mouse button lifted at `at`.
11480        pub fn release_mouse(&mut self, at: Point) {
11481            self.pointer_of(PointerKind::Up, at, false);
11482        }
11483
11484        /// The words a selection area has selected, as Copy would put them on the clipboard.
11485        pub fn area_selection(&self) -> Option<String> {
11486            self.shell.region_text()
11487        }
11488
11489        /// Select all, as the keyboard asks it of a selection area that holds a selection.
11490        pub fn area_select_all(&mut self) {
11491            self.shell.select_all_region();
11492        }
11493
11494        /// Escape, as the keyboard delivers it.
11495        pub fn escape(&mut self) {
11496            self.shell.escape();
11497        }
11498
11499        /// **A key pressed**, with the modifiers the stroke names held down for it, through
11500        /// the shell's own handling of a key (milestone 608): the arrows move the focus or a
11501        /// caret or answer the widget with the focus, Enter and Space activate, Escape
11502        /// closes, Tab moves on, a bound shortcut fires, a character is typed.
11503        ///
11504        /// The system's back key is not one of them: what it does last is quit, and a
11505        /// driver has no loop to quit.
11506        pub fn key(&mut self, stroke: frus_widgets::KeyStroke) {
11507            use frus_widgets::ShortcutKey as K;
11508            let named = |key| (WinitKey::Named(key), None);
11509            let (logical_key, text) = match stroke.key {
11510                K::Char(c) => {
11511                    let typed: winit::keyboard::SmolStr = c.to_string().into();
11512                    (WinitKey::Character(typed.clone()), Some(typed))
11513                }
11514                K::Enter => named(NamedKey::Enter),
11515                K::Escape => named(NamedKey::Escape),
11516                K::Tab => named(NamedKey::Tab),
11517                K::Space => (WinitKey::Named(NamedKey::Space), Some(" ".into())),
11518                K::Backspace => named(NamedKey::Backspace),
11519                K::Delete => named(NamedKey::Delete),
11520                K::Up => named(NamedKey::ArrowUp),
11521                K::Down => named(NamedKey::ArrowDown),
11522                K::Left => named(NamedKey::ArrowLeft),
11523                K::Right => named(NamedKey::ArrowRight),
11524                K::Home => named(NamedKey::Home),
11525                K::End => named(NamedKey::End),
11526                K::PageUp => named(NamedKey::PageUp),
11527                K::PageDown => named(NamedKey::PageDown),
11528                K::F(n) => named(match n {
11529                    1 => NamedKey::F1,
11530                    2 => NamedKey::F2,
11531                    3 => NamedKey::F3,
11532                    4 => NamedKey::F4,
11533                    5 => NamedKey::F5,
11534                    6 => NamedKey::F6,
11535                    7 => NamedKey::F7,
11536                    8 => NamedKey::F8,
11537                    9 => NamedKey::F9,
11538                    10 => NamedKey::F10,
11539                    11 => NamedKey::F11,
11540                    _ => NamedKey::F12,
11541                }),
11542            };
11543            // Where a letter is on a QWERTY keyboard: the clipboard's shortcuts are read off
11544            // the key's place, so that they work on any layout.
11545            const LETTERS: [KeyCode; 26] = [
11546                KeyCode::KeyA,
11547                KeyCode::KeyB,
11548                KeyCode::KeyC,
11549                KeyCode::KeyD,
11550                KeyCode::KeyE,
11551                KeyCode::KeyF,
11552                KeyCode::KeyG,
11553                KeyCode::KeyH,
11554                KeyCode::KeyI,
11555                KeyCode::KeyJ,
11556                KeyCode::KeyK,
11557                KeyCode::KeyL,
11558                KeyCode::KeyM,
11559                KeyCode::KeyN,
11560                KeyCode::KeyO,
11561                KeyCode::KeyP,
11562                KeyCode::KeyQ,
11563                KeyCode::KeyR,
11564                KeyCode::KeyS,
11565                KeyCode::KeyT,
11566                KeyCode::KeyU,
11567                KeyCode::KeyV,
11568                KeyCode::KeyW,
11569                KeyCode::KeyX,
11570                KeyCode::KeyY,
11571                KeyCode::KeyZ,
11572            ];
11573            let physical_key = match stroke.key {
11574                K::Char(c) if c.is_ascii_alphabetic() => {
11575                    PhysicalKey::Code(LETTERS[(c.to_ascii_lowercase() as u8 - b'a') as usize])
11576                }
11577                _ => PhysicalKey::Unidentified(winit::keyboard::NativeKeyCode::Unidentified),
11578            };
11579            let held = (
11580                self.shell.ctrl,
11581                self.shell.shift,
11582                self.shell.alt,
11583                self.shell.meta,
11584            );
11585            (
11586                self.shell.ctrl,
11587                self.shell.shift,
11588                self.shell.alt,
11589                self.shell.meta,
11590            ) = (stroke.ctrl, stroke.shift, stroke.alt, stroke.meta);
11591            self.shell.key_down(
11592                &KeyDown {
11593                    logical_key,
11594                    physical_key,
11595                    text,
11596                    repeat: false,
11597                },
11598                None,
11599            );
11600            (
11601                self.shell.ctrl,
11602                self.shell.shift,
11603                self.shell.alt,
11604                self.shell.meta,
11605            ) = held;
11606        }
11607
11608        /// The two handles of a selection area's touch selection, on the surface, if it has them.
11609        pub fn area_handles(&self) -> Option<[frus_widgets::SelectionHandle; 2]> {
11610            self.shell.region_handles().map(|(_, handles)| handles)
11611        }
11612
11613        fn pointer(&mut self, kind: PointerKind, at: Point) {
11614            self.pointer_of(kind, at, true);
11615        }
11616
11617        fn pointer_of(&mut self, kind: PointerKind, at: Point, touch: bool) {
11618            self.shell.pointer_event(PointerEvent {
11619                kind,
11620                position: at,
11621                touch,
11622            });
11623        }
11624
11625        /// One frame, the real frame's steps in the real frame's order, less the GPU.
11626        pub fn frame(&mut self, dt: f32) {
11627            let (width, height) = (self.size.width, self.size.height);
11628            let s = &mut self.shell;
11629            if let Some((now, _)) = s.gesture_clock.as_mut() {
11630                *now += dt;
11631            }
11632            if s.last_size != Some((width, height)) {
11633                s.last_size = Some((width, height));
11634                s.build_dirty = true;
11635                s.app.on_resize(width, height);
11636            }
11637            let mut app_animating = s.app.tick(dt);
11638            let settings = s
11639                .platform
11640                .accessibility
11641                .with_overrides(s.app.accessibility());
11642            let theme_moved = s.themes.advance(
11643                &s.app,
11644                s.platform.brightness,
11645                settings.high_contrast,
11646                settings.disable_animations,
11647                dt,
11648            );
11649            app_animating |= theme_moved | s.themes.animating();
11650            let theme = s.themes.displayed(&s.app);
11651            install_ambient(&s.app, &s.platform.locales);
11652            let _surface = s.media_query(width, height).install();
11653            s.runtime.still = settings.disable_animations;
11654            let was = std::mem::replace(&mut s.app_was_animating, app_animating);
11655            let asked = s.app.effects();
11656            s.run_command(asked);
11657            let arrived = s.take_task_arrivals();
11658            if frus_widgets::take_rebuild_request() || arrived {
11659                s.build_dirty = true;
11660            }
11661            s.report_focus();
11662            if frame_needs_build(
11663                s.build_dirty,
11664                app_animating,
11665                was,
11666                s.tree.is_none() || s.runtime.switching(),
11667            ) {
11668                s.tree = Some(build_view(&s.app, &theme, &s.runtime));
11669            }
11670            s.build_dirty = false;
11671            s.autoscroll_carried(dt);
11672            s.advance_reorder_springs(dt);
11673            let regions =
11674                s.ui.as_ref()
11675                    .map(|ui| ui.scroll_regions().to_vec())
11676                    .unwrap_or_default();
11677            s.runtime.sync_pages(&regions);
11678            s.runtime.sync_visible(&regions);
11679            s.runtime.advance(dt);
11680            s.runtime.advance_scroll(&regions, dt);
11681            let tree = s.tree.as_deref().expect("the view was built");
11682            // The widgets' own values, as the window's frame steps them: without it a value
11683            // a drag held would be held for ever here (milestone 638).
11684            s.runtime.advance_values(tree, dt);
11685            let ui = build_ui(tree, Size::new(width, height), &s.runtime, &theme);
11686            s.runtime.states.end_frame();
11687            let mut scene = ui.scene().clone();
11688            let ghost = if matches!(s.drag, Some(Drag::Reorder { moved: true, .. })) {
11689                s.paint_reorder_preview(&ui, &theme, &mut scene);
11690                // The ghost's outline is the last thing the preview draws, on the ghost's box.
11691                scene.primitives().last().map(|p| p.bounds())
11692            } else {
11693                None
11694            };
11695            let paged = ui.scroll_regions().to_vec();
11696            let scrolled: Vec<A::Message> = {
11697                let grain = |id| find_widget(tree, id).map_or(0.0, |w| w.scroll_grain());
11698                s.runtime
11699                    .scroll_changes(&paged, grain)
11700                    .into_iter()
11701                    .filter_map(|(id, position)| {
11702                        find_widget(tree, id).and_then(|w| w.on_scroll(position))
11703                    })
11704                    .collect()
11705            };
11706            s.ui = Some(ui);
11707            s.sync_regions(false);
11708            // As the shell does after every frame: focus that vanished goes back, and a
11709            // widget that takes the focus on appearing takes it (milestone 605).
11710            s.reconcile_focus();
11711            for message in scrolled {
11712                s.dispatch(message);
11713            }
11714            self.ghost = ghost;
11715        }
11716    }
11717}
11718
11719/// **What is carried stays carried, however far its list scrolls** (milestone 527, seen on
11720/// a phone).
11721///
11722/// A label held on the Kanban screen's strip and carried towards the strip's right edge was
11723/// not drawn lifted, the strip scrolled away under it, and the release put it somewhere the
11724/// finger had never been. The carried row's box came from the frame, and the frame keeps a
11725/// reorderable's box only while it is on screen, clipped to what shows: once auto-scroll had
11726/// pushed the row against the edge the box stopped moving with the content while the press
11727/// kept moving, the ghost ran ahead of the finger, the scroll fed on it, and a row scrolled
11728/// out of sight had no box at all.
11729///
11730/// These drive the shell's own input path and its frame, on the screen's shape: a bar, a strip
11731/// of ten 96-px labels that lift on a hold, and a board of cards, each in a horizontal scroll.
11732#[cfg(test)]
11733mod carried_row_tests {
11734    use super::testing::Driver;
11735    use crate::{Application, Command};
11736    use frus_widgets::{
11737        column, text, Axis, Button, CellFn, Container, Flex, Kanban, Point, Rect, ReorderAxis,
11738        ReorderGrab, ReorderableList, SingleChildScrollView, Theme, Widget,
11739    };
11740
11741    /// A phone, in logical pixels.
11742    const W: f32 = 392.7;
11743    const H: f32 = 850.9;
11744    /// The height of the labels' middle, under the 56-px bar and the strip's 12 px of padding.
11745    const STRIP_Y: f32 = 88.0;
11746    /// How wide the strip's content is: ten labels, nine gaps, and 24 px at either end.
11747    const STRIP_CONTENT: f32 = 24.0 + 10.0 * 96.0 + 9.0 * 8.0 + 24.0;
11748
11749    #[derive(Clone, Debug, PartialEq)]
11750    enum Board {
11751        Label(usize, usize),
11752        Card(usize, usize, usize, usize),
11753        Delete(usize, usize),
11754        Add(usize),
11755        Row(usize, usize),
11756    }
11757
11758    /// The Kanban screen, in the shape that matters.
11759    struct BoardApp {
11760        labels: Vec<usize>,
11761        cards: Vec<Vec<String>>,
11762        log: Vec<Board>,
11763    }
11764
11765    impl Default for BoardApp {
11766        fn default() -> Self {
11767            let cols: [&[&str]; 3] = [
11768                &["Design API", "Write spec", "Triage bugs"],
11769                &["Build widget"],
11770                &["Kickoff", "Research"],
11771            ];
11772            Self {
11773                labels: (0..10).collect(),
11774                cards: cols
11775                    .iter()
11776                    .map(|c| c.iter().map(|s| s.to_string()).collect())
11777                    .collect(),
11778                log: Vec::new(),
11779            }
11780        }
11781    }
11782
11783    impl Application for BoardApp {
11784        type Message = Board;
11785
11786        fn update(&mut self, message: Board) -> Command<Board> {
11787            self.log.push(message.clone());
11788            match message {
11789                Board::Label(from, to) => {
11790                    let label = self.labels.remove(from);
11791                    self.labels.insert(to, label);
11792                }
11793                Board::Card(from_col, from_pos, to_col, to_pos) => {
11794                    let card = self.cards[from_col].remove(from_pos);
11795                    let to_pos = to_pos.min(self.cards[to_col].len());
11796                    self.cards[to_col].insert(to_pos, card);
11797                }
11798                _ => {}
11799            }
11800            Command::none()
11801        }
11802
11803        fn view(&self, theme: &Theme) -> Box<dyn Widget<Board>> {
11804            let mut strip = ReorderableList::new(Board::Label)
11805                .axis(ReorderAxis::Horizontal)
11806                .grab(ReorderGrab::LongPress)
11807                .gap(8.0);
11808            for &label in &self.labels {
11809                strip = strip.keyed_row(
11810                    label as u64,
11811                    Container::new()
11812                        .width(96.0)
11813                        .padding(12.0)
11814                        .color(theme.surface)
11815                        .child(text(format!("label {label}")).size(14.0)),
11816                );
11817            }
11818            let strip = SingleChildScrollView::new()
11819                .axis(Axis::Horizontal)
11820                .width(W)
11821                .child(
11822                    Container::new()
11823                        .padding_each(12.0, 24.0, 0.0, 24.0)
11824                        .child(strip),
11825                );
11826            let mut board = Kanban::new(Board::Card)
11827                .on_add(Board::Add)
11828                .scrollable_columns();
11829            for (col, title) in ["To do", "Doing", "Done"].iter().enumerate() {
11830                let cards: Vec<CellFn<Board>> = self.cards[col]
11831                    .iter()
11832                    .enumerate()
11833                    .map(|(pos, label)| {
11834                        let label = label.clone();
11835                        Box::new(move || {
11836                            Box::new(
11837                                Flex::row()
11838                                    .child(text(label.clone()).size(14.0))
11839                                    .child(Flex::row().flex(1.0))
11840                                    .child(Button::new("x").on_press(Board::Delete(col, pos))),
11841                            ) as Box<dyn Widget<Board>>
11842                        }) as CellFn<Board>
11843                    })
11844                    .collect();
11845                board = board.column_widgets(*title, cards);
11846            }
11847            let board = SingleChildScrollView::new()
11848                .axis(Axis::Horizontal)
11849                .width(W)
11850                .flex(1.0)
11851                .child(Container::new().padding(24.0).child(board));
11852            Box::new(
11853                Container::new().width(W).height(H).child(
11854                    column![
11855                        Container::<Board>::new().width(W).height(56.0),
11856                        strip,
11857                        board
11858                    ]
11859                    .flex(1.0),
11860                ),
11861            )
11862        }
11863    }
11864
11865    /// Twenty 60-px rows that lift on a hold, in a list taller than the window.
11866    #[derive(Default)]
11867    struct ListApp {
11868        log: Vec<Board>,
11869    }
11870
11871    impl Application for ListApp {
11872        type Message = Board;
11873
11874        fn update(&mut self, message: Board) -> Command<Board> {
11875            self.log.push(message);
11876            Command::none()
11877        }
11878
11879        fn view(&self, _theme: &Theme) -> Box<dyn Widget<Board>> {
11880            let mut list = ReorderableList::new(Board::Row)
11881                .grab(ReorderGrab::LongPress)
11882                .width(W);
11883            for row in 0..20u64 {
11884                list = list.keyed_row(
11885                    row,
11886                    Container::new()
11887                        .height(60.0)
11888                        .child(text(format!("row {row}")).size(14.0)),
11889                );
11890            }
11891            Box::new(
11892                SingleChildScrollView::new()
11893                    .axis(Axis::Vertical)
11894                    .width(W)
11895                    .height(H)
11896                    .child(list),
11897            )
11898        }
11899    }
11900
11901    fn driver<A: Application>(app: A) -> Driver<A> {
11902        let mut driver = Driver::new(app, W, H);
11903        driver.run(0.2);
11904        driver
11905    }
11906
11907    /// A finger held still at `at` until the long press has lifted what is under it.
11908    fn hold<A: Application>(driver: &mut Driver<A>, at: Point) {
11909        driver.press(at);
11910        driver.run(0.5);
11911        assert!(
11912            driver.hold_deadline(),
11913            "the hold lifts what is under {at:?}"
11914        );
11915        driver.run(0.05);
11916    }
11917
11918    /// The finger through `points`, a tenth of a second apart, as the phone's were.
11919    fn carry<A: Application>(driver: &mut Driver<A>, points: impl IntoIterator<Item = Point>) {
11920        for at in points {
11921            driver.move_to(at);
11922            driver.run(0.1);
11923        }
11924    }
11925
11926    /// Frame by frame for `seconds` with the finger still at `finger`: the carried box keeps
11927    /// the place it had under the finger — `offset` from it and `size` — and the region
11928    /// `region` keeps scrolling the way it started. Hands back that region's last offset.
11929    fn held_under_the_finger<A: Application>(
11930        driver: &mut Driver<A>,
11931        seconds: f32,
11932        finger: Point,
11933        offset: (f32, f32),
11934        size: (f32, f32),
11935        region: usize,
11936    ) -> (f32, f32) {
11937        let mut last = driver.offsets()[region].1;
11938        for frame in 0..(seconds * 60.0) as usize {
11939            driver.run(1.0 / 60.0);
11940            let now = driver.offsets()[region].1;
11941            let carried: Rect = driver.carried().unwrap_or_else(|| {
11942                panic!("frame {frame}: nothing is carried any more, the list at {now:?}")
11943            });
11944            assert!(
11945                (carried.x - finger.x - offset.0).abs() < 0.5
11946                    && (carried.y - finger.y - offset.1).abs() < 0.5
11947                    && (carried.width - size.0).abs() < 0.5
11948                    && (carried.height - size.1).abs() < 0.5,
11949                "frame {frame}: carried at {carried:?}, the finger at {finger:?}, the list at {now:?}"
11950            );
11951            assert!(
11952                now.0 >= last.0 - 0.01 && now.1 >= last.1 - 0.01,
11953                "frame {frame}: the list went back, {last:?} then {now:?}"
11954            );
11955            // And it is drawn there: the ghost the finger sees is the box being carried.
11956            let ghost = driver
11957                .ghost()
11958                .unwrap_or_else(|| panic!("frame {frame}: no ghost is drawn"));
11959            assert!(
11960                (ghost.x - carried.x).abs() < 1.0
11961                    && (ghost.y - carried.y).abs() < 1.0
11962                    && (ghost.width - carried.width).abs() < 2.0,
11963                "frame {frame}: the ghost is drawn at {ghost:?}, what is carried is at {carried:?}"
11964            );
11965            last = now;
11966        }
11967        last
11968    }
11969
11970    /// **The reproduction.** A label held and carried past the strip's right edge stays under
11971    /// the finger for as long as it is held there, whole, while the strip scrolls on to its
11972    /// end; and the release drops it where the finger is, past the last label.
11973    #[test]
11974    fn a_label_carried_past_the_strips_edge_stays_under_the_finger() {
11975        let mut driver = driver(BoardApp::default());
11976        hold(&mut driver, Point::new(155.0, STRIP_Y));
11977        let lifted = driver.carried().expect("the label is carried");
11978        assert_eq!(
11979            lifted,
11980            Rect::new(128.0, 68.0, 96.0, 41.0),
11981            "Feature, lifted"
11982        );
11983        let finger = Point::new(370.0, STRIP_Y);
11984        carry(
11985            &mut driver,
11986            [200.0, 260.0, 320.0, finger.x].map(|x| Point::new(x, STRIP_Y)),
11987        );
11988        let strip = held_under_the_finger(
11989            &mut driver,
11990            1.5,
11991            finger,
11992            (lifted.x - 155.0, lifted.y - STRIP_Y),
11993            (96.0, 41.0),
11994            0,
11995        );
11996        assert!(
11997            (strip.0 - (STRIP_CONTENT - W)).abs() < 1.0,
11998            "the strip scrolled on to its end: {strip:?}"
11999        );
12000        driver.release(finger);
12001        driver.run(0.2);
12002        assert_eq!(
12003            driver.app().log,
12004            [Board::Label(1, 9)],
12005            "past the last label, it goes last; no card moves"
12006        );
12007    }
12008
12009    /// **The phone's own gesture**, where the label never reaches an edge: held on Feature,
12010    /// carried along x as the injected moves went, let go over Design's right half.
12011    #[test]
12012    fn a_hold_on_a_label_then_a_carry_along_x_reorders_the_labels_and_moves_no_card() {
12013        let mut driver = driver(BoardApp::default());
12014        hold(&mut driver, Point::new(155.0, STRIP_Y));
12015        carry(
12016            &mut driver,
12017            [167.0, 189.0, 218.0, 247.0, 276.0, 306.0].map(|x| Point::new(x, STRIP_Y)),
12018        );
12019        driver.run(0.4);
12020        assert!(driver.carried().is_some(), "still carried");
12021        driver.release(Point::new(306.0, STRIP_Y));
12022        driver.run(0.2);
12023        assert_eq!(driver.app().log, [Board::Label(1, 2)]);
12024        assert_eq!(driver.app().labels[..4], [0, 2, 1, 3]);
12025        assert_eq!(
12026            driver.app().cards,
12027            BoardApp::default().cards,
12028            "no card moved"
12029        );
12030        assert_eq!(driver.offsets()[0].1, (0.0, 0.0), "nothing scrolled");
12031        assert_eq!(driver.offsets()[1].1, (0.0, 0.0), "the board neither");
12032    }
12033
12034    /// **A card still crosses the board**: Design API, pressed and carried onto the upper
12035    /// half of Build widget, goes to the top of Doing; no label moves.
12036    #[test]
12037    fn a_card_carried_across_the_board_moves_the_card() {
12038        let mut driver = driver(BoardApp::default());
12039        driver.press(Point::new(100.0, 190.0));
12040        carry(
12041            &mut driver,
12042            [130.0, 180.0, 240.0, 300.0].map(|x| Point::new(x, 185.0)),
12043        );
12044        driver.release(Point::new(300.0, 185.0));
12045        driver.run(0.2);
12046        assert_eq!(driver.app().log, [Board::Card(0, 0, 1, 0)]);
12047        assert_eq!(driver.app().labels, (0..10).collect::<Vec<_>>());
12048    }
12049
12050    /// **The board has the same edge.** A card carried to the board's right edge scrolls
12051    /// the board and stays under the finger, although the column it came from scrolls away.
12052    #[test]
12053    fn a_card_carried_to_the_boards_edge_stays_under_the_finger() {
12054        let mut driver = driver(BoardApp::default());
12055        let press = Point::new(100.0, 190.0);
12056        driver.press(press);
12057        let finger = Point::new(385.0, 190.0);
12058        carry(
12059            &mut driver,
12060            [130.0, 200.0, 280.0, 340.0, finger.x].map(|x| Point::new(x, 190.0)),
12061        );
12062        let card = driver.carried().expect("the card is carried");
12063        let board = held_under_the_finger(
12064            &mut driver,
12065            1.0,
12066            finger,
12067            (card.x - finger.x, card.y - finger.y),
12068            (card.width, card.height),
12069            1,
12070        );
12071        assert!(
12072            board.0 > 250.0,
12073            "the board scrolled past the column the card came from: {board:?}"
12074        );
12075    }
12076
12077    /// **A list that runs down has the same edge**: row 0 held and carried to the bottom
12078    /// stays under the finger after its own place has scrolled off the top, and lands low in
12079    /// the list.
12080    #[test]
12081    fn a_row_carried_past_the_bottom_of_a_long_list_stays_under_the_finger() {
12082        let mut driver = driver(ListApp::default());
12083        hold(&mut driver, Point::new(100.0, 30.0));
12084        let lifted = driver.carried().expect("the row is carried");
12085        let finger = Point::new(100.0, 840.0);
12086        carry(
12087            &mut driver,
12088            [200.0, 400.0, 600.0, 800.0, finger.y].map(|y| Point::new(100.0, y)),
12089        );
12090        let list = held_under_the_finger(
12091            &mut driver,
12092            1.5,
12093            finger,
12094            (lifted.x - 100.0, lifted.y - 30.0),
12095            (lifted.width, lifted.height),
12096            0,
12097        );
12098        assert!(
12099            list.1 > 200.0,
12100            "the list scrolled past the row's own place: {list:?}"
12101        );
12102        driver.release(finger);
12103        driver.run(0.2);
12104        assert!(
12105            matches!(driver.app().log[..], [Board::Row(0, to)] if to > 15),
12106            "row 0 lands near the end: {:?}",
12107            driver.app().log
12108        );
12109    }
12110}
12111
12112/// **A press a scroll took is let go of** (milestone 534).
12113///
12114/// The shell's own types need a window and an event loop, so these drive a real frame
12115/// through the pieces `App::pointer` is made of, in the order it calls them: the hover, the
12116/// press where the pointer lands, the gesture the drag handler turns the press into, and
12117/// `settle_press` after every event. What is painted is then compared with a runtime
12118/// nobody pressed.
12119#[cfg(test)]
12120mod press_tests {
12121    use super::{gesture_was_a_tap, press_at, settle_press, Drag, Point, Runtime, Theme};
12122    use crate::gesture::PointerKind;
12123    use crate::hover::Hover;
12124    use frus_widgets::{
12125        build_ui, Axis, Button, Container, Flex, Primitive, SegmentedButton, SingleChildScrollView,
12126        Size, Ui, Widget,
12127    };
12128
12129    const SCREEN: Size = Size::new(300.0, 400.0);
12130    const DT: f32 = 1.0 / 60.0;
12131
12132    /// The phone's screen: a strip of segments too wide for it, in a horizontal scroll.
12133    fn filters() -> Box<dyn Widget<usize>> {
12134        let mut segments = SegmentedButton::new(0, |i| i);
12135        for label in ["All", "Active", "Done", "Archived", "Starred", "Shared"] {
12136            segments = segments.segment(label);
12137        }
12138        Box::new(
12139            Flex::column().width(300.0).height(400.0).child(
12140                SingleChildScrollView::new()
12141                    .axis(Axis::Horizontal)
12142                    .width(300.0)
12143                    .height(80.0)
12144                    .child(segments),
12145            ),
12146        )
12147    }
12148
12149    /// A plain button at the top of a page that scrolls down.
12150    fn page() -> Box<dyn Widget<usize>> {
12151        Box::new(
12152            Flex::column().width(300.0).height(400.0).child(
12153                SingleChildScrollView::new()
12154                    .width(300.0)
12155                    .height(400.0)
12156                    .child(
12157                        Flex::column()
12158                            .child(Button::new("Go").on_press(7usize))
12159                            .child(Container::new().width(300.0).height(1200.0)),
12160                    ),
12161            ),
12162        )
12163    }
12164
12165    fn frame(view: &dyn Widget<usize>, runtime: &Runtime) -> Ui<usize> {
12166        build_ui(view, SCREEN, runtime, &Theme::default())
12167    }
12168
12169    /// Frames go by with nothing else happening, as a phone left alone for `seconds`.
12170    fn idle(runtime: &mut Runtime, seconds: f32) {
12171        for _ in 0..(seconds / DT) as usize {
12172            runtime.advance(DT);
12173            runtime.advance_ink(DT);
12174        }
12175    }
12176
12177    /// What the screen shows.
12178    fn painted(view: &dyn Widget<usize>, runtime: &Runtime) -> Vec<Primitive> {
12179        frame(view, runtime).scene().primitives().to_vec()
12180    }
12181
12182    /// The same screen, scrolled the same, that no pointer has touched.
12183    fn untouched(view: &dyn Widget<usize>, runtime: &Runtime) -> Vec<Primitive> {
12184        let mut quiet = Runtime::default();
12185        quiet.scroll.clone_from(&runtime.scroll);
12186        quiet.scroll_target.clone_from(&runtime.scroll_target);
12187        idle(&mut quiet, 0.5);
12188        painted(view, &quiet)
12189    }
12190
12191    /// A pointer on a screen, routed as the shell routes one.
12192    struct Pointer<'a> {
12193        view: &'a dyn Widget<usize>,
12194        runtime: Runtime,
12195        hover: Hover,
12196        drag: Option<Drag>,
12197        touch: bool,
12198        at: Point,
12199    }
12200
12201    impl<'a> Pointer<'a> {
12202        fn new(view: &'a dyn Widget<usize>, touch: bool) -> Self {
12203            Self {
12204                view,
12205                runtime: Runtime::default(),
12206                hover: Hover::default(),
12207                drag: None,
12208                touch,
12209                at: Point::new(0.0, 0.0),
12210            }
12211        }
12212
12213        /// One event: `route` stands for what the shell's handler makes of it.
12214        fn event(&mut self, kind: PointerKind, at: Point, route: impl FnOnce(&mut Self)) {
12215            self.at = at;
12216            self.hover.event(kind, at, self.touch);
12217            let could_tap = gesture_was_a_tap(self.drag.as_ref());
12218            if kind == PointerKind::Down {
12219                let ui = frame(self.view, &self.runtime);
12220                self.runtime.input.hovered = self.hover.target(&ui);
12221                press_at(&mut self.runtime, Some(&ui), at);
12222            }
12223            route(self);
12224            settle_press(
12225                &mut self.runtime.input,
12226                kind,
12227                could_tap,
12228                self.drag.as_ref(),
12229                self.touch,
12230            );
12231            if matches!(kind, PointerKind::Up | PointerKind::Cancel) {
12232                let ui = frame(self.view, &self.runtime);
12233                self.runtime.input.hovered = self.hover.target(&ui);
12234            }
12235        }
12236
12237        /// A finger lands where a touch scroll takes it, as `pointer_down` prepares one.
12238        fn land(&mut self, at: Point) {
12239            self.event(PointerKind::Down, at, |p| {
12240                if !p.touch {
12241                    return;
12242                }
12243                let ui = frame(p.view, &p.runtime);
12244                let area = ui
12245                    .scroll_chain(at)
12246                    .find(|area| area.accepts_user_offset((0.0, 0.0)))
12247                    .expect("the region under the finger has somewhere to go");
12248                p.drag = Some(Drag::Scroll {
12249                    id: area.id,
12250                    last: at,
12251                    moved: false,
12252                    carried: (0.0, 0.0),
12253                    dismiss: None,
12254                    axis: None,
12255                    pan: None,
12256                });
12257            });
12258        }
12259
12260        /// The pointer moves by `by`; past the slop a touch scroll moves its content, as the
12261        /// drag handler moves it.
12262        fn slide(&mut self, by: (f32, f32)) {
12263            let to = Point::new(self.at.x + by.0, self.at.y + by.1);
12264            self.event(PointerKind::Move, to, |p| {
12265                let Some(Drag::Scroll {
12266                    id,
12267                    moved,
12268                    axis,
12269                    last,
12270                    ..
12271                }) = p.drag.as_mut()
12272                else {
12273                    return;
12274                };
12275                let (dx, dy) = (to.x - last.x, to.y - last.y);
12276                if dx.hypot(dy) <= super::TOUCH_SLOP && !*moved {
12277                    return;
12278                }
12279                *moved = true;
12280                *axis = Some(dy.abs() >= dx.abs());
12281                *last = to;
12282                let offset = p.runtime.scroll.get(id).copied().unwrap_or((0.0, 0.0));
12283                let moved_to = (offset.0 - by.0, offset.1 - by.1);
12284                p.runtime.scroll.insert(*id, moved_to);
12285                p.runtime.scroll_target.insert(*id, moved_to);
12286            });
12287        }
12288
12289        /// The pointer lifts; the release takes the gesture, as `pointer_up` does.
12290        fn lift(&mut self) -> Option<Drag> {
12291            let mut ended = None;
12292            self.event(PointerKind::Up, self.at, |p| ended = p.drag.take());
12293            ended
12294        }
12295    }
12296
12297    /// **Seen on a phone**: a swipe that started on "Done" scrolled the strip, selected
12298    /// nothing — and left "Done" grey, still there two seconds later. Afterwards the screen
12299    /// is the one nobody touched.
12300    #[test]
12301    fn a_segment_a_swipe_started_on_is_not_left_pressed() {
12302        let view = filters();
12303        let mut finger = Pointer::new(view.as_ref(), true);
12304        let done = Point::new(200.0, 16.0);
12305        finger.land(done);
12306        let segment = finger
12307            .runtime
12308            .input
12309            .pressed
12310            .expect("the segment is pressed");
12311        assert!(
12312            finger.runtime.ink.contains_key(&segment),
12313            "and it splashed: a segment takes ink"
12314        );
12315        assert!(
12316            frame(view.as_ref(), &finger.runtime)
12317                .scroll_chain(done)
12318                .any(|area| area.max_x > 0.0),
12319            "the strip is wider than the screen, as the phone's was"
12320        );
12321        for _ in 0..19 {
12322            finger.slide((-10.0, 0.0));
12323        }
12324        let ended = finger.lift();
12325        assert!(
12326            !gesture_was_a_tap(ended.as_ref()),
12327            "a gesture that scrolled clicks nothing"
12328        );
12329        idle(&mut finger.runtime, 2.0);
12330        // What the phone showed first, then why.
12331        let (shown, quiet) = (
12332            painted(view.as_ref(), &finger.runtime),
12333            untouched(view.as_ref(), &finger.runtime),
12334        );
12335        assert_eq!(
12336            shown.len(),
12337            quiet.len(),
12338            "two seconds on, the screen paints what nobody touched, and no layer more"
12339        );
12340        assert_eq!(shown, quiet, "nothing is highlighted");
12341        assert!(
12342            finger.runtime.ink.is_empty(),
12343            "no ink waits for a finger that has gone"
12344        );
12345        assert_eq!(finger.runtime.input.pressed, None, "the press went with it");
12346        assert_eq!(
12347            finger.runtime.input.hovered, None,
12348            "and a lifted finger hovers nothing"
12349        );
12350    }
12351
12352    /// The reference cancels the press **when the scroll wins**, not when the finger lifts:
12353    /// a strip being scrolled does not carry a pressed segment along with it.
12354    #[test]
12355    fn the_press_ends_when_the_scroll_takes_it() {
12356        let view = filters();
12357        let mut finger = Pointer::new(view.as_ref(), true);
12358        finger.land(Point::new(200.0, 16.0));
12359        finger.slide((-4.0, 0.0));
12360        assert!(
12361            finger.runtime.input.pressed.is_some(),
12362            "under the slop it may still be a tap"
12363        );
12364        finger.slide((-40.0, 0.0));
12365        assert_eq!(
12366            finger.runtime.input.pressed, None,
12367            "past it, the scroll has it"
12368        );
12369        assert_eq!(
12370            finger.runtime.input.hovered, None,
12371            "and the finger's hover with it"
12372        );
12373        idle(&mut finger.runtime, 0.5);
12374        assert_eq!(
12375            painted(view.as_ref(), &finger.runtime),
12376            untouched(view.as_ref(), &finger.runtime),
12377            "the finger is still down, and nothing under it is highlighted"
12378        );
12379    }
12380
12381    /// The same down a page: a plain button a scroll started on.
12382    #[test]
12383    fn a_button_a_page_scrolled_from_is_not_left_pressed() {
12384        let view = page();
12385        let mut finger = Pointer::new(view.as_ref(), true);
12386        let go = Point::new(20.0, 15.0);
12387        finger.land(go);
12388        let button = finger.runtime.input.pressed.expect("the button is pressed");
12389        assert_eq!(
12390            frame(view.as_ref(), &finger.runtime).msg_for(button),
12391            Some(7),
12392            "the press is on the button"
12393        );
12394        for _ in 0..10 {
12395            finger.slide((0.0, -30.0));
12396        }
12397        assert!(!gesture_was_a_tap(finger.lift().as_ref()));
12398        idle(&mut finger.runtime, 2.0);
12399        assert_eq!(finger.runtime.input.pressed, None);
12400        assert!(finger.runtime.ink.is_empty());
12401        assert_eq!(
12402            painted(view.as_ref(), &finger.runtime),
12403            untouched(view.as_ref(), &finger.runtime),
12404        );
12405    }
12406
12407    /// **A tap is still a tap**: a finger that stays inside the slop keeps its press to the
12408    /// release, which is what clicks; only then is it let go of.
12409    #[test]
12410    fn a_tap_still_presses_and_clicks() {
12411        let view = filters();
12412        let mut finger = Pointer::new(view.as_ref(), true);
12413        let segment = Point::new(200.0, 16.0);
12414        finger.land(segment);
12415        finger.slide((3.0, 2.0));
12416        let pressed = finger.runtime.input.pressed;
12417        assert!(pressed.is_some(), "a wobble under the slop takes nothing");
12418        assert_eq!(finger.runtime.input.hovered, pressed, "and the press shows");
12419        // What `pointer_up` reads, before the release lets it go.
12420        let ui = frame(view.as_ref(), &finger.runtime);
12421        assert_eq!(
12422            ui.hit(segment),
12423            pressed,
12424            "released on the widget it pressed"
12425        );
12426        assert!(
12427            pressed.and_then(|id| ui.msg_for(id)).is_some(),
12428            "and the segment it pressed has a choice to make"
12429        );
12430        let ended = finger.lift();
12431        assert!(
12432            gesture_was_a_tap(ended.as_ref()),
12433            "a scroll that never moved"
12434        );
12435        assert_eq!(
12436            finger.runtime.input.pressed, None,
12437            "let go of once released"
12438        );
12439    }
12440
12441    /// **A mouse is as it was**: it has no touch scroll, keeps its press while it moves, and
12442    /// still hovers where it is after its button comes up.
12443    #[test]
12444    fn a_mouse_keeps_its_press_while_it_moves_and_its_hover_after() {
12445        let view = filters();
12446        let mut mouse = Pointer::new(view.as_ref(), false);
12447        let done = Point::new(200.0, 16.0);
12448        mouse.land(done);
12449        assert!(mouse.drag.is_none(), "a mouse does not drag a scroll");
12450        let pressed = mouse.runtime.input.pressed.expect("pressed");
12451        mouse.slide((-60.0, 0.0));
12452        assert_eq!(mouse.runtime.input.pressed, Some(pressed), "still held");
12453        mouse.lift();
12454        assert_eq!(mouse.runtime.input.pressed, None);
12455        assert!(
12456            mouse.runtime.input.hovered.is_some(),
12457            "a released mouse is still over the strip"
12458        );
12459    }
12460
12461    /// A gesture a **mouse** moves past its slop takes the press as a finger's does — but
12462    /// not the hover, since the mouse is still there.
12463    #[test]
12464    fn a_gesture_takes_a_mouse_s_press_and_leaves_its_hover() {
12465        let id = frus_widgets::WidgetId::from_u64(3);
12466        let sheet = |moved| Drag::Sheet {
12467            id: frus_widgets::WidgetId::from_u64(9),
12468            last: Point::new(0.0, 0.0),
12469            moved,
12470            available: 800.0,
12471        };
12472        for touch in [false, true] {
12473            let mut input = frus_widgets::InputState {
12474                pressed: Some(id),
12475                hovered: Some(id),
12476                ..Default::default()
12477            };
12478            assert!(!settle_press(
12479                &mut input,
12480                PointerKind::Move,
12481                true,
12482                Some(&sheet(false)),
12483                touch
12484            ));
12485            assert_eq!(input.pressed, Some(id), "not yet past the slop");
12486            assert!(settle_press(
12487                &mut input,
12488                PointerKind::Move,
12489                true,
12490                Some(&sheet(true)),
12491                touch
12492            ));
12493            assert_eq!(input.pressed, None, "touch {touch}");
12494            assert_eq!(input.hovered, (!touch).then_some(id), "touch {touch}");
12495        }
12496        // A gesture that was never a tap — a slider, a selection — had no press to share,
12497        // and moving it lets go of nothing.
12498        let mut input = frus_widgets::InputState {
12499            pressed: Some(id),
12500            ..Default::default()
12501        };
12502        let select = Drag::TextSelect {
12503            id,
12504            rect: super::Rect::new(0.0, 0.0, 10.0, 10.0),
12505        };
12506        assert!(!settle_press(
12507            &mut input,
12508            PointerKind::Move,
12509            false,
12510            Some(&select),
12511            true
12512        ));
12513        assert_eq!(input.pressed, Some(id));
12514    }
12515}
12516
12517/// The mouse, the words and the selection a [`SelectionArea`](frus_widgets::SelectionArea) holds,
12518/// driven through the shell as a person drives it.
12519#[cfg(test)]
12520mod selection_area_tests {
12521    use super::testing::Driver;
12522    use crate::{Application, Command};
12523    use frus_widgets::{Button, Container, Flex, Point, SelectionArea, Text, Theme, Widget};
12524
12525    const W: f32 = 400.0;
12526    const H: f32 = 300.0;
12527
12528    /// A line outside the area, then an area of three lines with a button between two of them.
12529    #[derive(Default)]
12530    struct Doc {
12531        /// What the application says about the area's bar.
12532        bar: Bar,
12533        /// How many times the bar's own item was pressed.
12534        shared: std::rc::Rc<std::cell::Cell<u32>>,
12535        /// The theme's platform: Android unless a test says otherwise, because these are a
12536        /// finger's tests and Android's bar is the pill (milestone 618).
12537        platform: Option<frus_widgets::TargetPlatform>,
12538    }
12539
12540    /// The application's say over the area's bar (milestone 578).
12541    #[derive(Default, Clone, Copy)]
12542    enum Bar {
12543        /// Nothing: Copy and Select all.
12544        #[default]
12545        Default,
12546        /// Its own item, Share, after Copy, and no Select all.
12547        Share,
12548        /// No bar at all.
12549        Empty,
12550    }
12551
12552    impl Application for Doc {
12553        type Message = ();
12554
12555        fn update(&mut self, _message: ()) -> Command<()> {
12556            self.shared.set(self.shared.get() + 1);
12557            Command::none()
12558        }
12559
12560        fn theme(&self) -> Theme {
12561            Theme::default().with_platform(
12562                self.platform
12563                    .unwrap_or(frus_widgets::TargetPlatform::Android),
12564            )
12565        }
12566
12567        fn view(&self, _theme: &Theme) -> Box<dyn Widget<()>> {
12568            let area = SelectionArea::around(
12569                Flex::column()
12570                    .child(Text::new("first line of words").no_wrap())
12571                    .child(Text::new("second line").no_wrap())
12572                    .child(Button::new("Press").on_press(()))
12573                    .child(Text::new("third line ends here").no_wrap()),
12574            );
12575            let area = match self.bar {
12576                Bar::Default => area,
12577                Bar::Share => area.selection_toolbar(|_, items| {
12578                    let mut items: Vec<_> = items
12579                        .into_iter()
12580                        .filter(|item| item.label() == "Copy")
12581                        .collect();
12582                    items.push(frus_widgets::ToolbarItem::custom("Share", ()));
12583                    items
12584                }),
12585                Bar::Empty => area.selection_toolbar(|_, _| Vec::new()),
12586            };
12587            Box::new(
12588                Container::new().width(W).height(H).child(
12589                    Flex::column()
12590                        .child(Text::new("Outside the area").no_wrap())
12591                        .child(area),
12592                ),
12593            )
12594        }
12595    }
12596
12597    /// A row that answers a tap, with words in it, beside words that answer nothing.
12598    struct Tappable;
12599
12600    impl Application for Tappable {
12601        type Message = ();
12602
12603        fn update(&mut self, _message: ()) -> Command<()> {
12604            Command::none()
12605        }
12606
12607        fn view(&self, _theme: &Theme) -> Box<dyn Widget<()>> {
12608            Box::new(
12609                Container::new()
12610                    .width(W)
12611                    .height(H)
12612                    .child(SelectionArea::around(
12613                        Flex::column()
12614                            .child(
12615                                Container::new()
12616                                    .on_click(())
12617                                    .child(Text::new("tap this row").no_wrap()),
12618                            )
12619                            .child(Text::new("plain words here").no_wrap()),
12620                    )),
12621            )
12622        }
12623    }
12624
12625    /// **What answers a tap keeps it, even when it is not a focus stop**: words inside a row that
12626    /// takes a tap begin no selection, and words beside it do. (A button is a focus stop and is
12627    /// kept by that; a row that is only clickable is kept by nothing but this.)
12628    #[test]
12629    fn a_row_that_takes_a_tap_keeps_its_press() {
12630        let mut d = Driver::new(Tappable, W, H);
12631        d.run(0.2);
12632        let find = |d: &Driver<Tappable>, label: &str| {
12633            let (_, rect) = d
12634                .texts()
12635                .into_iter()
12636                .find(|(text, _)| text == label)
12637                .expect("on screen");
12638            Point::new(rect.x + 20.0, rect.y + 8.0)
12639        };
12640        let (tap, plain) = (find(&d, "tap this row"), find(&d, "plain words here"));
12641        let end = Point::new(plain.x + 60.0, plain.y);
12642        d.press_mouse(tap);
12643        d.run(0.05);
12644        d.move_mouse(end);
12645        d.run(0.05);
12646        d.release_mouse(end);
12647        d.run(0.05);
12648        assert_eq!(d.area_selection(), None, "the row's press stayed the row's");
12649
12650        d.press_mouse(plain);
12651        d.run(0.05);
12652        d.move_mouse(end);
12653        d.run(0.05);
12654        d.release_mouse(end);
12655        d.run(0.05);
12656        assert!(d.area_selection().is_some(), "plain words still select");
12657    }
12658
12659    fn driver() -> Driver<Doc> {
12660        driver_with(Bar::Default)
12661    }
12662
12663    fn driver_with(bar: Bar) -> Driver<Doc> {
12664        driver_on(bar, None)
12665    }
12666
12667    fn driver_on(bar: Bar, platform: Option<frus_widgets::TargetPlatform>) -> Driver<Doc> {
12668        let doc = Doc {
12669            bar,
12670            platform,
12671            ..Doc::default()
12672        };
12673        let mut driver = Driver::new(doc, W, H);
12674        driver.run(0.2);
12675        driver
12676    }
12677
12678    /// Where the word `label` starts, and a point on its line a fraction `across` of the way
12679    /// along it.
12680    fn on(driver: &Driver<Doc>, label: &str, across: f32) -> Point {
12681        let (_, rect) = driver
12682            .texts()
12683            .into_iter()
12684            .find(|(text, _)| text == label)
12685            .unwrap_or_else(|| panic!("{label:?} is on screen"));
12686        // A text primitive is a point at its top-left; the line is about 19 px tall and the
12687        // words a few px per character.
12688        Point::new(rect.x + across, rect.y + 8.0)
12689    }
12690
12691    fn drag(driver: &mut Driver<Doc>, from: Point, to: Point) {
12692        driver.press_mouse(from);
12693        driver.run(0.05);
12694        driver.move_mouse(to);
12695        driver.run(0.05);
12696        driver.release_mouse(to);
12697        driver.run(0.05);
12698    }
12699
12700    /// **A drag across texts selects across them**: the tail of the first, the whole of the one
12701    /// between, the head of the last — and what the button between them says is not among the
12702    /// words, because it is not a text of the area's.
12703    #[test]
12704    fn a_drag_selects_across_the_texts_of_an_area() {
12705        let mut d = driver();
12706        let (from, to) = (
12707            on(&d, "first line of words", 60.0),
12708            on(&d, "third line ends here", 40.0),
12709        );
12710        drag(&mut d, from, to);
12711        let copied = d.area_selection().expect("something is selected");
12712        let lines: Vec<&str> = copied.lines().collect();
12713        assert_eq!(lines.len(), 3, "one line per text: {copied:?}");
12714        assert!(
12715            !lines[0].is_empty() && "first line of words".ends_with(lines[0]),
12716            "the tail of the first: {:?}",
12717            lines[0]
12718        );
12719        assert!(lines[0].len() < "first line of words".len());
12720        assert_eq!(lines[1], "second line", "the whole of the middle one");
12721        assert!(
12722            !lines[2].is_empty() && "third line ends here".starts_with(lines[2]),
12723            "the head of the last: {:?}",
12724            lines[2]
12725        );
12726        assert!(lines[2].len() < "third line ends here".len());
12727        assert!(!copied.contains("Press"), "a button's label is not text");
12728    }
12729
12730    /// **Dragged upward it is the same words.**
12731    #[test]
12732    fn dragging_upward_selects_the_same_words() {
12733        let (down, up) = {
12734            let mut d = driver();
12735            let (a, b) = (
12736                on(&d, "first line of words", 60.0),
12737                on(&d, "third line ends here", 40.0),
12738            );
12739            drag(&mut d, a, b);
12740            let down = d.area_selection();
12741            drag(&mut d, b, a);
12742            (down, d.area_selection())
12743        };
12744        assert!(down.is_some());
12745        assert_eq!(down, up);
12746    }
12747
12748    /// **A drag that leaves the words still has an end**: past the right edge of the last line,
12749    /// the selection ends with it, and below everything it ends at the last text.
12750    #[test]
12751    fn a_drag_past_the_words_ends_at_the_last_of_them() {
12752        let mut d = driver();
12753        let from = on(&d, "second line", 5.0);
12754        drag(&mut d, from, Point::new(W - 2.0, H - 2.0));
12755        let copied = d.area_selection().expect("a selection");
12756        assert!(copied.ends_with("third line ends here"), "{copied:?}");
12757    }
12758
12759    /// **Words outside the area are not part of it**: a drag over them selects nothing, and
12760    /// one that starts inside never picks them up.
12761    #[test]
12762    fn words_outside_the_area_are_left_alone() {
12763        let mut d = driver();
12764        let outside = on(&d, "Outside the area", 30.0);
12765        let end = on(&d, "third line ends here", 40.0);
12766        drag(&mut d, outside, end);
12767        assert_eq!(d.area_selection(), None, "the press began outside");
12768
12769        let inside = on(&d, "first line of words", 30.0);
12770        drag(&mut d, inside, Point::new(20.0, 2.0));
12771        let copied = d.area_selection().expect("a selection");
12772        assert!(!copied.contains("Outside"), "{copied:?}");
12773    }
12774
12775    /// **A press on a button is the button's**: no selection begins under it.
12776    #[test]
12777    fn a_button_keeps_its_press() {
12778        let mut d = driver();
12779        let button = on(&d, "Press", 10.0);
12780        let end = on(&d, "third line ends here", 40.0);
12781        drag(&mut d, button, end);
12782        assert_eq!(d.area_selection(), None);
12783    }
12784
12785    /// **A press without a drag puts the selection away**, in the area or out of it.
12786    #[test]
12787    fn a_click_puts_the_selection_away() {
12788        let mut d = driver();
12789        let (from, to) = (
12790            on(&d, "first line of words", 60.0),
12791            on(&d, "third line ends here", 40.0),
12792        );
12793        drag(&mut d, from, to);
12794        assert!(d.area_selection().is_some());
12795        let elsewhere = on(&d, "second line", 30.0);
12796        drag(&mut d, elsewhere, elsewhere);
12797        assert_eq!(d.area_selection(), None, "a click in the area");
12798
12799        drag(&mut d, from, to);
12800        assert!(d.area_selection().is_some());
12801        let outside = on(&d, "Outside the area", 30.0);
12802        drag(&mut d, outside, outside);
12803        assert_eq!(d.area_selection(), None, "a click outside it");
12804    }
12805
12806    /// **A double click selects the word.**
12807    #[test]
12808    fn a_double_click_selects_the_word() {
12809        let mut d = driver();
12810        // "second" is the first word of the line: a few px in.
12811        let at = on(&d, "second line", 12.0);
12812        d.press_mouse(at);
12813        d.release_mouse(at);
12814        d.run(0.05);
12815        d.press_mouse(at);
12816        d.release_mouse(at);
12817        d.run(0.05);
12818        assert_eq!(d.area_selection().as_deref(), Some("second"));
12819    }
12820
12821    /// **Select all takes every text of the area** — and none of what is outside it.
12822    #[test]
12823    fn select_all_takes_the_whole_area() {
12824        let mut d = driver();
12825        let at = on(&d, "second line", 12.0);
12826        let end = on(&d, "second line", 40.0);
12827        drag(&mut d, at, end);
12828        assert!(d.area_selection().is_some());
12829        d.area_select_all();
12830        assert_eq!(
12831            d.area_selection().as_deref(),
12832            Some("first line of words\nsecond line\nthird line ends here")
12833        );
12834    }
12835
12836    /// **A finger scrolls, and does not select**: a touch drag over the words leaves them be.
12837    #[test]
12838    fn a_finger_does_not_select() {
12839        let mut d = driver();
12840        let (from, to) = (
12841            on(&d, "first line of words", 60.0),
12842            on(&d, "third line ends here", 40.0),
12843        );
12844        d.press(from);
12845        d.run(0.05);
12846        d.move_to(to);
12847        d.run(0.05);
12848        d.release(to);
12849        d.run(0.05);
12850        assert_eq!(d.area_selection(), None);
12851    }
12852
12853    /// A finger held still on `at` until the long press fires, then lifted.
12854    fn hold(d: &mut Driver<Doc>, at: Point) -> bool {
12855        d.press(at);
12856        d.run(0.1);
12857        let fired = d.hold_deadline();
12858        d.run(0.05);
12859        d.release(at);
12860        d.run(0.05);
12861        fired
12862    }
12863
12864    /// Where the bar's button reading `label` is, if the bar shows one.
12865    fn bar_button(d: &Driver<Doc>, label: &str) -> Option<Point> {
12866        let (ui, _) = d.frame_parts()?;
12867        d.texts()
12868            .into_iter()
12869            .filter(|(text, _)| text == label)
12870            .map(|(_, rect)| Point::new(rect.x + 4.0, rect.y + 6.0))
12871            .find(|at| ui.toolbar_contains(*at))
12872    }
12873
12874    /// **A finger held on words selects the word under it and opens the bar** — Copy and
12875    /// Select all — over it (milestone 576). A phone has no mouse to drag with.
12876    #[test]
12877    fn a_hold_selects_the_word_and_opens_the_bar() {
12878        let mut d = driver();
12879        let at = on(&d, "second line", 12.0);
12880        assert!(hold(&mut d, at), "the hold fired");
12881        assert_eq!(d.area_selection().as_deref(), Some("second"));
12882        assert!(bar_button(&d, "Copy").is_some(), "Copy is on the bar");
12883        assert!(bar_button(&d, "Select all").is_some(), "and Select all");
12884    }
12885
12886    /// **On a desktop theme the bar is the menu, and its rows act on the area as the pill's
12887    /// buttons do** (milestone 618).
12888    #[test]
12889    fn the_desktop_menus_rows_act_on_the_area() {
12890        let mut d = driver_on(Bar::Default, Some(frus_widgets::TargetPlatform::Linux));
12891        let at = on(&d, "second line", 12.0);
12892        hold(&mut d, at);
12893        let select_all = bar_button(&d, "Select all").expect("Select all");
12894        // The menu hangs from the selection's top, over the handles under it: the press is
12895        // the menu's, not a handle's.
12896        d.press(select_all);
12897        d.release(select_all);
12898        d.run(0.05);
12899        assert_eq!(
12900            d.area_selection().as_deref(),
12901            Some("first line of words\nsecond line\nthird line ends here")
12902        );
12903    }
12904
12905    /// **The bar's Select all takes the whole area and keeps the bar**, which no longer offers
12906    /// Select all; its Copy puts the selection away, as a platform's own does.
12907    #[test]
12908    fn the_bars_buttons_act_on_the_area() {
12909        let mut d = driver();
12910        let at = on(&d, "second line", 12.0);
12911        hold(&mut d, at);
12912        let select_all = bar_button(&d, "Select all").expect("Select all");
12913        d.press(select_all);
12914        d.release(select_all);
12915        d.run(0.05);
12916        assert_eq!(
12917            d.area_selection().as_deref(),
12918            Some("first line of words\nsecond line\nthird line ends here")
12919        );
12920        assert!(
12921            bar_button(&d, "Select all").is_none(),
12922            "nothing left to select"
12923        );
12924        let copy = bar_button(&d, "Copy").expect("Copy stays");
12925        d.press(copy);
12926        d.release(copy);
12927        d.run(0.05);
12928        assert_eq!(d.area_selection(), None, "copied, and put away");
12929        assert!(bar_button(&d, "Copy").is_none(), "the bar with it");
12930    }
12931
12932    /// **A hold on a button is the button's**, and a finger that moves is a scroll, not a hold.
12933    #[test]
12934    fn a_hold_on_a_button_selects_nothing() {
12935        let mut d = driver();
12936        let button = on(&d, "Press", 10.0);
12937        hold(&mut d, button);
12938        assert_eq!(d.area_selection(), None);
12939    }
12940
12941    /// **Escape, and a tap elsewhere, put the selection and its bar away.**
12942    #[test]
12943    fn escape_and_a_tap_put_the_bar_away() {
12944        let mut d = driver();
12945        let at = on(&d, "second line", 12.0);
12946        hold(&mut d, at);
12947        assert!(d.area_selection().is_some());
12948        d.escape();
12949        d.run(0.05);
12950        assert_eq!(d.area_selection(), None);
12951        assert!(bar_button(&d, "Copy").is_none());
12952
12953        hold(&mut d, at);
12954        assert!(d.area_selection().is_some());
12955        let elsewhere = on(&d, "Outside the area", 20.0);
12956        d.press(elsewhere);
12957        d.release(elsewhere);
12958        d.run(0.05);
12959        assert_eq!(d.area_selection(), None);
12960        assert!(bar_button(&d, "Copy").is_none());
12961    }
12962
12963    /// **A selection made with a mouse has no bar**: the bar is for a finger, which has no
12964    /// Ctrl+C.
12965    #[test]
12966    fn a_mouse_selection_has_no_bar() {
12967        let mut d = driver();
12968        let (from, to) = (
12969            on(&d, "first line of words", 60.0),
12970            on(&d, "third line ends here", 40.0),
12971        );
12972        drag(&mut d, from, to);
12973        assert!(d.area_selection().is_some());
12974        assert!(bar_button(&d, "Copy").is_none());
12975    }
12976
12977    /// **The application says what the bar holds** (milestone 578): its own item beside Copy,
12978    /// and no Select all — and pressing its item sends the application's message.
12979    #[test]
12980    fn the_application_says_what_the_bar_holds() {
12981        let mut d = driver_with(Bar::Share);
12982        let at = on(&d, "second line", 12.0);
12983        assert!(hold(&mut d, at));
12984        assert_eq!(d.area_selection().as_deref(), Some("second"));
12985        assert!(bar_button(&d, "Copy").is_some(), "Copy was kept");
12986        assert!(
12987            bar_button(&d, "Select all").is_none(),
12988            "Select all was taken off"
12989        );
12990        let share = bar_button(&d, "Share").expect("the application's own item");
12991        let shared = d.app().shared.clone();
12992        d.press(share);
12993        d.release(share);
12994        d.run(0.05);
12995        assert_eq!(shared.get(), 1, "its message was sent");
12996    }
12997
12998    /// **An empty list shows no bar**, and the selection is still made.
12999    #[test]
13000    fn an_empty_list_shows_no_bar() {
13001        let mut d = driver_with(Bar::Empty);
13002        let at = on(&d, "second line", 12.0);
13003        assert!(hold(&mut d, at));
13004        assert_eq!(d.area_selection().as_deref(), Some("second"));
13005        assert!(bar_button(&d, "Copy").is_none());
13006        assert!(d
13007            .frame_parts()
13008            .is_some_and(|(ui, _)| !ui.toolbar_contains(at)));
13009    }
13010
13011    /// A handle's middle — where a finger takes it.
13012    fn middle(rect: frus_widgets::Rect) -> Point {
13013        Point::new(rect.x + rect.width / 2.0, rect.y + rect.height / 2.0)
13014    }
13015
13016    /// A finger taking a handle at `from` and carrying it to `to`, a few steps at a time.
13017    fn carry_handle(d: &mut Driver<Doc>, from: Point, to: Point) {
13018        d.press(from);
13019        d.run(0.05);
13020        for step in 1..=4 {
13021            let f = step as f32 / 4.0;
13022            d.move_to(Point::new(
13023                from.x + (to.x - from.x) * f,
13024                from.y + (to.y - from.y) * f,
13025            ));
13026            d.run(0.05);
13027        }
13028        d.release(to);
13029        d.run(0.05);
13030    }
13031
13032    /// **A touch selection has two handles**, hanging below the ends of the word, the start one
13033    /// to the left of the end one — and a mouse selection has none.
13034    #[test]
13035    fn a_touch_selection_has_handles_and_a_mouse_one_does_not() {
13036        let mut d = driver();
13037        let paths = |d: &Driver<Doc>| {
13038            let (ui, _) = d.frame_parts().expect("a frame");
13039            ui.scene()
13040                .primitives()
13041                .iter()
13042                .filter(|p| matches!(p, frus_widgets::Primitive::Path { .. }))
13043                .count()
13044        };
13045        let before = paths(&d);
13046        let at = on(&d, "second line", 12.0);
13047        hold(&mut d, at);
13048        assert_eq!(paths(&d), before + 2, "the two handles are painted");
13049        let [start, end] = d.area_handles().expect("two handles");
13050        assert!(start.line_center.x < end.line_center.x, "{start:?} {end:?}");
13051        assert!(start.rect.y > at.y - 8.0, "they hang below the line");
13052
13053        let (from, to) = (
13054            on(&d, "first line of words", 60.0),
13055            on(&d, "third line ends here", 40.0),
13056        );
13057        drag(&mut d, from, to);
13058        assert!(d.area_selection().is_some());
13059        assert!(
13060            d.area_handles().is_none(),
13061            "a mouse selection has no handles"
13062        );
13063        d.area_select_all();
13064        assert!(
13065            d.area_handles().is_none(),
13066            "nor does it grow any when the keyboard selects all"
13067        );
13068    }
13069
13070    /// **The end handle carries the end of the selection to the next text**: dragged down to the
13071    /// third line, the selection runs from the word to it. The bar is put away while the handle
13072    /// moves, and back when it is let go.
13073    #[test]
13074    fn the_end_handle_widens_the_selection_across_texts() {
13075        let mut d = driver();
13076        let at = on(&d, "second line", 12.0);
13077        hold(&mut d, at);
13078        let [_, end] = d.area_handles().expect("handles");
13079        let third = on(&d, "third line ends here", 60.0);
13080        let from = middle(end.rect);
13081        // The handle hangs below its line: carried to where its line would meet the third.
13082        let to = Point::new(third.x, from.y + (third.y - at.y));
13083        d.press(from);
13084        d.run(0.05);
13085        d.move_to(Point::new(from.x, (from.y + to.y) / 2.0));
13086        d.run(0.05);
13087        d.move_to(to);
13088        d.run(0.05);
13089        assert!(
13090            bar_button(&d, "Copy").is_none(),
13091            "no bar while a handle moves"
13092        );
13093        d.release(to);
13094        d.run(0.05);
13095        let copied = d.area_selection().expect("a selection");
13096        let lines: Vec<&str> = copied.lines().collect();
13097        assert_eq!(lines.len(), 2, "{copied:?}");
13098        assert_eq!(lines[0], "second line", "the rest of the word's line");
13099        assert!("third line ends here".starts_with(lines[1]), "{copied:?}");
13100        assert!(!lines[1].is_empty());
13101        assert!(bar_button(&d, "Copy").is_some(), "the bar is back");
13102    }
13103
13104    /// **The start handle carries the start of it back up**: dragged to the first line, the
13105    /// selection begins there.
13106    #[test]
13107    fn the_start_handle_widens_it_upward() {
13108        let mut d = driver();
13109        let at = on(&d, "second line", 80.0);
13110        hold(&mut d, at);
13111        assert_eq!(d.area_selection().as_deref(), Some("line"));
13112        let [start, _] = d.area_handles().expect("handles");
13113        let first = on(&d, "first line of words", 30.0);
13114        let from = middle(start.rect);
13115        let to = Point::new(first.x, from.y + (first.y - at.y));
13116        carry_handle(&mut d, from, to);
13117        let copied = d.area_selection().expect("a selection");
13118        let lines: Vec<&str> = copied.lines().collect();
13119        assert_eq!(lines.len(), 2, "{copied:?}");
13120        assert!("first line of words".ends_with(lines[0]), "{copied:?}");
13121        assert_eq!(lines[1], "second line");
13122    }
13123
13124    /// **A handle cannot cross the other**: the end handle dragged back past the start leaves the
13125    /// selection as it was, rather than turning it inside out.
13126    #[test]
13127    fn a_handle_does_not_cross_the_other() {
13128        let mut d = driver();
13129        let at = on(&d, "second line", 80.0);
13130        hold(&mut d, at);
13131        let before = d.area_selection();
13132        assert!(before.is_some());
13133        let [_, end] = d.area_handles().expect("handles");
13134        let first = on(&d, "first line of words", 5.0);
13135        let from = middle(end.rect);
13136        // In one move, straight to a place before the start: refused, not turned inside out.
13137        let to = Point::new(first.x, from.y + (first.y - at.y));
13138        d.press(from);
13139        d.run(0.05);
13140        d.move_to(to);
13141        d.run(0.05);
13142        d.release(to);
13143        d.run(0.05);
13144        assert_eq!(d.area_selection(), before);
13145    }
13146
13147    /// **The selection is painted**: a highlight over each text it covers, in the theme's
13148    /// colour, and none over the one it does not.
13149    #[test]
13150    fn the_selection_is_highlighted_in_each_text() {
13151        let mut d = driver();
13152        let (from, to) = (
13153            on(&d, "first line of words", 60.0),
13154            on(&d, "third line ends here", 40.0),
13155        );
13156        let bare = rects(&d);
13157        drag(&mut d, from, to);
13158        // What the selection added to the frame: a box over each text it covers.
13159        let boxes: Vec<frus_widgets::Rect> = rects(&d)
13160            .into_iter()
13161            .filter(|rect| !bare.contains(rect))
13162            .collect();
13163        assert_eq!(boxes.len(), 3, "one per text covered: {boxes:?}");
13164        let outside = on(&d, "Outside the area", 0.0);
13165        assert!(
13166            boxes.iter().all(|b| b.y > outside.y + 10.0),
13167            "none over the line outside the area: {boxes:?}"
13168        );
13169    }
13170
13171    fn rects(d: &Driver<Doc>) -> Vec<frus_widgets::Rect> {
13172        let (ui, _) = d.frame_parts().expect("a frame");
13173        ui.scene()
13174            .primitives()
13175            .iter()
13176            .filter_map(|p| match p {
13177                frus_widgets::Primitive::Rect { rect, .. } => Some(*rect),
13178                _ => None,
13179            })
13180            .collect()
13181    }
13182}
13183
13184/// Taps, double taps, long presses and secondary clicks on a
13185/// [`GestureDetector`](frus_widgets::GestureDetector), driven through the shell (milestone 581).
13186#[cfg(test)]
13187mod gesture_detector_tests {
13188    use super::testing::Driver;
13189    use crate::{Application, Command};
13190    use frus_widgets::{Container, Flex, GestureDetector, Point, Text, Theme, Widget};
13191    use std::cell::RefCell;
13192    use std::rc::Rc;
13193
13194    const W: f32 = 400.0;
13195    const H: f32 = 400.0;
13196
13197    #[derive(Clone, Debug, PartialEq)]
13198    enum Msg {
13199        Tap,
13200        Double,
13201        Hold,
13202        Menu,
13203        Plain,
13204        OnlyDouble,
13205    }
13206
13207    /// Three detectors down a page: one that takes everything, one that takes only a tap, and
13208    /// one that takes only a double tap.
13209    #[derive(Default)]
13210    struct Page {
13211        heard: Rc<RefCell<Vec<Msg>>>,
13212    }
13213
13214    impl Application for Page {
13215        type Message = Msg;
13216
13217        fn update(&mut self, message: Msg) -> Command<Msg> {
13218            self.heard.borrow_mut().push(message);
13219            Command::none()
13220        }
13221
13222        fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
13223            let block = |label: &str| {
13224                Container::new()
13225                    .width(300.0)
13226                    .height(60.0)
13227                    .child(Text::new(label).no_wrap())
13228            };
13229            Box::new(
13230                Container::new().width(W).height(H).child(
13231                    Flex::column()
13232                        .gap(40.0)
13233                        .child(
13234                            GestureDetector::new(block("everything"))
13235                                .on_tap(Msg::Tap)
13236                                .on_double_tap(Msg::Double)
13237                                .on_long_press(Msg::Hold)
13238                                .on_secondary_tap(Msg::Menu),
13239                        )
13240                        .child(GestureDetector::new(block("tap only")).on_tap(Msg::Plain))
13241                        .child(
13242                            GestureDetector::new(block("double only"))
13243                                .on_double_tap(Msg::OnlyDouble),
13244                        ),
13245                ),
13246            )
13247        }
13248    }
13249
13250    fn driver() -> (Driver<Page>, Rc<RefCell<Vec<Msg>>>) {
13251        let page = Page::default();
13252        let heard = page.heard.clone();
13253        let mut driver = Driver::new(page, W, H);
13254        driver.run(0.2);
13255        (driver, heard)
13256    }
13257
13258    /// A point inside the block labelled `label`.
13259    fn on(d: &Driver<Page>, label: &str) -> Point {
13260        let (_, rect) = d
13261            .texts()
13262            .into_iter()
13263            .find(|(text, _)| text == label)
13264            .unwrap_or_else(|| panic!("{label:?} is on screen"));
13265        Point::new(rect.x + 20.0, rect.y + 8.0)
13266    }
13267
13268    fn tap(d: &mut Driver<Page>, at: Point) {
13269        d.press(at);
13270        d.run(0.02);
13271        d.release(at);
13272        d.run(0.02);
13273    }
13274
13275    /// **With a double tap possible, a tap waits**: nothing is sent at the release, and the
13276    /// tap is sent once the wait is over.
13277    #[test]
13278    fn a_tap_waits_while_a_double_tap_is_possible() {
13279        let (mut d, heard) = driver();
13280        let at = on(&d, "everything");
13281        tap(&mut d, at);
13282        assert!(heard.borrow().is_empty(), "not yet: {:?}", heard.borrow());
13283        assert!(d.tap_deadline(), "a tap was waiting");
13284        assert_eq!(*heard.borrow(), vec![Msg::Tap]);
13285    }
13286
13287    /// **The loop wakes when the wait is over**, so a waiting tap is sent without anything
13288    /// else happening; and nothing is scheduled once it is gone.
13289    #[test]
13290    fn the_loop_wakes_for_a_waiting_tap() {
13291        let (mut d, _) = driver();
13292        let before = std::time::Instant::now();
13293        let at = on(&d, "everything");
13294        tap(&mut d, at);
13295        let wake = d.next_wake().expect("a wake for the tap");
13296        let wait = wake.duration_since(before);
13297        assert!(
13298            wait <= std::time::Duration::from_millis(400),
13299            "about 300 ms: {wait:?}"
13300        );
13301        d.tap_deadline();
13302        assert_eq!(d.next_wake(), None);
13303    }
13304
13305    /// **Two taps in quick succession are a double tap**, and not also a tap.
13306    #[test]
13307    fn two_quick_taps_are_a_double_tap() {
13308        let (mut d, heard) = driver();
13309        let at = on(&d, "everything");
13310        tap(&mut d, at);
13311        tap(&mut d, Point::new(at.x + 6.0, at.y + 2.0));
13312        assert_eq!(*heard.borrow(), vec![Msg::Double]);
13313        assert!(!d.tap_deadline(), "nothing is left waiting");
13314        assert_eq!(*heard.borrow(), vec![Msg::Double]);
13315    }
13316
13317    /// **Without a double tap, a tap answers at once.**
13318    #[test]
13319    fn a_tap_alone_answers_at_once() {
13320        let (mut d, heard) = driver();
13321        let at = on(&d, "tap only");
13322        tap(&mut d, at);
13323        assert_eq!(*heard.borrow(), vec![Msg::Plain]);
13324        tap(&mut d, at);
13325        assert_eq!(
13326            *heard.borrow(),
13327            vec![Msg::Plain, Msg::Plain],
13328            "two taps are two taps"
13329        );
13330    }
13331
13332    /// **A press somewhere else ends the wait**: the first tap is a tap after all, sent before
13333    /// what the second press does.
13334    #[test]
13335    fn a_press_elsewhere_sends_the_waiting_tap() {
13336        let (mut d, heard) = driver();
13337        let (first, other) = (on(&d, "everything"), on(&d, "tap only"));
13338        tap(&mut d, first);
13339        tap(&mut d, other);
13340        assert_eq!(*heard.borrow(), vec![Msg::Tap, Msg::Plain]);
13341    }
13342
13343    /// **The secondary button** sends its own message, and no tap.
13344    #[test]
13345    fn a_secondary_click_sends_its_message() {
13346        let (mut d, heard) = driver();
13347        let at = on(&d, "everything");
13348        d.secondary_click(at);
13349        d.run(0.02);
13350        assert_eq!(*heard.borrow(), vec![Msg::Menu]);
13351        let plain = on(&d, "tap only");
13352        d.secondary_click(plain);
13353        assert_eq!(
13354            *heard.borrow(),
13355            vec![Msg::Menu],
13356            "a detector without one hears nothing"
13357        );
13358    }
13359
13360    /// **A long press** sends its message, and the release after it is not a tap.
13361    #[test]
13362    fn a_long_press_is_not_also_a_tap() {
13363        let (mut d, heard) = driver();
13364        let at = on(&d, "everything");
13365        d.press(at);
13366        d.run(0.1);
13367        assert!(d.hold_deadline(), "the hold fired");
13368        d.release(at);
13369        d.run(0.02);
13370        d.tap_deadline();
13371        assert_eq!(*heard.borrow(), vec![Msg::Hold]);
13372    }
13373
13374    /// **A detector with only a double tap** takes the press all the same: one tap sends
13375    /// nothing, two send the double tap.
13376    #[test]
13377    fn a_double_tap_alone_is_heard() {
13378        let (mut d, heard) = driver();
13379        let at = on(&d, "double only");
13380        tap(&mut d, at);
13381        d.tap_deadline();
13382        assert!(heard.borrow().is_empty(), "{:?}", heard.borrow());
13383        tap(&mut d, at);
13384        tap(&mut d, at);
13385        assert_eq!(*heard.borrow(), vec![Msg::OnlyDouble]);
13386    }
13387}
13388
13389/// Drags on a [`GestureDetector`](frus_widgets::GestureDetector), alone and inside a scroll,
13390/// driven through the shell (milestone 582).
13391#[cfg(test)]
13392mod gesture_pan_tests {
13393    use super::testing::Driver;
13394    use crate::{Application, Command};
13395    use frus_widgets::{
13396        Container, Flex, GestureDetector, PanAxis, Point, SingleChildScrollView, Text, Theme,
13397        Widget,
13398    };
13399    use std::cell::RefCell;
13400    use std::rc::Rc;
13401
13402    const W: f32 = 400.0;
13403    const H: f32 = 400.0;
13404
13405    #[derive(Clone, Debug, PartialEq)]
13406    enum Msg {
13407        Start(Point),
13408        Update(Point, Point),
13409        End(Point),
13410        Tap,
13411        Hold,
13412    }
13413
13414    /// A detector, either alone on the page or at the top of a tall vertical scroll.
13415    struct Page {
13416        axis: PanAxis,
13417        scrolled: bool,
13418        heard: Rc<RefCell<Vec<Msg>>>,
13419    }
13420
13421    impl Application for Page {
13422        type Message = Msg;
13423
13424        fn update(&mut self, message: Msg) -> Command<Msg> {
13425            self.heard.borrow_mut().push(message);
13426            Command::none()
13427        }
13428
13429        fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
13430            let detector = GestureDetector::new(
13431                Container::new()
13432                    .width(300.0)
13433                    .height(150.0)
13434                    .child(Text::new("drag me").no_wrap()),
13435            )
13436            .on_tap(Msg::Tap)
13437            .on_long_press(Msg::Hold)
13438            .on_pan_start(Msg::Start)
13439            .on_pan_update(Msg::Update)
13440            .on_pan_end(Msg::End)
13441            .pan_axis(self.axis);
13442            let content = Flex::column()
13443                .child(Container::new().width(300.0).height(30.0))
13444                .child(detector)
13445                .child(Container::new().width(300.0).height(1200.0));
13446            if self.scrolled {
13447                Box::new(
13448                    SingleChildScrollView::new()
13449                        .width(W)
13450                        .height(H)
13451                        .child(content),
13452                )
13453            } else {
13454                Box::new(Container::new().width(W).height(H).child(content))
13455            }
13456        }
13457    }
13458
13459    fn driver(axis: PanAxis, scrolled: bool) -> (Driver<Page>, Rc<RefCell<Vec<Msg>>>) {
13460        let heard = Rc::new(RefCell::new(Vec::new()));
13461        let page = Page {
13462            axis,
13463            scrolled,
13464            heard: heard.clone(),
13465        };
13466        let mut driver = Driver::new(page, W, H);
13467        driver.run(0.2);
13468        (driver, heard)
13469    }
13470
13471    /// Where the detector's words start.
13472    fn origin(d: &Driver<Page>) -> Point {
13473        let (_, rect) = d
13474            .texts()
13475            .into_iter()
13476            .find(|(text, _)| text == "drag me")
13477            .expect("on screen");
13478        Point::new(rect.x, rect.y)
13479    }
13480
13481    /// A finger from `from` by `(dx, dy)`, in eight steps.
13482    fn swipe(d: &mut Driver<Page>, from: Point, dx: f32, dy: f32) {
13483        d.press(from);
13484        d.run(0.02);
13485        for step in 1..=8 {
13486            let f = step as f32 / 8.0;
13487            d.move_to(Point::new(from.x + dx * f, from.y + dy * f));
13488            d.run(0.02);
13489        }
13490        d.release(Point::new(from.x + dx, from.y + dy));
13491        d.run(0.02);
13492    }
13493
13494    fn deltas(heard: &[Msg]) -> Point {
13495        heard.iter().fold(Point::new(0.0, 0.0), |sum, m| match m {
13496            Msg::Update(_, delta) => Point::new(sum.x + delta.x, sum.y + delta.y),
13497            _ => sum,
13498        })
13499    }
13500
13501    fn scroll_y(d: &Driver<Page>) -> f32 {
13502        d.offsets().first().map_or(0.0, |(_, offset)| offset.1)
13503    }
13504
13505    /// **A drag is a start, movements and an end**: the start where the finger went down in
13506    /// the detector's own coordinates, and movements that add up to the way it went once past
13507    /// the slop.
13508    #[test]
13509    fn a_drag_is_a_start_updates_and_an_end() {
13510        let (mut d, heard) = driver(PanAxis::Free, false);
13511        let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 40.0);
13512        swipe(&mut d, at, 120.0, 60.0);
13513        let heard = heard.borrow();
13514        assert!(matches!(heard.first(), Some(Msg::Start(_))), "{heard:?}");
13515        if let Some(Msg::Start(local)) = heard.first() {
13516            // The words sit at the detector's corner, 30 px down the page.
13517            let corner = origin(&d);
13518            assert!(corner.y >= 30.0, "{corner:?}");
13519            assert_eq!(*local, Point::new(at.x - corner.x, at.y - corner.y));
13520        }
13521        assert!(matches!(heard.last(), Some(Msg::End(_))), "{heard:?}");
13522        if let Some(Msg::End(velocity)) = heard.last() {
13523            assert!(velocity.x.hypot(velocity.y) <= 8000.0 + 0.5, "{velocity:?}");
13524        }
13525        let moved = deltas(&heard);
13526        assert!(moved.x > 60.0 && moved.y > 20.0, "{moved:?}");
13527        assert!(!heard.contains(&Msg::Tap), "a drag is not a tap");
13528        // The last movement is where the finger lifted: nothing was left behind on the way.
13529        let last = heard.iter().rev().find_map(|m| match m {
13530            Msg::Update(local, _) => Some(*local),
13531            _ => None,
13532        });
13533        let corner = origin(&d);
13534        assert_eq!(
13535            last,
13536            Some(Point::new(at.x + 120.0 - corner.x, at.y + 60.0 - corner.y))
13537        );
13538    }
13539
13540    /// **A press that barely moves is a tap**, and no drag.
13541    #[test]
13542    fn a_press_that_does_not_move_is_a_tap() {
13543        let (mut d, heard) = driver(PanAxis::Free, false);
13544        let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 40.0);
13545        swipe(&mut d, at, 3.0, 2.0);
13546        assert_eq!(*heard.borrow(), vec![Msg::Tap]);
13547    }
13548
13549    /// **A hold on a detector that also drags is a long press**: the drag waiting for the
13550    /// finger to move does not keep the hold from being one (seen on the phone).
13551    #[test]
13552    fn a_hold_on_a_dragging_detector_is_a_long_press() {
13553        let (mut d, heard) = driver(PanAxis::Free, false);
13554        let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 40.0);
13555        d.press(at);
13556        d.run(0.1);
13557        assert!(d.hold_deadline(), "the hold fired");
13558        d.release(at);
13559        d.run(0.02);
13560        assert_eq!(*heard.borrow(), vec![Msg::Hold]);
13561    }
13562
13563    /// **The mouse drags too**, with its own, finer slop.
13564    #[test]
13565    fn a_mouse_drags_it() {
13566        let (mut d, heard) = driver(PanAxis::Free, false);
13567        let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 40.0);
13568        d.press_mouse(at);
13569        d.run(0.02);
13570        d.move_mouse(Point::new(at.x + 30.0, at.y));
13571        d.run(0.02);
13572        d.release_mouse(Point::new(at.x + 30.0, at.y));
13573        d.run(0.02);
13574        let heard = heard.borrow();
13575        assert!(matches!(heard.first(), Some(Msg::Start(_))), "{heard:?}");
13576        assert!(matches!(heard.last(), Some(Msg::End(_))), "{heard:?}");
13577    }
13578
13579    /// **A horizontal drag in a vertical scroll**: across, the drag has it and the page stays;
13580    /// down, the page scrolls and the drag hears nothing. Its movements are only across.
13581    #[test]
13582    fn a_horizontal_drag_shares_a_vertical_scroll() {
13583        let (mut d, heard) = driver(PanAxis::Horizontal, true);
13584        let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 40.0);
13585        swipe(&mut d, at, 120.0, 10.0);
13586        assert!(
13587            matches!(heard.borrow().first(), Some(Msg::Start(_))),
13588            "{:?}",
13589            heard.borrow()
13590        );
13591        assert_eq!(deltas(&heard.borrow()).y, 0.0, "only across");
13592        assert_eq!(scroll_y(&d), 0.0, "the page did not move");
13593
13594        heard.borrow_mut().clear();
13595        let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 120.0);
13596        swipe(&mut d, at, 5.0, -120.0);
13597        assert!(heard.borrow().is_empty(), "{:?}", heard.borrow());
13598        assert!(scroll_y(&d) > 30.0, "the page scrolled: {}", scroll_y(&d));
13599    }
13600
13601    /// **A free drag in a vertical scroll** takes a movement across and leaves a movement
13602    /// down to the scroll, as the reference's pan does.
13603    #[test]
13604    fn a_free_drag_leaves_the_scroll_its_own_direction() {
13605        let (mut d, heard) = driver(PanAxis::Free, true);
13606        let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 40.0);
13607        swipe(&mut d, at, 150.0, 8.0);
13608        assert!(
13609            matches!(heard.borrow().first(), Some(Msg::Start(_))),
13610            "{:?}",
13611            heard.borrow()
13612        );
13613        assert_eq!(scroll_y(&d), 0.0);
13614
13615        heard.borrow_mut().clear();
13616        let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 120.0);
13617        swipe(&mut d, at, 4.0, -120.0);
13618        assert!(heard.borrow().is_empty(), "{:?}", heard.borrow());
13619        assert!(scroll_y(&d) > 30.0, "the page scrolled: {}", scroll_y(&d));
13620    }
13621
13622    /// **A machine that stalls in the middle of a drag does not change it** (milestone 584,
13623    /// seen in CI). The shell's gestures are timed by the driver's frames and not by the wall:
13624    /// with the wall's clock, twenty milliseconds lost between two movements a microsecond
13625    /// apart read as a fling in either direction, and a page scrolled down was thrown back to
13626    /// the top.
13627    #[test]
13628    fn a_stall_mid_drag_does_not_change_the_scroll() {
13629        let (mut d, heard) = driver(PanAxis::Free, true);
13630        let from = Point::new(origin(&d).x + 40.0, origin(&d).y + 120.0);
13631        d.press(from);
13632        d.run(0.02);
13633        for step in 1..=8 {
13634            let f = step as f32 / 8.0;
13635            d.move_to(Point::new(from.x + 4.0 * f, from.y - 120.0 * f));
13636            if step == 5 {
13637                std::thread::sleep(std::time::Duration::from_millis(20));
13638            }
13639            d.run(0.02);
13640        }
13641        d.release(Point::new(from.x + 4.0, from.y - 120.0));
13642        d.run(0.02);
13643        assert!(heard.borrow().is_empty(), "{:?}", heard.borrow());
13644        assert!(scroll_y(&d) > 30.0, "the page scrolled: {}", scroll_y(&d));
13645    }
13646
13647    /// **A vertical drag in a vertical scroll** is the detector's: it asked for that
13648    /// direction, and it is under the finger.
13649    #[test]
13650    fn a_vertical_drag_takes_it_from_the_scroll() {
13651        let (mut d, heard) = driver(PanAxis::Vertical, true);
13652        let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 120.0);
13653        swipe(&mut d, at, 6.0, -100.0);
13654        let heard = heard.borrow();
13655        assert!(matches!(heard.first(), Some(Msg::Start(_))), "{heard:?}");
13656        let moved = deltas(&heard);
13657        assert_eq!(moved.x, 0.0, "only down");
13658        assert!(moved.y < -40.0, "{moved:?}");
13659        assert_eq!(scroll_y(&d), 0.0, "the page did not move");
13660    }
13661}
13662
13663/// The pointer entering, moving over and leaving a
13664/// [`MouseRegion`](frus_widgets::MouseRegion), driven through the shell (milestone 583).
13665#[cfg(test)]
13666mod mouse_region_tests {
13667    use super::testing::Driver;
13668    use crate::{Application, Command};
13669    use frus_widgets::{
13670        Container, Cursor, Flex, MouseRegion, Point, Positioned, Stack, TextField, Theme, Widget,
13671    };
13672    use std::cell::RefCell;
13673    use std::rc::Rc;
13674
13675    const W: f32 = 400.0;
13676    const H: f32 = 600.0;
13677
13678    #[derive(Clone, Debug, PartialEq)]
13679    enum Msg {
13680        Enter(&'static str, Point),
13681        Move(&'static str),
13682        Exit(&'static str),
13683        Show,
13684    }
13685
13686    /// An outer region holding an inner one at its top; below them, a big region with a
13687    /// smaller one overlapping it — not inside it — in a stack; and a region that appears
13688    /// when told to.
13689    struct Page {
13690        front_opaque: bool,
13691        shown: bool,
13692        heard: Rc<RefCell<Vec<Msg>>>,
13693    }
13694
13695    fn region(name: &'static str, child: impl Widget<Msg> + 'static) -> MouseRegion<Msg> {
13696        MouseRegion::new(child)
13697            .on_enter(move |at| Msg::Enter(name, at))
13698            .on_hover(move |_| Msg::Move(name))
13699            .on_exit(move |_| Msg::Exit(name))
13700    }
13701
13702    impl Application for Page {
13703        type Message = Msg;
13704
13705        fn update(&mut self, message: Msg) -> Command<Msg> {
13706            if message == Msg::Show {
13707                self.shown = true;
13708            } else {
13709                self.heard.borrow_mut().push(message);
13710            }
13711            Command::none()
13712        }
13713
13714        fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
13715            // 0..200: outer (300 wide), with inner 100×50 at its top-left.
13716            let nested = region(
13717                "outer",
13718                Container::new().width(300.0).height(200.0).child(
13719                    region("inner", Container::new().width(100.0).height(50.0))
13720                        .cursor(Cursor::Pointer),
13721                ),
13722            )
13723            .cursor(Cursor::Grab);
13724            // 200..400: back fills a 300×200 stack; front sits at (50, 50), 100×100, over it.
13725            let stack = Stack::new()
13726                .width(300.0)
13727                .height(200.0)
13728                .layer(region("back", Container::new().width(300.0).height(200.0)))
13729                .layer(
13730                    Positioned::new(
13731                        region("front", Container::new().width(100.0).height(100.0))
13732                            .opaque(self.front_opaque),
13733                    )
13734                    .left(50.0)
13735                    .top(50.0),
13736                );
13737            // 400..500: a field inside a region that asks for a hand.
13738            let field = region("field", TextField::new("").width(300.0)).cursor(Cursor::Pointer);
13739            // 500..600: a region that appears.
13740            let appearing: Box<dyn Widget<Msg>> = if self.shown {
13741                Box::new(region("late", Container::new().width(300.0).height(100.0)))
13742            } else {
13743                Box::new(Container::new().width(300.0).height(100.0))
13744            };
13745            Box::new(
13746                Container::new().width(W).height(H).child(
13747                    Flex::column()
13748                        .child(nested)
13749                        .child(stack)
13750                        .child(Container::new().height(100.0).child(field))
13751                        .child(appearing),
13752                ),
13753            )
13754        }
13755    }
13756
13757    fn driver(front_opaque: bool) -> (Driver<Page>, Rc<RefCell<Vec<Msg>>>) {
13758        let heard = Rc::new(RefCell::new(Vec::new()));
13759        let page = Page {
13760            front_opaque,
13761            shown: false,
13762            heard: heard.clone(),
13763        };
13764        let mut driver = Driver::new(page, W, H);
13765        driver.run(0.2);
13766        (driver, heard)
13767    }
13768
13769    fn take(heard: &Rc<RefCell<Vec<Msg>>>) -> Vec<Msg> {
13770        std::mem::take(&mut *heard.borrow_mut())
13771    }
13772
13773    fn mouse(d: &mut Driver<Page>, x: f32, y: f32) {
13774        d.move_mouse(Point::new(x, y));
13775        d.run(0.02);
13776    }
13777
13778    /// **Into a region inside another, both are entered**, the outer first, each told where
13779    /// the pointer is in its own coordinates; a move inside is heard by both; out of the
13780    /// inner one only it is left, and out of both the outer one is too.
13781    #[test]
13782    fn nested_regions_are_entered_and_left_together() {
13783        let (mut d, heard) = driver(true);
13784        mouse(&mut d, 20.0, 10.0);
13785        assert_eq!(
13786            take(&heard),
13787            vec![
13788                Msg::Enter("outer", Point::new(20.0, 10.0)),
13789                Msg::Enter("inner", Point::new(20.0, 10.0)),
13790            ]
13791        );
13792        mouse(&mut d, 30.0, 12.0);
13793        let moved = take(&heard);
13794        assert!(moved.contains(&Msg::Move("inner")) && moved.contains(&Msg::Move("outer")));
13795        mouse(&mut d, 200.0, 150.0);
13796        assert_eq!(take(&heard), vec![Msg::Exit("inner"), Msg::Move("outer")]);
13797        mouse(&mut d, 350.0, 150.0);
13798        assert_eq!(take(&heard), vec![Msg::Exit("outer")]);
13799    }
13800
13801    /// **An opaque region hides the one behind it**: over the overlap, only the front one is
13802    /// in; one that is not opaque lets the back one in too.
13803    #[test]
13804    fn an_opaque_region_hides_the_one_behind() {
13805        let (mut d, heard) = driver(true);
13806        mouse(&mut d, 80.0, 280.0);
13807        let entered: Vec<_> = take(&heard);
13808        assert!(
13809            entered.iter().any(|m| matches!(m, Msg::Enter("front", _))),
13810            "{entered:?}"
13811        );
13812        assert!(
13813            !entered.iter().any(|m| matches!(m, Msg::Enter("back", _))),
13814            "{entered:?}"
13815        );
13816        // Out of the front one and still in the back: the back one is entered now.
13817        mouse(&mut d, 250.0, 280.0);
13818        let after = take(&heard);
13819        assert!(after.contains(&Msg::Exit("front")), "{after:?}");
13820        assert!(
13821            after.iter().any(|m| matches!(m, Msg::Enter("back", _))),
13822            "{after:?}"
13823        );
13824
13825        let (mut d, heard) = driver(false);
13826        mouse(&mut d, 80.0, 280.0);
13827        let entered = take(&heard);
13828        assert!(
13829            entered.iter().any(|m| matches!(m, Msg::Enter("front", _))),
13830            "{entered:?}"
13831        );
13832        assert!(
13833            entered.iter().any(|m| matches!(m, Msg::Enter("back", _))),
13834            "{entered:?}"
13835        );
13836    }
13837
13838    /// **With a button held, a region is entered and left but hears no moves**, as the
13839    /// reference's hover is only a pointer with nothing pressed.
13840    #[test]
13841    fn no_moves_while_a_button_is_held() {
13842        let (mut d, heard) = driver(true);
13843        mouse(&mut d, 350.0, 150.0);
13844        take(&heard);
13845        d.press_mouse(Point::new(350.0, 150.0));
13846        d.run(0.02);
13847        d.move_mouse(Point::new(40.0, 20.0));
13848        d.run(0.02);
13849        d.move_mouse(Point::new(45.0, 22.0));
13850        d.run(0.02);
13851        let heard_now = take(&heard);
13852        assert!(
13853            heard_now
13854                .iter()
13855                .any(|m| matches!(m, Msg::Enter("inner", _))),
13856            "{heard_now:?}"
13857        );
13858        assert!(
13859            !heard_now.iter().any(|m| matches!(m, Msg::Move(_))),
13860            "{heard_now:?}"
13861        );
13862        d.release_mouse(Point::new(45.0, 22.0));
13863        d.run(0.02);
13864    }
13865
13866    /// **A finger is in a region while it touches**: entered at the press, left at the lift.
13867    #[test]
13868    fn a_finger_enters_while_it_touches() {
13869        let (mut d, heard) = driver(true);
13870        d.press(Point::new(200.0, 150.0));
13871        d.run(0.02);
13872        assert_eq!(
13873            take(&heard),
13874            vec![Msg::Enter("outer", Point::new(200.0, 150.0))]
13875        );
13876        d.release(Point::new(200.0, 150.0));
13877        d.run(0.02);
13878        assert_eq!(take(&heard), vec![Msg::Exit("outer")]);
13879    }
13880
13881    /// **A region that comes under a still pointer is entered** at the next frame, with no
13882    /// movement at all.
13883    #[test]
13884    fn a_region_appearing_under_the_pointer_is_entered() {
13885        let (mut d, heard) = driver(true);
13886        mouse(&mut d, 150.0, 550.0);
13887        assert!(take(&heard).is_empty());
13888        d.update(Msg::Show);
13889        d.run(0.05);
13890        let heard_now = take(&heard);
13891        assert!(
13892            heard_now.iter().any(|m| matches!(m, Msg::Enter("late", _))),
13893            "{heard_now:?}"
13894        );
13895    }
13896
13897    /// **The cursor is the innermost region's**, unless what is under the pointer asks for
13898    /// one of its own: a field keeps its text cursor inside a region asking for a hand.
13899    #[test]
13900    fn the_innermost_region_sets_the_cursor() {
13901        let (mut d, _) = driver(true);
13902        mouse(&mut d, 20.0, 10.0);
13903        assert_eq!(d.cursor_shown(), Cursor::Pointer, "the inner region's");
13904        mouse(&mut d, 200.0, 150.0);
13905        assert_eq!(d.cursor_shown(), Cursor::Grab, "the outer region's");
13906        mouse(&mut d, 350.0, 150.0);
13907        assert_eq!(d.cursor_shown(), Cursor::Default, "no region");
13908        mouse(&mut d, 100.0, 420.0);
13909        assert_eq!(d.cursor_shown(), Cursor::Text, "the field's own");
13910    }
13911}
13912
13913/// The pointer's raw events on a [`Listener`](frus_widgets::Listener), driven through the
13914/// shell (milestone 586).
13915#[cfg(test)]
13916mod listener_tests {
13917    use super::testing::Driver;
13918    use crate::{Application, Command};
13919    use frus_widgets::{
13920        Button, Container, Flex, Listener, ListenerEvent, ListenerEventKind as Raw, Point,
13921        SingleChildScrollView, Theme, Widget,
13922    };
13923    use std::cell::RefCell;
13924    use std::rc::Rc;
13925
13926    const W: f32 = 400.0;
13927    const H: f32 = 400.0;
13928
13929    #[derive(Clone, Debug, PartialEq)]
13930    enum Msg {
13931        Heard(&'static str, Raw, Point),
13932        Pressed,
13933    }
13934
13935    /// In a scroll taller than the window: an outer listener (300×300 at the top, 40 px down)
13936    /// holding an inner one (100×100 at its top-left) with a button in it.
13937    struct Page {
13938        heard: Rc<RefCell<Vec<Msg>>>,
13939    }
13940
13941    fn listen(name: &'static str) -> impl Fn(ListenerEvent) -> Option<Msg> {
13942        move |event| Some(Msg::Heard(name, event.kind, event.local))
13943    }
13944
13945    impl Application for Page {
13946        type Message = Msg;
13947
13948        fn update(&mut self, message: Msg) -> Command<Msg> {
13949            self.heard.borrow_mut().push(message);
13950            Command::none()
13951        }
13952
13953        fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
13954            let inner = Listener::new(
13955                Container::new()
13956                    .width(100.0)
13957                    .height(100.0)
13958                    .child(Button::new("Press").on_press(Msg::Pressed)),
13959                listen("inner"),
13960            );
13961            let outer = Listener::new(
13962                Container::new().width(300.0).height(300.0).child(inner),
13963                listen("outer"),
13964            );
13965            Box::new(
13966                SingleChildScrollView::new().width(W).height(H).child(
13967                    Flex::column()
13968                        .child(Container::new().width(300.0).height(40.0))
13969                        .child(outer)
13970                        .child(Container::new().width(300.0).height(1000.0)),
13971                ),
13972            )
13973        }
13974    }
13975
13976    fn driver() -> (Driver<Page>, Rc<RefCell<Vec<Msg>>>) {
13977        let heard = Rc::new(RefCell::new(Vec::new()));
13978        let mut d = Driver::new(
13979            Page {
13980                heard: heard.clone(),
13981            },
13982            W,
13983            H,
13984        );
13985        d.run(0.2);
13986        (d, heard)
13987    }
13988
13989    fn take(heard: &Rc<RefCell<Vec<Msg>>>) -> Vec<Msg> {
13990        std::mem::take(&mut *heard.borrow_mut())
13991    }
13992
13993    /// The events `name` heard, in order.
13994    fn of(heard: &[Msg], name: &str) -> Vec<(Raw, Point)> {
13995        heard
13996            .iter()
13997            .filter_map(|m| match m {
13998                Msg::Heard(n, kind, at) if *n == name => Some((*kind, *at)),
13999                _ => None,
14000            })
14001            .collect()
14002    }
14003
14004    fn scroll_y(d: &Driver<Page>) -> f32 {
14005        d.offsets().first().map_or(0.0, |(_, offset)| offset.1)
14006    }
14007
14008    /// **A press is heard by every listener under it, the innermost first**, each in its own
14009    /// coordinates.
14010    #[test]
14011    fn a_press_is_heard_innermost_first() {
14012        let (mut d, heard) = driver();
14013        d.press(Point::new(250.0, 90.0));
14014        d.run(0.02);
14015        let h = take(&heard);
14016        assert_eq!(of(&h, "outer"), vec![(Raw::Down, Point::new(250.0, 50.0))]);
14017        assert!(of(&h, "inner").is_empty(), "outside the inner one: {h:?}");
14018        d.release(Point::new(250.0, 90.0));
14019        d.run(0.02);
14020        take(&heard);
14021
14022        d.press(Point::new(60.0, 80.0));
14023        d.run(0.02);
14024        let h = take(&heard);
14025        let order: Vec<_> = h
14026            .iter()
14027            .filter_map(|m| match m {
14028                Msg::Heard(n, Raw::Down, _) => Some(*n),
14029                _ => None,
14030            })
14031            .collect();
14032        assert_eq!(order, vec!["inner", "outer"], "{h:?}");
14033        assert_eq!(of(&h, "inner"), vec![(Raw::Down, Point::new(60.0, 40.0))]);
14034        d.release(Point::new(60.0, 80.0));
14035        d.run(0.02);
14036    }
14037
14038    /// **The pointer that went down inside is followed wherever it goes**, out of the box,
14039    /// until it lifts — and the listener does not keep the page from scrolling under it.
14040    #[test]
14041    fn the_pointer_is_followed_out_and_the_page_still_scrolls() {
14042        let (mut d, heard) = driver();
14043        let from = Point::new(250.0, 300.0);
14044        d.press(from);
14045        d.run(0.02);
14046        for step in 1..=8 {
14047            d.move_to(Point::new(250.0, 300.0 - 35.0 * step as f32));
14048            d.run(0.02);
14049        }
14050        d.release(Point::new(250.0, 20.0));
14051        d.run(0.02);
14052        let h = take(&heard);
14053        let outer = of(&h, "outer");
14054        assert_eq!(outer.first().map(|e| e.0), Some(Raw::Down), "{outer:?}");
14055        assert_eq!(outer.last().map(|e| e.0), Some(Raw::Up), "{outer:?}");
14056        assert!(
14057            outer.iter().filter(|e| e.0 == Raw::Move).count() >= 7,
14058            "{outer:?}"
14059        );
14060        assert!(scroll_y(&d) > 50.0, "the page scrolled: {}", scroll_y(&d));
14061    }
14062
14063    /// **A pointer that leaves the box is still heard**, outside it, until it lifts — a mouse,
14064    /// which scrolls nothing, carried right out of both listeners.
14065    #[test]
14066    fn a_pointer_that_leaves_is_still_heard() {
14067        let (mut d, heard) = driver();
14068        d.press_mouse(Point::new(60.0, 80.0));
14069        d.run(0.02);
14070        d.move_mouse(Point::new(390.0, 390.0));
14071        d.run(0.02);
14072        d.release_mouse(Point::new(390.0, 390.0));
14073        d.run(0.02);
14074        let h = take(&heard);
14075        let inner = of(&h, "inner");
14076        assert_eq!(
14077            inner,
14078            vec![
14079                (Raw::Down, Point::new(60.0, 40.0)),
14080                (Raw::Move, Point::new(390.0, 350.0)),
14081                (Raw::Up, Point::new(390.0, 350.0)),
14082            ],
14083            "{h:?}"
14084        );
14085        assert_eq!(of(&h, "outer").len(), 3, "{h:?}");
14086    }
14087
14088    /// **A button inside keeps its tap**, and the listeners hear the press and the release.
14089    #[test]
14090    fn a_button_inside_keeps_its_tap() {
14091        let (mut d, heard) = driver();
14092        let at = Point::new(30.0, 55.0);
14093        d.press(at);
14094        d.release(at);
14095        d.run(0.05);
14096        let h = take(&heard);
14097        assert!(h.contains(&Msg::Pressed), "{h:?}");
14098        let inner = of(&h, "inner");
14099        assert_eq!(
14100            inner.iter().map(|e| e.0).collect::<Vec<_>>(),
14101            vec![Raw::Down, Raw::Up]
14102        );
14103    }
14104
14105    /// **A mouse with nothing held is heard as a hover** by the listeners under it, and by no
14106    /// other.
14107    #[test]
14108    fn a_mouse_hovering_is_heard() {
14109        let (mut d, heard) = driver();
14110        d.move_mouse(Point::new(250.0, 90.0));
14111        d.run(0.02);
14112        let h = take(&heard);
14113        assert_eq!(of(&h, "outer"), vec![(Raw::Hover, Point::new(250.0, 50.0))]);
14114        assert!(of(&h, "inner").is_empty());
14115        d.move_mouse(Point::new(390.0, 390.0));
14116        d.run(0.02);
14117        assert!(take(&heard).is_empty(), "outside both");
14118    }
14119}
14120
14121#[cfg(test)]
14122mod menu_keyboard_tests {
14123    use super::testing::Driver;
14124    use crate::{Application, Command};
14125    use frus_widgets::{
14126        KeyStroke, MenuBar, MenuItem, MenuPath, Point, ShortcutKey, SubmenuButton, Theme, Widget,
14127    };
14128
14129    const W: f32 = 600.0;
14130    const H: f32 = 400.0;
14131
14132    #[derive(Clone, Debug, PartialEq)]
14133    enum Msg {
14134        Menu(MenuPath),
14135        Picked(&'static str),
14136    }
14137
14138    /// File: New, Open recent ▸ (notes.txt, todo.md), Quit.
14139    #[derive(Default)]
14140    struct Editor {
14141        menu: MenuPath,
14142        /// The last row picked.
14143        picked: Option<&'static str>,
14144    }
14145
14146    impl Application for Editor {
14147        type Message = Msg;
14148
14149        fn update(&mut self, message: Msg) -> Command<Msg> {
14150            match message {
14151                Msg::Menu(path) => self.menu = path,
14152                Msg::Picked(what) => {
14153                    self.picked = Some(what);
14154                    self.menu = MenuPath::closed();
14155                }
14156            }
14157            Command::none()
14158        }
14159
14160        fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
14161            Box::new(
14162                MenuBar::new(&self.menu, Msg::Menu).menu(
14163                    SubmenuButton::new("File")
14164                        .item(MenuItem::new("New", Msg::Picked("new")))
14165                        .submenu(
14166                            SubmenuButton::new("Open recent")
14167                                .item(MenuItem::new("notes.txt", Msg::Picked("notes")))
14168                                .item(MenuItem::new("todo.md", Msg::Picked("todo"))),
14169                        )
14170                        .item(MenuItem::new("Quit", Msg::Picked("quit"))),
14171                ),
14172            )
14173        }
14174    }
14175
14176    /// The identity of the row whose words are `label`, from what the last frame drew.
14177    fn row(d: &Driver<Editor>, label: &str) -> Option<frus_widgets::WidgetId> {
14178        let (_, rect) = d.texts().into_iter().find(|(text, _)| text == label)?;
14179        d.hit(Point::new(rect.x + 4.0, rect.y + 4.0))
14180    }
14181
14182    /// **A menu that opens gives its first row the focus**, a submenu that opens gives its
14183    /// own first row the focus, and Escape closes one level and hands the focus back.
14184    #[test]
14185    fn opening_a_menu_focuses_its_first_row_and_escape_goes_back_one_level() {
14186        let mut d = Driver::new(Editor::default(), W, H);
14187        d.run(0.1);
14188        assert_eq!(d.focused(), None);
14189
14190        d.update(Msg::Menu(MenuPath::from_indices(vec![0])));
14191        d.run(0.1);
14192        let new = row(&d, "New").expect("the menu is open");
14193        assert_eq!(d.focused(), Some(new), "the first row has the focus");
14194
14195        d.update(Msg::Menu(MenuPath::from_indices(vec![0, 1])));
14196        d.run(0.1);
14197        let notes = row(&d, "notes.txt").expect("the submenu is open");
14198        assert_eq!(d.focused(), Some(notes), "the submenu's first row has it");
14199
14200        d.escape();
14201        d.run(0.1);
14202        assert_eq!(
14203            d.app().menu,
14204            MenuPath::from_indices(vec![0]),
14205            "one level closed"
14206        );
14207        assert!(row(&d, "notes.txt").is_none());
14208        assert_eq!(d.focused(), Some(new), "and the focus is back in the menu");
14209
14210        d.escape();
14211        d.run(0.1);
14212        assert!(!d.app().menu.is_open(), "then the menu");
14213    }
14214
14215    /// **The whole menu from the keyboard**, through the shell's own handling of each key
14216    /// (milestone 608): down to the row that opens a submenu, right into it, down to its
14217    /// second row, left back out, up to the first row and Enter on it.
14218    #[test]
14219    fn the_menu_is_worked_from_the_keyboard() {
14220        let key = |d: &mut Driver<Editor>, k: ShortcutKey| {
14221            d.key(KeyStroke::new(k));
14222            d.run(0.1);
14223        };
14224        let mut d = Driver::new(Editor::default(), W, H);
14225        d.run(0.1);
14226        d.update(Msg::Menu(MenuPath::from_indices(vec![0])));
14227        d.run(0.1);
14228        let (new, recent) = (row(&d, "New").unwrap(), row(&d, "Open recent").unwrap());
14229        assert_eq!(d.focused(), Some(new));
14230
14231        key(&mut d, ShortcutKey::Down);
14232        assert_eq!(d.focused(), Some(recent), "down to the next row");
14233        key(&mut d, ShortcutKey::Right);
14234        assert_eq!(
14235            d.app().menu,
14236            MenuPath::from_indices(vec![0, 1]),
14237            "right opens it"
14238        );
14239        assert_eq!(
14240            d.focused(),
14241            row(&d, "notes.txt"),
14242            "and its first row has the focus"
14243        );
14244        key(&mut d, ShortcutKey::Down);
14245        assert_eq!(d.focused(), row(&d, "todo.md"));
14246        key(&mut d, ShortcutKey::Left);
14247        assert_eq!(
14248            d.app().menu,
14249            MenuPath::from_indices(vec![0]),
14250            "left closes it"
14251        );
14252        assert_eq!(
14253            d.focused(),
14254            Some(recent),
14255            "and the focus is back on its row"
14256        );
14257        key(&mut d, ShortcutKey::Up);
14258        assert_eq!(d.focused(), Some(new));
14259        key(&mut d, ShortcutKey::Enter);
14260        assert_eq!(d.app().picked, Some("new"), "Enter picks the row");
14261        assert!(!d.app().menu.is_open(), "and the menus close");
14262    }
14263
14264    /// **A key with modifiers held** reaches a shortcut bound to it, and the modifiers are
14265    /// let go after it: a bare key that follows is not taken for the shortcut.
14266    #[test]
14267    fn a_stroke_holds_its_modifiers_for_the_key_alone() {
14268        let mut d = Driver::new(Editor::default(), W, H);
14269        d.run(0.1);
14270        d.key(KeyStroke::new(ShortcutKey::Char('a')).ctrl());
14271        assert!(!d.shell_modifiers().0, "ctrl let go after the stroke");
14272    }
14273
14274    /// **The focus goes back to the word on the bar** when its menu closes: Tab onto File,
14275    /// Enter opens it and the focus goes into it, Escape closes it and the focus is on File
14276    /// again. A word whose wrappers changed as its menu opened was a different widget once
14277    /// it closed, and the focus had nowhere to go back to (milestone 608).
14278    #[test]
14279    fn closing_a_menu_hands_the_focus_back_to_its_word() {
14280        let key = |d: &mut Driver<Editor>, k: ShortcutKey| {
14281            d.key(KeyStroke::new(k));
14282            d.run(0.1);
14283        };
14284        let mut d = Driver::new(Editor::default(), W, H);
14285        d.run(0.1);
14286        key(&mut d, ShortcutKey::Tab);
14287        let file = d.focused().expect("Tab reached the bar");
14288        key(&mut d, ShortcutKey::Enter);
14289        assert!(d.app().menu.is_open(), "Enter opened the menu");
14290        assert_eq!(d.focused(), row(&d, "New"), "and the focus went into it");
14291        key(&mut d, ShortcutKey::Escape);
14292        assert!(!d.app().menu.is_open(), "Escape closed it");
14293        assert_eq!(d.focused(), Some(file), "and the focus is back on its word");
14294    }
14295
14296    /// **A row that is not the first gets the focus back too.** With the focus on Quit, the
14297    /// pointer opens the submenu beside Open recent, which takes the focus; Escape closes it,
14298    /// and the focus is on Quit again. The other rows of a menu gained a hover region when a
14299    /// submenu opened, which moved them, and the focus fell to the first row instead, the
14300    /// one that takes it when nothing else does (milestone 608).
14301    #[test]
14302    fn a_submenu_closing_hands_the_focus_back_to_any_row() {
14303        let key = |d: &mut Driver<Editor>, k: ShortcutKey| {
14304            d.key(KeyStroke::new(k));
14305            d.run(0.1);
14306        };
14307        let mut d = Driver::new(Editor::default(), W, H);
14308        d.run(0.1);
14309        d.update(Msg::Menu(MenuPath::from_indices(vec![0])));
14310        d.run(0.1);
14311        key(&mut d, ShortcutKey::Down);
14312        key(&mut d, ShortcutKey::Down);
14313        let quit = row(&d, "Quit");
14314        assert_eq!(d.focused(), quit, "down twice reaches Quit");
14315        // What the pointer moving onto Open recent sends.
14316        d.update(Msg::Menu(MenuPath::from_indices(vec![0, 1])));
14317        d.run(0.1);
14318        assert_eq!(d.focused(), row(&d, "notes.txt"));
14319        key(&mut d, ShortcutKey::Escape);
14320        assert_eq!(d.app().menu, MenuPath::from_indices(vec![0]));
14321        assert_eq!(d.focused(), quit, "the focus is back on Quit");
14322    }
14323}
14324
14325/// **A right-click on a field opens its selection bar**, the desktop's context menu for the
14326/// four things the bar holds (milestone 568), driven through the shell's own path: the
14327/// milestone had built it and not run it.
14328#[cfg(test)]
14329mod right_click_bar_tests {
14330    use super::testing::Driver;
14331    use crate::{Application, Command};
14332    use frus_widgets::{Point, TextField, Theme, Widget};
14333
14334    struct Form;
14335
14336    impl Application for Form {
14337        type Message = ();
14338
14339        fn update(&mut self, _message: ()) -> Command<()> {
14340            Command::none()
14341        }
14342
14343        fn view(&self, _theme: &Theme) -> Box<dyn Widget<()>> {
14344            Box::new(TextField::new("some words in a field"))
14345        }
14346    }
14347
14348    #[test]
14349    fn a_right_click_on_a_field_opens_its_bar() {
14350        let mut d = Driver::new(Form, 500.0, 300.0);
14351        d.run(0.1);
14352        let shown = |d: &Driver<Form>, label: &str| d.texts().iter().any(|(t, _)| t == label);
14353        assert!(!shown(&d, "Select all"), "no bar before the click");
14354        let (_, words) = d
14355            .texts()
14356            .into_iter()
14357            .find(|(t, _)| t.contains("some words"))
14358            .expect("the field's words");
14359        d.secondary_click(Point::new(words.x + 20.0, words.y + words.height * 0.5));
14360        d.run(0.1);
14361        // Only what applies: nothing is selected, so nothing to cut or copy; and the
14362        // driver's clipboard is its own and empty, so nothing to paste — it no longer
14363        // follows the machine's (a CI runner's had text on it).
14364        assert!(shown(&d, "Select all"), "the bar is open: {:?}", d.texts());
14365        assert!(!shown(&d, "Copy") && !shown(&d, "Cut"));
14366        assert!(
14367            !shown(&d, "Paste"),
14368            "an empty clipboard offers nothing to paste"
14369        );
14370    }
14371
14372    /// The same field, under a theme that follows Linux.
14373    struct DesktopForm;
14374
14375    impl Application for DesktopForm {
14376        type Message = ();
14377
14378        fn update(&mut self, _message: ()) -> Command<()> {
14379            Command::none()
14380        }
14381
14382        fn view(&self, _theme: &Theme) -> Box<dyn Widget<()>> {
14383            Box::new(TextField::new("some words in a field"))
14384        }
14385
14386        fn theme(&self) -> Theme {
14387            Theme::default().with_platform(frus_widgets::TargetPlatform::Linux)
14388        }
14389    }
14390
14391    /// **A right-click opens the menu where the pointer is** (milestone 618), as the
14392    /// reference's context menu goes to the last secondary tap: its first row's words sit
14393    /// 20 px after the pointer, on the pointer's line.
14394    #[test]
14395    fn a_right_click_opens_the_menu_at_the_pointer() {
14396        let mut d = Driver::new(DesktopForm, 500.0, 300.0);
14397        d.run(0.1);
14398        let (_, words) = d
14399            .texts()
14400            .into_iter()
14401            .find(|(t, _)| t.contains("some words"))
14402            .expect("the field's words");
14403        let click = Point::new(words.x + 20.0, words.y + words.height * 0.5);
14404        d.secondary_click(click);
14405        d.run(0.1);
14406        let (_, row) = d
14407            .texts()
14408            .into_iter()
14409            .find(|(t, _)| t == "Select all")
14410            .expect("the menu is open");
14411        assert_eq!(row.x, click.x + 20.0, "the menu starts at the pointer");
14412        assert!(
14413            row.y >= click.y && row.y < click.y + 36.0,
14414            "its first row is on the pointer's line: {row:?} {click:?}"
14415        );
14416    }
14417
14418    /// **On a desktop, the menu's Select all puts the menu away** (milestone 625), as the
14419    /// reference's field does on macOS, Linux and Windows. A test runs on one of the three,
14420    /// whose system — not the theme — decides. The selection stays: the menu opened again
14421    /// over it offers Copy.
14422    #[test]
14423    fn a_desktop_s_select_all_puts_the_menu_away() {
14424        assert!(frus_widgets::default_target_platform().is_desktop());
14425        let mut d = Driver::new(DesktopForm, 500.0, 300.0);
14426        d.run(0.1);
14427        let shown = |d: &Driver<DesktopForm>, label: &str| {
14428            d.texts()
14429                .into_iter()
14430                .find(|(t, _)| t == label)
14431                .map(|(_, r)| r)
14432        };
14433        let words = shown(&d, "some words in a field").expect("the field's words");
14434        let click = Point::new(words.x + 20.0, words.y + words.height * 0.5);
14435        d.secondary_click(click);
14436        d.run(0.1);
14437        let row = shown(&d, "Select all").expect("the menu is open");
14438        let row = Point::new(row.x + 10.0, row.y + row.height * 0.5);
14439        d.press(row);
14440        d.release(row);
14441        d.run(0.1);
14442        assert!(shown(&d, "Select all").is_none(), "the menu is gone");
14443        assert!(shown(&d, "Copy").is_none(), "all of it: {:?}", d.texts());
14444        d.secondary_click(click);
14445        d.run(0.1);
14446        assert!(shown(&d, "Copy").is_some(), "the text is still selected");
14447    }
14448}
14449
14450#[cfg(test)]
14451mod scroll_behavior_tests {
14452    use super::testing::Driver;
14453    use crate::{Application, Command};
14454    use frus_widgets::{
14455        Container, Point, ScrollBehavior, ScrollConfiguration, SingleChildScrollView,
14456        TargetPlatform, Theme, VelocityStrategy, Widget,
14457    };
14458
14459    /// A tall page in a scroll view that follows iOS, in an application whose theme says
14460    /// Android.
14461    struct Page;
14462
14463    impl Application for Page {
14464        type Message = ();
14465
14466        fn update(&mut self, _message: ()) -> Command<()> {
14467            Command::none()
14468        }
14469
14470        fn view(&self, _theme: &Theme) -> Box<dyn Widget<()>> {
14471            Box::new(ScrollConfiguration::new(
14472                ScrollBehavior::material().with_platform(TargetPlatform::Ios),
14473                SingleChildScrollView::new()
14474                    .width(300.0)
14475                    .height(400.0)
14476                    .child(Container::new().width(300.0).height(2000.0)),
14477            ))
14478        }
14479
14480        fn theme(&self) -> Theme {
14481            Theme::default().with_platform(TargetPlatform::Android)
14482        }
14483    }
14484
14485    /// **A drag on a scroll area reads its fling the way the area's behaviour does**
14486    /// (milestone 616), not the application's: the reference's scrollable builds its drag
14487    /// recognizer from its own configuration.
14488    #[test]
14489    fn a_drag_on_an_area_reads_its_fling_the_area_s_way() {
14490        let mut d = Driver::new(Page, 300.0, 400.0);
14491        d.run(0.2);
14492        d.press(Point::new(150.0, 200.0));
14493        assert_eq!(
14494            d.fling_strategy(),
14495            VelocityStrategy::RecentAverage(frus_widgets::BOUNCING_FLING_WEIGHTS)
14496        );
14497        d.release(Point::new(150.0, 200.0));
14498    }
14499}
14500
14501/// A switch's thumb dragged through the shell (milestone 638): it follows the finger, flips
14502/// when let go past half way and goes back when not, and a tap still flips it.
14503#[cfg(test)]
14504mod switch_drag_tests {
14505    use super::testing::Driver;
14506    use crate::{Application, Command};
14507    use frus_widgets::{Container, Point, Rect, Switch, Theme, Widget};
14508
14509    const W: f32 = 300.0;
14510    const H: f32 = 200.0;
14511
14512    #[derive(Clone, Debug, PartialEq)]
14513    enum Msg {
14514        Set(bool),
14515    }
14516
14517    #[derive(Default)]
14518    struct Page {
14519        on: bool,
14520        heard: Vec<Msg>,
14521    }
14522
14523    impl Application for Page {
14524        type Message = Msg;
14525
14526        fn update(&mut self, message: Msg) -> Command<Msg> {
14527            let Msg::Set(on) = message;
14528            self.on = on;
14529            self.heard.push(message);
14530            Command::none()
14531        }
14532
14533        fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
14534            Box::new(
14535                Container::new()
14536                    .width(W)
14537                    .height(H)
14538                    .child(Switch::new(self.on).on_toggle(Msg::Set)),
14539            )
14540        }
14541    }
14542
14543    fn driver() -> Driver<Page> {
14544        let mut d = Driver::new(Page::default(), W, H);
14545        d.run(0.2);
14546        d
14547    }
14548
14549    /// The track and the thumb the last frame drew: the 52 × 32 rectangle, and the last one.
14550    fn track_and_thumb(d: &Driver<Page>) -> (Rect, Rect) {
14551        let (ui, _) = d.frame_parts().expect("a frame");
14552        let rects: Vec<Rect> = ui
14553            .scene()
14554            .primitives()
14555            .iter()
14556            .filter_map(|p| match p {
14557                frus_widgets::Primitive::Rect { rect, .. } => Some(*rect),
14558                _ => None,
14559            })
14560            .collect();
14561        let track = *rects
14562            .iter()
14563            .find(|r| r.width == 52.0 && r.height == 32.0)
14564            .expect("the track");
14565        (track, *rects.last().expect("the thumb"))
14566    }
14567
14568    /// How far along its travel the thumb's centre is, 0 at the start and 1 at the end.
14569    fn along(d: &Driver<Page>) -> f32 {
14570        let (track, thumb) = track_and_thumb(d);
14571        (thumb.x + thumb.width * 0.5 - track.x - 16.0) / 20.0
14572    }
14573
14574    /// A finger on the thumb, moved by `dx` in eight steps; lifted when `lift`.
14575    fn drag(d: &mut Driver<Page>, dx: f32, lift: bool) {
14576        let (track, _) = track_and_thumb(d);
14577        let from = Point::new(track.x + 16.0, track.y + 16.0);
14578        d.press(from);
14579        d.run(0.02);
14580        for step in 1..=8 {
14581            d.move_to(Point::new(from.x + dx * step as f32 / 8.0, from.y));
14582            d.run(0.02);
14583        }
14584        if lift {
14585            d.release(Point::new(from.x + dx, from.y));
14586            d.run(0.02);
14587        }
14588    }
14589
14590    /// **The thumb is where the finger is** while it is down, past the slop: placed, not
14591    /// animated, and the switch has said nothing yet.
14592    #[test]
14593    fn the_thumb_follows_the_finger() {
14594        let mut d = driver();
14595        assert!(along(&d).abs() < 1e-3, "off at rest");
14596        drag(&mut d, 40.0, false);
14597        let now = along(&d);
14598        assert!(now > 0.3 && now <= 1.0, "moved with the finger: {now}");
14599        assert!(d.app().heard.is_empty(), "nothing said while held");
14600        // Held, it stays: frames pass and the thumb does not go back.
14601        d.run(0.5);
14602        assert!((along(&d) - now).abs() < 1e-3, "held where it was put");
14603    }
14604
14605    /// **Let go past half way, it flips** — and the release is not also a tap, which would
14606    /// flip it straight back.
14607    #[test]
14608    fn let_go_past_half_way_it_flips() {
14609        let mut d = driver();
14610        drag(&mut d, 60.0, true);
14611        assert_eq!(d.app().heard, vec![Msg::Set(true)]);
14612        d.run(0.5);
14613        assert!((along(&d) - 1.0).abs() < 1e-3, "settled on");
14614    }
14615
14616    /// **Short of half way, it goes back**, and says nothing.
14617    #[test]
14618    fn let_go_short_of_half_way_it_goes_back() {
14619        let mut d = driver();
14620        // Out past the slop, then most of the way back: a finger that thought better of it.
14621        drag(&mut d, 30.0, false);
14622        let (track, _) = track_and_thumb(&d);
14623        let back = Point::new(track.x + 16.0 + 15.0, track.y + 16.0);
14624        d.move_to(back);
14625        d.run(0.02);
14626        assert!(along(&d) > 0.0, "it had moved");
14627        assert!(along(&d) < 0.5, "but is short of half way: {}", along(&d));
14628        d.release(back);
14629        d.run(0.5);
14630        assert!(d.app().heard.is_empty(), "nothing said");
14631        assert!(along(&d).abs() < 1e-3, "back off: {}", along(&d));
14632    }
14633
14634    /// **A tap still flips it**: a drag begins only past the slop.
14635    #[test]
14636    fn a_tap_still_flips_it() {
14637        let mut d = driver();
14638        let (track, _) = track_and_thumb(&d);
14639        let at = Point::new(track.x + 16.0, track.y + 16.0);
14640        d.press(at);
14641        d.release(at);
14642        d.run(0.5);
14643        assert_eq!(d.app().heard, vec![Msg::Set(true)]);
14644        assert!((along(&d) - 1.0).abs() < 1e-3);
14645    }
14646}
14647
14648/// A range slider dragged through the shell (milestone 651): the press picks the nearer
14649/// thumb and the drag carries it; a press where the thumbs meet waits for the movement; the
14650/// thumb last taken is painted on top.
14651#[cfg(test)]
14652mod range_slider_drag_tests {
14653    use super::testing::Driver;
14654    use crate::{Application, Command};
14655    use frus_widgets::{Container, Point, RangeSlider, Rect, Theme, Widget};
14656
14657    #[derive(Clone, Debug, PartialEq)]
14658    enum Msg {
14659        Range(f32, f32),
14660        Start(f32, f32),
14661        End(f32, f32),
14662    }
14663
14664    struct Page {
14665        values: (f32, f32),
14666        heard: Vec<Msg>,
14667    }
14668
14669    impl Application for Page {
14670        type Message = Msg;
14671
14672        fn update(&mut self, message: Msg) -> Command<Msg> {
14673            if let Msg::Range(s, e) | Msg::End(s, e) = message {
14674                self.values = (s, e);
14675            }
14676            self.heard.push(message);
14677            Command::none()
14678        }
14679
14680        fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
14681            let (start, end) = self.values;
14682            Box::new(
14683                Container::new().width(300.0).height(200.0).child(
14684                    RangeSlider::new(start, end)
14685                        .width(248.0)
14686                        .divisions(20)
14687                        .on_change(Msg::Range)
14688                        .on_change_start(Msg::Start)
14689                        .on_change_end(Msg::End),
14690                ),
14691            )
14692        }
14693    }
14694
14695    fn driver(start: f32, end: f32) -> Driver<Page> {
14696        let page = Page {
14697            values: (start, end),
14698            heard: Vec::new(),
14699        };
14700        let mut d = Driver::new(page, 300.0, 200.0);
14701        d.run(0.2);
14702        d
14703    }
14704
14705    /// The thumbs the last frame drew, in the order they were painted.
14706    fn thumbs(d: &Driver<Page>) -> Vec<Rect> {
14707        let (ui, _) = d.frame_parts().expect("a frame");
14708        ui.scene()
14709            .primitives()
14710            .iter()
14711            .filter_map(|p| match p {
14712                frus_widgets::Primitive::Rect { rect, blur, .. }
14713                    if *blur == 0.0 && rect.width == 20.0 && rect.height == 20.0 =>
14714                {
14715                    Some(*rect)
14716                }
14717                _ => None,
14718            })
14719            .collect()
14720    }
14721
14722    fn centre(r: Rect) -> Point {
14723        Point::new(r.x + r.width * 0.5, r.y + r.height * 0.5)
14724    }
14725
14726    /// A finger pressed at `from`, moved by `dx` in four steps, and lifted.
14727    fn drag(d: &mut Driver<Page>, from: Point, dx: f32) {
14728        d.press(from);
14729        d.run(0.02);
14730        for step in 1..=4 {
14731            d.move_to(Point::new(from.x + dx * step as f32 / 4.0, from.y));
14732            d.run(0.02);
14733        }
14734        d.release(Point::new(from.x + dx, from.y));
14735        d.run(0.02);
14736    }
14737
14738    fn close(a: (f32, f32), b: (f32, f32)) -> bool {
14739        (a.0 - b.0).abs() < 1e-3 && (a.1 - b.1).abs() < 1e-3
14740    }
14741
14742    /// **The press takes the nearer thumb and the drag carries it**: the bracket opens on
14743    /// the interval as it was and closes on the one the drag settled on; the other thumb
14744    /// stays.
14745    #[test]
14746    fn a_drag_carries_the_nearer_thumb() {
14747        let mut d = driver(0.25, 0.75);
14748        let t = thumbs(&d);
14749        assert_eq!(t.len(), 2);
14750        let start = centre(t[0]).x.min(centre(t[1]).x);
14751        // Just right of the start thumb, nearer it than the end: it is taken, and carried
14752        // to 0.445 of the 200-pixel track, which snaps to 0.45.
14753        drag(&mut d, Point::new(start + 4.0, centre(t[0]).y), 34.0);
14754        let heard = &d.app().heard;
14755        assert_eq!(heard.first(), Some(&Msg::Start(0.25, 0.75)), "{heard:?}");
14756        let Some(Msg::End(s, e)) = heard.last().cloned() else {
14757            panic!("no end: {heard:?}");
14758        };
14759        assert!(close((s, e), (0.45, 0.75)), "({s}, {e})");
14760        assert!(close(d.app().values, (0.45, 0.75)));
14761        // The start, taken last, is now the one on top: painted last.
14762        let order = thumbs(&d);
14763        assert!(order[1].x < order[0].x, "the start on top: {order:?}");
14764    }
14765
14766    /// **Where the thumbs meet, the movement decides**: leftwards takes the start, and a
14767    /// thumb never passes the other.
14768    #[test]
14769    fn where_the_thumbs_meet_the_movement_decides() {
14770        let mut d = driver(0.5, 0.5);
14771        let at = centre(thumbs(&d)[0]);
14772        drag(&mut d, at, -40.0);
14773        assert!(close(d.app().values, (0.3, 0.5)), "{:?}", d.app().values);
14774
14775        let mut d = driver(0.5, 0.5);
14776        drag(&mut d, at, 40.0);
14777        assert!(close(d.app().values, (0.5, 0.7)), "{:?}", d.app().values);
14778
14779        // The start dragged past the end stops at it.
14780        let mut d = driver(0.3, 0.5);
14781        let start = centre(thumbs(&d)[0]);
14782        drag(&mut d, start, 120.0);
14783        assert!(close(d.app().values, (0.5, 0.5)), "{:?}", d.app().values);
14784    }
14785}
14786
14787/// The focus moving, through the shell (milestone 660): a group told when it arrives and
14788/// leaves, and a field told of a press outside it.
14789#[cfg(test)]
14790mod focus_change_tests {
14791    use super::testing::Driver;
14792    use crate::{Application, Command};
14793    use frus_widgets::{Flex, Focus, Point, Rect, TextField, Theme, Widget};
14794
14795    #[derive(Clone, Debug, PartialEq)]
14796    enum Msg {
14797        Group(bool),
14798        Outside,
14799        Typed,
14800    }
14801
14802    #[derive(Default)]
14803    struct Page {
14804        heard: Vec<Msg>,
14805    }
14806
14807    impl Application for Page {
14808        type Message = Msg;
14809
14810        fn update(&mut self, message: Msg) -> Command<Msg> {
14811            if message != Msg::Typed {
14812                self.heard.push(message);
14813            }
14814            Command::none()
14815        }
14816
14817        fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
14818            Box::new(
14819                Flex::column()
14820                    .width(300.0)
14821                    .height(400.0)
14822                    .gap(40.0)
14823                    .child(
14824                        Focus::new(
14825                            TextField::new("a")
14826                                .on_input(|_| Msg::Typed)
14827                                .on_tap_outside(Msg::Outside),
14828                        )
14829                        .skip_traversal()
14830                        .on_focus_change(Msg::Group),
14831                    )
14832                    .child(TextField::new("b").on_input(|_| Msg::Typed)),
14833            )
14834        }
14835    }
14836
14837    /// The two fields' boxes, `a` then `b`.
14838    fn fields(d: &Driver<Page>) -> (Rect, Rect) {
14839        let (ui, _) = d.frame_parts().expect("a frame");
14840        let rects: Vec<Rect> = ui
14841            .focusable_ids()
14842            .filter_map(|id| ui.widget_rect(id))
14843            .filter(|r| r.height < 100.0)
14844            .collect();
14845        (rects[0], rects[rects.len() - 1])
14846    }
14847
14848    fn centre(r: Rect) -> Point {
14849        Point::new(r.x + r.width / 2.0, r.y + r.height / 2.0)
14850    }
14851
14852    fn tap(d: &mut Driver<Page>, at: Point) {
14853        d.press(at);
14854        d.release(at);
14855        d.run(0.05);
14856    }
14857
14858    /// **A press on the field brings the focus into the group; a press elsewhere tells the
14859    /// field it was outside, then the group that the focus left.**
14860    #[test]
14861    fn the_shell_reports_the_focus_moving() {
14862        let mut d = Driver::new(Page::default(), 300.0, 400.0);
14863        d.run(0.2);
14864        let (a, b) = fields(&d);
14865        tap(&mut d, centre(a));
14866        assert_eq!(d.app().heard, vec![Msg::Group(true)], "arrived");
14867        tap(&mut d, centre(a));
14868        assert_eq!(
14869            d.app().heard,
14870            vec![Msg::Group(true)],
14871            "a press inside says nothing"
14872        );
14873        tap(&mut d, centre(b));
14874        assert_eq!(
14875            d.app().heard,
14876            vec![Msg::Group(true), Msg::Outside, Msg::Group(false)],
14877            "outside, then left"
14878        );
14879        tap(&mut d, Point::new(150.0, 390.0));
14880        assert_eq!(
14881            d.app().heard.len(),
14882            3,
14883            "the other field holds no `on_tap_outside`: {:?}",
14884            d.app().heard
14885        );
14886    }
14887}
14888
14889#[cfg(test)]
14890mod file_drop_tests {
14891    use std::path::PathBuf;
14892
14893    use super::testing::Driver;
14894    use crate::{Application, Command};
14895    use frus_widgets::{Container, DragTarget, Flex, Point, Primitive, Theme, Widget};
14896
14897    const W: f32 = 300.0;
14898    const H: f32 = 300.0;
14899
14900    #[derive(Clone, Debug, PartialEq)]
14901    enum Msg {
14902        Files(&'static str, Vec<PathBuf>),
14903    }
14904
14905    #[derive(Default)]
14906    struct Page {
14907        heard: Vec<Msg>,
14908    }
14909
14910    impl Application for Page {
14911        type Message = Msg;
14912
14913        fn update(&mut self, message: Msg) -> Command<Msg> {
14914            self.heard.push(message);
14915            Command::none()
14916        }
14917
14918        /// Two targets for files, 100 px tall each, and 100 px of nothing under them; the
14919        /// second takes text files only.
14920        fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
14921            Box::new(
14922                Flex::column()
14923                    .width(W)
14924                    .height(H)
14925                    .child(
14926                        DragTarget::new(Container::new().width(W).height(100.0))
14927                            .on_files(|paths| Msg::Files("top", paths)),
14928                    )
14929                    .child(
14930                        DragTarget::new(Container::new().width(W).height(100.0))
14931                            .on_files(|paths| Msg::Files("text", paths))
14932                            .accepts_files(|paths| {
14933                                paths
14934                                    .iter()
14935                                    .all(|p| p.extension().is_some_and(|e| e == "txt"))
14936                            }),
14937                    ),
14938            )
14939        }
14940    }
14941
14942    fn driver() -> Driver<Page> {
14943        let mut d = Driver::new(Page::default(), W, H);
14944        d.run(0.2);
14945        d
14946    }
14947
14948    fn paths(names: &[&str]) -> Vec<PathBuf> {
14949        names.iter().map(PathBuf::from).collect()
14950    }
14951
14952    /// The outlines the last frame drew: the "drop it here" highlight is the one with a
14953    /// border, everything else here being plain boxes.
14954    fn highlights(d: &Driver<Page>) -> Vec<frus_widgets::Rect> {
14955        let (ui, _) = d.frame_parts().expect("a frame");
14956        ui.scene()
14957            .primitives()
14958            .iter()
14959            .filter_map(|p| match p {
14960                Primitive::Rect {
14961                    rect, border_width, ..
14962                } if *border_width > 0.0 => Some(*rect),
14963                _ => None,
14964            })
14965            .collect()
14966    }
14967
14968    #[test]
14969    fn files_held_over_a_target_light_it_up_and_land_on_it() {
14970        let mut d = driver();
14971        assert!(highlights(&d).is_empty());
14972        let files = paths(&["notes.txt", "photo.png"]);
14973        d.hover_files(&files, Some(Point::new(50.0, 50.0)));
14974        assert!(d.drag_over().is_some());
14975        d.run(0.05);
14976        let lit = highlights(&d);
14977        assert_eq!(lit.len(), 1, "{lit:?}");
14978        assert!(
14979            lit[0].y < 1.0 && (lit[0].height - 100.0).abs() < 0.5,
14980            "{lit:?}"
14981        );
14982
14983        d.drop_files(&files, Some(Point::new(50.0, 50.0)));
14984        assert_eq!(d.app().heard, [Msg::Files("top", files)]);
14985        assert_eq!(d.drag_over(), None);
14986        d.run(0.05);
14987        assert!(
14988            highlights(&d).is_empty(),
14989            "the highlight went with the drop"
14990        );
14991    }
14992
14993    #[test]
14994    fn the_highlight_follows_the_files_and_a_refusing_target_stays_dark() {
14995        let mut d = driver();
14996        let text = paths(&["a.txt"]);
14997        d.hover_files(&text, Some(Point::new(50.0, 50.0)));
14998        let top = d.drag_over().expect("over the top target");
14999        d.move_files(Some(Point::new(50.0, 150.0)));
15000        let second = d.drag_over().expect("over the second target");
15001        assert_ne!(top, second);
15002        d.move_files(Some(Point::new(50.0, 250.0)));
15003        assert_eq!(d.drag_over(), None, "under both targets");
15004        d.cancel_files();
15005
15006        // A picture is not text: the second target does not light up for it, and a drop
15007        // there sends nothing.
15008        let picture = paths(&["a.png"]);
15009        d.hover_files(&picture, Some(Point::new(50.0, 150.0)));
15010        assert_eq!(d.drag_over(), None);
15011        d.drop_files(&picture, Some(Point::new(50.0, 150.0)));
15012        assert!(d.app().heard.is_empty(), "{:?}", d.app().heard);
15013    }
15014
15015    #[test]
15016    fn files_taken_away_leave_nothing_lit_and_send_nothing() {
15017        let mut d = driver();
15018        d.hover_files(&paths(&["a.txt"]), Some(Point::new(50.0, 150.0)));
15019        assert!(d.drag_over().is_some());
15020        d.cancel_files();
15021        assert_eq!(d.drag_over(), None);
15022        d.run(0.05);
15023        assert!(highlights(&d).is_empty());
15024        assert!(d.app().heard.is_empty());
15025    }
15026
15027    #[test]
15028    fn a_drop_hands_over_what_was_dropped_not_what_was_hovered() {
15029        let mut d = driver();
15030        d.hover_files(&paths(&["old.txt"]), Some(Point::new(50.0, 150.0)));
15031        let dropped = paths(&["b.txt", "a.txt"]);
15032        d.drop_files(&dropped, Some(Point::new(50.0, 150.0)));
15033        assert_eq!(d.app().heard, [Msg::Files("text", dropped)]);
15034    }
15035
15036    #[test]
15037    fn where_the_system_does_not_say_the_only_taker_takes_them() {
15038        let mut d = driver();
15039        // Only the top target takes a picture, so it is the one, wherever the pointer is.
15040        let picture = paths(&["a.png"]);
15041        d.hover_files(&picture, None);
15042        assert!(d.drag_over().is_some());
15043        d.drop_files(&picture, None);
15044        assert_eq!(d.app().heard, [Msg::Files("top", picture)]);
15045    }
15046}
15047
15048/// **What an assistive technology asks of a checkbox and a field is done** (milestone 710).
15049///
15050/// Through Windows' UI Automation, a task's checkbox stayed ticked after a Toggle and the
15051/// new-task field stayed empty after a SetValue, while an Invoke on a button worked. These
15052/// drive the shell's own path for those requests, on a checkbox and a field that say what they
15053/// are.
15054#[cfg(test)]
15055mod assistive_tech_tests {
15056    use super::testing::Driver;
15057    use crate::{Application, Command};
15058    use frus_widgets::{column, Checkbox, TextField, Theme, Widget};
15059
15060    #[derive(Clone, Debug, PartialEq)]
15061    enum Msg {
15062        Ticked(bool),
15063        Typed(String),
15064    }
15065
15066    #[derive(Default)]
15067    struct Form {
15068        done: bool,
15069        name: String,
15070    }
15071
15072    impl Application for Form {
15073        type Message = Msg;
15074
15075        fn update(&mut self, message: Msg) -> Command<Msg> {
15076            match message {
15077                Msg::Ticked(done) => self.done = done,
15078                Msg::Typed(name) => self.name = name,
15079            }
15080            Command::none()
15081        }
15082
15083        fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
15084            Box::new(column![
15085                Checkbox::new(self.done)
15086                    .label("Done")
15087                    .on_toggle(Msg::Ticked),
15088                TextField::new(self.name.clone())
15089                    .label("Name")
15090                    .on_input(Msg::Typed),
15091            ])
15092        }
15093    }
15094
15095    /// **Activate ticks a checkbox**, and again unticks it.
15096    #[test]
15097    fn activate_ticks_a_checkbox() {
15098        let mut driver = Driver::new(Form::default(), 400.0, 300.0);
15099        driver.frame(0.016);
15100        assert!(
15101            driver.assistive_click("Done"),
15102            "the checkbox is in the tree"
15103        );
15104        driver.frame(0.016);
15105        assert!(driver.app().done, "ticked");
15106        assert!(driver.assistive_click("Done"));
15107        driver.frame(0.016);
15108        assert!(!driver.app().done, "unticked");
15109    }
15110
15111    /// **Activate ticks a checkbox merged with its caption** — the shape a list row has,
15112    /// where the reader is to hear which task the box is for: the merged node is the
15113    /// wrapper's, which has no click of its own, and the press has to reach the checkbox in
15114    /// it.
15115    #[test]
15116    fn activate_ticks_a_checkbox_merged_with_its_caption() {
15117        struct Row(bool);
15118        impl Application for Row {
15119            type Message = Msg;
15120            fn update(&mut self, message: Msg) -> Command<Msg> {
15121                if let Msg::Ticked(done) = message {
15122                    self.0 = done;
15123                }
15124                Command::none()
15125            }
15126            fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
15127                Box::new(frus_widgets::Semantics::merge(frus_widgets::row![
15128                    Checkbox::new(self.0).on_toggle(Msg::Ticked),
15129                    frus_widgets::text("Water the plants"),
15130                ]))
15131            }
15132        }
15133        let mut driver = Driver::new(Row(false), 400.0, 300.0);
15134        driver.frame(0.016);
15135        assert!(
15136            driver.assistive_click("Water the plants"),
15137            "the merged node is in the tree"
15138        );
15139        driver.frame(0.016);
15140        assert!(driver.app().0, "ticked");
15141    }
15142
15143    /// **Set value fills a field**, through the message typing it would have produced.
15144    #[test]
15145    fn set_value_fills_a_field() {
15146        let mut driver = Driver::new(Form::default(), 400.0, 300.0);
15147        driver.frame(0.016);
15148        assert!(
15149            driver.assistive_set_value("Name", "Ada"),
15150            "the field is in the tree"
15151        );
15152        driver.frame(0.016);
15153        assert_eq!(driver.app().name, "Ada");
15154    }
15155}
15156
15157/// **A simulated device's keyboard comes up with a field's focus**, as a phone's does, and
15158/// goes when the focus leaves (milestone 722) — through the shell's own end-of-frame path.
15159#[cfg(test)]
15160mod simulated_keyboard_tests {
15161    use super::testing::Driver;
15162    use crate::{Application, Command};
15163    use frus_widgets::{Flex, Point, Text, TextField, Theme, Widget};
15164
15165    struct Form;
15166
15167    impl Application for Form {
15168        type Message = ();
15169
15170        fn update(&mut self, _message: ()) -> Command<()> {
15171            Command::none()
15172        }
15173
15174        fn view(&self, _theme: &Theme) -> Box<dyn Widget<()>> {
15175            Box::new(
15176                Flex::column()
15177                    .child(TextField::new("a field"))
15178                    .child(Text::new("some words")),
15179            )
15180        }
15181    }
15182
15183    #[test]
15184    fn the_keyboard_follows_a_field_s_focus() {
15185        let mut d = Driver::new(Form, 393.0, 852.0);
15186        d.simulate("iphone-15");
15187        d.run(0.1);
15188        assert!(
15189            !d.simulated_keyboard(),
15190            "no keyboard before anything is focused"
15191        );
15192
15193        let at = |d: &Driver<Form>, label: &str| {
15194            let (_, r) = d
15195                .texts()
15196                .into_iter()
15197                .find(|(t, _)| t.contains(label))
15198                .unwrap_or_else(|| panic!("{label}: {:?}", d.texts()));
15199            Point::new(r.x + 5.0, r.y + r.height * 0.5)
15200        };
15201        let field = at(&d, "a field");
15202        d.press(field);
15203        d.release(field);
15204        d.run(0.1);
15205        assert!(d.focused().is_some(), "the tap focused the field");
15206        assert!(
15207            d.simulated_keyboard(),
15208            "the keyboard came up with the field's focus"
15209        );
15210
15211        let words = at(&d, "some words");
15212        d.press(words);
15213        d.release(words);
15214        d.run(0.1);
15215        assert!(!d.simulated_keyboard(), "the keyboard went with the focus");
15216    }
15217
15218    /// **`T` steps through the reader's text sizes** and comes back to the usual one.
15219    #[test]
15220    fn the_text_size_cycles() {
15221        let mut size = 1.0;
15222        let mut seen = vec![size];
15223        for _ in 0..4 {
15224            size = super::next_text_scale(size);
15225            seen.push(size);
15226        }
15227        assert_eq!(seen, [1.0, 1.3, 2.0, 0.85, 1.0]);
15228        assert_eq!(
15229            super::next_text_scale(1.15),
15230            1.0,
15231            "an unusual size goes back to 1"
15232        );
15233    }
15234}