1use std::collections::HashMap;
10use std::sync::Arc;
11
12use web_time::Instant;
13
14use frus_gpu::Renderer;
15use frus_widgets::{
16 build_deferred, build_ui, collect_ids, find_by_key, find_path, find_widget,
17 nearest_reorder_slot, reflow_reorder_cards, reflow_reorder_columns, reorder_drop_after,
18 reorder_siblings, reorderable_owners, subtree_ids, Accessibility, Brightness, Color,
19 Cursor as UiCursor, Edit, EditKind, EditSnapshot, FocusDirection, Insets, Key, KeyResponse,
20 KeyStroke, MediaQuery, Point, Primitive, Rect, RegionPoint, RegionSelection, ReorderAxis,
21 Runtime, Scene, ScrollTo, Scrollable, SheetTo, ShortcutKey, Size, TextStop, Theme, ToolbarMark,
22 Ui, VelocityEstimate, VelocityTracker, Widget, WidgetId, WindowInsets,
23};
24use winit::application::ApplicationHandler;
25use winit::event::{
26 ElementState, MouseButton, MouseScrollDelta, StartCause, TouchPhase, WindowEvent,
27};
28use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoopProxy};
29use winit::keyboard::{Key as WinitKey, KeyCode, NamedKey, PhysicalKey};
30use winit::window::{CursorIcon, Window, WindowId};
31
32use crate::application::{Application, Lifecycle};
33
34#[derive(Clone, Debug, PartialEq)]
36pub(crate) struct PlatformSettings {
37 pub text_scaler: f32,
40 pub brightness: Brightness,
42 pub accessibility: Accessibility,
44 pub locales: Vec<frus_widgets::Locale>,
50}
51
52impl Default for PlatformSettings {
53 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
67enum ClipCommand {
68 Copy,
69 Cut,
70 Paste,
71}
72
73fn 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
106fn opens_selection_bar(logical: &WinitKey, shift: bool) -> bool {
111 match logical {
112 WinitKey::Named(NamedKey::ContextMenu) => true,
113 WinitKey::Named(NamedKey::F10) => shift,
114 _ => false,
115 }
116}
117
118#[cfg(any(android, test))]
131fn wants_keyboard<M>(widget: &dyn Widget<M>) -> bool {
132 is_text_field(widget) && widget.focusable() && widget.takes_typing()
133}
134
135fn is_text_field<M>(widget: &dyn Widget<M>) -> bool {
145 widget.text_value().is_some()
146}
147
148const REGION_DOUBLE_CLICK_SLOP: f32 = 8.0;
151
152const DOUBLE_TAP_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(300);
155
156const DOUBLE_TAP_SLOP: f32 = 100.0;
159
160const MAX_PAN_VELOCITY: f32 = 8000.0;
163
164struct PendingTap<M> {
167 id: WidgetId,
169 at: Instant,
171 position: Point,
173 single: Option<M>,
175}
176
177fn select_all_hides_bar(platform: frus_widgets::TargetPlatform) -> bool {
184 platform.is_desktop()
185}
186
187mod clip {
203 use frus_widgets::WidgetId;
204
205 #[derive(Clone, Debug, PartialEq, Eq)]
207 pub struct Pasted {
208 pub into: WidgetId,
209 pub text: String,
210 }
211
212 impl Pasted {
213 pub fn lands(&self, focused: Option<WidgetId>) -> bool {
219 focused == Some(self.into) && !self.text.is_empty()
220 }
221 }
222
223 #[cfg(not(web))]
225 impl Clipboard {
226 pub fn paste(
227 &mut self,
228 into: WidgetId,
229 _wake: Option<&std::sync::Arc<winit::window::Window>>,
230 ) -> Option<Pasted> {
231 self.get_text().map(|text| Pasted { into, text })
232 }
233 pub fn take_answered(&mut self) -> Option<Pasted> {
234 None
235 }
236 }
237
238 #[cfg(desktop)]
239 pub enum Clipboard {
240 System(Option<arboard::Clipboard>),
242 #[cfg(any(test, feature = "testing"))]
247 Memory(Option<String>),
248 }
249
250 #[cfg(desktop)]
251 impl Clipboard {
252 pub fn new() -> Self {
253 Self::System(arboard::Clipboard::new().ok())
254 }
255 #[cfg(any(test, feature = "testing"))]
257 pub fn in_memory() -> Self {
258 Self::Memory(None)
259 }
260 pub fn get_text(&mut self) -> Option<String> {
261 match self {
262 Self::System(system) => system.as_mut().and_then(|c| c.get_text().ok()),
263 #[cfg(any(test, feature = "testing"))]
264 Self::Memory(text) => text.clone(),
265 }
266 }
267 pub fn has_text(&mut self) -> bool {
268 self.get_text().is_some_and(|text| !text.is_empty())
269 }
270 pub fn set_text(&mut self, text: String) {
271 match self {
272 Self::System(system) => {
273 if let Some(c) = system.as_mut() {
274 let _ = c.set_text(text);
275 }
276 }
277 #[cfg(any(test, feature = "testing"))]
278 Self::Memory(held) => *held = Some(text),
279 }
280 }
281 }
282
283 #[cfg(all(not(desktop), any(test, feature = "testing")))]
285 impl Clipboard {
286 pub fn in_memory() -> Self {
287 Self::new()
288 }
289 }
290
291 #[cfg(android)]
292 pub struct Clipboard;
293
294 #[cfg(android)]
295 impl Clipboard {
296 pub fn new() -> Self {
297 Self
298 }
299 pub fn get_text(&mut self) -> Option<String> {
300 crate::android_clipboard::get_text()
301 }
302 pub fn has_text(&mut self) -> bool {
303 self.get_text().is_some_and(|text| !text.is_empty())
304 }
305 pub fn set_text(&mut self, text: String) {
306 crate::android_clipboard::set_text(&text);
307 }
308 }
309
310 #[cfg(any(web, test))]
315 #[derive(Default)]
316 pub struct Answers(std::rc::Rc<std::cell::RefCell<std::collections::VecDeque<Pasted>>>);
317
318 #[cfg(any(web, test))]
319 impl Answers {
320 pub fn answer_for(
323 &self,
324 into: WidgetId,
325 wake: impl FnOnce() + 'static,
326 ) -> impl FnOnce(String) + 'static {
327 let queue = std::rc::Rc::clone(&self.0);
328 move |text| {
329 queue.borrow_mut().push_back(Pasted { into, text });
330 wake();
331 }
332 }
333
334 pub fn take(&self) -> Option<Pasted> {
336 self.0.borrow_mut().pop_front()
337 }
338 }
339
340 #[cfg(web)]
343 pub struct Clipboard {
344 answered: Answers,
345 }
346
347 #[cfg(web)]
348 impl Clipboard {
349 pub fn new() -> Self {
350 Self {
351 answered: Answers::default(),
352 }
353 }
354 pub fn set_text(&mut self, text: String) {
355 crate::web_clipboard::set_text(&text);
356 }
357 pub fn has_text(&mut self) -> bool {
360 true
361 }
362 pub fn paste(
363 &mut self,
364 into: WidgetId,
365 wake: Option<&std::sync::Arc<winit::window::Window>>,
366 ) -> Option<Pasted> {
367 let wake = wake.cloned();
368 crate::web_clipboard::get_text(self.answered.answer_for(into, move || {
369 if let Some(window) = wake {
370 window.request_redraw();
371 }
372 }));
373 None
374 }
375 pub fn take_answered(&mut self) -> Option<Pasted> {
376 self.answered.take()
377 }
378 }
379
380 #[cfg(not(any(desktop, android, web)))]
381 pub struct Clipboard;
382
383 #[cfg(not(any(desktop, android, web)))]
384 impl Clipboard {
385 pub fn new() -> Self {
386 Self
387 }
388 pub fn get_text(&mut self) -> Option<String> {
389 None
390 }
391 pub fn has_text(&mut self) -> bool {
392 false
393 }
394 pub fn set_text(&mut self, _text: String) {}
395 }
396}
397
398#[cfg(web)]
403mod web_timer {
404 use wasm_bindgen::prelude::Closure;
405 use wasm_bindgen::JsCast;
406
407 pub(crate) struct Interval {
410 id: i32,
411 _closure: Closure<dyn FnMut()>,
413 }
414
415 impl Interval {
416 pub(crate) fn new(ms: i32, f: impl FnMut() + 'static) -> Option<Self> {
419 let window = web_sys::window()?;
420 let closure = Closure::wrap(Box::new(f) as Box<dyn FnMut()>);
421 let id = window
422 .set_interval_with_callback_and_timeout_and_arguments_0(
423 closure.as_ref().unchecked_ref(),
424 ms.max(1),
425 )
426 .ok()?;
427 Some(Self {
428 id,
429 _closure: closure,
430 })
431 }
432 }
433
434 impl Drop for Interval {
435 fn drop(&mut self) {
436 if let Some(window) = web_sys::window() {
437 window.clear_interval_with_handle(self.id);
438 }
439 }
440 }
441
442 pub(crate) fn after(ms: i32, f: impl FnOnce() + 'static) {
451 let Some(window) = web_sys::window() else {
452 return;
453 };
454 let callback = wasm_bindgen::closure::Closure::once_into_js(f);
455 let _ = window.set_timeout_with_callback_and_timeout_and_arguments_0(
456 callback.unchecked_ref(),
457 ms.max(0),
458 );
459 }
460}
461
462#[cfg(not(web))]
467type SubHandle = async_executor::Task<()>;
468#[cfg(web)]
469type SubHandle = Option<web_timer::Interval>;
470
471const SCROLL_SPEED: f32 = 40.0;
473
474const TOUCH_SLOP: f32 = 18.0;
483
484const PRECISE_SLOP: f32 = 1.0;
487
488fn fling_velocity(estimate: VelocityEstimate, slop: f32) -> (f32, f32) {
500 let gate = |velocity: f32, travelled: f32| {
501 if travelled.abs() > slop {
502 velocity
503 } else {
504 0.0
505 }
506 };
507 (
508 gate(estimate.velocity.x, estimate.offset.0),
509 gate(estimate.velocity.y, estimate.offset.1),
510 )
511}
512
513const SCROLL_OVER: f32 = 48.0;
515
516const PAN_FLING_MIN: f32 = 80.0;
518
519const BACK_EDGE: f32 = 24.0;
521
522mod drag_preview {
526 pub const SHADOW_OFFSET_Y: f32 = 4.0;
528 pub const SHADOW_BLUR: f32 = 12.0;
530 pub const SHADOW_ALPHA: f32 = 0.28;
532 pub const BORDER_ALPHA: f32 = 0.9;
534 pub const BORDER_WIDTH: f32 = 1.5;
536 pub const LIFT_Y: f32 = -2.0;
538 pub const INSERT_THICKNESS: f32 = 3.0;
540}
541
542fn frame_needs_build(dirty: bool, animating: bool, was_animating: bool, otherwise: bool) -> bool {
552 dirty || animating || was_animating || otherwise
553}
554
555fn hold_candidates<Msg: Clone>(
563 drag: Option<&Drag>,
564 ui: Option<&frus_widgets::Ui<Msg>>,
565 tree: Option<&dyn frus_widgets::Widget<Msg>>,
566 at: Point,
567) -> (Option<Msg>, Option<frus_widgets::DragSource>) {
568 if matches!(drag, Some(Drag::Back { .. })) {
573 return (None, None);
574 }
575 let Some(ui) = ui else {
576 return (None, None);
577 };
578 let free = matches!(
581 drag,
582 None | Some(Drag::Scroll { moved: false, .. }) | Some(Drag::Gesture { moved: false, .. })
583 );
584 let long_press = if free { ui.long_press_at(at) } else { None };
585 let lift = ui.drag_source_at(at).filter(|source| {
586 tree.and_then(|tree| find_widget(tree, source.id))
587 .is_some_and(|widget| widget.drag_needs_long_press())
588 });
589 (long_press, lift)
590}
591
592fn drag_after_hold(
601 drag: Option<Drag>,
602 lift: Option<frus_widgets::DragSource>,
603 reorder: Option<(WidgetId, usize, Point)>,
604 cursor: Point,
605) -> Option<Drag> {
606 if let Some(Drag::Back { start_x }) = drag {
609 return Some(Drag::Back { start_x });
610 }
611 if let Some(source) = lift {
612 Some(Drag::Item {
613 source,
614 start: cursor,
615 moved: true,
616 over: None,
617 })
618 } else if let Some((id, from, start)) = reorder {
619 Some(Drag::Reorder {
620 id,
621 from,
622 start,
623 moved: true,
624 carried: None,
625 })
626 } else {
627 None
628 }
629}
630
631enum Drag {
633 Scrollbar {
635 id: WidgetId,
636 vertical: bool,
637 grab: f32,
638 track_start: f32,
639 track_len: f32,
640 thumb_len: f32,
641 max: f32,
642 reverse: bool,
645 },
646 RegionSelect { area: WidgetId },
650 RegionHandle {
654 area: WidgetId,
655 handle: crate::selection::Handle,
656 grab: Point,
657 },
658 TextSelect {
660 id: WidgetId,
661 rect: frus_widgets::Rect,
662 },
663 SelectionHandle {
667 id: WidgetId,
668 rect: frus_widgets::Rect,
669 handle: crate::selection::Handle,
670 grab: Point,
671 },
672 Widget {
676 id: WidgetId,
677 rect: frus_widgets::Rect,
678 last_x: f32,
679 },
680 Reorder {
684 id: WidgetId,
685 from: usize,
686 start: Point,
687 moved: bool,
688 carried: Option<(WidgetId, Rect)>,
694 },
695 Pan {
701 id: WidgetId,
702 last: Point,
703 moved: bool,
704 viewport: frus_widgets::Rect,
705 },
706 Scroll {
709 id: WidgetId,
710 last: Point,
711 moved: bool,
712 carried: (f32, f32),
716 dismiss: Option<frus_widgets::Dismissable>,
721 pan: Option<(WidgetId, frus_widgets::PanAxis)>,
724 axis: Option<bool>,
734 },
735 Gesture {
739 id: WidgetId,
740 axis: frus_widgets::PanAxis,
741 start: Point,
742 last: Point,
743 moved: bool,
744 },
745 Dismiss {
749 item: frus_widgets::Dismissable,
750 last: Point,
751 moved: bool,
754 },
755 Item {
760 source: frus_widgets::DragSource,
761 start: Point,
762 moved: bool,
765 over: Option<WidgetId>,
766 },
767 Sheet {
772 id: WidgetId,
773 last: Point,
774 moved: bool,
777 available: f32,
778 },
779 Back { start_x: f32 },
782}
783
784fn install_ambient<A: Application>(app: &A, preferred: &[frus_widgets::Locale]) {
810 frus_widgets::ScrollConfiguration::set_root(app.scroll_behavior());
813 frus_widgets::locale::install(app.resolved_locale(preferred));
817 if let Some(table) = app.localizations() {
818 frus_widgets::localizations::install(table);
819 }
820}
821
822fn build_view<A: Application>(
823 app: &A,
824 theme: &Theme,
825 runtime: &frus_widgets::Runtime,
826) -> Box<dyn Widget<A::Message>> {
827 debug_assert!(
828 frus_widgets::MediaQuery::of().is_described(),
829 "a view is being built with no surface described: install one first — milestone 416"
830 );
831 runtime.states.begin_build();
834 let tree = app.view(theme);
835 build_deferred(tree.as_ref(), theme, runtime);
836 tree
837}
838
839struct Mailbox<M: 'static>(Option<EventLoopProxy<M>>);
842
843impl<M: 'static> Clone for Mailbox<M> {
844 fn clone(&self) -> Self {
845 Self(self.0.clone())
846 }
847}
848
849impl<M: 'static> Mailbox<M> {
850 fn send_event(&self, message: M) -> Result<(), ()> {
853 match &self.0 {
854 Some(proxy) => proxy.send_event(message).map_err(|_| ()),
855 None => Err(()),
856 }
857 }
858}
859
860pub struct App<A: Application> {
866 app: A,
868 proxy: Mailbox<A::Message>,
870 window: Option<Arc<Window>>,
871 renderer: Option<Renderer>,
872 #[cfg(web)]
876 pending_renderer: std::rc::Rc<std::cell::RefCell<Option<Renderer>>>,
877 #[cfg(web)]
879 history: crate::history::History,
880 #[cfg(web)]
883 address: (crate::LocationStrategy, String),
884 #[cfg(desktop)]
886 a11y: Option<crate::a11y::A11y>,
887 #[cfg(web)]
889 a11y: Option<crate::a11y_web::A11y>,
890 ui: Option<Ui<A::Message>>,
892 tree: Option<Box<dyn Widget<A::Message>>>,
894 cursor: Point,
896 hover: crate::hover::Hover,
899 scale: f32,
901 last_size: Option<(f32, f32)>,
903 runtime: Runtime,
905 themes: crate::theming::ThemeFade,
910 reload: Option<crate::reload::ReloadWatcher>,
912 reported_overflows: std::cell::RefCell<std::collections::HashSet<u64>>,
915 inspector: bool,
917 inspector_dump: bool,
919 shift: bool,
921 ctrl: bool,
922 alt: bool,
923 meta: bool,
924 goal_x: Option<f32>,
928 clipboard: clip::Clipboard,
931 started: bool,
934 last_frame: Option<Instant>,
936 build_dirty: bool,
942 app_was_animating: bool,
947 drag: Option<Drag>,
949 pointer_touch: bool,
952 gesture_velocity: VelocityTracker,
959 gesture_clock: Option<(f32, f32)>,
966 gesture_start: Instant,
967 reorder_x: f32,
971 reorder_y: f32,
976 #[cfg(any(desktop, web))]
980 announce: String,
981 press: PressRecognizer,
983 long_press_msg: Option<A::Message>,
985 pending_word: Option<WidgetId>,
988 pending_region_hold: bool,
991 caret: crate::caret::CaretBlink,
993 #[cfg(android)]
997 autofill_reported: Vec<crate::autofill::AutofillField>,
998 #[cfg(android)]
1000 autofill_focus: Option<WidgetId>,
1001 #[cfg(android)]
1003 autofill_group: Option<WidgetId>,
1004 last_edit_at: Option<Instant>,
1008 pending_lift: Option<frus_widgets::DragSource>,
1012 pending_reorder: Option<(WidgetId, usize, Point)>,
1020 last_click_time: Option<Instant>,
1022 last_region_click: Option<(Instant, Point)>,
1025 pending_tap: Option<PendingTap<A::Message>>,
1027 task_arrivals: u64,
1030 entered: Vec<(WidgetId, Rect)>,
1033 pointer_held: bool,
1035 captured: Vec<(WidgetId, Rect)>,
1038 leaving_counter: u64,
1040 running_subs: HashMap<u64, SubHandle>,
1042 pending_focus: Vec<u64>,
1045 pending_scroll: Vec<(u64, ScrollTo)>,
1049 retry_scroll: Vec<(u64, ScrollTo)>,
1053 pending_sheet: Vec<(u64, SheetTo)>,
1056 retry_sheet: Vec<(u64, SheetTo)>,
1059 focus_history: Vec<WidgetId>,
1064 prev_focus: Option<WidgetId>,
1066 autofocused: std::collections::HashSet<WidgetId>,
1069 frame_stats: crate::frame_stats::FrameStats,
1071 frame_costs: crate::frame_stats::FrameCosts,
1073 occluded: bool,
1075 elapsed: f32,
1077 last_insets: WindowInsets,
1079 #[cfg_attr(not(windows), allow(dead_code))]
1082 title_bar_on: bool,
1083 title_bar: Option<frus_widgets::TitleBar>,
1084 #[cfg_attr(not(windows), allow(dead_code))]
1086 window_icon: Option<&'static [u8]>,
1087 reveal_after_keyboard: bool,
1092 platform: PlatformSettings,
1101 inset_baseline: Option<(Insets, (u32, u32))>,
1105 lifecycle: Lifecycle,
1109 #[cfg(android)]
1110 android_app: Option<winit::platform::android::activity::AndroidApp>,
1111 #[cfg(android)]
1113 soft_input_shown: bool,
1114 #[cfg(android)]
1117 system_bars: Option<frus_widgets::SystemBars>,
1118}
1119
1120impl<A: Application> App<A> {
1121 pub fn new(app: A, proxy: EventLoopProxy<A::Message>) -> Self {
1123 Self::with_mailbox(app, Mailbox(Some(proxy)))
1124 }
1125
1126 #[cfg(any(test, feature = "testing"))]
1130 fn detached(app: A) -> Self {
1131 let mut shell = Self::with_mailbox(app, Mailbox(None));
1132 shell.clipboard = clip::Clipboard::in_memory();
1136 shell
1137 }
1138
1139 fn with_mailbox(app: A, proxy: Mailbox<A::Message>) -> Self {
1140 #[cfg(feature = "net")]
1145 frus_widgets::set_image_fetcher(fetch_image_bytes);
1146 #[cfg(not(web))]
1149 frus_widgets::set_task_spawner(|task| crate::runtime::spawn(task).detach());
1150 #[cfg(web)]
1151 frus_widgets::set_task_spawner(|task| wasm_bindgen_futures::spawn_local(task));
1152 Self {
1153 app,
1154 proxy,
1155 window: None,
1156 renderer: None,
1157 #[cfg(web)]
1158 pending_renderer: std::rc::Rc::new(std::cell::RefCell::new(None)),
1159 #[cfg(web)]
1160 history: crate::history::History::new(),
1161 #[cfg(web)]
1162 address: (crate::LocationStrategy::Hash, "/".to_string()),
1163 #[cfg(desktop)]
1164 a11y: None,
1165 #[cfg(web)]
1166 a11y: None,
1167 ui: None,
1168 tree: None,
1169 cursor: Point::new(0.0, 0.0),
1170 hover: crate::hover::Hover::default(),
1171 scale: 1.0,
1172 last_size: None,
1173 runtime: Runtime::default(),
1174 themes: crate::theming::ThemeFade::default(),
1175 reload: crate::reload::ReloadWatcher::new(),
1176 reported_overflows: std::cell::RefCell::new(std::collections::HashSet::new()),
1177 inspector: false,
1178 inspector_dump: false,
1179 shift: false,
1180 ctrl: false,
1181 alt: false,
1182 meta: false,
1183 goal_x: None,
1184 clipboard: clip::Clipboard::new(),
1185 started: false,
1186 last_frame: None,
1187 build_dirty: true,
1188 app_was_animating: false,
1189 drag: None,
1190 pointer_touch: false,
1191 gesture_velocity: VelocityTracker::default(),
1192 gesture_clock: None,
1193 gesture_start: Instant::now(),
1194 reorder_x: 0.0,
1195 reorder_y: 0.0,
1196 #[cfg(any(desktop, web))]
1197 announce: String::new(),
1198 press: PressRecognizer::new(),
1199 long_press_msg: None,
1200 pending_word: None,
1201 pending_region_hold: false,
1202 caret: crate::caret::CaretBlink::new(),
1203 #[cfg(android)]
1204 autofill_reported: Vec::new(),
1205 #[cfg(android)]
1206 autofill_focus: None,
1207 #[cfg(android)]
1208 autofill_group: None,
1209 last_edit_at: None,
1210 pending_lift: None,
1211 pending_reorder: None,
1212 last_click_time: None,
1213 last_region_click: None,
1214 pending_tap: None,
1215 task_arrivals: 0,
1216 entered: Vec::new(),
1217 pointer_held: false,
1218 captured: Vec::new(),
1219 leaving_counter: 0,
1220 running_subs: HashMap::new(),
1221 pending_focus: Vec::new(),
1222 pending_scroll: Vec::new(),
1223 retry_scroll: Vec::new(),
1224 pending_sheet: Vec::new(),
1225 retry_sheet: Vec::new(),
1226 focus_history: Vec::new(),
1227 prev_focus: None,
1228 autofocused: std::collections::HashSet::new(),
1229 frame_stats: crate::frame_stats::FrameStats::new(),
1230 frame_costs: crate::frame_stats::FrameCosts::default(),
1231 occluded: false,
1232 elapsed: 0.0,
1233 last_insets: WindowInsets::ZERO,
1234 title_bar_on: false,
1235 title_bar: None,
1236 window_icon: None,
1237 reveal_after_keyboard: false,
1238 platform: PlatformSettings::default(),
1239 inset_baseline: None,
1240 lifecycle: Lifecycle::Detached,
1242 #[cfg(android)]
1243 android_app: None,
1244 #[cfg(android)]
1245 soft_input_shown: false,
1246 #[cfg(android)]
1247 system_bars: None,
1248 }
1249 }
1250
1251 #[cfg(any(desktop, web))]
1255 fn activate_for_assistive_tech(&mut self, id: WidgetId) {
1256 if let Some(action) = self.ui.as_ref().and_then(|ui| ui.edit_action_for(id)) {
1257 self.perform_edit_action(action);
1258 return;
1259 }
1260 if let Some(msg) = self.ui.as_ref().and_then(|ui| ui.msg_for(id)) {
1261 if self.ui.as_ref().is_some_and(|ui| ui.hit_is_on_toolbar(id)) {
1263 self.runtime.selection_handles = None;
1264 self.hide_selection_toolbar();
1265 }
1266 self.dispatch(msg);
1267 self.request_redraw();
1268 }
1269 }
1270
1271 #[cfg(desktop)]
1274 fn drain_a11y_actions(&mut self) {
1275 use crate::a11y::A11yAction;
1276 let actions = match self.a11y.as_ref() {
1277 Some(a11y) => a11y.take_actions(),
1278 None => return,
1279 };
1280 for action in actions {
1281 match action {
1282 A11yAction::Click(id) => self.activate_for_assistive_tech(id),
1283 A11yAction::Focus(id) => {
1284 if self.ui.as_ref().is_some_and(|ui| ui.hit_is_on_toolbar(id)) {
1288 continue;
1289 }
1290 self.runtime.input.focused = Some(id);
1291 self.runtime.focus_visible = true;
1292 self.request_redraw();
1293 }
1294 }
1295 }
1296 }
1297
1298 #[cfg(web)]
1301 fn drain_a11y_actions(&mut self) {
1302 use crate::a11y_web::A11yAction;
1303 let actions = match self.a11y.as_ref() {
1304 Some(a11y) => a11y.take_actions(),
1305 None => return,
1306 };
1307 for action in actions {
1308 match action {
1309 A11yAction::Click(id) => self.activate_for_assistive_tech(id),
1310 A11yAction::Focus(id) => {
1311 if self.ui.as_ref().is_some_and(|ui| ui.hit_is_on_toolbar(id)) {
1315 continue;
1316 }
1317 self.runtime.input.focused = Some(id);
1318 self.runtime.focus_visible = true;
1319 self.request_redraw();
1320 }
1321 }
1322 }
1323 }
1324
1325 fn sync_system_bars(&mut self, theme: &Theme) {
1334 #[cfg(android)]
1335 {
1336 let Some(ui) = self.ui.as_ref() else {
1337 return;
1338 };
1339 let Some(window) = self.window.as_ref() else {
1340 return;
1341 };
1342 let scale = self.total_scale();
1343 let size = window.inner_size();
1344 let (width, height) = (size.width as f32 / scale, size.height as f32 / scale);
1345 let edges = self.last_insets.padding;
1346 let bars = ui
1347 .system_ui_style(
1348 frus_widgets::Point::new(width / 2.0, edges.top + 0.5),
1349 frus_widgets::Point::new(width / 2.0, height - edges.bottom - 0.5),
1350 )
1351 .resolve(theme);
1352 if self.system_bars != Some(bars) && crate::android_system_bars::apply(bars) {
1353 self.system_bars = Some(bars);
1354 }
1355 }
1356 #[cfg(not(android))]
1357 let _ = theme;
1358 }
1359
1360 fn sync_soft_input(&mut self) {
1364 #[cfg(android)]
1365 {
1366 let editing = self
1367 .runtime
1368 .input
1369 .focused
1370 .and_then(|id| {
1371 self.tree
1372 .as_ref()
1373 .and_then(|tree| find_widget(tree.as_ref(), id))
1374 })
1375 .is_some_and(wants_keyboard);
1376 if editing != self.soft_input_shown {
1377 self.soft_input_shown = editing;
1378 self.end_composition();
1379 if crate::android_ime::installed() {
1380 if editing {
1382 if let Some(id) = self.runtime.input.focused {
1383 self.push_ime_context(id);
1384 }
1385 crate::android_ime::start_input(self.focused_ime());
1386 } else {
1387 crate::android_ime::clear_editor_state();
1388 crate::android_ime::stop_input();
1389 }
1390 } else if let Some(app) = &self.android_app {
1391 if editing {
1393 app.show_soft_input(true);
1394 } else {
1395 app.hide_soft_input(false);
1396 }
1397 }
1398 }
1399 }
1400 }
1401
1402 #[cfg(android)]
1409 fn focused_ime(&self) -> frus_widgets::Ime {
1410 self.runtime
1411 .input
1412 .focused
1413 .and_then(|id| {
1414 self.tree
1415 .as_ref()
1416 .and_then(|tree| find_widget(tree.as_ref(), id))
1417 })
1418 .map(|widget| widget.ime())
1419 .unwrap_or_default()
1420 }
1421
1422 fn set_lifecycle(&mut self, state: Lifecycle) {
1424 if self.lifecycle != state {
1425 self.lifecycle = state;
1426 self.app.on_lifecycle(state);
1427 }
1428 }
1429
1430 fn request_soft_input(&mut self) {
1439 #[cfg(android)]
1440 {
1441 let typing = self
1444 .runtime
1445 .input
1446 .focused
1447 .and_then(|id| {
1448 self.tree
1449 .as_ref()
1450 .and_then(|tree| find_widget(tree.as_ref(), id))
1451 })
1452 .is_some_and(wants_keyboard);
1453 if !typing {
1454 return;
1455 }
1456 self.soft_input_shown = true;
1457 self.end_composition();
1458 if crate::android_ime::installed() {
1459 if let Some(id) = self.runtime.input.focused {
1460 self.push_ime_context(id);
1461 }
1462 crate::android_ime::start_input(self.focused_ime());
1463 } else if let Some(app) = &self.android_app {
1464 app.show_soft_input(true);
1465 }
1466 }
1467 }
1468
1469 #[cfg(android)]
1474 fn drain_ime(&mut self) {
1475 use crate::ime::Step;
1476 let events = crate::android_ime::drain();
1477 if events.is_empty() {
1478 return;
1479 }
1480 let Some(focused) = self.runtime.input.focused else {
1481 return;
1482 };
1483 for event in events {
1484 let edit = self
1485 .runtime
1486 .edits
1487 .get(&focused)
1488 .copied()
1489 .unwrap_or_default();
1490 let text = self
1493 .tree
1494 .as_ref()
1495 .and_then(|tree| find_widget(tree.as_ref(), focused))
1496 .and_then(|widget| widget.text_value().map(str::to_owned))
1497 .unwrap_or_default();
1498 for step in crate::ime::plan(&event, &edit, &text) {
1499 match step {
1500 Step::Place { cursor, anchor } => {
1501 let edit = self.runtime.edits.entry(focused).or_default();
1502 edit.cursor = cursor;
1503 edit.anchor = anchor;
1504 }
1505 Step::Key(key) => self.apply_key(focused, key),
1506 Step::Compose(region) => {
1507 if let Some(edit) = self.runtime.edits.get_mut(&focused) {
1508 edit.composing = region;
1509 }
1510 }
1511 Step::ComposeTo(start) => {
1512 if let Some(edit) = self.runtime.edits.get_mut(&focused) {
1513 let end = edit.cursor;
1514 edit.composing = (end > start).then_some((start, end));
1515 }
1516 }
1517 }
1518 }
1519 }
1520 self.push_ime_context(focused);
1522 self.request_redraw();
1523 }
1524
1525 #[cfg(android)]
1533 fn sync_autofill(&mut self) {
1534 use crate::{android_autofill as platform, autofill};
1535 if !crate::android_ime::installed() {
1536 return;
1537 }
1538 let scale = self.total_scale();
1539 let (Some(ui), Some(tree)) = (self.ui.as_ref(), self.tree.as_ref()) else {
1540 return;
1541 };
1542 let fields_of = |id: WidgetId| {
1543 autofill::structure(&ui.form_of(id), scale, |widget| {
1544 find_widget(tree.as_ref(), widget).map(|w| {
1545 (
1546 w.autofill_hints(),
1547 w.text_value().unwrap_or_default().to_owned(),
1548 )
1549 })
1550 })
1551 };
1552 let focus = self.runtime.input.focused.filter(|&id| {
1554 find_widget(tree.as_ref(), id).is_some_and(|w| !w.autofill_hints().is_empty())
1555 });
1556 if focus != self.autofill_focus {
1557 if let Some(left) = self.autofill_focus {
1558 platform::exit(autofill::virtual_id(left));
1559 }
1560 if let Some(entered) = focus {
1561 let group = ui.form_group(entered);
1562 let shown: &[autofill::AutofillField] =
1565 if group.is_some() && group == self.autofill_group {
1566 &self.autofill_reported
1567 } else {
1568 &[]
1569 };
1570 let fields = autofill::with_gone(fields_of(entered), shown);
1571 platform::publish(&fields);
1572 platform::enter(autofill::virtual_id(entered));
1573 self.autofill_reported = fields;
1574 self.autofill_group = group;
1575 }
1576 self.autofill_focus = focus;
1577 } else if let Some(id) = focus {
1578 let fields = autofill::with_gone(fields_of(id), &self.autofill_reported);
1579 for (virtual_id, value) in autofill::changed(&self.autofill_reported, &fields) {
1580 platform::value_changed(virtual_id, &value);
1581 }
1582 self.autofill_reported = fields;
1583 }
1584 if let Some(group) = self.autofill_group {
1585 if ui.group_stops(group).is_empty() {
1586 platform::commit();
1587 self.autofill_group = None;
1588 self.autofill_reported.clear();
1589 }
1590 }
1591 }
1592
1593 #[cfg(android)]
1597 fn drain_autofill(&mut self) {
1598 let fills = crate::android_autofill::drain();
1599 if fills.is_empty() {
1600 return;
1601 }
1602 for (widget, value) in crate::autofill::route(&self.autofill_reported, fills) {
1603 let message = self
1604 .tree
1605 .as_ref()
1606 .and_then(|tree| find_widget(tree.as_ref(), widget))
1607 .and_then(|w| w.replace_value(value.clone()));
1608 if let Some(message) = message {
1609 self.runtime.edits.insert(
1610 widget,
1611 Edit {
1612 cursor: value.chars().count(),
1613 anchor: None,
1614 composing: None,
1615 },
1616 );
1617 self.dispatch_edit(message);
1618 }
1619 }
1620 if let Some(id) = self.runtime.input.focused {
1621 self.push_ime_context(id);
1622 }
1623 self.request_redraw();
1624 }
1625
1626 #[cfg(android)]
1629 fn push_ime_context(&self, id: WidgetId) {
1630 let value = self
1631 .tree
1632 .as_ref()
1633 .and_then(|tree| find_widget(tree.as_ref(), id))
1634 .and_then(|widget| widget.text_value().map(|s| s.to_string()));
1635 if let Some(text) = value {
1636 let edit = self.runtime.edits.get(&id).copied().unwrap_or_default();
1637 crate::android_ime::set_editor_state(&text, edit.cursor, edit.selection_range());
1638 let len = text.chars().count();
1642 let at = |i: usize| crate::ime::utf16_index(&text, i.min(len)) as i32;
1643 let (start, end) = edit.selection_range().unwrap_or((edit.cursor, edit.cursor));
1644 let (cand_start, cand_end) = edit
1645 .composing
1646 .map(|(s, e)| (at(s), at(e)))
1647 .unwrap_or((-1, -1));
1648 crate::android_ime::update_selection(at(start), at(end), cand_start, cand_end);
1649 }
1650 }
1651
1652 #[cfg(android)]
1655 fn end_composition(&mut self) {
1656 if let Some(id) = self.runtime.input.focused {
1657 if let Some(edit) = self.runtime.edits.get_mut(&id) {
1658 edit.composing = None;
1659 }
1660 }
1661 }
1662
1663 #[cfg(android)]
1665 pub(crate) fn set_android_app(
1666 &mut self,
1667 android_app: winit::platform::android::activity::AndroidApp,
1668 ) {
1669 self.android_app = Some(android_app);
1670 }
1671
1672 fn compute_insets(&self, phys_w: u32, phys_h: u32, scale: f32) -> Insets {
1683 #[cfg(android)]
1684 if let Some(app) = &self.android_app {
1685 let r = app.content_rect();
1686 if r.right > r.left && r.bottom > r.top {
1688 let left = r.left.max(0) as f32;
1689 let top = r.top.max(0) as f32;
1690 let right = (phys_w as i32 - r.right).max(0) as f32;
1691 let bottom = (phys_h as i32 - r.bottom).max(0) as f32;
1692 return Insets::new(top / scale, right / scale, bottom / scale, left / scale);
1693 }
1694 }
1695 let _ = (phys_w, phys_h, scale);
1696 Insets::ZERO
1697 }
1698
1699 fn refresh_platform_settings(&mut self) {
1705 #[cfg(android)]
1706 if let Some(app) = &self.android_app {
1707 let read = crate::android_settings::read(app);
1708 if read != self.platform {
1709 self.platform = read;
1710 self.build_dirty = true;
1711 }
1712 return;
1713 }
1714 let brightness = self
1720 .window
1721 .as_ref()
1722 .and_then(|window| window.theme())
1723 .map(|theme| match theme {
1724 winit::window::Theme::Dark => frus_widgets::Brightness::Dark,
1725 winit::window::Theme::Light => frus_widgets::Brightness::Light,
1726 })
1727 .unwrap_or_default();
1728 if brightness != self.platform.brightness {
1729 self.platform.brightness = brightness;
1730 self.build_dirty = true;
1731 }
1732
1733 if self.platform.locales.is_empty() {
1740 let read = platform_locales();
1741 if !read.is_empty() {
1742 self.platform.locales = read;
1743 self.build_dirty = true;
1744 }
1745 }
1746 }
1747}
1748
1749#[cfg(android)]
1758fn platform_locales() -> Vec<frus_widgets::Locale> {
1759 Vec::new()
1760}
1761
1762#[cfg(not(android))]
1763fn platform_locales() -> Vec<frus_widgets::Locale> {
1764 #[cfg(not(web))]
1765 {
1766 sys_locale::get_locales()
1767 .filter_map(|tag| frus_widgets::Locale::parse(&tag))
1768 .collect()
1769 }
1770 #[cfg(web)]
1771 {
1772 let Some(navigator) = web_sys::window().map(|window| window.navigator()) else {
1775 return Vec::new();
1776 };
1777 let listed: Vec<frus_widgets::Locale> = navigator
1778 .languages()
1779 .iter()
1780 .filter_map(|value| value.as_string())
1781 .filter_map(|tag| frus_widgets::Locale::parse(&tag))
1782 .collect();
1783 if listed.is_empty() {
1784 return navigator
1785 .language()
1786 .and_then(|tag| frus_widgets::Locale::parse(&tag))
1787 .into_iter()
1788 .collect();
1789 }
1790 listed
1791 }
1792}
1793
1794impl<A: Application> ApplicationHandler<A::Message> for App<A> {
1795 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
1796 if self.window.is_some() {
1797 return;
1798 }
1799 self.set_lifecycle(Lifecycle::Resumed);
1801 self.refresh_platform_settings();
1804
1805 let mut attributes = Window::default_attributes()
1806 .with_title(self.app.title())
1807 .with_min_inner_size(winit::dpi::LogicalSize::new(360.0, 280.0));
1809 #[cfg(desktop)]
1812 {
1813 attributes = attributes.with_visible(false);
1814 }
1815 #[cfg(desktop)]
1819 {
1820 let icon = crate::icon::window_icon(&self.app.icon());
1821 #[cfg(windows)]
1822 {
1823 use winit::platform::windows::WindowAttributesExtWindows;
1824 attributes = attributes.with_taskbar_icon(icon.clone());
1825 }
1826 attributes = attributes.with_window_icon(icon);
1827 }
1828 #[cfg(not(web))]
1833 if let Some((w, h)) = self.app.window_size() {
1834 attributes = attributes.with_inner_size(winit::dpi::LogicalSize::new(w, h));
1835 }
1836 #[cfg(web)]
1838 {
1839 use winit::platform::web::WindowAttributesExtWebSys;
1840 attributes = attributes.with_append(true);
1841 }
1842 let window = match event_loop.create_window(attributes) {
1843 Ok(window) => Arc::new(window),
1844 Err(err) => {
1845 log::error!("failed to create the window: {err}");
1846 event_loop.exit();
1847 return;
1848 }
1849 };
1850
1851 #[cfg(web)]
1855 {
1856 self.scale = window.scale_factor() as f32;
1857 self.window = Some(window.clone());
1858 self.a11y = crate::a11y_web::A11y::new(&window);
1861 crate::web_keys::let_the_browser_keep_its_shortcuts();
1864 crate::web_icon::apply(&self.app.icon());
1866 self.build_dirty = true;
1867 if !self.started {
1868 self.started = true;
1869 let command = self.app.init();
1870 self.run_command(command);
1871 self.sync_subscriptions();
1872 self.open_launch_address(&window);
1873 }
1874 let slot = self.pending_renderer.clone();
1875 let win = window.clone();
1876 let size = window.inner_size();
1877 wasm_bindgen_futures::spawn_local(async move {
1878 match Renderer::new(win.clone(), size.width.max(1), size.height.max(1)).await {
1879 Ok(r) => {
1880 *slot.borrow_mut() = Some(r);
1881 win.request_redraw();
1882 }
1883 Err(err) => log::error!("failed to initialise the renderer (Web): {err:#}"),
1884 }
1885 });
1886 }
1887
1888 #[cfg(not(web))]
1889 {
1890 let size = window.inner_size();
1891 let (width, height) = (size.width.max(1), size.height.max(1));
1892 let see_through = if cfg!(windows) {
1896 match pollster::block_on(Renderer::see_through(window.clone(), width, height)) {
1897 Ok(renderer) => Some(renderer),
1898 Err(err) => {
1899 log::info!("title bar painted by frus: {err:#}");
1900 None
1901 }
1902 }
1903 } else {
1904 None
1905 };
1906 let renderer = match see_through {
1907 Some(renderer) => Ok(renderer),
1908 None => pollster::block_on(Renderer::new(window.clone(), width, height)),
1909 };
1910
1911 match renderer {
1912 Ok(renderer) => {
1913 self.scale = window.scale_factor() as f32;
1914 #[cfg(desktop)]
1917 {
1918 self.a11y = Some(crate::a11y::A11y::new(event_loop, &window));
1919 window.set_visible(true);
1920 crate::instance::set_window(window.clone());
1923 }
1924 self.window = Some(window.clone());
1925 #[cfg(android)]
1927 crate::android_ime::set_window(Some(window.clone()));
1928 self.renderer = Some(renderer);
1929 self.build_dirty = true;
1931 if !self.started {
1934 self.started = true;
1935 let command = self.app.init();
1936 self.run_command(command);
1937 self.sync_subscriptions();
1938 #[cfg(any(android, desktop))]
1939 self.open_launch_link();
1940 }
1941 window.request_redraw();
1942 }
1943 Err(err) => {
1944 log::error!("failed to initialise the renderer: {err:#}");
1945 event_loop.exit();
1946 }
1947 }
1948 }
1949 }
1950
1951 fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
1956 self.set_lifecycle(Lifecycle::Paused);
1958 self.renderer = None;
1959 self.window = None;
1960 #[cfg(android)]
1961 crate::android_ime::set_window(None);
1962 self.last_frame = None;
1963 }
1964
1965 fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
1968 self.set_lifecycle(Lifecycle::Detached);
1969 }
1970
1971 fn user_event(&mut self, _event_loop: &ActiveEventLoop, message: A::Message) {
1974 self.dispatch(message);
1975 self.request_redraw();
1976 }
1977
1978 fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) {
1982 if matches!(cause, StartCause::ResumeTimeReached { .. }) && self.press.poll(Instant::now())
1983 {
1984 self.hold_deadline_reached();
1985 }
1986 if matches!(cause, StartCause::ResumeTimeReached { .. }) {
1987 self.poll_pending_tap(Instant::now());
1988 }
1989
1990 if matches!(cause, StartCause::ResumeTimeReached { .. })
1992 && self.lifecycle == Lifecycle::Resumed
1993 && self.caret.next_toggle(Instant::now()).is_some()
1994 {
1995 self.request_redraw();
1996 }
1997
1998 #[cfg(android)]
2000 self.drain_ime();
2001 #[cfg(android)]
2003 self.drain_autofill();
2004
2005 #[cfg(desktop)]
2007 self.drain_a11y_actions();
2008 #[cfg(web)]
2010 self.drain_a11y_actions();
2011
2012 if let Some(watcher) = self.reload.as_mut() {
2015 if watcher.binary_changed() {
2016 watcher.handoff(self.app.save_state());
2017 }
2018 }
2019 event_loop.set_control_flow(self.idle_control_flow());
2020 }
2021
2022 fn window_event(
2023 &mut self,
2024 event_loop: &ActiveEventLoop,
2025 _window_id: WindowId,
2026 event: WindowEvent,
2027 ) {
2028 #[cfg(desktop)]
2030 if let (Some(a11y), Some(window)) = (self.a11y.as_mut(), self.window.as_ref()) {
2031 a11y.process_event(window, &event);
2032 }
2033
2034 match event {
2035 WindowEvent::CloseRequested => event_loop.exit(),
2036
2037 WindowEvent::Focused(focused) => {
2041 if !matches!(self.lifecycle, Lifecycle::Paused | Lifecycle::Detached) {
2042 self.set_lifecycle(if focused {
2043 Lifecycle::Resumed
2044 } else {
2045 Lifecycle::Inactive
2046 });
2047 }
2048 #[cfg(windows)]
2052 if self.title_bar_on {
2053 self.request_redraw();
2054 }
2055 }
2056
2057 WindowEvent::Resized(size) => {
2058 if let Some(renderer) = self.renderer.as_mut() {
2059 renderer.resize(size.width, size.height);
2060 }
2061 if let Some(window) = &self.window {
2062 window.request_redraw();
2063 }
2064 }
2065
2066 WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
2067 self.scale = scale_factor as f32;
2068 if let Some(window) = &self.window {
2070 let size = window.inner_size();
2071 if let Some(renderer) = self.renderer.as_mut() {
2072 renderer.resize(size.width, size.height);
2073 }
2074 }
2075 self.request_redraw();
2076 }
2077
2078 WindowEvent::Occluded(occluded) => {
2079 self.occluded = occluded;
2080 if !occluded {
2081 self.request_redraw();
2082 }
2083 }
2084
2085 WindowEvent::CursorMoved { position, .. } => {
2090 let scale = self.total_scale();
2091 let position = Point::new(position.x as f32 / scale, position.y as f32 / scale);
2092 self.pointer(
2093 event_loop,
2094 PointerEvent {
2095 kind: PointerKind::Move,
2096 position,
2097 touch: false,
2098 },
2099 );
2100 }
2101
2102 WindowEvent::CursorLeft { .. } => {
2104 self.hover.left();
2105 self.sync_hover();
2106 self.sync_regions(false);
2107 }
2108
2109 WindowEvent::Touch(touch) => {
2110 let scale = self.total_scale();
2111 let position = Point::new(
2112 touch.location.x as f32 / scale,
2113 touch.location.y as f32 / scale,
2114 );
2115 let kind = match touch.phase {
2116 TouchPhase::Started => PointerKind::Down,
2117 TouchPhase::Moved => PointerKind::Move,
2118 TouchPhase::Ended => PointerKind::Up,
2119 TouchPhase::Cancelled => PointerKind::Cancel,
2120 };
2121 self.pointer(
2122 event_loop,
2123 PointerEvent {
2124 kind,
2125 position,
2126 touch: true,
2127 },
2128 );
2129 }
2130
2131 WindowEvent::ModifiersChanged(modifiers) => {
2132 let state = modifiers.state();
2133 self.shift = state.shift_key();
2134 self.ctrl = state.control_key();
2135 self.alt = state.alt_key();
2136 self.meta = state.super_key();
2137 }
2138
2139 WindowEvent::MouseInput {
2140 state: ElementState::Pressed,
2141 button: MouseButton::Left,
2142 ..
2143 } => self.pointer(
2144 event_loop,
2145 PointerEvent {
2146 kind: PointerKind::Down,
2147 position: self.cursor,
2148 touch: false,
2149 },
2150 ),
2151
2152 WindowEvent::MouseInput {
2153 state: ElementState::Released,
2154 button: MouseButton::Left,
2155 ..
2156 } => self.pointer(
2157 event_loop,
2158 PointerEvent {
2159 kind: PointerKind::Up,
2160 position: self.cursor,
2161 touch: false,
2162 },
2163 ),
2164
2165 WindowEvent::MouseInput {
2168 state: ElementState::Pressed,
2169 button: MouseButton::Right,
2170 ..
2171 } => self.context_click(),
2172
2173 WindowEvent::KeyboardInput { event, .. } if event.state == ElementState::Pressed => {
2174 self.key_down(&KeyDown::from(&event), Some(event_loop));
2175 }
2176
2177 WindowEvent::MouseWheel { delta, .. } => {
2178 let (mut dx, mut dy) = match delta {
2179 MouseScrollDelta::LineDelta(x, y) => (x * SCROLL_SPEED, y * SCROLL_SPEED),
2180 MouseScrollDelta::PixelDelta(pos) => {
2182 let scale = self.total_scale();
2183 (pos.x as f32 / scale, pos.y as f32 / scale)
2184 }
2185 };
2186 if self.shift {
2188 dx = dy;
2189 dy = 0.0;
2190 }
2191 if let Some((id, viewport)) = self
2194 .ui
2195 .as_ref()
2196 .and_then(|ui| ui.interactive_at(self.cursor))
2197 {
2198 let (min, max) = self
2199 .tree
2200 .as_ref()
2201 .and_then(|tree| find_widget(tree.as_ref(), id))
2202 .and_then(|w| w.interactive())
2203 .unwrap_or((0.5, 4.0));
2204 let factor = (1.0 + dy * 0.1 / SCROLL_SPEED).clamp(0.2, 5.0);
2206 self.runtime.interactive_velocity.remove(&id);
2208 let view = self.runtime.interactive.entry(id).or_default();
2209 *view = view
2211 .zoom_at(factor, self.cursor, min, max)
2212 .clamped(viewport);
2213 self.request_redraw();
2214 return;
2215 }
2216 let hit = {
2220 let scroll = &self.runtime.scroll;
2221 self.ui.as_ref().and_then(|ui| {
2222 ui.scroll_chain(self.cursor).find(|area| {
2223 area.accepts_user_offset(
2224 scroll.get(&area.id).copied().unwrap_or((0.0, 0.0)),
2225 )
2226 })
2227 })
2228 };
2229 if let Some(area) = hit {
2230 let id = area.id;
2235 let physics = area.physics;
2236 let over = if physics.allows_overscroll() {
2237 SCROLL_OVER
2238 } else {
2239 0.0
2240 };
2241 self.runtime.stop_scroll_fling(id);
2244 let current = self.runtime.scroll.get(&id).copied().unwrap_or((0.0, 0.0));
2245 let (ddx, ddy) = area.offset_delta((dx, dy));
2249 let target = self.runtime.scroll_target.entry(id).or_insert(current);
2250 let (wanted_x, wanted_y) = (target.0 + ddx, target.1 + ddy);
2251 target.0 = wanted_x.clamp(-over, area.max_x + over);
2252 target.1 = wanted_y.clamp(-over, area.max_y + over);
2253 let refused = (wanted_x - target.0, wanted_y - target.1);
2254 self.runtime.scroll_velocity.entry(id).or_insert((0.0, 0.0));
2255 let cursor = self.cursor;
2258 for (refused, vertical, extent) in [
2259 (refused.0, false, area.viewport.width),
2260 (refused.1, true, area.viewport.height),
2261 ] {
2262 if refused.abs() < 1e-3 {
2263 continue;
2264 }
2265 let edge = area.refused_edge(vertical, refused);
2266 let (offset, cross) =
2267 frus_widgets::glow_cross_axis(area.viewport, edge, cursor);
2268 self.runtime.glow_pull(
2269 id,
2270 area.overscroll,
2271 edge,
2272 refused,
2273 extent,
2274 offset,
2275 cross,
2276 );
2277 self.runtime.glow_scroll_end(id);
2281 }
2282 self.request_redraw();
2283 }
2284 }
2285
2286 WindowEvent::RedrawRequested => {
2287 let measuring = self.frame_stats.enabled();
2289 let frame_start = measuring.then(Instant::now);
2290 self.frame_costs = crate::frame_stats::FrameCosts::default();
2291 #[cfg(web)]
2294 if self.renderer.is_none() {
2295 match self.pending_renderer.borrow_mut().take() {
2296 Some(mut renderer) => {
2297 if let Some(window) = &self.window {
2305 let size = window.inner_size();
2306 renderer.resize(size.width.max(1), size.height.max(1));
2307 }
2308 self.renderer = Some(renderer);
2309 }
2310 None => return,
2311 }
2312 }
2313 while let Some(pasted) = self.clipboard.take_answered() {
2317 self.land_paste(pasted);
2318 }
2319 if self
2322 .runtime
2323 .selection_toolbar
2324 .is_some_and(|mark| self.runtime.input.focused != Some(mark.id))
2325 {
2326 self.runtime.selection_toolbar = None;
2327 }
2328 if self.occluded {
2330 return;
2331 }
2332 let size = self
2333 .window
2334 .as_ref()
2335 .map(|w| w.inner_size())
2336 .unwrap_or_default();
2337 if size.width == 0 || size.height == 0 {
2339 self.last_frame = None; return;
2341 }
2342 let scale = self.total_scale();
2345 let (width, height) = (size.width as f32 / scale, size.height as f32 / scale);
2346
2347 if self.last_size != Some((width, height)) {
2350 self.last_size = Some((width, height));
2351 self.build_dirty = true;
2352 self.app.on_resize(width, height);
2353 }
2354
2355 let raw = self.compute_insets(size.width, size.height, scale);
2362 let phys = (size.width, size.height);
2363 let mut baseline = match self.inset_baseline {
2364 Some((b, s)) if s == phys => b,
2365 _ => {
2366 self.inset_baseline = Some((raw, phys));
2367 raw
2368 }
2369 };
2370 if raw.bottom < baseline.bottom {
2371 baseline = raw;
2372 self.inset_baseline = Some((raw, phys));
2373 }
2374 let insets = WindowInsets::from_baseline(baseline, raw);
2375 if self.last_insets != insets {
2376 if insets.view_insets.bottom > self.last_insets.view_insets.bottom {
2377 self.reveal_after_keyboard = true;
2378 }
2379 self.last_insets = insets;
2380 self.build_dirty = true;
2381 self.app.on_insets(insets);
2382 }
2383
2384 let now = Instant::now();
2386 let dt = self
2387 .last_frame
2388 .map(|prev| (now - prev).as_secs_f32().min(0.05))
2389 .unwrap_or(0.0);
2390 self.last_frame = Some(now);
2391
2392 self.elapsed += dt;
2394 self.runtime.time = self.elapsed;
2395
2396 let caret_now = Instant::now();
2401 let caret = if self.lifecycle == Lifecycle::Resumed {
2402 self.caret_signature()
2403 } else {
2404 None
2405 };
2406 self.caret.observe(caret_now, caret);
2407 self.runtime.caret_hidden = self.caret.hidden(caret_now);
2408
2409 let mut app_animating = self.app.tick(dt);
2411
2412 let settings = self
2421 .platform
2422 .accessibility
2423 .with_overrides(self.app.accessibility());
2424
2425 install_ambient(&self.app, &self.platform.locales);
2435
2436 let theme_moved = self.themes.advance(
2437 &self.app,
2438 self.platform.brightness,
2439 settings.high_contrast,
2440 settings.disable_animations,
2441 dt,
2442 );
2443 app_animating |= theme_moved | self.themes.animating();
2446 let theme = self.themes.displayed(&self.app);
2447
2448 let _surface = self.media_query(width, height).install();
2461
2462 self.runtime.still = settings.disable_animations;
2473 self.runtime.scrollbar_hovered = self
2481 .ui
2482 .as_ref()
2483 .and_then(|ui| ui.scrollbar_near(self.cursor))
2484 .map(|bar| bar.id);
2485 self.runtime.scrollbar_dragged = match self.drag {
2486 Some(Drag::Scrollbar { id, .. }) => Some(id),
2487 _ => None,
2488 };
2489 let asked = self.app.effects();
2496 self.run_command(asked);
2497 #[cfg(web)]
2498 self.sync_address();
2499 #[cfg(desktop)]
2500 self.open_links_from_other_instances();
2501 let arrived = self.take_task_arrivals();
2503 if frus_widgets::take_rebuild_request() || arrived {
2504 self.build_dirty = true;
2505 }
2506 let need_build = frame_needs_build(
2507 self.build_dirty,
2508 app_animating,
2509 std::mem::replace(&mut self.app_was_animating, app_animating),
2510 self.tree.is_none() || self.runtime.switching(),
2511 );
2512 if need_build {
2513 let build_start = measuring.then(Instant::now);
2521 let tree = build_view(&self.app, &theme, &self.runtime);
2522 if let Some(at) = build_start {
2523 self.frame_costs.build = Some(at.elapsed().as_secs_f32() * 1000.0);
2524 }
2525 let ids = collect_ids(tree.as_ref());
2526 let present: std::collections::HashSet<_> = ids.iter().copied().collect();
2527
2528 let leaving: std::collections::HashSet<u64> = self
2531 .runtime
2532 .mounted
2533 .iter()
2534 .filter(|id| !present.contains(id))
2535 .map(|id| id.as_u64())
2536 .collect();
2537 if !leaving.is_empty() {
2538 if let Some(ui) = &self.ui {
2539 let captured: Vec<_> = ui
2540 .scene()
2541 .primitives()
2542 .iter()
2543 .filter(|p| leaving.contains(&p.owner()))
2544 .cloned()
2545 .collect();
2546 if !captured.is_empty() {
2547 self.runtime
2548 .leaving
2549 .insert(self.leaving_counter, (captured, 1.0));
2550 self.leaving_counter = self.leaving_counter.wrapping_add(1);
2551 }
2552 }
2553 }
2554
2555 for &id in &ids {
2557 if self.runtime.mounted.insert(id) {
2558 self.runtime.anims.entry(id).or_default().opacity = 0.0;
2559 }
2560 }
2561 self.runtime.mounted.retain(|id| present.contains(id));
2562
2563 self.tree = Some(tree);
2564 self.sync_subscriptions();
2569
2570 self.runtime.paint_cache.borrow_mut().bump_generation();
2575 }
2576 self.build_dirty = false;
2577
2578 if !self.pending_focus.is_empty() {
2583 let keys = std::mem::take(&mut self.pending_focus);
2584 let ids: Vec<WidgetId> = self
2585 .tree
2586 .as_deref()
2587 .map(|tree| keys.iter().filter_map(|&k| find_by_key(tree, k)).collect())
2588 .unwrap_or_default();
2589 if let Some(&id) = ids.last() {
2590 self.runtime.input.focused = Some(id);
2591 self.runtime.focus_visible = true;
2592 }
2593 }
2594
2595 let autoscrolling = self.autoscroll_carried(dt);
2604
2605 let reorder_animating = self.advance_reorder_springs(dt);
2616
2617 let tree = self
2620 .tree
2621 .as_deref()
2622 .expect("the view was built at least once");
2623
2624 let scroll_regions = self
2627 .ui
2628 .as_ref()
2629 .map(|ui| ui.scroll_regions().to_vec())
2630 .unwrap_or_default();
2631 let refresh_areas = self
2634 .ui
2635 .as_ref()
2636 .map(|ui| ui.refresh_areas().to_vec())
2637 .unwrap_or_default();
2638 let dismissables = self
2639 .ui
2640 .as_ref()
2641 .map(|ui| ui.dismissables().to_vec())
2642 .unwrap_or_default();
2643 let mut dismissed: Vec<A::Message> = Vec::new();
2646 let sheet_areas = self
2647 .ui
2648 .as_ref()
2649 .map(|ui| ui.sheets().to_vec())
2650 .unwrap_or_default();
2651 let interactive_bounds = self
2652 .ui
2653 .as_ref()
2654 .map(|ui| ui.interactive_bounds())
2655 .unwrap_or_default();
2656
2657 self.runtime.sync_pages(&scroll_regions);
2661 self.runtime.sync_visible(&scroll_regions);
2665 let requests = std::mem::take(&mut self.pending_scroll);
2670 let (retry, scrolled) =
2671 apply_scroll_requests(&mut self.runtime, tree, requests, &scroll_regions);
2672 self.retry_scroll = retry;
2673 let requests = std::mem::take(&mut self.pending_sheet);
2676 let (retry, sheeted) =
2677 apply_sheet_requests(&mut self.runtime, tree, requests, &sheet_areas);
2678 self.retry_sheet = retry;
2679
2680 let moving: [(&str, bool); 25] = [
2684 ("scrolled", scrolled),
2685 ("sheeted", sheeted),
2686 ("anims", self.runtime.advance(dt)),
2687 ("leaving", self.runtime.advance_leaving(dt)),
2688 ("switchers", self.runtime.advance_switchers(tree, dt)),
2689 ("values", self.runtime.advance_values(tree, dt)),
2690 ("colors", self.runtime.advance_colors(tree, dt)),
2691 ("sizes", self.runtime.advance_sizes(tree, dt)),
2692 ("radii", self.runtime.advance_radii(tree, dt)),
2693 ("paddings", self.runtime.advance_paddings(tree, dt)),
2694 ("offsets", self.runtime.advance_offsets(tree, dt)),
2695 ("pins", self.runtime.advance_pins(tree, dt)),
2696 ("fractions", self.runtime.advance_fractions(tree, dt)),
2697 ("text_styles", self.runtime.advance_text_styles(tree, dt)),
2698 ("transforms", self.runtime.advance_transforms(tree, dt)),
2699 ("scroll", self.runtime.advance_scroll(&scroll_regions, dt)),
2700 ("glow", self.runtime.advance_glow(dt)),
2701 ("refresh", self.runtime.advance_refresh(&refresh_areas, dt)),
2702 ("dismiss", {
2703 let (moving, done) = self.runtime.advance_dismiss(&dismissables, dt);
2708 dismissed.extend(done.into_iter().filter_map(|(id, direction)| {
2709 find_widget(tree, id).and_then(|widget| widget.on_dismissed(direction))
2710 }));
2711 moving
2712 }),
2713 ("sheets", {
2714 let (moving, closed) = self.runtime.advance_sheets(&sheet_areas, dt);
2717 dismissed.extend(closed.into_iter().filter_map(|id| {
2718 find_widget(tree, id).and_then(|widget| widget.on_sheet_dismissed())
2719 }));
2720 let mut handed = false;
2723 for (list, velocity) in self.runtime.take_sheet_handovers() {
2724 if let Some(area) = scroll_regions.iter().find(|a| a.id == list) {
2725 handed |=
2726 self.runtime
2727 .fling_scroll(*area, area.physics, (0.0, velocity));
2728 }
2729 }
2730 moving | handed
2731 }),
2732 (
2733 "interactive",
2734 self.runtime.advance_interactive(&interactive_bounds, dt),
2735 ),
2736 ("ink", self.runtime.advance_ink(dt)),
2737 ("reorder", reorder_animating),
2738 ("autoscroll", autoscrolling),
2739 ("app", app_animating),
2740 ];
2741 let animating = moving.iter().any(|(_, on)| *on);
2742 self.frame_stats.note_moving(&moving);
2743 let layout_start = measuring.then(Instant::now);
2747 let (ui, scene) = if self.inspector {
2748 let (ui, nodes) = frus_widgets::build_ui_inspected(
2749 tree,
2750 Size::new(width, height),
2751 &self.runtime,
2752 &theme,
2753 );
2754 if std::mem::take(&mut self.inspector_dump) {
2755 log::info!(target: "frus::inspector", "
2757{}", frus_widgets::dump_tree(&nodes));
2758 }
2759 let mut scene = ui.scene().clone();
2760 frus_widgets::paint_inspector_overlay(
2761 &nodes,
2762 Some(self.cursor),
2763 Size::new(width, height),
2764 &theme,
2765 &mut scene,
2766 );
2767 (ui, scene.scaled(scale))
2769 } else {
2770 let ui = build_ui(tree, Size::new(width, height), &self.runtime, &theme);
2771 let scene = if matches!(self.drag, Some(Drag::Reorder { moved: true, .. })) {
2774 let mut scene = ui.scene().clone();
2775 self.paint_reorder_preview(&ui, &theme, &mut scene);
2776 scene.scaled(scale)
2777 } else if matches!(self.drag, Some(Drag::Item { moved: true, .. })) {
2778 let mut scene = ui.scene().clone();
2779 self.paint_drag_ghost(&ui, &theme, &mut scene);
2780 scene.scaled(scale)
2781 } else {
2782 ui.scene().scaled(scale)
2784 };
2785 (ui, scene)
2786 };
2787 if let Some(at) = layout_start {
2788 self.frame_costs.layout_paint = at.elapsed().as_secs_f32() * 1000.0;
2789 self.frame_costs.layers = count_layers(scene.primitives());
2790 }
2791 self.report_overflows(&ui);
2792 self.runtime.states.end_frame();
2796 if frus_widgets::rebuild_requested() {
2797 self.request_redraw();
2798 }
2799 let scene = if self.renderer.as_ref().is_some_and(Renderer::sees_through) {
2802 backdrop_under(scene, width * scale, height * scale, self.system_line())
2803 } else {
2804 scene
2805 };
2806 if let Some(renderer) = self.renderer.as_mut() {
2807 let render_start = measuring.then(Instant::now);
2808 match renderer.render(&scene) {
2809 frus_gpu::RenderOutcome::Presented => {}
2810 frus_gpu::RenderOutcome::NeedsReconfigure => {
2811 renderer.reconfigure();
2812 }
2813 frus_gpu::RenderOutcome::Skipped => {}
2814 }
2815 if let Some(at) = render_start {
2816 self.frame_costs.render = at.elapsed().as_secs_f32() * 1000.0;
2817 let timings = renderer.last_timings();
2818 self.frame_costs.acquire = timings.acquire;
2819 self.frame_costs.draw = timings.draw;
2820 self.frame_costs.gpu = timings.gpu;
2821 }
2822 }
2823
2824 let widget_asks = ui.wants_animation();
2834 let loading =
2835 frus_widgets::images_in_flight() > 0 || frus_widgets::tasks_in_flight() > 0;
2836 let wants_animation = widget_asks || loading;
2837
2838 self.ui = Some(ui);
2840 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 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 let paged = self
2882 .ui
2883 .as_ref()
2884 .map(|ui| ui.scroll_regions().to_vec())
2885 .unwrap_or_default();
2886 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 let (_, moved_late) = apply_scroll_requests(
2904 &mut self.runtime,
2905 tree,
2906 std::mem::take(&mut self.retry_scroll),
2907 &paged,
2908 );
2909 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 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 for message in dismissed.into_iter().chain(turned).chain(scrolled) {
2945 self.dispatch(message);
2946 }
2947 self.sync_regions(false);
2950 if moved_late || sheeted_late {
2951 self.request_redraw();
2954 }
2955
2956 self.reconcile_focus();
2960
2961 #[cfg(desktop)]
2963 if let Some(a11y) = self.a11y.as_mut() {
2964 let focus = self.runtime.input.focused;
2965 let title = self.app.title();
2966 if let Some(ui) = self.ui.as_ref() {
2967 a11y.update(ui.semantics(), focus, &title, &self.announce);
2968 }
2969 }
2970 #[cfg(web)]
2972 if let Some(a11y) = self.a11y.as_mut() {
2973 let focus = self.runtime.input.focused;
2974 let title = self.app.title();
2975 if let Some(ui) = self.ui.as_ref() {
2976 a11y.update(ui.semantics(), focus, &title, &self.announce);
2977 }
2978 }
2979
2980 self.sync_system_bars(&theme);
2982
2983 self.sync_soft_input();
2985 #[cfg(android)]
2987 self.sync_autofill();
2988
2989 if animating || wants_animation {
2991 self.request_redraw();
2992 }
2993 if let Some(start) = frame_start {
2994 use crate::frame_stats::Why;
2995 let why = [
2996 (app_animating, Why::APP),
2997 (animating, Why::RUNTIME),
2998 (widget_asks, Why::WIDGET),
2999 (loading, Why::LOADING),
3000 (rehovered || revealed, Why::POINTER),
3001 ];
3002 self.frame_costs.why = why
3003 .into_iter()
3004 .filter(|(on, _)| *on)
3005 .fold(Why::NONE, |all, (_, why)| all | why);
3006 self.frame_stats.frame(start, self.frame_costs);
3007 }
3008 #[cfg(windows)]
3010 self.sync_title_bar();
3011 }
3012
3013 _ => {}
3014 }
3015 }
3016}
3017
3018impl<A: Application> App<A> {
3019 fn system_line(&self) -> f32 {
3022 match self.title_bar {
3024 Some(line) if self.title_bar_on && line.system_paints => {
3025 line.height * self.total_scale()
3026 }
3027 _ => 0.0,
3028 }
3029 }
3030
3031 fn total_scale(&self) -> f32 {
3033 (self.scale * self.app.density()).max(0.1)
3034 }
3035
3036 fn media_query(&self, width: f32, height: f32) -> MediaQuery {
3052 MediaQuery::new(Size::new(width, height))
3053 .with_device_pixel_ratio(self.scale)
3054 .with_density(self.app.density())
3055 .with_text_scaler(self.platform.text_scaler)
3056 .with_platform_brightness(self.platform.brightness)
3057 .with_accessibility(
3058 self.platform
3059 .accessibility
3060 .with_overrides(self.app.accessibility()),
3061 )
3062 .with_insets(self.last_insets)
3063 .with_title_bar(self.title_bar)
3064 }
3065
3066 #[cfg(windows)]
3070 fn sync_title_bar(&mut self) {
3071 use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
3072 let Some(window) = self.window.clone() else {
3073 return;
3074 };
3075 let hwnd = match window.window_handle().map(|h| h.as_raw()) {
3076 Ok(RawWindowHandle::Win32(win)) => {
3077 win.hwnd.get() as windows_sys::Win32::Foundation::HWND
3078 }
3079 _ => return,
3080 };
3081 let wants = self.ui.as_ref().is_some_and(|ui| ui.wants_title_bar());
3082 if wants != self.title_bar_on {
3083 unsafe {
3085 if wants {
3086 crate::title_bar::enable(hwnd);
3087 let wake = window.clone();
3088 crate::title_bar::on_change(move || wake.request_redraw());
3089 } else {
3090 crate::title_bar::disable(hwnd);
3091 }
3092 }
3093 self.title_bar_on = wants;
3094 }
3095 let next = if self.title_bar_on {
3096 let scale = self.total_scale();
3097 let band = crate::title_bar::height() as f32 / scale;
3098 if let Some(ui) = &self.ui {
3100 let to_px = |r: frus_widgets::Rect| windows_sys::Win32::Foundation::RECT {
3101 left: (r.x * scale).floor() as i32,
3102 top: (r.y * scale).floor() as i32,
3103 right: ((r.x + r.width) * scale).ceil() as i32,
3104 bottom: ((r.y + r.height) * scale).ceil() as i32,
3105 };
3106 let interactive = ui
3107 .hit_rects()
3108 .filter(|r| r.y < band && r.width < self.logical_width())
3109 .map(to_px)
3110 .collect();
3111 let icon = ui
3112 .title_bar_regions()
3113 .iter()
3114 .find(|(_, role)| *role == frus_widgets::TitleBarRole::Icon)
3115 .map(|(r, _)| to_px(*r));
3116 crate::title_bar::publish(interactive, icon);
3117 }
3118 let buttons = unsafe { crate::title_bar::buttons(hwnd) }.map(|r| {
3120 frus_widgets::Rect::new(
3121 r.left as f32 / scale,
3122 r.top as f32 / scale,
3123 (r.right - r.left) as f32 / scale,
3124 (r.bottom - r.top) as f32 / scale,
3125 )
3126 });
3127 let button = |code: u32| {
3128 use windows_sys::Win32::UI::WindowsAndMessaging::{
3129 HTCLOSE, HTMAXBUTTON, HTMINBUTTON,
3130 };
3131 match code {
3132 HTMINBUTTON => Some(frus_widgets::CaptionButton::Minimize),
3133 HTMAXBUTTON => Some(frus_widgets::CaptionButton::Maximize),
3134 HTCLOSE => Some(frus_widgets::CaptionButton::Close),
3135 _ => None,
3136 }
3137 };
3138 let (hovered, pressed) = crate::title_bar::buttons_state();
3139 if self.window_icon.is_none() {
3140 self.window_icon = self
3141 .app
3142 .icon()
3143 .png()
3144 .map(|png| &*Box::leak(png.to_vec().into_boxed_slice()));
3145 }
3146 let active = window.has_focus();
3149 let dark = window.theme() == Some(winit::window::Theme::Dark);
3150 let (background, foreground) = crate::title_bar::caption_colors(dark, active);
3151 Some(frus_widgets::TitleBar {
3152 height: buttons.map_or(band, |b| (b.y + b.height).clamp(1.0, band)),
3156 leading: 0.0,
3157 buttons,
3158 icon: self.window_icon,
3159 hovered: button(hovered),
3160 pressed: button(pressed),
3161 maximized: window.is_maximized(),
3162 active,
3163 background: Some(background),
3164 foreground: Some(foreground),
3165 system_paints: self.renderer.as_ref().is_some_and(Renderer::sees_through)
3168 && unsafe { crate::title_bar::system_backdrop(hwnd, dark) },
3170 })
3171 } else {
3172 None
3173 };
3174 if next != self.title_bar {
3175 log::debug!(
3176 target: "frus::title_bar",
3177 "the line: {:?}",
3178 next.map(|line| (line.height, line.buttons, line.active, line.system_paints))
3179 );
3180 self.title_bar = next;
3181 self.build_dirty = true;
3182 self.request_redraw();
3183 }
3184 }
3185
3186 fn is_rtl(&self) -> bool {
3188 self.themes.displayed(&self.app).direction.is_rtl()
3189 }
3190
3191 fn logical_width(&self) -> f32 {
3193 let scale = self.total_scale();
3194 self.window
3195 .as_ref()
3196 .map(|w| w.inner_size().width as f32 / scale)
3197 .unwrap_or(1.0)
3198 .max(1.0)
3199 }
3200
3201 fn request_redraw(&self) {
3202 if let Some(window) = &self.window {
3203 window.request_redraw();
3204 }
3205 }
3206
3207 fn pointer(&mut self, event_loop: &ActiveEventLoop, event: PointerEvent) {
3211 self.pointer_event(event);
3212 event_loop.set_control_flow(self.idle_control_flow());
3214 }
3215
3216 fn pointer_event(&mut self, event: PointerEvent) {
3220 let kind = event.kind;
3221 self.cursor = event.position;
3224 self.route_listeners(kind, event.touch);
3225 self.route_pointer_event(event);
3226 self.pointer_held = match kind {
3227 PointerKind::Down => true,
3228 PointerKind::Up | PointerKind::Cancel => false,
3229 PointerKind::Move => self.pointer_held,
3230 };
3231 self.sync_regions(kind == PointerKind::Move);
3232 }
3233
3234 fn route_pointer_event(&mut self, event: PointerEvent) {
3236 self.cursor = event.position;
3237 self.hover.event(event.kind, event.position, event.touch);
3238 let could_tap = gesture_was_a_tap(self.drag.as_ref());
3241 match event.kind {
3242 PointerKind::Down => {
3243 self.sync_hover();
3246 self.runtime.focus_visible = false;
3248 self.pointer_down(event.touch);
3249 let (long_press, lift) = hold_candidates(
3250 self.drag.as_ref(),
3251 self.ui.as_ref(),
3252 self.tree.as_deref(),
3253 self.cursor,
3254 );
3255 self.long_press_msg = long_press;
3256 self.pending_lift = lift;
3257 let interested = self.long_press_msg.is_some()
3258 || self.pending_lift.is_some()
3259 || self.pending_reorder.is_some()
3260 || self.pending_word.is_some()
3261 || self.pending_region_hold;
3262 self.press.down(self.cursor, Instant::now(), interested);
3263 }
3264 PointerKind::Move => {
3265 self.press.moved(self.cursor);
3266 self.pointer_move();
3267 if self.inspector {
3270 self.request_redraw();
3271 }
3272 }
3273 PointerKind::Up => {
3274 let swallow = self.press.up();
3277 self.pending_lift = None;
3278 self.pending_reorder = None;
3279 self.pending_word = None;
3280 self.pending_region_hold = false;
3281 if swallow
3284 && !matches!(
3285 self.drag,
3286 Some(Drag::Item { moved: true, .. } | Drag::Reorder { moved: true, .. })
3287 )
3288 {
3289 self.drag = None;
3291 self.runtime.input.pressed = None;
3292 self.request_redraw();
3293 } else {
3294 self.pointer_up();
3295 }
3296 }
3297 PointerKind::Cancel => {
3298 self.press.cancel();
3299 self.pending_lift = None;
3300 self.pending_reorder = None;
3301 self.pending_word = None;
3302 self.pending_region_hold = false;
3303 if let Some(Drag::Dismiss { item, .. }) = self.drag {
3306 self.runtime.dismiss_release(
3308 item.id,
3309 0.0,
3310 0.0,
3311 item.spec.axis,
3312 f32::INFINITY,
3315 );
3316 }
3317 if let Some(Drag::Gesture { id, .. }) = self.drag {
3319 self.runtime.release_value(id);
3320 }
3321 if let Some(Drag::Scroll { id, .. }) = self.drag {
3322 self.runtime.release_scroll(id);
3323 self.runtime.glow_scroll_end(id);
3324 if let Some(host) = self.refresh_host_of(id) {
3327 self.runtime.refresh_cancel(host);
3328 }
3329 }
3330 let sheet = match self.drag {
3332 Some(Drag::Sheet { id, available, .. }) => self
3333 .ui
3334 .as_ref()
3335 .and_then(|ui| ui.sheet(id))
3336 .map(|sheet| (sheet.id, sheet.spec.clone(), available)),
3337 Some(Drag::Scroll { id, .. }) => self
3338 .ui
3339 .as_ref()
3340 .and_then(|ui| ui.sheet_holding(id))
3341 .map(|sheet| (sheet.id, sheet.spec.clone(), sheet.available)),
3342 _ => None,
3343 };
3344 if let Some((id, spec, available)) = sheet {
3345 self.runtime.sheet_release(id, &spec, available, 0.0, None);
3346 }
3347 self.drag = None;
3348 self.runtime.input.pressed = None;
3349 self.request_redraw();
3350 }
3351 }
3352 if settle_press(
3355 &mut self.runtime.input,
3356 event.kind,
3357 could_tap,
3358 self.drag.as_ref(),
3359 event.touch,
3360 ) {
3361 self.request_redraw();
3362 }
3363 if matches!(event.kind, PointerKind::Up | PointerKind::Cancel) {
3366 self.sync_hover();
3367 }
3368 }
3369
3370 fn hold_deadline_reached(&mut self) {
3372 let lifting = self.pending_lift.is_some() || self.pending_reorder.is_some();
3378 let word = self.pending_word.take();
3381 let region = std::mem::take(&mut self.pending_region_hold)
3384 && !lifting
3385 && word.is_none()
3386 && self.long_press_msg.is_none();
3387 if let Some(message) = self.long_press_msg.take() {
3388 if !lifting && word.is_none() {
3389 self.dispatch(message);
3390 }
3391 }
3392 if let Some(id) = word {
3393 self.select_held_word(id);
3394 }
3395 if region && self.select_region_word_held() {
3396 if let Some(Drag::Scroll { id, .. }) = self.drag {
3398 self.runtime.release_scroll(id);
3399 }
3400 }
3401 let lift = self.pending_lift.take();
3402 let reorder = self.pending_reorder.take();
3403 if lift.is_some() || reorder.is_some() {
3405 if let Some(Drag::Scroll { id, .. }) = self.drag {
3406 self.runtime.release_scroll(id);
3407 }
3408 }
3409 self.drag = drag_after_hold(self.drag.take(), lift, reorder, self.cursor);
3410 if matches!(self.drag, Some(Drag::Reorder { .. })) {
3411 self.reorder_x = self.cursor.x;
3412 self.reorder_y = self.cursor.y;
3413 }
3414 self.request_redraw();
3415 }
3416
3417 fn idle_control_flow(&self) -> ControlFlow {
3420 let press = self.press.deadline();
3421 let reload = self.reload.as_ref().map(|w| w.deadline());
3422 let caret = (self.lifecycle == Lifecycle::Resumed)
3425 .then(|| self.caret.next_toggle(Instant::now()))
3426 .flatten();
3427 let tap = self.pending_tap.as_ref().map(|p| p.at + DOUBLE_TAP_TIMEOUT);
3429 match [press, reload, caret, tap].into_iter().flatten().min() {
3430 Some(at) => ControlFlow::WaitUntil(at),
3431 None => ControlFlow::Wait,
3432 }
3433 }
3434
3435 fn caret_signature(&self) -> Option<crate::caret::Signature> {
3439 use std::hash::{Hash, Hasher};
3440 let field = self.runtime.input.focused?;
3441 let widget = find_widget(self.tree.as_ref()?.as_ref(), field)?;
3442 let text = widget.text_value()?;
3443 let mut hasher = std::collections::hash_map::DefaultHasher::new();
3444 text.hash(&mut hasher);
3445 Some(crate::caret::Signature {
3446 field,
3447 edit: self.runtime.edits.get(&field).copied().unwrap_or_default(),
3448 value: hasher.finish(),
3449 })
3450 }
3451
3452 fn pointer_move(&mut self) {
3455 if self.drag.is_some() {
3456 self.handle_drag();
3457 return;
3458 }
3459 let hovered = self.sync_hover();
3460 self.update_cursor_icon(hovered);
3464 }
3465
3466 fn sync_hover(&mut self) -> Option<WidgetId> {
3472 let hovered = self.ui.as_ref().and_then(|ui| self.hover.target(ui));
3473 if hovered != self.runtime.input.hovered {
3474 self.runtime.input.hovered = hovered;
3475 self.request_redraw();
3476 }
3477 hovered
3478 }
3479
3480 fn take_task_arrivals(&mut self) -> bool {
3483 let now = frus_widgets::task_arrivals();
3484 std::mem::replace(&mut self.task_arrivals, now) != now
3485 }
3486
3487 fn route_listeners(&mut self, kind: PointerKind, touch: bool) {
3491 use frus_widgets::ListenerEventKind as Raw;
3492 let at = self.cursor;
3493 let heard: Vec<(WidgetId, Rect, Raw)> = match kind {
3494 PointerKind::Down => {
3495 self.captured = self
3496 .ui
3497 .as_ref()
3498 .map(|ui| ui.pointer_listeners_at(at))
3499 .unwrap_or_default();
3500 self.captured
3501 .iter()
3502 .map(|(id, rect)| (*id, *rect, Raw::Down))
3503 .collect()
3504 }
3505 PointerKind::Move if self.pointer_held => self
3506 .captured
3507 .iter()
3508 .map(|(id, rect)| (*id, *rect, Raw::Move))
3509 .collect(),
3510 PointerKind::Move if !touch => self
3511 .ui
3512 .as_ref()
3513 .map(|ui| ui.pointer_listeners_at(at))
3514 .unwrap_or_default()
3515 .into_iter()
3516 .map(|(id, rect)| (id, rect, Raw::Hover))
3517 .collect(),
3518 PointerKind::Move => Vec::new(),
3519 PointerKind::Up | PointerKind::Cancel => {
3520 let raw = if kind == PointerKind::Up {
3521 Raw::Up
3522 } else {
3523 Raw::Cancel
3524 };
3525 std::mem::take(&mut self.captured)
3526 .into_iter()
3527 .map(|(id, rect)| (id, rect, raw))
3528 .collect()
3529 }
3530 };
3531 let messages: Vec<A::Message> = heard
3532 .into_iter()
3533 .flat_map(|(id, rect, kind)| {
3534 let event = frus_widgets::ListenerEvent {
3535 kind,
3536 local: Point::new(at.x - rect.x, at.y - rect.y),
3537 touch,
3538 };
3539 self.tree
3540 .as_ref()
3541 .and_then(|tree| find_widget(tree.as_ref(), id))
3542 .map(|widget| widget.on_pointer_event(event))
3543 .unwrap_or_default()
3544 })
3545 .collect();
3546 for message in messages {
3547 self.dispatch(message);
3548 self.request_redraw();
3549 }
3550 }
3551
3552 fn sync_regions(&mut self, moved: bool) {
3556 let point = self.hover.at();
3557 let now: Vec<(WidgetId, Rect)> = match (point, self.ui.as_ref()) {
3558 (Some(point), Some(ui)) => ui
3559 .hover_regions_at(point)
3560 .into_iter()
3561 .map(|(id, rect, _)| (id, rect))
3562 .collect(),
3563 _ => Vec::new(),
3564 };
3565 let at = point.unwrap_or(self.cursor);
3566 let local = |rect: Rect| Point::new(at.x - rect.x, at.y - rect.y);
3567 let before = std::mem::replace(&mut self.entered, now.clone());
3568 let mut events = Vec::new();
3569 for (id, rect) in &before {
3570 if !now.iter().any(|(n, _)| n == id) {
3571 events.push((
3572 *id,
3573 frus_widgets::HoverEvent::Exit {
3574 local: local(*rect),
3575 },
3576 ));
3577 }
3578 }
3579 for (id, rect) in now.iter().rev() {
3580 if !before.iter().any(|(b, _)| b == id) {
3581 events.push((
3582 *id,
3583 frus_widgets::HoverEvent::Enter {
3584 local: local(*rect),
3585 },
3586 ));
3587 } else if moved && !self.pointer_held {
3588 events.push((
3589 *id,
3590 frus_widgets::HoverEvent::Move {
3591 local: local(*rect),
3592 },
3593 ));
3594 }
3595 }
3596 let messages: Vec<_> = events
3597 .into_iter()
3598 .filter_map(|(id, event)| {
3599 self.tree
3600 .as_ref()
3601 .and_then(|tree| find_widget(tree.as_ref(), id))
3602 .and_then(|widget| widget.on_hover_event(event))
3603 })
3604 .collect();
3605 for message in messages {
3606 self.dispatch(message);
3607 self.request_redraw();
3608 }
3609 }
3610
3611 fn region_cursor(&self, requested: Option<UiCursor>) -> UiCursor {
3616 requested
3617 .or_else(|| {
3618 let point = self.hover.at()?;
3619 let (id, _) = self.ui.as_ref()?.focus_hit(point)?;
3620 let widget = find_widget(self.tree.as_ref()?.as_ref(), id)?;
3621 is_text_field(widget).then_some(UiCursor::Text)
3622 })
3623 .or_else(|| {
3624 self.hover.at().and_then(|point| {
3625 self.ui
3626 .as_ref()?
3627 .hover_regions_at(point)
3628 .into_iter()
3629 .find_map(|(_, _, region)| region.cursor)
3630 })
3631 })
3632 .unwrap_or(UiCursor::Default)
3633 }
3634
3635 fn update_cursor_icon(&mut self, hovered: Option<WidgetId>) {
3639 let requested = hovered.and_then(|id| {
3640 let rect = self.ui.as_ref()?.widget_rect(id)?;
3641 let widget = find_widget(self.tree.as_ref()?.as_ref(), id)?;
3642 widget.cursor_icon(
3643 self.cursor.x - rect.x,
3644 self.cursor.y - rect.y,
3645 rect.width,
3646 rect.height,
3647 )
3648 });
3649 let shown = self.region_cursor(requested);
3650 if let Some(window) = &self.window {
3651 window.set_cursor_visible(shown != UiCursor::None);
3652 window.set_cursor(cursor_icon(shown));
3653 }
3654 let hover_cursor = requested.map(|_| self.cursor);
3659 if hover_cursor != self.runtime.input.hover_cursor {
3660 self.runtime.input.hover_cursor = hover_cursor;
3661 self.request_redraw();
3662 }
3663 }
3664
3665 fn pointer_down(&mut self, touch: bool) {
3668 self.pointer_touch = touch;
3669 self.poll_pending_tap(Instant::now());
3672 if let Some(pending) = self.pending_tap.as_ref() {
3673 let here = self.ui.as_ref().and_then(|ui| ui.hit(self.cursor));
3674 let (dx, dy) = (
3675 self.cursor.x - pending.position.x,
3676 self.cursor.y - pending.position.y,
3677 );
3678 if here != Some(pending.id) || dx.hypot(dy) > DOUBLE_TAP_SLOP {
3679 self.flush_pending_tap();
3680 }
3681 }
3682 self.pending_word = None;
3683 self.pending_region_hold = false;
3684 if self
3690 .ui
3691 .as_ref()
3692 .is_some_and(|ui| ui.toolbar_contains(self.cursor))
3693 {
3694 press_at(&mut self.runtime, self.ui.as_ref(), self.cursor);
3695 self.request_redraw();
3696 return;
3697 }
3698 if let Some(drag) = self.grab_region_handle() {
3704 self.drag = Some(drag);
3705 if let Some(selection) = self.runtime.region.as_mut() {
3706 selection.bar = false;
3707 }
3708 self.request_redraw();
3709 return;
3710 }
3711 if let Some(drag) = self.grab_selection_handle() {
3712 self.drag = Some(drag);
3713 self.runtime.selection_toolbar = None;
3716 self.request_redraw();
3717 return;
3718 }
3719 if self.runtime.selection_handles.take().is_some() {
3720 self.request_redraw();
3721 }
3722 self.hide_selection_toolbar();
3723 if self.runtime.region.take().is_some() {
3726 self.request_redraw();
3727 }
3728 let above_the_pages = self.ui.as_ref().is_some_and(|ui| {
3733 ui.over_overlay(self.cursor)
3734 || ui
3735 .title_bar_row()
3736 .is_some_and(|row| row.contains(self.cursor))
3737 });
3738 let on_back_edge = !above_the_pages
3739 && if self.is_rtl() {
3740 self.cursor.x > self.logical_width() - BACK_EDGE
3741 } else {
3742 self.cursor.x < BACK_EDGE
3743 };
3744 if on_back_edge && self.app.can_go_back() {
3745 self.drag = Some(Drag::Back {
3746 start_x: self.cursor.x,
3747 });
3748 self.begin_gesture();
3749 self.build_dirty = true;
3750 self.app.back_gesture(0.0);
3751 self.request_redraw();
3752 return;
3753 }
3754
3755 if let Some(bar) = self.ui.as_ref().and_then(|ui| ui.scrollbar_at(self.cursor)) {
3757 let (along, thumb_start) = if bar.vertical {
3758 (self.cursor.y, bar.thumb.y)
3759 } else {
3760 (self.cursor.x, bar.thumb.x)
3761 };
3762 self.drag = Some(Drag::Scrollbar {
3763 id: bar.id,
3764 vertical: bar.vertical,
3765 grab: along - thumb_start,
3766 track_start: bar.track_start,
3767 track_len: bar.track_len,
3768 thumb_len: bar.thumb_len,
3769 max: bar.max,
3770 reverse: bar.reverse,
3771 });
3772 self.request_redraw();
3773 return;
3774 }
3775
3776 if let Some((id, rect)) = self.ui.as_ref().and_then(|ui| ui.draggable_at(self.cursor)) {
3778 self.drag = Some(Drag::Widget {
3779 id,
3780 rect,
3781 last_x: self.cursor.x,
3782 });
3783 self.dispatch_drag_edge(id, rect, Edge::Start);
3788 self.apply_widget_drag(id, rect, 0.0);
3790 self.request_redraw();
3791 return;
3792 }
3793
3794 if let Some((id, from, hold)) = self.reorderable_at(self.cursor) {
3803 if hold {
3804 self.pending_reorder = Some((id, from, self.cursor));
3805 } else {
3806 self.drag = Some(Drag::Reorder {
3807 id,
3808 from,
3809 start: self.cursor,
3810 moved: false,
3811 carried: None,
3812 });
3813 self.reorder_x = self.cursor.x; self.reorder_y = self.cursor.y; }
3816 }
3817
3818 if press_at(&mut self.runtime, self.ui.as_ref(), self.cursor) {
3819 self.request_redraw();
3820 }
3821 let previously_focused = self.runtime.input.focused;
3823 let focus = self.ui.as_ref().and_then(|ui| ui.focus_hit(self.cursor));
3824 self.runtime.input.focused = focus.map(|(id, _)| id);
3825 if let Some((id, rect)) = focus {
3826 let local_x = self.cursor.x - rect.x;
3827 let local_y =
3830 self.cursor.y - rect.y + self.runtime.scroll.get(&id).map(|s| s.1).unwrap_or(0.0);
3831 let scroll_cursor = if previously_focused == Some(id) {
3834 self.runtime.edits.get(&id).map(|e| e.cursor).unwrap_or(0)
3835 } else {
3836 0
3837 };
3838 let cursor = self
3842 .tree
3843 .as_ref()
3844 .and_then(|tree| find_widget(tree.as_ref(), id))
3845 .and_then(|widget| widget.cursor_at(local_x, local_y, rect.width, scroll_cursor));
3846 if let Some(cursor) = cursor {
3847 self.goal_x = None;
3849 self.runtime.edits.insert(
3850 id,
3851 Edit {
3852 cursor,
3853 anchor: None,
3854 composing: None,
3855 },
3856 );
3857 self.drag = Some(Drag::TextSelect { id, rect });
3858 if touch {
3860 self.pending_word = Some(id);
3861 }
3862 self.runtime.close_edit_run(id);
3865 self.request_soft_input();
3869
3870 let now = Instant::now();
3872 let double = self
3873 .last_click_time
3874 .map(|t| (now - t).as_secs_f32() < 0.4)
3875 .unwrap_or(false);
3876 self.last_click_time = Some(now);
3877 if double {
3878 if let Some((start, end)) = self
3879 .tree
3880 .as_ref()
3881 .and_then(|tree| find_widget(tree.as_ref(), id))
3882 .and_then(|widget| widget.word_at(cursor))
3883 {
3884 self.runtime.edits.insert(
3885 id,
3886 Edit {
3887 cursor: end,
3888 anchor: Some(start),
3889 composing: None,
3890 },
3891 );
3892 self.drag = None;
3893 }
3894 }
3895 }
3896 }
3897
3898 if self.drag.is_none() && focus.is_none() {
3902 if touch {
3903 self.pending_region_hold = self.region_press_target().is_some();
3904 } else {
3905 self.begin_region_selection();
3906 }
3907 }
3908
3909 if touch && self.drag.is_none() {
3913 let landed = {
3918 let scroll = &self.runtime.scroll;
3919 self.ui.as_ref().and_then(|ui| {
3920 ui.scroll_chain(self.cursor).find(|area| {
3921 area.accepts_user_offset(
3922 scroll.get(&area.id).copied().unwrap_or((0.0, 0.0)),
3923 )
3924 })
3925 })
3926 };
3927 if let Some(area) = landed {
3928 let physics = area.physics;
3932 let carried = self.runtime.catch_scroll_fling(area.id, physics);
3933 self.runtime.hold_scroll(area.id);
3935 if let Some(sheet) = self
3939 .ui
3940 .as_ref()
3941 .and_then(|ui| ui.sheet_holding(area.id))
3942 .map(|sheet| sheet.id)
3943 {
3944 self.runtime.sheet_hold(sheet);
3945 }
3946 self.drag = Some(Drag::Scroll {
3947 id: area.id,
3948 last: self.cursor,
3949 moved: false,
3950 carried,
3951 dismiss: self
3952 .ui
3953 .as_ref()
3954 .and_then(|ui| ui.dismissable_at(self.cursor)),
3955 axis: None,
3956 pan: self.pan_target(),
3957 });
3958 self.begin_gesture();
3959 self.gesture_velocity = area.velocity_tracker();
3962 }
3963 }
3964
3965 if self.drag.is_none() {
3968 if let Some((id, available)) = self
3969 .ui
3970 .as_ref()
3971 .and_then(|ui| ui.sheet_at(self.cursor))
3972 .map(|sheet| (sheet.id, sheet.available))
3973 {
3974 self.runtime.sheet_hold(id);
3975 self.drag = Some(Drag::Sheet {
3976 id,
3977 last: self.cursor,
3978 moved: false,
3979 available,
3980 });
3981 self.begin_gesture();
3982 }
3983 }
3984
3985 if self.drag.is_none() {
3989 if let Some(item) = self
3990 .ui
3991 .as_ref()
3992 .and_then(|ui| ui.dismissable_at(self.cursor))
3993 {
3994 self.drag = Some(Drag::Dismiss {
3995 item,
3996 last: self.cursor,
3997 moved: false,
3998 });
3999 self.begin_gesture();
4000 }
4001 }
4002
4003 if self.drag.is_none() {
4009 if let Some(source) = self
4010 .ui
4011 .as_ref()
4012 .and_then(|ui| ui.drag_source_at(self.cursor))
4013 .filter(|source| {
4014 !self
4016 .tree
4017 .as_ref()
4018 .and_then(|tree| find_widget(tree.as_ref(), source.id))
4019 .is_some_and(|widget| widget.drag_needs_long_press())
4020 })
4021 {
4022 self.drag = Some(Drag::Item {
4023 source,
4024 start: self.cursor,
4025 moved: false,
4026 over: None,
4027 });
4028 self.begin_gesture();
4029 }
4030 }
4031
4032 if self.drag.is_none() {
4035 if let Some((id, axis)) = self.pan_target() {
4036 self.drag = Some(Drag::Gesture {
4037 id,
4038 axis,
4039 start: self.cursor,
4040 last: self.cursor,
4041 moved: false,
4042 });
4043 self.begin_gesture();
4044 }
4045 }
4046
4047 if self.drag.is_none() {
4051 if let Some((id, viewport)) = self
4052 .ui
4053 .as_ref()
4054 .and_then(|ui| ui.interactive_at(self.cursor))
4055 {
4056 self.runtime.interactive_velocity.remove(&id);
4058 self.drag = Some(Drag::Pan {
4059 id,
4060 last: self.cursor,
4061 moved: false,
4062 viewport,
4063 });
4064 self.begin_gesture();
4065 }
4066 }
4067 self.request_redraw();
4068 }
4069
4070 fn positional_click_on(&self, id: WidgetId) -> Option<A::Message> {
4076 let rect = self.ui.as_ref()?.widget_rect(id)?;
4077 let widget = find_widget(self.tree.as_ref()?.as_ref(), id)?;
4078 widget.positional_click(
4079 self.cursor.x - rect.x,
4080 self.cursor.y - rect.y,
4081 rect.width,
4082 rect.height,
4083 )
4084 }
4085
4086 fn pointer_up(&mut self) {
4087 let ended = self.drag.take();
4088 if let Some(Drag::Back { .. }) = ended {
4089 let sign = if self.is_rtl() { -1.0 } else { 1.0 };
4092 let velocity = sign * self.gesture_estimate().velocity.x / self.logical_width();
4093 self.build_dirty = true;
4094 self.app.back_gesture_end(velocity);
4095 self.request_redraw();
4096 return;
4097 }
4098 let was_tap = gesture_was_a_tap(ended.as_ref());
4101 if let Some(Drag::Widget { id, rect, .. }) = ended {
4105 self.dispatch_drag_edge(id, rect, Edge::End);
4106 }
4107 #[cfg(android)]
4109 if let Some(Drag::SelectionHandle { id, .. }) = ended {
4110 self.push_ime_context(id);
4111 }
4112 if let Some(Drag::SelectionHandle { id, .. }) = ended {
4113 self.show_selection_toolbar(id);
4114 }
4115 if let Some(Drag::RegionHandle { .. }) = ended {
4117 if let Some(selection) = self.runtime.region.as_mut() {
4118 selection.bar = true;
4119 }
4120 }
4121 if let Some(Drag::Reorder {
4124 id,
4125 from,
4126 moved: true,
4127 carried,
4128 ..
4129 }) = &ended
4130 {
4131 let target = self.reorder_drop_target(*id, *from, *carried);
4132 let tree = self.tree.as_ref();
4133 let base = target
4134 .and_then(|tid| tree.and_then(|t| find_widget(t.as_ref(), tid)))
4135 .and_then(|widget| widget.reorder_index());
4136 let to = match (base, target) {
4141 (Some(base), Some(tid)) => {
4142 let after = self
4143 .ui
4144 .as_ref()
4145 .and_then(|ui| ui.widget_rect(tid))
4146 .map(|rect| self.reorder_insert_after(tid, rect))
4147 .unwrap_or(false);
4148 Some(base + after as usize)
4149 }
4150 _ => None,
4151 };
4152 let self_drop = target == Some(*id);
4156 let message = match to {
4157 Some(to) if to != *from && !self_drop => tree
4158 .and_then(|t| find_widget(t.as_ref(), *id))
4159 .and_then(|widget| widget.on_reorder(to)),
4160 _ => None,
4161 };
4162 if let Some(message) = message {
4163 let to = to.unwrap_or(*from);
4164 let widget = tree.and_then(|t| find_widget(t.as_ref(), *id));
4165 let axis = widget
4166 .map(|w| w.reorder_axis())
4167 .unwrap_or(ReorderAxis::Horizontal);
4168 let announcement = widget
4174 .and_then(|w| w.reorder_announcement(to))
4175 .unwrap_or_else(|| match axis {
4176 ReorderAxis::Horizontal => format!("Column moved to position {}", to + 1),
4177 ReorderAxis::Vertical => "Card moved".to_string(),
4178 });
4179 self.dispatch(message);
4180 self.set_announcement(announcement);
4181 }
4182 }
4183 if let Some(Drag::Dismiss { item, moved, .. }) = &ended {
4189 if *moved {
4190 let estimate = self.gesture_estimate();
4191 let horizontal = item.spec.axis.is_horizontal();
4192 let (along, across) = if horizontal {
4193 (estimate.velocity.x, estimate.velocity.y)
4194 } else {
4195 (estimate.velocity.y, estimate.velocity.x)
4196 };
4197 self.runtime.dismiss_release(
4198 item.id,
4199 along,
4200 across,
4201 item.spec.axis,
4202 item.spec.threshold,
4203 );
4204 self.request_redraw();
4205 return;
4206 }
4207 }
4208 if let Some(Drag::Sheet {
4211 id,
4212 moved,
4213 available,
4214 ..
4215 }) = &ended
4216 {
4217 let velocity = if *moved {
4218 -self.fling_velocity(self.gesture_estimate()).1
4219 } else {
4220 0.0
4221 };
4222 let sheet = self.ui.as_ref().and_then(|ui| ui.sheet(*id)).map(|sheet| {
4225 let list = match sheet.areas.as_slice() {
4226 [only] => Some(*only),
4227 _ => None,
4228 };
4229 (sheet.spec.clone(), list)
4230 });
4231 if let Some((spec, list)) = sheet {
4232 self.runtime
4233 .sheet_release(*id, &spec, *available, velocity, list);
4234 }
4235 if *moved {
4236 self.request_redraw();
4237 return;
4238 }
4239 }
4240 if let Some(Drag::Item {
4241 source,
4242 moved: true,
4243 over,
4244 ..
4245 }) = &ended
4246 {
4247 self.runtime.drag_over = None;
4251 let accepted = over.is_some();
4252 let dropped = over.and_then(|id| {
4253 self.tree
4254 .as_ref()
4255 .and_then(|tree| find_widget(tree.as_ref(), id))
4256 .and_then(|widget| widget.on_drop(source.payload))
4257 });
4258 let ended_msg = self
4259 .tree
4260 .as_ref()
4261 .and_then(|tree| find_widget(tree.as_ref(), source.id))
4262 .and_then(|widget| widget.on_dropped(accepted));
4263 for message in dropped.into_iter().chain(ended_msg) {
4264 self.dispatch(message);
4265 }
4266 self.request_redraw();
4267 return;
4268 }
4269 if let Some(Drag::Item { .. }) = &ended {
4270 self.runtime.drag_over = None;
4273 }
4274 let mut sheet_took = false;
4277 if let Some(Drag::Scroll { id, .. }) = &ended {
4278 let sheet = self
4279 .ui
4280 .as_ref()
4281 .and_then(|ui| ui.sheet_holding(*id))
4282 .cloned();
4283 if let Some(sheet) = sheet {
4284 let velocity = match &ended {
4287 Some(Drag::Scroll {
4288 moved: true,
4289 axis: Some(true),
4290 carried,
4291 ..
4292 }) => -self.fling_velocity(self.gesture_estimate()).1 + carried.1,
4293 _ => 0.0,
4294 };
4295 let offset = self.runtime.scroll.get(id).map_or(0.0, |o| o.1);
4296 let size = self.runtime.sheet_size(sheet.id, &sheet.spec);
4297 sheet_took = velocity != 0.0
4298 && frus_widgets::sheet_takes_release(
4299 velocity,
4300 offset,
4301 size,
4302 &sheet.spec,
4303 sheet.available,
4304 );
4305 self.runtime.sheet_release(
4308 sheet.id,
4309 &sheet.spec,
4310 sheet.available,
4311 if sheet_took { velocity } else { 0.0 },
4312 Some(*id),
4313 );
4314 }
4315 }
4316 if let Some(Drag::Scroll { id, .. }) = &ended {
4317 self.runtime.release_scroll(*id);
4319 self.runtime.glow_scroll_end(*id);
4321 self.release_refresh(*id);
4325 }
4326 if let Some(Drag::Scroll {
4327 id,
4328 moved: true,
4329 carried,
4330 axis,
4331 ..
4332 }) = &ended
4333 {
4334 let estimate = self.gesture_estimate();
4338 let velocity = self.fling_velocity(estimate);
4339 let velocity = match self.ui.as_ref().and_then(|u| u.scroll_region(*id)) {
4342 Some(area) => area.offset_delta(velocity),
4343 None => (-velocity.0, -velocity.1),
4344 };
4345 let launch = (velocity.0 + carried.0, velocity.1 + carried.1);
4349 if sheet_took {
4350 self.request_redraw();
4351 } else {
4352 self.fling(
4353 *id,
4354 match axis {
4355 Some(true) => (0.0, launch.1),
4356 Some(false) => (launch.0, 0.0),
4357 None => launch,
4358 },
4359 );
4360 }
4361 }
4362 if let Some(Drag::Gesture {
4365 id,
4366 axis,
4367 moved: true,
4368 ..
4369 }) = &ended
4370 {
4371 let velocity = self.fling_velocity(self.gesture_estimate());
4372 let velocity = mask_pan(*axis, Point::new(velocity.0, velocity.1));
4373 let speed = velocity.x.hypot(velocity.y);
4375 let velocity = if speed > MAX_PAN_VELOCITY {
4376 let k = MAX_PAN_VELOCITY / speed;
4377 Point::new(velocity.x * k, velocity.y * k)
4378 } else {
4379 velocity
4380 };
4381 self.send_pan(*id, frus_widgets::PanEvent::End { velocity });
4382 }
4383 if let Some(Drag::Pan {
4386 id, moved: true, ..
4387 }) = &ended
4388 {
4389 let estimate = self.gesture_estimate();
4390 let velocity = self.fling_velocity(estimate);
4391 if velocity.0.hypot(velocity.1) > PAN_FLING_MIN {
4392 self.runtime.interactive_velocity.insert(*id, velocity);
4393 }
4394 }
4395 if ended.is_some() && !was_tap {
4396 self.request_redraw();
4397 return;
4398 }
4399 let released = self.ui.as_ref().and_then(|ui| ui.hit(self.cursor));
4401 let bar_action = match (self.runtime.input.pressed, released) {
4404 (Some(pressed), Some(released)) if pressed == released => {
4405 self.ui.as_ref().and_then(|ui| ui.edit_action_for(pressed))
4406 }
4407 _ => None,
4408 };
4409 let on_bar = self
4410 .ui
4411 .as_ref()
4412 .is_some_and(|ui| ui.toolbar_contains(self.cursor));
4413 let field_positional = if bar_action.is_some() {
4418 None
4419 } else {
4420 self.ui
4421 .as_ref()
4422 .and_then(|ui| ui.focus_hit(self.cursor))
4423 .map(|(id, _)| id)
4424 .filter(|id| self.runtime.input.focused == Some(*id))
4425 .and_then(|id| self.positional_click_on(id))
4426 };
4427 let (message, announce) = match (self.runtime.input.pressed, released) {
4428 (Some(_), Some(_)) if bar_action.is_some() => (None, None),
4429 _ if field_positional.is_some() => (field_positional, None),
4430 (Some(pressed), Some(released)) if pressed == released => {
4431 let positional = self.positional_click_on(released);
4434 let message =
4435 positional.or_else(|| self.ui.as_ref().and_then(|ui| ui.msg_for(pressed)));
4436 let announce = self
4439 .tree
4440 .as_ref()
4441 .and_then(|tree| find_widget(tree.as_ref(), pressed))
4442 .and_then(|widget| widget.announce());
4443 (message, announce)
4444 }
4445 _ => (None, None),
4446 };
4447 let message = match (self.runtime.input.pressed, released) {
4449 (Some(pressed), Some(released)) if pressed == released && bar_action.is_none() => {
4450 self.double_tap(pressed, message)
4451 }
4452 _ => message,
4453 };
4454 if let (Some(pressed), Some(released)) = (self.runtime.input.pressed, released) {
4458 if pressed == released {
4459 self.runtime.ink_confirm(pressed);
4460 }
4461 }
4462 self.runtime.input.pressed = None;
4463 if let Some(action) = bar_action {
4464 self.perform_edit_action(action);
4465 } else if on_bar && message.is_some() {
4466 self.runtime.selection_handles = None;
4469 self.hide_selection_toolbar();
4470 }
4471 if let Some(message) = message {
4472 self.dispatch(message);
4473 if let Some(announce) = announce {
4474 self.set_announcement(announce);
4475 }
4476 }
4477 self.request_redraw();
4478 }
4479
4480 fn double_tap(&mut self, id: WidgetId, message: Option<A::Message>) -> Option<A::Message> {
4484 let Some(double) = self
4485 .tree
4486 .as_ref()
4487 .and_then(|tree| find_widget(tree.as_ref(), id))
4488 .and_then(|widget| widget.on_double_tap())
4489 else {
4490 return message;
4491 };
4492 let now = Instant::now();
4493 let second = self.pending_tap.as_ref().is_some_and(|pending| {
4494 let (dx, dy) = (
4495 self.cursor.x - pending.position.x,
4496 self.cursor.y - pending.position.y,
4497 );
4498 pending.id == id
4499 && now.duration_since(pending.at) <= DOUBLE_TAP_TIMEOUT
4500 && dx.hypot(dy) <= DOUBLE_TAP_SLOP
4501 });
4502 if second {
4503 self.pending_tap = None;
4504 return Some(double);
4505 }
4506 self.flush_pending_tap();
4507 self.pending_tap = Some(PendingTap {
4508 id,
4509 at: now,
4510 position: self.cursor,
4511 single: message,
4512 });
4513 None
4514 }
4515
4516 fn flush_pending_tap(&mut self) {
4518 if let Some(message) = self.pending_tap.take().and_then(|pending| pending.single) {
4519 self.dispatch(message);
4520 self.request_redraw();
4521 }
4522 }
4523
4524 fn poll_pending_tap(&mut self, now: Instant) -> bool {
4526 let due = self
4527 .pending_tap
4528 .as_ref()
4529 .is_some_and(|pending| now.duration_since(pending.at) >= DOUBLE_TAP_TIMEOUT);
4530 if due {
4531 self.flush_pending_tap();
4532 }
4533 due
4534 }
4535
4536 #[cfg(any(desktop, web))]
4541 fn set_announcement(&mut self, message: String) {
4542 self.announce = message;
4543 }
4544 #[cfg(not(any(desktop, web)))]
4545 fn set_announcement(&mut self, _message: String) {}
4546
4547 fn reconcile_focus(&mut self) {
4554 let present: std::collections::HashSet<WidgetId> = match self.ui.as_ref() {
4555 Some(ui) => ui.focusable_ids().collect(),
4556 None => return,
4557 };
4558 let autofocus: Vec<WidgetId> = self
4562 .ui
4563 .as_ref()
4564 .map(|ui| ui.autofocus_ids().collect())
4565 .unwrap_or_default();
4566 let appeared = autofocus
4567 .iter()
4568 .find(|id| !self.autofocused.contains(id))
4569 .copied();
4570 self.autofocused = autofocus.into_iter().collect();
4571 if let Some(id) = appeared {
4572 self.runtime.input.focused = Some(id);
4573 }
4574 let before = self.runtime.input.focused;
4575 let after = resolve_focus(
4576 before,
4577 &present,
4578 &mut self.focus_history,
4579 &mut self.prev_focus,
4580 );
4581 if after != before || appeared.is_some() {
4582 self.runtime.input.focused = after;
4583 self.request_redraw();
4584 }
4585 }
4586
4587 fn dispatch(&mut self, message: A::Message) {
4590 self.build_dirty = true;
4592 let command = self.app.update(message);
4593 self.run_command(command);
4594 let asked = self.app.effects();
4595 self.run_command(asked);
4596 #[cfg(web)]
4597 self.sync_address();
4598 self.sync_subscriptions();
4599 }
4600
4601 #[cfg(any(android, desktop))]
4604 fn open_launch_link(&mut self) {
4605 #[cfg(android)]
4606 let link = self
4607 .android_app
4608 .as_ref()
4609 .and_then(crate::android_link::launch_link);
4610 #[cfg(desktop)]
4611 let link = crate::link::link_among(std::env::args());
4612 if let Some(location) = link.as_deref().and_then(crate::link::location_of_link) {
4613 let command = self.app.open_location(&location);
4614 self.run_command(command);
4615 }
4616 }
4617
4618 #[cfg(desktop)]
4620 fn open_links_from_other_instances(&mut self) {
4621 for link in crate::instance::take_links() {
4622 if let Some(location) = crate::link::location_of_link(&link) {
4623 let command = self.app.open_location(&location);
4624 self.run_command(command);
4625 self.build_dirty = true;
4626 self.request_redraw();
4627 }
4628 }
4629 }
4630
4631 #[cfg(web)]
4634 fn open_launch_address(&mut self, window: &Arc<Window>) {
4635 let strategy = self.app.location_strategy();
4636 let base = crate::address::base();
4637 let (location, index) = crate::address::current(strategy, &base);
4638 self.history.opened(location.clone(), index);
4639 crate::address::listen(window.clone(), strategy, base.clone());
4640 self.address = (strategy, base);
4641 if let Some(location) = location {
4642 let command = self.app.open_location(&location);
4643 self.run_command(command);
4644 let (strategy, base) = &self.address;
4646 for (step, index) in self.history.seed(&self.app.location_stack()) {
4647 crate::address::apply(step, *strategy, base, index);
4648 }
4649 }
4650 }
4651
4652 #[cfg(web)]
4655 fn sync_address(&mut self) {
4656 if !self.started {
4657 return;
4658 }
4659 for (location, state) in crate::address::take_popped() {
4660 if self.history.popped(location.clone(), state) {
4661 let command = self.app.open_location(&location);
4662 self.run_command(command);
4663 self.build_dirty = true;
4664 self.request_redraw();
4667 }
4668 }
4669 if let Some(location) = self.app.location() {
4670 if let Some(step) = self
4671 .history
4672 .reflect(&location, self.app.location_replaces())
4673 {
4674 let (strategy, base) = &self.address;
4675 crate::address::apply(step, *strategy, base, self.history.index());
4676 }
4677 }
4678 }
4679
4680 fn run_command(&mut self, command: crate::command::Command<A::Message>) {
4697 let parts = command.into_parts();
4698 self.pending_focus.extend(parts.focus);
4699 self.pending_scroll.extend(parts.scrolls);
4700 self.pending_sheet.extend(parts.sheets);
4701 for task in parts.tasks {
4702 let proxy = self.proxy.clone();
4703 #[cfg(not(web))]
4704 std::thread::spawn(move || {
4705 if let Some(message) = task() {
4706 let _ = proxy.send_event(message);
4707 }
4708 });
4709 #[cfg(web)]
4710 wasm_bindgen_futures::spawn_local(async move {
4711 if let Some(message) = task() {
4712 let _ = proxy.send_event(message);
4713 }
4714 });
4715 }
4716 for future in parts.async_tasks {
4717 let proxy = self.proxy.clone();
4718 #[cfg(not(web))]
4722 crate::runtime::spawn(async move {
4723 if let Some(message) = future.await {
4724 let _ = proxy.send_event(message);
4725 }
4726 })
4727 .detach();
4728 #[cfg(web)]
4731 wasm_bindgen_futures::spawn_local(async move {
4732 if let Some(message) = future.await {
4733 let _ = proxy.send_event(message);
4734 }
4735 });
4736 }
4737 for (delay, message) in parts.timers {
4738 let proxy = self.proxy.clone();
4739 #[cfg(not(web))]
4740 crate::runtime::spawn(async move {
4741 crate::runtime::sleep(delay).await;
4742 let _ = proxy.send_event(message);
4743 })
4744 .detach();
4745 #[cfg(web)]
4746 web_timer::after(delay.as_millis() as i32, move || {
4747 let _ = proxy.send_event(message);
4748 });
4749 }
4750 }
4751
4752 fn sync_subscriptions(&mut self) {
4755 let entries = self.app.subscription().into_entries();
4756 let declared: std::collections::HashSet<u64> = entries.iter().map(|e| e.id).collect();
4757
4758 self.running_subs.retain(|id, _| declared.contains(id));
4760
4761 for entry in entries {
4763 if self.running_subs.contains_key(&entry.id) {
4764 continue;
4765 }
4766 let handle = self.start_subscription(entry.kind);
4767 self.running_subs.insert(entry.id, handle);
4768 }
4769 }
4770
4771 #[cfg(not(web))]
4780 fn start_subscription(&self, kind: crate::subscription::Kind<A::Message>) -> SubHandle {
4781 let proxy = self.proxy.clone();
4782 match kind {
4783 crate::subscription::Kind::Every { interval, make } => {
4784 crate::runtime::spawn(async move {
4785 loop {
4786 crate::runtime::sleep(interval).await;
4787 if proxy.send_event(make(Instant::now())).is_err() {
4790 break;
4791 }
4792 }
4793 })
4794 }
4795 }
4796 }
4797
4798 #[cfg(web)]
4802 fn start_subscription(&self, kind: crate::subscription::Kind<A::Message>) -> SubHandle {
4803 let proxy = self.proxy.clone();
4804 match kind {
4805 crate::subscription::Kind::Every { interval, make } => {
4806 let ms = interval.as_millis() as i32;
4807 web_timer::Interval::new(ms, move || {
4808 let _ = proxy.send_event(make(Instant::now()));
4809 })
4810 }
4811 }
4812 }
4813
4814 fn handle_drag(&mut self) {
4816 let Some(mut drag) = self.drag.take() else {
4817 return;
4818 };
4819 let slop = self.hit_slop();
4821 match &mut drag {
4822 Drag::Scrollbar {
4823 id,
4824 vertical,
4825 grab,
4826 track_start,
4827 track_len,
4828 thumb_len,
4829 max,
4830 reverse,
4831 } => {
4832 let along = if *vertical {
4833 self.cursor.y
4834 } else {
4835 self.cursor.x
4836 };
4837 let travel = (*track_len - *thumb_len).max(1.0);
4838 let thumb_start = (along - *grab).clamp(*track_start, *track_start + travel);
4839 let fraction = (thumb_start - *track_start) / travel;
4840 let fraction = if *reverse { 1.0 - fraction } else { fraction };
4843 let offset = (fraction * *max).clamp(0.0, *max);
4844 let entry = self.runtime.scroll.entry(*id).or_insert((0.0, 0.0));
4845 if *vertical {
4846 entry.1 = offset;
4847 } else {
4848 entry.0 = offset;
4849 }
4850 let synced = *entry;
4852 self.runtime.scroll_target.insert(*id, synced);
4853 self.runtime.scroll_velocity.remove(&*id);
4854 }
4855 Drag::RegionSelect { area } => {
4856 let area = *area;
4857 self.extend_region_selection(area);
4858 }
4859 Drag::RegionHandle { area, handle, grab } => {
4860 let (area, handle, grab) = (*area, *handle, *grab);
4861 self.drag_region_handle(area, handle, grab);
4862 }
4863 Drag::TextSelect { id, rect } => {
4864 let local_x = self.cursor.x - rect.x;
4865 let local_y = self.cursor.y - rect.y
4866 + self.runtime.scroll.get(id).map(|s| s.1).unwrap_or(0.0);
4867 let scroll_cursor = self.runtime.edits.get(id).map(|e| e.cursor).unwrap_or(0);
4869 let cursor = self
4870 .tree
4871 .as_ref()
4872 .and_then(|tree| find_widget(tree.as_ref(), *id))
4873 .and_then(|widget| {
4874 widget.cursor_at(local_x, local_y, rect.width, scroll_cursor)
4875 });
4876 if let Some(cursor) = cursor {
4877 let edit = self.runtime.edits.entry(*id).or_default();
4878 if edit.anchor.is_none() {
4879 edit.anchor = Some(edit.cursor);
4880 }
4881 edit.cursor = cursor;
4882 }
4883 }
4884 Drag::SelectionHandle {
4885 id,
4886 rect,
4887 handle,
4888 grab,
4889 } => {
4890 let local_x = self.cursor.x - rect.x + grab.x;
4893 let local_y = self.cursor.y - rect.y
4894 + grab.y
4895 + self.runtime.scroll.get(id).map(|s| s.1).unwrap_or(0.0);
4896 let edit = self.runtime.edits.get(id).copied().unwrap_or_default();
4897 let moved = self
4898 .tree
4899 .as_ref()
4900 .and_then(|tree| find_widget(tree.as_ref(), *id))
4901 .and_then(|widget| widget.cursor_at(local_x, local_y, rect.width, edit.cursor))
4902 .and_then(|to| crate::selection::drag(edit, *handle, to));
4903 if let Some(moved) = moved {
4904 self.runtime.edits.insert(*id, moved);
4905 }
4906 }
4907 Drag::Widget { id, rect, last_x } => {
4908 let dx = self.cursor.x - *last_x;
4909 *last_x = self.cursor.x;
4910 self.apply_widget_drag(*id, *rect, dx);
4911 }
4912 Drag::Reorder { start, moved, .. } => {
4913 let dx = self.cursor.x - start.x;
4915 let dy = self.cursor.y - start.y;
4916 if !*moved && (dx * dx + dy * dy) > slop * slop {
4917 *moved = true;
4918 }
4919 if *moved {
4921 self.request_redraw();
4922 }
4923 }
4924 Drag::Gesture {
4925 id,
4926 axis,
4927 start,
4928 last,
4929 moved,
4930 } => {
4931 let (id, axis) = (*id, *axis);
4932 if !*moved {
4933 let (tx, ty) = (self.cursor.x - start.x, self.cursor.y - start.y);
4937 let travelled = match axis {
4938 frus_widgets::PanAxis::Free => tx.hypot(ty),
4939 frus_widgets::PanAxis::Horizontal => tx.abs(),
4940 frus_widgets::PanAxis::Vertical => ty.abs(),
4941 };
4942 if travelled > slop {
4943 *moved = true;
4944 let local = self.pan_local(id, *start);
4945 self.send_pan(id, frus_widgets::PanEvent::Start { local });
4946 }
4947 }
4948 if *moved {
4949 let delta = mask_pan(
4950 axis,
4951 Point::new(self.cursor.x - last.x, self.cursor.y - last.y),
4952 );
4953 *last = self.cursor;
4954 self.track_gesture();
4955 if delta.x != 0.0 || delta.y != 0.0 {
4956 let local = self.pan_local(id, self.cursor);
4957 self.send_pan(id, frus_widgets::PanEvent::Update { local, delta });
4958 }
4959 }
4960 }
4961 Drag::Pan {
4962 id,
4963 last,
4964 moved,
4965 viewport,
4966 } => {
4967 let dx = self.cursor.x - last.x;
4968 let dy = self.cursor.y - last.y;
4969 if !*moved && (dx * dx + dy * dy) > slop * slop {
4970 *moved = true;
4971 }
4972 if *moved {
4973 let view = self.runtime.interactive.entry(*id).or_default();
4974 *view = view.pan(dx, dy).clamped(*viewport);
4976 *last = self.cursor;
4977 self.track_gesture();
4978 }
4979 }
4980 Drag::Item {
4981 source,
4982 start,
4983 moved,
4984 over,
4985 } => {
4986 let dx = self.cursor.x - start.x;
4987 let dy = self.cursor.y - start.y;
4988 if !*moved && (dx * dx + dy * dy) > slop * slop {
4991 *moved = true;
4992 }
4993 if !*moved {
4994 return;
4995 }
4996 let payload = source.payload;
5000 let candidate = self
5001 .ui
5002 .as_ref()
5003 .and_then(|ui| ui.drop_zone_at(self.cursor))
5004 .filter(|zone| {
5005 self.tree
5006 .as_ref()
5007 .and_then(|tree| find_widget(tree.as_ref(), zone.id))
5008 .is_some_and(|widget| widget.accepts_drag(payload))
5009 })
5010 .map(|zone| zone.id);
5011 if *over != candidate {
5012 *over = candidate;
5013 self.runtime.drag_over = candidate;
5014 }
5015 self.request_redraw();
5016 }
5017 Drag::Scroll {
5018 id,
5019 last,
5020 moved,
5021 carried,
5022 dismiss,
5023 axis,
5024 pan,
5025 } => {
5026 let dx = self.cursor.x - last.x;
5027 let dy = self.cursor.y - last.y;
5028 if !*moved && (dx * dx + dy * dy) > slop * slop {
5030 *moved = true;
5031 if let Some((target, pan_axis)) = pan.take() {
5042 let down = dy.abs() >= dx.abs();
5043 let wins = match pan_axis {
5044 frus_widgets::PanAxis::Horizontal => !down,
5045 frus_widgets::PanAxis::Vertical => down,
5046 frus_widgets::PanAxis::Free => {
5047 let chain: Vec<_> = self
5048 .ui
5049 .as_ref()
5050 .map(|ui| ui.scroll_chain(*last).collect())
5051 .unwrap_or_default();
5052 !chain.iter().any(|a| a.takes_user_scroll(down))
5055 }
5056 };
5057 if wins {
5058 self.runtime.release_scroll(*id);
5060 let start = *last;
5061 self.drag = Some(Drag::Gesture {
5062 id: target,
5063 axis: pan_axis,
5064 start,
5065 last: start,
5066 moved: false,
5067 });
5068 self.handle_drag();
5069 return;
5070 }
5071 }
5072 if let Some(item) = dismiss.take() {
5073 let along = if item.spec.axis.is_horizontal() {
5074 dx.abs() > dy.abs()
5075 } else {
5076 dy.abs() > dx.abs()
5077 };
5078 if along {
5079 self.runtime.release_scroll(*id);
5081 self.drag = Some(Drag::Dismiss {
5082 item,
5083 last: *last,
5084 moved: true,
5085 });
5086 self.handle_drag();
5087 return;
5088 }
5089 }
5090 let chain: Vec<_> = self
5100 .ui
5101 .as_ref()
5102 .map(|ui| ui.scroll_chain(*last).collect())
5103 .unwrap_or_default();
5104 let down = dy.abs() >= dx.abs();
5105 if let Some(area) = claim_area(&chain, *id, down) {
5106 if area.id != *id {
5107 self.runtime.release_scroll(*id);
5111 let physics = area.physics;
5112 *carried = self.runtime.catch_scroll_fling(area.id, physics);
5113 self.runtime.hold_scroll(area.id);
5114 *id = area.id;
5115 }
5116 *axis = Some(claim_axis(
5117 dx,
5118 dy,
5119 area.takes_user_scroll(false),
5120 area.takes_user_scroll(true),
5121 ));
5122 } else {
5123 *axis = Some(down);
5124 }
5125 }
5126 let (dx, mut dy) = match *axis {
5129 Some(true) => (0.0, dy),
5130 Some(false) => (dx, 0.0),
5131 None => (dx, dy),
5132 };
5133 if *moved && *axis == Some(true) {
5134 let sheet = self
5140 .ui
5141 .as_ref()
5142 .filter(|ui| ui.scroll_region(*id).is_some_and(|a| !a.reverse_y))
5143 .and_then(|ui| ui.sheet_holding(*id))
5144 .filter(|sheet| sheet.available > 0.0)
5145 .cloned();
5146 if let Some(sheet) = sheet {
5147 let px = sheet.available;
5148 let offset = self.runtime.scroll.get(id).map_or(0.0, |o| o.1);
5149 let size = self.runtime.sheet_size(sheet.id, &sheet.spec) * px;
5150 let (grown, listed) = frus_widgets::split_sheet_drag(
5151 -dy,
5152 offset,
5153 size,
5154 sheet.spec.floor() * px,
5155 sheet.spec.max * px,
5156 );
5157 if grown != 0.0 {
5158 self.runtime
5159 .sheet_drag(sheet.id, &sheet.spec, grown / px, px);
5160 }
5161 dy = -listed;
5162 }
5163 }
5164 if *moved {
5165 let area = self.ui.as_ref().and_then(|u| u.scroll_region(*id));
5166 let physics = area.map(|a| a.physics).unwrap_or_else(|| {
5168 self.app
5169 .scroll_behavior()
5170 .scroll_physics(&self.themes.displayed(&self.app))
5171 });
5172 let cur = self.runtime.scroll.get(id).copied().unwrap_or((0.0, 0.0));
5173 let axis = |metrics: frus_widgets::ScrollMetrics, delta: f32| {
5180 let applied = physics.apply_user_offset(metrics, delta);
5181 let proposed = metrics.pixels + applied;
5182 let refused = physics.apply_boundary_conditions(metrics, proposed);
5183 (proposed - refused, refused)
5184 };
5185 let (nx, ny, refused) = match area {
5186 Some(area) => {
5187 let (ddx, ddy) = area.offset_delta((dx, dy));
5188 let (nx, rx) = axis(area.metrics_x(cur.0), ddx);
5189 let (ny, ry) = axis(area.metrics_y(cur.1), ddy);
5190 (nx, ny, Some((area, rx, ry)))
5191 }
5192 None => (cur.0 - dx, cur.1 - dy, None),
5193 };
5194 if let Some((area, rx, ry)) = refused {
5195 if let Some(host) = area.refresh {
5201 self.feed_refresh(host, physics, area.viewport.height, cur.1, ny, ry);
5202 }
5203 let cursor = self.cursor;
5204 for (refused, vertical, extent) in [
5205 (rx, false, area.viewport.width),
5206 (ry, true, area.viewport.height),
5207 ] {
5208 if refused.abs() < 1e-3 {
5209 continue;
5210 }
5211 let edge = area.refused_edge(vertical, refused);
5212 if area.refresh.is_some() && edge == area.start_edge(vertical) {
5215 continue;
5216 }
5217 let (offset, cross) =
5218 frus_widgets::glow_cross_axis(area.viewport, edge, cursor);
5219 self.runtime.glow_pull(
5220 area.id,
5221 area.overscroll,
5222 edge,
5223 refused,
5224 extent,
5225 offset,
5226 cross,
5227 );
5228 }
5229 if rx.abs() < 1e-3 && ry.abs() < 1e-3 && (nx, ny) != cur {
5232 self.runtime.stretch_release(area.id);
5233 }
5234 }
5235 self.runtime.scroll.insert(*id, (nx, ny));
5236 self.runtime.scroll_target.insert(*id, (nx, ny));
5237 self.runtime.scroll_velocity.remove(id);
5238 *last = self.cursor;
5239 self.track_gesture();
5240 }
5241 }
5242 Drag::Dismiss { item, last, moved } => {
5243 let dx = self.cursor.x - last.x;
5244 let dy = self.cursor.y - last.y;
5245 if !*moved && (dx * dx + dy * dy) > slop * slop {
5246 *moved = true;
5247 }
5248 if *moved {
5249 let delta = if item.spec.axis.is_horizontal() {
5250 dx
5251 } else {
5252 dy
5253 };
5254 self.runtime
5255 .dismiss_drag(item.id, delta, item.extent(), item.spec.axis);
5256 *last = self.cursor;
5257 self.track_gesture();
5258 }
5259 }
5260 Drag::Sheet {
5261 id,
5262 last,
5263 moved,
5264 available,
5265 } => {
5266 let dx = self.cursor.x - last.x;
5267 let dy = self.cursor.y - last.y;
5268 if !*moved && (dx * dx + dy * dy) > slop * slop {
5269 *moved = true;
5270 }
5271 if *moved && *available > 0.0 {
5272 let spec = self
5273 .ui
5274 .as_ref()
5275 .and_then(|ui| ui.sheet(*id))
5276 .map(|sheet| sheet.spec.clone());
5277 if let Some(spec) = spec {
5278 self.runtime
5280 .sheet_drag(*id, &spec, -dy / *available, *available);
5281 }
5282 *last = self.cursor;
5283 self.track_gesture();
5284 }
5285 }
5286 Drag::Back { start_x } => {
5287 let width = self.logical_width();
5288 let sign = if self.is_rtl() { -1.0 } else { 1.0 };
5291 let progress = (sign * (self.cursor.x - *start_x) / width).clamp(0.0, 1.0);
5292 self.track_gesture();
5293 self.build_dirty = true;
5294 self.app.back_gesture(progress);
5295 }
5296 }
5297 self.drag = Some(drag);
5298 self.request_redraw();
5299 }
5300
5301 fn refresh_host_of(&self, scrollable: WidgetId) -> Option<WidgetId> {
5303 self.ui
5304 .as_ref()
5305 .and_then(|ui| ui.scroll_region(scrollable))
5306 .and_then(|area| area.refresh)
5307 }
5308
5309 fn release_refresh(&mut self, scrollable: WidgetId) {
5312 let Some(host) = self.refresh_host_of(scrollable) else {
5313 return;
5314 };
5315 if !self.runtime.refresh_release(host) {
5316 return;
5317 }
5318 let message = self
5319 .tree
5320 .as_ref()
5321 .and_then(|tree| find_widget(tree.as_ref(), host))
5322 .and_then(|widget| widget.on_refresh());
5323 if let Some(message) = message {
5324 self.dispatch(message);
5325 }
5326 }
5327
5328 fn feed_refresh(
5347 &mut self,
5348 host: WidgetId,
5349 physics: frus_widgets::ScrollPhysics,
5350 extent: f32,
5351 before: f32,
5352 after: f32,
5353 refused_y: f32,
5354 ) {
5355 if after > 0.5 {
5356 self.runtime.refresh_cancel(host);
5357 return;
5358 }
5359 let pulled = if physics.allows_overscroll() {
5360 (-after).max(0.0) - (-before).max(0.0)
5361 } else {
5362 -refused_y
5363 };
5364 if pulled.abs() > 1e-3 {
5365 self.runtime.refresh_pull(host, pulled, extent);
5366 }
5367 }
5368
5369 fn hit_slop(&self) -> f32 {
5372 if self.pointer_touch {
5373 TOUCH_SLOP
5374 } else {
5375 PRECISE_SLOP
5376 }
5377 }
5378
5379 fn gesture_now(&self) -> f32 {
5382 match self.gesture_clock {
5383 Some((now, start)) => now - start,
5385 None => (Instant::now() - self.gesture_start).as_secs_f32(),
5386 }
5387 }
5388
5389 fn begin_gesture(&mut self) {
5395 self.gesture_velocity = self
5396 .app
5397 .scroll_behavior()
5398 .velocity_tracker(&self.themes.displayed(&self.app));
5399 self.gesture_start = Instant::now();
5400 if let Some((now, start)) = self.gesture_clock.as_mut() {
5401 *start = *now;
5402 }
5403 self.track_gesture();
5404 }
5405
5406 fn track_gesture(&mut self) {
5408 let now = self.gesture_now();
5409 self.gesture_velocity.add_position(now, self.cursor);
5410 }
5411
5412 fn gesture_estimate(&self) -> VelocityEstimate {
5414 self.gesture_velocity
5415 .estimate(self.gesture_now())
5416 .unwrap_or(VelocityEstimate::STILL)
5417 }
5418
5419 fn fling_velocity(&self, estimate: VelocityEstimate) -> (f32, f32) {
5421 fling_velocity(estimate, self.hit_slop())
5422 }
5423
5424 fn fling(&mut self, id: WidgetId, velocity: (f32, f32)) {
5428 let Some(area) = self.ui.as_ref().and_then(|ui| ui.scroll_region(id)) else {
5429 return;
5430 };
5431 let physics = area.physics;
5432 if self.runtime.fling_scroll(area, physics, velocity) {
5436 self.request_redraw();
5437 }
5438 }
5439
5440 fn reorderable_at(&self, point: Point) -> Option<(WidgetId, usize, bool)> {
5444 let id = self.ui.as_ref()?.reorderable_at(point)?;
5445 let widget = self
5446 .tree
5447 .as_ref()
5448 .and_then(|tree| find_widget(tree.as_ref(), id))?;
5449 if !widget.reorder_draggable() {
5452 return None;
5453 }
5454 let from = widget.reorder_index()?;
5455 Some((id, from, widget.drag_needs_long_press()))
5456 }
5457
5458 fn reorder_source(&self, id: WidgetId, from: usize) -> Option<(WidgetId, Rect)> {
5470 let ui = self.ui.as_ref()?;
5471 let tree = self.tree.as_ref()?;
5472 let own = ui.widget_rect(id)?;
5473 let grabbed = find_widget(tree.as_ref(), id)?;
5474 if grabbed.reorder_droppable() {
5475 return Some((id, own));
5476 }
5477 let middle = Point::new(own.x + own.width * 0.5, own.y + own.height * 0.5);
5478 let slot = ui.reorderables_at(middle).find(|other| {
5479 find_widget(tree.as_ref(), *other)
5480 .is_some_and(|w| w.reorder_droppable() && w.reorder_index() == Some(from))
5481 })?;
5482 Some((slot, ui.widget_rect(slot).unwrap_or(own)))
5486 }
5487
5488 fn reorder_carried(&self) -> Option<(WidgetId, Rect)> {
5492 match &self.drag {
5493 Some(Drag::Reorder {
5494 carried: Some(carried),
5495 ..
5496 }) => Some(*carried),
5497 Some(Drag::Reorder { id, from, .. }) => self.reorder_source(*id, *from),
5498 _ => None,
5499 }
5500 }
5501
5502 fn keep_carried_box(&mut self) {
5507 let source = match &self.drag {
5508 Some(Drag::Reorder {
5509 id,
5510 from,
5511 moved: true,
5512 carried: None,
5513 ..
5514 }) => self.reorder_source(*id, *from),
5515 _ => return,
5516 };
5517 if let Some(Drag::Reorder { carried, .. }) = self.drag.as_mut() {
5518 *carried = source;
5519 }
5520 }
5521
5522 fn reorder_target_at(&self, point: Point) -> Option<WidgetId> {
5526 let ui = self.ui.as_ref()?;
5527 let tree = self.tree.as_ref()?;
5528 ui.reorderables_at(point)
5529 .find(|id| find_widget(tree.as_ref(), *id).is_some_and(|w| w.reorder_droppable()))
5530 }
5531
5532 fn reorder_drop_target(
5541 &self,
5542 id: WidgetId,
5543 from: usize,
5544 carried: Option<(WidgetId, Rect)>,
5545 ) -> Option<WidgetId> {
5546 if let Some(target) = self.reorder_target_at(self.cursor) {
5547 return Some(target);
5548 }
5549 let tree = self.tree.as_ref()?;
5550 let Some(ReorderMotion::Slots(axis)) = find_widget(tree.as_ref(), id).map(reorder_motion)
5551 else {
5552 return None;
5553 };
5554 let row = match carried {
5557 Some((row, _)) => row,
5558 None => self.reorder_source(id, from)?.0,
5559 };
5560 let slots = reorder_siblings(self.ui.as_ref()?, tree.as_ref(), row);
5561 let boxes: Vec<Rect> = slots.iter().map(|(_, rect)| *rect).collect();
5562 nearest_reorder_slot(self.cursor, &boxes, axis).map(|index| slots[index].0)
5563 }
5564
5565 fn carried_rect(&self) -> Option<Rect> {
5572 match &self.drag {
5573 Some(Drag::Reorder {
5574 start, moved: true, ..
5575 }) => {
5576 let (_, src) = self.reorder_carried()?;
5577 let (gx, gy) = ghost_offset(
5580 self.dragged_reorder_motion()
5581 .unwrap_or(ReorderMotion::Columns),
5582 (self.cursor.x - start.x, self.cursor.y - start.y),
5583 );
5584 Some(src.translate(gx, gy))
5585 }
5586 Some(Drag::Item {
5587 source,
5588 start,
5589 moved: true,
5590 ..
5591 }) => Some(
5592 source
5593 .rect
5594 .translate(self.cursor.x - start.x, self.cursor.y - start.y),
5595 ),
5596 _ => None,
5597 }
5598 }
5599
5600 fn autoscroll_carried(&mut self, dt: f32) -> bool {
5608 self.keep_carried_box();
5609 let Some(carried) = self.carried_rect() else {
5610 return false;
5611 };
5612 let area = {
5613 let scroll = &self.runtime.scroll;
5614 self.ui.as_ref().and_then(|ui| {
5615 ui.scroll_chain(self.cursor).find(|area| {
5616 area.accepts_user_offset(scroll.get(&area.id).copied().unwrap_or((0.0, 0.0)))
5617 })
5618 })
5619 };
5620 let Some(area) = area else {
5621 return false;
5622 };
5623 let offset = self
5624 .runtime
5625 .scroll
5626 .get(&area.id)
5627 .copied()
5628 .unwrap_or((0.0, 0.0));
5629 let room = |offset: f32, max: f32, reverse: bool| {
5632 if reverse {
5633 (max - offset, offset)
5634 } else {
5635 (offset, max - offset)
5636 }
5637 };
5638 let view = area.viewport;
5639 let dx = edge_autoscroll(
5640 (carried.x, carried.x + carried.width),
5641 (view.x, view.x + view.width),
5642 room(offset.0, area.max_x, area.reverse_x),
5643 dt,
5644 )
5645 .unwrap_or(0.0);
5646 let dy = edge_autoscroll(
5647 (carried.y, carried.y + carried.height),
5648 (view.y, view.y + view.height),
5649 room(offset.1, area.max_y, area.reverse_y),
5650 dt,
5651 )
5652 .unwrap_or(0.0);
5653 if dx == 0.0 && dy == 0.0 {
5654 return false;
5655 }
5656 let delta = area.offset_delta((dx, dy));
5659 let next = (
5660 (offset.0 + delta.0).clamp(0.0, area.max_x),
5661 (offset.1 + delta.1).clamp(0.0, area.max_y),
5662 );
5663 if next == offset {
5664 return false;
5665 }
5666 self.runtime.scroll.insert(area.id, next);
5667 self.runtime.scroll_target.insert(area.id, next);
5670 self.runtime.scroll_velocity.remove(&area.id);
5671 if let Some(drag) = self.drag.as_mut() {
5677 follow_content(
5678 drag,
5679 area.offset_delta((next.0 - offset.0, next.1 - offset.1)),
5680 );
5681 }
5682 true
5683 }
5684
5685 fn dragged_reorder_motion(&self) -> Option<ReorderMotion> {
5689 let Some(Drag::Reorder { id, .. }) = self.drag else {
5690 return None;
5691 };
5692 self.tree
5693 .as_ref()
5694 .and_then(|t| find_widget(t.as_ref(), id))
5695 .map(reorder_motion)
5696 }
5697
5698 fn advance_reorder_springs(&mut self, dt: f32) -> bool {
5701 let reorder_motion = matches!(self.drag, Some(Drag::Reorder { moved: true, .. }))
5702 .then(|| self.dragged_reorder_motion())
5703 .flatten();
5704 match reorder_motion {
5705 Some(ReorderMotion::Columns) => {
5706 self.reorder_x = spring_toward(self.reorder_x, self.cursor.x, dt, 0.07);
5707 (self.cursor.x - self.reorder_x).abs() > 0.5
5708 }
5709 Some(ReorderMotion::Slots(ReorderAxis::Vertical)) => {
5710 match self.reorder_drop_line(drag_preview::INSERT_THICKNESS) {
5711 Some(target) => {
5712 self.reorder_y = spring_toward(self.reorder_y, target.y, dt, 0.07);
5713 (target.y - self.reorder_y).abs() > 0.5
5714 }
5715 None => false,
5716 }
5717 }
5718 Some(ReorderMotion::Slots(ReorderAxis::Horizontal)) => {
5719 match self.reorder_drop_line(drag_preview::INSERT_THICKNESS) {
5720 Some(target) => {
5721 self.reorder_x = spring_toward(self.reorder_x, target.x, dt, 0.07);
5722 (target.x - self.reorder_x).abs() > 0.5
5723 }
5724 None => false,
5725 }
5726 }
5727 None => false,
5728 }
5729 }
5730
5731 fn report_overflows(&self, ui: &Ui<A::Message>) {
5747 for o in ui.overflows() {
5748 let mut key = std::collections::hash_map::DefaultHasher::new();
5752 use std::hash::{Hash, Hasher};
5753 (
5754 o.rect.width.to_bits(),
5755 o.rect.height.to_bits(),
5756 o.side as u8,
5757 o.amount.round() as i32,
5758 )
5759 .hash(&mut key);
5760 if !self.reported_overflows.borrow_mut().insert(key.finish()) {
5761 continue;
5762 }
5763 let side = match o.side {
5764 frus_widgets::Side::Left => "left",
5765 frus_widgets::Side::Right => "right",
5766 frus_widgets::Side::Top => "top",
5767 frus_widgets::Side::Bottom => "bottom",
5768 };
5769 log::warn!(
5770 "a box {:.0}x{:.0} is overflowed by {:.0} px on the {side}: its children do not \
5771 fit inside it, and what runs past the edge is drawn outside its parent — \
5772 invisible where something clips it, and untappable where it leaves the window. \
5773 Give the child that should give way an `Expanded`, or put the content in a \
5774 `SingleChildScrollView`.",
5775 o.rect.width,
5776 o.rect.height,
5777 o.amount,
5778 );
5779 }
5780 }
5781
5782 fn keystroke(&self, event: &KeyDown) -> Option<KeyStroke> {
5788 let key = match &event.logical_key {
5789 WinitKey::Named(NamedKey::Enter) => ShortcutKey::Enter,
5790 WinitKey::Named(NamedKey::Escape) => ShortcutKey::Escape,
5791 WinitKey::Named(NamedKey::Tab) => ShortcutKey::Tab,
5792 WinitKey::Named(NamedKey::Space) => ShortcutKey::Space,
5793 WinitKey::Named(NamedKey::Backspace) => ShortcutKey::Backspace,
5794 WinitKey::Named(NamedKey::Delete) => ShortcutKey::Delete,
5795 WinitKey::Named(NamedKey::ArrowUp) => ShortcutKey::Up,
5796 WinitKey::Named(NamedKey::ArrowDown) => ShortcutKey::Down,
5797 WinitKey::Named(NamedKey::ArrowLeft) => ShortcutKey::Left,
5798 WinitKey::Named(NamedKey::ArrowRight) => ShortcutKey::Right,
5799 WinitKey::Named(NamedKey::Home) => ShortcutKey::Home,
5800 WinitKey::Named(NamedKey::End) => ShortcutKey::End,
5801 WinitKey::Named(NamedKey::PageUp) => ShortcutKey::PageUp,
5802 WinitKey::Named(NamedKey::PageDown) => ShortcutKey::PageDown,
5803 WinitKey::Named(NamedKey::F1) => ShortcutKey::F(1),
5804 WinitKey::Named(NamedKey::F2) => ShortcutKey::F(2),
5805 WinitKey::Named(NamedKey::F3) => ShortcutKey::F(3),
5806 WinitKey::Named(NamedKey::F4) => ShortcutKey::F(4),
5807 WinitKey::Named(NamedKey::F5) => ShortcutKey::F(5),
5808 WinitKey::Named(NamedKey::F6) => ShortcutKey::F(6),
5809 WinitKey::Named(NamedKey::F7) => ShortcutKey::F(7),
5810 WinitKey::Named(NamedKey::F8) => ShortcutKey::F(8),
5811 WinitKey::Named(NamedKey::F9) => ShortcutKey::F(9),
5812 WinitKey::Named(NamedKey::F10) => ShortcutKey::F(10),
5813 WinitKey::Named(NamedKey::F11) => ShortcutKey::F(11),
5814 WinitKey::Character(text) => ShortcutKey::Char(text.chars().next()?),
5815 _ => return None,
5816 };
5817 Some(KeyStroke {
5818 key,
5819 ctrl: self.ctrl,
5820 shift: self.shift,
5821 alt: self.alt,
5822 meta: self.meta,
5823 })
5824 }
5825
5826 fn paint_reorder_preview(&self, ui: &Ui<A::Message>, theme: &Theme, scene: &mut Scene) {
5827 let Some(Drag::Reorder {
5828 start, moved: true, ..
5829 }) = self.drag
5830 else {
5831 return;
5832 };
5833 let Some((id, src)) = self.reorder_carried() else {
5836 return;
5837 };
5838 let motion = self
5839 .tree
5840 .as_ref()
5841 .and_then(|t| find_widget(t.as_ref(), id))
5842 .map(reorder_motion)
5843 .unwrap_or(ReorderMotion::Columns);
5844 let dx = self.cursor.x - start.x;
5845 let dy = self.cursor.y - start.y;
5846
5847 let (gx, gy) = ghost_offset(motion, (dx, dy));
5849
5850 let owners: std::collections::HashSet<u64> = self
5854 .tree
5855 .as_ref()
5856 .and_then(|t| find_widget(t.as_ref(), id))
5857 .map(|w| subtree_ids(w, id).iter().map(|i| i.as_u64()).collect())
5858 .unwrap_or_else(|| std::iter::once(id.as_u64()).collect());
5859
5860 match motion {
5861 ReorderMotion::Columns => {
5862 let reflowed =
5866 reflow_reorder_columns(scene.primitives(), src, self.reorder_x, id.as_u64());
5867 scene.clear();
5868 for primitive in reflowed {
5869 scene.push_primitive(primitive);
5870 }
5871 }
5872 ReorderMotion::Slots(axis) => {
5873 let line =
5884 self.reorder_drop_line(drag_preview::INSERT_THICKNESS)
5885 .map(|r| match axis {
5886 ReorderAxis::Vertical => Rect {
5887 y: self.reorder_y,
5888 ..r
5889 },
5890 ReorderAxis::Horizontal => Rect {
5891 x: self.reorder_x,
5892 ..r
5893 },
5894 });
5895 let movable = self
5901 .tree
5902 .as_ref()
5903 .map(|tree| reorderable_owners(ui, tree.as_ref()))
5904 .unwrap_or_default();
5905 let reflowed =
5906 reflow_reorder_cards(scene.primitives(), src, line, &owners, &movable, axis);
5907 scene.clear();
5908 for primitive in reflowed {
5909 scene.push_primitive(primitive);
5910 }
5911 if let Some(line) = line {
5912 let thickness = match axis {
5914 ReorderAxis::Vertical => line.height,
5915 ReorderAxis::Horizontal => line.width,
5916 };
5917 scene.set_clip(Rect::UNBOUNDED);
5918 scene.draw_rect(
5919 line,
5920 theme.primary,
5921 theme.radius.min(thickness * 0.5),
5922 0.0,
5923 Color::TRANSPARENT,
5924 );
5925 }
5926 }
5927 }
5928
5929 let ghost: Vec<Primitive> = ui
5935 .scene()
5936 .primitives()
5937 .iter()
5938 .filter(|p| owners.contains(&p.owner()))
5939 .map(|p| p.translated(gx, gy).with_clip(Rect::UNBOUNDED))
5940 .collect();
5941 draw_ghost_card(scene, theme, src.translate(gx, gy), &ghost);
5942 }
5943
5944 fn paint_drag_ghost(&self, ui: &Ui<A::Message>, theme: &Theme, scene: &mut Scene) {
5952 let Some(Drag::Item {
5953 source,
5954 start,
5955 moved: true,
5956 ..
5957 }) = &self.drag
5958 else {
5959 return;
5960 };
5961 let widget = self
5962 .tree
5963 .as_ref()
5964 .and_then(|tree| find_widget(tree.as_ref(), source.id));
5965 let owners: std::collections::HashSet<u64> = widget
5969 .map(|w| {
5970 subtree_ids(w, source.id)
5971 .iter()
5972 .map(|i| i.as_u64())
5973 .collect()
5974 })
5975 .unwrap_or_else(|| std::iter::once(source.id.as_u64()).collect());
5976 let opacity = widget.map(|w| w.drag_ghost_opacity()).unwrap_or(1.0);
5977 let dx = self.cursor.x - start.x;
5978 let dy = self.cursor.y - start.y;
5979
5980 let original: Vec<Primitive> = scene.primitives().to_vec();
5982 scene.clear();
5983 for primitive in &original {
5984 if owners.contains(&primitive.owner()) {
5985 scene.push_faded(primitive, opacity);
5986 } else {
5987 scene.push_primitive(primitive.clone());
5988 }
5989 }
5990
5991 let ghost: Vec<Primitive> = ui
5992 .scene()
5993 .primitives()
5994 .iter()
5995 .filter(|p| owners.contains(&p.owner()))
5996 .map(|p| p.translated(dx, dy).with_clip(Rect::UNBOUNDED))
5997 .collect();
5998 draw_ghost_card(scene, theme, source.rect.translate(dx, dy), &ghost);
5999 }
6000
6001 fn reorder_drop_line(&self, thickness: f32) -> Option<Rect> {
6007 let Some(Drag::Reorder {
6008 id, from, carried, ..
6009 }) = self.drag
6010 else {
6011 return None;
6012 };
6013 let target = self.reorder_drop_target(id, from, carried)?;
6016 let rect = self.ui.as_ref()?.widget_rect(target)?;
6017 let axis = match self
6018 .tree
6019 .as_ref()
6020 .and_then(|t| find_widget(t.as_ref(), target))
6021 .map(reorder_motion)
6022 {
6023 Some(ReorderMotion::Slots(axis)) => axis,
6024 _ => ReorderAxis::Vertical,
6025 };
6026 Some(drop_insertion_line(
6027 rect,
6028 thickness,
6029 self.reorder_insert_after(target, rect),
6030 axis,
6031 self.is_rtl(),
6032 ))
6033 }
6034
6035 fn reorder_insert_after(&self, target: WidgetId, rect: Rect) -> bool {
6041 match self
6042 .tree
6043 .as_ref()
6044 .and_then(|t| find_widget(t.as_ref(), target))
6045 .map(reorder_motion)
6046 {
6047 Some(ReorderMotion::Slots(axis)) => {
6048 reorder_drop_after(self.cursor, rect, axis, self.is_rtl())
6049 }
6050 _ => false,
6051 }
6052 }
6053
6054 fn pan_target(&self) -> Option<(WidgetId, frus_widgets::PanAxis)> {
6056 let (id, _) = self.ui.as_ref()?.pan_at(self.cursor)?;
6057 let axis = find_widget(self.tree.as_ref()?.as_ref(), id)?.pan_axis()?;
6058 Some((id, axis))
6059 }
6060
6061 fn pan_local(&self, id: WidgetId, at: Point) -> Point {
6063 let origin = self
6064 .ui
6065 .as_ref()
6066 .and_then(|ui| ui.widget_rect(id))
6067 .map_or(Point::new(0.0, 0.0), |rect| Point::new(rect.x, rect.y));
6068 Point::new(at.x - origin.x, at.y - origin.y)
6069 }
6070
6071 fn send_pan(&mut self, id: WidgetId, event: frus_widgets::PanEvent) {
6073 let rtl = self.is_rtl();
6074 let value = self.runtime.value(id);
6075 let (message, moved, released) = match self
6076 .tree
6077 .as_ref()
6078 .and_then(|tree| find_widget(tree.as_ref(), id))
6079 {
6080 Some(widget) => {
6081 let moved = widget.pan_value(event, value, rtl);
6085 let released = match event {
6086 frus_widgets::PanEvent::End { .. } if self.runtime.value_placed(id) => {
6087 Some(widget.on_value_release(value))
6088 }
6089 _ => None,
6090 };
6091 (widget.on_pan(event), moved, released)
6092 }
6093 None => (None, None, None),
6094 };
6095 if let Some(v) = moved {
6096 self.runtime.hold_value(id, v);
6097 }
6098 if let Some(release) = released {
6099 self.runtime.release_value(id);
6100 if let Some(message) = release {
6101 self.dispatch(message);
6102 }
6103 }
6104 if let Some(message) = message {
6105 self.dispatch(message);
6106 }
6107 self.request_redraw();
6108 }
6109
6110 fn dispatch_drag_edge(&mut self, id: WidgetId, rect: frus_widgets::Rect, edge: Edge) {
6117 let (x, rtl) = (self.cursor.x, self.is_rtl());
6118 let message = self
6119 .tree
6120 .as_ref()
6121 .and_then(|tree| find_widget(tree.as_ref(), id))
6122 .and_then(|widget| {
6123 let fraction = widget.drag_track().fraction(rect, x, rtl);
6124 match edge {
6125 Edge::Start => widget.on_drag_start(fraction),
6126 Edge::End => widget.on_drag_end(fraction),
6127 }
6128 });
6129 if let Some(message) = message {
6130 self.dispatch(message);
6131 }
6132 }
6133
6134 fn apply_widget_drag(&mut self, id: WidgetId, rect: frus_widgets::Rect, dx: f32) {
6135 let (x, rtl) = (self.cursor.x, self.is_rtl());
6136 let message = self
6139 .tree
6140 .as_ref()
6141 .and_then(|tree| find_widget(tree.as_ref(), id))
6142 .and_then(|widget| {
6143 widget
6144 .on_drag_delta(dx)
6145 .or_else(|| widget.on_drag(widget.drag_track().fraction(rect, x, rtl)))
6146 });
6147 if let Some(message) = message {
6148 self.dispatch(message);
6149 }
6150 }
6151
6152 fn key_down(&mut self, event: &KeyDown, event_loop: Option<&ActiveEventLoop>) {
6157 if !self.runtime.focus_visible {
6159 self.runtime.focus_visible = true;
6160 self.request_redraw();
6161 }
6162
6163 if matches!(
6166 event.logical_key,
6167 WinitKey::Named(NamedKey::BrowserBack) | WinitKey::Named(NamedKey::GoBack)
6168 ) {
6169 if !event.repeat {
6170 if let Some(event_loop) = event_loop {
6171 self.system_back(event_loop);
6172 }
6173 }
6174 return;
6175 }
6176
6177 if matches!(event.logical_key, WinitKey::Named(NamedKey::F12)) {
6180 if cfg!(debug_assertions) && !event.repeat {
6181 self.inspector = !self.inspector;
6182 self.inspector_dump = self.inspector;
6183 self.request_redraw();
6184 }
6185 return;
6186 }
6187
6188 if let Some(stroke) = self.keystroke(event) {
6196 let typing = !stroke.is_command() && self.runtime.input.focused.is_some();
6197 if !typing {
6198 let msgs = self
6199 .ui
6200 .as_ref()
6201 .map(|ui| ui.keystroke(stroke, self.runtime.input.focused))
6202 .unwrap_or_default();
6203 if !msgs.is_empty() {
6204 for msg in msgs {
6205 self.dispatch(msg);
6206 }
6207 self.request_redraw();
6208 return;
6209 }
6210 }
6211 }
6212
6213 if matches!(event.logical_key, WinitKey::Named(NamedKey::Tab)) {
6215 let forward = !self.shift;
6216 let next = self
6217 .ui
6218 .as_ref()
6219 .and_then(|ui| ui.focus_next(self.runtime.input.focused, forward));
6220 if next.is_some() {
6221 self.runtime.input.focused = next;
6222 self.runtime.region = None;
6223 self.reveal_focus();
6224 self.request_redraw();
6225 }
6226 return;
6227 }
6228
6229 if matches!(event.logical_key, WinitKey::Named(NamedKey::Escape)) {
6233 if !event.repeat {
6235 self.escape();
6236 }
6237 return;
6238 }
6239
6240 let Some(focused) = self.runtime.input.focused else {
6241 self.region_key(event);
6244 return;
6245 };
6246
6247 let arrow = match event.logical_key {
6251 WinitKey::Named(NamedKey::ArrowUp) => Some(FocusDirection::Up),
6252 WinitKey::Named(NamedKey::ArrowDown) => Some(FocusDirection::Down),
6253 WinitKey::Named(NamedKey::ArrowLeft) => Some(FocusDirection::Left),
6254 WinitKey::Named(NamedKey::ArrowRight) => Some(FocusDirection::Right),
6255 _ => None,
6256 };
6257 if matches!(
6260 event.logical_key,
6261 WinitKey::Named(NamedKey::PageUp) | WinitKey::Named(NamedKey::PageDown)
6262 ) {
6263 let down = matches!(event.logical_key, WinitKey::Named(NamedKey::PageDown));
6264 if self.move_caret_vertical(focused, down, true) {
6265 return;
6266 }
6267 }
6268
6269 if let Some(direction) = arrow {
6270 if matches!(direction, FocusDirection::Up | FocusDirection::Down) {
6274 let down = matches!(direction, FocusDirection::Down);
6275 if self.move_caret_vertical(focused, down, false) {
6276 return;
6277 }
6278 }
6279
6280 if matches!(direction, FocusDirection::Left | FocusDirection::Right) {
6284 let key = if matches!(direction, FocusDirection::Left) {
6285 Key::Left {
6286 shift: self.shift,
6287 word: self.ctrl,
6288 }
6289 } else {
6290 Key::Right {
6291 shift: self.shift,
6292 word: self.ctrl,
6293 }
6294 };
6295 let widget = self
6296 .tree
6297 .as_ref()
6298 .and_then(|tree| find_widget(tree.as_ref(), focused));
6299 let reorder_from = widget.and_then(|w| w.reorder_index());
6303 let handled = widget.map(|w| w.on_key(&key));
6304 if let Some(KeyResponse::Handled(message)) = handled {
6305 if let Some(message) = message {
6306 self.dispatch(message);
6307 }
6308 if let Some(from) = reorder_from {
6309 let to = if matches!(direction, FocusDirection::Left) {
6310 from.wrapping_sub(1)
6311 } else {
6312 from + 1
6313 };
6314 self.set_announcement(format!("Column moved to position {}", to + 1));
6315 }
6316 self.request_redraw();
6317 return;
6318 }
6319 }
6320
6321 let is_text = self
6322 .tree
6323 .as_ref()
6324 .and_then(|tree| find_widget(tree.as_ref(), focused))
6325 .is_some_and(is_text_field);
6326 let navigates =
6327 !is_text || matches!(direction, FocusDirection::Up | FocusDirection::Down);
6328 if navigates {
6329 if let Some(next) = self
6330 .ui
6331 .as_ref()
6332 .and_then(|ui| ui.focus_directional(focused, direction))
6333 {
6334 self.runtime.input.focused = Some(next);
6335 self.reveal_focus();
6336 self.request_redraw();
6337 }
6338 return;
6339 }
6340 }
6341
6342 if matches!(
6347 event.logical_key,
6348 WinitKey::Named(NamedKey::Home) | WinitKey::Named(NamedKey::End)
6349 ) {
6350 let key = if matches!(event.logical_key, WinitKey::Named(NamedKey::Home)) {
6351 Key::Home {
6352 shift: self.shift,
6353 doc: self.ctrl,
6354 }
6355 } else {
6356 Key::End {
6357 shift: self.shift,
6358 doc: self.ctrl,
6359 }
6360 };
6361 let handled = self
6362 .tree
6363 .as_ref()
6364 .and_then(|tree| find_widget(tree.as_ref(), focused))
6365 .map(|widget| widget.on_key(&key));
6366 if let Some(KeyResponse::Handled(message)) = handled {
6367 if let Some(message) = message {
6368 self.dispatch(message);
6369 }
6370 self.request_redraw();
6371 return;
6372 }
6373 }
6374
6375 if matches!(
6379 event.logical_key,
6380 WinitKey::Named(NamedKey::Enter) | WinitKey::Named(NamedKey::Space)
6381 ) {
6382 let widget = self
6383 .tree
6384 .as_ref()
6385 .and_then(|tree| find_widget(tree.as_ref(), focused))
6386 .filter(|widget| widget.focusable());
6387 let message = widget.and_then(|widget| widget.on_click());
6388 if let Some(message) = message {
6389 if !event.repeat {
6392 let announce = widget.and_then(|widget| widget.announce());
6395 self.dispatch(message);
6396 if let Some(announce) = announce {
6397 self.set_announcement(announce);
6398 }
6399 self.request_redraw();
6400 }
6401 return;
6402 }
6403 }
6404
6405 if let Some(command) = clipboard_command(&event.logical_key, event.physical_key, self.ctrl)
6407 {
6408 match command {
6409 ClipCommand::Copy => {
6410 self.copy_selection(focused);
6411 }
6412 ClipCommand::Cut => self.cut_selection(focused),
6413 ClipCommand::Paste => {
6414 if let Some(pasted) = self.clipboard.paste(focused, self.window.as_ref()) {
6416 self.land_paste(pasted);
6417 }
6418 }
6419 }
6420 return;
6421 }
6422
6423 if opens_selection_bar(&event.logical_key, self.shift)
6427 && self
6428 .tree
6429 .as_ref()
6430 .and_then(|tree| find_widget(tree.as_ref(), focused))
6431 .is_some_and(|widget| widget.text_value().is_some())
6432 {
6433 self.show_selection_toolbar(focused);
6434 return;
6435 }
6436
6437 if self.ctrl {
6439 match &event.logical_key {
6440 WinitKey::Character(c) if c.eq_ignore_ascii_case("a") => {
6441 self.select_all(focused);
6442 return;
6443 }
6444 WinitKey::Character(c) if c.eq_ignore_ascii_case("z") && !self.shift => {
6448 self.step_history(focused, false);
6449 self.request_redraw();
6450 return;
6451 }
6452 WinitKey::Character(c)
6453 if c.eq_ignore_ascii_case("y")
6454 || (c.eq_ignore_ascii_case("z") && self.shift) =>
6455 {
6456 self.step_history(focused, true);
6457 self.request_redraw();
6458 return;
6459 }
6460 _ => {}
6461 }
6462 }
6463
6464 #[cfg(android)]
6469 if crate::android_ime::installed() {
6470 return;
6471 }
6472
6473 let shift = self.shift;
6474 let key = match &event.logical_key {
6475 WinitKey::Named(NamedKey::Backspace) => Some(Key::Backspace),
6476 WinitKey::Named(NamedKey::Delete) => Some(Key::Delete),
6477 WinitKey::Named(NamedKey::Enter) if !event.repeat => Some(Key::Enter),
6480 WinitKey::Named(NamedKey::Enter) => None,
6481 WinitKey::Named(NamedKey::ArrowLeft) => Some(Key::Left {
6483 shift,
6484 word: self.ctrl,
6485 }),
6486 WinitKey::Named(NamedKey::ArrowRight) => Some(Key::Right {
6487 shift,
6488 word: self.ctrl,
6489 }),
6490 WinitKey::Named(NamedKey::Home) => Some(Key::Home {
6491 shift,
6492 doc: self.ctrl,
6493 }),
6494 WinitKey::Named(NamedKey::End) => Some(Key::End {
6495 shift,
6496 doc: self.ctrl,
6497 }),
6498 WinitKey::Named(NamedKey::Space) => Some(Key::Text(" ".to_string())),
6499 WinitKey::Character(c) if c == "\n" || c == "\r" => {
6503 if event.repeat {
6504 None
6505 } else {
6506 Some(Key::Enter)
6507 }
6508 }
6509 _ => event.text.as_ref().map(|text| Key::Text(text.to_string())),
6510 };
6511
6512 if let Some(key) = key {
6513 self.apply_key(focused, key);
6514 self.request_redraw();
6515 }
6516 }
6517
6518 fn system_back(&mut self, event_loop: &ActiveEventLoop) {
6525 if !self.system_back_step() {
6526 event_loop.exit();
6527 }
6528 }
6529
6530 fn system_back_step(&mut self) -> bool {
6533 if self.close_selection_toolbar() {
6536 return true;
6537 }
6538 if self
6540 .runtime
6541 .region
6542 .as_ref()
6543 .is_some_and(|selection| selection.bar)
6544 {
6545 self.runtime.region = None;
6546 self.request_redraw();
6547 return true;
6548 }
6549 if let Some(message) = self.ui.as_ref().and_then(|ui| ui.top_dismiss()) {
6550 self.dispatch(message);
6551 self.request_redraw();
6552 return true;
6553 }
6554 if self.app.can_go_back() {
6555 self.build_dirty = true;
6556 self.app.go_back();
6558 self.request_redraw();
6559 return true;
6560 }
6561 false
6562 }
6563
6564 fn escape(&mut self) {
6569 if self.close_selection_toolbar() {
6571 return;
6572 }
6573 if self.runtime.region.take().is_some() {
6575 self.request_redraw();
6576 return;
6577 }
6578 let outcome: Option<Option<A::Message>> = self.runtime.input.focused.and_then(|focused| {
6581 let tree = self.tree.as_ref()?;
6582 let path = find_path(tree.as_ref(), focused);
6583 for widget in path.iter().rev() {
6584 match widget.on_key(&Key::Escape) {
6585 KeyResponse::Handled(message) => return Some(message),
6586 KeyResponse::Skip => return Some(None),
6587 KeyResponse::Ignored => {}
6588 }
6589 }
6590 None
6591 });
6592
6593 match outcome {
6594 Some(message) => {
6595 if let Some(message) = message {
6596 self.dispatch(message);
6597 self.request_redraw();
6598 }
6599 }
6600 None => {
6602 if let Some(message) = self.ui.as_ref().and_then(|ui| ui.top_dismiss()) {
6603 self.dispatch(message);
6604 self.request_redraw();
6605 }
6606 }
6607 }
6608 }
6609
6610 fn move_caret_vertical(&mut self, id: WidgetId, down: bool, page: bool) -> bool {
6616 let width = self
6617 .ui
6618 .as_ref()
6619 .and_then(|ui| ui.widget_rect(id))
6620 .map(|r| r.width)
6621 .unwrap_or(0.0);
6622 let cursor = self.runtime.edits.get(&id).map(|e| e.cursor).unwrap_or(0);
6623 let moved = self
6624 .tree
6625 .as_ref()
6626 .and_then(|tree| find_widget(tree.as_ref(), id))
6627 .and_then(|widget| widget.caret_vertical(width, cursor, down, page, self.goal_x));
6628 let Some((new_cursor, goal)) = moved else {
6629 return false;
6630 };
6631 let shift = self.shift;
6632 let edit = self.runtime.edits.entry(id).or_default();
6633 if shift {
6634 if edit.anchor.is_none() {
6635 edit.anchor = Some(edit.cursor);
6636 }
6637 } else {
6638 edit.anchor = None;
6639 }
6640 edit.cursor = new_cursor;
6641 self.goal_x = Some(goal);
6643 self.reveal_caret(id, new_cursor);
6644 self.request_redraw();
6645 true
6646 }
6647
6648 fn select_held_word(&mut self, id: WidgetId) {
6651 let cursor = self.runtime.edits.get(&id).map(|e| e.cursor).unwrap_or(0);
6652 let word = self
6653 .tree
6654 .as_ref()
6655 .and_then(|tree| find_widget(tree.as_ref(), id))
6656 .and_then(|widget| widget.word_at(cursor))
6657 .filter(|(start, end)| start < end);
6658 let Some((start, end)) = word else {
6659 self.show_selection_toolbar(id);
6662 return;
6663 };
6664 self.runtime.edits.insert(
6665 id,
6666 Edit {
6667 cursor: end,
6668 anchor: Some(start),
6669 composing: None,
6670 },
6671 );
6672 self.runtime.selection_handles = Some(id);
6673 self.runtime.close_edit_run(id);
6674 self.show_selection_toolbar(id);
6675 #[cfg(android)]
6676 self.push_ime_context(id);
6677 }
6678
6679 fn grab_selection_handle(&self) -> Option<Drag> {
6682 let id = self.runtime.selection_handles?;
6683 if self.runtime.input.focused != Some(id) {
6684 return None;
6685 }
6686 let rect = self.ui.as_ref()?.widget_rect(id)?;
6687 let edit = self.runtime.edits.get(&id).copied()?;
6688 let scroll_y = self.runtime.scroll.get(&id).map(|s| s.1).unwrap_or(0.0);
6689 let handles = find_widget(self.tree.as_ref()?.as_ref(), id)?
6690 .selection_handles(rect.width, &edit, scroll_y)?;
6691 let local = Point::new(self.cursor.x - rect.x, self.cursor.y - rect.y);
6692 let handle = crate::selection::grab(&handles, local)?;
6693 let at = handles[handle.index()].line_center;
6694 Some(Drag::SelectionHandle {
6695 id,
6696 rect,
6697 handle,
6698 grab: Point::new(at.x - local.x, at.y - local.y),
6699 })
6700 }
6701
6702 fn apply_key(&mut self, id: WidgetId, key: Key) {
6705 self.goal_x = None;
6707 self.runtime.selection_handles = None;
6710 self.runtime.selection_toolbar = None;
6711 let was = self.runtime.edits.get(&id).copied().unwrap_or_default();
6712 let mut edit = was;
6713 let widget = self
6714 .tree
6715 .as_ref()
6716 .and_then(|tree| find_widget(tree.as_ref(), id));
6717 let before = widget
6720 .and_then(|widget| widget.text_value())
6721 .map(str::to_owned);
6722 let announce = widget.and_then(|widget| widget.announce());
6727 let message = widget.and_then(|widget| widget.on_edit(&mut edit, &key));
6728 self.runtime.edits.insert(id, edit);
6729 self.reveal_caret(id, edit.cursor);
6731 if let Some(message) = message {
6732 self.dispatch_edit(message);
6733 if let Some(announce) = announce {
6734 self.set_announcement(announce);
6735 }
6736 }
6737 let after = self
6743 .tree
6744 .as_ref()
6745 .and_then(|tree| find_widget(tree.as_ref(), id))
6746 .and_then(|widget| widget.text_value())
6747 .map(str::to_owned);
6748 match (before, after) {
6749 (Some(before), Some(after)) if before != after => {
6750 let kind = EditKind::of(&key, was.selection_range().is_some(), self.composing());
6751 let since = self.since_last_edit();
6752 self.runtime
6753 .record_edit(id, EditSnapshot::new(before, was), kind, since);
6754 }
6755 (Some(_), _) => self.runtime.close_edit_run(id),
6758 _ => {}
6759 }
6760 }
6761
6762 fn dispatch_edit(&mut self, message: A::Message) {
6770 self.dispatch(message);
6771 if let Some((width, height)) = self.last_size {
6772 let theme = self.themes.displayed(&self.app);
6773 let tree = self
6777 .media_query(width, height)
6778 .scope(|| build_view(&self.app, &theme, &self.runtime));
6779 self.tree = Some(tree);
6780 }
6781 }
6782
6783 fn since_last_edit(&mut self) -> f32 {
6786 let now = Instant::now();
6787 let since = self
6788 .last_edit_at
6789 .map(|then| now.duration_since(then).as_secs_f32())
6790 .unwrap_or(f32::INFINITY);
6791 self.last_edit_at = Some(now);
6792 since
6793 }
6794
6795 fn composing(&self) -> bool {
6797 self.runtime
6798 .input
6799 .focused
6800 .and_then(|id| self.runtime.edits.get(&id))
6801 .is_some_and(|edit| edit.composing.is_some())
6802 }
6803
6804 fn step_history(&mut self, id: WidgetId, forward: bool) -> bool {
6808 let value = self
6809 .tree
6810 .as_ref()
6811 .and_then(|tree| find_widget(tree.as_ref(), id))
6812 .and_then(|widget| widget.text_value())
6813 .map(str::to_owned);
6814 let Some(value) = value else {
6815 return false;
6816 };
6817 let edit = self.runtime.edits.get(&id).copied().unwrap_or_default();
6818 let current = EditSnapshot::new(value, edit);
6819 let target = if forward {
6820 self.runtime.redo_edit(id, current)
6821 } else {
6822 self.runtime.undo_edit(id, current)
6823 };
6824 let Some(target) = target else {
6825 return false;
6826 };
6827 let message = self
6828 .tree
6829 .as_ref()
6830 .and_then(|tree| find_widget(tree.as_ref(), id))
6831 .and_then(|widget| widget.replace_value(target.value.clone()));
6832 self.runtime.edits.insert(id, target.edit);
6835 self.reveal_caret(id, target.edit.cursor);
6836 if let Some(message) = message {
6837 self.dispatch_edit(message);
6838 }
6839 self.last_edit_at = None;
6842 true
6843 }
6844
6845 fn reveal_focus(&mut self) {
6857 let Some(id) = self.runtime.input.focused else {
6858 return;
6859 };
6860 let Some(moves) = self.ui.as_ref().map(|ui| ui.reveal(id, &self.runtime)) else {
6861 return;
6862 };
6863 for (area, offset) in moves {
6864 self.runtime.scroll_ballistic.remove(&area);
6865 self.runtime.scroll_velocity.remove(&area);
6866 self.runtime.scroll_target.insert(area, offset);
6867 }
6868 }
6869
6870 fn reveal_caret(&mut self, id: WidgetId, cursor: usize) {
6874 let Some(vp) = self.ui.as_ref().and_then(|ui| ui.scrollable_viewport(id)) else {
6875 return;
6876 };
6877 let metrics = self
6878 .tree
6879 .as_ref()
6880 .and_then(|tree| find_widget(tree.as_ref(), id))
6881 .and_then(|widget| widget.text_metrics(vp.width, cursor));
6882 let Some((content_h, visible_h, caret_top, caret_h)) = metrics else {
6883 return;
6884 };
6885 let max_y = (content_h - visible_h).max(0.0);
6886 let cur = self.runtime.scroll.get(&id).map(|s| s.1).unwrap_or(0.0);
6887 let lo = (caret_top + caret_h - visible_h).max(0.0);
6890 let hi = caret_top;
6891 let target = cur.clamp(lo.min(hi), lo.max(hi)).clamp(0.0, max_y);
6892 self.runtime.scroll.insert(id, (0.0, target));
6893 self.runtime.scroll_target.insert(id, (0.0, target));
6894 self.runtime.scroll_velocity.remove(&id);
6895 }
6896
6897 fn copy_selection(&mut self, id: WidgetId) -> bool {
6900 let edit = self.runtime.edits.get(&id).copied().unwrap_or_default();
6901 let text = self
6902 .tree
6903 .as_ref()
6904 .and_then(|tree| find_widget(tree.as_ref(), id))
6905 .and_then(|widget| widget.selected_text(&edit));
6906 match text {
6907 Some(text) => {
6908 self.clipboard.set_text(text);
6909 true
6910 }
6911 None => false,
6912 }
6913 }
6914
6915 fn region_stops(&self, area: WidgetId) -> Vec<(WidgetId, usize)> {
6918 let (Some(ui), Some(tree)) = (self.ui.as_ref(), self.tree.as_ref()) else {
6919 return Vec::new();
6920 };
6921 ui.text_stops_in(area)
6922 .filter_map(|stop| {
6923 let text = find_widget(tree.as_ref(), stop.id)?.selection_text()?;
6924 Some((stop.id, text.chars().count()))
6925 })
6926 .collect()
6927 }
6928
6929 fn region_selection(
6931 &self,
6932 area: WidgetId,
6933 anchor: RegionPoint,
6934 extent: RegionPoint,
6935 ) -> RegionSelection {
6936 RegionSelection::new(area, anchor, extent, &self.region_stops(area))
6937 }
6938
6939 fn region_index(&self, stop: &TextStop, at: Point) -> Option<usize> {
6941 find_widget(self.tree.as_ref()?.as_ref(), stop.id)?.selection_hit(
6942 at.x - stop.rect.x,
6943 at.y - stop.rect.y,
6944 stop.rect.width,
6945 )
6946 }
6947
6948 fn begin_region_selection(&mut self) {
6952 let Some(stop) = self.region_press_target() else {
6953 return;
6954 };
6955 let Some(index) = self.region_index(&stop, self.cursor) else {
6956 return;
6957 };
6958 let now = Instant::now();
6959 let here = self.cursor;
6960 let double = self.last_region_click.is_some_and(|(then, there)| {
6961 (now - then).as_secs_f32() < 0.4
6962 && (here.x - there.x).hypot(here.y - there.y) <= REGION_DOUBLE_CLICK_SLOP
6963 });
6964 self.last_region_click = Some((now, here));
6965 let word = double
6966 .then(|| {
6967 let tree = self.tree.as_ref()?;
6968 let text = find_widget(tree.as_ref(), stop.id)?.selection_text()?;
6969 Some(frus_widgets::word_bounds(text, index))
6970 })
6971 .flatten();
6972 let (anchor, extent) = match word {
6973 Some((start, end)) => (
6974 RegionPoint {
6975 text: stop.id,
6976 index: start,
6977 },
6978 RegionPoint {
6979 text: stop.id,
6980 index: end,
6981 },
6982 ),
6983 None => {
6984 let point = RegionPoint {
6985 text: stop.id,
6986 index,
6987 };
6988 (point, point)
6989 }
6990 };
6991 self.runtime.region = Some(self.region_selection(stop.area, anchor, extent));
6992 self.drag = word
6994 .is_none()
6995 .then_some(Drag::RegionSelect { area: stop.area });
6996 self.request_redraw();
6997 }
6998
6999 fn region_press_target(&self) -> Option<TextStop> {
7002 let ui = self.ui.as_ref()?;
7003 let claimed = self
7004 .runtime
7005 .input
7006 .pressed
7007 .is_some_and(|pressed| ui.msg_for(pressed).is_some());
7008 if claimed {
7009 return None;
7010 }
7011 ui.text_stop_at(self.cursor)
7012 }
7013
7014 fn select_region_word_held(&mut self) -> bool {
7017 let Some(stop) = self.ui.as_ref().and_then(|ui| ui.text_stop_at(self.cursor)) else {
7018 return false;
7019 };
7020 let Some(index) = self.region_index(&stop, self.cursor) else {
7021 return false;
7022 };
7023 let word = self
7024 .tree
7025 .as_ref()
7026 .and_then(|tree| find_widget(tree.as_ref(), stop.id))
7027 .and_then(|widget| widget.selection_text())
7028 .map(|text| frus_widgets::word_bounds(text, index))
7029 .filter(|(start, end)| start < end);
7030 let Some((start, end)) = word else {
7031 return false;
7032 };
7033 let anchor = RegionPoint {
7034 text: stop.id,
7035 index: start,
7036 };
7037 let extent = RegionPoint {
7038 text: stop.id,
7039 index: end,
7040 };
7041 self.runtime.region = Some(
7042 self.region_selection(stop.area, anchor, extent)
7043 .with_bar(true),
7044 );
7045 self.request_redraw();
7046 true
7047 }
7048
7049 fn region_handles(&self) -> Option<(WidgetId, [frus_widgets::SelectionHandle; 2])> {
7051 let selection = self.runtime.region.as_ref().filter(|s| s.handles)?;
7052 let (ui, tree) = (self.ui.as_ref()?, self.tree.as_ref()?);
7053 let at = |point: RegionPoint, start: bool| {
7054 let stop = ui
7055 .text_stops_in(selection.area)
7056 .find(|s| s.id == point.text)?;
7057 let handle = find_widget(tree.as_ref(), point.text)?.selection_grip(
7058 stop.rect.width,
7059 point.index,
7060 start,
7061 )?;
7062 let (dx, dy) = (stop.rect.x, stop.rect.y);
7063 Some(frus_widgets::SelectionHandle {
7064 rect: handle.rect.translate(dx, dy),
7065 line_center: Point::new(handle.line_center.x + dx, handle.line_center.y + dy),
7066 })
7067 };
7068 Some((
7069 selection.area,
7070 [at(selection.start?, true)?, at(selection.end?, false)?],
7071 ))
7072 }
7073
7074 fn grab_region_handle(&self) -> Option<Drag> {
7076 let (area, handles) = self.region_handles()?;
7077 let handle = crate::selection::grab(&handles, self.cursor)?;
7078 let at = handles[handle.index()].line_center;
7079 Some(Drag::RegionHandle {
7080 area,
7081 handle,
7082 grab: Point::new(at.x - self.cursor.x, at.y - self.cursor.y),
7083 })
7084 }
7085
7086 fn drag_region_handle(
7090 &mut self,
7091 area: WidgetId,
7092 handle: crate::selection::Handle,
7093 grab: Point,
7094 ) {
7095 let Some(selection) = self.runtime.region.as_ref() else {
7096 return;
7097 };
7098 let (Some(start), Some(end)) = (selection.start, selection.end) else {
7099 return;
7100 };
7101 let target = Point::new(self.cursor.x + grab.x, self.cursor.y + grab.y);
7102 let Some(stop) = self
7103 .ui
7104 .as_ref()
7105 .and_then(|ui| ui.nearest_text_stop(area, target))
7106 else {
7107 return;
7108 };
7109 let Some(index) = self.region_index(&stop, target) else {
7110 return;
7111 };
7112 let moved = RegionPoint {
7113 text: stop.id,
7114 index,
7115 };
7116 let stops = self.region_stops(area);
7117 let order = |point: RegionPoint| {
7118 stops
7119 .iter()
7120 .position(|(id, _)| *id == point.text)
7121 .map(|at| (at, point.index))
7122 };
7123 let (Some(to), Some(other)) = (
7124 order(moved),
7125 order(match handle {
7126 crate::selection::Handle::Start => end,
7127 crate::selection::Handle::End => start,
7128 }),
7129 ) else {
7130 return;
7131 };
7132 let (fixed, crosses) = match handle {
7133 crate::selection::Handle::Start => (end, to >= other),
7134 crate::selection::Handle::End => (start, to <= other),
7135 };
7136 if crosses {
7137 return;
7138 }
7139 let next = RegionSelection::new(area, fixed, moved, &stops).with_bar(true);
7140 let next = RegionSelection { bar: false, ..next };
7142 if self.runtime.region.as_ref() != Some(&next) {
7143 self.runtime.region = Some(next);
7144 self.request_redraw();
7145 }
7146 }
7147
7148 fn extend_region_selection(&mut self, area: WidgetId) {
7151 let Some(anchor) = self.runtime.region.as_ref().map(|region| region.anchor) else {
7152 return;
7153 };
7154 let Some(stop) = self
7155 .ui
7156 .as_ref()
7157 .and_then(|ui| ui.nearest_text_stop(area, self.cursor))
7158 else {
7159 return;
7160 };
7161 let Some(index) = self.region_index(&stop, self.cursor) else {
7162 return;
7163 };
7164 let extent = RegionPoint {
7165 text: stop.id,
7166 index,
7167 };
7168 let selection = self.region_selection(area, anchor, extent);
7169 if self.runtime.region.as_ref() != Some(&selection) {
7170 self.runtime.region = Some(selection);
7171 self.request_redraw();
7172 }
7173 }
7174
7175 fn region_text(&self) -> Option<String> {
7178 let region = self.runtime.region.as_ref()?;
7179 let (ui, tree) = (self.ui.as_ref()?, self.tree.as_ref()?);
7180 let texts: Vec<(WidgetId, &str)> = ui
7181 .text_stops_in(region.area)
7182 .filter_map(|stop| {
7183 let text = find_widget(tree.as_ref(), stop.id)?.selection_text()?;
7184 Some((stop.id, text))
7185 })
7186 .collect();
7187 let copied = frus_widgets::region_copy(&texts, ®ion.ranges);
7188 (!copied.is_empty()).then_some(copied)
7189 }
7190
7191 fn copy_region(&mut self) -> bool {
7193 match self.region_text() {
7194 Some(copied) => {
7195 self.clipboard.set_text(copied);
7196 true
7197 }
7198 None => false,
7199 }
7200 }
7201
7202 fn select_all_region(&mut self) {
7204 let Some(area) = self.runtime.region.as_ref().map(|region| region.area) else {
7205 return;
7206 };
7207 let stops = self.region_stops(area);
7208 let (Some(first), Some(last)) = (stops.first(), stops.last()) else {
7209 return;
7210 };
7211 let anchor = RegionPoint {
7212 text: first.0,
7213 index: 0,
7214 };
7215 let extent = RegionPoint {
7216 text: last.0,
7217 index: last.1,
7218 };
7219 let bar = self
7220 .runtime
7221 .region
7222 .as_ref()
7223 .is_some_and(|selection| selection.bar);
7224 self.runtime.region = Some(self.region_selection(area, anchor, extent).with_bar(bar));
7225 self.request_redraw();
7226 }
7227
7228 fn region_key(&mut self, event: &KeyDown) -> bool {
7231 if self.runtime.region.is_none() {
7232 return false;
7233 }
7234 if let Some(ClipCommand::Copy) =
7235 clipboard_command(&event.logical_key, event.physical_key, self.ctrl)
7236 {
7237 self.copy_region();
7238 return true;
7239 }
7240 let select_all = self.ctrl
7241 && matches!(&event.logical_key, WinitKey::Character(c) if c.eq_ignore_ascii_case("a"));
7242 if select_all {
7243 self.select_all_region();
7244 return true;
7245 }
7246 false
7247 }
7248
7249 fn cut_selection(&mut self, id: WidgetId) {
7253 if self.copy_selection(id) {
7254 self.apply_key(id, Key::Backspace);
7255 self.request_redraw();
7256 }
7257 }
7258
7259 fn select_all(&mut self, id: WidgetId) {
7261 self.runtime.edits.insert(
7262 id,
7263 Edit {
7264 cursor: usize::MAX,
7265 anchor: Some(0),
7266 composing: None,
7267 },
7268 );
7269 self.runtime.close_edit_run(id);
7272 self.request_redraw();
7273 }
7274
7275 fn show_selection_toolbar(&mut self, id: WidgetId) {
7279 self.show_selection_toolbar_at(id, None);
7280 }
7281
7282 fn show_selection_toolbar_at(&mut self, id: WidgetId, at: Option<Point>) {
7285 let can_paste = self.clipboard.has_text();
7286 self.runtime.selection_toolbar = Some(ToolbarMark { id, can_paste, at });
7287 self.request_redraw();
7288 }
7289
7290 fn close_selection_toolbar(&mut self) -> bool {
7293 if self.runtime.selection_toolbar.is_none() {
7294 return false;
7295 }
7296 self.runtime.selection_handles = None;
7297 self.hide_selection_toolbar();
7298 true
7299 }
7300
7301 fn hide_selection_toolbar(&mut self) {
7303 if self.runtime.selection_toolbar.take().is_some() {
7304 self.request_redraw();
7305 }
7306 }
7307
7308 fn perform_edit_action(&mut self, action: frus_widgets::EditAction) {
7313 use frus_widgets::EditAction;
7314 if self
7316 .runtime
7317 .region
7318 .as_ref()
7319 .is_some_and(|selection| selection.bar)
7320 {
7321 match action {
7322 EditAction::Copy => {
7323 self.copy_region();
7325 self.runtime.region = None;
7326 }
7327 EditAction::SelectAll => self.select_all_region(),
7328 EditAction::Cut | EditAction::Paste => {}
7329 }
7330 self.request_redraw();
7331 return;
7332 }
7333 let Some(id) = self.runtime.selection_toolbar.map(|mark| mark.id) else {
7334 return;
7335 };
7336 match action {
7337 EditAction::Copy => {
7338 if self.copy_selection(id) {
7339 if let Some(edit) = self.runtime.edits.get_mut(&id) {
7342 *edit = collapsed_to_end(*edit);
7343 }
7344 self.runtime.close_edit_run(id);
7345 }
7346 self.runtime.selection_handles = None;
7347 self.hide_selection_toolbar();
7348 }
7349 EditAction::Cut => {
7350 self.cut_selection(id);
7352 self.hide_selection_toolbar();
7353 }
7354 EditAction::Paste => {
7355 self.runtime.selection_handles = None;
7356 self.hide_selection_toolbar();
7357 if let Some(pasted) = self.clipboard.paste(id, self.window.as_ref()) {
7359 self.land_paste(pasted);
7360 }
7361 }
7362 EditAction::SelectAll => {
7363 self.select_all(id);
7364 if self.runtime.selection_handles.is_some() {
7367 self.runtime.selection_handles = Some(id);
7368 }
7369 if select_all_hides_bar(frus_widgets::default_target_platform()) {
7370 self.runtime.selection_handles = None;
7373 self.hide_selection_toolbar();
7374 } else {
7375 let at = self.runtime.selection_toolbar.and_then(|mark| mark.at);
7378 self.show_selection_toolbar_at(id, at);
7379 }
7380 }
7381 }
7382 #[cfg(android)]
7383 self.push_ime_context(id);
7384 }
7385
7386 fn context_click(&mut self) {
7390 let secondary = self
7392 .ui
7393 .as_ref()
7394 .and_then(|ui| ui.hit(self.cursor))
7395 .and_then(|id| {
7396 self.tree
7397 .as_ref()
7398 .and_then(|tree| find_widget(tree.as_ref(), id))
7399 })
7400 .and_then(|widget| widget.on_secondary_tap());
7401 if let Some(message) = secondary {
7402 self.hide_selection_toolbar();
7403 self.dispatch(message);
7404 self.request_redraw();
7405 return;
7406 }
7407 let hit = self.ui.as_ref().and_then(|ui| ui.focus_hit(self.cursor));
7408 let Some((id, rect)) = hit else {
7409 self.hide_selection_toolbar();
7410 return;
7411 };
7412 let was_focused = self.runtime.input.focused == Some(id);
7413 let scroll_y = self.runtime.scroll.get(&id).map(|s| s.1).unwrap_or(0.0);
7414 let scroll_cursor = if was_focused {
7415 self.runtime.edits.get(&id).map(|e| e.cursor).unwrap_or(0)
7416 } else {
7417 0
7418 };
7419 let cursor = self
7420 .tree
7421 .as_ref()
7422 .and_then(|tree| find_widget(tree.as_ref(), id))
7423 .and_then(|widget| {
7424 widget.cursor_at(
7425 self.cursor.x - rect.x,
7426 self.cursor.y - rect.y + scroll_y,
7427 rect.width,
7428 scroll_cursor,
7429 )
7430 });
7431 let Some(cursor) = cursor else {
7433 self.hide_selection_toolbar();
7434 return;
7435 };
7436 let inside = was_focused
7437 && self
7438 .runtime
7439 .edits
7440 .get(&id)
7441 .and_then(|edit| edit.selection_range())
7442 .is_some_and(|(start, end)| cursor >= start && cursor <= end);
7443 self.runtime.input.focused = Some(id);
7444 self.runtime.selection_handles = None;
7445 if !inside {
7446 self.goal_x = None;
7447 self.runtime.edits.insert(
7448 id,
7449 Edit {
7450 cursor,
7451 anchor: None,
7452 composing: None,
7453 },
7454 );
7455 self.runtime.close_edit_run(id);
7456 }
7457 self.show_selection_toolbar_at(id, Some(self.cursor));
7458 }
7459
7460 fn land_paste(&mut self, pasted: clip::Pasted) {
7463 if pasted.lands(self.runtime.input.focused) {
7464 self.apply_key(pasted.into, Key::Text(pasted.text));
7465 self.request_redraw();
7466 }
7467 }
7468}
7469
7470fn collapsed_to_end(edit: Edit) -> Edit {
7474 let cursor = edit.selection_range().map_or(edit.cursor, |(_, end)| end);
7475 Edit {
7476 cursor,
7477 anchor: None,
7478 composing: None,
7479 }
7480}
7481
7482#[derive(Clone, Copy, PartialEq, Eq)]
7484enum Edge {
7485 Start,
7486 End,
7487}
7488
7489fn cursor_icon(cursor: UiCursor) -> CursorIcon {
7502 match cursor {
7503 UiCursor::Default | UiCursor::None => CursorIcon::Default,
7504 UiCursor::Pointer => CursorIcon::Pointer,
7505 UiCursor::Text => CursorIcon::Text,
7506 UiCursor::Forbidden => CursorIcon::NotAllowed,
7507 UiCursor::Wait => CursorIcon::Wait,
7508 UiCursor::Progress => CursorIcon::Progress,
7509 UiCursor::Help => CursorIcon::Help,
7510 UiCursor::Precise => CursorIcon::Crosshair,
7511 UiCursor::Move => CursorIcon::Move,
7512 UiCursor::Grab => CursorIcon::Grab,
7513 UiCursor::Grabbing => CursorIcon::Grabbing,
7514 UiCursor::ResizeColumn => CursorIcon::ColResize,
7515 UiCursor::ResizeRow => CursorIcon::RowResize,
7516 UiCursor::ZoomIn => CursorIcon::ZoomIn,
7517 UiCursor::ZoomOut => CursorIcon::ZoomOut,
7518 UiCursor::ContextMenu => CursorIcon::ContextMenu,
7519 }
7520}
7521
7522fn mask_pan(axis: frus_widgets::PanAxis, value: Point) -> Point {
7524 match axis {
7525 frus_widgets::PanAxis::Free => value,
7526 frus_widgets::PanAxis::Horizontal => Point::new(value.x, 0.0),
7527 frus_widgets::PanAxis::Vertical => Point::new(0.0, value.y),
7528 }
7529}
7530
7531fn gesture_was_a_tap(ended: Option<&Drag>) -> bool {
7532 matches!(
7533 ended,
7534 Some(
7535 Drag::Scroll { moved: false, .. }
7536 | Drag::Pan { moved: false, .. }
7537 | Drag::Gesture { moved: false, .. }
7538 | Drag::Reorder { moved: false, .. }
7539 | Drag::Item { moved: false, .. }
7540 | Drag::Dismiss { moved: false, .. }
7541 | Drag::Sheet { moved: false, .. }
7542 )
7543 )
7544}
7545
7546fn press_at<Msg: Clone>(runtime: &mut Runtime, ui: Option<&Ui<Msg>>, at: Point) -> bool {
7550 runtime.input.pressed = ui.and_then(|ui| ui.hit(at));
7551 let Some(pressed) = runtime.input.pressed else {
7552 return false;
7553 };
7554 let Some(rect) = ui.and_then(|ui| ui.ink_box(pressed)) else {
7555 return false;
7556 };
7557 runtime.ink_press(
7558 pressed,
7559 Point::new(at.x - rect.x, at.y - rect.y),
7560 Size::new(rect.width, rect.height),
7561 );
7562 true
7563}
7564
7565fn settle_press(
7583 input: &mut frus_widgets::InputState,
7584 kind: PointerKind,
7585 could_tap: bool,
7586 drag: Option<&Drag>,
7587 touch: bool,
7588) -> bool {
7589 let taken = match kind {
7590 PointerKind::Down => false,
7591 PointerKind::Move => could_tap && !gesture_was_a_tap(drag),
7592 PointerKind::Up | PointerKind::Cancel => true,
7593 };
7594 if !taken {
7595 return false;
7596 }
7597 let mut changed = input.pressed.take().is_some();
7598 if touch {
7599 changed |= input.hovered.take().is_some();
7600 }
7601 changed
7602}
7603
7604const AUTOSCROLL_VELOCITY: f32 = 50.0;
7608
7609const AUTOSCROLL_MAX_OVERHANG: f32 = 20.0;
7613
7614fn apply_scroll_requests<Msg>(
7627 runtime: &mut Runtime,
7628 tree: &dyn Widget<Msg>,
7629 requests: Vec<(u64, ScrollTo)>,
7630 regions: &[Scrollable],
7631) -> (Vec<(u64, ScrollTo)>, bool) {
7632 let mut unplaced = Vec::new();
7633 let mut moved = false;
7634 for (key, to) in requests {
7635 let area = find_by_key(tree, key).and_then(|id| regions.iter().find(|a| a.id == id));
7636 match area {
7637 Some(area) => moved |= runtime.scroll_to(area, to),
7638 None => unplaced.push((key, to)),
7639 }
7640 }
7641 (unplaced, moved)
7642}
7643
7644fn apply_sheet_requests<Msg>(
7651 runtime: &mut Runtime,
7652 tree: &dyn Widget<Msg>,
7653 requests: Vec<(u64, SheetTo)>,
7654 sheets: &[frus_widgets::SheetArea],
7655) -> (Vec<(u64, SheetTo)>, bool) {
7656 let mut unplaced = Vec::new();
7657 let mut moved = false;
7658 for (key, to) in requests {
7659 let sheet = frus_widgets::find_sheet_by_key(tree, key)
7660 .and_then(|id| sheets.iter().find(|sheet| sheet.id == id));
7661 match sheet {
7662 Some(sheet) => moved |= runtime.sheet_to(sheet.id, &sheet.spec, &to),
7663 None => unplaced.push((key, to)),
7664 }
7665 }
7666 (unplaced, moved)
7667}
7668
7669fn follow_content(drag: &mut Drag, shift: (f32, f32)) {
7675 match drag {
7676 Drag::Reorder { start, carried, .. } => {
7677 start.x += shift.0;
7678 start.y += shift.1;
7679 if let Some((_, rect)) = carried {
7680 *rect = rect.translate(shift.0, shift.1);
7681 }
7682 }
7683 Drag::Item { source, start, .. } => {
7684 start.x += shift.0;
7685 start.y += shift.1;
7686 source.rect = source.rect.translate(shift.0, shift.1);
7687 }
7688 _ => {}
7689 }
7690}
7691
7692fn edge_autoscroll(item: (f32, f32), view: (f32, f32), room: (f32, f32), dt: f32) -> Option<f32> {
7705 let before = view.0 - item.0; let after = item.1 - view.1; let overhang = if before > 0.0 && room.0 > 0.0 {
7710 before.min(AUTOSCROLL_MAX_OVERHANG)
7711 } else if after > 0.0 && room.1 > 0.0 {
7712 -after.min(AUTOSCROLL_MAX_OVERHANG)
7713 } else {
7714 return None;
7715 };
7716 let travel = overhang * AUTOSCROLL_VELOCITY * dt;
7717 (travel != 0.0).then_some(travel)
7718}
7719
7720fn spring_toward(current: f32, target: f32, dt: f32, tau: f32) -> f32 {
7724 let k = 1.0 - (-dt / tau.max(1e-4)).exp();
7725 current + (target - current) * k
7726}
7727
7728const FOCUS_HISTORY_MAX: usize = 8;
7730
7731fn resolve_focus(
7739 current: Option<WidgetId>,
7740 present: &std::collections::HashSet<WidgetId>,
7741 history: &mut Vec<WidgetId>,
7742 prev: &mut Option<WidgetId>,
7743) -> Option<WidgetId> {
7744 let mut cur = current;
7745 if let Some(c) = cur {
7746 if !present.contains(&c) {
7747 cur = None;
7748 while let Some(cand) = history.pop() {
7749 if present.contains(&cand) {
7750 cur = Some(cand);
7751 break;
7752 }
7753 }
7754 }
7755 }
7756 if *prev != cur {
7757 if let Some(old) = *prev {
7758 if present.contains(&old) && Some(old) != cur {
7759 history.push(old);
7760 if history.len() > FOCUS_HISTORY_MAX {
7761 history.remove(0);
7762 }
7763 }
7764 }
7765 *prev = cur;
7766 }
7767 cur
7768}
7769
7770fn draw_ghost_card(scene: &mut Scene, theme: &Theme, card: Rect, ghost: &[Primitive]) {
7776 use drag_preview::{BORDER_ALPHA, BORDER_WIDTH, SHADOW_ALPHA, SHADOW_BLUR, SHADOW_OFFSET_Y};
7777 scene.set_clip(Rect::UNBOUNDED);
7778 let shadow = theme.scheme.shadow.with_alpha(SHADOW_ALPHA);
7782 scene.shadow(
7783 card.translate(0.0, SHADOW_OFFSET_Y),
7784 shadow,
7785 theme.radius,
7786 SHADOW_BLUR,
7787 );
7788 let border = theme.primary.fade(BORDER_ALPHA);
7789 if ghost.is_empty() {
7790 scene.draw_rect(card, theme.surface, theme.radius, BORDER_WIDTH, border);
7791 } else {
7792 scene.draw_rect(card, theme.surface, theme.radius, 0.0, Color::TRANSPARENT);
7793 for primitive in ghost {
7794 scene.push_primitive(primitive.clone());
7795 }
7796 scene.draw_rect(card, Color::TRANSPARENT, theme.radius, BORDER_WIDTH, border);
7797 }
7798}
7799
7800#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7802enum ReorderMotion {
7803 Columns,
7806 Slots(ReorderAxis),
7809}
7810
7811impl ReorderMotion {
7812 fn of(axis: ReorderAxis, inserts: bool) -> Self {
7816 match axis {
7817 ReorderAxis::Vertical => Self::Slots(ReorderAxis::Vertical),
7818 ReorderAxis::Horizontal if inserts => Self::Slots(ReorderAxis::Horizontal),
7819 ReorderAxis::Horizontal => Self::Columns,
7820 }
7821 }
7822}
7823
7824fn reorder_motion<Msg>(widget: &dyn Widget<Msg>) -> ReorderMotion {
7826 ReorderMotion::of(widget.reorder_axis(), widget.reorder_inserts())
7827}
7828
7829fn ghost_offset(motion: ReorderMotion, travel: (f32, f32)) -> (f32, f32) {
7838 match motion {
7839 ReorderMotion::Columns => (travel.0, drag_preview::LIFT_Y),
7840 ReorderMotion::Slots(ReorderAxis::Vertical) => travel,
7841 ReorderMotion::Slots(ReorderAxis::Horizontal) => (travel.0, 0.0),
7842 }
7843}
7844
7845fn drop_insertion_line(
7855 target: Rect,
7856 thickness: f32,
7857 after: bool,
7858 axis: ReorderAxis,
7859 rtl: bool,
7860) -> Rect {
7861 match axis {
7862 ReorderAxis::Vertical => {
7863 let edge = if after {
7864 target.y + target.height
7865 } else {
7866 target.y
7867 };
7868 Rect::new(target.x, edge - thickness * 0.5, target.width, thickness)
7869 }
7870 ReorderAxis::Horizontal => {
7871 let edge = if after != rtl {
7872 target.x + target.width
7873 } else {
7874 target.x
7875 };
7876 Rect::new(edge - thickness * 0.5, target.y, thickness, target.height)
7877 }
7878 }
7879}
7880
7881fn claim_axis(dx: f32, dy: f32, can_x: bool, can_y: bool) -> bool {
7890 match (can_x, can_y) {
7891 (true, false) => false,
7892 (false, true) => true,
7893 _ => dy.abs() >= dx.abs(),
7894 }
7895}
7896
7897fn claim_area(
7907 chain: &[frus_widgets::Scrollable],
7908 under: WidgetId,
7909 down: bool,
7910) -> Option<frus_widgets::Scrollable> {
7911 chain
7912 .iter()
7913 .find(|a| a.takes_user_scroll(down))
7917 .or_else(|| {
7918 chain
7919 .iter()
7920 .find(|a| a.id == under && a.physics.user() != frus_widgets::UserScroll::Never)
7921 })
7922 .copied()
7923}
7924
7925#[cfg(feature = "net")]
7940fn fetch_image_bytes(
7941 url: &str,
7942 deliver: Box<dyn FnOnce(Result<Vec<u8>, String>) + Send + 'static>,
7943) {
7944 const DEADLINE: std::time::Duration = std::time::Duration::from_secs(30);
7947
7948 let url = url.to_string();
7949 let run = async move {
7950 let result = crate::net::Request::get(url)
7951 .timeout(DEADLINE)
7952 .send_bytes()
7953 .await;
7954 deliver(result.map_err(|e| e.to_string()));
7955 };
7956 #[cfg(not(web))]
7957 crate::runtime::spawn(run).detach();
7958 #[cfg(web)]
7959 wasm_bindgen_futures::spawn_local(run);
7960}
7961
7962#[cfg(test)]
7963mod tests {
7964 use super::{
7965 build_view, claim_area, claim_axis, draw_ghost_card, drop_insertion_line, fling_velocity,
7966 gesture_was_a_tap, ghost_offset, install_ambient, reorder_motion, resolve_focus,
7967 spring_toward, Drag, Point, Rect, ReorderAxis, ReorderMotion, Scene, Theme,
7968 VelocityEstimate, PRECISE_SLOP, TOUCH_SLOP,
7969 };
7970 use super::{clipboard_command, ClipCommand, KeyCode, PhysicalKey, WinitKey};
7971 use super::{collect_ids, find_widget, MediaQuery};
7972 use frus_widgets::Locale;
7973
7974 #[test]
7977 fn select_all_hides_the_bar_on_the_desktops() {
7978 use frus_widgets::TargetPlatform as P;
7979 for p in [P::MacOs, P::Linux, P::Windows] {
7980 assert!(super::select_all_hides_bar(p), "{p:?}");
7981 }
7982 for p in [P::Android, P::Ios, P::Fuchsia] {
7983 assert!(!super::select_all_hides_bar(p), "{p:?}");
7984 }
7985 }
7986
7987 #[test]
7991 fn the_context_menu_key_and_shift_f10_open_the_bar() {
7992 use super::opens_selection_bar;
7993 assert!(opens_selection_bar(
7994 &WinitKey::Named(winit::keyboard::NamedKey::ContextMenu),
7995 false
7996 ));
7997 assert!(opens_selection_bar(
7998 &WinitKey::Named(winit::keyboard::NamedKey::F10),
7999 true
8000 ));
8001 assert!(!opens_selection_bar(
8002 &WinitKey::Named(winit::keyboard::NamedKey::F10),
8003 false
8004 ));
8005 assert!(!opens_selection_bar(
8006 &WinitKey::Named(winit::keyboard::NamedKey::Enter),
8007 true
8008 ));
8009 assert!(!opens_selection_bar(&character("m"), true));
8010 }
8011
8012 #[test]
8016 fn a_copy_collapses_the_selection_to_its_end() {
8017 use frus_widgets::Edit;
8018 let forwards = Edit {
8019 cursor: 11,
8020 anchor: Some(6),
8021 composing: None,
8022 };
8023 let backwards = Edit {
8024 cursor: 6,
8025 anchor: Some(11),
8026 composing: None,
8027 };
8028 let expect = Edit {
8029 cursor: 11,
8030 anchor: None,
8031 composing: None,
8032 };
8033 assert_eq!(super::collapsed_to_end(forwards), expect);
8034 assert_eq!(super::collapsed_to_end(backwards), expect);
8035 let caret = Edit {
8037 cursor: 3,
8038 anchor: None,
8039 composing: None,
8040 };
8041 assert_eq!(super::collapsed_to_end(caret), caret);
8042 let empty = Edit {
8044 cursor: 3,
8045 anchor: Some(3),
8046 composing: Some((1, 2)),
8047 };
8048 assert_eq!(super::collapsed_to_end(empty).anchor, None);
8049 assert_eq!(super::collapsed_to_end(empty).cursor, 3);
8050 }
8051
8052 #[test]
8058 fn a_field_with_a_clear_button_still_wants_the_keyboard() {
8059 use frus_widgets::{Container, Icons, TextField};
8060 let clearable = TextField::<()>::new("F")
8061 .suffix_icon(Icons::CLOSE)
8062 .on_suffix(());
8063 assert!(
8064 super::wants_keyboard(&clearable),
8065 "the field with its clear button"
8066 );
8067 assert!(
8068 !super::wants_keyboard(&TextField::<()>::new("F").enabled(false)),
8069 "a disabled field takes no typing"
8070 );
8071 assert!(
8072 !super::wants_keyboard(&Container::<()>::new()),
8073 "and what is not a field wants no keyboard"
8074 );
8075 }
8076
8077 #[test]
8084 fn a_field_with_a_clear_button_is_a_text_field_and_keeps_its_arrows() {
8085 use frus_widgets::{Container, Icons, TextField};
8086 let clearable = TextField::<()>::new("hello")
8087 .suffix_icon(Icons::CLOSE)
8088 .on_suffix(());
8089 assert!(
8090 frus_widgets::Widget::<()>::cursor_at(&clearable, 0.0, 0.0, 1.0, 0).is_none(),
8091 "the trap: the probe finds no caret at the corner a clear button covers"
8092 );
8093 assert!(
8094 super::is_text_field(&clearable),
8095 "and it is a text field all the same"
8096 );
8097 assert!(
8098 super::is_text_field(&TextField::<()>::new("")),
8099 "empty, too"
8100 );
8101 assert!(
8102 !super::is_text_field(&Container::<()>::new()),
8103 "what is not a field leaves its arrows to focus navigation"
8104 );
8105 }
8106
8107 #[test]
8113 fn a_text_that_can_only_be_read_wants_no_keyboard() {
8114 use frus_widgets::{Text, TextField};
8115 let read_only = TextField::<()>::new("REF-4417").read_only();
8116 assert!(super::is_text_field(&read_only), "it has a caret");
8117 assert!(
8118 !super::wants_keyboard(&read_only),
8119 "and no use for a keyboard"
8120 );
8121
8122 let selectable = Text::new("ORDER-4417").selectable();
8123 assert!(
8124 super::is_text_field::<()>(&selectable),
8125 "the arrows move its caret"
8126 );
8127 assert!(!super::wants_keyboard::<()>(&selectable));
8128
8129 assert!(
8130 super::wants_keyboard(&TextField::<()>::new("typed")),
8131 "and a field that can be typed into still does"
8132 );
8133 assert!(
8134 !super::is_text_field::<()>(&Text::new("ORDER-4417")),
8135 "an ordinary text leaves its arrows to focus navigation"
8136 );
8137 }
8138
8139 fn character(c: &str) -> WinitKey {
8140 WinitKey::Character(c.into())
8141 }
8142
8143 #[test]
8146 fn ctrl_c_x_v_are_the_clipboard_and_the_letters_alone_are_not() {
8147 let any = PhysicalKey::Code(KeyCode::KeyQ);
8148 for (letter, command) in [
8149 ("c", ClipCommand::Copy),
8150 ("X", ClipCommand::Cut),
8151 ("v", ClipCommand::Paste),
8152 ] {
8153 assert_eq!(
8154 clipboard_command(&character(letter), any, true),
8155 Some(command)
8156 );
8157 assert_eq!(
8158 clipboard_command(&character(letter), any, false),
8159 None,
8160 "{letter}"
8161 );
8162 }
8163 assert_eq!(clipboard_command(&character("a"), any, true), None);
8164 }
8165
8166 #[test]
8170 fn a_keyboards_own_clipboard_keys_work_by_their_physical_code() {
8171 let unidentified = WinitKey::Unidentified(winit::keyboard::NativeKey::Unidentified);
8172 for (code, command) in [
8173 (KeyCode::Copy, ClipCommand::Copy),
8174 (KeyCode::Cut, ClipCommand::Cut),
8175 (KeyCode::Paste, ClipCommand::Paste),
8176 ] {
8177 assert_eq!(
8178 clipboard_command(&unidentified, PhysicalKey::Code(code), false),
8179 Some(command),
8180 "{code:?}"
8181 );
8182 }
8183 let named = WinitKey::Named(super::NamedKey::Paste);
8185 let any = PhysicalKey::Code(KeyCode::KeyQ);
8186 assert_eq!(
8187 clipboard_command(&named, any, false),
8188 Some(ClipCommand::Paste)
8189 );
8190 }
8191
8192 #[test]
8198 fn a_paste_lands_in_the_field_that_asked_only_while_it_has_the_focus() {
8199 use super::clip::Pasted;
8200 let field = WidgetId::from_u64(1);
8201 let other = WidgetId::from_u64(2);
8202 let pasted = Pasted {
8203 into: field,
8204 text: "copied".into(),
8205 };
8206 assert!(pasted.lands(Some(field)));
8207 assert!(
8208 !pasted.lands(Some(other)),
8209 "the focus moved to another field"
8210 );
8211 assert!(!pasted.lands(None), "the focus left every field");
8212 let nothing = Pasted {
8213 into: field,
8214 text: String::new(),
8215 };
8216 assert!(!nothing.lands(Some(field)), "an empty clipboard");
8217 }
8218
8219 #[test]
8224 fn a_paste_answered_later_waits_for_the_frame_that_takes_it() {
8225 use super::clip::{Answers, Pasted};
8226 use std::cell::Cell;
8227 use std::rc::Rc;
8228 let first = WidgetId::from_u64(1);
8229 let second = WidgetId::from_u64(2);
8230 let answers = Answers::default();
8231 let wakes = Rc::new(Cell::new(0));
8232 let wake = || {
8233 let wakes = Rc::clone(&wakes);
8234 move || wakes.set(wakes.get() + 1)
8235 };
8236 let to_first = answers.answer_for(first, wake());
8237 let to_second = answers.answer_for(second, wake());
8238 assert_eq!(answers.take(), None, "nothing is answered yet");
8239 assert_eq!(wakes.get(), 0, "asking is not answering");
8240
8241 to_second("later".into());
8242 assert_eq!(wakes.get(), 1);
8243 to_first("sooner".into());
8244 assert_eq!(wakes.get(), 2);
8245
8246 assert_eq!(
8247 answers.take(),
8248 Some(Pasted {
8249 into: second,
8250 text: "later".into()
8251 })
8252 );
8253 assert_eq!(
8254 answers.take(),
8255 Some(Pasted {
8256 into: first,
8257 text: "sooner".into()
8258 })
8259 );
8260 assert_eq!(answers.take(), None, "each answer is taken once");
8261 }
8262
8263 struct Deferred;
8272
8273 impl crate::Application for Deferred {
8274 type Message = ();
8275
8276 fn update(&mut self, _message: ()) -> crate::Command<()> {
8277 crate::Command::none()
8278 }
8279
8280 fn view(&self, _theme: &Theme) -> Box<dyn frus_widgets::Widget<()>> {
8281 Box::new(frus_widgets::ThemeBuilder::new(|_: &Theme| {
8282 frus_widgets::Flex::column()
8283 .width(300.0)
8284 .height(80.0)
8285 .child(
8286 frus_widgets::TextField::new("hi")
8287 .width(200.0)
8288 .on_input(|_| ()),
8289 )
8290 }))
8291 }
8292 }
8293
8294 fn surfaced<R>(f: impl FnOnce() -> R) -> R {
8297 MediaQuery::new(frus_widgets::Size::new(300.0, 80.0)).scope(f)
8298 }
8299
8300 #[test]
8310 fn a_shell_installs_the_reader_s_language() {
8311 struct Speaks(Option<Locale>);
8314
8315 impl crate::Application for Speaks {
8316 type Message = ();
8317
8318 fn update(&mut self, _message: ()) -> crate::Command<()> {
8319 crate::Command::none()
8320 }
8321
8322 fn view(&self, _theme: &Theme) -> Box<dyn frus_widgets::Widget<()>> {
8323 Box::new(frus_widgets::Flex::<()>::column())
8324 }
8325
8326 fn supported_locales(&self) -> Vec<Locale> {
8327 vec![
8328 Locale::new("en"),
8329 Locale::new("fr"),
8330 Locale::with_country("fr", "CA"),
8331 ]
8332 }
8333
8334 fn locale(&self) -> Option<Locale> {
8335 self.0.clone()
8336 }
8337 }
8338
8339 install_ambient(&Speaks(None), &[Locale::with_country("fr", "BE")]);
8341 assert_eq!(frus_widgets::locale::of(), Locale::new("fr"));
8342
8343 install_ambient(&Speaks(None), &[]);
8346 assert_eq!(frus_widgets::locale::of(), Locale::new("en"));
8347
8348 install_ambient(
8351 &Speaks(Some(Locale::with_country("fr", "FR"))),
8352 &[Locale::new("en")],
8353 );
8354 assert_eq!(frus_widgets::locale::of(), Locale::new("fr"));
8355
8356 install_ambient(&Deferred, &[Locale::new("fr")]);
8358 assert_eq!(
8359 frus_widgets::locale::of(),
8360 Locale::new("en"),
8361 "it supports only English, so French resolves to it"
8362 );
8363 }
8364
8365 #[test]
8373 fn a_shell_installs_the_application_s_words() {
8374 struct Fr;
8375
8376 impl frus_widgets::localizations::Localizations for Fr {
8377 fn back_button_label(&self) -> &str {
8378 "Retour"
8379 }
8380
8381 fn first_day_of_week_index(&self) -> usize {
8382 1
8383 }
8384 }
8385
8386 struct Speaks;
8387
8388 impl crate::Application for Speaks {
8389 type Message = ();
8390
8391 fn update(&mut self, _message: ()) -> crate::Command<()> {
8392 crate::Command::none()
8393 }
8394
8395 fn view(&self, _theme: &Theme) -> Box<dyn frus_widgets::Widget<()>> {
8396 Box::new(frus_widgets::Flex::<()>::column())
8397 }
8398
8399 fn localizations(&self) -> Option<std::rc::Rc<dyn frus_widgets::Localizations>> {
8400 Some(std::rc::Rc::new(Fr))
8401 }
8402 }
8403
8404 install_ambient(&Speaks, &[]);
8405 assert_eq!(
8406 frus_widgets::localizations::of().back_button_label(),
8407 "Retour",
8408 "the shell read the application's table and installed it"
8409 );
8410 assert_eq!(
8411 frus_widgets::localizations::of().first_day_of_week_index(),
8412 1
8413 );
8414
8415 install_ambient(&Deferred, &[]);
8418 assert_eq!(
8419 frus_widgets::localizations::of().back_button_label(),
8420 "Retour",
8421 "saying nothing is not the same as saying English"
8422 );
8423 }
8424
8425 #[test]
8428 fn the_scroll_behaviour_at_the_root_is_the_applications() {
8429 struct Quiet;
8430
8431 impl crate::Application for Quiet {
8432 type Message = ();
8433
8434 fn update(&mut self, _message: ()) -> crate::Command<()> {
8435 crate::Command::none()
8436 }
8437
8438 fn view(&self, _theme: &Theme) -> Box<dyn frus_widgets::Widget<()>> {
8439 Box::new(frus_widgets::Flex::<()>::column())
8440 }
8441
8442 fn scroll_behavior(&self) -> frus_widgets::ScrollBehavior {
8443 frus_widgets::ScrollBehavior::material().with_overscroll(false)
8444 }
8445 }
8446
8447 struct Silent;
8448
8449 impl crate::Application for Silent {
8450 type Message = ();
8451
8452 fn update(&mut self, _message: ()) -> crate::Command<()> {
8453 crate::Command::none()
8454 }
8455
8456 fn view(&self, _theme: &Theme) -> Box<dyn frus_widgets::Widget<()>> {
8457 Box::new(frus_widgets::Flex::<()>::column())
8458 }
8459 }
8460
8461 install_ambient(&Quiet, &[]);
8462 assert_eq!(
8463 frus_widgets::ScrollConfiguration::of(),
8464 frus_widgets::ScrollBehavior::material().with_overscroll(false)
8465 );
8466 install_ambient(&Silent, &[]);
8467 assert_eq!(
8468 frus_widgets::ScrollConfiguration::of(),
8469 frus_widgets::ScrollBehavior::material()
8470 );
8471 }
8472
8473 #[test]
8484 fn a_view_is_built_ready_to_be_read() {
8485 let theme = Theme::default();
8486 let prepared =
8487 surfaced(|| build_view(&Deferred, &theme, &frus_widgets::Runtime::default()));
8488 let ids = collect_ids(prepared.as_ref());
8489 assert!(
8490 ids.len() > 1,
8491 "an application inside a deferred subtree has identities to mount: {ids:?}"
8492 );
8493 for id in &ids {
8495 assert!(
8496 find_widget(prepared.as_ref(), *id).is_some(),
8497 "{id:?} was counted but cannot be found"
8498 );
8499 }
8500 }
8501
8502 #[test]
8504 #[should_panic(expected = "before it was built")]
8505 fn an_unprepared_view_is_the_state_that_broke() {
8506 let theme = Theme::default();
8507 let raw = surfaced(|| crate::Application::view(&Deferred, &theme));
8508 let _ = collect_ids(raw.as_ref());
8509 }
8510
8511 #[test]
8519 fn a_swipe_that_never_started_is_still_a_tap() {
8520 let item = frus_widgets::Dismissable {
8521 id: WidgetId::from_u64(1),
8522 rect: Rect::new(0.0, 0.0, 300.0, 62.0),
8523 spec: frus_widgets::DismissSpec::default(),
8524 };
8525 let dismiss = |moved| Drag::Dismiss {
8526 item,
8527 last: Point::new(0.0, 0.0),
8528 moved,
8529 };
8530 assert!(
8531 gesture_was_a_tap(Some(&dismiss(false))),
8532 "a swipe under the threshold leaves the click alone"
8533 );
8534 assert!(
8535 !gesture_was_a_tap(Some(&dismiss(true))),
8536 "a swipe that ran has already been answered and must not also click"
8537 );
8538 let scroll = Drag::Scroll {
8541 id: WidgetId::from_u64(2),
8542 last: Point::new(0.0, 0.0),
8543 moved: false,
8544 carried: (0.0, 0.0),
8545 dismiss: None,
8546 axis: None,
8547 pan: None,
8548 };
8549 assert!(gesture_was_a_tap(Some(&scroll)));
8550 let sheet = |moved| Drag::Sheet {
8553 id: WidgetId::from_u64(4),
8554 last: Point::new(0.0, 0.0),
8555 moved,
8556 available: 800.0,
8557 };
8558 assert!(gesture_was_a_tap(Some(&sheet(false))));
8559 assert!(!gesture_was_a_tap(Some(&sheet(true))));
8560 let select = Drag::TextSelect {
8563 id: WidgetId::from_u64(3),
8564 rect: Rect::new(0.0, 0.0, 10.0, 10.0),
8565 };
8566 assert!(!gesture_was_a_tap(Some(&select)));
8567 assert!(!gesture_was_a_tap(None), "no gesture, no verdict to give");
8568 }
8569
8570 #[test]
8574 fn a_scroll_gesture_is_claimed_by_one_axis() {
8575 assert!(claim_axis(6.0, 40.0, true, true), "down the page");
8578 assert!(!claim_axis(40.0, 6.0, true, true), "across it");
8579 assert!(claim_axis(10.0, 10.0, true, true));
8581 assert!(claim_axis(40.0, 6.0, false, true), "a list only goes down");
8584 assert!(
8585 !claim_axis(6.0, 40.0, true, false),
8586 "a strip only goes across"
8587 );
8588 }
8589
8590 fn area(id: u64, max_x: f32, max_y: f32) -> frus_widgets::Scrollable {
8592 frus_widgets::Scrollable {
8593 id: WidgetId::from_u64(id),
8594 viewport: Rect::new(0.0, 0.0, 100.0, 100.0),
8595 max_x,
8596 max_y,
8597 axis: frus_widgets::Axis::Vertical,
8598 physics: frus_widgets::ScrollPhysics::CLAMPING,
8599 overscroll: Some(frus_widgets::OverscrollIndicator::Glow),
8600 fling: frus_widgets::VelocityStrategy::Regression,
8601 refresh: None,
8602 page: None,
8603 reverse_x: false,
8604 reverse_y: false,
8605 host: None,
8606 keep_visible: None,
8607 }
8608 }
8609
8610 #[test]
8614 fn the_gesture_goes_to_the_area_that_can_take_it() {
8615 let strip = area(1, 400.0, 0.0);
8616 let page = area(2, 0.0, 900.0);
8617 let chain = [strip, page]; assert_eq!(
8619 claim_area(&chain, strip.id, true).map(|a| a.id),
8620 Some(page.id),
8621 "down the page, over the strip"
8622 );
8623 assert_eq!(
8624 claim_area(&chain, strip.id, false).map(|a| a.id),
8625 Some(strip.id),
8626 "across the strip"
8627 );
8628 assert_eq!(
8631 claim_area(&[strip], strip.id, true).map(|a| a.id),
8632 Some(strip.id)
8633 );
8634 let inner = area(3, 0.0, 200.0);
8636 assert_eq!(
8637 claim_area(&[inner, strip, page], inner.id, true).map(|a| a.id),
8638 Some(inner.id)
8639 );
8640 }
8641
8642 #[test]
8646 fn the_physics_have_the_last_word_on_a_finger() {
8647 let mut inner = area(1, 0.0, 300.0);
8648 inner.physics = frus_widgets::ScrollPhysics::NEVER;
8649 assert!(!inner.accepts_user_offset((0.0, 0.0)));
8650 assert!(!inner.takes_user_scroll(true));
8651 let outer = area(2, 0.0, 600.0);
8652 let chain = [inner, outer];
8653 assert_eq!(
8654 claim_area(&chain, WidgetId::from_u64(1), true).map(|a| a.id),
8655 Some(WidgetId::from_u64(2)),
8656 "the page around it scrolls"
8657 );
8658 assert_eq!(
8659 claim_area(&[inner], WidgetId::from_u64(1), true).map(|a| a.id),
8660 None,
8661 "and alone it keeps nothing"
8662 );
8663
8664 let mut short = area(3, 0.0, 0.0);
8665 short.physics = frus_widgets::ScrollPhysics::ALWAYS;
8666 assert!(short.accepts_user_offset((0.0, 0.0)));
8667 assert!(short.takes_user_scroll(true), "along its axis");
8668 assert!(!short.takes_user_scroll(false), "and not across it");
8669 assert_eq!(
8670 claim_area(&[short, outer], WidgetId::from_u64(3), true).map(|a| a.id),
8671 Some(WidgetId::from_u64(3)),
8672 "a short list that always scrolls keeps the drag from the page"
8673 );
8674 let mut strip = short;
8675 strip.axis = frus_widgets::Axis::Horizontal;
8676 assert_eq!(
8677 claim_area(&[strip, outer], WidgetId::from_u64(3), true).map(|a| a.id),
8678 Some(WidgetId::from_u64(2)),
8679 "a strip that always scrolls across leaves a drag down to the page"
8680 );
8681 let mut both = short;
8682 both.axis = frus_widgets::Axis::Both;
8683 assert!(both.takes_user_scroll(false) && both.takes_user_scroll(true));
8684 }
8685
8686 #[test]
8690 fn an_area_whose_content_fits_takes_no_finger() {
8691 let fits = area(1, 0.0, 0.0);
8692 assert!(!fits.accepts_user_offset((0.0, 0.0)));
8693 assert!(fits.accepts_user_offset((0.0, 12.0)));
8696 let mut pullable = fits;
8698 pullable.refresh = Some(WidgetId::from_u64(9));
8699 assert!(pullable.accepts_user_offset((0.0, 0.0)));
8700 assert!(area(1, 0.0, 1.0).accepts_user_offset((0.0, 0.0)));
8702 assert!(area(1, 1.0, 0.0).accepts_user_offset((0.0, 0.0)));
8703 }
8704 use frus_widgets::WidgetId;
8705 use std::collections::HashSet;
8706
8707 fn released(velocity: (f32, f32), offset: (f32, f32)) -> VelocityEstimate {
8709 VelocityEstimate {
8710 velocity: frus_widgets::Velocity::new(velocity.0, velocity.1),
8711 confidence: 1.0,
8712 duration: 0.05,
8713 offset,
8714 }
8715 }
8716
8717 #[test]
8718 fn a_fast_twitch_that_went_nowhere_is_not_a_fling() {
8719 let (x, y) = fling_velocity(released((0.0, 2000.0), (0.0, 3.0)), TOUCH_SLOP);
8721 assert_eq!((x, y), (0.0, 0.0));
8722 let (_, y) = fling_velocity(released((0.0, 2000.0), (0.0, 60.0)), TOUCH_SLOP);
8724 assert_eq!(y, 2000.0);
8725 }
8726
8727 #[test]
8728 fn the_fling_gate_is_per_axis() {
8729 let (x, y) = fling_velocity(released((300.0, 1500.0), (5.0, 120.0)), TOUCH_SLOP);
8732 assert_eq!(x, 0.0, "the wobble is not a horizontal fling");
8733 assert_eq!(y, 1500.0, "the swipe still flings vertically");
8734 }
8735
8736 #[test]
8737 fn a_precise_pointer_needs_almost_no_travel() {
8738 let estimate = released((0.0, 900.0), (0.0, 3.0));
8740 assert_eq!(fling_velocity(estimate, TOUCH_SLOP).1, 0.0);
8741 assert_eq!(fling_velocity(estimate, PRECISE_SLOP).1, 900.0);
8742 }
8743
8744 #[test]
8745 fn insertion_line_sits_on_the_target_top_edge() {
8746 let line = drop_insertion_line(
8749 Rect::new(20.0, 100.0, 200.0, 44.0),
8750 4.0,
8751 false,
8752 ReorderAxis::Vertical,
8753 false,
8754 );
8755 assert_eq!(line.x, 20.0, "aligned to the target's left");
8756 assert_eq!(line.width, 200.0, "the target's full width");
8757 assert_eq!(line.y, 98.0, "centred on the top edge (100 - 4/2)");
8758 assert_eq!(line.height, 4.0);
8759 }
8760
8761 #[test]
8762 fn insertion_line_sits_on_the_target_bottom_edge_when_inserting_after() {
8763 let line = drop_insertion_line(
8766 Rect::new(20.0, 100.0, 200.0, 44.0),
8767 4.0,
8768 true,
8769 ReorderAxis::Vertical,
8770 false,
8771 );
8772 assert_eq!(line.x, 20.0, "still aligned to the left");
8773 assert_eq!(line.width, 200.0, "the target's full width");
8774 assert_eq!(line.y, 142.0, "centred on the bottom edge (144 - 4/2)");
8775 assert_eq!(line.height, 4.0);
8776 let rtl = drop_insertion_line(
8778 Rect::new(20.0, 100.0, 200.0, 44.0),
8779 4.0,
8780 true,
8781 ReorderAxis::Vertical,
8782 true,
8783 );
8784 assert_eq!(rtl, line, "right to left changes nothing down a list");
8785 }
8786
8787 #[test]
8791 fn a_horizontal_insertion_line_stands_on_the_edge_the_row_goes_to() {
8792 let chip = Rect::new(100.0, 20.0, 80.0, 48.0);
8793 let line = |after, rtl| drop_insertion_line(chip, 4.0, after, ReorderAxis::Horizontal, rtl);
8794 let before = line(false, false);
8795 assert_eq!(
8796 (before.x, before.y, before.width, before.height),
8797 (98.0, 20.0, 4.0, 48.0),
8798 "before a row: its left edge, the full height, 4 wide"
8799 );
8800 assert_eq!(
8801 line(true, false).x,
8802 178.0,
8803 "after it: its right edge (180 - 4/2)"
8804 );
8805 assert_eq!(
8807 line(true, true).x,
8808 98.0,
8809 "after, right to left: the left edge"
8810 );
8811 assert_eq!(
8812 line(false, true).x,
8813 178.0,
8814 "before, right to left: the right edge"
8815 );
8816 }
8817
8818 #[test]
8822 fn a_horizontal_reorderable_inserts_only_when_it_says_so() {
8823 assert_eq!(
8824 ReorderMotion::of(ReorderAxis::Horizontal, false),
8825 ReorderMotion::Columns,
8826 "a table's column"
8827 );
8828 assert_eq!(
8829 ReorderMotion::of(ReorderAxis::Horizontal, true),
8830 ReorderMotion::Slots(ReorderAxis::Horizontal),
8831 "a horizontal list's row"
8832 );
8833 for inserts in [false, true] {
8835 assert_eq!(
8836 ReorderMotion::of(ReorderAxis::Vertical, inserts),
8837 ReorderMotion::Slots(ReorderAxis::Vertical)
8838 );
8839 }
8840 assert_eq!(
8842 reorder_motion::<()>(&frus_widgets::Container::new()),
8843 ReorderMotion::Columns
8844 );
8845 for (axis, expected) in [
8847 (
8848 ReorderAxis::Vertical,
8849 ReorderMotion::Slots(ReorderAxis::Vertical),
8850 ),
8851 (
8852 ReorderAxis::Horizontal,
8853 ReorderMotion::Slots(ReorderAxis::Horizontal),
8854 ),
8855 ] {
8856 let list = frus_widgets::ReorderableList::<()>::new(|_, _| ())
8857 .axis(axis)
8858 .row(frus_widgets::Container::new().width(60.0));
8859 let row = frus_widgets::Widget::children(&list)[0].as_ref();
8860 assert_eq!(reorder_motion(row), expected, "{axis:?}");
8861 }
8862 }
8863
8864 #[test]
8868 fn a_horizontal_rows_ghost_follows_the_finger_along_x_only() {
8869 let travel = (120.0, 35.0);
8870 assert_eq!(
8871 ghost_offset(ReorderMotion::Columns, travel),
8872 (120.0, super::drag_preview::LIFT_Y)
8873 );
8874 assert_eq!(
8875 ghost_offset(ReorderMotion::Slots(ReorderAxis::Vertical), travel),
8876 (120.0, 35.0)
8877 );
8878 assert_eq!(
8879 ghost_offset(ReorderMotion::Slots(ReorderAxis::Horizontal), travel),
8880 (120.0, 0.0)
8881 );
8882 }
8883
8884 #[test]
8885 fn focus_returns_to_trigger_when_overlay_closes() {
8886 let anchor = WidgetId::from_u64(1);
8887 let item = WidgetId::from_u64(2);
8888 let (mut history, mut prev) = (Vec::new(), None);
8889
8890 let f = resolve_focus(
8892 Some(anchor),
8893 &HashSet::from([anchor]),
8894 &mut history,
8895 &mut prev,
8896 );
8897 assert_eq!(f, Some(anchor));
8898 assert!(history.is_empty());
8899
8900 let present = HashSet::from([anchor, item]);
8902 let f = resolve_focus(Some(item), &present, &mut history, &mut prev);
8903 assert_eq!(f, Some(item));
8904 assert_eq!(history, vec![anchor]);
8905
8906 let f = resolve_focus(
8908 Some(item),
8909 &HashSet::from([anchor]),
8910 &mut history,
8911 &mut prev,
8912 );
8913 assert_eq!(f, Some(anchor), "focus returns to the trigger");
8914 assert!(history.is_empty());
8915 }
8916
8917 #[test]
8918 fn focus_falls_to_none_when_no_trigger_remains() {
8919 let a = WidgetId::from_u64(1);
8920 let (mut history, mut prev) = (Vec::new(), Some(a));
8921 let f = resolve_focus(Some(a), &HashSet::new(), &mut history, &mut prev);
8923 assert_eq!(f, None);
8924 assert_eq!(prev, None);
8925 }
8926
8927 #[test]
8928 fn spring_approaches_target_monotonically_and_settles() {
8929 let mut x = 0.0;
8932 let mut prev = -1.0;
8933 for _ in 0..30 {
8934 x = spring_toward(x, 100.0, 0.016, 0.07);
8935 assert!(x > prev && x <= 100.0, "monotonic and bounded: {x}");
8936 prev = x;
8937 }
8938 assert!(x > 99.0, "all but reached after ~0.5 s: {x}");
8939 }
8940
8941 #[test]
8942 fn ghost_card_shape() {
8943 let theme = Theme::default();
8944 let card = Rect::new(140.0, 0.0, 80.0, 34.0);
8945 let mut scene = Scene::new();
8947 draw_ghost_card(&mut scene, &theme, card, &[]);
8948 assert_eq!(scene.primitives().len(), 2, "shadow plus solid card");
8949 }
8950}
8951
8952#[cfg(test)]
8955mod sheet_request_tests {
8956 use super::{apply_sheet_requests, Runtime, Widget};
8957 use crate::command::Command;
8958 use frus_widgets::{
8959 build_ui, keyed, Container, DraggableScrollableSheet, SheetTo, Size, Theme,
8960 };
8961
8962 fn view() -> Box<dyn Widget<()>> {
8965 Box::new(
8966 Container::<()>::new()
8967 .width(400.0)
8968 .height(800.0)
8969 .child(keyed(
8970 "places",
8971 DraggableScrollableSheet::<()>::new(Container::new()),
8972 )),
8973 )
8974 }
8975
8976 #[test]
8979 fn a_request_reaches_the_sheet_the_view_named() {
8980 let tree = view();
8981 let mut runtime = Runtime::default();
8982 let sheets = build_ui(
8983 tree.as_ref(),
8984 Size::new(400.0, 800.0),
8985 &runtime,
8986 &Theme::default(),
8987 )
8988 .sheets()
8989 .to_vec();
8990 let sheet = sheets.first().expect("the page has its sheet").clone();
8991
8992 let requests = Command::<()>::sheet("places", SheetTo::size(0.9))
8993 .into_parts()
8994 .sheets;
8995 let (unplaced, moved) =
8996 apply_sheet_requests(&mut runtime, tree.as_ref(), requests, &sheets);
8997 assert!(unplaced.is_empty());
8998 assert!(moved);
8999 assert_eq!(runtime.sheet_size(sheet.id, &sheet.spec), 0.9);
9000
9001 let elsewhere = Command::<()>::sheet("elsewhere", SheetTo::size(0.3))
9002 .into_parts()
9003 .sheets;
9004 let (unplaced, moved) =
9005 apply_sheet_requests(&mut runtime, tree.as_ref(), elsewhere, &sheets);
9006 assert_eq!(unplaced.len(), 1);
9007 assert!(!moved);
9008 let not_yet = Command::<()>::sheet("places", SheetTo::size(0.3))
9009 .into_parts()
9010 .sheets;
9011 let (unplaced, _) = apply_sheet_requests(&mut runtime, tree.as_ref(), not_yet, &[]);
9012 assert_eq!(unplaced.len(), 1, "kept for the frame that has the sheet");
9013 assert_eq!(
9014 runtime.sheet_size(sheet.id, &sheet.spec),
9015 0.9,
9016 "nothing moved"
9017 );
9018 }
9019}
9020
9021#[cfg(test)]
9022mod scroll_request_tests {
9023 use super::{apply_scroll_requests, Rect, Runtime, Scrollable, Widget, WidgetId};
9024 use crate::command::Command;
9025 use frus_widgets::{keyed, Container, ScrollTo, SingleChildScrollView};
9026
9027 fn view() -> Box<dyn Widget<()>> {
9029 Box::new(Container::<()>::new().child(keyed(
9030 "log",
9031 SingleChildScrollView::<()>::new().height(200.0),
9032 )))
9033 }
9034
9035 fn region(id: WidgetId) -> Scrollable {
9037 Scrollable {
9038 id,
9039 viewport: Rect {
9040 x: 0.0,
9041 y: 0.0,
9042 width: 300.0,
9043 height: 200.0,
9044 },
9045 max_x: 0.0,
9046 max_y: 1000.0,
9047 axis: frus_widgets::Axis::Vertical,
9048 physics: frus_widgets::ScrollPhysics::CLAMPING,
9049 overscroll: Some(frus_widgets::OverscrollIndicator::Glow),
9050 fling: frus_widgets::VelocityStrategy::Regression,
9051 refresh: None,
9052 page: None,
9053 reverse_x: false,
9054 reverse_y: false,
9055 keep_visible: None,
9056 host: None,
9057 }
9058 }
9059
9060 #[test]
9064 fn a_request_reaches_the_region_the_view_named() {
9065 let tree = view();
9066 let requests = Command::<()>::scroll("log", ScrollTo::end().instant())
9067 .into_parts()
9068 .scrolls;
9069 let id = frus_widgets::find_by_key(tree.as_ref(), requests[0].0)
9070 .expect("the view named it, so the tree knows it");
9071 let mut runtime = Runtime::default();
9072 let (unplaced, moved) =
9073 apply_scroll_requests(&mut runtime, tree.as_ref(), requests, &[region(id)]);
9074 assert!(unplaced.is_empty());
9075 assert!(moved);
9076 assert_eq!(runtime.scroll.get(&id), Some(&(0.0, 1000.0)));
9077 }
9078
9079 #[test]
9084 fn a_region_this_registry_has_not_got_is_handed_back() {
9085 let tree = view();
9086 let requests = Command::<()>::scroll("log", ScrollTo::y(120.0))
9087 .into_parts()
9088 .scrolls;
9089 let mut runtime = Runtime::default();
9090 let (unplaced, moved) = apply_scroll_requests(&mut runtime, tree.as_ref(), requests, &[]);
9091 assert_eq!(unplaced.len(), 1, "kept for the second attempt");
9092 assert!(!moved);
9093 assert!(runtime.scroll_target.is_empty());
9094 }
9095
9096 #[test]
9099 fn a_name_the_view_does_not_use_moves_nothing() {
9100 let tree = view();
9101 let requests = Command::<()>::scroll("ledger", ScrollTo::start())
9102 .into_parts()
9103 .scrolls;
9104 let mut runtime = Runtime::default();
9105 let id = frus_widgets::find_by_key(
9106 tree.as_ref(),
9107 Command::<()>::scroll("log", ScrollTo::start())
9108 .into_parts()
9109 .scrolls[0]
9110 .0,
9111 )
9112 .expect("the real one is there");
9113 let (unplaced, moved) =
9114 apply_scroll_requests(&mut runtime, tree.as_ref(), requests, &[region(id)]);
9115 assert_eq!(unplaced.len(), 1);
9116 assert!(!moved);
9117 assert!(runtime.scroll.is_empty() && runtime.scroll_target.is_empty());
9118 }
9119
9120 #[test]
9123 fn a_batch_places_every_request_it_carries() {
9124 let tree = view();
9125 let requests = Command::<()>::batch([
9126 Command::scroll("log", ScrollTo::y(400.0).instant()),
9127 Command::scroll("log", ScrollTo::y(600.0).instant()),
9128 ])
9129 .into_parts()
9130 .scrolls;
9131 assert_eq!(requests.len(), 2);
9132 let id = frus_widgets::find_by_key(tree.as_ref(), requests[0].0).expect("named");
9133 let mut runtime = Runtime::default();
9134 let (unplaced, moved) =
9135 apply_scroll_requests(&mut runtime, tree.as_ref(), requests, &[region(id)]);
9136 assert!(unplaced.is_empty() && moved);
9137 assert_eq!(
9138 runtime.scroll.get(&id),
9139 Some(&(0.0, 600.0)),
9140 "the last request is the current statement"
9141 );
9142 }
9143}
9144
9145#[cfg(test)]
9146mod autoscroll_tests {
9147 use super::{
9148 edge_autoscroll, follow_content, Drag, Point, Rect, WidgetId, AUTOSCROLL_MAX_OVERHANG,
9149 AUTOSCROLL_VELOCITY,
9150 };
9151
9152 const VIEW: (f32, f32) = (100.0, 500.0);
9154 const ROOM: (f32, f32) = (1000.0, 1000.0);
9155 const DT: f32 = 1.0 / 60.0;
9157
9158 #[test]
9159 fn a_row_inside_the_viewport_scrolls_nothing() {
9160 assert_eq!(edge_autoscroll((200.0, 260.0), VIEW, ROOM, DT), None);
9161 assert_eq!(edge_autoscroll((440.0, 500.0), VIEW, ROOM, DT), None);
9163 }
9164
9165 #[test]
9168 fn the_speed_follows_how_far_out_the_row_is() {
9169 let near = edge_autoscroll((460.0, 501.0), VIEW, ROOM, DT).expect("one pixel out");
9170 let far = edge_autoscroll((460.0, 510.0), VIEW, ROOM, DT).expect("ten pixels out");
9171 assert!(
9172 near < 0.0,
9173 "the content moves up to reveal what is below: {near}"
9174 );
9175 assert!(
9176 (far / near - 10.0).abs() < 0.01,
9177 "ten times as far, ten times as fast: {near} vs {far}"
9178 );
9179 assert!(
9180 (near + 1.0 * AUTOSCROLL_VELOCITY * DT).abs() < 1e-4,
9181 "one pixel out, at the stated velocity: {near}"
9182 );
9183 }
9184
9185 #[test]
9188 fn the_overhang_is_capped() {
9189 let far = edge_autoscroll((460.0, 520.0), VIEW, ROOM, DT).expect("twenty out");
9190 let absurd = edge_autoscroll((460.0, 5000.0), VIEW, ROOM, DT).expect("off the window");
9191 assert!((far - absurd).abs() < 1e-4, "{far} vs {absurd}");
9192 assert!((absurd + AUTOSCROLL_MAX_OVERHANG * AUTOSCROLL_VELOCITY * DT).abs() < 1e-4);
9193 }
9194
9195 #[test]
9198 fn above_the_top_the_content_comes_down() {
9199 let up = edge_autoscroll((90.0, 150.0), VIEW, ROOM, DT).expect("ten above");
9200 assert!(up > 0.0, "{up}");
9201 }
9202
9203 #[test]
9210 fn a_carried_item_follows_its_content() {
9211 let shift = (0.0, -95.0);
9213 let pressed = Point::new(900.0, 1330.0);
9214 let row_then = Rect::new(105.0, 1250.0, 700.0, 160.0);
9215 let row_now = row_then.translate(0.0, -95.0);
9216 let finger = Point::new(900.0, 2030.0);
9217
9218 let mut reorder = Drag::Reorder {
9219 id: WidgetId::from_u64(1),
9220 from: 0,
9221 start: pressed,
9222 moved: true,
9223 carried: Some((WidgetId::from_u64(1), row_then)),
9224 };
9225 follow_content(&mut reorder, shift);
9226 let Drag::Reorder { start, carried, .. } = reorder else {
9227 unreachable!("still a reorder");
9228 };
9229 let (_, carried) = carried.expect("the box it was carried from");
9233 assert_eq!(carried, row_now, "the carried box went with the content");
9234 let ghost = carried.translate(finger.x - start.x, finger.y - start.y);
9236 assert_eq!(
9237 ghost,
9238 row_then.translate(finger.x - pressed.x, finger.y - pressed.y),
9239 "the ghost is where it would be had nothing scrolled"
9240 );
9241
9242 let mut item = Drag::Item {
9243 source: frus_widgets::DragSource {
9244 id: WidgetId::from_u64(1),
9245 rect: row_then,
9246 payload: 7,
9247 },
9248 start: pressed,
9249 moved: true,
9250 over: None,
9251 };
9252 follow_content(&mut item, shift);
9253 let Drag::Item { source, start, .. } = item else {
9254 unreachable!("still an item");
9255 };
9256 assert_eq!(
9257 source.rect, row_now,
9258 "the box taken at the press went with the content"
9259 );
9260 assert_eq!(start, Point::new(900.0, 1235.0), "and so did the press");
9261 }
9262
9263 #[test]
9266 fn an_exhausted_edge_stays_still() {
9267 assert_eq!(
9268 edge_autoscroll((460.0, 520.0), VIEW, (1000.0, 0.0), DT),
9269 None
9270 );
9271 assert_eq!(
9272 edge_autoscroll((90.0, 150.0), VIEW, (0.0, 1000.0), DT),
9273 None
9274 );
9275 }
9276
9277 #[test]
9280 fn a_row_taller_than_the_viewport_follows_its_leading_edge() {
9281 let both = edge_autoscroll((50.0, 900.0), VIEW, ROOM, DT).expect("over both edges");
9282 assert!(both > 0.0, "the top edge wins: {both}");
9283 }
9284}
9285
9286#[cfg(test)]
9299mod back_gesture_tests {
9300 use super::{
9301 drag_after_hold, frame_needs_build, hold_candidates, Drag, Point, Theme, WidgetId,
9302 };
9303 use frus_widgets::{build_ui, column, Container, DragSource, Draggable, Navigator};
9304 use frus_widgets::{Rect, Runtime, Size, Ui, Widget};
9305
9306 const W: f32 = 392.0;
9308 const H: f32 = 850.0;
9309 const ROW_TOP: f32 = 480.0;
9312 const ROW_HEIGHT: f32 = 66.0;
9313 const ON_THE_ROW: Point = Point::new(3.0, 509.0);
9314 const ABOVE_THE_ROW: Point = Point::new(3.0, 200.0);
9315 const HELD: u8 = 1;
9317
9318 fn home() -> impl Widget<u8> {
9321 column![
9322 Container::<u8>::new().width(W).height(ROW_TOP),
9323 Draggable::new(
9324 Container::<u8>::new()
9325 .width(W)
9326 .height(ROW_HEIGHT)
9327 .on_long_press(HELD)
9328 )
9329 .payload(7)
9330 .long_press(),
9331 ]
9332 }
9333
9334 fn after_a_push() -> Navigator<u8> {
9337 Navigator::new("data", Container::<u8>::new().width(W).height(H))
9338 .size(W, H)
9339 .from("home", home(), 0.999, true)
9340 }
9341
9342 fn frame(root: &dyn Widget<u8>) -> Ui<u8> {
9343 build_ui(
9344 root,
9345 Size::new(W, H),
9346 &Runtime::default(),
9347 &Theme::default(),
9348 )
9349 }
9350
9351 fn back(at: Point) -> Drag {
9352 Drag::Back { start_x: at.x }
9353 }
9354
9355 fn still_scroll() -> Drag {
9356 Drag::Scroll {
9357 id: WidgetId::from_u64(9),
9358 last: ON_THE_ROW,
9359 moved: false,
9360 carried: (0.0, 0.0),
9361 dismiss: None,
9362 axis: None,
9363 pan: None,
9364 }
9365 }
9366
9367 fn row_source() -> DragSource {
9368 DragSource {
9369 id: WidgetId::from_u64(3),
9370 rect: Rect::new(-117.0, ROW_TOP, W, ROW_HEIGHT),
9371 payload: 7,
9372 }
9373 }
9374
9375 #[test]
9383 fn the_frame_an_animation_settles_in_is_built_again() {
9384 assert!(
9385 frame_needs_build(false, false, true, false),
9386 "the frame the application's animation stops in builds the view again"
9387 );
9388 assert!(
9389 frame_needs_build(false, true, true, false),
9390 "as does every frame while it moves"
9391 );
9392 assert!(
9393 !frame_needs_build(false, false, false, false),
9394 "and a still frame after a still one repaints the tree it has"
9395 );
9396 assert!(frame_needs_build(true, false, false, false), "unless asked");
9397 assert!(
9398 frame_needs_build(false, false, false, true),
9399 "or the frame has its own reason"
9400 );
9401 }
9402
9403 #[test]
9406 fn a_back_gesture_claims_nothing_under_its_finger() {
9407 let root = after_a_push();
9408 let ui = frame(&root);
9409 let (held, lift) = hold_candidates(None, Some(&ui), Some(&root), ON_THE_ROW);
9410 assert!(
9411 lift.is_some() && held == Some(HELD),
9412 "the frame the phone had: a press there, with no gesture, finds the row's hold"
9413 );
9414
9415 let gesture = back(ON_THE_ROW);
9416 let (held, lift) = hold_candidates(Some(&gesture), Some(&ui), Some(&root), ON_THE_ROW);
9417 assert!(
9418 lift.is_none(),
9419 "a back gesture's press arms no lift of the row under it"
9420 );
9421 assert_eq!(held, None, "nor its long press");
9422 }
9423
9424 #[test]
9427 fn a_hold_never_takes_the_pointer_from_a_back_gesture() {
9428 let reorder = Some((WidgetId::from_u64(4), 2, ON_THE_ROW));
9429 for (lift, reorder) in [
9430 (Some(row_source()), None),
9431 (None, reorder),
9432 (Some(row_source()), reorder),
9433 (None, None),
9434 ] {
9435 let after = drag_after_hold(Some(back(ON_THE_ROW)), lift, reorder, ON_THE_ROW);
9436 assert!(
9437 matches!(after, Some(Drag::Back { start_x }) if start_x == ON_THE_ROW.x),
9438 "the back gesture is still the drag (lift {}, reorder {})",
9439 lift.is_some(),
9440 reorder.is_some()
9441 );
9442 }
9443 }
9444
9445 #[test]
9447 fn a_back_gesture_where_nothing_is_under_it_still_owns_the_pointer() {
9448 let root = after_a_push();
9449 let ui = frame(&root);
9450 let gesture = back(ABOVE_THE_ROW);
9451 let (held, lift) = hold_candidates(Some(&gesture), Some(&ui), Some(&root), ABOVE_THE_ROW);
9452 assert!(held.is_none() && lift.is_none());
9453 assert!(matches!(
9454 drag_after_hold(Some(gesture), None, None, ABOVE_THE_ROW),
9455 Some(Drag::Back { .. })
9456 ));
9457 }
9458
9459 #[test]
9463 fn a_hold_on_a_row_without_a_back_gesture_still_lifts_it() {
9464 let root = home();
9465 let ui = frame(&root);
9466 let at = Point::new(40.0, ROW_TOP + 10.0);
9467 let scroll = still_scroll();
9468 let (held, lift) = hold_candidates(Some(&scroll), Some(&ui), Some(&root), at);
9469 assert_eq!(
9470 held,
9471 Some(HELD),
9472 "the long press is armed under a still scroll"
9473 );
9474 assert!(lift.is_some(), "and so is the lift");
9475 let (held, lift) = hold_candidates(None, Some(&ui), Some(&root), at);
9476 assert!(held.is_some() && lift.is_some(), "and with no drag at all");
9477
9478 let cursor = Point::new(41.0, ROW_TOP + 11.0);
9479 let lifted = drag_after_hold(Some(still_scroll()), Some(row_source()), None, cursor);
9480 assert!(
9481 matches!(lifted, Some(Drag::Item { moved: true, start, .. }) if start == cursor),
9482 "the hold lifts the item where the finger is"
9483 );
9484 let reorder = Some((WidgetId::from_u64(4), 2, at));
9485 let reordering = drag_after_hold(Some(still_scroll()), None, reorder, cursor);
9486 assert!(
9487 matches!(reordering, Some(Drag::Reorder { moved: true, from: 2, start, .. }) if start == at),
9488 "or picks the row up for a reorder, from where it was pressed"
9489 );
9490 assert!(
9491 drag_after_hold(Some(still_scroll()), None, None, cursor).is_none(),
9492 "a hold with nothing to lift ends the still scroll: the long press had it"
9493 );
9494 }
9495}
9496
9497fn count_layers(primitives: &[frus_widgets::Primitive]) -> u32 {
9499 primitives
9500 .iter()
9501 .map(|p| match p {
9502 frus_widgets::Primitive::Layer { primitives, .. } => 1 + count_layers(primitives),
9503 _ => 0,
9504 })
9505 .sum()
9506}
9507
9508pub(crate) struct KeyDown {
9512 logical_key: WinitKey,
9514 physical_key: PhysicalKey,
9516 text: Option<winit::keyboard::SmolStr>,
9518 repeat: bool,
9520}
9521
9522impl From<&winit::event::KeyEvent> for KeyDown {
9523 fn from(event: &winit::event::KeyEvent) -> Self {
9524 Self {
9525 logical_key: event.logical_key.clone(),
9526 physical_key: event.physical_key,
9527 text: event.text.clone(),
9528 repeat: event.repeat,
9529 }
9530 }
9531}
9532
9533fn backdrop_under(mut scene: Scene, width: f32, height: f32, line: f32) -> Scene {
9538 let mut framed = Scene::new();
9539 framed.fill_rect(
9540 frus_widgets::Rect::new(0.0, line, width, (height - line).max(0.0)),
9541 frus_gpu::backdrop(),
9542 );
9543 for primitive in scene.split_off(0) {
9544 framed.push_primitive(primitive);
9545 }
9546 framed
9547}
9548
9549#[cfg(test)]
9550mod backdrop_tests {
9551 use super::*;
9552
9553 #[test]
9556 fn the_background_leaves_the_system_s_line_clear() {
9557 let mut scene = Scene::new();
9558 scene.fill_rect(
9559 frus_widgets::Rect::new(10.0, 5.0, 20.0, 20.0),
9560 frus_widgets::Color::WHITE,
9561 );
9562 let framed = backdrop_under(scene.clone(), 800.0, 600.0, 48.0);
9563 let prims = framed.primitives();
9564 assert_eq!(prims.len(), 2);
9565 match &prims[0] {
9566 frus_widgets::Primitive::Rect { rect, color, .. } => {
9567 assert_eq!(*rect, frus_widgets::Rect::new(0.0, 48.0, 800.0, 552.0));
9568 assert_eq!(*color, frus_gpu::backdrop());
9569 assert_eq!(
9570 color.a, 1.0,
9571 "opaque: nothing behind the window shows there"
9572 );
9573 }
9574 other => panic!("the background first: {other:?}"),
9575 }
9576 assert_eq!(prims[1], scene.primitives()[0], "then the scene");
9577
9578 let whole = backdrop_under(Scene::new(), 800.0, 600.0, 0.0);
9579 assert!(
9580 matches!(&whole.primitives()[0],
9581 frus_widgets::Primitive::Rect { rect, .. } if *rect == frus_widgets::Rect::new(0.0, 0.0, 800.0, 600.0)),
9582 "no line: the whole window"
9583 );
9584 }
9585}
9586
9587#[cfg(any(test, feature = "testing"))]
9590#[doc(hidden)]
9591pub mod testing {
9592 use super::*;
9593 use crate::gesture::{PointerEvent, PointerKind, LONG_PRESS_DELAY};
9594
9595 pub type FrameParts<'a, Msg> = (&'a Ui<Msg>, &'a dyn Widget<Msg>);
9597
9598 pub struct Driver<A: Application> {
9600 shell: App<A>,
9601 size: Size,
9602 ghost: Option<Rect>,
9604 }
9605
9606 impl<A: Application> Driver<A> {
9607 pub fn new(app: A, width: f32, height: f32) -> Self {
9609 let mut shell = App::detached(app);
9610 shell.gesture_clock = Some((0.0, 0.0));
9612 Self {
9613 shell,
9614 size: Size::new(width, height),
9615 ghost: None,
9616 }
9617 }
9618
9619 pub fn system_back(&mut self) -> bool {
9622 self.shell.system_back_step()
9623 }
9624
9625 pub fn fling_strategy(&self) -> frus_widgets::VelocityStrategy {
9628 self.shell.gesture_velocity.strategy()
9629 }
9630
9631 pub fn carried(&self) -> Option<Rect> {
9634 self.shell.carried_rect()
9635 }
9636
9637 pub fn ghost(&self) -> Option<Rect> {
9640 self.ghost
9641 }
9642
9643 pub fn offsets(&self) -> Vec<(Rect, (f32, f32))> {
9645 let s = &self.shell;
9646 s.ui.as_ref()
9647 .map(|ui| {
9648 ui.scroll_regions()
9649 .iter()
9650 .map(|a| {
9651 (
9652 a.viewport,
9653 s.runtime.scroll.get(&a.id).copied().unwrap_or((0.0, 0.0)),
9654 )
9655 })
9656 .collect()
9657 })
9658 .unwrap_or_default()
9659 }
9660
9661 pub fn frame_parts(&self) -> Option<FrameParts<'_, A::Message>> {
9664 let s = &self.shell;
9665 Some((s.ui.as_ref()?, s.tree.as_deref()?))
9666 }
9667
9668 pub fn texts(&self) -> Vec<(String, Rect)> {
9670 self.shell
9671 .ui
9672 .as_ref()
9673 .map(|ui| {
9674 ui.scene()
9675 .primitives()
9676 .iter()
9677 .filter_map(|p| match p {
9678 frus_widgets::Primitive::Text { text, .. } => {
9679 Some((text.clone(), p.bounds()))
9680 }
9681 _ => None,
9682 })
9683 .collect()
9684 })
9685 .unwrap_or_default()
9686 }
9687
9688 pub fn tap_text(&mut self, label: &str) -> bool {
9691 let Some((_, rect)) = self.texts().into_iter().find(|(text, _)| text == label) else {
9692 return false;
9693 };
9694 let at = Point::new(rect.x + rect.width / 2.0, rect.y + rect.height / 2.0);
9695 self.press(at);
9696 self.release(at);
9697 self.frame(1.0 / 60.0);
9698 true
9699 }
9700
9701 pub fn app(&self) -> &A {
9703 &self.shell.app
9704 }
9705
9706 pub fn focused(&self) -> Option<WidgetId> {
9708 self.shell.runtime.input.focused
9709 }
9710
9711 pub fn shell_modifiers(&self) -> (bool, bool, bool, bool) {
9713 (
9714 self.shell.ctrl,
9715 self.shell.shift,
9716 self.shell.alt,
9717 self.shell.meta,
9718 )
9719 }
9720
9721 pub fn hit(&self, at: Point) -> Option<WidgetId> {
9723 self.shell.ui.as_ref().and_then(|ui| ui.hit(at))
9724 }
9725
9726 pub fn update(&mut self, message: A::Message) {
9728 self.shell.dispatch(message);
9729 }
9730
9731 pub fn run(&mut self, seconds: f32) {
9733 let n = (seconds * 60.0).round() as usize;
9734 for _ in 0..n.max(1) {
9735 self.frame(1.0 / 60.0);
9736 }
9737 }
9738
9739 pub fn press(&mut self, at: Point) {
9741 self.pointer(PointerKind::Down, at);
9742 }
9743
9744 pub fn move_to(&mut self, at: Point) {
9746 self.pointer(PointerKind::Move, at);
9747 }
9748
9749 pub fn release(&mut self, at: Point) {
9751 self.pointer(PointerKind::Up, at);
9752 }
9753
9754 pub fn hold_deadline(&mut self) -> bool {
9757 let fired = self.shell.press.poll(Instant::now() + LONG_PRESS_DELAY);
9758 if fired {
9759 self.shell.hold_deadline_reached();
9760 }
9761 fired
9762 }
9763
9764 pub fn tap_deadline(&mut self) -> bool {
9767 self.shell
9768 .poll_pending_tap(Instant::now() + DOUBLE_TAP_TIMEOUT)
9769 }
9770
9771 pub fn cursor_shown(&self) -> frus_widgets::Cursor {
9773 let s = &self.shell;
9774 let requested = s.runtime.input.hovered.and_then(|id| {
9775 let rect = s.ui.as_ref()?.widget_rect(id)?;
9776 let widget = find_widget(s.tree.as_ref()?.as_ref(), id)?;
9777 widget.cursor_icon(
9778 s.cursor.x - rect.x,
9779 s.cursor.y - rect.y,
9780 rect.width,
9781 rect.height,
9782 )
9783 });
9784 s.region_cursor(requested)
9785 }
9786
9787 pub fn next_wake(&self) -> Option<Instant> {
9789 match self.shell.idle_control_flow() {
9790 ControlFlow::WaitUntil(at) => Some(at),
9791 _ => None,
9792 }
9793 }
9794
9795 pub fn secondary_click(&mut self, at: Point) {
9797 self.pointer_of(PointerKind::Move, at, false);
9798 self.shell.context_click();
9799 }
9800
9801 pub fn press_mouse(&mut self, at: Point) {
9804 self.pointer_of(PointerKind::Down, at, false);
9805 }
9806
9807 pub fn move_mouse(&mut self, at: Point) {
9809 self.pointer_of(PointerKind::Move, at, false);
9810 }
9811
9812 pub fn release_mouse(&mut self, at: Point) {
9814 self.pointer_of(PointerKind::Up, at, false);
9815 }
9816
9817 pub fn area_selection(&self) -> Option<String> {
9819 self.shell.region_text()
9820 }
9821
9822 pub fn area_select_all(&mut self) {
9824 self.shell.select_all_region();
9825 }
9826
9827 pub fn escape(&mut self) {
9829 self.shell.escape();
9830 }
9831
9832 pub fn key(&mut self, stroke: frus_widgets::KeyStroke) {
9840 use frus_widgets::ShortcutKey as K;
9841 let named = |key| (WinitKey::Named(key), None);
9842 let (logical_key, text) = match stroke.key {
9843 K::Char(c) => {
9844 let typed: winit::keyboard::SmolStr = c.to_string().into();
9845 (WinitKey::Character(typed.clone()), Some(typed))
9846 }
9847 K::Enter => named(NamedKey::Enter),
9848 K::Escape => named(NamedKey::Escape),
9849 K::Tab => named(NamedKey::Tab),
9850 K::Space => (WinitKey::Named(NamedKey::Space), Some(" ".into())),
9851 K::Backspace => named(NamedKey::Backspace),
9852 K::Delete => named(NamedKey::Delete),
9853 K::Up => named(NamedKey::ArrowUp),
9854 K::Down => named(NamedKey::ArrowDown),
9855 K::Left => named(NamedKey::ArrowLeft),
9856 K::Right => named(NamedKey::ArrowRight),
9857 K::Home => named(NamedKey::Home),
9858 K::End => named(NamedKey::End),
9859 K::PageUp => named(NamedKey::PageUp),
9860 K::PageDown => named(NamedKey::PageDown),
9861 K::F(n) => named(match n {
9862 1 => NamedKey::F1,
9863 2 => NamedKey::F2,
9864 3 => NamedKey::F3,
9865 4 => NamedKey::F4,
9866 5 => NamedKey::F5,
9867 6 => NamedKey::F6,
9868 7 => NamedKey::F7,
9869 8 => NamedKey::F8,
9870 9 => NamedKey::F9,
9871 10 => NamedKey::F10,
9872 11 => NamedKey::F11,
9873 _ => NamedKey::F12,
9874 }),
9875 };
9876 const LETTERS: [KeyCode; 26] = [
9879 KeyCode::KeyA,
9880 KeyCode::KeyB,
9881 KeyCode::KeyC,
9882 KeyCode::KeyD,
9883 KeyCode::KeyE,
9884 KeyCode::KeyF,
9885 KeyCode::KeyG,
9886 KeyCode::KeyH,
9887 KeyCode::KeyI,
9888 KeyCode::KeyJ,
9889 KeyCode::KeyK,
9890 KeyCode::KeyL,
9891 KeyCode::KeyM,
9892 KeyCode::KeyN,
9893 KeyCode::KeyO,
9894 KeyCode::KeyP,
9895 KeyCode::KeyQ,
9896 KeyCode::KeyR,
9897 KeyCode::KeyS,
9898 KeyCode::KeyT,
9899 KeyCode::KeyU,
9900 KeyCode::KeyV,
9901 KeyCode::KeyW,
9902 KeyCode::KeyX,
9903 KeyCode::KeyY,
9904 KeyCode::KeyZ,
9905 ];
9906 let physical_key = match stroke.key {
9907 K::Char(c) if c.is_ascii_alphabetic() => {
9908 PhysicalKey::Code(LETTERS[(c.to_ascii_lowercase() as u8 - b'a') as usize])
9909 }
9910 _ => PhysicalKey::Unidentified(winit::keyboard::NativeKeyCode::Unidentified),
9911 };
9912 let held = (
9913 self.shell.ctrl,
9914 self.shell.shift,
9915 self.shell.alt,
9916 self.shell.meta,
9917 );
9918 (
9919 self.shell.ctrl,
9920 self.shell.shift,
9921 self.shell.alt,
9922 self.shell.meta,
9923 ) = (stroke.ctrl, stroke.shift, stroke.alt, stroke.meta);
9924 self.shell.key_down(
9925 &KeyDown {
9926 logical_key,
9927 physical_key,
9928 text,
9929 repeat: false,
9930 },
9931 None,
9932 );
9933 (
9934 self.shell.ctrl,
9935 self.shell.shift,
9936 self.shell.alt,
9937 self.shell.meta,
9938 ) = held;
9939 }
9940
9941 pub fn area_handles(&self) -> Option<[frus_widgets::SelectionHandle; 2]> {
9943 self.shell.region_handles().map(|(_, handles)| handles)
9944 }
9945
9946 fn pointer(&mut self, kind: PointerKind, at: Point) {
9947 self.pointer_of(kind, at, true);
9948 }
9949
9950 fn pointer_of(&mut self, kind: PointerKind, at: Point, touch: bool) {
9951 self.shell.pointer_event(PointerEvent {
9952 kind,
9953 position: at,
9954 touch,
9955 });
9956 }
9957
9958 pub fn frame(&mut self, dt: f32) {
9960 let (width, height) = (self.size.width, self.size.height);
9961 let s = &mut self.shell;
9962 if let Some((now, _)) = s.gesture_clock.as_mut() {
9963 *now += dt;
9964 }
9965 if s.last_size != Some((width, height)) {
9966 s.last_size = Some((width, height));
9967 s.build_dirty = true;
9968 s.app.on_resize(width, height);
9969 }
9970 let mut app_animating = s.app.tick(dt);
9971 let settings = s
9972 .platform
9973 .accessibility
9974 .with_overrides(s.app.accessibility());
9975 let theme_moved = s.themes.advance(
9976 &s.app,
9977 s.platform.brightness,
9978 settings.high_contrast,
9979 settings.disable_animations,
9980 dt,
9981 );
9982 app_animating |= theme_moved | s.themes.animating();
9983 let theme = s.themes.displayed(&s.app);
9984 install_ambient(&s.app, &s.platform.locales);
9985 let _surface = s.media_query(width, height).install();
9986 s.runtime.still = settings.disable_animations;
9987 let was = std::mem::replace(&mut s.app_was_animating, app_animating);
9988 let asked = s.app.effects();
9989 s.run_command(asked);
9990 let arrived = s.take_task_arrivals();
9991 if frus_widgets::take_rebuild_request() || arrived {
9992 s.build_dirty = true;
9993 }
9994 if frame_needs_build(
9995 s.build_dirty,
9996 app_animating,
9997 was,
9998 s.tree.is_none() || s.runtime.switching(),
9999 ) {
10000 s.tree = Some(build_view(&s.app, &theme, &s.runtime));
10001 }
10002 s.build_dirty = false;
10003 s.autoscroll_carried(dt);
10004 s.advance_reorder_springs(dt);
10005 let regions =
10006 s.ui.as_ref()
10007 .map(|ui| ui.scroll_regions().to_vec())
10008 .unwrap_or_default();
10009 s.runtime.sync_pages(®ions);
10010 s.runtime.sync_visible(®ions);
10011 s.runtime.advance(dt);
10012 s.runtime.advance_scroll(®ions, dt);
10013 let tree = s.tree.as_deref().expect("the view was built");
10014 s.runtime.advance_values(tree, dt);
10017 let ui = build_ui(tree, Size::new(width, height), &s.runtime, &theme);
10018 s.runtime.states.end_frame();
10019 let mut scene = ui.scene().clone();
10020 let ghost = if matches!(s.drag, Some(Drag::Reorder { moved: true, .. })) {
10021 s.paint_reorder_preview(&ui, &theme, &mut scene);
10022 scene.primitives().last().map(|p| p.bounds())
10024 } else {
10025 None
10026 };
10027 let paged = ui.scroll_regions().to_vec();
10028 let scrolled: Vec<A::Message> = {
10029 let grain = |id| find_widget(tree, id).map_or(0.0, |w| w.scroll_grain());
10030 s.runtime
10031 .scroll_changes(&paged, grain)
10032 .into_iter()
10033 .filter_map(|(id, position)| {
10034 find_widget(tree, id).and_then(|w| w.on_scroll(position))
10035 })
10036 .collect()
10037 };
10038 s.ui = Some(ui);
10039 s.sync_regions(false);
10040 s.reconcile_focus();
10043 for message in scrolled {
10044 s.dispatch(message);
10045 }
10046 self.ghost = ghost;
10047 }
10048 }
10049}
10050
10051#[cfg(test)]
10065mod carried_row_tests {
10066 use super::testing::Driver;
10067 use crate::{Application, Command};
10068 use frus_widgets::{
10069 column, text, Axis, Button, CellFn, Container, Flex, Kanban, Point, Rect, ReorderAxis,
10070 ReorderGrab, ReorderableList, SingleChildScrollView, Theme, Widget,
10071 };
10072
10073 const W: f32 = 392.7;
10075 const H: f32 = 850.9;
10076 const STRIP_Y: f32 = 88.0;
10078 const STRIP_CONTENT: f32 = 24.0 + 10.0 * 96.0 + 9.0 * 8.0 + 24.0;
10080
10081 #[derive(Clone, Debug, PartialEq)]
10082 enum Board {
10083 Label(usize, usize),
10084 Card(usize, usize, usize, usize),
10085 Delete(usize, usize),
10086 Add(usize),
10087 Row(usize, usize),
10088 }
10089
10090 struct BoardApp {
10092 labels: Vec<usize>,
10093 cards: Vec<Vec<String>>,
10094 log: Vec<Board>,
10095 }
10096
10097 impl Default for BoardApp {
10098 fn default() -> Self {
10099 let cols: [&[&str]; 3] = [
10100 &["Design API", "Write spec", "Triage bugs"],
10101 &["Build widget"],
10102 &["Kickoff", "Research"],
10103 ];
10104 Self {
10105 labels: (0..10).collect(),
10106 cards: cols
10107 .iter()
10108 .map(|c| c.iter().map(|s| s.to_string()).collect())
10109 .collect(),
10110 log: Vec::new(),
10111 }
10112 }
10113 }
10114
10115 impl Application for BoardApp {
10116 type Message = Board;
10117
10118 fn update(&mut self, message: Board) -> Command<Board> {
10119 self.log.push(message.clone());
10120 match message {
10121 Board::Label(from, to) => {
10122 let label = self.labels.remove(from);
10123 self.labels.insert(to, label);
10124 }
10125 Board::Card(from_col, from_pos, to_col, to_pos) => {
10126 let card = self.cards[from_col].remove(from_pos);
10127 let to_pos = to_pos.min(self.cards[to_col].len());
10128 self.cards[to_col].insert(to_pos, card);
10129 }
10130 _ => {}
10131 }
10132 Command::none()
10133 }
10134
10135 fn view(&self, theme: &Theme) -> Box<dyn Widget<Board>> {
10136 let mut strip = ReorderableList::new(Board::Label)
10137 .axis(ReorderAxis::Horizontal)
10138 .grab(ReorderGrab::LongPress)
10139 .gap(8.0);
10140 for &label in &self.labels {
10141 strip = strip.keyed_row(
10142 label as u64,
10143 Container::new()
10144 .width(96.0)
10145 .padding(12.0)
10146 .color(theme.surface)
10147 .child(text(format!("label {label}")).size(14.0)),
10148 );
10149 }
10150 let strip = SingleChildScrollView::new()
10151 .axis(Axis::Horizontal)
10152 .width(W)
10153 .child(
10154 Container::new()
10155 .padding_each(12.0, 24.0, 0.0, 24.0)
10156 .child(strip),
10157 );
10158 let mut board = Kanban::new(Board::Card)
10159 .on_add(Board::Add)
10160 .scrollable_columns();
10161 for (col, title) in ["To do", "Doing", "Done"].iter().enumerate() {
10162 let cards: Vec<CellFn<Board>> = self.cards[col]
10163 .iter()
10164 .enumerate()
10165 .map(|(pos, label)| {
10166 let label = label.clone();
10167 Box::new(move || {
10168 Box::new(
10169 Flex::row()
10170 .child(text(label.clone()).size(14.0))
10171 .child(Flex::row().flex(1.0))
10172 .child(Button::new("x").on_press(Board::Delete(col, pos))),
10173 ) as Box<dyn Widget<Board>>
10174 }) as CellFn<Board>
10175 })
10176 .collect();
10177 board = board.column_widgets(*title, cards);
10178 }
10179 let board = SingleChildScrollView::new()
10180 .axis(Axis::Horizontal)
10181 .width(W)
10182 .flex(1.0)
10183 .child(Container::new().padding(24.0).child(board));
10184 Box::new(
10185 Container::new().width(W).height(H).child(
10186 column![
10187 Container::<Board>::new().width(W).height(56.0),
10188 strip,
10189 board
10190 ]
10191 .flex(1.0),
10192 ),
10193 )
10194 }
10195 }
10196
10197 #[derive(Default)]
10199 struct ListApp {
10200 log: Vec<Board>,
10201 }
10202
10203 impl Application for ListApp {
10204 type Message = Board;
10205
10206 fn update(&mut self, message: Board) -> Command<Board> {
10207 self.log.push(message);
10208 Command::none()
10209 }
10210
10211 fn view(&self, _theme: &Theme) -> Box<dyn Widget<Board>> {
10212 let mut list = ReorderableList::new(Board::Row)
10213 .grab(ReorderGrab::LongPress)
10214 .width(W);
10215 for row in 0..20u64 {
10216 list = list.keyed_row(
10217 row,
10218 Container::new()
10219 .height(60.0)
10220 .child(text(format!("row {row}")).size(14.0)),
10221 );
10222 }
10223 Box::new(
10224 SingleChildScrollView::new()
10225 .axis(Axis::Vertical)
10226 .width(W)
10227 .height(H)
10228 .child(list),
10229 )
10230 }
10231 }
10232
10233 fn driver<A: Application>(app: A) -> Driver<A> {
10234 let mut driver = Driver::new(app, W, H);
10235 driver.run(0.2);
10236 driver
10237 }
10238
10239 fn hold<A: Application>(driver: &mut Driver<A>, at: Point) {
10241 driver.press(at);
10242 driver.run(0.5);
10243 assert!(
10244 driver.hold_deadline(),
10245 "the hold lifts what is under {at:?}"
10246 );
10247 driver.run(0.05);
10248 }
10249
10250 fn carry<A: Application>(driver: &mut Driver<A>, points: impl IntoIterator<Item = Point>) {
10252 for at in points {
10253 driver.move_to(at);
10254 driver.run(0.1);
10255 }
10256 }
10257
10258 fn held_under_the_finger<A: Application>(
10262 driver: &mut Driver<A>,
10263 seconds: f32,
10264 finger: Point,
10265 offset: (f32, f32),
10266 size: (f32, f32),
10267 region: usize,
10268 ) -> (f32, f32) {
10269 let mut last = driver.offsets()[region].1;
10270 for frame in 0..(seconds * 60.0) as usize {
10271 driver.run(1.0 / 60.0);
10272 let now = driver.offsets()[region].1;
10273 let carried: Rect = driver.carried().unwrap_or_else(|| {
10274 panic!("frame {frame}: nothing is carried any more, the list at {now:?}")
10275 });
10276 assert!(
10277 (carried.x - finger.x - offset.0).abs() < 0.5
10278 && (carried.y - finger.y - offset.1).abs() < 0.5
10279 && (carried.width - size.0).abs() < 0.5
10280 && (carried.height - size.1).abs() < 0.5,
10281 "frame {frame}: carried at {carried:?}, the finger at {finger:?}, the list at {now:?}"
10282 );
10283 assert!(
10284 now.0 >= last.0 - 0.01 && now.1 >= last.1 - 0.01,
10285 "frame {frame}: the list went back, {last:?} then {now:?}"
10286 );
10287 let ghost = driver
10289 .ghost()
10290 .unwrap_or_else(|| panic!("frame {frame}: no ghost is drawn"));
10291 assert!(
10292 (ghost.x - carried.x).abs() < 1.0
10293 && (ghost.y - carried.y).abs() < 1.0
10294 && (ghost.width - carried.width).abs() < 2.0,
10295 "frame {frame}: the ghost is drawn at {ghost:?}, what is carried is at {carried:?}"
10296 );
10297 last = now;
10298 }
10299 last
10300 }
10301
10302 #[test]
10306 fn a_label_carried_past_the_strips_edge_stays_under_the_finger() {
10307 let mut driver = driver(BoardApp::default());
10308 hold(&mut driver, Point::new(155.0, STRIP_Y));
10309 let lifted = driver.carried().expect("the label is carried");
10310 assert_eq!(
10311 lifted,
10312 Rect::new(128.0, 68.0, 96.0, 41.0),
10313 "Feature, lifted"
10314 );
10315 let finger = Point::new(370.0, STRIP_Y);
10316 carry(
10317 &mut driver,
10318 [200.0, 260.0, 320.0, finger.x].map(|x| Point::new(x, STRIP_Y)),
10319 );
10320 let strip = held_under_the_finger(
10321 &mut driver,
10322 1.5,
10323 finger,
10324 (lifted.x - 155.0, lifted.y - STRIP_Y),
10325 (96.0, 41.0),
10326 0,
10327 );
10328 assert!(
10329 (strip.0 - (STRIP_CONTENT - W)).abs() < 1.0,
10330 "the strip scrolled on to its end: {strip:?}"
10331 );
10332 driver.release(finger);
10333 driver.run(0.2);
10334 assert_eq!(
10335 driver.app().log,
10336 [Board::Label(1, 9)],
10337 "past the last label, it goes last; no card moves"
10338 );
10339 }
10340
10341 #[test]
10344 fn a_hold_on_a_label_then_a_carry_along_x_reorders_the_labels_and_moves_no_card() {
10345 let mut driver = driver(BoardApp::default());
10346 hold(&mut driver, Point::new(155.0, STRIP_Y));
10347 carry(
10348 &mut driver,
10349 [167.0, 189.0, 218.0, 247.0, 276.0, 306.0].map(|x| Point::new(x, STRIP_Y)),
10350 );
10351 driver.run(0.4);
10352 assert!(driver.carried().is_some(), "still carried");
10353 driver.release(Point::new(306.0, STRIP_Y));
10354 driver.run(0.2);
10355 assert_eq!(driver.app().log, [Board::Label(1, 2)]);
10356 assert_eq!(driver.app().labels[..4], [0, 2, 1, 3]);
10357 assert_eq!(
10358 driver.app().cards,
10359 BoardApp::default().cards,
10360 "no card moved"
10361 );
10362 assert_eq!(driver.offsets()[0].1, (0.0, 0.0), "nothing scrolled");
10363 assert_eq!(driver.offsets()[1].1, (0.0, 0.0), "the board neither");
10364 }
10365
10366 #[test]
10369 fn a_card_carried_across_the_board_moves_the_card() {
10370 let mut driver = driver(BoardApp::default());
10371 driver.press(Point::new(100.0, 190.0));
10372 carry(
10373 &mut driver,
10374 [130.0, 180.0, 240.0, 300.0].map(|x| Point::new(x, 185.0)),
10375 );
10376 driver.release(Point::new(300.0, 185.0));
10377 driver.run(0.2);
10378 assert_eq!(driver.app().log, [Board::Card(0, 0, 1, 0)]);
10379 assert_eq!(driver.app().labels, (0..10).collect::<Vec<_>>());
10380 }
10381
10382 #[test]
10385 fn a_card_carried_to_the_boards_edge_stays_under_the_finger() {
10386 let mut driver = driver(BoardApp::default());
10387 let press = Point::new(100.0, 190.0);
10388 driver.press(press);
10389 let finger = Point::new(385.0, 190.0);
10390 carry(
10391 &mut driver,
10392 [130.0, 200.0, 280.0, 340.0, finger.x].map(|x| Point::new(x, 190.0)),
10393 );
10394 let card = driver.carried().expect("the card is carried");
10395 let board = held_under_the_finger(
10396 &mut driver,
10397 1.0,
10398 finger,
10399 (card.x - finger.x, card.y - finger.y),
10400 (card.width, card.height),
10401 1,
10402 );
10403 assert!(
10404 board.0 > 250.0,
10405 "the board scrolled past the column the card came from: {board:?}"
10406 );
10407 }
10408
10409 #[test]
10413 fn a_row_carried_past_the_bottom_of_a_long_list_stays_under_the_finger() {
10414 let mut driver = driver(ListApp::default());
10415 hold(&mut driver, Point::new(100.0, 30.0));
10416 let lifted = driver.carried().expect("the row is carried");
10417 let finger = Point::new(100.0, 840.0);
10418 carry(
10419 &mut driver,
10420 [200.0, 400.0, 600.0, 800.0, finger.y].map(|y| Point::new(100.0, y)),
10421 );
10422 let list = held_under_the_finger(
10423 &mut driver,
10424 1.5,
10425 finger,
10426 (lifted.x - 100.0, lifted.y - 30.0),
10427 (lifted.width, lifted.height),
10428 0,
10429 );
10430 assert!(
10431 list.1 > 200.0,
10432 "the list scrolled past the row's own place: {list:?}"
10433 );
10434 driver.release(finger);
10435 driver.run(0.2);
10436 assert!(
10437 matches!(driver.app().log[..], [Board::Row(0, to)] if to > 15),
10438 "row 0 lands near the end: {:?}",
10439 driver.app().log
10440 );
10441 }
10442}
10443
10444#[cfg(test)]
10452mod press_tests {
10453 use super::{gesture_was_a_tap, press_at, settle_press, Drag, Point, Runtime, Theme};
10454 use crate::gesture::PointerKind;
10455 use crate::hover::Hover;
10456 use frus_widgets::{
10457 build_ui, Axis, Button, Container, Flex, Primitive, SegmentedButton, SingleChildScrollView,
10458 Size, Ui, Widget,
10459 };
10460
10461 const SCREEN: Size = Size::new(300.0, 400.0);
10462 const DT: f32 = 1.0 / 60.0;
10463
10464 fn filters() -> Box<dyn Widget<usize>> {
10466 let mut segments = SegmentedButton::new(0, |i| i);
10467 for label in ["All", "Active", "Done", "Archived", "Starred", "Shared"] {
10468 segments = segments.segment(label);
10469 }
10470 Box::new(
10471 Flex::column().width(300.0).height(400.0).child(
10472 SingleChildScrollView::new()
10473 .axis(Axis::Horizontal)
10474 .width(300.0)
10475 .height(80.0)
10476 .child(segments),
10477 ),
10478 )
10479 }
10480
10481 fn page() -> Box<dyn Widget<usize>> {
10483 Box::new(
10484 Flex::column().width(300.0).height(400.0).child(
10485 SingleChildScrollView::new()
10486 .width(300.0)
10487 .height(400.0)
10488 .child(
10489 Flex::column()
10490 .child(Button::new("Go").on_press(7usize))
10491 .child(Container::new().width(300.0).height(1200.0)),
10492 ),
10493 ),
10494 )
10495 }
10496
10497 fn frame(view: &dyn Widget<usize>, runtime: &Runtime) -> Ui<usize> {
10498 build_ui(view, SCREEN, runtime, &Theme::default())
10499 }
10500
10501 fn idle(runtime: &mut Runtime, seconds: f32) {
10503 for _ in 0..(seconds / DT) as usize {
10504 runtime.advance(DT);
10505 runtime.advance_ink(DT);
10506 }
10507 }
10508
10509 fn painted(view: &dyn Widget<usize>, runtime: &Runtime) -> Vec<Primitive> {
10511 frame(view, runtime).scene().primitives().to_vec()
10512 }
10513
10514 fn untouched(view: &dyn Widget<usize>, runtime: &Runtime) -> Vec<Primitive> {
10516 let mut quiet = Runtime::default();
10517 quiet.scroll.clone_from(&runtime.scroll);
10518 quiet.scroll_target.clone_from(&runtime.scroll_target);
10519 idle(&mut quiet, 0.5);
10520 painted(view, &quiet)
10521 }
10522
10523 struct Pointer<'a> {
10525 view: &'a dyn Widget<usize>,
10526 runtime: Runtime,
10527 hover: Hover,
10528 drag: Option<Drag>,
10529 touch: bool,
10530 at: Point,
10531 }
10532
10533 impl<'a> Pointer<'a> {
10534 fn new(view: &'a dyn Widget<usize>, touch: bool) -> Self {
10535 Self {
10536 view,
10537 runtime: Runtime::default(),
10538 hover: Hover::default(),
10539 drag: None,
10540 touch,
10541 at: Point::new(0.0, 0.0),
10542 }
10543 }
10544
10545 fn event(&mut self, kind: PointerKind, at: Point, route: impl FnOnce(&mut Self)) {
10547 self.at = at;
10548 self.hover.event(kind, at, self.touch);
10549 let could_tap = gesture_was_a_tap(self.drag.as_ref());
10550 if kind == PointerKind::Down {
10551 let ui = frame(self.view, &self.runtime);
10552 self.runtime.input.hovered = self.hover.target(&ui);
10553 press_at(&mut self.runtime, Some(&ui), at);
10554 }
10555 route(self);
10556 settle_press(
10557 &mut self.runtime.input,
10558 kind,
10559 could_tap,
10560 self.drag.as_ref(),
10561 self.touch,
10562 );
10563 if matches!(kind, PointerKind::Up | PointerKind::Cancel) {
10564 let ui = frame(self.view, &self.runtime);
10565 self.runtime.input.hovered = self.hover.target(&ui);
10566 }
10567 }
10568
10569 fn land(&mut self, at: Point) {
10571 self.event(PointerKind::Down, at, |p| {
10572 if !p.touch {
10573 return;
10574 }
10575 let ui = frame(p.view, &p.runtime);
10576 let area = ui
10577 .scroll_chain(at)
10578 .find(|area| area.accepts_user_offset((0.0, 0.0)))
10579 .expect("the region under the finger has somewhere to go");
10580 p.drag = Some(Drag::Scroll {
10581 id: area.id,
10582 last: at,
10583 moved: false,
10584 carried: (0.0, 0.0),
10585 dismiss: None,
10586 axis: None,
10587 pan: None,
10588 });
10589 });
10590 }
10591
10592 fn slide(&mut self, by: (f32, f32)) {
10595 let to = Point::new(self.at.x + by.0, self.at.y + by.1);
10596 self.event(PointerKind::Move, to, |p| {
10597 let Some(Drag::Scroll {
10598 id,
10599 moved,
10600 axis,
10601 last,
10602 ..
10603 }) = p.drag.as_mut()
10604 else {
10605 return;
10606 };
10607 let (dx, dy) = (to.x - last.x, to.y - last.y);
10608 if dx.hypot(dy) <= super::TOUCH_SLOP && !*moved {
10609 return;
10610 }
10611 *moved = true;
10612 *axis = Some(dy.abs() >= dx.abs());
10613 *last = to;
10614 let offset = p.runtime.scroll.get(id).copied().unwrap_or((0.0, 0.0));
10615 let moved_to = (offset.0 - by.0, offset.1 - by.1);
10616 p.runtime.scroll.insert(*id, moved_to);
10617 p.runtime.scroll_target.insert(*id, moved_to);
10618 });
10619 }
10620
10621 fn lift(&mut self) -> Option<Drag> {
10623 let mut ended = None;
10624 self.event(PointerKind::Up, self.at, |p| ended = p.drag.take());
10625 ended
10626 }
10627 }
10628
10629 #[test]
10633 fn a_segment_a_swipe_started_on_is_not_left_pressed() {
10634 let view = filters();
10635 let mut finger = Pointer::new(view.as_ref(), true);
10636 let done = Point::new(200.0, 16.0);
10637 finger.land(done);
10638 let segment = finger
10639 .runtime
10640 .input
10641 .pressed
10642 .expect("the segment is pressed");
10643 assert!(
10644 finger.runtime.ink.contains_key(&segment),
10645 "and it splashed: a segment takes ink"
10646 );
10647 assert!(
10648 frame(view.as_ref(), &finger.runtime)
10649 .scroll_chain(done)
10650 .any(|area| area.max_x > 0.0),
10651 "the strip is wider than the screen, as the phone's was"
10652 );
10653 for _ in 0..19 {
10654 finger.slide((-10.0, 0.0));
10655 }
10656 let ended = finger.lift();
10657 assert!(
10658 !gesture_was_a_tap(ended.as_ref()),
10659 "a gesture that scrolled clicks nothing"
10660 );
10661 idle(&mut finger.runtime, 2.0);
10662 let (shown, quiet) = (
10664 painted(view.as_ref(), &finger.runtime),
10665 untouched(view.as_ref(), &finger.runtime),
10666 );
10667 assert_eq!(
10668 shown.len(),
10669 quiet.len(),
10670 "two seconds on, the screen paints what nobody touched, and no layer more"
10671 );
10672 assert_eq!(shown, quiet, "nothing is highlighted");
10673 assert!(
10674 finger.runtime.ink.is_empty(),
10675 "no ink waits for a finger that has gone"
10676 );
10677 assert_eq!(finger.runtime.input.pressed, None, "the press went with it");
10678 assert_eq!(
10679 finger.runtime.input.hovered, None,
10680 "and a lifted finger hovers nothing"
10681 );
10682 }
10683
10684 #[test]
10687 fn the_press_ends_when_the_scroll_takes_it() {
10688 let view = filters();
10689 let mut finger = Pointer::new(view.as_ref(), true);
10690 finger.land(Point::new(200.0, 16.0));
10691 finger.slide((-4.0, 0.0));
10692 assert!(
10693 finger.runtime.input.pressed.is_some(),
10694 "under the slop it may still be a tap"
10695 );
10696 finger.slide((-40.0, 0.0));
10697 assert_eq!(
10698 finger.runtime.input.pressed, None,
10699 "past it, the scroll has it"
10700 );
10701 assert_eq!(
10702 finger.runtime.input.hovered, None,
10703 "and the finger's hover with it"
10704 );
10705 idle(&mut finger.runtime, 0.5);
10706 assert_eq!(
10707 painted(view.as_ref(), &finger.runtime),
10708 untouched(view.as_ref(), &finger.runtime),
10709 "the finger is still down, and nothing under it is highlighted"
10710 );
10711 }
10712
10713 #[test]
10715 fn a_button_a_page_scrolled_from_is_not_left_pressed() {
10716 let view = page();
10717 let mut finger = Pointer::new(view.as_ref(), true);
10718 let go = Point::new(20.0, 15.0);
10719 finger.land(go);
10720 let button = finger.runtime.input.pressed.expect("the button is pressed");
10721 assert_eq!(
10722 frame(view.as_ref(), &finger.runtime).msg_for(button),
10723 Some(7),
10724 "the press is on the button"
10725 );
10726 for _ in 0..10 {
10727 finger.slide((0.0, -30.0));
10728 }
10729 assert!(!gesture_was_a_tap(finger.lift().as_ref()));
10730 idle(&mut finger.runtime, 2.0);
10731 assert_eq!(finger.runtime.input.pressed, None);
10732 assert!(finger.runtime.ink.is_empty());
10733 assert_eq!(
10734 painted(view.as_ref(), &finger.runtime),
10735 untouched(view.as_ref(), &finger.runtime),
10736 );
10737 }
10738
10739 #[test]
10742 fn a_tap_still_presses_and_clicks() {
10743 let view = filters();
10744 let mut finger = Pointer::new(view.as_ref(), true);
10745 let segment = Point::new(200.0, 16.0);
10746 finger.land(segment);
10747 finger.slide((3.0, 2.0));
10748 let pressed = finger.runtime.input.pressed;
10749 assert!(pressed.is_some(), "a wobble under the slop takes nothing");
10750 assert_eq!(finger.runtime.input.hovered, pressed, "and the press shows");
10751 let ui = frame(view.as_ref(), &finger.runtime);
10753 assert_eq!(
10754 ui.hit(segment),
10755 pressed,
10756 "released on the widget it pressed"
10757 );
10758 assert!(
10759 pressed.and_then(|id| ui.msg_for(id)).is_some(),
10760 "and the segment it pressed has a choice to make"
10761 );
10762 let ended = finger.lift();
10763 assert!(
10764 gesture_was_a_tap(ended.as_ref()),
10765 "a scroll that never moved"
10766 );
10767 assert_eq!(
10768 finger.runtime.input.pressed, None,
10769 "let go of once released"
10770 );
10771 }
10772
10773 #[test]
10776 fn a_mouse_keeps_its_press_while_it_moves_and_its_hover_after() {
10777 let view = filters();
10778 let mut mouse = Pointer::new(view.as_ref(), false);
10779 let done = Point::new(200.0, 16.0);
10780 mouse.land(done);
10781 assert!(mouse.drag.is_none(), "a mouse does not drag a scroll");
10782 let pressed = mouse.runtime.input.pressed.expect("pressed");
10783 mouse.slide((-60.0, 0.0));
10784 assert_eq!(mouse.runtime.input.pressed, Some(pressed), "still held");
10785 mouse.lift();
10786 assert_eq!(mouse.runtime.input.pressed, None);
10787 assert!(
10788 mouse.runtime.input.hovered.is_some(),
10789 "a released mouse is still over the strip"
10790 );
10791 }
10792
10793 #[test]
10796 fn a_gesture_takes_a_mouse_s_press_and_leaves_its_hover() {
10797 let id = frus_widgets::WidgetId::from_u64(3);
10798 let sheet = |moved| Drag::Sheet {
10799 id: frus_widgets::WidgetId::from_u64(9),
10800 last: Point::new(0.0, 0.0),
10801 moved,
10802 available: 800.0,
10803 };
10804 for touch in [false, true] {
10805 let mut input = frus_widgets::InputState {
10806 pressed: Some(id),
10807 hovered: Some(id),
10808 ..Default::default()
10809 };
10810 assert!(!settle_press(
10811 &mut input,
10812 PointerKind::Move,
10813 true,
10814 Some(&sheet(false)),
10815 touch
10816 ));
10817 assert_eq!(input.pressed, Some(id), "not yet past the slop");
10818 assert!(settle_press(
10819 &mut input,
10820 PointerKind::Move,
10821 true,
10822 Some(&sheet(true)),
10823 touch
10824 ));
10825 assert_eq!(input.pressed, None, "touch {touch}");
10826 assert_eq!(input.hovered, (!touch).then_some(id), "touch {touch}");
10827 }
10828 let mut input = frus_widgets::InputState {
10831 pressed: Some(id),
10832 ..Default::default()
10833 };
10834 let select = Drag::TextSelect {
10835 id,
10836 rect: super::Rect::new(0.0, 0.0, 10.0, 10.0),
10837 };
10838 assert!(!settle_press(
10839 &mut input,
10840 PointerKind::Move,
10841 false,
10842 Some(&select),
10843 true
10844 ));
10845 assert_eq!(input.pressed, Some(id));
10846 }
10847}
10848
10849#[cfg(test)]
10852mod selection_area_tests {
10853 use super::testing::Driver;
10854 use crate::{Application, Command};
10855 use frus_widgets::{Button, Container, Flex, Point, SelectionArea, Text, Theme, Widget};
10856
10857 const W: f32 = 400.0;
10858 const H: f32 = 300.0;
10859
10860 #[derive(Default)]
10862 struct Doc {
10863 bar: Bar,
10865 shared: std::rc::Rc<std::cell::Cell<u32>>,
10867 platform: Option<frus_widgets::TargetPlatform>,
10870 }
10871
10872 #[derive(Default, Clone, Copy)]
10874 enum Bar {
10875 #[default]
10877 Default,
10878 Share,
10880 Empty,
10882 }
10883
10884 impl Application for Doc {
10885 type Message = ();
10886
10887 fn update(&mut self, _message: ()) -> Command<()> {
10888 self.shared.set(self.shared.get() + 1);
10889 Command::none()
10890 }
10891
10892 fn theme(&self) -> Theme {
10893 Theme::default().with_platform(
10894 self.platform
10895 .unwrap_or(frus_widgets::TargetPlatform::Android),
10896 )
10897 }
10898
10899 fn view(&self, _theme: &Theme) -> Box<dyn Widget<()>> {
10900 let area = SelectionArea::around(
10901 Flex::column()
10902 .child(Text::new("first line of words").no_wrap())
10903 .child(Text::new("second line").no_wrap())
10904 .child(Button::new("Press").on_press(()))
10905 .child(Text::new("third line ends here").no_wrap()),
10906 );
10907 let area = match self.bar {
10908 Bar::Default => area,
10909 Bar::Share => area.selection_toolbar(|_, items| {
10910 let mut items: Vec<_> = items
10911 .into_iter()
10912 .filter(|item| item.label() == "Copy")
10913 .collect();
10914 items.push(frus_widgets::ToolbarItem::custom("Share", ()));
10915 items
10916 }),
10917 Bar::Empty => area.selection_toolbar(|_, _| Vec::new()),
10918 };
10919 Box::new(
10920 Container::new().width(W).height(H).child(
10921 Flex::column()
10922 .child(Text::new("Outside the area").no_wrap())
10923 .child(area),
10924 ),
10925 )
10926 }
10927 }
10928
10929 struct Tappable;
10931
10932 impl Application for Tappable {
10933 type Message = ();
10934
10935 fn update(&mut self, _message: ()) -> Command<()> {
10936 Command::none()
10937 }
10938
10939 fn view(&self, _theme: &Theme) -> Box<dyn Widget<()>> {
10940 Box::new(
10941 Container::new()
10942 .width(W)
10943 .height(H)
10944 .child(SelectionArea::around(
10945 Flex::column()
10946 .child(
10947 Container::new()
10948 .on_click(())
10949 .child(Text::new("tap this row").no_wrap()),
10950 )
10951 .child(Text::new("plain words here").no_wrap()),
10952 )),
10953 )
10954 }
10955 }
10956
10957 #[test]
10961 fn a_row_that_takes_a_tap_keeps_its_press() {
10962 let mut d = Driver::new(Tappable, W, H);
10963 d.run(0.2);
10964 let find = |d: &Driver<Tappable>, label: &str| {
10965 let (_, rect) = d
10966 .texts()
10967 .into_iter()
10968 .find(|(text, _)| text == label)
10969 .expect("on screen");
10970 Point::new(rect.x + 20.0, rect.y + 8.0)
10971 };
10972 let (tap, plain) = (find(&d, "tap this row"), find(&d, "plain words here"));
10973 let end = Point::new(plain.x + 60.0, plain.y);
10974 d.press_mouse(tap);
10975 d.run(0.05);
10976 d.move_mouse(end);
10977 d.run(0.05);
10978 d.release_mouse(end);
10979 d.run(0.05);
10980 assert_eq!(d.area_selection(), None, "the row's press stayed the row's");
10981
10982 d.press_mouse(plain);
10983 d.run(0.05);
10984 d.move_mouse(end);
10985 d.run(0.05);
10986 d.release_mouse(end);
10987 d.run(0.05);
10988 assert!(d.area_selection().is_some(), "plain words still select");
10989 }
10990
10991 fn driver() -> Driver<Doc> {
10992 driver_with(Bar::Default)
10993 }
10994
10995 fn driver_with(bar: Bar) -> Driver<Doc> {
10996 driver_on(bar, None)
10997 }
10998
10999 fn driver_on(bar: Bar, platform: Option<frus_widgets::TargetPlatform>) -> Driver<Doc> {
11000 let doc = Doc {
11001 bar,
11002 platform,
11003 ..Doc::default()
11004 };
11005 let mut driver = Driver::new(doc, W, H);
11006 driver.run(0.2);
11007 driver
11008 }
11009
11010 fn on(driver: &Driver<Doc>, label: &str, across: f32) -> Point {
11013 let (_, rect) = driver
11014 .texts()
11015 .into_iter()
11016 .find(|(text, _)| text == label)
11017 .unwrap_or_else(|| panic!("{label:?} is on screen"));
11018 Point::new(rect.x + across, rect.y + 8.0)
11021 }
11022
11023 fn drag(driver: &mut Driver<Doc>, from: Point, to: Point) {
11024 driver.press_mouse(from);
11025 driver.run(0.05);
11026 driver.move_mouse(to);
11027 driver.run(0.05);
11028 driver.release_mouse(to);
11029 driver.run(0.05);
11030 }
11031
11032 #[test]
11036 fn a_drag_selects_across_the_texts_of_an_area() {
11037 let mut d = driver();
11038 let (from, to) = (
11039 on(&d, "first line of words", 60.0),
11040 on(&d, "third line ends here", 40.0),
11041 );
11042 drag(&mut d, from, to);
11043 let copied = d.area_selection().expect("something is selected");
11044 let lines: Vec<&str> = copied.lines().collect();
11045 assert_eq!(lines.len(), 3, "one line per text: {copied:?}");
11046 assert!(
11047 !lines[0].is_empty() && "first line of words".ends_with(lines[0]),
11048 "the tail of the first: {:?}",
11049 lines[0]
11050 );
11051 assert!(lines[0].len() < "first line of words".len());
11052 assert_eq!(lines[1], "second line", "the whole of the middle one");
11053 assert!(
11054 !lines[2].is_empty() && "third line ends here".starts_with(lines[2]),
11055 "the head of the last: {:?}",
11056 lines[2]
11057 );
11058 assert!(lines[2].len() < "third line ends here".len());
11059 assert!(!copied.contains("Press"), "a button's label is not text");
11060 }
11061
11062 #[test]
11064 fn dragging_upward_selects_the_same_words() {
11065 let (down, up) = {
11066 let mut d = driver();
11067 let (a, b) = (
11068 on(&d, "first line of words", 60.0),
11069 on(&d, "third line ends here", 40.0),
11070 );
11071 drag(&mut d, a, b);
11072 let down = d.area_selection();
11073 drag(&mut d, b, a);
11074 (down, d.area_selection())
11075 };
11076 assert!(down.is_some());
11077 assert_eq!(down, up);
11078 }
11079
11080 #[test]
11083 fn a_drag_past_the_words_ends_at_the_last_of_them() {
11084 let mut d = driver();
11085 let from = on(&d, "second line", 5.0);
11086 drag(&mut d, from, Point::new(W - 2.0, H - 2.0));
11087 let copied = d.area_selection().expect("a selection");
11088 assert!(copied.ends_with("third line ends here"), "{copied:?}");
11089 }
11090
11091 #[test]
11094 fn words_outside_the_area_are_left_alone() {
11095 let mut d = driver();
11096 let outside = on(&d, "Outside the area", 30.0);
11097 let end = on(&d, "third line ends here", 40.0);
11098 drag(&mut d, outside, end);
11099 assert_eq!(d.area_selection(), None, "the press began outside");
11100
11101 let inside = on(&d, "first line of words", 30.0);
11102 drag(&mut d, inside, Point::new(20.0, 2.0));
11103 let copied = d.area_selection().expect("a selection");
11104 assert!(!copied.contains("Outside"), "{copied:?}");
11105 }
11106
11107 #[test]
11109 fn a_button_keeps_its_press() {
11110 let mut d = driver();
11111 let button = on(&d, "Press", 10.0);
11112 let end = on(&d, "third line ends here", 40.0);
11113 drag(&mut d, button, end);
11114 assert_eq!(d.area_selection(), None);
11115 }
11116
11117 #[test]
11119 fn a_click_puts_the_selection_away() {
11120 let mut d = driver();
11121 let (from, to) = (
11122 on(&d, "first line of words", 60.0),
11123 on(&d, "third line ends here", 40.0),
11124 );
11125 drag(&mut d, from, to);
11126 assert!(d.area_selection().is_some());
11127 let elsewhere = on(&d, "second line", 30.0);
11128 drag(&mut d, elsewhere, elsewhere);
11129 assert_eq!(d.area_selection(), None, "a click in the area");
11130
11131 drag(&mut d, from, to);
11132 assert!(d.area_selection().is_some());
11133 let outside = on(&d, "Outside the area", 30.0);
11134 drag(&mut d, outside, outside);
11135 assert_eq!(d.area_selection(), None, "a click outside it");
11136 }
11137
11138 #[test]
11140 fn a_double_click_selects_the_word() {
11141 let mut d = driver();
11142 let at = on(&d, "second line", 12.0);
11144 d.press_mouse(at);
11145 d.release_mouse(at);
11146 d.run(0.05);
11147 d.press_mouse(at);
11148 d.release_mouse(at);
11149 d.run(0.05);
11150 assert_eq!(d.area_selection().as_deref(), Some("second"));
11151 }
11152
11153 #[test]
11155 fn select_all_takes_the_whole_area() {
11156 let mut d = driver();
11157 let at = on(&d, "second line", 12.0);
11158 let end = on(&d, "second line", 40.0);
11159 drag(&mut d, at, end);
11160 assert!(d.area_selection().is_some());
11161 d.area_select_all();
11162 assert_eq!(
11163 d.area_selection().as_deref(),
11164 Some("first line of words\nsecond line\nthird line ends here")
11165 );
11166 }
11167
11168 #[test]
11170 fn a_finger_does_not_select() {
11171 let mut d = driver();
11172 let (from, to) = (
11173 on(&d, "first line of words", 60.0),
11174 on(&d, "third line ends here", 40.0),
11175 );
11176 d.press(from);
11177 d.run(0.05);
11178 d.move_to(to);
11179 d.run(0.05);
11180 d.release(to);
11181 d.run(0.05);
11182 assert_eq!(d.area_selection(), None);
11183 }
11184
11185 fn hold(d: &mut Driver<Doc>, at: Point) -> bool {
11187 d.press(at);
11188 d.run(0.1);
11189 let fired = d.hold_deadline();
11190 d.run(0.05);
11191 d.release(at);
11192 d.run(0.05);
11193 fired
11194 }
11195
11196 fn bar_button(d: &Driver<Doc>, label: &str) -> Option<Point> {
11198 let (ui, _) = d.frame_parts()?;
11199 d.texts()
11200 .into_iter()
11201 .filter(|(text, _)| text == label)
11202 .map(|(_, rect)| Point::new(rect.x + 4.0, rect.y + 6.0))
11203 .find(|at| ui.toolbar_contains(*at))
11204 }
11205
11206 #[test]
11209 fn a_hold_selects_the_word_and_opens_the_bar() {
11210 let mut d = driver();
11211 let at = on(&d, "second line", 12.0);
11212 assert!(hold(&mut d, at), "the hold fired");
11213 assert_eq!(d.area_selection().as_deref(), Some("second"));
11214 assert!(bar_button(&d, "Copy").is_some(), "Copy is on the bar");
11215 assert!(bar_button(&d, "Select all").is_some(), "and Select all");
11216 }
11217
11218 #[test]
11221 fn the_desktop_menus_rows_act_on_the_area() {
11222 let mut d = driver_on(Bar::Default, Some(frus_widgets::TargetPlatform::Linux));
11223 let at = on(&d, "second line", 12.0);
11224 hold(&mut d, at);
11225 let select_all = bar_button(&d, "Select all").expect("Select all");
11226 d.press(select_all);
11229 d.release(select_all);
11230 d.run(0.05);
11231 assert_eq!(
11232 d.area_selection().as_deref(),
11233 Some("first line of words\nsecond line\nthird line ends here")
11234 );
11235 }
11236
11237 #[test]
11240 fn the_bars_buttons_act_on_the_area() {
11241 let mut d = driver();
11242 let at = on(&d, "second line", 12.0);
11243 hold(&mut d, at);
11244 let select_all = bar_button(&d, "Select all").expect("Select all");
11245 d.press(select_all);
11246 d.release(select_all);
11247 d.run(0.05);
11248 assert_eq!(
11249 d.area_selection().as_deref(),
11250 Some("first line of words\nsecond line\nthird line ends here")
11251 );
11252 assert!(
11253 bar_button(&d, "Select all").is_none(),
11254 "nothing left to select"
11255 );
11256 let copy = bar_button(&d, "Copy").expect("Copy stays");
11257 d.press(copy);
11258 d.release(copy);
11259 d.run(0.05);
11260 assert_eq!(d.area_selection(), None, "copied, and put away");
11261 assert!(bar_button(&d, "Copy").is_none(), "the bar with it");
11262 }
11263
11264 #[test]
11266 fn a_hold_on_a_button_selects_nothing() {
11267 let mut d = driver();
11268 let button = on(&d, "Press", 10.0);
11269 hold(&mut d, button);
11270 assert_eq!(d.area_selection(), None);
11271 }
11272
11273 #[test]
11275 fn escape_and_a_tap_put_the_bar_away() {
11276 let mut d = driver();
11277 let at = on(&d, "second line", 12.0);
11278 hold(&mut d, at);
11279 assert!(d.area_selection().is_some());
11280 d.escape();
11281 d.run(0.05);
11282 assert_eq!(d.area_selection(), None);
11283 assert!(bar_button(&d, "Copy").is_none());
11284
11285 hold(&mut d, at);
11286 assert!(d.area_selection().is_some());
11287 let elsewhere = on(&d, "Outside the area", 20.0);
11288 d.press(elsewhere);
11289 d.release(elsewhere);
11290 d.run(0.05);
11291 assert_eq!(d.area_selection(), None);
11292 assert!(bar_button(&d, "Copy").is_none());
11293 }
11294
11295 #[test]
11298 fn a_mouse_selection_has_no_bar() {
11299 let mut d = driver();
11300 let (from, to) = (
11301 on(&d, "first line of words", 60.0),
11302 on(&d, "third line ends here", 40.0),
11303 );
11304 drag(&mut d, from, to);
11305 assert!(d.area_selection().is_some());
11306 assert!(bar_button(&d, "Copy").is_none());
11307 }
11308
11309 #[test]
11312 fn the_application_says_what_the_bar_holds() {
11313 let mut d = driver_with(Bar::Share);
11314 let at = on(&d, "second line", 12.0);
11315 assert!(hold(&mut d, at));
11316 assert_eq!(d.area_selection().as_deref(), Some("second"));
11317 assert!(bar_button(&d, "Copy").is_some(), "Copy was kept");
11318 assert!(
11319 bar_button(&d, "Select all").is_none(),
11320 "Select all was taken off"
11321 );
11322 let share = bar_button(&d, "Share").expect("the application's own item");
11323 let shared = d.app().shared.clone();
11324 d.press(share);
11325 d.release(share);
11326 d.run(0.05);
11327 assert_eq!(shared.get(), 1, "its message was sent");
11328 }
11329
11330 #[test]
11332 fn an_empty_list_shows_no_bar() {
11333 let mut d = driver_with(Bar::Empty);
11334 let at = on(&d, "second line", 12.0);
11335 assert!(hold(&mut d, at));
11336 assert_eq!(d.area_selection().as_deref(), Some("second"));
11337 assert!(bar_button(&d, "Copy").is_none());
11338 assert!(d
11339 .frame_parts()
11340 .is_some_and(|(ui, _)| !ui.toolbar_contains(at)));
11341 }
11342
11343 fn middle(rect: frus_widgets::Rect) -> Point {
11345 Point::new(rect.x + rect.width / 2.0, rect.y + rect.height / 2.0)
11346 }
11347
11348 fn carry_handle(d: &mut Driver<Doc>, from: Point, to: Point) {
11350 d.press(from);
11351 d.run(0.05);
11352 for step in 1..=4 {
11353 let f = step as f32 / 4.0;
11354 d.move_to(Point::new(
11355 from.x + (to.x - from.x) * f,
11356 from.y + (to.y - from.y) * f,
11357 ));
11358 d.run(0.05);
11359 }
11360 d.release(to);
11361 d.run(0.05);
11362 }
11363
11364 #[test]
11367 fn a_touch_selection_has_handles_and_a_mouse_one_does_not() {
11368 let mut d = driver();
11369 let paths = |d: &Driver<Doc>| {
11370 let (ui, _) = d.frame_parts().expect("a frame");
11371 ui.scene()
11372 .primitives()
11373 .iter()
11374 .filter(|p| matches!(p, frus_widgets::Primitive::Path { .. }))
11375 .count()
11376 };
11377 let before = paths(&d);
11378 let at = on(&d, "second line", 12.0);
11379 hold(&mut d, at);
11380 assert_eq!(paths(&d), before + 2, "the two handles are painted");
11381 let [start, end] = d.area_handles().expect("two handles");
11382 assert!(start.line_center.x < end.line_center.x, "{start:?} {end:?}");
11383 assert!(start.rect.y > at.y - 8.0, "they hang below the line");
11384
11385 let (from, to) = (
11386 on(&d, "first line of words", 60.0),
11387 on(&d, "third line ends here", 40.0),
11388 );
11389 drag(&mut d, from, to);
11390 assert!(d.area_selection().is_some());
11391 assert!(
11392 d.area_handles().is_none(),
11393 "a mouse selection has no handles"
11394 );
11395 d.area_select_all();
11396 assert!(
11397 d.area_handles().is_none(),
11398 "nor does it grow any when the keyboard selects all"
11399 );
11400 }
11401
11402 #[test]
11406 fn the_end_handle_widens_the_selection_across_texts() {
11407 let mut d = driver();
11408 let at = on(&d, "second line", 12.0);
11409 hold(&mut d, at);
11410 let [_, end] = d.area_handles().expect("handles");
11411 let third = on(&d, "third line ends here", 60.0);
11412 let from = middle(end.rect);
11413 let to = Point::new(third.x, from.y + (third.y - at.y));
11415 d.press(from);
11416 d.run(0.05);
11417 d.move_to(Point::new(from.x, (from.y + to.y) / 2.0));
11418 d.run(0.05);
11419 d.move_to(to);
11420 d.run(0.05);
11421 assert!(
11422 bar_button(&d, "Copy").is_none(),
11423 "no bar while a handle moves"
11424 );
11425 d.release(to);
11426 d.run(0.05);
11427 let copied = d.area_selection().expect("a selection");
11428 let lines: Vec<&str> = copied.lines().collect();
11429 assert_eq!(lines.len(), 2, "{copied:?}");
11430 assert_eq!(lines[0], "second line", "the rest of the word's line");
11431 assert!("third line ends here".starts_with(lines[1]), "{copied:?}");
11432 assert!(!lines[1].is_empty());
11433 assert!(bar_button(&d, "Copy").is_some(), "the bar is back");
11434 }
11435
11436 #[test]
11439 fn the_start_handle_widens_it_upward() {
11440 let mut d = driver();
11441 let at = on(&d, "second line", 80.0);
11442 hold(&mut d, at);
11443 assert_eq!(d.area_selection().as_deref(), Some("line"));
11444 let [start, _] = d.area_handles().expect("handles");
11445 let first = on(&d, "first line of words", 30.0);
11446 let from = middle(start.rect);
11447 let to = Point::new(first.x, from.y + (first.y - at.y));
11448 carry_handle(&mut d, from, to);
11449 let copied = d.area_selection().expect("a selection");
11450 let lines: Vec<&str> = copied.lines().collect();
11451 assert_eq!(lines.len(), 2, "{copied:?}");
11452 assert!("first line of words".ends_with(lines[0]), "{copied:?}");
11453 assert_eq!(lines[1], "second line");
11454 }
11455
11456 #[test]
11459 fn a_handle_does_not_cross_the_other() {
11460 let mut d = driver();
11461 let at = on(&d, "second line", 80.0);
11462 hold(&mut d, at);
11463 let before = d.area_selection();
11464 assert!(before.is_some());
11465 let [_, end] = d.area_handles().expect("handles");
11466 let first = on(&d, "first line of words", 5.0);
11467 let from = middle(end.rect);
11468 let to = Point::new(first.x, from.y + (first.y - at.y));
11470 d.press(from);
11471 d.run(0.05);
11472 d.move_to(to);
11473 d.run(0.05);
11474 d.release(to);
11475 d.run(0.05);
11476 assert_eq!(d.area_selection(), before);
11477 }
11478
11479 #[test]
11482 fn the_selection_is_highlighted_in_each_text() {
11483 let mut d = driver();
11484 let (from, to) = (
11485 on(&d, "first line of words", 60.0),
11486 on(&d, "third line ends here", 40.0),
11487 );
11488 let bare = rects(&d);
11489 drag(&mut d, from, to);
11490 let boxes: Vec<frus_widgets::Rect> = rects(&d)
11492 .into_iter()
11493 .filter(|rect| !bare.contains(rect))
11494 .collect();
11495 assert_eq!(boxes.len(), 3, "one per text covered: {boxes:?}");
11496 let outside = on(&d, "Outside the area", 0.0);
11497 assert!(
11498 boxes.iter().all(|b| b.y > outside.y + 10.0),
11499 "none over the line outside the area: {boxes:?}"
11500 );
11501 }
11502
11503 fn rects(d: &Driver<Doc>) -> Vec<frus_widgets::Rect> {
11504 let (ui, _) = d.frame_parts().expect("a frame");
11505 ui.scene()
11506 .primitives()
11507 .iter()
11508 .filter_map(|p| match p {
11509 frus_widgets::Primitive::Rect { rect, .. } => Some(*rect),
11510 _ => None,
11511 })
11512 .collect()
11513 }
11514}
11515
11516#[cfg(test)]
11519mod gesture_detector_tests {
11520 use super::testing::Driver;
11521 use crate::{Application, Command};
11522 use frus_widgets::{Container, Flex, GestureDetector, Point, Text, Theme, Widget};
11523 use std::cell::RefCell;
11524 use std::rc::Rc;
11525
11526 const W: f32 = 400.0;
11527 const H: f32 = 400.0;
11528
11529 #[derive(Clone, Debug, PartialEq)]
11530 enum Msg {
11531 Tap,
11532 Double,
11533 Hold,
11534 Menu,
11535 Plain,
11536 OnlyDouble,
11537 }
11538
11539 #[derive(Default)]
11542 struct Page {
11543 heard: Rc<RefCell<Vec<Msg>>>,
11544 }
11545
11546 impl Application for Page {
11547 type Message = Msg;
11548
11549 fn update(&mut self, message: Msg) -> Command<Msg> {
11550 self.heard.borrow_mut().push(message);
11551 Command::none()
11552 }
11553
11554 fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
11555 let block = |label: &str| {
11556 Container::new()
11557 .width(300.0)
11558 .height(60.0)
11559 .child(Text::new(label).no_wrap())
11560 };
11561 Box::new(
11562 Container::new().width(W).height(H).child(
11563 Flex::column()
11564 .gap(40.0)
11565 .child(
11566 GestureDetector::new(block("everything"))
11567 .on_tap(Msg::Tap)
11568 .on_double_tap(Msg::Double)
11569 .on_long_press(Msg::Hold)
11570 .on_secondary_tap(Msg::Menu),
11571 )
11572 .child(GestureDetector::new(block("tap only")).on_tap(Msg::Plain))
11573 .child(
11574 GestureDetector::new(block("double only"))
11575 .on_double_tap(Msg::OnlyDouble),
11576 ),
11577 ),
11578 )
11579 }
11580 }
11581
11582 fn driver() -> (Driver<Page>, Rc<RefCell<Vec<Msg>>>) {
11583 let page = Page::default();
11584 let heard = page.heard.clone();
11585 let mut driver = Driver::new(page, W, H);
11586 driver.run(0.2);
11587 (driver, heard)
11588 }
11589
11590 fn on(d: &Driver<Page>, label: &str) -> Point {
11592 let (_, rect) = d
11593 .texts()
11594 .into_iter()
11595 .find(|(text, _)| text == label)
11596 .unwrap_or_else(|| panic!("{label:?} is on screen"));
11597 Point::new(rect.x + 20.0, rect.y + 8.0)
11598 }
11599
11600 fn tap(d: &mut Driver<Page>, at: Point) {
11601 d.press(at);
11602 d.run(0.02);
11603 d.release(at);
11604 d.run(0.02);
11605 }
11606
11607 #[test]
11610 fn a_tap_waits_while_a_double_tap_is_possible() {
11611 let (mut d, heard) = driver();
11612 let at = on(&d, "everything");
11613 tap(&mut d, at);
11614 assert!(heard.borrow().is_empty(), "not yet: {:?}", heard.borrow());
11615 assert!(d.tap_deadline(), "a tap was waiting");
11616 assert_eq!(*heard.borrow(), vec![Msg::Tap]);
11617 }
11618
11619 #[test]
11622 fn the_loop_wakes_for_a_waiting_tap() {
11623 let (mut d, _) = driver();
11624 let before = std::time::Instant::now();
11625 let at = on(&d, "everything");
11626 tap(&mut d, at);
11627 let wake = d.next_wake().expect("a wake for the tap");
11628 let wait = wake.duration_since(before);
11629 assert!(
11630 wait <= std::time::Duration::from_millis(400),
11631 "about 300 ms: {wait:?}"
11632 );
11633 d.tap_deadline();
11634 assert_eq!(d.next_wake(), None);
11635 }
11636
11637 #[test]
11639 fn two_quick_taps_are_a_double_tap() {
11640 let (mut d, heard) = driver();
11641 let at = on(&d, "everything");
11642 tap(&mut d, at);
11643 tap(&mut d, Point::new(at.x + 6.0, at.y + 2.0));
11644 assert_eq!(*heard.borrow(), vec![Msg::Double]);
11645 assert!(!d.tap_deadline(), "nothing is left waiting");
11646 assert_eq!(*heard.borrow(), vec![Msg::Double]);
11647 }
11648
11649 #[test]
11651 fn a_tap_alone_answers_at_once() {
11652 let (mut d, heard) = driver();
11653 let at = on(&d, "tap only");
11654 tap(&mut d, at);
11655 assert_eq!(*heard.borrow(), vec![Msg::Plain]);
11656 tap(&mut d, at);
11657 assert_eq!(
11658 *heard.borrow(),
11659 vec![Msg::Plain, Msg::Plain],
11660 "two taps are two taps"
11661 );
11662 }
11663
11664 #[test]
11667 fn a_press_elsewhere_sends_the_waiting_tap() {
11668 let (mut d, heard) = driver();
11669 let (first, other) = (on(&d, "everything"), on(&d, "tap only"));
11670 tap(&mut d, first);
11671 tap(&mut d, other);
11672 assert_eq!(*heard.borrow(), vec![Msg::Tap, Msg::Plain]);
11673 }
11674
11675 #[test]
11677 fn a_secondary_click_sends_its_message() {
11678 let (mut d, heard) = driver();
11679 let at = on(&d, "everything");
11680 d.secondary_click(at);
11681 d.run(0.02);
11682 assert_eq!(*heard.borrow(), vec![Msg::Menu]);
11683 let plain = on(&d, "tap only");
11684 d.secondary_click(plain);
11685 assert_eq!(
11686 *heard.borrow(),
11687 vec![Msg::Menu],
11688 "a detector without one hears nothing"
11689 );
11690 }
11691
11692 #[test]
11694 fn a_long_press_is_not_also_a_tap() {
11695 let (mut d, heard) = driver();
11696 let at = on(&d, "everything");
11697 d.press(at);
11698 d.run(0.1);
11699 assert!(d.hold_deadline(), "the hold fired");
11700 d.release(at);
11701 d.run(0.02);
11702 d.tap_deadline();
11703 assert_eq!(*heard.borrow(), vec![Msg::Hold]);
11704 }
11705
11706 #[test]
11709 fn a_double_tap_alone_is_heard() {
11710 let (mut d, heard) = driver();
11711 let at = on(&d, "double only");
11712 tap(&mut d, at);
11713 d.tap_deadline();
11714 assert!(heard.borrow().is_empty(), "{:?}", heard.borrow());
11715 tap(&mut d, at);
11716 tap(&mut d, at);
11717 assert_eq!(*heard.borrow(), vec![Msg::OnlyDouble]);
11718 }
11719}
11720
11721#[cfg(test)]
11724mod gesture_pan_tests {
11725 use super::testing::Driver;
11726 use crate::{Application, Command};
11727 use frus_widgets::{
11728 Container, Flex, GestureDetector, PanAxis, Point, SingleChildScrollView, Text, Theme,
11729 Widget,
11730 };
11731 use std::cell::RefCell;
11732 use std::rc::Rc;
11733
11734 const W: f32 = 400.0;
11735 const H: f32 = 400.0;
11736
11737 #[derive(Clone, Debug, PartialEq)]
11738 enum Msg {
11739 Start(Point),
11740 Update(Point, Point),
11741 End(Point),
11742 Tap,
11743 Hold,
11744 }
11745
11746 struct Page {
11748 axis: PanAxis,
11749 scrolled: bool,
11750 heard: Rc<RefCell<Vec<Msg>>>,
11751 }
11752
11753 impl Application for Page {
11754 type Message = Msg;
11755
11756 fn update(&mut self, message: Msg) -> Command<Msg> {
11757 self.heard.borrow_mut().push(message);
11758 Command::none()
11759 }
11760
11761 fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
11762 let detector = GestureDetector::new(
11763 Container::new()
11764 .width(300.0)
11765 .height(150.0)
11766 .child(Text::new("drag me").no_wrap()),
11767 )
11768 .on_tap(Msg::Tap)
11769 .on_long_press(Msg::Hold)
11770 .on_pan_start(Msg::Start)
11771 .on_pan_update(Msg::Update)
11772 .on_pan_end(Msg::End)
11773 .pan_axis(self.axis);
11774 let content = Flex::column()
11775 .child(Container::new().width(300.0).height(30.0))
11776 .child(detector)
11777 .child(Container::new().width(300.0).height(1200.0));
11778 if self.scrolled {
11779 Box::new(
11780 SingleChildScrollView::new()
11781 .width(W)
11782 .height(H)
11783 .child(content),
11784 )
11785 } else {
11786 Box::new(Container::new().width(W).height(H).child(content))
11787 }
11788 }
11789 }
11790
11791 fn driver(axis: PanAxis, scrolled: bool) -> (Driver<Page>, Rc<RefCell<Vec<Msg>>>) {
11792 let heard = Rc::new(RefCell::new(Vec::new()));
11793 let page = Page {
11794 axis,
11795 scrolled,
11796 heard: heard.clone(),
11797 };
11798 let mut driver = Driver::new(page, W, H);
11799 driver.run(0.2);
11800 (driver, heard)
11801 }
11802
11803 fn origin(d: &Driver<Page>) -> Point {
11805 let (_, rect) = d
11806 .texts()
11807 .into_iter()
11808 .find(|(text, _)| text == "drag me")
11809 .expect("on screen");
11810 Point::new(rect.x, rect.y)
11811 }
11812
11813 fn swipe(d: &mut Driver<Page>, from: Point, dx: f32, dy: f32) {
11815 d.press(from);
11816 d.run(0.02);
11817 for step in 1..=8 {
11818 let f = step as f32 / 8.0;
11819 d.move_to(Point::new(from.x + dx * f, from.y + dy * f));
11820 d.run(0.02);
11821 }
11822 d.release(Point::new(from.x + dx, from.y + dy));
11823 d.run(0.02);
11824 }
11825
11826 fn deltas(heard: &[Msg]) -> Point {
11827 heard.iter().fold(Point::new(0.0, 0.0), |sum, m| match m {
11828 Msg::Update(_, delta) => Point::new(sum.x + delta.x, sum.y + delta.y),
11829 _ => sum,
11830 })
11831 }
11832
11833 fn scroll_y(d: &Driver<Page>) -> f32 {
11834 d.offsets().first().map_or(0.0, |(_, offset)| offset.1)
11835 }
11836
11837 #[test]
11841 fn a_drag_is_a_start_updates_and_an_end() {
11842 let (mut d, heard) = driver(PanAxis::Free, false);
11843 let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 40.0);
11844 swipe(&mut d, at, 120.0, 60.0);
11845 let heard = heard.borrow();
11846 assert!(matches!(heard.first(), Some(Msg::Start(_))), "{heard:?}");
11847 if let Some(Msg::Start(local)) = heard.first() {
11848 let corner = origin(&d);
11850 assert!(corner.y >= 30.0, "{corner:?}");
11851 assert_eq!(*local, Point::new(at.x - corner.x, at.y - corner.y));
11852 }
11853 assert!(matches!(heard.last(), Some(Msg::End(_))), "{heard:?}");
11854 if let Some(Msg::End(velocity)) = heard.last() {
11855 assert!(velocity.x.hypot(velocity.y) <= 8000.0 + 0.5, "{velocity:?}");
11856 }
11857 let moved = deltas(&heard);
11858 assert!(moved.x > 60.0 && moved.y > 20.0, "{moved:?}");
11859 assert!(!heard.contains(&Msg::Tap), "a drag is not a tap");
11860 let last = heard.iter().rev().find_map(|m| match m {
11862 Msg::Update(local, _) => Some(*local),
11863 _ => None,
11864 });
11865 let corner = origin(&d);
11866 assert_eq!(
11867 last,
11868 Some(Point::new(at.x + 120.0 - corner.x, at.y + 60.0 - corner.y))
11869 );
11870 }
11871
11872 #[test]
11874 fn a_press_that_does_not_move_is_a_tap() {
11875 let (mut d, heard) = driver(PanAxis::Free, false);
11876 let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 40.0);
11877 swipe(&mut d, at, 3.0, 2.0);
11878 assert_eq!(*heard.borrow(), vec![Msg::Tap]);
11879 }
11880
11881 #[test]
11884 fn a_hold_on_a_dragging_detector_is_a_long_press() {
11885 let (mut d, heard) = driver(PanAxis::Free, false);
11886 let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 40.0);
11887 d.press(at);
11888 d.run(0.1);
11889 assert!(d.hold_deadline(), "the hold fired");
11890 d.release(at);
11891 d.run(0.02);
11892 assert_eq!(*heard.borrow(), vec![Msg::Hold]);
11893 }
11894
11895 #[test]
11897 fn a_mouse_drags_it() {
11898 let (mut d, heard) = driver(PanAxis::Free, false);
11899 let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 40.0);
11900 d.press_mouse(at);
11901 d.run(0.02);
11902 d.move_mouse(Point::new(at.x + 30.0, at.y));
11903 d.run(0.02);
11904 d.release_mouse(Point::new(at.x + 30.0, at.y));
11905 d.run(0.02);
11906 let heard = heard.borrow();
11907 assert!(matches!(heard.first(), Some(Msg::Start(_))), "{heard:?}");
11908 assert!(matches!(heard.last(), Some(Msg::End(_))), "{heard:?}");
11909 }
11910
11911 #[test]
11914 fn a_horizontal_drag_shares_a_vertical_scroll() {
11915 let (mut d, heard) = driver(PanAxis::Horizontal, true);
11916 let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 40.0);
11917 swipe(&mut d, at, 120.0, 10.0);
11918 assert!(
11919 matches!(heard.borrow().first(), Some(Msg::Start(_))),
11920 "{:?}",
11921 heard.borrow()
11922 );
11923 assert_eq!(deltas(&heard.borrow()).y, 0.0, "only across");
11924 assert_eq!(scroll_y(&d), 0.0, "the page did not move");
11925
11926 heard.borrow_mut().clear();
11927 let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 120.0);
11928 swipe(&mut d, at, 5.0, -120.0);
11929 assert!(heard.borrow().is_empty(), "{:?}", heard.borrow());
11930 assert!(scroll_y(&d) > 30.0, "the page scrolled: {}", scroll_y(&d));
11931 }
11932
11933 #[test]
11936 fn a_free_drag_leaves_the_scroll_its_own_direction() {
11937 let (mut d, heard) = driver(PanAxis::Free, true);
11938 let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 40.0);
11939 swipe(&mut d, at, 150.0, 8.0);
11940 assert!(
11941 matches!(heard.borrow().first(), Some(Msg::Start(_))),
11942 "{:?}",
11943 heard.borrow()
11944 );
11945 assert_eq!(scroll_y(&d), 0.0);
11946
11947 heard.borrow_mut().clear();
11948 let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 120.0);
11949 swipe(&mut d, at, 4.0, -120.0);
11950 assert!(heard.borrow().is_empty(), "{:?}", heard.borrow());
11951 assert!(scroll_y(&d) > 30.0, "the page scrolled: {}", scroll_y(&d));
11952 }
11953
11954 #[test]
11960 fn a_stall_mid_drag_does_not_change_the_scroll() {
11961 let (mut d, heard) = driver(PanAxis::Free, true);
11962 let from = Point::new(origin(&d).x + 40.0, origin(&d).y + 120.0);
11963 d.press(from);
11964 d.run(0.02);
11965 for step in 1..=8 {
11966 let f = step as f32 / 8.0;
11967 d.move_to(Point::new(from.x + 4.0 * f, from.y - 120.0 * f));
11968 if step == 5 {
11969 std::thread::sleep(std::time::Duration::from_millis(20));
11970 }
11971 d.run(0.02);
11972 }
11973 d.release(Point::new(from.x + 4.0, from.y - 120.0));
11974 d.run(0.02);
11975 assert!(heard.borrow().is_empty(), "{:?}", heard.borrow());
11976 assert!(scroll_y(&d) > 30.0, "the page scrolled: {}", scroll_y(&d));
11977 }
11978
11979 #[test]
11982 fn a_vertical_drag_takes_it_from_the_scroll() {
11983 let (mut d, heard) = driver(PanAxis::Vertical, true);
11984 let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 120.0);
11985 swipe(&mut d, at, 6.0, -100.0);
11986 let heard = heard.borrow();
11987 assert!(matches!(heard.first(), Some(Msg::Start(_))), "{heard:?}");
11988 let moved = deltas(&heard);
11989 assert_eq!(moved.x, 0.0, "only down");
11990 assert!(moved.y < -40.0, "{moved:?}");
11991 assert_eq!(scroll_y(&d), 0.0, "the page did not move");
11992 }
11993}
11994
11995#[cfg(test)]
11998mod mouse_region_tests {
11999 use super::testing::Driver;
12000 use crate::{Application, Command};
12001 use frus_widgets::{
12002 Container, Cursor, Flex, MouseRegion, Point, Positioned, Stack, TextField, Theme, Widget,
12003 };
12004 use std::cell::RefCell;
12005 use std::rc::Rc;
12006
12007 const W: f32 = 400.0;
12008 const H: f32 = 600.0;
12009
12010 #[derive(Clone, Debug, PartialEq)]
12011 enum Msg {
12012 Enter(&'static str, Point),
12013 Move(&'static str),
12014 Exit(&'static str),
12015 Show,
12016 }
12017
12018 struct Page {
12022 front_opaque: bool,
12023 shown: bool,
12024 heard: Rc<RefCell<Vec<Msg>>>,
12025 }
12026
12027 fn region(name: &'static str, child: impl Widget<Msg> + 'static) -> MouseRegion<Msg> {
12028 MouseRegion::new(child)
12029 .on_enter(move |at| Msg::Enter(name, at))
12030 .on_hover(move |_| Msg::Move(name))
12031 .on_exit(move |_| Msg::Exit(name))
12032 }
12033
12034 impl Application for Page {
12035 type Message = Msg;
12036
12037 fn update(&mut self, message: Msg) -> Command<Msg> {
12038 if message == Msg::Show {
12039 self.shown = true;
12040 } else {
12041 self.heard.borrow_mut().push(message);
12042 }
12043 Command::none()
12044 }
12045
12046 fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
12047 let nested = region(
12049 "outer",
12050 Container::new().width(300.0).height(200.0).child(
12051 region("inner", Container::new().width(100.0).height(50.0))
12052 .cursor(Cursor::Pointer),
12053 ),
12054 )
12055 .cursor(Cursor::Grab);
12056 let stack = Stack::new()
12058 .width(300.0)
12059 .height(200.0)
12060 .layer(region("back", Container::new().width(300.0).height(200.0)))
12061 .layer(
12062 Positioned::new(
12063 region("front", Container::new().width(100.0).height(100.0))
12064 .opaque(self.front_opaque),
12065 )
12066 .left(50.0)
12067 .top(50.0),
12068 );
12069 let field = region("field", TextField::new("").width(300.0)).cursor(Cursor::Pointer);
12071 let appearing: Box<dyn Widget<Msg>> = if self.shown {
12073 Box::new(region("late", Container::new().width(300.0).height(100.0)))
12074 } else {
12075 Box::new(Container::new().width(300.0).height(100.0))
12076 };
12077 Box::new(
12078 Container::new().width(W).height(H).child(
12079 Flex::column()
12080 .child(nested)
12081 .child(stack)
12082 .child(Container::new().height(100.0).child(field))
12083 .child(appearing),
12084 ),
12085 )
12086 }
12087 }
12088
12089 fn driver(front_opaque: bool) -> (Driver<Page>, Rc<RefCell<Vec<Msg>>>) {
12090 let heard = Rc::new(RefCell::new(Vec::new()));
12091 let page = Page {
12092 front_opaque,
12093 shown: false,
12094 heard: heard.clone(),
12095 };
12096 let mut driver = Driver::new(page, W, H);
12097 driver.run(0.2);
12098 (driver, heard)
12099 }
12100
12101 fn take(heard: &Rc<RefCell<Vec<Msg>>>) -> Vec<Msg> {
12102 std::mem::take(&mut *heard.borrow_mut())
12103 }
12104
12105 fn mouse(d: &mut Driver<Page>, x: f32, y: f32) {
12106 d.move_mouse(Point::new(x, y));
12107 d.run(0.02);
12108 }
12109
12110 #[test]
12114 fn nested_regions_are_entered_and_left_together() {
12115 let (mut d, heard) = driver(true);
12116 mouse(&mut d, 20.0, 10.0);
12117 assert_eq!(
12118 take(&heard),
12119 vec![
12120 Msg::Enter("outer", Point::new(20.0, 10.0)),
12121 Msg::Enter("inner", Point::new(20.0, 10.0)),
12122 ]
12123 );
12124 mouse(&mut d, 30.0, 12.0);
12125 let moved = take(&heard);
12126 assert!(moved.contains(&Msg::Move("inner")) && moved.contains(&Msg::Move("outer")));
12127 mouse(&mut d, 200.0, 150.0);
12128 assert_eq!(take(&heard), vec![Msg::Exit("inner"), Msg::Move("outer")]);
12129 mouse(&mut d, 350.0, 150.0);
12130 assert_eq!(take(&heard), vec![Msg::Exit("outer")]);
12131 }
12132
12133 #[test]
12136 fn an_opaque_region_hides_the_one_behind() {
12137 let (mut d, heard) = driver(true);
12138 mouse(&mut d, 80.0, 280.0);
12139 let entered: Vec<_> = take(&heard);
12140 assert!(
12141 entered.iter().any(|m| matches!(m, Msg::Enter("front", _))),
12142 "{entered:?}"
12143 );
12144 assert!(
12145 !entered.iter().any(|m| matches!(m, Msg::Enter("back", _))),
12146 "{entered:?}"
12147 );
12148 mouse(&mut d, 250.0, 280.0);
12150 let after = take(&heard);
12151 assert!(after.contains(&Msg::Exit("front")), "{after:?}");
12152 assert!(
12153 after.iter().any(|m| matches!(m, Msg::Enter("back", _))),
12154 "{after:?}"
12155 );
12156
12157 let (mut d, heard) = driver(false);
12158 mouse(&mut d, 80.0, 280.0);
12159 let entered = take(&heard);
12160 assert!(
12161 entered.iter().any(|m| matches!(m, Msg::Enter("front", _))),
12162 "{entered:?}"
12163 );
12164 assert!(
12165 entered.iter().any(|m| matches!(m, Msg::Enter("back", _))),
12166 "{entered:?}"
12167 );
12168 }
12169
12170 #[test]
12173 fn no_moves_while_a_button_is_held() {
12174 let (mut d, heard) = driver(true);
12175 mouse(&mut d, 350.0, 150.0);
12176 take(&heard);
12177 d.press_mouse(Point::new(350.0, 150.0));
12178 d.run(0.02);
12179 d.move_mouse(Point::new(40.0, 20.0));
12180 d.run(0.02);
12181 d.move_mouse(Point::new(45.0, 22.0));
12182 d.run(0.02);
12183 let heard_now = take(&heard);
12184 assert!(
12185 heard_now
12186 .iter()
12187 .any(|m| matches!(m, Msg::Enter("inner", _))),
12188 "{heard_now:?}"
12189 );
12190 assert!(
12191 !heard_now.iter().any(|m| matches!(m, Msg::Move(_))),
12192 "{heard_now:?}"
12193 );
12194 d.release_mouse(Point::new(45.0, 22.0));
12195 d.run(0.02);
12196 }
12197
12198 #[test]
12200 fn a_finger_enters_while_it_touches() {
12201 let (mut d, heard) = driver(true);
12202 d.press(Point::new(200.0, 150.0));
12203 d.run(0.02);
12204 assert_eq!(
12205 take(&heard),
12206 vec![Msg::Enter("outer", Point::new(200.0, 150.0))]
12207 );
12208 d.release(Point::new(200.0, 150.0));
12209 d.run(0.02);
12210 assert_eq!(take(&heard), vec![Msg::Exit("outer")]);
12211 }
12212
12213 #[test]
12216 fn a_region_appearing_under_the_pointer_is_entered() {
12217 let (mut d, heard) = driver(true);
12218 mouse(&mut d, 150.0, 550.0);
12219 assert!(take(&heard).is_empty());
12220 d.update(Msg::Show);
12221 d.run(0.05);
12222 let heard_now = take(&heard);
12223 assert!(
12224 heard_now.iter().any(|m| matches!(m, Msg::Enter("late", _))),
12225 "{heard_now:?}"
12226 );
12227 }
12228
12229 #[test]
12232 fn the_innermost_region_sets_the_cursor() {
12233 let (mut d, _) = driver(true);
12234 mouse(&mut d, 20.0, 10.0);
12235 assert_eq!(d.cursor_shown(), Cursor::Pointer, "the inner region's");
12236 mouse(&mut d, 200.0, 150.0);
12237 assert_eq!(d.cursor_shown(), Cursor::Grab, "the outer region's");
12238 mouse(&mut d, 350.0, 150.0);
12239 assert_eq!(d.cursor_shown(), Cursor::Default, "no region");
12240 mouse(&mut d, 100.0, 420.0);
12241 assert_eq!(d.cursor_shown(), Cursor::Text, "the field's own");
12242 }
12243}
12244
12245#[cfg(test)]
12248mod listener_tests {
12249 use super::testing::Driver;
12250 use crate::{Application, Command};
12251 use frus_widgets::{
12252 Button, Container, Flex, Listener, ListenerEvent, ListenerEventKind as Raw, Point,
12253 SingleChildScrollView, Theme, Widget,
12254 };
12255 use std::cell::RefCell;
12256 use std::rc::Rc;
12257
12258 const W: f32 = 400.0;
12259 const H: f32 = 400.0;
12260
12261 #[derive(Clone, Debug, PartialEq)]
12262 enum Msg {
12263 Heard(&'static str, Raw, Point),
12264 Pressed,
12265 }
12266
12267 struct Page {
12270 heard: Rc<RefCell<Vec<Msg>>>,
12271 }
12272
12273 fn listen(name: &'static str) -> impl Fn(ListenerEvent) -> Option<Msg> {
12274 move |event| Some(Msg::Heard(name, event.kind, event.local))
12275 }
12276
12277 impl Application for Page {
12278 type Message = Msg;
12279
12280 fn update(&mut self, message: Msg) -> Command<Msg> {
12281 self.heard.borrow_mut().push(message);
12282 Command::none()
12283 }
12284
12285 fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
12286 let inner = Listener::new(
12287 Container::new()
12288 .width(100.0)
12289 .height(100.0)
12290 .child(Button::new("Press").on_press(Msg::Pressed)),
12291 listen("inner"),
12292 );
12293 let outer = Listener::new(
12294 Container::new().width(300.0).height(300.0).child(inner),
12295 listen("outer"),
12296 );
12297 Box::new(
12298 SingleChildScrollView::new().width(W).height(H).child(
12299 Flex::column()
12300 .child(Container::new().width(300.0).height(40.0))
12301 .child(outer)
12302 .child(Container::new().width(300.0).height(1000.0)),
12303 ),
12304 )
12305 }
12306 }
12307
12308 fn driver() -> (Driver<Page>, Rc<RefCell<Vec<Msg>>>) {
12309 let heard = Rc::new(RefCell::new(Vec::new()));
12310 let mut d = Driver::new(
12311 Page {
12312 heard: heard.clone(),
12313 },
12314 W,
12315 H,
12316 );
12317 d.run(0.2);
12318 (d, heard)
12319 }
12320
12321 fn take(heard: &Rc<RefCell<Vec<Msg>>>) -> Vec<Msg> {
12322 std::mem::take(&mut *heard.borrow_mut())
12323 }
12324
12325 fn of(heard: &[Msg], name: &str) -> Vec<(Raw, Point)> {
12327 heard
12328 .iter()
12329 .filter_map(|m| match m {
12330 Msg::Heard(n, kind, at) if *n == name => Some((*kind, *at)),
12331 _ => None,
12332 })
12333 .collect()
12334 }
12335
12336 fn scroll_y(d: &Driver<Page>) -> f32 {
12337 d.offsets().first().map_or(0.0, |(_, offset)| offset.1)
12338 }
12339
12340 #[test]
12343 fn a_press_is_heard_innermost_first() {
12344 let (mut d, heard) = driver();
12345 d.press(Point::new(250.0, 90.0));
12346 d.run(0.02);
12347 let h = take(&heard);
12348 assert_eq!(of(&h, "outer"), vec![(Raw::Down, Point::new(250.0, 50.0))]);
12349 assert!(of(&h, "inner").is_empty(), "outside the inner one: {h:?}");
12350 d.release(Point::new(250.0, 90.0));
12351 d.run(0.02);
12352 take(&heard);
12353
12354 d.press(Point::new(60.0, 80.0));
12355 d.run(0.02);
12356 let h = take(&heard);
12357 let order: Vec<_> = h
12358 .iter()
12359 .filter_map(|m| match m {
12360 Msg::Heard(n, Raw::Down, _) => Some(*n),
12361 _ => None,
12362 })
12363 .collect();
12364 assert_eq!(order, vec!["inner", "outer"], "{h:?}");
12365 assert_eq!(of(&h, "inner"), vec![(Raw::Down, Point::new(60.0, 40.0))]);
12366 d.release(Point::new(60.0, 80.0));
12367 d.run(0.02);
12368 }
12369
12370 #[test]
12373 fn the_pointer_is_followed_out_and_the_page_still_scrolls() {
12374 let (mut d, heard) = driver();
12375 let from = Point::new(250.0, 300.0);
12376 d.press(from);
12377 d.run(0.02);
12378 for step in 1..=8 {
12379 d.move_to(Point::new(250.0, 300.0 - 35.0 * step as f32));
12380 d.run(0.02);
12381 }
12382 d.release(Point::new(250.0, 20.0));
12383 d.run(0.02);
12384 let h = take(&heard);
12385 let outer = of(&h, "outer");
12386 assert_eq!(outer.first().map(|e| e.0), Some(Raw::Down), "{outer:?}");
12387 assert_eq!(outer.last().map(|e| e.0), Some(Raw::Up), "{outer:?}");
12388 assert!(
12389 outer.iter().filter(|e| e.0 == Raw::Move).count() >= 7,
12390 "{outer:?}"
12391 );
12392 assert!(scroll_y(&d) > 50.0, "the page scrolled: {}", scroll_y(&d));
12393 }
12394
12395 #[test]
12398 fn a_pointer_that_leaves_is_still_heard() {
12399 let (mut d, heard) = driver();
12400 d.press_mouse(Point::new(60.0, 80.0));
12401 d.run(0.02);
12402 d.move_mouse(Point::new(390.0, 390.0));
12403 d.run(0.02);
12404 d.release_mouse(Point::new(390.0, 390.0));
12405 d.run(0.02);
12406 let h = take(&heard);
12407 let inner = of(&h, "inner");
12408 assert_eq!(
12409 inner,
12410 vec![
12411 (Raw::Down, Point::new(60.0, 40.0)),
12412 (Raw::Move, Point::new(390.0, 350.0)),
12413 (Raw::Up, Point::new(390.0, 350.0)),
12414 ],
12415 "{h:?}"
12416 );
12417 assert_eq!(of(&h, "outer").len(), 3, "{h:?}");
12418 }
12419
12420 #[test]
12422 fn a_button_inside_keeps_its_tap() {
12423 let (mut d, heard) = driver();
12424 let at = Point::new(30.0, 55.0);
12425 d.press(at);
12426 d.release(at);
12427 d.run(0.05);
12428 let h = take(&heard);
12429 assert!(h.contains(&Msg::Pressed), "{h:?}");
12430 let inner = of(&h, "inner");
12431 assert_eq!(
12432 inner.iter().map(|e| e.0).collect::<Vec<_>>(),
12433 vec![Raw::Down, Raw::Up]
12434 );
12435 }
12436
12437 #[test]
12440 fn a_mouse_hovering_is_heard() {
12441 let (mut d, heard) = driver();
12442 d.move_mouse(Point::new(250.0, 90.0));
12443 d.run(0.02);
12444 let h = take(&heard);
12445 assert_eq!(of(&h, "outer"), vec![(Raw::Hover, Point::new(250.0, 50.0))]);
12446 assert!(of(&h, "inner").is_empty());
12447 d.move_mouse(Point::new(390.0, 390.0));
12448 d.run(0.02);
12449 assert!(take(&heard).is_empty(), "outside both");
12450 }
12451}
12452
12453#[cfg(test)]
12454mod menu_keyboard_tests {
12455 use super::testing::Driver;
12456 use crate::{Application, Command};
12457 use frus_widgets::{
12458 KeyStroke, MenuBar, MenuItem, MenuPath, Point, ShortcutKey, SubmenuButton, Theme, Widget,
12459 };
12460
12461 const W: f32 = 600.0;
12462 const H: f32 = 400.0;
12463
12464 #[derive(Clone, Debug, PartialEq)]
12465 enum Msg {
12466 Menu(MenuPath),
12467 Picked(&'static str),
12468 }
12469
12470 #[derive(Default)]
12472 struct Editor {
12473 menu: MenuPath,
12474 picked: Option<&'static str>,
12476 }
12477
12478 impl Application for Editor {
12479 type Message = Msg;
12480
12481 fn update(&mut self, message: Msg) -> Command<Msg> {
12482 match message {
12483 Msg::Menu(path) => self.menu = path,
12484 Msg::Picked(what) => {
12485 self.picked = Some(what);
12486 self.menu = MenuPath::closed();
12487 }
12488 }
12489 Command::none()
12490 }
12491
12492 fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
12493 Box::new(
12494 MenuBar::new(&self.menu, Msg::Menu).menu(
12495 SubmenuButton::new("File")
12496 .item(MenuItem::new("New", Msg::Picked("new")))
12497 .submenu(
12498 SubmenuButton::new("Open recent")
12499 .item(MenuItem::new("notes.txt", Msg::Picked("notes")))
12500 .item(MenuItem::new("todo.md", Msg::Picked("todo"))),
12501 )
12502 .item(MenuItem::new("Quit", Msg::Picked("quit"))),
12503 ),
12504 )
12505 }
12506 }
12507
12508 fn row(d: &Driver<Editor>, label: &str) -> Option<frus_widgets::WidgetId> {
12510 let (_, rect) = d.texts().into_iter().find(|(text, _)| text == label)?;
12511 d.hit(Point::new(rect.x + 4.0, rect.y + 4.0))
12512 }
12513
12514 #[test]
12517 fn opening_a_menu_focuses_its_first_row_and_escape_goes_back_one_level() {
12518 let mut d = Driver::new(Editor::default(), W, H);
12519 d.run(0.1);
12520 assert_eq!(d.focused(), None);
12521
12522 d.update(Msg::Menu(MenuPath::from_indices(vec![0])));
12523 d.run(0.1);
12524 let new = row(&d, "New").expect("the menu is open");
12525 assert_eq!(d.focused(), Some(new), "the first row has the focus");
12526
12527 d.update(Msg::Menu(MenuPath::from_indices(vec![0, 1])));
12528 d.run(0.1);
12529 let notes = row(&d, "notes.txt").expect("the submenu is open");
12530 assert_eq!(d.focused(), Some(notes), "the submenu's first row has it");
12531
12532 d.escape();
12533 d.run(0.1);
12534 assert_eq!(
12535 d.app().menu,
12536 MenuPath::from_indices(vec![0]),
12537 "one level closed"
12538 );
12539 assert!(row(&d, "notes.txt").is_none());
12540 assert_eq!(d.focused(), Some(new), "and the focus is back in the menu");
12541
12542 d.escape();
12543 d.run(0.1);
12544 assert!(!d.app().menu.is_open(), "then the menu");
12545 }
12546
12547 #[test]
12551 fn the_menu_is_worked_from_the_keyboard() {
12552 let key = |d: &mut Driver<Editor>, k: ShortcutKey| {
12553 d.key(KeyStroke::new(k));
12554 d.run(0.1);
12555 };
12556 let mut d = Driver::new(Editor::default(), W, H);
12557 d.run(0.1);
12558 d.update(Msg::Menu(MenuPath::from_indices(vec![0])));
12559 d.run(0.1);
12560 let (new, recent) = (row(&d, "New").unwrap(), row(&d, "Open recent").unwrap());
12561 assert_eq!(d.focused(), Some(new));
12562
12563 key(&mut d, ShortcutKey::Down);
12564 assert_eq!(d.focused(), Some(recent), "down to the next row");
12565 key(&mut d, ShortcutKey::Right);
12566 assert_eq!(
12567 d.app().menu,
12568 MenuPath::from_indices(vec![0, 1]),
12569 "right opens it"
12570 );
12571 assert_eq!(
12572 d.focused(),
12573 row(&d, "notes.txt"),
12574 "and its first row has the focus"
12575 );
12576 key(&mut d, ShortcutKey::Down);
12577 assert_eq!(d.focused(), row(&d, "todo.md"));
12578 key(&mut d, ShortcutKey::Left);
12579 assert_eq!(
12580 d.app().menu,
12581 MenuPath::from_indices(vec![0]),
12582 "left closes it"
12583 );
12584 assert_eq!(
12585 d.focused(),
12586 Some(recent),
12587 "and the focus is back on its row"
12588 );
12589 key(&mut d, ShortcutKey::Up);
12590 assert_eq!(d.focused(), Some(new));
12591 key(&mut d, ShortcutKey::Enter);
12592 assert_eq!(d.app().picked, Some("new"), "Enter picks the row");
12593 assert!(!d.app().menu.is_open(), "and the menus close");
12594 }
12595
12596 #[test]
12599 fn a_stroke_holds_its_modifiers_for_the_key_alone() {
12600 let mut d = Driver::new(Editor::default(), W, H);
12601 d.run(0.1);
12602 d.key(KeyStroke::new(ShortcutKey::Char('a')).ctrl());
12603 assert!(!d.shell_modifiers().0, "ctrl let go after the stroke");
12604 }
12605
12606 #[test]
12611 fn closing_a_menu_hands_the_focus_back_to_its_word() {
12612 let key = |d: &mut Driver<Editor>, k: ShortcutKey| {
12613 d.key(KeyStroke::new(k));
12614 d.run(0.1);
12615 };
12616 let mut d = Driver::new(Editor::default(), W, H);
12617 d.run(0.1);
12618 key(&mut d, ShortcutKey::Tab);
12619 let file = d.focused().expect("Tab reached the bar");
12620 key(&mut d, ShortcutKey::Enter);
12621 assert!(d.app().menu.is_open(), "Enter opened the menu");
12622 assert_eq!(d.focused(), row(&d, "New"), "and the focus went into it");
12623 key(&mut d, ShortcutKey::Escape);
12624 assert!(!d.app().menu.is_open(), "Escape closed it");
12625 assert_eq!(d.focused(), Some(file), "and the focus is back on its word");
12626 }
12627
12628 #[test]
12634 fn a_submenu_closing_hands_the_focus_back_to_any_row() {
12635 let key = |d: &mut Driver<Editor>, k: ShortcutKey| {
12636 d.key(KeyStroke::new(k));
12637 d.run(0.1);
12638 };
12639 let mut d = Driver::new(Editor::default(), W, H);
12640 d.run(0.1);
12641 d.update(Msg::Menu(MenuPath::from_indices(vec![0])));
12642 d.run(0.1);
12643 key(&mut d, ShortcutKey::Down);
12644 key(&mut d, ShortcutKey::Down);
12645 let quit = row(&d, "Quit");
12646 assert_eq!(d.focused(), quit, "down twice reaches Quit");
12647 d.update(Msg::Menu(MenuPath::from_indices(vec![0, 1])));
12649 d.run(0.1);
12650 assert_eq!(d.focused(), row(&d, "notes.txt"));
12651 key(&mut d, ShortcutKey::Escape);
12652 assert_eq!(d.app().menu, MenuPath::from_indices(vec![0]));
12653 assert_eq!(d.focused(), quit, "the focus is back on Quit");
12654 }
12655}
12656
12657#[cfg(test)]
12661mod right_click_bar_tests {
12662 use super::testing::Driver;
12663 use crate::{Application, Command};
12664 use frus_widgets::{Point, TextField, Theme, Widget};
12665
12666 struct Form;
12667
12668 impl Application for Form {
12669 type Message = ();
12670
12671 fn update(&mut self, _message: ()) -> Command<()> {
12672 Command::none()
12673 }
12674
12675 fn view(&self, _theme: &Theme) -> Box<dyn Widget<()>> {
12676 Box::new(TextField::new("some words in a field"))
12677 }
12678 }
12679
12680 #[test]
12681 fn a_right_click_on_a_field_opens_its_bar() {
12682 let mut d = Driver::new(Form, 500.0, 300.0);
12683 d.run(0.1);
12684 let shown = |d: &Driver<Form>, label: &str| d.texts().iter().any(|(t, _)| t == label);
12685 assert!(!shown(&d, "Select all"), "no bar before the click");
12686 let (_, words) = d
12687 .texts()
12688 .into_iter()
12689 .find(|(t, _)| t.contains("some words"))
12690 .expect("the field's words");
12691 d.secondary_click(Point::new(words.x + 20.0, words.y + words.height * 0.5));
12692 d.run(0.1);
12693 assert!(shown(&d, "Select all"), "the bar is open: {:?}", d.texts());
12697 assert!(!shown(&d, "Copy") && !shown(&d, "Cut"));
12698 assert!(
12699 !shown(&d, "Paste"),
12700 "an empty clipboard offers nothing to paste"
12701 );
12702 }
12703
12704 struct DesktopForm;
12706
12707 impl Application for DesktopForm {
12708 type Message = ();
12709
12710 fn update(&mut self, _message: ()) -> Command<()> {
12711 Command::none()
12712 }
12713
12714 fn view(&self, _theme: &Theme) -> Box<dyn Widget<()>> {
12715 Box::new(TextField::new("some words in a field"))
12716 }
12717
12718 fn theme(&self) -> Theme {
12719 Theme::default().with_platform(frus_widgets::TargetPlatform::Linux)
12720 }
12721 }
12722
12723 #[test]
12727 fn a_right_click_opens_the_menu_at_the_pointer() {
12728 let mut d = Driver::new(DesktopForm, 500.0, 300.0);
12729 d.run(0.1);
12730 let (_, words) = d
12731 .texts()
12732 .into_iter()
12733 .find(|(t, _)| t.contains("some words"))
12734 .expect("the field's words");
12735 let click = Point::new(words.x + 20.0, words.y + words.height * 0.5);
12736 d.secondary_click(click);
12737 d.run(0.1);
12738 let (_, row) = d
12739 .texts()
12740 .into_iter()
12741 .find(|(t, _)| t == "Select all")
12742 .expect("the menu is open");
12743 assert_eq!(row.x, click.x + 20.0, "the menu starts at the pointer");
12744 assert!(
12745 row.y >= click.y && row.y < click.y + 36.0,
12746 "its first row is on the pointer's line: {row:?} {click:?}"
12747 );
12748 }
12749
12750 #[test]
12755 fn a_desktop_s_select_all_puts_the_menu_away() {
12756 assert!(frus_widgets::default_target_platform().is_desktop());
12757 let mut d = Driver::new(DesktopForm, 500.0, 300.0);
12758 d.run(0.1);
12759 let shown = |d: &Driver<DesktopForm>, label: &str| {
12760 d.texts()
12761 .into_iter()
12762 .find(|(t, _)| t == label)
12763 .map(|(_, r)| r)
12764 };
12765 let words = shown(&d, "some words in a field").expect("the field's words");
12766 let click = Point::new(words.x + 20.0, words.y + words.height * 0.5);
12767 d.secondary_click(click);
12768 d.run(0.1);
12769 let row = shown(&d, "Select all").expect("the menu is open");
12770 let row = Point::new(row.x + 10.0, row.y + row.height * 0.5);
12771 d.press(row);
12772 d.release(row);
12773 d.run(0.1);
12774 assert!(shown(&d, "Select all").is_none(), "the menu is gone");
12775 assert!(shown(&d, "Copy").is_none(), "all of it: {:?}", d.texts());
12776 d.secondary_click(click);
12777 d.run(0.1);
12778 assert!(shown(&d, "Copy").is_some(), "the text is still selected");
12779 }
12780}
12781
12782#[cfg(test)]
12783mod scroll_behavior_tests {
12784 use super::testing::Driver;
12785 use crate::{Application, Command};
12786 use frus_widgets::{
12787 Container, Point, ScrollBehavior, ScrollConfiguration, SingleChildScrollView,
12788 TargetPlatform, Theme, VelocityStrategy, Widget,
12789 };
12790
12791 struct Page;
12794
12795 impl Application for Page {
12796 type Message = ();
12797
12798 fn update(&mut self, _message: ()) -> Command<()> {
12799 Command::none()
12800 }
12801
12802 fn view(&self, _theme: &Theme) -> Box<dyn Widget<()>> {
12803 Box::new(ScrollConfiguration::new(
12804 ScrollBehavior::material().with_platform(TargetPlatform::Ios),
12805 SingleChildScrollView::new()
12806 .width(300.0)
12807 .height(400.0)
12808 .child(Container::new().width(300.0).height(2000.0)),
12809 ))
12810 }
12811
12812 fn theme(&self) -> Theme {
12813 Theme::default().with_platform(TargetPlatform::Android)
12814 }
12815 }
12816
12817 #[test]
12821 fn a_drag_on_an_area_reads_its_fling_the_area_s_way() {
12822 let mut d = Driver::new(Page, 300.0, 400.0);
12823 d.run(0.2);
12824 d.press(Point::new(150.0, 200.0));
12825 assert_eq!(
12826 d.fling_strategy(),
12827 VelocityStrategy::RecentAverage(frus_widgets::BOUNCING_FLING_WEIGHTS)
12828 );
12829 d.release(Point::new(150.0, 200.0));
12830 }
12831}
12832
12833#[cfg(test)]
12836mod switch_drag_tests {
12837 use super::testing::Driver;
12838 use crate::{Application, Command};
12839 use frus_widgets::{Container, Point, Rect, Switch, Theme, Widget};
12840
12841 const W: f32 = 300.0;
12842 const H: f32 = 200.0;
12843
12844 #[derive(Clone, Debug, PartialEq)]
12845 enum Msg {
12846 Set(bool),
12847 }
12848
12849 #[derive(Default)]
12850 struct Page {
12851 on: bool,
12852 heard: Vec<Msg>,
12853 }
12854
12855 impl Application for Page {
12856 type Message = Msg;
12857
12858 fn update(&mut self, message: Msg) -> Command<Msg> {
12859 let Msg::Set(on) = message;
12860 self.on = on;
12861 self.heard.push(message);
12862 Command::none()
12863 }
12864
12865 fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
12866 Box::new(
12867 Container::new()
12868 .width(W)
12869 .height(H)
12870 .child(Switch::new(self.on).on_toggle(Msg::Set)),
12871 )
12872 }
12873 }
12874
12875 fn driver() -> Driver<Page> {
12876 let mut d = Driver::new(Page::default(), W, H);
12877 d.run(0.2);
12878 d
12879 }
12880
12881 fn track_and_thumb(d: &Driver<Page>) -> (Rect, Rect) {
12883 let (ui, _) = d.frame_parts().expect("a frame");
12884 let rects: Vec<Rect> = ui
12885 .scene()
12886 .primitives()
12887 .iter()
12888 .filter_map(|p| match p {
12889 frus_widgets::Primitive::Rect { rect, .. } => Some(*rect),
12890 _ => None,
12891 })
12892 .collect();
12893 let track = *rects
12894 .iter()
12895 .find(|r| r.width == 52.0 && r.height == 32.0)
12896 .expect("the track");
12897 (track, *rects.last().expect("the thumb"))
12898 }
12899
12900 fn along(d: &Driver<Page>) -> f32 {
12902 let (track, thumb) = track_and_thumb(d);
12903 (thumb.x + thumb.width * 0.5 - track.x - 16.0) / 20.0
12904 }
12905
12906 fn drag(d: &mut Driver<Page>, dx: f32, lift: bool) {
12908 let (track, _) = track_and_thumb(d);
12909 let from = Point::new(track.x + 16.0, track.y + 16.0);
12910 d.press(from);
12911 d.run(0.02);
12912 for step in 1..=8 {
12913 d.move_to(Point::new(from.x + dx * step as f32 / 8.0, from.y));
12914 d.run(0.02);
12915 }
12916 if lift {
12917 d.release(Point::new(from.x + dx, from.y));
12918 d.run(0.02);
12919 }
12920 }
12921
12922 #[test]
12925 fn the_thumb_follows_the_finger() {
12926 let mut d = driver();
12927 assert!(along(&d).abs() < 1e-3, "off at rest");
12928 drag(&mut d, 40.0, false);
12929 let now = along(&d);
12930 assert!(now > 0.3 && now <= 1.0, "moved with the finger: {now}");
12931 assert!(d.app().heard.is_empty(), "nothing said while held");
12932 d.run(0.5);
12934 assert!((along(&d) - now).abs() < 1e-3, "held where it was put");
12935 }
12936
12937 #[test]
12940 fn let_go_past_half_way_it_flips() {
12941 let mut d = driver();
12942 drag(&mut d, 60.0, true);
12943 assert_eq!(d.app().heard, vec![Msg::Set(true)]);
12944 d.run(0.5);
12945 assert!((along(&d) - 1.0).abs() < 1e-3, "settled on");
12946 }
12947
12948 #[test]
12950 fn let_go_short_of_half_way_it_goes_back() {
12951 let mut d = driver();
12952 drag(&mut d, 30.0, false);
12954 let (track, _) = track_and_thumb(&d);
12955 let back = Point::new(track.x + 16.0 + 15.0, track.y + 16.0);
12956 d.move_to(back);
12957 d.run(0.02);
12958 assert!(along(&d) > 0.0, "it had moved");
12959 assert!(along(&d) < 0.5, "but is short of half way: {}", along(&d));
12960 d.release(back);
12961 d.run(0.5);
12962 assert!(d.app().heard.is_empty(), "nothing said");
12963 assert!(along(&d).abs() < 1e-3, "back off: {}", along(&d));
12964 }
12965
12966 #[test]
12968 fn a_tap_still_flips_it() {
12969 let mut d = driver();
12970 let (track, _) = track_and_thumb(&d);
12971 let at = Point::new(track.x + 16.0, track.y + 16.0);
12972 d.press(at);
12973 d.release(at);
12974 d.run(0.5);
12975 assert_eq!(d.app().heard, vec![Msg::Set(true)]);
12976 assert!((along(&d) - 1.0).abs() < 1e-3);
12977 }
12978}