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-core/src/theme.rs
StratoSDK / crates / strato-core / src / theme.rs
1//! Theming system for StratoUI
2//!
3//! Provides comprehensive theming support including dark/light modes,
4//! custom color schemes, typography, spacing, and component styling
5 
6use parking_lot::RwLock;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::sync::Arc;
10 
11/// Color representation with alpha channel
12#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
13pub struct Color {
14 pub r: f32,
15 pub g: f32,
16 pub b: f32,
17 pub a: f32,
18}
19 
20impl Color {
21 /// Create a new color from RGB values (0.0 - 1.0)
22 pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
23 Self { r, g, b, a: 1.0 }
24 }
25 
26 /// Create a new color from RGBA values (0.0 - 1.0)
27 pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
28 Self { r, g, b, a }
29 }
30 
31 /// Create a color from hex string (#RRGGBB or #RRGGBBAA)
32 pub fn from_hex(hex: &str) -> Result<Self, &'static str> {
33 let hex = hex.trim_start_matches('#');
34 
35 match hex.len() {
36 6 => {
37 let r = u8::from_str_radix(&hex[0..2], 16).map_err(|_| "Invalid hex color")?;
38 let g = u8::from_str_radix(&hex[2..4], 16).map_err(|_| "Invalid hex color")?;
39 let b = u8::from_str_radix(&hex[4..6], 16).map_err(|_| "Invalid hex color")?;
40 Ok(Self::rgb(
41 r as f32 / 255.0,
42 g as f32 / 255.0,
43 b as f32 / 255.0,
44 ))
45 }
46 8 => {
47 let r = u8::from_str_radix(&hex[0..2], 16).map_err(|_| "Invalid hex color")?;
48 let g = u8::from_str_radix(&hex[2..4], 16).map_err(|_| "Invalid hex color")?;
49 let b = u8::from_str_radix(&hex[4..6], 16).map_err(|_| "Invalid hex color")?;
50 let a = u8::from_str_radix(&hex[6..8], 16).map_err(|_| "Invalid hex color")?;
51 Ok(Self::rgba(
52 r as f32 / 255.0,
53 g as f32 / 255.0,
54 b as f32 / 255.0,
55 a as f32 / 255.0,
56 ))
57 }
58 _ => Err("Invalid hex color length"),
59 }
60 }
61 
62 /// Convert to hex string
63 pub fn to_hex(&self) -> String {
64 if self.a < 1.0 {
65 format!(
66 "#{:02X}{:02X}{:02X}{:02X}",
67 (self.r * 255.0).round() as u8,
68 (self.g * 255.0).round() as u8,
69 (self.b * 255.0).round() as u8,
70 (self.a * 255.0).round() as u8
71 )
72 } else {
73 format!(
74 "#{:02X}{:02X}{:02X}",
75 (self.r * 255.0).round() as u8,
76 (self.g * 255.0).round() as u8,
77 (self.b * 255.0).round() as u8
78 )
79 }
80 }
81 
82 /// Lighten the color by a factor (0.0 - 1.0)
83 pub fn lighten(&self, factor: f32) -> Self {
84 Self {
85 r: (self.r + (1.0 - self.r) * factor).min(1.0),
86 g: (self.g + (1.0 - self.g) * factor).min(1.0),
87 b: (self.b + (1.0 - self.b) * factor).min(1.0),
88 a: self.a,
89 }
90 }
91 
92 /// Darken the color by a factor (0.0 - 1.0)
93 pub fn darken(&self, factor: f32) -> Self {
94 Self {
95 r: (self.r * (1.0 - factor)).max(0.0),
96 g: (self.g * (1.0 - factor)).max(0.0),
97 b: (self.b * (1.0 - factor)).max(0.0),
98 a: self.a,
99 }
100 }
101 
102 /// Set alpha channel
103 pub fn with_alpha(&self, alpha: f32) -> Self {
104 Self {
105 r: self.r,
106 g: self.g,
107 b: self.b,
108 a: alpha.clamp(0.0, 1.0),
109 }
110 }
111 
112 /// Convert to array format [r, g, b, a] for renderer compatibility
113 pub fn to_array(&self) -> [f32; 4] {
114 [self.r, self.g, self.b, self.a]
115 }
116 
117 /// Mix with another color
118 pub fn mix(&self, other: &Color, factor: f32) -> Self {
119 let factor = factor.clamp(0.0, 1.0);
120 Self {
121 r: self.r + (other.r - self.r) * factor,
122 g: self.g + (other.g - self.g) * factor,
123 b: self.b + (other.b - self.b) * factor,
124 a: self.a + (other.a - self.a) * factor,
125 }
126 }
127 
128 /// Convert to types::Color for rendering
129 pub fn to_types_color(&self) -> crate::types::Color {
130 crate::types::Color {
131 r: self.r,
132 g: self.g,
133 b: self.b,
134 a: self.a,
135 }
136 }
137}
138 
139/// Common color constants
140impl Color {
141 pub const TRANSPARENT: Color = Color::rgba(0.0, 0.0, 0.0, 0.0);
142 pub const BLACK: Color = Color::rgb(0.0, 0.0, 0.0);
143 pub const WHITE: Color = Color::rgb(1.0, 1.0, 1.0);
144 pub const RED: Color = Color::rgb(1.0, 0.0, 0.0);
145 pub const GREEN: Color = Color::rgb(0.0, 1.0, 0.0);
146 pub const BLUE: Color = Color::rgb(0.0, 0.0, 1.0);
147 pub const YELLOW: Color = Color::rgb(1.0, 1.0, 0.0);
148 pub const CYAN: Color = Color::rgb(0.0, 1.0, 1.0);
149 pub const MAGENTA: Color = Color::rgb(1.0, 0.0, 1.0);
150 pub const GRAY: Color = Color::rgb(0.5, 0.5, 0.5);
151}
152 
153/// Typography configuration
154#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct Typography {
156 /// Font family
157 pub font_family: String,
158 /// Base font size in pixels
159 pub base_size: f32,
160 /// Line height multiplier
161 pub line_height: f32,
162 /// Font weight
163 pub weight: FontWeight,
164 /// Letter spacing
165 pub letter_spacing: f32,
166}
167 
168impl Default for Typography {
169 fn default() -> Self {
170 // Use platform-specific default fonts with proper fallbacks
171 #[cfg(target_os = "windows")]
172 let font_family = "Segoe UI, Tahoma, Arial, sans-serif";
173 
174 #[cfg(target_os = "macos")]
175 let font_family = "SF Pro Display, Helvetica Neue, Arial, sans-serif";
176 
177 #[cfg(target_os = "linux")]
178 let font_family = "Ubuntu, DejaVu Sans, Liberation Sans, Arial, sans-serif";
179 
180 #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
181 let font_family = "Arial, sans-serif";
182 
183 Self {
184 font_family: font_family.to_string(),
185 base_size: 14.0,
186 line_height: 1.5,
187 weight: FontWeight::Normal,
188 letter_spacing: 0.0,
189 }
190 }
191}
192 
193/// Font weight enumeration
194#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
195pub enum FontWeight {
196 Thin = 100,
197 ExtraLight = 200,
198 Light = 300,
199 Normal = 400,
200 Medium = 500,
201 SemiBold = 600,
202 Bold = 700,
203 ExtraBold = 800,
204 Black = 900,
205}
206 
207/// Spacing configuration
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct Spacing {
210 /// Extra small spacing
211 pub xs: f32,
212 /// Small spacing
213 pub sm: f32,
214 /// Medium spacing
215 pub md: f32,
216 /// Large spacing
217 pub lg: f32,
218 /// Extra large spacing
219 pub xl: f32,
220 /// Extra extra large spacing
221 pub xxl: f32,
222}
223 
224impl Default for Spacing {
225 fn default() -> Self {
226 Self {
227 xs: 4.0,
228 sm: 8.0,
229 md: 16.0,
230 lg: 24.0,
231 xl: 32.0,
232 xxl: 48.0,
233 }
234 }
235}
236 
237/// Border radius configuration
238#[derive(Debug, Clone, Serialize, Deserialize)]
239pub struct BorderRadius {
240 /// No radius
241 pub none: f32,
242 /// Small radius
243 pub sm: f32,
244 /// Medium radius
245 pub md: f32,
246 /// Large radius
247 pub lg: f32,
248 /// Full radius (circular)
249 pub full: f32,
250}
251 
252impl Default for BorderRadius {
253 fn default() -> Self {
254 Self {
255 none: 0.0,
256 sm: 4.0,
257 md: 8.0,
258 lg: 12.0,
259 full: 9999.0,
260 }
261 }
262}
263 
264/// Shadow configuration
265#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct Shadow {
267 pub color: Color,
268 pub offset_x: f32,
269 pub offset_y: f32,
270 pub blur_radius: f32,
271 pub spread_radius: f32,
272}
273 
274impl Shadow {
275 pub fn new(color: Color, offset_x: f32, offset_y: f32, blur_radius: f32) -> Self {
276 Self {
277 color,
278 offset_x,
279 offset_y,
280 blur_radius,
281 spread_radius: 0.0,
282 }
283 }
284}
285 
286/// Elevation shadows
287#[derive(Debug, Clone, Serialize, Deserialize)]
288pub struct Elevation {
289 pub none: Option<Shadow>,
290 pub sm: Shadow,
291 pub md: Shadow,
292 pub lg: Shadow,
293 pub xl: Shadow,
294}
295 
296impl Default for Elevation {
297 fn default() -> Self {
298 Self {
299 none: None,
300 sm: Shadow::new(Color::rgba(0.0, 0.0, 0.0, 0.1), 0.0, 1.0, 3.0),
301 md: Shadow::new(Color::rgba(0.0, 0.0, 0.0, 0.15), 0.0, 4.0, 6.0),
302 lg: Shadow::new(Color::rgba(0.0, 0.0, 0.0, 0.2), 0.0, 10.0, 15.0),
303 xl: Shadow::new(Color::rgba(0.0, 0.0, 0.0, 0.25), 0.0, 20.0, 25.0),
304 }
305 }
306}
307 
308/// Color palette for a theme
309#[derive(Debug, Clone, Serialize, Deserialize)]
310pub struct ColorPalette {
311 // Primary colors
312 pub primary: Color,
313 pub primary_variant: Color,
314 
315 // Secondary colors
316 pub secondary: Color,
317 pub secondary_variant: Color,
318 
319 // Background colors
320 pub background: Color,
321 pub surface: Color,
322 pub surface_variant: Color,
323 
324 // Text colors
325 pub on_primary: Color,
326 pub on_secondary: Color,
327 pub on_background: Color,
328 pub on_surface: Color,
329 
330 // State colors
331 pub error: Color,
332 pub on_error: Color,
333 pub warning: Color,
334 pub on_warning: Color,
335 pub success: Color,
336 pub on_success: Color,
337 pub info: Color,
338 pub on_info: Color,
339 
340 // Outline and divider
341 pub outline: Color,
342 pub outline_variant: Color,
343 pub divider: Color,
344 
345 // Disabled state
346 pub disabled: Color,
347 pub on_disabled: Color,
348}
349 
350impl ColorPalette {
351 /// Create a light theme color palette
352 pub fn light() -> Self {
353 Self {
354 primary: Color::from_hex("#6366F1").unwrap(),
355 primary_variant: Color::from_hex("#4F46E5").unwrap(),
356 secondary: Color::from_hex("#EC4899").unwrap(),
357 secondary_variant: Color::from_hex("#DB2777").unwrap(),
358 background: Color::WHITE,
359 surface: Color::from_hex("#F9FAFB").unwrap(),
360 surface_variant: Color::from_hex("#F3F4F6").unwrap(),
361 on_primary: Color::WHITE,
362 on_secondary: Color::WHITE,
363 on_background: Color::from_hex("#111827").unwrap(),
364 on_surface: Color::from_hex("#FFFF00").unwrap(), // Bright yellow for visibility
365 error: Color::from_hex("#EF4444").unwrap(),
366 on_error: Color::WHITE,
367 warning: Color::from_hex("#F59E0B").unwrap(),
368 on_warning: Color::WHITE,
369 success: Color::from_hex("#10B981").unwrap(),
370 on_success: Color::WHITE,
371 info: Color::from_hex("#3B82F6").unwrap(),
372 on_info: Color::WHITE,
373 outline: Color::from_hex("#D1D5DB").unwrap(),
374 outline_variant: Color::from_hex("#E5E7EB").unwrap(),
375 divider: Color::from_hex("#F3F4F6").unwrap(),
376 disabled: Color::from_hex("#9CA3AF").unwrap(),
377 on_disabled: Color::from_hex("#6B7280").unwrap(),
378 }
379 }
380 
381 /// Create a dark theme color palette
382 pub fn dark() -> Self {
383 Self {
384 primary: Color::from_hex("#818CF8").unwrap(),
385 primary_variant: Color::from_hex("#6366F1").unwrap(),
386 secondary: Color::from_hex("#F472B6").unwrap(),
387 secondary_variant: Color::from_hex("#EC4899").unwrap(),
388 background: Color::from_hex("#111827").unwrap(),
389 surface: Color::from_hex("#1F2937").unwrap(),
390 surface_variant: Color::from_hex("#374151").unwrap(),
391 on_primary: Color::from_hex("#1E1B4B").unwrap(),
392 on_secondary: Color::from_hex("#831843").unwrap(),
393 on_background: Color::from_hex("#F9FAFB").unwrap(),
394 on_surface: Color::from_hex("#00FFFF").unwrap(), // Bright cyan for visibility
395 error: Color::from_hex("#F87171").unwrap(),
396 on_error: Color::from_hex("#7F1D1D").unwrap(),
397 warning: Color::from_hex("#FBBF24").unwrap(),
398 on_warning: Color::from_hex("#78350F").unwrap(),
399 success: Color::from_hex("#34D399").unwrap(),
400 on_success: Color::from_hex("#064E3B").unwrap(),
401 info: Color::from_hex("#60A5FA").unwrap(),
402 on_info: Color::from_hex("#1E3A8A").unwrap(),
403 outline: Color::from_hex("#4B5563").unwrap(),
404 outline_variant: Color::from_hex("#374151").unwrap(),
405 divider: Color::from_hex("#374151").unwrap(),
406 disabled: Color::from_hex("#6B7280").unwrap(),
407 on_disabled: Color::from_hex("#9CA3AF").unwrap(),
408 }
409 }
410}
411 
412/// Theme mode enumeration
413#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
414pub enum ThemeMode {
415 Light,
416 Dark,
417 System,
418}
419 
420/// Complete theme configuration
421#[derive(Debug, Clone, Serialize, Deserialize)]
422pub struct Theme {
423 /// Theme name
424 pub name: String,
425 /// Theme mode
426 pub mode: ThemeMode,
427 /// Color palette
428 pub colors: ColorPalette,
429 /// Typography settings
430 pub typography: Typography,
431 /// Spacing configuration
432 pub spacing: Spacing,
433 /// Border radius configuration
434 pub border_radius: BorderRadius,
435 /// Elevation shadows
436 pub elevation: Elevation,
437 /// Custom properties
438 pub custom: HashMap<String, String>,
439}
440 
441impl Theme {
442 /// Create a new light theme
443 pub fn light() -> Self {
444 Self {
445 name: "Light".to_string(),
446 mode: ThemeMode::Light,
447 colors: ColorPalette::light(),
448 typography: Typography::default(),
449 spacing: Spacing::default(),
450 border_radius: BorderRadius::default(),
451 elevation: Elevation::default(),
452 custom: HashMap::new(),
453 }
454 }
455 
456 /// Create a new dark theme
457 pub fn dark() -> Self {
458 Self {
459 name: "Dark".to_string(),
460 mode: ThemeMode::Dark,
461 colors: ColorPalette::dark(),
462 typography: Typography::default(),
463 spacing: Spacing::default(),
464 border_radius: BorderRadius::default(),
465 elevation: Elevation::default(),
466 custom: HashMap::new(),
467 }
468 }
469 
470 /// Get a custom property
471 pub fn get_custom(&self, key: &str) -> Option<&String> {
472 self.custom.get(key)
473 }
474 
475 /// Set a custom property
476 pub fn set_custom(&mut self, key: String, value: String) {
477 self.custom.insert(key, value);
478 }
479}
480 
481/// Theme change event
482#[derive(Debug, Clone)]
483pub struct ThemeChangeEvent {
484 pub old_theme: String,
485 pub new_theme: String,
486 pub mode_changed: bool,
487}
488 
489/// Theme change listener
490pub trait ThemeChangeListener: Send + Sync {
491 fn on_theme_changed(&self, event: &ThemeChangeEvent);
492}
493 
494/// Theme manager for handling theme switching and persistence
495pub struct ThemeManager {
496 current_theme: Arc<RwLock<Theme>>,
497 themes: Arc<RwLock<HashMap<String, Theme>>>,
498 listeners: Arc<RwLock<Vec<Arc<dyn ThemeChangeListener>>>>,
499 system_theme_mode: Arc<RwLock<ThemeMode>>,
500}
501 
502impl ThemeManager {
503 /// Create a new theme manager
504 pub fn new() -> Self {
505 let mut themes = HashMap::new();
506 let light_theme = Theme::light();
507 let dark_theme = Theme::dark();
508 
509 themes.insert(light_theme.name.clone(), light_theme.clone());
510 themes.insert(dark_theme.name.clone(), dark_theme.clone());
511 
512 Self {
513 current_theme: Arc::new(RwLock::new(light_theme)),
514 themes: Arc::new(RwLock::new(themes)),
515 listeners: Arc::new(RwLock::new(Vec::new())),
516 system_theme_mode: Arc::new(RwLock::new(ThemeMode::Light)),
517 }
518 }
519 
520 /// Get the current theme
521 pub fn current_theme(&self) -> Theme {
522 self.current_theme.read().clone()
523 }
524 
525 /// Set the current theme by name
526 pub fn set_theme(&self, theme_name: &str) -> Result<(), &'static str> {
527 let themes = self.themes.read();
528 let new_theme = themes.get(theme_name).ok_or("Theme not found")?.clone();
529 
530 let old_theme_name = self.current_theme.read().name.clone();
531 let mode_changed = self.current_theme.read().mode != new_theme.mode;
532 
533 *self.current_theme.write() = new_theme;
534 
535 // Notify listeners
536 let event = ThemeChangeEvent {
537 old_theme: old_theme_name,
538 new_theme: theme_name.to_string(),
539 mode_changed,
540 };
541 
542 self.notify_listeners(&event);
543 Ok(())
544 }
545 
546 /// Switch between light and dark themes
547 pub fn toggle_theme_mode(&self) {
548 let current_mode = self.current_theme.read().mode;
549 let new_mode = match current_mode {
550 ThemeMode::Light => ThemeMode::Dark,
551 ThemeMode::Dark => ThemeMode::Light,
552 ThemeMode::System => {
553 // Toggle based on current system theme
554 let system_mode = *self.system_theme_mode.read();
555 match system_mode {
556 ThemeMode::Light => ThemeMode::Dark,
557 _ => ThemeMode::Light,
558 }
559 }
560 };
561 
562 let theme_name = match new_mode {
563 ThemeMode::Light => "Light",
564 ThemeMode::Dark => "Dark",
565 ThemeMode::System => "Light", // Fallback
566 };
567 
568 let _ = self.set_theme(theme_name);
569 }
570 
571 /// Add a custom theme
572 pub fn add_theme(&self, theme: Theme) {
573 let theme_name = theme.name.clone();
574 self.themes.write().insert(theme_name, theme);
575 }
576 
577 /// Remove a theme
578 pub fn remove_theme(&self, theme_name: &str) -> Result<(), &'static str> {
579 if theme_name == "Light" || theme_name == "Dark" {
580 return Err("Cannot remove built-in themes");
581 }
582 
583 let mut themes = self.themes.write();
584 themes.remove(theme_name).ok_or("Theme not found")?;
585 
586 Ok(())
587 }
588 
589 /// Get all available theme names
590 pub fn get_theme_names(&self) -> Vec<String> {
591 self.themes.read().keys().cloned().collect()
592 }
593 
594 /// Add a theme change listener
595 pub fn add_listener(&self, listener: Arc<dyn ThemeChangeListener>) {
596 self.listeners.write().push(listener);
597 }
598 
599 /// Remove all listeners
600 pub fn clear_listeners(&self) {
601 self.listeners.write().clear();
602 }
603 
604 /// Update system theme mode (for system theme detection)
605 pub fn update_system_theme_mode(&self, mode: ThemeMode) {
606 *self.system_theme_mode.write() = mode;
607 
608 // If current theme is system, update accordingly
609 if self.current_theme.read().mode == ThemeMode::System {
610 let theme_name = match mode {
611 ThemeMode::Light => "Light",
612 ThemeMode::Dark => "Dark",
613 ThemeMode::System => "Light", // Fallback
614 };
615 let _ = self.set_theme(theme_name);
616 }
617 }
618 
619 /// Notify all listeners of theme change
620 fn notify_listeners(&self, event: &ThemeChangeEvent) {
621 let listeners = self.listeners.read();
622 for listener in listeners.iter() {
623 listener.on_theme_changed(event);
624 }
625 }
626 
627 /// Export theme to JSON
628 pub fn export_theme(&self, theme_name: &str) -> Result<String, Box<dyn std::error::Error>> {
629 let themes = self.themes.read();
630 let theme = themes.get(theme_name).ok_or("Theme not found")?;
631 
632 Ok(serde_json::to_string_pretty(theme)?)
633 }
634 
635 /// Import theme from JSON
636 pub fn import_theme(&self, json: &str) -> Result<(), Box<dyn std::error::Error>> {
637 let theme: Theme = serde_json::from_str(json)?;
638 self.add_theme(theme);
639 Ok(())
640 }
641}
642 
643impl Default for ThemeManager {
644 fn default() -> Self {
645 Self::new()
646 }
647}
648 
649/// Utility functions for theme operations
650pub mod utils {
651 use super::*;
652 
653 /// Detect system theme preference (placeholder - would need platform-specific implementation)
654 pub fn detect_system_theme() -> ThemeMode {
655 // This would need platform-specific implementation
656 // For now, return Light as default
657 ThemeMode::Light
658 }
659 
660 /// Generate a color palette from a primary color
661 pub fn generate_palette_from_primary(primary: Color) -> ColorPalette {
662 let mut palette = ColorPalette::light();
663 palette.primary = primary;
664 palette.primary_variant = primary.darken(0.1);
665 palette
666 }
667 
668 /// Calculate contrast ratio between two colors
669 pub fn contrast_ratio(color1: &Color, color2: &Color) -> f32 {
670 let l1 = relative_luminance(color1);
671 let l2 = relative_luminance(color2);
672 
673 let lighter = l1.max(l2);
674 let darker = l1.min(l2);
675 
676 (lighter + 0.05) / (darker + 0.05)
677 }
678 
679 /// Calculate relative luminance of a color
680 fn relative_luminance(color: &Color) -> f32 {
681 let r = if color.r <= 0.03928 {
682 color.r / 12.92
683 } else {
684 ((color.r + 0.055) / 1.055).powf(2.4)
685 };
686 let g = if color.g <= 0.03928 {
687 color.g / 12.92
688 } else {
689 ((color.g + 0.055) / 1.055).powf(2.4)
690 };
691 let b = if color.b <= 0.03928 {
692 color.b / 12.92
693 } else {
694 ((color.b + 0.055) / 1.055).powf(2.4)
695 };
696 
697 0.2126 * r + 0.7152 * g + 0.0722 * b
698 }
699 
700 /// Check if a color combination meets WCAG accessibility standards
701 pub fn meets_wcag_aa(foreground: &Color, background: &Color) -> bool {
702 contrast_ratio(foreground, background) >= 4.5
703 }
704 
705 /// Check if a color combination meets WCAG AAA accessibility standards
706 pub fn meets_wcag_aaa(foreground: &Color, background: &Color) -> bool {
707 contrast_ratio(foreground, background) >= 7.0
708 }
709}
710 
711#[cfg(test)]
712mod tests {
713 use super::*;
714 
715 #[test]
716 fn test_color_creation() {
717 let color = Color::rgb(1.0, 0.5, 0.0);
718 assert_eq!(color.r, 1.0);
719 assert_eq!(color.g, 0.5);
720 assert_eq!(color.b, 0.0);
721 assert_eq!(color.a, 1.0);
722 }
723 
724 #[test]
725 fn test_color_from_hex() {
726 let color = Color::from_hex("#FF8000").unwrap();
727 assert!((color.r - 1.0).abs() < 0.01);
728 assert!((color.g - 0.5).abs() < 0.01);
729 assert!((color.b - 0.0).abs() < 0.01);
730 }
731 
732 #[test]
733 fn test_color_to_hex() {
734 let color = Color::rgb(1.0, 0.5, 0.0);
735 assert_eq!(color.to_hex(), "#FF8000");
736 }
737 
738 #[test]
739 fn test_color_lighten() {
740 let color = Color::rgb(0.5, 0.5, 0.5);
741 let lighter = color.lighten(0.2);
742 assert!(lighter.r > color.r);
743 assert!(lighter.g > color.g);
744 assert!(lighter.b > color.b);
745 }
746 
747 #[test]
748 fn test_color_darken() {
749 let color = Color::rgb(0.5, 0.5, 0.5);
750 let darker = color.darken(0.2);
751 assert!(darker.r < color.r);
752 assert!(darker.g < color.g);
753 assert!(darker.b < color.b);
754 }
755 
756 #[test]
757 fn test_theme_creation() {
758 let theme = Theme::light();
759 assert_eq!(theme.name, "Light");
760 assert_eq!(theme.mode, ThemeMode::Light);
761 }
762 
763 #[test]
764 fn test_theme_manager() {
765 let manager = ThemeManager::new();
766 assert_eq!(manager.current_theme().name, "Light");
767 
768 manager.set_theme("Dark").unwrap();
769 assert_eq!(manager.current_theme().name, "Dark");
770 }
771 
772 #[test]
773 fn test_contrast_ratio() {
774 let white = Color::WHITE;
775 let black = Color::BLACK;
776 let ratio = utils::contrast_ratio(&white, &black);
777 assert!(ratio > 20.0); // Should be 21:1 for perfect white/black
778 }
779 
780 #[test]
781 fn test_wcag_compliance() {
782 let white = Color::WHITE;
783 let black = Color::BLACK;
784 assert!(utils::meets_wcag_aa(&white, &black));
785 assert!(utils::meets_wcag_aaa(&white, &black));
786 }
787}
788