Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions src/platform/windows/menu_windows.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<UINT>(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<bool(const Menu&)> 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() {
Expand Down
28 changes: 28 additions & 0 deletions tests/menu_backend_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<MenuItem>("Pick me");
native_menu.AddItem(pick);
bool clicked = false;
pick->AddListener<MenuItemClickedEvent>([&](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) {
Expand Down