⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/browser/
style.rs

1//! Style Resolution
2//!
3//! Resolves computed styles for DOM nodes by cascading CSS rules,
4//! sorting by specificity, and inheriting inheritable properties
5//! from parent nodes. Includes user-agent default styles.
6
7#![allow(dead_code)]
8
9use alloc::{string::String, vec::Vec};
10
11use super::{
12    css_parser::{
13        named_color, px_to_fp, CssValue, Declaration, FixedPoint, Selector, SimpleSelector,
14        Specificity, Stylesheet,
15    },
16    dom::{Document, NodeId, NodeType},
17};
18
19/// CSS display property
20#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
21pub enum Display {
22    Block,
23    #[default]
24    Inline,
25    InlineBlock,
26    None,
27    Flex,
28    Table,
29    TableRow,
30    TableCell,
31    TableHeaderGroup,
32    TableRowGroup,
33    TableFooterGroup,
34    ListItem,
35}
36
37/// CSS position property
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
39pub enum Position {
40    #[default]
41    Static,
42    Relative,
43    Absolute,
44    Fixed,
45}
46
47/// CSS float property
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
49pub enum Float {
50    #[default]
51    None,
52    Left,
53    Right,
54}
55
56/// CSS clear property
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
58pub enum Clear {
59    #[default]
60    None,
61    Left,
62    Right,
63    Both,
64}
65
66/// CSS text-align property
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
68pub enum TextAlign {
69    #[default]
70    Left,
71    Center,
72    Right,
73    Justify,
74}
75
76/// CSS overflow property
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
78pub enum Overflow {
79    #[default]
80    Visible,
81    Hidden,
82    Scroll,
83    Auto,
84}
85
86/// CSS visibility property
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
88pub enum Visibility {
89    #[default]
90    Visible,
91    Hidden,
92    Collapse,
93}
94
95/// CSS border style
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
97pub enum BorderStyle {
98    #[default]
99    None,
100    Solid,
101    Dashed,
102    Dotted,
103    Double,
104    Groove,
105    Ridge,
106    Inset,
107    Outset,
108}
109
110/// CSS white-space property
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
112pub enum WhiteSpace {
113    #[default]
114    Normal,
115    Nowrap,
116    Pre,
117    PreWrap,
118    PreLine,
119}
120
121/// Computed style for a DOM node (all values resolved to final values)
122#[derive(Debug, Clone)]
123pub struct ComputedStyle {
124    pub display: Display,
125    pub position: Position,
126    pub width: Option<FixedPoint>,
127    pub height: Option<FixedPoint>,
128    pub min_width: FixedPoint,
129    pub min_height: FixedPoint,
130    pub max_width: Option<FixedPoint>,
131    pub max_height: Option<FixedPoint>,
132    pub margin_top: FixedPoint,
133    pub margin_right: FixedPoint,
134    pub margin_bottom: FixedPoint,
135    pub margin_left: FixedPoint,
136    pub padding_top: FixedPoint,
137    pub padding_right: FixedPoint,
138    pub padding_bottom: FixedPoint,
139    pub padding_left: FixedPoint,
140    pub color: u32,
141    pub background_color: u32,
142    pub font_size: FixedPoint,
143    pub font_weight: u16,
144    pub text_align: TextAlign,
145    pub line_height: FixedPoint,
146    pub border_top_width: FixedPoint,
147    pub border_right_width: FixedPoint,
148    pub border_bottom_width: FixedPoint,
149    pub border_left_width: FixedPoint,
150    pub border_top_color: u32,
151    pub border_right_color: u32,
152    pub border_bottom_color: u32,
153    pub border_left_color: u32,
154    pub border_top_style: BorderStyle,
155    pub border_right_style: BorderStyle,
156    pub border_bottom_style: BorderStyle,
157    pub border_left_style: BorderStyle,
158    pub overflow: Overflow,
159    pub visibility: Visibility,
160    pub opacity: u8,
161    pub float: Float,
162    pub clear: Clear,
163    pub z_index: i32,
164    pub text_decoration_underline: bool,
165    pub text_decoration_line_through: bool,
166    pub white_space: WhiteSpace,
167    pub list_style_type: Option<String>,
168}
169
170impl Default for ComputedStyle {
171    fn default() -> Self {
172        Self {
173            display: Display::Inline,
174            position: Position::Static,
175            width: None,
176            height: None,
177            min_width: 0,
178            min_height: 0,
179            max_width: None,
180            max_height: None,
181            margin_top: 0,
182            margin_right: 0,
183            margin_bottom: 0,
184            margin_left: 0,
185            padding_top: 0,
186            padding_right: 0,
187            padding_bottom: 0,
188            padding_left: 0,
189            color: 0xFF000000,            // black
190            background_color: 0x00000000, // transparent
191            font_size: px_to_fp(16),      // 16px default
192            font_weight: 400,             // normal
193            text_align: TextAlign::Left,
194            line_height: px_to_fp(20), // ~1.25 * 16px
195            border_top_width: 0,
196            border_right_width: 0,
197            border_bottom_width: 0,
198            border_left_width: 0,
199            border_top_color: 0xFF000000,
200            border_right_color: 0xFF000000,
201            border_bottom_color: 0xFF000000,
202            border_left_color: 0xFF000000,
203            border_top_style: BorderStyle::None,
204            border_right_style: BorderStyle::None,
205            border_bottom_style: BorderStyle::None,
206            border_left_style: BorderStyle::None,
207            overflow: Overflow::Visible,
208            visibility: Visibility::Visible,
209            opacity: 255,
210            float: Float::None,
211            clear: Clear::None,
212            z_index: 0,
213            text_decoration_underline: false,
214            text_decoration_line_through: false,
215            white_space: WhiteSpace::Normal,
216            list_style_type: None,
217        }
218    }
219}
220
221/// A matched rule with its specificity for sorting
222#[derive(Debug)]
223struct MatchedRule {
224    specificity: Specificity,
225    declarations: Vec<Declaration>,
226    important: bool,
227}
228
229/// Style resolver that applies CSS rules to DOM nodes
230pub struct StyleResolver {
231    pub stylesheets: Vec<Stylesheet>,
232    pub ua_stylesheet: Stylesheet,
233}
234
235impl Default for StyleResolver {
236    fn default() -> Self {
237        Self::new()
238    }
239}
240
241impl StyleResolver {
242    /// Create a new style resolver with user-agent defaults
243    pub fn new() -> Self {
244        Self {
245            stylesheets: Vec::new(),
246            ua_stylesheet: Self::default_ua_stylesheet(),
247        }
248    }
249
250    /// Add a stylesheet
251    pub fn add_stylesheet(&mut self, stylesheet: Stylesheet) {
252        self.stylesheets.push(stylesheet);
253    }
254
255    /// Generate user-agent default stylesheet
256    fn default_ua_stylesheet() -> Stylesheet {
257        use super::css_parser::CssParser;
258        CssParser::parse(
259            "html, body, div, section, article, aside, nav, header, footer, main, address, \
260             blockquote, figure, figcaption, details, summary, ul, ol, dl, pre, fieldset, form, \
261             hgroup, p, h1, h2, h3, h4, h5, h6 { display: block; }
262             li { display: list-item; }
263             table { display: table; }
264             thead { display: table-header-group; }
265             tbody { display: table-row-group; }
266             tfoot { display: table-footer-group; }
267             tr { display: table-row; }
268             td, th { display: table-cell; }
269             head, script, style, link, meta, title { display: none; }
270             b, strong { font-weight: bold; }
271             i, em { font-style: italic; }
272             h1 { font-size: 32px; font-weight: bold; margin-top: 21px; margin-bottom: 21px; }
273             h2 { font-size: 24px; font-weight: bold; margin-top: 19px; margin-bottom: 19px; }
274             h3 { font-size: 18px; font-weight: bold; margin-top: 18px; margin-bottom: 18px; }
275             h4 { font-size: 16px; font-weight: bold; margin-top: 21px; margin-bottom: 21px; }
276             h5 { font-size: 13px; font-weight: bold; margin-top: 22px; margin-bottom: 22px; }
277             h6 { font-size: 10px; font-weight: bold; margin-top: 24px; margin-bottom: 24px; }
278             p { margin-top: 16px; margin-bottom: 16px; }
279             body { margin-top: 8px; margin-right: 8px; margin-bottom: 8px; margin-left: 8px; }
280             ul, ol { margin-top: 16px; margin-bottom: 16px; padding-left: 40px; }
281             a { color: #0000ee; }
282             u { color: inherit; }
283             pre, code { font-family: monospace; }
284            ",
285        )
286    }
287
288    /// Resolve the computed style for a node
289    pub fn resolve(
290        &self,
291        doc: &Document,
292        node_id: NodeId,
293        parent_style: Option<&ComputedStyle>,
294    ) -> ComputedStyle {
295        let node = match doc.arena.get(node_id) {
296            Some(n) => n,
297            None => return ComputedStyle::default(),
298        };
299
300        // Non-element nodes get default or inherited style
301        if node.node_type != NodeType::Element {
302            let mut style = ComputedStyle::default();
303            if let Some(ps) = parent_style {
304                Self::inherit(&mut style, ps);
305            }
306            return style;
307        }
308
309        // Start with defaults
310        let mut style = ComputedStyle::default();
311
312        // Cascade: collect matching rules sorted by specificity
313        let matched = self.cascade(doc, node_id);
314
315        // Apply declarations in order of specificity
316        for decl in &matched {
317            Self::apply_declaration(&mut style, decl);
318        }
319
320        // Inherit from parent
321        if let Some(ps) = parent_style {
322            Self::inherit_if_not_set(&mut style, ps);
323        }
324
325        style
326    }
327
328    /// Collect all matching declarations, sorted by specificity
329    fn cascade(&self, doc: &Document, node_id: NodeId) -> Vec<Declaration> {
330        let mut matched_rules: Vec<MatchedRule> = Vec::new();
331
332        // UA stylesheet
333        self.collect_matching_rules(&self.ua_stylesheet, doc, node_id, &mut matched_rules);
334
335        // Author stylesheets
336        for ss in &self.stylesheets {
337            self.collect_matching_rules(ss, doc, node_id, &mut matched_rules);
338        }
339
340        // Sort by specificity (stable sort preserves source order)
341        matched_rules.sort_by(|a, b| a.specificity.cmp(&b.specificity));
342
343        // Flatten declarations, !important last
344        let mut normal = Vec::new();
345        let mut important = Vec::new();
346        for rule in matched_rules {
347            for decl in rule.declarations {
348                if decl.important {
349                    important.push(decl);
350                } else {
351                    normal.push(decl);
352                }
353            }
354        }
355        normal.extend(important);
356        normal
357    }
358
359    fn collect_matching_rules(
360        &self,
361        stylesheet: &Stylesheet,
362        doc: &Document,
363        node_id: NodeId,
364        matched: &mut Vec<MatchedRule>,
365    ) {
366        for rule in &stylesheet.rules {
367            for selector in &rule.selectors {
368                if Self::selector_matches(selector, doc, node_id) {
369                    matched.push(MatchedRule {
370                        specificity: selector.specificity(),
371                        declarations: rule.declarations.clone(),
372                        important: false,
373                    });
374                    break; // Only match first selector in the group
375                }
376            }
377        }
378    }
379
380    /// Check if a selector matches a node
381    fn selector_matches(selector: &Selector, doc: &Document, node_id: NodeId) -> bool {
382        match selector {
383            Selector::Universal => true,
384            Selector::Tag(tag) => doc.tag_name(node_id) == Some(tag.as_str()),
385            Selector::Id(id) => doc.get_attribute(node_id, "id").as_deref() == Some(id.as_str()),
386            Selector::Class(class) => doc
387                .arena
388                .get(node_id)
389                .and_then(|n| n.element_data.as_ref())
390                .map(|ed| ed.has_class(class))
391                .unwrap_or(false),
392            Selector::Simple(simple) => Self::simple_selector_matches(simple, doc, node_id),
393            Selector::Descendant(parts) => Self::descendant_matches(parts, doc, node_id),
394            Selector::Child(parts) => Self::child_matches(parts, doc, node_id),
395            Selector::Compound(parts) => parts
396                .iter()
397                .all(|p| Self::selector_matches(p, doc, node_id)),
398        }
399    }
400
401    fn simple_selector_matches(sel: &SimpleSelector, doc: &Document, node_id: NodeId) -> bool {
402        if let Some(ref tag) = sel.tag_name {
403            if doc.tag_name(node_id) != Some(tag.as_str()) {
404                return false;
405            }
406        }
407        if let Some(ref id) = sel.id {
408            if doc.get_attribute(node_id, "id").as_deref() != Some(id.as_str()) {
409                return false;
410            }
411        }
412        for class in &sel.classes {
413            let has = doc
414                .arena
415                .get(node_id)
416                .and_then(|n| n.element_data.as_ref())
417                .map(|ed| ed.has_class(class))
418                .unwrap_or(false);
419            if !has {
420                return false;
421            }
422        }
423        true
424    }
425
426    fn descendant_matches(parts: &[Selector], doc: &Document, node_id: NodeId) -> bool {
427        if parts.is_empty() {
428            return false;
429        }
430
431        // Last part must match current node
432        if !Self::selector_matches(&parts[parts.len() - 1], doc, node_id) {
433            return false;
434        }
435
436        if parts.len() == 1 {
437            return true;
438        }
439
440        // Walk ancestors to match remaining parts
441        let remaining = &parts[..parts.len() - 1];
442        let ancestors = doc.ancestors(node_id);
443        for &ancestor_id in &ancestors {
444            if Self::descendant_matches(remaining, doc, ancestor_id) {
445                return true;
446            }
447        }
448        false
449    }
450
451    fn child_matches(parts: &[Selector], doc: &Document, node_id: NodeId) -> bool {
452        if parts.is_empty() {
453            return false;
454        }
455
456        if !Self::selector_matches(&parts[parts.len() - 1], doc, node_id) {
457            return false;
458        }
459
460        if parts.len() == 1 {
461            return true;
462        }
463
464        // Direct parent must match
465        if let Some(node) = doc.arena.get(node_id) {
466            if let Some(parent_id) = node.parent {
467                return Self::child_matches(&parts[..parts.len() - 1], doc, parent_id);
468            }
469        }
470        false
471    }
472
473    /// Apply a CSS declaration to a computed style
474    fn apply_declaration(style: &mut ComputedStyle, decl: &Declaration) {
475        match decl.property.as_str() {
476            "display" => {
477                style.display = match &decl.value {
478                    CssValue::Keyword(k) => match k.as_str() {
479                        "block" => Display::Block,
480                        "inline" => Display::Inline,
481                        "inline-block" => Display::InlineBlock,
482                        "flex" => Display::Flex,
483                        "table" => Display::Table,
484                        "table-row" => Display::TableRow,
485                        "table-cell" => Display::TableCell,
486                        "table-header-group" => Display::TableHeaderGroup,
487                        "table-row-group" => Display::TableRowGroup,
488                        "table-footer-group" => Display::TableFooterGroup,
489                        "list-item" => Display::ListItem,
490                        _ => Display::Block,
491                    },
492                    CssValue::None => Display::None,
493                    _ => style.display,
494                };
495            }
496            "position" => {
497                if let CssValue::Keyword(k) = &decl.value {
498                    style.position = match k.as_str() {
499                        "static" => Position::Static,
500                        "relative" => Position::Relative,
501                        "absolute" => Position::Absolute,
502                        "fixed" => Position::Fixed,
503                        _ => style.position,
504                    };
505                }
506            }
507            "width" => {
508                style.width = Self::resolve_length(&decl.value);
509            }
510            "height" => {
511                style.height = Self::resolve_length(&decl.value);
512            }
513            "min-width" => {
514                if let Some(v) = Self::resolve_length(&decl.value) {
515                    style.min_width = v;
516                }
517            }
518            "min-height" => {
519                if let Some(v) = Self::resolve_length(&decl.value) {
520                    style.min_height = v;
521                }
522            }
523            "max-width" => {
524                style.max_width = Self::resolve_length(&decl.value);
525            }
526            "max-height" => {
527                style.max_height = Self::resolve_length(&decl.value);
528            }
529            "margin" => {
530                if let Some(v) = Self::resolve_length_or_zero(&decl.value) {
531                    style.margin_top = v;
532                    style.margin_right = v;
533                    style.margin_bottom = v;
534                    style.margin_left = v;
535                }
536            }
537            "margin-top" => {
538                if let Some(v) = Self::resolve_length_or_zero(&decl.value) {
539                    style.margin_top = v;
540                }
541            }
542            "margin-right" => {
543                if let Some(v) = Self::resolve_length_or_zero(&decl.value) {
544                    style.margin_right = v;
545                }
546            }
547            "margin-bottom" => {
548                if let Some(v) = Self::resolve_length_or_zero(&decl.value) {
549                    style.margin_bottom = v;
550                }
551            }
552            "margin-left" => {
553                if let Some(v) = Self::resolve_length_or_zero(&decl.value) {
554                    style.margin_left = v;
555                }
556            }
557            "padding" => {
558                if let Some(v) = Self::resolve_length_or_zero(&decl.value) {
559                    style.padding_top = v;
560                    style.padding_right = v;
561                    style.padding_bottom = v;
562                    style.padding_left = v;
563                }
564            }
565            "padding-top" => {
566                if let Some(v) = Self::resolve_length_or_zero(&decl.value) {
567                    style.padding_top = v;
568                }
569            }
570            "padding-right" => {
571                if let Some(v) = Self::resolve_length_or_zero(&decl.value) {
572                    style.padding_right = v;
573                }
574            }
575            "padding-bottom" => {
576                if let Some(v) = Self::resolve_length_or_zero(&decl.value) {
577                    style.padding_bottom = v;
578                }
579            }
580            "padding-left" => {
581                if let Some(v) = Self::resolve_length_or_zero(&decl.value) {
582                    style.padding_left = v;
583                }
584            }
585            "color" => {
586                if let Some(c) = Self::resolve_color(&decl.value) {
587                    style.color = c;
588                }
589            }
590            "background-color" | "background" => {
591                if let Some(c) = Self::resolve_color(&decl.value) {
592                    style.background_color = c;
593                }
594            }
595            "font-size" => {
596                if let Some(v) = Self::resolve_length_or_zero(&decl.value) {
597                    style.font_size = v;
598                } else if let CssValue::Keyword(k) = &decl.value {
599                    style.font_size = match k.as_str() {
600                        "xx-small" => px_to_fp(9),
601                        "x-small" => px_to_fp(10),
602                        "small" => px_to_fp(13),
603                        "medium" => px_to_fp(16),
604                        "large" => px_to_fp(18),
605                        "x-large" => px_to_fp(24),
606                        "xx-large" => px_to_fp(32),
607                        _ => style.font_size,
608                    };
609                }
610            }
611            "font-weight" => match &decl.value {
612                CssValue::Keyword(k) => {
613                    style.font_weight = match k.as_str() {
614                        "normal" => 400,
615                        "bold" => 700,
616                        "lighter" => 100,
617                        "bolder" => 900,
618                        _ => style.font_weight,
619                    };
620                }
621                CssValue::Number(n) => {
622                    style.font_weight = (*n as u16).clamp(100, 900);
623                }
624                _ => {}
625            },
626            "text-align" => {
627                if let CssValue::Keyword(k) = &decl.value {
628                    style.text_align = match k.as_str() {
629                        "left" => TextAlign::Left,
630                        "center" => TextAlign::Center,
631                        "right" => TextAlign::Right,
632                        "justify" => TextAlign::Justify,
633                        _ => style.text_align,
634                    };
635                }
636            }
637            "line-height" => {
638                if let Some(v) = Self::resolve_length_or_zero(&decl.value) {
639                    style.line_height = v;
640                }
641            }
642            "border-width" => {
643                if let Some(v) = Self::resolve_length_or_zero(&decl.value) {
644                    style.border_top_width = v;
645                    style.border_right_width = v;
646                    style.border_bottom_width = v;
647                    style.border_left_width = v;
648                }
649            }
650            "border-color" => {
651                if let Some(c) = Self::resolve_color(&decl.value) {
652                    style.border_top_color = c;
653                    style.border_right_color = c;
654                    style.border_bottom_color = c;
655                    style.border_left_color = c;
656                }
657            }
658            "border-style" => {
659                if let CssValue::Keyword(k) = &decl.value {
660                    let bs = Self::parse_border_style(k);
661                    style.border_top_style = bs;
662                    style.border_right_style = bs;
663                    style.border_bottom_style = bs;
664                    style.border_left_style = bs;
665                }
666            }
667            "border" => {
668                // Shorthand: width style color
669                // Simplified: just handle color or width
670                if let Some(c) = Self::resolve_color(&decl.value) {
671                    style.border_top_color = c;
672                    style.border_right_color = c;
673                    style.border_bottom_color = c;
674                    style.border_left_color = c;
675                }
676                if let Some(v) = Self::resolve_length_or_zero(&decl.value) {
677                    style.border_top_width = v;
678                    style.border_right_width = v;
679                    style.border_bottom_width = v;
680                    style.border_left_width = v;
681                }
682            }
683            "overflow" => {
684                if let CssValue::Keyword(k) = &decl.value {
685                    style.overflow = match k.as_str() {
686                        "hidden" => Overflow::Hidden,
687                        "scroll" => Overflow::Scroll,
688                        "auto" => Overflow::Auto,
689                        _ => Overflow::Visible,
690                    };
691                }
692            }
693            "visibility" => {
694                if let CssValue::Keyword(k) = &decl.value {
695                    style.visibility = match k.as_str() {
696                        "hidden" => Visibility::Hidden,
697                        "collapse" => Visibility::Collapse,
698                        _ => Visibility::Visible,
699                    };
700                }
701            }
702            "opacity" => {
703                if let CssValue::Number(n) = &decl.value {
704                    style.opacity = *n as u8;
705                }
706            }
707            "float" => {
708                if let CssValue::Keyword(k) = &decl.value {
709                    style.float = match k.as_str() {
710                        "left" => Float::Left,
711                        "right" => Float::Right,
712                        _ => Float::None,
713                    };
714                }
715            }
716            "clear" => {
717                if let CssValue::Keyword(k) = &decl.value {
718                    style.clear = match k.as_str() {
719                        "left" => Clear::Left,
720                        "right" => Clear::Right,
721                        "both" => Clear::Both,
722                        _ => Clear::None,
723                    };
724                }
725            }
726            "z-index" => {
727                if let CssValue::Number(n) = &decl.value {
728                    style.z_index = *n;
729                }
730            }
731            "text-decoration" => {
732                if let CssValue::Keyword(k) = &decl.value {
733                    match k.as_str() {
734                        "underline" => style.text_decoration_underline = true,
735                        "line-through" => style.text_decoration_line_through = true,
736                        "none" => {
737                            style.text_decoration_underline = false;
738                            style.text_decoration_line_through = false;
739                        }
740                        _ => {}
741                    }
742                }
743            }
744            "white-space" => {
745                if let CssValue::Keyword(k) = &decl.value {
746                    style.white_space = match k.as_str() {
747                        "nowrap" => WhiteSpace::Nowrap,
748                        "pre" => WhiteSpace::Pre,
749                        "pre-wrap" => WhiteSpace::PreWrap,
750                        "pre-line" => WhiteSpace::PreLine,
751                        _ => WhiteSpace::Normal,
752                    };
753                }
754            }
755            "list-style-type" => {
756                if let CssValue::Keyword(k) = &decl.value {
757                    style.list_style_type = Some(k.clone());
758                }
759            }
760            // font-style handled as keyword passthrough (italic is a text property)
761            "font-style" | "font-family" => {
762                // Ignored for layout purposes
763            }
764            _ => {
765                // Unknown property, ignore
766            }
767        }
768    }
769
770    fn resolve_length(value: &CssValue) -> Option<FixedPoint> {
771        match value {
772            CssValue::Length(v, _) => Some(*v),
773            CssValue::Number(0) => Some(0),
774            CssValue::Auto => None,
775            CssValue::None => None,
776            CssValue::Percentage(v) => Some(*v), // Percentage stored as fixed-point
777            _ => None,
778        }
779    }
780
781    fn resolve_length_or_zero(value: &CssValue) -> Option<FixedPoint> {
782        match value {
783            CssValue::Length(v, _) => Some(*v),
784            CssValue::Number(n) => Some(px_to_fp(*n)),
785            CssValue::Percentage(v) => Some(*v),
786            _ => None,
787        }
788    }
789
790    fn resolve_color(value: &CssValue) -> Option<u32> {
791        match value {
792            CssValue::Color(c) => Some(*c),
793            CssValue::Keyword(k) => named_color(k),
794            _ => None,
795        }
796    }
797
798    fn parse_border_style(s: &str) -> BorderStyle {
799        match s {
800            "solid" => BorderStyle::Solid,
801            "dashed" => BorderStyle::Dashed,
802            "dotted" => BorderStyle::Dotted,
803            "double" => BorderStyle::Double,
804            "groove" => BorderStyle::Groove,
805            "ridge" => BorderStyle::Ridge,
806            "inset" => BorderStyle::Inset,
807            "outset" => BorderStyle::Outset,
808            "none" => BorderStyle::None,
809            _ => BorderStyle::None,
810        }
811    }
812
813    /// Inherit inheritable properties from parent (always applied)
814    fn inherit(style: &mut ComputedStyle, parent: &ComputedStyle) {
815        style.color = parent.color;
816        style.font_size = parent.font_size;
817        style.font_weight = parent.font_weight;
818        style.line_height = parent.line_height;
819        style.text_align = parent.text_align;
820        style.visibility = parent.visibility;
821        style.white_space = parent.white_space;
822        style.list_style_type = parent.list_style_type.clone();
823    }
824
825    /// Inherit only if the property was not explicitly set (for cascade)
826    fn inherit_if_not_set(style: &mut ComputedStyle, parent: &ComputedStyle) {
827        // Color inherits unless explicitly set
828        // We treat 0xFF000000 (black) as the default
829        if style.color == 0xFF000000 && parent.color != 0xFF000000 {
830            style.color = parent.color;
831        }
832        // font-size: only inherit if still at default
833        if style.font_size == px_to_fp(16) && parent.font_size != px_to_fp(16) {
834            style.font_size = parent.font_size;
835        }
836        // line-height
837        if style.line_height == px_to_fp(20) && parent.line_height != px_to_fp(20) {
838            style.line_height = parent.line_height;
839        }
840        // text-align
841        if style.text_align == TextAlign::Left && parent.text_align != TextAlign::Left {
842            style.text_align = parent.text_align;
843        }
844        // visibility
845        if style.visibility == Visibility::Visible && parent.visibility != Visibility::Visible {
846            style.visibility = parent.visibility;
847        }
848        // white-space
849        if style.white_space == WhiteSpace::Normal && parent.white_space != WhiteSpace::Normal {
850            style.white_space = parent.white_space;
851        }
852        // list-style-type
853        if style.list_style_type.is_none() {
854            style.list_style_type = parent.list_style_type.clone();
855        }
856    }
857}
858
859#[cfg(test)]
860mod tests {
861    #[allow(unused_imports)]
862    use alloc::vec;
863
864    use super::{
865        super::{css_parser::CssParser, tree_builder::TreeBuilder},
866        *,
867    };
868
869    fn resolve_first_p(html: &str, css: &str) -> ComputedStyle {
870        let doc = TreeBuilder::build(html);
871        let stylesheet = CssParser::parse(css);
872        let mut resolver = StyleResolver::new();
873        resolver.add_stylesheet(stylesheet);
874        let ps = doc.get_elements_by_tag_name("p");
875        assert!(!ps.is_empty(), "No <p> found");
876        resolver.resolve(&doc, ps[0], None)
877    }
878
879    #[test]
880    fn test_default_style() {
881        let style = ComputedStyle::default();
882        assert_eq!(style.display, Display::Inline);
883        assert_eq!(style.color, 0xFF000000);
884        assert_eq!(style.font_size, px_to_fp(16));
885    }
886
887    #[test]
888    fn test_ua_block_display() {
889        let doc = TreeBuilder::build("<div>hello</div>");
890        let resolver = StyleResolver::new();
891        let divs = doc.get_elements_by_tag_name("div");
892        let style = resolver.resolve(&doc, divs[0], None);
893        assert_eq!(style.display, Display::Block);
894    }
895
896    #[test]
897    fn test_ua_heading_font_size() {
898        let doc = TreeBuilder::build("<h1>Title</h1>");
899        let resolver = StyleResolver::new();
900        let h1s = doc.get_elements_by_tag_name("h1");
901        let style = resolver.resolve(&doc, h1s[0], None);
902        assert_eq!(style.font_size, px_to_fp(32));
903        assert_eq!(style.font_weight, 700);
904    }
905
906    #[test]
907    fn test_ua_body_margin() {
908        let doc = TreeBuilder::build("<body></body>");
909        let resolver = StyleResolver::new();
910        let bodies = doc.get_elements_by_tag_name("body");
911        let style = resolver.resolve(&doc, bodies[0], None);
912        assert_eq!(style.margin_top, px_to_fp(8));
913    }
914
915    #[test]
916    fn test_color_override() {
917        let style = resolve_first_p("<p>text</p>", "p { color: #ff0000; }");
918        assert_eq!(style.color, 0xFFFF0000);
919    }
920
921    #[test]
922    fn test_display_none() {
923        let doc = TreeBuilder::build("<head><title>T</title></head>");
924        let resolver = StyleResolver::new();
925        let titles = doc.get_elements_by_tag_name("title");
926        if !titles.is_empty() {
927            let style = resolver.resolve(&doc, titles[0], None);
928            assert_eq!(style.display, Display::None);
929        }
930    }
931
932    #[test]
933    fn test_background_color() {
934        let style = resolve_first_p("<p>text</p>", "p { background-color: #00ff00; }");
935        assert_eq!(style.background_color, 0xFF00FF00);
936    }
937
938    #[test]
939    fn test_padding() {
940        let style = resolve_first_p("<p>text</p>", "p { padding: 10px; }");
941        assert_eq!(style.padding_top, px_to_fp(10));
942        assert_eq!(style.padding_right, px_to_fp(10));
943        assert_eq!(style.padding_bottom, px_to_fp(10));
944        assert_eq!(style.padding_left, px_to_fp(10));
945    }
946
947    #[test]
948    fn test_margin_individual() {
949        let style = resolve_first_p("<p>text</p>", "p { margin-left: 20px; }");
950        assert_eq!(style.margin_left, px_to_fp(20));
951    }
952
953    #[test]
954    fn test_font_weight_bold() {
955        let doc = TreeBuilder::build("<b>bold</b>");
956        let resolver = StyleResolver::new();
957        let bs = doc.get_elements_by_tag_name("b");
958        let style = resolver.resolve(&doc, bs[0], None);
959        assert_eq!(style.font_weight, 700);
960    }
961
962    #[test]
963    fn test_width_height() {
964        let style = resolve_first_p("<p>text</p>", "p { width: 200px; height: 100px; }");
965        assert_eq!(style.width, Some(px_to_fp(200)));
966        assert_eq!(style.height, Some(px_to_fp(100)));
967    }
968
969    #[test]
970    fn test_text_align() {
971        let style = resolve_first_p("<p>text</p>", "p { text-align: center; }");
972        assert_eq!(style.text_align, TextAlign::Center);
973    }
974
975    #[test]
976    fn test_float() {
977        let style = resolve_first_p("<p>text</p>", "p { float: left; }");
978        assert_eq!(style.float, Float::Left);
979    }
980
981    #[test]
982    fn test_position() {
983        let style = resolve_first_p("<p>text</p>", "p { position: absolute; }");
984        assert_eq!(style.position, Position::Absolute);
985    }
986
987    #[test]
988    fn test_border_style() {
989        let style = resolve_first_p("<p>text</p>", "p { border-style: solid; }");
990        assert_eq!(style.border_top_style, BorderStyle::Solid);
991    }
992
993    #[test]
994    fn test_overflow() {
995        let style = resolve_first_p("<p>text</p>", "p { overflow: hidden; }");
996        assert_eq!(style.overflow, Overflow::Hidden);
997    }
998
999    #[test]
1000    fn test_z_index() {
1001        let style = resolve_first_p("<p>text</p>", "p { z-index: 10; }");
1002        assert_eq!(style.z_index, 10);
1003    }
1004
1005    #[test]
1006    fn test_specificity_ordering() {
1007        let style = resolve_first_p(
1008            "<p id=\"x\">text</p>",
1009            "p { color: #ff0000; } #x { color: #0000ff; }",
1010        );
1011        // ID selector has higher specificity
1012        assert_eq!(style.color, 0xFF0000FF);
1013    }
1014
1015    #[test]
1016    fn test_named_color_resolution() {
1017        let style = resolve_first_p("<p>text</p>", "p { background-color: red; }");
1018        assert_eq!(style.background_color, 0xFFFF0000);
1019    }
1020
1021    #[test]
1022    fn test_class_selector_match() {
1023        let doc = TreeBuilder::build("<p class=\"highlight\">text</p>");
1024        let stylesheet = CssParser::parse(".highlight { color: #00ff00; }");
1025        let mut resolver = StyleResolver::new();
1026        resolver.add_stylesheet(stylesheet);
1027        let ps = doc.get_elements_by_tag_name("p");
1028        let style = resolver.resolve(&doc, ps[0], None);
1029        assert_eq!(style.color, 0xFF00FF00);
1030    }
1031
1032    #[test]
1033    fn test_visibility_hidden() {
1034        let style = resolve_first_p("<p>text</p>", "p { visibility: hidden; }");
1035        assert_eq!(style.visibility, Visibility::Hidden);
1036    }
1037
1038    #[test]
1039    fn test_white_space() {
1040        let style = resolve_first_p("<p>text</p>", "p { white-space: pre; }");
1041        assert_eq!(style.white_space, WhiteSpace::Pre);
1042    }
1043
1044    #[test]
1045    fn test_text_decoration() {
1046        let style = resolve_first_p("<p>text</p>", "p { text-decoration: underline; }");
1047        assert!(style.text_decoration_underline);
1048    }
1049
1050    #[test]
1051    fn test_default_resolver() {
1052        let _r = StyleResolver::default();
1053    }
1054}