import QtQuick import QtQuick.Layouts import QtQuick.Controls import Quickshell import Quickshell.Wayland import Quickshell.Hyprland import "../theme" import ".." PanelWindow { id: dockWindow property var modelData screen: modelData anchors { bottom: true } margins { bottom: 6 } // Adjust width to fit content implicitWidth: dockTray.implicitWidth + 32 implicitHeight: 90 color: "transparent" WlrLayershell.layer: WlrLayer.Top WlrLayershell.namespace: "quickshell-dock" WlrLayershell.keyboardFocus: WlrKeyboardFocus.None // Reserve 70px exclusive zone so windows don't overlap the dock exclusiveZone: 70 // Tooltip overlay container Item { id: tooltipContainer anchors.fill: parent z: 100 } // 2.5D Frosted Glass Dock Tray Rectangle { id: dockTray anchors.bottom: parent.bottom anchors.horizontalCenter: parent.horizontalCenter implicitWidth: dockLayout.implicitWidth + 24 height: 68 radius: Theme.radiusDock color: Theme.dockBg border.color: Theme.dockBorder border.width: 1 // Top glossy reflection edge line (Catalina shelf characteristic) Rectangle { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top anchors.leftMargin: 12 anchors.rightMargin: 12 height: 1 color: Theme.dockHighlight opacity: 0.8 } // Inner items layout RowLayout { id: dockLayout anchors.centerIn: parent spacing: 6 // ================================================================= // 1. PINNED APPS // ================================================================= DockItem { id: finderItem name: "Finder" iconSource: "../assets/finder.svg" execCmd: "foot -e sh -c 'cd ~; lf || yazi || ls -la; exec zsh'" appClass: "foot" isPinned: true } DockItem { name: "Launchpad" iconSource: "../assets/launchpad.svg" execCmd: "" isPinned: true onCustomClicked: GlobalState.toggleSpotlight() } DockItem { name: "Brave Browser" iconSource: "../assets/browser.svg" execCmd: "brave" appClass: "brave-browser" isPinned: true } DockItem { name: "Terminal" iconSource: "../assets/terminal.svg" execCmd: "foot" appClass: "foot" isPinned: true } DockItem { name: "Neovim" iconSource: "../assets/code.svg" execCmd: "foot -e nvim" appClass: "nvim" isPinned: true } DockItem { name: "System Preferences" iconSource: "../assets/preferences.svg" execCmd: "pavucontrol" appClass: "pavucontrol" isPinned: true } DockItem { name: "App Store" iconSource: "../assets/appstore.svg" execCmd: "foot -e zsh -c 'echo NixOS Packages; nix-env -qaP | head -n 50; exec zsh'" appClass: "" isPinned: true } DockItem { name: "Music" iconSource: "../assets/music.svg" execCmd: "" appClass: "" isPinned: true onCustomClicked: GlobalState.mediaPlayPause() } // ================================================================= // 2. DYNAMIC RUNNING APPS (Not pinned) // ================================================================= Repeater { model: { const pinnedClasses = ["foot", "brave-browser", "pavucontrol", "nvim"]; const list = []; const seen = {}; if (Array.isArray(GlobalState.runningApps)) { for (let i = 0; i < GlobalState.runningApps.length; i++) { const client = GlobalState.runningApps[i]; const cls = (client.class || "").toLowerCase(); if (cls && !pinnedClasses.includes(cls) && !seen[cls]) { seen[cls] = true; list.push({ name: client.title || client.class || "App", cls: client.class, address: client.address }); } } } return list; } delegate: DockItem { required property var modelData name: modelData.name iconSource: "../assets/code.svg" appClass: modelData.cls isPinned: false onCustomClicked: { if (modelData.address) { Hyprland.dispatch(`focuswindow address:${modelData.address}`); } } } } // ================================================================= // 3. DOCK SEPARATOR DIVIDER // ================================================================= Rectangle { Layout.preferredWidth: 1 Layout.preferredHeight: 44 Layout.alignment: Qt.AlignVCenter Layout.leftMargin: 4 Layout.rightMargin: 4 color: Qt.rgba(1.0, 1.0, 1.0, 0.22) } // ================================================================= // 4. DOWNLOADS FOLDER & TRASH // ================================================================= DockItem { name: "Downloads" iconSource: "../assets/folder-downloads.svg" execCmd: "foot -e sh -c 'cd ~/Downloads; ls -la; exec zsh'" isPinned: true } DockItem { name: "Trash" iconSource: "../assets/trash-empty.svg" execCmd: "foot -e sh -c 'cd ~/.local/share/Trash/files 2>/dev/null || cd ~; ls -la; exec zsh'" isPinned: true } } } // Reusable Dock Item Component component DockItem: Item { id: itemRoot property string name: "" property string iconSource: "" property string execCmd: "" property string appClass: "" property bool isPinned: true signal customClicked() // Check if app is running readonly property bool isRunning: { if (!itemRoot.appClass) return false; const target = itemRoot.appClass.toLowerCase(); if (GlobalState.activeWindowClass.toLowerCase().includes(target)) return true; if (Array.isArray(GlobalState.runningApps)) { for (let i = 0; i < GlobalState.runningApps.length; i++) { const c = GlobalState.runningApps[i]; if ((c.class || "").toLowerCase().includes(target)) return true; } } return false; } implicitWidth: 50 implicitHeight: 64 Layout.alignment: Qt.AlignBottom // Container with scale & bounce transform Item { id: iconWrapper anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom anchors.bottomMargin: 8 width: 48 height: 48 scale: itemArea.containsMouse ? 1.28 : 1.0 y: itemArea.containsMouse ? -8 : 0 Behavior on scale { NumberAnimation { duration: 180; easing.type: Easing.OutCubic } } Behavior on y { NumberAnimation { duration: 180; easing.type: Easing.OutCubic } } // Icon Image Image { anchors.centerIn: parent width: 46 height: 46 source: itemRoot.iconSource sourceSize: Qt.size(92, 92) smooth: true mipmap: true } // Click Bounce Sequential Animation SequentialAnimation { id: bounceAnim NumberAnimation { target: iconWrapper; property: "y"; to: -24; duration: 150; easing.type: Easing.OutQuad } NumberAnimation { target: iconWrapper; property: "y"; to: -4; duration: 130; easing.type: Easing.InQuad } NumberAnimation { target: iconWrapper; property: "y"; to: -14; duration: 100; easing.type: Easing.OutQuad } NumberAnimation { target: iconWrapper; property: "y"; to: 0; duration: 100; easing.type: Easing.InQuad } } } // Active App Running Indicator Dot (White glowing pill) Rectangle { anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom anchors.bottomMargin: 2 width: 4 height: 4 radius: 2 color: Theme.dockDot visible: itemRoot.isRunning } // Hover Tooltip Badge (macOS Catalina Style Black Pill) Rectangle { id: tooltipPill visible: itemArea.containsMouse opacity: itemArea.containsMouse ? 1.0 : 0.0 anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.top anchors.bottomMargin: 6 implicitWidth: tooltipText.implicitWidth + 16 height: 24 radius: 6 color: Qt.rgba(0.08, 0.08, 0.10, 0.92) border.color: Qt.rgba(1.0, 1.0, 1.0, 0.18) border.width: 1 Behavior on opacity { NumberAnimation { duration: 120 } } Text { id: tooltipText anchors.centerIn: parent text: itemRoot.name font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall font.weight: Font.Medium color: "#FFFFFF" } } // Mouse interaction MouseArea { id: itemArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { bounceAnim.start(); if (itemRoot.execCmd !== "") { GlobalState.launchApp(itemRoot.execCmd); } else { itemRoot.customClicked(); } } } } }