diff --git a/settings/quickshell.nix b/settings/quickshell.nix index 55c5e47..646e7d8 100644 --- a/settings/quickshell.nix +++ b/settings/quickshell.nix @@ -2116,32 +2116,38 @@ in // across. Hold the last loaded height until the new items // arrive so the morph stays continuous. The correction is // small, and nil when it is the same menu as last time. - readonly property bool menuLoaded: - menuOpener.children && menuOpener.children.values.length > 0 - // The model populating is NOT the same instant as the - // layout catching up: the Repeater still has to build its - // delegates and the Column re-measure. Gating on the model - // alone left a frame where menuLoaded was true but the Card - // was still empty-sized, so the panel collapsed and then - // regrew — visible when switching between two tray icons, - // which is the only path that reassigns the handle. - // Require the height to have actually grown past an empty - // Card (which measures 2*cardPad, not 0). - readonly property bool menuReady: - menuLoaded && menuItems.height > 2 * Theme.cardPad - property real lastMenuHeight: 0 - fullHeight: (menuReady ? menuItems.height - : Math.max(menuItems.height, lastMenuHeight)) - + 2 * Theme.panelGap + // Row metrics, shared by the delegate below and the height + // sum — these two must never drift apart. + readonly property int menuRowHeight: 28 + readonly property int menuSepHeight: 9 - Connections { - target: menuItems - function onHeightChanged() { - if (contextMenu.menuReady) - contextMenu.lastMenuHeight = menuItems.height; - } + // Size the panel from the MODEL, not from the laid-out + // Card. The Card only reaches its true height a frame or + // more after the model populates — the Repeater has to + // build delegates and the Column re-measure — and every + // previous attempt here read the Card during that gap, + // which is the flash. Summing the rows is exact and + // 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; + let h = 0; + for (let i = 0; i < items.length; i++) + h += items[i].isSeparator ? menuSepHeight : menuRowHeight; + return h; } + // Hold the previous size across the DBus fetch of a newly + // assigned menu (the tray-to-tray path), so the switch + // morphs instead of collapsing to empty and regrowing. + property real lastMenuHeight: 0 + onMenuContentHeightChanged: { + if (menuContentHeight > 0) lastMenuHeight = menuContentHeight; + } + 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 @@ -2177,7 +2183,8 @@ in Rectangle { required property var modelData width: parent.width - height: modelData.isSeparator ? 9 : 28 + height: modelData.isSeparator ? contextMenu.menuSepHeight + : contextMenu.menuRowHeight color: !modelData.isSeparator && itemMouse.containsMouse && modelData.enabled ? Theme.base02 : Theme.base02t Behavior on color { ColorAnimation { duration: Theme.animFade } }