Skip to content

[Windows] Restore and enable IAccessibleEx implementation#175406

Open
loic-peron-inetum-public wants to merge 17 commits intoflutter:masterfrom
inetum:iaccessibleex
Open

[Windows] Restore and enable IAccessibleEx implementation#175406
loic-peron-inetum-public wants to merge 17 commits intoflutter:masterfrom
inetum:iaccessibleex

Conversation

@loic-peron-inetum-public
Copy link
Contributor

@loic-peron-inetum-public loic-peron-inetum-public commented Sep 16, 2025

Restore and enable IAccessibleEx implementation.
Exposes new features of UI Automation to MSAA-based implementation.
Specifically usefull to expose AutomationId from SemanticsNode::identifier for UI test automation.

see #148763

Followup to #161955

Pre-launch Checklist

  • I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
  • I read the [Tree Hygiene] wiki page, which explains my responsibilities.
  • I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement].
  • I signed the [CLA].
  • I listed at least one issue that this PR fixes in the description above.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or this PR is [test-exempt].
  • I followed the [breaking change policy] and added [Data Driven Fixes] where supported.
  • All existing and new tests are passing.

@flutter-dashboard
Copy link

It looks like this pull request may not have tests. Please make sure to add tests or get an explicit test exemption before merging.

If you are not sure if you need tests, consider this rule of thumb: the purpose of a test is to make sure someone doesn't accidentally revert the fix. Ask yourself, is there anything in your PR that you feel it is important we not accidentally revert back to how it was before your fix?

Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. If you believe this PR qualifies for a test exemption, contact "@test-exemption-reviewer" in the #hackers channel in Discord (don't just cc them here, they won't see it!). The test exemption team is a small volunteer group, so all reviewers should feel empowered to ask for tests, without delegating that responsibility entirely to the test exemption group.

@github-actions github-actions bot added engine flutter/engine related. See also e: labels. a: accessibility Accessibility, e.g. VoiceOver or TalkBack. (aka a11y) platform-linux Building on or for Linux specifically a: desktop Running on desktop d: docs/ flutter/flutter/docs, for contributors platform-macos labels Sep 16, 2025
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request restores and enables the IAccessibleEx implementation, which exposes new UI Automation capabilities to the MSAA-based accessibility implementation on Windows. Specifically, it adds an identifier to SemanticsNode to expose AutomationId for UI testing. The changes correctly plumb this new field through the accessibility bridge and update relevant documentation and tests. My review includes a minor correction for the updated documentation to improve its accuracy.

@github-actions github-actions bot removed a: accessibility Accessibility, e.g. VoiceOver or TalkBack. (aka a11y) platform-linux Building on or for Linux specifically platform-macos labels Sep 16, 2025
@loic-peron-inetum-public loic-peron-inetum-public force-pushed the iaccessibleex branch 2 times, most recently from 7954197 to a4f81de Compare September 16, 2025 08:47
@loic-peron-inetum-public loic-peron-inetum-public changed the title Restore and enable IAccessibleEx implementation [Windows] Restore and enable IAccessibleEx implementation Sep 26, 2025
@loic-peron-inetum-public
Copy link
Contributor Author

@loic-sharma could you review this updated PR ?

@loic-sharma
Copy link
Member

@loic-peron-inetum-public Thanks for the pull request!

I'm a little concerned that this change might regress Flutter Windows's accessibility support in unexpected ways as a similar change to add UI Automation had caused unexpected problems.

Could you include details on how you verified this change? What scenarios did you test?

@loic-peron-inetum-public
Copy link
Contributor Author

Regarding how we use and tests this proposal:

We are building an application targeting iOS and Windows. We automate black-box tests using Appium on both platform. We use a custom build of the flutter engine for Windows that includes the required PR to get Semantics::identifer to be exposed as AutomationId. The application is not evaluated on its accessibility for actual users.

Regarding UI Automation-related regressions:

I understand that switching to UI Automation required a full switch in the accessibility interface exposed by Flutter. It seems that IAccessibleEx is a way to extend IAccessible-based exposition with the additional capabilities offered by UI Automation, without jeopardizing existing baheviours.

What are the unexpected ways that triggered unexpected problems and regressed Flutter accessibility support during a previous attempt to switch to UI Automation ? How can I check them ?

@loic-sharma
Copy link
Member

loic-sharma commented Oct 22, 2025

What are the unexpected ways that triggered unexpected problems and regressed Flutter accessibility support during a previous attempt to switch to UI Automation ? How can I check them ?

I would manually check that a sample Flutter Windows app still works well with JAWS, NVDA, and Windows Narrator. I'd make sure that as you tab around, the screen reader properly announces content, without any differences compared to today's implementation.


Could we de-risk this by making the IAccessibleEx implementation opt-in at runtime?

Flutter Windows's API has a DartProject that lets you configure your Flutter app. For example, you can configure which threads the apps uses (see this and this).

Could we update QueryService to only return an IAccessibleEx interface if the Flutter app turned on the IAccessibleEx feature?

Here's what this API might look like:

// Configures the accessibility implementation used by Flutter.
enum class AccessibilityMode {
  // Default value. Flutter will automatically select the best available
  // implementation.
  Default,
  // Use the IAccessible implementation.
  IAccessible,
  // Use the experimental IAccessibleEx implementation.
  IAccessibleEx,
};

class DartProject {
  // Sets the accessibility implementation used by Flutter.
  void set_accessibility_mode(AccessibilityMode accessibility_mode) {
    accessibility_mode_ = accessibility_mode;
  }
}

To opt-in to the IAccessibilityEx implementation, the app developer would need to modify their windows/runner/main.cpp file:

flutter::DartProject project(L"data");
project.set_accessibility_mode(flutter::AccessibilityMode::IAccessibleEx);

Then, QueryService would only return an IAccessibleEx interface if the app opted-in to AccessibilityMode::IAccessibleEx. What do you think of this approach?

@loic-sharma
Copy link
Member

Note to self: these docs are an excellent resource to review this PR: https://learn.microsoft.com/en-us/windows/win32/winauto/implementing-iaccessibleex-for-providers

@loic-peron-inetum-public
Copy link
Contributor Author

Could we de-risk this by making the IAccessibleEx implementation opt-in at runtime?

I will try and implement an engine-runtime configuration option in november.

Could this be enabled using an application-buildtime --dart-define= property ?

@loic-sharma
Copy link
Member

Could this be enabled using an application-buildtime --dart-define= property ?

Sadly, no. The Windows embedder doesn't have an easy way to read Dart defines. The suggestion I left above means that the app developer will need to update the C++ code in their Flutter Windows entry point (example).

@chinmaygarde
Copy link
Contributor

Is progress still being made on this?

@loic-peron-inetum-public
Copy link
Contributor Author

loic-peron-inetum-public commented Nov 14, 2025

Is progress still being made on this?

Hi, yes I still work on this, but I have little work-time currently allocated for this tack. I still work to map the way from DartProject to AXPlatformNodeWin at runtime.

Would it be acceptable to use a technical attribute in AXNodeData to represent IAccessibleEx activation on Windows ?

@loic-sharma
Copy link
Member

Would it be acceptable to use a technical attribute in AXNodeData to represent IAccessibleEx activation on Windows ?

Could you say more about why you need this?

I think it's OK to add more data to AXNodeData, but I'll defer to @chunhtai on that.

@chunhtai
Copy link
Contributor

Would it be acceptable to use a technical attribute in AXNodeData to represent IAccessibleEx activation on Windows ?

may I know what property you want to add? also is IAccessibleEx activation per node? if this is on/off for the entire app, I don't think we should use AXNodeData for this case.

@loic-sharma
Copy link
Member

@mattkae Could you do the second review for this PR?

@loic-sharma loic-sharma requested review from mattkae and removed request for chinmaygarde January 21, 2026 00:09
mattkae
mattkae previously approved these changes Jan 23, 2026
Copy link
Contributor

@mattkae mattkae left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work! I have one small nit, but looks good to me

Copy link
Contributor

@chunhtai chunhtai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There also doesn't seem to be any test added, should we get a test exemption? cc @loic-sharma since I am not too sure about the windows embedding code

UIThreadPolicy ui_thread_policy() const { return ui_thread_policy_; }

// Sets the accessibility implementation used by Flutter.
void set_accessibility_mode(AccessibilityMode accessibility_mode) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you know who will be setting the mode?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As proposed by @loic-sharma in #175406 (comment)

To opt-in to the IAccessibilityEx implementation, the app developer would need to modify their windows/runner/main.cpp file:

flutter::DartProject project(L"data");
project.set_accessibility_mode(flutter::AccessibilityMode::IAccessibleEx);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder why we don't do this the same way we do feature flags? @loic-sharma ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chunhtai Sadly, embedders don't have a good way to read feature flags today. Currently, feature flags are stamped as Dart defines in the snapshot, so feature flags can't be read until the engine has launched the Dart isolate.

