aboutsummaryrefslogtreecommitdiffstats
path: root/notifications/Balloons.qml
blob: d22e4a1d092152f1b60bc6d5ac3a4c26673920a6 (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
// 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.Io
import Quickshell.Wayland
import QtQuick

// The balloon stack, bottom-right of DP-1, over conky.
Scope {
    id: root

    // A tick drives both expiry and the end of a snooze without waiting for a
    // file change. 250ms keeps ronema's -t 1 near-instant.
    property double now: Date.now()
    Timer {
        interval: 250
        running: true
        repeat: true
        onTriggered: root.now = Date.now()
    }

    // The Repeater model is Notify.queue itself, so its identity only changes
    // when the daemon rewrites the file, not on the 250ms tick. Expiry and
    // snooze are the balloon's own business: the tick reassigns `now`, each
    // balloon re-evaluates its visible binding and leaves the column at
    // expiry. Rebuilding the model on the tick would recreate every delegate
    // (reloading icons, resetting hover) four times a second.
    readonly property var live: Notify.queue || []

    // The window border gradient, from the same file Hyprland reads, so the
    // balloons wear the windows' border. udt-accent writes it as Hyprland
    // colours, `rgb(rrggbb)` or `rgba(rrggbbaa)`; Qt wants `#aarrggbb`. The
    // fallback is the triple look_and_feel.lua falls back to.
    property var borderColors: ["#c6a0f6", "#b7bdf8", "#8aadf4"]
    FileView {
        path: `${Quickshell.env("HOME")}/.cache/udt/udt-border.lua`
        watchChanges: true
        onFileChanged: reload()
        onLoaded: {
            const re = /rgba?\(([0-9a-fA-F]{6})([0-9a-fA-F]{2})?\)/g;
            const out = [];
            let m;
            while ((m = re.exec(text())) !== null) out.push("#" + (m[2] ?? "") + m[1]);
            if (out.length > 0) root.borderColors = out;
        }
    }

    // Hyprland's borderangle is speed 30, linear, loop, which reads as 3s a
    // turn, but on a balloon that size looked slow next to the windows, so
    // the turn is tuned by eye rather than taken from the config. One angle for every balloon, so they turn together like the
    // windows do, and only while the stack is mapped.
    property real borderAngle: 0
    NumberAnimation on borderAngle {
        from: 0
        to: 2 * Math.PI
        duration: 1500
        loops: Animation.Infinite
        running: win.visible
    }

    PanelWindow {
        id: win

        // From the queue, never from layout: a hidden window stops polishing,
        // and Column computes implicitHeight during polish, so deriving
        // visibility from it would never turn true again after the first
        // hide. Queue membership is a plain property the binding re-reads
        // whether or not the window is mapped, so a later notification
        // re-maps it. Expired entries that stay in the queue keep a
        // zero-height window mapped, which is harmless.
        visible: root.live.length > 0
        screen: Quickshell.screens.find(s => s.name === "DP-1") ?? Quickshell.screens[0]
        anchors { bottom: true; right: true }
        margins { bottom: 12; right: 12 }
        implicitWidth: 340
        implicitHeight: column.implicitHeight
        color: "transparent"
        exclusionMode: ExclusionMode.Ignore
        WlrLayershell.layer: WlrLayer.Overlay
        WlrLayershell.namespace: "quickshell-notifications"
        WlrLayershell.keyboardFocus: WlrKeyboardFocus.None

        Column {
            id: column
            width: parent.width
            anchors { bottom: parent.bottom; right: parent.right }
            spacing: 8

            Repeater {
                model: root.live
                NotificationBalloon {
                    notification: modelData
                    now: root.now
                    borderColors: root.borderColors
                    borderAngle: root.borderAngle
                }
            }
        }
    }
}