Files
Panama/config/dot/quickshell/modules/settings/ThemeSaturationRow.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

192 lines
6.7 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Saturation, as a nudge rather than a stored number.
//
// There is no saturation preference: `setSaturation` re-saturates the palette
// that is in effect right now, which makes it compounding -- committing 1.5
// twice really is 2.25. So the row keeps the reading it has shown you and
// commits the RATIO between the old reading and the new one, which makes the
// number on screen an honest total rather than a lever that lies the second
// time it is pulled. Selecting a different theme resets the reading to neutral,
// because the new palette is a new ground.
//
// The commit happens on release only. A drag through 0.6 → 0.9 → 1.4 would
// otherwise rewrite all nineteen tokens at every pixel, and each rewrite is
// lossy: HSV saturation cannot be recovered once it has been rounded to a hex.
import QtQuick
import qs.config
import qs.services
SettingRow {
id: root
label: "Saturation"
detail: "Shifts every derived surface and text tone together"
controlWidth: 280
// 1.0 is the palette as it stands. Range 0 2, shown as 0 200%.
property real shown: 1
property real committed: 1
readonly property real maximum: 2
// A theme change replaces the ground this row was measuring against.
readonly property string activeId: ThemeProfiles.activeId
onActiveIdChanged: {
root.shown = 1;
root.committed = 1;
}
function apply(): void {
if (Math.abs(root.shown - root.committed) < 0.005)
return;
// Dividing by the last reading turns an absolute slider into the
// relative factor the service actually takes.
const factor = root.committed > 0.02 ? root.shown / root.committed : root.maximum;
if (ThemeProfiles.setSaturation(Math.max(0, Math.min(root.maximum, factor))))
root.committed = root.shown;
else
root.shown = root.committed;
}
function moveTo(ratio: real): void {
root.shown = Math.round(Math.max(0, Math.min(1, ratio)) * root.maximum * 100) / 100;
}
function step(direction: int): void {
root.moveTo((root.shown + direction * 0.05) / root.maximum);
root.apply();
}
Item {
id: control
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
width: 270
height: 32
activeFocusOnTab: true
Accessible.role: Accessible.Slider
Accessible.name: root.label
Accessible.description: Math.round(root.shown * 100) + " percent, range 0 to 200"
Accessible.focusable: true
Accessible.focused: control.activeFocus
Accessible.onIncreaseAction: root.step(1)
Accessible.onDecreaseAction: root.step(-1)
Keys.onPressed: event => {
if (event.key === Qt.Key_Left || event.key === Qt.Key_Down) {
root.step(-1);
event.accepted = true;
} else if (event.key === Qt.Key_Right || event.key === Qt.Key_Up) {
root.step(1);
event.accepted = true;
} else if (event.key === Qt.Key_Home) {
root.moveTo(0);
root.apply();
event.accepted = true;
} else if (event.key === Qt.Key_End) {
root.moveTo(1);
root.apply();
event.accepted = true;
}
}
Rectangle {
id: frame
anchors.left: parent.left
anchors.right: readout.left
anchors.rightMargin: 10
anchors.verticalCenter: parent.verticalCenter
height: 24
radius: 9
color: "transparent"
border.width: control.activeFocus ? 2 : 1
border.color: control.activeFocus
? Theme.accentSecondary : Theme.alpha(Theme.fg, 0.08)
// Written out rather than reusing ValueSlider: this row needs the
// release, and ValueSlider reports only movement.
Rectangle {
id: track
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: 4
height: 10
radius: 5
color: Theme.alpha(Theme.fg, 0.12)
border.width: 0
Rectangle {
id: fill
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
width: parent.width * Math.max(0, Math.min(1, root.shown / root.maximum))
radius: parent.radius
border.width: 0
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop { position: 0.0; color: Theme.accent }
GradientStop { position: 1.0; color: Theme.accentSecondary }
}
}
Rectangle {
width: 16
height: 16
radius: 8
border.width: 0
color: Theme.fg
anchors.verticalCenter: parent.verticalCenter
x: Math.max(0, Math.min(parent.width - width, fill.width - width / 2))
visible: drag.containsMouse || drag.pressed
}
MouseArea {
id: drag
anchors.fill: parent
anchors.margins: -8
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
function ratioAt(mouseX: real): real {
return (mouseX - 8) / track.width;
}
onPressed: event => root.moveTo(drag.ratioAt(event.x))
onPositionChanged: event => {
if (drag.pressed)
root.moveTo(drag.ratioAt(event.x));
}
onReleased: root.apply()
onWheel: event => {
root.moveTo((root.shown + (event.angleDelta.y > 0 ? 0.05 : -0.05))
/ root.maximum);
root.apply();
}
}
}
}
Text {
id: readout
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
width: 56
horizontalAlignment: Text.AlignRight
text: Math.round(root.shown * 100) + "%"
color: Theme.fgDim
font.family: Theme.fontFamily
font.features: Theme.tabularFigures
font.pixelSize: Theme.fontSizeSmall
}
}
}