From d866cae4f7a186aff8a5d5930376048515b0765c Mon Sep 17 00:00:00 2001 From: rope Date: Tue, 28 Jul 2026 15:16:10 +0100 Subject: [PATCH] quickshell: size the tray menu from the model, not the laid-out Card MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every previous attempt read the panel height off menuItems, which by definition lags the data: the Repeater must build delegates and the Column re-measure before the Card reports its real size. Reading it during that gap is the flash, and no amount of gating on the model closes it, because the gap is between the model and the layout. fullHeight now sums the rows from menuOpener.children directly. That is exact — cardSpacing is 0, so the sum plus 2*cardPad is precisely what the Card lays out — and it lands in the same binding pass as the data, so size and content never disagree. Row metrics moved to menuRowHeight/menuSepHeight, shared by the sum and the delegate so they cannot drift. The previous-height hold stays for the DBus fetch itself. Co-Authored-By: Claude Opus 5 --- settings/quickshell.nix | 55 +++++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 24 deletions(-) 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 } }