From 1217b25de5f408eeb57d6a2bd8bafe8e41154aa7 Mon Sep 17 00:00:00 2001 From: purian23 Date: Tue, 31 Mar 2026 21:25:51 -0400 Subject: [PATCH] (frame): Add blur support & cleanup --- quickshell/Common/SettingsData.qml | 14 +- quickshell/Common/settings/SettingsSpec.js | 6 +- quickshell/Common/settings/SettingsStore.js | 35 +---- quickshell/Modules/DankBar/DankBarContent.qml | 4 +- quickshell/Modules/DankBar/DankBarWindow.qml | 14 +- quickshell/Modules/Frame/FrameBorder.qml | 30 ++-- quickshell/Modules/Frame/FrameWindow.qml | 136 +++++++++++++++++- quickshell/Modules/Settings/FrameTab.qml | 71 ++++++--- 8 files changed, 232 insertions(+), 78 deletions(-) diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index f14783b7..77f372a7 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: 10 + readonly property int settingsConfigVersion: 11 readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true" @@ -209,18 +209,18 @@ Singleton { onFrameColorChanged: saveSettings() property real frameOpacity: 1.0 onFrameOpacityChanged: saveSettings() - property bool frameSyncBarColor: true - onFrameSyncBarColorChanged: saveSettings() property var frameScreenPreferences: ["all"] onFrameScreenPreferencesChanged: saveSettings() - property real frameBarThickness: 42 - onFrameBarThicknessChanged: saveSettings() + property real frameBarSize: 40 + onFrameBarSizeChanged: saveSettings() property bool frameShowOnOverview: false onFrameShowOnOverviewChanged: saveSettings() + property bool frameBlurEnabled: true + onFrameBlurEnabledChanged: saveSettings() readonly property color effectiveFrameColor: { const fc = frameColor; - if (!fc || fc === "default") return Theme.background; + if (!fc || fc === "default") return Theme.surfaceContainer; if (fc === "primary") return Theme.primary; if (fc === "surface") return Theme.surface; return fc; @@ -2010,7 +2010,7 @@ Singleton { } function getActiveBarThicknessForScreen(screen) { - if (frameEnabled) return frameBarThickness; + if (frameEnabled) return frameBarSize; if (!screen) return frameThickness; for (var i = 0; i < barConfigs.length; i++) { var bc = barConfigs[i]; diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index c0089600..f6029430 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -554,10 +554,10 @@ var SPEC = { frameRounding: { def: 23 }, frameColor: { def: "" }, frameOpacity: { def: 1.0 }, - frameSyncBarColor: { def: true }, frameScreenPreferences: { def: ["all"] }, - frameBarThickness: { def: 42 }, - frameShowOnOverview: { def: false } + frameBarSize: { def: 40 }, + frameShowOnOverview: { def: false }, + frameBlurEnabled: { def: true } }; function getValidKeys() { diff --git a/quickshell/Common/settings/SettingsStore.js b/quickshell/Common/settings/SettingsStore.js index d04e89fa..43c017a1 100644 --- a/quickshell/Common/settings/SettingsStore.js +++ b/quickshell/Common/settings/SettingsStore.js @@ -248,39 +248,8 @@ function migrateToVersion(obj, targetVersion) { settings.configVersion = 6; } - if (currentVersion < 7) { - console.info("Migrating settings from version", currentVersion, "to version 7"); - - if (settings.frameEnabled === undefined) settings.frameEnabled = false; - if (settings.frameThickness === undefined) settings.frameThickness = 16; - if (settings.frameRounding === undefined) settings.frameRounding = 23; - if (settings.frameColor === undefined) settings.frameColor = "#2a2a2a"; - if (settings.frameOpacity === undefined) settings.frameOpacity = 1.0; - if (settings.frameSyncBarColor === undefined) settings.frameSyncBarColor = true; - if (settings.frameScreenPreferences === undefined) settings.frameScreenPreferences = ["all"]; - - settings.configVersion = 7; - } - - if (currentVersion < 8) { - console.info("Migrating settings from version", currentVersion, "to version 8"); - - if (settings.frameBarThickness === undefined) settings.frameBarThickness = 42; - - settings.configVersion = 8; - } - - if (currentVersion < 9) { - console.info("Migrating settings from version", currentVersion, "to version 9"); - - if (settings.frameShowOnOverview === undefined) settings.frameShowOnOverview = false; - - settings.configVersion = 9; - } - - // v10 migration — Session 5 - if (currentVersion < 10) { - settings.configVersion = 10; + if (currentVersion < 11) { + settings.configVersion = 11; } return settings; diff --git a/quickshell/Modules/DankBar/DankBarContent.qml b/quickshell/Modules/DankBar/DankBarContent.qml index be782e3b..b3175b21 100644 --- a/quickshell/Modules/DankBar/DankBarContent.qml +++ b/quickshell/Modules/DankBar/DankBarContent.qml @@ -26,13 +26,13 @@ Item { readonly property real _frameLeftInset: { if (!SettingsData.frameEnabled || barWindow.isVertical) return 0 return barWindow.hasAdjacentLeftBar - ? SettingsData.frameBarThickness + ? SettingsData.frameBarSize : 0 } readonly property real _frameRightInset: { if (!SettingsData.frameEnabled || barWindow.isVertical) return 0 return barWindow.hasAdjacentRightBar - ? SettingsData.frameBarThickness + ? SettingsData.frameBarSize : 0 } readonly property real _frameTopInset: { diff --git a/quickshell/Modules/DankBar/DankBarWindow.qml b/quickshell/Modules/DankBar/DankBarWindow.qml index 11613945..a835e9f2 100644 --- a/quickshell/Modules/DankBar/DankBarWindow.qml +++ b/quickshell/Modules/DankBar/DankBarWindow.qml @@ -133,6 +133,11 @@ PanelWindow { teardown(); if (!BlurService.enabled || !BlurService.available) return; + // In frame mode, FrameWindow owns the blur region for the entire screen edge + // (including the bar area). The bar must not set its own competing blur region + // so that frameBlurEnabled acts as the single control for all blur in frame mode. + if (SettingsData.frameEnabled) + return; const widgets = barWindow._blurWidgetItems.filter(w => w && w.visible && w.width > 0 && w.height > 0); const hasBar = barHasTransparency; @@ -187,6 +192,11 @@ PanelWindow { } } + Connections { + target: SettingsData + function onFrameEnabledChanged() { barBlur.rebuild(); } + } + Connections { target: topBarSlide function onXChanged() { @@ -238,7 +248,7 @@ PanelWindow { readonly property color _surfaceContainer: Theme.surfaceContainer readonly property string _barId: barConfig?.id ?? "default" property real _backgroundAlpha: barConfig?.transparency ?? 1.0 - readonly property color _bgColor: (SettingsData.frameEnabled && SettingsData.frameSyncBarColor) + readonly property color _bgColor: SettingsData.frameEnabled ? Qt.rgba(SettingsData.effectiveFrameColor.r, SettingsData.effectiveFrameColor.g, SettingsData.effectiveFrameColor.b, SettingsData.frameOpacity) : Theme.withAlpha(_surfaceContainer, _backgroundAlpha) @@ -398,7 +408,7 @@ PanelWindow { readonly property int notificationCount: NotificationService.notifications.length readonly property real effectiveBarThickness: SettingsData.frameEnabled - ? SettingsData.frameBarThickness + ? SettingsData.frameBarSize : Theme.snap(Math.max(barWindow.widgetThickness + (barConfig?.innerPadding ?? 4) + 4, Theme.barHeight - 4 - (8 - (barConfig?.innerPadding ?? 4))), _dpr) readonly property bool effectiveOpenOnOverview: SettingsData.frameEnabled ? SettingsData.frameShowOnOverview diff --git a/quickshell/Modules/Frame/FrameBorder.qml b/quickshell/Modules/Frame/FrameBorder.qml index 0e88b9b4..6630b253 100644 --- a/quickshell/Modules/Frame/FrameBorder.qml +++ b/quickshell/Modules/Frame/FrameBorder.qml @@ -9,18 +9,24 @@ Item { anchors.fill: parent - required property var barEdges - - readonly property real _thickness: SettingsData.frameThickness - readonly property real _barThickness: SettingsData.frameBarThickness - readonly property real _rounding: SettingsData.frameRounding + required property real cutoutTopInset + required property real cutoutBottomInset + required property real cutoutLeftInset + required property real cutoutRightInset + required property real cutoutRadius Rectangle { id: borderRect anchors.fill: parent - color: SettingsData.effectiveFrameColor - opacity: SettingsData.frameOpacity + // Bake frameOpacity into the color alpha rather than using the `opacity` property. + // Qt Quick can skip layer.effect processing on items with opacity < 1 as an + // optimization, causing the MultiEffect inverted mask to stop working and the + // Rectangle to render as a plain square at low opacity values. + color: Qt.rgba(SettingsData.effectiveFrameColor.r, + SettingsData.effectiveFrameColor.g, + SettingsData.effectiveFrameColor.b, + SettingsData.frameOpacity) layer.enabled: true layer.effect: MultiEffect { @@ -42,12 +48,12 @@ Item { Rectangle { anchors { fill: parent - topMargin: root.barEdges.includes("top") ? root._barThickness : root._thickness - bottomMargin: root.barEdges.includes("bottom") ? root._barThickness : root._thickness - leftMargin: root.barEdges.includes("left") ? root._barThickness : root._thickness - rightMargin: root.barEdges.includes("right") ? root._barThickness : root._thickness + topMargin: root.cutoutTopInset + bottomMargin: root.cutoutBottomInset + leftMargin: root.cutoutLeftInset + rightMargin: root.cutoutRightInset } - radius: root._rounding + radius: root.cutoutRadius } } } diff --git a/quickshell/Modules/Frame/FrameWindow.qml b/quickshell/Modules/Frame/FrameWindow.qml index 0786432f..ab710052 100644 --- a/quickshell/Modules/Frame/FrameWindow.qml +++ b/quickshell/Modules/Frame/FrameWindow.qml @@ -4,6 +4,7 @@ import QtQuick import Quickshell import Quickshell.Wayland import qs.Common +import qs.Services PanelWindow { id: win @@ -29,9 +30,140 @@ PanelWindow { // No input — pass everything through to apps and bar mask: Region {} + readonly property var barEdges: { + SettingsData.barConfigs; + return SettingsData.getActiveBarEdgesForScreen(win.screen); + } + + readonly property real _dpr: CompositorService.getScreenScale(win.screen) + readonly property bool _frameActive: SettingsData.frameEnabled + && SettingsData.isScreenInPreferences(win.screen, SettingsData.frameScreenPreferences) + readonly property int _windowRegionWidth: win._regionInt(win.width) + readonly property int _windowRegionHeight: win._regionInt(win.height) + + function _regionInt(value) { + return Math.max(0, Math.round(Theme.px(value, win._dpr))); + } + + readonly property int cutoutTopInset: win._regionInt(barEdges.includes("top") ? SettingsData.frameBarSize : SettingsData.frameThickness) + readonly property int cutoutBottomInset: win._regionInt(barEdges.includes("bottom") ? SettingsData.frameBarSize : SettingsData.frameThickness) + readonly property int cutoutLeftInset: win._regionInt(barEdges.includes("left") ? SettingsData.frameBarSize : SettingsData.frameThickness) + readonly property int cutoutRightInset: win._regionInt(barEdges.includes("right") ? SettingsData.frameBarSize : SettingsData.frameThickness) + readonly property int cutoutWidth: Math.max(0, win._windowRegionWidth - win.cutoutLeftInset - win.cutoutRightInset) + readonly property int cutoutHeight: Math.max(0, win._windowRegionHeight - win.cutoutTopInset - win.cutoutBottomInset) + readonly property int cutoutRadius: { + const requested = win._regionInt(SettingsData.frameRounding); + const maxRadius = Math.floor(Math.min(win.cutoutWidth, win.cutoutHeight) / 2); + return Math.max(0, Math.min(requested, maxRadius)); + } + + // Slightly expand the subtractive blur cutout at very low opacity levels + readonly property int _blurCutoutCompensation: SettingsData.frameOpacity <= 0.2 ? 1 : 0 + readonly property int _blurCutoutLeft: Math.max(0, win.cutoutLeftInset - win._blurCutoutCompensation) + readonly property int _blurCutoutTop: Math.max(0, win.cutoutTopInset - win._blurCutoutCompensation) + readonly property int _blurCutoutRight: Math.min(win._windowRegionWidth, win._windowRegionWidth - win.cutoutRightInset + win._blurCutoutCompensation) + readonly property int _blurCutoutBottom: Math.min(win._windowRegionHeight, win._windowRegionHeight - win.cutoutBottomInset + win._blurCutoutCompensation) + readonly property int _blurCutoutRadius: { + const requested = win.cutoutRadius + win._blurCutoutCompensation; + const maxRadius = Math.floor(Math.min(_blurCutout.width, _blurCutout.height) / 2); + return Math.max(0, Math.min(requested, maxRadius)); + } + + // Must stay visible so Region.item can resolve scene coordinates. + Item { + id: _blurCutout + x: win._blurCutoutLeft + y: win._blurCutoutTop + width: Math.max(0, win._blurCutoutRight - win._blurCutoutLeft) + height: Math.max(0, win._blurCutoutBottom - win._blurCutoutTop) + } + + property var _frameBlurRegion: null + + function _buildBlur() { + _teardownBlur(); + // Follow the global blur toggle + if (!BlurService.enabled || !SettingsData.frameBlurEnabled || !win._frameActive || !win.visible) + return; + try { + const region = Qt.createQmlObject( + 'import QtQuick; import Quickshell; Region {' + + ' property Item cutoutItem;' + + ' property int cutoutRadius: 0;' + + ' Region {' + + ' item: cutoutItem;' + + ' intersection: Intersection.Subtract;' + + ' radius: cutoutRadius;' + + ' }' + + '}', + win, "FrameBlurRegion"); + + region.x = Qt.binding(() => 0); + region.y = Qt.binding(() => 0); + region.width = Qt.binding(() => win._windowRegionWidth); + region.height = Qt.binding(() => win._windowRegionHeight); + region.cutoutItem = _blurCutout; + region.cutoutRadius = Qt.binding(() => win._blurCutoutRadius); + + win.BackgroundEffect.blurRegion = region; + win._frameBlurRegion = region; + } catch (e) { + console.warn("FrameWindow: Failed to create blur region:", e); + } + } + + function _teardownBlur() { + if (!win._frameBlurRegion) + return; + try { + win.BackgroundEffect.blurRegion = null; + } catch (e) {} + win._frameBlurRegion.destroy(); + win._frameBlurRegion = null; + } + + Timer { + id: _blurRebuildTimer + interval: 1 + onTriggered: win._buildBlur() + } + + Connections { + target: SettingsData + function onFrameBlurEnabledChanged() { _blurRebuildTimer.restart(); } + function onFrameEnabledChanged() { _blurRebuildTimer.restart(); } + function onFrameThicknessChanged() { _blurRebuildTimer.restart(); } + function onFrameBarSizeChanged() { _blurRebuildTimer.restart(); } + function onFrameOpacityChanged() { _blurRebuildTimer.restart(); } + function onFrameRoundingChanged() { _blurRebuildTimer.restart(); } + function onFrameScreenPreferencesChanged() { _blurRebuildTimer.restart(); } + function onBarConfigsChanged() { _blurRebuildTimer.restart(); } + } + + Connections { + target: BlurService + function onEnabledChanged() { _blurRebuildTimer.restart(); } + } + + onVisibleChanged: { + if (visible) { + win._frameBlurRegion = null; + _blurRebuildTimer.restart(); + } else { + _teardownBlur(); + } + } + + Component.onCompleted: Qt.callLater(() => win._buildBlur()) + Component.onDestruction: win._teardownBlur() + FrameBorder { anchors.fill: parent - visible: SettingsData.frameEnabled && SettingsData.isScreenInPreferences(win.screen, SettingsData.frameScreenPreferences) - barEdges: { SettingsData.barConfigs; return SettingsData.getActiveBarEdgesForScreen(win.screen); } + visible: win._frameActive + cutoutTopInset: win.cutoutTopInset + cutoutBottomInset: win.cutoutBottomInset + cutoutLeftInset: win.cutoutLeftInset + cutoutRightInset: win.cutoutRightInset + cutoutRadius: win.cutoutRadius } } diff --git a/quickshell/Modules/Settings/FrameTab.qml b/quickshell/Modules/Settings/FrameTab.qml index 58396c15..59ea0487 100644 --- a/quickshell/Modules/Settings/FrameTab.qml +++ b/quickshell/Modules/Settings/FrameTab.qml @@ -55,7 +55,7 @@ Item { id: roundingSlider settingKey: "frameRounding" tags: ["frame", "border", "rounding", "radius", "corner"] - text: I18n.tr("Border rounding") + text: I18n.tr("Border Radius") unit: "px" minimum: 0 maximum: 100 @@ -75,7 +75,7 @@ Item { id: thicknessSlider settingKey: "frameThickness" tags: ["frame", "border", "thickness", "size", "width"] - text: I18n.tr("Border thickness") + text: I18n.tr("Border Width") unit: "px" minimum: 2 maximum: 100 @@ -93,22 +93,22 @@ Item { SettingsSliderRow { id: barThicknessSlider - settingKey: "frameBarThickness" + settingKey: "frameBarSize" tags: ["frame", "bar", "thickness", "size", "height", "width"] - text: I18n.tr("Bar-edge thickness") + text: I18n.tr("Size") description: I18n.tr("Height of horizontal bars / width of vertical bars in frame mode") unit: "px" minimum: 24 maximum: 100 step: 1 - defaultValue: 42 - value: SettingsData.frameBarThickness - onSliderDragFinished: v => SettingsData.set("frameBarThickness", v) + defaultValue: 40 + value: SettingsData.frameBarSize + onSliderDragFinished: v => SettingsData.set("frameBarSize", v) Binding { target: barThicknessSlider property: "value" - value: SettingsData.frameBarThickness + value: SettingsData.frameBarSize } } @@ -131,6 +131,50 @@ Item { } } + SettingsToggleRow { + id: frameBlurToggle + settingKey: "frameBlurEnabled" + tags: ["frame", "blur", "background", "glass", "transparency", "frosted"] + text: I18n.tr("Frame Blur") + description: !BlurService.available + ? I18n.tr("Requires a newer version of Quickshell") + : I18n.tr("Apply compositor blur behind the frame border") + checked: SettingsData.frameBlurEnabled + onToggled: checked => SettingsData.set("frameBlurEnabled", checked) + enabled: BlurService.available && SettingsData.blurEnabled + opacity: enabled ? 1.0 : 0.5 + visible: BlurService.available + } + + Item { + visible: BlurService.available && !SettingsData.blurEnabled + width: parent.width + height: blurToggleNote.height + Theme.spacingM * 2 + + Row { + id: blurToggleNote + x: Theme.spacingM + width: parent.width - Theme.spacingM * 2 + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: "blur_on" + size: Theme.fontSizeMedium + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Frame Blur is controlled by Background Blur in Theme & Colors") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width - Theme.fontSizeMedium - Theme.spacingS + } + } + } + // Color mode buttons SettingsButtonGroupRow { settingKey: "frameColor" @@ -217,17 +261,9 @@ Item { title: I18n.tr("Bar Integration") settingKey: "frameBarIntegration" collapsible: true + expanded: false visible: SettingsData.frameEnabled - SettingsToggleRow { - settingKey: "frameSyncBarColor" - tags: ["frame", "bar", "sync", "color", "background"] - text: I18n.tr("Sync bar background to frame") - description: I18n.tr("Sets the bar background color to match the frame border color for a seamless look") - checked: SettingsData.frameSyncBarColor - onToggled: checked => SettingsData.set("frameSyncBarColor", checked) - } - SettingsToggleRow { visible: CompositorService.isNiri settingKey: "frameShowOnOverview" @@ -246,6 +282,7 @@ Item { title: I18n.tr("Display Assignment") settingKey: "frameDisplays" collapsible: true + expanded: false visible: SettingsData.frameEnabled SettingsDisplayPicker {