The square markers, block meters and hard-cornered fills read as boxy next to the porthole. The desktop is now soft throughout: the porthole is the shape everything is built around and every smaller surface echoes it. - Theme gains radiusS / radiusM / radiusL. radiusL is frameRadius - padS, so anything inset padS inside one of the frame's curves is concentric with it. - P5Panel and Skew draw rounded rectangles (Geom.roundedRect) whenever they neither lean nor are chopped, which is always now; the polygon path is kept for shapes brought back on purpose. - Markers are dots; workspace pips, toggles, transport and media chips, notification actions and today's date are pills or discs; the slider handle is round. Meters are one continuous rounded line that glides to its level instead of a row of segments. - Album art is rounded with ClippingRectangle in the bar, the media popout and the dashboard. - Free-standing panels (launcher, ask) drop the corner brackets and take the frame radius. - Fix: a hovered notification's wash was a full-bleed square, so in the drawer's bottom corner it ran past the drawer's curve. It is inset by padS/2 and rounded by radiusL, which keeps it inside the curve. Hyprland: the discord special workspace and its window rule are gone, SUPER+S stays the only scratchpad, and SUPER+D now opens the dashboard (it was on SUPER+I). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
276 lines
9.3 KiB
QML
276 lines
9.3 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Widgets
|
|
import Quickshell.Services.Notifications
|
|
import "root:/config"
|
|
import "root:/components"
|
|
import "root:/services"
|
|
|
|
// One notification, used both in the history list and in the toast drawer.
|
|
//
|
|
// Laid out like an entry in NieR's message log rather than as a card: a dot
|
|
// and the sender's name running into a hairline, the summary, the body, and
|
|
// pill buttons for any actions. There is no panel of its own — both places it
|
|
// appears are already hull surfaces — so rest is bare, hover lifts the entry
|
|
// onto a soft rounded wash, and entries are divided by the hairline in their
|
|
// own header.
|
|
//
|
|
// Urgency is the dot: the light for normal, grey for low, red for critical.
|
|
// A critical entry also takes the red into its sender line, and nothing else.
|
|
Item {
|
|
id: root
|
|
|
|
required property var notif
|
|
property bool showActions: true
|
|
|
|
readonly property bool critical: root.notif?.urgency === NotificationUrgency.Critical
|
|
readonly property color tone: root.notif
|
|
? Notifs.urgencyColor(root.notif.urgency)
|
|
: Theme.primary
|
|
|
|
// Kept for callers that size things to the card's edges; there is no lean
|
|
// or chop under the YoRHa theme.
|
|
readonly property real lean: 0
|
|
readonly property real chop: 0
|
|
|
|
readonly property bool hovered: hover.hovered
|
|
|
|
signal dismissed()
|
|
|
|
implicitHeight: layout.implicitHeight + Theme.padM * 2
|
|
|
|
// Hover wash. Inset by padS and rounded by radiusL, which is the frame's
|
|
// radius less that inset: an entry in the bottom corner of the drawer gets
|
|
// a corner concentric with the drawer's own, instead of a square one that
|
|
// ran out past the drawer's curve.
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
anchors.margins: Theme.padS / 2
|
|
radius: Theme.radiusL
|
|
color: root.hovered ? Theme.treatmentHoverFill : "transparent"
|
|
|
|
Behavior on color {
|
|
ColorAnimation { duration: Theme.durFast }
|
|
}
|
|
}
|
|
|
|
Column {
|
|
id: layout
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
anchors.margins: Theme.padM
|
|
spacing: 4
|
|
|
|
// Sender: marker, glyph, name, then the hairline to the edge.
|
|
Item {
|
|
width: parent.width
|
|
height: 16
|
|
|
|
Rectangle {
|
|
id: marker
|
|
anchors.left: parent.left
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: Theme.marker
|
|
height: Theme.marker
|
|
radius: width / 2
|
|
color: root.tone
|
|
}
|
|
|
|
Icon {
|
|
id: glyph
|
|
visible: text !== ""
|
|
anchors.left: marker.right
|
|
anchors.leftMargin: Theme.padS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: Notifs.appIconFor(root.notif)
|
|
color: root.critical ? Theme.alarm : Theme.muted
|
|
font.pixelSize: Theme.fsBody
|
|
}
|
|
|
|
Text {
|
|
id: sender
|
|
anchors.left: glyph.visible ? glyph.right : marker.right
|
|
anchors.leftMargin: Theme.padS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: (root.notif?.appName || "System").toUpperCase()
|
|
color: root.critical ? Theme.alarm : Theme.muted
|
|
font.family: Theme.fontMono
|
|
font.pixelSize: Theme.fsMicro
|
|
font.letterSpacing: 2
|
|
renderType: Text.NativeRendering
|
|
}
|
|
|
|
Rectangle {
|
|
anchors.left: sender.right
|
|
anchors.leftMargin: Theme.padS
|
|
anchors.right: close.left
|
|
anchors.rightMargin: Theme.padS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
height: 1
|
|
color: Theme.alpha(Theme.outline, 0.5)
|
|
}
|
|
|
|
// Close affordance: always there, quiet until the row is hovered.
|
|
Icon {
|
|
id: close
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: ""
|
|
opacity: root.hovered ? 1 : 0.0
|
|
color: closeHover.hovered ? Theme.accent : Theme.muted
|
|
font.pixelSize: Theme.fsBody
|
|
|
|
Behavior on opacity {
|
|
NumberAnimation { duration: Theme.durFast }
|
|
}
|
|
|
|
HoverHandler {
|
|
id: closeHover
|
|
cursorShape: Qt.PointingHandCursor
|
|
}
|
|
|
|
TapHandler {
|
|
onTapped: root.dismissed()
|
|
}
|
|
}
|
|
}
|
|
|
|
Text {
|
|
width: parent.width
|
|
leftPadding: Theme.marker + Theme.padS
|
|
text: root.notif?.summary || ""
|
|
color: root.hovered ? Theme.accent : Theme.text
|
|
elide: Text.ElideRight
|
|
maximumLineCount: 2
|
|
wrapMode: Text.WordWrap
|
|
font.family: Theme.fontDisplay
|
|
font.pixelSize: Theme.fsBody
|
|
font.weight: Theme.weightDisplay
|
|
renderType: Text.NativeRendering
|
|
|
|
Behavior on color {
|
|
ColorAnimation { duration: Theme.durFast }
|
|
}
|
|
}
|
|
|
|
Text {
|
|
width: parent.width
|
|
leftPadding: Theme.marker + Theme.padS
|
|
visible: text !== ""
|
|
text: root.notif?.body || ""
|
|
color: Theme.subtext
|
|
elide: Text.ElideRight
|
|
maximumLineCount: 4
|
|
wrapMode: Text.WordWrap
|
|
textFormat: Text.StyledText
|
|
font.family: Theme.fontDisplay
|
|
font.pixelSize: Theme.fsSmall
|
|
lineHeight: 1.1
|
|
renderType: Text.NativeRendering
|
|
}
|
|
|
|
// Inline image, when the sender supplied one, rounded in a hairline frame.
|
|
ClippingRectangle {
|
|
x: Theme.marker + Theme.padS
|
|
visible: inline.source != "" && inline.status === Image.Ready
|
|
width: inline.width + 2
|
|
height: inline.height + 2
|
|
radius: Theme.radiusM
|
|
color: "transparent"
|
|
border.width: 1
|
|
border.color: Theme.overlay
|
|
|
|
Image {
|
|
id: inline
|
|
x: 1
|
|
y: 1
|
|
source: root.notif?.image || ""
|
|
sourceSize.height: 90
|
|
fillMode: Image.PreserveAspectFit
|
|
asynchronous: true
|
|
}
|
|
}
|
|
|
|
Row {
|
|
x: Theme.marker + Theme.padS
|
|
visible: root.showActions && (root.notif?.actions?.length ?? 0) > 0
|
|
spacing: Theme.padS
|
|
topPadding: 4
|
|
|
|
Repeater {
|
|
model: root.notif?.actions ?? []
|
|
|
|
// A hairline pill. Hover lifts and lights it; it never inverts.
|
|
Item {
|
|
id: actionChip
|
|
required property var modelData
|
|
|
|
implicitWidth: chipRow.implicitWidth + Theme.padM * 2
|
|
implicitHeight: 24
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
radius: height / 2
|
|
color: actionHover.hovered ? Theme.treatmentHoverFill : "transparent"
|
|
border.width: 1
|
|
border.color: actionHover.hovered ? Theme.treatmentHoverMark : Theme.overlay
|
|
|
|
Behavior on color {
|
|
ColorAnimation { duration: Theme.durFast }
|
|
}
|
|
}
|
|
|
|
Row {
|
|
id: chipRow
|
|
anchors.centerIn: parent
|
|
spacing: Theme.padS
|
|
|
|
Rectangle {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: 4
|
|
height: 4
|
|
radius: 2
|
|
color: actionLabel.color
|
|
opacity: actionHover.hovered ? 1 : 0.4
|
|
}
|
|
|
|
Text {
|
|
id: actionLabel
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: actionChip.modelData.text.toUpperCase()
|
|
color: actionHover.hovered ? Theme.accent : Theme.text
|
|
font.family: Theme.fontDisplay
|
|
font.pixelSize: Theme.fsMicro
|
|
font.letterSpacing: Theme.tracking
|
|
renderType: Text.NativeRendering
|
|
}
|
|
}
|
|
|
|
HoverHandler {
|
|
id: actionHover
|
|
cursorShape: Qt.PointingHandCursor
|
|
}
|
|
|
|
TapHandler {
|
|
onTapped: {
|
|
actionChip.modelData.invoke();
|
|
root.dismissed();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
HoverHandler {
|
|
id: hover
|
|
}
|
|
|
|
// Middle click dismisses, matching the old dunst muscle memory.
|
|
TapHandler {
|
|
acceptedButtons: Qt.MiddleButton
|
|
onTapped: root.dismissed()
|
|
}
|
|
}
|