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,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 } }