aboutsummaryrefslogtreecommitdiffstats
path: root/desktop
diff options
context:
space:
mode:
Diffstat (limited to 'desktop')
-rw-r--r--desktop/island/ControlTile.qml143
1 files changed, 143 insertions, 0 deletions
diff --git a/desktop/island/ControlTile.qml b/desktop/island/ControlTile.qml
new file mode 100644
index 0000000..873a42e
--- /dev/null
+++ b/desktop/island/ControlTile.qml
@@ -0,0 +1,143 @@
+// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+//
+// 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;
+ }
+ }
+}