Skip to content

Implement UIA TextPattern for TextBox on Windows - #21988

Closed
aryanchoudharypro wants to merge 1 commit into
AvaloniaUI:mainfrom
aryanchoudharypro:textbox-uia-textpattern
Closed

Implement UIA TextPattern for TextBox on Windows#21988
aryanchoudharypro wants to merge 1 commit into
AvaloniaUI:mainfrom
aryanchoudharypro:textbox-uia-textpattern

Conversation

@aryanchoudharypro

@aryanchoudharypro aryanchoudharypro commented Aug 15, 2026

Copy link
Copy Markdown

What does the pull request do?

TextBox on Windows previously only exposed ValuePattern, so UI Automation clients (screen readers such as NVDA) had no way to track the caret or selection while editing — focusing the control announced the whole text once, but arrowing through it, selecting, or editing gave no further feedback.

This adds a full TextPattern/TextRangeProvider implementation for TextBox on Windows:

  • New platform-agnostic Avalonia.Automation.Provider.ITextProvider/ITextRangeProvider contracts, implemented by TextBoxAutomationPeer and a nested TextRange type against TextBox/TextPresenter/TextLayout (character/word/line navigation, selection, bounding rectangles).
  • AutomationPeer gains TextSelectionChanged/TextChanged events, raised by TextBoxAutomationPeer and coalesced so a single edit produces one UIA event rather than one per touched property.
  • Win32 glue (AutomationNode.Text.cs, Win32TextRangeProvider) wires the above into the existing GeneratedComInterface-based UIA provider, wrapping fresh ITextRangeProvider instances since, unlike AutomationNode, they aren't cached per-peer.

