From fa6ffcdbf31cc4278eaa1464333b0cc512082de8 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Sun, 11 Oct 2026 21:19:43 +0800 Subject: [PATCH] fix(layout): apply 2D transforms written with 3D functions --- moli-layout/src/style.rs | 12 +- .../runtime/page_vm/tests/rendering_update.rs | 1 + .../rendering_update/planar_transforms.rs | 135 ++++++++++++++++++ 3 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 moli-renderer-v8/src/runtime/page_vm/tests/rendering_update/planar_transforms.rs diff --git a/moli-layout/src/style.rs b/moli-layout/src/style.rs index 22e58a4d67..f27d3d09e9 100644 --- a/moli-layout/src/style.rs +++ b/moli-layout/src/style.rs @@ -2385,14 +2385,14 @@ fn resolve_stylo_2d_transform( // precision even if they are widened again for LayoutTransform2D. .to_transform_3d_matrix_f64(Some(&reference_box)) { - Ok((_matrix, true)) => { - has_unsupported_3d = true; - None - } - Ok((matrix, false)) => Some(LayoutTransform2D::new([ + // Stylo's boolean reports 3D function syntax, not whether the + // resulting matrix needs 3D geometry. In particular, popovers + // commonly center with translate3d(-50%, 0, 0). Keep every matrix + // that is exactly representable in our 2D coordinate space. + Ok((matrix, _)) if matrix.is_2d() => Some(LayoutTransform2D::new([ matrix.m11, matrix.m12, matrix.m21, matrix.m22, matrix.m41, matrix.m42, ])), - Err(_) => { + Ok(_) | Err(_) => { has_unsupported_3d = true; None } diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update.rs b/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update.rs index 229142871c..eda088b39c 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update.rs @@ -1,5 +1,6 @@ use super::*; +mod planar_transforms; mod print; mod transform_precision; diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update/planar_transforms.rs b/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update/planar_transforms.rs new file mode 100644 index 0000000000..afe052ece0 --- /dev/null +++ b/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update/planar_transforms.rs @@ -0,0 +1,135 @@ +use super::*; + +#[tokio::test(flavor = "current_thread")] +async fn planar_3d_functions_match_chromium_popover_geometry_paint_and_hit_testing() { + run_page_vm_async_test(async move { + let loader = + crate::network::ResourceRequestClient::new(&FetchConfig::default()).expect("loader"); + let mut page_vm = test_page_vm_with_loader_and_document_url( + &loader, + Vec::new(), + Url::parse("https://example.com/planar-transforms.html")?, + ); + page_vm.vm_mut().eval( + r#" +document.head.innerHTML = ``; +document.body.innerHTML = `
+ +
`; +'installed' +"#, + )?; + page_vm.vm_mut().sync_live_document_style_sources(); + let viewport = moli_layout::PaintViewport::new(600, 400, 1.0); + let snapshot = page_vm + .vm_mut() + .screenshot_layout_snapshot(viewport)? + .expect("planar transform fixture must retain a layout root"); + assert!( + snapshot + .diagnostics + .iter() + .all(|diagnostic| diagnostic.code != "transform-3d-geometry-fallback"), + "3D function syntax with a 2D matrix must not trigger a fallback" + ); + + let result = page_vm.vm_mut().eval( + r#"JSON.stringify((()=>{ +const ids=['bottom','bottom-popover','content','right','right-popover','composed','matrix','zero-z']; +const rect=id=>{const r=document.getElementById(id).getBoundingClientRect();return [r.x,r.y,r.width,r.height]}; +return {geometry:Object.fromEntries(ids.map(id=>[id,rect(id)])), +hits:[[50,75],[60,90],[80,210],[240,220],[370,240],[420,320],[410,80]].map(([x,y])=>document.elementFromPoint(x,y)?.id||null)}; +})())"#, + )?; + let result: serde_json::Value = serde_json::from_str(&result)?; + // Recorded in Chromium. Odd border-box sizes also pin the percentage + // reference box and preserve the half-pixel centering translation. + for (id, expected) in [ + ("bottom", [200.0, 20.0, 50.0, 50.0]), + ("bottom-popover", [44.5, 70.0, 361.0, 80.0]), + ("content", [55.5, 81.0, 20.0, 20.0]), + ("right", [20.0, 220.0, 50.0, 50.0]), + ("right-popover", [70.0, 204.5, 120.0, 81.0]), + ("composed", [230.0, 200.0, 40.0, 160.0]), + ("matrix", [365.0, 235.0, 80.0, 40.0]), + ("zero-z", [415.0, 315.0, 40.0, 20.0]), + ] { + for (index, expected) in expected.into_iter().enumerate() { + let actual = result["geometry"][id][index] + .as_f64() + .expect("numeric geometry"); + assert!( + (actual - expected).abs() <= 0.001, + "{id}[{index}]: expected {expected}, got {actual}" + ); + } + } + assert_eq!( + result["hits"], + serde_json::json!([ + "bottom-popover", "content", "right-popover", "composed", "matrix", "zero-z", null + ]), + ); + + let image = moli_paint::raster_snapshot(&snapshot)?; + let pixel = |x: u32, y: u32| { + let index = ((y * image.width + x) * 4) as usize; + <[u8; 4]>::try_from(&image.rgba[index..index + 4]).expect("RGBA pixel") + }; + for (point, expected) in [ + ((50, 75), [255, 0, 0, 255]), + ((60, 90), [0, 255, 0, 255]), + ((80, 210), [255, 0, 0, 255]), + ((240, 220), [128, 0, 128, 255]), + ((370, 240), [255, 165, 0, 255]), + ((420, 320), [0, 128, 128, 255]), + ((410, 80), [255, 255, 255, 255]), + ] { + assert_eq!(pixel(point.0, point.1), expected, "pixel at {point:?}"); + } + + // A nonzero z translation still cannot be represented in 2D. Retain + // the existing diagnostic and conservative geometry for that case. + page_vm.vm_mut().eval( + r#" +const depth = document.createElement('div'); +depth.id = 'depth'; +depth.style.cssText = 'position:absolute;left:500px;top:20px;width:40px;height:20px;transform:translate3d(-10px,0,5px)'; +document.body.appendChild(depth); +'installed' +"#, + )?; + let snapshot = page_vm + .vm_mut() + .screenshot_layout_snapshot(viewport)? + .expect("depth fixture must retain a layout root"); + assert_eq!( + snapshot + .diagnostics + .iter() + .filter(|diagnostic| diagnostic.code == "transform-3d-geometry-fallback") + .count(), + 1, + ); + assert_eq!( + page_vm.vm_mut().eval( + "(()=>{const r=document.getElementById('depth').getBoundingClientRect();return [r.x,r.y,r.width,r.height].join('|')})()", + )?, + "500|20|40|20", + ); + Ok::<_, anyhow::Error>(()) + }) + .await + .expect("planar transform fixture should run"); +}