Files
omarchy/shell/plugins/notifications/components/NotificationCard.qml
T
David Heinemeier HanssonandClaude Opus 5.5 b18ab4952b Add a no-animations mode, on by default in VMs (#13550)
A machine without a GPU, like most VMs, renders through llvmpipe on the
CPU, where every animated frame and every translucent window costs. In a
VM, opening and closing a terminal took ~5s of CPU; a panel ~3.4s.

omarchy toggle animations (also under Toggle > Animations) places a Hyprland
flag that turns off animations, blur and shadows and makes windows opaque.
The shell follows Hyprland's animations:enabled, rereading it on every
config reload: its one-shot animations run for Style.duration(ms), which
is then 0, and its spinners, pulses and title marquee hold still. A new
install in a VM starts with the flag in place.

Measured in the ISO test VM (llvmpipe), CPU per interaction:
terminal open+close 5000ms -> 481ms, workspace switch 1670ms -> 409ms,
audio panel 3446ms -> 796ms, volume OSD 2324ms -> 584ms, menu 2028ms ->
1241ms.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:46:05 +02:00

229 lines
8.3 KiB
QML

// Notification card. Pure presentational — no service, Notification, or
// ListModel references. The popup container drives lifetime; the history
// panel drives static rendering. Both use the same component.
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Ui
import "../NotificationLogic.js" as NotificationLogic
BorderSurface {
id: root
property string app: ""
property string appIcon: ""
property string summary: ""
property string body: ""
property string image: ""
// Nerd Font glyph rendered in the icon slot when no real icon is set.
// Used by omarchy-notification-send so user-action toasts (`Silenced
// notifications` etc.) show their bell/lock/etc. glyph without leaking
// into the summary text.
property string glyph: ""
// NotificationUrgency: Low=0, Normal=1, Critical=2 (upstream).
property int urgency: 1
property double timestamp: 0
property int cornerRadius: 0
// System monospace font injected by the container.
property string fontFamily: ""
readonly property bool hovered: hoverTracker.hovered
signal closeRequested()
signal cardClicked()
// Prefer per-notification media/avatar data, then fall back to the app icon.
// The `check` flag avoids Qt's missing-texture placeholder for unknown names.
readonly property string smallIconSource: image.length > 0 ? image : iconSource(appIcon)
readonly property bool hasGlyph: glyph.length > 0
readonly property bool compactGlyph: NotificationLogic.shouldRenderCompactGlyph(glyph, smallIconSource, singleLineToast)
readonly property bool hasSmallIcon: smallIconSource.length > 0
readonly property bool summaryStartsWithGlyph: NotificationLogic.summaryStartsWithGlyph(summary)
readonly property bool singleLineToast: sanitizedBody.length === 0
readonly property bool collapseRedundantIcon: singleLineToast && !hasGlyph && summaryStartsWithGlyph
readonly property string sanitizedBody: sanitizeBody(body)
readonly property string styledBody: NotificationLogic.styledBody(body, app, appIcon)
readonly property color dimColor: Qt.darker(Color.notifications.text, 1.4)
readonly property color bodyColor: Qt.darker(Color.notifications.text, 1.15)
readonly property color accentColor: urgency === 2 ? Color.urgent : (urgency === 0 ? dimColor : Color.notifications.countdown)
readonly property var cardBorderSpec: Border.surfaceSpec("notifications", "border", Color.notifications.border, Math.max(1, Style.space(2)))
function sanitizeBody(s) {
return NotificationLogic.sanitizeBody(s, app, appIcon)
}
function iconSource(icon) {
var value = String(icon || "")
if (value.length === 0) return ""
if (value.indexOf("file://") === 0 || value.indexOf("image://") === 0) return value
if (value.charAt(0) === "/") return Util.fileUrl(value)
return Quickshell.iconPath(value, true)
}
implicitWidth: Style.space(380)
// Add vertical border insets so mainColumn (inset by border on top/left/right)
// doesn't push content under the bottom edge.
implicitHeight: mainColumn.implicitHeight + borderTop + borderBottom
radius: cornerRadius
color: Color.notifications.background
borderSpec: cardBorderSpec
clip: true
HoverHandler { id: hoverTracker }
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: function(mouse) {
if (mouse.button === Qt.RightButton) {
root.closeRequested()
} else {
root.cardClicked()
}
}
}
ColumnLayout {
id: mainColumn
// Inset by the card border so the content doesn't paint over the card's
// outer border.
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: root.borderTop
anchors.leftMargin: root.borderLeft
anchors.rightMargin: root.borderRight
spacing: 0
// Text content.
RowLayout {
Layout.fillWidth: true
Layout.leftMargin: Style.space(12)
Layout.rightMargin: Style.space(12)
Layout.topMargin: root.singleLineToast ? Style.space(7) : Style.space(10)
Layout.bottomMargin: root.singleLineToast ? Style.space(7) : Style.space(10)
spacing: root.collapseRedundantIcon ? 0 : (root.compactGlyph ? Style.space(8) : Style.space(12))
Item {
id: smallIconSlot
Layout.preferredWidth: visible ? Style.space(40) : 0
Layout.preferredHeight: visible ? Style.space(40) : 0
Layout.alignment: Qt.AlignVCenter
// Hide the slot when the icon failed to resolve (themed-icon name
// not in the user's icon theme) AND we don't have a glyph fallback
// — prevents rendering Qt's pink broken-image placeholder.
visible: !root.collapseRedundantIcon && !root.compactGlyph && (root.hasSmallIcon || root.hasGlyph) && (root.hasGlyph || smallIconImage.status !== Image.Error)
Image {
id: smallIconImage
anchors.fill: parent
source: root.smallIconSource
sourceSize.width: smallIconSlot.width * Screen.devicePixelRatio
sourceSize.height: smallIconSlot.height * Screen.devicePixelRatio
fillMode: Image.PreserveAspectFit
asynchronous: true
smooth: true
visible: !root.hasGlyph || smallIconImage.status === Image.Ready
}
// Glyph fallback (Nerd Font character) when no image icon is
// available. Used by omarchy-notification-send's `-g` flag.
Text {
textFormat: Text.PlainText
anchors.centerIn: parent
visible: root.hasGlyph && smallIconImage.status !== Image.Ready
text: root.glyph
color: Color.notifications.text
font.family: root.fontFamily
font.pixelSize: Style.font.displayLarge
}
}
Text {
textFormat: Text.PlainText
Layout.alignment: Qt.AlignVCenter
visible: root.compactGlyph
text: root.glyph
color: Color.notifications.text
font.family: root.fontFamily
font.pixelSize: Style.font.icon
}
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
// Keep the first line clear of the hover-revealed close button.
Layout.rightMargin: Style.space(10)
spacing: Style.space(2)
Text {
// The spec defines the summary as a single line of plain text, so
// AutoText could only ever promote a hostile string to rich text.
// The body below is StyledText on purpose — see Service.qml's
// bodyMarkupSupported — and is stripped in NotificationLogic.
textFormat: Text.PlainText
Layout.fillWidth: true
visible: root.summary.length > 0
text: root.summary
font.family: "Liberation Sans"
color: Color.notifications.text
font.pixelSize: Style.font.title
font.bold: true
wrapMode: Text.WordWrap
elide: Text.ElideRight
maximumLineCount: 2
}
Text {
Layout.fillWidth: true
Layout.topMargin: Style.space(2)
visible: root.sanitizedBody.length > 0
text: root.styledBody
textFormat: Text.StyledText
font.family: "Liberation Sans"
color: root.bodyColor
font.pixelSize: Style.font.title
wrapMode: Text.WordWrap
elide: Text.ElideRight
maximumLineCount: 3
}
}
}
}
// Hover-revealed close. Stacked after mainColumn so its MouseArea sits
// above the full-card one and the click never reaches cardClicked.
Item {
anchors.top: parent.top
anchors.right: parent.right
anchors.topMargin: root.borderTop + Style.space(3)
anchors.rightMargin: root.borderRight + Style.space(3)
width: Style.space(18)
height: Style.space(18)
visible: opacity > 0
opacity: root.hovered ? 1 : 0
Behavior on opacity { NumberAnimation { duration: Style.duration(100) } }
Text {
anchors.centerIn: parent
text: "✕"
color: closeArea.containsMouse ? Color.notifications.text : root.dimColor
font.pixelSize: Math.round(Style.font.caption * 1.44)
}
MouseArea {
id: closeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.closeRequested()
}
}
}