Compare commits
12
Commits
0.1.1
..
e60581a760
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e60581a760 | ||
|
|
5fdf50ba86 | ||
|
|
d69fe10b46 | ||
|
|
1735a5c796 | ||
|
|
3b89aa1c0c | ||
|
|
e773a47158 | ||
|
|
2b377ed58e | ||
|
|
b328a3cb8f | ||
|
|
5c2ed492d6 | ||
|
|
48b277207f | ||
|
|
375cfe1f00 | ||
|
|
b9dafe2ce1 |
@@ -42,6 +42,8 @@ This builds and hot-reloads the plugin into a running Hyprland session in one st
|
||||
|
||||
The script copies the built `.so` to a uniquely-suffixed filename (`hypr-chrome-<random>.so`) before loading it, and unloads/deletes prior copies. This works around Hyprland's plugin loader never fully `dlmunmap`ping a `.so` on unload — reloading the exact same path just re-serves the stale old mapping instead of the freshly built code.
|
||||
|
||||
It also unloads whatever copy the user's own Hyprland config loads (grepped out of `$XDG_CONFIG_HOME/hypr` rather than hardcoded, since a home-manager-installed one lives at a `/nix/store` path that changes on every rebuild). That copy is a separate `dlopen` with its own decorations, so leaving it loaded draws two frames per window.
|
||||
|
||||
Manual equivalent:
|
||||
|
||||
```sh
|
||||
@@ -56,7 +58,12 @@ 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
|
||||
- `glow_size` (int, px the border's color bleeds inward past the window edge, over the window itself — 0, the default, disables it)
|
||||
- `glow_strength` (float 0–1, peak opacity of that glow at the window edge, as a fraction of the border color's own alpha)
|
||||
- `shadow_size` (int, px the frame's drop shadow reaches past its own outline — 0, the default, disables it)
|
||||
- `shadow_color` (color, its alpha being the shadow's opacity)
|
||||
- `shadow_offset` (vec2, px the shadow is displaced by; positive is right/down)
|
||||
- `titlebar_height` (int, px)
|
||||
- `titlebar_text_size` (int, px)
|
||||
- `titlebar_font` (string, passed to `cairo_select_font_face`)
|
||||
@@ -76,16 +83,20 @@ Per-window decoration (`src/ChromeDecoration.hpp/.cpp`): implements `IHyprWindow
|
||||
- `getPositioningInfo()` reserves screen space around the window (`DECORATION_POSITION_ABSOLUTE`, all four edges) sized by `extent` (+ `titlebarHeight` on top) — this is what makes Hyprland leave room for the border/title bar instead of it overlapping neighboring windows.
|
||||
- `draw()` doesn't render directly; it enqueues a `ChromePassElement` into Hyprland's render pass each frame.
|
||||
- `GetBorderTexture(...)` and `GetTitleTexture(...)` are the actual cairo/Pango rendering entry points, each memoizing against their own last-seen parameters (size, extent, chamfer, color, title text, font, etc.) so unchanged frames reuse the cached `Render::ITexture` instead of re-rendering.
|
||||
- `FullDecorationExtentGlobal()` computes the decoration's box in global logical coordinates, accounting for workspace animation offset and floating-window offset.
|
||||
- `FullDecorationExtentGlobal()` computes the decoration's box in global logical coordinates, accounting for workspace animation offset and floating-window offset. It deliberately excludes the drop shadow (below), which is drawn outside it.
|
||||
- `GetShadowTexture(...)` renders the drop shadow. The silhouette is `AppendFrameOuterPath` — the same outline `GetBorderTexture` fills, extracted specifically so the two can't drift apart — filled solid into an A8 mask, blurred, then tinted. Three things about it are load-bearing: (1) the window's interior is cleared *after* the blur, not before, since punching it first would smear shadow inward across the window's own content; the cut lands exactly on the ring's inner boundary so the frame's opaque pixels hide it. (2) The shadow is not part of `getPositioningInfo`'s reserved extents — reserving it would push neighbouring windows away by the shadow's width — so it's simply drawn past the decoration's box, which is why `damageEntire()` and `boundingBox()` have to expand by `ShadowMarginLogical()` by hand. (3) It's rendered at most `kShadowMaxDim` px on the long edge and upscaled by the GPU; a blurred blob loses nothing to that, and it caps a cost that would otherwise be paid per frame of a resize animation. The blur itself is three box passes (`BlurA8Surface`), transposing between each so the vertical pass reuses the horizontal one's cache-friendly row code.
|
||||
- The inward glow (`glow_size`/`glow_strength`) is drawn by `DrawInwardGlow` into the hole the ring's even-odd fill leaves behind, so it lands on the window's own pixels (this decoration is `DECORATION_LAYER_OVER`, and the texture spans the whole window box, not just the ring). Its falloff is built from *overlapping* fills (one layer per px of depth, clamped to `kGlowMinLayers`/`kGlowMaxLayers`) — layer *i* covers the window edge inward to depth `glowPx * i / layers`, so a pixel `d` from the edge is painted by every layer deeper than `d`. The profile is stated explicitly (`strength * (1 - d/glowPx)^kGlowFalloffExponent` — fast off the edge, easing into a tail that reaches zero tangentially so there's no ring where it stops), and since each layer composites over every deeper one, a layer's own alpha is *not* its target: it's solved outermost-inward as `1 - a_j = (1 - T_j) / (1 - T_j+1)`. Changing the profile means changing `targetAt`, not the per-layer alphas. Abutting disjoint bands instead would leave an antialiasing seam at every shared edge; that's the reason for the overlap, don't "optimize" it away. Every layer's *outer* edge is the ring's inner boundary verbatim, chamfer vertices and all — filleting or otherwise altering it detaches the glow from the frame and opens a sliver of unpainted window at each corner. The corner softening lives entirely on the layers' *inner* edges, which are what the accumulated falloff's contours actually follow: each is inset by its own depth, filleted by `kGlowCornerSmoothing` × that depth (`AppendFilletedPolygon`, a quadratic Bezier through each vertex), and has its chamfer shrunk by `kChamferInsetShrink` × that depth. That last correction is not optional cosmetics — insetting a chamfered rect while holding its chamfer constant moves the 45° face in by `d·√2` rather than `d`, so without it the glow runs ~41% deeper at every corner than along the sides. The per-layer alpha is baked into the gradient pattern (`CreateGradientPattern`'s `alphaScale`) specifically so each layer can be a `cairo_fill` of its own band rather than a clip + `cairo_paint_with_alpha`, which would rasterize the clip's full extents — i.e. the whole window area — once per layer.
|
||||
|
||||
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
|
||||
@@ -79,5 +87,8 @@ hyprctl plugin unload "$(pwd)/hypr-chrome.so"
|
||||
loader never fully unmaps a `.so` once `dlopen`'d, so reloading the same path
|
||||
just re-serves the old (possibly stale) mapping — the script works around
|
||||
this by copying each build to a uniquely-suffixed filename before loading it
|
||||
and cleaning up prior copies. It hardcodes an absolute checkout path at the
|
||||
top; check it matches your checkout before running it.
|
||||
and cleaning up prior copies. It also unloads any copy your own Hyprland
|
||||
config loads (found by grepping `$XDG_CONFIG_HOME/hypr` for a `hypr-chrome`
|
||||
`.so` path), since that one is a separate `dlopen` that would draw a second
|
||||
frame on every window. It hardcodes an absolute checkout path at the top;
|
||||
check it matches your checkout before running it.
|
||||
|
||||
@@ -21,4 +21,19 @@ for old in hypr-chrome.so hypr-chrome-*.so; do
|
||||
[ "$old" = "hypr-chrome.so" ] || rm -f "$old"
|
||||
done
|
||||
|
||||
# The session also loads the plugin declaratively from the personal Hyprland
|
||||
# config (home-manager's wayland.windowManager.hyprland.plugins, which lands
|
||||
# as a /nix/store/.../lib/libhypr-chrome.so). That copy is a separate dlopen
|
||||
# with its own decorations, so leaving it loaded means every window gets two
|
||||
# frames drawn on top of each other. Its store path changes on every rebuild,
|
||||
# so read it back out of the config instead of hardcoding it (-R to follow
|
||||
# home-manager's symlinks into the store).
|
||||
HYPR_CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/hypr"
|
||||
if [ -d "$HYPR_CONFIG_DIR" ]; then
|
||||
for configured in $(grep -Rho '/[^[:space:]"'"'"']*hypr-chrome[^[:space:]"'"'"']*\.so' "$HYPR_CONFIG_DIR" 2>/dev/null | sort -u || true); do
|
||||
[ "$configured" = "$DIR/$NEW_SO" ] && continue
|
||||
hyprctl plugin unload "$configured" >/dev/null 2>&1 || true
|
||||
done
|
||||
fi
|
||||
|
||||
hyprctl plugin load "$DIR/$NEW_SO"
|
||||
|
||||
+24
-2
@@ -13,8 +13,30 @@ 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;
|
||||
|
||||
// How far the border's glow bleeds inward past the window edge, over the
|
||||
// window's own pixels, in logical pixels. 0 disables the glow entirely.
|
||||
SP<IntValue> glowSize;
|
||||
|
||||
// Peak opacity of that glow, at the window edge, as a fraction of the
|
||||
// border color's own alpha.
|
||||
SP<FloatValue> glowStrength;
|
||||
|
||||
// How far the frame's drop shadow reaches past its own outline, in logical
|
||||
// pixels. 0 disables the shadow entirely.
|
||||
SP<IntValue> shadowSize;
|
||||
|
||||
// Color the shadow is tinted with - its alpha is the shadow's opacity at
|
||||
// full coverage.
|
||||
SP<ColorValue> shadowColor;
|
||||
|
||||
// How far the shadow is displaced from the frame, in logical pixels;
|
||||
// positive is right/down.
|
||||
SP<Vec2Value> shadowOffset;
|
||||
|
||||
// Height of the title bar hanging off the floating top-border cutout, in
|
||||
// logical pixels.
|
||||
|
||||
+471
-85
@@ -11,6 +11,9 @@
|
||||
#include <cairo/cairo.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <numbers>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
@@ -27,6 +30,346 @@ 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;
|
||||
|
||||
// `alphaScale` multiplies every stop's own alpha - the glow layers below
|
||||
// reuse the border's gradient at a fraction of its opacity, and baking that
|
||||
// into the pattern lets them cairo_fill() (which only touches the filled
|
||||
// band) instead of clip+paint (which rasterizes the clip's whole extents).
|
||||
cairo_pattern_t* CreateGradientPattern(const ChromeGradient& gradient, double w, double h, double alphaScale = 1.0) {
|
||||
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 * alphaScale);
|
||||
}
|
||||
|
||||
return pattern;
|
||||
}
|
||||
|
||||
struct Point {
|
||||
double x = 0, y = 0;
|
||||
};
|
||||
|
||||
// The corners of a chamfered rectangle spanning [x0,x1]x[y0,y1], cut by
|
||||
// `chamfer` (clamped to what the rect can fit) - eight points, or the plain
|
||||
// four if the chamfer rounds away to nothing. Empty if the rect has collapsed.
|
||||
std::vector<Point> ChamferedRectPoints(float x0, float y0, float x1, float y1, float chamfer) {
|
||||
if (x1 <= x0 || y1 <= y0)
|
||||
return {};
|
||||
|
||||
const float c = std::clamp(chamfer, 0.F, std::min(x1 - x0, y1 - y0) / 2.F);
|
||||
if (c <= 0.F)
|
||||
return {{x0, y0}, {x1, y0}, {x1, y1}, {x0, y1}};
|
||||
|
||||
return {{x0 + c, y0}, {x1 - c, y0}, {x1, y0 + c}, {x1, y1 - c},
|
||||
{x1 - c, y1}, {x0 + c, y1}, {x0, y1 - c}, {x0, y0 + c}};
|
||||
}
|
||||
|
||||
// Appends `pts` as a closed subpath, with every vertex rounded off by a
|
||||
// `smooth`-px fillet: the path leaves the incoming edge `smooth` px early and
|
||||
// rejoins the outgoing one `smooth` px late, bridged by a quadratic Bezier
|
||||
// through the vertex itself (written as the equivalent cubic, which is all
|
||||
// cairo takes). `smooth` is capped at half the shortest edge so two adjacent
|
||||
// fillets meet at that edge's midpoint at worst instead of overrunning each
|
||||
// other; at 0 this is just a polyline.
|
||||
void AppendFilletedPolygon(cairo_t* cr, const std::vector<Point>& pts, double smooth) {
|
||||
const size_t n = pts.size();
|
||||
if (n < 3)
|
||||
return;
|
||||
|
||||
double maxSmooth = std::numeric_limits<double>::max();
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
const auto& a = pts[i];
|
||||
const auto& b = pts[(i + 1) % n];
|
||||
maxSmooth = std::min(maxSmooth, std::hypot(b.x - a.x, b.y - a.y) / 2.0);
|
||||
}
|
||||
|
||||
const double s = std::clamp(smooth, 0.0, maxSmooth);
|
||||
if (s <= 0.0) {
|
||||
cairo_move_to(cr, pts[0].x, pts[0].y);
|
||||
for (size_t i = 1; i < n; ++i)
|
||||
cairo_line_to(cr, pts[i].x, pts[i].y);
|
||||
cairo_close_path(cr);
|
||||
return;
|
||||
}
|
||||
|
||||
// `s` px from `from` towards `to`.
|
||||
const auto along = [](const Point& from, const Point& to, double d) {
|
||||
const double dx = to.x - from.x, dy = to.y - from.y;
|
||||
const double len = std::hypot(dx, dy);
|
||||
return len > 0 ? Point{from.x + dx / len * d, from.y + dy / len * d} : from;
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
const auto& prev = pts[(i + n - 1) % n];
|
||||
const auto& cur = pts[i];
|
||||
const auto& next = pts[(i + 1) % n];
|
||||
|
||||
const auto in = along(cur, prev, s);
|
||||
const auto out = along(cur, next, s);
|
||||
|
||||
if (i == 0)
|
||||
cairo_move_to(cr, in.x, in.y);
|
||||
else
|
||||
cairo_line_to(cr, in.x, in.y);
|
||||
|
||||
cairo_curve_to(cr,
|
||||
cur.x + (in.x - cur.x) / 3.0, cur.y + (in.y - cur.y) / 3.0,
|
||||
cur.x + (out.x - cur.x) / 3.0, cur.y + (out.y - cur.y) / 3.0,
|
||||
out.x, out.y);
|
||||
}
|
||||
|
||||
cairo_close_path(cr);
|
||||
}
|
||||
|
||||
// Appends a closed chamfered-rectangle subpath spanning [x0,x1]x[y0,y1], its
|
||||
// corners cut by `chamfer` and then optionally softened by a `smooth`-px
|
||||
// fillet. Returns false without touching the path if the rect has collapsed
|
||||
// to nothing.
|
||||
bool AppendChamferedRect(cairo_t* cr, float x0, float y0, float x1, float y1, float chamfer, float smooth = 0.F) {
|
||||
const auto pts = ChamferedRectPoints(x0, y0, x1, y1, chamfer);
|
||||
if (pts.empty())
|
||||
return false;
|
||||
|
||||
AppendFilletedPolygon(cr, pts, smooth);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Appends the frame's outer boundary as a closed subpath: the chamfered ring
|
||||
// edge, the title bar plateau hanging off the top-left (or, below the fullSpan
|
||||
// threshold, a flat top edge at the plateau's own height, since there's no
|
||||
// room for the dip back down), and the notch cut into either side edge. The
|
||||
// ring itself starts at geo.topPad rather than 0, leaving the strip above it
|
||||
// for the title bar to stick up into.
|
||||
//
|
||||
// Both the frame's own fill and the drop shadow's silhouette walk this, so the
|
||||
// shadow can't drift out of step with the shape casting it.
|
||||
void AppendFrameOuterPath(cairo_t* cr, const ChromeDecorationGeometry& geo) {
|
||||
const float w = geo.w;
|
||||
const float h = geo.h;
|
||||
|
||||
cairo_move_to(cr, geo.chamfer + geo.topPad, 0);
|
||||
|
||||
/* *** Top Edge *** */
|
||||
if (geo.fullSpan) {
|
||||
cairo_line_to(cr, w - geo.chamfer - geo.topPad, 0);
|
||||
cairo_line_to(cr, w, geo.topPad + geo.chamfer);
|
||||
} else {
|
||||
cairo_line_to(cr, geo.titleBarWidth, 0);
|
||||
cairo_line_to(cr, geo.titleBarWidth + geo.topPad + geo.extent, geo.topPad + geo.extent);
|
||||
|
||||
cairo_line_to(cr, geo.dipReturnX, geo.topPad + geo.extent);
|
||||
cairo_line_to(cr, geo.dipReturnX + geo.extent, geo.topPad);
|
||||
cairo_line_to(cr, w - geo.chamfer, geo.topPad);
|
||||
cairo_line_to(cr, w, geo.topPad + geo.chamfer);
|
||||
}
|
||||
|
||||
/* *** Right Edge *** */
|
||||
|
||||
cairo_line_to(cr, w, geo.insetStart);
|
||||
cairo_line_to(cr, w - geo.insetDepth, geo.insetStart + geo.insetChamfer);
|
||||
cairo_line_to(cr, w - geo.insetDepth, geo.insetEnd - geo.insetChamfer);
|
||||
cairo_line_to(cr, w, geo.insetEnd);
|
||||
|
||||
cairo_line_to(cr, w, h - geo.chamfer);
|
||||
cairo_line_to(cr, w - geo.chamfer, h);
|
||||
|
||||
/* *** Bottom Edge *** */
|
||||
|
||||
cairo_line_to(cr, geo.chamfer, h);
|
||||
cairo_line_to(cr, 0, h - geo.chamfer);
|
||||
cairo_line_to(cr, 0, geo.topPad + geo.chamfer);
|
||||
|
||||
/* *** Left Edge *** */
|
||||
cairo_line_to(cr, 0, geo.insetEnd);
|
||||
cairo_line_to(cr, 0 + geo.insetDepth, geo.insetEnd - geo.insetChamfer);
|
||||
cairo_line_to(cr, 0 + geo.insetDepth, geo.insetStart + geo.insetChamfer);
|
||||
cairo_line_to(cr, 0, geo.insetStart);
|
||||
|
||||
cairo_line_to(cr, 0, geo.topPad + geo.chamfer);
|
||||
|
||||
cairo_close_path(cr);
|
||||
}
|
||||
|
||||
// How many overlapping layers the inward glow's falloff is built from - one
|
||||
// per px of depth, so the bands stay sub-pixel-ish either way, bounded so a
|
||||
// hairline glow doesn't waste fills and a huge one doesn't run away with them
|
||||
// (each layer is another band-shaped fill, only on a cache miss).
|
||||
constexpr int kGlowMinLayers = 16;
|
||||
constexpr int kGlowMaxLayers = 64;
|
||||
|
||||
// Shape of the falloff: alpha at depth d is strength * (1 - d/glowPx)^this.
|
||||
// Above 1 the glow drops off steeply at the window edge and then eases out
|
||||
// into a long tail - and, because it reaches zero tangentially rather than
|
||||
// at an angle, without a visible ring where the tail finally stops.
|
||||
constexpr float kGlowFalloffExponent = 2.2F;
|
||||
|
||||
// How much of its own depth each glow layer's inner edge is filleted by, so
|
||||
// the glow's contours round off the further in they go. Only the inner edges
|
||||
// get this - the outermost one has to stay the ring's inner boundary exactly,
|
||||
// chamfer vertices and all, or the corners open up a sliver of unpainted
|
||||
// window between the frame and the glow.
|
||||
constexpr float kGlowCornerSmoothing = 0.5F;
|
||||
|
||||
// A chamfered rect inset by `d` only stays a constant `d` away from its
|
||||
// original on the straight edges: holding the chamfer itself constant pushes
|
||||
// the 45-degree face in by d*sqrt(2) rather than d, so the band would run
|
||||
// ~41% deeper at each corner than along the sides. Shrinking the chamfer by
|
||||
// this much per px of inset makes the offset properly parallel instead.
|
||||
constexpr float kChamferInsetShrink = 2.F - std::numbers::sqrt2_v<float>;
|
||||
|
||||
// Bleeds the border color inward past the window edge - over the window's own
|
||||
// pixels, since this decoration renders on DECORATION_LAYER_OVER - fading out
|
||||
// over `glowPx` and peaking at `strength` (times the gradient's own alpha) at
|
||||
// the edge itself.
|
||||
//
|
||||
// Each layer covers the region from the window edge inward to a progressively
|
||||
// greater depth, so a pixel `d` from the edge is painted by every layer deeper
|
||||
// than `d`. Overlapping the layers rather than abutting disjoint bands is what
|
||||
// keeps the ramp free of the antialiasing seams their shared edges would
|
||||
// otherwise leave - the price being that a layer can't just be given the alpha
|
||||
// the profile calls for, since it composites on top of every deeper layer too.
|
||||
//
|
||||
// Band j (between depths j-1 and j) is covered by layers j..n, landing at
|
||||
// 1 - prod(1 - a_i) for i >= j. Walking outermost-inward, each layer's own
|
||||
// alpha then falls out of the profile directly:
|
||||
//
|
||||
// 1 - a_j = (1 - T_j) / (1 - T_j+1)
|
||||
//
|
||||
// with T_j the target alpha sampled at band j's midpoint, and T_n+1 = 0.
|
||||
void DrawInwardGlow(cairo_t* cr, const ChromeDecorationGeometry& geo, const ChromeGradient& gradient, float glowPx, float strength, int w, int h) {
|
||||
if (glowPx < 1.F || strength <= 0.F || geo.innerW <= 0 || geo.innerH <= 0)
|
||||
return;
|
||||
|
||||
const int layers = std::clamp(static_cast<int>(std::ceil(glowPx)), kGlowMinLayers, kGlowMaxLayers);
|
||||
const double peak = std::clamp(strength, 0.F, 1.F);
|
||||
const auto targetAt = [&](double depth) {
|
||||
return peak * std::pow(1.0 - std::clamp(depth / glowPx, 0.0, 1.0), kGlowFalloffExponent);
|
||||
};
|
||||
|
||||
cairo_save(cr);
|
||||
cairo_set_fill_rule(cr, CAIRO_FILL_RULE_EVEN_ODD);
|
||||
|
||||
// Deepest (faintest) layer first, so each iteration already knows the
|
||||
// accumulated target of everything that will composite under it.
|
||||
double deeperTarget = 0.0;
|
||||
for (int i = layers; i >= 1; --i) {
|
||||
const float depth = glowPx * static_cast<float>(i) / layers;
|
||||
// Sampled at the band's shallow (brighter) edge rather than its midpoint,
|
||||
// so the innermost band lands on `strength` exactly instead of half a
|
||||
// band short of it. The half-band brightness bias that trades for is a
|
||||
// sub-pixel shift at these layer counts.
|
||||
const double target = targetAt(glowPx * (static_cast<double>(i) - 1.0) / layers);
|
||||
|
||||
const auto pattern = CreateGradientPattern(gradient, w, h, 1.0 - (1.0 - target) / (1.0 - deeperTarget));
|
||||
cairo_set_source(cr, pattern);
|
||||
deeperTarget = target;
|
||||
|
||||
// Every layer's outer edge is the ring's inner boundary verbatim, so the
|
||||
// glow always meets the frame exactly.
|
||||
AppendChamferedRect(cr, geo.innerX0, geo.innerY0, geo.innerX1, geo.innerY1, geo.innerChamfer);
|
||||
// Punches this layer's un-glowed middle back out - inset by `depth`, with
|
||||
// the corner both parallel-corrected and rounded off in proportion to how
|
||||
// deep it is. Since it's these edges that the accumulated falloff's
|
||||
// contours follow, the glow reads as chamfered where it meets the frame
|
||||
// and progressively rounder inward. On a window smaller than the glow is
|
||||
// deep the middle collapses to nothing and the layer just covers all of
|
||||
// it, which is the right answer anyway.
|
||||
AppendChamferedRect(cr,
|
||||
geo.innerX0 + depth, geo.innerY0 + depth, geo.innerX1 - depth, geo.innerY1 - depth,
|
||||
geo.innerChamfer - depth * kChamferInsetShrink, depth * kGlowCornerSmoothing);
|
||||
cairo_fill(cr);
|
||||
|
||||
cairo_pattern_destroy(pattern);
|
||||
}
|
||||
|
||||
cairo_restore(cr);
|
||||
}
|
||||
|
||||
// One box blur pass over an A8 surface's rows, `radius` either side, with a
|
||||
// running sum so the cost is independent of the radius. Reads past the row's
|
||||
// ends clamp to its end pixels - which for the shadow means clamping to the
|
||||
// transparent margin the silhouette is guaranteed to sit inside, i.e. the
|
||||
// same answer zero-padding would give.
|
||||
void BoxBlurRows(const uint8_t* src, uint8_t* dst, int w, int h, int srcStride, int dstStride, int radius) {
|
||||
const int window = radius * 2 + 1;
|
||||
|
||||
for (int y = 0; y < h; ++y) {
|
||||
const uint8_t* s = src + static_cast<size_t>(y) * srcStride;
|
||||
uint8_t* d = dst + static_cast<size_t>(y) * dstStride;
|
||||
|
||||
int sum = 0;
|
||||
for (int i = -radius; i <= radius; ++i)
|
||||
sum += s[std::clamp(i, 0, w - 1)];
|
||||
|
||||
for (int x = 0; x < w; ++x) {
|
||||
d[x] = static_cast<uint8_t>(sum / window);
|
||||
sum += s[std::clamp(x + radius + 1, 0, w - 1)] - s[std::clamp(x - radius, 0, w - 1)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Three passes of a box blur, which converges on a Gaussian fast enough that
|
||||
// nobody can tell the difference in a shadow. Transposing on each pass means
|
||||
// the column blur is the same (cache-friendly, row-major) code as the row one.
|
||||
constexpr int kShadowBlurPasses = 3;
|
||||
|
||||
void BlurA8Surface(cairo_surface_t* surface, int radius) {
|
||||
if (radius < 1)
|
||||
return;
|
||||
|
||||
const int w = cairo_image_surface_get_width(surface);
|
||||
const int h = cairo_image_surface_get_height(surface);
|
||||
const int stride = cairo_image_surface_get_stride(surface);
|
||||
uint8_t* data = cairo_image_surface_get_data(surface);
|
||||
if (!data || w < 1 || h < 1)
|
||||
return;
|
||||
|
||||
// Ping-pong buffer, sized for either orientation.
|
||||
std::vector<uint8_t> scratch(static_cast<size_t>(w) * h);
|
||||
std::vector<uint8_t> packed(static_cast<size_t>(w) * h);
|
||||
for (int y = 0; y < h; ++y)
|
||||
std::copy_n(data + static_cast<size_t>(y) * stride, w, packed.begin() + static_cast<size_t>(y) * w);
|
||||
|
||||
const auto transpose = [](const std::vector<uint8_t>& in, std::vector<uint8_t>& out, int inW, int inH) {
|
||||
for (int y = 0; y < inH; ++y)
|
||||
for (int x = 0; x < inW; ++x)
|
||||
out[static_cast<size_t>(x) * inH + y] = in[static_cast<size_t>(y) * inW + x];
|
||||
};
|
||||
|
||||
for (int pass = 0; pass < kShadowBlurPasses * 2; ++pass) {
|
||||
// Alternates orientation: rows, then (transposed) columns, then rows...
|
||||
const bool horizontal = pass % 2 == 0;
|
||||
const int curW = horizontal ? w : h;
|
||||
const int curH = horizontal ? h : w;
|
||||
|
||||
BoxBlurRows(packed.data(), scratch.data(), curW, curH, curW, curW, radius);
|
||||
transpose(scratch, packed, curW, curH);
|
||||
}
|
||||
|
||||
for (int y = 0; y < h; ++y)
|
||||
std::copy_n(packed.begin() + static_cast<size_t>(y) * w, w, data + static_cast<size_t>(y) * stride);
|
||||
|
||||
cairo_surface_mark_dirty(surface);
|
||||
}
|
||||
|
||||
// The shadow is a blurred blob - rendering it at full window resolution buys
|
||||
// nothing but cost, and the cost is paid again on every frame of a resize
|
||||
// animation. Past this many pixels on the long edge it's rendered smaller and
|
||||
// left to the GPU's bilinear filter on the way back up.
|
||||
constexpr double kShadowMaxDim = 512.0;
|
||||
}
|
||||
|
||||
ChromeDecoration::ChromeDecoration(PHLWINDOW window) : IHyprWindowDecoration(window) {
|
||||
@@ -82,7 +425,15 @@ eDecorationType ChromeDecoration::getDecorationType() { return DECORATION_CUSTOM
|
||||
void ChromeDecoration::updateWindow(PHLWINDOW window) { damageEntire(); }
|
||||
|
||||
void ChromeDecoration::damageEntire() {
|
||||
g_pHyprRenderer->damageBox(FullDecorationExtentGlobal());
|
||||
// The shadow hangs outside the decoration's own box (it's drawn, not
|
||||
// reserved - see ShadowMarginLogical), so damaging just that box would
|
||||
// leave its outer reaches stale.
|
||||
g_pHyprRenderer->damageBox(FullDecorationExtentGlobal().expand(ShadowMarginLogical()));
|
||||
}
|
||||
|
||||
double ChromeDecoration::ShadowMarginLogical() {
|
||||
const auto offset = PluginState->config.shadowOffset->value();
|
||||
return ShadowMarginPx(static_cast<float>(PluginState->config.shadowSize->value()), {offset.x, offset.y});
|
||||
}
|
||||
|
||||
eDecorationLayer ChromeDecoration::getDecorationLayer() { return DECORATION_LAYER_OVER; }
|
||||
@@ -91,13 +442,14 @@ 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, float glowPx, float glowStrength) {
|
||||
if (sizePx.x < 1 || sizePx.y < 1)
|
||||
return nullptr;
|
||||
|
||||
if (cachedTexture && cachedTexture->ok() && cachedTexSize == sizePx &&
|
||||
cachedExtent == extentPx && cachedChamfer == chamferPx && cachedColorValue == colorValue &&
|
||||
cachedTitleBarHeight == titleBarHeightPx && cachedTitleBarWidth == titleBarWidthPx)
|
||||
cachedExtent == extentPx && cachedChamfer == chamferPx && cachedGradient == gradient &&
|
||||
cachedTitleBarHeight == titleBarHeightPx && cachedTitleBarWidth == titleBarWidthPx &&
|
||||
cachedGlowSize == glowPx && cachedGlowStrength == glowStrength)
|
||||
return cachedTexture;
|
||||
|
||||
const int w = static_cast<int>(sizePx.x);
|
||||
@@ -116,88 +468,24 @@ 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,
|
||||
// leaving the reserved strip above it for the title bar.
|
||||
cairo_move_to(cr, geo.chamfer + geo.topPad, 0);
|
||||
|
||||
/* *** Top Edge *** */
|
||||
if (geo.fullSpan) {
|
||||
// Not enough width for a partial plateau + return trip to the normal
|
||||
// topPad-height edge (see ChromeDecorationGeometry::fullSpan) - the
|
||||
// whole top edge stays at y=0, both corners chamfered the same way the
|
||||
// top-left one already is.
|
||||
cairo_line_to(cr, w - geo.chamfer - geo.topPad, 0);
|
||||
cairo_line_to(cr, w, geo.topPad + geo.chamfer);
|
||||
} else {
|
||||
cairo_line_to(cr, geo.titleBarWidth, 0);
|
||||
cairo_line_to(cr, geo.titleBarWidth + geo.topPad + geo.extent, geo.topPad + geo.extent);
|
||||
|
||||
cairo_line_to(cr, geo.dipReturnX, geo.topPad + geo.extent);
|
||||
cairo_line_to(cr, geo.dipReturnX + geo.extent, geo.topPad);
|
||||
cairo_line_to(cr, w - geo.chamfer, geo.topPad);
|
||||
cairo_line_to(cr, w, geo.topPad + geo.chamfer);
|
||||
}
|
||||
|
||||
/* *** Right Edge *** */
|
||||
|
||||
cairo_line_to(cr, w, geo.insetStart);
|
||||
cairo_line_to(cr, w - geo.insetDepth, geo.insetStart + geo.insetChamfer);
|
||||
cairo_line_to(cr, w - geo.insetDepth, geo.insetEnd - geo.insetChamfer);
|
||||
cairo_line_to(cr, w, geo.insetEnd);
|
||||
|
||||
cairo_line_to(cr, w, h - geo.chamfer);
|
||||
cairo_line_to(cr, w - geo.chamfer, h);
|
||||
|
||||
/* *** Bottom Edge *** */
|
||||
|
||||
cairo_line_to(cr, geo.chamfer, h);
|
||||
cairo_line_to(cr, 0, h - geo.chamfer);
|
||||
cairo_line_to(cr, 0, geo.topPad + geo.chamfer);
|
||||
|
||||
/* *** Left Edge *** */
|
||||
cairo_line_to(cr, 0, geo.insetEnd);
|
||||
cairo_line_to(cr, 0 + geo.insetDepth, geo.insetEnd - geo.insetChamfer);
|
||||
cairo_line_to(cr, 0 + geo.insetDepth, geo.insetStart + geo.insetChamfer);
|
||||
cairo_line_to(cr, 0, geo.insetStart);
|
||||
|
||||
cairo_line_to(cr, 0, geo.topPad + geo.chamfer);
|
||||
|
||||
cairo_close_path(cr);
|
||||
AppendFrameOuterPath(cr, geo);
|
||||
|
||||
// inner boundary (window edge), inset by extent, chamfered - punches the
|
||||
// hole out of the outer path via even-odd fill, leaving just the ring.
|
||||
if (geo.innerW > 0 && geo.innerH > 0) {
|
||||
AppendChamferedRect(cr, geo.innerX0, geo.innerY0, geo.innerX1, geo.innerY1, geo.innerChamfer);
|
||||
|
||||
cairo_move_to(cr, geo.innerX0 + geo.innerChamfer, geo.innerY0);
|
||||
|
||||
/* *** Top Edge *** */
|
||||
|
||||
cairo_line_to(cr, geo.innerX1 - geo.innerChamfer, geo.innerY0);
|
||||
cairo_line_to(cr, geo.innerX1, geo.innerY0 + geo.innerChamfer);
|
||||
|
||||
/* *** Right Edge *** */
|
||||
|
||||
cairo_line_to(cr, geo.innerX1, geo.innerY1 - geo.innerChamfer);
|
||||
cairo_line_to(cr, geo.innerX1 - geo.innerChamfer, geo.innerY1);
|
||||
|
||||
/* *** Bottom Edge *** */
|
||||
|
||||
cairo_line_to(cr, geo.innerX0 + geo.innerChamfer, geo.innerY1);
|
||||
cairo_line_to(cr, geo.innerX0, geo.innerY1 - geo.innerChamfer);
|
||||
|
||||
/* *** Left Edge *** */
|
||||
|
||||
cairo_line_to(cr, geo.innerX0, geo.innerY0 + geo.innerChamfer);
|
||||
|
||||
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);
|
||||
|
||||
// Drawn after the ring, into the hole the fill above just left behind.
|
||||
DrawInwardGlow(cr, geo, gradient, glowPx, glowStrength, w, h);
|
||||
|
||||
cairo_surface_flush(surface);
|
||||
|
||||
@@ -205,9 +493,11 @@ SP<Render::ITexture> ChromeDecoration::GetBorderTexture(const Vector2D& sizePx,
|
||||
cachedTexSize = sizePx;
|
||||
cachedExtent = extentPx;
|
||||
cachedChamfer = chamferPx;
|
||||
cachedColorValue = colorValue;
|
||||
cachedGradient = gradient;
|
||||
cachedTitleBarHeight = titleBarHeightPx;
|
||||
cachedTitleBarWidth = titleBarWidthPx;
|
||||
cachedGlowSize = glowPx;
|
||||
cachedGlowStrength = glowStrength;
|
||||
|
||||
cairo_destroy(cr);
|
||||
cairo_surface_destroy(surface);
|
||||
@@ -215,18 +505,114 @@ 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) {
|
||||
float ChromeDecoration::ShadowMarginPx(float shadowSizePx, const Vector2D& offsetPx) {
|
||||
if (shadowSizePx < 1)
|
||||
return 0.F;
|
||||
|
||||
// The blur reaches `shadowSizePx` past the silhouette on every side (see
|
||||
// GetShadowTexture's per-pass radius), and the offset slides all of that
|
||||
// one way - so the margin has to cover both, on whichever side is worse.
|
||||
return shadowSizePx + static_cast<float>(std::max(std::abs(offsetPx.x), std::abs(offsetPx.y)));
|
||||
}
|
||||
|
||||
SP<Render::ITexture> ChromeDecoration::GetShadowTexture(const Vector2D& sizePx, float extentPx, float chamferPx, float titleBarHeightPx, float titleBarWidthPx, float shadowSizePx, const CHyprColor& color, const Vector2D& offsetPx) {
|
||||
if (sizePx.x < 1 || sizePx.y < 1 || shadowSizePx < 1 || color.a <= 0)
|
||||
return nullptr;
|
||||
|
||||
if (cachedShadowTex && cachedShadowTex->ok() && cachedShadowTexSize == sizePx &&
|
||||
cachedShadowExtent == extentPx && cachedShadowChamfer == chamferPx &&
|
||||
cachedShadowTitleBarHeight == titleBarHeightPx && cachedShadowTitleBarWidth == titleBarWidthPx &&
|
||||
cachedShadowSize == shadowSizePx && cachedShadowColor == color && cachedShadowOffset == offsetPx)
|
||||
return cachedShadowTex;
|
||||
|
||||
const auto geo = ChromeDecorationGeometry::ComputeBase(sizePx, extentPx, chamferPx, titleBarHeightPx).WithTitleBarWidth(titleBarWidthPx);
|
||||
|
||||
const float margin = ShadowMarginPx(shadowSizePx, offsetPx);
|
||||
const double fullW = sizePx.x + 2.0 * margin;
|
||||
const double fullH = sizePx.y + 2.0 * margin;
|
||||
const double scale = std::min(1.0, kShadowMaxDim / std::max(fullW, fullH));
|
||||
|
||||
const int w = std::max(1, static_cast<int>(std::ceil(fullW * scale)));
|
||||
const int h = std::max(1, static_cast<int>(std::ceil(fullH * scale)));
|
||||
|
||||
// The silhouette goes into an alpha-only mask, which is what gets blurred -
|
||||
// one channel instead of four, and the color is applied afterwards.
|
||||
const auto mask = cairo_image_surface_create(CAIRO_FORMAT_A8, w, h);
|
||||
const auto maskCr = cairo_create(mask);
|
||||
|
||||
cairo_save(maskCr);
|
||||
cairo_set_operator(maskCr, CAIRO_OPERATOR_CLEAR);
|
||||
cairo_paint(maskCr);
|
||||
cairo_restore(maskCr);
|
||||
|
||||
cairo_scale(maskCr, scale, scale);
|
||||
cairo_translate(maskCr, margin, margin);
|
||||
|
||||
// Solid, hole and all: the frame's own cutouts (the side notches, the strip
|
||||
// beside the title bar) are part of the outline, so they cast their shape
|
||||
// for free, but the window's interior must stay filled for now - punching
|
||||
// it before the blur would smear the shadow inward across the window.
|
||||
cairo_set_source_rgba(maskCr, 1, 1, 1, 1);
|
||||
AppendFrameOuterPath(maskCr, geo);
|
||||
cairo_fill(maskCr);
|
||||
|
||||
cairo_surface_flush(mask);
|
||||
BlurA8Surface(mask, std::max(1, static_cast<int>(std::lround(shadowSizePx * scale / kShadowBlurPasses))));
|
||||
|
||||
// Now clear where the window itself is. The cut lands exactly on the ring's
|
||||
// inner boundary, so the frame's own opaque pixels cover it and the hard
|
||||
// edge never shows - while anything that would have fallen on the window's
|
||||
// own content is gone. Offset back out, since the whole texture is drawn
|
||||
// shifted by `offsetPx` and this has to end up over the real window.
|
||||
cairo_save(maskCr);
|
||||
cairo_set_operator(maskCr, CAIRO_OPERATOR_CLEAR);
|
||||
AppendChamferedRect(maskCr,
|
||||
geo.innerX0 - static_cast<float>(offsetPx.x), geo.innerY0 - static_cast<float>(offsetPx.y),
|
||||
geo.innerX1 - static_cast<float>(offsetPx.x), geo.innerY1 - static_cast<float>(offsetPx.y),
|
||||
geo.innerChamfer);
|
||||
cairo_fill(maskCr);
|
||||
cairo_restore(maskCr);
|
||||
|
||||
const auto surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h);
|
||||
const auto cr = cairo_create(surface);
|
||||
|
||||
cairo_save(cr);
|
||||
cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
|
||||
cairo_paint(cr);
|
||||
cairo_restore(cr);
|
||||
|
||||
cairo_set_source_rgba(cr, color.r, color.g, color.b, color.a);
|
||||
cairo_mask_surface(cr, mask, 0, 0);
|
||||
cairo_surface_flush(surface);
|
||||
|
||||
cachedShadowTex = g_pHyprRenderer->createTexture(surface);
|
||||
cachedShadowTexSize = sizePx;
|
||||
cachedShadowExtent = extentPx;
|
||||
cachedShadowChamfer = chamferPx;
|
||||
cachedShadowTitleBarHeight = titleBarHeightPx;
|
||||
cachedShadowTitleBarWidth = titleBarWidthPx;
|
||||
cachedShadowSize = shadowSizePx;
|
||||
cachedShadowColor = color;
|
||||
cachedShadowOffset = offsetPx;
|
||||
|
||||
cairo_destroy(cr);
|
||||
cairo_surface_destroy(surface);
|
||||
cairo_destroy(maskCr);
|
||||
cairo_surface_destroy(mask);
|
||||
|
||||
return cachedShadowTex;
|
||||
}
|
||||
|
||||
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 +648,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;
|
||||
|
||||
+48
-10
@@ -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,35 @@ 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), plus -
|
||||
// if `glowPx` is non-zero - that same gradient bled `glowPx` inward past
|
||||
// the ring's inner boundary, over the window's own pixels, fading out from
|
||||
// `glowStrength` of its alpha at the window edge. 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, float glowPx, float glowStrength);
|
||||
|
||||
// Returns a texture of the frame's drop shadow: the same outer silhouette
|
||||
// GetBorderTexture fills, blurred by `shadowSizePx` and tinted `color`,
|
||||
// with the window's own interior cleared back out afterwards so the shadow
|
||||
// never lands on the window's content. The texture covers the frame's box
|
||||
// grown by ShadowMarginPx on every side, and is meant to be drawn at that
|
||||
// box translated by `offsetPx`. Cached like the others; nullptr when the
|
||||
// shadow is off (`shadowSizePx` < 1) or fully transparent.
|
||||
SP<Render::ITexture> GetShadowTexture(const Vector2D& sizePx, float extentPx, float chamferPx, float titleBarHeightPx, float titleBarWidthPx, float shadowSizePx, const CHyprColor& color, const Vector2D& offsetPx);
|
||||
|
||||
// How far past the frame's own box the shadow reaches, on every side, in
|
||||
// whatever units `shadowSizePx`/`offsetPx` are given in.
|
||||
static float ShadowMarginPx(float shadowSizePx, const Vector2D& offsetPx);
|
||||
|
||||
// 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,22 +83,41 @@ 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;
|
||||
float cachedGlowSize = -1.F;
|
||||
float cachedGlowStrength = -1.F;
|
||||
|
||||
SP<Render::ITexture> cachedShadowTex;
|
||||
Vector2D cachedShadowTexSize = {-1, -1};
|
||||
float cachedShadowExtent = -1.F;
|
||||
float cachedShadowChamfer = -1.F;
|
||||
float cachedShadowTitleBarHeight = -1.F;
|
||||
float cachedShadowTitleBarWidth = -1.F;
|
||||
float cachedShadowSize = -1.F;
|
||||
CHyprColor cachedShadowColor;
|
||||
Vector2D cachedShadowOffset = {0, 0};
|
||||
|
||||
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;
|
||||
|
||||
// The border frame's box in global (monitor-independent) logical
|
||||
// coordinates: the window's own box, expanded by `extent` and offset by
|
||||
// the window's workspace/floating animation offsets.
|
||||
// the window's workspace/floating animation offsets. Note this does NOT
|
||||
// include the shadow, which is drawn outside it - see ShadowMarginLogical.
|
||||
CBox FullDecorationExtentGlobal();
|
||||
|
||||
// ShadowMarginPx for the configured shadow, in logical px. The shadow is
|
||||
// deliberately not part of the decoration's reserved extents (that would
|
||||
// push neighbouring windows away by the shadow's width); it's simply drawn
|
||||
// past them, so damage and bounding boxes have to account for it by hand.
|
||||
static double ShadowMarginLogical();
|
||||
|
||||
friend class ChromePassElement;
|
||||
};
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
};
|
||||
+38
-12
@@ -34,15 +34,18 @@ 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 glowPx = static_cast<float>(PluginState->config.glowSize->value()) * monitor->m_scale;
|
||||
const float glowStrength = static_cast<float>(PluginState->config.glowStrength->value());
|
||||
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 +61,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,16 +75,39 @@ 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, glowPx, glowStrength);
|
||||
if (!tex || !tex->ok())
|
||||
return {};
|
||||
|
||||
std::vector<UP<IPassElement>> children;
|
||||
|
||||
// Emitted first so it lands under the frame - which matters, because the
|
||||
// shadow does run under the frame's own outline (only the window's interior
|
||||
// is cleared out of it), and the frame is what hides that cut.
|
||||
const float shadowSizePx = static_cast<float>(PluginState->config.shadowSize->value()) * monitor->m_scale;
|
||||
const auto configOffset = PluginState->config.shadowOffset->value();
|
||||
const Vector2D shadowOffsetPx = {configOffset.x * monitor->m_scale, configOffset.y * monitor->m_scale};
|
||||
const CHyprColor shadowColor{static_cast<uint64_t>(PluginState->config.shadowColor->value())};
|
||||
|
||||
if (const auto shadowTex = data.decoration->GetShadowTexture({box.w, box.h}, extentPx, chamferPx, titleBarHeightPx, borderTitleBarWidthPx, shadowSizePx, shadowColor, shadowOffsetPx);
|
||||
shadowTex && shadowTex->ok()) {
|
||||
const float margin = ChromeDecoration::ShadowMarginPx(shadowSizePx, shadowOffsetPx);
|
||||
|
||||
CTexPassElement::SRenderData shadowData;
|
||||
shadowData.tex = shadowTex;
|
||||
shadowData.box = CBox{
|
||||
box.x - margin + shadowOffsetPx.x, box.y - margin + shadowOffsetPx.y,
|
||||
box.w + 2.0 * margin, box.h + 2.0 * margin,
|
||||
};
|
||||
shadowData.a = data.alpha;
|
||||
children.emplace_back(makeUnique<CTexPassElement>(shadowData));
|
||||
}
|
||||
|
||||
CTexPassElement::SRenderData texData;
|
||||
texData.tex = tex;
|
||||
texData.box = box;
|
||||
texData.a = data.alpha;
|
||||
|
||||
std::vector<UP<IPassElement>> children;
|
||||
children.emplace_back(makeUnique<CTexPassElement>(texData));
|
||||
|
||||
if (titleTex && titleTex->ok() && titleTex->m_size.x > 0 && titleTex->m_size.y > 0) {
|
||||
@@ -110,5 +136,5 @@ std::optional<CBox> ChromePassElement::boundingBox() {
|
||||
|
||||
return data.decoration->FullDecorationExtentGlobal()
|
||||
.translate(-monitor->m_position)
|
||||
.expand(4);
|
||||
.expand(4 + ChromeDecoration::ShadowMarginLogical());
|
||||
}
|
||||
|
||||
@@ -4,14 +4,28 @@
|
||||
|
||||
#include <hyprland/src/config/values/types/BoolValue.hpp>
|
||||
#include <hyprland/src/config/values/types/ColorValue.hpp>
|
||||
#include <hyprland/src/config/values/types/FloatValue.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/config/values/types/Vec2Value.hpp>
|
||||
#include <hyprland/src/plugins/PluginAPI.hpp>
|
||||
|
||||
using IntValue = Config::Values::CIntValue;
|
||||
using FloatValue = Config::Values::CFloatValue;
|
||||
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;
|
||||
using Vec2Value = Config::Values::CVec2Value;
|
||||
|
||||
// The trailing options argument of the *Value constructors (min/max/...).
|
||||
// makeShared forwards its arguments through a template parameter, which a
|
||||
// bare braced-init-list can't be deduced from - these have to be named.
|
||||
using IntValueOptions = Config::Values::SIntValueOptions;
|
||||
using FloatValueOptions = Config::Values::SFloatValueOptions;
|
||||
|
||||
// Hyprland's raw color ints (and CHyprColor(uint64_t)) are AARRGGBB, but
|
||||
// literals are easier to read/write as the more common RRGGBBAA. Convert so
|
||||
|
||||
+34
-6
@@ -25,14 +25,37 @@ 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)}),
|
||||
.glowSize = makeShared<IntValue>(
|
||||
"plugin:hyprchrome:glow_size",
|
||||
"How far the border's glow bleeds inward over the window, in pixels (0 disables it)",
|
||||
0,
|
||||
IntValueOptions{.min = 0}),
|
||||
.glowStrength = makeShared<FloatValue>(
|
||||
"plugin:hyprchrome:glow_strength",
|
||||
"Peak opacity of the inward glow at the window edge, 0-1",
|
||||
0.35F,
|
||||
FloatValueOptions{.min = 0.F, .max = 1.F}),
|
||||
.shadowSize = makeShared<IntValue>(
|
||||
"plugin:hyprchrome:shadow_size",
|
||||
"How far the frame's drop shadow reaches past its outline, in pixels (0 disables it)",
|
||||
0,
|
||||
IntValueOptions{.min = 0}),
|
||||
.shadowColor = makeShared<ColorValue>(
|
||||
"plugin:hyprchrome:shadow_color",
|
||||
"Color of the frame's drop shadow; its alpha is the shadow's opacity",
|
||||
RGBAToARGB(0x000000B3)),
|
||||
.shadowOffset = makeShared<Vec2Value>(
|
||||
"plugin:hyprchrome:shadow_offset",
|
||||
"How far the shadow is displaced from the frame, in pixels (positive is right/down)",
|
||||
Config::VEC2{0.F, 0.F}),
|
||||
.titlebarHeight = makeShared<IntValue>(
|
||||
"plugin:hyprchrome:titlebar_height",
|
||||
"Height of the title bar hanging off the floating top-border cutout, in pixels",
|
||||
@@ -52,6 +75,11 @@ struct GlobalState {
|
||||
HyprlandAPI::addConfigValueV2(plugin, config.followHyprlandBorderColor);
|
||||
HyprlandAPI::addConfigValueV2(plugin, config.activeColor);
|
||||
HyprlandAPI::addConfigValueV2(plugin, config.inactiveColor);
|
||||
HyprlandAPI::addConfigValueV2(plugin, config.glowSize);
|
||||
HyprlandAPI::addConfigValueV2(plugin, config.glowStrength);
|
||||
HyprlandAPI::addConfigValueV2(plugin, config.shadowSize);
|
||||
HyprlandAPI::addConfigValueV2(plugin, config.shadowColor);
|
||||
HyprlandAPI::addConfigValueV2(plugin, config.shadowOffset);
|
||||
HyprlandAPI::addConfigValueV2(plugin, config.titlebarHeight);
|
||||
HyprlandAPI::addConfigValueV2(plugin, config.titlebarTextSize);
|
||||
HyprlandAPI::addConfigValueV2(plugin, config.titlebarFont);
|
||||
|
||||
Reference in New Issue
Block a user