Skip to content

fix(PopoverHost): keep dropdown panels on screen (right-align to trigger, flip, clamp, follow resizes) - #446

Open
lucianoltx wants to merge 1 commit into
ronitsingh10:mainfrom
lucianoltx:fix/popover-keep-on-screen
Open

lucianoltx wants to merge 1 commit into
ronitsingh10:mainfrom
lucianoltx:fix/popover-keep-on-screen

Conversation

@lucianoltx

Copy link
Copy Markdown

Summary

The per-app device picker (and any other PopoverHost dropdown) is positioned left-aligned with its trigger without looking at the screen. Icon-only triggers sit at the far right of the menu bar popup, and the popup itself hugs the menu bar icon near the screen edge, so the panel routinely ran past the right edge and was clipped — device names unreadable, "Multi" toggle unreachable.

Fixes #399, fixes #391, fixes #338, fixes #325.

What changes (single file: PopoverHost.swift)

  • Right-align the panel with its trigger when it would overflow to the right, so it still reads as attached to the button it came from (instead of sliding to the screen edge).
  • Flip above the trigger when there is no room below.
  • Clamp to the visibleFrame of the screen containing the trigger's center (multi-monitor safe; excludes menu bar and Dock).
  • Recompute the trigger's screen frame on every placement, so the panel follows its parent window if that window moves while the panel is open.
  • Re-run placement on resize — both in updateContent (using the intended size: setContentSize keeps the bottom-left origin and, in my testing, panel.frame.size is not yet updated at that point, so re-clamping with the old size leaves a growing panel off screen) and on NSWindow.didResizeNotification (NSHostingView can resize the panel asynchronously through Auto Layout).

Evidence

Verified with a small headless AppKit harness that compiles PopoverHost.swift alone and drives Coordinator.showPanel / updateContent against a borderless window placed like the menu bar popup (4 scenarios, 14 checks):

scenario original this PR
trigger at the right screen edge panel maxX = 1928 on a 1728-wide screen (208 pt off screen), still off screen after content grows within the 8 pt margin, right-aligned to the trigger, stays anchored after growing
trigger with plenty of room left-aligned (unchanged) left-aligned (unchanged)
trigger near the bottom of the screen panel minY = −118 flipped above the trigger, stays anchored after growing
parent window moved while the panel is open — panel follows the trigger on the next placement

Also tested by hand on macOS 26.5.2 (MacBook Pro 16", built-in display): Spotify → globe → picker fully visible; System Audio / built-in speakers / Bluetooth headphones all selectable.

Relation to #412

#412 also clamps this panel as part of a 12-file batch. This PR is a focused, single-file alternative that additionally keeps the panel attached to its trigger, handles the async resize path, and follows a moved parent window. Happy to close this one if #412 lands first, or rebase on it.

Icon-only triggers (the device picker in app rows) sit at the far right of
the menu bar popup, which itself hugs the screen edge, so the left-aligned
NSPanel routinely ran past the right edge and was clipped: device names
unreadable and the Multi toggle unreachable.

- right-align the panel with its trigger when it would overflow to the right
- flip it above the trigger when there is no room below
- clamp to the visible frame of the screen containing the trigger's center
- recompute the trigger's screen frame on every placement, so the panel
  follows its parent window if that window moves while the panel is open
- re-run placement on content resize (setContentSize keeps the bottom-left
  origin, so a growing panel used to creep upward) and on async Auto Layout
  resizes of the NSHostingView

Verified with a headless AppKit harness (4 scenarios): the original code
places the panel 208pt off screen for a trigger at the right edge; the
patched code keeps it within an 8pt margin, right-aligned to the trigger,
anchored through resizes and parent-window moves.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant