waterui_core/
resolve.rs

1//! # The Resolve Pattern
2//!
3//! The resolve pattern is WaterUI's core abstraction for **dynamic, reactive configuration**.
4//! Instead of hard-coding values, types implement [`Resolvable`] to look up their actual
5//! values from an [`Environment`] at runtime, returning a **reactive signal** that
6//! automatically updates when the environment changes.
7//!
8//! ## For Users
9//!
10//! ### What is Resolvable?
11//!
12//! When you use a `Color` or `Font` in WaterUI, you're not specifying a fixed value—you're
13//! specifying something that will be **resolved** against the current environment. This
14//! enables powerful features like theming and dark mode.
15//!
16//! ```ignore
17//! // This color isn't "#FF0000" - it's "whatever the Accent color is in this environment"
18//! use waterui::theme::color::Accent;
19//! text("Hello").foreground(Accent)
20//! ```
21//!
22//! ### Why Reactive?
23//!
24//! The key insight is that `resolve()` returns a [`Signal`](nami::Signal), not a plain value.
25//! This means:
26//!
27//! 1. **Native backends can inject reactive signals** - The iOS/Android runtime can inject
28//!    system colors that update when the user toggles dark mode.
29//! 2. **Views automatically re-render** - When the signal's value changes, any view using
30//!    that resolved value will update without manual intervention.
31//! 3. **No rebuild required** - Theme changes propagate instantly through the entire UI.
32//!
33//! ### The Flow
34//!
35//! ```text
36//! ┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
37//! │  Native Backend │     │   Environment   │     │      View       │
38//! │  (iOS/Android)  │     │                 │     │                 │
39//! └────────┬────────┘     └────────┬────────┘     └────────┬────────┘
40//!          │                       │                       │
41//!          │ 1. Create reactive    │                       │
42//!          │    signal (Computed)  │                       │
43//!          │──────────────────────>│                       │
44//!          │                       │                       │
45//!          │ 2. Install into env   │                       │
46//!          │    via Theme::install │                       │
47//!          │──────────────────────>│                       │
48//!          │                       │                       │
49//!          │                       │ 3. View resolves      │
50//!          │                       │    Accent.resolve(env)│
51//!          │                       │<──────────────────────│
52//!          │                       │                       │
53//!          │                       │ 4. Returns Computed   │
54//!          │                       │    (reactive signal)  │
55//!          │                       │──────────────────────>│
56//!          │                       │                       │
57//!          │ 5. User toggles       │                       │
58//!          │    dark mode          │                       │
59//!          │──────────────────────>│ 6. Signal updates     │
60//!          │                       │──────────────────────>│
61//!          │                       │    View re-renders    │
62//! ```
63//!
64//! ## For Maintainers
65//!
66//! ### Implementing Resolvable
67//!
68//! To make a type resolvable, implement the [`Resolvable`] trait:
69//!
70//! ```ignore
71//! use waterui_core::{Environment, resolve::Resolvable};
72//! use nami::{Computed, Signal};
73//!
74//! #[derive(Debug, Clone, Copy)]
75//! pub struct MyToken;
76//!
77//! impl Resolvable for MyToken {
78//!     type Resolved = MyResolvedValue;
79//!     
80//!     fn resolve(&self, env: &Environment) -> impl Signal<Output = Self::Resolved> {
81//!         // Option 1: Look up a signal from environment
82//!         env.query::<Self, Computed<Self::Resolved>>()
83//!             .cloned()
84//!             .unwrap_or_else(|| {
85//!                 // Option 2: Return a constant fallback
86//!                 Computed::constant(MyResolvedValue::default())
87//!             })
88//!     }
89//! }
90//! ```
91//!
92//! ### Key Types
93//!
94//! - [`Resolvable`] - The core trait. Implementations look up values from the environment.
95//! - [`AnyResolvable<T>`] - Type-erased wrapper for storing heterogeneous resolvables.
96//! - [`Map<R, F>`] - Transforms a resolvable's output (e.g., adjust opacity on a color).
97//!
98//! ### Integration with Theme System
99//!
100//! The theme system uses this pattern to inject platform-specific colors and fonts:
101//!
102//! 1. Native backend creates `Computed<ResolvedColor>` signals from system palette
103//! 2. `Theme::install()` stores these signals in the environment keyed by token type
104//! 3. Token types (e.g., `color::Foreground`) implement `Resolvable` to query these signals
105//! 4. When the native signal updates, all views using that token automatically update
106//!
107//! ### The nami Signal System
108//!
109//! This module integrates with [nami](https://github.com/aspect-rs/nami), WaterUI's reactive
110//! primitives library. Key concepts:
111//!
112//! - `Signal` - A trait for values that can change over time
113//! - `Computed<T>` - A cached, reactive value that re-evaluates when dependencies change
114//! - `.computed()` - Converts any `impl Signal` into a `Computed` for storage/cloning
115
116use alloc::boxed::Box;
117use core::fmt::Debug;
118
119use nami::{Computed, Signal, SignalExt};
120
121use crate::Environment;
122
123/// A trait for types that can be resolved to a reactive value from an environment.
124///
125/// This is the core abstraction for `WaterUI`'s dynamic configuration system. Types that
126/// implement `Resolvable` don't hold their final value directly—instead, they know how
127/// to look up or compute that value from an [`Environment`].
128///
129/// # Contract
130///
131/// - The same `Resolvable` instance resolved against the same `Environment` should return
132///   a signal that produces equivalent values (though the signal itself may be a new instance).
133/// - The returned signal is **reactive**: if the underlying data in the environment changes,
134///   the signal will emit updated values.
135///
136/// # Example
137///
138/// ```ignore
139/// use waterui_core::{Environment, resolve::Resolvable};
140/// use nami::{Computed, Signal};
141///
142/// /// A token representing the primary brand color.
143/// #[derive(Debug, Clone, Copy)]
144/// pub struct BrandColor;
145///
146/// impl Resolvable for BrandColor {
147///     type Resolved = ResolvedColor;
148///     
149///     fn resolve(&self, env: &Environment) -> impl Signal<Output = Self::Resolved> {
150///         // Query the environment for a pre-installed signal
151///         env.query::<Self, Computed<ResolvedColor>>()
152///             .cloned()
153///             .unwrap_or_else(|| Computed::constant(ResolvedColor::default()))
154///     }
155/// }
156/// ```
157pub trait Resolvable: Debug + Clone {
158    /// The concrete type produced after resolution.
159    type Resolved;
160
161    /// Resolves this value in the given environment, returning a reactive signal.
162    ///
163    /// The returned signal will emit the current resolved value and any future updates.
164    /// Callers typically use `.get()` for one-shot reads or subscribe for continuous updates.
165    fn resolve(&self, env: &Environment) -> impl Signal<Output = Self::Resolved>;
166}
167
168trait ResolvableImpl<T>: Debug {
169    fn resolve(&self, env: &Environment) -> Computed<T>;
170    fn clone_box(&self) -> Box<dyn ResolvableImpl<T>>;
171}
172
173impl<R: Resolvable + 'static> ResolvableImpl<R::Resolved> for R {
174    fn resolve(&self, env: &Environment) -> Computed<R::Resolved> {
175        self.resolve(env).computed()
176    }
177
178    fn clone_box(&self) -> Box<dyn ResolvableImpl<R::Resolved>> {
179        Box::new(self.clone())
180    }
181}
182
183/// A type-erased wrapper for any resolvable value.
184///
185/// `AnyResolvable<T>` allows storing different `Resolvable` implementations that all
186/// resolve to the same output type `T`. This is essential for types like `Color` and
187/// `Font` which can be constructed from many different sources (hex strings, theme
188/// tokens, computed values) but all resolve to the same concrete type.
189///
190/// # Example
191///
192/// ```ignore
193/// use waterui_core::resolve::AnyResolvable;
194///
195/// // These all resolve to ResolvedColor, but come from different sources
196/// let from_hex = AnyResolvable::new(Srgb::from_hex("#FF0000"));
197/// let from_token = AnyResolvable::new(theme::color::Accent);
198/// let from_computed = AnyResolvable::new(some_color.lighten(0.2));
199/// ```
200#[derive(Debug)]
201pub struct AnyResolvable<T> {
202    inner: Box<dyn ResolvableImpl<T>>,
203}
204
205impl<T> Resolvable for AnyResolvable<T>
206where
207    T: 'static + Debug,
208{
209    type Resolved = T;
210    fn resolve(&self, env: &Environment) -> impl Signal<Output = Self::Resolved> {
211        self.inner.resolve(env)
212    }
213}
214
215impl<T> Clone for AnyResolvable<T> {
216    fn clone(&self) -> Self {
217        Self {
218            inner: self.inner.clone_box(),
219        }
220    }
221}
222impl<T> AnyResolvable<T> {
223    /// Creates a new type-erased resolvable value.
224    ///
225    /// # Arguments
226    /// * `value` - The resolvable value to wrap
227    pub fn new(value: impl Resolvable<Resolved = T> + 'static) -> Self {
228        Self {
229            inner: Box::new(value),
230        }
231    }
232
233    /// Resolves this value in the given environment.
234    ///
235    /// # Arguments
236    /// * `env` - The environment to resolve in
237    #[must_use]
238    pub fn resolve(&self, env: &Environment) -> Computed<T> {
239        self.inner.resolve(env)
240    }
241}
242
243/// A mapping type that transforms a resolvable value using a function.
244///
245/// `Map` wraps an existing `Resolvable` and applies a transformation function to its
246/// resolved output. This enables fluent APIs like `color.lighten(0.2)` or
247/// `font.with_weight(Bold)` without losing reactivity.
248///
249/// # Example
250///
251/// ```ignore
252/// use waterui_core::resolve::Map;
253///
254/// // Create a lighter version of the accent color
255/// let lighter_accent = Map::new(
256///     theme::color::Accent,
257///     |color| color.with_lightness(color.lightness() + 0.2)
258/// );
259/// ```
260///
261/// The transformation is applied lazily when the signal emits, so if the underlying
262/// `Accent` color changes (e.g., dark mode toggle), the lighter version updates too.
263#[derive(Clone)]
264pub struct Map<R, F> {
265    resolvable: R,
266    func: F,
267}
268
269impl<R: Debug, F> Debug for Map<R, F> {
270    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
271        f.debug_struct("With")
272            .field("resolvable", &self.resolvable)
273            .field("func", &"Fn(...)")
274            .finish()
275    }
276}
277
278impl<R, F> Map<R, F> {
279    /// Creates a new mapping that transforms the resolved value using the given function.
280    #[must_use]
281    pub const fn new<T, U>(resolvable: R, func: F) -> Self
282    where
283        R: Resolvable<Resolved = T>,
284        F: Fn(T) -> U + Clone + 'static,
285        T: 'static,
286        U: 'static,
287    {
288        Self { resolvable, func }
289    }
290}
291
292impl<R, F, T, U> Resolvable for Map<R, F>
293where
294    R: Resolvable<Resolved = T>,
295    F: Fn(T) -> U + Clone + 'static,
296    T: 'static,
297    U: 'static,
298{
299    type Resolved = U;
300    fn resolve(&self, env: &Environment) -> impl Signal<Output = Self::Resolved> {
301        let func = self.func.clone();
302        self.resolvable.resolve(env).map(func)
303    }
304}