diff --git a/crates/obscura-render/src/dom.rs b/crates/obscura-render/src/dom.rs index e8bad25..6576b13 100644 --- a/crates/obscura-render/src/dom.rs +++ b/crates/obscura-render/src/dom.rs @@ -551,6 +551,118 @@ struct GeneratedBoxBuild { node: taffy::NodeId, } +fn sync_resolved_percentage_padding( + taffy_tree: &TaffyTree, + taffy_root: taffy::NodeId, + initial_containing_block_width: f32, + id_map: &HashMap, + generated: &[GeneratedBoxBuild], + styles: &mut HashMap, +) { + 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, + node: taffy::NodeId, + containing_block_width: f32, + id_map: &HashMap, + generated: &HashMap, + styles: &mut HashMap, + ) { + 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, + styles: &mut HashMap, +) { + 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 /// taffy-tree build: `whole` holds single-leaf containers keyed by the /// 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 - // reference sizes are known. Percentage padding/margin then - // resolve against the containing block WIDTH on every side - // (per CSS: `padding-top:56.25%` in a 1000px block is 562.5px, - // not a fraction of the height). Bake the resolved px back into - // `padding`/`margin`, which then feed taffy. + // reference sizes are known. Percentage margins still use the + // estimated containing-block width here. Pure percentage + // padding stays typed until Taffy knows the final flex/grid + // containing-block inline size; its used pixels are synced + // back into `LayoutStyle` after the final layout. for i in 0..4 { if let Some(expression) = style.padding_expressions[i].as_deref() { 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] { let px = frac * cb_w; 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] { let px = percent * cb_w; match index { @@ -3004,6 +3098,14 @@ fn layout_dom_once( 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 = ifc_items .generated .iter() @@ -3027,6 +3129,7 @@ fn layout_dom_once( synthesize_row_rects(tree, &mut rects); } } + sync_positioned_pseudo_percentage_padding(&rects, &mut styles); let mut clip_rects = 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_auto.iter().any(|value| *value) || style.padding != crate::Edges::default() + || style.padding_percent.iter().any(|value| value.is_some()) || style.border != crate::Edges::default() || style.background_color.is_some() || style.background_gradient.is_some() @@ -6859,6 +6963,10 @@ fn inline_wrapper_float( if wrapper_style.display != crate::Display::Inline || wrapper_style.margin != 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.before_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.padding.left != 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.right != 0.0 || style.margin.left != 0.0 @@ -7927,6 +8037,10 @@ fn build_children_with_float_zone( && matches!(style.max_height, crate::Dimension::Auto) && style.margin == crate::Edges::default() && style.padding == crate::Edges::default() + && style + .padding_percent + .iter() + .all(|value| value.is_none()) && style.border == crate::Edges::default() && style.before_pseudo.is_none() && style.after_pseudo.is_none() diff --git a/crates/obscura-render/src/lib.rs b/crates/obscura-render/src/lib.rs index 809a1cf..e7fc16c 100644 --- a/crates/obscura-render/src/lib.rs +++ b/crates/obscura-render/src/lib.rs @@ -341,9 +341,10 @@ pub struct LayoutStyle { /// 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 /// 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 - /// resolved to px in `dom::layout_dom`'s top-down pass and written back into - /// `padding`, which then feeds taffy as a fixed length. + /// 16:9 area). The percentage remains typed through Taffy layout so flex/grid + /// min/max sizing can establish the final containing-block width first. + /// `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; 4], /// Font- and viewport-relative padding lengths (top, right, bottom, left), /// 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.padding = rect_lp(style.padding); + s.padding = rect_lp_percent(style.padding, style.padding_percent); s.border = rect_lp(style.border); s } @@ -1208,6 +1209,22 @@ fn rect_lp(e: Edges) -> taffy::Rect { } } +fn rect_lp_percent( + e: Edges, + percent: [Option; 4], +) -> taffy::Rect { + 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 { let side = |value, is_auto| { if is_auto { diff --git a/crates/obscura-render/tests/layout_test.rs b/crates/obscura-render/tests/layout_test.rs index 473718f..bcc3047 100644 --- a/crates/obscura-render/tests/layout_test.rs +++ b/crates/obscura-render/tests/layout_test.rs @@ -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#" + +
+
+
+
+
+
+
+
+
+
+ + "#, + ); + 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#" + +
+
+
+
+
+ + "#, + ); + 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#" + +
+ "#, + ); + 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] fn inset_absolute_uses_nearest_positioned_ancestor() { let html = r##"