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    ///
59    /// # Panics
60    ///
61    /// Panics if called in a non-app thread.
62    pub fn mode(&self) -> InstantMode {
63        INSTANT_SV.read().mode
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        let mut sv = INSTANT_SV.write();
76        sv.mode = mode;
77        if let InstantMode::Now = mode {
78            sv.now = None;
79        }
80    }
81
82    /// Set the [`INSTANT.now`] for the app threads.
83    ///
84    /// # Panics
85    ///
86    /// Panics if the mode is [`InstantMode::Now`].
87    ///
88    /// [`INSTANT.now`]: INSTANT::now
89    pub fn set_now(&self, now: DInstant) {
90        let mut sv = INSTANT_SV.write();
91        if let InstantMode::Now = sv.mode {
92            panic!("cannot set now with `TimeMode::Now`");
93        }
94        sv.now = Some(now);
95    }
96
97    /// Set the [`INSTANT.now`] for the app threads to the current time plus `advance`.
98    ///
99    /// # Panics
100    ///
101    /// Panics if the mode is not [`InstantMode::Manual`].
102    ///
103    /// [`INSTANT.now`]: INSTANT::now
104    pub fn advance_now(&self, advance: Duration) {
105        let mut sv = INSTANT_SV.write();
106        if let InstantMode::Manual = sv.mode {
107            *sv.now.get_or_insert_with(|| DInstant(INSTANT.epoch().elapsed())) += advance;
108        } else {
109            panic!("cannot advance now, not `InstantMode::Manual`");
110        }
111    }
112
113    /// Unset the custom now value.
114    pub fn unset_now(&self) {
115        INSTANT_SV.write().now = None;
116    }
117
118    /// Gets the custom now value.
119    ///
120    /// This value is returned by [`INSTANT.now`] if set.
121    ///
122    /// [`INSTANT.now`]: INSTANT::now
123    pub fn custom_now(&self) -> Option<DInstant> {
124        INSTANT_SV.read().now
125    }
126
127    /// If mode is [`InstantMode::UpdatePaused`] sets the app custom_now to the current time and returns
128    /// an object that unsets the custom now on drop.
129    pub fn pause_for_update(&self) -> Option<InstantUpdatePause> {
130        let mut sv = INSTANT_SV.write();
131        match sv.mode {
132            InstantMode::UpdatePaused => {
133                let now = DInstant(INSTANT.epoch().elapsed());
134                sv.now = Some(now);
135                Some(InstantUpdatePause { now })
136            }
137            _ => None,
138        }
139    }
140}
141
142/// Unset now on drop.
143///
144/// The time is only unset if it is still set to the same pause time.
145#[must_use = "unset_now on drop"]
146pub struct InstantUpdatePause {
147    now: DInstant,
148}
149impl Drop for InstantUpdatePause {
150    fn drop(&mut self) {
151        let mut sv = INSTANT_SV.write();
152        if sv.now == Some(self.now) {
153            sv.now = None;
154        }
155    }
156}
157
158/// Duration elapsed since an epoch.
159///
160/// By default this is the duration elapsed since the first usage of [`INSTANT`] in the process.
161#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
162pub struct DInstant(Duration);
163impl DInstant {
164    /// Returns the amount of time elapsed since this instant.
165    pub fn elapsed(self) -> Duration {
166        INSTANT.now().0 - self.0
167    }
168
169    /// Returns the amount of time elapsed from another instant to this one,
170    /// or zero duration if that instant is later than this one.
171    pub fn duration_since(self, earlier: DInstant) -> Duration {
172        self.0 - earlier.0
173    }
174
175    /// Returns `Some(t)` where t is the time `self + duration` if t can be represented.
176    pub fn checked_add(&self, duration: Duration) -> Option<DInstant> {
177        self.0.checked_add(duration).map(Self)
178    }
179
180    /// Returns `Some(t)`` where t is the time `self - duration` if `duration` greater then the elapsed time
181    /// since the process start.
182    pub fn checked_sub(self, duration: Duration) -> Option<DInstant> {
183        self.0.checked_sub(duration).map(Self)
184    }
185
186    /// Returns the amount of time elapsed from another instant to this one, or None if that instant is later than this one.
187    pub fn checked_duration_since(&self, earlier: DInstant) -> Option<Duration> {
188        self.0.checked_sub(earlier.0)
189    }
190
191    /// Returns the amount of time elapsed from another instant to this one, or zero duration if that instant is later than this one.
192    pub fn saturating_duration_since(&self, earlier: DInstant) -> Duration {
193        self.0.saturating_sub(earlier.0)
194    }
195
196    /// Earliest instant.
197    pub const EPOCH: DInstant = DInstant(Duration::ZERO);
198
199    /// The maximum representable instant.
200    pub const MAX: DInstant = DInstant(Duration::MAX);
201}
202impl ops::Add<Duration> for DInstant {
203    type Output = Self;
204
205    fn add(self, rhs: Duration) -> Self {
206        Self(self.0.saturating_add(rhs))
207    }
208}
209impl ops::AddAssign<Duration> for DInstant {
210    fn add_assign(&mut self, rhs: Duration) {
211        self.0 = self.0.saturating_add(rhs);
212    }
213}
214impl ops::Sub<Duration> for DInstant {
215    type Output = Self;
216
217    fn sub(self, rhs: Duration) -> Self {
218        Self(self.0.saturating_sub(rhs))
219    }
220}
221impl ops::SubAssign<Duration> for DInstant {
222    fn sub_assign(&mut self, rhs: Duration) {
223        self.0 = self.0.saturating_sub(rhs);
224    }
225}
226impl ops::Sub for DInstant {
227    type Output = Duration;
228
229    fn sub(self, rhs: Self) -> Self::Output {
230        self.0.saturating_sub(rhs.0)
231    }
232}
233impl From<DInstant> for Instant {
234    fn from(t: DInstant) -> Self {
235        INSTANT.epoch() + t.0
236    }
237}
238impl From<Instant> for DInstant {
239    fn from(value: Instant) -> Self {
240        DInstant(value - INSTANT.epoch())
241    }
242}
243
244/// Defines how the [`INSTANT.now`] value updates in the app.
245///
246/// [`INSTANT.now`]: INSTANT::now
247#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
248pub enum InstantMode {
249    /// Calls during an update pass (or layout, render, etc.) read the same time.
250    /// Other calls to `now` resamples the time.
251    UpdatePaused,
252    /// Every call to `now` resamples the time.
253    Now,
254    /// Time is controlled by the app.
255    Manual,
256}
257
258static EPOCH: RwLock<Option<Instant>> = RwLock::new(None);
259
260app_local! {
261    static INSTANT_SV: InstantService = const {
262        InstantService {
263            mode: InstantMode::UpdatePaused,
264            now: None,
265        }
266    };
267}
268
269struct InstantService {
270    mode: InstantMode,
271    now: Option<DInstant>,
272}
273
274/// Represents a timeout instant.
275///
276/// Deadlines and timeouts can be specified as a [`DInstant`] in the future or as a [`Duration`] from now, both
277/// of these types can be converted to this `struct`.
278///
279/// # Examples
280///
281/// In the example below the timer function accepts `Deadline`, `DInstant` and `Duration` inputs.
282///
283/// ```
284/// # use zng_time::*;
285/// # trait TimeUnits { fn secs(self) -> std::time::Duration where Self: Sized { std::time::Duration::ZERO } }
286/// # impl TimeUnits for i32 { }
287/// fn timer(deadline: impl Into<Deadline>) {
288///     let deadline = deadline.into();
289///     // ..
290/// }
291///
292/// timer(5.secs());
293/// ```
294#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
295pub struct Deadline(pub DInstant);
296impl Deadline {
297    /// New deadline from now + `dur`.
298    pub fn timeout(dur: Duration) -> Self {
299        Deadline(INSTANT.now() + dur)
300    }
301
302    /// Returns `true` if the deadline was reached.
303    pub fn has_elapsed(self) -> bool {
304        self.0 <= INSTANT.now()
305    }
306
307    /// Returns the time left until the deadline is reached.
308    pub fn time_left(self) -> Option<Duration> {
309        self.0.checked_duration_since(INSTANT.now())
310    }
311
312    /// Returns the deadline further into the past or closest to now.
313    pub fn min(self, other: Deadline) -> Deadline {
314        Deadline(self.0.min(other.0))
315    }
316
317    /// Returns the deadline further into the future.
318    pub fn max(self, other: Deadline) -> Deadline {
319        Deadline(self.0.max(other.0))
320    }
321
322    /// Deadline that is always elapsed.
323    pub const ELAPSED: Deadline = Deadline(DInstant::EPOCH);
324
325    /// Deadline that is practically never reached.
326    pub const MAX: Deadline = Deadline(DInstant::MAX);
327}
328impl fmt::Display for Deadline {
329    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
330        let dur = self.0 - INSTANT.now();
331        write!(f, "{dur:?} left")
332    }
333}
334impl fmt::Debug for Deadline {
335    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
336        write!(f, "Deadline({self})")
337    }
338}
339impl From<DInstant> for Deadline {
340    fn from(value: DInstant) -> Self {
341        Deadline(value)
342    }
343}
344impl From<Duration> for Deadline {
345    fn from(value: Duration) -> Self {
346        Deadline::timeout(value)
347    }
348}
349impl From<Instant> for Deadline {
350    fn from(value: Instant) -> Self {
351        DInstant::from(value).into()
352    }
353}
354impl ops::Add<Duration> for Deadline {
355    type Output = Self;
356
357    fn add(mut self, rhs: Duration) -> Self {
358        self.0 += rhs;
359        self
360    }
361}
362impl ops::AddAssign<Duration> for Deadline {
363    fn add_assign(&mut self, rhs: Duration) {
364        self.0 += rhs;
365    }
366}
367impl ops::Sub<Duration> for Deadline {
368    type Output = Self;
369
370    fn sub(mut self, rhs: Duration) -> Self {
371        self.0 -= rhs;
372        self
373    }
374}
375impl ops::SubAssign<Duration> for Deadline {
376    fn sub_assign(&mut self, rhs: Duration) {
377        self.0 -= rhs;
378    }
379}