zng_time/
lib.rs

1#![doc(html_favicon_url = "https://zng-ui.github.io/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://zng-ui.github.io/res/zng-logo.png")]
3//!
4//! Configurable instant type and service.
5//!
6//! # Crate
7//!
8#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
9
10use std::{fmt, ops, time::Duration};
11
12use parking_lot::RwLock;
13use zng_app_context::app_local;
14
15#[cfg(not(target_arch = "wasm32"))]
16use std::time::Instant;
17
18#[cfg(target_arch = "wasm32")]
19use web_time::Instant;
20
21/// Instant service.
22pub struct INSTANT;
23impl INSTANT {
24    /// Returns an instant corresponding to "now" or an instant configured by the app.
25    ///
26    /// This method can be called in non-app threads. Apps can override this time in app threads,
27    /// by default the time is *paused* for each widget OP pass so that all widgets observe the same
28    /// time on the same pass, you can use [`mode`](Self::mode) to check how `now` updates and you
29    /// can use the `APP.pause_time_for_update` variable to disable pausing.
30    pub fn now(&self) -> DInstant {
31        if zng_app_context::LocalContext::current_app().is_some()
32            && let Some(now) = INSTANT_SV.read().now
33        {
34            return now;
35        }
36        DInstant(self.epoch().elapsed())
37    }
38
39    /// Instant of first usage of the [`INSTANT`] service in the process, minus one day.
40    pub fn epoch(&self) -> Instant {
41        if let Some(t) = *EPOCH.read() {
42            return t;
43        }
44        *EPOCH.write().get_or_insert_with(|| {
45            let mut now = Instant::now();
46            // some CI machines (Github Windows) fail to subtract 1 day.
47            for t in [60 * 60 * 24, 60 * 60, 60 * 30, 60 * 15, 60 * 10, 60] {
48                if let Some(t) = now.checked_sub(Duration::from_secs(t)) {
49                    now = t;
50                    break;
51                }
52            }
53            now
54        })
55    }
56
57    /// Defines how the `now` value updates.
58    pub fn mode(&self) -> InstantMode {
59        if zng_app_context::LocalContext::current_app().is_some() {
60            INSTANT_SV.read().mode
61        } else {
62            InstantMode::Now
63        }
64    }
65}
66
67/// App control of the [`INSTANT`] service in an app context.
68#[expect(non_camel_case_types)]
69pub struct INSTANT_APP;
70impl INSTANT_APP {
71    /// Set how the app controls the time.
72    ///
73    /// If mode is set to [`InstantMode::Now`] the custom now is unset.
74    pub fn set_mode(&self, mode: InstantMode) {
75        tracing::trace!("set INSTANT mode to {mode:?}");
76        let mut sv = INSTANT_SV.write();
77        sv.mode = mode;
78        if let InstantMode::Now = mode {
79            sv.now = None;
80        }
81    }
82
83    /// Set the [`INSTANT.now`] for the app threads.
84    ///
85    /// # Panics
86    ///
87    /// Panics if the mode is [`InstantMode::Now`].
88    ///
89    /// [`INSTANT.now`]: INSTANT::now
90    pub fn set_now(&self, now: DInstant) {
91        tracing::trace!("set INSTANT.now to {now:?}");
92
93        let mut sv = INSTANT_SV.write();
94        if let InstantMode::Now = sv.mode {
95            panic!("cannot set now with `TimeMode::Now`");
96        }
97        sv.now = Some(now);
98    }
99
100    /// Set the [`INSTANT.now`] for the app threads to the current time plus `advance`.
101    ///
102    /// # Panics
103    ///
104    /// Panics if the mode is not [`InstantMode::Manual`].
105    ///
106    /// [`INSTANT.now`]: INSTANT::now
107    pub fn advance_now(&self, advance: Duration) {
108        let mut sv = INSTANT_SV.write();
109        if let InstantMode::Manual = sv.mode {
110            tracing::trace!("advance INSTANT.now by {advance:?}");
111            *sv.now.get_or_insert_with(|| DInstant(INSTANT.epoch().elapsed())) += advance;
112        } else {
113            panic!("cannot advance now, not `InstantMode::Manual`");
114        }
115    }
116
117    /// Unset the custom now value.
118    pub fn unset_now(&self) {
119        tracing::trace!("unset custom INSTANT.now");
120        INSTANT_SV.write().now = None;
121    }
122
123    /// Gets the custom now value.
124    ///
125    /// This value is returned by [`INSTANT.now`] if set.
126    ///
127    /// [`INSTANT.now`]: INSTANT::now
128    pub fn custom_now(&self) -> Option<DInstant> {
129        INSTANT_SV.read().now
130    }
131
132    /// If mode is [`InstantMode::UpdatePaused`] sets the app custom_now to the current time and returns
133    /// an object that unsets the custom now on drop.
134    pub fn pause_for_update(&self) -> Option<InstantUpdatePause> {
135        let mut sv = INSTANT_SV.write();
136        match sv.mode {
137            InstantMode::UpdatePaused => {
138                tracing::trace!("pause time for update");
139                let now = DInstant(INSTANT.epoch().elapsed());
140                sv.now = Some(now);
141                Some(InstantUpdatePause { now })
142            }
143            _ => None,
144        }
145    }
146}
147
148/// Unset now on drop.
149///
150/// The time is only unset if it is still set to the same pause time.
151#[must_use = "unset_now on drop"]
152pub struct InstantUpdatePause {
153    now: DInstant,
154}
155impl Drop for InstantUpdatePause {
156    fn drop(&mut self) {
157        let mut sv = INSTANT_SV.write();
158        if sv.now == Some(self.now) {
159            tracing::trace!("unpause time after update");
160            sv.now = None;
161        }
162    }
163}
164
165/// Duration elapsed since an epoch.
166///
167/// By default this is the duration elapsed since the first usage of [`INSTANT`] in the process.
168#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
169pub struct DInstant(Duration);
170impl DInstant {
171    /// Returns the amount of time elapsed since this instant.
172    pub fn elapsed(self) -> Duration {
173        INSTANT.now().0 - self.0
174    }
175
176    /// Returns the amount of time elapsed from another instant to this one,
177    /// or zero duration if that instant is later than this one.
178    pub fn duration_since(self, earlier: DInstant) -> Duration {
179        self.0 - earlier.0
180    }
181
182    /// Returns `Some(t)` where t is the time `self + duration` if t can be represented.
183    pub fn checked_add(&self, duration: Duration) -> Option<DInstant> {
184        self.0.checked_add(duration).map(Self)
185    }
186
187    /// Returns `Some(t)`` where t is the time `self - duration` if `duration` greater then the elapsed time
188    /// since the process start.
189    pub fn checked_sub(self, duration: Duration) -> Option<DInstant> {
190        self.0.checked_sub(duration).map(Self)
191    }
192
193    /// Returns the amount of time elapsed from another instant to this one, or None if that instant is later than this one.
194    pub fn checked_duration_since(&self, earlier: DInstant) -> Option<Duration> {
195        self.0.checked_sub(earlier.0)
196    }
197
198    /// Returns the amount of time elapsed from another instant to this one, or zero duration if that instant is later than this one.
199    pub fn saturating_duration_since(&self, earlier: DInstant) -> Duration {
200        self.0.saturating_sub(earlier.0)
201    }
202
203    /// Earliest instant.
204    pub const EPOCH: DInstant = DInstant(Duration::ZERO);
205
206    /// The maximum representable instant.
207    pub const MAX: DInstant = DInstant(Duration::MAX);
208}
209impl ops::Add<Duration> for DInstant {
210    type Output = Self;
211
212    fn add(self, rhs: Duration) -> Self {
213        Self(self.0.saturating_add(rhs))
214    }
215}
216impl ops::AddAssign<Duration> for DInstant {
217    fn add_assign(&mut self, rhs: Duration) {
218        self.0 = self.0.saturating_add(rhs);
219    }
220}
221impl ops::Sub<Duration> for DInstant {
222    type Output = Self;
223
224    fn sub(self, rhs: Duration) -> Self {
225        Self(self.0.saturating_sub(rhs))
226    }
227}
228impl ops::SubAssign<Duration> for DInstant {
229    fn sub_assign(&mut self, rhs: Duration) {
230        self.0 = self.0.saturating_sub(rhs);
231    }
232}
233impl ops::Sub for DInstant {
234    type Output = Duration;
235
236    fn sub(self, rhs: Self) -> Self::Output {
237        self.0.saturating_sub(rhs.0)
238    }
239}
240impl From<DInstant> for Instant {
241    fn from(t: DInstant) -> Self {
242        INSTANT.epoch() + t.0
243    }
244}
245impl From<Instant> for DInstant {
246    fn from(value: Instant) -> Self {
247        DInstant(value - INSTANT.epoch())
248    }
249}
250
251/// Defines how the [`INSTANT.now`] value updates in the app.
252///
253/// [`INSTANT.now`]: INSTANT::now
254#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
255pub enum InstantMode {
256    /// Calls during an update pass (or layout, render, etc.) read the same time.
257    /// Other calls to `now` resamples the time.
258    UpdatePaused,
259    /// Every call to `now` resamples the time.
260    Now,
261    /// Time is controlled by the app.
262    Manual,
263}
264
265static EPOCH: RwLock<Option<Instant>> = RwLock::new(None);
266
267app_local! {
268    static INSTANT_SV: InstantService = const {
269        InstantService {
270            mode: InstantMode::UpdatePaused,
271            now: None,
272        }
273    };
274}
275
276struct InstantService {
277    mode: InstantMode,
278    now: Option<DInstant>,
279}
280
281/// Represents a timeout instant.
282///
283/// Deadlines and timeouts can be specified as a [`DInstant`] in the future or as a [`Duration`] from now, both
284/// of these types can be converted to this `struct`.
285///
286/// # Examples
287///
288/// In the example below the timer function accepts `Deadline`, `DInstant` and `Duration` inputs.
289///
290/// ```
291/// # use zng_time::*;
292/// # trait TimeUnits { fn secs(self) -> std::time::Duration where Self: Sized { std::time::Duration::ZERO } }
293/// # impl TimeUnits for i32 { }
294/// fn timer(deadline: impl Into<Deadline>) {
295///     let deadline = deadline.into();
296///     // ..
297/// }
298///
299/// timer(5.secs());
300/// ```
301#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
302pub struct Deadline(pub DInstant);
303impl Deadline {
304    /// New deadline from now + `dur`.
305    pub fn timeout(dur: Duration) -> Self {
306        Deadline(INSTANT.now() + dur)
307    }
308
309    /// Returns `true` if the deadline was reached.
310    pub fn has_elapsed(self) -> bool {
311        self.0 <= INSTANT.now()
312    }
313
314    /// Returns the time left until the deadline is reached.
315    pub fn time_left(self) -> Option<Duration> {
316        self.0.checked_duration_since(INSTANT.now())
317    }
318
319    /// Returns the deadline further into the past or closest to now.
320    pub fn min(self, other: Deadline) -> Deadline {
321        Deadline(self.0.min(other.0))
322    }
323
324    /// Returns the deadline further into the future.
325    pub fn max(self, other: Deadline) -> Deadline {
326        Deadline(self.0.max(other.0))
327    }
328
329    /// Deadline that is always elapsed.
330    pub const ELAPSED: Deadline = Deadline(DInstant::EPOCH);
331
332    /// Deadline that is practically never reached.
333    pub const MAX: Deadline = Deadline(DInstant::MAX);
334}
335impl fmt::Display for Deadline {
336    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
337        let dur = self.0 - INSTANT.now();
338        write!(f, "{dur:?} left")
339    }
340}
341impl fmt::Debug for Deadline {
342    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
343        write!(f, "Deadline({self})")
344    }
345}
346impl From<DInstant> for Deadline {
347    fn from(value: DInstant) -> Self {
348        Deadline(value)
349    }
350}
351impl From<Duration> for Deadline {
352    fn from(value: Duration) -> Self {
353        Deadline::timeout(value)
354    }
355}
356impl From<Instant> for Deadline {
357    fn from(value: Instant) -> Self {
358        DInstant::from(value).into()
359    }
360}
361impl ops::Add<Duration> for Deadline {
362    type Output = Self;
363
364    fn add(mut self, rhs: Duration) -> Self {
365        self.0 += rhs;
366        self
367    }
368}
369impl ops::AddAssign<Duration> for Deadline {
370    fn add_assign(&mut self, rhs: Duration) {
371        self.0 += rhs;
372    }
373}
374impl ops::Sub<Duration> for Deadline {
375    type Output = Self;
376
377    fn sub(mut self, rhs: Duration) -> Self {
378        self.0 -= rhs;
379        self
380    }
381}
382impl ops::SubAssign<Duration> for Deadline {
383    fn sub_assign(&mut self, rhs: Duration) {
384        self.0 -= rhs;
385    }
386}