Constrain non-stretched flex text
This commit is contained in:
1 parent
1434c2072d
commit
a008a484d3
1 file changed
+182
@@ -4950,6 +4950,23 @@ fn build(
|
|||||||
|
|
||||||
let mut taffy_style = to_taffy_style(style);
|
let mut taffy_style = to_taffy_style(style);
|
||||||
|
|
||||||
|
// A non-stretched flex item in a column flex container uses fit-content
|
||||||
|
// for its auto inline size. In a nested flex layout taffy can retain the
|
||||||
|
// item's earlier max-content measurement after the outer row shrinks its
|
||||||
|
// column, so direct prose stays one enormous line instead of being
|
||||||
|
// remeasured at the final column width (MDN's contributor quote).
|
||||||
|
//
|
||||||
|
// Taffy has no fit-content box-size value. For the narrow text-container
|
||||||
|
// case, a synthetic percentage max has the same final effect: it is
|
||||||
|
// indefinite during intrinsic measurement, then caps the item to its
|
||||||
|
// containing block once that width is known. The automatic min-content
|
||||||
|
// size still wins for an unbreakable word, preserving intentional
|
||||||
|
// overflow. Keep the workaround away from replaced/atomic boxes and
|
||||||
|
// authored sizing constraints.
|
||||||
|
if needs_column_flex_text_fit_content_cap(tree, id, style, styles) {
|
||||||
|
taffy_style.max_size.width = taffy::Dimension::percent(1.0);
|
||||||
|
}
|
||||||
|
|
||||||
// A grid item's automatic minimum size is clamped by a definite
|
// A grid item's automatic minimum size is clamped by a definite
|
||||||
// max-width. Taffy's intrinsic track pass cannot resolve a percentage max
|
// max-width. Taffy's intrinsic track pass cannot resolve a percentage max
|
||||||
// until the track exists, so `max-width:100%` otherwise participates with
|
// until the track exists, so `max-width:100%` otherwise participates with
|
||||||
@@ -5755,6 +5772,90 @@ fn build_mixed_block(
|
|||||||
Some(taffy_id)
|
Some(taffy_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn needs_column_flex_text_fit_content_cap(
|
||||||
|
tree: &DomTree,
|
||||||
|
id: NodeId,
|
||||||
|
style: &crate::LayoutStyle,
|
||||||
|
styles: &HashMap<NodeId, crate::LayoutStyle>,
|
||||||
|
) -> bool {
|
||||||
|
if style.display != crate::Display::Flex
|
||||||
|
|| style.internal_flex_container
|
||||||
|
|| style.is_inline_block
|
||||||
|
|| style.float.is_some()
|
||||||
|
|| matches!(style.position, Some(taffy::Position::Absolute))
|
||||||
|
|| style.aspect_ratio.is_some()
|
||||||
|
|| !matches!(
|
||||||
|
(style.width, style.min_width, style.max_width),
|
||||||
|
(
|
||||||
|
crate::Dimension::Auto,
|
||||||
|
crate::Dimension::Auto,
|
||||||
|
crate::Dimension::Auto
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|| style.size_expressions[0].is_some()
|
||||||
|
|| style.size_expressions[2].is_some()
|
||||||
|
|| style.size_expressions[4].is_some()
|
||||||
|
|| style.padding.left != 0.0
|
||||||
|
|| style.padding.right != 0.0
|
||||||
|
|| style.border.left != 0.0
|
||||||
|
|| style.border.right != 0.0
|
||||||
|
|| style.margin.left != 0.0
|
||||||
|
|| style.margin.right != 0.0
|
||||||
|
|| style.margin_auto[1]
|
||||||
|
|| style.margin_auto[3]
|
||||||
|
|| style.margin_percent[1].is_some()
|
||||||
|
|| style.margin_percent[3].is_some()
|
||||||
|
|| style.margin_relative[1].is_some()
|
||||||
|
|| style.margin_relative[3].is_some()
|
||||||
|
|| style.margin_expressions[1].is_some()
|
||||||
|
|| style.margin_expressions[3].is_some()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let has_direct_text = tree.children(id).into_iter().any(|child| {
|
||||||
|
tree.get_node(child).map_or(false, |node| {
|
||||||
|
matches!(
|
||||||
|
&node.data,
|
||||||
|
obscura_dom::tree::NodeData::Text { contents }
|
||||||
|
if !contents.trim().is_empty()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if !has_direct_text {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut parent = tree.get_node(id).and_then(|node| node.parent);
|
||||||
|
let parent_style = loop {
|
||||||
|
let Some(parent_id) = parent else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Some(parent_style) = styles.get(&parent_id) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if parent_style.display_contents {
|
||||||
|
parent = tree.get_node(parent_id).and_then(|node| node.parent);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break parent_style;
|
||||||
|
};
|
||||||
|
if parent_style.display != crate::Display::Flex
|
||||||
|
|| parent_style.internal_flex_container
|
||||||
|
|| !matches!(
|
||||||
|
parent_style.flex_direction,
|
||||||
|
Some(taffy::FlexDirection::Column | taffy::FlexDirection::ColumnReverse)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
style
|
||||||
|
.align_self
|
||||||
|
.unwrap_or(parent_style.align_items.unwrap_or(taffy::AlignItems::STRETCH))
|
||||||
|
!= taffy::AlignSelf::STRETCH
|
||||||
|
}
|
||||||
|
|
||||||
/// Expand `display: contents` wrappers so their children partition into the
|
/// Expand `display: contents` wrappers so their children partition into the
|
||||||
/// caller's segment list as if they were direct children (CSS Display 3).
|
/// caller's segment list as if they were direct children (CSS Display 3).
|
||||||
fn flatten_contents_children(
|
fn flatten_contents_children(
|
||||||
@@ -6744,6 +6845,87 @@ mod tests {
|
|||||||
assert!((quote.height - 60.0).abs() < 0.1, "{quote:?}");
|
assert!((quote.height - 60.0).abs() < 0.1, "{quote:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn auto_width_column_flex_text_uses_fit_content_width() {
|
||||||
|
// Chromium 145: the outer row leaves 533px beside the fixed 203px
|
||||||
|
// sibling and 32px gap. The auto-width quote is fit-content, so its
|
||||||
|
// direct anonymous text item reflows to five 24px lines. Measuring it
|
||||||
|
// only at max-content instead produces a 2097px-wide, 24px-tall line.
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"<style>
|
||||||
|
* { box-sizing: border-box }
|
||||||
|
body { margin: 0; font: 16px/24px "Liberation Sans" }
|
||||||
|
#row { display: flex; gap: 32px; width: 768px }
|
||||||
|
#column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
#quote { display: flex; gap: 8px; margin: 0 }
|
||||||
|
#quote::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
#logo { width: 203px; height: 128px; flex-shrink: 0 }
|
||||||
|
</style>
|
||||||
|
<section id="row">
|
||||||
|
<div id="column"><blockquote id="quote">MDN closely follows W3C standards which helps me keep up with important topics. It's a complete package as it caters to everything; complex APIs, new browser functionalities, and best practices. MDN serves as a truly valuable resource and continues to assist me in my everyday development.</blockquote></div>
|
||||||
|
<div id="logo"></div>
|
||||||
|
</section>"#,
|
||||||
|
);
|
||||||
|
let laid = layout_dom(&tree, (1280.0, 720.0));
|
||||||
|
let rect = |id| laid.rects[&tree.get_element_by_id(id).unwrap()];
|
||||||
|
let row = rect("row");
|
||||||
|
let column = rect("column");
|
||||||
|
let quote = rect("quote");
|
||||||
|
let logo = rect("logo");
|
||||||
|
assert!((row.width - 768.0).abs() < 0.1, "{row:?}");
|
||||||
|
assert!((row.height - 128.0).abs() < 0.1, "{row:?}");
|
||||||
|
assert!((column.width - 533.0).abs() < 0.1, "{column:?}");
|
||||||
|
assert!((quote.width - 533.0).abs() < 0.1, "{quote:?}");
|
||||||
|
assert!((quote.height - 120.0).abs() < 0.1, "{quote:?}");
|
||||||
|
assert!((logo.x - 565.0).abs() < 0.1, "{logo:?}");
|
||||||
|
assert!((logo.width - 203.0).abs() < 0.1, "{logo:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn column_flex_fit_content_preserves_unbreakable_min_content() {
|
||||||
|
// Chromium 145 keeps the unbreakable quote at 2561.25px and lets the
|
||||||
|
// constrained row overflow. The synthetic fit-content cap must not
|
||||||
|
// turn an intrinsic min-content floor into overflow-wrap:anywhere.
|
||||||
|
let word = "X".repeat(240);
|
||||||
|
let tree = parse_html(&format!(
|
||||||
|
r#"<style>
|
||||||
|
* {{ box-sizing: border-box }}
|
||||||
|
body {{ margin: 0; font: 16px/24px "Liberation Sans" }}
|
||||||
|
#row {{ display: flex; gap: 32px; width: 768px }}
|
||||||
|
#column {{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}}
|
||||||
|
#quote {{ display: flex; margin: 0 }}
|
||||||
|
#logo {{ width: 203px; height: 128px; flex-shrink: 0 }}
|
||||||
|
</style>
|
||||||
|
<section id="row">
|
||||||
|
<div id="column"><blockquote id="quote">{word}</blockquote></div>
|
||||||
|
<div id="logo"></div>
|
||||||
|
</section>"#
|
||||||
|
));
|
||||||
|
let laid = layout_dom(&tree, (1280.0, 720.0));
|
||||||
|
let rect = |id| laid.rects[&tree.get_element_by_id(id).unwrap()];
|
||||||
|
let column = rect("column");
|
||||||
|
let quote = rect("quote");
|
||||||
|
let logo = rect("logo");
|
||||||
|
assert!(quote.width > 2500.0, "{quote:?}");
|
||||||
|
assert!((column.width - quote.width).abs() < 0.1, "{column:?} {quote:?}");
|
||||||
|
assert!((quote.height - 24.0).abs() < 0.1, "{quote:?}");
|
||||||
|
assert!((logo.x - quote.width - 32.0).abs() < 0.1, "{logo:?}");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_document_is_safe() {
|
fn empty_document_is_safe() {
|
||||||
// html5ever always synthesizes html/head/body, so an empty document
|
// html5ever always synthesizes html/head/body, so an empty document
|
||||||
|
|||||||
Reference in new issue
Block a user