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 dioxus::prelude::*;
7use super::tokens::ThemeTokens;
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/// Theme provider component
79///
80/// Wraps children with theme context. Must be placed near the root of your app.
81///
82/// # Properties
83/// * `children` - Child elements to render
84/// * `initial_theme` - Optional initial theme (defaults to light theme)
85///
86/// # Example
87/// ```rust,ignore
88/// use dioxus_ui_system::theme::{ThemeProvider, ThemeTokens};
89///
90/// fn App() -> Element {
91///     rsx! {
92///         ThemeProvider {
93///             Home {}
94///         }
95///     }
96/// }
97/// ```
98#[component]
99pub fn ThemeProvider(
100    children: Element,
101    #[props(default)]
102    initial_theme: Option<ThemeTokens>,
103) -> Element {
104    let initial = initial_theme.unwrap_or_else(ThemeTokens::light);
105    let mut tokens = use_signal(|| initial);
106
107    let set_theme = Callback::new(move |new_theme: ThemeTokens| {
108        tokens.set(new_theme);
109    });
110
111    let toggle_mode = Callback::new(move |()| {
112        tokens.with_mut(|current| {
113            let new_theme = match current.mode {
114                super::tokens::ThemeMode::Light => ThemeTokens::dark(),
115                super::tokens::ThemeMode::Dark => ThemeTokens::light(),
116                super::tokens::ThemeMode::Brand(_) => ThemeTokens::light(),
117            };
118            *current = new_theme;
119        });
120    });
121
122    let set_theme_by_name = Callback::new(move |name: String| {
123        if let Some(new_theme) = ThemeTokens::by_name(&name) {
124            tokens.set(new_theme);
125        }
126    });
127
128    use_context_provider(|| ThemeContext {
129        tokens,
130        set_theme,
131        toggle_mode,
132        set_theme_by_name,
133    });
134
135    rsx! { {children} }
136}
137
138/// Theme toggle button component
139///
140/// A convenience component that toggles between light and dark modes
141#[component]
142pub fn ThemeToggle() -> Element {
143    let theme = use_theme();
144    let mode = use_style(|t| t.mode.clone());
145
146    let button_text = match mode() {
147        super::tokens::ThemeMode::Light => "🌙",
148        super::tokens::ThemeMode::Dark => "☀️",
149        super::tokens::ThemeMode::Brand(_) => "🎨",
150    };
151
152    rsx! {
153        button {
154            onclick: move |_| theme.toggle_mode.call(()),
155            "{button_text}"
156        }
157    }
158}
159
160/// Theme selector dropdown component
161///
162/// Allows users to select from all available preset themes
163#[component]
164pub fn ThemeSelector() -> Element {
165    let theme = use_theme();
166    let mut is_open = use_signal(|| false);
167    let current_mode = use_style(|t| t.mode.clone());
168    
169    let presets = ThemeTokens::presets();
170    
171    let current_name = match current_mode() {
172        super::tokens::ThemeMode::Light => "Light",
173        super::tokens::ThemeMode::Dark => "Dark",
174        super::tokens::ThemeMode::Brand(name) => match name.as_str() {
175            "rose" => "Rose",
176            "blue" => "Blue",
177            "green" => "Green",
178            "violet" => "Violet",
179            "orange" => "Orange",
180            _ => "Custom",
181        },
182    };
183
184    rsx! {
185        div {
186            style: "position: relative; display: inline-block;",
187            
188            // Trigger button
189            button {
190                style: "display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-radius: 6px; border: 1px solid #e2e8f0; background: white; cursor: pointer;",
191                onclick: move |_| is_open.toggle(),
192                
193                span { "🎨" }
194                span { "{current_name}" }
195                span { "▼" }
196            }
197            
198            // Dropdown
199            if is_open() {
200                div {
201                    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;",
202                    
203                    for (name, _) in presets {
204                        button {
205                            style: "display: block; width: 100%; padding: 8px 12px; text-align: left; background: none; border: none; cursor: pointer; border-radius: 6px; margin: 2px;",
206                            style: if current_name.to_lowercase() == name { "background: #f1f5f9;" } else { "" },
207                            onclick: move |_| {
208                                theme.set_theme_by_name.call(name.to_string());
209                                is_open.set(false);
210                            },
211                            "{name.chars().next().unwrap().to_uppercase()}{&name[1..]}"
212                        }
213                    }
214                }
215            }
216        }
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223    use super::super::tokens::ThemeMode;
224
225    #[test]
226    fn test_theme_context_creation() {
227        // Note: This is a basic test - full testing requires dioxus testing utilities
228        let tokens = ThemeTokens::light();
229        assert!(matches!(tokens.mode, ThemeMode::Light));
230    }
231}