diff --git a/settings/quickshell.nix b/settings/quickshell.nix index 646e7d8..1f88d66 100644 --- a/settings/quickshell.nix +++ b/settings/quickshell.nix @@ -1847,19 +1847,22 @@ in color: Theme.base05 } + // The icon's own menu cache: assigned from the + // moment the icon exists, so the DBus fetch + // (slow for Steam) happens at startup, not + // mid-animation. contextMenu just points at + // this — switching tray icons never reloads. + QsMenuOpener { + id: itemOpener + menu: modelData.menu + } + MouseArea { anchors.fill: parent hoverEnabled: true acceptedButtons: Qt.NoButton onEntered: { if (!modelData.hasMenu) return; - // Prefetch: you cannot click without - // hovering first, so start the DBus - // round-trip now and the menu is - // usually loaded before the click. - if (!bar.activeDropdown && menuOpener.menu !== modelData.menu) { - menuOpener.menu = modelData.menu; - } if (!bar.activeDropdown) return; if (bar.activeDropdown === contextMenu && contextMenu.trayItem === modelData) return; let pos = parent.mapToItem(bar.contentItem, 0, parent.height / 2); @@ -1867,9 +1870,9 @@ in // Same dropdown, just switch content contextMenu.dropdownY = pos.y; contextMenu.trayItem = modelData; - menuOpener.menu = modelData.menu; + contextMenu.activeOpener = itemOpener; } else { - contextMenu.openFor(modelData, pos.y); + contextMenu.openFor(modelData, pos.y, itemOpener); } } } @@ -1884,7 +1887,7 @@ in contextMenu.animateClose(); } else { let pos = parent.mapToItem(bar.contentItem, 0, parent.height / 2); - contextMenu.openFor(modelData, pos.y); + contextMenu.openFor(modelData, pos.y, itemOpener); } } } @@ -2130,8 +2133,8 @@ in // available the instant the data lands, so the height and // its content update in the same binding pass. readonly property real menuContentHeight: { - if (!menuOpener.children) return 0; - const items = menuOpener.children.values; + if (!menuModel) return 0; + const items = menuModel.values; let h = 0; for (let i = 0; i < items.length; i++) h += items[i].isSeparator ? menuSepHeight : menuRowHeight; @@ -2148,26 +2151,23 @@ in fullHeight: (menuContentHeight > 0 ? menuContentHeight : lastMenuHeight) + 2 * Theme.cardPad + 2 * Theme.panelGap - // Deliberately NOT cleared on close. Nulling the menu threw - // away the loaded items, and the only thing that refills - // them is a hover-enter — which never fires if the cursor - // is still sitting on the icon you just clicked. Reopening - // then re-fetched over DBus and the panel grew to its empty - // height first, then jumped once the items landed. Keeping - // the handle assigned makes reopening the same item free; - // openFor() reassigns it when you pick a different one. + // Which tray item's opener feeds the menu. Every tray icon + // owns an always-assigned QsMenuOpener (see the tray + // Repeater), fetched when the icon appears — so switching + // tray→tray swaps one POPULATED model for another in a + // single binding pass. The shared opener this replaces + // reloaded over DBus on every reassignment, and no sizing + // gate downstream could hide that the model itself went + // briefly empty (that gap was the flash). + property var activeOpener: null + readonly property var menuModel: activeOpener ? activeOpener.children : null - QsMenuOpener { - id: menuOpener - } - - // Opens immediately — never wait on the menu. Hovering the - // icon has already started the DBus fetch (see the tray - // MouseArea), so the size is normally settled by click. - function openFor(item, y) { + // Opens immediately — the item's opener has been loaded + // since its icon appeared, so there is nothing to wait on. + function openFor(item, y, opener) { dropdownY = y; trayItem = item; - menuOpener.menu = item.menu; + activeOpener = opener; bar.toggleDropdown(contextMenu, null); } @@ -2178,7 +2178,7 @@ in cardSpacing: 0 Repeater { - model: menuOpener.children + model: contextMenu.menuModel Rectangle { required property var modelData