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#"
+
+
+
+
+
+"#;
+ 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#"
+
+"#;
+ 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);
+ }
}