Skip to content

Industry standard to avoid side jumping app #377

@eonist

Description

@eonist

Now I have enough deep research. Here is the honest, well-sourced answer:


What the Community Actually Does About This

After studying the broader macOS ecosystem, here is the real picture. The situation is more nuanced than your current code treats it.

The Fundamental Truth: NSPopover Was Never Designed for Dynamic Resizing While Visible

Apple's own documentation states that NSPopover re-anchors any time contentSize changes while shown . This is not a bug — it is by design. The popover's position is entirely derived from its anchor rect + contentSize. Change one, and AppKit recomputes both X and Y from scratch using the status button's current screen coordinates, which due to coordinate system transforms ends up being wrong almost every time in a status bar context.

This means there is no safe, non-hacky way to resize an NSPopover while it is visible. Every approach you have tried — setFrameSize, contentSize writes, preferredContentSize KVO — all trigger this re-anchor. That's confirmed across dozens of Stack Overflow threads and open-source projects.


What Other Apps Actually Do (3 Proven Patterns)

Pattern 1 — Fix Size at Open, Let Content Scroll (Most Common)

The overwhelmingly dominant pattern in real-world status bar apps. The popover opens at a fixed or max-content-fitted size. All dynamic content uses ScrollView internally so the frame never needs to change. Apps like Lungo, Pockity, and virtually every Sindre Sorhus utility use this.

The main branch of runner-bar already does this correctly — fittingSize is read once before show(), height is clamped to maxHeight: 620, and ScrollView handles overflow. This works.

Pattern 2 — Abandon NSPopover: Use NSPanel Instead

A very significant chunk of serious macOS menu bar apps — including productivity tools like those described in and — fully abandon NSPopover in favour of a manually-positioned NSPanel. The reason:

"Every possible case for when the popover should be hidden needed to be manually listened for. Overall it felt like I was fighting against OSX way more than I should have."

With NSPanel, you position it yourself using the status button's screen coordinates, and you resize it freely with setFrame()at any time, while visible, with zero re-anchor issues because NSPanel has no anchor concept at all.

// NSPanel approach — resize freely while visible, no jump ever
let buttonFrame = buttonWindow.convertToScreen(button.frame)
let x = buttonFrame.midX - panel.frame.width / 2
let y = buttonFrame.minY - panel.frame.height - 4
panel.setFrameOrigin(NSPoint(x: x, y: y))
// Later, while visible — SAFE:
panel.setFrame(NSRect(origin: panel.frame.origin, size: newSize), display: true, animate: false)

Pattern 3 — MenuBarExtra(.window) (macOS 13+)

Apple's native SwiftUI API since Ventura. Automatically handles anchoring, dismissal, and content sizing. Dynamic height just works because Apple owns the entire rendering pipeline. The caveat: limited customisation and known layout issues with ScrollView in some macOS versions.


What This Means for runner-bar

The current NSPopover architecture on main is correct for what NSPopover can reliably do: measure once at open, show at fixed height, scroll internally. The fix pushed in the last commit (restore nav state before measuring fittingSize) is also correct — it ensures Settings and detail views get their proper height at open time.

The only remaining gap is: if you need the popover to grow taller while open (e.g. "Load 10 more actions…" expands the list), you cannot do that with NSPopover without jumping. The choices are:

  1. Accept the capmaxHeight: 620 with internal scroll. This is what every single comparable app does.
  2. Switch to NSPanel — gives you full resize freedom at the cost of implementing your own dismiss-on-outside-click, full-screen app support, and mission control handling.
  3. Switch to MenuBarExtra(.window) — simplest, but loses the fine-grained AppKit control you've built.

The jump regression you keep hitting is not a code quality problem — it is the documented, intentional behaviour of NSPopover.contentSize. The only real fix is to never write to it while shown. Your main branch already does that correctly.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions