Skip to content

[Breaking Change] Enabling Windows ShellHandwriting Support for WebView2 in WindowToVisual Mode #134

Description

@ojasvi1450

[Breaking Change] Enabling Windows ShellHandwriting Support for WebView2 in WindowToVisual Mode

Summary

WebView2 is introducing support for Windows shellhandwriting (pen handwriting-to-text) in WindowToVisual hosting mode, gated behind the feature flag msAbydosForWindowlessWV2.

Details

Scope:

  • In scope: WebView2 in WindowToVisual hosting mode on Windows.
  • Out of scope: VisualToVisual hosting mode — not supported in this change.
  • Out of scope: WindowToWindow hosting mode already supports Windows shellhandwriting.

Purpose: Enable Windows shell handwriting on edit fields inside WebView2 instances hosted in WindowToVisual mode.

To enable correct handwriting routing, WebView2 now registers a per-instance ITfHandwritingSink (as a TSF secondary client) for every WebView2 instance. This is a behavioral change in how WebView2 interacts with the Text Services Framework on the host app's UI thread. When the flag is enabled, the OS no longer falls back to its UIA-based default for handwriting target determination on the host's TSF thread.

Behavior

When msAbydosForWindowlessWV2 is enabled, WebView2 in WindowToVisual mode registers a per-instance ITfHandwritingSink on the host app's TSF thread. This enables shellhandwriting on edit fields inside WebView2, and changes how TSF handwriting events are dispatched on the shared TSF thread.

For HWNDs that WebView2 does not own, the sink returns E_NOTIMPL, allowing TSF to chain through to other registered sinks on the thread.

See Impact for what this means for your app and any required action.

Impact

This change affects WebView2 host apps running in WindowToVisual hosting mode on Windows. Apps in WindowToWindow or VisualToVisual mode are not affected (see Details).

⚠️ Regression risk for apps that do not register a TSF handwriting sink

If your app is in WindowToVisual mode and does not register its own ITfHandwritingSink, pen handwriting on your app's native edit fields may stop working once this change is live.

Cause: WebView2 now registers a per-instance ITfHandwritingSink on the host app's TSF thread. For HWNDs WebView2 does not own, the sink returns E_NOTIMPL, expecting TSF to chain through to other registered sinks. If your app has no sink of its own, there is nothing for TSF to chain to, and the OS does not fall back to its UIA-based default in this case.

Mitigation: Register your own ITfHandwritingSink (see App Action). This also enables pen handwriting inside WebView2 edit fields.

Audience summary

If your app… Behavior change (with flag enabled) Action
Already registers an ITfHandwritingSink on its TSF thread Pen handwriting now works inside WebView2 edit fields. Your native edit fields continue to work via your sink — TSF chains to it when WebView2 returns E_NOTIMPL. Recommended: smoke-test pen handwriting on your native edit fields with a WebView2 instance loaded, to confirm chaining works as expected in your scenario.
Does not register an ITfHandwritingSink Pen handwriting now works inside WebView2 edit fields, but pen handwriting on your app's native edit fields may stop working (see Regression risk above). Required: register an ITfHandwritingSink on your TSF thread. See App Action.

App Action

If your app does not already register its own ITfHandwritingSink, perform Steps 1 and 2 to preserve pen handwriting on your native edit fields. Pen handwriting inside WebView2 edit fields is enabled automatically by this change — no host-side action required.

Perform only Step 2 if you want to enable handwriting in WebView2 only.

Step 1 — Implement ITfHandwritingSink

Create a COM class (e.g., HostHandwritingSink) that implements ITfHandwritingSink. TSF calls its two methods in sequence for every pen interaction.

Class skeleton (illustrative):

class HostHandwritingSink
    : public Microsoft::WRL::RuntimeClass<
          Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>,
          ITfHandwritingSink>
{
public:
    HostHandwritingSink() = default;

    HRESULT STDMETHODCALLTYPE DetermineProximateHandwritingTarget(
        ITfDetermineProximateHandwritingTargetArgs* args) override;

    HRESULT STDMETHODCALLTYPE FocusHandwritingTarget(
        ITfFocusHandwritingTargetArgs* args) override;
};

1a. DetermineProximateHandwritingTarget

Purpose: TSF gives you a screen point near where the pen is hovering and asks, "Is there a text-input target around here that you own?"

What your implementation should do:

  • If HWND matches your host app's HWND, Set response as TF_HANDWRITING_TARGET_PROXIMATE and return S_OK — you have claimed the target.
  • Otherwise, return E_NOTIMPL so TSF can ask other registered sinks.

Result of returning TF_HANDWRITING_TARGET_PROXIMATE: TSF immediately follows up with FocusHandwritingTarget for the same HWND.

Example (illustrative — not production-ready):

HRESULT STDMETHODCALLTYPE DetermineProximateHandwritingTarget(
    ITfDetermineProximateHandwritingTargetArgs* args) override
{
    if (!args)
    {
        return E_NOTIMPL;
    }
    HWND hwnd = nullptr;
    POINT screenPt{};
    SIZE distThreshold{};
    args->GetPointerTargetInfo(&hwnd, &screenPt, &distThreshold);
    // IsTextInputHwnd is your own helper that decides whether the
    // candidate HWND is a text-input target your app wants to claim.
    // A common approach is to query UI Automation (TextPattern /
    // ValuePattern) and filter out read-only / disabled elements;
    // restrict the check to your own process to avoid cross-process
    // UIA stalls.
    if (IsTextInputHwnd(hwnd))
    {
        args->SetResponse(TF_HANDWRITING_TARGET_PROXIMATE);
        return S_OK;
    }
    return E_NOTIMPL;
}

1b. FocusHandwritingTarget

Purpose: TSF asks you to give keyboard focus to the target you just claimed, so recognized text has a control to land in.

What your implementation should do:

  • Retrieve the target HWND and verify that the HWND belongs to your native app.
  • Set focus on the HWND.
  • SetResponse as TF_HANDWRITING_TARGET_FOCUSED and return S_OK.

Result of returning TF_HANDWRITING_TARGET_FOCUSED: TSF opens the handwriting panel, captures ink, performs recognition, and injects the recognized text into the now-focused control.

Example (illustrative — not production-ready):

HRESULT STDMETHODCALLTYPE FocusHandwritingTarget(
    ITfFocusHandwritingTargetArgs* args) override
{
    if (!args)
    {
        return E_NOTIMPL;
    }
    HWND hwnd = nullptr;
    RECT screenArea{};
    SIZE distThreshold{};
    HRESULT hr = args->GetPointerTargetInfo(&hwnd, &screenArea, &distThreshold);
    if (FAILED(hr))
    {
        return E_NOTIMPL;
    }
    // IsTextInputHwnd is your own helper that decides whether the
    // candidate HWND is a text-input target your app wants to claim.
    // Decline if the HWND is not one of your text-input controls.
    if (!IsTextInputHwnd(hwnd))
    {
        return E_NOTIMPL;
    }
    SetFocus(hwnd);
    // SetFocus can no-op (e.g., HWND on a different thread, or its
    // top-level parent isn't active). Consider verifying with
    // GetFocus() and returning E_FAIL if focus didn't actually land,
    // to avoid lying to TSF about the focused state.
    args->SetResponse(TF_HANDWRITING_TARGET_FOCUSED);
    return S_OK;
}

Step 2 — Activate TSF and register the sink

Store two variables that hold the sink and the registration cookie:

  • A ComPtr<HostHandwritingSink> (e.g., hostSink).
  • A DWORD cookie initialized to TF_INVALID_COOKIE (e.g., hostSinkCookie).

Then, once during UI initialization (a good moment is when your top-level window or controller is created — COM is already initialized at that point), perform the steps below. Wrap the body in a one-shot helper and early-out if hostSinkCookie != TF_INVALID_COOKIE.

  1. Create the TSF thread manager:

    Microsoft::WRL::ComPtr<ITfThreadMgr> threadMgr;
    HRESULT hr = CoCreateInstance(
        CLSID_TF_ThreadMgr,        // rclsid
        nullptr,                   // pUnkOuter
        CLSCTX_INPROC_SERVER,      // dwClsContext
        IID_PPV_ARGS(&threadMgr)); // riid, ppv
  2. Activate it. Call threadMgr->Activate(&clientId) so TSF recognizes the thread as a TSF client.

  3. Enable handwriting. Query ITfHandwriting from the thread manager and call handwriting->SetHandwritingState(TF_HANDWRITING_ENABLED).

  4. Instantiate and register the sink. Create a HostHandwritingSink, query ITfSource from the handwriting interface, then call tfSource->AdviseSink(__uuidof(ITfHandwritingSink), sink, &hostSinkCookie).

That is it — TSF will now route pen-handwriting interactions on your app's native text controls through your sink.

Example (illustrative — not production-ready):

// Run once per UI thread during initialization.
Microsoft::WRL::ComPtr<ITfThreadMgr> threadMgr;
HRESULT hr = CoCreateInstance(
    CLSID_TF_ThreadMgr, nullptr, CLSCTX_INPROC_SERVER,
    IID_PPV_ARGS(&threadMgr));

TfClientId clientId = TF_CLIENTID_NULL;
if (SUCCEEDED(hr))
{
    hr = threadMgr->Activate(&clientId);
}

if (SUCCEEDED(hr))
{
    Microsoft::WRL::ComPtr<ITfHandwriting> handwriting;
    hr = threadMgr.As(&handwriting);
    if (SUCCEEDED(hr))
    {
        // Use TF_HANDWRITING_ENABLED (not TF_HANDWRITING_POINTERDELIVERY)
        // so TSF calls DetermineProximateHandwritingTarget per
        // interaction. This lets WebView2 opt into pointer delivery for
        // its own windows while host text fields keep the buffered flow.
        handwriting->SetHandwritingState(TF_HANDWRITING_ENABLED);

        auto hostSink = Microsoft::WRL::Make<HostHandwritingSink>();
        Microsoft::WRL::ComPtr<ITfSource> tfSource;
        DWORD hostSinkCookie = TF_INVALID_COOKIE;
        if (SUCCEEDED(handwriting.As(&tfSource)))
        {
            tfSource->AdviseSink(
                __uuidof(ITfHandwritingSink), hostSink.Get(),
                &hostSinkCookie);
        }
    }
}
// On thread teardown, symmetrically call
// tfSource->UnadviseSink(hostSinkCookie) and threadMgr->Deactivate().

Additional Changes

  • Feature flag opt-in: Pass the following when creating the WebView2 environment:

    --enable-features=msAbydosForWindowlessWV2
    

    Pass this via the WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS environment variable or via CoreWebView2EnvironmentOptions.AdditionalBrowserArguments.

  • Lifetime. Keep the TSF thread manager alive for the lifetime of your UI thread. Do not deactivate it while WebView2 instances are alive.

  • Required headers. The interfaces and constants used above are declared in:

    • msctf.hITfThreadMgr, ITfSource, CLSID_TF_ThreadMgr, TF_CLIENTID_NULL, TF_INVALID_COOKIE.
    • ShellHandwriting.hITfHandwriting, ITfHandwritingSink, ITfDetermineProximateHandwritingTargetArgs, ITfFocusHandwritingTargetArgs, TF_HANDWRITING_ENABLED, TF_HANDWRITING_TARGET_PROXIMATE, TF_HANDWRITING_TARGET_FOCUSED.

Testing

To test proactively, set the feature flag before launching your app:

set WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--enable-features=msAbydosForWindowlessWV2

Timeline

Release Date State Key Change
149 1st week of June, 2026 Off (flag gated) msAbydosForWindowlessWV2 disabled by default. Apps can opt in to test.
150 1st week of July, 2026 Off (flag gated) msAbydosForWindowlessWV2 disabled by default. Apps can opt in to test.
151 1st week of Aug, 2026 On by default Apps in WindowToVisual mode will see shell handwriting work in WebView2 edit fields. Apps that have registered an ITfHandwritingSink continue to receive handwriting events on their own text controls via the sink chain.

Contact

For questions or concerns, reach out to WebView2Feedback repo.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Breaking ChangeNotable change in behavior that may require updating your app

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions