use crate::signal::Signal;
use std::any::{Any, TypeId};
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
pub struct Context<T> {
signal: Signal<T>,
}
impl<T> Clone for Context<T> {
fn clone(&self) -> Self {
Context {
signal: self.signal.clone(),
}
}
}
impl<T: Clone + 'static> Context<T> {
pub fn new(value: T) -> Self {
Context {
signal: Signal::new(value),
}
}
pub fn from_signal(signal: Signal<T>) -> Self {
Context { signal }
}
pub fn get(&self) -> T
where
T: Clone,
{
self.signal.get()
}
pub fn set(&self, value: T) {
self.signal.set(value);
}
pub fn signal(&self) -> Signal<T> {
self.signal.clone()
}
}
thread_local! {
static PROVIDERS: RefCell<HashMap<TypeId, Vec<Rc<dyn Any>>>> =
RefCell::new(HashMap::new());
}
pub fn provide_context<T: 'static>(ctx: &Context<T>, scope: impl FnOnce()) {
let key = TypeId::of::<T>();
PROVIDERS.with(|p| {
p.borrow_mut()
.entry(key)
.or_insert_with(Vec::new)
.push(Rc::new(ctx.clone()) as Rc<dyn Any>);
});
scope();
PROVIDERS.with(|p| {
let mut map = p.borrow_mut();
if let Some(stack) = map.get_mut(&key) {
stack.pop();
if stack.is_empty() {
map.remove(&key);
}
}
});
}
pub fn use_context<T: 'static>() -> Option<Context<T>> {
let key = TypeId::of::<T>();
PROVIDERS.with(|p| {
let stack = p.borrow();
stack
.get(&key)
.and_then(|s| s.last())
.and_then(|rc| rc.downcast_ref::<Context<T>>())
.cloned()
})
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Clone, PartialEq)]
struct Theme {
dark: bool,
}
#[derive(Debug, Clone, PartialEq)]
struct User {
name: String,
}
#[test]
fn use_context_returns_none_when_unprovided() {
assert!(use_context::<Theme>().is_none());
}
#[test]
fn provide_then_use_resolves_nearest() {
let outer = Context::new(Theme { dark: false });
let mut observed = None;
provide_context(&outer, || {
observed = use_context::<Theme>().map(|c| c.get());
});
assert_eq!(observed, Some(Theme { dark: false }));
}
#[test]
fn inner_provider_shadows_outer() {
let outer = Context::new(Theme { dark: false });
let inner = Context::new(Theme { dark: true });
let mut outer_before = None;
let mut inner_seen = None;
let mut outer_after = None;
provide_context(&outer, || {
outer_before = use_context::<Theme>().map(|c| c.get());
provide_context(&inner, || {
inner_seen = use_context::<Theme>().map(|c| c.get());
});
outer_after = use_context::<Theme>().map(|c| c.get());
});
assert_eq!(inner_seen, Some(Theme { dark: true }));
assert_eq!(outer_before, Some(Theme { dark: false }));
assert_eq!(outer_after, Some(Theme { dark: false }));
}
#[test]
fn different_types_are_independent() {
let theme = Context::new(Theme { dark: true });
let user = Context::new(User {
name: "ada".to_string(),
});
provide_context(&theme, || {
provide_context(&user, || {
assert!(use_context::<Theme>().is_some());
assert!(use_context::<User>().is_some());
assert!(use_context::<i32>().is_none());
});
});
}
#[test]
fn context_updates_are_visible_to_consumers() {
let theme = Context::new(Theme { dark: false });
let observed = Rc::new(RefCell::new(None));
let obs = observed.clone();
provide_context(&theme, || {
if let Some(c) = use_context::<Theme>() {
*obs.borrow_mut() = Some(c.get());
c.set(Theme { dark: true });
*obs.borrow_mut() = Some(c.get());
}
});
assert_eq!(*observed.borrow(), Some(Theme { dark: true }));
}
}