import Quickshell import Quickshell.Wayland import QtQuick import qs.config import qs.components import qs.services // The volume and brightness sliders. Same full-screen layer surface as the // other menus, so click-away and Escape work on any wlroots compositor. // // A row is only here when there is something behind it: a machine with no // backlight gets the volume slider alone, and the panel shrinks to fit. PanelWindow { id: root required property var bar readonly property string screenName: root.bar.screen ? root.bar.screen.name : "" visible: Session.isOpen("controls", root.screenName) screen: root.bar.screen WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.namespace: Config.namespace + "-controls" WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive anchors { left: true right: true top: true bottom: true } // Reserve nothing, respect what others reserved — see Launcher.qml. Keeps // the menu clear of the bar and of an on-screen keyboard. exclusionMode: ExclusionMode.Normal exclusiveZone: 0 color: "transparent" readonly property int barInset: Config.reserveSpace ? 0 : Theme.barHeight // ---- Data ------------------------------------------------------------ // The rows on screen, in order, as ids the key handler can act on. readonly property var rows: [Audio.available ? "volume" : "", Brightness.available ? "brightness" : ""].filter(row => row !== "") property int currentIndex: 0 readonly property string currentRow: root.rows[root.currentIndex] ?? "" // One arrow key, in the step size the row it lands on uses. function adjust(direction: int): void { if (root.currentRow === "volume") Audio.step(direction * Config.volumeStep); else if (root.currentRow === "brightness") Brightness.step(direction * Config.brightnessStep); } onVisibleChanged: { if (root.visible) { root.currentIndex = 0; Brightness.refresh(); keyHandler.forceActiveFocus(); } } // sysfs raises no change events, so the backlight is polled — but only // while it is being looked at. The hardware keys move it behind our back; // nothing else does. Timer { interval: 1000 repeat: true running: root.visible && Brightness.available onTriggered: Brightness.refresh() } // ---- Chrome ---------------------------------------------------------- // Click-away scrim. MouseArea { anchors.fill: parent onClicked: Session.close() } Item { id: keyHandler anchors.fill: parent focus: true Keys.onEscapePressed: Session.close() Keys.onDownPressed: root.currentIndex = Math.min(root.currentIndex + 1, root.rows.length - 1) Keys.onUpPressed: root.currentIndex = Math.max(root.currentIndex - 1, 0) Keys.onLeftPressed: root.adjust(-1) Keys.onRightPressed: root.adjust(1) Keys.onReturnPressed: if (root.currentRow === "volume") Audio.toggleMute() Keys.onEnterPressed: if (root.currentRow === "volume") Audio.toggleMute() } Rectangle { id: menu // Anchored to the panel's right edge, above the button that opens it. anchors { right: parent.right bottom: parent.bottom rightMargin: Theme.padding bottomMargin: root.barInset + Theme.padding } width: Math.min(Config.controlsMenuWidth, root.width - Theme.padding * 2) height: Theme.padding * 4 + column.height color: Theme.surface radius: Theme.radius * 2 border.width: Theme.borderWidth border.color: Theme.border // Swallow clicks so they do not reach the scrim behind. MouseArea { anchors.fill: parent } Column { id: column anchors { left: parent.left right: parent.right verticalCenter: parent.verticalCenter margins: Theme.padding * 2 } spacing: Theme.padding SliderRow { width: column.width visible: Audio.available label: "Volume" valueText: Audio.muted ? "Muted" : Math.round(Audio.volume * 100) + "%" value: Audio.volume stepSize: Config.volumeStep selected: root.currentRow === "volume" iconClickable: true onMoved: value => { // Moving the slider is a request for that much sound, so a // muted sink comes back rather than moving silently. Audio.setMuted(false); Audio.setVolume(value); } onIconClicked: Audio.toggleMute() VolumeGlyph { size: 18 struck: Audio.muted level: Audio.muted ? 0 : Audio.volume > 0.5 ? 2 : Audio.volume > 0 ? 1 : 0 color: Audio.muted ? Theme.textDim : Theme.text } } SliderRow { width: column.width visible: Brightness.available label: "Brightness" valueText: Math.round(Brightness.value * 100) + "%" value: Brightness.value stepSize: Config.brightnessStep selected: root.currentRow === "brightness" onMoved: value => Brightness.set(value) BrightnessGlyph { size: 18 color: Theme.text } } } } }