Skip to main content

azul_core/
task.rs

1//! Timer and thread management for asynchronous operations.
2//!
3//! This module provides:
4//! - `TimerId` / `ThreadId`: Unique identifiers for timers and background threads
5//! - `Instant` / `Duration`: Cross-platform time types (works on no_std with tick counters)
6//! - `ThreadReceiver`: Channel for receiving messages from the main thread
7//! - Callback types for thread communication and system time queries
8
9#[cfg(not(feature = "std"))]
10use alloc::string::{String, ToString};
11use alloc::{
12    boxed::Box,
13    collections::btree_map::BTreeMap,
14    sync::{Arc, Weak},
15    vec::Vec,
16};
17use core::{
18    ffi::c_void,
19    fmt,
20    mem::ManuallyDrop,
21    sync::atomic::{AtomicUsize, Ordering},
22};
23#[cfg(feature = "std")]
24use std::sync::mpsc::{Receiver, Sender};
25#[cfg(feature = "std")]
26use std::sync::Mutex;
27#[cfg(feature = "std")]
28use std::thread::{self, JoinHandle};
29#[cfg(feature = "std")]
30use std::time::Duration as StdDuration;
31#[cfg(feature = "std")]
32use std::time::Instant as StdInstant;
33
34use azul_css::{props::property::CssProperty, AzString};
35use rust_fontconfig::FcFontCache;
36
37use crate::{
38    callbacks::{FocusTarget, TimerCallbackReturn, Update},
39    dom::{DomId, DomNodeId, OptionDomNodeId},
40    geom::{LogicalPosition, OptionLogicalPosition},
41    gl::OptionGlContextPtr,
42    hit_test::ScrollPosition,
43    id::NodeId,
44    refany::{OptionRefAny, RefAny},
45    resources::{ImageCache, ImageMask, ImageRef},
46    styled_dom::NodeHierarchyItemId,
47    window::RawWindowHandle,
48    FastBTreeSet, OrderedMap,
49};
50
51/// Should a timer terminate or not - used to remove active timers
52#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
53#[repr(C)]
54pub enum TerminateTimer {
55    /// Remove the timer from the list of active timers
56    Terminate,
57    /// Do nothing and let the timers continue to run
58    Continue,
59}
60
61// ============================================================================
62// Reserved System Timer IDs (0x0000 - 0x00FF)
63// ============================================================================
64// User timers start at 0x0100 to avoid conflicts with system timers.
65// These constants define well-known timer IDs for internal framework use.
66
67/// Timer ID for cursor blinking in contenteditable elements (~530ms interval)
68pub const CURSOR_BLINK_TIMER_ID: TimerId = TimerId { id: 0x0001 };
69/// Timer ID for scroll momentum/inertia animation
70pub const SCROLL_MOMENTUM_TIMER_ID: TimerId = TimerId { id: 0x0002 };
71/// Timer ID for auto-scroll during drag operations near edges
72pub const DRAG_AUTOSCROLL_TIMER_ID: TimerId = TimerId { id: 0x0003 };
73/// Timer ID for tooltip show delay.
74///
75/// Started by the platform event loop when the hover target changes to a node
76/// that advertises a tooltip source (`aria-label` / `alt` / `title`); fires
77/// once after `SystemStyle::input_metrics.hover_time_ms` (`SPI_GETMOUSEHOVERTIME`
78/// on Windows, default 400ms) and emits a `ShowTooltip` `CallbackChange`. The
79/// timer is torn down on hover loss, which also emits `HideTooltip`.
80///
81/// Double-click detection used to live on a neighbouring reserved ID but is
82/// now handled entirely by `GestureManager::detect_double_click`, so no
83/// equivalent `DOUBLE_CLICK_TIMER_ID` exists.
84pub const TOOLTIP_DELAY_TIMER_ID: TimerId = TimerId { id: 0x0004 };
85/// Timer ID for the single-threaded capability pump (MWA-A1).
86///
87/// Armed by `sync_capability_pump_timer` whenever a capability source needs
88/// polling or draining while the app is otherwise idle (gamepad listeners,
89/// sensor listeners, an active geolocation subscription). Each tick wakes the
90/// blocked platform loop; `invoke_expired_timers` then runs an event pass,
91/// whose top-of-pass pump drains the async capability channels. There is NO
92/// pump thread by design — a recurring shell timer is the only wake
93/// mechanism, so the identical code path works on WASM (no threads).
94pub const CAPABILITY_PUMP_TIMER_ID: TimerId = TimerId { id: 0x0005 };
95/// Timer ID for the one-shot long-press wake-up (MWA-B12).
96///
97/// Armed on every `MouseDown` for the long-press threshold: a motionless
98/// press generates no further events, so no pass would ever evaluate
99/// `detect_long_press` — this timer wakes the loop exactly once at the
100/// threshold, `invoke_expired_timers` runs an event pass, and the
101/// detection fires (or doesn't — moved/released holds are no-ops).
102pub const LONG_PRESS_TIMER_ID: TimerId = TimerId { id: 0x0006 };
103
104/// Reserved timer ID for the caret / selection tween driver (~16ms).
105///
106/// Armed by the shared event dispatcher whenever a text tween is in flight; the
107/// callback terminates itself the tick after the tween state goes idle.
108pub const CARET_TWEEN_TIMER_ID: TimerId = TimerId { id: 0x0007 };
109
110/// First available ID for user-defined timers
111pub const USER_TIMER_ID_START: usize = 0x0100;
112
113// User timers start at 0x0100 to avoid conflicts with reserved system timer IDs
114static MAX_TIMER_ID: AtomicUsize = AtomicUsize::new(USER_TIMER_ID_START);
115
116/// ID for uniquely identifying a timer
117#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
118#[repr(C)]
119pub struct TimerId {
120    pub id: usize,
121}
122
123impl TimerId {
124    /// Generates a new, unique `TimerId`.
125    #[must_use]
126    pub fn unique() -> Self {
127        Self {
128            id: MAX_TIMER_ID.fetch_add(1, Ordering::SeqCst),
129        }
130    }
131}
132
133impl_option!(
134    TimerId,
135    OptionTimerId,
136    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
137);
138
139impl_vec!(
140    TimerId,
141    TimerIdVec,
142    TimerIdVecDestructor,
143    TimerIdVecDestructorType,
144    TimerIdVecSlice,
145    OptionTimerId
146);
147impl_vec_debug!(TimerId, TimerIdVec);
148impl_vec_clone!(TimerId, TimerIdVec, TimerIdVecDestructor);
149impl_vec_partialeq!(TimerId, TimerIdVec);
150impl_vec_partialord!(TimerId, TimerIdVec);
151
152// Thread IDs 0-4 are reserved for internal framework use.
153// User threads start at RESERVED_THREAD_ID_COUNT.
154const RESERVED_THREAD_ID_COUNT: usize = 5;
155static MAX_THREAD_ID: AtomicUsize = AtomicUsize::new(RESERVED_THREAD_ID_COUNT);
156
157/// ID for uniquely identifying a background thread
158#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
159#[repr(C)]
160pub struct ThreadId {
161    id: usize,
162}
163
164impl_option!(
165    ThreadId,
166    OptionThreadId,
167    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
168);
169
170impl_vec!(
171    ThreadId,
172    ThreadIdVec,
173    ThreadIdVecDestructor,
174    ThreadIdVecDestructorType,
175    ThreadIdVecSlice,
176    OptionThreadId
177);
178impl_vec_debug!(ThreadId, ThreadIdVec);
179impl_vec_clone!(ThreadId, ThreadIdVec, ThreadIdVecDestructor);
180impl_vec_partialeq!(ThreadId, ThreadIdVec);
181impl_vec_partialord!(ThreadId, ThreadIdVec);
182
183impl ThreadId {
184    /// Generates a new, unique `ThreadId`.
185    #[must_use]
186    pub fn unique() -> Self {
187        Self {
188            id: MAX_THREAD_ID.fetch_add(1, Ordering::SeqCst),
189        }
190    }
191}
192
193/// A point in time, either from the system clock or a tick counter.
194///
195/// Use `Instant::System` on platforms with std, `Instant::Tick` on `embedded/no_std`.
196#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
197#[repr(C, u8)]
198pub enum Instant {
199    /// System time from `std::time::Instant` (requires "std" feature)
200    System(InstantPtr),
201    /// Tick-based time for embedded systems without a real-time clock
202    Tick(SystemTick),
203}
204
205#[cfg(feature = "std")]
206impl From<StdInstant> for Instant {
207    fn from(s: StdInstant) -> Self {
208        Self::System(s.into())
209    }
210}
211
212#[cfg(feature = "std")]
213std::thread_local! {
214    /// Injectable test-clock offset, in milliseconds, added to every
215    /// `Instant::now()` **on this thread**.
216    ///
217    /// Driven by the E2E `tick_ms` op. Everything time-driven in the engine —
218    /// scroll momentum, scrollbar fade, cursor blink, animations, timers —
219    /// reads the clock through `Instant::now()` / `get_system_time_libstd()`,
220    /// so advancing this offset moves all of them forward by exactly N ms
221    /// WITHOUT sleeping. That is what makes "drive the animation to completion
222    /// and assert it converges" deterministic instead of a `wait { ms }` race.
223    ///
224    /// Zero in production; only the debug-server `tick_ms` op ever writes it.
225    ///
226    /// # Why this is a thread-local and not a `static AtomicU64`
227    ///
228    /// It used to be process-global, which made the clock a shared mutable
229    /// resource: every scenario that ticked had to run SERIALLY, or scenario
230    /// A's `tick_ms` would shift scenario B's animations mid-frame. Since the
231    /// corpus is dominated by idle/animation scenarios, that serialised
232    /// essentially the whole suite.
233    ///
234    /// The read path is [`GetSystemTimeCallbackType`] — a bare
235    /// `extern "C" fn() -> Instant` in the public C API — plus ~140 direct
236    /// `Instant::now()` calls. Neither can carry a window, an app or a clock
237    /// handle without either breaking the C ABI for every language binding or
238    /// threading a time source through every call site including `no_std`
239    /// ones. A thread-local is the narrowest scope a context-free C callback
240    /// can read: it turns "the whole process" into "the thread that owns this
241    /// scenario", which is exactly the ownership boundary the parallel E2E
242    /// runner already establishes (one scenario runs start-to-finish on one
243    /// worker thread). [`reset_test_clock`] makes that boundary explicit.
244    static TEST_CLOCK_OFFSET_MS: core::cell::Cell<u64> = const { core::cell::Cell::new(0) };
245}
246
247/// Advance the injectable test clock by `ms` (E2E `tick_ms`), returning the new
248/// offset. Affects only the CURRENT thread — see [`TEST_CLOCK_OFFSET_MS`].
249#[cfg(feature = "std")]
250#[must_use]
251pub fn advance_test_clock_ms(ms: u64) -> u64 {
252    TEST_CLOCK_OFFSET_MS.with(|c| {
253        let next = c.get().saturating_add(ms);
254        c.set(next);
255        next
256    })
257}
258
259/// The current test-clock offset in ms (0 unless `tick_ms` was used on this
260/// thread).
261#[cfg(feature = "std")]
262#[must_use]
263pub fn test_clock_offset_ms() -> u64 {
264    TEST_CLOCK_OFFSET_MS.with(core::cell::Cell::get)
265}
266
267#[cfg(feature = "std")]
268#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
269std::thread_local! {
270    /// When set, this thread's clock is FROZEN at this instant: `Instant::now()`
271    /// answers `base + TEST_CLOCK_OFFSET_MS` and real time does not flow into it
272    /// at all. See [`freeze_test_clock`].
273    static TEST_CLOCK_BASE: core::cell::Cell<Option<StdInstant>> =
274        const { core::cell::Cell::new(None) };
275}
276
277/// Freeze this thread's clock, so engine time advances ONLY when a scenario says
278/// it does (`tick_ms` / `wait`) and never because wall time passed.
279///
280/// Offsetting alone is not enough. `Instant::now()` was
281/// `StdInstant::now() + offset`, so the REAL component still flowed and every
282/// time-driven behaviour rode on however long the machine happened to take:
283/// elapsed = (exact virtual) + (whatever this build, under this load, spent
284/// computing). The E2E suite runs 8 scenarios per core, so that second term is
285/// both large and variable, and an assertion on a blinking caret's phase would
286/// flip between runs on a loaded runner while passing every time in isolation.
287///
288/// Frozen, engine time becomes a pure function of the ops a scenario executed —
289/// identical on a debug build, a release build and a saturated CI box. That is
290/// also what makes an off-by-one in animation timing *observable*: advance
291/// exactly one interval and the frame either flipped or it did not, with no
292/// jitter to hide behind.
293///
294/// This deliberately does NOT touch [`Instant::Tick`]. Interval constants are
295/// built as `Duration::System` (e.g. the cursor blink in `text_edit`), and
296/// `Duration::greater_than` compares only matching variants — handing the engine
297/// `Tick` elapsed values against `System` intervals would mismatch and silently
298/// answer "not yet" forever. Freezing keeps every existing comparison intact.
299///
300/// Idempotent: re-freezing an already-frozen clock keeps the original base, so
301/// the offset stays the single source of elapsed time.
302#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
303pub fn freeze_test_clock() {
304    TEST_CLOCK_BASE.with(|c| {
305        if c.get().is_none() {
306            c.set(Some(StdInstant::now()));
307        }
308    });
309}
310
311/// Whether this thread's clock is frozen (see [`freeze_test_clock`]).
312#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
313#[must_use]
314pub fn test_clock_is_frozen() -> bool {
315    TEST_CLOCK_BASE.with(core::cell::Cell::get).is_some()
316}
317
318/// Put this thread's test clock back on real time.
319///
320/// Worker threads are REUSED across scenarios, so without this the next
321/// scenario scheduled onto this thread would inherit the previous one's
322/// accumulated offset — the same cross-contamination the process-global
323/// offset had, just at thread granularity. The E2E runner calls this at the
324/// start of every scenario. Clears the freeze as well, so a scenario cannot
325/// leave the next one's clock stopped.
326#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
327pub fn reset_test_clock() {
328    TEST_CLOCK_OFFSET_MS.with(|c| c.set(0));
329    TEST_CLOCK_BASE.with(|c| c.set(None));
330}
331
332/// Monotonic frame counter, and the ONLY clock a wasm build has.
333///
334/// `std::time::Instant::now()` PANICS on wasm32-unknown-unknown, and
335/// `#[cfg(feature = "std")]` does not exclude wasm here: azul-core is built with
336/// `default = ["std"]` for the web target, so every `std` path is compiled in.
337///
338/// Answering `Tick(0)` forever would stop the panic and freeze every animation
339/// instead — a silent stall, which is worse than a loud crash. So the web build
340/// gets a real monotonic source: the browser drives redraw, each produced DOM
341/// patch is one frame, and one frame is exactly what a `t` (tick) duration
342/// counts. `AzStartup_buildPatch` calls [`advance_system_tick`] once per patch.
343#[cfg(feature = "std")]
344static SYSTEM_TICK: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
345
346/// Advance the frame counter by one. Called once per produced frame by backends
347/// that have no wall clock. Cheap enough to call unconditionally.
348#[cfg(feature = "std")]
349pub fn advance_system_tick() {
350    SYSTEM_TICK.fetch_add(1, Ordering::Relaxed);
351}
352
353/// The current frame counter.
354#[cfg(feature = "std")]
355#[must_use]
356pub fn system_tick_now() -> u64 {
357    SYSTEM_TICK.load(Ordering::Relaxed)
358}
359
360/// `std::time::Instant::now()` shifted by the injectable test-clock offset, or —
361/// when the clock is frozen — built from the frozen base so real time cannot
362/// leak in.
363///
364/// NOT COMPILED on wasm32, where `std::time::Instant::now()` panics.
365///
366/// `web_lift` is deliberately NOT included here. That backend compiles natively
367/// and is lifted to wasm afterwards, so `target_arch` reads `x86_64` — but the
368/// lift walks the LLVM graph and auto-inserts calls out to JS for things like
369/// time, so it supplies its own clock and does not want this arm disabled.
370#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
371fn std_now_with_test_offset() -> StdInstant {
372    let offset = test_clock_offset_ms();
373    if let Some(base) = TEST_CLOCK_BASE.with(core::cell::Cell::get) {
374        return base + core::time::Duration::from_millis(offset);
375    }
376    if offset == 0 {
377        StdInstant::now()
378    } else {
379        StdInstant::now() + core::time::Duration::from_millis(offset)
380    }
381}
382
383impl Instant {
384    /// Returns the current system time.
385    ///
386    /// On systems with std, this uses `std::time::Instant::now()`.
387    /// On `no_std` systems, this returns a zero tick.
388    #[cfg(all(feature = "std", not(target_arch = "wasm32")))]
389    #[must_use]
390    pub fn now() -> Self {
391        std_now_with_test_offset().into()
392    }
393
394    /// Returns the current time on wasm32, which has no clock to read.
395    ///
396    /// `std::time::Instant::now()` panics on wasm32-unknown-unknown, and
397    /// `#[cfg(feature = "std")]` does not exclude wasm here — azul-core is built
398    /// with `default = ["std"]` for the web target, so the std path is compiled
399    /// in and would trap on the first frame.
400    ///
401    /// This deliberately does NOT answer a constant `Tick(0)`. That stops the
402    /// panic and freezes every animation instead, which is a silent stall — the
403    /// worse failure of the two. The browser drives redraw and each produced DOM
404    /// patch is one frame, so the frame counter IS the clock, and a frame is
405    /// exactly what a `t` (tick) duration counts. Elapsed values come out as
406    /// `Tick` and convert against `System` intervals through
407    /// `Duration::as_nanos`, so `60t` compares equal to one second.
408    #[cfg(all(feature = "std", target_arch = "wasm32"))]
409    #[must_use]
410    pub fn now() -> Self {
411        Instant::Tick(SystemTick::new(system_tick_now()))
412    }
413
414    /// Returns the current system time (no_std fallback).
415    #[cfg(not(feature = "std"))]
416    pub fn now() -> Self {
417        Instant::Tick(SystemTick::new(0))
418    }
419
420    /// Returns a number from 0.0 to 1.0 indicating the current
421    /// linear interpolation value between (start, end)
422    #[must_use]
423    pub fn linear_interpolate(&self, mut start: Self, mut end: Self) -> f32 {
424        use core::mem;
425
426        if end < start {
427            mem::swap(&mut start, &mut end);
428        }
429
430        if *self < start {
431            return 0.0;
432        }
433        if *self > end {
434            return 1.0;
435        }
436
437        // Zero-length interval: `duration_current / duration_total` would be
438        // `0/0 = NaN`. Treat a collapsed interval as fully elapsed (1.0) rather
439        // than propagating NaN into animation progress.
440        if start == end {
441            return 1.0;
442        }
443
444        let duration_total = end.duration_since(&start);
445        let duration_current = self.duration_since(&start);
446
447        let ratio = duration_current.div(&duration_total);
448        if ratio.is_nan() {
449            return 1.0;
450        }
451        ratio.clamp(0.0, 1.0)
452    }
453
454    /// Adds a duration to the instant.
455    ///
456    /// The duration's UNIT need not match the instant's: a `Tick` duration added
457    /// to a `System` instant is converted at [`TICKS_PER_SECOND`], and a `System`
458    /// duration added to a `Tick` instant is converted to whole ticks.
459    ///
460    /// # Why the mismatch is converted rather than dropped
461    ///
462    /// This used to return `self` unchanged for a unit mismatch, which turned a
463    /// `Duration::Tick` interval on a wall-clock timer into a schedule point of
464    /// `last_run + 0` — `Timer::instant_of_next_run` is literally
465    /// `last_run + delay + interval`, so the timer reported itself permanently
466    /// overdue and `LayoutWindow::time_until_next_timer_ms` answered `Some(0)`
467    /// for it, i.e. "block for zero milliseconds" to any loop that consults it.
468    ///
469    /// `System + System` still overflow-panics on an absurd duration (that is
470    /// `StdInstant`'s own behaviour, characterised in the tests); the tick arms
471    /// saturate.
472    #[must_use]
473    pub fn add_optional_duration(&self, duration: Option<&Duration>) -> Self {
474        duration.map_or_else(
475            || self.clone(),
476            |d| match (self, d) {
477                (Self::System(i), Duration::System(d)) => {
478                    #[cfg(feature = "std")]
479                    {
480                        let s: StdInstant = i.clone().into();
481                        let d: StdDuration = (*d).into();
482                        let new: InstantPtr = (s + d).into();
483                        Self::System(new)
484                    }
485                    #[cfg(not(feature = "std"))]
486                    {
487                        // A `System` instant cannot be constructed on no_std, so
488                        // this arm is unreachable in practice; return self rather
489                        // than aborting.
490                        let _ = (i, d);
491                        self.clone()
492                    }
493                }
494                (Self::Tick(s), Duration::Tick(d)) => Self::Tick(SystemTick {
495                    // Saturate so a runaway tick delta cannot overflow-panic.
496                    tick_counter: s.tick_counter.saturating_add(d.tick_diff),
497                }),
498                // System instant + Tick duration: convert the frame count to wall
499                // time. Routed through the same `System + System` arm so the
500                // overflow behaviour is identical for both units.
501                (Self::System(_), Duration::Tick(_)) => self.add_optional_duration(Some(
502                    &Duration::System(SystemTimeDiff::from_nanos_u128(d.as_nanos())),
503                )),
504                // Tick instant + System duration: convert to WHOLE ticks. A
505                // sub-frame duration therefore advances nothing, which is the
506                // truthful answer on a clock whose resolution is one frame.
507                (Self::Tick(s), Duration::System(_)) => Self::Tick(SystemTick {
508                    tick_counter: s.tick_counter.saturating_add(d.as_ticks()),
509                }),
510            },
511        )
512    }
513
514    /// Converts to `std::time::Instant` (panics if Tick variant).
515    #[cfg(feature = "std")]
516    #[must_use]
517    pub fn into_std_instant(self) -> StdInstant {
518        match self {
519            Self::System(s) => s.into(),
520            Self::Tick(_) => unreachable!(),
521        }
522    }
523
524    /// Calculates the duration since an earlier point in time.
525    ///
526    /// Saturates to a zero duration in the degenerate cases (earlier is actually
527    /// *later* than `self`, or the two instants are of mismatched kinds) instead
528    /// of panicking — this runs on the hot event-loop path and must not crash.
529    #[must_use]
530    pub fn duration_since(&self, earlier: &Self) -> Duration {
531        match (earlier, self) {
532            (Self::System(prev), Self::System(now)) => {
533                #[cfg(feature = "std")]
534                {
535                    let prev_instant: StdInstant = prev.clone().into();
536                    let now_instant: StdInstant = now.clone().into();
537                    // `saturating_duration_since` yields 0 if `prev` is later
538                    // than `now` (monotonic-clock skew / reordered instants).
539                    Duration::System(now_instant.saturating_duration_since(prev_instant).into())
540                }
541                #[cfg(not(feature = "std"))]
542                {
543                    // Unreachable on no_std (no System instants); saturate to 0.
544                    let _ = (prev, now);
545                    Duration::Tick(SystemTickDiff { tick_diff: 0 })
546                }
547            }
548            (
549                Self::Tick(SystemTick { tick_counter: prev }),
550                Self::Tick(SystemTick { tick_counter: now }),
551            ) => Duration::Tick(SystemTickDiff {
552                // Saturate: a "negative" span (prev > now) clamps to 0.
553                tick_diff: now.saturating_sub(*prev),
554            }),
555            // Mismatched kinds: no meaningful span -> saturate to 0.
556            _ => Duration::Tick(SystemTickDiff { tick_diff: 0 }),
557        }
558    }
559}
560
561/// Tick-based timestamp for systems without a real-time clock.
562///
563/// Used on embedded systems where time is measured in frame ticks or cycles.
564#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
565#[repr(C)]
566pub struct SystemTick {
567    pub tick_counter: u64,
568}
569
570impl SystemTick {
571    /// Creates a new tick timestamp from a counter value.
572    #[must_use]
573    pub const fn new(tick_counter: u64) -> Self {
574        Self { tick_counter }
575    }
576}
577
578/// FFI-safe wrapper around `std::time::Instant` with custom clone/drop callbacks.
579///
580/// Allows crossing FFI boundaries while maintaining proper memory management.
581#[repr(C)]
582pub struct InstantPtr {
583    /// `ManuallyDrop` so the owned `Box` is freed ONLY when `run_destructor` is
584    /// still set (see `Drop`). The codegen FFI wrappers (`AzTimerCallbackInfo`
585    /// etc.) embed this by value AND have their own `Drop` that `drop_in_place`s
586    /// the real type first; Rust's drop glue would then drop this `ptr` field a
587    /// SECOND time on the same bytes. Gating the `Box` free on `run_destructor`
588    /// (cleared by the first drop) makes that second drop a safe no-op. Layout is
589    /// unchanged: `ManuallyDrop<Box<T>>` is one pointer, like the old `Box<T>`.
590    #[cfg(feature = "std")]
591    pub ptr: ManuallyDrop<Box<StdInstant>>,
592    #[cfg(not(feature = "std"))]
593    pub ptr: *const c_void,
594    pub clone_fn: InstantPtrCloneCallback,
595    pub destructor: InstantPtrDestructorCallback,
596    pub run_destructor: bool,
597}
598
599pub type InstantPtrCloneCallbackType = extern "C" fn(*const InstantPtr) -> InstantPtr;
600#[repr(C)]
601pub struct InstantPtrCloneCallback {
602    pub cb: InstantPtrCloneCallbackType,
603}
604impl_callback_simple!(InstantPtrCloneCallback);
605
606pub type InstantPtrDestructorCallbackType = extern "C" fn(*mut InstantPtr);
607#[repr(C)]
608pub struct InstantPtrDestructorCallback {
609    pub cb: InstantPtrDestructorCallbackType,
610}
611impl_callback_simple!(InstantPtrDestructorCallback);
612
613// ----  LIBSTD implementation for InstantPtr BEGIN
614#[cfg(feature = "std")]
615impl fmt::Debug for InstantPtr {
616    fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
617        write!(f, "{:?}", self.get())
618    }
619}
620
621#[cfg(not(feature = "std"))]
622impl core::fmt::Debug for InstantPtr {
623    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
624        write!(f, "{:?}", self.ptr as usize)
625    }
626}
627
628#[cfg(feature = "std")]
629impl core::hash::Hash for InstantPtr {
630    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
631        self.get().hash(state);
632    }
633}
634
635#[cfg(not(feature = "std"))]
636impl core::hash::Hash for InstantPtr {
637    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
638        (self.ptr as usize).hash(state);
639    }
640}
641
642#[cfg(feature = "std")]
643impl PartialEq for InstantPtr {
644    fn eq(&self, other: &Self) -> bool {
645        self.get() == other.get()
646    }
647}
648
649#[cfg(not(feature = "std"))]
650impl PartialEq for InstantPtr {
651    fn eq(&self, other: &InstantPtr) -> bool {
652        (self.ptr as usize).eq(&(other.ptr as usize))
653    }
654}
655
656impl Eq for InstantPtr {}
657
658#[cfg(feature = "std")]
659impl PartialOrd for InstantPtr {
660    fn partial_cmp(&self, other: &Self) -> Option<::core::cmp::Ordering> {
661        Some((self.get()).cmp(&(other.get())))
662    }
663}
664
665#[cfg(not(feature = "std"))]
666impl PartialOrd for InstantPtr {
667    fn partial_cmp(&self, other: &Self) -> Option<::core::cmp::Ordering> {
668        Some((self.ptr as usize).cmp(&(other.ptr as usize)))
669    }
670}
671
672#[cfg(feature = "std")]
673impl Ord for InstantPtr {
674    fn cmp(&self, other: &Self) -> ::core::cmp::Ordering {
675        (self.get()).cmp(&(other.get()))
676    }
677}
678
679#[cfg(not(feature = "std"))]
680impl Ord for InstantPtr {
681    fn cmp(&self, other: &Self) -> ::core::cmp::Ordering {
682        (self.ptr as usize).cmp(&(other.ptr as usize))
683    }
684}
685
686#[cfg(feature = "std")]
687impl InstantPtr {
688    fn get(&self) -> StdInstant {
689        (**self.ptr)
690    }
691}
692
693impl Clone for InstantPtr {
694    fn clone(&self) -> Self {
695        (self.clone_fn.cb)(self)
696    }
697}
698
699#[cfg(feature = "std")]
700extern "C" fn std_instant_clone(ptr: *const InstantPtr) -> InstantPtr {
701    let az_instant_ptr = unsafe { &*ptr };
702    InstantPtr {
703        ptr: ManuallyDrop::new((*az_instant_ptr.ptr).clone()),
704        clone_fn: az_instant_ptr.clone_fn,
705        destructor: az_instant_ptr.destructor,
706        run_destructor: true,
707    }
708}
709
710#[cfg(feature = "std")]
711impl From<StdInstant> for InstantPtr {
712    fn from(s: StdInstant) -> Self {
713        Self {
714            ptr: ManuallyDrop::new(Box::new(s)),
715            clone_fn: InstantPtrCloneCallback {
716                cb: std_instant_clone,
717            },
718            destructor: InstantPtrDestructorCallback {
719                cb: std_instant_drop,
720            },
721            run_destructor: true,
722        }
723    }
724}
725
726#[cfg(feature = "std")]
727impl From<InstantPtr> for StdInstant {
728    fn from(s: InstantPtr) -> Self {
729        s.get()
730    }
731}
732
733impl Drop for InstantPtr {
734    fn drop(&mut self) {
735        if self.run_destructor {
736            self.run_destructor = false;
737            (self.destructor.cb)(self);
738            // Free the owned Box exactly once, here under the run_destructor guard.
739            // A second drop on the same bytes (the codegen wrapper's field-drop after
740            // its own `_delete` already ran the real drop) sees run_destructor=false
741            // and skips this -> no double-free. (non-std `ptr` is a raw POD pointer
742            // freed by the destructor callback above, so nothing to drop here.)
743            // SAFETY: `run_destructor` is set false above, so this arm runs at
744            // most once per InstantPtr value; the `Box` inside was never moved
745            // out, so it is live and owned here and safe to drop exactly once.
746            #[cfg(feature = "std")]
747            unsafe {
748                ManuallyDrop::drop(&mut self.ptr);
749            }
750        }
751    }
752}
753
754#[cfg(feature = "std")]
755const extern "C" fn std_instant_drop(_: *mut InstantPtr) {}
756
757// ----  LIBSTD implementation for InstantPtr END
758
759/// A span of time, either from the system clock or as tick difference.
760///
761/// Mirrors `Instant` variants - System durations work with System instants,
762/// Tick durations work with Tick instants.
763#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
764#[repr(C, u8)]
765pub enum Duration {
766    /// System duration from `std::time::Duration` (requires "std" feature)
767    System(SystemTimeDiff),
768    /// Tick-based duration for embedded systems
769    Tick(SystemTickDiff),
770}
771
772impl fmt::Display for Duration {
773    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
774        match self {
775            #[cfg(feature = "std")]
776            Self::System(s) => {
777                let s: StdDuration = (*s).into();
778                write!(f, "{s:?}")
779            }
780            #[cfg(not(feature = "std"))]
781            Duration::System(s) => write!(f, "({}s, {}ns)", s.secs, s.nanos),
782            Self::Tick(tick) => write!(f, "{} ticks", tick.tick_diff),
783        }
784    }
785}
786
787#[cfg(feature = "std")]
788impl From<StdDuration> for Duration {
789    fn from(s: StdDuration) -> Self {
790        Self::System(s.into())
791    }
792}
793
794/// Nominal engine tick (frame) rate — the single exchange rate between
795/// [`Duration::Tick`] (frames) and [`Duration::System`] (wall time).
796///
797/// Re-exported from `azul-css` so the CSS `t` unit and the engine's `Duration`
798/// arithmetic cannot drift apart. See [`azul_css::props::basic::time::TICKS_PER_SECOND`].
799pub use azul_css::props::basic::time::TICKS_PER_SECOND;
800
801impl Duration {
802    /// This duration on ONE canonical scale, in nanoseconds — the common ground
803    /// on which a `Tick` span and a `System` span can be compared.
804    ///
805    /// `u128` because a `System` duration holds up to `u64::MAX` *seconds*
806    /// (~1.8e28 ns), which does not fit `u64`. The tick conversion multiplies
807    /// before it divides so whole seconds stay exact: `60t` is `1_000_000_000`ns,
808    /// not `60 * 16_666_666 = 999_999_960`ns.
809    ///
810    /// Note this also normalises a DENORMALISED `SystemTimeDiff` (`nanos` past
811    /// `1e9`) the same way `std::time::Duration::new` would — except it cannot
812    /// panic on overflow while doing it.
813    // `as u128` rather than `u128::from`: this is a `const fn` and `From` is not
814    // const. Every one of these widenings is lossless.
815    #[allow(clippy::cast_lossless)]
816    #[must_use]
817    pub const fn as_nanos(&self) -> u128 {
818        match self {
819            Self::System(s) => (s.secs as u128) * (NANOS_PER_SEC as u128) + (s.nanos as u128),
820            Self::Tick(t) => {
821                (t.tick_diff as u128) * (NANOS_PER_SEC as u128) / (TICKS_PER_SECOND as u128)
822            }
823        }
824    }
825
826    /// A wall-clock duration of `ms` whole milliseconds.
827    #[must_use]
828    pub const fn from_millis(ms: u64) -> Self {
829        Self::System(SystemTimeDiff::from_millis(ms))
830    }
831
832    /// A duration of `ticks` engine frames — the clockless unit, and what the
833    /// CSS `t` unit becomes.
834    #[must_use]
835    pub const fn from_ticks(ticks: u64) -> Self {
836        Self::Tick(SystemTickDiff { tick_diff: ticks })
837    }
838
839    /// This duration in whole ticks (frames), truncating toward zero.
840    ///
841    /// A sub-frame span is **zero** ticks, not one: "how many whole frames fit",
842    /// never "round up so that something happens".
843    // `as` casts: `const fn`, so `From`/`TryFrom` are unavailable. The widenings
844    // are lossless and the u128 -> u64 narrowing is range-checked immediately
845    // above it.
846    #[allow(clippy::cast_lossless, clippy::cast_possible_truncation)]
847    #[must_use]
848    pub const fn as_ticks(&self) -> u64 {
849        match self {
850            Self::Tick(t) => t.tick_diff,
851            Self::System(_) => {
852                let ticks = self.as_nanos() * (TICKS_PER_SECOND as u128) / (NANOS_PER_SEC as u128);
853                if ticks > u64::MAX as u128 {
854                    u64::MAX
855                } else {
856                    ticks as u64
857                }
858            }
859        }
860    }
861
862    /// This duration in whole milliseconds, truncating toward zero and
863    /// saturating at `u64::MAX` rather than wrapping.
864    // `as` casts: `const fn`, so `From`/`TryFrom` are unavailable. The u128 ->
865    // u64 narrowing is range-checked immediately above it.
866    #[allow(clippy::cast_lossless, clippy::cast_possible_truncation)]
867    #[must_use]
868    pub const fn as_millis_u64(&self) -> u64 {
869        let ms = self.as_nanos() / (NANOS_PER_MILLI as u128);
870        if ms > u64::MAX as u128 {
871            u64::MAX
872        } else {
873            ms as u64
874        }
875    }
876
877    /// Returns the maximum possible duration.
878    #[must_use]
879    pub fn max() -> Self {
880        #[cfg(feature = "std")]
881        {
882            Self::System(StdDuration::new(core::u64::MAX, NANOS_PER_SEC - 1).into())
883        }
884        #[cfg(not(feature = "std"))]
885        {
886            Duration::Tick(SystemTickDiff {
887                tick_diff: u64::MAX,
888            })
889        }
890    }
891
892    /// Divides this duration by another, returning the ratio as f32.
893    ///
894    /// Same-unit division goes through the unit's own `div` so its exact
895    /// floating-point result is unchanged. Cross-unit division falls back to the
896    /// canonical nanosecond scale rather than returning `0.0` — a `0.0` ratio
897    /// here means "animation is at 0% progress", which is a frozen animation, not
898    /// an error anyone would notice.
899    // the f64 ratio is intentionally narrowed to the f32 return type; the value
900    // is a duration ratio, far inside f32's range.
901    #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
902    #[must_use]
903    pub fn div(&self, other: &Self) -> f32 {
904        use self::Duration::{System, Tick};
905        match (self, other) {
906            (System(s), System(s2)) => s.div(s2) as f32,
907            (Tick(t), Tick(t2)) => t.div(t2) as f32,
908            // u128 -> f64 loses precision only past 2^53 ns (~104 days), and the
909            // result is a ratio that is then narrowed to f32 anyway.
910            _ => (self.as_nanos() as f64 / other.as_nanos() as f64) as f32,
911        }
912    }
913
914    /// Returns the smaller of two durations.
915    #[must_use]
916    pub const fn min(self, other: Self) -> Self {
917        if self.smaller_than(&other) {
918            self
919        } else {
920            other
921        }
922    }
923
924    /// Returns true if self > other.
925    ///
926    /// Compares on the canonical nanosecond scale ([`Self::as_nanos`]), so a
927    /// `Tick` span and a `System` span compare TRUTHFULLY against each other.
928    ///
929    /// # Why this is not "mismatched kinds saturate to false"
930    ///
931    /// It used to be. That made a unit mismatch invisible and permanent instead
932    /// of loud: the engine's interval constants are `Duration::System` (the
933    /// cursor blink, the scrollbar fade, the tooltip delay), so the moment a
934    /// clock produced `Tick` elapsed values every one of those comparisons
935    /// answered "not yet" — forever. Nothing panicked, nothing logged, the UI
936    /// simply stopped animating. That is precisely the failure a clockless unit
937    /// is supposed to make *catchable*, so the comparison has to be total.
938    ///
939    /// Three behaviours changed, all in the safe direction:
940    ///
941    /// 1. Cross-unit comparisons now answer, instead of always `false`.
942    /// 2. On `no_std` the `System`/`System` arm used to be hardcoded `false`
943    ///    (there was no `StdDuration` to defer to); it now compares properly.
944    /// 3. A denormalised `SystemTimeDiff` whose `secs + nanos/1e9` overflows
945    ///    `u64` used to panic inside `StdDuration::new`; `u128` nanoseconds
946    ///    cannot overflow.
947    #[must_use]
948    pub const fn greater_than(&self, other: &Self) -> bool {
949        self.as_nanos() > other.as_nanos()
950    }
951
952    /// Returns true if self < other.
953    ///
954    /// Canonical-scale comparison; see [`Self::greater_than`] for why this is
955    /// unit-aware rather than saturating to `false` on a unit mismatch.
956    #[must_use]
957    pub const fn smaller_than(&self, other: &Self) -> bool {
958        self.as_nanos() < other.as_nanos()
959    }
960}
961
962/// Represents a difference in ticks for systems that
963/// don't support timing
964#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
965#[repr(C)]
966pub struct SystemTickDiff {
967    pub tick_diff: u64,
968}
969
970impl SystemTickDiff {
971    /// Divide duration A by duration B.
972    /// Returns `Inf` or `NaN` if `other` is zero.
973    // tick counts -> f64 for the ratio; precision only degrades past 2^53 ticks.
974    #[allow(clippy::cast_precision_loss)]
975    #[must_use]
976    pub fn div(&self, other: &Self) -> f64 {
977        self.tick_diff as f64 / other.tick_diff as f64
978    }
979}
980
981/// Duration represented as seconds + nanoseconds (mirrors `std::time::Duration`).
982#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
983#[repr(C)]
984pub struct SystemTimeDiff {
985    pub secs: u64,
986    pub nanos: u32,
987}
988
989impl SystemTimeDiff {
990    /// Divide duration A by duration B.
991    /// Returns `Inf` or `NaN` if `other` is zero.
992    #[must_use]
993    pub fn div(&self, other: &Self) -> f64 {
994        self.as_secs_f64() / other.as_secs_f64()
995    }
996    // secs (u64) -> f64 loses precision only past 2^53 seconds (~285M years).
997    #[allow(clippy::cast_precision_loss)]
998    fn as_secs_f64(&self) -> f64 {
999        (self.secs as f64) + (f64::from(self.nanos) / f64::from(NANOS_PER_SEC))
1000    }
1001}
1002
1003#[cfg(feature = "std")]
1004impl From<StdDuration> for SystemTimeDiff {
1005    fn from(d: StdDuration) -> Self {
1006        Self {
1007            secs: d.as_secs(),
1008            nanos: d.subsec_nanos(),
1009        }
1010    }
1011}
1012
1013#[cfg(feature = "std")]
1014impl From<SystemTimeDiff> for StdDuration {
1015    fn from(d: SystemTimeDiff) -> Self {
1016        Self::new(d.secs, d.nanos)
1017    }
1018}
1019
1020const MILLIS_PER_SEC: u64 = 1_000;
1021const NANOS_PER_MILLI: u32 = 1_000_000;
1022const NANOS_PER_SEC: u32 = 1_000_000_000;
1023
1024impl SystemTimeDiff {
1025    /// Creates a duration from whole seconds.
1026    #[must_use]
1027    pub const fn from_secs(secs: u64) -> Self {
1028        Self { secs, nanos: 0 }
1029    }
1030    /// Creates a duration from milliseconds.
1031    #[must_use]
1032    pub const fn from_millis(millis: u64) -> Self {
1033        Self {
1034            secs: millis / MILLIS_PER_SEC,
1035            nanos: ((millis % MILLIS_PER_SEC) as u32) * NANOS_PER_MILLI,
1036        }
1037    }
1038    /// Creates a duration from nanoseconds.
1039    // const fn (no const TryFrom); `nanos % NANOS_PER_SEC` is always < 10^9, which
1040    // fits u32, so the narrowing cast cannot truncate.
1041    #[allow(clippy::cast_possible_truncation)]
1042    #[must_use]
1043    pub const fn from_nanos(nanos: u64) -> Self {
1044        Self {
1045            secs: nanos / (NANOS_PER_SEC as u64),
1046            nanos: (nanos % (NANOS_PER_SEC as u64)) as u32,
1047        }
1048    }
1049
1050    /// Creates a duration from a `u128` nanosecond count, saturating at the
1051    /// largest representable duration instead of wrapping.
1052    ///
1053    /// Needed because [`Duration::as_nanos`] is `u128`: a tick count near
1054    /// `u64::MAX` converts to ~3e26 ns, far past what `u64` nanoseconds hold.
1055    // `nanos % NANOS_PER_SEC` is always < 10^9 and `secs` is range-checked above,
1056    // so neither narrowing cast can truncate. `as` widenings rather than `From`
1057    // because this is a `const fn`.
1058    #[allow(clippy::cast_possible_truncation, clippy::cast_lossless)]
1059    #[must_use]
1060    pub const fn from_nanos_u128(nanos: u128) -> Self {
1061        let secs = nanos / (NANOS_PER_SEC as u128);
1062        if secs > u64::MAX as u128 {
1063            Self {
1064                secs: u64::MAX,
1065                nanos: NANOS_PER_SEC - 1,
1066            }
1067        } else {
1068            Self {
1069                secs: secs as u64,
1070                nanos: (nanos % (NANOS_PER_SEC as u128)) as u32,
1071            }
1072        }
1073    }
1074    /// Adds two durations, returning None on overflow.
1075    #[must_use]
1076    pub const fn checked_add(self, rhs: Self) -> Option<Self> {
1077        if let Some(mut secs) = self.secs.checked_add(rhs.secs) {
1078            let mut nanos = self.nanos + rhs.nanos;
1079            if nanos >= NANOS_PER_SEC {
1080                nanos -= NANOS_PER_SEC;
1081                if let Some(new_secs) = secs.checked_add(1) {
1082                    secs = new_secs;
1083                } else {
1084                    return None;
1085                }
1086            }
1087            Some(Self { secs, nanos })
1088        } else {
1089            None
1090        }
1091    }
1092
1093    /// Returns the total duration in milliseconds.
1094    ///
1095    /// Saturates at `u64::MAX` instead of overflow-panicking for enormous
1096    /// `secs` values (`secs * 1000` overflows around ~1.8e16 seconds).
1097    #[must_use]
1098    pub const fn millis(&self) -> u64 {
1099        self.secs
1100            .saturating_mul(MILLIS_PER_SEC)
1101            .saturating_add((self.nanos / NANOS_PER_MILLI) as u64)
1102    }
1103
1104    /// Converts to `std::time::Duration`.
1105    #[cfg(feature = "std")]
1106    #[must_use]
1107    pub fn get(&self) -> StdDuration {
1108        (*self).into()
1109    }
1110}
1111
1112/// Bridge from the CSS-level duration to the engine-level one, preserving the
1113/// unit.
1114///
1115/// This is the join that makes a CSS `5t` mean five FRAMES all the way down to
1116/// the timer: `ms`/`s` become `Duration::System`, `t` becomes `Duration::Tick`.
1117/// Collapsing ticks to milliseconds here would put the wall clock back in the
1118/// path and make "advance exactly 5 ticks, assert the 5th frame flipped"
1119/// untestable again.
1120impl From<azul_css::props::basic::time::CssDuration> for Duration {
1121    fn from(d: azul_css::props::basic::time::CssDuration) -> Self {
1122        use azul_css::props::basic::time::CssDurationUnit;
1123        match d.unit {
1124            CssDurationUnit::Milliseconds => Self::from_millis(u64::from(d.inner)),
1125            CssDurationUnit::Ticks => Self::from_ticks(u64::from(d.inner)),
1126        }
1127    }
1128}
1129
1130impl_option!(
1131    Instant,
1132    OptionInstant,
1133    copy = false,
1134    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
1135);
1136impl_option!(
1137    Duration,
1138    OptionDuration,
1139    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
1140);
1141#[allow(variant_size_differences)]
1142// repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
1143/// Message that can be sent from the main thread to the Thread using the `ThreadId`.
1144///
1145/// The thread can ignore the event.
1146#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
1147#[repr(C, u8)]
1148pub enum ThreadSendMsg {
1149    /// The thread should terminate at the nearest
1150    TerminateThread,
1151    /// Next frame tick
1152    Tick,
1153    /// Custom data
1154    Custom(RefAny),
1155}
1156
1157impl_option!(
1158    ThreadSendMsg,
1159    OptionThreadSendMsg,
1160    copy = false,
1161    [Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash]
1162);
1163
1164/// Channel endpoint for receiving messages from the main thread in a background thread.
1165///
1166/// Thread-safe wrapper around the receiver end of a message channel.
1167#[derive(Debug)]
1168#[repr(C)]
1169pub struct ThreadReceiver {
1170    #[cfg(feature = "std")]
1171    pub ptr: Box<Arc<Mutex<ThreadReceiverInner>>>,
1172    #[cfg(not(feature = "std"))]
1173    pub ptr: *const c_void,
1174    pub run_destructor: bool,
1175    /// For FFI: stores the foreign callable (e.g., `PyFunction`)
1176    pub ctx: OptionRefAny,
1177}
1178
1179impl Clone for ThreadReceiver {
1180    fn clone(&self) -> Self {
1181        Self {
1182            ptr: self.ptr.clone(),
1183            run_destructor: true,
1184            ctx: self.ctx.clone(),
1185        }
1186    }
1187}
1188
1189impl Drop for ThreadReceiver {
1190    fn drop(&mut self) {
1191        self.run_destructor = false;
1192    }
1193}
1194
1195impl ThreadReceiver {
1196    /// Creates a new receiver (no-op on no_std).
1197    #[cfg(not(feature = "std"))]
1198    pub fn new(_t: ThreadReceiverInner) -> Self {
1199        Self {
1200            ptr: core::ptr::null(),
1201            run_destructor: false,
1202            ctx: OptionRefAny::None,
1203        }
1204    }
1205
1206    /// Creates a new receiver wrapping the inner channel.
1207    #[cfg(feature = "std")]
1208    #[must_use]
1209    pub fn new(t: ThreadReceiverInner) -> Self {
1210        Self {
1211            ptr: Box::new(Arc::new(Mutex::new(t))),
1212            run_destructor: true,
1213            ctx: OptionRefAny::None,
1214        }
1215    }
1216
1217    /// Get the FFI context (e.g., Python callable)
1218    #[must_use]
1219    pub fn get_ctx(&self) -> OptionRefAny {
1220        self.ctx.clone()
1221    }
1222
1223    /// Receives a message (returns None on no_std).
1224    #[cfg(not(feature = "std"))]
1225    pub fn recv(&mut self) -> OptionThreadSendMsg {
1226        None.into()
1227    }
1228
1229    /// Receives a message from the main thread, if available.
1230    #[cfg(feature = "std")]
1231    pub fn recv(&mut self) -> OptionThreadSendMsg {
1232        let Some(ts) = self.ptr.lock().ok() else {
1233            return None.into();
1234        };
1235        (ts.recv_fn.cb)(std::ptr::from_ref(ts.ptr.as_ref()) as *const c_void)
1236    }
1237}
1238
1239/// Inner receiver state containing the actual channel and callbacks.
1240#[derive(Debug)]
1241#[cfg_attr(not(feature = "std"), derive(PartialEq, PartialOrd, Eq, Ord))]
1242#[repr(C)]
1243pub struct ThreadReceiverInner {
1244    #[cfg(feature = "std")]
1245    pub ptr: Box<Receiver<ThreadSendMsg>>,
1246    #[cfg(not(feature = "std"))]
1247    pub ptr: *const c_void,
1248    pub recv_fn: ThreadRecvCallback,
1249    pub destructor: ThreadReceiverDestructorCallback,
1250}
1251
1252#[cfg(not(feature = "std"))]
1253unsafe impl Send for ThreadReceiverInner {}
1254
1255#[cfg(feature = "std")]
1256impl core::hash::Hash for ThreadReceiverInner {
1257    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
1258        (std::ptr::from_ref(self.ptr.as_ref()) as usize).hash(state);
1259    }
1260}
1261
1262#[cfg(feature = "std")]
1263impl PartialEq for ThreadReceiverInner {
1264    fn eq(&self, other: &Self) -> bool {
1265        std::ptr::eq(self.ptr.as_ref(), other.ptr.as_ref())
1266    }
1267}
1268
1269#[cfg(feature = "std")]
1270impl Eq for ThreadReceiverInner {}
1271
1272#[cfg(feature = "std")]
1273impl PartialOrd for ThreadReceiverInner {
1274    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
1275        Some(
1276            (std::ptr::from_ref(self.ptr.as_ref()) as usize)
1277                .cmp(&(std::ptr::from_ref(other.ptr.as_ref()) as usize)),
1278        )
1279    }
1280}
1281
1282#[cfg(feature = "std")]
1283impl Ord for ThreadReceiverInner {
1284    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
1285        (std::ptr::from_ref(self.ptr.as_ref()) as usize)
1286            .cmp(&(std::ptr::from_ref(other.ptr.as_ref()) as usize))
1287    }
1288}
1289
1290impl Drop for ThreadReceiverInner {
1291    fn drop(&mut self) {
1292        (self.destructor.cb)(self);
1293    }
1294}
1295
1296/// Get the current system type, equivalent to `std::time::Instant::now()`, except it
1297/// also works on systems that don't have a clock (such as embedded timers)
1298pub type GetSystemTimeCallbackType = extern "C" fn() -> Instant;
1299#[repr(C)]
1300pub struct GetSystemTimeCallback {
1301    pub cb: GetSystemTimeCallbackType,
1302}
1303impl_callback_simple!(GetSystemTimeCallback);
1304
1305/// Default implementation that gets the current system time.
1306///
1307/// On WASM targets `std::time::Instant::now()` panics, so we fall back to
1308/// a zero-tick instant instead.
1309#[cfg(all(feature = "std", not(target_arch = "wasm32")))]
1310#[must_use]
1311pub extern "C" fn get_system_time_libstd() -> Instant {
1312    // Honours the injectable E2E test clock (see TEST_CLOCK_OFFSET_MS).
1313    std_now_with_test_offset().into()
1314}
1315
1316/// Fallback for WASM (where `Instant::now()` panics) and no-std targets.
1317#[cfg(any(not(feature = "std"), target_arch = "wasm32"))]
1318pub extern "C" fn get_system_time_libstd() -> Instant {
1319    Instant::Tick(SystemTick::new(0))
1320}
1321
1322/// Callback to check if a thread has finished execution.
1323pub type CheckThreadFinishedCallbackType =
1324    extern "C" fn(/* dropcheck */ *const c_void) -> bool;
1325/// Wrapper for thread completion check callback.
1326#[repr(C)]
1327pub struct CheckThreadFinishedCallback {
1328    pub cb: CheckThreadFinishedCallbackType,
1329}
1330impl_callback_simple!(CheckThreadFinishedCallback);
1331
1332/// Callback to send a message to a background thread.
1333pub type LibrarySendThreadMsgCallbackType =
1334    extern "C" fn(/* Sender<ThreadSendMsg> */ *const c_void, ThreadSendMsg) -> bool;
1335/// Wrapper for thread message send callback.
1336#[repr(C)]
1337pub struct LibrarySendThreadMsgCallback {
1338    pub cb: LibrarySendThreadMsgCallbackType,
1339}
1340impl_callback_simple!(LibrarySendThreadMsgCallback);
1341
1342/// Callback for a running thread to receive messages from the main thread.
1343pub type ThreadRecvCallbackType =
1344    extern "C" fn(/* receiver.ptr */ *const c_void) -> OptionThreadSendMsg;
1345/// Wrapper for thread message receive callback.
1346#[repr(C)]
1347pub struct ThreadRecvCallback {
1348    pub cb: ThreadRecvCallbackType,
1349}
1350impl_callback_simple!(ThreadRecvCallback);
1351
1352/// Callback to destroy a `ThreadReceiver`.
1353pub type ThreadReceiverDestructorCallbackType = extern "C" fn(*mut ThreadReceiverInner);
1354/// Wrapper for thread receiver destructor callback.
1355#[repr(C)]
1356pub struct ThreadReceiverDestructorCallback {
1357    pub cb: ThreadReceiverDestructorCallbackType,
1358}
1359impl_callback_simple!(ThreadReceiverDestructorCallback);
1360
1361#[cfg(test)]
1362#[path = "task_test.rs"]
1363mod task_test;