Skip to main content

sequoia_view/
async_view.rs

1use std::rc::Rc;
2
3use sequoia_async::{AsyncState, Resource};
4use sequoia_input::{CursorIcon, Event, PointerButton, PointerEvent};
5use sequoia_layout::LayoutStyle;
6use sequoia_style::VisualState;
7
8use crate::any::AnyView;
9use crate::view::{EventCtx, View};
10use crate::views::{Column, Text, Unit};
11
12type BuildFn = Rc<dyn Fn() -> AnyView>;
13
14pub struct AsyncView<T: Clone + PartialEq + 'static> {
15    resource: Resource<T>,
16    loading: BuildFn,
17    data: Rc<dyn Fn(&T) -> AnyView>,
18    error: Rc<dyn Fn(&str) -> AnyView>,
19    idle: Option<BuildFn>,
20    cancelled: Option<BuildFn>,
21    retry_label: Option<String>,
22    on_retry: Option<Rc<dyn Fn()>>,
23    progress: Option<sequoia_reactive::Signal<f32>>,
24}
25
26impl<T: Clone + PartialEq + 'static> Clone for AsyncView<T> {
27    fn clone(&self) -> Self {
28        Self {
29            resource: self.resource.clone(),
30            loading: self.loading.clone(),
31            data: self.data.clone(),
32            error: self.error.clone(),
33            idle: self.idle.clone(),
34            cancelled: self.cancelled.clone(),
35            retry_label: self.retry_label.clone(),
36            on_retry: self.on_retry.clone(),
37            progress: self.progress.clone(),
38        }
39    }
40}
41
42impl<T: Clone + PartialEq + 'static> AsyncView<T> {
43    pub fn new(resource: Resource<T>) -> Self {
44        Self {
45            resource,
46            loading: Rc::new(|| AnyView::new(Text::new("Landing…").size(12.0))),
47            data: Rc::new(|_| AnyView::new(Unit)),
48            error: Rc::new(|e: &str| AnyView::new(Text::new(format!("Error: {e}")).size(12.0))),
49            idle: None,
50            cancelled: None,
51            retry_label: None,
52            on_retry: None,
53            progress: None,
54        }
55    }
56
57    pub fn loading<V: View + 'static>(mut self, f: impl Fn() -> V + 'static) -> Self {
58        self.loading = Rc::new(move || AnyView::new(f()));
59        self
60    }
61
62    pub fn data<V: View + 'static>(mut self, f: impl Fn(&T) -> V + 'static) -> Self {
63        self.data = Rc::new(move |v| AnyView::new(f(v)));
64        self
65    }
66
67    pub fn error<V: View + 'static>(mut self, f: impl Fn(&str) -> V + 'static) -> Self {
68        self.error = Rc::new(move |e| AnyView::new(f(e)));
69        self
70    }
71
72    pub fn idle<V: View + 'static>(mut self, f: impl Fn() -> V + 'static) -> Self {
73        self.idle = Some(Rc::new(move || AnyView::new(f())));
74        self
75    }
76
77    pub fn cancelled<V: View + 'static>(mut self, f: impl Fn() -> V + 'static) -> Self {
78        self.cancelled = Some(Rc::new(move || AnyView::new(f())));
79        self
80    }
81
82    pub fn retry(mut self, label: impl Into<String>, on_retry: impl Fn() + 'static) -> Self {
83        self.retry_label = Some(label.into());
84        self.on_retry = Some(Rc::new(on_retry));
85        self
86    }
87
88    pub fn progress(mut self, fraction: sequoia_reactive::Signal<f32>) -> Self {
89        self.progress = Some(fraction);
90        self
91    }
92
93    pub fn resource(&self) -> Resource<T> {
94        self.resource.clone()
95    }
96
97    fn loading_with_progress(&self) -> AnyView {
98        let body = (self.loading)();
99        match &self.progress {
100            Some(frac) => {
101                let bar = BarView { frac: frac.clone() };
102                AnyView::new(Column::new().fill_size().gap(8.0).child(bar).child(body))
103            }
104            None => body,
105        }
106    }
107}
108
109impl<T: Clone + PartialEq + 'static> View for AsyncView<T> {
110    fn body(&self) -> impl View + 'static {
111        Unit
112    }
113
114    fn children_override(&self) -> Option<Vec<AnyView>> {
115        let state = self.resource.state().get();
116        let view = match &state {
117            AsyncState::Idle => match &self.idle {
118                Some(f) => f(),
119                None => self.loading_with_progress(),
120            },
121            AsyncState::Loading => self.loading_with_progress(),
122            AsyncState::Ready(v) => (self.data)(v),
123            AsyncState::Failed(e) => {
124                let err = (self.error)(e);
125                match (&self.retry_label, &self.on_retry) {
126                    (Some(label), Some(cb)) => {
127                        let btn = RetryBtn {
128                            label: label.clone(),
129                            on_retry: cb.clone(),
130                        };
131                        AnyView::new(Column::new().gap(8.0).child(err).child(btn))
132                    }
133                    _ => err,
134                }
135            }
136            AsyncState::Cancelled => match &self.cancelled {
137                Some(f) => f(),
138                None => match &self.idle {
139                    Some(f) => f(),
140                    None => self.loading_with_progress(),
141                },
142            },
143            _ => self.loading_with_progress(),
144        };
145        Some(vec![view])
146    }
147
148    fn layout_style(&self) -> LayoutStyle {
149        LayoutStyle::default()
150    }
151}
152
153#[derive(Clone)]
154struct BarView {
155    frac: sequoia_reactive::Signal<f32>,
156}
157
158impl View for BarView {
159    fn body(&self) -> impl View + 'static {
160        Unit
161    }
162
163    fn layout_style(&self) -> LayoutStyle {
164        LayoutStyle {
165            width: sequoia_core::Length::Fill(1.0),
166            height: sequoia_core::Length::Px(6.0),
167            ..LayoutStyle::default()
168        }
169    }
170
171    fn paint_self(&self, ctx: &mut crate::view::PaintCtx) {
172        let _ = self.frac.get_untracked();
173        let theme = ctx.theme();
174        let r = ctx.rect;
175        ctx.quad(r, theme.surface, r.size.height * 0.5);
176        let w = r.size.width * self.frac.get_untracked().clamp(0.0, 1.0);
177        if w > 0.5 {
178            ctx.quad(
179                sequoia_core::Rect::from_xywh(r.left(), r.top(), w, r.size.height),
180                theme.accent,
181                r.size.height * 0.5,
182            );
183        }
184    }
185}
186
187#[derive(Clone)]
188struct RetryBtn {
189    label: String,
190    on_retry: Rc<dyn Fn()>,
191}
192
193impl View for RetryBtn {
194    fn body(&self) -> impl View + 'static {
195        Unit
196    }
197
198    fn children_override(&self) -> Option<Vec<AnyView>> {
199        Some(vec![AnyView::new(
200            Text::new(format!("↻ {}", self.label)).size(12.0).bold(),
201        )])
202    }
203
204    fn layout_style(&self) -> LayoutStyle {
205        LayoutStyle::default()
206    }
207
208    fn has_visual_state(&self) -> bool {
209        true
210    }
211
212    fn cursor_icon(&self, _state: VisualState) -> CursorIcon {
213        CursorIcon::Pointer
214    }
215
216    fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
217        match event {
218            Event::Pointer(PointerEvent::Up {
219                button: PointerButton::Left,
220                pos,
221                ..
222            }) if ctx.rect.contains(*pos) => {
223                (self.on_retry)();
224                ctx.handled();
225            }
226            _ => {}
227        }
228    }
229}