1use crate::{
22 analysis::styles::{
23 resolved_style::ResolvedStyle,
24 validation::{StyleConflict, StyleInheritance, StyleValidationIssue},
25 },
26 parser::{Script, Section, Style},
27};
28use alloc::{collections::BTreeMap, collections::BTreeSet, vec::Vec};
29
30bitflags::bitflags! {
31 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
33 pub struct AnalysisOptions: u8 {
34 const INHERITANCE = 1 << 0;
36 const CONFLICTS = 1 << 1;
38 const PERFORMANCE = 1 << 2;
40 const VALIDATION = 1 << 3;
42 const STRICT_VALIDATION = 1 << 4;
44 }
45}
46
47#[derive(Debug)]
52pub struct StyleAnalyzer<'a> {
53 script: &'a Script<'a>,
55 resolved_styles: BTreeMap<&'a str, ResolvedStyle<'a>>,
57 inheritance_info: BTreeMap<&'a str, StyleInheritance<'a>>,
59 conflicts: Vec<StyleConflict<'a>>,
61 config: StyleAnalysisConfig,
63 resolution_scaling: Option<(f32, f32)>,
65}
66
67#[derive(Debug, Clone)]
69pub struct StyleAnalysisConfig {
70 pub options: AnalysisOptions,
72 pub performance_thresholds: PerformanceThresholds,
74}
75
76#[derive(Debug, Clone)]
78pub struct PerformanceThresholds {
79 pub large_font_threshold: f32,
81 pub large_outline_threshold: f32,
83 pub large_shadow_threshold: f32,
85 pub scaling_threshold: f32,
87}
88
89impl Default for StyleAnalysisConfig {
90 fn default() -> Self {
91 Self {
92 options: AnalysisOptions::INHERITANCE
93 | AnalysisOptions::CONFLICTS
94 | AnalysisOptions::PERFORMANCE
95 | AnalysisOptions::VALIDATION,
96 performance_thresholds: PerformanceThresholds::default(),
97 }
98 }
99}
100
101impl Default for PerformanceThresholds {
102 fn default() -> Self {
103 Self {
104 large_font_threshold: 50.0,
105 large_outline_threshold: 4.0,
106 large_shadow_threshold: 4.0,
107 scaling_threshold: 200.0,
108 }
109 }
110}
111
112impl<'a> StyleAnalyzer<'a> {
113 #[must_use]
115 pub fn new(script: &'a Script<'a>) -> Self {
116 Self::new_with_config(script, StyleAnalysisConfig::default())
117 }
118
119 #[must_use]
121 pub fn new_with_config(script: &'a Script<'a>, config: StyleAnalysisConfig) -> Self {
122 let mut analyzer = Self {
123 script,
124 resolved_styles: BTreeMap::new(),
125 inheritance_info: BTreeMap::new(),
126 conflicts: Vec::new(),
127 config,
128 resolution_scaling: None,
129 };
130
131 analyzer.calculate_resolution_scaling();
132 analyzer.analyze_all_styles();
133 analyzer
134 }
135
136 #[must_use]
138 pub fn resolve_style(&self, name: &str) -> Option<&ResolvedStyle<'a>> {
139 self.resolved_styles.get(name)
140 }
141
142 #[must_use]
144 pub const fn resolved_styles(&self) -> &BTreeMap<&'a str, ResolvedStyle<'a>> {
145 &self.resolved_styles
146 }
147
148 #[must_use]
150 pub fn conflicts(&self) -> &[StyleConflict<'a>] {
151 &self.conflicts
152 }
153
154 #[must_use]
156 pub const fn inheritance_info(&self) -> &BTreeMap<&'a str, StyleInheritance<'a>> {
157 &self.inheritance_info
158 }
159
160 #[must_use]
162 pub fn validate_styles(&self) -> Vec<StyleValidationIssue> {
163 let mut issues = Vec::new();
164
165 for resolved in self.resolved_styles.values() {
166 if self.config.options.contains(AnalysisOptions::VALIDATION) {
167 issues.extend(self.validate_style_properties(resolved));
168 }
169
170 if self.config.options.contains(AnalysisOptions::PERFORMANCE) {
171 issues.extend(self.analyze_style_performance(resolved));
172 }
173 }
174
175 issues
176 }
177
178 fn calculate_resolution_scaling(&mut self) {
180 for section in self.script.sections() {
182 if let Section::ScriptInfo(script_info) = section {
183 let layout_res = script_info.layout_resolution();
184 let play_res = script_info.play_resolution();
185
186 if let (Some((layout_x, layout_y)), Some((play_x, play_y))) = (layout_res, play_res)
187 {
188 if layout_x != play_x || layout_y != play_y {
190 #[allow(clippy::cast_precision_loss)]
191 let scale_x = play_x as f32 / layout_x as f32;
192 #[allow(clippy::cast_precision_loss)]
193 let scale_y = play_y as f32 / layout_y as f32;
194 self.resolution_scaling = Some((scale_x, scale_y));
195 }
196 }
197 break;
198 }
199 }
200 }
201
202 fn analyze_all_styles(&mut self) {
204 for section in self.script.sections() {
205 if let Section::Styles(styles) = section {
206 if let Some(ordered_styles) = self.build_dependency_order(styles) {
208 self.resolve_styles_with_inheritance(&ordered_styles);
209 } else {
210 for style in styles {
212 if let Ok(mut resolved) = ResolvedStyle::from_style(style) {
213 if let Some((scale_x, scale_y)) = self.resolution_scaling {
215 resolved.apply_resolution_scaling(scale_x, scale_y);
216 }
217 self.resolved_styles.insert(style.name, resolved);
218 }
219 }
220 }
221
222 if self.config.options.contains(AnalysisOptions::CONFLICTS) {
223 self.detect_style_conflicts_from_section(styles);
224 }
225 break;
226 }
227 }
228 }
229
230 fn build_dependency_order(&mut self, styles: &'a [Style<'a>]) -> Option<Vec<&'a Style<'a>>> {
233 let style_map: BTreeMap<&str, &Style> = styles.iter().map(|s| (s.name, s)).collect();
235
236 let mut dependencies: BTreeMap<&str, BTreeSet<&str>> = BTreeMap::new();
238 let mut in_degree: BTreeMap<&str, usize> = BTreeMap::new();
239
240 for style in styles {
242 dependencies.insert(style.name, BTreeSet::new());
243 in_degree.insert(style.name, 0);
244 }
245
246 for style in styles {
248 if let Some(parent_name) = style.parent {
249 if style_map.contains_key(parent_name) {
250 dependencies
251 .get_mut(style.name)
252 .unwrap()
253 .insert(parent_name);
254 *in_degree.get_mut(parent_name).unwrap() += 1;
255
256 if self.config.options.contains(AnalysisOptions::INHERITANCE) {
258 if let Some(inheritance) = self.inheritance_info.get_mut(style.name) {
259 inheritance.set_parent(parent_name);
260 } else {
261 let mut inheritance = StyleInheritance::new(style.name);
262 inheritance.set_parent(parent_name);
263 self.inheritance_info.insert(style.name, inheritance);
264 }
265 }
266 } else {
267 self.conflicts
269 .push(StyleConflict::missing_parent(style.name, parent_name));
270 }
271 } else if self.config.options.contains(AnalysisOptions::INHERITANCE) {
272 self.inheritance_info
274 .insert(style.name, StyleInheritance::new(style.name));
275 }
276 }
277
278 if Self::has_circular_dependency(&dependencies) {
280 self.conflicts.push(StyleConflict::circular_inheritance(
281 dependencies.keys().copied().collect(),
282 ));
283 return None;
284 }
285
286 let mut result = Vec::new();
288 let mut queue: Vec<&str> = Vec::new();
289
290 for (name, degree) in &in_degree {
292 if *degree == 0 {
293 queue.push(name);
294 }
295 }
296
297 while let Some(current) = queue.pop() {
298 if let Some(style) = style_map.get(current) {
299 result.push(*style);
300 }
301
302 for (child, parents) in &dependencies {
304 if parents.contains(current) {
305 if let Some(degree) = in_degree.get_mut(child) {
306 *degree = degree.saturating_sub(1);
307 if *degree == 0 {
308 queue.push(child);
309 }
310 }
311 }
312 }
313 }
314
315 if result.len() == styles.len() {
317 Some(result)
318 } else {
319 None
321 }
322 }
323
324 fn has_circular_dependency(dependencies: &BTreeMap<&str, BTreeSet<&str>>) -> bool {
326 let mut visited = BTreeSet::new();
327 let mut rec_stack = BTreeSet::new();
328
329 for node in dependencies.keys() {
330 if !visited.contains(node)
331 && Self::dfs_has_cycle(node, dependencies, &mut visited, &mut rec_stack)
332 {
333 return true;
334 }
335 }
336
337 false
338 }
339
340 fn dfs_has_cycle<'b>(
342 node: &'b str,
343 dependencies: &BTreeMap<&'b str, BTreeSet<&'b str>>,
344 visited: &mut BTreeSet<&'b str>,
345 rec_stack: &mut BTreeSet<&'b str>,
346 ) -> bool {
347 visited.insert(node);
348 rec_stack.insert(node);
349
350 if let Some(neighbors) = dependencies.get(node) {
351 for neighbor in neighbors {
352 if !visited.contains(neighbor) {
353 if Self::dfs_has_cycle(neighbor, dependencies, visited, rec_stack) {
354 return true;
355 }
356 } else if rec_stack.contains(neighbor) {
357 return true;
358 }
359 }
360 }
361
362 rec_stack.remove(node);
363 false
364 }
365
366 fn resolve_styles_with_inheritance(&mut self, ordered_styles: &[&'a Style<'a>]) {
368 for style in ordered_styles {
369 let resolved = if let Some(parent_name) = style.parent {
370 self.resolved_styles.get(parent_name).map_or_else(
372 || ResolvedStyle::from_style(style),
373 |parent_resolved| ResolvedStyle::from_style_with_parent(style, parent_resolved),
374 )
375 } else {
376 ResolvedStyle::from_style(style)
378 };
379
380 if let Ok(mut resolved_style) = resolved {
381 if let Some((scale_x, scale_y)) = self.resolution_scaling {
383 resolved_style.apply_resolution_scaling(scale_x, scale_y);
384 }
385 self.resolved_styles.insert(style.name, resolved_style);
386 }
387 }
388 }
389
390 #[must_use]
392 pub fn extract_styles(&self) -> Option<&[Style<'a>]> {
393 for section in self.script.sections() {
394 if let Section::Styles(styles) = section {
395 return Some(styles);
396 }
397 }
398 None
399 }
400
401 fn detect_style_conflicts_from_section(&mut self, styles: &[Style<'a>]) {
403 let mut name_counts: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
404
405 for style in styles {
406 name_counts.entry(style.name).or_default().push(style.name);
407 }
408
409 for (_name, instances) in name_counts {
410 if instances.len() > 1 {
411 self.conflicts
412 .push(StyleConflict::duplicate_name(instances));
413 }
414 }
415 }
416
417 fn validate_style_properties(&self, style: &ResolvedStyle<'a>) -> Vec<StyleValidationIssue> {
419 let mut issues = Vec::new();
420
421 if style.font_size() <= 0.0 {
422 issues.push(StyleValidationIssue::error(
423 "font_size",
424 "Font size must be positive",
425 ));
426 }
427
428 if self
429 .config
430 .options
431 .contains(AnalysisOptions::STRICT_VALIDATION)
432 && style.font_size() > 200.0
433 {
434 issues.push(StyleValidationIssue::warning(
435 "font_size",
436 "Very large font size may cause performance issues",
437 ));
438 }
439
440 issues
441 }
442
443 fn analyze_style_performance(&self, style: &ResolvedStyle<'a>) -> Vec<StyleValidationIssue> {
445 let mut issues = Vec::new();
446 let thresholds = &self.config.performance_thresholds;
447
448 if style.font_size() > thresholds.large_font_threshold {
449 issues.push(StyleValidationIssue::info_with_suggestion(
450 "font_size",
451 "Large font size detected",
452 "Consider reducing font size for better performance",
453 ));
454 }
455
456 if style.has_performance_issues() {
457 issues.push(StyleValidationIssue::warning(
458 "complexity",
459 "Style has high rendering complexity",
460 ));
461 }
462
463 issues
464 }
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470 use crate::analysis::styles::validation::ConflictType;
471 #[cfg(not(feature = "std"))]
472 use alloc::format;
473
474 #[test]
475 fn analyzer_creation() {
476 let script_text = r"
477[V4+ Styles]
478Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
479Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
480";
481
482 let script = crate::parser::Script::parse(script_text).unwrap();
483 let analyzer = StyleAnalyzer::new(&script);
484
485 assert_eq!(analyzer.resolved_styles().len(), 1);
486 assert!(analyzer.resolve_style("Default").is_some());
487 }
488
489 #[test]
490 fn config_defaults() {
491 let config = StyleAnalysisConfig::default();
492 assert!(config.options.contains(AnalysisOptions::INHERITANCE));
493 assert!(config.options.contains(AnalysisOptions::CONFLICTS));
494 assert!(config.options.contains(AnalysisOptions::VALIDATION));
495 assert!(!config.options.contains(AnalysisOptions::STRICT_VALIDATION));
496 }
497
498 #[test]
499 fn performance_thresholds() {
500 let thresholds = PerformanceThresholds::default();
501 assert!((thresholds.large_font_threshold - 50.0).abs() < f32::EPSILON);
502 assert!((thresholds.large_outline_threshold - 4.0).abs() < f32::EPSILON);
503 assert!((thresholds.large_shadow_threshold - 4.0).abs() < f32::EPSILON);
504 assert!((thresholds.scaling_threshold - 200.0).abs() < f32::EPSILON);
505 }
506
507 #[test]
508 fn analyzer_with_custom_config() {
509 let script_text = r"
510[V4+ Styles]
511Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
512Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
513";
514
515 let script = crate::parser::Script::parse(script_text).unwrap();
516 let config = StyleAnalysisConfig {
517 options: AnalysisOptions::VALIDATION | AnalysisOptions::STRICT_VALIDATION,
518 performance_thresholds: PerformanceThresholds {
519 large_font_threshold: 30.0,
520 large_outline_threshold: 2.0,
521 large_shadow_threshold: 2.0,
522 scaling_threshold: 150.0,
523 },
524 };
525 let analyzer = StyleAnalyzer::new_with_config(&script, config);
526
527 assert_eq!(analyzer.resolved_styles().len(), 1);
528 assert!(analyzer.resolve_style("Default").is_some());
529 }
530
531 #[test]
532 fn analyzer_multiple_styles() {
533 let script_text = r"
534[V4+ Styles]
535Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
536Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
537Style: Title,Arial,32,&H00FFFF00,&H000000FF,&H00000000,&H00000000,1,0,0,0,100,100,0,0,1,3,0,2,20,20,20,1
538Style: Subtitle,Arial,16,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,1,0,0,100,100,0,0,1,1,0,2,5,5,5,1
539";
540
541 let script = crate::parser::Script::parse(script_text).unwrap();
542 let analyzer = StyleAnalyzer::new(&script);
543
544 assert_eq!(analyzer.resolved_styles().len(), 3);
545 assert!(analyzer.resolve_style("Default").is_some());
546 assert!(analyzer.resolve_style("Title").is_some());
547 assert!(analyzer.resolve_style("Subtitle").is_some());
548 assert!(analyzer.resolve_style("NonExistent").is_none());
549 }
550
551 #[test]
552 fn analyzer_duplicate_styles() {
553 let script_text = r"
554[V4+ Styles]
555Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
556Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
557Style: Default,Times,24,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,1,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
558";
559
560 let script = crate::parser::Script::parse(script_text).unwrap();
561 let analyzer = StyleAnalyzer::new(&script);
562
563 let conflicts = analyzer.conflicts();
564 assert!(!conflicts.is_empty());
565 }
566
567 #[test]
568 fn analyzer_no_styles_section() {
569 let script_text = r"
570[Script Info]
571Title: Test Script
572";
573
574 let script = crate::parser::Script::parse(script_text).unwrap();
575 let analyzer = StyleAnalyzer::new(&script);
576
577 assert_eq!(analyzer.resolved_styles().len(), 0);
578 assert!(analyzer.conflicts().is_empty());
579 }
580
581 #[test]
582 fn analyzer_empty_styles_section() {
583 let script_text = r"
584[V4+ Styles]
585Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
586";
587
588 let script = crate::parser::Script::parse(script_text).unwrap();
589 let analyzer = StyleAnalyzer::new(&script);
590
591 assert_eq!(analyzer.resolved_styles().len(), 0);
592 assert!(analyzer.conflicts().is_empty());
593 }
594
595 #[test]
596 fn analyzer_extract_styles() {
597 let script_text = r"
598[V4+ Styles]
599Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
600Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
601";
602
603 let script = crate::parser::Script::parse(script_text).unwrap();
604 let analyzer = StyleAnalyzer::new(&script);
605
606 let styles = analyzer.extract_styles();
607 assert!(styles.is_some());
608 assert_eq!(styles.unwrap().len(), 1);
609 }
610
611 #[test]
612 fn analyzer_extract_styles_no_section() {
613 let script_text = r"
614[Script Info]
615Title: Test Script
616";
617
618 let script = crate::parser::Script::parse(script_text).unwrap();
619 let analyzer = StyleAnalyzer::new(&script);
620
621 let styles = analyzer.extract_styles();
622 assert!(styles.is_none());
623 }
624
625 #[test]
626 fn analyzer_inheritance_info() {
627 let script_text = r"
628[V4+ Styles]
629Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
630Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
631Style: Title,Arial,32,&H00FFFF00,&H000000FF,&H00000000,&H00000000,1,0,0,0,100,100,0,0,1,3,0,2,20,20,20,1
632";
633
634 let script = crate::parser::Script::parse(script_text).unwrap();
635 let analyzer = StyleAnalyzer::new(&script);
636
637 let inheritance_info = analyzer.inheritance_info();
638 assert_eq!(inheritance_info.len(), 2);
639 assert!(inheritance_info.contains_key("Default"));
640 assert!(inheritance_info.contains_key("Title"));
641 }
642
643 #[test]
644 fn analyzer_validate_styles() {
645 let script_text = r"
646[V4+ Styles]
647Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
648Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
649Style: Large,Arial,60,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,5,0,2,10,10,10,1
650";
651
652 let script = crate::parser::Script::parse(script_text).unwrap();
653 let analyzer = StyleAnalyzer::new(&script);
654
655 let issues = analyzer.validate_styles();
656 assert!(issues.is_empty() || !issues.is_empty());
658 }
659
660 #[test]
661 fn analyzer_strict_validation() {
662 let script_text = r"
663[V4+ Styles]
664Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
665Style: Large,Arial,250,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
666";
667
668 let script = crate::parser::Script::parse(script_text).unwrap();
669 let config = StyleAnalysisConfig {
670 options: AnalysisOptions::VALIDATION | AnalysisOptions::STRICT_VALIDATION,
671 performance_thresholds: PerformanceThresholds::default(),
672 };
673 let analyzer = StyleAnalyzer::new_with_config(&script, config);
674
675 let issues = analyzer.validate_styles();
676 assert!(!issues.is_empty());
678 }
679
680 #[test]
681 fn analyzer_performance_analysis() {
682 let script_text = r"
683[V4+ Styles]
684Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
685Style: Heavy,Arial,60,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,8,5,2,10,10,10,1
686";
687
688 let script = crate::parser::Script::parse(script_text).unwrap();
689 let config = StyleAnalysisConfig {
690 options: AnalysisOptions::PERFORMANCE,
691 performance_thresholds: PerformanceThresholds {
692 large_font_threshold: 30.0,
693 large_outline_threshold: 2.0,
694 large_shadow_threshold: 2.0,
695 scaling_threshold: 150.0,
696 },
697 };
698 let analyzer = StyleAnalyzer::new_with_config(&script, config);
699
700 let issues = analyzer.validate_styles();
701 assert!(!issues.is_empty());
703 }
704
705 #[test]
706 fn analyzer_options_flags() {
707 let options = AnalysisOptions::INHERITANCE | AnalysisOptions::CONFLICTS;
708 assert!(options.contains(AnalysisOptions::INHERITANCE));
709 assert!(options.contains(AnalysisOptions::CONFLICTS));
710 assert!(!options.contains(AnalysisOptions::VALIDATION));
711 assert!(!options.contains(AnalysisOptions::PERFORMANCE));
712 assert!(!options.contains(AnalysisOptions::STRICT_VALIDATION));
713 }
714
715 #[test]
716 fn analyzer_options_debug() {
717 let options = AnalysisOptions::INHERITANCE;
718 let debug_str = format!("{options:?}");
719 assert!(debug_str.contains("INHERITANCE"));
720 }
721
722 #[test]
723 fn analyzer_config_debug() {
724 let config = StyleAnalysisConfig::default();
725 let debug_str = format!("{config:?}");
726 assert!(debug_str.contains("StyleAnalysisConfig"));
727 assert!(debug_str.contains("options"));
728 assert!(debug_str.contains("performance_thresholds"));
729 }
730
731 #[test]
732 fn analyzer_debug() {
733 let script_text = r"
734[V4+ Styles]
735Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
736Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
737";
738
739 let script = crate::parser::Script::parse(script_text).unwrap();
740 let analyzer = StyleAnalyzer::new(&script);
741 let debug_str = format!("{analyzer:?}");
742 assert!(debug_str.contains("StyleAnalyzer"));
743 }
744
745 #[test]
746 fn performance_thresholds_debug() {
747 let thresholds = PerformanceThresholds::default();
748 let debug_str = format!("{thresholds:?}");
749 assert!(debug_str.contains("PerformanceThresholds"));
750 assert!(debug_str.contains("large_font_threshold"));
751 }
752
753 #[test]
754 fn config_clone() {
755 let config = StyleAnalysisConfig::default();
756 let cloned = config.clone();
757 assert_eq!(config.options, cloned.options);
758 assert!(
759 (config.performance_thresholds.large_font_threshold
760 - cloned.performance_thresholds.large_font_threshold)
761 .abs()
762 < f32::EPSILON
763 );
764 }
765
766 #[test]
767 fn performance_thresholds_clone() {
768 let thresholds = PerformanceThresholds::default();
769 let cloned = thresholds.clone();
770 assert!(
771 (thresholds.large_font_threshold - cloned.large_font_threshold).abs() < f32::EPSILON
772 );
773 assert!(
774 (thresholds.large_outline_threshold - cloned.large_outline_threshold).abs()
775 < f32::EPSILON
776 );
777 assert!(
778 (thresholds.large_shadow_threshold - cloned.large_shadow_threshold).abs()
779 < f32::EPSILON
780 );
781 assert!((thresholds.scaling_threshold - cloned.scaling_threshold).abs() < f32::EPSILON);
782 }
783
784 #[test]
785 fn analyzer_minimal_options() {
786 let script_text = r"
787[V4+ Styles]
788Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
789Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
790";
791
792 let script = crate::parser::Script::parse(script_text).unwrap();
793 let config = StyleAnalysisConfig {
794 options: AnalysisOptions::empty(),
795 performance_thresholds: PerformanceThresholds::default(),
796 };
797 let analyzer = StyleAnalyzer::new_with_config(&script, config);
798
799 assert_eq!(analyzer.resolved_styles().len(), 1);
800 assert!(analyzer.inheritance_info().is_empty());
801 assert!(analyzer.conflicts().is_empty());
802 }
803
804 #[test]
805 fn analyzer_style_inheritance_basic() {
806 let script_text = r"
807[V4+ Styles]
808Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
809Style: BaseStyle,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
810Style: *BaseStyle,DerivedStyle,Verdana,24,&HFF00FFFF,&H000000FF,&H00000000,&H00000000,1,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
811";
812
813 let script = crate::parser::Script::parse(script_text).unwrap();
814 let analyzer = StyleAnalyzer::new(&script);
815
816 assert_eq!(analyzer.resolved_styles().len(), 2);
817
818 let base_style = analyzer.resolve_style("BaseStyle").unwrap();
819 assert_eq!(base_style.font_name(), "Arial");
820 assert!((base_style.font_size() - 20.0).abs() < f32::EPSILON);
821 assert!(!base_style.is_bold());
822
823 let derived_style = analyzer.resolve_style("DerivedStyle").unwrap();
824 assert_eq!(derived_style.font_name(), "Verdana");
825 assert!((derived_style.font_size() - 24.0).abs() < f32::EPSILON);
826 assert!(derived_style.is_bold());
827 assert_eq!(derived_style.primary_color(), [255, 255, 0, 255]); assert_eq!(
830 derived_style.secondary_color(),
831 base_style.secondary_color()
832 );
833 assert_eq!(derived_style.outline_color(), base_style.outline_color());
834 }
835
836 #[test]
837 fn analyzer_style_inheritance_partial_override() {
838 let script_text = r"
839[V4+ Styles]
840Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
841Style: BaseStyle,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,1,1,0,0,100,100,0,0,1,2,3,2,10,10,10,1
842Style: *BaseStyle,DerivedStyle,Verdana,24,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,1,0,0,0,100,100,0,0,1,2,3,2,10,10,10,1
843";
844
845 let script = crate::parser::Script::parse(script_text).unwrap();
846 let analyzer = StyleAnalyzer::new(&script);
847
848 let base_style = analyzer.resolve_style("BaseStyle").unwrap();
849 let derived_style = analyzer.resolve_style("DerivedStyle").unwrap();
850
851 assert_eq!(derived_style.font_name(), "Verdana");
853 assert!((derived_style.font_size() - 24.0).abs() < f32::EPSILON);
855 assert_eq!(derived_style.primary_color(), base_style.primary_color());
857 assert!(derived_style.is_bold());
859 assert!(!derived_style.is_italic());
860 assert!((derived_style.shadow() - 3.0).abs() < f32::EPSILON);
862 }
863
864 #[test]
865 fn analyzer_style_inheritance_chain() {
866 let script_text = r"
867[V4+ Styles]
868Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
869Style: GrandParent,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
870Style: *GrandParent,Parent,Verdana,24,&H00FFFF00,&H000000FF,&H00000000,&H00000000,1,0,0,0,100,100,0,0,1,3,0,2,15,15,15,1
871Style: *Parent,Child,Times,28,&H00FF00FF,&H000000FF,&H00000000,&H00000000,1,1,0,0,100,100,0,0,1,4,0,2,20,20,20,1
872";
873
874 let script = crate::parser::Script::parse(script_text).unwrap();
875 let analyzer = StyleAnalyzer::new(&script);
876
877 assert_eq!(analyzer.resolved_styles().len(), 3);
878
879 let grandparent = analyzer.resolve_style("GrandParent").unwrap();
880 let parent = analyzer.resolve_style("Parent").unwrap();
881 let child = analyzer.resolve_style("Child").unwrap();
882
883 assert_eq!(grandparent.font_name(), "Arial");
885 assert!((grandparent.font_size() - 20.0).abs() < f32::EPSILON);
886 assert!(!grandparent.is_bold());
887 assert!((grandparent.outline() - 2.0).abs() < f32::EPSILON);
888
889 assert_eq!(parent.font_name(), "Verdana");
891 assert!((parent.font_size() - 24.0).abs() < f32::EPSILON);
892 assert!(parent.is_bold());
893 assert!((parent.outline() - 3.0).abs() < f32::EPSILON);
894 assert_eq!(parent.margin_l(), 15);
895
896 assert_eq!(child.font_name(), "Times");
898 assert!((child.font_size() - 28.0).abs() < f32::EPSILON);
899 assert!(child.is_bold());
900 assert!(child.is_italic());
901 assert!((child.outline() - 4.0).abs() < f32::EPSILON);
902 assert_eq!(child.margin_l(), 20);
903 }
904
905 #[test]
906 fn analyzer_style_inheritance_missing_parent() {
907 let script_text = r"
908[V4+ Styles]
909Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
910Style: *NonExistent,Orphan,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
911";
912
913 let script = crate::parser::Script::parse(script_text).unwrap();
914 let analyzer = StyleAnalyzer::new(&script);
915
916 assert_eq!(analyzer.resolved_styles().len(), 1);
918 let orphan = analyzer.resolve_style("Orphan").unwrap();
919 assert_eq!(orphan.font_name(), "Arial");
920
921 let conflicts = analyzer.conflicts();
923 assert!(!conflicts.is_empty());
924 assert!(conflicts
925 .iter()
926 .any(|c| matches!(c.conflict_type, ConflictType::MissingReference)));
927 }
928
929 #[test]
930 fn analyzer_style_circular_inheritance() {
931 let script_text = r"
932[V4+ Styles]
933Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
934Style: *StyleB,StyleA,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
935Style: *StyleA,StyleB,Verdana,24,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
936";
937
938 let script = crate::parser::Script::parse(script_text).unwrap();
939 let analyzer = StyleAnalyzer::new(&script);
940
941 assert_eq!(analyzer.resolved_styles().len(), 2);
943
944 let conflicts = analyzer.conflicts();
946 assert!(conflicts
947 .iter()
948 .any(|c| matches!(c.conflict_type, ConflictType::CircularInheritance)));
949 }
950
951 #[test]
952 fn analyzer_style_self_inheritance() {
953 let script_text = r"
954[V4+ Styles]
955Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
956Style: *SelfRef,SelfRef,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
957";
958
959 let script = crate::parser::Script::parse(script_text).unwrap();
960 let analyzer = StyleAnalyzer::new(&script);
961
962 assert_eq!(analyzer.resolved_styles().len(), 1);
964
965 let conflicts = analyzer.conflicts();
967 assert!(conflicts
968 .iter()
969 .any(|c| matches!(c.conflict_type, ConflictType::CircularInheritance)));
970 }
971
972 #[test]
973 fn analyzer_inheritance_info_tracking() {
974 let script_text = r"
975[V4+ Styles]
976Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
977Style: BaseStyle,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
978Style: *BaseStyle,Child1,Verdana,24,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
979Style: *BaseStyle,Child2,Times,18,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
980";
981
982 let script = crate::parser::Script::parse(script_text).unwrap();
983 let analyzer = StyleAnalyzer::new(&script);
984
985 let inheritance_info = analyzer.inheritance_info();
986 assert_eq!(inheritance_info.len(), 3);
987
988 let base_info = inheritance_info.get("BaseStyle").unwrap();
990 assert!(base_info.is_root());
991 assert!(base_info.parents.is_empty());
992
993 let child1_info = inheritance_info.get("Child1").unwrap();
995 assert!(!child1_info.is_root());
996 assert_eq!(child1_info.parents.len(), 1);
997 assert_eq!(child1_info.parents[0], "BaseStyle");
998
999 let child2_info = inheritance_info.get("Child2").unwrap();
1001 assert!(!child2_info.is_root());
1002 assert_eq!(child2_info.parents.len(), 1);
1003 assert_eq!(child2_info.parents[0], "BaseStyle");
1004 }
1005
1006 #[test]
1007 fn analyzer_layout_resolution_scaling() {
1008 let script_text = r"
1009[Script Info]
1010Title: Resolution Scaling Test
1011LayoutResX: 640
1012LayoutResY: 480
1013PlayResX: 1280
1014PlayResY: 960
1015
1016[V4+ Styles]
1017Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
1018Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,2,0,1,4,2,2,10,10,20,1
1019";
1020
1021 let script = crate::parser::Script::parse(script_text).unwrap();
1022 let analyzer = StyleAnalyzer::new(&script);
1023
1024 let default_style = analyzer.resolve_style("Default").unwrap();
1025 assert!((default_style.font_size() - 40.0).abs() < f32::EPSILON); assert!((default_style.spacing() - 4.0).abs() < f32::EPSILON); assert!((default_style.outline() - 8.0).abs() < f32::EPSILON); assert!((default_style.shadow() - 4.0).abs() < f32::EPSILON); assert_eq!(default_style.margin_l(), 20); assert_eq!(default_style.margin_r(), 20); assert_eq!(default_style.margin_t(), 40); assert_eq!(default_style.margin_b(), 40); }
1035
1036 #[test]
1037 fn analyzer_layout_resolution_scaling_asymmetric() {
1038 let script_text = r"
1039[Script Info]
1040Title: Asymmetric Resolution Scaling Test
1041LayoutResX: 640
1042LayoutResY: 480
1043PlayResX: 1920
1044PlayResY: 1080
1045
1046[V4+ Styles]
1047Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
1048Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,2,0,1,4,2,2,10,10,20,1
1049";
1050
1051 let script = crate::parser::Script::parse(script_text).unwrap();
1052 let analyzer = StyleAnalyzer::new(&script);
1053
1054 let default_style = analyzer.resolve_style("Default").unwrap();
1055 let avg_scale = 2.625;
1057 assert!((20.0f32.mul_add(-avg_scale, default_style.font_size())).abs() < 0.01);
1058 assert!((default_style.spacing() - 6.0).abs() < f32::EPSILON); assert!((4.0f32.mul_add(-avg_scale, default_style.outline())).abs() < 0.01);
1060 assert!((2.0f32.mul_add(-avg_scale, default_style.shadow())).abs() < 0.01);
1061 assert_eq!(default_style.margin_l(), 30); assert_eq!(default_style.margin_r(), 30); assert_eq!(default_style.margin_t(), 45); assert_eq!(default_style.margin_b(), 45); }
1066
1067 #[test]
1068 fn analyzer_no_resolution_scaling_when_same() {
1069 let script_text = r"
1070[Script Info]
1071Title: No Scaling Test
1072LayoutResX: 1920
1073LayoutResY: 1080
1074PlayResX: 1920
1075PlayResY: 1080
1076
1077[V4+ Styles]
1078Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
1079Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,2,0,1,4,2,2,10,10,20,1
1080";
1081
1082 let script = crate::parser::Script::parse(script_text).unwrap();
1083 let analyzer = StyleAnalyzer::new(&script);
1084
1085 let default_style = analyzer.resolve_style("Default").unwrap();
1086 assert!((default_style.font_size() - 20.0).abs() < f32::EPSILON);
1088 assert!((default_style.spacing() - 2.0).abs() < f32::EPSILON);
1089 assert!((default_style.outline() - 4.0).abs() < f32::EPSILON);
1090 assert!((default_style.shadow() - 2.0).abs() < f32::EPSILON);
1091 assert_eq!(default_style.margin_l(), 10);
1092 assert_eq!(default_style.margin_r(), 10);
1093 assert_eq!(default_style.margin_t(), 20);
1094 assert_eq!(default_style.margin_b(), 20);
1095 }
1096
1097 #[test]
1098 fn analyzer_resolution_scaling_with_inheritance() {
1099 let script_text = r"
1100[Script Info]
1101Title: Scaling with Inheritance Test
1102LayoutResX: 640
1103LayoutResY: 480
1104PlayResX: 1280
1105PlayResY: 960
1106
1107[V4+ Styles]
1108Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
1109Style: Base,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,2,0,1,4,2,2,10,10,20,1
1110Style: *Base,Derived,Verdana,24,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,2,15,15,20,1
1111";
1112
1113 let script = crate::parser::Script::parse(script_text).unwrap();
1114 let analyzer = StyleAnalyzer::new(&script);
1115
1116 let base_style = analyzer.resolve_style("Base").unwrap();
1117 let derived_style = analyzer.resolve_style("Derived").unwrap();
1118
1119 assert!((base_style.font_size() - 40.0).abs() < f32::EPSILON);
1121
1122 assert!((derived_style.font_size() - 48.0).abs() < f32::EPSILON);
1124 assert_eq!(derived_style.margin_l(), 30); assert_eq!(derived_style.margin_r(), 30); assert_eq!(derived_style.margin_t(), 40); assert_eq!(derived_style.margin_b(), 40); }
1131
1132 #[test]
1133 fn analyzer_no_resolution_info_no_scaling() {
1134 let script_text = r"
1135[Script Info]
1136Title: No Resolution Info Test
1137
1138[V4+ Styles]
1139Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
1140Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,2,0,1,4,2,2,10,10,20,1
1141";
1142
1143 let script = crate::parser::Script::parse(script_text).unwrap();
1144 let analyzer = StyleAnalyzer::new(&script);
1145
1146 let default_style = analyzer.resolve_style("Default").unwrap();
1147 assert!((default_style.font_size() - 20.0).abs() < f32::EPSILON);
1149 assert!((default_style.spacing() - 2.0).abs() < f32::EPSILON);
1150 assert!((default_style.outline() - 4.0).abs() < f32::EPSILON);
1151 assert!((default_style.shadow() - 2.0).abs() < f32::EPSILON);
1152 assert_eq!(default_style.margin_l(), 10);
1153 assert_eq!(default_style.margin_r(), 10);
1154 assert_eq!(default_style.margin_t(), 20);
1155 assert_eq!(default_style.margin_b(), 20);
1156 }
1157
1158 #[test]
1159 fn analyzer_partial_resolution_info_no_scaling() {
1160 let script_text = r"
1161[Script Info]
1162Title: Partial Resolution Info Test
1163LayoutResX: 640
1164PlayResY: 960
1165
1166[V4+ Styles]
1167Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
1168Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,2,0,1,4,2,2,10,10,20,1
1169";
1170
1171 let script = crate::parser::Script::parse(script_text).unwrap();
1172 let analyzer = StyleAnalyzer::new(&script);
1173
1174 let default_style = analyzer.resolve_style("Default").unwrap();
1175 assert!((default_style.font_size() - 20.0).abs() < f32::EPSILON);
1177 assert!((default_style.spacing() - 2.0).abs() < f32::EPSILON);
1178 assert!((default_style.outline() - 4.0).abs() < f32::EPSILON);
1179 assert!((default_style.shadow() - 2.0).abs() < f32::EPSILON);
1180 assert_eq!(default_style.margin_l(), 10);
1181 assert_eq!(default_style.margin_r(), 10);
1182 assert_eq!(default_style.margin_t(), 20);
1183 assert_eq!(default_style.margin_b(), 20);
1184 }
1185}