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 { Column {
id: volDropdownCol id: volDropdownCol
anchors.centerIn: parent anchors.centerIn: parent
width: 260 width: 268
spacing: 8 spacing: 8
// Master volume // Master volume card
Text { Rectangle {
text: "\u{f057e} Master" width: parent.width
color: Theme.base05 height: masterCardCol.height + 16
font.family: Theme.fontFamily radius: 8
font.pixelSize: 13 color: Theme.base01
font.weight: Font.Medium
}
Row { Column {
width: parent.width id: masterCardCol
anchors.top: parent.top
anchors.topMargin: 8
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - 16
spacing: 8 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 { Rectangle {
id: masterSliderBg width: parent.width
width: parent.width - masterVolLabel.width - 8 height: 28
height: 20 color: masterMuteMa.containsMouse ? Theme.base02 : "transparent"
Behavior on color { ColorAnimation { duration: 120 } }
radius: 4 radius: 4
color: Theme.base01
anchors.verticalCenter: parent.verticalCenter
Rectangle { Text {
width: volWidget.sink && volWidget.sink.audio anchors.centerIn: parent
? Math.min(1, volWidget.sink.audio.volume) * parent.width : 0 text: volWidget.muted ? "\u{f0581} Unmute" : "\u{f057e} Mute"
height: parent.height color: Theme.base05
radius: 4 font.family: Theme.fontFamily
color: volWidget.muted ? Theme.base03 : Theme.base0D font.pixelSize: 12
Behavior on width { NumberAnimation { duration: 80 } }
} }
MouseArea { MouseArea {
id: masterMuteMa
anchors.fill: parent anchors.fill: parent
onPressed: (mouse) => setVolume(mouse) hoverEnabled: true
onPositionChanged: (mouse) => { if (pressed) setVolume(mouse); } cursorShape: Qt.PointingHandCursor
function setVolume(mouse) { onClicked: {
if (!volWidget.sink || !volWidget.sink.audio) return; if (volWidget.sink && volWidget.sink.audio)
let v = Math.max(0, Math.min(1, mouse.x / width)); volWidget.sink.audio.muted = !volWidget.sink.audio.muted;
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 // Applications card
Rectangle { Rectangle {
width: parent.width visible: appStreamsCol.childrenRect.height > 0
height: 28 width: parent.width
color: masterMuteMa.containsMouse ? Theme.base02 : "transparent" height: appsCardCol.height + 16
Behavior on color { ColorAnimation { duration: 120 } } radius: 8
radius: 4 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 { Column {
id: appStreamsCol id: appsCardCol
width: parent.width anchors.top: parent.top
spacing: 6 anchors.topMargin: 8
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - 16
spacing: 8
Repeater { Text {
id: appStreamsRepeater text: "\u{f0641} Applications"
model: Pipewire.nodes color: Theme.base05
font.family: Theme.fontFamily
font.pixelSize: 13
font.weight: Font.Medium
}
Column { // Per-app streams
required property var modelData Column {
width: parent.width id: appStreamsCol
spacing: 2 width: parent.width
visible: modelData.isStream && modelData.audio !== null spacing: 6
PwObjectTracker { Repeater {
objects: [modelData] id: appStreamsRepeater
} model: Pipewire.nodes
Text { Column {
text: modelData.properties["application.name"] || modelData.name || "Unknown" required property var modelData
color: Theme.base04
font.family: Theme.fontFamily
font.pixelSize: 11
elide: Text.ElideRight
width: parent.width width: parent.width
} spacing: 2
visible: modelData.isStream && modelData.audio !== null
Row { PwObjectTracker {
width: parent.width objects: [modelData]
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;
}
}
} }
Text { Text {
id: appVolLabel text: modelData.properties["application.name"] || modelData.name || "Unknown"
width: 36
text: modelData.audio ? Math.round(modelData.audio.volume * 100) + "%" : "0%"
color: Theme.base04 color: Theme.base04
font.family: Theme.fontFamily font.family: Theme.fontFamily
font.pixelSize: 10 font.pixelSize: 11
horizontalAlignment: Text.AlignRight elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter 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 { Column {
id: batteryDropdownCol id: batteryDropdownCol
anchors.centerIn: parent anchors.centerIn: parent
width: 200 width: 208
spacing: 8 spacing: 8
Row { // Battery status card
width: parent.width Rectangle {
spacing: 8 width: parent.width
height: battCardCol.height + 16
radius: 8
color: Theme.base01
Text { Column {
text: batteryWidget.batteryIcon id: battCardCol
color: Theme.base05 anchors.top: parent.top
font.family: Theme.fontFamily anchors.topMargin: 8
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
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
height: 1 width: parent.width - 16
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 }
}
}
Row { Row {
anchors.fill: parent width: parent.width
spacing: 4 spacing: 8
Repeater { Text {
model: [ text: batteryWidget.batteryIcon
{ name: "power-saver", profile: PowerProfile.PowerSaver, label: "\u{f0425}", tip: "Saver" }, color: Theme.base05
{ name: "balanced", profile: PowerProfile.Balanced, label: "\u{f0376}", tip: "Balanced" }, font.family: Theme.fontFamily
{ name: "performance", profile: PowerProfile.Performance, label: "\u{f0e0e}", tip: "Performance" } font.pixelSize: 18
] anchors.verticalCenter: parent.verticalCenter
}
Rectangle { Column {
required property var modelData anchors.verticalCenter: parent.verticalCenter
width: (parent.width - 8) / 3 Text {
height: 36 text: batteryWidget.batteryLevel + "%" + (batteryWidget.charging ? " Charging" : "")
radius: 6 color: Theme.base05
color: profMouse.containsMouse && batteryWidget.powerProfile !== modelData.name font.family: Theme.fontFamily
? Theme.base01 : "transparent" font.pixelSize: 13
Behavior on color { ColorAnimation { duration: 120 } } 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 { // Power profile card
anchors.centerIn: parent Rectangle {
spacing: 1 width: parent.width
Text { height: profCardCol.height + 16
anchors.horizontalCenter: parent.horizontalCenter radius: 8
text: modelData.label color: Theme.base01
color: batteryWidget.powerProfile === modelData.name
? Theme.base0D : Theme.base05 Column {
Behavior on color { ColorAnimation { duration: 200 } } id: profCardCol
font.family: Theme.fontFamily anchors.top: parent.top
font.pixelSize: 14 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 { MouseArea {
id: profMouse id: profMouse
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: PowerProfiles.profile = modelData.profile onClicked: PowerProfiles.profile = modelData.profile
}
} }
} }
} }
} }
} }
}
} }
} }
''} ''}