Skip to main content

frus_shell/
command.rs

1//! [`Command`]: the **effects** `Application::update` returns.
2//!
3//! A command describes work to be done **outside** the `update` cycle — a file
4//! write, a load, a network `fetch`, a long computation — whose result comes back as
5//! a **message** fed into `update`. There are three shapes:
6//!
7//! - **synchronous** ([`Command::perform`] / [`Command::run`]): a closure that
8//!   blocks. It gets a background thread natively, or a microtask on the Web.
9//! - **asynchronous** ([`Command::perform_async`] / [`Command::run_async`]): a
10//!   **future** that can `await`. Natively it goes to the shell's executor, which
11//!   has a reactor, so a future may wait on a timer or on I/O without holding a
12//!   thread. On the **Web**, which is single-threaded, the browser drives it
13//!   (`spawn_local`) and always did.
14//! - **delayed** ([`Command::after`]): a message on a real timer.
15//!
16//! The distinction that matters is the first against the second, and it is about
17//! **what the work does while it is not finished**. Something that blocks — a
18//! synchronous file read, a CPU-bound computation — must be a `perform`, because
19//! occupying a thread is exactly what a thread is for. Something that *waits* should
20//! be a `perform_async`, because waiting no longer costs a thread. Putting a blocking
21//! call inside `perform_async` is the one mistake worth naming here: it parks one of
22//! the executor's few workers and starves every other effect.
23//!
24//! Until milestone 303 the asynchronous side was a thread and a `block_on` with no
25//! reactor underneath it — `frus-shell`'s own `runtime` module records what that
26//! could and could not do.
27
28use std::future::Future;
29use std::pin::Pin;
30use std::time::Duration;
31
32use frus_widgets::{ScrollTo, SheetTo};
33
34/// A command taken apart, for the shell to run. A struct rather than a tuple: there
35/// are several kinds of effect, and a tuple at a call site says nothing about which is
36/// which.
37pub(crate) struct Parts<Msg> {
38    pub(crate) tasks: Vec<Task<Msg>>,
39    pub(crate) async_tasks: Vec<AsyncTask<Msg>>,
40    pub(crate) timers: Vec<(Duration, Msg)>,
41    pub(crate) focus: Vec<u64>,
42    pub(crate) scrolls: Vec<(u64, ScrollTo)>,
43    pub(crate) sheets: Vec<(u64, SheetTo)>,
44}
45
46/// A **synchronous** task: work that may produce a message.
47type Task<Msg> = Box<dyn FnOnce() -> Option<Msg> + Send + 'static>;
48
49/// An **asynchronous** task: a future that may produce a message.
50///
51/// **Natively** it crosses a thread (`block_on`), hence the `Send` bound. On the
52/// **Web**, which is single-threaded, the browser's futures (`JsFuture`/`fetch`) are
53/// **not** `Send` and do not need to be — hence the relaxed bound.
54#[cfg(not(web))]
55type AsyncTask<Msg> = Pin<Box<dyn Future<Output = Option<Msg>> + Send + 'static>>;
56#[cfg(web)]
57type AsyncTask<Msg> = Pin<Box<dyn Future<Output = Option<Msg>> + 'static>>;
58
59/// A batch of effects to run, possibly empty: background **tasks**, synchronous or
60/// asynchronous, and **focus** and **scroll** requests addressed by widget key.
61pub struct Command<Msg> {
62    tasks: Vec<Task<Msg>>,
63    async_tasks: Vec<AsyncTask<Msg>>,
64    /// Messages to deliver after a delay. Kept as data rather than as a future, so
65    /// that the one piece of platform knowledge — how this machine waits — stays in
66    /// the shell with all the other platform knowledge.
67    timers: Vec<(Duration, Msg)>,
68    /// The keys of widgets to focus — the key's hash, as in [`crate::Subscription`]
69    /// and the widgets' `keyed(...)`. The shell resolves them after the next build.
70    focus: Vec<u64>,
71    /// The scroll regions to move, by the same kind of key. Resolved against the frame
72    /// the request is returned into.
73    scrolls: Vec<(u64, ScrollTo)>,
74    /// The sheets to move, by the same kind of key and on the same terms.
75    sheets: Vec<(u64, SheetTo)>,
76}
77
78/// A key's hash — **identical** to the hash the widgets' `keyed(key, …)` uses, so that
79/// `Command::focus(k)` and `Command::scroll(k, …)` both target the `keyed(k, …)` widget.
80fn focus_key(key: impl std::hash::Hash) -> u64 {
81    use std::hash::Hasher;
82    let mut hasher = std::collections::hash_map::DefaultHasher::new();
83    key.hash(&mut hasher);
84    hasher.finish()
85}
86
87impl<Msg: Send + 'static> Command<Msg> {
88    /// No effect at all.
89    pub fn none() -> Self {
90        Self {
91            tasks: Vec::new(),
92            async_tasks: Vec::new(),
93            timers: Vec::new(),
94            focus: Vec::new(),
95            scrolls: Vec::new(),
96            sheets: Vec::new(),
97        }
98    }
99
100    /// Groups several commands into one.
101    pub fn batch(commands: impl IntoIterator<Item = Command<Msg>>) -> Self {
102        let mut tasks = Vec::new();
103        let mut async_tasks = Vec::new();
104        let mut timers = Vec::new();
105        let mut focus = Vec::new();
106        let mut scrolls = Vec::new();
107        let mut sheets = Vec::new();
108        for command in commands {
109            tasks.extend(command.tasks);
110            async_tasks.extend(command.async_tasks);
111            timers.extend(command.timers);
112            focus.extend(command.focus);
113            scrolls.extend(command.scrolls);
114            sheets.extend(command.sheets);
115        }
116        Self {
117            tasks,
118            async_tasks,
119            timers,
120            focus,
121            scrolls,
122            sheets,
123        }
124    }
125
126    /// Delivers `message` once, after `delay`.
127    ///
128    /// This waits without occupying anything: natively it is a task on the shell's
129    /// executor parked on the reactor's timer wheel, and on the Web a `setTimeout`.
130    /// A hundred pending timers are a hundred entries in a queue, not a hundred
131    /// threads.
132    ///
133    /// For a message that repeats, declare a [`Subscription::every`](crate::Subscription::every)
134    /// instead — a subscription is a function of the state and stops when the state
135    /// says so, where a chain of `after`s has to be stopped by hand.
136    pub fn after(delay: Duration, message: Msg) -> Self {
137        Self {
138            timers: vec![(delay, message)],
139            ..Self::none()
140        }
141    }
142
143    /// Runs a **synchronous** task in the background; its result becomes a message.
144    pub fn perform(task: impl FnOnce() -> Msg + Send + 'static) -> Self {
145        Self {
146            tasks: vec![Box::new(move || Some(task()))],
147            ..Self::none()
148        }
149    }
150
151    /// Runs a **synchronous** side effect; it may return a message (`None` for none).
152    pub fn run(task: impl FnOnce() -> Option<Msg> + Send + 'static) -> Self {
153        Self {
154            tasks: vec![Box::new(task)],
155            ..Self::none()
156        }
157    }
158
159    /// Runs an asynchronous **future**; its value becomes a message.
160    ///
161    /// On the **Web**, which is single-threaded, the browser drives the future
162    /// (`spawn_local`), so a real `fetch` can `await` without blocking. **Natively**
163    /// it runs on the shell's executor (the `runtime` module), which has a reactor: a
164    /// future may wait on a timer or on a socket and costs nothing while it does.
165    ///
166    /// Do not put a **blocking** call in here — that is what [`Command::perform`] is
167    /// for. The executor has four worker threads, and a blocking call holds one of
168    /// them for its whole duration.
169    #[cfg(not(web))]
170    pub fn perform_async<F>(future: F) -> Self
171    where
172        F: Future<Output = Msg> + Send + 'static,
173    {
174        Self {
175            async_tasks: vec![Box::pin(async move { Some(future.await) })],
176            ..Self::none()
177        }
178    }
179
180    /// Runs an asynchronous **future**; its value becomes a message. See the native
181    /// version for the full semantics; the `Send` bounds are relaxed on the Web.
182    #[cfg(web)]
183    pub fn perform_async<F>(future: F) -> Self
184    where
185        F: Future<Output = Msg> + 'static,
186    {
187        Self {
188            async_tasks: vec![Box::pin(async move { Some(future.await) })],
189            ..Self::none()
190        }
191    }
192
193    /// Runs an asynchronous **future** for its side effect; it may return a message
194    /// (`None` for none). The asynchronous counterpart of [`Command::run`].
195    #[cfg(not(web))]
196    pub fn run_async<F>(future: F) -> Self
197    where
198        F: Future<Output = Option<Msg>> + Send + 'static,
199    {
200        Self {
201            async_tasks: vec![Box::pin(future)],
202            ..Self::none()
203        }
204    }
205
206    /// Runs an asynchronous **future** for its side effect (`None` for no message).
207    /// The Web version, with the `Send` bound relaxed.
208    #[cfg(web)]
209    pub fn run_async<F>(future: F) -> Self
210    where
211        F: Future<Output = Option<Msg>> + 'static,
212    {
213        Self {
214            async_tasks: vec![Box::pin(future)],
215            ..Self::none()
216        }
217    }
218
219    /// Requests **focus** for the widget carrying `key` — the field wrapped in
220    /// `keyed(key, …)`. The shell resolves it after the view is next built; it is
221    /// typically returned when a form submission fails, to jump to the first invalid
222    /// field (`Form::first_invalid`).
223    pub fn focus(key: impl std::hash::Hash) -> Self {
224        Self {
225            focus: vec![focus_key(key)],
226            ..Self::none()
227        }
228    }
229
230    /// **Moves a scroll region**: the one named `key` goes where `to` says.
231    ///
232    /// A scroll is not a function of the state — the same list, the same offset, and yet
233    /// "go back to the top" happens once — so it is an effect and belongs here rather
234    /// than on a widget. The offset it changes *is* state, and it stays where it was: in
235    /// the runtime, beside every other offset, written by the frame this request is
236    /// resolved into.
237    ///
238    /// Named the way a focus request is named: the region is wrapped in `keyed(k, …)`
239    /// and addressed by `k`. The framework's own identities are hashes of a position in
240    /// a tree, which an application cannot know and should not have to.
241    ///
242    /// Resolved against the **frame that follows**, once and once only — the same rule
243    /// as [`Command::focus`]. A request naming a region that frame has not got is
244    /// dropped rather than kept waiting: an effect that outlived the state it was
245    /// written for would arrive at a list that had since become somebody else's.
246    ///
247    /// ```no_run
248    /// # use frus_shell::Command;
249    /// # use frus_widgets::ScrollTo;
250    /// # fn f() -> Command<()> {
251    /// Command::scroll("journal", ScrollTo::start())
252    /// # }
253    /// ```
254    pub fn scroll(key: impl std::hash::Hash, to: ScrollTo) -> Self {
255        Self {
256            scrolls: vec![(focus_key(key), to)],
257            ..Self::none()
258        }
259    }
260
261    /// **Moves a sheet**: the one named `key` goes where `to` says — the reference's
262    /// `DraggableScrollableController`, as an effect.
263    ///
264    /// For the reason a scroll is one: "raise the sheet" happens once, and the height it
265    /// leaves behind is state that stays where every other sheet height is, in the
266    /// runtime. Named the same way — the sheet, or anything around it, wrapped in
267    /// `keyed(k, …)` — and resolved on the same terms: against the frame that follows, a
268    /// sheet that has only just appeared included, and dropped if that frame has not got
269    /// it. A finger on the sheet refuses it.
270    ///
271    /// ```no_run
272    /// # use frus_shell::Command;
273    /// # use frus_widgets::{Curve, SheetTo};
274    /// # fn f() -> Command<()> {
275    /// Command::sheet("places", SheetTo::size(1.0).animate(0.3, Curve::ease()))
276    /// # }
277    /// ```
278    pub fn sheet(key: impl std::hash::Hash, to: SheetTo) -> Self {
279        Self {
280            sheets: vec![(focus_key(key), to)],
281            ..Self::none()
282        }
283    }
284
285    /// A focus request whose key is already a hash — what a component's effect carries.
286    pub(crate) fn focus_hashed(key: u64) -> Self {
287        Self {
288            focus: vec![key],
289            ..Self::none()
290        }
291    }
292
293    /// A scroll request whose key is already a hash.
294    pub(crate) fn scroll_hashed(key: u64, to: ScrollTo) -> Self {
295        Self {
296            scrolls: vec![(key, to)],
297            ..Self::none()
298        }
299    }
300
301    /// A sheet request whose key is already a hash.
302    pub(crate) fn sheet_hashed(key: u64, to: SheetTo) -> Self {
303        Self {
304            sheets: vec![(key, to)],
305            ..Self::none()
306        }
307    }
308
309    /// `true` when the command has no effect and no request of any kind.
310    pub fn is_empty(&self) -> bool {
311        self.tasks.is_empty()
312            && self.async_tasks.is_empty()
313            && self.timers.is_empty()
314            && self.focus.is_empty()
315            && self.scrolls.is_empty()
316            && self.sheets.is_empty()
317    }
318
319    /// Takes the command apart for the framework to run.
320    pub(crate) fn into_parts(self) -> Parts<Msg> {
321        Parts {
322            tasks: self.tasks,
323            async_tasks: self.async_tasks,
324            timers: self.timers,
325            focus: self.focus,
326            scrolls: self.scrolls,
327            sheets: self.sheets,
328        }
329    }
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335
336    #[test]
337    fn none_is_empty() {
338        assert!(Command::<u32>::none().is_empty());
339    }
340
341    #[test]
342    fn a_batch_keeps_its_sheet_requests() {
343        let sheet = Command::<u32>::sheet("places", SheetTo::size(1.0));
344        assert!(!sheet.is_empty());
345        let batch = Command::batch([
346            sheet,
347            Command::none(),
348            Command::sheet("other", SheetTo::initial()),
349        ]);
350        assert_eq!(batch.into_parts().sheets.len(), 2);
351    }
352
353    #[test]
354    fn perform_yields_a_message() {
355        let command = Command::perform(|| 42u32);
356        let tasks = command.into_parts().tasks;
357        assert_eq!(tasks.len(), 1);
358        let produced = (tasks.into_iter().next().unwrap())();
359        assert_eq!(produced, Some(42));
360    }
361
362    #[test]
363    fn run_may_produce_nothing() {
364        let command = Command::run(|| -> Option<u32> { None });
365        let tasks = command.into_parts().tasks;
366        assert_eq!((tasks.into_iter().next().unwrap())(), None);
367    }
368
369    #[test]
370    fn batch_flattens_and_drops_empties() {
371        let command = Command::batch([
372            Command::perform(|| 1u32),
373            Command::none(),
374            Command::run(|| Some(2u32)),
375        ]);
376        assert_eq!(command.into_parts().tasks.len(), 2);
377    }
378
379    #[test]
380    fn focus_carries_a_key_and_no_task() {
381        // A focus command carries no task, yet it is not "empty".
382        let f = Command::<u32>::focus("email");
383        assert!(!f.is_empty());
384        let parts = f.into_parts();
385        assert!(parts.tasks.is_empty());
386        assert!(parts.async_tasks.is_empty());
387        assert_eq!(parts.focus, vec![focus_key("email")]);
388    }
389
390    #[cfg(not(web))]
391    #[test]
392    fn perform_async_yields_a_message() {
393        // The future is driven to completion (`block_on`) and its value becomes a message.
394        let command = Command::perform_async(async { 7u32 });
395        let mut parts = command.into_parts();
396        assert!(parts.tasks.is_empty());
397        assert_eq!(parts.async_tasks.len(), 1);
398        let produced = pollster::block_on(parts.async_tasks.remove(0));
399        assert_eq!(produced, Some(7));
400    }
401
402    #[cfg(not(web))]
403    #[test]
404    fn run_async_may_produce_nothing() {
405        let command = Command::run_async(async { None::<u32> });
406        let mut parts = command.into_parts();
407        assert_eq!(pollster::block_on(parts.async_tasks.remove(0)), None);
408    }
409
410    #[cfg(not(web))]
411    #[test]
412    fn batch_combines_sync_and_async_tasks() {
413        let command = Command::batch([
414            Command::perform(|| 1u32),
415            Command::perform_async(async { 2u32 }),
416        ]);
417        let parts = command.into_parts();
418        assert_eq!(parts.tasks.len(), 1);
419        assert_eq!(parts.async_tasks.len(), 1);
420    }
421
422    #[test]
423    fn after_carries_its_delay_and_is_not_empty() {
424        let command = Command::after(Duration::from_millis(250), 9u32);
425        assert!(!command.is_empty(), "a pending timer is an effect");
426        let parts = command.into_parts();
427        assert!(parts.tasks.is_empty() && parts.async_tasks.is_empty());
428        assert_eq!(parts.timers, vec![(Duration::from_millis(250), 9u32)]);
429    }
430
431    #[test]
432    fn batch_carries_timers_through() {
433        let command = Command::batch([
434            Command::after(Duration::from_millis(10), 1u32),
435            Command::perform(|| 2u32),
436            Command::after(Duration::from_millis(20), 3u32),
437        ]);
438        let parts = command.into_parts();
439        assert_eq!(parts.tasks.len(), 1);
440        assert_eq!(parts.timers.len(), 2, "a batch must not swallow a timer");
441    }
442}