From ebe38322a03956b864171a4fdf6986c97befbd34 Mon Sep 17 00:00:00 2001 From: purian23 Date: Fri, 13 Feb 2026 20:09:59 -0500 Subject: [PATCH] Update more m3 baselines & spacing --- quickshell/Common/Theme.qml | 16 + .../Center/HistoryNotificationCard.qml | 67 ++-- .../KeyboardNavigatedNotificationList.qml | 31 +- .../Notifications/Center/NotificationCard.qml | 311 +++++++++++++----- .../Notifications/Popup/NotificationPopup.qml | 66 ++-- .../Popup/NotificationPopupManager.qml | 6 +- 6 files changed, 352 insertions(+), 145 deletions(-) diff --git a/quickshell/Common/Theme.qml b/quickshell/Common/Theme.qml index f905ffc4..d474417a 100644 --- a/quickshell/Common/Theme.qml +++ b/quickshell/Common/Theme.qml @@ -807,6 +807,22 @@ Singleton { return base === 0 ? 0 : Math.round(base * 0.85); } + readonly property real notificationIconSizeNormal: 56 + readonly property real notificationIconSizeCompact: 48 + readonly property real notificationExpandedIconSizeNormal: 48 + readonly property real notificationExpandedIconSizeCompact: 40 + readonly property real notificationActionMinWidth: 48 + readonly property real notificationButtonCornerRadius: cornerRadius / 2 + readonly property real notificationHoverRevealMargin: spacingXL + readonly property real notificationContentSpacing: spacingXS + readonly property real notificationCardPadding: spacingM + readonly property real notificationCardPaddingCompact: spacingS + + readonly property real stateLayerHover: 0.08 + readonly property real stateLayerFocus: 0.12 + readonly property real stateLayerPressed: 0.12 + readonly property real stateLayerDrag: 0.16 + readonly property int popoutAnimationDuration: { if (typeof SettingsData === "undefined") return 150; diff --git a/quickshell/Modules/Notifications/Center/HistoryNotificationCard.qml b/quickshell/Modules/Notifications/Center/HistoryNotificationCard.qml index 9a23a831..7e9e8bcb 100644 --- a/quickshell/Modules/Notifications/Center/HistoryNotificationCard.qml +++ b/quickshell/Modules/Notifications/Center/HistoryNotificationCard.qml @@ -21,8 +21,8 @@ Rectangle { } readonly property bool compactMode: SettingsData.notificationCompactMode - readonly property real cardPadding: compactMode ? Theme.spacingS : Theme.spacingM - readonly property real iconSize: compactMode ? 48 : 63 + readonly property real cardPadding: compactMode ? Theme.notificationCardPaddingCompact : Theme.notificationCardPadding + readonly property real iconSize: compactMode ? Theme.notificationIconSizeCompact : Theme.notificationIconSizeNormal readonly property real contentSpacing: compactMode ? Theme.spacingXS : Theme.spacingS readonly property real collapsedContentHeight: iconSize + cardPadding readonly property real baseCardHeight: cardPadding * 2 + collapsedContentHeight @@ -93,7 +93,7 @@ Rectangle { anchors.right: parent.right anchors.topMargin: cardPadding anchors.leftMargin: Theme.spacingL - anchors.rightMargin: Theme.spacingL + (compactMode ? 32 : 40) + anchors.rightMargin: Theme.spacingL + Theme.notificationHoverRevealMargin height: collapsedContentHeight + extraHeight DankCircularImage { @@ -165,32 +165,47 @@ Rectangle { Column { width: parent.width anchors.top: parent.top - spacing: compactMode ? 1 : 2 + spacing: Theme.notificationContentSpacing - StyledText { + Row { width: parent.width - text: { - const timeStr = NotificationService.formatHistoryTime(historyItem.timestamp); - const appName = historyItem.appName || ""; - return timeStr.length > 0 ? `${appName} • ${timeStr}` : appName; + spacing: Theme.spacingXS + readonly property real reservedTrailingWidth: historySeparator.implicitWidth + Math.max(historyTimeText.implicitWidth, 72) + spacing + + StyledText { + id: historyTitleText + width: Math.min(implicitWidth, Math.max(0, parent.width - parent.reservedTrailingWidth)) + text: { + let title = historyItem.summary || ""; + const appName = historyItem.appName || ""; + const prefix = appName + " • "; + if (appName && title.toLowerCase().startsWith(prefix.toLowerCase())) { + title = title.substring(prefix.length); + } + return title; + } + color: Theme.surfaceText + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + elide: Text.ElideRight + maximumLineCount: 1 + visible: text.length > 0 + } + StyledText { + id: historySeparator + text: (historyTitleText.text.length > 0 && historyTimeText.text.length > 0) ? " • " : "" + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Normal + } + StyledText { + id: historyTimeText + text: NotificationService.formatHistoryTime(historyItem.timestamp) + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Normal + visible: text.length > 0 } - color: Theme.surfaceVariantText - font.pixelSize: Theme.fontSizeSmall - font.weight: Font.Medium - elide: Text.ElideRight - maximumLineCount: 1 - visible: text.length > 0 - } - - StyledText { - text: historyItem.summary || "" - color: Theme.surfaceText - font.pixelSize: Theme.fontSizeMedium - font.weight: Font.Medium - width: parent.width - elide: Text.ElideRight - maximumLineCount: 1 - visible: text.length > 0 } StyledText { diff --git a/quickshell/Modules/Notifications/Center/KeyboardNavigatedNotificationList.qml b/quickshell/Modules/Notifications/Center/KeyboardNavigatedNotificationList.qml index fb9ee7ba..99a03aae 100644 --- a/quickshell/Modules/Notifications/Center/KeyboardNavigatedNotificationList.qml +++ b/quickshell/Modules/Notifications/Center/KeyboardNavigatedNotificationList.qml @@ -14,6 +14,8 @@ DankListView { property real stableContentHeight: 0 property bool cardAnimateExpansion: true property bool listInitialized: false + property real __pendingStableHeight: 0 + property real __heightUpdateThreshold: 20 Component.onCompleted: { Qt.callLater(() => { @@ -24,22 +26,43 @@ DankListView { }); } + 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; + } + } + } + onContentHeightChanged: { - if (!isAnimatingExpansion) - stableContentHeight = contentHeight; + if (!isAnimatingExpansion) { + __pendingStableHeight = contentHeight; + if (Math.abs(contentHeight - stableContentHeight) > __heightUpdateThreshold) { + heightUpdateDebounce.restart(); + } else { + stableContentHeight = contentHeight; + } + } } 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) delta += item.children[0].targetHeight - item.height; } - stableContentHeight = contentHeight + delta; + const targetHeight = contentHeight + delta; + // During expansion, always update immediately without threshold check + stableContentHeight = targetHeight; } else { - stableContentHeight = contentHeight; + __pendingStableHeight = contentHeight; + heightUpdateDebounce.restart(); } } diff --git a/quickshell/Modules/Notifications/Center/NotificationCard.qml b/quickshell/Modules/Notifications/Center/NotificationCard.qml index af526421..83d0130c 100644 --- a/quickshell/Modules/Notifications/Center/NotificationCard.qml +++ b/quickshell/Modules/Notifications/Center/NotificationCard.qml @@ -21,12 +21,12 @@ Rectangle { property bool keyboardNavigationActive: false readonly property bool compactMode: SettingsData.notificationCompactMode - readonly property real cardPadding: compactMode ? Theme.spacingS : Theme.spacingM - readonly property real iconSize: compactMode ? 48 : 63 + readonly property real cardPadding: compactMode ? Theme.notificationCardPaddingCompact : Theme.notificationCardPadding + readonly property real iconSize: compactMode ? Theme.notificationIconSizeCompact : Theme.notificationIconSizeNormal readonly property real contentSpacing: compactMode ? Theme.spacingXS : Theme.spacingS readonly property real badgeSize: compactMode ? 16 : 18 readonly property real actionButtonHeight: compactMode ? 20 : 24 - readonly property real collapsedContentHeight: iconSize + readonly property real collapsedContentHeight: Math.max(iconSize, Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 3)) readonly property real baseCardHeight: cardPadding * 2 + collapsedContentHeight + actionButtonHeight + contentSpacing width: parent ? parent.width : 400 @@ -85,6 +85,10 @@ Rectangle { } clip: true + HoverHandler { + id: cardHoverHandler + } + Rectangle { anchors.fill: parent radius: parent.radius @@ -111,15 +115,16 @@ Rectangle { id: collapsedContent readonly property real expandedTextHeight: descriptionText.contentHeight - readonly property real twoLineHeight: descriptionText.font.pixelSize * 1.2 * 2 - readonly property real extraHeight: (descriptionExpanded && expandedTextHeight > twoLineHeight + 2) ? (expandedTextHeight - twoLineHeight) : 0 + readonly property real collapsedLineCount: compactMode ? 1 : 3 + readonly property real collapsedLineHeight: descriptionText.font.pixelSize * 1.2 * collapsedLineCount + readonly property real extraHeight: (descriptionExpanded && expandedTextHeight > collapsedLineHeight + 2) ? (expandedTextHeight - collapsedLineHeight) : 0 anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right anchors.topMargin: cardPadding anchors.leftMargin: Theme.spacingL - anchors.rightMargin: Theme.spacingL + (compactMode ? 32 : 40) + anchors.rightMargin: Theme.spacingL + ((cardHoverHandler.hovered || (keyboardNavigationActive && (isGroupSelected || (expanded && selectedNotificationIndex >= 0)))) ? Theme.notificationHoverRevealMargin : 0) height: collapsedContentHeight + extraHeight visible: !expanded @@ -141,6 +146,9 @@ Rectangle { height: iconSize anchors.left: parent.left anchors.top: parent.top + anchors.topMargin: descriptionExpanded + ? Math.max(0, (Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 3)) / 2 - iconSize / 2) + : Math.max(0, textContainer.height / 2 - iconSize / 2) imageSource: { if (hasNotificationImage) @@ -214,28 +222,48 @@ Rectangle { Column { width: parent.width anchors.top: parent.top - spacing: compactMode ? 1 : 2 + spacing: Theme.notificationContentSpacing - StyledText { - text: (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.summary) || "" - color: Theme.surfaceText - font.pixelSize: Theme.fontSizeMedium - font.weight: Font.Medium + Row { width: parent.width - ((notificationGroup?.count || 0) > 1 ? 10 : 0) - elide: Text.ElideRight - maximumLineCount: 1 - visible: text.length > 0 - } + spacing: Theme.spacingXS + visible: (collapsedTitleText.text.length > 0 || collapsedTimeText.text.length > 0) + readonly property real reservedTrailingWidth: collapsedSeparator.implicitWidth + Math.max(collapsedTimeText.implicitWidth, 72) + spacing - StyledText { - width: parent.width - text: (notificationGroup && notificationGroup.latestNotification && notificationGroup.latestNotification.timeStr) || "" - color: Theme.surfaceVariantText - font.pixelSize: Theme.fontSizeSmall - font.weight: Font.Medium - elide: Text.ElideRight - maximumLineCount: 1 - visible: text.length > 0 + StyledText { + id: collapsedTitleText + width: Math.min(implicitWidth, Math.max(0, parent.width - parent.reservedTrailingWidth)) + text: { + let title = notificationGroup?.latestNotification?.summary || ""; + const appName = notificationGroup?.appName || ""; + const prefix = appName + " • "; + if (appName && title.toLowerCase().startsWith(prefix.toLowerCase())) { + title = title.substring(prefix.length); + } + return title; + } + color: Theme.surfaceText + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + elide: Text.ElideRight + maximumLineCount: 1 + visible: text.length > 0 + } + StyledText { + id: collapsedSeparator + text: (collapsedTitleText.text.length > 0 && collapsedTimeText.text.length > 0) ? " • " : "" + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Normal + } + StyledText { + id: collapsedTimeText + text: notificationGroup?.latestNotification?.timeStr || "" + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Normal + visible: text.length > 0 + } } StyledText { @@ -248,7 +276,7 @@ Rectangle { font.pixelSize: Theme.fontSizeSmall width: parent.width elide: Text.ElideRight - maximumLineCount: descriptionExpanded ? -1 : (compactMode ? 1 : 2) + maximumLineCount: descriptionExpanded ? -1 : (compactMode ? 1 : 3) wrapMode: Text.WordWrap visible: text.length > 0 linkColor: Theme.primary @@ -299,7 +327,7 @@ Rectangle { Row { anchors.left: parent.left anchors.right: parent.right - anchors.rightMargin: Theme.spacingL + (compactMode ? 32 : 40) + anchors.rightMargin: Theme.spacingL + ((cardHoverHandler.hovered || (keyboardNavigationActive && (isGroupSelected || (expanded && selectedNotificationIndex >= 0)))) ? Theme.notificationHoverRevealMargin : 0) anchors.verticalCenter: parent.verticalCenter spacing: Theme.spacingS @@ -341,51 +369,74 @@ Rectangle { objectName: "notificationRepeater" model: notificationGroup?.notifications?.slice(0, 10) || [] - delegate: Rectangle { - id: expandedDelegate + delegate: Item { + id: expandedDelegateWrapper required property var modelData required property int index readonly property bool messageExpanded: NotificationService.expandedMessages[modelData?.notification?.id] || false readonly property bool isSelected: root.selectedNotificationIndex === index - readonly property real expandedIconSize: compactMode ? 40 : 48 + readonly property bool actionsVisible: expandedDelegateHoverHandler.hovered || (root.keyboardNavigationActive && isSelected) + readonly property real expandedIconSize: compactMode ? Theme.notificationExpandedIconSizeCompact : Theme.notificationExpandedIconSizeNormal + + HoverHandler { + id: expandedDelegateHoverHandler + } readonly property real expandedItemPadding: compactMode ? Theme.spacingS : Theme.spacingM readonly property real expandedBaseHeight: expandedItemPadding * 2 + expandedIconSize + actionButtonHeight + contentSpacing * 2 property bool __delegateInitialized: false + property real swipeOffset: 0 + property bool isDismissing: false + readonly property real dismissThreshold: width * 0.35 Component.onCompleted: { Qt.callLater(() => { - if (expandedDelegate) - expandedDelegate.__delegateInitialized = true; + if (expandedDelegateWrapper) + expandedDelegateWrapper.__delegateInitialized = true; }); } width: parent.width - height: { - if (!messageExpanded) - return expandedBaseHeight; - const twoLineHeight = bodyText.font.pixelSize * 1.2 * 2; - if (bodyText.implicitHeight > twoLineHeight + 2) - return expandedBaseHeight + bodyText.implicitHeight - twoLineHeight; - return expandedBaseHeight; - } - radius: Theme.cornerRadius - color: isSelected ? Theme.primaryPressed : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) - border.color: isSelected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4) : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05) - border.width: 1 + height: delegateRect.height + clip: true - Behavior on border.color { - enabled: __delegateInitialized - ColorAnimation { - duration: __delegateInitialized ? Theme.shortDuration : 0 - easing.type: Theme.standardEasing + Rectangle { + id: delegateRect + x: parent.swipeOffset + width: parent.width + + Behavior on x { + enabled: !expandedSwipeHandler.active && !parent.parent.isDismissing + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } } - } + height: { + if (!messageExpanded) + return expandedBaseHeight; + const twoLineHeight = bodyText.font.pixelSize * 1.2 * 2; + if (bodyText.implicitHeight > twoLineHeight + 2) + return expandedBaseHeight + bodyText.implicitHeight - twoLineHeight; + return expandedBaseHeight; + } + radius: Theme.cornerRadius + color: isSelected ? Theme.primaryPressed : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) + border.color: isSelected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.4) : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.05) + border.width: 1 - Behavior on height { - enabled: false - } + Behavior on border.color { + enabled: __delegateInitialized + ColorAnimation { + duration: __delegateInitialized ? Theme.shortDuration : 0 + easing.type: Theme.standardEasing + } + } - Item { + Behavior on height { + enabled: false + } + + Item { anchors.fill: parent anchors.margins: compactMode ? Theme.spacingS : Theme.spacingM anchors.bottomMargin: contentSpacing @@ -452,28 +503,47 @@ Rectangle { anchors.top: parent.top anchors.bottom: buttonArea.top anchors.bottomMargin: contentSpacing - spacing: compactMode ? 1 : 2 + spacing: Theme.notificationContentSpacing - StyledText { + Row { width: parent.width - text: modelData?.timeStr || "" - color: Theme.surfaceVariantText - font.pixelSize: Theme.fontSizeSmall - font.weight: Font.Medium - elide: Text.ElideRight - maximumLineCount: 1 - visible: text.length > 0 - } + spacing: Theme.spacingXS + readonly property real reservedTrailingWidth: expandedDelegateSeparator.implicitWidth + Math.max(expandedDelegateTimeText.implicitWidth, 72) + spacing - StyledText { - width: parent.width - text: modelData?.summary || "" - color: Theme.surfaceText - font.pixelSize: Theme.fontSizeMedium - font.weight: Font.Medium - elide: Text.ElideRight - maximumLineCount: 1 - visible: text.length > 0 + StyledText { + id: expandedDelegateTitleText + width: Math.min(implicitWidth, Math.max(0, parent.width - parent.reservedTrailingWidth)) + text: { + let title = modelData?.summary || ""; + const appName = modelData?.appName || ""; + const prefix = appName + " • "; + if (appName && title.toLowerCase().startsWith(prefix.toLowerCase())) { + title = title.substring(prefix.length); + } + return title; + } + color: Theme.surfaceText + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + elide: Text.ElideRight + maximumLineCount: 1 + visible: text.length > 0 + } + StyledText { + id: expandedDelegateSeparator + text: (expandedDelegateTitleText.text.length > 0 && expandedDelegateTimeText.text.length > 0) ? " • " : "" + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Normal + } + StyledText { + id: expandedDelegateTimeText + text: modelData?.timeStr || "" + color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) + font.pixelSize: Theme.fontSizeSmall + font.weight: Font.Normal + visible: text.length > 0 + } } StyledText { @@ -523,20 +593,26 @@ Rectangle { height: actionButtonHeight + contentSpacing Row { + visible: expandedDelegateWrapper.actionsVisible + opacity: visible ? 1 : 0 anchors.right: parent.right anchors.bottom: parent.bottom spacing: contentSpacing + Behavior on opacity { + NumberAnimation { duration: Theme.shortDuration; easing.type: Theme.standardEasing } + } + Repeater { model: modelData?.actions || [] Rectangle { property bool isHovered: false - width: Math.max(expandedActionText.implicitWidth + Theme.spacingM, compactMode ? 40 : 50) + width: Math.max(expandedActionText.implicitWidth + Theme.spacingM, Theme.notificationActionMinWidth) height: actionButtonHeight - radius: Theme.spacingXS - color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent" + radius: Theme.notificationButtonCornerRadius + color: isHovered ? Theme.withAlpha(Theme.primary, Theme.stateLayerHover) : "transparent" StyledText { id: expandedActionText @@ -568,12 +644,19 @@ Rectangle { } Rectangle { + id: expandedDelegateDismissBtn property bool isHovered: false - width: Math.max(expandedClearText.implicitWidth + Theme.spacingM, compactMode ? 40 : 50) + visible: expandedDelegateWrapper.actionsVisible + opacity: visible ? 1 : 0 + width: Math.max(expandedClearText.implicitWidth + Theme.spacingM, Theme.notificationActionMinWidth) height: actionButtonHeight - radius: Theme.spacingXS - color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent" + radius: Theme.notificationButtonCornerRadius + color: isHovered ? Theme.withAlpha(Theme.primary, Theme.stateLayerHover) : "transparent" + + Behavior on opacity { + NumberAnimation { duration: Theme.shortDuration; easing.type: Theme.standardEasing } + } StyledText { id: expandedClearText @@ -598,28 +681,69 @@ Rectangle { } } } + } + } + + DragHandler { + id: expandedSwipeHandler + target: null + xAxis.enabled: true + yAxis.enabled: false + grabPermissions: PointerHandler.CanTakeOverFromItems | PointerHandler.CanTakeOverFromHandlersOfDifferentType + + onActiveChanged: { + if (active || parent.isDismissing) + return; + if (Math.abs(parent.swipeOffset) > parent.dismissThreshold) { + parent.isDismissing = true; + expandedSwipeDismissAnim.start(); + } else { + parent.swipeOffset = 0; + } + } + + onTranslationChanged: { + if (parent.isDismissing) + return; + parent.swipeOffset = translation.x; + } + } + + NumberAnimation { + id: expandedSwipeDismissAnim + target: parent + property: "swipeOffset" + to: parent.swipeOffset > 0 ? parent.width : -parent.width + duration: Theme.shortDuration + easing.type: Easing.OutCubic + onStopped: NotificationService.dismissNotification(modelData) } } } Row { - visible: !expanded + visible: !expanded && (cardHoverHandler.hovered || (keyboardNavigationActive && isGroupSelected)) + opacity: visible ? 1 : 0 anchors.right: clearButton.visible ? clearButton.left : parent.right anchors.rightMargin: clearButton.visible ? contentSpacing : Theme.spacingL anchors.top: collapsedContent.bottom anchors.topMargin: contentSpacing spacing: contentSpacing + Behavior on opacity { + NumberAnimation { duration: Theme.shortDuration; easing.type: Theme.standardEasing } + } + Repeater { model: notificationGroup?.latestNotification?.actions || [] Rectangle { property bool isHovered: false - width: Math.max(collapsedActionText.implicitWidth + Theme.spacingM, compactMode ? 40 : 50) + width: Math.max(collapsedActionText.implicitWidth + Theme.spacingM, Theme.notificationActionMinWidth) height: actionButtonHeight - radius: Theme.spacingXS - color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent" + radius: Theme.notificationButtonCornerRadius + color: isHovered ? Theme.withAlpha(Theme.primary, Theme.stateLayerHover) : "transparent" StyledText { id: collapsedActionText @@ -659,15 +783,19 @@ Rectangle { property bool isHovered: false readonly property int actionCount: (notificationGroup?.latestNotification?.actions || []).length - visible: !expanded && actionCount < 3 + visible: !expanded && actionCount < 3 && (cardHoverHandler.hovered || (keyboardNavigationActive && isGroupSelected)) + opacity: visible ? 1 : 0 + Behavior on opacity { + NumberAnimation { duration: Theme.shortDuration; easing.type: Theme.standardEasing } + } anchors.right: parent.right anchors.rightMargin: Theme.spacingL anchors.top: collapsedContent.bottom anchors.topMargin: contentSpacing - width: Math.max(collapsedClearText.implicitWidth + Theme.spacingM, compactMode ? 40 : 50) + width: Math.max(collapsedClearText.implicitWidth + Theme.spacingM, Theme.notificationActionMinWidth) height: actionButtonHeight - radius: Theme.spacingXS - color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent" + radius: Theme.notificationButtonCornerRadius + color: isHovered ? Theme.withAlpha(Theme.primary, Theme.stateLayerHover) : "transparent" StyledText { id: collapsedClearText @@ -706,6 +834,15 @@ Rectangle { anchors.rightMargin: Theme.spacingL width: compactMode ? 52 : 60 height: compactMode ? 24 : 28 + opacity: (cardHoverHandler.hovered || (keyboardNavigationActive && (isGroupSelected || (expanded && selectedNotificationIndex >= 0)))) ? 1 : 0 + visible: opacity > 0 + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } DankActionButton { anchors.left: parent.left diff --git a/quickshell/Modules/Notifications/Popup/NotificationPopup.qml b/quickshell/Modules/Notifications/Popup/NotificationPopup.qml index cae4e89b..e09515de 100644 --- a/quickshell/Modules/Notifications/Popup/NotificationPopup.qml +++ b/quickshell/Modules/Notifications/Popup/NotificationPopup.qml @@ -24,12 +24,12 @@ PanelWindow { property bool descriptionExpanded: false readonly property bool compactMode: SettingsData.notificationCompactMode - readonly property real cardPadding: compactMode ? Theme.spacingS : Theme.spacingM - readonly property real popupIconSize: compactMode ? 48 : 63 + readonly property real cardPadding: compactMode ? Theme.notificationCardPaddingCompact : Theme.notificationCardPadding + readonly property real popupIconSize: compactMode ? Theme.notificationIconSizeCompact : Theme.notificationIconSizeNormal readonly property real contentSpacing: compactMode ? Theme.spacingXS : Theme.spacingS readonly property real actionButtonHeight: compactMode ? 20 : 24 - readonly property real collapsedContentHeight: popupIconSize + (compactMode ? 0 : Theme.fontSizeSmall * 1.2) - readonly property real basePopupHeight: cardPadding * 2 + collapsedContentHeight + actionButtonHeight + Theme.spacingS + readonly property real collapsedContentHeight: Math.max(popupIconSize, Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 3)) + readonly property real basePopupHeight: cardPadding * 2 + collapsedContentHeight + actionButtonHeight + contentSpacing signal entered signal exitStarted @@ -310,6 +310,10 @@ PanelWindow { anchors.margins: Theme.snap(4, win.dpr) clip: true + HoverHandler { + id: cardHoverHandler + } + LayoutMirroring.enabled: I18n.isRtl LayoutMirroring.childrenInherit: true @@ -325,7 +329,7 @@ PanelWindow { anchors.right: parent.right anchors.topMargin: cardPadding anchors.leftMargin: Theme.spacingL - anchors.rightMargin: Theme.spacingL + (compactMode ? 32 : 40) + anchors.rightMargin: Theme.spacingL + (cardHoverHandler.hovered ? Theme.notificationHoverRevealMargin : 0) height: collapsedContentHeight + extraHeight DankCircularImage { @@ -348,6 +352,9 @@ PanelWindow { height: popupIconSize anchors.left: parent.left anchors.top: parent.top + anchors.topMargin: descriptionExpanded + ? Math.max(0, (Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 3)) / 2 - popupIconSize / 2) + : Math.max(0, textContainer.height / 2 - popupIconSize / 2) imageSource: { if (!notificationData) @@ -401,7 +408,7 @@ PanelWindow { anchors.leftMargin: Theme.spacingM anchors.right: parent.right anchors.top: parent.top - spacing: compactMode ? 1 : 2 + spacing: Theme.notificationContentSpacing StyledText { text: notificationData ? (notificationData.summary || "") : "" @@ -415,18 +422,6 @@ PanelWindow { visible: text.length > 0 } - StyledText { - width: parent.width - text: notificationData ? (notificationData.timeStr || "") : "" - color: Theme.surfaceVariantText - font.pixelSize: Theme.fontSizeSmall - font.weight: Font.Medium - elide: Text.ElideRight - horizontalAlignment: Text.AlignLeft - maximumLineCount: 1 - visible: text.length > 0 - } - StyledText { id: bodyText property bool hasMoreText: truncated @@ -477,6 +472,17 @@ PanelWindow { iconSize: compactMode ? 16 : 18 buttonSize: compactMode ? 24 : 28 z: 15 + opacity: cardHoverHandler.hovered ? 1 : 0 + visible: opacity > 0 + enabled: cardHoverHandler.hovered + + Behavior on opacity { + NumberAnimation { + duration: Theme.shortDuration + easing.type: Theme.standardEasing + } + } + onClicked: { if (notificationData && !win.exiting) notificationData.popup = false; @@ -484,6 +490,8 @@ PanelWindow { } Row { + visible: cardHoverHandler.hovered + opacity: visible ? 1 : 0 anchors.right: clearButton.visible ? clearButton.left : parent.right anchors.rightMargin: clearButton.visible ? contentSpacing : Theme.spacingL anchors.top: notificationContent.bottom @@ -491,16 +499,20 @@ PanelWindow { spacing: contentSpacing z: 20 + Behavior on opacity { + NumberAnimation { duration: Theme.shortDuration; easing.type: Theme.standardEasing } + } + Repeater { model: notificationData ? (notificationData.actions || []) : [] Rectangle { property bool isHovered: false - width: Math.max(actionText.implicitWidth + Theme.spacingM, compactMode ? 40 : 50) + width: Math.max(actionText.implicitWidth + Theme.spacingM, Theme.notificationActionMinWidth) height: actionButtonHeight - radius: Theme.spacingXS - color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent" + radius: Theme.notificationButtonCornerRadius + color: isHovered ? Theme.withAlpha(Theme.primary, Theme.stateLayerHover) : "transparent" StyledText { id: actionText @@ -537,15 +549,19 @@ PanelWindow { property bool isHovered: false readonly property int actionCount: notificationData ? (notificationData.actions || []).length : 0 - visible: actionCount < 3 + visible: actionCount < 3 && cardHoverHandler.hovered + opacity: visible ? 1 : 0 + Behavior on opacity { + NumberAnimation { duration: Theme.shortDuration; easing.type: Theme.standardEasing } + } anchors.right: parent.right anchors.rightMargin: Theme.spacingL anchors.top: notificationContent.bottom anchors.topMargin: contentSpacing - width: Math.max(clearTextLabel.implicitWidth + Theme.spacingM, compactMode ? 40 : 50) + width: Math.max(clearTextLabel.implicitWidth + Theme.spacingM, Theme.notificationActionMinWidth) height: actionButtonHeight - radius: Theme.spacingXS - color: isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1) : "transparent" + radius: Theme.notificationButtonCornerRadius + color: isHovered ? Theme.withAlpha(Theme.primary, Theme.stateLayerHover) : "transparent" z: 20 StyledText { diff --git a/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml b/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml index cb98880e..a26eb6b2 100644 --- a/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml +++ b/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml @@ -8,10 +8,10 @@ QtObject { property var modelData property int topMargin: 0 readonly property bool compactMode: SettingsData.notificationCompactMode - readonly property real cardPadding: compactMode ? Theme.spacingS : Theme.spacingM - readonly property real popupIconSize: compactMode ? 48 : 63 + readonly property real cardPadding: compactMode ? Theme.notificationCardPaddingCompact : Theme.notificationCardPadding + readonly property real popupIconSize: compactMode ? Theme.notificationIconSizeCompact : Theme.notificationIconSizeNormal readonly property real actionButtonHeight: compactMode ? 20 : 24 - readonly property real popupSpacing: 8 + readonly property real popupSpacing: Theme.spacingXS readonly property int baseNotificationHeight: cardPadding * 2 + popupIconSize + actionButtonHeight + Theme.spacingS + popupSpacing property int maxTargetNotifications: 4 property var popupWindows: [] // strong refs to windows (live until exitFinished)