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;
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(|| {
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#[allow(non_snake_case)]
136pub fn compositionLocalOf<T: Clone + PartialEq + 'static>(
137    default: impl Fn() -> T + 'static,
138) -> CompositionLocal<T> {
139    compositionLocalOfWithPolicy(default, |current, next| current == next)
140}
141
142#[allow(non_snake_case)]
143pub fn compositionLocalOfWithPolicy<T: Clone + 'static>(
144    default: impl Fn() -> T + 'static,
145    equivalent: impl Fn(&T, &T) -> bool + Send + Sync + 'static,
146) -> CompositionLocal<T> {
147    CompositionLocal {
148        key: crate::next_local_key(),
149        default: Rc::new(default),
150        equivalent: Arc::new(equivalent),
151    }
152}
153
154/// A `StaticCompositionLocal` is a CompositionLocal that is optimized for values that are
155/// unlikely to change. Unlike `CompositionLocal`, reads of a `StaticCompositionLocal` are not
156/// tracked by the recomposition system, which means:
157/// - Reading `.current()` does NOT establish a subscription
158/// - Changing the provided value does NOT automatically invalidate readers
159/// - This makes it more efficient for truly static values
160///
161/// This matches the API of Jetpack Compose's `staticCompositionLocalOf` but with simplified
162/// semantics. Use this for values that are guaranteed to never change during the lifetime of
163/// the CompositionLocalProvider scope (e.g., application-wide constants, configuration)
164#[derive(Clone)]
165pub struct StaticCompositionLocal<T: Clone + 'static> {
166    pub(crate) key: LocalKey,
167    default: Rc<dyn Fn() -> T>,
168}
169
170impl<T: Clone + 'static> PartialEq for StaticCompositionLocal<T> {
171    fn eq(&self, other: &Self) -> bool {
172        self.key == other.key
173    }
174}
175
176impl<T: Clone + 'static> Eq for StaticCompositionLocal<T> {}
177
178impl<T: Clone + 'static> StaticCompositionLocal<T> {
179    pub fn provides(&self, value: T) -> ProvidedValue {
180        let key = self.key;
181        ProvidedValue {
182            key,
183            apply: Box::new(move |composer: &Composer| {
184                let entry_ref = composer.remember(|| Rc::new(StaticLocalEntry::new(value.clone())));
185                entry_ref.update(|entry| entry.set(value.clone()));
186                entry_ref.with(|entry| entry.clone() as Rc<dyn Any>)
187            }),
188        }
189    }
190
191    pub fn current(&self) -> T {
192        composer_context::with_composer(|composer| composer.read_static_composition_local(self))
193    }
194
195    pub fn default_value(&self) -> T {
196        (self.default)()
197    }
198}
199
200#[allow(non_snake_case)]
201pub fn staticCompositionLocalOf<T: Clone + 'static>(
202    default: impl Fn() -> T + 'static,
203) -> StaticCompositionLocal<T> {
204    StaticCompositionLocal {
205        key: crate::next_local_key(),
206        default: Rc::new(default),
207    }
208}