import QtQuick import QtQuick.Effects import Quickshell import Quickshell.Wayland import Quickshell.Services.Notifications import "." as M PanelWindow { id: root required property var screen visible: M.NotifService.popups.length > 0 color: "transparent" WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.exclusiveZone: 0 WlrLayershell.namespace: "nova-notif-popup" anchors.top: true anchors.right: true margins.top: 0 margins.right: 8 implicitWidth: 320 implicitHeight: popupCol.implicitHeight Column { id: popupCol width: parent.width spacing: 6 property var _knownIds: ({}) Repeater { model: M.NotifService.popups.slice(0, M.Modules.notifications.maxPopups || 4) delegate: Item { id: popupItem required property var modelData required property int index width: popupCol.width height: _targetHeight * _heightScale opacity: 0 x: 320 clip: true readonly property real _targetHeight: _card.implicitHeight property real _heightScale: 0 // Glow on critical — layer effect composites everything including NotifCard layer.enabled: popupItem.modelData.urgency === NotificationUrgency.Critical layer.effect: MultiEffect { shadowEnabled: true shadowColor: M.Theme.base08 shadowBlur: 0.6 shadowVerticalOffset: 0 shadowHorizontalOffset: 0 } M.NotifCard { id: _card anchors.fill: parent notif: popupItem.modelData showAppName: true iconSize: 36 bodyMaxLines: 3 onDismissRequested: popupItem.animateDismiss(true) } property bool _entered: false Component.onCompleted: { if (popupCol._knownIds[modelData.id]) { opacity = 1; x = 0; _heightScale = 1; } else { popupCol._knownIds[modelData.id] = true; slideIn.start(); } _entered = true; } Connections { target: M.NotifService function onPopupExpiring(notifId) { if (notifId === popupItem.modelData.id) popupItem.animateDismiss(false); } } Behavior on y { enabled: popupItem._entered NumberAnimation { duration: 200 easing.type: Easing.OutCubic } } ParallelAnimation { id: slideIn NumberAnimation { target: popupItem property: "opacity" to: 1 duration: 250 easing.type: Easing.OutCubic } NumberAnimation { target: popupItem property: "x" to: 0 duration: 350 easing.type: Easing.OutBack easing.overshoot: 0.5 } NumberAnimation { target: popupItem property: "_heightScale" to: 1 duration: 250 easing.type: Easing.OutCubic } } property bool _fullDismiss: false function animateDismiss(full) { if (popupItem.modelData.state === "dismissing") return; popupItem.modelData.beginDismiss(); _fullDismiss = !!full; slideIn.stop(); slideOut.start(); } SequentialAnimation { id: slideOut ParallelAnimation { NumberAnimation { target: popupItem property: "x" to: 400 duration: 250 easing.type: Easing.InCubic } NumberAnimation { target: popupItem property: "opacity" to: 0 duration: 250 easing.type: Easing.InCubic } } NumberAnimation { target: popupItem property: "_heightScale" to: 0 duration: 200 easing.type: Easing.OutCubic } ScriptAction { script: popupItem._fullDismiss ? M.NotifService.dismiss(popupItem.modelData.id) : M.NotifService.dismissPopup(popupItem.modelData.id) } } // Click anywhere to dismiss (left = popup only, right = full dismiss) MouseArea { anchors.fill: parent z: -1 acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: mouse => popupItem.animateDismiss(mouse.button === Qt.RightButton) } } } } }