zng_time/
lib.rs

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