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