hypr-chrome v0.1
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "Common.hpp"
|
||||
|
||||
struct ChromeConfig {
|
||||
SP<BoolValue> enabled;
|
||||
|
||||
// How far the border extends past each window edge, in logical pixels.
|
||||
SP<IntValue> extent;
|
||||
|
||||
// When true, the border tracks Hyprland's own resolved
|
||||
// general:col.active_border/col.inactive_border instead of activeColor/
|
||||
// inactiveColor below.
|
||||
SP<BoolValue> followHyprlandBorderColor;
|
||||
|
||||
SP<ColorValue> activeColor;
|
||||
SP<ColorValue> inactiveColor;
|
||||
|
||||
// Height of the title bar hanging off the floating top-border cutout, in
|
||||
// logical pixels.
|
||||
SP<IntValue> titlebarHeight;
|
||||
|
||||
// Font size of the title bar's text, in logical pixels.
|
||||
SP<IntValue> titlebarTextSize;
|
||||
|
||||
// Font family of the title bar's text (passed to cairo_select_font_face).
|
||||
SP<StringValue> titlebarFont;
|
||||
};
|
||||
@@ -0,0 +1,294 @@
|
||||
#include "ChromeDecoration.hpp"
|
||||
#include "ChromeDecorationGeometry.hpp"
|
||||
#include "ChromePassElement.hpp"
|
||||
|
||||
#include <hyprland/src/desktop/view/Window.hpp>
|
||||
#include <hyprland/src/helpers/Color.hpp>
|
||||
#include <hyprland/src/render/Renderer.hpp>
|
||||
#include <hyprland/src/render/Texture.hpp>
|
||||
#include <hyprland/src/render/decorations/DecorationPositioner.hpp>
|
||||
|
||||
#include <cairo/cairo.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
// Byte offset of the start of each UTF-8 codepoint in `s`, plus a trailing
|
||||
// entry for s.size() - lets truncation pick a prefix length in codepoints
|
||||
// without ever splitting a multi-byte sequence.
|
||||
std::vector<size_t> Utf8CodepointStarts(const std::string& s) {
|
||||
std::vector<size_t> starts;
|
||||
for (size_t i = 0; i < s.size();) {
|
||||
starts.push_back(i);
|
||||
const auto c = static_cast<unsigned char>(s[i]);
|
||||
i += (c & 0x80) == 0 ? 1 : (c & 0xE0) == 0xC0 ? 2 : (c & 0xF0) == 0xE0 ? 3 : (c & 0xF8) == 0xF0 ? 4 : 1;
|
||||
}
|
||||
starts.push_back(s.size());
|
||||
return starts;
|
||||
}
|
||||
}
|
||||
|
||||
ChromeDecoration::ChromeDecoration(PHLWINDOW window) : IHyprWindowDecoration(window) {
|
||||
windowRef = window;
|
||||
|
||||
if (const auto monitor = window->m_monitor.lock())
|
||||
monitor->m_scheduledRecalc = true;
|
||||
}
|
||||
|
||||
ChromeDecoration::~ChromeDecoration() {
|
||||
g_pDecorationPositioner->uncacheDecoration(this);
|
||||
std::erase(PluginState->decorations, self);
|
||||
}
|
||||
|
||||
std::string ChromeDecoration::getDisplayName() { return "Chrome"; }
|
||||
|
||||
SDecorationPositioningInfo ChromeDecoration::getPositioningInfo() {
|
||||
const auto extent = static_cast<double>(PluginState->config.extent->value());
|
||||
const auto titlebarHeight = static_cast<double>(PluginState->config.titlebarHeight->value());
|
||||
|
||||
SDecorationPositioningInfo info;
|
||||
info.policy = DECORATION_POSITION_ABSOLUTE;
|
||||
info.reserved = true;
|
||||
info.edges = DECORATION_EDGE_TOP | DECORATION_EDGE_BOTTOM | DECORATION_EDGE_LEFT | DECORATION_EDGE_RIGHT;
|
||||
info.desiredExtents = {
|
||||
.topLeft = {extent, extent + titlebarHeight},
|
||||
.bottomRight = {extent, extent},
|
||||
};
|
||||
return info;
|
||||
}
|
||||
|
||||
void ChromeDecoration::onPositioningReply(const SDecorationPositioningReply& reply) {
|
||||
assignedBox = reply.assignedGeometry;
|
||||
}
|
||||
|
||||
void ChromeDecoration::draw(PHLMONITOR monitor, float const& alpha) {
|
||||
if (!PluginState->config.enabled->value())
|
||||
return;
|
||||
|
||||
const auto window = windowRef.lock();
|
||||
if (!window || !window->m_isMapped)
|
||||
return;
|
||||
|
||||
if (!window->m_ruleApplicator->decorate().valueOrDefault())
|
||||
return;
|
||||
|
||||
auto data = ChromePassElement::SData{this, alpha};
|
||||
g_pHyprRenderer->m_renderPass.add(makeUnique<ChromePassElement>(data));
|
||||
}
|
||||
|
||||
eDecorationType ChromeDecoration::getDecorationType() { return DECORATION_CUSTOM; }
|
||||
|
||||
void ChromeDecoration::updateWindow(PHLWINDOW window) { damageEntire(); }
|
||||
|
||||
void ChromeDecoration::damageEntire() {
|
||||
g_pHyprRenderer->damageBox(FullDecorationExtentGlobal());
|
||||
}
|
||||
|
||||
eDecorationLayer ChromeDecoration::getDecorationLayer() { return DECORATION_LAYER_OVER; }
|
||||
|
||||
uint64_t ChromeDecoration::getDecorationFlags() { return DECORATION_PART_OF_MAIN_WINDOW; }
|
||||
|
||||
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) {
|
||||
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)
|
||||
return cachedTexture;
|
||||
|
||||
const int w = static_cast<int>(sizePx.x);
|
||||
const int h = static_cast<int>(sizePx.y);
|
||||
// geo.topPad is the extra room reserved above the ring's own top edge for
|
||||
// the title bar to stick up into (see FullDecorationExtentGlobal/
|
||||
// getPositioningInfo, which grow the top side of the decoration box by
|
||||
// this same amount).
|
||||
const auto geo = ChromeDecorationGeometry::ComputeBase(sizePx, extentPx, chamferPx, titleBarHeightPx).WithTitleBarWidth(titleBarWidthPx);
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
// 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) {
|
||||
|
||||
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);
|
||||
cairo_fill(cr);
|
||||
|
||||
cairo_surface_flush(surface);
|
||||
|
||||
cachedTexture = g_pHyprRenderer->createTexture(surface);
|
||||
cachedTexSize = sizePx;
|
||||
cachedExtent = extentPx;
|
||||
cachedChamfer = chamferPx;
|
||||
cachedColorValue = colorValue;
|
||||
cachedTitleBarHeight = titleBarHeightPx;
|
||||
cachedTitleBarWidth = titleBarWidthPx;
|
||||
|
||||
cairo_destroy(cr);
|
||||
cairo_surface_destroy(surface);
|
||||
|
||||
return cachedTexture;
|
||||
}
|
||||
|
||||
SP<Render::ITexture> ChromeDecoration::GetTitleTexture(const std::string& title, float textSizePx, uint64_t colorValue, const std::string& fontFamily, int maxWidthPx, bool isActive) {
|
||||
if (title.empty())
|
||||
return nullptr;
|
||||
|
||||
if (cachedTitleTex && cachedTitleTex->ok() && cachedTitleTexTitle == title &&
|
||||
cachedTitleTexSize == textSizePx && cachedTitleTexColor == colorValue &&
|
||||
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};
|
||||
const int fontSizePt = static_cast<int>(std::max(1.F, textSizePx));
|
||||
|
||||
// Hyprland's renderText only ellipsizes a line that's also height-capped
|
||||
// (which this overload doesn't expose) - without that, text too wide for
|
||||
// maxWidthPx just wraps onto a second line instead, which pokes out of
|
||||
// the title bar's fixed height. Render unconstrained (maxWidth 0) so we
|
||||
// can measure the title's true single-line width and do the fitting
|
||||
// decision ourselves.
|
||||
const auto renderAt = [&](const std::string& text) {
|
||||
return g_pHyprRenderer->renderText(text, textColor, fontSizePt, false, fontFamily, 0, 700);
|
||||
};
|
||||
|
||||
auto tex = renderAt(title);
|
||||
if (tex && tex->ok() && static_cast<int>(tex->m_size.x) > maxWidthPx) {
|
||||
// Binary search the longest UTF-8-safe prefix whose rendering (plus an
|
||||
// ellipsis) still fits within maxWidthPx.
|
||||
const auto starts = Utf8CodepointStarts(title);
|
||||
size_t left = 0, right = starts.size() >= 2 ? starts.size() - 2 : 0;
|
||||
while (left < right) {
|
||||
const size_t mid = left + (right - left + 1) / 2;
|
||||
const auto candidate = renderAt(title.substr(0, starts[mid]) + "…");
|
||||
if (candidate && candidate->ok() && static_cast<int>(candidate->m_size.x) <= maxWidthPx)
|
||||
left = mid;
|
||||
else
|
||||
right = mid - 1;
|
||||
}
|
||||
tex = renderAt(title.substr(0, starts[left]) + "…");
|
||||
}
|
||||
|
||||
cachedTitleTex = tex;
|
||||
cachedTitleTexTitle = title;
|
||||
cachedTitleTexSize = textSizePx;
|
||||
cachedTitleTexColor = colorValue;
|
||||
cachedTitleTexFont = fontFamily;
|
||||
cachedTitleTexMaxWidth = maxWidthPx;
|
||||
cachedTitleTexActive = isActive;
|
||||
|
||||
return cachedTitleTex;
|
||||
}
|
||||
|
||||
CBox ChromeDecoration::FullDecorationExtentGlobal() {
|
||||
const auto window = windowRef.lock();
|
||||
if (!window)
|
||||
return {};
|
||||
|
||||
const auto workspace = window->m_workspace;
|
||||
const auto workspaceOffset = workspace && !window->m_pinned
|
||||
? workspace->m_renderOffset->value()
|
||||
: Vector2D();
|
||||
|
||||
const auto extent = static_cast<double>(PluginState->config.extent->value());
|
||||
const auto titlebarHeight = static_cast<double>(PluginState->config.titlebarHeight->value());
|
||||
|
||||
CBox box = {
|
||||
window->m_realPosition->value().x, window->m_realPosition->value().y,
|
||||
window->m_realSize->value().x, window->m_realSize->value().y,
|
||||
};
|
||||
|
||||
return box.addExtents({.topLeft = {extent, extent + titlebarHeight}, .bottomRight = {extent, extent}})
|
||||
.translate(workspaceOffset)
|
||||
.translate(window->m_floatingOffset);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
#pragma once
|
||||
|
||||
#define WLR_USE_UNSTABLE
|
||||
|
||||
#include "Globals.hpp"
|
||||
#include <hyprland/src/desktop/DesktopTypes.hpp>
|
||||
#include <hyprland/src/render/decorations/IHyprWindowDecoration.hpp>
|
||||
#include <hyprutils/math/Box.hpp>
|
||||
#include <hyprutils/math/Vector2D.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace Render {
|
||||
class ITexture;
|
||||
}
|
||||
|
||||
// A decoration that draws a chamfered HUD-style border frame behind each
|
||||
// window, expanding past the window's edges by `plugin:hyprchrome:extent`
|
||||
// pixels on every side. 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.
|
||||
class ChromeDecoration : public IHyprWindowDecoration {
|
||||
public:
|
||||
ChromeDecoration(PHLWINDOW window);
|
||||
virtual ~ChromeDecoration();
|
||||
|
||||
virtual SDecorationPositioningInfo getPositioningInfo();
|
||||
virtual void onPositioningReply(const SDecorationPositioningReply& reply);
|
||||
virtual void draw(PHLMONITOR monitor, float const& alpha);
|
||||
virtual eDecorationType getDecorationType();
|
||||
virtual void updateWindow(PHLWINDOW window);
|
||||
virtual void damageEntire();
|
||||
virtual eDecorationLayer getDecorationLayer();
|
||||
virtual uint64_t getDecorationFlags();
|
||||
virtual std::string getDisplayName();
|
||||
|
||||
PHLWINDOW GetOwner();
|
||||
|
||||
// Returns a cairo-rendered texture of the chamfered border frame sized to
|
||||
// `sizePx` (device pixels): a hollow ring `extentPx` thick inset from the
|
||||
// outer edge, with both the outer and inner boundaries corner-chamfered
|
||||
// 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);
|
||||
|
||||
// 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);
|
||||
|
||||
WP<ChromeDecoration> self;
|
||||
|
||||
private:
|
||||
PHLWINDOWREF windowRef;
|
||||
CBox assignedBox;
|
||||
|
||||
SP<Render::ITexture> cachedTexture;
|
||||
Vector2D cachedTexSize = {-1, -1};
|
||||
float cachedExtent = -1.F;
|
||||
float cachedChamfer = -1.F;
|
||||
uint64_t cachedColorValue = 0;
|
||||
float cachedTitleBarHeight = -1.F;
|
||||
float cachedTitleBarWidth = -1.F;
|
||||
|
||||
SP<Render::ITexture> cachedTitleTex;
|
||||
std::string cachedTitleTexTitle;
|
||||
float cachedTitleTexSize = -1.F;
|
||||
uint64_t cachedTitleTexColor = 0;
|
||||
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.
|
||||
CBox FullDecorationExtentGlobal();
|
||||
|
||||
friend class ChromePassElement;
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
#pragma once
|
||||
|
||||
#include <hyprutils/math/Vector2D.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
// hyprutils itself doesn't put Vector2D in the global namespace - the rest
|
||||
// of this codebase only sees the bare name because hyprland's own headers
|
||||
// pull in `using namespace Hyprutils::Math;` first. Don't rely on that
|
||||
// include-order accident here.
|
||||
using Vector2D = Hyprutils::Math::Vector2D;
|
||||
|
||||
struct ChromeDecorationGeometry {
|
||||
// Full width threshold below which we enter full span mode.
|
||||
static constexpr float kFullSpanThresholdPx = 250.F;
|
||||
static constexpr float kDipWidthMultiplier = 0.8F;
|
||||
|
||||
float w = 0.F;
|
||||
float h = 0.F;
|
||||
|
||||
float chamfer = 0.F; // outer corner chamfer
|
||||
float extent = 0.F; // border thickness
|
||||
float topPad = 0.F; // reserved titlebar strip height, above the ring's own top edge
|
||||
float titleBarWidth = 0.F; // x where the top-left plateau's flat run (at y=0) ends
|
||||
float dipReturnX = 0.F; // x where a partial plateau's dip returns to the normal topPad-height edge
|
||||
|
||||
|
||||
bool fullSpan = false;
|
||||
|
||||
// Title text bounding box, within the top-left plateau.
|
||||
float barX0 = 0.F;
|
||||
float barX1 = 0.F;
|
||||
float barW = 0.F;
|
||||
float barBottom = 0.F;
|
||||
|
||||
// Right/left edge inset notch.
|
||||
float insetStart = 0.F;
|
||||
float insetEnd = 0.F;
|
||||
float insetDepth = 0.F;
|
||||
float insetChamfer = 0.F;
|
||||
|
||||
// Inner boundary (window edge), inset by extent from the outer edge.
|
||||
float innerX0 = 0.F;
|
||||
float innerY0 = 0.F;
|
||||
float innerX1 = 0.F;
|
||||
float innerY1 = 0.F;
|
||||
float innerW = 0.F;
|
||||
float innerH = 0.F;
|
||||
float innerChamfer = 0.F;
|
||||
|
||||
float MinTitleBarWidth() const { return w * 0.25F; }
|
||||
|
||||
float MaxTitleBarWidth() const {
|
||||
return fullSpan ? std::max(MinTitleBarWidth(), w - chamfer - topPad) : std::max(MinTitleBarWidth(), dipReturnX * 0.8F - topPad - extent);
|
||||
}
|
||||
|
||||
// Fills in titleBarWidth/barX1/barW from the caller's desired width (e.g.
|
||||
// measured title text + padding), clamped to [MinTitleBarWidth,
|
||||
// MaxTitleBarWidth].
|
||||
ChromeDecorationGeometry WithTitleBarWidth(float titleBarWidthPx) const {
|
||||
auto g = *this;
|
||||
g.titleBarWidth = std::clamp(titleBarWidthPx, MinTitleBarWidth(), MaxTitleBarWidth());
|
||||
g.barX1 = g.titleBarWidth;
|
||||
g.barW = g.barX1 - g.barX0;
|
||||
return g;
|
||||
}
|
||||
|
||||
static ChromeDecorationGeometry ComputeBase(const Vector2D& sizePx, float extentPx, float chamferPx, float titleBarHeightPx) {
|
||||
ChromeDecorationGeometry g;
|
||||
g.w = static_cast<float>(sizePx.x);
|
||||
g.h = static_cast<float>(sizePx.y);
|
||||
|
||||
g.chamfer = std::clamp(chamferPx * 1.75F, 0.F, std::min(g.w, g.h) / 2.F);
|
||||
g.extent = std::clamp(extentPx, 0.F, std::min(g.w, g.h) / 2.F);
|
||||
g.topPad = std::clamp(titleBarHeightPx, 0.F, g.h);
|
||||
g.dipReturnX = g.w * kDipWidthMultiplier;
|
||||
g.fullSpan = g.w <= kFullSpanThresholdPx;
|
||||
|
||||
g.barX0 = g.chamfer + g.topPad;
|
||||
g.barBottom = g.topPad + g.extent;
|
||||
|
||||
g.insetStart = g.topPad + (g.h - g.topPad) * 0.25F;
|
||||
g.insetEnd = g.topPad + (g.h - g.topPad) * 0.75F;
|
||||
g.insetDepth = g.extent * 0.4F;
|
||||
g.insetChamfer = g.chamfer * 0.4F;
|
||||
|
||||
g.innerX0 = g.extent;
|
||||
g.innerY0 = g.topPad + g.extent;
|
||||
g.innerX1 = g.w - g.extent;
|
||||
g.innerY1 = g.h - g.extent;
|
||||
g.innerW = g.innerX1 - g.innerX0;
|
||||
g.innerH = g.innerY1 - g.innerY0;
|
||||
g.innerChamfer = (g.innerW > 0 && g.innerH > 0) ? std::clamp(chamferPx, 0.F, std::min(g.innerW, g.innerH) / 2.F) : 0.F;
|
||||
|
||||
return g;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,114 @@
|
||||
#include "ChromePassElement.hpp"
|
||||
#include "Globals.hpp"
|
||||
#include "ChromeDecoration.hpp"
|
||||
#include "ChromeDecorationGeometry.hpp"
|
||||
|
||||
#include <hyprland/src/Compositor.hpp>
|
||||
#include <hyprland/src/desktop/view/Window.hpp>
|
||||
#include <hyprland/src/render/Renderer.hpp>
|
||||
#include <hyprland/src/render/Texture.hpp>
|
||||
#include <hyprland/src/render/pass/TexPassElement.hpp>
|
||||
|
||||
ChromePassElement::ChromePassElement(const SData& data_) : data(data_) {}
|
||||
|
||||
std::vector<UP<IPassElement>> ChromePassElement::draw() {
|
||||
const auto monitor = g_pHyprRenderer->m_renderData.pMonitor.lock();
|
||||
if (!monitor)
|
||||
return {};
|
||||
|
||||
const auto window = data.decoration->GetOwner();
|
||||
if (!window)
|
||||
return {};
|
||||
|
||||
auto box = data.decoration->FullDecorationExtentGlobal();
|
||||
box.translate(-monitor->m_position).scale(monitor->m_scale).round();
|
||||
|
||||
if (box.w < 1 || box.h < 1)
|
||||
return {};
|
||||
|
||||
// Chamfer amount is inferred from the window's own border rounding, in
|
||||
// device pixels, so the frame's cut corners track the window's rounding
|
||||
// live (config reload, per-window rules, animations).
|
||||
const float chamferPx = window->rounding() * monitor->m_scale;
|
||||
const float extentPx = static_cast<float>(PluginState->config.extent->value()) * monitor->m_scale;
|
||||
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.
|
||||
// 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());
|
||||
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();
|
||||
|
||||
// Base geometry (everything but the title bar plateau's own width - that
|
||||
// depends on the rendered title text, which can only be measured by
|
||||
// actually rendering it). MaxTitleBarWidth() bounds that render so a long
|
||||
// title gets ellipsized rather than growing the plateau into the
|
||||
// self-intersection GetBorderTexture's cairo path would otherwise risk.
|
||||
const auto baseGeo = ChromeDecorationGeometry::ComputeBase({box.w, box.h}, extentPx, chamferPx, titleBarHeightPx);
|
||||
// Padding around the title text, inside the plateau.
|
||||
const float titlePadding = textSizePx * 1.2F;
|
||||
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)
|
||||
: nullptr;
|
||||
const float texW = (titleTex && titleTex->ok()) ? static_cast<float>(titleTex->m_size.x) : 0.F;
|
||||
|
||||
// Plateau grows to fit the measured text + padding, floored at 20% of the
|
||||
// total width regardless of how short (or absent) the title is.
|
||||
const float desiredTitleBarWidthPx = baseGeo.barX0 + texW + titlePadding * 2.F;
|
||||
const auto geo = baseGeo.WithTitleBarWidth(desiredTitleBarWidthPx);
|
||||
|
||||
// In fullSpan mode the outer path's shape is fixed regardless of text
|
||||
// width (see GetBorderTexture), so pass a value that doesn't fluctuate
|
||||
// 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);
|
||||
if (!tex || !tex->ok())
|
||||
return {};
|
||||
|
||||
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) {
|
||||
const float texH = static_cast<float>(titleTex->m_size.y);
|
||||
const float localX = geo.barX0 + titlePadding;
|
||||
const float localY = (geo.barBottom - texH) / 2.F;
|
||||
|
||||
CTexPassElement::SRenderData titleData;
|
||||
titleData.tex = titleTex;
|
||||
titleData.box = CBox{box.x + localX, box.y + localY, texW, texH};
|
||||
titleData.a = data.alpha;
|
||||
children.emplace_back(makeUnique<CTexPassElement>(titleData));
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
bool ChromePassElement::needsLiveBlur() { return false; }
|
||||
|
||||
bool ChromePassElement::needsPrecomputeBlur() { return false; }
|
||||
|
||||
std::optional<CBox> ChromePassElement::boundingBox() {
|
||||
const auto monitor = g_pHyprRenderer->m_renderData.pMonitor.lock();
|
||||
if (!monitor)
|
||||
return std::nullopt;
|
||||
|
||||
return data.decoration->FullDecorationExtentGlobal()
|
||||
.translate(-monitor->m_position)
|
||||
.expand(4);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <hyprland/src/render/pass/PassElement.hpp>
|
||||
|
||||
class ChromeDecoration;
|
||||
|
||||
// EK_CUSTOM render-pass element: blits the decoration's cairo-rendered
|
||||
// border frame texture, plus a separate title-text texture positioned over
|
||||
// the title bar plateau. Both textures are cached on ChromeDecoration.
|
||||
class ChromePassElement : public IPassElement {
|
||||
public:
|
||||
struct SData {
|
||||
ChromeDecoration* decoration = nullptr;
|
||||
float alpha = 1.F;
|
||||
};
|
||||
|
||||
ChromePassElement(const SData& data);
|
||||
virtual ~ChromePassElement() = default;
|
||||
|
||||
virtual std::vector<UP<IPassElement>> draw() override;
|
||||
virtual bool needsLiveBlur() override;
|
||||
virtual bool needsPrecomputeBlur() override;
|
||||
virtual std::optional<CBox> boundingBox() override;
|
||||
|
||||
virtual const char* passName() override { return "ChromePassElement"; }
|
||||
virtual ePassElementType type() override { return EK_CUSTOM; }
|
||||
|
||||
private:
|
||||
SData data;
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include <hyprutils/memory/SharedPtr.hpp>
|
||||
|
||||
#include <hyprland/src/config/values/types/BoolValue.hpp>
|
||||
#include <hyprland/src/config/values/types/ColorValue.hpp>
|
||||
#include <hyprland/src/config/values/types/IntValue.hpp>
|
||||
#include <hyprland/src/config/values/types/StringValue.hpp>
|
||||
#include <hyprland/src/plugins/PluginAPI.hpp>
|
||||
|
||||
using IntValue = Config::Values::CIntValue;
|
||||
using BoolValue = Config::Values::CBoolValue;
|
||||
using ColorValue = Config::Values::CColorValue;
|
||||
using StringValue = Config::Values::CStringValue;
|
||||
|
||||
// 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
|
||||
// config defaults can be written in that order.
|
||||
constexpr uint64_t RGBAToARGB(uint64_t rgba) {
|
||||
const uint64_t r = (rgba >> 24) & 0xFF;
|
||||
const uint64_t g = (rgba >> 16) & 0xFF;
|
||||
const uint64_t b = (rgba >> 8) & 0xFF;
|
||||
const uint64_t a = rgba & 0xFF;
|
||||
return (a << 24) | (r << 16) | (g << 8) | b;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include "Common.hpp"
|
||||
#include "ChromeConfig.hpp"
|
||||
#include <vector>
|
||||
|
||||
class ChromeDecoration;
|
||||
|
||||
struct GlobalState {
|
||||
ChromeConfig config;
|
||||
|
||||
std::vector<WP<ChromeDecoration>> decorations;
|
||||
|
||||
GlobalState(HANDLE plugin) {
|
||||
config = ChromeConfig{
|
||||
.enabled = makeShared<BoolValue>(
|
||||
"plugin:hyprchrome:enabled",
|
||||
"Whether the border decoration is enabled",
|
||||
true),
|
||||
.extent = makeShared<IntValue>(
|
||||
"plugin:hyprchrome:extent",
|
||||
"How far the border extends past each window edge, in pixels",
|
||||
12),
|
||||
.followHyprlandBorderColor = makeShared<BoolValue>(
|
||||
"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>(
|
||||
"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>(
|
||||
"plugin:hyprchrome:inactive_color",
|
||||
"Color of the border on unfocused windows, used when follow_hyprland_border_color is false",
|
||||
RGBAToARGB(0x6C6C6CFF)),
|
||||
.titlebarHeight = makeShared<IntValue>(
|
||||
"plugin:hyprchrome:titlebar_height",
|
||||
"Height of the title bar hanging off the floating top-border cutout, in pixels",
|
||||
8),
|
||||
.titlebarTextSize = makeShared<IntValue>(
|
||||
"plugin:hyprchrome:titlebar_text_size",
|
||||
"Font size of the title bar's text, in pixels",
|
||||
12),
|
||||
.titlebarFont = makeShared<StringValue>(
|
||||
"plugin:hyprchrome:titlebar_font",
|
||||
"Font family of the title bar's text",
|
||||
"sans-serif"),
|
||||
};
|
||||
|
||||
HyprlandAPI::addConfigValueV2(plugin, config.enabled);
|
||||
HyprlandAPI::addConfigValueV2(plugin, config.extent);
|
||||
HyprlandAPI::addConfigValueV2(plugin, config.followHyprlandBorderColor);
|
||||
HyprlandAPI::addConfigValueV2(plugin, config.activeColor);
|
||||
HyprlandAPI::addConfigValueV2(plugin, config.inactiveColor);
|
||||
HyprlandAPI::addConfigValueV2(plugin, config.titlebarHeight);
|
||||
HyprlandAPI::addConfigValueV2(plugin, config.titlebarTextSize);
|
||||
HyprlandAPI::addConfigValueV2(plugin, config.titlebarFont);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "GlobalState.hpp"
|
||||
|
||||
inline HANDLE PluginHandle = nullptr;
|
||||
inline PLUGIN_DESCRIPTION_INFO PluginInfo = {
|
||||
.name = "hypr-chrome",
|
||||
.description = "Draws a chamfered HUD-style border frame with a title bar around each window.",
|
||||
.author = "Erik",
|
||||
.version = "0.1",
|
||||
};
|
||||
|
||||
inline UP<GlobalState> PluginState;
|
||||
@@ -0,0 +1,88 @@
|
||||
#define WLR_USE_UNSTABLE
|
||||
|
||||
#include <hyprland/src/Compositor.hpp>
|
||||
#include <hyprland/src/desktop/view/Window.hpp>
|
||||
#include <hyprland/src/event/EventBus.hpp>
|
||||
#include <hyprland/src/plugins/PluginAPI.hpp>
|
||||
#include <hyprland/src/render/Renderer.hpp>
|
||||
#include <hyprland/src/render/decorations/DecorationPositioner.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "Globals.hpp"
|
||||
#include "ChromeDecoration.hpp"
|
||||
|
||||
// Do NOT change this function.
|
||||
APICALL EXPORT std::string PLUGIN_API_VERSION() { return HYPRLAND_API_VERSION; }
|
||||
|
||||
static void OnNewWindow(PHLWINDOW window) {
|
||||
if (window->m_X11DoesntWantBorders)
|
||||
return;
|
||||
|
||||
if (std::ranges::any_of(
|
||||
window->m_windowDecorations,
|
||||
[](const auto& d) { return d->getDisplayName() == "Chrome"; })) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto decoration = makeUnique<ChromeDecoration>(window);
|
||||
PluginState->decorations.emplace_back(decoration);
|
||||
decoration->self = decoration;
|
||||
HyprlandAPI::addWindowDecoration(PluginHandle, window, std::move(decoration));
|
||||
}
|
||||
|
||||
static void OnConfigReloaded() {
|
||||
for (auto& decoration : PluginState->decorations) {
|
||||
if (!decoration)
|
||||
continue;
|
||||
|
||||
// No cached rendering state to invalidate - color/extent are read live
|
||||
// from config - just re-run layout (extent may have changed) and
|
||||
// repaint.
|
||||
g_pDecorationPositioner->repositionDeco(decoration.get());
|
||||
decoration->damageEntire();
|
||||
}
|
||||
}
|
||||
|
||||
APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) {
|
||||
PluginHandle = handle;
|
||||
|
||||
const std::string hash = __hyprland_api_get_hash();
|
||||
const std::string clientHash = __hyprland_api_get_client_hash();
|
||||
|
||||
if (hash != clientHash) {
|
||||
HyprlandAPI::addNotification(
|
||||
PluginHandle,
|
||||
"[hypr-chrome] Failure in initialization: Version mismatch "
|
||||
"(headers ver is not equal to running hyprland ver)",
|
||||
CHyprColor{1.0, 0.2, 0.2, 1.0}, 5000);
|
||||
throw std::runtime_error("[hypr-chrome] Version mismatch");
|
||||
}
|
||||
|
||||
PluginState = makeUnique<GlobalState>(handle);
|
||||
|
||||
static auto onNewWindowListener = Event::bus()
|
||||
->m_events.window.open.listen([&](PHLWINDOW w) { OnNewWindow(w); });
|
||||
static auto onConfigReloadedListener = Event::bus()
|
||||
->m_events.config.reloaded.listen([&] { OnConfigReloaded(); });
|
||||
|
||||
// Attach the decoration to windows that were already open before the
|
||||
// plugin loaded.
|
||||
for (auto& window : g_pCompositor->m_windows) {
|
||||
if (window->isHidden() || !window->m_isMapped)
|
||||
continue;
|
||||
|
||||
OnNewWindow(window);
|
||||
}
|
||||
|
||||
HyprlandAPI::reloadConfig();
|
||||
|
||||
return PluginInfo;
|
||||
}
|
||||
|
||||
APICALL EXPORT void PLUGIN_EXIT() {
|
||||
for (auto& monitor : g_pCompositor->m_monitors)
|
||||
monitor->m_scheduledRecalc = true;
|
||||
|
||||
g_pHyprRenderer->m_renderPass.removeAllOfType("ChromePassElement");
|
||||
}
|
||||
Reference in New Issue
Block a user