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 {
151 match self.interval.as_ref() {
152 Some(d) => d.as_millis_u64(),
153 None => DEFAULT_TIMER_TICK_MS,
154 }
155 }
156
157 #[must_use] pub fn is_about_to_finish(&self, instant_now: &Instant) -> bool {
158 match self.timeout {
159 OptionDuration::Some(timeout) => {
160 instant_now.duration_since(&self.created).greater_than(&timeout)
161 }
162 OptionDuration::None => false,
163 }
164 }
165
166 #[must_use] pub fn instant_of_next_run(&self) -> Instant {
167 let last_run = self.last_run.as_ref().map_or(&self.created, |s| s);
168
169 last_run
170 .clone()
171 .add_optional_duration(self.delay.as_ref())
172 .add_optional_duration(self.interval.as_ref())
173 }
174
175 #[inline]
176 #[must_use] pub const fn with_delay(mut self, delay: Duration) -> Self {
177 self.delay = OptionDuration::Some(delay);
178 self
179 }
180
181 #[inline]
182 #[must_use] pub const fn with_interval(mut self, interval: Duration) -> Self {
183 self.interval = OptionDuration::Some(interval);
184 self
185 }
186
187 #[inline]
188 #[must_use] pub const fn with_timeout(mut self, timeout: Duration) -> Self {
189 self.timeout = OptionDuration::Some(timeout);
190 self
191 }
192
193 pub fn invoke(
199 &mut self,
200 callback_info: &CallbackInfo,
201 get_system_time_fn: &GetSystemTimeCallback,
202 ) -> TimerCallbackReturn {
203 let now = (get_system_time_fn.cb)();
204
205 match self.last_run.as_ref() {
207 Some(last_run) => {
208 if let OptionDuration::Some(interval) = self.interval {
210 if now.duration_since(last_run).smaller_than(&interval) {
211 return TimerCallbackReturn {
212 should_update: Update::DoNothing,
213 should_terminate: TerminateTimer::Continue,
214 };
215 }
216 }
217 }
218 None => {
219 if let OptionDuration::Some(delay) = self.delay {
221 if now.duration_since(&self.created).smaller_than(&delay) {
222 return TimerCallbackReturn {
223 should_update: Update::DoNothing,
224 should_terminate: TerminateTimer::Continue,
225 };
226 }
227 }
228 }
229 }
230
231 let is_about_to_finish = self.is_about_to_finish(&now);
232
233 let mut timer_callback_info = TimerCallbackInfo {
236 callback_info: *callback_info,
237 node_id: self.node_id,
238 frame_start: now.clone(),
239 call_count: self.run_count,
240 is_about_to_finish,
241 _abi_ref: core::ptr::null(),
242 _abi_mut: core::ptr::null_mut(),
243 };
244
245 let mut result = (self.callback.cb)(self.refany.clone(), timer_callback_info);
246
247 if is_about_to_finish {
248 result.should_terminate = TerminateTimer::Terminate;
249 }
250
251 self.run_count += 1;
252 self.last_run = OptionInstant::Some(now);
253
254 result
255 }
256}
257
258impl Default for Timer {
259 fn default() -> Self {
260 extern "C" fn default_callback(_: RefAny, _: TimerCallbackInfo) -> TimerCallbackReturn {
261 TimerCallbackReturn::terminate_unchanged()
262 }
263
264 const extern "C" fn default_time() -> Instant {
265 Instant::Tick(azul_core::task::SystemTick { tick_counter: 0 })
266 }
267
268 let cb: TimerCallbackType = default_callback;
269 Self::create(
270 RefAny::new(()),
271 cb,
272 GetSystemTimeCallback { cb: default_time },
273 )
274 }
275}
276
277#[derive(Debug, Clone)]
282#[repr(C)]
283#[allow(clippy::pub_underscore_fields)] pub struct TimerCallbackInfo {
285 pub callback_info: CallbackInfo,
286 pub node_id: OptionDomNodeId,
287 pub frame_start: Instant,
288 pub call_count: usize,
289 pub is_about_to_finish: bool,
290 pub _abi_ref: *const c_void,
291 pub _abi_mut: *mut c_void,
292}
293
294impl TimerCallbackInfo {
295 #[must_use] pub const fn create(
296 callback_info: CallbackInfo,
297 node_id: OptionDomNodeId,
298 frame_start: Instant,
299 call_count: usize,
300 is_about_to_finish: bool,
301 ) -> Self {
302 Self {
303 callback_info,
304 node_id,
305 frame_start,
306 call_count,
307 is_about_to_finish,
308 _abi_ref: core::ptr::null(),
309 _abi_mut: core::ptr::null_mut(),
310 }
311 }
312
313 #[must_use] pub fn get_attached_node_size(&self) -> Option<LogicalSize> {
314 let node_id = self.node_id.into_option()?;
315 self.callback_info.get_node_size(node_id)
316 }
317
318 #[must_use] pub fn get_attached_node_position(&self) -> Option<LogicalPosition> {
319 let node_id = self.node_id.into_option()?;
320 self.callback_info.get_node_position(node_id)
321 }
322
323 #[must_use] pub const fn get_callback_info(&self) -> &CallbackInfo {
324 &self.callback_info
325 }
326
327 pub const fn get_callback_info_mut(&mut self) -> &mut CallbackInfo {
328 &mut self.callback_info
329 }
330
331 #[must_use] pub fn get_ctx(&self) -> OptionRefAny {
337 self.callback_info.get_ctx()
338 }
339
340 pub fn add_timer(&mut self, timer_id: TimerId, timer: Timer) {
342 self.callback_info.add_timer(timer_id, timer);
343 }
344
345 pub fn remove_timer(&mut self, timer_id: TimerId) {
347 self.callback_info.remove_timer(timer_id);
348 }
349
350 pub fn add_thread(&mut self, thread_id: ThreadId, thread: Thread) {
352 self.callback_info.add_thread(thread_id, thread);
353 }
354
355 pub fn remove_thread(&mut self, thread_id: ThreadId) {
357 self.callback_info.remove_thread(thread_id);
358 }
359
360 pub fn stop_propagation(&mut self) {
362 self.callback_info.stop_propagation();
363 }
364
365 pub fn create_window(&mut self, options: WindowCreateOptions) {
367 self.callback_info.create_window(options);
368 }
369
370 pub fn close_window(&mut self) {
372 self.callback_info.close_window();
373 }
374
375 pub fn modify_window_state(&mut self, state: FullWindowState) {
377 self.callback_info.modify_window_state(state);
378 }
379
380 pub fn add_image_to_cache(&mut self, id: AzString, image: ImageRef) {
382 self.callback_info.add_image_to_cache(id, image);
383 }
384
385 pub fn remove_image_from_cache(&mut self, id: AzString) {
387 self.callback_info.remove_image_from_cache(id);
388 }
389
390 pub fn update_all_image_callbacks(&mut self) {
395 self.callback_info.update_all_image_callbacks();
396 }
397
398 pub fn trigger_virtual_view_rerender(&mut self, dom_id: DomId, node_id: NodeId) {
400 self.callback_info.trigger_virtual_view_rerender(dom_id, node_id);
401 }
402
403 pub fn reload_system_fonts(&mut self) {
405 self.callback_info.reload_system_fonts();
406 }
407
408 pub fn prevent_default(&mut self) {
410 self.callback_info.prevent_default();
411 }
412
413 pub fn open_menu(&mut self, menu: Menu) {
415 self.callback_info.open_menu(menu);
416 }
417
418 pub fn open_menu_at(&mut self, menu: Menu, position: LogicalPosition) {
420 self.callback_info.open_menu_at(menu, position);
421 }
422
423 pub fn show_tooltip(&mut self, text: AzString) {
425 self.callback_info.show_tooltip(text);
426 }
427
428 pub fn show_tooltip_at(&mut self, text: AzString, position: LogicalPosition) {
430 self.callback_info.show_tooltip_at(text, position);
431 }
432
433 pub fn hide_tooltip(&mut self) {
435 self.callback_info.hide_tooltip();
436 }
437
438 pub fn open_menu_for_hit_node(&mut self, menu: Menu) -> bool {
440 self.callback_info.open_menu_for_hit_node(menu)
441 }
442
443 #[must_use] pub const fn get_current_window_flags(&self) -> WindowFlags {
445 self.callback_info.get_current_window_flags()
446 }
447
448 #[must_use] pub fn get_current_keyboard_state(&self) -> KeyboardState {
450 self.callback_info.get_current_keyboard_state()
451 }
452
453 #[must_use] pub const fn get_current_mouse_state(&self) -> MouseState {
455 self.callback_info.get_current_mouse_state()
456 }
457
458 #[must_use] pub const fn get_cursor_relative_to_node(&self) -> azul_core::geom::OptionCursorNodePosition {
460 self.callback_info.get_cursor_relative_to_node()
461 }
462
463 #[must_use] pub const fn get_cursor_relative_to_viewport(&self) -> OptionLogicalPosition {
465 self.callback_info.get_cursor_relative_to_viewport()
466 }
467
468 #[must_use] pub fn get_cursor_position(&self) -> Option<LogicalPosition> {
470 self.callback_info.get_cursor_position()
471 }
472
473 #[must_use] pub fn get_current_time(&self) -> Instant {
475 self.frame_start.clone()
476 }
477
478 #[must_use] pub fn is_dom_focused(&self, dom_id: DomId) -> bool {
480 self.callback_info.is_dom_focused(dom_id)
481 }
482
483 #[must_use] pub fn is_pen_in_contact(&self) -> bool {
485 self.callback_info.is_pen_in_contact()
486 }
487
488 #[must_use] pub fn is_pen_eraser(&self) -> bool {
490 self.callback_info.is_pen_eraser()
491 }
492
493 #[must_use] pub fn is_pen_barrel_button_pressed(&self) -> bool {
495 self.callback_info.is_pen_barrel_button_pressed()
496 }
497
498 #[must_use] pub const fn is_dragging(&self) -> bool {
500 self.callback_info.get_current_mouse_state().left_down
501 }
502
503 #[must_use] pub const fn is_drag_active(&self) -> bool {
505 self.callback_info.get_current_mouse_state().left_down
506 }
507
508 #[must_use] pub const fn is_node_drag_active(&self) -> bool {
510 self.callback_info.get_current_mouse_state().left_down
511 }
512
513 #[must_use] pub fn is_file_drag_active(&self) -> bool {
515 self.callback_info.is_file_drag_active()
516 }
517
518 #[must_use] pub fn has_sufficient_history_for_gestures(&self) -> bool {
520 self.callback_info.has_sufficient_history_for_gestures()
521 }
522
523 #[must_use] pub fn get_scroll_node_info(
529 &self,
530 dom_id: DomId,
531 node_id: NodeId,
532 ) -> Option<crate::managers::scroll_state::ScrollNodeInfo> {
533 self.callback_info.get_scroll_node_info(dom_id, node_id)
534 }
535
536 #[must_use] pub fn find_scroll_parent(
541 &self,
542 dom_id: DomId,
543 node_id: NodeId,
544 ) -> Option<NodeId> {
545 self.callback_info.find_scroll_parent(dom_id, node_id)
546 }
547
548 #[cfg(feature = "std")]
553 #[must_use] pub fn get_scroll_input_queue(
554 &self,
555 ) -> crate::managers::scroll_state::ScrollInputQueue {
556 self.callback_info.get_scroll_input_queue()
557 }
558
559 pub fn scroll_to(
564 &mut self,
565 dom_id: DomId,
566 node_id: azul_core::styled_dom::NodeHierarchyItemId,
567 position: LogicalPosition,
568 ) {
569 self.callback_info.scroll_to(dom_id, node_id, position);
570 }
571
572 pub fn scroll_to_unclamped(
574 &mut self,
575 dom_id: DomId,
576 node_id: azul_core::styled_dom::NodeHierarchyItemId,
577 position: LogicalPosition,
578 ) {
579 self.callback_info.scroll_to_unclamped(dom_id, node_id, position);
580 }
581
582 pub fn set_cursor_visibility(&mut self, visible: bool) {
586 self.callback_info.set_cursor_visibility(visible);
587 }
588
589 pub fn set_cursor_visibility_toggle(&mut self) {
591 use crate::callbacks::CallbackChange;
592 self.callback_info.push_change(CallbackChange::ToggleCursorVisibility);
593 }
594
595 pub fn reset_cursor_blink(&mut self) {
597 self.callback_info.reset_cursor_blink();
598 }
599}
600
601#[derive(Debug, Clone)]
603#[repr(C, u8)]
604#[allow(variant_size_differences)] #[allow(clippy::large_enum_variant)]
607pub enum OptionTimer {
608 None,
609 Some(Timer),
610}
611
612impl From<Option<Timer>> for OptionTimer {
613 fn from(o: Option<Timer>) -> Self {
614 o.map_or_else(|| Self::None, Self::Some)
615 }
616}
617
618impl OptionTimer {
619 #[must_use] pub fn into_option(self) -> Option<Timer> {
620 match self {
621 Self::None => None,
622 Self::Some(t) => Some(t),
623 }
624 }
625}
626
627#[cfg(all(test, feature = "std"))]
628#[allow(
629 clippy::float_cmp,
630 clippy::too_many_lines,
631 clippy::unreadable_literal,
632 clippy::cognitive_complexity
633)]
634mod autotest_generated {
635 use std::{
636 collections::BTreeMap,
637 sync::{
638 atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering},
639 Arc, Mutex, MutexGuard, PoisonError,
640 },
641 };
642
643 use azul_core::{
644 dom::DomNodeId,
645 gl::OptionGlContextPtr,
646 hit_test::ScrollPosition,
647 menu::MenuItemVec,
648 resources::{RawImageFormat, RendererResources},
649 styled_dom::NodeHierarchyItemId,
650 task::{SystemTick, SystemTickDiff, SystemTimeDiff, ThreadReceiver},
651 window::{MonitorVec, RawWindowHandle},
652 };
653 use azul_css::system::SystemStyle;
654 use rust_fontconfig::FcFontCache;
655
656 use super::*;
657 #[cfg(feature = "icu")]
658 use crate::icu::IcuLocalizerHandle;
659 use crate::{
660 callbacks::{CallbackChange, CallbackInfoRefData, ExternalSystemCallbacks},
661 thread::{ThreadCallbackType, ThreadSender},
662 window::LayoutWindow,
663 };
664
665 fn tick(t: u64) -> Instant {
672 Instant::Tick(SystemTick::new(t))
673 }
674
675 const fn tick_dur(d: u64) -> Duration {
677 Duration::Tick(SystemTickDiff { tick_diff: d })
678 }
679
680 const fn sys_dur_millis(ms: u64) -> Duration {
684 Duration::System(SystemTimeDiff::from_millis(ms))
685 }
686
687 fn tick_of(i: &Instant) -> u64 {
689 match i {
690 Instant::Tick(t) => t.tick_counter,
691 Instant::System(_) => panic!("expected a Tick instant, got a System one"),
692 }
693 }
694
695 static CLOCK_LOCK: Mutex<()> = Mutex::new(());
705 static FAKE_TICK: AtomicU64 = AtomicU64::new(0);
706 static CB_INVOCATIONS: AtomicUsize = AtomicUsize::new(0);
707 static CB_SEEN_CALL_COUNT: AtomicUsize = AtomicUsize::new(0);
708 static CB_SEEN_FRAME_START: AtomicU64 = AtomicU64::new(0);
709 static CB_SEEN_ABOUT_TO_FINISH: AtomicBool = AtomicBool::new(false);
710 static CB_RETURN_TERMINATE: AtomicBool = AtomicBool::new(false);
711
712 fn clock_guard() -> MutexGuard<'static, ()> {
716 let guard = CLOCK_LOCK.lock().unwrap_or_else(PoisonError::into_inner);
717 FAKE_TICK.store(0, Ordering::SeqCst);
718 CB_INVOCATIONS.store(0, Ordering::SeqCst);
719 CB_SEEN_CALL_COUNT.store(0, Ordering::SeqCst);
720 CB_SEEN_FRAME_START.store(0, Ordering::SeqCst);
721 CB_SEEN_ABOUT_TO_FINISH.store(false, Ordering::SeqCst);
722 CB_RETURN_TERMINATE.store(false, Ordering::SeqCst);
723 guard
724 }
725
726 fn set_now(t: u64) {
727 FAKE_TICK.store(t, Ordering::SeqCst);
728 }
729
730 extern "C" fn fake_clock() -> Instant {
731 Instant::Tick(SystemTick::new(FAKE_TICK.load(Ordering::SeqCst)))
732 }
733
734 fn fake_clock_cb() -> GetSystemTimeCallback {
735 GetSystemTimeCallback { cb: fake_clock }
736 }
737
738 extern "C" fn recording_cb(_data: RefAny, info: TimerCallbackInfo) -> TimerCallbackReturn {
741 CB_INVOCATIONS.fetch_add(1, Ordering::SeqCst);
742 CB_SEEN_CALL_COUNT.store(info.call_count, Ordering::SeqCst);
743 CB_SEEN_ABOUT_TO_FINISH.store(info.is_about_to_finish, Ordering::SeqCst);
744 if let Instant::Tick(t) = &info.frame_start {
745 CB_SEEN_FRAME_START.store(t.tick_counter, Ordering::SeqCst);
746 }
747 TimerCallbackReturn {
748 should_update: Update::DoNothing,
749 should_terminate: if CB_RETURN_TERMINATE.load(Ordering::SeqCst) {
750 TerminateTimer::Terminate
751 } else {
752 TerminateTimer::Continue
753 },
754 }
755 }
756
757 extern "C" fn cb_alpha(_d: RefAny, _i: TimerCallbackInfo) -> TimerCallbackReturn {
761 TimerCallbackReturn {
762 should_update: Update::RefreshDom,
763 should_terminate: TerminateTimer::Terminate,
764 }
765 }
766 extern "C" fn cb_beta(_d: RefAny, _i: TimerCallbackInfo) -> TimerCallbackReturn {
767 TimerCallbackReturn {
768 should_update: Update::DoNothing,
769 should_terminate: TerminateTimer::Continue,
770 }
771 }
772
773 fn timer_at(created: u64, cb: TimerCallbackType) -> Timer {
775 set_now(created);
776 Timer::create(RefAny::new(0_usize), cb, fake_clock_cb())
777 }
778
779 struct Env<'a> {
784 ref_data: &'a CallbackInfoRefData<'a>,
785 changes: &'a Arc<Mutex<Vec<CallbackChange>>>,
786 }
787
788 impl Env<'_> {
789 fn info(&self) -> CallbackInfo {
790 self.info_with(OptionLogicalPosition::None, OptionLogicalPosition::None)
791 }
792
793 fn info_with(
794 &self,
795 cursor_relative_to_item: OptionLogicalPosition,
796 cursor_in_viewport: OptionLogicalPosition,
797 ) -> CallbackInfo {
798 CallbackInfo::new(
799 self.ref_data,
800 self.changes,
801 DomNodeId {
802 dom: DomId::ROOT_ID,
803 node: NodeHierarchyItemId::NONE,
804 },
805 cursor_relative_to_item,
806 cursor_in_viewport,
807 )
808 }
809
810 fn timer_info(&self) -> TimerCallbackInfo {
812 TimerCallbackInfo::create(self.info(), OptionDomNodeId::None, tick(0), 0, false)
813 }
814
815 fn take_changes(&self) -> Vec<CallbackChange> {
816 self.changes
817 .lock()
818 .map(|mut c| core::mem::take(&mut *c))
819 .unwrap_or_default()
820 }
821
822 fn take_one(&self) -> CallbackChange {
824 let mut changes = self.take_changes();
825 assert_eq!(changes.len(), 1, "expected exactly one change: {changes:?}");
826 changes.remove(0)
827 }
828 }
829
830 fn with_env<R>(f: impl FnOnce(&Env<'_>) -> R) -> R {
831 with_env_cfg(false, OptionRefAny::None, f)
832 }
833
834 fn with_env_cfg<R>(left_down: bool, ctx: OptionRefAny, f: impl FnOnce(&Env<'_>) -> R) -> R {
838 let layout_window =
839 LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new failed");
840 let renderer_resources = RendererResources::default();
841 let previous_window_state: Option<FullWindowState> = None;
842 let mut current_window_state = FullWindowState::default();
843 current_window_state.mouse_state.left_down = left_down;
844 let gl_context = OptionGlContextPtr::None;
845 let scroll_states: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> =
846 BTreeMap::new();
847 let window_handle = RawWindowHandle::Unsupported;
848 let system_callbacks = ExternalSystemCallbacks::rust_internal();
849
850 let ref_data = CallbackInfoRefData {
851 layout_window: &layout_window,
852 renderer_resources: &renderer_resources,
853 previous_window_state: &previous_window_state,
854 current_window_state: ¤t_window_state,
855 gl_context: &gl_context,
856 current_scroll_manager: &scroll_states,
857 current_window_handle: &window_handle,
858 system_callbacks: &system_callbacks,
859 system_style: Arc::new(SystemStyle::default()),
860 monitors: Arc::new(Mutex::new(MonitorVec::from_const_slice(&[]))),
861 #[cfg(feature = "icu")]
862 icu_localizer: IcuLocalizerHandle::default(),
863 ctx,
864 };
865
866 let changes: Arc<Mutex<Vec<CallbackChange>>> = Arc::new(Mutex::new(Vec::new()));
867 let env = Env {
868 ref_data: &ref_data,
869 changes: &changes,
870 };
871 f(&env)
872 }
873
874 fn empty_menu() -> Menu {
875 Menu::create(MenuItemVec::from_const_slice(&[]))
876 }
877
878 #[test]
883 fn timer_create_starts_completely_unarmed() {
884 let _g = clock_guard();
885 let t = timer_at(12_345, recording_cb as TimerCallbackType);
886
887 assert_eq!(tick_of(&t.created), 12_345, "created must come from the clock");
888 assert_eq!(t.run_count, 0);
889 assert_eq!(t.last_run, OptionInstant::None);
890 assert_eq!(t.delay, OptionDuration::None);
891 assert_eq!(t.interval, OptionDuration::None);
892 assert_eq!(t.timeout, OptionDuration::None);
893 assert_eq!(t.node_id, OptionDomNodeId::None);
894 }
895
896 #[test]
897 fn timer_create_at_max_tick_does_not_panic() {
898 let _g = clock_guard();
899 let t = timer_at(u64::MAX, recording_cb as TimerCallbackType);
900 assert_eq!(tick_of(&t.created), u64::MAX);
901 assert!(!t.is_about_to_finish(&tick(u64::MAX)));
903 assert_eq!(tick_of(&t.instant_of_next_run()), u64::MAX);
904 }
905
906 #[test]
907 fn timer_default_is_a_zero_tick_timer() {
908 let t = Timer::default();
909 assert_eq!(tick_of(&t.created), 0);
910 assert_eq!(t.run_count, 0);
911 assert_eq!(t.last_run, OptionInstant::None);
912 assert_eq!(t.tick_millis(), DEFAULT_TIMER_TICK_MS);
913 }
914
915 #[test]
916 fn timer_clone_equals_original() {
917 let _g = clock_guard();
918 let t = timer_at(7, recording_cb as TimerCallbackType)
919 .with_delay(tick_dur(1))
920 .with_interval(tick_dur(2))
921 .with_timeout(tick_dur(3));
922 let c = t.clone();
923 assert_eq!(t, c, "Clone must be value-preserving");
924 }
925
926 #[test]
931 fn tick_millis_falls_back_to_default_without_interval() {
932 let _g = clock_guard();
933 let t = timer_at(0, recording_cb as TimerCallbackType);
934 assert_eq!(t.tick_millis(), DEFAULT_TIMER_TICK_MS);
935 assert_eq!(t.tick_millis(), 10);
936
937 let t = t.with_delay(tick_dur(999)).with_timeout(tick_dur(888));
939 assert_eq!(t.tick_millis(), DEFAULT_TIMER_TICK_MS);
940 }
941
942 #[test]
947 fn tick_millis_converts_tick_intervals_at_the_nominal_frame_rate() {
948 let _g = clock_guard();
949 for (raw, expected_ms) in [
950 (0_u64, 0_u64),
951 (1, 16),
952 (5, 83),
953 (60, 1_000),
954 (600, 10_000),
955 ] {
956 let t = timer_at(0, recording_cb as TimerCallbackType).with_interval(tick_dur(raw));
957 assert_eq!(
958 t.tick_millis(),
959 expected_ms,
960 "a {raw}-tick interval is {expected_ms}ms of wall clock"
961 );
962 }
963 }
964
965 #[test]
969 fn tick_millis_saturates_on_an_absurd_tick_interval_instead_of_wrapping() {
970 let _g = clock_guard();
971 let t = timer_at(0, recording_cb as TimerCallbackType).with_interval(tick_dur(u64::MAX));
972 assert_eq!(t.tick_millis(), u64::MAX);
973
974 let fits = u64::MAX / 1000 * 60;
976 let t = timer_at(0, recording_cb as TimerCallbackType).with_interval(tick_dur(fits));
977 assert!(t.tick_millis() < u64::MAX, "{fits} ticks should not clamp");
978 }
979
980 #[test]
981 fn tick_millis_system_interval_round_trips_whole_millis() {
982 let _g = clock_guard();
983 for ms in [0_u64, 1, 999, 1_000, 1_001, 86_400_000, u64::MAX] {
986 let t = timer_at(0, recording_cb as TimerCallbackType).with_interval(sys_dur_millis(ms));
987 assert_eq!(t.tick_millis(), ms, "millis {ms} must round-trip");
988 }
989 }
990
991 #[test]
992 fn tick_millis_saturates_instead_of_overflowing() {
993 let _g = clock_guard();
994 let huge = Duration::System(SystemTimeDiff {
996 secs: u64::MAX,
997 nanos: 999_999_999,
998 });
999 let t = timer_at(0, recording_cb as TimerCallbackType).with_interval(huge);
1000 assert_eq!(t.tick_millis(), u64::MAX);
1001 }
1002
1003 #[test]
1004 fn tick_millis_truncates_sub_millisecond_intervals_to_zero() {
1005 let _g = clock_guard();
1006 let t = timer_at(0, recording_cb as TimerCallbackType)
1009 .with_interval(Duration::System(SystemTimeDiff::from_nanos(999_999)));
1010 assert_eq!(t.tick_millis(), 0);
1011 }
1012
1013 #[test]
1018 fn is_about_to_finish_is_false_without_a_timeout() {
1019 let _g = clock_guard();
1020 let t = timer_at(0, recording_cb as TimerCallbackType);
1021 assert!(!t.is_about_to_finish(&tick(0)));
1022 assert!(!t.is_about_to_finish(&tick(u64::MAX)), "no timeout = never finishes");
1023 }
1024
1025 #[test]
1026 fn is_about_to_finish_boundary_is_strictly_greater() {
1027 let _g = clock_guard();
1028 let t = timer_at(100, recording_cb as TimerCallbackType).with_timeout(tick_dur(50));
1029
1030 assert!(!t.is_about_to_finish(&tick(149)), "1 tick early");
1031 assert!(!t.is_about_to_finish(&tick(150)), "exactly at the timeout");
1033 assert!(t.is_about_to_finish(&tick(151)), "1 tick past the timeout");
1034 }
1035
1036 #[test]
1037 fn is_about_to_finish_saturates_when_the_clock_runs_backwards() {
1038 let _g = clock_guard();
1039 let t = timer_at(1_000, recording_cb as TimerCallbackType).with_timeout(tick_dur(10));
1040 assert!(!t.is_about_to_finish(&tick(0)));
1043 }
1044
1045 #[test]
1046 fn is_about_to_finish_at_the_u64_ceiling() {
1047 let _g = clock_guard();
1048 let t = timer_at(0, recording_cb as TimerCallbackType);
1049
1050 let max_timeout = t.clone().with_timeout(tick_dur(u64::MAX));
1051 assert!(
1052 !max_timeout.is_about_to_finish(&tick(u64::MAX)),
1053 "MAX elapsed is not > MAX timeout"
1054 );
1055
1056 let near_max = t.with_timeout(tick_dur(u64::MAX - 1));
1057 assert!(near_max.is_about_to_finish(&tick(u64::MAX)));
1058 }
1059
1060 #[test]
1068 fn is_about_to_finish_expires_a_wall_clock_timeout_on_a_tick_clock() {
1069 let _g = clock_guard();
1070 let t = timer_at(0, recording_cb as TimerCallbackType).with_timeout(sys_dur_millis(1));
1072 assert!(!t.is_about_to_finish(&tick(0)), "no time has passed yet");
1073 assert!(t.is_about_to_finish(&tick(1)));
1074 assert!(t.is_about_to_finish(&tick(u64::MAX)));
1075
1076 let t = timer_at(0, recording_cb as TimerCallbackType).with_timeout(sys_dur_millis(1_000));
1078 assert!(!t.is_about_to_finish(&tick(59)), "1 frame early");
1079 assert!(!t.is_about_to_finish(&tick(60)), "exactly at the timeout");
1080 assert!(t.is_about_to_finish(&tick(61)), "1 frame past the timeout");
1081 }
1082
1083 #[test]
1088 fn instant_of_next_run_is_created_when_nothing_is_armed() {
1089 let _g = clock_guard();
1090 let t = timer_at(42, recording_cb as TimerCallbackType);
1091 assert_eq!(tick_of(&t.instant_of_next_run()), 42);
1092 }
1093
1094 #[test]
1095 fn instant_of_next_run_prefers_last_run_over_created() {
1096 let _g = clock_guard();
1097 let mut t = timer_at(100, recording_cb as TimerCallbackType).with_interval(tick_dur(7));
1098 assert_eq!(tick_of(&t.instant_of_next_run()), 107, "no run yet: created + interval");
1099
1100 t.last_run = OptionInstant::Some(tick(500));
1101 assert_eq!(tick_of(&t.instant_of_next_run()), 507, "after a run: last_run + interval");
1102 }
1103
1104 #[test]
1105 fn instant_of_next_run_sums_delay_and_interval() {
1106 let _g = clock_guard();
1107 let mut t = timer_at(100, recording_cb as TimerCallbackType)
1112 .with_delay(tick_dur(5))
1113 .with_interval(tick_dur(7));
1114 assert_eq!(tick_of(&t.instant_of_next_run()), 112);
1115
1116 t.last_run = OptionInstant::Some(tick(200));
1117 assert_eq!(tick_of(&t.instant_of_next_run()), 212);
1118 }
1119
1120 #[test]
1121 fn instant_of_next_run_saturates_at_the_end_of_time() {
1122 let _g = clock_guard();
1123 let t = timer_at(u64::MAX, recording_cb as TimerCallbackType)
1124 .with_delay(tick_dur(u64::MAX))
1125 .with_interval(tick_dur(u64::MAX));
1126 assert_eq!(tick_of(&t.instant_of_next_run()), u64::MAX);
1128 }
1129
1130 #[test]
1135 fn instant_of_next_run_converts_wall_clock_delays_to_whole_frames() {
1136 let _g = clock_guard();
1137 let t = timer_at(42, recording_cb as TimerCallbackType)
1138 .with_delay(sys_dur_millis(1_000))
1139 .with_interval(sys_dur_millis(1_000));
1140 assert_eq!(tick_of(&t.instant_of_next_run()), 42 + 120);
1142 }
1143
1144 #[test]
1149 fn instant_of_next_run_rounds_a_sub_frame_interval_down_to_zero_ticks() {
1150 let _g = clock_guard();
1151 let t = timer_at(42, recording_cb as TimerCallbackType).with_interval(sys_dur_millis(1));
1152 assert_eq!(tick_of(&t.instant_of_next_run()), 42);
1153 }
1154
1155 #[test]
1160 fn with_setters_are_independent_and_preserve_the_rest() {
1161 let _g = clock_guard();
1162 let t = timer_at(9, recording_cb as TimerCallbackType)
1163 .with_delay(tick_dur(1))
1164 .with_interval(tick_dur(2))
1165 .with_timeout(tick_dur(3));
1166
1167 assert_eq!(t.delay, OptionDuration::Some(tick_dur(1)));
1168 assert_eq!(t.interval, OptionDuration::Some(tick_dur(2)));
1169 assert_eq!(t.timeout, OptionDuration::Some(tick_dur(3)));
1170 assert_eq!(tick_of(&t.created), 9);
1172 assert_eq!(t.run_count, 0);
1173 assert_eq!(t.last_run, OptionInstant::None);
1174 }
1175
1176 #[test]
1177 fn with_setters_are_last_write_wins() {
1178 let _g = clock_guard();
1179 let t = timer_at(0, recording_cb as TimerCallbackType)
1180 .with_delay(tick_dur(1))
1181 .with_delay(tick_dur(2))
1182 .with_interval(tick_dur(3))
1183 .with_interval(tick_dur(4))
1184 .with_timeout(tick_dur(5))
1185 .with_timeout(tick_dur(6));
1186
1187 assert_eq!(t.delay, OptionDuration::Some(tick_dur(2)));
1188 assert_eq!(t.interval, OptionDuration::Some(tick_dur(4)));
1189 assert_eq!(t.timeout, OptionDuration::Some(tick_dur(6)));
1190 }
1191
1192 #[test]
1193 fn with_setters_accept_extreme_durations() {
1194 let _g = clock_guard();
1195 let t = timer_at(0, recording_cb as TimerCallbackType)
1196 .with_delay(tick_dur(0))
1197 .with_interval(tick_dur(u64::MAX))
1198 .with_timeout(Duration::max());
1199
1200 assert_eq!(t.delay, OptionDuration::Some(tick_dur(0)));
1201 assert_eq!(t.tick_millis(), u64::MAX);
1202 assert!(!t.is_about_to_finish(&tick(u64::MAX)));
1204 }
1205
1206 #[test]
1211 fn timer_callback_create_has_no_ffi_ctx() {
1212 let cb = TimerCallback::create(cb_alpha as TimerCallbackType);
1213 assert_eq!(cb.ctx, OptionRefAny::None);
1214
1215 let from: TimerCallback = (cb_alpha as TimerCallbackType).into();
1217 assert_eq!(cb, from);
1218 }
1219
1220 #[test]
1221 fn timer_callback_identity_is_by_function_pointer() {
1222 let a1 = TimerCallback::create(cb_alpha as TimerCallbackType);
1223 let a2 = TimerCallback::create(cb_alpha as TimerCallbackType);
1224 let b = TimerCallback::create(cb_beta as TimerCallbackType);
1225
1226 assert_eq!(a1, a2, "same fn -> equal");
1227 assert_ne!(a1, b, "different fn -> not equal");
1228 assert_eq!(a1, a1.clone(), "Clone preserves identity");
1229 }
1230
1231 #[test]
1232 fn timer_callback_ord_and_hash_agree_with_eq() {
1233 use std::{
1234 collections::hash_map::DefaultHasher,
1235 hash::{Hash, Hasher},
1236 };
1237
1238 fn hash_of(cb: &TimerCallback) -> u64 {
1239 let mut h = DefaultHasher::new();
1240 cb.hash(&mut h);
1241 h.finish()
1242 }
1243
1244 let a = TimerCallback::create(cb_alpha as TimerCallbackType);
1245 let a2 = a.clone();
1246 let b = TimerCallback::create(cb_beta as TimerCallbackType);
1247
1248 assert_eq!(a.cmp(&a2), core::cmp::Ordering::Equal);
1249 assert_eq!(hash_of(&a), hash_of(&a2), "Eq values must hash equal");
1250
1251 assert_eq!(a.cmp(&b), a.partial_cmp(&b).unwrap());
1253 assert_eq!(a.cmp(&b).reverse(), b.cmp(&a));
1254 assert_ne!(a.cmp(&b), core::cmp::Ordering::Equal, "distinct fns must order strictly");
1255 }
1256
1257 #[test]
1258 fn timer_callback_debug_does_not_panic() {
1259 let s = format!("{:?}", TimerCallback::create(cb_alpha as TimerCallbackType));
1260 assert!(s.starts_with("TimerCallback"), "got {s}");
1261 }
1262
1263 #[test]
1268 fn option_timer_round_trips_both_variants() {
1269 let _g = clock_guard();
1270 assert!(OptionTimer::None.into_option().is_none());
1271 assert!(OptionTimer::from(None).into_option().is_none());
1272
1273 let t = timer_at(3, recording_cb as TimerCallbackType).with_interval(tick_dur(4));
1274 let round_tripped = OptionTimer::from(Some(t.clone()))
1275 .into_option()
1276 .expect("Some must survive the round-trip");
1277 assert_eq!(round_tripped, t, "encode == decode");
1278 }
1279
1280 #[test]
1285 fn timer_callback_info_create_preserves_extremes() {
1286 with_env(|env| {
1287 let info = TimerCallbackInfo::create(
1288 env.info(),
1289 OptionDomNodeId::None,
1290 tick(u64::MAX),
1291 usize::MAX,
1292 true,
1293 );
1294 assert_eq!(info.call_count, usize::MAX, "no wrap at usize::MAX");
1295 assert!(info.is_about_to_finish);
1296 assert_eq!(tick_of(&info.frame_start), u64::MAX);
1297 assert!(info._abi_ref.is_null());
1298 assert!(info._abi_mut.is_null());
1299
1300 let zero =
1301 TimerCallbackInfo::create(env.info(), OptionDomNodeId::None, tick(0), 0, false);
1302 assert_eq!(zero.call_count, 0);
1303 assert!(!zero.is_about_to_finish);
1304 assert_eq!(tick_of(&zero.frame_start), 0);
1305 });
1306 }
1307
1308 #[test]
1309 fn get_current_time_returns_frame_start_verbatim() {
1310 with_env(|env| {
1311 for t in [0_u64, 1, u64::MAX] {
1312 let info =
1313 TimerCallbackInfo::create(env.info(), OptionDomNodeId::None, tick(t), 0, false);
1314 assert_eq!(info.get_current_time(), tick(t));
1315 }
1316 });
1317 }
1318
1319 #[test]
1320 fn attached_node_queries_are_none_without_an_attached_node() {
1321 with_env(|env| {
1322 let info = env.timer_info();
1323 assert!(info.get_attached_node_size().is_none());
1324 assert!(info.get_attached_node_position().is_none());
1325 });
1326 }
1327
1328 #[test]
1329 fn attached_node_queries_are_none_for_a_bogus_node() {
1330 with_env(|env| {
1331 let bogus = DomNodeId {
1334 dom: DomId { inner: usize::MAX },
1335 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(usize::MAX - 1))),
1336 };
1337 let info = TimerCallbackInfo::create(
1338 env.info(),
1339 OptionDomNodeId::Some(bogus),
1340 tick(0),
1341 0,
1342 false,
1343 );
1344 assert!(info.get_attached_node_size().is_none(), "must not panic or index OOB");
1345 assert!(info.get_attached_node_position().is_none());
1346 });
1347 }
1348
1349 #[test]
1350 fn get_callback_info_and_mut_alias_the_same_inner_info() {
1351 with_env(|env| {
1352 let mut info = env.timer_info();
1353 let addr_shared = std::ptr::from_ref(info.get_callback_info());
1354 let addr_mut = std::ptr::from_mut(info.get_callback_info_mut()).cast_const();
1355 assert!(std::ptr::eq(addr_shared, addr_mut), "both must alias the inner CallbackInfo");
1356
1357 info.get_callback_info_mut().prevent_default();
1359 assert!(matches!(env.take_one(), CallbackChange::PreventDefault));
1360 });
1361 }
1362
1363 #[test]
1364 fn get_ctx_is_none_for_native_rust_callbacks() {
1365 with_env(|env| {
1366 assert_eq!(env.timer_info().get_ctx(), OptionRefAny::None);
1367 });
1368 }
1369
1370 #[test]
1371 fn get_ctx_hands_back_the_ffi_callable() {
1372 let ctx = RefAny::new(0xDEAD_BEEF_u32);
1373 with_env_cfg(false, OptionRefAny::Some(ctx.clone()), |env| {
1374 let got = env.timer_info().get_ctx().into_option().expect("ctx must survive");
1375 assert_eq!(got, ctx, "get_ctx must hand back the same RefAny");
1376 });
1377 }
1378
1379 #[test]
1384 fn add_and_remove_timer_push_the_matching_changes() {
1385 let _g = clock_guard();
1386 with_env(|env| {
1387 let mut info = env.timer_info();
1388 let id = TimerId { id: usize::MAX };
1389 info.add_timer(id, timer_at(1, recording_cb as TimerCallbackType));
1390 let CallbackChange::AddTimer { timer_id, timer } = env.take_one() else {
1391 panic!("expected AddTimer");
1392 };
1393 assert_eq!(timer_id, id);
1394 assert_eq!(tick_of(&timer.created), 1, "the timer must be stored verbatim");
1395
1396 info.remove_timer(TimerId { id: 0 });
1397 let CallbackChange::RemoveTimer { timer_id } = env.take_one() else {
1398 panic!("expected RemoveTimer");
1399 };
1400 assert_eq!(timer_id.id, 0, "id 0 (a reserved system id) is still accepted");
1401 });
1402 }
1403
1404 #[test]
1405 fn add_and_remove_thread_push_the_matching_changes() {
1406 extern "C" fn noop_worker(_d: RefAny, _s: ThreadSender, _r: ThreadReceiver) {}
1407
1408 with_env(|env| {
1409 let mut info = env.timer_info();
1410 let id = ThreadId::unique();
1411 let thread = Thread::create(
1412 RefAny::new(0_usize),
1413 RefAny::new(0_usize),
1414 noop_worker as ThreadCallbackType,
1415 );
1416 info.add_thread(id, thread);
1417 let CallbackChange::AddThread { thread_id, .. } = env.take_one() else {
1418 panic!("expected AddThread");
1419 };
1420 assert_eq!(thread_id, id);
1421
1422 info.remove_thread(id);
1423 let CallbackChange::RemoveThread { thread_id } = env.take_one() else {
1424 panic!("expected RemoveThread");
1425 };
1426 assert_eq!(thread_id, id);
1427 });
1428 }
1429
1430 #[test]
1431 fn nullary_mutators_push_exactly_one_change_each_in_order() {
1432 with_env(|env| {
1433 let mut info = env.timer_info();
1434 info.stop_propagation();
1435 info.prevent_default();
1436 info.close_window();
1437 info.hide_tooltip();
1438 info.reload_system_fonts();
1439 info.update_all_image_callbacks();
1440 info.reset_cursor_blink();
1441 info.set_cursor_visibility_toggle();
1442
1443 let changes = env.take_changes();
1444 assert_eq!(changes.len(), 8, "one change per call, no drops: {changes:?}");
1445 assert!(matches!(changes[0], CallbackChange::StopPropagation));
1446 assert!(matches!(changes[1], CallbackChange::PreventDefault));
1447 assert!(matches!(changes[2], CallbackChange::CloseWindow));
1448 assert!(matches!(changes[3], CallbackChange::HideTooltip));
1449 assert!(matches!(changes[4], CallbackChange::ReloadSystemFonts));
1450 assert!(matches!(changes[5], CallbackChange::UpdateAllImageCallbacks));
1451 assert!(matches!(changes[6], CallbackChange::ResetCursorBlink));
1452 assert!(matches!(changes[7], CallbackChange::ToggleCursorVisibility));
1453 });
1454 }
1455
1456 #[test]
1457 fn set_cursor_visibility_records_both_polarities() {
1458 with_env(|env| {
1459 let mut info = env.timer_info();
1460 info.set_cursor_visibility(true);
1461 info.set_cursor_visibility(false);
1462
1463 let changes = env.take_changes();
1464 assert_eq!(changes.len(), 2);
1465 let visibilities: Vec<bool> = changes
1466 .iter()
1467 .map(|c| match c {
1468 CallbackChange::SetCursorVisibility { visible } => *visible,
1469 other => panic!("expected SetCursorVisibility, got {other:?}"),
1470 })
1471 .collect();
1472 assert_eq!(visibilities, vec![true, false]);
1473 });
1474 }
1475
1476 #[test]
1477 fn create_window_and_modify_window_state_push_changes() {
1478 with_env(|env| {
1479 let mut info = env.timer_info();
1480 info.create_window(WindowCreateOptions::default());
1481 assert!(matches!(env.take_one(), CallbackChange::CreateNewWindow { .. }));
1482
1483 info.modify_window_state(FullWindowState::default());
1484 assert!(matches!(env.take_one(), CallbackChange::ModifyWindowState { .. }));
1485 });
1486 }
1487
1488 #[test]
1489 fn image_cache_mutators_accept_degenerate_and_unicode_ids() {
1490 with_env(|env| {
1491 let mut info = env.timer_info();
1492
1493 let img = ImageRef::null_image(0, 0, RawImageFormat::RGBA8, Vec::new());
1495 let id: AzString = String::new().into();
1496 info.add_image_to_cache(id.clone(), img);
1497 let CallbackChange::AddImageToCache { id: got, .. } = env.take_one() else {
1498 panic!("expected AddImageToCache");
1499 };
1500 assert_eq!(got, id, "an empty id is passed through, not rejected");
1501
1502 let nasty: AzString = String::from("🚀\u{0}\u{202E}id\u{1F600}").into();
1505 info.remove_image_from_cache(nasty.clone());
1506 let CallbackChange::RemoveImageFromCache { id: got } = env.take_one() else {
1507 panic!("expected RemoveImageFromCache");
1508 };
1509 assert_eq!(got.as_str(), nasty.as_str());
1510 });
1511 }
1512
1513 #[test]
1514 fn trigger_virtual_view_rerender_accepts_out_of_range_ids() {
1515 with_env(|env| {
1516 let mut info = env.timer_info();
1517 info.trigger_virtual_view_rerender(DomId { inner: usize::MAX }, NodeId::new(usize::MAX));
1518 let CallbackChange::UpdateVirtualView { dom_id, node_id } = env.take_one() else {
1519 panic!("expected UpdateVirtualView");
1520 };
1521 assert_eq!(dom_id.inner, usize::MAX);
1524 assert_eq!(node_id, NodeId::new(usize::MAX));
1525 });
1526 }
1527
1528 #[test]
1529 fn open_menu_has_no_position_and_open_menu_at_carries_one() {
1530 with_env(|env| {
1531 let mut info = env.timer_info();
1532
1533 info.open_menu(empty_menu());
1534 let CallbackChange::OpenMenu { position, .. } = env.take_one() else {
1535 panic!("expected OpenMenu");
1536 };
1537 assert!(position.is_none(), "open_menu must defer to menu.position");
1538
1539 info.open_menu_at(empty_menu(), LogicalPosition::new(-1.5, 2.5));
1540 let CallbackChange::OpenMenu { position, .. } = env.take_one() else {
1541 panic!("expected OpenMenu");
1542 };
1543 let p = position.expect("open_menu_at must pin a position");
1544 assert_eq!((p.x, p.y), (-1.5, 2.5), "negative coordinates are legal");
1545 });
1546 }
1547
1548 #[test]
1549 fn open_menu_at_passes_non_finite_coordinates_through_unchanged() {
1550 with_env(|env| {
1551 let mut info = env.timer_info();
1552 info.open_menu_at(
1553 empty_menu(),
1554 LogicalPosition::new(f32::NAN, f32::INFINITY),
1555 );
1556 let CallbackChange::OpenMenu { position, .. } = env.take_one() else {
1557 panic!("expected OpenMenu");
1558 };
1559 let p = position.expect("position must be recorded");
1560 assert!(p.x.is_nan());
1562 assert!(p.y.is_infinite() && p.y.is_sign_positive());
1563
1564 info.open_menu_at(empty_menu(), LogicalPosition::new(f32::MAX, f32::MIN));
1565 let CallbackChange::OpenMenu { position, .. } = env.take_one() else {
1566 panic!("expected OpenMenu");
1567 };
1568 let p = position.expect("position must be recorded");
1569 assert_eq!((p.x, p.y), (f32::MAX, f32::MIN));
1570 });
1571 }
1572
1573 #[test]
1574 fn open_menu_for_hit_node_is_false_and_silent_without_a_hit_node() {
1575 with_env(|env| {
1576 let mut info = env.timer_info();
1577 assert!(!info.open_menu_for_hit_node(empty_menu()));
1580 assert!(
1581 env.take_changes().is_empty(),
1582 "a failed anchor must not queue a half-open menu"
1583 );
1584 });
1585 }
1586
1587 #[test]
1588 fn show_tooltip_falls_back_to_the_origin_without_a_cursor() {
1589 with_env(|env| {
1590 let mut info = env.timer_info();
1591 info.show_tooltip(String::from("hi").into());
1592 let CallbackChange::ShowTooltip { text, position } = env.take_one() else {
1593 panic!("expected ShowTooltip");
1594 };
1595 assert_eq!(text.as_str(), "hi");
1596 assert_eq!((position.x, position.y), (0.0, 0.0), "no cursor -> origin");
1597 });
1598 }
1599
1600 #[test]
1601 fn show_tooltip_uses_the_viewport_cursor_when_there_is_one() {
1602 with_env(|env| {
1603 let cursor = LogicalPosition::new(3.0, 4.0);
1604 let mut info = TimerCallbackInfo::create(
1605 env.info_with(OptionLogicalPosition::None, OptionLogicalPosition::Some(cursor)),
1606 OptionDomNodeId::None,
1607 tick(0),
1608 0,
1609 false,
1610 );
1611 info.show_tooltip(String::from("t").into());
1612 let CallbackChange::ShowTooltip { position, .. } = env.take_one() else {
1613 panic!("expected ShowTooltip");
1614 };
1615 assert_eq!((position.x, position.y), (3.0, 4.0));
1616 });
1617 }
1618
1619 #[test]
1620 fn show_tooltip_at_records_empty_text_and_non_finite_positions() {
1621 with_env(|env| {
1622 let mut info = env.timer_info();
1623 info.show_tooltip_at(String::new().into(), LogicalPosition::new(f32::NAN, -0.0));
1624 let CallbackChange::ShowTooltip { text, position } = env.take_one() else {
1625 panic!("expected ShowTooltip");
1626 };
1627 assert_eq!(text.as_str(), "", "empty tooltip text is not rejected");
1628 assert!(position.x.is_nan());
1629 assert!(position.y.is_sign_negative());
1630 });
1631 }
1632
1633 #[test]
1638 fn scroll_to_and_unclamped_differ_only_in_the_clamp_flag() {
1639 with_env(|env| {
1640 let mut info = env.timer_info();
1641 let node = NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(0)));
1642 let pos = LogicalPosition::new(10.0, 20.0);
1643
1644 info.scroll_to(DomId::ROOT_ID, node, pos);
1645 info.scroll_to_unclamped(DomId::ROOT_ID, node, pos);
1646
1647 let changes = env.take_changes();
1648 assert_eq!(changes.len(), 2);
1649 let flags: Vec<bool> = changes
1650 .iter()
1651 .map(|c| match c {
1652 CallbackChange::ScrollTo {
1653 dom_id,
1654 node_id,
1655 position,
1656 unclamped,
1657 } => {
1658 assert_eq!(*dom_id, DomId::ROOT_ID);
1659 assert_eq!(*node_id, node);
1660 assert_eq!((position.x, position.y), (10.0, 20.0));
1661 *unclamped
1662 }
1663 other => panic!("expected ScrollTo, got {other:?}"),
1664 })
1665 .collect();
1666 assert_eq!(flags, vec![false, true], "only the overscroll flag differs");
1667 });
1668 }
1669
1670 #[test]
1671 fn scroll_to_records_zero_negative_and_non_finite_positions() {
1672 with_env(|env| {
1673 let mut info = env.timer_info();
1674 let node = NodeHierarchyItemId::NONE;
1675
1676 for pos in [
1677 LogicalPosition::new(0.0, 0.0),
1678 LogicalPosition::new(-1.0, -f32::MAX),
1679 LogicalPosition::new(f32::MAX, f32::INFINITY),
1680 ] {
1681 info.scroll_to(DomId::ROOT_ID, node, pos);
1682 let CallbackChange::ScrollTo { position, .. } = env.take_one() else {
1683 panic!("expected ScrollTo");
1684 };
1685 assert_eq!(position.x.to_bits(), pos.x.to_bits(), "x must be recorded bit-exact");
1686 assert_eq!(position.y.to_bits(), pos.y.to_bits(), "y must be recorded bit-exact");
1687 }
1688
1689 info.scroll_to_unclamped(
1691 DomId { inner: usize::MAX },
1692 node,
1693 LogicalPosition::new(f32::NAN, f32::NAN),
1694 );
1695 let CallbackChange::ScrollTo {
1696 position, unclamped, ..
1697 } = env.take_one()
1698 else {
1699 panic!("expected ScrollTo");
1700 };
1701 assert!(position.x.is_nan() && position.y.is_nan(), "NaN is passed through, not zeroed");
1702 assert!(unclamped);
1703 });
1704 }
1705
1706 #[test]
1707 fn scroll_queries_are_none_on_an_empty_window() {
1708 with_env(|env| {
1709 let info = env.timer_info();
1710 assert!(info.get_scroll_node_info(DomId::ROOT_ID, NodeId::new(0)).is_none());
1711 assert!(
1712 info.get_scroll_node_info(DomId { inner: usize::MAX }, NodeId::new(usize::MAX))
1713 .is_none(),
1714 "an out-of-range dom/node must return None, not panic"
1715 );
1716 assert!(info.find_scroll_parent(DomId::ROOT_ID, NodeId::new(0)).is_none());
1717 assert!(
1718 info.find_scroll_parent(DomId { inner: usize::MAX }, NodeId::new(usize::MAX))
1719 .is_none()
1720 );
1721 });
1722 }
1723
1724 #[test]
1725 fn scroll_input_queue_starts_empty_and_draining_is_idempotent() {
1726 with_env(|env| {
1727 let info = env.timer_info();
1728 let queue = info.get_scroll_input_queue();
1729 assert!(queue.take_all().is_empty());
1730 assert!(queue.take_all().is_empty(), "draining twice must stay empty");
1731 });
1732 }
1733
1734 #[test]
1739 fn the_three_drag_predicates_are_aliases_of_left_down() {
1740 for left_down in [false, true] {
1741 with_env_cfg(left_down, OptionRefAny::None, |env| {
1742 let info = env.timer_info();
1743 assert_eq!(info.get_current_mouse_state().left_down, left_down);
1744 assert_eq!(info.is_dragging(), left_down);
1745 assert_eq!(info.is_drag_active(), left_down);
1746 assert_eq!(info.is_node_drag_active(), left_down);
1747 });
1748 }
1749 }
1750
1751 #[test]
1752 fn pen_predicates_are_false_without_a_pen() {
1753 with_env(|env| {
1754 let info = env.timer_info();
1755 assert!(!info.is_pen_in_contact());
1756 assert!(!info.is_pen_eraser());
1757 assert!(!info.is_pen_barrel_button_pressed());
1758 });
1759 }
1760
1761 #[test]
1762 fn drag_and_gesture_predicates_are_false_on_a_fresh_window() {
1763 with_env(|env| {
1764 let info = env.timer_info();
1765 assert!(!info.is_file_drag_active());
1766 assert!(!info.has_sufficient_history_for_gestures());
1767 });
1768 }
1769
1770 #[test]
1771 fn is_dom_focused_is_false_for_every_dom_when_nothing_is_focused() {
1772 with_env(|env| {
1773 let info = env.timer_info();
1774 assert!(!info.is_dom_focused(DomId::ROOT_ID));
1775 assert!(!info.is_dom_focused(DomId { inner: usize::MAX }));
1776 });
1777 }
1778
1779 #[test]
1780 fn window_state_getters_mirror_the_current_window_state() {
1781 with_env(|env| {
1782 let info = env.timer_info();
1783 let default_state = FullWindowState::default();
1784 assert_eq!(info.get_current_window_flags(), default_state.flags);
1785 assert_eq!(info.get_current_keyboard_state(), default_state.keyboard_state);
1786 assert_eq!(info.get_current_mouse_state(), default_state.mouse_state);
1787 });
1788 }
1789
1790 #[test]
1791 fn cursor_getters_round_trip_including_nan() {
1792 with_env(|env| {
1793 let info = env.timer_info();
1794 assert!(info.get_cursor_position().is_none());
1795 assert_eq!(info.get_cursor_relative_to_viewport(), OptionLogicalPosition::None);
1796 assert!(info.get_cursor_relative_to_node().is_none());
1797
1798 let viewport = LogicalPosition::new(f32::NAN, 7.5);
1799 let relative = LogicalPosition::new(-3.0, f32::INFINITY);
1800 let info = TimerCallbackInfo::create(
1801 env.info_with(
1802 OptionLogicalPosition::Some(relative),
1803 OptionLogicalPosition::Some(viewport),
1804 ),
1805 OptionDomNodeId::None,
1806 tick(0),
1807 0,
1808 false,
1809 );
1810
1811 let got = info.get_cursor_position().expect("cursor must be Some");
1812 assert!(got.x.is_nan() && got.y == 7.5);
1813
1814 let node_rel = info
1815 .get_cursor_relative_to_node()
1816 .into_option()
1817 .expect("relative cursor must be Some");
1818 assert_eq!(node_rel.x, -3.0);
1819 assert!(node_rel.y.is_infinite());
1820 });
1821 }
1822
1823 #[test]
1828 fn invoke_does_not_run_the_callback_before_the_delay_elapses() {
1829 let _g = clock_guard();
1830 with_env(|env| {
1831 let mut t =
1832 timer_at(0, recording_cb as TimerCallbackType).with_delay(tick_dur(100));
1833 let info = env.info();
1834
1835 set_now(99);
1836 let r = t.invoke(&info, &fake_clock_cb());
1837 assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 0, "callback must not fire early");
1838 assert_eq!(r.should_update, Update::DoNothing);
1839 assert_eq!(r.should_terminate, TerminateTimer::Continue);
1840 assert_eq!(t.run_count, 0);
1842 assert_eq!(t.last_run, OptionInstant::None);
1843
1844 set_now(100);
1846 let r = t.invoke(&info, &fake_clock_cb());
1847 assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 1);
1848 assert_eq!(r.should_terminate, TerminateTimer::Continue);
1849 assert_eq!(t.run_count, 1);
1850 assert_eq!(t.last_run, OptionInstant::Some(tick(100)));
1851 });
1852 }
1853
1854 #[test]
1855 fn invoke_gates_subsequent_runs_on_the_interval() {
1856 let _g = clock_guard();
1857 with_env(|env| {
1858 let mut t =
1859 timer_at(0, recording_cb as TimerCallbackType).with_interval(tick_dur(10));
1860 let info = env.info();
1861
1862 let r = t.invoke(&info, &fake_clock_cb());
1864 assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 1);
1865 assert_eq!(r.should_terminate, TerminateTimer::Continue);
1866 assert_eq!(t.run_count, 1);
1867
1868 set_now(9);
1869 let r = t.invoke(&info, &fake_clock_cb());
1870 assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 1, "1 tick short of the interval");
1871 assert_eq!(r.should_update, Update::DoNothing);
1872 assert_eq!(t.run_count, 1, "a skipped tick must not count as a run");
1873
1874 set_now(10);
1875 let _ = t.invoke(&info, &fake_clock_cb());
1876 assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 2);
1877 assert_eq!(t.run_count, 2);
1878 assert_eq!(t.last_run, OptionInstant::Some(tick(10)));
1879 });
1880 }
1881
1882 #[test]
1883 fn invoke_hands_the_callback_the_run_count_and_frame_start() {
1884 let _g = clock_guard();
1885 with_env(|env| {
1886 let mut t = timer_at(0, recording_cb as TimerCallbackType);
1887 let info = env.info();
1888
1889 set_now(5);
1890 let _ = t.invoke(&info, &fake_clock_cb());
1891 assert_eq!(CB_SEEN_CALL_COUNT.load(Ordering::SeqCst), 0, "first run is call 0");
1892 assert_eq!(CB_SEEN_FRAME_START.load(Ordering::SeqCst), 5, "frame_start == now");
1893 assert!(!CB_SEEN_ABOUT_TO_FINISH.load(Ordering::SeqCst));
1894
1895 set_now(6);
1896 let _ = t.invoke(&info, &fake_clock_cb());
1897 assert_eq!(CB_SEEN_CALL_COUNT.load(Ordering::SeqCst), 1, "run_count increments by 1");
1898 assert_eq!(CB_SEEN_FRAME_START.load(Ordering::SeqCst), 6);
1899 });
1900 }
1901
1902 #[test]
1903 fn invoke_forces_terminate_once_the_timeout_expires() {
1904 let _g = clock_guard();
1905 with_env(|env| {
1906 let mut t = timer_at(0, recording_cb as TimerCallbackType).with_timeout(tick_dur(5));
1907 let info = env.info();
1908 CB_RETURN_TERMINATE.store(false, Ordering::SeqCst);
1910
1911 set_now(5);
1912 let r = t.invoke(&info, &fake_clock_cb());
1913 assert_eq!(r.should_terminate, TerminateTimer::Continue, "elapsed == timeout: alive");
1914 assert!(!CB_SEEN_ABOUT_TO_FINISH.load(Ordering::SeqCst));
1915
1916 set_now(6);
1917 let r = t.invoke(&info, &fake_clock_cb());
1918 assert!(CB_SEEN_ABOUT_TO_FINISH.load(Ordering::SeqCst), "last-call flag must be set");
1920 assert_eq!(r.should_terminate, TerminateTimer::Terminate);
1921 assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 2, "the final run still happens");
1922 });
1923 }
1924
1925 #[test]
1926 fn invoke_honours_a_callback_requested_terminate() {
1927 let _g = clock_guard();
1928 with_env(|env| {
1929 let mut t = timer_at(0, recording_cb as TimerCallbackType);
1930 let info = env.info();
1931 CB_RETURN_TERMINATE.store(true, Ordering::SeqCst);
1932
1933 let r = t.invoke(&info, &fake_clock_cb());
1934 assert_eq!(r.should_terminate, TerminateTimer::Terminate);
1935 assert_eq!(t.run_count, 1);
1937 assert_eq!(t.last_run, OptionInstant::Some(tick(0)));
1938 });
1939 }
1940
1941 #[test]
1942 fn invoke_skips_deterministically_when_the_clock_runs_backwards() {
1943 let _g = clock_guard();
1944 with_env(|env| {
1945 let mut t =
1946 timer_at(0, recording_cb as TimerCallbackType).with_interval(tick_dur(10));
1947 t.last_run = OptionInstant::Some(tick(1_000));
1948 let info = env.info();
1949
1950 set_now(0);
1953 let r = t.invoke(&info, &fake_clock_cb());
1954 assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 0);
1955 assert_eq!(r.should_terminate, TerminateTimer::Continue);
1956 assert_eq!(t.run_count, 0);
1957 assert_eq!(t.last_run, OptionInstant::Some(tick(1_000)), "last_run is untouched");
1958 });
1959 }
1960
1961 #[test]
1968 fn invoke_throttles_a_wall_clock_interval_on_a_tick_clock_at_the_exact_frame() {
1969 let _g = clock_guard();
1970 with_env(|env| {
1971 let mut t =
1973 timer_at(0, recording_cb as TimerCallbackType).with_interval(sys_dur_millis(60_000));
1974 let info = env.info();
1975
1976 let _ = t.invoke(&info, &fake_clock_cb());
1978 assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 1);
1979
1980 set_now(1);
1981 let _ = t.invoke(&info, &fake_clock_cb());
1982 assert_eq!(
1983 CB_INVOCATIONS.load(Ordering::SeqCst),
1984 1,
1985 "a 60s interval must not fire one frame later"
1986 );
1987
1988 set_now(3_599);
1990 let _ = t.invoke(&info, &fake_clock_cb());
1991 assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 1, "frame 3599 is early");
1992
1993 set_now(3_600);
1995 let _ = t.invoke(&info, &fake_clock_cb());
1996 assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 2, "frame 3600 is the flip");
1997 assert_eq!(t.run_count, 2);
1998 });
1999 }
2000
2001 #[test]
2014 fn invoke_with_a_tick_interval_fires_on_exactly_the_nth_frame() {
2015 let _g = clock_guard();
2016 with_env(|env| {
2017 let mut t = timer_at(0, recording_cb as TimerCallbackType).with_interval(tick_dur(5));
2018 let info = env.info();
2019
2020 let _ = t.invoke(&info, &fake_clock_cb());
2022 assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 1);
2023 assert_eq!(t.last_run, OptionInstant::Some(tick(0)));
2024
2025 for frame in 1..=4 {
2029 set_now(frame);
2030 let _ = t.invoke(&info, &fake_clock_cb());
2031 assert_eq!(
2032 CB_INVOCATIONS.load(Ordering::SeqCst),
2033 1,
2034 "frame {frame} is inside the 5-frame interval and must not fire"
2035 );
2036 }
2037
2038 set_now(5);
2040 let _ = t.invoke(&info, &fake_clock_cb());
2041 assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 2, "frame 5 must fire");
2042 assert_eq!(CB_SEEN_FRAME_START.load(Ordering::SeqCst), 5);
2043 assert_eq!(t.last_run, OptionInstant::Some(tick(5)));
2044
2045 set_now(6);
2047 let _ = t.invoke(&info, &fake_clock_cb());
2048 assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 2, "frame 6 restarts the wait");
2049
2050 set_now(10);
2051 let _ = t.invoke(&info, &fake_clock_cb());
2052 assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 3, "frame 10 is the second flip");
2053 });
2054 }
2055
2056 #[test]
2059 fn a_one_tick_interval_fires_on_every_frame() {
2060 let _g = clock_guard();
2061 with_env(|env| {
2062 let mut t = timer_at(0, recording_cb as TimerCallbackType).with_interval(tick_dur(1));
2063 let info = env.info();
2064
2065 for frame in 0..=10 {
2066 set_now(frame);
2067 let _ = t.invoke(&info, &fake_clock_cb());
2068 assert_eq!(
2069 CB_INVOCATIONS.load(Ordering::SeqCst),
2070 (frame + 1) as usize,
2071 "every frame up to {frame} must have fired exactly once"
2072 );
2073 }
2074 });
2075 }
2076
2077 #[test]
2080 fn a_tick_delay_gates_the_first_run_on_exactly_the_nth_frame() {
2081 let _g = clock_guard();
2082 with_env(|env| {
2083 let mut t = timer_at(0, recording_cb as TimerCallbackType).with_delay(tick_dur(3));
2084 let info = env.info();
2085
2086 for frame in 0..=2 {
2087 set_now(frame);
2088 let _ = t.invoke(&info, &fake_clock_cb());
2089 assert_eq!(
2090 CB_INVOCATIONS.load(Ordering::SeqCst),
2091 0,
2092 "frame {frame} is inside the 3-frame delay"
2093 );
2094 }
2095
2096 set_now(3);
2097 let _ = t.invoke(&info, &fake_clock_cb());
2098 assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 1, "frame 3 is the first run");
2099 });
2100 }
2101
2102 #[cfg(feature = "std")]
2113 #[test]
2114 fn a_tick_interval_throttles_a_wall_clock_timer_at_the_converted_boundary() {
2115 use azul_core::task::{
2116 advance_test_clock_ms, freeze_test_clock, get_system_time_libstd, reset_test_clock,
2117 };
2118
2119 let _g = clock_guard();
2120 reset_test_clock();
2121 freeze_test_clock();
2122 let real_clock = GetSystemTimeCallback {
2123 cb: get_system_time_libstd,
2124 };
2125
2126 with_env(|env| {
2127 let mut t = Timer::create(
2128 RefAny::new(0_usize),
2129 recording_cb as TimerCallbackType,
2130 real_clock,
2131 )
2132 .with_interval(tick_dur(5));
2133 let info = env.info();
2134
2135 let _ = t.invoke(&info, &real_clock);
2138 assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 1);
2139 assert!(
2140 matches!(t.last_run, OptionInstant::Some(Instant::System(_))),
2141 "this timer must really be running on the wall clock"
2142 );
2143
2144 let _ = advance_test_clock_ms(83);
2146 let _ = t.invoke(&info, &real_clock);
2147 assert_eq!(
2148 CB_INVOCATIONS.load(Ordering::SeqCst),
2149 1,
2150 "83ms is less than 5 frames (83.33ms) and must not fire"
2151 );
2152
2153 let _ = advance_test_clock_ms(1);
2155 let _ = t.invoke(&info, &real_clock);
2156 assert_eq!(
2157 CB_INVOCATIONS.load(Ordering::SeqCst),
2158 2,
2159 "84ms is past 5 frames and must fire"
2160 );
2161 });
2162
2163 reset_test_clock();
2164 }
2165
2166 #[test]
2167 fn invoke_at_the_end_of_time_does_not_panic() {
2168 let _g = clock_guard();
2169 with_env(|env| {
2170 let mut t = timer_at(u64::MAX, recording_cb as TimerCallbackType)
2171 .with_delay(tick_dur(u64::MAX))
2172 .with_interval(tick_dur(u64::MAX))
2173 .with_timeout(tick_dur(u64::MAX));
2174 let info = env.info();
2175
2176 set_now(u64::MAX);
2177 let r = t.invoke(&info, &fake_clock_cb());
2179 assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 0);
2180 assert_eq!(r.should_terminate, TerminateTimer::Continue);
2181 assert_eq!(t.run_count, 0);
2182 });
2183 }
2184}