1use core::ffi::c_void;
7
8use azul_core::{
9 callbacks::{TimerCallbackReturn, Update},
10 dom::{DomId, OptionDomNodeId},
11 geom::{LogicalPosition, LogicalSize, OptionLogicalPosition},
12 id::NodeId,
13 menu::Menu,
14 refany::{OptionRefAny, RefAny},
15 resources::ImageRef,
16 task::{
17 Duration, GetSystemTimeCallback, Instant, OptionDuration, OptionInstant, TerminateTimer,
18 ThreadId, TimerId,
19 },
20 window::{KeyboardState, MouseState, WindowFlags},
21};
22
23use azul_css::AzString;
24
25use crate::{
26 callbacks::CallbackInfo,
27 thread::Thread,
28 window_state::{FullWindowState, WindowCreateOptions},
29};
30
31const DEFAULT_TIMER_TICK_MS: u64 = 10;
33
34pub type TimerCallbackType = extern "C" fn(
36 RefAny,
37 TimerCallbackInfo,
38) -> TimerCallbackReturn;
39
40#[repr(C)]
42pub struct TimerCallback {
43 pub cb: TimerCallbackType,
44 pub ctx: OptionRefAny,
47}
48
49impl TimerCallback {
50 pub fn create(cb: TimerCallbackType) -> Self {
51 Self {
52 cb,
53 ctx: OptionRefAny::None,
54 }
55 }
56}
57
58impl core::fmt::Debug for TimerCallback {
59 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
60 write!(f, "TimerCallback {{ cb: {:p} }}", self.cb as *const ())
61 }
62}
63
64impl Clone for TimerCallback {
65 fn clone(&self) -> Self {
66 Self {
67 cb: self.cb,
68 ctx: self.ctx.clone(),
69 }
70 }
71}
72
73impl From<TimerCallbackType> for TimerCallback {
74 fn from(cb: TimerCallbackType) -> Self {
75 Self {
76 cb,
77 ctx: OptionRefAny::None,
78 }
79 }
80}
81
82impl PartialEq for TimerCallback {
83 fn eq(&self, other: &Self) -> bool {
84 std::ptr::eq(self.cb as *const (), other.cb as *const ())
85 }
86}
87
88impl Eq for TimerCallback {}
89
90impl PartialOrd for TimerCallback {
91 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
92 (self.cb as *const () as usize).partial_cmp(&(other.cb as *const () as usize))
93 }
94}
95
96impl Ord for TimerCallback {
97 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
98 (self.cb as *const () as usize).cmp(&(other.cb as *const () as usize))
99 }
100}
101
102impl core::hash::Hash for TimerCallback {
103 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
104 (self.cb as *const () as usize).hash(state);
105 }
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Hash)]
110#[repr(C)]
111pub struct Timer {
112 pub refany: RefAny,
113 pub node_id: OptionDomNodeId,
114 pub created: Instant,
115 pub last_run: OptionInstant,
116 pub run_count: usize,
117 pub delay: OptionDuration,
118 pub interval: OptionDuration,
119 pub timeout: OptionDuration,
120 pub callback: TimerCallback,
121}
122
123impl Timer {
124 pub fn create<C: Into<TimerCallback>>(
125 refany: RefAny,
126 callback: C,
127 get_system_time_fn: GetSystemTimeCallback,
128 ) -> Self {
129 Self {
130 refany,
131 node_id: None.into(),
132 created: (get_system_time_fn.cb)(),
133 run_count: 0,
134 last_run: OptionInstant::None,
135 delay: OptionDuration::None,
136 interval: OptionDuration::None,
137 timeout: OptionDuration::None,
138 callback: callback.into(),
139 }
140 }
141
142 #[must_use] pub const fn tick_millis(&self) -> u64 {
143 match self.interval.as_ref() {
144 Some(Duration::System(s)) => s.millis(),
145 Some(Duration::Tick(s)) => s.tick_diff,
146 None => DEFAULT_TIMER_TICK_MS,
147 }
148 }
149
150 #[must_use] pub fn is_about_to_finish(&self, instant_now: &Instant) -> bool {
151 match self.timeout {
152 OptionDuration::Some(timeout) => {
153 instant_now.duration_since(&self.created).greater_than(&timeout)
154 }
155 OptionDuration::None => false,
156 }
157 }
158
159 #[must_use] pub fn instant_of_next_run(&self) -> Instant {
160 let last_run = self.last_run.as_ref().map_or(&self.created, |s| s);
161
162 last_run
163 .clone()
164 .add_optional_duration(self.delay.as_ref())
165 .add_optional_duration(self.interval.as_ref())
166 }
167
168 #[inline]
169 #[must_use] pub const fn with_delay(mut self, delay: Duration) -> Self {
170 self.delay = OptionDuration::Some(delay);
171 self
172 }
173
174 #[inline]
175 #[must_use] pub const fn with_interval(mut self, interval: Duration) -> Self {
176 self.interval = OptionDuration::Some(interval);
177 self
178 }
179
180 #[inline]
181 #[must_use] pub const fn with_timeout(mut self, timeout: Duration) -> Self {
182 self.timeout = OptionDuration::Some(timeout);
183 self
184 }
185
186 pub fn invoke(
192 &mut self,
193 callback_info: &CallbackInfo,
194 get_system_time_fn: &GetSystemTimeCallback,
195 ) -> TimerCallbackReturn {
196 let now = (get_system_time_fn.cb)();
197
198 match self.last_run.as_ref() {
200 Some(last_run) => {
201 if let OptionDuration::Some(interval) = self.interval {
203 if now.duration_since(last_run).smaller_than(&interval) {
204 return TimerCallbackReturn {
205 should_update: Update::DoNothing,
206 should_terminate: TerminateTimer::Continue,
207 };
208 }
209 }
210 }
211 None => {
212 if let OptionDuration::Some(delay) = self.delay {
214 if now.duration_since(&self.created).smaller_than(&delay) {
215 return TimerCallbackReturn {
216 should_update: Update::DoNothing,
217 should_terminate: TerminateTimer::Continue,
218 };
219 }
220 }
221 }
222 }
223
224 let is_about_to_finish = self.is_about_to_finish(&now);
225
226 let mut timer_callback_info = TimerCallbackInfo {
229 callback_info: *callback_info,
230 node_id: self.node_id,
231 frame_start: now.clone(),
232 call_count: self.run_count,
233 is_about_to_finish,
234 _abi_ref: core::ptr::null(),
235 _abi_mut: core::ptr::null_mut(),
236 };
237
238 let mut result = (self.callback.cb)(self.refany.clone(), timer_callback_info);
239
240 if is_about_to_finish {
241 result.should_terminate = TerminateTimer::Terminate;
242 }
243
244 self.run_count += 1;
245 self.last_run = OptionInstant::Some(now);
246
247 result
248 }
249}
250
251impl Default for Timer {
252 fn default() -> Self {
253 extern "C" fn default_callback(_: RefAny, _: TimerCallbackInfo) -> TimerCallbackReturn {
254 TimerCallbackReturn::terminate_unchanged()
255 }
256
257 const extern "C" fn default_time() -> Instant {
258 Instant::Tick(azul_core::task::SystemTick { tick_counter: 0 })
259 }
260
261 let cb: TimerCallbackType = default_callback;
262 Self::create(
263 RefAny::new(()),
264 cb,
265 GetSystemTimeCallback { cb: default_time },
266 )
267 }
268}
269
270#[derive(Debug, Clone)]
275#[repr(C)]
276#[allow(clippy::pub_underscore_fields)] pub struct TimerCallbackInfo {
278 pub callback_info: CallbackInfo,
279 pub node_id: OptionDomNodeId,
280 pub frame_start: Instant,
281 pub call_count: usize,
282 pub is_about_to_finish: bool,
283 pub _abi_ref: *const c_void,
284 pub _abi_mut: *mut c_void,
285}
286
287impl TimerCallbackInfo {
288 #[must_use] pub const fn create(
289 callback_info: CallbackInfo,
290 node_id: OptionDomNodeId,
291 frame_start: Instant,
292 call_count: usize,
293 is_about_to_finish: bool,
294 ) -> Self {
295 Self {
296 callback_info,
297 node_id,
298 frame_start,
299 call_count,
300 is_about_to_finish,
301 _abi_ref: core::ptr::null(),
302 _abi_mut: core::ptr::null_mut(),
303 }
304 }
305
306 #[must_use] pub fn get_attached_node_size(&self) -> Option<LogicalSize> {
307 let node_id = self.node_id.into_option()?;
308 self.callback_info.get_node_size(node_id)
309 }
310
311 #[must_use] pub fn get_attached_node_position(&self) -> Option<LogicalPosition> {
312 let node_id = self.node_id.into_option()?;
313 self.callback_info.get_node_position(node_id)
314 }
315
316 #[must_use] pub const fn get_callback_info(&self) -> &CallbackInfo {
317 &self.callback_info
318 }
319
320 pub const fn get_callback_info_mut(&mut self) -> &mut CallbackInfo {
321 &mut self.callback_info
322 }
323
324 #[must_use] pub fn get_ctx(&self) -> OptionRefAny {
330 self.callback_info.get_ctx()
331 }
332
333 pub fn add_timer(&mut self, timer_id: TimerId, timer: Timer) {
335 self.callback_info.add_timer(timer_id, timer);
336 }
337
338 pub fn remove_timer(&mut self, timer_id: TimerId) {
340 self.callback_info.remove_timer(timer_id);
341 }
342
343 pub fn add_thread(&mut self, thread_id: ThreadId, thread: Thread) {
345 self.callback_info.add_thread(thread_id, thread);
346 }
347
348 pub fn remove_thread(&mut self, thread_id: ThreadId) {
350 self.callback_info.remove_thread(thread_id);
351 }
352
353 pub fn stop_propagation(&mut self) {
355 self.callback_info.stop_propagation();
356 }
357
358 pub fn create_window(&mut self, options: WindowCreateOptions) {
360 self.callback_info.create_window(options);
361 }
362
363 pub fn close_window(&mut self) {
365 self.callback_info.close_window();
366 }
367
368 pub fn modify_window_state(&mut self, state: FullWindowState) {
370 self.callback_info.modify_window_state(state);
371 }
372
373 pub fn add_image_to_cache(&mut self, id: AzString, image: ImageRef) {
375 self.callback_info.add_image_to_cache(id, image);
376 }
377
378 pub fn remove_image_from_cache(&mut self, id: AzString) {
380 self.callback_info.remove_image_from_cache(id);
381 }
382
383 pub fn update_all_image_callbacks(&mut self) {
388 self.callback_info.update_all_image_callbacks();
389 }
390
391 pub fn trigger_virtual_view_rerender(&mut self, dom_id: DomId, node_id: NodeId) {
393 self.callback_info.trigger_virtual_view_rerender(dom_id, node_id);
394 }
395
396 pub fn reload_system_fonts(&mut self) {
398 self.callback_info.reload_system_fonts();
399 }
400
401 pub fn prevent_default(&mut self) {
403 self.callback_info.prevent_default();
404 }
405
406 pub fn open_menu(&mut self, menu: Menu) {
408 self.callback_info.open_menu(menu);
409 }
410
411 pub fn open_menu_at(&mut self, menu: Menu, position: LogicalPosition) {
413 self.callback_info.open_menu_at(menu, position);
414 }
415
416 pub fn show_tooltip(&mut self, text: AzString) {
418 self.callback_info.show_tooltip(text);
419 }
420
421 pub fn show_tooltip_at(&mut self, text: AzString, position: LogicalPosition) {
423 self.callback_info.show_tooltip_at(text, position);
424 }
425
426 pub fn hide_tooltip(&mut self) {
428 self.callback_info.hide_tooltip();
429 }
430
431 pub fn open_menu_for_hit_node(&mut self, menu: Menu) -> bool {
433 self.callback_info.open_menu_for_hit_node(menu)
434 }
435
436 #[must_use] pub const fn get_current_window_flags(&self) -> WindowFlags {
438 self.callback_info.get_current_window_flags()
439 }
440
441 #[must_use] pub fn get_current_keyboard_state(&self) -> KeyboardState {
443 self.callback_info.get_current_keyboard_state()
444 }
445
446 #[must_use] pub const fn get_current_mouse_state(&self) -> MouseState {
448 self.callback_info.get_current_mouse_state()
449 }
450
451 #[must_use] pub const fn get_cursor_relative_to_node(&self) -> azul_core::geom::OptionCursorNodePosition {
453 self.callback_info.get_cursor_relative_to_node()
454 }
455
456 #[must_use] pub const fn get_cursor_relative_to_viewport(&self) -> OptionLogicalPosition {
458 self.callback_info.get_cursor_relative_to_viewport()
459 }
460
461 #[must_use] pub fn get_cursor_position(&self) -> Option<LogicalPosition> {
463 self.callback_info.get_cursor_position()
464 }
465
466 #[must_use] pub fn get_current_time(&self) -> Instant {
468 self.frame_start.clone()
469 }
470
471 #[must_use] pub fn is_dom_focused(&self, dom_id: DomId) -> bool {
473 self.callback_info.is_dom_focused(dom_id)
474 }
475
476 #[must_use] pub fn is_pen_in_contact(&self) -> bool {
478 self.callback_info.is_pen_in_contact()
479 }
480
481 #[must_use] pub fn is_pen_eraser(&self) -> bool {
483 self.callback_info.is_pen_eraser()
484 }
485
486 #[must_use] pub fn is_pen_barrel_button_pressed(&self) -> bool {
488 self.callback_info.is_pen_barrel_button_pressed()
489 }
490
491 #[must_use] pub const fn is_dragging(&self) -> bool {
493 self.callback_info.get_current_mouse_state().left_down
494 }
495
496 #[must_use] pub const fn is_drag_active(&self) -> bool {
498 self.callback_info.get_current_mouse_state().left_down
499 }
500
501 #[must_use] pub const fn is_node_drag_active(&self) -> bool {
503 self.callback_info.get_current_mouse_state().left_down
504 }
505
506 #[must_use] pub fn is_file_drag_active(&self) -> bool {
508 self.callback_info.is_file_drag_active()
509 }
510
511 #[must_use] pub fn has_sufficient_history_for_gestures(&self) -> bool {
513 self.callback_info.has_sufficient_history_for_gestures()
514 }
515
516 #[must_use] pub fn get_scroll_node_info(
522 &self,
523 dom_id: DomId,
524 node_id: NodeId,
525 ) -> Option<crate::managers::scroll_state::ScrollNodeInfo> {
526 self.callback_info.get_scroll_node_info(dom_id, node_id)
527 }
528
529 #[must_use] pub fn find_scroll_parent(
534 &self,
535 dom_id: DomId,
536 node_id: NodeId,
537 ) -> Option<NodeId> {
538 self.callback_info.find_scroll_parent(dom_id, node_id)
539 }
540
541 #[cfg(feature = "std")]
546 #[must_use] pub fn get_scroll_input_queue(
547 &self,
548 ) -> crate::managers::scroll_state::ScrollInputQueue {
549 self.callback_info.get_scroll_input_queue()
550 }
551
552 pub fn scroll_to(
557 &mut self,
558 dom_id: DomId,
559 node_id: azul_core::styled_dom::NodeHierarchyItemId,
560 position: LogicalPosition,
561 ) {
562 self.callback_info.scroll_to(dom_id, node_id, position);
563 }
564
565 pub fn scroll_to_unclamped(
567 &mut self,
568 dom_id: DomId,
569 node_id: azul_core::styled_dom::NodeHierarchyItemId,
570 position: LogicalPosition,
571 ) {
572 self.callback_info.scroll_to_unclamped(dom_id, node_id, position);
573 }
574
575 pub fn set_cursor_visibility(&mut self, visible: bool) {
579 self.callback_info.set_cursor_visibility(visible);
580 }
581
582 pub fn set_cursor_visibility_toggle(&mut self) {
584 use crate::callbacks::CallbackChange;
585 self.callback_info.push_change(CallbackChange::ToggleCursorVisibility);
586 }
587
588 pub fn reset_cursor_blink(&mut self) {
590 self.callback_info.reset_cursor_blink();
591 }
592}
593
594#[derive(Debug, Clone)]
596#[repr(C, u8)]
597#[allow(variant_size_differences)] #[allow(clippy::large_enum_variant)]
600pub enum OptionTimer {
601 None,
602 Some(Timer),
603}
604
605impl From<Option<Timer>> for OptionTimer {
606 fn from(o: Option<Timer>) -> Self {
607 o.map_or_else(|| Self::None, Self::Some)
608 }
609}
610
611impl OptionTimer {
612 #[must_use] pub fn into_option(self) -> Option<Timer> {
613 match self {
614 Self::None => None,
615 Self::Some(t) => Some(t),
616 }
617 }
618}
619
620#[cfg(all(test, feature = "std"))]
621#[allow(
622 clippy::float_cmp,
623 clippy::too_many_lines,
624 clippy::unreadable_literal,
625 clippy::cognitive_complexity
626)]
627mod autotest_generated {
628 use std::{
629 collections::BTreeMap,
630 sync::{
631 atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering},
632 Arc, Mutex, MutexGuard, PoisonError,
633 },
634 };
635
636 use azul_core::{
637 dom::DomNodeId,
638 gl::OptionGlContextPtr,
639 hit_test::ScrollPosition,
640 menu::MenuItemVec,
641 resources::{RawImageFormat, RendererResources},
642 styled_dom::NodeHierarchyItemId,
643 task::{SystemTick, SystemTickDiff, SystemTimeDiff, ThreadReceiver},
644 window::{MonitorVec, RawWindowHandle},
645 };
646 use azul_css::system::SystemStyle;
647 use rust_fontconfig::FcFontCache;
648
649 use super::*;
650 #[cfg(feature = "icu")]
651 use crate::icu::IcuLocalizerHandle;
652 use crate::{
653 callbacks::{CallbackChange, CallbackInfoRefData, ExternalSystemCallbacks},
654 thread::{ThreadCallbackType, ThreadSender},
655 window::LayoutWindow,
656 };
657
658 fn tick(t: u64) -> Instant {
665 Instant::Tick(SystemTick::new(t))
666 }
667
668 const fn tick_dur(d: u64) -> Duration {
670 Duration::Tick(SystemTickDiff { tick_diff: d })
671 }
672
673 const fn sys_dur_millis(ms: u64) -> Duration {
677 Duration::System(SystemTimeDiff::from_millis(ms))
678 }
679
680 fn tick_of(i: &Instant) -> u64 {
682 match i {
683 Instant::Tick(t) => t.tick_counter,
684 Instant::System(_) => panic!("expected a Tick instant, got a System one"),
685 }
686 }
687
688 static CLOCK_LOCK: Mutex<()> = Mutex::new(());
698 static FAKE_TICK: AtomicU64 = AtomicU64::new(0);
699 static CB_INVOCATIONS: AtomicUsize = AtomicUsize::new(0);
700 static CB_SEEN_CALL_COUNT: AtomicUsize = AtomicUsize::new(0);
701 static CB_SEEN_FRAME_START: AtomicU64 = AtomicU64::new(0);
702 static CB_SEEN_ABOUT_TO_FINISH: AtomicBool = AtomicBool::new(false);
703 static CB_RETURN_TERMINATE: AtomicBool = AtomicBool::new(false);
704
705 fn clock_guard() -> MutexGuard<'static, ()> {
709 let guard = CLOCK_LOCK.lock().unwrap_or_else(PoisonError::into_inner);
710 FAKE_TICK.store(0, Ordering::SeqCst);
711 CB_INVOCATIONS.store(0, Ordering::SeqCst);
712 CB_SEEN_CALL_COUNT.store(0, Ordering::SeqCst);
713 CB_SEEN_FRAME_START.store(0, Ordering::SeqCst);
714 CB_SEEN_ABOUT_TO_FINISH.store(false, Ordering::SeqCst);
715 CB_RETURN_TERMINATE.store(false, Ordering::SeqCst);
716 guard
717 }
718
719 fn set_now(t: u64) {
720 FAKE_TICK.store(t, Ordering::SeqCst);
721 }
722
723 extern "C" fn fake_clock() -> Instant {
724 Instant::Tick(SystemTick::new(FAKE_TICK.load(Ordering::SeqCst)))
725 }
726
727 fn fake_clock_cb() -> GetSystemTimeCallback {
728 GetSystemTimeCallback { cb: fake_clock }
729 }
730
731 extern "C" fn recording_cb(_data: RefAny, info: TimerCallbackInfo) -> TimerCallbackReturn {
734 CB_INVOCATIONS.fetch_add(1, Ordering::SeqCst);
735 CB_SEEN_CALL_COUNT.store(info.call_count, Ordering::SeqCst);
736 CB_SEEN_ABOUT_TO_FINISH.store(info.is_about_to_finish, Ordering::SeqCst);
737 if let Instant::Tick(t) = &info.frame_start {
738 CB_SEEN_FRAME_START.store(t.tick_counter, Ordering::SeqCst);
739 }
740 TimerCallbackReturn {
741 should_update: Update::DoNothing,
742 should_terminate: if CB_RETURN_TERMINATE.load(Ordering::SeqCst) {
743 TerminateTimer::Terminate
744 } else {
745 TerminateTimer::Continue
746 },
747 }
748 }
749
750 extern "C" fn cb_alpha(_d: RefAny, _i: TimerCallbackInfo) -> TimerCallbackReturn {
754 TimerCallbackReturn {
755 should_update: Update::RefreshDom,
756 should_terminate: TerminateTimer::Terminate,
757 }
758 }
759 extern "C" fn cb_beta(_d: RefAny, _i: TimerCallbackInfo) -> TimerCallbackReturn {
760 TimerCallbackReturn {
761 should_update: Update::DoNothing,
762 should_terminate: TerminateTimer::Continue,
763 }
764 }
765
766 fn timer_at(created: u64, cb: TimerCallbackType) -> Timer {
768 set_now(created);
769 Timer::create(RefAny::new(0_usize), cb, fake_clock_cb())
770 }
771
772 struct Env<'a> {
777 ref_data: &'a CallbackInfoRefData<'a>,
778 changes: &'a Arc<Mutex<Vec<CallbackChange>>>,
779 }
780
781 impl Env<'_> {
782 fn info(&self) -> CallbackInfo {
783 self.info_with(OptionLogicalPosition::None, OptionLogicalPosition::None)
784 }
785
786 fn info_with(
787 &self,
788 cursor_relative_to_item: OptionLogicalPosition,
789 cursor_in_viewport: OptionLogicalPosition,
790 ) -> CallbackInfo {
791 CallbackInfo::new(
792 self.ref_data,
793 self.changes,
794 DomNodeId {
795 dom: DomId::ROOT_ID,
796 node: NodeHierarchyItemId::NONE,
797 },
798 cursor_relative_to_item,
799 cursor_in_viewport,
800 )
801 }
802
803 fn timer_info(&self) -> TimerCallbackInfo {
805 TimerCallbackInfo::create(self.info(), OptionDomNodeId::None, tick(0), 0, false)
806 }
807
808 fn take_changes(&self) -> Vec<CallbackChange> {
809 self.changes
810 .lock()
811 .map(|mut c| core::mem::take(&mut *c))
812 .unwrap_or_default()
813 }
814
815 fn take_one(&self) -> CallbackChange {
817 let mut changes = self.take_changes();
818 assert_eq!(changes.len(), 1, "expected exactly one change: {changes:?}");
819 changes.remove(0)
820 }
821 }
822
823 fn with_env<R>(f: impl FnOnce(&Env<'_>) -> R) -> R {
824 with_env_cfg(false, OptionRefAny::None, f)
825 }
826
827 fn with_env_cfg<R>(left_down: bool, ctx: OptionRefAny, f: impl FnOnce(&Env<'_>) -> R) -> R {
831 let layout_window =
832 LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new failed");
833 let renderer_resources = RendererResources::default();
834 let previous_window_state: Option<FullWindowState> = None;
835 let mut current_window_state = FullWindowState::default();
836 current_window_state.mouse_state.left_down = left_down;
837 let gl_context = OptionGlContextPtr::None;
838 let scroll_states: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> =
839 BTreeMap::new();
840 let window_handle = RawWindowHandle::Unsupported;
841 let system_callbacks = ExternalSystemCallbacks::rust_internal();
842
843 let ref_data = CallbackInfoRefData {
844 layout_window: &layout_window,
845 renderer_resources: &renderer_resources,
846 previous_window_state: &previous_window_state,
847 current_window_state: ¤t_window_state,
848 gl_context: &gl_context,
849 current_scroll_manager: &scroll_states,
850 current_window_handle: &window_handle,
851 system_callbacks: &system_callbacks,
852 system_style: Arc::new(SystemStyle::default()),
853 monitors: Arc::new(Mutex::new(MonitorVec::from_const_slice(&[]))),
854 #[cfg(feature = "icu")]
855 icu_localizer: IcuLocalizerHandle::default(),
856 ctx,
857 };
858
859 let changes: Arc<Mutex<Vec<CallbackChange>>> = Arc::new(Mutex::new(Vec::new()));
860 let env = Env {
861 ref_data: &ref_data,
862 changes: &changes,
863 };
864 f(&env)
865 }
866
867 fn empty_menu() -> Menu {
868 Menu::create(MenuItemVec::from_const_slice(&[]))
869 }
870
871 #[test]
876 fn timer_create_starts_completely_unarmed() {
877 let _g = clock_guard();
878 let t = timer_at(12_345, recording_cb as TimerCallbackType);
879
880 assert_eq!(tick_of(&t.created), 12_345, "created must come from the clock");
881 assert_eq!(t.run_count, 0);
882 assert_eq!(t.last_run, OptionInstant::None);
883 assert_eq!(t.delay, OptionDuration::None);
884 assert_eq!(t.interval, OptionDuration::None);
885 assert_eq!(t.timeout, OptionDuration::None);
886 assert_eq!(t.node_id, OptionDomNodeId::None);
887 }
888
889 #[test]
890 fn timer_create_at_max_tick_does_not_panic() {
891 let _g = clock_guard();
892 let t = timer_at(u64::MAX, recording_cb as TimerCallbackType);
893 assert_eq!(tick_of(&t.created), u64::MAX);
894 assert!(!t.is_about_to_finish(&tick(u64::MAX)));
896 assert_eq!(tick_of(&t.instant_of_next_run()), u64::MAX);
897 }
898
899 #[test]
900 fn timer_default_is_a_zero_tick_timer() {
901 let t = Timer::default();
902 assert_eq!(tick_of(&t.created), 0);
903 assert_eq!(t.run_count, 0);
904 assert_eq!(t.last_run, OptionInstant::None);
905 assert_eq!(t.tick_millis(), DEFAULT_TIMER_TICK_MS);
906 }
907
908 #[test]
909 fn timer_clone_equals_original() {
910 let _g = clock_guard();
911 let t = timer_at(7, recording_cb as TimerCallbackType)
912 .with_delay(tick_dur(1))
913 .with_interval(tick_dur(2))
914 .with_timeout(tick_dur(3));
915 let c = t.clone();
916 assert_eq!(t, c, "Clone must be value-preserving");
917 }
918
919 #[test]
924 fn tick_millis_falls_back_to_default_without_interval() {
925 let _g = clock_guard();
926 let t = timer_at(0, recording_cb as TimerCallbackType);
927 assert_eq!(t.tick_millis(), DEFAULT_TIMER_TICK_MS);
928 assert_eq!(t.tick_millis(), 10);
929
930 let t = t.with_delay(tick_dur(999)).with_timeout(tick_dur(888));
932 assert_eq!(t.tick_millis(), DEFAULT_TIMER_TICK_MS);
933 }
934
935 #[test]
936 fn tick_millis_passes_tick_intervals_through_unclamped() {
937 let _g = clock_guard();
938 for raw in [0_u64, 1, 10, u64::from(u32::MAX), u64::MAX] {
939 let t = timer_at(0, recording_cb as TimerCallbackType).with_interval(tick_dur(raw));
940 assert_eq!(t.tick_millis(), raw, "tick interval {raw} must round-trip");
941 }
942 }
943
944 #[test]
945 fn tick_millis_system_interval_round_trips_whole_millis() {
946 let _g = clock_guard();
947 for ms in [0_u64, 1, 999, 1_000, 1_001, 86_400_000, u64::MAX] {
950 let t = timer_at(0, recording_cb as TimerCallbackType).with_interval(sys_dur_millis(ms));
951 assert_eq!(t.tick_millis(), ms, "millis {ms} must round-trip");
952 }
953 }
954
955 #[test]
956 fn tick_millis_saturates_instead_of_overflowing() {
957 let _g = clock_guard();
958 let huge = Duration::System(SystemTimeDiff {
960 secs: u64::MAX,
961 nanos: 999_999_999,
962 });
963 let t = timer_at(0, recording_cb as TimerCallbackType).with_interval(huge);
964 assert_eq!(t.tick_millis(), u64::MAX);
965 }
966
967 #[test]
968 fn tick_millis_truncates_sub_millisecond_intervals_to_zero() {
969 let _g = clock_guard();
970 let t = timer_at(0, recording_cb as TimerCallbackType)
973 .with_interval(Duration::System(SystemTimeDiff::from_nanos(999_999)));
974 assert_eq!(t.tick_millis(), 0);
975 }
976
977 #[test]
982 fn is_about_to_finish_is_false_without_a_timeout() {
983 let _g = clock_guard();
984 let t = timer_at(0, recording_cb as TimerCallbackType);
985 assert!(!t.is_about_to_finish(&tick(0)));
986 assert!(!t.is_about_to_finish(&tick(u64::MAX)), "no timeout = never finishes");
987 }
988
989 #[test]
990 fn is_about_to_finish_boundary_is_strictly_greater() {
991 let _g = clock_guard();
992 let t = timer_at(100, recording_cb as TimerCallbackType).with_timeout(tick_dur(50));
993
994 assert!(!t.is_about_to_finish(&tick(149)), "1 tick early");
995 assert!(!t.is_about_to_finish(&tick(150)), "exactly at the timeout");
997 assert!(t.is_about_to_finish(&tick(151)), "1 tick past the timeout");
998 }
999
1000 #[test]
1001 fn is_about_to_finish_saturates_when_the_clock_runs_backwards() {
1002 let _g = clock_guard();
1003 let t = timer_at(1_000, recording_cb as TimerCallbackType).with_timeout(tick_dur(10));
1004 assert!(!t.is_about_to_finish(&tick(0)));
1007 }
1008
1009 #[test]
1010 fn is_about_to_finish_at_the_u64_ceiling() {
1011 let _g = clock_guard();
1012 let t = timer_at(0, recording_cb as TimerCallbackType);
1013
1014 let max_timeout = t.clone().with_timeout(tick_dur(u64::MAX));
1015 assert!(
1016 !max_timeout.is_about_to_finish(&tick(u64::MAX)),
1017 "MAX elapsed is not > MAX timeout"
1018 );
1019
1020 let near_max = t.with_timeout(tick_dur(u64::MAX - 1));
1021 assert!(near_max.is_about_to_finish(&tick(u64::MAX)));
1022 }
1023
1024 #[test]
1025 fn is_about_to_finish_never_fires_on_a_mismatched_clock_kind() {
1026 let _g = clock_guard();
1027 let t = timer_at(0, recording_cb as TimerCallbackType).with_timeout(sys_dur_millis(1));
1032 assert!(!t.is_about_to_finish(&tick(u64::MAX)));
1033 }
1034
1035 #[test]
1040 fn instant_of_next_run_is_created_when_nothing_is_armed() {
1041 let _g = clock_guard();
1042 let t = timer_at(42, recording_cb as TimerCallbackType);
1043 assert_eq!(tick_of(&t.instant_of_next_run()), 42);
1044 }
1045
1046 #[test]
1047 fn instant_of_next_run_prefers_last_run_over_created() {
1048 let _g = clock_guard();
1049 let mut t = timer_at(100, recording_cb as TimerCallbackType).with_interval(tick_dur(7));
1050 assert_eq!(tick_of(&t.instant_of_next_run()), 107, "no run yet: created + interval");
1051
1052 t.last_run = OptionInstant::Some(tick(500));
1053 assert_eq!(tick_of(&t.instant_of_next_run()), 507, "after a run: last_run + interval");
1054 }
1055
1056 #[test]
1057 fn instant_of_next_run_sums_delay_and_interval() {
1058 let _g = clock_guard();
1059 let mut t = timer_at(100, recording_cb as TimerCallbackType)
1064 .with_delay(tick_dur(5))
1065 .with_interval(tick_dur(7));
1066 assert_eq!(tick_of(&t.instant_of_next_run()), 112);
1067
1068 t.last_run = OptionInstant::Some(tick(200));
1069 assert_eq!(tick_of(&t.instant_of_next_run()), 212);
1070 }
1071
1072 #[test]
1073 fn instant_of_next_run_saturates_at_the_end_of_time() {
1074 let _g = clock_guard();
1075 let t = timer_at(u64::MAX, recording_cb as TimerCallbackType)
1076 .with_delay(tick_dur(u64::MAX))
1077 .with_interval(tick_dur(u64::MAX));
1078 assert_eq!(tick_of(&t.instant_of_next_run()), u64::MAX);
1080 }
1081
1082 #[test]
1083 fn instant_of_next_run_ignores_mismatched_duration_kinds() {
1084 let _g = clock_guard();
1085 let t = timer_at(42, recording_cb as TimerCallbackType)
1088 .with_delay(sys_dur_millis(1_000))
1089 .with_interval(sys_dur_millis(1_000));
1090 assert_eq!(tick_of(&t.instant_of_next_run()), 42);
1091 }
1092
1093 #[test]
1098 fn with_setters_are_independent_and_preserve_the_rest() {
1099 let _g = clock_guard();
1100 let t = timer_at(9, recording_cb as TimerCallbackType)
1101 .with_delay(tick_dur(1))
1102 .with_interval(tick_dur(2))
1103 .with_timeout(tick_dur(3));
1104
1105 assert_eq!(t.delay, OptionDuration::Some(tick_dur(1)));
1106 assert_eq!(t.interval, OptionDuration::Some(tick_dur(2)));
1107 assert_eq!(t.timeout, OptionDuration::Some(tick_dur(3)));
1108 assert_eq!(tick_of(&t.created), 9);
1110 assert_eq!(t.run_count, 0);
1111 assert_eq!(t.last_run, OptionInstant::None);
1112 }
1113
1114 #[test]
1115 fn with_setters_are_last_write_wins() {
1116 let _g = clock_guard();
1117 let t = timer_at(0, recording_cb as TimerCallbackType)
1118 .with_delay(tick_dur(1))
1119 .with_delay(tick_dur(2))
1120 .with_interval(tick_dur(3))
1121 .with_interval(tick_dur(4))
1122 .with_timeout(tick_dur(5))
1123 .with_timeout(tick_dur(6));
1124
1125 assert_eq!(t.delay, OptionDuration::Some(tick_dur(2)));
1126 assert_eq!(t.interval, OptionDuration::Some(tick_dur(4)));
1127 assert_eq!(t.timeout, OptionDuration::Some(tick_dur(6)));
1128 }
1129
1130 #[test]
1131 fn with_setters_accept_extreme_durations() {
1132 let _g = clock_guard();
1133 let t = timer_at(0, recording_cb as TimerCallbackType)
1134 .with_delay(tick_dur(0))
1135 .with_interval(tick_dur(u64::MAX))
1136 .with_timeout(Duration::max());
1137
1138 assert_eq!(t.delay, OptionDuration::Some(tick_dur(0)));
1139 assert_eq!(t.tick_millis(), u64::MAX);
1140 assert!(!t.is_about_to_finish(&tick(u64::MAX)));
1142 }
1143
1144 #[test]
1149 fn timer_callback_create_has_no_ffi_ctx() {
1150 let cb = TimerCallback::create(cb_alpha as TimerCallbackType);
1151 assert_eq!(cb.ctx, OptionRefAny::None);
1152
1153 let from: TimerCallback = (cb_alpha as TimerCallbackType).into();
1155 assert_eq!(cb, from);
1156 }
1157
1158 #[test]
1159 fn timer_callback_identity_is_by_function_pointer() {
1160 let a1 = TimerCallback::create(cb_alpha as TimerCallbackType);
1161 let a2 = TimerCallback::create(cb_alpha as TimerCallbackType);
1162 let b = TimerCallback::create(cb_beta as TimerCallbackType);
1163
1164 assert_eq!(a1, a2, "same fn -> equal");
1165 assert_ne!(a1, b, "different fn -> not equal");
1166 assert_eq!(a1, a1.clone(), "Clone preserves identity");
1167 }
1168
1169 #[test]
1170 fn timer_callback_ord_and_hash_agree_with_eq() {
1171 use std::{
1172 collections::hash_map::DefaultHasher,
1173 hash::{Hash, Hasher},
1174 };
1175
1176 fn hash_of(cb: &TimerCallback) -> u64 {
1177 let mut h = DefaultHasher::new();
1178 cb.hash(&mut h);
1179 h.finish()
1180 }
1181
1182 let a = TimerCallback::create(cb_alpha as TimerCallbackType);
1183 let a2 = a.clone();
1184 let b = TimerCallback::create(cb_beta as TimerCallbackType);
1185
1186 assert_eq!(a.cmp(&a2), core::cmp::Ordering::Equal);
1187 assert_eq!(hash_of(&a), hash_of(&a2), "Eq values must hash equal");
1188
1189 assert_eq!(a.cmp(&b), a.partial_cmp(&b).unwrap());
1191 assert_eq!(a.cmp(&b).reverse(), b.cmp(&a));
1192 assert_ne!(a.cmp(&b), core::cmp::Ordering::Equal, "distinct fns must order strictly");
1193 }
1194
1195 #[test]
1196 fn timer_callback_debug_does_not_panic() {
1197 let s = format!("{:?}", TimerCallback::create(cb_alpha as TimerCallbackType));
1198 assert!(s.starts_with("TimerCallback"), "got {s}");
1199 }
1200
1201 #[test]
1206 fn option_timer_round_trips_both_variants() {
1207 let _g = clock_guard();
1208 assert!(OptionTimer::None.into_option().is_none());
1209 assert!(OptionTimer::from(None).into_option().is_none());
1210
1211 let t = timer_at(3, recording_cb as TimerCallbackType).with_interval(tick_dur(4));
1212 let round_tripped = OptionTimer::from(Some(t.clone()))
1213 .into_option()
1214 .expect("Some must survive the round-trip");
1215 assert_eq!(round_tripped, t, "encode == decode");
1216 }
1217
1218 #[test]
1223 fn timer_callback_info_create_preserves_extremes() {
1224 with_env(|env| {
1225 let info = TimerCallbackInfo::create(
1226 env.info(),
1227 OptionDomNodeId::None,
1228 tick(u64::MAX),
1229 usize::MAX,
1230 true,
1231 );
1232 assert_eq!(info.call_count, usize::MAX, "no wrap at usize::MAX");
1233 assert!(info.is_about_to_finish);
1234 assert_eq!(tick_of(&info.frame_start), u64::MAX);
1235 assert!(info._abi_ref.is_null());
1236 assert!(info._abi_mut.is_null());
1237
1238 let zero =
1239 TimerCallbackInfo::create(env.info(), OptionDomNodeId::None, tick(0), 0, false);
1240 assert_eq!(zero.call_count, 0);
1241 assert!(!zero.is_about_to_finish);
1242 assert_eq!(tick_of(&zero.frame_start), 0);
1243 });
1244 }
1245
1246 #[test]
1247 fn get_current_time_returns_frame_start_verbatim() {
1248 with_env(|env| {
1249 for t in [0_u64, 1, u64::MAX] {
1250 let info =
1251 TimerCallbackInfo::create(env.info(), OptionDomNodeId::None, tick(t), 0, false);
1252 assert_eq!(info.get_current_time(), tick(t));
1253 }
1254 });
1255 }
1256
1257 #[test]
1258 fn attached_node_queries_are_none_without_an_attached_node() {
1259 with_env(|env| {
1260 let info = env.timer_info();
1261 assert!(info.get_attached_node_size().is_none());
1262 assert!(info.get_attached_node_position().is_none());
1263 });
1264 }
1265
1266 #[test]
1267 fn attached_node_queries_are_none_for_a_bogus_node() {
1268 with_env(|env| {
1269 let bogus = DomNodeId {
1272 dom: DomId { inner: usize::MAX },
1273 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(usize::MAX - 1))),
1274 };
1275 let info = TimerCallbackInfo::create(
1276 env.info(),
1277 OptionDomNodeId::Some(bogus),
1278 tick(0),
1279 0,
1280 false,
1281 );
1282 assert!(info.get_attached_node_size().is_none(), "must not panic or index OOB");
1283 assert!(info.get_attached_node_position().is_none());
1284 });
1285 }
1286
1287 #[test]
1288 fn get_callback_info_and_mut_alias_the_same_inner_info() {
1289 with_env(|env| {
1290 let mut info = env.timer_info();
1291 let addr_shared = std::ptr::from_ref(info.get_callback_info());
1292 let addr_mut = std::ptr::from_mut(info.get_callback_info_mut()).cast_const();
1293 assert!(std::ptr::eq(addr_shared, addr_mut), "both must alias the inner CallbackInfo");
1294
1295 info.get_callback_info_mut().prevent_default();
1297 assert!(matches!(env.take_one(), CallbackChange::PreventDefault));
1298 });
1299 }
1300
1301 #[test]
1302 fn get_ctx_is_none_for_native_rust_callbacks() {
1303 with_env(|env| {
1304 assert_eq!(env.timer_info().get_ctx(), OptionRefAny::None);
1305 });
1306 }
1307
1308 #[test]
1309 fn get_ctx_hands_back_the_ffi_callable() {
1310 let ctx = RefAny::new(0xDEAD_BEEF_u32);
1311 with_env_cfg(false, OptionRefAny::Some(ctx.clone()), |env| {
1312 let got = env.timer_info().get_ctx().into_option().expect("ctx must survive");
1313 assert_eq!(got, ctx, "get_ctx must hand back the same RefAny");
1314 });
1315 }
1316
1317 #[test]
1322 fn add_and_remove_timer_push_the_matching_changes() {
1323 let _g = clock_guard();
1324 with_env(|env| {
1325 let mut info = env.timer_info();
1326 let id = TimerId { id: usize::MAX };
1327 info.add_timer(id, timer_at(1, recording_cb as TimerCallbackType));
1328 let CallbackChange::AddTimer { timer_id, timer } = env.take_one() else {
1329 panic!("expected AddTimer");
1330 };
1331 assert_eq!(timer_id, id);
1332 assert_eq!(tick_of(&timer.created), 1, "the timer must be stored verbatim");
1333
1334 info.remove_timer(TimerId { id: 0 });
1335 let CallbackChange::RemoveTimer { timer_id } = env.take_one() else {
1336 panic!("expected RemoveTimer");
1337 };
1338 assert_eq!(timer_id.id, 0, "id 0 (a reserved system id) is still accepted");
1339 });
1340 }
1341
1342 #[test]
1343 fn add_and_remove_thread_push_the_matching_changes() {
1344 extern "C" fn noop_worker(_d: RefAny, _s: ThreadSender, _r: ThreadReceiver) {}
1345
1346 with_env(|env| {
1347 let mut info = env.timer_info();
1348 let id = ThreadId::unique();
1349 let thread = Thread::create(
1350 RefAny::new(0_usize),
1351 RefAny::new(0_usize),
1352 noop_worker as ThreadCallbackType,
1353 );
1354 info.add_thread(id, thread);
1355 let CallbackChange::AddThread { thread_id, .. } = env.take_one() else {
1356 panic!("expected AddThread");
1357 };
1358 assert_eq!(thread_id, id);
1359
1360 info.remove_thread(id);
1361 let CallbackChange::RemoveThread { thread_id } = env.take_one() else {
1362 panic!("expected RemoveThread");
1363 };
1364 assert_eq!(thread_id, id);
1365 });
1366 }
1367
1368 #[test]
1369 fn nullary_mutators_push_exactly_one_change_each_in_order() {
1370 with_env(|env| {
1371 let mut info = env.timer_info();
1372 info.stop_propagation();
1373 info.prevent_default();
1374 info.close_window();
1375 info.hide_tooltip();
1376 info.reload_system_fonts();
1377 info.update_all_image_callbacks();
1378 info.reset_cursor_blink();
1379 info.set_cursor_visibility_toggle();
1380
1381 let changes = env.take_changes();
1382 assert_eq!(changes.len(), 8, "one change per call, no drops: {changes:?}");
1383 assert!(matches!(changes[0], CallbackChange::StopPropagation));
1384 assert!(matches!(changes[1], CallbackChange::PreventDefault));
1385 assert!(matches!(changes[2], CallbackChange::CloseWindow));
1386 assert!(matches!(changes[3], CallbackChange::HideTooltip));
1387 assert!(matches!(changes[4], CallbackChange::ReloadSystemFonts));
1388 assert!(matches!(changes[5], CallbackChange::UpdateAllImageCallbacks));
1389 assert!(matches!(changes[6], CallbackChange::ResetCursorBlink));
1390 assert!(matches!(changes[7], CallbackChange::ToggleCursorVisibility));
1391 });
1392 }
1393
1394 #[test]
1395 fn set_cursor_visibility_records_both_polarities() {
1396 with_env(|env| {
1397 let mut info = env.timer_info();
1398 info.set_cursor_visibility(true);
1399 info.set_cursor_visibility(false);
1400
1401 let changes = env.take_changes();
1402 assert_eq!(changes.len(), 2);
1403 let visibilities: Vec<bool> = changes
1404 .iter()
1405 .map(|c| match c {
1406 CallbackChange::SetCursorVisibility { visible } => *visible,
1407 other => panic!("expected SetCursorVisibility, got {other:?}"),
1408 })
1409 .collect();
1410 assert_eq!(visibilities, vec![true, false]);
1411 });
1412 }
1413
1414 #[test]
1415 fn create_window_and_modify_window_state_push_changes() {
1416 with_env(|env| {
1417 let mut info = env.timer_info();
1418 info.create_window(WindowCreateOptions::default());
1419 assert!(matches!(env.take_one(), CallbackChange::CreateNewWindow { .. }));
1420
1421 info.modify_window_state(FullWindowState::default());
1422 assert!(matches!(env.take_one(), CallbackChange::ModifyWindowState { .. }));
1423 });
1424 }
1425
1426 #[test]
1427 fn image_cache_mutators_accept_degenerate_and_unicode_ids() {
1428 with_env(|env| {
1429 let mut info = env.timer_info();
1430
1431 let img = ImageRef::null_image(0, 0, RawImageFormat::RGBA8, Vec::new());
1433 let id: AzString = String::new().into();
1434 info.add_image_to_cache(id.clone(), img);
1435 let CallbackChange::AddImageToCache { id: got, .. } = env.take_one() else {
1436 panic!("expected AddImageToCache");
1437 };
1438 assert_eq!(got, id, "an empty id is passed through, not rejected");
1439
1440 let nasty: AzString = String::from("🚀\u{0}\u{202E}id\u{1F600}").into();
1443 info.remove_image_from_cache(nasty.clone());
1444 let CallbackChange::RemoveImageFromCache { id: got } = env.take_one() else {
1445 panic!("expected RemoveImageFromCache");
1446 };
1447 assert_eq!(got.as_str(), nasty.as_str());
1448 });
1449 }
1450
1451 #[test]
1452 fn trigger_virtual_view_rerender_accepts_out_of_range_ids() {
1453 with_env(|env| {
1454 let mut info = env.timer_info();
1455 info.trigger_virtual_view_rerender(DomId { inner: usize::MAX }, NodeId::new(usize::MAX));
1456 let CallbackChange::UpdateVirtualView { dom_id, node_id } = env.take_one() else {
1457 panic!("expected UpdateVirtualView");
1458 };
1459 assert_eq!(dom_id.inner, usize::MAX);
1462 assert_eq!(node_id, NodeId::new(usize::MAX));
1463 });
1464 }
1465
1466 #[test]
1467 fn open_menu_has_no_position_and_open_menu_at_carries_one() {
1468 with_env(|env| {
1469 let mut info = env.timer_info();
1470
1471 info.open_menu(empty_menu());
1472 let CallbackChange::OpenMenu { position, .. } = env.take_one() else {
1473 panic!("expected OpenMenu");
1474 };
1475 assert!(position.is_none(), "open_menu must defer to menu.position");
1476
1477 info.open_menu_at(empty_menu(), LogicalPosition::new(-1.5, 2.5));
1478 let CallbackChange::OpenMenu { position, .. } = env.take_one() else {
1479 panic!("expected OpenMenu");
1480 };
1481 let p = position.expect("open_menu_at must pin a position");
1482 assert_eq!((p.x, p.y), (-1.5, 2.5), "negative coordinates are legal");
1483 });
1484 }
1485
1486 #[test]
1487 fn open_menu_at_passes_non_finite_coordinates_through_unchanged() {
1488 with_env(|env| {
1489 let mut info = env.timer_info();
1490 info.open_menu_at(
1491 empty_menu(),
1492 LogicalPosition::new(f32::NAN, f32::INFINITY),
1493 );
1494 let CallbackChange::OpenMenu { position, .. } = env.take_one() else {
1495 panic!("expected OpenMenu");
1496 };
1497 let p = position.expect("position must be recorded");
1498 assert!(p.x.is_nan());
1500 assert!(p.y.is_infinite() && p.y.is_sign_positive());
1501
1502 info.open_menu_at(empty_menu(), LogicalPosition::new(f32::MAX, f32::MIN));
1503 let CallbackChange::OpenMenu { position, .. } = env.take_one() else {
1504 panic!("expected OpenMenu");
1505 };
1506 let p = position.expect("position must be recorded");
1507 assert_eq!((p.x, p.y), (f32::MAX, f32::MIN));
1508 });
1509 }
1510
1511 #[test]
1512 fn open_menu_for_hit_node_is_false_and_silent_without_a_hit_node() {
1513 with_env(|env| {
1514 let mut info = env.timer_info();
1515 assert!(!info.open_menu_for_hit_node(empty_menu()));
1518 assert!(
1519 env.take_changes().is_empty(),
1520 "a failed anchor must not queue a half-open menu"
1521 );
1522 });
1523 }
1524
1525 #[test]
1526 fn show_tooltip_falls_back_to_the_origin_without_a_cursor() {
1527 with_env(|env| {
1528 let mut info = env.timer_info();
1529 info.show_tooltip(String::from("hi").into());
1530 let CallbackChange::ShowTooltip { text, position } = env.take_one() else {
1531 panic!("expected ShowTooltip");
1532 };
1533 assert_eq!(text.as_str(), "hi");
1534 assert_eq!((position.x, position.y), (0.0, 0.0), "no cursor -> origin");
1535 });
1536 }
1537
1538 #[test]
1539 fn show_tooltip_uses_the_viewport_cursor_when_there_is_one() {
1540 with_env(|env| {
1541 let cursor = LogicalPosition::new(3.0, 4.0);
1542 let mut info = TimerCallbackInfo::create(
1543 env.info_with(OptionLogicalPosition::None, OptionLogicalPosition::Some(cursor)),
1544 OptionDomNodeId::None,
1545 tick(0),
1546 0,
1547 false,
1548 );
1549 info.show_tooltip(String::from("t").into());
1550 let CallbackChange::ShowTooltip { position, .. } = env.take_one() else {
1551 panic!("expected ShowTooltip");
1552 };
1553 assert_eq!((position.x, position.y), (3.0, 4.0));
1554 });
1555 }
1556
1557 #[test]
1558 fn show_tooltip_at_records_empty_text_and_non_finite_positions() {
1559 with_env(|env| {
1560 let mut info = env.timer_info();
1561 info.show_tooltip_at(String::new().into(), LogicalPosition::new(f32::NAN, -0.0));
1562 let CallbackChange::ShowTooltip { text, position } = env.take_one() else {
1563 panic!("expected ShowTooltip");
1564 };
1565 assert_eq!(text.as_str(), "", "empty tooltip text is not rejected");
1566 assert!(position.x.is_nan());
1567 assert!(position.y.is_sign_negative());
1568 });
1569 }
1570
1571 #[test]
1576 fn scroll_to_and_unclamped_differ_only_in_the_clamp_flag() {
1577 with_env(|env| {
1578 let mut info = env.timer_info();
1579 let node = NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(0)));
1580 let pos = LogicalPosition::new(10.0, 20.0);
1581
1582 info.scroll_to(DomId::ROOT_ID, node, pos);
1583 info.scroll_to_unclamped(DomId::ROOT_ID, node, pos);
1584
1585 let changes = env.take_changes();
1586 assert_eq!(changes.len(), 2);
1587 let flags: Vec<bool> = changes
1588 .iter()
1589 .map(|c| match c {
1590 CallbackChange::ScrollTo {
1591 dom_id,
1592 node_id,
1593 position,
1594 unclamped,
1595 } => {
1596 assert_eq!(*dom_id, DomId::ROOT_ID);
1597 assert_eq!(*node_id, node);
1598 assert_eq!((position.x, position.y), (10.0, 20.0));
1599 *unclamped
1600 }
1601 other => panic!("expected ScrollTo, got {other:?}"),
1602 })
1603 .collect();
1604 assert_eq!(flags, vec![false, true], "only the overscroll flag differs");
1605 });
1606 }
1607
1608 #[test]
1609 fn scroll_to_records_zero_negative_and_non_finite_positions() {
1610 with_env(|env| {
1611 let mut info = env.timer_info();
1612 let node = NodeHierarchyItemId::NONE;
1613
1614 for pos in [
1615 LogicalPosition::new(0.0, 0.0),
1616 LogicalPosition::new(-1.0, -f32::MAX),
1617 LogicalPosition::new(f32::MAX, f32::INFINITY),
1618 ] {
1619 info.scroll_to(DomId::ROOT_ID, node, pos);
1620 let CallbackChange::ScrollTo { position, .. } = env.take_one() else {
1621 panic!("expected ScrollTo");
1622 };
1623 assert_eq!(position.x.to_bits(), pos.x.to_bits(), "x must be recorded bit-exact");
1624 assert_eq!(position.y.to_bits(), pos.y.to_bits(), "y must be recorded bit-exact");
1625 }
1626
1627 info.scroll_to_unclamped(
1629 DomId { inner: usize::MAX },
1630 node,
1631 LogicalPosition::new(f32::NAN, f32::NAN),
1632 );
1633 let CallbackChange::ScrollTo {
1634 position, unclamped, ..
1635 } = env.take_one()
1636 else {
1637 panic!("expected ScrollTo");
1638 };
1639 assert!(position.x.is_nan() && position.y.is_nan(), "NaN is passed through, not zeroed");
1640 assert!(unclamped);
1641 });
1642 }
1643
1644 #[test]
1645 fn scroll_queries_are_none_on_an_empty_window() {
1646 with_env(|env| {
1647 let info = env.timer_info();
1648 assert!(info.get_scroll_node_info(DomId::ROOT_ID, NodeId::new(0)).is_none());
1649 assert!(
1650 info.get_scroll_node_info(DomId { inner: usize::MAX }, NodeId::new(usize::MAX))
1651 .is_none(),
1652 "an out-of-range dom/node must return None, not panic"
1653 );
1654 assert!(info.find_scroll_parent(DomId::ROOT_ID, NodeId::new(0)).is_none());
1655 assert!(
1656 info.find_scroll_parent(DomId { inner: usize::MAX }, NodeId::new(usize::MAX))
1657 .is_none()
1658 );
1659 });
1660 }
1661
1662 #[test]
1663 fn scroll_input_queue_starts_empty_and_draining_is_idempotent() {
1664 with_env(|env| {
1665 let info = env.timer_info();
1666 let queue = info.get_scroll_input_queue();
1667 assert!(queue.take_all().is_empty());
1668 assert!(queue.take_all().is_empty(), "draining twice must stay empty");
1669 });
1670 }
1671
1672 #[test]
1677 fn the_three_drag_predicates_are_aliases_of_left_down() {
1678 for left_down in [false, true] {
1679 with_env_cfg(left_down, OptionRefAny::None, |env| {
1680 let info = env.timer_info();
1681 assert_eq!(info.get_current_mouse_state().left_down, left_down);
1682 assert_eq!(info.is_dragging(), left_down);
1683 assert_eq!(info.is_drag_active(), left_down);
1684 assert_eq!(info.is_node_drag_active(), left_down);
1685 });
1686 }
1687 }
1688
1689 #[test]
1690 fn pen_predicates_are_false_without_a_pen() {
1691 with_env(|env| {
1692 let info = env.timer_info();
1693 assert!(!info.is_pen_in_contact());
1694 assert!(!info.is_pen_eraser());
1695 assert!(!info.is_pen_barrel_button_pressed());
1696 });
1697 }
1698
1699 #[test]
1700 fn drag_and_gesture_predicates_are_false_on_a_fresh_window() {
1701 with_env(|env| {
1702 let info = env.timer_info();
1703 assert!(!info.is_file_drag_active());
1704 assert!(!info.has_sufficient_history_for_gestures());
1705 });
1706 }
1707
1708 #[test]
1709 fn is_dom_focused_is_false_for_every_dom_when_nothing_is_focused() {
1710 with_env(|env| {
1711 let info = env.timer_info();
1712 assert!(!info.is_dom_focused(DomId::ROOT_ID));
1713 assert!(!info.is_dom_focused(DomId { inner: usize::MAX }));
1714 });
1715 }
1716
1717 #[test]
1718 fn window_state_getters_mirror_the_current_window_state() {
1719 with_env(|env| {
1720 let info = env.timer_info();
1721 let default_state = FullWindowState::default();
1722 assert_eq!(info.get_current_window_flags(), default_state.flags);
1723 assert_eq!(info.get_current_keyboard_state(), default_state.keyboard_state);
1724 assert_eq!(info.get_current_mouse_state(), default_state.mouse_state);
1725 });
1726 }
1727
1728 #[test]
1729 fn cursor_getters_round_trip_including_nan() {
1730 with_env(|env| {
1731 let info = env.timer_info();
1732 assert!(info.get_cursor_position().is_none());
1733 assert_eq!(info.get_cursor_relative_to_viewport(), OptionLogicalPosition::None);
1734 assert!(info.get_cursor_relative_to_node().is_none());
1735
1736 let viewport = LogicalPosition::new(f32::NAN, 7.5);
1737 let relative = LogicalPosition::new(-3.0, f32::INFINITY);
1738 let info = TimerCallbackInfo::create(
1739 env.info_with(
1740 OptionLogicalPosition::Some(relative),
1741 OptionLogicalPosition::Some(viewport),
1742 ),
1743 OptionDomNodeId::None,
1744 tick(0),
1745 0,
1746 false,
1747 );
1748
1749 let got = info.get_cursor_position().expect("cursor must be Some");
1750 assert!(got.x.is_nan() && got.y == 7.5);
1751
1752 let node_rel = info
1753 .get_cursor_relative_to_node()
1754 .into_option()
1755 .expect("relative cursor must be Some");
1756 assert_eq!(node_rel.x, -3.0);
1757 assert!(node_rel.y.is_infinite());
1758 });
1759 }
1760
1761 #[test]
1766 fn invoke_does_not_run_the_callback_before_the_delay_elapses() {
1767 let _g = clock_guard();
1768 with_env(|env| {
1769 let mut t =
1770 timer_at(0, recording_cb as TimerCallbackType).with_delay(tick_dur(100));
1771 let info = env.info();
1772
1773 set_now(99);
1774 let r = t.invoke(&info, &fake_clock_cb());
1775 assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 0, "callback must not fire early");
1776 assert_eq!(r.should_update, Update::DoNothing);
1777 assert_eq!(r.should_terminate, TerminateTimer::Continue);
1778 assert_eq!(t.run_count, 0);
1780 assert_eq!(t.last_run, OptionInstant::None);
1781
1782 set_now(100);
1784 let r = t.invoke(&info, &fake_clock_cb());
1785 assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 1);
1786 assert_eq!(r.should_terminate, TerminateTimer::Continue);
1787 assert_eq!(t.run_count, 1);
1788 assert_eq!(t.last_run, OptionInstant::Some(tick(100)));
1789 });
1790 }
1791
1792 #[test]
1793 fn invoke_gates_subsequent_runs_on_the_interval() {
1794 let _g = clock_guard();
1795 with_env(|env| {
1796 let mut t =
1797 timer_at(0, recording_cb as TimerCallbackType).with_interval(tick_dur(10));
1798 let info = env.info();
1799
1800 let r = t.invoke(&info, &fake_clock_cb());
1802 assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 1);
1803 assert_eq!(r.should_terminate, TerminateTimer::Continue);
1804 assert_eq!(t.run_count, 1);
1805
1806 set_now(9);
1807 let r = t.invoke(&info, &fake_clock_cb());
1808 assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 1, "1 tick short of the interval");
1809 assert_eq!(r.should_update, Update::DoNothing);
1810 assert_eq!(t.run_count, 1, "a skipped tick must not count as a run");
1811
1812 set_now(10);
1813 let _ = t.invoke(&info, &fake_clock_cb());
1814 assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 2);
1815 assert_eq!(t.run_count, 2);
1816 assert_eq!(t.last_run, OptionInstant::Some(tick(10)));
1817 });
1818 }
1819
1820 #[test]
1821 fn invoke_hands_the_callback_the_run_count_and_frame_start() {
1822 let _g = clock_guard();
1823 with_env(|env| {
1824 let mut t = timer_at(0, recording_cb as TimerCallbackType);
1825 let info = env.info();
1826
1827 set_now(5);
1828 let _ = t.invoke(&info, &fake_clock_cb());
1829 assert_eq!(CB_SEEN_CALL_COUNT.load(Ordering::SeqCst), 0, "first run is call 0");
1830 assert_eq!(CB_SEEN_FRAME_START.load(Ordering::SeqCst), 5, "frame_start == now");
1831 assert!(!CB_SEEN_ABOUT_TO_FINISH.load(Ordering::SeqCst));
1832
1833 set_now(6);
1834 let _ = t.invoke(&info, &fake_clock_cb());
1835 assert_eq!(CB_SEEN_CALL_COUNT.load(Ordering::SeqCst), 1, "run_count increments by 1");
1836 assert_eq!(CB_SEEN_FRAME_START.load(Ordering::SeqCst), 6);
1837 });
1838 }
1839
1840 #[test]
1841 fn invoke_forces_terminate_once_the_timeout_expires() {
1842 let _g = clock_guard();
1843 with_env(|env| {
1844 let mut t = timer_at(0, recording_cb as TimerCallbackType).with_timeout(tick_dur(5));
1845 let info = env.info();
1846 CB_RETURN_TERMINATE.store(false, Ordering::SeqCst);
1848
1849 set_now(5);
1850 let r = t.invoke(&info, &fake_clock_cb());
1851 assert_eq!(r.should_terminate, TerminateTimer::Continue, "elapsed == timeout: alive");
1852 assert!(!CB_SEEN_ABOUT_TO_FINISH.load(Ordering::SeqCst));
1853
1854 set_now(6);
1855 let r = t.invoke(&info, &fake_clock_cb());
1856 assert!(CB_SEEN_ABOUT_TO_FINISH.load(Ordering::SeqCst), "last-call flag must be set");
1858 assert_eq!(r.should_terminate, TerminateTimer::Terminate);
1859 assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 2, "the final run still happens");
1860 });
1861 }
1862
1863 #[test]
1864 fn invoke_honours_a_callback_requested_terminate() {
1865 let _g = clock_guard();
1866 with_env(|env| {
1867 let mut t = timer_at(0, recording_cb as TimerCallbackType);
1868 let info = env.info();
1869 CB_RETURN_TERMINATE.store(true, Ordering::SeqCst);
1870
1871 let r = t.invoke(&info, &fake_clock_cb());
1872 assert_eq!(r.should_terminate, TerminateTimer::Terminate);
1873 assert_eq!(t.run_count, 1);
1875 assert_eq!(t.last_run, OptionInstant::Some(tick(0)));
1876 });
1877 }
1878
1879 #[test]
1880 fn invoke_skips_deterministically_when_the_clock_runs_backwards() {
1881 let _g = clock_guard();
1882 with_env(|env| {
1883 let mut t =
1884 timer_at(0, recording_cb as TimerCallbackType).with_interval(tick_dur(10));
1885 t.last_run = OptionInstant::Some(tick(1_000));
1886 let info = env.info();
1887
1888 set_now(0);
1891 let r = t.invoke(&info, &fake_clock_cb());
1892 assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 0);
1893 assert_eq!(r.should_terminate, TerminateTimer::Continue);
1894 assert_eq!(t.run_count, 0);
1895 assert_eq!(t.last_run, OptionInstant::Some(tick(1_000)), "last_run is untouched");
1896 });
1897 }
1898
1899 #[test]
1900 fn invoke_with_a_mismatched_interval_kind_runs_every_tick() {
1901 let _g = clock_guard();
1902 with_env(|env| {
1903 let mut t =
1907 timer_at(0, recording_cb as TimerCallbackType).with_interval(sys_dur_millis(60_000));
1908 let info = env.info();
1909
1910 let _ = t.invoke(&info, &fake_clock_cb());
1911 set_now(1);
1912 let _ = t.invoke(&info, &fake_clock_cb());
1913 assert_eq!(
1914 CB_INVOCATIONS.load(Ordering::SeqCst),
1915 2,
1916 "a 60s interval does not throttle a tick clock"
1917 );
1918 assert_eq!(t.run_count, 2);
1919 });
1920 }
1921
1922 #[test]
1923 fn invoke_at_the_end_of_time_does_not_panic() {
1924 let _g = clock_guard();
1925 with_env(|env| {
1926 let mut t = timer_at(u64::MAX, recording_cb as TimerCallbackType)
1927 .with_delay(tick_dur(u64::MAX))
1928 .with_interval(tick_dur(u64::MAX))
1929 .with_timeout(tick_dur(u64::MAX));
1930 let info = env.info();
1931
1932 set_now(u64::MAX);
1933 let r = t.invoke(&info, &fake_clock_cb());
1935 assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 0);
1936 assert_eq!(r.should_terminate, TerminateTimer::Continue);
1937 assert_eq!(t.run_count, 0);
1938 });
1939 }
1940}