Files
NixOS-Configs/home/hypr/quickshell/components/Dock.qml

339 lines
11 KiB
QML

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();
}
}
}
}
}