From cd4227687041b6c6fc3733a5ae1db9fb20cd0ec6 Mon Sep 17 00:00:00 2001 From: Alex Hulbert Date: Fri, 16 Oct 2026 00:10:35 -0400 Subject: [PATCH] feat(island): Add shadow to island and satellites support (#3798) --- quickshell/Modules/DBar/DBarBody.qml | 3 +- quickshell/Modules/DIsland/DIslandSurface.qml | 42 +++++++++++++++++-- quickshell/Modules/DIsland/IslandBarHost.qml | 5 ++- .../Modules/DIsland/IslandFreeHostWindow.qml | 17 ++++---- quickshell/Modules/SurfaceWidgets/BarPill.qml | 18 ++++++++ 5 files changed, 70 insertions(+), 15 deletions(-) diff --git a/quickshell/Modules/DBar/DBarBody.qml b/quickshell/Modules/DBar/DBarBody.qml index b3655fd73..34a45cd83 100644 --- a/quickshell/Modules/DBar/DBarBody.qml +++ b/quickshell/Modules/DBar/DBarBody.qml @@ -743,7 +743,8 @@ Item { readonly property real barSurfaceThickness: Theme.px(effectiveBarThickness + surfaceSpacing + ((renderBarConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel && spansEdge ? _wingR : 0), _dpr) + _shadowBuffer readonly property bool islandSheetOut: islandHost?.sheetOut ?? false - readonly property real hostThickness: isIsland ? (islandHost?.hostThickness ?? islandStripThickness) : Math.max(barSurfaceThickness, islandSheetOut ? islandHost.hostThickness : 0) + readonly property real islandSatelliteShadowBuffer: isIsland && islandSatellitesEnabled && !islandSatelliteBackground ? _shadowBuffer : 0 + readonly property real hostThickness: isIsland ? Math.max(islandHost?.hostThickness ?? 0, islandStripThickness + islandSatelliteShadowBuffer) : Math.max(barSurfaceThickness, islandSheetOut ? islandHost.hostThickness : 0) readonly property real hideSlideThickness: isIsland ? hostThickness : barSurfaceThickness readonly property real surfaceImplicitHeight: !isVertical ? hostThickness : 0 readonly property real surfaceImplicitWidth: isVertical ? hostThickness : 0 diff --git a/quickshell/Modules/DIsland/DIslandSurface.qml b/quickshell/Modules/DIsland/DIslandSurface.qml index 2602af2a1..b603f2493 100644 --- a/quickshell/Modules/DIsland/DIslandSurface.qml +++ b/quickshell/Modules/DIsland/DIslandSurface.qml @@ -75,6 +75,9 @@ Item { readonly property real islandOpacity: Math.max(0, Math.min(1, root.transparency)) readonly property color effectiveSurfaceColor: root.embedded ? root.bandColor : root.highContrast ? Theme.surfaceContainerHighest : Theme.withAlpha(root.surfaceColor, root.islandOpacity) readonly property real surfaceOpacity: root.effectiveSurfaceColor.a + readonly property bool shadowEnabled: !root.chromeless && root.surfaceOpacity > 0 && Theme.elevationEnabled && SettingsData.barElevationEnabled + readonly property string autoBarShadowDirection: root.controller.edge + readonly property string effectiveShadowDirection: Theme.elevationLightDirection === "autoBar" ? autoBarShadowDirection : Theme.elevationLightDirection readonly property real currentSurfaceRadius: Math.max(0, motion.currentTopLeftRadius, motion.currentBottomLeftRadius) readonly property color notificationAccentColor: { if (!root.controller.notificationActive) @@ -364,6 +367,13 @@ Item { height: (motion.running ? Math.max(root.motionStartBounds.y + root.motionStartBounds.height, root.targetVisualY + motion.targetHeight) : root.targetVisualY + motion.targetHeight) + overshootBudget - y } + Loader { + anchors.fill: island + z: island.z - 1 + active: root.shadowEnabled && !root.embedded + sourceComponent: shadowComponent + } + MorphSurface { id: island motion: root.surfaceMotion @@ -531,15 +541,24 @@ Item { active: root.embedded sourceComponent: Item { id: overhangClip + readonly property real shadowPad: root.shadowEnabled ? Theme.elevationRenderPadding(Theme.elevationLevel2, root.effectiveShadowDirection, Theme.spacingXS, Theme.spacingS, Theme.spacingL) : 0 visible: root.embeddedOverhang > 0 clip: true - x: root.isVertical ? (root.farEdge ? -root.embeddedOverhang : root.crossExtent) : 0 - y: root.isVertical ? 0 : (root.farEdge ? -root.embeddedOverhang : root.crossExtent) - width: root.isVertical ? root.embeddedOverhang : root.width - height: root.isVertical ? root.height : root.embeddedOverhang + x: root.isVertical ? (root.farEdge ? -root.embeddedOverhang - shadowPad : root.crossExtent) : -shadowPad + y: root.isVertical ? -shadowPad : (root.farEdge ? -root.embeddedOverhang - shadowPad : root.crossExtent) + width: root.isVertical ? root.embeddedOverhang + shadowPad : root.width + 2 * shadowPad + height: root.isVertical ? root.height + 2 * shadowPad : root.embeddedOverhang + shadowPad + + Loader { + anchors.fill: overhang + active: root.shadowEnabled + sourceComponent: shadowComponent + } MorphSurface { + id: overhang + motion: root.surfaceMotion x: root.currentVisualX - overhangClip.x y: root.currentVisualY - overhangClip.y @@ -576,6 +595,21 @@ Item { } } + Component { + id: shadowComponent + + ElevationShadow { + level: Theme.elevationLevel2 + direction: root.effectiveShadowDirection + fallbackOffset: Theme.spacingXS + targetColor: "transparent" + topLeftRadius: island.topLeftRadius + topRightRadius: island.topRightRadius + bottomLeftRadius: island.bottomLeftRadius + bottomRightRadius: island.bottomRightRadius + } + } + Component { id: compactHomeComponent diff --git a/quickshell/Modules/DIsland/IslandBarHost.qml b/quickshell/Modules/DIsland/IslandBarHost.qml index 5c3243252..48b0e9b9b 100644 --- a/quickshell/Modules/DIsland/IslandBarHost.qml +++ b/quickshell/Modules/DIsland/IslandBarHost.qml @@ -170,8 +170,9 @@ Item { readonly property int destinationMaxHeight: Math.max(destinationMinHeight, Math.min(destinationMaxHeightLimit, (root.screen?.height ?? referenceScreenHeight) - screenMargin)) readonly property int maxActivityHeight: Math.max(root.heldEditSurfaceHeight, controller.dashboardHeight, controller.controlCenterHeight, controller.launcherExpandedTarget.height, controller.clipboardExpandedTarget.height, controller.powerMenuExpandedTarget.height, destinationMaxHeight) readonly property int maxActivityWidth: Math.max(controller.dashboardMaxWidth, controller.controlCenterMaxWidth, controller.launcherExpandedTarget.width, controller.clipboardExpandedTarget.width, controller.powerMenuExpandedTarget.width, Math.min(activityMaxWidth, Math.max(activityMinWidth, (root.screen?.width ?? referenceScreenWidth) - screenMargin))) - readonly property int hostThickness: outerGap + nearInset + (root.isVertical ? maxActivityWidth : maxActivityHeight) + Theme.spacingS - readonly property real maximumAlongOffset: root.isVertical ? Math.max(0, (height - maxActivityHeight) / 2 - Theme.spacingS) : Math.max(0, (width - maxActivityWidth) / 2 - Theme.spacingS) + readonly property real shadowPadding: surface.shadowEnabled ? Theme.elevationRenderPadding(Theme.elevationLevel2, surface.effectiveShadowDirection, Theme.spacingXS, Theme.spacingS, Theme.spacingL) : 0 + readonly property int hostThickness: outerGap + nearInset + (root.isVertical ? maxActivityWidth : maxActivityHeight) + Theme.spacingS + shadowPadding + readonly property real maximumAlongOffset: root.isVertical ? Math.max(0, (height - maxActivityHeight) / 2 - Theme.spacingS - shadowPadding) : Math.max(0, (width - maxActivityWidth) / 2 - Theme.spacingS - shadowPadding) readonly property bool scrollEnabled: root.barConfig?.scrollEnabled ?? true property bool keyboardFocusArmed: true readonly property int keyboardFocusPolicy: KeyboardFocus.keyboardFocus(controller.keyboardDismissRequested && root.keyboardFocusArmed && !controller.keyboardYielded, null) diff --git a/quickshell/Modules/DIsland/IslandFreeHostWindow.qml b/quickshell/Modules/DIsland/IslandFreeHostWindow.qml index d10998d3d..ecd3fd131 100644 --- a/quickshell/Modules/DIsland/IslandFreeHostWindow.qml +++ b/quickshell/Modules/DIsland/IslandFreeHostWindow.qml @@ -30,8 +30,8 @@ PanelWindow { readonly property var storedAnchor: SessionData.islandFreePositions[root.positionKey] ?? null readonly property real storedAnchorX: root.clampAnchorX((typeof root.storedAnchor?.x === "number" ? root.storedAnchor.x : 0.5) * root.screenWidth) readonly property real storedAnchorY: root.clampAnchorY((typeof root.storedAnchor?.y === "number" ? root.storedAnchor.y : 0.5) * root.screenHeight) - readonly property int surfaceX: Math.floor(body.currentVisualX - root.hitPadding) - readonly property int surfaceY: Math.floor(body.currentVisualY - root.hitPadding) + readonly property int surfaceX: Math.floor(body.currentVisualX - root.hitPadding - body.shadowPadding) + readonly property int surfaceY: Math.floor(body.currentVisualY - root.hitPadding - body.shadowPadding) readonly property bool expanded: body.expanded readonly property bool pointerInside: body.islandController.pointerInside @@ -130,8 +130,8 @@ PanelWindow { left: root.surfaceX top: root.surfaceY } - implicitWidth: Math.ceil(body.currentVisualX + body.currentVisualWidth + root.hitPadding) - root.surfaceX - implicitHeight: Math.ceil(body.currentVisualY + body.currentVisualHeight + root.hitPadding) - root.surfaceY + implicitWidth: Math.ceil(body.currentVisualX + body.currentVisualWidth + root.hitPadding + body.shadowPadding) - root.surfaceX + implicitHeight: Math.ceil(body.currentVisualY + body.currentVisualHeight + root.hitPadding + body.shadowPadding) - root.surfaceY Component.onCompleted: { root.wake(); @@ -164,6 +164,7 @@ PanelWindow { Item { id: hitTarget anchors.fill: parent + anchors.margins: body.shadowPadding } // Keep the animated buffer tight; the unmoving input-only window catches outside clicks. @@ -185,10 +186,10 @@ PanelWindow { width: dismissWindow.width height: dismissWindow.height Region { - x: root.surfaceX - y: root.surfaceY - width: root.width - height: root.height + x: root.surfaceX + hitTarget.x + y: root.surfaceY + hitTarget.y + width: hitTarget.width + height: hitTarget.height intersection: Intersection.Subtract } } diff --git a/quickshell/Modules/SurfaceWidgets/BarPill.qml b/quickshell/Modules/SurfaceWidgets/BarPill.qml index 2da0ae037..999fe4dfb 100644 --- a/quickshell/Modules/SurfaceWidgets/BarPill.qml +++ b/quickshell/Modules/SurfaceWidgets/BarPill.qml @@ -90,6 +90,9 @@ Item { return BarMetrics.widgetFill(barConfig); } property color fillColor: defaultFillColor + readonly property bool shadowEnabled: barConfig?.island === true && !SettingsData.islandSetting(barConfig, "islandSatelliteBackground") && !noBackground && fillColor.a > 0 && Theme.elevationEnabled && SettingsData.barElevationEnabled + readonly property string autoBarShadowDirection: axis?.edge ?? "top" + readonly property string effectiveShadowDirection: Theme.elevationLightDirection === "autoBar" ? autoBarShadowDirection : Theme.elevationLightDirection property real topLeftRadius: cornerRadius(startJoined) property real topRightRadius: cornerRadius(isVerticalOrientation ? startJoined : endJoined) @@ -242,6 +245,21 @@ Item { width: root.isVerticalOrientation ? visualContent.width : Math.max(0, length) height: root.isVerticalOrientation ? Math.max(0, length) : visualContent.height + Loader { + anchors.fill: parent + active: root.shadowEnabled && width > 0 && height > 0 + sourceComponent: ElevationShadow { + level: BarMetrics.elevationLevel + direction: root.effectiveShadowDirection + fallbackOffset: Theme.spacingXS + targetColor: "transparent" + topLeftRadius: fill.topLeftRadius + topRightRadius: fill.topRightRadius + bottomLeftRadius: fill.bottomLeftRadius + bottomRightRadius: fill.bottomRightRadius + } + } + Rectangle { anchors.fill: parent anchors.margins: -root.outlineThickness