Skip to content

PNT1-shadow: box-shadow on iOS and Android (supersedes the shadow half of #80 and #82) - #236

Open
thejackshelton wants to merge 8 commits into
masterfrom
claude/native-paint-effects-cu20nu
Open

thejackshelton wants to merge 8 commits into
masterfrom
claude/native-paint-effects-cu20nu

Conversation

@thejackshelton

@thejackshelton thejackshelton commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Jack · project thread

PNT1-shadow: CSS box-shadow (outer and inset, several layers, blur, spread, offsets, colours, on rounded boxes) drawn by Dragon on iOS and Android, as Chrome 145 paints it.

Supersedes the shadow half of #80 and all of #82. Both were stacked and far behind master. This PR carries their shadow content onto current origin/master as one branch, adapted to master's per-feature registries, the way #198 carried radius. Nothing here is stacked: the base is master.

What changed

Engine (from #80, shadow half)

  • packages/layout/src/paint-shadow.ts: box-shadow layers as Blink 145 and Skia 2ab8add5 paint them: spread shapes with Blink's outset radius adjustment, the blur paths SkBlurMaskFilterImpl picks (analytic rect nine-patch, small rrect nine-patch, A8 mask plus box blur), each shadow blitted onto the backdrop its ancestors paint, encoded so the platform composite is Chrome's colour.
  • packages/layout/src/paint-aa.ts: the A8 coverage device and SkA8_Blitter's srcover (SkBlitter_A8.cpp srcover_p, A8_row_aa).
  • Their suites (layout/test/paint-shadow.test.ts, the A8 cases in paint-aa.test.ts), the harness's paint:shadow:* cases, and the paint-vector inputs from scripts/gen-paint-inputs-shadow.ts: a new regen step paint-inputs-shadow in scripts/regen-steps/pnt1.ts after paint-inputs-radius, with its .gitattributes line.
  • docs/ports.json: the SkBlitter_A8.cpp entry and the AdjustedRadiusDimension range of float_rounded_rect.cc (the Adobe notice is already on master; THIRD_PARTY_NOTICES.md is regenerated from the registry).

Compiler and native (from #82)

  • The box-shadow family (css/properties/shadow.ts) with Chrome's parse beyond the grammar: a calculation, a viewport unit, a font-metric unit and a colour outside the subset are refused; computed values in analysis/paint-values/shadow.ts (lengths to px, currentcolor to the colour, Chrome's serialization).
  • One box-shadow write per shadowed box with shadow facts (lower/paint/shadow.ts).
  • The emitter (emit/paint/shadow.ts) on both backends: dragonSetShadows; after every layout the translated paint-shadow.ts rasters the outer shadows into a companion view directly beneath the box (DragonTree.companion, already on master) and the inset shadows into the inset-shadow stage. A background write re-applies the shadows whose backdrop it changes (emit/paint/background.ts).
  • The shadow-offset-1 raster plant (PLANT_CASES calib-shadow-blur, PLANT_RULES shadow:).
  • animation-kinds/shadow.ts: box-shadow interpolates in Chrome, so it is UNADMITTED; the state runtime refuses a shadow write by name.
  • Fixtures shadow-basic, shadow-rounded, shadow-inset, shadow-cascade, calib-shadow-blur, calib-shadow-colors (both directions) and the rejects reject-shadow-calc, reject-shadow-viewport; fixture-groups/shadow.ts, paint-samples/shadow.ts, pnt1-shadow.test.ts (the device layers against the committed Chrome pixels), pnt1-shadow-facts.test.ts, dragon/test/paint-shadow.test.ts.
  • The shadow channel allowance stays GATE_CHANNEL_DELTA (allowances/shadow.ts is unchanged).

New in this PR: native refusals of what the companion view and backdrop do not cover on master. Each is DRAGON_UNSUPPORTED_VALUE at the box-shadow value, on ios and android only (web draws them itself), tested in dragon/test/paint-shadow.test.ts:

  • html / body (their background paints the canvas, which the shadow backdrop does not model);
  • a replaced element (no comparison proves it yet);
  • an inline box (native inline boxes are unpainted until INL1b, so the shadow would be dropped);
  • a positioned box (Chrome paints it after the in-flow content of its stacking context; without the stacking sort its companion stays at its tree place);
  • a box with a transform or will-change: transform | opacity (the companion view is not transformed with the box, and Chrome composites the group on its own);
  • a shadow inside such a box (shadow-in-group: Chrome blits the shadow into the group's raster, not onto the page backdrop).

What #196 (opacity + stacking) or a follow-up must add once both land

Changed tests (reasons)

  • layout/test/paint-seams.test: shadow is a filled seam, with its two index.ts lines.
  • dragon/test/s4b.test: box-shadow gets two shadow lists for the paint-never-reaches-layout check (colours are invalid for it).
  • parity/test/device-lanes.test (paint plant verdict): the verdict cases run on the shadow plant's rule (shadow:) and also check that a radius-rule hit does not catch the shadow plant; the radius plant's caught case is kept.
  • dragon/test/s4b.test (ii): box-shadow's paint-only check runs on position: static boxes, since ios refuses a shadow on a positioned box (this PR's refusal); every other longhand keeps position: relative. Intent unchanged: two values of each paint longhand lower to identical layout styles.
  • Hit lane (parity/test/hit-refusals-radius.ts): the rounded shadow fixtures (shadow-rounded, shadow-inset, calib-shadow-blur, both directions) join RADIUS_OUT: hitRefusal refuses them for their border radius, with the radius reason. hit-report.test and lanes.test read the list, so they stay exact.
  • parity/src/hit-capture.ts identity check: box-shadow joins the keys added since the identity base (every capture and emitted rule gained it), and the shadow fixtures join IDENTITY_NEW. hit-capture-identity.test gains a case proving box-shadow is stripped and a shadow-like key (text-shadow) is not.
  • Floors (dragon/test/paint-seams-floor.json, seams-floor.json): appended with DRAGON_FLOOR_WRITE=1 (box-shadow, the shadow plant and support files, and radius entries the earlier lane had not written). Nothing was removed.

Outside the spec

What passed

Head bbfac53b06afa50256aab435a26fae8bae8cbcec (after two catch-up merges of origin/master, which GitHub reported conflicting in generated files only; the merge drivers kept this side and regen rebuilt them).

  • Regen on CI: run https://github.com/compiled-run/dragoncss/actions/runs/37900368527 (commit 67c5b4ea0e), run https://github.com/compiled-run/dragoncss/actions/runs/37919424725 (commit c9cf64016b), and on the final merge run https://github.com/compiled-run/dragoncss/actions/runs/37933312205 (commit bbfac53b06).
  • Rows promoted by the regen: box-shadow <shadow-list>, none and inherit, ltr and rtl (6 rows per native target), are caveat on iOS and Android with chrome-dual computed-value proofs from the six shadow fixtures, until the driver's device run adds the paint proofs; exact on web. Before this PR box-shadow was DRAGON_UNSUPPORTED_PROPERTY.
  • pnpm typecheck: passes on bbfac53b06.
  • Platform-free, local, on bbfac53b06: all of packages/dragon/test plus layout/test/paint-shadow, paint-aa, paint-seams, paint-radius: 80 files, 1405 tests pass. (dragon/test/native-backends needs the toolchains; it passes on CI below.)
  • Chrome and native, ci:test-files run https://github.com/compiled-run/dragoncss/actions/runs/37942039246, tested bbfac53b06afa50256aab435a26fae8bae8cbcec, 24 files: all pass: pnt1-shadow (14: the device layers of every shadow fixture, both directions, every DPR, against the committed Chrome pixels within SHADOW_CHANNEL_DELTA = the gate delta), pnt1-shadow-facts, pnt1-radius, pnt1-radius-edges, chrome-ports, registry-claims (parity), android-profile, css-escapes, device-lanes, device-run, fixture-reader, hit-capture-identity, hit-report, native-compare, paint-seams, paint-vectors-cli, pnt2-samples, samples, corpus-dpr, native-backends, native-dpr-swift, native-dpr-kotlin, paint-roots (TS = Swift = Kotlin for the translated shadow roots). lanes.test: 367 passed, 1 failed: device step pending ("every device lane ran": ios layout-vectors-device not run).
  • pnpm pr:review 236 --once: exit 0 on bbfac53b06: CI checks passed, UNREVIEWED (Macroscope at its spending limit), 0 unanswered findings.

Reviewed diff (outside .macroscope/ignore.md): about 207 KB over 42 files. The compiler half needs the engine half, so splitting would leave one PR stacked on the other; it ships as one, as #198 did.

🤖 Generated with Claude Code

https://claude.ai/code/session_012dJVP4456dCkJKMJUUf3xG


Generated by Claude Code

…nto master

The shadow engine from #80 (paint-shadow.ts, paint-aa.ts's A8 coverage blit, their tests, the
paint-vector input generator and its regen step, the harness cases, the ports entries), and the
compiler and native half from #82 (the box-shadow family, its computed values, one write per
shadowed box, the companion-view and inset-stage emitters, the shadow-offset-1 plant, the shadow
fixtures, samples and Chrome comparison tests), adapted to master's per-feature registries.

