#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}; } };