// 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 colour 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 colour 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" Text { width: parent.width visible: wells.lastError !== "" || ColorScheme.lastError !== "" text: wells.lastError !== "" ? wells.lastError : ColorScheme.lastError color: Theme.warn font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall wrapMode: Text.WordWrap bottomPadding: 10 } 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 apps 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 } } }