Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-widgets/src/theme.rs
1//! Theming system for StratoUI
2 
3use strato_core::types::{Color, Shadow};
4// Removed unused strato_core::layout::EdgeInsets import
5use std::collections::HashMap;
6 
7/// Theme configuration
8#[derive(Debug, Clone)]
9pub struct Theme {
10 pub name: String,
11 pub colors: ColorPalette,
12 pub typography: Typography,
13 pub spacing: SpacingScale,
14 pub borders: BorderStyles,
15 pub shadows: ShadowStyles,
16 pub components: ComponentThemes,
17}
18 
19impl Theme {
20 /// Create a light theme
21 pub fn light() -> Self {
22 Self {
23 name: "Light".to_string(),
24 colors: ColorPalette::light(),
25 typography: Typography::default(),
26 spacing: SpacingScale::default(),
27 borders: BorderStyles::default(),
28 shadows: ShadowStyles::default(),
29 components: ComponentThemes::light(),
30 }
31 }
32 
33 /// Create a dark theme
34 pub fn dark() -> Self {
35 Self {
36 name: "Dark".to_string(),
37 colors: ColorPalette::dark(),
38 typography: Typography::default(),
39 spacing: SpacingScale::default(),
40 borders: BorderStyles::default(),
41 shadows: ShadowStyles::default(),
42 components: ComponentThemes::dark(),
43 }
44 }
45 
46 /// Create a high contrast theme
47 pub fn high_contrast() -> Self {
48 Self {
49 name: "High Contrast".to_string(),
50 colors: ColorPalette::high_contrast(),
51 typography: Typography::default(),
52 spacing: SpacingScale::default(),
53 borders: BorderStyles::default(), // Borders might need thickening?
54 shadows: ShadowStyles::default(), // Shadows less visible on black
55 components: ComponentThemes::default(), // Recycle light components for now
56 }
57 }
58 
59 /// Create a custom theme
60 pub fn custom(name: impl Into<String>) -> Self {
61 Self {
62 name: name.into(),
63 colors: ColorPalette::default(),
64 typography: Typography::default(),
65 spacing: SpacingScale::default(),
66 borders: BorderStyles::default(),
67 shadows: ShadowStyles::default(),
68 components: ComponentThemes::default(),
69 }
70 }
71}
72 
73impl Default for Theme {
74 fn default() -> Self {
75 Self::light()
76 }
77}
78 
79/// Color palette
80#[derive(Debug, Clone)]
81pub struct ColorPalette {
82 pub primary: Color,
83 pub secondary: Color,
84 pub success: Color,
85 pub warning: Color,
86 pub error: Color,
87 pub info: Color,
88 pub background: Color,
89 pub surface: Color,
90 pub text_primary: Color,
91 pub text_secondary: Color,
92 pub text_disabled: Color,
93 pub divider: Color,
94}
95 
96impl ColorPalette {
97 /// Light color palette
98 pub fn light() -> Self {
99 Self {
100 primary: Color::rgb(0.129, 0.588, 0.953),
101 secondary: Color::rgb(0.0, 0.737, 0.831),
102 success: Color::rgb(0.298, 0.686, 0.314),
103 warning: Color::rgb(1.0, 0.757, 0.027),
104 error: Color::rgb(0.956, 0.263, 0.212),
105 info: Color::rgb(0.012, 0.663, 0.957),
106 background: Color::rgb(0.98, 0.98, 0.98),
107 surface: Color::WHITE,
108 text_primary: Color::rgba(0.0, 0.0, 0.0, 0.87),
109 text_secondary: Color::rgba(0.0, 0.0, 0.0, 0.60),
110 text_disabled: Color::rgba(0.0, 0.0, 0.0, 0.38),
111 divider: Color::rgba(0.0, 0.0, 0.0, 0.12),
112 }
113 }
114 
115 /// Dark color palette
116 pub fn dark() -> Self {
117 Self {
118 primary: Color::rgb(0.353, 0.706, 1.0),
119 secondary: Color::rgb(0.0, 0.878, 0.922),
120 success: Color::rgb(0.463, 0.733, 0.463),
121 warning: Color::rgb(1.0, 0.835, 0.306),
122 error: Color::rgb(0.961, 0.498, 0.478),
123 info: Color::rgb(0.294, 0.745, 0.969),
124 background: Color::rgb(0.071, 0.071, 0.071),
125 surface: Color::rgb(0.118, 0.118, 0.118),
126 text_primary: Color::rgba(1.0, 1.0, 1.0, 0.87),
127 text_secondary: Color::rgba(1.0, 1.0, 1.0, 0.60),
128 text_disabled: Color::rgba(1.0, 1.0, 1.0, 0.38),
129 divider: Color::rgba(1.0, 1.0, 1.0, 0.12),
130 }
131 }
132 
133 /// High contrast color palette
134 pub fn high_contrast() -> Self {
135 Self {
136 primary: Color::rgb(0.0, 1.0, 1.0), // Cyan
137 secondary: Color::rgb(1.0, 1.0, 0.0), // Yellow
138 success: Color::rgb(0.0, 1.0, 0.0),
139 warning: Color::rgb(1.0, 0.5, 0.0),
140 error: Color::rgb(1.0, 0.0, 0.0),
141 info: Color::rgb(0.0, 1.0, 1.0),
142 background: Color::BLACK,
143 surface: Color::BLACK,
144 text_primary: Color::WHITE,
145 text_secondary: Color::WHITE, // No subtle text in HC
146 text_disabled: Color::rgb(0.7, 0.7, 0.7),
147 divider: Color::WHITE,
148 }
149 }
150}
151 
152impl Default for ColorPalette {
153 fn default() -> Self {
154 Self::light()
155 }
156}
157 
158/// Typography configuration
159#[derive(Debug, Clone)]
160pub struct Typography {
161 pub font_family: String,
162 pub font_family_mono: String,
163 pub h1: TextStyle,
164 pub h2: TextStyle,
165 pub h3: TextStyle,
166 pub h4: TextStyle,
167 pub h5: TextStyle,
168 pub h6: TextStyle,
169 pub body1: TextStyle,
170 pub body2: TextStyle,
171 pub subtitle1: TextStyle,
172 pub subtitle2: TextStyle,
173 pub button: TextStyle,
174 pub caption: TextStyle,
175 pub overline: TextStyle,
176}
177 
178impl Default for Typography {
179 fn default() -> Self {
180 // Use platform-specific default fonts with proper fallbacks
181 #[cfg(target_os = "windows")]
182 let font_family = "Segoe UI, Tahoma, Arial, sans-serif";
183 
184 #[cfg(target_os = "macos")]
185 let font_family = "SF Pro Display, Helvetica Neue, Arial, sans-serif";
186 
187 #[cfg(target_os = "linux")]
188 let font_family = "Ubuntu, DejaVu Sans, Liberation Sans, Arial, sans-serif";
189 
190 #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
191 let font_family = "Arial, sans-serif";
192 
193 Self {
194 font_family: font_family.to_string(),
195 font_family_mono: "Consolas, Monaco, monospace".to_string(),
196 h1: TextStyle {
197 size: 96.0,
198 weight: 300,
199 letter_spacing: -1.5,
200 },
201 h2: TextStyle {
202 size: 60.0,
203 weight: 300,
204 letter_spacing: -0.5,
205 },
206 h3: TextStyle {
207 size: 48.0,
208 weight: 400,
209 letter_spacing: 0.0,
210 },
211 h4: TextStyle {
212 size: 34.0,
213 weight: 400,
214 letter_spacing: 0.25,
215 },
216 h5: TextStyle {
217 size: 24.0,
218 weight: 400,
219 letter_spacing: 0.0,
220 },
221 h6: TextStyle {
222 size: 20.0,
223 weight: 500,
224 letter_spacing: 0.15,
225 },
226 body1: TextStyle {
227 size: 16.0,
228 weight: 400,
229 letter_spacing: 0.5,
230 },
231 body2: TextStyle {
232 size: 14.0,
233 weight: 400,
234 letter_spacing: 0.25,
235 },
236 subtitle1: TextStyle {
237 size: 16.0,
238 weight: 400,
239 letter_spacing: 0.15,
240 },
241 subtitle2: TextStyle {
242 size: 14.0,
243 weight: 500,
244 letter_spacing: 0.1,
245 },
246 button: TextStyle {
247 size: 14.0,
248 weight: 500,
249 letter_spacing: 1.25,
250 },
251 caption: TextStyle {
252 size: 12.0,
253 weight: 400,
254 letter_spacing: 0.4,
255 },
256 overline: TextStyle {
257 size: 10.0,
258 weight: 400,
259 letter_spacing: 1.5,
260 },
261 }
262 }
263}
264 
265/// Text style
266#[derive(Debug, Clone)]
267pub struct TextStyle {
268 pub size: f32,
269 pub weight: u16,
270 pub letter_spacing: f32,
271}
272 
273/// Spacing scale
274#[derive(Debug, Clone)]
275pub struct SpacingScale {
276 pub xs: f32,
277 pub sm: f32,
278 pub md: f32,
279 pub lg: f32,
280 pub xl: f32,
281 pub xxl: f32,
282}
283 
284impl Default for SpacingScale {
285 fn default() -> Self {
286 Self {
287 xs: 4.0,
288 sm: 8.0,
289 md: 16.0,
290 lg: 24.0,
291 xl: 32.0,
292 xxl: 48.0,
293 }
294 }
295}
296 
297/// Border styles
298#[derive(Debug, Clone)]
299pub struct BorderStyles {
300 pub thin: f32,
301 pub medium: f32,
302 pub thick: f32,
303 pub radius_sm: f32,
304 pub radius_md: f32,
305 pub radius_lg: f32,
306 pub radius_round: f32,
307}
308 
309impl Default for BorderStyles {
310 fn default() -> Self {
311 Self {
312 thin: 1.0,
313 medium: 2.0,
314 thick: 4.0,
315 radius_sm: 4.0,
316 radius_md: 8.0,
317 radius_lg: 16.0,
318 radius_round: 999.0,
319 }
320 }
321}
322 
323/// Shadow styles
324#[derive(Debug, Clone)]
325pub struct ShadowStyles {
326 pub sm: Shadow,
327 pub md: Shadow,
328 pub lg: Shadow,
329 pub xl: Shadow,
330}
331 
332impl Default for ShadowStyles {
333 fn default() -> Self {
334 Self {
335 sm: Shadow::new(
336 Color::rgba(0.0, 0.0, 0.0, 0.1),
337 strato_core::types::Point::new(0.0, 1.0),
338 2.0,
339 0.0,
340 ),
341 md: Shadow::new(
342 Color::rgba(0.0, 0.0, 0.0, 0.15),
343 strato_core::types::Point::new(0.0, 2.0),
344 4.0,
345 0.0,
346 ),
347 lg: Shadow::new(
348 Color::rgba(0.0, 0.0, 0.0, 0.2),
349 strato_core::types::Point::new(0.0, 4.0),
350 8.0,
351 0.0,
352 ),
353 xl: Shadow::new(
354 Color::rgba(0.0, 0.0, 0.0, 0.25),
355 strato_core::types::Point::new(0.0, 8.0),
356 16.0,
357 0.0,
358 ),
359 }
360 }
361}
362 
363/// Component-specific themes
364#[derive(Debug, Clone)]
365pub struct ComponentThemes {
366 pub button: HashMap<String, crate::button::ButtonStyle>,
367 pub input: HashMap<String, crate::input::InputStyle>,
368}
369 
370impl ComponentThemes {
371 /// Light component themes
372 pub fn light() -> Self {
373 let mut button = HashMap::new();
374 button.insert("primary".to_string(), crate::button::ButtonStyle::primary());
375 button.insert(
376 "secondary".to_string(),
377 crate::button::ButtonStyle::secondary(),
378 );
379 button.insert("text".to_string(), crate::button::ButtonStyle::ghost());
380 
381 let mut input = HashMap::new();
382 input.insert("outlined".to_string(), crate::input::InputStyle::outlined());
383 input.insert("filled".to_string(), crate::input::InputStyle::filled());
384 
385 Self { button, input }
386 }
387 
388 /// Dark component themes
389 pub fn dark() -> Self {
390 // TODO: Create dark variants
391 Self::light()
392 }
393}
394 
395impl Default for ComponentThemes {
396 fn default() -> Self {
397 Self::light()
398 }
399}
400 
401/// Theme provider for managing themes
402pub struct ThemeProvider {
403 current: Theme,
404 themes: HashMap<String, Theme>,
405}
406 
407impl ThemeProvider {
408 /// Create a new theme provider
409 pub fn new(theme: Theme) -> Self {
410 let mut themes = HashMap::new();
411 themes.insert("light".to_string(), Theme::light());
412 themes.insert("dark".to_string(), Theme::dark());
413 
414 Self {
415 current: theme,
416 themes,
417 }
418 }
419 
420 /// Get the current theme
421 pub fn current(&self) -> &Theme {
422 &self.current
423 }
424 
425 /// Set the current theme
426 pub fn set_theme(&mut self, name: &str) {
427 if let Some(theme) = self.themes.get(name).cloned() {
428 self.current = theme;
429 }
430 }
431 
432 /// Register a custom theme
433 pub fn register_theme(&mut self, name: impl Into<String>, theme: Theme) {
434 self.themes.insert(name.into(), theme);
435 }
436 
437 /// Toggle between light and dark themes
438 pub fn toggle_theme(&mut self) {
439 if self.current.name == "Light" {
440 self.set_theme("dark");
441 } else {
442 self.set_theme("light");
443 }
444 }
445}
446 
447impl Default for ThemeProvider {
448 fn default() -> Self {
449 Self::new(Theme::light())
450 }
451}
452