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
118#[cfg(any(android, test))]
131fn wants_keyboard<M>(widget: &dyn Widget<M>) -> bool {
132 is_text_field(widget) && widget.focusable() && widget.takes_typing()
133}
134
135fn is_text_field<M>(widget: &dyn Widget<M>) -> bool {
145 widget.text_value().is_some()
146}
147
148const REGION_DOUBLE_CLICK_SLOP: f32 = 8.0;
151
152const DOUBLE_TAP_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(300);
155
156const DOUBLE_TAP_SLOP: f32 = 100.0;
159
160const MAX_PAN_VELOCITY: f32 = 8000.0;
163
164struct PendingTap<M> {
167 id: WidgetId,
169 at: Instant,
171 position: Point,
173 single: Option<M>,
175}
176
177fn select_all_hides_bar(platform: frus_widgets::TargetPlatform) -> bool {
184 platform.is_desktop()
185}
186
187mod clip {
203 use frus_widgets::WidgetId;
204
205 #[derive(Clone, Debug, PartialEq, Eq)]
207 pub struct Pasted {
208 pub into: WidgetId,
209 pub text: String,
210 }
211
212 impl Pasted {
213 pub fn lands(&self, focused: Option<WidgetId>) -> bool {
219 focused == Some(self.into) && !self.text.is_empty()
220 }
221 }
222
223 #[cfg(not(web))]
225 impl Clipboard {
226 pub fn paste(
227 &mut self,
228 into: WidgetId,
229 _wake: Option<&std::sync::Arc<winit::window::Window>>,
230 ) -> Option<Pasted> {
231 self.get_text().map(|text| Pasted { into, text })
232 }
233 pub fn take_answered(&mut self) -> Option<Pasted> {
234 None
235 }
236 }
237
238 #[cfg(desktop)]
239 pub enum Clipboard {
240 System(Option<arboard::Clipboard>),
242 #[cfg(any(test, feature = "testing"))]
247 Memory(Option<String>),
248 }
249
250 #[cfg(desktop)]
251 impl Clipboard {
252 pub fn new() -> Self {
253 Self::System(arboard::Clipboard::new().ok())
254 }
255 #[cfg(any(test, feature = "testing"))]
257 pub fn in_memory() -> Self {
258 Self::Memory(None)
259 }
260 pub fn get_text(&mut self) -> Option<String> {
261 match self {
262 Self::System(system) => system.as_mut().and_then(|c| c.get_text().ok()),
263 #[cfg(any(test, feature = "testing"))]
264 Self::Memory(text) => text.clone(),
265 }
266 }
267 pub fn has_text(&mut self) -> bool {
268 self.get_text().is_some_and(|text| !text.is_empty())
269 }
270 pub fn set_text(&mut self, text: String) {
271 match self {
272 Self::System(system) => {
273 if let Some(c) = system.as_mut() {
274 let _ = c.set_text(text);
275 }
276 }
277 #[cfg(any(test, feature = "testing"))]
278 Self::Memory(held) => *held = Some(text),
279 }
280 }
281 }
282
283 #[cfg(all(not(desktop), any(test, feature = "testing")))]
285 impl Clipboard {
286 pub fn in_memory() -> Self {
287 Self::new()
288 }
289 }
290
291 #[cfg(android)]
292 pub struct Clipboard;
293
294 #[cfg(android)]
295 impl Clipboard {
296 pub fn new() -> Self {
297 Self
298 }
299 pub fn get_text(&mut self) -> Option<String> {
300 crate::android_clipboard::get_text()
301 }
302 pub fn has_text(&mut self) -> bool {
303 self.get_text().is_some_and(|text| !text.is_empty())
304 }
305 pub fn set_text(&mut self, text: String) {
306 crate::android_clipboard::set_text(&text);
307 }
308 }
309
310 #[cfg(any(web, test))]
315 #[derive(Default)]
316 pub struct Answers(std::rc::Rc<std::cell::RefCell<std::collections::VecDeque<Pasted>>>);
317
318 #[cfg(any(web, test))]
319 impl Answers {
320 pub fn answer_for(
323 &self,
324 into: WidgetId,
325 wake: impl FnOnce() + 'static,
326 ) -> impl FnOnce(String) + 'static {
327 let queue = std::rc::Rc::clone(&self.0);
328 move |text| {
329 queue.borrow_mut().push_back(Pasted { into, text });
330 wake();
331 }
332 }
333
334 pub fn take(&self) -> Option<Pasted> {
336 self.0.borrow_mut().pop_front()
337 }
338 }
339
340 #[cfg(web)]
343 pub struct Clipboard {
344 answered: Answers,
345 }
346
347 #[cfg(web)]
348 impl Clipboard {
349 pub fn new() -> Self {
350 Self {
351 answered: Answers::default(),
352 }
353 }
354 pub fn set_text(&mut self, text: String) {
355 crate::web_clipboard::set_text(&text);
356 }
357 pub fn has_text(&mut self) -> bool {
360 true
361 }
362 pub fn paste(
363 &mut self,
364 into: WidgetId,
365 wake: Option<&std::sync::Arc<winit::window::Window>>,
366 ) -> Option<Pasted> {
367 let wake = wake.cloned();
368 crate::web_clipboard::get_text(self.answered.answer_for(into, move || {
369 if let Some(window) = wake {
370 window.request_redraw();
371 }
372 }));
373 None
374 }
375 pub fn take_answered(&mut self) -> Option<Pasted> {
376 self.answered.take()
377 }
378 }
379
380 #[cfg(not(any(desktop, android, web)))]
381 pub struct Clipboard;
382
383 #[cfg(not(any(desktop, android, web)))]
384 impl Clipboard {
385 pub fn new() -> Self {
386 Self
387 }
388 pub fn get_text(&mut self) -> Option<String> {
389 None
390 }
391 pub fn has_text(&mut self) -> bool {
392 false
393 }
394 pub fn set_text(&mut self, _text: String) {}
395 }
396}
397
398#[cfg(web)]
403mod web_timer {
404 use wasm_bindgen::prelude::Closure;
405 use wasm_bindgen::JsCast;
406
407 pub(crate) struct Interval {
410 id: i32,
411 _closure: Closure<dyn FnMut()>,
413 }
414
415 impl Interval {
416 pub(crate) fn new(ms: i32, f: impl FnMut() + 'static) -> Option<Self> {
419 let window = web_sys::window()?;
420 let closure = Closure::wrap(Box::new(f) as Box<dyn FnMut()>);
421 let id = window
422 .set_interval_with_callback_and_timeout_and_arguments_0(
423 closure.as_ref().unchecked_ref(),
424 ms.max(1),
425 )
426 .ok()?;
427 Some(Self {
428 id,
429 _closure: closure,
430 })
431 }
432 }
433
434 impl Drop for Interval {
435 fn drop(&mut self) {
436 if let Some(window) = web_sys::window() {
437 window.clear_interval_with_handle(self.id);
438 }
439 }
440 }
441
442 pub(crate) fn after(ms: i32, f: impl FnOnce() + 'static) {
451 let Some(window) = web_sys::window() else {
452 return;
453 };
454 let callback = wasm_bindgen::closure::Closure::once_into_js(f);
455 let _ = window.set_timeout_with_callback_and_timeout_and_arguments_0(
456 callback.unchecked_ref(),
457 ms.max(0),
458 );
459 }
460}
461
462#[cfg(not(web))]
467type SubHandle = async_executor::Task<()>;
468#[cfg(web)]
469type SubHandle = Option<web_timer::Interval>;
470
471const SCROLL_SPEED: f32 = 40.0;
473
474const TOUCH_SLOP: f32 = 18.0;
483
484const PRECISE_SLOP: f32 = 1.0;
487
488fn fling_velocity(estimate: VelocityEstimate, slop: f32) -> (f32, f32) {
500 let gate = |velocity: f32, travelled: f32| {
501 if travelled.abs() > slop {
502 velocity
503 } else {
504 0.0
505 }
506 };
507 (
508 gate(estimate.velocity.x, estimate.offset.0),
509 gate(estimate.velocity.y, estimate.offset.1),
510 )
511}
512
513const SCROLL_OVER: f32 = 48.0;
515
516const PAN_FLING_MIN: f32 = 80.0;
518
519const BACK_EDGE: f32 = 24.0;
521
522mod drag_preview {
526 pub const SHADOW_OFFSET_Y: f32 = 4.0;
528 pub const SHADOW_BLUR: f32 = 12.0;
530 pub const SHADOW_ALPHA: f32 = 0.28;
532 pub const BORDER_ALPHA: f32 = 0.9;
534 pub const BORDER_WIDTH: f32 = 1.5;
536 pub const LIFT_Y: f32 = -2.0;
538 pub const INSERT_THICKNESS: f32 = 3.0;
540}
541
542fn frame_needs_build(dirty: bool, animating: bool, was_animating: bool, otherwise: bool) -> bool {
552 dirty || animating || was_animating || otherwise
553}
554
555fn hold_candidates<Msg: Clone>(
563 drag: Option<&Drag>,
564 ui: Option<&frus_widgets::Ui<Msg>>,
565 tree: Option<&dyn frus_widgets::Widget<Msg>>,
566 at: Point,
567) -> (Option<Msg>, Option<frus_widgets::DragSource>) {
568 if matches!(drag, Some(Drag::Back { .. })) {
573 return (None, None);
574 }
575 let Some(ui) = ui else {
576 return (None, None);
577 };
578 let free = matches!(
581 drag,
582 None | Some(Drag::Scroll { moved: false, .. }) | Some(Drag::Gesture { moved: false, .. })
583 );
584 let long_press = if free { ui.long_press_at(at) } else { None };
585 let lift = ui.drag_source_at(at).filter(|source| {
586 tree.and_then(|tree| find_widget(tree, source.id))
587 .is_some_and(|widget| widget.drag_needs_long_press())
588 });
589 (long_press, lift)
590}
591
592fn drag_after_hold(
601 drag: Option<Drag>,
602 lift: Option<frus_widgets::DragSource>,
603 reorder: Option<(WidgetId, usize, Point)>,
604 cursor: Point,
605) -> Option<Drag> {
606 if let Some(Drag::Back { start_x }) = drag {
609 return Some(Drag::Back { start_x });
610 }
611 if let Some(source) = lift {
612 Some(Drag::Item {
613 source,
614 start: cursor,
615 moved: true,
616 over: None,
617 })
618 } else if let Some((id, from, start)) = reorder {
619 Some(Drag::Reorder {
620 id,
621 from,
622 start,
623 moved: true,
624 carried: None,
625 })
626 } else {
627 None
628 }
629}
630
631enum Drag {
633 Scrollbar {
635 id: WidgetId,
636 vertical: bool,
637 grab: f32,
638 track_start: f32,
639 track_len: f32,
640 thumb_len: f32,
641 max: f32,
642 reverse: bool,
645 },
646 RegionSelect { area: WidgetId },
650 RegionHandle {
654 area: WidgetId,
655 handle: crate::selection::Handle,
656 grab: Point,
657 },
658 TextSelect {
660 id: WidgetId,
661 rect: frus_widgets::Rect,
662 },
663 SelectionHandle {
667 id: WidgetId,
668 rect: frus_widgets::Rect,
669 handle: crate::selection::Handle,
670 grab: Point,
671 },
672 Widget {
676 id: WidgetId,
677 rect: frus_widgets::Rect,
678 last_x: f32,
679 press_x: f32,
682 track: frus_widgets::DragTrack,
683 active: bool,
686 thumb: Option<usize>,
689 },
690 Reorder {
694 id: WidgetId,
695 from: usize,
696 start: Point,
697 moved: bool,
698 carried: Option<(WidgetId, Rect)>,
704 },
705 Pan {
711 id: WidgetId,
712 last: Point,
713 moved: bool,
714 viewport: frus_widgets::Rect,
715 },
716 Scroll {
719 id: WidgetId,
720 last: Point,
721 moved: bool,
722 carried: (f32, f32),
726 dismiss: Option<frus_widgets::Dismissable>,
731 pan: Option<(WidgetId, frus_widgets::PanAxis)>,
734 axis: Option<bool>,
744 },
745 Gesture {
749 id: WidgetId,
750 axis: frus_widgets::PanAxis,
751 start: Point,
752 last: Point,
753 moved: bool,
754 },
755 Dismiss {
759 item: frus_widgets::Dismissable,
760 last: Point,
761 moved: bool,
764 },
765 Item {
770 source: frus_widgets::DragSource,
771 start: Point,
772 moved: bool,
775 over: Option<WidgetId>,
776 },
777 Sheet {
782 id: WidgetId,
783 last: Point,
784 moved: bool,
787 available: f32,
788 },
789 Back { start_x: f32 },
792}
793
794fn install_ambient<A: Application>(app: &A, preferred: &[frus_widgets::Locale]) {
820 frus_widgets::ScrollConfiguration::set_root(app.scroll_behavior());
823 frus_widgets::locale::install(app.resolved_locale(preferred));
827 if let Some(table) = app.localizations() {
828 frus_widgets::localizations::install(table);
829 }
830}
831
832fn build_view<A: Application>(
833 app: &A,
834 theme: &Theme,
835 runtime: &frus_widgets::Runtime,
836) -> Box<dyn Widget<A::Message>> {
837 debug_assert!(
838 frus_widgets::MediaQuery::of().is_described(),
839 "a view is being built with no surface described: install one first — milestone 416"
840 );
841 runtime.states.begin_build();
844 let tree = app.view(theme);
845 build_deferred(tree.as_ref(), theme, runtime);
846 tree
847}
848
849struct Mailbox<M: 'static>(Option<EventLoopProxy<M>>);
852
853impl<M: 'static> Clone for Mailbox<M> {
854 fn clone(&self) -> Self {
855 Self(self.0.clone())
856 }
857}
858
859impl<M: 'static> Mailbox<M> {
860 fn send_event(&self, message: M) -> Result<(), ()> {
863 match &self.0 {
864 Some(proxy) => proxy.send_event(message).map_err(|_| ()),
865 None => Err(()),
866 }
867 }
868}
869
870pub struct App<A: Application> {
876 app: A,
878 proxy: Mailbox<A::Message>,
880 window: Option<Arc<Window>>,
881 renderer: Option<Renderer>,
882 renderer_lost: bool,
885 #[cfg(web)]
889 pending_renderer: std::rc::Rc<std::cell::RefCell<Option<Renderer>>>,
890 #[cfg(web)]
892 history: crate::history::History,
893 #[cfg(web)]
896 address: (crate::LocationStrategy, String),
897 #[cfg(desktop)]
899 a11y: Option<crate::a11y::A11y>,
900 #[cfg(web)]
902 a11y: Option<crate::a11y_web::A11y>,
903 ui: Option<Ui<A::Message>>,
905 tree: Option<Box<dyn Widget<A::Message>>>,
907 cursor: Point,
909 hover: crate::hover::Hover,
912 scale: f32,
914 last_size: Option<(f32, f32)>,
916 runtime: Runtime,
918 themes: crate::theming::ThemeFade,
923 reload: Option<crate::reload::ReloadWatcher>,
925 #[cfg(any(desktop, android))]
927 dev: Option<crate::dev::DevChannel>,
928 reported_overflows: std::cell::RefCell<std::collections::HashSet<u64>>,
931 inspector: bool,
933 inspector_dump: bool,
935 debug: DebugAsks,
938 shift: bool,
940 ctrl: bool,
941 alt: bool,
942 meta: bool,
943 goal_x: Option<f32>,
947 clipboard: clip::Clipboard,
950 started: bool,
953 last_frame: Option<Instant>,
955 build_dirty: bool,
961 app_was_animating: bool,
966 drag: Option<Drag>,
968 pointer_touch: bool,
971 gesture_velocity: VelocityTracker,
978 gesture_clock: Option<(f32, f32)>,
985 gesture_start: Instant,
986 reorder_x: f32,
990 reorder_y: f32,
995 #[cfg(any(desktop, web))]
999 announce: String,
1000 press: PressRecognizer,
1002 long_press_msg: Option<A::Message>,
1004 pending_word: Option<WidgetId>,
1007 pending_region_hold: bool,
1010 caret: crate::caret::CaretBlink,
1012 #[cfg(android)]
1016 autofill_reported: Vec<crate::autofill::AutofillField>,
1017 #[cfg(android)]
1019 autofill_focus: Option<WidgetId>,
1020 #[cfg(android)]
1022 autofill_group: Option<WidgetId>,
1023 last_edit_at: Option<Instant>,
1027 pending_lift: Option<frus_widgets::DragSource>,
1031 pending_reorder: Option<(WidgetId, usize, Point)>,
1039 last_click_time: Option<Instant>,
1041 last_region_click: Option<(Instant, Point)>,
1044 pending_tap: Option<PendingTap<A::Message>>,
1046 task_arrivals: u64,
1049 entered: Vec<(WidgetId, Rect)>,
1052 pointer_held: bool,
1054 captured: Vec<(WidgetId, Rect)>,
1057 leaving_counter: u64,
1059 running_subs: HashMap<u64, SubHandle>,
1061 pending_focus: Vec<u64>,
1064 pending_scroll: Vec<(u64, ScrollTo)>,
1068 retry_scroll: Vec<(u64, ScrollTo)>,
1072 pending_sheet: Vec<(u64, SheetTo)>,
1075 retry_sheet: Vec<(u64, SheetTo)>,
1078 focus_history: Vec<WidgetId>,
1083 prev_focus: Option<WidgetId>,
1085 focus_reported: Option<WidgetId>,
1088 autofocused: std::collections::HashSet<WidgetId>,
1091 frame_stats: crate::frame_stats::FrameStats,
1093 frame_costs: crate::frame_stats::FrameCosts,
1095 occluded: bool,
1097 elapsed: f32,
1099 last_insets: WindowInsets,
1101 #[cfg_attr(not(windows), allow(dead_code))]
1104 title_bar_on: bool,
1105 title_bar: Option<frus_widgets::TitleBar>,
1106 #[cfg_attr(not(windows), allow(dead_code))]
1108 window_icon: Option<&'static [u8]>,
1109 reveal_after_keyboard: bool,
1114 platform: PlatformSettings,
1123 inset_baseline: Option<(Insets, (u32, u32))>,
1127 lifecycle: Lifecycle,
1131 #[cfg(android)]
1132 android_app: Option<winit::platform::android::activity::AndroidApp>,
1133 #[cfg(android)]
1135 soft_input_shown: bool,
1136 #[cfg(android)]
1139 system_bars: Option<frus_widgets::SystemBars>,
1140}
1141
1142impl<A: Application> App<A> {
1143 pub fn new(app: A, proxy: EventLoopProxy<A::Message>) -> Self {
1145 Self::with_mailbox(app, Mailbox(Some(proxy)))
1146 }
1147
1148 #[cfg(any(test, feature = "testing"))]
1152 fn detached(app: A) -> Self {
1153 let mut shell = Self::with_mailbox(app, Mailbox(None));
1154 shell.clipboard = clip::Clipboard::in_memory();
1158 shell
1159 }
1160
1161 fn with_mailbox(app: A, proxy: Mailbox<A::Message>) -> Self {
1162 #[cfg(feature = "net")]
1167 frus_widgets::set_image_fetcher(fetch_image_bytes);
1168 #[cfg(not(web))]
1171 frus_widgets::set_task_spawner(|task| crate::runtime::spawn(task).detach());
1172 #[cfg(web)]
1173 frus_widgets::set_task_spawner(wasm_bindgen_futures::spawn_local);
1174 Self {
1175 app,
1176 proxy,
1177 window: None,
1178 renderer: None,
1179 renderer_lost: false,
1180 #[cfg(web)]
1181 pending_renderer: std::rc::Rc::new(std::cell::RefCell::new(None)),
1182 #[cfg(web)]
1183 history: crate::history::History::new(),
1184 #[cfg(web)]
1185 address: (crate::LocationStrategy::Hash, "/".to_string()),
1186 #[cfg(desktop)]
1187 a11y: None,
1188 #[cfg(web)]
1189 a11y: None,
1190 ui: None,
1191 tree: None,
1192 cursor: Point::new(0.0, 0.0),
1193 hover: crate::hover::Hover::default(),
1194 scale: 1.0,
1195 last_size: None,
1196 runtime: Runtime::default(),
1197 themes: crate::theming::ThemeFade::default(),
1198 reload: crate::reload::ReloadWatcher::new(),
1199 #[cfg(any(desktop, android))]
1200 dev: None,
1201 reported_overflows: std::cell::RefCell::new(std::collections::HashSet::new()),
1202 inspector: false,
1203 inspector_dump: false,
1204 debug: DebugAsks::default(),
1205 shift: false,
1206 ctrl: false,
1207 alt: false,
1208 meta: false,
1209 goal_x: None,
1210 clipboard: clip::Clipboard::new(),
1211 started: false,
1212 last_frame: None,
1213 build_dirty: true,
1214 app_was_animating: false,
1215 drag: None,
1216 pointer_touch: false,
1217 gesture_velocity: VelocityTracker::default(),
1218 gesture_clock: None,
1219 gesture_start: Instant::now(),
1220 reorder_x: 0.0,
1221 reorder_y: 0.0,
1222 #[cfg(any(desktop, web))]
1223 announce: String::new(),
1224 press: PressRecognizer::new(),
1225 long_press_msg: None,
1226 pending_word: None,
1227 pending_region_hold: false,
1228 caret: crate::caret::CaretBlink::new(),
1229 #[cfg(android)]
1230 autofill_reported: Vec::new(),
1231 #[cfg(android)]
1232 autofill_focus: None,
1233 #[cfg(android)]
1234 autofill_group: None,
1235 last_edit_at: None,
1236 pending_lift: None,
1237 pending_reorder: None,
1238 last_click_time: None,
1239 last_region_click: None,
1240 pending_tap: None,
1241 task_arrivals: 0,
1242 entered: Vec::new(),
1243 pointer_held: false,
1244 captured: Vec::new(),
1245 leaving_counter: 0,
1246 running_subs: HashMap::new(),
1247 pending_focus: Vec::new(),
1248 pending_scroll: Vec::new(),
1249 retry_scroll: Vec::new(),
1250 pending_sheet: Vec::new(),
1251 retry_sheet: Vec::new(),
1252 focus_history: Vec::new(),
1253 prev_focus: None,
1254 focus_reported: None,
1255 autofocused: std::collections::HashSet::new(),
1256 frame_stats: crate::frame_stats::FrameStats::new(),
1257 frame_costs: crate::frame_stats::FrameCosts::default(),
1258 occluded: false,
1259 elapsed: 0.0,
1260 last_insets: WindowInsets::ZERO,
1261 title_bar_on: false,
1262 title_bar: None,
1263 window_icon: None,
1264 reveal_after_keyboard: false,
1265 platform: PlatformSettings::default(),
1266 inset_baseline: None,
1267 lifecycle: Lifecycle::Detached,
1269 #[cfg(android)]
1270 android_app: None,
1271 #[cfg(android)]
1272 soft_input_shown: false,
1273 #[cfg(android)]
1274 system_bars: None,
1275 }
1276 }
1277
1278 #[cfg(any(desktop, web))]
1282 fn activate_for_assistive_tech(&mut self, id: WidgetId) {
1283 if let Some(action) = self.ui.as_ref().and_then(|ui| ui.edit_action_for(id)) {
1284 self.perform_edit_action(action);
1285 return;
1286 }
1287 if let Some(msg) = self.ui.as_ref().and_then(|ui| ui.msg_for(id)) {
1288 if self.ui.as_ref().is_some_and(|ui| ui.hit_is_on_toolbar(id)) {
1290 self.runtime.selection_handles = None;
1291 self.hide_selection_toolbar();
1292 }
1293 self.dispatch(msg);
1294 self.request_redraw();
1295 }
1296 }
1297
1298 #[cfg(desktop)]
1301 fn drain_a11y_actions(&mut self) {
1302 use crate::a11y::A11yAction;
1303 let actions = match self.a11y.as_ref() {
1304 Some(a11y) => a11y.take_actions(),
1305 None => return,
1306 };
1307 for action in actions {
1308 match action {
1309 A11yAction::Click(id) => self.activate_for_assistive_tech(id),
1310 A11yAction::Focus(id) => {
1311 if self.ui.as_ref().is_some_and(|ui| ui.hit_is_on_toolbar(id)) {
1315 continue;
1316 }
1317 self.runtime.input.focused = Some(id);
1318 self.runtime.focus_visible = true;
1319 self.request_redraw();
1320 }
1321 }
1322 }
1323 }
1324
1325 #[cfg(web)]
1328 fn drain_a11y_actions(&mut self) {
1329 use crate::a11y_web::A11yAction;
1330 let actions = match self.a11y.as_ref() {
1331 Some(a11y) => a11y.take_actions(),
1332 None => return,
1333 };
1334 for action in actions {
1335 match action {
1336 A11yAction::Click(id) => self.activate_for_assistive_tech(id),
1337 A11yAction::Focus(id) => {
1338 if self.ui.as_ref().is_some_and(|ui| ui.hit_is_on_toolbar(id)) {
1342 continue;
1343 }
1344 self.runtime.input.focused = Some(id);
1345 self.runtime.focus_visible = true;
1346 self.request_redraw();
1347 }
1348 }
1349 }
1350 }
1351
1352 fn sync_system_bars(&mut self, theme: &Theme) {
1361 #[cfg(android)]
1362 {
1363 let Some(ui) = self.ui.as_ref() else {
1364 return;
1365 };
1366 let Some(window) = self.window.as_ref() else {
1367 return;
1368 };
1369 let scale = self.total_scale();
1370 let size = window.inner_size();
1371 let (width, height) = (size.width as f32 / scale, size.height as f32 / scale);
1372 let edges = self.last_insets.padding;
1373 let bars = ui
1374 .system_ui_style(
1375 frus_widgets::Point::new(width / 2.0, edges.top + 0.5),
1376 frus_widgets::Point::new(width / 2.0, height - edges.bottom - 0.5),
1377 )
1378 .resolve(theme);
1379 if self.system_bars != Some(bars) && crate::android_system_bars::apply(bars) {
1380 self.system_bars = Some(bars);
1381 }
1382 }
1383 #[cfg(not(android))]
1384 let _ = theme;
1385 }
1386
1387 fn sync_soft_input(&mut self) {
1391 #[cfg(android)]
1392 {
1393 let editing = self
1394 .runtime
1395 .input
1396 .focused
1397 .and_then(|id| {
1398 self.tree
1399 .as_ref()
1400 .and_then(|tree| find_widget(tree.as_ref(), id))
1401 })
1402 .is_some_and(wants_keyboard);
1403 if editing != self.soft_input_shown {
1404 self.soft_input_shown = editing;
1405 self.end_composition();
1406 if crate::android_ime::installed() {
1407 if editing {
1409 if let Some(id) = self.runtime.input.focused {
1410 self.push_ime_context(id);
1411 }
1412 crate::android_ime::start_input(self.focused_ime());
1413 } else {
1414 crate::android_ime::clear_editor_state();
1415 crate::android_ime::stop_input();
1416 }
1417 } else if let Some(app) = &self.android_app {
1418 if editing {
1420 app.show_soft_input(true);
1421 } else {
1422 app.hide_soft_input(false);
1423 }
1424 }
1425 }
1426 }
1427 }
1428
1429 #[cfg(android)]
1436 fn focused_ime(&self) -> frus_widgets::Ime {
1437 self.runtime
1438 .input
1439 .focused
1440 .and_then(|id| {
1441 self.tree
1442 .as_ref()
1443 .and_then(|tree| find_widget(tree.as_ref(), id))
1444 })
1445 .map(|widget| widget.ime())
1446 .unwrap_or_default()
1447 }
1448
1449 fn set_lifecycle(&mut self, state: Lifecycle) {
1451 if self.lifecycle != state {
1452 self.lifecycle = state;
1453 self.app.on_lifecycle(state);
1454 }
1455 }
1456
1457 fn request_soft_input(&mut self) {
1466 #[cfg(android)]
1467 {
1468 let typing = self
1471 .runtime
1472 .input
1473 .focused
1474 .and_then(|id| {
1475 self.tree
1476 .as_ref()
1477 .and_then(|tree| find_widget(tree.as_ref(), id))
1478 })
1479 .is_some_and(wants_keyboard);
1480 if !typing {
1481 return;
1482 }
1483 self.soft_input_shown = true;
1484 self.end_composition();
1485 if crate::android_ime::installed() {
1486 if let Some(id) = self.runtime.input.focused {
1487 self.push_ime_context(id);
1488 }
1489 crate::android_ime::start_input(self.focused_ime());
1490 } else if let Some(app) = &self.android_app {
1491 app.show_soft_input(true);
1492 }
1493 }
1494 }
1495
1496 #[cfg(android)]
1501 fn drain_ime(&mut self) {
1502 use crate::ime::Step;
1503 let events = crate::android_ime::drain();
1504 if events.is_empty() {
1505 return;
1506 }
1507 let Some(focused) = self.runtime.input.focused else {
1508 return;
1509 };
1510 for event in events {
1511 let edit = self
1512 .runtime
1513 .edits
1514 .get(&focused)
1515 .copied()
1516 .unwrap_or_default();
1517 let text = self
1520 .tree
1521 .as_ref()
1522 .and_then(|tree| find_widget(tree.as_ref(), focused))
1523 .and_then(|widget| widget.text_value().map(str::to_owned))
1524 .unwrap_or_default();
1525 for step in crate::ime::plan(&event, &edit, &text) {
1526 match step {
1527 Step::Place { cursor, anchor } => {
1528 let edit = self.runtime.edits.entry(focused).or_default();
1529 edit.cursor = cursor;
1530 edit.anchor = anchor;
1531 }
1532 Step::Key(key) => self.apply_key(focused, key),
1533 Step::Compose(region) => {
1534 if let Some(edit) = self.runtime.edits.get_mut(&focused) {
1535 edit.composing = region;
1536 }
1537 }
1538 Step::ComposeTo(start) => {
1539 if let Some(edit) = self.runtime.edits.get_mut(&focused) {
1540 let end = edit.cursor;
1541 edit.composing = (end > start).then_some((start, end));
1542 }
1543 }
1544 }
1545 }
1546 }
1547 self.push_ime_context(focused);
1549 self.request_redraw();
1550 }
1551
1552 #[cfg(android)]
1560 fn sync_autofill(&mut self) {
1561 use crate::{android_autofill as platform, autofill};
1562 if !crate::android_ime::installed() {
1563 return;
1564 }
1565 let scale = self.total_scale();
1566 let (Some(ui), Some(tree)) = (self.ui.as_ref(), self.tree.as_ref()) else {
1567 return;
1568 };
1569 let fields_of = |id: WidgetId| {
1570 autofill::structure(&ui.form_of(id), scale, |widget| {
1571 find_widget(tree.as_ref(), widget).map(|w| {
1572 (
1573 w.autofill_hints(),
1574 w.text_value().unwrap_or_default().to_owned(),
1575 )
1576 })
1577 })
1578 };
1579 let focus = self.runtime.input.focused.filter(|&id| {
1581 find_widget(tree.as_ref(), id).is_some_and(|w| !w.autofill_hints().is_empty())
1582 });
1583 if focus != self.autofill_focus {
1584 if let Some(left) = self.autofill_focus {
1585 platform::exit(autofill::virtual_id(left));
1586 }
1587 if let Some(entered) = focus {
1588 let group = ui.form_group(entered);
1589 let shown: &[autofill::AutofillField] =
1592 if group.is_some() && group == self.autofill_group {
1593 &self.autofill_reported
1594 } else {
1595 &[]
1596 };
1597 let fields = autofill::with_gone(fields_of(entered), shown);
1598 platform::publish(&fields);
1599 platform::enter(autofill::virtual_id(entered));
1600 self.autofill_reported = fields;
1601 self.autofill_group = group;
1602 }
1603 self.autofill_focus = focus;
1604 } else if let Some(id) = focus {
1605 let fields = autofill::with_gone(fields_of(id), &self.autofill_reported);
1606 for (virtual_id, value) in autofill::changed(&self.autofill_reported, &fields) {
1607 platform::value_changed(virtual_id, &value);
1608 }
1609 self.autofill_reported = fields;
1610 }
1611 if let Some(group) = self.autofill_group {
1612 if ui.group_stops(group).is_empty() {
1613 platform::commit();
1614 self.autofill_group = None;
1615 self.autofill_reported.clear();
1616 }
1617 }
1618 }
1619
1620 #[cfg(android)]
1624 fn drain_autofill(&mut self) {
1625 let fills = crate::android_autofill::drain();
1626 if fills.is_empty() {
1627 return;
1628 }
1629 for (widget, value) in crate::autofill::route(&self.autofill_reported, fills) {
1630 let message = self
1631 .tree
1632 .as_ref()
1633 .and_then(|tree| find_widget(tree.as_ref(), widget))
1634 .and_then(|w| w.replace_value(value.clone()));
1635 if let Some(message) = message {
1636 self.runtime.edits.insert(
1637 widget,
1638 Edit {
1639 cursor: value.chars().count(),
1640 anchor: None,
1641 composing: None,
1642 },
1643 );
1644 self.dispatch_edit(message);
1645 }
1646 }
1647 if let Some(id) = self.runtime.input.focused {
1648 self.push_ime_context(id);
1649 }
1650 self.request_redraw();
1651 }
1652
1653 #[cfg(android)]
1656 fn push_ime_context(&self, id: WidgetId) {
1657 let value = self
1658 .tree
1659 .as_ref()
1660 .and_then(|tree| find_widget(tree.as_ref(), id))
1661 .and_then(|widget| widget.text_value().map(|s| s.to_string()));
1662 if let Some(text) = value {
1663 let edit = self.runtime.edits.get(&id).copied().unwrap_or_default();
1664 crate::android_ime::set_editor_state(&text, edit.cursor, edit.selection_range());
1665 let len = text.chars().count();
1669 let at = |i: usize| crate::ime::utf16_index(&text, i.min(len)) as i32;
1670 let (start, end) = edit.selection_range().unwrap_or((edit.cursor, edit.cursor));
1671 let (cand_start, cand_end) = edit
1672 .composing
1673 .map(|(s, e)| (at(s), at(e)))
1674 .unwrap_or((-1, -1));
1675 crate::android_ime::update_selection(at(start), at(end), cand_start, cand_end);
1676 }
1677 }
1678
1679 #[cfg(android)]
1682 fn end_composition(&mut self) {
1683 if let Some(id) = self.runtime.input.focused {
1684 if let Some(edit) = self.runtime.edits.get_mut(&id) {
1685 edit.composing = None;
1686 }
1687 }
1688 }
1689
1690 #[cfg(android)]
1692 pub(crate) fn set_android_app(
1693 &mut self,
1694 android_app: winit::platform::android::activity::AndroidApp,
1695 ) {
1696 self.android_app = Some(android_app);
1697 }
1698
1699 fn compute_insets(&self, phys_w: u32, phys_h: u32, scale: f32) -> Insets {
1710 #[cfg(android)]
1711 if let Some(app) = &self.android_app {
1712 let r = app.content_rect();
1713 if r.right > r.left && r.bottom > r.top {
1715 let left = r.left.max(0) as f32;
1716 let top = r.top.max(0) as f32;
1717 let right = (phys_w as i32 - r.right).max(0) as f32;
1718 let bottom = (phys_h as i32 - r.bottom).max(0) as f32;
1719 return Insets::new(top / scale, right / scale, bottom / scale, left / scale);
1720 }
1721 }
1722 let _ = (phys_w, phys_h, scale);
1723 Insets::ZERO
1724 }
1725
1726 fn refresh_platform_settings(&mut self) {
1732 #[cfg(android)]
1733 if let Some(app) = &self.android_app {
1734 let read = crate::android_settings::read(app);
1735 if read != self.platform {
1736 self.platform = read;
1737 self.build_dirty = true;
1738 }
1739 return;
1740 }
1741 let brightness = self
1747 .window
1748 .as_ref()
1749 .and_then(|window| window.theme())
1750 .map(|theme| match theme {
1751 winit::window::Theme::Dark => frus_widgets::Brightness::Dark,
1752 winit::window::Theme::Light => frus_widgets::Brightness::Light,
1753 })
1754 .unwrap_or_default();
1755 if brightness != self.platform.brightness {
1756 self.platform.brightness = brightness;
1757 self.build_dirty = true;
1758 }
1759
1760 if self.platform.locales.is_empty() {
1767 let read = platform_locales();
1768 if !read.is_empty() {
1769 self.platform.locales = read;
1770 self.build_dirty = true;
1771 }
1772 }
1773 }
1774}
1775
1776#[cfg(android)]
1785fn platform_locales() -> Vec<frus_widgets::Locale> {
1786 Vec::new()
1787}
1788
1789#[cfg(not(android))]
1790fn platform_locales() -> Vec<frus_widgets::Locale> {
1791 #[cfg(not(web))]
1792 {
1793 sys_locale::get_locales()
1794 .filter_map(|tag| frus_widgets::Locale::parse(&tag))
1795 .collect()
1796 }
1797 #[cfg(web)]
1798 {
1799 let Some(navigator) = web_sys::window().map(|window| window.navigator()) else {
1802 return Vec::new();
1803 };
1804 let listed: Vec<frus_widgets::Locale> = navigator
1805 .languages()
1806 .iter()
1807 .filter_map(|value| value.as_string())
1808 .filter_map(|tag| frus_widgets::Locale::parse(&tag))
1809 .collect();
1810 if listed.is_empty() {
1811 return navigator
1812 .language()
1813 .and_then(|tag| frus_widgets::Locale::parse(&tag))
1814 .into_iter()
1815 .collect();
1816 }
1817 listed
1818 }
1819}
1820
1821fn memory_trimmed() {
1823 let before = frus_widgets::image_cache_bytes();
1824 frus_widgets::trim_image_caches();
1825 log::info!(
1826 "memory is low: decoded network images trimmed from {} to {} KB",
1827 before / 1024,
1828 frus_widgets::image_cache_bytes() / 1024
1829 );
1830}
1831
1832impl<A: Application> ApplicationHandler<A::Message> for App<A> {
1833 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
1834 if self.window.is_some() {
1835 return;
1836 }
1837 self.set_lifecycle(Lifecycle::Resumed);
1839 self.refresh_platform_settings();
1842
1843 let mut attributes = Window::default_attributes()
1844 .with_title(self.app.title())
1845 .with_min_inner_size(winit::dpi::LogicalSize::new(360.0, 280.0));
1847 #[cfg(desktop)]
1850 {
1851 attributes = attributes.with_visible(false);
1852 }
1853 #[cfg(desktop)]
1857 {
1858 let icon = crate::icon::window_icon(&self.app.icon());
1859 #[cfg(windows)]
1860 {
1861 use winit::platform::windows::WindowAttributesExtWindows;
1862 attributes = attributes.with_taskbar_icon(icon.clone());
1863 }
1864 attributes = attributes.with_window_icon(icon);
1865 }
1866 #[cfg(not(web))]
1871 if let Some((w, h)) = self.app.window_size() {
1872 attributes = attributes.with_inner_size(winit::dpi::LogicalSize::new(w, h));
1873 }
1874 #[cfg(desktop)]
1876 if let Some([x, y, w, h]) = crate::dev::restore_window() {
1877 attributes = attributes
1878 .with_position(winit::dpi::PhysicalPosition::new(x, y))
1879 .with_inner_size(winit::dpi::PhysicalSize::new(w as u32, h as u32));
1880 }
1881 #[cfg(web)]
1883 {
1884 use winit::platform::web::WindowAttributesExtWebSys;
1885 attributes = attributes.with_append(true);
1886 }
1887 let window = match event_loop.create_window(attributes) {
1888 Ok(window) => {
1889 crate::startup::window_made();
1890 Arc::new(window)
1891 }
1892 Err(err) => {
1893 log::error!("failed to create the window: {err}");
1894 event_loop.exit();
1895 return;
1896 }
1897 };
1898
1899 #[cfg(web)]
1903 {
1904 self.scale = window.scale_factor() as f32;
1905 self.window = Some(window.clone());
1906 self.a11y = crate::a11y_web::A11y::new(&window);
1909 crate::web_keys::let_the_browser_keep_its_shortcuts();
1912 crate::web_icon::apply(&self.app.icon());
1914 self.build_dirty = true;
1915 if !self.started {
1916 self.started = true;
1917 let command = self.app.init();
1918 self.run_command(command);
1919 self.sync_subscriptions();
1920 self.open_launch_address(&window);
1921 crate::dev_commands::set_window(Some(window.clone()));
1922 if let Some(states) = crate::web_dev::start() {
1923 self.runtime.states.restore_from(&states);
1924 }
1925 #[cfg(feature = "hot-reload")]
1926 {
1927 crate::hot::set_window(window.clone());
1928 crate::hot::connect();
1929 }
1930 }
1931 let slot = self.pending_renderer.clone();
1932 let win = window.clone();
1933 let size = window.inner_size();
1934 wasm_bindgen_futures::spawn_local(async move {
1935 match Renderer::new(win.clone(), size.width.max(1), size.height.max(1)).await {
1936 Ok(r) => {
1937 crate::startup::renderer_made(r.startup_times());
1938 *slot.borrow_mut() = Some(r);
1939 win.request_redraw();
1940 }
1941 Err(err) => log::error!("failed to initialise the renderer (Web): {err:#}"),
1942 }
1943 });
1944 }
1945
1946 #[cfg(not(web))]
1947 {
1948 let size = window.inner_size();
1949 let (width, height) = (size.width.max(1), size.height.max(1));
1950 let see_through = if cfg!(windows) {
1954 match pollster::block_on(Renderer::see_through(window.clone(), width, height)) {
1955 Ok(renderer) => Some(renderer),
1956 Err(err) => {
1957 log::info!("title bar painted by frus: {err:#}");
1958 None
1959 }
1960 }
1961 } else {
1962 None
1963 };
1964 let renderer = match see_through {
1965 Some(renderer) => Ok(renderer),
1966 None => pollster::block_on(Renderer::new(window.clone(), width, height)),
1967 };
1968
1969 match renderer {
1970 Ok(renderer) => {
1971 crate::startup::renderer_made(renderer.startup_times());
1972 self.scale = window.scale_factor() as f32;
1973 #[cfg(desktop)]
1976 {
1977 self.a11y = Some(crate::a11y::A11y::new(event_loop, &window));
1978 window.set_visible(true);
1979 crate::instance::set_window(window.clone());
1982 }
1983 self.window = Some(window.clone());
1984 #[cfg(android)]
1986 crate::android_ime::set_window(Some(window.clone()));
1987 #[cfg(android)]
1988 crate::dev_commands::set_window(Some(window.clone()));
1989 self.renderer = Some(renderer);
1990 self.build_dirty = true;
1992 if !self.started {
1995 self.started = true;
1996 let command = self.app.init();
1997 self.run_command(command);
1998 self.sync_subscriptions();
1999 #[cfg(any(android, desktop))]
2000 self.open_launch_link();
2001 #[cfg(any(android, desktop))]
2002 self.connect_dev();
2003 #[cfg(feature = "hot-reload")]
2004 {
2005 crate::hot::set_window(window.clone());
2006 crate::hot::connect();
2007 }
2008 }
2009 window.request_redraw();
2010 }
2011 Err(err) => {
2012 log::error!("failed to initialise the renderer: {err:#}");
2013 event_loop.exit();
2014 }
2015 }
2016 }
2017 }
2018
2019 fn suspended(&mut self, _event_loop: &ActiveEventLoop) {
2024 self.set_lifecycle(Lifecycle::Paused);
2026 #[cfg(android)]
2028 if self.dev.is_some() {
2029 if let Some(storage) = self
2030 .android_app
2031 .as_ref()
2032 .and_then(|a| a.internal_data_path())
2033 {
2034 crate::dev::keep_states(&storage, &self.runtime.states.snapshot());
2035 }
2036 }
2037 self.renderer = None;
2038 self.window = None;
2039 #[cfg(android)]
2040 crate::android_ime::set_window(None);
2041 #[cfg(android)]
2042 crate::dev_commands::set_window(None);
2043 self.last_frame = None;
2044 }
2045
2046 fn exiting(&mut self, _event_loop: &ActiveEventLoop) {
2049 self.set_lifecycle(Lifecycle::Detached);
2050 }
2051
2052 fn memory_warning(&mut self, _event_loop: &ActiveEventLoop) {
2056 memory_trimmed();
2057 }
2058
2059 fn user_event(&mut self, _event_loop: &ActiveEventLoop, message: A::Message) {
2062 self.dispatch(message);
2063 self.request_redraw();
2064 }
2065
2066 fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) {
2070 if matches!(cause, StartCause::ResumeTimeReached { .. }) && self.press.poll(Instant::now())
2071 {
2072 self.hold_deadline_reached();
2073 }
2074 if matches!(cause, StartCause::ResumeTimeReached { .. }) {
2075 self.poll_pending_tap(Instant::now());
2076 }
2077
2078 if matches!(cause, StartCause::ResumeTimeReached { .. })
2080 && self.lifecycle == Lifecycle::Resumed
2081 && self.caret.next_toggle(Instant::now()).is_some()
2082 {
2083 self.request_redraw();
2084 }
2085
2086 #[cfg(android)]
2088 self.drain_ime();
2089 #[cfg(android)]
2091 self.drain_autofill();
2092
2093 #[cfg(desktop)]
2095 self.drain_a11y_actions();
2096 #[cfg(web)]
2098 self.drain_a11y_actions();
2099
2100 if let Some(watcher) = self.reload.as_mut() {
2103 if watcher.binary_changed() {
2104 watcher.handoff(self.app.save_state());
2105 }
2106 }
2107 event_loop.set_control_flow(self.idle_control_flow());
2108 }
2109
2110 fn window_event(
2111 &mut self,
2112 event_loop: &ActiveEventLoop,
2113 _window_id: WindowId,
2114 event: WindowEvent,
2115 ) {
2116 #[cfg(desktop)]
2118 if let (Some(a11y), Some(window)) = (self.a11y.as_mut(), self.window.as_ref()) {
2119 a11y.process_event(window, &event);
2120 }
2121
2122 match event {
2123 WindowEvent::CloseRequested => event_loop.exit(),
2124
2125 WindowEvent::Focused(focused) => {
2129 if !matches!(self.lifecycle, Lifecycle::Paused | Lifecycle::Detached) {
2130 self.set_lifecycle(if focused {
2131 Lifecycle::Resumed
2132 } else {
2133 Lifecycle::Inactive
2134 });
2135 }
2136 #[cfg(windows)]
2140 if self.title_bar_on {
2141 self.request_redraw();
2142 }
2143 }
2144
2145 WindowEvent::Resized(size) => {
2146 if let Some(renderer) = self.renderer.as_mut() {
2147 renderer.resize(size.width, size.height);
2148 }
2149 if let Some(window) = &self.window {
2150 window.request_redraw();
2151 }
2152 }
2153
2154 WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
2155 self.scale = scale_factor as f32;
2156 if let Some(window) = &self.window {
2158 let size = window.inner_size();
2159 if let Some(renderer) = self.renderer.as_mut() {
2160 renderer.resize(size.width, size.height);
2161 }
2162 }
2163 self.request_redraw();
2164 }
2165
2166 WindowEvent::Occluded(occluded) => {
2167 self.occluded = occluded;
2168 if !occluded {
2169 self.request_redraw();
2170 }
2171 }
2172
2173 WindowEvent::CursorMoved { position, .. } => {
2178 let scale = self.total_scale();
2179 let position = Point::new(position.x as f32 / scale, position.y as f32 / scale);
2180 self.pointer(
2181 event_loop,
2182 PointerEvent {
2183 kind: PointerKind::Move,
2184 position,
2185 touch: false,
2186 },
2187 );
2188 }
2189
2190 WindowEvent::CursorLeft { .. } => {
2192 self.hover.left();
2193 self.sync_hover();
2194 self.sync_regions(false);
2195 }
2196
2197 WindowEvent::Touch(touch) => {
2198 let scale = self.total_scale();
2199 let position = Point::new(
2200 touch.location.x as f32 / scale,
2201 touch.location.y as f32 / scale,
2202 );
2203 let kind = match touch.phase {
2204 TouchPhase::Started => PointerKind::Down,
2205 TouchPhase::Moved => PointerKind::Move,
2206 TouchPhase::Ended => PointerKind::Up,
2207 TouchPhase::Cancelled => PointerKind::Cancel,
2208 };
2209 self.pointer(
2210 event_loop,
2211 PointerEvent {
2212 kind,
2213 position,
2214 touch: true,
2215 },
2216 );
2217 }
2218
2219 WindowEvent::ModifiersChanged(modifiers) => {
2220 let state = modifiers.state();
2221 self.shift = state.shift_key();
2222 self.ctrl = state.control_key();
2223 self.alt = state.alt_key();
2224 self.meta = state.super_key();
2225 }
2226
2227 WindowEvent::MouseInput {
2228 state: ElementState::Pressed,
2229 button: MouseButton::Left,
2230 ..
2231 } => self.pointer(
2232 event_loop,
2233 PointerEvent {
2234 kind: PointerKind::Down,
2235 position: self.cursor,
2236 touch: false,
2237 },
2238 ),
2239
2240 WindowEvent::MouseInput {
2241 state: ElementState::Released,
2242 button: MouseButton::Left,
2243 ..
2244 } => self.pointer(
2245 event_loop,
2246 PointerEvent {
2247 kind: PointerKind::Up,
2248 position: self.cursor,
2249 touch: false,
2250 },
2251 ),
2252
2253 WindowEvent::MouseInput {
2256 state: ElementState::Pressed,
2257 button: MouseButton::Right,
2258 ..
2259 } => self.context_click(),
2260
2261 WindowEvent::KeyboardInput { event, .. } if event.state == ElementState::Pressed => {
2262 self.key_down(&KeyDown::from(&event), Some(event_loop));
2263 }
2264
2265 WindowEvent::MouseWheel { delta, .. } => {
2266 let (mut dx, mut dy) = match delta {
2267 MouseScrollDelta::LineDelta(x, y) => (x * SCROLL_SPEED, y * SCROLL_SPEED),
2268 MouseScrollDelta::PixelDelta(pos) => {
2270 let scale = self.total_scale();
2271 (pos.x as f32 / scale, pos.y as f32 / scale)
2272 }
2273 };
2274 if self.shift {
2276 dx = dy;
2277 dy = 0.0;
2278 }
2279 if let Some((id, viewport)) = self
2282 .ui
2283 .as_ref()
2284 .and_then(|ui| ui.interactive_at(self.cursor))
2285 {
2286 let (min, max) = self
2287 .tree
2288 .as_ref()
2289 .and_then(|tree| find_widget(tree.as_ref(), id))
2290 .and_then(|w| w.interactive())
2291 .unwrap_or((0.5, 4.0));
2292 let factor = (1.0 + dy * 0.1 / SCROLL_SPEED).clamp(0.2, 5.0);
2294 self.runtime.interactive_velocity.remove(&id);
2296 let view = self.runtime.interactive.entry(id).or_default();
2297 *view = view
2299 .zoom_at(factor, self.cursor, min, max)
2300 .clamped(viewport);
2301 self.request_redraw();
2302 return;
2303 }
2304 let hit = {
2308 let scroll = &self.runtime.scroll;
2309 self.ui.as_ref().and_then(|ui| {
2310 ui.scroll_chain(self.cursor).find(|area| {
2311 area.accepts_user_offset(
2312 scroll.get(&area.id).copied().unwrap_or((0.0, 0.0)),
2313 )
2314 })
2315 })
2316 };
2317 if let Some(area) = hit {
2318 let id = area.id;
2323 let physics = area.physics;
2324 let over = if physics.allows_overscroll() {
2325 SCROLL_OVER
2326 } else {
2327 0.0
2328 };
2329 self.runtime.stop_scroll_fling(id);
2332 let current = self.runtime.scroll.get(&id).copied().unwrap_or((0.0, 0.0));
2333 let (ddx, ddy) = area.offset_delta((dx, dy));
2337 let target = self.runtime.scroll_target.entry(id).or_insert(current);
2338 let (wanted_x, wanted_y) = (target.0 + ddx, target.1 + ddy);
2339 target.0 = wanted_x.clamp(-over, area.max_x + over);
2340 target.1 = wanted_y.clamp(-over, area.max_y + over);
2341 let refused = (wanted_x - target.0, wanted_y - target.1);
2342 self.runtime.scroll_velocity.entry(id).or_insert((0.0, 0.0));
2343 let cursor = self.cursor;
2346 for (refused, vertical, extent) in [
2347 (refused.0, false, area.viewport.width),
2348 (refused.1, true, area.viewport.height),
2349 ] {
2350 if refused.abs() < 1e-3 {
2351 continue;
2352 }
2353 let edge = area.refused_edge(vertical, refused);
2354 let (offset, cross) =
2355 frus_widgets::glow_cross_axis(area.viewport, edge, cursor);
2356 self.runtime.glow_pull(
2357 id,
2358 area.overscroll,
2359 edge,
2360 refused,
2361 extent,
2362 offset,
2363 cross,
2364 );
2365 self.runtime.glow_scroll_end(id);
2369 }
2370 self.request_redraw();
2371 }
2372 }
2373
2374 WindowEvent::RedrawRequested => {
2375 if std::mem::take(&mut self.renderer_lost) {
2377 self.rebuild_renderer();
2378 }
2379 let measuring = self.frame_stats.enabled();
2381 let frame_start = measuring.then(Instant::now);
2382 self.frame_costs = crate::frame_stats::FrameCosts::default();
2383 #[cfg(web)]
2386 if self.renderer.is_none() {
2387 match self.pending_renderer.borrow_mut().take() {
2388 Some(mut renderer) => {
2389 if let Some(window) = &self.window {
2397 let size = window.inner_size();
2398 renderer.resize(size.width.max(1), size.height.max(1));
2399 }
2400 self.renderer = Some(renderer);
2401 }
2402 None => return,
2403 }
2404 }
2405 while let Some(pasted) = self.clipboard.take_answered() {
2409 self.land_paste(pasted);
2410 }
2411 self.report_focus();
2414 if self
2417 .runtime
2418 .selection_toolbar
2419 .is_some_and(|mark| self.runtime.input.focused != Some(mark.id))
2420 {
2421 self.runtime.selection_toolbar = None;
2422 }
2423 if self.occluded {
2425 return;
2426 }
2427 let size = self
2428 .window
2429 .as_ref()
2430 .map(|w| w.inner_size())
2431 .unwrap_or_default();
2432 if size.width == 0 || size.height == 0 {
2434 self.last_frame = None; return;
2436 }
2437 let scale = self.total_scale();
2440 let (width, height) = (size.width as f32 / scale, size.height as f32 / scale);
2441
2442 if self.last_size != Some((width, height)) {
2445 self.last_size = Some((width, height));
2446 self.build_dirty = true;
2447 self.app.on_resize(width, height);
2448 }
2449
2450 let raw = self.compute_insets(size.width, size.height, scale);
2457 let phys = (size.width, size.height);
2458 let mut baseline = match self.inset_baseline {
2459 Some((b, s)) if s == phys => b,
2460 _ => {
2461 self.inset_baseline = Some((raw, phys));
2462 raw
2463 }
2464 };
2465 if raw.bottom < baseline.bottom {
2466 baseline = raw;
2467 self.inset_baseline = Some((raw, phys));
2468 }
2469 let insets = WindowInsets::from_baseline(baseline, raw);
2470 if self.last_insets != insets {
2471 if insets.view_insets.bottom > self.last_insets.view_insets.bottom {
2472 self.reveal_after_keyboard = true;
2473 }
2474 self.last_insets = insets;
2475 self.build_dirty = true;
2476 self.app.on_insets(insets);
2477 }
2478
2479 let now = Instant::now();
2481 let dt = self
2482 .last_frame
2483 .map(|prev| (now - prev).as_secs_f32().min(0.05))
2484 .unwrap_or(0.0);
2485 self.last_frame = Some(now);
2486
2487 self.elapsed += dt;
2489 self.runtime.time = self.elapsed;
2490
2491 let caret_now = Instant::now();
2496 let caret = if self.lifecycle == Lifecycle::Resumed {
2497 self.caret_signature()
2498 } else {
2499 None
2500 };
2501 self.caret.observe(caret_now, caret);
2502 self.runtime.caret_hidden = self.caret.hidden(caret_now);
2503
2504 let mut app_animating = self.app.tick(dt);
2506
2507 let settings = self
2516 .platform
2517 .accessibility
2518 .with_overrides(self.app.accessibility());
2519
2520 install_ambient(&self.app, &self.platform.locales);
2530
2531 let theme_moved = self.themes.advance(
2532 &self.app,
2533 self.brightness(),
2534 settings.high_contrast,
2535 settings.disable_animations,
2536 dt,
2537 );
2538 app_animating |= theme_moved | self.themes.animating();
2541 let theme = self.themes.displayed(&self.app);
2542
2543 let _surface = self.media_query(width, height).install();
2556
2557 self.runtime.still = settings.disable_animations;
2568 self.runtime.scrollbar_hovered = self
2576 .ui
2577 .as_ref()
2578 .and_then(|ui| ui.scrollbar_near(self.cursor))
2579 .map(|bar| bar.id);
2580 self.runtime.scrollbar_dragged = match self.drag {
2581 Some(Drag::Scrollbar { id, .. }) => Some(id),
2582 _ => None,
2583 };
2584 let asked = self.app.effects();
2591 self.run_command(asked);
2592 self.take_dev_commands();
2593 #[cfg(web)]
2594 self.sync_address();
2595 #[cfg(any(desktop, android))]
2596 self.report_dev();
2597 #[cfg(desktop)]
2598 self.open_links_from_other_instances();
2599 let arrived = self.take_task_arrivals();
2601 #[cfg(feature = "hot-reload")]
2603 let arrived = crate::hot::take_patched() || arrived;
2604 if frus_widgets::take_rebuild_request() || arrived {
2605 self.build_dirty = true;
2606 }
2607 let need_build = frame_needs_build(
2608 self.build_dirty,
2609 app_animating,
2610 std::mem::replace(&mut self.app_was_animating, app_animating),
2611 self.tree.is_none() || self.runtime.switching(),
2612 );
2613 if need_build {
2614 let build_start = measuring.then(Instant::now);
2622 let tree = build_view(&self.app, &theme, &self.runtime);
2623 if let Some(at) = build_start {
2624 self.frame_costs.build = Some(at.elapsed().as_secs_f32() * 1000.0);
2625 }
2626 let ids = collect_ids(tree.as_ref());
2627 let present: std::collections::HashSet<_> = ids.iter().copied().collect();
2628
2629 let leaving: std::collections::HashSet<u64> = self
2632 .runtime
2633 .mounted
2634 .iter()
2635 .filter(|id| !present.contains(id))
2636 .map(|id| id.as_u64())
2637 .collect();
2638 if !leaving.is_empty() {
2639 if let Some(ui) = &self.ui {
2640 let captured: Vec<_> = ui
2641 .scene()
2642 .primitives()
2643 .iter()
2644 .filter(|p| leaving.contains(&p.owner()))
2645 .cloned()
2646 .collect();
2647 if !captured.is_empty() {
2648 self.runtime
2649 .leaving
2650 .insert(self.leaving_counter, (captured, 1.0));
2651 self.leaving_counter = self.leaving_counter.wrapping_add(1);
2652 }
2653 }
2654 }
2655
2656 for &id in &ids {
2658 if self.runtime.mounted.insert(id) {
2659 self.runtime.anims.entry(id).or_default().opacity = 0.0;
2660 }
2661 }
2662 self.runtime.mounted.retain(|id| present.contains(id));
2663
2664 self.tree = Some(tree);
2665 self.sync_subscriptions();
2670
2671 self.runtime.paint_cache.borrow_mut().bump_generation();
2676 }
2677 self.build_dirty = false;
2678
2679 if !self.pending_focus.is_empty() {
2684 let keys = std::mem::take(&mut self.pending_focus);
2685 let ids: Vec<WidgetId> = self
2686 .tree
2687 .as_deref()
2688 .map(|tree| keys.iter().filter_map(|&k| find_by_key(tree, k)).collect())
2689 .unwrap_or_default();
2690 if let Some(&id) = ids.last() {
2691 self.runtime.input.focused = Some(id);
2692 self.runtime.focus_visible = true;
2693 }
2694 }
2695
2696 let autoscrolling = self.autoscroll_carried(dt);
2705
2706 let reorder_animating = self.advance_reorder_springs(dt);
2717
2718 let tree = self
2721 .tree
2722 .as_deref()
2723 .expect("the view was built at least once");
2724
2725 let scroll_regions = self
2728 .ui
2729 .as_ref()
2730 .map(|ui| ui.scroll_regions().to_vec())
2731 .unwrap_or_default();
2732 let refresh_areas = self
2735 .ui
2736 .as_ref()
2737 .map(|ui| ui.refresh_areas().to_vec())
2738 .unwrap_or_default();
2739 let dismissables = self
2740 .ui
2741 .as_ref()
2742 .map(|ui| ui.dismissables().to_vec())
2743 .unwrap_or_default();
2744 let mut dismissed: Vec<A::Message> = Vec::new();
2747 let sheet_areas = self
2748 .ui
2749 .as_ref()
2750 .map(|ui| ui.sheets().to_vec())
2751 .unwrap_or_default();
2752 let interactive_bounds = self
2753 .ui
2754 .as_ref()
2755 .map(|ui| ui.interactive_bounds())
2756 .unwrap_or_default();
2757
2758 self.runtime.sync_pages(&scroll_regions);
2762 self.runtime.sync_visible(&scroll_regions);
2766 let requests = std::mem::take(&mut self.pending_scroll);
2771 let (retry, scrolled) =
2772 apply_scroll_requests(&mut self.runtime, tree, requests, &scroll_regions);
2773 self.retry_scroll = retry;
2774 let requests = std::mem::take(&mut self.pending_sheet);
2777 let (retry, sheeted) =
2778 apply_sheet_requests(&mut self.runtime, tree, requests, &sheet_areas);
2779 self.retry_sheet = retry;
2780
2781 let moving: [(&str, bool); 25] = [
2785 ("scrolled", scrolled),
2786 ("sheeted", sheeted),
2787 ("anims", self.runtime.advance(dt)),
2788 ("leaving", self.runtime.advance_leaving(dt)),
2789 ("switchers", self.runtime.advance_switchers(tree, dt)),
2790 ("values", self.runtime.advance_values(tree, dt)),
2791 ("colors", self.runtime.advance_colors(tree, dt)),
2792 ("sizes", self.runtime.advance_sizes(tree, dt)),
2793 ("radii", self.runtime.advance_radii(tree, dt)),
2794 ("paddings", self.runtime.advance_paddings(tree, dt)),
2795 ("offsets", self.runtime.advance_offsets(tree, dt)),
2796 ("pins", self.runtime.advance_pins(tree, dt)),
2797 ("fractions", self.runtime.advance_fractions(tree, dt)),
2798 ("text_styles", self.runtime.advance_text_styles(tree, dt)),
2799 ("transforms", self.runtime.advance_transforms(tree, dt)),
2800 ("scroll", self.runtime.advance_scroll(&scroll_regions, dt)),
2801 ("glow", self.runtime.advance_glow(dt)),
2802 ("refresh", self.runtime.advance_refresh(&refresh_areas, dt)),
2803 ("dismiss", {
2804 let (moving, done) = self.runtime.advance_dismiss(&dismissables, dt);
2809 dismissed.extend(done.into_iter().filter_map(|(id, direction)| {
2810 find_widget(tree, id).and_then(|widget| widget.on_dismissed(direction))
2811 }));
2812 moving
2813 }),
2814 ("sheets", {
2815 let (moving, closed) = self.runtime.advance_sheets(&sheet_areas, dt);
2818 dismissed.extend(closed.into_iter().filter_map(|id| {
2819 find_widget(tree, id).and_then(|widget| widget.on_sheet_dismissed())
2820 }));
2821 let mut handed = false;
2824 for (list, velocity) in self.runtime.take_sheet_handovers() {
2825 if let Some(area) = scroll_regions.iter().find(|a| a.id == list) {
2826 handed |=
2827 self.runtime
2828 .fling_scroll(*area, area.physics, (0.0, velocity));
2829 }
2830 }
2831 moving | handed
2832 }),
2833 (
2834 "interactive",
2835 self.runtime.advance_interactive(&interactive_bounds, dt),
2836 ),
2837 ("ink", self.runtime.advance_ink(dt)),
2838 ("reorder", reorder_animating),
2839 ("autoscroll", autoscrolling),
2840 ("app", app_animating),
2841 ];
2842 let animating = moving.iter().any(|(_, on)| *on);
2843 self.frame_stats.note_moving(&moving);
2844 let layout_start = measuring.then(Instant::now);
2848 let inspected = self.inspector
2851 || self.debug.outlines
2852 || self.inspector_dump
2853 || self.debug.focus_dump;
2854 let (ui, scene) = if inspected {
2855 let (ui, nodes) = frus_widgets::build_ui_inspected(
2856 tree,
2857 Size::new(width, height),
2858 &self.runtime,
2859 &theme,
2860 );
2861 if std::mem::take(&mut self.inspector_dump) {
2862 log::info!(target: "frus::inspector", "
2864{}", frus_widgets::dump_tree(&nodes));
2865 }
2866 if std::mem::take(&mut self.debug.focus_dump) {
2867 let focusable: Vec<WidgetId> = ui.focusable_ids().collect();
2868 log::info!(target: "frus::inspector", "focus, in tab order:
2869{}", dump_focus(&nodes, &focusable, self.runtime.input.focused));
2870 }
2871 let mut scene = if self.inspector {
2872 on_top_of(ui.scene())
2875 } else {
2876 ui.scene().clone()
2877 };
2878 if self.inspector || self.debug.outlines {
2879 frus_widgets::paint_inspector_overlay(
2880 &nodes,
2881 self.inspector.then_some(self.cursor),
2882 Size::new(width, height),
2883 &theme,
2884 &mut scene,
2885 );
2886 }
2887 (ui, scene.scaled(scale))
2889 } else {
2890 let ui = build_ui(tree, Size::new(width, height), &self.runtime, &theme);
2891 let scene = if matches!(self.drag, Some(Drag::Reorder { moved: true, .. })) {
2894 let mut scene = ui.scene().clone();
2895 self.paint_reorder_preview(&ui, &theme, &mut scene);
2896 scene.scaled(scale)
2897 } else if matches!(self.drag, Some(Drag::Item { moved: true, .. })) {
2898 let mut scene = ui.scene().clone();
2899 self.paint_drag_ghost(&ui, &theme, &mut scene);
2900 scene.scaled(scale)
2901 } else {
2902 ui.scene().scaled(scale)
2904 };
2905 (ui, scene)
2906 };
2907 if let Some(at) = layout_start {
2908 self.frame_costs.layout_paint = at.elapsed().as_secs_f32() * 1000.0;
2909 self.frame_costs.layers = count_layers(scene.primitives());
2910 }
2911 self.report_overflows(&ui);
2912 self.runtime.states.end_frame();
2916 crate::startup::laid_out();
2917 #[cfg(web)]
2918 crate::web_dev::keep(|| self.runtime.states.snapshot());
2919 if frus_widgets::rebuild_requested() {
2920 self.request_redraw();
2921 }
2922 let scene = if self.renderer.as_ref().is_some_and(Renderer::sees_through) {
2925 backdrop_under(scene, width * scale, height * scale, self.system_line())
2926 } else {
2927 scene
2928 };
2929 if std::mem::take(&mut self.debug.semantics_dump) {
2931 log::info!(target: "frus::inspector", "semantics, in reading order:
2932{}", dump_semantics(ui.semantics()));
2933 }
2934 let scene = if self.debug.performance {
2935 let mut scene = on_top_of(&scene);
2939 let costs = self.debug.last_costs.as_ref().map(|(costs, _)| costs);
2940 let top = self.last_insets.padding.top * scale;
2942 paint_performance(
2943 &mut scene,
2944 costs,
2945 &self.debug.history,
2946 self.debug.fps,
2947 width * scale,
2948 top,
2949 scale,
2950 );
2951 scene
2952 } else {
2953 scene
2954 };
2955 #[cfg(desktop)]
2956 if let Some(path) = self.debug.screenshot.take() {
2957 write_screenshot(&scene, width * scale, height * scale, &path);
2958 }
2959 let mut device_lost = false;
2960 if let Some(renderer) = self.renderer.as_mut() {
2961 let render_start = measuring.then(Instant::now);
2962 match renderer.render(&scene) {
2963 frus_gpu::RenderOutcome::Presented => crate::startup::presented(),
2964 frus_gpu::RenderOutcome::NeedsReconfigure => {
2965 renderer.reconfigure();
2966 }
2967 frus_gpu::RenderOutcome::DeviceLost => device_lost = true,
2968 frus_gpu::RenderOutcome::Skipped => {}
2969 }
2970 #[cfg(web)]
2973 if std::mem::take(&mut self.debug.page_screenshot) {
2974 if let Some(window) = self.window.as_ref() {
2975 crate::web_dev::send_screenshot(window);
2976 }
2977 }
2978 if let Some(at) = render_start {
2979 self.frame_costs.render = at.elapsed().as_secs_f32() * 1000.0;
2980 let timings = renderer.last_timings();
2981 self.frame_costs.acquire = timings.acquire;
2982 self.frame_costs.draw = timings.draw;
2983 self.frame_costs.gpu = timings.gpu;
2984 }
2985 }
2986 if device_lost {
2989 self.renderer_lost = true;
2990 if let Some(window) = self.window.as_ref() {
2991 window.request_redraw();
2992 }
2993 }
2994 let widget_asks = ui.wants_animation();
3004 let loading =
3005 frus_widgets::images_in_flight() > 0 || frus_widgets::tasks_in_flight() > 0;
3006 let wants_animation = widget_asks || loading;
3007
3008 self.ui = Some(ui);
3010 let revealed = std::mem::take(&mut self.reveal_after_keyboard) && {
3016 let moves = match (self.runtime.input.focused, self.ui.as_ref()) {
3017 (Some(id), Some(ui)) => ui.reveal(id, &self.runtime),
3018 _ => Vec::new(),
3019 };
3020 let moved = !moves.is_empty();
3021 for (area, offset) in moves {
3022 self.runtime.scroll_ballistic.remove(&area);
3023 self.runtime.scroll_velocity.remove(&area);
3024 self.runtime.scroll_target.insert(area, offset);
3025 }
3026 moved
3027 };
3028 let rehovered = self.drag.is_none() && {
3038 let hovered = self.ui.as_ref().and_then(|ui| self.hover.target(ui));
3039 let changed = hovered != self.runtime.input.hovered;
3040 self.runtime.input.hovered = hovered;
3041 changed
3042 };
3043 if rehovered {
3044 self.build_dirty = true;
3045 }
3046 let wants_animation = wants_animation || rehovered || revealed;
3047
3048 let paged = self
3052 .ui
3053 .as_ref()
3054 .map(|ui| ui.scroll_regions().to_vec())
3055 .unwrap_or_default();
3056 self.runtime.keep_scroll_in_range(&paged);
3061 let turned: Vec<A::Message> = self
3062 .runtime
3063 .page_changes(&paged)
3064 .into_iter()
3065 .filter_map(|(id, page)| {
3066 find_widget(tree, id).and_then(|widget| widget.on_page_changed(page))
3067 })
3068 .collect();
3069
3070 let (_, moved_late) = apply_scroll_requests(
3074 &mut self.runtime,
3075 tree,
3076 std::mem::take(&mut self.retry_scroll),
3077 &paged,
3078 );
3079 let (_, sheeted_late) = {
3082 let sheets = self
3083 .ui
3084 .as_ref()
3085 .map(|ui| ui.sheets().to_vec())
3086 .unwrap_or_default();
3087 apply_sheet_requests(
3088 &mut self.runtime,
3089 tree,
3090 std::mem::take(&mut self.retry_sheet),
3091 &sheets,
3092 )
3093 };
3094
3095 let scrolled: Vec<A::Message> = {
3100 let grain =
3101 |id| find_widget(tree, id).map_or(0.0, |widget| widget.scroll_grain());
3102 self.runtime
3103 .scroll_changes(&paged, grain)
3104 .into_iter()
3105 .filter_map(|(id, position)| {
3106 find_widget(tree, id).and_then(|widget| widget.on_scroll(position))
3107 })
3108 .collect()
3109 };
3110
3111 for message in dismissed.into_iter().chain(turned).chain(scrolled) {
3115 self.dispatch(message);
3116 }
3117 self.sync_regions(false);
3120 if moved_late || sheeted_late {
3121 self.request_redraw();
3124 }
3125
3126 self.reconcile_focus();
3130
3131 #[cfg(desktop)]
3133 if let Some(a11y) = self.a11y.as_mut() {
3134 let focus = self.runtime.input.focused;
3135 let title = self.app.title();
3136 if let Some(ui) = self.ui.as_ref() {
3137 a11y.update(ui.semantics(), focus, &title, &self.announce);
3138 }
3139 }
3140 #[cfg(web)]
3142 if let Some(a11y) = self.a11y.as_mut() {
3143 let focus = self.runtime.input.focused;
3144 let title = self.app.title();
3145 if let Some(ui) = self.ui.as_ref() {
3146 a11y.update(ui.semantics(), focus, &title, &self.announce);
3147 }
3148 }
3149
3150 self.sync_system_bars(&theme);
3152
3153 self.sync_soft_input();
3155 #[cfg(android)]
3157 self.sync_autofill();
3158
3159 if animating || wants_animation {
3161 self.request_redraw();
3162 }
3163 if let Some(start) = frame_start {
3164 use crate::frame_stats::Why;
3165 let why = [
3166 (app_animating, Why::APP),
3167 (animating, Why::RUNTIME),
3168 (widget_asks, Why::WIDGET),
3169 (loading, Why::LOADING),
3170 (rehovered || revealed, Why::POINTER),
3171 ];
3172 self.frame_costs.why = why
3173 .into_iter()
3174 .filter(|(on, _)| *on)
3175 .fold(Why::NONE, |all, (_, why)| all | why);
3176 self.frame_stats.frame(start, self.frame_costs);
3177 if self.debug.performance {
3178 if let Some((_, before)) = self.debug.last_costs {
3179 let gap = start.saturating_duration_since(before).as_secs_f32();
3180 if gap > 0.0 {
3181 self.debug.fps = 1.0 / gap;
3182 }
3183 }
3184 if self.debug.last_costs.is_none() {
3186 self.request_redraw();
3187 }
3188 self.debug.last_costs = Some((self.frame_costs, start));
3189 self.debug.history.push(&self.frame_costs);
3190 }
3191 }
3192 #[cfg(windows)]
3194 self.sync_title_bar();
3195 }
3196
3197 _ => {}
3198 }
3199 }
3200}
3201
3202#[derive(Default)]
3204struct DebugAsks {
3205 outlines: bool,
3207 focus_dump: bool,
3209 semantics_dump: bool,
3211 brightness: Option<frus_widgets::Brightness>,
3213 performance: bool,
3215 last_costs: Option<(crate::frame_stats::FrameCosts, Instant)>,
3217 history: FrameHistory,
3219 fps: f32,
3221 #[cfg_attr(not(desktop), allow(dead_code))]
3223 screenshot: Option<std::path::PathBuf>,
3224 #[cfg_attr(not(web), allow(dead_code))]
3226 page_screenshot: bool,
3227}
3228
3229const PLATFORM_CYCLE: [frus_widgets::TargetPlatform; 6] = [
3231 frus_widgets::TargetPlatform::Android,
3232 frus_widgets::TargetPlatform::Ios,
3233 frus_widgets::TargetPlatform::Windows,
3234 frus_widgets::TargetPlatform::MacOs,
3235 frus_widgets::TargetPlatform::Linux,
3236 frus_widgets::TargetPlatform::Fuchsia,
3237];
3238
3239fn next_platform(current: frus_widgets::TargetPlatform) -> frus_widgets::TargetPlatform {
3241 let at = PLATFORM_CYCLE
3242 .iter()
3243 .position(|p| *p == current)
3244 .unwrap_or(0);
3245 PLATFORM_CYCLE[(at + 1) % PLATFORM_CYCLE.len()]
3246}
3247
3248fn dump_focus(
3250 nodes: &[frus_widgets::InspectorNode],
3251 focusable: &[WidgetId],
3252 focused: Option<WidgetId>,
3253) -> String {
3254 let mut out = String::new();
3255 for id in focusable {
3256 let node = nodes.iter().rev().find(|n| n.id == *id);
3257 let mark = if Some(*id) == focused { "▶ " } else { " " };
3258 match node {
3259 Some(n) => out.push_str(&format!(
3260 "{mark}{} {:.0},{:.0} {:.0}×{:.0} #{:08x}\n",
3261 n.name,
3262 n.rect.x,
3263 n.rect.y,
3264 n.rect.width,
3265 n.rect.height,
3266 id.as_u64() as u32
3267 )),
3268 None => out.push_str(&format!("{mark}#{:08x}\n", id.as_u64() as u32)),
3269 }
3270 }
3271 if out.is_empty() {
3272 out.push_str(" (nothing focusable)\n");
3273 }
3274 out
3275}
3276
3277fn dump_semantics(nodes: &[(WidgetId, Rect, frus_widgets::SemanticsProperties)]) -> String {
3279 let mut out = String::new();
3280 for (_, rect, props) in nodes {
3281 out.push_str(&format!(" {:?}", props.role));
3282 if let Some(label) = &props.label {
3283 out.push_str(&format!(" \"{label}\""));
3284 }
3285 if let Some(value) = &props.value {
3286 out.push_str(&format!(" = {value}"));
3287 }
3288 if props.clickable {
3289 out.push_str(" clickable");
3290 }
3291 if props.disabled {
3292 out.push_str(" disabled");
3293 }
3294 out.push_str(&format!(
3295 " {:.0},{:.0} {:.0}×{:.0}\n",
3296 rect.x, rect.y, rect.width, rect.height
3297 ));
3298 }
3299 if out.is_empty() {
3300 out.push_str(" (empty)\n");
3301 }
3302 out
3303}
3304
3305#[allow(clippy::too_many_arguments)]
3308fn paint_performance(
3309 scene: &mut frus_widgets::Scene,
3310 costs: Option<&crate::frame_stats::FrameCosts>,
3311 history: &FrameHistory,
3312 fps: f32,
3313 width: f32,
3314 top: f32,
3315 scale: f32,
3316) {
3317 let lines = match costs {
3318 Some(costs) => performance_lines(costs, fps),
3319 None => vec!["measuring…".to_string()],
3320 };
3321 paint_panel(scene, lines, history, width, top, scale);
3322}
3323
3324const HISTORY: usize = 90;
3326const BUDGET_MS: f32 = 1000.0 / 60.0;
3328const GRAPH_MS: f32 = BUDGET_MS * 2.0;
3330
3331#[derive(Default)]
3334struct FrameHistory {
3335 ui: std::collections::VecDeque<f32>,
3336 render: std::collections::VecDeque<f32>,
3337}
3338
3339impl FrameHistory {
3340 fn push(&mut self, costs: &crate::frame_stats::FrameCosts) {
3341 let ui = costs.build.unwrap_or(0.0) + costs.layout_paint;
3342 for (list, value) in [(&mut self.ui, ui), (&mut self.render, costs.render)] {
3343 if list.len() == HISTORY {
3344 list.pop_front();
3345 }
3346 list.push_back(value);
3347 }
3348 }
3349
3350 fn bars(values: &std::collections::VecDeque<f32>, height: f32) -> Vec<(f32, bool)> {
3353 values
3354 .iter()
3355 .map(|ms| ((ms / GRAPH_MS).min(1.0) * height, *ms > BUDGET_MS))
3356 .collect()
3357 }
3358}
3359
3360fn on_top_of(scene: &Scene) -> Scene {
3364 let mut over = Scene::new();
3365 over.layer(1.0, |inner| {
3366 for primitive in scene.primitives() {
3367 inner.push_primitive(primitive.clone());
3368 }
3369 });
3370 over
3371}
3372
3373fn performance_lines(costs: &crate::frame_stats::FrameCosts, fps: f32) -> Vec<String> {
3375 let build = costs
3376 .build
3377 .map_or_else(|| "kept".to_string(), |ms| format!("{ms:.1} ms"));
3378 let mut lines = vec![
3379 if fps > 0.0 {
3381 format!("{fps:.0} fps")
3382 } else {
3383 "idle".to_string()
3384 },
3385 format!("build {build} layout+paint {:.1} ms", costs.layout_paint),
3386 format!(
3387 "render {:.1} ms (acquire {:.1}, draw {:.1})",
3388 costs.render, costs.acquire, costs.draw
3389 ),
3390 ];
3391 if let Some(gpu) = costs.gpu {
3392 lines.push(format!("GPU {gpu:.1} ms"));
3393 }
3394 lines
3395}
3396
3397fn paint_panel(
3400 scene: &mut frus_widgets::Scene,
3401 lines: Vec<String>,
3402 history: &FrameHistory,
3403 width: f32,
3404 top: f32,
3405 scale: f32,
3406) {
3407 let size = 12.0 * scale;
3408 let style = frus_widgets::TextStyle::new(size).resolved();
3409 let pad = 6.0 * scale;
3410 let line_h = size * 1.4;
3411 let longest = lines
3414 .iter()
3415 .map(|line| line.chars().count())
3416 .max()
3417 .unwrap_or(0);
3418 let bar_w = 2.0 * scale;
3420 let graph_w = HISTORY as f32 * bar_w;
3421 let graph_h = 36.0 * scale;
3422 let graphs_h = if history.ui.is_empty() {
3423 0.0
3424 } else {
3425 2.0 * (graph_h + line_h) + pad
3426 };
3427 let text_w = (longest as f32 * size * 0.6).max(graph_w);
3428 let panel = Rect::new(
3429 (width - text_w - pad * 3.0).max(0.0),
3430 top + pad,
3431 text_w + pad * 2.0,
3432 line_h * lines.len() as f32 + pad * 2.0 + graphs_h,
3433 );
3434 scene.set_clip(Rect::UNBOUNDED);
3435 scene.set_owner(0);
3436 scene.draw_rect(
3438 panel,
3439 frus_widgets::Color::rgb(0.05, 0.05, 0.05),
3440 4.0 * scale,
3441 0.0,
3442 frus_widgets::Color::TRANSPARENT,
3443 );
3444 let count = lines.len();
3445 for (i, line) in lines.into_iter().enumerate() {
3446 scene.text(
3447 frus_widgets::Point::new(panel.x + pad, panel.y + pad + line_h * i as f32),
3448 line,
3449 &style,
3450 frus_widgets::Color::rgb(0.55, 1.0, 0.55),
3451 );
3452 }
3453 if history.ui.is_empty() {
3454 return;
3455 }
3456 let mut y = panel.y + pad + line_h * count as f32 + pad;
3457 for (title, values) in [
3458 ("interface (build, layout, paint)", &history.ui),
3459 ("drawing (encode, submit, present)", &history.render),
3460 ] {
3461 scene.text(
3462 frus_widgets::Point::new(panel.x + pad, y),
3463 title.to_string(),
3464 &style,
3465 frus_widgets::Color::rgb(0.75, 0.75, 0.75),
3466 );
3467 y += line_h;
3468 let area = Rect::new(panel.x + pad, y, graph_w, graph_h);
3469 scene.draw_rect(
3470 area,
3471 frus_widgets::Color::rgb(0.12, 0.12, 0.12),
3472 0.0,
3473 0.0,
3474 frus_widgets::Color::TRANSPARENT,
3475 );
3476 let bars = FrameHistory::bars(values, graph_h);
3478 let start = area.x + area.width - bars.len() as f32 * bar_w;
3479 for (i, (h, over)) in bars.into_iter().enumerate() {
3480 if h <= 0.0 {
3481 continue;
3482 }
3483 let color = if over {
3484 frus_widgets::Color::rgb(1.0, 0.35, 0.3)
3485 } else {
3486 frus_widgets::Color::rgb(0.4, 0.85, 0.45)
3487 };
3488 scene.fill_rect(
3489 Rect::new(
3490 start + i as f32 * bar_w,
3491 area.y + area.height - h,
3492 bar_w * 0.8,
3493 h,
3494 ),
3495 color,
3496 );
3497 }
3498 let budget_y = area.y + area.height * (1.0 - BUDGET_MS / GRAPH_MS);
3500 scene.fill_rect(
3501 Rect::new(area.x, budget_y, area.width, scale.max(1.0)),
3502 frus_widgets::Color::rgb(0.95, 0.95, 0.95),
3503 );
3504 y += graph_h + pad / 2.0;
3505 }
3506}
3507
3508#[cfg(desktop)]
3511fn write_screenshot(scene: &Scene, width: f32, height: f32, path: &std::path::Path) {
3512 let (w, h) = (
3513 width.round().max(1.0) as u32,
3514 height.round().max(1.0) as u32,
3515 );
3516 let Some(frame) = frus_gpu::render_offscreen(scene, w, h, frus_gpu::backdrop()) else {
3517 log::warn!(target: "frus::dev", "screenshot: no GPU to draw it with");
3518 return;
3519 };
3520 let png = crate::dev_commands::png(frame.width, frame.height, &frame.rgba);
3521 match std::fs::write(path, png) {
3522 Ok(()) => log::debug!(
3523 target: "frus::dev",
3524 "screenshot {}×{} written to {}",
3525 frame.width,
3526 frame.height,
3527 path.display()
3528 ),
3529 Err(error) => log::warn!(target: "frus::dev", "screenshot: {}: {error}", path.display()),
3530 }
3531}
3532
3533impl<A: Application> App<A> {
3534 fn rebuild_renderer(&mut self) {
3540 let see_through = self.renderer.as_ref().is_some_and(Renderer::sees_through);
3541 self.renderer = None;
3543 let Some(window) = self.window.clone() else {
3544 return;
3545 };
3546 let size = window.inner_size();
3547 let (width, height) = (size.width.max(1), size.height.max(1));
3548 log::warn!("the GPU device was lost: drawing again with a new one");
3549 #[cfg(not(web))]
3550 {
3551 let made = if see_through {
3552 pollster::block_on(Renderer::see_through(window.clone(), width, height))
3553 } else {
3554 pollster::block_on(Renderer::new(window.clone(), width, height))
3555 };
3556 match made {
3557 Ok(renderer) => self.renderer = Some(renderer),
3558 Err(err) => {
3559 log::error!("the GPU device was lost, and no new one could be made: {err:#}")
3560 }
3561 }
3562 }
3563 #[cfg(web)]
3564 {
3565 let _ = see_through;
3566 let slot = self.pending_renderer.clone();
3567 let win = window.clone();
3568 wasm_bindgen_futures::spawn_local(async move {
3569 match Renderer::new(win.clone(), width, height).await {
3570 Ok(renderer) => {
3571 *slot.borrow_mut() = Some(renderer);
3572 win.request_redraw();
3573 }
3574 Err(err) => {
3575 log::error!(
3576 "the GPU device was lost, and no new one could be made: {err:#}"
3577 )
3578 }
3579 }
3580 });
3581 }
3582 self.build_dirty = true;
3583 window.request_redraw();
3584 }
3585
3586 fn take_dev_commands(&mut self) {
3587 for command in crate::dev_commands::take_commands() {
3588 let (name, argument) = command
3589 .split_once(' ')
3590 .map_or((command.as_str(), ""), |(n, a)| (n, a.trim()));
3591 let on_off = |on: bool| if on { "on" } else { "off" };
3592 match name {
3593 "inspector" => {
3594 self.inspector = !self.inspector;
3595 self.inspector_dump = self.inspector;
3596 log::info!(target: "frus::dev", "inspector {}", on_off(self.inspector));
3597 }
3598 "outlines" => {
3599 self.debug.outlines = !self.debug.outlines;
3600 log::info!(target: "frus::dev", "debug paint {}", on_off(self.debug.outlines));
3601 }
3602 "widgets" => self.inspector_dump = true,
3603 "focus" => self.debug.focus_dump = true,
3604 "semantics" => self.debug.semantics_dump = true,
3605 "platform" => {
3606 let next = next_platform(frus_widgets::default_target_platform());
3607 if cfg!(debug_assertions) {
3608 frus_widgets::set_debug_default_target_platform_override(Some(next));
3609 log::info!(target: "frus::dev", "target platform: {next:?}");
3610 } else {
3611 log::warn!(target: "frus::dev", "the platform can be switched in a debug build only");
3612 }
3613 }
3614 "brightness" => {
3615 let now = self.debug.brightness.unwrap_or(self.platform.brightness);
3616 let next = match now {
3617 frus_widgets::Brightness::Light => frus_widgets::Brightness::Dark,
3618 frus_widgets::Brightness::Dark => frus_widgets::Brightness::Light,
3619 };
3620 self.debug.brightness = Some(next);
3621 log::info!(
3622 target: "frus::dev",
3623 "brightness: {next:?} (an application without a theme for it stays as it is)"
3624 );
3625 }
3626 "performance" => {
3627 self.debug.performance = !self.debug.performance;
3628 self.debug.last_costs = None;
3629 self.debug.history = FrameHistory::default();
3630 self.frame_stats.set_enabled(self.debug.performance);
3631 self.request_redraw();
3633 log::info!(target: "frus::dev", "performance panel {}", on_off(self.debug.performance));
3634 }
3635 "trim-memory" => memory_trimmed(),
3637 "lose-gpu" => {
3639 if let Some(renderer) = self.renderer.as_ref() {
3640 log::warn!(target: "frus::dev", "losing the GPU device, as a driver reset would");
3641 renderer.lose_device_for_testing();
3642 self.request_redraw();
3643 }
3644 }
3645 "screenshot" if !argument.is_empty() => {
3646 #[cfg(desktop)]
3647 {
3648 self.debug.screenshot = Some(std::path::PathBuf::from(argument));
3649 }
3650 #[cfg(web)]
3651 {
3652 self.debug.page_screenshot = true;
3653 }
3654 #[cfg(android)]
3655 log::warn!(target: "frus::dev", "the application takes no screenshot of itself here");
3656 }
3657 _ => {
3658 log::warn!(target: "frus::dev", "frus run asked for `{command}`, which this frus does not know")
3659 }
3660 }
3661 self.build_dirty = true;
3662 }
3663 }
3664
3665 fn brightness(&self) -> frus_widgets::Brightness {
3667 self.debug.brightness.unwrap_or(self.platform.brightness)
3668 }
3669
3670 fn system_line(&self) -> f32 {
3673 match self.title_bar {
3675 Some(line) if self.title_bar_on && line.system_paints => {
3676 line.height * self.total_scale()
3677 }
3678 _ => 0.0,
3679 }
3680 }
3681
3682 fn total_scale(&self) -> f32 {
3684 (self.scale * self.app.density()).max(0.1)
3685 }
3686
3687 fn media_query(&self, width: f32, height: f32) -> MediaQuery {
3703 MediaQuery::new(Size::new(width, height))
3704 .with_device_pixel_ratio(self.scale)
3705 .with_density(self.app.density())
3706 .with_text_scaler(self.platform.text_scaler)
3707 .with_platform_brightness(self.brightness())
3708 .with_accessibility(
3709 self.platform
3710 .accessibility
3711 .with_overrides(self.app.accessibility()),
3712 )
3713 .with_insets(self.last_insets)
3714 .with_title_bar(self.title_bar)
3715 }
3716
3717 #[cfg(windows)]
3721 fn sync_title_bar(&mut self) {
3722 use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
3723 let Some(window) = self.window.clone() else {
3724 return;
3725 };
3726 let hwnd = match window.window_handle().map(|h| h.as_raw()) {
3727 Ok(RawWindowHandle::Win32(win)) => {
3728 win.hwnd.get() as windows_sys::Win32::Foundation::HWND
3729 }
3730 _ => return,
3731 };
3732 let wants = self.ui.as_ref().is_some_and(|ui| ui.wants_title_bar());
3733 if wants != self.title_bar_on {
3734 unsafe {
3736 if wants {
3737 crate::title_bar::enable(hwnd);
3738 let wake = window.clone();
3739 crate::title_bar::on_change(move || wake.request_redraw());
3740 } else {
3741 crate::title_bar::disable(hwnd);
3742 }
3743 }
3744 self.title_bar_on = wants;
3745 }
3746 let next = if self.title_bar_on {
3747 let scale = self.total_scale();
3748 let band = crate::title_bar::height() as f32 / scale;
3749 if let Some(ui) = &self.ui {
3751 let to_px = |r: frus_widgets::Rect| windows_sys::Win32::Foundation::RECT {
3752 left: (r.x * scale).floor() as i32,
3753 top: (r.y * scale).floor() as i32,
3754 right: ((r.x + r.width) * scale).ceil() as i32,
3755 bottom: ((r.y + r.height) * scale).ceil() as i32,
3756 };
3757 let interactive = ui
3758 .hit_rects()
3759 .filter(|r| r.y < band && r.width < self.logical_width())
3760 .map(to_px)
3761 .collect();
3762 let icon = ui
3763 .title_bar_regions()
3764 .iter()
3765 .find(|(_, role)| *role == frus_widgets::TitleBarRole::Icon)
3766 .map(|(r, _)| to_px(*r));
3767 crate::title_bar::publish(interactive, icon);
3768 }
3769 let buttons = unsafe { crate::title_bar::buttons(hwnd) }.map(|r| {
3771 frus_widgets::Rect::new(
3772 r.left as f32 / scale,
3773 r.top as f32 / scale,
3774 (r.right - r.left) as f32 / scale,
3775 (r.bottom - r.top) as f32 / scale,
3776 )
3777 });
3778 let button = |code: u32| {
3779 use windows_sys::Win32::UI::WindowsAndMessaging::{
3780 HTCLOSE, HTMAXBUTTON, HTMINBUTTON,
3781 };
3782 match code {
3783 HTMINBUTTON => Some(frus_widgets::CaptionButton::Minimize),
3784 HTMAXBUTTON => Some(frus_widgets::CaptionButton::Maximize),
3785 HTCLOSE => Some(frus_widgets::CaptionButton::Close),
3786 _ => None,
3787 }
3788 };
3789 let (hovered, pressed) = crate::title_bar::buttons_state();
3790 if self.window_icon.is_none() {
3791 self.window_icon = self
3792 .app
3793 .icon()
3794 .png()
3795 .map(|png| &*Box::leak(png.to_vec().into_boxed_slice()));
3796 }
3797 let active = window.has_focus();
3800 let dark = window.theme() == Some(winit::window::Theme::Dark);
3801 let (background, foreground) = crate::title_bar::caption_colors(dark, active);
3802 Some(frus_widgets::TitleBar {
3803 height: buttons.map_or(band, |b| (b.y + b.height).clamp(1.0, band)),
3807 leading: 0.0,
3808 buttons,
3809 icon: self.window_icon,
3810 hovered: button(hovered),
3811 pressed: button(pressed),
3812 maximized: window.is_maximized(),
3813 active,
3814 background: Some(background),
3815 foreground: Some(foreground),
3816 system_paints: self.renderer.as_ref().is_some_and(Renderer::sees_through)
3819 && unsafe { crate::title_bar::system_backdrop(hwnd, dark) },
3821 })
3822 } else {
3823 None
3824 };
3825 if next != self.title_bar {
3826 log::debug!(
3827 target: "frus::title_bar",
3828 "the line: {:?}",
3829 next.map(|line| (line.height, line.buttons, line.active, line.system_paints))
3830 );
3831 self.title_bar = next;
3832 self.build_dirty = true;
3833 self.request_redraw();
3834 }
3835 }
3836
3837 fn is_rtl(&self) -> bool {
3839 self.themes.displayed(&self.app).direction.is_rtl()
3840 }
3841
3842 fn logical_width(&self) -> f32 {
3844 let scale = self.total_scale();
3845 self.window
3846 .as_ref()
3847 .map(|w| w.inner_size().width as f32 / scale)
3848 .unwrap_or(1.0)
3849 .max(1.0)
3850 }
3851
3852 fn request_redraw(&self) {
3853 if let Some(window) = &self.window {
3854 window.request_redraw();
3855 }
3856 }
3857
3858 fn pointer(&mut self, event_loop: &ActiveEventLoop, event: PointerEvent) {
3862 self.pointer_event(event);
3863 event_loop.set_control_flow(self.idle_control_flow());
3865 }
3866
3867 fn pointer_event(&mut self, event: PointerEvent) {
3871 let kind = event.kind;
3872 self.cursor = event.position;
3875 self.route_listeners(kind, event.touch);
3876 self.route_pointer_event(event);
3877 self.pointer_held = match kind {
3878 PointerKind::Down => true,
3879 PointerKind::Up | PointerKind::Cancel => false,
3880 PointerKind::Move => self.pointer_held,
3881 };
3882 self.sync_regions(kind == PointerKind::Move);
3883 }
3884
3885 fn route_pointer_event(&mut self, event: PointerEvent) {
3887 self.cursor = event.position;
3888 self.hover.event(event.kind, event.position, event.touch);
3889 let could_tap = gesture_was_a_tap(self.drag.as_ref());
3892 match event.kind {
3893 PointerKind::Down => {
3894 self.sync_hover();
3897 self.runtime.focus_visible = false;
3899 self.pointer_down(event.touch);
3900 let (long_press, lift) = hold_candidates(
3901 self.drag.as_ref(),
3902 self.ui.as_ref(),
3903 self.tree.as_deref(),
3904 self.cursor,
3905 );
3906 self.long_press_msg = long_press;
3907 self.pending_lift = lift;
3908 let interested = self.long_press_msg.is_some()
3909 || self.pending_lift.is_some()
3910 || self.pending_reorder.is_some()
3911 || self.pending_word.is_some()
3912 || self.pending_region_hold;
3913 self.press.down(self.cursor, Instant::now(), interested);
3914 }
3915 PointerKind::Move => {
3916 self.press.moved(self.cursor);
3917 self.pointer_move();
3918 if self.inspector {
3921 self.request_redraw();
3922 }
3923 }
3924 PointerKind::Up => {
3925 let swallow = self.press.up();
3928 self.pending_lift = None;
3929 self.pending_reorder = None;
3930 self.pending_word = None;
3931 self.pending_region_hold = false;
3932 if swallow
3935 && !matches!(
3936 self.drag,
3937 Some(Drag::Item { moved: true, .. } | Drag::Reorder { moved: true, .. })
3938 )
3939 {
3940 self.drag = None;
3942 self.runtime.input.pressed = None;
3943 self.request_redraw();
3944 } else {
3945 self.pointer_up();
3946 }
3947 }
3948 PointerKind::Cancel => {
3949 self.press.cancel();
3950 self.pending_lift = None;
3951 self.pending_reorder = None;
3952 self.pending_word = None;
3953 self.pending_region_hold = false;
3954 if let Some(Drag::Dismiss { item, .. }) = self.drag {
3957 self.runtime.dismiss_release(
3959 item.id,
3960 0.0,
3961 0.0,
3962 item.spec.axis,
3963 f32::INFINITY,
3966 );
3967 }
3968 if let Some(Drag::Gesture { id, .. }) = self.drag {
3970 self.runtime.release_value(id);
3971 }
3972 if let Some(Drag::Scroll { id, .. }) = self.drag {
3973 self.runtime.release_scroll(id);
3974 self.runtime.glow_scroll_end(id);
3975 if let Some(host) = self.refresh_host_of(id) {
3978 self.runtime.refresh_cancel(host);
3979 }
3980 }
3981 let sheet = match self.drag {
3983 Some(Drag::Sheet { id, available, .. }) => self
3984 .ui
3985 .as_ref()
3986 .and_then(|ui| ui.sheet(id))
3987 .map(|sheet| (sheet.id, sheet.spec.clone(), available)),
3988 Some(Drag::Scroll { id, .. }) => self
3989 .ui
3990 .as_ref()
3991 .and_then(|ui| ui.sheet_holding(id))
3992 .map(|sheet| (sheet.id, sheet.spec.clone(), sheet.available)),
3993 _ => None,
3994 };
3995 if let Some((id, spec, available)) = sheet {
3996 self.runtime.sheet_release(id, &spec, available, 0.0, None);
3997 }
3998 self.drag = None;
3999 self.runtime.input.pressed = None;
4000 self.request_redraw();
4001 }
4002 }
4003 if settle_press(
4006 &mut self.runtime.input,
4007 event.kind,
4008 could_tap,
4009 self.drag.as_ref(),
4010 event.touch,
4011 ) {
4012 self.request_redraw();
4013 }
4014 if matches!(event.kind, PointerKind::Up | PointerKind::Cancel) {
4017 self.sync_hover();
4018 }
4019 }
4020
4021 fn hold_deadline_reached(&mut self) {
4023 let lifting = self.pending_lift.is_some() || self.pending_reorder.is_some();
4029 let word = self.pending_word.take();
4032 let region = std::mem::take(&mut self.pending_region_hold)
4035 && !lifting
4036 && word.is_none()
4037 && self.long_press_msg.is_none();
4038 if let Some(message) = self.long_press_msg.take() {
4039 if !lifting && word.is_none() {
4040 self.dispatch(message);
4041 }
4042 }
4043 if let Some(id) = word {
4044 self.select_held_word(id);
4045 }
4046 if region && self.select_region_word_held() {
4047 if let Some(Drag::Scroll { id, .. }) = self.drag {
4049 self.runtime.release_scroll(id);
4050 }
4051 }
4052 let lift = self.pending_lift.take();
4053 let reorder = self.pending_reorder.take();
4054 if lift.is_some() || reorder.is_some() {
4056 if let Some(Drag::Scroll { id, .. }) = self.drag {
4057 self.runtime.release_scroll(id);
4058 }
4059 }
4060 self.drag = drag_after_hold(self.drag.take(), lift, reorder, self.cursor);
4061 if matches!(self.drag, Some(Drag::Reorder { .. })) {
4062 self.reorder_x = self.cursor.x;
4063 self.reorder_y = self.cursor.y;
4064 }
4065 self.request_redraw();
4066 }
4067
4068 fn idle_control_flow(&self) -> ControlFlow {
4071 let press = self.press.deadline();
4072 let reload = self.reload.as_ref().map(|w| w.deadline());
4073 let caret = (self.lifecycle == Lifecycle::Resumed)
4076 .then(|| self.caret.next_toggle(Instant::now()))
4077 .flatten();
4078 let tap = self.pending_tap.as_ref().map(|p| p.at + DOUBLE_TAP_TIMEOUT);
4080 match [press, reload, caret, tap].into_iter().flatten().min() {
4081 Some(at) => ControlFlow::WaitUntil(at),
4082 None => ControlFlow::Wait,
4083 }
4084 }
4085
4086 fn caret_signature(&self) -> Option<crate::caret::Signature> {
4090 use std::hash::{Hash, Hasher};
4091 let field = self.runtime.input.focused?;
4092 let widget = find_widget(self.tree.as_ref()?.as_ref(), field)?;
4093 let text = widget.text_value()?;
4094 let mut hasher = std::collections::hash_map::DefaultHasher::new();
4095 text.hash(&mut hasher);
4096 Some(crate::caret::Signature {
4097 field,
4098 edit: self.runtime.edits.get(&field).copied().unwrap_or_default(),
4099 value: hasher.finish(),
4100 })
4101 }
4102
4103 fn pointer_move(&mut self) {
4106 if self.drag.is_some() {
4107 self.handle_drag();
4108 return;
4109 }
4110 let hovered = self.sync_hover();
4111 self.update_cursor_icon(hovered);
4115 }
4116
4117 fn sync_hover(&mut self) -> Option<WidgetId> {
4123 let hovered = self.ui.as_ref().and_then(|ui| self.hover.target(ui));
4124 if hovered != self.runtime.input.hovered {
4125 self.runtime.input.hovered = hovered;
4126 self.request_redraw();
4127 }
4128 hovered
4129 }
4130
4131 fn take_task_arrivals(&mut self) -> bool {
4134 let now = frus_widgets::task_arrivals();
4135 std::mem::replace(&mut self.task_arrivals, now) != now
4136 }
4137
4138 fn route_listeners(&mut self, kind: PointerKind, touch: bool) {
4142 use frus_widgets::ListenerEventKind as Raw;
4143 let at = self.cursor;
4144 let heard: Vec<(WidgetId, Rect, Raw)> = match kind {
4145 PointerKind::Down => {
4146 self.captured = self
4147 .ui
4148 .as_ref()
4149 .map(|ui| ui.pointer_listeners_at(at))
4150 .unwrap_or_default();
4151 self.captured
4152 .iter()
4153 .map(|(id, rect)| (*id, *rect, Raw::Down))
4154 .collect()
4155 }
4156 PointerKind::Move if self.pointer_held => self
4157 .captured
4158 .iter()
4159 .map(|(id, rect)| (*id, *rect, Raw::Move))
4160 .collect(),
4161 PointerKind::Move if !touch => self
4162 .ui
4163 .as_ref()
4164 .map(|ui| ui.pointer_listeners_at(at))
4165 .unwrap_or_default()
4166 .into_iter()
4167 .map(|(id, rect)| (id, rect, Raw::Hover))
4168 .collect(),
4169 PointerKind::Move => Vec::new(),
4170 PointerKind::Up | PointerKind::Cancel => {
4171 let raw = if kind == PointerKind::Up {
4172 Raw::Up
4173 } else {
4174 Raw::Cancel
4175 };
4176 std::mem::take(&mut self.captured)
4177 .into_iter()
4178 .map(|(id, rect)| (id, rect, raw))
4179 .collect()
4180 }
4181 };
4182 let messages: Vec<A::Message> = heard
4183 .into_iter()
4184 .flat_map(|(id, rect, kind)| {
4185 let event = frus_widgets::ListenerEvent {
4186 kind,
4187 local: Point::new(at.x - rect.x, at.y - rect.y),
4188 touch,
4189 };
4190 self.tree
4191 .as_ref()
4192 .and_then(|tree| find_widget(tree.as_ref(), id))
4193 .map(|widget| widget.on_pointer_event(event))
4194 .unwrap_or_default()
4195 })
4196 .collect();
4197 for message in messages {
4198 self.dispatch(message);
4199 self.request_redraw();
4200 }
4201 }
4202
4203 fn sync_regions(&mut self, moved: bool) {
4207 let point = self.hover.at();
4208 let now: Vec<(WidgetId, Rect)> = match (point, self.ui.as_ref()) {
4209 (Some(point), Some(ui)) => ui
4210 .hover_regions_at(point)
4211 .into_iter()
4212 .map(|(id, rect, _)| (id, rect))
4213 .collect(),
4214 _ => Vec::new(),
4215 };
4216 let at = point.unwrap_or(self.cursor);
4217 let local = |rect: Rect| Point::new(at.x - rect.x, at.y - rect.y);
4218 let before = std::mem::replace(&mut self.entered, now.clone());
4219 let mut events = Vec::new();
4220 for (id, rect) in &before {
4221 if !now.iter().any(|(n, _)| n == id) {
4222 events.push((
4223 *id,
4224 frus_widgets::HoverEvent::Exit {
4225 local: local(*rect),
4226 },
4227 ));
4228 }
4229 }
4230 for (id, rect) in now.iter().rev() {
4231 if !before.iter().any(|(b, _)| b == id) {
4232 events.push((
4233 *id,
4234 frus_widgets::HoverEvent::Enter {
4235 local: local(*rect),
4236 },
4237 ));
4238 } else if moved && !self.pointer_held {
4239 events.push((
4240 *id,
4241 frus_widgets::HoverEvent::Move {
4242 local: local(*rect),
4243 },
4244 ));
4245 }
4246 }
4247 let messages: Vec<_> = events
4248 .into_iter()
4249 .filter_map(|(id, event)| {
4250 self.tree
4251 .as_ref()
4252 .and_then(|tree| find_widget(tree.as_ref(), id))
4253 .and_then(|widget| widget.on_hover_event(event))
4254 })
4255 .collect();
4256 for message in messages {
4257 self.dispatch(message);
4258 self.request_redraw();
4259 }
4260 }
4261
4262 fn region_cursor(&self, requested: Option<UiCursor>) -> UiCursor {
4267 requested
4268 .or_else(|| {
4269 let point = self.hover.at()?;
4270 let (id, _) = self.ui.as_ref()?.focus_hit(point)?;
4271 let widget = find_widget(self.tree.as_ref()?.as_ref(), id)?;
4272 is_text_field(widget).then_some(UiCursor::Text)
4273 })
4274 .or_else(|| {
4275 self.hover.at().and_then(|point| {
4276 self.ui
4277 .as_ref()?
4278 .hover_regions_at(point)
4279 .into_iter()
4280 .find_map(|(_, _, region)| region.cursor)
4281 })
4282 })
4283 .unwrap_or(UiCursor::Default)
4284 }
4285
4286 fn update_cursor_icon(&mut self, hovered: Option<WidgetId>) {
4290 let requested = hovered.and_then(|id| {
4291 let rect = self.ui.as_ref()?.widget_rect(id)?;
4292 let widget = find_widget(self.tree.as_ref()?.as_ref(), id)?;
4293 widget.cursor_icon(
4294 self.cursor.x - rect.x,
4295 self.cursor.y - rect.y,
4296 rect.width,
4297 rect.height,
4298 )
4299 });
4300 let shown = self.region_cursor(requested);
4301 if let Some(window) = &self.window {
4302 window.set_cursor_visible(shown != UiCursor::None);
4303 window.set_cursor(cursor_icon(shown));
4304 }
4305 let hover_cursor = requested.map(|_| self.cursor);
4310 if hover_cursor != self.runtime.input.hover_cursor {
4311 self.runtime.input.hover_cursor = hover_cursor;
4312 self.request_redraw();
4313 }
4314 }
4315
4316 fn pointer_down(&mut self, touch: bool) {
4319 self.pointer_touch = touch;
4320 self.poll_pending_tap(Instant::now());
4323 if let Some(pending) = self.pending_tap.as_ref() {
4324 let here = self.ui.as_ref().and_then(|ui| ui.hit(self.cursor));
4325 let (dx, dy) = (
4326 self.cursor.x - pending.position.x,
4327 self.cursor.y - pending.position.y,
4328 );
4329 if here != Some(pending.id) || dx.hypot(dy) > DOUBLE_TAP_SLOP {
4330 self.flush_pending_tap();
4331 }
4332 }
4333 self.pending_word = None;
4334 self.pending_region_hold = false;
4335 if self
4341 .ui
4342 .as_ref()
4343 .is_some_and(|ui| ui.toolbar_contains(self.cursor))
4344 {
4345 press_at(&mut self.runtime, self.ui.as_ref(), self.cursor);
4346 self.request_redraw();
4347 return;
4348 }
4349 if let Some(drag) = self.grab_region_handle() {
4355 self.drag = Some(drag);
4356 if let Some(selection) = self.runtime.region.as_mut() {
4357 selection.bar = false;
4358 }
4359 self.request_redraw();
4360 return;
4361 }
4362 if let Some(drag) = self.grab_selection_handle() {
4363 self.drag = Some(drag);
4364 self.runtime.selection_toolbar = None;
4367 self.request_redraw();
4368 return;
4369 }
4370 if self.runtime.selection_handles.take().is_some() {
4371 self.request_redraw();
4372 }
4373 self.hide_selection_toolbar();
4374 if self.runtime.region.take().is_some() {
4377 self.request_redraw();
4378 }
4379 let above_the_pages = self.ui.as_ref().is_some_and(|ui| {
4384 ui.over_overlay(self.cursor)
4385 || ui
4386 .title_bar_row()
4387 .is_some_and(|row| row.contains(self.cursor))
4388 });
4389 let on_back_edge = !above_the_pages
4390 && if self.is_rtl() {
4391 self.cursor.x > self.logical_width() - BACK_EDGE
4392 } else {
4393 self.cursor.x < BACK_EDGE
4394 };
4395 if on_back_edge && self.app.can_go_back() {
4396 self.drag = Some(Drag::Back {
4397 start_x: self.cursor.x,
4398 });
4399 self.begin_gesture();
4400 self.build_dirty = true;
4401 self.app.back_gesture(0.0);
4402 self.request_redraw();
4403 return;
4404 }
4405
4406 if let Some(bar) = self.ui.as_ref().and_then(|ui| ui.scrollbar_at(self.cursor)) {
4408 let (along, thumb_start) = if bar.vertical {
4409 (self.cursor.y, bar.thumb.y)
4410 } else {
4411 (self.cursor.x, bar.thumb.x)
4412 };
4413 self.drag = Some(Drag::Scrollbar {
4414 id: bar.id,
4415 vertical: bar.vertical,
4416 grab: along - thumb_start,
4417 track_start: bar.track_start,
4418 track_len: bar.track_len,
4419 thumb_len: bar.thumb_len,
4420 max: bar.max,
4421 reverse: bar.reverse,
4422 });
4423 self.request_redraw();
4424 return;
4425 }
4426
4427 if let Some((id, rect)) = self.ui.as_ref().and_then(|ui| ui.draggable_at(self.cursor)) {
4429 let (x, rtl) = (self.cursor.x, self.is_rtl());
4430 let track = self
4431 .tree
4432 .as_ref()
4433 .and_then(|tree| find_widget(tree.as_ref(), id))
4434 .map_or(frus_widgets::DragTrack::WHOLE, |widget| widget.drag_track());
4435 let active = track.engages(rect, x, rtl);
4436 let thumbs = matches!(track.mode, frus_widgets::DragMode::Thumbs { .. });
4437 let thumb = track.pick_thumb(rect, x, 0.0, rtl);
4438 self.drag = Some(Drag::Widget {
4439 id,
4440 rect,
4441 last_x: x,
4442 press_x: x,
4443 track,
4444 active,
4445 thumb,
4446 });
4447 if thumbs {
4448 if let Some(thumb) = thumb {
4450 let at = track.fraction(rect, x, rtl);
4451 self.thumb_drag(id, thumb, frus_widgets::ThumbDrag::Start, at);
4452 self.thumb_drag(id, thumb, frus_widgets::ThumbDrag::Move, at);
4453 }
4454 } else if active {
4455 self.dispatch_drag_edge(id, Edge::Start, 0.0);
4460 self.apply_widget_drag(id, 0.0, track.value_at(rect, x, x, false, rtl));
4462 }
4463 self.request_redraw();
4464 return;
4465 }
4466
4467 if let Some((id, from, hold)) = self.reorderable_at(self.cursor) {
4476 if hold {
4477 self.pending_reorder = Some((id, from, self.cursor));
4478 } else {
4479 self.drag = Some(Drag::Reorder {
4480 id,
4481 from,
4482 start: self.cursor,
4483 moved: false,
4484 carried: None,
4485 });
4486 self.reorder_x = self.cursor.x; self.reorder_y = self.cursor.y; }
4489 }
4490
4491 if press_at(&mut self.runtime, self.ui.as_ref(), self.cursor) {
4492 self.request_redraw();
4493 }
4494 let previously_focused = self.runtime.input.focused;
4496 let focus = self.ui.as_ref().and_then(|ui| ui.focus_hit(self.cursor));
4497 if let Some(held) = previously_focused {
4500 let inside = focus.is_some_and(|(id, _)| id == held);
4501 let outside = (!inside)
4502 .then(|| {
4503 self.tree
4504 .as_ref()
4505 .and_then(|tree| find_widget(tree.as_ref(), held))
4506 .and_then(|widget| widget.on_tap_outside())
4507 })
4508 .flatten();
4509 if let Some(message) = outside {
4510 self.dispatch(message);
4511 }
4512 }
4513 self.runtime.input.focused = focus.map(|(id, _)| id);
4514 if let Some((id, rect)) = focus {
4515 let local_x = self.cursor.x - rect.x;
4516 let local_y =
4519 self.cursor.y - rect.y + self.runtime.scroll.get(&id).map(|s| s.1).unwrap_or(0.0);
4520 let scroll_cursor = if previously_focused == Some(id) {
4523 self.runtime.edits.get(&id).map(|e| e.cursor).unwrap_or(0)
4524 } else {
4525 0
4526 };
4527 let cursor = self
4531 .tree
4532 .as_ref()
4533 .and_then(|tree| find_widget(tree.as_ref(), id))
4534 .and_then(|widget| widget.cursor_at(local_x, local_y, rect.width, scroll_cursor));
4535 if let Some(cursor) = cursor {
4536 self.goal_x = None;
4538 self.runtime.edits.insert(
4539 id,
4540 Edit {
4541 cursor,
4542 anchor: None,
4543 composing: None,
4544 },
4545 );
4546 self.drag = Some(Drag::TextSelect { id, rect });
4547 if touch {
4549 self.pending_word = Some(id);
4550 }
4551 self.runtime.close_edit_run(id);
4554 self.request_soft_input();
4558
4559 let now = Instant::now();
4561 let double = self
4562 .last_click_time
4563 .map(|t| (now - t).as_secs_f32() < 0.4)
4564 .unwrap_or(false);
4565 self.last_click_time = Some(now);
4566 if double {
4567 if let Some((start, end)) = self
4568 .tree
4569 .as_ref()
4570 .and_then(|tree| find_widget(tree.as_ref(), id))
4571 .and_then(|widget| widget.word_at(cursor))
4572 {
4573 self.runtime.edits.insert(
4574 id,
4575 Edit {
4576 cursor: end,
4577 anchor: Some(start),
4578 composing: None,
4579 },
4580 );
4581 self.drag = None;
4582 }
4583 }
4584 }
4585 }
4586
4587 if self.drag.is_none() && focus.is_none() {
4591 if touch {
4592 self.pending_region_hold = self.region_press_target().is_some();
4593 } else {
4594 self.begin_region_selection();
4595 }
4596 }
4597
4598 if touch && self.drag.is_none() {
4602 let landed = {
4607 let scroll = &self.runtime.scroll;
4608 self.ui.as_ref().and_then(|ui| {
4609 ui.scroll_chain(self.cursor).find(|area| {
4610 area.accepts_user_offset(
4611 scroll.get(&area.id).copied().unwrap_or((0.0, 0.0)),
4612 )
4613 })
4614 })
4615 };
4616 if let Some(area) = landed {
4617 let physics = area.physics;
4621 let carried = self.runtime.catch_scroll_fling(area.id, physics);
4622 self.runtime.hold_scroll(area.id);
4624 if let Some(sheet) = self
4628 .ui
4629 .as_ref()
4630 .and_then(|ui| ui.sheet_holding(area.id))
4631 .map(|sheet| sheet.id)
4632 {
4633 self.runtime.sheet_hold(sheet);
4634 }
4635 self.drag = Some(Drag::Scroll {
4636 id: area.id,
4637 last: self.cursor,
4638 moved: false,
4639 carried,
4640 dismiss: self
4641 .ui
4642 .as_ref()
4643 .and_then(|ui| ui.dismissable_at(self.cursor)),
4644 axis: None,
4645 pan: self.pan_target(),
4646 });
4647 self.begin_gesture();
4648 self.gesture_velocity = area.velocity_tracker();
4651 }
4652 }
4653
4654 if self.drag.is_none() {
4657 if let Some((id, available)) = self
4658 .ui
4659 .as_ref()
4660 .and_then(|ui| ui.sheet_at(self.cursor))
4661 .map(|sheet| (sheet.id, sheet.available))
4662 {
4663 self.runtime.sheet_hold(id);
4664 self.drag = Some(Drag::Sheet {
4665 id,
4666 last: self.cursor,
4667 moved: false,
4668 available,
4669 });
4670 self.begin_gesture();
4671 }
4672 }
4673
4674 if self.drag.is_none() {
4678 if let Some(item) = self
4679 .ui
4680 .as_ref()
4681 .and_then(|ui| ui.dismissable_at(self.cursor))
4682 {
4683 self.drag = Some(Drag::Dismiss {
4684 item,
4685 last: self.cursor,
4686 moved: false,
4687 });
4688 self.begin_gesture();
4689 }
4690 }
4691
4692 if self.drag.is_none() {
4698 if let Some(source) = self
4699 .ui
4700 .as_ref()
4701 .and_then(|ui| ui.drag_source_at(self.cursor))
4702 .filter(|source| {
4703 !self
4705 .tree
4706 .as_ref()
4707 .and_then(|tree| find_widget(tree.as_ref(), source.id))
4708 .is_some_and(|widget| widget.drag_needs_long_press())
4709 })
4710 {
4711 self.drag = Some(Drag::Item {
4712 source,
4713 start: self.cursor,
4714 moved: false,
4715 over: None,
4716 });
4717 self.begin_gesture();
4718 }
4719 }
4720
4721 if self.drag.is_none() {
4724 if let Some((id, axis)) = self.pan_target() {
4725 self.drag = Some(Drag::Gesture {
4726 id,
4727 axis,
4728 start: self.cursor,
4729 last: self.cursor,
4730 moved: false,
4731 });
4732 self.begin_gesture();
4733 }
4734 }
4735
4736 if self.drag.is_none() {
4740 if let Some((id, viewport)) = self
4741 .ui
4742 .as_ref()
4743 .and_then(|ui| ui.interactive_at(self.cursor))
4744 {
4745 self.runtime.interactive_velocity.remove(&id);
4747 self.drag = Some(Drag::Pan {
4748 id,
4749 last: self.cursor,
4750 moved: false,
4751 viewport,
4752 });
4753 self.begin_gesture();
4754 }
4755 }
4756 self.request_redraw();
4757 }
4758
4759 fn positional_click_on(&self, id: WidgetId) -> Option<A::Message> {
4765 let rect = self.ui.as_ref()?.widget_rect(id)?;
4766 let widget = find_widget(self.tree.as_ref()?.as_ref(), id)?;
4767 widget.positional_click(
4768 self.cursor.x - rect.x,
4769 self.cursor.y - rect.y,
4770 rect.width,
4771 rect.height,
4772 )
4773 }
4774
4775 fn pointer_up(&mut self) {
4776 let ended = self.drag.take();
4777 if let Some(Drag::Back { .. }) = ended {
4778 let sign = if self.is_rtl() { -1.0 } else { 1.0 };
4781 let velocity = sign * self.gesture_estimate().velocity.x / self.logical_width();
4782 self.build_dirty = true;
4783 self.app.back_gesture_end(velocity);
4784 self.request_redraw();
4785 return;
4786 }
4787 let was_tap = gesture_was_a_tap(ended.as_ref());
4790 if let Some(Drag::Widget {
4794 id,
4795 rect,
4796 press_x,
4797 track,
4798 active: true,
4799 thumb,
4800 ..
4801 }) = ended
4802 {
4803 let rtl = self.is_rtl();
4804 if matches!(track.mode, frus_widgets::DragMode::Thumbs { .. }) {
4805 if let Some(thumb) = thumb {
4806 let at = track.fraction(rect, self.cursor.x, rtl);
4807 self.thumb_drag(id, thumb, frus_widgets::ThumbDrag::End, at);
4808 }
4809 } else {
4810 let released = track.released_at(rect, press_x, self.cursor.x, rtl);
4811 self.dispatch_drag_edge(id, Edge::End, released);
4812 }
4813 }
4814 #[cfg(android)]
4816 if let Some(Drag::SelectionHandle { id, .. }) = ended {
4817 self.push_ime_context(id);
4818 }
4819 if let Some(Drag::SelectionHandle { id, .. }) = ended {
4820 self.show_selection_toolbar(id);
4821 }
4822 if let Some(Drag::RegionHandle { .. }) = ended {
4824 if let Some(selection) = self.runtime.region.as_mut() {
4825 selection.bar = true;
4826 }
4827 }
4828 if let Some(Drag::Reorder {
4831 id,
4832 from,
4833 moved: true,
4834 carried,
4835 ..
4836 }) = &ended
4837 {
4838 let target = self.reorder_drop_target(*id, *from, *carried);
4839 let tree = self.tree.as_ref();
4840 let base = target
4841 .and_then(|tid| tree.and_then(|t| find_widget(t.as_ref(), tid)))
4842 .and_then(|widget| widget.reorder_index());
4843 let to = match (base, target) {
4848 (Some(base), Some(tid)) => {
4849 let after = self
4850 .ui
4851 .as_ref()
4852 .and_then(|ui| ui.widget_rect(tid))
4853 .map(|rect| self.reorder_insert_after(tid, rect))
4854 .unwrap_or(false);
4855 Some(base + after as usize)
4856 }
4857 _ => None,
4858 };
4859 let self_drop = target == Some(*id);
4863 let message = match to {
4864 Some(to) if to != *from && !self_drop => tree
4865 .and_then(|t| find_widget(t.as_ref(), *id))
4866 .and_then(|widget| widget.on_reorder(to)),
4867 _ => None,
4868 };
4869 if let Some(message) = message {
4870 let to = to.unwrap_or(*from);
4871 let widget = tree.and_then(|t| find_widget(t.as_ref(), *id));
4872 let axis = widget
4873 .map(|w| w.reorder_axis())
4874 .unwrap_or(ReorderAxis::Horizontal);
4875 let announcement = widget
4881 .and_then(|w| w.reorder_announcement(to))
4882 .unwrap_or_else(|| match axis {
4883 ReorderAxis::Horizontal => format!("Column moved to position {}", to + 1),
4884 ReorderAxis::Vertical => "Card moved".to_string(),
4885 });
4886 self.dispatch(message);
4887 self.set_announcement(announcement);
4888 }
4889 }
4890 if let Some(Drag::Dismiss { item, moved, .. }) = &ended {
4896 if *moved {
4897 let estimate = self.gesture_estimate();
4898 let horizontal = item.spec.axis.is_horizontal();
4899 let (along, across) = if horizontal {
4900 (estimate.velocity.x, estimate.velocity.y)
4901 } else {
4902 (estimate.velocity.y, estimate.velocity.x)
4903 };
4904 self.runtime.dismiss_release(
4905 item.id,
4906 along,
4907 across,
4908 item.spec.axis,
4909 item.spec.threshold,
4910 );
4911 self.request_redraw();
4912 return;
4913 }
4914 }
4915 if let Some(Drag::Sheet {
4918 id,
4919 moved,
4920 available,
4921 ..
4922 }) = &ended
4923 {
4924 let velocity = if *moved {
4925 -self.fling_velocity(self.gesture_estimate()).1
4926 } else {
4927 0.0
4928 };
4929 let sheet = self.ui.as_ref().and_then(|ui| ui.sheet(*id)).map(|sheet| {
4932 let list = match sheet.areas.as_slice() {
4933 [only] => Some(*only),
4934 _ => None,
4935 };
4936 (sheet.spec.clone(), list)
4937 });
4938 if let Some((spec, list)) = sheet {
4939 self.runtime
4940 .sheet_release(*id, &spec, *available, velocity, list);
4941 }
4942 if *moved {
4943 self.request_redraw();
4944 return;
4945 }
4946 }
4947 if let Some(Drag::Item {
4948 source,
4949 moved: true,
4950 over,
4951 ..
4952 }) = &ended
4953 {
4954 self.runtime.drag_over = None;
4958 let accepted = over.is_some();
4959 let dropped = over.and_then(|id| {
4960 self.tree
4961 .as_ref()
4962 .and_then(|tree| find_widget(tree.as_ref(), id))
4963 .and_then(|widget| widget.on_drop(source.payload))
4964 });
4965 let ended_msg = self
4966 .tree
4967 .as_ref()
4968 .and_then(|tree| find_widget(tree.as_ref(), source.id))
4969 .and_then(|widget| widget.on_dropped(accepted));
4970 for message in dropped.into_iter().chain(ended_msg) {
4971 self.dispatch(message);
4972 }
4973 self.request_redraw();
4974 return;
4975 }
4976 if let Some(Drag::Item { .. }) = &ended {
4977 self.runtime.drag_over = None;
4980 }
4981 let mut sheet_took = false;
4984 if let Some(Drag::Scroll { id, .. }) = &ended {
4985 let sheet = self
4986 .ui
4987 .as_ref()
4988 .and_then(|ui| ui.sheet_holding(*id))
4989 .cloned();
4990 if let Some(sheet) = sheet {
4991 let velocity = match &ended {
4994 Some(Drag::Scroll {
4995 moved: true,
4996 axis: Some(true),
4997 carried,
4998 ..
4999 }) => -self.fling_velocity(self.gesture_estimate()).1 + carried.1,
5000 _ => 0.0,
5001 };
5002 let offset = self.runtime.scroll.get(id).map_or(0.0, |o| o.1);
5003 let size = self.runtime.sheet_size(sheet.id, &sheet.spec);
5004 sheet_took = velocity != 0.0
5005 && frus_widgets::sheet_takes_release(
5006 velocity,
5007 offset,
5008 size,
5009 &sheet.spec,
5010 sheet.available,
5011 );
5012 self.runtime.sheet_release(
5015 sheet.id,
5016 &sheet.spec,
5017 sheet.available,
5018 if sheet_took { velocity } else { 0.0 },
5019 Some(*id),
5020 );
5021 }
5022 }
5023 if let Some(Drag::Scroll { id, .. }) = &ended {
5024 self.runtime.release_scroll(*id);
5026 self.runtime.glow_scroll_end(*id);
5028 self.release_refresh(*id);
5032 }
5033 if let Some(Drag::Scroll {
5034 id,
5035 moved: true,
5036 carried,
5037 axis,
5038 ..
5039 }) = &ended
5040 {
5041 let estimate = self.gesture_estimate();
5045 let velocity = self.fling_velocity(estimate);
5046 let velocity = match self.ui.as_ref().and_then(|u| u.scroll_region(*id)) {
5049 Some(area) => area.offset_delta(velocity),
5050 None => (-velocity.0, -velocity.1),
5051 };
5052 let launch = (velocity.0 + carried.0, velocity.1 + carried.1);
5056 if sheet_took {
5057 self.request_redraw();
5058 } else {
5059 self.fling(
5060 *id,
5061 match axis {
5062 Some(true) => (0.0, launch.1),
5063 Some(false) => (launch.0, 0.0),
5064 None => launch,
5065 },
5066 );
5067 }
5068 }
5069 if let Some(Drag::Gesture {
5072 id,
5073 axis,
5074 moved: true,
5075 ..
5076 }) = &ended
5077 {
5078 let velocity = self.fling_velocity(self.gesture_estimate());
5079 let velocity = mask_pan(*axis, Point::new(velocity.0, velocity.1));
5080 let speed = velocity.x.hypot(velocity.y);
5082 let velocity = if speed > MAX_PAN_VELOCITY {
5083 let k = MAX_PAN_VELOCITY / speed;
5084 Point::new(velocity.x * k, velocity.y * k)
5085 } else {
5086 velocity
5087 };
5088 self.send_pan(*id, frus_widgets::PanEvent::End { velocity });
5089 }
5090 if let Some(Drag::Pan {
5093 id, moved: true, ..
5094 }) = &ended
5095 {
5096 let estimate = self.gesture_estimate();
5097 let velocity = self.fling_velocity(estimate);
5098 if velocity.0.hypot(velocity.1) > PAN_FLING_MIN {
5099 self.runtime.interactive_velocity.insert(*id, velocity);
5100 }
5101 }
5102 if ended.is_some() && !was_tap {
5103 self.request_redraw();
5104 return;
5105 }
5106 let released = self.ui.as_ref().and_then(|ui| ui.hit(self.cursor));
5108 let bar_action = match (self.runtime.input.pressed, released) {
5111 (Some(pressed), Some(released)) if pressed == released => {
5112 self.ui.as_ref().and_then(|ui| ui.edit_action_for(pressed))
5113 }
5114 _ => None,
5115 };
5116 let on_bar = self
5117 .ui
5118 .as_ref()
5119 .is_some_and(|ui| ui.toolbar_contains(self.cursor));
5120 let field_positional = if bar_action.is_some() {
5125 None
5126 } else {
5127 self.ui
5128 .as_ref()
5129 .and_then(|ui| ui.focus_hit(self.cursor))
5130 .map(|(id, _)| id)
5131 .filter(|id| self.runtime.input.focused == Some(*id))
5132 .and_then(|id| self.positional_click_on(id))
5133 };
5134 let (message, announce) = match (self.runtime.input.pressed, released) {
5135 (Some(_), Some(_)) if bar_action.is_some() => (None, None),
5136 _ if field_positional.is_some() => (field_positional, None),
5137 (Some(pressed), Some(released)) if pressed == released => {
5138 let positional = self.positional_click_on(released);
5141 let message =
5142 positional.or_else(|| self.ui.as_ref().and_then(|ui| ui.msg_for(pressed)));
5143 let announce = self
5146 .tree
5147 .as_ref()
5148 .and_then(|tree| find_widget(tree.as_ref(), pressed))
5149 .and_then(|widget| widget.announce());
5150 (message, announce)
5151 }
5152 _ => (None, None),
5153 };
5154 let message = match (self.runtime.input.pressed, released) {
5156 (Some(pressed), Some(released)) if pressed == released && bar_action.is_none() => {
5157 self.double_tap(pressed, message)
5158 }
5159 _ => message,
5160 };
5161 if let (Some(pressed), Some(released)) = (self.runtime.input.pressed, released) {
5165 if pressed == released {
5166 self.runtime.ink_confirm(pressed);
5167 }
5168 }
5169 self.runtime.input.pressed = None;
5170 if let Some(action) = bar_action {
5171 self.perform_edit_action(action);
5172 } else if on_bar && message.is_some() {
5173 self.runtime.selection_handles = None;
5176 self.hide_selection_toolbar();
5177 }
5178 if let Some(message) = message {
5179 self.dispatch(message);
5180 if let Some(announce) = announce {
5181 self.set_announcement(announce);
5182 }
5183 }
5184 self.request_redraw();
5185 }
5186
5187 fn double_tap(&mut self, id: WidgetId, message: Option<A::Message>) -> Option<A::Message> {
5191 let Some(double) = self
5192 .tree
5193 .as_ref()
5194 .and_then(|tree| find_widget(tree.as_ref(), id))
5195 .and_then(|widget| widget.on_double_tap())
5196 else {
5197 return message;
5198 };
5199 let now = Instant::now();
5200 let second = self.pending_tap.as_ref().is_some_and(|pending| {
5201 let (dx, dy) = (
5202 self.cursor.x - pending.position.x,
5203 self.cursor.y - pending.position.y,
5204 );
5205 pending.id == id
5206 && now.duration_since(pending.at) <= DOUBLE_TAP_TIMEOUT
5207 && dx.hypot(dy) <= DOUBLE_TAP_SLOP
5208 });
5209 if second {
5210 self.pending_tap = None;
5211 return Some(double);
5212 }
5213 self.flush_pending_tap();
5214 self.pending_tap = Some(PendingTap {
5215 id,
5216 at: now,
5217 position: self.cursor,
5218 single: message,
5219 });
5220 None
5221 }
5222
5223 fn flush_pending_tap(&mut self) {
5225 if let Some(message) = self.pending_tap.take().and_then(|pending| pending.single) {
5226 self.dispatch(message);
5227 self.request_redraw();
5228 }
5229 }
5230
5231 fn poll_pending_tap(&mut self, now: Instant) -> bool {
5233 let due = self
5234 .pending_tap
5235 .as_ref()
5236 .is_some_and(|pending| now.duration_since(pending.at) >= DOUBLE_TAP_TIMEOUT);
5237 if due {
5238 self.flush_pending_tap();
5239 }
5240 due
5241 }
5242
5243 #[cfg(any(desktop, web))]
5248 fn set_announcement(&mut self, message: String) {
5249 self.announce = message;
5250 }
5251 #[cfg(not(any(desktop, web)))]
5252 fn set_announcement(&mut self, _message: String) {}
5253
5254 fn reconcile_focus(&mut self) {
5261 let present: std::collections::HashSet<WidgetId> = match self.ui.as_ref() {
5262 Some(ui) => ui.focusable_ids().collect(),
5263 None => return,
5264 };
5265 let autofocus: Vec<WidgetId> = self
5269 .ui
5270 .as_ref()
5271 .map(|ui| ui.autofocus_ids().collect())
5272 .unwrap_or_default();
5273 let appeared = autofocus
5274 .iter()
5275 .find(|id| !self.autofocused.contains(id))
5276 .copied();
5277 self.autofocused = autofocus.into_iter().collect();
5278 if let Some(id) = appeared {
5279 self.runtime.input.focused = Some(id);
5280 }
5281 let before = self.runtime.input.focused;
5282 let after = resolve_focus(
5283 before,
5284 &present,
5285 &mut self.focus_history,
5286 &mut self.prev_focus,
5287 );
5288 if after != before || appeared.is_some() {
5289 self.runtime.input.focused = after;
5290 self.request_redraw();
5291 }
5292 }
5293
5294 fn report_focus(&mut self) {
5297 let now = self.runtime.input.focused;
5298 if now == self.focus_reported {
5301 return;
5302 }
5303 let before = std::mem::replace(&mut self.focus_reported, now);
5304 let messages = self
5305 .tree
5306 .as_ref()
5307 .map(|tree| focus_changes(tree.as_ref(), before, now))
5308 .unwrap_or_default();
5309 for message in messages {
5310 self.dispatch(message);
5311 }
5312 }
5313
5314 fn dispatch(&mut self, message: A::Message) {
5317 self.build_dirty = true;
5319 let command = self.app.update(message);
5320 self.run_command(command);
5321 let asked = self.app.effects();
5322 self.run_command(asked);
5323 #[cfg(web)]
5324 self.sync_address();
5325 #[cfg(any(desktop, android))]
5326 self.report_dev();
5327 self.sync_subscriptions();
5328 }
5329
5330 #[cfg(any(android, desktop))]
5333 fn open_launch_link(&mut self) {
5334 #[cfg(android)]
5335 let link = self
5336 .android_app
5337 .as_ref()
5338 .and_then(crate::android_link::launch_link);
5339 #[cfg(desktop)]
5341 let link = crate::link::link_among(std::env::args()).or_else(crate::dev::restore_link);
5342 if let Some(location) = link.as_deref().and_then(crate::link::location_of_link) {
5343 let command = self.app.open_location(&location);
5344 self.run_command(command);
5345 }
5346 }
5347
5348 #[cfg(any(android, desktop))]
5350 fn connect_dev(&mut self) {
5351 #[cfg(desktop)]
5352 {
5353 self.dev = crate::dev::address()
5354 .as_deref()
5355 .and_then(crate::dev::DevChannel::connect);
5356 crate::dev_commands::set_window(self.window.clone());
5357 if let Some(states) = crate::dev::restored_states() {
5358 self.runtime.states.restore_from(&states);
5359 }
5360 }
5361 #[cfg(android)]
5363 {
5364 let asked = self
5365 .android_app
5366 .as_ref()
5367 .and_then(|app| crate::android_link::launch_extra(app, crate::dev::DEV_EXTRA));
5368 #[cfg(feature = "hot-reload")]
5370 let asked = asked.or_else(|| crate::hot::under_engine().then(String::new));
5371 self.dev = asked.map(|_| crate::dev::DevChannel::through_log());
5372 if self.dev.is_some() {
5373 let storage = self
5374 .android_app
5375 .as_ref()
5376 .and_then(|app| app.internal_data_path());
5377 if let Some(states) = storage.as_deref().and_then(crate::dev::restored_states) {
5378 self.runtime.states.restore_from(&states);
5379 }
5380 if let Some(dir) = storage {
5381 crate::dev_commands::poll_commands(dir);
5382 }
5383 }
5384 }
5385 }
5386
5387 #[cfg(any(android, desktop))]
5390 fn report_dev(&mut self) {
5391 let Some(dev) = self.dev.as_mut() else {
5392 return;
5393 };
5394 #[cfg(desktop)]
5395 let window = self.window.as_ref().and_then(|window| {
5396 let at = window.outer_position().ok()?;
5397 let size = window.inner_size();
5398 Some([at.x, at.y, size.width as i32, size.height as i32])
5399 });
5400 #[cfg(android)]
5401 let window = None;
5402 if !dev.report(self.app.location(), window) {
5403 self.dev = None;
5404 return;
5405 }
5406 let states = &self.runtime.states;
5407 dev.save_if_asked(|| states.snapshot());
5408 }
5409
5410 #[cfg(desktop)]
5412 fn open_links_from_other_instances(&mut self) {
5413 for link in crate::instance::take_links() {
5414 if let Some(location) = crate::link::location_of_link(&link) {
5415 let command = self.app.open_location(&location);
5416 self.run_command(command);
5417 self.build_dirty = true;
5418 self.request_redraw();
5419 }
5420 }
5421 }
5422
5423 #[cfg(web)]
5426 fn open_launch_address(&mut self, window: &Arc<Window>) {
5427 let strategy = self.app.location_strategy();
5428 let base = crate::address::base();
5429 let (location, index) = crate::address::current(strategy, &base);
5430 self.history.opened(location.clone(), index);
5431 crate::address::listen(window.clone(), strategy, base.clone());
5432 self.address = (strategy, base);
5433 if let Some(location) = location {
5434 let command = self.app.open_location(&location);
5435 self.run_command(command);
5436 let (strategy, base) = &self.address;
5438 for (step, index) in self.history.seed(&self.app.location_stack()) {
5439 crate::address::apply(step, *strategy, base, index);
5440 }
5441 }
5442 }
5443
5444 #[cfg(web)]
5447 fn sync_address(&mut self) {
5448 if !self.started {
5449 return;
5450 }
5451 for (location, state) in crate::address::take_popped() {
5452 if self.history.popped(location.clone(), state) {
5453 let command = self.app.open_location(&location);
5454 self.run_command(command);
5455 self.build_dirty = true;
5456 self.request_redraw();
5459 }
5460 }
5461 if let Some(location) = self.app.location() {
5462 if let Some(step) = self
5463 .history
5464 .reflect(&location, self.app.location_replaces())
5465 {
5466 let (strategy, base) = &self.address;
5467 crate::address::apply(step, *strategy, base, self.history.index());
5468 }
5469 }
5470 }
5471
5472 fn run_command(&mut self, command: crate::command::Command<A::Message>) {
5489 let parts = command.into_parts();
5490 self.pending_focus.extend(parts.focus);
5491 self.pending_scroll.extend(parts.scrolls);
5492 self.pending_sheet.extend(parts.sheets);
5493 for task in parts.tasks {
5494 let proxy = self.proxy.clone();
5495 #[cfg(not(web))]
5496 std::thread::spawn(move || {
5497 if let Some(message) = task() {
5498 let _ = proxy.send_event(message);
5499 }
5500 });
5501 #[cfg(web)]
5502 wasm_bindgen_futures::spawn_local(async move {
5503 if let Some(message) = task() {
5504 let _ = proxy.send_event(message);
5505 }
5506 });
5507 }
5508 for future in parts.async_tasks {
5509 let proxy = self.proxy.clone();
5510 #[cfg(not(web))]
5514 crate::runtime::spawn(async move {
5515 if let Some(message) = future.await {
5516 let _ = proxy.send_event(message);
5517 }
5518 })
5519 .detach();
5520 #[cfg(web)]
5523 wasm_bindgen_futures::spawn_local(async move {
5524 if let Some(message) = future.await {
5525 let _ = proxy.send_event(message);
5526 }
5527 });
5528 }
5529 for (delay, message) in parts.timers {
5530 let proxy = self.proxy.clone();
5531 #[cfg(not(web))]
5532 crate::runtime::spawn(async move {
5533 crate::runtime::sleep(delay).await;
5534 let _ = proxy.send_event(message);
5535 })
5536 .detach();
5537 #[cfg(web)]
5538 web_timer::after(delay.as_millis() as i32, move || {
5539 let _ = proxy.send_event(message);
5540 });
5541 }
5542 }
5543
5544 fn sync_subscriptions(&mut self) {
5547 let entries = self.app.subscription().into_entries();
5548 let declared: std::collections::HashSet<u64> = entries.iter().map(|e| e.id).collect();
5549
5550 self.running_subs.retain(|id, _| declared.contains(id));
5552
5553 for entry in entries {
5555 if self.running_subs.contains_key(&entry.id) {
5556 continue;
5557 }
5558 let handle = self.start_subscription(entry.kind);
5559 self.running_subs.insert(entry.id, handle);
5560 }
5561 }
5562
5563 #[cfg(not(web))]
5572 fn start_subscription(&self, kind: crate::subscription::Kind<A::Message>) -> SubHandle {
5573 let proxy = self.proxy.clone();
5574 match kind {
5575 crate::subscription::Kind::Every { interval, make } => {
5576 crate::runtime::spawn(async move {
5577 loop {
5578 crate::runtime::sleep(interval).await;
5579 if proxy.send_event(make(Instant::now())).is_err() {
5582 break;
5583 }
5584 }
5585 })
5586 }
5587 }
5588 }
5589
5590 #[cfg(web)]
5594 fn start_subscription(&self, kind: crate::subscription::Kind<A::Message>) -> SubHandle {
5595 let proxy = self.proxy.clone();
5596 match kind {
5597 crate::subscription::Kind::Every { interval, make } => {
5598 let ms = interval.as_millis() as i32;
5599 web_timer::Interval::new(ms, move || {
5600 let _ = proxy.send_event(make(Instant::now()));
5601 })
5602 }
5603 }
5604 }
5605
5606 fn handle_drag(&mut self) {
5608 let Some(mut drag) = self.drag.take() else {
5609 return;
5610 };
5611 let slop = self.hit_slop();
5613 match &mut drag {
5614 Drag::Scrollbar {
5615 id,
5616 vertical,
5617 grab,
5618 track_start,
5619 track_len,
5620 thumb_len,
5621 max,
5622 reverse,
5623 } => {
5624 let along = if *vertical {
5625 self.cursor.y
5626 } else {
5627 self.cursor.x
5628 };
5629 let travel = (*track_len - *thumb_len).max(1.0);
5630 let thumb_start = (along - *grab).clamp(*track_start, *track_start + travel);
5631 let fraction = (thumb_start - *track_start) / travel;
5632 let fraction = if *reverse { 1.0 - fraction } else { fraction };
5635 let offset = (fraction * *max).clamp(0.0, *max);
5636 let entry = self.runtime.scroll.entry(*id).or_insert((0.0, 0.0));
5637 if *vertical {
5638 entry.1 = offset;
5639 } else {
5640 entry.0 = offset;
5641 }
5642 let synced = *entry;
5644 self.runtime.scroll_target.insert(*id, synced);
5645 self.runtime.scroll_velocity.remove(&*id);
5646 }
5647 Drag::RegionSelect { area } => {
5648 let area = *area;
5649 self.extend_region_selection(area);
5650 }
5651 Drag::RegionHandle { area, handle, grab } => {
5652 let (area, handle, grab) = (*area, *handle, *grab);
5653 self.drag_region_handle(area, handle, grab);
5654 }
5655 Drag::TextSelect { id, rect } => {
5656 let local_x = self.cursor.x - rect.x;
5657 let local_y = self.cursor.y - rect.y
5658 + self.runtime.scroll.get(id).map(|s| s.1).unwrap_or(0.0);
5659 let scroll_cursor = self.runtime.edits.get(id).map(|e| e.cursor).unwrap_or(0);
5661 let cursor = self
5662 .tree
5663 .as_ref()
5664 .and_then(|tree| find_widget(tree.as_ref(), *id))
5665 .and_then(|widget| {
5666 widget.cursor_at(local_x, local_y, rect.width, scroll_cursor)
5667 });
5668 if let Some(cursor) = cursor {
5669 let edit = self.runtime.edits.entry(*id).or_default();
5670 if edit.anchor.is_none() {
5671 edit.anchor = Some(edit.cursor);
5672 }
5673 edit.cursor = cursor;
5674 }
5675 }
5676 Drag::SelectionHandle {
5677 id,
5678 rect,
5679 handle,
5680 grab,
5681 } => {
5682 let local_x = self.cursor.x - rect.x + grab.x;
5685 let local_y = self.cursor.y - rect.y
5686 + grab.y
5687 + self.runtime.scroll.get(id).map(|s| s.1).unwrap_or(0.0);
5688 let edit = self.runtime.edits.get(id).copied().unwrap_or_default();
5689 let moved = self
5690 .tree
5691 .as_ref()
5692 .and_then(|tree| find_widget(tree.as_ref(), *id))
5693 .and_then(|widget| widget.cursor_at(local_x, local_y, rect.width, edit.cursor))
5694 .and_then(|to| crate::selection::drag(edit, *handle, to));
5695 if let Some(moved) = moved {
5696 self.runtime.edits.insert(*id, moved);
5697 }
5698 }
5699 Drag::Widget {
5700 id,
5701 rect,
5702 last_x,
5703 press_x,
5704 track,
5705 active,
5706 thumb,
5707 } => {
5708 let dx = self.cursor.x - *last_x;
5709 *last_x = self.cursor.x;
5710 if matches!(track.mode, frus_widgets::DragMode::Thumbs { .. }) {
5711 let rtl = self.is_rtl();
5712 let at = track.fraction(*rect, self.cursor.x, rtl);
5713 match *thumb {
5714 Some(picked) => {
5715 self.thumb_drag(*id, picked, frus_widgets::ThumbDrag::Move, at);
5716 }
5717 None => {
5719 let moved = self.cursor.x - *press_x;
5720 if let Some(picked) = track.pick_thumb(*rect, *press_x, moved, rtl) {
5721 *thumb = Some(picked);
5722 self.thumb_drag(*id, picked, frus_widgets::ThumbDrag::Start, at);
5723 self.thumb_drag(*id, picked, frus_widgets::ThumbDrag::Move, at);
5724 }
5725 }
5726 }
5727 } else if *active {
5728 let value = track.value_at(*rect, *press_x, self.cursor.x, true, self.is_rtl());
5729 self.apply_widget_drag(*id, dx, value);
5730 }
5731 }
5732 Drag::Reorder { start, moved, .. } => {
5733 let dx = self.cursor.x - start.x;
5735 let dy = self.cursor.y - start.y;
5736 if !*moved && (dx * dx + dy * dy) > slop * slop {
5737 *moved = true;
5738 }
5739 if *moved {
5741 self.request_redraw();
5742 }
5743 }
5744 Drag::Gesture {
5745 id,
5746 axis,
5747 start,
5748 last,
5749 moved,
5750 } => {
5751 let (id, axis) = (*id, *axis);
5752 if !*moved {
5753 let (tx, ty) = (self.cursor.x - start.x, self.cursor.y - start.y);
5757 let travelled = match axis {
5758 frus_widgets::PanAxis::Free => tx.hypot(ty),
5759 frus_widgets::PanAxis::Horizontal => tx.abs(),
5760 frus_widgets::PanAxis::Vertical => ty.abs(),
5761 };
5762 if travelled > slop {
5763 *moved = true;
5764 let local = self.pan_local(id, *start);
5765 self.send_pan(id, frus_widgets::PanEvent::Start { local });
5766 }
5767 }
5768 if *moved {
5769 let delta = mask_pan(
5770 axis,
5771 Point::new(self.cursor.x - last.x, self.cursor.y - last.y),
5772 );
5773 *last = self.cursor;
5774 self.track_gesture();
5775 if delta.x != 0.0 || delta.y != 0.0 {
5776 let local = self.pan_local(id, self.cursor);
5777 self.send_pan(id, frus_widgets::PanEvent::Update { local, delta });
5778 }
5779 }
5780 }
5781 Drag::Pan {
5782 id,
5783 last,
5784 moved,
5785 viewport,
5786 } => {
5787 let dx = self.cursor.x - last.x;
5788 let dy = self.cursor.y - last.y;
5789 if !*moved && (dx * dx + dy * dy) > slop * slop {
5790 *moved = true;
5791 }
5792 if *moved {
5793 let view = self.runtime.interactive.entry(*id).or_default();
5794 *view = view.pan(dx, dy).clamped(*viewport);
5796 *last = self.cursor;
5797 self.track_gesture();
5798 }
5799 }
5800 Drag::Item {
5801 source,
5802 start,
5803 moved,
5804 over,
5805 } => {
5806 let dx = self.cursor.x - start.x;
5807 let dy = self.cursor.y - start.y;
5808 if !*moved && (dx * dx + dy * dy) > slop * slop {
5811 *moved = true;
5812 }
5813 if !*moved {
5814 return;
5815 }
5816 let payload = source.payload;
5820 let candidate = self
5821 .ui
5822 .as_ref()
5823 .and_then(|ui| ui.drop_zone_at(self.cursor))
5824 .filter(|zone| {
5825 self.tree
5826 .as_ref()
5827 .and_then(|tree| find_widget(tree.as_ref(), zone.id))
5828 .is_some_and(|widget| widget.accepts_drag(payload))
5829 })
5830 .map(|zone| zone.id);
5831 if *over != candidate {
5832 *over = candidate;
5833 self.runtime.drag_over = candidate;
5834 }
5835 self.request_redraw();
5836 }
5837 Drag::Scroll {
5838 id,
5839 last,
5840 moved,
5841 carried,
5842 dismiss,
5843 axis,
5844 pan,
5845 } => {
5846 let dx = self.cursor.x - last.x;
5847 let dy = self.cursor.y - last.y;
5848 if !*moved && (dx * dx + dy * dy) > slop * slop {
5850 *moved = true;
5851 if let Some((target, pan_axis)) = pan.take() {
5862 let down = dy.abs() >= dx.abs();
5863 let wins = match pan_axis {
5864 frus_widgets::PanAxis::Horizontal => !down,
5865 frus_widgets::PanAxis::Vertical => down,
5866 frus_widgets::PanAxis::Free => {
5867 let chain: Vec<_> = self
5868 .ui
5869 .as_ref()
5870 .map(|ui| ui.scroll_chain(*last).collect())
5871 .unwrap_or_default();
5872 !chain.iter().any(|a| a.takes_user_scroll(down))
5875 }
5876 };
5877 if wins {
5878 self.runtime.release_scroll(*id);
5880 let start = *last;
5881 self.drag = Some(Drag::Gesture {
5882 id: target,
5883 axis: pan_axis,
5884 start,
5885 last: start,
5886 moved: false,
5887 });
5888 self.handle_drag();
5889 return;
5890 }
5891 }
5892 if let Some(item) = dismiss.take() {
5893 let along = if item.spec.axis.is_horizontal() {
5894 dx.abs() > dy.abs()
5895 } else {
5896 dy.abs() > dx.abs()
5897 };
5898 if along {
5899 self.runtime.release_scroll(*id);
5901 self.drag = Some(Drag::Dismiss {
5902 item,
5903 last: *last,
5904 moved: true,
5905 });
5906 self.handle_drag();
5907 return;
5908 }
5909 }
5910 let chain: Vec<_> = self
5920 .ui
5921 .as_ref()
5922 .map(|ui| ui.scroll_chain(*last).collect())
5923 .unwrap_or_default();
5924 let down = dy.abs() >= dx.abs();
5925 if let Some(area) = claim_area(&chain, *id, down) {
5926 if area.id != *id {
5927 self.runtime.release_scroll(*id);
5931 let physics = area.physics;
5932 *carried = self.runtime.catch_scroll_fling(area.id, physics);
5933 self.runtime.hold_scroll(area.id);
5934 *id = area.id;
5935 }
5936 *axis = Some(claim_axis(
5937 dx,
5938 dy,
5939 area.takes_user_scroll(false),
5940 area.takes_user_scroll(true),
5941 ));
5942 } else {
5943 *axis = Some(down);
5944 }
5945 }
5946 let (dx, mut dy) = match *axis {
5949 Some(true) => (0.0, dy),
5950 Some(false) => (dx, 0.0),
5951 None => (dx, dy),
5952 };
5953 if *moved && *axis == Some(true) {
5954 let sheet = self
5960 .ui
5961 .as_ref()
5962 .filter(|ui| ui.scroll_region(*id).is_some_and(|a| !a.reverse_y))
5963 .and_then(|ui| ui.sheet_holding(*id))
5964 .filter(|sheet| sheet.available > 0.0)
5965 .cloned();
5966 if let Some(sheet) = sheet {
5967 let px = sheet.available;
5968 let offset = self.runtime.scroll.get(id).map_or(0.0, |o| o.1);
5969 let size = self.runtime.sheet_size(sheet.id, &sheet.spec) * px;
5970 let (grown, listed) = frus_widgets::split_sheet_drag(
5971 -dy,
5972 offset,
5973 size,
5974 sheet.spec.floor() * px,
5975 sheet.spec.max * px,
5976 );
5977 if grown != 0.0 {
5978 self.runtime
5979 .sheet_drag(sheet.id, &sheet.spec, grown / px, px);
5980 }
5981 dy = -listed;
5982 }
5983 }
5984 if *moved {
5985 let area = self.ui.as_ref().and_then(|u| u.scroll_region(*id));
5986 let physics = area.map(|a| a.physics).unwrap_or_else(|| {
5988 self.app
5989 .scroll_behavior()
5990 .scroll_physics(&self.themes.displayed(&self.app))
5991 });
5992 let cur = self.runtime.scroll.get(id).copied().unwrap_or((0.0, 0.0));
5993 let axis = |metrics: frus_widgets::ScrollMetrics, delta: f32| {
6000 let applied = physics.apply_user_offset(metrics, delta);
6001 let proposed = metrics.pixels + applied;
6002 let refused = physics.apply_boundary_conditions(metrics, proposed);
6003 (proposed - refused, refused)
6004 };
6005 let (nx, ny, refused) = match area {
6006 Some(area) => {
6007 let (ddx, ddy) = area.offset_delta((dx, dy));
6008 let (nx, rx) = axis(area.metrics_x(cur.0), ddx);
6009 let (ny, ry) = axis(area.metrics_y(cur.1), ddy);
6010 (nx, ny, Some((area, rx, ry)))
6011 }
6012 None => (cur.0 - dx, cur.1 - dy, None),
6013 };
6014 if let Some((area, rx, ry)) = refused {
6015 if let Some(host) = area.refresh {
6021 self.feed_refresh(host, physics, area.viewport.height, cur.1, ny, ry);
6022 }
6023 let cursor = self.cursor;
6024 for (refused, vertical, extent) in [
6025 (rx, false, area.viewport.width),
6026 (ry, true, area.viewport.height),
6027 ] {
6028 if refused.abs() < 1e-3 {
6029 continue;
6030 }
6031 let edge = area.refused_edge(vertical, refused);
6032 if area.refresh.is_some() && edge == area.start_edge(vertical) {
6035 continue;
6036 }
6037 let (offset, cross) =
6038 frus_widgets::glow_cross_axis(area.viewport, edge, cursor);
6039 self.runtime.glow_pull(
6040 area.id,
6041 area.overscroll,
6042 edge,
6043 refused,
6044 extent,
6045 offset,
6046 cross,
6047 );
6048 }
6049 if rx.abs() < 1e-3 && ry.abs() < 1e-3 && (nx, ny) != cur {
6052 self.runtime.stretch_release(area.id);
6053 }
6054 }
6055 self.runtime.scroll.insert(*id, (nx, ny));
6056 self.runtime.scroll_target.insert(*id, (nx, ny));
6057 self.runtime.scroll_velocity.remove(id);
6058 *last = self.cursor;
6059 self.track_gesture();
6060 }
6061 }
6062 Drag::Dismiss { item, last, moved } => {
6063 let dx = self.cursor.x - last.x;
6064 let dy = self.cursor.y - last.y;
6065 if !*moved && (dx * dx + dy * dy) > slop * slop {
6066 *moved = true;
6067 }
6068 if *moved {
6069 let delta = if item.spec.axis.is_horizontal() {
6070 dx
6071 } else {
6072 dy
6073 };
6074 self.runtime
6075 .dismiss_drag(item.id, delta, item.extent(), item.spec.axis);
6076 *last = self.cursor;
6077 self.track_gesture();
6078 }
6079 }
6080 Drag::Sheet {
6081 id,
6082 last,
6083 moved,
6084 available,
6085 } => {
6086 let dx = self.cursor.x - last.x;
6087 let dy = self.cursor.y - last.y;
6088 if !*moved && (dx * dx + dy * dy) > slop * slop {
6089 *moved = true;
6090 }
6091 if *moved && *available > 0.0 {
6092 let spec = self
6093 .ui
6094 .as_ref()
6095 .and_then(|ui| ui.sheet(*id))
6096 .map(|sheet| sheet.spec.clone());
6097 if let Some(spec) = spec {
6098 self.runtime
6100 .sheet_drag(*id, &spec, -dy / *available, *available);
6101 }
6102 *last = self.cursor;
6103 self.track_gesture();
6104 }
6105 }
6106 Drag::Back { start_x } => {
6107 let width = self.logical_width();
6108 let sign = if self.is_rtl() { -1.0 } else { 1.0 };
6111 let progress = (sign * (self.cursor.x - *start_x) / width).clamp(0.0, 1.0);
6112 self.track_gesture();
6113 self.build_dirty = true;
6114 self.app.back_gesture(progress);
6115 }
6116 }
6117 self.drag = Some(drag);
6118 self.request_redraw();
6119 }
6120
6121 fn refresh_host_of(&self, scrollable: WidgetId) -> Option<WidgetId> {
6123 self.ui
6124 .as_ref()
6125 .and_then(|ui| ui.scroll_region(scrollable))
6126 .and_then(|area| area.refresh)
6127 }
6128
6129 fn release_refresh(&mut self, scrollable: WidgetId) {
6132 let Some(host) = self.refresh_host_of(scrollable) else {
6133 return;
6134 };
6135 if !self.runtime.refresh_release(host) {
6136 return;
6137 }
6138 let message = self
6139 .tree
6140 .as_ref()
6141 .and_then(|tree| find_widget(tree.as_ref(), host))
6142 .and_then(|widget| widget.on_refresh());
6143 if let Some(message) = message {
6144 self.dispatch(message);
6145 }
6146 }
6147
6148 fn feed_refresh(
6167 &mut self,
6168 host: WidgetId,
6169 physics: frus_widgets::ScrollPhysics,
6170 extent: f32,
6171 before: f32,
6172 after: f32,
6173 refused_y: f32,
6174 ) {
6175 if after > 0.5 {
6176 self.runtime.refresh_cancel(host);
6177 return;
6178 }
6179 let pulled = if physics.allows_overscroll() {
6180 (-after).max(0.0) - (-before).max(0.0)
6181 } else {
6182 -refused_y
6183 };
6184 if pulled.abs() > 1e-3 {
6185 self.runtime.refresh_pull(host, pulled, extent);
6186 }
6187 }
6188
6189 fn hit_slop(&self) -> f32 {
6192 if self.pointer_touch {
6193 TOUCH_SLOP
6194 } else {
6195 PRECISE_SLOP
6196 }
6197 }
6198
6199 fn gesture_now(&self) -> f32 {
6202 match self.gesture_clock {
6203 Some((now, start)) => now - start,
6205 None => (Instant::now() - self.gesture_start).as_secs_f32(),
6206 }
6207 }
6208
6209 fn begin_gesture(&mut self) {
6215 self.gesture_velocity = self
6216 .app
6217 .scroll_behavior()
6218 .velocity_tracker(&self.themes.displayed(&self.app));
6219 self.gesture_start = Instant::now();
6220 if let Some((now, start)) = self.gesture_clock.as_mut() {
6221 *start = *now;
6222 }
6223 self.track_gesture();
6224 }
6225
6226 fn track_gesture(&mut self) {
6228 let now = self.gesture_now();
6229 self.gesture_velocity.add_position(now, self.cursor);
6230 }
6231
6232 fn gesture_estimate(&self) -> VelocityEstimate {
6234 self.gesture_velocity
6235 .estimate(self.gesture_now())
6236 .unwrap_or(VelocityEstimate::STILL)
6237 }
6238
6239 fn fling_velocity(&self, estimate: VelocityEstimate) -> (f32, f32) {
6241 fling_velocity(estimate, self.hit_slop())
6242 }
6243
6244 fn fling(&mut self, id: WidgetId, velocity: (f32, f32)) {
6248 let Some(area) = self.ui.as_ref().and_then(|ui| ui.scroll_region(id)) else {
6249 return;
6250 };
6251 let physics = area.physics;
6252 if self.runtime.fling_scroll(area, physics, velocity) {
6256 self.request_redraw();
6257 }
6258 }
6259
6260 fn reorderable_at(&self, point: Point) -> Option<(WidgetId, usize, bool)> {
6264 let id = self.ui.as_ref()?.reorderable_at(point)?;
6265 let widget = self
6266 .tree
6267 .as_ref()
6268 .and_then(|tree| find_widget(tree.as_ref(), id))?;
6269 if !widget.reorder_draggable() {
6272 return None;
6273 }
6274 let from = widget.reorder_index()?;
6275 Some((id, from, widget.drag_needs_long_press()))
6276 }
6277
6278 fn reorder_source(&self, id: WidgetId, from: usize) -> Option<(WidgetId, Rect)> {
6290 let ui = self.ui.as_ref()?;
6291 let tree = self.tree.as_ref()?;
6292 let own = ui.widget_rect(id)?;
6293 let grabbed = find_widget(tree.as_ref(), id)?;
6294 if grabbed.reorder_droppable() {
6295 return Some((id, own));
6296 }
6297 let middle = Point::new(own.x + own.width * 0.5, own.y + own.height * 0.5);
6298 let slot = ui.reorderables_at(middle).find(|other| {
6299 find_widget(tree.as_ref(), *other)
6300 .is_some_and(|w| w.reorder_droppable() && w.reorder_index() == Some(from))
6301 })?;
6302 Some((slot, ui.widget_rect(slot).unwrap_or(own)))
6306 }
6307
6308 fn reorder_carried(&self) -> Option<(WidgetId, Rect)> {
6312 match &self.drag {
6313 Some(Drag::Reorder {
6314 carried: Some(carried),
6315 ..
6316 }) => Some(*carried),
6317 Some(Drag::Reorder { id, from, .. }) => self.reorder_source(*id, *from),
6318 _ => None,
6319 }
6320 }
6321
6322 fn keep_carried_box(&mut self) {
6327 let source = match &self.drag {
6328 Some(Drag::Reorder {
6329 id,
6330 from,
6331 moved: true,
6332 carried: None,
6333 ..
6334 }) => self.reorder_source(*id, *from),
6335 _ => return,
6336 };
6337 if let Some(Drag::Reorder { carried, .. }) = self.drag.as_mut() {
6338 *carried = source;
6339 }
6340 }
6341
6342 fn reorder_target_at(&self, point: Point) -> Option<WidgetId> {
6346 let ui = self.ui.as_ref()?;
6347 let tree = self.tree.as_ref()?;
6348 ui.reorderables_at(point)
6349 .find(|id| find_widget(tree.as_ref(), *id).is_some_and(|w| w.reorder_droppable()))
6350 }
6351
6352 fn reorder_drop_target(
6361 &self,
6362 id: WidgetId,
6363 from: usize,
6364 carried: Option<(WidgetId, Rect)>,
6365 ) -> Option<WidgetId> {
6366 if let Some(target) = self.reorder_target_at(self.cursor) {
6367 return Some(target);
6368 }
6369 let tree = self.tree.as_ref()?;
6370 let Some(ReorderMotion::Slots(axis)) = find_widget(tree.as_ref(), id).map(reorder_motion)
6371 else {
6372 return None;
6373 };
6374 let row = match carried {
6377 Some((row, _)) => row,
6378 None => self.reorder_source(id, from)?.0,
6379 };
6380 let slots = reorder_siblings(self.ui.as_ref()?, tree.as_ref(), row);
6381 let boxes: Vec<Rect> = slots.iter().map(|(_, rect)| *rect).collect();
6382 nearest_reorder_slot(self.cursor, &boxes, axis).map(|index| slots[index].0)
6383 }
6384
6385 fn carried_rect(&self) -> Option<Rect> {
6392 match &self.drag {
6393 Some(Drag::Reorder {
6394 start, moved: true, ..
6395 }) => {
6396 let (_, src) = self.reorder_carried()?;
6397 let (gx, gy) = ghost_offset(
6400 self.dragged_reorder_motion()
6401 .unwrap_or(ReorderMotion::Columns),
6402 (self.cursor.x - start.x, self.cursor.y - start.y),
6403 );
6404 Some(src.translate(gx, gy))
6405 }
6406 Some(Drag::Item {
6407 source,
6408 start,
6409 moved: true,
6410 ..
6411 }) => Some(
6412 source
6413 .rect
6414 .translate(self.cursor.x - start.x, self.cursor.y - start.y),
6415 ),
6416 _ => None,
6417 }
6418 }
6419
6420 fn autoscroll_carried(&mut self, dt: f32) -> bool {
6428 self.keep_carried_box();
6429 let Some(carried) = self.carried_rect() else {
6430 return false;
6431 };
6432 let area = {
6433 let scroll = &self.runtime.scroll;
6434 self.ui.as_ref().and_then(|ui| {
6435 ui.scroll_chain(self.cursor).find(|area| {
6436 area.accepts_user_offset(scroll.get(&area.id).copied().unwrap_or((0.0, 0.0)))
6437 })
6438 })
6439 };
6440 let Some(area) = area else {
6441 return false;
6442 };
6443 let offset = self
6444 .runtime
6445 .scroll
6446 .get(&area.id)
6447 .copied()
6448 .unwrap_or((0.0, 0.0));
6449 let room = |offset: f32, max: f32, reverse: bool| {
6452 if reverse {
6453 (max - offset, offset)
6454 } else {
6455 (offset, max - offset)
6456 }
6457 };
6458 let view = area.viewport;
6459 let dx = edge_autoscroll(
6460 (carried.x, carried.x + carried.width),
6461 (view.x, view.x + view.width),
6462 room(offset.0, area.max_x, area.reverse_x),
6463 dt,
6464 )
6465 .unwrap_or(0.0);
6466 let dy = edge_autoscroll(
6467 (carried.y, carried.y + carried.height),
6468 (view.y, view.y + view.height),
6469 room(offset.1, area.max_y, area.reverse_y),
6470 dt,
6471 )
6472 .unwrap_or(0.0);
6473 if dx == 0.0 && dy == 0.0 {
6474 return false;
6475 }
6476 let delta = area.offset_delta((dx, dy));
6479 let next = (
6480 (offset.0 + delta.0).clamp(0.0, area.max_x),
6481 (offset.1 + delta.1).clamp(0.0, area.max_y),
6482 );
6483 if next == offset {
6484 return false;
6485 }
6486 self.runtime.scroll.insert(area.id, next);
6487 self.runtime.scroll_target.insert(area.id, next);
6490 self.runtime.scroll_velocity.remove(&area.id);
6491 if let Some(drag) = self.drag.as_mut() {
6497 follow_content(
6498 drag,
6499 area.offset_delta((next.0 - offset.0, next.1 - offset.1)),
6500 );
6501 }
6502 true
6503 }
6504
6505 fn dragged_reorder_motion(&self) -> Option<ReorderMotion> {
6509 let Some(Drag::Reorder { id, .. }) = self.drag else {
6510 return None;
6511 };
6512 self.tree
6513 .as_ref()
6514 .and_then(|t| find_widget(t.as_ref(), id))
6515 .map(reorder_motion)
6516 }
6517
6518 fn advance_reorder_springs(&mut self, dt: f32) -> bool {
6521 let reorder_motion = matches!(self.drag, Some(Drag::Reorder { moved: true, .. }))
6522 .then(|| self.dragged_reorder_motion())
6523 .flatten();
6524 match reorder_motion {
6525 Some(ReorderMotion::Columns) => {
6526 self.reorder_x = spring_toward(self.reorder_x, self.cursor.x, dt, 0.07);
6527 (self.cursor.x - self.reorder_x).abs() > 0.5
6528 }
6529 Some(ReorderMotion::Slots(ReorderAxis::Vertical)) => {
6530 match self.reorder_drop_line(drag_preview::INSERT_THICKNESS) {
6531 Some(target) => {
6532 self.reorder_y = spring_toward(self.reorder_y, target.y, dt, 0.07);
6533 (target.y - self.reorder_y).abs() > 0.5
6534 }
6535 None => false,
6536 }
6537 }
6538 Some(ReorderMotion::Slots(ReorderAxis::Horizontal)) => {
6539 match self.reorder_drop_line(drag_preview::INSERT_THICKNESS) {
6540 Some(target) => {
6541 self.reorder_x = spring_toward(self.reorder_x, target.x, dt, 0.07);
6542 (target.x - self.reorder_x).abs() > 0.5
6543 }
6544 None => false,
6545 }
6546 }
6547 None => false,
6548 }
6549 }
6550
6551 fn report_overflows(&self, ui: &Ui<A::Message>) {
6567 for o in ui.overflows() {
6568 let mut key = std::collections::hash_map::DefaultHasher::new();
6572 use std::hash::{Hash, Hasher};
6573 (
6574 o.rect.width.to_bits(),
6575 o.rect.height.to_bits(),
6576 o.side as u8,
6577 o.amount.round() as i32,
6578 )
6579 .hash(&mut key);
6580 if !self.reported_overflows.borrow_mut().insert(key.finish()) {
6581 continue;
6582 }
6583 let side = match o.side {
6584 frus_widgets::Side::Left => "left",
6585 frus_widgets::Side::Right => "right",
6586 frus_widgets::Side::Top => "top",
6587 frus_widgets::Side::Bottom => "bottom",
6588 };
6589 log::warn!(
6590 "a box {:.0}x{:.0} is overflowed by {:.0} px on the {side}: its children do not \
6591 fit inside it, and what runs past the edge is drawn outside its parent — \
6592 invisible where something clips it, and untappable where it leaves the window. \
6593 Give the child that should give way an `Expanded`, or put the content in a \
6594 `SingleChildScrollView`.",
6595 o.rect.width,
6596 o.rect.height,
6597 o.amount,
6598 );
6599 }
6600 }
6601
6602 fn keystroke(&self, event: &KeyDown) -> Option<KeyStroke> {
6608 let key = match &event.logical_key {
6609 WinitKey::Named(NamedKey::Enter) => ShortcutKey::Enter,
6610 WinitKey::Named(NamedKey::Escape) => ShortcutKey::Escape,
6611 WinitKey::Named(NamedKey::Tab) => ShortcutKey::Tab,
6612 WinitKey::Named(NamedKey::Space) => ShortcutKey::Space,
6613 WinitKey::Named(NamedKey::Backspace) => ShortcutKey::Backspace,
6614 WinitKey::Named(NamedKey::Delete) => ShortcutKey::Delete,
6615 WinitKey::Named(NamedKey::ArrowUp) => ShortcutKey::Up,
6616 WinitKey::Named(NamedKey::ArrowDown) => ShortcutKey::Down,
6617 WinitKey::Named(NamedKey::ArrowLeft) => ShortcutKey::Left,
6618 WinitKey::Named(NamedKey::ArrowRight) => ShortcutKey::Right,
6619 WinitKey::Named(NamedKey::Home) => ShortcutKey::Home,
6620 WinitKey::Named(NamedKey::End) => ShortcutKey::End,
6621 WinitKey::Named(NamedKey::PageUp) => ShortcutKey::PageUp,
6622 WinitKey::Named(NamedKey::PageDown) => ShortcutKey::PageDown,
6623 WinitKey::Named(NamedKey::F1) => ShortcutKey::F(1),
6624 WinitKey::Named(NamedKey::F2) => ShortcutKey::F(2),
6625 WinitKey::Named(NamedKey::F3) => ShortcutKey::F(3),
6626 WinitKey::Named(NamedKey::F4) => ShortcutKey::F(4),
6627 WinitKey::Named(NamedKey::F5) => ShortcutKey::F(5),
6628 WinitKey::Named(NamedKey::F6) => ShortcutKey::F(6),
6629 WinitKey::Named(NamedKey::F7) => ShortcutKey::F(7),
6630 WinitKey::Named(NamedKey::F8) => ShortcutKey::F(8),
6631 WinitKey::Named(NamedKey::F9) => ShortcutKey::F(9),
6632 WinitKey::Named(NamedKey::F10) => ShortcutKey::F(10),
6633 WinitKey::Named(NamedKey::F11) => ShortcutKey::F(11),
6634 WinitKey::Character(text) => ShortcutKey::Char(text.chars().next()?),
6635 _ => return None,
6636 };
6637 Some(KeyStroke {
6638 key,
6639 ctrl: self.ctrl,
6640 shift: self.shift,
6641 alt: self.alt,
6642 meta: self.meta,
6643 })
6644 }
6645
6646 fn paint_reorder_preview(&self, ui: &Ui<A::Message>, theme: &Theme, scene: &mut Scene) {
6647 let Some(Drag::Reorder {
6648 start, moved: true, ..
6649 }) = self.drag
6650 else {
6651 return;
6652 };
6653 let Some((id, src)) = self.reorder_carried() else {
6656 return;
6657 };
6658 let motion = self
6659 .tree
6660 .as_ref()
6661 .and_then(|t| find_widget(t.as_ref(), id))
6662 .map(reorder_motion)
6663 .unwrap_or(ReorderMotion::Columns);
6664 let dx = self.cursor.x - start.x;
6665 let dy = self.cursor.y - start.y;
6666
6667 let (gx, gy) = ghost_offset(motion, (dx, dy));
6669
6670 let owners: std::collections::HashSet<u64> = self
6674 .tree
6675 .as_ref()
6676 .and_then(|t| find_widget(t.as_ref(), id))
6677 .map(|w| subtree_ids(w, id).iter().map(|i| i.as_u64()).collect())
6678 .unwrap_or_else(|| std::iter::once(id.as_u64()).collect());
6679
6680 match motion {
6681 ReorderMotion::Columns => {
6682 let reflowed =
6686 reflow_reorder_columns(scene.primitives(), src, self.reorder_x, id.as_u64());
6687 scene.clear();
6688 for primitive in reflowed {
6689 scene.push_primitive(primitive);
6690 }
6691 }
6692 ReorderMotion::Slots(axis) => {
6693 let line =
6704 self.reorder_drop_line(drag_preview::INSERT_THICKNESS)
6705 .map(|r| match axis {
6706 ReorderAxis::Vertical => Rect {
6707 y: self.reorder_y,
6708 ..r
6709 },
6710 ReorderAxis::Horizontal => Rect {
6711 x: self.reorder_x,
6712 ..r
6713 },
6714 });
6715 let movable = self
6721 .tree
6722 .as_ref()
6723 .map(|tree| reorderable_owners(ui, tree.as_ref()))
6724 .unwrap_or_default();
6725 let reflowed =
6726 reflow_reorder_cards(scene.primitives(), src, line, &owners, &movable, axis);
6727 scene.clear();
6728 for primitive in reflowed {
6729 scene.push_primitive(primitive);
6730 }
6731 if let Some(line) = line {
6732 let thickness = match axis {
6734 ReorderAxis::Vertical => line.height,
6735 ReorderAxis::Horizontal => line.width,
6736 };
6737 scene.set_clip(Rect::UNBOUNDED);
6738 scene.draw_rect(
6739 line,
6740 theme.primary,
6741 theme.radius.min(thickness * 0.5),
6742 0.0,
6743 Color::TRANSPARENT,
6744 );
6745 }
6746 }
6747 }
6748
6749 let ghost: Vec<Primitive> = ui
6755 .scene()
6756 .primitives()
6757 .iter()
6758 .filter(|p| owners.contains(&p.owner()))
6759 .map(|p| p.translated(gx, gy).with_clip(Rect::UNBOUNDED))
6760 .collect();
6761 draw_ghost_card(scene, theme, src.translate(gx, gy), &ghost);
6762 }
6763
6764 fn paint_drag_ghost(&self, ui: &Ui<A::Message>, theme: &Theme, scene: &mut Scene) {
6772 let Some(Drag::Item {
6773 source,
6774 start,
6775 moved: true,
6776 ..
6777 }) = &self.drag
6778 else {
6779 return;
6780 };
6781 let widget = self
6782 .tree
6783 .as_ref()
6784 .and_then(|tree| find_widget(tree.as_ref(), source.id));
6785 let owners: std::collections::HashSet<u64> = widget
6789 .map(|w| {
6790 subtree_ids(w, source.id)
6791 .iter()
6792 .map(|i| i.as_u64())
6793 .collect()
6794 })
6795 .unwrap_or_else(|| std::iter::once(source.id.as_u64()).collect());
6796 let opacity = widget.map(|w| w.drag_ghost_opacity()).unwrap_or(1.0);
6797 let dx = self.cursor.x - start.x;
6798 let dy = self.cursor.y - start.y;
6799
6800 let original: Vec<Primitive> = scene.primitives().to_vec();
6802 scene.clear();
6803 for primitive in &original {
6804 if owners.contains(&primitive.owner()) {
6805 scene.push_faded(primitive, opacity);
6806 } else {
6807 scene.push_primitive(primitive.clone());
6808 }
6809 }
6810
6811 let ghost: Vec<Primitive> = ui
6812 .scene()
6813 .primitives()
6814 .iter()
6815 .filter(|p| owners.contains(&p.owner()))
6816 .map(|p| p.translated(dx, dy).with_clip(Rect::UNBOUNDED))
6817 .collect();
6818 draw_ghost_card(scene, theme, source.rect.translate(dx, dy), &ghost);
6819 }
6820
6821 fn reorder_drop_line(&self, thickness: f32) -> Option<Rect> {
6827 let Some(Drag::Reorder {
6828 id, from, carried, ..
6829 }) = self.drag
6830 else {
6831 return None;
6832 };
6833 let target = self.reorder_drop_target(id, from, carried)?;
6836 let rect = self.ui.as_ref()?.widget_rect(target)?;
6837 let axis = match self
6838 .tree
6839 .as_ref()
6840 .and_then(|t| find_widget(t.as_ref(), target))
6841 .map(reorder_motion)
6842 {
6843 Some(ReorderMotion::Slots(axis)) => axis,
6844 _ => ReorderAxis::Vertical,
6845 };
6846 Some(drop_insertion_line(
6847 rect,
6848 thickness,
6849 self.reorder_insert_after(target, rect),
6850 axis,
6851 self.is_rtl(),
6852 ))
6853 }
6854
6855 fn reorder_insert_after(&self, target: WidgetId, rect: Rect) -> bool {
6861 match self
6862 .tree
6863 .as_ref()
6864 .and_then(|t| find_widget(t.as_ref(), target))
6865 .map(reorder_motion)
6866 {
6867 Some(ReorderMotion::Slots(axis)) => {
6868 reorder_drop_after(self.cursor, rect, axis, self.is_rtl())
6869 }
6870 _ => false,
6871 }
6872 }
6873
6874 fn pan_target(&self) -> Option<(WidgetId, frus_widgets::PanAxis)> {
6876 let (id, _) = self.ui.as_ref()?.pan_at(self.cursor)?;
6877 let axis = find_widget(self.tree.as_ref()?.as_ref(), id)?.pan_axis()?;
6878 Some((id, axis))
6879 }
6880
6881 fn pan_local(&self, id: WidgetId, at: Point) -> Point {
6883 let origin = self
6884 .ui
6885 .as_ref()
6886 .and_then(|ui| ui.widget_rect(id))
6887 .map_or(Point::new(0.0, 0.0), |rect| Point::new(rect.x, rect.y));
6888 Point::new(at.x - origin.x, at.y - origin.y)
6889 }
6890
6891 fn send_pan(&mut self, id: WidgetId, event: frus_widgets::PanEvent) {
6893 let rtl = self.is_rtl();
6894 let value = self.runtime.value(id);
6895 let (message, moved, released) = match self
6896 .tree
6897 .as_ref()
6898 .and_then(|tree| find_widget(tree.as_ref(), id))
6899 {
6900 Some(widget) => {
6901 let moved = widget.pan_value(event, value, rtl);
6905 let released = match event {
6906 frus_widgets::PanEvent::End { .. } if self.runtime.value_placed(id) => {
6907 Some(widget.on_value_release(value))
6908 }
6909 _ => None,
6910 };
6911 (widget.on_pan(event), moved, released)
6912 }
6913 None => (None, None, None),
6914 };
6915 if let Some(v) = moved {
6916 self.runtime.hold_value(id, v);
6917 }
6918 if let Some(release) = released {
6919 self.runtime.release_value(id);
6920 if let Some(message) = release {
6921 self.dispatch(message);
6922 }
6923 }
6924 if let Some(message) = message {
6925 self.dispatch(message);
6926 }
6927 self.request_redraw();
6928 }
6929
6930 fn dispatch_drag_edge(&mut self, id: WidgetId, edge: Edge, fraction: f32) {
6938 let message = self
6939 .tree
6940 .as_ref()
6941 .and_then(|tree| find_widget(tree.as_ref(), id))
6942 .and_then(|widget| match edge {
6943 Edge::Start => widget.on_drag_start(fraction),
6944 Edge::End => widget.on_drag_end(fraction),
6945 });
6946 if let Some(message) = message {
6947 self.dispatch(message);
6948 }
6949 }
6950
6951 fn thumb_drag(
6953 &mut self,
6954 id: WidgetId,
6955 thumb: usize,
6956 moment: frus_widgets::ThumbDrag,
6957 fraction: f32,
6958 ) {
6959 if moment == frus_widgets::ThumbDrag::Start {
6960 self.runtime.input.thumb = Some((id, thumb));
6962 }
6963 let message = self
6964 .tree
6965 .as_ref()
6966 .and_then(|tree| find_widget(tree.as_ref(), id))
6967 .and_then(|widget| widget.on_thumb_drag(thumb, moment, fraction));
6968 if let Some(message) = message {
6969 self.dispatch(message);
6970 }
6971 }
6972
6973 fn apply_widget_drag(&mut self, id: WidgetId, dx: f32, value: Option<f32>) {
6976 let message = self
6977 .tree
6978 .as_ref()
6979 .and_then(|tree| find_widget(tree.as_ref(), id))
6980 .and_then(|widget| {
6981 widget
6982 .on_drag_delta(dx)
6983 .or_else(|| value.and_then(|v| widget.on_drag(v)))
6984 });
6985 if let Some(message) = message {
6986 self.dispatch(message);
6987 }
6988 }
6989
6990 fn key_down(&mut self, event: &KeyDown, event_loop: Option<&ActiveEventLoop>) {
6995 if !self.runtime.focus_visible {
6997 self.runtime.focus_visible = true;
6998 self.request_redraw();
6999 }
7000
7001 if matches!(
7004 event.logical_key,
7005 WinitKey::Named(NamedKey::BrowserBack) | WinitKey::Named(NamedKey::GoBack)
7006 ) {
7007 if !event.repeat {
7008 if let Some(event_loop) = event_loop {
7009 self.system_back(event_loop);
7010 }
7011 }
7012 return;
7013 }
7014
7015 if matches!(event.logical_key, WinitKey::Named(NamedKey::F12)) {
7018 if cfg!(debug_assertions) && !event.repeat {
7019 self.inspector = !self.inspector;
7020 self.inspector_dump = self.inspector;
7021 self.request_redraw();
7022 }
7023 return;
7024 }
7025
7026 if let Some(stroke) = self.keystroke(event) {
7034 let typing = !stroke.is_command() && self.runtime.input.focused.is_some();
7035 if !typing {
7036 let msgs = self
7037 .ui
7038 .as_ref()
7039 .map(|ui| ui.keystroke(stroke, self.runtime.input.focused))
7040 .unwrap_or_default();
7041 if !msgs.is_empty() {
7042 for msg in msgs {
7043 self.dispatch(msg);
7044 }
7045 self.request_redraw();
7046 return;
7047 }
7048 }
7049 }
7050
7051 if matches!(event.logical_key, WinitKey::Named(NamedKey::Tab)) {
7053 let forward = !self.shift;
7054 let next = self
7055 .ui
7056 .as_ref()
7057 .and_then(|ui| ui.focus_next(self.runtime.input.focused, forward));
7058 if next.is_some() {
7059 self.runtime.input.focused = next;
7060 self.runtime.region = None;
7061 self.reveal_focus();
7062 self.request_redraw();
7063 }
7064 return;
7065 }
7066
7067 if matches!(event.logical_key, WinitKey::Named(NamedKey::Escape)) {
7071 if !event.repeat {
7073 self.escape();
7074 }
7075 return;
7076 }
7077
7078 let Some(focused) = self.runtime.input.focused else {
7079 self.region_key(event);
7082 return;
7083 };
7084
7085 let arrow = match event.logical_key {
7089 WinitKey::Named(NamedKey::ArrowUp) => Some(FocusDirection::Up),
7090 WinitKey::Named(NamedKey::ArrowDown) => Some(FocusDirection::Down),
7091 WinitKey::Named(NamedKey::ArrowLeft) => Some(FocusDirection::Left),
7092 WinitKey::Named(NamedKey::ArrowRight) => Some(FocusDirection::Right),
7093 _ => None,
7094 };
7095 if matches!(
7098 event.logical_key,
7099 WinitKey::Named(NamedKey::PageUp) | WinitKey::Named(NamedKey::PageDown)
7100 ) {
7101 let down = matches!(event.logical_key, WinitKey::Named(NamedKey::PageDown));
7102 if self.move_caret_vertical(focused, down, true) {
7103 return;
7104 }
7105 }
7106
7107 if let Some(direction) = arrow {
7108 if matches!(direction, FocusDirection::Up | FocusDirection::Down) {
7112 let down = matches!(direction, FocusDirection::Down);
7113 if self.move_caret_vertical(focused, down, false) {
7114 return;
7115 }
7116 }
7117
7118 if matches!(direction, FocusDirection::Left | FocusDirection::Right) {
7122 let key = if matches!(direction, FocusDirection::Left) {
7123 Key::Left {
7124 shift: self.shift,
7125 word: self.ctrl,
7126 }
7127 } else {
7128 Key::Right {
7129 shift: self.shift,
7130 word: self.ctrl,
7131 }
7132 };
7133 let widget = self
7134 .tree
7135 .as_ref()
7136 .and_then(|tree| find_widget(tree.as_ref(), focused));
7137 let reorder_from = widget.and_then(|w| w.reorder_index());
7141 let handled = widget.map(|w| w.on_key(&key));
7142 if let Some(KeyResponse::Handled(message)) = handled {
7143 if let Some(message) = message {
7144 self.dispatch(message);
7145 }
7146 if let Some(from) = reorder_from {
7147 let to = if matches!(direction, FocusDirection::Left) {
7148 from.wrapping_sub(1)
7149 } else {
7150 from + 1
7151 };
7152 self.set_announcement(format!("Column moved to position {}", to + 1));
7153 }
7154 self.request_redraw();
7155 return;
7156 }
7157 }
7158
7159 let is_text = self
7160 .tree
7161 .as_ref()
7162 .and_then(|tree| find_widget(tree.as_ref(), focused))
7163 .is_some_and(is_text_field);
7164 let navigates =
7165 !is_text || matches!(direction, FocusDirection::Up | FocusDirection::Down);
7166 if navigates {
7167 if let Some(next) = self
7168 .ui
7169 .as_ref()
7170 .and_then(|ui| ui.focus_directional(focused, direction))
7171 {
7172 self.runtime.input.focused = Some(next);
7173 self.reveal_focus();
7174 self.request_redraw();
7175 }
7176 return;
7177 }
7178 }
7179
7180 if matches!(
7185 event.logical_key,
7186 WinitKey::Named(NamedKey::Home) | WinitKey::Named(NamedKey::End)
7187 ) {
7188 let key = if matches!(event.logical_key, WinitKey::Named(NamedKey::Home)) {
7189 Key::Home {
7190 shift: self.shift,
7191 doc: self.ctrl,
7192 }
7193 } else {
7194 Key::End {
7195 shift: self.shift,
7196 doc: self.ctrl,
7197 }
7198 };
7199 let handled = self
7200 .tree
7201 .as_ref()
7202 .and_then(|tree| find_widget(tree.as_ref(), focused))
7203 .map(|widget| widget.on_key(&key));
7204 if let Some(KeyResponse::Handled(message)) = handled {
7205 if let Some(message) = message {
7206 self.dispatch(message);
7207 }
7208 self.request_redraw();
7209 return;
7210 }
7211 }
7212
7213 if matches!(
7217 event.logical_key,
7218 WinitKey::Named(NamedKey::Enter) | WinitKey::Named(NamedKey::Space)
7219 ) {
7220 let widget = self
7221 .tree
7222 .as_ref()
7223 .and_then(|tree| find_widget(tree.as_ref(), focused))
7224 .filter(|widget| widget.focusable());
7225 let message = widget.and_then(|widget| widget.on_click());
7226 if let Some(message) = message {
7227 if !event.repeat {
7230 let announce = widget.and_then(|widget| widget.announce());
7233 self.dispatch(message);
7234 if let Some(announce) = announce {
7235 self.set_announcement(announce);
7236 }
7237 self.request_redraw();
7238 }
7239 return;
7240 }
7241 }
7242
7243 if let Some(command) = clipboard_command(&event.logical_key, event.physical_key, self.ctrl)
7245 {
7246 match command {
7247 ClipCommand::Copy => {
7248 self.copy_selection(focused);
7249 }
7250 ClipCommand::Cut => self.cut_selection(focused),
7251 ClipCommand::Paste => {
7252 if let Some(pasted) = self.clipboard.paste(focused, self.window.as_ref()) {
7254 self.land_paste(pasted);
7255 }
7256 }
7257 }
7258 return;
7259 }
7260
7261 if opens_selection_bar(&event.logical_key, self.shift)
7265 && self
7266 .tree
7267 .as_ref()
7268 .and_then(|tree| find_widget(tree.as_ref(), focused))
7269 .is_some_and(|widget| widget.text_value().is_some())
7270 {
7271 self.show_selection_toolbar(focused);
7272 return;
7273 }
7274
7275 if self.ctrl {
7277 match &event.logical_key {
7278 WinitKey::Character(c) if c.eq_ignore_ascii_case("a") => {
7279 self.select_all(focused);
7280 return;
7281 }
7282 WinitKey::Character(c) if c.eq_ignore_ascii_case("z") && !self.shift => {
7286 self.step_history(focused, false);
7287 self.request_redraw();
7288 return;
7289 }
7290 WinitKey::Character(c)
7291 if c.eq_ignore_ascii_case("y")
7292 || (c.eq_ignore_ascii_case("z") && self.shift) =>
7293 {
7294 self.step_history(focused, true);
7295 self.request_redraw();
7296 return;
7297 }
7298 _ => {}
7299 }
7300 }
7301
7302 #[cfg(android)]
7307 if crate::android_ime::installed() {
7308 return;
7309 }
7310
7311 let shift = self.shift;
7312 let key = match &event.logical_key {
7313 WinitKey::Named(NamedKey::Backspace) => Some(Key::Backspace),
7314 WinitKey::Named(NamedKey::Delete) => Some(Key::Delete),
7315 WinitKey::Named(NamedKey::Enter) if !event.repeat => Some(Key::Enter),
7318 WinitKey::Named(NamedKey::Enter) => None,
7319 WinitKey::Named(NamedKey::ArrowLeft) => Some(Key::Left {
7321 shift,
7322 word: self.ctrl,
7323 }),
7324 WinitKey::Named(NamedKey::ArrowRight) => Some(Key::Right {
7325 shift,
7326 word: self.ctrl,
7327 }),
7328 WinitKey::Named(NamedKey::Home) => Some(Key::Home {
7329 shift,
7330 doc: self.ctrl,
7331 }),
7332 WinitKey::Named(NamedKey::End) => Some(Key::End {
7333 shift,
7334 doc: self.ctrl,
7335 }),
7336 WinitKey::Named(NamedKey::Space) => Some(Key::Text(" ".to_string())),
7337 WinitKey::Character(c) if c == "\n" || c == "\r" => {
7341 if event.repeat {
7342 None
7343 } else {
7344 Some(Key::Enter)
7345 }
7346 }
7347 _ => event.text.as_ref().map(|text| Key::Text(text.to_string())),
7348 };
7349
7350 if let Some(key) = key {
7351 self.apply_key(focused, key);
7352 self.request_redraw();
7353 }
7354 }
7355
7356 fn system_back(&mut self, event_loop: &ActiveEventLoop) {
7363 if !self.system_back_step() {
7364 event_loop.exit();
7365 }
7366 }
7367
7368 fn system_back_step(&mut self) -> bool {
7371 if self.close_selection_toolbar() {
7374 return true;
7375 }
7376 if self
7378 .runtime
7379 .region
7380 .as_ref()
7381 .is_some_and(|selection| selection.bar)
7382 {
7383 self.runtime.region = None;
7384 self.request_redraw();
7385 return true;
7386 }
7387 if let Some(message) = self.ui.as_ref().and_then(|ui| ui.top_dismiss()) {
7388 self.dispatch(message);
7389 self.request_redraw();
7390 return true;
7391 }
7392 if self.app.can_go_back() {
7393 self.build_dirty = true;
7394 self.app.go_back();
7396 self.request_redraw();
7397 return true;
7398 }
7399 false
7400 }
7401
7402 fn escape(&mut self) {
7407 if self.close_selection_toolbar() {
7409 return;
7410 }
7411 if self.runtime.region.take().is_some() {
7413 self.request_redraw();
7414 return;
7415 }
7416 let outcome: Option<Option<A::Message>> = self.runtime.input.focused.and_then(|focused| {
7419 let tree = self.tree.as_ref()?;
7420 let path = find_path(tree.as_ref(), focused);
7421 for widget in path.iter().rev() {
7422 match widget.on_key(&Key::Escape) {
7423 KeyResponse::Handled(message) => return Some(message),
7424 KeyResponse::Skip => return Some(None),
7425 KeyResponse::Ignored => {}
7426 }
7427 }
7428 None
7429 });
7430
7431 match outcome {
7432 Some(message) => {
7433 if let Some(message) = message {
7434 self.dispatch(message);
7435 self.request_redraw();
7436 }
7437 }
7438 None => {
7440 if let Some(message) = self.ui.as_ref().and_then(|ui| ui.top_dismiss()) {
7441 self.dispatch(message);
7442 self.request_redraw();
7443 }
7444 }
7445 }
7446 }
7447
7448 fn move_caret_vertical(&mut self, id: WidgetId, down: bool, page: bool) -> bool {
7454 let width = self
7455 .ui
7456 .as_ref()
7457 .and_then(|ui| ui.widget_rect(id))
7458 .map(|r| r.width)
7459 .unwrap_or(0.0);
7460 let cursor = self.runtime.edits.get(&id).map(|e| e.cursor).unwrap_or(0);
7461 let moved = self
7462 .tree
7463 .as_ref()
7464 .and_then(|tree| find_widget(tree.as_ref(), id))
7465 .and_then(|widget| widget.caret_vertical(width, cursor, down, page, self.goal_x));
7466 let Some((new_cursor, goal)) = moved else {
7467 return false;
7468 };
7469 let shift = self.shift;
7470 let edit = self.runtime.edits.entry(id).or_default();
7471 if shift {
7472 if edit.anchor.is_none() {
7473 edit.anchor = Some(edit.cursor);
7474 }
7475 } else {
7476 edit.anchor = None;
7477 }
7478 edit.cursor = new_cursor;
7479 self.goal_x = Some(goal);
7481 self.reveal_caret(id, new_cursor);
7482 self.request_redraw();
7483 true
7484 }
7485
7486 fn select_held_word(&mut self, id: WidgetId) {
7489 let cursor = self.runtime.edits.get(&id).map(|e| e.cursor).unwrap_or(0);
7490 let word = self
7491 .tree
7492 .as_ref()
7493 .and_then(|tree| find_widget(tree.as_ref(), id))
7494 .and_then(|widget| widget.word_at(cursor))
7495 .filter(|(start, end)| start < end);
7496 let Some((start, end)) = word else {
7497 self.show_selection_toolbar(id);
7500 return;
7501 };
7502 self.runtime.edits.insert(
7503 id,
7504 Edit {
7505 cursor: end,
7506 anchor: Some(start),
7507 composing: None,
7508 },
7509 );
7510 self.runtime.selection_handles = Some(id);
7511 self.runtime.close_edit_run(id);
7512 self.show_selection_toolbar(id);
7513 #[cfg(android)]
7514 self.push_ime_context(id);
7515 }
7516
7517 fn grab_selection_handle(&self) -> Option<Drag> {
7520 let id = self.runtime.selection_handles?;
7521 if self.runtime.input.focused != Some(id) {
7522 return None;
7523 }
7524 let rect = self.ui.as_ref()?.widget_rect(id)?;
7525 let edit = self.runtime.edits.get(&id).copied()?;
7526 let scroll_y = self.runtime.scroll.get(&id).map(|s| s.1).unwrap_or(0.0);
7527 let handles = find_widget(self.tree.as_ref()?.as_ref(), id)?
7528 .selection_handles(rect.width, &edit, scroll_y)?;
7529 let local = Point::new(self.cursor.x - rect.x, self.cursor.y - rect.y);
7530 let handle = crate::selection::grab(&handles, local)?;
7531 let at = handles[handle.index()].line_center;
7532 Some(Drag::SelectionHandle {
7533 id,
7534 rect,
7535 handle,
7536 grab: Point::new(at.x - local.x, at.y - local.y),
7537 })
7538 }
7539
7540 fn apply_key(&mut self, id: WidgetId, key: Key) {
7543 self.goal_x = None;
7545 self.runtime.selection_handles = None;
7548 self.runtime.selection_toolbar = None;
7549 let was = self.runtime.edits.get(&id).copied().unwrap_or_default();
7550 let mut edit = was;
7551 let widget = self
7552 .tree
7553 .as_ref()
7554 .and_then(|tree| find_widget(tree.as_ref(), id));
7555 let before = widget
7558 .and_then(|widget| widget.text_value())
7559 .map(str::to_owned);
7560 let announce = widget.and_then(|widget| widget.announce());
7565 let message = widget.and_then(|widget| widget.on_edit(&mut edit, &key));
7566 self.runtime.edits.insert(id, edit);
7567 self.reveal_caret(id, edit.cursor);
7569 if let Some(message) = message {
7570 self.dispatch_edit(message);
7571 if let Some(announce) = announce {
7572 self.set_announcement(announce);
7573 }
7574 }
7575 let after = self
7581 .tree
7582 .as_ref()
7583 .and_then(|tree| find_widget(tree.as_ref(), id))
7584 .and_then(|widget| widget.text_value())
7585 .map(str::to_owned);
7586 match (before, after) {
7587 (Some(before), Some(after)) if before != after => {
7588 let kind = EditKind::of(&key, was.selection_range().is_some(), self.composing());
7589 let since = self.since_last_edit();
7590 self.runtime
7591 .record_edit(id, EditSnapshot::new(before, was), kind, since);
7592 }
7593 (Some(_), _) => self.runtime.close_edit_run(id),
7596 _ => {}
7597 }
7598 }
7599
7600 fn dispatch_edit(&mut self, message: A::Message) {
7608 self.dispatch(message);
7609 if let Some((width, height)) = self.last_size {
7610 let theme = self.themes.displayed(&self.app);
7611 let tree = self
7615 .media_query(width, height)
7616 .scope(|| build_view(&self.app, &theme, &self.runtime));
7617 self.tree = Some(tree);
7618 }
7619 }
7620
7621 fn since_last_edit(&mut self) -> f32 {
7624 let now = Instant::now();
7625 let since = self
7626 .last_edit_at
7627 .map(|then| now.duration_since(then).as_secs_f32())
7628 .unwrap_or(f32::INFINITY);
7629 self.last_edit_at = Some(now);
7630 since
7631 }
7632
7633 fn composing(&self) -> bool {
7635 self.runtime
7636 .input
7637 .focused
7638 .and_then(|id| self.runtime.edits.get(&id))
7639 .is_some_and(|edit| edit.composing.is_some())
7640 }
7641
7642 fn step_history(&mut self, id: WidgetId, forward: bool) -> bool {
7646 let value = self
7647 .tree
7648 .as_ref()
7649 .and_then(|tree| find_widget(tree.as_ref(), id))
7650 .and_then(|widget| widget.text_value())
7651 .map(str::to_owned);
7652 let Some(value) = value else {
7653 return false;
7654 };
7655 let edit = self.runtime.edits.get(&id).copied().unwrap_or_default();
7656 let current = EditSnapshot::new(value, edit);
7657 let target = if forward {
7658 self.runtime.redo_edit(id, current)
7659 } else {
7660 self.runtime.undo_edit(id, current)
7661 };
7662 let Some(target) = target else {
7663 return false;
7664 };
7665 let message = self
7666 .tree
7667 .as_ref()
7668 .and_then(|tree| find_widget(tree.as_ref(), id))
7669 .and_then(|widget| widget.replace_value(target.value.clone()));
7670 self.runtime.edits.insert(id, target.edit);
7673 self.reveal_caret(id, target.edit.cursor);
7674 if let Some(message) = message {
7675 self.dispatch_edit(message);
7676 }
7677 self.last_edit_at = None;
7680 true
7681 }
7682
7683 fn reveal_focus(&mut self) {
7695 let Some(id) = self.runtime.input.focused else {
7696 return;
7697 };
7698 let Some(moves) = self.ui.as_ref().map(|ui| ui.reveal(id, &self.runtime)) else {
7699 return;
7700 };
7701 for (area, offset) in moves {
7702 self.runtime.scroll_ballistic.remove(&area);
7703 self.runtime.scroll_velocity.remove(&area);
7704 self.runtime.scroll_target.insert(area, offset);
7705 }
7706 }
7707
7708 fn reveal_caret(&mut self, id: WidgetId, cursor: usize) {
7712 let Some(vp) = self.ui.as_ref().and_then(|ui| ui.scrollable_viewport(id)) else {
7713 return;
7714 };
7715 let metrics = self
7716 .tree
7717 .as_ref()
7718 .and_then(|tree| find_widget(tree.as_ref(), id))
7719 .and_then(|widget| widget.text_metrics(vp.width, cursor));
7720 let Some((content_h, visible_h, caret_top, caret_h)) = metrics else {
7721 return;
7722 };
7723 let max_y = (content_h - visible_h).max(0.0);
7724 let cur = self.runtime.scroll.get(&id).map(|s| s.1).unwrap_or(0.0);
7725 let lo = (caret_top + caret_h - visible_h).max(0.0);
7728 let hi = caret_top;
7729 let target = cur.clamp(lo.min(hi), lo.max(hi)).clamp(0.0, max_y);
7730 self.runtime.scroll.insert(id, (0.0, target));
7731 self.runtime.scroll_target.insert(id, (0.0, target));
7732 self.runtime.scroll_velocity.remove(&id);
7733 }
7734
7735 fn copy_selection(&mut self, id: WidgetId) -> bool {
7738 let edit = self.runtime.edits.get(&id).copied().unwrap_or_default();
7739 let text = self
7740 .tree
7741 .as_ref()
7742 .and_then(|tree| find_widget(tree.as_ref(), id))
7743 .and_then(|widget| widget.selected_text(&edit));
7744 match text {
7745 Some(text) => {
7746 self.clipboard.set_text(text);
7747 true
7748 }
7749 None => false,
7750 }
7751 }
7752
7753 fn region_stops(&self, area: WidgetId) -> Vec<(WidgetId, usize)> {
7756 let (Some(ui), Some(tree)) = (self.ui.as_ref(), self.tree.as_ref()) else {
7757 return Vec::new();
7758 };
7759 ui.text_stops_in(area)
7760 .filter_map(|stop| {
7761 let text = find_widget(tree.as_ref(), stop.id)?.selection_text()?;
7762 Some((stop.id, text.chars().count()))
7763 })
7764 .collect()
7765 }
7766
7767 fn region_selection(
7769 &self,
7770 area: WidgetId,
7771 anchor: RegionPoint,
7772 extent: RegionPoint,
7773 ) -> RegionSelection {
7774 RegionSelection::new(area, anchor, extent, &self.region_stops(area))
7775 }
7776
7777 fn region_index(&self, stop: &TextStop, at: Point) -> Option<usize> {
7779 find_widget(self.tree.as_ref()?.as_ref(), stop.id)?.selection_hit(
7780 at.x - stop.rect.x,
7781 at.y - stop.rect.y,
7782 stop.rect.width,
7783 )
7784 }
7785
7786 fn begin_region_selection(&mut self) {
7790 let Some(stop) = self.region_press_target() else {
7791 return;
7792 };
7793 let Some(index) = self.region_index(&stop, self.cursor) else {
7794 return;
7795 };
7796 let now = Instant::now();
7797 let here = self.cursor;
7798 let double = self.last_region_click.is_some_and(|(then, there)| {
7799 (now - then).as_secs_f32() < 0.4
7800 && (here.x - there.x).hypot(here.y - there.y) <= REGION_DOUBLE_CLICK_SLOP
7801 });
7802 self.last_region_click = Some((now, here));
7803 let word = double
7804 .then(|| {
7805 let tree = self.tree.as_ref()?;
7806 let text = find_widget(tree.as_ref(), stop.id)?.selection_text()?;
7807 Some(frus_widgets::word_bounds(text, index))
7808 })
7809 .flatten();
7810 let (anchor, extent) = match word {
7811 Some((start, end)) => (
7812 RegionPoint {
7813 text: stop.id,
7814 index: start,
7815 },
7816 RegionPoint {
7817 text: stop.id,
7818 index: end,
7819 },
7820 ),
7821 None => {
7822 let point = RegionPoint {
7823 text: stop.id,
7824 index,
7825 };
7826 (point, point)
7827 }
7828 };
7829 self.runtime.region = Some(self.region_selection(stop.area, anchor, extent));
7830 self.drag = word
7832 .is_none()
7833 .then_some(Drag::RegionSelect { area: stop.area });
7834 self.request_redraw();
7835 }
7836
7837 fn region_press_target(&self) -> Option<TextStop> {
7840 let ui = self.ui.as_ref()?;
7841 let claimed = self
7842 .runtime
7843 .input
7844 .pressed
7845 .is_some_and(|pressed| ui.msg_for(pressed).is_some());
7846 if claimed {
7847 return None;
7848 }
7849 ui.text_stop_at(self.cursor)
7850 }
7851
7852 fn select_region_word_held(&mut self) -> bool {
7855 let Some(stop) = self.ui.as_ref().and_then(|ui| ui.text_stop_at(self.cursor)) else {
7856 return false;
7857 };
7858 let Some(index) = self.region_index(&stop, self.cursor) else {
7859 return false;
7860 };
7861 let word = self
7862 .tree
7863 .as_ref()
7864 .and_then(|tree| find_widget(tree.as_ref(), stop.id))
7865 .and_then(|widget| widget.selection_text())
7866 .map(|text| frus_widgets::word_bounds(text, index))
7867 .filter(|(start, end)| start < end);
7868 let Some((start, end)) = word else {
7869 return false;
7870 };
7871 let anchor = RegionPoint {
7872 text: stop.id,
7873 index: start,
7874 };
7875 let extent = RegionPoint {
7876 text: stop.id,
7877 index: end,
7878 };
7879 self.runtime.region = Some(
7880 self.region_selection(stop.area, anchor, extent)
7881 .with_bar(true),
7882 );
7883 self.request_redraw();
7884 true
7885 }
7886
7887 fn region_handles(&self) -> Option<(WidgetId, [frus_widgets::SelectionHandle; 2])> {
7889 let selection = self.runtime.region.as_ref().filter(|s| s.handles)?;
7890 let (ui, tree) = (self.ui.as_ref()?, self.tree.as_ref()?);
7891 let at = |point: RegionPoint, start: bool| {
7892 let stop = ui
7893 .text_stops_in(selection.area)
7894 .find(|s| s.id == point.text)?;
7895 let handle = find_widget(tree.as_ref(), point.text)?.selection_grip(
7896 stop.rect.width,
7897 point.index,
7898 start,
7899 )?;
7900 let (dx, dy) = (stop.rect.x, stop.rect.y);
7901 Some(frus_widgets::SelectionHandle {
7902 rect: handle.rect.translate(dx, dy),
7903 line_center: Point::new(handle.line_center.x + dx, handle.line_center.y + dy),
7904 })
7905 };
7906 Some((
7907 selection.area,
7908 [at(selection.start?, true)?, at(selection.end?, false)?],
7909 ))
7910 }
7911
7912 fn grab_region_handle(&self) -> Option<Drag> {
7914 let (area, handles) = self.region_handles()?;
7915 let handle = crate::selection::grab(&handles, self.cursor)?;
7916 let at = handles[handle.index()].line_center;
7917 Some(Drag::RegionHandle {
7918 area,
7919 handle,
7920 grab: Point::new(at.x - self.cursor.x, at.y - self.cursor.y),
7921 })
7922 }
7923
7924 fn drag_region_handle(
7928 &mut self,
7929 area: WidgetId,
7930 handle: crate::selection::Handle,
7931 grab: Point,
7932 ) {
7933 let Some(selection) = self.runtime.region.as_ref() else {
7934 return;
7935 };
7936 let (Some(start), Some(end)) = (selection.start, selection.end) else {
7937 return;
7938 };
7939 let target = Point::new(self.cursor.x + grab.x, self.cursor.y + grab.y);
7940 let Some(stop) = self
7941 .ui
7942 .as_ref()
7943 .and_then(|ui| ui.nearest_text_stop(area, target))
7944 else {
7945 return;
7946 };
7947 let Some(index) = self.region_index(&stop, target) else {
7948 return;
7949 };
7950 let moved = RegionPoint {
7951 text: stop.id,
7952 index,
7953 };
7954 let stops = self.region_stops(area);
7955 let order = |point: RegionPoint| {
7956 stops
7957 .iter()
7958 .position(|(id, _)| *id == point.text)
7959 .map(|at| (at, point.index))
7960 };
7961 let (Some(to), Some(other)) = (
7962 order(moved),
7963 order(match handle {
7964 crate::selection::Handle::Start => end,
7965 crate::selection::Handle::End => start,
7966 }),
7967 ) else {
7968 return;
7969 };
7970 let (fixed, crosses) = match handle {
7971 crate::selection::Handle::Start => (end, to >= other),
7972 crate::selection::Handle::End => (start, to <= other),
7973 };
7974 if crosses {
7975 return;
7976 }
7977 let next = RegionSelection::new(area, fixed, moved, &stops).with_bar(true);
7978 let next = RegionSelection { bar: false, ..next };
7980 if self.runtime.region.as_ref() != Some(&next) {
7981 self.runtime.region = Some(next);
7982 self.request_redraw();
7983 }
7984 }
7985
7986 fn extend_region_selection(&mut self, area: WidgetId) {
7989 let Some(anchor) = self.runtime.region.as_ref().map(|region| region.anchor) else {
7990 return;
7991 };
7992 let Some(stop) = self
7993 .ui
7994 .as_ref()
7995 .and_then(|ui| ui.nearest_text_stop(area, self.cursor))
7996 else {
7997 return;
7998 };
7999 let Some(index) = self.region_index(&stop, self.cursor) else {
8000 return;
8001 };
8002 let extent = RegionPoint {
8003 text: stop.id,
8004 index,
8005 };
8006 let selection = self.region_selection(area, anchor, extent);
8007 if self.runtime.region.as_ref() != Some(&selection) {
8008 self.runtime.region = Some(selection);
8009 self.request_redraw();
8010 }
8011 }
8012
8013 fn region_text(&self) -> Option<String> {
8016 let region = self.runtime.region.as_ref()?;
8017 let (ui, tree) = (self.ui.as_ref()?, self.tree.as_ref()?);
8018 let texts: Vec<(WidgetId, &str)> = ui
8019 .text_stops_in(region.area)
8020 .filter_map(|stop| {
8021 let text = find_widget(tree.as_ref(), stop.id)?.selection_text()?;
8022 Some((stop.id, text))
8023 })
8024 .collect();
8025 let copied = frus_widgets::region_copy(&texts, ®ion.ranges);
8026 (!copied.is_empty()).then_some(copied)
8027 }
8028
8029 fn copy_region(&mut self) -> bool {
8031 match self.region_text() {
8032 Some(copied) => {
8033 self.clipboard.set_text(copied);
8034 true
8035 }
8036 None => false,
8037 }
8038 }
8039
8040 fn select_all_region(&mut self) {
8042 let Some(area) = self.runtime.region.as_ref().map(|region| region.area) else {
8043 return;
8044 };
8045 let stops = self.region_stops(area);
8046 let (Some(first), Some(last)) = (stops.first(), stops.last()) else {
8047 return;
8048 };
8049 let anchor = RegionPoint {
8050 text: first.0,
8051 index: 0,
8052 };
8053 let extent = RegionPoint {
8054 text: last.0,
8055 index: last.1,
8056 };
8057 let bar = self
8058 .runtime
8059 .region
8060 .as_ref()
8061 .is_some_and(|selection| selection.bar);
8062 self.runtime.region = Some(self.region_selection(area, anchor, extent).with_bar(bar));
8063 self.request_redraw();
8064 }
8065
8066 fn region_key(&mut self, event: &KeyDown) -> bool {
8069 if self.runtime.region.is_none() {
8070 return false;
8071 }
8072 if let Some(ClipCommand::Copy) =
8073 clipboard_command(&event.logical_key, event.physical_key, self.ctrl)
8074 {
8075 self.copy_region();
8076 return true;
8077 }
8078 let select_all = self.ctrl
8079 && matches!(&event.logical_key, WinitKey::Character(c) if c.eq_ignore_ascii_case("a"));
8080 if select_all {
8081 self.select_all_region();
8082 return true;
8083 }
8084 false
8085 }
8086
8087 fn cut_selection(&mut self, id: WidgetId) {
8091 if self.copy_selection(id) {
8092 self.apply_key(id, Key::Backspace);
8093 self.request_redraw();
8094 }
8095 }
8096
8097 fn select_all(&mut self, id: WidgetId) {
8099 self.runtime.edits.insert(
8100 id,
8101 Edit {
8102 cursor: usize::MAX,
8103 anchor: Some(0),
8104 composing: None,
8105 },
8106 );
8107 self.runtime.close_edit_run(id);
8110 self.request_redraw();
8111 }
8112
8113 fn show_selection_toolbar(&mut self, id: WidgetId) {
8117 self.show_selection_toolbar_at(id, None);
8118 }
8119
8120 fn show_selection_toolbar_at(&mut self, id: WidgetId, at: Option<Point>) {
8123 let can_paste = self.clipboard.has_text();
8124 self.runtime.selection_toolbar = Some(ToolbarMark { id, can_paste, at });
8125 self.request_redraw();
8126 }
8127
8128 fn close_selection_toolbar(&mut self) -> bool {
8131 if self.runtime.selection_toolbar.is_none() {
8132 return false;
8133 }
8134 self.runtime.selection_handles = None;
8135 self.hide_selection_toolbar();
8136 true
8137 }
8138
8139 fn hide_selection_toolbar(&mut self) {
8141 if self.runtime.selection_toolbar.take().is_some() {
8142 self.request_redraw();
8143 }
8144 }
8145
8146 fn perform_edit_action(&mut self, action: frus_widgets::EditAction) {
8151 use frus_widgets::EditAction;
8152 if self
8154 .runtime
8155 .region
8156 .as_ref()
8157 .is_some_and(|selection| selection.bar)
8158 {
8159 match action {
8160 EditAction::Copy => {
8161 self.copy_region();
8163 self.runtime.region = None;
8164 }
8165 EditAction::SelectAll => self.select_all_region(),
8166 EditAction::Cut | EditAction::Paste => {}
8167 }
8168 self.request_redraw();
8169 return;
8170 }
8171 let Some(id) = self.runtime.selection_toolbar.map(|mark| mark.id) else {
8172 return;
8173 };
8174 match action {
8175 EditAction::Copy => {
8176 if self.copy_selection(id) {
8177 if let Some(edit) = self.runtime.edits.get_mut(&id) {
8180 *edit = collapsed_to_end(*edit);
8181 }
8182 self.runtime.close_edit_run(id);
8183 }
8184 self.runtime.selection_handles = None;
8185 self.hide_selection_toolbar();
8186 }
8187 EditAction::Cut => {
8188 self.cut_selection(id);
8190 self.hide_selection_toolbar();
8191 }
8192 EditAction::Paste => {
8193 self.runtime.selection_handles = None;
8194 self.hide_selection_toolbar();
8195 if let Some(pasted) = self.clipboard.paste(id, self.window.as_ref()) {
8197 self.land_paste(pasted);
8198 }
8199 }
8200 EditAction::SelectAll => {
8201 self.select_all(id);
8202 if self.runtime.selection_handles.is_some() {
8205 self.runtime.selection_handles = Some(id);
8206 }
8207 if select_all_hides_bar(frus_widgets::default_target_platform()) {
8208 self.runtime.selection_handles = None;
8211 self.hide_selection_toolbar();
8212 } else {
8213 let at = self.runtime.selection_toolbar.and_then(|mark| mark.at);
8216 self.show_selection_toolbar_at(id, at);
8217 }
8218 }
8219 }
8220 #[cfg(android)]
8221 self.push_ime_context(id);
8222 }
8223
8224 fn context_click(&mut self) {
8228 let secondary = self
8230 .ui
8231 .as_ref()
8232 .and_then(|ui| ui.hit(self.cursor))
8233 .and_then(|id| {
8234 self.tree
8235 .as_ref()
8236 .and_then(|tree| find_widget(tree.as_ref(), id))
8237 })
8238 .and_then(|widget| widget.on_secondary_tap());
8239 if let Some(message) = secondary {
8240 self.hide_selection_toolbar();
8241 self.dispatch(message);
8242 self.request_redraw();
8243 return;
8244 }
8245 let hit = self.ui.as_ref().and_then(|ui| ui.focus_hit(self.cursor));
8246 let Some((id, rect)) = hit else {
8247 self.hide_selection_toolbar();
8248 return;
8249 };
8250 let was_focused = self.runtime.input.focused == Some(id);
8251 let scroll_y = self.runtime.scroll.get(&id).map(|s| s.1).unwrap_or(0.0);
8252 let scroll_cursor = if was_focused {
8253 self.runtime.edits.get(&id).map(|e| e.cursor).unwrap_or(0)
8254 } else {
8255 0
8256 };
8257 let cursor = self
8258 .tree
8259 .as_ref()
8260 .and_then(|tree| find_widget(tree.as_ref(), id))
8261 .and_then(|widget| {
8262 widget.cursor_at(
8263 self.cursor.x - rect.x,
8264 self.cursor.y - rect.y + scroll_y,
8265 rect.width,
8266 scroll_cursor,
8267 )
8268 });
8269 let Some(cursor) = cursor else {
8271 self.hide_selection_toolbar();
8272 return;
8273 };
8274 let inside = was_focused
8275 && self
8276 .runtime
8277 .edits
8278 .get(&id)
8279 .and_then(|edit| edit.selection_range())
8280 .is_some_and(|(start, end)| cursor >= start && cursor <= end);
8281 self.runtime.input.focused = Some(id);
8282 self.runtime.selection_handles = None;
8283 if !inside {
8284 self.goal_x = None;
8285 self.runtime.edits.insert(
8286 id,
8287 Edit {
8288 cursor,
8289 anchor: None,
8290 composing: None,
8291 },
8292 );
8293 self.runtime.close_edit_run(id);
8294 }
8295 self.show_selection_toolbar_at(id, Some(self.cursor));
8296 }
8297
8298 fn land_paste(&mut self, pasted: clip::Pasted) {
8301 if pasted.lands(self.runtime.input.focused) {
8302 self.apply_key(pasted.into, Key::Text(pasted.text));
8303 self.request_redraw();
8304 }
8305 }
8306}
8307
8308fn collapsed_to_end(edit: Edit) -> Edit {
8312 let cursor = edit.selection_range().map_or(edit.cursor, |(_, end)| end);
8313 Edit {
8314 cursor,
8315 anchor: None,
8316 composing: None,
8317 }
8318}
8319
8320#[derive(Clone, Copy, PartialEq, Eq)]
8322enum Edge {
8323 Start,
8324 End,
8325}
8326
8327fn cursor_icon(cursor: UiCursor) -> CursorIcon {
8340 match cursor {
8341 UiCursor::Default | UiCursor::None => CursorIcon::Default,
8342 UiCursor::Pointer => CursorIcon::Pointer,
8343 UiCursor::Text => CursorIcon::Text,
8344 UiCursor::Forbidden => CursorIcon::NotAllowed,
8345 UiCursor::Wait => CursorIcon::Wait,
8346 UiCursor::Progress => CursorIcon::Progress,
8347 UiCursor::Help => CursorIcon::Help,
8348 UiCursor::Precise => CursorIcon::Crosshair,
8349 UiCursor::Move => CursorIcon::Move,
8350 UiCursor::Grab => CursorIcon::Grab,
8351 UiCursor::Grabbing => CursorIcon::Grabbing,
8352 UiCursor::ResizeColumn => CursorIcon::ColResize,
8353 UiCursor::ResizeRow => CursorIcon::RowResize,
8354 UiCursor::ZoomIn => CursorIcon::ZoomIn,
8355 UiCursor::ZoomOut => CursorIcon::ZoomOut,
8356 UiCursor::ContextMenu => CursorIcon::ContextMenu,
8357 }
8358}
8359
8360fn mask_pan(axis: frus_widgets::PanAxis, value: Point) -> Point {
8362 match axis {
8363 frus_widgets::PanAxis::Free => value,
8364 frus_widgets::PanAxis::Horizontal => Point::new(value.x, 0.0),
8365 frus_widgets::PanAxis::Vertical => Point::new(0.0, value.y),
8366 }
8367}
8368
8369fn gesture_was_a_tap(ended: Option<&Drag>) -> bool {
8370 matches!(
8371 ended,
8372 Some(
8373 Drag::Scroll { moved: false, .. }
8374 | Drag::Pan { moved: false, .. }
8375 | Drag::Gesture { moved: false, .. }
8376 | Drag::Reorder { moved: false, .. }
8377 | Drag::Item { moved: false, .. }
8378 | Drag::Dismiss { moved: false, .. }
8379 | Drag::Sheet { moved: false, .. }
8380 )
8381 )
8382}
8383
8384fn press_at<Msg: Clone>(runtime: &mut Runtime, ui: Option<&Ui<Msg>>, at: Point) -> bool {
8388 runtime.input.pressed = ui.and_then(|ui| ui.hit(at));
8389 let Some(pressed) = runtime.input.pressed else {
8390 return false;
8391 };
8392 let Some(rect) = ui.and_then(|ui| ui.ink_box(pressed)) else {
8393 return false;
8394 };
8395 runtime.ink_press(
8396 pressed,
8397 Point::new(at.x - rect.x, at.y - rect.y),
8398 Size::new(rect.width, rect.height),
8399 );
8400 true
8401}
8402
8403fn settle_press(
8421 input: &mut frus_widgets::InputState,
8422 kind: PointerKind,
8423 could_tap: bool,
8424 drag: Option<&Drag>,
8425 touch: bool,
8426) -> bool {
8427 let taken = match kind {
8428 PointerKind::Down => false,
8429 PointerKind::Move => could_tap && !gesture_was_a_tap(drag),
8430 PointerKind::Up | PointerKind::Cancel => true,
8431 };
8432 if !taken {
8433 return false;
8434 }
8435 let mut changed = input.pressed.take().is_some();
8436 if touch {
8437 changed |= input.hovered.take().is_some();
8438 }
8439 changed
8440}
8441
8442const AUTOSCROLL_VELOCITY: f32 = 50.0;
8446
8447const AUTOSCROLL_MAX_OVERHANG: f32 = 20.0;
8451
8452fn apply_scroll_requests<Msg>(
8465 runtime: &mut Runtime,
8466 tree: &dyn Widget<Msg>,
8467 requests: Vec<(u64, ScrollTo)>,
8468 regions: &[Scrollable],
8469) -> (Vec<(u64, ScrollTo)>, bool) {
8470 let mut unplaced = Vec::new();
8471 let mut moved = false;
8472 for (key, to) in requests {
8473 let area = find_by_key(tree, key).and_then(|id| regions.iter().find(|a| a.id == id));
8474 match area {
8475 Some(area) => moved |= runtime.scroll_to(area, to),
8476 None => unplaced.push((key, to)),
8477 }
8478 }
8479 (unplaced, moved)
8480}
8481
8482fn apply_sheet_requests<Msg>(
8489 runtime: &mut Runtime,
8490 tree: &dyn Widget<Msg>,
8491 requests: Vec<(u64, SheetTo)>,
8492 sheets: &[frus_widgets::SheetArea],
8493) -> (Vec<(u64, SheetTo)>, bool) {
8494 let mut unplaced = Vec::new();
8495 let mut moved = false;
8496 for (key, to) in requests {
8497 let sheet = frus_widgets::find_sheet_by_key(tree, key)
8498 .and_then(|id| sheets.iter().find(|sheet| sheet.id == id));
8499 match sheet {
8500 Some(sheet) => moved |= runtime.sheet_to(sheet.id, &sheet.spec, &to),
8501 None => unplaced.push((key, to)),
8502 }
8503 }
8504 (unplaced, moved)
8505}
8506
8507fn follow_content(drag: &mut Drag, shift: (f32, f32)) {
8513 match drag {
8514 Drag::Reorder { start, carried, .. } => {
8515 start.x += shift.0;
8516 start.y += shift.1;
8517 if let Some((_, rect)) = carried {
8518 *rect = rect.translate(shift.0, shift.1);
8519 }
8520 }
8521 Drag::Item { source, start, .. } => {
8522 start.x += shift.0;
8523 start.y += shift.1;
8524 source.rect = source.rect.translate(shift.0, shift.1);
8525 }
8526 _ => {}
8527 }
8528}
8529
8530fn edge_autoscroll(item: (f32, f32), view: (f32, f32), room: (f32, f32), dt: f32) -> Option<f32> {
8543 let before = view.0 - item.0; let after = item.1 - view.1; let overhang = if before > 0.0 && room.0 > 0.0 {
8548 before.min(AUTOSCROLL_MAX_OVERHANG)
8549 } else if after > 0.0 && room.1 > 0.0 {
8550 -after.min(AUTOSCROLL_MAX_OVERHANG)
8551 } else {
8552 return None;
8553 };
8554 let travel = overhang * AUTOSCROLL_VELOCITY * dt;
8555 (travel != 0.0).then_some(travel)
8556}
8557
8558fn spring_toward(current: f32, target: f32, dt: f32, tau: f32) -> f32 {
8562 let k = 1.0 - (-dt / tau.max(1e-4)).exp();
8563 current + (target - current) * k
8564}
8565
8566const FOCUS_HISTORY_MAX: usize = 8;
8568
8569fn resolve_focus(
8577 current: Option<WidgetId>,
8578 present: &std::collections::HashSet<WidgetId>,
8579 history: &mut Vec<WidgetId>,
8580 prev: &mut Option<WidgetId>,
8581) -> Option<WidgetId> {
8582 let mut cur = current;
8583 if let Some(c) = cur {
8584 if !present.contains(&c) {
8585 cur = None;
8586 while let Some(cand) = history.pop() {
8587 if present.contains(&cand) {
8588 cur = Some(cand);
8589 break;
8590 }
8591 }
8592 }
8593 }
8594 if *prev != cur {
8595 if let Some(old) = *prev {
8596 if present.contains(&old) && Some(old) != cur {
8597 history.push(old);
8598 if history.len() > FOCUS_HISTORY_MAX {
8599 history.remove(0);
8600 }
8601 }
8602 }
8603 *prev = cur;
8604 }
8605 cur
8606}
8607
8608fn draw_ghost_card(scene: &mut Scene, theme: &Theme, card: Rect, ghost: &[Primitive]) {
8614 use drag_preview::{BORDER_ALPHA, BORDER_WIDTH, SHADOW_ALPHA, SHADOW_BLUR, SHADOW_OFFSET_Y};
8615 scene.set_clip(Rect::UNBOUNDED);
8616 let shadow = theme.scheme.shadow.with_alpha(SHADOW_ALPHA);
8620 scene.shadow(
8621 card.translate(0.0, SHADOW_OFFSET_Y),
8622 shadow,
8623 theme.radius,
8624 SHADOW_BLUR,
8625 );
8626 let border = theme.primary.fade(BORDER_ALPHA);
8627 if ghost.is_empty() {
8628 scene.draw_rect(card, theme.surface, theme.radius, BORDER_WIDTH, border);
8629 } else {
8630 scene.draw_rect(card, theme.surface, theme.radius, 0.0, Color::TRANSPARENT);
8631 for primitive in ghost {
8632 scene.push_primitive(primitive.clone());
8633 }
8634 scene.draw_rect(card, Color::TRANSPARENT, theme.radius, BORDER_WIDTH, border);
8635 }
8636}
8637
8638#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8640enum ReorderMotion {
8641 Columns,
8644 Slots(ReorderAxis),
8647}
8648
8649impl ReorderMotion {
8650 fn of(axis: ReorderAxis, inserts: bool) -> Self {
8654 match axis {
8655 ReorderAxis::Vertical => Self::Slots(ReorderAxis::Vertical),
8656 ReorderAxis::Horizontal if inserts => Self::Slots(ReorderAxis::Horizontal),
8657 ReorderAxis::Horizontal => Self::Columns,
8658 }
8659 }
8660}
8661
8662fn reorder_motion<Msg>(widget: &dyn Widget<Msg>) -> ReorderMotion {
8664 ReorderMotion::of(widget.reorder_axis(), widget.reorder_inserts())
8665}
8666
8667fn ghost_offset(motion: ReorderMotion, travel: (f32, f32)) -> (f32, f32) {
8676 match motion {
8677 ReorderMotion::Columns => (travel.0, drag_preview::LIFT_Y),
8678 ReorderMotion::Slots(ReorderAxis::Vertical) => travel,
8679 ReorderMotion::Slots(ReorderAxis::Horizontal) => (travel.0, 0.0),
8680 }
8681}
8682
8683fn drop_insertion_line(
8693 target: Rect,
8694 thickness: f32,
8695 after: bool,
8696 axis: ReorderAxis,
8697 rtl: bool,
8698) -> Rect {
8699 match axis {
8700 ReorderAxis::Vertical => {
8701 let edge = if after {
8702 target.y + target.height
8703 } else {
8704 target.y
8705 };
8706 Rect::new(target.x, edge - thickness * 0.5, target.width, thickness)
8707 }
8708 ReorderAxis::Horizontal => {
8709 let edge = if after != rtl {
8710 target.x + target.width
8711 } else {
8712 target.x
8713 };
8714 Rect::new(edge - thickness * 0.5, target.y, thickness, target.height)
8715 }
8716 }
8717}
8718
8719fn claim_axis(dx: f32, dy: f32, can_x: bool, can_y: bool) -> bool {
8728 match (can_x, can_y) {
8729 (true, false) => false,
8730 (false, true) => true,
8731 _ => dy.abs() >= dx.abs(),
8732 }
8733}
8734
8735fn claim_area(
8745 chain: &[frus_widgets::Scrollable],
8746 under: WidgetId,
8747 down: bool,
8748) -> Option<frus_widgets::Scrollable> {
8749 chain
8750 .iter()
8751 .find(|a| a.takes_user_scroll(down))
8755 .or_else(|| {
8756 chain
8757 .iter()
8758 .find(|a| a.id == under && a.physics.user() != frus_widgets::UserScroll::Never)
8759 })
8760 .copied()
8761}
8762
8763#[cfg(feature = "net")]
8778fn fetch_image_bytes(
8779 url: &str,
8780 deliver: Box<dyn FnOnce(Result<Vec<u8>, String>) + Send + 'static>,
8781) {
8782 const DEADLINE: std::time::Duration = std::time::Duration::from_secs(30);
8785
8786 let url = url.to_string();
8787 let run = async move {
8788 let result = crate::net::Request::get(url)
8789 .timeout(DEADLINE)
8790 .send_bytes()
8791 .await;
8792 deliver(result.map_err(|e| e.to_string()));
8793 };
8794 #[cfg(not(web))]
8795 crate::runtime::spawn(run).detach();
8796 #[cfg(web)]
8797 wasm_bindgen_futures::spawn_local(run);
8798}
8799
8800#[cfg(test)]
8801mod debug_keys_tests {
8802 use super::*;
8803
8804 #[test]
8805 fn the_graphs_keep_the_latest_frames_and_mark_the_late_ones() {
8806 let mut history = FrameHistory::default();
8807 for i in 0..100 {
8808 history.push(&crate::frame_stats::FrameCosts {
8809 build: Some(1.0),
8810 layout_paint: i as f32,
8811 render: 5.0,
8812 ..Default::default()
8813 });
8814 }
8815 assert_eq!(history.ui.len(), HISTORY, "the oldest frames go");
8816 assert_eq!(
8817 history.ui.back().copied(),
8818 Some(100.0),
8819 "the newest is last"
8820 );
8821 let bars = FrameHistory::bars(&history.render, 36.0);
8822 assert!(bars.iter().all(|(_, over)| !over), "5 ms is within budget");
8823 let ui = FrameHistory::bars(&history.ui, 36.0);
8824 assert_eq!(
8825 ui.last().copied(),
8826 Some((36.0, true)),
8827 "over the top fills the graph"
8828 );
8829 let half = FrameHistory::bars(&[BUDGET_MS].into_iter().collect(), 36.0);
8830 assert!((half[0].0 - 18.0).abs() < 0.01 && !half[0].1, "{half:?}");
8831 }
8832
8833 #[test]
8834 fn the_platform_goes_round_as_the_reference_does() {
8835 use frus_widgets::TargetPlatform as P;
8836 let mut seen = vec![P::Android];
8837 for _ in 0..6 {
8838 seen.push(next_platform(*seen.last().unwrap()));
8839 }
8840 assert_eq!(
8841 seen,
8842 [
8843 P::Android,
8844 P::Ios,
8845 P::Windows,
8846 P::MacOs,
8847 P::Linux,
8848 P::Fuchsia,
8849 P::Android
8850 ]
8851 );
8852 }
8853
8854 #[test]
8855 fn the_dumps_say_what_there_is() {
8856 let id = WidgetId::from_u64(0x1234_5678);
8857 let other = WidgetId::from_u64(0x9);
8858 let nodes = [frus_widgets::InspectorNode {
8859 id,
8860 rect: Rect::new(10.0, 20.0, 100.0, 40.0),
8861 name: "Button",
8862 depth: 3,
8863 }];
8864 let focus = dump_focus(&nodes, &[id, other], Some(id));
8865 assert_eq!(focus, "▶ Button 10,20 100×40 #12345678\n #00000009\n");
8866 assert_eq!(dump_focus(&nodes, &[], None), " (nothing focusable)\n");
8867
8868 let semantics = [(
8869 id,
8870 Rect::new(0.0, 0.0, 50.0, 20.0),
8871 frus_widgets::SemanticsProperties::new(frus_widgets::Role::Button).label("Save"),
8872 )];
8873 let dump = dump_semantics(&semantics);
8874 assert!(dump.starts_with(" Button \"Save\""), "{dump}");
8875 assert!(dump.ends_with("0,0 50×20\n"), "{dump}");
8876 }
8877}
8878
8879#[cfg(test)]
8880mod tests {
8881 use super::{
8882 build_view, claim_area, claim_axis, draw_ghost_card, drop_insertion_line, fling_velocity,
8883 gesture_was_a_tap, ghost_offset, install_ambient, reorder_motion, resolve_focus,
8884 spring_toward, Drag, Point, Rect, ReorderAxis, ReorderMotion, Scene, Theme,
8885 VelocityEstimate, PRECISE_SLOP, TOUCH_SLOP,
8886 };
8887 use super::{clipboard_command, ClipCommand, KeyCode, PhysicalKey, WinitKey};
8888 use super::{collect_ids, find_widget, MediaQuery};
8889 use frus_widgets::Locale;
8890
8891 #[test]
8894 fn select_all_hides_the_bar_on_the_desktops() {
8895 use frus_widgets::TargetPlatform as P;
8896 for p in [P::MacOs, P::Linux, P::Windows] {
8897 assert!(super::select_all_hides_bar(p), "{p:?}");
8898 }
8899 for p in [P::Android, P::Ios, P::Fuchsia] {
8900 assert!(!super::select_all_hides_bar(p), "{p:?}");
8901 }
8902 }
8903
8904 #[test]
8908 fn the_context_menu_key_and_shift_f10_open_the_bar() {
8909 use super::opens_selection_bar;
8910 assert!(opens_selection_bar(
8911 &WinitKey::Named(winit::keyboard::NamedKey::ContextMenu),
8912 false
8913 ));
8914 assert!(opens_selection_bar(
8915 &WinitKey::Named(winit::keyboard::NamedKey::F10),
8916 true
8917 ));
8918 assert!(!opens_selection_bar(
8919 &WinitKey::Named(winit::keyboard::NamedKey::F10),
8920 false
8921 ));
8922 assert!(!opens_selection_bar(
8923 &WinitKey::Named(winit::keyboard::NamedKey::Enter),
8924 true
8925 ));
8926 assert!(!opens_selection_bar(&character("m"), true));
8927 }
8928
8929 #[test]
8933 fn a_copy_collapses_the_selection_to_its_end() {
8934 use frus_widgets::Edit;
8935 let forwards = Edit {
8936 cursor: 11,
8937 anchor: Some(6),
8938 composing: None,
8939 };
8940 let backwards = Edit {
8941 cursor: 6,
8942 anchor: Some(11),
8943 composing: None,
8944 };
8945 let expect = Edit {
8946 cursor: 11,
8947 anchor: None,
8948 composing: None,
8949 };
8950 assert_eq!(super::collapsed_to_end(forwards), expect);
8951 assert_eq!(super::collapsed_to_end(backwards), expect);
8952 let caret = Edit {
8954 cursor: 3,
8955 anchor: None,
8956 composing: None,
8957 };
8958 assert_eq!(super::collapsed_to_end(caret), caret);
8959 let empty = Edit {
8961 cursor: 3,
8962 anchor: Some(3),
8963 composing: Some((1, 2)),
8964 };
8965 assert_eq!(super::collapsed_to_end(empty).anchor, None);
8966 assert_eq!(super::collapsed_to_end(empty).cursor, 3);
8967 }
8968
8969 #[test]
8975 fn a_field_with_a_clear_button_still_wants_the_keyboard() {
8976 use frus_widgets::{Container, Icons, TextField};
8977 let clearable = TextField::<()>::new("F")
8978 .suffix_icon(Icons::CLOSE)
8979 .on_suffix(());
8980 assert!(
8981 super::wants_keyboard(&clearable),
8982 "the field with its clear button"
8983 );
8984 assert!(
8985 !super::wants_keyboard(&TextField::<()>::new("F").enabled(false)),
8986 "a disabled field takes no typing"
8987 );
8988 assert!(
8989 !super::wants_keyboard(&Container::<()>::new()),
8990 "and what is not a field wants no keyboard"
8991 );
8992 }
8993
8994 #[test]
9001 fn a_field_with_a_clear_button_is_a_text_field_and_keeps_its_arrows() {
9002 use frus_widgets::{Container, Icons, TextField};
9003 let clearable = TextField::<()>::new("hello")
9004 .suffix_icon(Icons::CLOSE)
9005 .on_suffix(());
9006 assert!(
9007 frus_widgets::Widget::<()>::cursor_at(&clearable, 0.0, 0.0, 1.0, 0).is_none(),
9008 "the trap: the probe finds no caret at the corner a clear button covers"
9009 );
9010 assert!(
9011 super::is_text_field(&clearable),
9012 "and it is a text field all the same"
9013 );
9014 assert!(
9015 super::is_text_field(&TextField::<()>::new("")),
9016 "empty, too"
9017 );
9018 assert!(
9019 !super::is_text_field(&Container::<()>::new()),
9020 "what is not a field leaves its arrows to focus navigation"
9021 );
9022 }
9023
9024 #[test]
9030 fn a_text_that_can_only_be_read_wants_no_keyboard() {
9031 use frus_widgets::{Text, TextField};
9032 let read_only = TextField::<()>::new("REF-4417").read_only();
9033 assert!(super::is_text_field(&read_only), "it has a caret");
9034 assert!(
9035 !super::wants_keyboard(&read_only),
9036 "and no use for a keyboard"
9037 );
9038
9039 let selectable = Text::new("ORDER-4417").selectable();
9040 assert!(
9041 super::is_text_field::<()>(&selectable),
9042 "the arrows move its caret"
9043 );
9044 assert!(!super::wants_keyboard::<()>(&selectable));
9045
9046 assert!(
9047 super::wants_keyboard(&TextField::<()>::new("typed")),
9048 "and a field that can be typed into still does"
9049 );
9050 assert!(
9051 !super::is_text_field::<()>(&Text::new("ORDER-4417")),
9052 "an ordinary text leaves its arrows to focus navigation"
9053 );
9054 }
9055
9056 fn character(c: &str) -> WinitKey {
9057 WinitKey::Character(c.into())
9058 }
9059
9060 #[test]
9063 fn ctrl_c_x_v_are_the_clipboard_and_the_letters_alone_are_not() {
9064 let any = PhysicalKey::Code(KeyCode::KeyQ);
9065 for (letter, command) in [
9066 ("c", ClipCommand::Copy),
9067 ("X", ClipCommand::Cut),
9068 ("v", ClipCommand::Paste),
9069 ] {
9070 assert_eq!(
9071 clipboard_command(&character(letter), any, true),
9072 Some(command)
9073 );
9074 assert_eq!(
9075 clipboard_command(&character(letter), any, false),
9076 None,
9077 "{letter}"
9078 );
9079 }
9080 assert_eq!(clipboard_command(&character("a"), any, true), None);
9081 }
9082
9083 #[test]
9087 fn a_keyboards_own_clipboard_keys_work_by_their_physical_code() {
9088 let unidentified = WinitKey::Unidentified(winit::keyboard::NativeKey::Unidentified);
9089 for (code, command) in [
9090 (KeyCode::Copy, ClipCommand::Copy),
9091 (KeyCode::Cut, ClipCommand::Cut),
9092 (KeyCode::Paste, ClipCommand::Paste),
9093 ] {
9094 assert_eq!(
9095 clipboard_command(&unidentified, PhysicalKey::Code(code), false),
9096 Some(command),
9097 "{code:?}"
9098 );
9099 }
9100 let named = WinitKey::Named(super::NamedKey::Paste);
9102 let any = PhysicalKey::Code(KeyCode::KeyQ);
9103 assert_eq!(
9104 clipboard_command(&named, any, false),
9105 Some(ClipCommand::Paste)
9106 );
9107 }
9108
9109 #[test]
9115 fn a_paste_lands_in_the_field_that_asked_only_while_it_has_the_focus() {
9116 use super::clip::Pasted;
9117 let field = WidgetId::from_u64(1);
9118 let other = WidgetId::from_u64(2);
9119 let pasted = Pasted {
9120 into: field,
9121 text: "copied".into(),
9122 };
9123 assert!(pasted.lands(Some(field)));
9124 assert!(
9125 !pasted.lands(Some(other)),
9126 "the focus moved to another field"
9127 );
9128 assert!(!pasted.lands(None), "the focus left every field");
9129 let nothing = Pasted {
9130 into: field,
9131 text: String::new(),
9132 };
9133 assert!(!nothing.lands(Some(field)), "an empty clipboard");
9134 }
9135
9136 #[test]
9141 fn a_paste_answered_later_waits_for_the_frame_that_takes_it() {
9142 use super::clip::{Answers, Pasted};
9143 use std::cell::Cell;
9144 use std::rc::Rc;
9145 let first = WidgetId::from_u64(1);
9146 let second = WidgetId::from_u64(2);
9147 let answers = Answers::default();
9148 let wakes = Rc::new(Cell::new(0));
9149 let wake = || {
9150 let wakes = Rc::clone(&wakes);
9151 move || wakes.set(wakes.get() + 1)
9152 };
9153 let to_first = answers.answer_for(first, wake());
9154 let to_second = answers.answer_for(second, wake());
9155 assert_eq!(answers.take(), None, "nothing is answered yet");
9156 assert_eq!(wakes.get(), 0, "asking is not answering");
9157
9158 to_second("later".into());
9159 assert_eq!(wakes.get(), 1);
9160 to_first("sooner".into());
9161 assert_eq!(wakes.get(), 2);
9162
9163 assert_eq!(
9164 answers.take(),
9165 Some(Pasted {
9166 into: second,
9167 text: "later".into()
9168 })
9169 );
9170 assert_eq!(
9171 answers.take(),
9172 Some(Pasted {
9173 into: first,
9174 text: "sooner".into()
9175 })
9176 );
9177 assert_eq!(answers.take(), None, "each answer is taken once");
9178 }
9179
9180 struct Deferred;
9189
9190 impl crate::Application for Deferred {
9191 type Message = ();
9192
9193 fn update(&mut self, _message: ()) -> crate::Command<()> {
9194 crate::Command::none()
9195 }
9196
9197 fn view(&self, _theme: &Theme) -> Box<dyn frus_widgets::Widget<()>> {
9198 Box::new(frus_widgets::ThemeBuilder::new(|_: &Theme| {
9199 frus_widgets::Flex::column()
9200 .width(300.0)
9201 .height(80.0)
9202 .child(
9203 frus_widgets::TextField::new("hi")
9204 .width(200.0)
9205 .on_input(|_| ()),
9206 )
9207 }))
9208 }
9209 }
9210
9211 fn surfaced<R>(f: impl FnOnce() -> R) -> R {
9214 MediaQuery::new(frus_widgets::Size::new(300.0, 80.0)).scope(f)
9215 }
9216
9217 #[test]
9227 fn a_shell_installs_the_reader_s_language() {
9228 struct Speaks(Option<Locale>);
9231
9232 impl crate::Application for Speaks {
9233 type Message = ();
9234
9235 fn update(&mut self, _message: ()) -> crate::Command<()> {
9236 crate::Command::none()
9237 }
9238
9239 fn view(&self, _theme: &Theme) -> Box<dyn frus_widgets::Widget<()>> {
9240 Box::new(frus_widgets::Flex::<()>::column())
9241 }
9242
9243 fn supported_locales(&self) -> Vec<Locale> {
9244 vec![
9245 Locale::new("en"),
9246 Locale::new("fr"),
9247 Locale::with_country("fr", "CA"),
9248 ]
9249 }
9250
9251 fn locale(&self) -> Option<Locale> {
9252 self.0.clone()
9253 }
9254 }
9255
9256 install_ambient(&Speaks(None), &[Locale::with_country("fr", "BE")]);
9258 assert_eq!(frus_widgets::locale::of(), Locale::new("fr"));
9259
9260 install_ambient(&Speaks(None), &[]);
9263 assert_eq!(frus_widgets::locale::of(), Locale::new("en"));
9264
9265 install_ambient(
9268 &Speaks(Some(Locale::with_country("fr", "FR"))),
9269 &[Locale::new("en")],
9270 );
9271 assert_eq!(frus_widgets::locale::of(), Locale::new("fr"));
9272
9273 install_ambient(&Deferred, &[Locale::new("fr")]);
9275 assert_eq!(
9276 frus_widgets::locale::of(),
9277 Locale::new("en"),
9278 "it supports only English, so French resolves to it"
9279 );
9280 }
9281
9282 #[test]
9290 fn a_shell_installs_the_application_s_words() {
9291 struct Fr;
9292
9293 impl frus_widgets::localizations::Localizations for Fr {
9294 fn back_button_label(&self) -> &str {
9295 "Retour"
9296 }
9297
9298 fn first_day_of_week_index(&self) -> usize {
9299 1
9300 }
9301 }
9302
9303 struct Speaks;
9304
9305 impl crate::Application for Speaks {
9306 type Message = ();
9307
9308 fn update(&mut self, _message: ()) -> crate::Command<()> {
9309 crate::Command::none()
9310 }
9311
9312 fn view(&self, _theme: &Theme) -> Box<dyn frus_widgets::Widget<()>> {
9313 Box::new(frus_widgets::Flex::<()>::column())
9314 }
9315
9316 fn localizations(&self) -> Option<std::rc::Rc<dyn frus_widgets::Localizations>> {
9317 Some(std::rc::Rc::new(Fr))
9318 }
9319 }
9320
9321 install_ambient(&Speaks, &[]);
9322 assert_eq!(
9323 frus_widgets::localizations::of().back_button_label(),
9324 "Retour",
9325 "the shell read the application's table and installed it"
9326 );
9327 assert_eq!(
9328 frus_widgets::localizations::of().first_day_of_week_index(),
9329 1
9330 );
9331
9332 install_ambient(&Deferred, &[]);
9335 assert_eq!(
9336 frus_widgets::localizations::of().back_button_label(),
9337 "Retour",
9338 "saying nothing is not the same as saying English"
9339 );
9340 }
9341
9342 #[test]
9345 fn the_scroll_behaviour_at_the_root_is_the_applications() {
9346 struct Quiet;
9347
9348 impl crate::Application for Quiet {
9349 type Message = ();
9350
9351 fn update(&mut self, _message: ()) -> crate::Command<()> {
9352 crate::Command::none()
9353 }
9354
9355 fn view(&self, _theme: &Theme) -> Box<dyn frus_widgets::Widget<()>> {
9356 Box::new(frus_widgets::Flex::<()>::column())
9357 }
9358
9359 fn scroll_behavior(&self) -> frus_widgets::ScrollBehavior {
9360 frus_widgets::ScrollBehavior::material().with_overscroll(false)
9361 }
9362 }
9363
9364 struct Silent;
9365
9366 impl crate::Application for Silent {
9367 type Message = ();
9368
9369 fn update(&mut self, _message: ()) -> crate::Command<()> {
9370 crate::Command::none()
9371 }
9372
9373 fn view(&self, _theme: &Theme) -> Box<dyn frus_widgets::Widget<()>> {
9374 Box::new(frus_widgets::Flex::<()>::column())
9375 }
9376 }
9377
9378 install_ambient(&Quiet, &[]);
9379 assert_eq!(
9380 frus_widgets::ScrollConfiguration::of(),
9381 frus_widgets::ScrollBehavior::material().with_overscroll(false)
9382 );
9383 install_ambient(&Silent, &[]);
9384 assert_eq!(
9385 frus_widgets::ScrollConfiguration::of(),
9386 frus_widgets::ScrollBehavior::material()
9387 );
9388 }
9389
9390 #[test]
9401 fn a_view_is_built_ready_to_be_read() {
9402 let theme = Theme::default();
9403 let prepared =
9404 surfaced(|| build_view(&Deferred, &theme, &frus_widgets::Runtime::default()));
9405 let ids = collect_ids(prepared.as_ref());
9406 assert!(
9407 ids.len() > 1,
9408 "an application inside a deferred subtree has identities to mount: {ids:?}"
9409 );
9410 for id in &ids {
9412 assert!(
9413 find_widget(prepared.as_ref(), *id).is_some(),
9414 "{id:?} was counted but cannot be found"
9415 );
9416 }
9417 }
9418
9419 #[test]
9421 #[should_panic(expected = "before it was built")]
9422 fn an_unprepared_view_is_the_state_that_broke() {
9423 let theme = Theme::default();
9424 let raw = surfaced(|| crate::Application::view(&Deferred, &theme));
9425 let _ = collect_ids(raw.as_ref());
9426 }
9427
9428 #[test]
9436 fn a_swipe_that_never_started_is_still_a_tap() {
9437 let item = frus_widgets::Dismissable {
9438 id: WidgetId::from_u64(1),
9439 rect: Rect::new(0.0, 0.0, 300.0, 62.0),
9440 spec: frus_widgets::DismissSpec::default(),
9441 };
9442 let dismiss = |moved| Drag::Dismiss {
9443 item,
9444 last: Point::new(0.0, 0.0),
9445 moved,
9446 };
9447 assert!(
9448 gesture_was_a_tap(Some(&dismiss(false))),
9449 "a swipe under the threshold leaves the click alone"
9450 );
9451 assert!(
9452 !gesture_was_a_tap(Some(&dismiss(true))),
9453 "a swipe that ran has already been answered and must not also click"
9454 );
9455 let scroll = Drag::Scroll {
9458 id: WidgetId::from_u64(2),
9459 last: Point::new(0.0, 0.0),
9460 moved: false,
9461 carried: (0.0, 0.0),
9462 dismiss: None,
9463 axis: None,
9464 pan: None,
9465 };
9466 assert!(gesture_was_a_tap(Some(&scroll)));
9467 let sheet = |moved| Drag::Sheet {
9470 id: WidgetId::from_u64(4),
9471 last: Point::new(0.0, 0.0),
9472 moved,
9473 available: 800.0,
9474 };
9475 assert!(gesture_was_a_tap(Some(&sheet(false))));
9476 assert!(!gesture_was_a_tap(Some(&sheet(true))));
9477 let select = Drag::TextSelect {
9480 id: WidgetId::from_u64(3),
9481 rect: Rect::new(0.0, 0.0, 10.0, 10.0),
9482 };
9483 assert!(!gesture_was_a_tap(Some(&select)));
9484 assert!(!gesture_was_a_tap(None), "no gesture, no verdict to give");
9485 }
9486
9487 #[test]
9491 fn a_scroll_gesture_is_claimed_by_one_axis() {
9492 assert!(claim_axis(6.0, 40.0, true, true), "down the page");
9495 assert!(!claim_axis(40.0, 6.0, true, true), "across it");
9496 assert!(claim_axis(10.0, 10.0, true, true));
9498 assert!(claim_axis(40.0, 6.0, false, true), "a list only goes down");
9501 assert!(
9502 !claim_axis(6.0, 40.0, true, false),
9503 "a strip only goes across"
9504 );
9505 }
9506
9507 fn area(id: u64, max_x: f32, max_y: f32) -> frus_widgets::Scrollable {
9509 frus_widgets::Scrollable {
9510 id: WidgetId::from_u64(id),
9511 viewport: Rect::new(0.0, 0.0, 100.0, 100.0),
9512 max_x,
9513 max_y,
9514 axis: frus_widgets::Axis::Vertical,
9515 physics: frus_widgets::ScrollPhysics::CLAMPING,
9516 overscroll: Some(frus_widgets::OverscrollIndicator::Glow),
9517 fling: frus_widgets::VelocityStrategy::Regression,
9518 refresh: None,
9519 page: None,
9520 reverse_x: false,
9521 reverse_y: false,
9522 host: None,
9523 keep_visible: None,
9524 }
9525 }
9526
9527 #[test]
9531 fn the_gesture_goes_to_the_area_that_can_take_it() {
9532 let strip = area(1, 400.0, 0.0);
9533 let page = area(2, 0.0, 900.0);
9534 let chain = [strip, page]; assert_eq!(
9536 claim_area(&chain, strip.id, true).map(|a| a.id),
9537 Some(page.id),
9538 "down the page, over the strip"
9539 );
9540 assert_eq!(
9541 claim_area(&chain, strip.id, false).map(|a| a.id),
9542 Some(strip.id),
9543 "across the strip"
9544 );
9545 assert_eq!(
9548 claim_area(&[strip], strip.id, true).map(|a| a.id),
9549 Some(strip.id)
9550 );
9551 let inner = area(3, 0.0, 200.0);
9553 assert_eq!(
9554 claim_area(&[inner, strip, page], inner.id, true).map(|a| a.id),
9555 Some(inner.id)
9556 );
9557 }
9558
9559 #[test]
9563 fn the_physics_have_the_last_word_on_a_finger() {
9564 let mut inner = area(1, 0.0, 300.0);
9565 inner.physics = frus_widgets::ScrollPhysics::NEVER;
9566 assert!(!inner.accepts_user_offset((0.0, 0.0)));
9567 assert!(!inner.takes_user_scroll(true));
9568 let outer = area(2, 0.0, 600.0);
9569 let chain = [inner, outer];
9570 assert_eq!(
9571 claim_area(&chain, WidgetId::from_u64(1), true).map(|a| a.id),
9572 Some(WidgetId::from_u64(2)),
9573 "the page around it scrolls"
9574 );
9575 assert_eq!(
9576 claim_area(&[inner], WidgetId::from_u64(1), true).map(|a| a.id),
9577 None,
9578 "and alone it keeps nothing"
9579 );
9580
9581 let mut short = area(3, 0.0, 0.0);
9582 short.physics = frus_widgets::ScrollPhysics::ALWAYS;
9583 assert!(short.accepts_user_offset((0.0, 0.0)));
9584 assert!(short.takes_user_scroll(true), "along its axis");
9585 assert!(!short.takes_user_scroll(false), "and not across it");
9586 assert_eq!(
9587 claim_area(&[short, outer], WidgetId::from_u64(3), true).map(|a| a.id),
9588 Some(WidgetId::from_u64(3)),
9589 "a short list that always scrolls keeps the drag from the page"
9590 );
9591 let mut strip = short;
9592 strip.axis = frus_widgets::Axis::Horizontal;
9593 assert_eq!(
9594 claim_area(&[strip, outer], WidgetId::from_u64(3), true).map(|a| a.id),
9595 Some(WidgetId::from_u64(2)),
9596 "a strip that always scrolls across leaves a drag down to the page"
9597 );
9598 let mut both = short;
9599 both.axis = frus_widgets::Axis::Both;
9600 assert!(both.takes_user_scroll(false) && both.takes_user_scroll(true));
9601 }
9602
9603 #[test]
9607 fn an_area_whose_content_fits_takes_no_finger() {
9608 let fits = area(1, 0.0, 0.0);
9609 assert!(!fits.accepts_user_offset((0.0, 0.0)));
9610 assert!(fits.accepts_user_offset((0.0, 12.0)));
9613 let mut pullable = fits;
9615 pullable.refresh = Some(WidgetId::from_u64(9));
9616 assert!(pullable.accepts_user_offset((0.0, 0.0)));
9617 assert!(area(1, 0.0, 1.0).accepts_user_offset((0.0, 0.0)));
9619 assert!(area(1, 1.0, 0.0).accepts_user_offset((0.0, 0.0)));
9620 }
9621 use frus_widgets::WidgetId;
9622 use std::collections::HashSet;
9623
9624 fn released(velocity: (f32, f32), offset: (f32, f32)) -> VelocityEstimate {
9626 VelocityEstimate {
9627 velocity: frus_widgets::Velocity::new(velocity.0, velocity.1),
9628 confidence: 1.0,
9629 duration: 0.05,
9630 offset,
9631 }
9632 }
9633
9634 #[test]
9635 fn a_fast_twitch_that_went_nowhere_is_not_a_fling() {
9636 let (x, y) = fling_velocity(released((0.0, 2000.0), (0.0, 3.0)), TOUCH_SLOP);
9638 assert_eq!((x, y), (0.0, 0.0));
9639 let (_, y) = fling_velocity(released((0.0, 2000.0), (0.0, 60.0)), TOUCH_SLOP);
9641 assert_eq!(y, 2000.0);
9642 }
9643
9644 #[test]
9645 fn the_fling_gate_is_per_axis() {
9646 let (x, y) = fling_velocity(released((300.0, 1500.0), (5.0, 120.0)), TOUCH_SLOP);
9649 assert_eq!(x, 0.0, "the wobble is not a horizontal fling");
9650 assert_eq!(y, 1500.0, "the swipe still flings vertically");
9651 }
9652
9653 #[test]
9654 fn a_precise_pointer_needs_almost_no_travel() {
9655 let estimate = released((0.0, 900.0), (0.0, 3.0));
9657 assert_eq!(fling_velocity(estimate, TOUCH_SLOP).1, 0.0);
9658 assert_eq!(fling_velocity(estimate, PRECISE_SLOP).1, 900.0);
9659 }
9660
9661 #[test]
9662 fn insertion_line_sits_on_the_target_top_edge() {
9663 let line = drop_insertion_line(
9666 Rect::new(20.0, 100.0, 200.0, 44.0),
9667 4.0,
9668 false,
9669 ReorderAxis::Vertical,
9670 false,
9671 );
9672 assert_eq!(line.x, 20.0, "aligned to the target's left");
9673 assert_eq!(line.width, 200.0, "the target's full width");
9674 assert_eq!(line.y, 98.0, "centred on the top edge (100 - 4/2)");
9675 assert_eq!(line.height, 4.0);
9676 }
9677
9678 #[test]
9679 fn insertion_line_sits_on_the_target_bottom_edge_when_inserting_after() {
9680 let line = drop_insertion_line(
9683 Rect::new(20.0, 100.0, 200.0, 44.0),
9684 4.0,
9685 true,
9686 ReorderAxis::Vertical,
9687 false,
9688 );
9689 assert_eq!(line.x, 20.0, "still aligned to the left");
9690 assert_eq!(line.width, 200.0, "the target's full width");
9691 assert_eq!(line.y, 142.0, "centred on the bottom edge (144 - 4/2)");
9692 assert_eq!(line.height, 4.0);
9693 let rtl = drop_insertion_line(
9695 Rect::new(20.0, 100.0, 200.0, 44.0),
9696 4.0,
9697 true,
9698 ReorderAxis::Vertical,
9699 true,
9700 );
9701 assert_eq!(rtl, line, "right to left changes nothing down a list");
9702 }
9703
9704 #[test]
9708 fn a_horizontal_insertion_line_stands_on_the_edge_the_row_goes_to() {
9709 let chip = Rect::new(100.0, 20.0, 80.0, 48.0);
9710 let line = |after, rtl| drop_insertion_line(chip, 4.0, after, ReorderAxis::Horizontal, rtl);
9711 let before = line(false, false);
9712 assert_eq!(
9713 (before.x, before.y, before.width, before.height),
9714 (98.0, 20.0, 4.0, 48.0),
9715 "before a row: its left edge, the full height, 4 wide"
9716 );
9717 assert_eq!(
9718 line(true, false).x,
9719 178.0,
9720 "after it: its right edge (180 - 4/2)"
9721 );
9722 assert_eq!(
9724 line(true, true).x,
9725 98.0,
9726 "after, right to left: the left edge"
9727 );
9728 assert_eq!(
9729 line(false, true).x,
9730 178.0,
9731 "before, right to left: the right edge"
9732 );
9733 }
9734
9735 #[test]
9739 fn a_horizontal_reorderable_inserts_only_when_it_says_so() {
9740 assert_eq!(
9741 ReorderMotion::of(ReorderAxis::Horizontal, false),
9742 ReorderMotion::Columns,
9743 "a table's column"
9744 );
9745 assert_eq!(
9746 ReorderMotion::of(ReorderAxis::Horizontal, true),
9747 ReorderMotion::Slots(ReorderAxis::Horizontal),
9748 "a horizontal list's row"
9749 );
9750 for inserts in [false, true] {
9752 assert_eq!(
9753 ReorderMotion::of(ReorderAxis::Vertical, inserts),
9754 ReorderMotion::Slots(ReorderAxis::Vertical)
9755 );
9756 }
9757 assert_eq!(
9759 reorder_motion::<()>(&frus_widgets::Container::new()),
9760 ReorderMotion::Columns
9761 );
9762 for (axis, expected) in [
9764 (
9765 ReorderAxis::Vertical,
9766 ReorderMotion::Slots(ReorderAxis::Vertical),
9767 ),
9768 (
9769 ReorderAxis::Horizontal,
9770 ReorderMotion::Slots(ReorderAxis::Horizontal),
9771 ),
9772 ] {
9773 let list = frus_widgets::ReorderableList::<()>::new(|_, _| ())
9774 .axis(axis)
9775 .row(frus_widgets::Container::new().width(60.0));
9776 let row = frus_widgets::Widget::children(&list)[0].as_ref();
9777 assert_eq!(reorder_motion(row), expected, "{axis:?}");
9778 }
9779 }
9780
9781 #[test]
9785 fn a_horizontal_rows_ghost_follows_the_finger_along_x_only() {
9786 let travel = (120.0, 35.0);
9787 assert_eq!(
9788 ghost_offset(ReorderMotion::Columns, travel),
9789 (120.0, super::drag_preview::LIFT_Y)
9790 );
9791 assert_eq!(
9792 ghost_offset(ReorderMotion::Slots(ReorderAxis::Vertical), travel),
9793 (120.0, 35.0)
9794 );
9795 assert_eq!(
9796 ghost_offset(ReorderMotion::Slots(ReorderAxis::Horizontal), travel),
9797 (120.0, 0.0)
9798 );
9799 }
9800
9801 #[test]
9802 fn focus_returns_to_trigger_when_overlay_closes() {
9803 let anchor = WidgetId::from_u64(1);
9804 let item = WidgetId::from_u64(2);
9805 let (mut history, mut prev) = (Vec::new(), None);
9806
9807 let f = resolve_focus(
9809 Some(anchor),
9810 &HashSet::from([anchor]),
9811 &mut history,
9812 &mut prev,
9813 );
9814 assert_eq!(f, Some(anchor));
9815 assert!(history.is_empty());
9816
9817 let present = HashSet::from([anchor, item]);
9819 let f = resolve_focus(Some(item), &present, &mut history, &mut prev);
9820 assert_eq!(f, Some(item));
9821 assert_eq!(history, vec![anchor]);
9822
9823 let f = resolve_focus(
9825 Some(item),
9826 &HashSet::from([anchor]),
9827 &mut history,
9828 &mut prev,
9829 );
9830 assert_eq!(f, Some(anchor), "focus returns to the trigger");
9831 assert!(history.is_empty());
9832 }
9833
9834 #[test]
9835 fn focus_falls_to_none_when_no_trigger_remains() {
9836 let a = WidgetId::from_u64(1);
9837 let (mut history, mut prev) = (Vec::new(), Some(a));
9838 let f = resolve_focus(Some(a), &HashSet::new(), &mut history, &mut prev);
9840 assert_eq!(f, None);
9841 assert_eq!(prev, None);
9842 }
9843
9844 #[test]
9845 fn spring_approaches_target_monotonically_and_settles() {
9846 let mut x = 0.0;
9849 let mut prev = -1.0;
9850 for _ in 0..30 {
9851 x = spring_toward(x, 100.0, 0.016, 0.07);
9852 assert!(x > prev && x <= 100.0, "monotonic and bounded: {x}");
9853 prev = x;
9854 }
9855 assert!(x > 99.0, "all but reached after ~0.5 s: {x}");
9856 }
9857
9858 #[test]
9859 fn ghost_card_shape() {
9860 let theme = Theme::default();
9861 let card = Rect::new(140.0, 0.0, 80.0, 34.0);
9862 let mut scene = Scene::new();
9864 draw_ghost_card(&mut scene, &theme, card, &[]);
9865 assert_eq!(scene.primitives().len(), 2, "shadow plus solid card");
9866 }
9867}
9868
9869#[cfg(test)]
9872mod sheet_request_tests {
9873 use super::{apply_sheet_requests, Runtime, Widget};
9874 use crate::command::Command;
9875 use frus_widgets::{
9876 build_ui, keyed, Container, DraggableScrollableSheet, SheetTo, Size, Theme,
9877 };
9878
9879 fn view() -> Box<dyn Widget<()>> {
9882 Box::new(
9883 Container::<()>::new()
9884 .width(400.0)
9885 .height(800.0)
9886 .child(keyed(
9887 "places",
9888 DraggableScrollableSheet::<()>::new(Container::new()),
9889 )),
9890 )
9891 }
9892
9893 #[test]
9896 fn a_request_reaches_the_sheet_the_view_named() {
9897 let tree = view();
9898 let mut runtime = Runtime::default();
9899 let sheets = build_ui(
9900 tree.as_ref(),
9901 Size::new(400.0, 800.0),
9902 &runtime,
9903 &Theme::default(),
9904 )
9905 .sheets()
9906 .to_vec();
9907 let sheet = sheets.first().expect("the page has its sheet").clone();
9908
9909 let requests = Command::<()>::sheet("places", SheetTo::size(0.9))
9910 .into_parts()
9911 .sheets;
9912 let (unplaced, moved) =
9913 apply_sheet_requests(&mut runtime, tree.as_ref(), requests, &sheets);
9914 assert!(unplaced.is_empty());
9915 assert!(moved);
9916 assert_eq!(runtime.sheet_size(sheet.id, &sheet.spec), 0.9);
9917
9918 let elsewhere = Command::<()>::sheet("elsewhere", SheetTo::size(0.3))
9919 .into_parts()
9920 .sheets;
9921 let (unplaced, moved) =
9922 apply_sheet_requests(&mut runtime, tree.as_ref(), elsewhere, &sheets);
9923 assert_eq!(unplaced.len(), 1);
9924 assert!(!moved);
9925 let not_yet = Command::<()>::sheet("places", SheetTo::size(0.3))
9926 .into_parts()
9927 .sheets;
9928 let (unplaced, _) = apply_sheet_requests(&mut runtime, tree.as_ref(), not_yet, &[]);
9929 assert_eq!(unplaced.len(), 1, "kept for the frame that has the sheet");
9930 assert_eq!(
9931 runtime.sheet_size(sheet.id, &sheet.spec),
9932 0.9,
9933 "nothing moved"
9934 );
9935 }
9936}
9937
9938#[cfg(test)]
9939mod scroll_request_tests {
9940 use super::{apply_scroll_requests, Rect, Runtime, Scrollable, Widget, WidgetId};
9941 use crate::command::Command;
9942 use frus_widgets::{keyed, Container, ScrollTo, SingleChildScrollView};
9943
9944 fn view() -> Box<dyn Widget<()>> {
9946 Box::new(Container::<()>::new().child(keyed(
9947 "log",
9948 SingleChildScrollView::<()>::new().height(200.0),
9949 )))
9950 }
9951
9952 fn region(id: WidgetId) -> Scrollable {
9954 Scrollable {
9955 id,
9956 viewport: Rect {
9957 x: 0.0,
9958 y: 0.0,
9959 width: 300.0,
9960 height: 200.0,
9961 },
9962 max_x: 0.0,
9963 max_y: 1000.0,
9964 axis: frus_widgets::Axis::Vertical,
9965 physics: frus_widgets::ScrollPhysics::CLAMPING,
9966 overscroll: Some(frus_widgets::OverscrollIndicator::Glow),
9967 fling: frus_widgets::VelocityStrategy::Regression,
9968 refresh: None,
9969 page: None,
9970 reverse_x: false,
9971 reverse_y: false,
9972 keep_visible: None,
9973 host: None,
9974 }
9975 }
9976
9977 #[test]
9981 fn a_request_reaches_the_region_the_view_named() {
9982 let tree = view();
9983 let requests = Command::<()>::scroll("log", ScrollTo::end().instant())
9984 .into_parts()
9985 .scrolls;
9986 let id = frus_widgets::find_by_key(tree.as_ref(), requests[0].0)
9987 .expect("the view named it, so the tree knows it");
9988 let mut runtime = Runtime::default();
9989 let (unplaced, moved) =
9990 apply_scroll_requests(&mut runtime, tree.as_ref(), requests, &[region(id)]);
9991 assert!(unplaced.is_empty());
9992 assert!(moved);
9993 assert_eq!(runtime.scroll.get(&id), Some(&(0.0, 1000.0)));
9994 }
9995
9996 #[test]
10001 fn a_region_this_registry_has_not_got_is_handed_back() {
10002 let tree = view();
10003 let requests = Command::<()>::scroll("log", ScrollTo::y(120.0))
10004 .into_parts()
10005 .scrolls;
10006 let mut runtime = Runtime::default();
10007 let (unplaced, moved) = apply_scroll_requests(&mut runtime, tree.as_ref(), requests, &[]);
10008 assert_eq!(unplaced.len(), 1, "kept for the second attempt");
10009 assert!(!moved);
10010 assert!(runtime.scroll_target.is_empty());
10011 }
10012
10013 #[test]
10016 fn a_name_the_view_does_not_use_moves_nothing() {
10017 let tree = view();
10018 let requests = Command::<()>::scroll("ledger", ScrollTo::start())
10019 .into_parts()
10020 .scrolls;
10021 let mut runtime = Runtime::default();
10022 let id = frus_widgets::find_by_key(
10023 tree.as_ref(),
10024 Command::<()>::scroll("log", ScrollTo::start())
10025 .into_parts()
10026 .scrolls[0]
10027 .0,
10028 )
10029 .expect("the real one is there");
10030 let (unplaced, moved) =
10031 apply_scroll_requests(&mut runtime, tree.as_ref(), requests, &[region(id)]);
10032 assert_eq!(unplaced.len(), 1);
10033 assert!(!moved);
10034 assert!(runtime.scroll.is_empty() && runtime.scroll_target.is_empty());
10035 }
10036
10037 #[test]
10040 fn a_batch_places_every_request_it_carries() {
10041 let tree = view();
10042 let requests = Command::<()>::batch([
10043 Command::scroll("log", ScrollTo::y(400.0).instant()),
10044 Command::scroll("log", ScrollTo::y(600.0).instant()),
10045 ])
10046 .into_parts()
10047 .scrolls;
10048 assert_eq!(requests.len(), 2);
10049 let id = frus_widgets::find_by_key(tree.as_ref(), requests[0].0).expect("named");
10050 let mut runtime = Runtime::default();
10051 let (unplaced, moved) =
10052 apply_scroll_requests(&mut runtime, tree.as_ref(), requests, &[region(id)]);
10053 assert!(unplaced.is_empty() && moved);
10054 assert_eq!(
10055 runtime.scroll.get(&id),
10056 Some(&(0.0, 600.0)),
10057 "the last request is the current statement"
10058 );
10059 }
10060}
10061
10062#[cfg(test)]
10063mod autoscroll_tests {
10064 use super::{
10065 edge_autoscroll, follow_content, Drag, Point, Rect, WidgetId, AUTOSCROLL_MAX_OVERHANG,
10066 AUTOSCROLL_VELOCITY,
10067 };
10068
10069 const VIEW: (f32, f32) = (100.0, 500.0);
10071 const ROOM: (f32, f32) = (1000.0, 1000.0);
10072 const DT: f32 = 1.0 / 60.0;
10074
10075 #[test]
10076 fn a_row_inside_the_viewport_scrolls_nothing() {
10077 assert_eq!(edge_autoscroll((200.0, 260.0), VIEW, ROOM, DT), None);
10078 assert_eq!(edge_autoscroll((440.0, 500.0), VIEW, ROOM, DT), None);
10080 }
10081
10082 #[test]
10085 fn the_speed_follows_how_far_out_the_row_is() {
10086 let near = edge_autoscroll((460.0, 501.0), VIEW, ROOM, DT).expect("one pixel out");
10087 let far = edge_autoscroll((460.0, 510.0), VIEW, ROOM, DT).expect("ten pixels out");
10088 assert!(
10089 near < 0.0,
10090 "the content moves up to reveal what is below: {near}"
10091 );
10092 assert!(
10093 (far / near - 10.0).abs() < 0.01,
10094 "ten times as far, ten times as fast: {near} vs {far}"
10095 );
10096 assert!(
10097 (near + 1.0 * AUTOSCROLL_VELOCITY * DT).abs() < 1e-4,
10098 "one pixel out, at the stated velocity: {near}"
10099 );
10100 }
10101
10102 #[test]
10105 fn the_overhang_is_capped() {
10106 let far = edge_autoscroll((460.0, 520.0), VIEW, ROOM, DT).expect("twenty out");
10107 let absurd = edge_autoscroll((460.0, 5000.0), VIEW, ROOM, DT).expect("off the window");
10108 assert!((far - absurd).abs() < 1e-4, "{far} vs {absurd}");
10109 assert!((absurd + AUTOSCROLL_MAX_OVERHANG * AUTOSCROLL_VELOCITY * DT).abs() < 1e-4);
10110 }
10111
10112 #[test]
10115 fn above_the_top_the_content_comes_down() {
10116 let up = edge_autoscroll((90.0, 150.0), VIEW, ROOM, DT).expect("ten above");
10117 assert!(up > 0.0, "{up}");
10118 }
10119
10120 #[test]
10127 fn a_carried_item_follows_its_content() {
10128 let shift = (0.0, -95.0);
10130 let pressed = Point::new(900.0, 1330.0);
10131 let row_then = Rect::new(105.0, 1250.0, 700.0, 160.0);
10132 let row_now = row_then.translate(0.0, -95.0);
10133 let finger = Point::new(900.0, 2030.0);
10134
10135 let mut reorder = Drag::Reorder {
10136 id: WidgetId::from_u64(1),
10137 from: 0,
10138 start: pressed,
10139 moved: true,
10140 carried: Some((WidgetId::from_u64(1), row_then)),
10141 };
10142 follow_content(&mut reorder, shift);
10143 let Drag::Reorder { start, carried, .. } = reorder else {
10144 unreachable!("still a reorder");
10145 };
10146 let (_, carried) = carried.expect("the box it was carried from");
10150 assert_eq!(carried, row_now, "the carried box went with the content");
10151 let ghost = carried.translate(finger.x - start.x, finger.y - start.y);
10153 assert_eq!(
10154 ghost,
10155 row_then.translate(finger.x - pressed.x, finger.y - pressed.y),
10156 "the ghost is where it would be had nothing scrolled"
10157 );
10158
10159 let mut item = Drag::Item {
10160 source: frus_widgets::DragSource {
10161 id: WidgetId::from_u64(1),
10162 rect: row_then,
10163 payload: 7,
10164 },
10165 start: pressed,
10166 moved: true,
10167 over: None,
10168 };
10169 follow_content(&mut item, shift);
10170 let Drag::Item { source, start, .. } = item else {
10171 unreachable!("still an item");
10172 };
10173 assert_eq!(
10174 source.rect, row_now,
10175 "the box taken at the press went with the content"
10176 );
10177 assert_eq!(start, Point::new(900.0, 1235.0), "and so did the press");
10178 }
10179
10180 #[test]
10183 fn an_exhausted_edge_stays_still() {
10184 assert_eq!(
10185 edge_autoscroll((460.0, 520.0), VIEW, (1000.0, 0.0), DT),
10186 None
10187 );
10188 assert_eq!(
10189 edge_autoscroll((90.0, 150.0), VIEW, (0.0, 1000.0), DT),
10190 None
10191 );
10192 }
10193
10194 #[test]
10197 fn a_row_taller_than_the_viewport_follows_its_leading_edge() {
10198 let both = edge_autoscroll((50.0, 900.0), VIEW, ROOM, DT).expect("over both edges");
10199 assert!(both > 0.0, "the top edge wins: {both}");
10200 }
10201}
10202
10203#[cfg(test)]
10216mod back_gesture_tests {
10217 use super::{
10218 drag_after_hold, frame_needs_build, hold_candidates, Drag, Point, Theme, WidgetId,
10219 };
10220 use frus_widgets::{build_ui, column, Container, DragSource, Draggable, Navigator};
10221 use frus_widgets::{Rect, Runtime, Size, Ui, Widget};
10222
10223 const W: f32 = 392.0;
10225 const H: f32 = 850.0;
10226 const ROW_TOP: f32 = 480.0;
10229 const ROW_HEIGHT: f32 = 66.0;
10230 const ON_THE_ROW: Point = Point::new(3.0, 509.0);
10231 const ABOVE_THE_ROW: Point = Point::new(3.0, 200.0);
10232 const HELD: u8 = 1;
10234
10235 fn home() -> impl Widget<u8> {
10238 column![
10239 Container::<u8>::new().width(W).height(ROW_TOP),
10240 Draggable::new(
10241 Container::<u8>::new()
10242 .width(W)
10243 .height(ROW_HEIGHT)
10244 .on_long_press(HELD)
10245 )
10246 .payload(7)
10247 .long_press(),
10248 ]
10249 }
10250
10251 fn after_a_push() -> Navigator<u8> {
10254 Navigator::new("data", Container::<u8>::new().width(W).height(H))
10255 .size(W, H)
10256 .from("home", home(), 0.999, true)
10257 }
10258
10259 fn frame(root: &dyn Widget<u8>) -> Ui<u8> {
10260 build_ui(
10261 root,
10262 Size::new(W, H),
10263 &Runtime::default(),
10264 &Theme::default(),
10265 )
10266 }
10267
10268 fn back(at: Point) -> Drag {
10269 Drag::Back { start_x: at.x }
10270 }
10271
10272 fn still_scroll() -> Drag {
10273 Drag::Scroll {
10274 id: WidgetId::from_u64(9),
10275 last: ON_THE_ROW,
10276 moved: false,
10277 carried: (0.0, 0.0),
10278 dismiss: None,
10279 axis: None,
10280 pan: None,
10281 }
10282 }
10283
10284 fn row_source() -> DragSource {
10285 DragSource {
10286 id: WidgetId::from_u64(3),
10287 rect: Rect::new(-117.0, ROW_TOP, W, ROW_HEIGHT),
10288 payload: 7,
10289 }
10290 }
10291
10292 #[test]
10300 fn the_frame_an_animation_settles_in_is_built_again() {
10301 assert!(
10302 frame_needs_build(false, false, true, false),
10303 "the frame the application's animation stops in builds the view again"
10304 );
10305 assert!(
10306 frame_needs_build(false, true, true, false),
10307 "as does every frame while it moves"
10308 );
10309 assert!(
10310 !frame_needs_build(false, false, false, false),
10311 "and a still frame after a still one repaints the tree it has"
10312 );
10313 assert!(frame_needs_build(true, false, false, false), "unless asked");
10314 assert!(
10315 frame_needs_build(false, false, false, true),
10316 "or the frame has its own reason"
10317 );
10318 }
10319
10320 #[test]
10323 fn a_back_gesture_claims_nothing_under_its_finger() {
10324 let root = after_a_push();
10325 let ui = frame(&root);
10326 let (held, lift) = hold_candidates(None, Some(&ui), Some(&root), ON_THE_ROW);
10327 assert!(
10328 lift.is_some() && held == Some(HELD),
10329 "the frame the phone had: a press there, with no gesture, finds the row's hold"
10330 );
10331
10332 let gesture = back(ON_THE_ROW);
10333 let (held, lift) = hold_candidates(Some(&gesture), Some(&ui), Some(&root), ON_THE_ROW);
10334 assert!(
10335 lift.is_none(),
10336 "a back gesture's press arms no lift of the row under it"
10337 );
10338 assert_eq!(held, None, "nor its long press");
10339 }
10340
10341 #[test]
10344 fn a_hold_never_takes_the_pointer_from_a_back_gesture() {
10345 let reorder = Some((WidgetId::from_u64(4), 2, ON_THE_ROW));
10346 for (lift, reorder) in [
10347 (Some(row_source()), None),
10348 (None, reorder),
10349 (Some(row_source()), reorder),
10350 (None, None),
10351 ] {
10352 let after = drag_after_hold(Some(back(ON_THE_ROW)), lift, reorder, ON_THE_ROW);
10353 assert!(
10354 matches!(after, Some(Drag::Back { start_x }) if start_x == ON_THE_ROW.x),
10355 "the back gesture is still the drag (lift {}, reorder {})",
10356 lift.is_some(),
10357 reorder.is_some()
10358 );
10359 }
10360 }
10361
10362 #[test]
10364 fn a_back_gesture_where_nothing_is_under_it_still_owns_the_pointer() {
10365 let root = after_a_push();
10366 let ui = frame(&root);
10367 let gesture = back(ABOVE_THE_ROW);
10368 let (held, lift) = hold_candidates(Some(&gesture), Some(&ui), Some(&root), ABOVE_THE_ROW);
10369 assert!(held.is_none() && lift.is_none());
10370 assert!(matches!(
10371 drag_after_hold(Some(gesture), None, None, ABOVE_THE_ROW),
10372 Some(Drag::Back { .. })
10373 ));
10374 }
10375
10376 #[test]
10380 fn a_hold_on_a_row_without_a_back_gesture_still_lifts_it() {
10381 let root = home();
10382 let ui = frame(&root);
10383 let at = Point::new(40.0, ROW_TOP + 10.0);
10384 let scroll = still_scroll();
10385 let (held, lift) = hold_candidates(Some(&scroll), Some(&ui), Some(&root), at);
10386 assert_eq!(
10387 held,
10388 Some(HELD),
10389 "the long press is armed under a still scroll"
10390 );
10391 assert!(lift.is_some(), "and so is the lift");
10392 let (held, lift) = hold_candidates(None, Some(&ui), Some(&root), at);
10393 assert!(held.is_some() && lift.is_some(), "and with no drag at all");
10394
10395 let cursor = Point::new(41.0, ROW_TOP + 11.0);
10396 let lifted = drag_after_hold(Some(still_scroll()), Some(row_source()), None, cursor);
10397 assert!(
10398 matches!(lifted, Some(Drag::Item { moved: true, start, .. }) if start == cursor),
10399 "the hold lifts the item where the finger is"
10400 );
10401 let reorder = Some((WidgetId::from_u64(4), 2, at));
10402 let reordering = drag_after_hold(Some(still_scroll()), None, reorder, cursor);
10403 assert!(
10404 matches!(reordering, Some(Drag::Reorder { moved: true, from: 2, start, .. }) if start == at),
10405 "or picks the row up for a reorder, from where it was pressed"
10406 );
10407 assert!(
10408 drag_after_hold(Some(still_scroll()), None, None, cursor).is_none(),
10409 "a hold with nothing to lift ends the still scroll: the long press had it"
10410 );
10411 }
10412}
10413
10414fn count_layers(primitives: &[frus_widgets::Primitive]) -> u32 {
10416 primitives
10417 .iter()
10418 .map(|p| match p {
10419 frus_widgets::Primitive::Layer { primitives, .. } => 1 + count_layers(primitives),
10420 _ => 0,
10421 })
10422 .sum()
10423}
10424
10425pub(crate) struct KeyDown {
10429 logical_key: WinitKey,
10431 physical_key: PhysicalKey,
10433 text: Option<winit::keyboard::SmolStr>,
10435 repeat: bool,
10437}
10438
10439impl From<&winit::event::KeyEvent> for KeyDown {
10440 fn from(event: &winit::event::KeyEvent) -> Self {
10441 Self {
10442 logical_key: event.logical_key.clone(),
10443 physical_key: event.physical_key,
10444 text: event.text.clone(),
10445 repeat: event.repeat,
10446 }
10447 }
10448}
10449
10450fn backdrop_under(mut scene: Scene, width: f32, height: f32, line: f32) -> Scene {
10455 let mut framed = Scene::new();
10456 framed.fill_rect(
10457 frus_widgets::Rect::new(0.0, line, width, (height - line).max(0.0)),
10458 frus_gpu::backdrop(),
10459 );
10460 for primitive in scene.split_off(0) {
10461 framed.push_primitive(primitive);
10462 }
10463 framed
10464}
10465
10466#[cfg(test)]
10467mod backdrop_tests {
10468 use super::*;
10469
10470 #[test]
10473 fn the_background_leaves_the_system_s_line_clear() {
10474 let mut scene = Scene::new();
10475 scene.fill_rect(
10476 frus_widgets::Rect::new(10.0, 5.0, 20.0, 20.0),
10477 frus_widgets::Color::WHITE,
10478 );
10479 let framed = backdrop_under(scene.clone(), 800.0, 600.0, 48.0);
10480 let prims = framed.primitives();
10481 assert_eq!(prims.len(), 2);
10482 match &prims[0] {
10483 frus_widgets::Primitive::Rect { rect, color, .. } => {
10484 assert_eq!(*rect, frus_widgets::Rect::new(0.0, 48.0, 800.0, 552.0));
10485 assert_eq!(*color, frus_gpu::backdrop());
10486 assert_eq!(
10487 color.a, 1.0,
10488 "opaque: nothing behind the window shows there"
10489 );
10490 }
10491 other => panic!("the background first: {other:?}"),
10492 }
10493 assert_eq!(prims[1], scene.primitives()[0], "then the scene");
10494
10495 let whole = backdrop_under(Scene::new(), 800.0, 600.0, 0.0);
10496 assert!(
10497 matches!(&whole.primitives()[0],
10498 frus_widgets::Primitive::Rect { rect, .. } if *rect == frus_widgets::Rect::new(0.0, 0.0, 800.0, 600.0)),
10499 "no line: the whole window"
10500 );
10501 }
10502}
10503
10504#[cfg(any(test, feature = "testing"))]
10507#[doc(hidden)]
10508pub mod testing {
10509 use super::*;
10510 use crate::gesture::{PointerEvent, PointerKind, LONG_PRESS_DELAY};
10511
10512 pub type FrameParts<'a, Msg> = (&'a Ui<Msg>, &'a dyn Widget<Msg>);
10514
10515 pub struct Driver<A: Application> {
10517 shell: App<A>,
10518 size: Size,
10519 ghost: Option<Rect>,
10521 }
10522
10523 impl<A: Application> Driver<A> {
10524 pub fn new(app: A, width: f32, height: f32) -> Self {
10526 let mut shell = App::detached(app);
10527 shell.gesture_clock = Some((0.0, 0.0));
10529 Self {
10530 shell,
10531 size: Size::new(width, height),
10532 ghost: None,
10533 }
10534 }
10535
10536 pub fn system_back(&mut self) -> bool {
10539 self.shell.system_back_step()
10540 }
10541
10542 pub fn fling_strategy(&self) -> frus_widgets::VelocityStrategy {
10545 self.shell.gesture_velocity.strategy()
10546 }
10547
10548 pub fn carried(&self) -> Option<Rect> {
10551 self.shell.carried_rect()
10552 }
10553
10554 pub fn ghost(&self) -> Option<Rect> {
10557 self.ghost
10558 }
10559
10560 pub fn offsets(&self) -> Vec<(Rect, (f32, f32))> {
10562 let s = &self.shell;
10563 s.ui.as_ref()
10564 .map(|ui| {
10565 ui.scroll_regions()
10566 .iter()
10567 .map(|a| {
10568 (
10569 a.viewport,
10570 s.runtime.scroll.get(&a.id).copied().unwrap_or((0.0, 0.0)),
10571 )
10572 })
10573 .collect()
10574 })
10575 .unwrap_or_default()
10576 }
10577
10578 pub fn frame_parts(&self) -> Option<FrameParts<'_, A::Message>> {
10581 let s = &self.shell;
10582 Some((s.ui.as_ref()?, s.tree.as_deref()?))
10583 }
10584
10585 pub fn texts(&self) -> Vec<(String, Rect)> {
10587 self.shell
10588 .ui
10589 .as_ref()
10590 .map(|ui| {
10591 ui.scene()
10592 .primitives()
10593 .iter()
10594 .filter_map(|p| match p {
10595 frus_widgets::Primitive::Text { text, .. } => {
10596 Some((text.clone(), p.bounds()))
10597 }
10598 _ => None,
10599 })
10600 .collect()
10601 })
10602 .unwrap_or_default()
10603 }
10604
10605 pub fn tap_text(&mut self, label: &str) -> bool {
10608 let Some((_, rect)) = self.texts().into_iter().find(|(text, _)| text == label) else {
10609 return false;
10610 };
10611 let at = Point::new(rect.x + rect.width / 2.0, rect.y + rect.height / 2.0);
10612 self.press(at);
10613 self.release(at);
10614 self.frame(1.0 / 60.0);
10615 true
10616 }
10617
10618 pub fn app(&self) -> &A {
10620 &self.shell.app
10621 }
10622
10623 pub fn focused(&self) -> Option<WidgetId> {
10625 self.shell.runtime.input.focused
10626 }
10627
10628 pub fn shell_modifiers(&self) -> (bool, bool, bool, bool) {
10630 (
10631 self.shell.ctrl,
10632 self.shell.shift,
10633 self.shell.alt,
10634 self.shell.meta,
10635 )
10636 }
10637
10638 pub fn hit(&self, at: Point) -> Option<WidgetId> {
10640 self.shell.ui.as_ref().and_then(|ui| ui.hit(at))
10641 }
10642
10643 pub fn update(&mut self, message: A::Message) {
10645 self.shell.dispatch(message);
10646 }
10647
10648 pub fn run(&mut self, seconds: f32) {
10650 let n = (seconds * 60.0).round() as usize;
10651 for _ in 0..n.max(1) {
10652 self.frame(1.0 / 60.0);
10653 }
10654 }
10655
10656 pub fn press(&mut self, at: Point) {
10658 self.pointer(PointerKind::Down, at);
10659 }
10660
10661 pub fn move_to(&mut self, at: Point) {
10663 self.pointer(PointerKind::Move, at);
10664 }
10665
10666 pub fn release(&mut self, at: Point) {
10668 self.pointer(PointerKind::Up, at);
10669 }
10670
10671 pub fn hold_deadline(&mut self) -> bool {
10674 let fired = self.shell.press.poll(Instant::now() + LONG_PRESS_DELAY);
10675 if fired {
10676 self.shell.hold_deadline_reached();
10677 }
10678 fired
10679 }
10680
10681 pub fn tap_deadline(&mut self) -> bool {
10684 self.shell
10685 .poll_pending_tap(Instant::now() + DOUBLE_TAP_TIMEOUT)
10686 }
10687
10688 pub fn cursor_shown(&self) -> frus_widgets::Cursor {
10690 let s = &self.shell;
10691 let requested = s.runtime.input.hovered.and_then(|id| {
10692 let rect = s.ui.as_ref()?.widget_rect(id)?;
10693 let widget = find_widget(s.tree.as_ref()?.as_ref(), id)?;
10694 widget.cursor_icon(
10695 s.cursor.x - rect.x,
10696 s.cursor.y - rect.y,
10697 rect.width,
10698 rect.height,
10699 )
10700 });
10701 s.region_cursor(requested)
10702 }
10703
10704 pub fn next_wake(&self) -> Option<Instant> {
10706 match self.shell.idle_control_flow() {
10707 ControlFlow::WaitUntil(at) => Some(at),
10708 _ => None,
10709 }
10710 }
10711
10712 pub fn secondary_click(&mut self, at: Point) {
10714 self.pointer_of(PointerKind::Move, at, false);
10715 self.shell.context_click();
10716 }
10717
10718 pub fn press_mouse(&mut self, at: Point) {
10721 self.pointer_of(PointerKind::Down, at, false);
10722 }
10723
10724 pub fn move_mouse(&mut self, at: Point) {
10726 self.pointer_of(PointerKind::Move, at, false);
10727 }
10728
10729 pub fn release_mouse(&mut self, at: Point) {
10731 self.pointer_of(PointerKind::Up, at, false);
10732 }
10733
10734 pub fn area_selection(&self) -> Option<String> {
10736 self.shell.region_text()
10737 }
10738
10739 pub fn area_select_all(&mut self) {
10741 self.shell.select_all_region();
10742 }
10743
10744 pub fn escape(&mut self) {
10746 self.shell.escape();
10747 }
10748
10749 pub fn key(&mut self, stroke: frus_widgets::KeyStroke) {
10757 use frus_widgets::ShortcutKey as K;
10758 let named = |key| (WinitKey::Named(key), None);
10759 let (logical_key, text) = match stroke.key {
10760 K::Char(c) => {
10761 let typed: winit::keyboard::SmolStr = c.to_string().into();
10762 (WinitKey::Character(typed.clone()), Some(typed))
10763 }
10764 K::Enter => named(NamedKey::Enter),
10765 K::Escape => named(NamedKey::Escape),
10766 K::Tab => named(NamedKey::Tab),
10767 K::Space => (WinitKey::Named(NamedKey::Space), Some(" ".into())),
10768 K::Backspace => named(NamedKey::Backspace),
10769 K::Delete => named(NamedKey::Delete),
10770 K::Up => named(NamedKey::ArrowUp),
10771 K::Down => named(NamedKey::ArrowDown),
10772 K::Left => named(NamedKey::ArrowLeft),
10773 K::Right => named(NamedKey::ArrowRight),
10774 K::Home => named(NamedKey::Home),
10775 K::End => named(NamedKey::End),
10776 K::PageUp => named(NamedKey::PageUp),
10777 K::PageDown => named(NamedKey::PageDown),
10778 K::F(n) => named(match n {
10779 1 => NamedKey::F1,
10780 2 => NamedKey::F2,
10781 3 => NamedKey::F3,
10782 4 => NamedKey::F4,
10783 5 => NamedKey::F5,
10784 6 => NamedKey::F6,
10785 7 => NamedKey::F7,
10786 8 => NamedKey::F8,
10787 9 => NamedKey::F9,
10788 10 => NamedKey::F10,
10789 11 => NamedKey::F11,
10790 _ => NamedKey::F12,
10791 }),
10792 };
10793 const LETTERS: [KeyCode; 26] = [
10796 KeyCode::KeyA,
10797 KeyCode::KeyB,
10798 KeyCode::KeyC,
10799 KeyCode::KeyD,
10800 KeyCode::KeyE,
10801 KeyCode::KeyF,
10802 KeyCode::KeyG,
10803 KeyCode::KeyH,
10804 KeyCode::KeyI,
10805 KeyCode::KeyJ,
10806 KeyCode::KeyK,
10807 KeyCode::KeyL,
10808 KeyCode::KeyM,
10809 KeyCode::KeyN,
10810 KeyCode::KeyO,
10811 KeyCode::KeyP,
10812 KeyCode::KeyQ,
10813 KeyCode::KeyR,
10814 KeyCode::KeyS,
10815 KeyCode::KeyT,
10816 KeyCode::KeyU,
10817 KeyCode::KeyV,
10818 KeyCode::KeyW,
10819 KeyCode::KeyX,
10820 KeyCode::KeyY,
10821 KeyCode::KeyZ,
10822 ];
10823 let physical_key = match stroke.key {
10824 K::Char(c) if c.is_ascii_alphabetic() => {
10825 PhysicalKey::Code(LETTERS[(c.to_ascii_lowercase() as u8 - b'a') as usize])
10826 }
10827 _ => PhysicalKey::Unidentified(winit::keyboard::NativeKeyCode::Unidentified),
10828 };
10829 let held = (
10830 self.shell.ctrl,
10831 self.shell.shift,
10832 self.shell.alt,
10833 self.shell.meta,
10834 );
10835 (
10836 self.shell.ctrl,
10837 self.shell.shift,
10838 self.shell.alt,
10839 self.shell.meta,
10840 ) = (stroke.ctrl, stroke.shift, stroke.alt, stroke.meta);
10841 self.shell.key_down(
10842 &KeyDown {
10843 logical_key,
10844 physical_key,
10845 text,
10846 repeat: false,
10847 },
10848 None,
10849 );
10850 (
10851 self.shell.ctrl,
10852 self.shell.shift,
10853 self.shell.alt,
10854 self.shell.meta,
10855 ) = held;
10856 }
10857
10858 pub fn area_handles(&self) -> Option<[frus_widgets::SelectionHandle; 2]> {
10860 self.shell.region_handles().map(|(_, handles)| handles)
10861 }
10862
10863 fn pointer(&mut self, kind: PointerKind, at: Point) {
10864 self.pointer_of(kind, at, true);
10865 }
10866
10867 fn pointer_of(&mut self, kind: PointerKind, at: Point, touch: bool) {
10868 self.shell.pointer_event(PointerEvent {
10869 kind,
10870 position: at,
10871 touch,
10872 });
10873 }
10874
10875 pub fn frame(&mut self, dt: f32) {
10877 let (width, height) = (self.size.width, self.size.height);
10878 let s = &mut self.shell;
10879 if let Some((now, _)) = s.gesture_clock.as_mut() {
10880 *now += dt;
10881 }
10882 if s.last_size != Some((width, height)) {
10883 s.last_size = Some((width, height));
10884 s.build_dirty = true;
10885 s.app.on_resize(width, height);
10886 }
10887 let mut app_animating = s.app.tick(dt);
10888 let settings = s
10889 .platform
10890 .accessibility
10891 .with_overrides(s.app.accessibility());
10892 let theme_moved = s.themes.advance(
10893 &s.app,
10894 s.platform.brightness,
10895 settings.high_contrast,
10896 settings.disable_animations,
10897 dt,
10898 );
10899 app_animating |= theme_moved | s.themes.animating();
10900 let theme = s.themes.displayed(&s.app);
10901 install_ambient(&s.app, &s.platform.locales);
10902 let _surface = s.media_query(width, height).install();
10903 s.runtime.still = settings.disable_animations;
10904 let was = std::mem::replace(&mut s.app_was_animating, app_animating);
10905 let asked = s.app.effects();
10906 s.run_command(asked);
10907 let arrived = s.take_task_arrivals();
10908 if frus_widgets::take_rebuild_request() || arrived {
10909 s.build_dirty = true;
10910 }
10911 s.report_focus();
10912 if frame_needs_build(
10913 s.build_dirty,
10914 app_animating,
10915 was,
10916 s.tree.is_none() || s.runtime.switching(),
10917 ) {
10918 s.tree = Some(build_view(&s.app, &theme, &s.runtime));
10919 }
10920 s.build_dirty = false;
10921 s.autoscroll_carried(dt);
10922 s.advance_reorder_springs(dt);
10923 let regions =
10924 s.ui.as_ref()
10925 .map(|ui| ui.scroll_regions().to_vec())
10926 .unwrap_or_default();
10927 s.runtime.sync_pages(®ions);
10928 s.runtime.sync_visible(®ions);
10929 s.runtime.advance(dt);
10930 s.runtime.advance_scroll(®ions, dt);
10931 let tree = s.tree.as_deref().expect("the view was built");
10932 s.runtime.advance_values(tree, dt);
10935 let ui = build_ui(tree, Size::new(width, height), &s.runtime, &theme);
10936 s.runtime.states.end_frame();
10937 let mut scene = ui.scene().clone();
10938 let ghost = if matches!(s.drag, Some(Drag::Reorder { moved: true, .. })) {
10939 s.paint_reorder_preview(&ui, &theme, &mut scene);
10940 scene.primitives().last().map(|p| p.bounds())
10942 } else {
10943 None
10944 };
10945 let paged = ui.scroll_regions().to_vec();
10946 let scrolled: Vec<A::Message> = {
10947 let grain = |id| find_widget(tree, id).map_or(0.0, |w| w.scroll_grain());
10948 s.runtime
10949 .scroll_changes(&paged, grain)
10950 .into_iter()
10951 .filter_map(|(id, position)| {
10952 find_widget(tree, id).and_then(|w| w.on_scroll(position))
10953 })
10954 .collect()
10955 };
10956 s.ui = Some(ui);
10957 s.sync_regions(false);
10958 s.reconcile_focus();
10961 for message in scrolled {
10962 s.dispatch(message);
10963 }
10964 self.ghost = ghost;
10965 }
10966 }
10967}
10968
10969#[cfg(test)]
10983mod carried_row_tests {
10984 use super::testing::Driver;
10985 use crate::{Application, Command};
10986 use frus_widgets::{
10987 column, text, Axis, Button, CellFn, Container, Flex, Kanban, Point, Rect, ReorderAxis,
10988 ReorderGrab, ReorderableList, SingleChildScrollView, Theme, Widget,
10989 };
10990
10991 const W: f32 = 392.7;
10993 const H: f32 = 850.9;
10994 const STRIP_Y: f32 = 88.0;
10996 const STRIP_CONTENT: f32 = 24.0 + 10.0 * 96.0 + 9.0 * 8.0 + 24.0;
10998
10999 #[derive(Clone, Debug, PartialEq)]
11000 enum Board {
11001 Label(usize, usize),
11002 Card(usize, usize, usize, usize),
11003 Delete(usize, usize),
11004 Add(usize),
11005 Row(usize, usize),
11006 }
11007
11008 struct BoardApp {
11010 labels: Vec<usize>,
11011 cards: Vec<Vec<String>>,
11012 log: Vec<Board>,
11013 }
11014
11015 impl Default for BoardApp {
11016 fn default() -> Self {
11017 let cols: [&[&str]; 3] = [
11018 &["Design API", "Write spec", "Triage bugs"],
11019 &["Build widget"],
11020 &["Kickoff", "Research"],
11021 ];
11022 Self {
11023 labels: (0..10).collect(),
11024 cards: cols
11025 .iter()
11026 .map(|c| c.iter().map(|s| s.to_string()).collect())
11027 .collect(),
11028 log: Vec::new(),
11029 }
11030 }
11031 }
11032
11033 impl Application for BoardApp {
11034 type Message = Board;
11035
11036 fn update(&mut self, message: Board) -> Command<Board> {
11037 self.log.push(message.clone());
11038 match message {
11039 Board::Label(from, to) => {
11040 let label = self.labels.remove(from);
11041 self.labels.insert(to, label);
11042 }
11043 Board::Card(from_col, from_pos, to_col, to_pos) => {
11044 let card = self.cards[from_col].remove(from_pos);
11045 let to_pos = to_pos.min(self.cards[to_col].len());
11046 self.cards[to_col].insert(to_pos, card);
11047 }
11048 _ => {}
11049 }
11050 Command::none()
11051 }
11052
11053 fn view(&self, theme: &Theme) -> Box<dyn Widget<Board>> {
11054 let mut strip = ReorderableList::new(Board::Label)
11055 .axis(ReorderAxis::Horizontal)
11056 .grab(ReorderGrab::LongPress)
11057 .gap(8.0);
11058 for &label in &self.labels {
11059 strip = strip.keyed_row(
11060 label as u64,
11061 Container::new()
11062 .width(96.0)
11063 .padding(12.0)
11064 .color(theme.surface)
11065 .child(text(format!("label {label}")).size(14.0)),
11066 );
11067 }
11068 let strip = SingleChildScrollView::new()
11069 .axis(Axis::Horizontal)
11070 .width(W)
11071 .child(
11072 Container::new()
11073 .padding_each(12.0, 24.0, 0.0, 24.0)
11074 .child(strip),
11075 );
11076 let mut board = Kanban::new(Board::Card)
11077 .on_add(Board::Add)
11078 .scrollable_columns();
11079 for (col, title) in ["To do", "Doing", "Done"].iter().enumerate() {
11080 let cards: Vec<CellFn<Board>> = self.cards[col]
11081 .iter()
11082 .enumerate()
11083 .map(|(pos, label)| {
11084 let label = label.clone();
11085 Box::new(move || {
11086 Box::new(
11087 Flex::row()
11088 .child(text(label.clone()).size(14.0))
11089 .child(Flex::row().flex(1.0))
11090 .child(Button::new("x").on_press(Board::Delete(col, pos))),
11091 ) as Box<dyn Widget<Board>>
11092 }) as CellFn<Board>
11093 })
11094 .collect();
11095 board = board.column_widgets(*title, cards);
11096 }
11097 let board = SingleChildScrollView::new()
11098 .axis(Axis::Horizontal)
11099 .width(W)
11100 .flex(1.0)
11101 .child(Container::new().padding(24.0).child(board));
11102 Box::new(
11103 Container::new().width(W).height(H).child(
11104 column![
11105 Container::<Board>::new().width(W).height(56.0),
11106 strip,
11107 board
11108 ]
11109 .flex(1.0),
11110 ),
11111 )
11112 }
11113 }
11114
11115 #[derive(Default)]
11117 struct ListApp {
11118 log: Vec<Board>,
11119 }
11120
11121 impl Application for ListApp {
11122 type Message = Board;
11123
11124 fn update(&mut self, message: Board) -> Command<Board> {
11125 self.log.push(message);
11126 Command::none()
11127 }
11128
11129 fn view(&self, _theme: &Theme) -> Box<dyn Widget<Board>> {
11130 let mut list = ReorderableList::new(Board::Row)
11131 .grab(ReorderGrab::LongPress)
11132 .width(W);
11133 for row in 0..20u64 {
11134 list = list.keyed_row(
11135 row,
11136 Container::new()
11137 .height(60.0)
11138 .child(text(format!("row {row}")).size(14.0)),
11139 );
11140 }
11141 Box::new(
11142 SingleChildScrollView::new()
11143 .axis(Axis::Vertical)
11144 .width(W)
11145 .height(H)
11146 .child(list),
11147 )
11148 }
11149 }
11150
11151 fn driver<A: Application>(app: A) -> Driver<A> {
11152 let mut driver = Driver::new(app, W, H);
11153 driver.run(0.2);
11154 driver
11155 }
11156
11157 fn hold<A: Application>(driver: &mut Driver<A>, at: Point) {
11159 driver.press(at);
11160 driver.run(0.5);
11161 assert!(
11162 driver.hold_deadline(),
11163 "the hold lifts what is under {at:?}"
11164 );
11165 driver.run(0.05);
11166 }
11167
11168 fn carry<A: Application>(driver: &mut Driver<A>, points: impl IntoIterator<Item = Point>) {
11170 for at in points {
11171 driver.move_to(at);
11172 driver.run(0.1);
11173 }
11174 }
11175
11176 fn held_under_the_finger<A: Application>(
11180 driver: &mut Driver<A>,
11181 seconds: f32,
11182 finger: Point,
11183 offset: (f32, f32),
11184 size: (f32, f32),
11185 region: usize,
11186 ) -> (f32, f32) {
11187 let mut last = driver.offsets()[region].1;
11188 for frame in 0..(seconds * 60.0) as usize {
11189 driver.run(1.0 / 60.0);
11190 let now = driver.offsets()[region].1;
11191 let carried: Rect = driver.carried().unwrap_or_else(|| {
11192 panic!("frame {frame}: nothing is carried any more, the list at {now:?}")
11193 });
11194 assert!(
11195 (carried.x - finger.x - offset.0).abs() < 0.5
11196 && (carried.y - finger.y - offset.1).abs() < 0.5
11197 && (carried.width - size.0).abs() < 0.5
11198 && (carried.height - size.1).abs() < 0.5,
11199 "frame {frame}: carried at {carried:?}, the finger at {finger:?}, the list at {now:?}"
11200 );
11201 assert!(
11202 now.0 >= last.0 - 0.01 && now.1 >= last.1 - 0.01,
11203 "frame {frame}: the list went back, {last:?} then {now:?}"
11204 );
11205 let ghost = driver
11207 .ghost()
11208 .unwrap_or_else(|| panic!("frame {frame}: no ghost is drawn"));
11209 assert!(
11210 (ghost.x - carried.x).abs() < 1.0
11211 && (ghost.y - carried.y).abs() < 1.0
11212 && (ghost.width - carried.width).abs() < 2.0,
11213 "frame {frame}: the ghost is drawn at {ghost:?}, what is carried is at {carried:?}"
11214 );
11215 last = now;
11216 }
11217 last
11218 }
11219
11220 #[test]
11224 fn a_label_carried_past_the_strips_edge_stays_under_the_finger() {
11225 let mut driver = driver(BoardApp::default());
11226 hold(&mut driver, Point::new(155.0, STRIP_Y));
11227 let lifted = driver.carried().expect("the label is carried");
11228 assert_eq!(
11229 lifted,
11230 Rect::new(128.0, 68.0, 96.0, 41.0),
11231 "Feature, lifted"
11232 );
11233 let finger = Point::new(370.0, STRIP_Y);
11234 carry(
11235 &mut driver,
11236 [200.0, 260.0, 320.0, finger.x].map(|x| Point::new(x, STRIP_Y)),
11237 );
11238 let strip = held_under_the_finger(
11239 &mut driver,
11240 1.5,
11241 finger,
11242 (lifted.x - 155.0, lifted.y - STRIP_Y),
11243 (96.0, 41.0),
11244 0,
11245 );
11246 assert!(
11247 (strip.0 - (STRIP_CONTENT - W)).abs() < 1.0,
11248 "the strip scrolled on to its end: {strip:?}"
11249 );
11250 driver.release(finger);
11251 driver.run(0.2);
11252 assert_eq!(
11253 driver.app().log,
11254 [Board::Label(1, 9)],
11255 "past the last label, it goes last; no card moves"
11256 );
11257 }
11258
11259 #[test]
11262 fn a_hold_on_a_label_then_a_carry_along_x_reorders_the_labels_and_moves_no_card() {
11263 let mut driver = driver(BoardApp::default());
11264 hold(&mut driver, Point::new(155.0, STRIP_Y));
11265 carry(
11266 &mut driver,
11267 [167.0, 189.0, 218.0, 247.0, 276.0, 306.0].map(|x| Point::new(x, STRIP_Y)),
11268 );
11269 driver.run(0.4);
11270 assert!(driver.carried().is_some(), "still carried");
11271 driver.release(Point::new(306.0, STRIP_Y));
11272 driver.run(0.2);
11273 assert_eq!(driver.app().log, [Board::Label(1, 2)]);
11274 assert_eq!(driver.app().labels[..4], [0, 2, 1, 3]);
11275 assert_eq!(
11276 driver.app().cards,
11277 BoardApp::default().cards,
11278 "no card moved"
11279 );
11280 assert_eq!(driver.offsets()[0].1, (0.0, 0.0), "nothing scrolled");
11281 assert_eq!(driver.offsets()[1].1, (0.0, 0.0), "the board neither");
11282 }
11283
11284 #[test]
11287 fn a_card_carried_across_the_board_moves_the_card() {
11288 let mut driver = driver(BoardApp::default());
11289 driver.press(Point::new(100.0, 190.0));
11290 carry(
11291 &mut driver,
11292 [130.0, 180.0, 240.0, 300.0].map(|x| Point::new(x, 185.0)),
11293 );
11294 driver.release(Point::new(300.0, 185.0));
11295 driver.run(0.2);
11296 assert_eq!(driver.app().log, [Board::Card(0, 0, 1, 0)]);
11297 assert_eq!(driver.app().labels, (0..10).collect::<Vec<_>>());
11298 }
11299
11300 #[test]
11303 fn a_card_carried_to_the_boards_edge_stays_under_the_finger() {
11304 let mut driver = driver(BoardApp::default());
11305 let press = Point::new(100.0, 190.0);
11306 driver.press(press);
11307 let finger = Point::new(385.0, 190.0);
11308 carry(
11309 &mut driver,
11310 [130.0, 200.0, 280.0, 340.0, finger.x].map(|x| Point::new(x, 190.0)),
11311 );
11312 let card = driver.carried().expect("the card is carried");
11313 let board = held_under_the_finger(
11314 &mut driver,
11315 1.0,
11316 finger,
11317 (card.x - finger.x, card.y - finger.y),
11318 (card.width, card.height),
11319 1,
11320 );
11321 assert!(
11322 board.0 > 250.0,
11323 "the board scrolled past the column the card came from: {board:?}"
11324 );
11325 }
11326
11327 #[test]
11331 fn a_row_carried_past_the_bottom_of_a_long_list_stays_under_the_finger() {
11332 let mut driver = driver(ListApp::default());
11333 hold(&mut driver, Point::new(100.0, 30.0));
11334 let lifted = driver.carried().expect("the row is carried");
11335 let finger = Point::new(100.0, 840.0);
11336 carry(
11337 &mut driver,
11338 [200.0, 400.0, 600.0, 800.0, finger.y].map(|y| Point::new(100.0, y)),
11339 );
11340 let list = held_under_the_finger(
11341 &mut driver,
11342 1.5,
11343 finger,
11344 (lifted.x - 100.0, lifted.y - 30.0),
11345 (lifted.width, lifted.height),
11346 0,
11347 );
11348 assert!(
11349 list.1 > 200.0,
11350 "the list scrolled past the row's own place: {list:?}"
11351 );
11352 driver.release(finger);
11353 driver.run(0.2);
11354 assert!(
11355 matches!(driver.app().log[..], [Board::Row(0, to)] if to > 15),
11356 "row 0 lands near the end: {:?}",
11357 driver.app().log
11358 );
11359 }
11360}
11361
11362#[cfg(test)]
11370mod press_tests {
11371 use super::{gesture_was_a_tap, press_at, settle_press, Drag, Point, Runtime, Theme};
11372 use crate::gesture::PointerKind;
11373 use crate::hover::Hover;
11374 use frus_widgets::{
11375 build_ui, Axis, Button, Container, Flex, Primitive, SegmentedButton, SingleChildScrollView,
11376 Size, Ui, Widget,
11377 };
11378
11379 const SCREEN: Size = Size::new(300.0, 400.0);
11380 const DT: f32 = 1.0 / 60.0;
11381
11382 fn filters() -> Box<dyn Widget<usize>> {
11384 let mut segments = SegmentedButton::new(0, |i| i);
11385 for label in ["All", "Active", "Done", "Archived", "Starred", "Shared"] {
11386 segments = segments.segment(label);
11387 }
11388 Box::new(
11389 Flex::column().width(300.0).height(400.0).child(
11390 SingleChildScrollView::new()
11391 .axis(Axis::Horizontal)
11392 .width(300.0)
11393 .height(80.0)
11394 .child(segments),
11395 ),
11396 )
11397 }
11398
11399 fn page() -> Box<dyn Widget<usize>> {
11401 Box::new(
11402 Flex::column().width(300.0).height(400.0).child(
11403 SingleChildScrollView::new()
11404 .width(300.0)
11405 .height(400.0)
11406 .child(
11407 Flex::column()
11408 .child(Button::new("Go").on_press(7usize))
11409 .child(Container::new().width(300.0).height(1200.0)),
11410 ),
11411 ),
11412 )
11413 }
11414
11415 fn frame(view: &dyn Widget<usize>, runtime: &Runtime) -> Ui<usize> {
11416 build_ui(view, SCREEN, runtime, &Theme::default())
11417 }
11418
11419 fn idle(runtime: &mut Runtime, seconds: f32) {
11421 for _ in 0..(seconds / DT) as usize {
11422 runtime.advance(DT);
11423 runtime.advance_ink(DT);
11424 }
11425 }
11426
11427 fn painted(view: &dyn Widget<usize>, runtime: &Runtime) -> Vec<Primitive> {
11429 frame(view, runtime).scene().primitives().to_vec()
11430 }
11431
11432 fn untouched(view: &dyn Widget<usize>, runtime: &Runtime) -> Vec<Primitive> {
11434 let mut quiet = Runtime::default();
11435 quiet.scroll.clone_from(&runtime.scroll);
11436 quiet.scroll_target.clone_from(&runtime.scroll_target);
11437 idle(&mut quiet, 0.5);
11438 painted(view, &quiet)
11439 }
11440
11441 struct Pointer<'a> {
11443 view: &'a dyn Widget<usize>,
11444 runtime: Runtime,
11445 hover: Hover,
11446 drag: Option<Drag>,
11447 touch: bool,
11448 at: Point,
11449 }
11450
11451 impl<'a> Pointer<'a> {
11452 fn new(view: &'a dyn Widget<usize>, touch: bool) -> Self {
11453 Self {
11454 view,
11455 runtime: Runtime::default(),
11456 hover: Hover::default(),
11457 drag: None,
11458 touch,
11459 at: Point::new(0.0, 0.0),
11460 }
11461 }
11462
11463 fn event(&mut self, kind: PointerKind, at: Point, route: impl FnOnce(&mut Self)) {
11465 self.at = at;
11466 self.hover.event(kind, at, self.touch);
11467 let could_tap = gesture_was_a_tap(self.drag.as_ref());
11468 if kind == PointerKind::Down {
11469 let ui = frame(self.view, &self.runtime);
11470 self.runtime.input.hovered = self.hover.target(&ui);
11471 press_at(&mut self.runtime, Some(&ui), at);
11472 }
11473 route(self);
11474 settle_press(
11475 &mut self.runtime.input,
11476 kind,
11477 could_tap,
11478 self.drag.as_ref(),
11479 self.touch,
11480 );
11481 if matches!(kind, PointerKind::Up | PointerKind::Cancel) {
11482 let ui = frame(self.view, &self.runtime);
11483 self.runtime.input.hovered = self.hover.target(&ui);
11484 }
11485 }
11486
11487 fn land(&mut self, at: Point) {
11489 self.event(PointerKind::Down, at, |p| {
11490 if !p.touch {
11491 return;
11492 }
11493 let ui = frame(p.view, &p.runtime);
11494 let area = ui
11495 .scroll_chain(at)
11496 .find(|area| area.accepts_user_offset((0.0, 0.0)))
11497 .expect("the region under the finger has somewhere to go");
11498 p.drag = Some(Drag::Scroll {
11499 id: area.id,
11500 last: at,
11501 moved: false,
11502 carried: (0.0, 0.0),
11503 dismiss: None,
11504 axis: None,
11505 pan: None,
11506 });
11507 });
11508 }
11509
11510 fn slide(&mut self, by: (f32, f32)) {
11513 let to = Point::new(self.at.x + by.0, self.at.y + by.1);
11514 self.event(PointerKind::Move, to, |p| {
11515 let Some(Drag::Scroll {
11516 id,
11517 moved,
11518 axis,
11519 last,
11520 ..
11521 }) = p.drag.as_mut()
11522 else {
11523 return;
11524 };
11525 let (dx, dy) = (to.x - last.x, to.y - last.y);
11526 if dx.hypot(dy) <= super::TOUCH_SLOP && !*moved {
11527 return;
11528 }
11529 *moved = true;
11530 *axis = Some(dy.abs() >= dx.abs());
11531 *last = to;
11532 let offset = p.runtime.scroll.get(id).copied().unwrap_or((0.0, 0.0));
11533 let moved_to = (offset.0 - by.0, offset.1 - by.1);
11534 p.runtime.scroll.insert(*id, moved_to);
11535 p.runtime.scroll_target.insert(*id, moved_to);
11536 });
11537 }
11538
11539 fn lift(&mut self) -> Option<Drag> {
11541 let mut ended = None;
11542 self.event(PointerKind::Up, self.at, |p| ended = p.drag.take());
11543 ended
11544 }
11545 }
11546
11547 #[test]
11551 fn a_segment_a_swipe_started_on_is_not_left_pressed() {
11552 let view = filters();
11553 let mut finger = Pointer::new(view.as_ref(), true);
11554 let done = Point::new(200.0, 16.0);
11555 finger.land(done);
11556 let segment = finger
11557 .runtime
11558 .input
11559 .pressed
11560 .expect("the segment is pressed");
11561 assert!(
11562 finger.runtime.ink.contains_key(&segment),
11563 "and it splashed: a segment takes ink"
11564 );
11565 assert!(
11566 frame(view.as_ref(), &finger.runtime)
11567 .scroll_chain(done)
11568 .any(|area| area.max_x > 0.0),
11569 "the strip is wider than the screen, as the phone's was"
11570 );
11571 for _ in 0..19 {
11572 finger.slide((-10.0, 0.0));
11573 }
11574 let ended = finger.lift();
11575 assert!(
11576 !gesture_was_a_tap(ended.as_ref()),
11577 "a gesture that scrolled clicks nothing"
11578 );
11579 idle(&mut finger.runtime, 2.0);
11580 let (shown, quiet) = (
11582 painted(view.as_ref(), &finger.runtime),
11583 untouched(view.as_ref(), &finger.runtime),
11584 );
11585 assert_eq!(
11586 shown.len(),
11587 quiet.len(),
11588 "two seconds on, the screen paints what nobody touched, and no layer more"
11589 );
11590 assert_eq!(shown, quiet, "nothing is highlighted");
11591 assert!(
11592 finger.runtime.ink.is_empty(),
11593 "no ink waits for a finger that has gone"
11594 );
11595 assert_eq!(finger.runtime.input.pressed, None, "the press went with it");
11596 assert_eq!(
11597 finger.runtime.input.hovered, None,
11598 "and a lifted finger hovers nothing"
11599 );
11600 }
11601
11602 #[test]
11605 fn the_press_ends_when_the_scroll_takes_it() {
11606 let view = filters();
11607 let mut finger = Pointer::new(view.as_ref(), true);
11608 finger.land(Point::new(200.0, 16.0));
11609 finger.slide((-4.0, 0.0));
11610 assert!(
11611 finger.runtime.input.pressed.is_some(),
11612 "under the slop it may still be a tap"
11613 );
11614 finger.slide((-40.0, 0.0));
11615 assert_eq!(
11616 finger.runtime.input.pressed, None,
11617 "past it, the scroll has it"
11618 );
11619 assert_eq!(
11620 finger.runtime.input.hovered, None,
11621 "and the finger's hover with it"
11622 );
11623 idle(&mut finger.runtime, 0.5);
11624 assert_eq!(
11625 painted(view.as_ref(), &finger.runtime),
11626 untouched(view.as_ref(), &finger.runtime),
11627 "the finger is still down, and nothing under it is highlighted"
11628 );
11629 }
11630
11631 #[test]
11633 fn a_button_a_page_scrolled_from_is_not_left_pressed() {
11634 let view = page();
11635 let mut finger = Pointer::new(view.as_ref(), true);
11636 let go = Point::new(20.0, 15.0);
11637 finger.land(go);
11638 let button = finger.runtime.input.pressed.expect("the button is pressed");
11639 assert_eq!(
11640 frame(view.as_ref(), &finger.runtime).msg_for(button),
11641 Some(7),
11642 "the press is on the button"
11643 );
11644 for _ in 0..10 {
11645 finger.slide((0.0, -30.0));
11646 }
11647 assert!(!gesture_was_a_tap(finger.lift().as_ref()));
11648 idle(&mut finger.runtime, 2.0);
11649 assert_eq!(finger.runtime.input.pressed, None);
11650 assert!(finger.runtime.ink.is_empty());
11651 assert_eq!(
11652 painted(view.as_ref(), &finger.runtime),
11653 untouched(view.as_ref(), &finger.runtime),
11654 );
11655 }
11656
11657 #[test]
11660 fn a_tap_still_presses_and_clicks() {
11661 let view = filters();
11662 let mut finger = Pointer::new(view.as_ref(), true);
11663 let segment = Point::new(200.0, 16.0);
11664 finger.land(segment);
11665 finger.slide((3.0, 2.0));
11666 let pressed = finger.runtime.input.pressed;
11667 assert!(pressed.is_some(), "a wobble under the slop takes nothing");
11668 assert_eq!(finger.runtime.input.hovered, pressed, "and the press shows");
11669 let ui = frame(view.as_ref(), &finger.runtime);
11671 assert_eq!(
11672 ui.hit(segment),
11673 pressed,
11674 "released on the widget it pressed"
11675 );
11676 assert!(
11677 pressed.and_then(|id| ui.msg_for(id)).is_some(),
11678 "and the segment it pressed has a choice to make"
11679 );
11680 let ended = finger.lift();
11681 assert!(
11682 gesture_was_a_tap(ended.as_ref()),
11683 "a scroll that never moved"
11684 );
11685 assert_eq!(
11686 finger.runtime.input.pressed, None,
11687 "let go of once released"
11688 );
11689 }
11690
11691 #[test]
11694 fn a_mouse_keeps_its_press_while_it_moves_and_its_hover_after() {
11695 let view = filters();
11696 let mut mouse = Pointer::new(view.as_ref(), false);
11697 let done = Point::new(200.0, 16.0);
11698 mouse.land(done);
11699 assert!(mouse.drag.is_none(), "a mouse does not drag a scroll");
11700 let pressed = mouse.runtime.input.pressed.expect("pressed");
11701 mouse.slide((-60.0, 0.0));
11702 assert_eq!(mouse.runtime.input.pressed, Some(pressed), "still held");
11703 mouse.lift();
11704 assert_eq!(mouse.runtime.input.pressed, None);
11705 assert!(
11706 mouse.runtime.input.hovered.is_some(),
11707 "a released mouse is still over the strip"
11708 );
11709 }
11710
11711 #[test]
11714 fn a_gesture_takes_a_mouse_s_press_and_leaves_its_hover() {
11715 let id = frus_widgets::WidgetId::from_u64(3);
11716 let sheet = |moved| Drag::Sheet {
11717 id: frus_widgets::WidgetId::from_u64(9),
11718 last: Point::new(0.0, 0.0),
11719 moved,
11720 available: 800.0,
11721 };
11722 for touch in [false, true] {
11723 let mut input = frus_widgets::InputState {
11724 pressed: Some(id),
11725 hovered: Some(id),
11726 ..Default::default()
11727 };
11728 assert!(!settle_press(
11729 &mut input,
11730 PointerKind::Move,
11731 true,
11732 Some(&sheet(false)),
11733 touch
11734 ));
11735 assert_eq!(input.pressed, Some(id), "not yet past the slop");
11736 assert!(settle_press(
11737 &mut input,
11738 PointerKind::Move,
11739 true,
11740 Some(&sheet(true)),
11741 touch
11742 ));
11743 assert_eq!(input.pressed, None, "touch {touch}");
11744 assert_eq!(input.hovered, (!touch).then_some(id), "touch {touch}");
11745 }
11746 let mut input = frus_widgets::InputState {
11749 pressed: Some(id),
11750 ..Default::default()
11751 };
11752 let select = Drag::TextSelect {
11753 id,
11754 rect: super::Rect::new(0.0, 0.0, 10.0, 10.0),
11755 };
11756 assert!(!settle_press(
11757 &mut input,
11758 PointerKind::Move,
11759 false,
11760 Some(&select),
11761 true
11762 ));
11763 assert_eq!(input.pressed, Some(id));
11764 }
11765}
11766
11767#[cfg(test)]
11770mod selection_area_tests {
11771 use super::testing::Driver;
11772 use crate::{Application, Command};
11773 use frus_widgets::{Button, Container, Flex, Point, SelectionArea, Text, Theme, Widget};
11774
11775 const W: f32 = 400.0;
11776 const H: f32 = 300.0;
11777
11778 #[derive(Default)]
11780 struct Doc {
11781 bar: Bar,
11783 shared: std::rc::Rc<std::cell::Cell<u32>>,
11785 platform: Option<frus_widgets::TargetPlatform>,
11788 }
11789
11790 #[derive(Default, Clone, Copy)]
11792 enum Bar {
11793 #[default]
11795 Default,
11796 Share,
11798 Empty,
11800 }
11801
11802 impl Application for Doc {
11803 type Message = ();
11804
11805 fn update(&mut self, _message: ()) -> Command<()> {
11806 self.shared.set(self.shared.get() + 1);
11807 Command::none()
11808 }
11809
11810 fn theme(&self) -> Theme {
11811 Theme::default().with_platform(
11812 self.platform
11813 .unwrap_or(frus_widgets::TargetPlatform::Android),
11814 )
11815 }
11816
11817 fn view(&self, _theme: &Theme) -> Box<dyn Widget<()>> {
11818 let area = SelectionArea::around(
11819 Flex::column()
11820 .child(Text::new("first line of words").no_wrap())
11821 .child(Text::new("second line").no_wrap())
11822 .child(Button::new("Press").on_press(()))
11823 .child(Text::new("third line ends here").no_wrap()),
11824 );
11825 let area = match self.bar {
11826 Bar::Default => area,
11827 Bar::Share => area.selection_toolbar(|_, items| {
11828 let mut items: Vec<_> = items
11829 .into_iter()
11830 .filter(|item| item.label() == "Copy")
11831 .collect();
11832 items.push(frus_widgets::ToolbarItem::custom("Share", ()));
11833 items
11834 }),
11835 Bar::Empty => area.selection_toolbar(|_, _| Vec::new()),
11836 };
11837 Box::new(
11838 Container::new().width(W).height(H).child(
11839 Flex::column()
11840 .child(Text::new("Outside the area").no_wrap())
11841 .child(area),
11842 ),
11843 )
11844 }
11845 }
11846
11847 struct Tappable;
11849
11850 impl Application for Tappable {
11851 type Message = ();
11852
11853 fn update(&mut self, _message: ()) -> Command<()> {
11854 Command::none()
11855 }
11856
11857 fn view(&self, _theme: &Theme) -> Box<dyn Widget<()>> {
11858 Box::new(
11859 Container::new()
11860 .width(W)
11861 .height(H)
11862 .child(SelectionArea::around(
11863 Flex::column()
11864 .child(
11865 Container::new()
11866 .on_click(())
11867 .child(Text::new("tap this row").no_wrap()),
11868 )
11869 .child(Text::new("plain words here").no_wrap()),
11870 )),
11871 )
11872 }
11873 }
11874
11875 #[test]
11879 fn a_row_that_takes_a_tap_keeps_its_press() {
11880 let mut d = Driver::new(Tappable, W, H);
11881 d.run(0.2);
11882 let find = |d: &Driver<Tappable>, label: &str| {
11883 let (_, rect) = d
11884 .texts()
11885 .into_iter()
11886 .find(|(text, _)| text == label)
11887 .expect("on screen");
11888 Point::new(rect.x + 20.0, rect.y + 8.0)
11889 };
11890 let (tap, plain) = (find(&d, "tap this row"), find(&d, "plain words here"));
11891 let end = Point::new(plain.x + 60.0, plain.y);
11892 d.press_mouse(tap);
11893 d.run(0.05);
11894 d.move_mouse(end);
11895 d.run(0.05);
11896 d.release_mouse(end);
11897 d.run(0.05);
11898 assert_eq!(d.area_selection(), None, "the row's press stayed the row's");
11899
11900 d.press_mouse(plain);
11901 d.run(0.05);
11902 d.move_mouse(end);
11903 d.run(0.05);
11904 d.release_mouse(end);
11905 d.run(0.05);
11906 assert!(d.area_selection().is_some(), "plain words still select");
11907 }
11908
11909 fn driver() -> Driver<Doc> {
11910 driver_with(Bar::Default)
11911 }
11912
11913 fn driver_with(bar: Bar) -> Driver<Doc> {
11914 driver_on(bar, None)
11915 }
11916
11917 fn driver_on(bar: Bar, platform: Option<frus_widgets::TargetPlatform>) -> Driver<Doc> {
11918 let doc = Doc {
11919 bar,
11920 platform,
11921 ..Doc::default()
11922 };
11923 let mut driver = Driver::new(doc, W, H);
11924 driver.run(0.2);
11925 driver
11926 }
11927
11928 fn on(driver: &Driver<Doc>, label: &str, across: f32) -> Point {
11931 let (_, rect) = driver
11932 .texts()
11933 .into_iter()
11934 .find(|(text, _)| text == label)
11935 .unwrap_or_else(|| panic!("{label:?} is on screen"));
11936 Point::new(rect.x + across, rect.y + 8.0)
11939 }
11940
11941 fn drag(driver: &mut Driver<Doc>, from: Point, to: Point) {
11942 driver.press_mouse(from);
11943 driver.run(0.05);
11944 driver.move_mouse(to);
11945 driver.run(0.05);
11946 driver.release_mouse(to);
11947 driver.run(0.05);
11948 }
11949
11950 #[test]
11954 fn a_drag_selects_across_the_texts_of_an_area() {
11955 let mut d = driver();
11956 let (from, to) = (
11957 on(&d, "first line of words", 60.0),
11958 on(&d, "third line ends here", 40.0),
11959 );
11960 drag(&mut d, from, to);
11961 let copied = d.area_selection().expect("something is selected");
11962 let lines: Vec<&str> = copied.lines().collect();
11963 assert_eq!(lines.len(), 3, "one line per text: {copied:?}");
11964 assert!(
11965 !lines[0].is_empty() && "first line of words".ends_with(lines[0]),
11966 "the tail of the first: {:?}",
11967 lines[0]
11968 );
11969 assert!(lines[0].len() < "first line of words".len());
11970 assert_eq!(lines[1], "second line", "the whole of the middle one");
11971 assert!(
11972 !lines[2].is_empty() && "third line ends here".starts_with(lines[2]),
11973 "the head of the last: {:?}",
11974 lines[2]
11975 );
11976 assert!(lines[2].len() < "third line ends here".len());
11977 assert!(!copied.contains("Press"), "a button's label is not text");
11978 }
11979
11980 #[test]
11982 fn dragging_upward_selects_the_same_words() {
11983 let (down, up) = {
11984 let mut d = driver();
11985 let (a, b) = (
11986 on(&d, "first line of words", 60.0),
11987 on(&d, "third line ends here", 40.0),
11988 );
11989 drag(&mut d, a, b);
11990 let down = d.area_selection();
11991 drag(&mut d, b, a);
11992 (down, d.area_selection())
11993 };
11994 assert!(down.is_some());
11995 assert_eq!(down, up);
11996 }
11997
11998 #[test]
12001 fn a_drag_past_the_words_ends_at_the_last_of_them() {
12002 let mut d = driver();
12003 let from = on(&d, "second line", 5.0);
12004 drag(&mut d, from, Point::new(W - 2.0, H - 2.0));
12005 let copied = d.area_selection().expect("a selection");
12006 assert!(copied.ends_with("third line ends here"), "{copied:?}");
12007 }
12008
12009 #[test]
12012 fn words_outside_the_area_are_left_alone() {
12013 let mut d = driver();
12014 let outside = on(&d, "Outside the area", 30.0);
12015 let end = on(&d, "third line ends here", 40.0);
12016 drag(&mut d, outside, end);
12017 assert_eq!(d.area_selection(), None, "the press began outside");
12018
12019 let inside = on(&d, "first line of words", 30.0);
12020 drag(&mut d, inside, Point::new(20.0, 2.0));
12021 let copied = d.area_selection().expect("a selection");
12022 assert!(!copied.contains("Outside"), "{copied:?}");
12023 }
12024
12025 #[test]
12027 fn a_button_keeps_its_press() {
12028 let mut d = driver();
12029 let button = on(&d, "Press", 10.0);
12030 let end = on(&d, "third line ends here", 40.0);
12031 drag(&mut d, button, end);
12032 assert_eq!(d.area_selection(), None);
12033 }
12034
12035 #[test]
12037 fn a_click_puts_the_selection_away() {
12038 let mut d = driver();
12039 let (from, to) = (
12040 on(&d, "first line of words", 60.0),
12041 on(&d, "third line ends here", 40.0),
12042 );
12043 drag(&mut d, from, to);
12044 assert!(d.area_selection().is_some());
12045 let elsewhere = on(&d, "second line", 30.0);
12046 drag(&mut d, elsewhere, elsewhere);
12047 assert_eq!(d.area_selection(), None, "a click in the area");
12048
12049 drag(&mut d, from, to);
12050 assert!(d.area_selection().is_some());
12051 let outside = on(&d, "Outside the area", 30.0);
12052 drag(&mut d, outside, outside);
12053 assert_eq!(d.area_selection(), None, "a click outside it");
12054 }
12055
12056 #[test]
12058 fn a_double_click_selects_the_word() {
12059 let mut d = driver();
12060 let at = on(&d, "second line", 12.0);
12062 d.press_mouse(at);
12063 d.release_mouse(at);
12064 d.run(0.05);
12065 d.press_mouse(at);
12066 d.release_mouse(at);
12067 d.run(0.05);
12068 assert_eq!(d.area_selection().as_deref(), Some("second"));
12069 }
12070
12071 #[test]
12073 fn select_all_takes_the_whole_area() {
12074 let mut d = driver();
12075 let at = on(&d, "second line", 12.0);
12076 let end = on(&d, "second line", 40.0);
12077 drag(&mut d, at, end);
12078 assert!(d.area_selection().is_some());
12079 d.area_select_all();
12080 assert_eq!(
12081 d.area_selection().as_deref(),
12082 Some("first line of words\nsecond line\nthird line ends here")
12083 );
12084 }
12085
12086 #[test]
12088 fn a_finger_does_not_select() {
12089 let mut d = driver();
12090 let (from, to) = (
12091 on(&d, "first line of words", 60.0),
12092 on(&d, "third line ends here", 40.0),
12093 );
12094 d.press(from);
12095 d.run(0.05);
12096 d.move_to(to);
12097 d.run(0.05);
12098 d.release(to);
12099 d.run(0.05);
12100 assert_eq!(d.area_selection(), None);
12101 }
12102
12103 fn hold(d: &mut Driver<Doc>, at: Point) -> bool {
12105 d.press(at);
12106 d.run(0.1);
12107 let fired = d.hold_deadline();
12108 d.run(0.05);
12109 d.release(at);
12110 d.run(0.05);
12111 fired
12112 }
12113
12114 fn bar_button(d: &Driver<Doc>, label: &str) -> Option<Point> {
12116 let (ui, _) = d.frame_parts()?;
12117 d.texts()
12118 .into_iter()
12119 .filter(|(text, _)| text == label)
12120 .map(|(_, rect)| Point::new(rect.x + 4.0, rect.y + 6.0))
12121 .find(|at| ui.toolbar_contains(*at))
12122 }
12123
12124 #[test]
12127 fn a_hold_selects_the_word_and_opens_the_bar() {
12128 let mut d = driver();
12129 let at = on(&d, "second line", 12.0);
12130 assert!(hold(&mut d, at), "the hold fired");
12131 assert_eq!(d.area_selection().as_deref(), Some("second"));
12132 assert!(bar_button(&d, "Copy").is_some(), "Copy is on the bar");
12133 assert!(bar_button(&d, "Select all").is_some(), "and Select all");
12134 }
12135
12136 #[test]
12139 fn the_desktop_menus_rows_act_on_the_area() {
12140 let mut d = driver_on(Bar::Default, Some(frus_widgets::TargetPlatform::Linux));
12141 let at = on(&d, "second line", 12.0);
12142 hold(&mut d, at);
12143 let select_all = bar_button(&d, "Select all").expect("Select all");
12144 d.press(select_all);
12147 d.release(select_all);
12148 d.run(0.05);
12149 assert_eq!(
12150 d.area_selection().as_deref(),
12151 Some("first line of words\nsecond line\nthird line ends here")
12152 );
12153 }
12154
12155 #[test]
12158 fn the_bars_buttons_act_on_the_area() {
12159 let mut d = driver();
12160 let at = on(&d, "second line", 12.0);
12161 hold(&mut d, at);
12162 let select_all = bar_button(&d, "Select all").expect("Select all");
12163 d.press(select_all);
12164 d.release(select_all);
12165 d.run(0.05);
12166 assert_eq!(
12167 d.area_selection().as_deref(),
12168 Some("first line of words\nsecond line\nthird line ends here")
12169 );
12170 assert!(
12171 bar_button(&d, "Select all").is_none(),
12172 "nothing left to select"
12173 );
12174 let copy = bar_button(&d, "Copy").expect("Copy stays");
12175 d.press(copy);
12176 d.release(copy);
12177 d.run(0.05);
12178 assert_eq!(d.area_selection(), None, "copied, and put away");
12179 assert!(bar_button(&d, "Copy").is_none(), "the bar with it");
12180 }
12181
12182 #[test]
12184 fn a_hold_on_a_button_selects_nothing() {
12185 let mut d = driver();
12186 let button = on(&d, "Press", 10.0);
12187 hold(&mut d, button);
12188 assert_eq!(d.area_selection(), None);
12189 }
12190
12191 #[test]
12193 fn escape_and_a_tap_put_the_bar_away() {
12194 let mut d = driver();
12195 let at = on(&d, "second line", 12.0);
12196 hold(&mut d, at);
12197 assert!(d.area_selection().is_some());
12198 d.escape();
12199 d.run(0.05);
12200 assert_eq!(d.area_selection(), None);
12201 assert!(bar_button(&d, "Copy").is_none());
12202
12203 hold(&mut d, at);
12204 assert!(d.area_selection().is_some());
12205 let elsewhere = on(&d, "Outside the area", 20.0);
12206 d.press(elsewhere);
12207 d.release(elsewhere);
12208 d.run(0.05);
12209 assert_eq!(d.area_selection(), None);
12210 assert!(bar_button(&d, "Copy").is_none());
12211 }
12212
12213 #[test]
12216 fn a_mouse_selection_has_no_bar() {
12217 let mut d = driver();
12218 let (from, to) = (
12219 on(&d, "first line of words", 60.0),
12220 on(&d, "third line ends here", 40.0),
12221 );
12222 drag(&mut d, from, to);
12223 assert!(d.area_selection().is_some());
12224 assert!(bar_button(&d, "Copy").is_none());
12225 }
12226
12227 #[test]
12230 fn the_application_says_what_the_bar_holds() {
12231 let mut d = driver_with(Bar::Share);
12232 let at = on(&d, "second line", 12.0);
12233 assert!(hold(&mut d, at));
12234 assert_eq!(d.area_selection().as_deref(), Some("second"));
12235 assert!(bar_button(&d, "Copy").is_some(), "Copy was kept");
12236 assert!(
12237 bar_button(&d, "Select all").is_none(),
12238 "Select all was taken off"
12239 );
12240 let share = bar_button(&d, "Share").expect("the application's own item");
12241 let shared = d.app().shared.clone();
12242 d.press(share);
12243 d.release(share);
12244 d.run(0.05);
12245 assert_eq!(shared.get(), 1, "its message was sent");
12246 }
12247
12248 #[test]
12250 fn an_empty_list_shows_no_bar() {
12251 let mut d = driver_with(Bar::Empty);
12252 let at = on(&d, "second line", 12.0);
12253 assert!(hold(&mut d, at));
12254 assert_eq!(d.area_selection().as_deref(), Some("second"));
12255 assert!(bar_button(&d, "Copy").is_none());
12256 assert!(d
12257 .frame_parts()
12258 .is_some_and(|(ui, _)| !ui.toolbar_contains(at)));
12259 }
12260
12261 fn middle(rect: frus_widgets::Rect) -> Point {
12263 Point::new(rect.x + rect.width / 2.0, rect.y + rect.height / 2.0)
12264 }
12265
12266 fn carry_handle(d: &mut Driver<Doc>, from: Point, to: Point) {
12268 d.press(from);
12269 d.run(0.05);
12270 for step in 1..=4 {
12271 let f = step as f32 / 4.0;
12272 d.move_to(Point::new(
12273 from.x + (to.x - from.x) * f,
12274 from.y + (to.y - from.y) * f,
12275 ));
12276 d.run(0.05);
12277 }
12278 d.release(to);
12279 d.run(0.05);
12280 }
12281
12282 #[test]
12285 fn a_touch_selection_has_handles_and_a_mouse_one_does_not() {
12286 let mut d = driver();
12287 let paths = |d: &Driver<Doc>| {
12288 let (ui, _) = d.frame_parts().expect("a frame");
12289 ui.scene()
12290 .primitives()
12291 .iter()
12292 .filter(|p| matches!(p, frus_widgets::Primitive::Path { .. }))
12293 .count()
12294 };
12295 let before = paths(&d);
12296 let at = on(&d, "second line", 12.0);
12297 hold(&mut d, at);
12298 assert_eq!(paths(&d), before + 2, "the two handles are painted");
12299 let [start, end] = d.area_handles().expect("two handles");
12300 assert!(start.line_center.x < end.line_center.x, "{start:?} {end:?}");
12301 assert!(start.rect.y > at.y - 8.0, "they hang below the line");
12302
12303 let (from, to) = (
12304 on(&d, "first line of words", 60.0),
12305 on(&d, "third line ends here", 40.0),
12306 );
12307 drag(&mut d, from, to);
12308 assert!(d.area_selection().is_some());
12309 assert!(
12310 d.area_handles().is_none(),
12311 "a mouse selection has no handles"
12312 );
12313 d.area_select_all();
12314 assert!(
12315 d.area_handles().is_none(),
12316 "nor does it grow any when the keyboard selects all"
12317 );
12318 }
12319
12320 #[test]
12324 fn the_end_handle_widens_the_selection_across_texts() {
12325 let mut d = driver();
12326 let at = on(&d, "second line", 12.0);
12327 hold(&mut d, at);
12328 let [_, end] = d.area_handles().expect("handles");
12329 let third = on(&d, "third line ends here", 60.0);
12330 let from = middle(end.rect);
12331 let to = Point::new(third.x, from.y + (third.y - at.y));
12333 d.press(from);
12334 d.run(0.05);
12335 d.move_to(Point::new(from.x, (from.y + to.y) / 2.0));
12336 d.run(0.05);
12337 d.move_to(to);
12338 d.run(0.05);
12339 assert!(
12340 bar_button(&d, "Copy").is_none(),
12341 "no bar while a handle moves"
12342 );
12343 d.release(to);
12344 d.run(0.05);
12345 let copied = d.area_selection().expect("a selection");
12346 let lines: Vec<&str> = copied.lines().collect();
12347 assert_eq!(lines.len(), 2, "{copied:?}");
12348 assert_eq!(lines[0], "second line", "the rest of the word's line");
12349 assert!("third line ends here".starts_with(lines[1]), "{copied:?}");
12350 assert!(!lines[1].is_empty());
12351 assert!(bar_button(&d, "Copy").is_some(), "the bar is back");
12352 }
12353
12354 #[test]
12357 fn the_start_handle_widens_it_upward() {
12358 let mut d = driver();
12359 let at = on(&d, "second line", 80.0);
12360 hold(&mut d, at);
12361 assert_eq!(d.area_selection().as_deref(), Some("line"));
12362 let [start, _] = d.area_handles().expect("handles");
12363 let first = on(&d, "first line of words", 30.0);
12364 let from = middle(start.rect);
12365 let to = Point::new(first.x, from.y + (first.y - at.y));
12366 carry_handle(&mut d, from, to);
12367 let copied = d.area_selection().expect("a selection");
12368 let lines: Vec<&str> = copied.lines().collect();
12369 assert_eq!(lines.len(), 2, "{copied:?}");
12370 assert!("first line of words".ends_with(lines[0]), "{copied:?}");
12371 assert_eq!(lines[1], "second line");
12372 }
12373
12374 #[test]
12377 fn a_handle_does_not_cross_the_other() {
12378 let mut d = driver();
12379 let at = on(&d, "second line", 80.0);
12380 hold(&mut d, at);
12381 let before = d.area_selection();
12382 assert!(before.is_some());
12383 let [_, end] = d.area_handles().expect("handles");
12384 let first = on(&d, "first line of words", 5.0);
12385 let from = middle(end.rect);
12386 let to = Point::new(first.x, from.y + (first.y - at.y));
12388 d.press(from);
12389 d.run(0.05);
12390 d.move_to(to);
12391 d.run(0.05);
12392 d.release(to);
12393 d.run(0.05);
12394 assert_eq!(d.area_selection(), before);
12395 }
12396
12397 #[test]
12400 fn the_selection_is_highlighted_in_each_text() {
12401 let mut d = driver();
12402 let (from, to) = (
12403 on(&d, "first line of words", 60.0),
12404 on(&d, "third line ends here", 40.0),
12405 );
12406 let bare = rects(&d);
12407 drag(&mut d, from, to);
12408 let boxes: Vec<frus_widgets::Rect> = rects(&d)
12410 .into_iter()
12411 .filter(|rect| !bare.contains(rect))
12412 .collect();
12413 assert_eq!(boxes.len(), 3, "one per text covered: {boxes:?}");
12414 let outside = on(&d, "Outside the area", 0.0);
12415 assert!(
12416 boxes.iter().all(|b| b.y > outside.y + 10.0),
12417 "none over the line outside the area: {boxes:?}"
12418 );
12419 }
12420
12421 fn rects(d: &Driver<Doc>) -> Vec<frus_widgets::Rect> {
12422 let (ui, _) = d.frame_parts().expect("a frame");
12423 ui.scene()
12424 .primitives()
12425 .iter()
12426 .filter_map(|p| match p {
12427 frus_widgets::Primitive::Rect { rect, .. } => Some(*rect),
12428 _ => None,
12429 })
12430 .collect()
12431 }
12432}
12433
12434#[cfg(test)]
12437mod gesture_detector_tests {
12438 use super::testing::Driver;
12439 use crate::{Application, Command};
12440 use frus_widgets::{Container, Flex, GestureDetector, Point, Text, Theme, Widget};
12441 use std::cell::RefCell;
12442 use std::rc::Rc;
12443
12444 const W: f32 = 400.0;
12445 const H: f32 = 400.0;
12446
12447 #[derive(Clone, Debug, PartialEq)]
12448 enum Msg {
12449 Tap,
12450 Double,
12451 Hold,
12452 Menu,
12453 Plain,
12454 OnlyDouble,
12455 }
12456
12457 #[derive(Default)]
12460 struct Page {
12461 heard: Rc<RefCell<Vec<Msg>>>,
12462 }
12463
12464 impl Application for Page {
12465 type Message = Msg;
12466
12467 fn update(&mut self, message: Msg) -> Command<Msg> {
12468 self.heard.borrow_mut().push(message);
12469 Command::none()
12470 }
12471
12472 fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
12473 let block = |label: &str| {
12474 Container::new()
12475 .width(300.0)
12476 .height(60.0)
12477 .child(Text::new(label).no_wrap())
12478 };
12479 Box::new(
12480 Container::new().width(W).height(H).child(
12481 Flex::column()
12482 .gap(40.0)
12483 .child(
12484 GestureDetector::new(block("everything"))
12485 .on_tap(Msg::Tap)
12486 .on_double_tap(Msg::Double)
12487 .on_long_press(Msg::Hold)
12488 .on_secondary_tap(Msg::Menu),
12489 )
12490 .child(GestureDetector::new(block("tap only")).on_tap(Msg::Plain))
12491 .child(
12492 GestureDetector::new(block("double only"))
12493 .on_double_tap(Msg::OnlyDouble),
12494 ),
12495 ),
12496 )
12497 }
12498 }
12499
12500 fn driver() -> (Driver<Page>, Rc<RefCell<Vec<Msg>>>) {
12501 let page = Page::default();
12502 let heard = page.heard.clone();
12503 let mut driver = Driver::new(page, W, H);
12504 driver.run(0.2);
12505 (driver, heard)
12506 }
12507
12508 fn on(d: &Driver<Page>, label: &str) -> Point {
12510 let (_, rect) = d
12511 .texts()
12512 .into_iter()
12513 .find(|(text, _)| text == label)
12514 .unwrap_or_else(|| panic!("{label:?} is on screen"));
12515 Point::new(rect.x + 20.0, rect.y + 8.0)
12516 }
12517
12518 fn tap(d: &mut Driver<Page>, at: Point) {
12519 d.press(at);
12520 d.run(0.02);
12521 d.release(at);
12522 d.run(0.02);
12523 }
12524
12525 #[test]
12528 fn a_tap_waits_while_a_double_tap_is_possible() {
12529 let (mut d, heard) = driver();
12530 let at = on(&d, "everything");
12531 tap(&mut d, at);
12532 assert!(heard.borrow().is_empty(), "not yet: {:?}", heard.borrow());
12533 assert!(d.tap_deadline(), "a tap was waiting");
12534 assert_eq!(*heard.borrow(), vec![Msg::Tap]);
12535 }
12536
12537 #[test]
12540 fn the_loop_wakes_for_a_waiting_tap() {
12541 let (mut d, _) = driver();
12542 let before = std::time::Instant::now();
12543 let at = on(&d, "everything");
12544 tap(&mut d, at);
12545 let wake = d.next_wake().expect("a wake for the tap");
12546 let wait = wake.duration_since(before);
12547 assert!(
12548 wait <= std::time::Duration::from_millis(400),
12549 "about 300 ms: {wait:?}"
12550 );
12551 d.tap_deadline();
12552 assert_eq!(d.next_wake(), None);
12553 }
12554
12555 #[test]
12557 fn two_quick_taps_are_a_double_tap() {
12558 let (mut d, heard) = driver();
12559 let at = on(&d, "everything");
12560 tap(&mut d, at);
12561 tap(&mut d, Point::new(at.x + 6.0, at.y + 2.0));
12562 assert_eq!(*heard.borrow(), vec![Msg::Double]);
12563 assert!(!d.tap_deadline(), "nothing is left waiting");
12564 assert_eq!(*heard.borrow(), vec![Msg::Double]);
12565 }
12566
12567 #[test]
12569 fn a_tap_alone_answers_at_once() {
12570 let (mut d, heard) = driver();
12571 let at = on(&d, "tap only");
12572 tap(&mut d, at);
12573 assert_eq!(*heard.borrow(), vec![Msg::Plain]);
12574 tap(&mut d, at);
12575 assert_eq!(
12576 *heard.borrow(),
12577 vec![Msg::Plain, Msg::Plain],
12578 "two taps are two taps"
12579 );
12580 }
12581
12582 #[test]
12585 fn a_press_elsewhere_sends_the_waiting_tap() {
12586 let (mut d, heard) = driver();
12587 let (first, other) = (on(&d, "everything"), on(&d, "tap only"));
12588 tap(&mut d, first);
12589 tap(&mut d, other);
12590 assert_eq!(*heard.borrow(), vec![Msg::Tap, Msg::Plain]);
12591 }
12592
12593 #[test]
12595 fn a_secondary_click_sends_its_message() {
12596 let (mut d, heard) = driver();
12597 let at = on(&d, "everything");
12598 d.secondary_click(at);
12599 d.run(0.02);
12600 assert_eq!(*heard.borrow(), vec![Msg::Menu]);
12601 let plain = on(&d, "tap only");
12602 d.secondary_click(plain);
12603 assert_eq!(
12604 *heard.borrow(),
12605 vec![Msg::Menu],
12606 "a detector without one hears nothing"
12607 );
12608 }
12609
12610 #[test]
12612 fn a_long_press_is_not_also_a_tap() {
12613 let (mut d, heard) = driver();
12614 let at = on(&d, "everything");
12615 d.press(at);
12616 d.run(0.1);
12617 assert!(d.hold_deadline(), "the hold fired");
12618 d.release(at);
12619 d.run(0.02);
12620 d.tap_deadline();
12621 assert_eq!(*heard.borrow(), vec![Msg::Hold]);
12622 }
12623
12624 #[test]
12627 fn a_double_tap_alone_is_heard() {
12628 let (mut d, heard) = driver();
12629 let at = on(&d, "double only");
12630 tap(&mut d, at);
12631 d.tap_deadline();
12632 assert!(heard.borrow().is_empty(), "{:?}", heard.borrow());
12633 tap(&mut d, at);
12634 tap(&mut d, at);
12635 assert_eq!(*heard.borrow(), vec![Msg::OnlyDouble]);
12636 }
12637}
12638
12639#[cfg(test)]
12642mod gesture_pan_tests {
12643 use super::testing::Driver;
12644 use crate::{Application, Command};
12645 use frus_widgets::{
12646 Container, Flex, GestureDetector, PanAxis, Point, SingleChildScrollView, Text, Theme,
12647 Widget,
12648 };
12649 use std::cell::RefCell;
12650 use std::rc::Rc;
12651
12652 const W: f32 = 400.0;
12653 const H: f32 = 400.0;
12654
12655 #[derive(Clone, Debug, PartialEq)]
12656 enum Msg {
12657 Start(Point),
12658 Update(Point, Point),
12659 End(Point),
12660 Tap,
12661 Hold,
12662 }
12663
12664 struct Page {
12666 axis: PanAxis,
12667 scrolled: bool,
12668 heard: Rc<RefCell<Vec<Msg>>>,
12669 }
12670
12671 impl Application for Page {
12672 type Message = Msg;
12673
12674 fn update(&mut self, message: Msg) -> Command<Msg> {
12675 self.heard.borrow_mut().push(message);
12676 Command::none()
12677 }
12678
12679 fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
12680 let detector = GestureDetector::new(
12681 Container::new()
12682 .width(300.0)
12683 .height(150.0)
12684 .child(Text::new("drag me").no_wrap()),
12685 )
12686 .on_tap(Msg::Tap)
12687 .on_long_press(Msg::Hold)
12688 .on_pan_start(Msg::Start)
12689 .on_pan_update(Msg::Update)
12690 .on_pan_end(Msg::End)
12691 .pan_axis(self.axis);
12692 let content = Flex::column()
12693 .child(Container::new().width(300.0).height(30.0))
12694 .child(detector)
12695 .child(Container::new().width(300.0).height(1200.0));
12696 if self.scrolled {
12697 Box::new(
12698 SingleChildScrollView::new()
12699 .width(W)
12700 .height(H)
12701 .child(content),
12702 )
12703 } else {
12704 Box::new(Container::new().width(W).height(H).child(content))
12705 }
12706 }
12707 }
12708
12709 fn driver(axis: PanAxis, scrolled: bool) -> (Driver<Page>, Rc<RefCell<Vec<Msg>>>) {
12710 let heard = Rc::new(RefCell::new(Vec::new()));
12711 let page = Page {
12712 axis,
12713 scrolled,
12714 heard: heard.clone(),
12715 };
12716 let mut driver = Driver::new(page, W, H);
12717 driver.run(0.2);
12718 (driver, heard)
12719 }
12720
12721 fn origin(d: &Driver<Page>) -> Point {
12723 let (_, rect) = d
12724 .texts()
12725 .into_iter()
12726 .find(|(text, _)| text == "drag me")
12727 .expect("on screen");
12728 Point::new(rect.x, rect.y)
12729 }
12730
12731 fn swipe(d: &mut Driver<Page>, from: Point, dx: f32, dy: f32) {
12733 d.press(from);
12734 d.run(0.02);
12735 for step in 1..=8 {
12736 let f = step as f32 / 8.0;
12737 d.move_to(Point::new(from.x + dx * f, from.y + dy * f));
12738 d.run(0.02);
12739 }
12740 d.release(Point::new(from.x + dx, from.y + dy));
12741 d.run(0.02);
12742 }
12743
12744 fn deltas(heard: &[Msg]) -> Point {
12745 heard.iter().fold(Point::new(0.0, 0.0), |sum, m| match m {
12746 Msg::Update(_, delta) => Point::new(sum.x + delta.x, sum.y + delta.y),
12747 _ => sum,
12748 })
12749 }
12750
12751 fn scroll_y(d: &Driver<Page>) -> f32 {
12752 d.offsets().first().map_or(0.0, |(_, offset)| offset.1)
12753 }
12754
12755 #[test]
12759 fn a_drag_is_a_start_updates_and_an_end() {
12760 let (mut d, heard) = driver(PanAxis::Free, false);
12761 let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 40.0);
12762 swipe(&mut d, at, 120.0, 60.0);
12763 let heard = heard.borrow();
12764 assert!(matches!(heard.first(), Some(Msg::Start(_))), "{heard:?}");
12765 if let Some(Msg::Start(local)) = heard.first() {
12766 let corner = origin(&d);
12768 assert!(corner.y >= 30.0, "{corner:?}");
12769 assert_eq!(*local, Point::new(at.x - corner.x, at.y - corner.y));
12770 }
12771 assert!(matches!(heard.last(), Some(Msg::End(_))), "{heard:?}");
12772 if let Some(Msg::End(velocity)) = heard.last() {
12773 assert!(velocity.x.hypot(velocity.y) <= 8000.0 + 0.5, "{velocity:?}");
12774 }
12775 let moved = deltas(&heard);
12776 assert!(moved.x > 60.0 && moved.y > 20.0, "{moved:?}");
12777 assert!(!heard.contains(&Msg::Tap), "a drag is not a tap");
12778 let last = heard.iter().rev().find_map(|m| match m {
12780 Msg::Update(local, _) => Some(*local),
12781 _ => None,
12782 });
12783 let corner = origin(&d);
12784 assert_eq!(
12785 last,
12786 Some(Point::new(at.x + 120.0 - corner.x, at.y + 60.0 - corner.y))
12787 );
12788 }
12789
12790 #[test]
12792 fn a_press_that_does_not_move_is_a_tap() {
12793 let (mut d, heard) = driver(PanAxis::Free, false);
12794 let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 40.0);
12795 swipe(&mut d, at, 3.0, 2.0);
12796 assert_eq!(*heard.borrow(), vec![Msg::Tap]);
12797 }
12798
12799 #[test]
12802 fn a_hold_on_a_dragging_detector_is_a_long_press() {
12803 let (mut d, heard) = driver(PanAxis::Free, false);
12804 let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 40.0);
12805 d.press(at);
12806 d.run(0.1);
12807 assert!(d.hold_deadline(), "the hold fired");
12808 d.release(at);
12809 d.run(0.02);
12810 assert_eq!(*heard.borrow(), vec![Msg::Hold]);
12811 }
12812
12813 #[test]
12815 fn a_mouse_drags_it() {
12816 let (mut d, heard) = driver(PanAxis::Free, false);
12817 let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 40.0);
12818 d.press_mouse(at);
12819 d.run(0.02);
12820 d.move_mouse(Point::new(at.x + 30.0, at.y));
12821 d.run(0.02);
12822 d.release_mouse(Point::new(at.x + 30.0, at.y));
12823 d.run(0.02);
12824 let heard = heard.borrow();
12825 assert!(matches!(heard.first(), Some(Msg::Start(_))), "{heard:?}");
12826 assert!(matches!(heard.last(), Some(Msg::End(_))), "{heard:?}");
12827 }
12828
12829 #[test]
12832 fn a_horizontal_drag_shares_a_vertical_scroll() {
12833 let (mut d, heard) = driver(PanAxis::Horizontal, true);
12834 let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 40.0);
12835 swipe(&mut d, at, 120.0, 10.0);
12836 assert!(
12837 matches!(heard.borrow().first(), Some(Msg::Start(_))),
12838 "{:?}",
12839 heard.borrow()
12840 );
12841 assert_eq!(deltas(&heard.borrow()).y, 0.0, "only across");
12842 assert_eq!(scroll_y(&d), 0.0, "the page did not move");
12843
12844 heard.borrow_mut().clear();
12845 let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 120.0);
12846 swipe(&mut d, at, 5.0, -120.0);
12847 assert!(heard.borrow().is_empty(), "{:?}", heard.borrow());
12848 assert!(scroll_y(&d) > 30.0, "the page scrolled: {}", scroll_y(&d));
12849 }
12850
12851 #[test]
12854 fn a_free_drag_leaves_the_scroll_its_own_direction() {
12855 let (mut d, heard) = driver(PanAxis::Free, true);
12856 let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 40.0);
12857 swipe(&mut d, at, 150.0, 8.0);
12858 assert!(
12859 matches!(heard.borrow().first(), Some(Msg::Start(_))),
12860 "{:?}",
12861 heard.borrow()
12862 );
12863 assert_eq!(scroll_y(&d), 0.0);
12864
12865 heard.borrow_mut().clear();
12866 let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 120.0);
12867 swipe(&mut d, at, 4.0, -120.0);
12868 assert!(heard.borrow().is_empty(), "{:?}", heard.borrow());
12869 assert!(scroll_y(&d) > 30.0, "the page scrolled: {}", scroll_y(&d));
12870 }
12871
12872 #[test]
12878 fn a_stall_mid_drag_does_not_change_the_scroll() {
12879 let (mut d, heard) = driver(PanAxis::Free, true);
12880 let from = Point::new(origin(&d).x + 40.0, origin(&d).y + 120.0);
12881 d.press(from);
12882 d.run(0.02);
12883 for step in 1..=8 {
12884 let f = step as f32 / 8.0;
12885 d.move_to(Point::new(from.x + 4.0 * f, from.y - 120.0 * f));
12886 if step == 5 {
12887 std::thread::sleep(std::time::Duration::from_millis(20));
12888 }
12889 d.run(0.02);
12890 }
12891 d.release(Point::new(from.x + 4.0, from.y - 120.0));
12892 d.run(0.02);
12893 assert!(heard.borrow().is_empty(), "{:?}", heard.borrow());
12894 assert!(scroll_y(&d) > 30.0, "the page scrolled: {}", scroll_y(&d));
12895 }
12896
12897 #[test]
12900 fn a_vertical_drag_takes_it_from_the_scroll() {
12901 let (mut d, heard) = driver(PanAxis::Vertical, true);
12902 let at = Point::new(origin(&d).x + 40.0, origin(&d).y + 120.0);
12903 swipe(&mut d, at, 6.0, -100.0);
12904 let heard = heard.borrow();
12905 assert!(matches!(heard.first(), Some(Msg::Start(_))), "{heard:?}");
12906 let moved = deltas(&heard);
12907 assert_eq!(moved.x, 0.0, "only down");
12908 assert!(moved.y < -40.0, "{moved:?}");
12909 assert_eq!(scroll_y(&d), 0.0, "the page did not move");
12910 }
12911}
12912
12913#[cfg(test)]
12916mod mouse_region_tests {
12917 use super::testing::Driver;
12918 use crate::{Application, Command};
12919 use frus_widgets::{
12920 Container, Cursor, Flex, MouseRegion, Point, Positioned, Stack, TextField, Theme, Widget,
12921 };
12922 use std::cell::RefCell;
12923 use std::rc::Rc;
12924
12925 const W: f32 = 400.0;
12926 const H: f32 = 600.0;
12927
12928 #[derive(Clone, Debug, PartialEq)]
12929 enum Msg {
12930 Enter(&'static str, Point),
12931 Move(&'static str),
12932 Exit(&'static str),
12933 Show,
12934 }
12935
12936 struct Page {
12940 front_opaque: bool,
12941 shown: bool,
12942 heard: Rc<RefCell<Vec<Msg>>>,
12943 }
12944
12945 fn region(name: &'static str, child: impl Widget<Msg> + 'static) -> MouseRegion<Msg> {
12946 MouseRegion::new(child)
12947 .on_enter(move |at| Msg::Enter(name, at))
12948 .on_hover(move |_| Msg::Move(name))
12949 .on_exit(move |_| Msg::Exit(name))
12950 }
12951
12952 impl Application for Page {
12953 type Message = Msg;
12954
12955 fn update(&mut self, message: Msg) -> Command<Msg> {
12956 if message == Msg::Show {
12957 self.shown = true;
12958 } else {
12959 self.heard.borrow_mut().push(message);
12960 }
12961 Command::none()
12962 }
12963
12964 fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
12965 let nested = region(
12967 "outer",
12968 Container::new().width(300.0).height(200.0).child(
12969 region("inner", Container::new().width(100.0).height(50.0))
12970 .cursor(Cursor::Pointer),
12971 ),
12972 )
12973 .cursor(Cursor::Grab);
12974 let stack = Stack::new()
12976 .width(300.0)
12977 .height(200.0)
12978 .layer(region("back", Container::new().width(300.0).height(200.0)))
12979 .layer(
12980 Positioned::new(
12981 region("front", Container::new().width(100.0).height(100.0))
12982 .opaque(self.front_opaque),
12983 )
12984 .left(50.0)
12985 .top(50.0),
12986 );
12987 let field = region("field", TextField::new("").width(300.0)).cursor(Cursor::Pointer);
12989 let appearing: Box<dyn Widget<Msg>> = if self.shown {
12991 Box::new(region("late", Container::new().width(300.0).height(100.0)))
12992 } else {
12993 Box::new(Container::new().width(300.0).height(100.0))
12994 };
12995 Box::new(
12996 Container::new().width(W).height(H).child(
12997 Flex::column()
12998 .child(nested)
12999 .child(stack)
13000 .child(Container::new().height(100.0).child(field))
13001 .child(appearing),
13002 ),
13003 )
13004 }
13005 }
13006
13007 fn driver(front_opaque: bool) -> (Driver<Page>, Rc<RefCell<Vec<Msg>>>) {
13008 let heard = Rc::new(RefCell::new(Vec::new()));
13009 let page = Page {
13010 front_opaque,
13011 shown: false,
13012 heard: heard.clone(),
13013 };
13014 let mut driver = Driver::new(page, W, H);
13015 driver.run(0.2);
13016 (driver, heard)
13017 }
13018
13019 fn take(heard: &Rc<RefCell<Vec<Msg>>>) -> Vec<Msg> {
13020 std::mem::take(&mut *heard.borrow_mut())
13021 }
13022
13023 fn mouse(d: &mut Driver<Page>, x: f32, y: f32) {
13024 d.move_mouse(Point::new(x, y));
13025 d.run(0.02);
13026 }
13027
13028 #[test]
13032 fn nested_regions_are_entered_and_left_together() {
13033 let (mut d, heard) = driver(true);
13034 mouse(&mut d, 20.0, 10.0);
13035 assert_eq!(
13036 take(&heard),
13037 vec![
13038 Msg::Enter("outer", Point::new(20.0, 10.0)),
13039 Msg::Enter("inner", Point::new(20.0, 10.0)),
13040 ]
13041 );
13042 mouse(&mut d, 30.0, 12.0);
13043 let moved = take(&heard);
13044 assert!(moved.contains(&Msg::Move("inner")) && moved.contains(&Msg::Move("outer")));
13045 mouse(&mut d, 200.0, 150.0);
13046 assert_eq!(take(&heard), vec![Msg::Exit("inner"), Msg::Move("outer")]);
13047 mouse(&mut d, 350.0, 150.0);
13048 assert_eq!(take(&heard), vec![Msg::Exit("outer")]);
13049 }
13050
13051 #[test]
13054 fn an_opaque_region_hides_the_one_behind() {
13055 let (mut d, heard) = driver(true);
13056 mouse(&mut d, 80.0, 280.0);
13057 let entered: Vec<_> = take(&heard);
13058 assert!(
13059 entered.iter().any(|m| matches!(m, Msg::Enter("front", _))),
13060 "{entered:?}"
13061 );
13062 assert!(
13063 !entered.iter().any(|m| matches!(m, Msg::Enter("back", _))),
13064 "{entered:?}"
13065 );
13066 mouse(&mut d, 250.0, 280.0);
13068 let after = take(&heard);
13069 assert!(after.contains(&Msg::Exit("front")), "{after:?}");
13070 assert!(
13071 after.iter().any(|m| matches!(m, Msg::Enter("back", _))),
13072 "{after:?}"
13073 );
13074
13075 let (mut d, heard) = driver(false);
13076 mouse(&mut d, 80.0, 280.0);
13077 let entered = take(&heard);
13078 assert!(
13079 entered.iter().any(|m| matches!(m, Msg::Enter("front", _))),
13080 "{entered:?}"
13081 );
13082 assert!(
13083 entered.iter().any(|m| matches!(m, Msg::Enter("back", _))),
13084 "{entered:?}"
13085 );
13086 }
13087
13088 #[test]
13091 fn no_moves_while_a_button_is_held() {
13092 let (mut d, heard) = driver(true);
13093 mouse(&mut d, 350.0, 150.0);
13094 take(&heard);
13095 d.press_mouse(Point::new(350.0, 150.0));
13096 d.run(0.02);
13097 d.move_mouse(Point::new(40.0, 20.0));
13098 d.run(0.02);
13099 d.move_mouse(Point::new(45.0, 22.0));
13100 d.run(0.02);
13101 let heard_now = take(&heard);
13102 assert!(
13103 heard_now
13104 .iter()
13105 .any(|m| matches!(m, Msg::Enter("inner", _))),
13106 "{heard_now:?}"
13107 );
13108 assert!(
13109 !heard_now.iter().any(|m| matches!(m, Msg::Move(_))),
13110 "{heard_now:?}"
13111 );
13112 d.release_mouse(Point::new(45.0, 22.0));
13113 d.run(0.02);
13114 }
13115
13116 #[test]
13118 fn a_finger_enters_while_it_touches() {
13119 let (mut d, heard) = driver(true);
13120 d.press(Point::new(200.0, 150.0));
13121 d.run(0.02);
13122 assert_eq!(
13123 take(&heard),
13124 vec![Msg::Enter("outer", Point::new(200.0, 150.0))]
13125 );
13126 d.release(Point::new(200.0, 150.0));
13127 d.run(0.02);
13128 assert_eq!(take(&heard), vec![Msg::Exit("outer")]);
13129 }
13130
13131 #[test]
13134 fn a_region_appearing_under_the_pointer_is_entered() {
13135 let (mut d, heard) = driver(true);
13136 mouse(&mut d, 150.0, 550.0);
13137 assert!(take(&heard).is_empty());
13138 d.update(Msg::Show);
13139 d.run(0.05);
13140 let heard_now = take(&heard);
13141 assert!(
13142 heard_now.iter().any(|m| matches!(m, Msg::Enter("late", _))),
13143 "{heard_now:?}"
13144 );
13145 }
13146
13147 #[test]
13150 fn the_innermost_region_sets_the_cursor() {
13151 let (mut d, _) = driver(true);
13152 mouse(&mut d, 20.0, 10.0);
13153 assert_eq!(d.cursor_shown(), Cursor::Pointer, "the inner region's");
13154 mouse(&mut d, 200.0, 150.0);
13155 assert_eq!(d.cursor_shown(), Cursor::Grab, "the outer region's");
13156 mouse(&mut d, 350.0, 150.0);
13157 assert_eq!(d.cursor_shown(), Cursor::Default, "no region");
13158 mouse(&mut d, 100.0, 420.0);
13159 assert_eq!(d.cursor_shown(), Cursor::Text, "the field's own");
13160 }
13161}
13162
13163#[cfg(test)]
13166mod listener_tests {
13167 use super::testing::Driver;
13168 use crate::{Application, Command};
13169 use frus_widgets::{
13170 Button, Container, Flex, Listener, ListenerEvent, ListenerEventKind as Raw, Point,
13171 SingleChildScrollView, Theme, Widget,
13172 };
13173 use std::cell::RefCell;
13174 use std::rc::Rc;
13175
13176 const W: f32 = 400.0;
13177 const H: f32 = 400.0;
13178
13179 #[derive(Clone, Debug, PartialEq)]
13180 enum Msg {
13181 Heard(&'static str, Raw, Point),
13182 Pressed,
13183 }
13184
13185 struct Page {
13188 heard: Rc<RefCell<Vec<Msg>>>,
13189 }
13190
13191 fn listen(name: &'static str) -> impl Fn(ListenerEvent) -> Option<Msg> {
13192 move |event| Some(Msg::Heard(name, event.kind, event.local))
13193 }
13194
13195 impl Application for Page {
13196 type Message = Msg;
13197
13198 fn update(&mut self, message: Msg) -> Command<Msg> {
13199 self.heard.borrow_mut().push(message);
13200 Command::none()
13201 }
13202
13203 fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
13204 let inner = Listener::new(
13205 Container::new()
13206 .width(100.0)
13207 .height(100.0)
13208 .child(Button::new("Press").on_press(Msg::Pressed)),
13209 listen("inner"),
13210 );
13211 let outer = Listener::new(
13212 Container::new().width(300.0).height(300.0).child(inner),
13213 listen("outer"),
13214 );
13215 Box::new(
13216 SingleChildScrollView::new().width(W).height(H).child(
13217 Flex::column()
13218 .child(Container::new().width(300.0).height(40.0))
13219 .child(outer)
13220 .child(Container::new().width(300.0).height(1000.0)),
13221 ),
13222 )
13223 }
13224 }
13225
13226 fn driver() -> (Driver<Page>, Rc<RefCell<Vec<Msg>>>) {
13227 let heard = Rc::new(RefCell::new(Vec::new()));
13228 let mut d = Driver::new(
13229 Page {
13230 heard: heard.clone(),
13231 },
13232 W,
13233 H,
13234 );
13235 d.run(0.2);
13236 (d, heard)
13237 }
13238
13239 fn take(heard: &Rc<RefCell<Vec<Msg>>>) -> Vec<Msg> {
13240 std::mem::take(&mut *heard.borrow_mut())
13241 }
13242
13243 fn of(heard: &[Msg], name: &str) -> Vec<(Raw, Point)> {
13245 heard
13246 .iter()
13247 .filter_map(|m| match m {
13248 Msg::Heard(n, kind, at) if *n == name => Some((*kind, *at)),
13249 _ => None,
13250 })
13251 .collect()
13252 }
13253
13254 fn scroll_y(d: &Driver<Page>) -> f32 {
13255 d.offsets().first().map_or(0.0, |(_, offset)| offset.1)
13256 }
13257
13258 #[test]
13261 fn a_press_is_heard_innermost_first() {
13262 let (mut d, heard) = driver();
13263 d.press(Point::new(250.0, 90.0));
13264 d.run(0.02);
13265 let h = take(&heard);
13266 assert_eq!(of(&h, "outer"), vec![(Raw::Down, Point::new(250.0, 50.0))]);
13267 assert!(of(&h, "inner").is_empty(), "outside the inner one: {h:?}");
13268 d.release(Point::new(250.0, 90.0));
13269 d.run(0.02);
13270 take(&heard);
13271
13272 d.press(Point::new(60.0, 80.0));
13273 d.run(0.02);
13274 let h = take(&heard);
13275 let order: Vec<_> = h
13276 .iter()
13277 .filter_map(|m| match m {
13278 Msg::Heard(n, Raw::Down, _) => Some(*n),
13279 _ => None,
13280 })
13281 .collect();
13282 assert_eq!(order, vec!["inner", "outer"], "{h:?}");
13283 assert_eq!(of(&h, "inner"), vec![(Raw::Down, Point::new(60.0, 40.0))]);
13284 d.release(Point::new(60.0, 80.0));
13285 d.run(0.02);
13286 }
13287
13288 #[test]
13291 fn the_pointer_is_followed_out_and_the_page_still_scrolls() {
13292 let (mut d, heard) = driver();
13293 let from = Point::new(250.0, 300.0);
13294 d.press(from);
13295 d.run(0.02);
13296 for step in 1..=8 {
13297 d.move_to(Point::new(250.0, 300.0 - 35.0 * step as f32));
13298 d.run(0.02);
13299 }
13300 d.release(Point::new(250.0, 20.0));
13301 d.run(0.02);
13302 let h = take(&heard);
13303 let outer = of(&h, "outer");
13304 assert_eq!(outer.first().map(|e| e.0), Some(Raw::Down), "{outer:?}");
13305 assert_eq!(outer.last().map(|e| e.0), Some(Raw::Up), "{outer:?}");
13306 assert!(
13307 outer.iter().filter(|e| e.0 == Raw::Move).count() >= 7,
13308 "{outer:?}"
13309 );
13310 assert!(scroll_y(&d) > 50.0, "the page scrolled: {}", scroll_y(&d));
13311 }
13312
13313 #[test]
13316 fn a_pointer_that_leaves_is_still_heard() {
13317 let (mut d, heard) = driver();
13318 d.press_mouse(Point::new(60.0, 80.0));
13319 d.run(0.02);
13320 d.move_mouse(Point::new(390.0, 390.0));
13321 d.run(0.02);
13322 d.release_mouse(Point::new(390.0, 390.0));
13323 d.run(0.02);
13324 let h = take(&heard);
13325 let inner = of(&h, "inner");
13326 assert_eq!(
13327 inner,
13328 vec![
13329 (Raw::Down, Point::new(60.0, 40.0)),
13330 (Raw::Move, Point::new(390.0, 350.0)),
13331 (Raw::Up, Point::new(390.0, 350.0)),
13332 ],
13333 "{h:?}"
13334 );
13335 assert_eq!(of(&h, "outer").len(), 3, "{h:?}");
13336 }
13337
13338 #[test]
13340 fn a_button_inside_keeps_its_tap() {
13341 let (mut d, heard) = driver();
13342 let at = Point::new(30.0, 55.0);
13343 d.press(at);
13344 d.release(at);
13345 d.run(0.05);
13346 let h = take(&heard);
13347 assert!(h.contains(&Msg::Pressed), "{h:?}");
13348 let inner = of(&h, "inner");
13349 assert_eq!(
13350 inner.iter().map(|e| e.0).collect::<Vec<_>>(),
13351 vec![Raw::Down, Raw::Up]
13352 );
13353 }
13354
13355 #[test]
13358 fn a_mouse_hovering_is_heard() {
13359 let (mut d, heard) = driver();
13360 d.move_mouse(Point::new(250.0, 90.0));
13361 d.run(0.02);
13362 let h = take(&heard);
13363 assert_eq!(of(&h, "outer"), vec![(Raw::Hover, Point::new(250.0, 50.0))]);
13364 assert!(of(&h, "inner").is_empty());
13365 d.move_mouse(Point::new(390.0, 390.0));
13366 d.run(0.02);
13367 assert!(take(&heard).is_empty(), "outside both");
13368 }
13369}
13370
13371#[cfg(test)]
13372mod menu_keyboard_tests {
13373 use super::testing::Driver;
13374 use crate::{Application, Command};
13375 use frus_widgets::{
13376 KeyStroke, MenuBar, MenuItem, MenuPath, Point, ShortcutKey, SubmenuButton, Theme, Widget,
13377 };
13378
13379 const W: f32 = 600.0;
13380 const H: f32 = 400.0;
13381
13382 #[derive(Clone, Debug, PartialEq)]
13383 enum Msg {
13384 Menu(MenuPath),
13385 Picked(&'static str),
13386 }
13387
13388 #[derive(Default)]
13390 struct Editor {
13391 menu: MenuPath,
13392 picked: Option<&'static str>,
13394 }
13395
13396 impl Application for Editor {
13397 type Message = Msg;
13398
13399 fn update(&mut self, message: Msg) -> Command<Msg> {
13400 match message {
13401 Msg::Menu(path) => self.menu = path,
13402 Msg::Picked(what) => {
13403 self.picked = Some(what);
13404 self.menu = MenuPath::closed();
13405 }
13406 }
13407 Command::none()
13408 }
13409
13410 fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
13411 Box::new(
13412 MenuBar::new(&self.menu, Msg::Menu).menu(
13413 SubmenuButton::new("File")
13414 .item(MenuItem::new("New", Msg::Picked("new")))
13415 .submenu(
13416 SubmenuButton::new("Open recent")
13417 .item(MenuItem::new("notes.txt", Msg::Picked("notes")))
13418 .item(MenuItem::new("todo.md", Msg::Picked("todo"))),
13419 )
13420 .item(MenuItem::new("Quit", Msg::Picked("quit"))),
13421 ),
13422 )
13423 }
13424 }
13425
13426 fn row(d: &Driver<Editor>, label: &str) -> Option<frus_widgets::WidgetId> {
13428 let (_, rect) = d.texts().into_iter().find(|(text, _)| text == label)?;
13429 d.hit(Point::new(rect.x + 4.0, rect.y + 4.0))
13430 }
13431
13432 #[test]
13435 fn opening_a_menu_focuses_its_first_row_and_escape_goes_back_one_level() {
13436 let mut d = Driver::new(Editor::default(), W, H);
13437 d.run(0.1);
13438 assert_eq!(d.focused(), None);
13439
13440 d.update(Msg::Menu(MenuPath::from_indices(vec![0])));
13441 d.run(0.1);
13442 let new = row(&d, "New").expect("the menu is open");
13443 assert_eq!(d.focused(), Some(new), "the first row has the focus");
13444
13445 d.update(Msg::Menu(MenuPath::from_indices(vec![0, 1])));
13446 d.run(0.1);
13447 let notes = row(&d, "notes.txt").expect("the submenu is open");
13448 assert_eq!(d.focused(), Some(notes), "the submenu's first row has it");
13449
13450 d.escape();
13451 d.run(0.1);
13452 assert_eq!(
13453 d.app().menu,
13454 MenuPath::from_indices(vec![0]),
13455 "one level closed"
13456 );
13457 assert!(row(&d, "notes.txt").is_none());
13458 assert_eq!(d.focused(), Some(new), "and the focus is back in the menu");
13459
13460 d.escape();
13461 d.run(0.1);
13462 assert!(!d.app().menu.is_open(), "then the menu");
13463 }
13464
13465 #[test]
13469 fn the_menu_is_worked_from_the_keyboard() {
13470 let key = |d: &mut Driver<Editor>, k: ShortcutKey| {
13471 d.key(KeyStroke::new(k));
13472 d.run(0.1);
13473 };
13474 let mut d = Driver::new(Editor::default(), W, H);
13475 d.run(0.1);
13476 d.update(Msg::Menu(MenuPath::from_indices(vec![0])));
13477 d.run(0.1);
13478 let (new, recent) = (row(&d, "New").unwrap(), row(&d, "Open recent").unwrap());
13479 assert_eq!(d.focused(), Some(new));
13480
13481 key(&mut d, ShortcutKey::Down);
13482 assert_eq!(d.focused(), Some(recent), "down to the next row");
13483 key(&mut d, ShortcutKey::Right);
13484 assert_eq!(
13485 d.app().menu,
13486 MenuPath::from_indices(vec![0, 1]),
13487 "right opens it"
13488 );
13489 assert_eq!(
13490 d.focused(),
13491 row(&d, "notes.txt"),
13492 "and its first row has the focus"
13493 );
13494 key(&mut d, ShortcutKey::Down);
13495 assert_eq!(d.focused(), row(&d, "todo.md"));
13496 key(&mut d, ShortcutKey::Left);
13497 assert_eq!(
13498 d.app().menu,
13499 MenuPath::from_indices(vec![0]),
13500 "left closes it"
13501 );
13502 assert_eq!(
13503 d.focused(),
13504 Some(recent),
13505 "and the focus is back on its row"
13506 );
13507 key(&mut d, ShortcutKey::Up);
13508 assert_eq!(d.focused(), Some(new));
13509 key(&mut d, ShortcutKey::Enter);
13510 assert_eq!(d.app().picked, Some("new"), "Enter picks the row");
13511 assert!(!d.app().menu.is_open(), "and the menus close");
13512 }
13513
13514 #[test]
13517 fn a_stroke_holds_its_modifiers_for_the_key_alone() {
13518 let mut d = Driver::new(Editor::default(), W, H);
13519 d.run(0.1);
13520 d.key(KeyStroke::new(ShortcutKey::Char('a')).ctrl());
13521 assert!(!d.shell_modifiers().0, "ctrl let go after the stroke");
13522 }
13523
13524 #[test]
13529 fn closing_a_menu_hands_the_focus_back_to_its_word() {
13530 let key = |d: &mut Driver<Editor>, k: ShortcutKey| {
13531 d.key(KeyStroke::new(k));
13532 d.run(0.1);
13533 };
13534 let mut d = Driver::new(Editor::default(), W, H);
13535 d.run(0.1);
13536 key(&mut d, ShortcutKey::Tab);
13537 let file = d.focused().expect("Tab reached the bar");
13538 key(&mut d, ShortcutKey::Enter);
13539 assert!(d.app().menu.is_open(), "Enter opened the menu");
13540 assert_eq!(d.focused(), row(&d, "New"), "and the focus went into it");
13541 key(&mut d, ShortcutKey::Escape);
13542 assert!(!d.app().menu.is_open(), "Escape closed it");
13543 assert_eq!(d.focused(), Some(file), "and the focus is back on its word");
13544 }
13545
13546 #[test]
13552 fn a_submenu_closing_hands_the_focus_back_to_any_row() {
13553 let key = |d: &mut Driver<Editor>, k: ShortcutKey| {
13554 d.key(KeyStroke::new(k));
13555 d.run(0.1);
13556 };
13557 let mut d = Driver::new(Editor::default(), W, H);
13558 d.run(0.1);
13559 d.update(Msg::Menu(MenuPath::from_indices(vec![0])));
13560 d.run(0.1);
13561 key(&mut d, ShortcutKey::Down);
13562 key(&mut d, ShortcutKey::Down);
13563 let quit = row(&d, "Quit");
13564 assert_eq!(d.focused(), quit, "down twice reaches Quit");
13565 d.update(Msg::Menu(MenuPath::from_indices(vec![0, 1])));
13567 d.run(0.1);
13568 assert_eq!(d.focused(), row(&d, "notes.txt"));
13569 key(&mut d, ShortcutKey::Escape);
13570 assert_eq!(d.app().menu, MenuPath::from_indices(vec![0]));
13571 assert_eq!(d.focused(), quit, "the focus is back on Quit");
13572 }
13573}
13574
13575#[cfg(test)]
13579mod right_click_bar_tests {
13580 use super::testing::Driver;
13581 use crate::{Application, Command};
13582 use frus_widgets::{Point, TextField, Theme, Widget};
13583
13584 struct Form;
13585
13586 impl Application for Form {
13587 type Message = ();
13588
13589 fn update(&mut self, _message: ()) -> Command<()> {
13590 Command::none()
13591 }
13592
13593 fn view(&self, _theme: &Theme) -> Box<dyn Widget<()>> {
13594 Box::new(TextField::new("some words in a field"))
13595 }
13596 }
13597
13598 #[test]
13599 fn a_right_click_on_a_field_opens_its_bar() {
13600 let mut d = Driver::new(Form, 500.0, 300.0);
13601 d.run(0.1);
13602 let shown = |d: &Driver<Form>, label: &str| d.texts().iter().any(|(t, _)| t == label);
13603 assert!(!shown(&d, "Select all"), "no bar before the click");
13604 let (_, words) = d
13605 .texts()
13606 .into_iter()
13607 .find(|(t, _)| t.contains("some words"))
13608 .expect("the field's words");
13609 d.secondary_click(Point::new(words.x + 20.0, words.y + words.height * 0.5));
13610 d.run(0.1);
13611 assert!(shown(&d, "Select all"), "the bar is open: {:?}", d.texts());
13615 assert!(!shown(&d, "Copy") && !shown(&d, "Cut"));
13616 assert!(
13617 !shown(&d, "Paste"),
13618 "an empty clipboard offers nothing to paste"
13619 );
13620 }
13621
13622 struct DesktopForm;
13624
13625 impl Application for DesktopForm {
13626 type Message = ();
13627
13628 fn update(&mut self, _message: ()) -> Command<()> {
13629 Command::none()
13630 }
13631
13632 fn view(&self, _theme: &Theme) -> Box<dyn Widget<()>> {
13633 Box::new(TextField::new("some words in a field"))
13634 }
13635
13636 fn theme(&self) -> Theme {
13637 Theme::default().with_platform(frus_widgets::TargetPlatform::Linux)
13638 }
13639 }
13640
13641 #[test]
13645 fn a_right_click_opens_the_menu_at_the_pointer() {
13646 let mut d = Driver::new(DesktopForm, 500.0, 300.0);
13647 d.run(0.1);
13648 let (_, words) = d
13649 .texts()
13650 .into_iter()
13651 .find(|(t, _)| t.contains("some words"))
13652 .expect("the field's words");
13653 let click = Point::new(words.x + 20.0, words.y + words.height * 0.5);
13654 d.secondary_click(click);
13655 d.run(0.1);
13656 let (_, row) = d
13657 .texts()
13658 .into_iter()
13659 .find(|(t, _)| t == "Select all")
13660 .expect("the menu is open");
13661 assert_eq!(row.x, click.x + 20.0, "the menu starts at the pointer");
13662 assert!(
13663 row.y >= click.y && row.y < click.y + 36.0,
13664 "its first row is on the pointer's line: {row:?} {click:?}"
13665 );
13666 }
13667
13668 #[test]
13673 fn a_desktop_s_select_all_puts_the_menu_away() {
13674 assert!(frus_widgets::default_target_platform().is_desktop());
13675 let mut d = Driver::new(DesktopForm, 500.0, 300.0);
13676 d.run(0.1);
13677 let shown = |d: &Driver<DesktopForm>, label: &str| {
13678 d.texts()
13679 .into_iter()
13680 .find(|(t, _)| t == label)
13681 .map(|(_, r)| r)
13682 };
13683 let words = shown(&d, "some words in a field").expect("the field's words");
13684 let click = Point::new(words.x + 20.0, words.y + words.height * 0.5);
13685 d.secondary_click(click);
13686 d.run(0.1);
13687 let row = shown(&d, "Select all").expect("the menu is open");
13688 let row = Point::new(row.x + 10.0, row.y + row.height * 0.5);
13689 d.press(row);
13690 d.release(row);
13691 d.run(0.1);
13692 assert!(shown(&d, "Select all").is_none(), "the menu is gone");
13693 assert!(shown(&d, "Copy").is_none(), "all of it: {:?}", d.texts());
13694 d.secondary_click(click);
13695 d.run(0.1);
13696 assert!(shown(&d, "Copy").is_some(), "the text is still selected");
13697 }
13698}
13699
13700#[cfg(test)]
13701mod scroll_behavior_tests {
13702 use super::testing::Driver;
13703 use crate::{Application, Command};
13704 use frus_widgets::{
13705 Container, Point, ScrollBehavior, ScrollConfiguration, SingleChildScrollView,
13706 TargetPlatform, Theme, VelocityStrategy, Widget,
13707 };
13708
13709 struct Page;
13712
13713 impl Application for Page {
13714 type Message = ();
13715
13716 fn update(&mut self, _message: ()) -> Command<()> {
13717 Command::none()
13718 }
13719
13720 fn view(&self, _theme: &Theme) -> Box<dyn Widget<()>> {
13721 Box::new(ScrollConfiguration::new(
13722 ScrollBehavior::material().with_platform(TargetPlatform::Ios),
13723 SingleChildScrollView::new()
13724 .width(300.0)
13725 .height(400.0)
13726 .child(Container::new().width(300.0).height(2000.0)),
13727 ))
13728 }
13729
13730 fn theme(&self) -> Theme {
13731 Theme::default().with_platform(TargetPlatform::Android)
13732 }
13733 }
13734
13735 #[test]
13739 fn a_drag_on_an_area_reads_its_fling_the_area_s_way() {
13740 let mut d = Driver::new(Page, 300.0, 400.0);
13741 d.run(0.2);
13742 d.press(Point::new(150.0, 200.0));
13743 assert_eq!(
13744 d.fling_strategy(),
13745 VelocityStrategy::RecentAverage(frus_widgets::BOUNCING_FLING_WEIGHTS)
13746 );
13747 d.release(Point::new(150.0, 200.0));
13748 }
13749}
13750
13751#[cfg(test)]
13754mod switch_drag_tests {
13755 use super::testing::Driver;
13756 use crate::{Application, Command};
13757 use frus_widgets::{Container, Point, Rect, Switch, Theme, Widget};
13758
13759 const W: f32 = 300.0;
13760 const H: f32 = 200.0;
13761
13762 #[derive(Clone, Debug, PartialEq)]
13763 enum Msg {
13764 Set(bool),
13765 }
13766
13767 #[derive(Default)]
13768 struct Page {
13769 on: bool,
13770 heard: Vec<Msg>,
13771 }
13772
13773 impl Application for Page {
13774 type Message = Msg;
13775
13776 fn update(&mut self, message: Msg) -> Command<Msg> {
13777 let Msg::Set(on) = message;
13778 self.on = on;
13779 self.heard.push(message);
13780 Command::none()
13781 }
13782
13783 fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
13784 Box::new(
13785 Container::new()
13786 .width(W)
13787 .height(H)
13788 .child(Switch::new(self.on).on_toggle(Msg::Set)),
13789 )
13790 }
13791 }
13792
13793 fn driver() -> Driver<Page> {
13794 let mut d = Driver::new(Page::default(), W, H);
13795 d.run(0.2);
13796 d
13797 }
13798
13799 fn track_and_thumb(d: &Driver<Page>) -> (Rect, Rect) {
13801 let (ui, _) = d.frame_parts().expect("a frame");
13802 let rects: Vec<Rect> = ui
13803 .scene()
13804 .primitives()
13805 .iter()
13806 .filter_map(|p| match p {
13807 frus_widgets::Primitive::Rect { rect, .. } => Some(*rect),
13808 _ => None,
13809 })
13810 .collect();
13811 let track = *rects
13812 .iter()
13813 .find(|r| r.width == 52.0 && r.height == 32.0)
13814 .expect("the track");
13815 (track, *rects.last().expect("the thumb"))
13816 }
13817
13818 fn along(d: &Driver<Page>) -> f32 {
13820 let (track, thumb) = track_and_thumb(d);
13821 (thumb.x + thumb.width * 0.5 - track.x - 16.0) / 20.0
13822 }
13823
13824 fn drag(d: &mut Driver<Page>, dx: f32, lift: bool) {
13826 let (track, _) = track_and_thumb(d);
13827 let from = Point::new(track.x + 16.0, track.y + 16.0);
13828 d.press(from);
13829 d.run(0.02);
13830 for step in 1..=8 {
13831 d.move_to(Point::new(from.x + dx * step as f32 / 8.0, from.y));
13832 d.run(0.02);
13833 }
13834 if lift {
13835 d.release(Point::new(from.x + dx, from.y));
13836 d.run(0.02);
13837 }
13838 }
13839
13840 #[test]
13843 fn the_thumb_follows_the_finger() {
13844 let mut d = driver();
13845 assert!(along(&d).abs() < 1e-3, "off at rest");
13846 drag(&mut d, 40.0, false);
13847 let now = along(&d);
13848 assert!(now > 0.3 && now <= 1.0, "moved with the finger: {now}");
13849 assert!(d.app().heard.is_empty(), "nothing said while held");
13850 d.run(0.5);
13852 assert!((along(&d) - now).abs() < 1e-3, "held where it was put");
13853 }
13854
13855 #[test]
13858 fn let_go_past_half_way_it_flips() {
13859 let mut d = driver();
13860 drag(&mut d, 60.0, true);
13861 assert_eq!(d.app().heard, vec![Msg::Set(true)]);
13862 d.run(0.5);
13863 assert!((along(&d) - 1.0).abs() < 1e-3, "settled on");
13864 }
13865
13866 #[test]
13868 fn let_go_short_of_half_way_it_goes_back() {
13869 let mut d = driver();
13870 drag(&mut d, 30.0, false);
13872 let (track, _) = track_and_thumb(&d);
13873 let back = Point::new(track.x + 16.0 + 15.0, track.y + 16.0);
13874 d.move_to(back);
13875 d.run(0.02);
13876 assert!(along(&d) > 0.0, "it had moved");
13877 assert!(along(&d) < 0.5, "but is short of half way: {}", along(&d));
13878 d.release(back);
13879 d.run(0.5);
13880 assert!(d.app().heard.is_empty(), "nothing said");
13881 assert!(along(&d).abs() < 1e-3, "back off: {}", along(&d));
13882 }
13883
13884 #[test]
13886 fn a_tap_still_flips_it() {
13887 let mut d = driver();
13888 let (track, _) = track_and_thumb(&d);
13889 let at = Point::new(track.x + 16.0, track.y + 16.0);
13890 d.press(at);
13891 d.release(at);
13892 d.run(0.5);
13893 assert_eq!(d.app().heard, vec![Msg::Set(true)]);
13894 assert!((along(&d) - 1.0).abs() < 1e-3);
13895 }
13896}
13897
13898#[cfg(test)]
13902mod range_slider_drag_tests {
13903 use super::testing::Driver;
13904 use crate::{Application, Command};
13905 use frus_widgets::{Container, Point, RangeSlider, Rect, Theme, Widget};
13906
13907 #[derive(Clone, Debug, PartialEq)]
13908 enum Msg {
13909 Range(f32, f32),
13910 Start(f32, f32),
13911 End(f32, f32),
13912 }
13913
13914 struct Page {
13915 values: (f32, f32),
13916 heard: Vec<Msg>,
13917 }
13918
13919 impl Application for Page {
13920 type Message = Msg;
13921
13922 fn update(&mut self, message: Msg) -> Command<Msg> {
13923 if let Msg::Range(s, e) | Msg::End(s, e) = message {
13924 self.values = (s, e);
13925 }
13926 self.heard.push(message);
13927 Command::none()
13928 }
13929
13930 fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
13931 let (start, end) = self.values;
13932 Box::new(
13933 Container::new().width(300.0).height(200.0).child(
13934 RangeSlider::new(start, end)
13935 .width(248.0)
13936 .divisions(20)
13937 .on_change(Msg::Range)
13938 .on_change_start(Msg::Start)
13939 .on_change_end(Msg::End),
13940 ),
13941 )
13942 }
13943 }
13944
13945 fn driver(start: f32, end: f32) -> Driver<Page> {
13946 let page = Page {
13947 values: (start, end),
13948 heard: Vec::new(),
13949 };
13950 let mut d = Driver::new(page, 300.0, 200.0);
13951 d.run(0.2);
13952 d
13953 }
13954
13955 fn thumbs(d: &Driver<Page>) -> Vec<Rect> {
13957 let (ui, _) = d.frame_parts().expect("a frame");
13958 ui.scene()
13959 .primitives()
13960 .iter()
13961 .filter_map(|p| match p {
13962 frus_widgets::Primitive::Rect { rect, blur, .. }
13963 if *blur == 0.0 && rect.width == 20.0 && rect.height == 20.0 =>
13964 {
13965 Some(*rect)
13966 }
13967 _ => None,
13968 })
13969 .collect()
13970 }
13971
13972 fn centre(r: Rect) -> Point {
13973 Point::new(r.x + r.width * 0.5, r.y + r.height * 0.5)
13974 }
13975
13976 fn drag(d: &mut Driver<Page>, from: Point, dx: f32) {
13978 d.press(from);
13979 d.run(0.02);
13980 for step in 1..=4 {
13981 d.move_to(Point::new(from.x + dx * step as f32 / 4.0, from.y));
13982 d.run(0.02);
13983 }
13984 d.release(Point::new(from.x + dx, from.y));
13985 d.run(0.02);
13986 }
13987
13988 fn close(a: (f32, f32), b: (f32, f32)) -> bool {
13989 (a.0 - b.0).abs() < 1e-3 && (a.1 - b.1).abs() < 1e-3
13990 }
13991
13992 #[test]
13996 fn a_drag_carries_the_nearer_thumb() {
13997 let mut d = driver(0.25, 0.75);
13998 let t = thumbs(&d);
13999 assert_eq!(t.len(), 2);
14000 let start = centre(t[0]).x.min(centre(t[1]).x);
14001 drag(&mut d, Point::new(start + 4.0, centre(t[0]).y), 34.0);
14004 let heard = &d.app().heard;
14005 assert_eq!(heard.first(), Some(&Msg::Start(0.25, 0.75)), "{heard:?}");
14006 let Some(Msg::End(s, e)) = heard.last().cloned() else {
14007 panic!("no end: {heard:?}");
14008 };
14009 assert!(close((s, e), (0.45, 0.75)), "({s}, {e})");
14010 assert!(close(d.app().values, (0.45, 0.75)));
14011 let order = thumbs(&d);
14013 assert!(order[1].x < order[0].x, "the start on top: {order:?}");
14014 }
14015
14016 #[test]
14019 fn where_the_thumbs_meet_the_movement_decides() {
14020 let mut d = driver(0.5, 0.5);
14021 let at = centre(thumbs(&d)[0]);
14022 drag(&mut d, at, -40.0);
14023 assert!(close(d.app().values, (0.3, 0.5)), "{:?}", d.app().values);
14024
14025 let mut d = driver(0.5, 0.5);
14026 drag(&mut d, at, 40.0);
14027 assert!(close(d.app().values, (0.5, 0.7)), "{:?}", d.app().values);
14028
14029 let mut d = driver(0.3, 0.5);
14031 let start = centre(thumbs(&d)[0]);
14032 drag(&mut d, start, 120.0);
14033 assert!(close(d.app().values, (0.5, 0.5)), "{:?}", d.app().values);
14034 }
14035}
14036
14037#[cfg(test)]
14040mod focus_change_tests {
14041 use super::testing::Driver;
14042 use crate::{Application, Command};
14043 use frus_widgets::{Flex, Focus, Point, Rect, TextField, Theme, Widget};
14044
14045 #[derive(Clone, Debug, PartialEq)]
14046 enum Msg {
14047 Group(bool),
14048 Outside,
14049 Typed,
14050 }
14051
14052 #[derive(Default)]
14053 struct Page {
14054 heard: Vec<Msg>,
14055 }
14056
14057 impl Application for Page {
14058 type Message = Msg;
14059
14060 fn update(&mut self, message: Msg) -> Command<Msg> {
14061 if message != Msg::Typed {
14062 self.heard.push(message);
14063 }
14064 Command::none()
14065 }
14066
14067 fn view(&self, _theme: &Theme) -> Box<dyn Widget<Msg>> {
14068 Box::new(
14069 Flex::column()
14070 .width(300.0)
14071 .height(400.0)
14072 .gap(40.0)
14073 .child(
14074 Focus::new(
14075 TextField::new("a")
14076 .on_input(|_| Msg::Typed)
14077 .on_tap_outside(Msg::Outside),
14078 )
14079 .skip_traversal()
14080 .on_focus_change(Msg::Group),
14081 )
14082 .child(TextField::new("b").on_input(|_| Msg::Typed)),
14083 )
14084 }
14085 }
14086
14087 fn fields(d: &Driver<Page>) -> (Rect, Rect) {
14089 let (ui, _) = d.frame_parts().expect("a frame");
14090 let rects: Vec<Rect> = ui
14091 .focusable_ids()
14092 .filter_map(|id| ui.widget_rect(id))
14093 .filter(|r| r.height < 100.0)
14094 .collect();
14095 (rects[0], rects[rects.len() - 1])
14096 }
14097
14098 fn centre(r: Rect) -> Point {
14099 Point::new(r.x + r.width / 2.0, r.y + r.height / 2.0)
14100 }
14101
14102 fn tap(d: &mut Driver<Page>, at: Point) {
14103 d.press(at);
14104 d.release(at);
14105 d.run(0.05);
14106 }
14107
14108 #[test]
14111 fn the_shell_reports_the_focus_moving() {
14112 let mut d = Driver::new(Page::default(), 300.0, 400.0);
14113 d.run(0.2);
14114 let (a, b) = fields(&d);
14115 tap(&mut d, centre(a));
14116 assert_eq!(d.app().heard, vec![Msg::Group(true)], "arrived");
14117 tap(&mut d, centre(a));
14118 assert_eq!(
14119 d.app().heard,
14120 vec![Msg::Group(true)],
14121 "a press inside says nothing"
14122 );
14123 tap(&mut d, centre(b));
14124 assert_eq!(
14125 d.app().heard,
14126 vec![Msg::Group(true), Msg::Outside, Msg::Group(false)],
14127 "outside, then left"
14128 );
14129 tap(&mut d, Point::new(150.0, 390.0));
14130 assert_eq!(
14131 d.app().heard.len(),
14132 3,
14133 "the other field holds no `on_tap_outside`: {:?}",
14134 d.app().heard
14135 );
14136 }
14137}