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

            
6
use core::ffi::c_void;
7

            
8
use 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

            
23
use azul_css::AzString;
24

            
25
use 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.
32
const DEFAULT_TIMER_TICK_MS: u64 = 10;
33

            
34
/// Callback type for timers
35
pub 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)]
42
pub 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

            
49
impl TimerCallback {
50
51
    pub fn create(cb: TimerCallbackType) -> Self {
51
51
        Self {
52
51
            cb,
53
51
            ctx: OptionRefAny::None,
54
51
        }
55
51
    }
56
}
57

            
58
impl core::fmt::Debug for TimerCallback {
59
1
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
60
1
        write!(f, "TimerCallback {{ cb: {:p} }}", self.cb as *const ())
61
1
    }
62
}
63

            
64
impl Clone for TimerCallback {
65
8
    fn clone(&self) -> Self {
66
8
        Self {
67
8
            cb: self.cb,
68
8
            ctx: self.ctx.clone(),
69
8
        }
70
8
    }
71
}
72

            
73
impl From<TimerCallbackType> for TimerCallback {
74
71
    fn from(cb: TimerCallbackType) -> Self {
75
71
        Self {
76
71
            cb,
77
71
            ctx: OptionRefAny::None,
78
71
        }
79
71
    }
80
}
81

            
82
impl PartialEq for TimerCallback {
83
6
    fn eq(&self, other: &Self) -> bool {
84
6
        std::ptr::eq(self.cb as *const (), other.cb as *const ())
85
6
    }
86
}
87

            
88
impl Eq for TimerCallback {}
89

            
90
impl PartialOrd for TimerCallback {
91
1
    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
92
1
        (self.cb as *const () as usize).partial_cmp(&(other.cb as *const () as usize))
93
1
    }
94
}
95

            
96
impl Ord for TimerCallback {
97
5
    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
98
5
        (self.cb as *const () as usize).cmp(&(other.cb as *const () as usize))
99
5
    }
100
}
101

            
102
impl core::hash::Hash for TimerCallback {
103
2
    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
104
2
        (self.cb as *const () as usize).hash(state);
105
2
    }
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)]
111
pub 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

            
123
impl Timer {
124
72
    pub fn create<C: Into<TimerCallback>>(
125
72
        refany: RefAny,
126
72
        callback: C,
127
72
        get_system_time_fn: GetSystemTimeCallback,
128
72
    ) -> Self {
129
72
        Self {
130
72
            refany,
131
72
            node_id: None.into(),
132
72
            created: (get_system_time_fn.cb)(),
133
72
            run_count: 0,
134
72
            last_run: OptionInstant::None,
135
72
            delay: OptionDuration::None,
136
72
            interval: OptionDuration::None,
137
72
            timeout: OptionDuration::None,
138
72
            callback: callback.into(),
139
72
        }
140
72
    }
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
22
    #[must_use] pub const fn tick_millis(&self) -> u64 {
151
22
        match self.interval.as_ref() {
152
18
            Some(d) => d.as_millis_u64(),
153
4
            None => DEFAULT_TIMER_TICK_MS,
154
        }
155
22
    }
156

            
157
50
    #[must_use] pub fn is_about_to_finish(&self, instant_now: &Instant) -> bool {
158
50
        match self.timeout {
159
15
            OptionDuration::Some(timeout) => {
160
15
                instant_now.duration_since(&self.created).greater_than(&timeout)
161
            }
162
35
            OptionDuration::None => false,
163
        }
164
50
    }
165

            
166
23
    #[must_use] pub fn instant_of_next_run(&self) -> Instant {
167
23
        let last_run = self.last_run.as_ref().map_or(&self.created, |s| s);
168

            
169
23
        last_run
170
23
            .clone()
171
23
            .add_optional_duration(self.delay.as_ref())
172
23
            .add_optional_duration(self.interval.as_ref())
173
23
    }
174

            
175
    #[inline]
176
12
    #[must_use] pub const fn with_delay(mut self, delay: Duration) -> Self {
177
12
        self.delay = OptionDuration::Some(delay);
178
12
        self
179
12
    }
180

            
181
    #[inline]
182
41
    #[must_use] pub const fn with_interval(mut self, interval: Duration) -> Self {
183
41
        self.interval = OptionDuration::Some(interval);
184
41
        self
185
41
    }
186

            
187
    #[inline]
188
14
    #[must_use] pub const fn with_timeout(mut self, timeout: Duration) -> Self {
189
14
        self.timeout = OptionDuration::Some(timeout);
190
14
        self
191
14
    }
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
61
    pub fn invoke(
199
61
        &mut self,
200
61
        callback_info: &CallbackInfo,
201
61
        get_system_time_fn: &GetSystemTimeCallback,
202
61
    ) -> TimerCallbackReturn {
203
61
        let now = (get_system_time_fn.cb)();
204

            
205
        // Check if timer should run based on last_run, delay, and interval
206
61
        match self.last_run.as_ref() {
207
43
            Some(last_run) => {
208
                // Timer has run before - check interval
209
43
                if let OptionDuration::Some(interval) = self.interval {
210
41
                    if now.duration_since(last_run).smaller_than(&interval) {
211
22
                        return TimerCallbackReturn {
212
22
                            should_update: Update::DoNothing,
213
22
                            should_terminate: TerminateTimer::Continue,
214
22
                        };
215
19
                    }
216
2
                }
217
            }
218
            None => {
219
                // Timer has never run - check delay (first run)
220
18
                if let OptionDuration::Some(delay) = self.delay {
221
7
                    if now.duration_since(&self.created).smaller_than(&delay) {
222
5
                        return TimerCallbackReturn {
223
5
                            should_update: Update::DoNothing,
224
5
                            should_terminate: TerminateTimer::Continue,
225
5
                        };
226
2
                    }
227
11
                }
228
            }
229
        }
230

            
231
34
        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
34
        let mut timer_callback_info = TimerCallbackInfo {
236
34
            callback_info: *callback_info,
237
34
            node_id: self.node_id,
238
34
            frame_start: now.clone(),
239
34
            call_count: self.run_count,
240
34
            is_about_to_finish,
241
34
            _abi_ref: core::ptr::null(),
242
34
            _abi_mut: core::ptr::null_mut(),
243
34
        };
244

            
245
        // Time THIS app callback under its own resolved symbol name
246
        // (`cb:my_timer_fn`): the per-phase histogram then compares one
247
        // callback's cost across versions, and a slow one is named in the
248
        // slow-span WARN. Resolution is cached; recording-off is one atomic.
249
34
        let _cb_span = crate::probe::Probe::span_for_fn(self.callback.cb as usize);
250
34
        let mut result = (self.callback.cb)(self.refany.clone(), timer_callback_info);
251

            
252
34
        if is_about_to_finish {
253
1
            result.should_terminate = TerminateTimer::Terminate;
254
33
        }
255

            
256
34
        self.run_count += 1;
257
34
        self.last_run = OptionInstant::Some(now);
258

            
259
34
        result
260
61
    }
261
}
262

            
263
impl Default for Timer {
264
21
    fn default() -> Self {
265
        extern "C" fn default_callback(_: RefAny, _: TimerCallbackInfo) -> TimerCallbackReturn {
266
            TimerCallbackReturn::terminate_unchanged()
267
        }
268

            
269
21
        const extern "C" fn default_time() -> Instant {
270
21
            Instant::Tick(azul_core::task::SystemTick { tick_counter: 0 })
271
21
        }
272

            
273
21
        let cb: TimerCallbackType = default_callback;
274
21
        Self::create(
275
21
            RefAny::new(()),
276
21
            cb,
277
21
            GetSystemTimeCallback { cb: default_time },
278
        )
279
21
    }
280
}
281

            
282
/// Information passed to timer callbacks.
283
///
284
/// This wraps `CallbackInfo` and adds timer-specific fields like `call_count` and `frame_start`.
285
/// `CallbackInfo` methods are available via explicit delegation methods below.
286
#[derive(Debug, Clone)]
287
#[repr(C)]
288
#[allow(clippy::pub_underscore_fields)] // _abi_ref/_abi_mut: intentional FFI/api.json ABI-stability placeholder fields
289
pub struct TimerCallbackInfo {
290
    pub callback_info: CallbackInfo,
291
    pub node_id: OptionDomNodeId,
292
    pub frame_start: Instant,
293
    pub call_count: usize,
294
    pub is_about_to_finish: bool,
295
    pub _abi_ref: *const c_void,
296
    pub _abi_mut: *mut c_void,
297
}
298

            
299
impl TimerCallbackInfo {
300
585
    #[must_use] pub const fn create(
301
585
        callback_info: CallbackInfo,
302
585
        node_id: OptionDomNodeId,
303
585
        frame_start: Instant,
304
585
        call_count: usize,
305
585
        is_about_to_finish: bool,
306
585
    ) -> Self {
307
585
        Self {
308
585
            callback_info,
309
585
            node_id,
310
585
            frame_start,
311
585
            call_count,
312
585
            is_about_to_finish,
313
585
            _abi_ref: core::ptr::null(),
314
585
            _abi_mut: core::ptr::null_mut(),
315
585
        }
316
585
    }
317

            
318
2
    #[must_use] pub fn get_attached_node_size(&self) -> Option<LogicalSize> {
319
2
        let node_id = self.node_id.into_option()?;
320
1
        self.callback_info.get_node_size(node_id)
321
2
    }
322

            
323
2
    #[must_use] pub fn get_attached_node_position(&self) -> Option<LogicalPosition> {
324
2
        let node_id = self.node_id.into_option()?;
325
1
        self.callback_info.get_node_position(node_id)
326
2
    }
327

            
328
1
    #[must_use] pub const fn get_callback_info(&self) -> &CallbackInfo {
329
1
        &self.callback_info
330
1
    }
331

            
332
2
    pub const fn get_callback_info_mut(&mut self) -> &mut CallbackInfo {
333
2
        &mut self.callback_info
334
2
    }
335

            
336
    // ==================== Delegated CallbackInfo methods ====================
337
    // These methods delegate to the inner callback_info to provide the same API
338
    // as CallbackInfo without using Deref (which causes issues with FFI codegen)
339

            
340
    /// Get the callable for FFI language bindings (Python, etc.)
341
2
    #[must_use] pub fn get_ctx(&self) -> OptionRefAny {
342
2
        self.callback_info.get_ctx()
343
2
    }
344

            
345
    /// Add a timer to this window (applied after callback returns)
346
1
    pub fn add_timer(&mut self, timer_id: TimerId, timer: Timer) {
347
1
        self.callback_info.add_timer(timer_id, timer);
348
1
    }
349

            
350
    /// Remove a timer from this window (applied after callback returns)
351
1
    pub fn remove_timer(&mut self, timer_id: TimerId) {
352
1
        self.callback_info.remove_timer(timer_id);
353
1
    }
354

            
355
    /// Add a thread to this window (applied after callback returns)
356
1
    pub fn add_thread(&mut self, thread_id: ThreadId, thread: Thread) {
357
1
        self.callback_info.add_thread(thread_id, thread);
358
1
    }
359

            
360
    /// Remove a thread from this window (applied after callback returns)
361
1
    pub fn remove_thread(&mut self, thread_id: ThreadId) {
362
1
        self.callback_info.remove_thread(thread_id);
363
1
    }
364

            
365
    /// Stop event propagation (applied after callback returns)
366
1
    pub fn stop_propagation(&mut self) {
367
1
        self.callback_info.stop_propagation();
368
1
    }
369

            
370
    /// Create a new window (applied after callback returns)
371
1
    pub fn create_window(&mut self, options: WindowCreateOptions) {
372
1
        self.callback_info.create_window(options);
373
1
    }
374

            
375
    /// Close the current window (applied after callback returns)
376
1
    pub fn close_window(&mut self) {
377
1
        self.callback_info.close_window();
378
1
    }
379

            
380
    /// Modify the window state (applied after callback returns)
381
1
    pub fn modify_window_state(&mut self, state: FullWindowState) {
382
1
        self.callback_info.modify_window_state(state);
383
1
    }
384

            
385
    /// Add an image to the image cache (applied after callback returns)
386
1
    pub fn add_image_to_cache(&mut self, id: AzString, image: ImageRef) {
387
1
        self.callback_info.add_image_to_cache(id, image);
388
1
    }
389

            
390
    /// Remove an image from the image cache (applied after callback returns)
391
1
    pub fn remove_image_from_cache(&mut self, id: AzString) {
392
1
        self.callback_info.remove_image_from_cache(id);
393
1
    }
394

            
395
    /// Re-render ALL image callbacks across all DOMs (applied after callback returns)
396
    ///
397
    /// This is the most efficient way to update animated GL textures from a timer.
398
    /// Triggers only texture re-rendering - no DOM rebuild or display list resubmission.
399
1
    pub fn update_all_image_callbacks(&mut self) {
400
1
        self.callback_info.update_all_image_callbacks();
401
1
    }
402

            
403
    /// Trigger re-rendering of a `VirtualView` (applied after callback returns)
404
1
    pub fn trigger_virtual_view_rerender(&mut self, dom_id: DomId, node_id: NodeId) {
405
1
        self.callback_info.trigger_virtual_view_rerender(dom_id, node_id);
406
1
    }
407

            
408
    /// Reload system fonts (applied after callback returns)
409
1
    pub fn reload_system_fonts(&mut self) {
410
1
        self.callback_info.reload_system_fonts();
411
1
    }
412

            
413
    /// Prevent the default action
414
1
    pub fn prevent_default(&mut self) {
415
1
        self.callback_info.prevent_default();
416
1
    }
417

            
418
    /// Open a menu
419
1
    pub fn open_menu(&mut self, menu: Menu) {
420
1
        self.callback_info.open_menu(menu);
421
1
    }
422

            
423
    /// Open a menu at a specific position
424
3
    pub fn open_menu_at(&mut self, menu: Menu, position: LogicalPosition) {
425
3
        self.callback_info.open_menu_at(menu, position);
426
3
    }
427

            
428
    /// Show a tooltip at the current cursor position
429
2
    pub fn show_tooltip(&mut self, text: AzString) {
430
2
        self.callback_info.show_tooltip(text);
431
2
    }
432

            
433
    /// Show a tooltip at a specific position
434
1
    pub fn show_tooltip_at(&mut self, text: AzString, position: LogicalPosition) {
435
1
        self.callback_info.show_tooltip_at(text, position);
436
1
    }
437

            
438
    /// Hide the currently displayed tooltip
439
1
    pub fn hide_tooltip(&mut self) {
440
1
        self.callback_info.hide_tooltip();
441
1
    }
442

            
443
    /// Open a menu positioned relative to the currently hit node
444
1
    pub fn open_menu_for_hit_node(&mut self, menu: Menu) -> bool {
445
1
        self.callback_info.open_menu_for_hit_node(menu)
446
1
    }
447

            
448
    /// Get current window flags
449
1
    #[must_use] pub const fn get_current_window_flags(&self) -> WindowFlags {
450
1
        self.callback_info.get_current_window_flags()
451
1
    }
452

            
453
    /// Get current keyboard state
454
1
    #[must_use] pub fn get_current_keyboard_state(&self) -> KeyboardState {
455
1
        self.callback_info.get_current_keyboard_state()
456
1
    }
457

            
458
    /// Get current mouse state
459
3
    #[must_use] pub const fn get_current_mouse_state(&self) -> MouseState {
460
3
        self.callback_info.get_current_mouse_state()
461
3
    }
462

            
463
    /// Get the cursor position relative to the hit node
464
2
    #[must_use] pub const fn get_cursor_relative_to_node(&self) -> azul_core::geom::OptionCursorNodePosition {
465
2
        self.callback_info.get_cursor_relative_to_node()
466
2
    }
467

            
468
    /// Get the cursor position relative to the viewport
469
1
    #[must_use] pub const fn get_cursor_relative_to_viewport(&self) -> OptionLogicalPosition {
470
1
        self.callback_info.get_cursor_relative_to_viewport()
471
1
    }
472

            
473
    /// Get the current cursor position
474
2
    #[must_use] pub fn get_cursor_position(&self) -> Option<LogicalPosition> {
475
2
        self.callback_info.get_cursor_position()
476
2
    }
477

            
478
    /// Get the current time (when the timer callback started)
479
8
    #[must_use] pub fn get_current_time(&self) -> Instant {
480
8
        self.frame_start.clone()
481
8
    }
482

            
483
    /// Check if any node in a specific DOM is focused
484
2
    #[must_use] pub fn is_dom_focused(&self, dom_id: DomId) -> bool {
485
2
        self.callback_info.is_dom_focused(dom_id)
486
2
    }
487

            
488
    /// Check if pen is in contact
489
1
    #[must_use] pub fn is_pen_in_contact(&self) -> bool {
490
1
        self.callback_info.is_pen_in_contact()
491
1
    }
492

            
493
    /// Check if pen eraser is active
494
1
    #[must_use] pub fn is_pen_eraser(&self) -> bool {
495
1
        self.callback_info.is_pen_eraser()
496
1
    }
497

            
498
    /// Check if pen barrel button is pressed
499
1
    #[must_use] pub fn is_pen_barrel_button_pressed(&self) -> bool {
500
1
        self.callback_info.is_pen_barrel_button_pressed()
501
1
    }
502

            
503
    /// Check if dragging is active
504
2
    #[must_use] pub const fn is_dragging(&self) -> bool {
505
2
        self.callback_info.get_current_mouse_state().left_down
506
2
    }
507

            
508
    /// Check if drag is active
509
2
    #[must_use] pub const fn is_drag_active(&self) -> bool {
510
2
        self.callback_info.get_current_mouse_state().left_down
511
2
    }
512

            
513
    /// Check if node drag is active
514
2
    #[must_use] pub const fn is_node_drag_active(&self) -> bool {
515
2
        self.callback_info.get_current_mouse_state().left_down
516
2
    }
517

            
518
    /// Check if file drag is active
519
1
    #[must_use] pub fn is_file_drag_active(&self) -> bool {
520
1
        self.callback_info.is_file_drag_active()
521
1
    }
522

            
523
    /// Check if there's sufficient history for gestures
524
1
    #[must_use] pub fn has_sufficient_history_for_gestures(&self) -> bool {
525
1
        self.callback_info.has_sufficient_history_for_gestures()
526
1
    }
527

            
528
    // ==================== Scroll Management (timer architecture) ====================
529

            
530
    /// Get a read-only snapshot of a scroll node's bounds and position.
531
    ///
532
    /// Timer callbacks use this to read current scroll state for physics calculation.
533
364
    #[must_use] pub fn get_scroll_node_info(
534
364
        &self,
535
364
        dom_id: DomId,
536
364
        node_id: NodeId,
537
364
    ) -> Option<crate::managers::scroll_state::ScrollNodeInfo> {
538
364
        self.callback_info.get_scroll_node_info(dom_id, node_id)
539
364
    }
540

            
541
    /// Find the closest scrollable ancestor of a node.
542
    ///
543
    /// Used by auto-scroll timer to find which container to scroll when
544
    /// the user drags beyond the container edge.
545
3
    #[must_use] pub fn find_scroll_parent(
546
3
        &self,
547
3
        dom_id: DomId,
548
3
        node_id: NodeId,
549
3
    ) -> Option<NodeId> {
550
3
        self.callback_info.find_scroll_parent(dom_id, node_id)
551
3
    }
552

            
553
    /// Get the scroll input queue for consuming pending scroll inputs.
554
    ///
555
    /// The physics timer calls `take_all()` each tick to drain inputs
556
    /// recorded by platform event handlers.
557
    #[cfg(feature = "std")]
558
1
    #[must_use] pub fn get_scroll_input_queue(
559
1
        &self,
560
1
    ) -> crate::managers::scroll_state::ScrollInputQueue {
561
1
        self.callback_info.get_scroll_input_queue()
562
1
    }
563

            
564
    /// Scroll a node to a specific position (via transactional `CallbackChange`).
565
    ///
566
    /// This is the primary way for timer callbacks to update scroll positions.
567
    /// The change is applied after the callback returns.
568
108
    pub fn scroll_to(
569
108
        &mut self,
570
108
        dom_id: DomId,
571
108
        node_id: azul_core::styled_dom::NodeHierarchyItemId,
572
108
        position: LogicalPosition,
573
108
    ) {
574
108
        self.callback_info.scroll_to(dom_id, node_id, position);
575
108
    }
576

            
577
    /// Scroll to position without clamping (for rubber-banding/overscroll).
578
127
    pub fn scroll_to_unclamped(
579
127
        &mut self,
580
127
        dom_id: DomId,
581
127
        node_id: azul_core::styled_dom::NodeHierarchyItemId,
582
127
        position: LogicalPosition,
583
127
    ) {
584
127
        self.callback_info.scroll_to_unclamped(dom_id, node_id, position);
585
127
    }
586

            
587
    // Cursor blink timer methods
588
    
589
    /// Set cursor visibility state (for cursor blink timer)
590
2
    pub fn set_cursor_visibility(&mut self, visible: bool) {
591
2
        self.callback_info.set_cursor_visibility(visible);
592
2
    }
593
    
594
    /// Toggle cursor visibility (for cursor blink timer).
