From a0c04c1d34dca5cb9f91b32341f2708592a87e2e Mon Sep 17 00:00:00 2001 From: Tim Uy Date: Tue, 15 Sep 2026 15:34:36 -0700 Subject: [PATCH] Emit Windows menu item clicks before Menu::Open returns TrackPopupMenu without TPM_RETURNCMD posts WM_COMMAND, so the click was emitted after Open() returned, from the host message loop. Bindings whose callbacks are only valid inside a call (Dart NativeCallable.isolateLocal) abort there: 'Cannot invoke native callback outside an isolate'. Use TPM_RETURNCMD and emit the chosen item's click (searching submenus) before returning, as the WinUI 3 backend already does. A genuine TrackPopupMenu failure still returns false. menu_backend_test: native-backend check that the click listener ran before Open() returned. --- src/platform/windows/menu_windows.cpp | 39 +++++++++++++++++++++++++-- tests/menu_backend_test.cpp | 28 +++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/platform/windows/menu_windows.cpp b/src/platform/windows/menu_windows.cpp index 36c174f..4d6e845 100644 --- a/src/platform/windows/menu_windows.cpp +++ b/src/platform/windows/menu_windows.cpp @@ -853,11 +853,46 @@ bool Menu::Open(const PositioningStrategy& strategy, Placement placement) { // - WM_INITMENUPOPUP is sent when the menu opens (triggers MenuOpenedEvent) // - WM_UNINITMENUPOPUP is sent when the menu closes (triggers // MenuClosedEvent) + // TPM_RETURNCMD: return the chosen item's id instead of posting WM_COMMAND. + // A posted WM_COMMAND is dispatched after Open() has returned, from the + // host's message loop, where no caller is on the stack; bindings whose + // listeners must run synchronously inside a call (Dart's + // NativeCallable.isolateLocal) then abort with "Cannot invoke native + // callback outside an isolate". Emitting the click here, before Open() + // returns, keeps every menu event inside the Open() call. pimpl_->opening_ = true; - const BOOL result = TrackPopupMenu(pimpl_->hmenu_, uFlags, pt.x, pt.y, 0, host_window, nullptr); + SetLastError(ERROR_SUCCESS); + const UINT cmd = static_cast(TrackPopupMenu( + pimpl_->hmenu_, uFlags | TPM_RETURNCMD, pt.x, pt.y, 0, host_window, nullptr)); + // With TPM_RETURNCMD, 0 means "dismissed" or "failed"; only the error code + // tells them apart. + const bool failed = cmd == 0 && GetLastError() != ERROR_SUCCESS; pimpl_->opening_ = false; + if (failed) { + return false; + } - return result != FALSE; + if (cmd != 0) { + // Find the item (searching submenus) and fire its click. + std::function dispatch = [&](const Menu& menu) -> bool { + for (const auto& item : menu.pimpl_->items_) { + if (item->GetId() == cmd) { + if (item->pimpl_->clicked_callback_) { + item->pimpl_->clicked_callback_(item->pimpl_->id_); + } + return true; + } + if (auto submenu = item->GetSubmenu(); submenu && dispatch(*submenu)) { + return true; + } + } + return false; + }; + dispatch(*this); + } + + // Shown (and possibly dismissed without a pick): success, as before. + return true; } bool Menu::Close() { diff --git a/tests/menu_backend_test.cpp b/tests/menu_backend_test.cpp index 1e89eb6..ccefaf5 100644 --- a/tests/menu_backend_test.cpp +++ b/tests/menu_backend_test.cpp @@ -45,6 +45,34 @@ int main(int argc, char**) { Menu wrapped(CreatePopupMenu()); if (!Check(!wrapped.SetBackend(MenuBackend::WinUI3), "Wrapped HMENU accepted modern backend")) return 1; if (argc <= 1) return 0; + { + // Native backend: the click must be emitted before Open() returns. A + // posted WM_COMMAND would arrive later from the message loop, where + // bindings with call-scoped callbacks (Dart NativeCallable.isolateLocal) + // cannot receive it. + Menu native_menu; + if (!Check(native_menu.SetBackend(MenuBackend::Native), "Native backend rejected")) return 1; + auto pick = std::make_shared("Pick me"); + native_menu.AddItem(pick); + bool clicked = false; + pick->AddListener([&](const auto&) { clicked = true; }); + auto pick_timer = SetTimer(nullptr, 0, 700, [](HWND, UINT, UINT_PTR t, DWORD) { + KillTimer(nullptr, t); + INPUT keys[4] = {}; + for (auto& k : keys) k.type = INPUT_KEYBOARD; + keys[0].ki.wVk = VK_DOWN; + keys[1].ki.wVk = VK_DOWN; + keys[1].ki.dwFlags = KEYEVENTF_KEYUP; + keys[2].ki.wVk = VK_RETURN; + keys[3].ki.wVk = VK_RETURN; + keys[3].ki.dwFlags = KEYEVENTF_KEYUP; + SendInput(4, keys, sizeof(INPUT)); + }); + const bool native_opened = + pick_timer && native_menu.Open(PositioningStrategy::Absolute({300, 300})); + if (!Check(native_opened, "Native Open failed") || + !Check(clicked, "Native click was not emitted before Open() returned")) return 1; + } if (!supported) return 1; HWND preview = nullptr; if (argc > 2) {