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, StateError, 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_application_reopen,
57    on_notification_activated, on_window_requested, 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.
186#[track_caller]
187pub fn remember<T: Clone + 'static>(init: impl FnOnce() -> T) -> T {
188    let site = std::panic::Location::caller();
189    let found = with_composer_if_idle(|composer| {
190        if composer.composing() {
191            Some(composer.remembered::<T>())
192        } else {
193            None
194        }
195    })
196    .flatten();
197    match found {
198        Some(Some(value)) => value,
199        Some(None) => {
200            let capture = state::Capture::begin(site);
201            let value = init();
202            let owned = capture.finish();
203            with_composer(|composer| composer.remember_new_owning(value.clone(), owned));
204            value
205        }
206        None => init(),
207    }
208}
209
210/// Like [`remember`], but computed again whenever `key` changes. Compose's
211/// `remember(key) { }`.
212#[track_caller]
213pub fn remember_with_key<K, T>(key: K, init: impl FnOnce() -> T) -> T
214where
215    K: PartialEq + Clone + 'static,
216    T: Clone + 'static,
217{
218    let site = std::panic::Location::caller();
219    let found = with_composer_if_idle(|composer| {
220        if composer.composing() {
221            Some(composer.remembered_if::<(K, T)>(|(stored, _)| *stored == key))
222        } else {
223            None
224        }
225    })
226    .flatten();
227    match found {
228        Some(Some((_, value))) => value,
229        Some(None) => {
230            let capture = state::Capture::begin(site);
231            let value = init();
232            let owned = capture.finish();
233            // The cursor did not move, so this replaces the stale value where it stood.
234            with_composer(|composer| composer.remember_new_owning((key, value.clone()), owned));
235            value
236        }
237        None => init(),
238    }
239}
240
241const KEY_BASE: u64 = composer::call_site("compose_rust::key", 0);
242
243/// Gives `content` an identity of its own, made from `key` rather than from its position.
244/// Compose's `key`.
245///
246/// In a loop over items that can move, each iteration's state belongs to whichever item it
247/// was composed for, and a reordered item's node is moved rather than rebuilt:
248///
249/// ```ignore
250/// for task in tasks.iter() {
251///     key(task.id, || TaskRow(task.clone()));
252/// }
253/// ```
254pub fn key<K: Hash, R>(key: K, content: impl FnOnce() -> R) -> R {
255    let mut hasher = Fnv(0xcbf2_9ce4_8422_2325);
256    key.hash(&mut hasher);
257    let _group = group_guard(composer::mix(KEY_BASE, hasher.finish()));
258    content()
259}
260
261/// FNV-1a, for keys: deterministic, so a key hashes the same in every run, and fast on
262/// the small values keys usually are.
263struct Fnv(u64);
264
265impl Hasher for Fnv {
266    fn finish(&self) -> u64 {
267        self.0
268    }
269
270    fn write(&mut self, bytes: &[u8]) {
271        for byte in bytes {
272            self.0 ^= u64::from(*byte);
273            self.0 = self.0.wrapping_mul(0x100_0000_01b3);
274        }
275    }
276}
277
278/// The scope composing now, to be told to run again by hand. Compose's
279/// `currentRecomposeScope`.
280#[derive(Clone, Copy, Debug)]
281pub struct RecomposeScope {
282    group: GroupId,
283    generation: u32,
284}
285
286impl RecomposeScope {
287    /// Runs the scope again on the next frame, whether or not anything it read changed.
288    /// Called on the UI thread; a worker writes a state instead.
289    pub fn invalidate(&self) {
290        let woke = with_composer_if_idle(|composer| {
291            composer.invalidate(self.group, self.generation);
292        });
293        if woke.is_some() && !crate::boundary::in_host_call() {
294            crate::boundary::request_frame_from_worker();
295        }
296    }
297}
298
299/// The innermost scope composing now.
300pub fn current_recompose_scope() -> Option<RecomposeScope> {
301    with_composer_if_idle(|composer| {
302        let group = *composer.scope_stack.last()?;
303        Some(RecomposeScope {
304            group,
305            generation: composer.groups[group as usize].generation,
306        })
307    })
308    .flatten()
309}
310
311/// What `#[Composable]` expands into. Not for calling by hand: the macro's guarantees are
312/// what make these safe to use.
313#[doc(hidden)]
314pub mod __private {
315    use super::composer::GroupId;
316    use super::{GroupGuard, group_guard, with_composer, with_composer_if_idle};
317    use std::rc::Rc;
318
319    pub use super::composer::call_site;
320
321    /// Opens a branch group.
322    pub fn group(key: u64) -> GroupGuard {
323        group_guard(key)
324    }
325
326    /// A restartable function's group, open until dropped.
327    #[must_use]
328    pub struct RestartGroup {
329        group: Option<GroupId>,
330    }
331
332    pub fn start_restart_group(key: u64) -> RestartGroup {
333        let group = with_composer_if_idle(|composer| {
334            composer
335                .composing()
336                .then(|| composer.start_restart_group(key))
337        })
338        .flatten();
339        RestartGroup { group }
340    }
341
342    impl RestartGroup {
343        /// Whether the body has to run regardless of the parameters.
344        pub fn must_run(&self) -> bool {
345            match self.group {
346                Some(group) => with_composer(|composer| composer.must_run(group)).unwrap_or(true),
347                None => true,
348            }
349        }
350
351        /// Carries the group over as it was, without running the body.
352        pub fn skip(&self) {
353            if self.group.is_some() {
354                with_composer(super::composer::Composer::skip_to_group_end);
355            }
356        }
357
358        /// Whether the restart has to be captured again: there is none yet, or a parameter
359        /// changed.
360        pub fn wants_restart(&self) -> bool {
361            match self.group {
362                Some(group) => {
363                    with_composer(|composer| composer.wants_restart(group)).unwrap_or(false)
364                }
365                None => false,
366            }
367        }
368
369        pub fn set_restart(&self, restart: impl Fn() + 'static) {
370            if let Some(group) = self.group {
371                let restart: Rc<dyn Fn()> = Rc::new(restart);
372                with_composer(|composer| composer.set_restart(group, restart));
373            }
374        }
375
376        /// Compares one parameter with the one the group last ran with, and keeps it.
377        pub fn param_changed<T: PartialEq + Clone + 'static>(
378            &self,
379            index: usize,
380            value: &T,
381        ) -> bool {
382            match self.group {
383                Some(group) => {
384                    with_composer(|composer| composer.param_changed(group, index, value))
385                        .unwrap_or(true)
386                }
387                None => true,
388            }
389        }
390    }
391
392    impl Drop for RestartGroup {
393        fn drop(&mut self) {
394            if self.group.is_some() {
395                with_composer_if_idle(super::composer::Composer::end_group);
396            }
397        }
398    }
399
400    /// A parameter, for the comparison that decides a skip. Which trait method resolves
401    /// for it is decided at compile time: a `PartialEq + Clone + 'static` value is
402    /// compared and kept, anything else is always treated as changed.
403    pub struct Param<'a, T>(pub &'a T);
404
405    pub trait StableParam {
406        type Value;
407        fn __compose_changed(&self, scope: &RestartGroup, index: usize) -> bool;
408        fn __compose_clone(&self) -> Option<Self::Value>;
409    }
410
411    impl<T: PartialEq + Clone + 'static> StableParam for Param<'_, T> {
412        type Value = T;
413
414        fn __compose_changed(&self, scope: &RestartGroup, index: usize) -> bool {
415            scope.param_changed(index, self.0)
416        }
417
418        fn __compose_clone(&self) -> Option<T> {
419            Some(self.0.clone())
420        }
421    }
422
423    pub trait UnstableParam {
424        type Value;
425        fn __compose_changed(&self, scope: &RestartGroup, index: usize) -> bool;
426        fn __compose_clone(&self) -> Option<Self::Value>;
427    }
428
429    impl<T> UnstableParam for &Param<'_, T> {
430        type Value = T;
431
432        fn __compose_changed(&self, _scope: &RestartGroup, _index: usize) -> bool {
433            true
434        }
435
436        fn __compose_clone(&self) -> Option<T> {
437            None
438        }
439    }
440}