Skip to main content

lv_tui/
style_parser.rs

1use crate::style::{Border, Color, Layout, Length, Style};
2
3/// CSS pseudo-class for state-based styling.
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5pub enum PseudoClass {
6    Focus,
7    Hover,
8    Disabled,
9    FocusWithin,
10}
11
12/// Component state used to resolve pseudo-class selectors.
13#[derive(Debug, Clone, Copy, Default)]
14pub struct WidgetState {
15    pub focused: bool,
16    pub hovered: bool,
17    pub disabled: bool,
18    pub focus_within: bool,
19}
20
21/// A CSS-like selector that matches components by type, class, id, and pseudo-class.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct Selector {
24    pub type_name: Option<String>,
25    pub class: Option<String>,
26    pub id: Option<String>,
27    pub pseudo_classes: Vec<PseudoClass>,
28}
29
30impl Selector {
31    fn new_type(name: String) -> Self {
32        Selector { type_name: Some(name), class: None, id: None, pseudo_classes: Vec::new() }
33    }
34    fn new_class(name: String) -> Self {
35        Selector { type_name: None, class: Some(name), id: None, pseudo_classes: Vec::new() }
36    }
37    fn new_id(name: String) -> Self {
38        Selector { type_name: None, class: None, id: Some(name), pseudo_classes: Vec::new() }
39    }
40
41    fn specificity(&self) -> u8 {
42        let mut score: u8 = 0;
43        if self.type_name.is_some() { score += 1; }
44        if self.class.is_some() { score += 10; }
45        if self.id.is_some() { score += 100; }
46        score += self.pseudo_classes.len() as u8;
47        score
48    }
49}
50
51/// A single style rule pairing a selector with its declarations.
52#[derive(Debug, Clone)]
53pub struct StyleRule {
54    /// The selector that determines which components this rule applies to.
55    pub selector: Selector,
56    /// The style properties to apply.
57    pub style: Style,
58}
59
60/// A parsed stylesheet containing an ordered list of rules.
61///
62/// Use [`StyleSheet::parse`] to parse a CSS-like string, then
63/// [`StyleSheet::resolve`] to compute the effective style for a component.
64#[derive(Debug, Clone, Default)]
65pub struct StyleSheet {
66    /// Rules in source order (higher-index rules override earlier ones
67    /// for the same specificity).
68    pub rules: Vec<StyleRule>,
69}
70
71impl StyleSheet {
72    /// Returns all parsed rules.
73    pub fn rules(&self) -> &[StyleRule] {
74        &self.rules
75    }
76
77    /// Parses a stylesheet string into a [`StyleSheet`].
78    pub fn parse(input: &str) -> Result<Self, String> {
79        let mut parser = Parser::new(input);
80        parser.parse()
81    }
82
83    /// Resolves the effective style for a component by matching its type name,
84    /// optional id, optional class, and pseudo-class state against all rules.
85    ///
86    /// Specificity order: type < class < id. Pseudo-class rules have higher
87    /// specificity than non-pseudo-class rules. Later rules override earlier
88    /// ones at the same specificity.
89    pub fn resolve(
90        &self,
91        type_name: &str,
92        id: Option<&str>,
93        class: Option<&str>,
94        state: &WidgetState,
95    ) -> Style {
96        let mut resolved = Style::default();
97
98        // Sort rules by specificity for correct cascade
99        let mut matching: Vec<&StyleRule> = Vec::new();
100        for rule in &self.rules {
101            let sel = &rule.selector;
102            // Check type
103            if let Some(ref t) = sel.type_name {
104                if t != type_name { continue; }
105            }
106            // Check class
107            if let Some(ref c) = sel.class {
108                if class != Some(c.as_str()) { continue; }
109            }
110            // Check id
111            if let Some(ref i) = sel.id {
112                if id != Some(i.as_str()) { continue; }
113            }
114            // Check pseudo-classes — ALL must match
115            if !sel.pseudo_classes.iter().all(|pc| match pc {
116                PseudoClass::Focus => state.focused,
117                PseudoClass::Hover => state.hovered,
118                PseudoClass::Disabled => state.disabled,
119                PseudoClass::FocusWithin => state.focus_within,
120            }) {
121                continue;
122            }
123            matching.push(rule);
124        }
125
126        // Apply in specificity order (lower first, so higher wins)
127        matching.sort_by_key(|r| r.selector.specificity());
128        for rule in matching {
129            resolved = merge_styles(resolved, &rule.style);
130        }
131
132        resolved
133    }
134}
135
136/// Merges styles for inheritance: parent provides defaults, child overrides.
137pub fn inherit_style(parent: &Style, child: &Style) -> Style {
138    Style {
139        fg: child.fg.or(parent.fg),
140        bg: child.bg.or(parent.bg),
141        bold: child.bold || parent.bold,
142        italic: child.italic || parent.italic,
143        underline: child.underline || parent.underline,
144        width: if child.width != Length::Auto { child.width } else { parent.width },
145        height: if child.height != Length::Auto { child.height } else { parent.height },
146        padding: if child.padding != crate::geom::Insets::ZERO { child.padding } else { parent.padding },
147        margin: if child.margin != crate::geom::Insets::ZERO { child.margin } else { parent.margin },
148        layout: if child.layout != Layout::None { child.layout } else { parent.layout },
149        gap: if child.gap != 0 { child.gap } else { parent.gap },
150        flex_grow: if child.flex_grow != 0 { child.flex_grow } else { parent.flex_grow },
151        flex_shrink: child.flex_shrink && parent.flex_shrink,
152        border: if child.border != Border::None { child.border } else { parent.border },
153    }
154}
155
156/// Merges two styles: `override_` values win over `base` where set.
157pub fn merge_styles(base: Style, override_: &Style) -> Style {
158    Style {
159        fg: override_.fg.or(base.fg),
160        bg: override_.bg.or(base.bg),
161        bold: override_.bold || base.bold,
162        italic: override_.italic || base.italic,
163        underline: override_.underline || base.underline,
164        width: if override_.width != Length::Auto { override_.width } else { base.width },
165        height: if override_.height != Length::Auto { override_.height } else { base.height },
166        padding: if override_.padding != crate::geom::Insets::ZERO { override_.padding } else { base.padding },
167        margin: if override_.margin != crate::geom::Insets::ZERO { override_.margin } else { base.margin },
168        layout: if override_.layout != Layout::None { override_.layout } else { base.layout },
169        gap: if override_.gap != 0 { override_.gap } else { base.gap },
170        flex_grow: if override_.flex_grow != 0 { override_.flex_grow } else { base.flex_grow },
171        flex_shrink: override_.flex_shrink && base.flex_shrink,
172        border: if override_.border != Border::None { override_.border } else { base.border },
173        ..base
174    }
175}
176
177struct Parser<'a> {
178    chars: std::iter::Peekable<std::str::Chars<'a>>,
179    pos: usize,
180}
181
182impl<'a> Parser<'a> {
183    fn new(input: &'a str) -> Self {
184        Self {
185            chars: input.chars().peekable(),
186            pos: 0,
187        }
188    }
189
190    fn parse(&mut self) -> Result<StyleSheet, String> {
191        let mut rules = Vec::new();
192        self.skip_whitespace_and_comments();
193        while self.chars.peek().is_some() {
194            rules.push(self.parse_rule()?);
195            self.skip_whitespace_and_comments();
196        }
197        Ok(StyleSheet { rules })
198    }
199
200    fn parse_rule(&mut self) -> Result<StyleRule, String> {
201        let selector = self.parse_selector()?;
202        self.skip_whitespace();
203        self.expect('{')?;
204        let style = self.parse_declarations()?;
205        self.expect('}')?;
206        Ok(StyleRule { selector, style })
207    }
208
209    fn parse_selector(&mut self) -> Result<Selector, String> {
210        let next = self.peek_char().ok_or("expected selector")?;
211        let mut selector = match next {
212            '.' => {
213                self.advance();
214                let name = self.parse_ident()?;
215                Selector::new_class(name)
216            }
217            '#' => {
218                self.advance();
219                let name = self.parse_ident()?;
220                Selector::new_id(name)
221            }
222            c if c.is_alphabetic() || c == '_' || c == '-' => {
223                let name = self.parse_ident()?;
224                Selector::new_type(name)
225            }
226            _ => return Err(format!("unexpected char '{}' in selector", next)),
227        };
228
229        // Parse optional pseudo-classes: :focus, :hover, etc.
230        while self.peek_char() == Some(':') {
231            self.advance(); // consume ':'
232            let pc_name = self.parse_ident()?;
233            match pc_name.as_str() {
234                "focus" => selector.pseudo_classes.push(PseudoClass::Focus),
235                "hover" => selector.pseudo_classes.push(PseudoClass::Hover),
236                "disabled" => selector.pseudo_classes.push(PseudoClass::Disabled),
237                "focus-within" => selector.pseudo_classes.push(PseudoClass::FocusWithin),
238                _ => return Err(format!("unknown pseudo-class ':{}'", pc_name)),
239            }
240        }
241
242        Ok(selector)
243    }
244
245    fn parse_declarations(&mut self) -> Result<Style, String> {
246        let mut style = Style::default();
247        loop {
248            self.skip_whitespace();
249            if self.peek_char() == Some('}') || self.peek_char().is_none() {
250                break;
251            }
252            let prop = self.parse_ident()?;
253            self.skip_whitespace();
254            self.expect(':')?;
255            self.skip_whitespace();
256            let value = self.parse_value(&prop)?;
257            self.apply_property(&mut style, &prop, &value);
258            self.skip_whitespace();
259            if self.peek_char() == Some(';') {
260                self.advance();
261            }
262        }
263        Ok(style)
264    }
265
266    fn parse_value(&mut self, _prop: &str) -> Result<String, String> {
267        let mut val = String::new();
268        while let Some(&c) = self.chars.peek() {
269            if c == ';' || c == '}' || c == '\n' {
270                break;
271            }
272            val.push(c);
273            self.advance();
274        }
275        Ok(val.trim().to_string())
276    }
277
278    fn apply_property(&self, style: &mut Style, prop: &str, value: &str) {
279        match prop {
280            "fg" | "color" => {
281                if let Some(c) = parse_color(value) {
282                    style.fg = Some(c);
283                }
284            }
285            "bg" | "background" => {
286                if let Some(c) = parse_color(value) {
287                    style.bg = Some(c);
288                }
289            }
290            "bold" => style.bold = value == "true",
291            "italic" => style.italic = value == "true",
292            "underline" => style.underline = value == "true",
293            "padding" => {
294                if let Ok(n) = value.parse::<u16>() {
295                    style.padding = crate::geom::Insets::all(n);
296                }
297            }
298            "margin" => {
299                if let Ok(n) = value.parse::<u16>() {
300                    style.margin = crate::geom::Insets::all(n);
301                }
302            }
303            "gap" => {
304                if let Ok(n) = value.parse::<u16>() {
305                    style.gap = n;
306                }
307            }
308            "width" => style.width = parse_length(value),
309            "height" => style.height = parse_length(value),
310            "layout" => {
311                style.layout = match value {
312                    "vertical" | "column" => Layout::Vertical,
313                    "horizontal" | "row" => Layout::Horizontal,
314                    _ => Layout::None,
315                }
316            }
317            "border" => {
318                style.border = match value {
319                    "plain" => Border::Plain,
320                    "rounded" => Border::Rounded,
321                    "double" => Border::Double,
322                    _ => Border::None,
323                }
324            }
325            _ => {} // 忽略未知属性
326        }
327    }
328
329    fn parse_ident(&mut self) -> Result<String, String> {
330        let mut ident = String::new();
331        while let Some(&c) = self.chars.peek() {
332            if c.is_alphanumeric() || c == '_' || c == '-' {
333                ident.push(c);
334                self.advance();
335            } else {
336                break;
337            }
338        }
339        if ident.is_empty() {
340            Err("expected identifier".into())
341        } else {
342            Ok(ident)
343        }
344    }
345
346    fn skip_whitespace(&mut self) {
347        while let Some(&c) = self.chars.peek() {
348            if c.is_whitespace() {
349                self.advance();
350            } else {
351                break;
352            }
353        }
354    }
355
356    fn skip_whitespace_and_comments(&mut self) {
357        loop {
358            self.skip_whitespace();
359            // Skip // line comments
360            if self.peek_char() == Some('/') {
361                // Peek next
362                let mut iter = self.chars.clone();
363                iter.next();
364                if iter.next() == Some('/') {
365                    // Skip comment until newline
366                    while let Some(&c) = self.chars.peek() {
367                        self.advance();
368                        if c == '\n' {
369                            break;
370                        }
371                    }
372                    continue;
373                }
374            }
375            break;
376        }
377    }
378
379    fn peek_char(&mut self) -> Option<char> {
380        self.chars.peek().copied()
381    }
382
383    fn advance(&mut self) -> Option<char> {
384        self.pos += 1;
385        self.chars.next()
386    }
387
388    fn expect(&mut self, expected: char) -> Result<(), String> {
389        match self.chars.peek() {
390            Some(&c) if c == expected => {
391                self.advance();
392                Ok(())
393            }
394            Some(&c) => Err(format!("expected '{}', found '{}'", expected, c)),
395            None => Err(format!("expected '{}', found EOF", expected)),
396        }
397    }
398}
399
400fn parse_color(s: &str) -> Option<Color> {
401    match s {
402        "black" => Some(Color::Black),
403        "red" => Some(Color::Red),
404        "green" => Some(Color::Green),
405        "yellow" => Some(Color::Yellow),
406        "blue" => Some(Color::Blue),
407        "magenta" => Some(Color::Magenta),
408        "cyan" => Some(Color::Cyan),
409        "white" => Some(Color::White),
410        "gray" | "grey" => Some(Color::Gray),
411        _ => None,
412    }
413}
414
415fn parse_length(s: &str) -> Length {
416    if s == "auto" {
417        return Length::Auto;
418    }
419    if let Some(pct) = s.strip_suffix('%') {
420        if let Ok(n) = pct.parse::<u16>() {
421            return Length::Percent(n);
422        }
423    }
424    if let Some(frac) = s.strip_suffix("fr") {
425        if let Ok(n) = frac.parse::<u16>() {
426            return Length::Fraction(n);
427        }
428    }
429    if let Ok(n) = s.parse::<u16>() {
430        return Length::Fixed(n);
431    }
432    Length::Auto
433}
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438
439    #[test]
440    fn test_parse_simple() {
441        let css = "Counter { fg: green; padding: 1; }";
442        let sheet = StyleSheet::parse(css).unwrap();
443        assert_eq!(sheet.rules.len(), 1);
444        assert_eq!(sheet.rules[0].selector.type_name, Some("Counter".into()));
445        assert_eq!(sheet.rules[0].style.fg, Some(Color::Green));
446        assert_eq!(sheet.rules[0].style.padding, crate::geom::Insets::all(1));
447    }
448
449    #[test]
450    fn test_parse_class_id() {
451        let css = ".card { border: rounded; } #header { bold: true; }";
452        let sheet = StyleSheet::parse(css).unwrap();
453        assert_eq!(sheet.rules.len(), 2);
454        assert_eq!(sheet.rules[0].selector.class, Some("card".into()));
455        assert_eq!(sheet.rules[1].selector.id, Some("header".into()));
456    }
457
458    #[test]
459    fn test_parse_focus_pseudo_class() {
460        let css = "Button:focus { fg: blue; }";
461        let sheet = StyleSheet::parse(css).unwrap();
462        assert_eq!(sheet.rules.len(), 1);
463        assert_eq!(sheet.rules[0].selector.type_name, Some("Button".into()));
464        assert_eq!(sheet.rules[0].selector.pseudo_classes.len(), 1);
465    }
466
467    #[test]
468    fn test_parse_hover_pseudo_class() {
469        let css = "Button:hover { bg: gray; }";
470        let sheet = StyleSheet::parse(css).unwrap();
471        assert_eq!(sheet.rules[0].selector.pseudo_classes.len(), 1);
472    }
473
474    #[test]
475    fn test_parse_multiple_pseudo_classes() {
476        let css = "Button:focus:hover { fg: white; }";
477        let sheet = StyleSheet::parse(css).unwrap();
478        assert_eq!(sheet.rules[0].selector.pseudo_classes.len(), 2);
479    }
480
481    #[test]
482    fn test_resolve_focus_override() {
483        let sheet = StyleSheet::parse(
484            "Button { fg: red; } Button:focus { fg: blue; }"
485        ).unwrap();
486
487        let unfocused = WidgetState::default();
488        let focused = WidgetState { focused: true, ..Default::default() };
489
490        let s1 = sheet.resolve("Button", None, None, &unfocused);
491        let s2 = sheet.resolve("Button", None, None, &focused);
492        assert_eq!(s1.fg, Some(Color::Red));
493        assert_eq!(s2.fg, Some(Color::Blue));
494    }
495
496    #[test]
497    fn test_resolve_pseudo_class_higher_priority() {
498        let sheet = StyleSheet::parse(
499            "Widget { bg: black; } Widget:focus { bg: white; }"
500        ).unwrap();
501        let state = WidgetState { focused: true, ..Default::default() };
502        let style = sheet.resolve("Widget", None, None, &state);
503        assert_eq!(style.bg, Some(Color::White));
504    }
505}