From f0fcc77bdbd54693cba354faa951d9ff6cd878ec Mon Sep 17 00:00:00 2001 From: purian23 Date: Sun, 1 Mar 2026 00:54:31 -0500 Subject: [PATCH] feat: Implement M3 design elevation & shadow effects - Added global toggles in the Themes tab - Light color & directional user ovverides - Independent shadow overrides per/bar - Refactored various components to sync the updated designs --- quickshell/Common/ElevationShadow.qml | 54 +++ quickshell/Common/SettingsData.qml | 22 +- quickshell/Common/Theme.qml | 226 +++++++++++ quickshell/Common/settings/SettingsSpec.js | 13 +- quickshell/Common/settings/SettingsStore.js | 19 + quickshell/Modals/Common/DankModal.qml | 24 +- .../DankLauncherV2/DankLauncherV2Modal.qml | 17 +- .../ControlCenter/ControlCenterPopout.qml | 2 +- quickshell/Modules/DankBar/BarCanvas.qml | 109 ++--- quickshell/Modules/DankBar/DankBarWindow.qml | 18 +- quickshell/Modules/DankBar/Widgets/Media.qml | 10 +- .../Modules/DankBar/Widgets/SystemTrayBar.qml | 90 ++--- .../Modules/DankBar/Widgets/SystemUpdate.qml | 5 +- .../Modules/DankDash/MediaDropdownOverlay.qml | 60 +-- .../Modules/DankDash/MediaPlayerTab.qml | 17 +- quickshell/Modules/DankDash/WeatherTab.qml | 43 +- .../Center/HistoryNotificationCard.qml | 19 +- .../Center/HistoryNotificationList.qml | 14 +- .../KeyboardNavigatedNotificationList.qml | 27 +- .../Notifications/Center/NotificationCard.qml | 66 ++- .../Center/NotificationCenterPopout.qml | 67 +++- .../Notifications/Popup/NotificationPopup.qml | 158 +++++--- quickshell/Modules/Settings/AboutTab.qml | 17 +- quickshell/Modules/Settings/DankBarTab.qml | 375 +++++++++++------- .../Modules/Settings/NotificationsTab.qml | 2 +- .../Modules/Settings/ThemeColorsTab.qml | 248 ++++++++++-- .../Modules/Settings/TimeWeatherTab.qml | 15 +- quickshell/Modules/Toast.qml | 18 +- .../WorkspaceOverlays/OverviewWidget.qml | 215 +++++----- quickshell/Widgets/AppIconRenderer.qml | 2 +- quickshell/Widgets/DankCircularImage.qml | 2 + quickshell/Widgets/DankDropdown.qml | 20 +- quickshell/Widgets/DankIconPicker.qml | 19 +- quickshell/Widgets/DankOSD.qml | 41 +- quickshell/Widgets/DankPopout.qml | 116 +++--- .../matugen/templates/firefox-userchrome.css | 14 +- .../translations/settings_search_index.json | 68 ++++ 37 files changed, 1599 insertions(+), 653 deletions(-) create mode 100644 quickshell/Common/ElevationShadow.qml diff --git a/quickshell/Common/ElevationShadow.qml b/quickshell/Common/ElevationShadow.qml new file mode 100644 index 00000000..dab80cdb --- /dev/null +++ b/quickshell/Common/ElevationShadow.qml @@ -0,0 +1,54 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Effects +import qs.Common + +Item { + id: root + + property var level: Theme.elevationLevel2 + property string direction: Theme.elevationLightDirection + property real fallbackOffset: 4 + + property color targetColor: "white" + property real targetRadius: Theme.cornerRadius + property color borderColor: "transparent" + property real borderWidth: 0 + + property bool shadowEnabled: Theme.elevationEnabled + property real shadowBlurPx: level && level.blurPx !== undefined ? level.blurPx : 0 + property real shadowSpreadPx: level && level.spreadPx !== undefined ? level.spreadPx : 0 + property real shadowOffsetX: Theme.elevationOffsetXFor(level, direction, fallbackOffset) + property real shadowOffsetY: Theme.elevationOffsetYFor(level, direction, fallbackOffset) + property color shadowColor: Theme.elevationShadowColor(level) + property real shadowOpacity: 1 + property real blurMax: Theme.elevationBlurMax + + property alias sourceRect: sourceRect + + layer.enabled: shadowEnabled + + layer.effect: MultiEffect { + autoPaddingEnabled: true + shadowEnabled: true + blurEnabled: false + maskEnabled: false + shadowBlur: Math.max(0, Math.min(1, root.shadowBlurPx / Math.max(1, root.blurMax))) + shadowScale: 1 + (2 * root.shadowSpreadPx) / Math.max(1, Math.min(root.width, root.height)) + shadowHorizontalOffset: root.shadowOffsetX + shadowVerticalOffset: root.shadowOffsetY + blurMax: root.blurMax + shadowColor: root.shadowColor + shadowOpacity: root.shadowOpacity + } + + Rectangle { + id: sourceRect + anchors.fill: parent + radius: root.targetRadius + color: root.targetColor + border.color: root.borderColor + border.width: root.borderWidth + } +} diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index 9a901ef6..be24edee 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -14,7 +14,7 @@ import "settings/SettingsStore.js" as Store Singleton { id: root - readonly property int settingsConfigVersion: 5 + readonly property int settingsConfigVersion: 6 readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true" @@ -166,6 +166,24 @@ Singleton { property int modalCustomAnimationDuration: 150 property bool enableRippleEffects: true onEnableRippleEffectsChanged: saveSettings() + property bool m3ElevationEnabled: true + onM3ElevationEnabledChanged: saveSettings() + property int m3ElevationIntensity: 12 + onM3ElevationIntensityChanged: saveSettings() + property int m3ElevationOpacity: 30 + onM3ElevationOpacityChanged: saveSettings() + property string m3ElevationColorMode: "default" + onM3ElevationColorModeChanged: saveSettings() + property string m3ElevationLightDirection: "top" + onM3ElevationLightDirectionChanged: saveSettings() + property string m3ElevationCustomColor: "#000000" + onM3ElevationCustomColorChanged: saveSettings() + property bool modalElevationEnabled: true + onModalElevationEnabledChanged: saveSettings() + property bool popoutElevationEnabled: true + onPopoutElevationEnabledChanged: saveSettings() + property bool barElevationEnabled: true + onBarElevationEnabledChanged: saveSettings() property string wallpaperFillMode: "Fill" property bool blurredWallpaperLayer: false property bool blurWallpaperOnOverview: false @@ -609,7 +627,7 @@ Singleton { "scrollYBehavior": "workspace", "shadowIntensity": 0, "shadowOpacity": 60, - "shadowColorMode": "text", + "shadowColorMode": "default", "shadowCustomColor": "#000000", "clickThrough": false } diff --git a/quickshell/Common/Theme.qml b/quickshell/Common/Theme.qml index f19ae06b..5d66e1dc 100644 --- a/quickshell/Common/Theme.qml +++ b/quickshell/Common/Theme.qml @@ -673,6 +673,232 @@ Singleton { property color shadowMedium: Qt.rgba(0, 0, 0, 0.08) property color shadowStrong: Qt.rgba(0, 0, 0, 0.3) + readonly property bool elevationEnabled: typeof SettingsData !== "undefined" && (SettingsData.m3ElevationEnabled ?? true) + readonly property real elevationBlurMax: typeof SettingsData !== "undefined" && SettingsData.m3ElevationIntensity !== undefined ? Math.min(128, Math.max(32, SettingsData.m3ElevationIntensity * 2)) : 64 + + readonly property real _elevMult: typeof SettingsData !== "undefined" && SettingsData.m3ElevationIntensity !== undefined ? SettingsData.m3ElevationIntensity / 12 : 1 + readonly property real _opMult: typeof SettingsData !== "undefined" && SettingsData.m3ElevationOpacity !== undefined ? SettingsData.m3ElevationOpacity / 60 : 1 + function normalizeElevationDirection(direction) { + switch (direction) { + case "top": + case "topLeft": + case "topRight": + case "bottom": + case "bottomLeft": + case "bottomRight": + case "left": + case "right": + case "autoBar": + return direction; + default: + return "top"; + } + } + + readonly property string elevationLightDirection: { + if (typeof SettingsData === "undefined" || !SettingsData.m3ElevationLightDirection) + return "top"; + switch (SettingsData.m3ElevationLightDirection) { + case "autoBar": + case "top": + case "topLeft": + case "topRight": + case "bottom": + return SettingsData.m3ElevationLightDirection; + default: + return "top"; + } + } + readonly property real _elevDiagRatio: 0.55 + readonly property string _globalElevationDirForTokens: { + const normalized = normalizeElevationDirection(elevationLightDirection); + return normalized === "autoBar" ? "top" : normalized; + } + readonly property real _elevDirX: { + switch (_globalElevationDirForTokens) { + case "topLeft": + case "bottomLeft": + case "left": + return 1; + case "topRight": + case "bottomRight": + case "right": + return -1; + default: + return 0; + } + } + readonly property real _elevDirY: { + switch (_globalElevationDirForTokens) { + case "bottom": + case "bottomLeft": + case "bottomRight": + return -1; + case "left": + case "right": + return 0; + default: + return 1; + } + } + readonly property real _elevDirXScale: (_globalElevationDirForTokens === "left" || _globalElevationDirForTokens === "right") ? 1 : _elevDiagRatio + + readonly property var elevationLevel1: ({ + blurPx: 4 * _elevMult, + offsetX: 1 * _elevMult * _elevDirXScale * _elevDirX, + offsetY: 1 * _elevMult * _elevDirY, + spreadPx: 0, + alpha: 0.2 * _opMult + }) + readonly property var elevationLevel2: ({ + blurPx: 8 * _elevMult, + offsetX: 4 * _elevMult * _elevDirXScale * _elevDirX, + offsetY: 4 * _elevMult * _elevDirY, + spreadPx: 0, + alpha: 0.25 * _opMult + }) + readonly property var elevationLevel3: ({ + blurPx: 12 * _elevMult, + offsetX: 6 * _elevMult * _elevDirXScale * _elevDirX, + offsetY: 6 * _elevMult * _elevDirY, + spreadPx: 0, + alpha: 0.3 * _opMult + }) + readonly property var elevationLevel4: ({ + blurPx: 16 * _elevMult, + offsetX: 8 * _elevMult * _elevDirXScale * _elevDirX, + offsetY: 8 * _elevMult * _elevDirY, + spreadPx: 0, + alpha: 0.3 * _opMult + }) + readonly property var elevationLevel5: ({ + blurPx: 20 * _elevMult, + offsetX: 10 * _elevMult * _elevDirXScale * _elevDirX, + offsetY: 10 * _elevMult * _elevDirY, + spreadPx: 0, + alpha: 0.3 * _opMult + }) + + function elevationOffsetMagnitude(level, fallback, direction) { + if (!level) { + return fallback !== undefined ? Math.abs(fallback) : 0; + } + const yMag = Math.abs(level.offsetY !== undefined ? level.offsetY : 0); + if (yMag > 0) + return yMag; + const xMag = Math.abs(level.offsetX !== undefined ? level.offsetX : 0); + if (xMag > 0) { + if (direction === "left" || direction === "right") + return xMag; + return xMag / _elevDiagRatio; + } + return fallback !== undefined ? Math.abs(fallback) : 0; + } + + function elevationOffsetXFor(level, direction, fallback) { + const dir = normalizeElevationDirection(direction || elevationLightDirection); + const mag = elevationOffsetMagnitude(level, fallback, dir); + switch (dir) { + case "topLeft": + case "bottomLeft": + return mag * _elevDiagRatio; + case "topRight": + case "bottomRight": + return -mag * _elevDiagRatio; + case "left": + return mag; + case "right": + return -mag; + default: + return 0; + } + } + + function elevationOffsetYFor(level, direction, fallback) { + const dir = normalizeElevationDirection(direction || elevationLightDirection); + const mag = elevationOffsetMagnitude(level, fallback, dir); + switch (dir) { + case "bottom": + case "bottomLeft": + case "bottomRight": + return -mag; + case "left": + case "right": + return 0; + default: + return mag; + } + } + + function elevationOffsetX(level, fallback) { + return elevationOffsetXFor(level, elevationLightDirection, fallback); + } + + function elevationOffsetY(level, fallback) { + return elevationOffsetYFor(level, elevationLightDirection, fallback); + } + + function elevationRenderPadding(level, direction, fallbackOffset, extraPadding, minPadding) { + const dir = direction !== undefined ? direction : elevationLightDirection; + const blur = (level && level.blurPx !== undefined) ? Math.max(0, level.blurPx) : 0; + const spread = (level && level.spreadPx !== undefined) ? Math.max(0, level.spreadPx) : 0; + const fallback = fallbackOffset !== undefined ? fallbackOffset : 0; + const extra = extraPadding !== undefined ? extraPadding : 8; + const minPad = minPadding !== undefined ? minPadding : 16; + const offsetX = Math.abs(elevationOffsetXFor(level, dir, fallback)); + const offsetY = Math.abs(elevationOffsetYFor(level, dir, fallback)); + return Math.max(minPad, blur + spread + Math.max(offsetX, offsetY) + extra); + } + + function elevationShadowColor(level) { + const alpha = (level && level.alpha !== undefined) ? level.alpha : 0.3; + let r = 0; + let g = 0; + let b = 0; + + if (typeof SettingsData !== "undefined") { + const mode = SettingsData.m3ElevationColorMode || "default"; + if (mode === "default") { + r = 0; + g = 0; + b = 0; + } else if (mode === "text") { + r = surfaceText.r; + g = surfaceText.g; + b = surfaceText.b; + } else if (mode === "primary") { + r = primary.r; + g = primary.g; + b = primary.b; + } else if (mode === "surfaceVariant") { + r = surfaceVariant.r; + g = surfaceVariant.g; + b = surfaceVariant.b; + } else if (mode === "custom" && SettingsData.m3ElevationCustomColor) { + const c = Qt.color(SettingsData.m3ElevationCustomColor); + r = c.r; + g = c.g; + b = c.b; + } + } + return Qt.rgba(r, g, b, alpha); + } + function elevationTintOpacity(level) { + if (!level) + return 0; + if (level === elevationLevel1) + return 0.05; + if (level === elevationLevel2) + return 0.08; + if (level === elevationLevel3) + return 0.11; + if (level === elevationLevel4) + return 0.12; + if (level === elevationLevel5) + return 0.14; + return 0.08; + } + readonly property var animationDurations: [ { "shorter": 0, diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index 3abf845d..90b0bbaa 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -21,7 +21,7 @@ var SPEC = { widgetColorMode: { def: "default" }, controlCenterTileColorMode: { def: "primary" }, buttonColorMode: { def: "primary" }, - cornerRadius: { def: 12, onChange: "updateCompositorLayout" }, + cornerRadius: { def: 16, onChange: "updateCompositorLayout" }, niriLayoutGapsOverride: { def: -1, onChange: "updateCompositorLayout" }, niriLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" }, niriLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" }, @@ -47,6 +47,15 @@ var SPEC = { modalAnimationSpeed: { def: 1 }, modalCustomAnimationDuration: { def: 150 }, enableRippleEffects: { def: true }, + m3ElevationEnabled: { def: true }, + m3ElevationIntensity: { def: 12 }, + m3ElevationOpacity: { def: 30 }, + m3ElevationColorMode: { def: "default" }, + m3ElevationLightDirection: { def: "top" }, + m3ElevationCustomColor: { def: "#000000" }, + modalElevationEnabled: { def: true }, + popoutElevationEnabled: { def: true }, + barElevationEnabled: { def: true }, wallpaperFillMode: { def: "Fill" }, blurredWallpaperLayer: { def: false }, blurWallpaperOnOverview: { def: false }, @@ -432,7 +441,7 @@ var SPEC = { scrollYBehavior: "workspace", shadowIntensity: 0, shadowOpacity: 60, - shadowColorMode: "text", + shadowColorMode: "default", shadowCustomColor: "#000000", clickThrough: false }], onChange: "updateBarConfigs" diff --git a/quickshell/Common/settings/SettingsStore.js b/quickshell/Common/settings/SettingsStore.js index 96e419a8..f88bb03b 100644 --- a/quickshell/Common/settings/SettingsStore.js +++ b/quickshell/Common/settings/SettingsStore.js @@ -229,6 +229,25 @@ function migrateToVersion(obj, targetVersion) { settings.configVersion = 5; } + if (currentVersion < 6) { + console.info("Migrating settings from version", currentVersion, "to version 6"); + + if (settings.barElevationEnabled === undefined) { + var legacyBars = Array.isArray(settings.barConfigs) ? settings.barConfigs : []; + var hadLegacyBarShadowEnabled = false; + for (var j = 0; j < legacyBars.length; j++) { + var legacyIntensity = Number(legacyBars[j] && legacyBars[j].shadowIntensity); + if (!isNaN(legacyIntensity) && legacyIntensity > 0) { + hadLegacyBarShadowEnabled = true; + break; + } + } + settings.barElevationEnabled = hadLegacyBarShadowEnabled; + } + + settings.configVersion = 6; + } + return settings; } diff --git a/quickshell/Modals/Common/DankModal.qml b/quickshell/Modals/Common/DankModal.qml index 3c94d189..f955ac64 100644 --- a/quickshell/Modals/Common/DankModal.qml +++ b/quickshell/Modals/Common/DankModal.qml @@ -32,9 +32,9 @@ Item { property list animationExitCurve: Theme.expressiveCurves.emphasized property color backgroundColor: Theme.surfaceContainer property color borderColor: Theme.outlineMedium - property real borderWidth: 1 + property real borderWidth: 0 property real cornerRadius: Theme.cornerRadius - property bool enableShadow: false + property bool enableShadow: true property alias modalFocusScope: focusScope property bool shouldBeVisible: false property bool shouldHaveFocus: shouldBeVisible @@ -142,7 +142,11 @@ Item { } } - readonly property real shadowBuffer: 5 + readonly property var shadowLevel: Theme.elevationLevel3 + readonly property real shadowFallbackOffset: 6 + readonly property real shadowRenderPadding: (root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0 + readonly property real shadowMotionPadding: animationType === "slide" ? 30 : Math.max(0, animationOffset) + readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr) readonly property real alignedWidth: Theme.px(modalWidth, dpr) readonly property real alignedHeight: Theme.px(modalHeight, dpr) @@ -377,12 +381,16 @@ Item { } } - Rectangle { + ElevationShadow { + id: modalShadowLayer anchors.fill: parent - color: root.backgroundColor - border.color: root.borderColor - border.width: root.borderWidth - radius: root.cornerRadius + level: root.shadowLevel + fallbackOffset: root.shadowFallbackOffset + targetRadius: root.cornerRadius + targetColor: root.backgroundColor + borderColor: root.borderColor + borderWidth: root.borderWidth + shadowEnabled: root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" } FocusScope { diff --git a/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml b/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml index 9668b7d7..9e60ff36 100644 --- a/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml +++ b/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml @@ -1,4 +1,5 @@ import QtQuick +import QtQuick.Effects import Quickshell import Quickshell.Wayland import Quickshell.Hyprland @@ -75,7 +76,7 @@ Item { return Theme.primary; } } - readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 1 + readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0 signal dialogClosed @@ -390,12 +391,16 @@ Item { } } - Rectangle { + ElevationShadow { + id: launcherShadowLayer anchors.fill: parent - color: root.backgroundColor - border.color: root.borderColor - border.width: root.borderWidth - radius: root.cornerRadius + level: Theme.elevationLevel3 + fallbackOffset: 6 + targetColor: root.backgroundColor + borderColor: root.borderColor + borderWidth: root.borderWidth + targetRadius: root.cornerRadius + shadowEnabled: Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" } MouseArea { diff --git a/quickshell/Modules/ControlCenter/ControlCenterPopout.qml b/quickshell/Modules/ControlCenter/ControlCenterPopout.qml index 065d09c1..0b9aa9ea 100644 --- a/quickshell/Modules/ControlCenter/ControlCenterPopout.qml +++ b/quickshell/Modules/ControlCenter/ControlCenterPopout.qml @@ -12,7 +12,7 @@ DankPopout { id: root layerNamespace: "dms:control-center" - fullHeightSurface: true + fullHeightSurface: false property string expandedSection: "" property var triggerScreen: null diff --git a/quickshell/Modules/DankBar/BarCanvas.qml b/quickshell/Modules/DankBar/BarCanvas.qml index ab0bdf97..df3f17fb 100644 --- a/quickshell/Modules/DankBar/BarCanvas.qml +++ b/quickshell/Modules/DankBar/BarCanvas.qml @@ -1,5 +1,4 @@ import QtQuick -import QtQuick.Effects import QtQuick.Shapes import qs.Common import qs.Services @@ -53,15 +52,43 @@ Item { } } - readonly property real shadowIntensity: barConfig?.shadowIntensity ?? 0 - readonly property bool shadowEnabled: shadowIntensity > 0 - readonly property int blurMax: 64 - readonly property real shadowBlurPx: shadowIntensity * 0.2 - readonly property real shadowBlur: Math.max(0, Math.min(1, shadowBlurPx / blurMax)) - readonly property real shadowOpacity: (barConfig?.shadowOpacity ?? 60) / 100 - readonly property string shadowColorMode: barConfig?.shadowColorMode ?? "text" - readonly property color shadowBaseColor: { - switch (shadowColorMode) { + // M3 elevation shadow — Level 2 baseline (navigation bar), with per-bar override support + readonly property bool hasPerBarOverride: (barConfig?.shadowIntensity ?? 0) > 0 + readonly property var elevLevel: Theme.elevationLevel2 + readonly property bool shadowEnabled: (Theme.elevationEnabled && (typeof SettingsData !== "undefined" ? (SettingsData.barElevationEnabled ?? true) : false)) || hasPerBarOverride + readonly property string autoBarShadowDirection: isTop ? "top" : (isBottom ? "bottom" : (isLeft ? "left" : (isRight ? "right" : "top"))) + readonly property string globalShadowDirection: Theme.elevationLightDirection === "autoBar" ? autoBarShadowDirection : Theme.elevationLightDirection + readonly property string perBarShadowDirectionMode: barConfig?.shadowDirectionMode ?? "inherit" + readonly property string perBarManualShadowDirection: { + switch (barConfig?.shadowDirection) { + case "top": + case "topLeft": + case "topRight": + case "bottom": + return barConfig.shadowDirection; + default: + return "top"; + } + } + readonly property string effectiveShadowDirection: { + if (!hasPerBarOverride) + return globalShadowDirection; + switch (perBarShadowDirectionMode) { + case "autoBar": + return autoBarShadowDirection; + case "manual": + return perBarManualShadowDirection === "autoBar" ? autoBarShadowDirection : perBarManualShadowDirection; + default: + return globalShadowDirection; + } + } + + // Per-bar override values (when barConfig.shadowIntensity > 0) + readonly property real overrideBlurPx: (barConfig?.shadowIntensity ?? 0) * 0.2 + readonly property real overrideOpacity: (barConfig?.shadowOpacity ?? 60) / 100 + readonly property string overrideColorMode: barConfig?.shadowColorMode ?? "default" + readonly property color overrideBaseColor: { + switch (overrideColorMode) { case "surface": return Theme.surface; case "primary": @@ -71,10 +98,16 @@ Item { case "custom": return barConfig?.shadowCustomColor ?? "#000000"; default: - return Theme.surfaceText; + return "#000000"; } } - readonly property color shadowColor: Theme.withAlpha(shadowBaseColor, shadowOpacity * barWindow._backgroundAlpha) + + // Resolved values — per-bar override wins if set, otherwise use global M3 elevation + readonly property real shadowBlurPx: hasPerBarOverride ? overrideBlurPx : (elevLevel.blurPx ?? 8) + readonly property color shadowColor: hasPerBarOverride ? Theme.withAlpha(overrideBaseColor, overrideOpacity) : Theme.elevationShadowColor(elevLevel) + readonly property real shadowOffsetMagnitude: hasPerBarOverride ? (overrideBlurPx * 0.5) : Theme.elevationOffsetMagnitude(elevLevel, 4, effectiveShadowDirection) + readonly property real shadowOffsetX: Theme.elevationOffsetXFor(hasPerBarOverride ? null : elevLevel, effectiveShadowDirection, shadowOffsetMagnitude) + readonly property real shadowOffsetY: Theme.elevationOffsetYFor(hasPerBarOverride ? null : elevLevel, effectiveShadowDirection, shadowOffsetMagnitude) readonly property string mainPath: generatePathForPosition(width, height) readonly property string borderFullPath: generateBorderFullPath(width, height) @@ -118,42 +151,28 @@ Item { } } - Loader { - id: shadowLoader - anchors.fill: parent - active: root.shadowEnabled && mainPathCorrectShape - asynchronous: false - sourceComponent: Item { - anchors.fill: parent + ElevationShadow { + id: barShadow + visible: root.shadowEnabled && root.width > 0 && root.height > 0 - layer.enabled: true - layer.smooth: true - layer.samples: 8 - layer.textureSize: Qt.size(Math.round(width * barWindow._dpr * 2), Math.round(height * barWindow._dpr * 2)) - layer.effect: MultiEffect { - shadowEnabled: true - shadowBlur: root.shadowBlur - shadowColor: root.shadowColor - shadowVerticalOffset: root.isTop ? root.shadowBlurPx * 0.5 : (root.isBottom ? -root.shadowBlurPx * 0.5 : 0) - shadowHorizontalOffset: root.isLeft ? root.shadowBlurPx * 0.5 : (root.isRight ? -root.shadowBlurPx * 0.5 : 0) - autoPaddingEnabled: true - } + // Size to the bar's rectangular body, excluding gothic wing extensions + x: root.isRight ? root.wing : 0 + y: root.isBottom ? root.wing : 0 + width: axis.isVertical ? (parent.width - root.wing) : parent.width + height: axis.isVertical ? parent.height : (parent.height - root.wing) - Shape { - anchors.fill: parent - preferredRendererType: Shape.CurveRenderer + shadowEnabled: root.shadowEnabled + level: root.hasPerBarOverride ? null : root.elevLevel + direction: root.effectiveShadowDirection + fallbackOffset: 4 + targetRadius: root.rt + targetColor: barWindow._bgColor - ShapePath { - fillColor: barWindow._bgColor - strokeColor: "transparent" - strokeWidth: 0 - - PathSvg { - path: root.mainPath - } - } - } - } + shadowBlurPx: root.shadowBlurPx + shadowOffsetX: root.shadowOffsetX + shadowOffsetY: root.shadowOffsetY + shadowColor: root.shadowColor + blurMax: Theme.elevationBlurMax } Loader { diff --git a/quickshell/Modules/DankBar/DankBarWindow.qml b/quickshell/Modules/DankBar/DankBarWindow.qml index d5db45e3..a48ba50d 100644 --- a/quickshell/Modules/DankBar/DankBarWindow.qml +++ b/quickshell/Modules/DankBar/DankBarWindow.qml @@ -140,6 +140,20 @@ PanelWindow { } readonly property real _dpr: CompositorService.getScreenScale(barWindow.screen) + // Shadow buffer: extra window space for shadow to render beyond bar bounds + readonly property bool _shadowActive: (Theme.elevationEnabled && (typeof SettingsData !== "undefined" ? (SettingsData.barElevationEnabled ?? true) : false)) || (barConfig?.shadowIntensity ?? 0) > 0 + readonly property real _shadowBuffer: { + if (!_shadowActive) + return 0; + const hasOverride = (barConfig?.shadowIntensity ?? 0) > 0; + if (hasOverride) { + const blur = (barConfig.shadowIntensity ?? 0) * 0.2; + const offset = blur * 0.5; + return Theme.snap(Math.max(16, blur + offset + 8), _dpr); + } + return Theme.snap(Theme.elevationRenderPadding(Theme.elevationLevel2, "top", 4, 8, 16), _dpr); + } + property string screenName: modelData.name property bool hasMaximizedToplevel: false @@ -354,8 +368,8 @@ PanelWindow { } screen: modelData - implicitHeight: !isVertical ? Theme.px(effectiveBarThickness + effectiveSpacing + ((barConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel ? _wingR : 0), _dpr) : 0 - implicitWidth: isVertical ? Theme.px(effectiveBarThickness + effectiveSpacing + ((barConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel ? _wingR : 0), _dpr) : 0 + implicitHeight: !isVertical ? Theme.px(effectiveBarThickness + effectiveSpacing + ((barConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel ? _wingR : 0), _dpr) + _shadowBuffer : 0 + implicitWidth: isVertical ? Theme.px(effectiveBarThickness + effectiveSpacing + ((barConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel ? _wingR : 0), _dpr) + _shadowBuffer : 0 color: "transparent" property var nativeInhibitor: null diff --git a/quickshell/Modules/DankBar/Widgets/Media.qml b/quickshell/Modules/DankBar/Widgets/Media.qml index 61c71276..c7d6aac9 100644 --- a/quickshell/Modules/DankBar/Widgets/Media.qml +++ b/quickshell/Modules/DankBar/Widgets/Media.qml @@ -178,8 +178,9 @@ BasePill { if (root.popoutTarget && root.popoutTarget.setTriggerPosition) { const globalPos = parent.mapToItem(null, 0, 0); const currentScreen = root.parentScreen || Screen; - const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, root.barThickness, parent.width); - root.popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, root.section, currentScreen); + const barPosition = root.axis?.edge === "left" ? 2 : (root.axis?.edge === "right" ? 3 : (root.axis?.edge === "top" ? 0 : 1)); + const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, root.barThickness, parent.width, root.barSpacing, barPosition, root.barConfig); + root.popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, root.section, currentScreen, barPosition, root.barThickness, root.barSpacing, root.barConfig); } root.clicked(); } @@ -334,8 +335,9 @@ BasePill { if (root.popoutTarget && root.popoutTarget.setTriggerPosition) { const globalPos = mapToItem(null, 0, 0); const currentScreen = root.parentScreen || Screen; - const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, root.barThickness, root.width); - root.popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, root.section, currentScreen); + const barPosition = root.axis?.edge === "left" ? 2 : (root.axis?.edge === "right" ? 3 : (root.axis?.edge === "top" ? 0 : 1)); + const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, root.barThickness, root.width, root.barSpacing, barPosition, root.barConfig); + root.popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, root.section, currentScreen, barPosition, root.barThickness, root.barSpacing, root.barConfig); } root.clicked(); } diff --git a/quickshell/Modules/DankBar/Widgets/SystemTrayBar.qml b/quickshell/Modules/DankBar/Widgets/SystemTrayBar.qml index 750b1d90..5cd01df5 100644 --- a/quickshell/Modules/DankBar/Widgets/SystemTrayBar.qml +++ b/quickshell/Modules/DankBar/Widgets/SystemTrayBar.qml @@ -940,9 +940,10 @@ BasePill { } })(), overflowMenu.dpr) - property real shadowBlurPx: 10 - property real shadowSpreadPx: 0 - property real shadowBaseAlpha: 0.60 + readonly property var elev: Theme.elevationLevel2 + property real shadowBlurPx: elev && elev.blurPx !== undefined ? elev.blurPx : 8 + property real shadowSpreadPx: elev && elev.spreadPx !== undefined ? elev.spreadPx : 0 + property real shadowBaseAlpha: elev && elev.alpha !== undefined ? elev.alpha : 0.25 readonly property real popupSurfaceAlpha: Theme.popupTransparency readonly property real effectiveShadowAlpha: Math.max(0, Math.min(1, shadowBaseAlpha * popupSurfaceAlpha)) @@ -963,37 +964,26 @@ BasePill { } } - Item { + ElevationShadow { id: bgShadowLayer anchors.fill: parent - layer.enabled: true + level: menuContainer.elev + fallbackOffset: 4 + shadowBlurPx: menuContainer.shadowBlurPx + shadowSpreadPx: menuContainer.shadowSpreadPx + shadowColor: { + const baseColor = Theme.isLightMode ? Qt.rgba(0, 0, 0, 1) : Theme.surfaceContainerHighest; + return Theme.withAlpha(baseColor, menuContainer.effectiveShadowAlpha); + } + targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) + targetRadius: Theme.cornerRadius + sourceRect.antialiasing: true + sourceRect.smooth: true + shadowEnabled: Theme.elevationEnabled layer.smooth: true layer.textureSize: Qt.size(Math.round(width * overflowMenu.dpr * 2), Math.round(height * overflowMenu.dpr * 2)) layer.textureMirroring: ShaderEffectSource.MirrorVertically layer.samples: 4 - - readonly property int blurMax: 64 - - layer.effect: MultiEffect { - autoPaddingEnabled: true - shadowEnabled: true - blurEnabled: false - maskEnabled: false - shadowBlur: Math.max(0, Math.min(1, menuContainer.shadowBlurPx / bgShadowLayer.blurMax)) - shadowScale: 1 + (2 * menuContainer.shadowSpreadPx) / Math.max(1, Math.min(bgShadowLayer.width, bgShadowLayer.height)) - shadowColor: { - const baseColor = Theme.isLightMode ? Qt.rgba(0, 0, 0, 1) : Theme.surfaceContainerHighest; - return Theme.withAlpha(baseColor, menuContainer.effectiveShadowAlpha); - } - } - - Rectangle { - anchors.fill: parent - color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) - radius: Theme.cornerRadius - antialiasing: true - smooth: true - } } Grid { @@ -1412,9 +1402,10 @@ BasePill { } })(), menuWindow.dpr) - property real shadowBlurPx: 10 - property real shadowSpreadPx: 0 - property real shadowBaseAlpha: 0.60 + readonly property var elev: Theme.elevationLevel2 + property real shadowBlurPx: elev && elev.blurPx !== undefined ? elev.blurPx : 8 + property real shadowSpreadPx: elev && elev.spreadPx !== undefined ? elev.spreadPx : 0 + property real shadowBaseAlpha: elev && elev.alpha !== undefined ? elev.alpha : 0.25 readonly property real popupSurfaceAlpha: Theme.popupTransparency readonly property real effectiveShadowAlpha: Math.max(0, Math.min(1, shadowBaseAlpha * popupSurfaceAlpha)) @@ -1435,35 +1426,24 @@ BasePill { } } - Item { + ElevationShadow { id: menuBgShadowLayer anchors.fill: parent - layer.enabled: true + level: menuContainer.elev + fallbackOffset: 4 + shadowBlurPx: menuContainer.shadowBlurPx + shadowSpreadPx: menuContainer.shadowSpreadPx + shadowColor: { + const baseColor = Theme.isLightMode ? Qt.rgba(0, 0, 0, 1) : Theme.surfaceContainerHighest; + return Theme.withAlpha(baseColor, menuContainer.effectiveShadowAlpha); + } + targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) + targetRadius: Theme.cornerRadius + sourceRect.antialiasing: true + shadowEnabled: Theme.elevationEnabled layer.smooth: true layer.textureSize: Qt.size(Math.round(width * menuWindow.dpr), Math.round(height * menuWindow.dpr)) layer.textureMirroring: ShaderEffectSource.MirrorVertically - - readonly property int blurMax: 64 - - layer.effect: MultiEffect { - autoPaddingEnabled: true - shadowEnabled: true - blurEnabled: false - maskEnabled: false - shadowBlur: Math.max(0, Math.min(1, menuContainer.shadowBlurPx / menuBgShadowLayer.blurMax)) - shadowScale: 1 + (2 * menuContainer.shadowSpreadPx) / Math.max(1, Math.min(menuBgShadowLayer.width, menuBgShadowLayer.height)) - shadowColor: { - const baseColor = Theme.isLightMode ? Qt.rgba(0, 0, 0, 1) : Theme.surfaceContainerHighest; - return Theme.withAlpha(baseColor, menuContainer.effectiveShadowAlpha); - } - } - - Rectangle { - anchors.fill: parent - color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) - radius: Theme.cornerRadius - antialiasing: true - } } QsMenuAnchor { diff --git a/quickshell/Modules/DankBar/Widgets/SystemUpdate.qml b/quickshell/Modules/DankBar/Widgets/SystemUpdate.qml index f03d1ecc..696c76c8 100644 --- a/quickshell/Modules/DankBar/Widgets/SystemUpdate.qml +++ b/quickshell/Modules/DankBar/Widgets/SystemUpdate.qml @@ -177,8 +177,9 @@ BasePill { if (popoutTarget && popoutTarget.setTriggerPosition) { const globalPos = root.visualContent.mapToItem(null, 0, 0); const currentScreen = parentScreen || Screen; - const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, root.visualWidth); - popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen); + const barPosition = root.axis?.edge === "left" ? 2 : (root.axis?.edge === "right" ? 3 : (root.axis?.edge === "top" ? 0 : 1)); + const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, root.visualWidth, root.barSpacing, barPosition, root.barConfig); + popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen, barPosition, barThickness, root.barSpacing, root.barConfig); } root.clicked(); } diff --git a/quickshell/Modules/DankDash/MediaDropdownOverlay.qml b/quickshell/Modules/DankDash/MediaDropdownOverlay.qml index 9c540f2d..35dfe974 100644 --- a/quickshell/Modules/DankDash/MediaDropdownOverlay.qml +++ b/quickshell/Modules/DankDash/MediaDropdownOverlay.qml @@ -89,14 +89,18 @@ Item { } } - layer.enabled: true - layer.effect: MultiEffect { - shadowEnabled: true - shadowHorizontalOffset: 0 - shadowVerticalOffset: 8 - shadowBlur: 1.0 - shadowColor: Qt.rgba(0, 0, 0, 0.4) - shadowOpacity: 0.7 + ElevationShadow { + id: volumeShadowLayer + anchors.fill: parent + z: -1 + level: Theme.elevationLevel2 + fallbackOffset: 4 + targetRadius: volumePanel.radius + targetColor: volumePanel.color + borderColor: volumePanel.border.color + borderWidth: volumePanel.border.width + shadowOpacity: Theme.elevationLevel2 && Theme.elevationLevel2.alpha !== undefined ? Theme.elevationLevel2.alpha : 0.25 + shadowEnabled: Theme.elevationEnabled } MouseArea { @@ -223,14 +227,18 @@ Item { } } - layer.enabled: true - layer.effect: MultiEffect { - shadowEnabled: true - shadowHorizontalOffset: 0 - shadowVerticalOffset: 8 - shadowBlur: 1.0 - shadowColor: Qt.rgba(0, 0, 0, 0.4) - shadowOpacity: 0.7 + ElevationShadow { + id: audioDevicesShadowLayer + anchors.fill: parent + z: -1 + level: Theme.elevationLevel2 + fallbackOffset: 4 + targetRadius: audioDevicesPanel.radius + targetColor: audioDevicesPanel.color + borderColor: audioDevicesPanel.border.color + borderWidth: audioDevicesPanel.border.width + shadowOpacity: Theme.elevationLevel2 && Theme.elevationLevel2.alpha !== undefined ? Theme.elevationLevel2.alpha : 0.25 + shadowEnabled: Theme.elevationEnabled } Column { @@ -373,14 +381,18 @@ Item { } } - layer.enabled: true - layer.effect: MultiEffect { - shadowEnabled: true - shadowHorizontalOffset: 0 - shadowVerticalOffset: 8 - shadowBlur: 1.0 - shadowColor: Qt.rgba(0, 0, 0, 0.4) - shadowOpacity: 0.7 + ElevationShadow { + id: playersShadowLayer + anchors.fill: parent + z: -1 + level: Theme.elevationLevel2 + fallbackOffset: 4 + targetRadius: playersPanel.radius + targetColor: playersPanel.color + borderColor: playersPanel.border.color + borderWidth: playersPanel.border.width + shadowOpacity: Theme.elevationLevel2 && Theme.elevationLevel2.alpha !== undefined ? Theme.elevationLevel2.alpha : 0.25 + shadowEnabled: Theme.elevationEnabled } Column { diff --git a/quickshell/Modules/DankDash/MediaPlayerTab.qml b/quickshell/Modules/DankDash/MediaPlayerTab.qml index 6a72bb9f..e65966d8 100644 --- a/quickshell/Modules/DankDash/MediaPlayerTab.qml +++ b/quickshell/Modules/DankDash/MediaPlayerTab.qml @@ -529,14 +529,15 @@ Item { onClicked: activePlayer && activePlayer.togglePlaying() } - layer.enabled: true - layer.effect: MultiEffect { - shadowEnabled: true - shadowHorizontalOffset: 0 - shadowVerticalOffset: 0 - shadowBlur: 1.0 - shadowColor: Qt.rgba(0, 0, 0, 0.3) - shadowOpacity: 0.3 + ElevationShadow { + anchors.fill: parent + z: -1 + level: Theme.elevationLevel1 + fallbackOffset: 1 + targetRadius: parent.radius + targetColor: parent.color + shadowOpacity: Theme.elevationLevel1 && Theme.elevationLevel1.alpha !== undefined ? Theme.elevationLevel1.alpha : 0.2 + shadowEnabled: Theme.elevationEnabled } } } diff --git a/quickshell/Modules/DankDash/WeatherTab.qml b/quickshell/Modules/DankDash/WeatherTab.qml index c2ee59b3..f7be2c6e 100644 --- a/quickshell/Modules/DankDash/WeatherTab.qml +++ b/quickshell/Modules/DankDash/WeatherTab.qml @@ -241,14 +241,15 @@ Item { color: Theme.primary anchors.verticalCenter: parent.verticalCenter - layer.enabled: true + layer.enabled: Theme.elevationEnabled layer.effect: MultiEffect { - shadowEnabled: true - shadowHorizontalOffset: 0 - shadowVerticalOffset: 4 - shadowBlur: 0.8 - shadowColor: Qt.rgba(0, 0, 0, 0.2) - shadowOpacity: 0.2 + shadowEnabled: Theme.elevationEnabled + shadowHorizontalOffset: Theme.elevationOffsetX(Theme.elevationLevel1) + shadowVerticalOffset: Theme.elevationOffsetY(Theme.elevationLevel1, 1) + shadowBlur: Theme.elevationEnabled ? Math.max(0, Math.min(1, (Theme.elevationLevel1 && Theme.elevationLevel1.blurPx !== undefined ? Theme.elevationLevel1.blurPx : 4) / Theme.elevationBlurMax)) : 0 + blurMax: Theme.elevationBlurMax + shadowColor: Theme.elevationShadowColor(Theme.elevationLevel1) + shadowOpacity: Theme.elevationLevel1 && Theme.elevationLevel1.alpha !== undefined ? Theme.elevationLevel1.alpha : 0.2 } } @@ -812,14 +813,14 @@ Item { x: (pos?.h ?? 0) * skyBox.effectiveWidth - (moonPhase.width / 2) + skyBox.hMargin y: (pos?.v ?? 0) * -(skyBox.effectiveHeight / 2) + skyBox.effectiveHeight / 2 - (moonPhase.height / 2) + skyBox.vMargin - layer.enabled: true + layer.enabled: Theme.elevationEnabled layer.effect: MultiEffect { - shadowEnabled: true - shadowHorizontalOffset: 0 - shadowVerticalOffset: 4 - shadowBlur: 0.8 - shadowColor: Qt.rgba(0, 0, 0, 0.2) - shadowOpacity: 0.2 + shadowEnabled: Theme.elevationEnabled + shadowHorizontalOffset: Theme.elevationOffsetX(Theme.elevationLevel2) + shadowVerticalOffset: Theme.elevationOffsetY(Theme.elevationLevel2, 4) + shadowBlur: Theme.elevationEnabled ? Math.max(0, Math.min(1, (Theme.elevationLevel2 && Theme.elevationLevel2.blurPx !== undefined ? Theme.elevationLevel2.blurPx : 8) / Theme.elevationBlurMax)) : 0 + blurMax: Theme.elevationBlurMax + shadowColor: Theme.elevationShadowColor(Theme.elevationLevel2) } } @@ -834,14 +835,14 @@ Item { x: (pos?.h ?? 0) * skyBox.effectiveWidth - (sun.width / 2) + skyBox.hMargin y: (pos?.v ?? 0) * -(skyBox.effectiveHeight / 2) + skyBox.effectiveHeight / 2 - (sun.height / 2) + skyBox.vMargin - layer.enabled: true + layer.enabled: Theme.elevationEnabled layer.effect: MultiEffect { - shadowEnabled: true - shadowHorizontalOffset: 0 - shadowVerticalOffset: 4 - shadowBlur: 0.8 - shadowColor: Qt.rgba(0, 0, 0, 0.2) - shadowOpacity: 0.2 + shadowEnabled: Theme.elevationEnabled + shadowHorizontalOffset: Theme.elevationOffsetX(Theme.elevationLevel2) + shadowVerticalOffset: Theme.elevationOffsetY(Theme.elevationLevel2, 4) + shadowBlur: Theme.elevationEnabled ? Math.max(0, Math.min(1, (Theme.elevationLevel2 && Theme.elevationLevel2.blurPx !== undefined ? Theme.elevationLevel2.blurPx : 8) / Theme.elevationBlurMax)) : 0 + blurMax: Theme.elevationBlurMax + shadowColor: Theme.elevationShadowColor(Theme.elevationLevel2) } } } diff --git a/quickshell/Modules/Notifications/Center/HistoryNotificationCard.qml b/quickshell/Modules/Notifications/Center/HistoryNotificationCard.qml index 7e9e8bcb..f93f35eb 100644 --- a/quickshell/Modules/Notifications/Center/HistoryNotificationCard.qml +++ b/quickshell/Modules/Notifications/Center/HistoryNotificationCard.qml @@ -1,4 +1,5 @@ import QtQuick +import QtQuick.Effects import Quickshell import qs.Common import qs.Services @@ -30,7 +31,21 @@ Rectangle { width: parent ? parent.width : 400 height: baseCardHeight + contentItem.extraHeight radius: Theme.cornerRadius - clip: true + clip: false + readonly property bool shadowsAllowed: Theme.elevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" + + ElevationShadow { + id: shadowLayer + anchors.fill: parent + z: -1 + level: Theme.elevationLevel1 + fallbackOffset: 1 + targetRadius: root.radius + targetColor: root.color + borderColor: root.border.color + borderWidth: root.border.width + shadowEnabled: root.shadowsAllowed + } color: { if (isSelected && keyboardNavigationActive) @@ -49,7 +64,7 @@ Rectangle { return 1.5; if (historyItem.urgency === 2) return 2; - return 1; + return 0; } Behavior on border.color { diff --git a/quickshell/Modules/Notifications/Center/HistoryNotificationList.qml b/quickshell/Modules/Notifications/Center/HistoryNotificationList.qml index 922a4865..69fea418 100644 --- a/quickshell/Modules/Notifications/Center/HistoryNotificationList.qml +++ b/quickshell/Modules/Notifications/Center/HistoryNotificationList.qml @@ -232,6 +232,11 @@ Item { height: parent.height - filterChips.height - Theme.spacingS clip: true spacing: Theme.spacingS + readonly property real horizontalShadowGutter: Theme.snap(Math.max(Theme.spacingXS, 4), 1) + readonly property real verticalShadowGutter: Theme.snap(Math.max(Theme.spacingS, 8), 1) + readonly property real delegateShadowGutter: Theme.snap(Math.max(Theme.spacingXS, 4), 1) + topMargin: verticalShadowGutter + bottomMargin: verticalShadowGutter model: ScriptModel { id: historyModel @@ -263,13 +268,14 @@ Item { } width: ListView.view.width - height: historyCard.height - clip: true + height: historyCard.height + historyListView.delegateShadowGutter + clip: false HistoryNotificationCard { id: historyCard - width: parent.width - x: delegateRoot.swipeOffset + width: Math.max(0, parent.width - (historyListView.horizontalShadowGutter * 2)) + y: historyListView.delegateShadowGutter / 2 + x: historyListView.horizontalShadowGutter + delegateRoot.swipeOffset historyItem: modelData isSelected: root.keyboardActive && root.selectedIndex === index keyboardNavigationActive: root.keyboardActive diff --git a/quickshell/Modules/Notifications/Center/KeyboardNavigatedNotificationList.qml b/quickshell/Modules/Notifications/Center/KeyboardNavigatedNotificationList.qml index db39155d..855163dd 100644 --- a/quickshell/Modules/Notifications/Center/KeyboardNavigatedNotificationList.qml +++ b/quickshell/Modules/Notifications/Center/KeyboardNavigatedNotificationList.qml @@ -18,6 +18,10 @@ DankListView { property real swipingCardOffset: 0 property real __pendingStableHeight: 0 property real __heightUpdateThreshold: 20 + readonly property real shadowBlurPx: Theme.elevationEnabled ? ((Theme.elevationLevel1 && Theme.elevationLevel1.blurPx !== undefined) ? Theme.elevationLevel1.blurPx : 4) : 0 + readonly property real shadowHorizontalGutter: Theme.snap(Math.max(Theme.spacingS, Math.min(32, shadowBlurPx * 1.5 + 6)), 1) + readonly property real shadowVerticalGutter: Theme.snap(Math.max(Theme.spacingXS, 6), 1) + readonly property real delegateShadowGutter: Theme.snap(Math.max(Theme.spacingXS, 4), 1) Component.onCompleted: { Qt.callLater(() => { @@ -56,21 +60,26 @@ DankListView { let delta = 0; for (let i = 0; i < count; i++) { const item = itemAtIndex(i); - if (item && item.children[0] && item.children[0].isAnimating) - delta += item.children[0].targetHeight - item.height; + if (item && item.children[0] && item.children[0].isAnimating) { + const targetDelegateHeight = item.children[0].targetHeight + listView.delegateShadowGutter; + delta += targetDelegateHeight - item.height; + } } const targetHeight = contentHeight + delta; // During expansion, always update immediately without threshold check stableContentHeight = targetHeight; } else { __pendingStableHeight = contentHeight; - heightUpdateDebounce.restart(); + heightUpdateDebounce.stop(); + stableContentHeight = __pendingStableHeight; } } clip: true model: NotificationService.groupedNotifications spacing: Theme.spacingL + topMargin: shadowVerticalGutter + bottomMargin: shadowVerticalGutter onIsUserScrollingChanged: { if (isUserScrolling && keyboardController && keyboardController.keyboardNavigationActive) { @@ -134,8 +143,7 @@ DankListView { readonly property real dismissThreshold: width * 0.35 property bool __delegateInitialized: false - readonly property bool isAdjacentToSwipe: listView.count >= 2 && listView.swipingCardIndex !== -1 && - (index === listView.swipingCardIndex - 1 || index === listView.swipingCardIndex + 1) + readonly property bool isAdjacentToSwipe: listView.count >= 2 && listView.swipingCardIndex !== -1 && (index === listView.swipingCardIndex - 1 || index === listView.swipingCardIndex + 1) readonly property real adjacentSwipeInfluence: isAdjacentToSwipe ? listView.swipingCardOffset * 0.10 : 0 readonly property real adjacentScaleInfluence: isAdjacentToSwipe ? 1.0 - Math.abs(listView.swipingCardOffset) / width * 0.02 : 1.0 readonly property real swipeFadeStartOffset: width * 0.75 @@ -149,13 +157,14 @@ DankListView { } width: ListView.view.width - height: notificationCard.height - clip: notificationCard.isAnimating + height: notificationCard.height + listView.delegateShadowGutter + clip: false NotificationCard { id: notificationCard - width: parent.width - x: delegateRoot.swipeOffset + delegateRoot.adjacentSwipeInfluence + width: Math.max(0, parent.width - (listView.shadowHorizontalGutter * 2)) + y: listView.delegateShadowGutter / 2 + x: listView.shadowHorizontalGutter + delegateRoot.swipeOffset + delegateRoot.adjacentSwipeInfluence listLevelAdjacentScaleInfluence: delegateRoot.adjacentScaleInfluence listLevelScaleAnimationsEnabled: listView.swipingCardIndex === -1 || !delegateRoot.isAdjacentToSwipe notificationGroup: modelData diff --git a/quickshell/Modules/Notifications/Center/NotificationCard.qml b/quickshell/Modules/Notifications/Center/NotificationCard.qml index af16d986..0875abed 100644 --- a/quickshell/Modules/Notifications/Center/NotificationCard.qml +++ b/quickshell/Modules/Notifications/Center/NotificationCard.qml @@ -1,5 +1,6 @@ import QtQuick import QtQuick.Controls +import QtQuick.Effects import Quickshell import Quickshell.Services.Notifications import qs.Common @@ -38,7 +39,14 @@ Rectangle { height: expanded ? (expandedContent.height + cardPadding * 2) : (baseCardHeight + collapsedContent.extraHeight) readonly property real targetHeight: expanded ? (expandedContent.height + cardPadding * 2) : (baseCardHeight + collapsedContent.extraHeight) radius: Theme.cornerRadius - scale: (cardHoverHandler.hovered ? 1.01 : 1.0) * listLevelAdjacentScaleInfluence + scale: (cardHoverHandler.hovered ? 1.004 : 1.0) * listLevelAdjacentScaleInfluence + readonly property bool shadowsAllowed: Theme.elevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" + readonly property var shadowElevation: Theme.elevationLevel1 + readonly property real baseShadowBlurPx: (shadowElevation && shadowElevation.blurPx !== undefined) ? shadowElevation.blurPx : 4 + readonly property real hoverShadowBlurBoost: cardHoverHandler.hovered ? Math.min(2, baseShadowBlurPx * 0.25) : 0 + property real shadowBlurPx: shadowsAllowed ? (baseShadowBlurPx + hoverShadowBlurBoost) : 0 + property real shadowOffsetXPx: shadowsAllowed ? Theme.elevationOffsetX(shadowElevation) : 0 + property real shadowOffsetYPx: shadowsAllowed ? (Theme.elevationOffsetY(shadowElevation, 1) + (cardHoverHandler.hovered ? 0.35 : 0)) : 0 property bool __initialized: false Component.onCompleted: { @@ -56,6 +64,27 @@ Rectangle { } } + Behavior on shadowBlurPx { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Behavior on shadowOffsetXPx { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + Behavior on shadowOffsetYPx { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + Behavior on border.color { enabled: root.__initialized ColorAnimation { @@ -95,14 +124,31 @@ Rectangle { if (notificationGroup?.latestNotification?.urgency === NotificationUrgency.Critical) { return 2; } - return 1; + return 0; } - clip: true + clip: false HoverHandler { id: cardHoverHandler } + ElevationShadow { + id: shadowLayer + anchors.fill: parent + z: -1 + level: root.shadowElevation + targetRadius: root.radius + targetColor: root.color + borderColor: root.border.color + borderWidth: root.border.width + shadowBlurPx: root.shadowBlurPx + shadowSpreadPx: 0 + shadowOffsetX: root.shadowOffsetXPx + shadowOffsetY: root.shadowOffsetYPx + shadowColor: root.shadowElevation ? Theme.elevationShadowColor(root.shadowElevation) : "transparent" + shadowEnabled: root.shadowsAllowed + } + Rectangle { anchors.fill: parent radius: parent.radius @@ -304,8 +350,13 @@ Rectangle { onClicked: mouse => { if (!parent.hoveredLink && (parent.hasMoreText || descriptionExpanded)) { + root.userInitiatedExpansion = true; const messageId = (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.notification && notificationGroup.latestNotification.notification.id) ? (notificationGroup.latestNotification.notification.id + "_desc") : ""; NotificationService.toggleMessageExpansion(messageId); + Qt.callLater(() => { + if (root && !root.isAnimating) + root.userInitiatedExpansion = false; + }); } } @@ -419,9 +470,7 @@ Rectangle { id: delegateRect width: parent.width - readonly property bool isAdjacentToSwipe: root.swipingNotificationIndex !== -1 && - (expandedDelegateWrapper.index === root.swipingNotificationIndex - 1 || - expandedDelegateWrapper.index === root.swipingNotificationIndex + 1) + readonly property bool isAdjacentToSwipe: root.swipingNotificationIndex !== -1 && (expandedDelegateWrapper.index === root.swipingNotificationIndex - 1 || expandedDelegateWrapper.index === root.swipingNotificationIndex + 1) readonly property real adjacentSwipeInfluence: isAdjacentToSwipe ? root.swipingNotificationOffset * 0.10 : 0 readonly property real adjacentScaleInfluence: isAdjacentToSwipe ? 1.0 - Math.abs(root.swipingNotificationOffset) / width * 0.02 : 1.0 @@ -605,7 +654,12 @@ Rectangle { onClicked: mouse => { if (!parent.hoveredLink && (bodyText.hasMoreText || messageExpanded)) { + root.userInitiatedExpansion = true; NotificationService.toggleMessageExpansion(modelData?.notification?.id || ""); + Qt.callLater(() => { + if (root && !root.isAnimating) + root.userInitiatedExpansion = false; + }); } } diff --git a/quickshell/Modules/Notifications/Center/NotificationCenterPopout.qml b/quickshell/Modules/Notifications/Center/NotificationCenterPopout.qml index e7b34c07..3f9c2f80 100644 --- a/quickshell/Modules/Notifications/Center/NotificationCenterPopout.qml +++ b/quickshell/Modules/Notifications/Center/NotificationCenterPopout.qml @@ -7,15 +7,22 @@ DankPopout { id: root layerNamespace: "dms:notification-center-popout" - fullHeightSurface: true + fullHeightSurface: false property bool notificationHistoryVisible: false property var triggerScreen: null property real stablePopupHeight: 400 property real _lastAlignedContentHeight: -1 + property bool _pendingSizedOpen: false function updateStablePopupHeight() { const item = contentLoader.item; + if (item && !root.shouldBeVisible) { + const notificationList = findChild(item, "notificationList"); + if (notificationList && typeof notificationList.forceLayout === "function") { + notificationList.forceLayout(); + } + } const target = item ? Theme.px(item.implicitHeight, dpr) : 400; if (Math.abs(target - _lastAlignedContentHeight) < 0.5) return; @@ -26,7 +33,7 @@ DankPopout { NotificationKeyboardController { id: keyboardController listView: null - isOpen: notificationHistoryVisible + isOpen: root.shouldBeVisible onClose: () => { notificationHistoryVisible = false; } @@ -40,20 +47,42 @@ DankPopout { suspendShadowWhileResizing: false screen: triggerScreen - shouldBeVisible: notificationHistoryVisible function toggle() { notificationHistoryVisible = !notificationHistoryVisible; } + function openSized() { + if (!notificationHistoryVisible) + return; + + primeContent(); + if (contentLoader.item) { + updateStablePopupHeight(); + _pendingSizedOpen = false; + Qt.callLater(() => { + if (!notificationHistoryVisible) + return; + updateStablePopupHeight(); + open(); + clearPrimedContent(); + }); + return; + } + + _pendingSizedOpen = true; + } + onBackgroundClicked: { notificationHistoryVisible = false; } onNotificationHistoryVisibleChanged: { if (notificationHistoryVisible) { - open(); + openSized(); } else { + _pendingSizedOpen = false; + clearPrimedContent(); close(); } } @@ -82,6 +111,17 @@ DankPopout { target: contentLoader function onLoaded() { root.updateStablePopupHeight(); + if (root._pendingSizedOpen && root.notificationHistoryVisible) { + Qt.callLater(() => { + if (!root._pendingSizedOpen || !root.notificationHistoryVisible) + return; + root.updateStablePopupHeight(); + root._pendingSizedOpen = false; + root.open(); + root.clearPrimedContent(); + }); + return; + } if (root.shouldBeVisible) Qt.callLater(root.setupKeyboardNavigation); } @@ -139,7 +179,8 @@ DankPopout { baseHeight += Theme.spacingM * 2; const settingsHeight = notificationSettings.expanded ? notificationSettings.contentHeight : 0; - let listHeight = notificationHeader.currentTab === 0 ? notificationList.stableContentHeight : Math.max(200, NotificationService.historyList.length * 80); + const currentListHeight = root.shouldBeVisible ? notificationList.stableContentHeight : notificationList.listContentHeight; + let listHeight = notificationHeader.currentTab === 0 ? currentListHeight : Math.max(200, NotificationService.historyList.length * 80); if (notificationHeader.currentTab === 0 && NotificationService.groupedNotifications.length === 0) { listHeight = 200; } @@ -233,13 +274,21 @@ DankPopout { expanded: notificationHeader.showSettings } - KeyboardNavigatedNotificationList { - id: notificationList - objectName: "notificationList" + Item { visible: notificationHeader.currentTab === 0 width: parent.width height: parent.height - notificationContent.cachedHeaderHeight - notificationSettings.height - contentColumnInner.spacing * 2 - cardAnimateExpansion: true + + KeyboardNavigatedNotificationList { + id: notificationList + objectName: "notificationList" + anchors.fill: parent + anchors.leftMargin: -shadowHorizontalGutter + anchors.rightMargin: -shadowHorizontalGutter + anchors.topMargin: -(shadowVerticalGutter + delegateShadowGutter / 2) + anchors.bottomMargin: -(shadowVerticalGutter + delegateShadowGutter / 2) + cardAnimateExpansion: true + } } HistoryNotificationList { diff --git a/quickshell/Modules/Notifications/Popup/NotificationPopup.qml b/quickshell/Modules/Notifications/Popup/NotificationPopup.qml index cf4f4e75..f1ee5c4d 100644 --- a/quickshell/Modules/Notifications/Popup/NotificationPopup.qml +++ b/quickshell/Modules/Notifications/Popup/NotificationPopup.qml @@ -118,8 +118,8 @@ PanelWindow { WlrLayershell.exclusiveZone: -1 WlrLayershell.keyboardFocus: WlrKeyboardFocus.None color: "transparent" - implicitWidth: screen ? Math.min(400, Math.max(320, screen.width * 0.23)) : 380 - implicitHeight: { + readonly property real contentImplicitWidth: screen ? Math.min(400, Math.max(320, screen.width * 0.23)) : 380 + readonly property real contentImplicitHeight: { if (SettingsData.notificationPopupPrivacyMode && !descriptionExpanded) return basePopupHeightPrivacy; if (!descriptionExpanded) @@ -130,6 +130,8 @@ PanelWindow { return basePopupHeight + bodyTextHeight - collapsedBodyHeight; return basePopupHeight; } + implicitWidth: contentImplicitWidth + (windowShadowPad * 2) + implicitHeight: contentImplicitHeight + (windowShadowPad * 2) Behavior on implicitHeight { enabled: !exiting && !_isDestroying @@ -182,11 +184,15 @@ PanelWindow { property bool isTopCenter: SettingsData.notificationPopupPosition === -1 property bool isBottomCenter: SettingsData.notificationPopupPosition === SettingsData.Position.BottomCenter property bool isCenterPosition: isTopCenter || isBottomCenter + readonly property real maxPopupShadowBlurPx: Math.max((Theme.elevationLevel3 && Theme.elevationLevel3.blurPx !== undefined) ? Theme.elevationLevel3.blurPx : 12, (Theme.elevationLevel4 && Theme.elevationLevel4.blurPx !== undefined) ? Theme.elevationLevel4.blurPx : 16) + readonly property real maxPopupShadowOffsetXPx: Math.max(Math.abs(Theme.elevationOffsetX(Theme.elevationLevel3)), Math.abs(Theme.elevationOffsetX(Theme.elevationLevel4))) + readonly property real maxPopupShadowOffsetYPx: Math.max(Math.abs(Theme.elevationOffsetY(Theme.elevationLevel3, 6)), Math.abs(Theme.elevationOffsetY(Theme.elevationLevel4, 8))) + readonly property real windowShadowPad: Theme.elevationEnabled && SettingsData.notificationPopupShadowEnabled ? Theme.snap(Math.max(16, maxPopupShadowBlurPx + Math.max(maxPopupShadowOffsetXPx, maxPopupShadowOffsetYPx) + 8), dpr) : 0 anchors.top: true - anchors.bottom: true - anchors.left: SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom - anchors.right: SettingsData.notificationPopupPosition === SettingsData.Position.Top || SettingsData.notificationPopupPosition === SettingsData.Position.Right + anchors.left: true + anchors.bottom: false + anchors.right: false mask: contentInputMask @@ -205,10 +211,10 @@ PanelWindow { } margins { - top: _storedTopMargin - bottom: _storedBottomMargin - left: getLeftMargin() - right: getRightMargin() + top: getWindowTopMargin() + bottom: 0 + left: getWindowLeftMargin() + right: 0 } function getBarInfo() { @@ -250,7 +256,7 @@ PanelWindow { function getLeftMargin() { if (isCenterPosition) - return screen ? (screen.width - implicitWidth) / 2 : 0; + return screen ? (screen.width - alignedWidth) / 2 : 0; const popupPos = SettingsData.notificationPopupPosition; const isLeft = popupPos === SettingsData.Position.Left || popupPos === SettingsData.Position.Bottom; @@ -274,23 +280,56 @@ PanelWindow { return barInfo.rightBar > 0 ? barInfo.rightBar : Theme.popupDistance; } + function getContentX() { + if (!screen) + return 0; + + const popupPos = SettingsData.notificationPopupPosition; + const barLeft = getLeftMargin(); + const barRight = getRightMargin(); + + if (isCenterPosition) + return Theme.snap((screen.width - alignedWidth) / 2, dpr); + if (popupPos === SettingsData.Position.Left || popupPos === SettingsData.Position.Bottom) + return Theme.snap(barLeft, dpr); + return Theme.snap(screen.width - alignedWidth - barRight, dpr); + } + + function getContentY() { + if (!screen) + return 0; + + const popupPos = SettingsData.notificationPopupPosition; + const barTop = getTopMargin(); + const barBottom = getBottomMargin(); + const isTop = isTopCenter || popupPos === SettingsData.Position.Top || popupPos === SettingsData.Position.Left; + if (isTop) + return Theme.snap(barTop, dpr); + return Theme.snap(screen.height - alignedHeight - barBottom, dpr); + } + + function getWindowLeftMargin() { + if (!screen) + return 0; + return Theme.snap(getContentX() - windowShadowPad, dpr); + } + + function getWindowTopMargin() { + if (!screen) + return 0; + return Theme.snap(getContentY() - windowShadowPad, dpr); + } + readonly property bool screenValid: win.screen && !_isDestroying readonly property real dpr: screenValid ? CompositorService.getScreenScale(win.screen) : 1 - readonly property real alignedWidth: Theme.px(implicitWidth, dpr) - readonly property real alignedHeight: Theme.px(implicitHeight, dpr) + readonly property real alignedWidth: Theme.px(Math.max(0, implicitWidth - (windowShadowPad * 2)), dpr) + readonly property real alignedHeight: Theme.px(Math.max(0, implicitHeight - (windowShadowPad * 2)), dpr) Item { id: content - x: Theme.snap((win.width - alignedWidth) / 2, dpr) - y: { - const isTop = isTopCenter || SettingsData.notificationPopupPosition === SettingsData.Position.Top || SettingsData.notificationPopupPosition === SettingsData.Position.Left; - if (isTop) { - return Theme.snap(screenY, dpr); - } else { - return Theme.snap(win.height - alignedHeight - screenY, dpr); - } - } + x: Theme.snap(windowShadowPad, dpr) + y: Theme.snap(windowShadowPad, dpr) width: alignedWidth height: alignedHeight visible: !win._finalized @@ -313,12 +352,13 @@ PanelWindow { readonly property bool swipeActive: swipeDragHandler.active property bool swipeDismissing: false - readonly property real radiusForShadow: Theme.cornerRadius - property real shadowBlurPx: SettingsData.notificationPopupShadowEnabled ? ((2 + radiusForShadow * 0.2) * (cardHoverHandler.hovered ? 1.2 : 1)) : 0 - property real shadowSpreadPx: SettingsData.notificationPopupShadowEnabled ? (radiusForShadow * (cardHoverHandler.hovered ? 0.06 : 0)) : 0 - property real shadowBaseAlpha: 0.35 - readonly property real popupSurfaceAlpha: SettingsData.popupTransparency - readonly property real effectiveShadowAlpha: Math.max(0, Math.min(1, shadowBaseAlpha * popupSurfaceAlpha)) + readonly property bool shadowsAllowed: Theme.elevationEnabled && SettingsData.notificationPopupShadowEnabled + readonly property var elevLevel: cardHoverHandler.hovered ? Theme.elevationLevel4 : Theme.elevationLevel3 + readonly property real cardInset: Theme.snap(4, win.dpr) + readonly property real shadowRenderPadding: shadowsAllowed ? Theme.snap(Math.max(16, shadowBlurPx + Math.max(Math.abs(shadowOffsetX), Math.abs(shadowOffsetY)) + 8), win.dpr) : 0 + property real shadowBlurPx: shadowsAllowed ? (elevLevel && elevLevel.blurPx !== undefined ? elevLevel.blurPx : 12) : 0 + property real shadowOffsetX: shadowsAllowed ? Theme.elevationOffsetX(elevLevel) : 0 + property real shadowOffsetY: shadowsAllowed ? Theme.elevationOffsetY(elevLevel, 6) : 0 Behavior on shadowBlurPx { NumberAnimation { @@ -327,50 +367,50 @@ PanelWindow { } } - Behavior on shadowSpreadPx { + Behavior on shadowOffsetX { NumberAnimation { duration: Theme.shortDuration easing.type: Theme.standardEasing } } - Item { + Behavior on shadowOffsetY { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + + ElevationShadow { id: bgShadowLayer anchors.fill: parent - anchors.margins: Theme.snap(4, win.dpr) - layer.enabled: !win._isDestroying && win.screenValid - layer.smooth: false + anchors.margins: -content.shadowRenderPadding + level: content.elevLevel + fallbackOffset: 6 + shadowBlurPx: content.shadowBlurPx + shadowOffsetX: content.shadowOffsetX + shadowOffsetY: content.shadowOffsetY + shadowColor: content.shadowsAllowed && content.elevLevel ? Theme.elevationShadowColor(content.elevLevel) : "transparent" + shadowEnabled: !win._isDestroying && win.screenValid && content.shadowsAllowed layer.textureSize: Qt.size(Math.round(width * win.dpr), Math.round(height * win.dpr)) layer.textureMirroring: ShaderEffectSource.MirrorVertically - readonly property int blurMax: 64 - - layer.effect: MultiEffect { - id: shadowFx - autoPaddingEnabled: true - shadowEnabled: SettingsData.notificationPopupShadowEnabled - blurEnabled: false - maskEnabled: false - shadowBlur: Math.max(0, Math.min(1, content.shadowBlurPx / bgShadowLayer.blurMax)) - shadowScale: 1 + (2 * content.shadowSpreadPx) / Math.max(1, Math.min(bgShadowLayer.width, bgShadowLayer.height)) - shadowColor: { - const baseColor = Theme.isLightMode ? Qt.rgba(0, 0, 0, 1) : Theme.surfaceContainerHighest; - return Theme.withAlpha(baseColor, content.effectiveShadowAlpha); - } - } + sourceRect.anchors.fill: undefined + sourceRect.x: content.shadowRenderPadding + content.cardInset + sourceRect.y: content.shadowRenderPadding + content.cardInset + sourceRect.width: Math.max(0, content.width - (content.cardInset * 2)) + sourceRect.height: Math.max(0, content.height - (content.cardInset * 2)) + sourceRect.radius: Theme.cornerRadius + sourceRect.color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) + sourceRect.border.color: notificationData && notificationData.urgency === NotificationUrgency.Critical ? Theme.withAlpha(Theme.primary, 0.3) : Theme.withAlpha(Theme.outline, 0.08) + sourceRect.border.width: notificationData && notificationData.urgency === NotificationUrgency.Critical ? 2 : 0 Rectangle { - id: shadowShapeSource - anchors.fill: parent - radius: Theme.cornerRadius - color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) - border.color: notificationData && notificationData.urgency === NotificationUrgency.Critical ? Theme.withAlpha(Theme.primary, 0.3) : Theme.withAlpha(Theme.outline, 0.08) - border.width: notificationData && notificationData.urgency === NotificationUrgency.Critical ? 2 : 0 - } - - Rectangle { - anchors.fill: parent - radius: shadowShapeSource.radius + x: bgShadowLayer.sourceRect.x + y: bgShadowLayer.sourceRect.y + width: bgShadowLayer.sourceRect.width + height: bgShadowLayer.sourceRect.height + radius: bgShadowLayer.sourceRect.radius visible: notificationData && notificationData.urgency === NotificationUrgency.Critical opacity: 1 clip: true @@ -399,7 +439,7 @@ PanelWindow { Item { id: backgroundContainer anchors.fill: parent - anchors.margins: Theme.snap(4, win.dpr) + anchors.margins: content.cardInset clip: true HoverHandler { diff --git a/quickshell/Modules/Settings/AboutTab.qml b/quickshell/Modules/Settings/AboutTab.qml index d2d9b14a..ce188eda 100644 --- a/quickshell/Modules/Settings/AboutTab.qml +++ b/quickshell/Modules/Settings/AboutTab.qml @@ -878,12 +878,17 @@ Item { x: hoveredButton ? hoveredButton.mapToItem(aboutTab, hoveredButton.width / 2, 0).x - width / 2 : 0 y: hoveredButton ? communityIcons.mapToItem(aboutTab, 0, 0).y - height - 8 : 0 - layer.enabled: true - layer.effect: MultiEffect { - shadowEnabled: true - shadowOpacity: 0.15 - shadowVerticalOffset: 2 - shadowBlur: 0.5 + ElevationShadow { + anchors.fill: parent + z: -1 + level: Theme.elevationLevel1 + fallbackOffset: 1 + targetRadius: communityTooltip.radius + targetColor: communityTooltip.color + borderColor: communityTooltip.border.color + borderWidth: communityTooltip.border.width + shadowOpacity: Theme.elevationLevel1 && Theme.elevationLevel1.alpha !== undefined ? Theme.elevationLevel1.alpha : 0.2 + shadowEnabled: Theme.elevationEnabled } StyledText { diff --git a/quickshell/Modules/Settings/DankBarTab.qml b/quickshell/Modules/Settings/DankBarTab.qml index dffe94a4..a4bd87d1 100644 --- a/quickshell/Modules/Settings/DankBarTab.qml +++ b/quickshell/Modules/Settings/DankBarTab.qml @@ -52,9 +52,11 @@ Item { } function _isBarActive(c) { - if (!c.enabled) return false; + if (!c.enabled) + return false; const prefs = c.screenPreferences || ["all"]; - if (prefs.length > 0) return true; + if (prefs.length > 0) + return true; return (c.showOnLastDisplay ?? true) && Quickshell.screens.length === 1; } @@ -64,7 +66,8 @@ Item { return; const hasHorizontal = configs.some(c => { - if (!_isBarActive(c)) return false; + if (!_isBarActive(c)) + return false; const p = c.position ?? SettingsData.Position.Top; return p === SettingsData.Position.Top || p === SettingsData.Position.Bottom; }); @@ -72,7 +75,8 @@ Item { return; const hasVertical = configs.some(c => { - if (!_isBarActive(c)) return false; + if (!_isBarActive(c)) + return false; const p = c.position ?? SettingsData.Position.Top; return p === SettingsData.Position.Left || p === SettingsData.Position.Right; }); @@ -136,7 +140,9 @@ Item { scrollYBehavior: defaultBar.scrollYBehavior ?? "workspace", shadowIntensity: defaultBar.shadowIntensity ?? 0, shadowOpacity: defaultBar.shadowOpacity ?? 60, - shadowColorMode: defaultBar.shadowColorMode ?? "text", + shadowDirectionMode: defaultBar.shadowDirectionMode ?? "inherit", + shadowDirection: defaultBar.shadowDirection ?? "top", + shadowColorMode: defaultBar.shadowColorMode ?? "default", shadowCustomColor: defaultBar.shadowCustomColor ?? "#000000" }; SettingsData.addBarConfig(newBar); @@ -1040,6 +1046,237 @@ Item { } } + SettingsCard { + id: shadowCard + iconName: "layers" + title: I18n.tr("Shadow Override", "bar shadow settings card") + settingKey: "barShadow" + collapsible: true + expanded: true + visible: selectedBarConfig?.enabled + + readonly property bool shadowActive: (selectedBarConfig?.shadowIntensity ?? 0) > 0 + readonly property bool isCustomColor: (selectedBarConfig?.shadowColorMode ?? "default") === "custom" + readonly property string directionSource: selectedBarConfig?.shadowDirectionMode ?? "inherit" + + StyledText { + width: parent.width + text: I18n.tr("Enable a custom override below to set per-bar shadow intensity, opacity, and color.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignLeft + } + + SettingsToggleRow { + text: I18n.tr("Custom Shadow Override") + description: I18n.tr("Override the global shadow with per-bar settings") + checked: shadowCard.shadowActive + onToggled: checked => { + if (checked) { + SettingsData.updateBarConfig(selectedBarId, { + shadowIntensity: 12, + shadowOpacity: 60 + }); + } else { + SettingsData.updateBarConfig(selectedBarId, { + shadowIntensity: 0 + }); + } + } + } + + SettingsSliderRow { + visible: shadowCard.shadowActive + text: I18n.tr("Intensity", "shadow intensity slider") + minimum: 0 + maximum: 100 + unit: "px" + defaultValue: 12 + value: selectedBarConfig?.shadowIntensity ?? 0 + onSliderValueChanged: newValue => SettingsData.updateBarConfig(selectedBarId, { + shadowIntensity: newValue + }) + } + + SettingsSliderRow { + visible: shadowCard.shadowActive + text: I18n.tr("Opacity") + minimum: 10 + maximum: 100 + unit: "%" + defaultValue: 60 + value: selectedBarConfig?.shadowOpacity ?? 60 + onSliderValueChanged: newValue => SettingsData.updateBarConfig(selectedBarId, { + shadowOpacity: newValue + }) + } + + SettingsDropdownRow { + visible: shadowCard.shadowActive + text: I18n.tr("Direction Source", "bar shadow direction source") + description: I18n.tr("Choose how this bar resolves shadow direction") + settingKey: "barShadowDirectionSource" + options: [I18n.tr("Inherit Global (Default)", "bar shadow direction source option"), I18n.tr("Auto (Bar-aware)", "bar shadow direction source option"), I18n.tr("Manual", "bar shadow direction source option")] + currentValue: { + switch (shadowCard.directionSource) { + case "autoBar": + return I18n.tr("Auto (Bar-aware)", "bar shadow direction source option"); + case "manual": + return I18n.tr("Manual", "bar shadow direction source option"); + default: + return I18n.tr("Inherit Global (Default)", "bar shadow direction source option"); + } + } + onValueChanged: value => { + if (value === I18n.tr("Auto (Bar-aware)", "bar shadow direction source option")) { + SettingsData.updateBarConfig(selectedBarId, { + shadowDirectionMode: "autoBar" + }); + } else if (value === I18n.tr("Manual", "bar shadow direction source option")) { + SettingsData.updateBarConfig(selectedBarId, { + shadowDirectionMode: "manual" + }); + } else { + SettingsData.updateBarConfig(selectedBarId, { + shadowDirectionMode: "inherit" + }); + } + } + } + + SettingsDropdownRow { + visible: shadowCard.shadowActive && shadowCard.directionSource === "manual" + text: I18n.tr("Manual Direction", "bar manual shadow direction") + description: I18n.tr("Use a fixed shadow direction for this bar") + settingKey: "barShadowDirectionManual" + options: [I18n.tr("Top", "shadow direction option"), I18n.tr("Top Left", "shadow direction option"), I18n.tr("Top Right", "shadow direction option"), I18n.tr("Bottom", "shadow direction option")] + currentValue: { + switch (selectedBarConfig?.shadowDirection) { + case "topLeft": + return I18n.tr("Top Left", "shadow direction option"); + case "topRight": + return I18n.tr("Top Right", "shadow direction option"); + case "bottom": + return I18n.tr("Bottom", "shadow direction option"); + default: + return I18n.tr("Top", "shadow direction option"); + } + } + onValueChanged: value => { + if (value === I18n.tr("Top Left", "shadow direction option")) { + SettingsData.updateBarConfig(selectedBarId, { + shadowDirection: "topLeft" + }); + } else if (value === I18n.tr("Top Right", "shadow direction option")) { + SettingsData.updateBarConfig(selectedBarId, { + shadowDirection: "topRight" + }); + } else if (value === I18n.tr("Bottom", "shadow direction option")) { + SettingsData.updateBarConfig(selectedBarId, { + shadowDirection: "bottom" + }); + } else { + SettingsData.updateBarConfig(selectedBarId, { + shadowDirection: "top" + }); + } + } + } + + Column { + visible: shadowCard.shadowActive + width: parent.width + spacing: Theme.spacingS + + StyledText { + text: I18n.tr("Color") + font.pixelSize: Theme.fontSizeMedium + color: Theme.surfaceText + horizontalAlignment: Text.AlignLeft + anchors.left: parent.left + anchors.leftMargin: Theme.spacingM + } + + Item { + width: parent.width + height: shadowColorGroup.implicitHeight + + DankButtonGroup { + id: shadowColorGroup + anchors.horizontalCenter: parent.horizontalCenter + buttonPadding: parent.width < 420 ? Theme.spacingXS : Theme.spacingS + minButtonWidth: parent.width < 420 ? 36 : 56 + textSize: parent.width < 420 ? Theme.fontSizeSmall : Theme.fontSizeMedium + model: [I18n.tr("Default (Black)"), I18n.tr("Surface", "shadow color option"), I18n.tr("Primary"), I18n.tr("Secondary"), I18n.tr("Custom")] + selectionMode: "single" + currentIndex: { + switch (selectedBarConfig?.shadowColorMode || "default") { + case "surface": + return 1; + case "primary": + return 2; + case "secondary": + return 3; + case "custom": + return 4; + default: + return 0; + } + } + onSelectionChanged: (index, selected) => { + if (!selected) + return; + let mode = "default"; + switch (index) { + case 1: + mode = "surface"; + break; + case 2: + mode = "primary"; + break; + case 3: + mode = "secondary"; + break; + case 4: + mode = "custom"; + break; + } + SettingsData.updateBarConfig(selectedBarId, { + shadowColorMode: mode + }); + } + } + } + + Rectangle { + visible: selectedBarConfig?.shadowColorMode === "custom" + width: 32 + height: 32 + radius: 16 + color: selectedBarConfig?.shadowCustomColor ?? "#000000" + border.color: Theme.outline + border.width: 1 + anchors.horizontalCenter: parent.horizontalCenter + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + PopoutService.colorPickerModal.selectedColor = selectedBarConfig?.shadowCustomColor ?? "#000000"; + PopoutService.colorPickerModal.pickerTitle = I18n.tr("Color"); + PopoutService.colorPickerModal.onColorSelectedCallback = function (color) { + SettingsData.updateBarConfig(selectedBarId, { + shadowCustomColor: color.toString() + }); + }; + PopoutService.colorPickerModal.show(); + } + } + } + } + } + SettingsCard { iconName: "rounded_corner" title: I18n.tr("Corners & Background") @@ -1142,134 +1379,6 @@ Item { } } - SettingsCard { - id: shadowCard - iconName: "layers" - title: I18n.tr("Shadow", "bar shadow settings card") - settingKey: "barShadow" - collapsible: true - expanded: false - visible: selectedBarConfig?.enabled - - readonly property bool shadowActive: (selectedBarConfig?.shadowIntensity ?? 0) > 0 - readonly property bool isCustomColor: (selectedBarConfig?.shadowColorMode ?? "text") === "custom" - - SettingsSliderRow { - text: I18n.tr("Intensity", "shadow intensity slider") - minimum: 0 - maximum: 100 - unit: "%" - value: selectedBarConfig?.shadowIntensity ?? 0 - onSliderValueChanged: newValue => SettingsData.updateBarConfig(selectedBarId, { - shadowIntensity: newValue - }) - } - - SettingsSliderRow { - visible: shadowCard.shadowActive - text: I18n.tr("Opacity") - minimum: 10 - maximum: 100 - unit: "%" - value: selectedBarConfig?.shadowOpacity ?? 60 - onSliderValueChanged: newValue => SettingsData.updateBarConfig(selectedBarId, { - shadowOpacity: newValue - }) - } - - Column { - visible: shadowCard.shadowActive - width: parent.width - spacing: Theme.spacingS - - StyledText { - text: I18n.tr("Color") - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText - horizontalAlignment: Text.AlignLeft - anchors.left: parent.left - anchors.leftMargin: Theme.spacingM - } - - Item { - width: parent.width - height: shadowColorGroup.implicitHeight - - DankButtonGroup { - id: shadowColorGroup - anchors.horizontalCenter: parent.horizontalCenter - buttonPadding: parent.width < 420 ? Theme.spacingXS : Theme.spacingS - minButtonWidth: parent.width < 420 ? 36 : 56 - textSize: parent.width < 420 ? Theme.fontSizeSmall : Theme.fontSizeMedium - model: [I18n.tr("Text", "shadow color option"), I18n.tr("Surface", "shadow color option"), I18n.tr("Primary"), I18n.tr("Secondary"), I18n.tr("Custom")] - selectionMode: "single" - currentIndex: { - switch (selectedBarConfig?.shadowColorMode || "text") { - case "surface": - return 1; - case "primary": - return 2; - case "secondary": - return 3; - case "custom": - return 4; - default: - return 0; - } - } - onSelectionChanged: (index, selected) => { - if (!selected) - return; - let mode = "text"; - switch (index) { - case 1: - mode = "surface"; - break; - case 2: - mode = "primary"; - break; - case 3: - mode = "secondary"; - break; - case 4: - mode = "custom"; - break; - } - SettingsData.updateBarConfig(selectedBarId, { - shadowColorMode: mode - }); - } - } - } - - Rectangle { - visible: selectedBarConfig?.shadowColorMode === "custom" - width: 32 - height: 32 - radius: 16 - color: selectedBarConfig?.shadowCustomColor ?? "#000000" - border.color: Theme.outline - border.width: 1 - anchors.horizontalCenter: parent.horizontalCenter - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: { - PopoutService.colorPickerModal.selectedColor = selectedBarConfig?.shadowCustomColor ?? "#000000"; - PopoutService.colorPickerModal.pickerTitle = I18n.tr("Color"); - PopoutService.colorPickerModal.onColorSelectedCallback = function (color) { - SettingsData.updateBarConfig(selectedBarId, { - shadowCustomColor: color.toString() - }); - }; - PopoutService.colorPickerModal.show(); - } - } - } - } - } - SettingsToggleCard { iconName: "border_style" title: I18n.tr("Border") diff --git a/quickshell/Modules/Settings/NotificationsTab.qml b/quickshell/Modules/Settings/NotificationsTab.qml index 2c3f9e75..7294e75d 100644 --- a/quickshell/Modules/Settings/NotificationsTab.qml +++ b/quickshell/Modules/Settings/NotificationsTab.qml @@ -274,7 +274,7 @@ Item { settingKey: "notificationPopupShadowEnabled" tags: ["notification", "popup", "shadow", "radius", "rounded"] text: I18n.tr("Popup Shadow") - description: I18n.tr("Show drop shadow on notification popups") + description: I18n.tr("Show drop shadow on notification popups. Requires M3 Elevation to be enabled in Theme & Colors.") checked: SettingsData.notificationPopupShadowEnabled onToggled: checked => SettingsData.set("notificationPopupShadowEnabled", checked) } diff --git a/quickshell/Modules/Settings/ThemeColorsTab.qml b/quickshell/Modules/Settings/ThemeColorsTab.qml index 45a7e641..22b060bc 100644 --- a/quickshell/Modules/Settings/ThemeColorsTab.qml +++ b/quickshell/Modules/Settings/ThemeColorsTab.qml @@ -126,6 +126,15 @@ Item { return Theme.warning; } + function openM3ShadowColorPicker() { + PopoutService.colorPickerModal.selectedColor = SettingsData.m3ElevationCustomColor ?? "#000000"; + PopoutService.colorPickerModal.pickerTitle = I18n.tr("Shadow Color"); + PopoutService.colorPickerModal.onColorSelectedCallback = function (color) { + SettingsData.set("m3ElevationCustomColor", color.toString()); + }; + PopoutService.colorPickerModal.show(); + } + function formatThemeAutoTime(isoString) { if (!isoString) return ""; @@ -1592,6 +1601,189 @@ Item { defaultValue: 12 onSliderValueChanged: newValue => SettingsData.setCornerRadius(newValue) } + + SettingsToggleRow { + tab: "theme" + tags: ["elevation", "shadow", "lift", "m3", "material"] + settingKey: "m3ElevationEnabled" + text: I18n.tr("Shadows") + description: I18n.tr("Material inspired shadows and elevation on modals, popouts, and dialogs") + checked: SettingsData.m3ElevationEnabled ?? true + onToggled: checked => SettingsData.set("m3ElevationEnabled", checked) + } + + SettingsSliderRow { + tab: "theme" + tags: ["elevation", "shadow", "intensity", "blur", "m3"] + settingKey: "m3ElevationIntensity" + text: I18n.tr("Shadow Intensity") + description: I18n.tr("Controls the base blur radius and offset of shadows") + value: SettingsData.m3ElevationIntensity ?? 12 + minimum: 0 + maximum: 100 + unit: "px" + defaultValue: 12 + visible: SettingsData.m3ElevationEnabled ?? true + onSliderValueChanged: newValue => SettingsData.set("m3ElevationIntensity", newValue) + } + + SettingsSliderRow { + tab: "theme" + tags: ["elevation", "shadow", "opacity", "transparency", "m3"] + settingKey: "m3ElevationOpacity" + text: I18n.tr("Shadow Opacity") + description: I18n.tr("Controls the transparency of the shadow") + value: SettingsData.m3ElevationOpacity ?? 30 + minimum: 0 + maximum: 100 + unit: "%" + defaultValue: 30 + visible: SettingsData.m3ElevationEnabled ?? true + onSliderValueChanged: newValue => SettingsData.set("m3ElevationOpacity", newValue) + } + + SettingsDropdownRow { + tab: "theme" + tags: ["elevation", "shadow", "color", "m3"] + settingKey: "m3ElevationColorMode" + text: I18n.tr("Shadow Color") + description: I18n.tr("Base color for shadows (opacity is applied automatically)") + options: [I18n.tr("Default (Black)", "shadow color option"), I18n.tr("Text Color", "shadow color option"), I18n.tr("Primary", "shadow color option"), I18n.tr("Surface Variant", "shadow color option"), I18n.tr("Custom", "shadow color option")] + currentValue: { + switch (SettingsData.m3ElevationColorMode) { + case "text": + return I18n.tr("Text Color", "shadow color option"); + case "primary": + return I18n.tr("Primary", "shadow color option"); + case "surfaceVariant": + return I18n.tr("Surface Variant", "shadow color option"); + case "custom": + return I18n.tr("Custom", "shadow color option"); + default: + return I18n.tr("Default (Black)", "shadow color option"); + } + } + visible: SettingsData.m3ElevationEnabled ?? true + onValueChanged: value => { + if (value === I18n.tr("Primary", "shadow color option")) { + SettingsData.set("m3ElevationColorMode", "primary"); + } else if (value === I18n.tr("Surface Variant", "shadow color option")) { + SettingsData.set("m3ElevationColorMode", "surfaceVariant"); + } else if (value === I18n.tr("Custom", "shadow color option")) { + SettingsData.set("m3ElevationColorMode", "custom"); + openM3ShadowColorPicker(); + } else if (value === I18n.tr("Text Color", "shadow color option")) { + SettingsData.set("m3ElevationColorMode", "text"); + } else { + SettingsData.set("m3ElevationColorMode", "default"); + } + } + } + + SettingsDropdownRow { + tab: "theme" + tags: ["elevation", "shadow", "direction", "light", "advanced", "m3"] + settingKey: "m3ElevationLightDirection" + text: I18n.tr("Light Direction") + description: I18n.tr("Controls shadow cast direction for elevation layers") + options: [I18n.tr("Auto (Bar-aware)", "shadow direction option"), I18n.tr("Top (Default)", "shadow direction option"), I18n.tr("Top Left", "shadow direction option"), I18n.tr("Top Right", "shadow direction option"), I18n.tr("Bottom", "shadow direction option")] + currentValue: { + switch (SettingsData.m3ElevationLightDirection) { + case "autoBar": + return I18n.tr("Auto (Bar-aware)", "shadow direction option"); + case "topLeft": + return I18n.tr("Top Left", "shadow direction option"); + case "topRight": + return I18n.tr("Top Right", "shadow direction option"); + case "bottom": + return I18n.tr("Bottom", "shadow direction option"); + default: + return I18n.tr("Top (Default)", "shadow direction option"); + } + } + visible: SettingsData.m3ElevationEnabled ?? true + onValueChanged: value => { + if (value === I18n.tr("Auto (Bar-aware)", "shadow direction option")) { + SettingsData.set("m3ElevationLightDirection", "autoBar"); + } else if (value === I18n.tr("Top Left", "shadow direction option")) { + SettingsData.set("m3ElevationLightDirection", "topLeft"); + } else if (value === I18n.tr("Top Right", "shadow direction option")) { + SettingsData.set("m3ElevationLightDirection", "topRight"); + } else if (value === I18n.tr("Bottom", "shadow direction option")) { + SettingsData.set("m3ElevationLightDirection", "bottom"); + } else { + SettingsData.set("m3ElevationLightDirection", "top"); + } + } + } + + Item { + visible: (SettingsData.m3ElevationEnabled ?? true) && SettingsData.m3ElevationColorMode === "custom" + width: parent.width + implicitHeight: 36 + height: implicitHeight + + Row { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingM + + StyledText { + text: I18n.tr("Custom Shadow Color") + color: Theme.surfaceText + font.pixelSize: Theme.fontSizeMedium + verticalAlignment: Text.AlignVCenter + } + + Rectangle { + width: 26 + height: 26 + radius: 13 + color: SettingsData.m3ElevationCustomColor ?? "#000000" + border.color: Theme.outline + border.width: 1 + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: openM3ShadowColorPicker() + } + } + } + } + + SettingsToggleRow { + tab: "theme" + tags: ["elevation", "shadow", "modal", "dialog", "m3"] + settingKey: "modalElevationEnabled" + text: I18n.tr("Modal Shadows") + description: I18n.tr("Shadow elevation on modals and dialogs") + checked: SettingsData.modalElevationEnabled ?? true + visible: SettingsData.m3ElevationEnabled ?? true + onToggled: checked => SettingsData.set("modalElevationEnabled", checked) + } + + SettingsToggleRow { + tab: "theme" + tags: ["elevation", "shadow", "popout", "popup", "osd", "dropdown", "m3"] + settingKey: "popoutElevationEnabled" + text: I18n.tr("Popout Shadows") + description: I18n.tr("Shadow elevation on popouts, OSDs, and dropdowns") + checked: SettingsData.popoutElevationEnabled ?? true + visible: SettingsData.m3ElevationEnabled ?? true + onToggled: checked => SettingsData.set("popoutElevationEnabled", checked) + } + + SettingsToggleRow { + tab: "theme" + tags: ["elevation", "shadow", "bar", "panel", "navigation", "m3"] + settingKey: "barElevationEnabled" + text: I18n.tr("Bar Shadows") + description: I18n.tr("Shadow elevation on bars and panels") + checked: SettingsData.barElevationEnabled ?? true + visible: SettingsData.m3ElevationEnabled ?? true + onToggled: checked => SettingsData.set("barElevationEnabled", checked) + } } SettingsCard { @@ -2138,12 +2330,41 @@ Item { } } + SettingsCard { + tab: "theme" + tags: ["icon", "theme", "system"] + title: I18n.tr("Icon Theme") + settingKey: "iconTheme" + iconName: "interests" + + SettingsDropdownRow { + tab: "theme" + tags: ["icon", "theme", "system"] + settingKey: "iconTheme" + text: I18n.tr("Icon Theme") + description: I18n.tr("DankShell & System Icons (requires restart)") + currentValue: SettingsData.iconTheme + enableFuzzySearch: true + popupWidthOffset: 100 + maxPopupHeight: 236 + options: cachedIconThemes + onValueChanged: value => { + SettingsData.setIconTheme(value); + if (Quickshell.env("QT_QPA_PLATFORMTHEME") != "gtk3" && Quickshell.env("QT_QPA_PLATFORMTHEME") != "qt6ct" && Quickshell.env("QT_QPA_PLATFORMTHEME_QT6") != "qt6ct") { + ToastService.showError(I18n.tr("Missing Environment Variables", "qt theme env error title"), I18n.tr("You need to set either:\nQT_QPA_PLATFORMTHEME=gtk3 OR\nQT_QPA_PLATFORMTHEME=qt6ct\nas environment variables, and then restart the shell.\n\nqt6ct requires qt6ct-kde to be installed.", "qt theme env error body")); + } + } + } + } + SettingsCard { tab: "theme" tags: ["matugen", "templates", "theming"] title: I18n.tr("Matugen Templates") settingKey: "matugenTemplates" iconName: "auto_awesome" + collapsible: true + expanded: false visible: Theme.matugenAvailable SettingsToggleRow { @@ -2448,33 +2669,6 @@ Item { } } - SettingsCard { - tab: "theme" - tags: ["icon", "theme", "system"] - title: I18n.tr("Icon Theme") - settingKey: "iconTheme" - iconName: "interests" - - SettingsDropdownRow { - tab: "theme" - tags: ["icon", "theme", "system"] - settingKey: "iconTheme" - text: I18n.tr("Icon Theme") - description: I18n.tr("DankShell & System Icons (requires restart)") - currentValue: SettingsData.iconTheme - enableFuzzySearch: true - popupWidthOffset: 100 - maxPopupHeight: 236 - options: cachedIconThemes - onValueChanged: value => { - SettingsData.setIconTheme(value); - if (Quickshell.env("QT_QPA_PLATFORMTHEME") != "gtk3" && Quickshell.env("QT_QPA_PLATFORMTHEME") != "qt6ct" && Quickshell.env("QT_QPA_PLATFORMTHEME_QT6") != "qt6ct") { - ToastService.showError(I18n.tr("Missing Environment Variables", "qt theme env error title"), I18n.tr("You need to set either:\nQT_QPA_PLATFORMTHEME=gtk3 OR\nQT_QPA_PLATFORMTHEME=qt6ct\nas environment variables, and then restart the shell.\n\nqt6ct requires qt6ct-kde to be installed.", "qt theme env error body")); - } - } - } - } - SettingsCard { tab: "theme" tags: ["system", "app", "theming", "gtk", "qt"] diff --git a/quickshell/Modules/Settings/TimeWeatherTab.qml b/quickshell/Modules/Settings/TimeWeatherTab.qml index 39c881f7..006b7b38 100644 --- a/quickshell/Modules/Settings/TimeWeatherTab.qml +++ b/quickshell/Modules/Settings/TimeWeatherTab.qml @@ -706,14 +706,15 @@ Item { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - layer.enabled: true + layer.enabled: Theme.elevationEnabled layer.effect: MultiEffect { - shadowEnabled: true - shadowHorizontalOffset: 0 - shadowVerticalOffset: 4 - shadowBlur: 0.8 - shadowColor: Qt.rgba(0, 0, 0, 0.2) - shadowOpacity: 0.2 + shadowEnabled: Theme.elevationEnabled + shadowHorizontalOffset: Theme.elevationOffsetX(Theme.elevationLevel1) + shadowVerticalOffset: Theme.elevationOffsetY(Theme.elevationLevel1, 1) + shadowBlur: Theme.elevationEnabled ? Math.max(0, Math.min(1, (Theme.elevationLevel1 && Theme.elevationLevel1.blurPx !== undefined ? Theme.elevationLevel1.blurPx : 4) / Theme.elevationBlurMax)) : 0 + blurMax: Theme.elevationBlurMax + shadowColor: Theme.elevationShadowColor(Theme.elevationLevel1) + shadowOpacity: Theme.elevationLevel1 && Theme.elevationLevel1.alpha !== undefined ? Theme.elevationLevel1.alpha : 0.2 } } diff --git a/quickshell/Modules/Toast.qml b/quickshell/Modules/Toast.qml index ae19efaf..2d1356e3 100644 --- a/quickshell/Modules/Toast.qml +++ b/quickshell/Modules/Toast.qml @@ -1,5 +1,4 @@ import QtQuick -import QtQuick.Effects import Quickshell import Quickshell.Wayland import qs.Common @@ -96,7 +95,6 @@ PanelWindow { } } radius: Theme.cornerRadius - layer.enabled: true opacity: shouldBeVisible ? 1 : 0 Column { @@ -406,13 +404,15 @@ PanelWindow { onClicked: ToastService.hideToast() } - layer.effect: MultiEffect { - shadowEnabled: true - shadowHorizontalOffset: 0 - shadowVerticalOffset: 4 - shadowBlur: 0.8 - shadowColor: Qt.rgba(0, 0, 0, 0.3) - shadowOpacity: 0.3 + ElevationShadow { + anchors.fill: parent + z: -1 + level: Theme.elevationLevel3 + fallbackOffset: 6 + targetRadius: toast.radius + targetColor: toast.color + shadowOpacity: Theme.elevationLevel3 && Theme.elevationLevel3.alpha !== undefined ? Theme.elevationLevel3.alpha : 0.3 + shadowEnabled: Theme.elevationEnabled } Behavior on opacity { diff --git a/quickshell/Modules/WorkspaceOverlays/OverviewWidget.qml b/quickshell/Modules/WorkspaceOverlays/OverviewWidget.qml index 6c9e8fa2..03d39fb7 100644 --- a/quickshell/Modules/WorkspaceOverlays/OverviewWidget.qml +++ b/quickshell/Modules/WorkspaceOverlays/OverviewWidget.qml @@ -1,5 +1,4 @@ import QtQuick -import QtQuick.Effects import QtQuick.Layouts import Quickshell import Quickshell.Hyprland @@ -17,59 +16,61 @@ Item { readonly property var allWorkspaces: Hyprland.workspaces?.values || [] readonly property var allWorkspaceIds: { - const workspaces = allWorkspaces - if (!workspaces || workspaces.length === 0) return [] + const workspaces = allWorkspaces; + if (!workspaces || workspaces.length === 0) + return []; try { - const ids = workspaces.map(ws => ws?.id).filter(id => id !== null && id !== undefined) - return ids.sort((a, b) => a - b) + const ids = workspaces.map(ws => ws?.id).filter(id => id !== null && id !== undefined); + return ids.sort((a, b) => a - b); } catch (e) { - return [] + return []; } } readonly property var thisMonitorWorkspaceIds: { - const workspaces = allWorkspaces - const mon = monitor - if (!workspaces || workspaces.length === 0 || !mon) return [] + const workspaces = allWorkspaces; + const mon = monitor; + if (!workspaces || workspaces.length === 0 || !mon) + return []; try { - const filtered = workspaces.filter(ws => ws?.monitor?.name === mon.name) - return filtered.map(ws => ws?.id).filter(id => id !== null && id !== undefined).sort((a, b) => a - b) + const filtered = workspaces.filter(ws => ws?.monitor?.name === mon.name); + return filtered.map(ws => ws?.id).filter(id => id !== null && id !== undefined).sort((a, b) => a - b); } catch (e) { - return [] + return []; } } readonly property var displayedWorkspaceIds: { if (!allWorkspaceIds || allWorkspaceIds.length === 0) { - const result = [] + const result = []; for (let i = 1; i <= workspacesShown; i++) { - result.push(i) + result.push(i); } - return result + return result; } try { - const maxExisting = Math.max(...allWorkspaceIds) - const totalNeeded = Math.max(workspacesShown, allWorkspaceIds.length) - const result = [] + const maxExisting = Math.max(...allWorkspaceIds); + const totalNeeded = Math.max(workspacesShown, allWorkspaceIds.length); + const result = []; for (let i = 1; i <= maxExisting; i++) { - result.push(i) + result.push(i); } - let nextId = maxExisting + 1 + let nextId = maxExisting + 1; while (result.length < totalNeeded) { - result.push(nextId) - nextId++ + result.push(nextId); + nextId++; } - return result + return result; } catch (e) { - const result = [] + const result = []; for (let i = 1; i <= workspacesShown; i++) { - result.push(i) + result.push(i); } - return result + return result; } } @@ -81,24 +82,27 @@ Item { readonly property int effectiveRows: Math.max(SettingsData.overviewRows, Math.ceil(displayWorkspaceCount / effectiveColumns)) function getWorkspaceMonitorName(workspaceId) { - if (!allWorkspaces || !workspaceId) return "" + if (!allWorkspaces || !workspaceId) + return ""; try { - const ws = allWorkspaces.find(w => w?.id === workspaceId) - return ws?.monitor?.name ?? "" + const ws = allWorkspaces.find(w => w?.id === workspaceId); + return ws?.monitor?.name ?? ""; } catch (e) { - return "" + return ""; } } function workspaceHasWindows(workspaceId) { - if (!workspaceId) return false + if (!workspaceId) + return false; try { - const workspace = allWorkspaces.find(ws => ws?.id === workspaceId) - if (!workspace) return false - const toplevels = workspace?.toplevels?.values || [] - return toplevels.length > 0 + const workspace = allWorkspaces.find(ws => ws?.id === workspaceId); + if (!workspace) + return false; + const toplevels = workspace?.toplevels?.values || []; + return toplevels.length > 0; } catch (e) { - return false + return false; } } @@ -124,16 +128,16 @@ Item { implicitHeight: overviewBackground.implicitHeight + Theme.spacingL * 2 Component.onCompleted: { - Hyprland.refreshToplevels() - Hyprland.refreshWorkspaces() - Hyprland.refreshMonitors() + Hyprland.refreshToplevels(); + Hyprland.refreshWorkspaces(); + Hyprland.refreshMonitors(); } onOverviewOpenChanged: { if (overviewOpen) { - Hyprland.refreshToplevels() - Hyprland.refreshWorkspaces() - Hyprland.refreshMonitors() + Hyprland.refreshToplevels(); + Hyprland.refreshWorkspaces(); + Hyprland.refreshMonitors(); } } @@ -148,15 +152,15 @@ Item { radius: Theme.cornerRadius color: Theme.surfaceContainer - layer.enabled: true - layer.effect: MultiEffect { - shadowEnabled: true - shadowBlur: 0.5 - shadowHorizontalOffset: 0 - shadowVerticalOffset: 4 - shadowColor: Theme.shadowStrong - shadowOpacity: 1 - blurMax: 32 + ElevationShadow { + anchors.fill: parent + z: -1 + level: Theme.elevationLevel2 + fallbackOffset: 4 + targetRadius: Theme.cornerRadius + targetColor: Theme.surfaceContainer + shadowOpacity: Theme.elevationLevel2 && Theme.elevationLevel2.alpha !== undefined ? Theme.elevationLevel2.alpha : 0.25 + shadowEnabled: Theme.elevationEnabled } ColumnLayout { @@ -217,8 +221,8 @@ Item { acceptedButtons: Qt.LeftButton onClicked: { if (root.draggingTargetWorkspace === -1) { - root.overviewOpen = false - Hyprland.dispatch(`workspace ${workspaceValue}`) + root.overviewOpen = false; + Hyprland.dispatch(`workspace ${workspaceValue}`); } } } @@ -226,13 +230,15 @@ Item { DropArea { anchors.fill: parent onEntered: { - root.draggingTargetWorkspace = workspaceValue - if (root.draggingFromWorkspace == root.draggingTargetWorkspace) return - hoveredWhileDragging = true + root.draggingTargetWorkspace = workspaceValue; + if (root.draggingFromWorkspace == root.draggingTargetWorkspace) + return; + hoveredWhileDragging = true; } onExited: { - hoveredWhileDragging = false - if (root.draggingTargetWorkspace == workspaceValue) root.draggingTargetWorkspace = -1 + hoveredWhileDragging = false; + if (root.draggingTargetWorkspace == workspaceValue) + root.draggingTargetWorkspace = -1; } } } @@ -250,27 +256,28 @@ Item { Repeater { model: ScriptModel { values: { - const workspaces = root.allWorkspaces - const minId = root.minWorkspaceId - const maxId = root.maxWorkspaceId + const workspaces = root.allWorkspaces; + const minId = root.minWorkspaceId; + const maxId = root.maxWorkspaceId; - if (!workspaces || workspaces.length === 0) return [] + if (!workspaces || workspaces.length === 0) + return []; try { - const result = [] + const result = []; for (const workspace of workspaces) { - const wsId = workspace?.id ?? -1 + const wsId = workspace?.id ?? -1; if (wsId >= minId && wsId <= maxId) { - const toplevels = workspace?.toplevels?.values || [] + const toplevels = workspace?.toplevels?.values || []; for (const toplevel of toplevels) { - result.push(toplevel) + result.push(toplevel); } } } - return result + return result; } catch (e) { - console.error("OverviewWidget filter error:", e) - return [] + console.error("OverviewWidget filter error:", e); + return []; } } } @@ -282,17 +289,19 @@ Item { readonly property int windowWorkspaceId: modelData?.workspace?.id ?? -1 function getWorkspaceIndex() { - if (!root.displayedWorkspaceIds || root.displayedWorkspaceIds.length === 0) return 0 - if (!windowWorkspaceId || windowWorkspaceId < 0) return 0 + if (!root.displayedWorkspaceIds || root.displayedWorkspaceIds.length === 0) + return 0; + if (!windowWorkspaceId || windowWorkspaceId < 0) + return 0; try { for (let i = 0; i < root.displayedWorkspaceIds.length; i++) { if (root.displayedWorkspaceIds[i] === windowWorkspaceId) { - return i + return i; } } - return 0 + return 0; } catch (e) { - return 0 + return 0; } } @@ -325,48 +334,48 @@ Item { acceptedButtons: Qt.LeftButton | Qt.MiddleButton drag.target: parent - onPressed: (mouse) => { - root.draggingFromWorkspace = windowData?.workspace.id - window.pressed = true - window.Drag.active = true - window.Drag.source = window - window.Drag.hotSpot.x = mouse.x - window.Drag.hotSpot.y = mouse.y + onPressed: mouse => { + root.draggingFromWorkspace = windowData?.workspace.id; + window.pressed = true; + window.Drag.active = true; + window.Drag.source = window; + window.Drag.hotSpot.x = mouse.x; + window.Drag.hotSpot.y = mouse.y; } onReleased: { - const targetWorkspace = root.draggingTargetWorkspace - window.pressed = false - window.Drag.active = false - root.draggingFromWorkspace = -1 - root.draggingTargetWorkspace = -1 + const targetWorkspace = root.draggingTargetWorkspace; + window.pressed = false; + window.Drag.active = false; + root.draggingFromWorkspace = -1; + root.draggingTargetWorkspace = -1; if (targetWorkspace !== -1 && targetWorkspace !== windowData?.workspace.id) { - Hyprland.dispatch(`movetoworkspacesilent ${targetWorkspace},address:${windowData?.address}`) + Hyprland.dispatch(`movetoworkspacesilent ${targetWorkspace},address:${windowData?.address}`); Qt.callLater(() => { - Hyprland.refreshToplevels() - Hyprland.refreshWorkspaces() + Hyprland.refreshToplevels(); + Hyprland.refreshWorkspaces(); Qt.callLater(() => { - window.x = window.initX - window.y = window.initY - }) - }) + window.x = window.initX; + window.y = window.initY; + }); + }); } else { - window.x = window.initX - window.y = window.initY + window.x = window.initX; + window.y = window.initY; } } - onClicked: (event) => { - if (!windowData || !windowData.address) return - + onClicked: event => { + if (!windowData || !windowData.address) + return; if (event.button === Qt.LeftButton) { - root.overviewOpen = false - Hyprland.dispatch(`focuswindow address:${windowData.address}`) - event.accepted = true + root.overviewOpen = false; + Hyprland.dispatch(`focuswindow address:${windowData.address}`); + event.accepted = true; } else if (event.button === Qt.MiddleButton) { - Hyprland.dispatch(`closewindow address:${windowData.address}`) - event.accepted = true + Hyprland.dispatch(`closewindow address:${windowData.address}`); + event.accepted = true; } } } diff --git a/quickshell/Widgets/AppIconRenderer.qml b/quickshell/Widgets/AppIconRenderer.qml index bb962ae9..289f92c4 100644 --- a/quickshell/Widgets/AppIconRenderer.qml +++ b/quickshell/Widgets/AppIconRenderer.qml @@ -98,7 +98,7 @@ Item { sourceComponent: IconImage { anchors.fill: parent source: root.iconPath - backer.sourceSize: Qt.size(root.iconSize, root.iconSize) + backer.sourceSize: Qt.size(root.iconSize * 2, root.iconSize * 2) mipmap: true asynchronous: true visible: status === Image.Ready diff --git a/quickshell/Widgets/DankCircularImage.qml b/quickshell/Widgets/DankCircularImage.qml index d7911eac..36cbfe4b 100644 --- a/quickshell/Widgets/DankCircularImage.qml +++ b/quickshell/Widgets/DankCircularImage.qml @@ -81,6 +81,8 @@ Rectangle { mipmap: true cache: true visible: false + sourceSize.width: Math.max(width * 2, 128) + sourceSize.height: Math.max(height * 2, 128) source: !root.shouldProbe ? root.imageSource : "" } diff --git a/quickshell/Widgets/DankDropdown.qml b/quickshell/Widgets/DankDropdown.qml index c1355d45..bd342b84 100644 --- a/quickshell/Widgets/DankDropdown.qml +++ b/quickshell/Widgets/DankDropdown.qml @@ -1,7 +1,6 @@ import "../Common/fzf.js" as Fzf import QtQuick import QtQuick.Controls -import QtQuick.Effects import Quickshell import qs.Common import qs.Widgets @@ -254,6 +253,8 @@ Item { } contentItem: Rectangle { + id: contentSurface + LayoutMirroring.enabled: I18n.isRtl LayoutMirroring.childrenInherit: true color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 1) @@ -261,12 +262,17 @@ Item { border.width: 2 radius: Theme.cornerRadius - layer.enabled: true - layer.effect: MultiEffect { - shadowEnabled: true - shadowBlur: 0.4 - shadowColor: Theme.shadowStrong - shadowVerticalOffset: 4 + ElevationShadow { + id: shadowLayer + anchors.fill: parent + z: -1 + level: Theme.elevationLevel2 + fallbackOffset: 4 + targetRadius: contentSurface.radius + targetColor: contentSurface.color + borderColor: contentSurface.border.color + borderWidth: contentSurface.border.width + shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled } Column { diff --git a/quickshell/Widgets/DankIconPicker.qml b/quickshell/Widgets/DankIconPicker.qml index 5d0386dc..3738525b 100644 --- a/quickshell/Widgets/DankIconPicker.qml +++ b/quickshell/Widgets/DankIconPicker.qml @@ -1,6 +1,5 @@ import QtQuick import QtQuick.Controls -import QtQuick.Effects import qs.Common import qs.Widgets @@ -132,16 +131,20 @@ Rectangle { } contentItem: Rectangle { + id: contentSurface color: Theme.surface radius: Theme.cornerRadius - layer.enabled: true - layer.effect: MultiEffect { - shadowEnabled: true - shadowColor: Theme.shadowStrong - shadowBlur: 0.8 - shadowHorizontalOffset: 0 - shadowVerticalOffset: 4 + ElevationShadow { + id: shadowLayer + anchors.fill: parent + z: -1 + level: Theme.elevationLevel2 + fallbackOffset: 4 + targetRadius: contentSurface.radius + targetColor: contentSurface.color + shadowOpacity: Theme.elevationLevel2 && Theme.elevationLevel2.alpha !== undefined ? Theme.elevationLevel2.alpha : 0.25 + shadowEnabled: Theme.elevationEnabled } Rectangle { diff --git a/quickshell/Widgets/DankOSD.qml b/quickshell/Widgets/DankOSD.qml index 2811540e..5a83562f 100644 --- a/quickshell/Widgets/DankOSD.qml +++ b/quickshell/Widgets/DankOSD.qml @@ -1,5 +1,4 @@ import QtQuick -import QtQuick.Effects import Quickshell import Quickshell.Wayland import qs.Common @@ -257,11 +256,7 @@ PanelWindow { scale: shouldBeVisible ? 1 : 0.9 property bool childHovered: false - property real shadowBlurPx: 10 - property real shadowSpreadPx: 0 - property real shadowBaseAlpha: 0.60 readonly property real popupSurfaceAlpha: SettingsData.popupTransparency - readonly property real effectiveShadowAlpha: shouldBeVisible ? Math.max(0, Math.min(1, shadowBaseAlpha * popupSurfaceAlpha)) : 0 Rectangle { id: background @@ -273,38 +268,20 @@ PanelWindow { z: -1 } - Item { + ElevationShadow { id: bgShadowLayer anchors.fill: parent visible: osdContainer.popupSurfaceAlpha >= 0.95 - layer.enabled: Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" - layer.smooth: false + z: -1 + level: Theme.elevationLevel3 + fallbackOffset: 6 + targetRadius: Theme.cornerRadius + targetColor: Theme.surfaceContainer + borderColor: Theme.outlineMedium + borderWidth: 1 + shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" layer.textureSize: Qt.size(Math.round(width * root.dpr), Math.round(height * root.dpr)) layer.textureMirroring: ShaderEffectSource.MirrorVertically - - readonly property int blurMax: 64 - - layer.effect: MultiEffect { - id: shadowFx - autoPaddingEnabled: true - shadowEnabled: true - blurEnabled: false - maskEnabled: false - shadowBlur: Math.max(0, Math.min(1, osdContainer.shadowBlurPx / bgShadowLayer.blurMax)) - shadowScale: 1 + (2 * osdContainer.shadowSpreadPx) / Math.max(1, Math.min(bgShadowLayer.width, bgShadowLayer.height)) - shadowColor: { - const baseColor = Theme.isLightMode ? Qt.rgba(0, 0, 0, 1) : Theme.surfaceContainerHighest; - return Theme.withAlpha(baseColor, osdContainer.effectiveShadowAlpha); - } - } - - Rectangle { - anchors.fill: parent - radius: Theme.cornerRadius - color: Theme.surfaceContainer - border.color: Theme.outlineMedium - border.width: 1 - } } MouseArea { diff --git a/quickshell/Widgets/DankPopout.qml b/quickshell/Widgets/DankPopout.qml index 1c9207b6..3bd301f0 100644 --- a/quickshell/Widgets/DankPopout.qml +++ b/quickshell/Widgets/DankPopout.qml @@ -1,5 +1,4 @@ import QtQuick -import QtQuick.Effects import Quickshell import Quickshell.Wayland import qs.Common @@ -31,6 +30,7 @@ Item { property bool backgroundInteractive: true property bool contentHandlesKeys: false property bool fullHeightSurface: false + property bool _primeContent: false property bool _resizeActive: false property real _surfaceMarginLeft: 0 property real _surfaceW: 0 @@ -77,6 +77,38 @@ Item { property int effectiveBarPosition: 0 property real effectiveBarBottomGap: 0 + readonly property string autoBarShadowDirection: { + const section = triggerSection || "center"; + switch (effectiveBarPosition) { + case SettingsData.Position.Top: + if (section === "left") + return "topLeft"; + if (section === "right") + return "topRight"; + return "top"; + case SettingsData.Position.Bottom: + if (section === "left") + return "bottomLeft"; + if (section === "right") + return "bottomRight"; + return "bottom"; + case SettingsData.Position.Left: + if (section === "left") + return "topLeft"; + if (section === "right") + return "bottomLeft"; + return "left"; + case SettingsData.Position.Right: + if (section === "left") + return "topRight"; + if (section === "right") + return "bottomRight"; + return "right"; + default: + return "top"; + } + } + readonly property string effectiveShadowDirection: Theme.elevationLightDirection === "autoBar" ? autoBarShadowDirection : Theme.elevationLightDirection // Snapshot mask geometry to prevent background damage on bar updates property real _frozenMaskX: 0 @@ -89,6 +121,14 @@ Item { effectiveBarBottomGap = bottomGap !== undefined ? bottomGap : 0; } + function primeContent() { + _primeContent = true; + } + + function clearPrimedContent() { + _primeContent = false; + } + function setTriggerPosition(x, y, width, section, targetScreen, barPosition, barThickness, barSpacing, barConfig) { triggerX = x; triggerY = y; @@ -152,6 +192,7 @@ Item { function close() { shouldBeVisible = false; + _primeContent = false; PopoutManager.popoutChanged(); closeTimer.restart(); } @@ -197,7 +238,11 @@ Item { readonly property real screenHeight: screen ? screen.height : 0 readonly property real dpr: screen ? screen.devicePixelRatio : 1 - readonly property real shadowBuffer: 5 + readonly property var shadowLevel: Theme.elevationLevel3 + readonly property real shadowFallbackOffset: 6 + readonly property real shadowRenderPadding: (Theme.elevationEnabled && SettingsData.popoutElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, effectiveShadowDirection, shadowFallbackOffset, 8, 16) : 0 + readonly property real shadowMotionPadding: Math.max(0, animationOffset) + readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr) readonly property real alignedWidth: Theme.px(popupWidth, dpr) readonly property real alignedHeight: Theme.px(popupHeight, dpr) @@ -257,29 +302,30 @@ Item { } })(), dpr) + readonly property real triggeringBarLeftExclusion: (effectiveBarPosition === SettingsData.Position.Left && barWidth > 0) ? Math.max(0, barX + barWidth) : 0 + readonly property real triggeringBarTopExclusion: (effectiveBarPosition === SettingsData.Position.Top && barHeight > 0) ? Math.max(0, barY + barHeight) : 0 + readonly property real triggeringBarRightExclusion: (effectiveBarPosition === SettingsData.Position.Right && barWidth > 0) ? Math.max(0, screenWidth - barX) : 0 + readonly property real triggeringBarBottomExclusion: (effectiveBarPosition === SettingsData.Position.Bottom && barHeight > 0) ? Math.max(0, screenHeight - barY) : 0 + readonly property real maskX: { - const triggeringBarX = (effectiveBarPosition === SettingsData.Position.Left && barWidth > 0) ? barWidth : 0; const adjacentLeftBar = adjacentBarInfo?.leftBar ?? 0; - return Math.max(triggeringBarX, adjacentLeftBar); + return Math.max(triggeringBarLeftExclusion, adjacentLeftBar); } readonly property real maskY: { - const triggeringBarY = (effectiveBarPosition === SettingsData.Position.Top && barHeight > 0) ? barHeight : 0; const adjacentTopBar = adjacentBarInfo?.topBar ?? 0; - return Math.max(triggeringBarY, adjacentTopBar); + return Math.max(triggeringBarTopExclusion, adjacentTopBar); } readonly property real maskWidth: { - const triggeringBarRight = (effectiveBarPosition === SettingsData.Position.Right && barWidth > 0) ? barWidth : 0; const adjacentRightBar = adjacentBarInfo?.rightBar ?? 0; - const rightExclusion = Math.max(triggeringBarRight, adjacentRightBar); + const rightExclusion = Math.max(triggeringBarRightExclusion, adjacentRightBar); return Math.max(100, screenWidth - maskX - rightExclusion); } readonly property real maskHeight: { - const triggeringBarBottom = (effectiveBarPosition === SettingsData.Position.Bottom && barHeight > 0) ? barHeight : 0; const adjacentBottomBar = adjacentBarInfo?.bottomBar ?? 0; - const bottomExclusion = Math.max(triggeringBarBottom, adjacentBottomBar); + const bottomExclusion = Math.max(triggeringBarBottomExclusion, adjacentBottomBar); return Math.max(100, screenHeight - maskY - bottomExclusion); } @@ -395,7 +441,7 @@ Item { implicitWidth: useBackgroundWindow ? root._surfaceW : 0 implicitHeight: (useBackgroundWindow && !_fullHeight) ? (root.alignedHeight + shadowBuffer * 2) : 0 - mask: (useBackgroundWindow && _fullHeight) ? contentInputMask : null + mask: useBackgroundWindow ? contentInputMask : null Region { id: contentInputMask @@ -405,10 +451,10 @@ Item { Item { id: contentMaskRect visible: false - x: contentContainer.x - root.shadowBuffer - y: contentContainer.y - root.shadowBuffer - width: root.alignedWidth + root.shadowBuffer * 2 - height: root.alignedHeight + root.shadowBuffer * 2 + x: contentContainer.x + y: contentContainer.y + width: root.alignedWidth + height: root.alignedHeight } MouseArea { @@ -480,42 +526,20 @@ Item { } } - Rectangle { + ElevationShadow { id: shadowSource - anchors.centerIn: parent width: parent.width height: parent.height - radius: Theme.cornerRadius - color: "black" - visible: false opacity: contentWrapper.opacity scale: contentWrapper.scale x: contentWrapper.x y: contentWrapper.y - - property real shadowBlurPx: 10 - property real shadowSpreadPx: 0 - property real shadowBaseAlpha: 0.60 - readonly property real popupSurfaceAlpha: SettingsData.popupTransparency - readonly property real effectiveShadowAlpha: Math.max(0, Math.min(1, shadowBaseAlpha * popupSurfaceAlpha)) - readonly property int blurMax: 64 - - layer.enabled: Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" && !(root.suspendShadowWhileResizing && root._resizeActive) - layer.smooth: false - - layer.effect: MultiEffect { - id: shadowFx - autoPaddingEnabled: true - shadowEnabled: true - blurEnabled: false - maskEnabled: false - shadowBlur: Math.max(0, Math.min(1, shadowSource.shadowBlurPx / shadowSource.blurMax)) - shadowScale: 1 + (2 * shadowSource.shadowSpreadPx) / Math.max(1, Math.min(shadowSource.width, shadowSource.height)) - shadowColor: { - const baseColor = Theme.isLightMode ? Qt.rgba(0, 0, 0, 1) : Theme.surfaceContainerHighest; - return Theme.withAlpha(baseColor, shadowSource.effectiveShadowAlpha); - } - } + level: root.shadowLevel + direction: root.effectiveShadowDirection + fallbackOffset: root.shadowFallbackOffset + targetRadius: Theme.cornerRadius + targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) + shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" && !(root.suspendShadowWhileResizing && root._resizeActive) } Item { @@ -546,13 +570,13 @@ Item { radius: Theme.cornerRadius color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) border.color: Theme.outlineMedium - border.width: 1 + border.width: 0 } Loader { id: contentLoader anchors.fill: parent - active: shouldBeVisible || contentWindow.visible + active: root._primeContent || shouldBeVisible || contentWindow.visible asynchronous: false } } diff --git a/quickshell/matugen/templates/firefox-userchrome.css b/quickshell/matugen/templates/firefox-userchrome.css index 2ee23124..07436cc2 100644 --- a/quickshell/matugen/templates/firefox-userchrome.css +++ b/quickshell/matugen/templates/firefox-userchrome.css @@ -53,8 +53,11 @@ --m3-radius: 12px; --m3-radius-sm: 10px; --m3-elev-0: none; - --m3-elev-1: 0 1px 2px rgba(0,0,0,.08), 0 1px 3px rgba(0,0,0,.06); - --m3-elev-2: 0 2px 6px rgba(0,0,0,.10), 0 1px 3px rgba(0,0,0,.06); + --m3-elev-1: 0 1px 2px color-mix(in srgb, var(--md-sys-color-shadow) 8%, transparent), 0 1px 3px color-mix(in srgb, var(--md-sys-color-shadow) 6%, transparent); + --m3-elev-2: 0 2px 6px color-mix(in srgb, var(--md-sys-color-shadow) 10%, transparent), 0 1px 3px color-mix(in srgb, var(--md-sys-color-shadow) 6%, transparent); + --m3-elev-3: 0 11px 7px color-mix(in srgb, var(--md-sys-color-shadow) 19%, transparent), 0 13px 25px color-mix(in srgb, var(--md-sys-color-shadow) 30%, transparent); + --m3-elev-4: 0 14px 12px color-mix(in srgb, var(--md-sys-color-shadow) 17%, transparent), 0 20px 40px color-mix(in srgb, var(--md-sys-color-shadow) 30%, transparent); + --m3-elev-5: 0 17px 17px color-mix(in srgb, var(--md-sys-color-shadow) 15%, transparent), 0 27px 55px color-mix(in srgb, var(--md-sys-color-shadow) 30%, transparent); --tab-height: 34px; --urlbar-height: 38px; @@ -118,8 +121,11 @@ --md-sys-color-surface-container-high: {{colors.surface_container_high.dark.hex}}; --md-sys-color-surface-container-highest: {{colors.surface_container_highest.dark.hex}}; - --m3-elev-1: 0 1px 2px rgba(0,0,0,.50), 0 1px 3px rgba(0,0,0,.35); - --m3-elev-2: 0 4px 10px rgba(0,0,0,.55), 0 1px 3px rgba(0,0,0,.35); + --m3-elev-1: 0 1px 2px color-mix(in srgb, var(--md-sys-color-shadow) 50%, transparent), 0 1px 3px color-mix(in srgb, var(--md-sys-color-shadow) 35%, transparent); + --m3-elev-2: 0 4px 10px color-mix(in srgb, var(--md-sys-color-shadow) 55%, transparent), 0 1px 3px color-mix(in srgb, var(--md-sys-color-shadow) 35%, transparent); + --m3-elev-3: 0 11px 7px color-mix(in srgb, var(--md-sys-color-shadow) 45%, transparent), 0 13px 25px color-mix(in srgb, var(--md-sys-color-shadow) 55%, transparent); + --m3-elev-4: 0 14px 12px color-mix(in srgb, var(--md-sys-color-shadow) 42%, transparent), 0 20px 40px color-mix(in srgb, var(--md-sys-color-shadow) 55%, transparent); + --m3-elev-5: 0 17px 17px color-mix(in srgb, var(--md-sys-color-shadow) 40%, transparent), 0 27px 55px color-mix(in srgb, var(--md-sys-color-shadow) 55%, transparent); --state-hover: color-mix(in srgb, var(--md-sys-color-on-surface) 6%, transparent); --state-press: color-mix(in srgb, var(--md-sys-color-on-surface) 10%, transparent); diff --git a/quickshell/translations/settings_search_index.json b/quickshell/translations/settings_search_index.json index 7874d363..7416566c 100644 --- a/quickshell/translations/settings_search_index.json +++ b/quickshell/translations/settings_search_index.json @@ -2416,6 +2416,74 @@ ], "description": "0 = square corners" }, + { + "section": "m3ElevationEnabled", + "label": "M3 Elevation & Shadows", + "tabIndex": 10, + "category": "Theme & Colors", + "keywords": [ + "appearance", + "elevation", + "lift", + "material", + "m3", + "shadow", + "shadows", + "theme" + ], + "description": "Material Design 3 shadows and elevation on modals, popouts, and dialogs" + }, + { + "section": "modalElevationEnabled", + "label": "Modal Shadows", + "tabIndex": 10, + "category": "Theme & Colors", + "keywords": [ + "dialog", + "elevation", + "m3", + "material", + "modal", + "shadow", + "shadows" + ], + "description": "Shadow elevation on modals and dialogs" + }, + { + "section": "popoutElevationEnabled", + "label": "Popout Shadows", + "tabIndex": 10, + "category": "Theme & Colors", + "keywords": [ + "dropdown", + "elevation", + "m3", + "material", + "osd", + "popout", + "popup", + "shadow", + "shadows" + ], + "description": "Shadow elevation on popouts, OSDs, and dropdowns" + }, + { + "section": "barElevationEnabled", + "label": "Bar Shadows", + "tabIndex": 10, + "category": "Theme & Colors", + "keywords": [ + "bar", + "elevation", + "m3", + "material", + "navigation", + "panel", + "shadow", + "shadows" + ], + "description": "Shadow elevation on bars and panels" + }, { "section": "cursorSize", "label": "Cursor Size",