Add a solid outline tracing the frame's outer edge
New plugin:hyprchrome:outline_size (px, 0 = off) and outline_color (default white) stroke the frame's outer silhouette - chamfers, title bar plateau and side notches - with a flat color, so it reads as a drawn line against the gradient-filled frame underneath. Off by default. The line sits inside the frame rather than centred on the silhouette. The outer path runs along the texture's own bounds, so half of a centred stroke would fall off the surface and vanish on exactly those sides; stroking at double width leaves the inner half, which comes to outline_size on every edge. The clip is the whole ring rather than just the outer path, so an outline thicker than the frame stops at the window's edge instead of spilling across the ring onto the window, and a miter spike at the plateau's dip stays confined to the frame. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,14 @@ struct ChromeConfig {
|
||||
// border color's own alpha.
|
||||
SP<FloatValue> glowStrength;
|
||||
|
||||
// Thickness of the solid outline tracing the frame's edges, in logical
|
||||
// pixels. 0 disables it entirely.
|
||||
SP<IntValue> outlineSize;
|
||||
|
||||
// Color of that outline - a flat color rather than a gradient, so it reads
|
||||
// as a drawn line against the gradient-filled frame it sits on.
|
||||
SP<ColorValue> outlineColor;
|
||||
|
||||
// How far the frame's drop shadow reaches past its own outline, in logical
|
||||
// pixels. 0 disables the shadow entirely.
|
||||
SP<IntValue> shadowSize;
|
||||
|
||||
@@ -298,6 +298,38 @@ void DrawInwardGlow(cairo_t* cr, const ChromeDecorationGeometry& geo, const Chro
|
||||
cairo_restore(cr);
|
||||
}
|
||||
|
||||
// Traces the frame's outer silhouette with a solid `outlinePx`-thick line -
|
||||
// only that edge, not the inner window-side boundary.
|
||||
//
|
||||
// The line lands *inside* the frame rather than centred on the silhouette:
|
||||
// the outer path runs along the texture's own bounds, so half a centred
|
||||
// stroke would fall outside the surface and be clipped away to nothing on
|
||||
// those sides. Stroking at double width puts exactly the inner half on the
|
||||
// surface, which comes to `outlinePx` everywhere.
|
||||
//
|
||||
// The clip is still the whole ring rather than just the outer path, so an
|
||||
// outline thicker than the frame stops at the window's edge instead of
|
||||
// spilling across the ring onto the window - and a miter spike at the
|
||||
// plateau's dip can't escape the frame either.
|
||||
void DrawOutline(cairo_t* cr, const ChromeDecorationGeometry& geo, float outlinePx, const CHyprColor& color) {
|
||||
if (outlinePx < 0.5F || color.a <= 0)
|
||||
return;
|
||||
|
||||
cairo_save(cr);
|
||||
cairo_set_fill_rule(cr, CAIRO_FILL_RULE_EVEN_ODD);
|
||||
|
||||
AppendFrameOuterPath(cr, geo);
|
||||
AppendChamferedRect(cr, geo.innerX0, geo.innerY0, geo.innerX1, geo.innerY1, geo.innerChamfer);
|
||||
cairo_clip(cr);
|
||||
|
||||
AppendFrameOuterPath(cr, geo);
|
||||
cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a);
|
||||
cairo_set_line_width(cr, outlinePx * 2.F);
|
||||
cairo_stroke(cr);
|
||||
|
||||
cairo_restore(cr);
|
||||
}
|
||||
|
||||
// One box blur pass over an A8 surface's rows, `radius` either side, with a
|
||||
// running sum so the cost is independent of the radius. Reads past the row's
|
||||
// ends clamp to its end pixels - which for the shadow means clamping to the
|
||||
@@ -442,14 +474,15 @@ uint64_t ChromeDecoration::getDecorationFlags() { return DECORATION_PART_OF_MAIN
|
||||
|
||||
PHLWINDOW ChromeDecoration::GetOwner() { return windowRef.lock(); }
|
||||
|
||||
SP<Render::ITexture> ChromeDecoration::GetBorderTexture(const Vector2D& sizePx, float extentPx, float chamferPx, const ChromeGradient& gradient, float titleBarHeightPx, float titleBarWidthPx, float glowPx, float glowStrength) {
|
||||
SP<Render::ITexture> ChromeDecoration::GetBorderTexture(const Vector2D& sizePx, float extentPx, float chamferPx, const ChromeGradient& gradient, float titleBarHeightPx, float titleBarWidthPx, float glowPx, float glowStrength, float outlinePx, const CHyprColor& outlineColor) {
|
||||
if (sizePx.x < 1 || sizePx.y < 1)
|
||||
return nullptr;
|
||||
|
||||
if (cachedTexture && cachedTexture->ok() && cachedTexSize == sizePx &&
|
||||
cachedExtent == extentPx && cachedChamfer == chamferPx && cachedGradient == gradient &&
|
||||
cachedTitleBarHeight == titleBarHeightPx && cachedTitleBarWidth == titleBarWidthPx &&
|
||||
cachedGlowSize == glowPx && cachedGlowStrength == glowStrength)
|
||||
cachedGlowSize == glowPx && cachedGlowStrength == glowStrength &&
|
||||
cachedOutlineSize == outlinePx && cachedOutlineColor == outlineColor)
|
||||
return cachedTexture;
|
||||
|
||||
const int w = static_cast<int>(sizePx.x);
|
||||
@@ -484,6 +517,8 @@ SP<Render::ITexture> ChromeDecoration::GetBorderTexture(const Vector2D& sizePx,
|
||||
cairo_fill(cr);
|
||||
cairo_pattern_destroy(pattern);
|
||||
|
||||
DrawOutline(cr, geo, outlinePx, outlineColor);
|
||||
|
||||
// Drawn after the ring, into the hole the fill above just left behind.
|
||||
DrawInwardGlow(cr, geo, gradient, glowPx, glowStrength, w, h);
|
||||
|
||||
@@ -498,6 +533,8 @@ SP<Render::ITexture> ChromeDecoration::GetBorderTexture(const Vector2D& sizePx,
|
||||
cachedTitleBarWidth = titleBarWidthPx;
|
||||
cachedGlowSize = glowPx;
|
||||
cachedGlowStrength = glowStrength;
|
||||
cachedOutlineSize = outlinePx;
|
||||
cachedOutlineColor = outlineColor;
|
||||
|
||||
cairo_destroy(cr);
|
||||
cairo_surface_destroy(surface);
|
||||
|
||||
@@ -47,9 +47,11 @@ public:
|
||||
// gradient's own axis (a single-stop gradient is just a flat fill), plus -
|
||||
// if `glowPx` is non-zero - that same gradient bled `glowPx` inward past
|
||||
// the ring's inner boundary, over the window's own pixels, fading out from
|
||||
// `glowStrength` of its alpha at the window edge. Cached and only
|
||||
// regenerated when any of these change from the last call.
|
||||
SP<Render::ITexture> GetBorderTexture(const Vector2D& sizePx, float extentPx, float chamferPx, const ChromeGradient& gradient, float titleBarHeightPx, float titleBarWidthPx, float glowPx, float glowStrength);
|
||||
// `glowStrength` of its alpha at the window edge, and - if `outlinePx` is
|
||||
// non-zero - both of the ring's edges traced by an `outlinePx`-thick line
|
||||
// in `outlineColor`, drawn just inside them. Cached and only regenerated
|
||||
// when any of these change from the last call.
|
||||
SP<Render::ITexture> GetBorderTexture(const Vector2D& sizePx, float extentPx, float chamferPx, const ChromeGradient& gradient, float titleBarHeightPx, float titleBarWidthPx, float glowPx, float glowStrength, float outlinePx, const CHyprColor& outlineColor);
|
||||
|
||||
// Returns a texture of the frame's drop shadow: the same outer silhouette
|
||||
// GetBorderTexture fills, blurred by `shadowSizePx` and tinted `color`,
|
||||
@@ -88,6 +90,8 @@ private:
|
||||
float cachedTitleBarWidth = -1.F;
|
||||
float cachedGlowSize = -1.F;
|
||||
float cachedGlowStrength = -1.F;
|
||||
float cachedOutlineSize = -1.F;
|
||||
CHyprColor cachedOutlineColor;
|
||||
|
||||
SP<Render::ITexture> cachedShadowTex;
|
||||
Vector2D cachedShadowTexSize = {-1, -1};
|
||||
|
||||
@@ -75,7 +75,10 @@ std::vector<UP<IPassElement>> ChromePassElement::draw() {
|
||||
// with title length - otherwise every title change would needlessly
|
||||
// regenerate an identical border texture.
|
||||
const float borderTitleBarWidthPx = geo.fullSpan ? geo.MaxTitleBarWidth() : geo.titleBarWidth;
|
||||
const auto tex = data.decoration->GetBorderTexture({box.w, box.h}, extentPx, chamferPx, gradient, titleBarHeightPx, borderTitleBarWidthPx, glowPx, glowStrength);
|
||||
const float outlinePx = static_cast<float>(PluginState->config.outlineSize->value()) * monitor->m_scale;
|
||||
const CHyprColor outlineColor{static_cast<uint64_t>(PluginState->config.outlineColor->value())};
|
||||
|
||||
const auto tex = data.decoration->GetBorderTexture({box.w, box.h}, extentPx, chamferPx, gradient, titleBarHeightPx, borderTitleBarWidthPx, glowPx, glowStrength, outlinePx, outlineColor);
|
||||
if (!tex || !tex->ok())
|
||||
return {};
|
||||
|
||||
|
||||
@@ -43,6 +43,15 @@ struct GlobalState {
|
||||
"Peak opacity of the inward glow at the window edge, 0-1",
|
||||
0.35F,
|
||||
FloatValueOptions{.min = 0.F, .max = 1.F}),
|
||||
.outlineSize = makeShared<IntValue>(
|
||||
"plugin:hyprchrome:outline_size",
|
||||
"Thickness of the solid outline tracing the frame's edges, in pixels (0 disables it)",
|
||||
0,
|
||||
IntValueOptions{.min = 0}),
|
||||
.outlineColor = makeShared<ColorValue>(
|
||||
"plugin:hyprchrome:outline_color",
|
||||
"Color of the outline tracing the frame's edges",
|
||||
RGBAToARGB(0xFFFFFFFF)),
|
||||
.shadowSize = makeShared<IntValue>(
|
||||
"plugin:hyprchrome:shadow_size",
|
||||
"How far the frame's drop shadow reaches past its outline, in pixels (0 disables it)",
|
||||
@@ -77,6 +86,8 @@ struct GlobalState {
|
||||
HyprlandAPI::addConfigValueV2(plugin, config.inactiveColor);
|
||||
HyprlandAPI::addConfigValueV2(plugin, config.glowSize);
|
||||
HyprlandAPI::addConfigValueV2(plugin, config.glowStrength);
|
||||
HyprlandAPI::addConfigValueV2(plugin, config.outlineSize);
|
||||
HyprlandAPI::addConfigValueV2(plugin, config.outlineColor);
|
||||
HyprlandAPI::addConfigValueV2(plugin, config.shadowSize);
|
||||
HyprlandAPI::addConfigValueV2(plugin, config.shadowColor);
|
||||
HyprlandAPI::addConfigValueV2(plugin, config.shadowOffset);
|
||||
|
||||
Reference in New Issue
Block a user