render: honor transform containing blocks
Track every non-none transform as a containing-block trigger, including unsupported visual transforms. Keep separate nearest-ancestor maps so positioned boxes capture absolute descendants while only transforms capture fixed descendants.
This commit is contained in:
@@ -1156,13 +1156,13 @@ fn compute_absolute_rects(
|
||||
/// their immediate DOM parent.
|
||||
///
|
||||
/// Taffy resolves an absolute child's insets against its direct layout-tree
|
||||
/// parent. CSS instead uses the nearest non-static ancestor, or the initial
|
||||
/// containing block for `position:fixed`. Gecko represents that distinction
|
||||
/// by reparenting the absolute frame and leaving a placeholder in normal
|
||||
/// flow. We can safely do the same without a placeholder when each axis has
|
||||
/// at least one specified inset: no static-position coordinate is needed.
|
||||
/// Boxes with an entirely auto axis stay under their DOM parent until the
|
||||
/// placeholder/static-position path is implemented.
|
||||
/// parent. CSS instead uses the nearest positioned or transformed ancestor;
|
||||
/// fixed boxes use the nearest transformed ancestor or the initial containing
|
||||
/// block. Gecko represents that distinction by reparenting the positioned
|
||||
/// frame and leaving a placeholder in normal flow. We can safely do the same
|
||||
/// without a placeholder when each axis has at least one specified inset: no
|
||||
/// static-position coordinate is needed. Boxes with an entirely auto axis
|
||||
/// stay under their DOM parent until the placeholder path is implemented.
|
||||
fn reparent_inset_positioned_nodes(
|
||||
tree: &DomTree,
|
||||
taffy_tree: &mut TaffyTree<usize>,
|
||||
@@ -1172,24 +1172,36 @@ fn reparent_inset_positioned_nodes(
|
||||
) {
|
||||
let reverse: HashMap<NodeId, taffy::NodeId> =
|
||||
id_map.iter().map(|(&taffy_id, &dom_id)| (dom_id, taffy_id)).collect();
|
||||
let mut nearest_cb_for_children: HashMap<NodeId, taffy::NodeId> = HashMap::new();
|
||||
let mut nearest_abs_cb_for_children: HashMap<NodeId, taffy::NodeId> = HashMap::new();
|
||||
let mut nearest_fixed_cb_for_children: HashMap<NodeId, taffy::NodeId> = HashMap::new();
|
||||
|
||||
for dom_id in tree.descendants(tree.document()) {
|
||||
let Some(style) = styles.get(&dom_id) else { continue };
|
||||
let parent = tree.get_node(dom_id).and_then(|node| node.parent);
|
||||
let inherited_cb = parent
|
||||
.and_then(|id| nearest_cb_for_children.get(&id).copied())
|
||||
let inherited_abs_cb = parent
|
||||
.and_then(|id| nearest_abs_cb_for_children.get(&id).copied())
|
||||
.unwrap_or(taffy_root);
|
||||
let inherited_fixed_cb = parent
|
||||
.and_then(|id| nearest_fixed_cb_for_children.get(&id).copied())
|
||||
.unwrap_or(taffy_root);
|
||||
|
||||
// Record this before any candidate early-exit so all descendants get
|
||||
// an O(1) nearest-containing-block lookup. The full walk stays O(n)
|
||||
// even on deeply nested or absolute-heavy documents.
|
||||
let child_cb = if style.position.is_some() {
|
||||
reverse.get(&dom_id).copied().unwrap_or(inherited_cb)
|
||||
// O(1) nearest-containing-block lookups. Positioned and transformed
|
||||
// boxes capture absolute descendants; only transformed boxes capture
|
||||
// fixed descendants. The full walk stays O(n).
|
||||
let own_box = reverse.get(&dom_id).copied();
|
||||
let abs_child_cb = if style.position.is_some() || style.transform_establishes_containing_block {
|
||||
own_box.unwrap_or(inherited_abs_cb)
|
||||
} else {
|
||||
inherited_cb
|
||||
inherited_abs_cb
|
||||
};
|
||||
nearest_cb_for_children.insert(dom_id, child_cb);
|
||||
let fixed_child_cb = if style.transform_establishes_containing_block {
|
||||
own_box.unwrap_or(inherited_fixed_cb)
|
||||
} else {
|
||||
inherited_fixed_cb
|
||||
};
|
||||
nearest_abs_cb_for_children.insert(dom_id, abs_child_cb);
|
||||
nearest_fixed_cb_for_children.insert(dom_id, fixed_child_cb);
|
||||
|
||||
if !matches!(style.position, Some(taffy::Position::Absolute)) {
|
||||
continue;
|
||||
@@ -1201,9 +1213,9 @@ fn reparent_inset_positioned_nodes(
|
||||
}
|
||||
let Some(&child) = reverse.get(&dom_id) else { continue };
|
||||
let target = if style.position_fixed {
|
||||
taffy_root
|
||||
inherited_fixed_cb
|
||||
} else {
|
||||
inherited_cb
|
||||
inherited_abs_cb
|
||||
};
|
||||
let Some(current) = taffy_tree.parent(child) else { continue };
|
||||
if current == target {
|
||||
|
||||
@@ -415,6 +415,10 @@ pub struct LayoutStyle {
|
||||
/// off-screen skip-link out of view instead of painting it on-screen. Not
|
||||
/// inherited (transform is a non-inherited property).
|
||||
pub transform_translate: Option<(Dimension, Dimension)>,
|
||||
/// Any computed transform other than `none` establishes containing blocks
|
||||
/// for absolute and fixed descendants, even when its visual transform
|
||||
/// function is not yet supported by the paint engine.
|
||||
pub transform_establishes_containing_block: bool,
|
||||
/// `transform: scale(sx[, sy])` / `scaleX` / `scaleY`, captured as (sx, sy).
|
||||
/// Parsed and stored so a value that mixes scale with translate still yields
|
||||
/// its translate part; scale is not yet folded into paint geometry (doing so
|
||||
|
||||
@@ -623,12 +623,18 @@ fn parse_transform(style: &mut LayoutStyle, value: &str) {
|
||||
if v.eq_ignore_ascii_case("none") {
|
||||
style.transform_translate = None;
|
||||
style.transform_scale = None;
|
||||
style.transform_establishes_containing_block = false;
|
||||
return;
|
||||
}
|
||||
let functions = transform_functions(v);
|
||||
if functions.is_empty() {
|
||||
return;
|
||||
}
|
||||
style.transform_establishes_containing_block = true;
|
||||
let zero = crate::Dimension::Px(0.0);
|
||||
let (mut tx, mut ty): (Option<crate::Dimension>, Option<crate::Dimension>) = (None, None);
|
||||
let (mut sx, mut sy): (Option<f32>, Option<f32>) = (None, None);
|
||||
for (func, args) in transform_functions(v) {
|
||||
for (func, args) in functions {
|
||||
let parts: Vec<&str> = args.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()).collect();
|
||||
match func.to_ascii_lowercase().as_str() {
|
||||
// translate3d's z component is ignored (no perspective model);
|
||||
|
||||
@@ -161,6 +161,48 @@ fn inset_absolute_uses_nearest_positioned_ancestor() {
|
||||
assert!((fixed.y - 40.0).abs() < 1.0, "fixed box did not use viewport y: {fixed:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transforms_establish_absolute_and_fixed_containing_blocks() {
|
||||
let html = r##"
|
||||
<body style="margin:0">
|
||||
<div id="outer" style="position:relative;margin:40px 0 0 50px;width:600px;height:400px;border:10px solid black;padding:20px;box-sizing:border-box">
|
||||
<div id="transformer" style="transform:translate(30px,20px);margin:40px 0 0 70px;width:300px;height:200px;border:5px solid black;padding:10px;box-sizing:border-box">
|
||||
<div style="position:static;margin:20px;width:80px;height:50px">
|
||||
<span id="abs" style="position:absolute;left:20px;top:25px;width:70px;height:55px"></span>
|
||||
<div id="fixed-transformed" style="position:fixed;left:150px;top:100px;width:80px;height:50px"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="identity-transform" style="transform:rotate(0deg);margin:-80px 0 0 400px;width:100px;height:80px">
|
||||
<div id="identity-abs" style="position:absolute;right:10px;bottom:10px;width:20px;height:20px"></div>
|
||||
</div>
|
||||
<div id="fixed-viewport" style="position:fixed;left:700px;top:40px;width:90px;height:60px"></div>
|
||||
</div>
|
||||
</body>
|
||||
"##;
|
||||
let tree = parse_html(html);
|
||||
let layout = layout_dom(&tree, (900.0, 700.0));
|
||||
let transformer_id = tree.get_element_by_id("transformer").unwrap();
|
||||
let transformer = layout.rects[&transformer_id];
|
||||
let abs = layout.rects[&tree.get_element_by_id("abs").unwrap()];
|
||||
let fixed_transformed = layout.rects[&tree.get_element_by_id("fixed-transformed").unwrap()];
|
||||
let identity_transform = layout.rects[&tree.get_element_by_id("identity-transform").unwrap()];
|
||||
let identity_abs = layout.rects[&tree.get_element_by_id("identity-abs").unwrap()];
|
||||
let fixed_viewport = layout.rects[&tree.get_element_by_id("fixed-viewport").unwrap()];
|
||||
|
||||
// Layout rects precede paint transforms. Insets use the transformer's
|
||||
// padding box; the shared translate is recorded separately for the whole
|
||||
// DOM subtree.
|
||||
assert!((abs.x - transformer.x - 25.0).abs() < 1.0, "wrong transformed abs x: transformer={transformer:?} abs={abs:?}");
|
||||
assert!((abs.y - transformer.y - 30.0).abs() < 1.0, "wrong transformed abs y: transformer={transformer:?} abs={abs:?}");
|
||||
assert!((fixed_transformed.x - transformer.x - 155.0).abs() < 1.0, "wrong transformed fixed x: {fixed_transformed:?}");
|
||||
assert!((fixed_transformed.y - transformer.y - 105.0).abs() < 1.0, "wrong transformed fixed y: {fixed_transformed:?}");
|
||||
assert_eq!(layout.translates[&transformer_id], (30.0, 20.0));
|
||||
assert!((identity_abs.x - identity_transform.x - 70.0).abs() < 1.0, "unsupported transform did not establish abs x: {identity_abs:?}");
|
||||
assert!((identity_abs.y - identity_transform.y - 50.0).abs() < 1.0, "unsupported transform did not establish abs y: {identity_abs:?}");
|
||||
assert!((fixed_viewport.x - 700.0).abs() < 1.0, "positioned ancestor captured fixed x: {fixed_viewport:?}");
|
||||
assert!((fixed_viewport.y - 40.0).abs() < 1.0, "positioned ancestor captured fixed y: {fixed_viewport:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_text_run_wraps_across_multiple_lines() {
|
||||
// A long single text node with no inline elements breaking it up must
|
||||
|
||||
Reference in New Issue
Block a user