From abd48a9c66896b1db310c09b268f0fb7eb92ba98 Mon Sep 17 00:00:00 2001 From: tux Date: Mon, 24 Aug 2026 18:18:10 +0530 Subject: [PATCH] feat(osd): add volume osd popup window --- config/Theme.qml | 9 +++++++ modules/osd/VolumeCard.qml | 42 ++++++++++++++++++++++++++++++ services/Audio.qml | 2 ++ shell.qml | 1 + windows/Volume.qml | 52 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 106 insertions(+) create mode 100644 modules/osd/VolumeCard.qml create mode 100644 windows/Volume.qml diff --git a/config/Theme.qml b/config/Theme.qml index 001a609..26059c2 100644 --- a/config/Theme.qml +++ b/config/Theme.qml @@ -138,4 +138,13 @@ Singleton { property int textSpacing: 2 property real bodyOpacity: 0.8 } + + property QtObject volume: QtObject { + property int width: 300 + property int height: 48 + property int iconSize: 22 + property int barHeight: 10 + property int timeout: 1500 + property real bottomMarginRatio: 0.2 + } } diff --git a/modules/osd/VolumeCard.qml b/modules/osd/VolumeCard.qml new file mode 100644 index 0000000..bce8441 --- /dev/null +++ b/modules/osd/VolumeCard.qml @@ -0,0 +1,42 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import qs.config +import qs.services +import qs.ui + +BarButton { + id: root + + readonly property real level: Math.max(0, Math.min(Audio.muted ? 0 : Audio.volume, 1)) + readonly property url iconUrl: `${Quickshell.shellPath("assets")}/icons/speaker.svg` + + hoverHighlight: false + pointerCursor: false + iconOnly: true + iconSource: root.iconUrl + icon.width: Theme.volume.iconSize + icon.height: Theme.volume.iconSize + + implicitWidth: Theme.volume.width + implicitHeight: Theme.volume.height + + Rectangle { + id: fill + + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + implicitWidth: parent.width * root.level + radius: Theme.radius + color: Audio.muted ? Theme.colors.error : Theme.colors.accent + opacity: 0.35 + + Behavior on width { + NumberAnimation { + duration: Theme.duration + } + } + } +} diff --git a/services/Audio.qml b/services/Audio.qml index 93f3ca9..fb07121 100644 --- a/services/Audio.qml +++ b/services/Audio.qml @@ -11,6 +11,8 @@ Singleton { readonly property string inputName: truncateText(root.displayName(Pipewire.defaultAudioSource), 14) readonly property url outputIcon: `${Quickshell.shellPath("assets")}/icons/speaker.svg` readonly property url inputIcon: `${Quickshell.shellPath("assets")}/icons/microphone.svg` + readonly property real volume: Pipewire.defaultAudioSink?.audio.volume ?? 0 + readonly property bool muted: Pipewire.defaultAudioSink?.audio.muted ?? false function truncateText(text, maxLength) { return text.length > maxLength ? text.slice(0, maxLength - 3) + "..." : text; diff --git a/shell.qml b/shell.qml index 80b77cb..66a9d6b 100644 --- a/shell.qml +++ b/shell.qml @@ -4,4 +4,5 @@ import qs.windows Scope { Bar {} Notification {} + Volume {} } diff --git a/windows/Volume.qml b/windows/Volume.qml new file mode 100644 index 0000000..9c4df0a --- /dev/null +++ b/windows/Volume.qml @@ -0,0 +1,52 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import qs.config +import qs.services +import "../modules/osd" + +PanelWindow { + id: root + + property bool shown: false + + color: "transparent" + exclusionMode: ExclusionMode.Ignore + visible: shown + implicitWidth: card.implicitWidth + implicitHeight: card.implicitHeight + + anchors.bottom: true + margins.bottom: screen.height * Theme.volume.bottomMarginRatio + + // An empty click mask prevents the window from blocking mouse events. + mask: Region {} + + Connections { + target: Audio + + function onVolumeChanged() { + root.shown = true; + hideTimer.restart(); + } + + function onMutedChanged() { + root.shown = true; + hideTimer.restart(); + } + } + + Timer { + id: hideTimer + + interval: Theme.volume.timeout + onTriggered: root.shown = false + } + + VolumeCard { + id: card + + anchors.fill: parent + } +}