import Quickshell import Quickshell.Wayland import Quickshell.Services.Pipewire import QtQuick import QtQuick.Layouts import "../../config" PanelWindow { id: osd visible: false color: "transparent" exclusiveZone: 0 // Bottom-center: anchoring only the bottom edge lets the layer shell // center it horizontally. anchors { bottom: true } margins.bottom: Appearance.spacing * 4 implicitWidth: 300 implicitHeight: 60 WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.namespace: "quickshell:osd" readonly property var sink: Pipewire.defaultAudioSink PwObjectTracker { objects: osd.sink ? [osd.sink] : [] } Timer { id: hideTimer interval: 1400 onTriggered: osd.visible = false } Connections { target: osd.sink ? osd.sink.audio : null function onVolumeChanged() { osd.visible = true; hideTimer.restart() } function onMutedChanged() { osd.visible = true; hideTimer.restart() } } Rectangle { anchors.fill: parent radius: Appearance.radius color: Colors.surface border.color: Colors.muted border.width: 1 RowLayout { anchors.fill: parent anchors.margins: 15 spacing: 13 Text { font.family: Appearance.iconFontFamilyRounded font.variableAxes: Appearance.iconFontAxes font.pixelSize: 21 color: Colors.foreground text: { const audio = osd.sink ? osd.sink.audio : null if (!audio || audio.muted || audio.volume <= 0) return "volume_off" return audio.volume < 0.5 ? "volume_down" : "volume_up" } } Item { id: slider Layout.fillWidth: true implicitHeight: 18 readonly property real value: osd.sink && osd.sink.audio ? osd.sink.audio.volume : 0 readonly property bool enabled: !!osd.sink Rectangle { id: track anchors.verticalCenter: parent.verticalCenter width: parent.width height: 5 radius: 0 color: Colors.muted opacity: 0.35 } Rectangle { anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left width: track.width * Math.max(0, Math.min(1, slider.value)) height: 5 radius: 0 color: Colors.primary opacity: slider.enabled ? 1 : 0.4 } Rectangle { width: 14 height: 14 radius: 7 color: Colors.primary opacity: slider.enabled ? 1 : 0.4 anchors.verticalCenter: parent.verticalCenter x: Math.max(0, Math.min(track.width - width, track.width * slider.value - width / 2)) } MouseArea { anchors.fill: parent enabled: slider.enabled cursorShape: Qt.PointingHandCursor function updateFromX(mx) { if (!osd.sink || !osd.sink.audio) return osd.sink.audio.volume = Math.max(0, Math.min(1, mx / track.width)) } onPressed: (mouse) => updateFromX(mouse.x) onPositionChanged: (mouse) => { if (pressed) updateFromX(mouse.x) } } } Text { Layout.preferredWidth: 38 horizontalAlignment: Text.AlignRight font.family: Appearance.fontFamily font.pixelSize: 13 color: Colors.muted text: Math.round(slider.value * 100) + "%" } } } }