diff --git a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js index 909f494d5d..7deef78cd2 100644 --- a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js +++ b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js @@ -626,3 +626,92 @@ test("Renders background blur clipped by a board with clip content", async ({ await expect(workspace.canvas).toHaveScreenshot(); }); + +test("Clips a group dragged into a board with clip content", async ({ + page, +}) => { + const workspace = new WasmWorkspacePage(page); + await workspace.setupEmptyFile(); + await workspace.mockGetFile("render-wasm/get-file-shapes-groups-boards.json"); + + await workspace.goToWorkspace({ + id: "53a7ff09-2228-81d3-8006-4b5eac177245", + pageId: "53a7ff09-2228-81d3-8006-4b5eac177246", + }); + await workspace.waitForFirstRenderWithoutUI(); + + // Select the group, then drag it so it straddles the right edge of the + // board. The overflow must not be painted while the pointer is still down. + await workspace.viewport.hover({ position: { x: 1028, y: 548 } }); + await page.mouse.down(); + await page.mouse.up(); + await page.waitForTimeout(200); + + await page.mouse.down(); + for (const [x, y] of [ + [1000, 540], + [920, 530], + [830, 520], + ]) { + await workspace.viewport.hover({ position: { x, y } }); + await page.waitForTimeout(100); + } + await page.waitForTimeout(600); + + await expect(workspace.canvas).toHaveScreenshot(); + + await page.mouse.up(); + await page.waitForTimeout(800); + + await expect(workspace.canvas).toHaveScreenshot(); +}); + +test("Clips a group dragged inside a board with clip content", async ({ + page, +}) => { + const workspace = new WasmWorkspacePage(page); + await workspace.setupEmptyFile(); + await workspace.mockGetFile("render-wasm/get-file-shapes-groups-boards.json"); + + await workspace.goToWorkspace({ + id: "53a7ff09-2228-81d3-8006-4b5eac177245", + pageId: "53a7ff09-2228-81d3-8006-4b5eac177246", + }); + await workspace.waitForFirstRenderWithoutUI(); + + await workspace.viewport.hover({ position: { x: 1028, y: 548 } }); + await page.mouse.down(); + await page.mouse.up(); + await page.waitForTimeout(200); + + // Drop the group inside the board so it becomes one of its children. + await page.mouse.down(); + for (const [x, y] of [ + [900, 540], + [700, 520], + ]) { + await workspace.viewport.hover({ position: { x, y } }); + await page.waitForTimeout(100); + } + await page.waitForTimeout(600); + await page.mouse.up(); + await page.waitForTimeout(800); + + // Drag it towards the right edge, now as a board child. + await page.mouse.down(); + for (const [x, y] of [ + [750, 520], + [830, 520], + ]) { + await workspace.viewport.hover({ position: { x, y } }); + await page.waitForTimeout(100); + } + await page.waitForTimeout(600); + + await expect(workspace.canvas).toHaveScreenshot(); + + await page.mouse.up(); + await page.waitForTimeout(800); + + await expect(workspace.canvas).toHaveScreenshot(); +}); diff --git a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Clips-a-group-dragged-inside-a-board-with-clip-content-1.png b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Clips-a-group-dragged-inside-a-board-with-clip-content-1.png new file mode 100644 index 0000000000..45d489646c Binary files /dev/null and b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Clips-a-group-dragged-inside-a-board-with-clip-content-1.png differ diff --git a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Clips-a-group-dragged-inside-a-board-with-clip-content-2.png b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Clips-a-group-dragged-inside-a-board-with-clip-content-2.png new file mode 100644 index 0000000000..e25d838426 Binary files /dev/null and b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Clips-a-group-dragged-inside-a-board-with-clip-content-2.png differ diff --git a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Clips-a-group-dragged-into-a-board-with-clip-content-1.png b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Clips-a-group-dragged-into-a-board-with-clip-content-1.png new file mode 100644 index 0000000000..353a9f9aac Binary files /dev/null and b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Clips-a-group-dragged-into-a-board-with-clip-content-1.png differ diff --git a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Clips-a-group-dragged-into-a-board-with-clip-content-2.png b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Clips-a-group-dragged-into-a-board-with-clip-content-2.png new file mode 100644 index 0000000000..1b1669ced3 Binary files /dev/null and b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Clips-a-group-dragged-into-a-board-with-clip-content-2.png differ diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index c2ee801c41..4286fe6cf0 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -438,6 +438,7 @@ pub(crate) struct RenderState { pub viewport_presented: bool, } +#[derive(Clone)] pub struct InteractiveDragCrop { pub src_doc_bounds: Rect, pub src_selrect: Rect, @@ -3725,7 +3726,21 @@ impl RenderState { ); if use_cached { - if let Some(crop) = self.backbuffer_crop_cache.get(&node_id) { + if let Some(crop) = self.backbuffer_crop_cache.get(&node_id).cloned() { + self.surfaces.canvas(target_surface).save(); + self.surfaces.canvas(target_surface).reset_matrix(); + + if let Some(clips) = clip_bounds.as_ref() { + let antialias = element + .should_use_antialias(scale, self.options.antialias_threshold); + self.clip_target_surface_to_stack( + clips, + target_surface, + scale, + antialias, + ); + } + let crop_image = &crop.image; let crop_src_selrect = crop.src_selrect; @@ -3737,14 +3752,11 @@ impl RenderState { ), None => (0.0, 0.0), }; - let scale = self.get_scale(); let translation = self .surfaces .get_render_context_translation(self.render_area, scale); let canvas = self.surfaces.canvas(target_surface); - canvas.save(); - canvas.reset_matrix(); // If the crop includes shadows/blur (extrect pixels outside the fill/stroke // silhouette), do NOT apply the silhouette clip or we'd cut those pixels. let should_clip_crop = element.shadows.is_empty() && element.blur.is_none(); diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index cb45ba7df3..0a5981ccb8 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -1535,6 +1535,10 @@ impl Shape { return false; } + if matches!(self.shape_type, Type::Group(_)) { + return false; + } + // If a frame shows overflow (clip_content=false) and its visible content exceeds the // frame bounds, a cached crop anchored to the frame can easily become incorrect while // moving (children can extend beyond selrect). Be conservative and render live.