diff --git a/src/ui/material/draw_helpers.h b/src/ui/material/draw_helpers.h index 9ff5a58..8f96232 100644 --- a/src/ui/material/draw_helpers.h +++ b/src/ui/material/draw_helpers.h @@ -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(); }