diff --git a/crates/browser/src/main.rs b/crates/browser/src/main.rs index 2fe4ad6..a3ae9bd 100644 --- a/crates/browser/src/main.rs +++ b/crates/browser/src/main.rs @@ -78,7 +78,11 @@ fn render_page(page: &PageState, font: &Font, bitmap: &mut BitmapContext) { } // Resolve computed styles from DOM + stylesheet. - let styled = match resolve_styles(&page.doc, std::slice::from_ref(&page.stylesheet)) { + let styled = match resolve_styles( + &page.doc, + std::slice::from_ref(&page.stylesheet), + (width as f32, height as f32), + ) { Some(s) => s, None => return, }; diff --git a/crates/layout/src/lib.rs b/crates/layout/src/lib.rs index 2b72121..50144a3 100644 --- a/crates/layout/src/lib.rs +++ b/crates/layout/src/lib.rs @@ -97,10 +97,16 @@ pub struct LayoutBox { pub overflow: Overflow, /// CSS `box-sizing` property. pub box_sizing: BoxSizing, - /// CSS `width` property (explicit or auto). + /// CSS `width` property (explicit or auto, may contain percentage). pub css_width: LengthOrAuto, - /// CSS `height` property (explicit or auto). + /// CSS `height` property (explicit or auto, may contain percentage). pub css_height: LengthOrAuto, + /// CSS margin values (may contain percentages for layout resolution). + pub css_margin: [LengthOrAuto; 4], + /// CSS padding values (may contain percentages for layout resolution). + pub css_padding: [LengthOrAuto; 4], + /// CSS position offset values (top, right, bottom, left) for relative positioning. + pub css_offsets: [LengthOrAuto; 4], /// CSS `visibility` property. pub visibility: Visibility, } @@ -140,6 +146,19 @@ impl LayoutBox { box_sizing: style.box_sizing, css_width: style.width, css_height: style.height, + css_margin: [ + style.margin_top, + style.margin_right, + style.margin_bottom, + style.margin_left, + ], + css_padding: [ + style.padding_top, + style.padding_right, + style.padding_bottom, + style.padding_left, + ], + css_offsets: [style.top, style.right, style.bottom, style.left], visibility: style.visibility, } } @@ -197,20 +216,25 @@ impl LayoutTree { // Resolve LengthOrAuto to f32 // --------------------------------------------------------------------------- -fn resolve_length(value: LengthOrAuto) -> f32 { +/// Resolve a `LengthOrAuto` to px. Percentages are resolved against +/// `reference` (typically the containing block width). Auto resolves to 0. +fn resolve_length_against(value: LengthOrAuto, reference: f32) -> f32 { match value { LengthOrAuto::Length(px) => px, + LengthOrAuto::Percentage(p) => p / 100.0 * reference, LengthOrAuto::Auto => 0.0, } } /// Resolve horizontal offset for `position: relative`. /// If both `left` and `right` are specified, `left` wins (CSS2 §9.4.3, ltr). -fn resolve_relative_horizontal(left: LengthOrAuto, right: LengthOrAuto) -> f32 { +fn resolve_relative_horizontal(left: LengthOrAuto, right: LengthOrAuto, cb_width: f32) -> f32 { match left { LengthOrAuto::Length(px) => px, + LengthOrAuto::Percentage(p) => p / 100.0 * cb_width, LengthOrAuto::Auto => match right { LengthOrAuto::Length(px) => -px, + LengthOrAuto::Percentage(p) => -(p / 100.0 * cb_width), LengthOrAuto::Auto => 0.0, }, } @@ -218,11 +242,13 @@ fn resolve_relative_horizontal(left: LengthOrAuto, right: LengthOrAuto) -> f32 { /// Resolve vertical offset for `position: relative`. /// If both `top` and `bottom` are specified, `top` wins (CSS2 §9.4.3). -fn resolve_relative_vertical(top: LengthOrAuto, bottom: LengthOrAuto) -> f32 { +fn resolve_relative_vertical(top: LengthOrAuto, bottom: LengthOrAuto, cb_height: f32) -> f32 { match top { LengthOrAuto::Length(px) => px, + LengthOrAuto::Percentage(p) => p / 100.0 * cb_height, LengthOrAuto::Auto => match bottom { LengthOrAuto::Length(px) => -px, + LengthOrAuto::Percentage(p) => -(p / 100.0 * cb_height), LengthOrAuto::Auto => 0.0, }, } @@ -263,17 +289,21 @@ fn build_box( return None; } + // Margin and padding: resolve absolute lengths now; percentages + // will be re-resolved in compute_layout against containing block. + // Use 0.0 as a placeholder reference for percentages — they'll be + // resolved properly in compute_layout. let margin = EdgeSizes { - top: resolve_length(style.margin_top), - right: resolve_length(style.margin_right), - bottom: resolve_length(style.margin_bottom), - left: resolve_length(style.margin_left), + top: resolve_length_against(style.margin_top, 0.0), + right: resolve_length_against(style.margin_right, 0.0), + bottom: resolve_length_against(style.margin_bottom, 0.0), + left: resolve_length_against(style.margin_left, 0.0), }; let padding = EdgeSizes { - top: style.padding_top, - right: style.padding_right, - bottom: style.padding_bottom, - left: style.padding_left, + top: resolve_length_against(style.padding_top, 0.0), + right: resolve_length_against(style.padding_right, 0.0), + bottom: resolve_length_against(style.padding_bottom, 0.0), + left: resolve_length_against(style.padding_left, 0.0), }; let border = EdgeSizes { top: if style.border_top_style != BorderStyle::None { @@ -326,12 +356,8 @@ fn build_box( b.replaced_size = Some((w, h)); } - // Compute relative position offset. - if style.position == Position::Relative { - let dx = resolve_relative_horizontal(style.left, style.right); - let dy = resolve_relative_vertical(style.top, style.bottom); - b.relative_offset = (dx, dy); - } + // Relative position offsets are resolved in compute_layout + // where the containing block dimensions are known. Some(b) } @@ -421,14 +447,49 @@ fn is_block_level(b: &LayoutBox) -> bool { // --------------------------------------------------------------------------- /// Position and size a layout box within `available_width` at position (`x`, `y`). +/// +/// `available_width` is the containing block width — used as the reference for +/// percentage widths, margins, and paddings (per CSS spec, even vertical margins/ +/// padding resolve against the containing block width). fn compute_layout( b: &mut LayoutBox, x: f32, y: f32, available_width: f32, + viewport_height: f32, font: &Font, doc: &Document, ) { + // Resolve percentage margins against containing block width. + // Only re-resolve percentages — absolute margins may have been modified + // by margin collapsing and must not be overwritten. + if matches!(b.css_margin[0], LengthOrAuto::Percentage(_)) { + b.margin.top = resolve_length_against(b.css_margin[0], available_width); + } + if matches!(b.css_margin[1], LengthOrAuto::Percentage(_)) { + b.margin.right = resolve_length_against(b.css_margin[1], available_width); + } + if matches!(b.css_margin[2], LengthOrAuto::Percentage(_)) { + b.margin.bottom = resolve_length_against(b.css_margin[2], available_width); + } + if matches!(b.css_margin[3], LengthOrAuto::Percentage(_)) { + b.margin.left = resolve_length_against(b.css_margin[3], available_width); + } + + // Resolve percentage padding against containing block width. + if matches!(b.css_padding[0], LengthOrAuto::Percentage(_)) { + b.padding.top = resolve_length_against(b.css_padding[0], available_width); + } + if matches!(b.css_padding[1], LengthOrAuto::Percentage(_)) { + b.padding.right = resolve_length_against(b.css_padding[1], available_width); + } + if matches!(b.css_padding[2], LengthOrAuto::Percentage(_)) { + b.padding.bottom = resolve_length_against(b.css_padding[2], available_width); + } + if matches!(b.css_padding[3], LengthOrAuto::Percentage(_)) { + b.padding.left = resolve_length_against(b.css_padding[3], available_width); + } + let content_x = x + b.margin.left + b.border.left + b.padding.left; let content_y = y + b.margin.top + b.border.top + b.padding.top; @@ -440,6 +501,13 @@ fn compute_layout( BoxSizing::ContentBox => w.max(0.0), BoxSizing::BorderBox => (w - horizontal_extra).max(0.0), }, + LengthOrAuto::Percentage(p) => { + let resolved = p / 100.0 * available_width; + match b.box_sizing { + BoxSizing::ContentBox => resolved.max(0.0), + BoxSizing::BorderBox => (resolved - horizontal_extra).max(0.0), + } + } LengthOrAuto::Auto => { (available_width - b.margin.left - b.margin.right - horizontal_extra).max(0.0) } @@ -451,18 +519,16 @@ fn compute_layout( // Replaced elements (e.g., ) have intrinsic dimensions. if let Some((rw, rh)) = b.replaced_size { - // Use CSS width/height if specified, otherwise use replaced dimensions. - // Content width is the minimum of replaced width and available width. b.rect.width = rw.min(content_width); b.rect.height = rh; - apply_relative_offset(b); + apply_relative_offset(b, available_width, viewport_height); return; } match &b.box_type { BoxType::Block(_) | BoxType::Anonymous => { if has_block_children(b) { - layout_block_children(b, font, doc); + layout_block_children(b, viewport_height, font, doc); } else { layout_inline_children(b, font, doc); } @@ -473,23 +539,43 @@ fn compute_layout( } // Apply explicit CSS height (adjusted for box-sizing), overriding auto height. - if let LengthOrAuto::Length(h) = b.css_height { - let vertical_extra = b.border.top + b.border.bottom + b.padding.top + b.padding.bottom; - b.rect.height = match b.box_sizing { - BoxSizing::ContentBox => h.max(0.0), - BoxSizing::BorderBox => (h - vertical_extra).max(0.0), - }; + match b.css_height { + LengthOrAuto::Length(h) => { + let vertical_extra = b.border.top + b.border.bottom + b.padding.top + b.padding.bottom; + b.rect.height = match b.box_sizing { + BoxSizing::ContentBox => h.max(0.0), + BoxSizing::BorderBox => (h - vertical_extra).max(0.0), + }; + } + LengthOrAuto::Percentage(p) => { + // Height percentage resolves against containing block height. + // For the root element, use viewport height. + let cb_height = viewport_height; + let resolved = p / 100.0 * cb_height; + let vertical_extra = b.border.top + b.border.bottom + b.padding.top + b.padding.bottom; + b.rect.height = match b.box_sizing { + BoxSizing::ContentBox => resolved.max(0.0), + BoxSizing::BorderBox => (resolved - vertical_extra).max(0.0), + }; + } + LengthOrAuto::Auto => {} } - apply_relative_offset(b); + apply_relative_offset(b, available_width, viewport_height); } /// Apply `position: relative` offset to a box and all its descendants. /// -/// This shifts the visual position without affecting the normal-flow layout -/// of surrounding elements (the original space is preserved). -fn apply_relative_offset(b: &mut LayoutBox) { - let (dx, dy) = b.relative_offset; +/// Resolves the CSS position offsets (which may contain percentages) and +/// shifts the visual position without affecting the normal-flow layout. +fn apply_relative_offset(b: &mut LayoutBox, cb_width: f32, cb_height: f32) { + if b.position != Position::Relative { + return; + } + let [top, right, bottom, left] = b.css_offsets; + let dx = resolve_relative_horizontal(left, right, cb_width); + let dy = resolve_relative_vertical(top, bottom, cb_height); + b.relative_offset = (dx, dy); if dx == 0.0 && dy == 0.0 { return; } @@ -627,7 +713,12 @@ fn last_block_bottom_margin(children: &[LayoutBox]) -> Option { /// Handles adjacent-sibling collapsing, empty-block collapsing, and /// parent-child internal spacing (the parent's external margins were already /// updated by `pre_collapse_margins`). -fn layout_block_children(parent: &mut LayoutBox, font: &Font, doc: &Document) { +fn layout_block_children( + parent: &mut LayoutBox, + viewport_height: f32, + font: &Font, + doc: &Document, +) { let content_x = parent.rect.x; let content_width = parent.rect.width; let mut cursor_y = parent.rect.y; @@ -685,6 +776,7 @@ fn layout_block_children(parent: &mut LayoutBox, font: &Font, doc: &Document) { content_x, y_for_child, content_width, + viewport_height, font, doc, ); @@ -1015,7 +1107,7 @@ pub fn layout( styled_root: &StyledNode, doc: &Document, viewport_width: f32, - _viewport_height: f32, + viewport_height: f32, font: &Font, image_sizes: &HashMap, ) -> LayoutTree { @@ -1033,7 +1125,15 @@ pub fn layout( // Pre-collapse parent-child margins before positioning. pre_collapse_margins(&mut root); - compute_layout(&mut root, 0.0, 0.0, viewport_width, font, doc); + compute_layout( + &mut root, + 0.0, + 0.0, + viewport_width, + viewport_height, + font, + doc, + ); let height = root.margin_box_height(); LayoutTree { @@ -1066,7 +1166,7 @@ mod tests { fn layout_doc(doc: &Document) -> LayoutTree { let font = test_font(); let sheets = extract_stylesheets(doc); - let styled = resolve_styles(doc, &sheets).unwrap(); + let styled = resolve_styles(doc, &sheets, (800.0, 600.0)).unwrap(); layout(&styled, doc, 800.0, 600.0, &font, &HashMap::new()) } @@ -1075,7 +1175,7 @@ mod tests { let doc = Document::new(); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)); if let Some(styled) = styled { let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); assert_eq!(tree.width, 800.0); @@ -1237,7 +1337,7 @@ mod tests { let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 100.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; let p_box = &body_box.children[0]; @@ -1403,7 +1503,7 @@ mod tests { let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; @@ -1478,7 +1578,7 @@ p { margin-top: 50px; margin-bottom: 50px; } let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; @@ -1509,7 +1609,7 @@ p { margin-top: 50px; margin-bottom: 50px; } let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; @@ -1529,7 +1629,7 @@ p { margin-top: 50px; margin-bottom: 50px; } let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; @@ -1551,7 +1651,7 @@ p { margin-top: 50px; margin-bottom: 50px; } let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; @@ -1577,7 +1677,7 @@ p { margin-top: 50px; margin-bottom: 50px; } let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; @@ -1604,7 +1704,7 @@ p { margin-top: 50px; margin-bottom: 50px; } let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; @@ -1631,7 +1731,7 @@ p { margin-top: 50px; margin-bottom: 50px; } let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; @@ -1658,7 +1758,7 @@ p { margin-top: 50px; margin-bottom: 50px; } let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; @@ -1691,7 +1791,7 @@ p { margin-top: 50px; margin-bottom: 50px; } let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; @@ -1714,7 +1814,7 @@ p { margin-top: 50px; margin-bottom: 50px; } let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); // Narrow viewport to force wrapping. let tree = layout(&styled, &doc, 100.0, 600.0, &font, &HashMap::new()); @@ -1747,7 +1847,7 @@ p { margin-top: 50px; margin-bottom: 50px; } let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; @@ -1787,7 +1887,7 @@ p { margin: 0; } let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; @@ -1825,7 +1925,7 @@ p { margin: 0; } let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; @@ -1848,7 +1948,7 @@ p { margin: 0; } let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; @@ -1871,7 +1971,7 @@ p { margin: 0; } let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; @@ -1892,7 +1992,7 @@ p { margin: 0; } let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; @@ -1926,7 +2026,7 @@ p { margin: 0; } let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; @@ -1955,7 +2055,7 @@ p { margin-top: 16px; margin-bottom: 16px; } let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; @@ -1991,7 +2091,7 @@ body { margin: 0; border-top: 1px solid black; } let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; @@ -2024,7 +2124,7 @@ body { margin: 0; border-top: 1px solid black; } let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; @@ -2052,7 +2152,7 @@ body { margin: 0; border-top: 1px solid black; } let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; @@ -2087,7 +2187,7 @@ body { margin: 0; border-top: 1px solid black; } let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; @@ -2122,7 +2222,7 @@ body { margin: 0; border-top: 1px solid black; } let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; @@ -2158,7 +2258,7 @@ body { margin: 0; border-top: 1px solid black; } let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; @@ -2187,7 +2287,7 @@ p { margin-top: 5px; margin-bottom: 5px; } let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; @@ -2238,7 +2338,7 @@ div { width: 200px; padding: 10px; border: 5px solid black; } let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; @@ -2268,7 +2368,7 @@ div { box-sizing: border-box; width: 200px; padding: 10px; border: 5px solid bla let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; @@ -2298,7 +2398,7 @@ div { box-sizing: border-box; width: 20px; padding: 15px; border: 5px solid blac let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; @@ -2325,7 +2425,7 @@ body { margin: 0; } let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; @@ -2354,7 +2454,7 @@ div { box-sizing: border-box; height: 100px; padding: 10px; border: 5px solid bl let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; @@ -2380,7 +2480,7 @@ div { height: 100px; padding: 10px; border: 5px solid black; } let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; @@ -2410,7 +2510,7 @@ p { margin: 0; } let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; @@ -2444,7 +2544,7 @@ p { margin: 0; } let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; @@ -2478,7 +2578,7 @@ body { margin: 0; } let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; @@ -2504,7 +2604,7 @@ body { margin: 0; } let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; @@ -2529,7 +2629,7 @@ div { visibility: collapse; height: 50px; } let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); let body_box = &tree.root.children[0]; @@ -2538,4 +2638,259 @@ div { visibility: collapse; height: 50px; } // Still occupies space (non-table collapse = hidden behavior). assert_eq!(div_box.rect.height, 50.0); } + + // --- Viewport units and percentage resolution tests --- + + #[test] + fn width_50_percent_resolves_to_half_containing_block() { + let html_str = r#" + + +
Half width
+"#; + let doc = we_html::parse_html(html_str); + let font = test_font(); + let sheets = extract_stylesheets(&doc); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); + let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); + + let body_box = &tree.root.children[0]; + let div_box = &body_box.children[0]; + assert!( + (div_box.rect.width - 400.0).abs() < 0.01, + "width: 50% should be 400px on 800px viewport, got {}", + div_box.rect.width + ); + } + + #[test] + fn margin_10_percent_resolves_against_containing_block_width() { + let html_str = r#" + + +
Box
+"#; + let doc = we_html::parse_html(html_str); + let font = test_font(); + let sheets = extract_stylesheets(&doc); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); + let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); + + let body_box = &tree.root.children[0]; + let div_box = &body_box.children[0]; + // All margins (including top/bottom) resolve against containing block WIDTH. + assert!( + (div_box.margin.left - 80.0).abs() < 0.01, + "margin-left: 10% should be 80px on 800px viewport, got {}", + div_box.margin.left + ); + assert!( + (div_box.margin.top - 80.0).abs() < 0.01, + "margin-top: 10% should be 80px (against width, not height), got {}", + div_box.margin.top + ); + } + + #[test] + fn width_50vw_resolves_to_half_viewport() { + let html_str = r#" + + +
Half viewport
+"#; + let doc = we_html::parse_html(html_str); + let font = test_font(); + let sheets = extract_stylesheets(&doc); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); + let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); + + let body_box = &tree.root.children[0]; + let div_box = &body_box.children[0]; + assert!( + (div_box.rect.width - 400.0).abs() < 0.01, + "width: 50vw should be 400px on 800px viewport, got {}", + div_box.rect.width + ); + } + + #[test] + fn height_100vh_resolves_to_full_viewport() { + let html_str = r#" + + +
Full height
+"#; + let doc = we_html::parse_html(html_str); + let font = test_font(); + let sheets = extract_stylesheets(&doc); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); + let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); + + let body_box = &tree.root.children[0]; + let div_box = &body_box.children[0]; + assert!( + (div_box.rect.height - 600.0).abs() < 0.01, + "height: 100vh should be 600px on 600px viewport, got {}", + div_box.rect.height + ); + } + + #[test] + fn font_size_5vmin_resolves_to_smaller_dimension() { + let html_str = r#" + + +

