Skip to main content

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}