render: respect CSS box sizing

This commit is contained in:
SGavrl committed 2026-07-23 10:06:55 +00:00
1 parent a11f87d0c9
commit 4979606a2f
4 files changed
+172 -16

No files matched your search

+95 -10
View File
@@ -347,6 +347,7 @@ pub fn layout_dom_with_images(
line_height: crate::LineHeight,
text_transform: crate::TextTransform,
italic: bool,
box_sizing: crate::BoxSizing,
/// Containing-block width in px for the current element, carried
/// down so percentage padding/margin (which resolve against the
/// containing block WIDTH, all sides) can be turned into px before
@@ -368,6 +369,7 @@ pub fn layout_dom_with_images(
line_height: crate::LineHeight::Normal,
text_transform: crate::TextTransform::None,
italic: false,
box_sizing: crate::BoxSizing::ContentBox,
cb_width: 0.0,
}
}
@@ -441,6 +443,10 @@ pub fn layout_dom_with_images(
match style.line_height { Some(v) => inh.line_height = v, None => style.line_height = Some(inh.line_height) }
match style.text_transform { Some(v) => inh.text_transform = v, None => style.text_transform = Some(inh.text_transform) }
match style.font_style_italic { Some(v) => inh.italic = v, None => style.font_style_italic = Some(inh.italic) }
if style.box_sizing == crate::BoxSizing::Inherit {
style.box_sizing = inh.box_sizing;
}
inh.box_sizing = style.box_sizing;
// Percentage padding/margin resolve against the containing
// block WIDTH on every side (per CSS: `padding-top:56.25%` in a
@@ -470,20 +476,28 @@ pub fn layout_dom_with_images(
}
// Containing-block width handed to this element's children is
// its own content-box width. taffy sizes width as border-box
// (its default box_sizing), so subtract this element's resolved
// padding and border; an auto width fills the containing block.
// its own content-box width. A definite content-box width is
// already that value; a border-box or auto width includes
// padding and border, which must be removed.
let used_w = match style.width {
crate::Dimension::Px(w) => w,
crate::Dimension::Percent(p) => p * cb_w,
_ => (cb_w - style.margin.left - style.margin.right).max(0.0),
};
child_cb_width = (used_w
- style.padding.left
- style.padding.right
- style.border.left
- style.border.right)
.max(0.0);
let definite_content_box = matches!(
style.width,
crate::Dimension::Px(_) | crate::Dimension::Percent(_)
) && style.box_sizing == crate::BoxSizing::ContentBox;
child_cb_width = if definite_content_box {
used_w.max(0.0)
} else {
(used_w
- style.padding.left
- style.padding.right
- style.border.left
- style.border.right)
.max(0.0)
};
}
inh.cb_width = child_cb_width;
for cid in tree.children(id).into_iter().rev() {
@@ -1614,7 +1628,22 @@ fn build_table(
if *cs != 1 || *c >= ncols {
continue;
}
let (px, pct) = style_width(*cid);
let (mut px, pct) = style_width(*cid);
// A fixed width declared on a cell describes its content box unless
// the author opted into border-box. Grid tracks describe the cell's
// outer border box, so carry padding and border into the pinned track.
// `<col>` widths above already describe the column track and must not
// receive a particular cell's box edges.
if let (Some(w), Some(s)) = (px, styles.get(cid)) {
if s.box_sizing == crate::BoxSizing::ContentBox {
px = Some(
w + s.padding.left
+ s.padding.right
+ s.border.left
+ s.border.right,
);
}
}
if let Some(w) = px {
col_px[*c] = Some(col_px[*c].map_or(w, |cur| cur.max(w)));
}
@@ -2380,4 +2409,60 @@ mod tests {
assert_eq!(style.background_color, Some([0, 128, 0, 255]), "wrong cascade color for {id}");
}
}
#[test]
fn box_sizing_controls_the_declared_size_edge() {
let tree = parse_html(
r#"<style>
body { margin: 0 }
.box { width:100px; height:50px; padding:10px; border:2px solid black }
.border { box-sizing:border-box }
.parent { width:400px }
.half { width:50%; padding:10px; border:2px solid black }
.limited { width:200px; max-width:100px; padding:10px; border:2px solid black }
.inherit-parent { box-sizing:border-box }
.inherit-child { box-sizing:inherit; width:100px; padding:10px; border:2px solid black }
</style>
<div id="content" class="box"></div>
<div id="border" class="box border"></div>
<div class="parent"><div id="half-content" class="half"></div><div id="half-border" class="half border"></div></div>
<div id="max-content" class="limited"></div>
<div id="max-border" class="limited border"></div>
<div class="inherit-parent"><div id="inherited-border" class="inherit-child"></div></div>"#,
);
let laid = layout_dom(&tree, (1280.0, 720.0));
let size = |id: &str| {
let nid = tree.query_selector(&format!("#{id}")).unwrap().unwrap();
let rect = laid.rects.get(&nid).unwrap();
(rect.width, rect.height)
};
assert_eq!(size("content"), (124.0, 74.0));
assert_eq!(size("border"), (100.0, 50.0));
assert_eq!(size("half-content").0, 224.0);
assert_eq!(size("half-border").0, 200.0);
assert_eq!(size("max-content").0, 124.0);
assert_eq!(size("max-border").0, 100.0);
assert_eq!(size("inherited-border").0, 100.0);
}
#[test]
fn table_cell_content_box_width_includes_padding_and_border_in_track() {
let tree = parse_html(
r#"<style>
table { border-spacing:0 }
td { width:100px; padding:10px; border:2px solid black }
.border { box-sizing:border-box }
</style>
<table><tr><td id="content-cell">content</td></tr></table>
<table><tr><td id="border-cell" class="border">border</td></tr></table>"#,
);
let laid = layout_dom(&tree, (1280.0, 720.0));
let width = |id: &str| {
let nid = tree.query_selector(&format!("#{id}")).unwrap().unwrap();
laid.rects.get(&nid).unwrap().width
};
assert_eq!(width("content-cell"), 124.0);
assert_eq!(width("border-cell"), 100.0);
}
}
+29 -6
View File
@@ -167,6 +167,9 @@ pub struct LayoutStyle {
pub display: Display,
pub width: Dimension,
pub height: Dimension,
/// Which box edge `width`/`height` and min/max sizes describe. CSS starts
/// at `content-box`; many modern reset sheets opt into `border-box`.
pub box_sizing: BoxSizing,
/// Whether `width`/`height` was set by an author rule (including an explicit
/// `auto`). Presentational `width`/`height` HTML attributes are a lower
/// priority than author CSS, so they apply only when these are false; an
@@ -428,6 +431,16 @@ pub enum Float {
Right,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BoxSizing {
#[default]
ContentBox,
BorderBox,
/// A specified CSS-wide `inherit` value. The DOM's top-down computed-style
/// pass resolves this to the parent's computed value before layout.
Inherit,
}
/// One `box-shadow` layer. Offsets, blur, and spread are in CSS px; `color` is
/// the resolved RGBA (falling back to the element's text color, per CSS
/// `currentColor`, when the value omits a color); `inset` distinguishes an
@@ -577,6 +590,13 @@ fn read_node(tree: &TaffyTree, id: NodeId) -> NodeRect {
pub(crate) fn to_taffy_style(style: &LayoutStyle) -> Style {
let mut s = Style::DEFAULT;
s.box_sizing = match style.box_sizing {
BoxSizing::ContentBox => taffy::BoxSizing::ContentBox,
BoxSizing::BorderBox => taffy::BoxSizing::BorderBox,
// Programmatic LayoutStyle users do not have a DOM inheritance pass;
// fall back to the property's CSS initial value in that case.
BoxSizing::Inherit => taffy::BoxSizing::ContentBox,
};
// A block box that wants non-default cross-axis alignment (from
// text-align: center/right, the only way our engine currently sets
@@ -820,8 +840,8 @@ mod tests {
}
#[test]
fn padding_insets_the_box() {
let root = LayoutNode {
fn padding_expands_content_box_but_not_border_box() {
let content_box = LayoutNode {
style: LayoutStyle {
display: Display::Block,
width: Dimension::Px(100.0),
@@ -832,9 +852,12 @@ mod tests {
text: None,
children: vec![],
};
let out = layout(&root, (1000.0, 800.0));
// Border box still matches the requested size; padding affects content,
// not the outer border box here.
assert_eq!(out.border_box.width, 100.0);
let content_out = layout(&content_box, (1000.0, 800.0));
assert_eq!(content_out.border_box.width, 120.0);
let mut border_box = content_box;
border_box.style.box_sizing = BoxSizing::BorderBox;
let border_out = layout(&border_box, (1000.0, 800.0));
assert_eq!(border_out.border_box.width, 100.0);
}
}
+35
View File
@@ -265,6 +265,17 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
style.height = dimension_value(value);
style.height_set = true;
}
"box-sizing" => {
style.box_sizing = if value.eq_ignore_ascii_case("border-box") {
crate::BoxSizing::BorderBox
} else if value.eq_ignore_ascii_case("content-box") {
crate::BoxSizing::ContentBox
} else if value.eq_ignore_ascii_case("inherit") {
crate::BoxSizing::Inherit
} else {
style.box_sizing
};
}
"min-width" => style.min_width = dimension_value(value),
"min-height" => style.min_height = dimension_value(value),
"max-width" => style.max_width = dimension_value(value),
@@ -2127,6 +2138,30 @@ mod tests {
assert_eq!(s.height, crate::Dimension::Auto);
}
#[test]
fn box_sizing_defaults_to_content_box_and_parses_both_keywords() {
assert_eq!(
compute_style("div", None).box_sizing,
crate::BoxSizing::ContentBox
);
assert_eq!(
compute_style("div", Some("box-sizing:border-box")).box_sizing,
crate::BoxSizing::BorderBox
);
assert_eq!(
compute_style(
"div",
Some("box-sizing:border-box;box-sizing:content-box")
)
.box_sizing,
crate::BoxSizing::ContentBox
);
assert_eq!(
compute_style("div", Some("box-sizing:inherit")).box_sizing,
crate::BoxSizing::Inherit
);
}
#[test]
fn calc_with_multiply_and_divide() {
// The exact shape MediaWiki uses to offset a TOC toggle button into