frus_widgets/asyncbuild.rs
1//! Work that finishes later, in a component: [`BuildContext::use_future`],
2//! [`BuildContext::use_stream`], and the builders over them — [`FutureBuilder`],
3//! [`StreamBuilder`] — with [`ValueListenableBuilder`], [`ListenableBuilder`] and [`Builder`]
4//! (milestone 585).
5//!
6//! The widget layer has no executor of its own and no dependency that brings one. The shell
7//! hands it one on the way up ([`set_task_spawner`]), as it hands it the image fetcher: its
8//! own executor natively, the browser's on the Web. A value that arrives is written where the
9//! component reads it, and the next frame rebuilds.
10
11use std::future::Future;
12use std::pin::Pin;
13use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
14use std::sync::{Arc, Mutex};
15
16use crate::component::{component, BuildContext, Component};
17use crate::notifier::{Listenable, ValueNotifier};
18use crate::widget::Widget;
19
20/// `Send` where there are threads, and nothing on the Web, where a future never leaves the
21/// thread it was made on — the bound the work handed to a component must meet.
22#[cfg(not(target_arch = "wasm32"))]
23pub trait MaybeSend: Send {}
24#[cfg(not(target_arch = "wasm32"))]
25impl<T: Send> MaybeSend for T {}
26/// `Send` where there are threads, and nothing on the Web.
27#[cfg(target_arch = "wasm32")]
28pub trait MaybeSend {}
29#[cfg(target_arch = "wasm32")]
30impl<T> MaybeSend for T {}
31
32/// A piece of work handed to the executor: it runs to its end and gives nothing back.
33#[cfg(not(target_arch = "wasm32"))]
34pub type BoxedTask = Pin<Box<dyn Future<Output = ()> + Send>>;
35/// A piece of work handed to the executor: it runs to its end and gives nothing back.
36#[cfg(target_arch = "wasm32")]
37pub type BoxedTask = Pin<Box<dyn Future<Output = ()>>>;
38
39/// Runs a [`BoxedTask`] somewhere, to its end.
40pub type TaskSpawner = fn(BoxedTask);
41
42static SPAWNER: Mutex<Option<TaskSpawner>> = Mutex::new(None);
43static IN_FLIGHT: AtomicUsize = AtomicUsize::new(0);
44static ARRIVALS: AtomicU64 = AtomicU64::new(0);
45
46/// Tells the widget layer how to run the work a component starts.
47///
48/// An application using the framework's own shell never calls this: the shell registers its
49/// executor on the way up. With none registered, each piece of work gets a thread of its own
50/// natively, and does not run on the Web.
51pub fn set_task_spawner(spawner: TaskSpawner) {
52 if let Ok(mut guard) = SPAWNER.lock() {
53 *guard = Some(spawner);
54 }
55}
56
57/// How many pieces of work a component started are still running. The shell keeps frames
58/// coming while there are any, so that what they bring is shown.
59pub fn tasks_in_flight() -> usize {
60 IN_FLIGHT.load(Ordering::SeqCst)
61}
62
63/// How many values pieces of work have brought so far, in this process. A shell keeps the
64/// count it last saw and rebuilds when it has grown: a count rather than a flag, so that one
65/// shell reading it takes nothing away from another.
66pub fn task_arrivals() -> u64 {
67 ARRIVALS.load(Ordering::SeqCst)
68}
69
70fn spawn(task: BoxedTask) {
71 let spawner = SPAWNER.lock().ok().and_then(|guard| *guard);
72 match spawner {
73 Some(spawner) => spawner(task),
74 None => fallback(task),
75 }
76}
77
78/// No executor registered: a thread of its own, parked while the work waits.
79#[cfg(not(target_arch = "wasm32"))]
80fn fallback(task: BoxedTask) {
81 std::thread::spawn(move || block_on(task));
82}
83
84/// No executor registered on the Web: there is no thread to give it, so it does not run.
85#[cfg(target_arch = "wasm32")]
86fn fallback(_task: BoxedTask) {}
87
88/// Drives `task` to its end on this thread, parking it while the task waits.
89#[cfg(not(target_arch = "wasm32"))]
90fn block_on(mut task: BoxedTask) {
91 use std::task::{Context, Wake, Waker};
92 struct Unpark(std::thread::Thread);
93 impl Wake for Unpark {
94 fn wake(self: Arc<Self>) {
95 self.0.unpark();
96 }
97 }
98 let waker = Waker::from(Arc::new(Unpark(std::thread::current())));
99 let mut cx = Context::from_waker(&waker);
100 while task.as_mut().poll(&mut cx).is_pending() {
101 std::thread::park();
102 }
103}
104
105/// Where a piece of work that finishes later has got to.
106#[derive(Clone, Copy, Debug, PartialEq, Eq)]
107pub enum ConnectionState {
108 /// Started, and nothing has come of it yet.
109 Waiting,
110 /// A stream that has given at least one value and has not finished.
111 Active,
112 /// Finished.
113 Done,
114}
115
116/// What a component knows of a piece of work that finishes later: where it has got to, and
117/// the last value it gave, if any — the reference's `AsyncSnapshot`.
118///
119/// An error is a value like any other: work that can fail gives a `Result`.
120#[derive(Clone, Debug, PartialEq)]
121pub struct AsyncSnapshot<T> {
122 /// Where the work has got to.
123 pub state: ConnectionState,
124 /// The last value it gave: a future's result once it is done, a stream's latest.
125 pub data: Option<T>,
126}
127
128impl<T> AsyncSnapshot<T> {
129 /// Started, nothing yet.
130 pub fn waiting() -> Self {
131 Self {
132 state: ConnectionState::Waiting,
133 data: None,
134 }
135 }
136
137 /// Whether a value has come.
138 pub fn has_data(&self) -> bool {
139 self.data.is_some()
140 }
141}
142
143type Slot<T> = Arc<Mutex<AsyncSnapshot<T>>>;
144
145/// Records that something arrived, so that the next frame rebuilds.
146fn arrived() {
147 ARRIVALS.fetch_add(1, Ordering::SeqCst);
148}
149
150/// Where a stream started by [`BuildContext::use_stream`] gives its values.
151pub struct StreamSink<T> {
152 slot: Slot<T>,
153}
154
155impl<T> StreamSink<T> {
156 /// Gives a value: the component sees it, as the latest, at the next frame.
157 pub fn add(&self, value: T) {
158 if let Ok(mut snapshot) = self.slot.lock() {
159 snapshot.state = ConnectionState::Active;
160 snapshot.data = Some(value);
161 }
162 arrived();
163 }
164}
165
166impl<'a> BuildContext<'a> {
167 /// Runs the future `make()` returns, **once until `deps` change**, and gives what is known
168 /// of it: waiting, then done with its value. A rebuild in between does not start it again.
169 ///
170 /// Natively the future runs on the shell's executor, so it must be `Send`; on the Web the
171 /// browser runs it. A value that arrives rebuilds the tree at the next frame. When `deps`
172 /// change, a new future starts, and what the old one gives is not shown.
173 ///
174 /// ```
175 /// use frus_widgets::{component, text, ConnectionState};
176 ///
177 /// let answer = component(|cx| {
178 /// let snapshot = cx.use_future((), || async { 6 * 7 });
179 /// Box::new(match snapshot.data {
180 /// Some(value) => text(format!("{value}")),
181 /// None => text("Working it out…"),
182 /// })
183 /// });
184 /// # let _ = answer;
185 /// ```
186 pub fn use_future<D, F>(&self, deps: D, make: impl FnOnce() -> F) -> AsyncSnapshot<F::Output>
187 where
188 D: PartialEq + 'static,
189 F: Future + MaybeSend + 'static,
190 F::Output: Clone + MaybeSend + 'static,
191 {
192 let slot = self.use_memo(deps, || {
193 let slot: Slot<F::Output> = Arc::new(Mutex::new(AsyncSnapshot::waiting()));
194 let written = slot.clone();
195 let future = make();
196 IN_FLIGHT.fetch_add(1, Ordering::SeqCst);
197 spawn(Box::pin(async move {
198 let value = future.await;
199 if let Ok(mut snapshot) = written.lock() {
200 *snapshot = AsyncSnapshot {
201 state: ConnectionState::Done,
202 data: Some(value),
203 };
204 }
205 IN_FLIGHT.fetch_sub(1, Ordering::SeqCst);
206 arrived();
207 }));
208 slot
209 });
210 let snapshot = slot.lock().map(|s| s.clone());
211 snapshot.unwrap_or_else(|_| AsyncSnapshot::waiting())
212 }
213
214 /// Runs the work `make` returns, **once until `deps` change**, handing it a
215 /// [`StreamSink`] to give its values to, and gives what is known of it: waiting, active
216 /// with the latest value, then done with the last one.
217 ///
218 /// The reference builds this on a stream type; here the stream is the work itself, giving
219 /// its values as it goes, which asks for no stream library.
220 ///
221 /// ```
222 /// use frus_widgets::{component, text};
223 ///
224 /// let countdown = component(|cx| {
225 /// let snapshot = cx.use_stream((), |sink| async move {
226 /// for left in (0..3).rev() {
227 /// sink.add(left);
228 /// }
229 /// });
230 /// Box::new(text(format!("{:?}", snapshot.data)))
231 /// });
232 /// # let _ = countdown;
233 /// ```
234 pub fn use_stream<D, T, F>(
235 &self,
236 deps: D,
237 make: impl FnOnce(StreamSink<T>) -> F,
238 ) -> AsyncSnapshot<T>
239 where
240 D: PartialEq + 'static,
241 T: Clone + MaybeSend + 'static,
242 F: Future<Output = ()> + MaybeSend + 'static,
243 {
244 let slot = self.use_memo(deps, || {
245 let slot: Slot<T> = Arc::new(Mutex::new(AsyncSnapshot::waiting()));
246 let written = slot.clone();
247 let future = make(StreamSink { slot: slot.clone() });
248 IN_FLIGHT.fetch_add(1, Ordering::SeqCst);
249 spawn(Box::pin(async move {
250 future.await;
251 if let Ok(mut snapshot) = written.lock() {
252 snapshot.state = ConnectionState::Done;
253 }
254 IN_FLIGHT.fetch_sub(1, Ordering::SeqCst);
255 arrived();
256 }));
257 slot
258 });
259 let snapshot = slot.lock().map(|s| s.clone());
260 snapshot.unwrap_or_else(|_| AsyncSnapshot::waiting())
261 }
262}
263
264/// A component built by a closure — the reference's `Builder`: a place in the tree that has a
265/// [`BuildContext`] of its own, for hooks, without declaring a widget type.
266pub struct Builder;
267
268impl Builder {
269 /// A component built by `build`. The same as [`component`], under the reference's name.
270 #[allow(clippy::new_ret_no_self)]
271 pub fn new(build: impl Fn(&BuildContext) -> Box<dyn Widget> + 'static) -> Component {
272 component(build)
273 }
274}
275
276/// Builds from a future — the reference's `FutureBuilder`, over
277/// [`BuildContext::use_future`].
278///
279/// ```
280/// use frus_widgets::{text, FutureBuilder};
281///
282/// let greeting = FutureBuilder::new(
283/// "en",
284/// |lang: &&str| {
285/// let lang = *lang;
286/// async move { if lang == "en" { "Hello" } else { "Bonjour" } }
287/// },
288/// |_, snapshot| Box::new(text(snapshot.data.unwrap_or("…"))),
289/// );
290/// # let _ = greeting;
291/// ```
292///
293/// The future starts once, and again only when `key` changes.
294pub struct FutureBuilder;
295
296impl FutureBuilder {
297 /// A component that runs `make(&key)` once per `key`, and builds from what it gives.
298 #[allow(clippy::new_ret_no_self)]
299 pub fn new<K, F>(
300 key: K,
301 make: impl Fn(&K) -> F + 'static,
302 build: impl Fn(&BuildContext, AsyncSnapshot<F::Output>) -> Box<dyn Widget> + 'static,
303 ) -> Component
304 where
305 K: Clone + PartialEq + 'static,
306 F: Future + MaybeSend + 'static,
307 F::Output: Clone + MaybeSend + 'static,
308 {
309 component(move |cx| {
310 let snapshot = cx.use_future(key.clone(), || make(&key));
311 build(cx, snapshot)
312 })
313 }
314}
315
316/// Builds from a stream of values — the reference's `StreamBuilder`, over
317/// [`BuildContext::use_stream`]: the work is given a [`StreamSink`] and adds its values to it.
318pub struct StreamBuilder;
319
320impl StreamBuilder {
321 /// A component that runs `make(&key, sink)` once per `key`, and builds from the latest
322 /// value it gave.
323 #[allow(clippy::new_ret_no_self)]
324 pub fn new<K, T, F>(
325 key: K,
326 make: impl Fn(&K, StreamSink<T>) -> F + 'static,
327 build: impl Fn(&BuildContext, AsyncSnapshot<T>) -> Box<dyn Widget> + 'static,
328 ) -> Component
329 where
330 K: Clone + PartialEq + 'static,
331 T: Clone + MaybeSend + 'static,
332 F: Future<Output = ()> + MaybeSend + 'static,
333 {
334 component(move |cx| {
335 let snapshot = cx.use_stream(key.clone(), |sink| make(&key, sink));
336 build(cx, snapshot)
337 })
338 }
339}
340
341/// Builds from a [`ValueNotifier`]'s value, and again whenever it changes — the reference's
342/// `ValueListenableBuilder`.
343///
344/// ```
345/// use frus_widgets::{text, ValueListenableBuilder, ValueNotifier};
346///
347/// let count = ValueNotifier::new(0);
348/// let label = ValueListenableBuilder::new(count.clone(), |_, n: &i32| Box::new(text(format!("{n}"))));
349/// count.set(1); // the label shows 1 at the next frame
350/// # let _ = label;
351/// ```
352pub struct ValueListenableBuilder;
353
354impl ValueListenableBuilder {
355 /// A component that builds from `notifier`'s value.
356 #[allow(clippy::new_ret_no_self)]
357 pub fn new<T: 'static>(
358 notifier: ValueNotifier<T>,
359 build: impl Fn(&BuildContext, &T) -> Box<dyn Widget> + 'static,
360 ) -> Component {
361 component(move |cx| notifier.with(|value| build(cx, value)))
362 }
363}
364
365/// Builds again whenever a [`Listenable`] says it changed — the reference's
366/// `ListenableBuilder`.
367///
368/// A notifier asks for a rebuild when it tells its listeners, so what is built here is always
369/// built from the notifier's present state: the notifier is kept so that the builder can read
370/// it, and to say in the tree what this part of it follows.
371pub struct ListenableBuilder;
372
373impl ListenableBuilder {
374 /// A component that builds from `listenable`, whenever it changes.
375 #[allow(clippy::new_ret_no_self)]
376 pub fn new<L: Listenable + 'static>(
377 listenable: L,
378 build: impl Fn(&BuildContext, &L) -> Box<dyn Widget> + 'static,
379 ) -> Component {
380 component(move |cx| build(cx, &listenable))
381 }
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387 use std::time::{Duration, Instant};
388
389 /// **With no executor registered, the work still runs** — on a thread of its own — and
390 /// its value arrives: the widget layer on its own, in a host that registered nothing.
391 #[test]
392 fn with_no_executor_the_work_runs_on_a_thread() {
393 let before = task_arrivals();
394 let slot: Slot<i32> = Arc::new(Mutex::new(AsyncSnapshot::waiting()));
395 let written = slot.clone();
396 spawn(Box::pin(async move {
397 if let Ok(mut snapshot) = written.lock() {
398 snapshot.state = ConnectionState::Done;
399 snapshot.data = Some(7);
400 }
401 arrived();
402 }));
403 let start = Instant::now();
404 while task_arrivals() == before && start.elapsed() < Duration::from_secs(2) {
405 std::thread::sleep(Duration::from_millis(2));
406 }
407 assert_eq!(slot.lock().expect("the slot").data, Some(7));
408 }
409
410 /// **A snapshot starts waiting, with nothing**, and says when it has a value.
411 #[test]
412 fn a_snapshot_starts_waiting() {
413 let waiting = AsyncSnapshot::<u8>::waiting();
414 assert_eq!(waiting.state, ConnectionState::Waiting);
415 assert!(!waiting.has_data());
416 let done = AsyncSnapshot {
417 state: ConnectionState::Done,
418 data: Some(1u8),
419 };
420 assert!(done.has_data());
421 }
422}