Skip to content

xpl: a popup surface cannot leave the frame, which caps what a plugin menu can be #23

Description

@baconpaul

Following on from the gap response — popup_tree_menu closed the richness half of §2.3 completely. Submenus, checkmarks, enable/disable, shortcut labels and command routing are all there and working, and it is genuinely nice to use. Thank you.

This is about the other half, which I did not separate out clearly enough the first time: where the menu is allowed to be. On the crossplatform host a popup is painted into the frame's surface, so it cannot extend past the editor window. For a desktop app that is fine. For an audio plugin it is a hard ceiling, and it is the one thing I have not been able to work around cleanly.

Why it matters

Plugin menus routinely exceed the editor. Surge XT's FX-selector menu is a three-column grid with bold section headers — roughly 1030 × 970 logical px — opened from a plugin editor considerably smaller than that. JUCE serves it by putting the menu on the desktop as its own top-level window (Component::addToDesktop), so it simply hangs over the DAW. (I'll drop a screenshot in a comment below.)

This is not an unusual UI. Preset browsers, modulation-routing pickers and effect selectors are all this shape, and the pattern is common across commercial plugins. Clamping such a menu into the editor is not a cosmetic regression — the content genuinely does not fit.

What the code does today

Both xpl menu paths paint into the frame:

  • Session::open_popup_menu (hosts/crossplatform/host.cpp) walks up from the anchor to build _popup_x_abs / _popup_y_abs as frame-local absolute coordinates, stopping at the widget with a native_handle, then paints via paint_popup_menu(ctx) from inside Session::paint_frame.
  • Session::show_tree_popup paints via paint_tree_popup(ctx, parent_index), also from paint_frame, reusing the menubar's cascade layout and edge clamping.

So the clamping is correct and deliberate — given a single frame surface there is nowhere else for the pixels to go.

The awkward part is the asymmetry with the native hosts:

escapes the frame submenus / checkmarks EMBED / A11Y / TIMER / POINTER
xpl no yes yes
native macOS yes (run_popup_menu_macos → real NSMenu + popUpMenuPositioningItem:) no (popup_tree_menu slot is NULL) no
native win32 (same shape) no no

The one host that can put a menu outside the window is the one a plugin cannot use; the host with everything a plugin needs is the one that cannot.

What I tried client-side, and where it fails

I built the workaround: one NEUI_W_PLUGWINDOW per menu level, positioned in screen coordinates via widgets->set_pos, each holding a CUSTOMDRAW panel that paints the rows.

Code, if it is useful to look at: PopupMenu.hMenuPlacement::desktop is the relevant path, and menudemo.cpp opens the same menu three ways (native popup_tree_menu, client-drawn in-frame, client-drawn on the desktop) so they can be compared side by side.

It half works. The menu does escape the frame, the cascade positions correctly, and a real NEUI_W_INPUTBOX in a row still behaves. But it is not a menu, because the popup window has no relationship with the editor:

  • It is not dismissed when the main window loses focus.
  • It is not raised with its owner. Click from the open menu to another application and then back to the app, and the menu is stranded behind the editor — still open, now unreachable.

That second one is the killer, and I do not think it is fixable from the client side: there is no way to express "this frame is subordinate to that frame" for z-order and activation purposes. widgets->set_owner is the nearest thing, but it is documented for DIALOG modality and input-blocks the owner while shown, which is the opposite of what a menu wants — clicking the editor should dismiss the menu, not be swallowed.

So the client can get the pixels outside the window, but not the window behaviour that makes those pixels a menu.

The ask

Some way for a popup surface to live in its own OS window on xpl: one borderless platform window per open menu level, painted with the existing painter and cascade code, positioned in screen coordinates, and owned by the frame for z-order, activation and dismissal.

The drawing machinery looks like it mostly exists — xpl already creates and manages top-level platform windows per frame, and paint_tree_popup / mb_build_columns already do layout, flipping and hit-testing. The new parts are window lifetime, not stealing activation from the editor, owner-follows z-order, and a grab so a click anywhere dismisses.

I am not attached to any particular shape. Alternatives that would also solve it from my side:

  1. An opt-in attr on the popup — e.g. NEUI_ATTR_POPUP_DESKTOP on NEUI_W_POPUPMENU — so existing in-frame behaviour stays the default and only clients that need it pay the cost.
  2. A general "frameless popup surface" widget type a client can fill with CUSTOMDRAW and position in screen coordinates, with the host owning only the window, activation, z-order and grab. That would also serve combo drop-lists and tooltips, which have the same ceiling.
  3. Just the owner relationship — if a PLUGWINDOW could be declared subordinate to a frame for z-order and activation without the modal input-block, the client-side version above would become viable and you would not have to own menus at all. This is the smallest of the three.

Something else entirely is fine too — you know the host architecture and I do not.

Not urgent

Nothing here blocks the widget-toolkit work; everything else is proceeding and small menus are entirely fine as they are today. This is the thing that would eventually stop a real plugin editor from being a faithful port, so I would rather raise it early than surprise you with it late.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions