Files
Panama/config/dot/quickshell/modules/settings/AppearancePage.qml
T

579 lines
20 KiB
QML

// Appearance — the page the rest of the settings vocabulary was built for.
//
// Before Stage 3 this page adjusted a clock format and three vitals toggles,
// and its "Theme" card was two rows of text pretending to be controls. It now
// drives the compositor directly: every row here is applied through
// `hyprctl eval` and confirmed by reading the value back before it is stored.
//
// The preview is pinned above the controls rather than sitting inline, because
// the numbers are meaningless on their own -- "outer gaps 24" only means
// something once you have watched the windows move apart by that much, at the
// scale of the display you actually use.
import QtQuick
import qs.config
import qs.services
SettingsPage {
id: root
property string expandedPicker: ""
// Whether the accent's HSV fine-tune is open. Collapsed by default: it is
// six sliders that answer a question the four color wells above have
// usually already answered.
property bool fineTuneOpen: false
// Which group of cards is on screen. Themes leads deliberately: light and
// dark is the control reached most often, and it used to be the third
// section down, below a wallpaper grid and the whole lock screen.
property string tab: "themes"
// Arriving from another page that named a section opens on it. Taken once
// rather than bound, so the tabs still work normally afterwards. The old
// `theme` id still resolves: deep links, IPC calls and muscle memory all
// hold it, and it means the same thing this tab does.
Component.onCompleted: {
const section = ShellState.takeSettingsSection();
if (section === "theme")
root.tab = "themes";
else if (section !== "")
root.tab = section;
}
title: "Appearance"
lede: "Tune the Prism shell and the applications that live inside it. The preview above is your real geometry, to scale."
header: Component {
Column {
spacing: 9
DesktopPreview {
width: parent.width
}
Text {
width: parent.width
horizontalAlignment: Text.AlignHCenter
text: SystemSettings.lastError !== ""
? SystemSettings.lastError
: "Live preview — the focused window wears the prism border"
color: SystemSettings.lastError !== "" ? Theme.warn : Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
elide: Text.ElideRight
}
}
}
SettingsTabs {
tabs: [
{ value: "themes", label: "Themes" },
{ value: "editor", label: "Theme editor" },
{ value: "background", label: "Background" },
{ value: "type", label: "Typography" },
{ value: "windows", label: "Windows" },
]
current: root.tab
onSelected: value => root.tab = value
}
SettingsCard {
visible: root.tab === "background"
title: "Background"
subtitle: Wallpaper.lastError !== ""
? Wallpaper.lastError
: "Applied to every display, from the folder below."
TextEntryRow { setting: "wallpaperDir"; placeholder: "Pictures/Wallpapers" }
WallpaperControls {
id: wallpaperControls
width: parent.width
}
WallpaperPicker {
id: wallpapers
width: parent.width
mode: wallpaperControls.mode
selectedOutput: wallpaperControls.selectedOutput
}
ActionRow {
visible: wallpapers.hidden > 0 || wallpapers.expanded
label: wallpapers.expanded ? "Showing every image" : wallpapers.hidden + " more available"
detail: "The grid is kept short so the rest of this page stays reachable"
action: wallpapers.expanded ? "Show fewer" : "Show all"
onTriggered: wallpapers.expanded = !wallpapers.expanded
}
ActionRow {
label: "Look for new images"
detail: Wallpaper.scanning
? "Scanning…"
: Wallpaper.available.length + " image" + (Wallpaper.available.length === 1 ? "" : "s") + " found"
action: "Rescan"
divider: false
enabled: !Wallpaper.scanning
onTriggered: Wallpaper.rescan()
}
}
SettingsCard {
visible: root.tab === "background"
title: "Video playback"
subtitle: VideoWallpaper.lastError !== ""
? VideoWallpaper.lastError
: "Videos loop muted and decode on the GPU. Considerate by default — a video wallpaper should never cost you a frame you care about."
TextEntryRow { setting: "videoWallpaperDir"; placeholder: "Videos/Wallpapers" }
ToggleRow { setting: "videoWallpaperPauseOnBattery" }
TextRow {
label: "During games and fullscreen"
detail: "Pauses automatically — not optional, and free"
value: "Automatic"
}
TextRow {
label: "Pause from the bar"
detail: "While a video plays, a pill sits in the bar — one click pauses it, for remote desktop or just quiet"
value: ""
}
TextRow {
label: "Lock screen"
detail: "Uses a still frame of the video — hyprlock cannot play motion"
value: "Still frame"
}
ActionRow {
label: "Look for new videos"
detail: VideoWallpaper.available
? VideoWallpaper.candidates.length + " video" + (VideoWallpaper.candidates.length === 1 ? "" : "s") + " found"
: "mpvpaper is not installed — run panama update to pick it up"
action: "Rescan"
divider: false
enabled: VideoWallpaper.available
onTriggered: VideoWallpaper.rescan()
}
}
SettingsCard {
visible: root.tab === "background"
title: "Lock screen"
subtitle: LockScreen.lastError !== ""
? LockScreen.lastError
: "A representative preview of the screen shown before authentication."
LockScreenPreview {
width: parent.width
}
ChoiceRow { setting: "lockBackgroundMode" }
SliderRow { setting: "lockBlurLevel"; zeroLabel: "Off" }
ToggleRow { setting: "lockShowClock" }
ToggleRow { setting: "lockShowDate" }
ToggleRow { setting: "lockShowUser" }
ToggleRow { setting: "lockFadeOnEmpty"; divider: false }
}
// ── Themes ──────────────────────────────────────────────────────────────
//
// The gallery is the page, not a card on it. Cards would put a heading and
// a border around each of two lists that are already visually separate, and
// the theme cards themselves are the only surfaces here that need edges.
ThemeModeHero {
visible: root.tab === "themes"
width: parent.width
}
Text {
visible: root.tab === "themes" && ThemeCatalog.lastError !== ""
width: parent.width
text: ThemeCatalog.lastError
color: Theme.warn
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
wrapMode: Text.WordWrap
}
ThemeGallery {
visible: root.tab === "themes"
width: parent.width
scheme: "dark"
heading: "Dark themes"
}
ThemeGallery {
visible: root.tab === "themes"
width: parent.width
scheme: "light"
heading: "Light themes"
}
// Dashed rather than solid, and the width of the page: this is a door out
// of the gallery, not an eleventh theme in it.
Rectangle {
id: buildLink
visible: root.tab === "themes"
width: parent.width
implicitHeight: buildCopy.implicitHeight + 30
radius: Theme.cardRadius + 2
color: Theme.alpha(Theme.accent, buildHover.hovered ? 0.12 : 0.07)
border.width: buildLink.activeFocus ? 2 : 1
border.color: buildLink.activeFocus
? Theme.accentSecondary : Theme.alpha(Theme.accent, 0.35)
activeFocusOnTab: true
Accessible.role: Accessible.Button
Accessible.name: "Build your own theme"
Accessible.description: "Opens the theme editor"
Accessible.focusable: true
Accessible.focused: buildLink.activeFocus
Keys.onReturnPressed: root.tab = "editor"
Keys.onSpacePressed: root.tab = "editor"
Text {
id: buildGlyph
anchors.left: parent.left
anchors.leftMargin: 17
anchors.verticalCenter: parent.verticalCenter
text: "◐"
color: Theme.accent
font.family: Theme.fontFamily
font.pixelSize: 20
}
Column {
id: buildCopy
anchors.left: buildGlyph.right
anchors.leftMargin: 13
anchors.right: buildArrow.left
anchors.rightMargin: 13
anchors.verticalCenter: parent.verticalCenter
spacing: 3
Text {
width: parent.width
text: "Build your own theme"
color: Theme.fg
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
font.weight: Font.DemiBold
}
Text {
width: parent.width
text: "Primary, secondary, background, foreground, effects — every color yours, saved as a theme that lives in the galleries above"
color: Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
wrapMode: Text.WordWrap
}
}
Text {
id: buildArrow
anchors.right: parent.right
anchors.rightMargin: 17
anchors.verticalCenter: parent.verticalCenter
text: "→"
color: Theme.accent
font.family: Theme.fontFamily
font.pixelSize: 17
}
HoverHandler {
id: buildHover
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: root.tab = "editor"
}
}
// ── Theme editor ────────────────────────────────────────────────────────
SettingsCard {
id: editorCard
visible: root.tab === "editor"
title: "Your theme"
// The identity of what is being edited, always. Failures get their own
// line below rather than displacing it -- a subtitle that sometimes
// says which theme you are editing and sometimes says something went
// wrong answers neither question reliably.
subtitle: ThemeProfiles.activeProfile.shipped === true
? ThemeProfiles.activeProfile.name
+ " — editing it forks a copy you can name below"
: "Based on " + ThemeProfiles.activeProfile.name
+ " · every change previews live on the real desktop"
ErrorRow {
label: "The theme needs attention"
message: wells.lastError !== "" ? wells.lastError : ColorScheme.lastError
divider: true
}
ThemeStartChips {
width: parent.width
bottomPadding: 13
}
SegmentRow {
label: "Scheme"
detail: "Flipping sides returns to the theme you last chose there"
options: [{ value: "dark", label: "Dark" }, { value: "light", label: "Light" }]
value: ThemeProfiles.scheme
onSelected: value => ThemeProfiles.setScheme(value)
}
Item { width: 1; height: 13 }
ThemeEditorWells {
id: wells
width: parent.width
}
Item { width: 1; height: 13 }
ThemeSaturationRow {
width: parent.width
}
ActionRow {
label: "Fine-tune"
detail: "Hue, saturation and value for each end of the accent"
action: root.fineTuneOpen ? "Close" : "Open"
onTriggered: root.fineTuneOpen = !root.fineTuneOpen
}
AccentEditor {
visible: root.fineTuneOpen
width: parent.width
}
ThemeSaveRow {
width: parent.width
}
}
// One selector for every font. The two always-open lists and the
// open-one-at-a-time disclosure both retire: a closed dropdown per row
// reads as five choices instead of a wall of samples, and each row names
// what it actually controls — the old tab never said which font was the
// shell's and which was the applications'.
SettingsCard {
visible: root.tab === "type"
title: "Fonts"
subtitle: {
if (Fonts.lastError !== "")
return Fonts.lastError;
if (DesktopStyle.lastError !== "")
return DesktopStyle.lastError;
return "Each row names what it controls. The menu shows every family in its own face — type to filter.";
}
SettingRow {
label: "Interface"
detail: Fonts.interfaceMissing
? "Not installed — fontconfig is substituting something else"
: "The shell — bar, dock, notifications, this window"
divider: false
controlWidth: 8
}
FontPicker {
width: parent.width
families: Fonts.interfaceFonts
current: Fonts.interfaceFont
emptyText: Fonts.scanning ? "Reading installed fonts…" : "No interface fonts found"
onPicked: family => Fonts.setInterface(family)
}
SettingRow {
label: "Icons"
detail: Fonts.iconMissing
? "Not installed — the shell's glyphs will not draw correctly"
: "Glyphs only, never text — needs a Nerd Font"
divider: false
controlWidth: 8
}
FontPicker {
width: parent.width
families: Fonts.iconFonts
current: Fonts.iconFont
emptyText: "No Nerd Fonts installed"
onPicked: family => Fonts.setIcon(family)
}
SettingRow {
label: "Application"
detail: "GTK and libadwaita applications that follow the desktop default"
divider: false
controlWidth: 8
}
FontPicker {
width: parent.width
families: Fonts.interfaceFonts
current: DesktopStyle.applicationFont
emptyText: Fonts.scanning ? "Reading installed fonts…" : "No application fonts found"
onPicked: family => DesktopStyle.setApplicationFont(family)
}
SettingRow {
label: "Document"
detail: "Long-form reading, where applications ask for it"
divider: false
controlWidth: 8
}
FontPicker {
width: parent.width
families: Fonts.interfaceFonts
current: DesktopStyle.documentFont
emptyText: Fonts.scanning ? "Reading installed fonts…" : "No document fonts found"
onPicked: family => DesktopStyle.setDocumentFont(family)
}
SettingRow {
label: "Monospace"
detail: "Terminals and code, wherever fixed width is asked for"
divider: false
controlWidth: 8
}
FontPicker {
width: parent.width
families: Fonts.monospaceFonts
current: DesktopStyle.monospaceFont
emptyText: Fonts.scanning ? "Reading installed fonts…" : "No monospace fonts found"
onPicked: family => DesktopStyle.setMonospaceFont(family)
}
}
SettingsCard {
visible: root.tab === "type"
title: "Sizes"
subtitle: "The interface size moves the shell's whole scale; the rest are per role."
SliderRow { setting: "interfaceFontSize" }
SliderRow { setting: "applicationFontSize" }
SliderRow { setting: "documentFontSize" }
SliderRow { setting: "monospaceFontSize"; divider: false }
}
SettingsCard {
visible: root.tab === "type"
title: "Rendering"
ChoiceRow { setting: "fontHinting" }
ChoiceRow { setting: "fontAntialiasing"; divider: false }
}
SettingsCard {
visible: root.tab === "type"
title: "Icons & pointer"
subtitle: DesktopStyle.lastError !== ""
? DesktopStyle.lastError
: "Installed themes only. The pointer updates in applications and Hyprland together."
ActionRow {
label: "Application icons"
detail: DesktopStyle.iconTheme
action: root.expandedPicker === "icon-theme" ? "Close" : "Choose"
enabled: DesktopStyle.catalogLoaded
onTriggered: root.expandedPicker = root.expandedPicker === "icon-theme" ? "" : "icon-theme"
}
SearchPicker {
visible: root.expandedPicker === "icon-theme"
width: parent.width
items: DesktopStyle.iconThemes
current: DesktopStyle.iconTheme
placeholder: "Search icon themes"
emptyText: DesktopStyle.scanning ? "Reading installed icon themes…" : "No icon themes found"
onPicked: value => {
if (DesktopStyle.setIconTheme(value))
root.expandedPicker = "";
}
}
ActionRow {
label: "Pointer theme"
detail: DesktopStyle.cursorTheme
action: root.expandedPicker === "cursor-theme" ? "Close" : "Choose"
enabled: DesktopStyle.catalogLoaded
divider: false
onTriggered: root.expandedPicker = root.expandedPicker === "cursor-theme" ? "" : "cursor-theme"
}
SearchPicker {
visible: root.expandedPicker === "cursor-theme"
width: parent.width
items: DesktopStyle.cursorThemes
current: DesktopStyle.cursorTheme
placeholder: "Search pointer themes"
emptyText: DesktopStyle.scanning ? "Reading installed pointer themes…" : "No pointer themes found"
onPicked: value => {
if (DesktopStyle.setCursorTheme(value))
root.expandedPicker = "";
}
}
}
SettingsCard {
visible: root.tab === "windows"
title: "Titlebars"
subtitle: "One rule for GNOME applications and Panama's own windows. There is no minimize or maximize toggle: Hyprland has no minimize, so the buttons would be lies."
ToggleRow { setting: "panamaTitlebar" }
ChoiceRow { setting: "titlebarButtonSide"; divider: false }
}
SettingsCard {
visible: root.tab === "windows"
title: "Windows"
subtitle: "Spacing and shape of tiled windows. Each change is applied to the compositor and confirmed before it is saved."
SliderRow { setting: "windowRounding" }
SliderRow { setting: "gapsIn" }
SliderRow { setting: "gapsOut" }
SliderRow { setting: "borderSize"; zeroLabel: "None" }
SliderRow { setting: "roundingPower" }
SliderRow { setting: "inactiveOpacity" }
SliderRow { setting: "activeOpacity" }
SliderRow { setting: "fullscreenOpacity"; divider: false }
}
// Effects belong to the theme now: saving snapshots all ten values into the
// record, and applying a theme that carries them puts them back. They sit
// in the editor rather than beside the galleries for exactly that reason.
SettingsCard {
visible: root.tab === "editor"
title: "Effects"
subtitle: "Part of your theme — corners, blur, shadows, glow and motion save with it. Each costs frame time; turning one off is a legitimate way to buy it back while gaming."
ToggleRow { setting: "blurEnabled" }
SliderRow { setting: "blurSize" }
SliderRow { setting: "blurPasses" }
ToggleRow { setting: "shadowEnabled" }
SliderRow { setting: "shadowRange"; zeroLabel: "None" }
SliderRow { setting: "shadowRenderPower" }
ToggleRow { setting: "shadowSharp" }
ToggleRow { setting: "glowEnabled" }
SliderRow { setting: "glowRange"; zeroLabel: "None" }
ToggleRow { setting: "animationsEnabled"; divider: false }
}
}