From eaf350482d2fe640c642d35a86b0189bf30f4212 Mon Sep 17 00:00:00 2001 From: purian23 Date: Fri, 3 Apr 2026 00:44:29 -0400 Subject: [PATCH] (frameInMotion): Initial Unified Frame Connected Mode --- quickshell/Common/AnimVariants.qml | 7 +- quickshell/Common/ElevationShadow.qml | 11 +- quickshell/Common/SettingsData.qml | 14 +- quickshell/Common/Theme.qml | 28 +- quickshell/Common/settings/SettingsSpec.js | 1 + quickshell/Modals/Common/DankModal.qml | 45 ++- .../DankLauncherV2/DankLauncherV2Modal.qml | 34 +- quickshell/Modules/Dock/Dock.qml | 183 ++++++++--- quickshell/Modules/Settings/DockTab.qml | 40 +++ quickshell/Modules/Settings/FrameTab.qml | 27 +- .../Modules/Settings/ThemeColorsTab.qml | 15 +- .../Modules/Settings/TypographyMotionTab.qml | 20 +- quickshell/Widgets/ConnectedCorner.qml | 152 +++++++++ quickshell/Widgets/DankPopout.qml | 296 ++++++++++++++++-- quickshell/Widgets/WindowBlur.qml | 6 +- 15 files changed, 778 insertions(+), 101 deletions(-) create mode 100644 quickshell/Widgets/ConnectedCorner.qml diff --git a/quickshell/Common/AnimVariants.qml b/quickshell/Common/AnimVariants.qml index 3b9f8295..25c9edd7 100644 --- a/quickshell/Common/AnimVariants.qml +++ b/quickshell/Common/AnimVariants.qml @@ -143,8 +143,13 @@ Singleton { return variantDuration(baseDuration, false) + variantExitCleanupPadding(); } - readonly property bool isDirectionalEffect: typeof SettingsData !== "undefined" && SettingsData.motionEffect === 1 + readonly property bool isDirectionalEffect: isConnectedEffect + || (typeof SettingsData !== "undefined" && SettingsData.motionEffect === 1) readonly property bool isDepthEffect: typeof SettingsData !== "undefined" && SettingsData.motionEffect === 2 + readonly property bool isConnectedEffect: typeof SettingsData !== "undefined" + && SettingsData.frameEnabled + && SettingsData.motionEffect === 1 + && SettingsData.directionalAnimationMode === 3 readonly property real effectScaleCollapsed: { if (typeof SettingsData === "undefined") diff --git a/quickshell/Common/ElevationShadow.qml b/quickshell/Common/ElevationShadow.qml index dab80cdb..1ccf6b1c 100644 --- a/quickshell/Common/ElevationShadow.qml +++ b/quickshell/Common/ElevationShadow.qml @@ -13,8 +13,13 @@ Item { property color targetColor: "white" property real targetRadius: Theme.cornerRadius + property real topLeftRadius: targetRadius + property real topRightRadius: targetRadius + property real bottomLeftRadius: targetRadius + property real bottomRightRadius: targetRadius property color borderColor: "transparent" property real borderWidth: 0 + property bool useCustomSource: false property bool shadowEnabled: Theme.elevationEnabled property real shadowBlurPx: level && level.blurPx !== undefined ? level.blurPx : 0 @@ -46,7 +51,11 @@ Item { Rectangle { id: sourceRect anchors.fill: parent - radius: root.targetRadius + visible: !root.useCustomSource + topLeftRadius: root.topLeftRadius + topRightRadius: root.topRightRadius + bottomLeftRadius: root.bottomLeftRadius + bottomRightRadius: root.bottomRightRadius color: root.targetColor border.color: root.borderColor border.width: root.borderWidth diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index b439c821..b8cbcbb7 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -235,6 +235,8 @@ Singleton { onFrameShowOnOverviewChanged: saveSettings() property bool frameBlurEnabled: true onFrameBlurEnabledChanged: saveSettings() + property int previousDirectionalMode: 1 + onPreviousDirectionalModeChanged: saveSettings() readonly property color effectiveFrameColor: { const fc = frameColor; @@ -1590,34 +1592,36 @@ Singleton { const position = barPosition !== undefined ? barPosition : (defaultBar?.position ?? SettingsData.Position.Top); const rawBottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : (defaultBar?.bottomGap ?? 0)) : (defaultBar?.bottomGap ?? 0); const bottomGap = Math.max(0, rawBottomGap); + const isConnected = frameEnabled && motionEffect === 1 && directionalAnimationMode === 3; const useAutoGaps = (barConfig && barConfig.popupGapsAuto !== undefined) ? barConfig.popupGapsAuto : (defaultBar?.popupGapsAuto ?? true); const manualGapValue = (barConfig && barConfig.popupGapsManual !== undefined) ? barConfig.popupGapsManual : (defaultBar?.popupGapsManual ?? 4); - const popupGap = useAutoGaps ? Math.max(4, spacing) : manualGapValue; + const popupGap = isConnected ? 0 : (useAutoGaps ? Math.max(4, spacing) : manualGapValue); + const edgeSpacing = isConnected ? 0 : spacing; switch (position) { case SettingsData.Position.Left: return { - "x": barThickness + spacing + popupGap, + "x": barThickness + edgeSpacing + popupGap, "y": relativeY, "width": widgetWidth }; case SettingsData.Position.Right: return { - "x": (screen?.width || 0) - (barThickness + spacing + popupGap), + "x": (screen?.width || 0) - (barThickness + edgeSpacing + popupGap), "y": relativeY, "width": widgetWidth }; case SettingsData.Position.Bottom: return { "x": relativeX, - "y": (screen?.height || 0) - (barThickness + spacing + bottomGap + popupGap), + "y": (screen?.height || 0) - (barThickness + edgeSpacing + bottomGap + popupGap), "width": widgetWidth }; default: return { "x": relativeX, - "y": barThickness + spacing + bottomGap + popupGap, + "y": barThickness + edgeSpacing + bottomGap + popupGap, "width": widgetWidth }; } diff --git a/quickshell/Common/Theme.qml b/quickshell/Common/Theme.qml index 3442204e..32fcd7ac 100644 --- a/quickshell/Common/Theme.qml +++ b/quickshell/Common/Theme.qml @@ -972,6 +972,22 @@ Singleton { readonly property real variantOpacityDurationScale: AnimVariants.variantOpacityDurationScale readonly property bool isDirectionalEffect: AnimVariants.isDirectionalEffect readonly property bool isDepthEffect: AnimVariants.isDepthEffect + readonly property bool isConnectedEffect: AnimVariants.isConnectedEffect + readonly property real connectedCornerRadius: { + if (typeof SettingsData === "undefined") return 12; + return SettingsData.frameEnabled ? SettingsData.frameRounding : cornerRadius; + } + readonly property color connectedSurfaceColor: { + if (typeof SettingsData === "undefined") + return withAlpha(surfaceContainer, popupTransparency); + return isConnectedEffect + ? Qt.rgba(SettingsData.effectiveFrameColor.r, SettingsData.effectiveFrameColor.g, SettingsData.effectiveFrameColor.b, SettingsData.frameOpacity) + : withAlpha(surfaceContainer, popupTransparency); + } + readonly property real connectedSurfaceRadius: isConnectedEffect ? connectedCornerRadius : cornerRadius + readonly property bool connectedSurfaceBlurEnabled: (typeof SettingsData === "undefined") + ? true + : (!isConnectedEffect || SettingsData.frameBlurEnabled) readonly property real effectScaleCollapsed: AnimVariants.effectScaleCollapsed readonly property real effectAnimOffset: AnimVariants.effectAnimOffset function variantDuration(baseDuration, entering) { return AnimVariants.variantDuration(baseDuration, entering); } @@ -1143,7 +1159,11 @@ Singleton { property real iconSizeLarge: 32 property real panelTransparency: 0.85 - property real popupTransparency: typeof SettingsData !== "undefined" && SettingsData.popupTransparency !== undefined ? SettingsData.popupTransparency : 1.0 + property real popupTransparency: { + if (typeof SettingsData === "undefined") + return 1.0; + return SettingsData.popupTransparency !== undefined ? SettingsData.popupTransparency : 1.0; + } function screenTransition() { if (CompositorService.isNiri) { @@ -1842,6 +1862,12 @@ Singleton { return Qt.rgba(c.r, c.g, c.b, a); } + function popupLayerColor(baseColor) { + if (isConnectedEffect) + return connectedSurfaceColor; + return withAlpha(baseColor, popupTransparency); + } + function blendAlpha(c, a) { return Qt.rgba(c.r, c.g, c.b, c.a * a); } diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index 8e51e04e..a0ad69c4 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -52,6 +52,7 @@ var SPEC = { animationVariant: { def: 0 }, motionEffect: { def: 0 }, directionalAnimationMode: { def: 0 }, + previousDirectionalMode: { def: 1 }, m3ElevationEnabled: { def: true }, m3ElevationIntensity: { def: 12 }, m3ElevationOpacity: { def: 30 }, diff --git a/quickshell/Modals/Common/DankModal.qml b/quickshell/Modals/Common/DankModal.qml index edb4413e..adab2fbd 100644 --- a/quickshell/Modals/Common/DankModal.qml +++ b/quickshell/Modals/Common/DankModal.qml @@ -3,6 +3,7 @@ import Quickshell import Quickshell.Wayland import qs.Common import qs.Services +import qs.Widgets Item { id: root @@ -34,6 +35,12 @@ Item { property color borderColor: Theme.outlineMedium property real borderWidth: 0 property real cornerRadius: Theme.cornerRadius + readonly property bool connectedSurfaceOverride: Theme.isConnectedEffect + readonly property color effectiveBackgroundColor: connectedSurfaceOverride ? Theme.connectedSurfaceColor : backgroundColor + readonly property color effectiveBorderColor: connectedSurfaceOverride ? "transparent" : borderColor + readonly property real effectiveBorderWidth: connectedSurfaceOverride ? 0 : borderWidth + readonly property real effectiveCornerRadius: connectedSurfaceOverride ? Theme.connectedSurfaceRadius : cornerRadius + readonly property bool effectiveBlurEnabled: Theme.connectedSurfaceBlurEnabled property bool enableShadow: true property alias modalFocusScope: focusScope property bool shouldBeVisible: false @@ -163,6 +170,8 @@ Item { 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: { + if (Theme.isConnectedEffect) + return 0; if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0 && Theme.isDirectionalEffect) return 0; // Wayland native overlap mask if (animationType === "slide") @@ -244,7 +253,7 @@ Item { visible: opacity > 0 Behavior on opacity { - enabled: root.animationsEnabled && !Theme.isDirectionalEffect + enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect) NumberAnimation { duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale) easing.type: Easing.BezierSpline @@ -259,6 +268,17 @@ Item { visible: false color: "transparent" + WindowBlur { + targetWindow: contentWindow + blurEnabled: root.effectiveBlurEnabled + readonly property real s: Math.min(1, modalContainer.scaleValue) + blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5 + Theme.snap(modalContainer.animX, root.dpr) + blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5 + Theme.snap(modalContainer.animY, root.dpr) + blurWidth: (root.shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.width * s : 0 + blurHeight: (root.shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.height * s : 0 + blurRadius: root.effectiveCornerRadius + } + WlrLayershell.namespace: root.layerNamespace WlrLayershell.layer: { if (root.useOverlayLayer) @@ -333,7 +353,7 @@ Item { visible: opacity > 0 Behavior on opacity { - enabled: root.animationsEnabled && !Theme.isDirectionalEffect + enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect) NumberAnimation { duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale) easing.type: Easing.BezierSpline @@ -487,12 +507,12 @@ Item { id: animatedContent anchors.fill: parent clip: false - opacity: Theme.isDirectionalEffect ? 1 : (root.shouldBeVisible ? 1 : 0) + opacity: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) ? 1 : (root.shouldBeVisible ? 1 : 0) scale: modalContainer.scaleValue transformOrigin: Item.Center Behavior on opacity { - enabled: root.animationsEnabled && !Theme.isDirectionalEffect + enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect) NumberAnimation { duration: Math.round(Theme.variantDuration(animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale) easing.type: Easing.BezierSpline @@ -505,13 +525,22 @@ Item { anchors.fill: parent level: root.shadowLevel fallbackOffset: root.shadowFallbackOffset - targetRadius: root.cornerRadius - targetColor: root.backgroundColor - borderColor: root.borderColor - borderWidth: root.borderWidth + targetRadius: root.effectiveCornerRadius + targetColor: root.effectiveBackgroundColor + borderColor: root.effectiveBorderColor + borderWidth: root.effectiveBorderWidth shadowEnabled: root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" } + Rectangle { + anchors.fill: parent + radius: root.effectiveCornerRadius + color: "transparent" + border.color: root.connectedSurfaceOverride ? "transparent" : BlurService.borderColor + border.width: root.connectedSurfaceOverride ? 0 : BlurService.borderWidth + z: 100 + } + FocusScope { anchors.fill: parent focus: root.shouldBeVisible diff --git a/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml b/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml index a400189d..df171985 100644 --- a/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml +++ b/quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml @@ -5,6 +5,7 @@ import Quickshell.Wayland import Quickshell.Hyprland import qs.Common import qs.Services +import qs.Widgets Item { id: root @@ -65,8 +66,9 @@ Item { readonly property real modalX: (screenWidth - modalWidth) / 2 readonly property real modalY: (screenHeight - modalHeight) / 2 - readonly property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) - readonly property real cornerRadius: Theme.cornerRadius + readonly property bool connectedSurfaceOverride: Theme.isConnectedEffect + readonly property color backgroundColor: connectedSurfaceOverride ? Theme.connectedSurfaceColor : Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) + readonly property real cornerRadius: connectedSurfaceOverride ? Theme.connectedSurfaceRadius : Theme.cornerRadius readonly property color borderColor: { if (!SettingsData.dankLauncherV2BorderEnabled) return Theme.outlineMedium; @@ -84,6 +86,9 @@ Item { } } readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0 + readonly property color effectiveBorderColor: connectedSurfaceOverride ? "transparent" : borderColor + readonly property int effectiveBorderWidth: connectedSurfaceOverride ? 0 : borderWidth + readonly property bool effectiveBlurEnabled: Theme.connectedSurfaceBlurEnabled // Shadow padding for the content window (render padding only, no motion padding) readonly property var shadowLevel: Theme.elevationLevel3 @@ -97,13 +102,13 @@ Item { // For directional/depth: window extends from screen top (content slides within) // For standard: small window tightly around the modal + shadow padding - readonly property bool _needsExtendedWindow: Theme.isDirectionalEffect || Theme.isDepthEffect + readonly property bool _needsExtendedWindow: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) || Theme.isDepthEffect // Content window geometry readonly property real _cwMarginLeft: Theme.snap(alignedX - shadowPad, dpr) readonly property real _cwMarginTop: _needsExtendedWindow ? 0 : Theme.snap(alignedY - shadowPad, dpr) readonly property real _cwWidth: alignedWidth + shadowPad * 2 readonly property real _cwHeight: { - if (Theme.isDirectionalEffect) + if (Theme.isDirectionalEffect && !Theme.isConnectedEffect) return screenHeight + shadowPad; if (Theme.isDepthEffect) return alignedY + alignedHeight + shadowPad; @@ -387,7 +392,7 @@ Item { visible: launcherMotionVisible || opacity > 0 Behavior on opacity { - enabled: root.animationsEnabled && !Theme.isDirectionalEffect + enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect) DankAnim { duration: Math.round(Theme.variantDuration(Theme.modalAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale) easing.bezierCurve: launcherMotionVisible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve @@ -408,6 +413,17 @@ Item { visible: false color: "transparent" + WindowBlur { + targetWindow: contentWindow + blurEnabled: root.effectiveBlurEnabled + readonly property real s: Math.min(1, contentContainer.scaleValue) + blurX: root._ccX + root.alignedWidth * (1 - s) * 0.5 + Theme.snap(contentContainer.animX, root.dpr) + blurY: root._ccY + root.alignedHeight * (1 - s) * 0.5 + Theme.snap(contentContainer.animY, root.dpr) + blurWidth: (root.spotlightOpen || root.isClosing) && contentWrapper.opacity > 0 ? root.alignedWidth * s : 0 + blurHeight: (root.spotlightOpen || root.isClosing) && contentWrapper.opacity > 0 ? root.alignedHeight * s : 0 + blurRadius: root.cornerRadius + } + WlrLayershell.namespace: "dms:spotlight" WlrLayershell.layer: { switch (Quickshell.env("DMS_MODAL_LAYER")) { @@ -572,8 +588,8 @@ Item { level: root.shadowLevel fallbackOffset: root.shadowFallbackOffset targetColor: root.backgroundColor - borderColor: root.borderColor - borderWidth: root.borderWidth + borderColor: root.effectiveBorderColor + borderWidth: root.effectiveBorderWidth targetRadius: root.cornerRadius shadowEnabled: Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" } @@ -583,14 +599,14 @@ Item { id: contentWrapper width: parent.width height: parent.height - opacity: Theme.isDirectionalEffect ? 1 : (launcherMotionVisible ? 1 : 0) + opacity: (Theme.isDirectionalEffect && !Theme.isConnectedEffect) ? 1 : (launcherMotionVisible ? 1 : 0) visible: opacity > 0 scale: contentContainer.scaleValue x: Theme.snap(contentContainer.animX + (parent.width - width) * (1 - contentContainer.scaleValue) * 0.5, root.dpr) y: Theme.snap(contentContainer.animY + (parent.height - height) * (1 - contentContainer.scaleValue) * 0.5, root.dpr) Behavior on opacity { - enabled: root.animationsEnabled && !Theme.isDirectionalEffect + enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || Theme.isConnectedEffect) DankAnim { duration: Math.round(Theme.variantDuration(Theme.modalAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale) easing.bezierCurve: launcherMotionVisible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve diff --git a/quickshell/Modules/Dock/Dock.qml b/quickshell/Modules/Dock/Dock.qml index 136f23ad..13804b2e 100644 --- a/quickshell/Modules/Dock/Dock.qml +++ b/quickshell/Modules/Dock/Dock.qml @@ -19,11 +19,12 @@ Variants { WindowBlur { targetWindow: dock - blurX: dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x + dockSlide.x - blurY: dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y + dockSlide.y - blurWidth: dock.hasApps && dock.reveal ? dockBackground.width : 0 - blurHeight: dock.hasApps && dock.reveal ? dockBackground.height : 0 - blurRadius: Theme.cornerRadius + blurEnabled: dock.effectiveBlurEnabled + blurX: dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x + dockSlide.x - dock.horizontalConnectorExtent + blurY: dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y + dockSlide.y - dock.verticalConnectorExtent + blurWidth: dock.hasApps && dock.reveal ? dockBackground.width + dock.horizontalConnectorExtent * 2 : 0 + blurHeight: dock.hasApps && dock.reveal ? dockBackground.height + dock.verticalConnectorExtent * 2 : 0 + blurRadius: dock.surfaceRadius } WlrLayershell.namespace: "dms:dock" @@ -42,6 +43,29 @@ Variants { property real backgroundTransparency: SettingsData.dockTransparency property bool groupByApp: SettingsData.dockGroupByApp readonly property int borderThickness: SettingsData.dockBorderEnabled ? SettingsData.dockBorderThickness : 0 + readonly property string connectedBarSide: SettingsData.dockPosition === SettingsData.Position.Top ? "top" + : SettingsData.dockPosition === SettingsData.Position.Bottom ? "bottom" + : SettingsData.dockPosition === SettingsData.Position.Left ? "left" : "right" + readonly property bool connectedBarActiveOnEdge: Theme.isConnectedEffect + && !!(dock.screen || modelData) + && SettingsData.getActiveBarEdgesForScreen(dock.screen || modelData).includes(connectedBarSide) + readonly property real connectedJoinInset: { + if (!Theme.isConnectedEffect) + return 0; + return connectedBarActiveOnEdge ? SettingsData.frameBarSize : SettingsData.frameThickness; + } + readonly property real surfaceRadius: Theme.connectedSurfaceRadius + readonly property color surfaceColor: Theme.isConnectedEffect + ? Theme.connectedSurfaceColor + : Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency) + readonly property color surfaceBorderColor: Theme.isConnectedEffect ? "transparent" : BlurService.borderColor + readonly property real surfaceBorderWidth: Theme.isConnectedEffect ? 0 : BlurService.borderWidth + readonly property real surfaceTopLeftRadius: Theme.isConnectedEffect && (SettingsData.dockPosition === SettingsData.Position.Top || SettingsData.dockPosition === SettingsData.Position.Left) ? 0 : surfaceRadius + readonly property real surfaceTopRightRadius: Theme.isConnectedEffect && (SettingsData.dockPosition === SettingsData.Position.Top || SettingsData.dockPosition === SettingsData.Position.Right) ? 0 : surfaceRadius + readonly property real surfaceBottomLeftRadius: Theme.isConnectedEffect && (SettingsData.dockPosition === SettingsData.Position.Bottom || SettingsData.dockPosition === SettingsData.Position.Left) ? 0 : surfaceRadius + readonly property real surfaceBottomRightRadius: Theme.isConnectedEffect && (SettingsData.dockPosition === SettingsData.Position.Bottom || SettingsData.dockPosition === SettingsData.Position.Right) ? 0 : surfaceRadius + readonly property real horizontalConnectorExtent: Theme.isConnectedEffect && !isVertical ? Theme.connectedCornerRadius : 0 + readonly property real verticalConnectorExtent: Theme.isConnectedEffect && isVertical ? Theme.connectedCornerRadius : 0 readonly property int hasApps: dockApps.implicitWidth > 0 || dockApps.implicitHeight > 0 @@ -113,13 +137,57 @@ Variants { return getBarHeight(leftBar); } - readonly property real dockMargin: SettingsData.dockSpacing - readonly property real positionSpacing: barSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + readonly property real dockMargin: SettingsData.dockMargin + readonly property bool effectiveBlurEnabled: Theme.connectedSurfaceBlurEnabled + readonly property real effectiveDockBottomGap: Theme.isConnectedEffect ? 0 : SettingsData.dockBottomGap + readonly property real effectiveDockMargin: Theme.isConnectedEffect ? 0 : SettingsData.dockMargin + readonly property real positionSpacing: barSpacing + effectiveDockBottomGap + effectiveDockMargin + readonly property real joinedEdgeMargin: Theme.isConnectedEffect ? 0 : (barSpacing + effectiveDockMargin + 1 + dock.borderThickness) readonly property real _dpr: (dock.screen && dock.screen.devicePixelRatio) ? dock.screen.devicePixelRatio : 1 function px(v) { return Math.round(v * _dpr) / _dpr; } + function connectorWidth(spacing) { + return dock.isVertical ? (spacing + Theme.connectedCornerRadius) : Theme.connectedCornerRadius; + } + + function connectorHeight(spacing) { + return dock.isVertical ? Theme.connectedCornerRadius : (spacing + Theme.connectedCornerRadius); + } + + function connectorSeamX(baseX, bodyWidth, placement) { + if (!dock.isVertical) + return placement === "left" ? baseX : baseX + bodyWidth; + return SettingsData.dockPosition === SettingsData.Position.Left ? baseX : baseX + bodyWidth; + } + + function connectorSeamY(baseY, bodyHeight, placement) { + if (SettingsData.dockPosition === SettingsData.Position.Top) + return baseY; + if (SettingsData.dockPosition === SettingsData.Position.Bottom) + return baseY + bodyHeight; + return placement === "left" ? baseY : baseY + bodyHeight; + } + + function connectorX(baseX, bodyWidth, placement, spacing) { + const seamX = connectorSeamX(baseX, bodyWidth, placement); + const width = connectorWidth(spacing); + if (!dock.isVertical) + return placement === "left" ? seamX - width : seamX; + return SettingsData.dockPosition === SettingsData.Position.Left ? seamX : seamX - width; + } + + function connectorY(baseY, bodyHeight, placement, spacing) { + const seamY = connectorSeamY(baseY, bodyHeight, placement); + const height = connectorHeight(spacing); + if (SettingsData.dockPosition === SettingsData.Position.Top) + return seamY; + if (SettingsData.dockPosition === SettingsData.Position.Bottom) + return seamY - height; + return placement === "left" ? seamY - height : seamY; + } + property bool contextMenuOpen: (dockVariants.contextMenu && dockVariants.contextMenu.visible && dockVariants.contextMenu.screen === modelData) property bool revealSticky: false @@ -130,7 +198,7 @@ Variants { return false; const screenName = dock.modelData?.name ?? ""; - const dockThickness = effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin; + const dockThickness = dock.connectedJoinInset + effectiveBarHeight + SettingsData.dockSpacing + dock.effectiveDockBottomGap + dock.effectiveDockMargin; const screenWidth = dock.screen?.width ?? 0; const screenHeight = dock.screen?.height ?? 0; @@ -302,13 +370,13 @@ Variants { return -1; if (barSpacing > 0) return -1; - return px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin); + return px(connectedJoinInset + effectiveBarHeight + SettingsData.dockSpacing + effectiveDockBottomGap + effectiveDockMargin); } property real animationHeadroom: Math.ceil(SettingsData.dockIconSize * 0.35) - implicitWidth: isVertical ? (px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0 - implicitHeight: !isVertical ? (px(effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0 + implicitWidth: isVertical ? (px(connectedJoinInset + effectiveBarHeight + SettingsData.dockSpacing + effectiveDockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0 + implicitHeight: !isVertical ? (px(connectedJoinInset + effectiveBarHeight + SettingsData.dockSpacing + effectiveDockMargin + SettingsData.dockIconSize * 0.3) + animationHeadroom) : 0 Item { id: maskItem @@ -318,17 +386,17 @@ Variants { x: { const baseX = dockCore.x + dockMouseArea.x; if (isVertical && SettingsData.dockPosition === SettingsData.Position.Right) - return baseX - (expanded ? animationHeadroom + borderThickness : 0); - return baseX - (expanded ? borderThickness : 0); + return baseX - (expanded ? animationHeadroom + borderThickness + dock.horizontalConnectorExtent : 0); + return baseX - (expanded ? borderThickness + dock.horizontalConnectorExtent : 0); } y: { const baseY = dockCore.y + dockMouseArea.y; if (!isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom) - return baseY - (expanded ? animationHeadroom + borderThickness : 0); - return baseY - (expanded ? borderThickness : 0); + return baseY - (expanded ? animationHeadroom + borderThickness + dock.verticalConnectorExtent : 0); + return baseY - (expanded ? borderThickness + dock.verticalConnectorExtent : 0); } - width: dockMouseArea.width + (isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 : 0) - height: dockMouseArea.height + (!isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 : 0) + width: dockMouseArea.width + (isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 + dock.horizontalConnectorExtent * 2 : 0) + height: dockMouseArea.height + (!isVertical && expanded ? animationHeadroom : 0) + (expanded ? borderThickness * 2 + dock.verticalConnectorExtent * 2 : 0) } mask: Region { @@ -388,7 +456,7 @@ Variants { const screenHeight = dock.screen ? dock.screen.height : 0; const gap = Theme.spacingS; - const bgMargin = barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness; + const bgMargin = dock.joinedEdgeMargin + dock.connectedJoinInset; const btnW = dock.hoveredButton.width; const btnH = dock.hoveredButton.height; @@ -459,11 +527,11 @@ Variants { // Keep the taller hit area regardless of the reveal state to prevent shrinking loop return Math.min(Math.max(dockBackground.height + 64, 200), maxDockHeight); } - return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1; + return dock.reveal ? px(dock.connectedJoinInset + dock.effectiveBarHeight + SettingsData.dockSpacing + dock.effectiveDockBottomGap + dock.effectiveDockMargin) : 1; } width: { if (dock.isVertical) { - return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1; + return dock.reveal ? px(dock.connectedJoinInset + dock.effectiveBarHeight + SettingsData.dockSpacing + dock.effectiveDockBottomGap + dock.effectiveDockMargin) : 1; } // Keep the wider hit area regardless of the reveal state to prevent shrinking loop return Math.min(dockBackground.width + 8 + dock.borderThickness, maxDockWidth); @@ -505,7 +573,7 @@ Variants { return 0; if (dock.reveal) return 0; - const hideDistance = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + 10; + const hideDistance = dock.connectedJoinInset + dock.effectiveBarHeight + SettingsData.dockSpacing + dock.effectiveDockBottomGap + dock.effectiveDockMargin + 10; if (SettingsData.dockPosition === SettingsData.Position.Right) { return hideDistance; } else { @@ -517,7 +585,7 @@ Variants { return 0; if (dock.reveal) return 0; - const hideDistance = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + 10; + const hideDistance = dock.connectedJoinInset + dock.effectiveBarHeight + SettingsData.dockSpacing + dock.effectiveDockBottomGap + dock.effectiveDockMargin + 10; if (SettingsData.dockPosition === SettingsData.Position.Bottom) { return hideDistance; } else { @@ -528,16 +596,26 @@ Variants { Behavior on x { NumberAnimation { id: slideXAnimation - duration: Theme.shortDuration - easing.type: Easing.OutCubic + duration: Theme.isConnectedEffect + ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) + : Theme.shortDuration + easing.type: Theme.isConnectedEffect ? Easing.BezierSpline : Easing.OutCubic + easing.bezierCurve: Theme.isConnectedEffect + ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) + : [] } } Behavior on y { NumberAnimation { id: slideYAnimation - duration: Theme.shortDuration - easing.type: Easing.OutCubic + duration: Theme.isConnectedEffect + ? Theme.variantDuration(Theme.popoutAnimationDuration, dock.reveal) + : Theme.shortDuration + easing.type: Theme.isConnectedEffect ? Easing.BezierSpline : Easing.OutCubic + easing.bezierCurve: Theme.isConnectedEffect + ? (dock.reveal ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) + : [] } } } @@ -553,47 +631,76 @@ Variants { right: dock.isVertical ? (SettingsData.dockPosition === SettingsData.Position.Right ? parent.right : undefined) : undefined verticalCenter: dock.isVertical ? parent.verticalCenter : undefined } - anchors.topMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Top ? barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness : 0 - anchors.bottomMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness : 0 - anchors.leftMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness : 0 - anchors.rightMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness : 0 + anchors.topMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Top ? (dock.connectedJoinInset + dock.joinedEdgeMargin) : 0 + anchors.bottomMargin: !dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Bottom ? (dock.connectedJoinInset + dock.joinedEdgeMargin) : 0 + anchors.leftMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? (dock.connectedJoinInset + dock.joinedEdgeMargin) : 0 + anchors.rightMargin: dock.isVertical && SettingsData.dockPosition === SettingsData.Position.Right ? (dock.connectedJoinInset + dock.joinedEdgeMargin) : 0 implicitWidth: dock.isVertical ? (dockApps.implicitHeight + SettingsData.dockSpacing * 2) : (dockApps.implicitWidth + SettingsData.dockSpacing * 2) implicitHeight: dock.isVertical ? (dockApps.implicitWidth + SettingsData.dockSpacing * 2) : (dockApps.implicitHeight + SettingsData.dockSpacing * 2) width: implicitWidth height: implicitHeight - layer.enabled: true + // Avoid an offscreen texture seam where the connected dock meets the frame. + layer.enabled: !Theme.isConnectedEffect clip: false Rectangle { anchors.fill: parent - color: Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency) - radius: Theme.cornerRadius + color: dock.surfaceColor + topLeftRadius: dock.surfaceTopLeftRadius + topRightRadius: dock.surfaceTopRightRadius + bottomLeftRadius: dock.surfaceBottomLeftRadius + bottomRightRadius: dock.surfaceBottomRightRadius } Rectangle { anchors.fill: parent color: "transparent" - radius: Theme.cornerRadius - border.color: BlurService.borderColor - border.width: BlurService.borderWidth + topLeftRadius: dock.surfaceTopLeftRadius + topRightRadius: dock.surfaceTopRightRadius + bottomLeftRadius: dock.surfaceBottomLeftRadius + bottomRightRadius: dock.surfaceBottomRightRadius + border.color: dock.surfaceBorderColor + border.width: dock.surfaceBorderWidth z: 100 } } + ConnectedCorner { + visible: Theme.isConnectedEffect && dock.reveal + barSide: dock.connectedBarSide + placement: "left" + spacing: 0 + connectorRadius: Theme.connectedCornerRadius + color: dock.surfaceColor + x: Theme.snap(dock.connectorX(dockBackground.x, dockBackground.width, placement, spacing), dock._dpr) + y: Theme.snap(dock.connectorY(dockBackground.y, dockBackground.height, placement, spacing), dock._dpr) + } + + ConnectedCorner { + visible: Theme.isConnectedEffect && dock.reveal + barSide: dock.connectedBarSide + placement: "right" + spacing: 0 + connectorRadius: Theme.connectedCornerRadius + color: dock.surfaceColor + x: Theme.snap(dock.connectorX(dockBackground.x, dockBackground.width, placement, spacing), dock._dpr) + y: Theme.snap(dock.connectorY(dockBackground.y, dockBackground.height, placement, spacing), dock._dpr) + } + Shape { id: dockBorderShape x: dockBackground.x - borderThickness y: dockBackground.y - borderThickness width: dockBackground.width + borderThickness * 2 height: dockBackground.height + borderThickness * 2 - visible: SettingsData.dockBorderEnabled && dock.hasApps + visible: SettingsData.dockBorderEnabled && dock.hasApps && !Theme.isConnectedEffect preferredRendererType: Shape.CurveRenderer readonly property real borderThickness: Math.max(1, dock.borderThickness) readonly property real i: borderThickness / 2 - readonly property real cr: Theme.cornerRadius + readonly property real cr: dock.surfaceRadius readonly property real w: dockBackground.width readonly property real h: dockBackground.height diff --git a/quickshell/Modules/Settings/DockTab.qml b/quickshell/Modules/Settings/DockTab.qml index 9b281a37..7be98194 100644 --- a/quickshell/Modules/Settings/DockTab.qml +++ b/quickshell/Modules/Settings/DockTab.qml @@ -7,6 +7,9 @@ import qs.Modules.Settings.Widgets Item { id: root + readonly property bool connectedFrameModeActive: SettingsData.frameEnabled + && SettingsData.motionEffect === 1 + && SettingsData.directionalAnimationMode === 3 FileBrowserModal { id: dockLogoFileBrowser @@ -544,6 +547,8 @@ Item { SettingsSliderRow { text: I18n.tr("Exclusive Zone Offset") + enabled: !root.connectedFrameModeActive + opacity: root.connectedFrameModeActive ? 0.5 : 1.0 value: SettingsData.dockBottomGap minimum: -100 maximum: 100 @@ -553,6 +558,8 @@ Item { SettingsSliderRow { text: I18n.tr("Margin") + enabled: !root.connectedFrameModeActive + opacity: root.connectedFrameModeActive ? 0.5 : 1.0 value: SettingsData.dockMargin minimum: 0 maximum: 100 @@ -561,11 +568,42 @@ Item { } } + Item { + visible: root.connectedFrameModeActive + width: parent.width + implicitHeight: dockConnectedNote.implicitHeight + Theme.spacingS * 2 + + Row { + id: dockConnectedNote + x: Theme.spacingM + width: parent.width - Theme.spacingM * 2 + anchors.verticalCenter: parent.verticalCenter + spacing: Theme.spacingS + + DankIcon { + name: "frame_source" + size: Theme.fontSizeMedium + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + StyledText { + text: I18n.tr("Connected Frame mode manages dock edge offset, transparency, blur, and border styling") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width - Theme.fontSizeMedium - Theme.spacingS + } + } + } + SettingsCard { width: parent.width iconName: "opacity" title: I18n.tr("Transparency") settingKey: "dockTransparency" + enabled: !root.connectedFrameModeActive + opacity: root.connectedFrameModeActive ? 0.5 : 1.0 SettingsSliderRow { text: I18n.tr("Dock Transparency") @@ -585,6 +623,8 @@ Item { settingKey: "dockBorder" collapsible: true expanded: false + enabled: !root.connectedFrameModeActive + opacity: root.connectedFrameModeActive ? 0.5 : 1.0 SettingsToggleRow { text: I18n.tr("Border") diff --git a/quickshell/Modules/Settings/FrameTab.qml b/quickshell/Modules/Settings/FrameTab.qml index 59ea0487..597ad6be 100644 --- a/quickshell/Modules/Settings/FrameTab.qml +++ b/quickshell/Modules/Settings/FrameTab.qml @@ -261,7 +261,7 @@ Item { title: I18n.tr("Bar Integration") settingKey: "frameBarIntegration" collapsible: true - expanded: false + expanded: true visible: SettingsData.frameEnabled SettingsToggleRow { @@ -273,6 +273,31 @@ Item { checked: SettingsData.frameShowOnOverview onToggled: checked => SettingsData.set("frameShowOnOverview", checked) } + + SettingsToggleRow { + visible: SettingsData.frameEnabled + settingKey: "directionalAnimationMode" + tags: ["frame", "connected", "popout", "corner", "animation"] + text: I18n.tr("Connected Mode") + description: I18n.tr("Popouts emerge flush from the bar edge as one continuous piece (based on Slide)") + checked: SettingsData.motionEffect === 1 && SettingsData.directionalAnimationMode === 3 + onToggled: checked => { + if (checked) { + if (SettingsData.directionalAnimationMode !== 3) + SettingsData.set("previousDirectionalMode", SettingsData.directionalAnimationMode); + SettingsData.set("motionEffect", 1); + SettingsData.set("directionalAnimationMode", 3); + } else { + SettingsData.set("directionalAnimationMode", SettingsData.previousDirectionalMode); + } + } + + Connections { + target: SettingsData + function onDirectionalAnimationModeChanged() {} + function onMotionEffectChanged() {} + } + } } // ── Display Assignment ──────────────────────────────────────────── diff --git a/quickshell/Modules/Settings/ThemeColorsTab.qml b/quickshell/Modules/Settings/ThemeColorsTab.qml index ce080c44..2b9a5c5f 100644 --- a/quickshell/Modules/Settings/ThemeColorsTab.qml +++ b/quickshell/Modules/Settings/ThemeColorsTab.qml @@ -11,6 +11,9 @@ import qs.Modules.Settings.Widgets Item { id: themeColorsTab + readonly property bool connectedFrameModeActive: SettingsData.frameEnabled + && SettingsData.motionEffect === 1 + && SettingsData.directionalAnimationMode === 3 property var cachedIconThemes: SettingsData.availableIconThemes property var cachedCursorThemes: SettingsData.availableCursorThemes property var cachedMatugenSchemes: Theme.availableMatugenSchemes.map(option => option.label) @@ -1618,7 +1621,11 @@ Item { tags: ["popup", "transparency", "opacity", "modal"] settingKey: "popupTransparency" text: I18n.tr("Popup Transparency") - description: I18n.tr("Controls opacity of all popouts, modals, and their content layers") + description: themeColorsTab.connectedFrameModeActive + ? I18n.tr("Connected Frame mode follows Frame Opacity for connected popouts, docks, and modal surfaces") + : I18n.tr("Controls opacity of all popouts, modals, and their content layers") + enabled: !themeColorsTab.connectedFrameModeActive + opacity: themeColorsTab.connectedFrameModeActive ? 0.5 : 1.0 value: Math.round(SettingsData.popupTransparency * 100) minimum: 0 maximum: 100 @@ -1837,7 +1844,11 @@ Item { tags: ["blur", "background", "transparency", "glass", "frosted"] settingKey: "blurEnabled" text: I18n.tr("Background Blur") - description: BlurService.available ? I18n.tr("Blur the background behind bars, popouts, modals, and notifications. Requires compositor support and configuration.") : I18n.tr("Requires a newer version of Quickshell") + description: !BlurService.available + ? I18n.tr("Requires a newer version of Quickshell") + : (themeColorsTab.connectedFrameModeActive + ? I18n.tr("Connected Frame mode follows Frame Blur for connected surfaces while this remains the master blur availability toggle") + : I18n.tr("Blur the background behind bars, popouts, modals, and notifications. Requires compositor support and configuration.")) checked: SettingsData.blurEnabled ?? false enabled: BlurService.available onToggled: checked => SettingsData.set("blurEnabled", checked) diff --git a/quickshell/Modules/Settings/TypographyMotionTab.qml b/quickshell/Modules/Settings/TypographyMotionTab.qml index 13042d8c..1e53dd5a 100644 --- a/quickshell/Modules/Settings/TypographyMotionTab.qml +++ b/quickshell/Modules/Settings/TypographyMotionTab.qml @@ -203,17 +203,25 @@ Item { SettingsDropdownRow { visible: SettingsData.motionEffect === 1 tab: "typography" - tags: ["animation", "directional", "behavior", "overlap", "sticky", "roll"] + tags: ["animation", "directional", "behavior", "overlap", "sticky", "roll", "connected"] settingKey: "directionalAnimationMode" text: I18n.tr("Directional Behavior") - description: I18n.tr("How the popout emerges from the DankBar") - options: [I18n.tr("Overlap"), I18n.tr("Slide"), I18n.tr("Roll")] + description: { + if (SettingsData.directionalAnimationMode === 3 && SettingsData.frameEnabled) + return I18n.tr("Popouts emerge flush from the bar edge as a single continuous piece, with corner connectors bridging the junction"); + return I18n.tr("How the popout emerges from the DankBar"); + } + options: SettingsData.frameEnabled + ? [I18n.tr("Overlap"), I18n.tr("Slide"), I18n.tr("Roll"), I18n.tr("Connected")] + : [I18n.tr("Overlap"), I18n.tr("Slide"), I18n.tr("Roll")] currentValue: { switch (SettingsData.directionalAnimationMode) { case 1: return I18n.tr("Slide"); case 2: return I18n.tr("Roll"); + case 3: + return SettingsData.frameEnabled ? I18n.tr("Connected") : I18n.tr("Slide"); default: return I18n.tr("Overlap"); } @@ -223,7 +231,11 @@ Item { SettingsData.set("directionalAnimationMode", 1); else if (value === I18n.tr("Roll")) SettingsData.set("directionalAnimationMode", 2); - else + else if (value === I18n.tr("Connected") && SettingsData.frameEnabled) { + if (SettingsData.directionalAnimationMode !== 3) + SettingsData.set("previousDirectionalMode", SettingsData.directionalAnimationMode); + SettingsData.set("directionalAnimationMode", 3); + } else SettingsData.set("directionalAnimationMode", 0); } } diff --git a/quickshell/Widgets/ConnectedCorner.qml b/quickshell/Widgets/ConnectedCorner.qml new file mode 100644 index 00000000..558c8a67 --- /dev/null +++ b/quickshell/Widgets/ConnectedCorner.qml @@ -0,0 +1,152 @@ +import QtQuick +import QtQuick.Shapes +import qs.Common + +// ConnectedCorner — Seam-complement connector that fills the void between +// a bar's rounded corner and a popout's flush edge, creating a seamless junction. +// +// Usage: Place as a sibling to contentWrapper inside unrollCounteract (DankPopout) +// or as a sibling to dockBackground (Dock). Position using contentWrapper.x/y. +// +// barSide: "top" | "bottom" | "left" | "right" — which edge the bar is on +// placement: "left" | "right" — which lateral end of that edge +// spacing: gap between bar surface and popout surface (storedBarSpacing, ~4px) +// connectorRadius: bar corner radius to match (frameRounding or Theme.cornerRadius) +// color: fill color matching the popout surface + +Item { + id: root + + property string barSide: "top" + property string placement: "left" + property real spacing: 4 + property real connectorRadius: 12 + property color color: "transparent" + + readonly property bool isHorizontalBar: barSide === "top" || barSide === "bottom" + readonly property bool isPlacementLeft: placement === "left" + readonly property string arcCorner: { + if (barSide === "top") + return isPlacementLeft ? "bottomLeft" : "bottomRight"; + if (barSide === "bottom") + return isPlacementLeft ? "topLeft" : "topRight"; + if (barSide === "left") + return isPlacementLeft ? "topRight" : "bottomRight"; + return isPlacementLeft ? "topLeft" : "bottomLeft"; + } + readonly property real pathStartX: { + switch (arcCorner) { + case "topLeft": + return width; + case "topRight": + case "bottomLeft": + return 0; + default: + return 0; + } + } + readonly property real pathStartY: { + switch (arcCorner) { + case "bottomRight": + return height; + default: + return 0; + } + } + readonly property real firstLineX: { + switch (arcCorner) { + case "topLeft": + case "bottomLeft": + return width; + default: + return 0; + } + } + readonly property real firstLineY: { + switch (arcCorner) { + case "topLeft": + case "topRight": + return height; + default: + return 0; + } + } + readonly property real secondLineX: { + switch (arcCorner) { + case "topRight": + case "bottomLeft": + case "bottomRight": + return width; + default: + return 0; + } + } + readonly property real secondLineY: { + switch (arcCorner) { + case "topLeft": + case "topRight": + case "bottomLeft": + return height; + default: + return 0; + } + } + readonly property real arcCenterX: arcCorner === "topRight" || arcCorner === "bottomRight" ? width : 0 + readonly property real arcCenterY: arcCorner === "bottomLeft" || arcCorner === "bottomRight" ? height : 0 + readonly property real arcStartAngle: { + switch (arcCorner) { + case "topLeft": + case "topRight": + return 90; + case "bottomLeft": + return 0; + default: + return -90; + } + } + readonly property real arcSweepAngle: { + switch (arcCorner) { + case "topRight": + return 90; + default: + return -90; + } + } + + // Horizontal bar: connector is tall (bridges vertical gap), narrow (corner radius wide) + // Vertical bar: connector is wide (bridges horizontal gap), short (corner radius tall) + width: isHorizontalBar ? connectorRadius : (spacing + connectorRadius) + height: isHorizontalBar ? (spacing + connectorRadius) : connectorRadius + + Shape { + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + + ShapePath { + fillColor: root.color + strokeColor: "transparent" + strokeWidth: 0 + startX: root.pathStartX + startY: root.pathStartY + + PathLine { + x: root.firstLineX + y: root.firstLineY + } + + PathLine { + x: root.secondLineX + y: root.secondLineY + } + + PathAngleArc { + centerX: root.arcCenterX + centerY: root.arcCenterY + radiusX: root.width + radiusY: root.height + startAngle: root.arcStartAngle + sweepAngle: root.arcSweepAngle + } + } + } +} diff --git a/quickshell/Widgets/DankPopout.qml b/quickshell/Widgets/DankPopout.qml index b2f7ee0f..0a749940 100644 --- a/quickshell/Widgets/DankPopout.qml +++ b/quickshell/Widgets/DankPopout.qml @@ -47,6 +47,8 @@ Item { property var screen: null readonly property real effectiveBarThickness: { + if (Theme.isConnectedEffect) + return Math.max(0, storedBarThickness); const padding = storedBarConfig ? (storedBarConfig.innerPadding !== undefined ? storedBarConfig.innerPadding : 4) : 4; return Math.max(26 + padding * 0.6, Theme.barHeight - 4 - (8 - padding)) + storedBarSpacing; } @@ -68,6 +70,7 @@ Item { readonly property real barWidth: barBounds.width readonly property real barHeight: barBounds.height readonly property real barWingSize: barBounds.wingSize + readonly property bool effectiveSurfaceBlurEnabled: Theme.connectedSurfaceBlurEnabled signal opened signal popoutClosed @@ -254,11 +257,25 @@ Item { readonly property real screenWidth: screen ? screen.width : 0 readonly property real screenHeight: screen ? screen.height : 0 readonly property real dpr: screen ? screen.devicePixelRatio : 1 + readonly property real frameInset: { + if (!SettingsData.frameEnabled) return 0; + const ft = SettingsData.frameThickness; + const fr = SettingsData.frameRounding; + const ccr = Theme.connectedCornerRadius; + if (Theme.isConnectedEffect) + return Math.max(ft * 4, ft + ccr * 2); + const useAutoGaps = storedBarConfig?.popupGapsAuto !== undefined ? storedBarConfig.popupGapsAuto : true; + const manualGapValue = storedBarConfig?.popupGapsManual !== undefined ? storedBarConfig.popupGapsManual : 6; + const gap = useAutoGaps ? Math.max(6, storedBarSpacing) : manualGapValue; + return Math.max(ft + gap, fr); + } 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: { + if (Theme.isConnectedEffect) + return Math.max(storedBarSpacing + Theme.connectedCornerRadius + 4, 40); if (Theme.isDirectionalEffect) { if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode !== 0) return 16; // Slide Behind and Roll Out do not add animationOffset, enabling strict Wayland clipping. @@ -271,6 +288,40 @@ Item { 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) + readonly property real connectedAnchorX: { + if (!Theme.isConnectedEffect) + return triggerX; + switch (effectiveBarPosition) { + case SettingsData.Position.Left: + return barX + barWidth; + case SettingsData.Position.Right: + return barX; + default: + return triggerX; + } + } + readonly property real connectedAnchorY: { + if (!Theme.isConnectedEffect) + return triggerY; + switch (effectiveBarPosition) { + case SettingsData.Position.Top: + return barY + barHeight; + case SettingsData.Position.Bottom: + return barY; + default: + return triggerY; + } + } + + function adjacentBarClearance(exclusion) { + if (exclusion <= 0) + return 0; + if (!Theme.isConnectedEffect) + return exclusion; + // In a shared frame corner, the adjacent connected bar already occupies + // one rounded-corner radius before the popout's own connector begins. + return exclusion + Theme.connectedCornerRadius * 2; + } onAlignedHeightChanged: { if (!suspendShadowWhileResizing || !shouldBeVisible) @@ -295,17 +346,22 @@ Item { readonly property real alignedX: Theme.snap((() => { const useAutoGaps = storedBarConfig?.popupGapsAuto !== undefined ? storedBarConfig.popupGapsAuto : true; const manualGapValue = storedBarConfig?.popupGapsManual !== undefined ? storedBarConfig.popupGapsManual : 4; - const popupGap = useAutoGaps ? Math.max(4, storedBarSpacing) : manualGapValue; + const rawPopupGap = useAutoGaps ? Math.max(4, storedBarSpacing) : manualGapValue; + const popupGap = Theme.isConnectedEffect ? 0 : rawPopupGap; + const edgeGap = Math.max(popupGap, frameInset); + const anchorX = Theme.isConnectedEffect ? connectedAnchorX : triggerX; switch (effectiveBarPosition) { case SettingsData.Position.Left: - return Math.max(popupGap, Math.min(screenWidth - popupWidth - popupGap, triggerX)); + // bar on left: left side is bar-adjacent (popupGap), right side is frame-perpendicular (edgeGap) + return Math.max(popupGap, Math.min(screenWidth - popupWidth - edgeGap, anchorX)); case SettingsData.Position.Right: - return Math.max(popupGap, Math.min(screenWidth - popupWidth - popupGap, triggerX - popupWidth)); + // bar on right: right side is bar-adjacent (popupGap), left side is frame-perpendicular (edgeGap) + return Math.max(edgeGap, Math.min(screenWidth - popupWidth - popupGap, anchorX - popupWidth)); default: const rawX = triggerX + (triggerWidth / 2) - (popupWidth / 2); - const minX = adjacentBarInfo.leftBar > 0 ? adjacentBarInfo.leftBar : popupGap; - const maxX = screenWidth - popupWidth - (adjacentBarInfo.rightBar > 0 ? adjacentBarInfo.rightBar : popupGap); + const minX = Math.max(edgeGap, adjacentBarClearance(adjacentBarInfo.leftBar)); + const maxX = screenWidth - popupWidth - Math.max(edgeGap, adjacentBarClearance(adjacentBarInfo.rightBar)); return Math.max(minX, Math.min(maxX, rawX)); } })(), dpr) @@ -313,17 +369,22 @@ Item { readonly property real alignedY: Theme.snap((() => { const useAutoGaps = storedBarConfig?.popupGapsAuto !== undefined ? storedBarConfig.popupGapsAuto : true; const manualGapValue = storedBarConfig?.popupGapsManual !== undefined ? storedBarConfig.popupGapsManual : 4; - const popupGap = useAutoGaps ? Math.max(4, storedBarSpacing) : manualGapValue; + const rawPopupGap = useAutoGaps ? Math.max(4, storedBarSpacing) : manualGapValue; + const popupGap = Theme.isConnectedEffect ? 0 : rawPopupGap; + const edgeGap = Math.max(popupGap, frameInset); + const anchorY = Theme.isConnectedEffect ? connectedAnchorY : triggerY; switch (effectiveBarPosition) { case SettingsData.Position.Bottom: - return Math.max(popupGap, Math.min(screenHeight - popupHeight - popupGap, triggerY - popupHeight)); + // bar on bottom: bottom side is bar-adjacent (popupGap), top side is frame-perpendicular (edgeGap) + return Math.max(edgeGap, Math.min(screenHeight - popupHeight - popupGap, anchorY - popupHeight)); case SettingsData.Position.Top: - return Math.max(popupGap, Math.min(screenHeight - popupHeight - popupGap, triggerY)); + // bar on top: top side is bar-adjacent (popupGap), bottom side is frame-perpendicular (edgeGap) + return Math.max(popupGap, Math.min(screenHeight - popupHeight - edgeGap, anchorY)); default: const rawY = triggerY - (popupHeight / 2); - const minY = adjacentBarInfo.topBar > 0 ? adjacentBarInfo.topBar : popupGap; - const maxY = screenHeight - popupHeight - (adjacentBarInfo.bottomBar > 0 ? adjacentBarInfo.bottomBar : popupGap); + const minY = Math.max(edgeGap, adjacentBarClearance(adjacentBarInfo.topBar)); + const maxY = screenHeight - popupHeight - Math.max(edgeGap, adjacentBarClearance(adjacentBarInfo.bottomBar)); return Math.max(minY, Math.min(maxY, rawY)); } })(), dpr) @@ -472,6 +533,18 @@ Item { visible: false color: "transparent" + WindowBlur { + id: popoutBlur + targetWindow: contentWindow + blurEnabled: root.effectiveSurfaceBlurEnabled + readonly property real s: Math.min(1, contentContainer.scaleValue) + blurX: contentContainer.x + contentContainer.width * (1 - s) * 0.5 + Theme.snap(contentContainer.animX, root.dpr) - contentContainer.horizontalConnectorExtent * s + blurY: contentContainer.y + contentContainer.height * (1 - s) * 0.5 + Theme.snap(contentContainer.animY, root.dpr) - contentContainer.verticalConnectorExtent * s + blurWidth: (shouldBeVisible && contentWrapper.opacity > 0) ? (contentContainer.width + contentContainer.horizontalConnectorExtent * 2) * s : 0 + blurHeight: (shouldBeVisible && contentWrapper.opacity > 0) ? (contentContainer.height + contentContainer.verticalConnectorExtent * 2) * s : 0 + blurRadius: Theme.connectedSurfaceRadius + } + WlrLayershell.namespace: root.layerNamespace WlrLayershell.layer: { switch (Quickshell.env("DMS_POPOUT_LAYER")) { @@ -524,10 +597,10 @@ Item { Item { id: contentMaskRect visible: false - x: contentContainer.x - y: contentContainer.y - width: root.alignedWidth - height: root.alignedHeight + x: contentContainer.x - contentContainer.horizontalConnectorExtent + y: contentContainer.y - contentContainer.verticalConnectorExtent + width: root.alignedWidth + contentContainer.horizontalConnectorExtent * 2 + height: root.alignedHeight + contentContainer.verticalConnectorExtent * 2 } MouseArea { @@ -556,12 +629,66 @@ Item { readonly property bool barBottom: effectiveBarPosition === SettingsData.Position.Bottom readonly property bool barLeft: effectiveBarPosition === SettingsData.Position.Left readonly property bool barRight: effectiveBarPosition === SettingsData.Position.Right + readonly property string connectedBarSide: barTop ? "top" : (barBottom ? "bottom" : (barLeft ? "left" : "right")) + readonly property real surfaceRadius: Theme.connectedSurfaceRadius + readonly property color surfaceColor: Theme.popupLayerColor(Theme.surfaceContainer) + readonly property color surfaceBorderColor: Theme.isConnectedEffect + ? "transparent" + : (BlurService.enabled ? BlurService.borderColor : Theme.outlineMedium) + readonly property real surfaceBorderWidth: Theme.isConnectedEffect ? 0 : BlurService.borderWidth + readonly property real surfaceTopLeftRadius: Theme.isConnectedEffect && (barTop || barLeft) ? 0 : surfaceRadius + readonly property real surfaceTopRightRadius: Theme.isConnectedEffect && (barTop || barRight) ? 0 : surfaceRadius + readonly property real surfaceBottomLeftRadius: Theme.isConnectedEffect && (barBottom || barLeft) ? 0 : surfaceRadius + readonly property real surfaceBottomRightRadius: Theme.isConnectedEffect && (barBottom || barRight) ? 0 : surfaceRadius readonly property bool directionalEffect: Theme.isDirectionalEffect readonly property bool depthEffect: Theme.isDepthEffect readonly property real directionalTravelX: Math.max(root.animationOffset, root.alignedWidth + Theme.spacingL) readonly property real directionalTravelY: Math.max(root.animationOffset, root.alignedHeight + Theme.spacingL) readonly property real depthTravel: Math.max(root.animationOffset * 0.7, 28) readonly property real sectionTilt: (triggerSection === "left" ? -1 : (triggerSection === "right" ? 1 : 0)) + readonly property real horizontalConnectorExtent: Theme.isConnectedEffect && (barTop || barBottom) ? Theme.connectedCornerRadius : 0 + readonly property real verticalConnectorExtent: Theme.isConnectedEffect && (barLeft || barRight) ? Theme.connectedCornerRadius : 0 + + function connectorWidth(spacing) { + return (barTop || barBottom) ? Theme.connectedCornerRadius : (spacing + Theme.connectedCornerRadius); + } + + function connectorHeight(spacing) { + return (barTop || barBottom) ? (spacing + Theme.connectedCornerRadius) : Theme.connectedCornerRadius; + } + + function connectorSeamX(baseX, bodyWidth, placement) { + if (barTop || barBottom) + return placement === "left" ? baseX : baseX + bodyWidth; + return barLeft ? baseX : baseX + bodyWidth; + } + + function connectorSeamY(baseY, bodyHeight, placement) { + if (barTop) + return baseY; + if (barBottom) + return baseY + bodyHeight; + return placement === "left" ? baseY : baseY + bodyHeight; + } + + function connectorX(baseX, bodyWidth, placement, spacing) { + const seamX = connectorSeamX(baseX, bodyWidth, placement); + const width = connectorWidth(spacing); + if (barTop || barBottom) + return placement === "left" ? seamX - width : seamX; + return barLeft ? seamX : seamX - width; + } + + function connectorY(baseY, bodyHeight, placement, spacing) { + const seamY = connectorSeamY(baseY, bodyHeight, placement); + const height = connectorHeight(spacing); + if (barTop) + return seamY; + if (barBottom) + return seamY - height; + return placement === "left" ? seamY - height : seamY; + } + readonly property real offsetX: { if (directionalEffect) { if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2) @@ -663,17 +790,38 @@ Item { Item { id: directionalClipMask - readonly property bool shouldClip: typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0 && Theme.isDirectionalEffect + readonly property bool shouldClip: Theme.isDirectionalEffect + && typeof SettingsData !== "undefined" + && SettingsData.directionalAnimationMode > 0 readonly property real clipOversize: 1000 + readonly property real connectedClipAllowance: Theme.isConnectedEffect + ? Math.ceil(root.shadowRenderPadding + BlurService.borderWidth + 2) + : 0 clip: shouldClip // Bound the clipping strictly to the bar side, allowing massive overflow on the other 3 sides for shadows - x: shouldClip ? (contentContainer.barRight ? -clipOversize : (contentContainer.barLeft ? 0 : -clipOversize)) : 0 - y: shouldClip ? (contentContainer.barBottom ? -clipOversize : (contentContainer.barTop ? 0 : -clipOversize)) : 0 + x: shouldClip ? (contentContainer.barLeft ? -connectedClipAllowance : -clipOversize) : 0 + y: shouldClip ? (contentContainer.barTop ? -connectedClipAllowance : -clipOversize) : 0 - width: shouldClip ? parent.width + clipOversize + (contentContainer.barLeft || contentContainer.barRight ? 0 : clipOversize) : parent.width - height: shouldClip ? parent.height + clipOversize + (contentContainer.barTop || contentContainer.barBottom ? 0 : clipOversize) : parent.height + width: { + if (!shouldClip) + return parent.width; + if (contentContainer.barLeft) + return parent.width + connectedClipAllowance + clipOversize; + if (contentContainer.barRight) + return parent.width + clipOversize + connectedClipAllowance; + return parent.width + clipOversize * 2; + } + height: { + if (!shouldClip) + return parent.height; + if (contentContainer.barTop) + return parent.height + connectedClipAllowance + clipOversize; + if (contentContainer.barBottom) + return parent.height + clipOversize + connectedClipAllowance; + return parent.height + clipOversize * 2; + } Item { id: aligner @@ -697,18 +845,75 @@ Item { ElevationShadow { id: shadowSource - width: parent.width - height: parent.height + readonly property real connectorExtent: Theme.isConnectedEffect ? Theme.connectedCornerRadius : 0 + readonly property real extraLeft: Theme.isConnectedEffect && (contentContainer.barTop || contentContainer.barBottom) ? connectorExtent : 0 + readonly property real extraRight: Theme.isConnectedEffect && (contentContainer.barTop || contentContainer.barBottom) ? connectorExtent : 0 + readonly property real extraTop: Theme.isConnectedEffect && (contentContainer.barLeft || contentContainer.barRight) ? connectorExtent : 0 + readonly property real extraBottom: Theme.isConnectedEffect && (contentContainer.barLeft || contentContainer.barRight) ? connectorExtent : 0 + readonly property real bodyX: extraLeft + readonly property real bodyY: extraTop + readonly property real bodyWidth: parent.width + readonly property real bodyHeight: parent.height + + width: parent.width + extraLeft + extraRight + height: parent.height + extraTop + extraBottom opacity: contentWrapper.opacity scale: contentWrapper.scale - x: contentWrapper.x - y: contentWrapper.y + x: contentWrapper.x - extraLeft + y: contentWrapper.y - extraTop level: root.shadowLevel direction: root.effectiveShadowDirection fallbackOffset: root.shadowFallbackOffset - targetRadius: Theme.cornerRadius - targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) + targetRadius: contentContainer.surfaceRadius + topLeftRadius: contentContainer.surfaceTopLeftRadius + topRightRadius: contentContainer.surfaceTopRightRadius + bottomLeftRadius: contentContainer.surfaceBottomLeftRadius + bottomRightRadius: contentContainer.surfaceBottomRightRadius + targetColor: contentContainer.surfaceColor + borderColor: contentContainer.surfaceBorderColor + borderWidth: contentContainer.surfaceBorderWidth + useCustomSource: Theme.isConnectedEffect shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" && !(root.suspendShadowWhileResizing && root._resizeActive) + + Item { + anchors.fill: parent + visible: Theme.isConnectedEffect + clip: false + + Rectangle { + x: shadowSource.bodyX + y: shadowSource.bodyY + width: shadowSource.bodyWidth + height: shadowSource.bodyHeight + topLeftRadius: contentContainer.surfaceTopLeftRadius + topRightRadius: contentContainer.surfaceTopRightRadius + bottomLeftRadius: contentContainer.surfaceBottomLeftRadius + bottomRightRadius: contentContainer.surfaceBottomRightRadius + color: contentContainer.surfaceColor + } + + ConnectedCorner { + visible: Theme.isConnectedEffect + barSide: contentContainer.connectedBarSide + placement: "left" + spacing: 0 + connectorRadius: Theme.connectedCornerRadius + color: contentContainer.surfaceColor + x: Theme.snap(contentContainer.connectorX(shadowSource.bodyX, shadowSource.bodyWidth, placement, spacing), root.dpr) + y: Theme.snap(contentContainer.connectorY(shadowSource.bodyY, shadowSource.bodyHeight, placement, spacing), root.dpr) + } + + ConnectedCorner { + visible: Theme.isConnectedEffect + barSide: contentContainer.connectedBarSide + placement: "right" + spacing: 0 + connectorRadius: Theme.connectedCornerRadius + color: contentContainer.surfaceColor + x: Theme.snap(contentContainer.connectorX(shadowSource.bodyX, shadowSource.bodyWidth, placement, spacing), root.dpr) + y: Theme.snap(contentContainer.connectorY(shadowSource.bodyY, shadowSource.bodyHeight, placement, spacing), root.dpr) + } + } } Item { @@ -735,12 +940,43 @@ Item { } } - Rectangle { + Item { anchors.fill: parent - radius: Theme.cornerRadius - color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) - border.color: Theme.outlineMedium - border.width: 0 + clip: false + visible: !Theme.isConnectedEffect + + Rectangle { + anchors.fill: parent + topLeftRadius: contentContainer.surfaceTopLeftRadius + topRightRadius: contentContainer.surfaceTopRightRadius + bottomLeftRadius: contentContainer.surfaceBottomLeftRadius + bottomRightRadius: contentContainer.surfaceBottomRightRadius + color: contentContainer.surfaceColor + border.color: contentContainer.surfaceBorderColor + border.width: contentContainer.surfaceBorderWidth + } + + ConnectedCorner { + visible: Theme.isConnectedEffect + barSide: contentContainer.connectedBarSide + placement: "left" + spacing: 0 + connectorRadius: Theme.connectedCornerRadius + color: contentContainer.surfaceColor + x: Theme.snap(contentContainer.connectorX(0, contentWrapper.width, placement, spacing), root.dpr) + y: Theme.snap(contentContainer.connectorY(0, contentWrapper.height, placement, spacing), root.dpr) + } + + ConnectedCorner { + visible: Theme.isConnectedEffect + barSide: contentContainer.connectedBarSide + placement: "right" + spacing: 0 + connectorRadius: Theme.connectedCornerRadius + color: contentContainer.surfaceColor + x: Theme.snap(contentContainer.connectorX(0, contentWrapper.width, placement, spacing), root.dpr) + y: Theme.snap(contentContainer.connectorY(0, contentWrapper.height, placement, spacing), root.dpr) + } } Loader { diff --git a/quickshell/Widgets/WindowBlur.qml b/quickshell/Widgets/WindowBlur.qml index 3b45212b..428f2d93 100644 --- a/quickshell/Widgets/WindowBlur.qml +++ b/quickshell/Widgets/WindowBlur.qml @@ -1,4 +1,5 @@ import QtQuick +import qs.Common import qs.Services Item { @@ -8,6 +9,7 @@ Item { required property var targetWindow property var blurItem: null + property bool blurEnabled: Theme.connectedSurfaceBlurEnabled property real blurX: 0 property real blurY: 0 property real blurWidth: 0 @@ -17,7 +19,7 @@ Item { property var _region: null function _apply() { - if (!BlurService.enabled || !targetWindow) { + if (!blurEnabled || !BlurService.enabled || !targetWindow) { _cleanup(); return; } @@ -43,6 +45,8 @@ Item { _region = null; } + onBlurEnabledChanged: _apply() + Connections { target: BlurService function onEnabledChanged() {