FYI, we used a similar approach for the merged threads migrations:

// Sets the thread policy for UI isolate.
void set_ui_thread_policy(UIThreadPolicy policy) {
ui_thread_policy_ = policy;
}

This is also similar to how we did the Impeller migration: each embedder exposed a configuration option to enable Impeller.

@loic-sharma
Copy link
Member

There also doesn't seem to be any test added, should we get a test exemption? cc @loic-sharma since I am not too sure about the windows embedding code

Whoops, good catch. @loic-peron-inetum-public could you add a test for this? I'd consider a test that sends a WM_GETOBJECT message that gets the IAccessibleEx node. These tests might be useful prior art:

TEST(MockWindow, DISABLED_GetObjectUia) {
MockWindow window;
bool uia_called = false;
ON_CALL(window, OnGetObject)
.WillByDefault(Invoke([&uia_called](UINT msg, WPARAM wpar, LPARAM lpar) {
#ifdef FLUTTER_ENGINE_USE_UIA
uia_called = true;
#endif // FLUTTER_ENGINE_USE_UIA
return static_cast<LRESULT>(0);
}));
EXPECT_CALL(window, OnGetObject).Times(1);
window.InjectWindowMessage(WM_GETOBJECT, 0, UiaRootObjectId);
EXPECT_TRUE(uia_called);
}

TEST_F(WindowsTest, GetGraphicsAdapterWithHighPerformancePreference) {
std::optional<LUID> luid = egl::Manager::GetHighPerformanceGpuLuid();
if (!luid) {
GTEST_SKIP() << "Not able to find high performance GPU, nothing to check.";
}
auto& context = GetContext();
WindowsConfigBuilder builder(context);
builder.SetGpuPreference(
FlutterDesktopGpuPreference::HighPerformancePreference);
ViewControllerPtr controller{builder.Run()};
ASSERT_NE(controller, nullptr);
auto view = FlutterDesktopViewControllerGetView(controller.get());
Microsoft::WRL::ComPtr<IDXGIAdapter> dxgi_adapter;
dxgi_adapter = FlutterDesktopViewGetGraphicsAdapter(view);
ASSERT_NE(dxgi_adapter, nullptr);
DXGI_ADAPTER_DESC desc{};
ASSERT_TRUE(SUCCEEDED(dxgi_adapter->GetDesc(&desc)));
ASSERT_EQ(desc.AdapterLuid.HighPart, luid->HighPart);
ASSERT_EQ(desc.AdapterLuid.LowPart, luid->LowPart);
}

Your test can use FlutterDesktopViewGetHWND to get the right HWND and send the WM_GETOBJECT message.

@loic-sharma loic-sharma self-requested a review January 26, 2026 23:03
@loic-peron-inetum-public
Copy link
Contributor Author

I do not see how I can link a mocked Window (as demonstrated in the first linked test) to a real engine configured for IAccessibleEx (as demonstrated in the second linked test).

I tried and failed to send WM_GETOBJECT to the HWND returned by FlutterDesktopViewGetHWND and convert the returned LRESULT with ObjectFromLresult to an IAccessible as described in Accessibility-on-Windows.md.

Is there any additional element I should be aware of while continuing to try and implement this test ?

@loic-peron-inetum-public
Copy link
Contributor Author

I cannot manage to SendMessage :

TEST_F(WindowsTest, EnableIAccessibleEx) {
  auto& context = GetContext();
  WindowsConfigBuilder builder(context);
  builder.SetAccessibilityMode(FlutterDesktopAccessibilityMode::IAccessibleExMode);
  ViewControllerPtr controller{builder.Run()};
  ASSERT_NE(controller, nullptr);
  auto view = FlutterDesktopViewControllerGetView(controller.get());
  ASSERT_NE(view, nullptr);
  HWND hwnd = FlutterDesktopViewGetHWND(view);
  ASSERT_NE(hwnd, nullptr);
  LRESULT lres = SendMessage(hwnd, WM_GETOBJECT, 0, OBJID_CLIENT);
  ASSERT_NE(lres, 0);
}

fails on ASSERT_NE(lres, 0)

@loic-sharma can you help me find what am I obviously missing ?

@loic-sharma
Copy link
Member

loic-sharma commented Mar 2, 2026

@loic-peron-inetum-public FYI, you can use Visual Studio to debug the unit test.

