Skip to main content

azul_core/
task.rs

1//! Timer and thread management for asynchronous operations.
2//!
3//! This module provides:
4//! - `TimerId` / `ThreadId`: Unique identifiers for timers and background threads
5//! - `Instant` / `Duration`: Cross-platform time types (works on no_std with tick counters)
6//! - `ThreadReceiver`: Channel for receiving messages from the main thread
7//! - Callback types for thread communication and system time queries
8
9#[cfg(not(feature = "std"))]
10use alloc::string::{String, ToString};
11use alloc::{
12    boxed::Box,
13    collections::btree_map::BTreeMap,
14    sync::{Arc, Weak},
15    vec::Vec,
16};
17use core::{
18    ffi::c_void,
19    fmt,
20    mem::ManuallyDrop,
21    sync::atomic::{AtomicUsize, Ordering},
22};
23#[cfg(feature = "std")]
24use std::sync::mpsc::{Receiver, Sender};
25#[cfg(feature = "std")]
26use std::sync::Mutex;
27#[cfg(feature = "std")]
28use std::thread::{self, JoinHandle};
29#[cfg(feature = "std")]
30use std::time::Duration as StdDuration;
31#[cfg(feature = "std")]
32use std::time::Instant as StdInstant;
33
34use azul_css::{props::property::CssProperty, AzString};
35use rust_fontconfig::FcFontCache;
36
37use crate::{
38    callbacks::{FocusTarget, TimerCallbackReturn, Update},
39    dom::{DomId, DomNodeId, OptionDomNodeId},
40    geom::{LogicalPosition, OptionLogicalPosition},
41    gl::OptionGlContextPtr,
42    hit_test::ScrollPosition,
43    id::NodeId,
44    refany::{OptionRefAny, RefAny},
45    resources::{ImageCache, ImageMask, ImageRef},
46    styled_dom::NodeHierarchyItemId,
47    window::RawWindowHandle,
48    FastBTreeSet, OrderedMap,
49};
50
51/// Should a timer terminate or not - used to remove active timers
52#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
53#[repr(C)]
54pub enum TerminateTimer {
55    /// Remove the timer from the list of active timers
56    Terminate,
57    /// Do nothing and let the timers continue to run
58    Continue,
59}
60
61// ============================================================================
62// Reserved System Timer IDs (0x0000 - 0x00FF)
63// ============================================================================
64// User timers start at 0x0100 to avoid conflicts with system timers.
65// These constants define well-known timer IDs for internal framework use.
66
67/// Timer ID for cursor blinking in contenteditable elements (~530ms interval)
68pub const CURSOR_BLINK_TIMER_ID: TimerId = TimerId { id: 0x0001 };
69/// Timer ID for scroll momentum/inertia animation
70pub const SCROLL_MOMENTUM_TIMER_ID: TimerId = TimerId { id: 0x0002 };
71/// Timer ID for auto-scroll during drag operations near edges
72pub const DRAG_AUTOSCROLL_TIMER_ID: TimerId = TimerId { id: 0x0003 };
73/// Timer ID for tooltip show delay.
74///
75/// Started by the platform event loop when the hover target changes to a node
76/// that advertises a tooltip source (`aria-label` / `alt` / `title`); fires
77/// once after `SystemStyle::input_metrics.hover_time_ms` (`SPI_GETMOUSEHOVERTIME`
78/// on Windows, default 400ms) and emits a `ShowTooltip` `CallbackChange`. The
79/// timer is torn down on hover loss, which also emits `HideTooltip`.
80///
81/// Double-click detection used to live on a neighbouring reserved ID but is
82/// now handled entirely by `GestureManager::detect_double_click`, so no
83/// equivalent `DOUBLE_CLICK_TIMER_ID` exists.
84pub const TOOLTIP_DELAY_TIMER_ID: TimerId = TimerId { id: 0x0004 };
85/// Timer ID for the single-threaded capability pump (MWA-A1).
86///
87/// Armed by `sync_capability_pump_timer` whenever a capability source needs
88/// polling or draining while the app is otherwise idle (gamepad listeners,
89/// sensor listeners, an active geolocation subscription). Each tick wakes the
90/// blocked platform loop; `invoke_expired_timers` then runs an event pass,
91/// whose top-of-pass pump drains the async capability channels. There is NO
92/// pump thread by design — a recurring shell timer is the only wake
93/// mechanism, so the identical code path works on WASM (no threads).
94pub const CAPABILITY_PUMP_TIMER_ID: TimerId = TimerId { id: 0x0005 };
95/// Timer ID for the one-shot long-press wake-up (MWA-B12).
96///
97/// Armed on every `MouseDown` for the long-press threshold: a motionless
98/// press generates no further events, so no pass would ever evaluate
99/// `detect_long_press` — this timer wakes the loop exactly once at the
100/// threshold, `invoke_expired_timers` runs an event pass, and the
101/// detection fires (or doesn't — moved/released holds are no-ops).
102pub const LONG_PRESS_TIMER_ID: TimerId = TimerId { id: 0x0006 };
103
104/// First available ID for user-defined timers
105pub const USER_TIMER_ID_START: usize = 0x0100;
106
107// User timers start at 0x0100 to avoid conflicts with reserved system timer IDs
108static MAX_TIMER_ID: AtomicUsize = AtomicUsize::new(USER_TIMER_ID_START);
109
110/// ID for uniquely identifying a timer
111#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
112#[repr(C)]
113pub struct TimerId {
114    pub id: usize,
115}
116
117impl TimerId {
118    /// Generates a new, unique `TimerId`.
119    #[must_use]
120    pub fn unique() -> Self {
121        Self {
122            id: MAX_TIMER_ID.fetch_add(1, Ordering::SeqCst),
123        }
124    }
125}
126
127impl_option!(
128    TimerId,
129    OptionTimerId,
130    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
131);
132
133impl_vec!(TimerId, TimerIdVec, TimerIdVecDestructor, TimerIdVecDestructorType, TimerIdVecSlice, OptionTimerId);
134impl_vec_debug!(TimerId, TimerIdVec);
135impl_vec_clone!(TimerId, TimerIdVec, TimerIdVecDestructor);
136impl_vec_partialeq!(TimerId, TimerIdVec);
137impl_vec_partialord!(TimerId, TimerIdVec);
138
139// Thread IDs 0-4 are reserved for internal framework use.
140// User threads start at RESERVED_THREAD_ID_COUNT.
141const RESERVED_THREAD_ID_COUNT: usize = 5;
142static MAX_THREAD_ID: AtomicUsize = AtomicUsize::new(RESERVED_THREAD_ID_COUNT);
143
144/// ID for uniquely identifying a background thread
145#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
146#[repr(C)]
147pub struct ThreadId {
148    id: usize,
149}
150
151impl_option!(
152    ThreadId,
153    OptionThreadId,
154    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
155);
156
157impl_vec!(ThreadId, ThreadIdVec, ThreadIdVecDestructor, ThreadIdVecDestructorType, ThreadIdVecSlice, OptionThreadId);
158impl_vec_debug!(ThreadId, ThreadIdVec);
159impl_vec_clone!(ThreadId, ThreadIdVec, ThreadIdVecDestructor);
160impl_vec_partialeq!(ThreadId, ThreadIdVec);
161impl_vec_partialord!(ThreadId, ThreadIdVec);
162
163impl ThreadId {
164    /// Generates a new, unique `ThreadId`.
165    #[must_use]
166    pub fn unique() -> Self {
167        Self {
168            id: MAX_THREAD_ID.fetch_add(1, Ordering::SeqCst),
169        }
170    }
171}
172
173/// A point in time, either from the system clock or a tick counter.
174///
175/// Use `Instant::System` on platforms with std, `Instant::Tick` on `embedded/no_std`.
176#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
177#[repr(C, u8)]
178pub enum Instant {
179    /// System time from `std::time::Instant` (requires "std" feature)
180    System(InstantPtr),
181    /// Tick-based time for embedded systems without a real-time clock
182    Tick(SystemTick),
183}
184
185#[cfg(feature = "std")]
186impl From<StdInstant> for Instant {
187    fn from(s: StdInstant) -> Self {
188        Self::System(s.into())
189    }
190}
191
192#[cfg(feature = "std")]
193std::thread_local! {
194    /// Injectable test-clock offset, in milliseconds, added to every
195    /// `Instant::now()` **on this thread**.
196    ///
197    /// Driven by the E2E `tick_ms` op. Everything time-driven in the engine —
198    /// scroll momentum, scrollbar fade, cursor blink, animations, timers —
199    /// reads the clock through `Instant::now()` / `get_system_time_libstd()`,
200    /// so advancing this offset moves all of them forward by exactly N ms
201    /// WITHOUT sleeping. That is what makes "drive the animation to completion
202    /// and assert it converges" deterministic instead of a `wait { ms }` race.
203    ///
204    /// Zero in production; only the debug-server `tick_ms` op ever writes it.
205    ///
206    /// # Why this is a thread-local and not a `static AtomicU64`
207    ///
208    /// It used to be process-global, which made the clock a shared mutable
209    /// resource: every scenario that ticked had to run SERIALLY, or scenario
210    /// A's `tick_ms` would shift scenario B's animations mid-frame. Since the
211    /// corpus is dominated by idle/animation scenarios, that serialised
212    /// essentially the whole suite.
213    ///
214    /// The read path is [`GetSystemTimeCallbackType`] — a bare
215    /// `extern "C" fn() -> Instant` in the public C API — plus ~140 direct
216    /// `Instant::now()` calls. Neither can carry a window, an app or a clock
217    /// handle without either breaking the C ABI for every language binding or
218    /// threading a time source through every call site including `no_std`
219    /// ones. A thread-local is the narrowest scope a context-free C callback
220    /// can read: it turns "the whole process" into "the thread that owns this
221    /// scenario", which is exactly the ownership boundary the parallel E2E
222    /// runner already establishes (one scenario runs start-to-finish on one
223    /// worker thread). [`reset_test_clock`] makes that boundary explicit.
224    static TEST_CLOCK_OFFSET_MS: core::cell::Cell<u64> = const { core::cell::Cell::new(0) };
225}
226
227/// Advance the injectable test clock by `ms` (E2E `tick_ms`), returning the new
228/// offset. Affects only the CURRENT thread — see [`TEST_CLOCK_OFFSET_MS`].
229#[cfg(feature = "std")]
230#[must_use]
231pub fn advance_test_clock_ms(ms: u64) -> u64 {
232    TEST_CLOCK_OFFSET_MS.with(|c| {
233        let next = c.get().saturating_add(ms);
234        c.set(next);
235        next
236    })
237}
238
239/// The current test-clock offset in ms (0 unless `tick_ms` was used on this
240/// thread).
241#[cfg(feature = "std")]
242#[must_use]
243pub fn test_clock_offset_ms() -> u64 {
244    TEST_CLOCK_OFFSET_MS.with(core::cell::Cell::get)
245}
246
247#[cfg(feature = "std")]
248#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
249std::thread_local! {
250    /// When set, this thread's clock is FROZEN at this instant: `Instant::now()`
251    /// answers `base + TEST_CLOCK_OFFSET_MS` and real time does not flow into it
252    /// at all. See [`freeze_test_clock`].
253    static TEST_CLOCK_BASE: core::cell::Cell<Option<StdInstant>> =
254        const { core::cell::Cell::new(None) };
255}
256
257/// Freeze this thread's clock, so engine time advances ONLY when a scenario says
258/// it does (`tick_ms` / `wait`) and never because wall time passed.
259///
260/// Offsetting alone is not enough. `Instant::now()` was
261/// `StdInstant::now() + offset`, so the REAL component still flowed and every
262/// time-driven behaviour rode on however long the machine happened to take:
263/// elapsed = (exact virtual) + (whatever this build, under this load, spent
264/// computing). The E2E suite runs 8 scenarios per core, so that second term is
265/// both large and variable, and an assertion on a blinking caret's phase would
266/// flip between runs on a loaded runner while passing every time in isolation.
267///
268/// Frozen, engine time becomes a pure function of the ops a scenario executed —
269/// identical on a debug build, a release build and a saturated CI box. That is
270/// also what makes an off-by-one in animation timing *observable*: advance
271/// exactly one interval and the frame either flipped or it did not, with no
272/// jitter to hide behind.
273///
274/// This deliberately does NOT touch [`Instant::Tick`]. Interval constants are
275/// built as `Duration::System` (e.g. the cursor blink in `text_edit`), and
276/// `Duration::greater_than` compares only matching variants — handing the engine
277/// `Tick` elapsed values against `System` intervals would mismatch and silently
278/// answer "not yet" forever. Freezing keeps every existing comparison intact.
279///
280/// Idempotent: re-freezing an already-frozen clock keeps the original base, so
281/// the offset stays the single source of elapsed time.
282#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
283pub fn freeze_test_clock() {
284    TEST_CLOCK_BASE.with(|c| {
285        if c.get().is_none() {
286            c.set(Some(StdInstant::now()));
287        }
288    });
289}
290
291/// Whether this thread's clock is frozen (see [`freeze_test_clock`]).
292#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
293#[must_use]
294pub fn test_clock_is_frozen() -> bool {
295    TEST_CLOCK_BASE.with(core::cell::Cell::get).is_some()
296}
297
298/// Put this thread's test clock back on real time.
299///
300/// Worker threads are REUSED across scenarios, so without this the next
301/// scenario scheduled onto this thread would inherit the previous one's
302/// accumulated offset — the same cross-contamination the process-global
303/// offset had, just at thread granularity. The E2E runner calls this at the
304/// start of every scenario. Clears the freeze as well, so a scenario cannot
305/// leave the next one's clock stopped.
306#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
307pub fn reset_test_clock() {
308    TEST_CLOCK_OFFSET_MS.with(|c| c.set(0));
309    TEST_CLOCK_BASE.with(|c| c.set(None));
310}
311
312/// Monotonic frame counter, and the ONLY clock a wasm build has.
313///
314/// `std::time::Instant::now()` PANICS on wasm32-unknown-unknown, and
315/// `#[cfg(feature = "std")]` does not exclude wasm here: azul-core is built with
316/// `default = ["std"]` for the web target, so every `std` path is compiled in.
317///
318/// Answering `Tick(0)` forever would stop the panic and freeze every animation
319/// instead — a silent stall, which is worse than a loud crash. So the web build
320/// gets a real monotonic source: the browser drives redraw, each produced DOM
321/// patch is one frame, and one frame is exactly what a `t` (tick) duration
322/// counts. `AzStartup_buildPatch` calls [`advance_system_tick`] once per patch.
323#[cfg(feature = "std")]
324static SYSTEM_TICK: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
325
326/// Advance the frame counter by one. Called once per produced frame by backends
327/// that have no wall clock. Cheap enough to call unconditionally.
328#[cfg(feature = "std")]
329pub fn advance_system_tick() {
330    SYSTEM_TICK.fetch_add(1, Ordering::Relaxed);
331}
332
333/// The current frame counter.
334#[cfg(feature = "std")]
335#[must_use]
336pub fn system_tick_now() -> u64 {
337    SYSTEM_TICK.load(Ordering::Relaxed)
338}
339
340/// `std::time::Instant::now()` shifted by the injectable test-clock offset, or —
341/// when the clock is frozen — built from the frozen base so real time cannot
342/// leak in.
343///
344/// NOT COMPILED on wasm32, where `std::time::Instant::now()` panics.
345///
346/// `web_lift` is deliberately NOT included here. That backend compiles natively
347/// and is lifted to wasm afterwards, so `target_arch` reads `x86_64` — but the
348/// lift walks the LLVM graph and auto-inserts calls out to JS for things like
349/// time, so it supplies its own clock and does not want this arm disabled.
350#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
351fn std_now_with_test_offset() -> StdInstant {
352    let offset = test_clock_offset_ms();
353    if let Some(base) = TEST_CLOCK_BASE.with(core::cell::Cell::get) {
354        return base + core::time::Duration::from_millis(offset);
355    }
356    if offset == 0 {
357        StdInstant::now()
358    } else {
359        StdInstant::now() + core::time::Duration::from_millis(offset)
360    }
361}
362
363impl Instant {
364    /// Returns the current system time.
365    ///
366    /// On systems with std, this uses `std::time::Instant::now()`.
367    /// On `no_std` systems, this returns a zero tick.
368    #[cfg(all(feature = "std", not(target_arch = "wasm32")))]
369    #[must_use] pub fn now() -> Self {
370        std_now_with_test_offset().into()
371    }
372
373    /// Returns the current time on wasm32, which has no clock to read.
374    ///
375    /// `std::time::Instant::now()` panics on wasm32-unknown-unknown, and
376    /// `#[cfg(feature = "std")]` does not exclude wasm here — azul-core is built
377    /// with `default = ["std"]` for the web target, so the std path is compiled
378    /// in and would trap on the first frame.
379    ///
380    /// This deliberately does NOT answer a constant `Tick(0)`. That stops the
381    /// panic and freezes every animation instead, which is a silent stall — the
382    /// worse failure of the two. The browser drives redraw and each produced DOM
383    /// patch is one frame, so the frame counter IS the clock, and a frame is
384    /// exactly what a `t` (tick) duration counts. Elapsed values come out as
385    /// `Tick` and convert against `System` intervals through
386    /// `Duration::as_nanos`, so `60t` compares equal to one second.
387    #[cfg(all(feature = "std", target_arch = "wasm32"))]
388    #[must_use] pub fn now() -> Self {
389        Instant::Tick(SystemTick::new(system_tick_now()))
390    }
391
392    /// Returns the current system time (no_std fallback).
393    #[cfg(not(feature = "std"))]
394    pub fn now() -> Self {
395        Instant::Tick(SystemTick::new(0))
396    }
397
398    /// Returns a number from 0.0 to 1.0 indicating the current
399    /// linear interpolation value between (start, end)
400    #[must_use] pub fn linear_interpolate(&self, mut start: Self, mut end: Self) -> f32 {
401        use core::mem;
402
403        if end < start {
404            mem::swap(&mut start, &mut end);
405        }
406
407        if *self < start {
408            return 0.0;
409        }
410        if *self > end {
411            return 1.0;
412        }
413
414        // Zero-length interval: `duration_current / duration_total` would be
415        // `0/0 = NaN`. Treat a collapsed interval as fully elapsed (1.0) rather
416        // than propagating NaN into animation progress.
417        if start == end {
418            return 1.0;
419        }
420
421        let duration_total = end.duration_since(&start);
422        let duration_current = self.duration_since(&start);
423
424        let ratio = duration_current.div(&duration_total);
425        if ratio.is_nan() {
426            return 1.0;
427        }
428        ratio.clamp(0.0, 1.0)
429    }
430
431    /// Adds a duration to the instant.
432    ///
433    /// The duration's UNIT need not match the instant's: a `Tick` duration added
434    /// to a `System` instant is converted at [`TICKS_PER_SECOND`], and a `System`
435    /// duration added to a `Tick` instant is converted to whole ticks.
436    ///
437    /// # Why the mismatch is converted rather than dropped
438    ///
439    /// This used to return `self` unchanged for a unit mismatch, which turned a
440    /// `Duration::Tick` interval on a wall-clock timer into a schedule point of
441    /// `last_run + 0` — `Timer::instant_of_next_run` is literally
442    /// `last_run + delay + interval`, so the timer reported itself permanently
443    /// overdue and `LayoutWindow::time_until_next_timer_ms` answered `Some(0)`
444    /// for it, i.e. "block for zero milliseconds" to any loop that consults it.
445    ///
446    /// `System + System` still overflow-panics on an absurd duration (that is
447    /// `StdInstant`'s own behaviour, characterised in the tests); the tick arms
448    /// saturate.
449    #[must_use] pub fn add_optional_duration(&self, duration: Option<&Duration>) -> Self {
450        duration.map_or_else(|| self.clone(), |d| match (self, d) {
451                (Self::System(i), Duration::System(d)) => {
452                    #[cfg(feature = "std")]
453                    {
454                        let s: StdInstant = i.clone().into();
455                        let d: StdDuration = (*d).into();
456                        let new: InstantPtr = (s + d).into();
457                        Self::System(new)
458                    }
459                    #[cfg(not(feature = "std"))]
460                    {
461                        // A `System` instant cannot be constructed on no_std, so
462                        // this arm is unreachable in practice; return self rather
463                        // than aborting.
464                        let _ = (i, d);
465                        self.clone()
466                    }
467                }
468                (Self::Tick(s), Duration::Tick(d)) => Self::Tick(SystemTick {
469                    // Saturate so a runaway tick delta cannot overflow-panic.
470                    tick_counter: s.tick_counter.saturating_add(d.tick_diff),
471                }),
472                // System instant + Tick duration: convert the frame count to wall
473                // time. Routed through the same `System + System` arm so the
474                // overflow behaviour is identical for both units.
475                (Self::System(_), Duration::Tick(_)) => {
476                    self.add_optional_duration(Some(&Duration::System(
477                        SystemTimeDiff::from_nanos_u128(d.as_nanos()),
478                    )))
479                }
480                // Tick instant + System duration: convert to WHOLE ticks. A
481                // sub-frame duration therefore advances nothing, which is the
482                // truthful answer on a clock whose resolution is one frame.
483                (Self::Tick(s), Duration::System(_)) => Self::Tick(SystemTick {
484                    tick_counter: s.tick_counter.saturating_add(d.as_ticks()),
485                }),
486            })
487    }
488
489    /// Converts to `std::time::Instant` (panics if Tick variant).
490    #[cfg(feature = "std")]
491    #[must_use] pub fn into_std_instant(self) -> StdInstant {
492        match self {
493            Self::System(s) => s.into(),
494            Self::Tick(_) => unreachable!(),
495        }
496    }
497
498    /// Calculates the duration since an earlier point in time.
499    ///
500    /// Saturates to a zero duration in the degenerate cases (earlier is actually
501    /// *later* than `self`, or the two instants are of mismatched kinds) instead
502    /// of panicking — this runs on the hot event-loop path and must not crash.
503    #[must_use] pub fn duration_since(&self, earlier: &Self) -> Duration {
504        match (earlier, self) {
505            (Self::System(prev), Self::System(now)) => {
506                #[cfg(feature = "std")]
507                {
508                    let prev_instant: StdInstant = prev.clone().into();
509                    let now_instant: StdInstant = now.clone().into();
510                    // `saturating_duration_since` yields 0 if `prev` is later
511                    // than `now` (monotonic-clock skew / reordered instants).
512                    Duration::System(now_instant.saturating_duration_since(prev_instant).into())
513                }
514                #[cfg(not(feature = "std"))]
515                {
516                    // Unreachable on no_std (no System instants); saturate to 0.
517                    let _ = (prev, now);
518                    Duration::Tick(SystemTickDiff { tick_diff: 0 })
519                }
520            }
521            (
522                Self::Tick(SystemTick { tick_counter: prev }),
523                Self::Tick(SystemTick { tick_counter: now }),
524            ) => Duration::Tick(SystemTickDiff {
525                // Saturate: a "negative" span (prev > now) clamps to 0.
526                tick_diff: now.saturating_sub(*prev),
527            }),
528            // Mismatched kinds: no meaningful span -> saturate to 0.
529            _ => Duration::Tick(SystemTickDiff { tick_diff: 0 }),
530        }
531    }
532}
533
534/// Tick-based timestamp for systems without a real-time clock.
535///
536/// Used on embedded systems where time is measured in frame ticks or cycles.
537#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
538#[repr(C)]
539pub struct SystemTick {
540    pub tick_counter: u64,
541}
542
543impl SystemTick {
544    /// Creates a new tick timestamp from a counter value.
545    #[must_use] pub const fn new(tick_counter: u64) -> Self {
546        Self { tick_counter }
547    }
548}
549
550/// FFI-safe wrapper around `std::time::Instant` with custom clone/drop callbacks.
551///
552/// Allows crossing FFI boundaries while maintaining proper memory management.
553#[repr(C)]
554pub struct InstantPtr {
555    /// `ManuallyDrop` so the owned `Box` is freed ONLY when `run_destructor` is
556    /// still set (see `Drop`). The codegen FFI wrappers (`AzTimerCallbackInfo`
557    /// etc.) embed this by value AND have their own `Drop` that `drop_in_place`s
558    /// the real type first; Rust's drop glue would then drop this `ptr` field a
559    /// SECOND time on the same bytes. Gating the `Box` free on `run_destructor`
560    /// (cleared by the first drop) makes that second drop a safe no-op. Layout is
561    /// unchanged: `ManuallyDrop<Box<T>>` is one pointer, like the old `Box<T>`.
562    #[cfg(feature = "std")]
563    pub ptr: ManuallyDrop<Box<StdInstant>>,
564    #[cfg(not(feature = "std"))]
565    pub ptr: *const c_void,
566    pub clone_fn: InstantPtrCloneCallback,
567    pub destructor: InstantPtrDestructorCallback,
568    pub run_destructor: bool,
569}
570
571pub type InstantPtrCloneCallbackType = extern "C" fn(*const InstantPtr) -> InstantPtr;
572#[repr(C)]
573pub struct InstantPtrCloneCallback {
574    pub cb: InstantPtrCloneCallbackType,
575}
576impl_callback_simple!(InstantPtrCloneCallback);
577
578pub type InstantPtrDestructorCallbackType = extern "C" fn(*mut InstantPtr);
579#[repr(C)]
580pub struct InstantPtrDestructorCallback {
581    pub cb: InstantPtrDestructorCallbackType,
582}
583impl_callback_simple!(InstantPtrDestructorCallback);
584
585// ----  LIBSTD implementation for InstantPtr BEGIN
586#[cfg(feature = "std")]
587impl fmt::Debug for InstantPtr {
588    fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
589        write!(f, "{:?}", self.get())
590    }
591}
592
593#[cfg(not(feature = "std"))]
594impl core::fmt::Debug for InstantPtr {
595    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
596        write!(f, "{:?}", self.ptr as usize)
597    }
598}
599
600#[cfg(feature = "std")]
601impl core::hash::Hash for InstantPtr {
602    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
603        self.get().hash(state);
604    }
605}
606
607#[cfg(not(feature = "std"))]
608impl core::hash::Hash for InstantPtr {
609    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
610        (self.ptr as usize).hash(state);
611    }
612}
613
614#[cfg(feature = "std")]
615impl PartialEq for InstantPtr {
616    fn eq(&self, other: &Self) -> bool {
617        self.get() == other.get()
618    }
619}
620
621#[cfg(not(feature = "std"))]
622impl PartialEq for InstantPtr {
623    fn eq(&self, other: &InstantPtr) -> bool {
624        (self.ptr as usize).eq(&(other.ptr as usize))
625    }
626}
627
628impl Eq for InstantPtr {}
629
630#[cfg(feature = "std")]
631impl PartialOrd for InstantPtr {
632    fn partial_cmp(&self, other: &Self) -> Option<::core::cmp::Ordering> {
633        Some((self.get()).cmp(&(other.get())))
634    }
635}
636
637#[cfg(not(feature = "std"))]
638impl PartialOrd for InstantPtr {
639    fn partial_cmp(&self, other: &Self) -> Option<::core::cmp::Ordering> {
640        Some((self.ptr as usize).cmp(&(other.ptr as usize)))
641    }
642}
643
644#[cfg(feature = "std")]
645impl Ord for InstantPtr {
646    fn cmp(&self, other: &Self) -> ::core::cmp::Ordering {
647        (self.get()).cmp(&(other.get()))
648    }
649}
650
651#[cfg(not(feature = "std"))]
652impl Ord for InstantPtr {
653    fn cmp(&self, other: &Self) -> ::core::cmp::Ordering {
654        (self.ptr as usize).cmp(&(other.ptr as usize))
655    }
656}
657
658#[cfg(feature = "std")]
659impl InstantPtr {
660    fn get(&self) -> StdInstant {
661        (**self.ptr)
662    }
663}
664
665impl Clone for InstantPtr {
666    fn clone(&self) -> Self {
667        (self.clone_fn.cb)(self)
668    }
669}
670
671#[cfg(feature = "std")]
672extern "C" fn std_instant_clone(ptr: *const InstantPtr) -> InstantPtr {
673    let az_instant_ptr = unsafe { &*ptr };
674    InstantPtr {
675        ptr: ManuallyDrop::new((*az_instant_ptr.ptr).clone()),
676        clone_fn: az_instant_ptr.clone_fn,
677        destructor: az_instant_ptr.destructor,
678        run_destructor: true,
679    }
680}
681
682#[cfg(feature = "std")]
683impl From<StdInstant> for InstantPtr {
684    fn from(s: StdInstant) -> Self {
685        Self {
686            ptr: ManuallyDrop::new(Box::new(s)),
687            clone_fn: InstantPtrCloneCallback {
688                cb: std_instant_clone,
689            },
690            destructor: InstantPtrDestructorCallback {
691                cb: std_instant_drop,
692            },
693            run_destructor: true,
694        }
695    }
696}
697
698#[cfg(feature = "std")]
699impl From<InstantPtr> for StdInstant {
700    fn from(s: InstantPtr) -> Self {
701        s.get()
702    }
703}
704
705impl Drop for InstantPtr {
706    fn drop(&mut self) {
707        if self.run_destructor {
708            self.run_destructor = false;
709            (self.destructor.cb)(self);
710            // Free the owned Box exactly once, here under the run_destructor guard.
711            // A second drop on the same bytes (the codegen wrapper's field-drop after
712            // its own `_delete` already ran the real drop) sees run_destructor=false
713            // and skips this -> no double-free. (non-std `ptr` is a raw POD pointer
714            // freed by the destructor callback above, so nothing to drop here.)
715            // SAFETY: `run_destructor` is set false above, so this arm runs at
716            // most once per InstantPtr value; the `Box` inside was never moved
717            // out, so it is live and owned here and safe to drop exactly once.
718            #[cfg(feature = "std")]
719            unsafe {
720                ManuallyDrop::drop(&mut self.ptr);
721            }
722        }
723    }
724}
725
726#[cfg(feature = "std")]
727const extern "C" fn std_instant_drop(_: *mut InstantPtr) {}
728
729// ----  LIBSTD implementation for InstantPtr END
730
731/// A span of time, either from the system clock or as tick difference.
732///
733/// Mirrors `Instant` variants - System durations work with System instants,
734/// Tick durations work with Tick instants.
735#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
736#[repr(C, u8)]
737pub enum Duration {
738    /// System duration from `std::time::Duration` (requires "std" feature)
739    System(SystemTimeDiff),
740    /// Tick-based duration for embedded systems
741    Tick(SystemTickDiff),
742}
743
744impl fmt::Display for Duration {
745    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
746        match self {
747            #[cfg(feature = "std")]
748            Self::System(s) => {
749                let s: StdDuration = (*s).into();
750                write!(f, "{s:?}")
751            }
752            #[cfg(not(feature = "std"))]
753            Duration::System(s) => write!(f, "({}s, {}ns)", s.secs, s.nanos),
754            Self::Tick(tick) => write!(f, "{} ticks", tick.tick_diff),
755        }
756    }
757}
758
759#[cfg(feature = "std")]
760impl From<StdDuration> for Duration {
761    fn from(s: StdDuration) -> Self {
762        Self::System(s.into())
763    }
764}
765
766/// Nominal engine tick (frame) rate — the single exchange rate between
767/// [`Duration::Tick`] (frames) and [`Duration::System`] (wall time).
768///
769/// Re-exported from `azul-css` so the CSS `t` unit and the engine's `Duration`
770/// arithmetic cannot drift apart. See [`azul_css::props::basic::time::TICKS_PER_SECOND`].
771pub use azul_css::props::basic::time::TICKS_PER_SECOND;
772
773impl Duration {
774    /// This duration on ONE canonical scale, in nanoseconds — the common ground
775    /// on which a `Tick` span and a `System` span can be compared.
776    ///
777    /// `u128` because a `System` duration holds up to `u64::MAX` *seconds*
778    /// (~1.8e28 ns), which does not fit `u64`. The tick conversion multiplies
779    /// before it divides so whole seconds stay exact: `60t` is `1_000_000_000`ns,
780    /// not `60 * 16_666_666 = 999_999_960`ns.
781    ///
782    /// Note this also normalises a DENORMALISED `SystemTimeDiff` (`nanos` past
783    /// `1e9`) the same way `std::time::Duration::new` would — except it cannot
784    /// panic on overflow while doing it.
785    // `as u128` rather than `u128::from`: this is a `const fn` and `From` is not
786    // const. Every one of these widenings is lossless.
787    #[allow(clippy::cast_lossless)]
788    #[must_use]
789    pub const fn as_nanos(&self) -> u128 {
790        match self {
791            Self::System(s) => (s.secs as u128) * (NANOS_PER_SEC as u128) + (s.nanos as u128),
792            Self::Tick(t) => (t.tick_diff as u128) * (NANOS_PER_SEC as u128) / (TICKS_PER_SECOND as u128),
793        }
794    }
795
796    /// A wall-clock duration of `ms` whole milliseconds.
797    #[must_use]
798    pub const fn from_millis(ms: u64) -> Self {
799        Self::System(SystemTimeDiff::from_millis(ms))
800    }
801
802    /// A duration of `ticks` engine frames — the clockless unit, and what the
803    /// CSS `t` unit becomes.
804    #[must_use]
805    pub const fn from_ticks(ticks: u64) -> Self {
806        Self::Tick(SystemTickDiff { tick_diff: ticks })
807    }
808
809    /// This duration in whole ticks (frames), truncating toward zero.
810    ///
811    /// A sub-frame span is **zero** ticks, not one: "how many whole frames fit",
812    /// never "round up so that something happens".
813    // `as` casts: `const fn`, so `From`/`TryFrom` are unavailable. The widenings
814    // are lossless and the u128 -> u64 narrowing is range-checked immediately
815    // above it.
816    #[allow(clippy::cast_lossless, clippy::cast_possible_truncation)]
817    #[must_use]
818    pub const fn as_ticks(&self) -> u64 {
819        match self {
820            Self::Tick(t) => t.tick_diff,
821            Self::System(_) => {
822                let ticks = self.as_nanos() * (TICKS_PER_SECOND as u128) / (NANOS_PER_SEC as u128);
823                if ticks > u64::MAX as u128 {
824                    u64::MAX
825                } else {
826                    ticks as u64
827                }
828            }
829        }
830    }
831
832    /// This duration in whole milliseconds, truncating toward zero and
833    /// saturating at `u64::MAX` rather than wrapping.
834    // `as` casts: `const fn`, so `From`/`TryFrom` are unavailable. The u128 ->
835    // u64 narrowing is range-checked immediately above it.
836    #[allow(clippy::cast_lossless, clippy::cast_possible_truncation)]
837    #[must_use]
838    pub const fn as_millis_u64(&self) -> u64 {
839        let ms = self.as_nanos() / (NANOS_PER_MILLI as u128);
840        if ms > u64::MAX as u128 {
841            u64::MAX
842        } else {
843            ms as u64
844        }
845    }
846
847    /// Returns the maximum possible duration.
848    #[must_use] pub fn max() -> Self {
849        #[cfg(feature = "std")]
850        {
851            Self::System(StdDuration::new(core::u64::MAX, NANOS_PER_SEC - 1).into())
852        }
853        #[cfg(not(feature = "std"))]
854        {
855            Duration::Tick(SystemTickDiff {
856                tick_diff: u64::MAX,
857            })
858        }
859    }
860
861    /// Divides this duration by another, returning the ratio as f32.
862    ///
863    /// Same-unit division goes through the unit's own `div` so its exact
864    /// floating-point result is unchanged. Cross-unit division falls back to the
865    /// canonical nanosecond scale rather than returning `0.0` — a `0.0` ratio
866    /// here means "animation is at 0% progress", which is a frozen animation, not
867    /// an error anyone would notice.
868    // the f64 ratio is intentionally narrowed to the f32 return type; the value
869    // is a duration ratio, far inside f32's range.
870    #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
871    #[must_use] pub fn div(&self, other: &Self) -> f32 {
872        use self::Duration::{System, Tick};
873        match (self, other) {
874            (System(s), System(s2)) => s.div(s2) as f32,
875            (Tick(t), Tick(t2)) => t.div(t2) as f32,
876            // u128 -> f64 loses precision only past 2^53 ns (~104 days), and the
877            // result is a ratio that is then narrowed to f32 anyway.
878            _ => (self.as_nanos() as f64 / other.as_nanos() as f64) as f32,
879        }
880    }
881
882    /// Returns the smaller of two durations.
883    #[must_use] pub const fn min(self, other: Self) -> Self {
884        if self.smaller_than(&other) {
885            self
886        } else {
887            other
888        }
889    }
890
891    /// Returns true if self > other.
892    ///
893    /// Compares on the canonical nanosecond scale ([`Self::as_nanos`]), so a
894    /// `Tick` span and a `System` span compare TRUTHFULLY against each other.
895    ///
896    /// # Why this is not "mismatched kinds saturate to false"
897    ///
898    /// It used to be. That made a unit mismatch invisible and permanent instead
899    /// of loud: the engine's interval constants are `Duration::System` (the
900    /// cursor blink, the scrollbar fade, the tooltip delay), so the moment a
901    /// clock produced `Tick` elapsed values every one of those comparisons
902    /// answered "not yet" — forever. Nothing panicked, nothing logged, the UI
903    /// simply stopped animating. That is precisely the failure a clockless unit
904    /// is supposed to make *catchable*, so the comparison has to be total.
905    ///
906    /// Three behaviours changed, all in the safe direction:
907    ///
908    /// 1. Cross-unit comparisons now answer, instead of always `false`.
909    /// 2. On `no_std` the `System`/`System` arm used to be hardcoded `false`
910    ///    (there was no `StdDuration` to defer to); it now compares properly.
911    /// 3. A denormalised `SystemTimeDiff` whose `secs + nanos/1e9` overflows
912    ///    `u64` used to panic inside `StdDuration::new`; `u128` nanoseconds
913    ///    cannot overflow.
914    #[must_use] pub const fn greater_than(&self, other: &Self) -> bool {
915        self.as_nanos() > other.as_nanos()
916    }
917
918    /// Returns true if self < other.
919    ///
920    /// Canonical-scale comparison; see [`Self::greater_than`] for why this is
921    /// unit-aware rather than saturating to `false` on a unit mismatch.
922    #[must_use] pub const fn smaller_than(&self, other: &Self) -> bool {
923        self.as_nanos() < other.as_nanos()
924    }
925}
926
927/// Represents a difference in ticks for systems that
928/// don't support timing
929#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
930#[repr(C)]
931pub struct SystemTickDiff {
932    pub tick_diff: u64,
933}
934
935impl SystemTickDiff {
936    /// Divide duration A by duration B.
937    /// Returns `Inf` or `NaN` if `other` is zero.
938    // tick counts -> f64 for the ratio; precision only degrades past 2^53 ticks.
939    #[allow(clippy::cast_precision_loss)]
940    #[must_use] pub fn div(&self, other: &Self) -> f64 {
941        self.tick_diff as f64 / other.tick_diff as f64
942    }
943}
944
945/// Duration represented as seconds + nanoseconds (mirrors `std::time::Duration`).
946#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
947#[repr(C)]
948pub struct SystemTimeDiff {
949    pub secs: u64,
950    pub nanos: u32,
951}
952
953impl SystemTimeDiff {
954    /// Divide duration A by duration B.
955    /// Returns `Inf` or `NaN` if `other` is zero.
956    #[must_use] pub fn div(&self, other: &Self) -> f64 {
957        self.as_secs_f64() / other.as_secs_f64()
958    }
959    // secs (u64) -> f64 loses precision only past 2^53 seconds (~285M years).
960    #[allow(clippy::cast_precision_loss)]
961    fn as_secs_f64(&self) -> f64 {
962        (self.secs as f64) + (f64::from(self.nanos) / f64::from(NANOS_PER_SEC))
963    }
964}
965
966#[cfg(feature = "std")]
967impl From<StdDuration> for SystemTimeDiff {
968    fn from(d: StdDuration) -> Self {
969        Self {
970            secs: d.as_secs(),
971            nanos: d.subsec_nanos(),
972        }
973    }
974}
975
976#[cfg(feature = "std")]
977impl From<SystemTimeDiff> for StdDuration {
978    fn from(d: SystemTimeDiff) -> Self {
979        Self::new(d.secs, d.nanos)
980    }
981}
982
983const MILLIS_PER_SEC: u64 = 1_000;
984const NANOS_PER_MILLI: u32 = 1_000_000;
985const NANOS_PER_SEC: u32 = 1_000_000_000;
986
987impl SystemTimeDiff {
988    /// Creates a duration from whole seconds.
989    #[must_use] pub const fn from_secs(secs: u64) -> Self {
990        Self { secs, nanos: 0 }
991    }
992    /// Creates a duration from milliseconds.
993    #[must_use] pub const fn from_millis(millis: u64) -> Self {
994        Self {
995            secs: millis / MILLIS_PER_SEC,
996            nanos: ((millis % MILLIS_PER_SEC) as u32) * NANOS_PER_MILLI,
997        }
998    }
999    /// Creates a duration from nanoseconds.
1000    // const fn (no const TryFrom); `nanos % NANOS_PER_SEC` is always < 10^9, which
1001    // fits u32, so the narrowing cast cannot truncate.
1002    #[allow(clippy::cast_possible_truncation)]
1003    #[must_use] pub const fn from_nanos(nanos: u64) -> Self {
1004        Self {
1005            secs: nanos / (NANOS_PER_SEC as u64),
1006            nanos: (nanos % (NANOS_PER_SEC as u64)) as u32,
1007        }
1008    }
1009
1010    /// Creates a duration from a `u128` nanosecond count, saturating at the
1011    /// largest representable duration instead of wrapping.
1012    ///
1013    /// Needed because [`Duration::as_nanos`] is `u128`: a tick count near
1014    /// `u64::MAX` converts to ~3e26 ns, far past what `u64` nanoseconds hold.
1015    // `nanos % NANOS_PER_SEC` is always < 10^9 and `secs` is range-checked above,
1016    // so neither narrowing cast can truncate. `as` widenings rather than `From`
1017    // because this is a `const fn`.
1018    #[allow(clippy::cast_possible_truncation, clippy::cast_lossless)]
1019    #[must_use] pub const fn from_nanos_u128(nanos: u128) -> Self {
1020        let secs = nanos / (NANOS_PER_SEC as u128);
1021        if secs > u64::MAX as u128 {
1022            Self {
1023                secs: u64::MAX,
1024                nanos: NANOS_PER_SEC - 1,
1025            }
1026        } else {
1027            Self {
1028                secs: secs as u64,
1029                nanos: (nanos % (NANOS_PER_SEC as u128)) as u32,
1030            }
1031        }
1032    }
1033    /// Adds two durations, returning None on overflow.
1034    #[must_use] pub const fn checked_add(self, rhs: Self) -> Option<Self> {
1035        if let Some(mut secs) = self.secs.checked_add(rhs.secs) {
1036            let mut nanos = self.nanos + rhs.nanos;
1037            if nanos >= NANOS_PER_SEC {
1038                nanos -= NANOS_PER_SEC;
1039                if let Some(new_secs) = secs.checked_add(1) {
1040                    secs = new_secs;
1041                } else {
1042                    return None;
1043                }
1044            }
1045            Some(Self { secs, nanos })
1046        } else {
1047            None
1048        }
1049    }
1050
1051    /// Returns the total duration in milliseconds.
1052    ///
1053    /// Saturates at `u64::MAX` instead of overflow-panicking for enormous
1054    /// `secs` values (`secs * 1000` overflows around ~1.8e16 seconds).
1055    #[must_use] pub const fn millis(&self) -> u64 {
1056        self.secs
1057            .saturating_mul(MILLIS_PER_SEC)
1058            .saturating_add((self.nanos / NANOS_PER_MILLI) as u64)
1059    }
1060
1061    /// Converts to `std::time::Duration`.
1062    #[cfg(feature = "std")]
1063    #[must_use] pub fn get(&self) -> StdDuration {
1064        (*self).into()
1065    }
1066}
1067
1068/// Bridge from the CSS-level duration to the engine-level one, preserving the
1069/// unit.
1070///
1071/// This is the join that makes a CSS `5t` mean five FRAMES all the way down to
1072/// the timer: `ms`/`s` become `Duration::System`, `t` becomes `Duration::Tick`.
1073/// Collapsing ticks to milliseconds here would put the wall clock back in the
1074/// path and make "advance exactly 5 ticks, assert the 5th frame flipped"
1075/// untestable again.
1076impl From<azul_css::props::basic::time::CssDuration> for Duration {
1077    fn from(d: azul_css::props::basic::time::CssDuration) -> Self {
1078        use azul_css::props::basic::time::CssDurationUnit;
1079        match d.unit {
1080            CssDurationUnit::Milliseconds => Self::from_millis(u64::from(d.inner)),
1081            CssDurationUnit::Ticks => Self::from_ticks(u64::from(d.inner)),
1082        }
1083    }
1084}
1085
1086impl_option!(
1087    Instant,
1088    OptionInstant,
1089    copy = false,
1090    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
1091);
1092impl_option!(
1093    Duration,
1094    OptionDuration,
1095    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
1096);
1097#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
1098/// Message that can be sent from the main thread to the Thread using the `ThreadId`.
1099///
1100/// The thread can ignore the event.
1101#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
1102#[repr(C, u8)]
1103pub enum ThreadSendMsg {
1104    /// The thread should terminate at the nearest
1105    TerminateThread,
1106    /// Next frame tick
1107    Tick,
1108    /// Custom data
1109    Custom(RefAny),
1110}
1111
1112impl_option!(
1113    ThreadSendMsg,
1114    OptionThreadSendMsg,
1115    copy = false,
1116    [Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash]
1117);
1118
1119/// Channel endpoint for receiving messages from the main thread in a background thread.
1120///
1121/// Thread-safe wrapper around the receiver end of a message channel.
1122#[derive(Debug)]
1123#[repr(C)]
1124pub struct ThreadReceiver {
1125    #[cfg(feature = "std")]
1126    pub ptr: Box<Arc<Mutex<ThreadReceiverInner>>>,
1127    #[cfg(not(feature = "std"))]
1128    pub ptr: *const c_void,
1129    pub run_destructor: bool,
1130    /// For FFI: stores the foreign callable (e.g., `PyFunction`)
1131    pub ctx: OptionRefAny,
1132}
1133
1134impl Clone for ThreadReceiver {
1135    fn clone(&self) -> Self {
1136        Self {
1137            ptr: self.ptr.clone(),
1138            run_destructor: true,
1139            ctx: self.ctx.clone(),
1140        }
1141    }
1142}
1143
1144impl Drop for ThreadReceiver {
1145    fn drop(&mut self) {
1146        self.run_destructor = false;
1147    }
1148}
1149
1150impl ThreadReceiver {
1151    /// Creates a new receiver (no-op on no_std).
1152    #[cfg(not(feature = "std"))]
1153    pub fn new(_t: ThreadReceiverInner) -> Self {
1154        Self {
1155            ptr: core::ptr::null(),
1156            run_destructor: false,
1157            ctx: OptionRefAny::None,
1158        }
1159    }
1160
1161    /// Creates a new receiver wrapping the inner channel.
1162    #[cfg(feature = "std")]
1163    #[must_use] pub fn new(t: ThreadReceiverInner) -> Self {
1164        Self {
1165            ptr: Box::new(Arc::new(Mutex::new(t))),
1166            run_destructor: true,
1167            ctx: OptionRefAny::None,
1168        }
1169    }
1170
1171    /// Get the FFI context (e.g., Python callable)
1172    #[must_use] pub fn get_ctx(&self) -> OptionRefAny {
1173        self.ctx.clone()
1174    }
1175
1176    /// Receives a message (returns None on no_std).
1177    #[cfg(not(feature = "std"))]
1178    pub fn recv(&mut self) -> OptionThreadSendMsg {
1179        None.into()
1180    }
1181
1182    /// Receives a message from the main thread, if available.
1183    #[cfg(feature = "std")]
1184    pub fn recv(&mut self) -> OptionThreadSendMsg {
1185        let Some(ts) = self.ptr.lock().ok() else {
1186            return None.into();
1187        };
1188        (ts.recv_fn.cb)(std::ptr::from_ref(ts.ptr.as_ref()) as *const c_void)
1189    }
1190}
1191
1192/// Inner receiver state containing the actual channel and callbacks.
1193#[derive(Debug)]
1194#[cfg_attr(not(feature = "std"), derive(PartialEq, PartialOrd, Eq, Ord))]
1195#[repr(C)]
1196pub struct ThreadReceiverInner {
1197    #[cfg(feature = "std")]
1198    pub ptr: Box<Receiver<ThreadSendMsg>>,
1199    #[cfg(not(feature = "std"))]
1200    pub ptr: *const c_void,
1201    pub recv_fn: ThreadRecvCallback,
1202    pub destructor: ThreadReceiverDestructorCallback,
1203}
1204
1205#[cfg(not(feature = "std"))]
1206unsafe impl Send for ThreadReceiverInner {}
1207
1208#[cfg(feature = "std")]
1209impl core::hash::Hash for ThreadReceiverInner {
1210    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
1211        (std::ptr::from_ref(self.ptr.as_ref()) as usize).hash(state);
1212    }
1213}
1214
1215#[cfg(feature = "std")]
1216impl PartialEq for ThreadReceiverInner {
1217    fn eq(&self, other: &Self) -> bool {
1218        std::ptr::eq(self.ptr.as_ref(), other.ptr.as_ref())
1219    }
1220}
1221
1222#[cfg(feature = "std")]
1223impl Eq for ThreadReceiverInner {}
1224
1225#[cfg(feature = "std")]
1226impl PartialOrd for ThreadReceiverInner {
1227    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
1228        Some(
1229            (std::ptr::from_ref(self.ptr.as_ref()) as usize)
1230                .cmp(&(std::ptr::from_ref(other.ptr.as_ref()) as usize)),
1231        )
1232    }
1233}
1234
1235#[cfg(feature = "std")]
1236impl Ord for ThreadReceiverInner {
1237    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
1238        (std::ptr::from_ref(self.ptr.as_ref()) as usize).cmp(&(std::ptr::from_ref(other.ptr.as_ref()) as usize))
1239    }
1240}
1241
1242impl Drop for ThreadReceiverInner {
1243    fn drop(&mut self) {
1244        (self.destructor.cb)(self);
1245    }
1246}
1247
1248/// Get the current system type, equivalent to `std::time::Instant::now()`, except it
1249/// also works on systems that don't have a clock (such as embedded timers)
1250pub type GetSystemTimeCallbackType = extern "C" fn() -> Instant;
1251#[repr(C)]
1252pub struct GetSystemTimeCallback {
1253    pub cb: GetSystemTimeCallbackType,
1254}
1255impl_callback_simple!(GetSystemTimeCallback);
1256
1257/// Default implementation that gets the current system time.
1258///
1259/// On WASM targets `std::time::Instant::now()` panics, so we fall back to
1260/// a zero-tick instant instead.
1261#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
1262#[must_use] pub extern "C" fn get_system_time_libstd() -> Instant {
1263    // Honours the injectable E2E test clock (see TEST_CLOCK_OFFSET_MS).
1264    std_now_with_test_offset().into()
1265}
1266
1267/// Fallback for WASM (where `Instant::now()` panics) and no-std targets.
1268#[cfg(any(not(feature = "std"), target_arch = "wasm32"))]
1269pub extern "C" fn get_system_time_libstd() -> Instant {
1270    Instant::Tick(SystemTick::new(0))
1271}
1272
1273/// Callback to check if a thread has finished execution.
1274pub type CheckThreadFinishedCallbackType =
1275    extern "C" fn(/* dropcheck */ *const c_void) -> bool;
1276/// Wrapper for thread completion check callback.
1277#[repr(C)]
1278pub struct CheckThreadFinishedCallback {
1279    pub cb: CheckThreadFinishedCallbackType,
1280}
1281impl_callback_simple!(CheckThreadFinishedCallback);
1282
1283/// Callback to send a message to a background thread.
1284pub type LibrarySendThreadMsgCallbackType =
1285    extern "C" fn(/* Sender<ThreadSendMsg> */ *const c_void, ThreadSendMsg) -> bool;
1286/// Wrapper for thread message send callback.
1287#[repr(C)]
1288pub struct LibrarySendThreadMsgCallback {
1289    pub cb: LibrarySendThreadMsgCallbackType,
1290}
1291impl_callback_simple!(LibrarySendThreadMsgCallback);
1292
1293/// Callback for a running thread to receive messages from the main thread.
1294pub type ThreadRecvCallbackType =
1295    extern "C" fn(/* receiver.ptr */ *const c_void) -> OptionThreadSendMsg;
1296/// Wrapper for thread message receive callback.
1297#[repr(C)]
1298pub struct ThreadRecvCallback {
1299    pub cb: ThreadRecvCallbackType,
1300}
1301impl_callback_simple!(ThreadRecvCallback);
1302
1303/// Callback to destroy a `ThreadReceiver`.
1304pub type ThreadReceiverDestructorCallbackType = extern "C" fn(*mut ThreadReceiverInner);
1305/// Wrapper for thread receiver destructor callback.
1306#[repr(C)]
1307pub struct ThreadReceiverDestructorCallback {
1308    pub cb: ThreadReceiverDestructorCallbackType,
1309}
1310impl_callback_simple!(ThreadReceiverDestructorCallback);
1311
1312#[cfg(test)]
1313#[allow(clippy::float_cmp)] // exact-value assertions on interpolation results
1314mod tests {
1315    use super::*;
1316
1317    fn tick(n: u64) -> Instant {
1318        Instant::Tick(SystemTick::new(n))
1319    }
1320    fn tick_dur(n: u64) -> Duration {
1321        Duration::Tick(SystemTickDiff { tick_diff: n })
1322    }
1323    fn sys_dur(secs: u64, nanos: u32) -> Duration {
1324        Duration::System(SystemTimeDiff { secs, nanos })
1325    }
1326
1327    /// The property the parallel E2E runner depends on: a `tick_ms` on one
1328    /// thread must not shift any other thread's clock. This is what allows a
1329    /// scenario that ticks to run in parallel with every other scenario
1330    /// instead of being serialised behind a process-global offset.
1331    #[test]
1332    #[cfg(feature = "std")]
1333    fn test_clock_offset_is_per_thread_not_process_global() {
1334        reset_test_clock();
1335        assert_eq!(test_clock_offset_ms(), 0);
1336
1337        let (tx, rx) = std::sync::mpsc::channel();
1338        let (go_tx, go_rx) = std::sync::mpsc::channel::<()>();
1339        let other = std::thread::spawn(move || {
1340            // Observed AFTER the main thread has advanced its own clock by 5 s.
1341            go_rx.recv().expect("handshake");
1342            let seen_after_main_ticked = test_clock_offset_ms();
1343            let _ = advance_test_clock_ms(7);
1344            tx.send((seen_after_main_ticked, test_clock_offset_ms()))
1345                .expect("send");
1346        });
1347
1348        assert_eq!(advance_test_clock_ms(5_000), 5_000);
1349        go_tx.send(()).expect("handshake");
1350        let (other_before, other_after) = rx.recv().expect("recv");
1351        other.join().expect("join");
1352
1353        assert_eq!(
1354            other_before, 0,
1355            "a tick on the main thread leaked into another thread's clock"
1356        );
1357        assert_eq!(other_after, 7, "the other thread must own its own offset");
1358        assert_eq!(
1359            test_clock_offset_ms(),
1360            5_000,
1361            "another thread's tick leaked into the main thread's clock"
1362        );
1363
1364        // And a reset really is a reset (worker threads are reused).
1365        reset_test_clock();
1366        assert_eq!(test_clock_offset_ms(), 0);
1367    }
1368
1369    /// A frozen clock must advance ONLY by what a scenario asks for.
1370    ///
1371    /// Offsetting alone left `Instant::now()` as `StdInstant::now() + offset`, so
1372    /// real time still flowed: elapsed time was what the scenario asked for PLUS
1373    /// whatever the machine happened to spend. Under the 8-wide E2E runner that
1374    /// second term is large and varies per run — enough to flip an assertion on a
1375    /// blinking caret's phase while the same scenario passes in isolation.
1376    #[test]
1377    #[cfg(feature = "std")]
1378    fn a_frozen_clock_advances_only_by_what_the_scenario_asks_for() {
1379        reset_test_clock();
1380        assert!(!test_clock_is_frozen());
1381
1382        freeze_test_clock();
1383        assert!(test_clock_is_frozen());
1384
1385        let t0 = Instant::now();
1386        // Burn REAL time. A frozen clock must not notice.
1387        std::thread::sleep(core::time::Duration::from_millis(25));
1388        let t1 = Instant::now();
1389        assert_eq!(
1390            t1.duration_since(&t0),
1391            Duration::System(SystemTimeDiff { secs: 0, nanos: 0 }),
1392            "real time leaked into a frozen clock",
1393        );
1394
1395        // Only an explicit advance moves it, and by exactly that much.
1396        let _ = advance_test_clock_ms(500);
1397        let t2 = Instant::now();
1398        assert_eq!(
1399            t2.duration_since(&t0),
1400            Duration::System(SystemTimeDiff { secs: 0, nanos: 500_000_000 }),
1401            "a 500 ms tick must read back as exactly 500 ms",
1402        );
1403
1404        // Freezing is idempotent: it must not re-base and lose the offset.
1405        freeze_test_clock();
1406        assert_eq!(
1407            Instant::now().duration_since(&t0),
1408            Duration::System(SystemTimeDiff { secs: 0, nanos: 500_000_000 }),
1409            "re-freezing re-based the clock and discarded elapsed virtual time",
1410        );
1411
1412        // A reset must unfreeze, or the next scenario on this reused worker
1413        // thread would start with time stopped.
1414        reset_test_clock();
1415        assert!(!test_clock_is_frozen());
1416        let r0 = Instant::now();
1417        std::thread::sleep(core::time::Duration::from_millis(15));
1418        assert!(
1419            Instant::now().duration_since(&r0)
1420                > Duration::System(SystemTimeDiff { secs: 0, nanos: 0 }),
1421            "reset_test_clock left the clock frozen",
1422        );
1423    }
1424
1425    #[test]
1426    fn linear_interpolate_zero_interval_is_one_not_nan() {
1427        let t = tick(5);
1428        let v = t.linear_interpolate(tick(5), tick(5));
1429        assert!(v.is_finite());
1430        assert_eq!(v, 1.0);
1431    }
1432
1433    #[test]
1434    fn linear_interpolate_midpoint() {
1435        let v = tick(5).linear_interpolate(tick(0), tick(10));
1436        assert!((v - 0.5).abs() < 1e-6);
1437    }
1438
1439    #[test]
1440    fn duration_since_saturates_on_negative() {
1441        // earlier is actually later -> saturate to zero, no panic.
1442        let d = tick(1).duration_since(&tick(10));
1443        assert_eq!(d, tick_dur(0));
1444    }
1445
1446    /// Cross-unit comparison must answer TRUTHFULLY, not saturate to `false`.
1447    ///
1448    /// The saturating version made a unit mismatch a permanent silent "not yet":
1449    /// every interval constant in the engine is a `Duration::System`, so a Tick
1450    /// elapsed value compared against one never expired and the UI simply stopped
1451    /// animating, with nothing to catch.
1452    #[test]
1453    fn duration_compare_is_unit_aware_across_ticks_and_wall_clock() {
1454        // 5 ticks at 60Hz is ~83ms, i.e. LESS than one second.
1455        let five_ticks = tick_dur(5);
1456        let one_second = sys_dur(1, 0);
1457        assert!(five_ticks.smaller_than(&one_second));
1458        assert!(!five_ticks.greater_than(&one_second));
1459        assert!(one_second.greater_than(&five_ticks));
1460        assert!(!one_second.smaller_than(&five_ticks));
1461
1462        // 120 ticks is two seconds, i.e. MORE than one second.
1463        assert!(tick_dur(120).greater_than(&one_second));
1464        assert!(one_second.smaller_than(&tick_dur(120)));
1465
1466        // Exactly 60 ticks IS one second: neither greater nor smaller.
1467        assert!(!tick_dur(60).greater_than(&one_second));
1468        assert!(!tick_dur(60).smaller_than(&one_second));
1469        assert!(!one_second.greater_than(&tick_dur(60)));
1470        assert!(!one_second.smaller_than(&tick_dur(60)));
1471    }
1472
1473    /// `Duration::max()` is `System`, and it must still dominate every tick count
1474    /// — including `u64::MAX` ticks, which is a bigger *number* but a smaller
1475    /// span.
1476    #[test]
1477    fn duration_compare_across_units_at_the_extremes() {
1478        assert!(Duration::max().greater_than(&tick_dur(u64::MAX)));
1479        assert!(tick_dur(u64::MAX).smaller_than(&Duration::max()));
1480        assert!(!tick_dur(0).greater_than(&sys_dur(0, 0)));
1481        assert!(!sys_dur(0, 0).greater_than(&tick_dur(0)));
1482        // One nanosecond beats zero ticks.
1483        assert!(sys_dur(0, 1).greater_than(&tick_dur(0)));
1484    }
1485
1486    #[test]
1487    fn add_optional_duration_converts_across_units() {
1488        let inst = tick(100);
1489        // A System duration on a Tick instant advances by WHOLE ticks: 1s = 60.
1490        assert_eq!(inst.add_optional_duration(Some(&sys_dur(1, 0))), tick(160));
1491        // Sub-frame durations advance nothing — one frame is the resolution.
1492        assert_eq!(inst.add_optional_duration(Some(&sys_dur(0, 1))), tick(100));
1493        // Matching kinds add and saturate.
1494        assert_eq!(inst.add_optional_duration(Some(&tick_dur(5))), tick(105));
1495        // Saturating add: near-max tick doesn't overflow-panic.
1496        let big = tick(u64::MAX);
1497        assert_eq!(big.add_optional_duration(Some(&tick_dur(10))), tick(u64::MAX));
1498        // ...and the cross-unit arm saturates too.
1499        assert_eq!(big.add_optional_duration(Some(&Duration::max())), tick(u64::MAX));
1500    }
1501
1502    #[test]
1503    fn millis_saturates_on_overflow() {
1504        let huge = SystemTimeDiff { secs: u64::MAX, nanos: 0 };
1505        assert_eq!(huge.millis(), u64::MAX);
1506        let normal = SystemTimeDiff { secs: 2, nanos: 500_000_000 };
1507        assert_eq!(normal.millis(), 2500);
1508    }
1509
1510    /// Cross-unit division goes through the canonical scale. Returning `0.0` (the
1511    /// old behaviour) reads downstream as "this animation is at 0% progress",
1512    /// i.e. a frozen animation that never reports an error.
1513    #[test]
1514    fn duration_div_is_unit_aware() {
1515        // 30 ticks is half a second.
1516        assert!((tick_dur(30).div(&sys_dur(1, 0)) - 0.5).abs() < 1e-6);
1517        // ...and one second is two lots of 30 ticks.
1518        assert!((sys_dur(1, 0).div(&tick_dur(30)) - 2.0).abs() < 1e-6);
1519        // Matching Tick kinds divide normally.
1520        assert!((tick_dur(5).div(&tick_dur(10)) - 0.5).abs() < 1e-6);
1521        // Matching System kinds divide normally.
1522        assert!((sys_dur(1, 0).div(&sys_dur(2, 0)) - 0.5).abs() < 1e-6);
1523    }
1524
1525    // Exercises the `unsafe` pointer work in `std_instant_clone` (`&*ptr`) and
1526    // the `ManuallyDrop::drop` guard in `InstantPtr::drop`: build an InstantPtr,
1527    // clone it (goes through the FFI clone callback + raw-ptr deref), then let
1528    // both drop. Under Miri this asserts the clone/drop path is UB-free and the
1529    // owned `Box` is freed exactly once per value (no double-free).
1530    #[cfg(feature = "std")]
1531    #[test]
1532    fn instant_ptr_clone_and_drop_no_ub() {
1533        let base = StdInstant::now();
1534        let a: InstantPtr = base.into();
1535        let b = a.clone();
1536        // The clone must observe the same underlying instant.
1537        assert_eq!(a, b);
1538        // Both `a` and `b` own independent Boxes; dropping both must not
1539        // double-free (each has run_destructor == true).
1540        drop(a);
1541        drop(b);
1542    }
1543}
1544
1545#[cfg(test)]
1546#[allow(clippy::float_cmp)] // exact-value assertions on ratios / interpolation results
1547mod autotest_generated {
1548    use super::*;
1549
1550    // ---- helpers -----------------------------------------------------------
1551
1552    fn tick(n: u64) -> Instant {
1553        Instant::Tick(SystemTick::new(n))
1554    }
1555    fn tick_dur(n: u64) -> Duration {
1556        Duration::Tick(SystemTickDiff { tick_diff: n })
1557    }
1558    fn sys_dur(secs: u64, nanos: u32) -> Duration {
1559        Duration::System(SystemTimeDiff { secs, nanos })
1560    }
1561
1562    // ========================================================================
1563    // TimerId::unique / ThreadId::unique  (monotonic, never hits reserved IDs)
1564    // ========================================================================
1565
1566    #[test]
1567    fn timer_id_unique_is_strictly_increasing_and_above_reserved_range() {
1568        let a = TimerId::unique();
1569        let b = TimerId::unique();
1570        assert_ne!(a, b);
1571        assert!(b.id > a.id, "unique() must strictly increase: {a:?} -> {b:?}");
1572        // User IDs must never land inside the reserved system-timer block.
1573        for id in [a, b] {
1574            assert!(
1575                id.id >= USER_TIMER_ID_START,
1576                "unique() handed out a reserved system ID: {id:?}"
1577            );
1578            assert_ne!(id, CURSOR_BLINK_TIMER_ID);
1579            assert_ne!(id, SCROLL_MOMENTUM_TIMER_ID);
1580            assert_ne!(id, DRAG_AUTOSCROLL_TIMER_ID);
1581            assert_ne!(id, TOOLTIP_DELAY_TIMER_ID);
1582            assert_ne!(id, CAPABILITY_PUMP_TIMER_ID);
1583            assert_ne!(id, LONG_PRESS_TIMER_ID);
1584        }
1585    }
1586
1587    #[test]
1588    fn thread_id_unique_is_strictly_increasing_and_above_reserved_range() {
1589        let a = ThreadId::unique();
1590        let b = ThreadId::unique();
1591        assert_ne!(a, b);
1592        assert!(b.id > a.id);
1593        assert!(a.id >= RESERVED_THREAD_ID_COUNT);
1594    }
1595
1596    // The counters are `AtomicUsize` + `fetch_add`, so concurrent callers must
1597    // never be handed the same ID. 8 threads x 64 IDs => 512 distinct values.
1598    #[cfg(feature = "std")]
1599    #[test]
1600    fn unique_ids_do_not_collide_across_threads() {
1601        use alloc::collections::BTreeSet;
1602
1603        let handles: Vec<_> = (0..8)
1604            .map(|_| {
1605                std::thread::spawn(|| {
1606                    let mut out = Vec::new();
1607                    for _ in 0..64 {
1608                        out.push((TimerId::unique().id, ThreadId::unique().id));
1609                    }
1610                    out
1611                })
1612            })
1613            .collect();
1614
1615        let mut timer_ids = BTreeSet::new();
1616        let mut thread_ids = BTreeSet::new();
1617        for h in handles {
1618            for (t, th) in h.join().expect("worker thread panicked") {
1619                assert!(timer_ids.insert(t), "duplicate TimerId handed out: {t}");
1620                assert!(thread_ids.insert(th), "duplicate ThreadId handed out: {th}");
1621            }
1622        }
1623        assert_eq!(timer_ids.len(), 8 * 64);
1624        assert_eq!(thread_ids.len(), 8 * 64);
1625    }
1626
1627    // ========================================================================
1628    // Instant::now / get_system_time_libstd
1629    // ========================================================================
1630
1631    #[cfg(feature = "std")]
1632    #[test]
1633    fn instant_now_is_system_and_monotonic() {
1634        let a = Instant::now();
1635        let b = Instant::now();
1636        assert!(matches!(a, Instant::System(_)));
1637        assert!(a <= b, "Instant::now() went backwards");
1638        // A later instant is never "before" an earlier one.
1639        assert_eq!(a.duration_since(&b), sys_dur(0, 0));
1640    }
1641
1642    #[cfg(all(feature = "std", not(target_arch = "wasm32")))]
1643    #[test]
1644    fn get_system_time_libstd_is_monotonic_system_instant() {
1645        let a = get_system_time_libstd();
1646        let b = get_system_time_libstd();
1647        assert!(matches!(a, Instant::System(_)));
1648        assert!(matches!(b, Instant::System(_)));
1649        assert!(a <= b);
1650    }
1651
1652    #[cfg(any(not(feature = "std"), target_arch = "wasm32"))]
1653    #[test]
1654    fn get_system_time_libstd_wasm_fallback_is_zero_tick() {
1655        // On WASM / no_std `StdInstant::now()` would panic, so the fallback must
1656        // hand back a tick instant instead of exploding.
1657        assert_eq!(get_system_time_libstd(), tick(0));
1658    }
1659
1660    // ========================================================================
1661    // Instant::linear_interpolate  (must never return NaN / escape [0.0, 1.0])
1662    // ========================================================================
1663
1664    #[test]
1665    fn linear_interpolate_clamps_outside_the_interval() {
1666        // before start -> 0.0, after end -> 1.0 (never negative / >1).
1667        assert_eq!(tick(0).linear_interpolate(tick(10), tick(20)), 0.0);
1668        assert_eq!(tick(999).linear_interpolate(tick(10), tick(20)), 1.0);
1669        // exactly on the boundaries
1670        assert_eq!(tick(10).linear_interpolate(tick(10), tick(20)), 0.0);
1671        assert_eq!(tick(20).linear_interpolate(tick(10), tick(20)), 1.0);
1672    }
1673
1674    #[test]
1675    fn linear_interpolate_reversed_interval_is_normalized() {
1676        // `end < start` is swapped internally, so the ratio is the same as the
1677        // correctly-ordered call rather than a garbage / negative value.
1678        let forwards = tick(5).linear_interpolate(tick(0), tick(10));
1679        let backwards = tick(5).linear_interpolate(tick(10), tick(0));
1680        assert_eq!(forwards, backwards);
1681        assert!((backwards - 0.5).abs() < 1e-6);
1682    }
1683
1684    #[test]
1685    fn linear_interpolate_saturating_extremes_stay_in_range() {
1686        // Full u64 span: the tick diff hits u64::MAX and the f64->f32 narrowing
1687        // must not produce inf/NaN.
1688        let v = tick(u64::MAX / 2).linear_interpolate(tick(0), tick(u64::MAX));
1689        assert!(v.is_finite(), "interpolation over the full u64 span went non-finite");
1690        assert!((0.0..=1.0).contains(&v));
1691        assert!((v - 0.5).abs() < 1e-3, "expected ~0.5, got {v}");
1692
1693        // Degenerate zero-length interval at the extremes -> 1.0, not 0/0 = NaN.
1694        let z = tick(u64::MAX).linear_interpolate(tick(u64::MAX), tick(u64::MAX));
1695        assert_eq!(z, 1.0);
1696        let z0 = tick(0).linear_interpolate(tick(0), tick(0));
1697        assert_eq!(z0, 1.0);
1698    }
1699
1700    #[cfg(feature = "std")]
1701    #[test]
1702    fn linear_interpolate_mismatched_kinds_never_nan() {
1703        // Every mismatched (System / Tick) permutation feeds a 0/0 division
1704        // internally; the guard must turn that into a finite value in [0, 1].
1705        let sys = Instant::now();
1706        let cases = [
1707            (tick(5), sys.clone(), tick(10)),
1708            (sys.clone(), tick(0), tick(10)),
1709            (tick(5), tick(0), sys.clone()),
1710            (sys.clone(), sys.clone(), tick(10)),
1711            (tick(5), sys.clone(), sys.clone()),
1712        ];
1713        for (this, start, end) in cases {
1714            let v = this.linear_interpolate(start, end);
1715            assert!(v.is_finite(), "mismatched-kind interpolation returned {v}");
1716            assert!(
1717                (0.0..=1.0).contains(&v),
1718                "mismatched-kind interpolation escaped [0,1]: {v}"
1719            );
1720        }
1721    }
1722
1723    // ========================================================================
1724    // Instant::add_optional_duration
1725    // ========================================================================
1726
1727    #[test]
1728    fn add_optional_duration_none_is_identity() {
1729        let t = tick(42);
1730        assert_eq!(t.add_optional_duration(None), t);
1731        assert_eq!(tick(u64::MAX).add_optional_duration(None), tick(u64::MAX));
1732    }
1733
1734    #[test]
1735    fn add_optional_duration_tick_saturates_at_u64_max() {
1736        // saturating_add: u64::MAX-1 + huge must clamp, not wrap or panic.
1737        let near_max = tick(u64::MAX - 1);
1738        assert_eq!(
1739            near_max.add_optional_duration(Some(&tick_dur(u64::MAX))),
1740            tick(u64::MAX)
1741        );
1742        assert_eq!(tick(0).add_optional_duration(Some(&tick_dur(0))), tick(0));
1743    }
1744
1745    #[cfg(feature = "std")]
1746    #[test]
1747    fn add_optional_duration_system_advances_by_the_duration() {
1748        let base = Instant::now();
1749        let later = base.add_optional_duration(Some(&Duration::System(SystemTimeDiff::from_secs(1))));
1750        assert!(later > base);
1751        let delta = later.duration_since(&base);
1752        assert_eq!(delta, sys_dur(1, 0));
1753        // ... and the reverse span saturates to zero rather than going negative.
1754        assert_eq!(base.duration_since(&later), sys_dur(0, 0));
1755    }
1756
1757    /// A `Tick` interval on a wall-clock instant has to ADVANCE that instant, not
1758    /// leave it alone. `Timer::instant_of_next_run` is exactly `last_run +
1759    /// delay + interval`; when this returned `self`, a tick-unit timer's next run
1760    /// was always "now" — permanently overdue, and `time_until_next_timer_ms`
1761    /// answered `Some(0)` for it.
1762    #[cfg(feature = "std")]
1763    #[test]
1764    fn add_optional_duration_converts_between_units_in_both_directions() {
1765        let sys = Instant::now();
1766        // System instant + Tick duration: 60 ticks is exactly one second.
1767        let later = sys.add_optional_duration(Some(&tick_dur(60)));
1768        assert!(later > sys, "a tick interval must advance a wall-clock instant");
1769        assert_eq!(later.duration_since(&sys), sys_dur(1, 0));
1770
1771        // 0 ticks is genuinely no time at all.
1772        assert_eq!(sys.add_optional_duration(Some(&tick_dur(0))), sys);
1773
1774        // Tick instant + System duration: 3s is 180 whole frames.
1775        assert_eq!(tick(7).add_optional_duration(Some(&sys_dur(3, 0))), tick(187));
1776    }
1777
1778    // A `System` instant plus an enormous `System` duration overflows the
1779    // platform clock representation: `StdInstant + StdDuration` panics with
1780    // "overflow when adding duration to instant". Unlike the mismatched-kind
1781    // case (documented to saturate), this arm has no guard -- characterized
1782    // here so a future saturating fix flips this test loudly.
1783    #[cfg(all(feature = "std", not(target_arch = "wasm32")))]
1784    #[test]
1785    #[should_panic(expected = "overflow")]
1786    fn add_optional_duration_system_overflow_panics_today() {
1787        let base = Instant::now();
1788        let _ = base.add_optional_duration(Some(&Duration::max()));
1789    }
1790
1791    // ========================================================================
1792    // Instant::duration_since / into_std_instant
1793    // ========================================================================
1794
1795    #[test]
1796    fn duration_since_tick_saturates_and_is_exact() {
1797        assert_eq!(tick(10).duration_since(&tick(4)), tick_dur(6));
1798        // self == earlier -> zero span
1799        assert_eq!(tick(10).duration_since(&tick(10)), tick_dur(0));
1800        // earlier is later -> saturate to zero, no underflow panic
1801        assert_eq!(tick(0).duration_since(&tick(u64::MAX)), tick_dur(0));
1802        // full-range span does not overflow
1803        assert_eq!(tick(u64::MAX).duration_since(&tick(0)), tick_dur(u64::MAX));
1804    }
1805
1806    #[cfg(feature = "std")]
1807    #[test]
1808    fn duration_since_mismatched_kinds_is_zero_tick_both_directions() {
1809        let sys = Instant::now();
1810        assert_eq!(sys.duration_since(&tick(5)), tick_dur(0));
1811        assert_eq!(tick(5).duration_since(&sys), tick_dur(0));
1812    }
1813
1814    #[cfg(feature = "std")]
1815    #[test]
1816    fn into_std_instant_round_trips_a_system_instant() {
1817        let base = StdInstant::now();
1818        let wrapped: Instant = base.into();
1819        assert_eq!(wrapped.into_std_instant(), base);
1820    }
1821
1822    #[cfg(feature = "std")]
1823    #[test]
1824    #[should_panic(expected = "internal error: entered unreachable code")]
1825    fn into_std_instant_on_tick_variant_panics() {
1826        // Documented: `into_std_instant` is `unreachable!()` for Tick instants.
1827        let _ = tick(1).into_std_instant();
1828    }
1829
1830    // ========================================================================
1831    // SystemTick::new
1832    // ========================================================================
1833
1834    #[test]
1835    fn system_tick_new_stores_the_counter_verbatim() {
1836        for n in [0_u64, 1, 0x0100, u64::MAX / 2, u64::MAX] {
1837            assert_eq!(SystemTick::new(n).tick_counter, n);
1838        }
1839        // Ordering follows the counter (used by Instant's derived Ord).
1840        assert!(SystemTick::new(0) < SystemTick::new(u64::MAX));
1841        assert_eq!(SystemTick::new(7), SystemTick::new(7));
1842    }
1843
1844    // ========================================================================
1845    // InstantPtr: get / std_instant_clone / std_instant_drop
1846    // ========================================================================
1847
1848    #[cfg(feature = "std")]
1849    #[test]
1850    fn instant_ptr_get_returns_the_wrapped_instant() {
1851        let base = StdInstant::now();
1852        let p: InstantPtr = base.into();
1853        assert_eq!(p.get(), base);
1854        // `get` is a copy, not a move: repeated reads stay stable.
1855        assert_eq!(p.get(), p.get());
1856        assert!(p.run_destructor);
1857        // Debug must not panic and must not be empty.
1858        assert!(!alloc::format!("{p:?}").is_empty());
1859    }
1860
1861    #[cfg(feature = "std")]
1862    #[test]
1863    fn std_instant_clone_deep_copies_and_arms_the_destructor() {
1864        let base = StdInstant::now();
1865        let a: InstantPtr = base.into();
1866        let cloned = std_instant_clone(core::ptr::from_ref(&a));
1867        assert_eq!(cloned.get(), base);
1868        // The clone owns its OWN box (freeing both must not double-free).
1869        assert!(!core::ptr::eq(&**a.ptr, &**cloned.ptr));
1870        assert!(cloned.run_destructor, "clone handed back a disarmed destructor");
1871        drop(cloned);
1872        // The source survives its clone being dropped.
1873        assert_eq!(a.get(), base);
1874    }
1875
1876    #[cfg(feature = "std")]
1877    #[test]
1878    fn std_instant_drop_is_a_noop_even_for_null() {
1879        // The libstd destructor callback is deliberately empty: the Box is freed
1880        // by `InstantPtr::drop` under the `run_destructor` guard. Calling it with
1881        // a null pointer must therefore be harmless.
1882        std_instant_drop(core::ptr::null_mut());
1883
1884        let mut p: InstantPtr = StdInstant::now().into();
1885        let before = p.get();
1886        std_instant_drop(core::ptr::from_mut(&mut p));
1887        // Value is untouched and still owned afterwards.
1888        assert_eq!(p.get(), before);
1889        assert!(p.run_destructor);
1890    }
1891
1892    // ========================================================================
1893    // Duration::fmt (Display)
1894    // ========================================================================
1895
1896    #[test]
1897    fn duration_display_tick_edge_values() {
1898        assert_eq!(alloc::format!("{}", tick_dur(0)), "0 ticks");
1899        assert_eq!(alloc::format!("{}", tick_dur(1)), "1 ticks");
1900        assert_eq!(
1901            alloc::format!("{}", tick_dur(u64::MAX)),
1902            "18446744073709551615 ticks"
1903        );
1904    }
1905
1906    #[cfg(feature = "std")]
1907    #[test]
1908    fn duration_display_system_edge_values_do_not_panic() {
1909        // zero, sub-second, denormalized nanos and the absolute maximum all have
1910        // to format without panicking and without producing an empty string.
1911        for d in [
1912            sys_dur(0, 0),
1913            sys_dur(1, 500_000_000),
1914            sys_dur(0, u32::MAX),
1915            sys_dur(u64::MAX, NANOS_PER_SEC - 1),
1916            Duration::max(),
1917        ] {
1918            let s = alloc::format!("{d}");
1919            assert!(!s.is_empty());
1920            assert!(!s.ends_with("ticks"), "System duration formatted as ticks: {s}");
1921        }
1922    }
1923
1924    // ========================================================================
1925    // Duration::max / div / min / greater_than / smaller_than
1926    // ========================================================================
1927
1928    #[cfg(feature = "std")]
1929    #[test]
1930    fn duration_max_is_the_upper_bound() {
1931        let m = Duration::max();
1932        assert_eq!(m, sys_dur(u64::MAX, NANOS_PER_SEC - 1));
1933        // Nothing of the same kind is greater than it...
1934        assert!(m.greater_than(&sys_dur(u64::MAX, NANOS_PER_SEC - 2)));
1935        assert!(m.greater_than(&sys_dur(0, 0)));
1936        // ... and it is not greater/smaller than itself.
1937        assert!(!m.greater_than(&m));
1938        assert!(!m.smaller_than(&m));
1939        // Converting the maximum back to std must not overflow-panic.
1940        let Duration::System(inner) = m else {
1941            panic!("Duration::max() is not a System duration under std")
1942        };
1943        assert_eq!(inner.get(), StdDuration::new(u64::MAX, NANOS_PER_SEC - 1));
1944    }
1945
1946    #[test]
1947    fn duration_div_by_zero_yields_inf_or_nan_not_a_panic() {
1948        // 0/0 -> NaN, x/0 -> +inf. Neither may panic.
1949        assert!(tick_dur(0).div(&tick_dur(0)).is_nan());
1950        let inf = tick_dur(5).div(&tick_dur(0));
1951        assert!(inf.is_infinite() && inf.is_sign_positive());
1952
1953        assert!(sys_dur(0, 0).div(&sys_dur(0, 0)).is_nan());
1954        let sinf = sys_dur(1, 0).div(&sys_dur(0, 0));
1955        assert!(sinf.is_infinite() && sinf.is_sign_positive());
1956    }
1957
1958    #[test]
1959    fn duration_div_extremes_stay_finite_in_f32() {
1960        // u64::MAX / 1 ~= 1.8e19, comfortably inside f32 range: the f64 -> f32
1961        // narrowing must not produce inf.
1962        let r = tick_dur(u64::MAX).div(&tick_dur(1));
1963        assert!(r.is_finite(), "u64::MAX tick ratio overflowed f32: {r}");
1964        assert!(r > 1e19);
1965        // Identity ratios are exactly 1.0 for both kinds.
1966        assert_eq!(tick_dur(u64::MAX).div(&tick_dur(u64::MAX)), 1.0);
1967        assert_eq!(sys_dur(3, 0).div(&sys_dur(2, 0)), 1.5);
1968    }
1969
1970    /// Cross-unit division converts instead of collapsing to `0.0`. 10 ticks is
1971    /// one sixth of a second, so the two ratios are reciprocals of each other —
1972    /// which is the property `0.0` both ways could never satisfy.
1973    #[test]
1974    fn duration_div_across_kinds_converts_both_ways() {
1975        assert!((sys_dur(1, 0).div(&tick_dur(10)) - 6.0).abs() < 1e-5);
1976        assert!((tick_dur(10).div(&sys_dur(1, 0)) - (1.0 / 6.0)).abs() < 1e-5);
1977    }
1978
1979    #[test]
1980    fn duration_min_picks_the_smaller_of_the_same_kind() {
1981        assert_eq!(tick_dur(5).min(tick_dur(10)), tick_dur(5));
1982        assert_eq!(tick_dur(10).min(tick_dur(5)), tick_dur(5));
1983        assert_eq!(tick_dur(7).min(tick_dur(7)), tick_dur(7));
1984        assert_eq!(tick_dur(0).min(tick_dur(u64::MAX)), tick_dur(0));
1985        // Comparison no longer needs std: it is u128 nanosecond arithmetic.
1986        assert_eq!(sys_dur(1, 0).min(sys_dur(1, 1)), sys_dur(1, 0));
1987    }
1988
1989    /// `min` is built on `smaller_than`, so it picks the genuinely shorter span
1990    /// across units and is COMMUTATIVE. It used to just return `other` whenever
1991    /// the units differed — so `a.min(b)` and `b.min(a)` disagreed, and the
1992    /// answer depended on argument order rather than on the durations.
1993    #[test]
1994    fn duration_min_across_kinds_picks_the_genuinely_shorter_span() {
1995        // 5 ticks is ~83ms, so it is shorter than a second either way round.
1996        assert_eq!(tick_dur(5).min(sys_dur(1, 0)), tick_dur(5));
1997        assert_eq!(sys_dur(1, 0).min(tick_dur(5)), tick_dur(5));
1998        // ...and 120 ticks is 2s, so the second wins either way round.
1999        assert_eq!(tick_dur(120).min(sys_dur(1, 0)), sys_dur(1, 0));
2000        assert_eq!(sys_dur(1, 0).min(tick_dur(120)), sys_dur(1, 0));
2001    }
2002
2003    #[test]
2004    fn duration_comparison_is_a_strict_total_order_within_a_kind() {
2005        let mut pairs = alloc::vec![(tick_dur(0), tick_dur(u64::MAX)), (tick_dur(1), tick_dur(2))];
2006        // System ordering no longer needs std: the comparison is u128 nanosecond
2007        // arithmetic. It used to defer to `StdDuration`, so on no_std it answered
2008        // `false` for every System pair — a total order that ordered nothing.
2009        pairs.extend_from_slice(&[
2010            (sys_dur(0, 0), sys_dur(u64::MAX, 0)),
2011            (sys_dur(1, 999_999_999), sys_dur(2, 0)),
2012        ]);
2013
2014        for (a, b) in pairs {
2015            assert!(a.smaller_than(&b));
2016            assert!(b.greater_than(&a));
2017            assert!(!a.greater_than(&b));
2018            assert!(!b.smaller_than(&a));
2019        }
2020        // Equal values: neither greater nor smaller (holds for both kinds).
2021        let eq = tick_dur(4);
2022        assert!(!eq.greater_than(&eq));
2023        assert!(!eq.smaller_than(&eq));
2024        let eq_sys = sys_dur(4, 2);
2025        assert!(!eq_sys.greater_than(&eq_sys));
2026        assert!(!eq_sys.smaller_than(&eq_sys));
2027    }
2028
2029    #[cfg(feature = "std")]
2030    #[test]
2031    fn duration_comparison_normalizes_denormalized_nanos() {
2032        // nanos == u32::MAX (> 1e9) is denormalized; the std conversion carries
2033        // it into secs, so {0, u32::MAX} == 4.294967295s > 4s.
2034        let denorm = sys_dur(0, u32::MAX);
2035        assert!(denorm.greater_than(&sys_dur(4, 0)));
2036        assert!(denorm.smaller_than(&sys_dur(5, 0)));
2037    }
2038
2039    // ========================================================================
2040    // SystemTickDiff::div / SystemTimeDiff::div + as_secs_f64
2041    // ========================================================================
2042
2043    #[test]
2044    fn system_tick_diff_div_edge_cases() {
2045        let zero = SystemTickDiff { tick_diff: 0 };
2046        let one = SystemTickDiff { tick_diff: 1 };
2047        let max = SystemTickDiff { tick_diff: u64::MAX };
2048
2049        assert!(zero.div(&zero).is_nan());
2050        assert!(one.div(&zero).is_infinite());
2051        assert_eq!(zero.div(&one), 0.0);
2052        assert_eq!(max.div(&max), 1.0);
2053        assert!(max.div(&one).is_finite());
2054        assert_eq!(SystemTickDiff { tick_diff: 5 }.div(&SystemTickDiff { tick_diff: 10 }), 0.5);
2055    }
2056
2057    #[test]
2058    fn system_time_diff_as_secs_f64_is_exact_for_representable_values() {
2059        assert_eq!(SystemTimeDiff { secs: 0, nanos: 0 }.as_secs_f64(), 0.0);
2060        assert_eq!(SystemTimeDiff { secs: 1, nanos: 500_000_000 }.as_secs_f64(), 1.5);
2061        assert_eq!(SystemTimeDiff { secs: 0, nanos: 500_000_000 }.as_secs_f64(), 0.5);
2062        // Extremes stay finite (u64::MAX secs ~= 1.8e19, well inside f64).
2063        let huge = SystemTimeDiff { secs: u64::MAX, nanos: NANOS_PER_SEC - 1 };
2064        assert!(huge.as_secs_f64().is_finite());
2065        assert!(huge.as_secs_f64() > 1e19);
2066        // Monotone in secs.
2067        assert!(
2068            SystemTimeDiff::from_secs(2).as_secs_f64() > SystemTimeDiff::from_secs(1).as_secs_f64()
2069        );
2070    }
2071
2072    #[test]
2073    fn system_time_diff_div_edge_cases() {
2074        let zero = SystemTimeDiff { secs: 0, nanos: 0 };
2075        let one = SystemTimeDiff::from_secs(1);
2076        let half = SystemTimeDiff { secs: 0, nanos: 500_000_000 };
2077
2078        assert!(zero.div(&zero).is_nan());
2079        assert!(one.div(&zero).is_infinite());
2080        assert_eq!(zero.div(&one), 0.0);
2081        assert_eq!(one.div(&one), 1.0);
2082        assert_eq!(one.div(&half), 2.0);
2083        let max = SystemTimeDiff { secs: u64::MAX, nanos: NANOS_PER_SEC - 1 };
2084        assert_eq!(max.div(&max), 1.0);
2085        assert!(max.div(&one).is_finite());
2086    }
2087
2088    // ========================================================================
2089    // SystemTimeDiff constructors: from_secs / from_millis / from_nanos
2090    // ========================================================================
2091
2092    #[test]
2093    fn from_secs_invariants() {
2094        for s in [0_u64, 1, 1_000, u64::MAX] {
2095            let d = SystemTimeDiff::from_secs(s);
2096            assert_eq!(d.secs, s);
2097            assert_eq!(d.nanos, 0, "from_secs must leave nanos at zero");
2098        }
2099    }
2100
2101    #[test]
2102    fn from_millis_normalizes_and_keeps_nanos_in_range() {
2103        assert_eq!(SystemTimeDiff::from_millis(0), SystemTimeDiff { secs: 0, nanos: 0 });
2104        assert_eq!(
2105            SystemTimeDiff::from_millis(999),
2106            SystemTimeDiff { secs: 0, nanos: 999_000_000 }
2107        );
2108        assert_eq!(SystemTimeDiff::from_millis(1_000), SystemTimeDiff { secs: 1, nanos: 0 });
2109        assert_eq!(
2110            SystemTimeDiff::from_millis(1_500),
2111            SystemTimeDiff { secs: 1, nanos: 500_000_000 }
2112        );
2113        // u64::MAX millis must not overflow the u32 nanos field.
2114        let max = SystemTimeDiff::from_millis(u64::MAX);
2115        assert!(max.nanos < NANOS_PER_SEC, "from_millis produced denormalized nanos");
2116        assert_eq!(max.secs, u64::MAX / MILLIS_PER_SEC);
2117    }
2118
2119    #[test]
2120    fn from_nanos_normalizes_and_keeps_nanos_in_range() {
2121        assert_eq!(SystemTimeDiff::from_nanos(0), SystemTimeDiff { secs: 0, nanos: 0 });
2122        assert_eq!(
2123            SystemTimeDiff::from_nanos(999_999_999),
2124            SystemTimeDiff { secs: 0, nanos: 999_999_999 }
2125        );
2126        assert_eq!(
2127            SystemTimeDiff::from_nanos(1_000_000_000),
2128            SystemTimeDiff { secs: 1, nanos: 0 }
2129        );
2130        for n in [0_u64, 1, 999_999_999, 1_000_000_001, u64::MAX] {
2131            let d = SystemTimeDiff::from_nanos(n);
2132            assert!(d.nanos < NANOS_PER_SEC, "from_nanos({n}) produced denormalized nanos");
2133            // Lossless round-trip: secs * 1e9 + nanos == n (checked in u128).
2134            let back =
2135                u128::from(d.secs) * u128::from(NANOS_PER_SEC) + u128::from(d.nanos);
2136            assert_eq!(back, u128::from(n), "from_nanos({n}) lost information");
2137        }
2138    }
2139
2140    // ========================================================================
2141    // Round-trip: from_millis <-> millis
2142    // ========================================================================
2143
2144    #[test]
2145    fn millis_round_trips_through_from_millis() {
2146        // Exact for every whole-millisecond value, INCLUDING u64::MAX (where
2147        // `secs * 1000 + 615` lands exactly on u64::MAX without saturating).
2148        for m in [0_u64, 1, 999, 1_000, 1_500, 86_400_000, u64::MAX] {
2149            assert_eq!(
2150                SystemTimeDiff::from_millis(m).millis(),
2151                m,
2152                "from_millis({m}).millis() is not lossless"
2153            );
2154        }
2155    }
2156
2157    #[test]
2158    fn millis_truncates_and_saturates_instead_of_panicking() {
2159        // Sub-millisecond nanos truncate towards zero.
2160        assert_eq!(SystemTimeDiff { secs: 0, nanos: 999_999 }.millis(), 0);
2161        assert_eq!(SystemTimeDiff { secs: 0, nanos: 999_999_999 }.millis(), 999);
2162        // secs * 1000 overflows u64 -> saturate at u64::MAX, no panic.
2163        assert_eq!(SystemTimeDiff { secs: u64::MAX, nanos: 0 }.millis(), u64::MAX);
2164        assert_eq!(
2165            SystemTimeDiff { secs: u64::MAX, nanos: NANOS_PER_SEC - 1 }.millis(),
2166            u64::MAX
2167        );
2168        assert_eq!(SystemTimeDiff::from_secs(u64::MAX / 1_000).millis(), (u64::MAX / 1_000) * 1_000);
2169    }
2170
2171    // ========================================================================
2172    // SystemTimeDiff::checked_add
2173    // ========================================================================
2174
2175    #[test]
2176    fn checked_add_carries_nanos_into_secs() {
2177        let a = SystemTimeDiff { secs: 0, nanos: 999_999_999 };
2178        let sum = a.checked_add(a).expect("0.999s + 0.999s must not overflow");
2179        assert_eq!(sum, SystemTimeDiff { secs: 1, nanos: 999_999_998 });
2180        // Exactly one second of nanos carries cleanly.
2181        let b = SystemTimeDiff { secs: 1, nanos: 500_000_000 };
2182        assert_eq!(
2183            b.checked_add(b),
2184            Some(SystemTimeDiff { secs: 3, nanos: 0 })
2185        );
2186    }
2187
2188    #[test]
2189    fn checked_add_returns_none_on_overflow_instead_of_panicking() {
2190        let max_secs = SystemTimeDiff { secs: u64::MAX, nanos: 0 };
2191        // secs overflow
2192        assert_eq!(max_secs.checked_add(SystemTimeDiff::from_secs(1)), None);
2193        // secs at max, nanos still fit -> Some
2194        assert_eq!(
2195            max_secs.checked_add(SystemTimeDiff { secs: 0, nanos: NANOS_PER_SEC - 1 }),
2196            Some(SystemTimeDiff { secs: u64::MAX, nanos: NANOS_PER_SEC - 1 })
2197        );
2198        // overflow that only happens because of the nanos CARRY
2199        let brim = SystemTimeDiff { secs: u64::MAX, nanos: NANOS_PER_SEC - 1 };
2200        assert_eq!(brim.checked_add(SystemTimeDiff { secs: 0, nanos: 1 }), None);
2201    }
2202
2203    #[test]
2204    fn checked_add_identity_and_commutativity() {
2205        let zero = SystemTimeDiff { secs: 0, nanos: 0 };
2206        for d in [
2207            SystemTimeDiff::from_secs(0),
2208            SystemTimeDiff::from_millis(1_500),
2209            SystemTimeDiff::from_nanos(u64::MAX),
2210            SystemTimeDiff { secs: u64::MAX, nanos: 0 },
2211        ] {
2212            assert_eq!(d.checked_add(zero), Some(d));
2213            assert_eq!(zero.checked_add(d), Some(d));
2214            // a + b == b + a for well-formed operands
2215            let other = SystemTimeDiff::from_millis(750);
2216            assert_eq!(d.checked_add(other), other.checked_add(d));
2217        }
2218    }
2219
2220    // ========================================================================
2221    // SystemTimeDiff::get  (std::time::Duration conversion round-trip)
2222    // ========================================================================
2223
2224    #[cfg(feature = "std")]
2225    #[test]
2226    fn system_time_diff_get_round_trips_std_duration() {
2227        for std_d in [
2228            StdDuration::ZERO,
2229            StdDuration::from_millis(1_500),
2230            StdDuration::from_nanos(1),
2231            StdDuration::new(u64::MAX, NANOS_PER_SEC - 1),
2232        ] {
2233            let mid: SystemTimeDiff = std_d.into();
2234            assert_eq!(mid.get(), std_d, "StdDuration -> SystemTimeDiff -> StdDuration lost data");
2235        }
2236    }
2237
2238    #[cfg(feature = "std")]
2239    #[test]
2240    fn system_time_diff_get_on_edge_values_does_not_panic() {
2241        assert_eq!(SystemTimeDiff { secs: 0, nanos: 0 }.get(), StdDuration::ZERO);
2242        // secs at max with zero nanos: no carry, so no overflow in Duration::new.
2243        assert_eq!(
2244            SystemTimeDiff::from_secs(u64::MAX).get(),
2245            StdDuration::new(u64::MAX, 0)
2246        );
2247        // Denormalized nanos (>= 1e9) are carried by Duration::new, not rejected.
2248        assert_eq!(
2249            SystemTimeDiff { secs: 0, nanos: u32::MAX }.get(),
2250            StdDuration::new(0, u32::MAX)
2251        );
2252    }
2253
2254    // ========================================================================
2255    // ThreadReceiver: new / get_ctx / recv / clone
2256    // ========================================================================
2257
2258    #[cfg(feature = "std")]
2259    extern "C" fn test_thread_recv(ptr: *const c_void) -> OptionThreadSendMsg {
2260        // Mirrors the real callback: `ThreadReceiver::recv` hands over a pointer
2261        // to the boxed `Receiver<ThreadSendMsg>` inside `ThreadReceiverInner`.
2262        let receiver = unsafe { &*(ptr.cast::<Receiver<ThreadSendMsg>>()) };
2263        receiver.try_recv().ok().into()
2264    }
2265
2266    #[cfg(feature = "std")]
2267    const extern "C" fn test_thread_recv_destructor(_: *mut ThreadReceiverInner) {}
2268
2269    #[cfg(feature = "std")]
2270    fn test_receiver() -> (Sender<ThreadSendMsg>, ThreadReceiver) {
2271        let (tx, rx) = std::sync::mpsc::channel::<ThreadSendMsg>();
2272        let inner = ThreadReceiverInner {
2273            ptr: Box::new(rx),
2274            recv_fn: ThreadRecvCallback { cb: test_thread_recv },
2275            destructor: ThreadReceiverDestructorCallback {
2276                cb: test_thread_recv_destructor,
2277            },
2278        };
2279        (tx, ThreadReceiver::new(inner))
2280    }
2281
2282    #[cfg(feature = "std")]
2283    #[test]
2284    fn thread_receiver_new_arms_destructor_and_has_no_ctx() {
2285        let (_tx, r) = test_receiver();
2286        assert!(r.run_destructor, "ThreadReceiver::new left the destructor disarmed");
2287        assert!(r.get_ctx().is_none(), "a fresh receiver must have no FFI context");
2288    }
2289
2290    #[cfg(feature = "std")]
2291    #[test]
2292    fn thread_receiver_recv_on_empty_and_disconnected_channel_is_none() {
2293        let (tx, mut r) = test_receiver();
2294        // Empty channel -> None (must not block / panic).
2295        assert!(r.recv().is_none());
2296        // Disconnected channel -> still None, not a panic.
2297        drop(tx);
2298        assert!(r.recv().is_none());
2299        assert!(r.recv().is_none());
2300    }
2301
2302    #[cfg(feature = "std")]
2303    #[test]
2304    fn thread_receiver_recv_delivers_messages_in_order() {
2305        let (tx, mut r) = test_receiver();
2306        tx.send(ThreadSendMsg::Tick).unwrap();
2307        tx.send(ThreadSendMsg::Custom(RefAny::new(42_u32))).unwrap();
2308        tx.send(ThreadSendMsg::TerminateThread).unwrap();
2309
2310        assert_eq!(r.recv(), OptionThreadSendMsg::Some(ThreadSendMsg::Tick));
2311        assert!(matches!(
2312            r.recv(),
2313            OptionThreadSendMsg::Some(ThreadSendMsg::Custom(_))
2314        ));
2315        assert_eq!(
2316            r.recv(),
2317            OptionThreadSendMsg::Some(ThreadSendMsg::TerminateThread)
2318        );
2319        // Drained.
2320        assert!(r.recv().is_none());
2321    }
2322
2323    #[cfg(feature = "std")]
2324    #[test]
2325    fn thread_receiver_clone_shares_the_same_channel() {
2326        let (tx, mut a) = test_receiver();
2327        let mut b = a.clone();
2328        assert!(b.run_destructor);
2329
2330        tx.send(ThreadSendMsg::Tick).unwrap();
2331        // The clone shares the Arc<Mutex<..>>: whichever half receives first
2332        // consumes the message; the other must see an empty channel, not a
2333        // duplicate and not a deadlock.
2334        assert_eq!(a.recv(), OptionThreadSendMsg::Some(ThreadSendMsg::Tick));
2335        assert!(b.recv().is_none());
2336
2337        tx.send(ThreadSendMsg::TerminateThread).unwrap();
2338        assert_eq!(
2339            b.recv(),
2340            OptionThreadSendMsg::Some(ThreadSendMsg::TerminateThread)
2341        );
2342        assert!(a.recv().is_none());
2343    }
2344
2345    #[cfg(feature = "std")]
2346    #[test]
2347    fn thread_receiver_get_ctx_clones_rather_than_takes() {
2348        let (_tx, mut r) = test_receiver();
2349        r.ctx = OptionRefAny::Some(RefAny::new(7_u64));
2350        // Repeated reads must all succeed -- `get_ctx` clones the RefAny (refcount
2351        // bump); a take/move would leave the second call empty.
2352        assert!(r.get_ctx().is_some());
2353        assert!(r.get_ctx().is_some());
2354        let held = r.get_ctx();
2355        drop(r);
2356        // The cloned handle outlives the receiver it came from.
2357        assert!(held.is_some());
2358    }
2359}