595
6
    pub fn set_cursor_visibility_toggle(&mut self) {
596
        use crate::callbacks::CallbackChange;
597
6
        self.callback_info.push_change(CallbackChange::ToggleCursorVisibility);
598
6
    }
599
    
600
    /// Reset cursor blink state on user input
601
1
    pub fn reset_cursor_blink(&mut self) {
602
1
        self.callback_info.reset_cursor_blink();
603
1
    }
604
}
605

            
606
/// Optional Timer type for API compatibility
607
#[derive(Debug, Clone)]
608
#[repr(C, u8)]
609
// FFI Option enum; boxing the Some variant would break the #[repr(C, u8)] C ABI / api.json.
610
#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
611
#[allow(clippy::large_enum_variant)]
612
pub enum OptionTimer {
613
    None,
614
    Some(Timer),
615
}
616

            
617
impl From<Option<Timer>> for OptionTimer {
618
2
    fn from(o: Option<Timer>) -> Self {
619
2
        o.map_or_else(|| Self::None, Self::Some)
620
2
    }
621
}
622

            
623
impl OptionTimer {
624
3
    #[must_use] pub fn into_option(self) -> Option<Timer> {
625
3
        match self {
626
2
            Self::None => None,
627
1
            Self::Some(t) => Some(t),
628
        }
629
3
    }
630
}
631

            
632
#[cfg(all(test, feature = "std"))]
633
#[allow(
634
    clippy::float_cmp,
635
    clippy::too_many_lines,
636
    clippy::unreadable_literal,
637
    clippy::cognitive_complexity
638
)]
639
mod autotest_generated {
640
    use std::{
641
        collections::BTreeMap,
642
        sync::{
643
            atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering},
644
            Arc, Mutex, MutexGuard, PoisonError,
645
        },
646
    };
647

            
648
    use azul_core::{
649
        dom::DomNodeId,
650
        gl::OptionGlContextPtr,
651
        hit_test::ScrollPosition,
652
        menu::MenuItemVec,
653
        resources::{RawImageFormat, RendererResources},
654
        styled_dom::NodeHierarchyItemId,
655
        task::{SystemTick, SystemTickDiff, SystemTimeDiff, ThreadReceiver},
656
        window::{MonitorVec, RawWindowHandle},
657
    };
658
    use azul_css::system::SystemStyle;
659
    use rust_fontconfig::FcFontCache;
660

            
661
    use super::*;
662
    #[cfg(feature = "icu")]
663
    use crate::icu::IcuLocalizerHandle;
664
    use crate::{
665
        callbacks::{CallbackChange, CallbackInfoRefData, ExternalSystemCallbacks},
666
        thread::{ThreadCallbackType, ThreadSender},
667
        window::LayoutWindow,
668
    };
669

            
670
    // ------------------------------------------------------------------
671
    // Time helpers
672
    // ------------------------------------------------------------------
673

            
674
    /// A tick-based `Instant` — the only kind constructible without a real clock,
675
    /// and the only kind whose arithmetic is fully deterministic.
676
    fn tick(t: u64) -> Instant {
677
        Instant::Tick(SystemTick::new(t))
678
    }
679

            
680
    /// A tick-based `Duration`.
681
    const fn tick_dur(d: u64) -> Duration {
682
        Duration::Tick(SystemTickDiff { tick_diff: d })
683
    }
684

            
685
    /// A wall-clock-based `Duration` (deliberately the *wrong kind* to pair with
686
    /// a `Tick` instant — several tests below pin the saturating behaviour of
687
    /// exactly that mismatch).
688
    const fn sys_dur_millis(ms: u64) -> Duration {
689
        Duration::System(SystemTimeDiff::from_millis(ms))
690
    }
691

            
692
    /// Extract the tick counter, asserting the instant really is tick-based.
693
    fn tick_of(i: &Instant) -> u64 {
694
        match i {
695
            Instant::Tick(t) => t.tick_counter,
696
            Instant::System(_) => panic!("expected a Tick instant, got a System one"),
697
        }
698
    }
699

            
700
    // ------------------------------------------------------------------
701
    // Fake clock + recording callback
702
    //
703
    // `GetSystemTimeCallbackType` is a bare `extern "C" fn()` with no context
704
    // pointer, so the fake clock has to live in statics. Every test that touches
705
    // them takes `clock_guard()` first, which serialises them against the rest of
706
    // the (parallel) test binary.
707
    // ------------------------------------------------------------------
708

            
709
    static CLOCK_LOCK: Mutex<()> = Mutex::new(());
710
    static FAKE_TICK: AtomicU64 = AtomicU64::new(0);
711
    static CB_INVOCATIONS: AtomicUsize = AtomicUsize::new(0);
712
    static CB_SEEN_CALL_COUNT: AtomicUsize = AtomicUsize::new(0);
713
    static CB_SEEN_FRAME_START: AtomicU64 = AtomicU64::new(0);
714
    static CB_SEEN_ABOUT_TO_FINISH: AtomicBool = AtomicBool::new(false);
715
    static CB_RETURN_TERMINATE: AtomicBool = AtomicBool::new(false);
716

            
717
    /// Serialises access to the fake-clock / recorder statics. Ignores poisoning:
718
    /// a `#[should_panic]`-free suite still panics on assertion failure, and a
719
    /// poisoned lock must not cascade into unrelated test failures.
720
    fn clock_guard() -> MutexGuard<'static, ()> {
721
        let guard = CLOCK_LOCK.lock().unwrap_or_else(PoisonError::into_inner);
722
        FAKE_TICK.store(0, Ordering::SeqCst);
723
        CB_INVOCATIONS.store(0, Ordering::SeqCst);
724
        CB_SEEN_CALL_COUNT.store(0, Ordering::SeqCst);
725
        CB_SEEN_FRAME_START.store(0, Ordering::SeqCst);
726
        CB_SEEN_ABOUT_TO_FINISH.store(false, Ordering::SeqCst);
727
        CB_RETURN_TERMINATE.store(false, Ordering::SeqCst);
728
        guard
729
    }
730

            
731
    fn set_now(t: u64) {
732
        FAKE_TICK.store(t, Ordering::SeqCst);
733
    }
734

            
735
    extern "C" fn fake_clock() -> Instant {
736
        Instant::Tick(SystemTick::new(FAKE_TICK.load(Ordering::SeqCst)))
737
    }
738

            
739
    fn fake_clock_cb() -> GetSystemTimeCallback {
740
        GetSystemTimeCallback { cb: fake_clock }
741
    }
742

            
743
    /// Records everything the timer machinery handed it, and returns whatever
744
    /// `CB_RETURN_TERMINATE` currently says.
745
    extern "C" fn recording_cb(_data: RefAny, info: TimerCallbackInfo) -> TimerCallbackReturn {
746
        CB_INVOCATIONS.fetch_add(1, Ordering::SeqCst);
747
        CB_SEEN_CALL_COUNT.store(info.call_count, Ordering::SeqCst);
748
        CB_SEEN_ABOUT_TO_FINISH.store(info.is_about_to_finish, Ordering::SeqCst);
749
        if let Instant::Tick(t) = &info.frame_start {
750
            CB_SEEN_FRAME_START.store(t.tick_counter, Ordering::SeqCst);
751
        }
752
        TimerCallbackReturn {
753
            should_update: Update::DoNothing,
754
            should_terminate: if CB_RETURN_TERMINATE.load(Ordering::SeqCst) {
755
                TerminateTimer::Terminate
756
            } else {
757
                TerminateTimer::Continue
758
            },
759
        }
760
    }
761

            
762
    // Two callbacks with *different bodies* — identical bodies are legal prey for
763
    // identical-code folding, which would silently merge their addresses and make
764
    // the `TimerCallback` Eq/Ord/Hash tests below vacuous.
765
    extern "C" fn cb_alpha(_d: RefAny, _i: TimerCallbackInfo) -> TimerCallbackReturn {
766
        TimerCallbackReturn {
767
            should_update: Update::RefreshDom,
768
            should_terminate: TerminateTimer::Terminate,
769
        }
770
    }
771
    extern "C" fn cb_beta(_d: RefAny, _i: TimerCallbackInfo) -> TimerCallbackReturn {
772
        TimerCallbackReturn {
773
            should_update: Update::DoNothing,
774
            should_terminate: TerminateTimer::Continue,
775
        }
776
    }
777

            
778
    /// A timer created at tick `created`, driven by the fake clock.
779
    fn timer_at(created: u64, cb: TimerCallbackType) -> Timer {
780
        set_now(created);
781
        Timer::create(RefAny::new(0_usize), cb, fake_clock_cb())
782
    }
783

            
784
    // ------------------------------------------------------------------
785
    // CallbackInfo harness (mirrors the one in `scroll_timer.rs`)
786
    // ------------------------------------------------------------------
787

            
788
    struct Env<'a> {
789
        ref_data: &'a CallbackInfoRefData<'a>,
790
        changes: &'a Arc<Mutex<Vec<CallbackChange>>>,
791
    }
792

            
793
    impl Env<'_> {
794
        fn info(&self) -> CallbackInfo {
795
            self.info_with(OptionLogicalPosition::None, OptionLogicalPosition::None)
796
        }
797

            
798
        fn info_with(
799
            &self,
800
            cursor_relative_to_item: OptionLogicalPosition,
801
            cursor_in_viewport: OptionLogicalPosition,
802
        ) -> CallbackInfo {
803
            CallbackInfo::new(
804
                self.ref_data,
805
                self.changes,
806
                DomNodeId {
807
                    dom: DomId::ROOT_ID,
808
                    node: NodeHierarchyItemId::NONE,
809
                },
810
                cursor_relative_to_item,
811
                cursor_in_viewport,
812
            )
813
        }
814

            
815
        /// A `TimerCallbackInfo` with no attached node, frame_start = tick 0.
816
        fn timer_info(&self) -> TimerCallbackInfo {
817
            TimerCallbackInfo::create(self.info(), OptionDomNodeId::None, tick(0), 0, false)
818
        }
819

            
820
        fn take_changes(&self) -> Vec<CallbackChange> {
821
            self.changes
822
                .lock()
823
                .map(|mut c| core::mem::take(&mut *c))
824
                .unwrap_or_default()
825
        }
826

            
827
        /// Drain the log, asserting it holds exactly one change, and return it.
828
        fn take_one(&self) -> CallbackChange {
829
            let mut changes = self.take_changes();
830
            assert_eq!(changes.len(), 1, "expected exactly one change: {changes:?}");
831
            changes.remove(0)
832
        }
833
    }
