// Copyright (C) 2026 Danilo M. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as // published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. import Quickshell import Quickshell.Widgets import Quickshell.Networking import Quickshell.Bluetooth import QtQuick import ".." import "Controls.js" as Controls import "Logic.js" as Logic // One control: icon over label. Left-click toggles, right-click opens the // drawer page that holds the rest, scroll changes volume on the two audio // tiles. Toggle tiles fill when on; audio tiles fill to their volume. Rectangle { id: tile required property string tileId // The sound module's Service. required property QtObject sound signal openPage(string page) readonly property var info: Controls.tile(tileId) readonly property bool isAudio: tileId === "output" || tileId === "mic" readonly property var audio: tileId === "output" ? (sound.sink?.audio ?? null) : tileId === "mic" ? (sound.source?.audio ?? null) : null readonly property var adapter: Bluetooth.defaultAdapter // Networking starts empty for about two seconds (AGENTS.md). readonly property bool wifiReady: (Networking.devices?.values ?? []).some(d => d.type === DeviceType.Wifi) readonly property bool ready: isAudio ? audio !== null : tileId === "bluetooth" ? adapter !== null : tileId === "wifi" ? wifiReady : true readonly property bool on: { switch (tileId) { case "wifi": return Networking.wifiEnabled; case "bluetooth": return adapter?.enabled ?? false; case "dnd": return Status.dnd; // Lit while a game holds it too; a click then clears only the manual // half (Status.qml), so the tile stays lit until the game ends. case "presentation": return Status.presentation; case "nolock": return !Status.nolock; default: return audio ? !audio.muted : false; } } function toggle() { switch (tileId) { case "wifi": Networking.wifiEnabled = !Networking.wifiEnabled; break; case "bluetooth": adapter.enabled = !adapter.enabled; break; case "dnd": case "presentation": case "nolock": Status.toggleMode(tileId); break; default: audio.muted = !audio.muted; } } radius: 12 color: Qt.alpha(Theme.surface, 0.5) border.width: 1 border.color: Qt.alpha(Theme.text, 0.08) opacity: ready ? 1 : 0.4 clip: true // Toggle tiles: lit when on. Rectangle { anchors.fill: parent radius: parent.radius visible: !tile.isAudio && tile.on color: Qt.alpha(Theme.accent, 0.35) } // Audio tiles: a bar to the volume. Muted keeps the width in a neutral // colour, so the level stays readable. Not the accent as the neutral: // udt can snap it onto a palette colour (AGENTS.md). Rectangle { visible: tile.isAudio anchors { left: parent.left; top: parent.top; bottom: parent.bottom } width: parent.width * Controls.fill(tile.audio?.volume) radius: parent.radius color: tile.audio?.muted ? Qt.alpha(Theme.overlay, 0.6) : Qt.alpha(Theme.accent, 0.35) } Column { anchors.centerIn: parent width: parent.width - 8 spacing: 4 IconImage { anchors.horizontalCenter: parent.horizontalCenter implicitSize: 22 source: Quickshell.iconPath(tile.tileId === "output" ? Logic.volumeIcon(tile.audio?.volume ?? 0, tile.audio?.muted ?? false) : Controls.icon(tile.tileId, tile.on), true) } Text { width: parent.width horizontalAlignment: Text.AlignHCenter elide: Text.ElideRight text: tile.info?.label ?? tile.tileId font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } color: Theme.text } } MouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton cursorShape: Qt.PointingHandCursor onClicked: mouse => { if (mouse.button === Qt.RightButton) tile.openPage(tile.info.page); else if (tile.ready) tile.toggle(); } // One step per wheel notch (120), as the layout flag does. A // MouseArea, not a WheelHandler: the handler never fired in the // island (AGENTS.md). property real acc: 0 onWheel: wheel => { wheel.accepted = true; if (!tile.isAudio || !tile.ready) return; acc += wheel.angleDelta.y; if (Math.abs(acc) < 120) return; const max = tile.tileId === "output" ? 1.5 : 1.0; tile.audio.volume = Controls.step(tile.audio.volume, acc > 0 ? 0.05 : -0.05, max); acc = 0; } } }