Give Sound the whole story, and keep the buttons inside the card
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
// The level of the device a card is about, written straight through to
|
||||
// PipeWire -- there is nothing to apply, so there is nothing to commit and
|
||||
// nothing to debounce.
|
||||
//
|
||||
// The range is a property rather than a constant because over-amplification
|
||||
// extends it to 150%. The track keeps its full length either way, so the same
|
||||
// slider position always means the same level; the region past 100% is marked
|
||||
// in the warning tone, because that is the part that can distort.
|
||||
//
|
||||
// Not a SettingRow: the control stacks below the label in a narrow window
|
||||
// exactly as SliderRow and GradientSliderRow do, which SettingRow's fixed
|
||||
// trailing column cannot.
|
||||
|
||||
import QtQuick
|
||||
import Quickshell.Services.Pipewire
|
||||
import qs.config
|
||||
import qs.widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var node: null
|
||||
property string label: "Volume"
|
||||
property string detail: ""
|
||||
property bool divider: true
|
||||
property real maximum: 1
|
||||
|
||||
readonly property bool available: !!root.node?.audio
|
||||
readonly property real volume: root.node?.audio?.volume ?? 0
|
||||
readonly property bool muted: root.node?.audio?.muted ?? false
|
||||
readonly property bool amplified: root.volume > 1.001
|
||||
|
||||
readonly property bool inline: root.width >= 520
|
||||
readonly property int controlSpan: 300
|
||||
|
||||
width: parent ? parent.width : 620
|
||||
implicitHeight: root.inline
|
||||
? Math.max(56, copy.implicitHeight + 20)
|
||||
: copy.implicitHeight + 32 + 30
|
||||
opacity: root.available ? 1 : 0.45
|
||||
|
||||
// Reading or writing audio state on an untracked node silently returns
|
||||
// zero, so the row binds the node it speaks for even though the device list
|
||||
// above it is already tracking the same object.
|
||||
PwObjectTracker {
|
||||
objects: root.node ? [root.node] : []
|
||||
}
|
||||
|
||||
function setVolume(ratio: real): void {
|
||||
if (!root.node?.audio)
|
||||
return;
|
||||
const value = Math.max(0, Math.min(root.maximum, ratio * root.maximum));
|
||||
// Moving the slider is also how you unmute, same as GNOME.
|
||||
root.node.audio.muted = false;
|
||||
root.node.audio.volume = value;
|
||||
}
|
||||
|
||||
Column {
|
||||
id: copy
|
||||
|
||||
x: 0
|
||||
y: root.inline ? (root.height - height) / 2 : 10
|
||||
width: root.inline ? root.width - root.controlSpan - 20 : root.width
|
||||
spacing: 3
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: root.label
|
||||
color: Theme.fg
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
visible: root.detail !== ""
|
||||
text: root.detail
|
||||
color: Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: control
|
||||
|
||||
width: root.inline ? root.controlSpan : root.width
|
||||
height: 32
|
||||
x: root.inline ? root.width - width : 0
|
||||
y: root.inline ? (root.height - height) / 2 : copy.y + copy.height + 10
|
||||
|
||||
ValueSlider {
|
||||
id: slider
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: readout.left
|
||||
anchors.rightMargin: 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
enabled: root.available
|
||||
value: root.muted ? 0 : root.volume / root.maximum
|
||||
onMoved: ratio => root.setVolume(ratio)
|
||||
}
|
||||
|
||||
// Everything above 100%. Drawn over the track rather than into it, so
|
||||
// the slider itself stays the one the rest of the shell uses.
|
||||
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
|
||||
}
|
||||
|
||||
Text {
|
||||
id: readout
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 46
|
||||
horizontalAlignment: Text.AlignRight
|
||||
text: Math.round(root.volume * 100) + "%"
|
||||
color: root.amplified ? Theme.warn : Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.features: Theme.tabularFigures
|
||||
font.pixelSize: Theme.fontSize
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: 1
|
||||
visible: root.divider
|
||||
color: Theme.alpha(Theme.fg, 0.065)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user