render: resolve contextual gaps and empty block lines
This commit is contained in:
1 parent
4d9da4eb31
commit
9b0b138fb9
6 files changed
+186
-4
No files matched your search
@@ -593,6 +593,26 @@ pub fn layout_dom_with_images(
|
|||||||
// em in non-font-size properties is relative to this element's
|
// em in non-font-size properties is relative to this element's
|
||||||
// OWN computed font-size; resolve every relative length now.
|
// OWN computed font-size; resolve every relative length now.
|
||||||
let em_px = style.font_size.unwrap_or(parent_fs);
|
let em_px = style.font_size.unwrap_or(parent_fs);
|
||||||
|
if let Some(expression) = style.row_gap_expression.as_deref() {
|
||||||
|
style.row_gap = crate::style::resolve_contextual_length(
|
||||||
|
expression,
|
||||||
|
em_px,
|
||||||
|
root_fs,
|
||||||
|
vw,
|
||||||
|
vh,
|
||||||
|
inh.cb_width,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(expression) = style.column_gap_expression.as_deref() {
|
||||||
|
style.column_gap = crate::style::resolve_contextual_length(
|
||||||
|
expression,
|
||||||
|
em_px,
|
||||||
|
root_fs,
|
||||||
|
vw,
|
||||||
|
vh,
|
||||||
|
inh.cb_width,
|
||||||
|
);
|
||||||
|
}
|
||||||
if let Some(expression) = style.line_height_expression.as_deref() {
|
if let Some(expression) = style.line_height_expression.as_deref() {
|
||||||
if let Some(resolved) = crate::style::resolve_contextual_length(
|
if let Some(resolved) = crate::style::resolve_contextual_length(
|
||||||
expression,
|
expression,
|
||||||
@@ -1646,6 +1666,8 @@ fn propagate_border_spacing(tree: &DomTree, styles: &mut HashMap<NodeId, crate::
|
|||||||
if let Some(s) = styles.get_mut(&cid) {
|
if let Some(s) = styles.get_mut(&cid) {
|
||||||
s.column_gap = Some(h);
|
s.column_gap = Some(h);
|
||||||
s.row_gap = Some(v);
|
s.row_gap = Some(v);
|
||||||
|
s.column_gap_expression = None;
|
||||||
|
s.row_gap_expression = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
apply_to_rows(tree, cid, h, v, styles);
|
apply_to_rows(tree, cid, h, v, styles);
|
||||||
@@ -1662,6 +1684,7 @@ fn propagate_border_spacing(tree: &DomTree, styles: &mut HashMap<NodeId, crate::
|
|||||||
let (h, v) = table_spacing(table_style);
|
let (h, v) = table_spacing(table_style);
|
||||||
if let Some(s) = styles.get_mut(&id) {
|
if let Some(s) = styles.get_mut(&id) {
|
||||||
s.row_gap = Some(v);
|
s.row_gap = Some(v);
|
||||||
|
s.row_gap_expression = None;
|
||||||
}
|
}
|
||||||
apply_to_rows(tree, id, h, v, styles);
|
apply_to_rows(tree, id, h, v, styles);
|
||||||
}
|
}
|
||||||
@@ -3434,6 +3457,18 @@ fn build(
|
|||||||
// it the correctly ordered item sequence is the missing translation.
|
// it the correctly ordered item sequence is the missing translation.
|
||||||
dom_children.sort_by_key(|cid| styles.get(cid).map(|style| style.order).unwrap_or(0));
|
dom_children.sort_by_key(|cid| styles.get(cid).map(|style| style.order).unwrap_or(0));
|
||||||
} else if style.display == crate::Display::Block {
|
} else if style.display == crate::Display::Block {
|
||||||
|
// Formatting whitespace between block lines does not generate an
|
||||||
|
// inline line box. Keeping a zero-height taffy leaf here still breaks
|
||||||
|
// sibling margin adjacency, turning `margin-bottom:30px` followed by
|
||||||
|
// `margin-top:40px` into 70px instead of the collapsed 40px. Preserve
|
||||||
|
// whitespace only when this parent actually has inline content, where
|
||||||
|
// the inter-element space belongs to the inline formatting context.
|
||||||
|
if !has_inline_ish_content {
|
||||||
|
dom_children.retain(|&cid| {
|
||||||
|
tree.get_node(cid).map_or(false, |node| node.is_element())
|
||||||
|
|| !tree.text_content(cid).trim().is_empty()
|
||||||
|
});
|
||||||
|
}
|
||||||
// A float nested directly in a transparent inline wrapper (the common
|
// A float nested directly in a transparent inline wrapper (the common
|
||||||
// `<a><img style="float:left"></a>` logo pattern) belongs to the
|
// `<a><img style="float:left"></a>` logo pattern) belongs to the
|
||||||
// ancestor block formatting context. Gecko reparents that out-of-flow
|
// ancestor block formatting context. Gecko reparents that out-of-flow
|
||||||
@@ -4694,6 +4729,50 @@ mod tests {
|
|||||||
assert_eq!(size("inherited-border").0, 100.0);
|
assert_eq!(size("inherited-border").0, 100.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn root_percentage_font_size_controls_rem_lengths() {
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"<style>
|
||||||
|
html { font-size: 62.5% }
|
||||||
|
body { margin: 0 }
|
||||||
|
#box {
|
||||||
|
display: grid;
|
||||||
|
width: 4rem;
|
||||||
|
margin-top: 3rem;
|
||||||
|
row-gap: 4rem;
|
||||||
|
column-gap: calc(1rem + 2vw);
|
||||||
|
}
|
||||||
|
#box > div { height: 2rem }
|
||||||
|
</style>
|
||||||
|
<div id="box"><div></div><div></div></div>"#,
|
||||||
|
);
|
||||||
|
let laid = layout_dom(&tree, (1280.0, 720.0));
|
||||||
|
let html = tree.query_selector("html").unwrap().unwrap();
|
||||||
|
let box_id = tree.query_selector("#box").unwrap().unwrap();
|
||||||
|
assert_eq!(laid.styles.get(&html).unwrap().font_size, Some(10.0));
|
||||||
|
assert_eq!(laid.styles.get(&box_id).unwrap().row_gap, Some(40.0));
|
||||||
|
assert_eq!(laid.styles.get(&box_id).unwrap().column_gap, Some(35.6));
|
||||||
|
assert_eq!(laid.rects.get(&box_id).unwrap().width, 40.0);
|
||||||
|
assert_eq!(laid.rects.get(&box_id).unwrap().height, 80.0);
|
||||||
|
assert_eq!(laid.rects.get(&box_id).unwrap().y, 30.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn block_and_grid_sibling_margins_collapse() {
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"<style>
|
||||||
|
body { margin: 0 }
|
||||||
|
.a { position: relative; height: 10px; margin-bottom: 30px }
|
||||||
|
.b { position: relative; display: grid; height: 10px; margin-top: 40px }
|
||||||
|
</style>
|
||||||
|
<div class="a"></div>
|
||||||
|
<div id="b" class="b"></div>"#,
|
||||||
|
);
|
||||||
|
let laid = layout_dom(&tree, (1280.0, 720.0));
|
||||||
|
let b = tree.query_selector("#b").unwrap().unwrap();
|
||||||
|
assert_eq!(laid.rects.get(&b).unwrap().y, 50.0);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn table_cell_content_box_width_includes_padding_and_border_in_track() {
|
fn table_cell_content_box_width_includes_padding_and_border_in_track() {
|
||||||
let tree = parse_html(
|
let tree = parse_html(
|
||||||
|
|||||||
@@ -356,6 +356,12 @@ pub struct LayoutStyle {
|
|||||||
pub grid_row_raw: Option<String>,
|
pub grid_row_raw: Option<String>,
|
||||||
pub column_gap: Option<f32>,
|
pub column_gap: Option<f32>,
|
||||||
pub row_gap: Option<f32>,
|
pub row_gap: Option<f32>,
|
||||||
|
/// Deferred gap values. Font- and viewport-relative units cannot be
|
||||||
|
/// converted until the element's computed font-size and the live viewport
|
||||||
|
/// are known; eagerly treating `rem` as 16px breaks pages that customize
|
||||||
|
/// the root font-size.
|
||||||
|
pub column_gap_expression: Option<String>,
|
||||||
|
pub row_gap_expression: Option<String>,
|
||||||
|
|
||||||
/// `border-spacing: <horizontal> <vertical>?` (or the `cellspacing`
|
/// `border-spacing: <horizontal> <vertical>?` (or the `cellspacing`
|
||||||
/// attribute). Only meaningful on a `<table>`; taffy has no native table
|
/// attribute). Only meaningful on a `<table>`; taffy has no native table
|
||||||
|
|||||||
@@ -763,11 +763,14 @@ fn apply_value(style: &mut LayoutStyle, name: &str, value: &str) {
|
|||||||
style.underline = Some(underline && !none);
|
style.underline = Some(underline && !none);
|
||||||
}
|
}
|
||||||
"gap" | "grid-gap" => {
|
"gap" | "grid-gap" => {
|
||||||
let dims: Vec<f32> = value.split_whitespace().filter_map(px_value).collect();
|
let values = split_ws_paren(value);
|
||||||
if let Some(&r) = dims.first() { style.row_gap = Some(r); style.column_gap = Some(*dims.get(1).unwrap_or(&r)); }
|
if let Some(row) = values.first() {
|
||||||
|
apply_gap_value(style, true, row);
|
||||||
|
apply_gap_value(style, false, values.get(1).copied().unwrap_or(row));
|
||||||
}
|
}
|
||||||
"row-gap" | "grid-row-gap" => style.row_gap = px(value),
|
}
|
||||||
"column-gap" | "grid-column-gap" => style.column_gap = px(value),
|
"row-gap" | "grid-row-gap" => apply_gap_value(style, true, value),
|
||||||
|
"column-gap" | "grid-column-gap" => apply_gap_value(style, false, value),
|
||||||
"border-spacing" => {
|
"border-spacing" => {
|
||||||
let dims: Vec<f32> = value.split_whitespace().filter_map(px_value).collect();
|
let dims: Vec<f32> = value.split_whitespace().filter_map(px_value).collect();
|
||||||
if let Some(&h) = dims.first() {
|
if let Some(&h) = dims.first() {
|
||||||
@@ -2421,6 +2424,36 @@ fn apply_font_size(style: &mut LayoutStyle, value: &str) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn apply_gap_value(style: &mut LayoutStyle, row: bool, value: &str) {
|
||||||
|
let value = value.trim();
|
||||||
|
let lower = value.to_ascii_lowercase();
|
||||||
|
let contextual = lower.contains('(')
|
||||||
|
|| lower.ends_with('%')
|
||||||
|
|| ["rem", "em", "ex", "vw", "vh", "vmin", "vmax"]
|
||||||
|
.iter()
|
||||||
|
.any(|unit| lower.ends_with(unit));
|
||||||
|
let expression = if value.eq_ignore_ascii_case("normal")
|
||||||
|
|| value.is_empty()
|
||||||
|
|| !contextual
|
||||||
|
{
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(value.to_string())
|
||||||
|
};
|
||||||
|
let immediate = if value.eq_ignore_ascii_case("normal") || value.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
px(value)
|
||||||
|
};
|
||||||
|
if row {
|
||||||
|
style.row_gap = immediate;
|
||||||
|
style.row_gap_expression = expression;
|
||||||
|
} else {
|
||||||
|
style.column_gap = immediate;
|
||||||
|
style.column_gap_expression = expression;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn line_height_expression_is_length(value: &str) -> bool {
|
pub(crate) fn line_height_expression_is_length(value: &str) -> bool {
|
||||||
let lower = value.to_ascii_lowercase();
|
let lower = value.to_ascii_lowercase();
|
||||||
lower.contains('%')
|
lower.contains('%')
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Block whitespace margin collapse</title>
|
||||||
|
<style>
|
||||||
|
html, body { margin: 0; }
|
||||||
|
.first {
|
||||||
|
height: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
background: #087f5b;
|
||||||
|
}
|
||||||
|
.second {
|
||||||
|
display: grid;
|
||||||
|
height: 20px;
|
||||||
|
margin-top: 40px;
|
||||||
|
background: #1971c2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="first"></div>
|
||||||
|
<div class="second"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -211,6 +211,16 @@
|
|||||||
{"name": "vmin margin resolves against smaller viewport axis", "color": "#f08c00", "x": 90, "y": 440, "width": 100, "height": 40},
|
{"name": "vmin margin resolves against smaller viewport axis", "color": "#f08c00", "x": 90, "y": 440, "width": 100, "height": 40},
|
||||||
{"name": "vmax margin resolves against larger viewport axis", "color": "#1098ad", "x": 100, "y": 520, "width": 100, "height": 40}
|
{"name": "vmax margin resolves against larger viewport axis", "color": "#1098ad", "x": 100, "y": 520, "width": 100, "height": 40}
|
||||||
],
|
],
|
||||||
|
"contextual-grid-gap": [
|
||||||
|
{"name": "first cell starts at the grid origin", "color": "#e03131", "x": 0, "y": 0, "width": 10, "height": 20},
|
||||||
|
{"name": "functional column gap uses root and viewport context", "color": "#2f9e44", "x": 38, "y": 0, "width": 10, "height": 20},
|
||||||
|
{"name": "rem row gap uses the computed root font size", "color": "#1971c2", "x": 0, "y": 60, "width": 10, "height": 20},
|
||||||
|
{"name": "both contextual gaps place the final cell", "color": "#f08c00", "x": 38, "y": 60, "width": 10, "height": 20}
|
||||||
|
],
|
||||||
|
"block-whitespace-margin-collapse": [
|
||||||
|
{"name": "first block keeps its declared geometry", "color": "#087f5b", "x": 0, "y": 0, "width": 900, "height": 20},
|
||||||
|
{"name": "formatting whitespace does not interrupt sibling margin collapse", "color": "#1971c2", "x": 0, "y": 60, "width": 900, "height": 20}
|
||||||
|
],
|
||||||
"replaced-grid-order": [
|
"replaced-grid-order": [
|
||||||
{"name": "order-modified description occupies the first grid cell", "color": "#e8590c", "x": 0, "y": 0, "width": 300, "height": 200},
|
{"name": "order-modified description occupies the first grid cell", "color": "#e8590c", "x": 0, "y": 0, "width": 300, "height": 200},
|
||||||
{"name": "media wrapper shares the first row", "color": "#1971c2", "x": 300, "y": 0, "width": 300, "height": 300},
|
{"name": "media wrapper shares the first row", "color": "#1971c2", "x": 300, "y": 0, "width": 300, "height": 300},
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Contextual grid gap</title>
|
||||||
|
<style>
|
||||||
|
html { font-size: 62.5%; }
|
||||||
|
body { margin: 0; }
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 10px 10px;
|
||||||
|
grid-template-rows: 20px 20px;
|
||||||
|
gap: 4rem calc(1rem + 2vw);
|
||||||
|
}
|
||||||
|
.a { background: #e03131; }
|
||||||
|
.b { background: #2f9e44; }
|
||||||
|
.c { background: #1971c2; }
|
||||||
|
.d { background: #f08c00; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="grid">
|
||||||
|
<div class="a"></div>
|
||||||
|
<div class="b"></div>
|
||||||
|
<div class="c"></div>
|
||||||
|
<div class="d"></div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in new issue
Block a user