Skip to main content

cranpose_core/
composition_locals.rs

1use std::{any::Any, cell::RefCell, rc::Rc, sync::Arc};
2
3use crate::{
4    Composer, LocalKey, RuntimeHandle, composer_context,
5    state::{MutationPolicy, OwnedMutableState},
6};
7
8fn provider_entry_source(key: &LocalKey, caller: crate::Key) -> crate::Key {
9    (key.entry_source() ^ caller).wrapping_mul(0x0000_0100_0000_01b3)
10}
11
12/// A composition local paired with the value to provide, made by `provides` and passed to
13/// `CompositionLocalProvider`.
14pub struct ProvidedValue {
15    key: LocalKey,
16    #[expect(clippy::type_complexity)]
17    apply: Box<dyn Fn(&Composer, crate::Key) -> (Rc<dyn Any>, bool)>,
18}
19
20impl ProvidedValue {
21    pub(crate) fn key(&self) -> &LocalKey {
22        &self.key
23    }
24
25    /// The provided entry, and whether it replaced a different value of a
26    /// static local that this site provided before.
27    pub(crate) fn into_entry(
28        self,
29        composer: &Composer,
30        site: crate::Key,
31    ) -> (LocalKey, Rc<dyn Any>, bool) {
32        let ProvidedValue { key, apply } = self;
33        let (entry, static_changed) = apply(composer, site);
34        (key, entry, static_changed)
35    }
36}
37
38/// Makes `values` available to `content` and everything it composes. Where two values are for the
39/// same local, the last wins. Compose's `CompositionLocalProvider`.
40#[expect(non_snake_case)]
41#[track_caller]
42pub fn CompositionLocalProvider(
43    values: impl IntoIterator<Item = ProvidedValue>,
44    content: impl FnOnce(),
45) {
46    let site = crate::caller_location_key();
47    composer_context::with_composer(|composer| {
48        composer.with_composition_locals(values, site, |_composer| content());
49    });
50}
51
52pub(crate) struct LocalStateEntry<T: Clone + 'static> {
53    state: OwnedMutableState<T>,
54}
55
56type LocalEquivalentFn<T> = dyn Fn(&T, &T) -> bool + Send + Sync + 'static;
57
58struct LocalValuePolicy<T: Clone + 'static> {
59    equivalent: Arc<LocalEquivalentFn<T>>,
60}
61
62impl<T: Clone + 'static> MutationPolicy<T> for LocalValuePolicy<T> {
63    fn equivalent(&self, a: &T, b: &T) -> bool {
64        (self.equivalent)(a, b)
65    }
66}
67
68impl<T: Clone + 'static> LocalStateEntry<T> {
69    fn new(initial: T, runtime: RuntimeHandle, equivalent: Arc<LocalEquivalentFn<T>>) -> Self {
70        Self {
71            state: OwnedMutableState::with_runtime_and_policy(
72                initial,
73                runtime,
74                Rc::new(LocalValuePolicy { equivalent }),
75            ),
76        }
77    }
78
79    fn set(&self, value: T) {
80        self.state.replace(value);
81    }
82
83    pub(crate) fn value(&self) -> T {
84        self.state.value()
85    }
86}
87
88pub(crate) struct StaticLocalEntry<T: Clone + 'static> {
89    value: RefCell<T>,
90}
91
92impl<T: Clone + 'static> StaticLocalEntry<T> {
93    fn new(value: T) -> Self {
94        Self {
95            value: RefCell::new(value),
96        }
97    }
98
99    /// Stores `value` and says whether it differs from the stored one.
100    fn set(&self, value: T, equivalent: &LocalEquivalentFn<T>) -> bool {
101        let mut current = self.value.borrow_mut();
102        if equivalent(&current, &value) {
103            return false;
104        }
105        *current = value;
106        true
107    }
108
109    pub(crate) fn value(&self) -> T {
110        self.value.borrow().clone()
111    }
112}
113
114/// A value that composables read from their surroundings without passing it as a parameter, as
115/// Jetpack Compose's `compositionLocalOf`. A scope that reads `current` recomposes when the
116/// provided value changes.
117#[derive(Clone)]
118pub struct CompositionLocal<T: Clone + 'static> {
119    pub(crate) key: LocalKey,
120    default: Rc<dyn Fn() -> T>,
121    equivalent: Arc<LocalEquivalentFn<T>>,
122}
123
124impl<T: Clone + 'static> PartialEq for CompositionLocal<T> {
125    fn eq(&self, other: &Self) -> bool {
126        self.key == other.key
127    }
128}
129
130impl<T: Clone + 'static> Eq for CompositionLocal<T> {}
131
132impl<T: Clone + 'static> CompositionLocal<T> {
133    /// Pairs this local with `value` for use in `CompositionLocalProvider`. When a later
134    /// composition provides a different value, the scopes that read the local recompose.
135    #[track_caller]
136    pub fn provides(&self, value: T) -> ProvidedValue {
137        let key = self.key.clone();
138        let entry_source = provider_entry_source(&key, crate::caller_location_key());
139        let equivalent = Arc::clone(&self.equivalent);
140        ProvidedValue {
141            key,
142            apply: Box::new(move |composer: &Composer, site: crate::Key| {
143                let runtime = composer.runtime_handle();
144                let source = (entry_source ^ site).wrapping_mul(0x0000_0100_0000_01b3);
145                let entry_ref = composer.remember_internal(source, || {
146                    Rc::new(LocalStateEntry::new(
147                        value.clone(),
148                        runtime.clone(),
149                        Arc::clone(&equivalent),
150                    ))
151                });
152                entry_ref.update(|entry| entry.set(value.clone()));
153                entry_ref.with(|entry| (entry.clone() as Rc<dyn Any>, false))
154            }),
155        }
156    }
157
158    /// Returns the value from the innermost enclosing provider, or the default value when none is
159    /// in scope. Must be called during composition. The reading scope recomposes when the provided
160    /// value changes.
161    pub fn current(&self) -> T {
162        composer_context::with_composer(|composer| composer.read_composition_local(self))
163    }
164
165    /// Returns the value that `current` gives where no provider is in scope.
166    pub fn default_value(&self) -> T {
167        (self.default)()
168    }
169}
170
171#[cfg(test)]
172fn malformed_provided_value_for_test(key: LocalKey, entry: Rc<dyn Any>) -> ProvidedValue {
173    ProvidedValue {
174        key,
175        apply: Box::new(move |_, _| (entry.clone(), false)),
176    }
177}
178
179#[cfg(test)]
180pub(crate) fn malformed_composition_local_for_test<T: Clone + 'static>(
181    local: &CompositionLocal<T>,
182    entry: Rc<dyn Any>,
183) -> ProvidedValue {
184    malformed_provided_value_for_test(local.key.clone(), entry)
185}
186
187/// Creates a [`CompositionLocal`] whose default is made by `default`. A provided value replaces the
188/// previous one when it differs by `==`. Compose's `compositionLocalOf`.
189#[expect(non_snake_case)]
190pub fn compositionLocalOf<T: Clone + PartialEq + 'static>(
191    default: impl Fn() -> T + 'static,
192) -> CompositionLocal<T> {
193    compositionLocalOfWithPolicy(default, |current, next| current == next)
194}
195
196/// Creates a [`CompositionLocal`] whose default is made by `default`. A provided value replaces the
197/// previous one only when `equivalent` returns `false` for the two.
198#[expect(non_snake_case)]
199pub fn compositionLocalOfWithPolicy<T: Clone + 'static>(
200    default: impl Fn() -> T + 'static,
201    equivalent: impl Fn(&T, &T) -> bool + Send + Sync + 'static,
202) -> CompositionLocal<T> {
203    CompositionLocal {
204        key: LocalKey::new(),
205        default: Rc::new(default),
206        equivalent: Arc::new(equivalent),
207    }
208}
209
210/// A composition local for values that rarely change, as Jetpack Compose's
211/// `staticCompositionLocalOf`. A read of `.current()` subscribes nothing, so
212/// it costs less than a read of a [`CompositionLocal`]. In return, a
213/// provider whose value changes recomposes everything it provides to, not
214/// only the readers: every composable call under it runs its body, and
215/// every subcomposition under it composes again the same way.
216#[derive(Clone)]
217pub struct StaticCompositionLocal<T: Clone + 'static> {
218    pub(crate) key: LocalKey,
219    default: Rc<dyn Fn() -> T>,
220    equivalent: Arc<LocalEquivalentFn<T>>,
221}
222
223impl<T: Clone + 'static> PartialEq for StaticCompositionLocal<T> {
224    fn eq(&self, other: &Self) -> bool {
225        self.key == other.key
226    }
227}
228
229impl<T: Clone + 'static> Eq for StaticCompositionLocal<T> {}
230
231impl<T: Clone + 'static> StaticCompositionLocal<T> {
232    /// Pairs this local with `value` for use in `CompositionLocalProvider`. When a later
233    /// composition provides a different value, everything under the provider recomposes.
234    #[track_caller]
235    pub fn provides(&self, value: T) -> ProvidedValue {
236        let key = self.key.clone();
237        let entry_source = provider_entry_source(&key, crate::caller_location_key());
238        let equivalent = Arc::clone(&self.equivalent);
239        ProvidedValue {
240            key,
241            apply: Box::new(move |composer: &Composer, site: crate::Key| {
242                let source = (entry_source ^ site).wrapping_mul(0x0000_0100_0000_01b3);
243                let entry_ref = composer
244                    .remember_internal(source, || Rc::new(StaticLocalEntry::new(value.clone())));
245                let changed = entry_ref.update(|entry| entry.set(value.clone(), &*equivalent));
246                entry_ref.with(|entry| (entry.clone() as Rc<dyn Any>, changed))
247            }),
248        }
249    }
250
251    /// Returns the value from the innermost enclosing provider, or the default value when none is
252    /// in scope. Must be called during composition. A read subscribes nothing.
253    pub fn current(&self) -> T {
254        composer_context::with_composer(|composer| composer.read_static_composition_local(self))
255    }
256
257    /// Returns the value that `current` gives where no provider is in scope.
258    pub fn default_value(&self) -> T {
259        (self.default)()
260    }
261}
262
263#[cfg(test)]
264pub(crate) fn malformed_static_composition_local_for_test<T: Clone + 'static>(
265    local: &StaticCompositionLocal<T>,
266    entry: Rc<dyn Any>,
267) -> ProvidedValue {
268    malformed_provided_value_for_test(local.key.clone(), entry)
269}
270
271/// A [`StaticCompositionLocal`] whose provided values compare with `==`.
272#[expect(non_snake_case)]
273pub fn staticCompositionLocalOf<T: Clone + PartialEq + 'static>(
274    default: impl Fn() -> T + 'static,
275) -> StaticCompositionLocal<T> {
276    staticCompositionLocalOfWithPolicy(default, |current, next| current == next)
277}
278
279/// A [`StaticCompositionLocal`] whose provider recomposes its content when
280/// `equivalent` says a new value differs from the last one.
281#[expect(non_snake_case)]
282pub fn staticCompositionLocalOfWithPolicy<T: Clone + 'static>(
283    default: impl Fn() -> T + 'static,
284    equivalent: impl Fn(&T, &T) -> bool + Send + Sync + 'static,
285) -> StaticCompositionLocal<T> {
286    StaticCompositionLocal {
287        key: LocalKey::new(),
288        default: Rc::new(default),
289        equivalent: Arc::new(equivalent),
290    }
291}