render: respect CSS box sizing
This commit is contained in:
1 parent
a11f87d0c9
commit
4979606a2f
4 files changed
+172
-16
No files matched your search
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user