diff --git a/components/controls/SplitButton.qml b/components/controls/SplitButton.qml index 37cc89fa..9724f6b6 100644 --- a/components/controls/SplitButton.qml +++ b/components/controls/SplitButton.qml @@ -19,14 +19,15 @@ Row { property bool menuOnTop property string fallbackIcon property string fallbackText + property real minLeftWidth property alias menuItems: menu.items property alias active: menu.active property alias expanded: menu.expanded - property alias menu: menu - property alias iconLabel: iconLabel - property alias label: label - property alias stateLayer: stateLayer + readonly property alias menu: menu + readonly property alias iconLabel: iconLabel + readonly property alias label: label + readonly property alias stateLayer: stateLayer property color colour: type == SplitButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondaryContainer property color textColour: type == SplitButton.Filled ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondaryContainer @@ -41,14 +42,14 @@ Row { bottomRightRadius: Tokens.rounding.medium / 2 color: root.disabled ? root.disabledColour : root.colour - implicitWidth: textRow.implicitWidth + root.horizontalPadding * 2 + implicitWidth: Math.max(root.minLeftWidth, textRow.implicitWidth + root.horizontalPadding * 2) implicitHeight: expandBtn.implicitHeight StateLayer { id: stateLayer - rect.topRightRadius: parent.topRightRadius - rect.bottomRightRadius: parent.bottomRightRadius + topRightRadius: parent.topRightRadius + bottomRightRadius: parent.bottomRightRadius color: root.textColour disabled: root.disabled onClicked: root.active?.clicked() diff --git a/modules/dashboard/Performance.qml b/modules/dashboard/Performance.qml index 7f341433..9b3c9a69 100644 --- a/modules/dashboard/Performance.qml +++ b/modules/dashboard/Performance.qml @@ -125,10 +125,8 @@ Item { visible: Config.dashboard.performance.showMemory } - StorageGaugeCard { - Layout.minimumWidth: 250 - Layout.preferredHeight: 220 - Layout.fillWidth: !Config.dashboard.performance.showNetwork + StorageCard { + Layout.fillHeight: true visible: Config.dashboard.performance.showStorage } @@ -385,132 +383,6 @@ Item { } } - component StorageGaugeCard: StyledRect { - id: storageGaugeCard - - property int currentDiskIndex: 0 - readonly property var currentDisk: SystemUsage.disks.length > 0 ? SystemUsage.disks[currentDiskIndex] : null - property int diskCount: 0 - readonly property real arcStartAngle: 0.75 * Math.PI - readonly property real arcSweep: 1.5 * Math.PI - property real animatedPercentage: 0 - property color accentColor: Colours.palette.m3secondary - - color: Colours.tPalette.m3surfaceContainer - radius: Tokens.rounding.extraLarge - clip: true - Component.onCompleted: { - diskCount = SystemUsage.disks.length; - if (currentDisk) - animatedPercentage = currentDisk.perc; - } - onCurrentDiskChanged: { - if (currentDisk) - animatedPercentage = currentDisk.perc; - } - - // Update diskCount and animatedPercentage when disks data changes - Connections { - function onDisksChanged() { - if (SystemUsage.disks.length !== storageGaugeCard.diskCount) - storageGaugeCard.diskCount = SystemUsage.disks.length; - - // Update animated percentage when disk data refreshes - if (storageGaugeCard.currentDisk) - storageGaugeCard.animatedPercentage = storageGaugeCard.currentDisk.perc; - } - - target: SystemUsage - } - - MouseArea { - anchors.fill: parent - onWheel: wheel => { - if (wheel.angleDelta.y > 0) - storageGaugeCard.currentDiskIndex = (storageGaugeCard.currentDiskIndex - 1 + storageGaugeCard.diskCount) % storageGaugeCard.diskCount; - else if (wheel.angleDelta.y < 0) - storageGaugeCard.currentDiskIndex = (storageGaugeCard.currentDiskIndex + 1) % storageGaugeCard.diskCount; - } - } - - ColumnLayout { - anchors.fill: parent - anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.medium - - CardHeader { - icon: "hard_disk" - title: { - const base = qsTr("Storage"); - if (!storageGaugeCard.currentDisk) - return base; - - return `${base} - ${storageGaugeCard.currentDisk.mount}`; - } - accentColor: storageGaugeCard.accentColor - - // Scroll hint icon - MaterialIcon { - text: "unfold_more" - color: Colours.palette.m3onSurfaceVariant - fontStyle: Tokens.font.icon.medium - visible: storageGaugeCard.diskCount > 1 - opacity: 0.7 - ToolTip.visible: hintHover.hovered - ToolTip.text: qsTr("Scroll to switch disks") - ToolTip.delay: 500 - - HoverHandler { - id: hintHover - } - } - } - - Item { - Layout.fillWidth: true - Layout.fillHeight: true - - ArcGauge { - anchors.centerIn: parent - width: Math.min(parent.width, parent.height) - height: width - percentage: storageGaugeCard.animatedPercentage - accentColor: storageGaugeCard.accentColor - trackColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) - startAngle: storageGaugeCard.arcStartAngle - sweepAngle: storageGaugeCard.arcSweep - } - - StyledText { - anchors.centerIn: parent - text: storageGaugeCard.currentDisk ? `${Math.round(storageGaugeCard.currentDisk.perc * 100)}%` : "—" - font: Tokens.font.body.builders.large.size(28).weight(Font.Medium).build() - color: storageGaugeCard.accentColor - } - } - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: { - if (!storageGaugeCard.currentDisk) - return "—"; - - const usedFmt = SystemUsage.formatKib(storageGaugeCard.currentDisk.used); - const totalFmt = SystemUsage.formatKib(storageGaugeCard.currentDisk.total); - return `${usedFmt.value.toFixed(1)} / ${Math.floor(totalFmt.value)} ${totalFmt.unit}`; - } - font: Tokens.font.body.small - color: Colours.palette.m3onSurfaceVariant - } - } - - Behavior on animatedPercentage { - Anim { - type: Anim.StandardLarge - } - } - } - component NetworkCard: StyledRect { id: networkCard diff --git a/modules/dashboard/performance/StorageCard.qml b/modules/dashboard/performance/StorageCard.qml new file mode 100644 index 00000000..c143121d --- /dev/null +++ b/modules/dashboard/performance/StorageCard.qml @@ -0,0 +1,130 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Caelestia.Config +import qs.components +import qs.components.controls +import qs.services + +StyledRect { + id: root + + readonly property color accent: Colours.palette.m3secondary + readonly property real percentage: SystemUsage.primaryDisk?.perc ?? 0 + + color: Colours.tPalette.m3surfaceContainer + radius: Tokens.rounding.extraLarge + + implicitWidth: layout.implicitWidth + layout.anchors.margins * 2 + implicitHeight: layout.implicitHeight + Tokens.padding.large * 2 + + ColumnLayout { + id: layout + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Tokens.padding.extraLarge + spacing: 0 + + RowLayout { + id: row + + spacing: Tokens.spacing.large + + CircularProgress { + fgColour: root.accent + value: root.percentage + implicitSize: usageColumn.implicitHeight + thickness + Tokens.padding.large * 2 + startAngle: -225 + sweepAngle: 270 + + Behavior on clampedVal { + Anim {} + } + + ColumnLayout { + id: usageColumn + + anchors.centerIn: parent + spacing: 0 + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "hard_drive" + color: root.accent + fontStyle: Tokens.font.icon.medium + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: Math.round(root.percentage * 100) + "%" + font: Tokens.font.title.builders.large.width(90).build() + color: root.accent + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Used") + font: Tokens.font.body.small + color: Colours.palette.m3onSurfaceVariant + } + } + } + + ColumnLayout { + Layout.minimumWidth: Tokens.sizes.dashboard.perfStorageTextWidth + spacing: Tokens.spacing.extraSmall + + StyledText { + text: qsTr("Storage") + font: Tokens.font.title.medium + } + + StyledText { + text: { + if (!SystemUsage.primaryDisk) + return qsTr("No disks detected"); + + const usedFmt = SystemUsage.formatKib(SystemUsage.primaryDisk.used); + const totalFmt = SystemUsage.formatKib(SystemUsage.primaryDisk.total); + return `${usedFmt.value.toFixed(1)} / ${Math.floor(totalFmt.value)} ${totalFmt.unit}`; + } + font: Tokens.font.body.large + color: root.accent + } + } + } + + SplitButton { + Layout.alignment: Qt.AlignHCenter + + type: SplitButton.Tonal + disabled: !SystemUsage.disks.length + fallbackIcon: "storage" + fallbackText: qsTr("No disks") + menuOnTop: true + minLeftWidth: row.implicitWidth * 0.6 + + menuItems: disks.instances + active: menuItems.find(m => m.modelData === SystemUsage.primaryDisk) ?? menuItems[0] ?? null + menu.onItemSelected: item => SystemUsage.manualPrimaryDisk = (item as DiskItem).modelData + + Variants { + id: disks + + model: SystemUsage.disks + + DiskItem {} + } + } + } + + component DiskItem: MenuItem { + required property var modelData + + icon: modelData === SystemUsage.primaryDisk ? "check" : "" + text: modelData.mount + activeIcon: "storage" + } +} diff --git a/plugin/src/Caelestia/Config/tokens.hpp b/plugin/src/Caelestia/Config/tokens.hpp index 818cf438..82f01915 100644 --- a/plugin/src/Caelestia/Config/tokens.hpp +++ b/plugin/src/Caelestia/Config/tokens.hpp @@ -191,6 +191,7 @@ class DashboardTokens : public ConfigObject { CONFIG_PROPERTY(int, mediaVisualiserSize, 80) CONFIG_PROPERTY(int, perfHeroCardWidth, 400) CONFIG_PROPERTY(int, perfUsageShapeSize, 100) + CONFIG_PROPERTY(int, perfStorageTextWidth, 160) public: explicit DashboardTokens(QObject* parent = nullptr) diff --git a/services/SystemUsage.qml b/services/SystemUsage.qml index 15dda611..cb1ff346 100644 --- a/services/SystemUsage.qml +++ b/services/SystemUsage.qml @@ -38,6 +38,8 @@ Singleton { // Individual disks: Array of { mount, used, total, free, perc } property var disks: [] + property var manualPrimaryDisk + readonly property var primaryDisk: manualPrimaryDisk ?? disks[0] ?? null property real lastCpuIdle property real lastCpuTotal