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.h — MenuPlacement::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:
- 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.
- 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.
- 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.
Following on from the gap response —
popup_tree_menuclosed 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_absas frame-local absolute coordinates, stopping at the widget with anative_handle, then paints viapaint_popup_menu(ctx)from insideSession::paint_frame.Session::show_tree_popuppaints viapaint_tree_popup(ctx, parent_index), also frompaint_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:
run_popup_menu_macos→ realNSMenu+popUpMenuPositioningItem:)popup_tree_menuslot is NULL)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_PLUGWINDOWper menu level, positioned in screen coordinates viawidgets->set_pos, each holding aCUSTOMDRAWpanel that paints the rows.Code, if it is useful to look at:
PopupMenu.h—MenuPlacement::desktopis the relevant path, andmenudemo.cppopens the same menu three ways (nativepopup_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_INPUTBOXin a row still behaves. But it is not a menu, because the popup window has no relationship with the editor: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_owneris 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_columnsalready 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:
NEUI_ATTR_POPUP_DESKTOPonNEUI_W_POPUPMENU— so existing in-frame behaviour stays the default and only clients that need it pay the cost.CUSTOMDRAWand 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.PLUGWINDOWcould 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.