Skip to main content

gpui_kit/
state.rs

1//! Explicit async states for truthful user interfaces.
2
3/// The states a value a host is fetching can be in.
4///
5/// Empty, unavailable, and failed are separate variants on purpose: a refusal
6/// rendered as an absence of data is a lie about the host.
7#[derive(Debug, Clone, PartialEq, Eq, Default)]
8pub enum Loadable<T, E = String> {
9    #[default]
10    Idle,
11    Loading,
12    Ready(T),
13    Empty,
14    Unavailable(String),
15    Error(E),
16}
17
18impl<T, E> Loadable<T, E> {
19    pub fn value(&self) -> Option<&T> {
20        match self {
21            Self::Ready(value) => Some(value),
22            _ => None,
23        }
24    }
25
26    pub fn is_loading(&self) -> bool {
27        matches!(self, Self::Loading)
28    }
29
30    pub fn map<U>(self, map: impl FnOnce(T) -> U) -> Loadable<U, E> {
31        match self {
32            Self::Idle => Loadable::Idle,
33            Self::Loading => Loadable::Loading,
34            Self::Ready(value) => Loadable::Ready(map(value)),
35            Self::Empty => Loadable::Empty,
36            Self::Unavailable(reason) => Loadable::Unavailable(reason),
37            Self::Error(error) => Loadable::Error(error),
38        }
39    }
40}
41
42/// A value and, separately, what is currently happening to it.
43///
44/// Splitting the two is what lets a failed refresh keep the last verified
45/// value on screen instead of replacing it with an error.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct AsyncValue<T, E = String> {
48    pub value: Option<T>,
49    pub status: AsyncStatus<E>,
50}
51
52/// What is happening to an [`AsyncValue`] right now.
53#[derive(Debug, Clone, PartialEq, Eq, Default)]
54pub enum AsyncStatus<E = String> {
55    #[default]
56    Idle,
57    Loading,
58    Refreshing,
59    Ready,
60    Empty,
61    Unavailable(String),
62    Error(E),
63}
64
65impl<T, E> AsyncValue<T, E> {
66    pub fn loading() -> Self {
67        Self {
68            value: None,
69            status: AsyncStatus::Loading,
70        }
71    }
72
73    pub fn ready(value: T) -> Self {
74        Self {
75            value: Some(value),
76            status: AsyncStatus::Ready,
77        }
78    }
79
80    pub fn refresh(&mut self) {
81        self.status = AsyncStatus::Refreshing;
82    }
83
84    pub fn fail_refresh(&mut self, error: E) {
85        self.status = AsyncStatus::Error(error);
86    }
87
88    pub fn is_stale(&self) -> bool {
89        self.value.is_some()
90            && matches!(self.status, AsyncStatus::Refreshing | AsyncStatus::Error(_))
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn a_refresh_failure_does_not_erase_real_data() {
100        let mut value = AsyncValue::<_, &str>::ready(vec!["real"]);
101        value.refresh();
102        value.fail_refresh("offline");
103        assert_eq!(value.value.as_deref(), Some(["real"].as_slice()));
104        assert!(value.is_stale());
105    }
106
107    #[test]
108    fn unavailable_is_not_empty() {
109        let unavailable: Loadable<Vec<u8>> = Loadable::Unavailable("unsupported".into());
110        assert_ne!(unavailable, Loadable::Empty);
111    }
112}