Skip to content

fix(paint): include box-shadow and descendant ink in the layer bounds - #223

Merged
LeadcodeDev merged 1 commit into
chantier/audit-2026-09from
fix/layer-bounds-outset-shadow
Sep 22, 2026
Merged

LeadcodeDev merged 1 commit into
chantier/audit-2026-09from
fix/layer-bounds-outset-shadow

Conversation

@LeadcodeDev

Copy link
Copy Markdown
Owner

Severity High, category correctness. Location: crates/rustmotion-core/src/engine/paint_pass.rs:390

Impact

bleed is derived only from node.css.filter (FilterFn::Blur / DropShadow in filter_bleed, line 610). It ignores the node's own box_shadow, which step 5 paints inside this layer (line 411-418) at layout.x + dx - spread, width + spread*2 — i.e. outside the border-box — and it ignores the whole subtree painted at steps 9-10, which with the default overflow: visible may legitimately extend past the parent box (absolutely-positioned children, a child's own scale/pulse transform, a child glow, marquee, which CLAUDE.md explicitly documents as "exempté (leur rôle est de bleed)"). The file's own comment on filter_bleed (line 605-609) states the rule: "a too-tight one would silently clip filter bleed, trading a perf bug for a correctness one". Concretely: a card with box-shadow: 0 20px 40px rgba(0,0,0,.5) loses its shadow the moment any fade_in drives opacity below 1.0, and regains it on the frame opacity reaches 1.0 — a visible pop mid-entrance. The repo already treats this exact invariant as load-bearing for overflow: hidden (test overflow_hidden_does_not_clip_own_outset_box_shadow, line 2909); the opacity layer violates it for the same shadow.

Fix

Compute the layer bounds from the union of: the border-box, the filter bleed, the outset box_shadow extents (|offset| + blur*1.5 + spread per shadow), and — when overflow is visible — the descendants' layout union. Alternatively move the outset box-shadow painting outside the opacity layer and multiply its paint alpha by opacity, and fall back to an unbounded layer when overflow: visible and children exist.

Evidence the audit read

let bleed = node
            .css
            .filter
            .as_deref()
            .map(|list| filter_bleed(list, &length_ctx))
            .unwrap_or(0.0);
        let bounds = Rect::from_xywh(
            box_layout.x - bleed,
            box_layout.y - bleed,
            box_layout.width + bleed * 2.0,
            box_layout.height + bleed * 2.0,
        );

Based directly on the chantier branch.

Part of the September 2026 audit remediation chantier. Refs #220 (RM-01).

@LeadcodeDev LeadcodeDev added the bug Something isn't working label Sep 21, 2026
@LeadcodeDev LeadcodeDev self-assigned this Sep 21, 2026
@LeadcodeDev
LeadcodeDev force-pushed the fix/layer-bounds-outset-shadow branch from baafd53 to 7c9e21a Compare September 21, 2026 23:39
@LeadcodeDev
LeadcodeDev force-pushed the fix/layer-bounds-outset-shadow branch 2 times, most recently from ee0e0e8 to 6890377 Compare September 22, 2026 08:33
bleed is derived only from node.css.filter (FilterFn::Blur / DropShadow in
filter_bleed, line 610). It ignores the node's own box_shadow, which step 5
paints *inside* this layer (line 411-418) at layout.x + dx - spread, width +
spread*2 — i.e. outside the border-box — and it ignores the whole subtree
painted at steps 9-10, which with the default overflow: visible may
legitimately extend past the parent box (absolutely-positioned children, a
child's own scale/pulse transform, a child glow, marquee, which CLAUDE.md
explicitly documents as "exempté (leur rôle est de bleed)"). The file's own
comment on filter_bleed (line 605-609) states the rule: "a *too-tight* one
would silently clip filter bleed, trading a perf bug for a correctness one".
Concretely: a card with box-shadow: 0 20px 40px rgba(0,0,0,.5) loses its
shadow the moment any fade_in drives opacity below 1.0, and regains it on
the frame opacity reaches 1.0 — a visible pop mid-entrance. The repo already
treats this exact invariant as load-bearing for overflow: hidden (test
overflow_hidden_does_not_clip_own_outset_box_shadow, line 2909); the opacity
layer violates it for the same shadow.

Fix: Compute the layer bounds from the union of: the border-box, the filter
bleed, the outset box_shadow extents (|offset| + blur*1.5 + spread per
shadow), and — when overflow is visible — the descendants' layout union.
Alternatively move the outset box-shadow painting outside the opacity layer
and multiply its paint alpha by opacity, and fall back to an unbounded layer
when overflow: visible and children exist.

Refs #220
@LeadcodeDev
LeadcodeDev force-pushed the fix/layer-bounds-outset-shadow branch from 6890377 to 7aa0de5 Compare September 22, 2026 08:43
@LeadcodeDev
LeadcodeDev merged commit 8444e4e into chantier/audit-2026-09 Sep 22, 2026
3 checks passed
@LeadcodeDev
LeadcodeDev deleted the fix/layer-bounds-outset-shadow branch September 22, 2026 08:52
LeadcodeDev added a commit that referenced this pull request Sep 22, 2026
…#223)

bleed is derived only from node.css.filter (FilterFn::Blur / DropShadow in
filter_bleed, line 610). It ignores the node's own box_shadow, which step 5
paints *inside* this layer (line 411-418) at layout.x + dx - spread, width +
spread*2 — i.e. outside the border-box — and it ignores the whole subtree
painted at steps 9-10, which with the default overflow: visible may
legitimately extend past the parent box (absolutely-positioned children, a
child's own scale/pulse transform, a child glow, marquee, which CLAUDE.md
explicitly documents as "exempté (leur rôle est de bleed)"). The file's own
comment on filter_bleed (line 605-609) states the rule: "a *too-tight* one
would silently clip filter bleed, trading a perf bug for a correctness one".
Concretely: a card with box-shadow: 0 20px 40px rgba(0,0,0,.5) loses its
shadow the moment any fade_in drives opacity below 1.0, and regains it on
the frame opacity reaches 1.0 — a visible pop mid-entrance. The repo already
treats this exact invariant as load-bearing for overflow: hidden (test
overflow_hidden_does_not_clip_own_outset_box_shadow, line 2909); the opacity
layer violates it for the same shadow.

Fix: Compute the layer bounds from the union of: the border-box, the filter
bleed, the outset box_shadow extents (|offset| + blur*1.5 + spread per
shadow), and — when overflow is visible — the descendants' layout union.
Alternatively move the outset box-shadow painting outside the opacity layer
and multiply its paint alpha by opacity, and fall back to an unbounded layer
when overflow: visible and children exist.

Refs #220
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant