Add an inward glow bleeding from the border over the window
Version Bump / check-bump-label (pull_request) Skipped
Version Bump / apply-version-bump (pull_request) Successful in 21s

New plugin:hyprchrome:glow_size (px, 0 = off) and glow_strength (0-1)
bleed the border's own gradient inward past the window edge, over the
window's own pixels. Off by default, so existing setups are unchanged.

Drawn into the hole the ring's even-odd fill already leaves behind, from
overlapping layers - one per px of depth - each covering the window edge
inward to a progressively greater depth. Overlapping rather than abutting
disjoint bands avoids an antialiasing seam at every shared edge; the cost
is that a layer composites over every deeper one, so its alpha is solved
outermost-inward from the target profile rather than being it.

That profile is strength * (1 - d/glow)^2.2: steep off the window edge,
easing into a tail that reaches zero tangentially so there's no ring
where it stops.

Corner handling, in order of how much it mattered:
- Each layer's outer edge is the ring's inner boundary verbatim, chamfer
  vertices and all. Anything else detaches the glow from the frame and
  opens a sliver of unpainted window at each corner.
- Each layer's inner edge - which is what the falloff's contours actually
  follow - is filleted in proportion to its own depth, so the glow reads
  as chamfered where it meets the frame and rounder as it fades inward.
- Those inner edges also shrink their chamfer by (2-sqrt2) per px of
  inset. Insetting a chamfered rect while holding the chamfer constant
  moves the 45-degree face in by d*sqrt(2) rather than d, which had the
  glow running ~41% deeper at every corner than along the sides.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 01:19:16 +02:00
