import QtQuick import Quickshell import Quickshell.Wayland import "." as M import "../services" as S import NovaStats as NS // Bar panel - fullscreen transparent window so content can resize freely // without triggering Wayland surface resizes. // // Parent drives visibility via showPanel. Click-outside or Esc dismisses // and emits dismissed(). Pass anchorItem for lazy position computation. PanelWindow { id: root property bool showPanel: false property Item anchorItem: null signal dismissed 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 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 = sw / 2; } panelContainer.x = Math.max(0, Math.min(Math.round(cx - root.contentWidth / 2), sw - root.contentWidth)); } // Grace period: after _show(), suppress click-outside dismiss briefly so // Niri has time to route wl_pointer.enter to the new overlay surface. property bool _grace: false Timer { id: _graceTimer interval: 400 onTriggered: root._grace = false } // Content-change grace: call keepOpen(ms) when panel content is about to // resize/rebuild (session switch, device list change, etc.). property bool _contentBusy: false Timer { id: _contentBusyTimer onTriggered: root._contentBusy = false } function keepOpen(ms) { _contentBusy = true; _contentBusyTimer.interval = ms ?? 400; _contentBusyTimer.restart(); } function _show() { _updatePosition(); if (!hideAnim.running) { panelContainer.y = -(panelContainer.height > 0 ? panelContainer.height : 400); panelContainer.opacity = 0; } _winVisible = true; hideAnim.stop(); if (S.ThemeUtil.reducedMotion) { panelContainer.opacity = 1; panelContainer.y = 0; } else { showAnim.start(); } _grace = true; _graceTimer.restart(); } function dismiss() { if (!_winVisible) return; showAnim.stop(); if (S.ThemeUtil.reducedMotion) { _winVisible = false; dismissed(); } else { hideAnim.start(); } _grace = false; _graceTimer.stop(); } onShowPanelChanged: { if (showPanel) { if (!_winVisible || hideAnim.running) _show(); } else { dismiss(); } } 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; root.dismissed(); } } // Click-outside dismiss. // TapHandler fires for all taps; position check skips taps inside panelContainer. // Gated on !_grace so spurious events during the opening window don't dismiss. Item { anchors.fill: parent TapHandler { enabled: !root._grace onTapped: { const p = point.position; const pad = 8; if (p.x < panelContainer.x - pad || p.x > panelContainer.x + panelContainer.width + pad || p.y < panelContainer.y - pad || p.y > panelContainer.y + panelContainer.height + pad) root.dismiss(); } } } // Esc dismiss Shortcut { sequence: "Escape" enabled: root._winVisible onActivated: root.dismiss() } M.PopupBackground { x: panelContainer.x y: panelContainer.y width: panelContainer.width height: panelContainer.height opacity: panelContainer.opacity * Math.max(NS.ThemeService.barOpacity, 0.85) accentColor: root.accentColor } Item { id: panelContainer x: 0 y: 0 width: root.contentWidth height: _panelColumn.height opacity: 0 Column { id: _panelColumn width: root.contentWidth Flickable { id: _flickable width: parent.width height: Math.min(panelContent.height, _maxContentHeight) contentHeight: panelContent.height clip: contentHeight > height boundsBehavior: Flickable.StopAtBounds readonly property real _maxContentHeight: (root.screen?.height ?? 1080) * 0.6 Column { id: panelContent width: parent.width } } } } // 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 radius: NS.ThemeService.radius opacity: panelContainer.opacity } }