How to use Visual Studio to debug the unit test...
  1. Build the engine. This produces a Visual Studio solution. For example: flutter\engine\src\out\host_debug_unopt\all.sln

  2. Open that solution in Visual Studio

  3. In the Solution Explorer pane, find the flutter_windows_unittests project.

    This project uses GoogleTest to test the Flutter Windows embedder. It is an executable that you can run and debug.

    1. Right-click the flutter_windows_unittests project and press Set as Startup Project:

      image
    2. Right-click the flutter_windows_unittests project and press Properties. Navigate to Debugging. Set Command Arguments to:

      --repeat=1 --gtest_filter="WindowsTest.EnableIAccessibleEx"
      
      image
  4. Navigate to the flutter_windows_unittests.cc file and set a breakpoint on your test EnableIAccessibleEx.

  5. Navigate to the flutter_window.cc file and set a breakpoint on FlutterWindow::HandleMessage where it handles the WM_GETOBJECT message.

  6. Now press the F5 button. This should launch your test and and hit your breakpoint.

    image

WM_GETOBJECT returns an LRESULT of 0 because the app does not have an accessibility tree yet. I'd recommend updating the test such that it:

  1. In flutter\engine\src\flutter\shell\platform\windows\fixtures\main.dart, add a new Dart test app that:
    1. Waits until semantics is enabled
    2. Sends a semantics tree
    3. Sends a signal to the test
  2. In flutter\engine\src\flutter\shell\platform\windows\flutter_windows_unittests.cc:
    1. Use WindowsConfigBuilder.SetDartEntrypoint to launch your new Dart test app
    2. Use WindowsTestContext::AddNativeFunction to listen from signals from the Dart app
    3. Pumps win32 messages until the app sends the signal
    4. Calls WM_GETOBJECT and checks the result.

Here's a Dart test app that sends a semantic tree:

@pragma('vm:entry-point')
// ignore: non_constant_identifier_names
Future<void> a11y_main_multi_view() async {
// 1: Return initial state (semantics disabled).
notifySemanticsEnabled(PlatformDispatcher.instance.semanticsEnabled);
// 2: Add the first view (implicitly handled by PlatformDispatcher).
// 3: Add the second view (implicitly handled by PlatformDispatcher).
// 4: Await semantics enabled from embedder.
await semanticsChanged;
notifySemanticsEnabled(PlatformDispatcher.instance.semanticsEnabled);
// 5: Return initial state of accessibility features.
notifyAccessibilityFeatures(PlatformDispatcher.instance.accessibilityFeatures.reduceMotion);
// 6: Fire semantics updates.
SemanticsUpdateBuilder createForView(FlutterView view) {
return SemanticsUpdateBuilder()..updateNode(
id: view.viewId + 1, // For simplicity, give each node an id of viewId + 1
identifier: '',
label: 'A: root',
labelAttributes: <StringAttribute>[],
rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
transform: kTestTransform,
hitTestTransform: kTestTransform,
traversalParent: 0,
childrenInTraversalOrder: Int32List.fromList(<int>[84, 96]),
childrenInHitTestOrder: Int32List.fromList(<int>[96, 84]),
actions: 0,
flags: SemanticsFlags.none,
maxValueLength: 0,
currentValueLength: 0,
textSelectionBase: 0,
textSelectionExtent: 0,
platformViewId: 0,
scrollChildren: 0,
scrollIndex: 0,
scrollPosition: 0.0,
scrollExtentMax: 0.0,
scrollExtentMin: 0.0,
hint: '',
hintAttributes: <StringAttribute>[],
value: '',
valueAttributes: <StringAttribute>[],
increasedValue: '',
increasedValueAttributes: <StringAttribute>[],
decreasedValue: '',
decreasedValueAttributes: <StringAttribute>[],
tooltip: 'tooltip',
textDirection: TextDirection.ltr,
additionalActions: Int32List(0),
controlsNodes: null,
inputType: SemanticsInputType.none,
locale: null,
minValue: '0',
maxValue: '0',
);
}
PlatformDispatcher.instance.setSemanticsTreeEnabled(true);
for (final FlutterView view in PlatformDispatcher.instance.views) {
view.updateSemantics(createForView(view).build());
}
signalNativeTest();
// 7: Await semantics disabled from embedder.
await semanticsChanged;
notifySemanticsEnabled(PlatformDispatcher.instance.semanticsEnabled);
}

Here's how you can pump win32 messages until the app sends a semantics tree:

while (!signaled) {
PumpMessage();
}

Please let me know if you have questions!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a: desktop Running on desktop d: docs/ flutter/flutter/docs, for contributors engine flutter/engine related. See also e: labels. platform-windows Building on or for Windows specifically

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants