Add a drop shadow cast by the frame
Version Bump / check-bump-label (pull_request) Skipped
Version Bump / apply-version-bump (pull_request) Successful in 20s

New plugin:hyprchrome:shadow_size (px, 0 = off), shadow_color and
shadow_offset (vec2). Off by default, so existing setups are unchanged.

The silhouette is the frame's own outer boundary - extracted out of
GetBorderTexture into AppendFrameOuterPath so the shadow and the shape
casting it can't drift apart - filled into an A8 mask, blurred with three
box passes, then tinted. The frame's cutouts (side notches, the strip
beside the title bar) are part of that outline, so they cast their shape
for free.

Three things worth not undoing later:

- The window's interior is cleared after the blur, not before. Punching
  it first leaves the blur smearing shadow inward across the window's own
  content. Clearing afterwards puts the cut exactly on the ring's inner
  boundary, where the frame's opaque pixels hide it.
- The shadow is not part of the decoration's reserved extents, since
  reserving it would push neighbouring windows away by the shadow's
  width. It's drawn past the decoration's box instead, which is why
  damageEntire() and boundingBox() expand by ShadowMarginLogical().
- It renders at no more than kShadowMaxDim px on the long edge and lets
  the GPU upscale. A blurred blob loses nothing to that, and a
  full-resolution blur would otherwise be re-run per frame for the whole
  length of a resize animation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 01:50:02 +02:00
co-authored by Claude Opus 5
parent 1735a5c796
commit d69fe10b46
7 changed files with 328 additions and 52 deletions
+25 -2
View File
@@ -79,12 +79,35 @@ std::vector<UP<IPassElement>> ChromePassElement::draw() {
if (!tex || !tex->ok())
return {};
std::vector<UP<IPassElement>> children;
// Emitted first so it lands under the frame - which matters, because the
// shadow does run under the frame's own outline (only the window's interior
// is cleared out of it), and the frame is what hides that cut.
const float shadowSizePx = static_cast<float>(PluginState->config.shadowSize->value()) * monitor->m_scale;
const auto configOffset = PluginState->config.shadowOffset->value();
const Vector2D shadowOffsetPx = {configOffset.x * monitor->m_scale, configOffset.y * monitor->m_scale};
const CHyprColor shadowColor{static_cast<uint64_t>(PluginState->config.shadowColor->value())};
if (const auto shadowTex = data.decoration->GetShadowTexture({box.w, box.h}, extentPx, chamferPx, titleBarHeightPx, borderTitleBarWidthPx, shadowSizePx, shadowColor, shadowOffsetPx);
shadowTex && shadowTex->ok()) {
const float margin = ChromeDecoration::ShadowMarginPx(shadowSizePx, shadowOffsetPx);
CTexPassElement::SRenderData shadowData;
shadowData.tex = shadowTex;
shadowData.box = CBox{
box.x - margin + shadowOffsetPx.x, box.y - margin + shadowOffsetPx.y,
box.w + 2.0 * margin, box.h + 2.0 * margin,
};
shadowData.a = data.alpha;
children.emplace_back(makeUnique<CTexPassElement>(shadowData));
}
CTexPassElement::SRenderData texData;
texData.tex = tex;
texData.box = box;
texData.a = data.alpha;
std::vector<UP<IPassElement>> children;
children.emplace_back(makeUnique<CTexPassElement>(texData));
if (titleTex && titleTex->ok() && titleTex->m_size.x > 0 && titleTex->m_size.y > 0) {
@@ -113,5 +136,5 @@ std::optional<CBox> ChromePassElement::boundingBox() {
return data.decoration->FullDecorationExtentGlobal()
.translate(-monitor->m_position)
.expand(4);
.expand(4 + ChromeDecoration::ShadowMarginLogical());
}