From fd5063dc309ea74310f6aee63e68d904c5e89d1f Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Mon, 15 Jun 2026 22:50:52 -0400 Subject: [PATCH] Fix notification popup placement --- .../notifications/NotificationLogic.js | 19 +++++++++++ shell/plugins/notifications/Service.qml | 24 ++++++++------ test/shell.d/notifications-test.sh | 33 +++++++++++++++++++ 3 files changed, 66 insertions(+), 10 deletions(-) diff --git a/shell/plugins/notifications/NotificationLogic.js b/shell/plugins/notifications/NotificationLogic.js index 53def626..9fb1688c 100644 --- a/shell/plugins/notifications/NotificationLogic.js +++ b/shell/plugins/notifications/NotificationLogic.js @@ -165,6 +165,24 @@ function dumpRows(rows) { return out } +function popupPlacement(barPosition, barClearance, gapsOut) { + var position = String(barPosition || "top") + var clearance = Number(barClearance) + var gap = Number(gapsOut) + if (!isFinite(clearance)) clearance = 0 + if (!isFinite(gap)) gap = 0 + + return { + anchors: { top: true, bottom: false, left: false, right: true }, + margins: { + top: position === "top" ? clearance : gap, + bottom: gap, + left: gap, + right: position === "right" ? clearance : gap + } + } +} + function imageExtension(srcPath) { var lower = String(srcPath || "").toLowerCase() var dot = lower.lastIndexOf(".") @@ -188,6 +206,7 @@ if (typeof module !== "undefined") { dedupeByOriginalId: dedupeByOriginalId, parseHistory: parseHistory, dumpRows: dumpRows, + popupPlacement: popupPlacement, imageExtension: imageExtension } } diff --git a/shell/plugins/notifications/Service.qml b/shell/plugins/notifications/Service.qml index 501b2b28..61293d06 100644 --- a/shell/plugins/notifications/Service.qml +++ b/shell/plugins/notifications/Service.qml @@ -32,8 +32,9 @@ Item { // Corner radius is shared with the menu and shell panels. // It mirrors Hyprland's current decoration:rounding value. readonly property int cornerRadius: Style.cornerRadius - // Surfaces anchor relative to the omarchy bar so popups and history land - // alongside the other shell panels rather than on top of the bar itself. + // Toasts are fixed to the top-right corner. They only clear the omarchy bar + // when the bar occupies the top or right edge, so left/bottom bars do not + // pull notification popups away from the expected top-right location. // Falls back to the bar's default size (26 horizontal / 28 vertical) when // shell.bar isn't reachable so the popup never lands on top of the bar. readonly property string barPosition: shell && shell.barConfig ? String(shell.barConfig.position || "top") : "top" @@ -738,17 +739,20 @@ Item { exclusionMode: ExclusionMode.Ignore color: "transparent" + readonly property var popupPlacement: NotificationLogic.popupPlacement( + service.barPosition, service.barClearance, Style.gapsOut) + anchors { - top: service.barPosition !== "bottom" - bottom: service.barPosition === "bottom" - left: service.barPosition === "left" - right: service.barPosition !== "left" + top: popupWindow.popupPlacement.anchors.top + bottom: popupWindow.popupPlacement.anchors.bottom + left: popupWindow.popupPlacement.anchors.left + right: popupWindow.popupPlacement.anchors.right } margins { - top: service.barPosition === "top" ? service.barClearance : Style.gapsOut - bottom: service.barPosition === "bottom" ? service.barClearance : Style.gapsOut - left: service.barPosition === "left" ? service.barClearance : Style.gapsOut - right: service.barPosition === "right" ? service.barClearance : Style.gapsOut + top: popupWindow.popupPlacement.margins.top + bottom: popupWindow.popupPlacement.margins.bottom + left: popupWindow.popupPlacement.margins.left + right: popupWindow.popupPlacement.margins.right } implicitWidth: popupColumn.implicitWidth diff --git a/test/shell.d/notifications-test.sh b/test/shell.d/notifications-test.sh index eb5d23d5..d687657c 100644 --- a/test/shell.d/notifications-test.sh +++ b/test/shell.d/notifications-test.sh @@ -47,6 +47,39 @@ assert(!notifications.shouldBypassDnd({ appName: 'Slack', urgency: 2 }, 2), 'cri assert(!notifications.shouldBypassDnd({ appName: 'omarchy-menu-keybindings', urgency: 1 }, 2), 'omarchy command app names do not bypass DND') assert(!notifications.isEphemeralApp('omarchy-menu-keybindings'), 'notifications treat omarchy command app names as normal apps') +assertDeepEqual( + notifications.popupPlacement('top', 32, 6), + { + anchors: { top: true, bottom: false, left: false, right: true }, + margins: { top: 32, bottom: 6, left: 6, right: 6 } + }, + 'notifications clear a top bar while staying anchored top-right' +) +assertDeepEqual( + notifications.popupPlacement('right', 32, 6), + { + anchors: { top: true, bottom: false, left: false, right: true }, + margins: { top: 6, bottom: 6, left: 6, right: 32 } + }, + 'notifications clear a right bar while staying anchored top-right' +) +assertDeepEqual( + notifications.popupPlacement('bottom', 32, 6), + { + anchors: { top: true, bottom: false, left: false, right: true }, + margins: { top: 6, bottom: 6, left: 6, right: 6 } + }, + 'notifications ignore a bottom bar for popup placement' +) +assertDeepEqual( + notifications.popupPlacement('left', 32, 6), + { + anchors: { top: true, bottom: false, left: false, right: true }, + margins: { top: 6, bottom: 6, left: 6, right: 6 } + }, + 'notifications ignore a left bar for popup placement' +) + const notification = { id: 12, appName: 'Mail',