834

            
835
    fn with_env<R>(f: impl FnOnce(&Env<'_>) -> R) -> R {
836
        with_env_cfg(false, OptionRefAny::None, f)
837
    }
838

            
839
    /// Builds a callback environment over an empty `LayoutWindow`. `left_down`
840
    /// drives the mouse state the drag predicates read; `ctx` is what `get_ctx`
841
    /// hands back to FFI bindings.
842
    fn with_env_cfg<R>(left_down: bool, ctx: OptionRefAny, f: impl FnOnce(&Env<'_>) -> R) -> R {
843
        let layout_window =
844
            LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new failed");
845
        let renderer_resources = RendererResources::default();
846
        let previous_window_state: Option<FullWindowState> = None;
847
        let mut current_window_state = FullWindowState::default();
848
        current_window_state.mouse_state.left_down = left_down;
849
        let gl_context = OptionGlContextPtr::None;
850
        let scroll_states: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> =
851
            BTreeMap::new();
852
        let window_handle = RawWindowHandle::Unsupported;
853
        let system_callbacks = ExternalSystemCallbacks::rust_internal();
854

            
855
        let ref_data = CallbackInfoRefData {
856
            layout_window: &layout_window,
857
            renderer_resources: &renderer_resources,
858
            previous_window_state: &previous_window_state,
859
            current_window_state: &current_window_state,
860
            gl_context: &gl_context,
861
            current_scroll_manager: &scroll_states,
862
            current_window_handle: &window_handle,
863
            system_callbacks: &system_callbacks,
864
            system_style: Arc::new(SystemStyle::default()),
865
            monitors: Arc::new(Mutex::new(MonitorVec::from_const_slice(&[]))),
866
            #[cfg(feature = "icu")]
867
            icu_localizer: IcuLocalizerHandle::default(),
868
            ctx,
869
        };
870

            
871
        let changes: Arc<Mutex<Vec<CallbackChange>>> = Arc::new(Mutex::new(Vec::new()));
872
        let env = Env {
873
            ref_data: &ref_data,
874
            changes: &changes,
875
        };
876
        f(&env)
877
    }
878

            
879
    fn empty_menu() -> Menu {
880
        Menu::create(MenuItemVec::from_const_slice(&[]))
881
    }
882

            
883
    // ==================================================================
884
    // Timer::create / Default — constructor invariants
885
    // ==================================================================
886

            
887
    #[test]
888
    fn timer_create_starts_completely_unarmed() {
889
        let _g = clock_guard();
890
        let t = timer_at(12_345, recording_cb as TimerCallbackType);
891

            
892
        assert_eq!(tick_of(&t.created), 12_345, "created must come from the clock");
893
        assert_eq!(t.run_count, 0);
894
        assert_eq!(t.last_run, OptionInstant::None);
895
        assert_eq!(t.delay, OptionDuration::None);
896
        assert_eq!(t.interval, OptionDuration::None);
897
        assert_eq!(t.timeout, OptionDuration::None);
898
        assert_eq!(t.node_id, OptionDomNodeId::None);
899
    }
900

            
901
    #[test]
902
    fn timer_create_at_max_tick_does_not_panic() {
903
        let _g = clock_guard();
904
        let t = timer_at(u64::MAX, recording_cb as TimerCallbackType);
905
        assert_eq!(tick_of(&t.created), u64::MAX);
906
        // Nothing is armed, so nothing can overflow off the end of time.
907
        assert!(!t.is_about_to_finish(&tick(u64::MAX)));
908
        assert_eq!(tick_of(&t.instant_of_next_run()), u64::MAX);
909
    }
910

            
911
    #[test]
912
    fn timer_default_is_a_zero_tick_timer() {
913
        let t = Timer::default();
914
        assert_eq!(tick_of(&t.created), 0);
915
        assert_eq!(t.run_count, 0);
916
        assert_eq!(t.last_run, OptionInstant::None);
917
        assert_eq!(t.tick_millis(), DEFAULT_TIMER_TICK_MS);
918
    }
919

            
920
    #[test]
921
    fn timer_clone_equals_original() {
922
        let _g = clock_guard();
923
        let t = timer_at(7, recording_cb as TimerCallbackType)
924
            .with_delay(tick_dur(1))
925
            .with_interval(tick_dur(2))
926
            .with_timeout(tick_dur(3));
927
        let c = t.clone();
928
        assert_eq!(t, c, "Clone must be value-preserving");
929
    }
930

            
931
    // ==================================================================
932
    // Timer::tick_millis — numeric limits / round-trip
933
    // ==================================================================
934

            
935
    #[test]
936
    fn tick_millis_falls_back_to_default_without_interval() {
937
        let _g = clock_guard();
938
        let t = timer_at(0, recording_cb as TimerCallbackType);
939
        assert_eq!(t.tick_millis(), DEFAULT_TIMER_TICK_MS);
940
        assert_eq!(t.tick_millis(), 10);
941

            
942
        // A delay/timeout must NOT be mistaken for an interval.
943
        let t = t.with_delay(tick_dur(999)).with_timeout(tick_dur(888));
944
        assert_eq!(t.tick_millis(), DEFAULT_TIMER_TICK_MS);
945
    }
946

            
947
    /// A tick interval is a FRAME count, so the platform wake-up interval it
948
    /// implies is `ticks / 60` seconds, not `ticks` milliseconds. The old
949
    /// pass-through made a `5t` timer ask the OS to wake every 5ms for a callback
950
    /// that can only fire every ~83ms.
951
    #[test]
952
    fn tick_millis_converts_tick_intervals_at_the_nominal_frame_rate() {
953
        let _g = clock_guard();
954
        for (raw, expected_ms) in [
955
            (0_u64, 0_u64),
956
            (1, 16),
957
            (5, 83),
958
            (60, 1_000),
959
            (600, 10_000),
960
        ] {
961
            let t = timer_at(0, recording_cb as TimerCallbackType).with_interval(tick_dur(raw));
962
            assert_eq!(
963
                t.tick_millis(),
964
                expected_ms,
965
                "a {raw}-tick interval is {expected_ms}ms of wall clock"
966
            );
967
        }
968
    }
969

            
970
    /// `u64::MAX` ticks is ~9.7 billion years, which does NOT fit in `u64`
971
    /// milliseconds. The conversion must clamp — wrapping would turn "never" into
972
    /// some small interval and busy-wake the event loop forever.
973
    #[test]
974
    fn tick_millis_saturates_on_an_absurd_tick_interval_instead_of_wrapping() {
975
        let _g = clock_guard();
976
        let t = timer_at(0, recording_cb as TimerCallbackType).with_interval(tick_dur(u64::MAX));
977
        assert_eq!(t.tick_millis(), u64::MAX);
978

            
979
        // The largest tick interval that still converts without clamping.
980
        let fits = u64::MAX / 1000 * 60;
981
        let t = timer_at(0, recording_cb as TimerCallbackType).with_interval(tick_dur(fits));
982
        assert!(t.tick_millis() < u64::MAX, "{fits} ticks should not clamp");
983
    }
984

            
985
    #[test]
986
    fn tick_millis_system_interval_round_trips_whole_millis() {
987
        let _g = clock_guard();
988
        // from_millis -> millis() is an exact round-trip, even at u64::MAX
989
        // (secs*1000 + 615 lands exactly on u64::MAX without saturating).
990
        for ms in [0_u64, 1, 999, 1_000, 1_001, 86_400_000, u64::MAX] {
991
            let t = timer_at(0, recording_cb as TimerCallbackType).with_interval(sys_dur_millis(ms));
992
            assert_eq!(t.tick_millis(), ms, "millis {ms} must round-trip");
993
        }
994
    }
995

            
996
    #[test]
997
    fn tick_millis_saturates_instead_of_overflowing() {
998
        let _g = clock_guard();
999
        // secs::MAX * 1000 overflows u64 — `millis()` must saturate, not panic.
        let huge = Duration::System(SystemTimeDiff {
            secs: u64::MAX,
            nanos: 999_999_999,
        });
        let t = timer_at(0, recording_cb as TimerCallbackType).with_interval(huge);
        assert_eq!(t.tick_millis(), u64::MAX);
    }
    #[test]
    fn tick_millis_truncates_sub_millisecond_intervals_to_zero() {
        let _g = clock_guard();
        // A 999_999ns interval is a *sub-millisecond* tick request; it truncates
        // to 0, i.e. "tick as fast as possible", not to 1.
        let t = timer_at(0, recording_cb as TimerCallbackType)
            .with_interval(Duration::System(SystemTimeDiff::from_nanos(999_999)));
        assert_eq!(t.tick_millis(), 0);
    }
    // ==================================================================
    // Timer::is_about_to_finish — predicate boundaries
    // ==================================================================
    #[test]
    fn is_about_to_finish_is_false_without_a_timeout() {
        let _g = clock_guard();
        let t = timer_at(0, recording_cb as TimerCallbackType);
        assert!(!t.is_about_to_finish(&tick(0)));
        assert!(!t.is_about_to_finish(&tick(u64::MAX)), "no timeout = never finishes");
    }
    #[test]
    fn is_about_to_finish_boundary_is_strictly_greater() {
        let _g = clock_guard();
        let t = timer_at(100, recording_cb as TimerCallbackType).with_timeout(tick_dur(50));
        assert!(!t.is_about_to_finish(&tick(149)), "1 tick early");
        // Elapsed == timeout is NOT "about to finish" — the comparison is `>`.
        assert!(!t.is_about_to_finish(&tick(150)), "exactly at the timeout");
        assert!(t.is_about_to_finish(&tick(151)), "1 tick past the timeout");
    }
    #[test]
    fn is_about_to_finish_saturates_when_the_clock_runs_backwards() {
        let _g = clock_guard();
        let t = timer_at(1_000, recording_cb as TimerCallbackType).with_timeout(tick_dur(10));
        // `now` older than `created`: duration_since saturates to 0 rather than
        // underflowing, so the timer is simply "not finished".
        assert!(!t.is_about_to_finish(&tick(0)));
    }
    #[test]
    fn is_about_to_finish_at_the_u64_ceiling() {
        let _g = clock_guard();
        let t = timer_at(0, recording_cb as TimerCallbackType);
        let max_timeout = t.clone().with_timeout(tick_dur(u64::MAX));
        assert!(
            !max_timeout.is_about_to_finish(&tick(u64::MAX)),
            "MAX elapsed is not > MAX timeout"
        );
        let near_max = t.with_timeout(tick_dur(u64::MAX - 1));
        assert!(near_max.is_about_to_finish(&tick(u64::MAX)));
    }
    /// A wall-clock timeout on a tick-driven timer must EXPIRE. `duration_since`
    /// yields a Tick duration, which is now compared against the System timeout
    /// on a canonical scale instead of saturating to `false`.
    ///
    /// The old saturating comparison made this timeout unexpirable — a timer
    /// created with a timeout silently became a timer with no timeout, and the
    /// only symptom was a callback that ran forever.
    #[test]
    fn is_about_to_finish_expires_a_wall_clock_timeout_on_a_tick_clock() {
        let _g = clock_guard();
        // 1ms at 60Hz is less than one frame, so the FIRST tick already exceeds it.
        let t = timer_at(0, recording_cb as TimerCallbackType).with_timeout(sys_dur_millis(1));
        assert!(!t.is_about_to_finish(&tick(0)), "no time has passed yet");
        assert!(t.is_about_to_finish(&tick(1)));
        assert!(t.is_about_to_finish(&tick(u64::MAX)));
        // A one-second timeout takes 60 whole frames, and expires strictly after.
        let t = timer_at(0, recording_cb as TimerCallbackType).with_timeout(sys_dur_millis(1_000));
        assert!(!t.is_about_to_finish(&tick(59)), "1 frame early");
        assert!(!t.is_about_to_finish(&tick(60)), "exactly at the timeout");
        assert!(t.is_about_to_finish(&tick(61)), "1 frame past the timeout");
    }
    // ==================================================================
    // Timer::instant_of_next_run — getter invariants
    // ==================================================================
    #[test]
    fn instant_of_next_run_is_created_when_nothing_is_armed() {
        let _g = clock_guard();
        let t = timer_at(42, recording_cb as TimerCallbackType);
        assert_eq!(tick_of(&t.instant_of_next_run()), 42);
    }
    #[test]
    fn instant_of_next_run_prefers_last_run_over_created() {
        let _g = clock_guard();
        let mut t = timer_at(100, recording_cb as TimerCallbackType).with_interval(tick_dur(7));
        assert_eq!(tick_of(&t.instant_of_next_run()), 107, "no run yet: created + interval");
        t.last_run = OptionInstant::Some(tick(500));
        assert_eq!(tick_of(&t.instant_of_next_run()), 507, "after a run: last_run + interval");
    }
    #[test]
    fn instant_of_next_run_sums_delay_and_interval() {
        let _g = clock_guard();
        // NOTE: when BOTH are set, the schedule point is `base + delay + interval`
        // — the delay is re-added on every subsequent run, even though `invoke`
        // only gates the *first* run on the delay. Pinned here as current
        // behaviour; see the report.
        let mut t = timer_at(100, recording_cb as TimerCallbackType)
            .with_delay(tick_dur(5))
            .with_interval(tick_dur(7));
        assert_eq!(tick_of(&t.instant_of_next_run()), 112);
        t.last_run = OptionInstant::Some(tick(200));
        assert_eq!(tick_of(&t.instant_of_next_run()), 212);
    }
    #[test]
    fn instant_of_next_run_saturates_at_the_end_of_time() {
        let _g = clock_guard();
        let t = timer_at(u64::MAX, recording_cb as TimerCallbackType)
            .with_delay(tick_dur(u64::MAX))
            .with_interval(tick_dur(u64::MAX));
        // Three MAXes added together: saturating_add, not an overflow panic.
        assert_eq!(tick_of(&t.instant_of_next_run()), u64::MAX);
    }
    /// System durations on a Tick instant are CONVERTED to whole frames, not
    /// dropped. Dropping them made `instant_of_next_run == created`, i.e. the
    /// timer claimed to be due immediately and stayed that way forever, which is
    /// how a unit mismatch turned into a scheduling bug with no error.
    #[test]
    fn instant_of_next_run_converts_wall_clock_delays_to_whole_frames() {
        let _g = clock_guard();
        let t = timer_at(42, recording_cb as TimerCallbackType)
            .with_delay(sys_dur_millis(1_000))
            .with_interval(sys_dur_millis(1_000));
        // 1000ms is 60 frames; delay + interval is 120.
        assert_eq!(tick_of(&t.instant_of_next_run()), 42 + 120);
    }
    /// A sub-frame wall-clock interval rounds DOWN to zero frames on a tick
    /// clock. That is the truthful answer at one-frame resolution — but it does
    /// mean such a timer is always due, so it is pinned here deliberately rather
    /// than rounded up to 1 to make something happen.
    #[test]
    fn instant_of_next_run_rounds_a_sub_frame_interval_down_to_zero_ticks() {
        let _g = clock_guard();
        let t = timer_at(42, recording_cb as TimerCallbackType).with_interval(sys_dur_millis(1));
        assert_eq!(tick_of(&t.instant_of_next_run()), 42);
    }
    // ==================================================================
    // with_delay / with_interval / with_timeout — builder invariants
    // ==================================================================
    #[test]
    fn with_setters_are_independent_and_preserve_the_rest() {
        let _g = clock_guard();
        let t = timer_at(9, recording_cb as TimerCallbackType)
            .with_delay(tick_dur(1))
            .with_interval(tick_dur(2))
            .with_timeout(tick_dur(3));
        assert_eq!(t.delay, OptionDuration::Some(tick_dur(1)));
        assert_eq!(t.interval, OptionDuration::Some(tick_dur(2)));
        assert_eq!(t.timeout, OptionDuration::Some(tick_dur(3)));
        // The builders must not disturb identity/progress fields.
        assert_eq!(tick_of(&t.created), 9);
        assert_eq!(t.run_count, 0);
        assert_eq!(t.last_run, OptionInstant::None);
    }
    #[test]
    fn with_setters_are_last_write_wins() {
        let _g = clock_guard();
        let t = timer_at(0, recording_cb as TimerCallbackType)
            .with_delay(tick_dur(1))
            .with_delay(tick_dur(2))
            .with_interval(tick_dur(3))
            .with_interval(tick_dur(4))
            .with_timeout(tick_dur(5))
            .with_timeout(tick_dur(6));
        assert_eq!(t.delay, OptionDuration::Some(tick_dur(2)));
        assert_eq!(t.interval, OptionDuration::Some(tick_dur(4)));
        assert_eq!(t.timeout, OptionDuration::Some(tick_dur(6)));
    }
    #[test]
    fn with_setters_accept_extreme_durations() {
        let _g = clock_guard();
        let t = timer_at(0, recording_cb as TimerCallbackType)
            .with_delay(tick_dur(0))
            .with_interval(tick_dur(u64::MAX))
            .with_timeout(Duration::max());
        assert_eq!(t.delay, OptionDuration::Some(tick_dur(0)));
        assert_eq!(t.tick_millis(), u64::MAX);
        // Duration::max() is a System duration -> never expires on a tick clock.
        assert!(!t.is_about_to_finish(&tick(u64::MAX)));
    }
    // ==================================================================
    // TimerCallback — identity, ordering, hashing
    // ==================================================================
    #[test]
    fn timer_callback_create_has_no_ffi_ctx() {
        let cb = TimerCallback::create(cb_alpha as TimerCallbackType);
        assert_eq!(cb.ctx, OptionRefAny::None);
        // `create` and the `From` impl must agree.
        let from: TimerCallback = (cb_alpha as TimerCallbackType).into();
        assert_eq!(cb, from);
    }
    #[test]
    fn timer_callback_identity_is_by_function_pointer() {
        let a1 = TimerCallback::create(cb_alpha as TimerCallbackType);
        let a2 = TimerCallback::create(cb_alpha as TimerCallbackType);
        let b = TimerCallback::create(cb_beta as TimerCallbackType);
        assert_eq!(a1, a2, "same fn -> equal");
        assert_ne!(a1, b, "different fn -> not equal");
        assert_eq!(a1, a1.clone(), "Clone preserves identity");
    }
    #[test]
    fn timer_callback_ord_and_hash_agree_with_eq() {
        use std::{
            collections::hash_map::DefaultHasher,
            hash::{Hash, Hasher},
        };
        fn hash_of(cb: &TimerCallback) -> u64 {
            let mut h = DefaultHasher::new();
            cb.hash(&mut h);
            h.finish()
        }
        let a = TimerCallback::create(cb_alpha as TimerCallbackType);
        let a2 = a.clone();
        let b = TimerCallback::create(cb_beta as TimerCallbackType);
        assert_eq!(a.cmp(&a2), core::cmp::Ordering::Equal);
        assert_eq!(hash_of(&a), hash_of(&a2), "Eq values must hash equal");
        // Ord must be antisymmetric and consistent with partial_cmp.
        assert_eq!(a.cmp(&b), a.partial_cmp(&b).unwrap());
        assert_eq!(a.cmp(&b).reverse(), b.cmp(&a));
        assert_ne!(a.cmp(&b), core::cmp::Ordering::Equal, "distinct fns must order strictly");
    }
    #[test]
    fn timer_callback_debug_does_not_panic() {
        let s = format!("{:?}", TimerCallback::create(cb_alpha as TimerCallbackType));
        assert!(s.starts_with("TimerCallback"), "got {s}");
    }
    // ==================================================================
    // OptionTimer — round-trip
    // ==================================================================
    #[test]
    fn option_timer_round_trips_both_variants() {
        let _g = clock_guard();
        assert!(OptionTimer::None.into_option().is_none());
        assert!(OptionTimer::from(None).into_option().is_none());
        let t = timer_at(3, recording_cb as TimerCallbackType).with_interval(tick_dur(4));
        let round_tripped = OptionTimer::from(Some(t.clone()))
            .into_option()
            .expect("Some must survive the round-trip");
        assert_eq!(round_tripped, t, "encode == decode");
    }
    // ==================================================================
    // TimerCallbackInfo::create + getters
    // ==================================================================
    #[test]
    fn timer_callback_info_create_preserves_extremes() {
        with_env(|env| {
            let info = TimerCallbackInfo::create(
                env.info(),
                OptionDomNodeId::None,
                tick(u64::MAX),
                usize::MAX,
                true,
            );
            assert_eq!(info.call_count, usize::MAX, "no wrap at usize::MAX");
            assert!(info.is_about_to_finish);
            assert_eq!(tick_of(&info.frame_start), u64::MAX);
            assert!(info._abi_ref.is_null());
            assert!(info._abi_mut.is_null());
            let zero =
                TimerCallbackInfo::create(env.info(), OptionDomNodeId::None, tick(0), 0, false);
            assert_eq!(zero.call_count, 0);
            assert!(!zero.is_about_to_finish);
            assert_eq!(tick_of(&zero.frame_start), 0);
        });
    }
    #[test]
    fn get_current_time_returns_frame_start_verbatim() {
        with_env(|env| {
            for t in [0_u64, 1, u64::MAX] {
                let info =
                    TimerCallbackInfo::create(env.info(), OptionDomNodeId::None, tick(t), 0, false);
                assert_eq!(info.get_current_time(), tick(t));
            }
        });
    }
    #[test]
    fn attached_node_queries_are_none_without_an_attached_node() {
        with_env(|env| {
            let info = env.timer_info();
            assert!(info.get_attached_node_size().is_none());
            assert!(info.get_attached_node_position().is_none());
        });
    }
    #[test]
    fn attached_node_queries_are_none_for_a_bogus_node() {
        with_env(|env| {
            // Largest node index that survives the +1 encoding, in a DOM that
            // doesn't exist, on a window with no layout results at all.
            let bogus = DomNodeId {
                dom: DomId { inner: usize::MAX },
                node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(usize::MAX - 1))),
            };
            let info = TimerCallbackInfo::create(
                env.info(),
                OptionDomNodeId::Some(bogus),
                tick(0),
                0,
                false,
            );
            assert!(info.get_attached_node_size().is_none(), "must not panic or index OOB");
            assert!(info.get_attached_node_position().is_none());
        });
    }
    #[test]
    fn get_callback_info_and_mut_alias_the_same_inner_info() {
        with_env(|env| {
            let mut info = env.timer_info();
            let addr_shared = std::ptr::from_ref(info.get_callback_info());
            let addr_mut = std::ptr::from_mut(info.get_callback_info_mut()).cast_const();
            assert!(std::ptr::eq(addr_shared, addr_mut), "both must alias the inner CallbackInfo");
            // A change pushed through the &mut view lands in the shared log.
            info.get_callback_info_mut().prevent_default();
            assert!(matches!(env.take_one(), CallbackChange::PreventDefault));
        });
    }
    #[test]
    fn get_ctx_is_none_for_native_rust_callbacks() {
        with_env(|env| {
            assert_eq!(env.timer_info().get_ctx(), OptionRefAny::None);
        });
    }
    #[test]
    fn get_ctx_hands_back_the_ffi_callable() {
        let ctx = RefAny::new(0xDEAD_BEEF_u32);
        with_env_cfg(false, OptionRefAny::Some(ctx.clone()), |env| {
            let got = env.timer_info().get_ctx().into_option().expect("ctx must survive");
            assert_eq!(got, ctx, "get_ctx must hand back the same RefAny");
        });
    }
    // ==================================================================
    // Delegated mutators — every one must land in the transaction log
    // ==================================================================
    #[test]
    fn add_and_remove_timer_push_the_matching_changes() {
        let _g = clock_guard();
        with_env(|env| {
            let mut info = env.timer_info();
            let id = TimerId { id: usize::MAX };
            info.add_timer(id, timer_at(1, recording_cb as TimerCallbackType));
            let CallbackChange::AddTimer { timer_id, timer } = env.take_one() else {
                panic!("expected AddTimer");
            };
            assert_eq!(timer_id, id);
            assert_eq!(tick_of(&timer.created), 1, "the timer must be stored verbatim");
            info.remove_timer(TimerId { id: 0 });
            let CallbackChange::RemoveTimer { timer_id } = env.take_one() else {
                panic!("expected RemoveTimer");
            };
            assert_eq!(timer_id.id, 0, "id 0 (a reserved system id) is still accepted");
        });
    }
    #[test]
    fn add_and_remove_thread_push_the_matching_changes() {
        extern "C" fn noop_worker(_d: RefAny, _s: ThreadSender, _r: ThreadReceiver) {}
        with_env(|env| {
            let mut info = env.timer_info();
            let id = ThreadId::unique();
            let thread = Thread::create(
                RefAny::new(0_usize),
                RefAny::new(0_usize),
                noop_worker as ThreadCallbackType,
            );
            info.add_thread(id, thread);
            let CallbackChange::AddThread { thread_id, .. } = env.take_one() else {
                panic!("expected AddThread");
            };
            assert_eq!(thread_id, id);
            info.remove_thread(id);
            let CallbackChange::RemoveThread { thread_id } = env.take_one() else {
                panic!("expected RemoveThread");
            };
            assert_eq!(thread_id, id);
        });
    }
    #[test]
    fn nullary_mutators_push_exactly_one_change_each_in_order() {
        with_env(|env| {
            let mut info = env.timer_info();
            info.stop_propagation();
            info.prevent_default();
            info.close_window();
            info.hide_tooltip();
            info.reload_system_fonts();
            info.update_all_image_callbacks();
            info.reset_cursor_blink();
            info.set_cursor_visibility_toggle();
            let changes = env.take_changes();
            assert_eq!(changes.len(), 8, "one change per call, no drops: {changes:?}");
            assert!(matches!(changes[0], CallbackChange::StopPropagation));
            assert!(matches!(changes[1], CallbackChange::PreventDefault));
            assert!(matches!(changes[2], CallbackChange::CloseWindow));
            assert!(matches!(changes[3], CallbackChange::HideTooltip));
            assert!(matches!(changes[4], CallbackChange::ReloadSystemFonts));
            assert!(matches!(changes[5], CallbackChange::UpdateAllImageCallbacks));
            assert!(matches!(changes[6], CallbackChange::ResetCursorBlink));
            assert!(matches!(changes[7], CallbackChange::ToggleCursorVisibility));
        });
    }
    #[test]
    fn set_cursor_visibility_records_both_polarities() {
        with_env(|env| {
            let mut info = env.timer_info();
            info.set_cursor_visibility(true);
            info.set_cursor_visibility(false);
            let changes = env.take_changes();
            assert_eq!(changes.len(), 2);
            let visibilities: Vec<bool> = changes
                .iter()
                .map(|c| match c {
                    CallbackChange::SetCursorVisibility { visible } => *visible,
                    other => panic!("expected SetCursorVisibility, got {other:?}"),
                })
                .collect();
            assert_eq!(visibilities, vec![true, false]);
        });
    }
    #[test]
    fn create_window_and_modify_window_state_push_changes() {
        with_env(|env| {
            let mut info = env.timer_info();
            info.create_window(WindowCreateOptions::default());
            assert!(matches!(env.take_one(), CallbackChange::CreateNewWindow { .. }));
            info.modify_window_state(FullWindowState::default());
            assert!(matches!(env.take_one(), CallbackChange::ModifyWindowState { .. }));
        });
    }
    #[test]
    fn image_cache_mutators_accept_degenerate_and_unicode_ids() {
        with_env(|env| {
            let mut info = env.timer_info();
            // A 0x0 null image with an empty tag is degenerate but legal.
            let img = ImageRef::null_image(0, 0, RawImageFormat::RGBA8, Vec::new());
            let id: AzString = String::new().into();
            info.add_image_to_cache(id.clone(), img);
            let CallbackChange::AddImageToCache { id: got, .. } = env.take_one() else {
                panic!("expected AddImageToCache");
            };
            assert_eq!(got, id, "an empty id is passed through, not rejected");
            // Embedded NUL, an RTL override and astral-plane chars must survive
            // the AzString round-trip byte-for-byte.
            let nasty: AzString = String::from("🚀\u{0}\u{202E}id\u{1F600}").into();
            info.remove_image_from_cache(nasty.clone());
            let CallbackChange::RemoveImageFromCache { id: got } = env.take_one() else {
                panic!("expected RemoveImageFromCache");
            };
            assert_eq!(got.as_str(), nasty.as_str());
        });
    }
    #[test]
    fn trigger_virtual_view_rerender_accepts_out_of_range_ids() {
        with_env(|env| {
            let mut info = env.timer_info();
            info.trigger_virtual_view_rerender(DomId { inner: usize::MAX }, NodeId::new(usize::MAX));
            let CallbackChange::UpdateVirtualView { dom_id, node_id } = env.take_one() else {
                panic!("expected UpdateVirtualView");
            };
            // Recorded verbatim — validation happens when the change is applied,
            // not here, and neither id may overflow on the way in.
            assert_eq!(dom_id.inner, usize::MAX);
            assert_eq!(node_id, NodeId::new(usize::MAX));
        });
    }
    #[test]
    fn open_menu_has_no_position_and_open_menu_at_carries_one() {
        with_env(|env| {
            let mut info = env.timer_info();
            info.open_menu(empty_menu());
            let CallbackChange::OpenMenu { position, .. } = env.take_one() else {
                panic!("expected OpenMenu");
            };
            assert!(position.is_none(), "open_menu must defer to menu.position");
            info.open_menu_at(empty_menu(), LogicalPosition::new(-1.5, 2.5));
            let CallbackChange::OpenMenu { position, .. } = env.take_one() else {
                panic!("expected OpenMenu");
            };
            let p = position.expect("open_menu_at must pin a position");
            assert_eq!((p.x, p.y), (-1.5, 2.5), "negative coordinates are legal");
        });
    }
    #[test]
    fn open_menu_at_passes_non_finite_coordinates_through_unchanged() {
        with_env(|env| {
            let mut info = env.timer_info();
            info.open_menu_at(
                empty_menu(),
                LogicalPosition::new(f32::NAN, f32::INFINITY),
            );
            let CallbackChange::OpenMenu { position, .. } = env.take_one() else {
                panic!("expected OpenMenu");
            };
            let p = position.expect("position must be recorded");
            // No clamping/sanitising at this layer — but it must not panic either.
            assert!(p.x.is_nan());
            assert!(p.y.is_infinite() && p.y.is_sign_positive());
            info.open_menu_at(empty_menu(), LogicalPosition::new(f32::MAX, f32::MIN));
            let CallbackChange::OpenMenu { position, .. } = env.take_one() else {
                panic!("expected OpenMenu");
            };
            let p = position.expect("position must be recorded");
            assert_eq!((p.x, p.y), (f32::MAX, f32::MIN));
        });
    }
    #[test]
    fn open_menu_for_hit_node_is_false_and_silent_without_a_hit_node() {
        with_env(|env| {
            let mut info = env.timer_info();
            // Hit node is NONE and the window has no layout results: the menu has
            // nothing to anchor to.
            assert!(!info.open_menu_for_hit_node(empty_menu()));
            assert!(
                env.take_changes().is_empty(),
                "a failed anchor must not queue a half-open menu"
            );
        });
    }
    #[test]
    fn show_tooltip_falls_back_to_the_origin_without_a_cursor() {
        with_env(|env| {
            let mut info = env.timer_info();
            info.show_tooltip(String::from("hi").into());
            let CallbackChange::ShowTooltip { text, position } = env.take_one() else {
                panic!("expected ShowTooltip");
            };
            assert_eq!(text.as_str(), "hi");
            assert_eq!((position.x, position.y), (0.0, 0.0), "no cursor -> origin");
        });
    }
    #[test]
    fn show_tooltip_uses_the_viewport_cursor_when_there_is_one() {
        with_env(|env| {
            let cursor = LogicalPosition::new(3.0, 4.0);
            let mut info = TimerCallbackInfo::create(
                env.info_with(OptionLogicalPosition::None, OptionLogicalPosition::Some(cursor)),
                OptionDomNodeId::None,
                tick(0),
                0,
                false,
            );
            info.show_tooltip(String::from("t").into());
            let CallbackChange::ShowTooltip { position, .. } = env.take_one() else {
                panic!("expected ShowTooltip");
            };
            assert_eq!((position.x, position.y), (3.0, 4.0));
        });
    }
    #[test]
    fn show_tooltip_at_records_empty_text_and_non_finite_positions() {
        with_env(|env| {
            let mut info = env.timer_info();
            info.show_tooltip_at(String::new().into(), LogicalPosition::new(f32::NAN, -0.0));
            let CallbackChange::ShowTooltip { text, position } = env.take_one() else {
                panic!("expected ShowTooltip");
            };
            assert_eq!(text.as_str(), "", "empty tooltip text is not rejected");
            assert!(position.x.is_nan());
            assert!(position.y.is_sign_negative());
        });
    }
    // ==================================================================
    // Scroll delegation — numeric edges
    // ==================================================================
    #[test]
    fn scroll_to_and_unclamped_differ_only_in_the_clamp_flag() {
        with_env(|env| {
            let mut info = env.timer_info();
            let node = NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(0)));
            let pos = LogicalPosition::new(10.0, 20.0);
            info.scroll_to(DomId::ROOT_ID, node, pos);
            info.scroll_to_unclamped(DomId::ROOT_ID, node, pos);
            let changes = env.take_changes();
            assert_eq!(changes.len(), 2);
            let flags: Vec<bool> = changes
                .iter()
                .map(|c| match c {
                    CallbackChange::ScrollTo {
                        dom_id,
                        node_id,
                        position,
                        unclamped,
                    } => {
                        assert_eq!(*dom_id, DomId::ROOT_ID);
                        assert_eq!(*node_id, node);
                        assert_eq!((position.x, position.y), (10.0, 20.0));
                        *unclamped
                    }
                    other => panic!("expected ScrollTo, got {other:?}"),
                })
                .collect();
            assert_eq!(flags, vec![false, true], "only the overscroll flag differs");
        });
    }
    #[test]
    fn scroll_to_records_zero_negative_and_non_finite_positions() {
        with_env(|env| {
            let mut info = env.timer_info();
            let node = NodeHierarchyItemId::NONE;
            for pos in [
                LogicalPosition::new(0.0, 0.0),
                LogicalPosition::new(-1.0, -f32::MAX),
                LogicalPosition::new(f32::MAX, f32::INFINITY),
            ] {
                info.scroll_to(DomId::ROOT_ID, node, pos);
                let CallbackChange::ScrollTo { position, .. } = env.take_one() else {
                    panic!("expected ScrollTo");
                };
                assert_eq!(position.x.to_bits(), pos.x.to_bits(), "x must be recorded bit-exact");
                assert_eq!(position.y.to_bits(), pos.y.to_bits(), "y must be recorded bit-exact");
            }
            // NaN separately — it is never == to itself.
            info.scroll_to_unclamped(
                DomId { inner: usize::MAX },
                node,
                LogicalPosition::new(f32::NAN, f32::NAN),
            );
            let CallbackChange::ScrollTo {
                position, unclamped, ..
            } = env.take_one()
            else {
                panic!("expected ScrollTo");
            };
            assert!(position.x.is_nan() && position.y.is_nan(), "NaN is passed through, not zeroed");
            assert!(unclamped);
        });
    }
    #[test]
    fn scroll_queries_are_none_on_an_empty_window() {
        with_env(|env| {
            let info = env.timer_info();
            assert!(info.get_scroll_node_info(DomId::ROOT_ID, NodeId::new(0)).is_none());
            assert!(
                info.get_scroll_node_info(DomId { inner: usize::MAX }, NodeId::new(usize::MAX))
                    .is_none(),
                "an out-of-range dom/node must return None, not panic"
            );
            assert!(info.find_scroll_parent(DomId::ROOT_ID, NodeId::new(0)).is_none());
            assert!(
                info.find_scroll_parent(DomId { inner: usize::MAX }, NodeId::new(usize::MAX))
                    .is_none()
            );
        });
    }
    #[test]
    fn scroll_input_queue_starts_empty_and_draining_is_idempotent() {
        with_env(|env| {
            let info = env.timer_info();
            let queue = info.get_scroll_input_queue();
            assert!(queue.take_all().is_empty());
            assert!(queue.take_all().is_empty(), "draining twice must stay empty");
        });
    }
    // ==================================================================
    // Predicates / state getters
    // ==================================================================
    #[test]
    fn the_three_drag_predicates_are_aliases_of_left_down() {
        for left_down in [false, true] {
            with_env_cfg(left_down, OptionRefAny::None, |env| {
                let info = env.timer_info();
                assert_eq!(info.get_current_mouse_state().left_down, left_down);
                assert_eq!(info.is_dragging(), left_down);
                assert_eq!(info.is_drag_active(), left_down);
                assert_eq!(info.is_node_drag_active(), left_down);
            });
        }
    }
    #[test]
    fn pen_predicates_are_false_without_a_pen() {
        with_env(|env| {
            let info = env.timer_info();
            assert!(!info.is_pen_in_contact());
            assert!(!info.is_pen_eraser());
            assert!(!info.is_pen_barrel_button_pressed());
        });
    }
    #[test]
    fn drag_and_gesture_predicates_are_false_on_a_fresh_window() {
        with_env(|env| {
            let info = env.timer_info();
            assert!(!info.is_file_drag_active());
            assert!(!info.has_sufficient_history_for_gestures());
        });
    }
    #[test]
    fn is_dom_focused_is_false_for_every_dom_when_nothing_is_focused() {
        with_env(|env| {
            let info = env.timer_info();
            assert!(!info.is_dom_focused(DomId::ROOT_ID));
            assert!(!info.is_dom_focused(DomId { inner: usize::MAX }));
        });
    }
    #[test]
    fn window_state_getters_mirror_the_current_window_state() {
        with_env(|env| {
            let info = env.timer_info();
            let default_state = FullWindowState::default();
            assert_eq!(info.get_current_window_flags(), default_state.flags);
            assert_eq!(info.get_current_keyboard_state(), default_state.keyboard_state);
            assert_eq!(info.get_current_mouse_state(), default_state.mouse_state);
        });
    }
    #[test]
    fn cursor_getters_round_trip_including_nan() {
        with_env(|env| {
            let info = env.timer_info();
            assert!(info.get_cursor_position().is_none());
            assert_eq!(info.get_cursor_relative_to_viewport(), OptionLogicalPosition::None);
            assert!(info.get_cursor_relative_to_node().is_none());
            let viewport = LogicalPosition::new(f32::NAN, 7.5);
            let relative = LogicalPosition::new(-3.0, f32::INFINITY);
            let info = TimerCallbackInfo::create(
                env.info_with(
                    OptionLogicalPosition::Some(relative),
                    OptionLogicalPosition::Some(viewport),
                ),
                OptionDomNodeId::None,
                tick(0),
                0,
                false,
            );
            let got = info.get_cursor_position().expect("cursor must be Some");
            assert!(got.x.is_nan() && got.y == 7.5);
            let node_rel = info
                .get_cursor_relative_to_node()
                .into_option()
                .expect("relative cursor must be Some");
            assert_eq!(node_rel.x, -3.0);
            assert!(node_rel.y.is_infinite());
        });
    }
    // ==================================================================
    // Timer::invoke — the scheduling state machine
    // ==================================================================
    #[test]
    fn invoke_does_not_run_the_callback_before_the_delay_elapses() {
        let _g = clock_guard();
        with_env(|env| {
            let mut t =
                timer_at(0, recording_cb as TimerCallbackType).with_delay(tick_dur(100));
            let info = env.info();
            set_now(99);
            let r = t.invoke(&info, &fake_clock_cb());
            assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 0, "callback must not fire early");
            assert_eq!(r.should_update, Update::DoNothing);
            assert_eq!(r.should_terminate, TerminateTimer::Continue);
            // A skipped tick must leave the timer's progress untouched.
            assert_eq!(t.run_count, 0);
            assert_eq!(t.last_run, OptionInstant::None);
            // The boundary is inclusive: elapsed == delay runs.
            set_now(100);
            let r = t.invoke(&info, &fake_clock_cb());
            assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 1);
            assert_eq!(r.should_terminate, TerminateTimer::Continue);
            assert_eq!(t.run_count, 1);
            assert_eq!(t.last_run, OptionInstant::Some(tick(100)));
        });
    }
    #[test]
    fn invoke_gates_subsequent_runs_on_the_interval() {
        let _g = clock_guard();
        with_env(|env| {
            let mut t =
                timer_at(0, recording_cb as TimerCallbackType).with_interval(tick_dur(10));
            let info = env.info();
            // No delay -> the first invoke runs immediately.
            let r = t.invoke(&info, &fake_clock_cb());
            assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 1);
            assert_eq!(r.should_terminate, TerminateTimer::Continue);
            assert_eq!(t.run_count, 1);
            set_now(9);
            let r = t.invoke(&info, &fake_clock_cb());
            assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 1, "1 tick short of the interval");
            assert_eq!(r.should_update, Update::DoNothing);
            assert_eq!(t.run_count, 1, "a skipped tick must not count as a run");
            set_now(10);
            let _ = t.invoke(&info, &fake_clock_cb());
            assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 2);
            assert_eq!(t.run_count, 2);
            assert_eq!(t.last_run, OptionInstant::Some(tick(10)));
        });
    }
    #[test]
    fn invoke_hands_the_callback_the_run_count_and_frame_start() {
        let _g = clock_guard();
        with_env(|env| {
            let mut t = timer_at(0, recording_cb as TimerCallbackType);
            let info = env.info();
            set_now(5);
            let _ = t.invoke(&info, &fake_clock_cb());
            assert_eq!(CB_SEEN_CALL_COUNT.load(Ordering::SeqCst), 0, "first run is call 0");
            assert_eq!(CB_SEEN_FRAME_START.load(Ordering::SeqCst), 5, "frame_start == now");
            assert!(!CB_SEEN_ABOUT_TO_FINISH.load(Ordering::SeqCst));
            set_now(6);
            let _ = t.invoke(&info, &fake_clock_cb());
            assert_eq!(CB_SEEN_CALL_COUNT.load(Ordering::SeqCst), 1, "run_count increments by 1");
            assert_eq!(CB_SEEN_FRAME_START.load(Ordering::SeqCst), 6);
        });
    }
    #[test]
    fn invoke_forces_terminate_once_the_timeout_expires() {
        let _g = clock_guard();
        with_env(|env| {
            let mut t = timer_at(0, recording_cb as TimerCallbackType).with_timeout(tick_dur(5));
            let info = env.info();
            // The callback insists on Continue...
            CB_RETURN_TERMINATE.store(false, Ordering::SeqCst);
            set_now(5);
            let r = t.invoke(&info, &fake_clock_cb());
            assert_eq!(r.should_terminate, TerminateTimer::Continue, "elapsed == timeout: alive");
            assert!(!CB_SEEN_ABOUT_TO_FINISH.load(Ordering::SeqCst));
            set_now(6);
            let r = t.invoke(&info, &fake_clock_cb());
            // ...but the timeout overrides it, and the callback is told so.
            assert!(CB_SEEN_ABOUT_TO_FINISH.load(Ordering::SeqCst), "last-call flag must be set");
            assert_eq!(r.should_terminate, TerminateTimer::Terminate);
            assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 2, "the final run still happens");
        });
    }
    #[test]
    fn invoke_honours_a_callback_requested_terminate() {
        let _g = clock_guard();
        with_env(|env| {
            let mut t = timer_at(0, recording_cb as TimerCallbackType);
            let info = env.info();
            CB_RETURN_TERMINATE.store(true, Ordering::SeqCst);
            let r = t.invoke(&info, &fake_clock_cb());
            assert_eq!(r.should_terminate, TerminateTimer::Terminate);
            // Termination is the caller's job; invoke still records the run.
            assert_eq!(t.run_count, 1);
            assert_eq!(t.last_run, OptionInstant::Some(tick(0)));
        });
    }
    #[test]
    fn invoke_skips_deterministically_when_the_clock_runs_backwards() {
        let _g = clock_guard();
        with_env(|env| {
            let mut t =
                timer_at(0, recording_cb as TimerCallbackType).with_interval(tick_dur(10));
            t.last_run = OptionInstant::Some(tick(1_000));
            let info = env.info();
            // now(0) is *older* than last_run(1000): duration_since saturates to 0,
            // 0 < 10, so the tick is skipped — no panic, no spurious run.
            set_now(0);
            let r = t.invoke(&info, &fake_clock_cb());
            assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 0);
            assert_eq!(r.should_terminate, TerminateTimer::Continue);
            assert_eq!(t.run_count, 0);
            assert_eq!(t.last_run, OptionInstant::Some(tick(1_000)), "last_run is untouched");
        });
    }
    /// A wall-clock interval on a tick-driven clock must THROTTLE, at the exact
    /// frame the interval converts to.
    ///
    /// It used to run on every single invoke: the Tick-vs-System comparison
    /// saturated to `false`, so the "not yet" branch was never taken and the
    /// interval was silently a no-op. A 60-second timer fired at 60Hz.
    #[test]
    fn invoke_throttles_a_wall_clock_interval_on_a_tick_clock_at_the_exact_frame() {
        let _g = clock_guard();
        with_env(|env| {
            // 60_000ms is exactly 3600 frames at 60Hz.
            let mut t =
                timer_at(0, recording_cb as TimerCallbackType).with_interval(sys_dur_millis(60_000));
            let info = env.info();
            // First invoke has no `last_run`, so only the (absent) delay gates it.
            let _ = t.invoke(&info, &fake_clock_cb());
            assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 1);
            set_now(1);
            let _ = t.invoke(&info, &fake_clock_cb());
            assert_eq!(
                CB_INVOCATIONS.load(Ordering::SeqCst),
                1,
                "a 60s interval must not fire one frame later"
            );
            // One frame BEFORE the interval: still nothing.
            set_now(3_599);
            let _ = t.invoke(&info, &fake_clock_cb());
            assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 1, "frame 3599 is early");
            // Exactly at the interval: fires, because the gate is `elapsed < interval`.
            set_now(3_600);
            let _ = t.invoke(&info, &fake_clock_cb());
            assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 2, "frame 3600 is the flip");
            assert_eq!(t.run_count, 2);
        });
    }
    // ==================================================================
    // The `t` (tick) unit, end to end through Timer::invoke
    //
    // These are the tests the CSS `t` unit exists for: advance the clock by an
    // EXACT number of frames and assert the callback fired on that frame and no
    // other. Nothing here can be perturbed by how fast the machine is.
    // ==================================================================
    /// A tick interval on a tick clock fires on exactly the Nth frame — not N-1,
    /// not N+1. This is the off-by-one detector: with a millisecond interval the
    /// boundary frame is whatever rounding produced, and a one-frame error hides
    /// inside the jitter.
    #[test]
    fn invoke_with_a_tick_interval_fires_on_exactly_the_nth_frame() {
        let _g = clock_guard();
        with_env(|env| {
            let mut t = timer_at(0, recording_cb as TimerCallbackType).with_interval(tick_dur(5));
            let info = env.info();
            // Frame 0: first run (no last_run, no delay).
            let _ = t.invoke(&info, &fake_clock_cb());
            assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 1);
            assert_eq!(t.last_run, OptionInstant::Some(tick(0)));
            // Frames 1..=4 are all early. Stepping one frame at a time is the
            // point: a boundary that is off by one shows up as a count mismatch
            // on a specific frame, not as a flaky total.
            for frame in 1..=4 {
                set_now(frame);
                let _ = t.invoke(&info, &fake_clock_cb());
                assert_eq!(
                    CB_INVOCATIONS.load(Ordering::SeqCst),
                    1,
                    "frame {frame} is inside the 5-frame interval and must not fire"
                );
            }
            // Frame 5 is the flip.
            set_now(5);
            let _ = t.invoke(&info, &fake_clock_cb());
            assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 2, "frame 5 must fire");
            assert_eq!(CB_SEEN_FRAME_START.load(Ordering::SeqCst), 5);
            assert_eq!(t.last_run, OptionInstant::Some(tick(5)));
            // ...and frame 6 is early again for the NEXT interval.
            set_now(6);
            let _ = t.invoke(&info, &fake_clock_cb());
            assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 2, "frame 6 restarts the wait");
            set_now(10);
            let _ = t.invoke(&info, &fake_clock_cb());
            assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 3, "frame 10 is the second flip");
        });
    }
    /// A `1t` interval fires on every single frame and never skips one — the
    /// degenerate case the unit has to get right for per-frame animation.
    #[test]
    fn a_one_tick_interval_fires_on_every_frame() {
        let _g = clock_guard();
        with_env(|env| {
            let mut t = timer_at(0, recording_cb as TimerCallbackType).with_interval(tick_dur(1));
            let info = env.info();
            for frame in 0..=10 {
                set_now(frame);
                let _ = t.invoke(&info, &fake_clock_cb());
                assert_eq!(
                    CB_INVOCATIONS.load(Ordering::SeqCst),
                    (frame + 1) as usize,
                    "every frame up to {frame} must have fired exactly once"
                );
            }
        });
    }
    /// A tick DELAY gates the first run on exactly the Nth frame, the same way a
    /// tick interval gates the rest.
    #[test]
    fn a_tick_delay_gates_the_first_run_on_exactly_the_nth_frame() {
        let _g = clock_guard();
        with_env(|env| {
            let mut t = timer_at(0, recording_cb as TimerCallbackType).with_delay(tick_dur(3));
            let info = env.info();
            for frame in 0..=2 {
                set_now(frame);
                let _ = t.invoke(&info, &fake_clock_cb());
                assert_eq!(
                    CB_INVOCATIONS.load(Ordering::SeqCst),
                    0,
                    "frame {frame} is inside the 3-frame delay"
                );
            }
            set_now(3);
            let _ = t.invoke(&info, &fake_clock_cb());
            assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 1, "frame 3 is the first run");
        });
    }
    /// A tick interval on a WALL-CLOCK timer converts, so a `5t` CSS interval
    /// behaves on a desktop shell exactly as it does on a tick clock: 5 frames is
    /// 83.33ms, so 83ms is early and 84ms fires.
    ///
    /// This is the direction that used to be broken the OTHER way round — the
    /// mismatched comparison never took the "not yet" branch, so a `5t` interval
    /// on a real shell fired on every wake of the platform loop.
    ///
    /// Driven by the injectable clock (frozen, then advanced by an exact number
    /// of milliseconds), so it is a wall-clock path with no wall-clock jitter.
    #[cfg(feature = "std")]
    #[test]
    fn a_tick_interval_throttles_a_wall_clock_timer_at_the_converted_boundary() {
        use azul_core::task::{
            advance_test_clock_ms, freeze_test_clock, get_system_time_libstd, reset_test_clock,
        };
        let _g = clock_guard();
        reset_test_clock();
        freeze_test_clock();
        let real_clock = GetSystemTimeCallback {
            cb: get_system_time_libstd,
        };
        with_env(|env| {
            let mut t = Timer::create(
                RefAny::new(0_usize),
                recording_cb as TimerCallbackType,
                real_clock,
            )
            .with_interval(tick_dur(5));
            let info = env.info();
            // The first invoke has no `last_run`, so only the (absent) delay
            // gates it: it runs at +0ms and arms the interval.
            let _ = t.invoke(&info, &real_clock);
            assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 1);
            assert!(
                matches!(t.last_run, OptionInstant::Some(Instant::System(_))),
                "this timer must really be running on the wall clock"
            );
            // 5 frames is 83.333ms, so 83ms is still inside the interval.
            let _ = advance_test_clock_ms(83);
            let _ = t.invoke(&info, &real_clock);
            assert_eq!(
                CB_INVOCATIONS.load(Ordering::SeqCst),
                1,
                "83ms is less than 5 frames (83.33ms) and must not fire"
            );
            // ...and one more millisecond is past it.
            let _ = advance_test_clock_ms(1);
            let _ = t.invoke(&info, &real_clock);
            assert_eq!(
                CB_INVOCATIONS.load(Ordering::SeqCst),
                2,
                "84ms is past 5 frames and must fire"
            );
        });
        reset_test_clock();
    }
    #[test]
    fn invoke_at_the_end_of_time_does_not_panic() {
        let _g = clock_guard();
        with_env(|env| {
            let mut t = timer_at(u64::MAX, recording_cb as TimerCallbackType)
                .with_delay(tick_dur(u64::MAX))
                .with_interval(tick_dur(u64::MAX))
                .with_timeout(tick_dur(u64::MAX));
            let info = env.info();
            set_now(u64::MAX);
            // elapsed = 0, delay = MAX -> 0 < MAX -> skipped, no overflow anywhere.
            let r = t.invoke(&info, &fake_clock_cb());
            assert_eq!(CB_INVOCATIONS.load(Ordering::SeqCst), 0);
            assert_eq!(r.should_terminate, TerminateTimer::Continue);
            assert_eq!(t.run_count, 0);
        });
    }
}