diff --git a/src/main.cpp b/src/main.cpp index 11b13c9..48108ed 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -667,7 +667,7 @@ int main(int argc, char *argv[]) } } - auto applyWindowEffects = [config](QQuickWindow *window) { + auto applyWindowEffects = [config, theme](QQuickWindow *window) { if (!window) return; @@ -680,8 +680,13 @@ int main(int argc, char *argv[]) const bool contrastAvailable = KWindowEffects::isEffectAvailable(KWindowEffects::BackgroundContrast); KWindowEffects::enableBackgroundContrast(window, blurRequested && contrastAvailable); + + // Expose compositor blur availability to QML so glass effects can + // adapt their intensity (reduce QML blur when compositor provides it). + theme->setCompositorBlurAvailable(blurAvailable); #else Q_UNUSED(window) + theme->setCompositorBlurAvailable(false); #endif }; diff --git a/src/qml/Main.qml b/src/qml/Main.qml index 80ad805..3c57f9f 100644 --- a/src/qml/Main.qml +++ b/src/qml/Main.qml @@ -3981,6 +3981,43 @@ ApplicationWindow { Layout.fillHeight: true color: Theme.contentBackground + // ── Glass Effects (content area) ───────────────────────── + Rectangle { + visible: Theme.hasEffects && Theme.gradientEnabled + anchors.fill: parent + z: 0 + gradient: Gradient { + orientation: Gradient.Vertical + GradientStop { position: 0.0; color: Qt.rgba(Theme.gradientColor.r, Theme.gradientColor.g, Theme.gradientColor.b, 0.12) } + GradientStop { position: 1.0; color: "transparent" } + } + } + + Rectangle { + visible: Theme.hasEffects && Theme.noiseEnabled && Theme.noiseOpacity > 0 + anchors.fill: parent + z: 0 + color: "transparent" + opacity: Theme.noiseOpacity * 0.5 + Canvas { + anchors.fill: parent + onPaint: { + var ctx = getContext("2d") + var w = width, h = height + var imgData = ctx.createImageData(w, h) + for (var i = 0; i < imgData.data.length; i += 4) { + var v = Math.random() * 255 + imgData.data[i] = v + imgData.data[i+1] = v + imgData.data[i+2] = v + imgData.data[i+3] = 20 + } + ctx.putImageData(imgData, 0, 0) + } + Component.onCompleted: requestPaint() + } + } + // Curved mantle fills for inverse rounded corners Shape { z: 1; width: Theme.radiusMedium; height: Theme.radiusMedium diff --git a/src/qml/components/SettingsPanel.qml b/src/qml/components/SettingsPanel.qml index 5d4d7c2..a30c2c5 100644 --- a/src/qml/components/SettingsPanel.qml +++ b/src/qml/components/SettingsPanel.qml @@ -239,6 +239,13 @@ Window { function setDraftTheme(themeName) { draftTheme = themeName draftDarkMode = isDarkTheme(themeName) + if (themeName === "liquid-dark") { + draftDarkTheme = "liquid-dark" + draftLightTheme = "liquid-light" + } else if (themeName === "liquid-light") { + draftDarkTheme = "liquid-dark" + draftLightTheme = "liquid-light" + } } function bindAppearancePreview() { @@ -661,6 +668,153 @@ Window { root.queueSettingsApply() } } + + // ── Glass Effects (only visible for themes with [effects]) ── + Text { + visible: Theme.hasEffects + text: "Glass Effects" + color: Theme.accent + font.pointSize: Theme.fontSmall + font.bold: true + Layout.topMargin: 12 + Layout.bottomMargin: 4 + } + + Text { + visible: Theme.hasEffects + text: "These effects are defined by the active theme. Adjust to taste." + color: Theme.subtext + font.pointSize: Theme.fontSmall + wrapMode: Text.WordWrap + Layout.fillWidth: true + Layout.bottomMargin: 4 + } + + Q.Slider { + visible: Theme.hasEffects + Layout.fillWidth: true + label: "Sidebar opacity" + from: 0 + to: 100 + stepSize: 1 + showValue: true + value: Theme.sidebarOpacity * 100 + onMoved: (value) => { + Theme.sidebarOpacity = value / 100 + } + } + + Q.Slider { + visible: Theme.hasEffects + Layout.fillWidth: true + label: "Content opacity" + from: 0 + to: 100 + stepSize: 1 + showValue: true + value: Theme.contentOpacity * 100 + onMoved: (value) => { + Theme.contentOpacity = value / 100 + } + } + + Q.Slider { + visible: Theme.hasEffects + Layout.fillWidth: true + label: "Toolbar opacity" + from: 0 + to: 100 + stepSize: 1 + showValue: true + value: Theme.toolbarOpacity * 100 + onMoved: (value) => { + Theme.toolbarOpacity = value / 100 + } + } + + Q.Slider { + visible: Theme.hasEffects && Theme.blurEnabled + Layout.fillWidth: true + label: "Blur radius" + from: 0 + to: 64 + stepSize: 1 + showValue: true + value: Theme.blurRadius + onMoved: (value) => { + Theme.blurRadius = Math.round(value) + } + } + + Q.Slider { + visible: Theme.hasEffects && Theme.glowEnabled + Layout.fillWidth: true + label: "Glow intensity" + from: 0 + to: 100 + stepSize: 1 + showValue: true + value: Theme.glowOpacity * 100 + onMoved: (value) => { + Theme.glowOpacity = value / 100 + } + } + + Q.Slider { + visible: Theme.hasEffects && Theme.glowEnabled + Layout.fillWidth: true + label: "Glow radius" + from: 0 + to: 64 + stepSize: 1 + showValue: true + value: Theme.glowRadius + onMoved: (value) => { + Theme.glowRadius = Math.round(value) + } + } + + Q.Slider { + visible: Theme.hasEffects && Theme.noiseEnabled + Layout.fillWidth: true + label: "Noise intensity" + from: 0 + to: 20 + stepSize: 1 + showValue: true + value: Theme.noiseOpacity * 100 + onMoved: (value) => { + Theme.noiseOpacity = value / 100 + } + } + + Q.Slider { + visible: Theme.hasEffects + Layout.fillWidth: true + label: "Saturation" + from: 50 + to: 200 + stepSize: 5 + showValue: true + value: Theme.saturation * 100 + onMoved: (value) => { + Theme.saturation = value / 100 + } + } + + Q.Slider { + visible: Theme.hasEffects && Theme.refractionEnabled + Layout.fillWidth: true + label: "Refraction strength" + from: 0 + to: 10 + stepSize: 1 + showValue: true + value: Theme.refractionStrength * 100 + onMoved: (value) => { + Theme.refractionStrength = value / 100 + } + } } } diff --git a/src/qml/components/Sidebar.qml b/src/qml/components/Sidebar.qml index 2f90201..85afcb5 100644 --- a/src/qml/components/Sidebar.qml +++ b/src/qml/components/Sidebar.qml @@ -26,7 +26,7 @@ Rectangle { signal collapseClicked() signal featureHintRequested(string message) - color: Theme.chromeBackground + color: Theme.sidebarBackground clip: false Component { id: iconHome; IconHome { size: 18; color: Theme.subtext } } @@ -48,7 +48,7 @@ Rectangle { z: 1; width: Theme.radiusMedium; height: Theme.radiusMedium anchors.top: parent.top; anchors.left: parent.right ShapePath { - fillColor: Theme.chromeBackground; strokeColor: "transparent" + fillColor: Theme.sidebarBackground; strokeColor: "transparent" startX: 0; startY: 0 PathLine { x: Theme.radiusMedium; y: 0 } PathArc { @@ -65,7 +65,7 @@ Rectangle { z: 1; width: Theme.radiusMedium; height: Theme.radiusMedium anchors.bottom: parent.bottom; anchors.left: parent.right ShapePath { - fillColor: Theme.chromeBackground; strokeColor: "transparent" + fillColor: Theme.sidebarBackground; strokeColor: "transparent" startX: 0; startY: Theme.radiusMedium PathLine { x: Theme.radiusMedium; y: Theme.radiusMedium } PathArc { @@ -77,6 +77,57 @@ Rectangle { } } + // ── Glass Effects (active only when theme provides [effects]) ───── + + // Gradient tint overlay + Rectangle { + visible: Theme.hasEffects && Theme.gradientEnabled + anchors.fill: parent + z: 0 + gradient: Gradient { + orientation: Theme.gradientDirection === "left_to_right" ? Gradient.Horizontal : Gradient.Vertical + GradientStop { position: 0.0; color: Qt.rgba(Theme.gradientColor.r, Theme.gradientColor.g, Theme.gradientColor.b, 0.6) } + GradientStop { position: 1.0; color: Qt.rgba(Theme.gradientColor.r, Theme.gradientColor.g, Theme.gradientColor.b, 0.15) } + } + } + + // Noise texture overlay (frosted grain) + Rectangle { + visible: Theme.hasEffects && Theme.noiseEnabled && Theme.noiseOpacity > 0 + anchors.fill: parent + z: 0 + color: "transparent" + opacity: Theme.noiseOpacity + Canvas { + anchors.fill: parent + onPaint: { + var ctx = getContext("2d") + var w = width, h = height + var imgData = ctx.createImageData(w, h) + for (var i = 0; i < imgData.data.length; i += 4) { + var v = Math.random() * 255 + imgData.data[i] = v + imgData.data[i+1] = v + imgData.data[i+2] = v + imgData.data[i+3] = 25 + } + ctx.putImageData(imgData, 0, 0) + } + Component.onCompleted: requestPaint() + } + } + + // Border glow effect + Rectangle { + visible: Theme.hasEffects && Theme.glowEnabled && Theme.glowOpacity > 0 + anchors.fill: parent + z: 0 + color: "transparent" + border.width: 1 + border.color: Qt.rgba(Theme.glowColor.r, Theme.glowColor.g, Theme.glowColor.b, Theme.glowOpacity * 0.5) + radius: Theme.radiusLarge + } + ColumnLayout { anchors.fill: parent spacing: 0 diff --git a/src/qml/components/Toolbar.qml b/src/qml/components/Toolbar.qml index 8511961..2f16911 100644 --- a/src/qml/components/Toolbar.qml +++ b/src/qml/components/Toolbar.qml @@ -96,6 +96,28 @@ Rectangle { implicitHeight: toolbarColumn.implicitHeight color: Theme.chromeBackground + // ── Glass Effects (toolbar) ────────────────────────────────────── + Rectangle { + visible: Theme.hasEffects && Theme.gradientEnabled + anchors.fill: parent + z: 0 + gradient: Gradient { + orientation: Gradient.Horizontal + GradientStop { position: 0.0; color: Qt.rgba(Theme.gradientColor.r, Theme.gradientColor.g, Theme.gradientColor.b, 0.25) } + GradientStop { position: 1.0; color: "transparent" } + } + } + + Rectangle { + visible: Theme.hasEffects && Theme.glowEnabled && Theme.glowOpacity > 0 + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: 1 + z: 0 + color: Qt.rgba(Theme.glowColor.r, Theme.glowColor.g, Theme.glowColor.b, Theme.glowOpacity * 0.3) + } + DragHandler { enabled: root.showWindowControls && root.window target: null diff --git a/src/qml/theme/Theme.qml b/src/qml/theme/Theme.qml index b502860..b1d93de 100644 --- a/src/qml/theme/Theme.qml +++ b/src/qml/theme/Theme.qml @@ -64,6 +64,37 @@ QtObject { // of the animEasing* properties also sets easing.bezierCurve to this. readonly property var animBezierCurve: [0.4, 0.0, 0.2, 1.0, 1.0, 1.0] + // ── Glass Effects ────────────────────────────────────────────────── + // Sourced from the [effects] section in the active theme's .toml. + // When absent, all effects default to "off" (hasEffects = false). + property bool hasEffects: theme.hasEffects + property real sidebarOpacity: theme.sidebarOpacity + property real contentOpacity: theme.contentOpacity + property real toolbarOpacity: theme.toolbarOpacity + property bool gradientEnabled: theme.gradientEnabled + property color gradientColor: theme.gradientColor + property string gradientDirection: theme.gradientDirection + property bool glowEnabled: theme.glowEnabled + property color glowColor: theme.glowColor + property real glowRadius: theme.glowRadius + property real glowOpacity: theme.glowOpacity + property bool blurEnabled: theme.blurEnabled + property real blurRadius: theme.blurRadius + property bool noiseEnabled: theme.noiseEnabled + property real noiseOpacity: theme.noiseOpacity + property real saturation: theme.saturation + property bool refractionEnabled: theme.refractionEnabled + property real refractionStrength: theme.refractionStrength + + // Whether the compositor provides blur behind the window + property bool compositorBlurAvailable: theme.compositorBlurAvailable + + // Effective blur radius: reduced when compositor provides blur + readonly property real effectiveBlurRadius: { + if (!hasEffects || !blurEnabled) return 0 + return compositorBlurAvailable ? Math.max(0, blurRadius * 0.3) : blurRadius + } + Behavior on base { ColorAnimation { duration: root.animDurationSlow; easing.type: root.animEasingTransition; easing.bezierCurve: root.animBezierCurve } } @@ -128,14 +159,58 @@ QtObject { NumberAnimation { duration: root.animDuration; easing.type: root.animEasingEnter; easing.bezierCurve: root.animBezierCurve } } + // Smooth transitions for glass effects + Behavior on sidebarOpacity { + NumberAnimation { duration: root.animDuration; easing.type: root.animEasingTransition; easing.bezierCurve: root.animBezierCurve } + } + Behavior on contentOpacity { + NumberAnimation { duration: root.animDuration; easing.type: root.animEasingTransition; easing.bezierCurve: root.animBezierCurve } + } + Behavior on toolbarOpacity { + NumberAnimation { duration: root.animDuration; easing.type: root.animEasingTransition; easing.bezierCurve: root.animBezierCurve } + } + Behavior on glowOpacity { + NumberAnimation { duration: root.animDuration; easing.type: root.animEasingTransition; easing.bezierCurve: root.animBezierCurve } + } + Behavior on blurRadius { + NumberAnimation { duration: root.animDuration; easing.type: root.animEasingTransition; easing.bezierCurve: root.animBezierCurve } + } + Behavior on noiseOpacity { + NumberAnimation { duration: root.animDuration; easing.type: root.animEasingTransition; easing.bezierCurve: root.animBezierCurve } + } + Behavior on saturation { + NumberAnimation { duration: root.animDuration; easing.type: root.animEasingTransition; easing.bezierCurve: root.animBezierCurve } + } + Behavior on gradientColor { + ColorAnimation { duration: root.animDurationSlow; easing.type: root.animEasingTransition; easing.bezierCurve: root.animBezierCurve } + } + Behavior on glowColor { + ColorAnimation { duration: root.animDurationSlow; easing.type: root.animEasingTransition; easing.bezierCurve: root.animBezierCurve } + } + function containerColor(color, defaultAlpha) { var strength = transparencyEnabled ? transparencyLevel : 0 var alpha = 1 - strength * (1 - defaultAlpha) return Qt.rgba(color.r, color.g, color.b, alpha) } - readonly property color chromeBackground: containerColor(mantle, 0.75) - readonly property color contentBackground: containerColor(base, 0.65) + // Glass-aware container color: uses per-zone opacity when effects active + function glassColor(color, zoneOpacity, fallbackAlpha) { + if (hasEffects && transparencyEnabled) { + return Qt.rgba(color.r, color.g, color.b, zoneOpacity) + } + return containerColor(color, fallbackAlpha) + } + + readonly property color chromeBackground: hasEffects + ? glassColor(mantle, toolbarOpacity, 0.75) + : containerColor(mantle, 0.75) + readonly property color contentBackground: hasEffects + ? glassColor(base, contentOpacity, 0.65) + : containerColor(base, 0.65) readonly property color overlayBackground: containerColor(mantle, 0.88) readonly property color popupBackground: containerColor(crust, 0.88) + readonly property color sidebarBackground: hasEffects + ? glassColor(crust, sidebarOpacity, 0.80) + : containerColor(crust, 0.80) } diff --git a/src/services/themeloader.cpp b/src/services/themeloader.cpp index c3b3224..0e5aee1 100644 --- a/src/services/themeloader.cpp +++ b/src/services/themeloader.cpp @@ -54,9 +54,32 @@ void ThemeLoader::watchThemeFile(const QString &filePath) m_watcher.addPath(QFileInfo(filePath).absolutePath()); } +void ThemeLoader::resetEffects() +{ + m_hasEffects = false; + m_sidebarOpacity = 1.0; + m_contentOpacity = 1.0; + m_toolbarOpacity = 1.0; + m_gradientEnabled = false; + m_gradientColor = QColor("#000000"); + m_gradientDirection = QStringLiteral("top_to_bottom"); + m_glowEnabled = false; + m_glowColor = QColor("#ffffff"); + m_glowRadius = 0.0; + m_glowOpacity = 0.0; + m_blurEnabled = false; + m_blurRadius = 0.0; + m_noiseEnabled = false; + m_noiseOpacity = 0.0; + m_saturation = 1.0; + m_refractionEnabled = false; + m_refractionStrength = 0.0; +} + void ThemeLoader::applyThemeFile(const QString &filePath) { m_colors = s_defaults; + resetEffects(); try { auto config = toml::parse_file(filePath.toStdString()); if (auto colors = config["colors"].as_table()) { @@ -69,6 +92,55 @@ void ThemeLoader::applyThemeFile(const QString &filePath) } } } + // Parse optional [effects] section for glass themes + if (auto effects = config["effects"].as_table()) { + m_hasEffects = true; + + if (auto v = effects->get("sidebar_opacity")) + m_sidebarOpacity = qBound(0.0, v->value_or(1.0), 1.0); + if (auto v = effects->get("content_opacity")) + m_contentOpacity = qBound(0.0, v->value_or(1.0), 1.0); + if (auto v = effects->get("toolbar_opacity")) + m_toolbarOpacity = qBound(0.0, v->value_or(1.0), 1.0); + + if (auto v = effects->get("gradient_enabled")) + m_gradientEnabled = v->value_or(false); + if (auto v = effects->get("gradient_color")) { + QColor c(QString::fromStdString(v->value_or(std::string("#000000")))); + if (c.isValid()) m_gradientColor = c; + } + if (auto v = effects->get("gradient_direction")) + m_gradientDirection = QString::fromStdString(v->value_or(std::string("top_to_bottom"))); + + if (auto v = effects->get("glow_enabled")) + m_glowEnabled = v->value_or(false); + if (auto v = effects->get("glow_color")) { + QColor c(QString::fromStdString(v->value_or(std::string("#ffffff")))); + if (c.isValid()) m_glowColor = c; + } + if (auto v = effects->get("glow_radius")) + m_glowRadius = qBound(0.0, v->value_or(0.0), 64.0); + if (auto v = effects->get("glow_opacity")) + m_glowOpacity = qBound(0.0, v->value_or(0.0), 1.0); + + if (auto v = effects->get("blur_enabled")) + m_blurEnabled = v->value_or(false); + if (auto v = effects->get("blur_radius")) + m_blurRadius = qBound(0.0, v->value_or(0.0), 64.0); + + if (auto v = effects->get("noise_enabled")) + m_noiseEnabled = v->value_or(false); + if (auto v = effects->get("noise_opacity")) + m_noiseOpacity = qBound(0.0, v->value_or(0.0), 0.2); + + if (auto v = effects->get("saturation")) + m_saturation = qBound(0.5, v->value_or(1.0), 2.0); + + if (auto v = effects->get("refraction_enabled")) + m_refractionEnabled = v->value_or(false); + if (auto v = effects->get("refraction_strength")) + m_refractionStrength = qBound(0.0, v->value_or(0.0), 0.1); + } } catch (const toml::parse_error &err) { qWarning() << "Theme parse error:" << err.what(); } @@ -95,6 +167,7 @@ void ThemeLoader::loadTheme(const QString &nameOrPath, const QStringList &themes if (filePath.isEmpty() || !QFile::exists(filePath)) { qWarning() << "Theme not found:" << nameOrPath; m_colors = s_defaults; + resetEffects(); emit themeChanged(); return; } diff --git a/src/services/themeloader.h b/src/services/themeloader.h index 123c20e..4b914b2 100644 --- a/src/services/themeloader.h +++ b/src/services/themeloader.h @@ -21,6 +21,27 @@ class ThemeLoader : public QObject Q_PROPERTY(QColor warning READ warning NOTIFY themeChanged) Q_PROPERTY(QColor error READ error NOTIFY themeChanged) + // Glass effects properties — optional [effects] section in theme TOML + Q_PROPERTY(bool hasEffects READ hasEffects NOTIFY themeChanged) + Q_PROPERTY(double sidebarOpacity READ sidebarOpacity NOTIFY themeChanged) + Q_PROPERTY(double contentOpacity READ contentOpacity NOTIFY themeChanged) + Q_PROPERTY(double toolbarOpacity READ toolbarOpacity NOTIFY themeChanged) + Q_PROPERTY(bool gradientEnabled READ gradientEnabled NOTIFY themeChanged) + Q_PROPERTY(QColor gradientColor READ gradientColor NOTIFY themeChanged) + Q_PROPERTY(QString gradientDirection READ gradientDirection NOTIFY themeChanged) + Q_PROPERTY(bool glowEnabled READ glowEnabled NOTIFY themeChanged) + Q_PROPERTY(QColor glowColor READ glowColor NOTIFY themeChanged) + Q_PROPERTY(double glowRadius READ glowRadius NOTIFY themeChanged) + Q_PROPERTY(double glowOpacity READ glowOpacity NOTIFY themeChanged) + Q_PROPERTY(bool blurEnabled READ blurEnabled NOTIFY themeChanged) + Q_PROPERTY(double blurRadius READ blurRadius NOTIFY themeChanged) + Q_PROPERTY(bool noiseEnabled READ noiseEnabled NOTIFY themeChanged) + Q_PROPERTY(double noiseOpacity READ noiseOpacity NOTIFY themeChanged) + Q_PROPERTY(double saturation READ saturation NOTIFY themeChanged) + Q_PROPERTY(bool refractionEnabled READ refractionEnabled NOTIFY themeChanged) + Q_PROPERTY(double refractionStrength READ refractionStrength NOTIFY themeChanged) + Q_PROPERTY(bool compositorBlurAvailable READ compositorBlurAvailable WRITE setCompositorBlurAvailable NOTIFY compositorBlurAvailableChanged) + public: explicit ThemeLoader(QObject *parent = nullptr); void loadTheme(const QString &nameOrPath, const QStringList &themesDirs); @@ -37,15 +58,66 @@ class ThemeLoader : public QObject QColor success() const { return color("success"); } QColor warning() const { return color("warning"); } QColor error() const { return color("error"); } + + // Glass effects accessors + bool hasEffects() const { return m_hasEffects; } + double sidebarOpacity() const { return m_sidebarOpacity; } + double contentOpacity() const { return m_contentOpacity; } + double toolbarOpacity() const { return m_toolbarOpacity; } + bool gradientEnabled() const { return m_gradientEnabled; } + QColor gradientColor() const { return m_gradientColor; } + QString gradientDirection() const { return m_gradientDirection; } + bool glowEnabled() const { return m_glowEnabled; } + QColor glowColor() const { return m_glowColor; } + double glowRadius() const { return m_glowRadius; } + double glowOpacity() const { return m_glowOpacity; } + bool blurEnabled() const { return m_blurEnabled; } + double blurRadius() const { return m_blurRadius; } + bool noiseEnabled() const { return m_noiseEnabled; } + double noiseOpacity() const { return m_noiseOpacity; } + double saturation() const { return m_saturation; } + bool refractionEnabled() const { return m_refractionEnabled; } + double refractionStrength() const { return m_refractionStrength; } + bool compositorBlurAvailable() const { return m_compositorBlurAvailable; } + void setCompositorBlurAvailable(bool available) { + if (m_compositorBlurAvailable != available) { + m_compositorBlurAvailable = available; + emit compositorBlurAvailableChanged(); + } + } + signals: void themeChanged(); + void compositorBlurAvailableChanged(); private: void applyThemeFile(const QString &filePath); void watchThemeFile(const QString &filePath); + void resetEffects(); QMap m_colors; QFileSystemWatcher m_watcher; QString m_watchedPath; QDateTime m_watchedModified; static QMap s_defaults; + + // Glass effects state + bool m_hasEffects = false; + bool m_compositorBlurAvailable = false; + double m_sidebarOpacity = 1.0; + double m_contentOpacity = 1.0; + double m_toolbarOpacity = 1.0; + bool m_gradientEnabled = false; + QColor m_gradientColor{"#000000"}; + QString m_gradientDirection{"top_to_bottom"}; + bool m_glowEnabled = false; + QColor m_glowColor{"#ffffff"}; + double m_glowRadius = 0.0; + double m_glowOpacity = 0.0; + bool m_blurEnabled = false; + double m_blurRadius = 0.0; + bool m_noiseEnabled = false; + double m_noiseOpacity = 0.0; + double m_saturation = 1.0; + bool m_refractionEnabled = false; + double m_refractionStrength = 0.0; }; diff --git a/tests/tst_themeloader.cpp b/tests/tst_themeloader.cpp index 73a5c59..f22a66c 100644 --- a/tests/tst_themeloader.cpp +++ b/tests/tst_themeloader.cpp @@ -224,6 +224,74 @@ private slots: QVERIFY(spy.wait(5000)); QCOMPARE(loader.color("base"), QColor("#222222")); } + + void testLiquidDarkTheme() + { + ThemeLoader loader; + loader.loadTheme("liquid-dark", QStringList{QString(THEMES_DIR)}); + QCOMPARE(loader.color("base"), QColor("#0a1628")); + QCOMPARE(loader.color("mantle"), QColor("#0d1f3c")); + QCOMPARE(loader.color("accent"), QColor("#4da6ff")); + QVERIFY(loader.hasEffects()); + QCOMPARE(loader.sidebarOpacity(), 0.72); + QCOMPARE(loader.contentOpacity(), 0.58); + QCOMPARE(loader.toolbarOpacity(), 0.70); + QVERIFY(loader.gradientEnabled()); + QCOMPARE(loader.gradientColor(), QColor("#0a1e3d")); + QCOMPARE(loader.gradientDirection(), QStringLiteral("top_to_bottom")); + QVERIFY(loader.glowEnabled()); + QCOMPARE(loader.glowColor(), QColor("#3388cc")); + QCOMPARE(loader.glowRadius(), 16.0); + QCOMPARE(loader.glowOpacity(), 0.35); + QVERIFY(loader.blurEnabled()); + QCOMPARE(loader.blurRadius(), 32.0); + QVERIFY(loader.noiseEnabled()); + QCOMPARE(loader.noiseOpacity(), 0.04); + QCOMPARE(loader.saturation(), 1.15); + QVERIFY(loader.refractionEnabled()); + QCOMPARE(loader.refractionStrength(), 0.02); + } + + void testLiquidLightTheme() + { + ThemeLoader loader; + loader.loadTheme("liquid-light", QStringList{QString(THEMES_DIR)}); + QCOMPARE(loader.color("base"), QColor("#eaf2fb")); + QCOMPARE(loader.color("accent"), QColor("#2080e0")); + QVERIFY(loader.hasEffects()); + QCOMPARE(loader.sidebarOpacity(), 0.75); + QCOMPARE(loader.contentOpacity(), 0.62); + QVERIFY(loader.blurEnabled()); + } + + void testEffectsFallbackForStandardTheme() + { + ThemeLoader loader; + loader.loadTheme("catppuccin-mocha", QStringList{QString(THEMES_DIR)}); + QVERIFY(!loader.hasEffects()); + QCOMPARE(loader.sidebarOpacity(), 1.0); + QCOMPARE(loader.contentOpacity(), 1.0); + QCOMPARE(loader.toolbarOpacity(), 1.0); + QVERIFY(!loader.gradientEnabled()); + QVERIFY(!loader.glowEnabled()); + QVERIFY(!loader.blurEnabled()); + QVERIFY(!loader.noiseEnabled()); + QCOMPARE(loader.saturation(), 1.0); + QVERIFY(!loader.refractionEnabled()); + } + + void testCompositorBlurAvailableProperty() + { + ThemeLoader loader; + QVERIFY(!loader.compositorBlurAvailable()); + QSignalSpy spy(&loader, &ThemeLoader::compositorBlurAvailableChanged); + loader.setCompositorBlurAvailable(true); + QVERIFY(loader.compositorBlurAvailable()); + QCOMPARE(spy.count(), 1); + // Idempotent call doesn't emit signal + loader.setCompositorBlurAvailable(true); + QCOMPARE(spy.count(), 1); + } }; QTEST_MAIN(TestThemeLoader) diff --git a/themes/liquid-dark.toml b/themes/liquid-dark.toml new file mode 100644 index 0000000..ce2d5c7 --- /dev/null +++ b/themes/liquid-dark.toml @@ -0,0 +1,38 @@ +[colors] +base = "#0a1628" +mantle = "#0d1f3c" +crust = "#061020" +surface = "#142a4a" +overlay = "#1a3358" +text = "#e0e8f5" +subtext = "#b0c0d8" +muted = "#5a7090" +accent = "#4da6ff" +success = "#5ce0a0" +warning = "#ffc04d" +error = "#ff6b8a" + +[effects] +sidebar_opacity = 0.72 +content_opacity = 0.58 +toolbar_opacity = 0.70 + +gradient_enabled = true +gradient_color = "#0a1e3d" +gradient_direction = "top_to_bottom" + +glow_enabled = true +glow_color = "#3388cc" +glow_radius = 16.0 +glow_opacity = 0.35 + +blur_enabled = true +blur_radius = 32.0 + +noise_enabled = true +noise_opacity = 0.04 + +saturation = 1.15 + +refraction_enabled = true +refraction_strength = 0.02 diff --git a/themes/liquid-light.toml b/themes/liquid-light.toml new file mode 100644 index 0000000..1d627f3 --- /dev/null +++ b/themes/liquid-light.toml @@ -0,0 +1,38 @@ +[colors] +base = "#eaf2fb" +mantle = "#dce8f5" +crust = "#d0dff0" +surface = "#c4d6ec" +overlay = "#b0c8e0" +text = "#1a2a40" +subtext = "#2a3d5a" +muted = "#6882a0" +accent = "#2080e0" +success = "#28a060" +warning = "#d89020" +error = "#d04060" + +[effects] +sidebar_opacity = 0.75 +content_opacity = 0.62 +toolbar_opacity = 0.72 + +gradient_enabled = true +gradient_color = "#c8ddf5" +gradient_direction = "top_to_bottom" + +glow_enabled = true +glow_color = "#80b8e8" +glow_radius = 14.0 +glow_opacity = 0.28 + +blur_enabled = true +blur_radius = 28.0 + +noise_enabled = true +noise_opacity = 0.03 + +saturation = 1.10 + +refraction_enabled = true +refraction_strength = 0.015