1use super::layout::LayoutConstraints;
8use super::style::{Color, StyleProperties};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14pub struct Theme {
15 pub name: String,
16 pub palette: ThemePalette,
17 pub typography: Typography,
18 pub spacing: SpacingScale,
19 pub base_styles: HashMap<String, StyleProperties>,
21 #[serde(default)]
23 pub extends: Option<String>,
24}
25
26impl Theme {
27 pub fn validate(&self, _allow_partial: bool) -> Result<(), String> {
37 let parent_name = self.extends.as_deref();
38
39 self.palette
42 .validate_with_inheritance(&self.name, parent_name)?;
43
44 self.typography
46 .validate_with_inheritance(&self.name, parent_name)?;
47
48 if let Some(ref unit) = self.spacing.unit {
50 if *unit <= 0.0 {
51 return Err(format!(
52 "Theme '{}': spacing unit must be positive, got {}\n\
53 Hint: Use a positive value like unit=\"8\" or unit=\"4\"",
54 self.name, unit
55 ));
56 }
57 }
58
59 for (widget_type, style) in &self.base_styles {
61 style.validate().map_err(|e| {
62 format!(
63 "Theme '{}': Invalid base style for '{}': {}",
64 self.name, widget_type, e
65 )
66 })?;
67 }
68
69 Ok(())
70 }
71
72 pub fn validate_inheritance(
79 &self,
80 all_themes: &HashMap<String, Theme>,
81 visited: &mut Vec<String>,
82 ) -> Result<(), ThemeError> {
83 if let Some(ref parent_name) = self.extends {
84 if visited.contains(parent_name) {
85 let chain = visited.join(" → ");
86 return Err(ThemeError {
87 kind: ThemeErrorKind::CircularInheritance,
88 message: format!(
89 "THEME_007: Circular theme inheritance detected: {} → {}",
90 chain, parent_name
91 ),
92 });
93 }
94
95 if !all_themes.contains_key(parent_name) {
96 return Err(ThemeError {
97 kind: ThemeErrorKind::ThemeNotFound,
98 message: format!(
99 "THEME_006: Parent theme '{}' not found for theme '{}'",
100 parent_name, self.name
101 ),
102 });
103 }
104
105 visited.push(self.name.clone());
106 if visited.len() > 5 {
107 return Err(ThemeError {
108 kind: ThemeErrorKind::ExceedsMaxDepth,
109 message: format!(
110 "THEME_008: Theme inheritance depth exceeds 5 levels for '{}'",
111 self.name
112 ),
113 });
114 }
115
116 if let Some(parent) = all_themes.get(parent_name) {
117 parent.validate_inheritance(all_themes, visited)?;
118 }
119 visited.pop();
120 }
121
122 Ok(())
123 }
124
125 pub fn inherit_from(&self, parent: &Theme) -> Self {
130 Theme {
131 name: self.name.clone(),
132 palette: self.palette.inherit_from(&parent.palette),
133 typography: self.typography.inherit_from(&parent.typography),
134 spacing: self.spacing.inherit_from(&parent.spacing),
135 base_styles: self.base_styles.clone(),
136 extends: self.extends.clone(),
137 }
138 }
139}
140
141#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
143pub struct ThemePalette {
144 pub primary: Option<Color>,
145 pub secondary: Option<Color>,
146 pub success: Option<Color>,
147 pub warning: Option<Color>,
148 pub danger: Option<Color>,
149 pub background: Option<Color>,
150 pub surface: Option<Color>,
151 pub text: Option<Color>,
152 pub text_secondary: Option<Color>,
153}
154
155impl ThemePalette {
156 pub fn validate_with_inheritance(
161 &self,
162 theme_name: &str,
163 parent_name: Option<&str>,
164 ) -> Result<(), String> {
165 let required = [
166 ("primary", "primary color for main UI elements"),
167 ("secondary", "secondary/accent color"),
168 ("success", "success state color"),
169 ("warning", "warning state color"),
170 ("danger", "danger/error state color"),
171 ("background", "background color for containers"),
172 ("surface", "surface color for cards, buttons, etc."),
173 ("text", "primary text color"),
174 ("text_secondary", "secondary/disabled text color"),
175 ];
176
177 let mut missing = Vec::new();
178 let mut invalid = Vec::new();
179
180 for (color, description) in &required {
182 let value = match *color {
183 "primary" => &self.primary,
184 "secondary" => &self.secondary,
185 "success" => &self.success,
186 "warning" => &self.warning,
187 "danger" => &self.danger,
188 "background" => &self.background,
189 "surface" => &self.surface,
190 "text" => &self.text,
191 "text_secondary" => &self.text_secondary,
192 _ => unreachable!(),
193 };
194
195 if value.is_none() {
196 missing.push((*color, *description));
197 }
198 }
199
200 if parent_name.is_some() && !missing.is_empty() {
202 } else if !missing.is_empty() {
204 let missing_list: Vec<_> = missing.iter().map(|(c, _)| *c).collect();
205 let mut message = format!(
206 "Theme '{}' is missing {} required color(s): {}",
207 theme_name,
208 missing.len(),
209 missing_list.join(", ")
210 );
211
212 if parent_name.is_none() {
213 message.push_str("\n\nTip: If you want to inherit colors from another theme, add 'extends=\"parent_theme\"' attribute to this theme.");
214 message.push_str("\nExample: <theme name=\"dark\" extends=\"base\">");
215 }
216
217 return Err(message);
218 }
219
220 for (color, _description) in &required {
222 let value = match *color {
223 "primary" => self.primary.as_ref(),
224 "secondary" => self.secondary.as_ref(),
225 "success" => self.success.as_ref(),
226 "warning" => self.warning.as_ref(),
227 "danger" => self.danger.as_ref(),
228 "background" => self.background.as_ref(),
229 "surface" => self.surface.as_ref(),
230 "text" => self.text.as_ref(),
231 "text_secondary" => self.text_secondary.as_ref(),
232 _ => unreachable!(),
233 };
234
235 if let Some(color_val) = value {
236 if let Err(e) = color_val.validate() {
237 invalid.push((*color, e));
238 }
239 }
240 }
241
242 if !invalid.is_empty() {
243 let mut message = format!("Theme '{}' has invalid color values:\n", theme_name);
244 for (color, error) in &invalid {
245 message.push_str(&format!(" - {}: {}\n", color, error));
246 }
247 message.push_str("\nValid color formats:\n");
248 message.push_str(" - Hex: #RRGGBB or #RRGGBBAA\n");
249 message.push_str(" - RGB: rgb(r, g, b) or rgba(r, g, b, a)\n");
250 message.push_str(" - HSL: hsl(h, s%, l%) or hsla(h, s%, l%, a)\n");
251 message.push_str(" - Named: red, blue, transparent, etc.");
252 return Err(message);
253 }
254
255 Ok(())
256 }
257
258 pub fn validate(&self) -> Result<(), String> {
260 self.validate_with_inheritance("theme", None)
261 }
262
263 pub fn inherit_from(&self, parent: &ThemePalette) -> Self {
265 Self {
266 primary: self.primary.or(parent.primary),
267 secondary: self.secondary.or(parent.secondary),
268 success: self.success.or(parent.success),
269 warning: self.warning.or(parent.warning),
270 danger: self.danger.or(parent.danger),
271 background: self.background.or(parent.background),
272 surface: self.surface.or(parent.surface),
273 text: self.text.or(parent.text),
274 text_secondary: self.text_secondary.or(parent.text_secondary),
275 }
276 }
277
278 #[allow(clippy::expect_used)]
283 pub fn iced_colors(&self) -> IcedPaletteColors {
284 IcedPaletteColors {
285 primary: (
286 self.primary.expect("primary color must be set").r,
287 self.primary.expect("primary color must be set").g,
288 self.primary.expect("primary color must be set").b,
289 ),
290 background: (
291 self.background.expect("background color must be set").r,
292 self.background.expect("background color must be set").g,
293 self.background.expect("background color must be set").b,
294 ),
295 text: (
296 self.text.expect("text color must be set").r,
297 self.text.expect("text color must be set").g,
298 self.text.expect("text color must be set").b,
299 ),
300 success: (
301 self.success.expect("success color must be set").r,
302 self.success.expect("success color must be set").g,
303 self.success.expect("success color must be set").b,
304 ),
305 warning: (
306 self.warning.expect("warning color must be set").r,
307 self.warning.expect("warning color must be set").g,
308 self.warning.expect("warning color must be set").b,
309 ),
310 danger: (
311 self.danger.expect("danger color must be set").r,
312 self.danger.expect("danger color must be set").g,
313 self.danger.expect("danger color must be set").b,
314 ),
315 }
316 }
317
318 pub fn light() -> Self {
328 use crate::ir::style::Color;
329 Self {
330 primary: Some(Color::from_rgb8(0x34, 0x98, 0xDB)),
331 secondary: Some(Color::from_rgb8(0x2E, 0xCC, 0x71)),
332 success: Some(Color::from_rgb8(0x27, 0xAE, 0x60)),
333 warning: Some(Color::from_rgb8(0xF3, 0x9C, 0x12)),
334 danger: Some(Color::from_rgb8(0xE7, 0x4C, 0x3C)),
335 background: Some(Color::from_rgb8(0xEC, 0xF0, 0xF1)),
336 surface: Some(Color::from_rgb8(0xFF, 0xFF, 0xFF)),
337 text: Some(Color::from_rgb8(0x2C, 0x3E, 0x50)),
338 text_secondary: Some(Color::from_rgb8(0x7F, 0x8C, 0x8D)),
339 }
340 }
341
342 pub fn dark() -> Self {
352 use crate::ir::style::Color;
353 Self {
354 primary: Some(Color::from_rgb8(0x5D, 0xAD, 0xE2)),
355 secondary: Some(Color::from_rgb8(0x52, 0xBE, 0x80)),
356 success: Some(Color::from_rgb8(0x27, 0xAE, 0x60)),
357 warning: Some(Color::from_rgb8(0xF3, 0x9C, 0x12)),
358 danger: Some(Color::from_rgb8(0xEC, 0x70, 0x63)),
359 background: Some(Color::from_rgb8(0x2C, 0x3E, 0x50)),
360 surface: Some(Color::from_rgb8(0x34, 0x49, 0x5E)),
361 text: Some(Color::from_rgb8(0xEC, 0xF0, 0xF1)),
362 text_secondary: Some(Color::from_rgb8(0x95, 0xA5, 0xA6)),
363 }
364 }
365}
366
367#[derive(Debug, Clone, Copy)]
369pub struct IcedPaletteColors {
370 pub primary: (f32, f32, f32),
371 pub background: (f32, f32, f32),
372 pub text: (f32, f32, f32),
373 pub success: (f32, f32, f32),
374 pub warning: (f32, f32, f32),
375 pub danger: (f32, f32, f32),
376}
377
378#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
380pub struct Typography {
381 pub font_family: Option<String>,
382 pub font_size_base: Option<f32>,
383 pub font_size_small: Option<f32>,
384 pub font_size_large: Option<f32>,
385 pub font_weight: FontWeight,
386 pub line_height: Option<f32>,
387}
388
389impl Typography {
390 pub fn validate_with_inheritance(
392 &self,
393 theme_name: &str,
394 parent_name: Option<&str>,
395 ) -> Result<(), String> {
396 let mut errors = Vec::new();
397
398 if let Some(size) = self.font_size_base {
399 if size <= 0.0 {
400 errors.push(format!("font_size_base must be positive, got {}", size));
401 } else if size < 8.0 {
402 errors.push(format!(
403 "font_size_base {} is very small (recommended: 14-18px)",
404 size
405 ));
406 } else if size > 32.0 {
407 errors.push(format!(
408 "font_size_base {} is very large (recommended: 14-18px)",
409 size
410 ));
411 }
412 }
413
414 if let Some(size) = self.font_size_small {
415 if size <= 0.0 {
416 errors.push(format!("font_size_small must be positive, got {}", size));
417 } else if size >= self.font_size_base.unwrap_or(16.0) {
418 errors.push("font_size_small should be smaller than font_size_base".to_string());
419 }
420 }
421
422 if let Some(size) = self.font_size_large {
423 if size <= 0.0 {
424 errors.push(format!("font_size_large must be positive, got {}", size));
425 } else if size <= self.font_size_base.unwrap_or(16.0) {
426 errors.push("font_size_large should be larger than font_size_base".to_string());
427 }
428 }
429
430 if let Some(height) = self.line_height {
431 if height <= 0.0 {
432 errors.push(format!("line_height must be positive, got {}", height));
433 } else if height < 1.0 {
434 errors.push(format!(
435 "line_height {} is too tight (recommended: 1.4-1.6)",
436 height
437 ));
438 } else if height > 2.5 {
439 errors.push(format!(
440 "line_height {} is too loose (recommended: 1.4-1.6)",
441 height
442 ));
443 }
444 }
445
446 if !errors.is_empty() {
447 let mut message = format!("Typography validation failed for theme '{}':\n", theme_name);
448 for error in &errors {
449 message.push_str(&format!(" - {}\n", error));
450 }
451
452 if parent_name.is_none() {
453 message.push_str("\nTip: Missing typography values will inherit from parent theme if 'extends' is used.");
454 }
455
456 message.push_str("\nExample typography configuration:");
457 message.push_str("\n <typography");
458 message.push_str("\n font_family=\"Inter, sans-serif\"");
459 message.push_str("\n font_size_base=\"16\"");
460 message.push_str("\n font_size_small=\"12\"");
461 message.push_str("\n font_size_large=\"20\"");
462 message.push_str("\n font_weight=\"normal\"");
463 message.push_str("\n line_height=\"1.5\" />");
464
465 return Err(message);
466 }
467
468 Ok(())
469 }
470
471 pub fn validate(&self) -> Result<(), String> {
473 self.validate_with_inheritance("theme", None)
474 }
475
476 pub fn inherit_from(&self, parent: &Typography) -> Self {
478 Self {
479 font_family: self
480 .font_family
481 .clone()
482 .or_else(|| parent.font_family.clone()),
483 font_size_base: self.font_size_base.or(parent.font_size_base),
484 font_size_small: self.font_size_small.or(parent.font_size_small),
485 font_size_large: self.font_size_large.or(parent.font_size_large),
486 font_weight: self.font_weight,
487 line_height: self.line_height.or(parent.line_height),
488 }
489 }
490}
491
492#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
494pub enum FontWeight {
495 Thin,
496 Light,
497 Normal,
498 Medium,
499 Bold,
500 Black,
501}
502
503impl FontWeight {
504 pub fn parse(s: &str) -> Result<Self, String> {
506 match s.trim().to_lowercase().as_str() {
507 "thin" => Ok(FontWeight::Thin),
508 "light" => Ok(FontWeight::Light),
509 "normal" => Ok(FontWeight::Normal),
510 "medium" => Ok(FontWeight::Medium),
511 "bold" => Ok(FontWeight::Bold),
512 "black" => Ok(FontWeight::Black),
513 _ => Err(format!(
514 "Invalid font weight: '{}'. Expected thin, light, normal, medium, bold, or black",
515 s
516 )),
517 }
518 }
519
520 pub fn to_css(&self) -> u16 {
522 match self {
523 FontWeight::Thin => 100,
524 FontWeight::Light => 300,
525 FontWeight::Normal => 400,
526 FontWeight::Medium => 500,
527 FontWeight::Bold => 700,
528 FontWeight::Black => 900,
529 }
530 }
531}
532
533#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
535pub struct SpacingScale {
536 pub unit: Option<f32>,
538}
539
540impl SpacingScale {
541 pub fn validate_with_inheritance(
543 &self,
544 theme_name: &str,
545 parent_name: Option<&str>,
546 ) -> Result<(), String> {
547 if let Some(unit) = self.unit {
548 if unit <= 0.0 {
549 let mut message = format!(
550 "Theme '{}': spacing unit must be positive, got {}\n",
551 theme_name, unit
552 );
553 message.push_str("Valid spacing examples:\n");
554 message.push_str(" - <spacing unit=\"4\" /> (4px base)\n");
555 message.push_str(" - <spacing unit=\"8\" /> (8px base, recommended)\n");
556 message.push_str(" - <spacing unit=\"16\" /> (16px base)\n");
557
558 if parent_name.is_none() {
559 message.push_str("\nTip: Missing spacing will inherit from parent theme if 'extends' is used.");
560 }
561
562 return Err(message);
563 }
564
565 if unit > 32.0 {
566 }
568 }
569 Ok(())
570 }
571
572 pub fn validate(&self) -> Result<(), String> {
574 self.validate_with_inheritance("theme", None)
575 }
576
577 pub fn get(&self, multiplier: u8) -> f32 {
579 (self.unit.unwrap_or(8.0)) * multiplier as f32
580 }
581
582 pub fn inherit_from(&self, parent: &SpacingScale) -> Self {
584 Self {
585 unit: self.unit.or(parent.unit),
586 }
587 }
588}
589
590#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
592pub struct StyleClass {
593 pub name: String,
594 pub style: StyleProperties,
595 pub layout: Option<LayoutConstraints>,
596 pub extends: Vec<String>,
598 pub state_variants: HashMap<WidgetState, StyleProperties>,
600 #[serde(default)]
602 pub combined_state_variants: HashMap<StateSelector, StyleProperties>,
603}
604
605impl StyleClass {
606 pub fn validate(&self, all_classes: &HashMap<String, StyleClass>) -> Result<(), String> {
615 self.style
617 .validate()
618 .map_err(|e| format!("Invalid style: {}", e))?;
619
620 if let Some(layout) = &self.layout {
621 layout
622 .validate()
623 .map_err(|e| format!("Invalid layout: {}", e))?;
624 }
625
626 self.check_inheritance_depth(all_classes, 0)?;
628
629 self.check_circular_dependency(all_classes, &mut Vec::new())?;
631
632 for (state, style) in &self.state_variants {
634 style
635 .validate()
636 .map_err(|e| format!("Invalid style for state {:?}: {}", state, e))?;
637 }
638
639 for (selector, style) in &self.combined_state_variants {
641 style
642 .validate()
643 .map_err(|e| format!("Invalid style for state selector {:?}: {}", selector, e))?;
644 }
645
646 for parent in &self.extends {
648 if !all_classes.contains_key(parent) {
649 return Err(format!("Parent class '{}' not found", parent));
650 }
651 }
652
653 Ok(())
654 }
655
656 fn check_inheritance_depth(
657 &self,
658 all_classes: &HashMap<String, StyleClass>,
659 depth: u8,
660 ) -> Result<(), String> {
661 if depth > 5 {
662 return Err(format!(
663 "Style class inheritance depth exceeds 5 levels (class: {})",
664 self.name
665 ));
666 }
667
668 for parent_name in &self.extends {
669 if let Some(parent) = all_classes.get(parent_name) {
670 parent.check_inheritance_depth(all_classes, depth + 1)?;
671 }
672 }
673
674 Ok(())
675 }
676
677 fn check_circular_dependency(
678 &self,
679 all_classes: &HashMap<String, StyleClass>,
680 path: &mut Vec<String>,
681 ) -> Result<(), String> {
682 if path.contains(&self.name) {
683 let chain = path.join(" → ");
684 return Err(format!(
685 "Circular style class dependency detected: {} → {}",
686 chain, self.name
687 ));
688 }
689
690 path.push(self.name.clone());
691
692 for parent_name in &self.extends {
693 if let Some(parent) = all_classes.get(parent_name) {
694 parent.check_circular_dependency(all_classes, path)?;
695 }
696 }
697
698 path.pop();
699 Ok(())
700 }
701}
702
703#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
705pub enum WidgetState {
706 Hover,
707 Focus,
708 Active,
709 Disabled,
710}
711
712impl WidgetState {
713 pub fn from_prefix(s: &str) -> Option<Self> {
715 match s.trim().to_lowercase().as_str() {
716 "hover" => Some(WidgetState::Hover),
717 "focus" => Some(WidgetState::Focus),
718 "active" => Some(WidgetState::Active),
719 "disabled" => Some(WidgetState::Disabled),
720 _ => None,
721 }
722 }
723}
724
725#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
727pub enum StateSelector {
728 Single(WidgetState),
730 Combined(Vec<WidgetState>),
733}
734
735impl StateSelector {
736 pub fn single(state: WidgetState) -> Self {
738 StateSelector::Single(state)
739 }
740
741 pub fn combined(mut states: Vec<WidgetState>) -> Self {
743 if states.len() == 1 {
744 StateSelector::Single(states[0])
745 } else {
746 states.sort();
748 states.dedup(); StateSelector::Combined(states)
750 }
751 }
752
753 pub fn matches(&self, active_states: &[WidgetState]) -> bool {
755 match self {
756 StateSelector::Single(state) => active_states.contains(state),
757 StateSelector::Combined(required_states) => {
758 required_states.iter().all(|s| active_states.contains(s))
759 }
760 }
761 }
762
763 pub fn specificity(&self) -> usize {
765 match self {
766 StateSelector::Single(_) => 1,
767 StateSelector::Combined(states) => states.len(),
768 }
769 }
770}
771
772#[derive(Debug, Clone, PartialEq)]
774pub enum ThemeErrorKind {
775 NoThemesDefined,
776 InvalidDefaultTheme,
777 MissingPaletteColor,
778 InvalidColorValue,
779 DuplicateThemeName,
780 ThemeNotFound,
781 CircularInheritance,
782 ExceedsMaxDepth,
783}
784
785#[derive(Debug, Clone, PartialEq)]
787pub struct ThemeError {
788 pub kind: ThemeErrorKind,
789 pub message: String,
790}
791
792impl std::fmt::Display for ThemeError {
793 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
794 write!(f, "{}", self.message)
795 }
796}
797
798impl std::error::Error for ThemeError {}
799
800impl std::fmt::Display for ThemeErrorKind {
801 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
802 match self {
803 ThemeErrorKind::NoThemesDefined => write!(f, "THEME_001"),
804 ThemeErrorKind::InvalidDefaultTheme => write!(f, "THEME_002"),
805 ThemeErrorKind::MissingPaletteColor => write!(f, "THEME_003"),
806 ThemeErrorKind::InvalidColorValue => write!(f, "THEME_004"),
807 ThemeErrorKind::DuplicateThemeName => write!(f, "THEME_005"),
808 ThemeErrorKind::ThemeNotFound => write!(f, "THEME_006"),
809 ThemeErrorKind::CircularInheritance => write!(f, "THEME_007"),
810 ThemeErrorKind::ExceedsMaxDepth => write!(f, "THEME_008"),
811 }
812 }
813}
814
815#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
817pub struct ThemeDocument {
818 pub themes: HashMap<String, Theme>,
820
821 pub default_theme: Option<String>,
824
825 pub follow_system: bool,
827}
828
829impl ThemeDocument {
830 pub fn validate(&self) -> Result<(), ThemeError> {
837 if self.themes.is_empty() {
838 return Err(ThemeError {
839 kind: ThemeErrorKind::NoThemesDefined,
840 message: "THEME_001: At least one theme must be defined in theme.dampen"
841 .to_string(),
842 });
843 }
844
845 if let Some(ref default) = self.default_theme {
846 if !self.themes.contains_key(default) {
847 let available: Vec<_> = self.themes.keys().cloned().collect();
848 return Err(ThemeError {
849 kind: ThemeErrorKind::InvalidDefaultTheme,
850 message: format!(
851 "THEME_002: Default theme '{}' not found. Available: {}",
852 default,
853 available.join(", ")
854 ),
855 });
856 }
857 }
858
859 for (name, theme) in &self.themes {
860 let allow_partial = theme.extends.is_some();
861 theme.validate(allow_partial).map_err(|e| ThemeError {
862 kind: ThemeErrorKind::MissingPaletteColor,
863 message: format!("THEME_003: Invalid theme '{}': {}", name, e),
864 })?;
865 }
866
867 Ok(())
868 }
869
870 pub fn validate_inheritance(&self) -> Result<(), ThemeError> {
877 for theme in self.themes.values() {
878 let mut visited = Vec::new();
879 theme.validate_inheritance(&self.themes, &mut visited)?;
880 }
881 Ok(())
882 }
883
884 pub fn resolve_inheritance(&self) -> HashMap<String, Theme> {
889 let mut resolved = HashMap::new();
890
891 fn resolve(
893 name: &str,
894 themes: &HashMap<String, Theme>,
895 resolved: &mut HashMap<String, Theme>,
896 ) {
897 if resolved.contains_key(name) {
898 return;
899 }
900
901 if let Some(theme) = themes.get(name) {
902 if let Some(ref parent_name) = theme.extends {
903 resolve(parent_name, themes, resolved);
904 if let Some(parent) = resolved.get(parent_name) {
905 resolved.insert(name.to_string(), theme.inherit_from(parent));
906 } else {
907 resolved.insert(name.to_string(), theme.clone());
909 }
910 } else {
911 resolved.insert(name.to_string(), theme.clone());
912 }
913 }
914 }
915
916 for name in self.themes.keys() {
917 resolve(name, &self.themes, &mut resolved);
918 }
919
920 resolved
921 }
922
923 pub fn effective_default<'a>(&'a self, system_preference: Option<&'a str>) -> &'a str {
927 if let Some(ref default) = self.default_theme {
928 if self.follow_system {
929 if let Some(sys) = system_preference {
930 if self.themes.contains_key(sys) {
931 return sys;
932 }
933 }
934 }
935 return default;
936 } else if self.follow_system {
937 if let Some(sys) = system_preference {
938 if self.themes.contains_key(sys) {
939 return sys;
940 }
941 }
942 }
943 "light"
944 }
945}
946
947#[cfg(test)]
948mod tests {
949 use super::*;
950
951 #[test]
952 fn test_widget_state_from_prefix_hover() {
953 assert_eq!(WidgetState::from_prefix("hover"), Some(WidgetState::Hover));
954 }
955
956 #[test]
957 fn test_widget_state_from_prefix_focus() {
958 assert_eq!(WidgetState::from_prefix("focus"), Some(WidgetState::Focus));
959 }
960
961 #[test]
962 fn test_widget_state_from_prefix_active() {
963 assert_eq!(
964 WidgetState::from_prefix("active"),
965 Some(WidgetState::Active)
966 );
967 }
968
969 #[test]
970 fn test_widget_state_from_prefix_disabled() {
971 assert_eq!(
972 WidgetState::from_prefix("disabled"),
973 Some(WidgetState::Disabled)
974 );
975 }
976
977 #[test]
978 fn test_widget_state_from_prefix_case_insensitive() {
979 assert_eq!(WidgetState::from_prefix("HOVER"), Some(WidgetState::Hover));
980 assert_eq!(WidgetState::from_prefix("Focus"), Some(WidgetState::Focus));
981 assert_eq!(
982 WidgetState::from_prefix("AcTiVe"),
983 Some(WidgetState::Active)
984 );
985 }
986
987 #[test]
988 fn test_widget_state_from_prefix_invalid() {
989 assert_eq!(WidgetState::from_prefix("unknown"), None);
990 assert_eq!(WidgetState::from_prefix("pressed"), None);
991 assert_eq!(WidgetState::from_prefix(""), None);
992 }
993
994 #[test]
995 fn test_widget_state_from_prefix_with_whitespace() {
996 assert_eq!(
998 WidgetState::from_prefix(" hover "),
999 Some(WidgetState::Hover)
1000 );
1001 }
1002}