Skip to main content

iris_cssom/
computed.rs

1//! Style computation: cascade, inheritance, and resolved values.
2
3use crate::StyleSheet;
4use std::collections::HashMap;
5
6/// Resolved, computed styles for a single element.
7#[derive(Debug, Clone, Default)]
8pub struct ComputedStyle {
9    pub properties: HashMap<String, String>,
10}
11
12impl ComputedStyle {
13    pub fn get(&self, prop: &str) -> Option<&str> {
14        self.properties.get(prop).map(|s| s.as_str())
15    }
16
17    pub fn set(&mut self, prop: &str, value: &str) {
18        self.properties.insert(prop.to_string(), value.to_string());
19    }
20}
21
22/// Compute styles for a set of classes + tag, cascading from multiple stylesheets.
23pub fn compute_styles(
24    sheets: &[StyleSheet],
25    classes: &[String],
26    tag: &str,
27    parent_style: Option<&ComputedStyle>,
28) -> ComputedStyle {
29    let mut map = HashMap::new();
30
31    // 1. Parent inherited properties
32    if let Some(parent) = parent_style {
33        for (prop, val) in &parent.properties {
34            if INHERITED_PROPS.contains(&prop.as_str()) {
35                map.entry(prop.clone()).or_insert_with(|| val.clone());
36            }
37        }
38    }
39
40    // 2. Tag selectors (lowest priority)
41    for sheet in sheets {
42        for decl in sheet.declarations_for_tag(tag) {
43            map.entry(decl.property.clone()).or_insert(decl.value);
44        }
45    }
46
47    // 3. Class selectors (higher priority)
48    for sheet in sheets {
49        for class in classes {
50            for decl in sheet.declarations_for_class(class) {
51                map.insert(decl.property, decl.value);
52            }
53        }
54    }
55
56    ComputedStyle { properties: map }
57}
58
59/// Properties that inherit from parent by default.
60const INHERITED_PROPS: &[&str] = &[
61    "color", "font-family", "font-size", "font-style", "font-weight",
62    "line-height", "text-align", "visibility", "cursor",
63    "direction", "letter-spacing", "word-spacing", "white-space",
64];
65
66/// Parse a CSS pixel value (e.g., "16px" → 16.0).
67pub fn parse_px(value: &str) -> f32 {
68    let value = value.trim();
69    if value.ends_with("px") {
70        value[..value.len()-2].trim().parse().unwrap_or(0.0)
71    } else {
72        value.parse().unwrap_or(0.0)
73    }
74}
75
76/// Convert CSS color string to RGBA tuple (r, g, b, a) in 0-1 range.
77/// Supports: #hex, rgb(), rgba(), named colors.
78pub fn parse_color(value: &str) -> (f32, f32, f32, f32) {
79    let value = value.trim().to_lowercase();
80
81    // Named colors (simplified)
82    if let Some(rgba) = named_color(&value) { return rgba; }
83
84    // #hex
85    if value.starts_with('#') {
86        return parse_hex_color(&value);
87    }
88
89    // rgb() / rgba()
90    if value.starts_with("rgb") {
91        let inner = value.trim_start_matches("rgba(").trim_start_matches("rgb(")
92            .trim_end_matches(')');
93        let parts: Vec<f32> = inner.split(',').filter_map(|s| {
94            let trimmed = s.trim();
95            if trimmed.ends_with('%') {
96                trimmed[..trimmed.len()-1].parse::<f32>().ok().map(|v| v / 100.0 * 255.0)
97            } else {
98                trimmed.parse::<f32>().ok()
99            }
100        }).collect();
101        if parts.len() >= 3 {
102            return (parts[0] / 255.0, parts[1] / 255.0, parts[2] / 255.0,
103                    parts.get(3).copied().unwrap_or(255.0) / 255.0);
104        }
105    }
106
107    (0.0, 0.0, 0.0, 1.0) // default black
108}
109
110fn parse_hex_color(hex: &str) -> (f32, f32, f32, f32) {
111    let hex = hex.trim_start_matches('#');
112    // 支持 3 位 hex: #rgb → #rrggbb
113    let expanded;
114    let hex = if hex.len() == 3 {
115        expanded = hex.chars()
116            .flat_map(|c| std::iter::repeat(c).take(2))
117            .collect::<String>();
118        expanded.as_str()
119    } else {
120        hex
121    };
122    // 支持 4 位 hex: #rgba → #rrggbbaa
123    let expanded4;
124    let hex = if hex.len() == 4 {
125        expanded4 = hex.chars()
126            .flat_map(|c| std::iter::repeat(c).take(2))
127            .collect::<String>();
128        expanded4.as_str()
129    } else {
130        hex
131    };
132    if hex.len() < 6 {
133        return (0.0, 0.0, 0.0, 1.0);
134    }
135    let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0) as f32 / 255.0;
136    let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0) as f32 / 255.0;
137    let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0) as f32 / 255.0;
138    let a = if hex.len() >= 8 {
139        u8::from_str_radix(&hex[6..8], 16).unwrap_or(255) as f32 / 255.0
140    } else { 1.0 };
141    (r, g, b, a)
142}
143
144fn named_color(name: &str) -> Option<(f32, f32, f32, f32)> {
145    // ✅ 统一颜色名列表,与 iris-engine 保持一致
146    let (r, g, b, has_alpha) = match name {
147        "red" => (1.0, 0.0, 0.0, false),
148        "green" => (0.0, 0.502, 0.0, false), // ✅ 统一为 0.502(与 engine 一致)
149        "blue" => (0.0, 0.0, 1.0, false),
150        "white" => (1.0, 1.0, 1.0, false),
151        "black" => (0.0, 0.0, 0.0, false),
152        "gray" | "grey" => (0.5, 0.5, 0.5, false),
153        "silver" => (0.75, 0.75, 0.75, false),
154        "yellow" => (1.0, 1.0, 0.0, false),
155        "orange" => (1.0, 0.65, 0.0, false),
156        "purple" => (0.5, 0.0, 0.5, false),
157        "pink" => (1.0, 0.75, 0.8, false),
158        "brown" => (0.65, 0.16, 0.16, false),
159        "transparent" => (0.0, 0.0, 0.0, true),
160        // ✅ 新增与 engine 一致的颜色
161        "cyan" => (0.0, 1.0, 1.0, false),
162        "magenta" => (1.0, 0.0, 1.0, false),
163        "maroon" => (0.5, 0.0, 0.0, false),
164        "olive" => (0.5, 0.5, 0.0, false),
165        "lime" => (0.0, 1.0, 0.0, false),
166        "teal" => (0.0, 0.5, 0.5, false),
167        "navy" => (0.0, 0.0, 0.5, false),
168        "coral" => (1.0, 0.5, 0.31, false),
169        "crimson" => (0.86, 0.08, 0.24, false),
170        "darkgray" => (0.66, 0.66, 0.66, false),
171        "darkgreen" => (0.0, 0.39, 0.0, false),
172        "darkblue" => (0.0, 0.0, 0.55, false),
173        "darkred" => (0.55, 0.0, 0.0, false),
174        _ => return None,
175    };
176    Some((r, g, b, if has_alpha { 0.0 } else { 1.0 }))
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn test_parse_px() {
185        assert_eq!(parse_px("16px"), 16.0);
186        assert_eq!(parse_px("0"), 0.0);
187    }
188
189    #[test]
190    fn test_parse_hex_color() {
191        let (r, g, b, a) = parse_color("#ff0000");
192        assert!((r - 1.0).abs() < 0.01);
193        assert!((g - 0.0).abs() < 0.01);
194        assert!((a - 1.0).abs() < 0.01);
195    }
196
197    #[test]
198    fn test_parse_named_color() {
199        let (r, g, b, _) = parse_color("red");
200        assert!((r - 1.0).abs() < 0.01);
201    }
202
203    #[test]
204    fn test_compute_styles() {
205        let css = ".btn { color: red; } .primary { color: blue; font-weight: bold; }";
206        let sheet = crate::parser::parse_css_simple(css);
207        let style = compute_styles(&[sheet], &["btn".into(), "primary".into()], "button", None);
208        assert_eq!(style.get("color").unwrap(), "blue"); // primary wins over btn
209        assert_eq!(style.get("font-weight").unwrap(), "bold");
210    }
211
212    #[test]
213    fn test_inherited_props() {
214        let parent = {
215            let mut s = ComputedStyle::default();
216            s.set("color", "red");
217            s.set("margin", "10px"); // not inherited
218            s
219        };
220        let sheet = StyleSheet::default();
221        let child = compute_styles(&[sheet], &[], "span", Some(&parent));
222        assert_eq!(child.get("color").unwrap(), "red"); // inherited
223        assert!(child.get("margin").is_none()); // not inherited
224    }
225}