dioxus_ui_system/theme/
context.rs1use dioxus::prelude::*;
7use super::tokens::ThemeTokens;
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#[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#[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#[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 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 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 let tokens = ThemeTokens::light();
229 assert!(matches!(tokens.mode, ThemeMode::Light));
230 }
231}