Resolve percentage padding from final geometry
This commit is contained in:
@@ -551,6 +551,118 @@ struct GeneratedBoxBuild {
|
|||||||
node: taffy::NodeId,
|
node: taffy::NodeId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sync_resolved_percentage_padding(
|
||||||
|
taffy_tree: &TaffyTree<usize>,
|
||||||
|
taffy_root: taffy::NodeId,
|
||||||
|
initial_containing_block_width: f32,
|
||||||
|
id_map: &HashMap<taffy::NodeId, NodeId>,
|
||||||
|
generated: &[GeneratedBoxBuild],
|
||||||
|
styles: &mut HashMap<NodeId, crate::LayoutStyle>,
|
||||||
|
) {
|
||||||
|
fn sync(style: &mut crate::LayoutStyle, containing_block_width: f32) {
|
||||||
|
if let Some(percent) = style.padding_percent[0] {
|
||||||
|
style.padding.top = percent * containing_block_width;
|
||||||
|
}
|
||||||
|
if let Some(percent) = style.padding_percent[1] {
|
||||||
|
style.padding.right = percent * containing_block_width;
|
||||||
|
}
|
||||||
|
if let Some(percent) = style.padding_percent[2] {
|
||||||
|
style.padding.bottom = percent * containing_block_width;
|
||||||
|
}
|
||||||
|
if let Some(percent) = style.padding_percent[3] {
|
||||||
|
style.padding.left = percent * containing_block_width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit(
|
||||||
|
taffy_tree: &TaffyTree<usize>,
|
||||||
|
node: taffy::NodeId,
|
||||||
|
containing_block_width: f32,
|
||||||
|
id_map: &HashMap<taffy::NodeId, NodeId>,
|
||||||
|
generated: &HashMap<taffy::NodeId, (NodeId, GeneratedBoxKind)>,
|
||||||
|
styles: &mut HashMap<NodeId, crate::LayoutStyle>,
|
||||||
|
) {
|
||||||
|
let Ok(layout) = taffy_tree.layout(node) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Some(dom_id) = id_map.get(&node) {
|
||||||
|
if let Some(style) = styles.get_mut(dom_id) {
|
||||||
|
sync(style, containing_block_width);
|
||||||
|
}
|
||||||
|
} else if let Some((host, kind)) = generated.get(&node) {
|
||||||
|
if let Some(host_style) = styles.get_mut(host) {
|
||||||
|
let pseudo = match kind {
|
||||||
|
GeneratedBoxKind::Before => host_style.before_pseudo.as_deref_mut(),
|
||||||
|
GeneratedBoxKind::After => host_style.after_pseudo.as_deref_mut(),
|
||||||
|
};
|
||||||
|
if let Some(pseudo) = pseudo {
|
||||||
|
sync(pseudo, containing_block_width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let child_containing_block_width = layout.content_box_width().max(0.0);
|
||||||
|
if let Ok(children) = taffy_tree.children(node) {
|
||||||
|
for child in children {
|
||||||
|
visit(
|
||||||
|
taffy_tree,
|
||||||
|
child,
|
||||||
|
child_containing_block_width,
|
||||||
|
id_map,
|
||||||
|
generated,
|
||||||
|
styles,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let generated = generated
|
||||||
|
.iter()
|
||||||
|
.map(|generated| (generated.node, (generated.host, generated.kind)))
|
||||||
|
.collect();
|
||||||
|
visit(
|
||||||
|
taffy_tree,
|
||||||
|
taffy_root,
|
||||||
|
initial_containing_block_width,
|
||||||
|
id_map,
|
||||||
|
&generated,
|
||||||
|
styles,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sync_positioned_pseudo_percentage_padding(
|
||||||
|
rects: &HashMap<NodeId, Rect>,
|
||||||
|
styles: &mut HashMap<NodeId, crate::LayoutStyle>,
|
||||||
|
) {
|
||||||
|
fn sync(pseudo: &mut crate::LayoutStyle, containing_block_width: f32) {
|
||||||
|
if pseudo.position != Some(taffy::Position::Absolute) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (index, percent) in pseudo.padding_percent.into_iter().enumerate() {
|
||||||
|
let Some(percent) = percent else { continue };
|
||||||
|
let value = percent * containing_block_width;
|
||||||
|
match index {
|
||||||
|
0 => pseudo.padding.top = value,
|
||||||
|
1 => pseudo.padding.right = value,
|
||||||
|
2 => pseudo.padding.bottom = value,
|
||||||
|
_ => pseudo.padding.left = value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (host, style) in styles {
|
||||||
|
let Some(rect) = rects.get(host) else { continue };
|
||||||
|
let containing_block_width =
|
||||||
|
(rect.width - style.border.left - style.border.right).max(0.0);
|
||||||
|
if let Some(pseudo) = style.before_pseudo.as_deref_mut() {
|
||||||
|
sync(pseudo, containing_block_width);
|
||||||
|
}
|
||||||
|
if let Some(pseudo) = style.after_pseudo.as_deref_mut() {
|
||||||
|
sync(pseudo, containing_block_width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Registry of cosmic-text inline formatting contexts created during the
|
/// Registry of cosmic-text inline formatting contexts created during the
|
||||||
/// taffy-tree build: `whole` holds single-leaf containers keyed by the
|
/// taffy-tree build: `whole` holds single-leaf containers keyed by the
|
||||||
/// container's own DOM id, `runs` holds anonymous inline-run leaves keyed by
|
/// container's own DOM id, `runs` holds anonymous inline-run leaves keyed by
|
||||||
@@ -1834,11 +1946,11 @@ fn layout_dom_once(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Resolve font/viewport-relative box edges now that their
|
// Resolve font/viewport-relative box edges now that their
|
||||||
// reference sizes are known. Percentage padding/margin then
|
// reference sizes are known. Percentage margins still use the
|
||||||
// resolve against the containing block WIDTH on every side
|
// estimated containing-block width here. Pure percentage
|
||||||
// (per CSS: `padding-top:56.25%` in a 1000px block is 562.5px,
|
// padding stays typed until Taffy knows the final flex/grid
|
||||||
// not a fraction of the height). Bake the resolved px back into
|
// containing-block inline size; its used pixels are synced
|
||||||
// `padding`/`margin`, which then feed taffy.
|
// back into `LayoutStyle` after the final layout.
|
||||||
for i in 0..4 {
|
for i in 0..4 {
|
||||||
if let Some(expression) = style.padding_expressions[i].as_deref() {
|
if let Some(expression) = style.padding_expressions[i].as_deref() {
|
||||||
if let Some(px) = crate::style::resolve_contextual_length(
|
if let Some(px) = crate::style::resolve_contextual_length(
|
||||||
@@ -1898,15 +2010,6 @@ fn layout_dom_once(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(frac) = style.padding_percent[i] {
|
|
||||||
let px = (frac * cb_w).max(0.0);
|
|
||||||
match i {
|
|
||||||
0 => style.padding.top = px,
|
|
||||||
1 => style.padding.right = px,
|
|
||||||
2 => style.padding.bottom = px,
|
|
||||||
_ => style.padding.left = px,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(frac) = style.margin_percent[i] {
|
if let Some(frac) = style.margin_percent[i] {
|
||||||
let px = frac * cb_w;
|
let px = frac * cb_w;
|
||||||
match i {
|
match i {
|
||||||
@@ -2051,15 +2154,6 @@ fn layout_dom_once(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(percent) = pseudo.padding_percent[index] {
|
|
||||||
let px = (percent * cb_w).max(0.0);
|
|
||||||
match index {
|
|
||||||
0 => pseudo.padding.top = px,
|
|
||||||
1 => pseudo.padding.right = px,
|
|
||||||
2 => pseudo.padding.bottom = px,
|
|
||||||
_ => pseudo.padding.left = px,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(percent) = pseudo.margin_percent[index] {
|
if let Some(percent) = pseudo.margin_percent[index] {
|
||||||
let px = percent * cb_w;
|
let px = percent * cb_w;
|
||||||
match index {
|
match index {
|
||||||
@@ -3004,6 +3098,14 @@ fn layout_dom_once(
|
|||||||
let _ = taffy_tree.compute_layout(taffy_root, available);
|
let _ = taffy_tree.compute_layout(taffy_root, available);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
sync_resolved_percentage_padding(
|
||||||
|
&taffy_tree,
|
||||||
|
taffy_root,
|
||||||
|
initial_cb_width,
|
||||||
|
&id_map,
|
||||||
|
&ifc_items.generated,
|
||||||
|
&mut styles,
|
||||||
|
);
|
||||||
let generated_nodes: HashMap<taffy::NodeId, usize> = ifc_items
|
let generated_nodes: HashMap<taffy::NodeId, usize> = ifc_items
|
||||||
.generated
|
.generated
|
||||||
.iter()
|
.iter()
|
||||||
@@ -3027,6 +3129,7 @@ fn layout_dom_once(
|
|||||||
synthesize_row_rects(tree, &mut rects);
|
synthesize_row_rects(tree, &mut rects);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
sync_positioned_pseudo_percentage_padding(&rects, &mut styles);
|
||||||
|
|
||||||
let mut clip_rects = HashMap::new();
|
let mut clip_rects = HashMap::new();
|
||||||
let mut translates = HashMap::new();
|
let mut translates = HashMap::new();
|
||||||
@@ -5179,6 +5282,7 @@ fn pseudo_requires_generated_box(style: &crate::LayoutStyle, content: Option<&st
|
|||||||
|| style.margin != crate::Edges::default()
|
|| style.margin != crate::Edges::default()
|
||||||
|| style.margin_auto.iter().any(|value| *value)
|
|| style.margin_auto.iter().any(|value| *value)
|
||||||
|| style.padding != crate::Edges::default()
|
|| style.padding != crate::Edges::default()
|
||||||
|
|| style.padding_percent.iter().any(|value| value.is_some())
|
||||||
|| style.border != crate::Edges::default()
|
|| style.border != crate::Edges::default()
|
||||||
|| style.background_color.is_some()
|
|| style.background_color.is_some()
|
||||||
|| style.background_gradient.is_some()
|
|| style.background_gradient.is_some()
|
||||||
@@ -6859,6 +6963,10 @@ fn inline_wrapper_float(
|
|||||||
if wrapper_style.display != crate::Display::Inline
|
if wrapper_style.display != crate::Display::Inline
|
||||||
|| wrapper_style.margin != crate::Edges::default()
|
|| wrapper_style.margin != crate::Edges::default()
|
||||||
|| wrapper_style.padding != crate::Edges::default()
|
|| wrapper_style.padding != crate::Edges::default()
|
||||||
|
|| wrapper_style
|
||||||
|
.padding_percent
|
||||||
|
.iter()
|
||||||
|
.any(|value| value.is_some())
|
||||||
|| wrapper_style.border != crate::Edges::default()
|
|| wrapper_style.border != crate::Edges::default()
|
||||||
|| wrapper_style.before_pseudo.is_some()
|
|| wrapper_style.before_pseudo.is_some()
|
||||||
|| wrapper_style.after_pseudo.is_some()
|
|| wrapper_style.after_pseudo.is_some()
|
||||||
@@ -7143,6 +7251,8 @@ fn needs_column_flex_text_fit_content_cap(
|
|||||||
|| style.size_expressions[4].is_some()
|
|| style.size_expressions[4].is_some()
|
||||||
|| style.padding.left != 0.0
|
|| style.padding.left != 0.0
|
||||||
|| style.padding.right != 0.0
|
|| style.padding.right != 0.0
|
||||||
|
|| style.padding_percent[1].is_some()
|
||||||
|
|| style.padding_percent[3].is_some()
|
||||||
|| style.border.left != 0.0
|
|| style.border.left != 0.0
|
||||||
|| style.border.right != 0.0
|
|| style.border.right != 0.0
|
||||||
|| style.margin.left != 0.0
|
|| style.margin.left != 0.0
|
||||||
@@ -7927,6 +8037,10 @@ fn build_children_with_float_zone(
|
|||||||
&& matches!(style.max_height, crate::Dimension::Auto)
|
&& matches!(style.max_height, crate::Dimension::Auto)
|
||||||
&& style.margin == crate::Edges::default()
|
&& style.margin == crate::Edges::default()
|
||||||
&& style.padding == crate::Edges::default()
|
&& style.padding == crate::Edges::default()
|
||||||
|
&& style
|
||||||
|
.padding_percent
|
||||||
|
.iter()
|
||||||
|
.all(|value| value.is_none())
|
||||||
&& style.border == crate::Edges::default()
|
&& style.border == crate::Edges::default()
|
||||||
&& style.before_pseudo.is_none()
|
&& style.before_pseudo.is_none()
|
||||||
&& style.after_pseudo.is_none()
|
&& style.after_pseudo.is_none()
|
||||||
|
|||||||
@@ -341,9 +341,10 @@ pub struct LayoutStyle {
|
|||||||
/// fraction, `None` when the side is a fixed length. All four sides resolve
|
/// fraction, `None` when the side is a fixed length. All four sides resolve
|
||||||
/// against the containing block's WIDTH (per CSS, including top/bottom): this
|
/// against the containing block's WIDTH (per CSS, including top/bottom): this
|
||||||
/// is the responsive aspect-ratio-box trick (`padding-top:56.25%` reserves a
|
/// is the responsive aspect-ratio-box trick (`padding-top:56.25%` reserves a
|
||||||
/// 16:9 area). The f32 `padding` cannot carry a percentage, so it is
|
/// 16:9 area). The percentage remains typed through Taffy layout so flex/grid
|
||||||
/// resolved to px in `dom::layout_dom`'s top-down pass and written back into
|
/// min/max sizing can establish the final containing-block width first.
|
||||||
/// `padding`, which then feeds taffy as a fixed length.
|
/// `dom::layout_dom` writes Taffy's resolved used pixels back into `padding`
|
||||||
|
/// before paint and geometry consumers inspect the computed layout.
|
||||||
pub padding_percent: [Option<f32>; 4],
|
pub padding_percent: [Option<f32>; 4],
|
||||||
/// Font- and viewport-relative padding lengths (top, right, bottom, left),
|
/// Font- and viewport-relative padding lengths (top, right, bottom, left),
|
||||||
/// resolved alongside `margin_relative` during the top-down pass.
|
/// resolved alongside `margin_relative` during the top-down pass.
|
||||||
@@ -1170,7 +1171,7 @@ pub(crate) fn to_taffy_style(style: &LayoutStyle) -> Style {
|
|||||||
}
|
}
|
||||||
|
|
||||||
s.margin = rect_auto(style.margin, style.margin_auto);
|
s.margin = rect_auto(style.margin, style.margin_auto);
|
||||||
s.padding = rect_lp(style.padding);
|
s.padding = rect_lp_percent(style.padding, style.padding_percent);
|
||||||
s.border = rect_lp(style.border);
|
s.border = rect_lp(style.border);
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
@@ -1208,6 +1209,22 @@ fn rect_lp(e: Edges) -> taffy::Rect<taffy::style::LengthPercentage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn rect_lp_percent(
|
||||||
|
e: Edges,
|
||||||
|
percent: [Option<f32>; 4],
|
||||||
|
) -> taffy::Rect<taffy::style::LengthPercentage> {
|
||||||
|
let side = |value, percent| match percent {
|
||||||
|
Some(percent) => taffy::style::LengthPercentage::percent(percent),
|
||||||
|
None => taffy::style::LengthPercentage::length(value),
|
||||||
|
};
|
||||||
|
taffy::Rect {
|
||||||
|
top: side(e.top, percent[0]),
|
||||||
|
right: side(e.right, percent[1]),
|
||||||
|
bottom: side(e.bottom, percent[2]),
|
||||||
|
left: side(e.left, percent[3]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn rect_auto(e: Edges, auto: [bool; 4]) -> taffy::Rect<taffy::style::LengthPercentageAuto> {
|
fn rect_auto(e: Edges, auto: [bool; 4]) -> taffy::Rect<taffy::style::LengthPercentageAuto> {
|
||||||
let side = |value, is_auto| {
|
let side = |value, is_auto| {
|
||||||
if is_auto {
|
if is_auto {
|
||||||
|
|||||||
@@ -162,6 +162,117 @@ fn percentage_padding_top_reserves_aspect_ratio_box() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn percentage_padding_uses_final_flex_containing_block_width() {
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"
|
||||||
|
<body style="margin:0">
|
||||||
|
<div style="display:flex;width:690px">
|
||||||
|
<div id="first" style="display:flex;flex-direction:column;width:100%;max-width:500px">
|
||||||
|
<div id="first-sizer" style="width:100%;padding-top:55%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;width:690px">
|
||||||
|
<div id="second" style="display:flex;flex-direction:column;width:100%;max-width:450px">
|
||||||
|
<div id="second-sizer" style="width:100%;padding-top:106.7%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
let layout = layout_dom(&tree, (1000.0, 1000.0));
|
||||||
|
let rect = |id| layout.rects[&tree.get_element_by_id(id).unwrap()];
|
||||||
|
let first = rect("first");
|
||||||
|
let first_sizer = rect("first-sizer");
|
||||||
|
let second = rect("second");
|
||||||
|
let second_sizer = rect("second-sizer");
|
||||||
|
|
||||||
|
assert!((first.width - 500.0).abs() < 0.01, "first flex item: {first:?}");
|
||||||
|
assert!(
|
||||||
|
(first_sizer.height - 275.0).abs() < 0.01,
|
||||||
|
"55% padding must use the final 500px containing block: {first_sizer:?}"
|
||||||
|
);
|
||||||
|
assert!((second.width - 450.0).abs() < 0.01, "second flex item: {second:?}");
|
||||||
|
assert!(
|
||||||
|
(second_sizer.height - 480.15).abs() < 0.51,
|
||||||
|
"106.7% padding must use the final 450px containing block: {second_sizer:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
for (id, expected) in [("first-sizer", 275.0), ("second-sizer", 480.15)] {
|
||||||
|
let node = tree.get_element_by_id(id).unwrap();
|
||||||
|
assert!(
|
||||||
|
(layout.styles[&node].padding.top - expected).abs() < 0.05,
|
||||||
|
"paint/content consumers must see final used padding for {id}: {:?}",
|
||||||
|
layout.styles[&node].padding
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mixed_percentage_padding_shorthand_preserves_fixed_sides_and_content_origin() {
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"
|
||||||
|
<body style="margin:0">
|
||||||
|
<div style="width:500px">
|
||||||
|
<div id="mixed" style="box-sizing:border-box;width:100%;padding:10% 20px 5% 7px">
|
||||||
|
<div id="ink" style="height:10px"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
let layout = layout_dom(&tree, (1000.0, 1000.0));
|
||||||
|
let mixed_id = tree.get_element_by_id("mixed").unwrap();
|
||||||
|
let mixed = layout.rects[&mixed_id];
|
||||||
|
let ink = layout.rects[&tree.get_element_by_id("ink").unwrap()];
|
||||||
|
let padding = layout.styles[&mixed_id].padding;
|
||||||
|
|
||||||
|
assert!((mixed.width - 500.0).abs() < 0.01, "border-box width: {mixed:?}");
|
||||||
|
assert!((mixed.height - 85.0).abs() < 0.01, "mixed padding height: {mixed:?}");
|
||||||
|
assert!(
|
||||||
|
(padding.top - 50.0).abs() < 0.01
|
||||||
|
&& (padding.right - 20.0).abs() < 0.01
|
||||||
|
&& (padding.bottom - 25.0).abs() < 0.01
|
||||||
|
&& (padding.left - 7.0).abs() < 0.01,
|
||||||
|
"resolved mixed shorthand: {padding:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
(ink.x - mixed.x - 7.0).abs() < 0.01 && (ink.y - mixed.y - 50.0).abs() < 0.01,
|
||||||
|
"content origin must use the resolved padding: mixed={mixed:?} ink={ink:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn positioned_pseudo_percentage_padding_uses_final_host_width() {
|
||||||
|
let tree = parse_html(
|
||||||
|
r#"
|
||||||
|
<style>
|
||||||
|
#host { position:relative; width:400px; height:100px; border:10px solid }
|
||||||
|
#host::before {
|
||||||
|
content:"x";
|
||||||
|
position:absolute;
|
||||||
|
width:100px;
|
||||||
|
height:50px;
|
||||||
|
padding:10% 5%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div id="host"></div>
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
let layout = layout_dom(&tree, (1000.0, 1000.0));
|
||||||
|
let host = tree.get_element_by_id("host").unwrap();
|
||||||
|
let pseudo = layout.styles[&host].before_pseudo.as_ref().unwrap();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
(pseudo.padding.top - 40.0).abs() < 0.01
|
||||||
|
&& (pseudo.padding.right - 20.0).abs() < 0.01
|
||||||
|
&& (pseudo.padding.bottom - 40.0).abs() < 0.01
|
||||||
|
&& (pseudo.padding.left - 20.0).abs() < 0.01,
|
||||||
|
"positioned pseudo padding must use its final containing block: {:?}",
|
||||||
|
pseudo.padding
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn inset_absolute_uses_nearest_positioned_ancestor() {
|
fn inset_absolute_uses_nearest_positioned_ancestor() {
|
||||||
let html = r##"
|
let html = r##"
|
||||||
|
|||||||
Reference in New Issue
Block a user