quickshell: size the tray menu from the model, not the laid-out Card

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 <noreply@anthropic.com>
This commit is contained in:
rope 2026-07-28 15:16:10 +01:00
parent 31c584e7d1
commit d866cae4f7

View file

@ -2116,31 +2116,37 @@ in
// across. Hold the last loaded height until the new items // across. Hold the last loaded height until the new items
// arrive so the morph stays continuous. The correction is // arrive so the morph stays continuous. The correction is
// small, and nil when it is the same menu as last time. // small, and nil when it is the same menu as last time.
readonly property bool menuLoaded: // Row metrics, shared by the delegate below and the height
menuOpener.children && menuOpener.children.values.length > 0 // sum these two must never drift apart.
// The model populating is NOT the same instant as the readonly property int menuRowHeight: 28
// layout catching up: the Repeater still has to build its readonly property int menuSepHeight: 9
// 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
Connections { // Size the panel from the MODEL, not from the laid-out
target: menuItems // Card. The Card only reaches its true height a frame or
function onHeightChanged() { // more after the model populates the Repeater has to
if (contextMenu.menuReady) // build delegates and the Column re-measure and every
contextMenu.lastMenuHeight = menuItems.height; // 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 // Deliberately NOT cleared on close. Nulling the menu threw
// away the loaded items, and the only thing that refills // away the loaded items, and the only thing that refills
@ -2177,7 +2183,8 @@ in
Rectangle { Rectangle {
required property var modelData required property var modelData
width: parent.width width: parent.width
height: modelData.isSeparator ? 9 : 28 height: modelData.isSeparator ? contextMenu.menuSepHeight
: contextMenu.menuRowHeight
color: !modelData.isSeparator && itemMouse.containsMouse && modelData.enabled color: !modelData.isSeparator && itemMouse.containsMouse && modelData.enabled
? Theme.base02 : Theme.base02t ? Theme.base02 : Theme.base02t
Behavior on color { ColorAnimation { duration: Theme.animFade } } Behavior on color { ColorAnimation { duration: Theme.animFade } }