pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import Quickshell.Services.Notifications import ZShell.Components import ZShell.Config import ZShell.Services import qs.Components import qs.Services CustomRect { id: root required property NotificationRow modelData required property bool expanded required property color expandedColour required property color avatarColour required property color buttonColour property bool showAvatar: true property bool single: false property bool ready: true property real headerHeight property real headerTitleWidth readonly property bool critical: modelData.urgency === NotificationUrgency.Critical readonly property bool low: modelData.urgency === NotificationUrgency.Low readonly property CustomText body: (expandedContent.item as ExpandedBody)?.body ?? null readonly property real padding: single ? 0 : Tokens.padding.medium readonly property real headerSummaryOffset: -(headerHeight + summaryHeightMetrics.height) / 2 readonly property real inset: expanded ? padding : 0 readonly property real summaryOffset: single && !expanded ? headerSummaryOffset : inset readonly property real nonAnimHeight: { if (!expanded) return summaryHeightMetrics.height; const contentHeight = summary.implicitHeight + expandedContent.implicitHeight + expandedContent.anchors.topMargin + padding * 2; return single ? contentHeight : Math.max(contentHeight, Config.notifs.sizes.image + Tokens.padding.extraSmall * 2); } signal linkActivated implicitHeight: nonAnimHeight radius: Tokens.rounding.medium color: { const c = root.critical ? Colors.palette.m3secondaryContainer : root.expandedColour; return expanded && !single ? c : Qt.alpha(c, 0); } TextMetrics { id: summaryHeightMetrics font: dummySummary.font text: " " // Use this height to prevent weird characters from changing the line height } TextMetrics { id: summaryMetrics font: dummySummary.font text: root.modelData.summary elide: Text.ElideRight elideWidth: root.single ? root.headerTitleWidth : root.width } WrappedLoader { id: avatar shouldBeActive: root.expanded && root.showAvatar anchors.top: parent.top anchors.left: parent.left anchors.topMargin: Tokens.padding.extraSmall anchors.leftMargin: -(Config.notifs.sizes.image + Tokens.spacing.medium) sourceComponent: NotifAvatar { owner: root.modelData image: root.modelData.image icon: root.modelData.appIcon urgency: root.modelData.urgency lowFillColour: root.avatarColour summary: root.modelData.summary progress: root.modelData.progress / 100 } } WrappedLoader { id: compactSummary shouldBeActive: !root.expanded anchors.top: parent.top anchors.left: parent.left anchors.leftMargin: root.inset anchors.topMargin: root.summaryOffset sourceComponent: CustomText { animate: root.ready text: summaryMetrics.elidedText color: root.critical ? Colors.palette.m3onSecondaryContainer : root.low ? Colors.palette.m3onSurfaceVariant : Colors.palette.m3onSurface } Behavior on anchors.leftMargin { Anim {} } Behavior on anchors.topMargin { Anim {} } } CustomText { id: dummySummary anchors.top: parent.top anchors.left: parent.left visible: false text: root.modelData.summary } WrappedLoader { id: compactBody shouldBeActive: !root.expanded anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right anchors.leftMargin: root.inset + (root.single ? 0 : dummySummary.width + Tokens.spacing.small) anchors.topMargin: root.single && root.expanded ? summary.implicitHeight + expandedContent.anchors.topMargin : root.inset sourceComponent: CustomText { text: root.modelData.body.replace(/\n/g, " ") color: root.critical ? Colors.palette.m3secondary : root.low ? Colors.palette.m3outline : Colors.palette.m3onSurfaceVariant elide: Text.ElideRight } Behavior on anchors.leftMargin { Anim {} } Behavior on anchors.topMargin { Anim {} } } WrappedLoader { id: summary shouldBeActive: root.expanded anchors.top: parent.top anchors.left: parent.left anchors.leftMargin: root.inset anchors.topMargin: root.summaryOffset width: root.width - root.padding * 2 - timeStr.implicitWidth - Tokens.spacing.small sourceComponent: CustomText { text: root.modelData.summary color: root.critical ? Colors.palette.m3onSecondaryContainer : root.low ? Colors.palette.m3onSurfaceVariant : Colors.palette.m3onSurface wrapMode: Text.WordWrap } Behavior on anchors.leftMargin { Anim {} } Behavior on anchors.topMargin { Anim {} } } WrappedLoader { id: timeStr shouldBeActive: root.expanded && !root.single anchors.top: parent.top anchors.right: parent.right anchors.margins: root.inset sourceComponent: CustomText { animate: true text: root.modelData.timeStr color: root.critical ? Colors.palette.m3secondary : Colors.palette.m3outline font: Tokens.font.body.small } Behavior on anchors.margins { Anim {} } } WrappedLoader { id: expandedContent shouldBeActive: root.expanded anchors.top: summary.bottom anchors.left: parent.left anchors.right: parent.right anchors.margins: root.inset anchors.topMargin: Tokens.spacing.extraSmall sourceComponent: ExpandedBody {} Behavior on anchors.margins { Anim {} } } Behavior on implicitHeight { Anim {} } component ExpandedBody: SnapLayout { readonly property alias body: bodyText orientation: Qt.Vertical spacing: Tokens.spacing.medium CustomText { id: bodyText Layout.fillWidth: true textFormat: Text.MarkdownText text: root.modelData.body.replace(/(.)\n(?!\n)/g, "$1\n\n") || qsTr("No body here! :/") color: root.critical ? Colors.palette.m3secondary : root.low ? Colors.palette.m3outline : Colors.palette.m3onSurfaceVariant wrapMode: Text.WordWrap onLinkActivated: link => { Qt.openUrlExternally(link); root.linkActivated(); } } NotifActions { Layout.fillWidth: true notif: root.modelData buttonColour: root.buttonColour } } component WrappedLoader: Loader { id: comp required property bool shouldBeActive active: false opacity: 0 // Makes the loader load on the same frame shouldBeActive becomes true, which ensures size is set states: State { name: "active" when: comp.shouldBeActive PropertyChanges { comp.opacity: 1 comp.active: true } } transitions: [ Transition { from: "" to: "active" SequentialAnimation { PropertyAction { property: "active" } Anim { type: Anim.DefaultEffects property: "opacity" } } }, Transition { from: "active" to: "" SequentialAnimation { Anim { type: Anim.DefaultEffects property: "opacity" } PropertyAction { property: "active" } } } ] } }