Files

243 lines
9.9 KiB
QML

pragma Singleton
import QtQuick
import "BorderGeometry.js" as Geometry
// Central border-spec factory for shell surfaces and controls. A spec carries
// color, optional gradient, and top/right/bottom/left widths so renderers can
// choose the cheap Rectangle path or the Shape-ring path without duplicating
// theme parsing logic.
QtObject {
id: root
function none() {
return flat("transparent", 0)
}
function flat(color, width) {
return {
color: color || "transparent",
widths: Geometry.parseWidthSpec(width, 0),
gradient: { colors: [], angle: 0, enabled: false },
}
}
function value(section, key) {
var v = Color.shellValues[section + "." + key]
return (v === undefined || v === null) ? "" : v
}
function valueOr(section, keys) {
for (var i = 0; i < keys.length; i++) {
var v = value(section, keys[i])
if (String(v).length > 0) return v
}
return ""
}
function resolveValueRef(raw) {
var s = String(raw || "").replace(/^\s+|\s+$/g, "")
var seen = {}
while (s.match(/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/) && !seen[s]) {
seen[s] = true
var next = Color.shellValues[s]
if (next === undefined || next === null || String(next).length === 0) break
s = String(next).replace(/^\s+|\s+$/g, "")
}
return s
}
function alpha(section, key, fallback) {
var raw = value(section, key)
if (String(raw).length === 0) return fallback
var n = Number(raw)
return isFinite(n) ? Geometry.clampAlpha(n) : fallback
}
function cssColor(color, opacity) {
var a = opacity === undefined || opacity === null ? 1 : Geometry.clampAlpha(opacity)
if (color && typeof color === "object" && color.r !== undefined) {
if (typeof Qt !== "undefined" && Qt.rgba) {
return Qt.rgba(color.r, color.g, color.b, (color.a === undefined ? 1 : color.a) * a)
}
return "#"
+ Geometry.padHex(color.r * 255)
+ Geometry.padHex(color.g * 255)
+ Geometry.padHex(color.b * 255)
+ Geometry.padHex((color.a === undefined ? 1 : color.a) * a * 255)
}
var s = String(color || "").replace(/^\s+|\s+$/g, "")
var role = s.toLowerCase()
if (role === "foreground" || role === "text") return cssColor(Color.foreground, a)
if (role === "accent") return cssColor(Color.accent, a)
if (role === "urgent") return cssColor(Color.urgent, a)
if (role === "background") return cssColor(Color.background, a)
if (role === "transparent") return "transparent"
return Geometry.canonicalColor(s, a)
}
function resolvedGradient(raw, fallbackColor, opacity) {
var s = String(raw || "").replace(/^\s+|\s+$/g, "")
if (s.length === 0) return { colors: [], angle: 0, enabled: false }
var parts = s.split(/\s+/)
var colors = []
var angle = 0
for (var i = 0; i < parts.length; i++) {
if (parts[i].match(/^-?\d+(?:\.\d+)?deg$/)) angle = Number(parts[i].replace(/deg$/, ""))
else colors.push(cssColor(parts[i], opacity))
}
if (colors.length === 0) colors.push(cssColor(fallbackColor, opacity))
return { colors: colors, angle: angle, enabled: colors.length > 1 }
}
function sameColor(a, b) {
if (typeof Qt === "undefined" || !Qt.color) return String(a) === String(b)
var ca = typeof a === "string" ? Qt.color(a) : a
var cb = typeof b === "string" ? Qt.color(b) : b
if (!ca || !cb || ca.r === undefined || cb.r === undefined) return String(a) === String(b)
return Math.round(ca.r * 255) === Math.round(cb.r * 255)
&& Math.round(ca.g * 255) === Math.round(cb.g * 255)
&& Math.round(ca.b * 255) === Math.round(cb.b * 255)
&& Math.round((ca.a === undefined ? 1 : ca.a) * 255) === Math.round((cb.a === undefined ? 1 : cb.a) * 255)
}
function localOrSurfaceSpec(section, token, localColor, defaultColor, fallbackWidth, alphaKey) {
if (!sameColor(localColor, defaultColor)) return flat(localColor, fallbackWidth)
return surfaceSpec(section, token, localColor, fallbackWidth, alphaKey)
}
function surfaceWidths(section, token, fallbackWidth) {
var base = valueOr(section, token === "border" ? ["border-width"] : [token + "-width", "border-width"])
var widths = Geometry.parseWidthSpec(base, fallbackWidth)
return Geometry.withSideOverrides(
widths,
valueOr(section, token === "border" ? ["border-width-top"] : [token + "-width-top", "border-width-top"]),
valueOr(section, token === "border" ? ["border-width-right"] : [token + "-width-right", "border-width-right"]),
valueOr(section, token === "border" ? ["border-width-bottom"] : [token + "-width-bottom", "border-width-bottom"]),
valueOr(section, token === "border" ? ["border-width-left"] : [token + "-width-left", "border-width-left"])
)
}
function borderValue(raw, fallbackColor, opacity, legacyGradientRaw) {
var fallback = cssColor(fallbackColor, opacity)
var primaryRaw = String(raw || "").replace(/^\s+|\s+$/g, "")
var primary = resolvedGradient(primaryRaw.length > 0 ? primaryRaw : fallbackColor, fallbackColor, opacity)
var color = primary.colors.length > 0 ? primary.colors[0] : fallback
var gradient = primary.enabled ? primary : { colors: [], angle: 0, enabled: false }
// Backward compatibility for existing configs written during the separate
// border-gradient experiment. New configs put solid colors and gradients in
// the same border token.
if (!gradient.enabled && String(legacyGradientRaw || "").replace(/^\s+|\s+$/g, "").length > 0) {
var legacy = resolvedGradient(legacyGradientRaw, color, opacity)
if (legacy.enabled) gradient = legacy
}
return { color: color, gradient: gradient }
}
function surfaceSpec(section, token, fallbackColor, fallbackWidth, alphaKey) {
var opacity = alpha(section, alphaKey || token + "-alpha", 1.0)
var legacyGradientRaw = valueOr(section, token === "border" ? ["border-gradient"] : [token + "-gradient", "border-gradient"])
var resolved = borderValue(resolveValueRef(value(section, token)), fallbackColor, opacity, legacyGradientRaw)
return {
color: resolved.color,
widths: surfaceWidths(section, token, fallbackWidth),
gradient: resolved.gradient,
}
}
function hyprlandActiveSpec(fallbackColor, fallbackWidth) {
var raw = value("hyprland", "active-border")
var opacity = alpha("hyprland", "active-border-alpha", 1.0)
// Existing generated themes predate [hyprland] and already keep the active
// border under [notifications]. Use it as the compatibility source until
// the next theme refresh writes the shared token.
if (String(raw).length === 0) {
raw = value("notifications", "border")
opacity = alpha("notifications", "border-alpha", opacity)
}
var resolved = borderValue(resolveValueRef(raw), fallbackColor, opacity, "")
return {
color: resolved.color,
widths: Geometry.parseWidthSpec(value("hyprland", "active-border-width"), fallbackWidth),
gradient: resolved.gradient,
}
}
function controlPrefix(state) {
if (state === "hover" || state === "hot") return "hover-cursor"
return state || "normal"
}
function controlColor(prefix, foreground, accent, urgent) {
if (prefix === "focus") return Style.focusStateColor(foreground, accent, urgent)
if (prefix === "hover-cursor") return Style.hoverStateColor(foreground, accent, urgent)
if (prefix === "selected") return Style.selectedStateColor(foreground, accent, urgent)
return Style.normalStateColor(foreground, accent, urgent)
}
function controlAlpha(prefix) {
if (prefix === "focus") return Style.focusBorderAlpha
if (prefix === "hover-cursor") return Style.hoverBorderAlpha
if (prefix === "selected") return Style.selectedBorderAlpha
return Style.normalBorderAlpha
}
function controlFallbackWidth(prefix) {
if (prefix === "focus") return Style.focusBorderWidth
if (prefix === "hover-cursor") return Style.hoverBorderWidth
if (prefix === "selected") return Style.selectedBorderWidth
return Style.normalBorderWidth
}
function controlWidths(state) {
var prefix = controlPrefix(state)
var fallbackWidth = controlFallbackWidth(prefix)
var base = Style.styleOverrides[prefix + "-border-width"]
var widths = Geometry.parseWidthSpec(base, fallbackWidth)
return Geometry.withSideOverrides(
widths,
Style.styleOverrides[prefix + "-border-width-top"],
Style.styleOverrides[prefix + "-border-width-right"],
Style.styleOverrides[prefix + "-border-width-bottom"],
Style.styleOverrides[prefix + "-border-width-left"]
)
}
function controlHasWidth(state) {
return Geometry.maxWidth(controlWidths(state)) > 0
}
function controlSpec(state, foreground, accent, urgent) {
var prefix = controlPrefix(state)
var resolved = borderValue(
Style.styleOverrides[prefix + "-border"],
controlColor(prefix, foreground, accent, urgent),
controlAlpha(prefix),
Style.styleOverrides[prefix + "-border-gradient"]
)
return { color: resolved.color, widths: controlWidths(prefix), gradient: resolved.gradient }
}
function withWidth(spec, width) {
if (!spec) return flat("transparent", 0)
return { color: spec.color, gradient: spec.gradient, widths: Geometry.parseWidthSpec(width, 0) }
}
function isNone(spec) { return !spec || Geometry.maxWidth(spec.widths) <= 0 }
function needsOverlay(spec) { return Geometry.needsOverlay(spec) }
function canUseNative(spec) { return Geometry.canUseNative(spec) }
function top(spec) { return spec && spec.widths ? spec.widths.top : 0 }
function right(spec) { return spec && spec.widths ? spec.widths.right : 0 }
function bottom(spec) { return spec && spec.widths ? spec.widths.bottom : 0 }
function left(spec) { return spec && spec.widths ? spec.widths.left : 0 }
function uniformWidth(spec) { return spec && spec.widths ? spec.widths.top : 0 }
function color(spec) { return spec ? spec.color : "transparent" }
}