Files
Panama/config/dot/quickshell/modules/settings/ThemeCard.qml
T
Gabriel Brown cb7c09d208 Give the desktop real themes, video wallpapers, and honest titlebars
Appearance now opens on Themes: light and dark side by side, each
remembering its own choice, over galleries of ten shipped themes —
Tokyo Moon and Day joined by Moon Rose, Catppuccin, Nord, Gruvbox and
Everforest in both modes. A theme is a complete palette: the catalog
lives in themes.json, Theme.qml reads every color token from the
active record, and one render pipeline carries it to kitty, tmux,
btop, GTK, Vicinae, Firefox's chrome, and the lock screen. The Theme
editor builds new ones from four wells — wheel, hex, or eyedropper —
with derived surfaces, a saturation slider, debounced fine-tune, and
effects that save with the theme. Custom edits finally keep GNOME's
accent, kitty's border, and hyprlock in sync.

Wallpapers can be video: mpvpaper per output, hardware-decoded, muted
and looped, supervised and respawned. Panama owns the pausing — games,
battery, and a bar pill for right now — because the compositor
rebuilds full-screen blur for every frame a video wallpaper draws.
The lock screen gets a still frame.

Titlebars stop lying. GNOME apps get close-only on your chosen side,
the maximize and double-click settings are gone, the Settings window
obeys the same rules, and its titlebar can be turned off entirely.
Typography becomes five labeled dropdowns instead of a wall of
samples.

Contracts updated and written throughout (165 now); per the redesign
workflow none were executed — the full sweep runs once at the end.

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-23 23:39:04 -04:00

208 lines
6.6 KiB
QML

// One theme, drawn as the desktop it produces.
//
// A row of flat swatches would name a theme's colors without showing what any
// of them does. So the card is a miniature of the real thing: the ground, a
// window sitting on it wearing the accent as a border, the two text weights
// the shell actually reads with, and only then the four remaining colors as
// dots. Clicking applies it immediately -- there is no preview mode here,
// because the whole shell is the preview.
import QtQuick
import qs.config
import qs.services
Rectangle {
id: root
// A catalog record or a saved profile -- the same shape either way.
required property var theme
signal chosen
signal removed
// A custom profile saved before palettes existed carries none of its own
// and inherits its scheme's default, exactly as ThemeProfiles resolves it.
readonly property var palette: root.theme.palette
?? ThemeCatalog.defaultPalette(root.theme.scheme)
readonly property bool active: root.theme.id === ThemeProfiles.activeId
readonly property bool shipped: root.theme.shipped === true
readonly property bool isDefault: root.theme.id === (root.theme.scheme === "light"
? ThemeCatalog.defaultLight : ThemeCatalog.defaultDark)
implicitHeight: preview.height + caption.height + 4
radius: Theme.cardRadius + 2
color: Theme.alpha(Theme.fg, 0.04)
border.width: 2
border.color: root.activeFocus
? Theme.accentSecondary
: (root.active ? Theme.accent : Theme.alpha(Theme.fg, 0.12))
clip: true
activeFocusOnTab: true
Accessible.role: Accessible.Button
Accessible.name: root.theme.name
Accessible.description: (root.theme.scheme === "light" ? "Light theme" : "Dark theme")
+ (root.active ? ", selected" : "")
Accessible.focusable: true
Accessible.focused: root.activeFocus
Keys.onReturnPressed: root.chosen()
Keys.onSpacePressed: root.chosen()
Rectangle {
id: preview
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 2
height: 84
color: root.palette.bg
border.width: 0
Rectangle {
anchors.fill: parent
anchors.margins: 11
radius: 7
color: root.palette.bgPanel
border.width: 2
border.color: root.theme.accent
Column {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 8
spacing: 7
Rectangle {
width: parent.width * 0.64
height: 4
radius: 2
color: root.palette.fg
border.width: 0
}
Row {
spacing: 4
Repeater {
model: [root.theme.accent, root.theme.secondary,
root.palette.fgDim, root.palette.fgMuted]
Rectangle {
required property var modelData
width: 9
height: 9
radius: 5
color: modelData
border.width: 0
}
}
}
}
}
}
Item {
id: caption
anchors.left: parent.left
anchors.right: parent.right
anchors.top: preview.bottom
anchors.leftMargin: 12
anchors.rightMargin: 10
height: 30
Text {
id: name
anchors.left: parent.left
anchors.right: marks.left
anchors.rightMargin: 6
anchors.verticalCenter: parent.verticalCenter
text: root.theme.name + (root.isDefault ? " · today's default" : "")
color: Theme.fg
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.DemiBold
elide: Text.ElideRight
}
Row {
id: marks
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 6
Text {
anchors.verticalCenter: parent.verticalCenter
visible: root.active
text: "✓"
color: Theme.accent
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
font.weight: Font.DemiBold
}
// Only saved themes can be removed, and the affordance is a
// separate focus stop so Tab never lands on "delete" when the
// person meant "select".
Rectangle {
id: remove
anchors.verticalCenter: parent.verticalCenter
visible: !root.shipped
width: 22
height: 22
radius: 7
color: removeHover.hovered
? Theme.alpha(Theme.danger, 0.22)
: Theme.alpha(Theme.fg, 0.07)
border.width: remove.activeFocus ? 2 : 1
border.color: remove.activeFocus
? Theme.danger : Theme.alpha(Theme.fg, 0.08)
activeFocusOnTab: visible
Accessible.role: Accessible.Button
Accessible.name: "Delete " + root.theme.name
Accessible.focusable: true
Accessible.focused: remove.activeFocus
Keys.onReturnPressed: root.removed()
Keys.onSpacePressed: root.removed()
Text {
anchors.centerIn: parent
text: "✕"
color: Theme.danger
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
HoverHandler {
id: removeHover
cursorShape: Qt.PointingHandCursor
}
// An exclusive grab, so the delete press does not also reach
// the card's own tap handler underneath and select the theme
// on its way out.
TapHandler {
gesturePolicy: TapHandler.ReleaseWithinBounds
onTapped: root.removed()
}
}
}
}
HoverHandler {
id: cardHover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: root.chosen()
}
}