diff --git a/packages/blitz-dom/src/layout/damage.rs b/packages/blitz-dom/src/layout/damage.rs index e44fbc9be..39e732a21 100644 --- a/packages/blitz-dom/src/layout/damage.rs +++ b/packages/blitz-dom/src/layout/damage.rs @@ -342,14 +342,41 @@ impl HoistedPaintChildren { let node = &doc.nodes[child.node_id]; let left = child.position.x + node.final_layout().location.x; let top = child.position.y + node.final_layout().location.y; - let right = left + node.final_layout().size.width; - let bottom = top + node.final_layout().size.height; + let size = node.final_layout().size; + + // A transform moves where the box paints and hit-tests (see + // Node::hit_inner), so the content area must cover the + // transformed corners, not the layout position. The transform + // operates in device pixels. + if let Some(t) = *node.transform() { + let scale = doc.viewport().scale_f64(); + let (mut min_x, mut min_y) = (f32::MAX, f32::MAX); + let (mut max_x, mut max_y) = (f32::MIN, f32::MIN); + for (dx, dy) in [ + (0.0, 0.0), + (size.width, 0.0), + (0.0, size.height), + (size.width, size.height), + ] { + let p = t * kurbo::Point::new(dx as f64 * scale, dy as f64 * scale); + min_x = min_x.min((p.x / scale) as f32); + min_y = min_y.min((p.y / scale) as f32); + max_x = max_x.max((p.x / scale) as f32); + max_y = max_y.max((p.y / scale) as f32); + } + return taffy::Rect { + left: left + min_x, + top: top + min_y, + right: left + max_x, + bottom: top + max_y, + }; + } taffy::Rect { top, left, - bottom, - right, + right: left + size.width, + bottom: top + size.height, } } diff --git a/tests/blitz-tests/tests/hoisted_transform_hit.rs b/tests/blitz-tests/tests/hoisted_transform_hit.rs new file mode 100644 index 000000000..07cd74932 --- /dev/null +++ b/tests/blitz-tests/tests/hoisted_transform_hit.rs @@ -0,0 +1,53 @@ +//! Hit testing must find a hoisted child where its transform paints it. +//! +//! A positioned child with a z-index is hoisted up to its stacking context +//! for painting, and hit testing only descends into hoisted children when +//! the point lies inside the recorded content area. A transform moves where +//! the child paints and hit-tests, so the area must cover the transformed +//! box, or a click on the child falls through to whatever it visually +//! covers - a menu positioned with a translate, for example, becomes +//! unclickable. + +use blitz_test_harness::{Harness, HarnessOptions}; + +/// A 60x40 child laid out at the stacking context's origin, painted at +/// (200, 120) by its transform. Both positions stay inside the stacking +/// context's own 300x200 box, so only the content area decides whether +/// the child is found. +/// +/// The tests pump twice: content areas are computed from the previous +/// pass's layout and transforms, so they settle one pass after the +/// document is built. +fn harness() -> Harness { + let html = "
\ +