Skip to content

Windows: real CSS-style box-shadow via DirectComposition (#210 follow-up) #230

Closed

Description

@proggeramlug

Context

#210 wired 4 of the 5 Windows styling stubs (decoration / opacity / border_color / border_width) at v0.5.347. The fifth — widget.shadow — remained Stub. Storage was already in place at crates/perry-ui-windows/src/widgets/mod.rs::SHADOW_PARAMS; what's missing is the rendering pass.

Why a separate issue

Real CSS box-shadow rendering on Win32 child controls (vs top-level windows) needs DirectComposition:

  • IDCompositionDevice / IDCompositionVisual / IDCompositionEffectGroup
  • IDCompositionDropShadowEffect (the named CSS-equivalent shape: color, blur, offset)
  • DXGI swap chain integration so DirectComposition can composite atop the GDI render

Or alternatively a custom alpha-blended WM_PAINT pass on the parent that draws N concentric translucent rectangles to approximate Gaussian blur. This is the classic GDI "fake shadow" — visible but not pixel-accurate.

Either path is multi-day work that didn't fit inside #210's scope. Decoration / opacity / border could land via small per-prop apply functions; shadow needs new infrastructure.

Acceptance criteria

  • widget.shadow row in the styling matrix flips from Stub → Wired for Windows.
  • A widget calling widgetSetShadow(handle, r, g, b, a, blur, offsetX, offsetY) produces a visible drop shadow under the widget's bounds, matching the CSS box-shadow reference within reasonable approximation (color, offset, and at least 8-16px of blur radius).
  • Existing widgets with corner radius (which uses SetWindowRgn) still render correctly — the shadow path must compose with the rounded clip, not fight it.
  • docs/examples/ui/styling/visual_test.ts section 4 (Shadow) renders visible shadows on Windows.

Hints for the implementer

  • crates/perry-ui-macos/src/widgets/mod.rs::set_shadow is the reference (CALayer-based — sets setMasksToBounds: false + shadowOpacity / shadowRadius / shadowOffset). Behavior to match: opaque shadow color with alpha riding on shadowOpacity; positive offsetY = downward (HTML convention).
  • The SHADOW_PARAMS store already exists (mod.rs:798) with the full 7-tuple. The apply_corner_radius deferred-paint pattern at mod.rs:962 is the canonical "call from layout pipeline" hook — apply_shadow(handle) should slot in symmetrically.
  • DirectComposition requires Windows 8+ (matches Perry's minimum). The windows-rs crate already enables Win32_Graphics_DirectComposition would need to be added to crates/perry-ui-windows/Cargo.toml's windows features.
  • Two-stage strategy that keeps initial scope manageable: (1) GDI alpha-blend approximation first — visible shadow but not perfectly Gaussian; (2) DirectComposition upgrade later for true CSS parity. This issue could be split if the GDI step is enough for a v1.

Closes

When this lands: Windows reaches 43/43 Wired alongside every other Perry-supported platform. Combined with #210 (now closed except this row), the styling matrix is fully green for issue #185.

Activity

  1. added a commit that references this issue on Apr 28, 2026
  2. proggeramlug commented on May 10, 2026

    @proggeramlug
    ContributorAuthor

    Closing as already-implemented — this slipped through tracking.

    Shipped in commit 3f79c09 ("feat(windows): #210 wire shadow paint pass — matrix is 43/43 Wired"), v0.5.347. That commit closed #210 but forgot to also close this follow-up.

    Acceptance criteria met:

    • Styling matrix row 33 (docs/src/ui/styling-matrix.md) shows Windows shadow ✓ Wired.
    • Real implementation at crates/perry-ui-windows/src/widgets/mod.rs:920-1323:
      • set_shadow(handle, r, g, b, a, blur, offset_x, offset_y) stores params in SHADOW_PARAMS and registers the widget with its parent HWND's shadow paint subclass.
      • paint_shadow_for_child renders into a 32bpp top-down DIB via CreateDIBSection, applies per-pixel quadratic falloff (alpha = base * (1 - d/blur)^2) as a cheap visible Gaussian approximation, then AlphaBlends onto the parent DC with AC_SRC_ALPHA + AC_SRC_OVER.
      • Bounded blur (≤64 px) and offset (±256 px) clamps to keep allocations sane; widget rect stays fully transparent so the child's own paint composites on top.
      • Cooperates with SetWindowRgn-based corner radius — shadow lands on the parent surface, not the child's clipped surface.

    Went a different path than the issue's "DirectComposition with IDCompositionDropShadowEffect" suggestion — DirectComposition would be more pixel-accurate but adds a DXGI swap-chain dep and substantial new infrastructure. The GDI alpha-blend approximation is the same path the issue's "Hints for the implementer" section listed as the bounded v1, and shipped within #210's scope rather than as a separate effort.

    If anyone hits a real-world fidelity gap with the GDI approximation (banding on very wide blur, color-bleeding at high alpha, etc.) please open a fresh issue with a screenshot — DirectComposition can be revisited for v2 then.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions