quickshell-toki-night/modules/dashboard/Workspaces.qml

496 lines
21 KiB
QML

import qs.modules.bar.popouts as BarPopouts
import qs.config
import qs.custom
import qs.services
import qs.util
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import Quickshell.Widgets
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
Item {
id: root
required property PersistentProperties uiState
required property BarPopouts.Wrapper popouts
width: Config.dashboard.workspaceWidth + 32
height: 750
property HyprlandWorkspace dragSourceWorkspace: null
property HyprlandWorkspace dragTargetWorkspace: null
// Controls scrolling at edge of panel while dragging
property real dragScrollVelocity: 0
CustomListView {
id: list
anchors.fill: parent
anchors.margins: 12
anchors.rightMargin: 0
spacing: 12
clip: true
CustomScrollBar.vertical: CustomScrollBar {
flickable: list
}
model: Hypr.monitors
delegate: Item {
id: monitor
required property int index
required property HyprlandMonitor modelData
readonly property ListModel workspaces: States.screens.get(modelData).workspaces
width: list.width - 20
height: childrenRect.height
Item {
id: monitorHeader
anchors.left: parent.left
anchors.right: parent.right
height: monIcon.height
MaterialIcon {
id: monIcon
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
text: "desktop_windows"
color: Config.colors.tertiary
}
CustomText {
id: monText
anchors.verticalCenter: parent.verticalCenter
anchors.left: monIcon.right
anchors.leftMargin: 4
text: monitor.modelData.name
color: Config.colors.tertiary
font.family: Config.font.family.mono
font.pointSize: Config.font.size.smaller
}
CustomRect {
anchors.verticalCenter: parent.verticalCenter
anchors.left: monText.right
anchors.right: parent.right
anchors.leftMargin: 8
anchors.rightMargin: 18
height: 1
color: Config.colors.inactive
}
}
ListView {
id: workspaceList
anchors.top: monitorHeader.bottom
anchors.topMargin: 8
width: list.width - 20
spacing: 22
height: contentHeight
acceptedButtons: Qt.NoButton
boundsBehavior: Flickable.StopAtBounds
model: monitor.workspaces
delegate: Column {
id: entry
required property int index
required property HyprlandWorkspace workspace
width: list.width - 20
spacing: 5
property bool hoveredWhileDragging: false
z: root.dragSourceWorkspace === workspace ? 10 : 0
Item {
id: header
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: labelState.height
property color nonAnimColor: entry.hoveredWhileDragging ? Config.colors.workspaceMove :
entry.workspace?.active ? Config.colors.workspaces : Config.colors.primary
property color color: nonAnimColor
Behavior on color {
CAnim {}
}
StateLayer {
id: labelState
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
implicitWidth: label.width + 20
implicitHeight: label.height + 8
color: Config.colors.secondary
function onClicked(): void {
Hypr.dispatch(`workspace ${entry.workspace.id}`)
}
Row {
id: label
anchors.centerIn: parent
spacing: 7
MaterialIcon {
text: Icons.getWorkspaceIcon(entry.workspace)
color: header.color
font.pointSize: Config.font.size.larger
animate: true
animateDuration: Config.anim.durations.small
}
CustomText {
anchors.top: parent.top
anchors.topMargin: 1
text: qsTr("Workspace %1").arg(index + 1)
font.family: Config.font.family.mono
font.pointSize: Config.font.size.normal
color: header.color
}
}
}
StateLayer {
anchors.verticalCenter: parent.verticalCenter
anchors.right: downArrow.left
anchors.rightMargin: 2
implicitWidth: implicitHeight
implicitHeight: upIcon.height + 6
disabled: entry.index === 0 && monitor.index === 0
function onClicked(): void {
if (entry.index !== 0) {
monitor.workspaces.move(entry.index, entry.index - 1, 1)
} else {
const workspace = entry.workspace;
monitor.workspaces.remove(entry.index, 1);
const otherWorkspaces = list.itemAtIndex(monitor.index - 1).workspaces;
otherWorkspaces.insert(0, {"workspace": workspace});
}
}
MaterialIcon {
id: upIcon
anchors.centerIn: parent
text: "keyboard_arrow_up"
color: parent.disabled ? Config.colors.inactive : header.nonAnimColor
font.pointSize: Config.font.size.larger
Behavior on color {
CAnim {}
}
}
}
StateLayer {
id: downArrow
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
implicitWidth: implicitHeight
implicitHeight: downIcon.height + 6
disabled: entry.index === monitor.workspaces.count - 1 && monitor.index === list.count - 1
function onClicked(): void {
if (entry.index !== monitor.workspaces.count - 1) {
root.uiState.workspaces.move(entry.index, entry.index + 1, 1)
} else {
const workspace = entry.workspace;
monitor.workspaces.remove(entry.index, 1);
const otherWorkspaces = list.itemAtIndex(monitor.index + 1).workspaces;
otherWorkspaces.append({"workspace": workspace});
}
}
MaterialIcon {
id: downIcon
anchors.centerIn: parent
text: "keyboard_arrow_down"
color: parent.disabled ? Config.colors.inactive : header.nonAnimColor
font.pointSize: Config.font.size.larger
Behavior on color {
CAnim {}
}
}
}
}
CustomRect {
id: preview
anchors.left: parent.left
anchors.right: parent.right
readonly property HyprlandMonitor mon: entry.workspace.monitor
// Exclude UI border and apply monitor scale
readonly property real monX: Config.border.thickness
readonly property real monY: Config.bar.height
readonly property real monWidth: (mon.width / mon.scale) - 2 * Config.border.thickness
readonly property real monHeight: (mon.height / mon.scale) - Config.bar.height - Config.border.thickness
readonly property real aspectRatio: monHeight / monWidth
readonly property real sizeRatio: Config.dashboard.workspaceWidth / monWidth
width: Config.dashboard.workspaceWidth
height: aspectRatio * width
radius: 7
color: entry.hoveredWhileDragging ? Config.colors.workspaceMove :
entry.workspace?.active ? Color.mute(Config.colors.workspaces, 1.5, 1.1) : Config.colors.container
Behavior on color {
CAnim {}
}
DropArea {
anchors.fill: parent
onEntered: {
root.dragTargetWorkspace = entry.workspace;
entry.hoveredWhileDragging = true;
}
onExited: {
if (root.draggingTargetWorkspace === entry.workspace)
root.draggingTargetWorkspace = null;
entry.hoveredWhileDragging = false;
}
}
Repeater {
anchors.fill: parent
model: entry.workspace?.toplevels
delegate: Item {
id: window
required property HyprlandToplevel modelData
property var ipc: modelData.lastIpcObject
opacity: ipc && ipc.at && !ipc.hidden ? 1 : 0
property real nonAnimX: ipc?.at ? (ipc.at[0] - preview.monX) * preview.sizeRatio : 0
property real nonAnimY: ipc?.at ? (ipc.at[1] - preview.monY) * preview.sizeRatio : 0
property real nonAnimWidth: ipc?.size ? ipc.size[0] * preview.sizeRatio : 0
property real nonAnimHeight: ipc?.size ? ipc.size[1] * preview.sizeRatio : 0
x: nonAnimX
y: nonAnimY
width: nonAnimWidth
height: nonAnimHeight
z: nonAnimX !== x || nonAnimY !== y ? 10 : 0
Behavior on x {
enabled: window.x !== 0
Anim {}
}
Behavior on y {
enabled: window.y !== 0
Anim {}
}
Behavior on width {
enabled: window.width !== 0
Anim {}
}
Behavior on height {
enabled: window.height !== 0
Anim {}
}
Behavior on opacity { Anim {} }
ScreencopyView {
id: view
visible: false
anchors.left: parent.left
anchors.top: parent.top
layer.enabled: true
// NOTE: Simulates cropping fill (which ScreencopyView does not natively support)
readonly property real aspectRatio: sourceSize.height / sourceSize.width
width: Math.max(window.nonAnimWidth, window.nonAnimHeight / aspectRatio)
height: Math.max(window.nonAnimHeight, window.nonAnimWidth * aspectRatio)
captureSource: window.modelData.wayland
live: true
CustomRect {
anchors.fill: parent
color: mouse.drag.active ?
Qt.tint(Qt.alpha(Config.colors.overlay, 0.7), Qt.alpha(Config.colors.workspaceMove, 0.2)) :
mouse.pressed ?
Qt.tint(Qt.alpha(Config.colors.overlay, 0.7), Qt.alpha(Config.colors.workspaces, 0.2)) :
mouse.containsMouse ?
Qt.tint(Qt.alpha(Config.colors.overlay, 0.5), Qt.alpha(Config.colors.workspaces, 0.1)) :
Qt.alpha(Config.colors.overlay, 0.5)
Behavior on color {
CAnim { duration: Config.anim.durations.small }
}
}
}
Item {
id: mask
visible: false
anchors.fill: view
layer.enabled: true
Rectangle {
width: window.width
height: window.height
radius: 8
}
}
MultiEffect {
anchors.fill: view
source: view
maskEnabled: true
maskSource: mask
}
IconImage {
id: icon
anchors.centerIn: parent
implicitSize: Math.min(48, window.width * 0.5, window.height * 0.5)
source: Icons.getAppIcon(window.ipc?.class ?? "", "")
}
// Interactions
Drag.active: mouse.drag.active
Drag.source: window
Drag.hotSpot.x: nonAnimWidth / 2
Drag.hotSpot.y: nonAnimHeight / 2
MouseArea {
id: mouse
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.AllButtons
drag.target: parent
onPressed: event => {
root.dragSourceWorkspace = entry.workspace;
window.Drag.hotSpot.x = event.x;
window.Drag.hotSpot.y = event.y;
}
onReleased: event => {
const targetWorkspace = root.dragTargetWorkspace;
root.dragSourceWorkspace = null;
root.dragScrollVelocity = 0;
if (targetWorkspace !== null && targetWorkspace !== entry.workspace) {
Hyprland.dispatch(`movetoworkspacesilent ${targetWorkspace.id}, address:0x${window.modelData.address}`)
} else {
window.x = Qt.binding(() => window.nonAnimX);
window.y = Qt.binding(() => window.nonAnimY);
}
}
onClicked: event => {
if (event.button === Qt.LeftButton) {
root.uiState.dashboard = false;
Hypr.dispatch(`focuswindow address:0x${window.modelData.address}`);
} else if (event.button === Qt.MiddleButton) {
Hypr.dispatch(`closewindow address:0x${window.modelData.address}`);
} else if (event.button === Qt.RightButton) {
root.uiState.dashboard = false;
popouts.currentName = "activewindow";
popouts.currentCenter = QsWindow.window.width / 2;
popouts.window = window.modelData;
popouts.hasCurrent = true;
}
}
onPositionChanged: event => {
if (!drag.active) return;
const y = root.mapFromItem(this, 0, event.y).y;
if (y < 100) {
root.dragScrollVelocity = (y - 100) / 25;
} else if (y > root.height - 100) {
root.dragScrollVelocity = (y - root.height + 100) / 25;
} else {
root.dragScrollVelocity = 0;
}
}
}
}
}
}
}
add: Transition {
Anim {
property: "opacity"
from: 0
to: 1
}
}
remove: Transition {
Anim {
property: "opacity"
from: 1
to: 0
}
}
move: Transition {
Anim {
property: "y"
}
Anim {
properties: "opacity"
to: 1
}
}
displaced: Transition {
Anim {
property: "y"
}
Anim {
properties: "opacity"
to: 1
}
}
}
}
}
Timer {
running: root.dragScrollVelocity !== 0
repeat: true
interval: 10
onTriggered: {
list.contentY += root.dragScrollVelocity;
list.returnToBounds();
}
}
}