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>
129 lines
4.8 KiB
C++
129 lines
4.8 KiB
C++
#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};
|
|
}
|
|
};
|