Very basic theming support

This commit is contained in:
quadbyte 2025-08-09 12:53:10 -04:00
parent b08944988d
commit 821b4e8cad
6 changed files with 309 additions and 58 deletions

View file

@ -1,5 +1,7 @@
import QtQuick
import Quickshell
import qs.Widgets
import qs.Theme
PanelWindow {
id: root
@ -21,8 +23,11 @@ PanelWindow {
Rectangle {
anchors.fill: parent
color: "purple"
color: Theme.backgroundPrimary
layer.enabled: true
}
// Just testing
NoctaliaToggle {}
}
}

27
Services/Settings.qml Normal file
View file

@ -0,0 +1,27 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Services
Singleton {
id: root
property string shellName: "Noctalia"
property string settingsDir: Quickshell.env("NOCTALIA_SETTINGS_DIR")
|| (Quickshell.env("XDG_CONFIG_HOME")
|| Quickshell.env(
"HOME") + "/.config") + "/" + shellName + "/"
property string settingsFile: Quickshell.env("NOCTALIA_SETTINGS_FILE")
|| (settingsDir + "Settings.json")
property string themeFile: Quickshell.env("NOCTALIA_THEME_FILE")
|| (settingsDir + "Theme.json")
Item {
Component.onCompleted: {
// ensure settings dir
Quickshell.execDetached(["mkdir", "-p", settingsDir])
}
}
}

View file

@ -1,4 +1,5 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
@ -6,6 +7,7 @@ import Quickshell.Io
Singleton {
id: root
/*
Preset sizes for font, radii, ?
*/
@ -42,4 +44,3 @@ Singleton {
property int animationNormal: 300
property int animationSlow: 500
}

143
Theme/Theme.qml Normal file
View file

@ -0,0 +1,143 @@
// Theme.qml
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Services
Singleton {
id: root
// Backgrounds
property color backgroundPrimary: themeData.backgroundPrimary
property color backgroundSecondary: themeData.backgroundSecondary
property color backgroundTertiary: themeData.backgroundTertiary
// Surfaces & Elevation
property color surface: themeData.surface
property color surfaceVariant: themeData.surfaceVariant
// Text Colors
property color textPrimary: themeData.textPrimary
property color textSecondary: themeData.textSecondary
property color textDisabled: themeData.textDisabled
// Accent Colors
property color accentPrimary: themeData.accentPrimary
property color accentSecondary: themeData.accentSecondary
property color accentTertiary: themeData.accentTertiary
// Error/Warning
property color error: themeData.error
property color warning: themeData.warning
// Highlights & Focus
property color highlight: themeData.highlight
property color rippleEffect: themeData.rippleEffect
// Additional Theme Properties
property color onAccent: themeData.onAccent
property color outline: themeData.outline
// Shadows & Overlays
property color shadow: applyOpacity(themeData.shadow, "B3")
property color overlay: applyOpacity(themeData.overlay, "66")
// Font Properties
property string fontFamily: "Roboto" // Family for all text
// Design reference resolution (for scale = 1.0)
readonly property int designScreenWidth: 2560
readonly property int designScreenHeight: 1440
// Automatic, orientation-agnostic scaling
function scale(currentScreen) {
return 1.0
// // 1) Per-monitor override wins
// try {
// const overrides = Settings.settings.monitorScaleOverrides || {};
// if (currentScreen && currentScreen.name && overrides[currentScreen.name] !== undefined) {
// const overrideValue = overrides[currentScreen.name]
// if (isFinite(overrideValue)) return overrideValue
// }
// } catch (e) {
// // ignore
// }
// // 2) Fallback: scale by diagonal pixel count relative to design resolution
// try {
// const w = Math.max(1, currentScreen ? (currentScreen.width || 0) : 0)
// const h = Math.max(1, currentScreen ? (currentScreen.height || 0) : 0)
// if (w > 1 && h > 1) {
// const diag = Math.sqrt(w * w + h * h)
// const baseDiag = Math.sqrt(designScreenWidth * designScreenWidth + designScreenHeight * designScreenHeight)
// const ratio = diag / baseDiag
// // Clamp to a reasonable range for UI legibility
// return Math.max(0.9, Math.min(1.6, ratio))
// }
// } catch (e) {
// // ignore and fall through
// }
// // 3) Safe default
// return 1.0
}
function applyOpacity(color, opacity) {
return color.replace("#", "#" + opacity)
}
// FileView to load theme data from JSON file
FileView {
id: themeFile
path: Settings.themeFile
watchChanges: true
onFileChanged: reload()
onAdapterUpdated: writeAdapter()
onLoadFailed: function (error) {
if (error.toString().includes("No such file") || error === 2) {
// File doesn't exist, create it with default values
writeAdapter()
}
}
JsonAdapter {
id: themeData
// Backgrounds
property string backgroundPrimary: "#191724"
property string backgroundSecondary: "#1f1d2e"
property string backgroundTertiary: "#26233a"
// Surfaces & Elevation
property string surface: "#1f1d2e"
property string surfaceVariant: "#37354c"
// Text Colors
property string textPrimary: "#e0def4"
property string textSecondary: "#908caa"
property string textDisabled: "#6e6a86"
// Accent Colors
property string accentPrimary: "#ebbcba"
property string accentSecondary: "#31748f"
property string accentTertiary: "#9ccfd8"
// Error/Warning
property string error: "#eb6f92"
property string warning: "#f6c177"
// Highlights & Focus
property string highlight: "#c4a7e7"
property string rippleEffect: "#9ccfd8"
// Additional Theme Properties
property string onAccent: "#191724"
property string outline: "#44415a"
// Shadows & Overlays
property string shadow: "#191724"
property string overlay: "#191724"
}
}
}

View file

@ -0,0 +1,76 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Theme
RowLayout {
id: root
// Local scale convenience with safe fallback
readonly property real scale: (typeof screen !== 'undefined'
&& screen) ? Theme.scale(screen) : 1.0
property string label: ""
property string description: ""
property bool value: false
property var onToggled: function () {}
Layout.fillWidth: true
ColumnLayout {
spacing: 4 * scale
Layout.fillWidth: true
Text {
text: label
font.pixelSize: 13 * scale
font.bold: true
color: Theme.textPrimary
}
Text {
text: description
font.pixelSize: 12 * scale
color: Theme.textSecondary
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
Rectangle {
id: switcher
width: 52 * scale
height: 32 * scale
radius: height / 2
color: value ? Theme.accentPrimary : Theme.surfaceVariant
border.color: value ? Theme.accentPrimary : Theme.outline
border.width: 2 * scale
Rectangle {
width: 28 * scale
height: 28 * scale
radius: height / 2
color: Theme.surface
border.color: Theme.outline
border.width: 1 * scale
y: 2 * scale
x: value ? switcher.width - width - 2 * scale : 2 * scale
Behavior on x {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
root.onToggled()
}
}
}
}

View file

@ -1,9 +1,10 @@
/*
Here we go, this is it. Rebuild time...
No spaghetti code, preset sizing, proper project structure
only "spawn" UI, do not do anything else here.
*/
import QtQuick
import Quickshell
import Quickshell.Io
@ -19,7 +20,5 @@ ShellRoot {
delegate: Bar {
modelData: item
}
}
}