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