// Volume row: mute button, slider, and a chevron that opens the device picker. // // PwObjectTracker is mandatory — without it node.audio.volume reads 0 and // writes are dropped on the floor with no error. import QtQuick import Quickshell import Quickshell.Services.Pipewire import qs.widgets import qs.config Item { id: root property var node: null // Output vs input, purely for icon selection. property bool output: true property bool expanded: false signal toggleExpanded implicitHeight: 32 visible: root.node !== null readonly property real volume: root.node && root.node.audio ? root.node.audio.volume : 0 readonly property bool muted: root.node && root.node.audio ? root.node.audio.muted : false // Over-amplification extends the output to 150%. The input is never // extended: a microphone above 100% is gain on noise, not loudness. readonly property real maximum: root.output && Settings.overAmplification ? 1.5 : 1 PwObjectTracker { objects: root.node ? [root.node] : [] } function iconName(): string { if (root.output) { if (root.muted || root.volume <= 0.001) return "audio-volume-muted-symbolic"; if (root.volume < 0.34) return "audio-volume-low-symbolic"; if (root.volume < 0.67) return "audio-volume-medium-symbolic"; return "audio-volume-high-symbolic"; } return root.muted ? "microphone-sensitivity-muted-symbolic" : "audio-input-microphone-symbolic"; } IconButton { id: muteButton anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter size: 30 iconSize: 17 icon: root.iconName() iconFallback: root.output ? "audio-volume-high-symbolic" : "audio-input-microphone-symbolic" onClicked: { if (root.node && root.node.audio) root.node.audio.muted = !root.node.audio.muted; } } ValueSlider { id: slider anchors.left: muteButton.right anchors.leftMargin: 6 anchors.right: chevron.left anchors.rightMargin: 4 anchors.verticalCenter: parent.verticalCenter value: root.muted ? 0 : root.volume / root.maximum onMoved: v => { if (!root.node || !root.node.audio) return; // Nudging the slider is also how you unmute, same as GNOME. root.node.audio.muted = false; root.node.audio.volume = v * root.maximum; } } // Everything above 100%, marked over the track so the loud end of the // slider is visibly the loud end rather than more of the same. Rectangle { anchors.right: slider.right anchors.verticalCenter: slider.verticalCenter width: root.maximum > 1 ? slider.width * (1 - 1 / root.maximum) : 0 height: 10 radius: 2 visible: root.maximum > 1 color: Theme.alpha(Theme.warn, 0.30) border.width: 0 } Rectangle { anchors.verticalCenter: slider.verticalCenter x: slider.x + slider.width / root.maximum - 1 width: 2 height: 16 radius: 1 visible: root.maximum > 1 color: Theme.alpha(Theme.warn, 0.7) border.width: 0 } IconButton { id: chevron anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter size: 28 iconSize: 14 icon: root.expanded ? "pan-up-symbolic" : "pan-down-symbolic" iconFallback: "go-next-symbolic" onClicked: root.toggleExpanded() } }