feat: clickable workspaces, per monitor workspaces, hyprland support

This commit is contained in:
ferreo 2025-07-15 23:18:29 +01:00
parent a9e9dcab18
commit 67eade1c1f
4 changed files with 500 additions and 164 deletions

View file

@ -102,6 +102,7 @@ Scope {
Workspace {
id: workspace
screen: modelData
anchors.horizontalCenter: barBackground.horizontalCenter
anchors.verticalCenter: barBackground.verticalCenter
}

View file

@ -3,19 +3,20 @@ import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Window
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Io
import qs.Settings
import qs.Services
Item {
id: root
property ListModel workspaces: ListModel {}
required property ShellScreen screen
property bool isDestroying: false
property bool hovered: false
signal workspaceChanged(int workspaceId, color accentColor)
property ListModel localWorkspaces: ListModel {}
property real masterProgress: 0.0
property bool effectsActive: false
property color effectColor: Theme.accentPrimary
@ -24,35 +25,62 @@ Item {
property int spacingBetweenPills: 8
width: {
let total = 0
for (let i = 0; i < workspaces.count; i++) {
const ws = workspaces.get(i)
if (ws.isFocused) total += 44
else if (ws.isActive) total += 28
else total += 16
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(workspaces.count - 1, 0) * spacingBetweenPills
total += horizontalPadding * 2
return total
total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills;
total += horizontalPadding * 2;
return total;
}
height: 36
Component.onCompleted: updateWorkspaceList()
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();
}
Connections {
target: Niri
function onWorkspacesChanged() { updateWorkspaceList(); }
function onFocusedWorkspaceIndexChanged() { updateWorkspaceFocus(); }
target: WorkspaceManager
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();
}
}
function triggerUnifiedWave() {
effectColor = Theme.accentPrimary
masterAnimation.restart()
effectColor = Theme.accentPrimary;
masterAnimation.restart();
}
SequentialAnimation {
id: masterAnimation
PropertyAction { target: root; property: "effectsActive"; value: true }
PropertyAction {
target: root
property: "effectsActive"
value: true
}
NumberAnimation {
target: root
property: "masterProgress"
@ -61,41 +89,25 @@ Item {
duration: 1000
easing.type: Easing.OutQuint
}
PropertyAction { target: root; property: "effectsActive"; value: false }
PropertyAction { target: root; property: "masterProgress"; value: 0.0 }
}
function updateWorkspaceList() {
const newList = Niri.workspaces || []
workspaces.clear()
for (let i = 0; i < newList.length; i++) {
const ws = newList[i]
workspaces.append({
id: ws.id,
idx: ws.idx,
name: ws.name || "",
output: ws.output,
isActive: ws.is_active,
isFocused: ws.is_focused,
isUrgent: ws.is_urgent
})
PropertyAction {
target: root
property: "effectsActive"
value: false
}
PropertyAction {
target: root
property: "masterProgress"
value: 0.0
}
updateWorkspaceFocus()
}
function updateWorkspaceFocus() {
const focusedId = Niri.workspaces?.[Niri.focusedWorkspaceIndex]?.id ?? -1
for (let i = 0; i < workspaces.count; i++) {
const ws = workspaces.get(i)
const isFocused = ws.id === focusedId
const isActive = isFocused
if (ws.isFocused !== isFocused || ws.isActive !== isActive) {
workspaces.setProperty(i, "isFocused", isFocused)
workspaces.setProperty(i, "isActive", isActive)
if (isFocused) {
root.triggerUnifiedWave()
root.workspaceChanged(ws.id, Theme.accentPrimary)
}
for (let i = 0; i < localWorkspaces.count; i++) {
const ws = localWorkspaces.get(i);
if (ws.isFocused === true) {
root.triggerUnifiedWave();
root.workspaceChanged(ws.id, Theme.accentPrimary);
break;
}
}
}
@ -128,28 +140,94 @@ Item {
width: root.width - horizontalPadding * 2
x: horizontalPadding
Repeater {
model: root.workspaces
Rectangle {
id: workspacePill
id: workspaceRepeater
model: localWorkspaces
Item {
id: workspacePillContainer
height: 12
width: {
if (model.isFocused) return 44
else if (model.isActive) return 28
else return 16
if (model.isFocused)
return 44;
else if (model.isActive)
return 28;
else
return 16;
}
radius: {
if (model.isFocused) return 12 // half of focused height (if you want to animate this too)
else return 6
Rectangle {
id: workspacePill
anchors.fill: parent
radius: {
if (model.isFocused)
return 12;
else
// half of focused height (if you want to animate this too)
return 6;
}
color: {
if (model.isFocused)
return Theme.accentPrimary;
if (model.isActive)
return Theme.accentPrimary.lighter(130);
if (model.isUrgent)
return Theme.error;
return Qt.lighter(Theme.surfaceVariant, 1.6);
}
scale: model.isFocused ? 1.0 : 0.9
z: 0
ToolTip.visible: pillMouseArea.containsMouse
ToolTip.text: `${model.output}:${model.idx} (ID: ${model.id})`
ToolTip.delay: 500
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
}
}
}
color: {
if (model.isFocused) return Theme.accentPrimary
if (model.isActive) return Theme.accentPrimary.lighter(130)
if (model.isUrgent) return Theme.error
return Qt.lighter(Theme.surfaceVariant, 1.6)
}
scale: model.isFocused ? 1.0 : 0.9
z: 0
// Material 3-inspired smooth animation for width, height, scale, color, opacity, and radius
Behavior on width {
NumberAnimation {
duration: 350
@ -162,43 +240,17 @@ Item {
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
}
}
// Burst effect overlay for focused pill (smaller outline)
Rectangle {
id: pillBurst
anchors.centerIn: parent
width: parent.width + 18 * root.masterProgress
height: parent.height + 18 * root.masterProgress
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.0 - root.masterProgress)
opacity: root.effectsActive && model.isFocused
? (1.0 - root.masterProgress) * 0.7
: 0
opacity: root.effectsActive && model.isFocused ? (1.0 - root.masterProgress) * 0.7 : 0
visible: root.effectsActive && model.isFocused
z: 1
}
@ -206,24 +258,7 @@ Item {
}
}
// MouseArea to open/close Applauncher
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
if (appLauncherPanel && appLauncherPanel.visible) {
appLauncherPanel.hidePanel();
} else if (appLauncherPanel) {
appLauncherPanel.showAt();
}
}
z: 1000 // ensure it's above other content
hoverEnabled: true
onEntered: root.hovered = true
onExited: root.hovered = false
}
Component.onDestruction: {
root.isDestroying = true
root.isDestroying = true;
}
}