//! CSS cascade and computed style resolution. //! //! For each DOM element, resolves the final computed value of every CSS property //! by collecting matching rules, applying the cascade (specificity + source order), //! handling property inheritance, and resolving relative values. use we_css::parser::{Declaration, Stylesheet}; use we_css::values::{expand_shorthand, parse_value, Color, CssValue, LengthUnit}; use we_dom::{Document, NodeData, NodeId}; use crate::matching::collect_matching_rules; // --------------------------------------------------------------------------- // Display // --------------------------------------------------------------------------- /// CSS `display` property values (subset). #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum Display { Block, #[default] Inline, None, } // --------------------------------------------------------------------------- // Position // --------------------------------------------------------------------------- /// CSS `position` property values. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum Position { #[default] Static, Relative, Absolute, Fixed, } // --------------------------------------------------------------------------- // FontWeight // --------------------------------------------------------------------------- /// CSS `font-weight` as a numeric value (100-900). #[derive(Debug, Clone, Copy, PartialEq)] pub struct FontWeight(pub f32); impl Default for FontWeight { fn default() -> Self { FontWeight(400.0) // normal } } // --------------------------------------------------------------------------- // FontStyle // --------------------------------------------------------------------------- #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum FontStyle { #[default] Normal, Italic, Oblique, } // --------------------------------------------------------------------------- // TextAlign // --------------------------------------------------------------------------- #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum TextAlign { #[default] Left, Right, Center, Justify, } // --------------------------------------------------------------------------- // TextDecoration // --------------------------------------------------------------------------- #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum TextDecoration { #[default] None, Underline, Overline, LineThrough, } // --------------------------------------------------------------------------- // Overflow // --------------------------------------------------------------------------- #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum Overflow { #[default] Visible, Hidden, Scroll, Auto, } // --------------------------------------------------------------------------- // Visibility // --------------------------------------------------------------------------- #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum Visibility { #[default] Visible, Hidden, Collapse, } // --------------------------------------------------------------------------- // BorderStyle // --------------------------------------------------------------------------- #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum BorderStyle { #[default] None, Hidden, Dotted, Dashed, Solid, Double, Groove, Ridge, Inset, Outset, } // --------------------------------------------------------------------------- // LengthOrAuto // --------------------------------------------------------------------------- /// A computed length (resolved to px) or `auto`. #[derive(Debug, Clone, Copy, PartialEq, Default)] pub enum LengthOrAuto { Length(f32), #[default] Auto, } // --------------------------------------------------------------------------- // ComputedStyle // --------------------------------------------------------------------------- /// The fully resolved computed style for a single element. #[derive(Debug, Clone, PartialEq)] pub struct ComputedStyle { // Display pub display: Display, // Box model: margin pub margin_top: LengthOrAuto, pub margin_right: LengthOrAuto, pub margin_bottom: LengthOrAuto, pub margin_left: LengthOrAuto, // Box model: padding (no auto for padding) pub padding_top: f32, pub padding_right: f32, pub padding_bottom: f32, pub padding_left: f32, // Box model: border width pub border_top_width: f32, pub border_right_width: f32, pub border_bottom_width: f32, pub border_left_width: f32, // Box model: border style pub border_top_style: BorderStyle, pub border_right_style: BorderStyle, pub border_bottom_style: BorderStyle, pub border_left_style: BorderStyle, // Box model: border color pub border_top_color: Color, pub border_right_color: Color, pub border_bottom_color: Color, pub border_left_color: Color, // Box model: dimensions pub width: LengthOrAuto, pub height: LengthOrAuto, // Text / inherited pub color: Color, pub font_size: f32, pub font_weight: FontWeight, pub font_style: FontStyle, pub font_family: String, pub text_align: TextAlign, pub text_decoration: TextDecoration, pub line_height: f32, // Background pub background_color: Color, // Position pub position: Position, pub top: LengthOrAuto, pub right: LengthOrAuto, pub bottom: LengthOrAuto, pub left: LengthOrAuto, // Overflow pub overflow: Overflow, // Visibility (inherited) pub visibility: Visibility, } impl Default for ComputedStyle { fn default() -> Self { ComputedStyle { display: Display::Inline, margin_top: LengthOrAuto::Length(0.0), margin_right: LengthOrAuto::Length(0.0), margin_bottom: LengthOrAuto::Length(0.0), margin_left: LengthOrAuto::Length(0.0), padding_top: 0.0, padding_right: 0.0, padding_bottom: 0.0, padding_left: 0.0, border_top_width: 0.0, border_right_width: 0.0, border_bottom_width: 0.0, border_left_width: 0.0, border_top_style: BorderStyle::None, border_right_style: BorderStyle::None, border_bottom_style: BorderStyle::None, border_left_style: BorderStyle::None, border_top_color: Color::rgb(0, 0, 0), border_right_color: Color::rgb(0, 0, 0), border_bottom_color: Color::rgb(0, 0, 0), border_left_color: Color::rgb(0, 0, 0), width: LengthOrAuto::Auto, height: LengthOrAuto::Auto, color: Color::rgb(0, 0, 0), font_size: 16.0, font_weight: FontWeight(400.0), font_style: FontStyle::Normal, font_family: String::new(), text_align: TextAlign::Left, text_decoration: TextDecoration::None, line_height: 19.2, // 1.2 * 16 background_color: Color::new(0, 0, 0, 0), // transparent position: Position::Static, top: LengthOrAuto::Auto, right: LengthOrAuto::Auto, bottom: LengthOrAuto::Auto, left: LengthOrAuto::Auto, overflow: Overflow::Visible, visibility: Visibility::Visible, } } } // --------------------------------------------------------------------------- // Property classification: inherited vs non-inherited // --------------------------------------------------------------------------- fn is_inherited_property(property: &str) -> bool { matches!( property, "color" | "font-size" | "font-weight" | "font-style" | "font-family" | "text-align" | "text-decoration" | "line-height" | "visibility" ) } // --------------------------------------------------------------------------- // User-agent stylesheet // --------------------------------------------------------------------------- /// Returns the user-agent default stylesheet. pub fn ua_stylesheet() -> Stylesheet { use we_css::parser::Parser; Parser::parse(UA_CSS) } const UA_CSS: &str = r#" html, body, div, p, pre, h1, h2, h3, h4, h5, h6, ul, ol, li, blockquote, section, article, nav, header, footer, main, hr { display: block; } span, a, em, strong, b, i, u, code, small, sub, sup, br { display: inline; } head, title, script, style, link, meta { display: none; } body { margin: 8px; } h1 { font-size: 2em; margin-top: 0.67em; margin-bottom: 0.67em; font-weight: bold; } h2 { font-size: 1.5em; margin-top: 0.83em; margin-bottom: 0.83em; font-weight: bold; } h3 { font-size: 1.17em; margin-top: 1em; margin-bottom: 1em; font-weight: bold; } h4 { font-size: 1em; margin-top: 1.33em; margin-bottom: 1.33em; font-weight: bold; } h5 { font-size: 0.83em; margin-top: 1.67em; margin-bottom: 1.67em; font-weight: bold; } h6 { font-size: 0.67em; margin-top: 2.33em; margin-bottom: 2.33em; font-weight: bold; } p { margin-top: 1em; margin-bottom: 1em; } strong, b { font-weight: bold; } em, i { font-style: italic; } a { color: blue; text-decoration: underline; } u { text-decoration: underline; } "#; // --------------------------------------------------------------------------- // Resolve a CssValue to f32 px given context // --------------------------------------------------------------------------- fn resolve_length(value: &CssValue, _parent_font_size: f32, current_font_size: f32) -> Option { match value { // Em units resolve relative to the element's own computed font-size // (for properties other than font-size, which has its own handling). CssValue::Length(n, unit) => Some(resolve_length_unit(*n, *unit, current_font_size)), CssValue::Percentage(p) => Some((*p / 100.0) as f32 * current_font_size), CssValue::Zero => Some(0.0), CssValue::Number(n) if *n == 0.0 => Some(0.0), _ => None, } } fn resolve_length_unit(value: f64, unit: LengthUnit, em_base: f32) -> f32 { let v = value as f32; match unit { LengthUnit::Px => v, LengthUnit::Em => v * em_base, LengthUnit::Rem => v * 16.0, // root font size is always 16px for now LengthUnit::Pt => v * (96.0 / 72.0), LengthUnit::Cm => v * (96.0 / 2.54), LengthUnit::Mm => v * (96.0 / 25.4), LengthUnit::In => v * 96.0, LengthUnit::Pc => v * 16.0, // Viewport units: use a reasonable default (can be parameterized later) LengthUnit::Vw | LengthUnit::Vh | LengthUnit::Vmin | LengthUnit::Vmax => v, } } fn resolve_length_or_auto( value: &CssValue, parent_font_size: f32, current_font_size: f32, ) -> LengthOrAuto { match value { CssValue::Auto => LengthOrAuto::Auto, _ => resolve_length(value, parent_font_size, current_font_size) .map(LengthOrAuto::Length) .unwrap_or(LengthOrAuto::Auto), } } fn resolve_color(value: &CssValue, current_color: Color) -> Option { match value { CssValue::Color(c) => Some(*c), CssValue::CurrentColor => Some(current_color), CssValue::Transparent => Some(Color::new(0, 0, 0, 0)), _ => None, } } // --------------------------------------------------------------------------- // Apply a single property value to a ComputedStyle // --------------------------------------------------------------------------- fn apply_property( style: &mut ComputedStyle, property: &str, value: &CssValue, parent: &ComputedStyle, ) { // Handle inherit/initial/unset match value { CssValue::Inherit => { inherit_property(style, property, parent); return; } CssValue::Initial => { reset_property_to_initial(style, property); return; } CssValue::Unset => { if is_inherited_property(property) { inherit_property(style, property, parent); } else { reset_property_to_initial(style, property); } return; } _ => {} } let parent_fs = parent.font_size; let current_fs = style.font_size; match property { "display" => { style.display = match value { CssValue::Keyword(k) => match k.as_str() { "block" => Display::Block, "inline" => Display::Inline, _ => Display::Block, }, CssValue::None => Display::None, _ => style.display, }; } // Margin "margin-top" => { style.margin_top = resolve_length_or_auto(value, parent_fs, current_fs); } "margin-right" => { style.margin_right = resolve_length_or_auto(value, parent_fs, current_fs); } "margin-bottom" => { style.margin_bottom = resolve_length_or_auto(value, parent_fs, current_fs); } "margin-left" => { style.margin_left = resolve_length_or_auto(value, parent_fs, current_fs); } // Padding "padding-top" => { if let Some(v) = resolve_length(value, parent_fs, current_fs) { style.padding_top = v; } } "padding-right" => { if let Some(v) = resolve_length(value, parent_fs, current_fs) { style.padding_right = v; } } "padding-bottom" => { if let Some(v) = resolve_length(value, parent_fs, current_fs) { style.padding_bottom = v; } } "padding-left" => { if let Some(v) = resolve_length(value, parent_fs, current_fs) { style.padding_left = v; } } // Border width "border-top-width" | "border-right-width" | "border-bottom-width" | "border-left-width" => { let w = resolve_border_width(value, parent_fs); match property { "border-top-width" => style.border_top_width = w, "border-right-width" => style.border_right_width = w, "border-bottom-width" => style.border_bottom_width = w, "border-left-width" => style.border_left_width = w, _ => {} } } // Border width shorthand (single value applied to all sides) "border-width" => { let w = resolve_border_width(value, parent_fs); style.border_top_width = w; style.border_right_width = w; style.border_bottom_width = w; style.border_left_width = w; } // Border style "border-top-style" | "border-right-style" | "border-bottom-style" | "border-left-style" => { let s = parse_border_style(value); match property { "border-top-style" => style.border_top_style = s, "border-right-style" => style.border_right_style = s, "border-bottom-style" => style.border_bottom_style = s, "border-left-style" => style.border_left_style = s, _ => {} } } "border-style" => { let s = parse_border_style(value); style.border_top_style = s; style.border_right_style = s; style.border_bottom_style = s; style.border_left_style = s; } // Border color "border-top-color" | "border-right-color" | "border-bottom-color" | "border-left-color" => { if let Some(c) = resolve_color(value, style.color) { match property { "border-top-color" => style.border_top_color = c, "border-right-color" => style.border_right_color = c, "border-bottom-color" => style.border_bottom_color = c, "border-left-color" => style.border_left_color = c, _ => {} } } } "border-color" => { if let Some(c) = resolve_color(value, style.color) { style.border_top_color = c; style.border_right_color = c; style.border_bottom_color = c; style.border_left_color = c; } } // Dimensions "width" => { style.width = resolve_length_or_auto(value, parent_fs, current_fs); } "height" => { style.height = resolve_length_or_auto(value, parent_fs, current_fs); } // Color (inherited) "color" => { if let Some(c) = resolve_color(value, parent.color) { style.color = c; // Update border colors to match (currentColor default) } } // Font-size (inherited) — special: em units relative to parent "font-size" => { match value { CssValue::Length(n, unit) => { style.font_size = resolve_length_unit(*n, *unit, parent_fs); } CssValue::Percentage(p) => { style.font_size = (*p / 100.0) as f32 * parent_fs; } CssValue::Zero => { style.font_size = 0.0; } CssValue::Keyword(k) => { style.font_size = match k.as_str() { "xx-small" => 9.0, "x-small" => 10.0, "small" => 13.0, "medium" => 16.0, "large" => 18.0, "x-large" => 24.0, "xx-large" => 32.0, "smaller" => parent_fs * 0.833, "larger" => parent_fs * 1.2, _ => style.font_size, }; } _ => {} } // Update line-height when font-size changes style.line_height = style.font_size * 1.2; } // Font-weight (inherited) "font-weight" => { style.font_weight = match value { CssValue::Keyword(k) => match k.as_str() { "normal" => FontWeight(400.0), "bold" => FontWeight(700.0), "lighter" => FontWeight((parent.font_weight.0 - 100.0).max(100.0)), "bolder" => FontWeight((parent.font_weight.0 + 300.0).min(900.0)), _ => style.font_weight, }, CssValue::Number(n) => FontWeight(*n as f32), _ => style.font_weight, }; } // Font-style (inherited) "font-style" => { style.font_style = match value { CssValue::Keyword(k) => match k.as_str() { "normal" => FontStyle::Normal, "italic" => FontStyle::Italic, "oblique" => FontStyle::Oblique, _ => style.font_style, }, _ => style.font_style, }; } // Font-family (inherited) "font-family" => { if let CssValue::String(s) | CssValue::Keyword(s) = value { style.font_family = s.clone(); } } // Text-align (inherited) "text-align" => { style.text_align = match value { CssValue::Keyword(k) => match k.as_str() { "left" => TextAlign::Left, "right" => TextAlign::Right, "center" => TextAlign::Center, "justify" => TextAlign::Justify, _ => style.text_align, }, _ => style.text_align, }; } // Text-decoration (inherited) "text-decoration" => { style.text_decoration = match value { CssValue::Keyword(k) => match k.as_str() { "underline" => TextDecoration::Underline, "overline" => TextDecoration::Overline, "line-through" => TextDecoration::LineThrough, _ => style.text_decoration, }, CssValue::None => TextDecoration::None, _ => style.text_decoration, }; } // Line-height (inherited) "line-height" => match value { CssValue::Keyword(k) if k == "normal" => { style.line_height = style.font_size * 1.2; } CssValue::Number(n) => { style.line_height = *n as f32 * style.font_size; } CssValue::Length(n, unit) => { style.line_height = resolve_length_unit(*n, *unit, style.font_size); } CssValue::Percentage(p) => { style.line_height = (*p / 100.0) as f32 * style.font_size; } _ => {} }, // Background color "background-color" => { if let Some(c) = resolve_color(value, style.color) { style.background_color = c; } } // Position "position" => { style.position = match value { CssValue::Keyword(k) => match k.as_str() { "static" => Position::Static, "relative" => Position::Relative, "absolute" => Position::Absolute, "fixed" => Position::Fixed, _ => style.position, }, _ => style.position, }; } // Position offsets "top" => style.top = resolve_length_or_auto(value, parent_fs, current_fs), "right" => style.right = resolve_length_or_auto(value, parent_fs, current_fs), "bottom" => style.bottom = resolve_length_or_auto(value, parent_fs, current_fs), "left" => style.left = resolve_length_or_auto(value, parent_fs, current_fs), // Overflow "overflow" => { style.overflow = match value { CssValue::Keyword(k) => match k.as_str() { "visible" => Overflow::Visible, "hidden" => Overflow::Hidden, "scroll" => Overflow::Scroll, _ => style.overflow, }, CssValue::Auto => Overflow::Auto, _ => style.overflow, }; } // Visibility (inherited) "visibility" => { style.visibility = match value { CssValue::Keyword(k) => match k.as_str() { "visible" => Visibility::Visible, "hidden" => Visibility::Hidden, "collapse" => Visibility::Collapse, _ => style.visibility, }, _ => style.visibility, }; } _ => {} // Unknown property — ignore } } fn resolve_border_width(value: &CssValue, em_base: f32) -> f32 { match value { CssValue::Length(n, unit) => resolve_length_unit(*n, *unit, em_base), CssValue::Zero => 0.0, CssValue::Number(n) if *n == 0.0 => 0.0, CssValue::Keyword(k) => match k.as_str() { "thin" => 1.0, "medium" => 3.0, "thick" => 5.0, _ => 0.0, }, _ => 0.0, } } fn parse_border_style(value: &CssValue) -> BorderStyle { match value { CssValue::Keyword(k) => match k.as_str() { "none" => BorderStyle::None, "hidden" => BorderStyle::Hidden, "dotted" => BorderStyle::Dotted, "dashed" => BorderStyle::Dashed, "solid" => BorderStyle::Solid, "double" => BorderStyle::Double, "groove" => BorderStyle::Groove, "ridge" => BorderStyle::Ridge, "inset" => BorderStyle::Inset, "outset" => BorderStyle::Outset, _ => BorderStyle::None, }, CssValue::None => BorderStyle::None, _ => BorderStyle::None, } } fn inherit_property(style: &mut ComputedStyle, property: &str, parent: &ComputedStyle) { match property { "color" => style.color = parent.color, "font-size" => { style.font_size = parent.font_size; style.line_height = style.font_size * 1.2; } "font-weight" => style.font_weight = parent.font_weight, "font-style" => style.font_style = parent.font_style, "font-family" => style.font_family = parent.font_family.clone(), "text-align" => style.text_align = parent.text_align, "text-decoration" => style.text_decoration = parent.text_decoration, "line-height" => style.line_height = parent.line_height, "visibility" => style.visibility = parent.visibility, // Non-inherited properties: inherit from parent if explicitly requested "display" => style.display = parent.display, "margin-top" => style.margin_top = parent.margin_top, "margin-right" => style.margin_right = parent.margin_right, "margin-bottom" => style.margin_bottom = parent.margin_bottom, "margin-left" => style.margin_left = parent.margin_left, "padding-top" => style.padding_top = parent.padding_top, "padding-right" => style.padding_right = parent.padding_right, "padding-bottom" => style.padding_bottom = parent.padding_bottom, "padding-left" => style.padding_left = parent.padding_left, "width" => style.width = parent.width, "height" => style.height = parent.height, "background-color" => style.background_color = parent.background_color, "position" => style.position = parent.position, "overflow" => style.overflow = parent.overflow, _ => {} } } fn reset_property_to_initial(style: &mut ComputedStyle, property: &str) { let initial = ComputedStyle::default(); match property { "display" => style.display = initial.display, "margin-top" => style.margin_top = initial.margin_top, "margin-right" => style.margin_right = initial.margin_right, "margin-bottom" => style.margin_bottom = initial.margin_bottom, "margin-left" => style.margin_left = initial.margin_left, "padding-top" => style.padding_top = initial.padding_top, "padding-right" => style.padding_right = initial.padding_right, "padding-bottom" => style.padding_bottom = initial.padding_bottom, "padding-left" => style.padding_left = initial.padding_left, "border-top-width" => style.border_top_width = initial.border_top_width, "border-right-width" => style.border_right_width = initial.border_right_width, "border-bottom-width" => style.border_bottom_width = initial.border_bottom_width, "border-left-width" => style.border_left_width = initial.border_left_width, "width" => style.width = initial.width, "height" => style.height = initial.height, "color" => style.color = initial.color, "font-size" => { style.font_size = initial.font_size; style.line_height = initial.line_height; } "font-weight" => style.font_weight = initial.font_weight, "font-style" => style.font_style = initial.font_style, "font-family" => style.font_family = initial.font_family.clone(), "text-align" => style.text_align = initial.text_align, "text-decoration" => style.text_decoration = initial.text_decoration, "line-height" => style.line_height = initial.line_height, "background-color" => style.background_color = initial.background_color, "position" => style.position = initial.position, "top" => style.top = initial.top, "right" => style.right = initial.right, "bottom" => style.bottom = initial.bottom, "left" => style.left = initial.left, "overflow" => style.overflow = initial.overflow, "visibility" => style.visibility = initial.visibility, _ => {} } } // --------------------------------------------------------------------------- // Styled tree // --------------------------------------------------------------------------- /// A node in the styled tree: a DOM node paired with its computed style. #[derive(Debug)] pub struct StyledNode { pub node: NodeId, pub style: ComputedStyle, pub children: Vec, } /// Extract CSS stylesheets from `