diff --git a/quickshell/Common/ConnectedModeState.qml b/quickshell/Common/ConnectedModeState.qml index b4925eff..af1f2144 100644 --- a/quickshell/Common/ConnectedModeState.qml +++ b/quickshell/Common/ConnectedModeState.qml @@ -115,6 +115,32 @@ Singleton { return true; } + function setPopoutBody(claimId, bodyX, bodyY, bodyW, bodyH) { + if (!hasPopoutOwner(claimId)) + return false; + if (bodyX !== undefined) { + const nextX = Number(bodyX); + if (!isNaN(nextX) && popoutBodyX !== nextX) + popoutBodyX = nextX; + } + if (bodyY !== undefined) { + const nextY = Number(bodyY); + if (!isNaN(nextY) && popoutBodyY !== nextY) + popoutBodyY = nextY; + } + if (bodyW !== undefined) { + const nextW = Number(bodyW); + if (!isNaN(nextW) && popoutBodyW !== nextW) + popoutBodyW = nextW; + } + if (bodyH !== undefined) { + const nextH = Number(bodyH); + if (!isNaN(nextH) && popoutBodyH !== nextH) + popoutBodyH = nextH; + } + return true; + } + function _cloneDockStates() { const next = {}; for (const screenName in dockStates) diff --git a/quickshell/Modules/ControlCenter/Components/DetailHost.qml b/quickshell/Modules/ControlCenter/Components/DetailHost.qml index a3b11564..7091d321 100644 --- a/quickshell/Modules/ControlCenter/Components/DetailHost.qml +++ b/quickshell/Modules/ControlCenter/Components/DetailHost.qml @@ -37,7 +37,7 @@ Item { Loader { id: pluginDetailLoader width: parent.width - height: parent.height - Theme.spacingS + height: Math.max(0, parent.height - Theme.spacingS) y: Theme.spacingS active: false sourceComponent: null @@ -46,7 +46,7 @@ Item { Loader { id: coreDetailLoader width: parent.width - height: parent.height - Theme.spacingS + height: Math.max(0, parent.height - Theme.spacingS) y: Theme.spacingS active: false sourceComponent: null @@ -134,7 +134,7 @@ Item { } pluginDetailLoader.sourceComponent = builtinInstance.ccDetailContent; - pluginDetailLoader.active = parent.height > 0; + pluginDetailLoader.active = true; return; } @@ -155,19 +155,19 @@ Item { } pluginDetailLoader.sourceComponent = pluginDetailInstance.ccDetailContent; - pluginDetailLoader.active = parent.height > 0; + pluginDetailLoader.active = true; return; } if (root.expandedSection.startsWith("diskUsage_")) { coreDetailLoader.sourceComponent = diskUsageDetailComponent; - coreDetailLoader.active = parent.height > 0; + coreDetailLoader.active = true; return; } if (root.expandedSection.startsWith("brightnessSlider_")) { coreDetailLoader.sourceComponent = brightnessDetailComponent; - coreDetailLoader.active = parent.height > 0; + coreDetailLoader.active = true; return; } @@ -192,7 +192,7 @@ Item { return; } - coreDetailLoader.active = parent.height > 0; + coreDetailLoader.active = true; } Component { diff --git a/quickshell/Modules/ControlCenter/Components/DragDropGrid.qml b/quickshell/Modules/ControlCenter/Components/DragDropGrid.qml index ff70ed6f..1f4eea42 100644 --- a/quickshell/Modules/ControlCenter/Components/DragDropGrid.qml +++ b/quickshell/Modules/ControlCenter/Components/DragDropGrid.qml @@ -51,6 +51,35 @@ Column { return Math.max(100, maxPopoutHeight - totalRowHeight - rowSpacing); } + readonly property real targetImplicitHeight: { + const rows = layoutResult.rows; + let totalHeight = 0; + for (let i = 0; i < rows.length; i++) { + const widgets = rows[i] || []; + const sliderOnly = widgets.length > 0 && widgets.every(w => { + const id = w.id || ""; + return id === "volumeSlider" || id === "brightnessSlider" || id === "inputVolumeSlider"; + }); + totalHeight += sliderOnly ? (editMode ? 56 : 36) : 60; + if (expandedSection !== "" && i === expandedRowIndex) + totalHeight += detailHeightForSection(expandedSection) + Theme.spacingS; + } + totalHeight += Math.max(0, rows.length - 1) * spacing; + return totalHeight; + } + + function detailHeightForSection(section) { + if (!section) + return 0; + if (section === "wifi" || section === "bluetooth" || section === "builtin_vpn") + return Math.min(350, _maxDetailHeight); + if (section.startsWith("brightnessSlider_")) + return Math.min(400, _maxDetailHeight); + if (section.startsWith("plugin_")) + return Math.min(250, _maxDetailHeight); + return Math.min(250, _maxDetailHeight); + } + function calculateRowsAndWidgets() { return LayoutUtils.calculateRowsAndWidgets(root, expandedSection, expandedWidgetIndex); } @@ -179,7 +208,10 @@ Column { id: detailHost width: parent.width maxAvailableHeight: root._maxDetailHeight - height: active ? (getDetailHeight(root.expandedSection) + Theme.spacingS) : 0 + height: active ? (root.detailHeightForSection(root.expandedSection) + Theme.spacingS) : 0 + clip: true + property string retainedSection: "" + property var retainedWidgetData: null property bool active: { if (root.expandedSection === "") return false; @@ -196,14 +228,47 @@ Column { return rowIndex === root.expandedRowIndex; } - visible: active - expandedSection: root.expandedSection - expandedWidgetData: root.expandedWidgetData + visible: active || height > 0.5 + expandedSection: active ? root.expandedSection : retainedSection + expandedWidgetData: active ? root.expandedWidgetData : retainedWidgetData bluetoothCodecSelector: root.bluetoothCodecSelector widgetModel: root.model collapseCallback: root.requestCollapse screenName: root.screenName screenModel: root.screenModel + + function retainActiveDetail() { + if (!active || !root.expandedSection) + return; + retainedSection = root.expandedSection; + retainedWidgetData = root.expandedWidgetData; + } + + onActiveChanged: retainActiveDetail() + onHeightChanged: { + if (!active && height <= 0.5) { + retainedSection = ""; + retainedWidgetData = null; + } + } + + Connections { + target: root + function onExpandedSectionChanged() { + detailHost.retainActiveDetail(); + } + function onExpandedWidgetDataChanged() { + detailHost.retainActiveDetail(); + } + } + + Behavior on height { + NumberAnimation { + duration: Theme.variantDuration(Theme.popoutAnimationDuration, detailHost.active) + easing.type: Easing.BezierSpline + easing.bezierCurve: detailHost.active ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve + } + } } } } diff --git a/quickshell/Modules/ControlCenter/ControlCenterPopout.qml b/quickshell/Modules/ControlCenter/ControlCenterPopout.qml index 7c2bd634..a4cb5f92 100644 --- a/quickshell/Modules/ControlCenter/ControlCenterPopout.qml +++ b/quickshell/Modules/ControlCenter/ControlCenterPopout.qml @@ -20,19 +20,53 @@ DankPopout { property int expandedWidgetIndex: -1 property var expandedWidgetData: null property bool powerMenuOpen: powerMenuModalLoader?.item?.shouldBeVisible ?? false + property real targetPopupHeight: 400 + property bool _heightUpdatePending: false signal lockRequested + function _maxPopupHeight() { + const screenHeight = (triggerScreen?.height ?? 1080); + return screenHeight - 100; + } + + function _contentTargetHeight() { + const item = contentLoader.item; + if (!item) + return 400; + const naturalHeight = item.targetImplicitHeight !== undefined ? item.targetImplicitHeight : item.implicitHeight; + return Math.max(300, naturalHeight + 20); + } + + function updateTargetPopupHeight() { + const target = Math.min(_maxPopupHeight(), _contentTargetHeight()); + if (Math.abs(targetPopupHeight - target) < 0.5) + return; + targetPopupHeight = target; + } + + function queueTargetPopupHeightUpdate() { + if (_heightUpdatePending) + return; + _heightUpdatePending = true; + Qt.callLater(() => { + _heightUpdatePending = false; + updateTargetPopupHeight(); + }); + } + function collapseAll() { expandedSection = ""; expandedWidgetIndex = -1; expandedWidgetData = null; + queueTargetPopupHeightUpdate(); } onEditModeChanged: { if (editMode) { collapseAll(); } + queueTargetPopupHeightUpdate(); } onVisibleChanged: { @@ -52,12 +86,7 @@ DankPopout { } popupWidth: 550 - popupHeight: { - const screenHeight = (triggerScreen?.height ?? 1080); - const maxHeight = screenHeight - 100; - const contentHeight = contentLoader.item && contentLoader.item.implicitHeight > 0 ? contentLoader.item.implicitHeight + 20 : 400; - return Math.min(maxHeight, contentHeight); - } + popupHeight: targetPopupHeight triggerWidth: 80 positioning: "" screen: triggerScreen @@ -95,6 +124,7 @@ DankPopout { onShouldBeVisibleChanged: { if (shouldBeVisible) { collapseAll(); + queueTargetPopupHeightUpdate(); Qt.callLater(() => { if (NetworkService.activeService) NetworkService.activeService.autoRefreshEnabled = NetworkService.wifiEnabled; @@ -111,6 +141,28 @@ DankPopout { } } + onExpandedSectionChanged: queueTargetPopupHeightUpdate() + onExpandedWidgetIndexChanged: queueTargetPopupHeightUpdate() + onTriggerScreenChanged: queueTargetPopupHeightUpdate() + + Connections { + target: contentLoader + function onLoaded() { + root.queueTargetPopupHeightUpdate(); + } + } + + Connections { + target: contentLoader.item + ignoreUnknownSignals: true + function onTargetImplicitHeightChanged() { + root.queueTargetPopupHeightUpdate(); + } + function onImplicitHeightChanged() { + root.queueTargetPopupHeightUpdate(); + } + } + WidgetModel { id: widgetModel } @@ -122,7 +174,13 @@ DankPopout { LayoutMirroring.enabled: I18n.isRtl LayoutMirroring.childrenInherit: true - implicitHeight: mainColumn.implicitHeight + Theme.spacingM + readonly property real targetImplicitHeight: { + let total = headerPane.implicitHeight + Theme.spacingS + widgetGrid.targetImplicitHeight; + if (editControls.visible) + total += Theme.spacingS + editControls.height; + return total + Theme.spacingM; + } + implicitHeight: targetImplicitHeight property alias bluetoothCodecSelector: bluetoothCodecSelector color: "transparent" @@ -145,84 +203,94 @@ DankPopout { } } - Column { - id: mainColumn - width: parent.width - Theme.spacingL * 2 - x: Theme.spacingL - y: Theme.spacingL - spacing: Theme.spacingS + DankFlickable { + id: contentFlickable + anchors.fill: parent + clip: true + contentWidth: width + contentHeight: Math.max(height, mainColumn.implicitHeight + Theme.spacingM) + interactive: contentHeight > height - HeaderPane { - id: headerPane - width: parent.width - editMode: root.editMode - onEditModeToggled: root.editMode = !root.editMode - onPowerButtonClicked: { - if (powerMenuModalLoader) { - powerMenuModalLoader.active = true; - if (powerMenuModalLoader.item) { - const bounds = Qt.rect(root.alignedX, root.alignedY, root.popupWidth, root.popupHeight); - powerMenuModalLoader.item.openFromControlCenter(bounds, root.screen); + Column { + id: mainColumn + width: contentFlickable.width - Theme.spacingL * 2 + x: Theme.spacingL + y: Theme.spacingL + spacing: Theme.spacingS + + HeaderPane { + id: headerPane + width: parent.width + editMode: root.editMode + onEditModeToggled: root.editMode = !root.editMode + onPowerButtonClicked: { + if (powerMenuModalLoader) { + powerMenuModalLoader.active = true; + if (powerMenuModalLoader.item) { + const bounds = Qt.rect(root.alignedX, root.alignedY, root.popupWidth, root.popupHeight); + powerMenuModalLoader.item.openFromControlCenter(bounds, root.screen); + } } } - } - onLockRequested: { - root.close(); - root.lockRequested(); - } - onSettingsButtonClicked: { - root.close(); - } - } - - DragDropGrid { - id: widgetGrid - width: parent.width - editMode: root.editMode - maxPopoutHeight: { - const screenHeight = (root.triggerScreen?.height ?? 1080); - return screenHeight - 100 - Theme.spacingL - headerPane.height - Theme.spacingS; - } - expandedSection: root.expandedSection - expandedWidgetIndex: root.expandedWidgetIndex - expandedWidgetData: root.expandedWidgetData - model: widgetModel - bluetoothCodecSelector: bluetoothCodecSelector - colorPickerModal: root.colorPickerModal - screenName: root.triggerScreen?.name || "" - screenModel: root.triggerScreen?.model || "" - parentScreen: root.triggerScreen - onExpandClicked: (widgetData, globalIndex) => { - root.expandedWidgetIndex = globalIndex; - root.expandedWidgetData = widgetData; - if (widgetData.id === "diskUsage") { - root.toggleSection("diskUsage_" + (widgetData.instanceId || "default")); - } else if (widgetData.id === "brightnessSlider") { - root.toggleSection("brightnessSlider_" + (widgetData.instanceId || "default")); - } else { - root.toggleSection(widgetData.id); + onLockRequested: { + root.close(); + root.lockRequested(); + } + onSettingsButtonClicked: { + root.close(); } } - onRemoveWidget: index => widgetModel.removeWidget(index) - onMoveWidget: (fromIndex, toIndex) => widgetModel.moveWidget(fromIndex, toIndex) - onToggleWidgetSize: index => widgetModel.toggleWidgetSize(index) - onCollapseRequested: root.collapseAll() - } - EditControls { - width: parent.width - visible: editMode - popoutContent: controlContent - availableWidgets: { - if (!editMode) - return []; - const existingIds = (SettingsData.controlCenterWidgets || []).map(w => w.id); - const allWidgets = widgetModel.baseWidgetDefinitions.concat(widgetModel.getPluginWidgets()); - return allWidgets.filter(w => w.allowMultiple || !existingIds.includes(w.id)); + DragDropGrid { + id: widgetGrid + width: parent.width + editMode: root.editMode + maxPopoutHeight: { + const screenHeight = (root.triggerScreen?.height ?? 1080); + return screenHeight - 100 - Theme.spacingL - headerPane.implicitHeight - Theme.spacingS; + } + expandedSection: root.expandedSection + expandedWidgetIndex: root.expandedWidgetIndex + expandedWidgetData: root.expandedWidgetData + model: widgetModel + bluetoothCodecSelector: bluetoothCodecSelector + colorPickerModal: root.colorPickerModal + screenName: root.triggerScreen?.name || "" + screenModel: root.triggerScreen?.model || "" + parentScreen: root.triggerScreen + onExpandClicked: (widgetData, globalIndex) => { + root.expandedWidgetIndex = globalIndex; + root.expandedWidgetData = widgetData; + if (widgetData.id === "diskUsage") { + root.toggleSection("diskUsage_" + (widgetData.instanceId || "default")); + } else if (widgetData.id === "brightnessSlider") { + root.toggleSection("brightnessSlider_" + (widgetData.instanceId || "default")); + } else { + root.toggleSection(widgetData.id); + } + } + onRemoveWidget: index => widgetModel.removeWidget(index) + onMoveWidget: (fromIndex, toIndex) => widgetModel.moveWidget(fromIndex, toIndex) + onToggleWidgetSize: index => widgetModel.toggleWidgetSize(index) + onCollapseRequested: root.collapseAll() + } + + EditControls { + id: editControls + width: parent.width + visible: editMode + popoutContent: controlContent + availableWidgets: { + if (!editMode) + return []; + const existingIds = (SettingsData.controlCenterWidgets || []).map(w => w.id); + const allWidgets = widgetModel.baseWidgetDefinitions.concat(widgetModel.getPluginWidgets()); + return allWidgets.filter(w => w.allowMultiple || !existingIds.includes(w.id)); + } + onAddWidget: widgetId => widgetModel.addWidget(widgetId) + onResetToDefault: () => widgetModel.resetToDefault() + onClearAll: () => widgetModel.clearAll() } - onAddWidget: widgetId => widgetModel.addWidget(widgetId) - onResetToDefault: () => widgetModel.resetToDefault() - onClearAll: () => widgetModel.clearAll() } } diff --git a/quickshell/Modules/Notifications/Center/KeyboardNavigatedNotificationList.qml b/quickshell/Modules/Notifications/Center/KeyboardNavigatedNotificationList.qml index 855163dd..9587e7a6 100644 --- a/quickshell/Modules/Notifications/Center/KeyboardNavigatedNotificationList.qml +++ b/quickshell/Modules/Notifications/Center/KeyboardNavigatedNotificationList.qml @@ -16,8 +16,7 @@ DankListView { property bool listInitialized: false property int swipingCardIndex: -1 property real swipingCardOffset: 0 - property real __pendingStableHeight: 0 - property real __heightUpdateThreshold: 20 + property bool _stableHeightUpdatePending: false readonly property real shadowBlurPx: Theme.elevationEnabled ? ((Theme.elevationLevel1 && Theme.elevationLevel1.blurPx !== undefined) ? Theme.elevationLevel1.blurPx : 4) : 0 readonly property real shadowHorizontalGutter: Theme.snap(Math.max(Theme.spacingS, Math.min(32, shadowBlurPx * 1.5 + 6)), 1) readonly property real shadowVerticalGutter: Theme.snap(Math.max(Theme.spacingXS, 6), 1) @@ -27,51 +26,52 @@ DankListView { Qt.callLater(() => { if (listView) { listView.listInitialized = true; - listView.stableContentHeight = listView.contentHeight; + listView.syncStableContentHeight(false); } }); } - Timer { - id: heightUpdateDebounce - interval: Theme.mediumDuration + 20 - repeat: false - onTriggered: { - if (!listView.isAnimatingExpansion && Math.abs(listView.__pendingStableHeight - listView.stableContentHeight) > listView.__heightUpdateThreshold) { - listView.stableContentHeight = listView.__pendingStableHeight; - } + function targetContentHeight() { + if (count <= 0) + return contentHeight; + + let total = topMargin + bottomMargin + Math.max(0, count - 1) * spacing; + for (let i = 0; i < count; i++) { + const item = itemAtIndex(i); + if (!item || item.nonAnimHeight === undefined) + return contentHeight; + total += item.nonAnimHeight; } + return Math.max(0, total); + } + + function syncStableContentHeight(useTarget) { + const nextHeight = useTarget ? targetContentHeight() : contentHeight; + if (Math.abs(nextHeight - stableContentHeight) <= 0.5) + return; + stableContentHeight = nextHeight; + } + + function queueStableContentHeightUpdate(useTarget) { + if (_stableHeightUpdatePending) + return; + _stableHeightUpdatePending = true; + Qt.callLater(() => { + _stableHeightUpdatePending = false; + syncStableContentHeight(useTarget || isAnimatingExpansion); + }); } onContentHeightChanged: { - if (!isAnimatingExpansion) { - __pendingStableHeight = contentHeight; - if (Math.abs(contentHeight - stableContentHeight) > __heightUpdateThreshold) { - heightUpdateDebounce.restart(); - } else { - stableContentHeight = contentHeight; - } - } + if (!isAnimatingExpansion) + queueStableContentHeightUpdate(false); } onIsAnimatingExpansionChanged: { if (isAnimatingExpansion) { - heightUpdateDebounce.stop(); - let delta = 0; - for (let i = 0; i < count; i++) { - const item = itemAtIndex(i); - if (item && item.children[0] && item.children[0].isAnimating) { - const targetDelegateHeight = item.children[0].targetHeight + listView.delegateShadowGutter; - delta += targetDelegateHeight - item.height; - } - } - const targetHeight = contentHeight + delta; - // During expansion, always update immediately without threshold check - stableContentHeight = targetHeight; + syncStableContentHeight(true); } else { - __pendingStableHeight = contentHeight; - heightUpdateDebounce.stop(); - stableContentHeight = __pendingStableHeight; + queueStableContentHeightUpdate(false); } } @@ -148,11 +148,14 @@ DankListView { readonly property real adjacentScaleInfluence: isAdjacentToSwipe ? 1.0 - Math.abs(listView.swipingCardOffset) / width * 0.02 : 1.0 readonly property real swipeFadeStartOffset: width * 0.75 readonly property real swipeFadeDistance: Math.max(1, width - swipeFadeStartOffset) + readonly property real nonAnimHeight: notificationCard.targetHeight + listView.delegateShadowGutter Component.onCompleted: { Qt.callLater(() => { - if (delegateRoot) + if (delegateRoot) { delegateRoot.__delegateInitialized = true; + listView.queueStableContentHeightUpdate(listView.isAnimatingExpansion); + } }); } @@ -180,6 +183,7 @@ DankListView { onIsAnimatingChanged: { if (isAnimating) { listView.isAnimatingExpansion = true; + listView.syncStableContentHeight(true); } else { Qt.callLater(() => { if (!notificationCard || !listView) @@ -197,6 +201,13 @@ DankListView { } } + onTargetHeightChanged: { + if (isAnimating || listView.isAnimatingExpansion) + listView.syncStableContentHeight(true); + else + listView.queueStableContentHeightUpdate(false); + } + isGroupSelected: { if (!keyboardController || !keyboardController.keyboardNavigationActive || !listView.keyboardActive) return false; diff --git a/quickshell/Modules/Notifications/Center/NotificationCard.qml b/quickshell/Modules/Notifications/Center/NotificationCard.qml index e83d35e0..8da0b93e 100644 --- a/quickshell/Modules/Notifications/Center/NotificationCard.qml +++ b/quickshell/Modules/Notifications/Center/NotificationCard.qml @@ -16,6 +16,12 @@ Rectangle { property bool userInitiatedExpansion: false property bool isAnimating: false property bool animateExpansion: true + property bool _retainedExpandedContent: false + property bool _clipAnimatedContent: false + property real expandedContentOpacity: expanded ? 1 : 0 + property real collapsedContentOpacity: expanded ? 0 : 1 + readonly property bool renderExpandedContent: expanded || _retainedExpandedContent + readonly property bool renderCollapsedContent: !expanded property bool isGroupSelected: false property int selectedNotificationIndex: -1 @@ -57,6 +63,14 @@ Rectangle { }); } + function expansionMotionDuration() { + return root.connectedFrameMode ? Theme.variantDuration(Theme.popoutAnimationDuration, root.expanded) : (root.expanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration); + } + + function expansionMotionCurve() { + return root.connectedFrameMode ? (root.expanded ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : Theme.expressiveCurves.emphasized; + } + Behavior on scale { enabled: listLevelScaleAnimationsEnabled NumberAnimation { @@ -66,6 +80,7 @@ Rectangle { } Behavior on shadowBlurPx { + enabled: !root.connectedFrameMode NumberAnimation { duration: Theme.shortDuration easing.type: Theme.standardEasing @@ -73,6 +88,7 @@ Rectangle { } Behavior on shadowOffsetXPx { + enabled: !root.connectedFrameMode NumberAnimation { duration: Theme.shortDuration easing.type: Theme.standardEasing @@ -80,6 +96,7 @@ Rectangle { } Behavior on shadowOffsetYPx { + enabled: !root.connectedFrameMode NumberAnimation { duration: Theme.shortDuration easing.type: Theme.standardEasing @@ -94,6 +111,24 @@ Rectangle { } } + Behavior on expandedContentOpacity { + enabled: root.__initialized && root.userInitiatedExpansion && root.animateExpansion + NumberAnimation { + duration: root.expansionMotionDuration() + easing.type: Easing.BezierSpline + easing.bezierCurve: root.expansionMotionCurve() + } + } + + Behavior on collapsedContentOpacity { + enabled: root.__initialized && root.userInitiatedExpansion && root.animateExpansion + NumberAnimation { + duration: root.expansionMotionDuration() + easing.type: Easing.BezierSpline + easing.bezierCurve: root.expansionMotionCurve() + } + } + color: { if (isGroupSelected && keyboardNavigationActive) { return Theme.primaryPressed; @@ -129,7 +164,31 @@ Rectangle { } return 0; } - clip: false + clip: connectedFrameMode && _clipAnimatedContent + + onExpandedChanged: { + if (connectedFrameMode && __initialized && userInitiatedExpansion && animateExpansion) + _clipAnimatedContent = true; + if (expanded) { + _retainedExpandedContent = false; + return; + } + if (connectedFrameMode && __initialized && userInitiatedExpansion && animateExpansion) + _retainedExpandedContent = true; + } + + onHeightChanged: { + if (Math.abs(height - targetHeight) > 0.5) + return; + _clipAnimatedContent = false; + if (!expanded && _retainedExpandedContent) + _retainedExpandedContent = false; + } + + onExpandedContentOpacityChanged: { + if (!expanded && _retainedExpandedContent && expandedContentOpacity <= 0.01) + _retainedExpandedContent = false; + } HoverHandler { id: cardHoverHandler @@ -149,7 +208,7 @@ Rectangle { shadowOffsetX: root.shadowOffsetXPx shadowOffsetY: root.shadowOffsetYPx shadowColor: root.shadowElevation ? Theme.elevationShadowColor(root.shadowElevation) : "transparent" - shadowEnabled: root.shadowsAllowed + shadowEnabled: root.shadowsAllowed && !root.connectedFrameMode } Rectangle { @@ -189,7 +248,8 @@ Rectangle { anchors.leftMargin: Theme.spacingL anchors.rightMargin: Theme.spacingL + Theme.notificationHoverRevealMargin height: collapsedContentHeight + extraHeight - visible: !expanded + visible: renderCollapsedContent + opacity: root.collapsedContentOpacity DankCircularImage { id: iconContainer @@ -388,7 +448,8 @@ Rectangle { anchors.leftMargin: Theme.spacingL anchors.rightMargin: Theme.spacingL spacing: compactMode ? Theme.spacingXS : Theme.spacingS - visible: expanded + visible: renderExpandedContent + opacity: root.expandedContentOpacity Item { width: parent.width @@ -831,7 +892,8 @@ Rectangle { } Row { - visible: !expanded + visible: renderCollapsedContent + opacity: root.collapsedContentOpacity anchors.right: clearButton.visible ? clearButton.left : parent.right anchors.rightMargin: clearButton.visible ? contentSpacing : Theme.spacingL anchors.top: collapsedContent.bottom @@ -887,7 +949,8 @@ Rectangle { property bool isHovered: false readonly property int actionCount: (notificationGroup?.latestNotification?.actions || []).length - visible: !expanded && actionCount < 3 + visible: renderCollapsedContent && actionCount < 3 + opacity: root.collapsedContentOpacity anchors.right: parent.right anchors.rightMargin: Theme.spacingL anchors.top: collapsedContent.bottom @@ -918,7 +981,7 @@ Rectangle { MouseArea { anchors.fill: parent - visible: !expanded && (notificationGroup?.count || 0) > 1 && !descriptionExpanded + visible: renderCollapsedContent && (notificationGroup?.count || 0) > 1 && !descriptionExpanded cursorShape: Qt.PointingHandCursor onClicked: { root.userInitiatedExpansion = true; @@ -962,15 +1025,17 @@ Rectangle { Behavior on height { enabled: root.__initialized && root.userInitiatedExpansion && root.animateExpansion NumberAnimation { - duration: root.connectedFrameMode ? Theme.variantDuration(Theme.popoutAnimationDuration, root.expanded) : (root.expanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration) + duration: root.expansionMotionDuration() easing.type: Easing.BezierSpline - easing.bezierCurve: root.connectedFrameMode ? (root.expanded ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve) : Theme.expressiveCurves.emphasized + easing.bezierCurve: root.expansionMotionCurve() onRunningChanged: { if (running) { root.isAnimating = true; } else { root.isAnimating = false; root.userInitiatedExpansion = false; + root._retainedExpandedContent = false; + root._clipAnimatedContent = false; } } } diff --git a/quickshell/Modules/Notifications/Center/NotificationCenterPopout.qml b/quickshell/Modules/Notifications/Center/NotificationCenterPopout.qml index c2808653..a8f06c7a 100644 --- a/quickshell/Modules/Notifications/Center/NotificationCenterPopout.qml +++ b/quickshell/Modules/Notifications/Center/NotificationCenterPopout.qml @@ -14,6 +14,7 @@ DankPopout { property real stablePopupHeight: 400 property real _lastAlignedContentHeight: -1 property bool _pendingSizedOpen: false + property bool _heightUpdatePending: false function updateStablePopupHeight() { const item = contentLoader.item; @@ -30,6 +31,16 @@ DankPopout { stablePopupHeight = target; } + function queueStablePopupHeightUpdate() { + if (_heightUpdatePending) + return; + _heightUpdatePending = true; + Qt.callLater(() => { + _heightUpdatePending = false; + updateStablePopupHeight(); + }); + } + NotificationKeyboardController { id: keyboardController listView: null @@ -128,7 +139,7 @@ DankPopout { Connections { target: contentLoader.item function onImplicitHeightChanged() { - root.updateStablePopupHeight(); + root.queueStablePopupHeightUpdate(); } } diff --git a/quickshell/Modules/Notifications/Popup/NotificationPopup.qml b/quickshell/Modules/Notifications/Popup/NotificationPopup.qml index e8701df2..6317ec62 100644 --- a/quickshell/Modules/Notifications/Popup/NotificationPopup.qml +++ b/quickshell/Modules/Notifications/Popup/NotificationPopup.qml @@ -537,21 +537,21 @@ PanelWindow { Behavior on shadowBlurPx { NumberAnimation { - duration: Theme.shortDuration + duration: win.descriptionExpanded ? Theme.notificationExpandDuration : Theme.shortDuration easing.type: Theme.standardEasing } } Behavior on shadowOffsetX { NumberAnimation { - duration: Theme.shortDuration + duration: win.descriptionExpanded ? Theme.notificationExpandDuration : Theme.shortDuration easing.type: Theme.standardEasing } } Behavior on shadowOffsetY { NumberAnimation { - duration: Theme.shortDuration + duration: win.descriptionExpanded ? Theme.notificationExpandDuration : Theme.shortDuration easing.type: Theme.standardEasing } } @@ -566,7 +566,7 @@ PanelWindow { shadowOffsetX: content.shadowOffsetX shadowOffsetY: content.shadowOffsetY shadowColor: content.shadowsAllowed && content.elevLevel ? Theme.elevationShadowColor(content.elevLevel) : "transparent" - shadowEnabled: !win._isDestroying && win.screenValid && content.shadowsAllowed + shadowEnabled: !win._isDestroying && win.screenValid && content.shadowsAllowed && !win.connectedFrameMode layer.textureSize: Qt.size(Math.round(width * win.dpr), Math.round(height * win.dpr)) layer.textureMirroring: ShaderEffectSource.MirrorVertically @@ -578,39 +578,39 @@ PanelWindow { sourceRect.radius: win.connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius sourceRect.color: win.connectedFrameMode ? Theme.popupLayerColor(Theme.surfaceContainer) : Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) sourceRect.antialiasing: true - sourceRect.layer.enabled: win.connectedFrameMode - sourceRect.layer.smooth: true - sourceRect.layer.textureSize: win.connectedFrameMode && win.dpr > 1 ? Qt.size(Math.ceil(sourceRect.width * win.dpr), Math.ceil(sourceRect.height * win.dpr)) : Qt.size(0, 0) + sourceRect.layer.enabled: false + sourceRect.layer.textureSize: Qt.size(0, 0) sourceRect.border.color: notificationData && notificationData.urgency === NotificationUrgency.Critical ? Theme.withAlpha(Theme.primary, 0.3) : Theme.withAlpha(Theme.outline, 0.08) sourceRect.border.width: notificationData && notificationData.urgency === NotificationUrgency.Critical ? 2 : 0 + } - Rectangle { - x: bgShadowLayer.sourceRect.x - y: bgShadowLayer.sourceRect.y - width: bgShadowLayer.sourceRect.width - height: bgShadowLayer.sourceRect.height - radius: bgShadowLayer.sourceRect.radius - visible: notificationData && notificationData.urgency === NotificationUrgency.Critical - opacity: 1 - clip: true + // Keep critical accent outside shadow rendering so connected mode still shows it. + Rectangle { + x: content.cardInset + y: content.cardInset + width: Math.max(0, content.width - content.cardInset * 2) + height: Math.max(0, content.height - content.cardInset * 2) + radius: win.connectedFrameMode ? Theme.connectedSurfaceRadius : Theme.cornerRadius + visible: win.notificationData && win.notificationData.urgency === NotificationUrgency.Critical + opacity: 1 + clip: true - gradient: Gradient { - orientation: Gradient.Horizontal + gradient: Gradient { + orientation: Gradient.Horizontal - GradientStop { - position: 0 - color: Theme.primary - } + GradientStop { + position: 0 + color: Theme.primary + } - GradientStop { - position: 0.02 - color: Theme.primary - } + GradientStop { + position: 0.02 + color: Theme.primary + } - GradientStop { - position: 0.021 - color: "transparent" - } + GradientStop { + position: 0.021 + color: "transparent" } } } diff --git a/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml b/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml index 5c9267d5..ec216019 100644 --- a/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml +++ b/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml @@ -36,6 +36,7 @@ QtObject { property var pendingDestroys: [] property int destroyDelayMs: 100 property bool _chromeSyncPending: false + property bool _syncingVisibleNotifications: false readonly property real chromeOpenProgressThreshold: 0.10 readonly property real chromeReleaseTailStart: 0.90 readonly property real chromeReleaseDropProgress: 0.995 @@ -160,6 +161,7 @@ QtObject { function _sync(newWrappers) { let needsReposition = false; + _syncingVisibleNotifications = true; for (const p of popupWindows.slice()) { if (!_isValidWindow(p) || p.exiting) continue; @@ -171,10 +173,10 @@ QtObject { } for (const w of newWrappers) { if (w && !_hasWindowFor(w) && _isFocusedScreen()) { - _insertAtTop(w); - needsReposition = false; + needsReposition = _insertAtTop(w, true) || needsReposition; } } + _syncingVisibleNotifications = false; if (needsReposition) _repositionAll(); } @@ -183,9 +185,9 @@ QtObject { return (p.alignedHeight || p.implicitHeight || (baseNotificationHeight - popupSpacing)) + popupSpacing; } - function _insertAtTop(wrapper) { + function _insertAtTop(wrapper, deferReposition) { if (!wrapper) - return; + return false; const notificationId = wrapper?.notification ? wrapper.notification.id : ""; const win = popupComponent.createObject(null, { "notificationData": wrapper, @@ -194,15 +196,17 @@ QtObject { "screen": manager.modelData }); if (!win) - return; + return false; if (!win.hasValidData) { win.destroy(); - return; + return false; } popupWindows.unshift(win); - _repositionAll(); + if (!deferReposition) + _repositionAll(); if (!sweeper.running) sweeper.start(); + return true; } function _repositionAll() { @@ -321,6 +325,10 @@ QtObject { if (!rect || p !== trailing || !p.popupChromeReleaseProgress) return rect; + // Keep maxed-stack chrome anchored while a replacement tail exits. + if (p.exiting && p.notificationData?.removedByLimit && _layoutWindows().length > 0) + return rect; + const progress = _chromeReleaseTailProgress(p.popupChromeReleaseProgress()); if (progress <= 0) return rect; @@ -489,17 +497,34 @@ QtObject { _scheduleNotificationChromeSync(); } + // Coalesce resize repositioning; exit-path moves remain immediate. + property bool _repositionPending: false + + function _queueReposition() { + if (_repositionPending) + return; + _repositionPending = true; + Qt.callLater(_flushReposition); + } + + function _flushReposition() { + _repositionPending = false; + _repositionAll(); + } + function _onPopupHeightChanged(p) { if (!p || p.exiting || p._isDestroying) return; if (popupWindows.indexOf(p) === -1) return; - _repositionAll(); + _queueReposition(); } function _onPopupExitStarted(p) { if (!p || popupWindows.indexOf(p) === -1) return; + if (_syncingVisibleNotifications) + return; _repositionAll(); } diff --git a/quickshell/Widgets/DankPopout.qml b/quickshell/Widgets/DankPopout.qml index 8eba24a3..798d6bf9 100644 --- a/quickshell/Widgets/DankPopout.qml +++ b/quickshell/Widgets/DankPopout.qml @@ -32,8 +32,6 @@ Item { property bool fullHeightSurface: false property bool _primeContent: false property bool _resizeActive: false - property real _surfaceMarginLeft: 0 - property real _surfaceW: 0 property string _chromeClaimId: "" property int _connectedChromeSerial: 0 property real _chromeAnimTravelX: 1 @@ -49,6 +47,8 @@ Item { "rightBar": 0 }) property var screen: null + // Connected resize uses one full-screen surface; body-sized regions are masks. + readonly property bool useBackgroundWindow: false readonly property real effectiveBarThickness: { if (Theme.isConnectedEffect) @@ -118,12 +118,6 @@ Item { } readonly property string effectiveShadowDirection: Theme.elevationLightDirection === "autoBar" ? autoBarShadowDirection : Theme.elevationLightDirection - // Snapshot mask geometry to prevent background damage on bar updates - property real _frozenMaskX: 0 - property real _frozenMaskY: 0 - property real _frozenMaskWidth: 0 - property real _frozenMaskHeight: 0 - function setBarContext(position, bottomGap) { effectiveBarPosition = position !== undefined ? position : 0; effectiveBarBottomGap = bottomGap !== undefined ? bottomGap : 0; @@ -181,7 +175,7 @@ Item { if (barSide !== "top" && barSide !== "bottom") return contentContainer.animY; - const extent = Math.max(0, root.alignedHeight); + const extent = Math.max(0, root.renderedAlignedHeight); const progress = Math.min(1, Math.abs(contentContainer.animY) / Math.max(1, _chromeAnimTravelY)); const offset = Theme.snap(extent * progress, root.dpr); return contentContainer.animY < 0 ? -offset : offset; @@ -193,9 +187,9 @@ Item { "visible": visible, "barSide": contentContainer.connectedBarSide, "bodyX": root.alignedX, - "bodyY": root.alignedY, + "bodyY": root.renderedAlignedY, "bodyW": root.alignedWidth, - "bodyH": root.alignedHeight, + "bodyH": root.renderedAlignedHeight, "animX": _connectedChromeAnimX(), "animY": _connectedChromeAnimY(), "screen": root.screen ? root.screen.name : "", @@ -258,16 +252,28 @@ Item { ConnectedModeState.setPopoutAnim(_chromeClaimId, syncX ? _connectedChromeAnimX() : undefined, syncY ? _connectedChromeAnimY() : undefined); } + function _syncPopoutBody() { + if (!root.frameOwnsConnectedChrome || !_chromeClaimId) + return; + if (!contentWindow.visible && !shouldBeVisible) + return; + ConnectedModeState.setPopoutBody(_chromeClaimId, root.alignedX, root.renderedAlignedY, root.alignedWidth, root.renderedAlignedHeight); + } + function _flushFullSync() { _fullSyncPending = false; - _syncPopoutChromeState(); + if (root && typeof root._syncPopoutChromeState === "function") + root._syncPopoutChromeState(); } function _queueFullSync() { if (_fullSyncPending) return; _fullSyncPending = true; - Qt.callLater(root._flushFullSync); + Qt.callLater(() => { + if (root && typeof root._flushFullSync === "function") + root._flushFullSync(); + }); } onAlignedXChanged: _queueFullSync() @@ -275,6 +281,8 @@ Item { onAlignedWidthChanged: _queueFullSync() onContentAnimXChanged: _syncPopoutAnim("x") onContentAnimYChanged: _syncPopoutAnim("y") + onRenderedAlignedYChanged: _syncPopoutBody() + onRenderedAlignedHeightChanged: _syncPopoutBody() onScreenChanged: _syncPopoutChromeState() onEffectiveBarPositionChanged: _syncPopoutChromeState() @@ -306,18 +314,10 @@ Item { } } - readonly property bool useBackgroundWindow: !CompositorService.isHyprland || CompositorService.useHyprlandFocusGrab readonly property bool frameOwnsConnectedChrome: SettingsData.connectedFrameModeActive && !!root.screen && SettingsData.isScreenInPreferences(root.screen, SettingsData.frameScreenPreferences) - function updateSurfacePosition() { - if (useBackgroundWindow && shouldBeVisible) { - _surfaceMarginLeft = alignedX - shadowBuffer; - _surfaceW = alignedWidth + shadowBuffer * 2; - } - } - property bool animationsEnabled: true function open() { @@ -328,16 +328,8 @@ Item { animationsEnabled = false; _primeContent = true; - // Snapshot mask geometry - _frozenMaskX = maskX; - _frozenMaskY = maskY; - _frozenMaskWidth = maskWidth; - _frozenMaskHeight = maskHeight; - if (_lastOpenedScreen !== null && _lastOpenedScreen !== screen) { contentWindow.visible = false; - if (useBackgroundWindow) - backgroundWindow.visible = false; } _lastOpenedScreen = screen; @@ -355,19 +347,12 @@ Item { _chromeClaimId = ""; } - if (useBackgroundWindow) { - _surfaceMarginLeft = alignedX - shadowBuffer; - _surfaceW = alignedWidth + shadowBuffer * 2; - backgroundWindow.visible = true; - } contentWindow.visible = true; Qt.callLater(() => { animationsEnabled = true; shouldBeVisible = true; if (shouldBeVisible && screen) { - if (useBackgroundWindow) - backgroundWindow.visible = true; contentWindow.visible = true; PopoutManager.showPopout(root); opened(); @@ -413,8 +398,6 @@ Item { if (!shouldBeVisible) { isClosing = false; contentWindow.visible = false; - if (useBackgroundWindow) - backgroundWindow.visible = false; PopoutManager.hidePopout(root); popoutClosed(); } @@ -536,6 +519,27 @@ 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) + property real renderedAlignedY: alignedY + property real renderedAlignedHeight: alignedHeight + readonly property bool renderedGeometryGrowing: alignedHeight >= renderedAlignedHeight + + Behavior on renderedAlignedY { + enabled: root.animationsEnabled && contentWindow.visible && root.shouldBeVisible + NumberAnimation { + duration: Theme.variantDuration(root.animationDuration, root.renderedGeometryGrowing) + easing.type: Easing.BezierSpline + easing.bezierCurve: root.renderedGeometryGrowing ? root.animationEnterCurve : root.animationExitCurve + } + } + + Behavior on renderedAlignedHeight { + enabled: root.animationsEnabled && contentWindow.visible && root.shouldBeVisible + NumberAnimation { + duration: Theme.variantDuration(root.animationDuration, root.renderedGeometryGrowing) + easing.type: Easing.BezierSpline + easing.bezierCurve: root.renderedGeometryGrowing ? root.animationEnterCurve : root.animationExitCurve + } + } readonly property real connectedAnchorX: { if (!Theme.isConnectedEffect) return triggerX; @@ -572,7 +576,7 @@ Item { } onAlignedHeightChanged: { - _syncPopoutChromeState(); + _queueFullSync(); if (!suspendShadowWhileResizing || !shouldBeVisible) return; _resizeActive = true; @@ -661,117 +665,6 @@ Item { return Math.max(100, screenHeight - maskY - bottomExclusion); } - PanelWindow { - id: backgroundWindow - screen: root.screen - visible: false - color: "transparent" - Component.onCompleted: { - if (typeof updatesEnabled !== "undefined" && !root.overlayContent) - updatesEnabled = false; - } - - WlrLayershell.namespace: root.layerNamespace + ":background" - WlrLayershell.layer: WlrLayershell.Top - WlrLayershell.exclusiveZone: -1 - WlrLayershell.keyboardFocus: WlrKeyboardFocus.None - - anchors { - top: true - left: true - right: true - bottom: true - } - - mask: Region { - item: maskRect - Region { - item: contentExclusionRect - intersection: Intersection.Subtract - } - } - - Rectangle { - id: maskRect - visible: false - color: "transparent" - x: root._frozenMaskX - y: root._frozenMaskY - width: (backgroundWindow.visible && backgroundInteractive) ? root._frozenMaskWidth : 0 - height: (backgroundWindow.visible && backgroundInteractive) ? root._frozenMaskHeight : 0 - } - - Item { - id: contentExclusionRect - visible: false - x: root.alignedX - y: root.alignedY - width: root.alignedWidth - height: root.alignedHeight - } - - Item { - id: outsideClickCatcher - x: root._frozenMaskX - y: root._frozenMaskY - width: root._frozenMaskWidth - height: root._frozenMaskHeight - enabled: root.shouldBeVisible && root.backgroundInteractive - - readonly property real contentLeft: Math.max(0, root.alignedX - x) - readonly property real contentTop: Math.max(0, root.alignedY - y) - readonly property real contentRight: Math.min(width, contentLeft + root.alignedWidth) - readonly property real contentBottom: Math.min(height, contentTop + root.alignedHeight) - - MouseArea { - x: 0 - y: 0 - width: outsideClickCatcher.width - height: Math.max(0, outsideClickCatcher.contentTop) - enabled: parent.enabled - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - onClicked: root.backgroundClicked() - } - - MouseArea { - x: 0 - y: outsideClickCatcher.contentBottom - width: outsideClickCatcher.width - height: Math.max(0, outsideClickCatcher.height - outsideClickCatcher.contentBottom) - enabled: parent.enabled - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - onClicked: root.backgroundClicked() - } - - MouseArea { - x: 0 - y: outsideClickCatcher.contentTop - width: Math.max(0, outsideClickCatcher.contentLeft) - height: Math.max(0, outsideClickCatcher.contentBottom - outsideClickCatcher.contentTop) - enabled: parent.enabled - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - onClicked: root.backgroundClicked() - } - - MouseArea { - x: outsideClickCatcher.contentRight - y: outsideClickCatcher.contentTop - width: Math.max(0, outsideClickCatcher.width - outsideClickCatcher.contentRight) - height: Math.max(0, outsideClickCatcher.contentBottom - outsideClickCatcher.contentTop) - enabled: parent.enabled - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - onClicked: root.backgroundClicked() - } - } - - Loader { - id: overlayLoader - anchors.fill: parent - active: root.overlayContent !== null && backgroundWindow.visible - sourceComponent: root.overlayContent - } - } - PanelWindow { id: contentWindow screen: root.screen @@ -824,27 +717,37 @@ Item { return WlrKeyboardFocus.Exclusive; } - readonly property bool _fullHeight: useBackgroundWindow && root.fullHeightSurface + readonly property bool _fullHeight: root.fullHeightSurface anchors { left: true top: true - right: !useBackgroundWindow - bottom: _fullHeight || !useBackgroundWindow + right: true + bottom: true } WlrLayershell.margins { - left: useBackgroundWindow ? root._surfaceMarginLeft : 0 - top: (useBackgroundWindow && !_fullHeight) ? (root.alignedY - shadowBuffer) : 0 + left: 0 + top: 0 } - implicitWidth: useBackgroundWindow ? root._surfaceW : 0 - implicitHeight: (useBackgroundWindow && !_fullHeight) ? (root.alignedHeight + shadowBuffer * 2) : 0 + implicitWidth: 0 + implicitHeight: 0 - mask: useBackgroundWindow ? contentInputMask : null + mask: contentInputMask Region { id: contentInputMask - item: contentMaskRect + // Outside-click dismissal needs full-screen input only while interactive. + item: (shouldBeVisible && backgroundInteractive) ? fullScreenMaskItem : contentMaskRect + } + + Item { + id: fullScreenMaskItem + visible: false + x: 0 + y: 0 + width: 32767 + height: 32767 } Item { @@ -853,18 +756,18 @@ Item { x: contentContainer.x - contentContainer.horizontalConnectorExtent y: contentContainer.y - contentContainer.verticalConnectorExtent width: root.alignedWidth + contentContainer.horizontalConnectorExtent * 2 - height: root.alignedHeight + contentContainer.verticalConnectorExtent * 2 + height: root.renderedAlignedHeight + contentContainer.verticalConnectorExtent * 2 } MouseArea { anchors.fill: parent - enabled: !useBackgroundWindow && shouldBeVisible && backgroundInteractive + enabled: shouldBeVisible && backgroundInteractive acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton z: -1 onClicked: mouse => { const clickX = mouse.x; const clickY = mouse.y; - const outsideContent = clickX < root.alignedX || clickX > root.alignedX + root.alignedWidth || clickY < root.alignedY || clickY > root.alignedY + root.alignedHeight; + const outsideContent = clickX < root.alignedX || clickX > root.alignedX + root.alignedWidth || clickY < root.renderedAlignedY || clickY > root.renderedAlignedY + root.renderedAlignedHeight; if (!outsideContent) return; backgroundClicked(); @@ -873,10 +776,10 @@ Item { Item { id: contentContainer - x: useBackgroundWindow ? shadowBuffer : root.alignedX - y: (useBackgroundWindow && !contentWindow._fullHeight) ? shadowBuffer : root.alignedY + x: root.alignedX + y: root.renderedAlignedY width: root.alignedWidth - height: root.alignedHeight + height: root.renderedAlignedHeight readonly property bool barTop: effectiveBarPosition === SettingsData.Position.Top readonly property bool barBottom: effectiveBarPosition === SettingsData.Position.Bottom @@ -1082,154 +985,147 @@ Item { return parent.height + clipOversize * 2; } + // Roll-out clips a wrapper while content and shadow keep full-size geometry. Item { - id: aligner + id: rollOutAdjuster readonly property real baseWidth: contentContainer.width readonly property real baseHeight: contentContainer.height readonly property bool isRollOut: typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect - x: (directionalClipMask.x !== 0 ? -directionalClipMask.x : 0) + (isRollOut && contentContainer.barRight ? baseWidth * (1 - contentContainer.scaleValue) : 0) - y: (directionalClipMask.y !== 0 ? -directionalClipMask.y : 0) + (isRollOut && contentContainer.barBottom ? baseHeight * (1 - contentContainer.scaleValue) : 0) + x: directionalClipMask.x !== 0 ? -directionalClipMask.x : 0 + y: directionalClipMask.y !== 0 ? -directionalClipMask.y : 0 width: isRollOut && (contentContainer.barLeft || contentContainer.barRight) ? Math.max(0, baseWidth * contentContainer.scaleValue) : baseWidth height: isRollOut && (contentContainer.barTop || contentContainer.barBottom) ? Math.max(0, baseHeight * contentContainer.scaleValue) : baseHeight clip: isRollOut + ElevationShadow { + id: shadowSource + 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: rollOutAdjuster.baseWidth + readonly property real bodyHeight: rollOutAdjuster.baseHeight + + width: rollOutAdjuster.baseWidth + extraLeft + extraRight + height: rollOutAdjuster.baseHeight + extraTop + extraBottom + opacity: contentWrapper.opacity + scale: contentWrapper.scale + x: contentWrapper.x - extraLeft + y: contentWrapper.y - extraTop + level: root.shadowLevel + direction: root.effectiveShadowDirection + fallbackOffset: root.shadowFallbackOffset + 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) && !root.frameOwnsConnectedChrome + + Item { + anchors.fill: parent + visible: Theme.isConnectedEffect && !root.frameOwnsConnectedChrome + 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 + dpr: root.dpr + 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 + dpr: root.dpr + 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 { - id: unrollCounteract - x: aligner.isRollOut && contentContainer.barRight ? -(aligner.baseWidth * (1 - contentContainer.scaleValue)) : 0 - y: aligner.isRollOut && contentContainer.barBottom ? -(aligner.baseHeight * (1 - contentContainer.scaleValue)) : 0 - width: aligner.baseWidth - height: aligner.baseHeight + id: contentWrapper + width: rollOutAdjuster.baseWidth + height: rollOutAdjuster.baseHeight + opacity: Theme.isDirectionalEffect ? 1 : (shouldBeVisible ? 1 : 0) + visible: opacity > 0 - ElevationShadow { - id: shadowSource - 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 + scale: rollOutAdjuster.isRollOut ? 1.0 : contentContainer.scaleValue + x: Theme.snap(contentContainer.animX + (rollOutAdjuster.baseWidth - width) * (1 - scale) * 0.5, root.dpr) + y: Theme.snap(contentContainer.animY + (rollOutAdjuster.baseHeight - height) * (1 - scale) * 0.5, root.dpr) - width: parent.width + extraLeft + extraRight - height: parent.height + extraTop + extraBottom - opacity: contentWrapper.opacity - scale: contentWrapper.scale - x: contentWrapper.x - extraLeft - y: contentWrapper.y - extraTop - level: root.shadowLevel - direction: root.effectiveShadowDirection - fallbackOffset: root.shadowFallbackOffset - 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) && !root.frameOwnsConnectedChrome + layer.enabled: contentWrapper.opacity < 1 + layer.smooth: false + layer.textureSize: root.dpr > 1 ? Qt.size(Math.ceil(width * root.dpr), Math.ceil(height * root.dpr)) : Qt.size(0, 0) - Item { - anchors.fill: parent - visible: Theme.isConnectedEffect && !root.frameOwnsConnectedChrome - 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 - dpr: root.dpr - 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 - dpr: root.dpr - 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) - } + Behavior on opacity { + enabled: !Theme.isDirectionalEffect + NumberAnimation { + duration: Math.round(Theme.variantDuration(animationDuration, shouldBeVisible) * Theme.variantOpacityDurationScale) + easing.type: Easing.BezierSpline + easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve } } Item { - id: contentWrapper - width: parent.width - height: parent.height - opacity: Theme.isDirectionalEffect ? 1 : (shouldBeVisible ? 1 : 0) - visible: opacity > 0 + anchors.fill: parent + clip: false + visible: !Theme.isConnectedEffect - scale: aligner.isRollOut ? 1.0 : contentContainer.scaleValue - x: Theme.snap(contentContainer.animX + (parent.width - width) * (1 - scale) * 0.5, root.dpr) - y: Theme.snap(contentContainer.animY + (parent.height - height) * (1 - scale) * 0.5, root.dpr) - - layer.enabled: contentWrapper.opacity < 1 - layer.smooth: false - layer.textureSize: root.dpr > 1 ? Qt.size(Math.ceil(width * root.dpr), Math.ceil(height * root.dpr)) : Qt.size(0, 0) - - Behavior on opacity { - enabled: !Theme.isDirectionalEffect - NumberAnimation { - duration: Math.round(Theme.variantDuration(animationDuration, shouldBeVisible) * Theme.variantOpacityDurationScale) - easing.type: Easing.BezierSpline - easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve - } - } - - Item { + Rectangle { anchors.fill: parent - clip: false - visible: !Theme.isConnectedEffect - - Rectangle { - anchors.fill: parent - antialiasing: true - topLeftRadius: contentContainer.surfaceTopLeftRadius - topRightRadius: contentContainer.surfaceTopRightRadius - bottomLeftRadius: contentContainer.surfaceBottomLeftRadius - bottomRightRadius: contentContainer.surfaceBottomRightRadius - color: contentContainer.surfaceColor - border.color: contentContainer.surfaceBorderColor - border.width: contentContainer.surfaceBorderWidth - } + antialiasing: true + topLeftRadius: contentContainer.surfaceTopLeftRadius + topRightRadius: contentContainer.surfaceTopRightRadius + bottomLeftRadius: contentContainer.surfaceBottomLeftRadius + bottomRightRadius: contentContainer.surfaceBottomRightRadius + color: contentContainer.surfaceColor + border.color: contentContainer.surfaceBorderColor + border.width: contentContainer.surfaceBorderWidth } + } - Loader { - id: contentLoader - anchors.fill: parent - active: root._primeContent || shouldBeVisible || contentWindow.visible - asynchronous: false - } - } // closes contentWrapper - } // closes unrollCounteract - } // closes aligner - } // closes directionalClipMask - } // closes contentContainer + Loader { + id: contentLoader + anchors.fill: parent + active: root._primeContent || shouldBeVisible || contentWindow.visible + asynchronous: false + } + } + } + } + } Item { id: focusHelper @@ -1247,5 +1143,12 @@ Item { } } } + + Loader { + id: overlayLoader + anchors.fill: parent + active: root.overlayContent !== null && contentWindow.visible + sourceComponent: root.overlayContent + } } }