1use crate::style::{Border, Color, Layout, Length, Style};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
5pub enum PseudoClass {
6 Focus,
7 Hover,
8 Disabled,
9 FocusWithin,
10}
11
12#[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#[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#[derive(Debug, Clone)]
53pub struct StyleRule {
54 pub selector: Selector,
56 pub style: Style,
58}
59
60#[derive(Debug, Clone, Default)]
65pub struct StyleSheet {
66 pub rules: Vec<StyleRule>,
69}
70
71impl StyleSheet {
72 pub fn rules(&self) -> &[StyleRule] {
74 &self.rules
75 }
76
77 pub fn parse(input: &str) -> Result<Self, String> {
79 let mut parser = Parser::new(input);
80 parser.parse()
81 }
82
83 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 let mut matching: Vec<&StyleRule> = Vec::new();
100 for rule in &self.rules {
101 let sel = &rule.selector;
102 if let Some(ref t) = sel.type_name {
104 if t != type_name { continue; }
105 }
106 if let Some(ref c) = sel.class {
108 if class != Some(c.as_str()) { continue; }
109 }
110 if let Some(ref i) = sel.id {
112 if id != Some(i.as_str()) { continue; }
113 }
114 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 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
136pub 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
156pub 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 while self.peek_char() == Some(':') {
231 self.advance(); 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 _ => {} }
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 if self.peek_char() == Some('/') {
361 let mut iter = self.chars.clone();
363 iter.next();
364 if iter.next() == Some('/') {
365 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}