From 04707e82d98118bf40eab8e07f29198b22035011 Mon Sep 17 00:00:00 2001 From: Yunfei He Date: Mon, 25 May 2026 22:53:41 +0800 Subject: [PATCH] fix: intersect all clip rects in stack for nested overflow:hidden Previously Output.get() only used the last clip rect (clips.at(-1)), which meant an inner overflow:hidden box wider than its outer ancestor could effectively loosen the outer clip. Now all active clips are intersected via reduce, and writes are skipped entirely when the intersection is empty. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/runtime/src/paint/paint.ts | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/runtime/src/paint/paint.ts b/packages/runtime/src/paint/paint.ts index c8d2175..20be6d2 100644 --- a/packages/runtime/src/paint/paint.ts +++ b/packages/runtime/src/paint/paint.ts @@ -93,12 +93,26 @@ class Output { return l; }); - // Apply active clip rect - const clip = clips.at(-1); - if (clip) { + // Apply active clip rect — intersect ALL clips in the stack so nested + // overflow:hidden boxes correctly constrain children. + if (clips.length > 0) { + const clip = clips.reduce( + (acc, c) => ({ + x1: acc.x1 != null && c.x1 != null ? Math.max(acc.x1, c.x1) : (acc.x1 ?? c.x1), + x2: acc.x2 != null && c.x2 != null ? Math.min(acc.x2, c.x2) : (acc.x2 ?? c.x2), + y1: acc.y1 != null && c.y1 != null ? Math.max(acc.y1, c.y1) : (acc.y1 ?? c.y1), + y2: acc.y2 != null && c.y2 != null ? Math.min(acc.y2, c.y2) : (acc.y2 ?? c.y2), + }), + { x1: undefined, x2: undefined, y1: undefined, y2: undefined }, + ); + const clipH = typeof clip.x1 === "number" && typeof clip.x2 === "number"; const clipV = typeof clip.y1 === "number" && typeof clip.y2 === "number"; + // If the intersection is empty, skip the write entirely + if (clipH && clip.x1! >= clip.x2!) continue; + if (clipV && clip.y1! >= clip.y2!) continue; + // Skip entirely out-of-bounds writes if (clipV) { const height = lines.length;