pragma ComponentBehavior: Bound import QtQuick import QtQuick.Shapes import Quickshell import ZShell import M3Shapes import ZShell.Config import qs.Components import qs.Helpers import qs.Services import Quickshell.Services.Notifications Item { id: root required property var owner required property string image required property string icon required property int urgency required property string summary required property color lowFillColour property real progress: 0 property string shownImage property string shownIcon readonly property color fillColour: urgency === NotificationUrgency.Critical ? Colors.palette.m3primary : urgency === NotificationUrgency.Low ? lowFillColour : Colors.palette.m3secondaryContainer readonly property color iconColour: urgency === NotificationUrgency.Critical ? Colors.palette.m3onPrimary : urgency === NotificationUrgency.Low ? Colors.palette.m3onSurfaceVariant : Colors.palette.m3onSecondaryContainer implicitWidth: Config.notifs.sizes.image implicitHeight: Config.notifs.sizes.image // Keeping the previously shown url avoids replaying the fade when a row // swaps between its raw source and the cached blob. onImageChanged: { if (image && !owner.isSameImage(shownImage)) shownImage = image; } onIconChanged: { if (icon && !owner.isSameAppIcon(shownIcon)) shownIcon = icon; } Component.onCompleted: { shownImage = image; shownIcon = icon; } Component { id: imageComp FadeImage { source: Qt.resolvedUrl(root.shownImage) width: Config.notifs.sizes.image height: Config.notifs.sizes.image fadeInLargeAnim: Anim.DefaultEffects } } Component { id: appIconComp FadeIcon { size: Math.round(Config.notifs.sizes.image * 0.6) } } Component { id: materialIconComp MaterialIcon { animate: true text: Icons.getNotifIcon(root.summary, root.urgency) color: root.iconColour fontStyle: Tokens.font.icon.medium } } MaterialShape { id: bgShape anchors.fill: parent shape: contentLoader.sourceComponent === materialIconComp && root.urgency === NotificationUrgency.Critical ? MaterialShape.SoftBurst : MaterialShape.Circle color: Qt.alpha(root.fillColour, 1) opacity: root.fillColour.a Behavior on color { CAnim {} } Behavior on opacity { Anim { type: Anim.SlowEffects } } } Item { anchors.fill: parent Loader { id: contentLoader anchors.centerIn: parent anchors.verticalCenterOffset: sourceComponent === materialIconComp ? 1 : 0 asynchronous: true sourceComponent: root.image ? imageComp : root.icon ? appIconComp : materialIconComp } } Loader { asynchronous: true anchors.right: parent.right anchors.bottom: parent.bottom active: opacity > 0 opacity: root.icon && root.image ? 1 : 0 sourceComponent: CustomRect { implicitWidth: Config.notifs.sizes.badge implicitHeight: Config.notifs.sizes.badge color: root.fillColour radius: Tokens.rounding.full FadeIcon { anchors.centerIn: parent size: Math.round(Config.notifs.sizes.badge * 0.6) } } Behavior on opacity { Anim { type: root.icon && root.image ? Anim.DefaultEffects : Anim.FastEffects } } } Loader { asynchronous: true anchors.fill: parent active: root.progress > 0 sourceComponent: Shape { preferredRendererType: Shape.CurveRenderer ShapePath { id: progressShape property real sweep: root.progress * 360 property real spacing: root.urgency === NotificationUrgency.Critical ? Tokens.spacing.extraSmall : 0 capStyle: ShapePath.RoundCap fillColor: "transparent" strokeWidth: 2 strokeColor: Colors.palette.m3primary PathPolyline { function pointAt(angle: real): point { const dist = bgShape.distanceAtAngle(angle) + progressShape.strokeWidth / 2 + progressShape.spacing; const rad = angle * Math.PI / 180; return Qt.point(root.width / 2 + dist * Math.sin(rad), root.height / 2 - dist * Math.cos(rad)); } path: { bgShape.morphProgress; // mapToItem-free dependency: re-run when the shape morphs const points = []; for (let angle = 0; angle < progressShape.sweep; angle += 3) points.push(pointAt(angle)); points.push(pointAt(progressShape.sweep)); return points; } } Behavior on sweep { Anim { easing: Tokens.anim.emphasizedDecel } } Behavior on spacing { Anim {} } Behavior on strokeColor { CAnim {} } } } } component FadeIcon: FadeImage { id: fadeIcon required property int size width: size height: size source: Quickshell.iconPath(root.shownIcon) fillMode: Image.PreserveAspectFit fadeInLargeAnim: Anim.DefaultEffects } }