New: native refusals, at the box-shadow value, of what the companion view and the shadow backdrop
do not cover on master: html and body, replaced elements, inline boxes, positioned boxes (paint
order without the stacking sort), and a shadow on or inside a transform or a compositing
will-change. Floors appended with DRAGON_FLOOR_WRITE=1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dJVP4456dCkJKMJUUf3xG
@thejackshelton thejackshelton self-assigned this Oct 9, 2026
github-actions Bot and others added 7 commits October 9, 2026 09:29
Commands: pnpm regen --skip lanes-host --skip tw-sweep (macos-26) and pnpm regen --only lanes-host (xcode-27), alternated
to a fixed point, then pnpm regen --only tw-sweep (macos-26).
Base: 89276b6
Run: https://github.com/compiled-run/dragoncss/actions/runs/37900368527
…t refuse

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dJVP4456dCkJKMJUUf3xG
…hadow and the shadow fixtures join the identity check's post-base keys and files

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dJVP4456dCkJKMJUUf3xG
…eported a conflict; generated files keep this side, regen rebuilds them)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dJVP4456dCkJKMJUUf3xG
Commands: pnpm regen --skip lanes-host --skip tw-sweep (macos-26) and pnpm regen --only lanes-host (xcode-27), alternated
to a fixed point, then pnpm regen --only tw-sweep (macos-26).
Base: 8be7846
Run: https://github.com/compiled-run/dragoncss/actions/runs/37919424725
…eported a conflict after #232 and #233; generated files keep this side, regen rebuilds them)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dJVP4456dCkJKMJUUf3xG
Commands: pnpm regen --skip lanes-host --skip tw-sweep (macos-26) and pnpm regen --only lanes-host (xcode-27), alternated
to a fixed point, then pnpm regen --only tw-sweep (macos-26).
Base: 197c05f
Run: https://github.com/compiled-run/dragoncss/actions/runs/37933312205

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants