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"))]
10
use alloc::string::{String, ToString};
11
use alloc::{
12
    boxed::Box,
13
    collections::btree_map::BTreeMap,
14
    sync::{Arc, Weak},
15
    vec::Vec,
16
};
17
use core::{
18
    ffi::c_void,
19
    fmt,
20
    mem::ManuallyDrop,
21
    sync::atomic::{AtomicUsize, Ordering},
22
};
23
#[cfg(feature = "std")]
24
use std::sync::mpsc::{Receiver, Sender};
25
#[cfg(feature = "std")]
26
use std::sync::Mutex;
27
#[cfg(feature = "std")]
28
use std::thread::{self, JoinHandle};
29
#[cfg(feature = "std")]
30
use std::time::Duration as StdDuration;
31
#[cfg(feature = "std")]
32
use std::time::Instant as StdInstant;
33

            
34
use azul_css::{props::property::CssProperty, AzString};
35
use rust_fontconfig::FcFontCache;
36

            
37
use 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)]
54
pub 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)
68
pub const CURSOR_BLINK_TIMER_ID: TimerId = TimerId { id: 0x0001 };
69
/// Timer ID for scroll momentum/inertia animation
70
pub const SCROLL_MOMENTUM_TIMER_ID: TimerId = TimerId { id: 0x0002 };
71
/// Timer ID for auto-scroll during drag operations near edges
72
pub 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.
84
pub 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).
94
pub 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).
102
pub 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.
108
pub const CARET_TWEEN_TIMER_ID: TimerId = TimerId { id: 0x0007 };
109

            
110
/// First available ID for user-defined timers
111
pub const USER_TIMER_ID_START: usize = 0x0100;
112

            
113
// User timers start at 0x0100 to avoid conflicts with reserved system timer IDs
114
static 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)]
119
pub struct TimerId {
120
    pub id: usize,
121
}
122

            
123
impl TimerId {
124
    /// Generates a new, unique `TimerId`.
125
    #[must_use]
126
536
    pub fn unique() -> Self {
127
536
        Self {
128
536
            id: MAX_TIMER_ID.fetch_add(1, Ordering::SeqCst),
129
536
        }
130
536
    }
131
}
132

            
133
impl_option!(
134
    TimerId,
135
    OptionTimerId,
136
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
137
);
138

            
139
impl_vec!(TimerId, TimerIdVec, TimerIdVecDestructor, TimerIdVecDestructorType, TimerIdVecSlice, OptionTimerId);
140
impl_vec_debug!(TimerId, TimerIdVec);
141
impl_vec_clone!(TimerId, TimerIdVec, TimerIdVecDestructor);
142
impl_vec_partialeq!(TimerId, TimerIdVec);
143
impl_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.
147
const RESERVED_THREAD_ID_COUNT: usize = 5;
148
static 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)]
153
pub struct ThreadId {
154
    id: usize,
155
}
156

            
157
impl_option!(
158
    ThreadId,
159
    OptionThreadId,
160
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
161
);
162

            
163
impl_vec!(ThreadId, ThreadIdVec, ThreadIdVecDestructor, ThreadIdVecDestructorType, ThreadIdVecSlice, OptionThreadId);
164
impl_vec_debug!(ThreadId, ThreadIdVec);
165
impl_vec_clone!(ThreadId, ThreadIdVec, ThreadIdVecDestructor);
166
impl_vec_partialeq!(ThreadId, ThreadIdVec);
167
impl_vec_partialord!(ThreadId, ThreadIdVec);
168

            
169
impl ThreadId {
170
    /// Generates a new, unique `ThreadId`.
171
    #[must_use]
172
998
    pub fn unique() -> Self {
173
998
        Self {
174
998
            id: MAX_THREAD_ID.fetch_add(1, Ordering::SeqCst),
175
998
        }
176
998
    }
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)]
184
pub 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")]
192
impl From<StdInstant> for Instant {
193
89359
    fn from(s: StdInstant) -> Self {
194
89359
        Self::System(s.into())
195
89359
    }
196
}
197

            
198
#[cfg(feature = "std")]
199
std::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]
237
186
pub fn advance_test_clock_ms(ms: u64) -> u64 {
238
186
    TEST_CLOCK_OFFSET_MS.with(|c| {
239
186
        let next = c.get().saturating_add(ms);
240
186
        c.set(next);
241
186
        next
242
186
    })
243
186
}
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]
249
61213
pub fn test_clock_offset_ms() -> u64 {
250
61213
    TEST_CLOCK_OFFSET_MS.with(core::cell::Cell::get)
251
61213
}
252

            
253
#[cfg(feature = "std")]
254
#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
255
std::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")))]
289
70
pub fn freeze_test_clock() {
290
70
    TEST_CLOCK_BASE.with(|c| {
291
70
        if c.get().is_none() {
292
69
            c.set(Some(StdInstant::now()));
293
69
        }
294
70
    });
295
70
}
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]
300
3
pub fn test_clock_is_frozen() -> bool {
301
3
    TEST_CLOCK_BASE.with(core::cell::Cell::get).is_some()
302
3
}
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")))]
313
83
pub fn reset_test_clock() {
314
83
    TEST_CLOCK_OFFSET_MS.with(|c| c.set(0));
315
83
    TEST_CLOCK_BASE.with(|c| c.set(None));
316
83
}
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")]
330
static 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")]
335
pub 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]
342
pub 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")))]
357
61187
fn std_now_with_test_offset() -> StdInstant {
358
61187
    let offset = test_clock_offset_ms();
359
61187
    if let Some(base) = TEST_CLOCK_BASE.with(core::cell::Cell::get) {
360
17352
        return base + core::time::Duration::from_millis(offset);
361
43835
    }
362
43835
    if offset == 0 {
363
43835
        StdInstant::now()
364
    } else {
365
        StdInstant::now() + core::time::Duration::from_millis(offset)
366
    }
367
61187
}
368

            
369
impl 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
34508
    #[must_use] pub fn now() -> Self {
376
34508
        std_now_with_test_offset().into()
377
34508
    }
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
16
    #[must_use] pub fn linear_interpolate(&self, mut start: Self, mut end: Self) -> f32 {
407
        use core::mem;
408

            
409
16
        if end < start {
410
2
            mem::swap(&mut start, &mut end);
411
14
        }
412

            
413
16
        if *self < start {
414
2
            return 0.0;
415
14
        }
416
14
        if *self > end {
417
3
            return 1.0;
418
11
        }
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
11
        if start == end {
424
3
            return 1.0;
425
8
        }
426

            
427
8
        let duration_total = end.duration_since(&start);
428
8
        let duration_current = self.duration_since(&start);
429

            
430
8
        let ratio = duration_current.div(&duration_total);
431
8
        if ratio.is_nan() {
432
2
            return 1.0;
433
6
        }
434
6
        ratio.clamp(0.0, 1.0)
435
16
    }
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
6154
    #[must_use] pub fn add_optional_duration(&self, duration: Option<&Duration>) -> Self {
456
6154
        duration.map_or_else(|| self.clone(), |d| match (self, d) {
457
5636
                (Self::System(i), Duration::System(d)) => {
458
                    #[cfg(feature = "std")]
459
                    {
460
5636
                        let s: StdInstant = i.clone().into();
461
5636
                        let d: StdDuration = (*d).into();
462
5636
                        let new: InstantPtr = (s + d).into();
463
5636
                        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
213
                (Self::Tick(s), Duration::Tick(d)) => Self::Tick(SystemTick {
475
213
                    // Saturate so a runaway tick delta cannot overflow-panic.
476
213
                    tick_counter: s.tick_counter.saturating_add(d.tick_diff),
477
213
                }),
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
2
                    self.add_optional_duration(Some(&Duration::System(
483
2
                        SystemTimeDiff::from_nanos_u128(d.as_nanos()),
484
2
                    )))
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
37
                (Self::Tick(s), Duration::System(_)) => Self::Tick(SystemTick {
490
37
                    tick_counter: s.tick_counter.saturating_add(d.as_ticks()),
491
37
                }),
492
5888
            })
493
6154
    }
494

            
495
    /// Converts to `std::time::Instant` (panics if Tick variant).
496
    #[cfg(feature = "std")]
497
2
    #[must_use] pub fn into_std_instant(self) -> StdInstant {
498
2
        match self {
499
1
            Self::System(s) => s.into(),
500
1
            Self::Tick(_) => unreachable!(),
501
        }
502
1
    }
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
21762
    #[must_use] pub fn duration_since(&self, earlier: &Self) -> Duration {
510
21762
        match (earlier, self) {
511
18418
            (Self::System(prev), Self::System(now)) => {
512
                #[cfg(feature = "std")]
513
                {
514
18418
                    let prev_instant: StdInstant = prev.clone().into();
515
18418
                    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
18418
                    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
3273
                Self::Tick(SystemTick { tick_counter: prev }),
529
3273
                Self::Tick(SystemTick { tick_counter: now }),
530
3273
            ) => Duration::Tick(SystemTickDiff {
531
3273
                // Saturate: a "negative" span (prev > now) clamps to 0.
532
3273
                tick_diff: now.saturating_sub(*prev),
533
3273
            }),
534
            // Mismatched kinds: no meaningful span -> saturate to 0.
535
71
            _ => Duration::Tick(SystemTickDiff { tick_diff: 0 }),
536
        }
537
21762
    }
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)]
545
pub struct SystemTick {
546
    pub tick_counter: u64,
547
}
548

            
549
impl SystemTick {
550
    /// Creates a new tick timestamp from a counter value.
551
18615
    #[must_use] pub const fn new(tick_counter: u64) -> Self {
552
18615
        Self { tick_counter }
553
18615
    }
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)]
560
pub 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

            
577
pub type InstantPtrCloneCallbackType = extern "C" fn(*const InstantPtr) -> InstantPtr;
578
#[repr(C)]
579
pub struct InstantPtrCloneCallback {
580
    pub cb: InstantPtrCloneCallbackType,
581
}
582
impl_callback_simple!(InstantPtrCloneCallback);
583

            
584
pub type InstantPtrDestructorCallbackType = extern "C" fn(*mut InstantPtr);
585
#[repr(C)]
586
pub struct InstantPtrDestructorCallback {
587
    pub cb: InstantPtrDestructorCallbackType,
588
}
589
impl_callback_simple!(InstantPtrDestructorCallback);
590

            
591
// ----  LIBSTD implementation for InstantPtr BEGIN
592
#[cfg(feature = "std")]
593
impl fmt::Debug for InstantPtr {
594
1
    fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
595
1
        write!(f, "{:?}", self.get())
596
1
    }
597
}
598

            
599
#[cfg(not(feature = "std"))]
600
impl 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")]
607
impl 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"))]
614
impl 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")]
621
impl PartialEq for InstantPtr {
622
35
    fn eq(&self, other: &Self) -> bool {
623
35
        self.get() == other.get()
624
35
    }
625
}
626

            
627
#[cfg(not(feature = "std"))]
628
impl PartialEq for InstantPtr {
629
    fn eq(&self, other: &InstantPtr) -> bool {
630
        (self.ptr as usize).eq(&(other.ptr as usize))
631
    }
632
}
633

            
634
impl Eq for InstantPtr {}
635

            
636
#[cfg(feature = "std")]
637
impl PartialOrd for InstantPtr {
638
7
    fn partial_cmp(&self, other: &Self) -> Option<::core::cmp::Ordering> {
639
7
        Some((self.get()).cmp(&(other.get())))
640
7
    }
641
}
642

            
643
#[cfg(not(feature = "std"))]
644
impl 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")]
651
impl Ord for InstantPtr {
652
14980
    fn cmp(&self, other: &Self) -> ::core::cmp::Ordering {
653
14980
        (self.get()).cmp(&(other.get()))
654
14980
    }
655
}
656

            
657
#[cfg(not(feature = "std"))]
658
impl 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")]
665
impl InstantPtr {
666
75521
    fn get(&self) -> StdInstant {
667
75521
        (**self.ptr)
668
75521
    }
669
}
670

            
671
impl Clone for InstantPtr {
672
78199
    fn clone(&self) -> Self {
673
78199
        (self.clone_fn.cb)(self)
674
78199
    }
675
}
676

            
677
#[cfg(feature = "std")]
678
78200
extern "C" fn std_instant_clone(ptr: *const InstantPtr) -> InstantPtr {
679
78200
    let az_instant_ptr = unsafe { &*ptr };
680
78200
    InstantPtr {
681
78200
        ptr: ManuallyDrop::new((*az_instant_ptr.ptr).clone()),
682
78200
        clone_fn: az_instant_ptr.clone_fn,
683
78200
        destructor: az_instant_ptr.destructor,
684
78200
        run_destructor: true,
685
78200
    }
686
78200
}
687

            
688
#[cfg(feature = "std")]
689
impl From<StdInstant> for InstantPtr {
690
94998
    fn from(s: StdInstant) -> Self {
691
94998
        Self {
692
94998
            ptr: ManuallyDrop::new(Box::new(s)),
693
94998
            clone_fn: InstantPtrCloneCallback {
694
94998
                cb: std_instant_clone,
695
94998
            },
696
94998
            destructor: InstantPtrDestructorCallback {
697
94998
                cb: std_instant_drop,
698
94998
            },
699
94998
            run_destructor: true,
700
94998
        }
701
94998
    }
702
}
703

            
704
#[cfg(feature = "std")]
705
impl From<InstantPtr> for StdInstant {
706
42473
    fn from(s: InstantPtr) -> Self {
707
42473
        s.get()
708
42473
    }
709
}
710

            
711
impl Drop for InstantPtr {
712
173198
    fn drop(&mut self) {
713
173198
        if self.run_destructor {
714
173198
            self.run_destructor = false;
715
173198
            (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
173198
            unsafe {
726
173198
                ManuallyDrop::drop(&mut self.ptr);
727
173198
            }
728
        }
729
173198
    }
730
}
731

            
732
#[cfg(feature = "std")]
733
173200
const 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)]
743
pub 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

            
750
impl fmt::Display for Duration {
751
8
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
752
8
        match self {
753
            #[cfg(feature = "std")]
754
5
            Self::System(s) => {
755
5
                let s: StdDuration = (*s).into();
756
5
                write!(f, "{s:?}")
757
            }
758
            #[cfg(not(feature = "std"))]
759
            Duration::System(s) => write!(f, "({}s, {}ns)", s.secs, s.nanos),
760
3
            Self::Tick(tick) => write!(f, "{} ticks", tick.tick_diff),
761
        }
762
8
    }
