Files

80 lines
2.9 KiB
QML

// The banner stack: newest at the top, tucked under the bar on the right.
//
// The window is exactly as wide and tall as the stack, so everything outside
// it stays clickable; the mask trims it further to the cards themselves.
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.config
import qs.services
PanelWindow {
id: root
// Whether a notification is allowed to be a banner is decided once, in
// Notifs.handleNotification: a focus mode's allow-list and the
// critical-breakthrough switch are both exceptions to Do Not Disturb, and
// anything they let past is already in `popups`. Re-testing doNotDisturb
// here would override that three-way decision and leave an allowed app
// chiming at an empty screen.
visible: Notifs.popups.length > 0
color: "transparent"
anchors.top: true
anchors.right: true
// The gap ALONE, not the bar height plus the gap. exclusiveZone 0 means
// "reserve nothing, but respect what others reserved", so this surface
// already begins below the bar's zone -- adding the bar height here counted
// it twice and left the surface floating 48px under the bar instead of 12.
margins.top: Theme.barGap * 2
margins.right: Theme.barSideMargin
exclusiveZone: 0
implicitWidth: 400
implicitHeight: Math.max(1, stack.implicitHeight)
WlrLayershell.namespace: "qs-notifications"
WlrLayershell.layer: WlrLayer.Overlay
// Normally inert so banners never steal a keystroke. An open inline reply
// is the one case where the surface has to be typeable.
WlrLayershell.keyboardFocus: root.replyFocused ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
property bool replyFocused: false
mask: Region {
item: stack
}
Column {
id: stack
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
spacing: Theme.itemSpacing
Repeater {
// Notifs.popups.slice() is a fresh array on every change (a new
// arrival, a dismissal, a sibling toast timing out). Handing that
// straight to Repeater would reset the model and rebuild every
// delegate each time, blowing away whichever toast has a reply
// field mid-typing. ScriptModel diffs by object identity
// (Notification instances are unique QObjects), so only genuinely
// added/removed notifications add/remove delegates — unrelated
// toasts, and their slide-in animations, are untouched.
model: ScriptModel {
values: Notifs.popups.slice(0, Settings.maxVisibleToasts)
}
Toast {
required property var modelData
width: stack.width
notification: modelData
onReplyFocusChanged: focused => root.replyFocused = focused
}
}
}
}