darmanandClaude Opus 5 ec9e2fb7cd Take the gradient out of the inward glow's per-layer fills
DrawInwardGlow filled each of its ~glowPx overlapping layers with the
border gradient directly. cairo evaluates a gradient source roughly eight
times slower than a solid colour, so that cost was paid once per layer:
at 1920x1080 with a 20px glow, 33ms per render, against 4.3ms for the
identical layers filled solid. At 3840x2160 it was 124ms - eight frames.

The layers now accumulate as alpha only, with a solid source, into a
scratch surface; the gradient is applied to the finished falloff in a
single masked pass. The product is what the per-layer gradient fills
produced before - gradientAlpha(p) * accumulated(p), in the gradient's own
colour - so the falloff math, the layer overlap, and every edge of every
layer are untouched.

Two non-obvious details, both measured rather than reasoned:

The scratch surface is ARGB32 despite only its alpha ever being read.
cairo has no optimized compositing path for A8 destinations, and
rendering these same layers into an A8 surface measured ~6x slower than
into ARGB32 (26.5ms vs 4.3ms).

The colorizing pass is clipped to the glow band. Left unclipped,
cairo_mask_surface evaluates the gradient across the mask's full extents -
the entire window - rather than the perimeter-deep sliver that is actually
non-zero, which was ~25ms of the total on its own. The clip is pushed
kGlowClipSlack past the glow on both edges: a clip edge lying exactly on
the mask's own antialiased edge multiplies the two coverages together and
darkens that boundary by up to a third, which is precisely the corner
seam the layer geometry is built to avoid. Slackened, max alpha error
against the old output drops from 27/255 to 6/255, the remainder being
8-bit quantization through the mask.

Net ~3.5x at 1080p (33ms -> 9.4ms), ~3.2x at 1440p, ~2.6x at 4K.

CreateGradientPattern's alphaScale parameter existed only to serve the
per-layer fills and is now dead, so it and its rationale are gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 14:57:23 +02:00
2026-08-01 09:45:14 +00:00
2026-07-28 21:22:13 +02:00
2026-07-28 21:22:13 +02:00
2026-07-28 21:22:13 +02:00

hypr-chrome

A Hyprland plugin that draws a chamfered HUD-style border frame behind each window, expanded past the window's own edges, with a floating title bar 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, gradients included.

Config

plugin {
    hyprchrome {
        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             # 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
    }
}

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

hyprpm add https://git.mgaction.town/darman/hypr-chrome.git
hyprpm enable hypr-chrome

hyprpm builds the plugin from source against your own installed Hyprland, using the build commands declared in hyprpm.toml (the same cmake invocations as below).

NixOS

flake.nix exposes packages.x86_64-linux.default, built against pkgs.hyprland from this flake's own nixpkgs input (with Hyprland's own compiler/stdenv, for ABI correctness). Add this repo as a flake input and set inputs.hypr-chrome.inputs.nixpkgs.follows = "nixpkgs" (your own system's nixpkgs input) so it's built against the exact Hyprland your system runs - then add inputs.hypr-chrome.packages.${system}.default directly to your wayland.windowManager.hyprland.plugins (NixOS/home-manager) list. The package installs as lib/libhypr-chrome.so (the lib<pname>.so convention that module derives its path from automatically) even though the plain hyprctl/hyprpm path above uses the unprefixed hypr-chrome.so.

Building

Requires the Hyprland headers/libs (hyprland, libdrm, libinput, libudev, wayland-server, xkbcommon, pixman-1, cairo, etc.), ABI-locked to the exact Hyprland build you'll load the plugin into. A pinned Nix flake dev shell provides all of it:

nix develop
cmake -B build -S .
cmake --build build

This produces hypr-chrome.so at the repo root (not inside build/, so the hyprctl/buildAndLoad.sh commands below can find it directly).

Developing

Load/reload the plugin into a running Hyprland session:

hyprctl plugin load "$(pwd)/hypr-chrome.so"
hyprctl plugin unload "$(pwd)/hypr-chrome.so"

buildAndLoad.sh chains build + reload for iteration. Hyprland's plugin 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 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.

S
Description
No description provided
Readme
142 KiB
Languages
C++ 89.5%
Nix 5.5%
CMake 3%
Shell 2%