aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/island/ControlTile.qml
blob: 873a42ec2667f43ea6c589c49b35b52b295428b0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
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;
        }
    }
}