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    #[must_use] pub const fn tick_millis(&self) -> u64 {
143        match self.interval.as_ref() {
144            Some(Duration::System(s)) => s.millis(),
145            Some(Duration::Tick(s)) => s.tick_diff,
146            None => DEFAULT_TIMER_TICK_MS,
147        }
148    }
149
150    #[must_use] pub fn is_about_to_finish(&self, instant_now: &Instant) -> bool {
151        match self.timeout {
152            OptionDuration::Some(timeout) => {
153                instant_now.duration_since(&self.created).greater_than(&timeout)
154            }
155            OptionDuration::None => false,
156        }
157    }
158
159    #[must_use] pub fn instant_of_next_run(&self) -> Instant {
160        let last_run = self.last_run.as_ref().map_or(&self.created, |s| s);
161
162        last_run
163            .clone()
164            .add_optional_duration(self.delay.as_ref())
165            .add_optional_duration(self.interval.as_ref())
166    }
167
168    #[inline]
169    #[must_use] pub const fn with_delay(mut self, delay: Duration) -> Self {
170        self.delay = OptionDuration::Some(delay);
171        self
172    }
173
174    #[inline]
175    #[must_use] pub const fn with_interval(mut self, interval: Duration) -> Self {
176        self.interval = OptionDuration::Some(interval);
177        self
178    }
179
180    #[inline]
181    #[must_use] pub const fn with_timeout(mut self, timeout: Duration) -> Self {
182        self.timeout = OptionDuration::Some(timeout);
183        self
184    }
185
186    /// Invoke the timer callback and update internal state.
187    ///
188    /// Returns a `TimerCallbackReturn` with `DoNothing` + `Continue` if the timer
189    /// is not ready to run yet (delay not elapsed for first run, or interval not
190    /// elapsed for subsequent runs). Forces `Terminate` when the timeout expires.
191    pub fn invoke(
192        &mut self,
193        callback_info: &CallbackInfo,
194        get_system_time_fn: &GetSystemTimeCallback,
195    ) -> TimerCallbackReturn {
196        let now = (get_system_time_fn.cb)();
197
198        // Check if timer should run based on last_run, delay, and interval
199        match self.last_run.as_ref() {
200            Some(last_run) => {
201                // Timer has run before - check interval
202                if let OptionDuration::Some(interval) = self.interval {
203                    if now.duration_since(last_run).smaller_than(&interval) {
204                        return TimerCallbackReturn {
205                            should_update: Update::DoNothing,
206                            should_terminate: TerminateTimer::Continue,
207                        };
208                    }
209                }
210            }
211            None => {
212                // Timer has never run - check delay (first run)
213                if let OptionDuration::Some(delay) = self.delay {
214                    if now.duration_since(&self.created).smaller_than(&delay) {
215                        return TimerCallbackReturn {
216                            should_update: Update::DoNothing,
217                            should_terminate: TerminateTimer::Continue,
218                        };
219                    }
220                }
221            }
222        }
223
224        let is_about_to_finish = self.is_about_to_finish(&now);
225
226        // Create a new TimerCallbackInfo wrapping the callback_info
227        // CallbackInfo is Copy, so we can just copy it directly
228        let mut timer_callback_info = TimerCallbackInfo {
229            callback_info: *callback_info,
230            node_id: self.node_id,
231            frame_start: now.clone(),
232            call_count: self.run_count,
233            is_about_to_finish,
234            _abi_ref: core::ptr::null(),
235            _abi_mut: core::ptr::null_mut(),
236        };
237
238        let mut result = (self.callback.cb)(self.refany.clone(), timer_callback_info);
239
240        if is_about_to_finish {
241            result.should_terminate = TerminateTimer::Terminate;
242        }
243
244        self.run_count += 1;
245        self.last_run = OptionInstant::Some(now);
246
247        result
248    }
249}
250
251impl Default for Timer {
252    fn default() -> Self {
253        extern "C" fn default_callback(_: RefAny, _: TimerCallbackInfo) -> TimerCallbackReturn {
254            TimerCallbackReturn::terminate_unchanged()
255        }
256
257        const extern "C" fn default_time() -> Instant {
258            Instant::Tick(azul_core::task::SystemTick { tick_counter: 0 })
259        }
260
261        let cb: TimerCallbackType = default_callback;
262        Self::create(
263            RefAny::new(()),
264            cb,
265            GetSystemTimeCallback { cb: default_time },
266        )
267    }
268}
269
270/// Information passed to timer callbacks.
271///
272/// This wraps `CallbackInfo` and adds timer-specific fields like `call_count` and `frame_start`.
273/// `CallbackInfo` methods are available via explicit delegation methods below.
274#[derive(Debug, Clone)]
275#[repr(C)]
276#[allow(clippy::pub_underscore_fields)] // _abi_ref/_abi_mut: intentional FFI/api.json ABI-stability placeholder fields
277pub struct TimerCallbackInfo {
278    pub callback_info: CallbackInfo,
279    pub node_id: OptionDomNodeId,
280    pub frame_start: Instant,
281    pub call_count: usize,
282    pub is_about_to_finish: bool,
283    pub _abi_ref: *const c_void,
284    pub _abi_mut: *mut c_void,
285}
286
287impl TimerCallbackInfo {
288    #[must_use] pub const fn create(
289        callback_info: CallbackInfo,
290        node_id: OptionDomNodeId,
291        frame_start: Instant,
292        call_count: usize,
293        is_about_to_finish: bool,
294    ) -> Self {
295        Self {
296            callback_info,
297            node_id,
298            frame_start,
299            call_count,
300            is_about_to_finish,
301            _abi_ref: core::ptr::null(),
302            _abi_mut: core::ptr::null_mut(),
303        }
304    }
305
306    #[must_use] pub fn get_attached_node_size(&self) -> Option<LogicalSize> {
307        let node_id = self.node_id.into_option()?;
308        self.callback_info.get_node_size(node_id)
309    }
310
311    #[must_use] pub fn get_attached_node_position(&self) -> Option<LogicalPosition> {
312        let node_id = self.node_id.into_option()?;
313        self.callback_info.get_node_position(node_id)
314    }
315
316    #[must_use] pub const fn get_callback_info(&self) -> &CallbackInfo {
317        &self.callback_info
318    }
319
320    pub const fn get_callback_info_mut(&mut self) -> &mut CallbackInfo {
321        &mut self.callback_info
322    }
323
324    // ==================== Delegated CallbackInfo methods ====================
325    // These methods delegate to the inner callback_info to provide the same API
326    // as CallbackInfo without using Deref (which causes issues with FFI codegen)
327
328    /// Get the callable for FFI language bindings (Python, etc.)
329    #[must_use] pub fn get_ctx(&self) -> OptionRefAny {
330        self.callback_info.get_ctx()
331    }
332
333    /// Add a timer to this window (applied after callback returns)
334    pub fn add_timer(&mut self, timer_id: TimerId, timer: Timer) {
335        self.callback_info.add_timer(timer_id, timer);
336    }
337
338    /// Remove a timer from this window (applied after callback returns)
339    pub fn remove_timer(&mut self, timer_id: TimerId) {
340        self.callback_info.remove_timer(timer_id);
341    }
342
343    /// Add a thread to this window (applied after callback returns)
344    pub fn add_thread(&mut self, thread_id: ThreadId, thread: Thread) {
345        self.callback_info.add_thread(thread_id, thread);
346    }
347
348    /// Remove a thread from this window (applied after callback returns)
349    pub fn remove_thread(&mut self, thread_id: ThreadId) {
350        self.callback_info.remove_thread(thread_id);
351    }
352
353    /// Stop event propagation (applied after callback returns)
354    pub fn stop_propagation(&mut self) {
355        self.callback_info.stop_propagation();
356    }
357
358    /// Create a new window (applied after callback returns)
359    pub fn create_window(&mut self, options: WindowCreateOptions) {
360        self.callback_info.create_window(options);
361    }
362
363    /// Close the current window (applied after callback returns)
364    pub fn close_window(&mut self) {
365        self.callback_info.close_window();
366    }
367
368    /// Modify the window state (applied after callback returns)
369    pub fn modify_window_state(&mut self, state: FullWindowState) {
370        self.callback_info.modify_window_state(state);
371    }
372
373    /// Add an image to the image cache (applied after callback returns)
374    pub fn add_image_to_cache(&mut self, id: AzString, image: ImageRef) {
375        self.callback_info.add_image_to_cache(id, image);
376    }
377
378    /// Remove an image from the image cache (applied after callback returns)
379    pub fn remove_image_from_cache(&mut self, id: AzString) {
380        self.callback_info.remove_image_from_cache(id);
381    }
382
383    /// Re-render ALL image callbacks across all DOMs (applied after callback returns)
384    ///
385    /// This is the most efficient way to update animated GL textures from a timer.
386    /// Triggers only texture re-rendering - no DOM rebuild or display list resubmission.
387    pub fn update_all_image_callbacks(&mut self) {
388        self.callback_info.update_all_image_callbacks();
389    }
390
391    /// Trigger re-rendering of a `VirtualView` (applied after callback returns)
392    pub fn trigger_virtual_view_rerender(&mut self, dom_id: DomId, node_id: NodeId) {
393        self.callback_info.trigger_virtual_view_rerender(dom_id, node_id);
394    }
395
396    /// Reload system fonts (applied after callback returns)
397    pub fn reload_system_fonts(&mut self) {
398        self.callback_info.reload_system_fonts();
399    }
400
401    /// Prevent the default action
402    pub fn prevent_default(&mut self) {
403        self.callback_info.prevent_default();
404    }
405
406    /// Open a menu
407    pub fn open_menu(&mut self, menu: Menu) {
408        self.callback_info.open_menu(menu);
409    }
410
411    /// Open a menu at a specific position
412    pub fn open_menu_at(&mut self, menu: Menu, position: LogicalPosition) {
413        self.callback_info.open_menu_at(menu, position);
414    }
415
416    /// Show a tooltip at the current cursor position
417    pub fn show_tooltip(&mut self, text: AzString) {
418        self.callback_info.show_tooltip(text);
419    }
420
421    /// Show a tooltip at a specific position
422    pub fn show_tooltip_at(&mut self, text: AzString, position: LogicalPosition) {
423        self.callback_info.show_tooltip_at(text, position);
424    }
425
426    /// Hide the currently displayed tooltip
427    pub fn hide_tooltip(&mut self) {
428        self.callback_info.hide_tooltip();
429    }
430
431    /// Open a menu positioned relative to the currently hit node
432    pub fn open_menu_for_hit_node(&mut self, menu: Menu) -> bool {
433        self.callback_info.open_menu_for_hit_node(menu)
434    }
435
436    /// Get current window flags
437    #[must_use] pub const fn get_current_window_flags(&self) -> WindowFlags {
438        self.callback_info.get_current_window_flags()
439    }
440
441    /// Get current keyboard state
442    #[must_use] pub fn get_current_keyboard_state(&self) -> KeyboardState {
443        self.callback_info.get_current_keyboard_state()
444    }
445
446    /// Get current mouse state
447    #[must_use] pub const fn get_current_mouse_state(&self) -> MouseState {
448        self.callback_info.get_current_mouse_state()
449    }
450
451    /// Get the cursor position relative to the hit node
452    #[must_use] pub const fn get_cursor_relative_to_node(&self) -> azul_core::geom::OptionCursorNodePosition {
453        self.callback_info.get_cursor_relative_to_node()
454    }
455
456    /// Get the cursor position relative to the viewport
457    #[must_use] pub const fn get_cursor_relative_to_viewport(&self) -> OptionLogicalPosition {
458        self.callback_info.get_cursor_relative_to_viewport()
459    }
460
461    /// Get the current cursor position
462    #[must_use] pub fn get_cursor_position(&self) -> Option<LogicalPosition> {
463        self.callback_info.get_cursor_position()
464    }
465
466    /// Get the current time (when the timer callback started)
467    #[must_use] pub fn get_current_time(&self) -> Instant {
468        self.frame_start.clone()
469    }
470
471    /// Check if any node in a specific DOM is focused
472    #[must_use] pub fn is_dom_focused(&self, dom_id: DomId) -> bool {
473        self.callback_info.is_dom_focused(dom_id)
474    }
475
476    /// Check if pen is in contact
477    #[must_use] pub fn is_pen_in_contact(&self) -> bool {
478        self.callback_info.is_pen_in_contact()
479    }
480
481    /// Check if pen eraser is active
482    #[must_use] pub fn is_pen_eraser(&self) -> bool {
483        self.callback_info.is_pen_eraser()
484    }
485
486    /// Check if pen barrel button is pressed
487    #[must_use] pub fn is_pen_barrel_button_pressed(&self) -> bool {
488        self.callback_info.is_pen_barrel_button_pressed()
489    }
490
491    /// Check if dragging is active
492    #[must_use] pub const fn is_dragging(&self) -> bool {
493        self.callback_info.get_current_mouse_state().left_down
494    }
495
496    /// Check if drag is active
497    #[must_use] pub const fn is_drag_active(&self) -> bool {
498        self.callback_info.get_current_mouse_state().left_down
499    }
500
501    /// Check if node drag is active
502    #[must_use] pub const fn is_node_drag_active(&self) -> bool {
503        self.callback_info.get_current_mouse_state().left_down
504    }
505
506    /// Check if file drag is active
507    #[must_use] pub fn is_file_drag_active(&self) -> bool {
508        self.callback_info.is_file_drag_active()
509    }
510
511    /// Check if there's sufficient history for gestures
512    #[must_use] pub fn has_sufficient_history_for_gestures(&self) -> bool {
513        self.callback_info.has_sufficient_history_for_gestures()
514    }
515
516    // ==================== Scroll Management (timer architecture) ====================
517
518    /// Get a read-only snapshot of a scroll node's bounds and position.
519    ///
520    /// Timer callbacks use this to read current scroll state for physics calculation.
521    #[must_use] pub fn get_scroll_node_info(
522        &self,
523        dom_id: DomId,
524        node_id: NodeId,
525    ) -> Option<crate::managers::scroll_state::ScrollNodeInfo> {
526        self.callback_info.get_scroll_node_info(dom_id, node_id)
527    }
528
529    /// Find the closest scrollable ancestor of a node.
530    ///
531    /// Used by auto-scroll timer to find which container to scroll when
532    /// the user drags beyond the container edge.
533    #[must_use] pub fn find_scroll_parent(
534        &self,
535        dom_id: DomId,
536        node_id: NodeId,
537    ) -> Option<NodeId> {
538        self.callback_info.find_scroll_parent(dom_id, node_id)
539    }
540
541    /// Get the scroll input queue for consuming pending scroll inputs.
542    ///
543    /// The physics timer calls `take_all()` each tick to drain inputs
544    /// recorded by platform event handlers.
545    #[cfg(feature = "std")]
546    #[must_use] pub fn get_scroll_input_queue(
547        &self,
548    ) -> crate::managers::scroll_state::ScrollInputQueue {
549        self.callback_info.get_scroll_input_queue()
550    }
551
552    /// Scroll a node to a specific position (via transactional `CallbackChange`).
553    ///
554    /// This is the primary way for timer callbacks to update scroll positions.
555    /// The change is applied after the callback returns.
556    pub fn scroll_to(
557        &mut self,
558        dom_id: DomId,
559        node_id: azul_core::styled_dom::NodeHierarchyItemId,
560        position: LogicalPosition,
561    ) {
562        self.callback_info.scroll_to(dom_id, node_id, position);
563    }
564
565    /// Scroll to position without clamping (for rubber-banding/overscroll).
566    pub fn scroll_to_unclamped(
567        &mut self,
568        dom_id: DomId,
569        node_id: azul_core::styled_dom::NodeHierarchyItemId,
570        position: LogicalPosition,
571    ) {
572        self.callback_info.scroll_to_unclamped(dom_id, node_id, position);
573    }
574
575    // Cursor blink timer methods
576    
577    /// Set cursor visibility state (for cursor blink timer)
578    pub fn set_cursor_visibility(&mut self, visible: bool) {
579        self.callback_info.set_cursor_visibility(visible);
580    }
581    
582    /// Toggle cursor visibility (for cursor blink timer).
583    pub fn set_cursor_visibility_toggle(&mut self) {
584        use crate::callbacks::CallbackChange;
585        self.callback_info.push_change(CallbackChange::ToggleCursorVisibility);
586    }
587    
588    /// Reset cursor blink state on user input
589    pub fn reset_cursor_blink(&mut self) {
590        self.callback_info.reset_cursor_blink();
591    }
592}
593
594/// Optional Timer type for API compatibility
595#[derive(Debug, Clone)]
596#[repr(C, u8)]
597// FFI Option enum; boxing the Some variant would break the #[repr(C, u8)] C ABI / api.json.
598#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
599#[allow(clippy::large_enum_variant)]
600pub enum OptionTimer {
601    None,
602    Some(Timer),
603}
604
605impl From<Option<Timer>> for OptionTimer {
606    fn from(o: Option<Timer>) -> Self {
607        o.map_or_else(|| Self::None, Self::Some)
608    }
609}
610
611impl OptionTimer {
612    #[must_use] pub fn into_option(self) -> Option<Timer> {
613        match self {
614            Self::None => None,
615            Self::Some(t) => Some(t),
616        }
617    }
618}
619
620#[cfg(all(test, feature = "std"))]
621#[allow(
622    clippy::float_cmp,
623    clippy::too_many_lines,
624    clippy::unreadable_literal,
625    clippy::cognitive_complexity
626)]
627mod autotest_generated {
628    use std::{
629        collections::BTreeMap,
630        sync::{
631            atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering},
632            Arc, Mutex, MutexGuard, PoisonError,
633        },
634    };
635
636    use azul_core::{
637        dom::DomNodeId,
638        gl::OptionGlContextPtr,
639        hit_test::ScrollPosition,
640        menu::MenuItemVec,
641        resources::{RawImageFormat, RendererResources},
642        styled_dom::NodeHierarchyItemId,
643        task::{SystemTick, SystemTickDiff, SystemTimeDiff, ThreadReceiver},
644        window::{MonitorVec, RawWindowHandle},
645    };
646    use azul_css::system::SystemStyle;
647    use rust_fontconfig::FcFontCache;
648
649    use super::*;
650    #[cfg(feature = "icu")]
651    use crate::icu::IcuLocalizerHandle;
652    use crate::{
653        callbacks::{CallbackChange, CallbackInfoRefData, ExternalSystemCallbacks},
654        thread::{ThreadCallbackType, ThreadSender},
655        window::LayoutWindow,
656    };
657
658    // ------------------------------------------------------------------
659    // Time helpers
660    // ------------------------------------------------------------------
661
662    /// A tick-based `Instant` — the only kind constructible without a real clock,
663    /// and the only kind whose arithmetic is fully deterministic.
664    fn tick(t: u64) -> Instant {
665        Instant::Tick(SystemTick::new(t))
666    }
667
668    /// A tick-based `Duration`.
669    const fn tick_dur(d: u64) -> Duration {
670        Duration::Tick(SystemTickDiff { tick_diff: d })
671    }
672
673    /// A wall-clock-based `Duration` (deliberately the *wrong kind* to pair with
674    /// a `Tick` instant — several tests below pin the saturating behaviour of
675    /// exactly that mismatch).
676    const fn sys_dur_millis(ms: u64) -> Duration {
677        Duration::System(SystemTimeDiff::from_millis(ms))
678    }
679
680    /// Extract the tick counter, asserting the instant really is tick-based.
681    fn tick_of(i: &Instant) -> u64 {
682        match i {
683            Instant::Tick(t) => t.tick_counter,
684            Instant::System(_) => panic!("expected a Tick instant, got a System one"),
685        }
686    }
687
688    // ------------------------------------------------------------------
689    // Fake clock + recording callback
690    //
691    // `GetSystemTimeCallbackType` is a bare `extern "C" fn()` with no context
692    // pointer, so the fake clock has to live in statics. Every test that touches
693    // them takes `clock_guard()` first, which serialises them against the rest of
694    // the (parallel) test binary.
695    // ------------------------------------------------------------------
696
697    static CLOCK_LOCK: Mutex<()> = Mutex::new(());
698    static FAKE_TICK: AtomicU64 = AtomicU64::new(0);
699    static CB_INVOCATIONS: AtomicUsize = AtomicUsize::new(0);
700    static CB_SEEN_CALL_COUNT: AtomicUsize = AtomicUsize::new(0);
701    static CB_SEEN_FRAME_START: AtomicU64 = AtomicU64::new(0);
702    static CB_SEEN_ABOUT_TO_FINISH: AtomicBool = AtomicBool::new(false);
703    static CB_RETURN_TERMINATE: AtomicBool = AtomicBool::new(false);
704
705    /// Serialises access to the fake-clock / recorder statics. Ignores poisoning:
706    /// a `#[should_panic]`-free suite still panics on assertion failure, and a
707    /// poisoned lock must not cascade into unrelated test failures.
708    fn clock_guard() -> MutexGuard<'static, ()> {
709        let guard = CLOCK_LOCK.lock().unwrap_or_else(PoisonError::into_inner);
710        FAKE_TICK.store(0, Ordering::SeqCst);
711        CB_INVOCATIONS.store(0, Ordering::SeqCst);
712        CB_SEEN_CALL_COUNT.store(0, Ordering::SeqCst);
713        CB_SEEN_FRAME_START.store(0, Ordering::SeqCst);
714        CB_SEEN_ABOUT_TO_FINISH.store(false, Ordering::SeqCst);
715        CB_RETURN_TERMINATE.store(false, Ordering::SeqCst);
716        guard
717    }
718
719    fn set_now(t: u64) {
720        FAKE_TICK.store(t, Ordering::SeqCst);
721    }
722
723    extern "C" fn fake_clock() -> Instant {
724        Instant::Tick(SystemTick::new(FAKE_TICK.load(Ordering::SeqCst)))
725    }
726
727    fn fake_clock_cb() -> GetSystemTimeCallback {
728        GetSystemTimeCallback { cb: fake_clock }
729    }
730
731    /// Records everything the timer machinery handed it, and returns whatever
732    /// `CB_RETURN_TERMINATE` currently says.
733    extern "C" fn recording_cb(_data: RefAny, info: TimerCallbackInfo) -> TimerCallbackReturn {
734        CB_INVOCATIONS.fetch_add(1, Ordering::SeqCst);
735        CB_SEEN_CALL_COUNT.store(info.call_count, Ordering::SeqCst);
736        CB_SEEN_ABOUT_TO_FINISH.store(info.is_about_to_finish, Ordering::SeqCst);
737        if let Instant::Tick(t) = &info.frame_start {
738            CB_SEEN_FRAME_START.store(t.tick_counter, Ordering::SeqCst);
739        }
740        TimerCallbackReturn {
741            should_update: Update::DoNothing,
742            should_terminate: if CB_RETURN_TERMINATE.load(Ordering::SeqCst) {
743                TerminateTimer::Terminate
744            } else {
745                TerminateTimer::Continue
746            },
747        }
748    }
749
750    // Two callbacks with *different bodies* — identical bodies are legal prey for
751    // identical-code folding, which would silently merge their addresses and make
752    // the `TimerCallback` Eq/Ord/Hash tests below vacuous.
753    extern "C" fn cb_alpha(_d: RefAny, _i: TimerCallbackInfo) -> TimerCallbackReturn {
754        TimerCallbackReturn {
755            should_update: Update::RefreshDom,
756            should_terminate: TerminateTimer::Terminate,
757        }
758    }
759    extern "C" fn cb_beta(_d: RefAny, _i: TimerCallbackInfo) -> TimerCallbackReturn {
760        TimerCallbackReturn {
761            should_update: Update::DoNothing,
762            should_terminate: TerminateTimer::Continue,
763        }
764    }
765
766    /// A timer created at tick `created`, driven by the fake clock.
767    fn timer_at(created: u64, cb: TimerCallbackType) -> Timer {
768        set_now(created);
769        Timer::create(RefAny::new(0_usize), cb, fake_clock_cb())
770    }
771
772    // ------------------------------------------------------------------
773    // CallbackInfo harness (mirrors the one in `scroll_timer.rs`)
774    // ------------------------------------------------------------------
775
776    struct Env<'a> {
777        ref_data: &'a CallbackInfoRefData<'a>,
778        changes: &'a Arc<Mutex<Vec<CallbackChange>>>,
779    }
780
781    impl Env<'_> {
782        fn info(&self) -> CallbackInfo {
783            self.info_with(OptionLogicalPosition::None, OptionLogicalPosition::None)
784        }
785
786        fn info_with(
787            &self,
788            cursor_relative_to_item: OptionLogicalPosition,
789            cursor_in_viewport: OptionLogicalPosition,
790        ) -> CallbackInfo {
791            CallbackInfo::new(
792                self.ref_data,
793                self.changes,
794                DomNodeId {
795                    dom: DomId::ROOT_ID,
796                    node: NodeHierarchyItemId::NONE,
797                },
798                cursor_relative_to_item,
799                cursor_in_viewport,
800            )
801        }
802
803        /// A `TimerCallbackInfo` with no attached node, frame_start = tick 0.
804        fn timer_info(&self) -> TimerCallbackInfo {
805            TimerCallbackInfo::create(self.info(), OptionDomNodeId::None, tick(0), 0, false)
806        }
807
808        fn take_changes(&self) -> Vec<CallbackChange> {
809            self.changes
810                .lock()
811                .map(|mut c| core::mem::take(&mut *c))
812                .unwrap_or_default()
813        }
814
815        /// Drain the log, asserting it holds exactly one change, and return it.
816        fn take_one(&self) -> CallbackChange {
817            let mut changes = self.take_changes();
818            assert_eq!(changes.len(), 1, "expected exactly one change: {changes:?}");
819            changes.remove(0)
820        }
821    }
822
823    fn with_env<R>(f: impl FnOnce(&Env<'_>) -> R) -> R {
824        with_env_cfg(false, OptionRefAny::None, f)
825    }
826
827    /// Builds a callback environment over an empty `LayoutWindow`. `left_down`
828    /// drives the mouse state the drag predicates read; `ctx` is what `get_ctx`
829    /// hands back to FFI bindings.
830    fn with_env_cfg<R>(left_down: bool, ctx: OptionRefAny, f: impl FnOnce(&Env<'_>) -> R) -> R {
831        let layout_window =
832            LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new failed");
833        let renderer_resources = RendererResources::default();
834        let previous_window_state: Option<FullWindowState> = None;
835        let mut current_window_state = FullWindowState::default();
836        current_window_state.mouse_state.left_down = left_down;
837        let gl_context = OptionGlContextPtr::None;
838        let scroll_states: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> =
839            BTreeMap::new();
840        let window_handle = RawWindowHandle::Unsupported;
841        let system_callbacks = ExternalSystemCallbacks::rust_internal();
842
843        let ref_data = CallbackInfoRefData {
844            layout_window: &layout_window,
845            renderer_resources: &renderer_resources,
846            previous_window_state: &previous_window_state,
847            current_window_state: &current_window_state,
848            gl_context: &gl_context,
849            current_scroll_manager: &scroll_states,
850            current_window_handle: &window_handle,
851            system_callbacks: &system_callbacks,
852            system_style: Arc::new(SystemStyle::default()),
853            monitors: Arc::new(Mutex::new(MonitorVec::from_const_slice(&[]))),
854            #[cfg(feature = "icu")]
855            icu_localizer: IcuLocalizerHandle::default(),
856            ctx,
857        };
858
859        let changes: Arc<Mutex<Vec<CallbackChange>>> = Arc::new(Mutex::new(Vec::new()));
860        let env = Env {
861            ref_data: &ref_data,
862            changes: &changes,
863        };
864        f(&env)
865    }
866
867    fn empty_menu() -> Menu {
868        Menu::create(MenuItemVec::from_const_slice(&[]))
869    }
870
871    // ==================================================================
872    // Timer::create / Default — constructor invariants
873    // ==================================================================
874
875    #[test]
876    fn timer_create_starts_completely_unarmed() {
877        let _g = clock_guard();
878        let t = timer_at(12_345, recording_cb as TimerCallbackType);
879
880        assert_eq!(tick_of(&t.created), 12_345, "created must come from the clock");
881        assert_eq!(t.run_count, 0);
882        assert_eq!(t.last_run, OptionInstant::None);
883        assert_eq!(t.delay, OptionDuration::None);
884        assert_eq!(t.interval, OptionDuration::None);
885        assert_eq!(t.timeout, OptionDuration::None);
886        assert_eq!(t.node_id, OptionDomNodeId::None);
887    }
888
889    #[test]
890    fn timer_create_at_max_tick_does_not_panic() {
891        let _g = clock_guard();
892        let t = timer_at(u64::MAX, recording_cb as TimerCallbackType);
893        assert_eq!(tick_of(&t.created), u64::MAX);
894        // Nothing is armed, so nothing can overflow off the end of time.
895        assert!(!t.is_about_to_finish(&tick(u64::MAX)));
896        assert_eq!(tick_of(&t.instant_of_next_run()), u64::MAX);
897    }
898
899    #[test]
900    fn timer_default_is_a_zero_tick_timer() {
901        let t = Timer::default();
902        assert_eq!(tick_of(&t.created), 0);
903        assert_eq!(t.run_count, 0);
904        assert_eq!(t.last_run, OptionInstant::None);
905        assert_eq!(t.tick_millis(), DEFAULT_TIMER_TICK_MS);
906    }
907
908    #[test]
909    fn timer_clone_equals_original() {
910        let _g = clock_guard();
911        let t = timer_at(7, recording_cb as TimerCallbackType)
912            .with_delay(tick_dur(1))
913            .with_interval(tick_dur(2))
914            .with_timeout(tick_dur(3));
915        let c = t.clone();
916        assert_eq!(t, c, "Clone must be value-preserving");
917    }
918
919    // ==================================================================
920    // Timer::tick_millis — numeric limits / round-trip
921    // ==================================================================
922
923    #[test]
924    fn tick_millis_falls_back_to_default_without_interval() {
925        let _g = clock_guard();
926        let t = timer_at(0, recording_cb as TimerCallbackType);
927        assert_eq!(t.tick_millis(), DEFAULT_TIMER_TICK_MS);
928        assert_eq!(t.tick_millis(), 10);
929
930        // A delay/timeout must NOT be mistaken for an interval.
931        let t = t.with_delay(tick_dur(999)).with_timeout(tick_dur(888));
932        assert_eq!(t.tick_millis(), DEFAULT_TIMER_TICK_MS);
933    }
934
935    #[test]
936    fn tick_millis_passes_tick_intervals_through_unclamped() {
937        let _g = clock_guard();
938        for raw in [0_u64, 1, 10, u64::from(u32::MAX), u64::MAX] {
939            let t = timer_at(0, recording_cb as TimerCallbackType).with_interval(tick_dur(raw));
940            assert_eq!(t.tick_millis(), raw, "tick interval {raw} must round-trip");
941        }
942    }
943
944    #[test]
945    fn tick_millis_system_interval_round_trips_whole_millis() {
946        let _g = clock_guard();
947        // from_millis -> millis() is an exact round-trip, even at u64::MAX
948        // (secs*1000 + 615 lands exactly on u64::MAX without saturating).
949        for ms in [0_u64, 1, 999, 1_000, 1_001, 86_400_000, u64::MAX] {
950            let t = timer_at(0, recording_cb as TimerCallbackType).with_interval(sys_dur_millis(ms));
951            assert_eq!(t.tick_millis(), ms, "millis {ms} must round-trip");
952        }
953    }
954
955    #[test]
956    fn tick_millis_saturates_instead_of_overflowing() {
957        let _g = clock_guard();
958        // secs::MAX * 1000 overflows u64 — `millis()` must saturate, not panic.
959        let huge = Duration::System(SystemTimeDiff {
960            secs: u64::MAX,
961            nanos: 999_999_999,
962        });
963        let t = timer_at(0, recording_cb as TimerCallbackType).with_interval(huge);
964        assert_eq!(t.tick_millis(), u64::MAX);
965    }
966
967    #[test]
968    fn tick_millis_truncates_sub_millisecond_intervals_to_zero() {
969        let _g = clock_guard();
970        // A 999_999ns interval is a *sub-millisecond* tick request; it truncates
971        // to 0, i.e. "tick as fast as possible", not to 1.
972        let t = timer_at(0, recording_cb as TimerCallbackType)
973            .with_interval(Duration::System(SystemTimeDiff::from_nanos(999_999)));
974        assert_eq!(t.tick_millis(), 0);
975    }
976
977    // ==================================================================
978    // Timer::is_about_to_finish — predicate boundaries
979    // ==================================================================
980
981    #[test]
982    fn is_about_to_finish_is_false_without_a_timeout() {
983        let _g = clock_guard();
984        let t = timer_at(0, recording_cb as TimerCallbackType);
985        assert!(!t.is_about_to_finish(&tick(0)));
986        assert!(!t.is_about_to_finish(&tick(u64::MAX)), "no timeout = never finishes");
987    }
988
989    #[test]
990    fn is_about_to_finish_boundary_is_strictly_greater() {
991        let _g = clock_guard();
992        let t = timer_at(100, recording_cb as TimerCallbackType).with_timeout(tick_dur(50));
993
994        assert!(!t.is_about_to_finish(&tick(149)), "1 tick early");
995        // Elapsed == timeout is NOT "about to finish" — the comparison is `>`.
996        assert!(!t.is_about_to_finish(&tick(150)), "exactly at the timeout");
997        assert!(t.is_about_to_finish(&tick(151)), "1 tick past the timeout");
998    }
999
1000    #[test]
1001    fn is_about_to_finish_saturates_when_the_clock_runs_backwards() {
1002        let _g = clock_guard();
1003        let t = timer_at(1_000, recording_cb as TimerCallbackType).with_timeout(tick_dur(10));
1004        // `now` older than `created`: duration_since saturates to 0 rather than
1005        // underflowing, so the timer is simply "not finished".
1006        assert!(!t.is_about_to_finish(&tick(0)));
1007    }
1008
1009    #[test]
1010    fn is_about_to_finish_at_the_u64_ceiling() {
1011        let _g = clock_guard();
1012        let t = timer_at(0, recording_cb as TimerCallbackType);
1013
1014        let max_timeout = t.clone().with_timeout(tick_dur(u64::MAX));
1015        assert!(
1016            !max_timeout.is_about_to_finish(&tick(u64::MAX)),
1017            "MAX elapsed is not > MAX timeout"
1018        );
1019
1020        let near_max = t.with_timeout(tick_dur(u64::MAX - 1));
1021        assert!(near_max.is_about_to_finish(&tick(u64::MAX)));
1022    }
1023
1024    #[test]
1025    fn is_about_to_finish_never_fires_on_a_mismatched_clock_kind() {
1026        let _g = clock_guard();
1027        // A wall-clock timeout on a tick-driven timer: `duration_since` yields a
1028        // Tick duration, and Tick-vs-System comparison saturates to false. The
1029        // timeout can therefore NEVER expire — it degrades to "no timeout"
1030        // instead of panicking or firing immediately.
1031        let t = timer_at(0, recording_cb as TimerCallbackType).with_timeout(sys_dur_millis(1));
1032        assert!(!t.is_about_to_finish(&tick(u64::MAX)));
1033    }
1034
1035    // ==================================================================
1036    // Timer::instant_of_next_run — getter invariants
1037    // ==================================================================
1038
1039    #[test]
1040    fn instant_of_next_run_is_created_when_nothing_is_armed() {
1041        let _g = clock_guard();
1042        let t = timer_at(42, recording_cb as TimerCallbackType);
1043        assert_eq!(tick_of(&t.instant_of_next_run()), 42);
1044    }
1045
1046    #[test]
1047    fn instant_of_next_run_prefers_last_run_over_created() {
1048        let _g = clock_guard();
1049        let mut t = timer_at(100, recording_cb as TimerCallbackType).with_interval(tick_dur(7));
1050        assert_eq!(tick_of(&t.instant_of_next_run()), 107, "no run yet: created + interval");
1051
1052        t.last_run = OptionInstant::Some(tick(500));
1053        assert_eq!(tick_of(&t.instant_of_next_run()), 507, "after a run: last_run + interval");
1054    }
1055
1056    #[test]
1057    fn instant_of_next_run_sums_delay_and_interval() {
1058        let _g = clock_guard();
1059        // NOTE: when BOTH are set, the schedule point is `base + delay + interval`
1060        // — the delay is re-added on every subsequent run, even though `invoke`
1061        // only gates the *first* run on the delay. Pinned here as current
1062        // behaviour; see the report.
1063        let mut t = timer_at(100, recording_cb as TimerCallbackType)
1064            .with_delay(tick_dur(5))
1065            .with_interval(tick_dur(7));
1066        assert_eq!(tick_of(&t.instant_of_next_run()), 112);
1067
1068        t.last_run = OptionInstant::Some(tick(200));
1069        assert_eq!(tick_of(&t.instant_of_next_run()), 212);
1070    }
1071
1072    #[test]
1073    fn instant_of_next_run_saturates_at_the_end_of_time() {
1074        let _g = clock_guard();
1075        let t = timer_at(u64::MAX, recording_cb as TimerCallbackType)
1076            .with_delay(tick_dur(u64::MAX))
1077            .with_interval(tick_dur(u64::MAX));
1078        // Three MAXes added together: saturating_add, not an overflow panic.
1079        assert_eq!(tick_of(&t.instant_of_next_run()), u64::MAX);
1080    }
1081
1082    #[test]
1083    fn instant_of_next_run_ignores_mismatched_duration_kinds() {
1084        let _g = clock_guard();
1085        // System durations on a Tick instant are an undefined combination and are
1086        // dropped (saturate to `self`) rather than panicking.
1087        let t = timer_at(42, recording_cb as TimerCallbackType)
1088            .with_delay(sys_dur_millis(1_000))
1089            .with_interval(sys_dur_millis(1_000));
1090        assert_eq!(tick_of(&t.instant_of_next_run()), 42);
1091    }
1092
1093    // ==================================================================
1094    // with_delay / with_interval / with_timeout — builder invariants
1095    // ==================================================================
1096
1097    #[test]
1098    fn with_setters_are_independent_and_preserve_the_rest() {
1099        let _g = clock_guard();
1100        let t = timer_at(9, recording_cb as TimerCallbackType)
1101            .with_delay(tick_dur(1))
1102            .with_interval(tick_dur(2))
1103            .with_timeout(tick_dur(3));
1104
1105        assert_eq!(t.delay, OptionDuration::Some(tick_dur(1)));
1106        assert_eq!(t.interval, OptionDuration::Some(tick_dur(2)));
1107        assert_eq!(t.timeout, OptionDuration::Some(tick_dur(3)));
1108        // The builders must not disturb identity/progress fields.
1109        assert_eq!(tick_of(&t.created), 9);
1110        assert_eq!(t.run_count, 0);
1111        assert_eq!(t.last_run, OptionInstant::None);
1112    }
1113
1114    #[test]
1115    fn with_setters_are_last_write_wins() {
1116        let _g = clock_guard();
1117        let t = timer_at(0, recording_cb as TimerCallbackType)
1118            .with_delay(tick_dur(1))
1119            .with_delay(tick_dur(2))
1120            .with_interval(tick_dur(3))
1121            .with_interval(tick_dur(4))
1122            .with_timeout(tick_dur(5))
1123            .with_timeout(tick_dur(6));
1124
1125        assert_eq!(t.delay, OptionDuration::Some(tick_dur(2)));
1126        assert_eq!(t.interval, OptionDuration::Some(tick_dur(4)));
1127        assert_eq!(t.timeout, OptionDuration::Some(tick_dur(6)));
1128    }
1129
1130    #[test]
1131    fn with_setters_accept_extreme_durations() {
1132        let _g = clock_guard();
1133        let t = timer_at(0, recording_cb as TimerCallbackType)
1134            .with_delay(tick_dur(0))
1135            .with_interval(tick_dur(u64::MAX))
1136            .with_timeout(Duration::max());
1137
1138        assert_eq!(t.delay, OptionDuration::Some(tick_dur(0)));
1139        assert_eq!(t.tick_millis(), u64::MAX);
1140        // Duration::max() is a System duration -> never expires on a tick clock.
1141        assert!(!t.is_about_to_finish(&tick(u64::MAX)));
1142    }
1143
1144    // ==================================================================
1145    // TimerCallback — identity, ordering, hashing
1146    // ==================================================================
1147
1148    #[test]
1149    fn timer_callback_create_has_no_ffi_ctx() {
1150        let cb = TimerCallback::create(cb_alpha as TimerCallbackType);
1151        assert_eq!(cb.ctx, OptionRefAny::None);
1152
1153        // `create` and the `From` impl must agree.
1154        let from: TimerCallback = (cb_alpha as TimerCallbackType).into();
1155        assert_eq!(cb, from);
1156    }
1157
1158    #[test]
1159    fn timer_callback_identity_is_by_function_pointer() {
1160        let a1 = TimerCallback::create(cb_alpha as TimerCallbackType);
1161        let a2 = TimerCallback::create(cb_alpha as TimerCallbackType);
1162        let b = TimerCallback::create(cb_beta as TimerCallbackType);
1163
1164        assert_eq!(a1, a2, "same fn -> equal");
1165        assert_ne!(a1, b, "different fn -> not equal");
1166        assert_eq!(a1, a1.clone(), "Clone preserves identity");
1167    }
1168
1169    #[test]
1170    fn timer_callback_ord_and_hash_agree_with_eq() {
1171        use std::{
1172            collections::hash_map::DefaultHasher,
1173            hash::{Hash, Hasher},
1174        };
1175
1176        fn hash_of(cb: &TimerCallback) -> u64 {
1177            let mut h = DefaultHasher::new();
1178            cb.hash(&mut h);
1179            h.finish()
1180        }
1181
1182        let a = TimerCallback::create(cb_alpha as TimerCallbackType);
1183        let a2 = a.clone();
1184        let b = TimerCallback::create(cb_beta as TimerCallbackType);
1185
1186        assert_eq!(a.cmp(&a2), core::cmp::Ordering::Equal);
1187        assert_eq!(hash_of(&a), hash_of(&a2), "Eq values must hash equal");
1188
1189        // Ord must be antisymmetric and consistent with partial_cmp.
1190        assert_eq!(a.cmp(&b), a.partial_cmp(&b).unwrap());
1191        assert_eq!(a.cmp(&b).reverse(), b.cmp(&a));
1192        assert_ne!(a.cmp(&b), core::cmp::Ordering::Equal, "distinct fns must order strictly");
1193    }
1194
1195    #[test]
1196    fn timer_callback_debug_does_not_panic() {
1197        let s = format!("{:?}", TimerCallback::create(cb_alpha as TimerCallbackType));
1198        assert!(s.starts_with("TimerCallback"), "got {s}");
1199    }
1200
1201    // ==================================================================
1202    // OptionTimer — round-trip
1203    // ==================================================================
1204
1205    #[test]
1206    fn option_timer_round_trips_both_variants() {
1207        let _g = clock_guard();
1208        assert!(OptionTimer::None.into_option().is_none());
1209        assert!(OptionTimer::from(None).into_option().is_none());
1210
1211        let t = timer_at(3, recording_cb as TimerCallbackType).with_interval(tick_dur(4));
1212        let round_tripped = OptionTimer::from(Some(t.clone()))
1213            .into_option()
1214            .expect("Some must survive the round-trip");
1215        assert_eq!(round_tripped, t, "encode == decode");
1216    }
1217
1218    // ==================================================================
1219    // TimerCallbackInfo::create + getters
1220    // ==================================================================
1221
1222    #[test]
1223    fn timer_callback_info_create_preserves_extremes() {
1224        with_env(|env| {
1225            let info = TimerCallbackInfo::create(
1226                env.info(),
1227                OptionDomNodeId::None,
1228                tick(u64::MAX),
1229                usize::MAX,
1230                true,
1231            );
1232            assert_eq!(info.call_count, usize::MAX, "no wrap at usize::MAX");
1233            assert!(info.is_about_to_finish);
1234            assert_eq!(tick_of(&info.frame_start), u64::MAX);
1235            assert!(info._abi_ref.is_null());
1236            assert!(info._abi_mut.is_null());
1237
1238            let zero =
1239                TimerCallbackInfo::create(env.info(), OptionDomNodeId::None, tick(0), 0, false);
1240            assert_eq!(zero.call_count, 0);
1241            assert!(!zero.is_about_to_finish);
1242            assert_eq!(tick_of(&zero.frame_start), 0);
1243        });
1244    }
1245
1246    #[test]
1247    fn get_current_time_returns_frame_start_verbatim() {
1248        with_env(|env| {
1249            for t in [0_u64, 1, u64::MAX] {
1250                let info =
1251                    TimerCallbackInfo::create(env.info(), OptionDomNodeId::None, tick(t), 0, false);
1252                assert_eq!(info.get_current_time(), tick(t));
1253            }
1254        });
1255    }
1256
1257    #[test]
1258    fn attached_node_queries_are_none_without_an_attached_node() {
1259        with_env(|env| {
1260            let info = env.timer_info();
1261            assert!(info.get_attached_node_size().is_none());
1262            assert!(info.get_attached_node_position().is_none());
1263        });
1264    }
1265
1266    #[test]
1267    fn attached_node_queries_are_none_for_a_bogus_node() {
1268        with_env(|env| {
1269            // Largest node index that survives the +1 encoding, in a DOM that
1270            // doesn't exist, on a window with no layout results at all.
1271            let bogus = DomNodeId {
1272                dom: DomId { inner: usize::MAX },
1273                node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(usize::MAX - 1))),
1274            };
1275            let info = TimerCallbackInfo::create(
1276                env.info(),
1277                OptionDomNodeId::Some(bogus),
1278                tick(0),
1279                0,
1280                false,
1281            );
1282            assert!(info.get_attached_node_size().is_none(), "must not panic or index OOB");
1283            assert!(info.get_attached_node_position().is_none());
1284        });
1285    }
1286
1287    #[test]
1288    fn get_callback_info_and_mut_alias_the_same_inner_info() {
1289        with_env(|env| {
1290            let mut info = env.timer_info();
1291            let addr_shared = std::ptr::from_ref(info.get_callback_info());
1292            let addr_mut = std::ptr::from_mut(info.get_callback_info_mut()).cast_const();
1293            assert!(std::ptr::eq(addr_shared, addr_mut), "both must alias the inner CallbackInfo");
1294
1295            // A change pushed through the &mut view lands in the shared log.
1296            info.get_callback_info_mut().prevent_default();
1297            assert!(matches!(env.take_one(), CallbackChange::PreventDefault));
1298        });
1299    }
1300
1301    #[test]
1302    fn get_ctx_is_none_for_native_rust_callbacks() {
1303        with_env(|env| {
1304            assert_eq!(env.timer_info().get_ctx(), OptionRefAny::None);
1305        });
1306    }
1307
1308    #[test]
1309    fn get_ctx_hands_back_the_ffi_callable() {
1310        let ctx = RefAny::new(0xDEAD_BEEF_u32);
1311        with_env_cfg(false, OptionRefAny::Some(ctx.clone()), |env| {
1312            let got = env.timer_info().get_ctx().into_option().expect("ctx must survive");
1313            assert_eq!(got, ctx, "get_ctx must hand back the same RefAny");
1314        });
1315    }
1316
1317    // ==================================================================
1318    // Delegated mutators — every one must land in the transaction log
1319    // ==================================================================
1320
1321    #[test]
1322    fn add_and_remove_timer_push_the_matching_changes() {
1323        let _g = clock_guard();
1324        with_env(|env| {
1325            let mut info = env.timer_info();
1326            let id = TimerId { id: usize::MAX };
1327            info.add_timer(id, timer_at(1, recording_cb as TimerCallbackType));
1328            let CallbackChange::AddTimer { timer_id, timer } = env.take_one() else {
1329                panic!("expected AddTimer");
1330            };
1331            assert_eq!(timer_id, id);
1332            assert_eq!(tick_of(&timer.created), 1, "the timer must be stored verbatim");
1333
1334            info.remove_timer(TimerId { id: 0 });
1335            let CallbackChange::RemoveTimer { timer_id } = env.take_one() else {
1336                panic!("expected RemoveTimer");
1337            };
1338            assert_eq!(timer_id.id, 0, "id 0 (a reserved system id) is still accepted");
1339        });
1340    }
1341
1342    #[test]
1343    fn add_and_remove_thread_push_the_matching_changes() {
1344        extern "C" fn noop_worker(_d: RefAny, _s: ThreadSender, _r: ThreadReceiver) {}
1345
1346        with_env(|env| {
1347            let mut info = env.timer_info();
1348            let id = ThreadId::unique();
1349            let thread = Thread::create(
1350                RefAny::new(0_usize),
1351                RefAny::new(0_usize),
1352                noop_worker as ThreadCallbackType,
1353            );
1354            info.add_thread(id, thread);
1355            let CallbackChange::AddThread { thread_id, .. } = env.take_one() else {
1356                panic!("expected AddThread");
1357            };
1358            assert_eq!(thread_id, id);
1359
1360            info.remove_thread(id);
1361            let CallbackChange::RemoveThread { thread_id } = env.take_one() else {
1362                panic!("expected RemoveThread");
1363            };
1364            assert_eq!(thread_id, id);
1365        });
1366    }
1367
1368    #[test]
1369    fn nullary_mutators_push_exactly_one_change_each_in_order() {
1370        with_env(|env| {
1371            let mut info = env.timer_info();
1372            info.stop_propagation();
1373            info.prevent_default();
1374            info.close_window();
1375            info.hide_tooltip();
1376            info.reload_system_fonts();
1377            info.update_all_image_callbacks();
1378            info.reset_cursor_blink();
1379            info.set_cursor_visibility_toggle();
1380
1381            let changes = env.take_changes();
1382            assert_eq!(changes.len(), 8, "one change per call, no drops: {changes:?}");
1383            assert!(matches!(changes[0], CallbackChange::StopPropagation));
1384            assert!(matches!(changes[1], CallbackChange::PreventDefault));
1385            assert!(matches!(changes[2], CallbackChange::CloseWindow));
1386            assert!(matches!(changes[3], CallbackChange::HideTooltip));
1387            assert!(matches!(changes[4], CallbackChange::ReloadSystemFonts));
1388            assert!(matches!(changes[5], CallbackChange::UpdateAllImageCallbacks));
1389            assert!(matches!(changes[6], CallbackChange::ResetCursorBlink));
1390            assert!(matches!(changes[7], CallbackChange::ToggleCursorVisibility));
1391        });
1392    }
1393
1394    #[test]
1395    fn set_cursor_visibility_records_both_polarities() {
1396        with_env(|env| {
1397            let mut info = env.timer_info();
1398            info.set_cursor_visibility(true);
1399            info.set_cursor_visibility(false);
1400
1401            let changes = env.take_changes();
1402            assert_eq!(changes.len(), 2);
1403            let visibilities: Vec<bool> = changes
1404                .iter()
1405                .map(|c| match c {
1406                    CallbackChange::SetCursorVisibility { visible } => *visible,
1407                    other => panic!("expected SetCursorVisibility, got {other:?}"),
1408                })
1409                .collect();
1410            assert_eq!(visibilities, vec![true, false]);
1411        });
1412    }
1413
1414    #[test]
1415    fn create_window_and_modify_window_state_push_changes() {
1416        with_env(|env| {
1417            let mut info = env.timer_info();
1418            info.create_window(WindowCreateOptions::default());
1419            assert!(matches!(env.take_one(), CallbackChange::CreateNewWindow { .. }));
1420
1421            info.modify_window_state(FullWindowState::default());
1422            assert!(matches!(env.take_one(), CallbackChange::ModifyWindowState { .. }));
1423        });
1424    }
1425
1426    #[test]
1427    fn image_cache_mutators_accept_degenerate_and_unicode_ids() {
1428        with_env(|env| {
1429            let mut info = env.timer_info();
1430
1431            // A 0x0 null image with an empty tag is degenerate but legal.
1432            let img = ImageRef::null_image(0, 0, RawImageFormat::RGBA8, Vec::new());
1433            let id: AzString = String::new().into();
1434            info.add_image_to_cache(id.clone(), img);
1435            let CallbackChange::AddImageToCache { id: got, .. } = env.take_one() else {
1436                panic!("expected AddImageToCache");
1437            };
1438            assert_eq!(got, id, "an empty id is passed through, not rejected");
1439
1440            // Embedded NUL, an RTL override and astral-plane chars must survive
1441            // the AzString round-trip byte-for-byte.
1442            let nasty: AzString = String::from("🚀\u{0}\u{202E}id\u{1F600}").into();
1443            info.remove_image_from_cache(nasty.clone());
1444            let CallbackChange::RemoveImageFromCache { id: got } = env.take_one() else {
1445                panic!("expected RemoveImageFromCache");
1446            };
1447            assert_eq!(got.as_str(), nasty.as_str());
1448        });
1449    }
1450
1451    #[test]
1452    fn trigger_virtual_view_rerender_accepts_out_of_range_ids() {
1453        with_env(|env| {
1454            let mut info = env.timer_info();
1455            info.trigger_virtual_view_rerender(DomId { inner: usize::MAX }, NodeId::new(usize::MAX));
1456            let CallbackChange::UpdateVirtualView { dom_id, node_id } = env.take_one() else {
1457                panic!("expected UpdateVirtualView");
1458            };
1459            // Recorded verbatim — validation happens when the change is applied,
1460            // not here, and neither id may overflow on the way in.
1461            assert_eq!(dom_id.inner, usize::MAX);
1462            assert_eq!(node_id, NodeId::new(usize::MAX));
1463        });
1464    }
1465
1466    #[test]
1467    fn open_menu_has_no_position_and_open_menu_at_carries_one() {
1468        with_env(|env| {
1469            let mut info = env.timer_info();
1470
1471            info.open_menu(empty_menu());
1472            let CallbackChange::OpenMenu { position, .. } = env.take_one() else {
1473                panic!("expected OpenMenu");
1474            };
1475            assert!(position.is_none(), "open_menu must defer to menu.position");
1476
1477            info.open_menu_at(empty_menu(), LogicalPosition::new(-1.5, 2.5));
1478            let CallbackChange::OpenMenu { position, .. } = env.take_one() else {
1479                panic!("expected OpenMenu");
1480            };
1481            let p = position.expect("open_menu_at must pin a position");
1482            assert_eq!((p.x, p.y), (-1.5, 2.5), "negative coordinates are legal");
1483        });
1484    }
1485
1486    #[test]
1487    fn open_menu_at_passes_non_finite_coordinates_through_unchanged() {
1488        with_env(|env| {
1489            let mut info = env.timer_info();
1490            info.open_menu_at(
1491                empty_menu(),
1492                LogicalPosition::new(f32::NAN, f32::INFINITY),
1493            );
1494            let CallbackChange::OpenMenu { position, .. } = env.take_one() else {
1495                panic!("expected OpenMenu");
1496            };
1497            let p = position.expect("position must be recorded");
1498            // No clamping/sanitising at this layer — but it must not panic either.
1499            assert!(p.x.is_nan());
1500            assert!(p.y.is_infinite() && p.y.is_sign_positive());
1501
1502            info.open_menu_at(empty_menu(), LogicalPosition::new(f32::MAX, f32::MIN));
1503            let CallbackChange::OpenMenu { position, .. } = env.take_one() else {
1504                panic!("expected OpenMenu");
1505            };
1506            let p = position.expect("position must be recorded");
1507            assert_eq!((p.x, p.y), (f32::MAX, f32::MIN));
1508        });
1509    }
1510
1511    #[test]
1512    fn open_menu_for_hit_node_is_false_and_silent_without_a_hit_node() {
1513        with_env(|env| {
1514            let mut info = env.timer_info();
1515            // Hit node is NONE and the window has no layout results: the menu has
1516            // nothing to anchor to.
1517            assert!(!info.open_menu_for_hit_node(empty_menu()));
1518            assert!(
1519                env.take_changes().is_empty(),
1520                "a failed anchor must not queue a half-open menu"
1521            );
1522        });
1523    }
1524
1525    #[test]
1526    fn show_tooltip_falls_back_to_the_origin_without_a_cursor() {
1527        with_env(|env| {
1528            let mut info = env.timer_info();
1529            info.show_tooltip(String::from("hi").into());
1530            let CallbackChange::ShowTooltip { text, position } = env.take_one() else {
1531                panic!("expected ShowTooltip");
1532            };
1533            assert_eq!(text.as_str(), "hi");
1534            assert_eq!((position.x, position.y), (0.0, 0.0), "no cursor -> origin");
1535        });
1536    }
1537
1538    #[test]
1539    fn show_tooltip_uses_the_viewport_cursor_when_there_is_one() {
1540        with_env(|env| {
1541            let cursor = LogicalPosition::new(3.0, 4.0);
1542            let mut info = TimerCallbackInfo::create(
1543                env.info_with(OptionLogicalPosition::None, OptionLogicalPosition::Some(cursor)),
1544                OptionDomNodeId::None,
1545                tick(0),
1546                0,
1547                false,
1548            );
1549            info.show_tooltip(String::from("t").into());
1550            let CallbackChange::ShowTooltip { position, .. } = env.take_one() else {
1551                panic!("expected ShowTooltip");
1552            };
1553            assert_eq!((position.x, position.y), (3.0, 4.0));
1554        });
1555    }
1556
1557    #[test]
1558    fn show_tooltip_at_records_empty_text_and_non_finite_positions() {
1559        with_env(|env| {
1560            let mut info = env.timer_info();
1561            info.show_tooltip_at(String::new().into(), LogicalPosition::new(f32::NAN, -0.0));
1562            let CallbackChange::ShowTooltip { text, position } = env.take_one() else {
1563                panic!("expected ShowTooltip");
1564            };
1565            assert_eq!(text.as_str(), "", "empty tooltip text is not rejected");
1566            assert!(position.x.is_nan());
1567            assert!(position.y.is_sign_negative());
1568        });
1569    }
1570
1571    // ==================================================================
1572    // Scroll delegation — numeric edges
1573    // ==================================================================
1574
1575    #[test]
1576    fn scroll_to_and_unclamped_differ_only_in_the_clamp_flag() {
1577        with_env(|env| {
1578            let mut info = env.timer_info();
1579            let node = NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(0)));
1580            let pos = LogicalPosition::new(10.0, 20.0);
1581
1582            info.scroll_to(DomId::ROOT_ID, node, pos);
1583            info.scroll_to_unclamped(DomId::ROOT_ID, node, pos);
1584
1585            let changes = env.take_changes();
1586            assert_eq!(changes.len(), 2);
1587            let flags: Vec<bool> = changes
1588                .iter()
1589                .map(|c| match c {
1590                    CallbackChange::ScrollTo {
1591                        dom_id,
1592                        node_id,
1593                        position,
1594                        unclamped,
1595                    } => {
1596                        assert_eq!(*dom_id, DomId::ROOT_ID);
1597                        assert_eq!(*node_id, node);
1598                        assert_eq!((position.x, position.y), (10.0, 20.0));
1599                        *unclamped
1600                    }
1601                    other => panic!("expected ScrollTo, got {other:?}"),
1602                })
1603                .collect();
1604            assert_eq!(flags, vec![false, true], "only the overscroll flag differs");
1605        });
1606    }
1607
1608    #[test]
1609    fn scroll_to_records_zero_negative_and_non_finite_positions() {
1610        with_env(|env| {
1611            let mut info = env.timer_info();
1612            let node = NodeHierarchyItemId::NONE;
1613
1614            for pos in [
1615                LogicalPosition::new(0.0, 0.0),
1616                LogicalPosition::new(-1.0, -f32::MAX),
1617                LogicalPosition::new(f32::MAX, f32::INFINITY),
1618            ] {
1619                info.scroll_to(DomId::ROOT_ID, node, pos);
1620                let CallbackChange::ScrollTo { position, .. } = env.take_one() else {
1621                    panic!("expected ScrollTo");
1622                };
1623                assert_eq!(position.x.to_bits(), pos.x.to_bits(), "x must be recorded bit-exact");
1624                assert_eq!(position.y.to_bits(), pos.y.to_bits(), "y must be recorded bit-exact");
1625            }
1626
1627            // NaN separately — it is never == to itself.
1628            info.scroll_to_unclamped(
1629                DomId { inner: usize::MAX },
1630                node,
1631                LogicalPosition::new(f32::NAN, f32::NAN),
1632            );
1633            let CallbackChange::ScrollTo {
1634                position, unclamped, ..
1635            } = env.take_one()
1636            else {
1637                panic!("expected ScrollTo");
1638            };
1639            assert!(position.x.is_nan() && position.y.is_nan(), "NaN is passed through, not zeroed");
1640            assert!(unclamped);
1641        });
1642    }
1643
1644    #[test]
1645    fn scroll_queries_are_none_on_an_empty_window() {
1646        with_env(|env| {
1647            let info = env.timer_info();
1648            assert!(info.get_scroll_node_info(DomId::ROOT_ID, NodeId::new(0)).is_none());
1649            assert!(
1650                info.get_scroll_node_info(DomId { inner: usize::MAX }, NodeId::new(usize::MAX))
1651                    .is_none(),
1652                "an out-of-range dom/node must return None, not panic"
1653            );
1654            assert!(info.find_scroll_parent(DomId::ROOT_ID, NodeId::new(0)).is_none());
1655            assert!(
1656                info.find_scroll_parent(DomId { inner: usize::MAX }, NodeId::new(usize::MAX))
1657                    .is_none()
1658            );
1659        });
1660    }
1661
1662    #[test]
1663    fn scroll_input_queue_starts_empty_and_draining_is_idempotent() {
1664        with_env(|env| {
1665            let info = env.timer_info();
1666            let queue = info.get_scroll_input_queue();
1667            assert!(queue.take_all().is_empty());
1668            assert!(queue.take_all().is_empty(), "draining twice must stay empty");
1669        });
1670    }
1671
1672    // ==================================================================
1673    // Predicates / state getters
1674    // ==================================================================
1675
1676    #[test]
1677    fn the_three_drag_predicates_are_aliases_of_left_down() {
1678        for left_down in [false, true] {
1679            with_env_cfg(left_down, OptionRefAny::None, |env| {
1680                let info = env.timer_info();
1681                assert_eq!(info.get_current_mouse_state().left_down, left_down);
1682                assert_eq!(info.is_dragging(), left_down);
1683                assert_eq!(info.is_drag_active(), left_down);
1684                assert_eq!(info.is_node_drag_active(), left_down);
1685            });
1686        }
1687    }
1688
1689    #[test]
1690    fn pen_predicates_are_false_without_a_pen() {
1691        with_env(|env| {
1692            let info = env.timer_info();
1693            assert!(!info.is_pen_in_contact());
1694            assert!(!info.is_pen_eraser());
1695            assert!(!info.is_pen_barrel_button_pressed());
1696        });
1697    }
1698
1699    #[test]
1700    fn drag_and_gesture_predicates_are_false_on_a_fresh_window() {
1701        with_env(|env| {
1702            let info = env.timer_info();
1703            assert!(!info.is_file_drag_active());
1704            assert!(!info.has_sufficient_history_for_gestures());
1705        });
1706    }
1707
1708    #[test]
1709    fn is_dom_focused_is_false_for_every_dom_when_nothing_is_focused() {
1710        with_env(|env| {
1711            let info = env.timer_info();
1712            assert!(!info.is_dom_focused(DomId::ROOT_ID));
1713            assert!(!info.is_dom_focused(DomId { inner: usize::MAX }));
1714        });
1715    }
1716
1717    #[test]
1718    fn window_state_getters_mirror_the_current_window_state() {
1719        with_env(|env| {
1720            let info = env.timer_info();
1721            let default_state = FullWindowState::default();
1722            assert_eq!(info.get_current_window_flags(), default_state.flags);
1723            assert_eq!(info.get_current_keyboard_state(), default_state.keyboard_state);
1724            assert_eq!(info.get_current_mouse_state(), default_state.mouse_state);
1725        });
1726    }
1727
1728    #[test]
1729    fn cursor_getters_round_trip_including_nan() {
1730        with_env(|env| {
1731            let info = env.timer_info();
1732            assert!(info.get_cursor_position().is_none());
1733            assert_eq!(info.get_cursor_relative_to_viewport(), OptionLogicalPosition::None);
1734            assert!(info.get_cursor_relative_to_node().is_none());
1735
1736            let viewport = LogicalPosition::new(f32::NAN, 7.5);
1737            let relative = LogicalPosition::new(-3.0, f32::INFINITY);
1738            let info = TimerCallbackInfo::create(
1739                env.info_with(
1740                    OptionLogicalPosition::Some(relative),
1741                    OptionLogicalPosition::Some(viewport),
1742                ),
1743                OptionDomNodeId::None,
1744                tick(0),
1745                0,
1746                false,
1747            );
1748
1749            let got = info.get_cursor_position().expect("cursor must be Some");
1750            assert!(got.x.is_nan() && got.y == 7.5);
1751
1752            let node_rel = info
1753                .get_cursor_relative_to_node()
1754                .into_option()
1755                .expect("relative cursor must be Some");
1756            assert_eq!(node_rel.x, -3.0);
1757            assert!(node_rel.y.is_infinite());
1758        });
1759    }
1760
1761    // ==================================================================
1762    // Timer::invoke — the scheduling state machine
1763    // ==================================================================
1764
1765    #[test]
1766    fn invoke_does_not_run_the_callback_before_the_delay_elapses() {
1767        let _g = clock_guard();
1768        with_env(|env| {
1769            let mut t =
1770                timer_at(0, recording_cb as TimerCallbackType).with_delay(tick_dur(100));
1771            let info = env.info();
1772
1773            set_now(99);
1774            let r = t.invoke(&info, &fake_clock_cb());
1775            assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 0, "callback must not fire early");
1776            assert_eq!(r.should_update, Update::DoNothing);
1777            assert_eq!(r.should_terminate, TerminateTimer::Continue);
1778            // A skipped tick must leave the timer's progress untouched.
1779            assert_eq!(t.run_count, 0);
1780            assert_eq!(t.last_run, OptionInstant::None);
1781
1782            // The boundary is inclusive: elapsed == delay runs.
1783            set_now(100);
1784            let r = t.invoke(&info, &fake_clock_cb());
1785            assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 1);
1786            assert_eq!(r.should_terminate, TerminateTimer::Continue);
1787            assert_eq!(t.run_count, 1);
1788            assert_eq!(t.last_run, OptionInstant::Some(tick(100)));
1789        });
1790    }
1791
1792    #[test]
1793    fn invoke_gates_subsequent_runs_on_the_interval() {
1794        let _g = clock_guard();
1795        with_env(|env| {
1796            let mut t =
1797                timer_at(0, recording_cb as TimerCallbackType).with_interval(tick_dur(10));
1798            let info = env.info();
1799
1800            // No delay -> the first invoke runs immediately.
1801            let r = t.invoke(&info, &fake_clock_cb());
1802            assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 1);
1803            assert_eq!(r.should_terminate, TerminateTimer::Continue);
1804            assert_eq!(t.run_count, 1);
1805
1806            set_now(9);
1807            let r = t.invoke(&info, &fake_clock_cb());
1808            assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 1, "1 tick short of the interval");
1809            assert_eq!(r.should_update, Update::DoNothing);
1810            assert_eq!(t.run_count, 1, "a skipped tick must not count as a run");
1811
1812            set_now(10);
1813            let _ = t.invoke(&info, &fake_clock_cb());
1814            assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 2);
1815            assert_eq!(t.run_count, 2);
1816            assert_eq!(t.last_run, OptionInstant::Some(tick(10)));
1817        });
1818    }
1819
1820    #[test]
1821    fn invoke_hands_the_callback_the_run_count_and_frame_start() {
1822        let _g = clock_guard();
1823        with_env(|env| {
1824            let mut t = timer_at(0, recording_cb as TimerCallbackType);
1825            let info = env.info();
1826
1827            set_now(5);
1828            let _ = t.invoke(&info, &fake_clock_cb());
1829            assert_eq!(CB_SEEN_CALL_COUNT.load(Ordering::SeqCst), 0, "first run is call 0");
1830            assert_eq!(CB_SEEN_FRAME_START.load(Ordering::SeqCst), 5, "frame_start == now");
1831            assert!(!CB_SEEN_ABOUT_TO_FINISH.load(Ordering::SeqCst));
1832
1833            set_now(6);
1834            let _ = t.invoke(&info, &fake_clock_cb());
1835            assert_eq!(CB_SEEN_CALL_COUNT.load(Ordering::SeqCst), 1, "run_count increments by 1");
1836            assert_eq!(CB_SEEN_FRAME_START.load(Ordering::SeqCst), 6);
1837        });
1838    }
1839
1840    #[test]
1841    fn invoke_forces_terminate_once_the_timeout_expires() {
1842        let _g = clock_guard();
1843        with_env(|env| {
1844            let mut t = timer_at(0, recording_cb as TimerCallbackType).with_timeout(tick_dur(5));
1845            let info = env.info();
1846            // The callback insists on Continue...
1847            CB_RETURN_TERMINATE.store(false, Ordering::SeqCst);
1848
1849            set_now(5);
1850            let r = t.invoke(&info, &fake_clock_cb());
1851            assert_eq!(r.should_terminate, TerminateTimer::Continue, "elapsed == timeout: alive");
1852            assert!(!CB_SEEN_ABOUT_TO_FINISH.load(Ordering::SeqCst));
1853
1854            set_now(6);
1855            let r = t.invoke(&info, &fake_clock_cb());
1856            // ...but the timeout overrides it, and the callback is told so.
1857            assert!(CB_SEEN_ABOUT_TO_FINISH.load(Ordering::SeqCst), "last-call flag must be set");
1858            assert_eq!(r.should_terminate, TerminateTimer::Terminate);
1859            assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 2, "the final run still happens");
1860        });
1861    }
1862
1863    #[test]
1864    fn invoke_honours_a_callback_requested_terminate() {
1865        let _g = clock_guard();
1866        with_env(|env| {
1867            let mut t = timer_at(0, recording_cb as TimerCallbackType);
1868            let info = env.info();
1869            CB_RETURN_TERMINATE.store(true, Ordering::SeqCst);
1870
1871            let r = t.invoke(&info, &fake_clock_cb());
1872            assert_eq!(r.should_terminate, TerminateTimer::Terminate);
1873            // Termination is the caller's job; invoke still records the run.
1874            assert_eq!(t.run_count, 1);
1875            assert_eq!(t.last_run, OptionInstant::Some(tick(0)));
1876        });
1877    }
1878
1879    #[test]
1880    fn invoke_skips_deterministically_when_the_clock_runs_backwards() {
1881        let _g = clock_guard();
1882        with_env(|env| {
1883            let mut t =
1884                timer_at(0, recording_cb as TimerCallbackType).with_interval(tick_dur(10));
1885            t.last_run = OptionInstant::Some(tick(1_000));
1886            let info = env.info();
1887
1888            // now(0) is *older* than last_run(1000): duration_since saturates to 0,
1889            // 0 < 10, so the tick is skipped — no panic, no spurious run.
1890            set_now(0);
1891            let r = t.invoke(&info, &fake_clock_cb());
1892            assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 0);
1893            assert_eq!(r.should_terminate, TerminateTimer::Continue);
1894            assert_eq!(t.run_count, 0);
1895            assert_eq!(t.last_run, OptionInstant::Some(tick(1_000)), "last_run is untouched");
1896        });
1897    }
1898
1899    #[test]
1900    fn invoke_with_a_mismatched_interval_kind_runs_every_tick() {
1901        let _g = clock_guard();
1902        with_env(|env| {
1903            // A wall-clock interval on a tick-driven timer: the Tick-vs-System
1904            // comparison saturates to false, so the "not yet" branch is never
1905            // taken and the interval degrades to "run on every invoke".
1906            let mut t =
1907                timer_at(0, recording_cb as TimerCallbackType).with_interval(sys_dur_millis(60_000));
1908            let info = env.info();
1909
1910            let _ = t.invoke(&info, &fake_clock_cb());
1911            set_now(1);
1912            let _ = t.invoke(&info, &fake_clock_cb());
1913            assert_eq!(
1914                CB_INVOCATIONS.load(Ordering::SeqCst),
1915                2,
1916                "a 60s interval does not throttle a tick clock"
1917            );
1918            assert_eq!(t.run_count, 2);
1919        });
1920    }
1921
1922    #[test]
1923    fn invoke_at_the_end_of_time_does_not_panic() {
1924        let _g = clock_guard();
1925        with_env(|env| {
1926            let mut t = timer_at(u64::MAX, recording_cb as TimerCallbackType)
1927                .with_delay(tick_dur(u64::MAX))
1928                .with_interval(tick_dur(u64::MAX))
1929                .with_timeout(tick_dur(u64::MAX));
1930            let info = env.info();
1931
1932            set_now(u64::MAX);
1933            // elapsed = 0, delay = MAX -> 0 < MAX -> skipped, no overflow anywhere.
1934            let r = t.invoke(&info, &fake_clock_cb());
1935            assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 0);
1936            assert_eq!(r.should_terminate, TerminateTimer::Continue);
1937            assert_eq!(t.run_count, 0);
1938        });
1939    }
1940}