import QtQuick import Quickshell import Quickshell.Wayland import "." as M // Unified bar panel — fullscreen transparent window so content can resize // freely without triggering Wayland surface resizes. // // Hover mode (popupMode: false, default): // Parent drives visibility via showPanel. Panel auto-closes when showPanel // drops, with a 50ms debounce. Reports panelHovered back to parent. // Pass anchorItem for lazy position computation on each show. // // Popup mode (popupMode: true): // Shows immediately on creation. Stays open until click-outside or // dismiss() call. Emits dismissed() when closed — caller's LazyLoader // sets active: false. Pass anchorX (screen-relative centre x). PanelWindow { id: root property bool popupMode: false // Hover mode property bool showPanel: true property Item anchorItem: null property bool panelHovered: false // Popup mode property real anchorX: -1 signal dismissed // Shared required property color accentColor property string panelNamespace: "nova-panel" property real contentWidth: 220 default property alias content: panelContent.children visible: _winVisible color: "transparent" property bool _winVisible: false WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.exclusiveZone: 0 WlrLayershell.namespace: root.panelNamespace anchors.top: true anchors.left: true anchors.right: true anchors.bottom: true margins.top: 0 function _updatePosition() { const scr = screen; const sw = scr?.width ?? 1920; let cx; if (root.anchorItem) { const pt = root.anchorItem.mapToGlobal(root.anchorItem.width / 2, 0); cx = pt.x - (scr?.x ?? 0); } else { cx = root.anchorX; } panelContainer.x = Math.max(0, Math.min(Math.round(cx - root.contentWidth / 2), sw - root.contentWidth)); } function _show() { _updatePosition(); _winVisible = true; hideAnim.stop(); showAnim.start(); } function dismiss() { showAnim.stop(); hideAnim.start(); } Component.onCompleted: if (popupMode) _show() Timer { id: _hideTimer interval: 50 onTriggered: if (!root.showPanel) root.dismiss() } onShowPanelChanged: { if (root.popupMode) return; if (showPanel) { _hideTimer.stop(); _show(); } else { _hideTimer.restart(); } } ParallelAnimation { id: showAnim NumberAnimation { target: panelContainer property: "opacity" to: 1 duration: 120 easing.type: Easing.OutCubic } NumberAnimation { target: panelContainer property: "y" to: 0 duration: 150 easing.type: Easing.OutCubic } } ParallelAnimation { id: hideAnim NumberAnimation { target: panelContainer property: "opacity" to: 0 duration: 150 easing.type: Easing.InCubic } NumberAnimation { target: panelContainer property: "y" to: -panelContainer.height duration: 150 easing.type: Easing.InCubic } onFinished: { root._winVisible = false; if (root.popupMode) root.dismissed(); } } // Popup mode: click-outside dismiss (declared first = lowest z = below content) MouseArea { anchors.fill: parent visible: root.popupMode enabled: root.popupMode onClicked: root.dismiss() } M.PopupBackground { x: panelContainer.x y: panelContainer.y width: panelContainer.width height: panelContainer.height opacity: panelContainer.opacity * Math.max(M.Theme.barOpacity, 0.85) accentColor: root.accentColor } Item { id: panelContainer x: 0 y: -height width: root.contentWidth height: panelContent.height opacity: 0 // Popup mode: eat clicks on panel background so outer dismiss doesn't fire MouseArea { anchors.fill: parent visible: root.popupMode enabled: root.popupMode } Column { id: panelContent width: root.contentWidth HoverHandler { enabled: !root.popupMode onHoveredChanged: if (!root.popupMode) root.panelHovered = hovered } } } // Border overlay — on top of content so full-bleed items don't cover it Rectangle { x: panelContainer.x y: panelContainer.y width: panelContainer.width height: panelContainer.height color: "transparent" border.color: root.accentColor border.width: 1 bottomLeftRadius: M.Theme.radius bottomRightRadius: M.Theme.radius opacity: panelContainer.opacity } }