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