nova-shell/modules/HoverPanel.qml

218 lines
5.6 KiB
QML

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
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.dismiss();
}
}
function _show() {
_updatePosition();
_winVisible = true;
hideAnim.stop();
showAnim.start();
_grace = true;
_graceTimer.restart();
}
function dismiss() {
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.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
}
}