dioxus_ui_system/theme/
context.rs1use super::tokens::ThemeTokens;
7use dioxus::prelude::*;
8
9#[derive(Clone)]
11pub struct ThemeContext {
12 pub tokens: Signal<ThemeTokens>,
14 pub set_theme: Callback<ThemeTokens>,
16 pub toggle_mode: Callback<()>,
18 pub set_theme_by_name: Callback<String>,
20}
21
22impl ThemeContext {
23 pub fn current(&self) -> ThemeTokens {
25 self.tokens.read().clone()
26 }
27}
28
29pub fn use_theme() -> ThemeContext {
48 use_context::<ThemeContext>()
49}
50
51pub 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#[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#[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#[component]
140pub fn ThemeProvider(
141 children: Element,
142 #[props(default)] initial_theme: Option<ThemeTokens>,
143 #[props(default = false)] persist_theme: bool,
144) -> Element {
145 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 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 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#[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#[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 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 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 let tokens = ThemeTokens::light();
300 assert!(matches!(tokens.mode, ThemeMode::Light));
301 }
302}