Skip to content

sni/item: wait for a fresh tray menu to actually populate before showing it - #5322

Closed
daviesjamesdaniel wants to merge 2 commits into
Alexays:masterfrom
daviesjamesdaniel:fix/sni-first-open-menu-glitch
Closed

daviesjamesdaniel wants to merge 2 commits into
Alexays:masterfrom
daviesjamesdaniel:fix/sni-first-open-menu-glitch

Conversation

@daviesjamesdaniel

@daviesjamesdaniel daviesjamesdaniel commented Sep 12, 2026

Copy link
Copy Markdown

What does this PR do?

Fixes a tray context menu that renders mis-sized/glitched the very first time it's opened after an app starts, but always renders correctly on any later open of the same menu.

Root cause: dbusmenu_gtkmenu_new() (in Item::makeMenu()) only starts an async D-Bus fetch of the menu's real layout - it doesn't wait for it. On the very first show for a given item, handleClick() called popup_at_pointer() immediately afterward, while the menu widget could still have none of its real children. GTK measures that (empty) size for the Wayland xdg_popup positioner, and that size is locked in immediately - it can't grow once the real items arrive a moment later. Any later open of the same, by-then-populated, cached menu renders correctly, which is why this only ever shows up on first open.

Evidence

WAYLAND_DEBUG=1 against unpatched Waybar, opening the same tray item's menu twice in a row - first open (glitched) vs. second open (fine):

# First open - positioner created and configured for an essentially empty menu
-> xdg_positioner#45.set_size(50, 8)
-> xdg_surface#46.get_popup(new id xdg_popup#47, nil, xdg_positioner#45)
   xdg_popup#47.configure(1326, 22, 50, 8)

# Second open of the same item, moments later - correctly sized
-> xdg_positioner#48.set_size(98, 56)
-> xdg_surface#45.get_popup(new id xdg_popup#46, nil, xdg_positioner#48)
   xdg_popup#46.configure(1326, 21, 98, 56)

Waybar's own stderr at the moment of the glitched (50x8) open:

*** BUG ***
In pixman_region32_init_rect: Invalid rectangle passed
Set a breakpoint on '_pixman_log_error' to debug

Confirmed this correlates exactly with the visible glitch across every run during testing: present on every glitched open, absent on every clean one.

Also confirmed live, instrumenting Item::handleClick/makeMenu directly, that neither of the two more "obvious" signals to wait on actually works:

  • The DbusmenuClient's own "layout-updated" signal fired while gtk_menu->get_children().size() was still 0.
  • The container's "add" signal never fired even once against a real 16-item menu (Steam's tray menu) despite it populating correctly within 200ms - polling get_children() directly was the only approach that reliably observed the real count in both the trivial and the 16-item case.

The fix

On the first-ever show of a menu, instead of popping up immediately, poll get_children() every 30ms and wait for the count to hold steady (unchanged) for 3 consecutive ticks before showing it, with a 2-second ceiling for a menu that's genuinely empty or an app that never responds. The polling logic treats a steady run of zero differently from a steady run of a positive count - otherwise a menu that simply hasn't gained its first item yet within 3 ticks gets shown empty, which is the same bug through a different door; only a positive, unchanging count counts toward "settled."

A second click landing while a first show is still pending is guarded against (it would otherwise see a non-null gtk_menu and pop up immediately on the same still-populating menu), and the wait is cancelled cleanly if the tray item is destroyed before it settles.

Tested repeatedly, live, against both a trivial 2-item menu and Steam's real ~16-item one, across many fresh app launches, on Hyprland/Wayland. No pixman/GTK errors and no visible glitch in any run after the fix, versus a reliably reproducible one before it.

Related issues

Not a duplicate of #1149 (that's about a menu's content changing after creation going stale) - this reproduces on a menu whose content never changes at all, and the mechanism is different (initial-population timing, not a later update).

Checklist

  • Code is formatted with clang-format
  • Builds locally (ninja -C build)
  • Man page updated for any new/changed user-facing option (N/A - no user-facing option added)
  • Tested against the affected module(s)

…ing it

A tray item's context menu (dbusmenu_gtkmenu_new()) only starts an async
D-Bus fetch of its real layout - it doesn't wait for it. On the very
first show for a given menu, popup_at_pointer() could run while the menu
widget still had none of its real children, so GTK measured an empty
menu's size for the Wayland xdg_popup positioner. That size is locked in
immediately and can't grow once the real items arrive a moment later,
producing a visibly mis-sized/glitched popup on first open only - any
later open of the same (by-then-populated, cached) menu renders fine.

Confirmed live against both a trivial static 2-item menu and a real
16-item one (Steam): the client's own "layout-updated" signal fires
before the corresponding GTK widgets are packed in, and the container's
"add" signal isn't a reliable substitute either - it never fired once
for the 16-item case despite the menu populating correctly within
200ms. Polling get_children() directly did reliably observe the real
count in both cases, so the first show now waits for that count to hold
steady for a few ticks (or a fixed ceiling, for a menu that's genuinely
empty or an app that never responds) before popping up.

A second click landing on the same still-pending first show is guarded
against with a bool - without it, that second click would see a
non-null gtk_menu and pop up immediately on the still-populating menu.
A pointer to the pending wait is also kept on the Item so its destructor
can cancel it if the tray item disappears before it settles.
- Move the poll-wait helpers into Item as static methods (matching the
  existing onMenuDestroyed pattern for GObject C callbacks), instead of
  free functions - free functions are snake_case in this codebase, but
  this idiom is the established one for exactly this situation.
- Give the two new Item member variables the trailing underscore the
  naming convention requires, and move them to the private section
  alongside the rest of the item's internal-only state.
- Trim comments that restated the commit message rather than explaining
  a non-obvious invariant in the code itself.
- Run clang-format.
@daviesjamesdaniel
daviesjamesdaniel marked this pull request as ready for review September 12, 2026 19:17
@daviesjamesdaniel

Copy link
Copy Markdown
Author

Closing in favor of #5315, which supersedes #5293 and fixes the actual regression (missing makeMenu() call after b58018b/#5238) rather than working around it generically. Left independent corroborating evidence on #5315 - real-tray-icon Wayland protocol traces and the same pixman signature, gathered before I'd found either of these PRs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant