Conversation
IME and accessibility both walk a text document by position, range and boundary unit, and each does it today with control-specific integer maths over a flattened string. This is the shared core they can be written against. - Positions are opaque pointers with gravity, so one stays valid across an edit and a boundary position knows which unit it belongs to; a bare offset can do neither. - Pointers carry provenance: one produced by a different navigation is rejected rather than reinterpreted in the wrong index space. - Offsets and lengths are UTF-16 code units, and a contiguous change is reported as a delta rather than leaving consumers to diff the document. - Any backing store can implement it - string, gap buffer, piece table, or a rope with an element tree. - TextSegmentation supplies the unit boundaries: grapheme clusters for Character, UAX-29 for Word with a word owning its trailing spaces and tabs, mandatory breaks for Line, and a heuristic for Sentence until real sentence segmentation lands. The surface is [Unstable] while the IME and accessibility consumers land on it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An accessibility provider has to read and walk a control's text without reaching into its layout, which nothing in Controls offered. - TextBoxTextNavigation reads and walks the control's text, maps ranges to and from top-level coordinates, and reports changes. - TextBox grows four internal helpers that answer those queries from the presenter's TextLayout: range rectangles, offset from a point, the visible range, and scroll-into-view. - IAccessibleText extends the navigation with what a screen reader needs beyond reading: selection, geometry and hit testing, the visible range, scrolling, and formatting attributes over the run they are uniform across. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Public API diff for this branch against Generated with Avalonia.Base namespace Avalonia.Input.TextInput
{
+ public interface ITextNavigation
+ {
+ event System.EventHandler<Avalonia.Input.TextInput.TextChange?>? TextChanged;
+ int GetOffset(Avalonia.Input.TextInput.ITextPointer from, Avalonia.Input.TextInput.ITextPointer to);
+ Avalonia.Input.TextInput.ITextPointer GetPosition(Avalonia.Input.TextInput.ITextPointer origin, Avalonia.Input.TextInput.TextUnit unit, int count);
+ Avalonia.Input.TextInput.ITextPointer GetPosition(Avalonia.Input.TextInput.ITextPointer origin, int distance);
+ Avalonia.Input.TextInput.ITextRange GetRange(Avalonia.Input.TextInput.ITextPointer a, Avalonia.Input.TextInput.ITextPointer b);
+ Avalonia.Input.TextInput.ITextRange GetRangeEnclosing(Avalonia.Input.TextInput.ITextPointer position, Avalonia.Input.TextInput.TextUnit unit);
+ string GetText(Avalonia.Input.TextInput.ITextRange range);
+ Avalonia.Input.TextInput.ITextPointer DocumentEnd { get; }
+ Avalonia.Input.TextInput.ITextRange DocumentRange { get; }
+ Avalonia.Input.TextInput.ITextPointer DocumentStart { get; }
+ long DocumentVersion { get; }
+ }
+ public interface ITextPointer
+ {
+ Avalonia.Media.TextFormatting.LogicalDirection Gravity { get; }
+ int Offset { get; }
+ }
+ public interface ITextRange
+ {
+ Avalonia.Input.TextInput.ITextPointer End { get; }
+ bool IsEmpty { get; }
+ Avalonia.Input.TextInput.ITextPointer Start { get; }
+ }
+ public sealed class TextAttribute
+ {
+ public const Avalonia.Input.TextInput.TextAttribute Background = 5;
+ public const Avalonia.Input.TextInput.TextAttribute FontFamily = 0;
+ public const Avalonia.Input.TextInput.TextAttribute FontSize = 1;
+ public const Avalonia.Input.TextInput.TextAttribute FontStyle = 3;
+ public const Avalonia.Input.TextInput.TextAttribute FontWeight = 2;
+ public const Avalonia.Input.TextInput.TextAttribute Foreground = 4;
+ public const Avalonia.Input.TextInput.TextAttribute IsReadOnly = 6;
+ public const Avalonia.Input.TextInput.TextAttribute StyleId = 7;
+ public int value__;
+ }
+ public sealed class TextChange
+ {
+ public TextChange(Avalonia.Input.TextInput.ITextPointer Position, int OldLength, int NewLength);
+ public void Deconstruct(out Avalonia.Input.TextInput.ITextPointer Position, out int OldLength, out int NewLength);
+ public bool Equals(Avalonia.Input.TextInput.TextChange other);
+ public override bool Equals(object obj);
+ public override int GetHashCode();
+ public static bool operator ==(Avalonia.Input.TextInput.TextChange left, Avalonia.Input.TextInput.TextChange right);
+ public static bool operator !=(Avalonia.Input.TextInput.TextChange left, Avalonia.Input.TextInput.TextChange right);
+ public override string ToString();
+ public int NewLength { get; init; }
+ public int OldLength { get; init; }
+ public Avalonia.Input.TextInput.ITextPointer Position { get; init; }
+ }
+ public sealed class TextStyleId
+ {
+ public const Avalonia.Input.TextInput.TextStyleId BulletedList = 15;
+ public const Avalonia.Input.TextInput.TextStyleId Custom = 0;
+ public const Avalonia.Input.TextInput.TextStyleId Emphasis = 13;
+ public const Avalonia.Input.TextInput.TextStyleId Heading1 = 1;
+ public const Avalonia.Input.TextInput.TextStyleId Heading2 = 2;
+ public const Avalonia.Input.TextInput.TextStyleId Heading3 = 3;
+ public const Avalonia.Input.TextInput.TextStyleId Heading4 = 4;
+ public const Avalonia.Input.TextInput.TextStyleId Heading5 = 5;
+ public const Avalonia.Input.TextInput.TextStyleId Heading6 = 6;
+ public const Avalonia.Input.TextInput.TextStyleId Heading7 = 7;
+ public const Avalonia.Input.TextInput.TextStyleId Heading8 = 8;
+ public const Avalonia.Input.TextInput.TextStyleId Heading9 = 9;
+ public const Avalonia.Input.TextInput.TextStyleId Normal = 12;
+ public const Avalonia.Input.TextInput.TextStyleId NumberedList = 16;
+ public const Avalonia.Input.TextInput.TextStyleId Quote = 14;
+ public const Avalonia.Input.TextInput.TextStyleId Subtitle = 11;
+ public const Avalonia.Input.TextInput.TextStyleId Title = 10;
+ public int value__;
+ }
+ public sealed class TextUnit
+ {
+ public const Avalonia.Input.TextInput.TextUnit Character = 0;
+ public const Avalonia.Input.TextInput.TextUnit Document = 7;
+ public const Avalonia.Input.TextInput.TextUnit Format = 1;
+ public const Avalonia.Input.TextInput.TextUnit Line = 4;
+ public const Avalonia.Input.TextInput.TextUnit Page = 6;
+ public const Avalonia.Input.TextInput.TextUnit Paragraph = 5;
+ public const Avalonia.Input.TextInput.TextUnit Sentence = 3;
+ public int value__;
+ public const Avalonia.Input.TextInput.TextUnit Word = 2;
+ }
}Avalonia.Controls namespace Avalonia.Automation.Provider
{
+ public interface IAccessibleText : Avalonia.Input.TextInput.ITextNavigation
+ {
+ string GetBlockSeparatedText(Avalonia.Input.TextInput.ITextRange range);
+ Avalonia.Rect[] GetBoundingRectangles(Avalonia.Input.TextInput.ITextRange range);
+ Avalonia.Input.TextInput.ITextPointer? GetPositionFromPoint(Avalonia.Point? point);
+ Avalonia.Input.TextInput.ITextRange GetSelection();
+ (System.Collections.Generic.IReadOnlyDictionary<Avalonia.Input.TextInput.TextAttribute, object>, Avalonia.Input.TextInput.ITextRange) GetTextAttributes(Avalonia.Input.TextInput.ITextPointer position);
+ Avalonia.Input.TextInput.ITextRange? GetVisibleRange();
+ void ScrollIntoView(Avalonia.Input.TextInput.ITextRange range);
+ void SetSelection(Avalonia.Input.TextInput.ITextRange range);
+ }
}Three notes on how the tool renders things, so the diff is not read as saying something it does not:
Every new type carries 🤖 Generated with Claude Code |
|
You can test this PR using the following package version. |
What does the pull request do?
Part of the structured text input work tracked in #22168.
Adds
ITextNavigation, the layout-free core that IME and accessibility both need to walk a text document, and implements it overTextBox.Today each consumer does that walk itself, with control-specific integer maths over a flattened string: the IME client, the automation peer and every platform bridge carry their own notion of where a word ends. This is the shared contract they can be written against instead.
ITextNavigationaddresses a document by opaqueITextPointerpositions with gravity, normalizedITextRangevalues, and boundary units (TextUnit), and raises aTextChangedelta when the text changes. Positions and lengths are UTF-16 code units; a pointer stays valid across edits, which a bare offset cannot, and a pointer produced by a different navigation is rejected rather than silently reinterpreted in the wrong index space.TextSegmentation(internal, inAvalonia.Base) supplies the unit boundaries: grapheme clusters forCharacter, UAX-29 forWord, mandatory line breaks forLine, and a heuristic forSentenceuntil real sentence segmentation lands. A word owns its trailing spaces and tabs, which is what UIA's "word plus trailing whitespace" and AT-SPI'sGRANULARITY_WORDexpect.IAccessibleTextextends the navigation with what a screen reader needs beyond reading: selection, geometry and hit testing, the visible range, scrolling, and formatting attributes over the run they are uniform across, reported through theTextAttributeandTextStyleIdvocabularies.TextBoxTextNavigationimplements it forTextBox, backed by four new internalTextBoxhelpers that answer geometry queries from the presenter'sTextLayout.Everything new in the public surface is
[Unstable].This is the first slice of #22168, which #20890 carried as one unreviewable change. The accessibility providers (UIA, AT-SPI) and the IME contract land on top of it as separate PRs; nothing consumes
TextBoxTextNavigationyet, so this PR changes no behavior on its own.What is the updated/expected behavior with this PR?
No user-visible change. What a reviewer can check is the contract's own semantics: offsets clamp to the document, ranges normalize regardless of argument order,
Charactersteps by grapheme cluster rather than code unit,Wordfollows UAX-29 and keeps contractions together, gravity decides which unit a boundary position belongs to,TextChangedreports the changed span and bumps the document version, and a foreign pointer throws on both the read and the mutation path.Checklist
Breaking changes
None. The new types are additive and
[Unstable];TextSegmentationandTextBoxTextNavigationare internal.Obsoletions / Deprecations
None.
Fixed issues
Part of #22168
🤖 Generated with Claude Code