hypr-chrome v0.1.2 #10
@@ -56,7 +56,7 @@ Config values live under `plugin:hyprchrome:*` and are declared in `src/GlobalSt
|
|||||||
- `enabled` (bool)
|
- `enabled` (bool)
|
||||||
- `extent` (int, px the border extends past the window edge)
|
- `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`
|
- `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_height` (int, px)
|
||||||
- `titlebar_text_size` (int, px)
|
- `titlebar_text_size` (int, px)
|
||||||
- `titlebar_font` (string, passed to `cairo_select_font_face`)
|
- `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:
|
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.
|
- 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.
|
- 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.
|
- 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.
|
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`.
|
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.
|
`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
|
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
|
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
|
than a hard rectangular corner. Border color tracks Hyprland's own
|
||||||
active/inactive border color live.
|
active/inactive border color live, gradients included.
|
||||||
|
|
||||||
## Config
|
## Config
|
||||||
|
|
||||||
@@ -16,8 +16,8 @@ plugin {
|
|||||||
enabled = true # bool
|
enabled = true # bool
|
||||||
extent = 12 # int, px the border extends past the window edge
|
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
|
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
|
active_color = 0xFFD063FF # 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
|
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_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_text_size = 12 # int, px font size of the title bar's text
|
||||||
titlebar_font = sans-serif # string, passed to cairo_select_font_face
|
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
|
## Installing
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
@@ -13,8 +13,10 @@ struct ChromeConfig {
|
|||||||
// inactiveColor below.
|
// inactiveColor below.
|
||||||
SP<BoolValue> followHyprlandBorderColor;
|
SP<BoolValue> followHyprlandBorderColor;
|
||||||
|
|
||||||
SP<ColorValue> activeColor;
|
// Gradients (multiple stops + an optional angle), same syntax as
|
||||||
SP<ColorValue> inactiveColor;
|
// 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
|
// Height of the title bar hanging off the floating top-border cutout, in
|
||||||
// logical pixels.
|
// logical pixels.
|
||||||
|
|||||||
+35
-11
@@ -27,6 +27,29 @@ std::vector<size_t> Utf8CodepointStarts(const std::string& s) {
|
|||||||
starts.push_back(s.size());
|
starts.push_back(s.size());
|
||||||
return starts;
|
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) {
|
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(); }
|
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)
|
if (sizePx.x < 1 || sizePx.y < 1)
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|
||||||
if (cachedTexture && cachedTexture->ok() && cachedTexSize == sizePx &&
|
if (cachedTexture && cachedTexture->ok() && cachedTexSize == sizePx &&
|
||||||
cachedExtent == extentPx && cachedChamfer == chamferPx && cachedColorValue == colorValue &&
|
cachedExtent == extentPx && cachedChamfer == chamferPx && cachedGradient == gradient &&
|
||||||
cachedTitleBarHeight == titleBarHeightPx && cachedTitleBarWidth == titleBarWidthPx)
|
cachedTitleBarHeight == titleBarHeightPx && cachedTitleBarWidth == titleBarWidthPx)
|
||||||
return cachedTexture;
|
return cachedTexture;
|
||||||
|
|
||||||
@@ -116,8 +139,6 @@ SP<Render::ITexture> ChromeDecoration::GetBorderTexture(const Vector2D& sizePx,
|
|||||||
cairo_paint(cr);
|
cairo_paint(cr);
|
||||||
cairo_restore(cr);
|
cairo_restore(cr);
|
||||||
|
|
||||||
const CHyprColor color{colorValue};
|
|
||||||
|
|
||||||
cairo_set_fill_rule(cr, CAIRO_FILL_RULE_EVEN_ODD);
|
cairo_set_fill_rule(cr, CAIRO_FILL_RULE_EVEN_ODD);
|
||||||
|
|
||||||
// outer boundary, chamfered - the ring itself starts at geo.topPad, not 0,
|
// 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_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_fill(cr);
|
||||||
|
cairo_pattern_destroy(pattern);
|
||||||
|
|
||||||
cairo_surface_flush(surface);
|
cairo_surface_flush(surface);
|
||||||
|
|
||||||
@@ -205,7 +231,7 @@ SP<Render::ITexture> ChromeDecoration::GetBorderTexture(const Vector2D& sizePx,
|
|||||||
cachedTexSize = sizePx;
|
cachedTexSize = sizePx;
|
||||||
cachedExtent = extentPx;
|
cachedExtent = extentPx;
|
||||||
cachedChamfer = chamferPx;
|
cachedChamfer = chamferPx;
|
||||||
cachedColorValue = colorValue;
|
cachedGradient = gradient;
|
||||||
cachedTitleBarHeight = titleBarHeightPx;
|
cachedTitleBarHeight = titleBarHeightPx;
|
||||||
cachedTitleBarWidth = titleBarWidthPx;
|
cachedTitleBarWidth = titleBarWidthPx;
|
||||||
|
|
||||||
@@ -215,18 +241,16 @@ SP<Render::ITexture> ChromeDecoration::GetBorderTexture(const Vector2D& sizePx,
|
|||||||
return cachedTexture;
|
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())
|
if (title.empty())
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|
||||||
if (cachedTitleTex && cachedTitleTex->ok() && cachedTitleTexTitle == title &&
|
if (cachedTitleTex && cachedTitleTex->ok() && cachedTitleTexTitle == title &&
|
||||||
cachedTitleTexSize == textSizePx && cachedTitleTexColor == colorValue &&
|
cachedTitleTexSize == textSizePx && cachedTitleTexAlpha == alpha &&
|
||||||
cachedTitleTexFont == fontFamily && cachedTitleTexMaxWidth == maxWidthPx &&
|
cachedTitleTexFont == fontFamily && cachedTitleTexMaxWidth == maxWidthPx &&
|
||||||
cachedTitleTexActive == isActive)
|
cachedTitleTexActive == isActive)
|
||||||
return cachedTitleTex;
|
return cachedTitleTex;
|
||||||
|
|
||||||
const CHyprColor borderColor{colorValue};
|
|
||||||
const float alpha = static_cast<float>(borderColor.a);
|
|
||||||
const CHyprColor textColor = isActive
|
const CHyprColor textColor = isActive
|
||||||
? CHyprColor{0.F, 0.F, 0.F, alpha}
|
? CHyprColor{0.F, 0.F, 0.F, alpha}
|
||||||
: CHyprColor{0xEE / 255.F, 0xEE / 255.F, 0xEE / 255.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;
|
cachedTitleTex = tex;
|
||||||
cachedTitleTexTitle = title;
|
cachedTitleTexTitle = title;
|
||||||
cachedTitleTexSize = textSizePx;
|
cachedTitleTexSize = textSizePx;
|
||||||
cachedTitleTexColor = colorValue;
|
cachedTitleTexAlpha = alpha;
|
||||||
cachedTitleTexFont = fontFamily;
|
cachedTitleTexFont = fontFamily;
|
||||||
cachedTitleTexMaxWidth = maxWidthPx;
|
cachedTitleTexMaxWidth = maxWidthPx;
|
||||||
cachedTitleTexActive = isActive;
|
cachedTitleTexActive = isActive;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#define WLR_USE_UNSTABLE
|
#define WLR_USE_UNSTABLE
|
||||||
|
|
||||||
#include "Globals.hpp"
|
#include "Globals.hpp"
|
||||||
|
#include "ChromeGradient.hpp"
|
||||||
#include <hyprland/src/desktop/DesktopTypes.hpp>
|
#include <hyprland/src/desktop/DesktopTypes.hpp>
|
||||||
#include <hyprland/src/render/decorations/IHyprWindowDecoration.hpp>
|
#include <hyprland/src/render/decorations/IHyprWindowDecoration.hpp>
|
||||||
#include <hyprutils/math/Box.hpp>
|
#include <hyprutils/math/Box.hpp>
|
||||||
@@ -42,17 +43,19 @@ public:
|
|||||||
// by `chamferPx`, plus a title bar of height `titleBarHeightPx` and width
|
// by `chamferPx`, plus a title bar of height `titleBarHeightPx` and width
|
||||||
// `titleBarWidthPx` (already measured/clamped by the caller - see
|
// `titleBarWidthPx` (already measured/clamped by the caller - see
|
||||||
// ChromeDecorationGeometry) hanging off the floating top-border
|
// ChromeDecorationGeometry) hanging off the floating top-border
|
||||||
// cutout. Cached and only regenerated when any of these change from the
|
// cutout. Filled with `gradient` swept across the whole texture along that
|
||||||
// last call.
|
// gradient's own axis (a single-stop gradient is just a flat fill). Cached
|
||||||
SP<Render::ITexture> GetBorderTexture(const Vector2D& sizePx, float extentPx, float chamferPx, uint64_t colorValue, float titleBarHeightPx, float titleBarWidthPx);
|
// 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
|
// Returns a texture of `title` rendered via Hyprland's own text renderer
|
||||||
// (Pango, through IHyprRenderer::renderText) at `textSizePx` using
|
// (Pango, through IHyprRenderer::renderText) at `textSizePx` using
|
||||||
// `fontFamily`, truncated with a trailing "…" if it doesn't fit within
|
// `fontFamily`, truncated with a trailing "…" if it doesn't fit within
|
||||||
// `maxWidthPx` - black while `isActive`, 0xEEEEEEFF otherwise, both at the
|
// `maxWidthPx` - black while `isActive`, 0xEEEEEE otherwise, both at
|
||||||
// border color's own alpha. Cached and only regenerated when any of these
|
// `alpha` (the border's own alpha, so translucent borders keep translucent
|
||||||
// change from the last call. Returns nullptr if `title` is empty.
|
// text). Cached and only regenerated when any of these change from the last
|
||||||
SP<Render::ITexture> GetTitleTexture(const std::string& title, float textSizePx, uint64_t colorValue, const std::string& fontFamily, int maxWidthPx, bool isActive);
|
// 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;
|
WP<ChromeDecoration> self;
|
||||||
|
|
||||||
@@ -64,14 +67,14 @@ private:
|
|||||||
Vector2D cachedTexSize = {-1, -1};
|
Vector2D cachedTexSize = {-1, -1};
|
||||||
float cachedExtent = -1.F;
|
float cachedExtent = -1.F;
|
||||||
float cachedChamfer = -1.F;
|
float cachedChamfer = -1.F;
|
||||||
uint64_t cachedColorValue = 0;
|
ChromeGradient cachedGradient;
|
||||||
float cachedTitleBarHeight = -1.F;
|
float cachedTitleBarHeight = -1.F;
|
||||||
float cachedTitleBarWidth = -1.F;
|
float cachedTitleBarWidth = -1.F;
|
||||||
|
|
||||||
SP<Render::ITexture> cachedTitleTex;
|
SP<Render::ITexture> cachedTitleTex;
|
||||||
std::string cachedTitleTexTitle;
|
std::string cachedTitleTexTitle;
|
||||||
float cachedTitleTexSize = -1.F;
|
float cachedTitleTexSize = -1.F;
|
||||||
uint64_t cachedTitleTexColor = 0;
|
float cachedTitleTexAlpha = -1.F;
|
||||||
std::string cachedTitleTexFont;
|
std::string cachedTitleTexFont;
|
||||||
int cachedTitleTexMaxWidth = -1;
|
int cachedTitleTexMaxWidth = -1;
|
||||||
bool cachedTitleTexActive = false;
|
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);
|
const bool isActive = g_pCompositor->isWindowActive(window);
|
||||||
// When follow_hyprland_border_color is set, track Hyprland's own
|
// When follow_hyprland_border_color is set, track Hyprland's own
|
||||||
// general:col.active_border / col.inactive_border (already resolved per
|
// general:col.active_border / col.inactive_border (already resolved per
|
||||||
// focus state and animated by the compositor) - gradients are collapsed
|
// focus state and animated by the compositor), stops and angle included.
|
||||||
// to their first stop, since the ring is a flat fill, not a shader.
|
|
||||||
// Otherwise (or if Hyprland reports no border color at all) fall back to
|
// Otherwise (or if Hyprland reports no border color at all) fall back to
|
||||||
// our own active_color/inactive_color config.
|
// our own active_color/inactive_color config, which take the same
|
||||||
const auto& borderGradient = window->m_realBorderColor;
|
// gradient syntax.
|
||||||
const bool followHyprlandColor = PluginState->config.followHyprlandBorderColor->value() && !borderGradient.m_colors.empty();
|
const auto& hyprlandBorder = window->m_realBorderColor;
|
||||||
const uint64_t colorValue = followHyprlandColor
|
const bool followHyprlandColor = PluginState->config.followHyprlandBorderColor->value() && !hyprlandBorder.m_colors.empty();
|
||||||
? static_cast<uint64_t>(borderGradient.m_colors.front().getAsHex())
|
const auto gradient = ChromeGradient::From(followHyprlandColor
|
||||||
: static_cast<uint64_t>(isActive ? PluginState->config.activeColor->value() : PluginState->config.inactiveColor->value());
|
? 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 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 float textSizePx = static_cast<float>(PluginState->config.titlebarTextSize->value()) * monitor->m_scale;
|
||||||
const std::string fontFamily = PluginState->config.titlebarFont->value();
|
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 int measureMaxWidthPx = static_cast<int>(baseGeo.MaxTitleBarWidth() - baseGeo.barX0 - titlePadding * 2.F);
|
||||||
|
|
||||||
const auto titleTex = measureMaxWidthPx > 0
|
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;
|
: nullptr;
|
||||||
const float texW = (titleTex && titleTex->ok()) ? static_cast<float>(titleTex->m_size.x) : 0.F;
|
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
|
// with title length - otherwise every title change would needlessly
|
||||||
// regenerate an identical border texture.
|
// regenerate an identical border texture.
|
||||||
const float borderTitleBarWidthPx = geo.fullSpan ? geo.MaxTitleBarWidth() : geo.titleBarWidth;
|
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())
|
if (!tex || !tex->ok())
|
||||||
return {};
|
return {};
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include <hyprland/src/config/values/types/BoolValue.hpp>
|
#include <hyprland/src/config/values/types/BoolValue.hpp>
|
||||||
#include <hyprland/src/config/values/types/ColorValue.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/IntValue.hpp>
|
||||||
#include <hyprland/src/config/values/types/StringValue.hpp>
|
#include <hyprland/src/config/values/types/StringValue.hpp>
|
||||||
#include <hyprland/src/plugins/PluginAPI.hpp>
|
#include <hyprland/src/plugins/PluginAPI.hpp>
|
||||||
@@ -11,6 +12,9 @@
|
|||||||
using IntValue = Config::Values::CIntValue;
|
using IntValue = Config::Values::CIntValue;
|
||||||
using BoolValue = Config::Values::CBoolValue;
|
using BoolValue = Config::Values::CBoolValue;
|
||||||
using ColorValue = Config::Values::CColorValue;
|
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;
|
using StringValue = Config::Values::CStringValue;
|
||||||
|
|
||||||
// Hyprland's raw color ints (and CHyprColor(uint64_t)) are AARRGGBB, but
|
// 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",
|
"plugin:hyprchrome:follow_hyprland_border_color",
|
||||||
"Whether the border tracks Hyprland's own active/inactive border color instead of active_color/inactive_color",
|
"Whether the border tracks Hyprland's own active/inactive border color instead of active_color/inactive_color",
|
||||||
true),
|
true),
|
||||||
.activeColor = makeShared<ColorValue>(
|
.activeColor = makeShared<GradientValue>(
|
||||||
"plugin:hyprchrome:active_color",
|
"plugin:hyprchrome:active_color",
|
||||||
"Color of the border on the focused window, used when follow_hyprland_border_color is false",
|
"Color (or gradient) of the border on the focused window, used when follow_hyprland_border_color is false",
|
||||||
RGBAToARGB(0xFFD063FF)),
|
CHyprColor{RGBAToARGB(0xFFD063FF)}),
|
||||||
.inactiveColor = makeShared<ColorValue>(
|
.inactiveColor = makeShared<GradientValue>(
|
||||||
"plugin:hyprchrome:inactive_color",
|
"plugin:hyprchrome:inactive_color",
|
||||||
"Color of the border on unfocused windows, used when follow_hyprland_border_color is false",
|
"Color (or gradient) of the border on unfocused windows, used when follow_hyprland_border_color is false",
|
||||||
RGBAToARGB(0x6C6C6CFF)),
|
CHyprColor{RGBAToARGB(0x6C6C6CFF)}),
|
||||||
.titlebarHeight = makeShared<IntValue>(
|
.titlebarHeight = makeShared<IntValue>(
|
||||||
"plugin:hyprchrome:titlebar_height",
|
"plugin:hyprchrome:titlebar_height",
|
||||||
"Height of the title bar hanging off the floating top-border cutout, in pixels",
|
"Height of the title bar hanging off the floating top-border cutout, in pixels",
|
||||||
|
|||||||
Reference in New Issue
Block a user