Skip to main content

compose_rust/runtime/
mod.rs

1//! compose-rust's own runtime: positional memoisation on a slot table, with Compose's
2//! shape and Compose's names.
3//!
4//! ```ignore
5//! use compose_rust::runtime::*;
6//! use compose_rust::ui::*;
7//!
8//! #[composable]
9//! fn Counter() {
10//!     let count = remember(|| mutable_state_of(0));
11//!     Column().content(|| {
12//!         Text(format!("Count: {}", count.get()));
13//!         let more = count.clone();
14//!         Button("Increment").on_click(move || more.update(|count| *count += 1));
15//!     });
16//! }
17//!
18//! fn main() {
19//!     compose_rust::ui::application(Counter);
20//! }
21//! ```
22//!
23//! **Nothing is compared to find out what changed.** A state remembers which scopes read
24//! it; writing it marks those scopes invalid, and the next frame runs exactly them again.
25//! Inside a scope that runs, a composable whose parameters equal the ones it last had is
26//! skipped, its slots and nodes carried over untouched. What a widget writes is compared
27//! with what it wrote last time, one attribute at a time, and only an attribute that
28//! changed becomes a record.
29//!
30//! **Positional memoisation.** [`remember`] belongs to its call site: the group path the
31//! `#[composable]` macro builds around every function and every branch, and the order of
32//! calls inside each group. A branch that stops being taken takes what it remembered with
33//! it; a loop that gets shorter drops the iterations it lost. Where order is not identity,
34//! because items move, [`key`] says which item is which.
35//!
36//! **One thread.** Composition, effects and event handlers run on the Renderer's UI
37//! thread, inside a Host call. Other threads write [`MutableState`]s and wake futures; both
38//! queue an id and ask for a frame, and the UI thread does the rest.
39
40pub(crate) mod composer;
41pub(crate) mod effects;
42pub(crate) mod host;
43mod seam;
44pub(crate) mod state;
45
46pub use compose_rust_macros::composable;
47pub use effects::{
48    CoroutineScope, DisposableEffectResult, delay, disposable_effect, launched_effect, on_dispose,
49    remember_coroutine_scope, side_effect, with_frame_nanos, with_worker,
50};
51pub use host::{ComposeHost, Recomposer, application, launch, recomposer_for};
52pub use seam::{Batch, Runtime};
53pub use state::{
54    DerivedState, MutableState, SnapshotMutationPolicy, State, current_design_system,
55    current_notification_permission, current_window_size, derived_state_of, mutable_state_of,
56    mutable_state_of_with_policy, never_equal_policy, on_notification_activated,
57    remember_node_size, structural_equality_policy,
58};
59
60use composer::{Composer, GroupId};
61use std::cell::RefCell;
62use std::hash::{Hash, Hasher};
63use std::rc::Rc;
64
65/// One composition and everything it owns. Compose's `Composition`.
66pub(crate) struct Composition {
67    pub(crate) composer: RefCell<Composer>,
68}
69
70thread_local! {
71    /// The runtime whose Host call is running on this thread.
72    static CURRENT: RefCell<Option<Rc<Composition>>> = const { RefCell::new(None) };
73}
74
75/// Makes a runtime current for the length of a Host call, and puts back whatever was
76/// current before when dropped.
77pub(crate) struct Enter {
78    previous: Option<Rc<Composition>>,
79}
80
81impl Composition {
82    pub(crate) fn new() -> Rc<Self> {
83        Rc::new(Self {
84            composer: RefCell::new(Composer::new()),
85        })
86    }
87
88    pub(crate) fn enter(self: &Rc<Self>) -> Enter {
89        let previous = CURRENT.with(|current| current.borrow_mut().replace(Rc::clone(self)));
90        Enter { previous }
91    }
92}
93
94impl Drop for Enter {
95    fn drop(&mut self) {
96        let previous = self.previous.take();
97        let _ = CURRENT.try_with(|current| *current.borrow_mut() = previous);
98    }
99}
100
101/// Runs `f` on the current composer.
102///
103/// Panics if the composer is already in use further up this thread's stack. Nothing in
104/// the runtime calls application code while it holds the composer, so reaching that state
105/// means a destructor or a callback was run at the wrong moment, and saying so is better
106/// than corrupting the slot table.
107pub(crate) fn with_composer<R>(f: impl FnOnce(&mut Composer) -> R) -> Option<R> {
108    CURRENT.with(|current| {
109        let current = current.borrow();
110        let runtime = current.as_ref()?;
111        let mut composer = runtime
112            .composer
113            .try_borrow_mut()
114            .expect("the composer was reached again while it was already in use");
115        Some(f(&mut composer))
116    })
117}
118
119/// Like [`with_composer`], but answers `None` rather than panicking when there is no
120/// runtime or the composer is busy. For reads that are allowed anywhere, such as a state
121/// read in a destructor.
122pub(crate) fn with_composer_if_idle<R>(f: impl FnOnce(&mut Composer) -> R) -> Option<R> {
123    CURRENT
124        .try_with(|current| {
125            let current = current.try_borrow().ok()?;
126            let runtime = current.as_ref()?;
127            let mut composer = runtime.composer.try_borrow_mut().ok()?;
128            Some(f(&mut composer))
129        })
130        .ok()
131        .flatten()
132}
133
134/// Drops what left the composition, now that the composer is free for any destructor
135/// that reaches back into the runtime.
136pub(crate) fn drain_graveyard() {
137    loop {
138        let dead =
139            with_composer(|composer| std::mem::take(&mut composer.graveyard)).unwrap_or_default();
140        if dead.is_empty() {
141            return;
142        }
143        drop(dead);
144    }
145}
146
147/// Holds a group open for the rest of a block. The group closes on every way out of the
148/// block, `return`, `?`, `break` and an unwind included, because it closes in `Drop`.
149#[must_use = "the group closes when this is dropped, so it has to be bound for the length of the block"]
150pub struct GroupGuard {
151    active: bool,
152}
153
154impl Drop for GroupGuard {
155    fn drop(&mut self) {
156        // Closed on an unwind too: a panic caught further up the composition leaves the
157        // walk where it would have been had the block returned. The composer is free by
158        // then, because the borrow that was held when the panic started was released on
159        // the way out, and if it is not, nothing is done rather than panicking twice.
160        if self.active {
161            with_composer_if_idle(Composer::end_group);
162        }
163    }
164}
165
166/// Opens a group with this key, if a composition is running.
167pub(crate) fn group_guard(key: u64) -> GroupGuard {
168    let active = with_composer_if_idle(|composer| {
169        if composer.composing() {
170            composer.start_group(key);
171            true
172        } else {
173            false
174        }
175    })
176    .unwrap_or(false);
177    GroupGuard { active }
178}
179
180/// The value this call site remembered, or `init()`'s, kept for next time. Compose's
181/// `remember`.
182///
183/// The value is returned by clone, so what is remembered is usually a handle: a
184/// [`MutableState`], an `Rc`, a derived state. Outside a composition nothing is kept and
185/// `init` runs every time.
186pub fn remember<T: Clone + 'static>(init: impl FnOnce() -> T) -> T {
187    let found = with_composer_if_idle(|composer| {
188        if composer.composing() {
189            Some(composer.remembered::<T>())
190        } else {
191            None
192        }
193    })
194    .flatten();
195    match found {
196        Some(Some(value)) => value,
197        Some(None) => {
198            let value = init();
199            with_composer(|composer| composer.remember_new(value.clone()));
200            value
201        }
202        None => init(),
203    }
204}
205
206/// Like [`remember`], but computed again whenever `key` changes. Compose's
207/// `remember(key) { }`.
208pub fn remember_with_key<K, T>(key: K, init: impl FnOnce() -> T) -> T
209where
210    K: PartialEq + Clone + 'static,
211    T: Clone + 'static,
212{
213    let found = with_composer_if_idle(|composer| {
214        if composer.composing() {
215            Some(composer.remembered_if::<(K, T)>(|(stored, _)| *stored == key))
216        } else {
217            None
218        }
219    })
220    .flatten();
221    match found {
222        Some(Some((_, value))) => value,
223        Some(None) => {
224            let value = init();
225            // The cursor did not move, so this replaces the stale value where it stood.
226            with_composer(|composer| composer.remember_new((key, value.clone())));
227            value
228        }
229        None => init(),
230    }
231}
232
233const KEY_BASE: u64 = composer::call_site("compose_rust::key", 0);
234
235/// Gives `content` an identity of its own, made from `key` rather than from its position.
236/// Compose's `key`.
237///
238/// In a loop over items that can move, each iteration's state belongs to whichever item it
239/// was composed for, and a reordered item's node is moved rather than rebuilt:
240///
241/// ```ignore
242/// for task in tasks.iter() {
243///     key(task.id, || TaskRow(task.clone()));
244/// }
245/// ```
246pub fn key<K: Hash, R>(key: K, content: impl FnOnce() -> R) -> R {
247    let mut hasher = Fnv(0xcbf2_9ce4_8422_2325);
248    key.hash(&mut hasher);
249    let _group = group_guard(composer::mix(KEY_BASE, hasher.finish()));
250    content()
251}
252
253/// FNV-1a, for keys: deterministic, so a key hashes the same in every run, and fast on
254/// the small values keys usually are.
255struct Fnv(u64);
256
257impl Hasher for Fnv {
258    fn finish(&self) -> u64 {
259        self.0
260    }
261
262    fn write(&mut self, bytes: &[u8]) {
263        for byte in bytes {
264            self.0 ^= u64::from(*byte);
265            self.0 = self.0.wrapping_mul(0x100_0000_01b3);
266        }
267    }
268}
269
270/// The scope composing now, to be told to run again by hand. Compose's
271/// `currentRecomposeScope`.
272#[derive(Clone, Copy, Debug)]
273pub struct RecomposeScope {
274    group: GroupId,
275    generation: u32,
276}
277
278impl RecomposeScope {
279    /// Runs the scope again on the next frame, whether or not anything it read changed.
280    /// Called on the UI thread; a worker writes a state instead.
281    pub fn invalidate(&self) {
282        let woke = with_composer_if_idle(|composer| {
283            composer.invalidate(self.group, self.generation);
284        });
285        if woke.is_some() && !crate::boundary::in_host_call() {
286            crate::boundary::request_frame_from_worker();
287        }
288    }
289}
290
291/// The innermost scope composing now.
292pub fn current_recompose_scope() -> Option<RecomposeScope> {
293    with_composer_if_idle(|composer| {
294        let group = *composer.scope_stack.last()?;
295        Some(RecomposeScope {
296            group,
297            generation: composer.groups[group as usize].generation,
298        })
299    })
300    .flatten()
301}
302
303/// What `#[composable]` expands into. Not for calling by hand: the macro's guarantees are
304/// what make these safe to use.
305#[doc(hidden)]
306pub mod __private {
307    use super::composer::GroupId;
308    use super::{GroupGuard, group_guard, with_composer, with_composer_if_idle};
309    use std::rc::Rc;
310
311    pub use super::composer::call_site;
312
313    /// Opens a branch group.
314    pub fn group(key: u64) -> GroupGuard {
315        group_guard(key)
316    }
317
318    /// A restartable function's group, open until dropped.
319    #[must_use]
320    pub struct RestartGroup {
321        group: Option<GroupId>,
322    }
323
324    pub fn start_restart_group(key: u64) -> RestartGroup {
325        let group = with_composer_if_idle(|composer| {
326            composer
327                .composing()
328                .then(|| composer.start_restart_group(key))
329        })
330        .flatten();
331        RestartGroup { group }
332    }
333
334    impl RestartGroup {
335        /// Whether the body has to run regardless of the parameters.
336        pub fn must_run(&self) -> bool {
337            match self.group {
338                Some(group) => with_composer(|composer| composer.must_run(group)).unwrap_or(true),
339                None => true,
340            }
341        }
342
343        /// Carries the group over as it was, without running the body.
344        pub fn skip(&self) {
345            if self.group.is_some() {
346                with_composer(super::composer::Composer::skip_to_group_end);
347            }
348        }
349
350        /// Whether the restart has to be captured again: there is none yet, or a parameter
351        /// changed.
352        pub fn wants_restart(&self) -> bool {
353            match self.group {
354                Some(group) => {
355                    with_composer(|composer| composer.wants_restart(group)).unwrap_or(false)
356                }
357                None => false,
358            }
359        }
360
361        pub fn set_restart(&self, restart: impl Fn() + 'static) {
362            if let Some(group) = self.group {
363                let restart: Rc<dyn Fn()> = Rc::new(restart);
364                with_composer(|composer| composer.set_restart(group, restart));
365            }
366        }
367
368        /// Compares one parameter with the one the group last ran with, and keeps it.
369        pub fn param_changed<T: PartialEq + Clone + 'static>(
370            &self,
371            index: usize,
372            value: &T,
373        ) -> bool {
374            match self.group {
375                Some(group) => {
376                    with_composer(|composer| composer.param_changed(group, index, value))
377                        .unwrap_or(true)
378                }
379                None => true,
380            }
381        }
382    }
383
384    impl Drop for RestartGroup {
385        fn drop(&mut self) {
386            if self.group.is_some() {
387                with_composer_if_idle(super::composer::Composer::end_group);
388            }
389        }
390    }
391
392    /// A parameter, for the comparison that decides a skip. Which trait method resolves
393    /// for it is decided at compile time: a `PartialEq + Clone + 'static` value is
394    /// compared and kept, anything else is always treated as changed.
395    pub struct Param<'a, T>(pub &'a T);
396
397    pub trait StableParam {
398        type Value;
399        fn __compose_changed(&self, scope: &RestartGroup, index: usize) -> bool;
400        fn __compose_clone(&self) -> Option<Self::Value>;
401    }
402
403    impl<T: PartialEq + Clone + 'static> StableParam for Param<'_, T> {
404        type Value = T;
405
406        fn __compose_changed(&self, scope: &RestartGroup, index: usize) -> bool {
407            scope.param_changed(index, self.0)
408        }
409
410        fn __compose_clone(&self) -> Option<T> {
411            Some(self.0.clone())
412        }
413    }
414
415    pub trait UnstableParam {
416        type Value;
417        fn __compose_changed(&self, scope: &RestartGroup, index: usize) -> bool;
418        fn __compose_clone(&self) -> Option<Self::Value>;
419    }
420
421    impl<T> UnstableParam for &Param<'_, T> {
422        type Value = T;
423
424        fn __compose_changed(&self, _scope: &RestartGroup, _index: usize) -> bool {
425            true
426        }
427
428        fn __compose_clone(&self) -> Option<T> {
429            None
430        }
431    }
432}