Skip to main content

cranpose_core/
composition_locals.rs

1use std::{
2    any::Any,
3    cell::RefCell,
4    hash::{Hash, Hasher},
5    rc::{Rc, Weak},
6    sync::Arc,
7};
8
9use crate::{
10    Composer, Key, RuntimeHandle, avalanche_location_key, composer, composer_context,
11    state::{MutationPolicy, OwnedMutableState},
12};
13
14fn provider_entry_source(key: &LocalKey, caller: Key) -> Key {
15    key.entry_source().mixed(caller)
16}
17
18/// A composition local paired with the value to provide, made by `provides` and passed to
19/// `CompositionLocalProvider`.
20pub struct ProvidedValue {
21    key: LocalKey,
22    #[expect(clippy::type_complexity, reason = "one boxed closure per local")]
23    apply: Box<dyn Fn(&Composer, Key) -> (Rc<dyn Any>, bool)>,
24}
25
26impl ProvidedValue {
27    pub(crate) fn key(&self) -> &LocalKey {
28        &self.key
29    }
30
31    /// The provided entry, and whether it replaced a different value of a
32    /// static local that this site provided before.
33    pub(crate) fn into_entry(
34        self,
35        composer: &Composer,
36        site: Key,
37    ) -> (LocalKey, Rc<dyn Any>, bool) {
38        let Self { key, apply } = self;
39        let (entry, static_changed) = apply(composer, site);
40        (key, entry, static_changed)
41    }
42}
43
44/// Makes `values` available to `content` and everything it composes. Where two values are for the
45/// same local, the last wins. Compose's `CompositionLocalProvider`.
46#[expect(non_snake_case, reason = "names follow Compose's casing")]
47#[track_caller]
48pub fn CompositionLocalProvider(
49    values: impl IntoIterator<Item = ProvidedValue>,
50    content: impl FnOnce(),
51) {
52    let site = crate::caller_location_key();
53    composer_context::with_composer(|composer| {
54        composer.with_composition_locals(values, site, |_composer| content());
55    });
56}
57
58pub(crate) struct LocalStateEntry<T: Clone + 'static> {
59    state: OwnedMutableState<T>,
60}
61
62type LocalEquivalentFn<T> = dyn Fn(&T, &T) -> bool + Send + Sync + 'static;
63
64struct LocalValuePolicy<T: Clone + 'static> {
65    equivalent: Arc<LocalEquivalentFn<T>>,
66}
67
68impl<T: Clone + 'static> MutationPolicy<T> for LocalValuePolicy<T> {
69    fn equivalent(&self, a: &T, b: &T) -> bool {
70        (self.equivalent)(a, b)
71    }
72}
73
74impl<T: Clone + 'static> LocalStateEntry<T> {
75    fn new(initial: T, runtime: RuntimeHandle, equivalent: Arc<LocalEquivalentFn<T>>) -> Self {
76        Self {
77            state: OwnedMutableState::with_runtime_and_policy(
78                initial,
79                runtime,
80                Rc::new(LocalValuePolicy { equivalent }),
81            ),
82        }
83    }
84
85    fn set(&self, value: T) {
86        self.state.replace(value);
87    }
88
89    pub(crate) fn value(&self) -> T {
90        self.state.value()
91    }
92}
93
94pub(crate) struct StaticLocalEntry<T: Clone + 'static> {
95    value: RefCell<T>,
96}
97
98impl<T: Clone + 'static> StaticLocalEntry<T> {
99    fn new(value: T) -> Self {
100        Self {
101            value: RefCell::new(value),
102        }
103    }
104
105    /// Stores `value` and says whether it differs from the stored one.
106    fn set(&self, value: T, equivalent: &LocalEquivalentFn<T>) -> bool {
107        let mut current = self.value.borrow_mut();
108        if equivalent(&current, &value) {
109            return false;
110        }
111        *current = value;
112        true
113    }
114
115    pub(crate) fn value(&self) -> T {
116        self.value.borrow().clone()
117    }
118}
119
120type ComputeLocalFn<T> = dyn Fn(&CompositionLocalAccessorScope) -> T;
121
122/// The value of a local whose provider computes it where it is read, from
123/// the other locals there. A reader recomposes when the provider gives a
124/// new computation, as with Compose's lambdas that are not remembered.
125pub(crate) struct ComputedLocalEntry<T: 'static> {
126    compute: OwnedMutableState<Rc<ComputeLocalFn<T>>>,
127}
128
129impl<T: 'static> ComputedLocalEntry<T> {
130    pub(crate) fn value(&self) -> T {
131        let compute = self.compute.value();
132        compute(&CompositionLocalAccessorScope { _private: () })
133    }
134}
135
136/// A static local's computed value: see [`ComputedLocalEntry`]. A new
137/// computation recomposes all the provider's content, as a new static value
138/// does.
139pub(crate) struct StaticComputedLocalEntry<T: 'static> {
140    compute: Rc<ComputeLocalFn<T>>,
141}
142
143impl<T: 'static> StaticComputedLocalEntry<T> {
144    pub(crate) fn value(&self) -> T {
145        (self.compute)(&CompositionLocalAccessorScope { _private: () })
146    }
147}
148
149/// What a computed composition local's value reads other locals through:
150/// their values where the computed local is read, not where it was
151/// provided. Compose's `CompositionLocalAccessorScope`.
152pub struct CompositionLocalAccessorScope {
153    _private: (),
154}
155
156impl CompositionLocalAccessorScope {
157    /// The value `local` has where the computed local is read. Compose's
158    /// `CompositionLocal.currentValue` in this scope.
159    pub fn current_value<T>(&self, local: &impl ReadableCompositionLocal<T>) -> T {
160        local.read_current()
161    }
162}
163
164/// A composition local whose current value a computed local can read; see
165/// [`CompositionLocalAccessorScope`].
166pub trait ReadableCompositionLocal<T> {
167    /// The local's value where it is read, as `current` gives it.
168    fn read_current(&self) -> T;
169}
170
171impl<T: Clone + 'static> ReadableCompositionLocal<T> for CompositionLocal<T> {
172    fn read_current(&self) -> T {
173        self.current()
174    }
175}
176
177impl<T: Clone + 'static> ReadableCompositionLocal<T> for StaticCompositionLocal<T> {
178    fn read_current(&self) -> T {
179        self.current()
180    }
181}
182
183/// A value that composables read from their surroundings without passing it as a parameter, as
184/// Jetpack Compose's `compositionLocalOf`. A scope that reads `current` recomposes when the
185/// provided value changes.
186#[derive(Clone)]
187pub struct CompositionLocal<T: Clone + 'static> {
188    pub(crate) key: LocalKey,
189    default: Rc<dyn Fn() -> T>,
190    equivalent: Arc<LocalEquivalentFn<T>>,
191}
192
193impl<T: Clone + 'static> PartialEq for CompositionLocal<T> {
194    fn eq(&self, other: &Self) -> bool {
195        self.key == other.key
196    }
197}
198
199impl<T: Clone + 'static> Eq for CompositionLocal<T> {}
200
201impl<T: Clone + 'static> CompositionLocal<T> {
202    /// Pairs this local with `value` for use in `CompositionLocalProvider`. When a later
203    /// composition provides a different value, the scopes that read the local recompose.
204    #[track_caller]
205    pub fn provides(&self, value: T) -> ProvidedValue {
206        let key = self.key.clone();
207        let entry_source = provider_entry_source(&key, crate::caller_location_key());
208        let equivalent = Arc::clone(&self.equivalent);
209        ProvidedValue {
210            key,
211            apply: Box::new(move |composer: &Composer, site: Key| {
212                let runtime = composer.runtime_handle();
213                let source = entry_source.mixed(site);
214                let entry_ref = composer.remember_internal(source, || {
215                    Rc::new(LocalStateEntry::new(
216                        value.clone(),
217                        runtime.clone(),
218                        Arc::clone(&equivalent),
219                    ))
220                });
221                entry_ref.update(|entry| entry.set(value.clone()));
222                entry_ref.with(|entry| (Rc::clone(entry) as Rc<dyn Any>, false))
223            }),
224        }
225    }
226
227    /// Pairs this local with `compute`, which runs at each read of the local
228    /// under the provider and reads other locals there through its scope.
229    /// A reader recomposes when a local the computation read changes, and
230    /// when a later composition of the provider gives a new computation.
231    /// Compose's `providesComputed`.
232    #[track_caller]
233    pub fn provides_computed(
234        &self,
235        compute: impl Fn(&CompositionLocalAccessorScope) -> T + 'static,
236    ) -> ProvidedValue {
237        let key = self.key.clone();
238        let entry_source = provider_entry_source(&key, crate::caller_location_key());
239        let compute: Rc<ComputeLocalFn<T>> = Rc::new(compute);
240        ProvidedValue {
241            key,
242            apply: Box::new(move |composer: &Composer, site: Key| {
243                let runtime = composer.runtime_handle();
244                let source = entry_source.mixed(site);
245                let entry_ref = composer.remember_internal(source, || {
246                    Rc::new(ComputedLocalEntry {
247                        compute: OwnedMutableState::with_runtime_and_policy(
248                            Rc::clone(&compute),
249                            runtime.clone(),
250                            Rc::new(LocalValuePolicy::<Rc<ComputeLocalFn<T>>> {
251                                equivalent: Arc::new(Rc::ptr_eq),
252                            }),
253                        ),
254                    })
255                });
256                entry_ref.update(|entry| entry.compute.replace(Rc::clone(&compute)));
257                entry_ref.with(|entry| (Rc::clone(entry) as Rc<dyn Any>, false))
258            }),
259        }
260    }
261
262    /// Returns the value from the innermost enclosing provider, or the default value when none is
263    /// in scope. Must be called during composition. The reading scope recomposes when the provided
264    /// value changes.
265    pub fn current(&self) -> T {
266        composer_context::with_composer(|composer| composer.read_composition_local(self))
267    }
268
269    /// Returns the value that `current` gives where no provider is in scope.
270    pub fn default_value(&self) -> T {
271        (self.default)()
272    }
273}
274
275#[cfg(test)]
276fn malformed_provided_value_for_test(key: LocalKey, entry: Rc<dyn Any>) -> ProvidedValue {
277    ProvidedValue {
278        key,
279        apply: Box::new(move |_, _| (Rc::clone(&entry), false)),
280    }
281}
282
283#[cfg(test)]
284pub(crate) fn malformed_composition_local_for_test<T: Clone + 'static>(
285    local: &CompositionLocal<T>,
286    entry: Rc<dyn Any>,
287) -> ProvidedValue {
288    malformed_provided_value_for_test(local.key.clone(), entry)
289}
290
291/// Creates a [`CompositionLocal`] whose default is made by `default`. A provided value replaces the
292/// previous one when it differs by `==`. Compose's `compositionLocalOf`.
293#[expect(non_snake_case, reason = "names follow Compose's casing")]
294pub fn compositionLocalOf<T: Clone + PartialEq + 'static>(
295    default: impl Fn() -> T + 'static,
296) -> CompositionLocal<T> {
297    compositionLocalOfWithPolicy(default, |current, next| current == next)
298}
299
300/// Creates a [`CompositionLocal`] whose default is made by `default`. A provided value replaces the
301/// previous one only when `equivalent` returns `false` for the two.
302#[expect(non_snake_case, reason = "names follow Compose's casing")]
303pub fn compositionLocalOfWithPolicy<T: Clone + 'static>(
304    default: impl Fn() -> T + 'static,
305    equivalent: impl Fn(&T, &T) -> bool + Send + Sync + 'static,
306) -> CompositionLocal<T> {
307    CompositionLocal {
308        key: LocalKey::new(),
309        default: Rc::new(default),
310        equivalent: Arc::new(equivalent),
311    }
312}
313
314/// A composition local for values that rarely change, as Jetpack Compose's
315/// `staticCompositionLocalOf`. A read of `.current()` subscribes nothing, so
316/// it costs less than a read of a [`CompositionLocal`]. In return, a
317/// provider whose value changes recomposes everything it provides to, not
318/// only the readers: every composable call under it runs its body, and
319/// every subcomposition under it composes again the same way.
320#[derive(Clone)]
321pub struct StaticCompositionLocal<T: Clone + 'static> {
322    pub(crate) key: LocalKey,
323    default: Rc<dyn Fn() -> T>,
324    equivalent: Arc<LocalEquivalentFn<T>>,
325    /// The provider frame the last read read under and what it found there,
326    /// shared by the local's clones: the next read under the same frame
327    /// takes it without walking the providers.
328    last_read: Rc<RefCell<Option<StaticRead<T>>>>,
329}
330
331/// What a static local's read found under a provider frame.
332enum Provided<T: Clone + 'static> {
333    Value(Rc<StaticLocalEntry<T>>),
334    Computed(Rc<StaticComputedLocalEntry<T>>),
335    Default,
336}
337
338impl<T: Clone + 'static> Provided<T> {
339    fn downgrade(&self) -> StaticFound<T> {
340        match self {
341            Self::Value(entry) => StaticFound::Value(Rc::downgrade(entry)),
342            Self::Computed(entry) => StaticFound::Computed(Rc::downgrade(entry)),
343            Self::Default => StaticFound::Default,
344        }
345    }
346
347    /// The value of an entry, read each time as its provider updates the
348    /// entry in place, or what reads it once the provider stack is free.
349    fn read(self) -> Result<T, StaticDeferred<T>> {
350        match self {
351            Self::Value(entry) => Ok(entry.value()),
352            Self::Computed(entry) => Err(StaticDeferred::Computed(entry)),
353            Self::Default => Err(StaticDeferred::Default),
354        }
355    }
356}
357
358/// [`Provided`], held so the last read keeps no provider's value alive.
359enum StaticFound<T: Clone + 'static> {
360    Value(Weak<StaticLocalEntry<T>>),
361    Computed(Weak<StaticComputedLocalEntry<T>>),
362    Default,
363}
364
365impl<T: Clone + 'static> StaticFound<T> {
366    fn upgrade(&self) -> Option<Provided<T>> {
367        match self {
368            Self::Value(entry) => entry.upgrade().map(Provided::Value),
369            Self::Computed(entry) => entry.upgrade().map(Provided::Computed),
370            Self::Default => Some(Provided::Default),
371        }
372    }
373}
374
375/// A static local's value that runs once the provider stack is released,
376/// as it may read other locals.
377pub(crate) enum StaticDeferred<T: Clone + 'static> {
378    Computed(Rc<StaticComputedLocalEntry<T>>),
379    Default,
380}
381
382impl<T: Clone + 'static> StaticDeferred<T> {
383    pub(crate) fn value(self, local: &StaticCompositionLocal<T>) -> T {
384        match self {
385            Self::Computed(entry) => entry.value(),
386            Self::Default => local.default_value(),
387        }
388    }
389}
390
391/// The innermost provider frame a static local was last read under, and
392/// what it found there.
393struct StaticRead<T: Clone + 'static> {
394    frame: Option<Weak<composer::LocalFrame>>,
395    found: StaticFound<T>,
396}
397
398/// Whether `frame` is the innermost frame of `stack`. A weak handle keeps
399/// its frame's allocation, so no other frame takes the address meanwhile.
400fn is_top_frame(frame: &Option<Weak<composer::LocalFrame>>, stack: &LocalStackSnapshot) -> bool {
401    match (frame, stack) {
402        (Some(frame), Some(top)) => std::ptr::eq(frame.as_ptr(), Rc::as_ptr(top)),
403        (None, None) => true,
404        _ => false,
405    }
406}
407
408impl<T: Clone + 'static> PartialEq for StaticCompositionLocal<T> {
409    fn eq(&self, other: &Self) -> bool {
410        self.key == other.key
411    }
412}
413
414impl<T: Clone + 'static> Eq for StaticCompositionLocal<T> {}
415
416impl<T: Clone + 'static> StaticCompositionLocal<T> {
417    /// The value `stack` provides for this local, or what to read it from
418    /// when it is computed or the default: those may read other locals, so
419    /// they run once the caller releases the stack.
420    pub(crate) fn provided_under(
421        &self,
422        stack: &LocalStackSnapshot,
423    ) -> Result<T, StaticDeferred<T>> {
424        let mut last = self.last_read.borrow_mut();
425        if let Some(read) = &*last
426            && is_top_frame(&read.frame, stack)
427            && let Some(provided) = read.found.upgrade()
428        {
429            return provided.read();
430        }
431        let provided = self.find(stack);
432        *last = Some(StaticRead {
433            frame: stack.as_ref().map(Rc::downgrade),
434            found: provided.downgrade(),
435        });
436        provided.read()
437    }
438
439    fn find(&self, stack: &LocalStackSnapshot) -> Provided<T> {
440        let Some(entry) = composer::provided_entry(stack, &self.key) else {
441            return Provided::Default;
442        };
443        match entry.downcast::<StaticLocalEntry<T>>() {
444            Ok(entry) => Provided::Value(entry),
445            Err(entry) => match entry.downcast::<StaticComputedLocalEntry<T>>() {
446                Ok(entry) => Provided::Computed(entry),
447                Err(_) => {
448                    log::error!(
449                        "composition local entry type mismatch for key {:?}",
450                        self.key
451                    );
452                    Provided::Default
453                }
454            },
455        }
456    }
457
458    /// Pairs this local with `value` for use in `CompositionLocalProvider`. When a later
459    /// composition provides a different value, everything under the provider recomposes.
460    #[track_caller]
461    pub fn provides(&self, value: T) -> ProvidedValue {
462        let key = self.key.clone();
463        let entry_source = provider_entry_source(&key, crate::caller_location_key());
464        let equivalent = Arc::clone(&self.equivalent);
465        ProvidedValue {
466            key,
467            apply: Box::new(move |composer: &Composer, site: Key| {
468                let source = entry_source.mixed(site);
469                let entry_ref = composer
470                    .remember_internal(source, || Rc::new(StaticLocalEntry::new(value.clone())));
471                let changed = entry_ref.update(|entry| entry.set(value.clone(), &*equivalent));
472                entry_ref.with(|entry| (Rc::clone(entry) as Rc<dyn Any>, changed))
473            }),
474        }
475    }
476
477    /// Pairs this local with `compute`, which runs at each read of the local
478    /// under the provider and reads other locals there through its scope.
479    /// Each composition of the provider gives a new computation, which
480    /// recomposes everything under it. Compose's `providesComputed`.
481    #[track_caller]
482    pub fn provides_computed(
483        &self,
484        compute: impl Fn(&CompositionLocalAccessorScope) -> T + 'static,
485    ) -> ProvidedValue {
486        let compute: Rc<ComputeLocalFn<T>> = Rc::new(compute);
487        ProvidedValue {
488            key: self.key.clone(),
489            apply: Box::new(move |_: &Composer, _: Key| {
490                let entry = Rc::new(StaticComputedLocalEntry {
491                    compute: Rc::clone(&compute),
492                });
493                (entry as Rc<dyn Any>, true)
494            }),
495        }
496    }
497
498    /// Returns the value from the innermost enclosing provider, or the default value when none is
499    /// in scope. Must be called during composition. A read subscribes nothing.
500    pub fn current(&self) -> T {
501        composer_context::with_composer(|composer| composer.read_static_composition_local(self))
502    }
503
504    /// Returns the value that `current` gives where no provider is in scope.
505    pub fn default_value(&self) -> T {
506        (self.default)()
507    }
508}
509
510#[cfg(test)]
511pub(crate) fn malformed_static_composition_local_for_test<T: Clone + 'static>(
512    local: &StaticCompositionLocal<T>,
513    entry: Rc<dyn Any>,
514) -> ProvidedValue {
515    malformed_provided_value_for_test(local.key.clone(), entry)
516}
517
518/// A [`StaticCompositionLocal`] whose provided values compare with `==`.
519#[expect(non_snake_case, reason = "names follow Compose's casing")]
520pub fn staticCompositionLocalOf<T: Clone + PartialEq + 'static>(
521    default: impl Fn() -> T + 'static,
522) -> StaticCompositionLocal<T> {
523    staticCompositionLocalOfWithPolicy(default, |current, next| current == next)
524}
525
526/// A [`StaticCompositionLocal`] whose provider recomposes its content when
527/// `equivalent` says a new value differs from the last one.
528#[expect(non_snake_case, reason = "names follow Compose's casing")]
529pub fn staticCompositionLocalOfWithPolicy<T: Clone + 'static>(
530    default: impl Fn() -> T + 'static,
531    equivalent: impl Fn(&T, &T) -> bool + Send + Sync + 'static,
532) -> StaticCompositionLocal<T> {
533    StaticCompositionLocal {
534        key: LocalKey::new(),
535        default: Rc::new(default),
536        equivalent: Arc::new(equivalent),
537        last_read: Rc::default(),
538    }
539}
540
541/// A [`CompositionLocal`] whose default is computed at each read without a
542/// provider, from the other locals there. A scope that reads it recomposes
543/// when a dynamic local the computation read changes. As the default reads
544/// locals, it is computed during composition only. Compose's
545/// `compositionLocalWithComputedDefaultOf`.
546#[expect(non_snake_case, reason = "names follow Compose's casing")]
547pub fn compositionLocalWithComputedDefaultOf<T: Clone + PartialEq + 'static>(
548    compute: impl Fn(&CompositionLocalAccessorScope) -> T + 'static,
549) -> CompositionLocal<T> {
550    compositionLocalOf(move || compute(&CompositionLocalAccessorScope { _private: () }))
551}
552
553/// A [`StaticCompositionLocal`] whose default is computed at each read
554/// without a provider, from the other locals there. Compose's
555/// `staticCompositionLocalWithComputedDefaultOf`.
556#[expect(non_snake_case, reason = "names follow Compose's casing")]
557pub fn staticCompositionLocalWithComputedDefaultOf<T: Clone + PartialEq + 'static>(
558    compute: impl Fn(&CompositionLocalAccessorScope) -> T + 'static,
559) -> StaticCompositionLocal<T> {
560    staticCompositionLocalOf(move || compute(&CompositionLocalAccessorScope { _private: () }))
561}
562
563/// The composition locals in scope: the innermost provider's frame, whose
564/// parents lead out to the outermost, or `None` outside every provider. A
565/// provider pushes one frame over the chain, so a snapshot of the locals is
566/// a shared pointer and never a copy of them.
567pub(crate) type LocalStackSnapshot = Option<Rc<composer::LocalFrame>>;
568
569#[derive(Clone)]
570pub(crate) struct LocalKey(Rc<()>);
571
572impl LocalKey {
573    fn new() -> Self {
574        Self(Rc::new(()))
575    }
576
577    pub(crate) fn entry_source(&self) -> Key {
578        Key(avalanche_location_key(Rc::as_ptr(&self.0) as usize as u64))
579    }
580}
581
582impl std::fmt::Debug for LocalKey {
583    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
584        f.debug_tuple("LocalKey")
585            .field(&(Rc::as_ptr(&self.0) as usize))
586            .finish()
587    }
588}
589
590impl PartialEq for LocalKey {
591    fn eq(&self, other: &Self) -> bool {
592        Rc::ptr_eq(&self.0, &other.0)
593    }
594}
595
596impl Eq for LocalKey {}
597
598impl Hash for LocalKey {
599    fn hash<H: Hasher>(&self, state: &mut H) {
600        Rc::as_ptr(&self.0).hash(state);
601    }
602}