Render border gradients instead of a flat first stop
The frame collapsed Hyprland's border gradient to m_colors.front(), so a gradient col.active_border showed up as a single flat color. Snapshot the gradient (stops + angle) into a new header-only ChromeGradient and fill the cairo path with a linear gradient built from it: - AxisFor() reproduces the border shader's quadrant-folding progress function (y*sin(a) + x*(1-sin(a)), not a true rotation) as a cairo axis, so the frame's sweep stays in step with the window border it wraps. Checked against a port of the shader across all 360 degrees: cardinal angles exact, worst case 0.003 of the sweep (the shader folds on literal 1.57/3.14/4.71 where this uses pi). - SampleAt() interpolates in OkLab, where the shader interpolates, with each segment subdivided into sampled cairo stops so cairo's own sRGB lerp tracks that curve. active_color/inactive_color become gradient config values, taking the same syntax as general:col.active_border. Single-color configs are unaffected. The cross-focus fade (m_realBorderColorPrevious + fade progress, lerped between two gradients in-shader) is still not reproduced. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -13,8 +13,10 @@ struct ChromeConfig {
|
||||
// inactiveColor below.
|
||||
SP<BoolValue> followHyprlandBorderColor;
|
||||
|
||||
SP<ColorValue> activeColor;
|
||||
SP<ColorValue> inactiveColor;
|
||||
// Gradients (multiple stops + an optional angle), same syntax as
|
||||
// Hyprland's general:col.active_border/col.inactive_border.
|
||||
SP<GradientValue> activeColor;
|
||||
SP<GradientValue> inactiveColor;
|
||||
|
||||
// Height of the title bar hanging off the floating top-border cutout, in
|
||||
// logical pixels.
|
||||
|
||||
+35
-11
@@ -27,6 +27,29 @@ std::vector<size_t> 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<int>(gradient.colors.size()) - 1) * kStopsPerSegment);
|
||||
for (int i = 0; i <= steps; ++i) {
|
||||
const double t = static_cast<double>(i) / steps;
|
||||
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);
|
||||
}
|
||||
|
||||
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<Render::ITexture> ChromeDecoration::GetBorderTexture(const Vector2D& sizePx, float extentPx, float chamferPx, uint64_t colorValue, float titleBarHeightPx, float titleBarWidthPx) {
|
||||
SP<Render::ITexture> 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<Render::ITexture> 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<Render::ITexture> 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<Render::ITexture> ChromeDecoration::GetBorderTexture(const Vector2D& sizePx,
|
||||
cachedTexSize = sizePx;
|
||||
cachedExtent = extentPx;
|
||||
cachedChamfer = chamferPx;
|
||||
cachedColorValue = colorValue;
|
||||
cachedGradient = gradient;
|
||||
cachedTitleBarHeight = titleBarHeightPx;
|
||||
cachedTitleBarWidth = titleBarWidthPx;
|
||||
|
||||
@@ -215,18 +241,16 @@ SP<Render::ITexture> ChromeDecoration::GetBorderTexture(const Vector2D& sizePx,
|
||||
return cachedTexture;
|
||||
}
|
||||
|
||||
SP<Render::ITexture> ChromeDecoration::GetTitleTexture(const std::string& title, float textSizePx, uint64_t colorValue, const std::string& fontFamily, int maxWidthPx, bool isActive) {
|
||||
SP<Render::ITexture> 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<float>(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<Render::ITexture> ChromeDecoration::GetTitleTexture(const std::string& title,
|
||||
cachedTitleTex = tex;
|
||||
cachedTitleTexTitle = title;
|
||||
cachedTitleTexSize = textSizePx;
|
||||
cachedTitleTexColor = colorValue;
|
||||
cachedTitleTexAlpha = alpha;
|
||||
cachedTitleTexFont = fontFamily;
|
||||
cachedTitleTexMaxWidth = maxWidthPx;
|
||||
cachedTitleTexActive = isActive;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#define WLR_USE_UNSTABLE
|
||||
|
||||
#include "Globals.hpp"
|
||||
#include "ChromeGradient.hpp"
|
||||
#include <hyprland/src/desktop/DesktopTypes.hpp>
|
||||
#include <hyprland/src/render/decorations/IHyprWindowDecoration.hpp>
|
||||
#include <hyprutils/math/Box.hpp>
|
||||
@@ -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<Render::ITexture> 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<Render::ITexture> 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<Render::ITexture> 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<Render::ITexture> GetTitleTexture(const std::string& title, float textSizePx, float alpha, const std::string& fontFamily, int maxWidthPx, bool isActive);
|
||||
|
||||
WP<ChromeDecoration> 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<Render::ITexture> 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;
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
#pragma once
|
||||
|
||||
#include <hyprland/src/config/shared/complex/ComplexDataTypes.hpp>
|
||||
#include <hyprland/src/helpers/Color.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <numbers>
|
||||
#include <vector>
|
||||
|
||||
// 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<CHyprColor> 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<float>(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<float>(colors.size() - 1);
|
||||
const size_t lower = std::min(static_cast<size_t>(std::floor(progress)), colors.size() - 2);
|
||||
const float frac = progress - static_cast<float>(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<float>(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<double>(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};
|
||||
}
|
||||
};
|
||||
+11
-10
@@ -34,15 +34,16 @@ std::vector<UP<IPassElement>> 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<uint64_t>(borderGradient.m_colors.front().getAsHex())
|
||||
: static_cast<uint64_t>(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<float>(PluginState->config.titlebarHeight->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();
|
||||
@@ -58,7 +59,7 @@ std::vector<UP<IPassElement>> ChromePassElement::draw() {
|
||||
const int measureMaxWidthPx = static_cast<int>(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<float>(titleTex->m_size.x) : 0.F;
|
||||
|
||||
@@ -72,7 +73,7 @@ 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, colorValue, titleBarHeightPx, borderTitleBarWidthPx);
|
||||
const auto tex = data.decoration->GetBorderTexture({box.w, box.h}, extentPx, chamferPx, gradient, titleBarHeightPx, borderTitleBarWidthPx);
|
||||
if (!tex || !tex->ok())
|
||||
return {};
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <hyprland/src/config/values/types/BoolValue.hpp>
|
||||
#include <hyprland/src/config/values/types/ColorValue.hpp>
|
||||
#include <hyprland/src/config/values/types/GradientValue.hpp>
|
||||
#include <hyprland/src/config/values/types/IntValue.hpp>
|
||||
#include <hyprland/src/config/values/types/StringValue.hpp>
|
||||
#include <hyprland/src/plugins/PluginAPI.hpp>
|
||||
@@ -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
|
||||
|
||||
+6
-6
@@ -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<ColorValue>(
|
||||
.activeColor = makeShared<GradientValue>(
|
||||
"plugin:hyprchrome:active_color",
|
||||
"Color of the border on the focused window, used when follow_hyprland_border_color is false",
|
||||
RGBAToARGB(0xFFD063FF)),
|
||||
.inactiveColor = makeShared<ColorValue>(
|
||||
"Color (or gradient) of the border on the focused window, used when follow_hyprland_border_color is false",
|
||||
CHyprColor{RGBAToARGB(0xFFD063FF)}),
|
||||
.inactiveColor = makeShared<GradientValue>(
|
||||
"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<IntValue>(
|
||||
"plugin:hyprchrome:titlebar_height",
|
||||
"Height of the title bar hanging off the floating top-border cutout, in pixels",
|
||||
|
||||
Reference in New Issue
Block a user