Files
Panama/config/dot/quickshell/config/Theme.qml
T

253 lines
14 KiB
QML

pragma Singleton
// ─────────────────────────────────────────────────────────────────────────────
// Tokyo Night Moon — the single source of truth for every color, radius,
// duration and font in the shell. Nothing below this file should hardcode a
// color; if you find yourself writing "#" outside this file, add a token here.
//
// The bar keeps GNOME's 36px information density but sits directly on the
// desktop, edge to edge. The dock retains the former glass vocabulary: bottom,
// 48px icons and dot indicators.
// ─────────────────────────────────────────────────────────────────────────────
import Quickshell
// QtQuick is required even though nothing visual is declared here: `color` is a
// QtQuick value type, and Qt.rgba() lives in its JS namespace.
import QtQuick
import qs.services
Singleton {
id: root
// ── Color scheme ───────────────────────────────────────────────────────
// The scheme still exists as a boolean because alphas and a handful of
// mix directions depend on which side of the ground the theme sits — but
// the palette itself now comes from the active theme record.
//
// Every token below is a binding, so selecting a theme repaints the whole
// shell without anything needing to know it happened.
readonly property bool dark: DesktopPreferences.get("colorScheme") !== "light"
// ── Palette ─────────────────────────────────────────────────────────────
// The active theme's palette — ThemeProfiles resolves it and it is always
// complete: a shipped theme carries its own, a custom theme without one
// inherits its scheme's default (Tokyo Moon or Tokyo Day) from the
// catalog. Nothing here needs a per-token fallback.
readonly property var palette: ThemeProfiles.activePalette
readonly property color bg: root.palette.bg
readonly property color bgDark: root.palette.bgDark
readonly property color bgHighlight: root.palette.bgHighlight
readonly property color bgPanel: root.palette.bgPanel // dock glass surface
readonly property color bgPopover: root.palette.bgPopover // Openbar submenu background
readonly property color fg: root.palette.fg
readonly property color fgDim: root.palette.fgDim
readonly property color fgMuted: root.palette.fgMuted
readonly property color gutter: root.palette.gutter
// ── Bar text ────────────────────────────────────────────────────────────
// Every other surface in the shell draws on a ground the theme chose. The
// bar draws on the wallpaper, which the theme has never seen — so a
// photograph with a bright sky can leave the palette's own fg unreadable
// exactly where the clock is.
//
// Hence a second neutral family, used by the bar and nowhere else. Left
// alone it *is* the fg family, so nothing changes for anyone who never
// asks. Forced light or dark, it becomes a run that survives whatever is
// underneath it, and the two dims are mixed toward the opposite end rather
// than picked by hand, so the three stay a family either way.
readonly property string barTextTone: DesktopPreferences.get("barTextTone")
readonly property color barFg: {
if (root.barTextTone === "light")
return "#f4f6ff";
if (root.barTextTone === "dark")
return "#1b2130";
return root.fg;
}
readonly property color barFgDim: {
if (root.barTextTone === "light")
return root.mix(root.barFg, "#20242f", 0.35);
if (root.barTextTone === "dark")
return root.mix(root.barFg, "#ffffff", 0.35);
return root.fgDim;
}
readonly property color barFgMuted: {
if (root.barTextTone === "light")
return root.mix(root.barFg, "#20242f", 0.5);
if (root.barTextTone === "dark")
return root.mix(root.barFg, "#ffffff", 0.5);
return root.fgMuted;
}
// ── The accent ──────────────────────────────────────────────────────────
//
// `accent` is the primary and carries every state meaning (focused, active,
// on). `accentSecondary` never appears alone, only as the far end of a
// gradient. That restraint is the whole point: the two colors meeting is
// the signature, so the second color stops being special the moment it is
// used as a flat fill.
//
// NAMED accents rather than a free color. Each name carries a curated
// triple per scheme, because an arbitrary hex cannot work in both: a color
// legible on the Moon background is usually illegible on the Day one, and a
// picker that lets someone choose an unreadable desktop is not a feature.
// Every pair below measures at least 3:1 against the ground it sits on.
// This is also GNOME's model, which is the parity being chased.
//
// Blue is the shipped Prism -- blue leading, orchid following -- and stays
// the default.
//
// The table itself lives in services/ThemeProfileModel.js so a curated
// accent and a custom profile are the same kind of record. Its `gnome`
// member is the nearest name in GNOME's own accent-color enum, which is a
// fixed list of nine we do not get to extend; it is what libadwaita
// applications -- Files, Papers, Loupe -- are told to use, so choosing an
// accent here recolors them too instead of leaving them in GNOME blue.
readonly property var accents: ThemeProfiles.curatedAccents
readonly property var activeProfile: ThemeProfiles.activeProfile
// ThemeProfiles validates every persisted record before it can become
// active, so these bindings are both reactive and safe to expose as the
// shell-wide colour roles.
readonly property color accent: root.activeProfile.accent
readonly property color accentSecondary: root.activeProfile.secondary
readonly property color accentAlt: root.palette.accentAlt
readonly property color cyan: root.palette.cyan
readonly property color teal: root.palette.teal
readonly property color green: root.palette.green
readonly property color yellow: root.palette.yellow
readonly property color orange: root.palette.orange
readonly property color red: root.palette.red
readonly property color redDeep: root.palette.redDeep
readonly property color magenta: root.palette.magenta
readonly property color pink: root.palette.pink
// Semantic aliases — prefer these in widgets so intent survives a repaint.
readonly property color ok: green
readonly property color warn: yellow
readonly property color danger: red
readonly property color urgent: orange
// ── Surface alphas ──────────────────────────────────────────────────────
// The dock sits on compositor blur, so its fill is deliberately very
// translucent. Popovers are near-opaque because they carry text that must
// stay legible. The bar itself has no surface or alpha token.
// Light surfaces need more opacity for the same sense of a solid panel: the
// same 0.34 that reads as glass over a dark desktop reads as haze over a
// light one, and text on it stops being legible.
readonly property real dockAlpha: root.dark ? 0.34 : 0.62
readonly property real popoverAlpha: root.dark ? 0.92 : 0.97
// Toasts sit a little lighter than a popover you opened on purpose. A
// notification arrives unbidden over whatever you were doing, and at full
// popover weight it reads as a dialog demanding an answer -- but the 6%
// foreground tint it used to have left the text competing with the desktop
// behind it. This is the point between the two.
readonly property real toastAlpha: root.dark ? 0.86 : 0.94
readonly property real overlayAlpha: root.dark ? 0.55 : 0.40
readonly property real hoverAlpha: root.dark ? 0.14 : 0.10
readonly property real activeAlpha: root.dark ? 0.24 : 0.18
// ── Geometry ────────────────────────────────────────────────────────────
readonly property int barHeight: 36
readonly property int barGap: 6 // breathing room below the bar for popovers
readonly property int barSideMargin: 10 // inset for floating popovers
readonly property int dockIconSize: DesktopPreferences.get("dockIconSize")
readonly property int dockPadding: 8
readonly property int dockGap: 8
readonly property int dockRadius: 20
readonly property int popoverRadius: 18
readonly property int popoverPadding: 14
readonly property int popoverWidth: 380
readonly property int controlCenterWidth: 430
readonly property int cardRadius: 12
readonly property int pillRadius: 999
readonly property int itemSpacing: 8
readonly property int sectionSpacing: 14
// ── Type ────────────────────────────────────────────────────────────────
// Adwaita Sans for everything the user reads as text. No exceptions: a
// monospaced clock or percentage reads as a terminal readout pasted into a
// UI, which is the opposite of the intent here.
readonly property string fontFamily: DesktopPreferences.get("interfaceFont")
// Nerd Font, used ONLY to draw icon glyphs — never for text. It is the
// pragmatic alternative to freedesktop symbolic icons, which ship with a
// hardcoded dark fill Qt will not recolor (see widgets/ThemedIcon.qml).
readonly property string fontMono: DesktopPreferences.get("iconFont")
// Apply to any text whose digits change in place — clocks, percentages,
// elapsed timers, dimension readouts. Tabular figures all share one
// advance width, so the text stops twitching as the numbers tick without
// needing a monospaced face.
//
// font.family: Theme.fontFamily
// font.features: Theme.tabularFigures
readonly property var tabularFigures: ({
"tnum": 1
})
// The four sizes are a scale, not four independent numbers: they move
// together so the relationship between body, caption, heading and title
// survives a change to the base rather than drifting apart.
readonly property int fontSize: DesktopPreferences.get("interfaceFontSize")
readonly property int fontSizeSmall: Math.max(9, root.fontSize - 2)
readonly property int fontSizeLarge: root.fontSize + 3
readonly property int fontSizeTitle: root.fontSize + 7
// ── Motion ──────────────────────────────────────────────────────────────
// Event-driven only. Nothing in this shell animates while idle — no pulse,
// no shimmer, no spinners. These durations are used for open/close/hover.
//
// All of them collapse to zero when Reduce motion is on: the Accessibility
// toggle used to still the compositor's windows while the shell's own bar,
// dock and panels kept moving, which made it a half-truth. A duration of 0
// is a completed animation, so every Behavior and NumberAnimation in the
// shell obeys the switch without knowing it exists.
readonly property bool motionEnabled: Settings.animationsEnabled
readonly property int durFast: motionEnabled ? 120 : 0
readonly property int durNormal: motionEnabled ? 200 : 0
readonly property int durSlow: motionEnabled ? 320 : 0
// The dock revealing is the one animation that answers a live pointer
// movement, so it gets its own (much shorter) duration. Anything slower
// reads as the desktop lagging behind the cursor rather than as motion.
readonly property int durDockReveal: motionEnabled ? 90 : 0
// Matches the "snappy" spring curve defined in hypr/looks.lua.
readonly property list<real> easeStandard: [0.05, 0.9, 0.1, 1.0]
readonly property list<real> easeOvershoot: [0.34, 1.56, 0.64, 1.0]
// ── Helpers ─────────────────────────────────────────────────────────────
// Qt.alpha() does not exist; this is the idiomatic way to re-alpha a token.
function alpha(c: color, a: real): color {
return Qt.rgba(c.r, c.g, c.b, a);
}
// Blend two tokens — used for hover/pressed states so we never invent a
// color that isn't derived from the palette.
function mix(a: color, b: color, ratio: real): color {
return Qt.rgba(a.r + (b.r - a.r) * ratio, a.g + (b.g - a.g) * ratio, a.b + (b.b - a.b) * ratio, a.a + (b.a - a.a) * ratio);
}
// ── The prism ───────────────────────────────────────────────────────────
// Where the two accents meet. Used for the hairline along the top edge of
// every glass surface (widgets/PrismEdge.qml), the focused window border,
// the active workspace pill and slider fills.
//
// Where the gradient runs left-to-right the blue leads, because the eye
// reads left first and blue is the color that carries meaning.
readonly property real prismStart: 0.22
readonly property real prismEnd: 0.78
// Opacity of the hairline itself. Deliberately below 1: at full strength it
// reads as a drawn line rather than as light catching an edge.
readonly property real prismEdgeAlpha: 0.85
}