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}