From 417e474551478e02853e997af8d7c89473a0d7fc Mon Sep 17 00:00:00 2001 From: Anurag Kumar Date: Tue, 31 Mar 2026 09:21:56 +0530 Subject: [PATCH 01/17] initial api design --- specs/custommenuspellcheck.md | 381 ++++++++++++++++++++++++++++++++++ 1 file changed, 381 insertions(+) create mode 100644 specs/custommenuspellcheck.md diff --git a/specs/custommenuspellcheck.md b/specs/custommenuspellcheck.md new file mode 100644 index 00000000..252234c2 --- /dev/null +++ b/specs/custommenuspellcheck.md @@ -0,0 +1,381 @@ +Custom Context Menu SpellCheck +=== + +# Background + +When a host application renders a custom context menu via the `ContextMenuRequested` event, spellcheck +suggestions for misspelled words are not available. The browser's built-in spellcheck pipeline resolves +suggestions asynchronously, but there is no mechanism for custom context menu hosts to retrieve or apply +these suggestions. + +# Description + +We propose extending the existing `ContextMenuRequested` API surface with spellcheck support for custom +context menus. This adds the ability to: + +1. Query spellcheck information (misspelled word, readiness state, suggestions) from the context menu target. +2. Subscribe to an asynchronous notification when spellcheck suggestions become available. +3. Apply a selected spellcheck suggestion to replace the misspelled word in the DOM. + +Hosts opt in by calling `QueryInterface` for the new `ICoreWebView2ContextMenuTarget2` and +`ICoreWebView2ContextMenuRequestedEventArgs2` interfaces. Existing `ContextMenuRequested` consumers +are unaffected. + +# Examples + +## Win32 C++ + +```cpp +void ShowCustomContextMenuWithSpellCheck( + ICoreWebView2ContextMenuRequestedEventArgs* args) +{ + wil::com_ptr target; + CHECK_FAILURE(args->get_ContextMenuTarget(&target)); + + BOOL isEditable = FALSE; + CHECK_FAILURE(target->get_IsEditable(&isEditable)); + + HMENU hMenu = CreatePopupMenu(); + UINT menuIndex = 0; + + if (isEditable) + { + auto target2 = target.try_query(); + auto args2 = wil::com_ptr_query(args); + + if (target2 && args2) + { + wil::unique_cotaskmem_string misspelledWord; + COREWEBVIEW2_SPELL_CHECK_READINESS spellState; + wil::com_ptr suggestions; + CHECK_FAILURE(target2->GetSpellCheckInfo( + &misspelledWord, &spellState, &suggestions)); + + if (spellState == COREWEBVIEW2_SPELL_CHECK_READINESS_READY) + { + // Suggestions are available — add them to the menu. + UINT32 count = 0; + suggestions->get_Count(&count); + for (UINT32 i = 0; i < count && i < 5; i++) + { + wil::unique_cotaskmem_string suggestion; + suggestions->GetValueAtIndex(i, &suggestion); + MENUITEMINFO mii = {}; + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_STRING | MIIM_ID; + mii.wID = IDM_SPELL_SUGGESTION_BASE + i; + mii.dwTypeData = suggestion.get(); + InsertMenuItem(hMenu, menuIndex++, TRUE, &mii); + } + } + else if (spellState == COREWEBVIEW2_SPELL_CHECK_READINESS_NOT_READY) + { + // Suggestions pending — show placeholder and register async handler. + MENUITEMINFO mii = {}; + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_STRING | MIIM_ID | MIIM_STATE; + mii.fState = MFS_DISABLED; + mii.wID = IDM_SPELL_PLACEHOLDER; + mii.dwTypeData = const_cast(L"Loading suggestions..."); + InsertMenuItem(hMenu, menuIndex++, TRUE, &mii); + + // Handler fires when suggestions arrive (or immediately if + // already resolved). During TrackPopupMenu's modal loop, + // the handler updates the menu in-place. + CHECK_FAILURE(args2->add_SpellCheckSuggestionsReady( + Callback( + [hMenu, target2, args2]( + ICoreWebView2ContextMenuRequestedEventArgs* sender, + IUnknown* eventArgs) -> HRESULT + { + wil::unique_cotaskmem_string word; + COREWEBVIEW2_SPELL_CHECK_READINESS state; + wil::com_ptr suggs; + CHECK_FAILURE(target2->GetSpellCheckInfo( + &word, &state, &suggs)); + + if (state == COREWEBVIEW2_SPELL_CHECK_READINESS_READY) + { + UINT32 count = 0; + suggs->get_Count(&count); + if (count > 0) + { + // Replace placeholder with first suggestion. + wil::unique_cotaskmem_string first; + suggs->GetValueAtIndex(0, &first); + MENUITEMINFO mii = {}; + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_STRING | MIIM_ID | MIIM_STATE; + mii.fState = MFS_ENABLED; + mii.wID = IDM_SPELL_SUGGESTION_BASE; + mii.dwTypeData = first.get(); + SetMenuItemInfo( + hMenu, IDM_SPELL_PLACEHOLDER, FALSE, &mii); + + // Insert remaining suggestions. + for (UINT32 i = 1; i < count && i < 5; i++) + { + wil::unique_cotaskmem_string s; + suggs->GetValueAtIndex(i, &s); + MENUITEMINFO item = {}; + item.cbSize = sizeof(item); + item.fMask = MIIM_STRING | MIIM_ID; + item.wID = IDM_SPELL_SUGGESTION_BASE + i; + item.dwTypeData = s.get(); + InsertMenuItem(hMenu, i, TRUE, &item); + } + } + + // Repaint the popup menu. + HWND hPopup = FindWindow(L"#32768", nullptr); + if (hPopup) + { + RedrawWindow( + hPopup, nullptr, nullptr, + RDW_INVALIDATE | RDW_UPDATENOW | RDW_ERASE); + } + } + return S_OK; + }) + .Get(), + &m_spellCheckToken)); + } + } + } + + // Add standard WebView2 context menu items. + wil::com_ptr items; + CHECK_FAILURE(args->get_MenuItems(&items)); + UINT32 itemCount; + CHECK_FAILURE(items->get_Count(&itemCount)); + for (UINT32 i = 0; i < itemCount; i++) + { + wil::com_ptr item; + CHECK_FAILURE(items->GetValueAtIndex(i, &item)); + // ... add each item to hMenu ... + } + + // Show the menu. + UINT selectedId = TrackPopupMenu( + hMenu, TPM_RETURNCMD, pt.x, pt.y, 0, m_hWnd, nullptr); + + // Handle selection. + if (selectedId >= IDM_SPELL_SUGGESTION_BASE && + selectedId < IDM_SPELL_SUGGESTION_BASE + 5) + { + // Apply the selected spellcheck suggestion. + wil::unique_cotaskmem_string word; + COREWEBVIEW2_SPELL_CHECK_READINESS state; + wil::com_ptr suggs; + target2->GetSpellCheckInfo(&word, &state, &suggs); + wil::unique_cotaskmem_string chosen; + suggs->GetValueAtIndex(selectedId - IDM_SPELL_SUGGESTION_BASE, &chosen); + args2->ApplySpellCheckSuggestion(chosen.get()); + } + + args->put_Handled(TRUE); + DestroyMenu(hMenu); +} +``` + +## C#/.NET + +```csharp +void ShowCustomContextMenuWithSpellCheck( + object sender, CoreWebView2ContextMenuRequestedEventArgs args) +{ + var target = args.ContextMenuTarget; + var menuItems = new List(); + + if (target.IsEditable) + { + string misspelledWord = target.MisspelledWord; + CoreWebView2SpellCheckReadiness spellState = target.SpellCheckReadiness; + IReadOnlyList suggestions = target.SpellCheckSuggestions; + + if (spellState == CoreWebView2SpellCheckReadiness.Ready && suggestions.Count > 0) + { + // Suggestions available — add them directly. + foreach (string suggestion in suggestions.Take(5)) + { + var item = new ToolStripMenuItem(suggestion); + item.Click += (s, e) => + { + args.ApplySpellCheckSuggestion(suggestion); + }; + menuItems.Add(item); + } + menuItems.Add(new ToolStripSeparator()); + } + else if (spellState == CoreWebView2SpellCheckReadiness.NotReady) + { + // Suggestions pending — show placeholder. + var placeholder = new ToolStripMenuItem("Loading suggestions...") + { + Enabled = false + }; + menuItems.Add(placeholder); + menuItems.Add(new ToolStripSeparator()); + + // Register async handler. Fires when suggestions resolve + // or immediately if already resolved. + args.SpellCheckSuggestionsReady += (s, e) => + { + string word = target.MisspelledWord; + var readySuggestions = target.SpellCheckSuggestions; + if (target.SpellCheckReadiness == CoreWebView2SpellCheckReadiness.Ready + && readySuggestions.Count > 0) + { + // Replace placeholder with actual suggestions. + int index = menuItems.IndexOf(placeholder); + menuItems.Remove(placeholder); + foreach (string suggestion in readySuggestions.Take(5)) + { + var item = new ToolStripMenuItem(suggestion); + item.Click += (s2, e2) => + { + args.ApplySpellCheckSuggestion(suggestion); + }; + menuItems.Insert(index++, item); + } + } + }; + } + } + + // Add standard WebView2 context menu items. + foreach (var menuItem in args.MenuItems) + { + // ... add each item to menuItems ... + } + + // Show the context menu. + var contextMenu = new ContextMenuStrip(); + contextMenu.Items.AddRange(menuItems.ToArray()); + contextMenu.Show(webView, webView.PointToClient(Cursor.Position)); + + args.Handled = true; +} +``` + +# API Details + +## Win32 C++ + +```idl +/// Indicates the readiness of spellcheck suggestions for the context menu +/// target. Used by hosts rendering custom context menus. +[v1_enum] +typedef enum COREWEBVIEW2_SPELL_CHECK_READINESS { + /// Spellcheck suggestions are available and ready to display. + COREWEBVIEW2_SPELL_CHECK_READINESS_READY, + /// Spellcheck is active but suggestions have not yet been resolved + /// (asynchronous retrieval in progress). + COREWEBVIEW2_SPELL_CHECK_READINESS_NOT_READY, + /// Spellcheck suggestions are not applicable for the current context + /// (not editable, no misspelling, or spellcheck disabled). + COREWEBVIEW2_SPELL_CHECK_READINESS_NOT_AVAILABLE, + /// Spellcheck resolution failed due to an internal error. + /// No suggestions will arrive. + COREWEBVIEW2_SPELL_CHECK_READINESS_ERROR, +} COREWEBVIEW2_SPELL_CHECK_READINESS; + +/// Receives `SpellCheckSuggestionsReady` events from +/// ICoreWebView2ContextMenuRequestedEventArgs2. +[uuid(c5d6e7f8-9a0b-1c2d-3e4f-5a6b7c8d9e0f), object, pointer_default(unique)] +interface ICoreWebView2SpellCheckSuggestionsReadyEventHandler : IUnknown { + /// Provides the event args for the corresponding event. + HRESULT Invoke( + [in] ICoreWebView2ContextMenuRequestedEventArgs* sender, + [in] IUnknown* args); +} + +/// Extends ICoreWebView2ContextMenuTarget with spellcheck information +/// for custom context menu integration. Allows host applications to retrieve +/// spellcheck suggestions and metadata when rendering custom menus +/// on editable fields. +[uuid(d3f7e01a-9b5c-4e8f-a1d2-7c6b3e4f5a80), object, pointer_default(unique)] +interface ICoreWebView2ContextMenuTarget2 : ICoreWebView2ContextMenuTarget { + /// Gets spellcheck information for the current context menu target in a + /// single call. Returns the misspelled word (empty if none), the readiness + /// state of suggestions, and a collection of suggestion strings. + /// + /// The caller must free `misspelledWord` with `CoTaskMemFree`. + /// + /// When `state` is `COREWEBVIEW2_SPELL_CHECK_READINESS_READY`, the + /// `suggestions` collection is populated with correction strings. + /// When `state` is `COREWEBVIEW2_SPELL_CHECK_READINESS_NOT_READY`, the + /// collection is empty; subscribe to `SpellCheckSuggestionsReady` on + /// `ICoreWebView2ContextMenuRequestedEventArgs2` to be notified when + /// suggestions become available, then call this method again. + /// When `state` is `COREWEBVIEW2_SPELL_CHECK_READINESS_NOT_AVAILABLE` or + /// `COREWEBVIEW2_SPELL_CHECK_READINESS_ERROR`, no suggestions will arrive. + HRESULT GetSpellCheckInfo( + [out] LPWSTR* misspelledWord, + [out] COREWEBVIEW2_SPELL_CHECK_READINESS* state, + [out, retval] ICoreWebView2StringCollection** suggestions); +} + +/// Extends ICoreWebView2ContextMenuRequestedEventArgs with methods to apply +/// spellcheck corrections and subscribe to asynchronous suggestion delivery. +[uuid(e4a8f3b2-6c1d-4e9a-b5f7-2d8c9a0e1b34), object, pointer_default(unique)] +interface ICoreWebView2ContextMenuRequestedEventArgs2 + : ICoreWebView2ContextMenuRequestedEventArgs { + /// Applies the selected spellcheck suggestion by replacing the misspelled + /// word in the currently focused editable field. The `suggestion` parameter + /// must be one of the strings obtained from the `suggestions` collection + /// returned by `ICoreWebView2ContextMenuTarget2::GetSpellCheckInfo`. + /// The runtime handles all editing internally, including routing to the + /// correct frame for nested iframes. + HRESULT ApplySpellCheckSuggestion([in] LPCWSTR suggestion); + + /// Registers an event handler for `SpellCheckSuggestionsReady`. This fires + /// when asynchronous spellcheck suggestions become available. If suggestions + /// are already in `READY` state at registration time, the handler fires + /// immediately and synchronously. + HRESULT add_SpellCheckSuggestionsReady( + [in] ICoreWebView2SpellCheckSuggestionsReadyEventHandler* eventHandler, + [out] EventRegistrationToken* token); + + /// Removes the event handler previously added with + /// `add_SpellCheckSuggestionsReady`. + HRESULT remove_SpellCheckSuggestionsReady( + [in] EventRegistrationToken token); +} +``` + +## .NET/C# + +```csharp +namespace Microsoft.Web.WebView2.Core +{ + enum CoreWebView2SpellCheckReadiness + { + Ready = 0, + NotReady = 1, + NotAvailable = 2, + Error = 3, + }; + + runtimeclass CoreWebView2ContextMenuTarget + { + [interface_name("Microsoft.Web.WebView2.Core.ICoreWebView2ContextMenuTarget2")] + { + String MisspelledWord { get; }; + CoreWebView2SpellCheckReadiness SpellCheckReadiness { get; }; + IVectorView SpellCheckSuggestions { get; }; + } + } + + runtimeclass CoreWebView2ContextMenuRequestedEventArgs + { + [interface_name("Microsoft.Web.WebView2.Core.ICoreWebView2ContextMenuRequestedEventArgs2")] + { + void ApplySpellCheckSuggestion(String suggestion); + event Windows.Foundation.TypedEventHandler< + CoreWebView2ContextMenuRequestedEventArgs, Object> + SpellCheckSuggestionsReady; + } + } +} +``` From aab5c7a29b4d70802f3d66c96375a9e752abfdc4 Mon Sep 17 00:00:00 2001 From: Anurag Kumar Date: Tue, 31 Mar 2026 12:18:39 +0530 Subject: [PATCH 02/17] usage refactoring --- specs/custommenuspellcheck.md | 403 ++++++++++++++++++++-------------- 1 file changed, 239 insertions(+), 164 deletions(-) diff --git a/specs/custommenuspellcheck.md b/specs/custommenuspellcheck.md index 252234c2..0f7fcc62 100644 --- a/specs/custommenuspellcheck.md +++ b/specs/custommenuspellcheck.md @@ -26,235 +26,310 @@ are unaffected. ## Win32 C++ ```cpp +static constexpr INT32 kSuggestionBase = 50000; +static constexpr UINT32 kMaxSuggestions = 5; + +// Shared state between the menu builder and the async handler +// that updates the live popup menu in-place. +struct SpellCheckMenuState +{ + HMENU hPopupMenu = nullptr; + std::vector suggestionLabels; + bool suggestionsApplied = false; +}; + void ShowCustomContextMenuWithSpellCheck( + ICoreWebView2* webview, + ICoreWebView2Controller* controller, ICoreWebView2ContextMenuRequestedEventArgs* args) { wil::com_ptr target; CHECK_FAILURE(args->get_ContextMenuTarget(&target)); BOOL isEditable = FALSE; - CHECK_FAILURE(target->get_IsEditable(&isEditable)); + target->get_IsEditable(&isEditable); + + if (!isEditable) + return; + + // Suppress the default context menu; the host will render its own. + CHECK_FAILURE(args->put_Handled(TRUE)); + + // Take a deferral so the runtime keeps the event args alive + // while the popup menu is open. + wil::com_ptr deferral; + CHECK_FAILURE(args->GetDeferral(&deferral)); + + auto spState = std::make_shared(); + spState->hPopupMenu = CreatePopupMenu(); + HMENU hPopupMenu = spState->hPopupMenu; + + auto target2 = target.try_query(); + auto args2 = args.try_query(); - HMENU hMenu = CreatePopupMenu(); - UINT menuIndex = 0; + // --- Query spellcheck state --- + COREWEBVIEW2_SPELL_CHECK_READINESS spellState = + COREWEBVIEW2_SPELL_CHECK_READINESS_NOT_AVAILABLE; + bool hasMisspelled = false; - if (isEditable) + if (target2 && args2) { - auto target2 = target.try_query(); - auto args2 = wil::com_ptr_query(args); + LPWSTR misspelledRaw = nullptr; + wil::com_ptr suggestions; + HRESULT hr = target2->GetSpellCheckInfo( + &misspelledRaw, &spellState, &suggestions); - if (target2 && args2) - { - wil::unique_cotaskmem_string misspelledWord; - COREWEBVIEW2_SPELL_CHECK_READINESS spellState; - wil::com_ptr suggestions; - CHECK_FAILURE(target2->GetSpellCheckInfo( - &misspelledWord, &spellState, &suggestions)); + hasMisspelled = SUCCEEDED(hr) && misspelledRaw && misspelledRaw[0]; + if (hasMisspelled) + { if (spellState == COREWEBVIEW2_SPELL_CHECK_READINESS_READY) { - // Suggestions are available — add them to the menu. + // Suggestions available — add them directly. UINT32 count = 0; - suggestions->get_Count(&count); - for (UINT32 i = 0; i < count && i < 5; i++) + if (suggestions) + suggestions->get_Count(&count); + for (UINT32 i = 0; i < count && i < kMaxSuggestions; i++) { - wil::unique_cotaskmem_string suggestion; - suggestions->GetValueAtIndex(i, &suggestion); - MENUITEMINFO mii = {}; - mii.cbSize = sizeof(mii); - mii.fMask = MIIM_STRING | MIIM_ID; - mii.wID = IDM_SPELL_SUGGESTION_BASE + i; - mii.dwTypeData = suggestion.get(); - InsertMenuItem(hMenu, menuIndex++, TRUE, &mii); + LPWSTR sugRaw = nullptr; + suggestions->GetValueAtIndex(i, &sugRaw); + if (sugRaw && sugRaw[0]) + { + spState->suggestionLabels.push_back(sugRaw); + AppendMenu(hPopupMenu, MF_STRING, + kSuggestionBase + i, sugRaw); + } + if (sugRaw) + CoTaskMemFree(sugRaw); } + spState->suggestionsApplied = true; } else if (spellState == COREWEBVIEW2_SPELL_CHECK_READINESS_NOT_READY) { - // Suggestions pending — show placeholder and register async handler. - MENUITEMINFO mii = {}; - mii.cbSize = sizeof(mii); - mii.fMask = MIIM_STRING | MIIM_ID | MIIM_STATE; - mii.fState = MFS_DISABLED; - mii.wID = IDM_SPELL_PLACEHOLDER; - mii.dwTypeData = const_cast(L"Loading suggestions..."); - InsertMenuItem(hMenu, menuIndex++, TRUE, &mii); - - // Handler fires when suggestions arrive (or immediately if - // already resolved). During TrackPopupMenu's modal loop, - // the handler updates the menu in-place. - CHECK_FAILURE(args2->add_SpellCheckSuggestionsReady( - Callback( - [hMenu, target2, args2]( - ICoreWebView2ContextMenuRequestedEventArgs* sender, - IUnknown* eventArgs) -> HRESULT - { - wil::unique_cotaskmem_string word; - COREWEBVIEW2_SPELL_CHECK_READINESS state; - wil::com_ptr suggs; - CHECK_FAILURE(target2->GetSpellCheckInfo( - &word, &state, &suggs)); + // Suggestions pending — show placeholder. + AppendMenu(hPopupMenu, MF_GRAYED | MF_STRING, + kSuggestionBase, L"Loading suggestions..."); + } + } + if (misspelledRaw) + CoTaskMemFree(misspelledRaw); + } - if (state == COREWEBVIEW2_SPELL_CHECK_READINESS_READY) + // --- Register async handler for in-place update if NOT_READY --- + if (spellState == COREWEBVIEW2_SPELL_CHECK_READINESS_NOT_READY + && hasMisspelled && args2 && target2) + { + EventRegistrationToken token; + args2->add_SpellCheckSuggestionsReady( + Callback( + [spState, target2]( + ICoreWebView2ContextMenuRequestedEventArgs*, + IUnknown*) -> HRESULT + { + if (spState->suggestionsApplied || !spState->hPopupMenu) + return S_OK; + + // Re-query — should be READY now. + LPWSTR w = nullptr; + COREWEBVIEW2_SPELL_CHECK_READINESS st; + wil::com_ptr sc; + target2->GetSpellCheckInfo(&w, &st, &sc); + if (w) + CoTaskMemFree(w); + + UINT32 count = 0; + if (sc) + sc->get_Count(&count); + + for (UINT32 i = 0; i < count && i < kMaxSuggestions; i++) + { + LPWSTR sr = nullptr; + sc->GetValueAtIndex(i, &sr); + if (sr && sr[0]) + { + spState->suggestionLabels.push_back(sr); + if (i == 0) { - UINT32 count = 0; - suggs->get_Count(&count); - if (count > 0) - { - // Replace placeholder with first suggestion. - wil::unique_cotaskmem_string first; - suggs->GetValueAtIndex(0, &first); - MENUITEMINFO mii = {}; - mii.cbSize = sizeof(mii); - mii.fMask = MIIM_STRING | MIIM_ID | MIIM_STATE; - mii.fState = MFS_ENABLED; - mii.wID = IDM_SPELL_SUGGESTION_BASE; - mii.dwTypeData = first.get(); - SetMenuItemInfo( - hMenu, IDM_SPELL_PLACEHOLDER, FALSE, &mii); - - // Insert remaining suggestions. - for (UINT32 i = 1; i < count && i < 5; i++) - { - wil::unique_cotaskmem_string s; - suggs->GetValueAtIndex(i, &s); - MENUITEMINFO item = {}; - item.cbSize = sizeof(item); - item.fMask = MIIM_STRING | MIIM_ID; - item.wID = IDM_SPELL_SUGGESTION_BASE + i; - item.dwTypeData = s.get(); - InsertMenuItem(hMenu, i, TRUE, &item); - } - } - - // Repaint the popup menu. - HWND hPopup = FindWindow(L"#32768", nullptr); - if (hPopup) - { - RedrawWindow( - hPopup, nullptr, nullptr, - RDW_INVALIDATE | RDW_UPDATENOW | RDW_ERASE); - } + // Replace placeholder with first suggestion. + MENUITEMINFOW mii = {}; + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_STRING | MIIM_STATE | MIIM_ID; + mii.fState = MFS_ENABLED; + mii.wID = kSuggestionBase; + mii.dwTypeData = sr; + SetMenuItemInfoW(spState->hPopupMenu, + kSuggestionBase, FALSE, &mii); } - return S_OK; - }) - .Get(), - &m_spellCheckToken)); - } - } + else + { + // Insert additional suggestions. + MENUITEMINFOW mii = {}; + mii.cbSize = sizeof(mii); + mii.fMask = + MIIM_STRING | MIIM_STATE | MIIM_ID | MIIM_FTYPE; + mii.fType = MFT_STRING; + mii.fState = MFS_ENABLED; + mii.wID = kSuggestionBase + i; + mii.dwTypeData = sr; + InsertMenuItemW(spState->hPopupMenu, + i, TRUE, &mii); + } + } + if (sr) + CoTaskMemFree(sr); + } + spState->suggestionsApplied = true; + + // Force redraw of the live popup menu. + HWND hMenuWnd = FindWindowW(L"#32768", nullptr); + if (hMenuWnd) + { + RedrawWindow(hMenuWnd, nullptr, nullptr, + RDW_INVALIDATE | RDW_ERASE | + RDW_FRAME | RDW_ALLCHILDREN); + } + return S_OK; + }) + .Get(), + &token); } - // Add standard WebView2 context menu items. + // --- Add standard WebView2 context menu items --- wil::com_ptr items; CHECK_FAILURE(args->get_MenuItems(&items)); UINT32 itemCount; CHECK_FAILURE(items->get_Count(&itemCount)); for (UINT32 i = 0; i < itemCount; i++) { - wil::com_ptr item; - CHECK_FAILURE(items->GetValueAtIndex(i, &item)); - // ... add each item to hMenu ... + wil::com_ptr current; + items->GetValueAtIndex(i, ¤t); + // ... add each item to hPopupMenu ... } - // Show the menu. - UINT selectedId = TrackPopupMenu( - hMenu, TPM_RETURNCMD, pt.x, pt.y, 0, m_hWnd, nullptr); + // --- Show popup menu (blocks, but pumps messages so the + // SpellCheckSuggestionsReady callback can fire during this) --- + HWND hWnd; + controller->get_ParentWindow(&hWnd); + POINT location; + args->get_Location(&location); + // Adjust location for WebView bounds and DPI scale as needed. - // Handle selection. - if (selectedId >= IDM_SPELL_SUGGESTION_BASE && - selectedId < IDM_SPELL_SUGGESTION_BASE + 5) + INT32 selectedCmd = TrackPopupMenu( + hPopupMenu, TPM_TOPALIGN | TPM_LEFTALIGN | TPM_RETURNCMD, + location.x, location.y, 0, hWnd, nullptr); + + spState->hPopupMenu = nullptr; + + // --- Handle selection --- + if (selectedCmd >= kSuggestionBase && + selectedCmd < kSuggestionBase + (INT32)kMaxSuggestions) { - // Apply the selected spellcheck suggestion. - wil::unique_cotaskmem_string word; - COREWEBVIEW2_SPELL_CHECK_READINESS state; - wil::com_ptr suggs; - target2->GetSpellCheckInfo(&word, &state, &suggs); - wil::unique_cotaskmem_string chosen; - suggs->GetValueAtIndex(selectedId - IDM_SPELL_SUGGESTION_BASE, &chosen); - args2->ApplySpellCheckSuggestion(chosen.get()); + UINT32 idx = selectedCmd - kSuggestionBase; + if (idx < spState->suggestionLabels.size() && args2) + { + args2->ApplySpellCheckSuggestion( + spState->suggestionLabels[idx].c_str()); + } + } + else if (selectedCmd > 0) + { + args->put_SelectedCommandId(selectedCmd); } - args->put_Handled(TRUE); - DestroyMenu(hMenu); + DestroyMenu(hPopupMenu); + deferral->Complete(); } ``` ## C#/.NET ```csharp -void ShowCustomContextMenuWithSpellCheck( +async void ShowCustomContextMenuWithSpellCheck( object sender, CoreWebView2ContextMenuRequestedEventArgs args) { var target = args.ContextMenuTarget; - var menuItems = new List(); - if (target.IsEditable) + if (!target.IsEditable) + return; + + // Suppress the default context menu. + args.Handled = true; + + // Take a deferral so the runtime keeps the event args alive. + var deferral = args.GetDeferral(); + + try { string misspelledWord = target.MisspelledWord; CoreWebView2SpellCheckReadiness spellState = target.SpellCheckReadiness; IReadOnlyList suggestions = target.SpellCheckSuggestions; - if (spellState == CoreWebView2SpellCheckReadiness.Ready && suggestions.Count > 0) + var contextMenu = new ContextMenuStrip(); + + if (!string.IsNullOrEmpty(misspelledWord)) { - // Suggestions available — add them directly. - foreach (string suggestion in suggestions.Take(5)) + if (spellState == CoreWebView2SpellCheckReadiness.Ready + && suggestions.Count > 0) { - var item = new ToolStripMenuItem(suggestion); - item.Click += (s, e) => + // Suggestions available — add them directly. + foreach (string suggestion in suggestions.Take(5)) { - args.ApplySpellCheckSuggestion(suggestion); - }; - menuItems.Add(item); + var item = new ToolStripMenuItem(suggestion); + item.Click += (s, e) => + { + args.ApplySpellCheckSuggestion(suggestion); + }; + contextMenu.Items.Add(item); + } + contextMenu.Items.Add(new ToolStripSeparator()); } - menuItems.Add(new ToolStripSeparator()); - } - else if (spellState == CoreWebView2SpellCheckReadiness.NotReady) - { - // Suggestions pending — show placeholder. - var placeholder = new ToolStripMenuItem("Loading suggestions...") - { - Enabled = false - }; - menuItems.Add(placeholder); - menuItems.Add(new ToolStripSeparator()); - - // Register async handler. Fires when suggestions resolve - // or immediately if already resolved. - args.SpellCheckSuggestionsReady += (s, e) => + else if (spellState == CoreWebView2SpellCheckReadiness.NotReady) { - string word = target.MisspelledWord; - var readySuggestions = target.SpellCheckSuggestions; - if (target.SpellCheckReadiness == CoreWebView2SpellCheckReadiness.Ready - && readySuggestions.Count > 0) + // Suggestions pending — show placeholder. + var placeholder = new ToolStripMenuItem("Loading suggestions...") { - // Replace placeholder with actual suggestions. - int index = menuItems.IndexOf(placeholder); - menuItems.Remove(placeholder); - foreach (string suggestion in readySuggestions.Take(5)) + Enabled = false + }; + contextMenu.Items.Insert(0, placeholder); + contextMenu.Items.Insert(1, new ToolStripSeparator()); + + // Handler fires when suggestions resolve + // or immediately if already resolved. + args.SpellCheckSuggestionsReady += (s, e) => + { + var readySuggestions = target.SpellCheckSuggestions; + if (target.SpellCheckReadiness == + CoreWebView2SpellCheckReadiness.Ready + && readySuggestions.Count > 0) { - var item = new ToolStripMenuItem(suggestion); - item.Click += (s2, e2) => + int index = contextMenu.Items.IndexOf(placeholder); + contextMenu.Items.Remove(placeholder); + foreach (string suggestion in readySuggestions.Take(5)) { - args.ApplySpellCheckSuggestion(suggestion); - }; - menuItems.Insert(index++, item); + var item = new ToolStripMenuItem(suggestion); + item.Click += (s2, e2) => + { + args.ApplySpellCheckSuggestion(suggestion); + }; + contextMenu.Items.Insert(index++, item); + } } - } - }; + }; + } } - } - // Add standard WebView2 context menu items. - foreach (var menuItem in args.MenuItems) + // Add standard WebView2 context menu items. + foreach (var menuItem in args.MenuItems) + { + // ... add each item to contextMenu ... + } + + contextMenu.Show(webView, webView.PointToClient(Cursor.Position)); + } + finally { - // ... add each item to menuItems ... + deferral.Complete(); } - - // Show the context menu. - var contextMenu = new ContextMenuStrip(); - contextMenu.Items.AddRange(menuItems.ToArray()); - contextMenu.Show(webView, webView.PointToClient(Cursor.Position)); - - args.Handled = true; } ``` From db3dacbf64d2f967eb392c28effcc3606e76bb35 Mon Sep 17 00:00:00 2001 From: Anurag Kumar Date: Tue, 31 Mar 2026 12:20:07 +0530 Subject: [PATCH 03/17] rename md file --- specs/{custommenuspellcheck.md => CustomContextMenuSpellcheck.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename specs/{custommenuspellcheck.md => CustomContextMenuSpellcheck.md} (100%) diff --git a/specs/custommenuspellcheck.md b/specs/CustomContextMenuSpellcheck.md similarity index 100% rename from specs/custommenuspellcheck.md rename to specs/CustomContextMenuSpellcheck.md From c7d73dbe6722e0f1f650dc4cbc6016844e97335a Mon Sep 17 00:00:00 2001 From: Anurag Kumar Date: Wed, 1 Apr 2026 09:39:02 +0530 Subject: [PATCH 04/17] remove misspelled word, split propget on context menu info --- specs/CustomContextMenuSpellcheck.md | 219 ++++++++++++++------------- 1 file changed, 113 insertions(+), 106 deletions(-) diff --git a/specs/CustomContextMenuSpellcheck.md b/specs/CustomContextMenuSpellcheck.md index 0f7fcc62..c710de15 100644 --- a/specs/CustomContextMenuSpellcheck.md +++ b/specs/CustomContextMenuSpellcheck.md @@ -13,7 +13,7 @@ these suggestions. We propose extending the existing `ContextMenuRequested` API surface with spellcheck support for custom context menus. This adds the ability to: -1. Query spellcheck information (misspelled word, readiness state, suggestions) from the context menu target. +1. Query spellcheck suggestion readiness state and suggestions from the context menu target. 2. Subscribe to an asynchronous notification when spellcheck suggestions become available. 3. Apply a selected spellcheck suggestion to replace the misspelled word in the DOM. @@ -47,7 +47,7 @@ void ShowCustomContextMenuWithSpellCheck( CHECK_FAILURE(args->get_ContextMenuTarget(&target)); BOOL isEditable = FALSE; - target->get_IsEditable(&isEditable); + CHECK_FAILURE(target->get_IsEditable(&isEditable)); if (!isEditable) return; @@ -70,54 +70,49 @@ void ShowCustomContextMenuWithSpellCheck( // --- Query spellcheck state --- COREWEBVIEW2_SPELL_CHECK_READINESS spellState = COREWEBVIEW2_SPELL_CHECK_READINESS_NOT_AVAILABLE; - bool hasMisspelled = false; if (target2 && args2) { - LPWSTR misspelledRaw = nullptr; - wil::com_ptr suggestions; - HRESULT hr = target2->GetSpellCheckInfo( - &misspelledRaw, &spellState, &suggestions); + COREWEBVIEW2_SPELL_CHECK_READINESS readiness; + HRESULT hr = target2->get_SpellCheckReadiness(&readiness); - hasMisspelled = SUCCEEDED(hr) && misspelledRaw && misspelledRaw[0]; + if (SUCCEEDED(hr)) + spellState = readiness; - if (hasMisspelled) + if (spellState == COREWEBVIEW2_SPELL_CHECK_READINESS_READY) { - if (spellState == COREWEBVIEW2_SPELL_CHECK_READINESS_READY) + // Suggestions available — add them directly. + wil::com_ptr suggestions; + target2->get_SpellCheckSuggestions(&suggestions); + UINT32 count = 0; + if (suggestions) + suggestions->get_Count(&count); + for (UINT32 i = 0; i < count && i < kMaxSuggestions; i++) { - // Suggestions available — add them directly. - UINT32 count = 0; - if (suggestions) - suggestions->get_Count(&count); - for (UINT32 i = 0; i < count && i < kMaxSuggestions; i++) + LPWSTR sugRaw = nullptr; + suggestions->GetValueAtIndex(i, &sugRaw); + if (sugRaw && sugRaw[0]) { - LPWSTR sugRaw = nullptr; - suggestions->GetValueAtIndex(i, &sugRaw); - if (sugRaw && sugRaw[0]) - { - spState->suggestionLabels.push_back(sugRaw); - AppendMenu(hPopupMenu, MF_STRING, - kSuggestionBase + i, sugRaw); - } - if (sugRaw) - CoTaskMemFree(sugRaw); + spState->suggestionLabels.push_back(sugRaw); + AppendMenu(hPopupMenu, MF_STRING, + kSuggestionBase + i, sugRaw); } - spState->suggestionsApplied = true; - } - else if (spellState == COREWEBVIEW2_SPELL_CHECK_READINESS_NOT_READY) - { - // Suggestions pending — show placeholder. - AppendMenu(hPopupMenu, MF_GRAYED | MF_STRING, - kSuggestionBase, L"Loading suggestions..."); + if (sugRaw) + CoTaskMemFree(sugRaw); } + spState->suggestionsApplied = true; + } + else if (spellState == COREWEBVIEW2_SPELL_CHECK_READINESS_NOT_READY) + { + // Suggestions pending — show placeholder. + AppendMenu(hPopupMenu, MF_GRAYED | MF_STRING, + kSuggestionBase, L"Loading suggestions..."); } - if (misspelledRaw) - CoTaskMemFree(misspelledRaw); } // --- Register async handler for in-place update if NOT_READY --- if (spellState == COREWEBVIEW2_SPELL_CHECK_READINESS_NOT_READY - && hasMisspelled && args2 && target2) + && args2 && target2) { EventRegistrationToken token; args2->add_SpellCheckSuggestionsReady( @@ -130,12 +125,11 @@ void ShowCustomContextMenuWithSpellCheck( return S_OK; // Re-query — should be READY now. - LPWSTR w = nullptr; COREWEBVIEW2_SPELL_CHECK_READINESS st; + target2->get_SpellCheckReadiness(&st); + wil::com_ptr sc; - target2->GetSpellCheckInfo(&w, &st, &sc); - if (w) - CoTaskMemFree(w); + target2->get_SpellCheckSuggestions(&sc); UINT32 count = 0; if (sc) @@ -210,13 +204,31 @@ void ShowCustomContextMenuWithSpellCheck( // SpellCheckSuggestionsReady callback can fire during this) --- HWND hWnd; controller->get_ParentWindow(&hWnd); + SetForegroundWindow(hWnd); + + // Convert WebView-relative coordinates to screen coordinates. + RECT bounds; + controller->get_Bounds(&bounds); + RECT clientRect; + GetClientRect(hWnd, &clientRect); + POINT topLeft = {clientRect.left, clientRect.top}; + ClientToScreen(hWnd, &topLeft); + POINT location; args->get_Location(&location); - // Adjust location for WebView bounds and DPI scale as needed. + + // Account for DPI scaling. + double scale = 1.0; + wil::com_ptr ctrl3; + if (SUCCEEDED(controller->QueryInterface(IID_PPV_ARGS(&ctrl3)))) + ctrl3->get_RasterizationScale(&scale); + + int screenX = bounds.left + topLeft.x + static_cast(location.x * scale); + int screenY = bounds.top + topLeft.y + static_cast(location.y * scale); INT32 selectedCmd = TrackPopupMenu( hPopupMenu, TPM_TOPALIGN | TPM_LEFTALIGN | TPM_RETURNCMD, - location.x, location.y, 0, hWnd, nullptr); + screenX, screenY, 0, hWnd, nullptr); spState->hPopupMenu = nullptr; @@ -237,14 +249,14 @@ void ShowCustomContextMenuWithSpellCheck( } DestroyMenu(hPopupMenu); - deferral->Complete(); + CHECK_FAILURE(deferral->Complete()); } ``` -## C#/.NET +## .NET/WinRT ```csharp -async void ShowCustomContextMenuWithSpellCheck( +void ShowCustomContextMenuWithSpellCheck( object sender, CoreWebView2ContextMenuRequestedEventArgs args) { var target = args.ContextMenuTarget; @@ -260,62 +272,58 @@ async void ShowCustomContextMenuWithSpellCheck( try { - string misspelledWord = target.MisspelledWord; CoreWebView2SpellCheckReadiness spellState = target.SpellCheckReadiness; IReadOnlyList suggestions = target.SpellCheckSuggestions; var contextMenu = new ContextMenuStrip(); - if (!string.IsNullOrEmpty(misspelledWord)) + if (spellState == CoreWebView2SpellCheckReadiness.Ready + && suggestions.Count > 0) { - if (spellState == CoreWebView2SpellCheckReadiness.Ready - && suggestions.Count > 0) + // Suggestions available — add them directly. + foreach (string suggestion in suggestions.Take(5)) { - // Suggestions available — add them directly. - foreach (string suggestion in suggestions.Take(5)) + var item = new ToolStripMenuItem(suggestion); + item.Click += (s, e) => { - var item = new ToolStripMenuItem(suggestion); - item.Click += (s, e) => - { - args.ApplySpellCheckSuggestion(suggestion); - }; - contextMenu.Items.Add(item); - } - contextMenu.Items.Add(new ToolStripSeparator()); + args.ApplySpellCheckSuggestion(suggestion); + }; + contextMenu.Items.Add(item); } - else if (spellState == CoreWebView2SpellCheckReadiness.NotReady) + contextMenu.Items.Add(new ToolStripSeparator()); + } + else if (spellState == CoreWebView2SpellCheckReadiness.NotReady) + { + // Suggestions pending — show placeholder. + var placeholder = new ToolStripMenuItem("Loading suggestions...") { - // Suggestions pending — show placeholder. - var placeholder = new ToolStripMenuItem("Loading suggestions...") - { - Enabled = false - }; - contextMenu.Items.Insert(0, placeholder); - contextMenu.Items.Insert(1, new ToolStripSeparator()); - - // Handler fires when suggestions resolve - // or immediately if already resolved. - args.SpellCheckSuggestionsReady += (s, e) => + Enabled = false + }; + contextMenu.Items.Insert(0, placeholder); + contextMenu.Items.Insert(1, new ToolStripSeparator()); + + // Handler fires when suggestions resolve + // or immediately if already resolved. + args.SpellCheckSuggestionsReady += (s, e) => + { + var readySuggestions = target.SpellCheckSuggestions; + if (target.SpellCheckReadiness == + CoreWebView2SpellCheckReadiness.Ready + && readySuggestions.Count > 0) { - var readySuggestions = target.SpellCheckSuggestions; - if (target.SpellCheckReadiness == - CoreWebView2SpellCheckReadiness.Ready - && readySuggestions.Count > 0) + int index = contextMenu.Items.IndexOf(placeholder); + contextMenu.Items.Remove(placeholder); + foreach (string suggestion in readySuggestions.Take(5)) { - int index = contextMenu.Items.IndexOf(placeholder); - contextMenu.Items.Remove(placeholder); - foreach (string suggestion in readySuggestions.Take(5)) + var item = new ToolStripMenuItem(suggestion); + item.Click += (s2, e2) => { - var item = new ToolStripMenuItem(suggestion); - item.Click += (s2, e2) => - { - args.ApplySpellCheckSuggestion(suggestion); - }; - contextMenu.Items.Insert(index++, item); - } + args.ApplySpellCheckSuggestion(suggestion); + }; + contextMenu.Items.Insert(index++, item); } - }; - } + } + }; } // Add standard WebView2 context menu items. @@ -367,28 +375,28 @@ interface ICoreWebView2SpellCheckSuggestionsReadyEventHandler : IUnknown { /// Extends ICoreWebView2ContextMenuTarget with spellcheck information /// for custom context menu integration. Allows host applications to retrieve -/// spellcheck suggestions and metadata when rendering custom menus -/// on editable fields. +/// spellcheck suggestions when rendering custom menus on editable fields. [uuid(d3f7e01a-9b5c-4e8f-a1d2-7c6b3e4f5a80), object, pointer_default(unique)] interface ICoreWebView2ContextMenuTarget2 : ICoreWebView2ContextMenuTarget { - /// Gets spellcheck information for the current context menu target in a - /// single call. Returns the misspelled word (empty if none), the readiness - /// state of suggestions, and a collection of suggestion strings. - /// - /// The caller must free `misspelledWord` with `CoTaskMemFree`. + /// Gets the readiness state of spellcheck suggestions for the current + /// context menu target. /// - /// When `state` is `COREWEBVIEW2_SPELL_CHECK_READINESS_READY`, the - /// `suggestions` collection is populated with correction strings. - /// When `state` is `COREWEBVIEW2_SPELL_CHECK_READINESS_NOT_READY`, the + /// When the value is `COREWEBVIEW2_SPELL_CHECK_READINESS_READY`, the + /// `SpellCheckSuggestions` collection is populated with correction strings. + /// When the value is `COREWEBVIEW2_SPELL_CHECK_READINESS_NOT_READY`, the /// collection is empty; subscribe to `SpellCheckSuggestionsReady` on /// `ICoreWebView2ContextMenuRequestedEventArgs2` to be notified when - /// suggestions become available, then call this method again. - /// When `state` is `COREWEBVIEW2_SPELL_CHECK_READINESS_NOT_AVAILABLE` or + /// suggestions become available. + /// When the value is `COREWEBVIEW2_SPELL_CHECK_READINESS_NOT_AVAILABLE` or /// `COREWEBVIEW2_SPELL_CHECK_READINESS_ERROR`, no suggestions will arrive. - HRESULT GetSpellCheckInfo( - [out] LPWSTR* misspelledWord, - [out] COREWEBVIEW2_SPELL_CHECK_READINESS* state, - [out, retval] ICoreWebView2StringCollection** suggestions); + [propget] HRESULT SpellCheckReadiness( + [out, retval] COREWEBVIEW2_SPELL_CHECK_READINESS* value); + + /// Gets the collection of spellcheck suggestion strings for the misspelled + /// word at the current context menu target. The collection is empty when + /// `SpellCheckReadiness` is not `READY`. + [propget] HRESULT SpellCheckSuggestions( + [out, retval] ICoreWebView2StringCollection** value); } /// Extends ICoreWebView2ContextMenuRequestedEventArgs with methods to apply @@ -398,8 +406,8 @@ interface ICoreWebView2ContextMenuRequestedEventArgs2 : ICoreWebView2ContextMenuRequestedEventArgs { /// Applies the selected spellcheck suggestion by replacing the misspelled /// word in the currently focused editable field. The `suggestion` parameter - /// must be one of the strings obtained from the `suggestions` collection - /// returned by `ICoreWebView2ContextMenuTarget2::GetSpellCheckInfo`. + /// must be one of the strings obtained from the `SpellCheckSuggestions` + /// collection on `ICoreWebView2ContextMenuTarget2`. /// The runtime handles all editing internally, including routing to the /// correct frame for nested iframes. HRESULT ApplySpellCheckSuggestion([in] LPCWSTR suggestion); @@ -419,7 +427,7 @@ interface ICoreWebView2ContextMenuRequestedEventArgs2 } ``` -## .NET/C# +## .NET/WinRT ```csharp namespace Microsoft.Web.WebView2.Core @@ -436,7 +444,6 @@ namespace Microsoft.Web.WebView2.Core { [interface_name("Microsoft.Web.WebView2.Core.ICoreWebView2ContextMenuTarget2")] { - String MisspelledWord { get; }; CoreWebView2SpellCheckReadiness SpellCheckReadiness { get; }; IVectorView SpellCheckSuggestions { get; }; } From d2397e0f93a67769664ad671ae156ed3516da243 Mon Sep 17 00:00:00 2001 From: Anurag Kumar Date: Wed, 1 Apr 2026 12:00:35 +0530 Subject: [PATCH 05/17] use a one shot callback handler instead of event registration --- specs/CustomContextMenuSpellcheck.md | 84 +++++++++++----------------- 1 file changed, 34 insertions(+), 50 deletions(-) diff --git a/specs/CustomContextMenuSpellcheck.md b/specs/CustomContextMenuSpellcheck.md index c710de15..dc331587 100644 --- a/specs/CustomContextMenuSpellcheck.md +++ b/specs/CustomContextMenuSpellcheck.md @@ -14,7 +14,7 @@ We propose extending the existing `ContextMenuRequested` API surface with spellc context menus. This adds the ability to: 1. Query spellcheck suggestion readiness state and suggestions from the context menu target. -2. Subscribe to an asynchronous notification when spellcheck suggestions become available. +2. Request asynchronous delivery of spellcheck suggestions via a one-shot completion handler. 3. Apply a selected spellcheck suggestion to replace the misspelled word in the DOM. Hosts opt in by calling `QueryInterface` for the new `ICoreWebView2ContextMenuTarget2` and @@ -110,35 +110,29 @@ void ShowCustomContextMenuWithSpellCheck( } } - // --- Register async handler for in-place update if NOT_READY --- + // --- Register async completion handler for in-place update if NOT_READY --- if (spellState == COREWEBVIEW2_SPELL_CHECK_READINESS_NOT_READY && args2 && target2) { - EventRegistrationToken token; - args2->add_SpellCheckSuggestionsReady( - Callback( - [spState, target2]( - ICoreWebView2ContextMenuRequestedEventArgs*, - IUnknown*) -> HRESULT + args2->GetSpellCheckSuggestionsAsync( + Callback( + [spState]( + HRESULT errorCode, + ICoreWebView2StringCollection* suggestions) -> HRESULT { if (spState->suggestionsApplied || !spState->hPopupMenu) return S_OK; - // Re-query — should be READY now. - COREWEBVIEW2_SPELL_CHECK_READINESS st; - target2->get_SpellCheckReadiness(&st); - - wil::com_ptr sc; - target2->get_SpellCheckSuggestions(&sc); + if (FAILED(errorCode) || !suggestions) + return S_OK; UINT32 count = 0; - if (sc) - sc->get_Count(&count); + suggestions->get_Count(&count); for (UINT32 i = 0; i < count && i < kMaxSuggestions; i++) { LPWSTR sr = nullptr; - sc->GetValueAtIndex(i, &sr); + suggestions->GetValueAtIndex(i, &sr); if (sr && sr[0]) { spState->suggestionLabels.push_back(sr); @@ -184,8 +178,7 @@ void ShowCustomContextMenuWithSpellCheck( } return S_OK; }) - .Get(), - &token); + .Get()); } // --- Add standard WebView2 context menu items --- @@ -201,7 +194,7 @@ void ShowCustomContextMenuWithSpellCheck( } // --- Show popup menu (blocks, but pumps messages so the - // SpellCheckSuggestionsReady callback can fire during this) --- + // GetSpellCheckSuggestionsAsync callback can fire during this) --- HWND hWnd; controller->get_ParentWindow(&hWnd); SetForegroundWindow(hWnd); @@ -302,14 +295,11 @@ void ShowCustomContextMenuWithSpellCheck( contextMenu.Items.Insert(0, placeholder); contextMenu.Items.Insert(1, new ToolStripSeparator()); - // Handler fires when suggestions resolve + // One-shot completion handler fires when suggestions resolve // or immediately if already resolved. - args.SpellCheckSuggestionsReady += (s, e) => + args.GetSpellCheckSuggestionsAsync((errorCode, readySuggestions) => { - var readySuggestions = target.SpellCheckSuggestions; - if (target.SpellCheckReadiness == - CoreWebView2SpellCheckReadiness.Ready - && readySuggestions.Count > 0) + if (readySuggestions != null && readySuggestions.Count > 0) { int index = contextMenu.Items.IndexOf(placeholder); contextMenu.Items.Remove(placeholder); @@ -323,7 +313,7 @@ void ShowCustomContextMenuWithSpellCheck( contextMenu.Items.Insert(index++, item); } } - }; + }); } // Add standard WebView2 context menu items. @@ -363,14 +353,13 @@ typedef enum COREWEBVIEW2_SPELL_CHECK_READINESS { COREWEBVIEW2_SPELL_CHECK_READINESS_ERROR, } COREWEBVIEW2_SPELL_CHECK_READINESS; -/// Receives `SpellCheckSuggestionsReady` events from -/// ICoreWebView2ContextMenuRequestedEventArgs2. +/// Receives the result of `GetSpellCheckSuggestionsAsync`. [uuid(c5d6e7f8-9a0b-1c2d-3e4f-5a6b7c8d9e0f), object, pointer_default(unique)] -interface ICoreWebView2SpellCheckSuggestionsReadyEventHandler : IUnknown { - /// Provides the event args for the corresponding event. +interface ICoreWebView2GetSpellCheckSuggestionsCompletedHandler : IUnknown { + /// Provides the result of the corresponding asynchronous method. HRESULT Invoke( - [in] ICoreWebView2ContextMenuRequestedEventArgs* sender, - [in] IUnknown* args); + [in] HRESULT errorCode, + [in] ICoreWebView2StringCollection* suggestions); } /// Extends ICoreWebView2ContextMenuTarget with spellcheck information @@ -384,7 +373,7 @@ interface ICoreWebView2ContextMenuTarget2 : ICoreWebView2ContextMenuTarget { /// When the value is `COREWEBVIEW2_SPELL_CHECK_READINESS_READY`, the /// `SpellCheckSuggestions` collection is populated with correction strings. /// When the value is `COREWEBVIEW2_SPELL_CHECK_READINESS_NOT_READY`, the - /// collection is empty; subscribe to `SpellCheckSuggestionsReady` on + /// collection is empty; call `GetSpellCheckSuggestionsAsync` on /// `ICoreWebView2ContextMenuRequestedEventArgs2` to be notified when /// suggestions become available. /// When the value is `COREWEBVIEW2_SPELL_CHECK_READINESS_NOT_AVAILABLE` or @@ -400,7 +389,7 @@ interface ICoreWebView2ContextMenuTarget2 : ICoreWebView2ContextMenuTarget { } /// Extends ICoreWebView2ContextMenuRequestedEventArgs with methods to apply -/// spellcheck corrections and subscribe to asynchronous suggestion delivery. +/// spellcheck corrections and asynchronously retrieve suggestions. [uuid(e4a8f3b2-6c1d-4e9a-b5f7-2d8c9a0e1b34), object, pointer_default(unique)] interface ICoreWebView2ContextMenuRequestedEventArgs2 : ICoreWebView2ContextMenuRequestedEventArgs { @@ -412,18 +401,14 @@ interface ICoreWebView2ContextMenuRequestedEventArgs2 /// correct frame for nested iframes. HRESULT ApplySpellCheckSuggestion([in] LPCWSTR suggestion); - /// Registers an event handler for `SpellCheckSuggestionsReady`. This fires - /// when asynchronous spellcheck suggestions become available. If suggestions - /// are already in `READY` state at registration time, the handler fires - /// immediately and synchronously. - HRESULT add_SpellCheckSuggestionsReady( - [in] ICoreWebView2SpellCheckSuggestionsReadyEventHandler* eventHandler, - [out] EventRegistrationToken* token); - - /// Removes the event handler previously added with - /// `add_SpellCheckSuggestionsReady`. - HRESULT remove_SpellCheckSuggestionsReady( - [in] EventRegistrationToken token); + /// Asynchronously retrieves spellcheck suggestions for the misspelled word + /// at the current context menu target. The `handler` is invoked exactly once + /// when suggestions become available. If suggestions are already in `READY` + /// state, the handler is invoked immediately and synchronously. + /// The handler receives `S_OK` and the suggestions collection on success, + /// or an error HRESULT and `nullptr` on failure. + HRESULT GetSpellCheckSuggestionsAsync( + [in] ICoreWebView2GetSpellCheckSuggestionsCompletedHandler* handler); } ``` @@ -454,9 +439,8 @@ namespace Microsoft.Web.WebView2.Core [interface_name("Microsoft.Web.WebView2.Core.ICoreWebView2ContextMenuRequestedEventArgs2")] { void ApplySpellCheckSuggestion(String suggestion); - event Windows.Foundation.TypedEventHandler< - CoreWebView2ContextMenuRequestedEventArgs, Object> - SpellCheckSuggestionsReady; + Windows.Foundation.IAsyncOperation> + GetSpellCheckSuggestionsAsync(); } } } From 4615e9747fb4c23f1a98722cdd5b820ee8590d12 Mon Sep 17 00:00:00 2001 From: Anurag Kumar Date: Wed, 1 Apr 2026 14:25:52 +0530 Subject: [PATCH 06/17] keep only async approach --- specs/CustomContextMenuSpellcheck.md | 433 +++++---------------------- 1 file changed, 79 insertions(+), 354 deletions(-) diff --git a/specs/CustomContextMenuSpellcheck.md b/specs/CustomContextMenuSpellcheck.md index dc331587..e46378e2 100644 --- a/specs/CustomContextMenuSpellcheck.md +++ b/specs/CustomContextMenuSpellcheck.md @@ -2,333 +2,112 @@ Custom Context Menu SpellCheck === # Background - When a host application renders a custom context menu via the `ContextMenuRequested` event, spellcheck suggestions for misspelled words are not available. The browser's built-in spellcheck pipeline resolves suggestions asynchronously, but there is no mechanism for custom context menu hosts to retrieve or apply these suggestions. # Description - We propose extending the existing `ContextMenuRequested` API surface with spellcheck support for custom context menus. This adds the ability to: -1. Query spellcheck suggestion readiness state and suggestions from the context menu target. -2. Request asynchronous delivery of spellcheck suggestions via a one-shot completion handler. -3. Apply a selected spellcheck suggestion to replace the misspelled word in the DOM. +1. Asynchronously retrieve spellcheck suggestions via a one-shot completion handler. +2. Apply a selected spellcheck suggestion to replace the misspelled word in the DOM. + +The design uses a purely asynchronous approach: the host always calls `GetSpellCheckSuggestionsAsync` +which fires the handler exactly once either immediately (if suggestions are already resolved) or +when they become available. There is no synchronous readiness query. -Hosts opt in by calling `QueryInterface` for the new `ICoreWebView2ContextMenuTarget2` and -`ICoreWebView2ContextMenuRequestedEventArgs2` interfaces. Existing `ContextMenuRequested` consumers -are unaffected. +Hosts opt in by calling `QueryInterface` for the new `ICoreWebView2ContextMenuRequestedEventArgs2` +interface. Existing `ContextMenuRequested` consumers are unaffected. # Examples ## Win32 C++ ```cpp -static constexpr INT32 kSuggestionBase = 50000; -static constexpr UINT32 kMaxSuggestions = 5; - -// Shared state between the menu builder and the async handler -// that updates the live popup menu in-place. -struct SpellCheckMenuState -{ - HMENU hPopupMenu = nullptr; - std::vector suggestionLabels; - bool suggestionsApplied = false; -}; - -void ShowCustomContextMenuWithSpellCheck( - ICoreWebView2* webview, - ICoreWebView2Controller* controller, - ICoreWebView2ContextMenuRequestedEventArgs* args) -{ - wil::com_ptr target; - CHECK_FAILURE(args->get_ContextMenuTarget(&target)); - - BOOL isEditable = FALSE; - CHECK_FAILURE(target->get_IsEditable(&isEditable)); - - if (!isEditable) - return; - - // Suppress the default context menu; the host will render its own. - CHECK_FAILURE(args->put_Handled(TRUE)); +// Inside the ContextMenuRequested handler for an editable target: +args->put_Handled(TRUE); +wil::com_ptr deferral; +args->GetDeferral(&deferral); - // Take a deferral so the runtime keeps the event args alive - // while the popup menu is open. - wil::com_ptr deferral; - CHECK_FAILURE(args->GetDeferral(&deferral)); +HMENU hMenu = CreatePopupMenu(); - auto spState = std::make_shared(); - spState->hPopupMenu = CreatePopupMenu(); - HMENU hPopupMenu = spState->hPopupMenu; +auto args2 = + wil::try_com_query(args); - auto target2 = target.try_query(); - auto args2 = args.try_query(); - - // --- Query spellcheck state --- - COREWEBVIEW2_SPELL_CHECK_READINESS spellState = - COREWEBVIEW2_SPELL_CHECK_READINESS_NOT_AVAILABLE; - - if (target2 && args2) - { - COREWEBVIEW2_SPELL_CHECK_READINESS readiness; - HRESULT hr = target2->get_SpellCheckReadiness(&readiness); - - if (SUCCEEDED(hr)) - spellState = readiness; - - if (spellState == COREWEBVIEW2_SPELL_CHECK_READINESS_READY) - { - // Suggestions available — add them directly. - wil::com_ptr suggestions; - target2->get_SpellCheckSuggestions(&suggestions); - UINT32 count = 0; - if (suggestions) - suggestions->get_Count(&count); - for (UINT32 i = 0; i < count && i < kMaxSuggestions; i++) +if (args2) +{ + // Show placeholder while suggestions load. + AppendMenu(hMenu, MF_GRAYED | MF_STRING, IDM_SUGGESTION_BASE, + L"Loading suggestions..."); + + // Handler fires immediately if resolved, or when ready. + args2->GetSpellCheckSuggestionsAsync( + Callback( + [hMenu, args2](HRESULT errorCode, + ICoreWebView2StringCollection* suggestions) + -> HRESULT { - LPWSTR sugRaw = nullptr; - suggestions->GetValueAtIndex(i, &sugRaw); - if (sugRaw && sugRaw[0]) - { - spState->suggestionLabels.push_back(sugRaw); - AppendMenu(hPopupMenu, MF_STRING, - kSuggestionBase + i, sugRaw); - } - if (sugRaw) - CoTaskMemFree(sugRaw); - } - spState->suggestionsApplied = true; - } - else if (spellState == COREWEBVIEW2_SPELL_CHECK_READINESS_NOT_READY) - { - // Suggestions pending — show placeholder. - AppendMenu(hPopupMenu, MF_GRAYED | MF_STRING, - kSuggestionBase, L"Loading suggestions..."); - } - } - - // --- Register async completion handler for in-place update if NOT_READY --- - if (spellState == COREWEBVIEW2_SPELL_CHECK_READINESS_NOT_READY - && args2 && target2) - { - args2->GetSpellCheckSuggestionsAsync( - Callback( - [spState]( - HRESULT errorCode, - ICoreWebView2StringCollection* suggestions) -> HRESULT - { - if (spState->suggestionsApplied || !spState->hPopupMenu) - return S_OK; - - if (FAILED(errorCode) || !suggestions) - return S_OK; - - UINT32 count = 0; - suggestions->get_Count(&count); - - for (UINT32 i = 0; i < count && i < kMaxSuggestions; i++) - { - LPWSTR sr = nullptr; - suggestions->GetValueAtIndex(i, &sr); - if (sr && sr[0]) - { - spState->suggestionLabels.push_back(sr); - if (i == 0) - { - // Replace placeholder with first suggestion. - MENUITEMINFOW mii = {}; - mii.cbSize = sizeof(mii); - mii.fMask = MIIM_STRING | MIIM_STATE | MIIM_ID; - mii.fState = MFS_ENABLED; - mii.wID = kSuggestionBase; - mii.dwTypeData = sr; - SetMenuItemInfoW(spState->hPopupMenu, - kSuggestionBase, FALSE, &mii); - } - else - { - // Insert additional suggestions. - MENUITEMINFOW mii = {}; - mii.cbSize = sizeof(mii); - mii.fMask = - MIIM_STRING | MIIM_STATE | MIIM_ID | MIIM_FTYPE; - mii.fType = MFT_STRING; - mii.fState = MFS_ENABLED; - mii.wID = kSuggestionBase + i; - mii.dwTypeData = sr; - InsertMenuItemW(spState->hPopupMenu, - i, TRUE, &mii); - } - } - if (sr) - CoTaskMemFree(sr); - } - spState->suggestionsApplied = true; - - // Force redraw of the live popup menu. - HWND hMenuWnd = FindWindowW(L"#32768", nullptr); - if (hMenuWnd) - { - RedrawWindow(hMenuWnd, nullptr, nullptr, - RDW_INVALIDATE | RDW_ERASE | - RDW_FRAME | RDW_ALLCHILDREN); - } + if (FAILED(errorCode) || !suggestions) return S_OK; - }) - .Get()); - } - - // --- Add standard WebView2 context menu items --- - wil::com_ptr items; - CHECK_FAILURE(args->get_MenuItems(&items)); - UINT32 itemCount; - CHECK_FAILURE(items->get_Count(&itemCount)); - for (UINT32 i = 0; i < itemCount; i++) - { - wil::com_ptr current; - items->GetValueAtIndex(i, ¤t); - // ... add each item to hPopupMenu ... - } - - // --- Show popup menu (blocks, but pumps messages so the - // GetSpellCheckSuggestionsAsync callback can fire during this) --- - HWND hWnd; - controller->get_ParentWindow(&hWnd); - SetForegroundWindow(hWnd); - // Convert WebView-relative coordinates to screen coordinates. - RECT bounds; - controller->get_Bounds(&bounds); - RECT clientRect; - GetClientRect(hWnd, &clientRect); - POINT topLeft = {clientRect.left, clientRect.top}; - ClientToScreen(hWnd, &topLeft); - - POINT location; - args->get_Location(&location); - - // Account for DPI scaling. - double scale = 1.0; - wil::com_ptr ctrl3; - if (SUCCEEDED(controller->QueryInterface(IID_PPV_ARGS(&ctrl3)))) - ctrl3->get_RasterizationScale(&scale); - - int screenX = bounds.left + topLeft.x + static_cast(location.x * scale); - int screenY = bounds.top + topLeft.y + static_cast(location.y * scale); + UINT32 count = 0; + suggestions->get_Count(&count); - INT32 selectedCmd = TrackPopupMenu( - hPopupMenu, TPM_TOPALIGN | TPM_LEFTALIGN | TPM_RETURNCMD, - screenX, screenY, 0, hWnd, nullptr); + // Replace placeholder and add suggestion items. + for (UINT32 i = 0; i < count && i < 5; i++) + { + LPWSTR word = nullptr; + suggestions->GetValueAtIndex(i, &word); + // ... update menu items with word ... + CoTaskMemFree(word); + } + return S_OK; + }) + .Get()); +} - spState->hPopupMenu = nullptr; +// ... add other menu items, show popup with TrackPopupMenu ... - // --- Handle selection --- - if (selectedCmd >= kSuggestionBase && - selectedCmd < kSuggestionBase + (INT32)kMaxSuggestions) - { - UINT32 idx = selectedCmd - kSuggestionBase; - if (idx < spState->suggestionLabels.size() && args2) - { - args2->ApplySpellCheckSuggestion( - spState->suggestionLabels[idx].c_str()); - } - } - else if (selectedCmd > 0) - { - args->put_SelectedCommandId(selectedCmd); - } +// When the user picks a suggestion: +LPWSTR chosenSuggestion = /* label from selected menu item */; +args2->ApplySpellCheckSuggestion(chosenSuggestion); - DestroyMenu(hPopupMenu); - CHECK_FAILURE(deferral->Complete()); -} +deferral->Complete(); ``` ## .NET/WinRT ```csharp -void ShowCustomContextMenuWithSpellCheck( - object sender, CoreWebView2ContextMenuRequestedEventArgs args) -{ - var target = args.ContextMenuTarget; - - if (!target.IsEditable) - return; +// Inside the ContextMenuRequested handler for an editable target: +args.Handled = true; +var deferral = args.GetDeferral(); - // Suppress the default context menu. - args.Handled = true; +var contextMenu = new ContextMenuStrip(); +var placeholder = new ToolStripMenuItem("Loading...") { Enabled = false }; +contextMenu.Items.Add(placeholder); - // Take a deferral so the runtime keeps the event args alive. - var deferral = args.GetDeferral(); +// Handler fires immediately if resolved, or when ready. +args.GetSpellCheckSuggestionsAsync().Completed = (op, status) => +{ + if (status != AsyncStatus.Completed) return; + var suggestions = op.GetResults(); - try + contextMenu.Invoke(() => { - CoreWebView2SpellCheckReadiness spellState = target.SpellCheckReadiness; - IReadOnlyList suggestions = target.SpellCheckSuggestions; - - var contextMenu = new ContextMenuStrip(); - - if (spellState == CoreWebView2SpellCheckReadiness.Ready - && suggestions.Count > 0) - { - // Suggestions available — add them directly. - foreach (string suggestion in suggestions.Take(5)) - { - var item = new ToolStripMenuItem(suggestion); - item.Click += (s, e) => - { - args.ApplySpellCheckSuggestion(suggestion); - }; - contextMenu.Items.Add(item); - } - contextMenu.Items.Add(new ToolStripSeparator()); - } - else if (spellState == CoreWebView2SpellCheckReadiness.NotReady) - { - // Suggestions pending — show placeholder. - var placeholder = new ToolStripMenuItem("Loading suggestions...") - { - Enabled = false - }; - contextMenu.Items.Insert(0, placeholder); - contextMenu.Items.Insert(1, new ToolStripSeparator()); - - // One-shot completion handler fires when suggestions resolve - // or immediately if already resolved. - args.GetSpellCheckSuggestionsAsync((errorCode, readySuggestions) => - { - if (readySuggestions != null && readySuggestions.Count > 0) - { - int index = contextMenu.Items.IndexOf(placeholder); - contextMenu.Items.Remove(placeholder); - foreach (string suggestion in readySuggestions.Take(5)) - { - var item = new ToolStripMenuItem(suggestion); - item.Click += (s2, e2) => - { - args.ApplySpellCheckSuggestion(suggestion); - }; - contextMenu.Items.Insert(index++, item); - } - } - }); - } - - // Add standard WebView2 context menu items. - foreach (var menuItem in args.MenuItems) + contextMenu.Items.Remove(placeholder); + foreach (string s in suggestions.Take(5)) { - // ... add each item to contextMenu ... + var item = new ToolStripMenuItem(s); + item.Click += (_, _) => args.ApplySpellCheckSuggestion(s); + contextMenu.Items.Insert(0, item); } + }); +}; - contextMenu.Show(webView, webView.PointToClient(Cursor.Position)); - } - finally - { - deferral.Complete(); - } -} +// ... add other menu items, show contextMenu ... +deferral.Complete(); ``` # API Details @@ -336,24 +115,10 @@ void ShowCustomContextMenuWithSpellCheck( ## Win32 C++ ```idl -/// Indicates the readiness of spellcheck suggestions for the context menu -/// target. Used by hosts rendering custom context menus. -[v1_enum] -typedef enum COREWEBVIEW2_SPELL_CHECK_READINESS { - /// Spellcheck suggestions are available and ready to display. - COREWEBVIEW2_SPELL_CHECK_READINESS_READY, - /// Spellcheck is active but suggestions have not yet been resolved - /// (asynchronous retrieval in progress). - COREWEBVIEW2_SPELL_CHECK_READINESS_NOT_READY, - /// Spellcheck suggestions are not applicable for the current context - /// (not editable, no misspelling, or spellcheck disabled). - COREWEBVIEW2_SPELL_CHECK_READINESS_NOT_AVAILABLE, - /// Spellcheck resolution failed due to an internal error. - /// No suggestions will arrive. - COREWEBVIEW2_SPELL_CHECK_READINESS_ERROR, -} COREWEBVIEW2_SPELL_CHECK_READINESS; - /// Receives the result of `GetSpellCheckSuggestionsAsync`. +/// The handler is invoked exactly once — either immediately (if suggestions +/// are already resolved when `GetSpellCheckSuggestionsAsync` is called) or +/// when the spellcheck pipeline finishes resolving suggestions. [uuid(c5d6e7f8-9a0b-1c2d-3e4f-5a6b7c8d9e0f), object, pointer_default(unique)] interface ICoreWebView2GetSpellCheckSuggestionsCompletedHandler : IUnknown { /// Provides the result of the corresponding asynchronous method. @@ -362,51 +127,28 @@ interface ICoreWebView2GetSpellCheckSuggestionsCompletedHandler : IUnknown { [in] ICoreWebView2StringCollection* suggestions); } -/// Extends ICoreWebView2ContextMenuTarget with spellcheck information -/// for custom context menu integration. Allows host applications to retrieve -/// spellcheck suggestions when rendering custom menus on editable fields. -[uuid(d3f7e01a-9b5c-4e8f-a1d2-7c6b3e4f5a80), object, pointer_default(unique)] -interface ICoreWebView2ContextMenuTarget2 : ICoreWebView2ContextMenuTarget { - /// Gets the readiness state of spellcheck suggestions for the current - /// context menu target. - /// - /// When the value is `COREWEBVIEW2_SPELL_CHECK_READINESS_READY`, the - /// `SpellCheckSuggestions` collection is populated with correction strings. - /// When the value is `COREWEBVIEW2_SPELL_CHECK_READINESS_NOT_READY`, the - /// collection is empty; call `GetSpellCheckSuggestionsAsync` on - /// `ICoreWebView2ContextMenuRequestedEventArgs2` to be notified when - /// suggestions become available. - /// When the value is `COREWEBVIEW2_SPELL_CHECK_READINESS_NOT_AVAILABLE` or - /// `COREWEBVIEW2_SPELL_CHECK_READINESS_ERROR`, no suggestions will arrive. - [propget] HRESULT SpellCheckReadiness( - [out, retval] COREWEBVIEW2_SPELL_CHECK_READINESS* value); - - /// Gets the collection of spellcheck suggestion strings for the misspelled - /// word at the current context menu target. The collection is empty when - /// `SpellCheckReadiness` is not `READY`. - [propget] HRESULT SpellCheckSuggestions( - [out, retval] ICoreWebView2StringCollection** value); -} - /// Extends ICoreWebView2ContextMenuRequestedEventArgs with methods to apply -/// spellcheck corrections and asynchronously retrieve suggestions. +/// spellcheck corrections and asynchronously retrieve suggestions for custom +/// context menus. [uuid(e4a8f3b2-6c1d-4e9a-b5f7-2d8c9a0e1b34), object, pointer_default(unique)] interface ICoreWebView2ContextMenuRequestedEventArgs2 : ICoreWebView2ContextMenuRequestedEventArgs { /// Applies the selected spellcheck suggestion by replacing the misspelled /// word in the currently focused editable field. The `suggestion` parameter - /// must be one of the strings obtained from the `SpellCheckSuggestions` - /// collection on `ICoreWebView2ContextMenuTarget2`. + /// should be one of the strings obtained from the completion handler passed + /// to `GetSpellCheckSuggestionsAsync`. /// The runtime handles all editing internally, including routing to the /// correct frame for nested iframes. HRESULT ApplySpellCheckSuggestion([in] LPCWSTR suggestion); /// Asynchronously retrieves spellcheck suggestions for the misspelled word /// at the current context menu target. The `handler` is invoked exactly once - /// when suggestions become available. If suggestions are already in `READY` - /// state, the handler is invoked immediately and synchronously. + /// when suggestions become available. If suggestions are already resolved, + /// the handler is invoked immediately and synchronously. /// The handler receives `S_OK` and the suggestions collection on success, /// or an error HRESULT and `nullptr` on failure. + /// Only one handler can be registered at a time; calling this method again + /// replaces any previously registered handler. HRESULT GetSpellCheckSuggestionsAsync( [in] ICoreWebView2GetSpellCheckSuggestionsCompletedHandler* handler); } @@ -417,23 +159,6 @@ interface ICoreWebView2ContextMenuRequestedEventArgs2 ```csharp namespace Microsoft.Web.WebView2.Core { - enum CoreWebView2SpellCheckReadiness - { - Ready = 0, - NotReady = 1, - NotAvailable = 2, - Error = 3, - }; - - runtimeclass CoreWebView2ContextMenuTarget - { - [interface_name("Microsoft.Web.WebView2.Core.ICoreWebView2ContextMenuTarget2")] - { - CoreWebView2SpellCheckReadiness SpellCheckReadiness { get; }; - IVectorView SpellCheckSuggestions { get; }; - } - } - runtimeclass CoreWebView2ContextMenuRequestedEventArgs { [interface_name("Microsoft.Web.WebView2.Core.ICoreWebView2ContextMenuRequestedEventArgs2")] From a170b519622cc8b055e4b27616911ccc1753c589 Mon Sep 17 00:00:00 2001 From: Anurag Kumar Date: Sun, 5 Apr 2026 22:12:10 +0530 Subject: [PATCH 07/17] api changed to be a seperate interface --- specs/CustomContextMenuSpellcheck.md | 118 ++++++++++++++------------- 1 file changed, 62 insertions(+), 56 deletions(-) diff --git a/specs/CustomContextMenuSpellcheck.md b/specs/CustomContextMenuSpellcheck.md index e46378e2..5c972739 100644 --- a/specs/CustomContextMenuSpellcheck.md +++ b/specs/CustomContextMenuSpellcheck.md @@ -8,18 +8,21 @@ suggestions asynchronously, but there is no mechanism for custom context menu ho these suggestions. # Description -We propose extending the existing `ContextMenuRequested` API surface with spellcheck support for custom -context menus. This adds the ability to: +We propose a separate `ICoreWebView2ContextMenuSpellCheck` interface, discoverable via `QueryInterface` +from `ICoreWebView2ContextMenuRequestedEventArgs`, that provides spellcheck support for custom context +menus. This adds the ability to: 1. Asynchronously retrieve spellcheck suggestions via a one-shot completion handler. -2. Apply a selected spellcheck suggestion to replace the misspelled word in the DOM. +2. Apply a selected spellcheck suggestion by zero-based index (opaque identifier), preventing arbitrary + text injection. -The design uses a purely asynchronous approach: the host always calls `GetSpellCheckSuggestionsAsync` -which fires the handler exactly once either immediately (if suggestions are already resolved) or -when they become available. There is no synchronous readiness query. +The design uses a purely asynchronous approach: the host calls `GetSpellCheckSuggestionsAsync` which +fires the handler exactly once, always asynchronously (posted to the caller's message loop, never +invoked inline). There is no synchronous readiness query. -Hosts opt in by calling `QueryInterface` for the new `ICoreWebView2ContextMenuRequestedEventArgs2` -interface. Existing `ContextMenuRequested` consumers are unaffected. +`QueryInterface` for `ICoreWebView2ContextMenuSpellCheck` returns `E_NOINTERFACE` when spellcheck is +not applicable (e.g., right-click on a correctly-spelled word or non-editable field). This serves as +the availability check. Existing `ContextMenuRequested` consumers are unaffected. # Examples @@ -33,20 +36,19 @@ args->GetDeferral(&deferral); HMENU hMenu = CreatePopupMenu(); -auto args2 = - wil::try_com_query(args); +auto spellCheck = + wil::try_com_query(args); -if (args2) +if (spellCheck) { - // Show placeholder while suggestions load. AppendMenu(hMenu, MF_GRAYED | MF_STRING, IDM_SUGGESTION_BASE, L"Loading suggestions..."); - // Handler fires immediately if resolved, or when ready. - args2->GetSpellCheckSuggestionsAsync( + // Handler always fires asynchronously, never inline. + spellCheck->GetSpellCheckSuggestionsAsync( Callback( - [hMenu, args2](HRESULT errorCode, - ICoreWebView2StringCollection* suggestions) + [hMenu](HRESULT errorCode, + ICoreWebView2StringCollection* suggestions) -> HRESULT { if (FAILED(errorCode) || !suggestions) @@ -70,9 +72,8 @@ if (args2) // ... add other menu items, show popup with TrackPopupMenu ... -// When the user picks a suggestion: -LPWSTR chosenSuggestion = /* label from selected menu item */; -args2->ApplySpellCheckSuggestion(chosenSuggestion); +// When the user picks suggestion at index `idx`: +spellCheck->ApplySpellCheckSuggestion(idx); deferral->Complete(); ``` @@ -88,23 +89,29 @@ var contextMenu = new ContextMenuStrip(); var placeholder = new ToolStripMenuItem("Loading...") { Enabled = false }; contextMenu.Items.Add(placeholder); -// Handler fires immediately if resolved, or when ready. -args.GetSpellCheckSuggestionsAsync().Completed = (op, status) => +// QI returns null if spellcheck is not applicable. +var spellCheck = args as ICoreWebView2ContextMenuSpellCheck; +if (spellCheck != null) { - if (status != AsyncStatus.Completed) return; - var suggestions = op.GetResults(); - - contextMenu.Invoke(() => + // Handler always fires asynchronously. + spellCheck.GetSpellCheckSuggestionsAsync().Completed = (op, status) => { - contextMenu.Items.Remove(placeholder); - foreach (string s in suggestions.Take(5)) + if (status != AsyncStatus.Completed) return; + var suggestions = op.GetResults(); + + contextMenu.Invoke(() => { - var item = new ToolStripMenuItem(s); - item.Click += (_, _) => args.ApplySpellCheckSuggestion(s); - contextMenu.Items.Insert(0, item); - } - }); -}; + contextMenu.Items.Remove(placeholder); + for (int i = 0; i < Math.Min(suggestions.Count, 5); i++) + { + int idx = i; + var item = new ToolStripMenuItem(suggestions[i]); + item.Click += (_, _) => spellCheck.ApplySpellCheckSuggestion((uint)idx); + contextMenu.Items.Insert(idx, item); + } + }); + }; +} // ... add other menu items, show contextMenu ... deferral.Complete(); @@ -116,9 +123,9 @@ deferral.Complete(); ```idl /// Receives the result of `GetSpellCheckSuggestionsAsync`. -/// The handler is invoked exactly once — either immediately (if suggestions -/// are already resolved when `GetSpellCheckSuggestionsAsync` is called) or -/// when the spellcheck pipeline finishes resolving suggestions. +/// The handler is invoked exactly once, always asynchronously (posted to the +/// caller's message loop, never invoked inline during the call to +/// `GetSpellCheckSuggestionsAsync`). [uuid(c5d6e7f8-9a0b-1c2d-3e4f-5a6b7c8d9e0f), object, pointer_default(unique)] interface ICoreWebView2GetSpellCheckSuggestionsCompletedHandler : IUnknown { /// Provides the result of the corresponding asynchronous method. @@ -127,28 +134,30 @@ interface ICoreWebView2GetSpellCheckSuggestionsCompletedHandler : IUnknown { [in] ICoreWebView2StringCollection* suggestions); } -/// Extends ICoreWebView2ContextMenuRequestedEventArgs with methods to apply -/// spellcheck corrections and asynchronously retrieve suggestions for custom -/// context menus. +/// Provides spellcheck capabilities for custom context menus. Obtained via +/// `QueryInterface` from `ICoreWebView2ContextMenuRequestedEventArgs` when +/// spellcheck is applicable (editable target with a misspelled word and +/// spellcheck enabled). If QI returns `E_NOINTERFACE`, spellcheck is not +/// applicable for this context menu invocation. [uuid(e4a8f3b2-6c1d-4e9a-b5f7-2d8c9a0e1b34), object, pointer_default(unique)] -interface ICoreWebView2ContextMenuRequestedEventArgs2 - : ICoreWebView2ContextMenuRequestedEventArgs { +interface ICoreWebView2ContextMenuSpellCheck : IUnknown { /// Applies the selected spellcheck suggestion by replacing the misspelled - /// word in the currently focused editable field. The `suggestion` parameter - /// should be one of the strings obtained from the completion handler passed - /// to `GetSpellCheckSuggestionsAsync`. + /// word in the currently focused editable field. The `suggestionIndex` + /// parameter is a zero-based index into the suggestions collection + /// delivered by `GetSpellCheckSuggestionsAsync`. Returns `E_INVALIDARG` + /// if the index is out of range or suggestions have not been resolved yet. /// The runtime handles all editing internally, including routing to the /// correct frame for nested iframes. - HRESULT ApplySpellCheckSuggestion([in] LPCWSTR suggestion); + HRESULT ApplySpellCheckSuggestion([in] UINT32 suggestionIndex); /// Asynchronously retrieves spellcheck suggestions for the misspelled word /// at the current context menu target. The `handler` is invoked exactly once - /// when suggestions become available. If suggestions are already resolved, - /// the handler is invoked immediately and synchronously. + /// when suggestions become available, always asynchronously (posted to the + /// caller's message loop, never invoked inline during this call). /// The handler receives `S_OK` and the suggestions collection on success, /// or an error HRESULT and `nullptr` on failure. - /// Only one handler can be registered at a time; calling this method again - /// replaces any previously registered handler. + /// Only one handler may be registered. Returns `E_ILLEGAL_METHOD_CALL` + /// if a handler is already registered. HRESULT GetSpellCheckSuggestionsAsync( [in] ICoreWebView2GetSpellCheckSuggestionsCompletedHandler* handler); } @@ -159,14 +168,11 @@ interface ICoreWebView2ContextMenuRequestedEventArgs2 ```csharp namespace Microsoft.Web.WebView2.Core { - runtimeclass CoreWebView2ContextMenuRequestedEventArgs + runtimeclass CoreWebView2ContextMenuSpellCheck { - [interface_name("Microsoft.Web.WebView2.Core.ICoreWebView2ContextMenuRequestedEventArgs2")] - { - void ApplySpellCheckSuggestion(String suggestion); - Windows.Foundation.IAsyncOperation> - GetSpellCheckSuggestionsAsync(); - } + void ApplySpellCheckSuggestion(UInt32 suggestionIndex); + Windows.Foundation.IAsyncOperation> + GetSpellCheckSuggestionsAsync(); } } ``` From e4be18875fd8218667043e7cd653f3723e762ea2 Mon Sep 17 00:00:00 2001 From: Anurag Kumar Date: Tue, 7 Apr 2026 16:20:10 +0530 Subject: [PATCH 08/17] update api spec as per feedback --- specs/CustomContextMenuSpellcheck.md | 455 +++++++++++++++++++++------ 1 file changed, 357 insertions(+), 98 deletions(-) diff --git a/specs/CustomContextMenuSpellcheck.md b/specs/CustomContextMenuSpellcheck.md index 5c972739..74c16a13 100644 --- a/specs/CustomContextMenuSpellcheck.md +++ b/specs/CustomContextMenuSpellcheck.md @@ -2,119 +2,222 @@ Custom Context Menu SpellCheck === # Background + When a host application renders a custom context menu via the `ContextMenuRequested` event, spellcheck suggestions for misspelled words are not available. The browser's built-in spellcheck pipeline resolves suggestions asynchronously, but there is no mechanism for custom context menu hosts to retrieve or apply these suggestions. +This feature adds spellcheck support to custom context menus. Because spellcheck suggestions arrive +asynchronously (after the `ContextMenuRequested` event fires), the feature introduces a deferred +capability discovery pattern on `ICoreWebView2ContextMenuRequestedEventArgs2` that allows the host to +detect and acquire async capabilities at event time. SpellCheck is the first capability delivered +through this pattern; the same extensible mechanism will support future capabilities (emoji panel, +voice typing, etc.) without requiring additional EventArgs versions. + # Description -We propose a separate `ICoreWebView2ContextMenuSpellCheck` interface, discoverable via `QueryInterface` -from `ICoreWebView2ContextMenuRequestedEventArgs`, that provides spellcheck support for custom context -menus. This adds the ability to: -1. Asynchronously retrieve spellcheck suggestions via a one-shot completion handler. -2. Apply a selected spellcheck suggestion by zero-based index (opaque identifier), preventing arbitrary - text injection. +The `ContextMenuRequested` event is extended with `ICoreWebView2ContextMenuRequestedEventArgs2`. +This new interface provides: + +- **`DeferredCapabilities`** — A flags bitmask indicating which async capabilities are available for + this specific context menu invocation. For spellcheck, the host checks the `SPELL_CHECK` flag. +- **`GetDeferredCapability(REFIID, void**)`** — An IID-based accessor to acquire the capability + interface. For spellcheck, the host passes `IID_ICoreWebView2ContextMenuSpellCheck`. + +**Runtime version detection:** If `QueryInterface` for `EventArgs2` returns `E_NOINTERFACE`, the host +is running on an older runtime that doesn't support this feature. + +The spellcheck capability interface (`ICoreWebView2ContextMenuSpellCheck`) provides: -The design uses a purely asynchronous approach: the host calls `GetSpellCheckSuggestionsAsync` which -fires the handler exactly once, always asynchronously (posted to the caller's message loop, never -invoked inline). There is no synchronous readiness query. +- **`MisspelledWord`** — Read-only property returning the misspelled word under the cursor. Useful for + displaying "Suggestions for 'teh':" headers in custom menus. +- **`GetSpellCheckSuggestionsAsync`** — Retrieves suggestions as `ICoreWebView2ContextMenuItem` objects. + Each suggestion has a `Label` (display text) and `CommandId` (opaque identifier). -`QueryInterface` for `ICoreWebView2ContextMenuSpellCheck` returns `E_NOINTERFACE` when spellcheck is -not applicable (e.g., right-click on a correctly-spelled word or non-editable field). This serves as -the availability check. Existing `ContextMenuRequested` consumers are unaffected. +**Commanding model:** The host applies a suggestion by passing its `CommandId` to +`put_SelectedCommandId` on the EventArgs — the same execution path used for Cut, Copy, Paste, and +all other context menu items. No separate execution method is needed. + +**Async contract:** The handler fires exactly once, always asynchronously (posted to the caller's +message loop, never invoked inline). Only one handler may be registered; a second call returns +`E_ILLEGAL_METHOD_CALL`. + +**Availability:** If the `SPELL_CHECK` flag is not set in `DeferredCapabilities`, spellcheck is not +applicable (non-editable field, correctly-spelled word, or spellcheck disabled by policy). # Examples ## Win32 C++ ```cpp -// Inside the ContextMenuRequested handler for an editable target: -args->put_Handled(TRUE); -wil::com_ptr deferral; -args->GetDeferral(&deferral); +// Inside ContextMenuRequested handler for an editable field: -HMENU hMenu = CreatePopupMenu(); +// ── Step 1: Runtime version check ── +auto args2 = wil::try_com_query< + ICoreWebView2ContextMenuRequestedEventArgs2>(args); +if (!args2) + return S_OK; // Old runtime — use default menu. -auto spellCheck = - wil::try_com_query(args); +// ── Step 2: Discover deferred capabilities ── +COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES caps = + COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES_NONE; +CHECK_FAILURE(args2->get_DeferredCapabilities(&caps)); -if (spellCheck) -{ - AppendMenu(hMenu, MF_GRAYED | MF_STRING, IDM_SUGGESTION_BASE, - L"Loading suggestions..."); - - // Handler always fires asynchronously, never inline. - spellCheck->GetSpellCheckSuggestionsAsync( - Callback( - [hMenu](HRESULT errorCode, - ICoreWebView2StringCollection* suggestions) - -> HRESULT - { - if (FAILED(errorCode) || !suggestions) - return S_OK; +if (!(caps & COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES_SPELL_CHECK)) + return S_OK; // No misspelling — use default menu. + +// ── Step 3: Acquire spellcheck interface ── +wil::com_ptr spellCheck; +CHECK_FAILURE(args2->GetDeferredCapability(IID_PPV_ARGS(&spellCheck))); + +// ── Step 4: Take over menu rendering (only after confirming spellcheck) ── +CHECK_FAILURE(args->put_Handled(TRUE)); +wil::com_ptr deferral; +CHECK_FAILURE(args->GetDeferral(&deferral)); - UINT32 count = 0; - suggestions->get_Count(&count); +// ── Step 5: Read misspelled word (synchronous) ── +wil::unique_cotaskmem_string misspelledWord; +spellCheck->get_MisspelledWord(&misspelledWord); - // Replace placeholder and add suggestion items. - for (UINT32 i = 0; i < count && i < 5; i++) +// ── Step 6: Get suggestions and build menu in the callback ── +// Build the menu inside the callback so all items are present before display. +wil::com_ptr items; +args->get_MenuItems(&items); + +m_appWindow->RunAsync( + [this, args, spellCheck, items, deferral, + word = std::wstring(misspelledWord.get())]() + { + spellCheck->GetSpellCheckSuggestionsAsync( + Callback( + [this, args, items, deferral, word]( + HRESULT errorCode, + ICoreWebView2ContextMenuItemCollection* suggestions) -> HRESULT { - LPWSTR word = nullptr; - suggestions->GetValueAtIndex(i, &word); - // ... update menu items with word ... - CoTaskMemFree(word); - } - return S_OK; - }) - .Get()); -} + HMENU hMenu = CreatePopupMenu(); + + // Add spellcheck suggestions at the top. + UINT32 sugCount = 0; + if (SUCCEEDED(errorCode) && suggestions) + suggestions->get_Count(&sugCount); -// ... add other menu items, show popup with TrackPopupMenu ... + if (sugCount > 0) + { + AppendMenu(hMenu, MF_GRAYED | MF_STRING, 0, + (L"Suggestions for '" + word + L"':").c_str()); + for (UINT32 i = 0; i < sugCount; i++) + { + wil::com_ptr item; + suggestions->GetValueAtIndex(i, &item); + wil::unique_cotaskmem_string label; + item->get_Label(&label); + INT32 cmdId; + item->get_CommandId(&cmdId); + AppendMenu(hMenu, MF_STRING, cmdId, label.get()); + } + AppendMenu(hMenu, MF_SEPARATOR, 0, nullptr); + } -// When the user picks suggestion at index `idx`: -spellCheck->ApplySpellCheckSuggestion(idx); + // Add standard items (skip default spellcheck items by name). + AddMenuItems(hMenu, items.get()); // Your helper function -deferral->Complete(); + // Show popup and get user selection. + INT32 selectedCmd = ShowPopupAtCursor(hMenu, args); + + // ── Unified commanding ── + // Works for spellcheck suggestions AND standard items alike. + if (selectedCmd > 0) + args->put_SelectedCommandId(selectedCmd); + + DestroyMenu(hMenu); + deferral->Complete(); + return S_OK; + }).Get()); + }); +return S_OK; ``` -## .NET/WinRT +## .NET / C# ```csharp -// Inside the ContextMenuRequested handler for an editable target: -args.Handled = true; -var deferral = args.GetDeferral(); +webView.CoreWebView2.ContextMenuRequested += async (sender, args) => +{ + // Step 1: Discover deferred capabilities. + var caps = args.DeferredCapabilities; + if (!caps.HasFlag(CoreWebView2ContextMenuDeferredCapabilities.SpellCheck)) + return; // No misspelling — let default menu show. -var contextMenu = new ContextMenuStrip(); -var placeholder = new ToolStripMenuItem("Loading...") { Enabled = false }; -contextMenu.Items.Add(placeholder); + // Step 2: Take over menu rendering and hold deferral. + args.Handled = true; + var deferral = args.GetDeferral(); -// QI returns null if spellcheck is not applicable. -var spellCheck = args as ICoreWebView2ContextMenuSpellCheck; -if (spellCheck != null) -{ - // Handler always fires asynchronously. - spellCheck.GetSpellCheckSuggestionsAsync().Completed = (op, status) => + // Step 3: Acquire spellcheck capability. + var spellCheck = args.GetDeferredCapability(); + + // Step 4: Read misspelled word. + string misspelledWord = spellCheck.MisspelledWord; // e.g., "teh" + + // Step 5: Get suggestions. + var suggestions = await spellCheck.GetSpellCheckSuggestionsAsync(); + + // Step 6: Build custom menu. + var contextMenu = new ContextMenuStrip(); + bool completed = false; + + contextMenu.Items.Add(new ToolStripMenuItem( + $"Suggestions for '{misspelledWord}':") { Enabled = false }); + + foreach (var suggestion in suggestions) { - if (status != AsyncStatus.Completed) return; - var suggestions = op.GetResults(); + var item = new ToolStripMenuItem(suggestion.Label); + var capturedId = suggestion.CommandId; + item.Click += (_, _) => + { + // Unified commanding — same as Cut, Copy, Paste. + args.SelectedCommandId = capturedId; + }; + contextMenu.Items.Add(item); + } + + // Add standard items from args.MenuItems... - contextMenu.Invoke(() => + // Complete deferral once on menu close (covers both selection and dismissal). + contextMenu.Closed += (_, _) => + { + if (!completed) { - contextMenu.Items.Remove(placeholder); - for (int i = 0; i < Math.Min(suggestions.Count, 5); i++) - { - int idx = i; - var item = new ToolStripMenuItem(suggestions[i]); - item.Click += (_, _) => spellCheck.ApplySpellCheckSuggestion((uint)idx); - contextMenu.Items.Insert(idx, item); - } - }); + completed = true; + deferral.Complete(); + } }; + contextMenu.Show(webView, new Point(args.Location.X, args.Location.Y)); +}; +``` + +## Future Capabilities (Extensibility Pattern) + +When additional capabilities are added in the future, the same discovery pattern applies, no new +EventArgs versions are needed: + +```cpp +COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES caps; +args2->get_DeferredCapabilities(&caps); + +if (caps & COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES_SPELL_CHECK) +{ + wil::com_ptr spellCheck; + args2->GetDeferredCapability(IID_PPV_ARGS(&spellCheck)); + // ... use spellCheck ... } -// ... add other menu items, show contextMenu ... -deferral.Complete(); +// Future: no new EventArgs versions needed, just new flag constants. +// if (caps & COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES_EMOJI) +// { +// wil::com_ptr emoji; +// args2->GetDeferredCapability(IID_PPV_ARGS(&emoji)); +// // ... same pattern ... +// } ``` # API Details @@ -122,6 +225,66 @@ deferral.Complete(); ## Win32 C++ ```idl +// ─── EventArgs2: Deferred Capability Discovery (introduced for SpellCheck) ─── + +/// Flags indicating which deferred capabilities are available for a given +/// context menu invocation. Treat as a bitmask — test individual flags +/// with bitwise AND. +[v1_enum] +typedef enum COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES { + /// No deferred capabilities are available. + COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES_NONE = 0x0, + /// Spellcheck is available — the target is an editable field with a + /// misspelled word and spellcheck is enabled. + COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES_SPELL_CHECK = 0x1, + // Future capabilities add new flag constants here. No interface changes. + // COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES_EMOJI = 0x2, + // COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES_VOICE_TYPING = 0x4, +} COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES; + +/// Extends `ICoreWebView2ContextMenuRequestedEventArgs` with deferred capability +/// discovery and acquisition. Introduced as part of the SpellCheck feature, this +/// extensible pattern supports all current and future deferred capabilities — +/// new capabilities add a flag constant and an interface definition, not new +/// EventArgs versions. +/// +/// The host checks `DeferredCapabilities` flags to discover what async +/// capabilities exist for this invocation, then calls `GetDeferredCapability` +/// with the desired interface IID to acquire it. +[uuid(f1b2c3d4-5e6f-7a8b-9c0d-e1f2a3b4c5d6), object, pointer_default(unique)] +interface ICoreWebView2ContextMenuRequestedEventArgs2 + : ICoreWebView2ContextMenuRequestedEventArgs { + + /// Returns a bitmask of deferred capabilities available for this context + /// menu invocation. The host checks individual flags to determine which + /// capability interfaces can be acquired via `GetDeferredCapability`. + /// + /// A flag being set means the corresponding capability is applicable AND + /// enabled for this invocation. Flags not set means either the capability + /// is not applicable (e.g., no misspelling) or the capability is disabled + /// (e.g., spellcheck off by policy). + [propget] HRESULT DeferredCapabilities( + [out, retval] COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES* value); + + /// Retrieves the capability-specific interface for a deferred capability. + /// + /// Pass the IID of the desired capability interface. Returns `S_OK` and + /// the interface pointer if the capability is available. Returns + /// `E_NOINTERFACE` if the capability is not applicable for this invocation. + /// + /// Supported IIDs: + /// `IID_ICoreWebView2ContextMenuSpellCheck` + /// (future: emoji, voice typing, writing direction, etc.) + /// + /// The returned interface is valid for the lifetime of the event + /// (until the deferral is completed). + HRESULT GetDeferredCapability( + [in] REFIID riid, + [out, iid_is(riid), retval] void** capability); +} + +// ─── SpellCheck Capability ─── + /// Receives the result of `GetSpellCheckSuggestionsAsync`. /// The handler is invoked exactly once, always asynchronously (posted to the /// caller's message loop, never invoked inline during the call to @@ -129,50 +292,146 @@ deferral.Complete(); [uuid(c5d6e7f8-9a0b-1c2d-3e4f-5a6b7c8d9e0f), object, pointer_default(unique)] interface ICoreWebView2GetSpellCheckSuggestionsCompletedHandler : IUnknown { /// Provides the result of the corresponding asynchronous method. + /// Each item in the `suggestions` collection is an + /// `ICoreWebView2ContextMenuItem` whose `Label` is the suggestion text + /// and whose `CommandId` uniquely identifies it. To apply a suggestion, + /// pass the selected item's `CommandId` to + /// `ICoreWebView2ContextMenuRequestedEventArgs.put_SelectedCommandId`. HRESULT Invoke( [in] HRESULT errorCode, - [in] ICoreWebView2StringCollection* suggestions); + [in] ICoreWebView2ContextMenuItemCollection* suggestions); } -/// Provides spellcheck capabilities for custom context menus. Obtained via -/// `QueryInterface` from `ICoreWebView2ContextMenuRequestedEventArgs` when -/// spellcheck is applicable (editable target with a misspelled word and -/// spellcheck enabled). If QI returns `E_NOINTERFACE`, spellcheck is not -/// applicable for this context menu invocation. +/// Provides spellcheck capabilities for custom context menus. Acquired via +/// `ICoreWebView2ContextMenuRequestedEventArgs2::GetDeferredCapability` +/// when the `SPELL_CHECK` flag is set in `DeferredCapabilities`. +/// +/// To apply a suggestion, retrieve the `CommandId` from the desired +/// `ICoreWebView2ContextMenuItem` in the suggestions collection and pass it +/// to `ICoreWebView2ContextMenuRequestedEventArgs.put_SelectedCommandId`. +/// This follows the same commanding model used for all other context menu +/// items (Cut, Copy, Paste, etc.). [uuid(e4a8f3b2-6c1d-4e9a-b5f7-2d8c9a0e1b34), object, pointer_default(unique)] interface ICoreWebView2ContextMenuSpellCheck : IUnknown { - /// Applies the selected spellcheck suggestion by replacing the misspelled - /// word in the currently focused editable field. The `suggestionIndex` - /// parameter is a zero-based index into the suggestions collection - /// delivered by `GetSpellCheckSuggestionsAsync`. Returns `E_INVALIDARG` - /// if the index is out of range or suggestions have not been resolved yet. - /// The runtime handles all editing internally, including routing to the - /// correct frame for nested iframes. - HRESULT ApplySpellCheckSuggestion([in] UINT32 suggestionIndex); + /// Gets the misspelled word at the current context menu target location. + /// This is the word that spellcheck flagged as incorrect and for which + /// suggestions are available via `GetSpellCheckSuggestionsAsync`. + /// The caller must free the returned string using `CoTaskMemFree`. + /// + /// Use this to display contextual headers like "Suggestions for 'teh':" + /// in your custom context menu. + [propget] HRESULT MisspelledWord([out, retval] LPWSTR* value); /// Asynchronously retrieves spellcheck suggestions for the misspelled word /// at the current context menu target. The `handler` is invoked exactly once /// when suggestions become available, always asynchronously (posted to the /// caller's message loop, never invoked inline during this call). - /// The handler receives `S_OK` and the suggestions collection on success, - /// or an error HRESULT and `nullptr` on failure. - /// Only one handler may be registered. Returns `E_ILLEGAL_METHOD_CALL` - /// if a handler is already registered. + /// + /// The handler receives `S_OK` and a collection of + /// `ICoreWebView2ContextMenuItem` objects on success, or an error HRESULT + /// and `nullptr` on failure. Each item's `Label` is the suggestion text + /// and its `CommandId` can be passed to `put_SelectedCommandId` to apply + /// the correction. + /// + /// Only one handler may be registered per event invocation. A second call + /// returns `E_ILLEGAL_METHOD_CALL`. HRESULT GetSpellCheckSuggestionsAsync( [in] ICoreWebView2GetSpellCheckSuggestionsCompletedHandler* handler); } ``` -## .NET/WinRT +## .NET / WinRT ```csharp namespace Microsoft.Web.WebView2.Core { + [Flags] + enum CoreWebView2ContextMenuDeferredCapabilities + { + None = 0x0, + SpellCheck = 0x1, + // Future: Emoji = 0x2, VoiceTyping = 0x4, ... + } + + // Extended event args — includes deferred capability discovery for spellcheck. + runtimeclass CoreWebView2ContextMenuRequestedEventArgs2 + : CoreWebView2ContextMenuRequestedEventArgs + { + CoreWebView2ContextMenuDeferredCapabilities DeferredCapabilities { get; }; + + // Generic accessor — returns null if capability is not applicable. + T GetDeferredCapability(); + } + runtimeclass CoreWebView2ContextMenuSpellCheck { - void ApplySpellCheckSuggestion(UInt32 suggestionIndex); - Windows.Foundation.IAsyncOperation> + // The misspelled word under the cursor. + String MisspelledWord { get; }; + + // Returns suggestions as ContextMenuItem objects. + // Apply via args.SelectedCommandId = suggestion.CommandId. + Windows.Foundation.IAsyncOperation> GetSpellCheckSuggestionsAsync(); } } ``` + +# Behavioral Details + +## Discovery and Acquisition Flow + +| Step | Action | Result | +|------|--------|--------| +| 1 | QI for `EventArgs2` from EventArgs | `E_NOINTERFACE` → old runtime, fall back to default menu | +| 2 | Read `DeferredCapabilities` flags | Bitmask of available capabilities for this invocation | +| 3 | Check `SPELL_CHECK` flag | Flag set → misspelling present; flag not set → no misspelling | +| 4 | Call `GetDeferredCapability(IID_SpellCheck)` | Returns spellcheck interface with `AddRef` | + +## Suggestion Item Properties + +Each `ICoreWebView2ContextMenuItem` returned by `GetSpellCheckSuggestionsAsync` has: + +| Property | Value | +|----------|-------| +| `Label` | Suggestion text (e.g., "the") | +| `CommandId` | WebView2-allocated opaque ID (e.g., 50001) | +| `Name` | `"spellcheck"` | +| `Kind` | `COREWEBVIEW2_CONTEXT_MENU_ITEM_KIND_COMMAND` | +| `IsEnabled` | `TRUE` | +| `IsChecked` | `FALSE` | +| `Icon` | `nullptr` | +| `ShortcutKeyDescription` | `L""` | +| `Children` | `nullptr` | + +## Error Handling + +| Scenario | Behavior | +|----------|----------| +| QI for EventArgs2 fails | Old runtime — use default menu | +| `SPELL_CHECK` flag not set | No misspelling — skip spellcheck UI | +| `GetDeferredCapability` returns `E_NOINTERFACE` | Capability not available (consistent with flags) | +| `MisspelledWord` returns empty string | Spellcheck applicable but word is empty — defensive check recommended | +| Second call to `GetSpellCheckSuggestionsAsync` | Returns `E_ILLEGAL_METHOD_CALL` | +| Suggestions handler — no suggestions available | `count == 0` — show "No suggestions" or skip | +| User dismisses menu without selecting | Set `SelectedCommandId` to 0 or simply complete the deferral | + +# Appendix + +## Extensibility + +The deferred capability pattern introduced by this feature is designed for zero-versioning extension: + +| To add a new capability | Required changes | +|------------------------|-----------------| +| New flag constant | for example - `COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES_EMOJI = 0x2` | +| New capability interface | `ICoreWebView2ContextMenuEmoji : IUnknown { ... }` | + +## Relationship to Existing APIs + +| Existing API | This Feature | +|-------------|-------------| +| `EventArgs.MenuItems` | Synchronous snapshot of menu items | +| `EventArgs.SelectedCommandId` | Execution path — now also used for spellcheck suggestions | +| `ContextMenuItem.CommandId` | Already used for all items — spellcheck items join this pool | +| `ContextMenuItem.Label` | Display text — spellcheck suggestions use this for the suggestion word | +| `EventArgs.GetDeferral()` | Must be held across the async `GetSpellCheckSuggestionsAsync` gap | From cc97cc80eb72dea651d7da15ec38b88eb84a9cfa Mon Sep 17 00:00:00 2001 From: Anurag Kumar Date: Tue, 7 Apr 2026 16:53:51 +0530 Subject: [PATCH 09/17] minor name refactoring --- specs/CustomContextMenuSpellcheck.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/specs/CustomContextMenuSpellcheck.md b/specs/CustomContextMenuSpellcheck.md index 74c16a13..6e39b53c 100644 --- a/specs/CustomContextMenuSpellcheck.md +++ b/specs/CustomContextMenuSpellcheck.md @@ -395,7 +395,7 @@ Each `ICoreWebView2ContextMenuItem` returned by `GetSpellCheckSuggestionsAsync` |----------|-------| | `Label` | Suggestion text (e.g., "the") | | `CommandId` | WebView2-allocated opaque ID (e.g., 50001) | -| `Name` | `"spellcheck"` | +| `Name` | `"spellCheckSuggestion"` | | `Kind` | `COREWEBVIEW2_CONTEXT_MENU_ITEM_KIND_COMMAND` | | `IsEnabled` | `TRUE` | | `IsChecked` | `FALSE` | @@ -426,6 +426,22 @@ The deferred capability pattern introduced by this feature is designed for zero- | New flag constant | for example - `COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES_EMOJI = 0x2` | | New capability interface | `ICoreWebView2ContextMenuEmoji : IUnknown { ... }` | +## Planned SpellCheck Extensions + +The following actions will be added as additional `ICoreWebView2ContextMenuItem` entries in the +collection returned by `GetSpellCheckSuggestionsAsync`. No new interfaces or methods are required: + +| Action | `Name` value | +|--------|-------------| +| Add to Dictionary | `"spellCheckAddToDictionary"` | +| Ignore (session) | `"spellCheckIgnore"` | + +These follow the same commanding model: the host renders them like any other item and applies via +`SelectedCommandId`. A `Language` property (BCP-47 tag of the dictionary that flagged the misspelling) +will also be added to `ICoreWebView2ContextMenuSpellCheck` in a follow-up version. Profile-level +spellcheck configuration (`IsSpellCheckEnabled`, `SpellCheckLanguages`) is tracked as a separate +follow-up. + ## Relationship to Existing APIs | Existing API | This Feature | From 74ae966483a8aedefd5ee3c614e11c8b2f13ee2e Mon Sep 17 00:00:00 2001 From: Anurag Kumar Date: Wed, 8 Apr 2026 13:58:16 +0530 Subject: [PATCH 10/17] review commits addressed --- specs/CustomContextMenuSpellcheck.md | 329 ++++++++++++++++++--------- 1 file changed, 219 insertions(+), 110 deletions(-) diff --git a/specs/CustomContextMenuSpellcheck.md b/specs/CustomContextMenuSpellcheck.md index 6e39b53c..754c57db 100644 --- a/specs/CustomContextMenuSpellcheck.md +++ b/specs/CustomContextMenuSpellcheck.md @@ -1,4 +1,4 @@ -Custom Context Menu SpellCheck +Spellcheck Support for Custom Context Menus === # Background @@ -11,7 +11,7 @@ these suggestions. This feature adds spellcheck support to custom context menus. Because spellcheck suggestions arrive asynchronously (after the `ContextMenuRequested` event fires), the feature introduces a deferred capability discovery pattern on `ICoreWebView2ContextMenuRequestedEventArgs2` that allows the host to -detect and acquire async capabilities at event time. SpellCheck is the first capability delivered +detect and acquire async capabilities at event time. Spellcheck is the first capability delivered through this pattern; the same extensible mechanism will support future capabilities (emoji panel, voice typing, etc.) without requiring additional EventArgs versions. @@ -26,7 +26,7 @@ This new interface provides: interface. For spellcheck, the host passes `IID_ICoreWebView2ContextMenuSpellCheck`. **Runtime version detection:** If `QueryInterface` for `EventArgs2` returns `E_NOINTERFACE`, the host -is running on an older runtime that doesn't support this feature. +is running on an older runtime that does not support this feature. The spellcheck capability interface (`ICoreWebView2ContextMenuSpellCheck`) provides: @@ -48,97 +48,168 @@ applicable (non-editable field, correctly-spelled word, or spellcheck disabled b # Examples -## Win32 C++ +## Win32 C++ — Display Custom Context Menu with Spellcheck Suggestions ```cpp -// Inside ContextMenuRequested handler for an editable field: - -// ── Step 1: Runtime version check ── -auto args2 = wil::try_com_query< - ICoreWebView2ContextMenuRequestedEventArgs2>(args); -if (!args2) - return S_OK; // Old runtime — use default menu. - -// ── Step 2: Discover deferred capabilities ── -COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES caps = - COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES_NONE; -CHECK_FAILURE(args2->get_DeferredCapabilities(&caps)); - -if (!(caps & COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES_SPELL_CHECK)) - return S_OK; // No misspelling — use default menu. - -// ── Step 3: Acquire spellcheck interface ── -wil::com_ptr spellCheck; -CHECK_FAILURE(args2->GetDeferredCapability(IID_PPV_ARGS(&spellCheck))); - -// ── Step 4: Take over menu rendering (only after confirming spellcheck) ── -CHECK_FAILURE(args->put_Handled(TRUE)); -wil::com_ptr deferral; -CHECK_FAILURE(args->GetDeferral(&deferral)); - -// ── Step 5: Read misspelled word (synchronous) ── -wil::unique_cotaskmem_string misspelledWord; -spellCheck->get_MisspelledWord(&misspelledWord); - -// ── Step 6: Get suggestions and build menu in the callback ── -// Build the menu inside the callback so all items are present before display. -wil::com_ptr items; -args->get_MenuItems(&items); - -m_appWindow->RunAsync( - [this, args, spellCheck, items, deferral, - word = std::wstring(misspelledWord.get())]() - { - spellCheck->GetSpellCheckSuggestionsAsync( - Callback( - [this, args, items, deferral, word]( - HRESULT errorCode, - ICoreWebView2ContextMenuItemCollection* suggestions) -> HRESULT +webView->add_ContextMenuRequested( + Callback( + [this](ICoreWebView2* sender, + ICoreWebView2ContextMenuRequestedEventArgs* args) -> HRESULT + { + // ── Step 1: Runtime version check ── + auto args2 = wil::try_com_query< + ICoreWebView2ContextMenuRequestedEventArgs2>(args); + if (!args2) + return S_OK; // Old runtime — use default menu. + + // ── Step 2: Discover deferred capabilities ── + COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES caps = + COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES_NONE; + CHECK_FAILURE(args2->get_DeferredCapabilities(&caps)); + + if (!(caps & + COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES_SPELL_CHECK)) + return S_OK; // No misspelling — use default menu. + + // ── Step 3: Acquire spellcheck interface ── + wil::com_ptr spellCheck; + CHECK_FAILURE( + args2->GetDeferredCapability(IID_PPV_ARGS(&spellCheck))); + + // ── Step 4: Take over rendering (only after confirming spellcheck) ── + CHECK_FAILURE(args->put_Handled(TRUE)); + wil::com_ptr deferral; + CHECK_FAILURE(args->GetDeferral(&deferral)); + + // ── Step 5: Read misspelled word (synchronous) ── + wil::unique_cotaskmem_string misspelledWord; + CHECK_FAILURE(spellCheck->get_MisspelledWord(&misspelledWord)); + + // ── Step 6: Get suggestions and build menu in the callback ── + wil::com_ptr items; + args->get_MenuItems(&items); + + m_appWindow->RunAsync( + [this, args, spellCheck, items, deferral, + word = std::wstring(misspelledWord.get())]() { - HMENU hMenu = CreatePopupMenu(); - - // Add spellcheck suggestions at the top. - UINT32 sugCount = 0; - if (SUCCEEDED(errorCode) && suggestions) - suggestions->get_Count(&sugCount); - - if (sugCount > 0) - { - AppendMenu(hMenu, MF_GRAYED | MF_STRING, 0, - (L"Suggestions for '" + word + L"':").c_str()); - for (UINT32 i = 0; i < sugCount; i++) - { - wil::com_ptr item; - suggestions->GetValueAtIndex(i, &item); - wil::unique_cotaskmem_string label; - item->get_Label(&label); - INT32 cmdId; - item->get_CommandId(&cmdId); - AppendMenu(hMenu, MF_STRING, cmdId, label.get()); - } - AppendMenu(hMenu, MF_SEPARATOR, 0, nullptr); - } - - // Add standard items (skip default spellcheck items by name). - AddMenuItems(hMenu, items.get()); // Your helper function - - // Show popup and get user selection. - INT32 selectedCmd = ShowPopupAtCursor(hMenu, args); - - // ── Unified commanding ── - // Works for spellcheck suggestions AND standard items alike. - if (selectedCmd > 0) - args->put_SelectedCommandId(selectedCmd); - - DestroyMenu(hMenu); - deferral->Complete(); - return S_OK; - }).Get()); - }); -return S_OK; + spellCheck->GetSpellCheckSuggestionsAsync( + Callback< + ICoreWebView2GetSpellCheckSuggestionsCompletedHandler>( + [this, args, items, deferral, word]( + HRESULT errorCode, + ICoreWebView2ContextMenuItemCollection* + suggestions) -> HRESULT + { + HMENU hMenu = CreatePopupMenu(); + + // Add spellcheck suggestions at the top. + UINT32 sugCount = 0; + if (SUCCEEDED(errorCode) && suggestions) + suggestions->get_Count(&sugCount); + + if (sugCount > 0) + { + AppendMenuW( + hMenu, MF_GRAYED | MF_STRING, 0, + (L"Suggestions for '" + word + L"':") + .c_str()); + for (UINT32 i = 0; i < sugCount; i++) + { + wil::com_ptr< + ICoreWebView2ContextMenuItem> + item; + suggestions->GetValueAtIndex(i, &item); + wil::unique_cotaskmem_string label; + item->get_Label(&label); + INT32 cmdId; + item->get_CommandId(&cmdId); + AppendMenuW( + hMenu, MF_STRING, cmdId, + label.get()); + } + AppendMenuW( + hMenu, MF_SEPARATOR, 0, nullptr); + } + + // Add remaining standard context menu items, + // skipping built-in spellcheck entries. + UINT32 itemCount = 0; + items->get_Count(&itemCount); + for (UINT32 i = 0; i < itemCount; i++) + { + wil::com_ptr + cur; + items->GetValueAtIndex(i, &cur); + wil::unique_cotaskmem_string name; + cur->get_Name(&name); + // Skip built-in spellcheck items already + // handled above. + if (wcsstr(name.get(), L"spellCheck")) + continue; + COREWEBVIEW2_CONTEXT_MENU_ITEM_KIND kind; + cur->get_Kind(&kind); + if (kind == + COREWEBVIEW2_CONTEXT_MENU_ITEM_KIND_SEPARATOR) + { + AppendMenuW( + hMenu, MF_SEPARATOR, 0, nullptr); + } + else + { + wil::unique_cotaskmem_string label; + cur->get_Label(&label); + INT32 cmdId; + cur->get_CommandId(&cmdId); + BOOL enabled = FALSE; + cur->get_IsEnabled(&enabled); + AppendMenuW( + hMenu, + MF_STRING | + (enabled ? 0 : MF_GRAYED), + cmdId, label.get()); + } + } + + // Show popup at the context menu location. + HWND hWnd = m_appWindow->GetMainWindow(); + POINT pt; + wil::com_ptr + target; + args->get_ContextMenuTarget(&target); + POINT location; + args->get_Location(&location); + RECT bounds; + GetWindowRect(hWnd, &bounds); + // location is in WebView client coordinates; + // convert to screen. + pt = {bounds.left + location.x, + bounds.top + location.y}; + INT32 selectedCmd = TrackPopupMenu( + hMenu, + TPM_TOPALIGN | TPM_LEFTALIGN | + TPM_RETURNCMD, + pt.x, pt.y, 0, hWnd, nullptr); + + // ── Unified commanding ── + // Works for spellcheck suggestions AND + // standard items alike. + if (selectedCmd > 0) + args->put_SelectedCommandId(selectedCmd); + + DestroyMenu(hMenu); + deferral->Complete(); + return S_OK; + }) + .Get()); + }); + return S_OK; + }) + .Get(), + &m_contextMenuRequestedToken); ``` -## .NET / C# +## .NET / C# — Display Custom Context Menu with Spellcheck Suggestions ```csharp webView.CoreWebView2.ContextMenuRequested += async (sender, args) => @@ -180,7 +251,27 @@ webView.CoreWebView2.ContextMenuRequested += async (sender, args) => contextMenu.Items.Add(item); } - // Add standard items from args.MenuItems... + // Add standard items, skipping built-in spellcheck entries. + foreach (var menuItem in args.MenuItems) + { + if (menuItem.Name.StartsWith("spellCheck")) + continue; + if (menuItem.Kind == CoreWebView2ContextMenuItemKind.Separator) + { + contextMenu.Items.Add(new ToolStripSeparator()); + } + else + { + var stdItem = new ToolStripMenuItem(menuItem.Label); + stdItem.Enabled = menuItem.IsEnabled; + var stdCmdId = menuItem.CommandId; + stdItem.Click += (_, _) => + { + args.SelectedCommandId = stdCmdId; + }; + contextMenu.Items.Add(stdItem); + } + } // Complete deferral once on menu close (covers both selection and dismissal). contextMenu.Closed += (_, _) => @@ -195,7 +286,7 @@ webView.CoreWebView2.ContextMenuRequested += async (sender, args) => }; ``` -## Future Capabilities (Extensibility Pattern) +## Win32 C++ — Check Multiple Deferred Capabilities (Future Pattern) When additional capabilities are added in the future, the same discovery pattern applies, no new EventArgs versions are needed: @@ -225,7 +316,7 @@ if (caps & COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES_SPELL_CHECK) ## Win32 C++ ```idl -// ─── EventArgs2: Deferred Capability Discovery (introduced for SpellCheck) ─── +// ─── EventArgs2: Deferred Capability Discovery (introduced for spellcheck) ─── /// Flags indicating which deferred capabilities are available for a given /// context menu invocation. Treat as a bitmask — test individual flags @@ -243,7 +334,7 @@ typedef enum COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES { } COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES; /// Extends `ICoreWebView2ContextMenuRequestedEventArgs` with deferred capability -/// discovery and acquisition. Introduced as part of the SpellCheck feature, this +/// discovery and acquisition. Introduced as part of the spellcheck feature, this /// extensible pattern supports all current and future deferred capabilities — /// new capabilities add a flag constant and an interface definition, not new /// EventArgs versions. @@ -251,7 +342,7 @@ typedef enum COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES { /// The host checks `DeferredCapabilities` flags to discover what async /// capabilities exist for this invocation, then calls `GetDeferredCapability` /// with the desired interface IID to acquire it. -[uuid(f1b2c3d4-5e6f-7a8b-9c0d-e1f2a3b4c5d6), object, pointer_default(unique)] +[uuid(54ab63d2-9c3b-45a1-88d8-6c5a561784c9), object, pointer_default(unique)] interface ICoreWebView2ContextMenuRequestedEventArgs2 : ICoreWebView2ContextMenuRequestedEventArgs { @@ -283,13 +374,13 @@ interface ICoreWebView2ContextMenuRequestedEventArgs2 [out, iid_is(riid), retval] void** capability); } -// ─── SpellCheck Capability ─── +// ─── Spellcheck Capability ─── /// Receives the result of `GetSpellCheckSuggestionsAsync`. /// The handler is invoked exactly once, always asynchronously (posted to the /// caller's message loop, never invoked inline during the call to /// `GetSpellCheckSuggestionsAsync`). -[uuid(c5d6e7f8-9a0b-1c2d-3e4f-5a6b7c8d9e0f), object, pointer_default(unique)] +[uuid(d73832f9-d05b-438d-bb6d-644124521fe3), object, pointer_default(unique)] interface ICoreWebView2GetSpellCheckSuggestionsCompletedHandler : IUnknown { /// Provides the result of the corresponding asynchronous method. /// Each item in the `suggestions` collection is an @@ -311,7 +402,7 @@ interface ICoreWebView2GetSpellCheckSuggestionsCompletedHandler : IUnknown { /// to `ICoreWebView2ContextMenuRequestedEventArgs.put_SelectedCommandId`. /// This follows the same commanding model used for all other context menu /// items (Cut, Copy, Paste, etc.). -[uuid(e4a8f3b2-6c1d-4e9a-b5f7-2d8c9a0e1b34), object, pointer_default(unique)] +[uuid(aa742569-d944-4510-8924-c2c0583fa320), object, pointer_default(unique)] interface ICoreWebView2ContextMenuSpellCheck : IUnknown { /// Gets the misspelled word at the current context menu target location. /// This is the word that spellcheck flagged as incorrect and for which @@ -345,6 +436,10 @@ interface ICoreWebView2ContextMenuSpellCheck : IUnknown { ```csharp namespace Microsoft.Web.WebView2.Core { + /// + /// Flags indicating which deferred capabilities are available for a given + /// context menu invocation. + /// [Flags] enum CoreWebView2ContextMenuDeferredCapabilities { @@ -353,23 +448,37 @@ namespace Microsoft.Web.WebView2.Core // Future: Emoji = 0x2, VoiceTyping = 0x4, ... } - // Extended event args — includes deferred capability discovery for spellcheck. - runtimeclass CoreWebView2ContextMenuRequestedEventArgs2 - : CoreWebView2ContextMenuRequestedEventArgs + runtimeclass CoreWebView2ContextMenuRequestedEventArgs { - CoreWebView2ContextMenuDeferredCapabilities DeferredCapabilities { get; }; + // Existing members unchanged. - // Generic accessor — returns null if capability is not applicable. - T GetDeferredCapability(); + [interface_name("ICoreWebView2ContextMenuRequestedEventArgs2")] + { + /// + /// Returns a bitmask of deferred capabilities available for this + /// context menu invocation. + /// + CoreWebView2ContextMenuDeferredCapabilities DeferredCapabilities { get; }; + + /// + /// Retrieves the capability-specific interface for a deferred + /// capability. Returns null if the capability is not applicable. + /// + T GetDeferredCapability(); + } } runtimeclass CoreWebView2ContextMenuSpellCheck { - // The misspelled word under the cursor. + /// + /// The misspelled word under the cursor. + /// String MisspelledWord { get; }; - // Returns suggestions as ContextMenuItem objects. - // Apply via args.SelectedCommandId = suggestion.CommandId. + /// + /// Asynchronously retrieves spellcheck suggestions. Each item's + /// CommandId can be passed to SelectedCommandId to apply the correction. + /// Windows.Foundation.IAsyncOperation> GetSpellCheckSuggestionsAsync(); } @@ -410,10 +519,10 @@ Each `ICoreWebView2ContextMenuItem` returned by `GetSpellCheckSuggestionsAsync` | QI for EventArgs2 fails | Old runtime — use default menu | | `SPELL_CHECK` flag not set | No misspelling — skip spellcheck UI | | `GetDeferredCapability` returns `E_NOINTERFACE` | Capability not available (consistent with flags) | -| `MisspelledWord` returns empty string | Spellcheck applicable but word is empty — defensive check recommended | +| `MisspelledWord` returns empty string | Unexpected — indicates a runtime bug. Host should treat as "no spellcheck" and skip spellcheck UI | | Second call to `GetSpellCheckSuggestionsAsync` | Returns `E_ILLEGAL_METHOD_CALL` | | Suggestions handler — no suggestions available | `count == 0` — show "No suggestions" or skip | -| User dismisses menu without selecting | Set `SelectedCommandId` to 0 or simply complete the deferral | +| User dismisses menu without selecting | Do not set `SelectedCommandId` (its default value of −1 indicates no selection) and complete the deferral | # Appendix @@ -423,10 +532,10 @@ The deferred capability pattern introduced by this feature is designed for zero- | To add a new capability | Required changes | |------------------------|-----------------| -| New flag constant | for example - `COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES_EMOJI = 0x2` | +| New flag constant | `COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES_EMOJI = 0x2` | | New capability interface | `ICoreWebView2ContextMenuEmoji : IUnknown { ... }` | -## Planned SpellCheck Extensions +## Planned Spellcheck Extensions The following actions will be added as additional `ICoreWebView2ContextMenuItem` entries in the collection returned by `GetSpellCheckSuggestionsAsync`. No new interfaces or methods are required: From 77ae92ec6f7cfab14760ce4da6141a69ae5dc0d4 Mon Sep 17 00:00:00 2001 From: Anurag Kumar Date: Wed, 8 Apr 2026 17:21:32 +0530 Subject: [PATCH 11/17] review comment addressed --- specs/CustomContextMenuSpellcheck.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/specs/CustomContextMenuSpellcheck.md b/specs/CustomContextMenuSpellcheck.md index 754c57db..87394a88 100644 --- a/specs/CustomContextMenuSpellcheck.md +++ b/specs/CustomContextMenuSpellcheck.md @@ -87,7 +87,7 @@ webView->add_ContextMenuRequested( // ── Step 6: Get suggestions and build menu in the callback ── wil::com_ptr items; - args->get_MenuItems(&items); + CHECK_FAILURE(args->get_MenuItems(&items)); m_appWindow->RunAsync( [this, args, spellCheck, items, deferral, @@ -209,7 +209,7 @@ webView->add_ContextMenuRequested( &m_contextMenuRequestedToken); ``` -## .NET / C# — Display Custom Context Menu with Spellcheck Suggestions +## .NET/WinRT — Display Custom Context Menu with Spellcheck Suggestions ```csharp webView.CoreWebView2.ContextMenuRequested += async (sender, args) => @@ -313,7 +313,12 @@ if (caps & COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES_SPELL_CHECK) # API Details -## Win32 C++ +## Win32 COM IDL + +> **Note:** This spec uses COM IDL (MIDL2) syntax, consistent with the existing +> `WebView2.idl` and `WebView2Staging.idl` in the repository. Verify with the +> SDK team whether MIDL3 format is required for new specs submitted to Windows +> API review. ```idl // ─── EventArgs2: Deferred Capability Discovery (introduced for spellcheck) ─── @@ -431,7 +436,7 @@ interface ICoreWebView2ContextMenuSpellCheck : IUnknown { } ``` -## .NET / WinRT +## .NET/WinRT ```csharp namespace Microsoft.Web.WebView2.Core From bdea7683ca63c08f01ba80874529c89e949f838d Mon Sep 17 00:00:00 2001 From: Anurag Kumar Date: Wed, 8 Apr 2026 19:29:26 +0530 Subject: [PATCH 12/17] minor refactoring of spec --- specs/CustomContextMenuSpellcheck.md | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/specs/CustomContextMenuSpellcheck.md b/specs/CustomContextMenuSpellcheck.md index 87394a88..d63c4b59 100644 --- a/specs/CustomContextMenuSpellcheck.md +++ b/specs/CustomContextMenuSpellcheck.md @@ -315,11 +315,6 @@ if (caps & COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES_SPELL_CHECK) ## Win32 COM IDL -> **Note:** This spec uses COM IDL (MIDL2) syntax, consistent with the existing -> `WebView2.idl` and `WebView2Staging.idl` in the repository. Verify with the -> SDK team whether MIDL3 format is required for new specs submitted to Windows -> API review. - ```idl // ─── EventArgs2: Deferred Capability Discovery (introduced for spellcheck) ─── @@ -540,22 +535,6 @@ The deferred capability pattern introduced by this feature is designed for zero- | New flag constant | `COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES_EMOJI = 0x2` | | New capability interface | `ICoreWebView2ContextMenuEmoji : IUnknown { ... }` | -## Planned Spellcheck Extensions - -The following actions will be added as additional `ICoreWebView2ContextMenuItem` entries in the -collection returned by `GetSpellCheckSuggestionsAsync`. No new interfaces or methods are required: - -| Action | `Name` value | -|--------|-------------| -| Add to Dictionary | `"spellCheckAddToDictionary"` | -| Ignore (session) | `"spellCheckIgnore"` | - -These follow the same commanding model: the host renders them like any other item and applies via -`SelectedCommandId`. A `Language` property (BCP-47 tag of the dictionary that flagged the misspelling) -will also be added to `ICoreWebView2ContextMenuSpellCheck` in a follow-up version. Profile-level -spellcheck configuration (`IsSpellCheckEnabled`, `SpellCheckLanguages`) is tracked as a separate -follow-up. - ## Relationship to Existing APIs | Existing API | This Feature | From e29d67c832232c0e2219602c0103a1cc5e266a4e Mon Sep 17 00:00:00 2001 From: Anurag Kumar Date: Fri, 10 Apr 2026 19:11:41 +0530 Subject: [PATCH 13/17] review comments addressed --- specs/CustomContextMenuSpellcheck.md | 177 ++++++++++++++++++--------- 1 file changed, 121 insertions(+), 56 deletions(-) diff --git a/specs/CustomContextMenuSpellcheck.md b/specs/CustomContextMenuSpellcheck.md index d63c4b59..b6543740 100644 --- a/specs/CustomContextMenuSpellcheck.md +++ b/specs/CustomContextMenuSpellcheck.md @@ -11,9 +11,7 @@ these suggestions. This feature adds spellcheck support to custom context menus. Because spellcheck suggestions arrive asynchronously (after the `ContextMenuRequested` event fires), the feature introduces a deferred capability discovery pattern on `ICoreWebView2ContextMenuRequestedEventArgs2` that allows the host to -detect and acquire async capabilities at event time. Spellcheck is the first capability delivered -through this pattern; the same extensible mechanism will support future capabilities (emoji panel, -voice typing, etc.) without requiring additional EventArgs versions. +detect and acquire async capabilities at event time. # Description @@ -32,16 +30,16 @@ The spellcheck capability interface (`ICoreWebView2ContextMenuSpellCheck`) provi - **`MisspelledWord`** — Read-only property returning the misspelled word under the cursor. Useful for displaying "Suggestions for 'teh':" headers in custom menus. -- **`GetSpellCheckSuggestionsAsync`** — Retrieves suggestions as `ICoreWebView2ContextMenuItem` objects. +- **`GetSpellCheckSuggestions`** — Retrieves suggestions as `ICoreWebView2ContextMenuItem` objects. Each suggestion has a `Label` (display text) and `CommandId` (opaque identifier). **Commanding model:** The host applies a suggestion by passing its `CommandId` to `put_SelectedCommandId` on the EventArgs — the same execution path used for Cut, Copy, Paste, and all other context menu items. No separate execution method is needed. -**Async contract:** The handler fires exactly once, always asynchronously (posted to the caller's -message loop, never invoked inline). Only one handler may be registered; a second call returns -`E_ILLEGAL_METHOD_CALL`. +**Async contract:** This is a standard async method. The handler is invoked exactly once when +suggestions are available, always asynchronously (posted to the caller's message loop, never +invoked inline). **Availability:** If the `SPELL_CHECK` flag is not set in `DeferredCapabilities`, spellcheck is not applicable (non-editable field, correctly-spelled word, or spellcheck disabled by policy). @@ -93,7 +91,7 @@ webView->add_ContextMenuRequested( [this, args, spellCheck, items, deferral, word = std::wstring(misspelledWord.get())]() { - spellCheck->GetSpellCheckSuggestionsAsync( + spellCheck->GetSpellCheckSuggestions( Callback< ICoreWebView2GetSpellCheckSuggestionsCompletedHandler>( [this, args, items, deferral, word]( @@ -132,8 +130,12 @@ webView->add_ContextMenuRequested( hMenu, MF_SEPARATOR, 0, nullptr); } - // Add remaining standard context menu items, - // skipping built-in spellcheck entries. + // Add remaining standard context menu items. + // The MenuItems collection contains built-in + // spellcheck items (Name = "spellcheck") that + // duplicate the suggestions from + // GetSpellCheckSuggestions — filter them out + // to avoid showing duplicate entries. UINT32 itemCount = 0; items->get_Count(&itemCount); for (UINT32 i = 0; i < itemCount; i++) @@ -143,8 +145,9 @@ webView->add_ContextMenuRequested( items->GetValueAtIndex(i, &cur); wil::unique_cotaskmem_string name; cur->get_Name(&name); - // Skip built-in spellcheck items already - // handled above. + // Filter out built-in spellcheck items + // (Name = "spellcheck") — we render our + // own from GetSpellCheckSuggestions above. if (wcsstr(name.get(), L"spellCheck")) continue; COREWEBVIEW2_CONTEXT_MENU_ITEM_KIND kind; @@ -286,31 +289,6 @@ webView.CoreWebView2.ContextMenuRequested += async (sender, args) => }; ``` -## Win32 C++ — Check Multiple Deferred Capabilities (Future Pattern) - -When additional capabilities are added in the future, the same discovery pattern applies, no new -EventArgs versions are needed: - -```cpp -COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES caps; -args2->get_DeferredCapabilities(&caps); - -if (caps & COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES_SPELL_CHECK) -{ - wil::com_ptr spellCheck; - args2->GetDeferredCapability(IID_PPV_ARGS(&spellCheck)); - // ... use spellCheck ... -} - -// Future: no new EventArgs versions needed, just new flag constants. -// if (caps & COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES_EMOJI) -// { -// wil::com_ptr emoji; -// args2->GetDeferredCapability(IID_PPV_ARGS(&emoji)); -// // ... same pattern ... -// } -``` - # API Details ## Win32 COM IDL @@ -328,16 +306,10 @@ typedef enum COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES { /// Spellcheck is available — the target is an editable field with a /// misspelled word and spellcheck is enabled. COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES_SPELL_CHECK = 0x1, - // Future capabilities add new flag constants here. No interface changes. - // COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES_EMOJI = 0x2, - // COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES_VOICE_TYPING = 0x4, } COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES; /// Extends `ICoreWebView2ContextMenuRequestedEventArgs` with deferred capability -/// discovery and acquisition. Introduced as part of the spellcheck feature, this -/// extensible pattern supports all current and future deferred capabilities — -/// new capabilities add a flag constant and an interface definition, not new -/// EventArgs versions. +/// discovery and acquisition for spellcheck. /// /// The host checks `DeferredCapabilities` flags to discover what async /// capabilities exist for this invocation, then calls `GetDeferredCapability` @@ -365,7 +337,6 @@ interface ICoreWebView2ContextMenuRequestedEventArgs2 /// /// Supported IIDs: /// `IID_ICoreWebView2ContextMenuSpellCheck` - /// (future: emoji, voice typing, writing direction, etc.) /// /// The returned interface is valid for the lifetime of the event /// (until the deferral is completed). @@ -376,10 +347,10 @@ interface ICoreWebView2ContextMenuRequestedEventArgs2 // ─── Spellcheck Capability ─── -/// Receives the result of `GetSpellCheckSuggestionsAsync`. +/// Receives the result of `GetSpellCheckSuggestions`. /// The handler is invoked exactly once, always asynchronously (posted to the /// caller's message loop, never invoked inline during the call to -/// `GetSpellCheckSuggestionsAsync`). +/// `GetSpellCheckSuggestions`). [uuid(d73832f9-d05b-438d-bb6d-644124521fe3), object, pointer_default(unique)] interface ICoreWebView2GetSpellCheckSuggestionsCompletedHandler : IUnknown { /// Provides the result of the corresponding asynchronous method. @@ -406,7 +377,7 @@ interface ICoreWebView2GetSpellCheckSuggestionsCompletedHandler : IUnknown { interface ICoreWebView2ContextMenuSpellCheck : IUnknown { /// Gets the misspelled word at the current context menu target location. /// This is the word that spellcheck flagged as incorrect and for which - /// suggestions are available via `GetSpellCheckSuggestionsAsync`. + /// suggestions are available via `GetSpellCheckSuggestions`. /// The caller must free the returned string using `CoTaskMemFree`. /// /// Use this to display contextual headers like "Suggestions for 'teh':" @@ -423,10 +394,7 @@ interface ICoreWebView2ContextMenuSpellCheck : IUnknown { /// and `nullptr` on failure. Each item's `Label` is the suggestion text /// and its `CommandId` can be passed to `put_SelectedCommandId` to apply /// the correction. - /// - /// Only one handler may be registered per event invocation. A second call - /// returns `E_ILLEGAL_METHOD_CALL`. - HRESULT GetSpellCheckSuggestionsAsync( + HRESULT GetSpellCheckSuggestions( [in] ICoreWebView2GetSpellCheckSuggestionsCompletedHandler* handler); } ``` @@ -445,7 +413,6 @@ namespace Microsoft.Web.WebView2.Core { None = 0x0, SpellCheck = 0x1, - // Future: Emoji = 0x2, VoiceTyping = 0x4, ... } runtimeclass CoreWebView2ContextMenuRequestedEventArgs @@ -498,7 +465,7 @@ namespace Microsoft.Web.WebView2.Core ## Suggestion Item Properties -Each `ICoreWebView2ContextMenuItem` returned by `GetSpellCheckSuggestionsAsync` has: +Each `ICoreWebView2ContextMenuItem` returned by `GetSpellCheckSuggestions` has: | Property | Value | |----------|-------| @@ -512,6 +479,28 @@ Each `ICoreWebView2ContextMenuItem` returned by `GetSpellCheckSuggestionsAsync` | `ShortcutKeyDescription` | `L""` | | `Children` | `nullptr` | +## Relationship to Built-in Spellcheck Menu Items + +When a misspelled word is present, the `MenuItems` collection (from `get_MenuItems`) already +contains Chromium's **built-in** spellcheck suggestion items with `Name = "spellcheck"`. These +are the same suggestions that appear in the browser's default context menu. + +The items returned by `GetSpellCheckSuggestions` are **separate objects** with +`Name = "spellCheckSuggestion"` and different `CommandId` values. They represent the same +underlying suggestions but are delivered through the new async API with full +`ICoreWebView2ContextMenuItem` semantics. + +**Why both exist:** The built-in `"spellcheck"` items are part of the synchronous `MenuItems` +snapshot that all hosts already receive. The new `"spellCheckSuggestion"` items are delivered +asynchronously via `GetSpellCheckSuggestions` for hosts that want explicit control over +spellcheck rendering. Hosts using `GetSpellCheckSuggestions` should **filter out** the built-in +`"spellcheck"` items from `MenuItems` to avoid showing duplicate suggestions. + +**Placement guidance:** The built-in `"spellcheck"` items appear at the top of `MenuItems` +(matching browser default behavior). Hosts may insert the new suggestion items at the same +position by scanning `MenuItems` for the first `"spellcheck"` entry's index, or simply place +them at the top of their custom menu as shown in the examples above. + ## Error Handling | Scenario | Behavior | @@ -520,10 +509,49 @@ Each `ICoreWebView2ContextMenuItem` returned by `GetSpellCheckSuggestionsAsync` | `SPELL_CHECK` flag not set | No misspelling — skip spellcheck UI | | `GetDeferredCapability` returns `E_NOINTERFACE` | Capability not available (consistent with flags) | | `MisspelledWord` returns empty string | Unexpected — indicates a runtime bug. Host should treat as "no spellcheck" and skip spellcheck UI | -| Second call to `GetSpellCheckSuggestionsAsync` | Returns `E_ILLEGAL_METHOD_CALL` | +| Second call to `GetSpellCheckSuggestions` | Standard async behavior — each call registers its own handler | | Suggestions handler — no suggestions available | `count == 0` — show "No suggestions" or skip | | User dismisses menu without selecting | Do not set `SelectedCommandId` (its default value of −1 indicates no selection) and complete the deferral | +## Async Timing + +Spellcheck suggestions are resolved asynchronously by the platform spellchecker in the browser +process. When `ContextMenuRequested` fires, the `spellCheckState` may be: + +| State | Meaning | `GetSpellCheckSuggestions` behavior | +|-------|---------|-------------------------------------| +| **Ready** | Suggestions already resolved before the event fired | Handler fires on next message loop iteration (sub-millisecond) | +| **Not Ready** | Platform spellchecker still working | Handler is stored; fires when browser delivers results via IPC | + +The host does **not** need to check readiness — `GetSpellCheckSuggestions` handles both cases +transparently. However, the host should be aware that the handler may fire with a variable delay +in the Not Ready case, depending on the platform spellchecker's response time. + +### Host Patterns + +**Pattern 1: Wait-then-show** (simpler — used in the examples above) + +The host defers the context menu, calls `GetSpellCheckSuggestions`, and builds/shows the menu +only after the handler fires. This produces a complete menu in one shot but delays appearance +if suggestions are not yet ready. + +``` +ContextMenuRequested → put_Handled(TRUE) + GetDeferral → GetSpellCheckSuggestions + → [handler fires] → build & show menu → complete deferral +``` + +**Pattern 2: Show-then-update** (responsive — mirrors browser built-in behavior) + +The host shows the context menu immediately with a placeholder (e.g., "Loading suggestions…") +and updates it in-place when the handler fires. This keeps menu appearance instant at the cost +of added complexity. + +``` +ContextMenuRequested → put_Handled(TRUE) + GetDeferral → show menu with placeholder + → GetSpellCheckSuggestions → [handler fires] → update menu items in-place + → [user selects] → complete deferral +``` + # Appendix ## Extensibility @@ -535,6 +563,43 @@ The deferred capability pattern introduced by this feature is designed for zero- | New flag constant | `COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES_EMOJI = 0x2` | | New capability interface | `ICoreWebView2ContextMenuEmoji : IUnknown { ... }` | +Example of how a host would check multiple capabilities in the future: + +```cpp +COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES caps; +args2->get_DeferredCapabilities(&caps); + +if (caps & COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES_SPELL_CHECK) +{ + wil::com_ptr spellCheck; + args2->GetDeferredCapability(IID_PPV_ARGS(&spellCheck)); + // ... use spellCheck ... +} + +// Future: no new EventArgs versions needed, just new flag constants. +// if (caps & COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES_EMOJI) +// { +// wil::com_ptr emoji; +// args2->GetDeferredCapability(IID_PPV_ARGS(&emoji)); +// } +``` + +## Planned Spellcheck Extensions + +The following actions will be added as additional `ICoreWebView2ContextMenuItem` entries in the +collection returned by `GetSpellCheckSuggestions`. No new interfaces or methods are required: + +| Action | `Name` value | +|--------|-------------| +| Add to Dictionary | `"spellCheckAddToDictionary"` | +| Ignore (session) | `"spellCheckIgnore"` | + +These follow the same commanding model: the host renders them like any other item and applies via +`SelectedCommandId`. A `Language` property (BCP-47 tag of the dictionary that flagged the misspelling) +will also be added to `ICoreWebView2ContextMenuSpellCheck` in a follow-up version. Profile-level +spellcheck configuration (`IsSpellCheckEnabled`, `SpellCheckLanguages`) is tracked as a separate +follow-up. + ## Relationship to Existing APIs | Existing API | This Feature | @@ -543,4 +608,4 @@ The deferred capability pattern introduced by this feature is designed for zero- | `EventArgs.SelectedCommandId` | Execution path — now also used for spellcheck suggestions | | `ContextMenuItem.CommandId` | Already used for all items — spellcheck items join this pool | | `ContextMenuItem.Label` | Display text — spellcheck suggestions use this for the suggestion word | -| `EventArgs.GetDeferral()` | Must be held across the async `GetSpellCheckSuggestionsAsync` gap | +| `EventArgs.GetDeferral()` | Must be held across the async `GetSpellCheckSuggestions` gap | From d99166d5a631fd7dbcc1800ed76c5699e23e131e Mon Sep 17 00:00:00 2001 From: Anurag Kumar Date: Tue, 28 Apr 2026 12:18:22 +0530 Subject: [PATCH 14/17] updated approach and interfaces --- specs/CustomContextMenuSpellcheck.md | 586 +++++++++------------------ 1 file changed, 201 insertions(+), 385 deletions(-) diff --git a/specs/CustomContextMenuSpellcheck.md b/specs/CustomContextMenuSpellcheck.md index b6543740..061c23ae 100644 --- a/specs/CustomContextMenuSpellcheck.md +++ b/specs/CustomContextMenuSpellcheck.md @@ -8,42 +8,35 @@ suggestions for misspelled words are not available. The browser's built-in spell suggestions asynchronously, but there is no mechanism for custom context menu hosts to retrieve or apply these suggestions. -This feature adds spellcheck support to custom context menus. Because spellcheck suggestions arrive -asynchronously (after the `ContextMenuRequested` event fires), the feature introduces a deferred -capability discovery pattern on `ICoreWebView2ContextMenuRequestedEventArgs2` that allows the host to -detect and acquire async capabilities at event time. +This feature adds spellcheck support to custom context menus by extending +`ICoreWebView2ContextMenuTarget` with a new `ICoreWebView2ContextMenuTarget2` interface. The host +checks whether a misspelled word is present, then asynchronously retrieves spelling suggestions. # Description -The `ContextMenuRequested` event is extended with `ICoreWebView2ContextMenuRequestedEventArgs2`. +The `ICoreWebView2ContextMenuTarget` is extended with `ICoreWebView2ContextMenuTarget2`. This new interface provides: -- **`DeferredCapabilities`** — A flags bitmask indicating which async capabilities are available for - this specific context menu invocation. For spellcheck, the host checks the `SPELL_CHECK` flag. -- **`GetDeferredCapability(REFIID, void**)`** — An IID-based accessor to acquire the capability - interface. For spellcheck, the host passes `IID_ICoreWebView2ContextMenuSpellCheck`. +- **`HasMisspelledWord`** — Read-only BOOL property indicating whether the context menu target + contains a misspelled word. This is always available synchronously when the event fires. +- **`GetSpellCheckSuggestions(handler)`** — Asynchronously retrieves spellcheck suggestions as + `ICoreWebView2ContextMenuItem` objects. Each suggestion has a `Label` (display text) and + `CommandId` (opaque identifier). -**Runtime version detection:** If `QueryInterface` for `EventArgs2` returns `E_NOINTERFACE`, the host +**Runtime version detection:** If `QueryInterface` for `Target2` returns `E_NOINTERFACE`, the host is running on an older runtime that does not support this feature. -The spellcheck capability interface (`ICoreWebView2ContextMenuSpellCheck`) provides: - -- **`MisspelledWord`** — Read-only property returning the misspelled word under the cursor. Useful for - displaying "Suggestions for 'teh':" headers in custom menus. -- **`GetSpellCheckSuggestions`** — Retrieves suggestions as `ICoreWebView2ContextMenuItem` objects. - Each suggestion has a `Label` (display text) and `CommandId` (opaque identifier). +**Why async?** Spellcheck suggestions are resolved asynchronously by the platform spellchecker +(e.g., Windows `ISpellChecker`). When `ContextMenuRequested` fires, suggestions may not yet be +available. `GetSpellCheckSuggestions` handles this transparently — it invokes the handler +immediately if suggestions are ready, or waits for the platform spellchecker to deliver them. +If the platform spellchecker does not respond within an internal timeout, the handler is invoked +with an empty collection. **Commanding model:** The host applies a suggestion by passing its `CommandId` to `put_SelectedCommandId` on the EventArgs — the same execution path used for Cut, Copy, Paste, and all other context menu items. No separate execution method is needed. -**Async contract:** This is a standard async method. The handler is invoked exactly once when -suggestions are available, always asynchronously (posted to the caller's message loop, never -invoked inline). - -**Availability:** If the `SPELL_CHECK` flag is not set in `DeferredCapabilities`, spellcheck is not -applicable (non-editable field, correctly-spelled word, or spellcheck disabled by policy). - # Examples ## Win32 C++ — Display Custom Context Menu with Spellcheck Suggestions @@ -54,158 +47,104 @@ webView->add_ContextMenuRequested( [this](ICoreWebView2* sender, ICoreWebView2ContextMenuRequestedEventArgs* args) -> HRESULT { - // ── Step 1: Runtime version check ── - auto args2 = wil::try_com_query< - ICoreWebView2ContextMenuRequestedEventArgs2>(args); - if (!args2) + // ── Step 1: Get context menu target ── + wil::com_ptr target; + CHECK_FAILURE(args->get_ContextMenuTarget(&target)); + + // ── Step 2: Check for Target2 (runtime version check) ── + auto target2 = wil::try_com_query< + ICoreWebView2ContextMenuTarget2>(target); + if (!target2) return S_OK; // Old runtime — use default menu. - // ── Step 2: Discover deferred capabilities ── - COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES caps = - COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES_NONE; - CHECK_FAILURE(args2->get_DeferredCapabilities(&caps)); - - if (!(caps & - COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES_SPELL_CHECK)) + // ── Step 3: Check for misspelled word ── + BOOL hasMisspelledWord = FALSE; + CHECK_FAILURE(target2->get_HasMisspelledWord(&hasMisspelledWord)); + if (!hasMisspelledWord) return S_OK; // No misspelling — use default menu. - // ── Step 3: Acquire spellcheck interface ── - wil::com_ptr spellCheck; - CHECK_FAILURE( - args2->GetDeferredCapability(IID_PPV_ARGS(&spellCheck))); - - // ── Step 4: Take over rendering (only after confirming spellcheck) ── + // ── Step 4: Take over rendering and hold deferral ── CHECK_FAILURE(args->put_Handled(TRUE)); wil::com_ptr deferral; CHECK_FAILURE(args->GetDeferral(&deferral)); - // ── Step 5: Read misspelled word (synchronous) ── - wil::unique_cotaskmem_string misspelledWord; - CHECK_FAILURE(spellCheck->get_MisspelledWord(&misspelledWord)); - - // ── Step 6: Get suggestions and build menu in the callback ── + // ── Step 5: Get menu items for later use ── wil::com_ptr items; CHECK_FAILURE(args->get_MenuItems(&items)); - m_appWindow->RunAsync( - [this, args, spellCheck, items, deferral, - word = std::wstring(misspelledWord.get())]() - { - spellCheck->GetSpellCheckSuggestions( - Callback< - ICoreWebView2GetSpellCheckSuggestionsCompletedHandler>( - [this, args, items, deferral, word]( - HRESULT errorCode, - ICoreWebView2ContextMenuItemCollection* - suggestions) -> HRESULT + // ── Step 6: Get suggestions asynchronously and build menu ── + CHECK_FAILURE(target2->GetSpellCheckSuggestions( + Callback< + ICoreWebView2GetSpellCheckSuggestionsCompletedHandler>( + [this, args, items, deferral]( + HRESULT errorCode, + ICoreWebView2ContextMenuItemCollection* + suggestions) -> HRESULT + { + HMENU hMenu = CreatePopupMenu(); + + // Add spellcheck suggestions at the top. + UINT32 sugCount = 0; + if (SUCCEEDED(errorCode) && suggestions) + suggestions->get_Count(&sugCount); + + if (sugCount > 0) + { + AppendMenuW( + hMenu, MF_GRAYED | MF_STRING, 0, + L"Spelling suggestions:"); + for (UINT32 i = 0; i < sugCount; i++) { - HMENU hMenu = CreatePopupMenu(); - - // Add spellcheck suggestions at the top. - UINT32 sugCount = 0; - if (SUCCEEDED(errorCode) && suggestions) - suggestions->get_Count(&sugCount); - - if (sugCount > 0) - { - AppendMenuW( - hMenu, MF_GRAYED | MF_STRING, 0, - (L"Suggestions for '" + word + L"':") - .c_str()); - for (UINT32 i = 0; i < sugCount; i++) - { - wil::com_ptr< - ICoreWebView2ContextMenuItem> - item; - suggestions->GetValueAtIndex(i, &item); - wil::unique_cotaskmem_string label; - item->get_Label(&label); - INT32 cmdId; - item->get_CommandId(&cmdId); - AppendMenuW( - hMenu, MF_STRING, cmdId, - label.get()); - } - AppendMenuW( - hMenu, MF_SEPARATOR, 0, nullptr); - } - - // Add remaining standard context menu items. - // The MenuItems collection contains built-in - // spellcheck items (Name = "spellcheck") that - // duplicate the suggestions from - // GetSpellCheckSuggestions — filter them out - // to avoid showing duplicate entries. - UINT32 itemCount = 0; - items->get_Count(&itemCount); - for (UINT32 i = 0; i < itemCount; i++) - { - wil::com_ptr - cur; - items->GetValueAtIndex(i, &cur); - wil::unique_cotaskmem_string name; - cur->get_Name(&name); - // Filter out built-in spellcheck items - // (Name = "spellcheck") — we render our - // own from GetSpellCheckSuggestions above. - if (wcsstr(name.get(), L"spellCheck")) - continue; - COREWEBVIEW2_CONTEXT_MENU_ITEM_KIND kind; - cur->get_Kind(&kind); - if (kind == - COREWEBVIEW2_CONTEXT_MENU_ITEM_KIND_SEPARATOR) - { - AppendMenuW( - hMenu, MF_SEPARATOR, 0, nullptr); - } - else - { - wil::unique_cotaskmem_string label; - cur->get_Label(&label); - INT32 cmdId; - cur->get_CommandId(&cmdId); - BOOL enabled = FALSE; - cur->get_IsEnabled(&enabled); - AppendMenuW( - hMenu, - MF_STRING | - (enabled ? 0 : MF_GRAYED), - cmdId, label.get()); - } - } - - // Show popup at the context menu location. - HWND hWnd = m_appWindow->GetMainWindow(); - POINT pt; - wil::com_ptr - target; - args->get_ContextMenuTarget(&target); - POINT location; - args->get_Location(&location); - RECT bounds; - GetWindowRect(hWnd, &bounds); - // location is in WebView client coordinates; - // convert to screen. - pt = {bounds.left + location.x, - bounds.top + location.y}; - INT32 selectedCmd = TrackPopupMenu( - hMenu, - TPM_TOPALIGN | TPM_LEFTALIGN | - TPM_RETURNCMD, - pt.x, pt.y, 0, hWnd, nullptr); - - // ── Unified commanding ── - // Works for spellcheck suggestions AND - // standard items alike. - if (selectedCmd > 0) - args->put_SelectedCommandId(selectedCmd); - - DestroyMenu(hMenu); - deferral->Complete(); - return S_OK; - }) - .Get()); - }); + wil::com_ptr< + ICoreWebView2ContextMenuItem> + item; + suggestions->GetValueAtIndex(i, &item); + wil::unique_cotaskmem_string label; + item->get_Label(&label); + INT32 cmdId; + item->get_CommandId(&cmdId); + AppendMenuW( + hMenu, MF_STRING, cmdId, + label.get()); + } + AppendMenuW( + hMenu, MF_SEPARATOR, 0, nullptr); + } + else + { + AppendMenuW( + hMenu, MF_GRAYED | MF_STRING, 0, + L"No suggestions available"); + AppendMenuW( + hMenu, MF_SEPARATOR, 0, nullptr); + } + + // Add remaining standard context menu items. + AddMenuItems(hMenu, items.get()); + + // Show popup at the context menu location. + HWND hWnd = m_appWindow->GetMainWindow(); + POINT location; + args->get_Location(&location); + RECT bounds; + GetWindowRect(hWnd, &bounds); + POINT pt = {bounds.left + location.x, + bounds.top + location.y}; + INT32 selectedCmd = TrackPopupMenu( + hMenu, + TPM_TOPALIGN | TPM_LEFTALIGN | + TPM_RETURNCMD, + pt.x, pt.y, 0, hWnd, nullptr); + + // ── Unified commanding ── + if (selectedCmd > 0) + args->put_SelectedCommandId(selectedCmd); + + DestroyMenu(hMenu); + deferral->Complete(); + return S_OK; + }) + .Get())); return S_OK; }) .Get(), @@ -217,48 +156,53 @@ webView->add_ContextMenuRequested( ```csharp webView.CoreWebView2.ContextMenuRequested += async (sender, args) => { - // Step 1: Discover deferred capabilities. - var caps = args.DeferredCapabilities; - if (!caps.HasFlag(CoreWebView2ContextMenuDeferredCapabilities.SpellCheck)) + // Step 1: Get context menu target. + var target = args.ContextMenuTarget; + + // Step 2: Check for misspelled word (Target2 property). + if (!target.HasMisspelledWord) return; // No misspelling — let default menu show. - // Step 2: Take over menu rendering and hold deferral. + // Step 3: Take over menu rendering and hold deferral. args.Handled = true; var deferral = args.GetDeferral(); - // Step 3: Acquire spellcheck capability. - var spellCheck = args.GetDeferredCapability(); + // Step 4: Get suggestions asynchronously. + IReadOnlyList suggestions = + await target.GetSpellCheckSuggestionsAsync(); - // Step 4: Read misspelled word. - string misspelledWord = spellCheck.MisspelledWord; // e.g., "teh" - - // Step 5: Get suggestions. - var suggestions = await spellCheck.GetSpellCheckSuggestionsAsync(); - - // Step 6: Build custom menu. + // Step 5: Build custom menu. var contextMenu = new ContextMenuStrip(); bool completed = false; - contextMenu.Items.Add(new ToolStripMenuItem( - $"Suggestions for '{misspelledWord}':") { Enabled = false }); - - foreach (var suggestion in suggestions) + if (suggestions.Count > 0) { - var item = new ToolStripMenuItem(suggestion.Label); - var capturedId = suggestion.CommandId; - item.Click += (_, _) => + contextMenu.Items.Add(new ToolStripMenuItem( + "Spelling suggestions:") { Enabled = false }); + + foreach (var suggestion in suggestions) { - // Unified commanding — same as Cut, Copy, Paste. - args.SelectedCommandId = capturedId; - }; - contextMenu.Items.Add(item); + var item = new ToolStripMenuItem(suggestion.Label); + var capturedId = suggestion.CommandId; + item.Click += (_, _) => + { + // Unified commanding — same as Cut, Copy, Paste. + args.SelectedCommandId = capturedId; + }; + contextMenu.Items.Add(item); + } + contextMenu.Items.Add(new ToolStripSeparator()); + } + else + { + contextMenu.Items.Add(new ToolStripMenuItem( + "No suggestions available") { Enabled = false }); + contextMenu.Items.Add(new ToolStripSeparator()); } - // Add standard items, skipping built-in spellcheck entries. + // Add standard items. foreach (var menuItem in args.MenuItems) { - if (menuItem.Name.StartsWith("spellCheck")) - continue; if (menuItem.Kind == CoreWebView2ContextMenuItemKind.Separator) { contextMenu.Items.Add(new ToolStripSeparator()); @@ -276,7 +220,7 @@ webView.CoreWebView2.ContextMenuRequested += async (sender, args) => } } - // Complete deferral once on menu close (covers both selection and dismissal). + // Complete deferral on menu close. contextMenu.Closed += (_, _) => { if (!completed) @@ -294,64 +238,42 @@ webView.CoreWebView2.ContextMenuRequested += async (sender, args) => ## Win32 COM IDL ```idl -// ─── EventArgs2: Deferred Capability Discovery (introduced for spellcheck) ─── - -/// Flags indicating which deferred capabilities are available for a given -/// context menu invocation. Treat as a bitmask — test individual flags -/// with bitwise AND. -[v1_enum] -typedef enum COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES { - /// No deferred capabilities are available. - COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES_NONE = 0x0, - /// Spellcheck is available — the target is an editable field with a - /// misspelled word and spellcheck is enabled. - COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES_SPELL_CHECK = 0x1, -} COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES; - -/// Extends `ICoreWebView2ContextMenuRequestedEventArgs` with deferred capability -/// discovery and acquisition for spellcheck. +// ─── ContextMenuTarget2: Spellcheck support ─── + +/// Extends `ICoreWebView2ContextMenuTarget` with spellcheck support for +/// custom context menus. +/// +/// The host can `QueryInterface` the `ICoreWebView2ContextMenuTarget` returned +/// by `ICoreWebView2ContextMenuRequestedEventArgs::get_ContextMenuTarget` to +/// obtain this interface. Check `HasMisspelledWord` to determine whether +/// the context menu was invoked on a misspelled word, then call +/// `GetSpellCheckSuggestions` to asynchronously retrieve spelling corrections. /// -/// The host checks `DeferredCapabilities` flags to discover what async -/// capabilities exist for this invocation, then calls `GetDeferredCapability` -/// with the desired interface IID to acquire it. -[uuid(54ab63d2-9c3b-45a1-88d8-6c5a561784c9), object, pointer_default(unique)] -interface ICoreWebView2ContextMenuRequestedEventArgs2 - : ICoreWebView2ContextMenuRequestedEventArgs { - - /// Returns a bitmask of deferred capabilities available for this context - /// menu invocation. The host checks individual flags to determine which - /// capability interfaces can be acquired via `GetDeferredCapability`. - /// - /// A flag being set means the corresponding capability is applicable AND - /// enabled for this invocation. Flags not set means either the capability - /// is not applicable (e.g., no misspelling) or the capability is disabled - /// (e.g., spellcheck off by policy). - [propget] HRESULT DeferredCapabilities( - [out, retval] COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES* value); - - /// Retrieves the capability-specific interface for a deferred capability. - /// - /// Pass the IID of the desired capability interface. Returns `S_OK` and - /// the interface pointer if the capability is available. Returns - /// `E_NOINTERFACE` if the capability is not applicable for this invocation. - /// - /// Supported IIDs: - /// `IID_ICoreWebView2ContextMenuSpellCheck` - /// - /// The returned interface is valid for the lifetime of the event - /// (until the deferral is completed). - HRESULT GetDeferredCapability( - [in] REFIID riid, - [out, iid_is(riid), retval] void** capability); +/// To apply a suggestion, pass the selected item's `CommandId` to +/// `ICoreWebView2ContextMenuRequestedEventArgs::put_SelectedCommandId`. +[uuid(f7a3b8c1-2d4e-5f6a-8b9c-0d1e2f3a4b5c), object, pointer_default(unique)] +interface ICoreWebView2ContextMenuTarget2 : ICoreWebView2ContextMenuTarget { + /// Returns TRUE if the context menu target contains a misspelled word. + /// When TRUE, call `GetSpellCheckSuggestions` to retrieve the available + /// spelling correction suggestions asynchronously. + [propget] HRESULT HasMisspelledWord([out, retval] BOOL* value); + + /// Asynchronously retrieves spellcheck suggestion options as a collection + /// of context menu items. The handler is invoked immediately if suggestions + /// are already available, or when they become available from the platform + /// spellcheck engine. Each item's `Label` is the suggestion text and its + /// `CommandId` can be passed to `put_SelectedCommandId` to apply the + /// correction. The handler receives an empty collection if no suggestions + /// are available, if `HasMisspelledWord` is FALSE, or if the underlying + /// spellcheck service does not respond within an internal timeout. + /// Only one outstanding request is allowed; calling this method while a + /// previous request is pending returns `E_ILLEGAL_METHOD_CALL`. + HRESULT GetSpellCheckSuggestions( + [in] ICoreWebView2GetSpellCheckSuggestionsCompletedHandler* handler); } -// ─── Spellcheck Capability ─── - -/// Receives the result of `GetSpellCheckSuggestions`. -/// The handler is invoked exactly once, always asynchronously (posted to the -/// caller's message loop, never invoked inline during the call to -/// `GetSpellCheckSuggestions`). -[uuid(d73832f9-d05b-438d-bb6d-644124521fe3), object, pointer_default(unique)] +/// Receives the result of the `GetSpellCheckSuggestions` method. +[uuid(d73832f9-d05b-438d-bb6d-6441245221e3), object, pointer_default(unique)] interface ICoreWebView2GetSpellCheckSuggestionsCompletedHandler : IUnknown { /// Provides the result of the corresponding asynchronous method. /// Each item in the `suggestions` collection is an @@ -363,40 +285,6 @@ interface ICoreWebView2GetSpellCheckSuggestionsCompletedHandler : IUnknown { [in] HRESULT errorCode, [in] ICoreWebView2ContextMenuItemCollection* suggestions); } - -/// Provides spellcheck capabilities for custom context menus. Acquired via -/// `ICoreWebView2ContextMenuRequestedEventArgs2::GetDeferredCapability` -/// when the `SPELL_CHECK` flag is set in `DeferredCapabilities`. -/// -/// To apply a suggestion, retrieve the `CommandId` from the desired -/// `ICoreWebView2ContextMenuItem` in the suggestions collection and pass it -/// to `ICoreWebView2ContextMenuRequestedEventArgs.put_SelectedCommandId`. -/// This follows the same commanding model used for all other context menu -/// items (Cut, Copy, Paste, etc.). -[uuid(aa742569-d944-4510-8924-c2c0583fa320), object, pointer_default(unique)] -interface ICoreWebView2ContextMenuSpellCheck : IUnknown { - /// Gets the misspelled word at the current context menu target location. - /// This is the word that spellcheck flagged as incorrect and for which - /// suggestions are available via `GetSpellCheckSuggestions`. - /// The caller must free the returned string using `CoTaskMemFree`. - /// - /// Use this to display contextual headers like "Suggestions for 'teh':" - /// in your custom context menu. - [propget] HRESULT MisspelledWord([out, retval] LPWSTR* value); - - /// Asynchronously retrieves spellcheck suggestions for the misspelled word - /// at the current context menu target. The `handler` is invoked exactly once - /// when suggestions become available, always asynchronously (posted to the - /// caller's message loop, never invoked inline during this call). - /// - /// The handler receives `S_OK` and a collection of - /// `ICoreWebView2ContextMenuItem` objects on success, or an error HRESULT - /// and `nullptr` on failure. Each item's `Label` is the suggestion text - /// and its `CommandId` can be passed to `put_SelectedCommandId` to apply - /// the correction. - HRESULT GetSpellCheckSuggestions( - [in] ICoreWebView2GetSpellCheckSuggestionsCompletedHandler* handler); -} ``` ## .NET/WinRT @@ -404,64 +292,37 @@ interface ICoreWebView2ContextMenuSpellCheck : IUnknown { ```csharp namespace Microsoft.Web.WebView2.Core { - /// - /// Flags indicating which deferred capabilities are available for a given - /// context menu invocation. - /// - [Flags] - enum CoreWebView2ContextMenuDeferredCapabilities - { - None = 0x0, - SpellCheck = 0x1, - } - - runtimeclass CoreWebView2ContextMenuRequestedEventArgs + runtimeclass CoreWebView2ContextMenuTarget { // Existing members unchanged. - [interface_name("ICoreWebView2ContextMenuRequestedEventArgs2")] + [interface_name("ICoreWebView2ContextMenuTarget2")] { /// - /// Returns a bitmask of deferred capabilities available for this - /// context menu invocation. + /// Returns TRUE if the context menu target contains a misspelled word. /// - CoreWebView2ContextMenuDeferredCapabilities DeferredCapabilities { get; }; + Boolean HasMisspelledWord { get; }; /// - /// Retrieves the capability-specific interface for a deferred - /// capability. Returns null if the capability is not applicable. + /// Asynchronously retrieves spellcheck suggestions. Each item's + /// CommandId can be passed to SelectedCommandId to apply the correction. /// - T GetDeferredCapability(); + Windows.Foundation.IAsyncOperation> + GetSpellCheckSuggestionsAsync(); } } - - runtimeclass CoreWebView2ContextMenuSpellCheck - { - /// - /// The misspelled word under the cursor. - /// - String MisspelledWord { get; }; - - /// - /// Asynchronously retrieves spellcheck suggestions. Each item's - /// CommandId can be passed to SelectedCommandId to apply the correction. - /// - Windows.Foundation.IAsyncOperation> - GetSpellCheckSuggestionsAsync(); - } } ``` # Behavioral Details -## Discovery and Acquisition Flow +## Discovery Flow | Step | Action | Result | |------|--------|--------| -| 1 | QI for `EventArgs2` from EventArgs | `E_NOINTERFACE` → old runtime, fall back to default menu | -| 2 | Read `DeferredCapabilities` flags | Bitmask of available capabilities for this invocation | -| 3 | Check `SPELL_CHECK` flag | Flag set → misspelling present; flag not set → no misspelling | -| 4 | Call `GetDeferredCapability(IID_SpellCheck)` | Returns spellcheck interface with `AddRef` | +| 1 | QI for `Target2` from `ContextMenuTarget` | `E_NOINTERFACE` → old runtime, fall back to default menu | +| 2 | Read `HasMisspelledWord` | `TRUE` → misspelling present; `FALSE` → no misspelling | +| 3 | Call `GetSpellCheckSuggestions(handler)` | Handler invoked when suggestions are available | ## Suggestion Item Properties @@ -479,53 +340,32 @@ Each `ICoreWebView2ContextMenuItem` returned by `GetSpellCheckSuggestions` has: | `ShortcutKeyDescription` | `L""` | | `Children` | `nullptr` | -## Relationship to Built-in Spellcheck Menu Items - -When a misspelled word is present, the `MenuItems` collection (from `get_MenuItems`) already -contains Chromium's **built-in** spellcheck suggestion items with `Name = "spellcheck"`. These -are the same suggestions that appear in the browser's default context menu. - -The items returned by `GetSpellCheckSuggestions` are **separate objects** with -`Name = "spellCheckSuggestion"` and different `CommandId` values. They represent the same -underlying suggestions but are delivered through the new async API with full -`ICoreWebView2ContextMenuItem` semantics. - -**Why both exist:** The built-in `"spellcheck"` items are part of the synchronous `MenuItems` -snapshot that all hosts already receive. The new `"spellCheckSuggestion"` items are delivered -asynchronously via `GetSpellCheckSuggestions` for hosts that want explicit control over -spellcheck rendering. Hosts using `GetSpellCheckSuggestions` should **filter out** the built-in -`"spellcheck"` items from `MenuItems` to avoid showing duplicate suggestions. - -**Placement guidance:** The built-in `"spellcheck"` items appear at the top of `MenuItems` -(matching browser default behavior). Hosts may insert the new suggestion items at the same -position by scanning `MenuItems` for the first `"spellcheck"` entry's index, or simply place -them at the top of their custom menu as shown in the examples above. - ## Error Handling | Scenario | Behavior | |----------|----------| -| QI for EventArgs2 fails | Old runtime — use default menu | -| `SPELL_CHECK` flag not set | No misspelling — skip spellcheck UI | -| `GetDeferredCapability` returns `E_NOINTERFACE` | Capability not available (consistent with flags) | -| `MisspelledWord` returns empty string | Unexpected — indicates a runtime bug. Host should treat as "no spellcheck" and skip spellcheck UI | -| Second call to `GetSpellCheckSuggestions` | Standard async behavior — each call registers its own handler | +| QI for Target2 fails | Old runtime — use default menu | +| `HasMisspelledWord` is FALSE | No misspelling — skip spellcheck UI | +| `GetSpellCheckSuggestions` with null handler | Returns `E_POINTER` | +| Concurrent call to `GetSpellCheckSuggestions` | Returns `E_ILLEGAL_METHOD_CALL` | | Suggestions handler — no suggestions available | `count == 0` — show "No suggestions" or skip | -| User dismisses menu without selecting | Do not set `SelectedCommandId` (its default value of −1 indicates no selection) and complete the deferral | +| Platform spellchecker does not respond | Handler invoked with empty collection after internal timeout | +| User dismisses menu without selecting | Do not set `SelectedCommandId` (default −1) and complete the deferral | ## Async Timing Spellcheck suggestions are resolved asynchronously by the platform spellchecker in the browser -process. When `ContextMenuRequested` fires, the `spellCheckState` may be: +process. When `ContextMenuRequested` fires, the suggestions may be: | State | Meaning | `GetSpellCheckSuggestions` behavior | |-------|---------|-------------------------------------| -| **Ready** | Suggestions already resolved before the event fired | Handler fires on next message loop iteration (sub-millisecond) | -| **Not Ready** | Platform spellchecker still working | Handler is stored; fires when browser delivers results via IPC | +| **Ready** | Suggestions already resolved before the event fired | Handler invoked immediately | +| **Not Ready** | Platform spellchecker still working | Handler stored; invoked when browser delivers results via IPC, or after internal timeout with empty collection | The host does **not** need to check readiness — `GetSpellCheckSuggestions` handles both cases -transparently. However, the host should be aware that the handler may fire with a variable delay -in the Not Ready case, depending on the platform spellchecker's response time. +transparently. In the typical case, the platform spellchecker responds within a few milliseconds. +The internal timeout is a conservative safeguard for edge cases where the platform spellchecker +is slow or unresponsive. ### Host Patterns @@ -544,7 +384,8 @@ ContextMenuRequested → put_Handled(TRUE) + GetDeferral → GetSpellCheckSugges The host shows the context menu immediately with a placeholder (e.g., "Loading suggestions…") and updates it in-place when the handler fires. This keeps menu appearance instant at the cost -of added complexity. +of added complexity. Since the host owns the custom context menu UI, it can modify the menu +while it is open. ``` ContextMenuRequested → put_Handled(TRUE) + GetDeferral → show menu with placeholder @@ -552,37 +393,11 @@ ContextMenuRequested → put_Handled(TRUE) + GetDeferral → show menu with plac → [user selects] → complete deferral ``` -# Appendix - -## Extensibility - -The deferred capability pattern introduced by this feature is designed for zero-versioning extension: - -| To add a new capability | Required changes | -|------------------------|-----------------| -| New flag constant | `COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES_EMOJI = 0x2` | -| New capability interface | `ICoreWebView2ContextMenuEmoji : IUnknown { ... }` | - -Example of how a host would check multiple capabilities in the future: - -```cpp -COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES caps; -args2->get_DeferredCapabilities(&caps); - -if (caps & COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES_SPELL_CHECK) -{ - wil::com_ptr spellCheck; - args2->GetDeferredCapability(IID_PPV_ARGS(&spellCheck)); - // ... use spellCheck ... -} +Either pattern is valid. Pattern 1 is recommended for most hosts because the delay is typically +imperceptible (suggestions often resolve before the event fires or within a few milliseconds +after). Pattern 2 is appropriate for hosts that require guaranteed instant menu appearance. -// Future: no new EventArgs versions needed, just new flag constants. -// if (caps & COREWEBVIEW2_CONTEXT_MENU_DEFERRED_CAPABILITIES_EMOJI) -// { -// wil::com_ptr emoji; -// args2->GetDeferredCapability(IID_PPV_ARGS(&emoji)); -// } -``` +# Appendix ## Planned Spellcheck Extensions @@ -596,7 +411,7 @@ collection returned by `GetSpellCheckSuggestions`. No new interfaces or methods These follow the same commanding model: the host renders them like any other item and applies via `SelectedCommandId`. A `Language` property (BCP-47 tag of the dictionary that flagged the misspelling) -will also be added to `ICoreWebView2ContextMenuSpellCheck` in a follow-up version. Profile-level +may also be added to `ICoreWebView2ContextMenuTarget2` in a follow-up version. Profile-level spellcheck configuration (`IsSpellCheckEnabled`, `SpellCheckLanguages`) is tracked as a separate follow-up. @@ -604,8 +419,9 @@ follow-up. | Existing API | This Feature | |-------------|-------------| -| `EventArgs.MenuItems` | Synchronous snapshot of menu items | +| `EventArgs.MenuItems` | Synchronous snapshot of menu items | | `EventArgs.SelectedCommandId` | Execution path — now also used for spellcheck suggestions | | `ContextMenuItem.CommandId` | Already used for all items — spellcheck items join this pool | | `ContextMenuItem.Label` | Display text — spellcheck suggestions use this for the suggestion word | | `EventArgs.GetDeferral()` | Must be held across the async `GetSpellCheckSuggestions` gap | +| `ContextMenuTarget` | Base target — QI to `Target2` for spellcheck support | From b4fc535dc75d1b3c1d38ae57ada4fea58a5999ce Mon Sep 17 00:00:00 2001 From: Anurag Kumar Date: Tue, 28 Apr 2026 12:32:49 +0530 Subject: [PATCH 15/17] minor sample usage refactoring --- specs/CustomContextMenuSpellcheck.md | 165 ++++++--------------------- 1 file changed, 38 insertions(+), 127 deletions(-) diff --git a/specs/CustomContextMenuSpellcheck.md b/specs/CustomContextMenuSpellcheck.md index 061c23ae..018a310e 100644 --- a/specs/CustomContextMenuSpellcheck.md +++ b/specs/CustomContextMenuSpellcheck.md @@ -39,7 +39,7 @@ all other context menu items. No separate execution method is needed. # Examples -## Win32 C++ — Display Custom Context Menu with Spellcheck Suggestions +## Win32 C++ ```cpp webView->add_ContextMenuRequested( @@ -47,100 +47,54 @@ webView->add_ContextMenuRequested( [this](ICoreWebView2* sender, ICoreWebView2ContextMenuRequestedEventArgs* args) -> HRESULT { - // ── Step 1: Get context menu target ── wil::com_ptr target; CHECK_FAILURE(args->get_ContextMenuTarget(&target)); - // ── Step 2: Check for Target2 (runtime version check) ── + // QI for Target2 — returns E_NOINTERFACE on older runtimes. auto target2 = wil::try_com_query< ICoreWebView2ContextMenuTarget2>(target); if (!target2) - return S_OK; // Old runtime — use default menu. + return S_OK; - // ── Step 3: Check for misspelled word ── + // Check if the context menu target has a misspelled word. BOOL hasMisspelledWord = FALSE; CHECK_FAILURE(target2->get_HasMisspelledWord(&hasMisspelledWord)); if (!hasMisspelledWord) - return S_OK; // No misspelling — use default menu. + return S_OK; - // ── Step 4: Take over rendering and hold deferral ── + // Take deferral — menu will be shown after async callback. CHECK_FAILURE(args->put_Handled(TRUE)); wil::com_ptr deferral; CHECK_FAILURE(args->GetDeferral(&deferral)); - // ── Step 5: Get menu items for later use ── - wil::com_ptr items; - CHECK_FAILURE(args->get_MenuItems(&items)); - - // ── Step 6: Get suggestions asynchronously and build menu ── + // Asynchronously retrieve spellcheck suggestions. CHECK_FAILURE(target2->GetSpellCheckSuggestions( Callback< ICoreWebView2GetSpellCheckSuggestionsCompletedHandler>( - [this, args, items, deferral]( + [args, deferral]( HRESULT errorCode, ICoreWebView2ContextMenuItemCollection* suggestions) -> HRESULT { - HMENU hMenu = CreatePopupMenu(); - - // Add spellcheck suggestions at the top. - UINT32 sugCount = 0; + // Enumerate suggestions — each has Label and CommandId. + UINT32 count = 0; if (SUCCEEDED(errorCode) && suggestions) - suggestions->get_Count(&sugCount); + suggestions->get_Count(&count); - if (sugCount > 0) - { - AppendMenuW( - hMenu, MF_GRAYED | MF_STRING, 0, - L"Spelling suggestions:"); - for (UINT32 i = 0; i < sugCount; i++) - { - wil::com_ptr< - ICoreWebView2ContextMenuItem> - item; - suggestions->GetValueAtIndex(i, &item); - wil::unique_cotaskmem_string label; - item->get_Label(&label); - INT32 cmdId; - item->get_CommandId(&cmdId); - AppendMenuW( - hMenu, MF_STRING, cmdId, - label.get()); - } - AppendMenuW( - hMenu, MF_SEPARATOR, 0, nullptr); - } - else + for (UINT32 i = 0; i < count; i++) { - AppendMenuW( - hMenu, MF_GRAYED | MF_STRING, 0, - L"No suggestions available"); - AppendMenuW( - hMenu, MF_SEPARATOR, 0, nullptr); + wil::com_ptr item; + suggestions->GetValueAtIndex(i, &item); + wil::unique_cotaskmem_string label; + item->get_Label(&label); + INT32 cmdId; + item->get_CommandId(&cmdId); + // ... add to custom menu using label and cmdId ... } - // Add remaining standard context menu items. - AddMenuItems(hMenu, items.get()); - - // Show popup at the context menu location. - HWND hWnd = m_appWindow->GetMainWindow(); - POINT location; - args->get_Location(&location); - RECT bounds; - GetWindowRect(hWnd, &bounds); - POINT pt = {bounds.left + location.x, - bounds.top + location.y}; - INT32 selectedCmd = TrackPopupMenu( - hMenu, - TPM_TOPALIGN | TPM_LEFTALIGN | - TPM_RETURNCMD, - pt.x, pt.y, 0, hWnd, nullptr); - - // ── Unified commanding ── - if (selectedCmd > 0) - args->put_SelectedCommandId(selectedCmd); - - DestroyMenu(hMenu); + // Apply selection via unified commanding. + // args->put_SelectedCommandId(selectedCmdId); + deferral->Complete(); return S_OK; }) @@ -151,84 +105,41 @@ webView->add_ContextMenuRequested( &m_contextMenuRequestedToken); ``` -## .NET/WinRT — Display Custom Context Menu with Spellcheck Suggestions +## .NET/WinRT ```csharp webView.CoreWebView2.ContextMenuRequested += async (sender, args) => { - // Step 1: Get context menu target. var target = args.ContextMenuTarget; - // Step 2: Check for misspelled word (Target2 property). + // Check if the context menu target has a misspelled word. if (!target.HasMisspelledWord) - return; // No misspelling — let default menu show. + return; - // Step 3: Take over menu rendering and hold deferral. + // Take deferral — menu will be shown after async call completes. args.Handled = true; var deferral = args.GetDeferral(); - // Step 4: Get suggestions asynchronously. + // Asynchronously retrieve spellcheck suggestions. IReadOnlyList suggestions = await target.GetSpellCheckSuggestionsAsync(); - // Step 5: Build custom menu. + // Build custom menu with suggestions. var contextMenu = new ContextMenuStrip(); - bool completed = false; - - if (suggestions.Count > 0) + foreach (var suggestion in suggestions) { - contextMenu.Items.Add(new ToolStripMenuItem( - "Spelling suggestions:") { Enabled = false }); - - foreach (var suggestion in suggestions) + var item = new ToolStripMenuItem(suggestion.Label); + var capturedId = suggestion.CommandId; + item.Click += (_, _) => { - var item = new ToolStripMenuItem(suggestion.Label); - var capturedId = suggestion.CommandId; - item.Click += (_, _) => - { - // Unified commanding — same as Cut, Copy, Paste. - args.SelectedCommandId = capturedId; - }; - contextMenu.Items.Add(item); - } - contextMenu.Items.Add(new ToolStripSeparator()); - } - else - { - contextMenu.Items.Add(new ToolStripMenuItem( - "No suggestions available") { Enabled = false }); - contextMenu.Items.Add(new ToolStripSeparator()); + // Apply selection via unified commanding. + args.SelectedCommandId = capturedId; + }; + contextMenu.Items.Add(item); } - // Add standard items. - foreach (var menuItem in args.MenuItems) - { - if (menuItem.Kind == CoreWebView2ContextMenuItemKind.Separator) - { - contextMenu.Items.Add(new ToolStripSeparator()); - } - else - { - var stdItem = new ToolStripMenuItem(menuItem.Label); - stdItem.Enabled = menuItem.IsEnabled; - var stdCmdId = menuItem.CommandId; - stdItem.Click += (_, _) => - { - args.SelectedCommandId = stdCmdId; - }; - contextMenu.Items.Add(stdItem); - } - } - - // Complete deferral on menu close. - contextMenu.Closed += (_, _) => - { - if (!completed) - { - completed = true; - deferral.Complete(); - } - }; + // Show menu and complete deferral when closed. + contextMenu.Closed += (_, _) => deferral.Complete(); contextMenu.Show(webView, new Point(args.Location.X, args.Location.Y)); }; ``` From 45db244c6d38f30a6af2dd9d90686f81c7745330 Mon Sep 17 00:00:00 2001 From: Anurag Kumar Date: Tue, 28 Apr 2026 18:41:41 +0530 Subject: [PATCH 16/17] review addressed --- specs/CustomContextMenuSpellcheck.md | 84 ++++++++++++---------------- 1 file changed, 36 insertions(+), 48 deletions(-) diff --git a/specs/CustomContextMenuSpellcheck.md b/specs/CustomContextMenuSpellcheck.md index 018a310e..130080ef 100644 --- a/specs/CustomContextMenuSpellcheck.md +++ b/specs/CustomContextMenuSpellcheck.md @@ -1,14 +1,14 @@ -Spellcheck Support for Custom Context Menus +Spell Check Support for Custom Context Menus === # Background -When a host application renders a custom context menu via the `ContextMenuRequested` event, spellcheck -suggestions for misspelled words are not available. The browser's built-in spellcheck pipeline resolves +When a host application renders a custom context menu via the `ContextMenuRequested` event, spell check +suggestions for misspelled words are not available. The browser's built-in spell check pipeline resolves suggestions asynchronously, but there is no mechanism for custom context menu hosts to retrieve or apply these suggestions. -This feature adds spellcheck support to custom context menus by extending +This feature adds spell check support to custom context menus by extending `ICoreWebView2ContextMenuTarget` with a new `ICoreWebView2ContextMenuTarget2` interface. The host checks whether a misspelled word is present, then asynchronously retrieves spelling suggestions. @@ -19,18 +19,18 @@ This new interface provides: - **`HasMisspelledWord`** — Read-only BOOL property indicating whether the context menu target contains a misspelled word. This is always available synchronously when the event fires. -- **`GetSpellCheckSuggestions(handler)`** — Asynchronously retrieves spellcheck suggestions as +- **`GetSpellCheckSuggestions(handler)`** — Asynchronously retrieves spell check suggestions as `ICoreWebView2ContextMenuItem` objects. Each suggestion has a `Label` (display text) and `CommandId` (opaque identifier). -**Runtime version detection:** If `QueryInterface` for `Target2` returns `E_NOINTERFACE`, the host +**Runtime version detection:** If `QueryInterface` (QI) for `Target2` returns `E_NOINTERFACE`, the host is running on an older runtime that does not support this feature. -**Why async?** Spellcheck suggestions are resolved asynchronously by the platform spellchecker +**Why async?** Spell check suggestions are resolved asynchronously by the platform spell checker (e.g., Windows `ISpellChecker`). When `ContextMenuRequested` fires, suggestions may not yet be available. `GetSpellCheckSuggestions` handles this transparently — it invokes the handler -immediately if suggestions are ready, or waits for the platform spellchecker to deliver them. -If the platform spellchecker does not respond within an internal timeout, the handler is invoked +immediately if suggestions are ready, or waits for the platform spell checker to deliver them. +If the platform spell checker does not respond within an internal timeout, the handler is invoked with an empty collection. **Commanding model:** The host applies a suggestion by passing its `CommandId` to @@ -63,11 +63,11 @@ webView->add_ContextMenuRequested( return S_OK; // Take deferral — menu will be shown after async callback. - CHECK_FAILURE(args->put_Handled(TRUE)); wil::com_ptr deferral; CHECK_FAILURE(args->GetDeferral(&deferral)); + CHECK_FAILURE(args->put_Handled(true)); - // Asynchronously retrieve spellcheck suggestions. + // Asynchronously retrieve spell check suggestions. CHECK_FAILURE(target2->GetSpellCheckSuggestions( Callback< ICoreWebView2GetSpellCheckSuggestionsCompletedHandler>( @@ -117,10 +117,10 @@ webView.CoreWebView2.ContextMenuRequested += async (sender, args) => return; // Take deferral — menu will be shown after async call completes. - args.Handled = true; var deferral = args.GetDeferral(); + args.Handled = true; - // Asynchronously retrieve spellcheck suggestions. + // Asynchronously retrieve spell check suggestions. IReadOnlyList suggestions = await target.GetSpellCheckSuggestionsAsync(); @@ -149,9 +149,9 @@ webView.CoreWebView2.ContextMenuRequested += async (sender, args) => ## Win32 COM IDL ```idl -// ─── ContextMenuTarget2: Spellcheck support ─── +// ─── ContextMenuTarget2: Spell check support ─── -/// Extends `ICoreWebView2ContextMenuTarget` with spellcheck support for +/// Extends `ICoreWebView2ContextMenuTarget` with spell check support for /// custom context menus. /// /// The host can `QueryInterface` the `ICoreWebView2ContextMenuTarget` returned @@ -169,16 +169,16 @@ interface ICoreWebView2ContextMenuTarget2 : ICoreWebView2ContextMenuTarget { /// spelling correction suggestions asynchronously. [propget] HRESULT HasMisspelledWord([out, retval] BOOL* value); - /// Asynchronously retrieves spellcheck suggestion options as a collection + /// Asynchronously retrieves spell check suggestion options as a collection /// of context menu items. The handler is invoked immediately if suggestions /// are already available, or when they become available from the platform - /// spellcheck engine. Each item's `Label` is the suggestion text and its + /// spell check engine. Each item's `Label` is the suggestion text and its /// `CommandId` can be passed to `put_SelectedCommandId` to apply the /// correction. The handler receives an empty collection if no suggestions /// are available, if `HasMisspelledWord` is FALSE, or if the underlying - /// spellcheck service does not respond within an internal timeout. - /// Only one outstanding request is allowed; calling this method while a - /// previous request is pending returns `E_ILLEGAL_METHOD_CALL`. + /// spell check service does not respond within an internal timeout. + /// Returns `E_POINTER` if `handler` is null. + /// Returns `E_ILLEGAL_METHOD_CALL` if a previous request is still pending. HRESULT GetSpellCheckSuggestions( [in] ICoreWebView2GetSpellCheckSuggestionsCompletedHandler* handler); } @@ -215,7 +215,7 @@ namespace Microsoft.Web.WebView2.Core Boolean HasMisspelledWord { get; }; /// - /// Asynchronously retrieves spellcheck suggestions. Each item's + /// Asynchronously retrieves spell check suggestions. Each item's /// CommandId can be passed to SelectedCommandId to apply the correction. /// Windows.Foundation.IAsyncOperation> @@ -245,37 +245,25 @@ Each `ICoreWebView2ContextMenuItem` returned by `GetSpellCheckSuggestions` has: | `CommandId` | WebView2-allocated opaque ID (e.g., 50001) | | `Name` | `"spellCheckSuggestion"` | | `Kind` | `COREWEBVIEW2_CONTEXT_MENU_ITEM_KIND_COMMAND` | -| `IsEnabled` | `TRUE` | -| `IsChecked` | `FALSE` | -| `Icon` | `nullptr` | -| `ShortcutKeyDescription` | `L""` | -| `Children` | `nullptr` | - -## Error Handling - -| Scenario | Behavior | -|----------|----------| -| QI for Target2 fails | Old runtime — use default menu | -| `HasMisspelledWord` is FALSE | No misspelling — skip spellcheck UI | -| `GetSpellCheckSuggestions` with null handler | Returns `E_POINTER` | -| Concurrent call to `GetSpellCheckSuggestions` | Returns `E_ILLEGAL_METHOD_CALL` | -| Suggestions handler — no suggestions available | `count == 0` — show "No suggestions" or skip | -| Platform spellchecker does not respond | Handler invoked with empty collection after internal timeout | -| User dismisses menu without selecting | Do not set `SelectedCommandId` (default −1) and complete the deferral | +| `IsEnabled` | true | +| `IsChecked` | false | +| `Icon` | null | +| `ShortcutKeyDescription` | empty string | +| `Children` | null | ## Async Timing -Spellcheck suggestions are resolved asynchronously by the platform spellchecker in the browser +Spell check suggestions are resolved asynchronously by the platform spell checker in the browser process. When `ContextMenuRequested` fires, the suggestions may be: | State | Meaning | `GetSpellCheckSuggestions` behavior | |-------|---------|-------------------------------------| | **Ready** | Suggestions already resolved before the event fired | Handler invoked immediately | -| **Not Ready** | Platform spellchecker still working | Handler stored; invoked when browser delivers results via IPC, or after internal timeout with empty collection | +| **Not Ready** | Platform spell checker still working | Handler stored; invoked when browser delivers results via IPC, or after internal timeout with empty collection | The host does **not** need to check readiness — `GetSpellCheckSuggestions` handles both cases -transparently. In the typical case, the platform spellchecker responds within a few milliseconds. -The internal timeout is a conservative safeguard for edge cases where the platform spellchecker +transparently. In the typical case, the platform spell checker responds within a few milliseconds. +The internal timeout is a conservative safeguard for rare scenarios where the platform spell checker is slow or unresponsive. ### Host Patterns @@ -310,7 +298,7 @@ after). Pattern 2 is appropriate for hosts that require guaranteed instant menu # Appendix -## Planned Spellcheck Extensions +## Planned Spell Check Extensions The following actions will be added as additional `ICoreWebView2ContextMenuItem` entries in the collection returned by `GetSpellCheckSuggestions`. No new interfaces or methods are required: @@ -323,7 +311,7 @@ collection returned by `GetSpellCheckSuggestions`. No new interfaces or methods These follow the same commanding model: the host renders them like any other item and applies via `SelectedCommandId`. A `Language` property (BCP-47 tag of the dictionary that flagged the misspelling) may also be added to `ICoreWebView2ContextMenuTarget2` in a follow-up version. Profile-level -spellcheck configuration (`IsSpellCheckEnabled`, `SpellCheckLanguages`) is tracked as a separate +spell check configuration (`IsSpellCheckEnabled`, `SpellCheckLanguages`) is tracked as a separate follow-up. ## Relationship to Existing APIs @@ -331,8 +319,8 @@ follow-up. | Existing API | This Feature | |-------------|-------------| | `EventArgs.MenuItems` | Synchronous snapshot of menu items | -| `EventArgs.SelectedCommandId` | Execution path — now also used for spellcheck suggestions | -| `ContextMenuItem.CommandId` | Already used for all items — spellcheck items join this pool | -| `ContextMenuItem.Label` | Display text — spellcheck suggestions use this for the suggestion word | +| `EventArgs.SelectedCommandId` | Execution path — now also used for spell check suggestions | +| `ContextMenuItem.CommandId` | Already used for all items — spell check items join this pool | +| `ContextMenuItem.Label` | Display text — spell check suggestions use this for the suggestion word | | `EventArgs.GetDeferral()` | Must be held across the async `GetSpellCheckSuggestions` gap | -| `ContextMenuTarget` | Base target — QI to `Target2` for spellcheck support | +| `ContextMenuTarget` | Base target — QI to `Target2` for spell check support | From a3e21c153f7a241ea4eedbfff628947f4cc70ea8 Mon Sep 17 00:00:00 2001 From: Anurag Kumar Date: Wed, 29 Apr 2026 11:34:13 +0530 Subject: [PATCH 17/17] minor rafactoring" --- specs/CustomContextMenuSpellcheck.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/specs/CustomContextMenuSpellcheck.md b/specs/CustomContextMenuSpellcheck.md index 130080ef..64f087e3 100644 --- a/specs/CustomContextMenuSpellcheck.md +++ b/specs/CustomContextMenuSpellcheck.md @@ -177,8 +177,9 @@ interface ICoreWebView2ContextMenuTarget2 : ICoreWebView2ContextMenuTarget { /// correction. The handler receives an empty collection if no suggestions /// are available, if `HasMisspelledWord` is FALSE, or if the underlying /// spell check service does not respond within an internal timeout. + /// Multiple concurrent calls are supported; each handler will be invoked + /// with the same result when suggestions become available. /// Returns `E_POINTER` if `handler` is null. - /// Returns `E_ILLEGAL_METHOD_CALL` if a previous request is still pending. HRESULT GetSpellCheckSuggestions( [in] ICoreWebView2GetSpellCheckSuggestionsCompletedHandler* handler); }