pragma ComponentBehavior: Bound import Qt5Compat.GraphicalEffects import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell import Quickshell.Widgets import Quickshell.Wayland import qs.services import qs.modules.common import qs.modules.common.widgets import qs.modules.common.functions Item { id: root property real maxWindowPreviewHeight: 200 property real maxWindowPreviewWidth: 300 property real windowControlsHeight: 30 property real buttonPadding: 5 property Item lastHoveredButton: null property bool buttonHovered: false property bool requestDockShow: previewPopup.show // Drag-to-combine target, shared across all dock buttons/stacks. A // DropArea's 500ms dwell sets these; the dragged button reads them on // release to decide the combine. dragTargetStackId non-empty means the // drop target is an existing stack (add into it); empty means a plain // app (make a new stack). property string dragTargetAppId: "" property string dragTargetStackId: "" // Cap from Dock.qml: the widest the app list may grow before the dock // would outgrow the screen. Past it the ListView scrolls horizontally. property real maxWidth: -1 Layout.fillHeight: true // No top-only margin: it shoved the whole icon list DOWN inside the bar // with nothing balancing it — the actual cause of icons drooping below // the dock. fillHeight + the dockRow centering handle vertical placement. implicitWidth: maxWidth >= 0 ? Math.min(listView.contentWidth, maxWidth) : listView.contentWidth Behavior on implicitWidth { animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) } function popupCenterXForButton(button) { if (!button || !root.QsWindow) return 0; return root.QsWindow.mapFromItem(button, button.width / 2, 0).x; } StyledListView { id: listView spacing: 2 orientation: ListView.Horizontal anchors.fill: parent clip: true // Only flickable when the list actually overflows, so touch drags on // the buttons (drag-to-combine) aren't stolen while everything fits. interactive: contentWidth > width boundsBehavior: Flickable.StopAtBounds model: ScriptModel { objectProp: "appId" values: TaskbarApps.apps } delegate: Loader { required property var modelData // Fan-out stack entries render as DockStack; everything else // (pinned apps, running windows, separators) as DockAppButton. sourceComponent: modelData.isStack ? stackComp : appComp Component { id: appComp DockAppButton { appToplevel: modelData appListRoot: root topInset: Appearance.sizes.hyprlandGapsOut + root.buttonPadding bottomInset: Appearance.sizes.hyprlandGapsOut + root.buttonPadding } } Component { id: stackComp DockStack { appToplevel: modelData appListRoot: root topInset: Appearance.sizes.hyprlandGapsOut + root.buttonPadding bottomInset: Appearance.sizes.hyprlandGapsOut + root.buttonPadding } } } } PopupWindow { id: previewPopup property var appTopLevel: root.lastHoveredButton?.appToplevel property bool shouldShow: (popupMouseArea.containsMouse || root.buttonHovered) && appTopLevel && appTopLevel.toplevels && appTopLevel.toplevels.length > 0 property bool show: false property real cachedCenterX: 0 Connections { target: root function onLastHoveredButtonChanged() { if (root.lastHoveredButton && root.QsWindow) previewPopup.cachedCenterX = root.popupCenterXForButton(root.lastHoveredButton); } function onButtonHoveredChanged() { if (root.buttonHovered && root.lastHoveredButton && root.QsWindow) previewPopup.cachedCenterX = root.popupCenterXForButton(root.lastHoveredButton); updateTimer.restart(); } } onShouldShowChanged: { updateTimer.restart(); } Timer { id: updateTimer interval: 100 onTriggered: { previewPopup.show = previewPopup.shouldShow; } } anchor { window: root.QsWindow.window adjustment: PopupAdjustment.None gravity: Edges.Top | Edges.Right edges: Edges.Top | Edges.Left } visible: popupBackground.opacity > 0 color: "transparent" implicitWidth: root.QsWindow.window?.width ?? 1 implicitHeight: popupMouseArea.implicitHeight + root.windowControlsHeight + Appearance.sizes.elevationMargin * 2 MouseArea { id: popupMouseArea anchors.bottom: parent.bottom implicitWidth: popupBackground.implicitWidth + Appearance.sizes.elevationMargin * 2 implicitHeight: root.maxWindowPreviewHeight + root.windowControlsHeight + Appearance.sizes.elevationMargin * 2 hoverEnabled: true x: previewPopup.cachedCenterX - width / 2 StyledRectangularShadow { target: popupBackground opacity: previewPopup.show ? 1 : 0 visible: opacity > 0 Behavior on opacity { animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) } } Rectangle { id: popupBackground property real padding: 5 opacity: previewPopup.show ? 1 : 0 visible: opacity > 0 Behavior on opacity { animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) } clip: true color: Appearance.m3colors.m3surfaceContainer radius: Appearance.rounding.normal anchors.bottom: parent.bottom anchors.bottomMargin: Appearance.sizes.elevationMargin anchors.horizontalCenter: parent.horizontalCenter implicitHeight: previewRowLayout.implicitHeight + padding * 2 implicitWidth: previewRowLayout.implicitWidth + padding * 2 Behavior on implicitWidth { animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) } Behavior on implicitHeight { animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) } RowLayout { id: previewRowLayout anchors.centerIn: parent Repeater { model: ScriptModel { values: previewPopup.appTopLevel?.toplevels ?? [] } RippleButton { id: windowButton Layout.fillHeight: true required property var modelData padding: 0 middleClickAction: () => { windowButton.modelData?.close(); } onClicked: { windowButton.modelData?.activate(); } contentItem: ColumnLayout { implicitWidth: screencopyView.implicitWidth implicitHeight: screencopyView.implicitHeight ButtonGroup { contentWidth: parent.width - anchors.margins * 2 StyledText { Layout.margins: 5 Layout.fillWidth: true font.pixelSize: Appearance.font.pixelSize.small text: windowButton.modelData?.title elide: Text.ElideRight color: Appearance.m3colors.m3onSurface } GroupButton { id: closeButton colBackground: ColorUtils.transparentize(Appearance.colors.colSurfaceContainer) baseWidth: root.windowControlsHeight baseHeight: root.windowControlsHeight buttonRadius: Appearance.rounding.full contentItem: MaterialSymbol { anchors.centerIn: parent horizontalAlignment: Text.AlignHCenter text: "close" iconSize: Appearance.font.pixelSize.normal color: Appearance.m3colors.m3onSurface } onClicked: { windowButton.modelData?.close(); } } } Item { Layout.fillWidth: true Layout.fillHeight: true implicitHeight: screencopyView.height implicitWidth: screencopyView.width ScreencopyView { id: screencopyView anchors.centerIn: parent captureSource: windowButton.modelData live: true paintCursor: true constraintSize: Qt.size(root.maxWindowPreviewWidth, root.maxWindowPreviewHeight) layer.enabled: true layer.effect: OpacityMask { maskSource: Rectangle { width: screencopyView.width height: screencopyView.height radius: Appearance.rounding.small } } } } } } } } } } } }