quickshell: slim pill sliders; card-style volume and battery dropdowns

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-06-12 10:41:13 +01:00
parent 9a38cacc05
commit 4540e38321

View file

@ -1365,185 +1365,230 @@ in
Column {
id: volDropdownCol
anchors.centerIn: parent
width: 260
spacing: 8
width: 268
spacing: 8
// Master volume
Text {
text: "\u{f057e} Master"
color: Theme.base05
font.family: Theme.fontFamily
font.pixelSize: 13
font.weight: Font.Medium
}
// Master volume card
Rectangle {
width: parent.width
height: masterCardCol.height + 16
radius: 8
color: Theme.base01
Row {
width: parent.width
Column {
id: masterCardCol
anchors.top: parent.top
anchors.topMargin: 8
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - 16
spacing: 8
Text {
text: "\u{f057e} Master"
color: Theme.base05
font.family: Theme.fontFamily
font.pixelSize: 13
font.weight: Font.Medium
}
Row {
width: parent.width
spacing: 8
// Slim pill slider: 6px fully-rounded track,
// 20px invisible hit area for comfy dragging
Item {
id: masterSliderBg
width: parent.width - masterVolLabel.width - 8
height: 20
anchors.verticalCenter: parent.verticalCenter
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: parent.width
height: 6
radius: 3
color: Theme.base02
}
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: {
let v = volWidget.sink && volWidget.sink.audio
? Math.min(1, volWidget.sink.audio.volume) : 0;
return v > 0 ? Math.max(6, v * parent.width) : 0;
}
height: 6
radius: 3
color: volWidget.muted ? Theme.base03 : Theme.base0D
Behavior on width { NumberAnimation { duration: 80 } }
}
MouseArea {
anchors.fill: parent
onPressed: (mouse) => setVolume(mouse)
onPositionChanged: (mouse) => { if (pressed) setVolume(mouse); }
function setVolume(mouse) {
if (!volWidget.sink || !volWidget.sink.audio) return;
let v = Math.max(0, Math.min(1, mouse.x / width));
volWidget.sink.audio.volume = v;
}
}
}
Text {
id: masterVolLabel
width: 36
text: volWidget.vol + "%"
color: Theme.base05
font.family: Theme.fontFamily
font.pixelSize: 11
horizontalAlignment: Text.AlignRight
anchors.verticalCenter: parent.verticalCenter
}
}
// Mute button
Rectangle {
id: masterSliderBg
width: parent.width - masterVolLabel.width - 8
height: 20
width: parent.width
height: 28
color: masterMuteMa.containsMouse ? Theme.base02 : "transparent"
Behavior on color { ColorAnimation { duration: 120 } }
radius: 4
color: Theme.base01
anchors.verticalCenter: parent.verticalCenter
Rectangle {
width: volWidget.sink && volWidget.sink.audio
? Math.min(1, volWidget.sink.audio.volume) * parent.width : 0
height: parent.height
radius: 4
color: volWidget.muted ? Theme.base03 : Theme.base0D
Behavior on width { NumberAnimation { duration: 80 } }
Text {
anchors.centerIn: parent
text: volWidget.muted ? "\u{f0581} Unmute" : "\u{f057e} Mute"
color: Theme.base05
font.family: Theme.fontFamily
font.pixelSize: 12
}
MouseArea {
id: masterMuteMa
anchors.fill: parent
onPressed: (mouse) => setVolume(mouse)
onPositionChanged: (mouse) => { if (pressed) setVolume(mouse); }
function setVolume(mouse) {
if (!volWidget.sink || !volWidget.sink.audio) return;
let v = Math.max(0, Math.min(1, mouse.x / width));
volWidget.sink.audio.volume = v;
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (volWidget.sink && volWidget.sink.audio)
volWidget.sink.audio.muted = !volWidget.sink.audio.muted;
}
}
}
Text {
id: masterVolLabel
width: 36
text: volWidget.vol + "%"
color: Theme.base05
font.family: Theme.fontFamily
font.pixelSize: 11
horizontalAlignment: Text.AlignRight
anchors.verticalCenter: parent.verticalCenter
}
}
}
// Mute button
Rectangle {
width: parent.width
height: 28
color: masterMuteMa.containsMouse ? Theme.base02 : "transparent"
Behavior on color { ColorAnimation { duration: 120 } }
radius: 4
// Applications card
Rectangle {
visible: appStreamsCol.childrenRect.height > 0
width: parent.width
height: appsCardCol.height + 16
radius: 8
color: Theme.base01
Text {
anchors.centerIn: parent
text: volWidget.muted ? "\u{f0581} Unmute" : "\u{f057e} Mute"
color: Theme.base05
font.family: Theme.fontFamily
font.pixelSize: 12
}
MouseArea {
id: masterMuteMa
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (volWidget.sink && volWidget.sink.audio)
volWidget.sink.audio.muted = !volWidget.sink.audio.muted;
}
}
}
// Separator
Rectangle {
width: parent.width - 20
anchors.horizontalCenter: parent.horizontalCenter
height: 1
color: Theme.base02
visible: appStreamsCol.childrenRect.height > 0
}
// App streams header
Text {
visible: appStreamsCol.childrenRect.height > 0
text: "\u{f0641} Applications"
color: Theme.base05
font.family: Theme.fontFamily
font.pixelSize: 13
font.weight: Font.Medium
}
// Per-app streams
Column {
id: appStreamsCol
width: parent.width
spacing: 6
id: appsCardCol
anchors.top: parent.top
anchors.topMargin: 8
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - 16
spacing: 8
Repeater {
id: appStreamsRepeater
model: Pipewire.nodes
Text {
text: "\u{f0641} Applications"
color: Theme.base05
font.family: Theme.fontFamily
font.pixelSize: 13
font.weight: Font.Medium
}
Column {
required property var modelData
width: parent.width
spacing: 2
visible: modelData.isStream && modelData.audio !== null
// Per-app streams
Column {
id: appStreamsCol
width: parent.width
spacing: 6
PwObjectTracker {
objects: [modelData]
}
Repeater {
id: appStreamsRepeater
model: Pipewire.nodes
Text {
text: modelData.properties["application.name"] || modelData.name || "Unknown"
color: Theme.base04
font.family: Theme.fontFamily
font.pixelSize: 11
elide: Text.ElideRight
Column {
required property var modelData
width: parent.width
}
spacing: 2
visible: modelData.isStream && modelData.audio !== null
Row {
width: parent.width
spacing: 8
Rectangle {
width: parent.width - appVolLabel.width - 8
height: 16
radius: 3
color: Theme.base01
anchors.verticalCenter: parent.verticalCenter
Rectangle {
width: modelData.audio
? Math.min(1, modelData.audio.volume) * parent.width : 0
height: parent.height
radius: 3
color: modelData.audio && modelData.audio.muted
? Theme.base03 : Theme.base0C
Behavior on width { NumberAnimation { duration: 80 } }
}
MouseArea {
anchors.fill: parent
onPressed: (mouse) => setVol(mouse)
onPositionChanged: (mouse) => { if (pressed) setVol(mouse); }
function setVol(mouse) {
if (!modelData.audio) return;
let v = Math.max(0, Math.min(1, mouse.x / width));
modelData.audio.volume = v;
}
}
PwObjectTracker {
objects: [modelData]
}
Text {
id: appVolLabel
width: 36
text: modelData.audio ? Math.round(modelData.audio.volume * 100) + "%" : "0%"
text: modelData.properties["application.name"] || modelData.name || "Unknown"
color: Theme.base04
font.family: Theme.fontFamily
font.pixelSize: 10
horizontalAlignment: Text.AlignRight
anchors.verticalCenter: parent.verticalCenter
font.pixelSize: 11
elide: Text.ElideRight
width: parent.width
}
Row {
width: parent.width
spacing: 8
// Slim pill slider, 16px hit area
Item {
width: parent.width - appVolLabel.width - 8
height: 16
anchors.verticalCenter: parent.verticalCenter
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: parent.width
height: 4
radius: 2
color: Theme.base02
}
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: {
let v = modelData.audio ? Math.min(1, modelData.audio.volume) : 0;
return v > 0 ? Math.max(4, v * parent.width) : 0;
}
height: 4
radius: 2
color: modelData.audio && modelData.audio.muted
? Theme.base03 : Theme.base0C
Behavior on width { NumberAnimation { duration: 80 } }
}
MouseArea {
anchors.fill: parent
onPressed: (mouse) => setVol(mouse)
onPositionChanged: (mouse) => { if (pressed) setVol(mouse); }
function setVol(mouse) {
if (!modelData.audio) return;
let v = Math.max(0, Math.min(1, mouse.x / width));
modelData.audio.volume = v;
}
}
}
Text {
id: appVolLabel
width: 36
text: modelData.audio ? Math.round(modelData.audio.volume * 100) + "%" : "0%"
color: Theme.base04
font.family: Theme.fontFamily
font.pixelSize: 10
horizontalAlignment: Text.AlignRight
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
}
}
}
}
@ -1701,132 +1746,158 @@ in
Column {
id: batteryDropdownCol
anchors.centerIn: parent
width: 200
spacing: 8
width: 208
spacing: 8
Row {
width: parent.width
spacing: 8
// Battery status card
Rectangle {
width: parent.width
height: battCardCol.height + 16
radius: 8
color: Theme.base01
Text {
text: batteryWidget.batteryIcon
color: Theme.base05
font.family: Theme.fontFamily
font.pixelSize: 18
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
Text {
text: batteryWidget.batteryLevel + "%" + (batteryWidget.charging ? " Charging" : "")
color: Theme.base05
font.family: Theme.fontFamily
font.pixelSize: 13
font.weight: Font.Medium
}
Text {
text: batteryWidget.powerDraw.toFixed(1) + " W"
+ (batteryWidget.timeRemaining !== "" ? " \u2022 " + batteryWidget.timeRemaining + (batteryWidget.charging ? " to full" : " left") : "")
color: Theme.base04
font.family: Theme.fontFamily
font.pixelSize: 11
}
}
}
Rectangle {
width: parent.width - 10
Column {
id: battCardCol
anchors.top: parent.top
anchors.topMargin: 8
anchors.horizontalCenter: parent.horizontalCenter
height: 1
color: Theme.base03
}
Text {
text: "Power Profile"
color: Theme.base03
font.family: Theme.fontFamily
font.pixelSize: 11
}
Item {
width: parent.width
height: 36
// Sliding selection pill glides between
// profiles instead of each button flipping.
Rectangle {
id: profilePill
readonly property int selIdx:
batteryWidget.powerProfile === "power-saver" ? 0
: batteryWidget.powerProfile === "performance" ? 2
: 1
width: (parent.width - 8) / 3
height: 36
radius: 6
color: Theme.base02
border.width: 1
border.color: Theme.base03
x: selIdx * (width + 4)
Behavior on x {
NumberAnimation { duration: 250; easing.type: Easing.OutExpo }
}
}
width: parent.width - 16
Row {
anchors.fill: parent
spacing: 4
width: parent.width
spacing: 8
Repeater {
model: [
{ name: "power-saver", profile: PowerProfile.PowerSaver, label: "\u{f0425}", tip: "Saver" },
{ name: "balanced", profile: PowerProfile.Balanced, label: "\u{f0376}", tip: "Balanced" },
{ name: "performance", profile: PowerProfile.Performance, label: "\u{f0e0e}", tip: "Performance" }
]
Text {
text: batteryWidget.batteryIcon
color: Theme.base05
font.family: Theme.fontFamily
font.pixelSize: 18
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
required property var modelData
width: (parent.width - 8) / 3
height: 36
radius: 6
color: profMouse.containsMouse && batteryWidget.powerProfile !== modelData.name
? Theme.base01 : "transparent"
Behavior on color { ColorAnimation { duration: 120 } }
Column {
anchors.verticalCenter: parent.verticalCenter
Text {
text: batteryWidget.batteryLevel + "%" + (batteryWidget.charging ? " Charging" : "")
color: Theme.base05
font.family: Theme.fontFamily
font.pixelSize: 13
font.weight: Font.Medium
}
Text {
text: batteryWidget.powerDraw.toFixed(1) + " W"
+ (batteryWidget.timeRemaining !== "" ? " " + batteryWidget.timeRemaining + (batteryWidget.charging ? " to full" : " left") : "")
color: Theme.base04
font.family: Theme.fontFamily
font.pixelSize: 11
}
}
}
}
}
Column {
anchors.centerIn: parent
spacing: 1
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: modelData.label
color: batteryWidget.powerProfile === modelData.name
? Theme.base0D : Theme.base05
Behavior on color { ColorAnimation { duration: 200 } }
font.family: Theme.fontFamily
font.pixelSize: 14
// Power profile card
Rectangle {
width: parent.width
height: profCardCol.height + 16
radius: 8
color: Theme.base01
Column {
id: profCardCol
anchors.top: parent.top
anchors.topMargin: 8
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - 16
spacing: 6
Text {
text: "Power Profile"
color: Theme.base04
font.family: Theme.fontFamily
font.pixelSize: 11
}
Item {
width: parent.width
height: 36
// Sliding selection pill glides between
// profiles instead of each button flipping.
Rectangle {
id: profilePill
readonly property int selIdx:
batteryWidget.powerProfile === "power-saver" ? 0
: batteryWidget.powerProfile === "performance" ? 2
: 1
width: (parent.width - 8) / 3
height: 36
radius: 6
color: Theme.base02
border.width: 1
border.color: Theme.base03
x: selIdx * (width + 4)
Behavior on x {
NumberAnimation { duration: 250; easing.type: Easing.OutExpo }
}
}
Row {
anchors.fill: parent
spacing: 4
Repeater {
model: [
{ name: "power-saver", profile: PowerProfile.PowerSaver, label: "\u{f0425}", tip: "Saver" },
{ name: "balanced", profile: PowerProfile.Balanced, label: "\u{f0376}", tip: "Balanced" },
{ name: "performance", profile: PowerProfile.Performance, label: "\u{f0e0e}", tip: "Performance" }
]
Rectangle {
required property var modelData
width: (parent.width - 8) / 3
height: 36
radius: 6
color: profMouse.containsMouse && batteryWidget.powerProfile !== modelData.name
? Theme.base02 : "transparent"
Behavior on color { ColorAnimation { duration: 120 } }
Column {
anchors.centerIn: parent
spacing: 1
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: modelData.label
color: batteryWidget.powerProfile === modelData.name
? Theme.base0D : Theme.base05
Behavior on color { ColorAnimation { duration: 200 } }
font.family: Theme.fontFamily
font.pixelSize: 14
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: modelData.tip
color: batteryWidget.powerProfile === modelData.name
? Theme.base05 : Theme.base04
Behavior on color { ColorAnimation { duration: 200 } }
font.family: Theme.fontFamily
font.pixelSize: 9
}
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: modelData.tip
color: batteryWidget.powerProfile === modelData.name
? Theme.base05 : Theme.base04
Behavior on color { ColorAnimation { duration: 200 } }
font.family: Theme.fontFamily
font.pixelSize: 9
}
}
MouseArea {
id: profMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: PowerProfiles.profile = modelData.profile
MouseArea {
id: profMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: PowerProfiles.profile = modelData.profile
}
}
}
}
}
}
}
}
}
''}