Text

+"#; + let doc = we_html::parse_html(html_str); + let font = test_font(); + let sheets = extract_stylesheets(&doc); + // viewport 800x600 → vmin = 600 + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); + let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); + + let body_box = &tree.root.children[0]; + let p_box = &body_box.children[0]; + // 5vmin = 5% of min(800, 600) = 5% of 600 = 30px + assert!( + (p_box.font_size - 30.0).abs() < 0.01, + "font-size: 5vmin should be 30px, got {}", + p_box.font_size + ); + } + + #[test] + fn nested_percentage_widths_compound() { + let html_str = r#" + + + +
Nested
+ +"#; + let doc = we_html::parse_html(html_str); + let font = test_font(); + let sheets = extract_stylesheets(&doc); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); + let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); + + let body_box = &tree.root.children[0]; + let outer_box = &body_box.children[0]; + let inner_box = &outer_box.children[0]; + // outer = 50% of 800 = 400 + assert!( + (outer_box.rect.width - 400.0).abs() < 0.01, + "outer width should be 400px, got {}", + outer_box.rect.width + ); + // inner = 50% of 400 = 200 + assert!( + (inner_box.rect.width - 200.0).abs() < 0.01, + "inner width should be 200px (50% of 400), got {}", + inner_box.rect.width + ); + } + + #[test] + fn padding_percent_resolves_against_width() { + let html_str = r#" + + +
Content
+"#; + let doc = we_html::parse_html(html_str); + let font = test_font(); + let sheets = extract_stylesheets(&doc); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); + let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); + + let body_box = &tree.root.children[0]; + let div_box = &body_box.children[0]; + // padding: 5% resolves against containing block width (800px) + assert!( + (div_box.padding.top - 40.0).abs() < 0.01, + "padding-top: 5% should be 40px (5% of 800), got {}", + div_box.padding.top + ); + assert!( + (div_box.padding.left - 40.0).abs() < 0.01, + "padding-left: 5% should be 40px (5% of 800), got {}", + div_box.padding.left + ); + } + + #[test] + fn vmax_uses_larger_dimension() { + let html_str = r#" + + +
Content
+"#; + let doc = we_html::parse_html(html_str); + let font = test_font(); + let sheets = extract_stylesheets(&doc); + // viewport 800x600 → vmax = 800 + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); + let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); + + let body_box = &tree.root.children[0]; + let div_box = &body_box.children[0]; + // 10vmax = 10% of max(800, 600) = 10% of 800 = 80px + assert!( + (div_box.rect.width - 80.0).abs() < 0.01, + "width: 10vmax should be 80px, got {}", + div_box.rect.width + ); + } + + #[test] + fn height_50_percent_resolves_against_viewport() { + let html_str = r#" + + +
Half height
+"#; + let doc = we_html::parse_html(html_str); + let font = test_font(); + let sheets = extract_stylesheets(&doc); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); + let tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); + + let body_box = &tree.root.children[0]; + let div_box = &body_box.children[0]; + // height: 50% resolves against viewport height (600) + assert!( + (div_box.rect.height - 300.0).abs() < 0.01, + "height: 50% should be 300px on 600px viewport, got {}", + div_box.rect.height + ); + } } diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 8641992..9101f51 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -490,7 +490,7 @@ mod tests { fn layout_doc(doc: &Document) -> we_layout::LayoutTree { let font = test_font(); let sheets = extract_stylesheets(doc); - let styled = resolve_styles(doc, &sheets).unwrap(); + let styled = resolve_styles(doc, &sheets, (800.0, 600.0)).unwrap(); we_layout::layout( &styled, doc, @@ -551,7 +551,7 @@ mod tests { let doc = Document::new(); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)); if let Some(styled) = styled { let tree = we_layout::layout( &styled, @@ -726,7 +726,7 @@ mod tests { let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = we_layout::layout( &styled, &doc, @@ -760,7 +760,7 @@ mod tests { let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = we_layout::layout( &styled, &doc, @@ -793,7 +793,7 @@ mod tests { let doc = we_html::parse_html(html_str); let font = test_font(); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let tree = we_layout::layout( &styled, &doc, diff --git a/crates/style/src/computed.rs b/crates/style/src/computed.rs index c34ffe2..1997b9a 100644 --- a/crates/style/src/computed.rs +++ b/crates/style/src/computed.rs @@ -149,10 +149,12 @@ pub enum BorderStyle { // LengthOrAuto // --------------------------------------------------------------------------- -/// A computed length (resolved to px) or `auto`. +/// A computed length (resolved to px), a percentage (unresolved), or `auto`. #[derive(Debug, Clone, Copy, PartialEq, Default)] pub enum LengthOrAuto { Length(f32), + /// Percentage value (0.0–100.0), resolved during layout against the containing block. + Percentage(f32), #[default] Auto, } @@ -173,11 +175,11 @@ pub struct ComputedStyle { 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: padding (percentages resolve against containing block width) + pub padding_top: LengthOrAuto, + pub padding_right: LengthOrAuto, + pub padding_bottom: LengthOrAuto, + pub padding_left: LengthOrAuto, // Box model: border width pub border_top_width: f32, @@ -241,10 +243,10 @@ impl Default for ComputedStyle { 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, + padding_top: LengthOrAuto::Length(0.0), + padding_right: LengthOrAuto::Length(0.0), + padding_bottom: LengthOrAuto::Length(0.0), + padding_left: LengthOrAuto::Length(0.0), border_top_width: 0.0, border_right_width: 0.0, @@ -406,20 +408,9 @@ u { // 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 { +fn resolve_length_unit(value: f64, unit: LengthUnit, em_base: f32, viewport: (f32, f32)) -> f32 { let v = value as f32; + let (vw, vh) = viewport; match unit { LengthUnit::Px => v, LengthUnit::Em => v * em_base, @@ -429,21 +420,31 @@ fn resolve_length_unit(value: f64, unit: LengthUnit, em_base: f32) -> f32 { 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, + LengthUnit::Vw => v * vw / 100.0, + LengthUnit::Vh => v * vh / 100.0, + LengthUnit::Vmin => v * vw.min(vh) / 100.0, + LengthUnit::Vmax => v * vw.max(vh) / 100.0, } } -fn resolve_length_or_auto( +/// Resolve a CSS value to `LengthOrAuto` for layout properties (width, height, +/// margin, padding, top/right/bottom/left). Percentages are preserved as +/// `LengthOrAuto::Percentage` for later resolution during layout against the +/// containing block. +fn resolve_layout_length_or_auto( value: &CssValue, - parent_font_size: f32, current_font_size: f32, + viewport: (f32, f32), ) -> LengthOrAuto { match value { CssValue::Auto => LengthOrAuto::Auto, - _ => resolve_length(value, parent_font_size, current_font_size) - .map(LengthOrAuto::Length) - .unwrap_or(LengthOrAuto::Auto), + CssValue::Percentage(p) => LengthOrAuto::Percentage(*p as f32), + CssValue::Length(n, unit) => { + LengthOrAuto::Length(resolve_length_unit(*n, *unit, current_font_size, viewport)) + } + CssValue::Zero => LengthOrAuto::Length(0.0), + CssValue::Number(n) if *n == 0.0 => LengthOrAuto::Length(0.0), + _ => LengthOrAuto::Auto, } } @@ -465,6 +466,7 @@ fn apply_property( property: &str, value: &CssValue, parent: &ComputedStyle, + viewport: (f32, f32), ) { // Handle inherit/initial/unset match value { @@ -503,45 +505,37 @@ fn apply_property( }; } - // Margin + // Margin (percentages preserved for layout resolution) "margin-top" => { - style.margin_top = resolve_length_or_auto(value, parent_fs, current_fs); + style.margin_top = resolve_layout_length_or_auto(value, current_fs, viewport); } "margin-right" => { - style.margin_right = resolve_length_or_auto(value, parent_fs, current_fs); + style.margin_right = resolve_layout_length_or_auto(value, current_fs, viewport); } "margin-bottom" => { - style.margin_bottom = resolve_length_or_auto(value, parent_fs, current_fs); + style.margin_bottom = resolve_layout_length_or_auto(value, current_fs, viewport); } "margin-left" => { - style.margin_left = resolve_length_or_auto(value, parent_fs, current_fs); + style.margin_left = resolve_layout_length_or_auto(value, current_fs, viewport); } - // Padding + // Padding (percentages preserved for layout resolution) "padding-top" => { - if let Some(v) = resolve_length(value, parent_fs, current_fs) { - style.padding_top = v; - } + style.padding_top = resolve_layout_length_or_auto(value, current_fs, viewport); } "padding-right" => { - if let Some(v) = resolve_length(value, parent_fs, current_fs) { - style.padding_right = v; - } + style.padding_right = resolve_layout_length_or_auto(value, current_fs, viewport); } "padding-bottom" => { - if let Some(v) = resolve_length(value, parent_fs, current_fs) { - style.padding_bottom = v; - } + style.padding_bottom = resolve_layout_length_or_auto(value, current_fs, viewport); } "padding-left" => { - if let Some(v) = resolve_length(value, parent_fs, current_fs) { - style.padding_left = v; - } + style.padding_left = resolve_layout_length_or_auto(value, current_fs, viewport); } // Border width "border-top-width" | "border-right-width" | "border-bottom-width" | "border-left-width" => { - let w = resolve_border_width(value, parent_fs); + let w = resolve_border_width(value, parent_fs, viewport); match property { "border-top-width" => style.border_top_width = w, "border-right-width" => style.border_right_width = w, @@ -553,7 +547,7 @@ fn apply_property( // Border width shorthand (single value applied to all sides) "border-width" => { - let w = resolve_border_width(value, parent_fs); + let w = resolve_border_width(value, parent_fs, viewport); style.border_top_width = w; style.border_right_width = w; style.border_bottom_width = w; @@ -602,12 +596,12 @@ fn apply_property( } } - // Dimensions + // Dimensions (percentages preserved for layout resolution) "width" => { - style.width = resolve_length_or_auto(value, parent_fs, current_fs); + style.width = resolve_layout_length_or_auto(value, current_fs, viewport); } "height" => { - style.height = resolve_length_or_auto(value, parent_fs, current_fs); + style.height = resolve_layout_length_or_auto(value, current_fs, viewport); } // Box sizing @@ -634,7 +628,7 @@ fn apply_property( "font-size" => { match value { CssValue::Length(n, unit) => { - style.font_size = resolve_length_unit(*n, *unit, parent_fs); + style.font_size = resolve_length_unit(*n, *unit, parent_fs, viewport); } CssValue::Percentage(p) => { style.font_size = (*p / 100.0) as f32 * parent_fs; @@ -734,7 +728,7 @@ fn apply_property( style.line_height = *n as f32 * style.font_size; } CssValue::Length(n, unit) => { - style.line_height = resolve_length_unit(*n, *unit, style.font_size); + style.line_height = resolve_length_unit(*n, *unit, style.font_size, viewport); } CssValue::Percentage(p) => { style.line_height = (*p / 100.0) as f32 * style.font_size; @@ -763,11 +757,11 @@ fn apply_property( }; } - // 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), + // Position offsets (percentages preserved for layout resolution) + "top" => style.top = resolve_layout_length_or_auto(value, current_fs, viewport), + "right" => style.right = resolve_layout_length_or_auto(value, current_fs, viewport), + "bottom" => style.bottom = resolve_layout_length_or_auto(value, current_fs, viewport), + "left" => style.left = resolve_layout_length_or_auto(value, current_fs, viewport), // Overflow "overflow" => { @@ -800,9 +794,9 @@ fn apply_property( } } -fn resolve_border_width(value: &CssValue, em_base: f32) -> f32 { +fn resolve_border_width(value: &CssValue, em_base: f32, viewport: (f32, f32)) -> f32 { match value { - CssValue::Length(n, unit) => resolve_length_unit(*n, *unit, em_base), + CssValue::Length(n, unit) => resolve_length_unit(*n, *unit, em_base, viewport), CssValue::Zero => 0.0, CssValue::Number(n) if *n == 0.0 => 0.0, CssValue::Keyword(k) => match k.as_str() { @@ -959,7 +953,11 @@ fn collect_style_elements(doc: &Document, node: NodeId, stylesheets: &mut Vec Option { +pub fn resolve_styles( + doc: &Document, + author_stylesheets: &[Stylesheet], + viewport: (f32, f32), +) -> Option { let ua = ua_stylesheet(); // Combine UA + author stylesheets into a single list for rule collection. @@ -972,7 +970,7 @@ pub fn resolve_styles(doc: &Document, author_stylesheets: &[Stylesheet]) -> Opti } let root = doc.root(); - resolve_node(doc, root, &combined, &ComputedStyle::default()) + resolve_node(doc, root, &combined, &ComputedStyle::default(), viewport) } fn resolve_node( @@ -980,13 +978,14 @@ fn resolve_node( node: NodeId, stylesheet: &Stylesheet, parent_style: &ComputedStyle, + viewport: (f32, f32), ) -> Option { match doc.node_data(node) { NodeData::Document => { // Document node: resolve children, return first element child or wrapper. let mut children = Vec::new(); for child in doc.children(node) { - if let Some(styled) = resolve_node(doc, child, stylesheet, parent_style) { + if let Some(styled) = resolve_node(doc, child, stylesheet, parent_style, viewport) { children.push(styled); } } @@ -1003,7 +1002,7 @@ fn resolve_node( } } NodeData::Element { .. } => { - let style = compute_style_for_element(doc, node, stylesheet, parent_style); + let style = compute_style_for_element(doc, node, stylesheet, parent_style, viewport); if style.display == Display::None { return None; @@ -1011,7 +1010,7 @@ fn resolve_node( let mut children = Vec::new(); for child in doc.children(node) { - if let Some(styled) = resolve_node(doc, child, stylesheet, &style) { + if let Some(styled) = resolve_node(doc, child, stylesheet, &style, viewport) { children.push(styled); } } @@ -1043,6 +1042,7 @@ fn compute_style_for_element( node: NodeId, stylesheet: &Stylesheet, parent_style: &ComputedStyle, + viewport: (f32, f32), ) -> ComputedStyle { // Start from initial values, inheriting inherited properties from parent let mut style = ComputedStyle { @@ -1097,7 +1097,7 @@ fn compute_style_for_element( // Step 5: Apply normal declarations (already in specificity order) for (prop, value) in &normal_decls { - apply_property(&mut style, prop, value, parent_style); + apply_property(&mut style, prop, value, parent_style, viewport); } // Step 6: Apply inline style normal declarations (override stylesheet normals) @@ -1106,18 +1106,18 @@ fn compute_style_for_element( let property = decl.property.as_str(); if let Some(longhands) = expand_shorthand(property, &decl.value, false) { for lh in &longhands { - apply_property(&mut style, &lh.property, &lh.value, parent_style); + apply_property(&mut style, &lh.property, &lh.value, parent_style, viewport); } } else { let value = parse_value(&decl.value); - apply_property(&mut style, property, &value, parent_style); + apply_property(&mut style, property, &value, parent_style, viewport); } } } // Step 7: Apply !important declarations (override everything normal) for (prop, value) in &important_decls { - apply_property(&mut style, prop, value, parent_style); + apply_property(&mut style, prop, value, parent_style, viewport); } // Step 8: Apply inline style !important declarations (highest priority) @@ -1126,11 +1126,11 @@ fn compute_style_for_element( let property = decl.property.as_str(); if let Some(longhands) = expand_shorthand(property, &decl.value, true) { for lh in &longhands { - apply_property(&mut style, &lh.property, &lh.value, parent_style); + apply_property(&mut style, &lh.property, &lh.value, parent_style, viewport); } } else { let value = parse_value(&decl.value); - apply_property(&mut style, property, &value, parent_style); + apply_property(&mut style, property, &value, parent_style, viewport); } } } @@ -1180,7 +1180,7 @@ mod tests { #[test] fn ua_body_has_8px_margin() { let (doc, _, _, _) = make_doc_with_body(); - let styled = resolve_styles(&doc, &[]).unwrap(); + let styled = resolve_styles(&doc, &[], (800.0, 600.0)).unwrap(); // styled is , first child is let body = &styled.children[0]; assert_eq!(body.style.margin_top, LengthOrAuto::Length(8.0)); @@ -1197,7 +1197,7 @@ mod tests { doc.append_child(body, h1); doc.append_child(h1, text); - let styled = resolve_styles(&doc, &[]).unwrap(); + let styled = resolve_styles(&doc, &[], (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; let h1_node = &body_node.children[0]; @@ -1212,7 +1212,7 @@ mod tests { let h2 = doc.create_element("h2"); doc.append_child(body, h2); - let styled = resolve_styles(&doc, &[]).unwrap(); + let styled = resolve_styles(&doc, &[], (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; let h2_node = &body_node.children[0]; @@ -1228,7 +1228,7 @@ mod tests { doc.append_child(body, p); doc.append_child(p, text); - let styled = resolve_styles(&doc, &[]).unwrap(); + let styled = resolve_styles(&doc, &[], (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; let p_node = &body_node.children[0]; @@ -1243,7 +1243,7 @@ mod tests { let div = doc.create_element("div"); doc.append_child(body, div); - let styled = resolve_styles(&doc, &[]).unwrap(); + let styled = resolve_styles(&doc, &[], (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; let div_node = &body_node.children[0]; assert_eq!(div_node.style.display, Display::Block); @@ -1259,7 +1259,7 @@ mod tests { doc.append_child(p, span); doc.append_child(span, text); - let styled = resolve_styles(&doc, &[]).unwrap(); + let styled = resolve_styles(&doc, &[], (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; let p_node = &body_node.children[0]; let span_node = &p_node.children[0]; @@ -1274,7 +1274,7 @@ mod tests { doc.append_child(html, head); doc.append_child(head, title); - let styled = resolve_styles(&doc, &[]).unwrap(); + let styled = resolve_styles(&doc, &[], (800.0, 600.0)).unwrap(); // head should not appear in styled tree (display: none) for child in &styled.children { if let NodeData::Element { tag_name, .. } = doc.node_data(child.node) { @@ -1296,7 +1296,7 @@ mod tests { doc.append_child(p, text); let ss = Parser::parse("p { color: red; }"); - let styled = resolve_styles(&doc, &[ss]).unwrap(); + let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; let p_node = &body_node.children[0]; assert_eq!(p_node.style.color, Color::rgb(255, 0, 0)); @@ -1309,7 +1309,7 @@ mod tests { doc.append_child(body, div); let ss = Parser::parse("div { background-color: blue; }"); - let styled = resolve_styles(&doc, &[ss]).unwrap(); + let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; let div_node = &body_node.children[0]; assert_eq!(div_node.style.background_color, Color::rgb(0, 0, 255)); @@ -1322,7 +1322,7 @@ mod tests { doc.append_child(body, p); let ss = Parser::parse("p { font-size: 24px; }"); - let styled = resolve_styles(&doc, &[ss]).unwrap(); + let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; let p_node = &body_node.children[0]; assert_eq!(p_node.style.font_size, 24.0); @@ -1335,7 +1335,7 @@ mod tests { doc.append_child(body, div); let ss = Parser::parse("div { margin-top: 20px; margin-bottom: 10px; }"); - let styled = resolve_styles(&doc, &[ss]).unwrap(); + let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; let div_node = &body_node.children[0]; assert_eq!(div_node.style.margin_top, LengthOrAuto::Length(20.0)); @@ -1357,7 +1357,7 @@ mod tests { // .highlight (0,1,0) > p (0,0,1) let ss = Parser::parse("p { color: red; } .highlight { color: green; }"); - let styled = resolve_styles(&doc, &[ss]).unwrap(); + let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; let p_node = &body_node.children[0]; assert_eq!(p_node.style.color, Color::rgb(0, 128, 0)); @@ -1373,7 +1373,7 @@ mod tests { // Same specificity: later wins let ss = Parser::parse("p { color: red; } p { color: blue; }"); - let styled = resolve_styles(&doc, &[ss]).unwrap(); + let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; let p_node = &body_node.children[0]; assert_eq!(p_node.style.color, Color::rgb(0, 0, 255)); @@ -1390,7 +1390,7 @@ mod tests { // #main (1,0,0) has higher specificity, but p has !important let ss = Parser::parse("#main { color: blue; } p { color: red !important; }"); - let styled = resolve_styles(&doc, &[ss]).unwrap(); + let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; let p_node = &body_node.children[0]; assert_eq!(p_node.style.color, Color::rgb(255, 0, 0)); @@ -1411,7 +1411,7 @@ mod tests { doc.append_child(p, text); let ss = Parser::parse("div { color: green; }"); - let styled = resolve_styles(&doc, &[ss]).unwrap(); + let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; let div_node = &body_node.children[0]; let p_node = &div_node.children[0]; @@ -1432,7 +1432,7 @@ mod tests { doc.append_child(p, text); let ss = Parser::parse("div { font-size: 20px; }"); - let styled = resolve_styles(&doc, &[ss]).unwrap(); + let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; let div_node = &body_node.children[0]; let p_node = &div_node.children[0]; @@ -1452,7 +1452,7 @@ mod tests { doc.append_child(span, text); let ss = Parser::parse("div { margin-top: 50px; }"); - let styled = resolve_styles(&doc, &[ss]).unwrap(); + let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; let div_node = &body_node.children[0]; let span_node = &div_node.children[0]; @@ -1477,7 +1477,7 @@ mod tests { doc.append_child(p, text); let ss = Parser::parse("div { background-color: red; } p { background-color: inherit; }"); - let styled = resolve_styles(&doc, &[ss]).unwrap(); + let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; let div_node = &body_node.children[0]; let p_node = &div_node.children[0]; @@ -1498,7 +1498,7 @@ mod tests { // div sets color to red, p resets to initial (black) let ss = Parser::parse("div { color: red; } p { color: initial; }"); - let styled = resolve_styles(&doc, &[ss]).unwrap(); + let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; let div_node = &body_node.children[0]; let p_node = &div_node.children[0]; @@ -1519,7 +1519,7 @@ mod tests { // color is inherited, so unset => inherit let ss = Parser::parse("div { color: green; } p { color: unset; }"); - let styled = resolve_styles(&doc, &[ss]).unwrap(); + let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; let div_node = &body_node.children[0]; let p_node = &div_node.children[0]; @@ -1539,7 +1539,7 @@ mod tests { // margin is non-inherited, so unset => initial (0) let ss = Parser::parse("p { margin-top: unset; }"); - let styled = resolve_styles(&doc, &[ss]).unwrap(); + let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; let p_node = &body_node.children[0]; @@ -1560,7 +1560,7 @@ mod tests { doc.append_child(div, text); let ss = Parser::parse("div { font-size: 20px; margin-top: 2em; }"); - let styled = resolve_styles(&doc, &[ss]).unwrap(); + let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; let div_node = &body_node.children[0]; @@ -1580,7 +1580,7 @@ mod tests { doc.append_child(p, text); let ss = Parser::parse("div { font-size: 20px; } p { font-size: 1.5em; }"); - let styled = resolve_styles(&doc, &[ss]).unwrap(); + let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; let div_node = &body_node.children[0]; let p_node = &div_node.children[0]; @@ -1604,7 +1604,7 @@ mod tests { doc.append_child(p, text); let ss = Parser::parse("p { color: red; }"); - let styled = resolve_styles(&doc, &[ss]).unwrap(); + let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; let p_node = &body_node.children[0]; @@ -1622,7 +1622,7 @@ mod tests { doc.append_child(p, text); let ss = Parser::parse("p { color: red !important; }"); - let styled = resolve_styles(&doc, &[ss]).unwrap(); + let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; let p_node = &body_node.children[0]; @@ -1643,7 +1643,7 @@ mod tests { doc.append_child(div, text); let ss = Parser::parse("div { margin: 10px 20px; }"); - let styled = resolve_styles(&doc, &[ss]).unwrap(); + let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; let div_node = &body_node.children[0]; @@ -1662,14 +1662,14 @@ mod tests { doc.append_child(div, text); let ss = Parser::parse("div { padding: 5px; }"); - let styled = resolve_styles(&doc, &[ss]).unwrap(); + let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; let div_node = &body_node.children[0]; - assert_eq!(div_node.style.padding_top, 5.0); - assert_eq!(div_node.style.padding_right, 5.0); - assert_eq!(div_node.style.padding_bottom, 5.0); - assert_eq!(div_node.style.padding_left, 5.0); + assert_eq!(div_node.style.padding_top, LengthOrAuto::Length(5.0)); + assert_eq!(div_node.style.padding_right, LengthOrAuto::Length(5.0)); + assert_eq!(div_node.style.padding_bottom, LengthOrAuto::Length(5.0)); + assert_eq!(div_node.style.padding_left, LengthOrAuto::Length(5.0)); } // ----------------------------------------------------------------------- @@ -1686,7 +1686,7 @@ mod tests { // UA gives p margin-top=1em=16px. Author overrides to 0. let ss = Parser::parse("p { margin-top: 0; margin-bottom: 0; }"); - let styled = resolve_styles(&doc, &[ss]).unwrap(); + let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; let p_node = &body_node.children[0]; @@ -1707,7 +1707,7 @@ mod tests { doc.append_child(p, text); let ss = Parser::parse("p { color: red; font-size: 20px; }"); - let styled = resolve_styles(&doc, &[ss]).unwrap(); + let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; let p_node = &body_node.children[0]; let text_node = &p_node.children[0]; @@ -1730,7 +1730,7 @@ mod tests { let ss1 = Parser::parse("p { color: red; }"); let ss2 = Parser::parse("p { color: blue; }"); - let styled = resolve_styles(&doc, &[ss1, ss2]).unwrap(); + let styled = resolve_styles(&doc, &[ss1, ss2], (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; let p_node = &body_node.children[0]; @@ -1751,7 +1751,7 @@ mod tests { doc.append_child(div, text); let ss = Parser::parse("div { border: 2px solid red; }"); - let styled = resolve_styles(&doc, &[ss]).unwrap(); + let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; let div_node = &body_node.children[0]; @@ -1773,7 +1773,7 @@ mod tests { doc.append_child(div, text); let ss = Parser::parse("div { position: relative; top: 10px; }"); - let styled = resolve_styles(&doc, &[ss]).unwrap(); + let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; let div_node = &body_node.children[0]; @@ -1800,7 +1800,7 @@ mod tests { doc.append_child(div2, t2); let ss = Parser::parse(".hidden { display: none; }"); - let styled = resolve_styles(&doc, &[ss]).unwrap(); + let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; // Only div1 should appear @@ -1822,7 +1822,7 @@ mod tests { doc.append_child(p, text); let ss = Parser::parse("div { visibility: hidden; }"); - let styled = resolve_styles(&doc, &[ss]).unwrap(); + let styled = resolve_styles(&doc, &[ss], (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; let div_node = &body_node.children[0]; let p_node = &div_node.children[0]; @@ -1920,7 +1920,7 @@ mod tests { doc.append_child(p, text); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; let p_node = &body_node.children[0]; @@ -1941,7 +1941,7 @@ mod tests { let sheets = extract_stylesheets(&doc); assert!(sheets.is_empty()); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; let div_node = &body_node.children[0]; @@ -1967,7 +1967,7 @@ mod tests { doc.append_child(p, text); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; let p_node = &body_node.children[0]; @@ -2003,7 +2003,7 @@ mod tests { let sheets = extract_stylesheets(&doc); assert_eq!(sheets.len(), 2); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let body_node = &styled.children[0]; let p_node = &body_node.children[0]; @@ -2017,7 +2017,7 @@ mod tests {
Test
"#; let doc = we_html::parse_html(html_str); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let body = &styled.children[0]; let div = &body.children[0]; assert_eq!(div.style.box_sizing, BoxSizing::ContentBox); @@ -2030,7 +2030,7 @@ mod tests {
Test
"#; let doc = we_html::parse_html(html_str); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let body = &styled.children[0]; let div = &body.children[0]; assert_eq!(div.style.box_sizing, BoxSizing::BorderBox); @@ -2043,7 +2043,7 @@ mod tests {

