import Qt5Compat.GraphicalEffects import QtQuick import QtQuick.Controls import QtQuick.Layouts import QtQuick.Window import Quickshell import Quickshell.Io import qs.Constants import qs.Services Item { id: root required property ShellScreen screen property bool hovered: false property ListModel localWorkspaces property real masterProgress: 0 property bool effectsActive: false property color effectColor: Colors.primary property int horizontalPadding: 16 property int spacingBetweenPills: 8 property bool isDestroying: false signal workspaceChanged(int workspaceId, color primaryColor) function triggerUnifiedWave() { effectColor = Colors.primary; masterAnimation.restart(); } function updateWorkspaceFocus() { for (let i = 0; i < localWorkspaces.count; i++) { const ws = localWorkspaces.get(i); if (ws.isFocused === true) { root.triggerUnifiedWave(); root.workspaceChanged(ws.id, Colors.primary); break; } } } implicitWidth: { let total = 0; for (let i = 0; i < localWorkspaces.count; i++) { const ws = localWorkspaces.get(i); if (ws.isFocused) total += 44; else if (ws.isActive) total += 28; else total += 16; } total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills; total += horizontalPadding * 2; return total; } height: parent.height Component.onCompleted: { localWorkspaces.clear(); for (let i = 0; i < WorkspaceManager.workspaces.count; i++) { const ws = WorkspaceManager.workspaces.get(i); if (ws.output.toLowerCase() === screen.name.toLowerCase()) localWorkspaces.append(ws); } workspaceRepeater.model = localWorkspaces; updateWorkspaceFocus(); } Component.onDestruction: { root.isDestroying = true; } Connections { function onWorkspacesChanged() { localWorkspaces.clear(); for (let i = 0; i < WorkspaceManager.workspaces.count; i++) { const ws = WorkspaceManager.workspaces.get(i); if (ws.output.toLowerCase() === screen.name.toLowerCase()) localWorkspaces.append(ws); } workspaceRepeater.model = localWorkspaces; updateWorkspaceFocus(); } target: WorkspaceManager } SequentialAnimation { id: masterAnimation PropertyAction { target: root property: "effectsActive" value: true } NumberAnimation { target: root property: "masterProgress" from: 0 to: 1 duration: 1000 easing.type: Easing.OutQuint } PropertyAction { target: root property: "effectsActive" value: false } PropertyAction { target: root property: "masterProgress" value: 0 } } Rectangle { id: workspaceBackground width: parent.width - 15 height: 26 anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter radius: 12 color: Colors.transparent layer.enabled: true layer.effect: DropShadow { color: "black" radius: 12 samples: 24 verticalOffset: 0 horizontalOffset: 0 opacity: 0.1 } } Row { id: pillRow spacing: spacingBetweenPills anchors.verticalCenter: workspaceBackground.verticalCenter width: root.width - horizontalPadding * 2 x: horizontalPadding Repeater { id: workspaceRepeater model: localWorkspaces Item { id: workspacePillContainer height: 12 width: { if (model.isFocused) return 44; else if (model.isActive) return 28; else return 16; } Rectangle { // half of focused height (if you want to animate this too) id: workspacePill anchors.fill: parent radius: { if (model.isFocused) return 12; else return 6; } color: { if (model.isFocused) return Colors.primary; if (model.isActive) return Colors.primary.lighter(130); if (model.isUrgent) return Theme.error; return Colors.surface2; } scale: model.isFocused ? 1 : 0.9 z: 0 MouseArea { id: pillMouseArea anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { WorkspaceManager.switchToWorkspace(model.idx); } z: 20 hoverEnabled: true } // Material 3-inspired smooth animation for width, height, scale, color, opacity, and radius Behavior on width { NumberAnimation { duration: 350 easing.type: Easing.OutBack } } Behavior on height { NumberAnimation { duration: 350 easing.type: Easing.OutBack } } Behavior on scale { NumberAnimation { duration: 300 easing.type: Easing.OutBack } } Behavior on color { ColorAnimation { duration: 200 easing.type: Easing.InOutCubic } } Behavior on opacity { NumberAnimation { duration: 200 easing.type: Easing.InOutCubic } } Behavior on radius { NumberAnimation { duration: 350 easing.type: Easing.OutBack } } } Rectangle { id: pillBurst anchors.centerIn: workspacePillContainer width: workspacePillContainer.width + 18 * root.masterProgress height: workspacePillContainer.height + 18 * root.masterProgress radius: width / 2 color: "transparent" border.color: root.effectColor border.width: 2 + 6 * (1 - root.masterProgress) opacity: root.effectsActive && model.isFocused ? (1 - root.masterProgress) * 0.7 : 0 visible: root.effectsActive && model.isFocused z: 1 } Behavior on width { NumberAnimation { duration: 350 easing.type: Easing.OutBack } } Behavior on height { NumberAnimation { duration: 350 easing.type: Easing.OutBack } } } } } localWorkspaces: ListModel { } }