diff --git a/crates/layout/src/lib.rs b/crates/layout/src/lib.rs index 6cc4b41..7636f94 100644 --- a/crates/layout/src/lib.rs +++ b/crates/layout/src/lib.rs @@ -8,8 +8,8 @@ use std::collections::HashMap; use we_css::values::Color; use we_dom::{Document, NodeData, NodeId}; use we_style::computed::{ - BorderStyle, ComputedStyle, Display, LengthOrAuto, Overflow, Position, StyledNode, TextAlign, - TextDecoration, + BorderStyle, BoxSizing, ComputedStyle, Display, LengthOrAuto, Overflow, Position, StyledNode, + TextAlign, TextDecoration, }; use we_text::font::Font; @@ -95,6 +95,12 @@ pub struct LayoutBox { pub relative_offset: (f32, f32), /// CSS `overflow` property. pub overflow: Overflow, + /// CSS `box-sizing` property. + pub box_sizing: BoxSizing, + /// CSS `width` property (explicit or auto). + pub css_width: LengthOrAuto, + /// CSS `height` property (explicit or auto). + pub css_height: LengthOrAuto, } impl LayoutBox { @@ -129,6 +135,9 @@ impl LayoutBox { position: style.position, relative_offset: (0.0, 0.0), overflow: style.overflow, + box_sizing: style.box_sizing, + css_width: style.width, + css_height: style.height, } } @@ -419,14 +428,19 @@ fn compute_layout( ) { 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; - let content_width = (available_width - - b.margin.left - - b.margin.right - - b.border.left - - b.border.right - - b.padding.left - - b.padding.right) - .max(0.0); + + let horizontal_extra = b.border.left + b.border.right + b.padding.left + b.padding.right; + + // Resolve content width: explicit CSS width (adjusted for box-sizing) or auto. + let content_width = match b.css_width { + LengthOrAuto::Length(w) => match b.box_sizing { + BoxSizing::ContentBox => w.max(0.0), + BoxSizing::BorderBox => (w - horizontal_extra).max(0.0), + }, + LengthOrAuto::Auto => { + (available_width - b.margin.left - b.margin.right - horizontal_extra).max(0.0) + } + }; b.rect.x = content_x; b.rect.y = content_y; @@ -455,6 +469,15 @@ 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), + }; + } + apply_relative_offset(b); } @@ -2193,4 +2216,174 @@ p { margin-top: 5px; margin-bottom: 5px; } assert_eq!(collapse_margins(-5.0, 20.0), 15.0); assert_eq!(collapse_margins(0.0, 0.0), 0.0); } + + // --- Box-sizing tests --- + + #[test] + fn content_box_default_width_applies_to_content() { + // Default box-sizing (content-box): width = content width only. + 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).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]; + + // content-box: rect.width = 200 (the specified width IS the content) + assert_eq!(div_box.rect.width, 200.0); + assert_eq!(div_box.padding.left, 10.0); + assert_eq!(div_box.padding.right, 10.0); + assert_eq!(div_box.border.left, 5.0); + assert_eq!(div_box.border.right, 5.0); + } + + #[test] + fn border_box_width_includes_padding_and_border() { + // box-sizing: border-box: width includes padding and border. + 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).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]; + + // border-box: content width = 200 - 10*2 (padding) - 5*2 (border) = 170 + assert_eq!(div_box.rect.width, 170.0); + assert_eq!(div_box.padding.left, 10.0); + assert_eq!(div_box.padding.right, 10.0); + assert_eq!(div_box.border.left, 5.0); + assert_eq!(div_box.border.right, 5.0); + } + + #[test] + fn border_box_padding_exceeds_width_clamps_to_zero() { + // border-box with padding+border > specified width: content clamps to 0. + let html_str = r#" + + + +
X
+ +"#; + 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 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]; + + // border-box: content = 20 - 15*2 - 5*2 = 20 - 40 = -20 → clamped to 0 + assert_eq!(div_box.rect.width, 0.0); + } + + #[test] + fn box_sizing_is_not_inherited() { + // box-sizing is not inherited: child should use default content-box. + let html_str = r#" + + + +
Inner
+ +"#; + 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 tree = layout(&styled, &doc, 800.0, 600.0, &font, &HashMap::new()); + + let body_box = &tree.root.children[0]; + let parent_box = &body_box.children[0]; + let child_box = &parent_box.children[0]; + + // Parent: border-box → content = 300 - 20 - 10 = 270 + assert_eq!(parent_box.rect.width, 270.0); + // Child: default content-box → content = 100 (not reduced by padding/border) + assert_eq!(child_box.rect.width, 100.0); + } + + #[test] + fn border_box_height() { + // box-sizing: border-box also applies to height. + 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).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]; + + // border-box: content height = 100 - 10*2 (padding) - 5*2 (border) = 70 + assert_eq!(div_box.rect.height, 70.0); + } + + #[test] + fn content_box_explicit_height() { + // content-box: height applies to content only. + 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).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]; + + // content-box: rect.height = 100 (specified height IS content height) + assert_eq!(div_box.rect.height, 100.0); + } } diff --git a/crates/style/src/computed.rs b/crates/style/src/computed.rs index eba5c12..c34ffe2 100644 --- a/crates/style/src/computed.rs +++ b/crates/style/src/computed.rs @@ -89,6 +89,18 @@ pub enum TextDecoration { LineThrough, } +// --------------------------------------------------------------------------- +// BoxSizing +// --------------------------------------------------------------------------- + +/// CSS `box-sizing` property values. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum BoxSizing { + #[default] + ContentBox, + BorderBox, +} + // --------------------------------------------------------------------------- // Overflow // --------------------------------------------------------------------------- @@ -189,6 +201,9 @@ pub struct ComputedStyle { pub width: LengthOrAuto, pub height: LengthOrAuto, + // Box model: sizing + pub box_sizing: BoxSizing, + // Text / inherited pub color: Color, pub font_size: f32, @@ -249,6 +264,8 @@ impl Default for ComputedStyle { width: LengthOrAuto::Auto, height: LengthOrAuto::Auto, + box_sizing: BoxSizing::ContentBox, + color: Color::rgb(0, 0, 0), font_size: 16.0, font_weight: FontWeight(400.0), @@ -593,6 +610,18 @@ fn apply_property( style.height = resolve_length_or_auto(value, parent_fs, current_fs); } + // Box sizing + "box-sizing" => { + style.box_sizing = match value { + CssValue::Keyword(k) => match k.as_str() { + "content-box" => BoxSizing::ContentBox, + "border-box" => BoxSizing::BorderBox, + _ => style.box_sizing, + }, + _ => style.box_sizing, + }; + } + // Color (inherited) "color" => { if let Some(c) = resolve_color(value, parent.color) { @@ -832,6 +861,7 @@ fn inherit_property(style: &mut ComputedStyle, property: &str, parent: &Computed "padding-left" => style.padding_left = parent.padding_left, "width" => style.width = parent.width, "height" => style.height = parent.height, + "box-sizing" => style.box_sizing = parent.box_sizing, "background-color" => style.background_color = parent.background_color, "position" => style.position = parent.position, "overflow" => style.overflow = parent.overflow, @@ -857,6 +887,7 @@ fn reset_property_to_initial(style: &mut ComputedStyle, property: &str) { "border-left-width" => style.border_left_width = initial.border_left_width, "width" => style.width = initial.width, "height" => style.height = initial.height, + "box-sizing" => style.box_sizing = initial.box_sizing, "color" => style.color = initial.color, "font-size" => { style.font_size = initial.font_size; @@ -1979,4 +2010,44 @@ mod tests { // Later stylesheet wins. assert_eq!(p_node.style.color, Color::rgb(0, 0, 255)); } + + #[test] + fn box_sizing_content_box_default() { + let html_str = r#" +
Test
"#; + let doc = we_html::parse_html(html_str); + let sheets = extract_stylesheets(&doc); + let styled = resolve_styles(&doc, &sheets).unwrap(); + let body = &styled.children[0]; + let div = &body.children[0]; + assert_eq!(div.style.box_sizing, BoxSizing::ContentBox); + } + + #[test] + fn box_sizing_border_box_parsed() { + let html_str = r#" + +
Test
"#; + let doc = we_html::parse_html(html_str); + let sheets = extract_stylesheets(&doc); + let styled = resolve_styles(&doc, &sheets).unwrap(); + let body = &styled.children[0]; + let div = &body.children[0]; + assert_eq!(div.style.box_sizing, BoxSizing::BorderBox); + } + + #[test] + fn box_sizing_not_inherited() { + let html_str = r#" + +

Child

"#; + let doc = we_html::parse_html(html_str); + let sheets = extract_stylesheets(&doc); + let styled = resolve_styles(&doc, &sheets).unwrap(); + let body = &styled.children[0]; + let parent = &body.children[0]; + let child = &parent.children[0]; + assert_eq!(parent.style.box_sizing, BoxSizing::BorderBox); + assert_eq!(child.style.box_sizing, BoxSizing::ContentBox); + } }