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 property bool _pinned: false // When pinned, only the pin button area receives input — everything else passes through mask: _pinned ? _pinMask : null property Region _pinMask: Region { x: panelContainer.x + panelContainer.width - 28 y: 0 width: 28 height: 28 } WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.exclusiveZone: 0 WlrLayershell.namespace: root.panelNamespace property real topMargin: 0 anchors.top: true anchors.left: true anchors.right: true anchors.bottom: true margins.top: topMargin 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)); } // Grace period: after _show(), suppress auto-close briefly so Niri has time // to route wl_pointer.enter to the new overlay surface (cursor may be stationary). property bool _grace: false Timer { id: _graceTimer interval: 400 onTriggered: { root._grace = false; if (!root.showPanel && !root._pinned) root.dismiss(); } } function _show() { _updatePosition(); _winVisible = true; hideAnim.stop(); showAnim.start(); _grace = true; _graceTimer.restart(); } function dismiss() { _pinned = false; showAnim.stop(); hideAnim.start(); _grace = false; _graceTimer.stop(); } Component.onCompleted: if (popupMode) _show() Timer { id: _hideTimer interval: 150 onTriggered: if (!root.showPanel && !root._grace && !root._pinned) 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 && !root._pinned onHoveredChanged: if (!root.popupMode && !root._pinned) root.panelHovered = hovered } } // Pin button — top-right corner, hover mode only Item { visible: !root.popupMode x: parent.width - width - 4 y: 4 width: 20 height: 20 z: 2 opacity: pinHover.hovered || root._pinned ? 1 : 0.35 Behavior on opacity { NumberAnimation { duration: 100 } } HoverHandler { id: pinHover } TapHandler { cursorShape: Qt.PointingHandCursor onTapped: { root._pinned = !root._pinned; if (!root._pinned && !root.showPanel) root.dismiss(); } } Text { anchors.centerIn: parent text: root._pinned ? "\uEB40" : "\uEB3F" color: root._pinned ? root.accentColor : M.Theme.base04 font.pixelSize: M.Theme.fontSize - 1 font.family: M.Theme.iconFontFamily Behavior on color { ColorAnimation { duration: 100 } } } } } // 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 } }