Skip to main content

azul_layout/
timer.rs

1//! Timer callback information and utilities for azul-layout
2//!
3//! This module provides Timer, TimerCallbackInfo and related types for
4//! managing timers that run on the main UI thread.
5
6use 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
31/// Default timer tick interval in milliseconds when no interval is configured.
32const DEFAULT_TIMER_TICK_MS: u64 = 10;
33
34/// Callback type for timers
35pub type TimerCallbackType = extern "C" fn(
36    /* timer internal refany */ RefAny,
37    TimerCallbackInfo,
38) -> TimerCallbackReturn;
39
40/// Callback that runs on every frame on the main thread
41#[repr(C)]
42pub struct TimerCallback {
43    pub cb: TimerCallbackType,
44    /// For FFI: stores the foreign callable (e.g., `PyFunction`)
45    /// Native Rust code sets this to None
46    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/// A `Timer` is a function that runs on every frame or at intervals.
109#[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    /// How often, in REAL milliseconds, the platform layer should wake to service
143    /// this timer.
144    ///
145    /// A `Duration::Tick` interval is a FRAME count, so it is converted at the
146    /// nominal frame rate rather than passed through. Passing it through treated
147    /// one tick as one millisecond, so a `5t` interval asked the OS to wake ~16x
148    /// more often than the timer could possibly fire — a measurable idle burn for
149    /// a timer that flips at most every 5 frames.
150    #[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    /// Invoke the timer callback and update internal state.
194    ///
195    /// Returns a `TimerCallbackReturn` with `DoNothing` + `Continue` if the timer
196    /// is not ready to run yet (delay not elapsed for first run, or interval not
197    /// elapsed for subsequent runs). Forces `Terminate` when the timeout expires.
198    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        // Check if timer should run based on last_run, delay, and interval
206        match self.last_run.as_ref() {
207            Some(last_run) => {
208                // Timer has run before - check interval
209                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                // Timer has never run - check delay (first run)
220                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        // Create a new TimerCallbackInfo wrapping the callback_info
234        // CallbackInfo is Copy, so we can just copy it directly
235        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/// Information passed to timer callbacks.
278///
279/// This wraps `CallbackInfo` and adds timer-specific fields like `call_count` and `frame_start`.
280/// `CallbackInfo` methods are available via explicit delegation methods below.
281#[derive(Debug, Clone)]
282#[repr(C)]
283#[allow(clippy::pub_underscore_fields)] // _abi_ref/_abi_mut: intentional FFI/api.json ABI-stability placeholder fields
284pub 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    // ==================== Delegated CallbackInfo methods ====================
332    // These methods delegate to the inner callback_info to provide the same API
333    // as CallbackInfo without using Deref (which causes issues with FFI codegen)
334
335    /// Get the callable for FFI language bindings (Python, etc.)
336    #[must_use] pub fn get_ctx(&self) -> OptionRefAny {
337        self.callback_info.get_ctx()
338    }
339
340    /// Add a timer to this window (applied after callback returns)
341    pub fn add_timer(&mut self, timer_id: TimerId, timer: Timer) {
342        self.callback_info.add_timer(timer_id, timer);
343    }
344
345    /// Remove a timer from this window (applied after callback returns)
346    pub fn remove_timer(&mut self, timer_id: TimerId) {
347        self.callback_info.remove_timer(timer_id);
348    }
349
350    /// Add a thread to this window (applied after callback returns)
351    pub fn add_thread(&mut self, thread_id: ThreadId, thread: Thread) {
352        self.callback_info.add_thread(thread_id, thread);
353    }
354
355    /// Remove a thread from this window (applied after callback returns)
356    pub fn remove_thread(&mut self, thread_id: ThreadId) {
357        self.callback_info.remove_thread(thread_id);
358    }
359
360    /// Stop event propagation (applied after callback returns)
361    pub fn stop_propagation(&mut self) {
362        self.callback_info.stop_propagation();
363    }
364
365    /// Create a new window (applied after callback returns)
366    pub fn create_window(&mut self, options: WindowCreateOptions) {
367        self.callback_info.create_window(options);
368    }
369
370    /// Close the current window (applied after callback returns)
371    pub fn close_window(&mut self) {
372        self.callback_info.close_window();
373    }
374
375    /// Modify the window state (applied after callback returns)
376    pub fn modify_window_state(&mut self, state: FullWindowState) {
377        self.callback_info.modify_window_state(state);
378    }
379
380    /// Add an image to the image cache (applied after callback returns)
381    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    /// Remove an image from the image cache (applied after callback returns)
386    pub fn remove_image_from_cache(&mut self, id: AzString) {
387        self.callback_info.remove_image_from_cache(id);
388    }
389
390    /// Re-render ALL image callbacks across all DOMs (applied after callback returns)
391    ///
392    /// This is the most efficient way to update animated GL textures from a timer.
393    /// Triggers only texture re-rendering - no DOM rebuild or display list resubmission.
394    pub fn update_all_image_callbacks(&mut self) {
395        self.callback_info.update_all_image_callbacks();
396    }
397
398    /// Trigger re-rendering of a `VirtualView` (applied after callback returns)
399    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    /// Reload system fonts (applied after callback returns)
404    pub fn reload_system_fonts(&mut self) {
405        self.callback_info.reload_system_fonts();
406    }
407
408    /// Prevent the default action
409    pub fn prevent_default(&mut self) {
410        self.callback_info.prevent_default();
411    }
412
413    /// Open a menu
414    pub fn open_menu(&mut self, menu: Menu) {
415        self.callback_info.open_menu(menu);
416    }
417
418    /// Open a menu at a specific position
419    pub fn open_menu_at(&mut self, menu: Menu, position: LogicalPosition) {
420        self.callback_info.open_menu_at(menu, position);
421    }
422
423    /// Show a tooltip at the current cursor position
424    pub fn show_tooltip(&mut self, text: AzString) {
425        self.callback_info.show_tooltip(text);
426    }
427
428    /// Show a tooltip at a specific position
429    pub fn show_tooltip_at(&mut self, text: AzString, position: LogicalPosition) {
430        self.callback_info.show_tooltip_at(text, position);
431    }
432
433    /// Hide the currently displayed tooltip
434    pub fn hide_tooltip(&mut self) {
435        self.callback_info.hide_tooltip();
436    }
437
438    /// Open a menu positioned relative to the currently hit node
439    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    /// Get current window flags
444    #[must_use] pub const fn get_current_window_flags(&self) -> WindowFlags {
445        self.callback_info.get_current_window_flags()
446    }
447
448    /// Get current keyboard state
449    #[must_use] pub fn get_current_keyboard_state(&self) -> KeyboardState {
450        self.callback_info.get_current_keyboard_state()
451    }
452
453    /// Get current mouse state
454    #[must_use] pub const fn get_current_mouse_state(&self) -> MouseState {
455        self.callback_info.get_current_mouse_state()
456    }
457
458    /// Get the cursor position relative to the hit node
459    #[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    /// Get the cursor position relative to the viewport
464    #[must_use] pub const fn get_cursor_relative_to_viewport(&self) -> OptionLogicalPosition {
465        self.callback_info.get_cursor_relative_to_viewport()
466    }
467
468    /// Get the current cursor position
469    #[must_use] pub fn get_cursor_position(&self) -> Option<LogicalPosition> {
470        self.callback_info.get_cursor_position()
471    }
472
473    /// Get the current time (when the timer callback started)
474    #[must_use] pub fn get_current_time(&self) -> Instant {
475        self.frame_start.clone()
476    }
477
478    /// Check if any node in a specific DOM is focused
479    #[must_use] pub fn is_dom_focused(&self, dom_id: DomId) -> bool {
480        self.callback_info.is_dom_focused(dom_id)
481    }
482
483    /// Check if pen is in contact
484    #[must_use] pub fn is_pen_in_contact(&self) -> bool {
485        self.callback_info.is_pen_in_contact()
486    }
487
488    /// Check if pen eraser is active
489    #[must_use] pub fn is_pen_eraser(&self) -> bool {
490        self.callback_info.is_pen_eraser()
491    }
492
493    /// Check if pen barrel button is pressed
494    #[must_use] pub fn is_pen_barrel_button_pressed(&self) -> bool {
495        self.callback_info.is_pen_barrel_button_pressed()
496    }
497
498    /// Check if dragging is active
499    #[must_use] pub const fn is_dragging(&self) -> bool {
500        self.callback_info.get_current_mouse_state().left_down
501    }
502
503    /// Check if drag is active
504    #[must_use] pub const fn is_drag_active(&self) -> bool {
505        self.callback_info.get_current_mouse_state().left_down
506    }
507
508    /// Check if node drag is active
509    #[must_use] pub const fn is_node_drag_active(&self) -> bool {
510        self.callback_info.get_current_mouse_state().left_down
511    }
512
513    /// Check if file drag is active
514    #[must_use] pub fn is_file_drag_active(&self) -> bool {
515        self.callback_info.is_file_drag_active()
516    }
517
518    /// Check if there's sufficient history for gestures
519    #[must_use] pub fn has_sufficient_history_for_gestures(&self) -> bool {
520        self.callback_info.has_sufficient_history_for_gestures()
521    }
522
523    // ==================== Scroll Management (timer architecture) ====================
524
525    /// Get a read-only snapshot of a scroll node's bounds and position.
526    ///
527    /// Timer callbacks use this to read current scroll state for physics calculation.
528    #[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    /// Find the closest scrollable ancestor of a node.
537    ///
538    /// Used by auto-scroll timer to find which container to scroll when
539    /// the user drags beyond the container edge.
540    #[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    /// Get the scroll input queue for consuming pending scroll inputs.
549    ///
550    /// The physics timer calls `take_all()` each tick to drain inputs
551    /// recorded by platform event handlers.
552    #[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    /// Scroll a node to a specific position (via transactional `CallbackChange`).
560    ///
561    /// This is the primary way for timer callbacks to update scroll positions.
562    /// The change is applied after the callback returns.
563    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    /// Scroll to position without clamping (for rubber-banding/overscroll).
573    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    // Cursor blink timer methods
583    
584    /// Set cursor visibility state (for cursor blink timer)
585    pub fn set_cursor_visibility(&mut self, visible: bool) {
586        self.callback_info.set_cursor_visibility(visible);
587    }
588    
589    /// Toggle cursor visibility (for cursor blink timer).
590    pub fn set_cursor_visibility_toggle(&mut self) {
591        use crate::callbacks::CallbackChange;
592        self.callback_info.push_change(CallbackChange::ToggleCursorVisibility);
593    }
594    
595    /// Reset cursor blink state on user input
596    pub fn reset_cursor_blink(&mut self) {
597        self.callback_info.reset_cursor_blink();
598    }
599}
600
601/// Optional Timer type for API compatibility
602#[derive(Debug, Clone)]
603#[repr(C, u8)]
604// FFI Option enum; boxing the Some variant would break the #[repr(C, u8)] C ABI / api.json.
605#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
606#[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    // ------------------------------------------------------------------
666    // Time helpers
667    // ------------------------------------------------------------------
668
669    /// A tick-based `Instant` — the only kind constructible without a real clock,
670    /// and the only kind whose arithmetic is fully deterministic.
671    fn tick(t: u64) -> Instant {
672        Instant::Tick(SystemTick::new(t))
673    }
674
675    /// A tick-based `Duration`.
676    const fn tick_dur(d: u64) -> Duration {
677        Duration::Tick(SystemTickDiff { tick_diff: d })
678    }
679
680    /// A wall-clock-based `Duration` (deliberately the *wrong kind* to pair with
681    /// a `Tick` instant — several tests below pin the saturating behaviour of
682    /// exactly that mismatch).
683    const fn sys_dur_millis(ms: u64) -> Duration {
684        Duration::System(SystemTimeDiff::from_millis(ms))
685    }
686
687    /// Extract the tick counter, asserting the instant really is tick-based.
688    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    // ------------------------------------------------------------------
696    // Fake clock + recording callback
697    //
698    // `GetSystemTimeCallbackType` is a bare `extern "C" fn()` with no context
699    // pointer, so the fake clock has to live in statics. Every test that touches
700    // them takes `clock_guard()` first, which serialises them against the rest of
701    // the (parallel) test binary.
702    // ------------------------------------------------------------------
703
704    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    /// Serialises access to the fake-clock / recorder statics. Ignores poisoning:
713    /// a `#[should_panic]`-free suite still panics on assertion failure, and a
714    /// poisoned lock must not cascade into unrelated test failures.
715    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    /// Records everything the timer machinery handed it, and returns whatever
739    /// `CB_RETURN_TERMINATE` currently says.
740    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    // Two callbacks with *different bodies* — identical bodies are legal prey for
758    // identical-code folding, which would silently merge their addresses and make
759    // the `TimerCallback` Eq/Ord/Hash tests below vacuous.
760    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    /// A timer created at tick `created`, driven by the fake clock.
774    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    // ------------------------------------------------------------------
780    // CallbackInfo harness (mirrors the one in `scroll_timer.rs`)
781    // ------------------------------------------------------------------
782
783    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        /// A `TimerCallbackInfo` with no attached node, frame_start = tick 0.
811        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        /// Drain the log, asserting it holds exactly one change, and return it.
823        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    /// Builds a callback environment over an empty `LayoutWindow`. `left_down`
835    /// drives the mouse state the drag predicates read; `ctx` is what `get_ctx`
836    /// hands back to FFI bindings.
837    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: &current_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    // ==================================================================
879    // Timer::create / Default — constructor invariants
880    // ==================================================================
881
882    #[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        // Nothing is armed, so nothing can overflow off the end of time.
902        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    // ==================================================================
927    // Timer::tick_millis — numeric limits / round-trip
928    // ==================================================================
929
930    #[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        // A delay/timeout must NOT be mistaken for an interval.
938        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    /// A tick interval is a FRAME count, so the platform wake-up interval it
943    /// implies is `ticks / 60` seconds, not `ticks` milliseconds. The old
944    /// pass-through made a `5t` timer ask the OS to wake every 5ms for a callback
945    /// that can only fire every ~83ms.
946    #[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    /// `u64::MAX` ticks is ~9.7 billion years, which does NOT fit in `u64`
966    /// milliseconds. The conversion must clamp — wrapping would turn "never" into
967    /// some small interval and busy-wake the event loop forever.
968    #[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        // The largest tick interval that still converts without clamping.
975        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        // from_millis -> millis() is an exact round-trip, even at u64::MAX
984        // (secs*1000 + 615 lands exactly on u64::MAX without saturating).
985        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        // secs::MAX * 1000 overflows u64 — `millis()` must saturate, not panic.
995        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        // A 999_999ns interval is a *sub-millisecond* tick request; it truncates
1007        // to 0, i.e. "tick as fast as possible", not to 1.
1008        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    // ==================================================================
1014    // Timer::is_about_to_finish — predicate boundaries
1015    // ==================================================================
1016
1017    #[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        // Elapsed == timeout is NOT "about to finish" — the comparison is `>`.
1032        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        // `now` older than `created`: duration_since saturates to 0 rather than
1041        // underflowing, so the timer is simply "not finished".
1042        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    /// A wall-clock timeout on a tick-driven timer must EXPIRE. `duration_since`
1061    /// yields a Tick duration, which is now compared against the System timeout
1062    /// on a canonical scale instead of saturating to `false`.
1063    ///
1064    /// The old saturating comparison made this timeout unexpirable — a timer
1065    /// created with a timeout silently became a timer with no timeout, and the
1066    /// only symptom was a callback that ran forever.
1067    #[test]
1068    fn is_about_to_finish_expires_a_wall_clock_timeout_on_a_tick_clock() {
1069        let _g = clock_guard();
1070        // 1ms at 60Hz is less than one frame, so the FIRST tick already exceeds it.
1071        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        // A one-second timeout takes 60 whole frames, and expires strictly after.
1077        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    // ==================================================================
1084    // Timer::instant_of_next_run — getter invariants
1085    // ==================================================================
1086
1087    #[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        // NOTE: when BOTH are set, the schedule point is `base + delay + interval`
1108        // — the delay is re-added on every subsequent run, even though `invoke`
1109        // only gates the *first* run on the delay. Pinned here as current
1110        // behaviour; see the report.
1111        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        // Three MAXes added together: saturating_add, not an overflow panic.
1127        assert_eq!(tick_of(&t.instant_of_next_run()), u64::MAX);
1128    }
1129
1130    /// System durations on a Tick instant are CONVERTED to whole frames, not
1131    /// dropped. Dropping them made `instant_of_next_run == created`, i.e. the
1132    /// timer claimed to be due immediately and stayed that way forever, which is
1133    /// how a unit mismatch turned into a scheduling bug with no error.
1134    #[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        // 1000ms is 60 frames; delay + interval is 120.
1141        assert_eq!(tick_of(&t.instant_of_next_run()), 42 + 120);
1142    }
1143
1144    /// A sub-frame wall-clock interval rounds DOWN to zero frames on a tick
1145    /// clock. That is the truthful answer at one-frame resolution — but it does
1146    /// mean such a timer is always due, so it is pinned here deliberately rather
1147    /// than rounded up to 1 to make something happen.
1148    #[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    // ==================================================================
1156    // with_delay / with_interval / with_timeout — builder invariants
1157    // ==================================================================
1158
1159    #[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        // The builders must not disturb identity/progress fields.
1171        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        // Duration::max() is a System duration -> never expires on a tick clock.
1203        assert!(!t.is_about_to_finish(&tick(u64::MAX)));
1204    }
1205
1206    // ==================================================================
1207    // TimerCallback — identity, ordering, hashing
1208    // ==================================================================
1209
1210    #[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        // `create` and the `From` impl must agree.
1216        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        // Ord must be antisymmetric and consistent with partial_cmp.
1252        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    // ==================================================================
1264    // OptionTimer — round-trip
1265    // ==================================================================
1266
1267    #[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    // ==================================================================
1281    // TimerCallbackInfo::create + getters
1282    // ==================================================================
1283
1284    #[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            // Largest node index that survives the +1 encoding, in a DOM that
1332            // doesn't exist, on a window with no layout results at all.
1333            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            // A change pushed through the &mut view lands in the shared log.
1358            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    // ==================================================================
1380    // Delegated mutators — every one must land in the transaction log
1381    // ==================================================================
1382
1383    #[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            // A 0x0 null image with an empty tag is degenerate but legal.
1494            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            // Embedded NUL, an RTL override and astral-plane chars must survive
1503            // the AzString round-trip byte-for-byte.
1504            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            // Recorded verbatim — validation happens when the change is applied,
1522            // not here, and neither id may overflow on the way in.
1523            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            // No clamping/sanitising at this layer — but it must not panic either.
1561            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            // Hit node is NONE and the window has no layout results: the menu has
1578            // nothing to anchor to.
1579            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    // ==================================================================
1634    // Scroll delegation — numeric edges
1635    // ==================================================================
1636
1637    #[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            // NaN separately — it is never == to itself.
1690            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    // ==================================================================
1735    // Predicates / state getters
1736    // ==================================================================
1737
1738    #[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    // ==================================================================
1824    // Timer::invoke — the scheduling state machine
1825    // ==================================================================
1826
1827    #[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            // A skipped tick must leave the timer's progress untouched.
1841            assert_eq!(t.run_count, 0);
1842            assert_eq!(t.last_run, OptionInstant::None);
1843
1844            // The boundary is inclusive: elapsed == delay runs.
1845            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            // No delay -> the first invoke runs immediately.
1863            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            // The callback insists on Continue...
1909            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            // ...but the timeout overrides it, and the callback is told so.
1919            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            // Termination is the caller's job; invoke still records the run.
1936            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            // now(0) is *older* than last_run(1000): duration_since saturates to 0,
1951            // 0 < 10, so the tick is skipped — no panic, no spurious run.
1952            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    /// A wall-clock interval on a tick-driven clock must THROTTLE, at the exact
1962    /// frame the interval converts to.
1963    ///
1964    /// It used to run on every single invoke: the Tick-vs-System comparison
1965    /// saturated to `false`, so the "not yet" branch was never taken and the
1966    /// interval was silently a no-op. A 60-second timer fired at 60Hz.
1967    #[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            // 60_000ms is exactly 3600 frames at 60Hz.
1972            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            // First invoke has no `last_run`, so only the (absent) delay gates it.
1977            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            // One frame BEFORE the interval: still nothing.
1989            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            // Exactly at the interval: fires, because the gate is `elapsed < interval`.
1994            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    // ==================================================================
2002    // The `t` (tick) unit, end to end through Timer::invoke
2003    //
2004    // These are the tests the CSS `t` unit exists for: advance the clock by an
2005    // EXACT number of frames and assert the callback fired on that frame and no
2006    // other. Nothing here can be perturbed by how fast the machine is.
2007    // ==================================================================
2008
2009    /// A tick interval on a tick clock fires on exactly the Nth frame — not N-1,
2010    /// not N+1. This is the off-by-one detector: with a millisecond interval the
2011    /// boundary frame is whatever rounding produced, and a one-frame error hides
2012    /// inside the jitter.
2013    #[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            // Frame 0: first run (no last_run, no delay).
2021            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            // Frames 1..=4 are all early. Stepping one frame at a time is the
2026            // point: a boundary that is off by one shows up as a count mismatch
2027            // on a specific frame, not as a flaky total.
2028            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            // Frame 5 is the flip.
2039            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            // ...and frame 6 is early again for the NEXT interval.
2046            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    /// A `1t` interval fires on every single frame and never skips one — the
2057    /// degenerate case the unit has to get right for per-frame animation.
2058    #[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    /// A tick DELAY gates the first run on exactly the Nth frame, the same way a
2078    /// tick interval gates the rest.
2079    #[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    /// A tick interval on a WALL-CLOCK timer converts, so a `5t` CSS interval
2103    /// behaves on a desktop shell exactly as it does on a tick clock: 5 frames is
2104    /// 83.33ms, so 83ms is early and 84ms fires.
2105    ///
2106    /// This is the direction that used to be broken the OTHER way round — the
2107    /// mismatched comparison never took the "not yet" branch, so a `5t` interval
2108    /// on a real shell fired on every wake of the platform loop.
2109    ///
2110    /// Driven by the injectable clock (frozen, then advanced by an exact number
2111    /// of milliseconds), so it is a wall-clock path with no wall-clock jitter.
2112    #[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            // The first invoke has no `last_run`, so only the (absent) delay
2136            // gates it: it runs at +0ms and arms the interval.
2137            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            // 5 frames is 83.333ms, so 83ms is still inside the interval.
2145            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            // ...and one more millisecond is past it.
2154            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            // elapsed = 0, delay = MAX -> 0 < MAX -> skipped, no overflow anywhere.
2178            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}