Skip to main content

cranpose_core/
composition_locals.rs

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