// 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 visible: !Notifs.doNotDisturb && 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 } } } }