Child

"#; let doc = we_html::parse_html(html_str); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let body = &styled.children[0]; let parent = &body.children[0]; let child = &parent.children[0]; diff --git a/crates/style/tests/style_extraction.rs b/crates/style/tests/style_extraction.rs index 41d450e..6e01623 100644 --- a/crates/style/tests/style_extraction.rs +++ b/crates/style/tests/style_extraction.rs @@ -23,7 +23,7 @@ h1 { color: blue; } let sheets = extract_stylesheets(&doc); assert_eq!(sheets.len(), 1); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); // styled root is // Find body (first visible child) let body = &styled.children[0]; @@ -52,7 +52,7 @@ fn html_with_multiple_style_elements() { let sheets = extract_stylesheets(&doc); assert_eq!(sheets.len(), 2); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let body = &styled.children[0]; let p = &body.children[0]; @@ -75,7 +75,7 @@ fn html_with_inline_style_attribute() { let doc = we_html::parse_html(html); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let body = &styled.children[0]; let p = &body.children[0]; @@ -97,7 +97,7 @@ fn html_no_style_uses_ua_defaults() { let sheets = extract_stylesheets(&doc); assert!(sheets.is_empty()); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); let body = &styled.children[0]; // Body should have 8px margin from UA stylesheet. @@ -122,7 +122,7 @@ fn style_element_display_none_in_styled_tree() { let doc = we_html::parse_html(html); let sheets = extract_stylesheets(&doc); - let styled = resolve_styles(&doc, &sheets).unwrap(); + let styled = resolve_styles(&doc, &sheets, (800.0, 600.0)).unwrap(); // The and