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:
+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;
|
||||
|
||||
Reference in New Issue
Block a user