aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--notifications/Balloons.qml39
-rw-r--r--notifications/NotificationBalloon.qml61
2 files changed, 95 insertions, 5 deletions
diff --git a/notifications/Balloons.qml b/notifications/Balloons.qml
index 3676e67..d22e4a1 100644
--- a/notifications/Balloons.qml
+++ b/notifications/Balloons.qml
@@ -10,6 +10,7 @@
// GNU General Public License for more details.
import Quickshell
+import Quickshell.Io
import Quickshell.Wayland
import QtQuick
@@ -35,6 +36,37 @@ Scope {
// (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
@@ -65,7 +97,12 @@ Scope {
Repeater {
model: root.live
- NotificationBalloon { notification: modelData; now: root.now }
+ NotificationBalloon {
+ notification: modelData
+ now: root.now
+ borderColors: root.borderColors
+ borderAngle: root.borderAngle
+ }
}
}
}
diff --git a/notifications/NotificationBalloon.qml b/notifications/NotificationBalloon.qml
index b01a848..a9f9459 100644
--- a/notifications/NotificationBalloon.qml
+++ b/notifications/NotificationBalloon.qml
@@ -10,6 +10,7 @@
// GNU General Public License for more details.
import QtQuick
+import QtQuick.Shapes
// One balloon: icon at the left, app, summary and body, an X, and the click
// targets. The body is markup, which is why the daemon advertises body-markup.
@@ -22,6 +23,8 @@ Rectangle {
// silently arrives undefined and every binding on it fails.
property var notification
property double now: 0
+ property var borderColors: []
+ property real borderAngle: 0
// Driven by the shell's tick. The balloon drops itself once its expiry
// passes, or while a suppression says so: the drawer and snooze withhold
@@ -38,10 +41,60 @@ Rectangle {
width: parent ? parent.width : 340
implicitHeight: texts.implicitHeight + 20 + (preview.visible ? preview.height + 8 : 0)
- radius: 10
- color: Qt.alpha(Theme.base, 0.82)
- border.width: 1
- border.color: Qt.alpha(Theme.text, 0.12)
+ // The body is drawn by the Shape below, not by the Rectangle, so its
+ // corners can be Hyprland's: rounding 15 at rounding_power 4, a
+ // superellipse a Rectangle radius cannot draw.
+ color: "transparent"
+
+ // A rounded rectangle as an SVG path, each corner the cubic that best
+ // fits |x|^4 + |y|^4 = 1. A circle's control points sit 0.552 of the
+ // radius from each end; matching the superellipse at 45 degrees,
+ // (4 + 3k) / 8 = 2^-1/4, puts them at k = 0.909.
+ function squircle(x, y, w, h, r) {
+ const c = r * (1 - 0.909);
+ const R = x + w, B = y + h;
+ return `M ${x + r} ${y} L ${R - r} ${y} C ${R - c} ${y} ${R} ${y + c} ${R} ${y + r}`
+ + ` L ${R} ${B - r} C ${R} ${B - c} ${R - c} ${B} ${R - r} ${B}`
+ + ` L ${x + r} ${B} C ${x + c} ${B} ${x} ${B - c} ${x} ${B - r}`
+ + ` L ${x} ${y + r} C ${x} ${y + c} ${x + c} ${y} ${x + r} ${y} Z`;
+ }
+
+ // Hyprland's window border: 2px, the udt gradient, rotating. A Rectangle
+ // border takes one colour, so this is a ring, two squircles
+ // filled odd-even, which keeps the translucent body from showing the
+ // gradient through. The gradient line runs through the centre at the
+ // current angle and is just long enough to span the box.
+ // ponytail: three stops, which is what udt-accent writes; GradientStops
+ // cannot come from a Repeater, so a longer list would need more stops.
+ Shape {
+ id: ring
+ anchors.fill: parent
+ preferredRendererType: Shape.CurveRenderer
+ readonly property real dx: Math.cos(b.borderAngle)
+ readonly property real dy: Math.sin(b.borderAngle)
+ readonly property real reach: Math.abs(width / 2 * dx) + Math.abs(height / 2 * dy)
+
+ ShapePath {
+ strokeWidth: -1
+ fillColor: Qt.alpha(Theme.base, 0.82)
+ PathSvg { path: b.squircle(0, 0, ring.width, ring.height, 15) }
+ }
+
+ ShapePath {
+ strokeWidth: -1
+ fillRule: ShapePath.OddEvenFill
+ fillGradient: LinearGradient {
+ x1: ring.width / 2 - ring.dx * ring.reach
+ y1: ring.height / 2 - ring.dy * ring.reach
+ x2: ring.width / 2 + ring.dx * ring.reach
+ y2: ring.height / 2 + ring.dy * ring.reach
+ GradientStop { position: 0.0; color: b.borderColors[0] ?? Theme.accent }
+ GradientStop { position: 0.5; color: b.borderColors[1] ?? Theme.accent }
+ GradientStop { position: 1.0; color: b.borderColors[2] ?? Theme.accent }
+ }
+ PathSvg { path: b.squircle(0, 0, ring.width, ring.height, 15) + " " + b.squircle(2, 2, ring.width - 4, ring.height - 4, 13) }
+ }
+ }
// The background click target is declared first so the X, declared later,
// sits above it and wins its corner.