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, focus_changes,
17 nearest_reorder_slot, reflow_reorder_cards, reflow_reorder_columns, reorder_drop_after,
18 reorder_siblings, reorderable_owners, subtree_ids, Accessibility, Brightness, Color,
19 Cursor as UiCursor, Edit, EditKind, EditSnapshot, FocusDirection, Insets, Key, KeyResponse,
20 KeyStroke, MediaQuery, Point, Primitive, Rect, RegionPoint, RegionSelection, ReorderAxis,
21 Runtime, Scene, ScrollTo, Scrollable, SheetTo, ShortcutKey, Size, TextStop, Theme, ToolbarMark,
22 Ui, VelocityEstimate, VelocityTracker, Widget, WidgetId, WindowInsets,
23};
24use winit::application::ApplicationHandler;
25use winit::event::{
26 ElementState, MouseButton, MouseScrollDelta, StartCause, TouchPhase, WindowEvent,
27};
28use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoopProxy};
29use winit::keyboard::{Key as WinitKey, KeyCode, NamedKey, PhysicalKey};
30use winit::window::{CursorIcon, Window, WindowId};
31
32use crate::application::{Application, Lifecycle};
33
34#[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
118fn wants_keyboard<M>(widget: &dyn Widget<M>) -> bool {
131 is_text_field(widget) && widget.focusable() && widget.takes_typing()
132}
133
134fn is_text_field<M>(widget: &dyn Widget<M>) -> bool {
144 widget.text_value().is_some()
145}
146
147const REGION_DOUBLE_CLICK_SLOP: f32 = 8.0;
150
151const DOUBLE_TAP_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(300);
154
155const FILE_DRAG_POLL: std::time::Duration = std::time::Duration::from_millis(33);
159
160const DOUBLE_TAP_SLOP: f32 = 100.0;
163
164const MAX_PAN_VELOCITY: f32 = 8000.0;
167
168struct PendingTap<M> {
171 id: WidgetId,
173 at: Instant,
175 position: Point,
177 single: Option<M>,
179}
180
181fn select_all_hides_bar(platform: frus_widgets::TargetPlatform) -> bool {
188 platform.is_desktop()
189}
190
191mod clip {
207 use frus_widgets::WidgetId;
208
209 #[derive(Clone, Debug, PartialEq, Eq)]
211 pub struct Pasted {
212 pub into: WidgetId,
213 pub text: String,
214 }
215
216 impl Pasted {
217 pub fn lands(&self, focused: Option<WidgetId>) -> bool {
223 focused == Some(self.into) && !self.text.is_empty()
224 }
225 }
226
227 #[cfg(not(web))]
229 impl Clipboard {
230 pub fn paste(
231 &mut self,
232 into: WidgetId,
233 _wake: Option<&std::sync::Arc<winit::window::Window>>,
234 ) -> Option<Pasted> {
235 self.get_text().map(|text| Pasted { into, text })
236 }
237 pub fn take_answered(&mut self) -> Option<Pasted> {
238 None
239 }
240 }
241
242 #[cfg(desktop)]
243 pub enum Clipboard {
244 System(Option<arboard::Clipboard>),
246 #[cfg(any(test, feature = "testing"))]
251 Memory(Option<String>),
252 }
253
254 #[cfg(desktop)]
255 impl Clipboard {
256 pub fn new() -> Self {
257 Self::System(arboard::Clipboard::new().ok())
258 }
259 #[cfg(any(test, feature = "testing"))]
261 pub fn in_memory() -> Self {
262 Self::Memory(None)
263 }
264 pub fn get_text(&mut self) -> Option<String> {
265 match self {
266 Self::System(system) => system.as_mut().and_then(|c| c.get_text().ok()),
267 #[cfg(any(test, feature = "testing"))]
268 Self::Memory(text) => text.clone(),
269 }
270 }
271 pub fn has_text(&mut self) -> bool {
272 self.get_text().is_some_and(|text| !text.is_empty())
273 }
274 pub fn set_text(&mut self, text: String) {
275 match self {
276 Self::System(system) => {
277 if let Some(c) = system.as_mut() {
278 let _ = c.set_text(text);
279 }
280 }
281 #[cfg(any(test, feature = "testing"))]
282 Self::Memory(held) => *held = Some(text),
283 }
284 }
285 }
286
287 #[cfg(all(not(desktop), any(test, feature = "testing")))]
289 impl Clipboard {
290 pub fn in_memory() -> Self {
291 Self::new()
292 }
293 }
294
295 #[cfg(android)]
296 pub struct Clipboard;
297
298 #[cfg(android)]
299 impl Clipboard {
300 pub fn new() -> Self {
301 Self
302 }
303 pub fn get_text(&mut self) -> Option<String> {
304 crate::android_clipboard::get_text()
305 }
306 pub fn has_text(&mut self) -> bool {
307 self.get_text().is_some_and(|text| !text.is_empty())
308 }
309 pub fn set_text(&mut self, text: String) {
310 crate::android_clipboard::set_text(&text);
311 }
312 }
313
314 #[cfg(any(web, test))]
319 #[derive(Default)]
320 pub struct Answers(std::rc::Rc<std::cell::RefCell<std::collections::VecDeque<Pasted>>>);
321
322 #[cfg(any(web, test))]
323 impl Answers {
324 pub fn answer_for(
327 &self,
328 into: WidgetId,
329 wake: impl FnOnce() + 'static,
330 ) -> impl FnOnce(String) + 'static {
331 let queue = std::rc::Rc::clone(&self.0);
332 move |text| {
333 queue.borrow_mut().push_back(Pasted { into, text });
334 wake();
335 }
336 }
337
338 pub fn take(&self) -> Option<Pasted> {
340 self.0.borrow_mut().pop_front()
341 }
342 }
343
344 #[cfg(web)]
347 pub struct Clipboard {
348 answered: Answers,
349 }
350
351 #[cfg(web)]
352 impl Clipboard {
353 pub fn new() -> Self {
354 Self {
355 answered: Answers::default(),
356 }
357 }
358 pub fn set_text(&mut self, text: String) {
359 crate::web_clipboard::set_text(&text);
360 }
361 pub fn has_text(&mut self) -> bool {
364 true
365 }
366 pub fn paste(
367 &mut self,
368 into: WidgetId,
369 wake: Option<&std::sync::Arc<winit::window::Window>>,
370 ) -> Option<Pasted> {
371 let wake = wake.cloned();
372 crate::web_clipboard::get_text(self.answered.answer_for(into, move || {
373 if let Some(window) = wake {
374 window.request_redraw();
375 }
376 }));
377 None
378 }
379 pub fn take_answered(&mut self) -> Option<Pasted> {
380 self.answered.take()
381 }
382 }
383
384 #[cfg(not(any(desktop, android, web)))]
385 pub struct Clipboard;
386
387 #[cfg(not(any(desktop, android, web)))]
388 impl Clipboard {
389 pub fn new() -> Self {
390 Self
391 }
392 pub fn get_text(&mut self) -> Option<String> {
393 None
394 }
395 pub fn has_text(&mut self) -> bool {
396 false
397 }
398 pub fn set_text(&mut self, _text: String) {}
399 }
400}
401
402#[cfg(web)]
407mod web_timer {
408 use wasm_bindgen::prelude::Closure;
409 use wasm_bindgen::JsCast;
410
411 pub(crate) struct Interval {
414 id: i32,
415 _closure: Closure<dyn FnMut()>,
417 }
418
419 impl Interval {
420 pub(crate) fn new(ms: i32, f: impl FnMut() + 'static) -> Option<Self> {
423 let window = web_sys::window()?;
424 let closure = Closure::wrap(Box::new(f) as Box<dyn FnMut()>);
425 let id = window
426 .set_interval_with_callback_and_timeout_and_arguments_0(
427 closure.as_ref().unchecked_ref(),
428 ms.max(1),
429 )
430 .ok()?;
431 Some(Self {
432 id,
433 _closure: closure,
434 })
435 }
436 }
437
438 impl Drop for Interval {
439 fn drop(&mut self) {
440 if let Some(window) = web_sys::window() {
441 window.clear_interval_with_handle(self.id);
442 }
443 }
444 }
445
446 pub(crate) fn after(ms: i32, f: impl FnOnce() + 'static) {
455 let Some(window) = web_sys::window() else {
456 return;
457 };
458 let callback = wasm_bindgen::closure::Closure::once_into_js(f);
459 let _ = window.set_timeout_with_callback_and_timeout_and_arguments_0(
460 callback.unchecked_ref(),
461 ms.max(0),
462 );
463 }
464}
465
466#[cfg(not(web))]
471type SubHandle = async_executor::Task<()>;
472#[cfg(web)]
473type SubHandle = Option<web_timer::Interval>;
474
475const SCROLL_SPEED: f32 = 40.0;
477
478const TOUCH_SLOP: f32 = 18.0;
487
488const PRECISE_SLOP: f32 = 1.0;
491
492fn fling_velocity(estimate: VelocityEstimate, slop: f32) -> (f32, f32) {
504 let gate = |velocity: f32, travelled: f32| {
505 if travelled.abs() > slop {
506 velocity
507 } else {
508 0.0
509 }
510 };
511 (
512 gate(estimate.velocity.x, estimate.offset.0),
513 gate(estimate.velocity.y, estimate.offset.1),
514 )
515}
516
517const SCROLL_OVER: f32 = 48.0;
519
520const PAN_FLING_MIN: f32 = 80.0;
522
523const BACK_EDGE: f32 = 24.0;
525
526mod drag_preview {
530 pub const SHADOW_OFFSET_Y: f32 = 4.0;
532 pub const SHADOW_BLUR: f32 = 12.0;
534 pub const SHADOW_ALPHA: f32 = 0.28;
536 pub const BORDER_ALPHA: f32 = 0.9;
538 pub const BORDER_WIDTH: f32 = 1.5;
540 pub const LIFT_Y: f32 = -2.0;
542 pub const INSERT_THICKNESS: f32 = 3.0;
544}
545
546fn frame_needs_build(dirty: bool, animating: bool, was_animating: bool, otherwise: bool) -> bool {
556 dirty || animating || was_animating || otherwise
557}
558
559fn hold_candidates<Msg: Clone>(
567 drag: Option<&Drag>,
568 ui: Option<&frus_widgets::Ui<Msg>>,
569 tree: Option<&dyn frus_widgets::Widget<Msg>>,
570 at: Point,
571) -> (Option<Msg>, Option<frus_widgets::DragSource>) {
572 if matches!(drag, Some(Drag::Back { .. })) {
577 return (None, None);
578 }
579 let Some(ui) = ui else {
580 return (None, None);
581 };
582 let free = matches!(
585 drag,
586 None | Some(Drag::Scroll { moved: false, .. }) | Some(Drag::Gesture { moved: false, .. })
587 );
588 let long_press = if free { ui.long_press_at(at) } else { None };
589 let lift = ui.drag_source_at(at).filter(|source| {
590 tree.and_then(|tree| find_widget(tree, source.id))
591 .is_some_and(|widget| widget.drag_needs_long_press())
592 });
593 (long_press, lift)
594}
595
596fn drag_after_hold(
605 drag: Option<Drag>,
606 lift: Option<frus_widgets::DragSource>,
607 reorder: Option<(WidgetId, usize, Point)>,
608 cursor: Point,
609) -> Option<Drag> {
610 if let Some(Drag::Back { start_x }) = drag {
613 return Some(Drag::Back { start_x });
614 }
615 if let Some(source) = lift {
616 Some(Drag::Item {
617 source,
618 start: cursor,
619 moved: true,
620 over: None,
621 })
622 } else if let Some((id, from, start)) = reorder {
623 Some(Drag::Reorder {
624 id,
625 from,
626 start,
627 moved: true,
628 carried: None,
629 })
630 } else {
631 None
632 }
633}
634
635enum Drag {
637 Scrollbar {
639 id: WidgetId,
640 vertical: bool,
641 grab: f32,
642 track_start: f32,
643 track_len: f32,
644 thumb_len: f32,
645 max: f32,
646 reverse: bool,
649 },
650 RegionSelect { area: WidgetId },
654 RegionHandle {
658 area: WidgetId,
659 handle: crate::selection::Handle,
660 grab: Point,
661 },
662 TextSelect {
664 id: WidgetId,
665 rect: frus_widgets::Rect,
666 },
667 SelectionHandle {
671 id: WidgetId,
672 rect: frus_widgets::Rect,
673 handle: crate::selection::Handle,
674 grab: Point,
675 },
676 Widget {
680 id: WidgetId,
681 rect: frus_widgets::Rect,
682 last_x: f32,
683 press_x: f32,
686 track: frus_widgets::DragTrack,
687 active: bool,
690 thumb: Option<usize>,
693 },
694 Reorder {
698 id: WidgetId,
699 from: usize,
700 start: Point,
701 moved: bool,
702 carried: Option<(WidgetId, Rect)>,
708 },
709 Pan {
715 id: WidgetId,
716 last: Point,
717 moved: bool,
718 viewport: frus_widgets::Rect,
719 },
720 Scroll {
723 id: WidgetId,
724 last: Point,
725 moved: bool,
726 carried: (f32, f32),
730 dismiss: Option<frus_widgets::Dismissable>,
735 pan: Option<(WidgetId, frus_widgets::PanAxis)>,
738 axis: Option<bool>,
748 },
749 Gesture {
753 id: WidgetId,
754 axis: frus_widgets::PanAxis,
755 start: Point,
756 last: Point,
757 moved: bool,
758 },
759 Dismiss {
763 item: frus_widgets::Dismissable,
764 last: Point,
765 moved: bool,
768 },
769 Item {
774 source: frus_widgets::DragSource,
775 start: Point,
776 moved: bool,
779 over: Option<WidgetId>,
780 },
781 Sheet {
786 id: WidgetId,
787 last: Point,
788 moved: bool,
791 available: f32,
792 },
793 Back { start_x: f32 },
796}
797
798fn install_ambient<A: Application>(app: &A, preferred: &[frus_widgets::Locale]) {
824 frus_widgets::ScrollConfiguration::set_root(app.scroll_behavior());
827 frus_widgets::locale::install(app.resolved_locale(preferred));
831 if let Some(table) = app.localizations() {
832 frus_widgets::localizations::install(table);
833 }
834}
835
836fn build_view<A: Application>(
837 app: &A,
838 theme: &Theme,
839 runtime: &frus_widgets::Runtime,
840) -> Box<dyn Widget<A::Message>> {
841 debug_assert!(
842 frus_widgets::MediaQuery::of().is_described(),
843 "a view is being built with no surface described: install one first — milestone 416"
844 );
845 runtime.states.begin_build();
848 let tree = app.view(theme);
849 build_deferred(tree.as_ref(), theme, runtime);
850 tree
851}
852
853struct Mailbox<M: 'static>(Option<EventLoopProxy<M>>);
856
857impl<M: 'static> Clone for Mailbox<M> {
858 fn clone(&self) -> Self {
859 Self(self.0.clone())
860 }
861}
862
863impl<M: 'static> Mailbox<M> {
864 fn send_event(&self, message: M) -> Result<(), ()> {
867 match &self.0 {
868 Some(proxy) => proxy.send_event(message).map_err(|_| ()),
869 None => Err(()),
870 }
871 }
872}
873
874pub struct App<A: Application> {
880 app: A,
882 proxy: Mailbox<A::Message>,
884 window: Option<Arc<Window>>,
885 renderer: Option<Renderer>,
886 renderer_lost: bool,
889 #[cfg(web)]
893 pending_renderer: std::rc::Rc<std::cell::RefCell<Option<Renderer>>>,
894 #[cfg(web)]
896 history: crate::history::History,
897 #[cfg(web)]
900 address: (crate::LocationStrategy, String),
901 #[cfg(desktop)]
903 a11y: Option<crate::a11y::A11y>,
904 #[cfg(web)]
906 a11y: Option<crate::a11y_web::A11y>,
907 #[cfg(web)]
909 web_editor: Option<crate::web_editing::Editor>,
910 ui: Option<Ui<A::Message>>,
912 tree: Option<Box<dyn Widget<A::Message>>>,
914 cursor: Point,
916 hover: crate::hover::Hover,
919 scale: f32,
921 last_size: Option<(f32, f32)>,
923 runtime: Runtime,
925 themes: crate::theming::ThemeFade,
930 reload: Option<crate::reload::ReloadWatcher>,
932 #[cfg(any(desktop, android))]
934 dev: Option<crate::dev::DevChannel>,
935 reported_overflows: std::cell::RefCell<std::collections::HashSet<u64>>,
938 inspector: bool,
940 inspector_dump: bool,
942 debug: DebugAsks,
945 shift: bool,
947 ctrl: bool,
948 alt: bool,
949 meta: bool,
950 goal_x: Option<f32>,
954 clipboard: clip::Clipboard,
957 started: bool,
960 last_frame: Option<Instant>,
962 build_dirty: bool,
968 app_was_animating: bool,
973 drag: Option<Drag>,
975 file_drag: Option<crate::file_drop::FileDrag>,
978 pointer_touch: bool,
981 gesture_velocity: VelocityTracker,
988 gesture_clock: Option<(f32, f32)>,
995 gesture_start: Instant,
996 reorder_x: f32,
1000 reorder_y: f32,
1005 #[cfg(any(desktop, web))]
1009 announce: String,
1010 press: PressRecognizer,
1012 long_press_msg: Option<A::Message>,
1014 pending_word: Option<WidgetId>,
1017 pending_region_hold: bool,
1020 caret: crate::caret::CaretBlink,
1022 #[cfg(android)]
1026 autofill_reported: Vec<crate::autofill::AutofillField>,
1027 #[cfg(android)]
1029 autofill_focus: Option<WidgetId>,
1030 #[cfg(android)]
1032 autofill_group: Option<WidgetId>,
1033 last_edit_at: Option<Instant>,
1037 pending_lift: Option<frus_widgets::DragSource>,
1041 pending_reorder: Option<(WidgetId, usize, Point)>,
1049 last_click_time: Option<Instant>,
1051 last_region_click: Option<(Instant, Point)>,
1054 pending_tap: Option<PendingTap<A::Message>>,
1056 task_arrivals: u64,
1059 entered: Vec<(WidgetId, Rect)>,
1062 pointer_held: bool,
1064 captured: Vec<(WidgetId, Rect)>,
1067 leaving_counter: u64,
1069 running_subs: HashMap<u64, SubHandle>,
1071 pending_focus: Vec<u64>,
1074 pending_scroll: Vec<(u64, ScrollTo)>,
1078 retry_scroll: Vec<(u64, ScrollTo)>,
1082 pending_sheet: Vec<(u64, SheetTo)>,
1085 retry_sheet: Vec<(u64, SheetTo)>,
1088 focus_history: Vec<WidgetId>,
1093 prev_focus: Option<WidgetId>,
1095 focus_reported: Option<WidgetId>,
1098 autofocused: std::collections::HashSet<WidgetId>,
1101 frame_stats: crate::frame_stats::FrameStats,
1103 frame_costs: crate::frame_stats::FrameCosts,
1105 occluded: bool,
1107 elapsed: f32,
1109 last_insets: WindowInsets,
1111 simulated: Option<crate::simulated::Simulated>,
1113 #[cfg_attr(not(windows), allow(dead_code))]
1116 title_bar_on: bool,
1117 title_bar: Option<frus_widgets::TitleBar>,
1118 #[cfg(all(desktop, feature = "tray"))]
1120 tray: Option<crate::tray::native::Live>,
1121 #[cfg(all(desktop, feature = "tray"))]
1123 tray_keeps_running: bool,
1124 #[cfg(all(desktop, feature = "taskbar"))]
1126 taskbar: crate::Taskbar,
1127 #[cfg(all(desktop, feature = "global-shortcuts"))]
1129 shortcuts: crate::shortcuts::native::Registered,
1130 #[cfg(all(desktop, feature = "global-shortcuts"))]
1132 shortcuts_refused: std::collections::HashSet<String>,
1133 #[cfg_attr(not(windows), allow(dead_code))]
1135 window_icon: Option<&'static [u8]>,
1136 reveal_after_keyboard: bool,
1141 platform: PlatformSettings,
1150 inset_baseline: Option<(Insets, (u32, u32))>,
1154 lifecycle: Lifecycle,
1158 #[cfg(android)]
1159 android_app: Option<winit::platform::android::activity::AndroidApp>,
1160 #[cfg(android)]
1162 soft_input_shown: bool,
1163 #[cfg(android)]
1166 system_bars: Option<frus_widgets::SystemBars>,
1167}
1168
1169impl<A: Application> App<A> {
1170 pub fn new(app: A, proxy: EventLoopProxy<A::Message>) -> Self {
1172 let mut shell = Self::with_mailbox(app, Mailbox(Some(proxy)));
1173 shell.simulated = crate::simulated::Simulated::from_env();
1176 if let Some(simulated) = &shell.simulated {
1177 frus_widgets::set_debug_default_target_platform_override(Some(
1178 simulated.profile.platform,
1179 ));
1180 }
1181 shell
1182 }
1183
1184 #[cfg(any(test, feature = "testing"))]
1188 fn detached(app: A) -> Self {
1189 let mut shell = Self::with_mailbox(app, Mailbox(None));
1190 shell.clipboard = clip::Clipboard::in_memory();
1194 shell
1195 }
1196
1197 fn with_mailbox(app: A, proxy: Mailbox<A::Message>) -> Self {
1198 #[cfg(feature = "net")]
1203 frus_widgets::set_image_fetcher(fetch_image_bytes);
1204 #[cfg(not(web))]
1207 frus_widgets::set_task_spawner(|task| crate::runtime::spawn(task).detach());
1208 #[cfg(web)]
1209 frus_widgets::set_task_spawner(wasm_bindgen_futures::spawn_local);
1210 Self {
1211 app,
1212 proxy,
1213 window: None,
1214 renderer: None,
1215 renderer_lost: false,
1216 #[cfg(web)]
1217 pending_renderer: std::rc::Rc::new(std::cell::RefCell::new(None)),
1218 #[cfg(web)]
1219 history: crate::history::History::new(),
1220 #[cfg(web)]
1221 address: (crate::LocationStrategy::Hash, "/".to_string()),
1222 #[cfg(desktop)]
1223 a11y: None,
1224 #[cfg(web)]
1225 a11y: None,
1226 #[cfg(web)]
1227 web_editor: None,
1228 ui: None,
1229 tree: None,
1230 cursor: Point::new(0.0, 0.0),
1231 hover: crate::hover::Hover::default(),
1232 scale: 1.0,
1233 last_size: None,
1234 runtime: Runtime::default(),
1235 themes: crate::theming::ThemeFade::default(),
1236 reload: crate::reload::ReloadWatcher::new(),
1237 #[cfg(any(desktop, android))]
1238 dev: None,
1239 reported_overflows: std::cell::RefCell::new(std::collections::HashSet::new()),
1240 inspector: false,
1241 inspector_dump: false,
1242 debug: DebugAsks::default(),
1243 shift: false,
1244 ctrl: false,
1245 alt: false,
1246 meta: false,
1247 goal_x: None,
1248 clipboard: clip::Clipboard::new(),
1249 started: false,
1250 last_frame: None,
1251 build_dirty: true,
1252 app_was_animating: false,
1253 drag: None,
1254 file_drag: None,
1255 pointer_touch: false,
1256 gesture_velocity: VelocityTracker::default(),
1257 gesture_clock: None,
1258 gesture_start: Instant::now(),
1259 reorder_x: 0.0,
1260 reorder_y: 0.0,
1261 #[cfg(any(desktop, web))]
1262 announce: String::new(),
1263 press: PressRecognizer::new(),
1264 long_press_msg: None,
1265 pending_word: None,
1266 pending_region_hold: false,
1267 caret: crate::caret::CaretBlink::new(),
1268 #[cfg(android)]
1269 autofill_reported: Vec::new(),
1270 #[cfg(android)]
1271 autofill_focus: None,
1272 #[cfg(android)]
1273 autofill_group: None,
1274 last_edit_at: None,
1275 pending_lift: None,
1276 pending_reorder: None,
1277 last_click_time: None,
1278 last_region_click: None,
1279 pending_tap: None,
1280 task_arrivals: 0,
1281 entered: Vec::new(),
1282 pointer_held: false,
1283 captured: Vec::new(),
1284 leaving_counter: 0,
1285 running_subs: HashMap::new(),
1286 pending_focus: Vec::new(),
1287 pending_scroll: Vec::new(),
1288 retry_scroll: Vec::new(),
1289 pending_sheet: Vec::new(),
1290 retry_sheet: Vec::new(),
1291 focus_history: Vec::new(),
1292 prev_focus: None,
1293 focus_reported: None,
1294 autofocused: std::collections::HashSet::new(),
1295 frame_stats: crate::frame_stats::FrameStats::new(),
1296 frame_costs: crate::frame_stats::FrameCosts::default(),
1297 occluded: false,
1298 elapsed: 0.0,
1299 last_insets: WindowInsets::ZERO,
1300 simulated: None,
1301 title_bar_on: false,
1302 title_bar: None,
1303 #[cfg(all(desktop, feature = "tray"))]
1304 tray: None,
1305 #[cfg(all(desktop, feature = "tray"))]
1306 tray_keeps_running: false,
1307 #[cfg(all(desktop, feature = "taskbar"))]
1308 taskbar: crate::Taskbar::new(),
1309 #[cfg(all(desktop, feature = "global-shortcuts"))]
1310 shortcuts: Default::default(),
1311 #[cfg(all(desktop, feature = "global-shortcuts"))]
1312 shortcuts_refused: Default::default(),
1313 window_icon: None,
1314 reveal_after_keyboard: false,
1315 platform: PlatformSettings::default(),
1316 inset_baseline: None,
1317 lifecycle: Lifecycle::Detached,
1319 #[cfg(android)]
1320 android_app: None,
1321 #[cfg(android)]
1322 soft_input_shown: false,
1323 #[cfg(android)]
1324 system_bars: None,
1325 }
1326 }
1327
1328 #[cfg(any(desktop, web))]
1332 fn activate_for_assistive_tech(&mut self, id: WidgetId) {
1333 if let Some(action) = self.ui.as_ref().and_then(|ui| ui.edit_action_for(id)) {
1334 self.perform_edit_action(action);
1335 return;
1336 }
1337 if let Some(msg) = self.ui.as_ref().and_then(|ui| ui.msg_for(id)) {
1338 if self.ui.as_ref().is_some_and(|ui| ui.hit_is_on_toolbar(id)) {
1340 self.runtime.selection_handles = None;
1341 self.hide_selection_toolbar();
1342 }
1343 self.dispatch(msg);
1344 self.request_redraw();
1345 }
1346 }
1347
1348 #[cfg(any(desktop, test, feature = "testing"))]
1352 fn set_value_for_assistive_tech(&mut self, id: WidgetId, value: String) {
1353 let message = self
1354 .tree
1355 .as_ref()
1356 .and_then(|tree| find_widget(tree.as_ref(), id))
1357 .and_then(|widget| widget.replace_value(value.clone()));
1358 let Some(message) = message else {
1359 return;
1360 };
1361 self.runtime.edits.insert(
1362 id,
1363 Edit {
1364 cursor: value.chars().count(),
1365 anchor: None,
1366 composing: None,
1367 },
1368 );
1369 self.dispatch_edit(message);
1370 self.request_redraw();
1371 }
1372
1373 #[cfg(desktop)]
1376 fn drain_a11y_actions(&mut self) {
1377 use crate::a11y::A11yAction;
1378 let actions = match self.a11y.as_ref() {
1379 Some(a11y) => a11y.take_actions(),
1380 None => return,
1381 };
1382 for action in actions {
1383 match action {
1384 A11yAction::Click(id) => self.activate_for_assistive_tech(id),
1385 A11yAction::SetValue(id, value) => self.set_value_for_assistive_tech(id, value),
1386 A11yAction::Focus(id) => {
1387 if self.ui.as_ref().is_some_and(|ui| ui.hit_is_on_toolbar(id)) {
1391 continue;
1392 }
1393 self.runtime.input.focused = Some(id);
1394 self.runtime.focus_visible = true;
1395 self.request_redraw();
1396 }
1397 }
1398 }
1399 }
1400
1401 #[cfg(web)]
1405 fn sync_web_editing(&mut self) {
1406 let placement = self.placement();
1409 let css = if self.simulated.is_some() {
1410 1.0
1411 } else {
1412 self.app.density()
1413 };
1414 let Some(editor) = self.web_editor.as_mut() else {
1415 return;
1416 };
1417 let field = self.runtime.input.focused.and_then(|id| {
1418 let widget = find_widget(self.tree.as_ref()?.as_ref(), id)?;
1419 if !wants_keyboard(widget) {
1420 return None;
1421 }
1422 let (_, rect, semantics) = self
1423 .ui
1424 .as_ref()?
1425 .semantics()
1426 .iter()
1427 .find(|(node, ..)| *node == id)?;
1428 Some(crate::web_editing::Field {
1429 id,
1430 rect: match placement {
1431 Some(placement) => crate::simulated::Simulated::to_window(placement, *rect),
1432 None => *rect,
1433 },
1434 web: widget.ime().web(),
1435 label: semantics.label.as_deref(),
1436 text: widget.text_value().unwrap_or_default(),
1437 edit: self.runtime.edits.get(&id).copied().unwrap_or_default(),
1438 })
1439 });
1440 editor.sync(field, css);
1441 }
1442
1443 #[cfg(web)]
1448 fn drain_web_editing(&mut self) {
1449 let changes = match self.web_editor.as_ref() {
1450 Some(editor) => editor.take_changes(),
1451 None => return,
1452 };
1453 if changes.is_empty() {
1454 return;
1455 }
1456 for (id, element) in changes {
1457 if self.runtime.input.focused != Some(id) {
1459 continue;
1460 }
1461 let (value, edit) = crate::web_editing::read(&element);
1462 let message = self
1463 .tree
1464 .as_ref()
1465 .and_then(|tree| find_widget(tree.as_ref(), id))
1466 .filter(|widget| widget.text_value() != Some(value.as_str()))
1467 .and_then(|widget| widget.replace_value(value));
1468 self.runtime.edits.insert(id, edit);
1469 if let Some(message) = message {
1470 self.dispatch_edit(message);
1471 }
1472 }
1473 self.request_redraw();
1474 }
1475
1476 #[cfg(web)]
1479 fn drain_a11y_actions(&mut self) {
1480 use crate::a11y_web::A11yAction;
1481 let actions = match self.a11y.as_ref() {
1482 Some(a11y) => a11y.take_actions(),
1483 None => return,
1484 };
1485 for action in actions {
1486 match action {
1487 A11yAction::Click(id) => self.activate_for_assistive_tech(id),
1488 A11yAction::Focus(id) => {
1489 if self.ui.as_ref().is_some_and(|ui| ui.hit_is_on_toolbar(id)) {
1493 continue;
1494 }
1495 self.runtime.input.focused = Some(id);
1496 self.runtime.focus_visible = true;
1497 self.request_redraw();
1498 }
1499 }
1500 }
1501 }
1502
1503 fn sync_system_bars(&mut self, theme: &Theme) {
1512 #[cfg(android)]
1513 {
1514 let Some(ui) = self.ui.as_ref() else {
1515 return;
1516 };
1517 let Some(window) = self.window.as_ref() else {
1518 return;
1519 };
1520 let scale = self.total_scale();
1521 let size = window.inner_size();
1522 let (width, height) = (size.width as f32 / scale, size.height as f32 / scale);
1523 let edges = self.last_insets.padding;
1524 let bars = ui
1525 .system_ui_style(
1526 frus_widgets::Point::new(width / 2.0, edges.top + 0.5),
1527 frus_widgets::Point::new(width / 2.0, height - edges.bottom - 0.5),
1528 )
1529 .resolve(theme);
1530 if self.system_bars != Some(bars) && crate::android_system_bars::apply(bars) {
1531 self.system_bars = Some(bars);
1532 }
1533 }
1534 #[cfg(not(android))]
1535 let _ = theme;
1536 }
1537
1538 fn sync_soft_input(&mut self) {
1542 if self.simulated.is_some() {
1545 let editing = self
1546 .runtime
1547 .input
1548 .focused
1549 .and_then(|id| {
1550 self.tree
1551 .as_ref()
1552 .and_then(|tree| find_widget(tree.as_ref(), id))
1553 })
1554 .is_some_and(wants_keyboard);
1555 if let Some(simulated) = self.simulated.as_mut() {
1556 if simulated.keyboard != editing {
1557 simulated.keyboard = editing;
1558 self.build_dirty = true;
1559 self.request_redraw();
1560 }
1561 }
1562 }
1563 #[cfg(android)]
1565 if self.simulated.is_none() {
1566 let editing = self
1567 .runtime
1568 .input
1569 .focused
1570 .and_then(|id| {
1571 self.tree
1572 .as_ref()
1573 .and_then(|tree| find_widget(tree.as_ref(), id))
1574 })
1575 .is_some_and(wants_keyboard);
1576 if editing != self.soft_input_shown {
1577 self.soft_input_shown = editing;
1578 self.end_composition();
1579 if crate::android_ime::installed() {
1580 if editing {
1582 if let Some(id) = self.runtime.input.focused {
1583 self.push_ime_context(id);
1584 }
1585 crate::android_ime::start_input(self.focused_ime());
1586 } else {
1587 crate::android_ime::clear_editor_state();
1588 crate::android_ime::stop_input();
1589 }
1590 } else if let Some(app) = &self.android_app {
1591 if editing {
1593 app.show_soft_input(true);
1594 } else {
1595 app.hide_soft_input(false);
1596 }
1597 }
1598 }
1599 }
1600 }
1601
1602 #[cfg(android)]
1609 fn focused_ime(&self) -> frus_widgets::Ime {
1610 self.runtime
1611 .input
1612 .focused
1613 .and_then(|id| {
1614 self.tree
1615 .as_ref()
1616 .and_then(|tree| find_widget(tree.as_ref(), id))
1617 })
1618 .map(|widget| widget.ime())
1619 .unwrap_or_default()
1620 }
1621
1622 fn set_lifecycle(&mut self, state: Lifecycle) {
1624 if self.lifecycle != state {
1625 self.lifecycle = state;
1626 self.app.on_lifecycle(state);
1627 }
1628 }
1629
1630 fn request_soft_input(&mut self) {
1639 #[cfg(android)]
1640 {
1641 let typing = self
1644 .runtime
1645 .input
1646 .focused
1647 .and_then(|id| {
1648 self.tree
1649 .as_ref()
1650 .and_then(|tree| find_widget(tree.as_ref(), id))
1651 })
1652 .is_some_and(wants_keyboard);
1653 if !typing {
1654 return;
1655 }
1656 self.soft_input_shown = true;
1657 self.end_composition();
1658 if crate::android_ime::installed() {
1659 if let Some(id) = self.runtime.input.focused {
1660 self.push_ime_context(id);
1661 }
1662 crate::android_ime::start_input(self.focused_ime());
1663 } else if let Some(app) = &self.android_app {
1664 app.show_soft_input(true);
1665 }
1666 }
1667 }
1668
1669 #[cfg(android)]
1674 fn drain_ime(&mut self) {
1675 use crate::ime::Step;
1676 let events = crate::android_ime::drain();
1677 if events.is_empty() {
1678 return;
1679 }
1680 let Some(focused) = self.runtime.input.focused else {
1681 return;
1682 };
1683 for event in events {
1684 let edit = self
1685 .runtime
1686 .edits
1687 .get(&focused)
1688 .copied()
1689 .unwrap_or_default();
1690 let text = self
1693 .tree
1694 .as_ref()
1695 .and_then(|tree| find_widget(tree.as_ref(), focused))
1696 .and_then(|widget| widget.text_value().map(str::to_owned))
1697 .unwrap_or_default();
1698 for step in crate::ime::plan(&event, &edit, &text) {
1699 match step {
1700 Step::Place { cursor, anchor } => {
1701 let edit = self.runtime.edits.entry(focused).or_default();
1702 edit.cursor = cursor;
1703 edit.anchor = anchor;
1704 }
1705 Step::Key(key) => self.apply_key(focused, key),
1706 Step::Compose(region) => {
1707 if let Some(edit) = self.runtime.edits.get_mut(&focused) {
1708 edit.composing = region;
1709 }
1710 }
1711 Step::ComposeTo(start) => {
1712 if let Some(edit) = self.runtime.edits.get_mut(&focused) {
1713 let end = edit.cursor;
1714 edit.composing = (end > start).then_some((start, end));
1715 }
1716 }
1717 }
1718 }
1719 }
1720 self.push_ime_context(focused);
1722 self.request_redraw();
1723 }
1724
1725 #[cfg(android)]
1733 fn sync_autofill(&mut self) {
1734 use crate::{android_autofill as platform, autofill};
1735 if !crate::android_ime::installed() {
1736 return;
1737 }
1738 let scale = self.total_scale();
1739 let (Some(ui), Some(tree)) = (self.ui.as_ref(), self.tree.as_ref()) else {
1740 return;
1741 };
1742 let fields_of = |id: WidgetId| {
1743 autofill::structure(&ui.form_of(id), scale, |widget| {
1744 find_widget(tree.as_ref(), widget).map(|w| {
1745 (
1746 w.autofill_hints(),
1747 w.text_value().unwrap_or_default().to_owned(),
1748 )
1749 })
1750 })
1751 };
1752 let focus = self.runtime.input.focused.filter(|&id| {
1754 find_widget(tree.as_ref(), id).is_some_and(|w| !w.autofill_hints().is_empty())
1755 });
1756 if focus != self.autofill_focus {
1757 if let Some(left) = self.autofill_focus {
1758 platform::exit(autofill::virtual_id(left));
1759 }
1760 if let Some(entered) = focus {
1761 let group = ui.form_group(entered);
1762 let shown: &[autofill::AutofillField] =
1765 if group.is_some() && group == self.autofill_group {
1766 &self.autofill_reported
1767 } else {
1768 &[]
1769 };
1770 let fields = autofill::with_gone(fields_of(entered), shown);
1771 platform::publish(&fields);
1772 platform::enter(autofill::virtual_id(entered));
1773 self.autofill_reported = fields;
1774 self.autofill_group = group;
1775 }
1776 self.autofill_focus = focus;
1777 } else if let Some(id) = focus {
1778 let fields = autofill::with_gone(fields_of(id), &self.autofill_reported);
1779 for (virtual_id, value) in autofill::changed(&self.autofill_reported, &fields) {
1780 platform::value_changed(virtual_id, &value);
1781 }
1782 self.autofill_reported = fields;
1783 }
1784 if let Some(group) = self.autofill_group {
1785 if ui.group_stops(group).is_empty() {
1786 platform::commit();
1787 self.autofill_group = None;
1788 self.autofill_reported.clear();
1789 }
1790 }
1791 }
1792
1793 #[cfg(android)]
1797 fn drain_autofill(&mut self) {
1798 let fills = crate::android_autofill::drain();
1799 if fills.is_empty() {
1800 return;
1801 }
1802 for (widget, value) in crate::autofill::route(&self.autofill_reported, fills) {
1803 let message = self
1804 .tree
1805 .as_ref()
1806 .and_then(|tree| find_widget(tree.as_ref(), widget))
1807 .and_then(|w| w.replace_value(value.clone()));
1808 if let Some(message) = message {
1809 self.runtime.edits.insert(
1810 widget,
1811 Edit {
1812 cursor: value.chars().count(),
1813 anchor: None,
1814 composing: None,
1815 },
1816 );
1817 self.dispatch_edit(message);
1818 }
1819 }
1820 if let Some(id) = self.runtime.input.focused {
1821 self.push_ime_context(id);
1822 }
1823 self.request_redraw();
1824 }
1825
1826 #[cfg(android)]
1829 fn push_ime_context(&self, id: WidgetId) {
1830 let value = self
1831 .tree
1832 .as_ref()
1833 .and_then(|tree| find_widget(tree.as_ref(), id))
1834 .and_then(|widget| widget.text_value().map(|s| s.to_string()));
1835 if let Some(text) = value {
1836 let edit = self.runtime.edits.get(&id).copied().unwrap_or_default();
1837 crate::android_ime::set_editor_state(&text, edit.cursor, edit.selection_range());
1838 let len = text.chars().count();
1842 let at = |i: usize| crate::ime::utf16_index(&text, i.min(len)) as i32;
1843 let (start, end) = edit.selection_range().unwrap_or((edit.cursor, edit.cursor));
1844 let (cand_start, cand_end) = edit
1845 .composing
1846 .map(|(s, e)| (at(s), at(e)))
1847 .unwrap_or((-1, -1));
1848 crate::android_ime::update_selection(at(start), at(end), cand_start, cand_end);
1849 }
1850 }
1851
1852 #[cfg(android)]
1855 fn end_composition(&mut self) {
1856 if let Some(id) = self.runtime.input.focused {
1857 if let Some(edit) = self.runtime.edits.get_mut(&id) {
1858 edit.composing = None;
1859 }
1860 }
1861 }
1862
1863 #[cfg(android)]
1865 pub(crate) fn set_android_app(
1866 &mut self,
1867 android_app: winit::platform::android::activity::AndroidApp,
1868 ) {
1869 self.android_app = Some(android_app);
1870 }
1871
1872 fn compute_insets(&self, phys_w: u32, phys_h: u32, scale: f32) -> Insets {
1883 #[cfg(android)]
1884 if let Some(app) = &self.android_app {
1885 let r = app.content_rect();
1886 if r.right > r.left && r.bottom > r.top {
1888 let left = r.left.max(0) as f32;
1889 let top = r.top.max(0) as f32;
1890 let right = (phys_w as i32 - r.right).max(0) as f32;
1891 let bottom = (phys_h as i32 - r.bottom).max(0) as f32;
1892 return Insets::new(top / scale, right / scale, bottom / scale, left / scale);
1893 }
1894 }
1895 let _ = (phys_w, phys_h, scale);
1896 Insets::ZERO
1897 }
1898
1899 fn refresh_platform_settings(&mut self) {
1905 #[cfg(android)]
1906 if let Some(app) = &self.android_app {
1907 let read = crate::android_settings::read(app);
1908 if read != self.platform {
1909 self.platform = read;
1910 self.build_dirty = true;
1911 }
1912 return;
1913 }
1914 let brightness = self
1920 .window
1921 .as_ref()
1922 .and_then(|window| window.theme())
1923 .map(|theme| match theme {
1924 winit::window::Theme::Dark => frus_widgets::Brightness::Dark,
1925 winit::window::Theme::Light => frus_widgets::Brightness::Light,
1926 })
1927 .unwrap_or_default();
1928 if brightness != self.platform.brightness {
1929 self.platform.brightness = brightness;
1930 self.build_dirty = true;
1931 }
1932
1933 if self.platform.locales.is_empty() {
1940 let read = platform_locales();
1941 if !read.is_empty() {
1942 self.platform.locales = read;
1943 self.build_dirty = true;
1944 }
1945 }
1946 }
1947}
1948
1949#[cfg(android)]
1958fn platform_locales() -> Vec<frus_widgets::Locale> {
1959 Vec::new()
1960}
1961
1962#[cfg(not(android))]
1963fn platform_locales() -> Vec<frus_widgets::Locale> {
1964 #[cfg(not(web))]
1965 {
1966 sys_locale::get_locales()
1967 .filter_map(|tag| frus_widgets::Locale::parse(&tag))
1968 .collect()
1969 }
1970 #[cfg(web)]
1971 {
1972 let Some(navigator) = web_sys::window().map(|window| window.navigator()) else {
1975 return Vec::new();
1976 };
1977 let listed: Vec<frus_widgets::Locale> = navigator
1978 .languages()
1979 .iter()
1980 .filter_map(|value| value.as_string())
1981 .filter_map(|tag| frus_widgets::Locale::parse(&tag))
1982 .collect();
1983 if listed.is_empty() {
1984 return navigator
1985 .language()
1986 .and_then(|tag| frus_widgets::Locale::parse(&tag))
1987 .into_iter()
1988 .collect();
1989 }
1990 listed
1991 }
1992}
1993
1994fn memory_trimmed() {
1996 let before = frus_widgets::image_cache_bytes();
1997 frus_widgets::trim_image_caches();
1998 log::info!(
1999 "memory is low: decoded network images trimmed from {} to {} KB",
2000 before / 1024,
2001 frus_widgets::image_cache_bytes() / 1024
2002 );
2003}
2004
2005impl<A: Application> ApplicationHandler<A::Message> for App<A> {
2006 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
2007 if self.window.is_some() {
2008 return;
2009 }
2010 self.set_lifecycle(Lifecycle::Resumed);
2012 self.refresh_platform_settings();
2015
2016 let mut attributes = Window::default_attributes()
2017 .with_title(self.app.title())
2018 .with_min_inner_size(winit::dpi::LogicalSize::new(360.0, 280.0));
2020 #[cfg(desktop)]
2023 {
2024 attributes = attributes.with_visible(false);
2025 }
2026 #[cfg(desktop)]
2030 {
2031 let icon = crate::icon::window_icon(&self.app.icon());
2032 #[cfg(windows)]
2033 {
2034 use winit::platform::windows::WindowAttributesExtWindows;
2035 attributes = attributes.with_taskbar_icon(icon.clone());
2036 }
2037 attributes = attributes.with_window_icon(icon);
2038 }
2039 #[cfg(not(web))]
2044 if let Some((w, h)) = self.app.window_size() {
2045 attributes = attributes.with_inner_size(winit::dpi::LogicalSize::new(w, h));
2046 }
2047 #[cfg(desktop)]
2050 if let Some(simulated) = &self.simulated {
2051 let room = simulated.window_size();
2052 let fit = event_loop
2053 .primary_monitor()
2054 .map(|monitor| {
2055 let screen = monitor.size().to_logical::<f32>(monitor.scale_factor());
2056 (0.9 * screen.width / room.width)
2057 .min(0.9 * screen.height / room.height)
2058 .min(1.0)
2059 })
2060 .unwrap_or(1.0);
2061 attributes = attributes.with_inner_size(winit::dpi::LogicalSize::new(
2062 room.width * fit,
2063 room.height * fit,
2064 ));
2065 }
2066 #[cfg(desktop)]
2068 if let Some([x, y, w, h]) = crate::dev::restore_window() {
2069 attributes = attributes
2070 .with_position(winit::dpi::PhysicalPosition::new(x, y))
2071 .with_inner_size(winit::dpi::PhysicalSize::new(w as u32, h as u32));
2072 }
2073 #[cfg(web)]
2075 {
2076 use winit::platform::web::WindowAttributesExtWebSys;
2077 attributes = attributes.with_append(true);
2078 }
2079 let window = match event_loop.create_window(attributes) {
2080 Ok(window) => {
2081 crate::startup::window_made();
2082 #[cfg(any(
2083 all(desktop, feature = "file-dialogs"),
2084 all(any(desktop, android), feature = "notifications")
2085 ))]
2086 crate::interface_thread::window_made();
2087 #[cfg(all(desktop, feature = "file-dialogs"))]
2088 crate::file_dialog::window_made(&window);
2089 #[cfg(all(desktop, feature = "notifications"))]
2090 crate::notification::window_made(&self.app.title(), self.app.icon().png());
2091 Arc::new(window)
2092 }
2093 Err(err) => {
2094 log::error!("failed to create the window: {err}");
2095 event_loop.exit();
2096 return;
2097 }
2098 };
2099
2100 #[cfg(web)]
2104 {
2105 self.scale = window.scale_factor() as f32;
2106 self.window = Some(window.clone());
2107 self.a11y = crate::a11y_web::A11y::new(&window);
2110 self.web_editor = crate::web_editing::Editor::new(&window);
2112 crate::web_keys::let_the_browser_keep_its_shortcuts();
2115 crate::web_icon::apply(&self.app.icon());
2117 self.build_dirty = true;
2118 if !self.started {
2119 self.started = true;
2120 let command = self.app.init();
2121 self.run_command(command);
2122 self.sync_subscriptions();
2123 self.open_launch_address(&window);
2124 crate::dev_commands::set_window(Some(window.clone()));
2125 if let Some(states) = crate::web_dev::start() {
2126 self.runtime.states.restore_from(&states);
2127 }
2128 #[cfg(feature = "hot-reload")]
2129 {
2130 crate::hot::set_window(window.clone());
2131 crate::hot::connect();
2132 }
2133 }
2134 let slot = self.pending_renderer.clone();
2135 let win = window.clone();
2136 let size = window.inner_size();
2137 wasm_bindgen_futures::spawn_local(async move {
2138 match Renderer::new(win.clone(), size.width.max(1), size.height.max(1)).await {
2139 Ok(r) => {
2140 crate::startup::renderer_made(r.startup_times());
2141 *slot.borrow_mut() = Some(r);
2142 win.request_redraw();
2143 }
2144 Err(err) => log::error!("failed to initialise the renderer (Web): {err:#}"),
2145 }
2146 });
2147 }
2148
2149 #[cfg(not(web))]
2150 {
2151 let size = window.inner_size();
2152 let (width, height) = (size.width.max(1), size.height.max(1));
2153 let see_through = if cfg!(windows) {
2157 match pollster::block_on(Renderer::see_through(window.clone(), width, height)) {
2158 Ok(renderer) => Some(renderer),
2159 Err(err) => {
2160 log::info!("title bar painted by frus: {err:#}");
2161 None
2162 }
2163 }
2164 } else {
2165 None
2166 };
2167 let renderer = match see_through {
2168 Some(renderer) => Ok(renderer),
2169 None => pollster::block_on(Renderer::new(window.clone(), width, height)),
2170 };
2171
2172 match renderer {
2173 Ok(renderer) => {
2174 crate::startup::renderer_made(renderer.startup_times());
2175 self.scale = window.scale_factor() as f32;
2176 #[cfg(desktop)]
2179 {
2180 self.a11y = Some(crate::a11y::A11y::new(event_loop, &window));
2181 window.set_visible(true);
2182 crate::instance::set_window(window.clone());
2185 }
2186 self.window = Some(window.clone());
2187 #[cfg(android)]
2189 crate::android_ime::set_window(Some(window.clone()));
2190 #[cfg(android)]
2191 crate::dev_commands::set_window(Some(window.clone()));
2192 self.renderer = Some(renderer);
2193 self.build_dirty = true;
2195 if !self.started {
2198 self.started = true;
2199 let command = self.app.init();
2200 self.run_command(command);
2201 self.sync_subscriptions();
2202 #[cfg(any(android, desktop))]
2203 self.open_launch_link();
2204 #[cfg(any(android, desktop))]
2205 self.connect_dev();
2206 #[cfg(feature = "hot-reload")]
2207 {
2208 crate::hot::set_window(window.clone());
2209 crate::hot::connect();
2210 }
2211 }
2212 window.request_redraw();
2213 }
2214 Err(err) => {
2215 log::error!("failed to initialise the renderer: {err:#}");
2216 event_loop.exit();
2217 }
2218 }
2219 }
2220 }
2221
2222 fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
2227 self.set_lifecycle(Lifecycle::Paused);
2229 #[cfg(android)]
2231 if self.dev.is_some() {
2232 if let Some(storage) = self
2233 .android_app
2234 .as_ref()
2235 .and_then(|a| a.internal_data_path())
2236 {
2237 crate::dev::keep_states(&storage, &self.runtime.states.snapshot());
2238 }
2239 }
2240 self.renderer = None;
2241 self.window = None;
2242 #[cfg(android)]
2243 crate::android_ime::set_window(None);
2244 #[cfg(android)]
2245 crate::dev_commands::set_window(None);
2246 self.last_frame = None;
2247 }
2248
2249 fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
2252 self.set_lifecycle(Lifecycle::Detached);
2253 }
2254
2255 fn memory_warning(&mut self, _event_loop: &ActiveEventLoop) {
2259 memory_trimmed();
2260 }
2261
2262 fn user_event(&mut self, _event_loop: &ActiveEventLoop, message: A::Message) {
2265 #[cfg(all(desktop, feature = "notifications"))]
2267 if crate::notification::take_raise() {
2268 if let Some(window) = &self.window {
2269 window.set_minimized(false);
2270 window.focus_window();
2271 }
2272 }
2273 self.dispatch(message);
2274 self.request_redraw();
2275 }
2276
2277 fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
2281 #[cfg(all(desktop, feature = "global-shortcuts"))]
2282 self.shortcut_presses();
2283 #[cfg(all(desktop, feature = "tray"))]
2284 self.tray_events(event_loop);
2285 if self.file_drag.is_none() {
2286 return;
2287 }
2288 self.deliver_dropped_files();
2289 if self.file_drag.is_some() {
2290 let at = self.system_cursor();
2291 self.files_moved(at);
2292 }
2293 event_loop.set_control_flow(self.idle_control_flow());
2294 }
2295
2296 fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) {
2300 if matches!(cause, StartCause::ResumeTimeReached { .. }) && self.press.poll(Instant::now())
2301 {
2302 self.hold_deadline_reached();
2303 }
2304 if matches!(cause, StartCause::ResumeTimeReached { .. }) {
2305 self.poll_pending_tap(Instant::now());
2306 }
2307
2308 if matches!(cause, StartCause::ResumeTimeReached { .. })
2310 && self.lifecycle == Lifecycle::Resumed
2311 && self.caret.next_toggle(Instant::now()).is_some()
2312 {
2313 self.request_redraw();
2314 }
2315
2316 #[cfg(android)]
2318 self.drain_ime();
2319 #[cfg(android)]
2321 self.drain_autofill();
2322
2323 #[cfg(desktop)]
2325 self.drain_a11y_actions();
2326 #[cfg(web)]
2328 self.drain_a11y_actions();
2329 #[cfg(web)]
2331 self.drain_web_editing();
2332
2333 if let Some(watcher) = self.reload.as_mut() {
2336 if watcher.binary_changed() {
2337 watcher.handoff(self.app.save_state());
2338 }
2339 }
2340 event_loop.set_control_flow(self.idle_control_flow());
2341 }
2342
2343 fn window_event(
2344 &mut self,
2345 event_loop: &ActiveEventLoop,
2346 _window_id: WindowId,
2347 event: WindowEvent,
2348 ) {
2349 #[cfg(desktop)]
2351 if let (Some(a11y), Some(window)) = (self.a11y.as_mut(), self.window.as_ref()) {
2352 a11y.process_event(window, &event);
2353 }
2354
2355 match event {
2356 WindowEvent::CloseRequested => {
2357 #[cfg(all(desktop, feature = "tray"))]
2360 if self.tray.is_some() && self.tray_keeps_running {
2361 if let Some(window) = &self.window {
2362 window.set_visible(false);
2363 }
2364 return;
2365 }
2366 event_loop.exit()
2367 }
2368
2369 WindowEvent::Focused(focused) => {
2373 #[cfg(web)]
2377 let focused = focused
2378 || web_sys::window()
2379 .and_then(|window| window.document())
2380 .and_then(|document| document.has_focus().ok())
2381 .unwrap_or(false);
2382 if !matches!(self.lifecycle, Lifecycle::Paused | Lifecycle::Detached) {
2383 self.set_lifecycle(if focused {
2384 Lifecycle::Resumed
2385 } else {
2386 Lifecycle::Inactive
2387 });
2388 }
2389 #[cfg(windows)]
2393 if self.title_bar_on {
2394 self.request_redraw();
2395 }
2396 }
2397
2398 WindowEvent::Resized(size) => {
2399 if let Some(renderer) = self.renderer.as_mut() {
2400 renderer.resize(size.width, size.height);
2401 }
2402 if let Some(window) = &self.window {
2403 window.request_redraw();
2404 }
2405 }
2406
2407 WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
2408 self.scale = scale_factor as f32;
2409 if let Some(window) = &self.window {
2411 let size = window.inner_size();
2412 if let Some(renderer) = self.renderer.as_mut() {
2413 renderer.resize(size.width, size.height);
2414 }
2415 }
2416 self.request_redraw();
2417 }
2418
2419 WindowEvent::Occluded(occluded) => {
2420 self.occluded = occluded;
2421 if !occluded {
2422 self.request_redraw();
2423 }
2424 }
2425
2426 WindowEvent::CursorMoved { position, .. } => {
2431 let position = self.to_logical(position.x, position.y);
2432 self.pointer(
2433 event_loop,
2434 PointerEvent {
2435 kind: PointerKind::Move,
2436 position,
2437 touch: false,
2438 },
2439 );
2440 }
2441
2442 WindowEvent::HoveredFile(path) => {
2446 let at = self.system_cursor();
2447 self.file_event(path, false, at);
2448 event_loop.set_control_flow(self.idle_control_flow());
2449 }
2450 WindowEvent::DroppedFile(path) => {
2451 let at = self.system_cursor();
2452 self.file_event(path, true, at);
2453 }
2454 WindowEvent::HoveredFileCancelled => self.files_left(),
2455
2456 WindowEvent::CursorLeft { .. } => {
2458 self.hover.left();
2459 self.sync_hover();
2460 self.sync_regions(false);
2461 }
2462
2463 WindowEvent::Touch(touch) => {
2464 let position = self.to_logical(touch.location.x, touch.location.y);
2465 let kind = match touch.phase {
2466 TouchPhase::Started => PointerKind::Down,
2467 TouchPhase::Moved => PointerKind::Move,
2468 TouchPhase::Ended => PointerKind::Up,
2469 TouchPhase::Cancelled => PointerKind::Cancel,
2470 };
2471 self.pointer(
2472 event_loop,
2473 PointerEvent {
2474 kind,
2475 position,
2476 touch: true,
2477 },
2478 );
2479 }
2480
2481 WindowEvent::ModifiersChanged(modifiers) => {
2482 let state = modifiers.state();
2483 self.shift = state.shift_key();
2484 self.ctrl = state.control_key();
2485 self.alt = state.alt_key();
2486 self.meta = state.super_key();
2487 }
2488
2489 WindowEvent::MouseInput {
2490 state: ElementState::Pressed,
2491 button: MouseButton::Left,
2492 ..
2493 } => self.pointer(
2494 event_loop,
2495 PointerEvent {
2496 kind: PointerKind::Down,
2497 position: self.cursor,
2498 touch: false,
2499 },
2500 ),
2501
2502 WindowEvent::MouseInput {
2503 state: ElementState::Released,
2504 button: MouseButton::Left,
2505 ..
2506 } => self.pointer(
2507 event_loop,
2508 PointerEvent {
2509 kind: PointerKind::Up,
2510 position: self.cursor,
2511 touch: false,
2512 },
2513 ),
2514
2515 WindowEvent::MouseInput {
2518 state: ElementState::Pressed,
2519 button: MouseButton::Right,
2520 ..
2521 } => self.context_click(),
2522
2523 WindowEvent::KeyboardInput { event, .. } if event.state == ElementState::Pressed => {
2524 self.key_down(&KeyDown::from(&event), Some(event_loop));
2525 #[cfg(web)]
2529 self.sync_web_editing();
2530 }
2531
2532 WindowEvent::MouseWheel { delta, .. } => {
2533 let (mut dx, mut dy) = match delta {
2534 MouseScrollDelta::LineDelta(x, y) => (x * SCROLL_SPEED, y * SCROLL_SPEED),
2535 MouseScrollDelta::PixelDelta(pos) => {
2537 let scale = self.total_scale();
2538 (pos.x as f32 / scale, pos.y as f32 / scale)
2539 }
2540 };
2541 if self.shift {
2543 dx = dy;
2544 dy = 0.0;
2545 }
2546 if let Some((id, viewport)) = self
2549 .ui
2550 .as_ref()
2551 .and_then(|ui| ui.interactive_at(self.cursor))
2552 {
2553 let (min, max) = self
2554 .tree
2555 .as_ref()
2556 .and_then(|tree| find_widget(tree.as_ref(), id))
2557 .and_then(|w| w.interactive())
2558 .unwrap_or((0.5, 4.0));
2559 let factor = (1.0 + dy * 0.1 / SCROLL_SPEED).clamp(0.2, 5.0);
2561 self.runtime.interactive_velocity.remove(&id);
2563 let view = self.runtime.interactive.entry(id).or_default();
2564 *view = view
2566 .zoom_at(factor, self.cursor, min, max)
2567 .clamped(viewport);
2568 self.request_redraw();
2569 return;
2570 }
2571 let hit = {
2575 let scroll = &self.runtime.scroll;
2576 self.ui.as_ref().and_then(|ui| {
2577 ui.scroll_chain(self.cursor).find(|area| {
2578 area.accepts_user_offset(
2579 scroll.get(&area.id).copied().unwrap_or((0.0, 0.0)),
2580 )
2581 })
2582 })
2583 };
2584 if let Some(area) = hit {
2585 let id = area.id;
2590 let physics = area.physics;
2591 let over = if physics.allows_overscroll() {
2592 SCROLL_OVER
2593 } else {
2594 0.0
2595 };
2596 self.runtime.stop_scroll_fling(id);
2599 let current = self.runtime.scroll.get(&id).copied().unwrap_or((0.0, 0.0));
2600 let (ddx, ddy) = area.offset_delta((dx, dy));
2604 let target = self.runtime.scroll_target.entry(id).or_insert(current);
2605 let (wanted_x, wanted_y) = (target.0 + ddx, target.1 + ddy);
2606 target.0 = wanted_x.clamp(-over, area.max_x + over);
2607 target.1 = wanted_y.clamp(-over, area.max_y + over);
2608 let refused = (wanted_x - target.0, wanted_y - target.1);
2609 self.runtime.scroll_velocity.entry(id).or_insert((0.0, 0.0));
2610 let cursor = self.cursor;
2613 for (refused, vertical, extent) in [
2614 (refused.0, false, area.viewport.width),
2615 (refused.1, true, area.viewport.height),
2616 ] {
2617 if refused.abs() < 1e-3 {
2618 continue;
2619 }
2620 let edge = area.refused_edge(vertical, refused);
2621 let (offset, cross) =
2622 frus_widgets::glow_cross_axis(area.viewport, edge, cursor);
2623 self.runtime.glow_pull(
2624 id,
2625 area.overscroll,
2626 edge,
2627 refused,
2628 extent,
2629 offset,
2630 cross,
2631 );
2632 self.runtime.glow_scroll_end(id);
2636 }
2637 self.request_redraw();
2638 }
2639 }
2640
2641 WindowEvent::RedrawRequested => {
2642 if std::mem::take(&mut self.renderer_lost) {
2644 self.rebuild_renderer();
2645 }
2646 let measuring = self.frame_stats.enabled();
2648 let frame_start = measuring.then(Instant::now);
2649 let timed = measuring || crate::startup::pending();
2652 self.frame_costs = crate::frame_stats::FrameCosts::default();
2653 #[cfg(web)]
2656 if self.renderer.is_none() {
2657 match self.pending_renderer.borrow_mut().take() {
2658 Some(mut renderer) => {
2659 if let Some(window) = &self.window {
2667 let size = window.inner_size();
2668 renderer.resize(size.width.max(1), size.height.max(1));
2669 }
2670 self.renderer = Some(renderer);
2671 }
2672 None => return,
2673 }
2674 }
2675 while let Some(pasted) = self.clipboard.take_answered() {
2679 self.land_paste(pasted);
2680 }
2681 self.report_focus();
2684 if self
2687 .runtime
2688 .selection_toolbar
2689 .is_some_and(|mark| self.runtime.input.focused != Some(mark.id))
2690 {
2691 self.runtime.selection_toolbar = None;
2692 }
2693 if self.occluded {
2695 return;
2696 }
2697 let size = self
2698 .window
2699 .as_ref()
2700 .map(|w| w.inner_size())
2701 .unwrap_or_default();
2702 if size.width == 0 || size.height == 0 {
2704 self.last_frame = None; return;
2706 }
2707 let scale = self.total_scale();
2710 let (width, height) = self.logical_size(size.width, size.height);
2711
2712 if self.last_size != Some((width, height)) {
2715 self.last_size = Some((width, height));
2716 self.build_dirty = true;
2717 self.app.on_resize(width, height);
2718 }
2719
2720 let raw = self.compute_insets(size.width, size.height, scale);
2727 let phys = (size.width, size.height);
2728 let mut baseline = match self.inset_baseline {
2729 Some((b, s)) if s == phys => b,
2730 _ => {
2731 self.inset_baseline = Some((raw, phys));
2732 raw
2733 }
2734 };
2735 if raw.bottom < baseline.bottom {
2736 baseline = raw;
2737 self.inset_baseline = Some((raw, phys));
2738 }
2739 let insets = match &self.simulated {
2740 Some(simulated) => simulated.insets(),
2742 None => WindowInsets::from_baseline(baseline, raw),
2743 };
2744 if self.last_insets != insets {
2745 if insets.view_insets.bottom > self.last_insets.view_insets.bottom {
2746 self.reveal_after_keyboard = true;
2747 }
2748 self.last_insets = insets;
2749 self.build_dirty = true;
2750 self.app.on_insets(insets);
2751 }
2752
2753 let now = Instant::now();
2755 let dt = self
2756 .last_frame
2757 .map(|prev| (now - prev).as_secs_f32().min(0.05))
2758 .unwrap_or(0.0);
2759 self.last_frame = Some(now);
2760
2761 self.elapsed += dt;
2763 self.runtime.time = self.elapsed;
2764
2765 let caret_now = Instant::now();
2770 let caret = if self.lifecycle == Lifecycle::Resumed {
2771 self.caret_signature()
2772 } else {
2773 None
2774 };
2775 self.caret.observe(caret_now, caret);
2776 self.runtime.caret_hidden = self.caret.hidden(caret_now);
2777
2778 let mut app_animating = self.app.tick(dt);
2780
2781 let settings = self
2790 .platform
2791 .accessibility
2792 .with_overrides(self.app.accessibility());
2793
2794 install_ambient(&self.app, &self.platform.locales);
2804
2805 let theme_moved = self.themes.advance(
2806 &self.app,
2807 self.brightness(),
2808 settings.high_contrast,
2809 settings.disable_animations,
2810 dt,
2811 );
2812 app_animating |= theme_moved | self.themes.animating();
2815 let theme = self.themes.displayed(&self.app);
2816
2817 let _surface = self.media_query(width, height).install();
2830
2831 self.runtime.still = settings.disable_animations;
2842 self.runtime.scrollbar_hovered = self
2850 .ui
2851 .as_ref()
2852 .and_then(|ui| ui.scrollbar_near(self.cursor))
2853 .map(|bar| bar.id);
2854 self.runtime.scrollbar_dragged = match self.drag {
2855 Some(Drag::Scrollbar { id, .. }) => Some(id),
2856 _ => None,
2857 };
2858 let asked = self.app.effects();
2865 self.run_command(asked);
2866 self.take_dev_commands();
2867 #[cfg(web)]
2868 self.sync_address();
2869 #[cfg(any(desktop, android))]
2870 self.report_dev();
2871 #[cfg(desktop)]
2872 self.open_links_from_other_instances();
2873 let arrived = self.take_task_arrivals();
2875 #[cfg(feature = "hot-reload")]
2877 let arrived = crate::hot::take_patched() || arrived;
2878 if frus_widgets::take_rebuild_request() || arrived {
2879 self.build_dirty = true;
2880 }
2881 let need_build = frame_needs_build(
2882 self.build_dirty,
2883 app_animating,
2884 std::mem::replace(&mut self.app_was_animating, app_animating),
2885 self.tree.is_none() || self.runtime.switching(),
2886 );
2887 if need_build {
2888 let build_start = timed.then(Instant::now);
2896 let tree = build_view(&self.app, &theme, &self.runtime);
2897 if let Some(at) = build_start {
2898 let took = at.elapsed();
2899 self.frame_costs.build = Some(took.as_secs_f32() * 1000.0);
2900 crate::startup::built(took);
2901 }
2902 let ids = collect_ids(tree.as_ref());
2903 let present: std::collections::HashSet<_> = ids.iter().copied().collect();
2904
2905 let leaving: std::collections::HashSet<u64> = self
2908 .runtime
2909 .mounted
2910 .iter()
2911 .filter(|id| !present.contains(id))
2912 .map(|id| id.as_u64())
2913 .collect();
2914 if !leaving.is_empty() {
2915 if let Some(ui) = &self.ui {
2916 let captured: Vec<_> = ui
2917 .scene()
2918 .primitives()
2919 .iter()
2920 .filter(|p| leaving.contains(&p.owner()))
2921 .cloned()
2922 .collect();
2923 if !captured.is_empty() {
2924 self.runtime
2925 .leaving
2926 .insert(self.leaving_counter, (captured, 1.0));
2927 self.leaving_counter = self.leaving_counter.wrapping_add(1);
2928 }
2929 }
2930 }
2931
2932 for &id in &ids {
2934 if self.runtime.mounted.insert(id) {
2935 self.runtime.anims.entry(id).or_default().opacity = 0.0;
2936 }
2937 }
2938 self.runtime.mounted.retain(|id| present.contains(id));
2939
2940 self.tree = Some(tree);
2941 self.sync_subscriptions();
2946
2947 self.runtime.paint_cache.borrow_mut().bump_generation();
2952 }
2953 self.build_dirty = false;
2954
2955 if !self.pending_focus.is_empty() {
2960 let keys = std::mem::take(&mut self.pending_focus);
2961 let ids: Vec<WidgetId> = self
2962 .tree
2963 .as_deref()
2964 .map(|tree| keys.iter().filter_map(|&k| find_by_key(tree, k)).collect())
2965 .unwrap_or_default();
2966 if let Some(&id) = ids.last() {
2967 self.runtime.input.focused = Some(id);
2968 self.runtime.focus_visible = true;
2969 }
2970 }
2971
2972 let autoscrolling = self.autoscroll_carried(dt);
2981
2982 let reorder_animating = self.advance_reorder_springs(dt);
2993
2994 let tree = self
2997 .tree
2998 .as_deref()
2999 .expect("the view was built at least once");
3000
3001 let scroll_regions = self
3004 .ui
3005 .as_ref()
3006 .map(|ui| ui.scroll_regions().to_vec())
3007 .unwrap_or_default();
3008 let refresh_areas = self
3011 .ui
3012 .as_ref()
3013 .map(|ui| ui.refresh_areas().to_vec())
3014 .unwrap_or_default();
3015 let dismissables = self
3016 .ui
3017 .as_ref()
3018 .map(|ui| ui.dismissables().to_vec())
3019 .unwrap_or_default();
3020 let mut dismissed: Vec<A::Message> = Vec::new();
3023 let sheet_areas = self
3024 .ui
3025 .as_ref()
3026 .map(|ui| ui.sheets().to_vec())
3027 .unwrap_or_default();
3028 let interactive_bounds = self
3029 .ui
3030 .as_ref()
3031 .map(|ui| ui.interactive_bounds())
3032 .unwrap_or_default();
3033
3034 self.runtime.sync_pages(&scroll_regions);
3038 self.runtime.sync_visible(&scroll_regions);
3042 let requests = std::mem::take(&mut self.pending_scroll);
3047 let (retry, scrolled) =
3048 apply_scroll_requests(&mut self.runtime, tree, requests, &scroll_regions);
3049 self.retry_scroll = retry;
3050 let requests = std::mem::take(&mut self.pending_sheet);
3053 let (retry, sheeted) =
3054 apply_sheet_requests(&mut self.runtime, tree, requests, &sheet_areas);
3055 self.retry_sheet = retry;
3056
3057 let moving: [(&str, bool); 25] = [
3061 ("scrolled", scrolled),
3062 ("sheeted", sheeted),
3063 ("anims", self.runtime.advance(dt)),
3064 ("leaving", self.runtime.advance_leaving(dt)),
3065 ("switchers", self.runtime.advance_switchers(tree, dt)),
3066 ("values", self.runtime.advance_values(tree, dt)),
3067 ("colors", self.runtime.advance_colors(tree, dt)),
3068 ("sizes", self.runtime.advance_sizes(tree, dt)),
3069 ("radii", self.runtime.advance_radii(tree, dt)),
3070 ("paddings", self.runtime.advance_paddings(tree, dt)),
3071 ("offsets", self.runtime.advance_offsets(tree, dt)),
3072 ("pins", self.runtime.advance_pins(tree, dt)),
3073 ("fractions", self.runtime.advance_fractions(tree, dt)),
3074 ("text_styles", self.runtime.advance_text_styles(tree, dt)),
3075 ("transforms", self.runtime.advance_transforms(tree, dt)),
3076 ("scroll", self.runtime.advance_scroll(&scroll_regions, dt)),
3077 ("glow", self.runtime.advance_glow(dt)),
3078 ("refresh", self.runtime.advance_refresh(&refresh_areas, dt)),
3079 ("dismiss", {
3080 let (moving, done) = self.runtime.advance_dismiss(&dismissables, dt);
3085 dismissed.extend(done.into_iter().filter_map(|(id, direction)| {
3086 find_widget(tree, id).and_then(|widget| widget.on_dismissed(direction))
3087 }));
3088 moving
3089 }),
3090 ("sheets", {
3091 let (moving, closed) = self.runtime.advance_sheets(&sheet_areas, dt);
3094 dismissed.extend(closed.into_iter().filter_map(|id| {
3095 find_widget(tree, id).and_then(|widget| widget.on_sheet_dismissed())
3096 }));
3097 let mut handed = false;
3100 for (list, velocity) in self.runtime.take_sheet_handovers() {
3101 if let Some(area) = scroll_regions.iter().find(|a| a.id == list) {
3102 handed |=
3103 self.runtime
3104 .fling_scroll(*area, area.physics, (0.0, velocity));
3105 }
3106 }
3107 moving | handed
3108 }),
3109 (
3110 "interactive",
3111 self.runtime.advance_interactive(&interactive_bounds, dt),
3112 ),
3113 ("ink", self.runtime.advance_ink(dt)),
3114 ("reorder", reorder_animating),
3115 ("autoscroll", autoscrolling),
3116 ("app", app_animating),
3117 ];
3118 let animating = moving.iter().any(|(_, on)| *on);
3119 self.frame_stats.note_moving(&moving);
3120 let layout_start = timed.then(Instant::now);
3124 let inspected = self.inspector
3127 || self.debug.outlines
3128 || self.inspector_dump
3129 || self.debug.focus_dump;
3130 let (ui, scene) = if inspected {
3131 let (ui, nodes) = frus_widgets::build_ui_inspected(
3132 tree,
3133 Size::new(width, height),
3134 &self.runtime,
3135 &theme,
3136 );
3137 if std::mem::take(&mut self.inspector_dump) {
3138 log::info!(target: "frus::inspector", "
3140{}", frus_widgets::dump_tree(&nodes));
3141 }
3142 if std::mem::take(&mut self.debug.focus_dump) {
3143 let focusable: Vec<WidgetId> = ui.focusable_ids().collect();
3144 log::info!(target: "frus::inspector", "focus, in tab order:
3145{}", dump_focus(&nodes, &focusable, self.runtime.input.focused));
3146 }
3147 let mut scene = if self.inspector {
3148 on_top_of(ui.scene())
3151 } else {
3152 ui.scene().clone()
3153 };
3154 if self.inspector || self.debug.outlines {
3155 frus_widgets::paint_inspector_overlay(
3156 &nodes,
3157 self.inspector.then_some(self.cursor),
3158 Size::new(width, height),
3159 &theme,
3160 &mut scene,
3161 );
3162 }
3163 (ui, scene.scaled(scale))
3165 } else {
3166 let ui = build_ui(tree, Size::new(width, height), &self.runtime, &theme);
3167 let scene = if matches!(self.drag, Some(Drag::Reorder { moved: true, .. })) {
3170 let mut scene = ui.scene().clone();
3171 self.paint_reorder_preview(&ui, &theme, &mut scene);
3172 scene.scaled(scale)
3173 } else if matches!(self.drag, Some(Drag::Item { moved: true, .. })) {
3174 let mut scene = ui.scene().clone();
3175 self.paint_drag_ghost(&ui, &theme, &mut scene);
3176 scene.scaled(scale)
3177 } else {
3178 ui.scene().scaled(scale)
3180 };
3181 (ui, scene)
3182 };
3183 let layout_took = layout_start.map(|at| at.elapsed());
3184 if let Some(took) = layout_took {
3185 self.frame_costs.layout_paint = took.as_secs_f32() * 1000.0;
3186 self.frame_costs.layers = count_layers(scene.primitives());
3187 }
3188 self.report_overflows(&ui);
3189 self.runtime.states.end_frame();
3193 crate::startup::laid_out(layout_took);
3194 #[cfg(web)]
3195 crate::web_dev::keep(|| self.runtime.states.snapshot());
3196 if frus_widgets::rebuild_requested() {
3197 self.request_redraw();
3198 }
3199 let scene = if self.renderer.as_ref().is_some_and(Renderer::sees_through) {
3202 backdrop_under(scene, width * scale, height * scale, self.system_line())
3203 } else {
3204 scene
3205 };
3206 if std::mem::take(&mut self.debug.semantics_dump) {
3208 log::info!(target: "frus::inspector", "semantics, in reading order:
3209{}", dump_semantics(ui.semantics()));
3210 }
3211 let scene = if self.debug.performance {
3212 let mut scene = on_top_of(&scene);
3216 let costs = self.debug.last_costs.as_ref().map(|(costs, _)| costs);
3217 let top = self.last_insets.padding.top * scale;
3219 paint_performance(
3220 &mut scene,
3221 costs,
3222 &self.debug.history,
3223 self.debug.fps,
3224 width * scale,
3225 top,
3226 scale,
3227 );
3228 scene
3229 } else {
3230 scene
3231 };
3232 let (scene, shot) = match (&self.simulated, self.placement()) {
3235 (Some(simulated), Some(placement)) => {
3236 let edges = self.last_insets.padding;
3237 let icons = ui
3240 .system_ui_style(
3241 Point::new(width / 2.0, edges.top + 0.5),
3242 Point::new(width / 2.0, height - edges.bottom - 0.5),
3243 )
3244 .resolve(&theme)
3245 .status_bar_icons;
3246 let window = Size::new(
3247 size.width as f32 / self.scale.max(0.1),
3248 size.height as f32 / self.scale.max(0.1),
3249 );
3250 let framed = simulated.paint(
3251 &scene,
3252 placement,
3253 self.scale,
3254 window,
3255 icons,
3256 self.brightness(),
3257 );
3258 (framed, (size.width as f32, size.height as f32))
3259 }
3260 _ => (scene, (width * scale, height * scale)),
3261 };
3262 #[cfg(desktop)]
3263 if let Some(path) = self.debug.screenshot.take() {
3264 write_screenshot(&scene, shot.0, shot.1, &path);
3265 }
3266 #[cfg(not(desktop))]
3267 let _ = shot;
3268 let mut device_lost = false;
3269 if let Some(renderer) = self.renderer.as_mut() {
3270 let render_start = timed.then(Instant::now);
3271 match renderer.render(&scene) {
3272 frus_gpu::RenderOutcome::Presented => {
3273 crate::startup::presented(render_start.map(|at| at.elapsed()))
3274 }
3275 frus_gpu::RenderOutcome::NeedsReconfigure => {
3276 renderer.reconfigure();
3277 }
3278 frus_gpu::RenderOutcome::DeviceLost => device_lost = true,
3279 frus_gpu::RenderOutcome::Skipped => {}
3280 }
3281 #[cfg(web)]
3284 if std::mem::take(&mut self.debug.page_screenshot) {
3285 if let Some(window) = self.window.as_ref() {
3286 crate::web_dev::send_screenshot(window);
3287 }
3288 }
3289 if let Some(at) = render_start {
3290 self.frame_costs.render = at.elapsed().as_secs_f32() * 1000.0;
3291 let timings = renderer.last_timings();
3292 self.frame_costs.acquire = timings.acquire;
3293 self.frame_costs.draw = timings.draw;
3294 self.frame_costs.gpu = timings.gpu;
3295 }
3296 }
3297 if device_lost {
3300 self.renderer_lost = true;
3301 if let Some(window) = self.window.as_ref() {
3302 window.request_redraw();
3303 }
3304 }
3305 let widget_asks = ui.wants_animation();
3315 let loading =
3316 frus_widgets::images_in_flight() > 0 || frus_widgets::tasks_in_flight() > 0;
3317 let wants_animation = widget_asks || loading;
3318
3319 self.ui = Some(ui);
3321 let revealed = std::mem::take(&mut self.reveal_after_keyboard) && {
3327 let moves = match (self.runtime.input.focused, self.ui.as_ref()) {
3328 (Some(id), Some(ui)) => ui.reveal(id, &self.runtime),
3329 _ => Vec::new(),
3330 };
3331 let moved = !moves.is_empty();
3332 for (area, offset) in moves {
3333 self.runtime.scroll_ballistic.remove(&area);
3334 self.runtime.scroll_velocity.remove(&area);
3335 self.runtime.scroll_target.insert(area, offset);
3336 }
3337 moved
3338 };
3339 let rehovered = self.drag.is_none() && {
3349 let hovered = self.ui.as_ref().and_then(|ui| self.hover.target(ui));
3350 let changed = hovered != self.runtime.input.hovered;
3351 self.runtime.input.hovered = hovered;
3352 changed
3353 };
3354 if rehovered {
3355 self.build_dirty = true;
3356 }
3357 let wants_animation = wants_animation || rehovered || revealed;
3358
3359 let paged = self
3363 .ui
3364 .as_ref()
3365 .map(|ui| ui.scroll_regions().to_vec())
3366 .unwrap_or_default();
3367 self.runtime.keep_scroll_in_range(&paged);
3372 let turned: Vec<A::Message> = self
3373 .runtime
3374 .page_changes(&paged)
3375 .into_iter()
3376 .filter_map(|(id, page)| {
3377 find_widget(tree, id).and_then(|widget| widget.on_page_changed(page))
3378 })
3379 .collect();
3380
3381 let (_, moved_late) = apply_scroll_requests(
3385 &mut self.runtime,
3386 tree,
3387 std::mem::take(&mut self.retry_scroll),
3388 &paged,
3389 );
3390 let (_, sheeted_late) = {
3393 let sheets = self
3394 .ui
3395 .as_ref()
3396 .map(|ui| ui.sheets().to_vec())
3397 .unwrap_or_default();
3398 apply_sheet_requests(
3399 &mut self.runtime,
3400 tree,
3401 std::mem::take(&mut self.retry_sheet),
3402 &sheets,
3403 )
3404 };
3405
3406 let scrolled: Vec<A::Message> = {
3411 let grain =
3412 |id| find_widget(tree, id).map_or(0.0, |widget| widget.scroll_grain());
3413 self.runtime
3414 .scroll_changes(&paged, grain)
3415 .into_iter()
3416 .filter_map(|(id, position)| {
3417 find_widget(tree, id).and_then(|widget| widget.on_scroll(position))
3418 })
3419 .collect()
3420 };
3421
3422 for message in dismissed.into_iter().chain(turned).chain(scrolled) {
3426 self.dispatch(message);
3427 }
3428 self.sync_regions(false);
3431 if moved_late || sheeted_late {
3432 self.request_redraw();
3435 }
3436
3437 self.reconcile_focus();
3441
3442 #[cfg(desktop)]
3446 let (scale, origin) = (
3447 f64::from(self.total_scale()),
3448 self.placement().map_or((0.0, 0.0), |at| {
3449 let os = f64::from(self.scale);
3450 (f64::from(at.screen.x) * os, f64::from(at.screen.y) * os)
3451 }),
3452 );
3453 #[cfg(desktop)]
3454 if let Some(a11y) = self.a11y.as_mut() {
3455 let focus = self.runtime.input.focused;
3456 let title = self.app.title();
3457 if let Some(ui) = self.ui.as_ref() {
3458 a11y.update(ui.semantics(), focus, &title, &self.announce, scale, origin);
3459 }
3460 }
3461 #[cfg(web)]
3464 self.sync_web_editing();
3465 #[cfg(web)]
3468 if let Some(a11y) = self.a11y.as_mut() {
3469 let focus = self.runtime.input.focused;
3470 let editing = self.web_editor.as_ref().and_then(|editor| editor.editing());
3471 let title = self.app.title();
3472 if let Some(ui) = self.ui.as_ref() {
3473 a11y.update(ui.semantics(), focus, editing, &title, &self.announce);
3474 }
3475 }
3476
3477 self.sync_system_bars(&theme);
3479
3480 self.sync_soft_input();
3482 #[cfg(android)]
3484 self.sync_autofill();
3485
3486 if animating || wants_animation {
3488 self.request_redraw();
3489 }
3490 if let Some(start) = frame_start {
3491 use crate::frame_stats::Why;
3492 let why = [
3493 (app_animating, Why::APP),
3494 (animating, Why::RUNTIME),
3495 (widget_asks, Why::WIDGET),
3496 (loading, Why::LOADING),
3497 (rehovered || revealed, Why::POINTER),
3498 ];
3499 self.frame_costs.why = why
3500 .into_iter()
3501 .filter(|(on, _)| *on)
3502 .fold(Why::NONE, |all, (_, why)| all | why);
3503 self.frame_stats.frame(start, self.frame_costs);
3504 if self.debug.performance {
3505 if let Some((_, before)) = self.debug.last_costs {
3506 let gap = start.saturating_duration_since(before).as_secs_f32();
3507 if gap > 0.0 {
3508 self.debug.fps = 1.0 / gap;
3509 }
3510 }
3511 if self.debug.last_costs.is_none() {
3513 self.request_redraw();
3514 }
3515 self.debug.last_costs = Some((self.frame_costs, start));
3516 self.debug.history.push(&self.frame_costs);
3517 }
3518 }
3519 #[cfg(windows)]
3521 self.sync_title_bar();
3522 }
3523
3524 _ => {}
3525 }
3526 }
3527}
3528
3529#[derive(Default)]
3531struct DebugAsks {
3532 outlines: bool,
3534 focus_dump: bool,
3536 semantics_dump: bool,
3538 brightness: Option<frus_widgets::Brightness>,
3540 text_scale: Option<f32>,
3542 performance: bool,
3544 last_costs: Option<(crate::frame_stats::FrameCosts, Instant)>,
3546 history: FrameHistory,
3548 fps: f32,
3550 #[cfg_attr(not(desktop), allow(dead_code))]
3552 screenshot: Option<std::path::PathBuf>,
3553 #[cfg_attr(not(web), allow(dead_code))]
3555 page_screenshot: bool,
3556}
3557
3558const PLATFORM_CYCLE: [frus_widgets::TargetPlatform; 6] = [
3560 frus_widgets::TargetPlatform::Android,
3561 frus_widgets::TargetPlatform::Ios,
3562 frus_widgets::TargetPlatform::Windows,
3563 frus_widgets::TargetPlatform::MacOs,
3564 frus_widgets::TargetPlatform::Linux,
3565 frus_widgets::TargetPlatform::Fuchsia,
3566];
3567
3568const TEXT_SCALE_CYCLE: [f32; 4] = [1.0, 1.3, 2.0, 0.85];
3571
3572fn next_text_scale(current: f32) -> f32 {
3574 TEXT_SCALE_CYCLE
3575 .iter()
3576 .position(|s| (s - current).abs() < 1e-3)
3577 .map_or(1.0, |i| TEXT_SCALE_CYCLE[(i + 1) % TEXT_SCALE_CYCLE.len()])
3578}
3579
3580fn next_platform(current: frus_widgets::TargetPlatform) -> frus_widgets::TargetPlatform {
3582 let at = PLATFORM_CYCLE
3583 .iter()
3584 .position(|p| *p == current)
3585 .unwrap_or(0);
3586 PLATFORM_CYCLE[(at + 1) % PLATFORM_CYCLE.len()]
3587}
3588
3589fn dump_focus(
3591 nodes: &[frus_widgets::InspectorNode],
3592 focusable: &[WidgetId],
3593 focused: Option<WidgetId>,
3594) -> String {
3595 let mut out = String::new();
3596 for id in focusable {
3597 let node = nodes.iter().rev().find(|n| n.id == *id);
3598 let mark = if Some(*id) == focused { "▶ " } else { " " };
3599 match node {
3600 Some(n) => out.push_str(&format!(
3601 "{mark}{} {:.0},{:.0} {:.0}×{:.0} #{:08x}\n",
3602 n.name,
3603 n.rect.x,
3604 n.rect.y,
3605 n.rect.width,
3606 n.rect.height,
3607 id.as_u64() as u32
3608 )),
3609 None => out.push_str(&format!("{mark}#{:08x}\n", id.as_u64() as u32)),
3610 }
3611 }
3612 if out.is_empty() {
3613 out.push_str(" (nothing focusable)\n");
3614 }
3615 out
3616}
3617
3618fn dump_semantics(nodes: &[(WidgetId, Rect, frus_widgets::SemanticsProperties)]) -> String {
3620 let mut out = String::new();
3621 for (_, rect, props) in nodes {
3622 out.push_str(&format!(" {:?}", props.role));
3623 if let Some(label) = &props.label {
3624 out.push_str(&format!(" \"{label}\""));
3625 }
3626 if let Some(value) = &props.value {
3627 out.push_str(&format!(" = {value}"));
3628 }
3629 if props.clickable {
3630 out.push_str(" clickable");
3631 }
3632 if props.disabled {
3633 out.push_str(" disabled");
3634 }
3635 out.push_str(&format!(
3636 " {:.0},{:.0} {:.0}×{:.0}\n",
3637 rect.x, rect.y, rect.width, rect.height
3638 ));
3639 }
3640 if out.is_empty() {
3641 out.push_str(" (empty)\n");
3642 }
3643 out
3644}
3645
3646#[allow(clippy::too_many_arguments)]
3649fn paint_performance(
3650 scene: &mut frus_widgets::Scene,
3651 costs: Option<&crate::frame_stats::FrameCosts>,
3652 history: &FrameHistory,
3653 fps: f32,
3654 width: f32,
3655 top: f32,
3656 scale: f32,
3657) {
3658 let lines = match costs {
3659 Some(costs) => performance_lines(costs, fps),
3660 None => vec!["measuring…".to_string()],
3661 };
3662 paint_panel(scene, lines, history, width, top, scale);
3663}
3664
3665const HISTORY: usize = 90;
3667const BUDGET_MS: f32 = 1000.0 / 60.0;
3669const GRAPH_MS: f32 = BUDGET_MS * 2.0;
3671
3672#[derive(Default)]
3675struct FrameHistory {
3676 ui: std::collections::VecDeque<f32>,
3677 render: std::collections::VecDeque<f32>,
3678}
3679
3680impl FrameHistory {
3681 fn push(&mut self, costs: &crate::frame_stats::FrameCosts) {
3682 let ui = costs.build.unwrap_or(0.0) + costs.layout_paint;
3683 for (list, value) in [(&mut self.ui, ui), (&mut self.render, costs.render)] {
3684 if list.len() == HISTORY {
3685 list.pop_front();
3686 }
3687 list.push_back(value);
3688 }
3689 }
3690
3691 fn bars(values: &std::collections::VecDeque<f32>, height: f32) -> Vec<(f32, bool)> {
3694 values
3695 .iter()
3696 .map(|ms| ((ms / GRAPH_MS).min(1.0) * height, *ms > BUDGET_MS))
3697 .collect()
3698 }
3699}
3700
3701fn on_top_of(scene: &Scene) -> Scene {
3705 let mut over = Scene::new();
3706 over.layer(1.0, |inner| {
3707 for primitive in scene.primitives() {
3708 inner.push_primitive(primitive.clone());
3709 }
3710 });
3711 over
3712}
3713
3714fn performance_lines(costs: &crate::frame_stats::FrameCosts, fps: f32) -> Vec<String> {
3716 let build = costs
3717 .build
3718 .map_or_else(|| "kept".to_string(), |ms| format!("{ms:.1} ms"));
3719 let mut lines = vec![
3720 if fps > 0.0 {
3722 format!("{fps:.0} fps")
3723 } else {
3724 "idle".to_string()
3725 },
3726 format!("build {build} layout+paint {:.1} ms", costs.layout_paint),
3727 format!(
3728 "render {:.1} ms (acquire {:.1}, draw {:.1})",
3729 costs.render, costs.acquire, costs.draw
3730 ),
3731 ];
3732 if let Some(gpu) = costs.gpu {
3733 lines.push(format!("GPU {gpu:.1} ms"));
3734 }
3735 lines
3736}
3737
3738fn paint_panel(
3741 scene: &mut frus_widgets::Scene,
3742 lines: Vec<String>,
3743 history: &FrameHistory,
3744 width: f32,
3745 top: f32,
3746 scale: f32,
3747) {
3748 let size = 12.0 * scale;
3749 let style = frus_widgets::TextStyle::new(size).resolved();
3750 let pad = 6.0 * scale;
3751 let line_h = size * 1.4;
3752 let longest = lines
3755 .iter()
3756 .map(|line| line.chars().count())
3757 .max()
3758 .unwrap_or(0);
3759 let bar_w = 2.0 * scale;
3761 let graph_w = HISTORY as f32 * bar_w;
3762 let graph_h = 36.0 * scale;
3763 let graphs_h = if history.ui.is_empty() {
3764 0.0
3765 } else {
3766 2.0 * (graph_h + line_h) + pad
3767 };
3768 let text_w = (longest as f32 * size * 0.6).max(graph_w);
3769 let panel = Rect::new(
3770 (width - text_w - pad * 3.0).max(0.0),
3771 top + pad,
3772 text_w + pad * 2.0,
3773 line_h * lines.len() as f32 + pad * 2.0 + graphs_h,
3774 );
3775 scene.set_clip(Rect::UNBOUNDED);
3776 scene.set_owner(0);
3777 scene.draw_rect(
3779 panel,
3780 frus_widgets::Color::rgb(0.05, 0.05, 0.05),
3781 4.0 * scale,
3782 0.0,
3783 frus_widgets::Color::TRANSPARENT,
3784 );
3785 let count = lines.len();
3786 for (i, line) in lines.into_iter().enumerate() {
3787 scene.text(
3788 frus_widgets::Point::new(panel.x + pad, panel.y + pad + line_h * i as f32),
3789 line,
3790 &style,
3791 frus_widgets::Color::rgb(0.55, 1.0, 0.55),
3792 );
3793 }
3794 if history.ui.is_empty() {
3795 return;
3796 }
3797 let mut y = panel.y + pad + line_h * count as f32 + pad;
3798 for (title, values) in [
3799 ("interface (build, layout, paint)", &history.ui),
3800 ("drawing (encode, submit, present)", &history.render),
3801 ] {
3802 scene.text(
3803 frus_widgets::Point::new(panel.x + pad, y),
3804 title.to_string(),
3805 &style,
3806 frus_widgets::Color::rgb(0.75, 0.75, 0.75),
3807 );
3808 y += line_h;
3809 let area = Rect::new(panel.x + pad, y, graph_w, graph_h);
3810 scene.draw_rect(
3811 area,
3812 frus_widgets::Color::rgb(0.12, 0.12, 0.12),
3813 0.0,
3814 0.0,
3815 frus_widgets::Color::TRANSPARENT,
3816 );
3817 let bars = FrameHistory::bars(values, graph_h);
3819 let start = area.x + area.width - bars.len() as f32 * bar_w;
3820 for (i, (h, over)) in bars.into_iter().enumerate() {
3821 if h <= 0.0 {
3822 continue;
3823 }
3824 let color = if over {
3825 frus_widgets::Color::rgb(1.0, 0.35, 0.3)
3826 } else {
3827 frus_widgets::Color::rgb(0.4, 0.85, 0.45)
3828 };
3829 scene.fill_rect(
3830 Rect::new(
3831 start + i as f32 * bar_w,
3832 area.y + area.height - h,
3833 bar_w * 0.8,
3834 h,
3835 ),
3836 color,
3837 );
3838 }
3839 let budget_y = area.y + area.height * (1.0 - BUDGET_MS / GRAPH_MS);
3841 scene.fill_rect(
3842 Rect::new(area.x, budget_y, area.width, scale.max(1.0)),
3843 frus_widgets::Color::rgb(0.95, 0.95, 0.95),
3844 );
3845 y += graph_h + pad / 2.0;
3846 }
3847}
3848
3849#[cfg(desktop)]
3852fn write_screenshot(scene: &Scene, width: f32, height: f32, path: &std::path::Path) {
3853 let (w, h) = (
3854 width.round().max(1.0) as u32,
3855 height.round().max(1.0) as u32,
3856 );
3857 let Some(frame) = frus_gpu::render_offscreen(scene, w, h, frus_gpu::backdrop()) else {
3858 log::warn!(target: "frus::dev", "screenshot: no GPU to draw it with");
3859 return;
3860 };
3861 let png = crate::dev_commands::png(frame.width, frame.height, &frame.rgba);
3862 match std::fs::write(path, png) {
3863 Ok(()) => log::debug!(
3864 target: "frus::dev",
3865 "screenshot {}×{} written to {}",
3866 frame.width,
3867 frame.height,
3868 path.display()
3869 ),
3870 Err(error) => log::warn!(target: "frus::dev", "screenshot: {}: {error}", path.display()),
3871 }
3872}
3873
3874impl<A: Application> App<A> {
3875 fn rebuild_renderer(&mut self) {
3881 let see_through = self.renderer.as_ref().is_some_and(Renderer::sees_through);
3882 self.renderer = None;
3884 let Some(window) = self.window.clone() else {
3885 return;
3886 };
3887 let size = window.inner_size();
3888 let (width, height) = (size.width.max(1), size.height.max(1));
3889 log::warn!("the GPU device was lost: drawing again with a new one");
3890 #[cfg(not(web))]
3891 {
3892 let made = if see_through {
3893 pollster::block_on(Renderer::see_through(window.clone(), width, height))
3894 } else {
3895 pollster::block_on(Renderer::new(window.clone(), width, height))
3896 };
3897 match made {
3898 Ok(renderer) => self.renderer = Some(renderer),
3899 Err(err) => {
3900 log::error!("the GPU device was lost, and no new one could be made: {err:#}")
3901 }
3902 }
3903 }
3904 #[cfg(web)]
3905 {
3906 let _ = see_through;
3907 let slot = self.pending_renderer.clone();
3908 let win = window.clone();
3909 wasm_bindgen_futures::spawn_local(async move {
3910 match Renderer::new(win.clone(), width, height).await {
3911 Ok(renderer) => {
3912 *slot.borrow_mut() = Some(renderer);
3913 win.request_redraw();
3914 }
3915 Err(err) => {
3916 log::error!(
3917 "the GPU device was lost, and no new one could be made: {err:#}"
3918 )
3919 }
3920 }
3921 });
3922 }
3923 self.build_dirty = true;
3924 window.request_redraw();
3925 }
3926
3927 fn take_dev_commands(&mut self) {
3928 for command in crate::dev_commands::take_commands() {
3929 let (name, argument) = command
3930 .split_once(' ')
3931 .map_or((command.as_str(), ""), |(n, a)| (n, a.trim()));
3932 let on_off = |on: bool| if on { "on" } else { "off" };
3933 match name {
3934 "inspector" => {
3935 self.inspector = !self.inspector;
3936 self.inspector_dump = self.inspector;
3937 log::info!(target: "frus::dev", "inspector {}", on_off(self.inspector));
3938 }
3939 "outlines" => {
3940 self.debug.outlines = !self.debug.outlines;
3941 log::info!(target: "frus::dev", "debug paint {}", on_off(self.debug.outlines));
3942 }
3943 "widgets" => self.inspector_dump = true,
3944 "focus" => self.debug.focus_dump = true,
3945 "semantics" => self.debug.semantics_dump = true,
3946 "platform" => {
3947 let next = next_platform(frus_widgets::default_target_platform());
3948 if cfg!(debug_assertions) {
3949 frus_widgets::set_debug_default_target_platform_override(Some(next));
3950 log::info!(target: "frus::dev", "target platform: {next:?}");
3951 } else {
3952 log::warn!(target: "frus::dev", "the platform can be switched in a debug build only");
3953 }
3954 }
3955 "brightness" => {
3956 let now = self.debug.brightness.unwrap_or(self.platform.brightness);
3957 let next = match now {
3958 frus_widgets::Brightness::Light => frus_widgets::Brightness::Dark,
3959 frus_widgets::Brightness::Dark => frus_widgets::Brightness::Light,
3960 };
3961 self.debug.brightness = Some(next);
3962 log::info!(
3963 target: "frus::dev",
3964 "brightness: {next:?} (an application without a theme for it stays as it is)"
3965 );
3966 }
3967 "performance" => {
3968 self.debug.performance = !self.debug.performance;
3969 self.debug.last_costs = None;
3970 self.debug.history = FrameHistory::default();
3971 self.frame_stats.set_enabled(self.debug.performance);
3972 self.request_redraw();
3974 log::info!(target: "frus::dev", "performance panel {}", on_off(self.debug.performance));
3975 }
3976 "text-scale" => {
3978 let now = self.debug.text_scale.unwrap_or(self.platform.text_scaler);
3979 let next = next_text_scale(now);
3980 self.debug.text_scale = Some(next);
3981 log::info!(target: "frus::dev", "text size: {:.0}%", next * 100.0);
3982 }
3983 "keyboard" => match self.simulated.as_mut() {
3986 Some(simulated) => {
3987 simulated.keyboard_held = !simulated.keyboard_held;
3988 log::info!(
3989 target: "frus::dev",
3990 "keyboard held {}",
3991 on_off(simulated.keyboard_held)
3992 );
3993 self.request_redraw();
3994 }
3995 None => log::info!(
3996 target: "frus::dev",
3997 "no keyboard to show: the window is not simulating a device (frus run -d)"
3998 ),
3999 },
4000 "rotate" => match self.simulated.as_mut() {
4002 Some(simulated) => {
4003 simulated.orientation = match simulated.orientation {
4004 frus_widgets::Orientation::Portrait => {
4005 frus_widgets::Orientation::Landscape
4006 }
4007 frus_widgets::Orientation::Landscape => {
4008 frus_widgets::Orientation::Portrait
4009 }
4010 };
4011 log::info!(
4012 target: "frus::dev",
4013 "{} {:?}",
4014 simulated.profile.name,
4015 simulated.orientation
4016 );
4017 #[cfg(desktop)]
4022 if let Some(window) = &self.window {
4023 let size = window.inner_size();
4024 let _ = window.request_inner_size(winit::dpi::PhysicalSize::new(
4025 size.height,
4026 size.width,
4027 ));
4028 }
4029 self.request_redraw();
4030 }
4031 None => log::info!(
4032 target: "frus::dev",
4033 "nothing to rotate: the window is not simulating a device (frus run -d)"
4034 ),
4035 },
4036 "trim-memory" => memory_trimmed(),
4038 "lose-gpu" => {
4040 if let Some(renderer) = self.renderer.as_ref() {
4041 log::warn!(target: "frus::dev", "losing the GPU device, as a driver reset would");
4042 renderer.lose_device_for_testing();
4043 self.request_redraw();
4044 }
4045 }
4046 "screenshot" if !argument.is_empty() => {
4047 #[cfg(desktop)]
4048 {
4049 self.debug.screenshot = Some(std::path::PathBuf::from(argument));
4050 }
4051 #[cfg(web)]
4052 {
4053 self.debug.page_screenshot = true;
4054 }
4055 #[cfg(android)]
4056 log::warn!(target: "frus::dev", "the application takes no screenshot of itself here");
4057 }
4058 _ => {
4059 log::warn!(target: "frus::dev", "frus run asked for `{command}`, which this frus does not know")
4060 }
4061 }
4062 self.build_dirty = true;
4063 }
4064 }
4065
4066 fn brightness(&self) -> frus_widgets::Brightness {
4068 self.debug.brightness.unwrap_or(self.platform.brightness)
4069 }
4070
4071 fn system_line(&self) -> f32 {
4074 match self.title_bar {
4076 Some(line) if self.title_bar_on && line.system_paints => {
4077 line.height * self.total_scale()
4078 }
4079 _ => 0.0,
4080 }
4081 }
4082
4083 fn total_scale(&self) -> f32 {
4088 if let Some(placement) = self.placement() {
4089 return (self.scale * placement.zoom).max(0.01);
4090 }
4091 (self.scale * self.app.density()).max(0.1)
4092 }
4093
4094 fn placement(&self) -> Option<crate::simulated::Placement> {
4096 let simulated = self.simulated.as_ref()?;
4097 let size = self.window.as_ref()?.inner_size();
4098 let scale = self.scale.max(0.1);
4099 Some(simulated.place(Size::new(
4100 size.width as f32 / scale,
4101 size.height as f32 / scale,
4102 )))
4103 }
4104
4105 fn to_logical(&self, x: f64, y: f64) -> Point {
4107 if let Some(placement) = self.placement() {
4108 let scale = self.scale.max(0.1);
4109 return crate::simulated::Simulated::to_screen(
4110 placement,
4111 Point::new(x as f32 / scale, y as f32 / scale),
4112 );
4113 }
4114 let scale = self.total_scale();
4115 Point::new(x as f32 / scale, y as f32 / scale)
4116 }
4117
4118 fn logical_size(&self, width: u32, height: u32) -> (f32, f32) {
4121 if let Some(simulated) = &self.simulated {
4122 let screen = simulated.screen_size();
4123 return (screen.width, screen.height);
4124 }
4125 let scale = self.total_scale();
4126 (width as f32 / scale, height as f32 / scale)
4127 }
4128
4129 fn media_query(&self, width: f32, height: f32) -> MediaQuery {
4145 let (ratio, density) = match &self.simulated {
4147 Some(simulated) => (simulated.profile.pixel_ratio, 1.0),
4148 None => (self.scale, self.app.density()),
4149 };
4150 MediaQuery::new(Size::new(width, height))
4151 .with_device_pixel_ratio(ratio)
4152 .with_density(density)
4153 .with_text_scaler(self.debug.text_scale.unwrap_or(self.platform.text_scaler))
4154 .with_platform_brightness(self.brightness())
4155 .with_accessibility(
4156 self.platform
4157 .accessibility
4158 .with_overrides(self.app.accessibility()),
4159 )
4160 .with_insets(self.last_insets)
4161 .with_title_bar(self.title_bar)
4162 }
4163
4164 #[cfg(windows)]
4168 fn sync_title_bar(&mut self) {
4169 use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
4170 let Some(window) = self.window.clone() else {
4171 return;
4172 };
4173 let hwnd = match window.window_handle().map(|h| h.as_raw()) {
4174 Ok(RawWindowHandle::Win32(win)) => {
4175 win.hwnd.get() as windows_sys::Win32::Foundation::HWND
4176 }
4177 _ => return,
4178 };
4179 let wants =
4181 self.simulated.is_none() && self.ui.as_ref().is_some_and(|ui| ui.wants_title_bar());
4182 if wants != self.title_bar_on {
4183 unsafe {
4185 if wants {
4186 crate::title_bar::enable(hwnd);
4187 let wake = window.clone();
4188 crate::title_bar::on_change(move || wake.request_redraw());
4189 } else {
4190 crate::title_bar::disable(hwnd);
4191 }
4192 }
4193 self.title_bar_on = wants;
4194 }
4195 let next = if self.title_bar_on {
4196 let scale = self.total_scale();
4197 let band = crate::title_bar::height() as f32 / scale;
4198 if let Some(ui) = &self.ui {
4200 let to_px = |r: frus_widgets::Rect| windows_sys::Win32::Foundation::RECT {
4201 left: (r.x * scale).floor() as i32,
4202 top: (r.y * scale).floor() as i32,
4203 right: ((r.x + r.width) * scale).ceil() as i32,
4204 bottom: ((r.y + r.height) * scale).ceil() as i32,
4205 };
4206 let interactive = ui
4207 .hit_rects()
4208 .filter(|r| r.y < band && r.width < self.logical_width())
4209 .map(to_px)
4210 .collect();
4211 let icon = ui
4212 .title_bar_regions()
4213 .iter()
4214 .find(|(_, role)| *role == frus_widgets::TitleBarRole::Icon)
4215 .map(|(r, _)| to_px(*r));
4216 crate::title_bar::publish(interactive, icon);
4217 }
4218 let buttons = unsafe { crate::title_bar::buttons(hwnd) }.map(|r| {
4220 frus_widgets::Rect::new(
4221 r.left as f32 / scale,
4222 r.top as f32 / scale,
4223 (r.right - r.left) as f32 / scale,
4224 (r.bottom - r.top) as f32 / scale,
4225 )
4226 });
4227 let button = |code: u32| {
4228 use windows_sys::Win32::UI::WindowsAndMessaging::{
4229 HTCLOSE, HTMAXBUTTON, HTMINBUTTON,
4230 };
4231 match code {
4232 HTMINBUTTON => Some(frus_widgets::CaptionButton::Minimize),
4233 HTMAXBUTTON => Some(frus_widgets::CaptionButton::Maximize),
4234 HTCLOSE => Some(frus_widgets::CaptionButton::Close),
4235 _ => None,
4236 }
4237 };
4238 let (hovered, pressed) = crate::title_bar::buttons_state();
4239 if self.window_icon.is_none() {
4240 self.window_icon = self
4241 .app
4242 .icon()
4243 .png()
4244 .map(|png| &*Box::leak(png.to_vec().into_boxed_slice()));
4245 }
4246 let active = window.has_focus();
4249 let dark = window.theme() == Some(winit::window::Theme::Dark);
4250 let (background, foreground) = crate::title_bar::caption_colors(dark, active);
4251 Some(frus_widgets::TitleBar {
4252 height: buttons.map_or(band, |b| (b.y + b.height).clamp(1.0, band)),
4256 leading: 0.0,
4257 buttons,
4258 icon: self.window_icon,
4259 hovered: button(hovered),
4260 pressed: button(pressed),
4261 maximized: window.is_maximized(),
4262 active,
4263 background: Some(background),
4264 foreground: Some(foreground),
4265 system_paints: self.renderer.as_ref().is_some_and(Renderer::sees_through)
4268 && unsafe { crate::title_bar::system_backdrop(hwnd, dark) },
4270 })
4271 } else {
4272 None
4273 };
4274 if next != self.title_bar {
4275 log::debug!(
4276 target: "frus::title_bar",
4277 "the line: {:?}",
4278 next.map(|line| (line.height, line.buttons, line.active, line.system_paints))
4279 );
4280 self.title_bar = next;
4281 self.build_dirty = true;
4282 self.request_redraw();
4283 }
4284 }
4285
4286 fn is_rtl(&self) -> bool {
4288 self.themes.displayed(&self.app).direction.is_rtl()
4289 }
4290
4291 fn logical_width(&self) -> f32 {
4293 if let Some(simulated) = &self.simulated {
4294 return simulated.screen_size().width;
4295 }
4296 let scale = self.total_scale();
4297 self.window
4298 .as_ref()
4299 .map(|w| w.inner_size().width as f32 / scale)
4300 .unwrap_or(1.0)
4301 .max(1.0)
4302 }
4303
4304 fn request_redraw(&self) {
4305 if let Some(window) = &self.window {
4306 window.request_redraw();
4307 }
4308 }
4309
4310 fn system_cursor(&self) -> Option<Point> {
4313 let window = self.window.as_ref()?;
4314 let physical = crate::file_drop::cursor_in(window, 1.0)?;
4315 Some(self.to_logical(physical.x as f64, physical.y as f64))
4316 }
4317
4318 fn file_event(&mut self, path: std::path::PathBuf, dropping: bool, at: Option<Point>) {
4321 self.file_drag
4322 .get_or_insert_with(Default::default)
4323 .add(path, dropping);
4324 self.files_moved(at);
4325 }
4326
4327 fn files_moved(&mut self, at: Option<Point>) {
4330 let Some(drag) = self.file_drag.as_ref() else {
4331 return;
4332 };
4333 let over = match (self.ui.as_ref(), self.tree.as_ref()) {
4334 (Some(ui), Some(tree)) => {
4335 crate::file_drop::target(ui, tree.as_ref(), &drag.paths, at, self.cursor)
4336 }
4337 _ => None,
4338 };
4339 if drag.over != over {
4340 if let Some(drag) = self.file_drag.as_mut() {
4341 drag.over = over;
4342 }
4343 self.runtime.drag_over = over;
4344 self.request_redraw();
4345 }
4346 }
4347
4348 fn files_left(&mut self) {
4350 if let Some(drag) = self.file_drag.take() {
4351 if drag.over.is_some() {
4352 self.runtime.drag_over = None;
4353 self.request_redraw();
4354 }
4355 }
4356 }
4357
4358 fn deliver_dropped_files(&mut self) {
4361 if !self.file_drag.as_ref().is_some_and(|drag| drag.dropped) {
4362 return;
4363 }
4364 let Some(drag) = self.file_drag.take() else {
4365 return;
4366 };
4367 if drag.over.is_some() {
4368 self.runtime.drag_over = None;
4369 }
4370 let message = drag.over.and_then(|id| {
4371 self.tree
4372 .as_ref()
4373 .and_then(|tree| find_widget(tree.as_ref(), id))
4374 .and_then(|widget| widget.on_files_dropped(drag.paths.clone()))
4375 });
4376 if message.is_none() {
4377 log::debug!(
4378 "{} file(s) dropped where nothing takes them",
4379 drag.paths.len()
4380 );
4381 }
4382 if let Some(message) = message {
4383 self.dispatch(message);
4384 }
4385 self.request_redraw();
4386 }
4387
4388 fn pointer(&mut self, event_loop: &ActiveEventLoop, event: PointerEvent) {
4392 self.pointer_event(event);
4393 event_loop.set_control_flow(self.idle_control_flow());
4395 }
4396
4397 fn pointer_event(&mut self, event: PointerEvent) {
4401 let kind = event.kind;
4402 self.cursor = event.position;
4405 self.route_listeners(kind, event.touch);
4406 self.route_pointer_event(event);
4407 self.pointer_held = match kind {
4408 PointerKind::Down => true,
4409 PointerKind::Up | PointerKind::Cancel => false,
4410 PointerKind::Move => self.pointer_held,
4411 };
4412 self.sync_regions(kind == PointerKind::Move);
4413 }
4414
4415 fn route_pointer_event(&mut self, event: PointerEvent) {
4417 self.cursor = event.position;
4418 self.hover.event(event.kind, event.position, event.touch);
4419 let could_tap = gesture_was_a_tap(self.drag.as_ref());
4422 match event.kind {
4423 PointerKind::Down => {
4424 self.sync_hover();
4427 self.runtime.focus_visible = false;
4429 self.pointer_down(event.touch);
4430 let (long_press, lift) = hold_candidates(
4431 self.drag.as_ref(),
4432 self.ui.as_ref(),
4433 self.tree.as_deref(),
4434 self.cursor,
4435 );
4436 self.long_press_msg = long_press;
4437 self.pending_lift = lift;
4438 let interested = self.long_press_msg.is_some()
4439 || self.pending_lift.is_some()
4440 || self.pending_reorder.is_some()
4441 || self.pending_word.is_some()
4442 || self.pending_region_hold;
4443 self.press.down(self.cursor, Instant::now(), interested);
4444 }
4445 PointerKind::Move => {
4446 self.press.moved(self.cursor);
4447 self.pointer_move();
4448 if self.inspector {
4451 self.request_redraw();
4452 }
4453 }
4454 PointerKind::Up => {
4455 let swallow = self.press.up();
4458 self.pending_lift = None;
4459 self.pending_reorder = None;
4460 self.pending_word = None;
4461 self.pending_region_hold = false;
4462 if swallow
4465 && !matches!(
4466 self.drag,
4467 Some(Drag::Item { moved: true, .. } | Drag::Reorder { moved: true, .. })
4468 )
4469 {
4470 self.drag = None;
4472 self.runtime.input.pressed = None;
4473 self.request_redraw();
4474 } else {
4475 self.pointer_up();
4476 }
4477 }
4478 PointerKind::Cancel => {
4479 self.press.cancel();
4480 self.pending_lift = None;
4481 self.pending_reorder = None;
4482 self.pending_word = None;
4483 self.pending_region_hold = false;
4484 if let Some(Drag::Dismiss { item, .. }) = self.drag {
4487 self.runtime.dismiss_release(
4489 item.id,
4490 0.0,
4491 0.0,
4492 item.spec.axis,
4493 f32::INFINITY,
4496 );
4497 }
4498 if let Some(Drag::Gesture { id, .. }) = self.drag {
4500 self.runtime.release_value(id);
4501 }
4502 if let Some(Drag::Scroll { id, .. }) = self.drag {
4503 self.runtime.release_scroll(id);
4504 self.runtime.glow_scroll_end(id);
4505 if let Some(host) = self.refresh_host_of(id) {
4508 self.runtime.refresh_cancel(host);
4509 }
4510 }
4511 let sheet = match self.drag {
4513 Some(Drag::Sheet { id, available, .. }) => self
4514 .ui
4515 .as_ref()
4516 .and_then(|ui| ui.sheet(id))
4517 .map(|sheet| (sheet.id, sheet.spec.clone(), available)),
4518 Some(Drag::Scroll { id, .. }) => self
4519 .ui
4520 .as_ref()
4521 .and_then(|ui| ui.sheet_holding(id))
4522 .map(|sheet| (sheet.id, sheet.spec.clone(), sheet.available)),
4523 _ => None,
4524 };
4525 if let Some((id, spec, available)) = sheet {
4526 self.runtime.sheet_release(id, &spec, available, 0.0, None);
4527 }
4528 self.drag = None;
4529 self.runtime.input.pressed = None;
4530 self.request_redraw();
4531 }
4532 }
4533 if settle_press(
4536 &mut self.runtime.input,
4537 event.kind,
4538 could_tap,
4539 self.drag.as_ref(),
4540 event.touch,
4541 ) {
4542 self.request_redraw();
4543 }
4544 if matches!(event.kind, PointerKind::Up | PointerKind::Cancel) {
4547 self.sync_hover();
4548 }
4549 }
4550
4551 fn hold_deadline_reached(&mut self) {
4553 let lifting = self.pending_lift.is_some() || self.pending_reorder.is_some();
4559 let word = self.pending_word.take();
4562 let region = std::mem::take(&mut self.pending_region_hold)
4565 && !lifting
4566 && word.is_none()
4567 && self.long_press_msg.is_none();
4568 if let Some(message) = self.long_press_msg.take() {
4569 if !lifting && word.is_none() {
4570 self.dispatch(message);
4571 }
4572 }
4573 if let Some(id) = word {
4574 self.select_held_word(id);
4575 }
4576 if region && self.select_region_word_held() {
4577 if let Some(Drag::Scroll { id, .. }) = self.drag {
4579 self.runtime.release_scroll(id);
4580 }
4581 }
4582 let lift = self.pending_lift.take();
4583 let reorder = self.pending_reorder.take();
4584 if lift.is_some() || reorder.is_some() {
4586 if let Some(Drag::Scroll { id, .. }) = self.drag {
4587 self.runtime.release_scroll(id);
4588 }
4589 }
4590 self.drag = drag_after_hold(self.drag.take(), lift, reorder, self.cursor);
4591 if matches!(self.drag, Some(Drag::Reorder { .. })) {
4592 self.reorder_x = self.cursor.x;
4593 self.reorder_y = self.cursor.y;
4594 }
4595 self.request_redraw();
4596 }
4597
4598 fn idle_control_flow(&self) -> ControlFlow {
4602 let press = self.press.deadline();
4603 let reload = self.reload.as_ref().map(|w| w.deadline());
4604 let caret = (self.lifecycle == Lifecycle::Resumed)
4607 .then(|| self.caret.next_toggle(Instant::now()))
4608 .flatten();
4609 let tap = self.pending_tap.as_ref().map(|p| p.at + DOUBLE_TAP_TIMEOUT);
4611 let files =
4614 (cfg!(windows) && self.file_drag.is_some()).then(|| Instant::now() + FILE_DRAG_POLL);
4615 match [press, reload, caret, tap, files]
4616 .into_iter()
4617 .flatten()
4618 .min()
4619 {
4620 Some(at) => ControlFlow::WaitUntil(at),
4621 None => ControlFlow::Wait,
4622 }
4623 }
4624
4625 fn caret_signature(&self) -> Option<crate::caret::Signature> {
4629 use std::hash::{Hash, Hasher};
4630 let field = self.runtime.input.focused?;
4631 let widget = find_widget(self.tree.as_ref()?.as_ref(), field)?;
4632 let text = widget.text_value()?;
4633 let mut hasher = std::collections::hash_map::DefaultHasher::new();
4634 text.hash(&mut hasher);
4635 Some(crate::caret::Signature {
4636 field,
4637 edit: self.runtime.edits.get(&field).copied().unwrap_or_default(),
4638 value: hasher.finish(),
4639 })
4640 }
4641
4642 fn pointer_move(&mut self) {
4645 if self.drag.is_some() {
4646 self.handle_drag();
4647 return;
4648 }
4649 let hovered = self.sync_hover();
4650 self.update_cursor_icon(hovered);
4654 }
4655
4656 fn sync_hover(&mut self) -> Option<WidgetId> {
4662 let hovered = self.ui.as_ref().and_then(|ui| self.hover.target(ui));
4663 if hovered != self.runtime.input.hovered {
4664 self.runtime.input.hovered = hovered;
4665 self.request_redraw();
4666 }
4667 hovered
4668 }
4669
4670 fn take_task_arrivals(&mut self) -> bool {
4673 let now = frus_widgets::task_arrivals();
4674 std::mem::replace(&mut self.task_arrivals, now) != now
4675 }
4676
4677 fn route_listeners(&mut self, kind: PointerKind, touch: bool) {
4681 use frus_widgets::ListenerEventKind as Raw;
4682 let at = self.cursor;
4683 let heard: Vec<(WidgetId, Rect, Raw)> = match kind {
4684 PointerKind::Down => {
4685 self.captured = self
4686 .ui
4687 .as_ref()
4688 .map(|ui| ui.pointer_listeners_at(at))
4689 .unwrap_or_default();
4690 self.captured
4691 .iter()
4692 .map(|(id, rect)| (*id, *rect, Raw::Down))
4693 .collect()
4694 }
4695 PointerKind::Move if self.pointer_held => self
4696 .captured
4697 .iter()
4698 .map(|(id, rect)| (*id, *rect, Raw::Move))
4699 .collect(),
4700 PointerKind::Move if !touch => self
4701 .ui
4702 .as_ref()
4703 .map(|ui| ui.pointer_listeners_at(at))
4704 .unwrap_or_default()
4705 .into_iter()
4706 .map(|(id, rect)| (id, rect, Raw::Hover))
4707 .collect(),
4708 PointerKind::Move => Vec::new(),
4709 PointerKind::Up | PointerKind::Cancel => {
4710 let raw = if kind == PointerKind::Up {
4711 Raw::Up
4712 } else {
4713 Raw::Cancel
4714 };
4715 std::mem::take(&mut self.captured)
4716 .into_iter()
4717 .map(|(id, rect)| (id, rect, raw))
4718 .collect()
4719 }
4720 };
4721 let messages: Vec<A::Message> = heard
4722 .into_iter()
4723 .flat_map(|(id, rect, kind)| {
4724 let event = frus_widgets::ListenerEvent {
4725 kind,
4726 local: Point::new(at.x - rect.x, at.y - rect.y),
4727 touch,
4728 };
4729 self.tree
4730 .as_ref()
4731 .and_then(|tree| find_widget(tree.as_ref(), id))
4732 .map(|widget| widget.on_pointer_event(event))
4733 .unwrap_or_default()
4734 })
4735 .collect();
4736 for message in messages {
4737 self.dispatch(message);
4738 self.request_redraw();
4739 }
4740 }
4741
4742 fn sync_regions(&mut self, moved: bool) {
4746 let point = self.hover.at();
4747 let now: Vec<(WidgetId, Rect)> = match (point, self.ui.as_ref()) {
4748 (Some(point), Some(ui)) => ui
4749 .hover_regions_at(point)
4750 .into_iter()
4751 .map(|(id, rect, _)| (id, rect))
4752 .collect(),
4753 _ => Vec::new(),
4754 };
4755 let at = point.unwrap_or(self.cursor);
4756 let local = |rect: Rect| Point::new(at.x - rect.x, at.y - rect.y);
4757 let before = std::mem::replace(&mut self.entered, now.clone());
4758 let mut events = Vec::new();
4759 for (id, rect) in &before {
4760 if !now.iter().any(|(n, _)| n == id) {
4761 events.push((
4762 *id,
4763 frus_widgets::HoverEvent::Exit {
4764 local: local(*rect),
4765 },
4766 ));
4767 }
4768 }
4769 for (id, rect) in now.iter().rev() {
4770 if !before.iter().any(|(b, _)| b == id) {
4771 events.push((
4772 *id,
4773 frus_widgets::HoverEvent::Enter {
4774 local: local(*rect),
4775 },
4776 ));
4777 } else if moved && !self.pointer_held {
4778 events.push((
4779 *id,
4780 frus_widgets::HoverEvent::Move {
4781 local: local(*rect),
4782 },
4783 ));
4784 }
4785 }
4786 let messages: Vec<_> = events
4787 .into_iter()
4788 .filter_map(|(id, event)| {
4789 self.tree
4790 .as_ref()
4791 .and_then(|tree| find_widget(tree.as_ref(), id))
4792 .and_then(|widget| widget.on_hover_event(event))
4793 })
4794 .collect();
4795 for message in messages {
4796 self.dispatch(message);
4797 self.request_redraw();
4798 }
4799 }
4800
4801 fn region_cursor(&self, requested: Option<UiCursor>) -> UiCursor {
4806 requested
4807 .or_else(|| {
4808 let point = self.hover.at()?;
4809 let (id, _) = self.ui.as_ref()?.focus_hit(point)?;
4810 let widget = find_widget(self.tree.as_ref()?.as_ref(), id)?;
4811 is_text_field(widget).then_some(UiCursor::Text)
4812 })
4813 .or_else(|| {
4814 self.hover.at().and_then(|point| {
4815 self.ui
4816 .as_ref()?
4817 .hover_regions_at(point)
4818 .into_iter()
4819 .find_map(|(_, _, region)| region.cursor)
4820 })
4821 })
4822 .unwrap_or(UiCursor::Default)
4823 }
4824
4825 fn update_cursor_icon(&mut self, hovered: Option<WidgetId>) {
4829 let requested = hovered.and_then(|id| {
4830 let rect = self.ui.as_ref()?.widget_rect(id)?;
4831 let widget = find_widget(self.tree.as_ref()?.as_ref(), id)?;
4832 widget.cursor_icon(
4833 self.cursor.x - rect.x,
4834 self.cursor.y - rect.y,
4835 rect.width,
4836 rect.height,
4837 )
4838 });
4839 let shown = self.region_cursor(requested);
4840 if let Some(window) = &self.window {
4841 window.set_cursor_visible(shown != UiCursor::None);
4842 window.set_cursor(cursor_icon(shown));
4843 }
4844 let hover_cursor = requested.map(|_| self.cursor);
4849 if hover_cursor != self.runtime.input.hover_cursor {
4850 self.runtime.input.hover_cursor = hover_cursor;
4851 self.request_redraw();
4852 }
4853 }
4854
4855 fn pointer_down(&mut self, touch: bool) {
4858 self.pointer_touch = touch;
4859 self.poll_pending_tap(Instant::now());
4862 if let Some(pending) = self.pending_tap.as_ref() {
4863 let here = self.ui.as_ref().and_then(|ui| ui.hit(self.cursor));
4864 let (dx, dy) = (
4865 self.cursor.x - pending.position.x,
4866 self.cursor.y - pending.position.y,
4867 );
4868 if here != Some(pending.id) || dx.hypot(dy) > DOUBLE_TAP_SLOP {
4869 self.flush_pending_tap();
4870 }
4871 }
4872 self.pending_word = None;
4873 self.pending_region_hold = false;
4874 if self
4880 .ui
4881 .as_ref()
4882 .is_some_and(|ui| ui.toolbar_contains(self.cursor))
4883 {
4884 press_at(&mut self.runtime, self.ui.as_ref(), self.cursor);
4885 self.request_redraw();
4886 return;
4887 }
4888 if let Some(drag) = self.grab_region_handle() {
4894 self.drag = Some(drag);
4895 if let Some(selection) = self.runtime.region.as_mut() {
4896 selection.bar = false;
4897 }
4898 self.request_redraw();
4899 return;
4900 }
4901 if let Some(drag) = self.grab_selection_handle() {
4902 self.drag = Some(drag);
4903 self.runtime.selection_toolbar = None;
4906 self.request_redraw();
4907 return;
4908 }
4909 if self.runtime.selection_handles.take().is_some() {
4910 self.request_redraw();
4911 }
4912 self.hide_selection_toolbar();
4913 if self.runtime.region.take().is_some() {
4916 self.request_redraw();
4917 }
4918 let above_the_pages = self.ui.as_ref().is_some_and(|ui| {
4923 ui.over_overlay(self.cursor)
4924 || ui
4925 .title_bar_row()
4926 .is_some_and(|row| row.contains(self.cursor))
4927 });
4928 let on_back_edge = !above_the_pages
4929 && if self.is_rtl() {
4930 self.cursor.x > self.logical_width() - BACK_EDGE
4931 } else {
4932 self.cursor.x < BACK_EDGE
4933 };
4934 if on_back_edge && self.app.can_go_back() {
4935 self.drag = Some(Drag::Back {
4936 start_x: self.cursor.x,
4937 });
4938 self.begin_gesture();
4939 self.build_dirty = true;
4940 self.app.back_gesture(0.0);
4941 self.request_redraw();
4942 return;
4943 }
4944
4945 if let Some(bar) = self.ui.as_ref().and_then(|ui| ui.scrollbar_at(self.cursor)) {
4947 let (along, thumb_start) = if bar.vertical {
4948 (self.cursor.y, bar.thumb.y)
4949 } else {
4950 (self.cursor.x, bar.thumb.x)
4951 };
4952 self.drag = Some(Drag::Scrollbar {
4953 id: bar.id,
4954 vertical: bar.vertical,
4955 grab: along - thumb_start,
4956 track_start: bar.track_start,
4957 track_len: bar.track_len,
4958 thumb_len: bar.thumb_len,
4959 max: bar.max,
4960 reverse: bar.reverse,
4961 });
4962 self.request_redraw();
4963 return;
4964 }
4965
4966 if let Some((id, rect)) = self.ui.as_ref().and_then(|ui| ui.draggable_at(self.cursor)) {
4968 let (x, rtl) = (self.cursor.x, self.is_rtl());
4969 let track = self
4970 .tree
4971 .as_ref()
4972 .and_then(|tree| find_widget(tree.as_ref(), id))
4973 .map_or(frus_widgets::DragTrack::WHOLE, |widget| widget.drag_track());
4974 let active = track.engages(rect, x, rtl);
4975 let thumbs = matches!(track.mode, frus_widgets::DragMode::Thumbs { .. });
4976 let thumb = track.pick_thumb(rect, x, 0.0, rtl);
4977 self.drag = Some(Drag::Widget {
4978 id,
4979 rect,
4980 last_x: x,
4981 press_x: x,
4982 track,
4983 active,
4984 thumb,
4985 });
4986 if thumbs {
4987 if let Some(thumb) = thumb {
4989 let at = track.fraction(rect, x, rtl);
4990 self.thumb_drag(id, thumb, frus_widgets::ThumbDrag::Start, at);
4991 self.thumb_drag(id, thumb, frus_widgets::ThumbDrag::Move, at);
4992 }
4993 } else if active {
4994 self.dispatch_drag_edge(id, Edge::Start, 0.0);
4999 self.apply_widget_drag(id, 0.0, track.value_at(rect, x, x, false, rtl));
5001 }
5002 self.request_redraw();
5003 return;
5004 }
5005
5006 if let Some((id, from, hold)) = self.reorderable_at(self.cursor) {
5015 if hold {
5016 self.pending_reorder = Some((id, from, self.cursor));
5017 } else {
5018 self.drag = Some(Drag::Reorder {
5019 id,
5020 from,
5021 start: self.cursor,
5022 moved: false,
5023 carried: None,
5024 });
5025 self.reorder_x = self.cursor.x; self.reorder_y = self.cursor.y; }
5028 }
5029
5030 if press_at(&mut self.runtime, self.ui.as_ref(), self.cursor) {
5031 self.request_redraw();
5032 }
5033 let previously_focused = self.runtime.input.focused;
5035 let focus = self.ui.as_ref().and_then(|ui| ui.focus_hit(self.cursor));
5036 if let Some(held) = previously_focused {
5039 let inside = focus.is_some_and(|(id, _)| id == held);
5040 let outside = (!inside)
5041 .then(|| {
5042 self.tree
5043 .as_ref()
5044 .and_then(|tree| find_widget(tree.as_ref(), held))
5045 .and_then(|widget| widget.on_tap_outside())
5046 })
5047 .flatten();
5048 if let Some(message) = outside {
5049 self.dispatch(message);
5050 }
5051 }
5052 self.runtime.input.focused = focus.map(|(id, _)| id);
5053 if let Some((id, rect)) = focus {
5054 let local_x = self.cursor.x - rect.x;
5055 let local_y =
5058 self.cursor.y - rect.y + self.runtime.scroll.get(&id).map(|s| s.1).unwrap_or(0.0);
5059 let scroll_cursor = if previously_focused == Some(id) {
5062 self.runtime.edits.get(&id).map(|e| e.cursor).unwrap_or(0)
5063 } else {
5064 0
5065 };
5066 let cursor = self
5070 .tree
5071 .as_ref()
5072 .and_then(|tree| find_widget(tree.as_ref(), id))
5073 .and_then(|widget| widget.cursor_at(local_x, local_y, rect.width, scroll_cursor));
5074 if let Some(cursor) = cursor {
5075 self.goal_x = None;
5077 self.runtime.edits.insert(
5078 id,
5079 Edit {
5080 cursor,
5081 anchor: None,
5082 composing: None,
5083 },
5084 );
5085 self.drag = Some(Drag::TextSelect { id, rect });
5086 if touch {
5088 self.pending_word = Some(id);
5089 }
5090 self.runtime.close_edit_run(id);
5093 self.request_soft_input();
5097
5098 let now = Instant::now();
5100 let double = self
5101 .last_click_time
5102 .map(|t| (now - t).as_secs_f32() < 0.4)
5103 .unwrap_or(false);
5104 self.last_click_time = Some(now);
5105 if double {
5106 if let Some((start, end)) = self
5107 .tree
5108 .as_ref()
5109 .and_then(|tree| find_widget(tree.as_ref(), id))
5110 .and_then(|widget| widget.word_at(cursor))
5111 {
5112 self.runtime.edits.insert(
5113 id,
5114 Edit {
5115 cursor: end,
5116 anchor: Some(start),
5117 composing: None,
5118 },
5119 );
5120 self.drag = None;
5121 }
5122 }
5123 }
5124 }
5125
5126 if self.drag.is_none() && focus.is_none() {
5130 if touch {
5131 self.pending_region_hold = self.region_press_target().is_some();
5132 } else {
5133 self.begin_region_selection();
5134 }
5135 }
5136
5137 if touch && self.drag.is_none() {
5141 let landed = {
5146 let scroll = &self.runtime.scroll;
5147 self.ui.as_ref().and_then(|ui| {
5148 ui.scroll_chain(self.cursor).find(|area| {
5149 area.accepts_user_offset(
5150 scroll.get(&area.id).copied().unwrap_or((0.0, 0.0)),
5151 )
5152 })
5153 })
5154 };
5155 if let Some(area) = landed {
5156 let physics = area.physics;
5160 let carried = self.runtime.catch_scroll_fling(area.id, physics);
5161 self.runtime.hold_scroll(area.id);
5163 if let Some(sheet) = self
5167 .ui
5168 .as_ref()
5169 .and_then(|ui| ui.sheet_holding(area.id))
5170 .map(|sheet| sheet.id)
5171 {
5172 self.runtime.sheet_hold(sheet);
5173 }
5174 self.drag = Some(Drag::Scroll {
5175 id: area.id,
5176 last: self.cursor,
5177 moved: false,
5178 carried,
5179 dismiss: self
5180 .ui
5181 .as_ref()
5182 .and_then(|ui| ui.dismissable_at(self.cursor)),
5183 axis: None,
5184 pan: self.pan_target(),
5185 });
5186 self.begin_gesture();
5187 self.gesture_velocity = area.velocity_tracker();
5190 }
5191 }
5192
5193 if self.drag.is_none() {
5196 if let Some((id, available)) = self
5197 .ui
5198 .as_ref()
5199 .and_then(|ui| ui.sheet_at(self.cursor))
5200 .map(|sheet| (sheet.id, sheet.available))
5201 {
5202 self.runtime.sheet_hold(id);
5203 self.drag = Some(Drag::Sheet {
5204 id,
5205 last: self.cursor,
5206 moved: false,
5207 available,
5208 });
5209 self.begin_gesture();
5210 }
5211 }
5212
5213 if self.drag.is_none() {
5217 if let Some(item) = self
5218 .ui
5219 .as_ref()
5220 .and_then(|ui| ui.dismissable_at(self.cursor))
5221 {
5222 self.drag = Some(Drag::Dismiss {
5223 item,
5224 last: self.cursor,
5225 moved: false,
5226 });
5227 self.begin_gesture();
5228 }
5229 }
5230
5231 if self.drag.is_none() {
5237 if let Some(source) = self
5238 .ui
5239 .as_ref()
5240 .and_then(|ui| ui.drag_source_at(self.cursor))
5241 .filter(|source| {
5242 !self
5244 .tree
5245 .as_ref()
5246 .and_then(|tree| find_widget(tree.as_ref(), source.id))
5247 .is_some_and(|widget| widget.drag_needs_long_press())
5248 })
5249 {
5250 self.drag = Some(Drag::Item {
5251 source,
5252 start: self.cursor,
5253 moved: false,
5254 over: None,
5255 });
5256 self.begin_gesture();
5257 }
5258 }
5259
5260 if self.drag.is_none() {
5263 if let Some((id, axis)) = self.pan_target() {
5264 self.drag = Some(Drag::Gesture {
5265 id,
5266 axis,
5267 start: self.cursor,
5268 last: self.cursor,
5269 moved: false,
5270 });
5271 self.begin_gesture();
5272 }
5273 }
5274
5275 if self.drag.is_none() {
5279 if let Some((id, viewport)) = self
5280 .ui
5281 .as_ref()
5282 .and_then(|ui| ui.interactive_at(self.cursor))
5283 {
5284 self.runtime.interactive_velocity.remove(&id);
5286 self.drag = Some(Drag::Pan {
5287 id,
5288 last: self.cursor,
5289 moved: false,
5290 viewport,
5291 });
5292 self.begin_gesture();
5293 }
5294 }
5295 self.request_redraw();
5296 }
5297
5298 fn positional_click_on(&self, id: WidgetId) -> Option<A::Message> {
5304 let rect = self.ui.as_ref()?.widget_rect(id)?;
5305 let widget = find_widget(self.tree.as_ref()?.as_ref(), id)?;
5306 widget.positional_click(
5307 self.cursor.x - rect.x,
5308 self.cursor.y - rect.y,
5309 rect.width,
5310 rect.height,
5311 )
5312 }
5313
5314 fn pointer_up(&mut self) {
5315 let ended = self.drag.take();
5316 if let Some(Drag::Back { .. }) = ended {
5317 let sign = if self.is_rtl() { -1.0 } else { 1.0 };
5320 let velocity = sign * self.gesture_estimate().velocity.x / self.logical_width();
5321 self.build_dirty = true;
5322 self.app.back_gesture_end(velocity);
5323 self.request_redraw();
5324 return;
5325 }
5326 let was_tap = gesture_was_a_tap(ended.as_ref());
5329 if let Some(Drag::Widget {
5333 id,
5334 rect,
5335 press_x,
5336 track,
5337 active: true,
5338 thumb,
5339 ..
5340 }) = ended
5341 {
5342 let rtl = self.is_rtl();
5343 if matches!(track.mode, frus_widgets::DragMode::Thumbs { .. }) {
5344 if let Some(thumb) = thumb {
5345 let at = track.fraction(rect, self.cursor.x, rtl);
5346 self.thumb_drag(id, thumb, frus_widgets::ThumbDrag::End, at);
5347 }
5348 } else {
5349 let released = track.released_at(rect, press_x, self.cursor.x, rtl);
5350 self.dispatch_drag_edge(id, Edge::End, released);
5351 }
5352 }
5353 #[cfg(android)]
5355 if let Some(Drag::SelectionHandle { id, .. }) = ended {
5356 self.push_ime_context(id);
5357 }
5358 if let Some(Drag::SelectionHandle { id, .. }) = ended {
5359 self.show_selection_toolbar(id);
5360 }
5361 if let Some(Drag::RegionHandle { .. }) = ended {
5363 if let Some(selection) = self.runtime.region.as_mut() {
5364 selection.bar = true;
5365 }
5366 }
5367 if let Some(Drag::Reorder {
5370 id,
5371 from,
5372 moved: true,
5373 carried,
5374 ..
5375 }) = &ended
5376 {
5377 let target = self.reorder_drop_target(*id, *from, *carried);
5378 let tree = self.tree.as_ref();
5379 let base = target
5380 .and_then(|tid| tree.and_then(|t| find_widget(t.as_ref(), tid)))
5381 .and_then(|widget| widget.reorder_index());
5382 let to = match (base, target) {
5387 (Some(base), Some(tid)) => {
5388 let after = self
5389 .ui
5390 .as_ref()
5391 .and_then(|ui| ui.widget_rect(tid))
5392 .map(|rect| self.reorder_insert_after(tid, rect))
5393 .unwrap_or(false);
5394 Some(base + after as usize)
5395 }
5396 _ => None,
5397 };
5398 let self_drop = target == Some(*id);
5402 let message = match to {
5403 Some(to) if to != *from && !self_drop => tree
5404 .and_then(|t| find_widget(t.as_ref(), *id))
5405 .and_then(|widget| widget.on_reorder(to)),
5406 _ => None,
5407 };
5408 if let Some(message) = message {
5409 let to = to.unwrap_or(*from);
5410 let widget = tree.and_then(|t| find_widget(t.as_ref(), *id));
5411 let axis = widget
5412 .map(|w| w.reorder_axis())
5413 .unwrap_or(ReorderAxis::Horizontal);
5414 let announcement = widget
5420 .and_then(|w| w.reorder_announcement(to))
5421 .unwrap_or_else(|| match axis {
5422 ReorderAxis::Horizontal => format!("Column moved to position {}", to + 1),
5423 ReorderAxis::Vertical => "Card moved".to_string(),
5424 });
5425 self.dispatch(message);
5426 self.set_announcement(announcement);
5427 }
5428 }
5429 if let Some(Drag::Dismiss { item, moved, .. }) = &ended {
5435 if *moved {
5436 let estimate = self.gesture_estimate();
5437 let horizontal = item.spec.axis.is_horizontal();
5438 let (along, across) = if horizontal {
5439 (estimate.velocity.x, estimate.velocity.y)
5440 } else {
5441 (estimate.velocity.y, estimate.velocity.x)
5442 };
5443 self.runtime.dismiss_release(
5444 item.id,
5445 along,
5446 across,
5447 item.spec.axis,
5448 item.spec.threshold,
5449 );
5450 self.request_redraw();
5451 return;
5452 }
5453 }
5454 if let Some(Drag::Sheet {
5457 id,
5458 moved,
5459 available,
5460 ..
5461 }) = &ended
5462 {
5463 let velocity = if *moved {
5464 -self.fling_velocity(self.gesture_estimate()).1
5465 } else {
5466 0.0
5467 };
5468 let sheet = self.ui.as_ref().and_then(|ui| ui.sheet(*id)).map(|sheet| {
5471 let list = match sheet.areas.as_slice() {
5472 [only] => Some(*only),
5473 _ => None,
5474 };
5475 (sheet.spec.clone(), list)
5476 });
5477 if let Some((spec, list)) = sheet {
5478 self.runtime
5479 .sheet_release(*id, &spec, *available, velocity, list);
5480 }
5481 if *moved {
5482 self.request_redraw();
5483 return;
5484 }
5485 }
5486 if let Some(Drag::Item {
5487 source,
5488 moved: true,
5489 over,
5490 ..
5491 }) = &ended
5492 {
5493 self.runtime.drag_over = None;
5497 let accepted = over.is_some();
5498 let dropped = over.and_then(|id| {
5499 self.tree
5500 .as_ref()
5501 .and_then(|tree| find_widget(tree.as_ref(), id))
5502 .and_then(|widget| widget.on_drop(source.payload))
5503 });
5504 let ended_msg = self
5505 .tree
5506 .as_ref()
5507 .and_then(|tree| find_widget(tree.as_ref(), source.id))
5508 .and_then(|widget| widget.on_dropped(accepted));
5509 for message in dropped.into_iter().chain(ended_msg) {
5510 self.dispatch(message);
5511 }
5512 self.request_redraw();
5513 return;
5514 }
5515 if let Some(Drag::Item { .. }) = &ended {
5516 self.runtime.drag_over = None;
5519 }
5520 let mut sheet_took = false;
5523 if let Some(Drag::Scroll { id, .. }) = &ended {
5524 let sheet = self
5525 .ui
5526 .as_ref()
5527 .and_then(|ui| ui.sheet_holding(*id))
5528 .cloned();
5529 if let Some(sheet) = sheet {
5530 let velocity = match &ended {
5533 Some(Drag::Scroll {
5534 moved: true,
5535 axis: Some(true),
5536 carried,
5537 ..
5538 }) => -self.fling_velocity(self.gesture_estimate()).1 + carried.1,
5539 _ => 0.0,
5540 };
5541 let offset = self.runtime.scroll.get(id).map_or(0.0, |o| o.1);
5542 let size = self.runtime.sheet_size(sheet.id, &sheet.spec);
5543 sheet_took = velocity != 0.0
5544 && frus_widgets::sheet_takes_release(
5545 velocity,
5546 offset,
5547 size,
5548 &sheet.spec,
5549 sheet.available,
5550 );
5551 self.runtime.sheet_release(
5554 sheet.id,
5555 &sheet.spec,
5556 sheet.available,
5557 if sheet_took { velocity } else { 0.0 },
5558 Some(*id),
5559 );
5560 }
5561 }
5562 if let Some(Drag::Scroll { id, .. }) = &ended {
5563 self.runtime.release_scroll(*id);
5565 self.runtime.glow_scroll_end(*id);
5567 self.release_refresh(*id);
5571 }
5572 if let Some(Drag::Scroll {
5573 id,
5574 moved: true,
5575 carried,
5576 axis,
5577 ..
5578 }) = &ended
5579 {
5580 let estimate = self.gesture_estimate();
5584 let velocity = self.fling_velocity(estimate);
5585 let velocity = match self.ui.as_ref().and_then(|u| u.scroll_region(*id)) {
5588 Some(area) => area.offset_delta(velocity),
5589 None => (-velocity.0, -velocity.1),
5590 };
5591 let launch = (velocity.0 + carried.0, velocity.1 + carried.1);
5595 if sheet_took {
5596 self.request_redraw();
5597 } else {
5598 self.fling(
5599 *id,
5600 match axis {
5601 Some(true) => (0.0, launch.1),
5602 Some(false) => (launch.0, 0.0),
5603 None => launch,
5604 },
5605 );
5606 }
5607 }
5608 if let Some(Drag::Gesture {
5611 id,
5612 axis,
5613 moved: true,
5614 ..
5615 }) = &ended
5616 {
5617 let velocity = self.fling_velocity(self.gesture_estimate());
5618 let velocity = mask_pan(*axis, Point::new(velocity.0, velocity.1));
5619 let speed = velocity.x.hypot(velocity.y);
5621 let velocity = if speed > MAX_PAN_VELOCITY {
5622 let k = MAX_PAN_VELOCITY / speed;
5623 Point::new(velocity.x * k, velocity.y * k)
5624 } else {
5625 velocity
5626 };
5627 self.send_pan(*id, frus_widgets::PanEvent::End { velocity });
5628 }
5629 if let Some(Drag::Pan {
5632 id, moved: true, ..
5633 }) = &ended
5634 {
5635 let estimate = self.gesture_estimate();
5636 let velocity = self.fling_velocity(estimate);
5637 if velocity.0.hypot(velocity.1) > PAN_FLING_MIN {
5638 self.runtime.interactive_velocity.insert(*id, velocity);
5639 }
5640 }
5641 if ended.is_some() && !was_tap {
5642 self.request_redraw();
5643 return;
5644 }
5645 let released = self.ui.as_ref().and_then(|ui| ui.hit(self.cursor));
5647 let bar_action = match (self.runtime.input.pressed, released) {
5650 (Some(pressed), Some(released)) if pressed == released => {
5651 self.ui.as_ref().and_then(|ui| ui.edit_action_for(pressed))
5652 }
5653 _ => None,
5654 };
5655 let on_bar = self
5656 .ui
5657 .as_ref()
5658 .is_some_and(|ui| ui.toolbar_contains(self.cursor));
5659 let field_positional = if bar_action.is_some() {
5664 None
5665 } else {
5666 self.ui
5667 .as_ref()
5668 .and_then(|ui| ui.focus_hit(self.cursor))
5669 .map(|(id, _)| id)
5670 .filter(|id| self.runtime.input.focused == Some(*id))
5671 .and_then(|id| self.positional_click_on(id))
5672 };
5673 let (message, announce) = match (self.runtime.input.pressed, released) {
5674 (Some(_), Some(_)) if bar_action.is_some() => (None, None),
5675 _ if field_positional.is_some() => (field_positional, None),
5676 (Some(pressed), Some(released)) if pressed == released => {
5677 let positional = self.positional_click_on(released);
5680 let message =
5681 positional.or_else(|| self.ui.as_ref().and_then(|ui| ui.msg_for(pressed)));
5682 let announce = self
5685 .tree
5686 .as_ref()
5687 .and_then(|tree| find_widget(tree.as_ref(), pressed))
5688 .and_then(|widget| widget.announce());
5689 (message, announce)
5690 }
5691 _ => (None, None),
5692 };
5693 let message = match (self.runtime.input.pressed, released) {
5695 (Some(pressed), Some(released)) if pressed == released && bar_action.is_none() => {
5696 self.double_tap(pressed, message)
5697 }
5698 _ => message,
5699 };
5700 if let (Some(pressed), Some(released)) = (self.runtime.input.pressed, released) {
5704 if pressed == released {
5705 self.runtime.ink_confirm(pressed);
5706 }
5707 }
5708 self.runtime.input.pressed = None;
5709 if let Some(action) = bar_action {
5710 self.perform_edit_action(action);
5711 } else if on_bar && message.is_some() {
5712 self.runtime.selection_handles = None;
5715 self.hide_selection_toolbar();
5716 }
5717 if let Some(message) = message {
5718 self.dispatch(message);
5719 if let Some(announce) = announce {
5720 self.set_announcement(announce);
5721 }
5722 }
5723 self.request_redraw();
5724 }
5725
5726 fn double_tap(&mut self, id: WidgetId, message: Option<A::Message>) -> Option<A::Message> {
5730 let Some(double) = self
5731 .tree
5732 .as_ref()
5733 .and_then(|tree| find_widget(tree.as_ref(), id))
5734 .and_then(|widget| widget.on_double_tap())
5735 else {
5736 return message;
5737 };
5738 let now = Instant::now();
5739 let second = self.pending_tap.as_ref().is_some_and(|pending| {
5740 let (dx, dy) = (
5741 self.cursor.x - pending.position.x,
5742 self.cursor.y - pending.position.y,
5743 );
5744 pending.id == id
5745 && now.duration_since(pending.at) <= DOUBLE_TAP_TIMEOUT
5746 && dx.hypot(dy) <= DOUBLE_TAP_SLOP
5747 });
5748 if second {
5749 self.pending_tap = None;
5750 return Some(double);
5751 }
5752 self.flush_pending_tap();
5753 self.pending_tap = Some(PendingTap {
5754 id,
5755 at: now,
5756 position: self.cursor,
5757 single: message,
5758 });
5759 None
5760 }
5761
5762 fn flush_pending_tap(&mut self) {
5764 if let Some(message) = self.pending_tap.take().and_then(|pending| pending.single) {
5765 self.dispatch(message);
5766 self.request_redraw();
5767 }
5768 }
5769
5770 fn poll_pending_tap(&mut self, now: Instant) -> bool {
5772 let due = self
5773 .pending_tap
5774 .as_ref()
5775 .is_some_and(|pending| now.duration_since(pending.at) >= DOUBLE_TAP_TIMEOUT);
5776 if due {
5777 self.flush_pending_tap();
5778 }
5779 due
5780 }
5781
5782 #[cfg(any(desktop, web))]
5787 fn set_announcement(&mut self, message: String) {
5788 self.announce = message;
5789 }
5790 #[cfg(not(any(desktop, web)))]
5791 fn set_announcement(&mut self, _message: String) {}
5792
5793 fn reconcile_focus(&mut self) {
5800 let present: std::collections::HashSet<WidgetId> = match self.ui.as_ref() {
5801 Some(ui) => ui.focusable_ids().collect(),
5802 None => return,
5803 };
5804 let autofocus: Vec<WidgetId> = self
5808 .ui
5809 .as_ref()
5810 .map(|ui| ui.autofocus_ids().collect())
5811 .unwrap_or_default();
5812 let appeared = autofocus
5813 .iter()
5814 .find(|id| !self.autofocused.contains(id))
5815 .copied();
5816 self.autofocused = autofocus.into_iter().collect();
5817 if let Some(id) = appeared {
5818 self.runtime.input.focused = Some(id);
5819 }
5820 let before = self.runtime.input.focused;
5821 let after = resolve_focus(
5822 before,
5823 &present,
5824 &mut self.focus_history,
5825 &mut self.prev_focus,
5826 );
5827 if after != before || appeared.is_some() {
5828 self.runtime.input.focused = after;
5829 self.request_redraw();
5830 }
5831 }
5832
5833 fn report_focus(&mut self) {
5836 let now = self.runtime.input.focused;
5837 if now == self.focus_reported {
5840 return;
5841 }
5842 let before = std::mem::replace(&mut self.focus_reported, now);
5843 let messages = self
5844 .tree
5845 .as_ref()
5846 .map(|tree| focus_changes(tree.as_ref(), before, now))
5847 .unwrap_or_default();
5848 for message in messages {
5849 self.dispatch(message);
5850 }
5851 }
5852
5853 fn dispatch(&mut self, message: A::Message) {
5856 self.build_dirty = true;
5858 let command = self.app.update(message);
5859 self.run_command(command);
5860 let asked = self.app.effects();
5861 self.run_command(asked);
5862 #[cfg(web)]
5863 self.sync_address();
5864 #[cfg(any(desktop, android))]
5865 self.report_dev();
5866 self.sync_subscriptions();
5867 }
5868
5869 #[cfg(any(android, desktop))]
5872 fn open_launch_link(&mut self) {
5873 #[cfg(android)]
5874 let link = self
5875 .android_app
5876 .as_ref()
5877 .and_then(crate::android_link::launch_link);
5878 #[cfg(desktop)]
5880 let link = crate::link::link_among(std::env::args()).or_else(crate::dev::restore_link);
5881 if let Some(location) = link.as_deref().and_then(crate::link::location_of_link) {
5882 let command = self.app.open_location(&location);
5883 self.run_command(command);
5884 }
5885 }
5886
5887 #[cfg(any(android, desktop))]
5889 fn connect_dev(&mut self) {
5890 #[cfg(desktop)]
5891 {
5892 self.dev = crate::dev::address()
5893 .as_deref()
5894 .and_then(crate::dev::DevChannel::connect);
5895 crate::dev_commands::set_window(self.window.clone());
5896 if let Some(states) = crate::dev::restored_states() {
5897 self.runtime.states.restore_from(&states);
5898 }
5899 }
5900 #[cfg(android)]
5902 {
5903 let asked = self
5904 .android_app
5905 .as_ref()
5906 .and_then(|app| crate::android_link::launch_extra(app, crate::dev::DEV_EXTRA));
5907 #[cfg(feature = "hot-reload")]
5909 let asked = asked.or_else(|| crate::hot::under_engine().then(String::new));
5910 self.dev = asked.map(|_| crate::dev::DevChannel::through_log());
5911 if self.dev.is_some() {
5912 let storage = self
5913 .android_app
5914 .as_ref()
5915 .and_then(|app| app.internal_data_path());
5916 if let Some(states) = storage.as_deref().and_then(crate::dev::restored_states) {
5917 self.runtime.states.restore_from(&states);
5918 }
5919 if let Some(dir) = storage {
5920 crate::dev_commands::poll_commands(dir);
5921 }
5922 }
5923 }
5924 }
5925
5926 #[cfg(any(android, desktop))]
5929 fn report_dev(&mut self) {
5930 let Some(dev) = self.dev.as_mut() else {
5931 return;
5932 };
5933 #[cfg(desktop)]
5934 let window = self.window.as_ref().and_then(|window| {
5935 let at = window.outer_position().ok()?;
5936 let size = window.inner_size();
5937 Some([at.x, at.y, size.width as i32, size.height as i32])
5938 });
5939 #[cfg(android)]
5940 let window = None;
5941 if !dev.report(self.app.location(), window) {
5942 self.dev = None;
5943 return;
5944 }
5945 let states = &self.runtime.states;
5946 dev.save_if_asked(|| states.snapshot());
5947 }
5948
5949 #[cfg(desktop)]
5951 fn open_links_from_other_instances(&mut self) {
5952 for link in crate::instance::take_links() {
5953 if let Some(location) = crate::link::location_of_link(&link) {
5954 let command = self.app.open_location(&location);
5955 self.run_command(command);
5956 self.build_dirty = true;
5957 self.request_redraw();
5958 }
5959 }
5960 }
5961
5962 #[cfg(web)]
5965 fn open_launch_address(&mut self, window: &Arc<Window>) {
5966 let strategy = self.app.location_strategy();
5967 let base = crate::address::base();
5968 let (location, index) = crate::address::current(strategy, &base);
5969 self.history.opened(location.clone(), index);
5970 crate::address::listen(window.clone(), strategy, base.clone());
5971 self.address = (strategy, base);
5972 if let Some(location) = location {
5973 let command = self.app.open_location(&location);
5974 self.run_command(command);
5975 let (strategy, base) = &self.address;
5977 for (step, index) in self.history.seed(&self.app.location_stack()) {
5978 crate::address::apply(step, *strategy, base, index);
5979 }
5980 }
5981 }
5982
5983 #[cfg(web)]
5986 fn sync_address(&mut self) {
5987 if !self.started {
5988 return;
5989 }
5990 for (location, state) in crate::address::take_popped() {
5991 if self.history.popped(location.clone(), state) {
5992 let command = self.app.open_location(&location);
5993 self.run_command(command);
5994 self.build_dirty = true;
5995 self.request_redraw();
5998 }
5999 }
6000 if let Some(location) = self.app.location() {
6001 if let Some(step) = self
6002 .history
6003 .reflect(&location, self.app.location_replaces())
6004 {
6005 let (strategy, base) = &self.address;
6006 crate::address::apply(step, *strategy, base, self.history.index());
6007 }
6008 }
6009 }
6010
6011 fn run_command(&mut self, command: crate::command::Command<A::Message>) {
6028 let parts = command.into_parts();
6029 self.pending_focus.extend(parts.focus);
6030 self.pending_scroll.extend(parts.scrolls);
6031 self.pending_sheet.extend(parts.sheets);
6032 for task in parts.tasks {
6033 let proxy = self.proxy.clone();
6034 #[cfg(not(web))]
6035 std::thread::spawn(move || {
6036 if let Some(message) = task() {
6037 let _ = proxy.send_event(message);
6038 }
6039 });
6040 #[cfg(web)]
6041 wasm_bindgen_futures::spawn_local(async move {
6042 if let Some(message) = task() {
6043 let _ = proxy.send_event(message);
6044 }
6045 });
6046 }
6047 for future in parts.async_tasks {
6048 let proxy = self.proxy.clone();
6049 #[cfg(not(web))]
6053 crate::runtime::spawn(async move {
6054 if let Some(message) = future.await {
6055 let _ = proxy.send_event(message);
6056 }
6057 })
6058 .detach();
6059 #[cfg(web)]
6062 wasm_bindgen_futures::spawn_local(async move {
6063 if let Some(message) = future.await {
6064 let _ = proxy.send_event(message);
6065 }
6066 });
6067 }
6068 for (delay, message) in parts.timers {
6069 let proxy = self.proxy.clone();
6070 #[cfg(not(web))]
6071 crate::runtime::spawn(async move {
6072 crate::runtime::sleep(delay).await;
6073 let _ = proxy.send_event(message);
6074 })
6075 .detach();
6076 #[cfg(web)]
6077 web_timer::after(delay.as_millis() as i32, move || {
6078 let _ = proxy.send_event(message);
6079 });
6080 }
6081 }
6082
6083 fn sync_subscriptions(&mut self) {
6086 let entries = self.app.subscription().into_entries();
6087 let declared: std::collections::HashSet<u64> = entries.iter().map(|e| e.id).collect();
6088
6089 self.running_subs.retain(|id, _| declared.contains(id));
6091
6092 for entry in entries {
6094 if self.running_subs.contains_key(&entry.id) {
6095 continue;
6096 }
6097 let handle = self.start_subscription(entry.kind);
6098 self.running_subs.insert(entry.id, handle);
6099 }
6100 #[cfg(all(desktop, feature = "tray"))]
6102 self.sync_tray();
6103 #[cfg(all(desktop, feature = "taskbar"))]
6105 if let Some(window) = &self.window {
6106 let wanted = self.app.taskbar();
6107 self.taskbar = crate::taskbar::native::apply(window, &self.taskbar, &wanted);
6108 }
6109 #[cfg(all(desktop, feature = "global-shortcuts"))]
6111 self.sync_shortcuts();
6112 }
6113
6114 #[cfg(all(desktop, feature = "global-shortcuts"))]
6117 fn declared_shortcuts(&mut self) -> Vec<(String, crate::GlobalShortcut<A::Message>)> {
6118 let mut declared = Vec::new();
6119 for shortcut in self.app.global_shortcuts() {
6120 match crate::shortcuts::canonical(&shortcut.keys, cfg!(target_os = "macos")) {
6121 Ok(keys) => declared.push((keys, shortcut)),
6122 Err(why) => {
6123 if self.shortcuts_refused.insert(shortcut.keys.clone()) {
6124 log::warn!("global shortcuts: refused {why}");
6125 }
6126 }
6127 }
6128 }
6129 declared
6130 }
6131
6132 #[cfg(all(desktop, feature = "global-shortcuts"))]
6135 fn sync_shortcuts(&mut self) {
6136 let Some(window) = self.window.clone() else {
6137 return;
6138 };
6139 let wanted: Vec<String> = self
6140 .declared_shortcuts()
6141 .into_iter()
6142 .map(|(keys, _)| keys)
6143 .collect();
6144 crate::shortcuts::native::sync(&mut self.shortcuts, &wanted, &window);
6145 }
6146
6147 #[cfg(all(desktop, feature = "global-shortcuts"))]
6150 fn shortcut_presses(&mut self) {
6151 for keys in crate::shortcuts::native::take_pressed(&self.shortcuts) {
6152 let Some((_, shortcut)) = self
6153 .declared_shortcuts()
6154 .into_iter()
6155 .find(|(declared, _)| *declared == keys)
6156 else {
6157 continue;
6158 };
6159 match shortcut.action {
6160 crate::shortcuts::ShortcutAction::Message(message) => {
6161 self.dispatch(message);
6162 self.request_redraw();
6163 }
6164 crate::shortcuts::ShortcutAction::ShowWindow => {
6165 if let Some(window) = &self.window {
6166 window.set_visible(true);
6167 window.set_minimized(false);
6168 window.focus_window();
6169 window.request_redraw();
6170 }
6171 }
6172 }
6173 }
6174 }
6175
6176 #[cfg(all(desktop, feature = "tray"))]
6178 fn sync_tray(&mut self) {
6179 match self.app.tray() {
6180 None => {
6181 self.tray = None;
6182 self.tray_keeps_running = false;
6183 }
6184 Some(_) if self.window.is_none() => {}
6188 Some(tray) => {
6189 self.tray_keeps_running = tray.keep_running;
6190 let shape = tray.shape(&self.app.icon());
6191 crate::tray::native::sync(&mut self.tray, shape, self.window.as_ref());
6192 }
6193 }
6194 }
6195
6196 #[cfg(all(desktop, feature = "tray"))]
6199 fn tray_events(&mut self, event_loop: &ActiveEventLoop) {
6200 for event in crate::tray::native::take_events() {
6201 let Some(action) = self.app.tray().and_then(|tray| tray.action(event)) else {
6202 continue;
6203 };
6204 match action {
6205 crate::tray::TrayAction::Message(message) => {
6206 self.dispatch(message);
6207 self.request_redraw();
6208 }
6209 crate::tray::TrayAction::ShowWindow => {
6210 if let Some(window) = &self.window {
6211 window.set_visible(true);
6212 window.set_minimized(false);
6213 window.focus_window();
6214 window.request_redraw();
6215 }
6216 }
6217 crate::tray::TrayAction::Quit => event_loop.exit(),
6218 }
6219 }
6220 }
6221
6222 #[cfg(not(web))]
6231 fn start_subscription(&self, kind: crate::subscription::Kind<A::Message>) -> SubHandle {
6232 let proxy = self.proxy.clone();
6233 match kind {
6234 crate::subscription::Kind::Every { interval, make } => {
6235 crate::runtime::spawn(async move {
6236 loop {
6237 crate::runtime::sleep(interval).await;
6238 if proxy.send_event(make(Instant::now())).is_err() {
6241 break;
6242 }
6243 }
6244 })
6245 }
6246 }
6247 }
6248
6249 #[cfg(web)]
6253 fn start_subscription(&self, kind: crate::subscription::Kind<A::Message>) -> SubHandle {
6254 let proxy = self.proxy.clone();
6255 match kind {
6256 crate::subscription::Kind::Every { interval, make } => {
6257 let ms = interval.as_millis() as i32;
6258 web_timer::Interval::new(ms, move || {
6259 let _ = proxy.send_event(make(Instant::now()));
6260 })
6261 }
6262 }
6263 }
6264
6265 fn handle_drag(&mut self) {
6267 let Some(mut drag) = self.drag.take() else {
6268 return;
6269 };
6270 let slop = self.hit_slop();
6272 match &mut drag {
6273 Drag::Scrollbar {
6274 id,
6275 vertical,
6276 grab,
6277 track_start,
6278 track_len,
6279 thumb_len,
6280 max,
6281 reverse,
6282 } => {
6283 let along = if *vertical {
6284 self.cursor.y
6285 } else {
6286 self.cursor.x
6287 };
6288 let travel = (*track_len - *thumb_len).max(1.0);
6289 let thumb_start = (along - *grab).clamp(*track_start, *track_start + travel);
6290 let fraction = (thumb_start - *track_start) / travel;
6291 let fraction = if *reverse { 1.0 - fraction } else { fraction };
6294 let offset = (fraction * *max).clamp(0.0, *max);
6295 let entry = self.runtime.scroll.entry(*id).or_insert((0.0, 0.0));
6296 if *vertical {
6297 entry.1 = offset;
6298 } else {
6299 entry.0 = offset;
6300 }
6301 let synced = *entry;
6303 self.runtime.scroll_target.insert(*id, synced);
6304 self.runtime.scroll_velocity.remove(&*id);
6305 }
6306 Drag::RegionSelect { area } => {
6307 let area = *area;
6308 self.extend_region_selection(area);
6309 }
6310 Drag::RegionHandle { area, handle, grab } => {
6311 let (area, handle, grab) = (*area, *handle, *grab);
6312 self.drag_region_handle(area, handle, grab);
6313 }
6314 Drag::TextSelect { id, rect } => {
6315 let local_x = self.cursor.x - rect.x;
6316 let local_y = self.cursor.y - rect.y
6317 + self.runtime.scroll.get(id).map(|s| s.1).unwrap_or(0.0);
6318 let scroll_cursor = self.runtime.edits.get(id).map(|e| e.cursor).unwrap_or(0);
6320 let cursor = self
6321 .tree
6322 .as_ref()
6323 .and_then(|tree| find_widget(tree.as_ref(), *id))
6324 .and_then(|widget| {
6325 widget.cursor_at(local_x, local_y, rect.width, scroll_cursor)
6326 });
6327 if let Some(cursor) = cursor {
6328 let edit = self.runtime.edits.entry(*id).or_default();
6329 if edit.anchor.is_none() {
6330 edit.anchor = Some(edit.cursor);
6331 }
6332 edit.cursor = cursor;
6333 }
6334 }
6335 Drag::SelectionHandle {
6336 id,
6337 rect,
6338 handle,
6339 grab,
6340 } => {
6341 let local_x = self.cursor.x - rect.x + grab.x;
6344 let local_y = self.cursor.y - rect.y
6345 + grab.y
6346 + self.runtime.scroll.get(id).map(|s| s.1).unwrap_or(0.0);
6347 let edit = self.runtime.edits.get(id).copied().unwrap_or_default();
6348 let moved = self
6349 .tree
6350 .as_ref()
6351 .and_then(|tree| find_widget(tree.as_ref(), *id))
6352 .and_then(|widget| widget.cursor_at(local_x, local_y, rect.width, edit.cursor))
6353 .and_then(|to| crate::selection::drag(edit, *handle, to));
6354 if let Some(moved) = moved {
6355 self.runtime.edits.insert(*id, moved);
6356 }
6357 }
6358 Drag::Widget {
6359 id,
6360 rect,
6361 last_x,
6362 press_x,
6363 track,
6364 active,
6365 thumb,
6366 } => {
6367 let dx = self.cursor.x - *last_x;
6368 *last_x = self.cursor.x;
6369 if matches!(track.mode, frus_widgets::DragMode::Thumbs { .. }) {
6370 let rtl = self.is_rtl();
6371 let at = track.fraction(*rect, self.cursor.x, rtl);
6372 match *thumb {
6373 Some(picked) => {
6374 self.thumb_drag(*id, picked, frus_widgets::ThumbDrag::Move, at);
6375 }
6376 None => {
6378 let moved = self.cursor.x - *press_x;
6379 if let Some(picked) = track.pick_thumb(*rect, *press_x, moved, rtl) {
6380 *thumb = Some(picked);
6381 self.thumb_drag(*id, picked, frus_widgets::ThumbDrag::Start, at);
6382 self.thumb_drag(*id, picked, frus_widgets::ThumbDrag::Move, at);
6383 }
6384 }
6385 }
6386 } else if *active {
6387 let value = track.value_at(*rect, *press_x, self.cursor.x, true, self.is_rtl());
6388 self.apply_widget_drag(*id, dx, value);
6389 }
6390 }
6391 Drag::Reorder { start, moved, .. } => {
6392 let dx = self.cursor.x - start.x;
6394 let dy = self.cursor.y - start.y;
6395 if !*moved && (dx * dx + dy * dy) > slop * slop {
6396 *moved = true;
6397 }
6398 if *moved {
6400 self.request_redraw();
6401 }
6402 }
6403 Drag::Gesture {
6404 id,
6405 axis,
6406 start,
6407 last,
6408 moved,
6409 } => {
6410 let (id, axis) = (*id, *axis);
6411 if !*moved {
6412 let (tx, ty) = (self.cursor.x - start.x, self.cursor.y - start.y);
6416 let travelled = match axis {
6417 frus_widgets::PanAxis::Free => tx.hypot(ty),
6418 frus_widgets::PanAxis::Horizontal => tx.abs(),
6419 frus_widgets::PanAxis::Vertical => ty.abs(),
6420 };
6421 if travelled > slop {
6422 *moved = true;
6423 let local = self.pan_local(id, *start);
6424 self.send_pan(id, frus_widgets::PanEvent::Start { local });
6425 }
6426 }
6427 if *moved {
6428 let delta = mask_pan(
6429 axis,
6430 Point::new(self.cursor.x - last.x, self.cursor.y - last.y),
6431 );
6432 *last = self.cursor;
6433 self.track_gesture();
6434 if delta.x != 0.0 || delta.y != 0.0 {
6435 let local = self.pan_local(id, self.cursor);
6436 self.send_pan(id, frus_widgets::PanEvent::Update { local, delta });
6437 }
6438 }
6439 }
6440 Drag::Pan {
6441 id,
6442 last,
6443 moved,
6444 viewport,
6445 } => {
6446 let dx = self.cursor.x - last.x;
6447 let dy = self.cursor.y - last.y;
6448 if !*moved && (dx * dx + dy * dy) > slop * slop {
6449 *moved = true;
6450 }
6451 if *moved {
6452 let view = self.runtime.interactive.entry(*id).or_default();
6453 *view = view.pan(dx, dy).clamped(*viewport);
6455 *last = self.cursor;
6456 self.track_gesture();
6457 }
6458 }
6459 Drag::Item {
6460 source,
6461 start,
6462 moved,
6463 over,
6464 } => {
6465 let dx = self.cursor.x - start.x;
6466 let dy = self.cursor.y - start.y;
6467 if !*moved && (dx * dx + dy * dy) > slop * slop {
6470 *moved = true;
6471 }
6472 if !*moved {
6473 return;
6474 }
6475 let payload = source.payload;
6479 let candidate = self
6480 .ui
6481 .as_ref()
6482 .and_then(|ui| ui.drop_zone_at(self.cursor))
6483 .filter(|zone| {
6484 self.tree
6485 .as_ref()
6486 .and_then(|tree| find_widget(tree.as_ref(), zone.id))
6487 .is_some_and(|widget| widget.accepts_drag(payload))
6488 })
6489 .map(|zone| zone.id);
6490 if *over != candidate {
6491 *over = candidate;
6492 self.runtime.drag_over = candidate;
6493 }
6494 self.request_redraw();
6495 }
6496 Drag::Scroll {
6497 id,
6498 last,
6499 moved,
6500 carried,
6501 dismiss,
6502 axis,
6503 pan,
6504 } => {
6505 let dx = self.cursor.x - last.x;
6506 let dy = self.cursor.y - last.y;
6507 if !*moved && (dx * dx + dy * dy) > slop * slop {
6509 *moved = true;
6510 if let Some((target, pan_axis)) = pan.take() {
6521 let down = dy.abs() >= dx.abs();
6522 let wins = match pan_axis {
6523 frus_widgets::PanAxis::Horizontal => !down,
6524 frus_widgets::PanAxis::Vertical => down,
6525 frus_widgets::PanAxis::Free => {
6526 let chain: Vec<_> = self
6527 .ui
6528 .as_ref()
6529 .map(|ui| ui.scroll_chain(*last).collect())
6530 .unwrap_or_default();
6531 !chain.iter().any(|a| a.takes_user_scroll(down))
6534 }
6535 };
6536 if wins {
6537 self.runtime.release_scroll(*id);
6539 let start = *last;
6540 self.drag = Some(Drag::Gesture {
6541 id: target,
6542 axis: pan_axis,
6543 start,
6544 last: start,
6545 moved: false,
6546 });
6547 self.handle_drag();
6548 return;
6549 }
6550 }
6551 if let Some(item) = dismiss.take() {
6552 let along = if item.spec.axis.is_horizontal() {
6553 dx.abs() > dy.abs()
6554 } else {
6555 dy.abs() > dx.abs()
6556 };
6557 if along {
6558 self.runtime.release_scroll(*id);
6560 self.drag = Some(Drag::Dismiss {
6561 item,
6562 last: *last,
6563 moved: true,
6564 });
6565 self.handle_drag();
6566 return;
6567 }
6568 }
6569 let chain: Vec<_> = self
6579 .ui
6580 .as_ref()
6581 .map(|ui| ui.scroll_chain(*last).collect())
6582 .unwrap_or_default();
6583 let down = dy.abs() >= dx.abs();
6584 if let Some(area) = claim_area(&chain, *id, down) {
6585 if area.id != *id {
6586 self.runtime.release_scroll(*id);
6590 let physics = area.physics;
6591 *carried = self.runtime.catch_scroll_fling(area.id, physics);
6592 self.runtime.hold_scroll(area.id);
6593 *id = area.id;
6594 }
6595 *axis = Some(claim_axis(
6596 dx,
6597 dy,
6598 area.takes_user_scroll(false),
6599 area.takes_user_scroll(true),
6600 ));
6601 } else {
6602 *axis = Some(down);
6603 }
6604 }
6605 let (dx, mut dy) = match *axis {
6608 Some(true) => (0.0, dy),
6609 Some(false) => (dx, 0.0),
6610 None => (dx, dy),
6611 };
6612 if *moved && *axis == Some(true) {
6613 let sheet = self
6619 .ui
6620 .as_ref()
6621 .filter(|ui| ui.scroll_region(*id).is_some_and(|a| !a.reverse_y))
6622 .and_then(|ui| ui.sheet_holding(*id))
6623 .filter(|sheet| sheet.available > 0.0)
6624 .cloned();
6625 if let Some(sheet) = sheet {
6626 let px = sheet.available;
6627 let offset = self.runtime.scroll.get(id).map_or(0.0, |o| o.1);
6628 let size = self.runtime.sheet_size(sheet.id, &sheet.spec) * px;
6629 let (grown, listed) = frus_widgets::split_sheet_drag(
6630 -dy,
6631 offset,
6632 size,
6633 sheet.spec.floor() * px,
6634 sheet.spec.max * px,
6635 );
6636 if grown != 0.0 {
6637 self.runtime
6638 .sheet_drag(sheet.id, &sheet.spec, grown / px, px);
6639 }
6640 dy = -listed;
6641 }
6642 }
6643 if *moved {
6644 let area = self.ui.as_ref().and_then(|u| u.scroll_region(*id));
6645 let physics = area.map(|a| a.physics).unwrap_or_else(|| {
6647 self.app
6648 .scroll_behavior()
6649 .scroll_physics(&self.themes.displayed(&self.app))
6650 });
6651 let cur = self.runtime.scroll.get(id).copied().unwrap_or((0.0, 0.0));
6652 let axis = |metrics: frus_widgets::ScrollMetrics, delta: f32| {
6659 let applied = physics.apply_user_offset(metrics, delta);
6660 let proposed = metrics.pixels + applied;
6661 let refused = physics.apply_boundary_conditions(metrics, proposed);
6662 (proposed - refused, refused)
6663 };
6664 let (nx, ny, refused) = match area {
6665 Some(area) => {
6666 let (ddx, ddy) = area.offset_delta((dx, dy));
6667 let (nx, rx) = axis(area.metrics_x(cur.0), ddx);
6668 let (ny, ry) = axis(area.metrics_y(cur.1), ddy);
6669 (nx, ny, Some((area, rx, ry)))
6670 }
6671 None => (cur.0 - dx, cur.1 - dy, None),
6672 };
6673 if let Some((area, rx, ry)) = refused {
6674 if let Some(host) = area.refresh {
6680 self.feed_refresh(host, physics, area.viewport.height, cur.1, ny, ry);
6681 }
6682 let cursor = self.cursor;
6683 for (refused, vertical, extent) in [
6684 (rx, false, area.viewport.width),
6685 (ry, true, area.viewport.height),
6686 ] {
6687 if refused.abs() < 1e-3 {
6688 continue;
6689 }
6690 let edge = area.refused_edge(vertical, refused);
6691 if area.refresh.is_some() && edge == area.start_edge(vertical) {
6694 continue;
6695 }
6696 let (offset, cross) =
6697 frus_widgets::glow_cross_axis(area.viewport, edge, cursor);
6698 self.runtime.glow_pull(
6699 area.id,
6700 area.overscroll,
6701 edge,
6702 refused,
6703 extent,
6704 offset,
6705 cross,
6706 );
6707 }
6708 if rx.abs() < 1e-3 && ry.abs() < 1e-3 && (nx, ny) != cur {
6711 self.runtime.stretch_release(area.id);
6712 }
6713 }
6714 self.runtime.scroll.insert(*id, (nx, ny));
6715 self.runtime.scroll_target.insert(*id, (nx, ny));
6716 self.runtime.scroll_velocity.remove(id);
6717 *last = self.cursor;
6718 self.track_gesture();
6719 }
6720 }
6721 Drag::Dismiss { item, last, moved } => {
6722 let dx = self.cursor.x - last.x;
6723 let dy = self.cursor.y - last.y;
6724 if !*moved && (dx * dx + dy * dy) > slop * slop {
6725 *moved = true;
6726 }
6727 if *moved {
6728 let delta = if item.spec.axis.is_horizontal() {
6729 dx
6730 } else {
6731 dy
6732 };
6733 self.runtime
6734 .dismiss_drag(item.id, delta, item.extent(), item.spec.axis);
6735 *last = self.cursor;
6736 self.track_gesture();
6737 }
6738 }
6739 Drag::Sheet {
6740 id,
6741 last,
6742 moved,
6743 available,
6744 } => {
6745 let dx = self.cursor.x - last.x;
6746 let dy = self.cursor.y - last.y;
6747 if !*moved && (dx * dx + dy * dy) > slop * slop {
6748 *moved = true;
6749 }
6750 if *moved && *available > 0.0 {
6751 let spec = self
6752 .ui
6753 .as_ref()
6754 .and_then(|ui| ui.sheet(*id))
6755 .map(|sheet| sheet.spec.clone());
6756 if let Some(spec) = spec {
6757 self.runtime
6759 .sheet_drag(*id, &spec, -dy / *available, *available);
6760 }
6761 *last = self.cursor;
6762 self.track_gesture();
6763 }
6764 }
6765 Drag::Back { start_x } => {
6766 let width = self.logical_width();
6767 let sign = if self.is_rtl() { -1.0 } else { 1.0 };
6770 let progress = (sign * (self.cursor.x - *start_x) / width).clamp(0.0, 1.0);
6771 self.track_gesture();
6772 self.build_dirty = true;
6773 self.app.back_gesture(progress);
6774 }
6775 }
6776 self.drag = Some(drag);
6777 self.request_redraw();
6778 }
6779
6780 fn refresh_host_of(&self, scrollable: WidgetId) -> Option<WidgetId> {
6782 self.ui
6783 .as_ref()
6784 .and_then(|ui| ui.scroll_region(scrollable))
6785 .and_then(|area| area.refresh)
6786 }
6787
6788 fn release_refresh(&mut self, scrollable: WidgetId) {
6791 let Some(host) = self.refresh_host_of(scrollable) else {
6792 return;
6793 };
6794 if !self.runtime.refresh_release(host) {
6795 return;
6796 }
6797 let message = self
6798 .tree
6799 .as_ref()
6800 .and_then(|tree| find_widget(tree.as_ref(), host))
6801 .and_then(|widget| widget.on_refresh());
6802 if let Some(message) = message {
6803 self.dispatch(message);
6804 }
6805 }
6806
6807 fn feed_refresh(
6826 &mut self,
6827 host: WidgetId,
6828 physics: frus_widgets::ScrollPhysics,
6829 extent: f32,
6830 before: f32,
6831 after: f32,
6832 refused_y: f32,
6833 ) {
6834 if after > 0.5 {
6835 self.runtime.refresh_cancel(host);
6836 return;
6837 }
6838 let pulled = if physics.allows_overscroll() {
6839 (-after).max(0.0) - (-before).max(0.0)
6840 } else {
6841 -refused_y
6842 };
6843 if pulled.abs() > 1e-3 {
6844 self.runtime.refresh_pull(host, pulled, extent);
6845 }
6846 }
6847
6848 fn hit_slop(&self) -> f32 {
6851 if self.pointer_touch {
6852 TOUCH_SLOP
6853 } else {
6854 PRECISE_SLOP
6855 }
6856 }
6857
6858 fn gesture_now(&self) -> f32 {
6861 match self.gesture_clock {
6862 Some((now, start)) => now - start,
6864 None => (Instant::now() - self.gesture_start).as_secs_f32(),
6865 }
6866 }
6867
6868 fn begin_gesture(&mut self) {
6874 self.gesture_velocity = self
6875 .app
6876 .scroll_behavior()
6877 .velocity_tracker(&self.themes.displayed(&self.app));
6878 self.gesture_start = Instant::now();
6879 if let Some((now, start)) = self.gesture_clock.as_mut() {
6880 *start = *now;
6881 }
6882 self.track_gesture();
6883 }
6884
6885 fn track_gesture(&mut self) {
6887 let now = self.gesture_now();
6888 self.gesture_velocity.add_position(now, self.cursor);
6889 }
6890
6891 fn gesture_estimate(&self) -> VelocityEstimate {
6893 self.gesture_velocity
6894 .estimate(self.gesture_now())
6895 .unwrap_or(VelocityEstimate::STILL)
6896 }
6897
6898 fn fling_velocity(&self, estimate: VelocityEstimate) -> (f32, f32) {
6900 fling_velocity(estimate, self.hit_slop())
6901 }
6902
6903 fn fling(&mut self, id: WidgetId, velocity: (f32, f32)) {
6907 let Some(area) = self.ui.as_ref().and_then(|ui| ui.scroll_region(id)) else {
6908 return;
6909 };
6910 let physics = area.physics;
6911 if self.runtime.fling_scroll(area, physics, velocity) {
6915 self.request_redraw();
6916 }
6917 }
6918
6919 fn reorderable_at(&self, point: Point) -> Option<(WidgetId, usize, bool)> {
6923 let id = self.ui.as_ref()?.reorderable_at(point)?;
6924 let widget = self
6925 .tree
6926 .as_ref()
6927 .and_then(|tree| find_widget(tree.as_ref(), id))?;
6928 if !widget.reorder_draggable() {
6931 return None;
6932 }
6933 let from = widget.reorder_index()?;
6934 Some((id, from, widget.drag_needs_long_press()))
6935 }
6936
6937 fn reorder_source(&self, id: WidgetId, from: usize) -> Option<(WidgetId, Rect)> {
6949 let ui = self.ui.as_ref()?;
6950 let tree = self.tree.as_ref()?;
6951 let own = ui.widget_rect(id)?;
6952 let grabbed = find_widget(tree.as_ref(), id)?;
6953 if grabbed.reorder_droppable() {
6954 return Some((id, own));
6955 }
6956 let middle = Point::new(own.x + own.width * 0.5, own.y + own.height * 0.5);
6957 let slot = ui.reorderables_at(middle).find(|other| {
6958 find_widget(tree.as_ref(), *other)
6959 .is_some_and(|w| w.reorder_droppable() && w.reorder_index() == Some(from))
6960 })?;
6961 Some((slot, ui.widget_rect(slot).unwrap_or(own)))
6965 }
6966
6967 fn reorder_carried(&self) -> Option<(WidgetId, Rect)> {
6971 match &self.drag {
6972 Some(Drag::Reorder {
6973 carried: Some(carried),
6974 ..
6975 }) => Some(*carried),
6976 Some(Drag::Reorder { id, from, .. }) => self.reorder_source(*id, *from),
6977 _ => None,
6978 }
6979 }
6980
6981 fn keep_carried_box(&mut self) {
6986 let source = match &self.drag {
6987 Some(Drag::Reorder {
6988 id,
6989 from,
6990 moved: true,
6991 carried: None,
6992 ..
6993 }) => self.reorder_source(*id, *from),
6994 _ => return,
6995 };
6996 if let Some(Drag::Reorder { carried, .. }) = self.drag.as_mut() {
6997 *carried = source;
6998 }
6999 }
7000
7001 fn reorder_target_at(&self, point: Point) -> Option<WidgetId> {
7005 let ui = self.ui.as_ref()?;
7006 let tree = self.tree.as_ref()?;
7007 ui.reorderables_at(point)
7008 .find(|id| find_widget(tree.as_ref(), *id).is_some_and(|w| w.reorder_droppable()))
7009 }
7010
7011 fn reorder_drop_target(
7020 &self,
7021 id: WidgetId,
7022 from: usize,
7023 carried: Option<(WidgetId, Rect)>,
7024 ) -> Option<WidgetId> {
7025 if let Some(target) = self.reorder_target_at(self.cursor) {
7026 return Some(target);
7027 }
7028 let tree = self.tree.as_ref()?;
7029 let Some(ReorderMotion::Slots(axis)) = find_widget(tree.as_ref(), id).map(reorder_motion)
7030 else {
7031 return None;
7032 };
7033 let row = match carried {
7036 Some((row, _)) => row,
7037 None => self.reorder_source(id, from)?.0,
7038 };
7039 let slots = reorder_siblings(self.ui.as_ref()?, tree.as_ref(), row);
7040 let boxes: Vec<Rect> = slots.iter().map(|(_, rect)| *rect).collect();
7041 nearest_reorder_slot(self.cursor, &boxes, axis).map(|index| slots[index].0)
7042 }
7043
7044 fn carried_rect(&self) -> Option<Rect> {
7051 match &self.drag {
7052 Some(Drag::Reorder {
7053 start, moved: true, ..
7054 }) => {
7055 let (_, src) = self.reorder_carried()?;
7056 let (gx, gy) = ghost_offset(
7059 self.dragged_reorder_motion()
7060 .unwrap_or(ReorderMotion::Columns),
7061 (self.cursor.x - start.x, self.cursor.y - start.y),
7062 );
7063 Some(src.translate(gx, gy))
7064 }
7065 Some(Drag::Item {
7066 source,
7067 start,
7068 moved: true,
7069 ..
7070 }) => Some(
7071 source
7072 .rect
7073 .translate(self.cursor.x - start.x, self.cursor.y - start.y),
7074 ),
7075 _ => None,
7076 }
7077 }
7078
7079 fn autoscroll_carried(&mut self, dt: f32) -> bool {
7087 self.keep_carried_box();
7088 let Some(carried) = self.carried_rect() else {
7089 return false;
7090 };
7091 let area = {
7092 let scroll = &self.runtime.scroll;
7093 self.ui.as_ref().and_then(|ui| {
7094 ui.scroll_chain(self.cursor).find(|area| {
7095 area.accepts_user_offset(scroll.get(&area.id).copied().unwrap_or((0.0, 0.0)))
7096 })
7097 })
7098 };
7099 let Some(area) = area else {
7100 return false;
7101 };
7102 let offset = self
7103 .runtime
7104 .scroll
7105 .get(&area.id)
7106 .copied()
7107 .unwrap_or((0.0, 0.0));
7108 let room = |offset: f32, max: f32, reverse: bool| {
7111 if reverse {
7112 (max - offset, offset)
7113 } else {
7114 (offset, max - offset)
7115 }
7116 };
7117 let view = area.viewport;
7118 let dx = edge_autoscroll(
7119 (carried.x, carried.x + carried.width),
7120 (view.x, view.x + view.width),
7121 room(offset.0, area.max_x, area.reverse_x),
7122 dt,
7123 )
7124 .unwrap_or(0.0);
7125 let dy = edge_autoscroll(
7126 (carried.y, carried.y + carried.height),
7127 (view.y, view.y + view.height),
7128 room(offset.1, area.max_y, area.reverse_y),
7129 dt,
7130 )
7131 .unwrap_or(0.0);
7132 if dx == 0.0 && dy == 0.0 {
7133 return false;
7134 }
7135 let delta = area.offset_delta((dx, dy));
7138 let next = (
7139 (offset.0 + delta.0).clamp(0.0, area.max_x),
7140 (offset.1 + delta.1).clamp(0.0, area.max_y),
7141 );
7142 if next == offset {
7143 return false;
7144 }
7145 self.runtime.scroll.insert(area.id, next);
7146 self.runtime.scroll_target.insert(area.id, next);
7149 self.runtime.scroll_velocity.remove(&area.id);
7150 if let Some(drag) = self.drag.as_mut() {
7156 follow_content(
7157 drag,
7158 area.offset_delta((next.0 - offset.0, next.1 - offset.1)),
7159 );
7160 }
7161 true
7162 }
7163
7164 fn dragged_reorder_motion(&self) -> Option<ReorderMotion> {
7168 let Some(Drag::Reorder { id, .. }) = self.drag else {
7169 return None;
7170 };
7171 self.tree
7172 .as_ref()
7173 .and_then(|t| find_widget(t.as_ref(), id))
7174 .map(reorder_motion)
7175 }
7176
7177 fn advance_reorder_springs(&mut self, dt: f32) -> bool {
7180 let reorder_motion = matches!(self.drag, Some(Drag::Reorder { moved: true, .. }))
7181 .then(|| self.dragged_reorder_motion())
7182 .flatten();
7183 match reorder_motion {
7184 Some(ReorderMotion::Columns) => {
7185 self.reorder_x = spring_toward(self.reorder_x, self.cursor.x, dt, 0.07);
7186 (self.cursor.x - self.reorder_x).abs() > 0.5
7187 }
7188 Some(ReorderMotion::Slots(ReorderAxis::Vertical)) => {
7189 match self.reorder_drop_line(drag_preview::INSERT_THICKNESS) {
7190 Some(target) => {
7191 self.reorder_y = spring_toward(self.reorder_y, target.y, dt, 0.07);
7192 (target.y - self.reorder_y).abs() > 0.5
7193 }
7194 None => false,
7195 }
7196 }
7197 Some(ReorderMotion::Slots(ReorderAxis::Horizontal)) => {
7198 match self.reorder_drop_line(drag_preview::INSERT_THICKNESS) {
7199 Some(target) => {
7200 self.reorder_x = spring_toward(self.reorder_x, target.x, dt, 0.07);
7201 (target.x - self.reorder_x).abs() > 0.5
7202 }
7203 None => false,
7204 }
7205 }
7206 None => false,
7207 }
7208 }
7209
7210 fn report_overflows(&self, ui: &Ui<A::Message>) {
7226 for o in ui.overflows() {
7227 let mut key = std::collections::hash_map::DefaultHasher::new();
7231 use std::hash::{Hash, Hasher};
7232 (
7233 o.rect.width.to_bits(),
7234 o.rect.height.to_bits(),
7235 o.side as u8,
7236 o.amount.round() as i32,
7237 )
7238 .hash(&mut key);
7239 if !self.reported_overflows.borrow_mut().insert(key.finish()) {
7240 continue;
7241 }
7242 let side = match o.side {
7243 frus_widgets::Side::Left => "left",
7244 frus_widgets::Side::Right => "right",
7245 frus_widgets::Side::Top => "top",
7246 frus_widgets::Side::Bottom => "bottom",
7247 };
7248 log::warn!(
7249 "a box {:.0}x{:.0} is overflowed by {:.0} px on the {side}: its children do not \
7250 fit inside it, and what runs past the edge is drawn outside its parent — \
7251 invisible where something clips it, and untappable where it leaves the window. \
7252 Give the child that should give way an `Expanded`, or put the content in a \
7253 `SingleChildScrollView`.",
7254 o.rect.width,
7255 o.rect.height,
7256 o.amount,
7257 );
7258 }
7259 }
7260
7261 fn keystroke(&self, event: &KeyDown) -> Option<KeyStroke> {
7267 let key = match &event.logical_key {
7268 WinitKey::Named(NamedKey::Enter) => ShortcutKey::Enter,
7269 WinitKey::Named(NamedKey::Escape) => ShortcutKey::Escape,
7270 WinitKey::Named(NamedKey::Tab) => ShortcutKey::Tab,
7271 WinitKey::Named(NamedKey::Space) => ShortcutKey::Space,
7272 WinitKey::Named(NamedKey::Backspace) => ShortcutKey::Backspace,
7273 WinitKey::Named(NamedKey::Delete) => ShortcutKey::Delete,
7274 WinitKey::Named(NamedKey::ArrowUp) => ShortcutKey::Up,
7275 WinitKey::Named(NamedKey::ArrowDown) => ShortcutKey::Down,
7276 WinitKey::Named(NamedKey::ArrowLeft) => ShortcutKey::Left,
7277 WinitKey::Named(NamedKey::ArrowRight) => ShortcutKey::Right,
7278 WinitKey::Named(NamedKey::Home) => ShortcutKey::Home,
7279 WinitKey::Named(NamedKey::End) => ShortcutKey::End,
7280 WinitKey::Named(NamedKey::PageUp) => ShortcutKey::PageUp,
7281 WinitKey::Named(NamedKey::PageDown) => ShortcutKey::PageDown,
7282 WinitKey::Named(NamedKey::F1) => ShortcutKey::F(1),
7283 WinitKey::Named(NamedKey::F2) => ShortcutKey::F(2),
7284 WinitKey::Named(NamedKey::F3) => ShortcutKey::F(3),
7285 WinitKey::Named(NamedKey::F4) => ShortcutKey::F(4),
7286 WinitKey::Named(NamedKey::F5) => ShortcutKey::F(5),
7287 WinitKey::Named(NamedKey::F6) => ShortcutKey::F(6),
7288 WinitKey::Named(NamedKey::F7) => ShortcutKey::F(7),
7289 WinitKey::Named(NamedKey::F8) => ShortcutKey::F(8),
7290 WinitKey::Named(NamedKey::F9) => ShortcutKey::F(9),
7291 WinitKey::Named(NamedKey::F10) => ShortcutKey::F(10),
7292 WinitKey::Named(NamedKey::F11) => ShortcutKey::F(11),
7293 WinitKey::Character(text) => ShortcutKey::Char(text.chars().next()?),
7294 _ => return None,
7295 };
7296 Some(KeyStroke {
7297 key,
7298 ctrl: self.ctrl,
7299 shift: self.shift,
7300 alt: self.alt,
7301 meta: self.meta,
7302 })
7303 }
7304
7305 fn paint_reorder_preview(&self, ui: &Ui<A::Message>, theme: &Theme, scene: &mut Scene) {
7306 let Some(Drag::Reorder {
7307 start, moved: true, ..
7308 }) = self.drag
7309 else {
7310 return;
7311 };
7312 let Some((id, src)) = self.reorder_carried() else {
7315 return;
7316 };
7317 let motion = self
7318 .tree
7319 .as_ref()
7320 .and_then(|t| find_widget(t.as_ref(), id))
7321 .map(reorder_motion)
7322 .unwrap_or(ReorderMotion::Columns);
7323 let dx = self.cursor.x - start.x;
7324 let dy = self.cursor.y - start.y;
7325
7326 let (gx, gy) = ghost_offset(motion, (dx, dy));
7328
7329 let owners: std::collections::HashSet<u64> = self
7333 .tree
7334 .as_ref()
7335 .and_then(|t| find_widget(t.as_ref(), id))
7336 .map(|w| subtree_ids(w, id).iter().map(|i| i.as_u64()).collect())
7337 .unwrap_or_else(|| std::iter::once(id.as_u64()).collect());
7338
7339 match motion {
7340 ReorderMotion::Columns => {
7341 let reflowed =
7345 reflow_reorder_columns(scene.primitives(), src, self.reorder_x, id.as_u64());
7346 scene.clear();
7347 for primitive in reflowed {
7348 scene.push_primitive(primitive);
7349 }
7350 }
7351 ReorderMotion::Slots(axis) => {
7352 let line =
7363 self.reorder_drop_line(drag_preview::INSERT_THICKNESS)
7364 .map(|r| match axis {
7365 ReorderAxis::Vertical => Rect {
7366 y: self.reorder_y,
7367 ..r
7368 },
7369 ReorderAxis::Horizontal => Rect {
7370 x: self.reorder_x,
7371 ..r
7372 },
7373 });
7374 let movable = self
7380 .tree
7381 .as_ref()
7382 .map(|tree| reorderable_owners(ui, tree.as_ref()))
7383 .unwrap_or_default();
7384 let reflowed =
7385 reflow_reorder_cards(scene.primitives(), src, line, &owners, &movable, axis);
7386 scene.clear();
7387 for primitive in reflowed {
7388 scene.push_primitive(primitive);
7389 }
7390 if let Some(line) = line {
7391 let thickness = match axis {
7393 ReorderAxis::Vertical => line.height,
7394 ReorderAxis::Horizontal => line.width,
7395 };
7396 scene.set_clip(Rect::UNBOUNDED);
7397 scene.draw_rect(
7398 line,
7399 theme.primary,
7400 theme.radius.min(thickness * 0.5),
7401 0.0,
7402 Color::TRANSPARENT,
7403 );
7404 }
7405 }
7406 }
7407
7408 let ghost: Vec<Primitive> = ui
7414 .scene()
7415 .primitives()
7416 .iter()
7417 .filter(|p| owners.contains(&p.owner()))
7418 .map(|p| p.translated(gx, gy).with_clip(Rect::UNBOUNDED))
7419 .collect();
7420 draw_ghost_card(scene, theme, src.translate(gx, gy), &ghost);
7421 }
7422
7423 fn paint_drag_ghost(&self, ui: &Ui<A::Message>, theme: &Theme, scene: &mut Scene) {
7431 let Some(Drag::Item {
7432 source,
7433 start,
7434 moved: true,
7435 ..
7436 }) = &self.drag
7437 else {
7438 return;
7439 };
7440 let widget = self
7441 .tree
7442 .as_ref()
7443 .and_then(|tree| find_widget(tree.as_ref(), source.id));
7444 let owners: std::collections::HashSet<u64> = widget
7448 .map(|w| {
7449 subtree_ids(w, source.id)
7450 .iter()
7451 .map(|i| i.as_u64())
7452 .collect()
7453 })
7454 .unwrap_or_else(|| std::iter::once(source.id.as_u64()).collect());
7455 let opacity = widget.map(|w| w.drag_ghost_opacity()).unwrap_or(1.0);
7456 let dx = self.cursor.x - start.x;
7457 let dy = self.cursor.y - start.y;
7458
7459 let original: Vec<Primitive> = scene.primitives().to_vec();
7461 scene.clear();
7462 for primitive in &original {
7463 if owners.contains(&primitive.owner()) {
7464 scene.push_faded(primitive, opacity);
7465 } else {
7466 scene.push_primitive(primitive.clone());
7467 }
7468 }
7469
7470 let ghost: Vec<Primitive> = ui
7471 .scene()
7472 .primitives()
7473 .iter()
7474 .filter(|p| owners.contains(&p.owner()))
7475 .map(|p| p.translated(dx, dy).with_clip(Rect::UNBOUNDED))
7476 .collect();
7477 draw_ghost_card(scene, theme, source.rect.translate(dx, dy), &ghost);
7478 }
7479
7480 fn reorder_drop_line(&self, thickness: f32) -> Option<Rect> {
7486 let Some(Drag::Reorder {
7487 id, from, carried, ..
7488 }) = self.drag
7489 else {
7490 return None;
7491 };
7492 let target = self.reorder_drop_target(id, from, carried)?;
7495 let rect = self.ui.as_ref()?.widget_rect(target)?;
7496 let axis = match self
7497 .tree
7498 .as_ref()
7499 .and_then(|t| find_widget(t.as_ref(), target))
7500 .map(reorder_motion)
7501 {
7502 Some(ReorderMotion::Slots(axis)) => axis,
7503 _ => ReorderAxis::Vertical,
7504 };
7505 Some(drop_insertion_line(
7506 rect,
7507 thickness,
7508 self.reorder_insert_after(target, rect),
7509 axis,
7510 self.is_rtl(),
7511 ))
7512 }
7513
7514 fn reorder_insert_after(&self, target: WidgetId, rect: Rect) -> bool {
7520 match self
7521 .tree
7522 .as_ref()
7523 .and_then(|t| find_widget(t.as_ref(), target))
7524 .map(reorder_motion)
7525 {
7526 Some(ReorderMotion::Slots(axis)) => {
7527 reorder_drop_after(self.cursor, rect, axis, self.is_rtl())
7528 }
7529 _ => false,
7530 }
7531 }
7532
7533 fn pan_target(&self) -> Option<(WidgetId, frus_widgets::PanAxis)> {
7535 let (id, _) = self.ui.as_ref()?.pan_at(self.cursor)?;
7536 let axis = find_widget(self.tree.as_ref()?.as_ref(), id)?.pan_axis()?;
7537 Some((id, axis))
7538 }
7539
7540 fn pan_local(&self, id: WidgetId, at: Point) -> Point {
7542 let origin = self
7543 .ui
7544 .as_ref()
7545 .and_then(|ui| ui.widget_rect(id))
7546 .map_or(Point::new(0.0, 0.0), |rect| Point::new(rect.x, rect.y));
7547 Point::new(at.x - origin.x, at.y - origin.y)
7548 }
7549
7550 fn send_pan(&mut self, id: WidgetId, event: frus_widgets::PanEvent) {
7552 let rtl = self.is_rtl();
7553 let value = self.runtime.value(id);
7554 let (message, moved, released) = match self
7555 .tree
7556 .as_ref()
7557 .and_then(|tree| find_widget(tree.as_ref(), id))
7558 {
7559 Some(widget) => {
7560 let moved = widget.pan_value(event, value, rtl);
7564 let released = match event {
7565 frus_widgets::PanEvent::End { .. } if self.runtime.value_placed(id) => {
7566 Some(widget.on_value_release(value))
7567 }
7568 _ => None,
7569 };
7570 (widget.on_pan(event), moved, released)
7571 }
7572 None => (None, None, None),
7573 };
7574 if let Some(v) = moved {
7575 self.runtime.hold_value(id, v);
7576 }
7577 if let Some(release) = released {
7578 self.runtime.release_value(id);
7579 if let Some(message) = release {
7580 self.dispatch(message);
7581 }
7582 }
7583 if let Some(message) = message {
7584 self.dispatch(message);
7585 }
7586 self.request_redraw();
7587 }
7588
7589 fn dispatch_drag_edge(&mut self, id: WidgetId, edge: Edge, fraction: f32) {
7597 let message = self
7598 .tree
7599 .as_ref()
7600 .and_then(|tree| find_widget(tree.as_ref(), id))
7601 .and_then(|widget| match edge {
7602 Edge::Start => widget.on_drag_start(fraction),
7603 Edge::End => widget.on_drag_end(fraction),
7604 });
7605 if let Some(message) = message {
7606 self.dispatch(message);
7607 }
7608 }
7609
7610 fn thumb_drag(
7612 &mut self,
7613 id: WidgetId,
7614 thumb: usize,
7615 moment: frus_widgets::ThumbDrag,
7616 fraction: f32,
7617 ) {
7618 if moment == frus_widgets::ThumbDrag::Start {
7619 self.runtime.input.thumb = Some((id, thumb));
7621 }
7622 let message = self
7623 .tree
7624 .as_ref()
7625 .and_then(|tree| find_widget(tree.as_ref(), id))
7626 .and_then(|widget| widget.on_thumb_drag(thumb, moment, fraction));
7627 if let Some(message) = message {
7628 self.dispatch(message);
7629 }
7630 }
7631
7632 fn apply_widget_drag(&mut self, id: WidgetId, dx: f32, value: Option<f32>) {
7635 let message = self
7636 .tree
7637 .as_ref()
7638 .and_then(|tree| find_widget(tree.as_ref(), id))
7639 .and_then(|widget| {
7640 widget
7641 .on_drag_delta(dx)
7642 .or_else(|| value.and_then(|v| widget.on_drag(v)))
7643 });
7644 if let Some(message) = message {
7645 self.dispatch(message);
7646 }
7647 }
7648
7649 fn key_down(&mut self, event: &KeyDown, event_loop: Option<&ActiveEventLoop>) {
7654 #[cfg(web)]
7659 self.drain_web_editing();
7660 if !self.runtime.focus_visible {
7662 self.runtime.focus_visible = true;
7663 self.request_redraw();
7664 }
7665
7666 if matches!(
7669 event.logical_key,
7670 WinitKey::Named(NamedKey::BrowserBack) | WinitKey::Named(NamedKey::GoBack)
7671 ) {
7672 if !event.repeat {
7673 if let Some(event_loop) = event_loop {
7674 self.system_back(event_loop);
7675 }
7676 }
7677 return;
7678 }
7679
7680 if matches!(event.logical_key, WinitKey::Named(NamedKey::F12)) {
7683 if cfg!(debug_assertions) && !event.repeat {
7684 self.inspector = !self.inspector;
7685 self.inspector_dump = self.inspector;
7686 self.request_redraw();
7687 }
7688 return;
7689 }
7690
7691 if let Some(stroke) = self.keystroke(event) {
7699 let typing = !stroke.is_command() && self.runtime.input.focused.is_some();
7700 if !typing {
7701 let msgs = self
7702 .ui
7703 .as_ref()
7704 .map(|ui| ui.keystroke(stroke, self.runtime.input.focused))
7705 .unwrap_or_default();
7706 if !msgs.is_empty() {
7707 for msg in msgs {
7708 self.dispatch(msg);
7709 }
7710 self.request_redraw();
7711 return;
7712 }
7713 }
7714 }
7715
7716 if matches!(event.logical_key, WinitKey::Named(NamedKey::Tab)) {
7718 let forward = !self.shift;
7719 let next = self
7720 .ui
7721 .as_ref()
7722 .and_then(|ui| ui.focus_next(self.runtime.input.focused, forward));
7723 if next.is_some() {
7724 self.runtime.input.focused = next;
7725 self.runtime.region = None;
7726 self.reveal_focus();
7727 self.request_redraw();
7728 }
7729 return;
7730 }
7731
7732 if matches!(event.logical_key, WinitKey::Named(NamedKey::Escape)) {
7736 if !event.repeat {
7738 self.escape();
7739 }
7740 return;
7741 }
7742
7743 let Some(focused) = self.runtime.input.focused else {
7744 self.region_key(event);
7747 return;
7748 };
7749
7750 let arrow = match event.logical_key {
7754 WinitKey::Named(NamedKey::ArrowUp) => Some(FocusDirection::Up),
7755 WinitKey::Named(NamedKey::ArrowDown) => Some(FocusDirection::Down),
7756 WinitKey::Named(NamedKey::ArrowLeft) => Some(FocusDirection::Left),
7757 WinitKey::Named(NamedKey::ArrowRight) => Some(FocusDirection::Right),
7758 _ => None,
7759 };
7760 if matches!(
7763 event.logical_key,
7764 WinitKey::Named(NamedKey::PageUp) | WinitKey::Named(NamedKey::PageDown)
7765 ) {
7766 let down = matches!(event.logical_key, WinitKey::Named(NamedKey::PageDown));
7767 if self.move_caret_vertical(focused, down, true) {
7768 return;
7769 }
7770 }
7771
7772 if let Some(direction) = arrow {
7773 if matches!(direction, FocusDirection::Up | FocusDirection::Down) {
7777 let down = matches!(direction, FocusDirection::Down);
7778 if self.move_caret_vertical(focused, down, false) {
7779 return;
7780 }
7781 }
7782
7783 if matches!(direction, FocusDirection::Left | FocusDirection::Right) {
7787 let key = if matches!(direction, FocusDirection::Left) {
7788 Key::Left {
7789 shift: self.shift,
7790 word: self.ctrl,
7791 }
7792 } else {
7793 Key::Right {
7794 shift: self.shift,
7795 word: self.ctrl,
7796 }
7797 };
7798 let widget = self
7799 .tree
7800 .as_ref()
7801 .and_then(|tree| find_widget(tree.as_ref(), focused));
7802 let reorder_from = widget.and_then(|w| w.reorder_index());
7806 let handled = widget.map(|w| w.on_key(&key));
7807 if let Some(KeyResponse::Handled(message)) = handled {
7808 if let Some(message) = message {
7809 self.dispatch(message);
7810 }
7811 if let Some(from) = reorder_from {
7812 let to = if matches!(direction, FocusDirection::Left) {
7813 from.wrapping_sub(1)
7814 } else {
7815 from + 1
7816 };
7817 self.set_announcement(format!("Column moved to position {}", to + 1));
7818 }
7819 self.request_redraw();
7820 return;
7821 }
7822 }
7823
7824 let is_text = self
7825 .tree
7826 .as_ref()
7827 .and_then(|tree| find_widget(tree.as_ref(), focused))
7828 .is_some_and(is_text_field);
7829 let navigates =
7830 !is_text || matches!(direction, FocusDirection::Up | FocusDirection::Down);
7831 if navigates {
7832 if let Some(next) = self
7833 .ui
7834 .as_ref()
7835 .and_then(|ui| ui.focus_directional(focused, direction))
7836 {
7837 self.runtime.input.focused = Some(next);
7838 self.reveal_focus();
7839 self.request_redraw();
7840 }
7841 return;
7842 }
7843 }
7844
7845 if matches!(
7850 event.logical_key,
7851 WinitKey::Named(NamedKey::Home) | WinitKey::Named(NamedKey::End)
7852 ) {
7853 let key = if matches!(event.logical_key, WinitKey::Named(NamedKey::Home)) {
7854 Key::Home {
7855 shift: self.shift,
7856 doc: self.ctrl,
7857 }
7858 } else {
7859 Key::End {
7860 shift: self.shift,
7861 doc: self.ctrl,
7862 }
7863 };
7864 let handled = self
7865 .tree
7866 .as_ref()
7867 .and_then(|tree| find_widget(tree.as_ref(), focused))
7868 .map(|widget| widget.on_key(&key));
7869 if let Some(KeyResponse::Handled(message)) = handled {
7870 if let Some(message) = message {
7871 self.dispatch(message);
7872 }
7873 self.request_redraw();
7874 return;
7875 }
7876 }
7877
7878 if matches!(
7882 event.logical_key,
7883 WinitKey::Named(NamedKey::Enter) | WinitKey::Named(NamedKey::Space)
7884 ) {
7885 let widget = self
7886 .tree
7887 .as_ref()
7888 .and_then(|tree| find_widget(tree.as_ref(), focused))
7889 .filter(|widget| widget.focusable());
7890 let message = widget.and_then(|widget| widget.on_click());
7891 if let Some(message) = message {
7892 if !event.repeat {
7895 let announce = widget.and_then(|widget| widget.announce());
7898 self.dispatch(message);
7899 if let Some(announce) = announce {
7900 self.set_announcement(announce);
7901 }
7902 self.request_redraw();
7903 }
7904 return;
7905 }
7906 }
7907
7908 if let Some(command) = clipboard_command(&event.logical_key, event.physical_key, self.ctrl)
7910 {
7911 match command {
7912 ClipCommand::Copy => {
7913 self.copy_selection(focused);
7914 }
7915 ClipCommand::Cut => self.cut_selection(focused),
7916 ClipCommand::Paste => {
7917 if let Some(pasted) = self.clipboard.paste(focused, self.window.as_ref()) {
7919 self.land_paste(pasted);
7920 }
7921 }
7922 }
7923 return;
7924 }
7925
7926 if opens_selection_bar(&event.logical_key, self.shift)
7930 && self
7931 .tree
7932 .as_ref()
7933 .and_then(|tree| find_widget(tree.as_ref(), focused))
7934 .is_some_and(|widget| widget.text_value().is_some())
7935 {
7936 self.show_selection_toolbar(focused);
7937 return;
7938 }
7939
7940 if self.ctrl {
7942 match &event.logical_key {
7943 WinitKey::Character(c) if c.eq_ignore_ascii_case("a") => {
7944 self.select_all(focused);
7945 return;
7946 }
7947 WinitKey::Character(c) if c.eq_ignore_ascii_case("z") && !self.shift => {
7951 self.step_history(focused, false);
7952 self.request_redraw();
7953 return;
7954 }
7955 WinitKey::Character(c)
7956 if c.eq_ignore_ascii_case("y")
7957 || (c.eq_ignore_ascii_case("z") && self.shift) =>
7958 {
7959 self.step_history(focused, true);
7960 self.request_redraw();
7961 return;
7962 }
7963 _ => {}
7964 }
7965 }
7966
7967 #[cfg(android)]
7972 if crate::android_ime::installed() {
7973 return;
7974 }
7975
7976 let shift = self.shift;
7977 let key = match &event.logical_key {
7978 WinitKey::Named(NamedKey::Backspace) => Some(Key::Backspace),
7979 WinitKey::Named(NamedKey::Delete) => Some(Key::Delete),
7980 WinitKey::Named(NamedKey::Enter) if !event.repeat => Some(Key::Enter),
7983 WinitKey::Named(NamedKey::Enter) => None,
7984 WinitKey::Named(NamedKey::ArrowLeft) => Some(Key::Left {
7986 shift,
7987 word: self.ctrl,
7988 }),
7989 WinitKey::Named(NamedKey::ArrowRight) => Some(Key::Right {
7990 shift,
7991 word: self.ctrl,
7992 }),
7993 WinitKey::Named(NamedKey::Home) => Some(Key::Home {
7994 shift,
7995 doc: self.ctrl,
7996 }),
7997 WinitKey::Named(NamedKey::End) => Some(Key::End {
7998 shift,
7999 doc: self.ctrl,
8000 }),
8001 WinitKey::Named(NamedKey::Space) => Some(Key::Text(" ".to_string())),
8002 WinitKey::Character(c) if c == "\n" || c == "\r" => {
8006 if event.repeat {
8007 None
8008 } else {
8009 Some(Key::Enter)
8010 }
8011 }
8012 _ => event.text.as_ref().map(|text| Key::Text(text.to_string())),
8013 };
8014
8015 if let Some(key) = key {
8016 self.apply_key(focused, key);
8017 self.request_redraw();
8018 }
8019 }
8020
8021 fn system_back(&mut self, event_loop: &ActiveEventLoop) {
8028 if !self.system_back_step() {
8029 event_loop.exit();
8030 }
8031 }
8032
8033 fn system_back_step(&mut self) -> bool {
8036 if self.close_selection_toolbar() {
8039 return true;
8040 }
8041 if self
8043 .runtime
8044 .region
8045 .as_ref()
8046 .is_some_and(|selection| selection.bar)
8047 {
8048 self.runtime.region = None;
8049 self.request_redraw();
8050 return true;
8051 }
8052 if let Some(message) = self.ui.as_ref().and_then(|ui| ui.top_dismiss()) {
8053 self.dispatch(message);
8054 self.request_redraw();
8055 return true;
8056 }
8057 if self.app.can_go_back() {
8058 self.build_dirty = true;
8059 self.app.go_back();
8061 self.request_redraw();
8062 return true;
8063 }
8064 false
8065 }
8066
8067 fn escape(&mut self) {
8072 if self.close_selection_toolbar() {
8074 return;
8075 }
8076 if self.runtime.region.take().is_some() {
8078 self.request_redraw();
8079 return;
8080 }
8081 let outcome: Option<Option<A::Message>> = self.runtime.input.focused.and_then(|focused| {
8084 let tree = self.tree.as_ref()?;
8085 let path = find_path(tree.as_ref(), focused);
8086 for widget in path.iter().rev() {
8087 match widget.on_key(&Key::Escape) {
8088 KeyResponse::Handled(message) => return Some(message),
8089 KeyResponse::Skip => return Some(None),
8090 KeyResponse::Ignored => {}
8091 }
8092 }
8093 None
8094 });
8095
8096 match outcome {
8097 Some(message) => {
8098 if let Some(message) = message {
8099 self.dispatch(message);
8100 self.request_redraw();
8101 }
8102 }
8103 None => {
8105 if let Some(message) = self.ui.as_ref().and_then(|ui| ui.top_dismiss()) {
8106 self.dispatch(message);
8107 self.request_redraw();
8108 }
8109 }
8110 }
8111 }
8112
8113 fn move_caret_vertical(&mut self, id: WidgetId, down: bool, page: bool) -> bool {
8119 let width = self
8120 .ui
8121 .as_ref()
8122 .and_then(|ui| ui.widget_rect(id))
8123 .map(|r| r.width)
8124 .unwrap_or(0.0);
8125 let cursor = self.runtime.edits.get(&id).map(|e| e.cursor).unwrap_or(0);
8126 let moved = self
8127 .tree
8128 .as_ref()
8129 .and_then(|tree| find_widget(tree.as_ref(), id))
8130 .and_then(|widget| widget.caret_vertical(width, cursor, down, page, self.goal_x));
8131 let Some((new_cursor, goal)) = moved else {
8132 return false;
8133 };
8134 let shift = self.shift;
8135 let edit = self.runtime.edits.entry(id).or_default();
8136 if shift {
8137 if edit.anchor.is_none() {
8138 edit.anchor = Some(edit.cursor);
8139 }
8140 } else {
8141 edit.anchor = None;
8142 }
8143 edit.cursor = new_cursor;
8144 self.goal_x = Some(goal);
8146 self.reveal_caret(id, new_cursor);
8147 self.request_redraw();
8148 true
8149 }
8150
8151 fn select_held_word(&mut self, id: WidgetId) {
8154 let cursor = self.runtime.edits.get(&id).map(|e| e.cursor).unwrap_or(0);
8155 let word = self
8156 .tree
8157 .as_ref()
8158 .and_then(|tree| find_widget(tree.as_ref(), id))
8159 .and_then(|widget| widget.word_at(cursor))
8160 .filter(|(start, end)| start < end);
8161 let Some((start, end)) = word else {
8162 self.show_selection_toolbar(id);
8165 return;
8166 };
8167 self.runtime.edits.insert(
8168 id,
8169 Edit {
8170 cursor: end,
8171 anchor: Some(start),
8172 composing: None,
8173 },
8174 );
8175 self.runtime.selection_handles = Some(id);
8176 self.runtime.close_edit_run(id);
8177 self.show_selection_toolbar(id);
8178 #[cfg(android)]
8179 self.push_ime_context(id);
8180 }
8181
8182 fn grab_selection_handle(&self) -> Option<Drag> {
8185 let id = self.runtime.selection_handles?;
8186 if self.runtime.input.focused != Some(id) {
8187 return None;
8188 }
8189 let rect = self.ui.as_ref()?.widget_rect(id)?;
8190 let edit = self.runtime.edits.get(&id).copied()?;
8191 let scroll_y = self.runtime.scroll.get(&id).map(|s| s.1).unwrap_or(0.0);
8192 let handles = find_widget(self.tree.as_ref()?.as_ref(), id)?
8193 .selection_handles(rect.width, &edit, scroll_y)?;
8194 let local = Point::new(self.cursor.x - rect.x, self.cursor.y - rect.y);
8195 let handle = crate::selection::grab(&handles, local)?;
8196 let at = handles[handle.index()].line_center;
8197 Some(Drag::SelectionHandle {
8198 id,
8199 rect,
8200 handle,
8201 grab: Point::new(at.x - local.x, at.y - local.y),
8202 })
8203 }
8204
8205 fn apply_key(&mut self, id: WidgetId, key: Key) {
8208 self.goal_x = None;
8210 self.runtime.selection_handles = None;
8213 self.runtime.selection_toolbar = None;
8214 let was = self.runtime.edits.get(&id).copied().unwrap_or_default();
8215 let mut edit = was;
8216 let widget = self
8217 .tree
8218 .as_ref()
8219 .and_then(|tree| find_widget(tree.as_ref(), id));
8220 let before = widget
8223 .and_then(|widget| widget.text_value())
8224 .map(str::to_owned);
8225 let announce = widget.and_then(|widget| widget.announce());
8230 let message = widget.and_then(|widget| widget.on_edit(&mut edit, &key));
8231 self.runtime.edits.insert(id, edit);
8232 self.reveal_caret(id, edit.cursor);
8234 if let Some(message) = message {
8235 self.dispatch_edit(message);
8236 if let Some(announce) = announce {
8237 self.set_announcement(announce);
8238 }
8239 }
8240 let after = self
8246 .tree
8247 .as_ref()
8248 .and_then(|tree| find_widget(tree.as_ref(), id))
8249 .and_then(|widget| widget.text_value())
8250 .map(str::to_owned);
8251 match (before, after) {
8252 (Some(before), Some(after)) if before != after => {
8253 let kind = EditKind::of(&key, was.selection_range().is_some(), self.composing());
8254 let since = self.since_last_edit();
8255 self.runtime
8256 .record_edit(id, EditSnapshot::new(before, was), kind, since);
8257 }
8258 (Some(_), _) => self.runtime.close_edit_run(id),
8261 _ => {}
8262 }
8263 }
8264
8265 fn dispatch_edit(&mut self, message: A::Message) {
8273 self.dispatch(message);
8274 if let Some((width, height)) = self.last_size {
8275 let theme = self.themes.displayed(&self.app);
8276 let tree = self
8280 .media_query(width, height)
8281 .scope(|| build_view(&self.app, &theme, &self.runtime));
8282 self.tree = Some(tree);
8283 }
8284 }
8285
8286 fn since_last_edit(&mut self) -> f32 {
8289 let now = Instant::now();
8290 let since = self
8291 .last_edit_at
8292 .map(|then| now.duration_since(then).as_secs_f32())
8293 .unwrap_or(f32::INFINITY);
8294 self.last_edit_at = Some(now);
8295 since
8296 }
8297
8298 fn composing(&self) -> bool {
8300 self.runtime
8301 .input
8302 .focused
8303 .and_then(|id| self.runtime.edits.get(&id))
8304 .is_some_and(|edit| edit.composing.is_some())
8305 }
8306
8307 fn step_history(&mut self, id: WidgetId, forward: bool) -> bool {
8311 let value = self
8312 .tree
8313 .as_ref()
8314 .and_then(|tree| find_widget(tree.as_ref(), id))
8315 .and_then(|widget| widget.text_value())
8316 .map(str::to_owned);
8317 let Some(value) = value else {
8318 return false;
8319 };
8320 let edit = self.runtime.edits.get(&id).copied().unwrap_or_default();
8321 let current = EditSnapshot::new(value, edit);
8322 let target = if forward {
8323 self.runtime.redo_edit(id, current)
8324 } else {
8325 self.runtime.undo_edit(id, current)
8326 };
8327 let Some(target) = target else {
8328 return false;
8329 };
8330 let message = self
8331 .tree
8332 .as_ref()
8333 .and_then(|tree| find_widget(tree.as_ref(), id))
8334 .and_then(|widget| widget.replace_value(target.value.clone()));
8335 self.runtime.edits.insert(id, target.edit);
8338 self.reveal_caret(id, target.edit.cursor);
8339 if let Some(message) = message {
8340 self.dispatch_edit(message);
8341 }
8342 self.last_edit_at = None;
8345 true
8346 }
8347
8348 fn reveal_focus(&mut self) {
8360 let Some(id) = self.runtime.input.focused else {
8361 return;
8362 };
8363 let Some(moves) = self.ui.as_ref().map(|ui| ui.reveal(id, &self.runtime)) else {
8364 return;
8365 };
8366 for (area, offset) in moves {
8367 self.runtime.scroll_ballistic.remove(&area);
8368 self.runtime.scroll_velocity.remove(&area);
8369 self.runtime.scroll_target.insert(area, offset);
8370 }
8371 }
8372
8373 fn reveal_caret(&mut self, id: WidgetId, cursor: usize) {
8377 let Some(vp) = self.ui.as_ref().and_then(|ui| ui.scrollable_viewport(id)) else {
8378 return;
8379 };
8380 let metrics = self
8381 .tree
8382 .as_ref()
8383 .and_then(|tree| find_widget(tree.as_ref(), id))
8384 .and_then(|widget| widget.text_metrics(vp.width, cursor));
8385 let Some((content_h, visible_h, caret_top, caret_h)) = metrics else {
8386 return;
8387 };
8388 let max_y = (content_h - visible_h).max(0.0);
8389 let cur = self.runtime.scroll.get(&id).map(|s| s.1).unwrap_or(0.0);
8390 let lo = (caret_top + caret_h - visible_h).max(0.0);
8393 let hi = caret_top;
8394 let target = cur.clamp(lo.min(hi), lo.max(hi)).clamp(0.0, max_y);
8395 self.runtime.scroll.insert(id, (0.0, target));
8396 self.runtime.scroll_target.insert(id, (0.0, target));
8397 self.runtime.scroll_velocity.remove(&id);
8398 }
8399
8400 fn copy_selection(&mut self, id: WidgetId) -> bool {
8403 let edit = self.runtime.edits.get(&id).copied().unwrap_or_default();
8404 let text = self
8405 .tree
8406 .as_ref()
8407 .and_then(|tree| find_widget(tree.as_ref(), id))
8408 .and_then(|widget| widget.selected_text(&edit));
8409 match text {
8410 Some(text) => {
8411 self.clipboard.set_text(text);
8412 true
8413 }
8414 None => false,
8415 }
8416 }
8417
8418 fn region_stops(&self, area: WidgetId) -> Vec<(WidgetId, usize)> {
8421 let (Some(ui), Some(tree)) = (self.ui.as_ref(), self.tree.as_ref()) else {
8422 return Vec::new();
8423 };
8424 ui.text_stops_in(area)
8425 .filter_map(|stop| {
8426 let text = find_widget(tree.as_ref(), stop.id)?.selection_text()?;
8427 Some((stop.id, text.chars().count()))
8428 })
8429 .collect()
8430 }
8431
8432 fn region_selection(
8434 &self,
8435 area: WidgetId,
8436 anchor: RegionPoint,
8437 extent: RegionPoint,
8438 ) -> RegionSelection {
8439 RegionSelection::new(area, anchor, extent, &self.region_stops(area))
8440 }
8441
8442 fn region_index(&self, stop: &TextStop, at: Point) -> Option<usize> {
8444 find_widget(self.tree.as_ref()?.as_ref(), stop.id)?.selection_hit(
8445 at.x - stop.rect.x,
8446 at.y - stop.rect.y,
8447 stop.rect.width,
8448 )
8449 }
8450
8451 fn begin_region_selection(&mut self) {
8455 let Some(stop) = self.region_press_target() else {
8456 return;
8457 };
8458 let Some(index) = self.region_index(&stop, self.cursor) else {
8459 return;
8460 };
8461 let now = Instant::now();
8462 let here = self.cursor;
8463 let double = self.last_region_click.is_some_and(|(then, there)| {
8464 (now - then).as_secs_f32() < 0.4
8465 && (here.x - there.x).hypot(here.y - there.y) <= REGION_DOUBLE_CLICK_SLOP
8466 });
8467 self.last_region_click = Some((now, here));
8468 let word = double
8469 .then(|| {
8470 let tree = self.tree.as_ref()?;
8471 let text = find_widget(tree.as_ref(), stop.id)?.selection_text()?;
8472 Some(frus_widgets::word_bounds(text, index))
8473 })
8474 .flatten();
8475 let (anchor, extent) = match word {
8476 Some((start, end)) => (
8477 RegionPoint {
8478 text: stop.id,
8479 index: start,
8480 },
8481 RegionPoint {
8482 text: stop.id,
8483 index: end,
8484 },
8485 ),
8486 None => {
8487 let point = RegionPoint {
8488 text: stop.id,
8489 index,
8490 };
8491 (point, point)
8492 }
8493 };
8494 self.runtime.region = Some(self.region_selection(stop.area, anchor, extent));
8495 self.drag = word
8497 .is_none()
8498 .then_some(Drag::RegionSelect { area: stop.area });
8499 self.request_redraw();
8500 }
8501
8502 fn region_press_target(&self) -> Option<TextStop> {
8505 let ui = self.ui.as_ref()?;
8506 let claimed = self
8507 .runtime
8508 .input
8509 .pressed
8510 .is_some_and(|pressed| ui.msg_for(pressed).is_some());
8511 if claimed {
8512 return None;
8513 }
8514 ui.text_stop_at(self.cursor)
8515 }
8516
8517 fn select_region_word_held(&mut self) -> bool {
8520 let Some(stop) = self.ui.as_ref().and_then(|ui| ui.text_stop_at(self.cursor)) else {
8521 return false;
8522 };
8523 let Some(index) = self.region_index(&stop, self.cursor) else {
8524 return false;
8525 };
8526 let word = self
8527 .tree
8528 .as_ref()
8529 .and_then(|tree| find_widget(tree.as_ref(), stop.id))
8530 .and_then(|widget| widget.selection_text())
8531 .map(|text| frus_widgets::word_bounds(text, index))
8532 .filter(|(start, end)| start < end);
8533 let Some((start, end)) = word else {
8534 return false;
8535 };
8536 let anchor = RegionPoint {
8537 text: stop.id,
8538 index: start,
8539 };
8540 let extent = RegionPoint {
8541 text: stop.id,
8542 index: end,
8543 };
8544 self.runtime.region = Some(
8545 self.region_selection(stop.area, anchor, extent)
8546 .with_bar(true),
8547 );
8548 self.request_redraw();
8549 true
8550 }
8551
8552 fn region_handles(&self) -> Option<(WidgetId, [frus_widgets::SelectionHandle; 2])> {
8554 let selection = self.runtime.region.as_ref().filter(|s| s.handles)?;
8555 let (ui, tree) = (self.ui.as_ref()?, self.tree.as_ref()?);
8556 let at = |point: RegionPoint, start: bool| {
8557 let stop = ui
8558 .text_stops_in(selection.area)
8559 .find(|s| s.id == point.text)?;
8560 let handle = find_widget(tree.as_ref(), point.text)?.selection_grip(
8561 stop.rect.width,
8562 point.index,
8563 start,
8564 )?;
8565 let (dx, dy) = (stop.rect.x, stop.rect.y);
8566 Some(frus_widgets::SelectionHandle {
8567 rect: handle.rect.translate(dx, dy),
8568 line_center: Point::new(handle.line_center.x + dx, handle.line_center.y + dy),
8569 })
8570 };
8571 Some((
8572 selection.area,
8573 [at(selection.start?, true)?, at(selection.end?, false)?],
8574 ))
8575 }
8576
8577 fn grab_region_handle(&self) -> Option<Drag> {
8579 let (area, handles) = self.region_handles()?;
8580 let handle = crate::selection::grab(&handles, self.cursor)?;
8581 let at = handles[handle.index()].line_center;
8582 Some(Drag::RegionHandle {
8583 area,
8584 handle,
8585 grab: Point::new(at.x - self.cursor.x, at.y - self.cursor.y),
8586 })
8587 }
8588
8589 fn drag_region_handle(
8593 &mut self,
8594 area: WidgetId,
8595 handle: crate::selection::Handle,
8596 grab: Point,
8597 ) {
8598 let Some(selection) = self.runtime.region.as_ref() else {
8599 return;
8600 };
8601 let (Some(start), Some(end)) = (selection.start, selection.end) else {
8602 return;
8603 };
8604 let target = Point::new(self.cursor.x + grab.x, self.cursor.y + grab.y);
8605 let Some(stop) = self
8606 .ui
8607 .as_ref()
8608 .and_then(|ui| ui.nearest_text_stop(area, target))
8609 else {
8610 return;
8611 };
8612 let Some(index) = self.region_index(&stop, target) else {
8613 return;
8614 };
8615 let moved = RegionPoint {
8616 text: stop.id,
8617 index,
8618 };
8619 let stops = self.region_stops(area);
8620 let order = |point: RegionPoint| {
8621 stops
8622 .iter()
8623 .position(|(id, _)| *id == point.text)
8624 .map(|at| (at, point.index))
8625 };
8626 let (Some(to), Some(other)) = (
8627 order(moved),
8628 order(match handle {
8629 crate::selection::Handle::Start => end,
8630 crate::selection::Handle::End => start,
8631 }),
8632 ) else {
8633 return;
8634 };
8635 let (fixed, crosses) = match handle {
8636 crate::selection::Handle::Start => (end, to >= other),
8637 crate::selection::Handle::End => (start, to <= other),
8638 };
8639 if crosses {
8640 return;
8641 }
8642 let next = RegionSelection::new(area, fixed, moved, &stops).with_bar(true);
8643 let next = RegionSelection { bar: false, ..next };
8645 if self.runtime.region.as_ref() != Some(&next) {
8646 self.runtime.region = Some(next);
8647 self.request_redraw();
8648 }
8649 }
8650
8651 fn extend_region_selection(&mut self, area: WidgetId) {
8654 let Some(anchor) = self.runtime.region.as_ref().map(|region| region.anchor) else {
8655 return;
8656 };
8657 let Some(stop) = self
8658 .ui
8659 .as_ref()
8660 .and_then(|ui| ui.nearest_text_stop(area, self.cursor))
8661 else {
8662 return;
8663 };
8664 let Some(index) = self.region_index(&stop, self.cursor) else {
8665 return;
8666 };
8667 let extent = RegionPoint {
8668 text: stop.id,
8669 index,
8670 };
8671 let selection = self.region_selection(area, anchor, extent);
8672 if self.runtime.region.as_ref() != Some(&selection) {
8673 self.runtime.region = Some(selection);
8674 self.request_redraw();
8675 }
8676 }
8677
8678 fn region_text(&self) -> Option<String> {
8681 let region = self.runtime.region.as_ref()?;
8682 let (ui, tree) = (self.ui.as_ref()?, self.tree.as_ref()?);
8683 let texts: Vec<(WidgetId, &str)> = ui
8684 .text_stops_in(region.area)
8685 .filter_map(|stop| {
8686 let text = find_widget(tree.as_ref(), stop.id)?.selection_text()?;
8687 Some((stop.id, text))
8688 })
8689 .collect();
8690 let copied = frus_widgets::region_copy(&texts, ®ion.ranges);
8691 (!copied.is_empty()).then_some(copied)
8692 }
8693
8694 fn copy_region(&mut self) -> bool {
8696 match self.region_text() {
8697 Some(copied) => {
8698 self.clipboard.set_text(copied);
8699 true
8700 }
8701 None => false,
8702 }
8703 }
8704
8705 fn select_all_region(&mut self) {
8707 let Some(area) = self.runtime.region.as_ref().map(|region| region.area) else {
8708 return;
8709 };
8710 let stops = self.region_stops(area);
8711 let (Some(first), Some(last)) = (stops.first(), stops.last()) else {
8712 return;
8713 };
8714 let anchor = RegionPoint {
8715 text: first.0,
8716 index: 0,
8717 };
8718 let extent = RegionPoint {
8719 text: last.0,
8720 index: last.1,
8721 };
8722 let bar = self
8723 .runtime
8724 .region
8725 .as_ref()
8726 .is_some_and(|selection| selection.bar);
8727 self.runtime.region = Some(self.region_selection(area, anchor, extent).with_bar(bar));
8728 self.request_redraw();
8729 }
8730
8731 fn region_key(&mut self, event: &KeyDown) -> bool {
8734 if self.runtime.region.is_none() {
8735 return false;
8736 }
8737 if let Some(ClipCommand::Copy) =
8738 clipboard_command(&event.logical_key, event.physical_key, self.ctrl)
8739 {
8740 self.copy_region();
8741 return true;
8742 }
8743 let select_all = self.ctrl
8744 && matches!(&event.logical_key, WinitKey::Character(c) if c.eq_ignore_ascii_case("a"));
8745 if select_all {
8746 self.select_all_region();
8747 return true;
8748 }
8749 false
8750 }
8751
8752 fn cut_selection(&mut self, id: WidgetId) {
8756 if self.copy_selection(id) {
8757 self.apply_key(id, Key::Backspace);
8758 self.request_redraw();
8759 }
8760 }
8761
8762 fn select_all(&mut self, id: WidgetId) {
8764 self.runtime.edits.insert(
8765 id,
8766 Edit {
8767 cursor: usize::MAX,
8768 anchor: Some(0),
8769 composing: None,
8770 },
8771 );
8772 self.runtime.close_edit_run(id);
8775 self.request_redraw();
8776 }
8777
8778 fn show_selection_toolbar(&mut self, id: WidgetId) {
8782 self.show_selection_toolbar_at(id, None);
8783 }
8784
8785 fn show_selection_toolbar_at(&mut self, id: WidgetId, at: Option<Point>) {
8788 let can_paste = self.clipboard.has_text();
8789 self.runtime.selection_toolbar = Some(ToolbarMark { id, can_paste, at });
8790 self.request_redraw();
8791 }
8792
8793 fn close_selection_toolbar(&mut self) -> bool {
8796 if self.runtime.selection_toolbar.is_none() {
8797 return false;
8798 }
8799 self.runtime.selection_handles = None;
8800 self.hide_selection_toolbar();
8801 true
8802 }
8803
8804 fn hide_selection_toolbar(&mut self) {
8806 if self.runtime.selection_toolbar.take().is_some() {
8807 self.request_redraw();
8808 }
8809 }
8810
8811 fn perform_edit_action(&mut self, action: frus_widgets::EditAction) {
8816 use frus_widgets::EditAction;
8817 if self
8819 .runtime
8820 .region
8821 .as_ref()
8822 .is_some_and(|selection| selection.bar)
8823 {
8824 match action {
8825 EditAction::Copy => {
8826 self.copy_region();
8828 self.runtime.region = None;
8829 }
8830 EditAction::SelectAll => self.select_all_region(),
8831 EditAction::Cut | EditAction::Paste => {}
8832 }
8833 self.request_redraw();
8834 return;
8835 }
8836 let Some(id) = self.runtime.selection_toolbar.map(|mark| mark.id) else {
8837 return;
8838 };
8839 match action {
8840 EditAction::Copy => {
8841 if self.copy_selection(id) {
8842 if let Some(edit) = self.runtime.edits.get_mut(&id) {
8845 *edit = collapsed_to_end(*edit);
8846 }
8847 self.runtime.close_edit_run(id);
8848 }
8849 self.runtime.selection_handles = None;
8850 self.hide_selection_toolbar();
8851 }
8852 EditAction::Cut => {
8853 self.cut_selection(id);
8855 self.hide_selection_toolbar();
8856 }
8857 EditAction::Paste => {
8858 self.runtime.selection_handles = None;
8859 self.hide_selection_toolbar();
8860 if let Some(pasted) = self.clipboard.paste(id, self.window.as_ref()) {
8862 self.land_paste(pasted);
8863 }
8864 }
8865 EditAction::SelectAll => {
8866 self.select_all(id);
8867 if self.runtime.selection_handles.is_some() {
8870 self.runtime.selection_handles = Some(id);
8871 }
8872 if select_all_hides_bar(frus_widgets::default_target_platform()) {
8873 self.runtime.selection_handles = None;
8876 self.hide_selection_toolbar();
8877 } else {
8878 let at = self.runtime.selection_toolbar.and_then(|mark| mark.at);
8881 self.show_selection_toolbar_at(id, at);
8882 }
8883 }
8884 }
8885 #[cfg(android)]
8886 self.push_ime_context(id);
8887 }
8888
8889 fn context_click(&mut self) {
8893 let secondary = self
8895 .ui
8896 .as_ref()
8897 .and_then(|ui| ui.hit(self.cursor))
8898 .and_then(|id| {
8899 self.tree
8900 .as_ref()
8901 .and_then(|tree| find_widget(tree.as_ref(), id))
8902 })
8903 .and_then(|widget| widget.on_secondary_tap());
8904 if let Some(message) = secondary {
8905 self.hide_selection_toolbar();
8906 self.dispatch(message);
8907 self.request_redraw();
8908 return;
8909 }
8910 let hit = self.ui.as_ref().and_then(|ui| ui.focus_hit(self.cursor));
8911 let Some((id, rect)) = hit else {
8912 self.hide_selection_toolbar();
8913 return;
8914 };
8915 let was_focused = self.runtime.input.focused == Some(id);
8916 let scroll_y = self.runtime.scroll.get(&id).map(|s| s.1).unwrap_or(0.0);
8917 let scroll_cursor = if was_focused {
8918 self.runtime.edits.get(&id).map(|e| e.cursor).unwrap_or(0)
8919 } else {
8920 0
8921 };
8922 let cursor = self
8923 .tree
8924 .as_ref()
8925 .and_then(|tree| find_widget(tree.as_ref(), id))
8926 .and_then(|widget| {
8927 widget.cursor_at(
8928 self.cursor.x - rect.x,
8929 self.cursor.y - rect.y + scroll_y,
8930 rect.width,
8931 scroll_cursor,
8932 )
8933 });
8934 let Some(cursor) = cursor else {
8936 self.hide_selection_toolbar();
8937 return;
8938 };
8939 let inside = was_focused
8940 && self
8941 .runtime
8942 .edits
8943 .get(&id)
8944 .and_then(|edit| edit.selection_range())
8945 .is_some_and(|(start, end)| cursor >= start && cursor <= end);
8946 self.runtime.input.focused = Some(id);
8947 self.runtime.selection_handles = None;
8948 if !inside {
8949 self.goal_x = None;
8950 self.runtime.edits.insert(
8951 id,
8952 Edit {
8953 cursor,
8954 anchor: None,
8955 composing: None,
8956 },
8957 );
8958 self.runtime.close_edit_run(id);
8959 }
8960 self.show_selection_toolbar_at(id, Some(self.cursor));
8961 }
8962
8963 fn land_paste(&mut self, pasted: clip::Pasted) {
8966 if pasted.lands(self.runtime.input.focused) {
8967 self.apply_key(pasted.into, Key::Text(pasted.text));
8968 self.request_redraw();
8969 }
8970 }
8971}
8972
8973fn collapsed_to_end(edit: Edit) -> Edit {
8977 let cursor = edit.selection_range().map_or(edit.cursor, |(_, end)| end);
8978 Edit {
8979 cursor,
8980 anchor: None,
8981 composing: None,
8982 }
8983}
8984
8985#[derive(Clone, Copy, PartialEq, Eq)]
8987enum Edge {
8988 Start,
8989 End,
8990}
8991
8992fn cursor_icon(cursor: UiCursor) -> CursorIcon {
9005 match cursor {
9006 UiCursor::Default | UiCursor::None => CursorIcon::Default,
9007 UiCursor::Pointer => CursorIcon::Pointer,
9008 UiCursor::Text => CursorIcon::Text,
9009 UiCursor::Forbidden => CursorIcon::NotAllowed,
9010 UiCursor::Wait => CursorIcon::Wait,
9011 UiCursor::Progress => CursorIcon::Progress,
9012 UiCursor::Help => CursorIcon::Help,
9013 UiCursor::Precise => CursorIcon::Crosshair,
9014 UiCursor::Move => CursorIcon::Move,
9015 UiCursor::Grab => CursorIcon::Grab,
9016 UiCursor::Grabbing => CursorIcon::Grabbing,
9017 UiCursor::ResizeColumn => CursorIcon::ColResize,
9018 UiCursor::ResizeRow => CursorIcon::RowResize,
9019 UiCursor::ZoomIn => CursorIcon::ZoomIn,
9020 UiCursor::ZoomOut => CursorIcon::ZoomOut,
9021 UiCursor::ContextMenu => CursorIcon::ContextMenu,
9022 }
9023}
9024
9025fn mask_pan(axis: frus_widgets::PanAxis, value: Point) -> Point {
9027 match axis {
9028 frus_widgets::PanAxis::Free => value,
9029 frus_widgets::PanAxis::Horizontal => Point::new(value.x, 0.0),
9030 frus_widgets::PanAxis::Vertical => Point::new(0.0, value.y),
9031 }
9032}
9033
9034fn gesture_was_a_tap(ended: Option<&Drag>) -> bool {
9035 matches!(
9036 ended,
9037 Some(
9038 Drag::Scroll { moved: false, .. }
9039 | Drag::Pan { moved: false, .. }
9040 | Drag::Gesture { moved: false, .. }
9041 | Drag::Reorder { moved: false, .. }
9042 | Drag::Item { moved: false, .. }
9043 | Drag::Dismiss { moved: false, .. }
9044 | Drag::Sheet { moved: false, .. }
9045 )
9046 )
9047}
9048
9049fn press_at<Msg: Clone>(runtime: &mut Runtime, ui: Option<&Ui<Msg>>, at: Point) -> bool {
9053 runtime.input.pressed = ui.and_then(|ui| ui.hit(at));
9054 let Some(pressed) = runtime.input.pressed else {
9055 return false;
9056 };
9057 let Some(rect) = ui.and_then(|ui| ui.ink_box(pressed)) else {
9058 return false;
9059 };
9060 runtime.ink_press(
9061 pressed,
9062 Point::new(at.x - rect.x, at.y - rect.y),
9063 Size::new(rect.width, rect.height),
9064 );
9065 true
9066}
9067
9068fn settle_press(
9086 input: &mut frus_widgets::InputState,
9087 kind: PointerKind,
9088 could_tap: bool,
9089 drag: Option<&Drag>,
9090 touch: bool,
9091) -> bool {
9092 let taken = match kind {
9093 PointerKind::Down => false,
9094 PointerKind::Move => could_tap && !gesture_was_a_tap(drag),
9095 PointerKind::Up | PointerKind::Cancel => true,
9096 };
9097 if !taken {
9098 return false;
9099 }
9100 let mut changed = input.pressed.take().is_some();
9101 if touch {
9102 changed |= input.hovered.take().is_some();
9103 }
9104 changed
9105}
9106
9107const AUTOSCROLL_VELOCITY: f32 = 50.0;
9111
9112const AUTOSCROLL_MAX_OVERHANG: f32 = 20.0;
9116
9117fn apply_scroll_requests<Msg>(
9130 runtime: &mut Runtime,
9131 tree: &dyn Widget<Msg>,
9132 requests: Vec<(u64, ScrollTo)>,
9133 regions: &[Scrollable],
9134) -> (Vec<(u64, ScrollTo)>, bool) {
9135 let mut unplaced = Vec::new();
9136 let mut moved = false;
9137 for (key, to) in requests {
9138 let area = find_by_key(tree, key).and_then(|id| regions.iter().find(|a| a.id == id));
9139 match area {
9140 Some(area) => moved |= runtime.scroll_to(area, to),
9141 None => unplaced.push((key, to)),
9142 }
9143 }
9144 (unplaced, moved)
9145}
9146
9147fn apply_sheet_requests<Msg>(
9154 runtime: &mut Runtime,
9155 tree: &dyn Widget<Msg>,
9156 requests: Vec<(u64, SheetTo)>,
9157 sheets: &[frus_widgets::SheetArea],
9158) -> (Vec<(u64, SheetTo)>, bool) {
9159 let mut unplaced = Vec::new();
9160 let mut moved = false;
9161 for (key, to) in requests {
9162 let sheet = frus_widgets::find_sheet_by_key(tree, key)
9163 .and_then(|id| sheets.iter().find(|sheet| sheet.id == id));
9164 match sheet {
9165 Some(sheet) => moved |= runtime.sheet_to(sheet.id, &sheet.spec, &to),
9166 None => unplaced.push((key, to)),
9167 }
9168 }
9169 (unplaced, moved)
9170}
9171
9172fn follow_content(drag: &mut Drag, shift: (f32, f32)) {
9178 match drag {
9179 Drag::Reorder { start, carried, .. } => {
9180 start.x += shift.0;
9181 start.y += shift.1;
9182 if let Some((_, rect)) = carried {
9183 *rect = rect.translate(shift.0, shift.1);
9184 }
9185 }
9186 Drag::Item { source, start, .. } => {
9187 start.x += shift.0;
9188 start.y += shift.1;
9189 source.rect = source.rect.translate(shift.0, shift.1);
9190 }
9191 _ => {}
9192 }
9193}
9194
9195fn edge_autoscroll(item: (f32, f32), view: (f32, f32), room: (f32, f32), dt: f32) -> Option<f32> {
9208 let before = view.0 - item.0; let after = item.1 - view.1; let overhang = if before > 0.0 && room.0 > 0.0 {
9213 before.min(AUTOSCROLL_MAX_OVERHANG)
9214 } else if after > 0.0 && room.1 > 0.0 {
9215 -after.min(AUTOSCROLL_MAX_OVERHANG)
9216 } else {
9217 return None;
9218 };
9219 let travel = overhang * AUTOSCROLL_VELOCITY * dt;
9220 (travel != 0.0).then_some(travel)
9221}
9222
9223fn spring_toward(current: f32, target: f32, dt: f32, tau: f32) -> f32 {
9227 let k = 1.0 - (-dt / tau.max(1e-4)).exp();
9228 current + (target - current) * k
9229}
9230
9231const FOCUS_HISTORY_MAX: usize = 8;
9233
9234fn resolve_focus(
9242 current: Option<WidgetId>,
9243 present: &std::collections::HashSet<WidgetId>,
9244 history: &mut Vec<WidgetId>,
9245 prev: &mut Option<WidgetId>,
9246) -> Option<WidgetId> {
9247 let mut cur = current;
9248 if let Some(c) = cur {
9249 if !present.contains(&c) {
9250 cur = None;
9251 while let Some(cand) = history.pop() {
9252 if present.contains(&cand) {
9253 cur = Some(cand);
9254 break;
9255 }
9256 }
9257 }
9258 }
9259 if *prev != cur {
9260 if let Some(old) = *prev {
9261 if present.contains(&old) && Some(old) != cur {
9262 history.push(old);
9263 if history.len() > FOCUS_HISTORY_MAX {
9264 history.remove(0);
9265 }
9266 }
9267 }
9268 *prev = cur;
9269 }
9270 cur
9271}
9272
9273fn draw_ghost_card(scene: &mut Scene, theme: &Theme, card: Rect, ghost: &[Primitive]) {
9279 use drag_preview::{BORDER_ALPHA, BORDER_WIDTH, SHADOW_ALPHA, SHADOW_BLUR, SHADOW_OFFSET_Y};
9280 scene.set_clip(Rect::UNBOUNDED);
9281 let shadow = theme.scheme.shadow.with_alpha(SHADOW_ALPHA);
9285 scene.shadow(
9286 card.translate(0.0, SHADOW_OFFSET_Y),
9287 shadow,
9288 theme.radius,
9289 SHADOW_BLUR,
9290 );
9291 let border = theme.primary.fade(BORDER_ALPHA);
9292 if ghost.is_empty() {
9293 scene.draw_rect(card, theme.surface, theme.radius, BORDER_WIDTH, border);
9294 } else {
9295 scene.draw_rect(card, theme.surface, theme.radius, 0.0, Color::TRANSPARENT);
9296 for primitive in ghost {
9297 scene.push_primitive(primitive.clone());
9298 }
9299 scene.draw_rect(card, Color::TRANSPARENT, theme.radius, BORDER_WIDTH, border);
9300 }
9301}
9302
9303#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9305enum ReorderMotion {
9306 Columns,
9309 Slots(ReorderAxis),
9312}
9313
9314impl ReorderMotion {
9315 fn of(axis: ReorderAxis, inserts: bool) -> Self {
9319 match axis {
9320 ReorderAxis::Vertical => Self::Slots(ReorderAxis::Vertical),
9321 ReorderAxis::Horizontal if inserts => Self::Slots(ReorderAxis::Horizontal),
9322 ReorderAxis::Horizontal => Self::Columns,
9323 }
9324 }
9325}
9326
9327fn reorder_motion<Msg>(widget: &dyn Widget<Msg>) -> ReorderMotion {
9329 ReorderMotion::of(widget.reorder_axis(), widget.reorder_inserts())
9330}
9331
9332fn ghost_offset(motion: ReorderMotion, travel: (f32, f32)) -> (f32, f32) {
9341 match motion {
9342 ReorderMotion::Columns => (travel.0, drag_preview::LIFT_Y),
9343 ReorderMotion::Slots(ReorderAxis::Vertical) => travel,
9344 ReorderMotion::Slots(ReorderAxis::Horizontal) => (travel.0, 0.0),
9345 }
9346}
9347
9348fn drop_insertion_line(
9358 target: Rect,
9359 thickness: f32,
9360 after: bool,
9361 axis: ReorderAxis,
9362 rtl: bool,
9363) -> Rect {
9364 match axis {
9365 ReorderAxis::Vertical => {
9366 let edge = if after {
9367 target.y + target.height
9368 } else {
9369 target.y
9370 };
9371 Rect::new(target.x, edge - thickness * 0.5, target.width, thickness)
9372 }
9373 ReorderAxis::Horizontal => {
9374 let edge = if after != rtl {
9375 target.x + target.width
9376 } else {
9377 target.x
9378 };
9379 Rect::new(edge - thickness * 0.5, target.y, thickness, target.height)
9380 }
9381 }
9382}
9383
9384fn claim_axis(dx: f32, dy: f32, can_x: bool, can_y: bool) -> bool {
9393 match (can_x, can_y) {
9394 (true, false) => false,
9395 (false, true) => true,
9396 _ => dy.abs() >= dx.abs(),
9397 }
9398}
9399
9400fn claim_area(
9410 chain: &[frus_widgets::Scrollable],
9411 under: WidgetId,
9412 down: bool,
9413) -> Option<frus_widgets::Scrollable> {
9414 chain
9415 .iter()
9416 .find(|a| a.takes_user_scroll(down))
9420 .or_else(|| {
9421 chain
9422 .iter()
9423 .find(|a| a.id == under && a.physics.user() != frus_widgets::UserScroll::Never)
9424 })
9425 .copied()
9426}
9427
9428#[cfg(feature = "net")]
9443fn fetch_image_bytes(
9444 url: &str,
9445 deliver: Box<dyn FnOnce(Result<Vec<u8>, String>) + Send + 'static>,
9446) {
9447 const DEADLINE: std::time::Duration = std::time::Duration::from_secs(30);
9450
9451 let url = url.to_string();
9452 let run = async move {
9453 let result = crate::net::Request::get(url)
9454 .timeout(DEADLINE)
9455 .send_bytes()
9456 .await;
9457 deliver(result.map_err(|e| e.to_string()));
9458 };
9459 #[cfg(not(web))]
9460 crate::runtime::spawn(run).detach();
9461 #[cfg(web)]
9462 wasm_bindgen_futures::spawn_local(run);
9463}
9464
9465#[cfg(test)]
9466mod debug_keys_tests {
9467 use super::*;
9468
9469 #[test]
9470 fn the_graphs_keep_the_latest_frames_and_mark_the_late_ones() {
9471 let mut history = FrameHistory::default();
9472 for i in 0..100 {
9473 history.push(&crate::frame_stats::FrameCosts {
9474 build: Some(1.0),
9475 layout_paint: i as f32,
9476 render: 5.0,
9477 ..Default::default()
9478 });
9479 }
9480 assert_eq!(history.ui.len(), HISTORY, "the oldest frames go");
9481 assert_eq!(
9482 history.ui.back().copied(),
9483 Some(100.0),
9484 "the newest is last"
9485 );
9486 let bars = FrameHistory::bars(&history.render, 36.0);
9487 assert!(bars.iter().all(|(_, over)| !over), "5 ms is within budget");
9488 let ui = FrameHistory::bars(&history.ui, 36.0);
9489 assert_eq!(
9490 ui.last().copied(),
9491 Some((36.0, true)),
9492 "over the top fills the graph"
9493 );
9494 let half = FrameHistory::bars(&[BUDGET_MS].into_iter().collect(), 36.0);
9495 assert!((half[0].0 - 18.0).abs() < 0.01 && !half[0].1, "{half:?}");
9496 }
9497
9498 #[test]
9499 fn the_platform_goes_round_as_the_reference_does() {
9500 use frus_widgets::TargetPlatform as P;
9501 let mut seen = vec![P::Android];
9502 for _ in 0..6 {
9503 seen.push(next_platform(*seen.last().unwrap()));
9504 }
9505 assert_eq!(
9506 seen,
9507 [
9508 P::Android,
9509 P::Ios,
9510 P::Windows,
9511 P::MacOs,
9512 P::Linux,
9513 P::Fuchsia,
9514 P::Android
9515 ]
9516 );
9517 }
9518
9519 #[test]
9520 fn the_dumps_say_what_there_is() {
9521 let id = WidgetId::from_u64(0x1234_5678);
9522 let other = WidgetId::from_u64(0x9);
9523 let nodes = [frus_widgets::InspectorNode {
9524 id,
9525 rect: Rect::new(10.0, 20.0, 100.0, 40.0),
9526 name: "Button",
9527 depth: 3,
9528 }];
9529 let focus = dump_focus(&nodes, &[id, other], Some(id));
9530 assert_eq!(focus, "▶ Button 10,20 100×40 #12345678\n #00000009\n");
9531 assert_eq!(dump_focus(&nodes, &[], None), " (nothing focusable)\n");
9532
9533 let semantics = [(
9534 id,
9535 Rect::new(0.0, 0.0, 50.0, 20.0),
9536 frus_widgets::SemanticsProperties::new(frus_widgets::Role::Button).label("Save"),
9537 )];
9538 let dump = dump_semantics(&semantics);
9539 assert!(dump.starts_with(" Button \"Save\""), "{dump}");
9540 assert!(dump.ends_with("0,0 50×20\n"), "{dump}");
9541 }
9542}
9543
9544#[cfg(test)]
9545mod tests {
9546 use super::{
9547 build_view, claim_area, claim_axis, draw_ghost_card, drop_insertion_line, fling_velocity,
9548 gesture_was_a_tap, ghost_offset, install_ambient, reorder_motion, resolve_focus,
9549 spring_toward, Drag, Point, Rect, ReorderAxis, ReorderMotion, Scene, Theme,
9550 VelocityEstimate, PRECISE_SLOP, TOUCH_SLOP,
9551 };
9552 use super::{clipboard_command, ClipCommand, KeyCode, PhysicalKey, WinitKey};
9553 use super::{collect_ids, find_widget, MediaQuery};
9554 use frus_widgets::Locale;
9555
9556 #[test]
9559 fn select_all_hides_the_bar_on_the_desktops() {
9560 use frus_widgets::TargetPlatform as P;
9561 for p in [P::MacOs, P::Linux, P::Windows] {
9562 assert!(super::select_all_hides_bar(p), "{p:?}");
9563 }
9564 for p in [P::Android, P::Ios, P::Fuchsia] {
9565 assert!(!super::select_all_hides_bar(p), "{p:?}");
9566 }
9567 }
9568
9569 #[test]
9573 fn the_context_menu_key_and_shift_f10_open_the_bar() {
9574 use super::opens_selection_bar;
9575 assert!(opens_selection_bar(
9576 &WinitKey::Named(winit::keyboard::NamedKey::ContextMenu),
9577 false
9578 ));
9579 assert!(opens_selection_bar(
9580 &WinitKey::Named(winit::keyboard::NamedKey::F10),
9581 true
9582 ));
9583 assert!(!opens_selection_bar(
9584 &WinitKey::Named(winit::keyboard::NamedKey::F10),
9585 false
9586 ));
9587 assert!(!opens_selection_bar(
9588 &WinitKey::Named(winit::keyboard::NamedKey::Enter),
9589 true
9590 ));
9591 assert!(!opens_selection_bar(&character("m"), true));
9592 }
9593
9594 #[test]
9598 fn a_copy_collapses_the_selection_to_its_end() {
9599 use frus_widgets::Edit;
9600 let forwards = Edit {
9601 cursor: 11,
9602 anchor: Some(6),
9603 composing: None,
9604 };
9605 let backwards = Edit {
9606 cursor: 6,
9607 anchor: Some(11),
9608 composing: None,
9609 };
9610 let expect = Edit {
9611 cursor: 11,
9612 anchor: None,
9613 composing: None,
9614 };
9615 assert_eq!(super::collapsed_to_end(forwards), expect);
9616 assert_eq!(super::collapsed_to_end(backwards), expect);
9617 let caret = Edit {
9619 cursor: 3,
9620 anchor: None,
9621 composing: None,
9622 };
9623 assert_eq!(super::collapsed_to_end(caret), caret);
9624 let empty = Edit {
9626 cursor: 3,
9627 anchor: Some(3),
9628 composing: Some((1, 2)),
9629 };
9630 assert_eq!(super::collapsed_to_end(empty).anchor, None);
9631 assert_eq!(super::collapsed_to_end(empty).cursor, 3);
9632 }
9633
9634 #[test]
9640 fn a_field_with_a_clear_button_still_wants_the_keyboard() {
9641 use frus_widgets::{Container, Icons, TextField};
9642 let clearable = TextField::<()>::new("F")
9643 .suffix_icon(Icons::CLOSE)
9644 .on_suffix(());
9645 assert!(
9646 super::wants_keyboard(&clearable),
9647 "the field with its clear button"
9648 );
9649 assert!(
9650 !super::wants_keyboard(&TextField::<()>::new("F").enabled(false)),
9651 "a disabled field takes no typing"
9652 );
9653 assert!(
9654 !super::wants_keyboard(&Container::<()>::new()),
9655 "and what is not a field wants no keyboard"
9656 );
9657 }
9658
9659 #[test]
9666 fn a_field_with_a_clear_button_is_a_text_field_and_keeps_its_arrows() {
9667 use frus_widgets::{Container, Icons, TextField};
9668 let clearable = TextField::<()>::new("hello")
9669 .suffix_icon(Icons::CLOSE)
9670 .on_suffix(());
9671 assert!(
9672 frus_widgets::Widget::<()>::cursor_at(&clearable, 0.0, 0.0, 1.0, 0).is_none(),
9673 "the trap: the probe finds no caret at the corner a clear button covers"
9674 );
9675 assert!(
9676 super::is_text_field(&clearable),
9677 "and it is a text field all the same"
9678 );
9679 assert!(
9680 super::is_text_field(&TextField::<()>::new("")),
9681 "empty, too"
9682 );
9683 assert!(
9684 !super::is_text_field(&Container::<()>::new()),
9685 "what is not a field leaves its arrows to focus navigation"
9686 );
9687 }
9688
9689 #[test]
9695 fn a_text_that_can_only_be_read_wants_no_keyboard() {
9696 use frus_widgets::{Text, TextField};
9697 let read_only = TextField::<()>::new("REF-4417").read_only();
9698 assert!(super::is_text_field(&read_only), "it has a caret");
9699 assert!(
9700 !super::wants_keyboard(&read_only),
9701 "and no use for a keyboard"
9702 );
9703
9704 let selectable = Text::new("ORDER-4417").selectable();
9705 assert!(
9706 super::is_text_field::<()>(&selectable),
9707 "the arrows move its caret"
9708 );
9709 assert!(!super::wants_keyboard::<()>(&selectable));
9710
9711 assert!(
9712 super::wants_keyboard(&TextField::<()>::new("typed")),
9713 "and a field that can be typed into still does"
9714 );
9715 assert!(
9716 !super::is_text_field::<()>(&Text::new("ORDER-4417")),
9717 "an ordinary text leaves its arrows to focus navigation"
9718 );
9719 }
9720
9721 fn character(c: &str) -> WinitKey {
9722 WinitKey::Character(c.into())
9723 }
9724
9725 #[test]
9728 fn ctrl_c_x_v_are_the_clipboard_and_the_letters_alone_are_not() {
9729 let any = PhysicalKey::Code(KeyCode::KeyQ);
9730 for (letter, command) in [
9731 ("c", ClipCommand::Copy),
9732 ("X", ClipCommand::Cut),
9733 ("v", ClipCommand::Paste),
9734 ] {
9735 assert_eq!(
9736 clipboard_command(&character(letter), any, true),
9737 Some(command)
9738 );
9739 assert_eq!(
9740 clipboard_command(&character(letter), any, false),
9741 None,
9742 "{letter}"
9743 );
9744 }
9745 assert_eq!(clipboard_command(&character("a"), any, true), None);
9746 }
9747
9748 #[test]
9752 fn a_keyboards_own_clipboard_keys_work_by_their_physical_code() {
9753 let unidentified = WinitKey::Unidentified(winit::keyboard::NativeKey::Unidentified);
9754 for (code, command) in [
9755 (KeyCode::Copy, ClipCommand::Copy),
9756 (KeyCode::Cut, ClipCommand::Cut),
9757 (KeyCode::Paste, ClipCommand::Paste),
9758 ] {
9759 assert_eq!(
9760 clipboard_command(&unidentified, PhysicalKey::Code(code), false),
9761 Some(command),
9762 "{code:?}"
9763 );
9764 }
9765 let named = WinitKey::Named(super::NamedKey::Paste);
9767 let any = PhysicalKey::Code(KeyCode::KeyQ);
9768 assert_eq!(
9769 clipboard_command(&named, any, false),
9770 Some(ClipCommand::Paste)
9771 );
9772 }
9773
9774 #[test]
9780 fn a_paste_lands_in_the_field_that_asked_only_while_it_has_the_focus() {
9781 use super::clip::Pasted;
9782 let field = WidgetId::from_u64(1);
9783 let other = WidgetId::from_u64(2);
9784 let pasted = Pasted {
9785 into: field,
9786 text: "copied".into(),
9787 };
9788 assert!(pasted.lands(Some(field)));
9789 assert!(
9790 !pasted.lands(Some(other)),
9791 "the focus moved to another field"
9792 );
9793 assert!(!pasted.lands(None), "the focus left every field");
9794 let nothing = Pasted {
9795 into: field,
9796 text: String::new(),
9797 };
9798 assert!(!nothing.lands(Some(field)), "an empty clipboard");
9799 }
9800
9801 #[test]
9806 fn a_paste_answered_later_waits_for_the_frame_that_takes_it() {
9807 use super::clip::{Answers, Pasted};
9808 use std::cell::Cell;
9809 use std::rc::Rc;
9810 let first = WidgetId::from_u64(1);
9811 let second = WidgetId::from_u64(2);
9812 let answers = Answers::default();
9813 let wakes = Rc::new(Cell::new(0));
9814 let wake = || {
9815 let wakes = Rc::clone(&wakes);
9816 move || wakes.set(wakes.get() + 1)
9817 };
9818 let to_first = answers.answer_for(first, wake());
9819 let to_second = answers.answer_for(second, wake());
9820 assert_eq!(answers.take(), None, "nothing is answered yet");
9821 assert_eq!(wakes.get(), 0, "asking is not answering");
9822
9823 to_second("later".into());
9824 assert_eq!(wakes.get(), 1);
9825 to_first("sooner".into());
9826 assert_eq!(wakes.get(), 2);
9827
9828 assert_eq!(
9829 answers.take(),
9830 Some(Pasted {
9831 into: second,
9832 text: "later".into()
9833 })
9834 );
9835 assert_eq!(
9836 answers.take(),
9837 Some(Pasted {
9838 into: first,
9839 text: "sooner".into()
9840 })
9841 );
9842 assert_eq!(answers.take(), None, "each answer is taken once");
9843 }
9844
9845 struct Deferred;
9854
9855 impl crate::Application for Deferred {
9856 type Message = ();
9857
9858 fn update(&mut self, _message: ()) -> crate::Command<()> {
9859 crate::Command::none()
9860 }
9861
9862 fn view(&self, _theme: &Theme) -> Box<dyn frus_widgets::Widget<()>> {
9863 Box::new(frus_widgets::ThemeBuilder::new(|_: &Theme| {
9864 frus_widgets::Flex::column()
9865 .width(300.0)
9866 .height(80.0)
9867 .child(
9868 frus_widgets::TextField::new("hi")
9869 .width(200.0)
9870 .on_input(|_| ()),
9871 )
9872 }))
9873 }
9874 }
9875
9876 fn surfaced<R>(f: impl FnOnce() -> R) -> R {
9879 MediaQuery::new(frus_widgets::Size::new(300.0, 80.0)).scope(f)
9880 }
9881
9882 #[test]
9892 fn a_shell_installs_the_reader_s_language() {
9893 struct Speaks(Option<Locale>);
9896
9897 impl crate::Application for Speaks {
9898 type Message = ();
9899
9900 fn update(&mut self, _message: ()) -> crate::Command<()> {
9901 crate::Command::none()
9902 }
9903
9904 fn view(&self, _theme: &Theme) -> Box<dyn frus_widgets::Widget<()>> {
9905 Box::new(frus_widgets::Flex::<()>::column())
9906 }
9907
9908 fn supported_locales(&self) -> Vec<Locale> {
9909 vec![
9910 Locale::new("en"),
9911 Locale::new("fr"),
9912 Locale::with_country("fr", "CA"),
9913 ]
9914 }
9915
9916 fn locale(&self) -> Option<Locale> {
9917 self.0.clone()
9918 }
9919 }
9920
9921 install_ambient(&Speaks(None), &[Locale::with_country("fr", "BE")]);
9923 assert_eq!(frus_widgets::locale::of(), Locale::new("fr"));
9924
9925 install_ambient(&Speaks(None), &[]);
9928 assert_eq!(frus_widgets::locale::of(), Locale::new("en"));
9929
9930 install_ambient(
9933 &Speaks(Some(Locale::with_country("fr", "FR"))),
9934 &[Locale::new("en")],
9935 );
9936 assert_eq!(frus_widgets::locale::of(), Locale::new("fr"));
9937
9938 install_ambient(&Deferred, &[Locale::new("fr")]);
9940 assert_eq!(
9941 frus_widgets::locale::of(),
9942 Locale::new("en"),
9943 "it supports only English, so French resolves to it"
9944 );
9945 }
9946
9947 #[test]
9955 fn a_shell_installs_the_application_s_words() {
9956 struct Fr;
9957
9958 impl frus_widgets::localizations::Localizations for Fr {
9959 fn back_button_label(&self) -> &str {
9960 "Retour"
9961 }
9962
9963 fn first_day_of_week_index(&self) -> usize {
9964 1
9965 }
9966 }
9967
9968 struct Speaks;
9969
9970 impl crate::Application for Speaks {
9971 type Message = ();
9972
9973 fn update(&mut self, _message: ()) -> crate::Command<()> {
9974 crate::Command::none()
9975 }
9976
9977 fn view(&self, _theme: &Theme) -> Box<dyn frus_widgets::Widget<()>> {
9978 Box::new(frus_widgets::Flex::<()>::column())
9979 }
9980
9981 fn localizations(&self) -> Option<std::rc::Rc<dyn frus_widgets::Localizations>> {
9982 Some(std::rc::Rc::new(Fr))
9983 }
9984 }
9985
9986 install_ambient(&Speaks, &[]);
9987 assert_eq!(
9988 frus_widgets::localizations::of().back_button_label(),
9989 "Retour",
9990 "the shell read the application's table and installed it"
9991 );
9992 assert_eq!(
9993 frus_widgets::localizations::of().first_day_of_week_index(),
9994 1
9995 );
9996
9997 install_ambient(&Deferred, &[]);
10000 assert_eq!(
10001 frus_widgets::localizations::of().back_button_label(),
10002 "Retour",
10003 "saying nothing is not the same as saying English"
10004 );
10005 }
10006
10007 #[test]
10010 fn the_scroll_behaviour_at_the_root_is_the_applications() {
10011 struct Quiet;
10012
10013 impl crate::Application for Quiet {
10014 type Message = ();
10015
10016 fn update(&mut self, _message: ()) -> crate::Command<()> {
10017 crate::Command::none()
10018 }
10019
10020 fn view(&self, _theme: &Theme) -> Box<dyn frus_widgets::Widget<()>> {
10021 Box::new(frus_widgets::Flex::<()>::column())
10022 }
10023
10024 fn scroll_behavior(&self) -> frus_widgets::ScrollBehavior {
10025 frus_widgets::ScrollBehavior::material().with_overscroll(false)
10026 }
10027 }
10028
10029 struct Silent;
10030
10031 impl crate::Application for Silent {
10032 type Message = ();
10033
10034 fn update(&mut self, _message: ()) -> crate::Command<()> {
10035 crate::Command::none()
10036 }
10037
10038 fn view(&self, _theme: &Theme) -> Box<dyn frus_widgets::Widget<()>> {
10039 Box::new(frus_widgets::Flex::<()>::column())
10040 }
10041 }
10042
10043 install_ambient(&Quiet, &[]);
10044 assert_eq!(
10045 frus_widgets::ScrollConfiguration::of(),
10046 frus_widgets::ScrollBehavior::material().with_overscroll(false)
10047 );
10048 install_ambient(&Silent, &[]);
10049 assert_eq!(
10050 frus_widgets::ScrollConfiguration::of(),
10051 frus_widgets::ScrollBehavior::material()
10052 );
10053 }
10054
10055 #[test]
10066 fn a_view_is_built_ready_to_be_read() {
10067 let theme = Theme::default();
10068 let prepared =
10069 surfaced(|| build_view(&Deferred, &theme, &frus_widgets::Runtime::default()));
10070 let ids = collect_ids(prepared.as_ref());
10071 assert!(
10072 ids.len() > 1,
10073 "an application inside a deferred subtree has identities to mount: {ids:?}"
10074 );
10075 for id in &ids {
10077 assert!(
10078 find_widget(prepared.as_ref(), *id).is_some(),
10079 "{id:?} was counted but cannot be found"
10080 );
10081 }
10082 }
10083
10084 #[test]
10086 #[should_panic(expected = "before it was built")]
10087 fn an_unprepared_view_is_the_state_that_broke() {
10088 let theme = Theme::default();
10089 let raw = surfaced(|| crate::Application::view(&Deferred, &theme));
10090 let _ = collect_ids(raw.as_ref());
10091 }
10092
10093 #[test]
10101 fn a_swipe_that_never_started_is_still_a_tap() {
10102 let item = frus_widgets::Dismissable {
10103 id: WidgetId::from_u64(1),
10104 rect: Rect::new(0.0, 0.0, 300.0, 62.0),
10105 spec: frus_widgets::DismissSpec::default(),
10106 };
10107 let dismiss = |moved| Drag::Dismiss {
10108 item,
10109 last: Point::new(0.0, 0.0),
10110 moved,
10111 };
10112 assert!(
10113 gesture_was_a_tap(Some(&dismiss(false))),
10114 "a swipe under the threshold leaves the click alone"
10115 );
10116 assert!(
10117 !gesture_was_a_tap(Some(&dismiss(true))),
10118 "a swipe that ran has already been answered and must not also click"
10119 );
10120 let scroll = Drag::Scroll {
10123 id: WidgetId::from_u64(2),
10124 last: Point::new(0.0, 0.0),
10125 moved: false,
10126 carried: (0.0, 0.0),
10127 dismiss: None,
10128 axis: None,
10129 pan: None,
10130 };
10131 assert!(gesture_was_a_tap(Some(&scroll)));
10132 let sheet = |moved| Drag::Sheet {
10135 id: WidgetId::from_u64(4),
10136 last: Point::new(0.0, 0.0),
10137 moved,
10138 available: 800.0,
10139 };
10140 assert!(gesture_was_a_tap(Some(&sheet(false))));
10141 assert!(!gesture_was_a_tap(Some(&sheet(true))));
10142 let select = Drag::TextSelect {
10145 id: WidgetId::from_u64(3),
10146 rect: Rect::new(0.0, 0.0, 10.0, 10.0),
10147 };
10148 assert!(!gesture_was_a_tap(Some(&select)));
10149 assert!(!gesture_was_a_tap(None), "no gesture, no verdict to give");
10150 }
10151
10152 #[test]
10156 fn a_scroll_gesture_is_claimed_by_one_axis() {
10157 assert!(claim_axis(6.0, 40.0, true, true), "down the page");
10160 assert!(!claim_axis(40.0, 6.0, true, true), "across it");
10161 assert!(claim_axis(10.0, 10.0, true, true));
10163 assert!(claim_axis(40.0, 6.0, false, true), "a list only goes down");
10166 assert!(
10167 !claim_axis(6.0, 40.0, true, false),
10168 "a strip only goes across"
10169 );
10170 }
10171
10172 fn area(id: u64, max_x: f32, max_y: f32) -> frus_widgets::Scrollable {
10174 frus_widgets::Scrollable {
10175 id: WidgetId::from_u64(id),
10176 viewport: Rect::new(0.0, 0.0, 100.0, 100.0),
10177 max_x,
10178 max_y,
10179 axis: frus_widgets::Axis::Vertical,
10180 physics: frus_widgets::ScrollPhysics::CLAMPING,
10181 overscroll: Some(frus_widgets::OverscrollIndicator::Glow),
10182 fling: frus_widgets::VelocityStrategy::Regression,
10183 refresh: None,
10184 page: None,
10185 reverse_x: false,
10186 reverse_y: false,
10187 host: None,
10188 keep_visible: None,
10189 }
10190 }
10191
10192 #[test]
10196 fn the_gesture_goes_to_the_area_that_can_take_it() {
10197 let strip = area(1, 400.0, 0.0);
10198 let page = area(2, 0.0, 900.0);
10199 let chain = [strip, page]; assert_eq!(
10201 claim_area(&chain, strip.id, true).map(|a| a.id),
10202 Some(page.id),
10203 "down the page, over the strip"
10204 );
10205 assert_eq!(
10206 claim_area(&chain, strip.id, false).map(|a| a.id),
10207 Some(strip.id),
10208 "across the strip"
10209 );
10210 assert_eq!(
10213 claim_area(&[strip], strip.id, true).map(|a| a.id),
10214 Some(strip.id)
10215 );
10216 let inner = area(3, 0.0, 200.0);
10218 assert_eq!(
10219 claim_area(&[inner, strip, page], inner.id, true).map(|a| a.id),
10220 Some(inner.id)
10221 );
10222 }
10223
10224 #[test]
10228 fn the_physics_have_the_last_word_on_a_finger() {
10229 let mut inner = area(1, 0.0, 300.0);
10230 inner.physics = frus_widgets::ScrollPhysics::NEVER;
10231 assert!(!inner.accepts_user_offset((0.0, 0.0)));
10232 assert!(!inner.takes_user_scroll(true));
10233 let outer = area(2, 0.0, 600.0);
10234 let chain = [inner, outer];
10235 assert_eq!(
10236 claim_area(&chain, WidgetId::from_u64(1), true).map(|a| a.id),
10237 Some(WidgetId::from_u64(2)),
10238 "the page around it scrolls"
10239 );
10240 assert_eq!(
10241 claim_area(&[inner], WidgetId::from_u64(1), true).map(|a| a.id),
10242 None,
10243 "and alone it keeps nothing"
10244 );
10245
10246 let mut short = area(3, 0.0, 0.0);
10247 short.physics = frus_widgets::ScrollPhysics::ALWAYS;
10248 assert!(short.accepts_user_offset((0.0, 0.0)));
10249 assert!(short.takes_user_scroll(true), "along its axis");
10250 assert!(!short.takes_user_scroll(false), "and not across it");
10251 assert_eq!(
10252 claim_area(&[short, outer], WidgetId::from_u64(3), true).map(|a| a.id),
10253 Some(WidgetId::from_u64(3)),
10254 "a short list that always scrolls keeps the drag from the page"
10255 );
10256 let mut strip = short;
10257 strip.axis = frus_widgets::Axis::Horizontal;
10258 assert_eq!(
10259 claim_area(&[strip, outer], WidgetId::from_u64(3), true).map(|a| a.id),
10260 Some(WidgetId::from_u64(2)),
10261 "a strip that always scrolls across leaves a drag down to the page"
10262 );
10263 let mut both = short;
10264 both.axis = frus_widgets::Axis::Both;
10265 assert!(both.takes_user_scroll(false) && both.takes_user_scroll(true));
10266 }
10267
10268 #[test]
10272 fn an_area_whose_content_fits_takes_no_finger() {
10273 let fits = area(1, 0.0, 0.0);
10274 assert!(!fits.accepts_user_offset((0.0, 0.0)));
10275 assert!(fits.accepts_user_offset((0.0, 12.0)));
10278 let mut pullable = fits;
10280 pullable.refresh = Some(WidgetId::from_u64(9));
10281 assert!(pullable.accepts_user_offset((0.0, 0.0)));
10282 assert!(area(1, 0.0, 1.0).accepts_user_offset((0.0, 0.0)));
10284 assert!(area(1, 1.0, 0.0).accepts_user_offset((0.0, 0.0)));
10285 }
10286 use frus_widgets::WidgetId;
10287 use std::collections::HashSet;
10288
10289 fn released(velocity: (f32, f32), offset: (f32, f32)) -> VelocityEstimate {
10291 VelocityEstimate {
10292 velocity: frus_widgets::Velocity::new(velocity.0, velocity.1),
10293 confidence: 1.0,
10294 duration: 0.05,
10295 offset,
10296 }
10297 }
10298
10299 #[test]
10300 fn a_fast_twitch_that_went_nowhere_is_not_a_fling() {
10301 let (x, y) = fling_velocity(released((0.0, 2000.0), (0.0, 3.0)), TOUCH_SLOP);
10303 assert_eq!((x, y), (0.0, 0.0));
10304 let (_, y) = fling_velocity(released((0.0, 2000.0), (0.0, 60.0)), TOUCH_SLOP);
10306 assert_eq!(y, 2000.0);
10307 }
10308
10309 #[test]
10310 fn the_fling_gate_is_per_axis() {
10311 let (x, y) = fling_velocity(released((300.0, 1500.0), (5.0, 120.0)), TOUCH_SLOP);
10314 assert_eq!(x, 0.0, "the wobble is not a horizontal fling");
10315 assert_eq!(y, 1500.0, "the swipe still flings vertically");
10316 }
10317
10318 #[test]
10319 fn a_precise_pointer_needs_almost_no_travel() {
10320 let estimate = released((0.0, 900.0), (0.0, 3.0));
10322 assert_eq!(fling_velocity(estimate, TOUCH_SLOP).1, 0.0);
10323 assert_eq!(fling_velocity(estimate, PRECISE_SLOP).1, 900.0);
10324 }
10325
10326 #[test]
10327 fn insertion_line_sits_on_the_target_top_edge() {
10328 let line = drop_insertion_line(
10331 Rect::new(20.0, 100.0, 200.0, 44.0),
10332 4.0,
10333 false,
10334 ReorderAxis::Vertical,
10335 false,
10336 );
10337 assert_eq!(line.x, 20.0, "aligned to the target's left");
10338 assert_eq!(line.width, 200.0, "the target's full width");
10339 assert_eq!(line.y, 98.0, "centred on the top edge (100 - 4/2)");
10340 assert_eq!(line.height, 4.0);
10341 }
10342
10343 #[test]
10344 fn insertion_line_sits_on_the_target_bottom_edge_when_inserting_after() {
10345 let line = drop_insertion_line(
10348 Rect::new(20.0, 100.0, 200.0, 44.0),
10349 4.0,
10350 true,
10351 ReorderAxis::Vertical,
10352 false,
10353 );
10354 assert_eq!(line.x, 20.0, "still aligned to the left");
10355 assert_eq!(line.width, 200.0, "the target's full width");
10356 assert_eq!(line.y, 142.0, "centred on the bottom edge (144 - 4/2)");
10357 assert_eq!(line.height, 4.0);
10358 let rtl = drop_insertion_line(
10360 Rect::new(20.0, 100.0, 200.0, 44.0),
10361 4.0,
10362 true,
10363 ReorderAxis::Vertical,
10364 true,
10365 );
10366 assert_eq!(rtl, line, "right to left changes nothing down a list");
10367 }
10368
10369 #[test]
10373 fn a_horizontal_insertion_line_stands_on_the_edge_the_row_goes_to() {
10374 let chip = Rect::new(100.0, 20.0, 80.0, 48.0);
10375 let line = |after, rtl| drop_insertion_line(chip, 4.0, after, ReorderAxis::Horizontal, rtl);
10376 let before = line(false, false);
10377 assert_eq!(
10378 (before.x, before.y, before.width, before.height),
10379 (98.0, 20.0, 4.0, 48.0),
10380 "before a row: its left edge, the full height, 4 wide"
10381 );
10382 assert_eq!(
10383 line(true, false).x,
10384 178.0,
10385 "after it: its right edge (180 - 4/2)"
10386 );
10387 assert_eq!(
10389 line(true, true).x,
10390 98.0,
10391 "after, right to left: the left edge"
10392 );
10393 assert_eq!(
10394 line(false, true).x,
10395 178.0,
10396 "before, right to left: the right edge"
10397 );
10398 }
10399
10400 #[test]
10404 fn a_horizontal_reorderable_inserts_only_when_it_says_so() {
10405 assert_eq!(
10406 ReorderMotion::of(ReorderAxis::Horizontal, false),
10407 ReorderMotion::Columns,
10408 "a table's column"
10409 );
10410 assert_eq!(
10411 ReorderMotion::of(ReorderAxis::Horizontal, true),
10412 ReorderMotion::Slots(ReorderAxis::Horizontal),
10413 "a horizontal list's row"
10414 );
10415 for inserts in [false, true] {
10417 assert_eq!(
10418 ReorderMotion::of(ReorderAxis::Vertical, inserts),
10419 ReorderMotion::Slots(ReorderAxis::Vertical)
10420 );
10421 }
10422 assert_eq!(
10424 reorder_motion::<()>(&frus_widgets::Container::new()),
10425 ReorderMotion::Columns
10426 );
10427 for (axis, expected) in [
10429 (
10430 ReorderAxis::Vertical,
10431 ReorderMotion::Slots(ReorderAxis::Vertical),
10432 ),
10433 (
10434 ReorderAxis::Horizontal,
10435 ReorderMotion::Slots(ReorderAxis::Horizontal),
10436 ),
10437 ] {
10438 let list = frus_widgets::ReorderableList::<()>::new(|_, _| ())
10439 .axis(axis)
10440 .row(frus_widgets::Container::new().width(60.0));
10441 let row = frus_widgets::Widget::children(&list)[0].as_ref();
10442 assert_eq!(reorder_motion(row), expected, "{axis:?}");
10443 }
10444 }
10445
10446 #[test]
10450 fn a_horizontal_rows_ghost_follows_the_finger_along_x_only() {
10451 let travel = (120.0, 35.0);
10452 assert_eq!(
10453 ghost_offset(ReorderMotion::Columns, travel),
10454 (120.0, super::drag_preview::LIFT_Y)
10455 );
10456 assert_eq!(
10457 ghost_offset(ReorderMotion::Slots(ReorderAxis::Vertical), travel),
10458 (120.0, 35.0)
10459 );
10460 assert_eq!(
10461 ghost_offset(ReorderMotion::Slots(ReorderAxis::Horizontal), travel),
10462 (120.0, 0.0)
10463 );
10464 }
10465
10466 #[test]
10467 fn focus_returns_to_trigger_when_overlay_closes() {
10468 let anchor = WidgetId::from_u64(1);
10469 let item = WidgetId::from_u64(2);
10470 let (mut history, mut prev) = (Vec::new(), None);
10471
10472 let f = resolve_focus(
10474 Some(anchor),
10475 &HashSet::from([anchor]),
10476 &mut history,
10477 &mut prev,
10478 );
10479 assert_eq!(f, Some(anchor));
10480 assert!(history.is_empty());
10481
10482 let present = HashSet::from([anchor, item]);
10484 let f = resolve_focus(Some(item), &present, &mut history, &mut prev);
10485 assert_eq!(f, Some(item));
10486 assert_eq!(history, vec![anchor]);
10487
10488 let f = resolve_focus(
10490 Some(item),
10491 &HashSet::from([anchor]),
10492 &mut history,
10493 &mut prev,
10494 );
10495 assert_eq!(f, Some(anchor), "focus returns to the trigger");
10496 assert!(history.is_empty());
10497 }
10498
10499 #[test]
10500 fn focus_falls_to_none_when_no_trigger_remains() {
10501 let a = WidgetId::from_u64(1);
10502 let (mut history, mut prev) = (Vec::new(), Some(a));
10503 let f = resolve_focus(Some(a), &HashSet::new(), &mut history, &mut prev);
10505 assert_eq!(f, None);
10506 assert_eq!(prev, None);
10507 }
10508
10509 #[test]
10510 fn spring_approaches_target_monotonically_and_settles() {
10511 let mut x = 0.0;
10514 let mut prev = -1.0;
10515 for _ in 0..30 {
10516 x = spring_toward(x, 100.0, 0.016, 0.07);
10517 assert!(x > prev && x <= 100.0, "monotonic and bounded: {x}");
10518 prev = x;
10519 }
10520 assert!(x > 99.0, "all but reached after ~0.5 s: {x}");
10521 }
10522
10523 #[test]
10524 fn ghost_card_shape() {
10525 let theme = Theme::default();
10526 let card = Rect::new(140.0, 0.0, 80.0, 34.0);
10527 let mut scene = Scene::new();
10529 draw_ghost_card(&mut scene, &theme, card, &[]);
10530 assert_eq!(scene.primitives().len(), 2, "shadow plus solid card");
10531 }
10532}
10533
10534#[cfg(test)]
10537mod sheet_request_tests {
10538 use super::{apply_sheet_requests, Runtime, Widget};
10539 use crate::command::Command;
10540 use frus_widgets::{
10541 build_ui, keyed, Container, DraggableScrollableSheet, SheetTo, Size, Theme,
10542 };
10543
10544 fn view() -> Box<dyn Widget<()>> {
10547 Box::new(
10548 Container::<()>::new()
10549 .width(400.0)
10550 .height(800.0)
10551 .child(keyed(
10552 "places",
10553 DraggableScrollableSheet::<()>::new(Container::new()),
10554 )),
10555 )
10556 }
10557
10558 #[test]
10561 fn a_request_reaches_the_sheet_the_view_named() {
10562 let tree = view();
10563 let mut runtime = Runtime::default();
10564 let sheets = build_ui(
10565 tree.as_ref(),
10566 Size::new(400.0, 800.0),
10567 &runtime,
10568 &Theme::default(),
10569 )
10570 .sheets()
10571 .to_vec();
10572 let sheet = sheets.first().expect("the page has its sheet").clone();
10573
10574 let requests = Command::<()>::sheet("places", SheetTo::size(0.9))
10575 .into_parts()
10576 .sheets;
10577 let (unplaced, moved) =
10578 apply_sheet_requests(&mut runtime, tree.as_ref(), requests, &sheets);
10579 assert!(unplaced.is_empty());
10580 assert!(moved);
10581 assert_eq!(runtime.sheet_size(sheet.id, &sheet.spec), 0.9);
10582
10583 let elsewhere = Command::<()>::sheet("elsewhere", SheetTo::size(0.3))
10584 .into_parts()
10585 .sheets;
10586 let (unplaced, moved) =
10587 apply_sheet_requests(&mut runtime, tree.as_ref(), elsewhere, &sheets);
10588 assert_eq!(unplaced.len(), 1);
10589 assert!(!moved);
10590 let not_yet = Command::<()>::sheet("places", SheetTo::size(0.3))
10591 .into_parts()
10592 .sheets;
10593 let (unplaced, _) = apply_sheet_requests(&mut runtime, tree.as_ref(), not_yet, &[]);
10594 assert_eq!(unplaced.len(), 1, "kept for the frame that has the sheet");
10595 assert_eq!(
10596 runtime.sheet_size(sheet.id, &sheet.spec),
10597 0.9,
10598 "nothing moved"
10599 );
10600 }
10601}
10602
10603#[cfg(test)]
10604mod scroll_request_tests {
10605 use super::{apply_scroll_requests, Rect, Runtime, Scrollable, Widget, WidgetId};
10606 use crate::command::Command;
10607 use frus_widgets::{keyed, Container, ScrollTo, SingleChildScrollView};
10608
10609 fn view() -> Box<dyn Widget<()>> {
10611 Box::new(Container::<()>::new().child(keyed(
10612 "log",
10613 SingleChildScrollView::<()>::new().height(200.0),
10614 )))
10615 }
10616
10617 fn region(id: WidgetId) -> Scrollable {
10619 Scrollable {
10620 id,
10621 viewport: Rect {
10622 x: 0.0,
10623 y: 0.0,
10624 width: 300.0,
10625 height: 200.0,
10626 },
10627 max_x: 0.0,
10628 max_y: 1000.0,
10629 axis: frus_widgets::Axis::Vertical,
10630 physics: frus_widgets::ScrollPhysics::CLAMPING,
10631 overscroll: Some(frus_widgets::OverscrollIndicator::Glow),
10632 fling: frus_widgets::VelocityStrategy::Regression,
10633 refresh: None,
10634 page: None,
10635 reverse_x: false,
10636 reverse_y: false,
10637 keep_visible: None,
10638 host: None,
10639 }
10640 }
10641
10642 #[test]
10646 fn a_request_reaches_the_region_the_view_named() {
10647 let tree = view();
10648 let requests = Command::<()>::scroll("log", ScrollTo::end().instant())
10649 .into_parts()
10650 .scrolls;
10651 let id = frus_widgets::find_by_key(tree.as_ref(), requests[0].0)
10652 .expect("the view named it, so the tree knows it");
10653 let mut runtime = Runtime::default();
10654 let (unplaced, moved) =
10655 apply_scroll_requests(&mut runtime, tree.as_ref(), requests, &[region(id)]);
10656 assert!(unplaced.is_empty());
10657 assert!(moved);
10658 assert_eq!(runtime.scroll.get(&id), Some(&(0.0, 1000.0)));
10659 }
10660
10661 #[test]
10666 fn a_region_this_registry_has_not_got_is_handed_back() {
10667 let tree = view();
10668 let requests = Command::<()>::scroll("log", ScrollTo::y(120.0))
10669 .into_parts()
10670 .scrolls;
10671 let mut runtime = Runtime::default();
10672 let (unplaced, moved) = apply_scroll_requests(&mut runtime, tree.as_ref(), requests, &[]);
10673 assert_eq!(unplaced.len(), 1, "kept for the second attempt");
10674 assert!(!moved);
10675 assert!(runtime.scroll_target.is_empty());
10676 }
10677
10678 #[test]
10681 fn a_name_the_view_does_not_use_moves_nothing() {
10682 let tree = view();
10683 let requests = Command::<()>::scroll("ledger", ScrollTo::start())
10684 .into_parts()
10685 .scrolls;
10686 let mut runtime = Runtime::default();
10687 let id = frus_widgets::find_by_key(
10688 tree.as_ref(),
10689 Command::<()>::scroll("log", ScrollTo::start())
10690 .into_parts()
10691 .scrolls[0]
10692 .0,
10693 )
10694 .expect("the real one is there");
10695 let (unplaced, moved) =
10696 apply_scroll_requests(&mut runtime, tree.as_ref(), requests, &[region(id)]);
10697 assert_eq!(unplaced.len(), 1);
10698 assert!(!moved);
10699 assert!(runtime.scroll.is_empty() && runtime.scroll_target.is_empty());
10700 }
10701
10702 #[test]
10705 fn a_batch_places_every_request_it_carries() {
10706 let tree = view();
10707 let requests = Command::<()>::batch([
10708 Command::scroll("log", ScrollTo::y(400.0).instant()),
10709 Command::scroll("log", ScrollTo::y(600.0).instant()),
10710 ])
10711 .into_parts()
10712 .scrolls;
10713 assert_eq!(requests.len(), 2);
10714 let id = frus_widgets::find_by_key(tree.as_ref(), requests[0].0).expect("named");
10715 let mut runtime = Runtime::default();
10716 let (unplaced, moved) =
10717 apply_scroll_requests(&mut runtime, tree.as_ref(), requests, &[region(id)]);
10718 assert!(unplaced.is_empty() && moved);
10719 assert_eq!(
10720 runtime.scroll.get(&id),
10721 Some(&(0.0, 600.0)),
10722 "the last request is the current statement"
10723 );
10724 }
10725}
10726
10727#[cfg(test)]
10728mod autoscroll_tests {
10729 use super::{
10730 edge_autoscroll, follow_content, Drag, Point, Rect, WidgetId, AUTOSCROLL_MAX_OVERHANG,
10731 AUTOSCROLL_VELOCITY,
10732 };
10733
10734 const VIEW: (f32, f32) = (100.0, 500.0);
10736 const ROOM: (f32, f32) = (1000.0, 1000.0);
10737 const DT: f32 = 1.0 / 60.0;
10739
10740 #[test]
10741 fn a_row_inside_the_viewport_scrolls_nothing() {
10742 assert_eq!(edge_autoscroll((200.0, 260.0), VIEW, ROOM, DT), None);
10743 assert_eq!(edge_autoscroll((440.0, 500.0), VIEW, ROOM, DT), None);
10745 }
10746
10747 #[test]
10750 fn the_speed_follows_how_far_out_the_row_is() {
10751 let near = edge_autoscroll((460.0, 501.0), VIEW, ROOM, DT).expect("one pixel out");
10752 let far = edge_autoscroll((460.0, 510.0), VIEW, ROOM, DT).expect("ten pixels out");
10753 assert!(
10754 near < 0.0,
10755 "the content moves up to reveal what is below: {near}"
10756 );
10757 assert!(
10758 (far / near - 10.0).abs() < 0.01,
10759 "ten times as far, ten times as fast: {near} vs {far}"
10760 );
10761 assert!(
10762 (near + 1.0 * AUTOSCROLL_VELOCITY * DT).abs() < 1e-4,
10763 "one pixel out, at the stated velocity: {near}"
10764 );
10765 }
10766
10767 #[test]
10770 fn the_overhang_is_capped() {
10771 let far = edge_autoscroll((460.0, 520.0), VIEW, ROOM, DT).expect("twenty out");
10772 let absurd = edge_autoscroll((460.0, 5000.0), VIEW, ROOM, DT).expect("off the window");
10773 assert!((far - absurd).abs() < 1e-4, "{far} vs {absurd}");
10774 assert!((absurd + AUTOSCROLL_MAX_OVERHANG * AUTOSCROLL_VELOCITY * DT).abs() < 1e-4);
10775 }
10776
10777 #[test]
10780 fn above_the_top_the_content_comes_down() {
10781 let up = edge_autoscroll((90.0, 150.0), VIEW, ROOM, DT).expect("ten above");
10782 assert!(up > 0.0, "{up}");
10783 }
10784
10785 #[test]
10792 fn a_carried_item_follows_its_content() {
10793 let shift = (0.0, -95.0);
10795 let pressed = Point::new(900.0, 1330.0);
10796 let row_then = Rect::new(105.0, 1250.0, 700.0, 160.0);
10797 let row_now = row_then.translate(0.0, -95.0);
10798 let finger = Point::new(900.0, 2030.0);
10799
10800 let mut reorder = Drag::Reorder {
10801 id: WidgetId::from_u64(1),
10802 from: 0,
10803 start: pressed,
10804 moved: true,
10805 carried: Some((WidgetId::from_u64(1), row_then)),
10806 };
10807 follow_content(&mut reorder, shift);
10808 let Drag::Reorder { start, carried, .. } = reorder else {
10809 unreachable!("still a reorder");
10810 };
10811 let (_, carried) = carried.expect("the box it was carried from");
10815 assert_eq!(carried, row_now, "the carried box went with the content");
10816 let ghost = carried.translate(finger.x - start.x, finger.y - start.y);
10818 assert_eq!(
10819 ghost,
10820 row_then.translate(finger.x - pressed.x, finger.y - pressed.y),
10821 "the ghost is where it would be had nothing scrolled"
10822 );
10823
10824 let mut item = Drag::Item {
10825 source: frus_widgets::DragSource {
10826 id: WidgetId::from_u64(1),
10827 rect: row_then,
10828 payload: 7,
10829 },
10830 start: pressed,
10831 moved: true,
10832 over: None,
10833 };
10834 follow_content(&mut item, shift);
10835 let Drag::Item { source, start, .. } = item else {
10836 unreachable!("still an item");
10837 };
10838 assert_eq!(
10839 source.rect, row_now,
10840 "the box taken at the press went with the content"
10841 );
10842 assert_eq!(start, Point::new(900.0, 1235.0), "and so did the press");
10843 }
10844
10845 #[test]
10848 fn an_exhausted_edge_stays_still() {
10849 assert_eq!(
10850 edge_autoscroll((460.0, 520.0), VIEW, (1000.0, 0.0), DT),
10851 None
10852 );
10853 assert_eq!(
10854 edge_autoscroll((90.0, 150.0), VIEW, (0.0, 1000.0), DT),
10855 None
10856 );
10857 }
10858
10859 #[test]
10862 fn a_row_taller_than_the_viewport_follows_its_leading_edge() {
10863 let both = edge_autoscroll((50.0, 900.0), VIEW, ROOM, DT).expect("over both edges");
10864 assert!(both > 0.0, "the top edge wins: {both}");
10865 }
10866}
10867
10868#[cfg(test)]
10881mod back_gesture_tests {
10882 use super::{
10883 drag_after_hold, frame_needs_build, hold_candidates, Drag, Point, Theme, WidgetId,
10884 };
10885 use frus_widgets::{build_ui, column, Container, DragSource, Draggable, Navigator};
10886 use frus_widgets::{Rect, Runtime, Size, Ui, Widget};
10887
10888 const W: f32 = 392.0;
10890 const H: f32 = 850.0;
10891 const ROW_TOP: f32 = 480.0;
10894 const ROW_HEIGHT: f32 = 66.0;
10895 const ON_THE_ROW: Point = Point::new(3.0, 509.0);
10896 const ABOVE_THE_ROW: Point = Point::new(3.0, 200.0);
10897 const HELD: u8 = 1;
10899
10900 fn home() -> impl Widget<u8> {
10903 column![
10904 Container::<u8>::new().width(W).height(ROW_TOP),
10905 Draggable::new(
10906 Container::<u8>::new()
10907 .width(W)
10908 .height(ROW_HEIGHT)
10909 .on_long_press(HELD)
10910 )
10911 .payload(7)
10912 .long_press(),
10913 ]
10914 }
10915
10916 fn after_a_push() -> Navigator<u8> {
10919 Navigator::new("data", Container::<u8>::new().width(W).height(H))
10920 .size(W, H)
10921 .from("home", home(), 0.999, true)
10922 }
10923
10924 fn frame(root: &dyn Widget<u8>) -> Ui<u8> {
10925 build_ui(
10926 root,
10927 Size::new(W, H),
10928 &Runtime::default(),
10929 &Theme::default(),
10930 )
10931 }
10932
10933 fn back(at: Point) -> Drag {
10934 Drag::Back { start_x: at.x }
10935 }
10936
10937 fn still_scroll() -> Drag {
10938 Drag::Scroll {
10939 id: WidgetId::from_u64(9),
10940 last: ON_THE_ROW,
10941 moved: false,
10942 carried: (0.0, 0.0),
10943 dismiss: None,
10944 axis: None,
10945 pan: None,
10946 }
10947 }
10948
10949 fn row_source() -> DragSource {
10950 DragSource {
10951 id: WidgetId::from_u64(3),
10952 rect: Rect::new(-117.0, ROW_TOP, W, ROW_HEIGHT),
10953 payload: 7,
10954 }
10955 }
10956
10957 #[test]
10965 fn the_frame_an_animation_settles_in_is_built_again() {
10966 assert!(
10967 frame_needs_build(false, false, true, false),
10968 "the frame the application's animation stops in builds the view again"
10969 );
10970 assert!(
10971 frame_needs_build(false, true, true, false),
10972 "as does every frame while it moves"
10973 );
10974 assert!(
10975 !frame_needs_build(false, false, false, false),
10976 "and a still frame after a still one repaints the tree it has"
10977 );
10978 assert!(frame_needs_build(true, false, false, false), "unless asked");
10979 assert!(
10980 frame_needs_build(false, false, false, true),
10981 "or the frame has its own reason"
10982 );
10983 }
10984
10985 #[test]
10988 fn a_back_gesture_claims_nothing_under_its_finger() {
10989 let root = after_a_push();
10990 let ui = frame(&root);
10991 let (held, lift) = hold_candidates(None, Some(&ui), Some(&root), ON_THE_ROW);
10992 assert!(
10993 lift.is_some() && held == Some(HELD),
10994 "the frame the phone had: a press there, with no gesture, finds the row's hold"
10995 );
10996
10997 let gesture = back(ON_THE_ROW);
10998 let (held, lift) = hold_candidates(Some(&gesture), Some(&ui), Some(&root), ON_THE_ROW);
10999 assert!(
11000 lift.is_none(),
11001 "a back gesture's press arms no lift of the row under it"
11002 );
11003 assert_eq!(held, None, "nor its long press");
11004 }
11005
11006 #[test]
11009 fn a_hold_never_takes_the_pointer_from_a_back_gesture() {
11010 let reorder = Some((WidgetId::from_u64(4), 2, ON_THE_ROW));
11011 for (lift, reorder) in [
11012 (Some(row_source()), None),
11013 (None, reorder),
11014 (Some(row_source()), reorder),
11015 (None, None),
11016 ] {
11017 let after = drag_after_hold(Some(back(ON_THE_ROW)), lift, reorder, ON_THE_ROW);
11018 assert!(
11019 matches!(after, Some(Drag::Back { start_x }) if start_x == ON_THE_ROW.x),
11020 "the back gesture is still the drag (lift {}, reorder {})",
11021 lift.is_some(),
11022 reorder.is_some()
11023 );
11024 }
11025 }
11026
11027 #[test]
11029 fn a_back_gesture_where_nothing_is_under_it_still_owns_the_pointer() {
11030 let root = after_a_push();
11031 let ui = frame(&root);
11032 let gesture = back(ABOVE_THE_ROW);
11033 let (held, lift) = hold_candidates(Some(&gesture), Some(&ui), Some(&root), ABOVE_THE_ROW);
11034 assert!(held.is_none() && lift.is_none());
11035 assert!(matches!(
11036 drag_after_hold(Some(gesture), None, None, ABOVE_THE_ROW),
11037 Some(Drag::Back { .. })
11038 ));
11039 }
11040
11041 #[test]
11045 fn a_hold_on_a_row_without_a_back_gesture_still_lifts_it() {
11046 let root = home();
11047 let ui = frame(&root);
11048 let at = Point::new(40.0, ROW_TOP + 10.0);
11049 let scroll = still_scroll();
11050 let (held, lift) = hold_candidates(Some(&scroll), Some(&ui), Some(&root), at);
11051 assert_eq!(
11052 held,
11053 Some(HELD),
11054 "the long press is armed under a still scroll"
11055 );
11056 assert!(lift.is_some(), "and so is the lift");
11057 let (held, lift) = hold_candidates(None, Some(&ui), Some(&root), at);
11058 assert!(held.is_some() && lift.is_some(), "and with no drag at all");
11059
11060 let cursor = Point::new(41.0, ROW_TOP + 11.0);
11061 let lifted = drag_after_hold(Some(still_scroll()), Some(row_source()), None, cursor);
11062 assert!(
11063 matches!(lifted, Some(Drag::Item { moved: true, start, .. }) if start == cursor),
11064 "the hold lifts the item where the finger is"
11065 );
11066 let reorder = Some((WidgetId::from_u64(4), 2, at));
11067 let reordering = drag_after_hold(Some(still_scroll()), None, reorder, cursor);
11068 assert!(
11069 matches!(reordering, Some(Drag::Reorder { moved: true, from: 2, start, .. }) if start == at),
11070 "or picks the row up for a reorder, from where it was pressed"
11071 );
11072 assert!(
11073 drag_after_hold(Some(still_scroll()), None, None, cursor).is_none(),
11074 "a hold with nothing to lift ends the still scroll: the long press had it"
11075 );
11076 }
11077}
11078
11079fn count_layers(primitives: &[frus_widgets::Primitive]) -> u32 {
11081 primitives
11082 .iter()
11083 .map(|p| match p {
11084 frus_widgets::Primitive::Layer { primitives, .. } => 1 + count_layers(primitives),
11085 _ => 0,
11086 })
11087 .sum()
11088}
11089
11090pub(crate) struct KeyDown {
11094 logical_key: WinitKey,
11096 physical_key: PhysicalKey,
11098 text: Option<winit::keyboard::SmolStr>,
11100 repeat: bool,
11102}
11103
11104impl From<&winit::event::KeyEvent> for KeyDown {
11105 fn from(event: &winit::event::KeyEvent) -> Self {
11106 Self {
11107 logical_key: event.logical_key.clone(),
11108 physical_key: event.physical_key,
11109 text: event.text.clone(),
11110 repeat: event.repeat,
11111 }
11112 }
11113}
11114
11115fn backdrop_under(mut scene: Scene, width: f32, height: f32, line: f32) -> Scene {
11120 let mut framed = Scene::new();
11121 framed.fill_rect(
11122 frus_widgets::Rect::new(0.0, line, width, (height - line).max(0.0)),
11123 frus_gpu::backdrop(),
11124 );
11125 for primitive in scene.split_off(0) {
11126 framed.push_primitive(primitive);
11127 }
11128 framed
11129}
11130
11131#[cfg(test)]
11132mod backdrop_tests {
11133 use super::*;
11134
11135 #[test]
11138 fn the_background_leaves_the_system_s_line_clear() {
11139 let mut scene = Scene::new();
11140 scene.fill_rect(
11141 frus_widgets::Rect::new(10.0, 5.0, 20.0, 20.0),
11142 frus_widgets::Color::WHITE,
11143 );
11144 let framed = backdrop_under(scene.clone(), 800.0, 600.0, 48.0);
11145 let prims = framed.primitives();
11146 assert_eq!(prims.len(), 2);
11147 match &prims[0] {
11148 frus_widgets::Primitive::Rect { rect, color, .. } => {
11149 assert_eq!(*rect, frus_widgets::Rect::new(0.0, 48.0, 800.0, 552.0));
11150 assert_eq!(*color, frus_gpu::backdrop());
11151 assert_eq!(
11152 color.a, 1.0,
11153 "opaque: nothing behind the window shows there"
11154 );
11155 }
11156 other => panic!("the background first: {other:?}"),
11157 }
11158 assert_eq!(prims[1], scene.primitives()[0], "then the scene");
11159
11160 let whole = backdrop_under(Scene::new(), 800.0, 600.0, 0.0);
11161 assert!(
11162 matches!(&whole.primitives()[0],
11163 frus_widgets::Primitive::Rect { rect, .. } if *rect == frus_widgets::Rect::new(0.0, 0.0, 800.0, 600.0)),
11164 "no line: the whole window"
11165 );
11166 }
11167}
11168
11169#[cfg(any(test, feature = "testing"))]
11172#[doc(hidden)]
11173pub mod testing {
11174 use super::*;
11175 use crate::gesture::{PointerEvent, PointerKind, LONG_PRESS_DELAY};
11176
11177 pub type FrameParts<'a, Msg> = (&'a Ui<Msg>, &'a dyn Widget<Msg>);
11179
11180 pub struct Driver<A: Application> {
11182 shell: App<A>,
11183 size: Size,
11184 ghost: Option<Rect>,
11186 }
11187
11188 impl<A: Application> Driver<A> {
11189 pub fn new(app: A, width: f32, height: f32) -> Self {
11191 let mut shell = App::detached(app);
11192 shell.gesture_clock = Some((0.0, 0.0));
11194 Self {
11195 shell,
11196 size: Size::new(width, height),
11197 ghost: None,
11198 }
11199 }
11200
11201 pub fn system_back(&mut self) -> bool {
11204 self.shell.system_back_step()
11205 }
11206
11207 pub fn fling_strategy(&self) -> frus_widgets::VelocityStrategy {
11210 self.shell.gesture_velocity.strategy()
11211 }
11212
11213 pub fn carried(&self) -> Option<Rect> {
11216 self.shell.carried_rect()
11217 }
11218
11219 pub fn ghost(&self) -> Option<Rect> {
11222 self.ghost
11223 }
11224
11225 pub fn offsets(&self) -> Vec<(Rect, (f32, f32))> {
11227 let s = &self.shell;
11228 s.ui.as_ref()
11229 .map(|ui| {
11230 ui.scroll_regions()
11231 .iter()
11232 .map(|a| {
11233 (
11234 a.viewport,
11235 s.runtime.scroll.get(&a.id).copied().unwrap_or((0.0, 0.0)),
11236 )
11237 })
11238 .collect()
11239 })
11240 .unwrap_or_default()
11241 }
11242
11243 #[cfg(all(test, desktop, feature = "tray"))]
11247 pub(crate) fn has_system_tray(&self) -> bool {
11248 self.shell.tray.is_some()
11249 }
11250
11251 pub fn assistive_click(&mut self, label: &str) -> bool {
11254 let Some(id) = self.node_labelled(label) else {
11255 return false;
11256 };
11257 self.shell.activate_for_assistive_tech(id);
11258 true
11259 }
11260
11261 pub fn assistive_set_value(&mut self, label: &str, value: &str) -> bool {
11264 let Some(id) = self.node_labelled(label) else {
11265 return false;
11266 };
11267 self.shell
11268 .set_value_for_assistive_tech(id, value.to_owned());
11269 true
11270 }
11271
11272 fn node_labelled(&self, label: &str) -> Option<WidgetId> {
11274 self.shell
11275 .ui
11276 .as_ref()?
11277 .semantics()
11278 .iter()
11279 .find(|(_, _, sem)| sem.label.as_deref() == Some(label))
11280 .map(|(id, _, _)| *id)
11281 }
11282
11283 pub fn frame_parts(&self) -> Option<FrameParts<'_, A::Message>> {
11284 let s = &self.shell;
11285 Some((s.ui.as_ref()?, s.tree.as_deref()?))
11286 }
11287
11288 pub fn texts(&self) -> Vec<(String, Rect)> {
11290 self.shell
11291 .ui
11292 .as_ref()
11293 .map(|ui| {
11294 ui.scene()
11295 .primitives()
11296 .iter()
11297 .filter_map(|p| match p {
11298 frus_widgets::Primitive::Text { text, .. } => {
11299 Some((text.clone(), p.bounds()))
11300 }
11301 _ => None,
11302 })
11303 .collect()
11304 })
11305 .unwrap_or_default()
11306 }
11307
11308 pub fn tap_text(&mut self, label: &str) -> bool {
11311 let Some((_, rect)) = self.texts().into_iter().find(|(text, _)| text == label) else {
11312 return false;
11313 };
11314 let at = Point::new(rect.x + rect.width / 2.0, rect.y + rect.height / 2.0);
11315 self.press(at);
11316 self.release(at);
11317 self.frame(1.0 / 60.0);
11318 true
11319 }
11320
11321 pub fn app(&self) -> &A {
11323 &self.shell.app
11324 }
11325
11326 pub fn focused(&self) -> Option<WidgetId> {
11328 self.shell.runtime.input.focused
11329 }
11330
11331 #[cfg(test)]
11334 pub(crate) fn simulate(&mut self, device: &str) {
11335 self.shell.simulated = Some(crate::simulated::Simulated::named(device).unwrap());
11336 }
11337
11338 #[cfg(test)]
11341 pub(crate) fn simulated_keyboard(&mut self) -> bool {
11342 self.shell.sync_soft_input();
11343 self.shell.simulated.as_ref().is_some_and(|s| s.keyboard)
11344 }
11345
11346 pub fn shell_modifiers(&self) -> (bool, bool, bool, bool) {
11348 (
11349 self.shell.ctrl,
11350 self.shell.shift,
11351 self.shell.alt,
11352 self.shell.meta,
11353 )
11354 }
11355
11356 pub fn hit(&self, at: Point) -> Option<WidgetId> {
11358 self.shell.ui.as_ref().and_then(|ui| ui.hit(at))
11359 }
11360
11361 pub fn update(&mut self, message: A::Message) {
11363 self.shell.dispatch(message);
11364 }
11365
11366 pub fn run(&mut self, seconds: f32) {
11368 let n = (seconds * 60.0).round() as usize;
11369 for _ in 0..n.max(1) {
11370 self.frame(1.0 / 60.0);
11371 }
11372 }
11373
11374 pub fn hover_files(&mut self, paths: &[std::path::PathBuf], at: Option<Point>) {
11377 for path in paths {
11378 self.shell.file_event(path.clone(), false, at);
11379 }
11380 }
11381
11382 pub fn move_files(&mut self, at: Option<Point>) {
11384 self.shell.files_moved(at);
11385 }
11386
11387 pub fn drop_files(&mut self, paths: &[std::path::PathBuf], at: Option<Point>) {
11390 for path in paths {
11391 self.shell.file_event(path.clone(), true, at);
11392 }
11393 self.shell.deliver_dropped_files();
11394 }
11395
11396 pub fn cancel_files(&mut self) {
11398 self.shell.files_left();
11399 }
11400
11401 pub fn drag_over(&self) -> Option<WidgetId> {
11403 self.shell.runtime.drag_over
11404 }
11405
11406 pub fn press(&mut self, at: Point) {
11408 self.pointer(PointerKind::Down, at);
11409 }
11410
11411 pub fn move_to(&mut self, at: Point) {
11413 self.pointer(PointerKind::Move, at);
11414 }
11415
11416 pub fn release(&mut self, at: Point) {
11418 self.pointer(PointerKind::Up, at);
11419 }
11420
11421 pub fn hold_deadline(&mut self) -> bool {
11424 let fired = self.shell.press.poll(Instant::now() + LONG_PRESS_DELAY);
11425 if fired {
11426 self.shell.hold_deadline_reached();
11427 }
11428 fired
11429 }
11430
11431 pub fn tap_deadline(&mut self) -> bool {
11434 self.shell
11435 .poll_pending_tap(Instant::now() + DOUBLE_TAP_TIMEOUT)
11436 }
11437
11438 pub fn cursor_shown(&self) -> frus_widgets::Cursor {
11440 let s = &self.shell;
11441 let requested = s.runtime.input.hovered.and_then(|id| {
11442 let rect = s.ui.as_ref()?.widget_rect(id)?;
11443 let widget = find_widget(s.tree.as_ref()?.as_ref(), id)?;
11444 widget.cursor_icon(
11445 s.cursor.x - rect.x,
11446 s.cursor.y - rect.y,
11447 rect.width,
11448 rect.height,
11449 )
11450 });
11451 s.region_cursor(requested)
11452 }
11453
11454 pub fn next_wake(&self) -> Option<Instant> {
11456 match self.shell.idle_control_flow() {
11457 ControlFlow::WaitUntil(at) => Some(at),
11458 _ => None,
11459 }
11460 }
11461
11462 pub fn secondary_click(&mut self, at: Point) {
11464 self.pointer_of(PointerKind::Move, at, false);
11465 self.shell.context_click();
11466 }
11467
11468 pub fn press_mouse(&mut self, at: Point) {
11471 self.pointer_of(PointerKind::Down, at, false);
11472 }
11473
11474 pub fn move_mouse(&mut self, at: Point) {
11476 self.pointer_of(PointerKind::Move, at, false);
11477 }
11478
11479 pub fn release_mouse(&mut self, at: Point) {
11481 self.pointer_of(PointerKind::Up, at, false);
11482 }
11483
11484 pub fn area_selection(&self) -> Option<String> {
11486 self.shell.region_text()
11487 }
11488
11489 pub fn area_select_all(&mut self) {
11491 self.shell.select_all_region();
11492 }
11493
11494 pub fn escape(&mut self) {
11496 self.shell.escape();
11497 }
11498
11499 pub fn key(&mut self, stroke: frus_widgets::KeyStroke) {
11507 use frus_widgets::ShortcutKey as K;
11508 let named = |key| (WinitKey::Named(key), None);
11509 let (logical_key, text) = match stroke.key {
11510 K::Char(c) => {
11511 let typed: winit::keyboard::SmolStr = c.to_string().into();
11512 (WinitKey::Character(typed.clone()), Some(typed))
11513 }
11514 K::Enter => named(NamedKey::Enter),
11515 K::Escape => named(NamedKey::Escape),
11516 K::Tab => named(NamedKey::Tab),
11517 K::Space => (WinitKey::Named(NamedKey::Space), Some(" ".into())),
11518 K::Backspace => named(NamedKey::Backspace),
11519 K::Delete => named(NamedKey::Delete),
11520 K::Up => named(NamedKey::ArrowUp),
11521 K::Down => named(NamedKey::ArrowDown),
11522 K::Left => named(NamedKey::ArrowLeft),
11523 K::Right => named(NamedKey::ArrowRight),
11524 K::Home => named(NamedKey::Home),
11525 K::End => named(NamedKey::End),
11526 K::PageUp => named(NamedKey::PageUp),
11527 K::PageDown => named(NamedKey::PageDown),
11528 K::F(n) => named(match n {
11529 1 => NamedKey::F1,
11530 2 => NamedKey::F2,
11531 3 => NamedKey::F3,
11532 4 => NamedKey::F4,
11533 5 => NamedKey::F5,
11534 6 => NamedKey::F6,
11535 7 => NamedKey::F7,
11536 8 => NamedKey::F8,
11537 9 => NamedKey::F9,
11538 10 => NamedKey::F10,
11539 11 => NamedKey::F11,
11540 _ => NamedKey::F12,
11541 }),
11542 };
11543 const LETTERS: [KeyCode; 26] = [
11546 KeyCode::KeyA,
11547 KeyCode::KeyB,
11548 KeyCode::KeyC,
11549 KeyCode::KeyD,
11550 KeyCode::KeyE,
11551 KeyCode::KeyF,
11552 KeyCode::KeyG,
11553 KeyCode::KeyH,
11554 KeyCode::KeyI,
11555 KeyCode::KeyJ,
11556 KeyCode::KeyK,
11557 KeyCode::KeyL,
11558 KeyCode::KeyM,
11559 KeyCode::KeyN,
11560 KeyCode::KeyO,
11561 KeyCode::KeyP,
11562 KeyCode::KeyQ,
11563 KeyCode::KeyR,
11564 KeyCode::KeyS,
11565 KeyCode::KeyT,
11566 KeyCode::KeyU,
11567 KeyCode::KeyV,
11568 KeyCode::KeyW,
11569 KeyCode::KeyX,
11570 KeyCode::KeyY,
11571 KeyCode::KeyZ,
11572 ];
11573 let physical_key = match stroke.key {
11574 K::Char(c) if c.is_ascii_alphabetic() => {
11575 PhysicalKey::Code(LETTERS[(c.to_ascii_lowercase() as u8 - b'a') as usize])
11576 }
11577 _ => PhysicalKey::Unidentified(winit::keyboard::NativeKeyCode::Unidentified),
11578 };
11579 let held = (
11580 self.shell.ctrl,
11581 self.shell.shift,
11582 self.shell.alt,
11583 self.shell.meta,
11584 );
11585 (
11586 self.shell.ctrl,
11587 self.shell.shift,
11588 self.shell.alt,
11589 self.shell.meta,
11590 ) = (stroke.ctrl, stroke.shift, stroke.alt, stroke.meta);
11591 self.shell.key_down(
11592 &KeyDown {
11593 logical_key,
11594 physical_key,
11595 text,
11596 repeat: false,
11597 },
11598 None,
11599 );
11600 (
11601 self.shell.ctrl,
11602 self.shell.shift,
11603 self.shell.alt,
11604 self.shell.meta,
11605 ) = held;
11606 }
11607
11608 pub fn area_handles(&self) -> Option<[frus_widgets::SelectionHandle; 2]> {
11610 self.shell.region_handles().map(|(_, handles)| handles)
11611 }
11612
11613 fn pointer(&mut self, kind: PointerKind, at: Point) {
11614 self.pointer_of(kind, at, true);
11615 }
11616
11617 fn pointer_of(&mut self, kind: PointerKind, at: Point, touch: bool) {
11618 self.shell.pointer_event(PointerEvent {
11619 kind,
11620 position: at,
11621 touch,
11622 });
11623 }
11624
11625 pub fn frame(&mut self, dt: f32) {
11627 let (width, height) = (self.size.width, self.size.height);
11628 let s = &mut self.shell;
11629 if let Some((now, _)) = s.gesture_clock.as_mut() {
11630 *now += dt;
11631 }
11632 if s.last_size != Some((width, height)) {
11633 s.last_size = Some((width, height));
11634 s.build_dirty = true;
11635 s.app.on_resize(width, height);
11636 }
11637 let mut app_animating = s.app.tick(dt);
11638 let settings = s
11639 .platform
11640 .accessibility
11641 .with_overrides(s.app.accessibility());
11642 let theme_moved = s.themes.advance(
11643 &s.app,
11644 s.platform.brightness,
11645 settings.high_contrast,
11646 settings.disable_animations,
11647 dt,
11648 );
11649 app_animating |= theme_moved | s.themes.animating();
11650 let theme = s.themes.displayed(&s.app);
11651 install_ambient(&s.app, &s.platform.locales);
11652 let _surface = s.media_query(width, height).install();
11653 s.runtime.still = settings.disable_animations;
11654 let was = std::mem::replace(&mut s.app_was_animating, app_animating);
11655 let asked = s.app.effects();
11656 s.run_command(asked);
11657 let arrived = s.take_task_arrivals();
11658 if frus_widgets::take_rebuild_request() || arrived {
11659 s.build_dirty = true;
11660 }
11661 s.report_focus();
11662 if frame_needs_build(
11663 s.build_dirty,
11664 app_animating,
11665 was,
11666 s.tree.is_none() || s.runtime.switching(),
11667 ) {
11668 s.tree = Some(build_view(&s.app, &theme, &s.runtime));
11669 }
11670 s.build_dirty = false;
11671 s.autoscroll_carried(dt);
11672 s.advance_reorder_springs(dt);
11673 let regions =
11674 s.ui.as_ref()
11675 .map(|ui| ui.scroll_regions().to_vec())
11676 .unwrap_or_default();
11677 s.runtime.sync_pages(®ions);
11678 s.runtime.sync_visible(®ions);
11679 s.runtime.advance(dt);
11680 s.runtime.advance_scroll(®ions, dt);
11681 let tree = s.tree.as_deref().expect("the view was built");
11682 s.runtime.advance_values(tree, dt);
11685 let ui = build_ui(tree, Size::new(width, height), &s.runtime, &theme);
11686 s.runtime.states.end_frame();
11687 let mut scene = ui.scene().clone();
11688 let ghost = if matches!(s.drag, Some(Drag::Reorder { moved: true, .. })) {
11689 s.paint_reorder_preview(&ui, &theme, &mut scene);
11690 scene.primitives().last().map(|p| p.bounds())
11692 } else {
11693 None
11694 };
11695 let paged = ui.scroll_regions().to_vec();
11696 let scrolled: Vec<A::Message> = {
11697 let grain = |id| find_widget(tree, id).map_or(0.0, |w| w.scroll_grain());
11698 s.runtime
11699 .scroll_changes(&paged, grain)
11700 .into_iter()
11701 .filter_map(|(id, position)| {
11702 find_widget(tree, id).and_then(|w| w.on_scroll(position))
11703 })
11704 .collect()
11705 };
11706 s.ui = Some(ui);
11707 s.sync_regions(false);
11708 s.reconcile_focus();
11711 for message in scrolled {
11712 s.dispatch(message);
11713 }
11714 self.ghost = ghost;
11715 }
11716 }
11717}
11718
11719#[cfg(test)]
11733mod carried_row_tests {
11734 use super::testing::Driver;
11735 use crate::{Application, Command};
11736 use frus_widgets::{
11737 column, text, Axis, Button, CellFn, Container, Flex, Kanban, Point, Rect, ReorderAxis,
11738 ReorderGrab, ReorderableList, SingleChildScrollView, Theme, Widget,
11739 };
11740
11741 const W: f32 = 392.7;
11743 const H: f32 = 850.9;
11744 const STRIP_Y: f32 = 88.0;
11746 const STRIP_CONTENT: f32 = 24.0 + 10.0 * 96.0 + 9.0 * 8.0 + 24.0;
11748
11749 #[derive(Clone, Debug, PartialEq)]
11750 enum Board {
11751 Label(usize, usize),
11752 Card(usize, usize, usize, usize),
11753 Delete(usize, usize),
11754 Add(usize),
11755 Row(usize, usize),
11756 }
11757
11758 struct BoardApp {
11760 labels: Vec<usize>,
11761 cards: Vec<Vec<String>>,
11762 log: Vec<Board>,
11763 }
11764
11765 impl Default for BoardApp {
11766 fn default() -> Self {
11767 let cols: [&[&str]; 3] = [
11768 &["Design API", "Write spec", "Triage bugs"],
11769 &["Build widget"],
11770 &["Kickoff", "Research"],
11771 ];
11772 Self {
11773 labels: (0..10).collect(),
11774 cards: cols
11775 .iter()
11776 .map(|c| c.iter().map(|s| s.to_string()).collect())
11777 .collect(),
11778 log: Vec::new(),
11779 }
11780 }
11781 }
11782
11783 impl Application for BoardApp {
11784 type Message = Board;
11785
11786 fn update(&mut self, message: Board) -> Command<Board> {
11787 self.log.push(message.clone());
11788 match message {
11789 Board::Label(from, to) => {
11790 let label = self.labels.remove(from);
11791 self.labels.insert(to, label);
11792 }
11793 Board::Card(from_col, from_pos, to_col, to_pos) => {
11794 let card = self.cards[from_col].remove(from_pos);
11795 let to_pos = to_pos.min(self.cards[to_col].len());
11796 self.cards[to_col].insert(to_pos, card);
11797 }
11798 _ => {}
11799 }
11800 Command::none()
11801 }
11802
11803 fn view(&self, theme: &Theme) -> Box<dyn Widget<Board>> {
11804 let mut strip = ReorderableList::new(Board::Label)
11805 .axis(ReorderAxis::Horizontal)
11806 .grab(ReorderGrab::LongPress)
11807 .gap(8.0);
11808 for &label in &self.labels {
11809 strip = strip.keyed_row(
11810 label as u64,
11811 Container::new()
11812 .width(96.0)
11813 .padding(12.0)
11814 .color(theme.surface)
11815 .child(text(format!("label {label}")).size(14.0)),
11816 );
11817 }
11818 let strip = SingleChildScrollView::new()
11819 .axis(Axis::Horizontal)
11820 .width(W)
11821 .child(
11822 Container::new()
11823 .padding_each(12.0, 24.0, 0.0, 24.0)
11824 .child(strip),
11825 );
11826 let mut board = Kanban::new(Board::Card)
11827 .on_add(Board::Add)
11828 .scrollable_columns();
11829 for (col, title) in ["To do", "Doing", "Done"].iter().enumerate() {
11830 let cards: Vec<CellFn<Board>> = self.cards[col]
11831 .iter()
11832 .enumerate()
11833 .map(|(pos, label)| {
11834 let label = label.clone();
11835 Box::new(move || {
11836 Box::new(
11837 Flex::row()
11838 .child(text(label.clone()).size(14.0))
11839 .child(Flex::row().flex(1.0))
11840 .child(Button::new("x").on_press(Board::Delete(col, pos))),
11841 ) as Box<dyn Widget<Board>>
11842 }) as CellFn<Board>
11843 })
11844 .collect();
11845 board = board.column_widgets(*title, cards);
11846 }
11847 let board = SingleChildScrollView::new()
11848 .axis(Axis::Horizontal)
11849 .width(W)
11850 .flex(1.0)
11851 .child(Container::new().padding(24.0).child(board));
11852 Box::new(
11853 Container::new().width(W).height(H).child(
11854 column![
11855 Container::<Board>::new().width(W).height(56.0),
11856 strip,
11857 board
11858 ]
11859 .flex(1.0),
11860 ),
11861 )
11862 }
11863 }
11864
11865 #[derive(Default)]
11867 struct ListApp {
11868 log: Vec<Board>,
11869 }
11870
11871 impl Application for ListApp {
11872 type Message = Board;
11873
11874 fn update(&mut self, message: Board) -> Command<Board> {
11875 self.log.push(message);
11876 Command::none()
11877 }
11878
11879 fn view(&self, _theme: &Theme) -> Box<dyn Widget<Board>> {
11880 let mut list = ReorderableList::new(Board::Row)
11881 .grab(ReorderGrab::LongPress)
11882 .width(W);
11883 for row in 0..20u64 {
11884 list = list.keyed_row(
11885 row,
11886 Container::new()
11887 .height(60.0)
11888 .child(text(format!("row {row}")).size(14.0)),
11889 );
11890 }
11891 Box::new(
11892 SingleChildScrollView::new()
11893 .axis(Axis::Vertical)
11894 .width(W)
11895 .height(H)
11896 .child(list),
11897 )
11898 }
11899 }
11900
11901 fn driver<A: Application>(app: A) -> Driver<A> {
11902 let mut driver = Driver::new(app, W, H);
11903 driver.run(0.2);
11904 driver
11905 }
11906
11907 fn hold<A: Application>(driver: &mut Driver<A>, at: Point) {
11909 driver.press(at);
11910 driver.run(0.5);
11911 assert!(
11912 driver.hold_deadline(),
11913 "the hold lifts what is under {at:?}"
11914 );
11915 driver.run(0.05);
11916 }
11917
11918 fn carry<A: Application>(driver: &mut Driver<A>, points: impl IntoIterator<Item = Point>) {
11920 for at in points {
11921 driver.move_to(at);
11922 driver.run(0.1);
11923 }
11924 }
11925
11926 fn held_under_the_finger<A: Application>(
11930 driver: &mut Driver<A>,
11931 seconds: f32,
11932 finger: Point,
11933 offset: (f32, f32),
11934 size: (f32, f32),
11935 region: usize,
11936 ) -> (f32, f32) {
11937 let mut last = driver.offsets()[region].1;
11938 for frame in 0..(seconds * 60.0) as usize {
11939 driver.run(1.0 / 60.0);
11940 let now = driver.offsets()[region].1;
11941 let carried: Rect = driver.carried().unwrap_or_else(|| {
11942 panic!("frame {frame}: nothing is carried any more, the list at {now:?}")
11943 });
11944 assert!(
11945 (carried.x - finger.x - offset.0).abs() < 0.5
11946 && (carried.y - finger.y - offset.1).abs() < 0.5
11947 && (carried.width - size.0).abs() < 0.5
11948 && (carried.height - size.1).abs() < 0.5,
11949 "frame {frame}: carried at {carried:?}, the finger at {finger:?}, the list at {now:?}"
11950 );
11951 assert!(
11952 now.0 >= last.0 - 0.01 && now.1 >= last.1 - 0.01,
11953 "frame {frame}: the list went back, {last:?} then {now:?}"
11954 );
11955 let ghost = driver
11957 .ghost()
11958 .unwrap_or_else(|| panic!("frame {frame}: no ghost is drawn"));
11959 assert!(
11960 (ghost.x - carried.x).abs() < 1.0
11961 && (ghost.y - carried.y).abs() < 1.0
11962 && (ghost.width - carried.width).abs() < 2.0,
11963 "frame {frame}: the ghost is drawn at {ghost:?}, what is carried is at {carried:?}"
11964 );
11965 last = now;
11966 }
11967 last
11968 }
11969
11970 #[test]
11974 fn a_label_carried_past_the_strips_edge_stays_under_the_finger() {
11975 let mut driver = driver(BoardApp::default());
11976 hold(&mut driver, Point::new(155.0, STRIP_Y));
11977 let lifted = driver.carried().expect("the label is carried");
11978 assert_eq!(
11979 lifted,
11980 Rect::new(128.0, 68.0, 96.0, 41.0),
11981 "Feature, lifted"
11982 );
11983 let finger = Point::new(370.0, STRIP_Y);
11984 carry(
11985 &mut driver,
11986 [200.0, 260.0, 320.0, finger.x].map(|x| Point::new(x, STRIP_Y)),
11987 );
11988 let strip = held_under_the_finger(
11989 &mut driver,
11990 1.5,
11991 finger,
11992 (lifted.x - 155.0, lifted.y - STRIP_Y),
11993 (96.0, 41.0),
11994 0,
11995 );
11996 assert!(
11997 (strip.0 - (STRIP_CONTENT - W)).abs() < 1.0,
11998 "the strip scrolled on to its end: {strip:?}"
11999 );
12000 driver.release(finger);
12001 driver.run(0.2);
12002 assert_eq!(
12003 driver.app().log,
12004 [Board::Label(1, 9)],
12005 "past the last label, it goes last; no card moves"
12006 );
12007 }
12008
12009 #[test]
12012 fn a_hold_on_a_label_then_a_carry_along_x_reorders_the_labels_and_moves_no_card() {
12013 let mut driver = driver(BoardApp::default());
12014 hold(&mut driver, Point::new(155.0, STRIP_Y));
12015 carry(
12016 &mut driver,
12017 [167.0, 189.0, 218.0, 247.0, 276.0, 306.0].map(|x| Point::new(x, STRIP_Y)),
12018 );
12019 driver.run(0.4);
12020 assert!(driver.carried().is_some(), "still carried");
12021 driver.release(Point::new(306.0, STRIP_Y));
12022 driver.run(0.2);
12023 assert_eq!(driver.app().log, [Board::Label(1, 2)]);
12024 assert_eq!(driver.app().labels[..4], [0, 2, 1, 3]);
12025 assert_eq!(
12026 driver.app().cards,
12027 BoardApp::default().cards,
12028 "no card moved"
12029 );
12030 assert_eq!(driver.offsets()[0].1, (0.0, 0.0), "nothing scrolled");
12031 assert_eq!(driver.offsets()[1].1, (0.0, 0.0), "the board neither");
12032 }
12033
12034 #[test]
12037 fn a_card_carried_across_the_board_moves_the_card() {
12038 let mut driver = driver(BoardApp::default());
12039 driver.press(Point::new(100.0, 190.0));
12040 carry(
12041 &mut driver,
12042 [130.0, 180.0, 240.0, 300.0].map(|x| Point::new(x, 185.0)),
12043 );
12044 driver.release(Point::new(300.0, 185.0));
12045 driver.run(0.2);
12046 assert_eq!(driver.app().log, [Board::Card(0, 0, 1, 0)]);
12047 assert_eq!(driver.app().labels, (0..10).collect::<Vec<_>>());
12048 }
12049
12050 #[test]
12053 fn a_card_carried_to_the_boards_edge_stays_under_the_finger() {
12054 let mut driver = driver(BoardApp::default());
12055 let press = Point::new(100.0, 190.0);
12056 driver.press(press);
12057 let finger = Point::new(385.0, 190.0);
12058 carry(
12059 &mut driver,
12060 [130.0, 200.0, 280.0, 340.0, finger.x].map(|x| Point::new(x, 190.0)),
12061 );
12062 let card = driver.carried().expect("the card is carried");
12063 let board = held_under_the_finger(
12064 &mut driver,
12065 1.0,
12066 finger,
12067 (card.x - finger.x, card.y - finger.y),
12068 (card.width, card.height),
12069 1,
12070 );
12071 assert!(
12072 board.0 > 250.0,
12073 "the board scrolled past the column the card came from: {board:?}"
12074 );
12075 }
12076
12077 #[test]
12081 fn a_row_carried_past_the_bottom_of_a_long_list_stays_under_the_finger() {
12082 let mut driver = driver(ListApp::default());
12083 hold(&mut driver, Point::new(100.0, 30.0));
12084 let lifted = driver.carried().expect("the row is carried");
12085 let finger = Point::new(100.0, 840.0);
12086 carry(
12087 &mut driver,
12088 [200.0, 400.0, 600.0, 800.0, finger.y].map(|y| Point::new(100.0, y)),
12089 );
12090 let list = held_under_the_finger(
12091 &mut driver,
12092 1.5,
12093 finger,
12094 (lifted.x - 100.0, lifted.y - 30.0),
12095 (lifted.width, lifted.height),
12096 0,
12097 );
12098 assert!(
12099 list.1 > 200.0,
12100 "the list scrolled past the row's own place: {list:?}"
12101 );
12102 driver.release(finger);
12103 driver.run(0.2);
12104 assert!(
12105 matches!(driver.app().log[..], [Board::Row(0, to)] if to > 15),
12106 "row 0 lands near the end: {:?}",
12107 driver.app().log
12108 );
12109 }
12110}
12111
12112#[cfg(test)]
12120mod press_tests {
12121 use super::{gesture_was_a_tap, press_at, settle_press, Drag, Point, Runtime, Theme};
12122 use crate::gesture::PointerKind;
12123 use crate::hover::Hover;
12124 use frus_widgets::{
12125 build_ui, Axis, Button, Container, Flex, Primitive, SegmentedButton, SingleChildScrollView,
12126 Size, Ui, Widget,
12127 };
12128
12129 const SCREEN: Size = Size::new(300.0, 400.0);
12130 const DT: f32 = 1.0 / 60.0;
12131
12132 fn filters() -> Box<dyn Widget<usize>> {
12134 let mut segments = SegmentedButton::new(0, |i| i);
12135 for label in ["All", "Active", "Done", "Archived", "Starred", "Shared"] {
12136 segments = segments.segment(label);
12137 }
12138 Box::new(
12139 Flex::column().width(300.0).height(400.0).child(
12140 SingleChildScrollView::new()
12141 .axis(Axis::Horizontal)
12142 .width(300.0)
12143 .height(80.0)
12144 .child(segments),
12145 ),
12146 )
12147 }
12148
12149 fn page() -> Box<dyn Widget<usize>> {
12151 Box::new(
12152 Flex::column().width(300.0).height(400.0).child(
12153 SingleChildScrollView::new()
12154 .width(300.0)
12155 .height(400.0)
12156 .child(
12157 Flex::column()
12158 .child(Button::new("Go").on_press(7usize))
12159 .child(Container::new().width(300.0).height(1200.0)),
12160 ),
12161 ),
12162 )
12163 }
12164
12165 fn frame(view: &dyn Widget<usize>, runtime: &Runtime) -> Ui<usize> {
12166 build_ui(view, SCREEN, runtime, &Theme::default())
12167 }
12168
12169 fn idle(runtime: &mut Runtime, seconds: f32) {
12171 for _ in 0..(seconds / DT) as usize {
12172 runtime.advance(DT);
12173 runtime.advance_ink(DT);
12174 }
12175 }
12176
12177 fn painted(view: &dyn Widget<usize>, runtime: &Runtime) -> Vec<Primitive> {
12179 frame(view, runtime).scene().primitives().to_vec()
12180 }
12181
12182 fn untouched(view: &dyn Widget<usize>, runtime: &Runtime) -> Vec<Primitive> {
12184 let mut quiet = Runtime::default();
12185 quiet.scroll.clone_from(&runtime.scroll);
12186 quiet.scroll_target.clone_from(&runtime.scroll_target);
12187 idle(&mut quiet, 0.5);
12188 painted(view, &quiet)
12189 }
12190
12191 struct Pointer<'a> {
12193 view: &'a dyn Widget<usize>,
12194 runtime: Runtime,
12195 hover: Hover,
12196 drag: Option<Drag>,
12197 touch: bool,
12198 at: Point,
12199 }
12200
12201 impl<'a> Pointer<'a> {
12202 fn new(view: &'a dyn Widget<usize>, touch: bool) -> Self {
12203 Self {
12204 view,
12205 runtime: Runtime::default(),
12206 hover: Hover::default(),
12207 drag: None,
12208 touch,
12209 at: Point::new(0.0, 0.0),
12210 }
12211 }
12212
12213 fn event(&mut self, kind: PointerKind, at: Point, route: impl FnOnce(&mut Self)) {
12215 self.at = at;
12216 self.hover.event(kind, at, self.touch);
12217 let could_tap = gesture_was_a_tap(self.drag.as_ref());
12218 if kind == PointerKind::Down {
12219 let ui = frame(self.view, &self.runtime);
12220 self.runtime.input.hovered = self.hover.target(&ui);
12221 press_at(&mut self.runtime, Some(&ui), at);
12222 }
12223 route(self);
12224 settle_press(
12225 &mut self.runtime.input,
12226 kind,
12227 could_tap,
12228 self.drag.as_ref(),
12229 self.touch,
12230 );
12231 if matches!(kind, PointerKind::Up | PointerKind::Cancel) {
12232 let ui = frame(self.view, &self.runtime);
12233 self.runtime.input.hovered = self.hover.target(&ui);
12234 }
12235 }
12236
12237 fn land(&mut self, at: Point) {
12239 self.event(PointerKind::Down, at, |p| {
12240 if !p.touch {
12241 return;
12242 }
12243 let ui = frame(p.view, &p.runtime);
12244 let area = ui
12245 .scroll_chain(at)
12246 .find(|area| area.accepts_user_offset((0.0, 0.0)))
12247 .expect("the region under the finger has somewhere to go");
12248 p.drag = Some(Drag::Scroll {
12249 id: area.id,
12250 last: at,
12251 moved: false,
12252 carried: (0.0, 0.0),
12253 dismiss: None,
12254 axis: None,
12255 pan: None,
12256 });
12257 });
12258 }
12259
12260 fn slide(&mut self, by: (f32, f32)) {
12263 let to = Point::new(self.at.x + by.0, self.at.y + by.1);
12264 self.event(PointerKind::Move, to, |p| {
12265 let Some(Drag::Scroll {
12266 id,
12267 moved,
12268 axis,
12269 last,
12270 ..
12271 }) = p.drag.as_mut()
12272 else {
12273 return;
12274 };
12275 let (dx, dy) = (to.x - last.x, to.y - last.y);
12276 if dx.hypot(dy) <= super::TOUCH_SLOP && !*moved {
12277 return;
12278 }
12279 *moved = true;
12280 *axis = Some(dy.abs() >= dx.abs());
12281 *last = to;
12282 let offset = p.runtime.scroll.get(id).copied().unwrap_or((0.0, 0.0));
12283 let moved_to = (offset.0 - by.0, offset.1 - by.1);
12284 p.runtime.scroll.insert(*id, moved_to);
12285 p.runtime.scroll_target.insert(*id, moved_to);
12286 });
12287 }
12288
12289 fn lift(&mut self) -> Option<Drag> {
12291 let mut ended = None;
12292 self.event(PointerKind::Up, self.at, |p| ended = p.drag.take());
12293 ended
12294 }
12295 }
12296
12297 #[test]
12301 fn a_segment_a_swipe_started_on_is_not_left_pressed() {
12302 let view = filters();
12303 let mut finger = Pointer::new(view.as_ref(), true);
12304 let done = Point::new(200.0, 16.0);
12305 finger.land(done);
12306 let segment = finger
12307 .runtime
12308 .input
12309 .pressed
12310 .expect("the segment is pressed");
12311 assert!(
12312 finger.runtime.ink.contains_key(&segment),
12313 "and it splashed: a segment takes ink"
12314 );
12315 assert!(
12316 frame(view.as_ref(), &finger.runtime)
12317 .scroll_chain(done)
12318 .any(|area| area.max_x > 0.0),
12319 "the strip is wider than the screen, as the phone's was"
12320 );
12321 for _ in 0..19 {
12322 finger.slide((-10.0, 0.0));
12323 }
12324 let ended = finger.lift();
12325 assert!(
12326 !gesture_was_a_tap(ended.as_ref()),
12327 "a gesture that scrolled clicks nothing"
12328 );
12329 idle(&mut finger.runtime, 2.0);
12330 let (shown, quiet) = (
12332 painted(view.as_ref(), &finger.runtime),
12333 untouched(view.as_ref(), &finger.runtime),
12334 );
12335 assert_eq!(
12336 shown.len(),
12337 quiet.len(),
12338 "two seconds on, the screen paints what nobody touched, and no layer more"
12339 );
12340 assert_eq!(shown, quiet, "nothing is highlighted");
12341 assert!(
12342 finger.runtime.ink.is_empty(),
12343 "no ink waits for a finger that has gone"
12344 );
12345 assert_eq!(finger.runtime.input.pressed, None, "the press went with it");
12346 assert_eq!(
12347 finger.runtime.input.hovered, None,
12348 "and a lifted finger hovers nothing"
12349 );
12350 }
12351
12352 #[test]
12355 fn the_press_ends_when_the_scroll_takes_it() {
12356 let view = filters();
12357 let mut finger = Pointer::new(view.as_ref(), true);
12358 finger.land(Point::new(200.0, 16.0));
12359 finger.slide((-4.0, 0.0));
12360 assert!(
12361 finger.runtime.input.pressed.is_some(),
12362 "under the slop it may still be a tap"
12363 );
12364 finger.slide((-40.0, 0.0));
12365 assert_eq!(
12366 finger.runtime.input.pressed, None,
12367 "past it, the scroll has it"
12368 );
12369 assert_eq!(
12370 finger.runtime.input.hovered, None,
12371 "and the finger's hover with it"
12372 );
12373 idle(&mut finger.runtime, 0.5);
12374 assert_eq!(
12375 painted(view.as_ref(), &finger.runtime),
12376 untouched(view.as_ref(), &finger.runtime),
12377 "the finger is still down, and nothing under it is highlighted"
12378 );
12379 }
12380
12381 #[test]
12383 fn a_button_a_page_scrolled_from_is_not_left_pressed() {
12384 let view = page();
12385 let mut finger = Pointer::new(view.as_ref(), true);
12386 let go = Point::new(20.0, 15.0);
12387 finger.land(go);
12388 let button = finger.runtime.input.pressed.expect("the button is pressed");
12389 assert_eq!(
12390 frame(view.as_ref(), &finger.runtime).msg_for(button),
12391 Some(7),
12392 "the press is on the button"
12393 );
12394 for _ in 0..10 {
12395 finger.slide((0.0, -30.0));
12396 }
12397 assert!(!gesture_was_a_tap(finger.lift().as_ref()));
12398 idle(&mut finger.runtime, 2.0);
12399 assert_eq!(finger.runtime.input.pressed, None);
12400 assert!(finger.runtime.ink.is_empty());
12401 assert_eq!(
12402 painted(view.as_ref(), &finger.runtime),
12403 untouched(view.as_ref(), &finger.runtime),
12404 );
12405 }
12406
12407 #[test]
12410 fn a_tap_still_presses_and_clicks() {
12411 let view = filters();
12412 let mut finger = Pointer::new(view.as_ref(), true);
12413 let segment = Point::new(200.0, 16.0);
12414 finger.land(segment);
12415 finger.slide((3.0, 2.0));
12416 let pressed = finger.runtime.input.pressed;
12417 assert!(pressed.is_some(), "a wobble under the slop takes nothing");
12418 assert_eq!(finger.runtime.input.hovered, pressed, "and the press shows");
12419 let ui = frame(view.as_ref(), &finger.runtime);
12421 assert_eq!(
12422 ui.hit(segment),
12423 pressed,
12424 "released on the widget it pressed"
12425 );
12426 assert!(
12427 pressed.and_then(|id| ui.msg_for(id)).is_some(),
12428 "and the segment it pressed has a choice to make"
12429 );
12430 let ended = finger.lift();
12431 assert!(
12432 gesture_was_a_tap(ended.as_ref()),
12433 "a scroll that never moved"
12434 );
12435 assert_eq!(
12436 finger.runtime.input.pressed, None,
12437 "let go of once released"
12438 );
12439 }
12440
12441 #[test]
12444 fn a_mouse_keeps_its_press_while_it_moves_and_its_hover_after() {
12445 let view = filters();
12446 let mut mouse = Pointer::new(view.as_ref(), false);
12447 let done = Point::new(200.0, 16.0);
12448 mouse.land(done);
12449 assert!(mouse.drag.is_none(), "a mouse does not drag a scroll");
12450 let pressed = mouse.runtime.input.pressed.expect("pressed");
12451 mouse.slide((-60.0, 0.0));
12452 assert_eq!(mouse.runtime.input.pressed, Some(pressed), "still held");
12453 mouse.lift();
12454 assert_eq!(mouse.runtime.input.pressed, None);
12455 assert!(
12456 mouse.runtime.input.hovered.is_some(),
12457 "a released mouse is still over the strip"
12458 );
12459 }
12460
12461 #[test]
12464 fn a_gesture_takes_a_mouse_s_press_and_leaves_its_hover() {
12465 let id = frus_widgets::WidgetId::from_u64(3);
12466 let sheet = |moved| Drag::Sheet {
12467 id: frus_widgets::WidgetId::from_u64(9),
12468 last: Point::new(0.0, 0.0),
12469 moved,
12470 available: 800.0,
12471 };
12472 for touch in [false, true] {
12473 let mut input = frus_widgets::InputState {
12474 pressed: Some(id),
12475 hovered: Some(id),
12476 ..Default::default()
12477 };
12478 assert!(!settle_press(
12479 &mut input,
12480 PointerKind::Move,
12481 true,
12482 Some(&sheet(false)),
12483 touch
12484 ));
12485 assert_eq!(input.pressed, Some(id), "not yet past the slop");
12486 assert!(settle_press(
12487 &mut input,
12488 PointerKind::Move,
12489 true,
12490 Some(&sheet(true)),
12491 touch
12492 ));
12493 assert_eq!(input.pressed, None, "touch {touch}");
12494 assert_eq!(input.hovered, (!touch).then_some(id), "touch {touch}");
12495 }
12496 let mut input = frus_widgets::InputState {
12499 pressed: Some(id),
12500 ..Default::default()
12501 };
12502 let select = Drag::TextSelect {
12503 id,
12504 rect: super::Rect::new(0.0, 0.0, 10.0, 10.0),
12505 };
12506 assert!(!settle_press(
12507 &mut input,
12508 PointerKind::Move,
12509 false,
12510 Some(&select),
12511 true
12512 ));
12513 assert_eq!(input.pressed, Some(id));
12514 }
12515}
12516
12517#[cfg(test)]
12520mod selection_area_tests {
12521 use super::testing::Driver;
12522 use crate::{Application, Command};
12523 use frus_widgets::{Button, Container, Flex, Point, SelectionArea, Text, Theme, Widget};
12524
12525 const W: f32 = 400.0;
12526 const H: f32 = 300.0;
12527
12528 #[derive(Default)]
12530 struct Doc {
12531 bar: Bar,
12533 shared: std::rc::Rc<std::cell::Cell<u32>>,
12535 platform: Option<frus_widgets::TargetPlatform>,
12538 }
12539
12540 #[derive(Default, Clone, Copy)]
12542 enum Bar {
12543 #[default]
12545 Default,
12546 Share,
12548 Empty,
12550 }
12551
12552 impl Application for Doc {
12553 type Message = ();
12554
12555 fn update(&mut self, _message: ()) -> Command<()> {
12556 self.shared.set(self.shared.get() + 1);
12557 Command::none()
12558 }
12559
12560 fn theme(&self) -> Theme {
12561 Theme::default().with_platform(
12562 self.platform
12563 .unwrap_or(frus_widgets::TargetPlatform::Android),
12564 )
12565 }
12566
12567 fn view(&self, _theme: &Theme) -> Box<dyn Widget<()>> {
12568 let area = SelectionArea::around(
12569 Flex::column()
12570 .child(Text::new("first line of words").no_wrap())
12571 .child(Text::new("second line").no_wrap())
12572 .child(Button::new("Press").on_press(()))
12573 .child(Text::new("third line ends here").no_wrap()),
12574 );
12575 let area = match self.bar {
12576 Bar::Default => area,
12577 Bar::Share => area.selection_toolbar(|_, items| {
12578 let mut items: Vec<_> = items
12579 .into_iter()
12580 .filter(|item| item.label() == "Copy")
12581 .collect();
12582 items.push(frus_widgets::ToolbarItem::custom("Share", ()));
12583 items
12584 }),
12585 Bar::Empty => area.selection_toolbar(|_, _| Vec::new()),
12586 };
12587 Box::new(
12588 Container::new().width(W).height(H).child(
12589 Flex::column()
12590 .child(Text::new("Outside the area").no_wrap())
12591 .child(area),
12592 ),
12593 )
12594 }
12595 }
12596
12597 struct Tappable;
12599
12600 impl Application for Tappable {
12601 type Message = ();
12602
12603 fn update(&mut self, _message: ()) -> Command<()> {
12604 Command::none()
12605 }
12606
12607 fn view(&self, _theme: &Theme) -> Box<dyn Widget<()>> {
12608 Box::new(
12609 Container::new()
12610 .width(W)
12611 .height(H)
12612 .child(SelectionArea::around(
12613 Flex::column()
12614 .child(
12615 Container::new()
12616 .on_click(())
12617 .child(Text::new("tap this row").no_wrap()),
12618 )
12619 .child(Text::new("plain words here").no_wrap()),
12620 )),
12621 )
12622 }
12623 }
12624
12625 #[test]
12629 fn a_row_that_takes_a_tap_keeps_its_press() {
12630 let mut d = Driver::new(Tappable, W, H);
12631 d.run(0.2);
12632 let find = |d: &Driver<Tappable>, label: &str| {
12633 let (_, rect) = d
12634 .texts()
12635 .into_iter()
12636 .find(|(text, _)| text == label)
12637 .expect("on screen");
12638 Point::new(rect.x + 20.0, rect.y + 8.0)
12639 };
12640 let (tap, plain) = (find(&d, "tap this row"), find(&d, "plain words here"));
12641 let end = Point::new(plain.x + 60.0, plain.y);
12642 d.press_mouse(tap);
12643 d.run(0.05);
12644 d.move_mouse(end);
12645 d.run(0.05);
12646 d.release_mouse(end);
12647 d.run(0.05);
12648 assert_eq!(d.area_selection(), None, "the row's press stayed the row's");
12649
12650 d.press_mouse(plain);
12651 d.run(0.05);
12652 d.move_mouse(end);
12653 d.run(0.05);
12654 d.release_mouse(end);
12655 d.run(0.05);
12656 assert!(d.area_selection().is_some(), "plain words still select");
12657 }
12658
12659 fn driver() -> Driver<Doc> {
12660 driver_with(Bar::Default)
12661 }
12662
12663 fn driver_with(bar: Bar) -> Driver<Doc> {
12664 driver_on(bar, None)
12665 }
12666
12667 fn driver_on(bar: Bar, platform: Option<frus_widgets::TargetPlatform>) -> Driver<Doc> {
12668 let doc = Doc {
12669 bar,
12670 platform,
12671 ..Doc::default()
12672 };
12673 let mut driver = Driver::new(doc, W, H);
12674 driver.run(0.2);
12675 driver
12676 }
12677
12678 fn on(driver: &Driver<Doc>, label: &str, across: f32) -> Point {
12681 let (_, rect) = driver
12682 .texts()
12683 .into_iter()
12684 .find(|(text, _)| text == label)
12685 .unwrap_or_else(|| panic!("{label:?} is on screen"));
12686 Point::new(rect.x + across, rect.y + 8.0)
12689 }
12690
12691 fn drag(driver: &mut Driver<Doc>, from: Point, to: Point) {
12692 driver.press_mouse(from);
12693 driver.run(0.05);
12694 driver.move_mouse(to);
12695 driver.run(0.05);
12696 driver.release_mouse(to);
12697 driver.run(0.05);
12698 }
12699
12700 #[test]
12704 fn a_drag_selects_across_the_texts_of_an_area() {
12705 let mut d = driver();
12706 let (from, to) = (
12707 on(&d, "first line of words", 60.0),
12708 on(&d, "third line ends here", 40.0),
12709 );
12710 drag(&mut d, from, to);
12711 let copied = d.area_selection().expect("something is selected");
12712 let lines: Vec<&str> = copied.lines().collect();
12713 assert_eq!(lines.len(), 3, "one line per text: {copied:?}");
12714 assert!(
12715 !lines[0].is_empty() && "first line of words".ends_with(lines[0]),
12716 "the tail of the first: {:?}",
12717 lines[0]
12718 );
12719 assert!(lines[0].len() < "first line of words".len());
12720 assert_eq!(lines[1], "second line", "the whole of the middle one");
12721 assert!(
12722 !lines[2].is_empty() && "third line ends here".starts_with(lines[2]),
12723 "the head of the last: {:?}",
12724 lines[2]
12725 );
12726 assert!(lines[2].len() < "third line ends here".len());
12727 assert!(!copied.contains("Press"), "a button's label is not text");
12728 }
12729
12730 #[test]
12732 fn dragging_upward_selects_the_same_words() {
12733 let (down, up) = {
12734 let mut d = driver();
12735 let (a, b) = (
12736 on(&d, "first line of words", 60.0),
12737 on(&d, "third line ends here", 40.0),
12738 );
12739 drag(&mut d, a, b);
12740 let down = d.area_selection();
12741 drag(&mut d, b, a);
12742 (down, d.area_selection())
12743 };
12744 assert!(down.is_some());
12745 assert_eq!(down, up);
12746 }
12747
12748 #[test]
12751 fn a_drag_past_the_words_ends_at_the_last_of_them() {
12752 let mut d = driver();
12753 let from = on(&d, "second line", 5.0);
12754 drag(&mut d, from, Point::new(W - 2.0, H - 2.0));
12755 let copied = d.area_selection().expect("a selection");
12756 assert!(copied.ends_with("third line ends here"), "{copied:?}");
12757 }
12758
12759 #[test]
12762 fn words_outside_the_area_are_left_alone() {
12763 let mut d = driver();
12764 let outside = on(&d, "Outside the area", 30.0);
12765 let end = on(&d, "third line ends here", 40.0);
12766 drag(&mut d, outside, end);
12767 assert_eq!(d.area_selection(), None, "the press began outside");
12768
12769 let inside = on(&d, "first line of words", 30.0);
12770 drag(&mut d, inside, Point::new(20.0, 2.0));
12771 let copied = d.area_selection().expect("a selection");
12772 assert!(!copied.contains("Outside"), "{copied:?}");
12773 }
12774
12775 #[test]
12777 fn a_button_keeps_its_press() {
12778 let mut d = driver();
12779 let button = on(&d, "Press", 10.0);
12780 let end = on(&d, "third line ends here", 40.0);
12781 drag(&mut d, button, end);
12782 assert_eq!(d.area_selection(), None);
12783 }
12784
12785 #[test]
12787 fn a_click_puts_the_selection_away() {
12788 let mut d = driver();
12789 let (from, to) = (
12790 on(&d, "first line of words", 60.0),
12791 on(&d, "third line ends here", 40.0),
12792 );
12793 drag(&mut d, from, to);
12794 assert!(d.area_selection().is_some());
12795 let elsewhere = on(&d, "second line", 30.0);
12796 drag(&mut d, elsewhere, elsewhere);
12797 assert_eq!(d.area_selection(), None, "a click in the area");
12798
12799 drag(&mut d, from, to);
12800 assert!(d.area_selection().is_some());
12801 let outside = on(&d, "Outside the area", 30.0);
12802 drag(&mut d, outside, outside);
12803 assert_eq!(d.area_selection(), None, "a click outside it");
12804 }
12805
12806 #[test]
12808 fn a_double_click_selects_the_word() {
12809 let mut d = driver();
12810 let at = on(&d, "second line", 12.0);
12812 d.press_mouse(at);
12813 d.release_mouse(at);
12814 d.run(0.05);
12815 d.press_mouse(at);
12816 d.release_mouse(at);
12817 d.run(0.05);
12818 assert_eq!(d.area_selection().as_deref(), Some("second"));
12819 }
12820
12821 #[test]
12823 fn select_all_takes_the_whole_area() {
12824 let mut d = driver();
12825 let at = on(&d, "second line", 12.0);
12826 let end = on(&d, "second line", 40.0);
12827 drag(&mut d, at, end);
12828 assert!(d.area_selection().is_some());
12829 d.area_select_all();
12830 assert_eq!(
12831 d.area_selection().as_deref(),
12832 Some("first line of words\nsecond line\nthird line ends here")
12833 );
12834 }
12835
12836 #[test]
12838 fn a_finger_does_not_select() {
12839 let mut d = driver();
12840 let (from, to) = (
12841 on(&d, "first line of words", 60.0),
12842 on(&d, "third line ends here", 40.0),
12843 );
12844 d.press(from);
12845 d.run(0.05);
12846 d.move_to(to);
12847 d.run(0.05);
12848 d.release(to);
12849 d.run(0.05);
12850 assert_eq!(d.area_selection(), None);
12851 }
12852
12853 fn hold(d: &mut Driver<Doc>, at: Point) -> bool {
12855 d.press(at);
12856 d.run(0.1);
12857 let fired = d.hold_deadline();
12858 d.run(0.05);
12859 d.release(at);
12860 d.run(0.05);
12861 fired
12862 }
12863
12864 fn bar_button(d: &Driver<Doc>, label: &str) -> Option<Point> {
12866 let (ui, _) = d.frame_parts()?;
12867 d.texts()
12868 .into_iter()
12869 .filter(|(text, _)| text == label)
12870 .map(|(_, rect)| Point::new(rect.x + 4.0, rect.y + 6.0))
12871 .find(|at| ui.toolbar_contains(*at))
12872 }
12873
12874 #[test]
12877 fn a_hold_selects_the_word_and_opens_the_bar() {
12878 let mut d = driver();
12879 let at = on(&d, "second line", 12.0);
12880 assert!(hold(&mut d, at), "the hold fired");
12881 assert_eq!(d.area_selection().as_deref(), Some("second"));
12882 assert!(bar_button(&d, "Copy").is_some(), "Copy is on the bar");
12883 assert!(bar_button(&d, "Select all").is_some(), "and Select all");
12884 }
12885
12886 #[test]
12889 fn the_desktop_menus_rows_act_on_the_area() {
12890 let mut d = driver_on(Bar::Default, Some(frus_widgets::TargetPlatform::Linux));
12891 let at = on(&d, "second line", 12.0);
12892 hold(&mut d, at);
12893 let select_all = bar_button(&d, "Select all").expect("Select all");
12894 d.press(select_all);
12897 d.release(select_all);
12898 d.run(0.05);
12899 assert_eq!(
12900 d.area_selection().as_deref(),
12901 Some("first line of words\nsecond line\nthird line ends here")
12902 );
12903 }
12904
12905 #[test]
12908 fn the_bars_buttons_act_on_the_area() {
12909 let mut d = driver();
12910 let at = on(&d, "second line", 12.0);
12911 hold(&mut d, at);
12912 let select_all = bar_button(&d, "Select all").expect("Select all");
12913 d.press(select_all);
12914 d.release(select_all);
12915 d.run(0.05);
12916 assert_eq!(
12917 d.area_selection().as_deref(),
12918 Some("first line of words\nsecond line\nthird line ends here")
12919 );
12920 assert!(
12921 bar_button(&d, "Select all").is_none(),
12922 "nothing left to select"
12923 );
12924 let copy = bar_button(&d, "Copy").expect("Copy stays");
12925 d.press(copy);
12926 d.release(copy);
12927 d.run(0.05);
12928 assert_eq!(d.area_selection(), None, "copied, and put away");
12929 assert!(bar_button(&d, "Copy").is_none(), "the bar with it");
12930 }
12931
12932 #[test]
12934 fn a_hold_on_a_button_selects_nothing() {
12935 let mut d = driver();
12936 let button = on(&d, "Press", 10.0);
12937 hold(&mut d, button);
12938 assert_eq!(d.area_selection(), None);
12939 }
12940
12941 #[test]
12943 fn escape_and_a_tap_put_the_bar_away() {
12944 let mut d = driver();
12945 let at = on(&d, "second line", 12.0);
12946 hold(&mut d, at);
12947 assert!(d.area_selection().is_some());
12948 d.escape();
12949 d.run(0.05);
12950 assert_eq!(d.area_selection(), None);
12951 assert!(bar_button(&d, "Copy").is_none());
12952
12953 hold(&mut d, at);
12954 assert!(d.area_selection().is_some());
12955 let elsewhere = on(&d, "Outside the area", 20.0);
12956 d.press(elsewhere);
12957 d.release(elsewhere);
12958 d.run(0.05);
12959 assert_eq!(d.area_selection(), None);
12960 assert!(bar_button(&d, "Copy").is_none());
12961 }
12962
12963 #[test]
12966 fn a_mouse_selection_has_no_bar() {
12967 let mut d = driver();
12968 let (from, to) = (
12969 on(&d, "first line of words", 60.0),
12970 on(&d, "third line ends here", 40.0),
12971 );
12972 drag(&mut d, from, to);
12973 assert!(d.area_selection().is_some());
12974 assert!(bar_button(&d, "Copy").is_none());
12975 }
12976
12977 #[test]
12980 fn the_application_says_what_the_bar_holds() {
12981 let mut d = driver_with(Bar::Share);
12982 let at = on(&d, "second line", 12.0);
12983 assert!(hold(&mut d, at));
12984 assert_eq!(d.area_selection().as_deref(), Some("second"));
12985 assert!(bar_button(&d, "Copy").is_some(), "Copy was kept");
12986 assert!(
12987 bar_button(&d, "Select all").is_none(),
12988 "Select all was taken off"
12989 );
12990 let share = bar_button(&d, "Share").expect("the application's own item");
12991 let shared = d.app().shared.clone();
12992 d.press(share);
12993 d.release(share);
12994 d.run(0.05);
12995 assert_eq!(shared.get(), 1, "its message was sent");
12996 }
12997
12998 #[test]
13000 fn an_empty_list_shows_no_bar() {
13001 let mut d = driver_with(Bar::Empty);
13002 let at = on(&d, "second line", 12.0);
13003 assert!(hold(&mut d, at));
13004 assert_eq!(d.area_selection().as_deref(), Some("second"));
13005 assert!(bar_button(&d, "Copy").is_none());
13006 assert!(d
13007 .frame_parts()
13008 .is_some_and(|(ui, _)| !ui.toolbar_contains(at)));
13009 }
13010
13011 fn middle(rect: frus_widgets::Rect) -> Point {
13013 Point::new(rect.x + rect.width / 2.0, rect.y + rect.height / 2.0)
13014 }
13015
13016 fn carry_handle(d: &mut Driver<Doc>, from: Point, to: Point) {
13018 d.press(from);
13019 d.run(0.05);
13020 for step in 1..=4 {
13021 let f = step as f32 / 4.0;
13022 d.move_to(Point::new(
13023 from.x + (to.x - from.x) * f,
13024 from.y + (to.y - from.y) * f,
13025 ));
13026 d.run(0.05);
13027 }
13028 d.release(to);
13029 d.run(0.05);
13030 }
13031
13032 #[test]
13035 fn a_touch_selection_has_handles_and_a_mouse_one_does_not() {
13036 let mut d = driver();
13037 let paths = |d: &Driver<Doc>| {
13038 let (ui, _) = d.frame_parts().expect("a frame");
13039 ui.scene()
13040 .primitives()
13041 .iter()
13042 .filter(|p| matches!(p, frus_widgets::Primitive::Path { .. }))
13043 .count()
13044 };
13045 let before = paths(&d);
13046 let at = on(&d, "second line", 12.0);
13047 hold(&mut d, at);
13048 assert_eq!(paths(&d), before + 2, "the two handles are painted");
13049 let [start, end] = d.area_handles().expect("two handles");
13050 assert!(start.line_center.x < end.line_center.x, "{start:?} {end:?}");
13051 assert!(start.rect.y > at.y - 8.0, "they hang below the line");
13052
13053 let (from, to) = (
13054 on(&d, "first line of words", 60.0),
13055 on(&d, "third line ends here", 40.0),
13056 );
13057 drag(&mut d, from, to);
13058 assert!(d.area_selection().is_some());
13059 assert!(
13060 d.area_handles().is_none(),
13061 "a mouse selection has no handles"
13062 );
13063 d.area_select_all();
13064 assert!(
13065 d.area_handles().is_none(),
13066 "nor does it grow any when the keyboard selects all"
13067 );
13068 }
13069
13070 #[test]
13074 fn the_end_handle_widens_the_selection_across_texts() {
13075 let mut d = driver();
13076 let at = on(&d, "second line", 12.0);
13077 hold(&mut d, at);
13078 let [_, end] = d.area_handles().expect("handles");
13079 let third = on(&d, "third line ends here", 60.0);
13080 let from = middle(end.rect);
13081 let to = Point::new(third.x, from.y + (third.y - at.y));
13083 d.press(from);
13084 d.run(0.05);
13085 d.move_to(Point::new(from.x, (from.y + to.y) / 2.0));
13086 d.run(0.05);
13087 d.move_to(to);
13088 d.run(0.05);
13089 assert!(
13090 bar_button(&d, "Copy").is_none(),
13091 "no bar while a handle moves"
13092 );
13093 d.release(to);
13094 d.run(0.05);
13095 let copied = d.area_selection().expect("a selection");
13096 let lines: Vec<&str> = copied.lines().collect();
13097 assert_eq!(lines.len(), 2, "{copied:?}");
13098 assert_eq!(lines[0], "second line", "the rest of the word's line");
13099 assert!("third line ends here".starts_with(lines[1]), "{copied:?}");
13100 assert!(!lines[1].is_empty());
13101 assert!(bar_button(&d, "Copy").is_some(), "the bar is back");
13102 }
13103
13104 #[test]
13107 fn the_start_handle_widens_it_upward() {
13108 let mut d = driver();
13109 let at = on(&d, "second line", 80.0);
13110 hold(&mut d, at);
13111 assert_eq!(d.area_selection().as_deref(), Some("line"));
13112 let [start, _] = d.area_handles().expect("handles");
13113 let first = on(&d, "first line of words", 30.0);
13114 let from = middle(start.rect);
13115 let to = Point::new(first.x, from.y + (first.y - at.y));
13116 carry_handle(&mut d, from, to);
13117 let copied = d.area_selection().expect("a selection");
13118 let lines: Vec<&str> = copied.lines().collect();
13119 assert_eq!(lines.len(), 2, "{copied:?}");
13120 assert!("first line of words".ends_with(lines[0]), "{copied:?}");
13121 assert_eq!(lines[1], "second line");
13122 }
13123
13124 #[test]
13127 fn a_handle_does_not_cross_the_other() {
13128 let mut d = driver();
13129 let at = on(&d, "second line", 80.0);
13130 hold(&mut d, at);
13131 let before = d.area_selection();
13132 assert!(before.is_some());
13133 let [_, end] = d.area_handles().expect("handles");
13134 let first = on(&d, "first line of words", 5.0);
13135 let from = middle(end.rect);
13136 let to = Point::new(first.x, from.y + (first.y - at.y));
13138 d.press(from);
13139 d.run(0.05);
13140 d.move_to(to);
13141 d.run(0.05);
13142 d.release(to);
13143 d.run(0.05);
13144 assert_eq!(d.area_selection(), before);
13145 }
13146
13147 #[test]
13150 fn the_selection_is_highlighted_in_each_text() {
13151 let mut d = driver();
13152 let (from, to) = (
13153 on(&d, "first line of words", 60.0),
13154 on(&d, "third line ends here", 40.0),
13155 );
13156 let bare = rects(&d);
13157 drag(&mut d, from, to);
13158 let boxes: Vec<frus_widgets::Rect> = rects(&d)
13160 .into_iter()
13161 .filter(|rect| !bare.contains(rect))
13162 .collect();
13163 assert_eq!(boxes.len(), 3, "one per text covered: {boxes:?}");
13164 let outside = on(&d, "Outside the area", 0.0);
13165 assert!(
13166 boxes.iter().all(|b| b.y > outside.y + 10.0),
13167 "none over the line outside the area: {boxes:?}"
13168 );
13169 }
13170
13171 fn rects(d: &Driver<Doc>) -> Vec<frus_widgets::Rect> {
13172 let (ui, _) = d.frame_parts().expect("a frame");
13173 ui.scene()
13174 .primitives()
13175 .iter()
13176 .filter_map(|p| match p {
13177 frus_widgets::Primitive::Rect { rect, .. } => Some(*rect),
13178 _ => None,
13179 })
13180 .collect()
13181 }
13182}
13183
13184#[cfg(test)]
13187mod gesture_detector_tests {
13188 use super::testing::Driver;
13189 use crate::{Application, Command};
13190 use frus_widgets::{Container, Flex, GestureDetector, Point, Text, Theme, Widget};
13191 use std::cell::RefCell;
13192 use std::rc::Rc;
13193
13194 const W: f32 = 400.0;
13195 const H: f32 = 400.0;
13196
13197 #[derive(Clone, Debug, PartialEq)]
13198 enum Msg {
13199 Tap,
13200 Double,
13201 Hold,
13202 Menu,
13203 Plain,
13204 OnlyDouble,
13205 }
13206
13207 #[derive(Default)]
13210 struct Page {
13211 heard: Rc<RefCell<Vec<Msg>>>,
13212 }
13213
13214 impl Application for Page {
13215 type Message = Msg;
13216
13217 fn update(&mut self, message: Msg) -> Command<Msg> {
13218 self.heard.borrow_mut().push(message);
13219 Command::none()
13220 }
13221
13222 fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
13223 let block = |label: &str| {
13224 Container::new()
13225 .width(300.0)
13226 .height(60.0)
13227 .child(Text::new(label).no_wrap())
13228 };
13229 Box::new(
13230 Container::new().width(W).height(H).child(
13231 Flex::column()
13232 .gap(40.0)
13233 .child(
13234 GestureDetector::new(block("everything"))
13235 .on_tap(Msg::Tap)
13236 .on_double_tap(Msg::Double)
13237 .on_long_press(Msg::Hold)
13238 .on_secondary_tap(Msg::Menu),
13239 )
13240 .child(GestureDetector::new(block("tap only")).on_tap(Msg::Plain))
13241 .child(
13242 GestureDetector::new(block("double only"))
13243 .on_double_tap(Msg::OnlyDouble),
13244 ),
13245 ),
13246 )
13247 }
13248 }
13249
13250 fn driver() -> (Driver<Page>, Rc<RefCell<Vec<Msg>>>) {
13251 let page = Page::default();
13252 let heard = page.heard.clone();
13253 let mut driver = Driver::new(page, W, H);
13254 driver.run(0.2);
13255 (driver, heard)
13256 }
13257
13258 fn on(d: &Driver<Page>, label: &str) -> Point {
13260 let (_, rect) = d
13261 .texts()
13262 .into_iter()
13263 .find(|(text, _)| text == label)
13264 .unwrap_or_else(|| panic!("{label:?} is on screen"));
13265 Point::new(rect.x + 20.0, rect.y + 8.0)
13266 }
13267
13268 fn tap(d: &mut Driver<Page>, at: Point) {
13269 d.press(at);
13270 d.run(0.02);
13271 d.release(at);
13272 d.run(0.02);
13273 }
13274
13275 #[test]
13278 fn a_tap_waits_while_a_double_tap_is_possible() {
13279 let (mut d, heard) = driver();
13280 let at = on(&d, "everything");
13281 tap(&mut d, at);
13282 assert!(heard.borrow().is_empty(), "not yet: {:?}", heard.borrow());
13283 assert!(d.tap_deadline(), "a tap was waiting");
13284 assert_eq!(*heard.borrow(), vec![Msg::Tap]);
13285 }
13286
13287 #[test]
13290 fn the_loop_wakes_for_a_waiting_tap() {
13291 let (mut d, _) = driver();
13292 let before = std::time::Instant::now();
13293 let at = on(&d, "everything");
13294 tap(&mut d, at);
13295 let wake = d.next_wake().expect("a wake for the tap");
13296 let wait = wake.duration_since(before);
13297 assert!(
13298 wait <= std::time::Duration::from_millis(400),
13299 "about 300 ms: {wait:?}"
13300 );
13301 d.tap_deadline();
13302 assert_eq!(d.next_wake(), None);
13303 }
13304
13305 #[test]
13307 fn two_quick_taps_are_a_double_tap() {
13308 let (mut d, heard) = driver();
13309 let at = on(&d, "everything");
13310 tap(&mut d, at);
13311 tap(&mut d, Point::new(at.x + 6.0, at.y + 2.0));
13312 assert_eq!(*heard.borrow(), vec![Msg::Double]);
13313 assert!(!d.tap_deadline(), "nothing is left waiting");
13314 assert_eq!(*heard.borrow(), vec![Msg::Double]);
13315 }
13316
13317 #[test]
13319 fn a_tap_alone_answers_at_once() {
13320 let (mut d, heard) = driver();
13321 let at = on(&d, "tap only");
13322 tap(&mut d, at);
13323 assert_eq!(*heard.borrow(), vec![Msg::Plain]);
13324 tap(&mut d, at);
13325 assert_eq!(
13326 *heard.borrow(),
13327 vec![Msg::Plain, Msg::Plain],
13328 "two taps are two taps"
13329 );
13330 }
13331
13332 #[test]
13335 fn a_press_elsewhere_sends_the_waiting_tap() {
13336 let (mut d, heard) = driver();
13337 let (first, other) = (on(&d, "everything"), on(&d, "tap only"));
13338 tap(&mut d, first);
13339 tap(&mut d, other);
13340 assert_eq!(*heard.borrow(), vec![Msg::Tap, Msg::Plain]);
13341 }
13342
13343 #[test]
13345 fn a_secondary_click_sends_its_message() {
13346 let (mut d, heard) = driver();
13347 let at = on(&d, "everything");
13348 d.secondary_click(at);
13349 d.run(0.02);
13350 assert_eq!(*heard.borrow(), vec![Msg::Menu]);
13351 let plain = on(&d, "tap only");
13352 d.secondary_click(plain);
13353 assert_eq!(
13354 *heard.borrow(),
13355 vec![Msg::Menu],
13356 "a detector without one hears nothing"
13357 );
13358 }
13359
13360 #[test]
13362 fn a_long_press_is_not_also_a_tap() {
13363 let (mut d, heard) = driver();
13364 let at = on(&d, "everything");
13365 d.press(at);
13366 d.run(0.1);
13367 assert!(d.hold_deadline(), "the hold fired");
13368 d.release(at);
13369 d.run(0.02);
13370 d.tap_deadline();
13371 assert_eq!(*heard.borrow(), vec![Msg::Hold]);
13372 }
13373
13374 #[test]
13377 fn a_double_tap_alone_is_heard() {
13378 let (mut d, heard) = driver();
13379 let at = on(&d, "double only");
13380 tap(&mut d, at);
13381 d.tap_deadline();
13382 assert!(heard.borrow().is_empty(), "{:?}", heard.borrow());
13383 tap(&mut d, at);
13384 tap(&mut d, at);
13385 assert_eq!(*heard.borrow(), vec![Msg::OnlyDouble]);
13386 }
13387}
13388
13389#[cfg(test)]
13392mod gesture_pan_tests {
13393 use super::testing::Driver;
13394 use crate::{Application, Command};
13395 use frus_widgets::{
13396 Container, Flex, GestureDetector, PanAxis, Point, SingleChildScrollView, Text, Theme,
13397 Widget,
13398 };
13399 use std::cell::RefCell;
13400 use std::rc::Rc;
13401
13402 const W: f32 = 400.0;
13403 const H: f32 = 400.0;
13404
13405 #[derive(Clone, Debug, PartialEq)]
13406 enum Msg {
13407 Start(Point),
13408 Update(Point, Point),
13409 End(Point),
13410 Tap,
13411 Hold,
13412 }
13413
13414 struct Page {
13416 axis: PanAxis,
13417 scrolled: bool,
13418 heard: Rc<RefCell<Vec<Msg>>>,
13419 }
13420
13421 impl Application for Page {
13422 type Message = Msg;
13423
13424 fn update(&mut self, message: Msg) -> Command<Msg> {
13425 self.heard.borrow_mut().push(message);
13426 Command::none()
13427 }
13428
13429 fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
13430 let detector = GestureDetector::new(
13431 Container::new()
13432 .width(300.0)
13433 .height(150.0)
13434 .child(Text::new("drag me").no_wrap()),
13435 )
13436 .on_tap(Msg::Tap)
13437 .on_long_press(Msg::Hold)
13438 .on_pan_start(Msg::Start)
13439 .on_pan_update(Msg::Update)
13440 .on_pan_end(Msg::End)
13441 .pan_axis(self.axis);
13442 let content = Flex::column()
13443 .child(Container::new().width(300.0).height(30.0))
13444 .child(detector)
13445 .child(Container::new().width(300.0).height(1200.0));
13446 if self.scrolled {
13447 Box::new(
13448 SingleChildScrollView::new()
13449 .width(W)
13450 .height(H)
13451 .child(content),
13452 )
13453 } else {
13454 Box::new(Container::new().width(W).height(H).child(content))
13455 }
13456 }
13457 }
13458
13459 fn driver(axis: PanAxis, scrolled: bool) -> (Driver<Page>, Rc<RefCell<Vec<Msg>>>) {
13460 let heard = Rc::new(RefCell::new(Vec::new()));
13461 let page = Page {
13462 axis,
13463 scrolled,
13464 heard: heard.clone(),
13465 };
13466 let mut driver = Driver::new(page, W, H);
13467 driver.run(0.2);
13468 (driver, heard)
13469 }
13470
13471 fn origin(d: &Driver<Page>) -> Point {
13473 let (_, rect) = d
13474 .texts()
13475 .into_iter()
13476 .find(|(text, _)| text == "drag me")
13477 .expect("on screen");
13478 Point::new(rect.x, rect.y)
13479 }
13480
13481 fn swipe(d: &mut Driver<Page>, from: Point, dx: f32, dy: f32) {
13483 d.press(from);
13484 d.run(0.02);
13485 for step in 1..=8 {
13486 let f = step as f32 / 8.0;
13487 d.move_to(Point::new(from.x + dx * f, from.y + dy * f));
13488 d.run(0.02);
13489 }
13490 d.release(Point::new(from.x + dx, from.y + dy));
13491 d.run(0.02);
13492 }
13493
13494 fn deltas(heard: &[Msg]) -> Point {
13495 heard.iter().fold(Point::new(0.0, 0.0), |sum, m| match m {
13496 Msg::Update(_, delta) => Point::new(sum.x + delta.x, sum.y + delta.y),
13497 _ => sum,
13498 })
13499 }
13500
13501 fn scroll_y(d: &Driver<Page>) -> f32 {
13502 d.offsets().first().map_or(0.0, |(_, offset)| offset.1)
13503 }
13504
13505 #[test]
13509 fn a_drag_is_a_start_updates_and_an_end() {
13510 let (mut d, heard) = driver(PanAxis::Free, false);
13511 let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 40.0);
13512 swipe(&mut d, at, 120.0, 60.0);
13513 let heard = heard.borrow();
13514 assert!(matches!(heard.first(), Some(Msg::Start(_))), "{heard:?}");
13515 if let Some(Msg::Start(local)) = heard.first() {
13516 let corner = origin(&d);
13518 assert!(corner.y >= 30.0, "{corner:?}");
13519 assert_eq!(*local, Point::new(at.x - corner.x, at.y - corner.y));
13520 }
13521 assert!(matches!(heard.last(), Some(Msg::End(_))), "{heard:?}");
13522 if let Some(Msg::End(velocity)) = heard.last() {
13523 assert!(velocity.x.hypot(velocity.y) <= 8000.0 + 0.5, "{velocity:?}");
13524 }
13525 let moved = deltas(&heard);
13526 assert!(moved.x > 60.0 && moved.y > 20.0, "{moved:?}");
13527 assert!(!heard.contains(&Msg::Tap), "a drag is not a tap");
13528 let last = heard.iter().rev().find_map(|m| match m {
13530 Msg::Update(local, _) => Some(*local),
13531 _ => None,
13532 });
13533 let corner = origin(&d);
13534 assert_eq!(
13535 last,
13536 Some(Point::new(at.x + 120.0 - corner.x, at.y + 60.0 - corner.y))
13537 );
13538 }
13539
13540 #[test]
13542 fn a_press_that_does_not_move_is_a_tap() {
13543 let (mut d, heard) = driver(PanAxis::Free, false);
13544 let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 40.0);
13545 swipe(&mut d, at, 3.0, 2.0);
13546 assert_eq!(*heard.borrow(), vec![Msg::Tap]);
13547 }
13548
13549 #[test]
13552 fn a_hold_on_a_dragging_detector_is_a_long_press() {
13553 let (mut d, heard) = driver(PanAxis::Free, false);
13554 let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 40.0);
13555 d.press(at);
13556 d.run(0.1);
13557 assert!(d.hold_deadline(), "the hold fired");
13558 d.release(at);
13559 d.run(0.02);
13560 assert_eq!(*heard.borrow(), vec![Msg::Hold]);
13561 }
13562
13563 #[test]
13565 fn a_mouse_drags_it() {
13566 let (mut d, heard) = driver(PanAxis::Free, false);
13567 let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 40.0);
13568 d.press_mouse(at);
13569 d.run(0.02);
13570 d.move_mouse(Point::new(at.x + 30.0, at.y));
13571 d.run(0.02);
13572 d.release_mouse(Point::new(at.x + 30.0, at.y));
13573 d.run(0.02);
13574 let heard = heard.borrow();
13575 assert!(matches!(heard.first(), Some(Msg::Start(_))), "{heard:?}");
13576 assert!(matches!(heard.last(), Some(Msg::End(_))), "{heard:?}");
13577 }
13578
13579 #[test]
13582 fn a_horizontal_drag_shares_a_vertical_scroll() {
13583 let (mut d, heard) = driver(PanAxis::Horizontal, true);
13584 let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 40.0);
13585 swipe(&mut d, at, 120.0, 10.0);
13586 assert!(
13587 matches!(heard.borrow().first(), Some(Msg::Start(_))),
13588 "{:?}",
13589 heard.borrow()
13590 );
13591 assert_eq!(deltas(&heard.borrow()).y, 0.0, "only across");
13592 assert_eq!(scroll_y(&d), 0.0, "the page did not move");
13593
13594 heard.borrow_mut().clear();
13595 let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 120.0);
13596 swipe(&mut d, at, 5.0, -120.0);
13597 assert!(heard.borrow().is_empty(), "{:?}", heard.borrow());
13598 assert!(scroll_y(&d) > 30.0, "the page scrolled: {}", scroll_y(&d));
13599 }
13600
13601 #[test]
13604 fn a_free_drag_leaves_the_scroll_its_own_direction() {
13605 let (mut d, heard) = driver(PanAxis::Free, true);
13606 let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 40.0);
13607 swipe(&mut d, at, 150.0, 8.0);
13608 assert!(
13609 matches!(heard.borrow().first(), Some(Msg::Start(_))),
13610 "{:?}",
13611 heard.borrow()
13612 );
13613 assert_eq!(scroll_y(&d), 0.0);
13614
13615 heard.borrow_mut().clear();
13616 let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 120.0);
13617 swipe(&mut d, at, 4.0, -120.0);
13618 assert!(heard.borrow().is_empty(), "{:?}", heard.borrow());
13619 assert!(scroll_y(&d) > 30.0, "the page scrolled: {}", scroll_y(&d));
13620 }
13621
13622 #[test]
13628 fn a_stall_mid_drag_does_not_change_the_scroll() {
13629 let (mut d, heard) = driver(PanAxis::Free, true);
13630 let from = Point::new(origin(&d).x + 40.0, origin(&d).y + 120.0);
13631 d.press(from);
13632 d.run(0.02);
13633 for step in 1..=8 {
13634 let f = step as f32 / 8.0;
13635 d.move_to(Point::new(from.x + 4.0 * f, from.y - 120.0 * f));
13636 if step == 5 {
13637 std::thread::sleep(std::time::Duration::from_millis(20));
13638 }
13639 d.run(0.02);
13640 }
13641 d.release(Point::new(from.x + 4.0, from.y - 120.0));
13642 d.run(0.02);
13643 assert!(heard.borrow().is_empty(), "{:?}", heard.borrow());
13644 assert!(scroll_y(&d) > 30.0, "the page scrolled: {}", scroll_y(&d));
13645 }
13646
13647 #[test]
13650 fn a_vertical_drag_takes_it_from_the_scroll() {
13651 let (mut d, heard) = driver(PanAxis::Vertical, true);
13652 let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 120.0);
13653 swipe(&mut d, at, 6.0, -100.0);
13654 let heard = heard.borrow();
13655 assert!(matches!(heard.first(), Some(Msg::Start(_))), "{heard:?}");
13656 let moved = deltas(&heard);
13657 assert_eq!(moved.x, 0.0, "only down");
13658 assert!(moved.y < -40.0, "{moved:?}");
13659 assert_eq!(scroll_y(&d), 0.0, "the page did not move");
13660 }
13661}
13662
13663#[cfg(test)]
13666mod mouse_region_tests {
13667 use super::testing::Driver;
13668 use crate::{Application, Command};
13669 use frus_widgets::{
13670 Container, Cursor, Flex, MouseRegion, Point, Positioned, Stack, TextField, Theme, Widget,
13671 };
13672 use std::cell::RefCell;
13673 use std::rc::Rc;
13674
13675 const W: f32 = 400.0;
13676 const H: f32 = 600.0;
13677
13678 #[derive(Clone, Debug, PartialEq)]
13679 enum Msg {
13680 Enter(&'static str, Point),
13681 Move(&'static str),
13682 Exit(&'static str),
13683 Show,
13684 }
13685
13686 struct Page {
13690 front_opaque: bool,
13691 shown: bool,
13692 heard: Rc<RefCell<Vec<Msg>>>,
13693 }
13694
13695 fn region(name: &'static str, child: impl Widget<Msg> + 'static) -> MouseRegion<Msg> {
13696 MouseRegion::new(child)
13697 .on_enter(move |at| Msg::Enter(name, at))
13698 .on_hover(move |_| Msg::Move(name))
13699 .on_exit(move |_| Msg::Exit(name))
13700 }
13701
13702 impl Application for Page {
13703 type Message = Msg;
13704
13705 fn update(&mut self, message: Msg) -> Command<Msg> {
13706 if message == Msg::Show {
13707 self.shown = true;
13708 } else {
13709 self.heard.borrow_mut().push(message);
13710 }
13711 Command::none()
13712 }
13713
13714 fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
13715 let nested = region(
13717 "outer",
13718 Container::new().width(300.0).height(200.0).child(
13719 region("inner", Container::new().width(100.0).height(50.0))
13720 .cursor(Cursor::Pointer),
13721 ),
13722 )
13723 .cursor(Cursor::Grab);
13724 let stack = Stack::new()
13726 .width(300.0)
13727 .height(200.0)
13728 .layer(region("back", Container::new().width(300.0).height(200.0)))
13729 .layer(
13730 Positioned::new(
13731 region("front", Container::new().width(100.0).height(100.0))
13732 .opaque(self.front_opaque),
13733 )
13734 .left(50.0)
13735 .top(50.0),
13736 );
13737 let field = region("field", TextField::new("").width(300.0)).cursor(Cursor::Pointer);
13739 let appearing: Box<dyn Widget<Msg>> = if self.shown {
13741 Box::new(region("late", Container::new().width(300.0).height(100.0)))
13742 } else {
13743 Box::new(Container::new().width(300.0).height(100.0))
13744 };
13745 Box::new(
13746 Container::new().width(W).height(H).child(
13747 Flex::column()
13748 .child(nested)
13749 .child(stack)
13750 .child(Container::new().height(100.0).child(field))
13751 .child(appearing),
13752 ),
13753 )
13754 }
13755 }
13756
13757 fn driver(front_opaque: bool) -> (Driver<Page>, Rc<RefCell<Vec<Msg>>>) {
13758 let heard = Rc::new(RefCell::new(Vec::new()));
13759 let page = Page {
13760 front_opaque,
13761 shown: false,
13762 heard: heard.clone(),
13763 };
13764 let mut driver = Driver::new(page, W, H);
13765 driver.run(0.2);
13766 (driver, heard)
13767 }
13768
13769 fn take(heard: &Rc<RefCell<Vec<Msg>>>) -> Vec<Msg> {
13770 std::mem::take(&mut *heard.borrow_mut())
13771 }
13772
13773 fn mouse(d: &mut Driver<Page>, x: f32, y: f32) {
13774 d.move_mouse(Point::new(x, y));
13775 d.run(0.02);
13776 }
13777
13778 #[test]
13782 fn nested_regions_are_entered_and_left_together() {
13783 let (mut d, heard) = driver(true);
13784 mouse(&mut d, 20.0, 10.0);
13785 assert_eq!(
13786 take(&heard),
13787 vec![
13788 Msg::Enter("outer", Point::new(20.0, 10.0)),
13789 Msg::Enter("inner", Point::new(20.0, 10.0)),
13790 ]
13791 );
13792 mouse(&mut d, 30.0, 12.0);
13793 let moved = take(&heard);
13794 assert!(moved.contains(&Msg::Move("inner")) && moved.contains(&Msg::Move("outer")));
13795 mouse(&mut d, 200.0, 150.0);
13796 assert_eq!(take(&heard), vec![Msg::Exit("inner"), Msg::Move("outer")]);
13797 mouse(&mut d, 350.0, 150.0);
13798 assert_eq!(take(&heard), vec![Msg::Exit("outer")]);
13799 }
13800
13801 #[test]
13804 fn an_opaque_region_hides_the_one_behind() {
13805 let (mut d, heard) = driver(true);
13806 mouse(&mut d, 80.0, 280.0);
13807 let entered: Vec<_> = take(&heard);
13808 assert!(
13809 entered.iter().any(|m| matches!(m, Msg::Enter("front", _))),
13810 "{entered:?}"
13811 );
13812 assert!(
13813 !entered.iter().any(|m| matches!(m, Msg::Enter("back", _))),
13814 "{entered:?}"
13815 );
13816 mouse(&mut d, 250.0, 280.0);
13818 let after = take(&heard);
13819 assert!(after.contains(&Msg::Exit("front")), "{after:?}");
13820 assert!(
13821 after.iter().any(|m| matches!(m, Msg::Enter("back", _))),
13822 "{after:?}"
13823 );
13824
13825 let (mut d, heard) = driver(false);
13826 mouse(&mut d, 80.0, 280.0);
13827 let entered = take(&heard);
13828 assert!(
13829 entered.iter().any(|m| matches!(m, Msg::Enter("front", _))),
13830 "{entered:?}"
13831 );
13832 assert!(
13833 entered.iter().any(|m| matches!(m, Msg::Enter("back", _))),
13834 "{entered:?}"
13835 );
13836 }
13837
13838 #[test]
13841 fn no_moves_while_a_button_is_held() {
13842 let (mut d, heard) = driver(true);
13843 mouse(&mut d, 350.0, 150.0);
13844 take(&heard);
13845 d.press_mouse(Point::new(350.0, 150.0));
13846 d.run(0.02);
13847 d.move_mouse(Point::new(40.0, 20.0));
13848 d.run(0.02);
13849 d.move_mouse(Point::new(45.0, 22.0));
13850 d.run(0.02);
13851 let heard_now = take(&heard);
13852 assert!(
13853 heard_now
13854 .iter()
13855 .any(|m| matches!(m, Msg::Enter("inner", _))),
13856 "{heard_now:?}"
13857 );
13858 assert!(
13859 !heard_now.iter().any(|m| matches!(m, Msg::Move(_))),
13860 "{heard_now:?}"
13861 );
13862 d.release_mouse(Point::new(45.0, 22.0));
13863 d.run(0.02);
13864 }
13865
13866 #[test]
13868 fn a_finger_enters_while_it_touches() {
13869 let (mut d, heard) = driver(true);
13870 d.press(Point::new(200.0, 150.0));
13871 d.run(0.02);
13872 assert_eq!(
13873 take(&heard),
13874 vec![Msg::Enter("outer", Point::new(200.0, 150.0))]
13875 );
13876 d.release(Point::new(200.0, 150.0));
13877 d.run(0.02);
13878 assert_eq!(take(&heard), vec![Msg::Exit("outer")]);
13879 }
13880
13881 #[test]
13884 fn a_region_appearing_under_the_pointer_is_entered() {
13885 let (mut d, heard) = driver(true);
13886 mouse(&mut d, 150.0, 550.0);
13887 assert!(take(&heard).is_empty());
13888 d.update(Msg::Show);
13889 d.run(0.05);
13890 let heard_now = take(&heard);
13891 assert!(
13892 heard_now.iter().any(|m| matches!(m, Msg::Enter("late", _))),
13893 "{heard_now:?}"
13894 );
13895 }
13896
13897 #[test]
13900 fn the_innermost_region_sets_the_cursor() {
13901 let (mut d, _) = driver(true);
13902 mouse(&mut d, 20.0, 10.0);
13903 assert_eq!(d.cursor_shown(), Cursor::Pointer, "the inner region's");
13904 mouse(&mut d, 200.0, 150.0);
13905 assert_eq!(d.cursor_shown(), Cursor::Grab, "the outer region's");
13906 mouse(&mut d, 350.0, 150.0);
13907 assert_eq!(d.cursor_shown(), Cursor::Default, "no region");
13908 mouse(&mut d, 100.0, 420.0);
13909 assert_eq!(d.cursor_shown(), Cursor::Text, "the field's own");
13910 }
13911}
13912
13913#[cfg(test)]
13916mod listener_tests {
13917 use super::testing::Driver;
13918 use crate::{Application, Command};
13919 use frus_widgets::{
13920 Button, Container, Flex, Listener, ListenerEvent, ListenerEventKind as Raw, Point,
13921 SingleChildScrollView, Theme, Widget,
13922 };
13923 use std::cell::RefCell;
13924 use std::rc::Rc;
13925
13926 const W: f32 = 400.0;
13927 const H: f32 = 400.0;
13928
13929 #[derive(Clone, Debug, PartialEq)]
13930 enum Msg {
13931 Heard(&'static str, Raw, Point),
13932 Pressed,
13933 }
13934
13935 struct Page {
13938 heard: Rc<RefCell<Vec<Msg>>>,
13939 }
13940
13941 fn listen(name: &'static str) -> impl Fn(ListenerEvent) -> Option<Msg> {
13942 move |event| Some(Msg::Heard(name, event.kind, event.local))
13943 }
13944
13945 impl Application for Page {
13946 type Message = Msg;
13947
13948 fn update(&mut self, message: Msg) -> Command<Msg> {
13949 self.heard.borrow_mut().push(message);
13950 Command::none()
13951 }
13952
13953 fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
13954 let inner = Listener::new(
13955 Container::new()
13956 .width(100.0)
13957 .height(100.0)
13958 .child(Button::new("Press").on_press(Msg::Pressed)),
13959 listen("inner"),
13960 );
13961 let outer = Listener::new(
13962 Container::new().width(300.0).height(300.0).child(inner),
13963 listen("outer"),
13964 );
13965 Box::new(
13966 SingleChildScrollView::new().width(W).height(H).child(
13967 Flex::column()
13968 .child(Container::new().width(300.0).height(40.0))
13969 .child(outer)
13970 .child(Container::new().width(300.0).height(1000.0)),
13971 ),
13972 )
13973 }
13974 }
13975
13976 fn driver() -> (Driver<Page>, Rc<RefCell<Vec<Msg>>>) {
13977 let heard = Rc::new(RefCell::new(Vec::new()));
13978 let mut d = Driver::new(
13979 Page {
13980 heard: heard.clone(),
13981 },
13982 W,
13983 H,
13984 );
13985 d.run(0.2);
13986 (d, heard)
13987 }
13988
13989 fn take(heard: &Rc<RefCell<Vec<Msg>>>) -> Vec<Msg> {
13990 std::mem::take(&mut *heard.borrow_mut())
13991 }
13992
13993 fn of(heard: &[Msg], name: &str) -> Vec<(Raw, Point)> {
13995 heard
13996 .iter()
13997 .filter_map(|m| match m {
13998 Msg::Heard(n, kind, at) if *n == name => Some((*kind, *at)),
13999 _ => None,
14000 })
14001 .collect()
14002 }
14003
14004 fn scroll_y(d: &Driver<Page>) -> f32 {
14005 d.offsets().first().map_or(0.0, |(_, offset)| offset.1)
14006 }
14007
14008 #[test]
14011 fn a_press_is_heard_innermost_first() {
14012 let (mut d, heard) = driver();
14013 d.press(Point::new(250.0, 90.0));
14014 d.run(0.02);
14015 let h = take(&heard);
14016 assert_eq!(of(&h, "outer"), vec![(Raw::Down, Point::new(250.0, 50.0))]);
14017 assert!(of(&h, "inner").is_empty(), "outside the inner one: {h:?}");
14018 d.release(Point::new(250.0, 90.0));
14019 d.run(0.02);
14020 take(&heard);
14021
14022 d.press(Point::new(60.0, 80.0));
14023 d.run(0.02);
14024 let h = take(&heard);
14025 let order: Vec<_> = h
14026 .iter()
14027 .filter_map(|m| match m {
14028 Msg::Heard(n, Raw::Down, _) => Some(*n),
14029 _ => None,
14030 })
14031 .collect();
14032 assert_eq!(order, vec!["inner", "outer"], "{h:?}");
14033 assert_eq!(of(&h, "inner"), vec![(Raw::Down, Point::new(60.0, 40.0))]);
14034 d.release(Point::new(60.0, 80.0));
14035 d.run(0.02);
14036 }
14037
14038 #[test]
14041 fn the_pointer_is_followed_out_and_the_page_still_scrolls() {
14042 let (mut d, heard) = driver();
14043 let from = Point::new(250.0, 300.0);
14044 d.press(from);
14045 d.run(0.02);
14046 for step in 1..=8 {
14047 d.move_to(Point::new(250.0, 300.0 - 35.0 * step as f32));
14048 d.run(0.02);
14049 }
14050 d.release(Point::new(250.0, 20.0));
14051 d.run(0.02);
14052 let h = take(&heard);
14053 let outer = of(&h, "outer");
14054 assert_eq!(outer.first().map(|e| e.0), Some(Raw::Down), "{outer:?}");
14055 assert_eq!(outer.last().map(|e| e.0), Some(Raw::Up), "{outer:?}");
14056 assert!(
14057 outer.iter().filter(|e| e.0 == Raw::Move).count() >= 7,
14058 "{outer:?}"
14059 );
14060 assert!(scroll_y(&d) > 50.0, "the page scrolled: {}", scroll_y(&d));
14061 }
14062
14063 #[test]
14066 fn a_pointer_that_leaves_is_still_heard() {
14067 let (mut d, heard) = driver();
14068 d.press_mouse(Point::new(60.0, 80.0));
14069 d.run(0.02);
14070 d.move_mouse(Point::new(390.0, 390.0));
14071 d.run(0.02);
14072 d.release_mouse(Point::new(390.0, 390.0));
14073 d.run(0.02);
14074 let h = take(&heard);
14075 let inner = of(&h, "inner");
14076 assert_eq!(
14077 inner,
14078 vec![
14079 (Raw::Down, Point::new(60.0, 40.0)),
14080 (Raw::Move, Point::new(390.0, 350.0)),
14081 (Raw::Up, Point::new(390.0, 350.0)),
14082 ],
14083 "{h:?}"
14084 );
14085 assert_eq!(of(&h, "outer").len(), 3, "{h:?}");
14086 }
14087
14088 #[test]
14090 fn a_button_inside_keeps_its_tap() {
14091 let (mut d, heard) = driver();
14092 let at = Point::new(30.0, 55.0);
14093 d.press(at);
14094 d.release(at);
14095 d.run(0.05);
14096 let h = take(&heard);
14097 assert!(h.contains(&Msg::Pressed), "{h:?}");
14098 let inner = of(&h, "inner");
14099 assert_eq!(
14100 inner.iter().map(|e| e.0).collect::<Vec<_>>(),
14101 vec![Raw::Down, Raw::Up]
14102 );
14103 }
14104
14105 #[test]
14108 fn a_mouse_hovering_is_heard() {
14109 let (mut d, heard) = driver();
14110 d.move_mouse(Point::new(250.0, 90.0));
14111 d.run(0.02);
14112 let h = take(&heard);
14113 assert_eq!(of(&h, "outer"), vec![(Raw::Hover, Point::new(250.0, 50.0))]);
14114 assert!(of(&h, "inner").is_empty());
14115 d.move_mouse(Point::new(390.0, 390.0));
14116 d.run(0.02);
14117 assert!(take(&heard).is_empty(), "outside both");
14118 }
14119}
14120
14121#[cfg(test)]
14122mod menu_keyboard_tests {
14123 use super::testing::Driver;
14124 use crate::{Application, Command};
14125 use frus_widgets::{
14126 KeyStroke, MenuBar, MenuItem, MenuPath, Point, ShortcutKey, SubmenuButton, Theme, Widget,
14127 };
14128
14129 const W: f32 = 600.0;
14130 const H: f32 = 400.0;
14131
14132 #[derive(Clone, Debug, PartialEq)]
14133 enum Msg {
14134 Menu(MenuPath),
14135 Picked(&'static str),
14136 }
14137
14138 #[derive(Default)]
14140 struct Editor {
14141 menu: MenuPath,
14142 picked: Option<&'static str>,
14144 }
14145
14146 impl Application for Editor {
14147 type Message = Msg;
14148
14149 fn update(&mut self, message: Msg) -> Command<Msg> {
14150 match message {
14151 Msg::Menu(path) => self.menu = path,
14152 Msg::Picked(what) => {
14153 self.picked = Some(what);
14154 self.menu = MenuPath::closed();
14155 }
14156 }
14157 Command::none()
14158 }
14159
14160 fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
14161 Box::new(
14162 MenuBar::new(&self.menu, Msg::Menu).menu(
14163 SubmenuButton::new("File")
14164 .item(MenuItem::new("New", Msg::Picked("new")))
14165 .submenu(
14166 SubmenuButton::new("Open recent")
14167 .item(MenuItem::new("notes.txt", Msg::Picked("notes")))
14168 .item(MenuItem::new("todo.md", Msg::Picked("todo"))),
14169 )
14170 .item(MenuItem::new("Quit", Msg::Picked("quit"))),
14171 ),
14172 )
14173 }
14174 }
14175
14176 fn row(d: &Driver<Editor>, label: &str) -> Option<frus_widgets::WidgetId> {
14178 let (_, rect) = d.texts().into_iter().find(|(text, _)| text == label)?;
14179 d.hit(Point::new(rect.x + 4.0, rect.y + 4.0))
14180 }
14181
14182 #[test]
14185 fn opening_a_menu_focuses_its_first_row_and_escape_goes_back_one_level() {
14186 let mut d = Driver::new(Editor::default(), W, H);
14187 d.run(0.1);
14188 assert_eq!(d.focused(), None);
14189
14190 d.update(Msg::Menu(MenuPath::from_indices(vec![0])));
14191 d.run(0.1);
14192 let new = row(&d, "New").expect("the menu is open");
14193 assert_eq!(d.focused(), Some(new), "the first row has the focus");
14194
14195 d.update(Msg::Menu(MenuPath::from_indices(vec![0, 1])));
14196 d.run(0.1);
14197 let notes = row(&d, "notes.txt").expect("the submenu is open");
14198 assert_eq!(d.focused(), Some(notes), "the submenu's first row has it");
14199
14200 d.escape();
14201 d.run(0.1);
14202 assert_eq!(
14203 d.app().menu,
14204 MenuPath::from_indices(vec![0]),
14205 "one level closed"
14206 );
14207 assert!(row(&d, "notes.txt").is_none());
14208 assert_eq!(d.focused(), Some(new), "and the focus is back in the menu");
14209
14210 d.escape();
14211 d.run(0.1);
14212 assert!(!d.app().menu.is_open(), "then the menu");
14213 }
14214
14215 #[test]
14219 fn the_menu_is_worked_from_the_keyboard() {
14220 let key = |d: &mut Driver<Editor>, k: ShortcutKey| {
14221 d.key(KeyStroke::new(k));
14222 d.run(0.1);
14223 };
14224 let mut d = Driver::new(Editor::default(), W, H);
14225 d.run(0.1);
14226 d.update(Msg::Menu(MenuPath::from_indices(vec![0])));
14227 d.run(0.1);
14228 let (new, recent) = (row(&d, "New").unwrap(), row(&d, "Open recent").unwrap());
14229 assert_eq!(d.focused(), Some(new));
14230
14231 key(&mut d, ShortcutKey::Down);
14232 assert_eq!(d.focused(), Some(recent), "down to the next row");
14233 key(&mut d, ShortcutKey::Right);
14234 assert_eq!(
14235 d.app().menu,
14236 MenuPath::from_indices(vec![0, 1]),
14237 "right opens it"
14238 );
14239 assert_eq!(
14240 d.focused(),
14241 row(&d, "notes.txt"),
14242 "and its first row has the focus"
14243 );
14244 key(&mut d, ShortcutKey::Down);
14245 assert_eq!(d.focused(), row(&d, "todo.md"));
14246 key(&mut d, ShortcutKey::Left);
14247 assert_eq!(
14248 d.app().menu,
14249 MenuPath::from_indices(vec![0]),
14250 "left closes it"
14251 );
14252 assert_eq!(
14253 d.focused(),
14254 Some(recent),
14255 "and the focus is back on its row"
14256 );
14257 key(&mut d, ShortcutKey::Up);
14258 assert_eq!(d.focused(), Some(new));
14259 key(&mut d, ShortcutKey::Enter);
14260 assert_eq!(d.app().picked, Some("new"), "Enter picks the row");
14261 assert!(!d.app().menu.is_open(), "and the menus close");
14262 }
14263
14264 #[test]
14267 fn a_stroke_holds_its_modifiers_for_the_key_alone() {
14268 let mut d = Driver::new(Editor::default(), W, H);
14269 d.run(0.1);
14270 d.key(KeyStroke::new(ShortcutKey::Char('a')).ctrl());
14271 assert!(!d.shell_modifiers().0, "ctrl let go after the stroke");
14272 }
14273
14274 #[test]
14279 fn closing_a_menu_hands_the_focus_back_to_its_word() {
14280 let key = |d: &mut Driver<Editor>, k: ShortcutKey| {
14281 d.key(KeyStroke::new(k));
14282 d.run(0.1);
14283 };
14284 let mut d = Driver::new(Editor::default(), W, H);
14285 d.run(0.1);
14286 key(&mut d, ShortcutKey::Tab);
14287 let file = d.focused().expect("Tab reached the bar");
14288 key(&mut d, ShortcutKey::Enter);
14289 assert!(d.app().menu.is_open(), "Enter opened the menu");
14290 assert_eq!(d.focused(), row(&d, "New"), "and the focus went into it");
14291 key(&mut d, ShortcutKey::Escape);
14292 assert!(!d.app().menu.is_open(), "Escape closed it");
14293 assert_eq!(d.focused(), Some(file), "and the focus is back on its word");
14294 }
14295
14296 #[test]
14302 fn a_submenu_closing_hands_the_focus_back_to_any_row() {
14303 let key = |d: &mut Driver<Editor>, k: ShortcutKey| {
14304 d.key(KeyStroke::new(k));
14305 d.run(0.1);
14306 };
14307 let mut d = Driver::new(Editor::default(), W, H);
14308 d.run(0.1);
14309 d.update(Msg::Menu(MenuPath::from_indices(vec![0])));
14310 d.run(0.1);
14311 key(&mut d, ShortcutKey::Down);
14312 key(&mut d, ShortcutKey::Down);
14313 let quit = row(&d, "Quit");
14314 assert_eq!(d.focused(), quit, "down twice reaches Quit");
14315 d.update(Msg::Menu(MenuPath::from_indices(vec![0, 1])));
14317 d.run(0.1);
14318 assert_eq!(d.focused(), row(&d, "notes.txt"));
14319 key(&mut d, ShortcutKey::Escape);
14320 assert_eq!(d.app().menu, MenuPath::from_indices(vec![0]));
14321 assert_eq!(d.focused(), quit, "the focus is back on Quit");
14322 }
14323}
14324
14325#[cfg(test)]
14329mod right_click_bar_tests {
14330 use super::testing::Driver;
14331 use crate::{Application, Command};
14332 use frus_widgets::{Point, TextField, Theme, Widget};
14333
14334 struct Form;
14335
14336 impl Application for Form {
14337 type Message = ();
14338
14339 fn update(&mut self, _message: ()) -> Command<()> {
14340 Command::none()
14341 }
14342
14343 fn view(&self, _theme: &Theme) -> Box<dyn Widget<()>> {
14344 Box::new(TextField::new("some words in a field"))
14345 }
14346 }
14347
14348 #[test]
14349 fn a_right_click_on_a_field_opens_its_bar() {
14350 let mut d = Driver::new(Form, 500.0, 300.0);
14351 d.run(0.1);
14352 let shown = |d: &Driver<Form>, label: &str| d.texts().iter().any(|(t, _)| t == label);
14353 assert!(!shown(&d, "Select all"), "no bar before the click");
14354 let (_, words) = d
14355 .texts()
14356 .into_iter()
14357 .find(|(t, _)| t.contains("some words"))
14358 .expect("the field's words");
14359 d.secondary_click(Point::new(words.x + 20.0, words.y + words.height * 0.5));
14360 d.run(0.1);
14361 assert!(shown(&d, "Select all"), "the bar is open: {:?}", d.texts());
14365 assert!(!shown(&d, "Copy") && !shown(&d, "Cut"));
14366 assert!(
14367 !shown(&d, "Paste"),
14368 "an empty clipboard offers nothing to paste"
14369 );
14370 }
14371
14372 struct DesktopForm;
14374
14375 impl Application for DesktopForm {
14376 type Message = ();
14377
14378 fn update(&mut self, _message: ()) -> Command<()> {
14379 Command::none()
14380 }
14381
14382 fn view(&self, _theme: &Theme) -> Box<dyn Widget<()>> {
14383 Box::new(TextField::new("some words in a field"))
14384 }
14385
14386 fn theme(&self) -> Theme {
14387 Theme::default().with_platform(frus_widgets::TargetPlatform::Linux)
14388 }
14389 }
14390
14391 #[test]
14395 fn a_right_click_opens_the_menu_at_the_pointer() {
14396 let mut d = Driver::new(DesktopForm, 500.0, 300.0);
14397 d.run(0.1);
14398 let (_, words) = d
14399 .texts()
14400 .into_iter()
14401 .find(|(t, _)| t.contains("some words"))
14402 .expect("the field's words");
14403 let click = Point::new(words.x + 20.0, words.y + words.height * 0.5);
14404 d.secondary_click(click);
14405 d.run(0.1);
14406 let (_, row) = d
14407 .texts()
14408 .into_iter()
14409 .find(|(t, _)| t == "Select all")
14410 .expect("the menu is open");
14411 assert_eq!(row.x, click.x + 20.0, "the menu starts at the pointer");
14412 assert!(
14413 row.y >= click.y && row.y < click.y + 36.0,
14414 "its first row is on the pointer's line: {row:?} {click:?}"
14415 );
14416 }
14417
14418 #[test]
14423 fn a_desktop_s_select_all_puts_the_menu_away() {
14424 assert!(frus_widgets::default_target_platform().is_desktop());
14425 let mut d = Driver::new(DesktopForm, 500.0, 300.0);
14426 d.run(0.1);
14427 let shown = |d: &Driver<DesktopForm>, label: &str| {
14428 d.texts()
14429 .into_iter()
14430 .find(|(t, _)| t == label)
14431 .map(|(_, r)| r)
14432 };
14433 let words = shown(&d, "some words in a field").expect("the field's words");
14434 let click = Point::new(words.x + 20.0, words.y + words.height * 0.5);
14435 d.secondary_click(click);
14436 d.run(0.1);
14437 let row = shown(&d, "Select all").expect("the menu is open");
14438 let row = Point::new(row.x + 10.0, row.y + row.height * 0.5);
14439 d.press(row);
14440 d.release(row);
14441 d.run(0.1);
14442 assert!(shown(&d, "Select all").is_none(), "the menu is gone");
14443 assert!(shown(&d, "Copy").is_none(), "all of it: {:?}", d.texts());
14444 d.secondary_click(click);
14445 d.run(0.1);
14446 assert!(shown(&d, "Copy").is_some(), "the text is still selected");
14447 }
14448}
14449
14450#[cfg(test)]
14451mod scroll_behavior_tests {
14452 use super::testing::Driver;
14453 use crate::{Application, Command};
14454 use frus_widgets::{
14455 Container, Point, ScrollBehavior, ScrollConfiguration, SingleChildScrollView,
14456 TargetPlatform, Theme, VelocityStrategy, Widget,
14457 };
14458
14459 struct Page;
14462
14463 impl Application for Page {
14464 type Message = ();
14465
14466 fn update(&mut self, _message: ()) -> Command<()> {
14467 Command::none()
14468 }
14469
14470 fn view(&self, _theme: &Theme) -> Box<dyn Widget<()>> {
14471 Box::new(ScrollConfiguration::new(
14472 ScrollBehavior::material().with_platform(TargetPlatform::Ios),
14473 SingleChildScrollView::new()
14474 .width(300.0)
14475 .height(400.0)
14476 .child(Container::new().width(300.0).height(2000.0)),
14477 ))
14478 }
14479
14480 fn theme(&self) -> Theme {
14481 Theme::default().with_platform(TargetPlatform::Android)
14482 }
14483 }
14484
14485 #[test]
14489 fn a_drag_on_an_area_reads_its_fling_the_area_s_way() {
14490 let mut d = Driver::new(Page, 300.0, 400.0);
14491 d.run(0.2);
14492 d.press(Point::new(150.0, 200.0));
14493 assert_eq!(
14494 d.fling_strategy(),
14495 VelocityStrategy::RecentAverage(frus_widgets::BOUNCING_FLING_WEIGHTS)
14496 );
14497 d.release(Point::new(150.0, 200.0));
14498 }
14499}
14500
14501#[cfg(test)]
14504mod switch_drag_tests {
14505 use super::testing::Driver;
14506 use crate::{Application, Command};
14507 use frus_widgets::{Container, Point, Rect, Switch, Theme, Widget};
14508
14509 const W: f32 = 300.0;
14510 const H: f32 = 200.0;
14511
14512 #[derive(Clone, Debug, PartialEq)]
14513 enum Msg {
14514 Set(bool),
14515 }
14516
14517 #[derive(Default)]
14518 struct Page {
14519 on: bool,
14520 heard: Vec<Msg>,
14521 }
14522
14523 impl Application for Page {
14524 type Message = Msg;
14525
14526 fn update(&mut self, message: Msg) -> Command<Msg> {
14527 let Msg::Set(on) = message;
14528 self.on = on;
14529 self.heard.push(message);
14530 Command::none()
14531 }
14532
14533 fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
14534 Box::new(
14535 Container::new()
14536 .width(W)
14537 .height(H)
14538 .child(Switch::new(self.on).on_toggle(Msg::Set)),
14539 )
14540 }
14541 }
14542
14543 fn driver() -> Driver<Page> {
14544 let mut d = Driver::new(Page::default(), W, H);
14545 d.run(0.2);
14546 d
14547 }
14548
14549 fn track_and_thumb(d: &Driver<Page>) -> (Rect, Rect) {
14551 let (ui, _) = d.frame_parts().expect("a frame");
14552 let rects: Vec<Rect> = ui
14553 .scene()
14554 .primitives()
14555 .iter()
14556 .filter_map(|p| match p {
14557 frus_widgets::Primitive::Rect { rect, .. } => Some(*rect),
14558 _ => None,
14559 })
14560 .collect();
14561 let track = *rects
14562 .iter()
14563 .find(|r| r.width == 52.0 && r.height == 32.0)
14564 .expect("the track");
14565 (track, *rects.last().expect("the thumb"))
14566 }
14567
14568 fn along(d: &Driver<Page>) -> f32 {
14570 let (track, thumb) = track_and_thumb(d);
14571 (thumb.x + thumb.width * 0.5 - track.x - 16.0) / 20.0
14572 }
14573
14574 fn drag(d: &mut Driver<Page>, dx: f32, lift: bool) {
14576 let (track, _) = track_and_thumb(d);
14577 let from = Point::new(track.x + 16.0, track.y + 16.0);
14578 d.press(from);
14579 d.run(0.02);
14580 for step in 1..=8 {
14581 d.move_to(Point::new(from.x + dx * step as f32 / 8.0, from.y));
14582 d.run(0.02);
14583 }
14584 if lift {
14585 d.release(Point::new(from.x + dx, from.y));
14586 d.run(0.02);
14587 }
14588 }
14589
14590 #[test]
14593 fn the_thumb_follows_the_finger() {
14594 let mut d = driver();
14595 assert!(along(&d).abs() < 1e-3, "off at rest");
14596 drag(&mut d, 40.0, false);
14597 let now = along(&d);
14598 assert!(now > 0.3 && now <= 1.0, "moved with the finger: {now}");
14599 assert!(d.app().heard.is_empty(), "nothing said while held");
14600 d.run(0.5);
14602 assert!((along(&d) - now).abs() < 1e-3, "held where it was put");
14603 }
14604
14605 #[test]
14608 fn let_go_past_half_way_it_flips() {
14609 let mut d = driver();
14610 drag(&mut d, 60.0, true);
14611 assert_eq!(d.app().heard, vec![Msg::Set(true)]);
14612 d.run(0.5);
14613 assert!((along(&d) - 1.0).abs() < 1e-3, "settled on");
14614 }
14615
14616 #[test]
14618 fn let_go_short_of_half_way_it_goes_back() {
14619 let mut d = driver();
14620 drag(&mut d, 30.0, false);
14622 let (track, _) = track_and_thumb(&d);
14623 let back = Point::new(track.x + 16.0 + 15.0, track.y + 16.0);
14624 d.move_to(back);
14625 d.run(0.02);
14626 assert!(along(&d) > 0.0, "it had moved");
14627 assert!(along(&d) < 0.5, "but is short of half way: {}", along(&d));
14628 d.release(back);
14629 d.run(0.5);
14630 assert!(d.app().heard.is_empty(), "nothing said");
14631 assert!(along(&d).abs() < 1e-3, "back off: {}", along(&d));
14632 }
14633
14634 #[test]
14636 fn a_tap_still_flips_it() {
14637 let mut d = driver();
14638 let (track, _) = track_and_thumb(&d);
14639 let at = Point::new(track.x + 16.0, track.y + 16.0);
14640 d.press(at);
14641 d.release(at);
14642 d.run(0.5);
14643 assert_eq!(d.app().heard, vec![Msg::Set(true)]);
14644 assert!((along(&d) - 1.0).abs() < 1e-3);
14645 }
14646}
14647
14648#[cfg(test)]
14652mod range_slider_drag_tests {
14653 use super::testing::Driver;
14654 use crate::{Application, Command};
14655 use frus_widgets::{Container, Point, RangeSlider, Rect, Theme, Widget};
14656
14657 #[derive(Clone, Debug, PartialEq)]
14658 enum Msg {
14659 Range(f32, f32),
14660 Start(f32, f32),
14661 End(f32, f32),
14662 }
14663
14664 struct Page {
14665 values: (f32, f32),
14666 heard: Vec<Msg>,
14667 }
14668
14669 impl Application for Page {
14670 type Message = Msg;
14671
14672 fn update(&mut self, message: Msg) -> Command<Msg> {
14673 if let Msg::Range(s, e) | Msg::End(s, e) = message {
14674 self.values = (s, e);
14675 }
14676 self.heard.push(message);
14677 Command::none()
14678 }
14679
14680 fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
14681 let (start, end) = self.values;
14682 Box::new(
14683 Container::new().width(300.0).height(200.0).child(
14684 RangeSlider::new(start, end)
14685 .width(248.0)
14686 .divisions(20)
14687 .on_change(Msg::Range)
14688 .on_change_start(Msg::Start)
14689 .on_change_end(Msg::End),
14690 ),
14691 )
14692 }
14693 }
14694
14695 fn driver(start: f32, end: f32) -> Driver<Page> {
14696 let page = Page {
14697 values: (start, end),
14698 heard: Vec::new(),
14699 };
14700 let mut d = Driver::new(page, 300.0, 200.0);
14701 d.run(0.2);
14702 d
14703 }
14704
14705 fn thumbs(d: &Driver<Page>) -> Vec<Rect> {
14707 let (ui, _) = d.frame_parts().expect("a frame");
14708 ui.scene()
14709 .primitives()
14710 .iter()
14711 .filter_map(|p| match p {
14712 frus_widgets::Primitive::Rect { rect, blur, .. }
14713 if *blur == 0.0 && rect.width == 20.0 && rect.height == 20.0 =>
14714 {
14715 Some(*rect)
14716 }
14717 _ => None,
14718 })
14719 .collect()
14720 }
14721
14722 fn centre(r: Rect) -> Point {
14723 Point::new(r.x + r.width * 0.5, r.y + r.height * 0.5)
14724 }
14725
14726 fn drag(d: &mut Driver<Page>, from: Point, dx: f32) {
14728 d.press(from);
14729 d.run(0.02);
14730 for step in 1..=4 {
14731 d.move_to(Point::new(from.x + dx * step as f32 / 4.0, from.y));
14732 d.run(0.02);
14733 }
14734 d.release(Point::new(from.x + dx, from.y));
14735 d.run(0.02);
14736 }
14737
14738 fn close(a: (f32, f32), b: (f32, f32)) -> bool {
14739 (a.0 - b.0).abs() < 1e-3 && (a.1 - b.1).abs() < 1e-3
14740 }
14741
14742 #[test]
14746 fn a_drag_carries_the_nearer_thumb() {
14747 let mut d = driver(0.25, 0.75);
14748 let t = thumbs(&d);
14749 assert_eq!(t.len(), 2);
14750 let start = centre(t[0]).x.min(centre(t[1]).x);
14751 drag(&mut d, Point::new(start + 4.0, centre(t[0]).y), 34.0);
14754 let heard = &d.app().heard;
14755 assert_eq!(heard.first(), Some(&Msg::Start(0.25, 0.75)), "{heard:?}");
14756 let Some(Msg::End(s, e)) = heard.last().cloned() else {
14757 panic!("no end: {heard:?}");
14758 };
14759 assert!(close((s, e), (0.45, 0.75)), "({s}, {e})");
14760 assert!(close(d.app().values, (0.45, 0.75)));
14761 let order = thumbs(&d);
14763 assert!(order[1].x < order[0].x, "the start on top: {order:?}");
14764 }
14765
14766 #[test]
14769 fn where_the_thumbs_meet_the_movement_decides() {
14770 let mut d = driver(0.5, 0.5);
14771 let at = centre(thumbs(&d)[0]);
14772 drag(&mut d, at, -40.0);
14773 assert!(close(d.app().values, (0.3, 0.5)), "{:?}", d.app().values);
14774
14775 let mut d = driver(0.5, 0.5);
14776 drag(&mut d, at, 40.0);
14777 assert!(close(d.app().values, (0.5, 0.7)), "{:?}", d.app().values);
14778
14779 let mut d = driver(0.3, 0.5);
14781 let start = centre(thumbs(&d)[0]);
14782 drag(&mut d, start, 120.0);
14783 assert!(close(d.app().values, (0.5, 0.5)), "{:?}", d.app().values);
14784 }
14785}
14786
14787#[cfg(test)]
14790mod focus_change_tests {
14791 use super::testing::Driver;
14792 use crate::{Application, Command};
14793 use frus_widgets::{Flex, Focus, Point, Rect, TextField, Theme, Widget};
14794
14795 #[derive(Clone, Debug, PartialEq)]
14796 enum Msg {
14797 Group(bool),
14798 Outside,
14799 Typed,
14800 }
14801
14802 #[derive(Default)]
14803 struct Page {
14804 heard: Vec<Msg>,
14805 }
14806
14807 impl Application for Page {
14808 type Message = Msg;
14809
14810 fn update(&mut self, message: Msg) -> Command<Msg> {
14811 if message != Msg::Typed {
14812 self.heard.push(message);
14813 }
14814 Command::none()
14815 }
14816
14817 fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
14818 Box::new(
14819 Flex::column()
14820 .width(300.0)
14821 .height(400.0)
14822 .gap(40.0)
14823 .child(
14824 Focus::new(
14825 TextField::new("a")
14826 .on_input(|_| Msg::Typed)
14827 .on_tap_outside(Msg::Outside),
14828 )
14829 .skip_traversal()
14830 .on_focus_change(Msg::Group),
14831 )
14832 .child(TextField::new("b").on_input(|_| Msg::Typed)),
14833 )
14834 }
14835 }
14836
14837 fn fields(d: &Driver<Page>) -> (Rect, Rect) {
14839 let (ui, _) = d.frame_parts().expect("a frame");
14840 let rects: Vec<Rect> = ui
14841 .focusable_ids()
14842 .filter_map(|id| ui.widget_rect(id))
14843 .filter(|r| r.height < 100.0)
14844 .collect();
14845 (rects[0], rects[rects.len() - 1])
14846 }
14847
14848 fn centre(r: Rect) -> Point {
14849 Point::new(r.x + r.width / 2.0, r.y + r.height / 2.0)
14850 }
14851
14852 fn tap(d: &mut Driver<Page>, at: Point) {
14853 d.press(at);
14854 d.release(at);
14855 d.run(0.05);
14856 }
14857
14858 #[test]
14861 fn the_shell_reports_the_focus_moving() {
14862 let mut d = Driver::new(Page::default(), 300.0, 400.0);
14863 d.run(0.2);
14864 let (a, b) = fields(&d);
14865 tap(&mut d, centre(a));
14866 assert_eq!(d.app().heard, vec![Msg::Group(true)], "arrived");
14867 tap(&mut d, centre(a));
14868 assert_eq!(
14869 d.app().heard,
14870 vec![Msg::Group(true)],
14871 "a press inside says nothing"
14872 );
14873 tap(&mut d, centre(b));
14874 assert_eq!(
14875 d.app().heard,
14876 vec![Msg::Group(true), Msg::Outside, Msg::Group(false)],
14877 "outside, then left"
14878 );
14879 tap(&mut d, Point::new(150.0, 390.0));
14880 assert_eq!(
14881 d.app().heard.len(),
14882 3,
14883 "the other field holds no `on_tap_outside`: {:?}",
14884 d.app().heard
14885 );
14886 }
14887}
14888
14889#[cfg(test)]
14890mod file_drop_tests {
14891 use std::path::PathBuf;
14892
14893 use super::testing::Driver;
14894 use crate::{Application, Command};
14895 use frus_widgets::{Container, DragTarget, Flex, Point, Primitive, Theme, Widget};
14896
14897 const W: f32 = 300.0;
14898 const H: f32 = 300.0;
14899
14900 #[derive(Clone, Debug, PartialEq)]
14901 enum Msg {
14902 Files(&'static str, Vec<PathBuf>),
14903 }
14904
14905 #[derive(Default)]
14906 struct Page {
14907 heard: Vec<Msg>,
14908 }
14909
14910 impl Application for Page {
14911 type Message = Msg;
14912
14913 fn update(&mut self, message: Msg) -> Command<Msg> {
14914 self.heard.push(message);
14915 Command::none()
14916 }
14917
14918 fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
14921 Box::new(
14922 Flex::column()
14923 .width(W)
14924 .height(H)
14925 .child(
14926 DragTarget::new(Container::new().width(W).height(100.0))
14927 .on_files(|paths| Msg::Files("top", paths)),
14928 )
14929 .child(
14930 DragTarget::new(Container::new().width(W).height(100.0))
14931 .on_files(|paths| Msg::Files("text", paths))
14932 .accepts_files(|paths| {
14933 paths
14934 .iter()
14935 .all(|p| p.extension().is_some_and(|e| e == "txt"))
14936 }),
14937 ),
14938 )
14939 }
14940 }
14941
14942 fn driver() -> Driver<Page> {
14943 let mut d = Driver::new(Page::default(), W, H);
14944 d.run(0.2);
14945 d
14946 }
14947
14948 fn paths(names: &[&str]) -> Vec<PathBuf> {
14949 names.iter().map(PathBuf::from).collect()
14950 }
14951
14952 fn highlights(d: &Driver<Page>) -> Vec<frus_widgets::Rect> {
14955 let (ui, _) = d.frame_parts().expect("a frame");
14956 ui.scene()
14957 .primitives()
14958 .iter()
14959 .filter_map(|p| match p {
14960 Primitive::Rect {
14961 rect, border_width, ..
14962 } if *border_width > 0.0 => Some(*rect),
14963 _ => None,
14964 })
14965 .collect()
14966 }
14967
14968 #[test]
14969 fn files_held_over_a_target_light_it_up_and_land_on_it() {
14970 let mut d = driver();
14971 assert!(highlights(&d).is_empty());
14972 let files = paths(&["notes.txt", "photo.png"]);
14973 d.hover_files(&files, Some(Point::new(50.0, 50.0)));
14974 assert!(d.drag_over().is_some());
14975 d.run(0.05);
14976 let lit = highlights(&d);
14977 assert_eq!(lit.len(), 1, "{lit:?}");
14978 assert!(
14979 lit[0].y < 1.0 && (lit[0].height - 100.0).abs() < 0.5,
14980 "{lit:?}"
14981 );
14982
14983 d.drop_files(&files, Some(Point::new(50.0, 50.0)));
14984 assert_eq!(d.app().heard, [Msg::Files("top", files)]);
14985 assert_eq!(d.drag_over(), None);
14986 d.run(0.05);
14987 assert!(
14988 highlights(&d).is_empty(),
14989 "the highlight went with the drop"
14990 );
14991 }
14992
14993 #[test]
14994 fn the_highlight_follows_the_files_and_a_refusing_target_stays_dark() {
14995 let mut d = driver();
14996 let text = paths(&["a.txt"]);
14997 d.hover_files(&text, Some(Point::new(50.0, 50.0)));
14998 let top = d.drag_over().expect("over the top target");
14999 d.move_files(Some(Point::new(50.0, 150.0)));
15000 let second = d.drag_over().expect("over the second target");
15001 assert_ne!(top, second);
15002 d.move_files(Some(Point::new(50.0, 250.0)));
15003 assert_eq!(d.drag_over(), None, "under both targets");
15004 d.cancel_files();
15005
15006 let picture = paths(&["a.png"]);
15009 d.hover_files(&picture, Some(Point::new(50.0, 150.0)));
15010 assert_eq!(d.drag_over(), None);
15011 d.drop_files(&picture, Some(Point::new(50.0, 150.0)));
15012 assert!(d.app().heard.is_empty(), "{:?}", d.app().heard);
15013 }
15014
15015 #[test]
15016 fn files_taken_away_leave_nothing_lit_and_send_nothing() {
15017 let mut d = driver();
15018 d.hover_files(&paths(&["a.txt"]), Some(Point::new(50.0, 150.0)));
15019 assert!(d.drag_over().is_some());
15020 d.cancel_files();
15021 assert_eq!(d.drag_over(), None);
15022 d.run(0.05);
15023 assert!(highlights(&d).is_empty());
15024 assert!(d.app().heard.is_empty());
15025 }
15026
15027 #[test]
15028 fn a_drop_hands_over_what_was_dropped_not_what_was_hovered() {
15029 let mut d = driver();
15030 d.hover_files(&paths(&["old.txt"]), Some(Point::new(50.0, 150.0)));
15031 let dropped = paths(&["b.txt", "a.txt"]);
15032 d.drop_files(&dropped, Some(Point::new(50.0, 150.0)));
15033 assert_eq!(d.app().heard, [Msg::Files("text", dropped)]);
15034 }
15035
15036 #[test]
15037 fn where_the_system_does_not_say_the_only_taker_takes_them() {
15038 let mut d = driver();
15039 let picture = paths(&["a.png"]);
15041 d.hover_files(&picture, None);
15042 assert!(d.drag_over().is_some());
15043 d.drop_files(&picture, None);
15044 assert_eq!(d.app().heard, [Msg::Files("top", picture)]);
15045 }
15046}
15047
15048#[cfg(test)]
15055mod assistive_tech_tests {
15056 use super::testing::Driver;
15057 use crate::{Application, Command};
15058 use frus_widgets::{column, Checkbox, TextField, Theme, Widget};
15059
15060 #[derive(Clone, Debug, PartialEq)]
15061 enum Msg {
15062 Ticked(bool),
15063 Typed(String),
15064 }
15065
15066 #[derive(Default)]
15067 struct Form {
15068 done: bool,
15069 name: String,
15070 }
15071
15072 impl Application for Form {
15073 type Message = Msg;
15074
15075 fn update(&mut self, message: Msg) -> Command<Msg> {
15076 match message {
15077 Msg::Ticked(done) => self.done = done,
15078 Msg::Typed(name) => self.name = name,
15079 }
15080 Command::none()
15081 }
15082
15083 fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
15084 Box::new(column![
15085 Checkbox::new(self.done)
15086 .label("Done")
15087 .on_toggle(Msg::Ticked),
15088 TextField::new(self.name.clone())
15089 .label("Name")
15090 .on_input(Msg::Typed),
15091 ])
15092 }
15093 }
15094
15095 #[test]
15097 fn activate_ticks_a_checkbox() {
15098 let mut driver = Driver::new(Form::default(), 400.0, 300.0);
15099 driver.frame(0.016);
15100 assert!(
15101 driver.assistive_click("Done"),
15102 "the checkbox is in the tree"
15103 );
15104 driver.frame(0.016);
15105 assert!(driver.app().done, "ticked");
15106 assert!(driver.assistive_click("Done"));
15107 driver.frame(0.016);
15108 assert!(!driver.app().done, "unticked");
15109 }
15110
15111 #[test]
15116 fn activate_ticks_a_checkbox_merged_with_its_caption() {
15117 struct Row(bool);
15118 impl Application for Row {
15119 type Message = Msg;
15120 fn update(&mut self, message: Msg) -> Command<Msg> {
15121 if let Msg::Ticked(done) = message {
15122 self.0 = done;
15123 }
15124 Command::none()
15125 }
15126 fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
15127 Box::new(frus_widgets::Semantics::merge(frus_widgets::row![
15128 Checkbox::new(self.0).on_toggle(Msg::Ticked),
15129 frus_widgets::text("Water the plants"),
15130 ]))
15131 }
15132 }
15133 let mut driver = Driver::new(Row(false), 400.0, 300.0);
15134 driver.frame(0.016);
15135 assert!(
15136 driver.assistive_click("Water the plants"),
15137 "the merged node is in the tree"
15138 );
15139 driver.frame(0.016);
15140 assert!(driver.app().0, "ticked");
15141 }
15142
15143 #[test]
15145 fn set_value_fills_a_field() {
15146 let mut driver = Driver::new(Form::default(), 400.0, 300.0);
15147 driver.frame(0.016);
15148 assert!(
15149 driver.assistive_set_value("Name", "Ada"),
15150 "the field is in the tree"
15151 );
15152 driver.frame(0.016);
15153 assert_eq!(driver.app().name, "Ada");
15154 }
15155}
15156
15157#[cfg(test)]
15160mod simulated_keyboard_tests {
15161 use super::testing::Driver;
15162 use crate::{Application, Command};
15163 use frus_widgets::{Flex, Point, Text, TextField, Theme, Widget};
15164
15165 struct Form;
15166
15167 impl Application for Form {
15168 type Message = ();
15169
15170 fn update(&mut self, _message: ()) -> Command<()> {
15171 Command::none()
15172 }
15173
15174 fn view(&self, _theme: &Theme) -> Box<dyn Widget<()>> {
15175 Box::new(
15176 Flex::column()
15177 .child(TextField::new("a field"))
15178 .child(Text::new("some words")),
15179 )
15180 }
15181 }
15182
15183 #[test]
15184 fn the_keyboard_follows_a_field_s_focus() {
15185 let mut d = Driver::new(Form, 393.0, 852.0);
15186 d.simulate("iphone-15");
15187 d.run(0.1);
15188 assert!(
15189 !d.simulated_keyboard(),
15190 "no keyboard before anything is focused"
15191 );
15192
15193 let at = |d: &Driver<Form>, label: &str| {
15194 let (_, r) = d
15195 .texts()
15196 .into_iter()
15197 .find(|(t, _)| t.contains(label))
15198 .unwrap_or_else(|| panic!("{label}: {:?}", d.texts()));
15199 Point::new(r.x + 5.0, r.y + r.height * 0.5)
15200 };
15201 let field = at(&d, "a field");
15202 d.press(field);
15203 d.release(field);
15204 d.run(0.1);
15205 assert!(d.focused().is_some(), "the tap focused the field");
15206 assert!(
15207 d.simulated_keyboard(),
15208 "the keyboard came up with the field's focus"
15209 );
15210
15211 let words = at(&d, "some words");
15212 d.press(words);
15213 d.release(words);
15214 d.run(0.1);
15215 assert!(!d.simulated_keyboard(), "the keyboard went with the focus");
15216 }
15217
15218 #[test]
15220 fn the_text_size_cycles() {
15221 let mut size = 1.0;
15222 let mut seen = vec![size];
15223 for _ in 0..4 {
15224 size = super::next_text_scale(size);
15225 seen.push(size);
15226 }
15227 assert_eq!(seen, [1.0, 1.3, 2.0, 0.85, 1.0]);
15228 assert_eq!(
15229 super::next_text_scale(1.15),
15230 1.0,
15231 "an unusual size goes back to 1"
15232 );
15233 }
15234}