diff --git a/CLAUDE.md b/CLAUDE.md index b7408c8..8c0cfb4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -56,7 +56,7 @@ Config values live under `plugin:hyprchrome:*` and are declared in `src/GlobalSt - `enabled` (bool) - `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` -- `active_color` / `inactive_color` (ARGB) — 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 - `titlebar_height` (int, px) - `titlebar_text_size` (int, px) - `titlebar_font` (string, passed to `cairo_select_font_face`) @@ -80,12 +80,14 @@ Per-window decoration (`src/ChromeDecoration.hpp/.cpp`): implements `IHyprWindow 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. -- When `follow_hyprland_border_color` is true, derives the fill color from the window's *own* resolved/animated `m_realBorderColor` (matching Hyprland's `general:col.active_border`/`col.inactive_border`, already focus-aware); otherwise (or if that gradient is empty) uses the plugin's own `active_color`/`inactive_color` config, picked by `g_pCompositor->isWindowActive(window)`. Gradients are collapsed to their first stop since the ring is a flat cairo fill, not a shader. +- When `follow_hyprland_border_color` is true, derives the fill from the window's *own* resolved/animated `m_realBorderColor` (matching Hyprland's `general:col.active_border`/`col.inactive_border`, already focus-aware); otherwise (or if that gradient is empty) uses the plugin's own `active_color`/`inactive_color` config, picked by `g_pCompositor->isWindowActive(window)`. Either way it's snapshotted into a `ChromeGradient` (`src/ChromeGradient.hpp`) — stops + angle — and filled as a cairo linear gradient across the whole texture. Note the cross-focus *fade* (`m_realBorderColorPrevious` + `m_borderFadeAnimationProgress`, which Hyprland's shader lerps between two gradients) is not reproduced; the frame snaps to the new gradient. - Measures the window title (via `GetTitleTexture`, which itself goes through Hyprland's own Pango-based `IHyprRenderer::renderText`) to size the title bar plateau to fit the text (clamped between `MinTitleBarWidth`/`MaxTitleBarWidth`), then requests the border texture at that plateau width. - Emits the border texture plus (if there's a title) the title texture as child `CTexPassElement`s. Geometry math (`src/ChromeDecorationGeometry.hpp`): a pure, header-only value type (`ChromeDecorationGeometry`) that computes every coordinate used by the cairo path in `GetBorderTexture` — outer/inner chamfered boundaries, the title bar plateau's flat run and "dip" back down to the normal top edge, the left/right edge inset notch, etc. `ComputeBase(...)` computes everything except the title bar's own width (which depends on measured text and is filled in afterward via `WithTitleBarWidth(...)`). This is the file to read/edit when changing the border's shape — `ChromeDecoration::GetBorderTexture` just walks cairo path commands using these precomputed coordinates. +Gradient handling (`src/ChromeGradient.hpp`): a pure, header-only snapshot of a border gradient (stops + angle) with two things the cairo fill needs. `SampleAt()` interpolates in **OkLab**, because that's where Hyprland's border shader interpolates (it uploads `m_colorsOkLabA`) — `GetBorderTexture` subdivides each segment into `kStopsPerSegment` sampled cairo stops so cairo's own sRGB lerp between them tracks that curve. `AxisFor()` converts the angle into a cairo linear-gradient axis, reproducing the shader's quadrant-folding formula (`progress = y·sin(a) + x·(1-sin(a))`, which is *not* a true rotation) rather than a rotated axis, so the frame's sweep stays in step with the window border it wraps. + Below `kFullSpanThresholdPx` (250px window width), `fullSpan` mode kicks in: there isn't room for the plateau + its dip back to the normal edge height, so the entire top edge stays flat at the reserved title-bar height instead. This is why some cairo path logic in `GetBorderTexture` branches on `geo.fullSpan`. `Common.hpp` provides the `Config::Values::C*Value` type aliases (`IntValue`/`BoolValue`/`ColorValue`/`StringValue`) and `RGBAToARGB()` — Hyprland's raw color ints are AARRGGBB, but config default literals are written in the more familiar RRGGBBAA and converted at compile time. diff --git a/README.md b/README.md index 624c7ff..8cc9d19 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ hanging off the top-left corner. The frame's corners are chamfered by an amount inferred from the window's own border rounding, so the border peeking out around a rounded window's corners reads as a matching diagonal cut rather than a hard rectangular corner. Border color tracks Hyprland's own -active/inactive border color live. +active/inactive border color live, gradients included. ## Config @@ -16,8 +16,8 @@ plugin { enabled = true # bool extent = 12 # int, px the border extends past the window edge follow_hyprland_border_color = true # bool, use Hyprland's own active/inactive border color instead of active_color/inactive_color below - active_color = 0xFFD063FF # AARRGGBB, focused-window border color, used when follow_hyprland_border_color is false - inactive_color = 0xFF6C6C6C # AARRGGBB, unfocused-window border color, used when follow_hyprland_border_color is false + active_color = 0xFFD063FF # focused-window border color, used when follow_hyprland_border_color is false + inactive_color = 0xFF6C6C6C # unfocused-window border color, used when follow_hyprland_border_color is false titlebar_height = 8 # int, px height of the floating title bar titlebar_text_size = 12 # int, px font size of the title bar's text titlebar_font = sans-serif # string, passed to cairo_select_font_face @@ -25,6 +25,14 @@ plugin { } ``` +`active_color`/`inactive_color` take the same syntax as Hyprland's own +`general:col.active_border` — a single color, or several plus an optional +angle for a gradient: + +``` +active_color = rgba(ff0000ff) rgba(00ff00ff) 45deg +``` + ## Installing ```sh diff --git a/src/ChromeConfig.hpp b/src/ChromeConfig.hpp index f66bb6a..6b32930 100644 --- a/src/ChromeConfig.hpp +++ b/src/ChromeConfig.hpp @@ -13,8 +13,10 @@ struct ChromeConfig { // inactiveColor below. SP followHyprlandBorderColor; - SP activeColor; - SP inactiveColor; + // Gradients (multiple stops + an optional angle), same syntax as + // Hyprland's general:col.active_border/col.inactive_border. + SP activeColor; + SP inactiveColor; // Height of the title bar hanging off the floating top-border cutout, in // logical pixels. diff --git a/src/ChromeDecoration.cpp b/src/ChromeDecoration.cpp index ebcab94..7e67b60 100644 --- a/src/ChromeDecoration.cpp +++ b/src/ChromeDecoration.cpp @@ -27,6 +27,29 @@ std::vector Utf8CodepointStarts(const std::string& s) { starts.push_back(s.size()); return starts; } + +// cairo interpolates between color stops linearly in (premultiplied) sRGB, +// Hyprland's border shader interpolates in OkLab - so rather than handing +// cairo the gradient's own stops, each segment between them is subdivided +// into this many sampled sub-stops. cairo's straight lines between those then +// track the OkLab curve closely enough to be indistinguishable from the +// compositor's own border, and it only costs anything on a cache miss. +constexpr int kStopsPerSegment = 8; + +cairo_pattern_t* CreateGradientPattern(const ChromeGradient& gradient, double w, double h) { + const auto axis = gradient.AxisFor(w, h); + const auto pattern = cairo_pattern_create_linear(axis.x0, axis.y0, axis.x1, axis.y1); + + // A single stop still needs two identical cairo stops to fill at all. + const int steps = std::max(1, (static_cast(gradient.colors.size()) - 1) * kStopsPerSegment); + for (int i = 0; i <= steps; ++i) { + const double t = static_cast(i) / steps; + const auto color = gradient.SampleAt(static_cast(t)); + cairo_pattern_add_color_stop_rgba(pattern, t, color.r, color.g, color.b, color.a); + } + + return pattern; +} } ChromeDecoration::ChromeDecoration(PHLWINDOW window) : IHyprWindowDecoration(window) { @@ -91,12 +114,12 @@ uint64_t ChromeDecoration::getDecorationFlags() { return DECORATION_PART_OF_MAIN PHLWINDOW ChromeDecoration::GetOwner() { return windowRef.lock(); } -SP ChromeDecoration::GetBorderTexture(const Vector2D& sizePx, float extentPx, float chamferPx, uint64_t colorValue, float titleBarHeightPx, float titleBarWidthPx) { +SP ChromeDecoration::GetBorderTexture(const Vector2D& sizePx, float extentPx, float chamferPx, const ChromeGradient& gradient, float titleBarHeightPx, float titleBarWidthPx) { if (sizePx.x < 1 || sizePx.y < 1) return nullptr; if (cachedTexture && cachedTexture->ok() && cachedTexSize == sizePx && - cachedExtent == extentPx && cachedChamfer == chamferPx && cachedColorValue == colorValue && + cachedExtent == extentPx && cachedChamfer == chamferPx && cachedGradient == gradient && cachedTitleBarHeight == titleBarHeightPx && cachedTitleBarWidth == titleBarWidthPx) return cachedTexture; @@ -116,8 +139,6 @@ SP ChromeDecoration::GetBorderTexture(const Vector2D& sizePx, cairo_paint(cr); cairo_restore(cr); - const CHyprColor color{colorValue}; - cairo_set_fill_rule(cr, CAIRO_FILL_RULE_EVEN_ODD); // outer boundary, chamfered - the ring itself starts at geo.topPad, not 0, @@ -196,8 +217,13 @@ SP ChromeDecoration::GetBorderTexture(const Vector2D& sizePx, cairo_close_path(cr); } - cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a); + // 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 + // way Hyprland's own border does. + const auto pattern = CreateGradientPattern(gradient, w, h); + cairo_set_source(cr, pattern); cairo_fill(cr); + cairo_pattern_destroy(pattern); cairo_surface_flush(surface); @@ -205,7 +231,7 @@ SP ChromeDecoration::GetBorderTexture(const Vector2D& sizePx, cachedTexSize = sizePx; cachedExtent = extentPx; cachedChamfer = chamferPx; - cachedColorValue = colorValue; + cachedGradient = gradient; cachedTitleBarHeight = titleBarHeightPx; cachedTitleBarWidth = titleBarWidthPx; @@ -215,18 +241,16 @@ SP ChromeDecoration::GetBorderTexture(const Vector2D& sizePx, return cachedTexture; } -SP ChromeDecoration::GetTitleTexture(const std::string& title, float textSizePx, uint64_t colorValue, const std::string& fontFamily, int maxWidthPx, bool isActive) { +SP ChromeDecoration::GetTitleTexture(const std::string& title, float textSizePx, float alpha, const std::string& fontFamily, int maxWidthPx, bool isActive) { if (title.empty()) return nullptr; if (cachedTitleTex && cachedTitleTex->ok() && cachedTitleTexTitle == title && - cachedTitleTexSize == textSizePx && cachedTitleTexColor == colorValue && + cachedTitleTexSize == textSizePx && cachedTitleTexAlpha == alpha && cachedTitleTexFont == fontFamily && cachedTitleTexMaxWidth == maxWidthPx && cachedTitleTexActive == isActive) return cachedTitleTex; - const CHyprColor borderColor{colorValue}; - const float alpha = static_cast(borderColor.a); const CHyprColor textColor = isActive ? CHyprColor{0.F, 0.F, 0.F, alpha} : CHyprColor{0xEE / 255.F, 0xEE / 255.F, 0xEE / 255.F, alpha}; @@ -262,7 +286,7 @@ SP ChromeDecoration::GetTitleTexture(const std::string& title, cachedTitleTex = tex; cachedTitleTexTitle = title; cachedTitleTexSize = textSizePx; - cachedTitleTexColor = colorValue; + cachedTitleTexAlpha = alpha; cachedTitleTexFont = fontFamily; cachedTitleTexMaxWidth = maxWidthPx; cachedTitleTexActive = isActive; diff --git a/src/ChromeDecoration.hpp b/src/ChromeDecoration.hpp index 74368ad..39d1236 100644 --- a/src/ChromeDecoration.hpp +++ b/src/ChromeDecoration.hpp @@ -3,6 +3,7 @@ #define WLR_USE_UNSTABLE #include "Globals.hpp" +#include "ChromeGradient.hpp" #include #include #include @@ -42,17 +43,19 @@ public: // by `chamferPx`, plus a title bar of height `titleBarHeightPx` and width // `titleBarWidthPx` (already measured/clamped by the caller - see // ChromeDecorationGeometry) hanging off the floating top-border - // cutout. Cached and only regenerated when any of these change from the - // last call. - SP GetBorderTexture(const Vector2D& sizePx, float extentPx, float chamferPx, uint64_t colorValue, float titleBarHeightPx, float titleBarWidthPx); + // 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 + // and only regenerated when any of these change from the last call. + SP GetBorderTexture(const Vector2D& sizePx, float extentPx, float chamferPx, const ChromeGradient& gradient, float titleBarHeightPx, float titleBarWidthPx); // Returns a texture of `title` rendered via Hyprland's own text renderer // (Pango, through IHyprRenderer::renderText) at `textSizePx` using // `fontFamily`, truncated with a trailing "…" if it doesn't fit within - // `maxWidthPx` - black while `isActive`, 0xEEEEEEFF otherwise, both at the - // border color's own alpha. Cached and only regenerated when any of these - // change from the last call. Returns nullptr if `title` is empty. - SP GetTitleTexture(const std::string& title, float textSizePx, uint64_t colorValue, const std::string& fontFamily, int maxWidthPx, bool isActive); + // `maxWidthPx` - black while `isActive`, 0xEEEEEE otherwise, both at + // `alpha` (the border's own alpha, so translucent borders keep translucent + // text). Cached and only regenerated when any of these change from the last + // call. Returns nullptr if `title` is empty. + SP GetTitleTexture(const std::string& title, float textSizePx, float alpha, const std::string& fontFamily, int maxWidthPx, bool isActive); WP self; @@ -64,14 +67,14 @@ private: Vector2D cachedTexSize = {-1, -1}; float cachedExtent = -1.F; float cachedChamfer = -1.F; - uint64_t cachedColorValue = 0; + ChromeGradient cachedGradient; float cachedTitleBarHeight = -1.F; float cachedTitleBarWidth = -1.F; SP cachedTitleTex; std::string cachedTitleTexTitle; float cachedTitleTexSize = -1.F; - uint64_t cachedTitleTexColor = 0; + float cachedTitleTexAlpha = -1.F; std::string cachedTitleTexFont; int cachedTitleTexMaxWidth = -1; bool cachedTitleTexActive = false; diff --git a/src/ChromeGradient.hpp b/src/ChromeGradient.hpp new file mode 100644 index 0000000..f1f7a85 --- /dev/null +++ b/src/ChromeGradient.hpp @@ -0,0 +1,128 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include + +// A snapshot of a border gradient: its color stops (evenly spaced along the +// gradient's axis) plus that axis' angle in radians - the same shape as +// Hyprland's own Config::CGradientValueData, copied out of the compositor's +// live (animated) gradient so it can be compared against the parameters the +// cached border texture was rendered with. +// +// A single-stop gradient is just a flat color, which is what both Hyprland's +// shader and the cairo path below degrade to. +struct ChromeGradient { + std::vector colors; + float angle = 0.F; + + bool empty() const { return colors.empty(); } + + bool operator==(const ChromeGradient& other) const { + return angle == other.angle && colors == other.colors; + } + + static ChromeGradient From(const Config::CGradientValueData& data) { + return {.colors = data.m_colors, .angle = data.m_angle}; + } + + // Alpha of the first stop, used for content drawn *with* the border (the + // title text) so it fades along with a translucent border color. + float FirstAlpha() const { return colors.empty() ? 1.F : static_cast(colors.front().a); } + + // Color at `t` (0..1) along the axis. Hyprland uploads its stops to the + // border shader already converted to OkLab and interpolates there, so + // interpolating in sRGB instead would visibly diverge (muddy midpoints) on + // anything but near-identical stops. + CHyprColor SampleAt(float t) const { + if (colors.empty()) + return CHyprColor{0.F, 0.F, 0.F, 0.F}; + if (colors.size() == 1) + return colors.front(); + + const float progress = std::clamp(t, 0.F, 1.F) * static_cast(colors.size() - 1); + const size_t lower = std::min(static_cast(std::floor(progress)), colors.size() - 2); + const float frac = progress - static_cast(lower); + + const auto a = colors[lower].asOkLab(); + const auto b = colors[lower + 1].asOkLab(); + const Hyprgraphics::CColor::SOkLab mixed{ + .l = std::lerp(a.l, b.l, frac), + .a = std::lerp(a.a, b.a, frac), + .b = std::lerp(a.b, b.b, frac), + }; + return CHyprColor{Hyprgraphics::CColor{mixed}, static_cast(std::lerp(colors[lower].a, colors[lower + 1].a, frac))}; + } + + // Endpoints of the axis along which `t` runs 0 -> 1, in pixels within a + // `width` x `height` texture. + struct SAxis { + double x0 = 0, y0 = 0, x1 = 0, y1 = 0; + }; + + // Hyprland's border shader doesn't rotate its gradient axis; it folds the + // angle into the first quadrant (mirroring the coordinate instead) and then + // lerps between a purely horizontal and a purely vertical sweep by sin() of + // the folded angle: + // + // progress = y * sin(a) + x * (1 - sin(a)) (x, y normalized 0..1) + // + // So 0deg sweeps left->right, 90deg top->bottom, and 45deg reaches the + // opposite corner - but the in-between angles are *not* a true rotation. + // Reproduce that function here rather than a rotated axis, so the frame's + // gradient stays in step with the window border it wraps. (The shader folds + // on literal 1.57/3.14/4.71 where this uses exact pi; that costs at most + // ~0.3% of the sweep near those boundaries, which isn't visible.) + // + // `progress` is affine in (x, y), so it maps onto a cairo linear gradient + // exactly: for progress = g . P + c (with g the per-pixel gradient vector), + // cairo's own t = (P - P0) . d / |d|^2 matches when d = g / |g|^2 and + // P0 = -c * d. + SAxis AxisFor(double width, double height) const { + static constexpr double TAU = 2.0 * std::numbers::pi; + + double ang = std::fmod(static_cast(angle), TAU); + if (ang < 0) + ang += TAU; + + bool flipX = false, flipY = false; + double folded = ang; + if (ang > 1.5 * std::numbers::pi) { + flipY = true; + folded = TAU - ang; + } else if (ang > std::numbers::pi) { + flipX = flipY = true; + folded = ang - std::numbers::pi; + } else if (ang > 0.5 * std::numbers::pi) { + flipX = true; + folded = std::numbers::pi - ang; + } + + const double sine = std::sin(folded); + + // progress = xWeight * x + yWeight * y + offset, in normalized coords. + double xWeight = 1.0 - sine, yWeight = sine, offset = 0.0; + if (flipX) { + offset += xWeight; + xWeight = -xWeight; + } + if (flipY) { + offset += yWeight; + yWeight = -yWeight; + } + + const double gx = xWeight / std::max(width, 1.0); + const double gy = yWeight / std::max(height, 1.0); + const double gLenSq = gx * gx + gy * gy; + if (gLenSq <= 0) + return {.x0 = 0, .y0 = 0, .x1 = std::max(width, 1.0), .y1 = 0}; + + const double dx = gx / gLenSq, dy = gy / gLenSq; + return {.x0 = -offset * dx, .y0 = -offset * dy, .x1 = -offset * dx + dx, .y1 = -offset * dy + dy}; + } +}; diff --git a/src/ChromePassElement.cpp b/src/ChromePassElement.cpp index 7127a45..4544599 100644 --- a/src/ChromePassElement.cpp +++ b/src/ChromePassElement.cpp @@ -34,15 +34,16 @@ std::vector> ChromePassElement::draw() { const bool isActive = g_pCompositor->isWindowActive(window); // When follow_hyprland_border_color is set, track Hyprland's own // general:col.active_border / col.inactive_border (already resolved per - // focus state and animated by the compositor) - gradients are collapsed - // to their first stop, since the ring is a flat fill, not a shader. + // focus state and animated by the compositor), stops and angle included. // Otherwise (or if Hyprland reports no border color at all) fall back to - // our own active_color/inactive_color config. - const auto& borderGradient = window->m_realBorderColor; - const bool followHyprlandColor = PluginState->config.followHyprlandBorderColor->value() && !borderGradient.m_colors.empty(); - const uint64_t colorValue = followHyprlandColor - ? static_cast(borderGradient.m_colors.front().getAsHex()) - : static_cast(isActive ? PluginState->config.activeColor->value() : PluginState->config.inactiveColor->value()); + // our own active_color/inactive_color config, which take the same + // gradient syntax. + const auto& hyprlandBorder = window->m_realBorderColor; + const bool followHyprlandColor = PluginState->config.followHyprlandBorderColor->value() && !hyprlandBorder.m_colors.empty(); + const auto gradient = ChromeGradient::From(followHyprlandColor + ? hyprlandBorder + : (isActive ? PluginState->config.activeColor->value() : PluginState->config.inactiveColor->value())); + const float borderAlpha = gradient.FirstAlpha(); const float titleBarHeightPx = static_cast(PluginState->config.titlebarHeight->value()) * monitor->m_scale; const float textSizePx = static_cast(PluginState->config.titlebarTextSize->value()) * monitor->m_scale; const std::string fontFamily = PluginState->config.titlebarFont->value(); @@ -58,7 +59,7 @@ std::vector> ChromePassElement::draw() { const int measureMaxWidthPx = static_cast(baseGeo.MaxTitleBarWidth() - baseGeo.barX0 - titlePadding * 2.F); const auto titleTex = measureMaxWidthPx > 0 - ? data.decoration->GetTitleTexture(window->m_title, textSizePx, colorValue, fontFamily, measureMaxWidthPx, isActive) + ? data.decoration->GetTitleTexture(window->m_title, textSizePx, borderAlpha, fontFamily, measureMaxWidthPx, isActive) : nullptr; const float texW = (titleTex && titleTex->ok()) ? static_cast(titleTex->m_size.x) : 0.F; @@ -72,7 +73,7 @@ std::vector> 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, colorValue, titleBarHeightPx, borderTitleBarWidthPx); + const auto tex = data.decoration->GetBorderTexture({box.w, box.h}, extentPx, chamferPx, gradient, titleBarHeightPx, borderTitleBarWidthPx); if (!tex || !tex->ok()) return {}; diff --git a/src/Common.hpp b/src/Common.hpp index 2e1f41a..83ee176 100644 --- a/src/Common.hpp +++ b/src/Common.hpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -11,6 +12,9 @@ using IntValue = Config::Values::CIntValue; using BoolValue = Config::Values::CBoolValue; using ColorValue = Config::Values::CColorValue; +// Accepts the same syntax as Hyprland's own general:col.* values - a single +// color, or several plus an optional trailing angle ("... 45deg"). +using GradientValue = Config::Values::CGradientValue; using StringValue = Config::Values::CStringValue; // Hyprland's raw color ints (and CHyprColor(uint64_t)) are AARRGGBB, but diff --git a/src/GlobalState.hpp b/src/GlobalState.hpp index c3b76c4..a137935 100644 --- a/src/GlobalState.hpp +++ b/src/GlobalState.hpp @@ -25,14 +25,14 @@ struct GlobalState { "plugin:hyprchrome:follow_hyprland_border_color", "Whether the border tracks Hyprland's own active/inactive border color instead of active_color/inactive_color", true), - .activeColor = makeShared( + .activeColor = makeShared( "plugin:hyprchrome:active_color", - "Color of the border on the focused window, used when follow_hyprland_border_color is false", - RGBAToARGB(0xFFD063FF)), - .inactiveColor = makeShared( + "Color (or gradient) of the border on the focused window, used when follow_hyprland_border_color is false", + CHyprColor{RGBAToARGB(0xFFD063FF)}), + .inactiveColor = makeShared( "plugin:hyprchrome:inactive_color", - "Color of the border on unfocused windows, used when follow_hyprland_border_color is false", - RGBAToARGB(0x6C6C6CFF)), + "Color (or gradient) of the border on unfocused windows, used when follow_hyprland_border_color is false", + CHyprColor{RGBAToARGB(0x6C6C6CFF)}), .titlebarHeight = makeShared( "plugin:hyprchrome:titlebar_height", "Height of the title bar hanging off the floating top-border cutout, in pixels",