fix(ui): tighter, darker, non-clipping card drop shadow

Rework DrawCardDropShadow (Light/Marble): a Gaussian-sampled ring stack
matching a `0 0 4px rgb(50 53 58 / 34a)` box-shadow — offset-free, darker,
and tighter than the old faint 14px black halo. Clip to the shadow's own
bounding box (card ± reach) instead of the child window bounds so top/bottom
shadows are no longer chopped where a card sits flush with its container,
while staying on the card's own draw list (correct z-order in modals/popups).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-11 12:32:42 -05:00
parent cff958c21e
commit aa60188665

View File

@@ -568,28 +568,39 @@ inline bool CardDropShadowWanted()
inline void DrawCardDropShadow(ImDrawList* dl, const ImVec2& pMin, const ImVec2& pMax, float rounding)
{
const float dp = Layout::dpiScale();
const int kRings = 12;
const float spread = 14.0f * dp; // how far the shadow reaches beyond the card edge (all sides)
// Matches the design mockup's `box-shadow: 0 0 4px rgb(50 53 58 / 26%)`: a tight, darker, colored
// uniform shadow. Sampled as a stack of rounded-rect strokes stepping OUTWARD from the card edge
// with a Gaussian alpha falloff (darkest at the edge, ~0 by the tail). ~1px-spaced, near-1px-thick
// rings keep overlap (and thus alpha accumulation) low, so the peak reads close to the target.
const float blur = 4.0f * dp; // CSS-like blur radius
const float reach = blur * 1.5f; // how far the shadow extends beyond the card edge (all sides)
const float sigma = blur * 0.6f;
const float peak = 34.0f; // edge alpha for rgb(50,53,58) (~13% after the card fill masks the inner half)
const int steps = std::max(6, (int)(reach / dp + 0.5f));
// Cards live inside clipping child windows, so the shadow (which extends past the card edge) is
// otherwise cut off at the child boundary — most visibly on the sidebar's inner edge. Widen the
// clip HORIZONTALLY (to the card + spread, clamped to the viewport) so the side shadows reach into
// the gaps; keep the VERTICAL clip at the child bounds so a card scrolled to a child's top/bottom
// edge can't bleed its shadow into the header/footer above or below the scroll area.
// The shadow rides on the card's OWN draw list (so it keeps the right z-order in EVERY context —
// main content, modals, foreground popups, notifications; a global background draw list would drop
// a modal/popup card's shadow behind its scrim). But cards often live inside clipping child windows
// whose clip rect chops the shadow wherever a card sits flush with the child's top/bottom/side edge.
// Clip instead to the shadow's own bounding box (card ± reach), so all four sides show in full —
// bounded on each side to at most `reach` beyond the child clip (and the viewport) so a card scrolled
// partly out of a scroll area still can't smear its shadow more than the halo width into neighbours.
ImGuiViewport* vp = ImGui::GetMainViewport();
const ImVec2 cur0 = dl->GetClipRectMin();
const ImVec2 cur1 = dl->GetClipRectMax();
const float x0 = std::max(vp->Pos.x, std::min(cur0.x, pMin.x - spread));
const float x1 = std::min(vp->Pos.x + vp->Size.x, std::max(cur1.x, pMax.x + spread));
dl->PushClipRect(ImVec2(x0, cur0.y), ImVec2(x1, cur1.y), false);
for (int i = kRings; i >= 1; --i) {
float t = (float)i / (float)kRings; // 1 (outer) .. near 0 (by the card)
float e = spread * t; // expansion at this ring
int a = (int)(18.0f * (1.0f - t) + 0.5f); // alpha: ~0 at the outer edge, strongest by the card
const float x0 = std::max(vp->Pos.x, std::max(pMin.x - reach, cur0.x - reach));
const float y0 = std::max(vp->Pos.y, std::max(pMin.y - reach, cur0.y - reach));
const float x1 = std::min(vp->Pos.x + vp->Size.x, std::min(pMax.x + reach, cur1.x + reach));
const float y1 = std::min(vp->Pos.y + vp->Size.y, std::min(pMax.y + reach, cur1.y + reach));
dl->PushClipRect(ImVec2(x0, y0), ImVec2(x1, y1), false);
for (int i = 0; i < steps; ++i) {
float d = reach * (float)i / (float)(steps - 1); // 0 (card edge) .. reach (outer tail)
float g = std::exp(-(d * d) / (2.0f * sigma * sigma));
int a = (int)(peak * g + 0.5f);
if (a < 1) continue;
ImVec2 smn(pMin.x - e, pMin.y - e);
ImVec2 smx(pMax.x + e, pMax.y + e);
dl->AddRect(smn, smx, IM_COL32(0, 0, 0, a), rounding + e, 0, 2.0f * dp);
ImVec2 smn(pMin.x - d, pMin.y - d);
ImVec2 smx(pMax.x + d, pMax.y + d);
dl->AddRect(smn, smx, IM_COL32(50, 53, 58, a), rounding + d, 0, 1.3f * dp);
}
dl->PopClipRect();
}