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
12pub struct ProvidedValue {
13    key: LocalKey,
14    #[expect(clippy::type_complexity)]
15    apply: Box<dyn Fn(&Composer, crate::Key) -> Rc<dyn Any>>,
16}
17
18impl ProvidedValue {
19    pub(crate) fn key(&self) -> &LocalKey {
20        &self.key
21    }
22
23    pub(crate) fn into_entry(
24        self,
25        composer: &Composer,
26        site: crate::Key,
27    ) -> (LocalKey, Rc<dyn Any>) {
28        let ProvidedValue { key, apply } = self;
29        let entry = apply(composer, site);
30        (key, entry)
31    }
32}
33
34#[expect(non_snake_case)]
35#[track_caller]
36pub fn CompositionLocalProvider(
37    values: impl IntoIterator<Item = ProvidedValue>,
38    content: impl FnOnce(),
39) {
40    let site = crate::caller_location_key();
41    composer_context::with_composer(|composer| {
42        composer.with_composition_locals(values, site, |_composer| content());
43    });
44}
45
46pub(crate) struct LocalStateEntry<T: Clone + 'static> {
47    state: OwnedMutableState<T>,
48}
49
50type LocalEquivalentFn<T> = dyn Fn(&T, &T) -> bool + Send + Sync + 'static;
51
52struct LocalValuePolicy<T: Clone + 'static> {
53    equivalent: Arc<LocalEquivalentFn<T>>,
54}
55
56impl<T: Clone + 'static> MutationPolicy<T> for LocalValuePolicy<T> {
57    fn equivalent(&self, a: &T, b: &T) -> bool {
58        (self.equivalent)(a, b)
59    }
60}
61
62impl<T: Clone + 'static> LocalStateEntry<T> {
63    fn new(initial: T, runtime: RuntimeHandle, equivalent: Arc<LocalEquivalentFn<T>>) -> Self {
64        Self {
65            state: OwnedMutableState::with_runtime_and_policy(
66                initial,
67                runtime,
68                Arc::new(LocalValuePolicy { equivalent }),
69            ),
70        }
71    }
72
73    fn set(&self, value: T) {
74        self.state.replace(value);
75    }
76
77    pub(crate) fn value(&self) -> T {
78        self.state.value()
79    }
80}
81
82pub(crate) struct StaticLocalEntry<T: Clone + 'static> {
83    value: RefCell<T>,
84}
85
86impl<T: Clone + 'static> StaticLocalEntry<T> {
87    fn new(value: T) -> Self {
88        Self {
89            value: RefCell::new(value),
90        }
91    }
92
93    fn set(&self, value: T) {
94        *self.value.borrow_mut() = value;
95    }
96
97    pub(crate) fn value(&self) -> T {
98        self.value.borrow().clone()
99    }
100}
101
102#[derive(Clone)]
103pub struct CompositionLocal<T: Clone + 'static> {
104    pub(crate) key: LocalKey,
105    default: Rc<dyn Fn() -> T>,
106    equivalent: Arc<LocalEquivalentFn<T>>,
107}
108
109impl<T: Clone + 'static> PartialEq for CompositionLocal<T> {
110    fn eq(&self, other: &Self) -> bool {
111        self.key == other.key
112    }
113}
114
115impl<T: Clone + 'static> Eq for CompositionLocal<T> {}
116
117impl<T: Clone + 'static> CompositionLocal<T> {
118    #[track_caller]
119    pub fn provides(&self, value: T) -> ProvidedValue {
120        let key = self.key.clone();
121        let entry_source = provider_entry_source(&key, crate::caller_location_key());
122        let equivalent = Arc::clone(&self.equivalent);
123        ProvidedValue {
124            key,
125            apply: Box::new(move |composer: &Composer, site: crate::Key| {
126                let runtime = composer.runtime_handle();
127                let source = (entry_source ^ site).wrapping_mul(0x0000_0100_0000_01b3);
128                let entry_ref = composer.remember_internal(source, || {
129                    Rc::new(LocalStateEntry::new(
130                        value.clone(),
131                        runtime.clone(),
132                        Arc::clone(&equivalent),
133                    ))
134                });
135                entry_ref.update(|entry| entry.set(value.clone()));
136                entry_ref.with(|entry| entry.clone() as Rc<dyn Any>)
137            }),
138        }
139    }
140
141    pub fn current(&self) -> T {
142        composer_context::with_composer(|composer| composer.read_composition_local(self))
143    }
144
145    pub fn default_value(&self) -> T {
146        (self.default)()
147    }
148}
149
150#[cfg(test)]
151fn malformed_provided_value_for_test(key: LocalKey, entry: Rc<dyn Any>) -> ProvidedValue {
152    ProvidedValue {
153        key,
154        apply: Box::new(move |_, _| entry.clone()),
155    }
156}
157
158#[cfg(test)]
159pub(crate) fn malformed_composition_local_for_test<T: Clone + 'static>(
160    local: &CompositionLocal<T>,
161    entry: Rc<dyn Any>,
162) -> ProvidedValue {
163    malformed_provided_value_for_test(local.key.clone(), entry)
164}
165
166#[expect(non_snake_case)]
167pub fn compositionLocalOf<T: Clone + PartialEq + 'static>(
168    default: impl Fn() -> T + 'static,
169) -> CompositionLocal<T> {
170    compositionLocalOfWithPolicy(default, |current, next| current == next)
171}
172
173#[expect(non_snake_case)]
174pub fn compositionLocalOfWithPolicy<T: Clone + 'static>(
175    default: impl Fn() -> T + 'static,
176    equivalent: impl Fn(&T, &T) -> bool + Send + Sync + 'static,
177) -> CompositionLocal<T> {
178    CompositionLocal {
179        key: LocalKey::new(),
180        default: Rc::new(default),
181        equivalent: Arc::new(equivalent),
182    }
183}
184
185/// A `StaticCompositionLocal` is a CompositionLocal that is optimized for values that are
186/// unlikely to change. Unlike `CompositionLocal`, reads of a `StaticCompositionLocal` are not
187/// tracked by the recomposition system, which means:
188/// - Reading `.current()` does NOT establish a subscription
189/// - Changing the provided value does NOT automatically invalidate readers
190/// - This makes it more efficient for truly static values
191///
192/// This matches the API of Jetpack Compose's `staticCompositionLocalOf` but with simplified
193/// semantics. Use this for values that are guaranteed to never change during the lifetime of
194/// the CompositionLocalProvider scope (e.g., application-wide constants, configuration)
195#[derive(Clone)]
196pub struct StaticCompositionLocal<T: Clone + 'static> {
197    pub(crate) key: LocalKey,
198    default: Rc<dyn Fn() -> T>,
199}
200
201impl<T: Clone + 'static> PartialEq for StaticCompositionLocal<T> {
202    fn eq(&self, other: &Self) -> bool {
203        self.key == other.key
204    }
205}
206
207impl<T: Clone + 'static> Eq for StaticCompositionLocal<T> {}
208
209impl<T: Clone + 'static> StaticCompositionLocal<T> {
210    #[track_caller]
211    pub fn provides(&self, value: T) -> ProvidedValue {
212        let key = self.key.clone();
213        let entry_source = provider_entry_source(&key, crate::caller_location_key());
214        ProvidedValue {
215            key,
216            apply: Box::new(move |composer: &Composer, site: crate::Key| {
217                let source = (entry_source ^ site).wrapping_mul(0x0000_0100_0000_01b3);
218                let entry_ref = composer
219                    .remember_internal(source, || Rc::new(StaticLocalEntry::new(value.clone())));
220                entry_ref.update(|entry| entry.set(value.clone()));
221                entry_ref.with(|entry| entry.clone() as Rc<dyn Any>)
222            }),
223        }
224    }
225
226    pub fn current(&self) -> T {
227        composer_context::with_composer(|composer| composer.read_static_composition_local(self))
228    }
229
230    pub fn default_value(&self) -> T {
231        (self.default)()
232    }
233}
234
235#[cfg(test)]
236pub(crate) fn malformed_static_composition_local_for_test<T: Clone + 'static>(
237    local: &StaticCompositionLocal<T>,
238    entry: Rc<dyn Any>,
239) -> ProvidedValue {
240    malformed_provided_value_for_test(local.key.clone(), entry)
241}
242
243#[expect(non_snake_case)]
244pub fn staticCompositionLocalOf<T: Clone + 'static>(
245    default: impl Fn() -> T + 'static,
246) -> StaticCompositionLocal<T> {
247    StaticCompositionLocal {
248        key: LocalKey::new(),
249        default: Rc::new(default),
250    }
251}