sequoia_view/
async_view.rs1use 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}