Along the way, found and fixed several real bugs in the existing (previously unused/untested) Win32.Automation interop and marshalling code, surfaced only once an out-of-process client (NVDA) actually exercised it:

  • ITextProvider.RangeFromPoint took two scalar doubles instead of the UiaPoint struct the real UIA ABI expects.
  • SafeArrayRef.CreateFromObjects used ComWrappers.TryGetComInstance (only returns a wrapper that already exists, doesn't create one) instead of GetOrCreateComInterfaceForObject, silently leaving uninitialized ArrayPool-rented pointer data in the SAFEARRAY for objects with no prior COM exposure — this reliably crashed the process (UIAutomationCore!ProtobufHelper::WriteTextRangeArray, a null-pointer dereference) as soon as NVDA queried GetSelection().
  • Interface-typed SAFEARRAYs were built with SafeArrayCreate instead of SafeArrayCreateEx, so they carried no IID (FADF_HAVEIID) — native code marshaling such an array to an out-of-process client has no way to know which interface each element pointer is.
  • ITextRangeProvider.GetAttributeValue returned a plain -1 for unsupported attributes instead of UIA's reserved "not supported" sentinel (UiaGetReservedNotSupportedValue). NVDA reads a plain -1 as a real (truthy) attribute value; for the hyperlink attribute this made a plain TextBox get announced as a link.

What is the current behavior?

TextBox on Windows only implements ValuePattern. Screen readers get a single whole-text announcement on focus and no further caret/selection/edit feedback.

What is the updated/expected behavior with this PR?

TextBox on Windows now implements TextPattern as well. To test: run a Windows build with NVDA (or another UIA-based screen reader) active, focus a TextBox, and confirm caret position, character/word navigation, and selection are announced correctly while typing and arrowing through text.

How was the solution implemented (if it's not obvious)?

Three layers, built and verified in this order:

  1. Platform-agnostic layer (Avalonia.Controls) — ITextProvider/ITextRangeProvider interfaces plus the TextBoxAutomationPeer/TextRange implementation. Covered by unit tests, no COM involved.
  2. Win32 COM glue (Avalonia.Win32.Automation) — AutomationNode.Text.cs implements the existing (previously unused) UIA.ITextProvider interop interface by delegating to layer 1 via the existing InvokeSync dispatcher-marshaling helpers (now shared via the extracted Win32DispatcherHelper, since Win32TextRangeProvider isn't an AutomationNode and needs the same marshaling). Win32TextRangeProvider wraps a single ITextRangeProvider instance as its UIA.ITextRangeProvider COM counterpart.
  3. Marshalling fixes — the four bugs listed above in SafeArrayRef/ITextProvider's interop declaration, none of which were previously exercised by any existing pattern (ISelectionProvider etc. only ever return already-wrapped, long-lived AutomationNode instances, so the TryGetComInstance/missing-IID gaps never surfaced before).

Root-caused the crash and the "announced as a link" misbehavior using cdb (WinDbg's console debugger) attached to the live process, and verified the fix with NVDA actually running against a real build.

Checklist

Breaking changes

None. TextBoxAutomationPeer gains new interface implementations (ITextProvider) but its existing public surface (IValueProvider) is unchanged.

Obsoletions / Deprecations

None.

Fixed issues

TextBox previously only exposed ValuePattern on Windows, so screen
readers (e.g. NVDA) had no way to track the caret or selection while
editing: focusing the control announced the whole text once, but
arrowing through it, selecting, or editing gave no further feedback.

Adds a full TextPattern/TextRangeProvider implementation:
- Avalonia.Automation.Provider.ITextProvider/ITextRangeProvider: new
  platform-agnostic contracts, implemented by TextBoxAutomationPeer
  and its nested TextRange type against TextBox/TextPresenter/
  TextLayout (character/word/line navigation, selection, bounding
  rects).
- AutomationPeer gains TextSelectionChanged/TextChanged events, raised
  by TextBoxAutomationPeer and coalesced so a single edit produces one
  UIA event rather than one per touched property.
- Win32 glue (AutomationNode.Text.cs, Win32TextRangeProvider) wires
  the above into the existing GeneratedComInterface-based UIA
  provider, wrapping fresh ITextRangeProvider instances since, unlike
  AutomationNode, they aren't cached per-peer.

Also fixes several bugs in the existing (previously unused/untested)
Win32.Automation interop and marshalling code, found while getting
TextPattern to work correctly with an out-of-process client:
- ITextProvider.RangeFromPoint took two scalar doubles; the real UIA
  ABI passes a UiaPoint struct by value.
- SafeArrayRef.CreateFromObjects used ComWrappers.TryGetComInstance,
  which only returns a wrapper that already exists rather than
  creating one. For objects with no prior COM exposure (unlike
  long-lived AutomationNode instances, which usually already have a
  wrapper via prior tree navigation) this silently left uninitialized
  ArrayPool-rented pointer data in the SAFEARRAY, corrupting it.
- Interface-typed SAFEARRAYs were built with SafeArrayCreate instead
  of SafeArrayCreateEx, so they carried no IID (FADF_HAVEIID). Native
  code marshaling such an array to an out-of-process UIA client has no
  way to know which interface each element pointer is.
- ITextRangeProvider.GetAttributeValue returned a plain -1 for
  unsupported attributes instead of UIA's reserved "not supported"
  sentinel (UiaGetReservedNotSupportedValue). At least NVDA reads a
  plain -1 as a real (truthy) attribute value; for the hyperlink
  attribute this made a plain TextBox get announced as a link.

Adds TextBoxAutomationPeerTests covering the platform-agnostic layer:
caret-degenerate selection, forward/reverse selection normalization,
character-unit move/expand with clamping, word-unit expansion,
bounding rectangles, and coalesced selection-changed events.
@avaloniaui-bot

Copy link
Copy Markdown

You can test this PR using the following package version. 12.2.999-cibuild0068366-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@cla-avalonia

cla-avalonia commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator
  • All contributors have signed the CLA.

@aryanchoudharypro

Copy link
Copy Markdown
Author

@cla-avalonia agree

@Gillibald

Copy link
Copy Markdown
Contributor

This is already part of #20890

@MrJul

MrJul commented Aug 19, 2026

Copy link
Copy Markdown
Member

Thank you for your contribution. I'm closing this PR in favor of #20890.

@MrJul MrJul closed this Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants