diff --git a/settings/quickshell.nix b/settings/quickshell.nix index 04fa476..17b938a 100644 --- a/settings/quickshell.nix +++ b/settings/quickshell.nix @@ -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 + } } } } } } + } } } ''}