co-authored by Claude Opus 5
parent 3b89aa1c0c
commit 1735a5c796
7 changed files with 244 additions and 33 deletions
+3
View File
@@ -59,6 +59,8 @@ Config values live under `plugin:hyprchrome:*` and are declared in `src/GlobalSt
- `extent` (int, px the border extends past the window edge) - `extent` (int, px the border extends past the window edge)
- `follow_hyprland_border_color` (bool) — when true (default), the border tracks Hyprland's own resolved active/inactive border color instead of `active_color`/`inactive_color` - `follow_hyprland_border_color` (bool) — when true (default), the border tracks Hyprland's own resolved active/inactive border color instead of `active_color`/`inactive_color`
- `active_color` / `inactive_color` (gradient — one or more colors + optional angle, same syntax as `general:col.active_border`) — used when `follow_hyprland_border_color` is false, or when Hyprland reports no border color at all - `active_color` / `inactive_color` (gradient — one or more colors + optional angle, same syntax as `general:col.active_border`) — used when `follow_hyprland_border_color` is false, or when Hyprland reports no border color at all
- `glow_size` (int, px the border's color bleeds inward past the window edge, over the window itself — 0, the default, disables it)
- `glow_strength` (float 01, peak opacity of that glow at the window edge, as a fraction of the border color's own alpha)
- `titlebar_height` (int, px) - `titlebar_height` (int, px)
- `titlebar_text_size` (int, px) - `titlebar_text_size` (int, px)
- `titlebar_font` (string, passed to `cairo_select_font_face`) - `titlebar_font` (string, passed to `cairo_select_font_face`)
@@ -79,6 +81,7 @@ Per-window decoration (`src/ChromeDecoration.hpp/.cpp`): implements `IHyprWindow
- `draw()` doesn't render directly; it enqueues a `ChromePassElement` into Hyprland's render pass each frame. - `draw()` doesn't render directly; it enqueues a `ChromePassElement` into Hyprland's render pass each frame.
- `GetBorderTexture(...)` and `GetTitleTexture(...)` are the actual cairo/Pango rendering entry points, each memoizing against their own last-seen parameters (size, extent, chamfer, color, title text, font, etc.) so unchanged frames reuse the cached `Render::ITexture` instead of re-rendering. - `GetBorderTexture(...)` and `GetTitleTexture(...)` are the actual cairo/Pango rendering entry points, each memoizing against their own last-seen parameters (size, extent, chamfer, color, title text, font, etc.) so unchanged frames reuse the cached `Render::ITexture` instead of re-rendering.
- `FullDecorationExtentGlobal()` computes the decoration's box in global logical coordinates, accounting for workspace animation offset and floating-window offset. - `FullDecorationExtentGlobal()` computes the decoration's box in global logical coordinates, accounting for workspace animation offset and floating-window offset.
- The inward glow (`glow_size`/`glow_strength`) is drawn by `DrawInwardGlow` into the hole the ring's even-odd fill leaves behind, so it lands on the window's own pixels (this decoration is `DECORATION_LAYER_OVER`, and the texture spans the whole window box, not just the ring). Its falloff is built from *overlapping* fills (one layer per px of depth, clamped to `kGlowMinLayers`/`kGlowMaxLayers`) — layer *i* covers the window edge inward to depth `glowPx * i / layers`, so a pixel `d` from the edge is painted by every layer deeper than `d`. The profile is stated explicitly (`strength * (1 - d/glowPx)^kGlowFalloffExponent` — fast off the edge, easing into a tail that reaches zero tangentially so there's no ring where it stops), and since each layer composites over every deeper one, a layer's own alpha is *not* its target: it's solved outermost-inward as `1 - a_j = (1 - T_j) / (1 - T_j+1)`. Changing the profile means changing `targetAt`, not the per-layer alphas. Abutting disjoint bands instead would leave an antialiasing seam at every shared edge; that's the reason for the overlap, don't "optimize" it away. Every layer's *outer* edge is the ring's inner boundary verbatim, chamfer vertices and all — filleting or otherwise altering it detaches the glow from the frame and opens a sliver of unpainted window at each corner. The corner softening lives entirely on the layers' *inner* edges, which are what the accumulated falloff's contours actually follow: each is inset by its own depth, filleted by `kGlowCornerSmoothing` × that depth (`AppendFilletedPolygon`, a quadratic Bezier through each vertex), and has its chamfer shrunk by `kChamferInsetShrink` × that depth. That last correction is not optional cosmetics — insetting a chamfered rect while holding its chamfer constant moves the 45° face in by `d·√2` rather than `d`, so without it the glow runs ~41% deeper at every corner than along the sides. The per-layer alpha is baked into the gradient pattern (`CreateGradientPattern`'s `alphaScale`) specifically so each layer can be a `cairo_fill` of its own band rather than a clip + `cairo_paint_with_alpha`, which would rasterize the clip's full extents — i.e. the whole window area — once per layer.
Render-pass element (`src/ChromePassElement.hpp/.cpp`): an `EK_CUSTOM` pass element (`ChromePassElement::draw()`) that runs once per frame per window and: Render-pass element (`src/ChromePassElement.hpp/.cpp`): an `EK_CUSTOM` pass element (`ChromePassElement::draw()`) that runs once per frame per window and:
- Derives the corner chamfer live from the window's own `rounding()` (in device px, scaled by monitor scale) — so the border's cut corners track the window's rounding through config reloads, per-window rules, and animations. - Derives the corner chamfer live from the window's own `rounding()` (in device px, scaled by monitor scale) — so the border's cut corners track the window's rounding through config reloads, per-window rules, and animations.
+8
View File
@@ -18,6 +18,14 @@ struct ChromeConfig {
SP<GradientValue> activeColor; SP<GradientValue> activeColor;
SP<GradientValue> inactiveColor; SP<GradientValue> inactiveColor;
// How far the border's glow bleeds inward past the window edge, over the
// window's own pixels, in logical pixels. 0 disables the glow entirely.
SP<IntValue> glowSize;
// Peak opacity of that glow, at the window edge, as a fraction of the
// border color's own alpha.
SP<FloatValue> glowStrength;
// Height of the title bar hanging off the floating top-border cutout, in // Height of the title bar hanging off the floating top-border cutout, in
// logical pixels. // logical pixels.
SP<IntValue> titlebarHeight; SP<IntValue> titlebarHeight;
+202 -29
View File
@@ -11,6 +11,9 @@
#include <cairo/cairo.h> #include <cairo/cairo.h>
#include <algorithm> #include <algorithm>
#include <cmath>
#include <limits>
#include <numbers>
#include <vector> #include <vector>
namespace { namespace {
@@ -36,7 +39,11 @@ std::vector<size_t> Utf8CodepointStarts(const std::string& s) {
// compositor's own border, and it only costs anything on a cache miss. // compositor's own border, and it only costs anything on a cache miss.
constexpr int kStopsPerSegment = 8; constexpr int kStopsPerSegment = 8;
cairo_pattern_t* CreateGradientPattern(const ChromeGradient& gradient, double w, double h) { // `alphaScale` multiplies every stop's own alpha - the glow layers below
// reuse the border's gradient at a fraction of its opacity, and baking that
// into the pattern lets them cairo_fill() (which only touches the filled
// band) instead of clip+paint (which rasterizes the clip's whole extents).
cairo_pattern_t* CreateGradientPattern(const ChromeGradient& gradient, double w, double h, double alphaScale = 1.0) {
const auto axis = gradient.AxisFor(w, h); const auto axis = gradient.AxisFor(w, h);
const auto pattern = cairo_pattern_create_linear(axis.x0, axis.y0, axis.x1, axis.y1); const auto pattern = cairo_pattern_create_linear(axis.x0, axis.y0, axis.x1, axis.y1);
@@ -45,11 +52,195 @@ cairo_pattern_t* CreateGradientPattern(const ChromeGradient& gradient, double w,
for (int i = 0; i <= steps; ++i) { for (int i = 0; i <= steps; ++i) {
const double t = static_cast<double>(i) / steps; const double t = static_cast<double>(i) / steps;
const auto color = gradient.SampleAt(static_cast<float>(t)); const auto color = gradient.SampleAt(static_cast<float>(t));
cairo_pattern_add_color_stop_rgba(pattern, t, color.r, color.g, color.b, color.a); cairo_pattern_add_color_stop_rgba(pattern, t, color.r, color.g, color.b, color.a * alphaScale);
} }
return pattern; return pattern;
} }
struct Point {
double x = 0, y = 0;
};
// The corners of a chamfered rectangle spanning [x0,x1]x[y0,y1], cut by
// `chamfer` (clamped to what the rect can fit) - eight points, or the plain
// four if the chamfer rounds away to nothing. Empty if the rect has collapsed.
std::vector<Point> ChamferedRectPoints(float x0, float y0, float x1, float y1, float chamfer) {
if (x1 <= x0 || y1 <= y0)
return {};
const float c = std::clamp(chamfer, 0.F, std::min(x1 - x0, y1 - y0) / 2.F);
if (c <= 0.F)
return {{x0, y0}, {x1, y0}, {x1, y1}, {x0, y1}};
return {{x0 + c, y0}, {x1 - c, y0}, {x1, y0 + c}, {x1, y1 - c},
{x1 - c, y1}, {x0 + c, y1}, {x0, y1 - c}, {x0, y0 + c}};
}
// Appends `pts` as a closed subpath, with every vertex rounded off by a
// `smooth`-px fillet: the path leaves the incoming edge `smooth` px early and
// rejoins the outgoing one `smooth` px late, bridged by a quadratic Bezier
// through the vertex itself (written as the equivalent cubic, which is all
// cairo takes). `smooth` is capped at half the shortest edge so two adjacent
// fillets meet at that edge's midpoint at worst instead of overrunning each
// other; at 0 this is just a polyline.
void AppendFilletedPolygon(cairo_t* cr, const std::vector<Point>& pts, double smooth) {
const size_t n = pts.size();
if (n < 3)
return;
double maxSmooth = std::numeric_limits<double>::max();
for (size_t i = 0; i < n; ++i) {
const auto& a = pts[i];
const auto& b = pts[(i + 1) % n];
maxSmooth = std::min(maxSmooth, std::hypot(b.x - a.x, b.y - a.y) / 2.0);
}
const double s = std::clamp(smooth, 0.0, maxSmooth);
if (s <= 0.0) {
cairo_move_to(cr, pts[0].x, pts[0].y);
for (size_t i = 1; i < n; ++i)
cairo_line_to(cr, pts[i].x, pts[i].y);
cairo_close_path(cr);
return;
}
// `s` px from `from` towards `to`.
const auto along = [](const Point& from, const Point& to, double d) {
const double dx = to.x - from.x, dy = to.y - from.y;
const double len = std::hypot(dx, dy);
return len > 0 ? Point{from.x + dx / len * d, from.y + dy / len * d} : from;
};
for (size_t i = 0; i < n; ++i) {
const auto& prev = pts[(i + n - 1) % n];
const auto& cur = pts[i];
const auto& next = pts[(i + 1) % n];
const auto in = along(cur, prev, s);
const auto out = along(cur, next, s);
if (i == 0)
cairo_move_to(cr, in.x, in.y);
else
cairo_line_to(cr, in.x, in.y);
cairo_curve_to(cr,
cur.x + (in.x - cur.x) / 3.0, cur.y + (in.y - cur.y) / 3.0,
cur.x + (out.x - cur.x) / 3.0, cur.y + (out.y - cur.y) / 3.0,
out.x, out.y);
}
cairo_close_path(cr);
}
// Appends a closed chamfered-rectangle subpath spanning [x0,x1]x[y0,y1], its
// corners cut by `chamfer` and then optionally softened by a `smooth`-px
// fillet. Returns false without touching the path if the rect has collapsed
// to nothing.
bool AppendChamferedRect(cairo_t* cr, float x0, float y0, float x1, float y1, float chamfer, float smooth = 0.F) {
const auto pts = ChamferedRectPoints(x0, y0, x1, y1, chamfer);
if (pts.empty())
return false;
AppendFilletedPolygon(cr, pts, smooth);
return true;
}
// How many overlapping layers the inward glow's falloff is built from - one
// per px of depth, so the bands stay sub-pixel-ish either way, bounded so a
// hairline glow doesn't waste fills and a huge one doesn't run away with them
// (each layer is another band-shaped fill, only on a cache miss).
constexpr int kGlowMinLayers = 16;
constexpr int kGlowMaxLayers = 64;
// Shape of the falloff: alpha at depth d is strength * (1 - d/glowPx)^this.
// Above 1 the glow drops off steeply at the window edge and then eases out
// into a long tail - and, because it reaches zero tangentially rather than
// at an angle, without a visible ring where the tail finally stops.
constexpr float kGlowFalloffExponent = 2.2F;
// How much of its own depth each glow layer's inner edge is filleted by, so
// the glow's contours round off the further in they go. Only the inner edges
// get this - the outermost one has to stay the ring's inner boundary exactly,
// chamfer vertices and all, or the corners open up a sliver of unpainted
// window between the frame and the glow.
constexpr float kGlowCornerSmoothing = 0.5F;
// A chamfered rect inset by `d` only stays a constant `d` away from its
// original on the straight edges: holding the chamfer itself constant pushes
// the 45-degree face in by d*sqrt(2) rather than d, so the band would run
// ~41% deeper at each corner than along the sides. Shrinking the chamfer by
// this much per px of inset makes the offset properly parallel instead.
constexpr float kChamferInsetShrink = 2.F - std::numbers::sqrt2_v<float>;
// Bleeds the border color inward past the window edge - over the window's own
// pixels, since this decoration renders on DECORATION_LAYER_OVER - fading out
// over `glowPx` and peaking at `strength` (times the gradient's own alpha) at
// the edge itself.
//
// Each layer covers the region from the window edge inward to a progressively
// greater depth, so a pixel `d` from the edge is painted by every layer deeper
// than `d`. Overlapping the layers rather than abutting disjoint bands is what
// keeps the ramp free of the antialiasing seams their shared edges would
// otherwise leave - the price being that a layer can't just be given the alpha
// the profile calls for, since it composites on top of every deeper layer too.
//
// Band j (between depths j-1 and j) is covered by layers j..n, landing at
// 1 - prod(1 - a_i) for i >= j. Walking outermost-inward, each layer's own
// alpha then falls out of the profile directly:
//
// 1 - a_j = (1 - T_j) / (1 - T_j+1)
//
// with T_j the target alpha sampled at band j's midpoint, and T_n+1 = 0.
void DrawInwardGlow(cairo_t* cr, const ChromeDecorationGeometry& geo, const ChromeGradient& gradient, float glowPx, float strength, int w, int h) {
if (glowPx < 1.F || strength <= 0.F || geo.innerW <= 0 || geo.innerH <= 0)
return;
const int layers = std::clamp(static_cast<int>(std::ceil(glowPx)), kGlowMinLayers, kGlowMaxLayers);
const double peak = std::clamp(strength, 0.F, 1.F);
const auto targetAt = [&](double depth) {
return peak * std::pow(1.0 - std::clamp(depth / glowPx, 0.0, 1.0), kGlowFalloffExponent);
};
cairo_save(cr);
cairo_set_fill_rule(cr, CAIRO_FILL_RULE_EVEN_ODD);
// Deepest (faintest) layer first, so each iteration already knows the
// accumulated target of everything that will composite under it.
double deeperTarget = 0.0;
for (int i = layers; i >= 1; --i) {
const float depth = glowPx * static_cast<float>(i) / layers;
// Sampled at the band's shallow (brighter) edge rather than its midpoint,
// so the innermost band lands on `strength` exactly instead of half a
// band short of it. The half-band brightness bias that trades for is a
// sub-pixel shift at these layer counts.
const double target = targetAt(glowPx * (static_cast<double>(i) - 1.0) / layers);
const auto pattern = CreateGradientPattern(gradient, w, h, 1.0 - (1.0 - target) / (1.0 - deeperTarget));
cairo_set_source(cr, pattern);
deeperTarget = target;
// Every layer's outer edge is the ring's inner boundary verbatim, so the
// glow always meets the frame exactly.
AppendChamferedRect(cr, geo.innerX0, geo.innerY0, geo.innerX1, geo.innerY1, geo.innerChamfer);
// Punches this layer's un-glowed middle back out - inset by `depth`, with
// the corner both parallel-corrected and rounded off in proportion to how
// deep it is. Since it's these edges that the accumulated falloff's
// contours follow, the glow reads as chamfered where it meets the frame
// and progressively rounder inward. On a window smaller than the glow is
// deep the middle collapses to nothing and the layer just covers all of
// it, which is the right answer anyway.
AppendChamferedRect(cr,
geo.innerX0 + depth, geo.innerY0 + depth, geo.innerX1 - depth, geo.innerY1 - depth,
geo.innerChamfer - depth * kChamferInsetShrink, depth * kGlowCornerSmoothing);
cairo_fill(cr);
cairo_pattern_destroy(pattern);
}
cairo_restore(cr);
}
} }
ChromeDecoration::ChromeDecoration(PHLWINDOW window) : IHyprWindowDecoration(window) { ChromeDecoration::ChromeDecoration(PHLWINDOW window) : IHyprWindowDecoration(window) {
@@ -114,13 +305,14 @@ uint64_t ChromeDecoration::getDecorationFlags() { return DECORATION_PART_OF_MAIN
PHLWINDOW ChromeDecoration::GetOwner() { return windowRef.lock(); } 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) { SP<Render::ITexture> ChromeDecoration::GetBorderTexture(const Vector2D& sizePx, float extentPx, float chamferPx, const ChromeGradient& gradient, float titleBarHeightPx, float titleBarWidthPx, float glowPx, float glowStrength) {
if (sizePx.x < 1 || sizePx.y < 1) if (sizePx.x < 1 || sizePx.y < 1)
return nullptr; return nullptr;
if (cachedTexture && cachedTexture->ok() && cachedTexSize == sizePx && if (cachedTexture && cachedTexture->ok() && cachedTexSize == sizePx &&
cachedExtent == extentPx && cachedChamfer == chamferPx && cachedGradient == gradient && cachedExtent == extentPx && cachedChamfer == chamferPx && cachedGradient == gradient &&
cachedTitleBarHeight == titleBarHeightPx && cachedTitleBarWidth == titleBarWidthPx) cachedTitleBarHeight == titleBarHeightPx && cachedTitleBarWidth == titleBarWidthPx &&
cachedGlowSize == glowPx && cachedGlowStrength == glowStrength)
return cachedTexture; return cachedTexture;
const int w = static_cast<int>(sizePx.x); const int w = static_cast<int>(sizePx.x);
@@ -191,31 +383,7 @@ SP<Render::ITexture> ChromeDecoration::GetBorderTexture(const Vector2D& sizePx,
// inner boundary (window edge), inset by extent, chamfered - punches the // inner boundary (window edge), inset by extent, chamfered - punches the
// hole out of the outer path via even-odd fill, leaving just the ring. // hole out of the outer path via even-odd fill, leaving just the ring.
if (geo.innerW > 0 && geo.innerH > 0) { AppendChamferedRect(cr, geo.innerX0, geo.innerY0, geo.innerX1, geo.innerY1, geo.innerChamfer);
cairo_move_to(cr, geo.innerX0 + geo.innerChamfer, geo.innerY0);
/* *** Top Edge *** */
cairo_line_to(cr, geo.innerX1 - geo.innerChamfer, geo.innerY0);
cairo_line_to(cr, geo.innerX1, geo.innerY0 + geo.innerChamfer);
/* *** Right Edge *** */
cairo_line_to(cr, geo.innerX1, geo.innerY1 - geo.innerChamfer);
cairo_line_to(cr, geo.innerX1 - geo.innerChamfer, geo.innerY1);
/* *** Bottom Edge *** */
cairo_line_to(cr, geo.innerX0 + geo.innerChamfer, geo.innerY1);
cairo_line_to(cr, geo.innerX0, geo.innerY1 - geo.innerChamfer);
/* *** Left Edge *** */
cairo_line_to(cr, geo.innerX0, geo.innerY0 + geo.innerChamfer);
cairo_close_path(cr);
}
// The gradient's axis spans the whole texture (frame + title bar), not just // The gradient's axis spans the whole texture (frame + title bar), not just
// the ring, so opposite sides of the frame land at opposite ends of it the // the ring, so opposite sides of the frame land at opposite ends of it the
@@ -225,6 +393,9 @@ SP<Render::ITexture> ChromeDecoration::GetBorderTexture(const Vector2D& sizePx,
cairo_fill(cr); cairo_fill(cr);
cairo_pattern_destroy(pattern); cairo_pattern_destroy(pattern);
// Drawn after the ring, into the hole the fill above just left behind.
DrawInwardGlow(cr, geo, gradient, glowPx, glowStrength, w, h);
cairo_surface_flush(surface); cairo_surface_flush(surface);
cachedTexture = g_pHyprRenderer->createTexture(surface); cachedTexture = g_pHyprRenderer->createTexture(surface);
@@ -234,6 +405,8 @@ SP<Render::ITexture> ChromeDecoration::GetBorderTexture(const Vector2D& sizePx,
cachedGradient = gradient; cachedGradient = gradient;
cachedTitleBarHeight = titleBarHeightPx; cachedTitleBarHeight = titleBarHeightPx;
cachedTitleBarWidth = titleBarWidthPx; cachedTitleBarWidth = titleBarWidthPx;
cachedGlowSize = glowPx;
cachedGlowStrength = glowStrength;
cairo_destroy(cr); cairo_destroy(cr);
cairo_surface_destroy(surface); cairo_surface_destroy(surface);
+8 -3
View File
@@ -44,9 +44,12 @@ public:
// `titleBarWidthPx` (already measured/clamped by the caller - see // `titleBarWidthPx` (already measured/clamped by the caller - see
// ChromeDecorationGeometry) hanging off the floating top-border // ChromeDecorationGeometry) hanging off the floating top-border
// cutout. Filled with `gradient` swept across the whole texture along that // cutout. Filled with `gradient` swept across the whole texture along that
// gradient's own axis (a single-stop gradient is just a flat fill). Cached // gradient's own axis (a single-stop gradient is just a flat fill), plus -
// and only regenerated when any of these change from the last call. // if `glowPx` is non-zero - that same gradient bled `glowPx` inward past
SP<Render::ITexture> GetBorderTexture(const Vector2D& sizePx, float extentPx, float chamferPx, const ChromeGradient& gradient, float titleBarHeightPx, float titleBarWidthPx); // 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);
// Returns a texture of `title` rendered via Hyprland's own text renderer // Returns a texture of `title` rendered via Hyprland's own text renderer
// (Pango, through IHyprRenderer::renderText) at `textSizePx` using // (Pango, through IHyprRenderer::renderText) at `textSizePx` using
@@ -70,6 +73,8 @@ private:
ChromeGradient cachedGradient; ChromeGradient cachedGradient;
float cachedTitleBarHeight = -1.F; float cachedTitleBarHeight = -1.F;
float cachedTitleBarWidth = -1.F; float cachedTitleBarWidth = -1.F;
float cachedGlowSize = -1.F;
float cachedGlowStrength = -1.F;
SP<Render::ITexture> cachedTitleTex; SP<Render::ITexture> cachedTitleTex;
std::string cachedTitleTexTitle; std::string cachedTitleTexTitle;
+3 -1
View File
@@ -44,6 +44,8 @@ std::vector<UP<IPassElement>> ChromePassElement::draw() {
? hyprlandBorder ? hyprlandBorder
: (isActive ? PluginState->config.activeColor->value() : PluginState->config.inactiveColor->value())); : (isActive ? PluginState->config.activeColor->value() : PluginState->config.inactiveColor->value()));
const float borderAlpha = gradient.FirstAlpha(); const float borderAlpha = gradient.FirstAlpha();
const float glowPx = static_cast<float>(PluginState->config.glowSize->value()) * monitor->m_scale;
const float glowStrength = static_cast<float>(PluginState->config.glowStrength->value());
const float titleBarHeightPx = static_cast<float>(PluginState->config.titlebarHeight->value()) * monitor->m_scale; const float titleBarHeightPx = static_cast<float>(PluginState->config.titlebarHeight->value()) * monitor->m_scale;
const float textSizePx = static_cast<float>(PluginState->config.titlebarTextSize->value()) * monitor->m_scale; const float textSizePx = static_cast<float>(PluginState->config.titlebarTextSize->value()) * monitor->m_scale;
const std::string fontFamily = PluginState->config.titlebarFont->value(); const std::string fontFamily = PluginState->config.titlebarFont->value();
@@ -73,7 +75,7 @@ std::vector<UP<IPassElement>> ChromePassElement::draw() {
// with title length - otherwise every title change would needlessly // with title length - otherwise every title change would needlessly
// regenerate an identical border texture. // regenerate an identical border texture.
const float borderTitleBarWidthPx = geo.fullSpan ? geo.MaxTitleBarWidth() : geo.titleBarWidth; const float borderTitleBarWidthPx = geo.fullSpan ? geo.MaxTitleBarWidth() : geo.titleBarWidth;
const auto tex = data.decoration->GetBorderTexture({box.w, box.h}, extentPx, chamferPx, gradient, titleBarHeightPx, borderTitleBarWidthPx); const auto tex = data.decoration->GetBorderTexture({box.w, box.h}, extentPx, chamferPx, gradient, titleBarHeightPx, borderTitleBarWidthPx, glowPx, glowStrength);
if (!tex || !tex->ok()) if (!tex || !tex->ok())
return {}; return {};
+8
View File
@@ -4,12 +4,14 @@
#include <hyprland/src/config/values/types/BoolValue.hpp> #include <hyprland/src/config/values/types/BoolValue.hpp>
#include <hyprland/src/config/values/types/ColorValue.hpp> #include <hyprland/src/config/values/types/ColorValue.hpp>
#include <hyprland/src/config/values/types/FloatValue.hpp>
#include <hyprland/src/config/values/types/GradientValue.hpp> #include <hyprland/src/config/values/types/GradientValue.hpp>
#include <hyprland/src/config/values/types/IntValue.hpp> #include <hyprland/src/config/values/types/IntValue.hpp>
#include <hyprland/src/config/values/types/StringValue.hpp> #include <hyprland/src/config/values/types/StringValue.hpp>
#include <hyprland/src/plugins/PluginAPI.hpp> #include <hyprland/src/plugins/PluginAPI.hpp>
using IntValue = Config::Values::CIntValue; using IntValue = Config::Values::CIntValue;
using FloatValue = Config::Values::CFloatValue;
using BoolValue = Config::Values::CBoolValue; using BoolValue = Config::Values::CBoolValue;
using ColorValue = Config::Values::CColorValue; using ColorValue = Config::Values::CColorValue;
// Accepts the same syntax as Hyprland's own general:col.* values - a single // Accepts the same syntax as Hyprland's own general:col.* values - a single
@@ -17,6 +19,12 @@ using ColorValue = Config::Values::CColorValue;
using GradientValue = Config::Values::CGradientValue; using GradientValue = Config::Values::CGradientValue;
using StringValue = Config::Values::CStringValue; using StringValue = Config::Values::CStringValue;
// The trailing options argument of the *Value constructors (min/max/...).
// makeShared forwards its arguments through a template parameter, which a
// bare braced-init-list can't be deduced from - these have to be named.
using IntValueOptions = Config::Values::SIntValueOptions;
using FloatValueOptions = Config::Values::SFloatValueOptions;
// Hyprland's raw color ints (and CHyprColor(uint64_t)) are AARRGGBB, but // Hyprland's raw color ints (and CHyprColor(uint64_t)) are AARRGGBB, but
// literals are easier to read/write as the more common RRGGBBAA. Convert so // literals are easier to read/write as the more common RRGGBBAA. Convert so
// config defaults can be written in that order. // config defaults can be written in that order.
+12
View File
@@ -33,6 +33,16 @@ struct GlobalState {
"plugin:hyprchrome:inactive_color", "plugin:hyprchrome:inactive_color",
"Color (or gradient) of the border on unfocused windows, used when follow_hyprland_border_color is false", "Color (or gradient) of the border on unfocused windows, used when follow_hyprland_border_color is false",
CHyprColor{RGBAToARGB(0x6C6C6CFF)}), CHyprColor{RGBAToARGB(0x6C6C6CFF)}),
.glowSize = makeShared<IntValue>(
"plugin:hyprchrome:glow_size",
"How far the border's glow bleeds inward over the window, in pixels (0 disables it)",
0,
IntValueOptions{.min = 0}),
.glowStrength = makeShared<FloatValue>(
"plugin:hyprchrome:glow_strength",
"Peak opacity of the inward glow at the window edge, 0-1",
0.35F,
FloatValueOptions{.min = 0.F, .max = 1.F}),
.titlebarHeight = makeShared<IntValue>( .titlebarHeight = makeShared<IntValue>(
"plugin:hyprchrome:titlebar_height", "plugin:hyprchrome:titlebar_height",
"Height of the title bar hanging off the floating top-border cutout, in pixels", "Height of the title bar hanging off the floating top-border cutout, in pixels",
@@ -52,6 +62,8 @@ struct GlobalState {
HyprlandAPI::addConfigValueV2(plugin, config.followHyprlandBorderColor); HyprlandAPI::addConfigValueV2(plugin, config.followHyprlandBorderColor);
HyprlandAPI::addConfigValueV2(plugin, config.activeColor); HyprlandAPI::addConfigValueV2(plugin, config.activeColor);
HyprlandAPI::addConfigValueV2(plugin, config.inactiveColor); HyprlandAPI::addConfigValueV2(plugin, config.inactiveColor);
HyprlandAPI::addConfigValueV2(plugin, config.glowSize);
HyprlandAPI::addConfigValueV2(plugin, config.glowStrength);
HyprlandAPI::addConfigValueV2(plugin, config.titlebarHeight); HyprlandAPI::addConfigValueV2(plugin, config.titlebarHeight);
HyprlandAPI::addConfigValueV2(plugin, config.titlebarTextSize); HyprlandAPI::addConfigValueV2(plugin, config.titlebarTextSize);
HyprlandAPI::addConfigValueV2(plugin, config.titlebarFont); HyprlandAPI::addConfigValueV2(plugin, config.titlebarFont);