Skip to main content

dioxus_ui_system/theme/
context.rs

1//! Theme context for Dioxus
2//!
3//! Provides a context provider and hooks for accessing and modifying the theme
4//! throughout the component tree.
5
6use super::tokens::ThemeTokens;
7use dioxus::prelude::*;
8
9/// Theme context providing access to current theme and theme switching
10#[derive(Clone)]
11pub struct ThemeContext {
12    /// Current theme tokens (reactive signal)
13    pub tokens: Signal<ThemeTokens>,
14    /// Callback to set a new theme
15    pub set_theme: Callback<ThemeTokens>,
16    /// Callback to toggle between light and dark modes
17    pub toggle_mode: Callback<()>,
18    /// Callback to set theme by name (preset)
19    pub set_theme_by_name: Callback<String>,
20}
21
22impl ThemeContext {
23    /// Get the current theme tokens value
24    pub fn current(&self) -> ThemeTokens {
25        self.tokens.read().clone()
26    }
27}
28
29/// Hook to access the theme context
30///
31/// # Panics
32/// Panics if called outside of a ThemeProvider
33///
34/// # Example
35/// ```rust,ignore
36/// use dioxus_ui_system::theme::use_theme;
37///
38/// fn MyComponent() -> Element {
39///     let theme = use_theme();
40///     let bg_color = theme.tokens.read().colors.background.to_rgba();
41///     
42///     rsx! {
43///         div { style: "background-color: {bg_color}", "Hello" }
44///     }
45/// }
46/// ```
47pub fn use_theme() -> ThemeContext {
48    use_context::<ThemeContext>()
49}
50
51/// Hook for computing memoized styles based on theme
52///
53/// # Type Parameters
54/// * `F` - Function type that computes a value from theme tokens
55/// * `R` - Return type (must be PartialEq for memoization)
56///
57/// # Example
58/// ```rust,ignore
59/// use dioxus_ui_system::theme::use_style;
60///
61/// fn MyComponent() -> Element {
62///     let bg_color = use_style(|tokens| tokens.colors.background.to_rgba());
63///     
64///     rsx! {
65///         div { style: "background-color: {bg_color}", "Hello" }
66///     }
67/// }
68/// ```
69pub fn use_style<F, R>(f: F) -> Memo<R>
70where
71    F: Fn(&ThemeTokens) -> R + 'static,
72    R: PartialEq + 'static,
73{
74    let theme = use_theme();
75    use_memo(move || f(&theme.tokens.read()))
76}
77
78#[cfg(all(feature = "web", target_arch = "wasm32"))]
79const THEME_STORAGE_KEY: &str = "dioxus-ui-theme";
80
81/// Load theme from localStorage (web only)
82#[cfg(all(feature = "web", target_arch = "wasm32"))]
83fn load_theme_from_storage() -> Option<ThemeTokens> {
84    web_sys::window()
85        .and_then(|w| w.local_storage().ok())
86        .flatten()
87        .and_then(|storage| storage.get_item(THEME_STORAGE_KEY).ok())
88        .flatten()
89        .and_then(|name| ThemeTokens::by_name(&name))
90}
91
92/// Save theme to localStorage (web only)
93#[cfg(all(feature = "web", target_arch = "wasm32"))]
94fn save_theme_to_storage(theme_name: &str) {
95    if let Some(storage) = web_sys::window()
96        .and_then(|w| w.local_storage().ok())
97        .flatten()
98    {
99        let _ = storage.set_item(THEME_STORAGE_KEY, theme_name);
100    }
101}
102
103#[cfg(not(all(feature = "web", target_arch = "wasm32")))]
104fn load_theme_from_storage() -> Option<ThemeTokens> {
105    None
106}
107
108#[cfg(not(all(feature = "web", target_arch = "wasm32")))]
109fn save_theme_to_storage(_theme_name: &str) {}
110
111/// Theme provider component
112///
113/// Wraps children with theme context. Must be placed near the root of your app.
114/// Optionally persists theme selection to localStorage on web.
115///
116/// # Properties
117/// * `children` - Child elements to render
118/// * `initial_theme` - Optional initial theme (defaults to light theme, or loaded from localStorage if persistence enabled)
119/// * `persist_theme` - Whether to save/load theme from localStorage (default: false)
120///
121/// # Example
122/// ```rust,ignore
123/// use dioxus_ui_system::theme::{ThemeProvider, ThemeTokens};
124///
125/// fn App() -> Element {
126///     rsx! {
127///         // With persistence
128///         ThemeProvider { persist_theme: true,
129///             Home {}
130///         }
131///         
132///         // Without persistence (default)
133///         ThemeProvider {
134///             Home {}
135///         }
136///     }
137/// }
138/// ```
139#[component]
140pub fn ThemeProvider(
141    children: Element,
142    #[props(default)] initial_theme: Option<ThemeTokens>,
143    #[props(default = false)] persist_theme: bool,
144) -> Element {
145    // Determine initial theme: explicit prop > localStorage (if enabled) > default light
146    let initial = if persist_theme {
147        initial_theme
148            .or_else(load_theme_from_storage)
149            .unwrap_or_else(ThemeTokens::light)
150    } else {
151        initial_theme.unwrap_or_else(ThemeTokens::light)
152    };
153
154    let mut tokens = use_signal(|| initial);
155    let persist = use_signal(|| persist_theme);
156
157    let set_theme = Callback::new(move |new_theme: ThemeTokens| {
158        // Save theme name to localStorage only if persistence is enabled
159        if persist() {
160            let theme_name = match &new_theme.mode {
161                super::tokens::ThemeMode::Light => "light",
162                super::tokens::ThemeMode::Dark => "dark",
163                super::tokens::ThemeMode::Brand(name) => name.as_str(),
164            };
165            save_theme_to_storage(theme_name);
166        }
167        tokens.set(new_theme);
168    });
169
170    let toggle_mode = Callback::new(move |()| {
171        tokens.with_mut(|current| {
172            let new_theme = match current.mode {
173                super::tokens::ThemeMode::Light => ThemeTokens::dark(),
174                super::tokens::ThemeMode::Dark => ThemeTokens::light(),
175                super::tokens::ThemeMode::Brand(_) => ThemeTokens::light(),
176            };
177            // Save to localStorage only if persistence is enabled
178            if persist() {
179                let theme_name = match &new_theme.mode {
180                    super::tokens::ThemeMode::Light => "light",
181                    super::tokens::ThemeMode::Dark => "dark",
182                    super::tokens::ThemeMode::Brand(name) => name.as_str(),
183                };
184                save_theme_to_storage(theme_name);
185            }
186            *current = new_theme;
187        });
188    });
189
190    let set_theme_by_name = Callback::new(move |name: String| {
191        if let Some(new_theme) = ThemeTokens::by_name(&name) {
192            if persist() {
193                save_theme_to_storage(&name);
194            }
195            tokens.set(new_theme);
196        }
197    });
198
199    use_context_provider(|| ThemeContext {
200        tokens,
201        set_theme,
202        toggle_mode,
203        set_theme_by_name,
204    });
205
206    rsx! { {children} }
207}
208
209/// Theme toggle button component
210///
211/// A convenience component that toggles between light and dark modes
212#[component]
213pub fn ThemeToggle() -> Element {
214    let theme = use_theme();
215    let mode = use_style(|t| t.mode.clone());
216
217    let button_text = match mode() {
218        super::tokens::ThemeMode::Light => "🌙",
219        super::tokens::ThemeMode::Dark => "☀️",
220        super::tokens::ThemeMode::Brand(_) => "🎨",
221    };
222
223    rsx! {
224        button {
225            onclick: move |_| theme.toggle_mode.call(()),
226            "{button_text}"
227        }
228    }
229}
230
231/// Theme selector dropdown component
232///
233/// Allows users to select from all available preset themes
234#[component]
235pub fn ThemeSelector() -> Element {
236    let theme = use_theme();
237    let mut is_open = use_signal(|| false);
238    let current_mode = use_style(|t| t.mode.clone());
239
240    let presets = ThemeTokens::presets();
241
242    let current_name = match current_mode() {
243        super::tokens::ThemeMode::Light => "Light",
244        super::tokens::ThemeMode::Dark => "Dark",
245        super::tokens::ThemeMode::Brand(name) => match name.as_str() {
246            "rose" => "Rose",
247            "blue" => "Blue",
248            "green" => "Green",
249            "violet" => "Violet",
250            "orange" => "Orange",
251            _ => "Custom",
252        },
253    };
254
255    rsx! {
256        div {
257            style: "position: relative; display: inline-block;",
258
259            // Trigger button
260            button {
261                style: "display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-radius: 6px; border: 1px solid #e2e8f0; background: white; cursor: pointer;",
262                onclick: move |_| is_open.toggle(),
263
264                span { "🎨" }
265                span { "{current_name}" }
266                span { "▼" }
267            }
268
269            // Dropdown
270            if is_open() {
271                div {
272                    style: "position: absolute; top: calc(100% + 4px); right: 0; min-width: 150px; background: white; border-radius: 8px; border: 1px solid #e2e8f0; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); z-index: 100;",
273
274                    for (name, _) in presets {
275                        button {
276                            style: "display: block; width: 100%; padding: 8px 12px; text-align: left; background: none; border: none; cursor: pointer; border-radius: 6px; margin: 2px;",
277                            style: if current_name.to_lowercase() == name { "background: #f1f5f9;" } else { "" },
278                            onclick: move |_| {
279                                theme.set_theme_by_name.call(name.to_string());
280                                is_open.set(false);
281                            },
282                            "{name.chars().next().unwrap().to_uppercase()}{&name[1..]}"
283                        }
284                    }
285                }
286            }
287        }
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::super::tokens::ThemeMode;
294    use super::*;
295
296    #[test]
297    fn test_theme_context_creation() {
298        // Note: This is a basic test - full testing requires dioxus testing utilities
299        let tokens = ThemeTokens::light();
300        assert!(matches!(tokens.mode, ThemeMode::Light));
301    }
302}