Compare commits
2 commits
a502faef19
...
7cd6716eb8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cd6716eb8 | ||
|
|
862169aba0 |
4 changed files with 211 additions and 36 deletions
|
|
@ -28,7 +28,7 @@ Item {
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: _hover.hovered ? M.Theme.base02 : M.Theme.base01
|
color: _hover.hovered ? M.Theme.base02 : M.Theme.base01
|
||||||
opacity: Math.max(M.Theme.barOpacity, 0.9)
|
opacity: _hover.hovered ? 1.0 : Math.max(M.Theme.barOpacity, 0.9)
|
||||||
radius: M.Theme.radius
|
radius: M.Theme.radius
|
||||||
|
|
||||||
Behavior on color {
|
Behavior on color {
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,18 @@ M.HoverPanel {
|
||||||
|
|
||||||
property var _pendingDismissIds: []
|
property var _pendingDismissIds: []
|
||||||
|
|
||||||
|
// Collapsed groups set — reassign to trigger reactivity
|
||||||
|
property var _collapsedGroups: ({})
|
||||||
|
|
||||||
|
function _toggleCollapse(appName) {
|
||||||
|
const next = Object.assign({}, _collapsedGroups);
|
||||||
|
if (next[appName])
|
||||||
|
delete next[appName];
|
||||||
|
else
|
||||||
|
next[appName] = true;
|
||||||
|
_collapsedGroups = next;
|
||||||
|
}
|
||||||
|
|
||||||
// Group notifications by appName, sorted by max urgency desc then most recent time desc
|
// Group notifications by appName, sorted by max urgency desc then most recent time desc
|
||||||
readonly property var _groups: {
|
readonly property var _groups: {
|
||||||
const map = {};
|
const map = {};
|
||||||
|
|
@ -93,22 +105,27 @@ M.HoverPanel {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flat model: group header followed by its notifications
|
// Flat model: group header followed by its notifications (omitted when collapsed)
|
||||||
readonly property var _flatModel: {
|
readonly property var _flatModel: {
|
||||||
const arr = [];
|
const arr = [];
|
||||||
for (const g of _groups) {
|
for (const g of _groups) {
|
||||||
|
const collapsed = !!_collapsedGroups[g.appName];
|
||||||
arr.push({
|
arr.push({
|
||||||
type: "header",
|
type: "header",
|
||||||
appName: g.appName,
|
appName: g.appName,
|
||||||
appIcon: g.appIcon,
|
appIcon: g.appIcon,
|
||||||
count: g.notifs.length
|
count: g.notifs.length,
|
||||||
|
collapsed: collapsed,
|
||||||
|
summaries: g.notifs.map(n => n.summary || "").filter(Boolean).join(" · ")
|
||||||
});
|
});
|
||||||
|
if (!collapsed) {
|
||||||
for (const n of g.notifs)
|
for (const n of g.notifs)
|
||||||
arr.push({
|
arr.push({
|
||||||
type: "notif",
|
type: "notif",
|
||||||
data: n
|
data: n
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return arr;
|
return arr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -218,11 +235,25 @@ M.HoverPanel {
|
||||||
readonly property var _notif: _type === "notif" ? modelData.data : null
|
readonly property var _notif: _type === "notif" ? modelData.data : null
|
||||||
|
|
||||||
width: menuWindow.contentWidth
|
width: menuWindow.contentWidth
|
||||||
height: _targetHeight * _heightScale
|
height: _displayTargetHeight * _heightScale
|
||||||
clip: true
|
clip: true
|
||||||
opacity: 0
|
opacity: 0
|
||||||
|
|
||||||
readonly property real _targetHeight: _type === "header" ? 28 : _notifCard.implicitHeight
|
readonly property real _targetHeight: {
|
||||||
|
if (_type === "header")
|
||||||
|
return modelData.collapsed ? (28 + M.Theme.fontSize + 4) : 28;
|
||||||
|
return _notifCard.implicitHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Animated version of _targetHeight — smoothly transitions header height on collapse
|
||||||
|
property real _displayTargetHeight: _targetHeight
|
||||||
|
Behavior on _displayTargetHeight {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 150
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
property real _heightScale: 1
|
property real _heightScale: 1
|
||||||
property bool _skipDismiss: false
|
property bool _skipDismiss: false
|
||||||
|
|
||||||
|
|
@ -257,11 +288,29 @@ M.HoverPanel {
|
||||||
visible: notifDelegate._type === "header"
|
visible: notifDelegate._type === "header"
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
|
HoverHandler {
|
||||||
|
id: _headerHover
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tap target for collapse — covers header row only, excludes dismiss button
|
||||||
|
Item {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: _groupDismissBtn.left
|
||||||
|
anchors.top: parent.top
|
||||||
|
height: 28
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onTapped: menuWindow._toggleCollapse(notifDelegate.modelData.appName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: _headerIcon
|
id: _headerIcon
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.leftMargin: 10
|
anchors.leftMargin: 10
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: (28 - height) / 2
|
||||||
width: M.Theme.fontSize + 2
|
width: M.Theme.fontSize + 2
|
||||||
height: M.Theme.fontSize + 2
|
height: M.Theme.fontSize + 2
|
||||||
source: {
|
source: {
|
||||||
|
|
@ -278,12 +327,29 @@ M.HoverPanel {
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Collapse chevron
|
||||||
|
Text {
|
||||||
|
id: _chevron
|
||||||
|
anchors.right: _groupDismissBtn.left
|
||||||
|
anchors.rightMargin: 8
|
||||||
|
anchors.top: parent.top
|
||||||
|
height: 28
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
text: notifDelegate._type === "header" && notifDelegate.modelData.collapsed ? "\u25B8" : "\u25BE"
|
||||||
|
color: M.Theme.base04
|
||||||
|
font.pixelSize: M.Theme.fontSize - 2
|
||||||
|
font.family: M.Theme.fontFamily
|
||||||
|
}
|
||||||
|
|
||||||
|
// App name
|
||||||
Text {
|
Text {
|
||||||
anchors.left: _headerIcon.visible ? _headerIcon.right : parent.left
|
anchors.left: _headerIcon.visible ? _headerIcon.right : parent.left
|
||||||
anchors.leftMargin: _headerIcon.visible ? 6 : 10
|
anchors.leftMargin: _headerIcon.visible ? 6 : 10
|
||||||
anchors.right: _groupDismissBtn.left
|
anchors.right: _chevron.left
|
||||||
anchors.rightMargin: 6
|
anchors.rightMargin: 4
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.top: parent.top
|
||||||
|
height: 28
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
text: notifDelegate._type === "header" ? (notifDelegate.modelData.appName || "Unknown") : ""
|
text: notifDelegate._type === "header" ? (notifDelegate.modelData.appName || "Unknown") : ""
|
||||||
color: M.Theme.base05
|
color: M.Theme.base05
|
||||||
font.pixelSize: M.Theme.fontSize - 1
|
font.pixelSize: M.Theme.fontSize - 1
|
||||||
|
|
@ -292,28 +358,50 @@ M.HoverPanel {
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dismiss button — opacity-hidden when header not hovered
|
||||||
Text {
|
Text {
|
||||||
id: _groupDismissBtn
|
id: _groupDismissBtn
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.rightMargin: 10
|
anchors.rightMargin: 10
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.top: parent.top
|
||||||
|
height: 28
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
text: "\uF1F8"
|
text: "\uF1F8"
|
||||||
color: _groupDismissArea.containsMouse ? M.Theme.base08 : M.Theme.base04
|
color: _groupDismissHover.hovered ? M.Theme.base08 : M.Theme.base04
|
||||||
font.pixelSize: M.Theme.fontSize - 1
|
font.pixelSize: M.Theme.fontSize - 1
|
||||||
font.family: M.Theme.iconFontFamily
|
font.family: M.Theme.iconFontFamily
|
||||||
|
opacity: _headerHover.hovered ? 1 : 0
|
||||||
|
|
||||||
MouseArea {
|
HoverHandler {
|
||||||
id: _groupDismissArea
|
id: _groupDismissHover
|
||||||
anchors.fill: parent
|
}
|
||||||
anchors.margins: -4
|
|
||||||
hoverEnabled: true
|
TapHandler {
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: {
|
onTapped: {
|
||||||
if (notifDelegate._type === "header")
|
if (notifDelegate._type === "header")
|
||||||
menuWindow._cascadeGroupDismiss(notifDelegate.modelData.appName);
|
menuWindow._cascadeGroupDismiss(notifDelegate.modelData.appName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Collapsed preview: notification summaries on a subtitle row
|
||||||
|
Text {
|
||||||
|
visible: notifDelegate._type === "header" && notifDelegate.modelData.collapsed
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 10
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 10
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.topMargin: 28
|
||||||
|
height: M.Theme.fontSize + 4
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
text: notifDelegate._type === "header" ? (notifDelegate.modelData.summaries ?? "") : ""
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.pixelSize: M.Theme.fontSize - 2
|
||||||
|
font.family: M.Theme.fontFamily
|
||||||
|
color: M.Theme.base04
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Individual notification ----
|
// ---- Individual notification ----
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,9 @@ PanelWindow {
|
||||||
|
|
||||||
property real uSize: 50.0
|
property real uSize: 50.0
|
||||||
property real uWavePhase: -200
|
property real uWavePhase: -200
|
||||||
|
property real uBreath: 0
|
||||||
|
property real uGlitch: 0
|
||||||
|
property real uGlitchSeed: 0.0
|
||||||
property vector4d uResolution: Qt.vector4d(width, height, 0, 0)
|
property vector4d uResolution: Qt.vector4d(width, height, 0, 0)
|
||||||
property color uC0: M.Theme.base0C
|
property color uC0: M.Theme.base0C
|
||||||
property color uC1: M.Theme.base0E
|
property color uC1: M.Theme.base0E
|
||||||
|
|
@ -40,8 +43,10 @@ PanelWindow {
|
||||||
Connections {
|
Connections {
|
||||||
target: M.NiriIpc
|
target: M.NiriIpc
|
||||||
function onOverviewOpenChanged() {
|
function onOverviewOpenChanged() {
|
||||||
if (!M.NiriIpc.overviewOpen)
|
if (!M.NiriIpc.overviewOpen) {
|
||||||
fx.uWavePhase = -200;
|
fx.uWavePhase = -200;
|
||||||
|
fx.uBreath = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,5 +64,66 @@ PanelWindow {
|
||||||
duration: 8000
|
duration: 8000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Breathing pulse while overview is open
|
||||||
|
SequentialAnimation on uBreath {
|
||||||
|
loops: Animation.Infinite
|
||||||
|
running: M.NiriIpc.overviewOpen && !M.Theme.reducedMotion
|
||||||
|
NumberAnimation {
|
||||||
|
from: 0
|
||||||
|
to: 1
|
||||||
|
duration: 2500
|
||||||
|
easing.type: Easing.InOutSine
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
from: 1
|
||||||
|
to: 0
|
||||||
|
duration: 2500
|
||||||
|
easing.type: Easing.InOutSine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Random subtle glitches — fire every 12–37s, total ~250ms each
|
||||||
|
Timer {
|
||||||
|
interval: 20000
|
||||||
|
repeat: true
|
||||||
|
running: !M.Theme.reducedMotion
|
||||||
|
onTriggered: {
|
||||||
|
interval = 12000 + Math.floor(Math.random() * 25000);
|
||||||
|
fx.uGlitchSeed = Math.random() * 1000.0;
|
||||||
|
_glitchAnim.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SequentialAnimation {
|
||||||
|
id: _glitchAnim
|
||||||
|
NumberAnimation {
|
||||||
|
target: fx
|
||||||
|
property: "uGlitch"
|
||||||
|
to: 0.7
|
||||||
|
duration: 50
|
||||||
|
easing.type: Easing.OutQuad
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: fx
|
||||||
|
property: "uGlitch"
|
||||||
|
to: 0.15
|
||||||
|
duration: 50
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: fx
|
||||||
|
property: "uGlitch"
|
||||||
|
to: 0.85
|
||||||
|
duration: 60
|
||||||
|
easing.type: Easing.OutQuad
|
||||||
|
}
|
||||||
|
NumberAnimation {
|
||||||
|
target: fx
|
||||||
|
property: "uGlitch"
|
||||||
|
to: 0
|
||||||
|
duration: 100
|
||||||
|
easing.type: Easing.InQuad
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,9 @@ layout(std140, binding = 0) uniform buf {
|
||||||
float qt_Opacity;
|
float qt_Opacity;
|
||||||
float uSize;
|
float uSize;
|
||||||
float uWavePhase;
|
float uWavePhase;
|
||||||
|
float uBreath;
|
||||||
|
float uGlitch;
|
||||||
|
float uGlitchSeed;
|
||||||
vec4 uResolution;
|
vec4 uResolution;
|
||||||
vec4 uC0;
|
vec4 uC0;
|
||||||
vec4 uC1;
|
vec4 uC1;
|
||||||
|
|
@ -29,9 +32,24 @@ vec3 themeGradient(float t) {
|
||||||
: mix(uC1.rgb, uC2.rgb, (t - 0.5) * 2.0);
|
: mix(uC1.rgb, uC2.rgb, (t - 0.5) * 2.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cheap hash for glitch bands
|
||||||
|
float hash(float n) {
|
||||||
|
return fract(sin(n) * 43758.5453123);
|
||||||
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
vec2 res = uResolution.xy;
|
vec2 res = uResolution.xy;
|
||||||
|
|
||||||
|
// Glitch: shift some horizontal bands slightly
|
||||||
vec2 frag = qt_TexCoord0 * res;
|
vec2 frag = qt_TexCoord0 * res;
|
||||||
|
if (uGlitch > 0.0) {
|
||||||
|
float band = floor(frag.y / 7.0);
|
||||||
|
float h = hash(band * 127.1 + uGlitchSeed * 311.7);
|
||||||
|
if (h > 0.72) {
|
||||||
|
float shift = (hash(band * 93.9 + uGlitchSeed) - 0.5) * 14.0 * uGlitch;
|
||||||
|
frag.x = clamp(frag.x + shift, 0.0, res.x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
float dx = uSize * 1.5;
|
float dx = uSize * 1.5;
|
||||||
float dy = uSize * 1.7320508;
|
float dy = uSize * 1.7320508;
|
||||||
|
|
@ -41,12 +59,12 @@ void main() {
|
||||||
float row = round((frag.y - yoff) / dy);
|
float row = round((frag.y - yoff) / dy);
|
||||||
vec2 center = vec2(col * dx, row * dy + yoff);
|
vec2 center = vec2(col * dx, row * dy + yoff);
|
||||||
|
|
||||||
// Wave factor
|
// Wave — wide gaussian so it reads more as a broad pulse than a sharp sweep
|
||||||
float dist = center.x - uWavePhase;
|
float dist = center.x - uWavePhase;
|
||||||
float wf = exp(-dist * dist / 9000.0);
|
float wf = exp(-dist * dist / 40000.0);
|
||||||
|
|
||||||
float baseR = uSize * 0.48;
|
float baseR = uSize * 0.48;
|
||||||
float inradius = baseR * (1.0 + 0.35 * wf);
|
float inradius = baseR * (1.0 + 0.15 * wf);
|
||||||
|
|
||||||
vec2 p = frag - center;
|
vec2 p = frag - center;
|
||||||
float d = sdHexagon(p.yx, inradius); // swap for flat-top
|
float d = sdHexagon(p.yx, inradius); // swap for flat-top
|
||||||
|
|
@ -62,28 +80,31 @@ void main() {
|
||||||
? mix(uC0.rgb, uC1.rgb, fx * 2.0)
|
? mix(uC0.rgb, uC1.rgb, fx * 2.0)
|
||||||
: mix(uC1.rgb, uC2.rgb, (fx - 0.5) * 2.0);
|
: mix(uC1.rgb, uC2.rgb, (fx - 0.5) * 2.0);
|
||||||
|
|
||||||
// Alpha from distance to center
|
// Alpha from distance to center (vignette)
|
||||||
float fy = clamp(center.y / res.y, 0.0, 1.0);
|
float fy = clamp(center.y / res.y, 0.0, 1.0);
|
||||||
float dc = length(vec2(fx - 0.5, fy - 0.5));
|
float dc = length(vec2(fx - 0.5, fy - 0.5));
|
||||||
float a = 0.03 + dc * 0.06;
|
float a = 0.03 + dc * 0.06;
|
||||||
|
|
||||||
// Wave brighten
|
// Breathing pulse (when overview open)
|
||||||
rgb = min(rgb + vec3(0.3 * wf), vec3(1.0));
|
a += 0.025 * uBreath;
|
||||||
a += 0.12 * wf;
|
rgb = min(rgb + vec3(0.04 * uBreath), vec3(1.0));
|
||||||
|
|
||||||
// Rainbow shimmer on hex edges when wave passes
|
// Wave brighten (softer than before)
|
||||||
|
rgb = min(rgb + vec3(0.15 * wf), vec3(1.0));
|
||||||
|
a += 0.07 * wf;
|
||||||
|
|
||||||
|
// Shimmer on hex edges as wave passes
|
||||||
float edgeWidth = 5.0;
|
float edgeWidth = 5.0;
|
||||||
float edgeFactor = smoothstep(-edgeWidth, 0.0, d); // 0 at interior, 1 at edge
|
float edgeFactor = smoothstep(-edgeWidth, 0.0, d);
|
||||||
if (wf > 0.01 && edgeFactor > 0.0) {
|
if (wf > 0.01 && edgeFactor > 0.0) {
|
||||||
// Vary shimmer color across theme gradient using angle + position
|
|
||||||
float angle = atan(p.y, p.x);
|
float angle = atan(p.y, p.x);
|
||||||
float rawT = fract((angle + 3.14159) / 6.28318 + center.x * 0.003 + center.y * 0.005);
|
float rawT = fract((angle + 3.14159) / 6.28318 + center.x * 0.003 + center.y * 0.005);
|
||||||
float t = abs(rawT * 2.0 - 1.0); // triangle wave → uC0→uC1→uC2→uC1→uC0, no hard jump
|
float t = abs(rawT * 2.0 - 1.0);
|
||||||
vec3 shimmerColor = themeGradient(t);
|
vec3 shimmerColor = themeGradient(t);
|
||||||
|
|
||||||
float shimmer = edgeFactor * wf;
|
float shimmer = edgeFactor * wf;
|
||||||
rgb = mix(rgb, shimmerColor, shimmer * 0.8);
|
rgb = mix(rgb, shimmerColor, shimmer * 0.5);
|
||||||
a = mix(a, 0.5, shimmer * 0.6);
|
a = mix(a, 0.5, shimmer * 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Anti-alias outer edge
|
// Anti-alias outer edge
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue