From d0db9a14d7a8047a0c304a9ee5b26637a6d4f4c9 Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Sat, 22 Nov 2025 17:33:08 +0100 Subject: [PATCH] dynamic color scheme progress --- .gitignore | 1 + Bar.qml | 14 +- Clock.qml | 8 +- Config/Config.qml | 2 + Config/DynamicColors.qml | 217 ++++++++++++++++++++++++++++++ Modules/AudioWidget.qml | 32 +++-- Modules/CAnim.qml | 8 ++ Modules/CustomTextField.qml | 3 + Modules/Resource.qml | 15 ++- Modules/Resources.qml | 23 +++- Modules/TrayItem.qml | 3 + Modules/UpdatesWidget.qml | 17 ++- Modules/WindowTitle.qml | 14 +- Modules/Workspaces.qml | 28 ++-- Plugins/ZShell/CMakeLists.txt | 1 + Plugins/ZShell/imageanalyser.cpp | 223 +++++++++++++++++++++++++++++++ Plugins/ZShell/imageanalyser.hpp | 61 +++++++++ Time.qml | 1 - image_colors.json | 56 ++++++++ scripts/SchemeColorGen.py | 102 ++++++++++++++ 20 files changed, 794 insertions(+), 35 deletions(-) create mode 100644 Config/DynamicColors.qml create mode 100644 Modules/CAnim.qml create mode 100644 Plugins/ZShell/imageanalyser.cpp create mode 100644 Plugins/ZShell/imageanalyser.hpp create mode 100644 image_colors.json create mode 100644 scripts/SchemeColorGen.py diff --git a/.gitignore b/.gitignore index 14739c1..7ad6b58 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ .qmlls.ini build/ compile_commands.json +testpython diff --git a/Bar.qml b/Bar.qml index ca6b9a5..670ab2a 100644 --- a/Bar.qml +++ b/Bar.qml @@ -39,9 +39,13 @@ Scope { Rectangle { id: backgroundRect anchors.fill: parent - color: Config.baseBgColor + color: Config.useDynamicColors ? DynamicColors.tPalette.m3surface : Config.baseBgColor radius: 0 + Behavior on color { + CAnim {} + } + RowLayout { anchors.fill: parent anchors.leftMargin: 5 @@ -92,11 +96,17 @@ Scope { Text { id: notificationCenterIcon + property color iconColor: Config.useDynamicColors ? DynamicColors.palette.m3tertiaryFixed : "white" Layout.alignment: Qt.AlignVCenter text: HasNotifications.hasNotifications ? "\uf4fe" : "\ue7f4" font.family: "Material Symbols Rounded" font.pixelSize: 20 - color: "white" + color: iconColor + + Behavior on color { + CAnim {} + } + MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor diff --git a/Clock.qml b/Clock.qml index 45d4f12..ac8e8e0 100644 --- a/Clock.qml +++ b/Clock.qml @@ -1,6 +1,12 @@ import QtQuick +import qs.Config +import qs.Modules Text { text: Time.time - color: "white" + color: Config.useDynamicColors ? DynamicColors.palette.m3tertiary : "white" + + Behavior on color { + CAnim {} + } } diff --git a/Config/Config.qml b/Config/Config.qml index ccae1fc..f1a0497 100644 --- a/Config/Config.qml +++ b/Config/Config.qml @@ -17,6 +17,7 @@ Singleton { property alias colors: adapter.colors property alias gpuType: adapter.gpuType property alias background: adapter.background + property alias useDynamicColors: adapter.useDynamicColors FileView { id: root @@ -42,6 +43,7 @@ Singleton { property Colors colors: Colors {} property string gpuType: "" property BackgroundConfig background: BackgroundConfig {} + property bool useDynamicColors: false } } } diff --git a/Config/DynamicColors.qml b/Config/DynamicColors.qml new file mode 100644 index 0000000..bd90994 --- /dev/null +++ b/Config/DynamicColors.qml @@ -0,0 +1,217 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Io +import QtQuick +import ZShell +import qs.Helpers +import qs.Paths + +Singleton { + id: root + + property bool showPreview + property string scheme + property string flavour + readonly property bool light: showPreview ? previewLight : currentLight + property bool currentLight + property bool previewLight + readonly property M3Palette palette: showPreview ? preview : current + readonly property M3TPalette tPalette: M3TPalette {} + readonly property M3Palette current: M3Palette {} + readonly property M3Palette preview: M3Palette {} + readonly property Transparency transparency: Transparency {} + readonly property alias wallLuminance: analyser.luminance + + function getLuminance(c: color): real { + if (c.r == 0 && c.g == 0 && c.b == 0) + return 0; + return Math.sqrt(0.299 * (c.r ** 2) + 0.587 * (c.g ** 2) + 0.114 * (c.b ** 2)); + } + + function alterColor(c: color, a: real, layer: int): color { + const luminance = getLuminance(c); + + const offset = (!light || layer == 1 ? 1 : -layer / 2) * (light ? 0.2 : 0.3) * (1 - transparency.base) * (1 + wallLuminance * (light ? (layer == 1 ? 3 : 1) : 2.5)); + const scale = (luminance + offset) / luminance; + const r = Math.max(0, Math.min(1, c.r * scale)); + const g = Math.max(0, Math.min(1, c.g * scale)); + const b = Math.max(0, Math.min(1, c.b * scale)); + + return Qt.rgba(r, g, b, a); + } + + function layer(c: color, layer: var): color { + if (!transparency.enabled) + return c; + + return layer === 0 ? Qt.alpha(c, transparency.base) : alterColor(c, transparency.layers, layer ?? 1); + } + + function on(c: color): color { + if (c.hslLightness < 0.5) + return Qt.hsla(c.hslHue, c.hslSaturation, 0.9, 1); + return Qt.hsla(c.hslHue, c.hslSaturation, 0.1, 1); + } + + function load(data: string, isPreview: bool): void { + const colors = isPreview ? preview : current; + const scheme = JSON.parse(data); + + if (!isPreview) { + root.scheme = scheme.name; + flavour = scheme.flavour; + currentLight = scheme.mode === "light"; + } else { + previewLight = scheme.mode === "light"; + } + + for (const [name, color] of Object.entries(scheme.colors)) { + const propName = name.startsWith("term") ? name : `m3${name}`; + if (colors.hasOwnProperty(propName)) + colors[propName] = `${color}`; + } + } + + FileView { + path: `${Paths.state}/scheme.json` + watchChanges: true + onFileChanged: reload() + onLoaded: root.load(text(), false) + } + + ImageAnalyser { + id: analyser + + source: WallpaperPath.currentWallpaperPath + } + + component Transparency: QtObject { + readonly property bool enabled: true + readonly property real base: 0.85 - (root.light ? 0.1 : 0) + readonly property real layers: 0.4 + } + + component M3TPalette: QtObject { + readonly property color m3primary_paletteKeyColor: root.layer(root.palette.m3primary_paletteKeyColor) + readonly property color m3secondary_paletteKeyColor: root.layer(root.palette.m3secondary_paletteKeyColor) + readonly property color m3tertiary_paletteKeyColor: root.layer(root.palette.m3tertiary_paletteKeyColor) + readonly property color m3neutral_paletteKeyColor: root.layer(root.palette.m3neutral_paletteKeyColor) + readonly property color m3neutral_variant_paletteKeyColor: root.layer(root.palette.m3neutral_variant_paletteKeyColor) + readonly property color m3background: root.layer(root.palette.m3background, 0) + readonly property color m3onBackground: root.layer(root.palette.m3onBackground) + readonly property color m3surface: root.layer(root.palette.m3surface, 0) + readonly property color m3surfaceDim: root.layer(root.palette.m3surfaceDim, 0) + readonly property color m3surfaceBright: root.layer(root.palette.m3surfaceBright, 0) + readonly property color m3surfaceContainerLowest: root.layer(root.palette.m3surfaceContainerLowest) + readonly property color m3surfaceContainerLow: root.layer(root.palette.m3surfaceContainerLow) + readonly property color m3surfaceContainer: root.layer(root.palette.m3surfaceContainer) + readonly property color m3surfaceContainerHigh: root.layer(root.palette.m3surfaceContainerHigh) + readonly property color m3surfaceContainerHighest: root.layer(root.palette.m3surfaceContainerHighest) + readonly property color m3onSurface: root.layer(root.palette.m3onSurface) + readonly property color m3surfaceVariant: root.layer(root.palette.m3surfaceVariant, 0) + readonly property color m3onSurfaceVariant: root.layer(root.palette.m3onSurfaceVariant) + readonly property color m3inverseSurface: root.layer(root.palette.m3inverseSurface, 0) + readonly property color m3inverseOnSurface: root.layer(root.palette.m3inverseOnSurface) + readonly property color m3outline: root.layer(root.palette.m3outline) + readonly property color m3outlineVariant: root.layer(root.palette.m3outlineVariant) + readonly property color m3shadow: root.layer(root.palette.m3shadow) + readonly property color m3scrim: root.layer(root.palette.m3scrim) + readonly property color m3surfaceTint: root.layer(root.palette.m3surfaceTint) + readonly property color m3primary: root.layer(root.palette.m3primary) + readonly property color m3onPrimary: root.layer(root.palette.m3onPrimary) + readonly property color m3primaryContainer: root.layer(root.palette.m3primaryContainer) + readonly property color m3onPrimaryContainer: root.layer(root.palette.m3onPrimaryContainer) + readonly property color m3inversePrimary: root.layer(root.palette.m3inversePrimary) + readonly property color m3secondary: root.layer(root.palette.m3secondary) + readonly property color m3onSecondary: root.layer(root.palette.m3onSecondary) + readonly property color m3secondaryContainer: root.layer(root.palette.m3secondaryContainer) + readonly property color m3onSecondaryContainer: root.layer(root.palette.m3onSecondaryContainer) + readonly property color m3tertiary: root.layer(root.palette.m3tertiary) + readonly property color m3onTertiary: root.layer(root.palette.m3onTertiary) + readonly property color m3tertiaryContainer: root.layer(root.palette.m3tertiaryContainer) + readonly property color m3onTertiaryContainer: root.layer(root.palette.m3onTertiaryContainer) + readonly property color m3error: root.layer(root.palette.m3error) + readonly property color m3onError: root.layer(root.palette.m3onError) + readonly property color m3errorContainer: root.layer(root.palette.m3errorContainer) + readonly property color m3onErrorContainer: root.layer(root.palette.m3onErrorContainer) + readonly property color m3success: root.layer(root.palette.m3success) + readonly property color m3onSuccess: root.layer(root.palette.m3onSuccess) + readonly property color m3successContainer: root.layer(root.palette.m3successContainer) + readonly property color m3onSuccessContainer: root.layer(root.palette.m3onSuccessContainer) + readonly property color m3primaryFixed: root.layer(root.palette.m3primaryFixed) + readonly property color m3primaryFixedDim: root.layer(root.palette.m3primaryFixedDim) + readonly property color m3onPrimaryFixed: root.layer(root.palette.m3onPrimaryFixed) + readonly property color m3onPrimaryFixedVariant: root.layer(root.palette.m3onPrimaryFixedVariant) + readonly property color m3secondaryFixed: root.layer(root.palette.m3secondaryFixed) + readonly property color m3secondaryFixedDim: root.layer(root.palette.m3secondaryFixedDim) + readonly property color m3onSecondaryFixed: root.layer(root.palette.m3onSecondaryFixed) + readonly property color m3onSecondaryFixedVariant: root.layer(root.palette.m3onSecondaryFixedVariant) + readonly property color m3tertiaryFixed: root.layer(root.palette.m3tertiaryFixed) + readonly property color m3tertiaryFixedDim: root.layer(root.palette.m3tertiaryFixedDim) + readonly property color m3onTertiaryFixed: root.layer(root.palette.m3onTertiaryFixed) + readonly property color m3onTertiaryFixedVariant: root.layer(root.palette.m3onTertiaryFixedVariant) + } + + component M3Palette: QtObject { + property color m3primary_paletteKeyColor: "#a8627b" + property color m3secondary_paletteKeyColor: "#8e6f78" + property color m3tertiary_paletteKeyColor: "#986e4c" + property color m3neutral_paletteKeyColor: "#807477" + property color m3neutral_variant_paletteKeyColor: "#837377" + property color m3background: "#191114" + property color m3onBackground: "#efdfe2" + property color m3surface: "#191114" + property color m3surfaceDim: "#191114" + property color m3surfaceBright: "#403739" + property color m3surfaceContainerLowest: "#130c0e" + property color m3surfaceContainerLow: "#22191c" + property color m3surfaceContainer: "#261d20" + property color m3surfaceContainerHigh: "#31282a" + property color m3surfaceContainerHighest: "#3c3235" + property color m3onSurface: "#efdfe2" + property color m3surfaceVariant: "#514347" + property color m3onSurfaceVariant: "#d5c2c6" + property color m3inverseSurface: "#efdfe2" + property color m3inverseOnSurface: "#372e30" + property color m3outline: "#9e8c91" + property color m3outlineVariant: "#514347" + property color m3shadow: "#000000" + property color m3scrim: "#000000" + property color m3surfaceTint: "#ffb0ca" + property color m3primary: "#ffb0ca" + property color m3onPrimary: "#541d34" + property color m3primaryContainer: "#6f334a" + property color m3onPrimaryContainer: "#ffd9e3" + property color m3inversePrimary: "#8b4a62" + property color m3secondary: "#e2bdc7" + property color m3onSecondary: "#422932" + property color m3secondaryContainer: "#5a3f48" + property color m3onSecondaryContainer: "#ffd9e3" + property color m3tertiary: "#f0bc95" + property color m3onTertiary: "#48290c" + property color m3tertiaryContainer: "#b58763" + property color m3onTertiaryContainer: "#000000" + property color m3error: "#ffb4ab" + property color m3onError: "#690005" + property color m3errorContainer: "#93000a" + property color m3onErrorContainer: "#ffdad6" + property color m3success: "#B5CCBA" + property color m3onSuccess: "#213528" + property color m3successContainer: "#374B3E" + property color m3onSuccessContainer: "#D1E9D6" + property color m3primaryFixed: "#ffd9e3" + property color m3primaryFixedDim: "#ffb0ca" + property color m3onPrimaryFixed: "#39071f" + property color m3onPrimaryFixedVariant: "#6f334a" + property color m3secondaryFixed: "#ffd9e3" + property color m3secondaryFixedDim: "#e2bdc7" + property color m3onSecondaryFixed: "#2b151d" + property color m3onSecondaryFixedVariant: "#5a3f48" + property color m3tertiaryFixed: "#ffdcc3" + property color m3tertiaryFixedDim: "#f0bc95" + property color m3onTertiaryFixed: "#2f1500" + property color m3onTertiaryFixedVariant: "#623f21" + } +} diff --git a/Modules/AudioWidget.qml b/Modules/AudioWidget.qml index dd3f36a..336b8c9 100644 --- a/Modules/AudioWidget.qml +++ b/Modules/AudioWidget.qml @@ -5,6 +5,7 @@ import Quickshell.Io import Quickshell.Services.Pipewire import Quickshell.Widgets import qs.Modules +import qs.Config Item { id: root @@ -12,6 +13,7 @@ Item { implicitHeight: 34 property bool expanded: false + property color textColor: Config.useDynamicColors ? DynamicColors.palette.m3tertiaryFixed : "#ffffff" Behavior on implicitWidth { NumberAnimation { @@ -34,8 +36,11 @@ Item { anchors.right: parent.right height: 22 radius: height / 2 - color: "#40000000" + color: Config.useDynamicColors ? DynamicColors.tPalette.m3surfaceContainer : "#40000000" + Behavior on color { + CAnim {} + } // Background circle Rectangle { @@ -66,8 +71,11 @@ Item { Layout.alignment: Qt.AlignVCenter font.family: "Material Symbols Rounded" font.pixelSize: 18 - text: "\ue050" // volume_up icon - color: "#ffffff" + text: "\ue050" // volumeUp icon + color: root.textColor + Behavior on color { + CAnim {} + } } Rectangle { @@ -87,7 +95,7 @@ Item { implicitWidth: parent.width * ( Pipewire.defaultAudioSink?.audio.volume ?? 0 ) radius: parent.radius - color: "#ffffff" + color: root.textColor } Rectangle { @@ -97,7 +105,7 @@ Item { width: sinkVolumeMouseArea.pressed ? 25 : 12 height: sinkVolumeMouseArea.pressed ? 25 : 12 radius: width / 2 - color: sinkVolumeMouseArea.containsMouse || sinkVolumeMouseArea.pressed ? "#ffffff" : "#aaaaaa" + color: sinkVolumeMouseArea.containsMouse || sinkVolumeMouseArea.pressed ? (Config.useDynamicColors ? DynamicColors.palette.m3onSurface : "#ffffff") : (Config.useDynamicColors ? DynamicColors.palette.m3onSurfaceVariant : "#aaaaaa") border.color: "#40000000" border.width: 2 anchors.verticalCenter: parent.verticalCenter @@ -171,7 +179,11 @@ Item { font.family: "Material Symbols Rounded" font.pixelSize: 18 text: "\ue029" - color: ( Pipewire.defaultAudioSource?.audio.muted ?? false ) ? "#ff4444" : "#ffffff" + color: ( Pipewire.defaultAudioSource?.audio.muted ?? false ) ? (Config.useDynamicColors ? DynamicColors.palette.m3error : "#ff4444") : root.textColor + + Behavior on color { + CAnim {} + } } Rectangle { @@ -191,7 +203,11 @@ Item { implicitWidth: parent.width * ( Pipewire.defaultAudioSource?.audio.volume ?? 0 ) radius: parent.radius - color: ( Pipewire.defaultAudioSource?.audio.muted ?? false ) ? "#ff4444" : "#ffffff" + color: ( Pipewire.defaultAudioSource?.audio.muted ?? false ) ? (Config.useDynamicColors ? DynamicColors.palette.m3error : "#ff4444") : root.textColor + + Behavior on color { + CAnim {} + } } Rectangle { @@ -201,7 +217,7 @@ Item { width: sourceVolumeMouseArea.pressed ? 25 : 12 height: sourceVolumeMouseArea.pressed ? 25 : 12 radius: width / 2 - color: sourceVolumeMouseArea.containsMouse || sourceVolumeMouseArea.pressed ? "#ffffff" : "#aaaaaa" + color: sourceVolumeMouseArea.containsMouse || sourceVolumeMouseArea.pressed ? (Config.useDynamicColors ? DynamicColors.palette.m3onSurface : "#ffffff") : (Config.useDynamicColors ? DynamicColors.palette.m3onSurfaceVariant : "#aaaaaa") border.color: "#40000000" border.width: 2 anchors.verticalCenter: parent.verticalCenter diff --git a/Modules/CAnim.qml b/Modules/CAnim.qml new file mode 100644 index 0000000..93cfba4 --- /dev/null +++ b/Modules/CAnim.qml @@ -0,0 +1,8 @@ +import QtQuick +import qs.Modules + +ColorAnimation { + duration: 400 + easing.type: Easing.BezierSpline + easing.bezierCurve: MaterialEasing.standard +} diff --git a/Modules/CustomTextField.qml b/Modules/CustomTextField.qml index d0d8561..cbf0f48 100644 --- a/Modules/CustomTextField.qml +++ b/Modules/CustomTextField.qml @@ -3,6 +3,7 @@ import QtQuick import QtQuick.Controls import qs.Helpers import qs.Config +import qs.Paths TextField { id: root @@ -84,6 +85,8 @@ TextField { launcherWindow.visible = false; } else if ( wallpaperPickerLoader.active ) { SearchWallpapers.setWallpaper(wallpaperPickerLoader.item.currentItem.modelData.path) + if ( Config.useDynamicColors ) + Quickshell.execDetached(["python3", Quickshell.shellPath("scripts/SchemeColorGen.py"), `--path=${wallpaperPickerLoader.item.currentItem.modelData.path}`, `--thumbnail=${Paths.cache}/imagecache/thumbnail.jpg`, `--output=${Paths.state}/scheme.json`]); if ( Config.wallust ) { Wallust.generateColors(WallpaperPath.currentWallpaperPath); } diff --git a/Modules/Resource.qml b/Modules/Resource.qml index 3378456..719b5f9 100644 --- a/Modules/Resource.qml +++ b/Modules/Resource.qml @@ -15,6 +15,8 @@ Item { implicitWidth: resourceRowLayout.x < 0 ? 0 : resourceRowLayout.implicitWidth implicitHeight: 22 property bool warning: percentage * 100 >= warningThreshold + property color usageColor: Config.useDynamicColors ? ( warning ? DynamicColors.palette.m3error : DynamicColors.palette.m3primary ) : ( warning ? Config.accentColor.accents.warning : Config.accentColor.accents.primary ) + property color borderColor: Config.useDynamicColors ? ( warning ? DynamicColors.palette.m3onError : DynamicColors.palette.m3onPrimary ) : ( warning ? Config.accentColor.accents.warningAlt : Config.accentColor.accents.primaryAlt ) Behavior on percentage { NumberAnimation { @@ -55,9 +57,14 @@ Item { ShapePath { strokeWidth: 0 - fillColor: root.warning ? Config.accentColor.accents.warning : Config.accentColor.accents.primary + fillColor: root.usageColor startX: backgroundCircle.width / 2 startY: backgroundCircle.height / 2 + + Behavior on fillColor { + CAnim {} + } + PathLine { x: backgroundCircle.width / 2 y: 0 + ( 1 / 2 ) @@ -80,10 +87,14 @@ Item { ShapePath { strokeWidth: 1 - strokeColor: root.warning ? Config.accentColor.accents.warningAlt : Config.accentColor.accents.primaryAlt + strokeColor: root.borderColor fillColor: "transparent" capStyle: ShapePath.FlatCap + Behavior on strokeColor { + CAnim {} + } + PathAngleArc { centerX: backgroundCircle.width / 2 centerY: backgroundCircle.height / 2 diff --git a/Modules/Resources.qml b/Modules/Resources.qml index 8df7ffc..ddf9c78 100644 --- a/Modules/Resources.qml +++ b/Modules/Resources.qml @@ -10,6 +10,7 @@ Item { id: root implicitWidth: rowLayout.implicitWidth + rowLayout.anchors.leftMargin + rowLayout.anchors.rightMargin implicitHeight: 34 + property color textColor: Config.useDynamicColors ? DynamicColors.palette.m3tertiaryFixed : "#ffffff" Rectangle { anchors { @@ -18,7 +19,7 @@ Item { verticalCenter: parent.verticalCenter } implicitHeight: 22 - color: "#40000000" + color: Config.useDynamicColors ? DynamicColors.tPalette.m3surfaceContainer : "#40000000" radius: height / 2 RowLayout { id: rowLayout @@ -33,7 +34,10 @@ Item { font.family: "Material Symbols Rounded" font.pixelSize: 18 text: "\uf7a3" - color: "#ffffff" + color: root.textColor + Behavior on color { + CAnim {} + } } Resource { @@ -46,7 +50,10 @@ Item { font.family: "Material Symbols Rounded" font.pixelSize: 18 text: "\ue322" - color: "#ffffff" + color: root.textColor + Behavior on color { + CAnim {} + } } Resource { @@ -59,7 +66,10 @@ Item { font.family: "Material Symbols Rounded" font.pixelSize: 16 text: "\ue30f" - color: "#ffffff" + color: root.textColor + Behavior on color { + CAnim {} + } } Resource { @@ -71,7 +81,10 @@ Item { font.family: "Material Symbols Rounded" font.pixelSize: 18 text: "\ue30d" - color: "#ffffff" + color: root.textColor + Behavior on color { + CAnim {} + } } Resource { diff --git a/Modules/TrayItem.qml b/Modules/TrayItem.qml index 36cc30c..a26ee09 100644 --- a/Modules/TrayItem.qml +++ b/Modules/TrayItem.qml @@ -2,8 +2,11 @@ import QtQuick import Quickshell import Quickshell.Services.SystemTray import Quickshell.Io +import Quickshell.Widgets import qs.Modules import qs.Config +import Caelestia +import QtQuick.Effects MouseArea { id: root diff --git a/Modules/UpdatesWidget.qml b/Modules/UpdatesWidget.qml index a5d959b..2ce264c 100644 --- a/Modules/UpdatesWidget.qml +++ b/Modules/UpdatesWidget.qml @@ -1,17 +1,22 @@ import QtQuick import QtQuick.Layouts import qs.Modules +import qs.Config Item { id: root required property int countUpdates implicitWidth: contentRow.childrenRect.width + 10 implicitHeight: 22 + property color textColor: Config.useDynamicColors ? DynamicColors.palette.m3tertiaryFixed : "#ffffff" Rectangle { anchors.fill: parent radius: height / 2 - color: "#40000000" + color: Config.useDynamicColors ? DynamicColors.tPalette.m3surfaceContainer : "#40000000" + Behavior on color { + CAnim {} + } } RowLayout { @@ -28,7 +33,10 @@ Item { font.family: "Material Symbols Rounded" font.pixelSize: 18 text: "\uf569" - color: "#ffffff" + color: root.textColor + Behavior on color { + CAnim {} + } } TextMetrics { @@ -40,7 +48,10 @@ Item { Text { Layout.alignment: Qt.AlignVCenter | Qt.AlignRight text: textMetrics.text - color: "white" + color: root.textColor + Behavior on color { + CAnim {} + } } } } diff --git a/Modules/WindowTitle.qml b/Modules/WindowTitle.qml index 6d8b57e..ebe6f77 100644 --- a/Modules/WindowTitle.qml +++ b/Modules/WindowTitle.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Layouts import Quickshell.Hyprland import qs.Helpers +import qs.Config Item { id: root @@ -11,6 +12,7 @@ Item { clip: true property bool showFirst: true + property color textColor: Config.useDynamicColors ? DynamicColors.palette.m3primary : "white" Component.onCompleted: { Hyprland.rawEvent.connect(( event ) => { @@ -37,7 +39,7 @@ Item { anchors.fill: parent anchors.margins: 5 text: root.currentTitle - color: "white" + color: root.textColor elide: Text.ElideRight font.pixelSize: 16 horizontalAlignment: Text.AlignHCenter @@ -46,13 +48,17 @@ Item { Behavior on opacity { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } } + + Behavior on color { + CAnim {} + } } Text { id: titleText2 anchors.fill: parent anchors.margins: 5 - color: "white" + color: root.textColor elide: Text.ElideRight font.pixelSize: 16 horizontalAlignment: Text.AlignHCenter @@ -61,5 +67,9 @@ Item { Behavior on opacity { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } } + + Behavior on color { + CAnim {} + } } } diff --git a/Modules/Workspaces.qml b/Modules/Workspaces.qml index 1d3a3f6..6397a96 100644 --- a/Modules/Workspaces.qml +++ b/Modules/Workspaces.qml @@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Controls import QtQuick.Layouts +import QtQuick.Effects import Quickshell import Quickshell.Hyprland import qs.Config @@ -26,7 +27,7 @@ Rectangle { } } - color: "#40000000" + color: Config.useDynamicColors ? DynamicColors.tPalette.m3surfaceContainer : "#40000000" radius: height / 2 Behavior on implicitWidth { @@ -35,7 +36,11 @@ Rectangle { easing.type: Easing.InOutQuad } } - + + Behavior on color { + CAnim {} + } + RowLayout { id: workspacesRow anchors.left: parent.left @@ -47,15 +52,16 @@ Rectangle { model: Hyprland.workspaces Rectangle { + id: workspaceIndicator required property var modelData width: 16 height: 16 radius: height / 2 - color: modelData.id === Hyprland.focusedWorkspace.id ? Config.accentColor.accents.primary : "#606060" + color: modelData.id === Hyprland.focusedWorkspace.id ? ( Config.useDynamicColors ? DynamicColors.palette.m3primary : Config.accentColor.accents.primary ) : ( Config.useDynamicColors ? DynamicColors.palette.m3inverseOnSurface : "#606060" ) - border.color: modelData.id === Hyprland.focusedWorkspace.id ? Config.accentColor.accents.primaryAlt : "#808080" + border.color: modelData.id === Hyprland.focusedWorkspace.id ? ( Config.useDynamicColors ? DynamicColors.palette.m3onPrimary : Config.accentColor.accents.primaryAlt ) : ( Config.useDynamicColors ? DynamicColors.palette.m3inverseOnSurface : "#808080" ) border.width: 1 visible: root.shouldShow( modelData.monitor ) @@ -90,13 +96,13 @@ Rectangle { duration: 200 } - Text { - anchors.centerIn: parent - text: modelData.id - font.pixelSize: 10 - font.family: "Rubik" - color: modelData.id === Hyprland.focusedWorkspace.id ? Config.workspaceWidget.textColor : Config.workspaceWidget.inactiveTextColor - } + // Text { + // anchors.centerIn: parent + // text: modelData.id + // font.pixelSize: 10 + // font.family: "Rubik" + // color: modelData.id === Hyprland.focusedWorkspace.id ? Config.workspaceWidget.textColor : Config.workspaceWidget.inactiveTextColor + // } MouseArea { anchors.fill: parent diff --git a/Plugins/ZShell/CMakeLists.txt b/Plugins/ZShell/CMakeLists.txt index 2d9880c..f76f23b 100644 --- a/Plugins/ZShell/CMakeLists.txt +++ b/Plugins/ZShell/CMakeLists.txt @@ -33,6 +33,7 @@ qml_module(ZShell SOURCES writefile.hpp writefile.cpp appdb.hpp appdb.cpp + imageanalyser.hpp imageanalyser.cpp LIBRARIES Qt::Gui Qt::Quick diff --git a/Plugins/ZShell/imageanalyser.cpp b/Plugins/ZShell/imageanalyser.cpp new file mode 100644 index 0000000..08e3eec --- /dev/null +++ b/Plugins/ZShell/imageanalyser.cpp @@ -0,0 +1,223 @@ +#include "imageanalyser.hpp" + +#include +#include +#include +#include +#include + +namespace ZShell { + +ImageAnalyser::ImageAnalyser(QObject* parent) + : QObject(parent) + , m_futureWatcher(new QFutureWatcher(this)) + , m_source("") + , m_sourceItem(nullptr) + , m_rescaleSize(128) + , m_dominantColour(0, 0, 0) + , m_luminance(0) { + QObject::connect(m_futureWatcher, &QFutureWatcher::finished, this, [this]() { + if (!m_futureWatcher->future().isResultReadyAt(0)) { + return; + } + + const auto result = m_futureWatcher->result(); + if (m_dominantColour != result.first) { + m_dominantColour = result.first; + emit dominantColourChanged(); + } + if (!qFuzzyCompare(m_luminance + 1.0, result.second + 1.0)) { + m_luminance = result.second; + emit luminanceChanged(); + } + }); +} + +QString ImageAnalyser::source() const { + return m_source; +} + +void ImageAnalyser::setSource(const QString& source) { + if (m_source == source) { + return; + } + + m_source = source; + emit sourceChanged(); + + if (m_sourceItem) { + m_sourceItem = nullptr; + emit sourceItemChanged(); + } + + requestUpdate(); +} + +QQuickItem* ImageAnalyser::sourceItem() const { + return m_sourceItem; +} + +void ImageAnalyser::setSourceItem(QQuickItem* sourceItem) { + if (m_sourceItem == sourceItem) { + return; + } + + m_sourceItem = sourceItem; + emit sourceItemChanged(); + + if (!m_source.isEmpty()) { + m_source = ""; + emit sourceChanged(); + } + + requestUpdate(); +} + +int ImageAnalyser::rescaleSize() const { + return m_rescaleSize; +} + +void ImageAnalyser::setRescaleSize(int rescaleSize) { + if (m_rescaleSize == rescaleSize) { + return; + } + + m_rescaleSize = rescaleSize; + emit rescaleSizeChanged(); + + requestUpdate(); +} + +QColor ImageAnalyser::dominantColour() const { + return m_dominantColour; +} + +qreal ImageAnalyser::luminance() const { + return m_luminance; +} + +void ImageAnalyser::requestUpdate() { + if (m_source.isEmpty() && !m_sourceItem) { + return; + } + + if (!m_sourceItem || (m_sourceItem->window() && m_sourceItem->window()->isVisible() && m_sourceItem->width() > 0 && + m_sourceItem->height() > 0)) { + update(); + } else if (m_sourceItem) { + if (!m_sourceItem->window()) { + QObject::connect(m_sourceItem, &QQuickItem::windowChanged, this, &ImageAnalyser::requestUpdate, + Qt::SingleShotConnection); + } else if (!m_sourceItem->window()->isVisible()) { + QObject::connect(m_sourceItem->window(), &QQuickWindow::visibleChanged, this, &ImageAnalyser::requestUpdate, + Qt::SingleShotConnection); + } + if (m_sourceItem->width() <= 0) { + QObject::connect( + m_sourceItem, &QQuickItem::widthChanged, this, &ImageAnalyser::requestUpdate, Qt::SingleShotConnection); + } + if (m_sourceItem->height() <= 0) { + QObject::connect(m_sourceItem, &QQuickItem::heightChanged, this, &ImageAnalyser::requestUpdate, + Qt::SingleShotConnection); + } + } +} + +void ImageAnalyser::update() { + if (m_source.isEmpty() && !m_sourceItem) { + return; + } + + if (m_futureWatcher->isRunning()) { + m_futureWatcher->cancel(); + } + + if (m_sourceItem) { + const QSharedPointer grabResult = m_sourceItem->grabToImage(); + QObject::connect(grabResult.data(), &QQuickItemGrabResult::ready, this, [grabResult, this]() { + m_futureWatcher->setFuture(QtConcurrent::run(&ImageAnalyser::analyse, grabResult->image(), m_rescaleSize)); + }); + } else { + m_futureWatcher->setFuture(QtConcurrent::run([=, this](QPromise& promise) { + const QImage image(m_source); + analyse(promise, image, m_rescaleSize); + })); + } +} + +void ImageAnalyser::analyse(QPromise& promise, const QImage& image, int rescaleSize) { + if (image.isNull()) { + qWarning() << "ImageAnalyser::analyse: image is null"; + return; + } + + QImage img = image; + + if (rescaleSize > 0 && (img.width() > rescaleSize || img.height() > rescaleSize)) { + img = img.scaled(rescaleSize, rescaleSize, Qt::KeepAspectRatio, Qt::FastTransformation); + } + + if (promise.isCanceled()) { + return; + } + + if (img.format() != QImage::Format_ARGB32) { + img = img.convertToFormat(QImage::Format_ARGB32); + } + + if (promise.isCanceled()) { + return; + } + + const uchar* data = img.bits(); + const int width = img.width(); + const int height = img.height(); + const qsizetype bytesPerLine = img.bytesPerLine(); + + std::unordered_map colours; + qreal totalLuminance = 0.0; + int count = 0; + + for (int y = 0; y < height; ++y) { + const uchar* line = data + y * bytesPerLine; + for (int x = 0; x < width; ++x) { + if (promise.isCanceled()) { + return; + } + + const uchar* pixel = line + x * 4; + + if (pixel[3] == 0) { + continue; + } + + const quint32 mr = static_cast(pixel[0] & 0xF8); + const quint32 mg = static_cast(pixel[1] & 0xF8); + const quint32 mb = static_cast(pixel[2] & 0xF8); + ++colours[(mr << 16) | (mg << 8) | mb]; + + const qreal r = pixel[0] / 255.0; + const qreal g = pixel[1] / 255.0; + const qreal b = pixel[2] / 255.0; + totalLuminance += std::sqrt(0.299 * r * r + 0.587 * g * g + 0.114 * b * b); + ++count; + } + } + + quint32 dominantColour = 0; + int maxCount = 0; + for (const auto& [colour, colourCount] : colours) { + if (promise.isCanceled()) { + return; + } + + if (colourCount > maxCount) { + dominantColour = colour; + maxCount = colourCount; + } + } + + promise.addResult(qMakePair(QColor((0xFFu << 24) | dominantColour), count == 0 ? 0.0 : totalLuminance / count)); +} + +} // namespace ZShell diff --git a/Plugins/ZShell/imageanalyser.hpp b/Plugins/ZShell/imageanalyser.hpp new file mode 100644 index 0000000..829669e --- /dev/null +++ b/Plugins/ZShell/imageanalyser.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace ZShell { + +class ImageAnalyser : public QObject { + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(QString source READ source WRITE setSource NOTIFY sourceChanged) + Q_PROPERTY(QQuickItem* sourceItem READ sourceItem WRITE setSourceItem NOTIFY sourceItemChanged) + Q_PROPERTY(int rescaleSize READ rescaleSize WRITE setRescaleSize NOTIFY rescaleSizeChanged) + Q_PROPERTY(QColor dominantColour READ dominantColour NOTIFY dominantColourChanged) + Q_PROPERTY(qreal luminance READ luminance NOTIFY luminanceChanged) + +public: + explicit ImageAnalyser(QObject* parent = nullptr); + + [[nodiscard]] QString source() const; + void setSource(const QString& source); + + [[nodiscard]] QQuickItem* sourceItem() const; + void setSourceItem(QQuickItem* sourceItem); + + [[nodiscard]] int rescaleSize() const; + void setRescaleSize(int rescaleSize); + + [[nodiscard]] QColor dominantColour() const; + [[nodiscard]] qreal luminance() const; + + Q_INVOKABLE void requestUpdate(); + +signals: + void sourceChanged(); + void sourceItemChanged(); + void rescaleSizeChanged(); + void dominantColourChanged(); + void luminanceChanged(); + +private: + using AnalyseResult = QPair; + + QFutureWatcher* const m_futureWatcher; + + QString m_source; + QQuickItem* m_sourceItem; + int m_rescaleSize; + + QColor m_dominantColour; + qreal m_luminance; + + void update(); + static void analyse(QPromise& promise, const QImage& image, int rescaleSize); +}; + +} // namespace ZShell diff --git a/Time.qml b/Time.qml index 88670da..438fbea 100644 --- a/Time.qml +++ b/Time.qml @@ -1,4 +1,3 @@ -// Time.qml pragma Singleton import Quickshell diff --git a/image_colors.json b/image_colors.json new file mode 100644 index 0000000..5672f64 --- /dev/null +++ b/image_colors.json @@ -0,0 +1,56 @@ +{ + "primary_paletteKeyColor": "#A56A24", + "secondary_paletteKeyColor": "#587C8E", + "tertiary_paletteKeyColor": "#3A8188", + "neutral_paletteKeyColor": "#6F7979", + "neutral_variant_paletteKeyColor": "#697A7B", + "background": "#0C1515", + "onBackground": "#DAE4E4", + "surface": "#0C1515", + "surfaceDim": "#0C1515", + "surfaceBright": "#313B3B", + "surfaceContainerLowest": "#071010", + "surfaceContainerLow": "#141D1E", + "surfaceContainer": "#182122", + "surfaceContainerHigh": "#222C2C", + "surfaceContainerHighest": "#2D3637", + "onSurface": "#DAE4E4", + "surfaceVariant": "#394A4A", + "onSurfaceVariant": "#B8CACA", + "inverseSurface": "#DAE4E4", + "inverseOnSurface": "#293232", + "outline": "#829494", + "outlineVariant": "#394A4A", + "shadow": "#000000", + "scrim": "#000000", + "surfaceTint": "#FFB86E", + "primary": "#FFB86E", + "onPrimary": "#492900", + "primaryContainer": "#693C00", + "onPrimaryContainer": "#FFDCBD", + "inversePrimary": "#88520A", + "secondary": "#A6CCE0", + "onSecondary": "#0A3545", + "secondaryContainer": "#284E5E", + "onSecondaryContainer": "#C2E8FD", + "tertiary": "#8CD2D9", + "onTertiary": "#00363B", + "tertiaryContainer": "#569BA2", + "onTertiaryContainer": "#000000", + "error": "#FFB4AB", + "onError": "#690005", + "errorContainer": "#93000A", + "onErrorContainer": "#FFDAD6", + "primaryFixed": "#FFDCBD", + "primaryFixedDim": "#FFB86E", + "onPrimaryFixed": "#2C1600", + "onPrimaryFixedVariant": "#693C00", + "secondaryFixed": "#C2E8FD", + "secondaryFixedDim": "#A6CCE0", + "onSecondaryFixed": "#001F2A", + "onSecondaryFixedVariant": "#264B5C", + "tertiaryFixed": "#A8EEF6", + "tertiaryFixedDim": "#8CD2D9", + "onTertiaryFixed": "#002022", + "onTertiaryFixedVariant": "#004F55" +} \ No newline at end of file diff --git a/scripts/SchemeColorGen.py b/scripts/SchemeColorGen.py new file mode 100644 index 0000000..a1364c1 --- /dev/null +++ b/scripts/SchemeColorGen.py @@ -0,0 +1,102 @@ +import json +import argparse +from pathlib import Path +from PIL import Image +from materialyoucolor.quantize import QuantizeCelebi +from materialyoucolor.score.score import Score +from materialyoucolor.dynamiccolor.material_dynamic_colors import MaterialDynamicColors +from materialyoucolor.scheme.scheme_tonal_spot import SchemeTonalSpot +from materialyoucolor.hct.hct import Hct + + +def generate_thumbnail(image_path, thumbnail_path, size=(128, 128)): + thumbnail_file = Path(thumbnail_path) + + image = Image.open(image_path) + image = image.convert("RGB") + image.thumbnail(size, Image.NEAREST) + + thumbnail_file.parent.mkdir(parents=True, exist_ok=True) + image.save(thumbnail_path, "JPEG") + + +def generate_color_scheme(thumbnail_path, output_path): + image = Image.open(thumbnail_path) + pixel_len = image.width * image.height + image_data = image.getdata() + + quality = 1 + pixel_array = [image_data[_] for _ in range(0, pixel_len, quality)] + + result = QuantizeCelebi(pixel_array, 128) + score = Score.score(result)[0] + + scheme = SchemeTonalSpot( + Hct.from_int(score), + True, + 0.0 + ) + + color_dict = {} + for color in vars(MaterialDynamicColors).keys(): + color_name = getattr(MaterialDynamicColors, color) + if hasattr(color_name, "get_hct"): + color_int = color_name.get_hct(scheme).to_int() + color_dict[color] = int_to_hex(color_int) + + output_dict = { + "name": "dynamic", + "flavour": "default", + "mode": "dark", + "variant": "tonalspot", + "colors": color_dict + } + + output_file = Path(output_path) + output_file.parent.mkdir(parents=True, exist_ok=True) + + with open(output_file, "w") as f: + json.dump(output_dict, f, indent=4) + + +def int_to_hex(argb_int): + return "#{:06X}".format(argb_int & 0xFFFFFF) + + +def main(): + parser = argparse.ArgumentParser( + description="Generate color scheme from wallpaper image" + ) + + parser.add_argument( + "--path", + required=True, + help="Path to the wallpaper image" + ) + + parser.add_argument( + "--output", + required=True, + help="Path to save the color scheme JSON file" + ) + + parser.add_argument( + "--thumbnail", + required=True, + help="Path to save the thumbnail image" + ) + + args = parser.parse_args() + + try: + generate_thumbnail(args.path, str(args.thumbnail)) + generate_color_scheme(str(args.thumbnail), args.output) + except Exception as e: + print(f"Error: {e}") + return 1 + + return 0 + + +if __name__ == "__main__": + exit(main())