763
}
764

            
765
#[cfg(feature = "std")]
766
impl From<StdDuration> for Duration {
767
12
    fn from(s: StdDuration) -> Self {
768
12
        Self::System(s.into())
769
12
    }
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`].
777
pub use azul_css::props::basic::time::TICKS_PER_SECOND;
778

            
779
impl 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
3172
    pub const fn as_nanos(&self) -> u128 {
796
3172
        match self {
797
1545
            Self::System(s) => (s.secs as u128) * (NANOS_PER_SEC as u128) + (s.nanos as u128),
798
1627
            Self::Tick(t) => (t.tick_diff as u128) * (NANOS_PER_SEC as u128) / (TICKS_PER_SECOND as u128),
799
        }
800
3172
    }
801

            
802
    /// A wall-clock duration of `ms` whole milliseconds.
803
    #[must_use]
804
25310
    pub const fn from_millis(ms: u64) -> Self {
805
25310
        Self::System(SystemTimeDiff::from_millis(ms))
806
25310
    }
807

            
808
    /// A duration of `ticks` engine frames — the clockless unit, and what the
809
    /// CSS `t` unit becomes.
810
    #[must_use]
811
160
    pub const fn from_ticks(ticks: u64) -> Self {
812
160
        Self::Tick(SystemTickDiff { tick_diff: ticks })
813
160
    }
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
37
    pub const fn as_ticks(&self) -> u64 {
825
37
        match self {
826
            Self::Tick(t) => t.tick_diff,
827
            Self::System(_) => {
828
37
                let ticks = self.as_nanos() * (TICKS_PER_SECOND as u128) / (NANOS_PER_SEC as u128);
829
37
                if ticks > u64::MAX as u128 {
830
1
                    u64::MAX
831
                } else {
832
36
                    ticks as u64
833
                }
834
            }
835
        }
836
37
    }
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
1110
    pub const fn as_millis_u64(&self) -> u64 {
845
1110
        let ms = self.as_nanos() / (NANOS_PER_MILLI as u128);
846
1110
        if ms > u64::MAX as u128 {
847
90
            u64::MAX
848
        } else {
849
1020
            ms as u64
850
        }
851
1110
    }
852

            
853
    /// Returns the maximum possible duration.
854
17
    #[must_use] pub fn max() -> Self {
855
        #[cfg(feature = "std")]
856
        {
857
17
            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
17
    }
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
56442
    #[must_use] pub fn div(&self, other: &Self) -> f32 {
878
        use self::Duration::{System, Tick};
879
56442
        match (self, other) {
880
50551
            (System(s), System(s2)) => s.div(s2) as f32,
881
5820
            (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
71
            _ => (self.as_nanos() as f64 / other.as_nanos() as f64) as f32,
885
        }
886
56442
    }
887

            
888
    /// Returns the smaller of two durations.
889
9
    #[must_use] pub const fn min(self, other: Self) -> Self {
890
9
        if self.smaller_than(&other) {
891
5
            self
892
        } else {
893
4
            other
894
        }
895
9
    }
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
428
    #[must_use] pub const fn greater_than(&self, other: &Self) -> bool {
921
428
        self.as_nanos() > other.as_nanos()
922
428
    }
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
395
    #[must_use] pub const fn smaller_than(&self, other: &Self) -> bool {
929
395
        self.as_nanos() < other.as_nanos()
930
395
    }
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)]
937
pub struct SystemTickDiff {
938
    pub tick_diff: u64,
939
}
940

            
941
impl 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
5826
    #[must_use] pub fn div(&self, other: &Self) -> f64 {
947
5826
        self.tick_diff as f64 / other.tick_diff as f64
948
5826
    }
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)]
954
pub struct SystemTimeDiff {
955
    pub secs: u64,
956
    pub nanos: u32,
957
}
958

            
959
impl SystemTimeDiff {
960
    /// Divide duration A by duration B.
961
    /// Returns `Inf` or `NaN` if `other` is zero.
962
50558
    #[must_use] pub fn div(&self, other: &Self) -> f64 {
963
50558
        self.as_secs_f64() / other.as_secs_f64()
964
50558
    }
965
    // secs (u64) -> f64 loses precision only past 2^53 seconds (~285M years).
966
    #[allow(clippy::cast_precision_loss)]
967
101123
    fn as_secs_f64(&self) -> f64 {
968
101123
        (self.secs as f64) + (f64::from(self.nanos) / f64::from(NANOS_PER_SEC))
969
101123
    }
970
}
971

            
972
#[cfg(feature = "std")]
973
impl From<StdDuration> for SystemTimeDiff {
974
18484
    fn from(d: StdDuration) -> Self {
975
18484
        Self {
976
18484
            secs: d.as_secs(),
977
18484
            nanos: d.subsec_nanos(),
978
18484
        }
979
18484
    }
980
}
981

            
982
#[cfg(feature = "std")]
983
impl From<SystemTimeDiff> for StdDuration {
984
5649
    fn from(d: SystemTimeDiff) -> Self {
985
5649
        Self::new(d.secs, d.nanos)
986
5649
    }
987
}
988

            
989
const MILLIS_PER_SEC: u64 = 1_000;
990
const NANOS_PER_MILLI: u32 = 1_000_000;
991
const NANOS_PER_SEC: u32 = 1_000_000_000;
992

            
993
impl SystemTimeDiff {
994
    /// Creates a duration from whole seconds.
995
12
    #[must_use] pub const fn from_secs(secs: u64) -> Self {
996
12
        Self { secs, nanos: 0 }
997
12
    }
998
    /// Creates a duration from milliseconds.
999
79712
    #[must_use] pub const fn from_millis(millis: u64) -> Self {
79712
        Self {
79712
            secs: millis / MILLIS_PER_SEC,
79712
            nanos: ((millis % MILLIS_PER_SEC) as u32) * NANOS_PER_MILLI,
79712
        }
79712
    }
    /// Creates a duration from nanoseconds.
    // const fn (no const TryFrom); `nanos % NANOS_PER_SEC` is always < 10^9, which
    // fits u32, so the narrowing cast cannot truncate.
    #[allow(clippy::cast_possible_truncation)]
14
    #[must_use] pub const fn from_nanos(nanos: u64) -> Self {
14
        Self {
14
            secs: nanos / (NANOS_PER_SEC as u64),
14
            nanos: (nanos % (NANOS_PER_SEC as u64)) as u32,
14
        }
14
    }
    /// Creates a duration from a `u128` nanosecond count, saturating at the
    /// largest representable duration instead of wrapping.
    ///
    /// Needed because [`Duration::as_nanos`] is `u128`: a tick count near
    /// `u64::MAX` converts to ~3e26 ns, far past what `u64` nanoseconds hold.
    // `nanos % NANOS_PER_SEC` is always < 10^9 and `secs` is range-checked above,
    // so neither narrowing cast can truncate. `as` widenings rather than `From`
    // because this is a `const fn`.
    #[allow(clippy::cast_possible_truncation, clippy::cast_lossless)]
2
    #[must_use] pub const fn from_nanos_u128(nanos: u128) -> Self {
2
        let secs = nanos / (NANOS_PER_SEC as u128);
2
        if secs > u64::MAX as u128 {
            Self {
                secs: u64::MAX,
                nanos: NANOS_PER_SEC - 1,
            }
        } else {
2
            Self {
2
                secs: secs as u64,
2
                nanos: (nanos % (NANOS_PER_SEC as u128)) as u32,
2
            }
        }
2
    }
    /// Adds two durations, returning None on overflow.
21
    #[must_use] pub const fn checked_add(self, rhs: Self) -> Option<Self> {
21
        if let Some(mut secs) = self.secs.checked_add(rhs.secs) {
20
            let mut nanos = self.nanos + rhs.nanos;
20
            if nanos >= NANOS_PER_SEC {
7
                nanos -= NANOS_PER_SEC;
7
                if let Some(new_secs) = secs.checked_add(1) {
6
                    secs = new_secs;
6
                } else {
1
                    return None;
                }
13
            }
19
            Some(Self { secs, nanos })
        } else {
1
            None
        }
21
    }
    /// Returns the total duration in milliseconds.
    ///
    /// Saturates at `u64::MAX` instead of overflow-panicking for enormous
    /// `secs` values (`secs * 1000` overflows around ~1.8e16 seconds).
14
    #[must_use] pub const fn millis(&self) -> u64 {
14
        self.secs
14
            .saturating_mul(MILLIS_PER_SEC)
14
            .saturating_add((self.nanos / NANOS_PER_MILLI) as u64)
14
    }
    /// Converts to `std::time::Duration`.
    #[cfg(feature = "std")]
8
    #[must_use] pub fn get(&self) -> StdDuration {
8
        (*self).into()
8
    }
}
/// Bridge from the CSS-level duration to the engine-level one, preserving the
/// unit.
///
/// This is the join that makes a CSS `5t` mean five FRAMES all the way down to
/// the timer: `ms`/`s` become `Duration::System`, `t` becomes `Duration::Tick`.
/// Collapsing ticks to milliseconds here would put the wall clock back in the
/// path and make "advance exactly 5 ticks, assert the 5th frame flipped"
/// untestable again.
impl From<azul_css::props::basic::time::CssDuration> for Duration {
40
    fn from(d: azul_css::props::basic::time::CssDuration) -> Self {
        use azul_css::props::basic::time::CssDurationUnit;
40
        match d.unit {
20
            CssDurationUnit::Milliseconds => Self::from_millis(u64::from(d.inner)),
20
            CssDurationUnit::Ticks => Self::from_ticks(u64::from(d.inner)),
        }
40
    }
}
impl_option!(
    Instant,
    OptionInstant,
    copy = false,
    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
);
impl_option!(
    Duration,
    OptionDuration,
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
);
#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
/// Message that can be sent from the main thread to the Thread using the `ThreadId`.
///
/// The thread can ignore the event.
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[repr(C, u8)]
pub enum ThreadSendMsg {
    /// The thread should terminate at the nearest
    TerminateThread,
    /// Next frame tick
    Tick,
    /// Custom data
    Custom(RefAny),
}
impl_option!(
    ThreadSendMsg,
    OptionThreadSendMsg,
    copy = false,
    [Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash]
);
/// Channel endpoint for receiving messages from the main thread in a background thread.
///
/// Thread-safe wrapper around the receiver end of a message channel.
#[derive(Debug)]
#[repr(C)]
pub struct ThreadReceiver {
    #[cfg(feature = "std")]
    pub ptr: Box<Arc<Mutex<ThreadReceiverInner>>>,
    #[cfg(not(feature = "std"))]
    pub ptr: *const c_void,
    pub run_destructor: bool,
    /// For FFI: stores the foreign callable (e.g., `PyFunction`)
    pub ctx: OptionRefAny,
}
impl Clone for ThreadReceiver {
1
    fn clone(&self) -> Self {
1
        Self {
1
            ptr: self.ptr.clone(),
1
            run_destructor: true,
1
            ctx: self.ctx.clone(),
1
        }
1
    }
}
impl Drop for ThreadReceiver {
646
    fn drop(&mut self) {
646
        self.run_destructor = false;
646
    }
}
impl ThreadReceiver {
    /// Creates a new receiver (no-op on no_std).
    #[cfg(not(feature = "std"))]
    pub fn new(_t: ThreadReceiverInner) -> Self {
        Self {
            ptr: core::ptr::null(),
            run_destructor: false,
            ctx: OptionRefAny::None,
        }
    }
    /// Creates a new receiver wrapping the inner channel.
    #[cfg(feature = "std")]
709
    #[must_use] pub fn new(t: ThreadReceiverInner) -> Self {
709
        Self {
709
            ptr: Box::new(Arc::new(Mutex::new(t))),
709
            run_destructor: true,
709
            ctx: OptionRefAny::None,
709
        }
709
    }
    /// Get the FFI context (e.g., Python callable)
4
    #[must_use] pub fn get_ctx(&self) -> OptionRefAny {
4
        self.ctx.clone()
4
    }
    /// Receives a message (returns None on no_std).
    #[cfg(not(feature = "std"))]
    pub fn recv(&mut self) -> OptionThreadSendMsg {
        None.into()
    }
    /// Receives a message from the main thread, if available.
    #[cfg(feature = "std")]
275
    pub fn recv(&mut self) -> OptionThreadSendMsg {
275
        let Some(ts) = self.ptr.lock().ok() else {
            return None.into();
        };
275
        (ts.recv_fn.cb)(std::ptr::from_ref(ts.ptr.as_ref()) as *const c_void)
275
    }
}
/// Inner receiver state containing the actual channel and callbacks.
#[derive(Debug)]
#[cfg_attr(not(feature = "std"), derive(PartialEq, PartialOrd, Eq, Ord))]
#[repr(C)]
pub struct ThreadReceiverInner {
    #[cfg(feature = "std")]
    pub ptr: Box<Receiver<ThreadSendMsg>>,
    #[cfg(not(feature = "std"))]
    pub ptr: *const c_void,
    pub recv_fn: ThreadRecvCallback,
    pub destructor: ThreadReceiverDestructorCallback,
}
#[cfg(not(feature = "std"))]
unsafe impl Send for ThreadReceiverInner {}
#[cfg(feature = "std")]
impl core::hash::Hash for ThreadReceiverInner {
    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
        (std::ptr::from_ref(self.ptr.as_ref()) as usize).hash(state);
    }
}
#[cfg(feature = "std")]
impl PartialEq for ThreadReceiverInner {
    fn eq(&self, other: &Self) -> bool {
        std::ptr::eq(self.ptr.as_ref(), other.ptr.as_ref())
    }
}
#[cfg(feature = "std")]
impl Eq for ThreadReceiverInner {}
#[cfg(feature = "std")]
impl PartialOrd for ThreadReceiverInner {
    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
        Some(
            (std::ptr::from_ref(self.ptr.as_ref()) as usize)
                .cmp(&(std::ptr::from_ref(other.ptr.as_ref()) as usize)),
        )
    }
}
#[cfg(feature = "std")]
impl Ord for ThreadReceiverInner {
    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
        (std::ptr::from_ref(self.ptr.as_ref()) as usize).cmp(&(std::ptr::from_ref(other.ptr.as_ref()) as usize))
    }
}
impl Drop for ThreadReceiverInner {
709
    fn drop(&mut self) {
709
        (self.destructor.cb)(self);
709
    }
}
/// Get the current system type, equivalent to `std::time::Instant::now()`, except it
/// also works on systems that don't have a clock (such as embedded timers)
pub type GetSystemTimeCallbackType = extern "C" fn() -> Instant;
#[repr(C)]
pub struct GetSystemTimeCallback {
    pub cb: GetSystemTimeCallbackType,
}
impl_callback_simple!(GetSystemTimeCallback);
/// Default implementation that gets the current system time.
///
/// On WASM targets `std::time::Instant::now()` panics, so we fall back to
/// a zero-tick instant instead.
#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
26679
#[must_use] pub extern "C" fn get_system_time_libstd() -> Instant {
    // Honours the injectable E2E test clock (see TEST_CLOCK_OFFSET_MS).
26679
    std_now_with_test_offset().into()
26679
}
/// Fallback for WASM (where `Instant::now()` panics) and no-std targets.
#[cfg(any(not(feature = "std"), target_arch = "wasm32"))]
pub extern "C" fn get_system_time_libstd() -> Instant {
    Instant::Tick(SystemTick::new(0))
}
/// Callback to check if a thread has finished execution.
pub type CheckThreadFinishedCallbackType =
    extern "C" fn(/* dropcheck */ *const c_void) -> bool;
/// Wrapper for thread completion check callback.
#[repr(C)]
pub struct CheckThreadFinishedCallback {
    pub cb: CheckThreadFinishedCallbackType,
}
impl_callback_simple!(CheckThreadFinishedCallback);
/// Callback to send a message to a background thread.
pub type LibrarySendThreadMsgCallbackType =
    extern "C" fn(/* Sender<ThreadSendMsg> */ *const c_void, ThreadSendMsg) -> bool;
/// Wrapper for thread message send callback.
#[repr(C)]
pub struct LibrarySendThreadMsgCallback {
    pub cb: LibrarySendThreadMsgCallbackType,
}
impl_callback_simple!(LibrarySendThreadMsgCallback);
/// Callback for a running thread to receive messages from the main thread.
pub type ThreadRecvCallbackType =
    extern "C" fn(/* receiver.ptr */ *const c_void) -> OptionThreadSendMsg;
/// Wrapper for thread message receive callback.
#[repr(C)]
pub struct ThreadRecvCallback {
    pub cb: ThreadRecvCallbackType,
}
impl_callback_simple!(ThreadRecvCallback);
/// Callback to destroy a `ThreadReceiver`.
pub type ThreadReceiverDestructorCallbackType = extern "C" fn(*mut ThreadReceiverInner);
/// Wrapper for thread receiver destructor callback.
#[repr(C)]
pub struct ThreadReceiverDestructorCallback {
    pub cb: ThreadReceiverDestructorCallbackType,
}
impl_callback_simple!(ThreadReceiverDestructorCallback);
#[cfg(test)]
#[allow(clippy::float_cmp)] // exact-value assertions on interpolation results
mod tests {
    use super::*;
15
    fn tick(n: u64) -> Instant {
15
        Instant::Tick(SystemTick::new(n))
15
    }
19
    fn tick_dur(n: u64) -> Duration {
19
        Duration::Tick(SystemTickDiff { tick_diff: n })
19
    }
10
    fn sys_dur(secs: u64, nanos: u32) -> Duration {
10
        Duration::System(SystemTimeDiff { secs, nanos })
10
    }
    /// The property the parallel E2E runner depends on: a `tick_ms` on one
    /// thread must not shift any other thread's clock. This is what allows a
    /// scenario that ticks to run in parallel with every other scenario
    /// instead of being serialised behind a process-global offset.
    #[test]
    #[cfg(feature = "std")]
1
    fn test_clock_offset_is_per_thread_not_process_global() {
1
        reset_test_clock();
1
        assert_eq!(test_clock_offset_ms(), 0);
1
        let (tx, rx) = std::sync::mpsc::channel();
1
        let (go_tx, go_rx) = std::sync::mpsc::channel::<()>();
1
        let other = std::thread::spawn(move || {
            // Observed AFTER the main thread has advanced its own clock by 5 s.
1
            go_rx.recv().expect("handshake");
1
            let seen_after_main_ticked = test_clock_offset_ms();
1
            let _ = advance_test_clock_ms(7);
1
            tx.send((seen_after_main_ticked, test_clock_offset_ms()))
1
                .expect("send");
1
        });
1
        assert_eq!(advance_test_clock_ms(5_000), 5_000);
1
        go_tx.send(()).expect("handshake");
1
        let (other_before, other_after) = rx.recv().expect("recv");
1
        other.join().expect("join");
1
        assert_eq!(
            other_before, 0,
            "a tick on the main thread leaked into another thread's clock"
        );
1
        assert_eq!(other_after, 7, "the other thread must own its own offset");
1
        assert_eq!(
1
            test_clock_offset_ms(),
            5_000,
            "another thread's tick leaked into the main thread's clock"
        );
        // And a reset really is a reset (worker threads are reused).
1
        reset_test_clock();
1
        assert_eq!(test_clock_offset_ms(), 0);
1
    }
    /// A frozen clock must advance ONLY by what a scenario asks for.
    ///
    /// Offsetting alone left `Instant::now()` as `StdInstant::now() + offset`, so
    /// real time still flowed: elapsed time was what the scenario asked for PLUS
    /// whatever the machine happened to spend. Under the 8-wide E2E runner that
    /// second term is large and varies per run — enough to flip an assertion on a
    /// blinking caret's phase while the same scenario passes in isolation.
    #[test]
    #[cfg(feature = "std")]
1
    fn a_frozen_clock_advances_only_by_what_the_scenario_asks_for() {
1
        reset_test_clock();
1
        assert!(!test_clock_is_frozen());
1
        freeze_test_clock();
1
        assert!(test_clock_is_frozen());
1
        let t0 = Instant::now();
        // Burn REAL time. A frozen clock must not notice.
1
        std::thread::sleep(core::time::Duration::from_millis(25));
1
        let t1 = Instant::now();
1
        assert_eq!(
1
            t1.duration_since(&t0),
            Duration::System(SystemTimeDiff { secs: 0, nanos: 0 }),
            "real time leaked into a frozen clock",
        );
        // Only an explicit advance moves it, and by exactly that much.
1
        let _ = advance_test_clock_ms(500);
1
        let t2 = Instant::now();
1
        assert_eq!(
1
            t2.duration_since(&t0),
            Duration::System(SystemTimeDiff { secs: 0, nanos: 500_000_000 }),
            "a 500 ms tick must read back as exactly 500 ms",
        );
        // Freezing is idempotent: it must not re-base and lose the offset.
1
        freeze_test_clock();
1
        assert_eq!(
1
            Instant::now().duration_since(&t0),
            Duration::System(SystemTimeDiff { secs: 0, nanos: 500_000_000 }),
            "re-freezing re-based the clock and discarded elapsed virtual time",
        );
        // A reset must unfreeze, or the next scenario on this reused worker
        // thread would start with time stopped.
1
        reset_test_clock();
1
        assert!(!test_clock_is_frozen());
1
        let r0 = Instant::now();
1
        std::thread::sleep(core::time::Duration::from_millis(15));
1
        assert!(
1
            Instant::now().duration_since(&r0)
1
                > Duration::System(SystemTimeDiff { secs: 0, nanos: 0 }),
            "reset_test_clock left the clock frozen",
        );
1
    }
    #[test]
1
    fn linear_interpolate_zero_interval_is_one_not_nan() {
1
        let t = tick(5);
1
        let v = t.linear_interpolate(tick(5), tick(5));
1
        assert!(v.is_finite());
1
        assert_eq!(v, 1.0);
1
    }
    #[test]
1
    fn linear_interpolate_midpoint() {
1
        let v = tick(5).linear_interpolate(tick(0), tick(10));
1
        assert!((v - 0.5).abs() < 1e-6);
1
    }
    #[test]
1
    fn duration_since_saturates_on_negative() {
        // earlier is actually later -> saturate to zero, no panic.
1
        let d = tick(1).duration_since(&tick(10));
1
        assert_eq!(d, tick_dur(0));
1
    }
    /// Cross-unit comparison must answer TRUTHFULLY, not saturate to `false`.
    ///
    /// The saturating version made a unit mismatch a permanent silent "not yet":
    /// every interval constant in the engine is a `Duration::System`, so a Tick
    /// elapsed value compared against one never expired and the UI simply stopped
    /// animating, with nothing to catch.
    #[test]
1
    fn duration_compare_is_unit_aware_across_ticks_and_wall_clock() {
        // 5 ticks at 60Hz is ~83ms, i.e. LESS than one second.
1
        let five_ticks = tick_dur(5);
1
        let one_second = sys_dur(1, 0);
1
        assert!(five_ticks.smaller_than(&one_second));
1
        assert!(!five_ticks.greater_than(&one_second));
1
        assert!(one_second.greater_than(&five_ticks));
1
        assert!(!one_second.smaller_than(&five_ticks));
        // 120 ticks is two seconds, i.e. MORE than one second.
1
        assert!(tick_dur(120).greater_than(&one_second));
1
        assert!(one_second.smaller_than(&tick_dur(120)));
        // Exactly 60 ticks IS one second: neither greater nor smaller.
1
        assert!(!tick_dur(60).greater_than(&one_second));
1
        assert!(!tick_dur(60).smaller_than(&one_second));
1
        assert!(!one_second.greater_than(&tick_dur(60)));
1
        assert!(!one_second.smaller_than(&tick_dur(60)));
1
    }
    /// `Duration::max()` is `System`, and it must still dominate every tick count
    /// — including `u64::MAX` ticks, which is a bigger *number* but a smaller
    /// span.
    #[test]
1
    fn duration_compare_across_units_at_the_extremes() {
1
        assert!(Duration::max().greater_than(&tick_dur(u64::MAX)));
1
        assert!(tick_dur(u64::MAX).smaller_than(&Duration::max()));
1
        assert!(!tick_dur(0).greater_than(&sys_dur(0, 0)));
1
        assert!(!sys_dur(0, 0).greater_than(&tick_dur(0)));
        // One nanosecond beats zero ticks.
1
        assert!(sys_dur(0, 1).greater_than(&tick_dur(0)));
1
    }
    #[test]
1
    fn add_optional_duration_converts_across_units() {
1
        let inst = tick(100);
        // A System duration on a Tick instant advances by WHOLE ticks: 1s = 60.
1
        assert_eq!(inst.add_optional_duration(Some(&sys_dur(1, 0))), tick(160));
        // Sub-frame durations advance nothing — one frame is the resolution.
1
        assert_eq!(inst.add_optional_duration(Some(&sys_dur(0, 1))), tick(100));
        // Matching kinds add and saturate.
1
        assert_eq!(inst.add_optional_duration(Some(&tick_dur(5))), tick(105));
        // Saturating add: near-max tick doesn't overflow-panic.
1
        let big = tick(u64::MAX);
1
        assert_eq!(big.add_optional_duration(Some(&tick_dur(10))), tick(u64::MAX));
        // ...and the cross-unit arm saturates too.
1
        assert_eq!(big.add_optional_duration(Some(&Duration::max())), tick(u64::MAX));
1
    }
    #[test]
1
    fn millis_saturates_on_overflow() {
1
        let huge = SystemTimeDiff { secs: u64::MAX, nanos: 0 };
1
        assert_eq!(huge.millis(), u64::MAX);
1
        let normal = SystemTimeDiff { secs: 2, nanos: 500_000_000 };
1
        assert_eq!(normal.millis(), 2500);
1
    }
    /// Cross-unit division goes through the canonical scale. Returning `0.0` (the
    /// old behaviour) reads downstream as "this animation is at 0% progress",
    /// i.e. a frozen animation that never reports an error.
    #[test]
1
    fn duration_div_is_unit_aware() {
        // 30 ticks is half a second.
1
        assert!((tick_dur(30).div(&sys_dur(1, 0)) - 0.5).abs() < 1e-6);
        // ...and one second is two lots of 30 ticks.
1
        assert!((sys_dur(1, 0).div(&tick_dur(30)) - 2.0).abs() < 1e-6);
        // Matching Tick kinds divide normally.
1
        assert!((tick_dur(5).div(&tick_dur(10)) - 0.5).abs() < 1e-6);
        // Matching System kinds divide normally.
1
        assert!((sys_dur(1, 0).div(&sys_dur(2, 0)) - 0.5).abs() < 1e-6);
1
    }
    // Exercises the `unsafe` pointer work in `std_instant_clone` (`&*ptr`) and
    // the `ManuallyDrop::drop` guard in `InstantPtr::drop`: build an InstantPtr,
    // clone it (goes through the FFI clone callback + raw-ptr deref), then let
    // both drop. Under Miri this asserts the clone/drop path is UB-free and the
    // owned `Box` is freed exactly once per value (no double-free).
    #[cfg(feature = "std")]
    #[test]
1
    fn instant_ptr_clone_and_drop_no_ub() {
1
        let base = StdInstant::now();
1
        let a: InstantPtr = base.into();
1
        let b = a.clone();
        // The clone must observe the same underlying instant.
1
        assert_eq!(a, b);
        // Both `a` and `b` own independent Boxes; dropping both must not
        // double-free (each has run_destructor == true).
1
        drop(a);
1
        drop(b);
1
    }
}
#[cfg(test)]
#[allow(clippy::float_cmp)] // exact-value assertions on ratios / interpolation results
mod autotest_generated {
    use super::*;
    // ---- helpers -----------------------------------------------------------
    fn tick(n: u64) -> Instant {
        Instant::Tick(SystemTick::new(n))
    }
    fn tick_dur(n: u64) -> Duration {
        Duration::Tick(SystemTickDiff { tick_diff: n })
    }
    fn sys_dur(secs: u64, nanos: u32) -> Duration {
        Duration::System(SystemTimeDiff { secs, nanos })
    }
    // ========================================================================
    // TimerId::unique / ThreadId::unique  (monotonic, never hits reserved IDs)
    // ========================================================================
    #[test]
    fn timer_id_unique_is_strictly_increasing_and_above_reserved_range() {
        let a = TimerId::unique();
        let b = TimerId::unique();
        assert_ne!(a, b);
        assert!(b.id > a.id, "unique() must strictly increase: {a:?} -> {b:?}");
        // User IDs must never land inside the reserved system-timer block.
        for id in [a, b] {
            assert!(
                id.id >= USER_TIMER_ID_START,
                "unique() handed out a reserved system ID: {id:?}"
            );
            assert_ne!(id, CURSOR_BLINK_TIMER_ID);
            assert_ne!(id, SCROLL_MOMENTUM_TIMER_ID);
            assert_ne!(id, DRAG_AUTOSCROLL_TIMER_ID);
            assert_ne!(id, TOOLTIP_DELAY_TIMER_ID);
            assert_ne!(id, CAPABILITY_PUMP_TIMER_ID);
            assert_ne!(id, LONG_PRESS_TIMER_ID);
        }
    }
    #[test]
    fn thread_id_unique_is_strictly_increasing_and_above_reserved_range() {
        let a = ThreadId::unique();
        let b = ThreadId::unique();
        assert_ne!(a, b);
        assert!(b.id > a.id);
        assert!(a.id >= RESERVED_THREAD_ID_COUNT);
    }
    // The counters are `AtomicUsize` + `fetch_add`, so concurrent callers must
    // never be handed the same ID. 8 threads x 64 IDs => 512 distinct values.
    #[cfg(feature = "std")]
    #[test]
    fn unique_ids_do_not_collide_across_threads() {
        use alloc::collections::BTreeSet;
        let handles: Vec<_> = (0..8)
            .map(|_| {
                std::thread::spawn(|| {
                    let mut out = Vec::new();
                    for _ in 0..64 {
                        out.push((TimerId::unique().id, ThreadId::unique().id));
                    }
                    out
                })
            })
            .collect();
        let mut timer_ids = BTreeSet::new();
        let mut thread_ids = BTreeSet::new();
        for h in handles {
            for (t, th) in h.join().expect("worker thread panicked") {
                assert!(timer_ids.insert(t), "duplicate TimerId handed out: {t}");
                assert!(thread_ids.insert(th), "duplicate ThreadId handed out: {th}");
            }
        }
        assert_eq!(timer_ids.len(), 8 * 64);
        assert_eq!(thread_ids.len(), 8 * 64);
    }
    // ========================================================================
    // Instant::now / get_system_time_libstd
    // ========================================================================
    #[cfg(feature = "std")]
    #[test]
    fn instant_now_is_system_and_monotonic() {
        let a = Instant::now();
        let b = Instant::now();
        assert!(matches!(a, Instant::System(_)));
        assert!(a <= b, "Instant::now() went backwards");
        // A later instant is never "before" an earlier one.
        assert_eq!(a.duration_since(&b), sys_dur(0, 0));
    }
    #[cfg(all(feature = "std", not(target_arch = "wasm32")))]
    #[test]
    fn get_system_time_libstd_is_monotonic_system_instant() {
        let a = get_system_time_libstd();
        let b = get_system_time_libstd();
        assert!(matches!(a, Instant::System(_)));
        assert!(matches!(b, Instant::System(_)));
        assert!(a <= b);
    }
    #[cfg(any(not(feature = "std"), target_arch = "wasm32"))]
    #[test]
    fn get_system_time_libstd_wasm_fallback_is_zero_tick() {
        // On WASM / no_std `StdInstant::now()` would panic, so the fallback must
        // hand back a tick instant instead of exploding.
        assert_eq!(get_system_time_libstd(), tick(0));
    }
    // ========================================================================
    // Instant::linear_interpolate  (must never return NaN / escape [0.0, 1.0])
    // ========================================================================
    #[test]
    fn linear_interpolate_clamps_outside_the_interval() {
        // before start -> 0.0, after end -> 1.0 (never negative / >1).
        assert_eq!(tick(0).linear_interpolate(tick(10), tick(20)), 0.0);
        assert_eq!(tick(999).linear_interpolate(tick(10), tick(20)), 1.0);
        // exactly on the boundaries
        assert_eq!(tick(10).linear_interpolate(tick(10), tick(20)), 0.0);
        assert_eq!(tick(20).linear_interpolate(tick(10), tick(20)), 1.0);
    }
    #[test]
    fn linear_interpolate_reversed_interval_is_normalized() {
        // `end < start` is swapped internally, so the ratio is the same as the
        // correctly-ordered call rather than a garbage / negative value.
        let forwards = tick(5).linear_interpolate(tick(0), tick(10));
        let backwards = tick(5).linear_interpolate(tick(10), tick(0));
        assert_eq!(forwards, backwards);
        assert!((backwards - 0.5).abs() < 1e-6);
    }
    #[test]
    fn linear_interpolate_saturating_extremes_stay_in_range() {
        // Full u64 span: the tick diff hits u64::MAX and the f64->f32 narrowing
        // must not produce inf/NaN.
        let v = tick(u64::MAX / 2).linear_interpolate(tick(0), tick(u64::MAX));
        assert!(v.is_finite(), "interpolation over the full u64 span went non-finite");
        assert!((0.0..=1.0).contains(&v));
        assert!((v - 0.5).abs() < 1e-3, "expected ~0.5, got {v}");
        // Degenerate zero-length interval at the extremes -> 1.0, not 0/0 = NaN.
        let z = tick(u64::MAX).linear_interpolate(tick(u64::MAX), tick(u64::MAX));
        assert_eq!(z, 1.0);
        let z0 = tick(0).linear_interpolate(tick(0), tick(0));
        assert_eq!(z0, 1.0);
    }
    #[cfg(feature = "std")]
    #[test]
    fn linear_interpolate_mismatched_kinds_never_nan() {
        // Every mismatched (System / Tick) permutation feeds a 0/0 division
        // internally; the guard must turn that into a finite value in [0, 1].
        let sys = Instant::now();
        let cases = [
            (tick(5), sys.clone(), tick(10)),
            (sys.clone(), tick(0), tick(10)),
            (tick(5), tick(0), sys.clone()),
            (sys.clone(), sys.clone(), tick(10)),
            (tick(5), sys.clone(), sys.clone()),
        ];
        for (this, start, end) in cases {
            let v = this.linear_interpolate(start, end);
            assert!(v.is_finite(), "mismatched-kind interpolation returned {v}");
            assert!(
                (0.0..=1.0).contains(&v),
                "mismatched-kind interpolation escaped [0,1]: {v}"
            );
        }
    }
    // ========================================================================
    // Instant::add_optional_duration
    // ========================================================================
    #[test]
    fn add_optional_duration_none_is_identity() {
        let t = tick(42);
        assert_eq!(t.add_optional_duration(None), t);
        assert_eq!(tick(u64::MAX).add_optional_duration(None), tick(u64::MAX));
    }
    #[test]
    fn add_optional_duration_tick_saturates_at_u64_max() {
        // saturating_add: u64::MAX-1 + huge must clamp, not wrap or panic.
        let near_max = tick(u64::MAX - 1);
        assert_eq!(
            near_max.add_optional_duration(Some(&tick_dur(u64::MAX))),
            tick(u64::MAX)
        );
        assert_eq!(tick(0).add_optional_duration(Some(&tick_dur(0))), tick(0));
    }
    #[cfg(feature = "std")]
    #[test]
    fn add_optional_duration_system_advances_by_the_duration() {
        let base = Instant::now();
        let later = base.add_optional_duration(Some(&Duration::System(SystemTimeDiff::from_secs(1))));
        assert!(later > base);
        let delta = later.duration_since(&base);
        assert_eq!(delta, sys_dur(1, 0));
        // ... and the reverse span saturates to zero rather than going negative.
        assert_eq!(base.duration_since(&later), sys_dur(0, 0));
    }
    /// A `Tick` interval on a wall-clock instant has to ADVANCE that instant, not
    /// leave it alone. `Timer::instant_of_next_run` is exactly `last_run +
    /// delay + interval`; when this returned `self`, a tick-unit timer's next run
    /// was always "now" — permanently overdue, and `time_until_next_timer_ms`
    /// answered `Some(0)` for it.
    #[cfg(feature = "std")]
    #[test]
    fn add_optional_duration_converts_between_units_in_both_directions() {
        let sys = Instant::now();
        // System instant + Tick duration: 60 ticks is exactly one second.
        let later = sys.add_optional_duration(Some(&tick_dur(60)));
        assert!(later > sys, "a tick interval must advance a wall-clock instant");
        assert_eq!(later.duration_since(&sys), sys_dur(1, 0));
        // 0 ticks is genuinely no time at all.
        assert_eq!(sys.add_optional_duration(Some(&tick_dur(0))), sys);
        // Tick instant + System duration: 3s is 180 whole frames.
        assert_eq!(tick(7).add_optional_duration(Some(&sys_dur(3, 0))), tick(187));
    }
    // A `System` instant plus an enormous `System` duration overflows the
    // platform clock representation: `StdInstant + StdDuration` panics with
    // "overflow when adding duration to instant". Unlike the mismatched-kind
    // case (documented to saturate), this arm has no guard -- characterized
    // here so a future saturating fix flips this test loudly.
    #[cfg(all(feature = "std", not(target_arch = "wasm32")))]
    #[test]
    #[should_panic(expected = "overflow")]
    fn add_optional_duration_system_overflow_panics_today() {
        let base = Instant::now();
        let _ = base.add_optional_duration(Some(&Duration::max()));
    }
    // ========================================================================
    // Instant::duration_since / into_std_instant
    // ========================================================================
    #[test]
    fn duration_since_tick_saturates_and_is_exact() {
        assert_eq!(tick(10).duration_since(&tick(4)), tick_dur(6));
        // self == earlier -> zero span
        assert_eq!(tick(10).duration_since(&tick(10)), tick_dur(0));
        // earlier is later -> saturate to zero, no underflow panic
        assert_eq!(tick(0).duration_since(&tick(u64::MAX)), tick_dur(0));
        // full-range span does not overflow
        assert_eq!(tick(u64::MAX).duration_since(&tick(0)), tick_dur(u64::MAX));
    }
    #[cfg(feature = "std")]
    #[test]
    fn duration_since_mismatched_kinds_is_zero_tick_both_directions() {
        let sys = Instant::now();
        assert_eq!(sys.duration_since(&tick(5)), tick_dur(0));
        assert_eq!(tick(5).duration_since(&sys), tick_dur(0));
    }
    #[cfg(feature = "std")]
    #[test]
    fn into_std_instant_round_trips_a_system_instant() {
        let base = StdInstant::now();
        let wrapped: Instant = base.into();
        assert_eq!(wrapped.into_std_instant(), base);
    }
    #[cfg(feature = "std")]
    #[test]
    #[should_panic(expected = "internal error: entered unreachable code")]
    fn into_std_instant_on_tick_variant_panics() {
        // Documented: `into_std_instant` is `unreachable!()` for Tick instants.
        let _ = tick(1).into_std_instant();
    }
    // ========================================================================
    // SystemTick::new
    // ========================================================================
    #[test]
    fn system_tick_new_stores_the_counter_verbatim() {
        for n in [0_u64, 1, 0x0100, u64::MAX / 2, u64::MAX] {
            assert_eq!(SystemTick::new(n).tick_counter, n);
        }
        // Ordering follows the counter (used by Instant's derived Ord).
        assert!(SystemTick::new(0) < SystemTick::new(u64::MAX));
        assert_eq!(SystemTick::new(7), SystemTick::new(7));
    }
    // ========================================================================
    // InstantPtr: get / std_instant_clone / std_instant_drop
    // ========================================================================
    #[cfg(feature = "std")]
    #[test]
    fn instant_ptr_get_returns_the_wrapped_instant() {
        let base = StdInstant::now();
        let p: InstantPtr = base.into();
        assert_eq!(p.get(), base);
        // `get` is a copy, not a move: repeated reads stay stable.
        assert_eq!(p.get(), p.get());
        assert!(p.run_destructor);
        // Debug must not panic and must not be empty.
        assert!(!alloc::format!("{p:?}").is_empty());
    }
    #[cfg(feature = "std")]
    #[test]
    fn std_instant_clone_deep_copies_and_arms_the_destructor() {
        let base = StdInstant::now();
        let a: InstantPtr = base.into();
        let cloned = std_instant_clone(core::ptr::from_ref(&a));
        assert_eq!(cloned.get(), base);
        // The clone owns its OWN box (freeing both must not double-free).
        assert!(!core::ptr::eq(&**a.ptr, &**cloned.ptr));
        assert!(cloned.run_destructor, "clone handed back a disarmed destructor");
        drop(cloned);
        // The source survives its clone being dropped.
        assert_eq!(a.get(), base);
    }
    #[cfg(feature = "std")]
    #[test]
    fn std_instant_drop_is_a_noop_even_for_null() {
        // The libstd destructor callback is deliberately empty: the Box is freed
        // by `InstantPtr::drop` under the `run_destructor` guard. Calling it with
        // a null pointer must therefore be harmless.
        std_instant_drop(core::ptr::null_mut());
        let mut p: InstantPtr = StdInstant::now().into();
        let before = p.get();
        std_instant_drop(core::ptr::from_mut(&mut p));
        // Value is untouched and still owned afterwards.
        assert_eq!(p.get(), before);
        assert!(p.run_destructor);
    }
    // ========================================================================
    // Duration::fmt (Display)
    // ========================================================================
    #[test]
    fn duration_display_tick_edge_values() {
        assert_eq!(alloc::format!("{}", tick_dur(0)), "0 ticks");
        assert_eq!(alloc::format!("{}", tick_dur(1)), "1 ticks");
        assert_eq!(
            alloc::format!("{}", tick_dur(u64::MAX)),
            "18446744073709551615 ticks"
        );
    }
    #[cfg(feature = "std")]
    #[test]
    fn duration_display_system_edge_values_do_not_panic() {
        // zero, sub-second, denormalized nanos and the absolute maximum all have
        // to format without panicking and without producing an empty string.
        for d in [
            sys_dur(0, 0),
            sys_dur(1, 500_000_000),
            sys_dur(0, u32::MAX),
            sys_dur(u64::MAX, NANOS_PER_SEC - 1),
            Duration::max(),
        ] {
            let s = alloc::format!("{d}");
            assert!(!s.is_empty());
            assert!(!s.ends_with("ticks"), "System duration formatted as ticks: {s}");
        }
    }
    // ========================================================================
    // Duration::max / div / min / greater_than / smaller_than
    // ========================================================================
    #[cfg(feature = "std")]
    #[test]
    fn duration_max_is_the_upper_bound() {
        let m = Duration::max();
        assert_eq!(m, sys_dur(u64::MAX, NANOS_PER_SEC - 1));
        // Nothing of the same kind is greater than it...
        assert!(m.greater_than(&sys_dur(u64::MAX, NANOS_PER_SEC - 2)));
        assert!(m.greater_than(&sys_dur(0, 0)));
        // ... and it is not greater/smaller than itself.
        assert!(!m.greater_than(&m));
        assert!(!m.smaller_than(&m));
        // Converting the maximum back to std must not overflow-panic.
        let Duration::System(inner) = m else {
            panic!("Duration::max() is not a System duration under std")
        };
        assert_eq!(inner.get(), StdDuration::new(u64::MAX, NANOS_PER_SEC - 1));
    }
    #[test]
    fn duration_div_by_zero_yields_inf_or_nan_not_a_panic() {
        // 0/0 -> NaN, x/0 -> +inf. Neither may panic.
        assert!(tick_dur(0).div(&tick_dur(0)).is_nan());
        let inf = tick_dur(5).div(&tick_dur(0));
        assert!(inf.is_infinite() && inf.is_sign_positive());
        assert!(sys_dur(0, 0).div(&sys_dur(0, 0)).is_nan());
        let sinf = sys_dur(1, 0).div(&sys_dur(0, 0));
        assert!(sinf.is_infinite() && sinf.is_sign_positive());
    }
    #[test]
    fn duration_div_extremes_stay_finite_in_f32() {
        // u64::MAX / 1 ~= 1.8e19, comfortably inside f32 range: the f64 -> f32
        // narrowing must not produce inf.
        let r = tick_dur(u64::MAX).div(&tick_dur(1));
        assert!(r.is_finite(), "u64::MAX tick ratio overflowed f32: {r}");
        assert!(r > 1e19);
        // Identity ratios are exactly 1.0 for both kinds.
        assert_eq!(tick_dur(u64::MAX).div(&tick_dur(u64::MAX)), 1.0);
        assert_eq!(sys_dur(3, 0).div(&sys_dur(2, 0)), 1.5);
    }
    /// Cross-unit division converts instead of collapsing to `0.0`. 10 ticks is
    /// one sixth of a second, so the two ratios are reciprocals of each other —
    /// which is the property `0.0` both ways could never satisfy.
    #[test]
    fn duration_div_across_kinds_converts_both_ways() {
        assert!((sys_dur(1, 0).div(&tick_dur(10)) - 6.0).abs() < 1e-5);
        assert!((tick_dur(10).div(&sys_dur(1, 0)) - (1.0 / 6.0)).abs() < 1e-5);
    }
    #[test]
    fn duration_min_picks_the_smaller_of_the_same_kind() {
        assert_eq!(tick_dur(5).min(tick_dur(10)), tick_dur(5));
        assert_eq!(tick_dur(10).min(tick_dur(5)), tick_dur(5));
        assert_eq!(tick_dur(7).min(tick_dur(7)), tick_dur(7));
        assert_eq!(tick_dur(0).min(tick_dur(u64::MAX)), tick_dur(0));
        // Comparison no longer needs std: it is u128 nanosecond arithmetic.
        assert_eq!(sys_dur(1, 0).min(sys_dur(1, 1)), sys_dur(1, 0));
    }
    /// `min` is built on `smaller_than`, so it picks the genuinely shorter span
    /// across units and is COMMUTATIVE. It used to just return `other` whenever
    /// the units differed — so `a.min(b)` and `b.min(a)` disagreed, and the
    /// answer depended on argument order rather than on the durations.
    #[test]
    fn duration_min_across_kinds_picks_the_genuinely_shorter_span() {
        // 5 ticks is ~83ms, so it is shorter than a second either way round.
        assert_eq!(tick_dur(5).min(sys_dur(1, 0)), tick_dur(5));
        assert_eq!(sys_dur(1, 0).min(tick_dur(5)), tick_dur(5));
        // ...and 120 ticks is 2s, so the second wins either way round.
        assert_eq!(tick_dur(120).min(sys_dur(1, 0)), sys_dur(1, 0));
        assert_eq!(sys_dur(1, 0).min(tick_dur(120)), sys_dur(1, 0));
    }
    #[test]
    fn duration_comparison_is_a_strict_total_order_within_a_kind() {
        let mut pairs = alloc::vec![(tick_dur(0), tick_dur(u64::MAX)), (tick_dur(1), tick_dur(2))];
        // System ordering no longer needs std: the comparison is u128 nanosecond
        // arithmetic. It used to defer to `StdDuration`, so on no_std it answered
        // `false` for every System pair — a total order that ordered nothing.
        pairs.extend_from_slice(&[
            (sys_dur(0, 0), sys_dur(u64::MAX, 0)),
            (sys_dur(1, 999_999_999), sys_dur(2, 0)),
        ]);
        for (a, b) in pairs {
            assert!(a.smaller_than(&b));
            assert!(b.greater_than(&a));
            assert!(!a.greater_than(&b));
            assert!(!b.smaller_than(&a));
        }
        // Equal values: neither greater nor smaller (holds for both kinds).
        let eq = tick_dur(4);
        assert!(!eq.greater_than(&eq));
        assert!(!eq.smaller_than(&eq));
        let eq_sys = sys_dur(4, 2);
        assert!(!eq_sys.greater_than(&eq_sys));
        assert!(!eq_sys.smaller_than(&eq_sys));
    }
    #[cfg(feature = "std")]
    #[test]
    fn duration_comparison_normalizes_denormalized_nanos() {
        // nanos == u32::MAX (> 1e9) is denormalized; the std conversion carries
        // it into secs, so {0, u32::MAX} == 4.294967295s > 4s.
        let denorm = sys_dur(0, u32::MAX);
        assert!(denorm.greater_than(&sys_dur(4, 0)));
        assert!(denorm.smaller_than(&sys_dur(5, 0)));
    }
    // ========================================================================
    // SystemTickDiff::div / SystemTimeDiff::div + as_secs_f64
    // ========================================================================
    #[test]
    fn system_tick_diff_div_edge_cases() {
        let zero = SystemTickDiff { tick_diff: 0 };
        let one = SystemTickDiff { tick_diff: 1 };
        let max = SystemTickDiff { tick_diff: u64::MAX };
        assert!(zero.div(&zero).is_nan());
        assert!(one.div(&zero).is_infinite());
        assert_eq!(zero.div(&one), 0.0);
        assert_eq!(max.div(&max), 1.0);
        assert!(max.div(&one).is_finite());
        assert_eq!(SystemTickDiff { tick_diff: 5 }.div(&SystemTickDiff { tick_diff: 10 }), 0.5);
    }
    #[test]
    fn system_time_diff_as_secs_f64_is_exact_for_representable_values() {
        assert_eq!(SystemTimeDiff { secs: 0, nanos: 0 }.as_secs_f64(), 0.0);
        assert_eq!(SystemTimeDiff { secs: 1, nanos: 500_000_000 }.as_secs_f64(), 1.5);
        assert_eq!(SystemTimeDiff { secs: 0, nanos: 500_000_000 }.as_secs_f64(), 0.5);
        // Extremes stay finite (u64::MAX secs ~= 1.8e19, well inside f64).
        let huge = SystemTimeDiff { secs: u64::MAX, nanos: NANOS_PER_SEC - 1 };
        assert!(huge.as_secs_f64().is_finite());
        assert!(huge.as_secs_f64() > 1e19);
        // Monotone in secs.
        assert!(
            SystemTimeDiff::from_secs(2).as_secs_f64() > SystemTimeDiff::from_secs(1).as_secs_f64()
        );
    }
    #[test]
    fn system_time_diff_div_edge_cases() {
        let zero = SystemTimeDiff { secs: 0, nanos: 0 };
        let one = SystemTimeDiff::from_secs(1);
        let half = SystemTimeDiff { secs: 0, nanos: 500_000_000 };
        assert!(zero.div(&zero).is_nan());
        assert!(one.div(&zero).is_infinite());
        assert_eq!(zero.div(&one), 0.0);
        assert_eq!(one.div(&one), 1.0);
        assert_eq!(one.div(&half), 2.0);
        let max = SystemTimeDiff { secs: u64::MAX, nanos: NANOS_PER_SEC - 1 };
        assert_eq!(max.div(&max), 1.0);
        assert!(max.div(&one).is_finite());
    }
    // ========================================================================
    // SystemTimeDiff constructors: from_secs / from_millis / from_nanos
    // ========================================================================
    #[test]
    fn from_secs_invariants() {
        for s in [0_u64, 1, 1_000, u64::MAX] {
            let d = SystemTimeDiff::from_secs(s);
            assert_eq!(d.secs, s);
            assert_eq!(d.nanos, 0, "from_secs must leave nanos at zero");
        }
    }
    #[test]
    fn from_millis_normalizes_and_keeps_nanos_in_range() {
        assert_eq!(SystemTimeDiff::from_millis(0), SystemTimeDiff { secs: 0, nanos: 0 });
        assert_eq!(
            SystemTimeDiff::from_millis(999),
            SystemTimeDiff { secs: 0, nanos: 999_000_000 }
        );
        assert_eq!(SystemTimeDiff::from_millis(1_000), SystemTimeDiff { secs: 1, nanos: 0 });
        assert_eq!(
            SystemTimeDiff::from_millis(1_500),
            SystemTimeDiff { secs: 1, nanos: 500_000_000 }
        );
        // u64::MAX millis must not overflow the u32 nanos field.
        let max = SystemTimeDiff::from_millis(u64::MAX);
        assert!(max.nanos < NANOS_PER_SEC, "from_millis produced denormalized nanos");
        assert_eq!(max.secs, u64::MAX / MILLIS_PER_SEC);
    }
    #[test]
    fn from_nanos_normalizes_and_keeps_nanos_in_range() {
        assert_eq!(SystemTimeDiff::from_nanos(0), SystemTimeDiff { secs: 0, nanos: 0 });
        assert_eq!(
            SystemTimeDiff::from_nanos(999_999_999),
            SystemTimeDiff { secs: 0, nanos: 999_999_999 }
        );
        assert_eq!(
            SystemTimeDiff::from_nanos(1_000_000_000),
            SystemTimeDiff { secs: 1, nanos: 0 }
        );
        for n in [0_u64, 1, 999_999_999, 1_000_000_001, u64::MAX] {
            let d = SystemTimeDiff::from_nanos(n);
            assert!(d.nanos < NANOS_PER_SEC, "from_nanos({n}) produced denormalized nanos");
            // Lossless round-trip: secs * 1e9 + nanos == n (checked in u128).
            let back =
                u128::from(d.secs) * u128::from(NANOS_PER_SEC) + u128::from(d.nanos);
            assert_eq!(back, u128::from(n), "from_nanos({n}) lost information");
        }
    }
    // ========================================================================
    // Round-trip: from_millis <-> millis
    // ========================================================================
    #[test]
    fn millis_round_trips_through_from_millis() {
        // Exact for every whole-millisecond value, INCLUDING u64::MAX (where
        // `secs * 1000 + 615` lands exactly on u64::MAX without saturating).
        for m in [0_u64, 1, 999, 1_000, 1_500, 86_400_000, u64::MAX] {
            assert_eq!(
                SystemTimeDiff::from_millis(m).millis(),
                m,
                "from_millis({m}).millis() is not lossless"
            );
        }
    }
    #[test]
    fn millis_truncates_and_saturates_instead_of_panicking() {
        // Sub-millisecond nanos truncate towards zero.
        assert_eq!(SystemTimeDiff { secs: 0, nanos: 999_999 }.millis(), 0);
        assert_eq!(SystemTimeDiff { secs: 0, nanos: 999_999_999 }.millis(), 999);
        // secs * 1000 overflows u64 -> saturate at u64::MAX, no panic.
        assert_eq!(SystemTimeDiff { secs: u64::MAX, nanos: 0 }.millis(), u64::MAX);
        assert_eq!(
            SystemTimeDiff { secs: u64::MAX, nanos: NANOS_PER_SEC - 1 }.millis(),
            u64::MAX
        );
        assert_eq!(SystemTimeDiff::from_secs(u64::MAX / 1_000).millis(), (u64::MAX / 1_000) * 1_000);
    }
    // ========================================================================
    // SystemTimeDiff::checked_add
    // ========================================================================
    #[test]
    fn checked_add_carries_nanos_into_secs() {
        let a = SystemTimeDiff { secs: 0, nanos: 999_999_999 };
        let sum = a.checked_add(a).expect("0.999s + 0.999s must not overflow");
        assert_eq!(sum, SystemTimeDiff { secs: 1, nanos: 999_999_998 });
        // Exactly one second of nanos carries cleanly.
        let b = SystemTimeDiff { secs: 1, nanos: 500_000_000 };
        assert_eq!(
            b.checked_add(b),
            Some(SystemTimeDiff { secs: 3, nanos: 0 })
        );
    }
    #[test]
    fn checked_add_returns_none_on_overflow_instead_of_panicking() {
        let max_secs = SystemTimeDiff { secs: u64::MAX, nanos: 0 };
        // secs overflow
        assert_eq!(max_secs.checked_add(SystemTimeDiff::from_secs(1)), None);
        // secs at max, nanos still fit -> Some
        assert_eq!(
            max_secs.checked_add(SystemTimeDiff { secs: 0, nanos: NANOS_PER_SEC - 1 }),
            Some(SystemTimeDiff { secs: u64::MAX, nanos: NANOS_PER_SEC - 1 })
        );
        // overflow that only happens because of the nanos CARRY
        let brim = SystemTimeDiff { secs: u64::MAX, nanos: NANOS_PER_SEC - 1 };
        assert_eq!(brim.checked_add(SystemTimeDiff { secs: 0, nanos: 1 }), None);
    }
    #[test]
    fn checked_add_identity_and_commutativity() {
        let zero = SystemTimeDiff { secs: 0, nanos: 0 };
        for d in [
            SystemTimeDiff::from_secs(0),
            SystemTimeDiff::from_millis(1_500),
            SystemTimeDiff::from_nanos(u64::MAX),
            SystemTimeDiff { secs: u64::MAX, nanos: 0 },
        ] {
            assert_eq!(d.checked_add(zero), Some(d));
            assert_eq!(zero.checked_add(d), Some(d));
            // a + b == b + a for well-formed operands
            let other = SystemTimeDiff::from_millis(750);
            assert_eq!(d.checked_add(other), other.checked_add(d));
        }
    }
    // ========================================================================
    // SystemTimeDiff::get  (std::time::Duration conversion round-trip)
    // ========================================================================
    #[cfg(feature = "std")]
    #[test]
    fn system_time_diff_get_round_trips_std_duration() {
        for std_d in [
            StdDuration::ZERO,
            StdDuration::from_millis(1_500),
            StdDuration::from_nanos(1),
            StdDuration::new(u64::MAX, NANOS_PER_SEC - 1),
        ] {
            let mid: SystemTimeDiff = std_d.into();
            assert_eq!(mid.get(), std_d, "StdDuration -> SystemTimeDiff -> StdDuration lost data");
        }
    }
    #[cfg(feature = "std")]
    #[test]
    fn system_time_diff_get_on_edge_values_does_not_panic() {
        assert_eq!(SystemTimeDiff { secs: 0, nanos: 0 }.get(), StdDuration::ZERO);
        // secs at max with zero nanos: no carry, so no overflow in Duration::new.
        assert_eq!(
            SystemTimeDiff::from_secs(u64::MAX).get(),
            StdDuration::new(u64::MAX, 0)
        );
        // Denormalized nanos (>= 1e9) are carried by Duration::new, not rejected.
        assert_eq!(
            SystemTimeDiff { secs: 0, nanos: u32::MAX }.get(),
            StdDuration::new(0, u32::MAX)
        );
    }
    // ========================================================================
    // ThreadReceiver: new / get_ctx / recv / clone
    // ========================================================================
    #[cfg(feature = "std")]
    extern "C" fn test_thread_recv(ptr: *const c_void) -> OptionThreadSendMsg {
        // Mirrors the real callback: `ThreadReceiver::recv` hands over a pointer
        // to the boxed `Receiver<ThreadSendMsg>` inside `ThreadReceiverInner`.
        let receiver = unsafe { &*(ptr.cast::<Receiver<ThreadSendMsg>>()) };
        receiver.try_recv().ok().into()
    }
    #[cfg(feature = "std")]
    const extern "C" fn test_thread_recv_destructor(_: *mut ThreadReceiverInner) {}
    #[cfg(feature = "std")]
    fn test_receiver() -> (Sender<ThreadSendMsg>, ThreadReceiver) {
        let (tx, rx) = std::sync::mpsc::channel::<ThreadSendMsg>();
        let inner = ThreadReceiverInner {
            ptr: Box::new(rx),
            recv_fn: ThreadRecvCallback { cb: test_thread_recv },
            destructor: ThreadReceiverDestructorCallback {
                cb: test_thread_recv_destructor,
            },
        };
        (tx, ThreadReceiver::new(inner))
    }
    #[cfg(feature = "std")]
    #[test]
    fn thread_receiver_new_arms_destructor_and_has_no_ctx() {
        let (_tx, r) = test_receiver();
        assert!(r.run_destructor, "ThreadReceiver::new left the destructor disarmed");
        assert!(r.get_ctx().is_none(), "a fresh receiver must have no FFI context");
    }
    #[cfg(feature = "std")]
    #[test]
    fn thread_receiver_recv_on_empty_and_disconnected_channel_is_none() {
        let (tx, mut r) = test_receiver();
        // Empty channel -> None (must not block / panic).
        assert!(r.recv().is_none());
        // Disconnected channel -> still None, not a panic.
        drop(tx);
        assert!(r.recv().is_none());
        assert!(r.recv().is_none());
    }
    #[cfg(feature = "std")]
    #[test]
    fn thread_receiver_recv_delivers_messages_in_order() {
        let (tx, mut r) = test_receiver();
        tx.send(ThreadSendMsg::Tick).unwrap();
        tx.send(ThreadSendMsg::Custom(RefAny::new(42_u32))).unwrap();
        tx.send(ThreadSendMsg::TerminateThread).unwrap();
        assert_eq!(r.recv(), OptionThreadSendMsg::Some(ThreadSendMsg::Tick));
        assert!(matches!(
            r.recv(),
            OptionThreadSendMsg::Some(ThreadSendMsg::Custom(_))
        ));
        assert_eq!(
            r.recv(),
            OptionThreadSendMsg::Some(ThreadSendMsg::TerminateThread)
        );
        // Drained.
        assert!(r.recv().is_none());
    }
    #[cfg(feature = "std")]
    #[test]
    fn thread_receiver_clone_shares_the_same_channel() {
        let (tx, mut a) = test_receiver();
        let mut b = a.clone();
        assert!(b.run_destructor);
        tx.send(ThreadSendMsg::Tick).unwrap();
        // The clone shares the Arc<Mutex<..>>: whichever half receives first
        // consumes the message; the other must see an empty channel, not a
        // duplicate and not a deadlock.
        assert_eq!(a.recv(), OptionThreadSendMsg::Some(ThreadSendMsg::Tick));
        assert!(b.recv().is_none());
        tx.send(ThreadSendMsg::TerminateThread).unwrap();
        assert_eq!(
            b.recv(),
            OptionThreadSendMsg::Some(ThreadSendMsg::TerminateThread)
        );
        assert!(a.recv().is_none());
    }
    #[cfg(feature = "std")]
    #[test]
    fn thread_receiver_get_ctx_clones_rather_than_takes() {
        let (_tx, mut r) = test_receiver();
        r.ctx = OptionRefAny::Some(RefAny::new(7_u64));
        // Repeated reads must all succeed -- `get_ctx` clones the RefAny (refcount
        // bump); a take/move would leave the second call empty.
        assert!(r.get_ctx().is_some());
        assert!(r.get_ctx().is_some());
        let held = r.get_ctx();
        drop(r);
        // The cloned handle outlives the receiver it came from.
        assert!(held.is_some());
    }
}