Skip to main content

cranpose_core/
composer_context.rs

1use std::{
2    rc::Rc,
3    sync::atomic::{AtomicUsize, Ordering},
4};
5
6use crate::{Composer, ComposerCore, StateId, thread_state};
7
8/// How many threads have a composer installed. Code outside any
9/// composition, as a draw closure's branch guards are, reads this instead of
10/// the thread-local, which costs a call into the platform on Android.
11static INSTALLED_COMPOSERS: AtomicUsize = AtomicUsize::new(0);
12
13/// Whether any thread may have a composer: `false` means this one has none.
14#[inline]
15fn any_composer_installed() -> bool {
16    INSTALLED_COMPOSERS.load(Ordering::Relaxed) != 0
17}
18
19/// Restores the previous composer when its scope ends.
20#[must_use = "ComposerScopeGuard restores the previous composer on drop"]
21pub struct ComposerScopeGuard {
22    previous: Option<Rc<ComposerCore>>,
23}
24
25impl Drop for ComposerScopeGuard {
26    fn drop(&mut self) {
27        replace_current(self.previous.take());
28    }
29}
30
31/// Sets the current thread's composer for the duration of the scope.
32/// Returns a guard that restores the previous composer on drop.
33pub fn enter(composer: &Composer) -> ComposerScopeGuard {
34    ComposerScopeGuard {
35        previous: replace_current(Some(composer.clone_core())),
36    }
37}
38
39fn replace_current(composer: Option<Rc<ComposerCore>>) -> Option<Rc<ComposerCore>> {
40    let installs = composer.is_some();
41    let previous = thread_state::runtimes(|state| state.current_composer.replace(composer));
42    match (previous.is_some(), installs) {
43        (false, true) => {
44            INSTALLED_COMPOSERS.fetch_add(1, Ordering::Relaxed);
45        }
46        (true, false) => {
47            INSTALLED_COMPOSERS.fetch_sub(1, Ordering::Relaxed);
48        }
49        _ => {}
50    }
51    previous
52}
53
54pub(crate) fn without_composer<R>(f: impl FnOnce() -> R) -> R {
55    let _suspended = ComposerScopeGuard {
56        previous: replace_current(None),
57    };
58    f()
59}
60
61/// Access the current thread's composer.
62///
63/// # Panics
64/// Panics if there is no active composer.
65#[inline]
66pub fn with_composer<R>(f: impl FnOnce(&Composer) -> R) -> R {
67    f(&__current_composer())
68}
69
70/// The current thread's composer, for the code `#[composable]` expands to.
71///
72/// # Panics
73/// Panics if there is no active composer.
74#[doc(hidden)]
75pub fn __current_composer() -> Composer {
76    thread_state::runtimes(|state| {
77        Composer::from_core(Rc::clone(
78            state
79                .current_composer
80                .borrow()
81                .as_ref()
82                .expect("with_composer: no active composer"),
83        ))
84    })
85}
86
87/// Runs `f` with the current thread's composer core, `None` without one.
88/// The check that no thread has one is inlined where it is asked: a draw
89/// closure's guards run it many times a draw, and a function holding the
90/// lookup too saved its registers before it checked.
91#[inline]
92pub(crate) fn with_current_core<R>(f: impl FnOnce(&ComposerCore) -> R) -> Option<R> {
93    if !any_composer_installed() {
94        return None;
95    }
96    with_installed_core(f)
97}
98
99#[inline]
100fn with_installed_core<R>(f: impl FnOnce(&ComposerCore) -> R) -> Option<R> {
101    installed_core().map(|core| f(&core))
102}
103
104// The thread's composer core outside the thread state, so the caller's
105// closure runs with no borrow held, and a generic caller makes no thread
106// state access of its own: each one was a function a web page compiles at
107// its first call (624 in the demo's Liquid UI tab).
108#[inline(never)]
109fn installed_core() -> Option<Rc<ComposerCore>> {
110    thread_state::try_runtimes(|state| state.current_composer.borrow().clone()).flatten()
111}
112
113// Whether this thread is composing.
114#[inline]
115pub(crate) fn has_composer() -> bool {
116    any_composer_installed() && installed_core().is_some()
117}
118
119/// Return the current thread's composer.
120pub fn current_composer() -> Option<Composer> {
121    if !any_composer_installed() {
122        return None;
123    }
124    thread_state::try_runtimes(|state| {
125        let core = Rc::clone(state.current_composer.borrow().as_ref()?);
126        Some(Composer::from_core(core))
127    })
128    .flatten()
129}
130
131/// Registers `host`, the slot table of a subcomposition, as nested in the active slot table of the
132/// current composer, so that forgetting effects in the outer table reaches `host` too. Does nothing
133/// without a current composer.
134pub fn note_nested_slots_host(host: &Rc<crate::SlotsHost>) {
135    let Some(composer) = current_composer() else {
136        return;
137    };
138    let holder = composer.active_slots_host();
139    if Rc::ptr_eq(&holder, host) {
140        return;
141    }
142    holder.note_nested_host(host);
143}
144
145/// Try to access the current thread's composer.
146/// Returns None if there is no active composer.
147pub fn try_with_composer<R>(f: impl FnOnce(&Composer) -> R) -> Option<R> {
148    current_composer().map(|composer| f(&composer))
149}
150
151// forwards on purpose: Compose's name for with_current_composer
152/// Runs `f` with the composer that is current on this thread. Panics if there is none. Same as
153/// `with_current_composer`.
154#[expect(non_snake_case, reason = "names follow Compose's casing")]
155pub fn withCurrentComposer<R>(f: impl FnOnce(&Composer) -> R) -> R {
156    with_composer(f)
157}
158
159pub(crate) fn with_current_composer_opt<R>(f: impl FnOnce(&Composer) -> R) -> Option<R> {
160    try_with_composer(f)
161}
162
163/// Runs the body of the scope that composes now once more after it ends, in
164/// this composition: for a body that changed, as it composed, what an earlier
165/// part of it composed. Does nothing outside a scope.
166#[doc(hidden)]
167pub fn recompose_current_scope_again() {
168    if let Some(scope) = with_current_composer_opt(Composer::current_recompose_scope).flatten() {
169        scope.request_pending_recompose();
170    }
171}
172
173#[doc(hidden)]
174pub fn current_recompose_scope_invalidated_only_by(
175    allowed_sources: impl IntoIterator<Item = StateId>,
176) -> Option<bool> {
177    with_current_composer_opt(|composer| {
178        // The bodies that run now run because the restarted scope called
179        // them, with the arguments its body worked out: those come from the
180        // states that invalidated it.
181        composer
182            .restarted_scope()?
183            .invalidated_only_by(&allowed_sources.into_iter().collect())
184    })
185    .flatten()
186}