import qs.config import QtQuick import QtQuick.Effects import QtQuick.Layouts import Quickshell import Quickshell.Widgets import Quickshell.Wayland LazyLoader { id: root property string displayName: screen?.name ?? "" property PanelWindow instance: null property HoverHandler hoverTarget property real margin: Metrics.margin(10) default property list content property bool startAnim: false property bool isVisible: false property bool keepAlive: false property bool interactable: false property bool hasHitbox: true property bool hCenterOnItem: false property bool followMouse: false property list childPopouts: [] property bool requiresHover: true property bool _manualControl: false property int hoverDelay: Metrics.chronoDuration(250) property bool targetHovered: hoverTarget && hoverTarget.hovered property bool containerHovered: interactable && root.item && root.item.containerHovered property bool selfHovered: targetHovered || containerHovered property bool childrenHovered: { for (let i = 0; i < childPopouts.length; i++) { if (childPopouts[i].selfHovered) return true; } return false; } property bool hoverActive: selfHovered || childrenHovered property Timer showDelayTimer: Timer { interval: root.hoverDelay repeat: false onTriggered: { root.keepAlive = true; root.isVisible = true; root.startAnim = true; } } property Timer hangTimer: Timer { interval: Metrics.chronoDuration(200) repeat: false onTriggered: { root.startAnim = false; cleanupTimer.restart(); } } property Timer cleanupTimer: Timer { interval: Metrics.chronoDuration("small") repeat: false onTriggered: { root.isVisible = false; root.keepAlive = false; root._manualControl = false; root.instance = null; } } onHoverActiveChanged: { if (_manualControl) return; if (!requiresHover) return; if (hoverActive) { hangTimer.stop(); cleanupTimer.stop(); if (hoverDelay > 0) { showDelayTimer.restart(); } else { root.keepAlive = true; root.isVisible = true; root.startAnim = true; } } else { showDelayTimer.stop(); hangTimer.restart(); } } function show() { hangTimer.stop(); cleanupTimer.stop(); showDelayTimer.stop(); _manualControl = true; keepAlive = true; isVisible = true; startAnim = true; } function hide() { _manualControl = true; showDelayTimer.stop(); startAnim = false; hangTimer.stop(); cleanupTimer.restart(); } active: keepAlive component: PanelWindow { id: popoutWindow color: "transparent" visible: root.isVisible WlrLayershell.namespace: "whisker:popout" WlrLayershell.layer: WlrLayer.Overlay exclusionMode: ExclusionMode.Ignore exclusiveZone: 0 anchors { left: true top: true right: true bottom: true } property bool exceedingHalf: false property var parentPopoutWindow: null property point mousePos: Qt.point(0, 0) property bool containerHovered: root.interactable && containerHoverHandler.hovered HoverHandler { id: windowHover onPointChanged: point => { if (root.followMouse) popoutWindow.mousePos = point.position; } } mask: Region { x: !root.hasHitbox ? 0 : !requiresHover ? 0 : container.x y: !root.hasHitbox ? 0 : !requiresHover ? 0 : container.y width: !root.hasHitbox ? 0 : !requiresHover ? popoutWindow.width : container.implicitWidth height: !root.hasHitbox ? 0 : !requiresHover ? popoutWindow.height : container.implicitHeight } MouseArea { id: mouseArea anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton hoverEnabled: false onPressed: mouse => { if (!containerHoverHandler.containsMouse && root.isVisible) { root.hide(); } } } Item { id: container implicitWidth: contentArea.implicitWidth + root.margin * 2 implicitHeight: contentArea.implicitHeight + root.margin * 2 x: { let xValue; if (root.followMouse) xValue = mousePos.x + 10; else { let targetItem = hoverTarget?.parent; if (!targetItem) xValue = 0; else { let baseX = targetItem.mapToGlobal(Qt.point(0, 0)).x; if (parentPopoutWindow) baseX += parentPopoutWindow.x; let targetWidth = targetItem.width; let popupWidth = container.implicitWidth; if (root.hCenterOnItem) { let centeredX = baseX + (targetWidth - popupWidth) / 2; if (centeredX + popupWidth > screen.width) centeredX = screen.width - popupWidth - 10; if (centeredX < 10) centeredX = 10; xValue = centeredX; } else { let xPos = baseX - ((ConfigResolver.bar(root.displayName).position === "top" || ConfigResolver.bar(root.displayName).position === "top") ? 20 : -40); if (xPos + popupWidth > screen.width) { exceedingHalf = true; xValue = baseX - popupWidth; } else { exceedingHalf = false; xValue = xPos; } } } } return root.cleanupTimer.running ? xValue : Math.round(xValue); } y: { let yValue; if (root.followMouse) yValue = mousePos.y + 10; else { let targetItem = hoverTarget?.parent; if (!targetItem) yValue = 0; else { let baseY = targetItem.mapToGlobal(Qt.point(0, 0)).y; if (parentPopoutWindow) baseY += parentPopoutWindow.y; let targetHeight = targetItem.height; let popupHeight = container.implicitHeight; let yPos = baseY + ((ConfigResolver.bar(root.displayName).position === "top" || ConfigResolver.bar(root.displayName).position === "top") ? targetHeight : 0); if (yPos > screen.height / 2) yPos = baseY - popupHeight; if (yPos + popupHeight > screen.height) yPos = screen.height - popupHeight - 10; if (yPos < 10) yPos = 10; yValue = yPos; } } return root.cleanupTimer.running ? yValue : Math.round(yValue); } opacity: root.startAnim ? 1 : 0 scale: root.interactable ? 1 : root.startAnim ? 1 : 0.9 layer.enabled: true layer.effect: MultiEffect { shadowEnabled: true shadowOpacity: 1 shadowColor: Appearance.m3colors.m3shadow shadowBlur: 1 shadowScale: 1 } Behavior on opacity { NumberAnimation { duration: Metrics.chronoDuration("small") easing.type: Appearance.animation.easing } } Behavior on scale { enabled: !root.interactable NumberAnimation { duration: Metrics.chronoDuration("small") easing.type: Appearance.animation.easing } } Behavior on implicitWidth { enabled: root.interactable NumberAnimation { duration: Metrics.chronoDuration("small") easing.type: Appearance.animation.easing } } Behavior on implicitHeight { enabled: root.interactable NumberAnimation { duration: Metrics.chronoDuration("small") easing.type: Appearance.animation.easing } } ClippingRectangle { id: popupBackground anchors.fill: parent color: Appearance.m3colors.m3surface radius: Appearance.rounding.normal ColumnLayout { id: contentArea anchors.fill: parent anchors.margins: root.margin } } HoverHandler { id: containerHoverHandler enabled: root.interactable } } Component.onCompleted: { root.instance = popoutWindow; for (let i = 0; i < root.content.length; i++) { const comp = root.content[i]; if (comp && comp.createObject) { comp.createObject(contentArea); } else { console.warn("StyledPopout: invalid content:", comp); } } let parentPopout = root.parent; while (parentPopout && !parentPopout.childPopouts) parentPopout = parentPopout.parent; if (parentPopout) { parentPopout.childPopouts.push(root); if (parentPopout.item) popoutWindow.parentPopoutWindow = parentPopout.item; } } } }