pragma ComponentBehavior: Bound import Quickshell import Quickshell.Services.Notifications import Quickshell.Widgets import QtQuick import QtQuick.Layouts import ".." // Notification toast overlay. // Toasts slide in from the right, stack downward, and auto-dismiss. // Urgency colouring: Low → fgDim stripe, Normal → accent stripe, Critical → critical border + stripe. PanelWindow { id: root anchors { top: true right: true } exclusionMode: ExclusionMode.Ignore color: "transparent" implicitWidth: 356 // 340 toast + 8 left + 8 right margin implicitHeight: Math.max(1, (clearAllBtn.visible ? clearAllBtn.height + 6 : 0) + notifList.contentHeight + 16) // ── Notification server ────────────────────────────────────────────────── NotificationServer { id: server onNotification: function (notif) { notifModel.insert(0, { "nid": notif.id, "app": notif.appName, "icon": notif.appIcon, "summary": notif.summary !== "" ? notif.summary : notif.appName, "body": notif.body, "urgency": notif.urgency }); } } ListModel { id: notifModel } function removeById(nid) { for (let i = 0; i < notifModel.count; i++) { if (notifModel.get(i).nid === nid) { notifModel.remove(i); return; } } } // ── Toast list ─────────────────────────────────────────────────────────── // ── Clear all button (visible when 2+ notifications) ──────────────────── Rectangle { id: clearAllBtn visible: notifModel.count >= 2 anchors { top: parent.top; right: parent.right; topMargin: 8; rightMargin: 8 } width: 340 height: 26 color: clearHover.hovered ? Colors.surface : Colors.bgDark border.color: Colors.surface border.width: 1 radius: 6 Text { anchors.centerIn: parent text: "Clear all (" + notifModel.count + ")" color: Colors.fgDim font.pixelSize: 12 } HoverHandler { id: clearHover } TapHandler { onTapped: notifModel.clear() } } ListView { id: notifList anchors { top: clearAllBtn.visible ? clearAllBtn.bottom : parent.top right: parent.right topMargin: clearAllBtn.visible ? 6 : 8 rightMargin: 8 } width: 340 height: contentHeight model: notifModel spacing: 6 interactive: false add: Transition { NumberAnimation { property: "x" from: 356 to: 0 duration: 220 easing.type: Easing.OutCubic } NumberAnimation { property: "opacity" from: 0 to: 1 duration: 180 } } remove: Transition { NumberAnimation { property: "opacity" to: 0 duration: 130 easing.type: Easing.InCubic } NumberAnimation { property: "x" to: 356 duration: 180 easing.type: Easing.InCubic } } displaced: Transition { NumberAnimation { properties: "y" duration: 220 easing.type: Easing.OutCubic } NumberAnimation { property: "opacity" to: 1 duration: 220 } } delegate: Item { id: toast required property int index required property int nid required property string app required property string icon required property string summary required property string body required property int urgency width: 340 height: toastBg.height // ── Background card ────────────────────────────────────────────── Rectangle { id: toastBg width: parent.width height: cardContent.implicitHeight + 20 color: Colors.bgDark border.color: toast.urgency === 2 ? Colors.critical : Colors.surface border.width: 1 radius: 8 // Urgency stripe on left edge Rectangle { anchors { left: parent.left top: parent.top bottom: parent.bottom topMargin: 1 bottomMargin: 1 leftMargin: 1 } width: 3 radius: 8 color: toast.urgency === 0 ? Colors.fgDim : toast.urgency === 2 ? Colors.critical : Colors.accent } ColumnLayout { id: cardContent anchors { left: parent.left right: parent.right top: parent.top leftMargin: 14 rightMargin: 10 topMargin: 10 } spacing: 4 // ── Header row: icon + summary + app name + close button ─ RowLayout { Layout.fillWidth: true spacing: 6 // App icon (shown when available) IconImage { visible: toast.icon !== "" source: toast.icon width: 16 height: 16 mipmap: true } Text { text: toast.summary color: toast.urgency === 2 ? Colors.critical : toast.urgency === 0 ? Colors.fgDim : Colors.accent font.pixelSize: 13 font.weight: Font.Medium Layout.fillWidth: true elide: Text.ElideRight } Text { text: toast.app color: Colors.fgDim font.pixelSize: 11 elide: Text.ElideRight Layout.maximumWidth: 90 } Rectangle { width: 18 height: 18 radius: 4 color: closeHover.hovered ? Colors.surface : "transparent" HoverHandler { id: closeHover } TapHandler { onTapped: root.removeById(toast.nid) } Text { anchors.centerIn: parent text: "✕" color: Colors.fgDim font.pixelSize: 11 } } } // ── Body text ──────────────────────────────────────────── Text { visible: toast.body !== "" text: toast.body color: Colors.fg font.pixelSize: 12 Layout.fillWidth: true wrapMode: Text.WordWrap maximumLineCount: 3 elide: Text.ElideRight bottomPadding: 2 } } } } } }