1use super::GeneratedCode;
8use crate::ir::style::{
9 Background, Border, BorderRadius, Color, Gradient, Shadow, StyleProperties,
10};
11use crate::ir::theme::{StyleClass, ThemeDocument, WidgetState};
12use proc_macro2::TokenStream;
13use quote::quote;
14use std::collections::HashMap;
15
16fn generate_color_expr(color: &Color) -> TokenStream {
22 let r = color.r;
23 let g = color.g;
24 let b = color.b;
25 let a = color.a;
26 quote! {
27 iced::Color::from_rgba(#r, #g, #b, #a)
28 }
29}
30
31fn generate_background_expr(bg: &Background) -> TokenStream {
33 match bg {
34 Background::Color(color) => {
35 let color_expr = generate_color_expr(color);
36 quote! { iced::Background::Color(#color_expr) }
37 }
38 Background::Gradient(gradient) => generate_gradient_expr(gradient),
39 Background::Image { .. } => {
40 quote! { iced::Background::Color(iced::Color::TRANSPARENT) }
42 }
43 }
44}
45
46fn generate_gradient_expr(gradient: &Gradient) -> TokenStream {
48 match gradient {
49 Gradient::Linear { angle, stops } => {
50 let radians = angle * (std::f32::consts::PI / 180.0);
51
52 let stop_exprs: Vec<TokenStream> = stops
53 .iter()
54 .take(8)
55 .map(|stop| {
56 let offset = stop.offset;
57 let color_expr = generate_color_expr(&stop.color);
58 quote! { .add_stop(#offset, #color_expr) }
59 })
60 .collect();
61
62 quote! {
63 iced::Background::Gradient(
64 iced::Gradient::Linear(
65 iced::gradient::Linear::new(iced::Radians(#radians))
66 #(#stop_exprs)*
67 )
68 )
69 }
70 }
71 Gradient::Radial { stops, .. } => {
72 let radians = 0.0f32;
74 let stop_exprs: Vec<TokenStream> = stops
75 .iter()
76 .take(8)
77 .map(|stop| {
78 let offset = stop.offset;
79 let color_expr = generate_color_expr(&stop.color);
80 quote! { .add_stop(#offset, #color_expr) }
81 })
82 .collect();
83
84 quote! {
85 iced::Background::Gradient(
86 iced::Gradient::Linear(
87 iced::gradient::Linear::new(iced::Radians(#radians))
88 #(#stop_exprs)*
89 )
90 )
91 }
92 }
93 }
94}
95
96fn generate_border_expr(border: &Border) -> TokenStream {
98 let width = border.width;
99 let color_expr = generate_color_expr(&border.color);
100 let radius_expr = generate_border_radius_expr(&border.radius);
101
102 quote! {
103 iced::Border {
104 width: #width,
105 color: #color_expr,
106 radius: #radius_expr,
107 }
108 }
109}
110
111fn generate_border_radius_expr(radius: &BorderRadius) -> TokenStream {
113 let tl = radius.top_left;
114 let tr = radius.top_right;
115 let br = radius.bottom_right;
116 let bl = radius.bottom_left;
117
118 quote! {
119 iced::border::Radius::from(#tl)
120 .top_right(#tr)
121 .bottom_right(#br)
122 .bottom_left(#bl)
123 }
124}
125
126fn generate_shadow_expr(shadow: &Shadow) -> TokenStream {
128 let offset_x = shadow.offset_x;
129 let offset_y = shadow.offset_y;
130 let blur = shadow.blur_radius;
131 let color_expr = generate_color_expr(&shadow.color);
132
133 quote! {
134 iced::Shadow {
135 color: #color_expr,
136 offset: iced::Vector::new(#offset_x, #offset_y),
137 blur_radius: #blur,
138 }
139 }
140}
141
142fn generate_button_style_struct(style: &StyleProperties) -> TokenStream {
144 let background_expr = if let Some(ref bg) = style.background {
145 let bg_expr = generate_background_expr(bg);
146 quote! { Some(#bg_expr) }
147 } else {
148 quote! { None }
149 };
150
151 let text_color_expr = if let Some(ref color) = style.color {
152 generate_color_expr(color)
153 } else {
154 quote! { _theme.extended_palette().background.base.text }
155 };
156
157 let border_expr = if let Some(ref border) = style.border {
158 generate_border_expr(border)
159 } else {
160 quote! { iced::Border::default() }
161 };
162
163 let shadow_expr = if let Some(ref shadow) = style.shadow {
164 generate_shadow_expr(shadow)
165 } else {
166 quote! { iced::Shadow::default() }
167 };
168
169 quote! {
170 iced::widget::button::Style {
171 background: #background_expr,
172 text_color: #text_color_expr,
173 border: #border_expr,
174 shadow: #shadow_expr,
175 snap: false,
176 }
177 }
178}
179
180fn generate_container_style_struct(style: &StyleProperties) -> TokenStream {
182 let background_expr = if let Some(ref bg) = style.background {
183 let bg_expr = generate_background_expr(bg);
184 quote! { Some(#bg_expr) }
185 } else {
186 quote! { None }
187 };
188
189 let text_color_expr = if let Some(ref color) = style.color {
190 let color_expr = generate_color_expr(color);
191 quote! { Some(#color_expr) }
192 } else {
193 quote! { None }
194 };
195
196 let border_expr = if let Some(ref border) = style.border {
197 generate_border_expr(border)
198 } else {
199 quote! { iced::Border::default() }
200 };
201
202 let shadow_expr = if let Some(ref shadow) = style.shadow {
203 generate_shadow_expr(shadow)
204 } else {
205 quote! { iced::Shadow::default() }
206 };
207
208 quote! {
209 iced::widget::container::Style {
210 background: #background_expr,
211 text_color: #text_color_expr,
212 border: #border_expr,
213 shadow: #shadow_expr,
214 snap: false,
215 }
216 }
217}
218
219fn merge_style_properties(
221 base: &StyleProperties,
222 override_props: &StyleProperties,
223) -> StyleProperties {
224 StyleProperties {
225 background: override_props
226 .background
227 .clone()
228 .or_else(|| base.background.clone()),
229 color: override_props.color.or(base.color),
230 border: override_props
231 .border
232 .clone()
233 .or_else(|| base.border.clone()),
234 shadow: override_props.shadow.or(base.shadow),
235 opacity: override_props.opacity.or(base.opacity),
236 transform: override_props
237 .transform
238 .clone()
239 .or_else(|| base.transform.clone()),
240 }
241}
242
243fn infer_widget_type_from_class(style_class: &StyleClass) -> &'static str {
245 if !style_class.state_variants.is_empty() {
248 "button"
249 } else {
250 "container"
251 }
252}
253
254fn generate_state_match_for_button(style_class: &StyleClass) -> TokenStream {
256 let mut match_arms = Vec::new();
257
258 let base_style_expr = generate_button_style_struct(&style_class.style);
260 match_arms.push(quote! {
261 iced::widget::button::Status::Active => #base_style_expr
262 });
263
264 for (state, override_style) in &style_class.state_variants {
266 let merged_style = merge_style_properties(&style_class.style, override_style);
267 let style_expr = generate_button_style_struct(&merged_style);
268
269 let status_variant = match state {
270 WidgetState::Hover => quote! { iced::widget::button::Status::Hovered },
271 WidgetState::Active => quote! { iced::widget::button::Status::Pressed },
272 WidgetState::Disabled => quote! { iced::widget::button::Status::Disabled },
273 WidgetState::Focus => {
274 continue;
276 }
277 };
278
279 match_arms.push(quote! {
280 #status_variant => #style_expr
281 });
282 }
283
284 let fallback_style = generate_button_style_struct(&style_class.style);
286 match_arms.push(quote! {
287 _ => #fallback_style
288 });
289
290 quote! {
291 match status {
292 #(#match_arms),*
293 }
294 }
295}
296
297fn generate_style_class_function(
299 class_name: &str,
300 style_class: &StyleClass,
301) -> Result<String, String> {
302 let fn_name = format!("style_{}", class_name.replace("-", "_").replace(":", "_"));
303 let widget_type = infer_widget_type_from_class(style_class);
304
305 let mut code = String::new();
306 code.push_str(&format!("/// Style function for class '{}'\n", class_name));
307
308 if widget_type == "button" && !style_class.state_variants.is_empty() {
309 code.push_str(&format!(
311 "pub fn {}(_theme: &iced::Theme, status: iced::widget::button::Status) -> iced::widget::button::Style {{\n",
312 fn_name
313 ));
314
315 let match_expr = generate_state_match_for_button(style_class);
316 let match_str = match_expr.to_string();
317 code.push_str(" ");
318 code.push_str(&match_str);
319 code.push('\n');
320 } else {
321 code.push_str(&format!(
323 "pub fn {}(_theme: &iced::Theme) -> iced::widget::container::Style {{\n",
324 fn_name
325 ));
326
327 let style_expr = generate_container_style_struct(&style_class.style);
328 let style_str = style_expr.to_string();
329 code.push_str(" ");
330 code.push_str(&style_str);
331 code.push('\n');
332 }
333
334 code.push_str("}\n\n");
335
336 Ok(code)
337}
338
339pub fn generate_theme_code(
398 document: &ThemeDocument,
399 style_classes: &HashMap<String, StyleClass>,
400 module_name: &str,
401) -> Result<GeneratedCode, String> {
402 if document.themes.is_empty() {
403 return Err("THEME_001: At least one theme must be defined".to_string());
404 }
405
406 let mut code = String::new();
407
408 code.push_str("// Generated theme code - DO NOT EDIT\n");
409 code.push_str("// This file is auto-generated by the dampen codegen.\n\n");
410
411 code.push_str("use std::cell::RefCell;\n\n");
413 code.push_str("thread_local! {\n");
414 code.push_str(
415 " static CURRENT_THEME: RefCell<Option<String>> = const { RefCell::new(None) };\n",
416 );
417 code.push_str("}\n\n");
418
419 code.push_str("/// Set the current theme by name\n");
420 code.push_str(&format!(
421 "pub fn {}_set_current_theme(name: &str) {{\n",
422 module_name
423 ));
424 code.push_str(" CURRENT_THEME.with(|t| {\n");
425 code.push_str(" *t.borrow_mut() = Some(name.to_string());\n");
426 code.push_str(" });\n");
427 code.push_str("}\n\n");
428
429 code.push_str("/// Get the current theme name\n");
430 code.push_str(&format!(
431 "pub fn {}_current_theme_name() -> String {{\n",
432 module_name
433 ));
434 code.push_str(" CURRENT_THEME.with(|t| {\n");
435 code.push_str(" t.borrow().clone().unwrap_or_else(|| {\n");
436 let effective_default = document.effective_default(None);
437 code.push_str(&format!(
438 " \"{}\".to_string()\n",
439 effective_default
440 ));
441 code.push_str(" })\n");
442 code.push_str(" })\n");
443 code.push_str("}\n\n");
444
445 code.push_str(
446 "/// Get the current theme (respects system preference when follow_system is enabled)\n",
447 );
448 code.push_str(&format!("pub fn {}_theme() -> Theme {{\n", module_name));
449 code.push_str(&format!(
450 " let name = {}_current_theme_name();\n",
451 module_name
452 ));
453 code.push_str(&format!(
454 " {}_theme_named(&name).unwrap_or_else(|| {}_default_theme())\n",
455 module_name, module_name
456 ));
457 code.push_str("}\n\n");
458
459 code.push_str("/// Get a specific theme by name\n");
460 code.push_str(&format!(
461 "pub fn {}_theme_named(name: &str) -> Option<Theme> {{\n",
462 module_name
463 ));
464 code.push_str(&format!(" let themes = {}_themes();\n", module_name));
465 code.push_str(" themes.get(name).cloned()\n");
466 code.push_str("}\n\n");
467
468 code.push_str("/// Get all available themes\n");
469 code.push_str(&format!(
470 "pub fn {}_themes() -> HashMap<&'static str, Theme> {{\n",
471 module_name
472 ));
473 code.push_str(" let mut themes = HashMap::new();\n");
474
475 let mut theme_names: Vec<&str> = document.themes.keys().map(|s| s.as_str()).collect();
476 theme_names.sort();
477
478 for theme_name in &theme_names {
479 code.push_str(&format!(
480 " themes.insert(\"{}\", {}_{}());\n",
481 theme_name, module_name, theme_name
482 ));
483 }
484
485 code.push_str(" themes\n");
486 code.push_str("}\n\n");
487
488 code.push_str("/// Get the default theme\n");
489 code.push_str(&format!(
490 "pub fn {}_default_theme() -> Theme {{\n",
491 module_name
492 ));
493 code.push_str(&format!(" {}_{}()\n", module_name, effective_default));
494 code.push_str("}\n\n");
495
496 code.push_str("/// Get the default theme name as a string\n");
497 code.push_str(&format!(
498 "pub fn {}_default_theme_name() -> &'static str {{\n",
499 module_name
500 ));
501 code.push_str(&format!(" \"{}\"\n", effective_default));
502 code.push_str("}\n\n");
503
504 code.push_str("/// Get whether the theme follows system preference\n");
505 code.push_str(&format!(
506 "pub fn {}_follows_system() -> bool {{\n",
507 module_name
508 ));
509 code.push_str(&format!(" {}\n", document.follow_system));
510 code.push_str("}\n\n");
511
512 for theme_name in &theme_names {
513 let theme = match document.themes.get(*theme_name) {
514 Some(t) => t,
515 None => continue,
516 };
517
518 let theme_fn_name = format!("{}_{}", module_name, theme_name);
519 code.push_str(&format!("/// Theme: {}\n", theme_name));
520 code.push_str("fn ");
521 code.push_str(&theme_fn_name);
522 code.push_str("() -> Theme {\n");
523
524 let palette = &theme.palette;
525 let primary = color_to_rgb8_tuple(palette.primary.as_ref());
526 let background = color_to_rgb8_tuple(palette.background.as_ref());
527 let text = color_to_rgb8_tuple(palette.text.as_ref());
528 let success = color_to_rgb8_tuple(palette.success.as_ref());
529 let warning = color_to_rgb8_tuple(palette.warning.as_ref());
530 let danger = color_to_rgb8_tuple(palette.danger.as_ref());
531
532 code.push_str(" Theme::custom(\n");
533 code.push_str(&format!(" \"{}\".to_string(),\n", theme_name));
534 code.push_str(" iced::theme::Palette {\n");
535 code.push_str(&format!(
536 " background: iced::Color::from_rgb8(0x{:02X}, 0x{:02X}, 0x{:02X}),\n",
537 (background.0 * 255.0) as u8,
538 (background.1 * 255.0) as u8,
539 (background.2 * 255.0) as u8
540 ));
541 code.push_str(&format!(
542 " text: iced::Color::from_rgb8(0x{:02X}, 0x{:02X}, 0x{:02X}),\n",
543 (text.0 * 255.0) as u8,
544 (text.1 * 255.0) as u8,
545 (text.2 * 255.0) as u8
546 ));
547 code.push_str(&format!(
548 " primary: iced::Color::from_rgb8(0x{:02X}, 0x{:02X}, 0x{:02X}),\n",
549 (primary.0 * 255.0) as u8,
550 (primary.1 * 255.0) as u8,
551 (primary.2 * 255.0) as u8
552 ));
553 code.push_str(&format!(
554 " success: iced::Color::from_rgb8(0x{:02X}, 0x{:02X}, 0x{:02X}),\n",
555 (success.0 * 255.0) as u8,
556 (success.1 * 255.0) as u8,
557 (success.2 * 255.0) as u8
558 ));
559 code.push_str(&format!(
560 " warning: iced::Color::from_rgb8(0x{:02X}, 0x{:02X}, 0x{:02X}),\n",
561 (warning.0 * 255.0) as u8,
562 (warning.1 * 255.0) as u8,
563 (warning.2 * 255.0) as u8
564 ));
565 code.push_str(&format!(
566 " danger: iced::Color::from_rgb8(0x{:02X}, 0x{:02X}, 0x{:02X}),\n",
567 (danger.0 * 255.0) as u8,
568 (danger.1 * 255.0) as u8,
569 (danger.2 * 255.0) as u8
570 ));
571 code.push_str(" }\n");
572 code.push_str(" )\n");
573 code.push_str("}\n\n");
574 }
575
576 if !style_classes.is_empty() {
580 code.push_str("// ========================================\n");
581 code.push_str("// Style Class Functions\n");
582 code.push_str("// ========================================\n\n");
583
584 let mut class_names: Vec<&str> = style_classes.keys().map(|s| s.as_str()).collect();
585 class_names.sort();
586
587 for class_name in class_names {
588 if let Some(style_class) = style_classes.get(class_name) {
589 let class_fn_code = generate_style_class_function(class_name, style_class)?;
590 code.push_str(&class_fn_code);
591 }
592 }
593 }
594
595 let source_file = format!("{}/theme.dampen", module_name);
596 Ok(GeneratedCode::new(
597 code,
598 format!("{}_theme", module_name),
599 std::path::PathBuf::from(source_file),
600 ))
601}
602
603fn color_to_rgb8_tuple(color: Option<&Color>) -> (f32, f32, f32) {
605 match color {
606 Some(c) => (c.r, c.g, c.b),
607 None => (0.0, 0.0, 0.0),
608 }
609}
610
611#[cfg(test)]
612mod tests {
613 use super::*;
614 use crate::ir::style::Color;
615 use crate::ir::theme::{SpacingScale, Theme, ThemePalette, Typography};
616
617 fn create_test_palette_with_hex(hex: &str) -> ThemePalette {
618 ThemePalette {
619 primary: Some(Color::from_hex(hex).unwrap()),
620 secondary: Some(Color::from_hex("#2ecc71").unwrap()),
621 success: Some(Color::from_hex("#27ae60").unwrap()),
622 warning: Some(Color::from_hex("#f39c12").unwrap()),
623 danger: Some(Color::from_hex("#e74c3c").unwrap()),
624 background: Some(Color::from_hex("#ecf0f1").unwrap()),
625 surface: Some(Color::from_hex("#ffffff").unwrap()),
626 text: Some(Color::from_hex("#2c3e50").unwrap()),
627 text_secondary: Some(Color::from_hex("#7f8c8d").unwrap()),
628 }
629 }
630
631 fn create_test_theme(name: &str, primary_hex: &str) -> Theme {
632 Theme {
633 name: name.to_string(),
634 palette: create_test_palette_with_hex(primary_hex),
635 typography: Typography {
636 font_family: Some("sans-serif".to_string()),
637 font_size_base: Some(16.0),
638 font_size_small: Some(12.0),
639 font_size_large: Some(24.0),
640 font_weight: crate::ir::theme::FontWeight::Normal,
641 line_height: Some(1.5),
642 },
643 spacing: SpacingScale { unit: Some(8.0) },
644 base_styles: std::collections::HashMap::new(),
645 extends: None,
646 }
647 }
648
649 #[test]
650 fn test_generate_theme_code_basic() {
651 let doc = ThemeDocument {
652 themes: std::collections::HashMap::from([(
653 "light".to_string(),
654 create_test_theme("light", "#3498db"),
655 )]),
656 default_theme: Some("light".to_string()),
657 follow_system: false,
658 };
659
660 let style_classes = HashMap::new();
661 let result = generate_theme_code(&doc, &style_classes, "test");
662
663 assert!(result.is_ok());
664 let code = result.unwrap().code;
665
666 assert!(code.contains("pub fn test_theme()"));
667 assert!(code.contains("pub fn test_themes()"));
668 assert!(code.contains("pub fn test_default_theme()"));
669 assert!(code.contains("fn test_light()"));
670 assert!(code.contains("Theme::custom"));
671 assert!(code.contains("Color::from_rgb8"));
672 }
673
674 #[test]
675 fn test_generate_theme_code_multiple_themes() {
676 let doc = ThemeDocument {
677 themes: std::collections::HashMap::from([
678 ("light".to_string(), create_test_theme("light", "#3498db")),
679 ("dark".to_string(), create_test_theme("dark", "#5dade2")),
680 ]),
681 default_theme: Some("light".to_string()),
682 follow_system: true,
683 };
684
685 let style_classes = HashMap::new();
686 let result = generate_theme_code(&doc, &style_classes, "app");
687
688 assert!(result.is_ok());
689 let code = result.unwrap().code;
690
691 assert!(code.contains("fn app_light()"));
692 assert!(code.contains("fn app_dark()"));
693 assert!(code.contains("themes.insert(\"light\""));
694 assert!(code.contains("themes.insert(\"dark\""));
695 }
696
697 #[test]
698 fn test_generate_theme_code_empty_themes_error() {
699 let doc = ThemeDocument {
700 themes: std::collections::HashMap::new(),
701 default_theme: None,
702 follow_system: true,
703 };
704
705 let style_classes = HashMap::new();
706 let result = generate_theme_code(&doc, &style_classes, "app");
707
708 assert!(result.is_err());
709 let err = result.unwrap_err();
710 assert!(err.contains("THEME_001") || err.contains("no themes"));
711 }
712
713 #[test]
714 fn test_generate_theme_code_valid_rust_syntax() {
715 let doc = ThemeDocument {
716 themes: std::collections::HashMap::from([(
717 "test".to_string(),
718 create_test_theme("test", "#ff0000"),
719 )]),
720 default_theme: Some("test".to_string()),
721 follow_system: false,
722 };
723
724 let style_classes = HashMap::new();
725 let result = generate_theme_code(&doc, &style_classes, "test");
726
727 assert!(result.is_ok());
728 let code = result.unwrap().code;
729
730 let parsed = syn::parse_file(&code);
731 assert!(
732 parsed.is_ok(),
733 "Generated code should be valid Rust syntax: {:?}",
734 parsed.err()
735 );
736 }
737
738 #[test]
739 fn test_generate_theme_code_contains_color_values() {
740 let doc = ThemeDocument {
741 themes: std::collections::HashMap::from([(
742 "custom".to_string(),
743 create_test_theme("custom", "#AABBCC"),
744 )]),
745 default_theme: Some("custom".to_string()),
746 follow_system: false,
747 };
748
749 let style_classes = HashMap::new();
750 let result = generate_theme_code(&doc, &style_classes, "myapp");
751
752 assert!(result.is_ok());
753 let code = result.unwrap().code;
754
755 assert!(
756 code.contains("0xAA") || code.contains("0xBB") || code.contains("0xCC"),
757 "Generated code should contain the color values"
758 );
759 }
760
761 #[test]
762 fn test_generate_style_class_simple() {
763 let style_class = StyleClass {
764 name: "primary-button".to_string(),
765 style: StyleProperties {
766 background: Some(Background::Color(Color::from_rgb8(52, 152, 219))),
767 color: Some(Color::from_rgb8(255, 255, 255)),
768 border: None,
769 shadow: None,
770 opacity: None,
771 transform: None,
772 },
773 layout: None,
774 extends: vec![],
775 state_variants: HashMap::new(),
776 combined_state_variants: HashMap::new(),
777 };
778
779 let mut style_classes = HashMap::new();
780 style_classes.insert("primary-button".to_string(), style_class);
781
782 let theme_doc = ThemeDocument {
783 themes: HashMap::from([("light".to_string(), create_test_theme("light", "#3498db"))]),
784 default_theme: Some("light".to_string()),
785 follow_system: false,
786 };
787
788 let result = generate_theme_code(&theme_doc, &style_classes, "test");
789 assert!(result.is_ok());
790
791 let code = result.unwrap().code;
792 assert!(code.contains("pub fn style_primary_button"));
793 assert!(code.contains("Style Class Functions"));
794 }
795
796 #[test]
797 fn test_generate_style_with_hover() {
798 let mut state_variants = HashMap::new();
799 state_variants.insert(
800 WidgetState::Hover,
801 StyleProperties {
802 background: Some(Background::Color(Color::from_rgb8(74, 172, 239))),
803 color: None,
804 border: None,
805 shadow: None,
806 opacity: None,
807 transform: None,
808 },
809 );
810
811 let style_class = StyleClass {
812 name: "hover-button".to_string(),
813 style: StyleProperties {
814 background: Some(Background::Color(Color::from_rgb8(52, 152, 219))),
815 color: Some(Color::from_rgb8(255, 255, 255)),
816 border: None,
817 shadow: None,
818 opacity: None,
819 transform: None,
820 },
821 layout: None,
822 extends: vec![],
823 state_variants,
824 combined_state_variants: HashMap::new(),
825 };
826
827 let mut style_classes = HashMap::new();
828 style_classes.insert("hover-button".to_string(), style_class);
829
830 let theme_doc = ThemeDocument {
831 themes: HashMap::from([("light".to_string(), create_test_theme("light", "#3498db"))]),
832 default_theme: Some("light".to_string()),
833 follow_system: false,
834 };
835
836 let result = generate_theme_code(&theme_doc, &style_classes, "test");
837 assert!(result.is_ok());
838
839 let code = result.unwrap().code;
840 assert!(code.contains("style_hover_button"));
841 assert!(code.contains("Status :: Active"));
842 assert!(code.contains("Status :: Hovered"));
843 }
844}