Browse Source

Initial implementation of UI automation.

Follows WPF/UWP API as closely as possible. Limited to win32 right now. Broken in many places.
Steven Kirk 4 years ago
parent
commit
2a44d8b564
100 changed files with 4093 additions and 29 deletions
  1. 28 0
      src/Avalonia.Controls/Automation/AutomationElementIdentifiers.cs
  2. 28 0
      src/Avalonia.Controls/Automation/AutomationLiveSetting.cs
  3. 539 0
      src/Avalonia.Controls/Automation/AutomationProperties.cs
  4. 11 0
      src/Avalonia.Controls/Automation/AutomationProperty.cs
  5. 10 0
      src/Avalonia.Controls/Automation/ElementNotEnabledException.cs
  6. 15 0
      src/Avalonia.Controls/Automation/ExpandCollapsePatternIdentifiers.cs
  7. 29 0
      src/Avalonia.Controls/Automation/ExpandCollapseState.cs
  8. 26 0
      src/Avalonia.Controls/Automation/IsOffscreenBehavior.cs
  9. 227 0
      src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs
  10. 29 0
      src/Avalonia.Controls/Automation/Peers/ButtonAutomationPeer.cs
  11. 20 0
      src/Avalonia.Controls/Automation/Peers/CheckBoxAutomationPeer.cs
  12. 123 0
      src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs
  13. 39 0
      src/Avalonia.Controls/Automation/Peers/ContentControlAutomationPeer.cs
  14. 196 0
      src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
  15. 20 0
      src/Avalonia.Controls/Automation/Peers/ImageAutomationPeer.cs
  16. 57 0
      src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs
  17. 82 0
      src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs
  18. 22 0
      src/Avalonia.Controls/Automation/Peers/MenuAutomationPeer.cs
  19. 39 0
      src/Avalonia.Controls/Automation/Peers/MenuItemAutomationPeer.cs
  20. 28 0
      src/Avalonia.Controls/Automation/Peers/NoneAutomationPeer.cs
  21. 36 0
      src/Avalonia.Controls/Automation/Peers/PopupRootAutomationPeer.cs
  22. 34 0
      src/Avalonia.Controls/Automation/Peers/RangeBaseAutomationPeer.cs
  23. 175 0
      src/Avalonia.Controls/Automation/Peers/ScrollViewerAutomationPeer.cs
  24. 87 0
      src/Avalonia.Controls/Automation/Peers/SelectingItemsControlAutomationPeer.cs
  25. 20 0
      src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs
  26. 20 0
      src/Avalonia.Controls/Automation/Peers/TabControlAutomationPeer.cs
  27. 18 0
      src/Avalonia.Controls/Automation/Peers/TabItemAutomationPeer.cs
  28. 28 0
      src/Avalonia.Controls/Automation/Peers/TextAutomationPeer.cs
  29. 21 0
      src/Avalonia.Controls/Automation/Peers/TextBoxAutomationPeer.cs
  30. 39 0
      src/Avalonia.Controls/Automation/Peers/ToggleButtonAutomationPeer.cs
  31. 32 0
      src/Avalonia.Controls/Automation/Peers/UnrealizedElementAutomationPeer.cs
  32. 39 0
      src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs
  33. 72 0
      src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs
  34. 32 0
      src/Avalonia.Controls/Automation/Platform/IAutomationNode.cs
  35. 18 0
      src/Avalonia.Controls/Automation/Platform/IAutomationNodeFactory.cs
  36. 20 0
      src/Avalonia.Controls/Automation/Platform/IRootAutomationNode.cs
  37. 24 0
      src/Avalonia.Controls/Automation/Provider/IExpandCollapseProvider.cs
  38. 15 0
      src/Avalonia.Controls/Automation/Provider/IInvokeProvider.cs
  39. 37 0
      src/Avalonia.Controls/Automation/Provider/IRangeValueProvider.cs
  40. 14 0
      src/Avalonia.Controls/Automation/Provider/IRootProvider.cs
  41. 71 0
      src/Avalonia.Controls/Automation/Provider/IScrollProvider.cs
  42. 37 0
      src/Avalonia.Controls/Automation/Provider/ISelectionItemProvider .cs
  43. 29 0
      src/Avalonia.Controls/Automation/Provider/ISelectionProvider.cs
  44. 40 0
      src/Avalonia.Controls/Automation/Provider/IToggleProvider.cs
  45. 31 0
      src/Avalonia.Controls/Automation/Provider/IValueProvider.cs
  46. 30 0
      src/Avalonia.Controls/Automation/RangeValuePatternIdentifiers.cs
  47. 45 0
      src/Avalonia.Controls/Automation/ScrollPatternIdentifiers.cs
  48. 25 0
      src/Avalonia.Controls/Automation/SelectionPatternIdentifiers.cs
  49. 9 0
      src/Avalonia.Controls/Button.cs
  50. 6 0
      src/Avalonia.Controls/CheckBox.cs
  51. 7 0
      src/Avalonia.Controls/ComboBox.cs
  52. 21 0
      src/Avalonia.Controls/Control.cs
  53. 7 0
      src/Avalonia.Controls/Image.cs
  54. 7 0
      src/Avalonia.Controls/ItemsControl.cs
  55. 7 1
      src/Avalonia.Controls/ListBoxItem.cs
  56. 7 0
      src/Avalonia.Controls/Menu.cs
  57. 7 0
      src/Avalonia.Controls/MenuItem.cs
  58. 13 1
      src/Avalonia.Controls/Primitives/Popup.cs
  59. 7 0
      src/Avalonia.Controls/Primitives/PopupRoot.cs
  60. 7 0
      src/Avalonia.Controls/Primitives/ToggleButton.cs
  61. 7 0
      src/Avalonia.Controls/ScrollViewer.cs
  62. 7 0
      src/Avalonia.Controls/Slider.cs
  63. 7 0
      src/Avalonia.Controls/TabControl.cs
  64. 7 0
      src/Avalonia.Controls/TabItem.cs
  65. 8 1
      src/Avalonia.Controls/TextBlock.cs
  66. 7 0
      src/Avalonia.Controls/TextBox.cs
  67. 7 0
      src/Avalonia.Controls/Window.cs
  68. 6 26
      src/Avalonia.Input/KeyboardDevice.cs
  69. 19 0
      src/Windows/Avalonia.Win32/Automation/AutomationNode.ExpandCollapse.cs
  70. 19 0
      src/Windows/Avalonia.Win32/Automation/AutomationNode.RangeValue.cs
  71. 32 0
      src/Windows/Avalonia.Win32/Automation/AutomationNode.Scroll.cs
  72. 38 0
      src/Windows/Avalonia.Win32/Automation/AutomationNode.Selection.cs
  73. 13 0
      src/Windows/Avalonia.Win32/Automation/AutomationNode.Toggle.cs
  74. 18 0
      src/Windows/Avalonia.Win32/Automation/AutomationNode.Value.cs
  75. 328 0
      src/Windows/Avalonia.Win32/Automation/AutomationNode.cs
  76. 20 0
      src/Windows/Avalonia.Win32/Automation/AutomationNodeFactory.cs
  77. 72 0
      src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs
  78. 3 0
      src/Windows/Avalonia.Win32/Avalonia.Win32.csproj
  79. 26 0
      src/Windows/Avalonia.Win32/Interop/Automation/IDockProvider.cs
  80. 16 0
      src/Windows/Avalonia.Win32/Interop/Automation/IExpandCollapseProvider.cs
  81. 17 0
      src/Windows/Avalonia.Win32/Interop/Automation/IGridItemProvider.cs
  82. 15 0
      src/Windows/Avalonia.Win32/Interop/Automation/IGridProvider.cs
  83. 19 0
      src/Windows/Avalonia.Win32/Interop/Automation/IInvokeProvider.cs
  84. 16 0
      src/Windows/Avalonia.Win32/Interop/Automation/IMultipleViewProvider.cs
  85. 19 0
      src/Windows/Avalonia.Win32/Interop/Automation/IRangeValueProvider.cs
  86. 15 0
      src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderAdviseEvents.cs
  87. 34 0
      src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderFragment.cs
  88. 14 0
      src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderFragmentRoot.cs
  89. 285 0
      src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderSimple.cs
  90. 14 0
      src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderSimple2.cs
  91. 13 0
      src/Windows/Avalonia.Win32/Interop/Automation/IScrollItemProvider.cs
  92. 21 0
      src/Windows/Avalonia.Win32/Interop/Automation/IScrollProvider.cs
  93. 17 0
      src/Windows/Avalonia.Win32/Interop/Automation/ISelectionItemProvider.cs
  94. 15 0
      src/Windows/Avalonia.Win32/Interop/Automation/ISelectionProvider.cs
  95. 26 0
      src/Windows/Avalonia.Win32/Interop/Automation/ISynchronizedInputProvider.cs
  96. 14 0
      src/Windows/Avalonia.Win32/Interop/Automation/ITableItemProvider.cs
  97. 24 0
      src/Windows/Avalonia.Win32/Interop/Automation/ITableProvider.cs
  98. 30 0
      src/Windows/Avalonia.Win32/Interop/Automation/ITextProvider.cs
  99. 48 0
      src/Windows/Avalonia.Win32/Interop/Automation/ITextRangeProvider.cs
  100. 15 0
      src/Windows/Avalonia.Win32/Interop/Automation/IToggleProvider.cs

+ 28 - 0
src/Avalonia.Controls/Automation/AutomationElementIdentifiers.cs

@@ -0,0 +1,28 @@
+using Avalonia.Automation.Peers;
+
+namespace Avalonia.Automation
+{
+    /// <summary>
+    /// Contains values used as automation property identifiers by UI Automation providers.
+    /// </summary>
+    public static class AutomationElementIdentifiers
+    {
+        /// <summary>
+        /// Identifies the bounding rectangle automation property. The bounding rectangle property
+        /// value is returned by the <see cref="AutomationPeer.GetBoundingRectangle"/> method.
+        /// </summary>
+        public static AutomationProperty BoundingRectangleProperty { get; } = new();
+
+        /// <summary>
+        /// Identifies the class name automation property. The class name property value is returned
+        /// by the <see cref="AutomationPeer.GetClassName"/> method.
+        /// </summary>
+        public static AutomationProperty ClassNameProperty { get; } = new();
+
+        /// <summary>
+        /// Identifies the name automation property. The class name property value is returned
+        /// by the <see cref="AutomationPeer.GetName"/> method.
+        /// </summary>
+        public static AutomationProperty NameProperty { get; } = new();
+    }
+}

+ 28 - 0
src/Avalonia.Controls/Automation/AutomationLiveSetting.cs

@@ -0,0 +1,28 @@
+namespace Avalonia.Automation
+{
+    /// <summary>
+    /// Describes the notification characteristics of a particular live region
+    /// </summary>
+    public enum AutomationLiveSetting
+    {
+        /// <summary>
+        /// The element does not send notifications if the content of the live region has changed.
+        /// </summary>
+        Off = 0,
+
+        /// <summary>
+        /// The element sends non-interruptive notifications if the content of the live region has
+        /// changed. With this setting, UI Automation clients and assistive technologies are expected 
+        /// to not interrupt the user to inform of changes to the live region.
+        /// </summary>
+        Polite = 1,
+
+        /// <summary>
+        /// The element sends interruptive notifications if the content of the live region has changed. 
+        /// With this setting, UI Automation clients and assistive technologies are expected to interrupt 
+        /// the user to inform of changes to the live region.
+        /// </summary>
+        Assertive = 2,
+    }
+}
+

+ 539 - 0
src/Avalonia.Controls/Automation/AutomationProperties.cs

@@ -0,0 +1,539 @@
+using System;
+using Avalonia.Controls;
+
+namespace Avalonia.Automation
+{
+    public static class AutomationProperties
+    {
+        internal const int AutomationPositionInSetDefault = -1;
+        internal const int AutomationSizeOfSetDefault = -1;
+
+        /// <summary>
+        /// Defines the AutomationProperties.AcceleratorKey attached property.
+        /// </summary>
+        public static readonly AttachedProperty<string> AcceleratorKeyProperty =
+            AvaloniaProperty.RegisterAttached<StyledElement, string>(
+                "AcceleratorKey",
+                typeof(AutomationProperties));
+
+        /// <summary>
+        /// Defines the AutomationProperties.AccessKey attached property
+        /// </summary>
+        public static readonly AttachedProperty<string> AccessKeyProperty =
+            AvaloniaProperty.RegisterAttached<StyledElement, string>(
+                "AccessKey",
+                typeof(AutomationProperties));
+
+        /// <summary>
+        /// Defines the AutomationProperties.AutomationId attached property.
+        /// </summary>
+        public static readonly AttachedProperty<string> AutomationIdProperty =
+            AvaloniaProperty.RegisterAttached<StyledElement, string>(
+                "AutomationId",
+                typeof(AutomationProperties));
+
+        /// <summary>
+        /// Defines the AutomationProperties.HelpText attached property.
+        /// </summary>
+        public static readonly AttachedProperty<string> HelpTextProperty =
+            AvaloniaProperty.RegisterAttached<StyledElement, string>(
+                "HelpText",
+                typeof(AutomationProperties));
+
+        /// <summary>
+        /// Defines the AutomationProperties.IsColumnHeader attached property.
+        /// </summary>
+        public static readonly AttachedProperty<bool> IsColumnHeaderProperty =
+            AvaloniaProperty.RegisterAttached<StyledElement, bool>(
+                "IsColumnHeader",
+                typeof(AutomationProperties),
+                false);
+
+        /// <summary>
+        /// Defines the AutomationProperties.IsRequiredForForm attached property.
+        /// </summary>
+        public static readonly AttachedProperty<bool> IsRequiredForFormProperty =
+            AvaloniaProperty.RegisterAttached<StyledElement, bool>(
+                "IsRequiredForForm",
+                typeof(AutomationProperties),
+                false);
+
+        /// <summary>
+        /// Defines the AutomationProperties.IsRowHeader attached property.
+        /// </summary>
+        public static readonly AttachedProperty<bool> IsRowHeaderProperty =
+            AvaloniaProperty.RegisterAttached<StyledElement, bool>(
+                "IsRowHeader",
+                typeof(AutomationProperties),
+                false);
+
+        /// <summary>
+        /// Defines the AutomationProperties.IsOffscreenBehavior attached property.
+        /// </summary>
+        public static readonly AttachedProperty<IsOffscreenBehavior> IsOffscreenBehaviorProperty =
+            AvaloniaProperty.RegisterAttached<StyledElement, IsOffscreenBehavior>(
+                "IsOffscreenBehavior",
+                typeof(AutomationProperties),
+                IsOffscreenBehavior.Default);
+
+        /// <summary>
+        /// Defines the AutomationProperties.ItemStatus attached property.
+        /// </summary>
+        public static readonly AttachedProperty<string> ItemStatusProperty =
+            AvaloniaProperty.RegisterAttached<StyledElement, string>(
+                "ItemStatus",
+                typeof(AutomationProperties));
+
+        /// <summary>
+        /// Defines the AutomationProperties.ItemType attached property.
+        /// </summary>
+        public static readonly AttachedProperty<string> ItemTypeProperty =
+            AvaloniaProperty.RegisterAttached<StyledElement, string>(
+                "ItemType",
+                typeof(AutomationProperties));
+
+        /// <summary>
+        /// Defines the AutomationProperties.LabeledBy attached property.
+        /// </summary>
+        public static readonly AttachedProperty<IControl> LabeledByProperty =
+            AvaloniaProperty.RegisterAttached<StyledElement, IControl>(
+                "LabeledBy",
+                typeof(AutomationProperties));
+
+        /// <summary>
+        /// Defines the AutomationProperties.LiveSetting attached property.
+        /// </summary>
+        public static readonly AttachedProperty<AutomationLiveSetting> LiveSettingProperty =
+            AvaloniaProperty.RegisterAttached<StyledElement, AutomationLiveSetting>(
+                "LiveSetting",
+                typeof(AutomationProperties),
+                AutomationLiveSetting.Off);
+
+        /// <summary>
+        /// Defines the AutomationProperties.Name attached attached property.
+        /// </summary>
+        public static readonly AttachedProperty<string> NameProperty =
+            AvaloniaProperty.RegisterAttached<StyledElement, string>(
+                "Name",
+                typeof(AutomationProperties));
+
+        /// <summary>
+        /// Defines the AutomationProperties.PositionInSet attached property.
+        /// </summary>
+        /// <remarks>
+        /// The PositionInSet property describes the ordinal location of the element within a set
+        /// of elements which are considered to be siblings. PositionInSet works in coordination
+        /// with the SizeOfSet property to describe the ordinal location in the set.
+        /// </remarks>
+        public static readonly AttachedProperty<int> PositionInSetProperty =
+            AvaloniaProperty.RegisterAttached<StyledElement, int>(
+                "PositionInSet",
+                typeof(AutomationProperties),
+                AutomationPositionInSetDefault);
+
+        /// <summary>
+        /// Defines the AutomationProperties.SizeOfSet attached property.
+        /// </summary>
+        /// <remarks>
+        /// The SizeOfSet property describes the count of automation elements in a group or set
+        /// that are considered to be siblings. SizeOfSet works in coordination with the PositionInSet
+        /// property to describe the count of items in the set.
+        /// </remarks>
+        public static readonly AttachedProperty<int> SizeOfSetProperty =
+            AvaloniaProperty.RegisterAttached<StyledElement, int>(
+                "SizeOfSet",
+                typeof(AutomationProperties),
+                AutomationSizeOfSetDefault);
+
+        /// <summary>
+        /// Helper for setting AcceleratorKey property on a StyledElement. 
+        /// </summary>
+        public static void SetAcceleratorKey(StyledElement element, string value)
+        {
+            if (element == null)
+            {
+                throw new ArgumentNullException(nameof(element));
+            }
+
+            element.SetValue(AcceleratorKeyProperty, value);
+        }
+
+        /// <summary>
+        /// Helper for reading AcceleratorKey property from a StyledElement.
+        /// </summary>
+        public static string GetAcceleratorKey(StyledElement element)
+        {
+            if (element == null)
+            {
+                throw new ArgumentNullException(nameof(element));
+            }
+
+            return ((string)element.GetValue(AcceleratorKeyProperty));
+        }
+
+        /// <summary>
+        /// Helper for setting AccessKey property on a StyledElement. 
+        /// </summary>
+        public static void SetAccessKey(StyledElement element, string value)
+        {
+            if (element == null)
+            {
+                throw new ArgumentNullException(nameof(element));
+            }
+
+            element.SetValue(AccessKeyProperty, value);
+        }
+
+        /// <summary>
+        /// Helper for reading AccessKey property from a StyledElement.
+        /// </summary>
+        public static string GetAccessKey(StyledElement element)
+        {
+            if (element == null)
+            {
+                throw new ArgumentNullException(nameof(element));
+            }
+
+            return ((string)element.GetValue(AccessKeyProperty));
+        }
+
+        /// <summary>
+        /// Helper for setting AutomationId property on a StyledElement. 
+        /// </summary>
+        public static void SetAutomationId(StyledElement element, string value)
+        {
+            if (element == null)
+            {
+                throw new ArgumentNullException(nameof(element));
+            }
+
+            element.SetValue(AutomationIdProperty, value);
+        }
+
+        /// <summary>
+        /// Helper for reading AutomationId property from a StyledElement.
+        /// </summary>
+        public static string GetAutomationId(StyledElement element)
+        {
+            if (element == null)
+            {
+                throw new ArgumentNullException(nameof(element));
+            }
+
+            return element.GetValue(AutomationIdProperty);
+        }
+
+        /// <summary>
+        /// Helper for setting HelpText property on a StyledElement. 
+        /// </summary>
+        public static void SetHelpText(StyledElement element, string value)
+        {
+            if (element == null)
+            {
+                throw new ArgumentNullException(nameof(element));
+            }
+
+            element.SetValue(HelpTextProperty, value);
+        }
+
+        /// <summary>
+        /// Helper for reading HelpText property from a StyledElement.
+        /// </summary>
+        public static string GetHelpText(StyledElement element)
+        {
+            if (element == null)
+            {
+                throw new ArgumentNullException(nameof(element));
+            }
+
+            return ((string)element.GetValue(HelpTextProperty));
+        }
+
+        /// <summary>
+        /// Helper for setting IsColumnHeader property on a StyledElement. 
+        /// </summary>
+        public static void SetIsColumnHeader(StyledElement element, bool value)
+        {
+            if (element == null)
+            {
+                throw new ArgumentNullException(nameof(element));
+            }
+
+            element.SetValue(IsColumnHeaderProperty, value);
+        }
+
+        /// <summary>
+        /// Helper for reading IsColumnHeader property from a StyledElement.
+        /// </summary>
+        public static bool GetIsColumnHeader(StyledElement element)
+        {
+            if (element == null)
+            {
+                throw new ArgumentNullException(nameof(element));
+            }
+
+            return ((bool)element.GetValue(IsColumnHeaderProperty));
+        }
+
+        /// <summary>
+        /// Helper for setting IsRequiredForForm property on a StyledElement. 
+        /// </summary>
+        public static void SetIsRequiredForForm(StyledElement element, bool value)
+        {
+            if (element == null)
+            {
+                throw new ArgumentNullException(nameof(element));
+            }
+
+            element.SetValue(IsRequiredForFormProperty, value);
+        }
+
+        /// <summary>
+        /// Helper for reading IsRequiredForForm property from a StyledElement.
+        /// </summary>
+        public static bool GetIsRequiredForForm(StyledElement element)
+        {
+            if (element == null)
+            {
+                throw new ArgumentNullException(nameof(element));
+            }
+
+            return ((bool)element.GetValue(IsRequiredForFormProperty));
+        }
+
+        /// <summary>
+        /// Helper for reading IsRowHeader property from a StyledElement.
+        /// </summary>
+        public static bool GetIsRowHeader(StyledElement element)
+        {
+            if (element == null)
+            {
+                throw new ArgumentNullException(nameof(element));
+            }
+
+            return ((bool)element.GetValue(IsRowHeaderProperty));
+        }
+
+        /// <summary>
+        /// Helper for setting IsRowHeader property on a StyledElement. 
+        /// </summary>
+        public static void SetIsRowHeader(StyledElement element, bool value)
+        {
+            if (element == null)
+            {
+                throw new ArgumentNullException(nameof(element));
+            }
+
+            element.SetValue(IsRowHeaderProperty, value);
+        }
+
+        /// <summary>
+        /// Helper for setting IsOffscreenBehavior property on a StyledElement. 
+        /// </summary>
+        public static void SetIsOffscreenBehavior(StyledElement element, IsOffscreenBehavior value)
+        {
+            if (element == null)
+            {
+                throw new ArgumentNullException(nameof(element));
+            }
+
+            element.SetValue(IsOffscreenBehaviorProperty, value);
+        }
+
+        /// <summary>
+        /// Helper for reading IsOffscreenBehavior property from a StyledElement.
+        /// </summary>
+        public static IsOffscreenBehavior GetIsOffscreenBehavior(StyledElement element)
+        {
+            if (element == null)
+            {
+                throw new ArgumentNullException(nameof(element));
+            }
+
+            return ((IsOffscreenBehavior)element.GetValue(IsOffscreenBehaviorProperty));
+        }
+
+        /// <summary>
+        /// Helper for setting ItemStatus property on a StyledElement. 
+        /// </summary>
+        public static void SetItemStatus(StyledElement element, string value)
+        {
+            if (element == null)
+            {
+                throw new ArgumentNullException(nameof(element));
+            }
+
+            element.SetValue(ItemStatusProperty, value);
+        }
+
+        /// <summary>
+        /// Helper for reading ItemStatus property from a StyledElement.
+        /// </summary>
+        public static string GetItemStatus(StyledElement element)
+        {
+            if (element == null)
+            {
+                throw new ArgumentNullException(nameof(element));
+            }
+
+            return ((string)element.GetValue(ItemStatusProperty));
+        }
+
+        /// <summary>
+        /// Helper for setting ItemType property on a StyledElement. 
+        /// </summary>
+        public static void SetItemType(StyledElement element, string value)
+        {
+            if (element == null)
+            {
+                throw new ArgumentNullException(nameof(element));
+            }
+
+            element.SetValue(ItemTypeProperty, value);
+        }
+
+        /// <summary>
+        /// Helper for reading ItemType property from a StyledElement.
+        /// </summary>
+        public static string GetItemType(StyledElement element)
+        {
+            if (element == null)
+            {
+                throw new ArgumentNullException(nameof(element));
+            }
+
+            return ((string)element.GetValue(ItemTypeProperty));
+        }
+
+        /// <summary>
+        /// Helper for setting LabeledBy property on a StyledElement. 
+        /// </summary>
+        public static void SetLabeledBy(StyledElement element, IControl value)
+        {
+            if (element == null)
+            {
+                throw new ArgumentNullException(nameof(element));
+            }
+
+            element.SetValue(LabeledByProperty, value);
+        }
+
+        /// <summary>
+        /// Helper for reading LabeledBy property from a StyledElement.
+        /// </summary>
+        public static IControl GetLabeledBy(StyledElement element)
+        {
+            if (element == null)
+            {
+                throw new ArgumentNullException(nameof(element));
+            }
+
+            return element.GetValue(LabeledByProperty);
+        }
+
+        /// <summary>
+        /// Helper for setting LiveSetting property on a StyledElement. 
+        /// </summary>
+        public static void SetLiveSetting(StyledElement element, AutomationLiveSetting value)
+        {
+            if (element == null)
+            {
+                throw new ArgumentNullException(nameof(element));
+            }
+
+            element.SetValue(LiveSettingProperty, value);
+        }
+
+        /// <summary>
+        /// Helper for reading LiveSetting property from a StyledElement.
+        /// </summary>
+        public static AutomationLiveSetting GetLiveSetting(StyledElement element)
+        {
+            if (element == null)
+            {
+                throw new ArgumentNullException(nameof(element));
+            }
+
+            return ((AutomationLiveSetting)element.GetValue(LiveSettingProperty));
+        }
+
+        /// <summary>
+        /// Helper for setting Name property on a StyledElement. 
+        /// </summary>
+        public static void SetName(StyledElement element, string value)
+        {
+            if (element == null)
+            {
+                throw new ArgumentNullException(nameof(element));
+            }
+
+            element.SetValue(NameProperty, value);
+        }
+
+        /// <summary>
+        /// Helper for reading Name property from a StyledElement.
+        /// </summary>
+        public static string GetName(StyledElement element)
+        {
+            if (element == null)
+            {
+                throw new ArgumentNullException(nameof(element));
+            }
+
+            return ((string)element.GetValue(NameProperty));
+        }
+
+        /// <summary>
+        /// Helper for setting PositionInSet property on a StyledElement. 
+        /// </summary>
+        public static void SetPositionInSet(StyledElement element, int value)
+        {
+            if (element == null)
+            {
+                throw new ArgumentNullException(nameof(element));
+            }
+
+            element.SetValue(PositionInSetProperty, value);
+        }
+
+        /// <summary>
+        /// Helper for reading PositionInSet property from a StyledElement.
+        /// </summary>
+        public static int GetPositionInSet(StyledElement element)
+        {
+            if (element == null)
+            {
+                throw new ArgumentNullException(nameof(element));
+            }
+
+            return ((int)element.GetValue(PositionInSetProperty));
+        }
+
+        /// <summary>
+        /// Helper for setting SizeOfSet property on a StyledElement. 
+        /// </summary>
+        public static void SetSizeOfSet(StyledElement element, int value)
+        {
+            if (element == null)
+            {
+                throw new ArgumentNullException(nameof(element));
+            }
+
+            element.SetValue(SizeOfSetProperty, value);
+        }
+
+        /// <summary>
+        /// Helper for reading SizeOfSet property from a StyledElement.
+        /// </summary>
+        public static int GetSizeOfSet(StyledElement element)
+        {
+            if (element == null)
+            {
+                throw new ArgumentNullException(nameof(element));
+            }
+
+            return ((int)element.GetValue(SizeOfSetProperty));
+        }
+    }
+}
+

+ 11 - 0
src/Avalonia.Controls/Automation/AutomationProperty.cs

@@ -0,0 +1,11 @@
+namespace Avalonia.Automation
+{
+    /// <summary>
+    /// Identifies a property of <see cref="AutomationElementIdentifiers"/> or of a specific
+    /// control pattern.
+    /// </summary>
+    public sealed class AutomationProperty
+    {
+        internal AutomationProperty() { }
+    }
+}

+ 10 - 0
src/Avalonia.Controls/Automation/ElementNotEnabledException.cs

@@ -0,0 +1,10 @@
+using System;
+
+namespace Avalonia.Automation
+{
+    public class ElementNotEnabledException : Exception
+    {
+        public ElementNotEnabledException() : base("Element not enabled.") { }
+        public ElementNotEnabledException(string message) : base(message) { }
+    }
+}

+ 15 - 0
src/Avalonia.Controls/Automation/ExpandCollapsePatternIdentifiers.cs

@@ -0,0 +1,15 @@
+using Avalonia.Automation.Provider;
+
+namespace Avalonia.Automation
+{
+    /// <summary>
+    /// Contains values used as identifiers by <see cref="IExpandCollapseProvider"/>.
+    /// </summary>
+    public static class ExpandCollapsePatternIdentifiers
+    {
+        /// <summary>
+        /// Identifies <see cref="IExpandCollapseProvider.ExpandCollapseState"/> automation property.
+        /// </summary>
+        public static AutomationProperty ExpandCollapseStateProperty { get; } = new();
+    }
+}

+ 29 - 0
src/Avalonia.Controls/Automation/ExpandCollapseState.cs

@@ -0,0 +1,29 @@
+namespace Avalonia.Automation
+{
+    /// <summary>
+    /// Contains values that specify the <see cref="ExpandCollapseState"/> of a UI Automation element.
+    /// </summary>
+    public enum ExpandCollapseState
+    {
+        /// <summary>
+        /// No child nodes, controls, or content of the UI Automation element are displayed.
+        /// </summary>
+        Collapsed,
+
+        /// <summary>
+        /// All child nodes, controls or content of the UI Automation element are displayed.
+        /// </summary>
+        Expanded,
+
+        /// <summary>
+        /// The UI Automation element has no child nodes, controls, or content to display.
+        /// </summary>
+        LeafNode,
+
+        /// <summary>
+        /// Some, but not all, child nodes, controls, or content of the UI Automation element are
+        /// displayed.
+        /// </summary>
+        PartiallyExpanded
+    }
+}

+ 26 - 0
src/Avalonia.Controls/Automation/IsOffscreenBehavior.cs

@@ -0,0 +1,26 @@
+namespace Avalonia.Automation
+{
+    /// <summary>
+    /// This enum offers different ways of evaluating the IsOffscreen AutomationProperty
+    /// </summary>
+    public enum IsOffscreenBehavior
+    {
+        /// <summary>
+        /// The AutomationProperty IsOffscreen is calculated based on IsVisible.
+        /// </summary>
+        Default,
+        /// <summary>
+        /// The AutomationProperty IsOffscreen is false.
+        /// </summary>
+        Onscreen,
+        /// <summary>
+        /// The AutomationProperty IsOffscreen if true.
+        /// </summary>
+        Offscreen,
+        /// <summary>
+        /// The AutomationProperty IsOffscreen is calculated based on clip regions.
+        /// </summary>
+        FromClip,
+    }
+}
+

+ 227 - 0
src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs

@@ -0,0 +1,227 @@
+using System.Collections.Generic;
+using System.Globalization;
+using Avalonia.Automation.Platform;
+
+#nullable enable
+
+namespace Avalonia.Automation.Peers
+{
+    public enum AutomationControlType
+    {
+        Button,
+        Calendar,
+        CheckBox,
+        ComboBox,
+        Edit,
+        Hyperlink,
+        Image,
+        ListItem,
+        List,
+        Menu,
+        MenuBar,
+        MenuItem,
+        ProgressBar,
+        RadioButton,
+        ScrollBar,
+        Slider,
+        Spinner,
+        StatusBar,
+        Tab,
+        TabItem,
+        Text,
+        ToolBar,
+        ToolTip,
+        Tree,
+        TreeItem,
+        Custom,
+        Group,
+        Thumb,
+        DataGrid,
+        DataItem,
+        Document,
+        SplitButton,
+        Window,
+        Pane,
+        Header,
+        HeaderItem,
+        Table,
+        TitleBar,
+        Separator,
+    }
+
+    /// <summary>
+    /// Provides a base class that exposes an element to UI Automation.
+    /// </summary>
+    public abstract class AutomationPeer
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AutomationPeer"/> class.
+        /// </summary>
+        /// <param name="factory">
+        /// The factory to use to create the platform automation node.
+        /// </param>
+        protected AutomationPeer(IAutomationNodeFactory factory)
+        {
+            Node = factory.CreateNode(this);
+        }
+
+        /// <summary>
+        /// Gets the related node in the platform UI Automation tree.
+        /// </summary>
+        public IAutomationNode Node { get; }
+
+        /// <summary>
+        /// Attempts to bring the element associated with the automation peer into view.
+        /// </summary>
+        public void BringIntoView() => BringIntoViewCore();
+
+        /// <summary>
+        /// Gets the control type for the element that is associated with the UI Automation peer.
+        /// </summary>
+        public AutomationControlType GetAutomationControlType() => GetAutomationControlTypeCore();
+
+        /// <summary>
+        /// Gets the automation ID of the element that is associated with the UI Automation peer.
+        /// </summary>
+        public string? GetAutomationId() => GetAutomationIdCore();
+
+        /// <summary>
+        /// Gets the bounding rectangle of the element that is associated with the automation peer
+        /// in top-level coordinates.
+        /// </summary>
+        public Rect GetBoundingRectangle() => GetBoundingRectangleCore();
+
+        /// <summary>
+        /// Gets the child automation peers.
+        /// </summary>
+        public IReadOnlyList<AutomationPeer> GetChildren() => GetOrCreateChildrenCore();
+
+        /// <summary>
+        /// Gets a string that describes the class of the element.
+        /// </summary>
+        public string GetClassName() => GetClassNameCore() ?? string.Empty;
+
+        /// <summary>
+        /// Gets a human-readable localized string that represents the type of the control that is
+        /// associated with this automation peer.
+        /// </summary>
+        public string GetLocalizedControlType() => GetLocalizedControlTypeCore();
+
+        /// <summary>
+        /// Gets text that describes the element that is associated with this automation peer.
+        /// </summary>
+        public string GetName() => GetNameCore() ?? string.Empty;
+
+        /// <summary>
+        /// Gets the <see cref="AutomationPeer"/> that is the parent of this <see cref="AutomationPeer"/>.
+        /// </summary>
+        /// <returns></returns>
+        public AutomationPeer? GetParent() => GetParentCore();
+
+        /// <summary>
+        /// Gets a value that indicates whether the element that is associated with this automation
+        /// peer currently has keyboard focus.
+        /// </summary>
+        public bool HasKeyboardFocus() => HasKeyboardFocusCore();
+
+        /// <summary>
+        /// Gets a value that indicates whether the element that is associated with this automation
+        /// peer contains data that is presented to the user.
+        /// </summary>
+        public bool IsContentElement() => IsControlElement() && IsContentElementCore();
+
+        /// <summary>
+        /// Gets a value that indicates whether the element is understood by the user as
+        /// interactive or as contributing to the logical structure of the control in the GUI.
+        /// </summary>
+        public bool IsControlElement() => IsControlElementCore();
+
+        /// <summary>
+        /// Gets a value indicating whether the control is enabled for user interaction.
+        /// </summary>
+        public bool IsEnabled() => IsEnabledCore();
+
+        /// <summary>
+        /// Gets a value that indicates whether the element can accept keyboard focus.
+        /// </summary>
+        /// <returns></returns>
+        public bool IsKeyboardFocusable() => IsKeyboardFocusableCore();
+
+        /// <summary>
+        /// Sets the keyboard focus on the element that is associated with this automation peer.
+        /// </summary>
+        public void SetFocus() => SetFocusCore();
+
+        /// <summary>
+        /// Shows the context menu for the element that is associated with this automation peer.
+        /// </summary>
+        /// <returns>true if a context menu is present for the element; otherwise false.</returns>
+        public bool ShowContextMenu() => ShowContextMenuCore();
+
+        /// <summary>
+        /// Raises an event to notify the automation client of a changed property value.
+        /// </summary>
+        /// <param name="automationProperty">The property that changed.</param>
+        /// <param name="oldValue">The previous value of the property.</param>
+        /// <param name="newValue">The new value of the property.</param>
+        public void RaisePropertyChangedEvent(
+            AutomationProperty automationProperty,
+            object? oldValue,
+            object? newValue)
+        {
+            Node.PropertyChanged(automationProperty, oldValue, newValue);
+        }
+
+        protected virtual string GetLocalizedControlTypeCore()
+        {
+            var controlType = GetAutomationControlType();
+
+            return controlType switch
+            {
+                AutomationControlType.CheckBox => "check box",
+                AutomationControlType.ComboBox => "combo box",
+                AutomationControlType.ListItem => "list item",
+                AutomationControlType.MenuBar => "menu bar",
+                AutomationControlType.MenuItem => "menu item",
+                AutomationControlType.ProgressBar => "progress bar",
+                AutomationControlType.RadioButton => "radio button",
+                AutomationControlType.ScrollBar => "scroll bar",
+                AutomationControlType.StatusBar => "status bar",
+                AutomationControlType.TabItem => "tab item",
+                AutomationControlType.ToolBar => "toolbar",
+                AutomationControlType.ToolTip => "tooltip",
+                AutomationControlType.TreeItem => "tree item",
+                AutomationControlType.Custom => "custom",
+                AutomationControlType.DataGrid => "data grid",
+                AutomationControlType.DataItem => "data item",
+                AutomationControlType.SplitButton => "split button",
+                AutomationControlType.HeaderItem => "header item",
+                AutomationControlType.TitleBar => "title bar",
+                _ => controlType.ToString().ToLowerInvariant(),
+            };
+        }
+
+        protected abstract void BringIntoViewCore();
+        protected abstract AutomationControlType GetAutomationControlTypeCore();
+        protected abstract string? GetAutomationIdCore();
+        protected abstract Rect GetBoundingRectangleCore();
+        protected abstract IReadOnlyList<AutomationPeer> GetOrCreateChildrenCore();
+        protected abstract string GetClassNameCore();
+        protected abstract string? GetNameCore();
+        protected abstract AutomationPeer? GetParentCore();
+        protected abstract bool HasKeyboardFocusCore();
+        protected abstract bool IsContentElementCore();
+        protected abstract bool IsControlElementCore();
+        protected abstract bool IsEnabledCore();
+        protected abstract bool IsKeyboardFocusableCore();
+        protected abstract void SetFocusCore();
+        protected abstract bool ShowContextMenuCore();
+        protected internal abstract bool TrySetParent(AutomationPeer? parent);
+
+        protected void EnsureEnabled()
+        {
+            if (!IsEnabled())
+                throw new ElementNotEnabledException();
+        }
+    }
+}

+ 29 - 0
src/Avalonia.Controls/Automation/Peers/ButtonAutomationPeer.cs

@@ -0,0 +1,29 @@
+using Avalonia.Automation.Platform;
+using Avalonia.Automation.Provider;
+using Avalonia.Controls;
+
+#nullable enable
+
+namespace Avalonia.Automation.Peers
+{
+    public class ButtonAutomationPeer : ContentControlAutomationPeer,
+        IInvokeProvider
+    {
+        public ButtonAutomationPeer(IAutomationNodeFactory factory,  Button owner)
+            : base(factory, owner) 
+        {
+        }
+        
+        public void Invoke()
+        {
+            EnsureEnabled();
+            (Owner as Button)?.PerformClick();
+        }
+
+        protected override AutomationControlType GetAutomationControlTypeCore()
+        {
+            return AutomationControlType.Button;
+        }
+    }
+}
+

+ 20 - 0
src/Avalonia.Controls/Automation/Peers/CheckBoxAutomationPeer.cs

@@ -0,0 +1,20 @@
+using Avalonia.Automation.Platform;
+using Avalonia.Controls;
+
+#nullable enable
+
+namespace Avalonia.Automation.Peers
+{
+    public class CheckBoxAutomationPeer : ToggleButtonAutomationPeer
+    {
+        public CheckBoxAutomationPeer(IAutomationNodeFactory factory, CheckBox owner)
+            : base(factory, owner) 
+        {
+        }
+
+        protected override AutomationControlType GetAutomationControlTypeCore()
+        {
+            return AutomationControlType.CheckBox;
+        }
+    }
+}

+ 123 - 0
src/Avalonia.Controls/Automation/Peers/ComboBoxAutomationPeer.cs

@@ -0,0 +1,123 @@
+using System.Collections.Generic;
+using Avalonia.Automation.Platform;
+using Avalonia.Automation.Provider;
+using Avalonia.Controls;
+
+#nullable enable
+
+namespace Avalonia.Automation.Peers
+{
+    public class ComboBoxAutomationPeer : SelectingItemsControlAutomationPeer,
+        IExpandCollapseProvider
+    {
+        private UnrealizedSelectionPeer[]? _selection;
+
+        public ComboBoxAutomationPeer(IAutomationNodeFactory factory, ComboBox owner)
+            : base(factory, owner) 
+        {
+        }
+
+        public new ComboBox Owner => (ComboBox)base.Owner;
+
+        public ExpandCollapseState ExpandCollapseState => ToState(Owner.IsDropDownOpen);
+        public void Collapse() => Owner.IsDropDownOpen = false;
+        public void Expand() => Owner.IsDropDownOpen = true;
+
+        protected override AutomationControlType GetAutomationControlTypeCore()
+        {
+            return AutomationControlType.ComboBox;
+        }
+
+        protected override IReadOnlyList<AutomationPeer>? GetSelectionCore()
+        {
+            if (ExpandCollapseState == ExpandCollapseState.Expanded)
+                return base.GetSelectionCore();
+
+            // If the combo box is not open then we won't have an ItemsPresenter so the default
+            // GetSelectionCore implementation won't work. For this case we create a separate
+            // peer to represent the unrealized item.
+            if (Owner.SelectedItem is object selection)
+            {
+                _selection ??= new[] { new UnrealizedSelectionPeer(Node.Factory, this) };
+                _selection[0].Item = selection;
+                return _selection;
+            }
+
+            return null;
+        }
+
+        protected override void OwnerPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
+        {
+            base.OwnerPropertyChanged(sender, e);
+
+            if (e.Property == ComboBox.IsDropDownOpenProperty)
+            {
+                RaisePropertyChangedEvent(
+                    ExpandCollapsePatternIdentifiers.ExpandCollapseStateProperty,
+                    ToState((bool)e.OldValue!),
+                    ToState((bool)e.NewValue!));
+            }
+        }
+
+        private ExpandCollapseState ToState(bool value)
+        {
+            return value ? ExpandCollapseState.Expanded : ExpandCollapseState.Collapsed;
+        }
+
+        private class UnrealizedSelectionPeer : UnrealizedElementAutomationPeer
+        {
+            private readonly ComboBoxAutomationPeer _owner;
+            private object? _item;
+
+            public UnrealizedSelectionPeer(IAutomationNodeFactory factory, ComboBoxAutomationPeer owner)
+                : base(factory) 
+            {
+                _owner = owner;
+            }
+
+            public object? Item
+            {
+                get => _item;
+                set
+                {
+                    if (_item != value)
+                    {
+                        var oldValue = GetNameCore();
+                        _item = value;
+                        RaisePropertyChangedEvent(
+                            AutomationElementIdentifiers.NameProperty,
+                            oldValue,
+                            GetNameCore());
+                    }
+                }
+            }
+
+            protected override string? GetAutomationIdCore() => null;
+            protected override string GetClassNameCore() => typeof(ComboBoxItem).Name;
+            protected override AutomationPeer? GetParentCore() => _owner;
+            protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.ListItem;
+
+            protected override string? GetNameCore()
+            {
+                if (_item is Control c)
+                {
+                    var result = AutomationProperties.GetName(c);
+
+                    if (result is null && c is ContentControl cc && cc.Presenter?.Child is TextBlock text)
+                    {
+                        result = text.Text;
+                    }
+
+                    if (result is null)
+                    {
+                        result = c.GetValue(ContentControl.ContentProperty)?.ToString();
+                    }
+
+                    return result;
+                }
+
+                return _item?.ToString();
+            }
+        }
+    }
+}

+ 39 - 0
src/Avalonia.Controls/Automation/Peers/ContentControlAutomationPeer.cs

@@ -0,0 +1,39 @@
+using Avalonia.Automation.Platform;
+using Avalonia.Controls;
+
+#nullable enable
+
+namespace Avalonia.Automation.Peers
+{
+    public class ContentControlAutomationPeer : ControlAutomationPeer
+    {
+        protected ContentControlAutomationPeer(IAutomationNodeFactory factory, ContentControl owner)
+            : base(factory, owner) 
+        { 
+        }
+
+        public new ContentControl Owner => (ContentControl)base.Owner;
+
+        protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Pane;
+
+        protected override string? GetNameCore()
+        {
+            var result = base.GetNameCore();
+
+            if (result is null && Owner.Presenter?.Child is TextBlock text)
+            {
+                result = text.Text;
+            }
+
+            if (result is null)
+            {
+                result = Owner.Content?.ToString();
+            }
+
+            return result;
+        }
+
+        protected override bool IsContentElementCore() => false;
+        protected override bool IsControlElementCore() => false;
+    }
+}

+ 196 - 0
src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs

@@ -0,0 +1,196 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Automation.Platform;
+using Avalonia.Controls;
+using Avalonia.VisualTree;
+
+#nullable enable
+
+namespace Avalonia.Automation.Peers
+{
+    /// <summary>
+    /// An automation peer which represents a <see cref="Control"/> element.
+    /// </summary>
+    public class ControlAutomationPeer : AutomationPeer
+    {
+        private IReadOnlyList<AutomationPeer>? _children;
+        private bool _childrenValid;
+        private AutomationPeer? _parent;
+        private bool _parentValid;
+
+        public ControlAutomationPeer(IAutomationNodeFactory factory, Control owner)
+            : base(factory)
+        {
+            Owner = owner ?? throw new ArgumentNullException("owner");
+
+            owner.PropertyChanged += OwnerPropertyChanged;
+            var visualChildren = ((IVisual)owner).VisualChildren;
+            visualChildren.CollectionChanged += VisualChildrenChanged;
+        }
+
+        public Control Owner { get; }
+
+        public static AutomationPeer GetOrCreatePeer(IAutomationNodeFactory factory, Control element)
+        {
+            element = element ?? throw new ArgumentNullException("element");
+            return element.GetOrCreateAutomationPeer(factory);
+        }
+
+        public AutomationPeer GetOrCreatePeer(Control element)
+        {
+            return element == Owner ? this : GetOrCreatePeer(Node.Factory, element);
+        }
+
+        protected override void BringIntoViewCore() => Owner.BringIntoView();
+        protected override string? GetAutomationIdCore() => AutomationProperties.GetAutomationId(Owner);
+        protected override Rect GetBoundingRectangleCore() => GetBounds(Owner.TransformedBounds);
+
+        protected virtual IReadOnlyList<AutomationPeer>? GetChildrenCore()
+        {
+            var children = ((IVisual)Owner).VisualChildren;
+
+            if (children.Count == 0)
+                return null;
+
+            var result = new List<AutomationPeer>();
+
+            foreach (var child in children)
+            {
+                if (child is Control c && c.IsVisible)
+                {
+                    result.Add(GetOrCreatePeer(c));
+                }
+            }
+
+            return result;
+        }
+
+        protected override AutomationPeer? GetParentCore()
+        {
+            EnsureConnected();
+            return _parent;
+        }
+
+        /// <summary>
+        /// Invalidates the peer's children and causes a re-read from <see cref="GetChildrenCore"/>.
+        /// </summary>
+        protected void InvalidateChildren()
+        {
+            _childrenValid = false;
+            Node!.ChildrenChanged();
+        }
+
+        /// <summary>
+        /// Invalidates the peer's parent.
+        /// </summary>
+        protected void InvalidateParent()
+        {
+            _parent = null;
+            _parentValid = false;
+        }
+
+        protected override IReadOnlyList<AutomationPeer> GetOrCreateChildrenCore()
+        {
+            var children = _children ?? Array.Empty<AutomationPeer>();
+
+            if (_childrenValid)
+                return children;
+
+            var newChildren = GetChildrenCore() ?? Array.Empty<AutomationPeer>();
+
+            foreach (var peer in children.Except(newChildren))
+                peer.TrySetParent(null);
+            foreach (var peer in newChildren)
+                peer.TrySetParent(this);
+
+            _childrenValid = true;
+            return _children = newChildren;
+        }
+
+        protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Custom;
+        protected override string GetClassNameCore() => Owner.GetType().Name;
+        protected override string? GetNameCore() => AutomationProperties.GetName(Owner);
+        protected override bool HasKeyboardFocusCore() => Owner.IsFocused;
+        protected override bool IsContentElementCore() => true;
+        protected override bool IsControlElementCore() => true;
+        protected override bool IsEnabledCore() => Owner.IsEnabled;
+        protected override bool IsKeyboardFocusableCore() => Owner.Focusable;
+        protected override void SetFocusCore() => Owner.Focus();
+
+        protected override bool ShowContextMenuCore()
+        {
+            var c = Owner;
+
+            while (c is object)
+            {
+                if (c.ContextMenu is object)
+                {
+                    c.ContextMenu.Open(c);
+                    return true;
+                }
+
+                c = c.Parent as Control;
+            }
+
+            return false;
+        }
+
+        protected internal override bool TrySetParent(AutomationPeer? parent)
+        {
+            _parent = parent;
+            return true;
+        }
+
+        private Rect GetBounds(TransformedBounds? bounds)
+        {
+            return bounds?.Bounds.TransformToAABB(bounds!.Value.Transform) ?? default;
+        }
+
+        private void VisualChildrenChanged(object sender, EventArgs e) => InvalidateChildren();
+
+        private void OwnerPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Property == Visual.IsVisibleProperty)
+            {
+                var parent = Owner.GetVisualParent();
+                if (parent is Control c)
+                    (GetOrCreatePeer(c) as ControlAutomationPeer)?.InvalidateChildren();
+            }
+            else if (e.Property == Visual.TransformedBoundsProperty)
+            {
+                RaisePropertyChangedEvent(
+                    AutomationElementIdentifiers.BoundingRectangleProperty,
+                    GetBounds((TransformedBounds?)e.OldValue),
+                    GetBounds((TransformedBounds?)e.NewValue));
+            }
+            else if (e.Property == Visual.VisualParentProperty)
+            {
+                InvalidateParent();
+            }
+        }
+
+
+        private void EnsureConnected()
+        {
+            if (!_parentValid)
+            {
+                var parent = Owner.GetVisualParent();
+
+                while (parent is object)
+                {
+                    if (parent is Control c)
+                    {
+                        var parentPeer = GetOrCreatePeer(c);
+                        parentPeer.GetChildren();
+                    }
+
+                    parent = parent.GetVisualParent();
+                }
+
+                _parentValid = true;
+            }
+        }
+    }
+}
+

+ 20 - 0
src/Avalonia.Controls/Automation/Peers/ImageAutomationPeer.cs

@@ -0,0 +1,20 @@
+using Avalonia.Automation.Platform;
+using Avalonia.Controls;
+
+#nullable enable
+
+namespace Avalonia.Automation.Peers
+{
+    public class ImageAutomationPeer : ControlAutomationPeer
+    {
+        public ImageAutomationPeer(IAutomationNodeFactory factory, Control owner)
+            : base(factory, owner)
+        {
+        }
+
+        protected override AutomationControlType GetAutomationControlTypeCore()
+        {
+            return AutomationControlType.Image;
+        }
+    }
+}

+ 57 - 0
src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs

@@ -0,0 +1,57 @@
+using Avalonia.Automation.Platform;
+using Avalonia.Automation.Provider;
+using Avalonia.Controls;
+
+#nullable enable
+
+namespace Avalonia.Automation.Peers
+{
+    public class ItemsControlAutomationPeer : ControlAutomationPeer, IScrollProvider
+    {
+        private bool _searchedForScrollable;
+        private IScrollProvider? _scroller;
+
+        public ItemsControlAutomationPeer(IAutomationNodeFactory factory, ItemsControl owner)
+            : base(factory, owner)
+        {
+        }
+
+        public new ItemsControl Owner => (ItemsControl)base.Owner;
+        public bool HorizontallyScrollable => _scroller?.HorizontallyScrollable ?? false;
+        public double HorizontalScrollPercent => _scroller?.HorizontalScrollPercent ?? -1;
+        public double HorizontalViewSize => _scroller?.HorizontalViewSize ?? 0;
+        public bool VerticallyScrollable => _scroller?.VerticallyScrollable ?? false;
+        public double VerticalScrollPercent => _scroller?.VerticalScrollPercent ?? -1;
+        public double VerticalViewSize => _scroller?.VerticalViewSize ?? 0;
+
+        protected virtual IScrollProvider? Scroller
+        {
+            get
+            {
+                if (!_searchedForScrollable)
+                {
+                    if (Owner.GetValue(ListBox.ScrollProperty) is Control scrollable)
+                        _scroller = GetOrCreatePeer(scrollable) as IScrollProvider;
+                    _searchedForScrollable = true;
+                }
+
+                return _scroller;
+            }
+        }
+
+        protected override AutomationControlType GetAutomationControlTypeCore()
+        {
+            return AutomationControlType.List;
+        }
+
+        public void Scroll(ScrollAmount horizontalAmount, ScrollAmount verticalAmount)
+        {
+            _scroller?.Scroll(horizontalAmount, verticalAmount);
+        }
+
+        public void SetScrollPercent(double horizontalPercent, double verticalPercent)
+        {
+            _scroller?.SetScrollPercent(horizontalPercent, verticalPercent);
+        }
+    }
+}

+ 82 - 0
src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs

@@ -0,0 +1,82 @@
+using System;
+using Avalonia.Automation.Platform;
+using Avalonia.Automation.Provider;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Selection;
+
+#nullable enable
+
+namespace Avalonia.Automation.Peers
+{
+    public class ListItemAutomationPeer : ContentControlAutomationPeer,
+        ISelectionItemProvider
+    {
+        public ListItemAutomationPeer(IAutomationNodeFactory factory, ContentControl owner)
+            : base(factory, owner)
+        {
+        }
+
+        public bool IsSelected => Owner.GetValue(ListBoxItem.IsSelectedProperty);
+
+        public ISelectionProvider? SelectionContainer
+        {
+            get
+            {
+                if (Owner.Parent is Control parent)
+                {
+                    var parentPeer = GetOrCreatePeer(parent);
+                    return parentPeer as ISelectionProvider;
+                }
+
+                return null;
+            }
+        }
+
+        public void Select()
+        {
+            EnsureEnabled();
+
+            if (Owner.Parent is SelectingItemsControl parent)
+            {
+                var index = parent.ItemContainerGenerator.IndexFromContainer(Owner);
+
+                if (index != -1)
+                    parent.SelectedIndex = index;
+            }
+        }
+
+        void ISelectionItemProvider.AddToSelection()
+        {
+            EnsureEnabled();
+
+            if (Owner.Parent is ItemsControl parent &&
+                parent.GetValue(ListBox.SelectionProperty) is ISelectionModel selectionModel)
+            {
+                var index = parent.ItemContainerGenerator.IndexFromContainer(Owner);
+
+                if (index != -1)
+                    selectionModel.Select(index);
+            }
+        }
+
+        void ISelectionItemProvider.RemoveFromSelection()
+        {
+            EnsureEnabled();
+
+            if (Owner.Parent is ItemsControl parent &&
+                parent.GetValue(ListBox.SelectionProperty) is ISelectionModel selectionModel)
+            {
+                var index = parent.ItemContainerGenerator.IndexFromContainer(Owner);
+
+                if (index != -1)
+                    selectionModel.Deselect(index);
+            }
+        }
+
+        protected override AutomationControlType GetAutomationControlTypeCore()
+        {
+            return AutomationControlType.ListItem;
+        }
+    }
+}

+ 22 - 0
src/Avalonia.Controls/Automation/Peers/MenuAutomationPeer.cs

@@ -0,0 +1,22 @@
+using Avalonia.Automation.Platform;
+using Avalonia.Controls;
+
+#nullable enable
+
+namespace Avalonia.Automation.Peers
+{
+    public class MenuAutomationPeer : ControlAutomationPeer
+    {
+        public MenuAutomationPeer(IAutomationNodeFactory factory, Menu owner)
+            : base(factory, owner) 
+        { 
+        }
+
+        protected override AutomationControlType GetAutomationControlTypeCore()
+        {
+            return AutomationControlType.Menu;
+        }
+
+        protected override bool IsContentElementCore() => false;
+    }
+}

+ 39 - 0
src/Avalonia.Controls/Automation/Peers/MenuItemAutomationPeer.cs

@@ -0,0 +1,39 @@
+using Avalonia.Automation.Platform;
+using Avalonia.Controls;
+
+#nullable enable
+
+namespace Avalonia.Automation.Peers
+{
+    public class MenuItemAutomationPeer : ControlAutomationPeer
+    {
+        public MenuItemAutomationPeer(IAutomationNodeFactory factory, MenuItem owner)
+            : base(factory, owner) 
+        { 
+        }
+
+        public new MenuItem Owner => (MenuItem)base.Owner;
+
+        protected override AutomationControlType GetAutomationControlTypeCore()
+        {
+            return AutomationControlType.MenuItem;
+        }
+
+        protected override string? GetNameCore()
+        {
+            var result = base.GetNameCore();
+
+            if (result is null && Owner.HeaderPresenter.Child is TextBlock text)
+            {
+                result = text.Text;
+            }
+
+            if (result is null)
+            {
+                result = Owner.Header?.ToString();
+            }
+
+            return result;
+        }
+    }
+}

+ 28 - 0
src/Avalonia.Controls/Automation/Peers/NoneAutomationPeer.cs

@@ -0,0 +1,28 @@
+using Avalonia.Automation.Platform;
+using Avalonia.Controls;
+
+#nullable enable
+
+namespace Avalonia.Automation.Peers
+{
+    /// <summary>
+    /// An automation peer which represents an element that is exposed to automation as non-
+    /// interactive or as not contributing to the logical structure of the application.
+    /// </summary>
+    public class NoneAutomationPeer : ControlAutomationPeer
+    {
+        public NoneAutomationPeer(IAutomationNodeFactory factory, Control owner)
+            : base(factory, owner) 
+        { 
+        }
+
+        protected override AutomationControlType GetAutomationControlTypeCore()
+        {
+            return AutomationControlType.Pane;
+        }
+
+        protected override bool IsContentElementCore() => false;
+        protected override bool IsControlElementCore() => false;
+    }
+}
+

+ 36 - 0
src/Avalonia.Controls/Automation/Peers/PopupRootAutomationPeer.cs

@@ -0,0 +1,36 @@
+using System;
+using Avalonia.Automation.Platform;
+using Avalonia.Controls.Primitives;
+
+#nullable enable
+
+namespace Avalonia.Automation.Peers
+{
+    public class PopupRootAutomationPeer : WindowBaseAutomationPeer
+    {
+        public PopupRootAutomationPeer(IAutomationNodeFactory factory, PopupRoot owner)
+            : base(factory, owner)
+        {
+            if (owner.IsVisible)
+                StartTrackingFocus();
+            else
+                owner.Opened += OnOpened;
+            owner.Closed += OnClosed;
+        }
+
+        protected override bool IsContentElementCore() => false;
+        protected override bool IsControlElementCore() => false;
+
+        private void OnOpened(object sender, EventArgs e)
+        {
+            ((PopupRoot)Owner).Opened -= OnOpened;
+            StartTrackingFocus();
+        }
+
+        private void OnClosed(object sender, EventArgs e)
+        {
+            ((PopupRoot)Owner).Closed -= OnClosed;
+            StopTrackingFocus();
+        }
+    }
+}

+ 34 - 0
src/Avalonia.Controls/Automation/Peers/RangeBaseAutomationPeer.cs

@@ -0,0 +1,34 @@
+using Avalonia.Automation.Platform;
+using Avalonia.Automation.Provider;
+using Avalonia.Controls.Primitives;
+
+#nullable enable
+
+namespace Avalonia.Automation.Peers
+{
+    public abstract class RangeBaseAutomationPeer : ControlAutomationPeer, IRangeValueProvider
+    {
+        public RangeBaseAutomationPeer(IAutomationNodeFactory factory, RangeBase owner)
+            : base(factory, owner) 
+        {
+            owner.PropertyChanged += OwnerPropertyChanged;
+        }
+
+        public new RangeBase Owner => (RangeBase)base.Owner;
+        public virtual bool IsReadOnly => false;
+        public double Maximum => Owner.Maximum;
+        public double Minimum => Owner.Minimum;
+        public double Value => Owner.Value;
+        public void SetValue(double value) => Owner.Value = value;
+
+        protected virtual void OwnerPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Property == RangeBase.MinimumProperty)
+                RaisePropertyChangedEvent(RangeValuePatternIdentifiers.MinimumProperty, e.OldValue, e.NewValue);
+            else if (e.Property == RangeBase.MaximumProperty)
+                RaisePropertyChangedEvent(RangeValuePatternIdentifiers.MaximumProperty, e.OldValue, e.NewValue);
+            else if (e.Property == RangeBase.ValueProperty)
+                RaisePropertyChangedEvent(RangeValuePatternIdentifiers.ValueProperty, e.OldValue, e.NewValue);
+        }
+    }
+}

+ 175 - 0
src/Avalonia.Controls/Automation/Peers/ScrollViewerAutomationPeer.cs

@@ -0,0 +1,175 @@
+using System;
+using Avalonia.Automation.Platform;
+using Avalonia.Automation.Provider;
+using Avalonia.Controls;
+using Avalonia.Utilities;
+
+#nullable enable
+
+namespace Avalonia.Automation.Peers
+{
+    public class ScrollViewerAutomationPeer : ControlAutomationPeer, IScrollProvider
+    {
+        public ScrollViewerAutomationPeer(IAutomationNodeFactory factory, ScrollViewer owner)
+            : base(factory, owner)
+        {
+        }
+
+        public new ScrollViewer Owner => (ScrollViewer)base.Owner;
+
+        public bool HorizontallyScrollable
+        {
+            get => MathUtilities.GreaterThan(Owner.Extent.Width, Owner.Viewport.Width);
+        }
+
+        public double HorizontalScrollPercent
+        {
+            get
+            {
+                if (!HorizontallyScrollable)
+                    return ScrollPatternIdentifiers.NoScroll;
+                return (double)(Owner.Offset.X * 100.0 / (Owner.Extent.Width - Owner.Viewport.Width));
+            }
+        }
+
+        public double HorizontalViewSize
+        {
+            get
+            {
+                if (MathUtilities.IsZero(Owner.Extent.Width))
+                    return 100;
+                return Math.Min(100, Owner.Viewport.Width * 100.0 / Owner.Extent.Width);
+            }
+        }
+
+        public bool VerticallyScrollable
+        {
+            get => MathUtilities.GreaterThan(Owner.Extent.Height, Owner.Viewport.Height);
+        }
+
+        public double VerticalScrollPercent
+        {
+            get
+            {
+                if (!VerticallyScrollable)
+                    return ScrollPatternIdentifiers.NoScroll;
+                return (double)(Owner.Offset.Y * 100.0 / (Owner.Extent.Height - Owner.Viewport.Height));
+            }
+        }
+
+        public double VerticalViewSize
+        {
+            get
+            {
+                if (MathUtilities.IsZero(Owner.Extent.Height))
+                    return 100;
+                return Math.Min(100, Owner.Viewport.Height * 100.0 / Owner.Extent.Height);
+            }
+        }
+
+        protected override AutomationControlType GetAutomationControlTypeCore()
+        {
+            return AutomationControlType.Pane;
+        }
+
+        protected override bool IsContentElementCore() => false;
+
+        protected override bool IsControlElementCore()
+        {
+            // Return false if the control is part of a control template.
+            return Owner.TemplatedParent is null && base.IsControlElementCore();
+        }
+
+        public void Scroll(ScrollAmount horizontalAmount, ScrollAmount verticalAmount)
+        {
+            if (!IsEnabled())
+                throw new ElementNotEnabledException();
+
+            var scrollHorizontally = horizontalAmount != ScrollAmount.NoAmount;
+            var scrollVertically = verticalAmount != ScrollAmount.NoAmount;
+
+            if (scrollHorizontally && !HorizontallyScrollable || scrollVertically && !VerticallyScrollable)
+            {
+                throw new InvalidOperationException("Operation cannot be performed");
+            }
+
+            switch (horizontalAmount)
+            {
+                case ScrollAmount.LargeDecrement:
+                    Owner.PageLeft();
+                    break;
+                case ScrollAmount.SmallDecrement:
+                    Owner.LineLeft();
+                    break;
+                case ScrollAmount.SmallIncrement:
+                    Owner.LineRight();
+                    break;
+                case ScrollAmount.LargeIncrement:
+                    Owner.PageRight();
+                    break;
+                case ScrollAmount.NoAmount:
+                    break;
+                default:
+                    throw new InvalidOperationException("Operation cannot be performed");
+            }
+
+            switch (verticalAmount)
+            {
+                case ScrollAmount.LargeDecrement:
+                    Owner.PageUp();
+                    break;
+                case ScrollAmount.SmallDecrement:
+                    Owner.LineUp();
+                    break;
+                case ScrollAmount.SmallIncrement:
+                    Owner.LineDown();
+                    break;
+                case ScrollAmount.LargeIncrement:
+                    Owner.PageDown();
+                    break;
+                case ScrollAmount.NoAmount:
+                    break;
+                default:
+                    throw new InvalidOperationException("Operation cannot be performed");
+            }
+        }
+
+        public void SetScrollPercent(double horizontalPercent, double verticalPercent)
+        {
+            if (!IsEnabled())
+                throw new ElementNotEnabledException();
+
+            var scrollHorizontally = horizontalPercent != ScrollPatternIdentifiers.NoScroll;
+            var scrollVertically = verticalPercent != ScrollPatternIdentifiers.NoScroll;
+
+            if (scrollHorizontally && !HorizontallyScrollable || scrollVertically && !VerticallyScrollable)
+            {
+                throw new InvalidOperationException("Operation cannot be performed");
+            }
+
+            if (scrollHorizontally && (horizontalPercent < 0.0) || (horizontalPercent > 100.0))
+            {
+                throw new ArgumentOutOfRangeException("horizontalPercent");
+            }
+
+            if (scrollVertically && (verticalPercent < 0.0) || (verticalPercent > 100.0))
+            {
+                throw new ArgumentOutOfRangeException("verticalPercent");
+            }
+
+            var offset = Owner.Offset;
+
+            if (scrollHorizontally)
+            {
+                offset = offset.WithX((Owner.Extent.Width - Owner.Viewport.Width) * horizontalPercent * 0.01);
+            }
+            
+            if (scrollVertically)
+            {
+                offset = offset.WithY((Owner.Extent.Height - Owner.Viewport.Height) * verticalPercent * 0.01);
+            }
+
+            Owner.Offset = offset;
+        }
+    }
+}

+ 87 - 0
src/Avalonia.Controls/Automation/Peers/SelectingItemsControlAutomationPeer.cs

@@ -0,0 +1,87 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Automation.Platform;
+using Avalonia.Automation.Provider;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Selection;
+using Avalonia.VisualTree;
+
+#nullable enable
+
+namespace Avalonia.Automation.Peers
+{
+    public abstract class SelectingItemsControlAutomationPeer : ItemsControlAutomationPeer,
+        ISelectionProvider
+    {
+        private ISelectionModel _selection;
+
+        protected SelectingItemsControlAutomationPeer(IAutomationNodeFactory factory, SelectingItemsControl owner)
+            : base(factory, owner) 
+        {
+            _selection = owner.GetValue(ListBox.SelectionProperty);
+            _selection.SelectionChanged += OwnerSelectionChanged;
+            owner.PropertyChanged += OwnerPropertyChanged;
+        }
+
+        public bool CanSelectMultiple => GetSelectionModeCore().HasFlagCustom(SelectionMode.Multiple);
+        public bool IsSelectionRequired => GetSelectionModeCore().HasFlagCustom(SelectionMode.AlwaysSelected);
+        public IReadOnlyList<AutomationPeer> GetSelection() => GetSelectionCore() ?? Array.Empty<AutomationPeer>();
+
+        protected virtual IReadOnlyList<AutomationPeer>? GetSelectionCore()
+        {
+            List<AutomationPeer>? result = null;
+
+            if (Owner is SelectingItemsControl owner)
+            {
+                var selection = Owner.GetValue(ListBox.SelectionProperty);
+
+                foreach (var i in selection.SelectedIndexes)
+                {
+                    var container = owner.ItemContainerGenerator.ContainerFromIndex(i);
+
+                    if (container is Control c && ((IVisual)c).IsAttachedToVisualTree)
+                    {
+                        var peer = GetOrCreatePeer(c);
+
+                        if (peer is object)
+                        {
+                            result ??= new List<AutomationPeer>();
+                            result.Add(peer);
+                        }
+                    }
+                }
+
+                return result;
+            }
+
+            return result;
+        }
+
+        protected virtual SelectionMode GetSelectionModeCore()
+        {
+            return (Owner as SelectingItemsControl)?.GetValue(ListBox.SelectionModeProperty) ?? SelectionMode.Single;
+        }
+
+        protected virtual void OwnerPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.Property == ListBox.SelectionProperty)
+            {
+                _selection.SelectionChanged -= OwnerSelectionChanged;
+                _selection = Owner.GetValue(ListBox.SelectionProperty);
+                _selection.SelectionChanged += OwnerSelectionChanged;
+                RaiseSelectionChanged();
+            }
+        }
+
+        protected virtual void OwnerSelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e)
+        {
+            RaiseSelectionChanged();
+        }
+
+        private void RaiseSelectionChanged()
+        {
+            RaisePropertyChangedEvent(SelectionPatternIdentifiers.SelectionProperty, null, null);
+        }
+    }
+}

+ 20 - 0
src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs

@@ -0,0 +1,20 @@
+using Avalonia.Automation.Platform;
+using Avalonia.Controls;
+
+#nullable enable
+
+namespace Avalonia.Automation.Peers
+{
+    public class SliderAutomationPeer : RangeBaseAutomationPeer
+    {
+        public SliderAutomationPeer(IAutomationNodeFactory factory, Slider owner)
+            : base(factory, owner)
+        {
+        }
+
+        protected override AutomationControlType GetAutomationControlTypeCore()
+        {
+            return AutomationControlType.Slider;
+        }
+    }
+}

+ 20 - 0
src/Avalonia.Controls/Automation/Peers/TabControlAutomationPeer.cs

@@ -0,0 +1,20 @@
+using Avalonia.Automation.Platform;
+using Avalonia.Controls;
+
+#nullable enable
+
+namespace Avalonia.Automation.Peers
+{
+    public class TabControlAutomationPeer : SelectingItemsControlAutomationPeer
+    {
+        public TabControlAutomationPeer(IAutomationNodeFactory factory, TabControl owner)
+            : base(factory, owner) 
+        {
+        }
+
+        protected override AutomationControlType GetAutomationControlTypeCore()
+        {
+            return AutomationControlType.Tab;
+        }
+    }
+}

+ 18 - 0
src/Avalonia.Controls/Automation/Peers/TabItemAutomationPeer.cs

@@ -0,0 +1,18 @@
+using Avalonia.Automation.Platform;
+using Avalonia.Controls;
+
+namespace Avalonia.Automation.Peers
+{
+    public class TabItemAutomationPeer : ListItemAutomationPeer
+    {
+        public TabItemAutomationPeer(IAutomationNodeFactory factory, TabItem owner)
+            : base(factory, owner)
+        {
+        }
+
+        protected override AutomationControlType GetAutomationControlTypeCore()
+        {
+            return AutomationControlType.TabItem;
+        }
+    }
+}

+ 28 - 0
src/Avalonia.Controls/Automation/Peers/TextAutomationPeer.cs

@@ -0,0 +1,28 @@
+using Avalonia.Automation.Platform;
+using Avalonia.Controls;
+
+#nullable enable
+
+namespace Avalonia.Automation.Peers
+{
+    public class TextAutomationPeer : ControlAutomationPeer
+    {
+        public TextAutomationPeer(IAutomationNodeFactory factory, Control owner)
+            : base(factory, owner)
+        {
+        }
+
+        protected override AutomationControlType GetAutomationControlTypeCore()
+        {
+            return AutomationControlType.Text;
+        }
+
+        protected override string? GetNameCore() => Owner.GetValue(TextBlock.TextProperty);
+
+        protected override bool IsControlElementCore()
+        {
+            // Return false if the control is part of a control template.
+            return Owner.TemplatedParent is null && base.IsControlElementCore();
+        }
+    }
+}

+ 21 - 0
src/Avalonia.Controls/Automation/Peers/TextBoxAutomationPeer.cs

@@ -0,0 +1,21 @@
+using Avalonia.Automation.Platform;
+using Avalonia.Automation.Provider;
+using Avalonia.Controls;
+
+#nullable enable
+
+namespace Avalonia.Automation.Peers
+{
+    public class TextBoxAutomationPeer : TextAutomationPeer, IValueProvider
+    {
+        public TextBoxAutomationPeer(IAutomationNodeFactory factory, TextBox owner)
+            : base(factory, owner)
+        {
+        }
+
+        public new TextBox Owner => (TextBox)base.Owner;
+        public bool IsReadOnly => Owner.IsReadOnly;
+        public string? Value => Owner.Text;
+        public void SetValue(string? value) => Owner.Text = value;
+    }
+}

+ 39 - 0
src/Avalonia.Controls/Automation/Peers/ToggleButtonAutomationPeer.cs

@@ -0,0 +1,39 @@
+using Avalonia.Automation.Platform;
+using Avalonia.Automation.Provider;
+using Avalonia.Controls.Primitives;
+
+#nullable enable
+
+namespace Avalonia.Automation.Peers
+{
+    public class ToggleButtonAutomationPeer : ContentControlAutomationPeer, IToggleProvider
+    {
+        public ToggleButtonAutomationPeer(IAutomationNodeFactory factory, ToggleButton owner)
+            : base(factory, owner)
+        {
+        }
+
+        public new ToggleButton Owner => (ToggleButton)base.Owner;
+
+        ToggleState IToggleProvider.ToggleState
+        {
+            get => Owner.IsChecked switch
+            {
+                true => ToggleState.On,
+                false => ToggleState.Off,
+                null => ToggleState.Indeterminate,
+            };
+        }
+
+        void IToggleProvider.Toggle()
+        {
+            EnsureEnabled();
+            Owner.PerformClick();
+        }
+
+        protected override AutomationControlType GetAutomationControlTypeCore()
+        {
+            return AutomationControlType.Button;
+        }
+    }
+}

+ 32 - 0
src/Avalonia.Controls/Automation/Peers/UnrealizedElementAutomationPeer.cs

@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Automation.Platform;
+
+#nullable enable
+
+namespace Avalonia.Automation.Peers
+{
+    /// <summary>
+    /// An automation peer which represents an unrealized element
+    /// </summary>
+    public abstract class UnrealizedElementAutomationPeer : AutomationPeer
+    {
+        protected UnrealizedElementAutomationPeer(IAutomationNodeFactory factory)
+            : base(factory)
+        {
+        }
+
+        public void SetParent(AutomationPeer? parent) => TrySetParent(parent);
+        protected override void BringIntoViewCore() => GetParent()?.BringIntoView();
+        protected override Rect GetBoundingRectangleCore() => GetParent()?.GetBoundingRectangle() ?? default;
+        protected override IReadOnlyList<AutomationPeer> GetOrCreateChildrenCore() => Array.Empty<AutomationPeer>();
+        protected override bool HasKeyboardFocusCore() => false;
+        protected override bool IsContentElementCore() => false;
+        protected override bool IsControlElementCore() => false;
+        protected override bool IsEnabledCore() => true;
+        protected override bool IsKeyboardFocusableCore() => false;
+        protected override void SetFocusCore() { }
+        protected override bool ShowContextMenuCore() => false;
+        protected internal override bool TrySetParent(AutomationPeer? parent) => false;
+    }
+}

+ 39 - 0
src/Avalonia.Controls/Automation/Peers/WindowAutomationPeer.cs

@@ -0,0 +1,39 @@
+using System;
+using Avalonia.Automation.Platform;
+using Avalonia.Controls;
+
+#nullable enable
+
+namespace Avalonia.Automation.Peers
+{
+    public class WindowAutomationPeer : WindowBaseAutomationPeer
+    {
+        public WindowAutomationPeer(IAutomationNodeFactory factory, Window owner)
+            : base(factory, owner)
+        {
+            if (owner.IsVisible)
+                StartTrackingFocus();
+            else
+                owner.Opened += OnOpened;
+            owner.Closed += OnClosed;
+        }
+
+        public new Window Owner => (Window)base.Owner;
+
+        protected override string GetNameCore() => Owner.Title;
+
+        private void OnOpened(object sender, EventArgs e)
+        {
+            Owner.Opened -= OnOpened;
+            StartTrackingFocus();
+        }
+
+        private void OnClosed(object sender, EventArgs e)
+        {
+            Owner.Closed -= OnClosed;
+            StopTrackingFocus();
+        }
+    }
+}
+
+

+ 72 - 0
src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs

@@ -0,0 +1,72 @@
+using System.ComponentModel;
+using Avalonia.Automation.Platform;
+using Avalonia.Automation.Provider;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Platform;
+using Avalonia.VisualTree;
+
+#nullable enable
+
+namespace Avalonia.Automation.Peers
+{
+    public class WindowBaseAutomationPeer : ControlAutomationPeer, IRootProvider
+    {
+        private Control? _focus;
+
+        public WindowBaseAutomationPeer(IAutomationNodeFactory factory, WindowBase owner)
+            : base(factory, owner)
+        {
+        }
+
+        public new WindowBase Owner => (WindowBase)base.Owner;
+        public ITopLevelImpl PlatformImpl => Owner.PlatformImpl;
+
+        protected override AutomationControlType GetAutomationControlTypeCore()
+        {
+            return AutomationControlType.Window;
+        }
+
+        public AutomationPeer? GetFocus() => _focus is object ? GetOrCreatePeer(_focus) : null;
+
+        public AutomationPeer? GetPeerFromPoint(Point p)
+        {
+            var hit = Owner.GetVisualAt(p)?.FindAncestorOfType<Control>(includeSelf: true);
+            return hit is object ? GetOrCreatePeer(hit) : null;
+        }
+
+        protected void StartTrackingFocus()
+        {
+            KeyboardDevice.Instance.PropertyChanged += KeyboardDevicePropertyChanged;
+            OnFocusChanged(KeyboardDevice.Instance.FocusedElement);
+        }
+
+        protected void StopTrackingFocus()
+        {
+            KeyboardDevice.Instance.PropertyChanged -= KeyboardDevicePropertyChanged;
+        }
+
+        private void OnFocusChanged(IInputElement? focus)
+        {
+            var oldFocus = _focus;
+            
+            _focus = focus?.VisualRoot == Owner ? focus as Control : null;
+            
+            if (_focus != oldFocus)
+            {
+                var peer = _focus is object ? GetOrCreatePeer(_focus) : null;
+                ((IRootAutomationNode)Node).FocusChanged(peer);
+            }
+        }
+
+        private void KeyboardDevicePropertyChanged(object sender, PropertyChangedEventArgs e)
+        {
+            if (e.PropertyName == nameof(KeyboardDevice.FocusedElement))
+            {
+                OnFocusChanged(KeyboardDevice.Instance.FocusedElement);
+            }
+        }
+    }
+}
+
+

+ 32 - 0
src/Avalonia.Controls/Automation/Platform/IAutomationNode.cs

@@ -0,0 +1,32 @@
+using System;
+using Avalonia.Automation.Peers;
+
+#nullable enable
+
+namespace Avalonia.Automation.Platform
+{
+    /// <summary>
+    /// Represents a platform implementation of a node in the UI Automation tree.
+    /// </summary>
+    public interface IAutomationNode
+    {
+        /// <summary>
+        /// Gets a factory which can be used to create child nodes.
+        /// </summary>
+        IAutomationNodeFactory Factory { get; }
+
+        /// <summary>
+        /// Called by the <see cref="AutomationPeer"/> when the children of the peer change.
+        /// </summary>
+        void ChildrenChanged();
+
+        /// <summary>
+        /// Called by the <see cref="AutomationPeer"/> when a property other than the parent,
+        /// children or root changes.
+        /// </summary>
+        /// <param name="property">The property that changed.</param>
+        /// <param name="oldValue">The previous value of the property.</param>
+        /// <param name="newValue">The new value of the property.</param>
+        void PropertyChanged(AutomationProperty property, object? oldValue, object? newValue);
+    }
+}

+ 18 - 0
src/Avalonia.Controls/Automation/Platform/IAutomationNodeFactory.cs

@@ -0,0 +1,18 @@
+using Avalonia.Automation.Peers;
+
+#nullable enable
+
+namespace Avalonia.Automation.Platform
+{
+    /// <summary>
+    /// Creates nodes in the UI Automation tree of the underlying platform.
+    /// </summary>
+    public interface IAutomationNodeFactory
+    {
+        /// <summary>
+        /// Creates an automation node for a peer.
+        /// </summary>
+        /// <param name="peer">The peer.</param>
+        IAutomationNode CreateNode(AutomationPeer peer);
+    }
+}

+ 20 - 0
src/Avalonia.Controls/Automation/Platform/IRootAutomationNode.cs

@@ -0,0 +1,20 @@
+using Avalonia.Automation.Peers;
+
+#nullable enable
+
+namespace Avalonia.Automation.Platform
+{
+    /// <summary>
+    /// Represents a platform implementation of a root node in the UI Automation tree.
+    /// </summary>
+    public interface IRootAutomationNode : IAutomationNode
+    {
+        /// <summary>
+        /// Called by the <see cref="IRootProvider"/> when its focus changes.
+        /// </summary>
+        /// <param name="focus">
+        /// The automation peer for the newly focused control or null if no control is focused.
+        /// </param>
+        void FocusChanged(AutomationPeer? focus);
+    }
+}

+ 24 - 0
src/Avalonia.Controls/Automation/Provider/IExpandCollapseProvider.cs

@@ -0,0 +1,24 @@
+namespace Avalonia.Automation.Provider
+{
+    /// <summary>
+    /// Exposes methods and properties to support UI Automation client access to controls that
+    /// visually expand to display content and collapse to hide content.
+    /// </summary>
+    public interface IExpandCollapseProvider
+    {
+        /// <summary>
+        /// Gets the state, expanded or collapsed, of the control.
+        /// </summary>
+        ExpandCollapseState ExpandCollapseState { get; }
+
+        /// <summary>
+        /// Displays all child nodes, controls, or content of the control.
+        /// </summary>
+        void Expand();
+
+        /// <summary>
+        /// Hides all nodes, controls, or content that are descendants of the control.
+        /// </summary>
+        void Collapse();
+    }
+}

+ 15 - 0
src/Avalonia.Controls/Automation/Provider/IInvokeProvider.cs

@@ -0,0 +1,15 @@
+namespace Avalonia.Automation.Provider
+{
+    /// <summary>
+    /// Exposes methods and properties to support UI Automation client access to controls that
+    /// initiate or perform a single, unambiguous action and do not maintain state when
+    /// activated.
+    /// </summary>
+    public interface IInvokeProvider
+    {
+        /// <summary>
+        /// Sends a request to activate a control and initiate its single, unambiguous action.
+        /// </summary>
+        void Invoke();
+    }
+}

+ 37 - 0
src/Avalonia.Controls/Automation/Provider/IRangeValueProvider.cs

@@ -0,0 +1,37 @@
+#nullable enable
+
+namespace Avalonia.Automation.Provider
+{
+    /// <summary>
+    /// Exposes methods and properties to support access by a UI Automation client to controls
+    /// that can be set to a value within a range.
+    /// </summary>
+    public interface IRangeValueProvider
+    {
+        /// <summary>
+        /// Gets a value that indicates whether the value of a control is read-only.
+        /// </summary>
+        bool IsReadOnly { get; }
+
+        /// <summary>
+        /// Gets the minimum range value that is supported by the control.
+        /// </summary>
+        double Minimum { get; }
+
+        /// <summary>
+        /// Gets the maximum range value that is supported by the control.
+        /// </summary>
+        double Maximum { get; }
+
+        /// <summary>
+        /// Gets the value of the control.
+        /// </summary>
+        double Value { get; }
+
+        /// <summary>
+        /// Sets the value of the control.
+        /// </summary>
+        /// <param name="value">The value to set.</param>
+        public void SetValue(double value);
+    }
+}

+ 14 - 0
src/Avalonia.Controls/Automation/Provider/IRootProvider.cs

@@ -0,0 +1,14 @@
+using Avalonia.Automation.Peers;
+using Avalonia.Platform;
+
+#nullable enable
+
+namespace Avalonia.Automation.Provider
+{
+    public interface IRootProvider
+    {
+        ITopLevelImpl? PlatformImpl { get; }
+        AutomationPeer? GetFocus();
+        AutomationPeer? GetPeerFromPoint(Point p);
+    }
+}

+ 71 - 0
src/Avalonia.Controls/Automation/Provider/IScrollProvider.cs

@@ -0,0 +1,71 @@
+namespace Avalonia.Automation.Provider
+{
+    public enum ScrollAmount
+    {
+        LargeDecrement,
+        SmallDecrement,
+        NoAmount,
+        LargeIncrement,
+        SmallIncrement,
+    }
+
+    /// <summary>
+    /// Exposes methods and properties to support access by a UI Automation client to a control
+    /// that acts as a scrollable container for a collection of child objects. 
+    /// </summary>
+    public interface IScrollProvider
+    {
+        /// <summary>
+        /// Gets a value that indicates whether the control can scroll horizontally.
+        /// </summary>
+        bool HorizontallyScrollable { get; }
+
+        /// <summary>
+        /// Gets the current horizontal scroll position.
+        /// </summary>
+        double HorizontalScrollPercent { get; }
+
+        /// <summary>
+        /// Gets the current horizontal view size.
+        /// </summary>
+        double HorizontalViewSize { get; }
+
+        /// <summary>
+        /// Gets a value that indicates whether the control can scroll vertically.
+        /// </summary>
+        bool VerticallyScrollable { get; }
+
+        /// <summary>
+        /// Gets the current vertical scroll position.
+        /// </summary>
+        double VerticalScrollPercent { get; }
+
+        /// <summary>
+        /// Gets the vertical view size.
+        /// </summary>
+        double VerticalViewSize { get; }
+
+        /// <summary>
+        /// Scrolls the visible region of the content area horizontally and vertically.
+        /// </summary>
+        /// <param name="horizontalAmount">The horizontal increment specific to the control.</param>
+        /// <param name="verticalAmount">The vertical increment specific to the control.</param>
+        void Scroll(ScrollAmount horizontalAmount, ScrollAmount verticalAmount);
+
+        /// <summary>
+        /// Sets the horizontal and vertical scroll position as a percentage of the total content
+        /// area within the control.
+        /// </summary>
+        /// <param name="horizontalPercent">
+        /// The horizontal position as a percentage of the content area's total range.
+        /// <see cref="ScrollPatternIdentifiers.NoScroll"/> should be passed in if the control
+        /// cannot be scrolled in this direction.
+        /// </param>
+        /// <param name="verticalPercent">
+        /// The vertical position as a percentage of the content area's total range.
+        /// <see cref="ScrollPatternIdentifiers.NoScroll"/> should be passed in if the control
+        /// cannot be scrolled in this direction.
+        /// </param>
+        void SetScrollPercent(double horizontalPercent, double verticalPercent);
+    }
+}

+ 37 - 0
src/Avalonia.Controls/Automation/Provider/ISelectionItemProvider .cs

@@ -0,0 +1,37 @@
+#nullable enable
+
+namespace Avalonia.Automation.Provider
+{
+    /// <summary>
+    /// Exposes methods and properties to support access by a UI Automation client to individual,
+    /// selectable child controls of containers that implement <see cref="ISelectionProvider"/>.
+    /// </summary>
+    public interface ISelectionItemProvider
+    {
+        /// <summary>
+        /// Gets a value that indicates whether an item is selected.
+        /// </summary>
+        bool IsSelected { get; }
+
+        /// <summary>
+        /// Gets the UI Automation provider that implements <see cref="ISelectionProvider"/> and
+        /// acts as the container for the calling object.
+        /// </summary>
+        ISelectionProvider? SelectionContainer { get; }
+
+        /// <summary>
+        /// Adds the current element to the collection of selected items.
+        /// </summary>
+        void AddToSelection();
+
+        /// <summary>
+        /// Removes the current element from the collection of selected items.
+        /// </summary>
+        void RemoveFromSelection();
+
+        /// <summary>
+        /// Clears any existing selection and then selects the current element.
+        /// </summary>
+        void Select();
+    }
+}

+ 29 - 0
src/Avalonia.Controls/Automation/Provider/ISelectionProvider.cs

@@ -0,0 +1,29 @@
+using System.Collections.Generic;
+using Avalonia.Automation.Peers;
+
+namespace Avalonia.Automation.Provider
+{
+    /// <summary>
+    /// Exposes methods and properties to support access by a UI Automation client to controls
+    /// that act as containers for a collection of individual, selectable child items.
+    /// </summary>
+    public interface ISelectionProvider
+    {
+        /// <summary>
+        /// Gets a value that indicates whether the provider allows more than one child element
+        /// to be selected concurrently.
+        /// </summary>
+        bool CanSelectMultiple { get; }
+
+        /// <summary>
+        /// Gets a value that indicates whether the provider requires at least one child element
+        /// to be selected.
+        /// </summary>
+        bool IsSelectionRequired { get; }
+
+        /// <summary>
+        /// Retrieves a provider for each child element that is selected.
+        /// </summary>
+        IReadOnlyList<AutomationPeer> GetSelection();
+    }
+}

+ 40 - 0
src/Avalonia.Controls/Automation/Provider/IToggleProvider.cs

@@ -0,0 +1,40 @@
+namespace Avalonia.Automation.Provider
+{
+    /// <summary>
+    /// Contains values that specify the toggle state of a UI Automation element.
+    /// </summary>
+    public enum ToggleState
+    {
+        /// <summary>
+        /// The UI Automation element isn't selected, checked, marked, or otherwise activated.
+        /// </summary>
+        Off,
+
+        /// <summary>
+        /// The UI Automation element is selected, checked, marked, or otherwise activated.
+        /// </summary>
+        On,
+
+        /// <summary>
+        /// The UI Automation element is in an indeterminate state.
+        /// </summary>
+        Indeterminate,
+    }
+
+    /// <summary>
+    /// Exposes methods and properties to support UI Automation client access to controls that can
+    /// cycle through a set of states and maintain a particular state. 
+    /// </summary>
+    public interface IToggleProvider
+    {
+        /// <summary>
+        /// Gets the toggle state of the control.
+        /// </summary>
+        ToggleState ToggleState { get; }
+
+        /// <summary>
+        /// Cycles through the toggle states of a control.
+        /// </summary>
+        void Toggle();
+    }
+}

+ 31 - 0
src/Avalonia.Controls/Automation/Provider/IValueProvider.cs

@@ -0,0 +1,31 @@
+#nullable enable
+
+namespace Avalonia.Automation.Provider
+{
+    /// <summary>
+    /// Exposes methods and properties to support access by a UI Automation client to controls
+    /// that have an intrinsic value that does not span a range and that can be represented as
+    /// a string.
+    /// </summary>
+    public interface IValueProvider
+    {
+        /// <summary>
+        /// Gets a value that indicates whether the value of a control is read-only.
+        /// </summary>
+        bool IsReadOnly { get; }
+
+        /// <summary>
+        /// Gets the value of the control.
+        /// </summary>
+        public string? Value { get; }
+
+        /// <summary>
+        /// Sets the value of a control.
+        /// </summary>
+        /// <param name="value">
+        /// The value to set. The provider is responsible for converting the value to the
+        /// appropriate data type.
+        /// </param>
+        public void SetValue(string? value);
+    }
+}

+ 30 - 0
src/Avalonia.Controls/Automation/RangeValuePatternIdentifiers.cs

@@ -0,0 +1,30 @@
+using Avalonia.Automation.Provider;
+
+namespace Avalonia.Automation
+{
+    /// <summary>
+    /// Contains values used as identifiers by <see cref="IRangeValueProvider"/>.
+    /// </summary>
+    public static class RangeValuePatternIdentifiers
+    {
+        /// <summary>
+        /// Identifies <see cref="IRangeValueProvider.IsReadOnly"/> automation property.
+        /// </summary>
+        public static AutomationProperty IsReadOnlyProperty { get; } = new();
+
+        /// <summary>
+        /// Identifies <see cref="IRangeValueProvider.Minimum"/> automation property.
+        /// </summary>
+        public static AutomationProperty MinimumProperty { get; } = new();
+
+        /// <summary>
+        /// Identifies <see cref="IRangeValueProvider.Maximum"/> automation property.
+        /// </summary>
+        public static AutomationProperty MaximumProperty { get; } = new();
+
+        /// <summary>
+        /// Identifies <see cref="IRangeValueProvider.Value"/> automation property.
+        /// </summary>
+        public static AutomationProperty ValueProperty { get; } = new();
+    }
+}

+ 45 - 0
src/Avalonia.Controls/Automation/ScrollPatternIdentifiers.cs

@@ -0,0 +1,45 @@
+using Avalonia.Automation.Provider;
+
+namespace Avalonia.Automation
+{
+    /// <summary>
+    /// Contains values used as identifiers by <see cref="IScrollProvider"/>.
+    /// </summary>
+    public static class ScrollPatternIdentifiers
+    {
+        /// <summary>
+        /// Specifies that scrolling should not be performed.
+        /// </summary>
+        public const double NoScroll = -1;
+
+        /// <summary>
+        /// Identifies <see cref="IScrollProvider.HorizontallyScrollable"/> automation property.
+        /// </summary>
+        public static AutomationProperty HorizontallyScrollableProperty { get; } = new();
+
+        /// <summary>
+        /// Identifies <see cref="IScrollProvider.HorizontalScrollPercent"/> automation property.
+        /// </summary>
+        public static AutomationProperty HorizontalScrollPercentProperty { get; } = new();
+
+        /// <summary>
+        /// Identifies <see cref="IScrollProvider.HorizontalViewSize"/> automation property.
+        /// </summary>
+        public static AutomationProperty HorizontalViewSizeProperty { get; } = new();
+
+        /// <summary>
+        /// Identifies <see cref="IScrollProvider.VerticallyScrollable"/> automation property.
+        /// </summary>
+        public static AutomationProperty VerticallyScrollableProperty { get; } = new();
+
+        /// <summary>
+        /// Identifies <see cref="IScrollProvider.VerticalScrollPercent"/> automation property.
+        /// </summary>
+        public static AutomationProperty VerticalScrollPercentProperty { get; } = new();
+
+        /// <summary>
+        /// Identifies <see cref="IScrollProvider.VerticalViewSize"/> automation property.
+        /// </summary>
+        public static AutomationProperty VerticalViewSizeProperty { get; } = new();
+    }
+}

+ 25 - 0
src/Avalonia.Controls/Automation/SelectionPatternIdentifiers.cs

@@ -0,0 +1,25 @@
+using Avalonia.Automation.Provider;
+
+namespace Avalonia.Automation
+{
+    /// <summary>
+    /// Contains values used as identifiers by <see cref="ISelectionProvider"/>.
+    /// </summary>
+    public static class SelectionPatternIdentifiers
+    {
+        /// <summary>
+        /// Identifies <see cref="ISelectionProvider.CanSelectMultiple"/> automation property.
+        /// </summary>
+        public static AutomationProperty CanSelectMultipleProperty { get; } = new();
+
+        /// <summary>
+        /// Identifies <see cref="ISelectionProvider.IsSelectionRequired"/> automation property.
+        /// </summary>
+        public static AutomationProperty IsSelectionRequiredProperty { get; } = new();
+
+        /// <summary>
+        /// Identifies the property that gets the selected items in a container.
+        /// </summary>
+        public static AutomationProperty SelectionProperty { get; } = new();
+    }
+}

+ 9 - 0
src/Avalonia.Controls/Button.cs

@@ -1,6 +1,8 @@
 using System;
 using System.Linq;
 using System.Windows.Input;
+using Avalonia.Automation.Peers;
+using Avalonia.Automation.Platform;
 using Avalonia.Controls.Metadata;
 using Avalonia.Data;
 using Avalonia.Input;
@@ -338,6 +340,11 @@ namespace Avalonia.Controls
             }
         }
 
+        protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory)
+        {
+            return new ButtonAutomationPeer(factory, this);
+        }
+
         protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
         {
             base.UpdateDataValidation(property, value);
@@ -354,6 +361,8 @@ namespace Avalonia.Controls
             }
         }
 
+        internal void PerformClick() => OnClick();
+
         /// <summary>
         /// Called when the <see cref="Command"/> property changes.
         /// </summary>

+ 6 - 0
src/Avalonia.Controls/CheckBox.cs

@@ -1,3 +1,5 @@
+using Avalonia.Automation.Peers;
+using Avalonia.Automation.Platform;
 using Avalonia.Controls.Primitives;
 
 namespace Avalonia.Controls
@@ -7,5 +9,9 @@ namespace Avalonia.Controls
     /// </summary>
     public class CheckBox : ToggleButton
     {
+        protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory)
+        {
+            return new CheckBoxAutomationPeer(factory, this);
+        }
     }
 }

+ 7 - 0
src/Avalonia.Controls/ComboBox.cs

@@ -1,5 +1,7 @@
 using System;
 using System.Linq;
+using Avalonia.Automation.Peers;
+using Avalonia.Automation.Platform;
 using Avalonia.Controls.Generators;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
@@ -335,6 +337,11 @@ namespace Avalonia.Controls
             _popup.Opened += PopupOpened;
         }
 
+        protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory)
+        {
+            return new ComboBoxAutomationPeer(factory, this);
+        }
+
         internal void ItemFocused(ComboBoxItem dropDownItem)
         {
             if (IsDropDownOpen && dropDownItem.IsFocused && dropDownItem.IsArrangeValid)

+ 21 - 0
src/Avalonia.Controls/Control.cs

@@ -1,5 +1,7 @@
 using System;
 using System.ComponentModel;
+using Avalonia.Automation.Peers;
+using Avalonia.Automation.Platform;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
 using Avalonia.Input;
@@ -48,6 +50,7 @@ namespace Avalonia.Controls
 
         private DataTemplates? _dataTemplates;
         private IControl? _focusAdorner;
+        private AutomationPeer? _automationPeer;
 
         /// <summary>
         /// Gets or sets the control's focus adorner.
@@ -188,5 +191,23 @@ namespace Avalonia.Controls
                 _focusAdorner = null;
             }
         }
+
+        protected virtual AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory)
+        {
+            return new NoneAutomationPeer(factory, this);
+        }
+
+        internal AutomationPeer GetOrCreateAutomationPeer(IAutomationNodeFactory factory)
+        {
+            VerifyAccess();
+
+            if (_automationPeer is object)
+            {
+                return _automationPeer;
+            }
+
+            _automationPeer = OnCreateAutomationPeer(factory);
+            return _automationPeer;
+        }
     }
 }

+ 7 - 0
src/Avalonia.Controls/Image.cs

@@ -1,3 +1,5 @@
+using Avalonia.Automation.Peers;
+using Avalonia.Automation.Platform;
 using Avalonia.Media;
 using Avalonia.Media.Imaging;
 
@@ -122,5 +124,10 @@ namespace Avalonia.Controls
                 return new Size();
             }
         }
+
+        protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory)
+        {
+            return new ImageAutomationPeer(factory, this);
+        }
     }
 }

+ 7 - 0
src/Avalonia.Controls/ItemsControl.cs

@@ -3,6 +3,8 @@ using System.Collections;
 using System.Collections.Generic;
 using System.Collections.Specialized;
 using Avalonia.Collections;
+using Avalonia.Automation.Peers;
+using Avalonia.Automation.Platform;
 using Avalonia.Controls.Generators;
 using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Presenters;
@@ -323,6 +325,11 @@ namespace Avalonia.Controls
             base.OnKeyDown(e);
         }
 
+        protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory)
+        {
+            return new ItemsControlAutomationPeer(factory, this);
+        }
+
         /// <summary>
         /// Called when the <see cref="Items"/> property changes.
         /// </summary>

+ 7 - 1
src/Avalonia.Controls/ListBoxItem.cs

@@ -1,6 +1,7 @@
+using Avalonia.Automation.Peers;
+using Avalonia.Automation.Platform;
 using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Mixins;
-using Avalonia.Input;
 
 namespace Avalonia.Controls
 {
@@ -34,5 +35,10 @@ namespace Avalonia.Controls
             get { return GetValue(IsSelectedProperty); }
             set { SetValue(IsSelectedProperty, value); }
         }
+
+        protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory)
+        {
+            return new ListItemAutomationPeer(factory, this);
+        }
     }
 }

+ 7 - 0
src/Avalonia.Controls/Menu.cs

@@ -1,3 +1,5 @@
+using Avalonia.Automation.Peers;
+using Avalonia.Automation.Platform;
 using Avalonia.Controls.Platform;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
@@ -92,5 +94,10 @@ namespace Avalonia.Controls
                 inputRoot.AccessKeyHandler.MainMenu = this;
             }
         }
+
+        protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory)
+        {
+            return new MenuAutomationPeer(factory, this);
+        }
     }
 }

+ 7 - 0
src/Avalonia.Controls/MenuItem.cs

@@ -3,6 +3,8 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Reactive.Linq;
 using System.Windows.Input;
+using Avalonia.Automation.Peers;
+using Avalonia.Automation.Platform;
 using Avalonia.Controls.Generators;
 using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Mixins;
@@ -477,6 +479,11 @@ namespace Avalonia.Controls
             }
         }
 
+        protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory)
+        {
+            return new MenuItemAutomationPeer(factory, this);
+        }
+
         protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
         {
             base.UpdateDataValidation(property, value);

+ 13 - 1
src/Avalonia.Controls/Primitives/Popup.cs

@@ -1,6 +1,8 @@
 using System;
 using System.Linq;
 using System.Reactive.Disposables;
+using Avalonia.Automation.Peers;
+using Avalonia.Automation.Platform;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives.PopupPositioning;
 using Avalonia.Input;
@@ -596,7 +598,17 @@ namespace Avalonia.Controls.Primitives
             {
                 if (PlacementTarget != null)
                 {
-                    FocusManager.Instance?.Focus(PlacementTarget);
+                    var e = (IControl?)PlacementTarget;
+
+                    while (e is object && (!e.Focusable || !e.IsEffectivelyEnabled || !e.IsVisible))
+                    {
+                        e = e.Parent;
+                    }
+
+                    if (e is object)
+                    {
+                        FocusManager.Instance?.Focus(e);
+                    }
                 }
                 else
                 {

+ 7 - 0
src/Avalonia.Controls/Primitives/PopupRoot.cs

@@ -1,6 +1,8 @@
 using System;
 using System.Collections.Generic;
 using System.Reactive.Disposables;
+using Avalonia.Automation.Peers;
+using Avalonia.Automation.Platform;
 using Avalonia.Controls.Primitives.PopupPositioning;
 using Avalonia.Interactivity;
 using Avalonia.Media;
@@ -168,5 +170,10 @@ namespace Avalonia.Controls.Primitives
                 return ClientSize;
             }
         }
+
+        protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory)
+        {
+            return new PopupRootAutomationPeer(factory, this);
+        }
     }
 }

+ 7 - 0
src/Avalonia.Controls/Primitives/ToggleButton.cs

@@ -1,4 +1,6 @@
 using System;
+using Avalonia.Automation.Peers;
+using Avalonia.Automation.Platform;
 using Avalonia.Controls.Metadata;
 using Avalonia.Data;
 using Avalonia.Interactivity;
@@ -169,6 +171,11 @@ namespace Avalonia.Controls.Primitives
             RaiseEvent(e);
         }
 
+        protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory)
+        {
+            return new ToggleButtonAutomationPeer(factory, this);
+        }
+
         private void OnIsCheckedChanged(AvaloniaPropertyChangedEventArgs e)
         {
             var newValue = (bool?)e.NewValue;

+ 7 - 0
src/Avalonia.Controls/ScrollViewer.cs

@@ -1,5 +1,7 @@
 using System;
 using System.Reactive.Linq;
+using Avalonia.Automation.Peers;
+using Avalonia.Automation.Platform;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
 using Avalonia.Input;
@@ -688,6 +690,11 @@ namespace Avalonia.Controls
             _scrollBarExpandSubscription = SubscribeToScrollBars(e);
         }
 
+        protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory)
+        {
+            return new ScrollViewerAutomationPeer(factory, this);
+        }
+
         private IDisposable SubscribeToScrollBars(TemplateAppliedEventArgs e)
         {
             static IObservable<bool> GetExpandedObservable(ScrollBar scrollBar)

+ 7 - 0
src/Avalonia.Controls/Slider.cs

@@ -1,5 +1,7 @@
 using System;
 using Avalonia.Collections;
+using Avalonia.Automation.Peers;
+using Avalonia.Automation.Platform;
 using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Mixins;
 using Avalonia.Controls.Primitives;
@@ -209,6 +211,11 @@ namespace Avalonia.Controls
             _pointerMovedDispose = this.AddDisposableHandler(PointerMovedEvent, TrackMoved, RoutingStrategies.Tunnel);
         }
 
+        protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory)
+        {
+            return new SliderAutomationPeer(factory, this);
+        }
+
         protected override void OnKeyDown(KeyEventArgs e)
         {
             base.OnKeyDown(e);

+ 7 - 0
src/Avalonia.Controls/TabControl.cs

@@ -1,6 +1,8 @@
 using System.ComponentModel;
 using System.Linq;
 using Avalonia.Collections;
+using Avalonia.Automation.Peers;
+using Avalonia.Automation.Platform;
 using Avalonia.Controls.Generators;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
@@ -230,5 +232,10 @@ namespace Avalonia.Controls
                 }
             }
         }
+
+        protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory)
+        {
+            return new TabControlAutomationPeer(factory, this);
+        }
     }
 }

+ 7 - 0
src/Avalonia.Controls/TabItem.cs

@@ -1,3 +1,5 @@
+using Avalonia.Automation.Peers;
+using Avalonia.Automation.Platform;
 using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Mixins;
 using Avalonia.Controls.Primitives;
@@ -80,5 +82,10 @@ namespace Avalonia.Controls
                 }
             }          
         }
+
+        protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory)
+        {
+            return new ListItemAutomationPeer(factory, this);
+        }
     }
 }

+ 8 - 1
src/Avalonia.Controls/TextBlock.cs

@@ -1,9 +1,11 @@
 using System.Reactive.Linq;
+using Avalonia.Automation.Peers;
+using Avalonia.Automation.Platform;
+using Avalonia.Layout;
 using Avalonia.LogicalTree;
 using Avalonia.Media;
 using Avalonia.Media.TextFormatting;
 using Avalonia.Metadata;
-using Avalonia.Layout;
 
 namespace Avalonia.Controls
 {
@@ -532,6 +534,11 @@ namespace Avalonia.Controls
             InvalidateMeasure();
         }
 
+        protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory)
+        {
+            return new TextAutomationPeer(factory, this);
+        }
+
         private static bool IsValidMaxLines(int maxLines) => maxLines >= 0;
 
         private static bool IsValidLineHeight(double lineHeight) => double.IsNaN(lineHeight) || lineHeight > 0;

+ 7 - 0
src/Avalonia.Controls/TextBox.cs

@@ -14,6 +14,8 @@ using Avalonia.Data;
 using Avalonia.Layout;
 using Avalonia.Utilities;
 using Avalonia.Controls.Metadata;
+using Avalonia.Automation.Peers;
+using Avalonia.Automation.Platform;
 
 namespace Avalonia.Controls
 {
@@ -904,6 +906,11 @@ namespace Avalonia.Controls
             }
         }
 
+        protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory)
+        {
+            return new TextBoxAutomationPeer(factory, this);
+        }
+
         protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
         {
             if (property == TextProperty)

+ 7 - 0
src/Avalonia.Controls/Window.cs

@@ -4,6 +4,8 @@ using System.ComponentModel;
 using System.Linq;
 using System.Reactive.Linq;
 using System.Threading.Tasks;
+using Avalonia.Automation.Peers;
+using Avalonia.Automation.Platform;
 using Avalonia.Controls.Platform;
 using Avalonia.Input;
 using Avalonia.Interactivity;
@@ -965,5 +967,10 @@ namespace Avalonia.Controls
                 }
             }
         }
+
+        protected override AutomationPeer OnCreateAutomationPeer(IAutomationNodeFactory factory)
+        {
+            return new WindowAutomationPeer(factory, this);
+        }
     }
 }

+ 6 - 26
src/Avalonia.Input/KeyboardDevice.cs

@@ -24,30 +24,7 @@ namespace Avalonia.Input
         // the source of truth about the input focus is in KeyboardDevice
         private readonly TextInputMethodManager _textInputManager = new TextInputMethodManager();
 
-        public IInputElement? FocusedElement
-        {
-            get
-            {
-                return _focusedElement;
-            }
-
-            private set
-            {
-                _focusedElement = value;
-
-                if (_focusedElement != null && _focusedElement.IsAttachedToVisualTree)
-                {
-                    _focusedRoot = _focusedElement.VisualRoot as IInputRoot;
-                }
-                else
-                {
-                    _focusedRoot = null;
-                }
-                
-                RaisePropertyChanged();
-                _textInputManager.SetFocusedElement(value);
-            }
-        }
+        public IInputElement? FocusedElement => _focusedElement;
 
         private void ClearFocusWithinAncestors(IInputElement? element)
         {
@@ -162,8 +139,8 @@ namespace Avalonia.Input
                 }
                 
                 SetIsFocusWithin(FocusedElement, element);
-                
-                FocusedElement = element;
+                _focusedElement = element;
+                _focusedRoot = _focusedElement?.VisualRoot as IInputRoot;
 
                 interactive?.RaiseEvent(new RoutedEventArgs
                 {
@@ -178,6 +155,9 @@ namespace Avalonia.Input
                     NavigationMethod = method,
                     KeyModifiers = keyModifiers,
                 });
+
+                _textInputManager.SetFocusedElement(element);
+                RaisePropertyChanged(nameof(FocusedElement));
             }
         }
 

+ 19 - 0
src/Windows/Avalonia.Win32/Automation/AutomationNode.ExpandCollapse.cs

@@ -0,0 +1,19 @@
+using Avalonia.Automation;
+using Avalonia.Automation.Provider;
+using UIA = Avalonia.Win32.Interop.Automation;
+
+#nullable enable
+
+namespace Avalonia.Win32.Automation
+{
+    internal partial class AutomationNode : UIA.IExpandCollapseProvider
+    {
+        public ExpandCollapseState ExpandCollapseState
+        {
+            get => InvokeSync<IExpandCollapseProvider, ExpandCollapseState>(x => x.ExpandCollapseState);
+        }
+
+        public void Expand() => InvokeSync<IExpandCollapseProvider>(x => x.Expand());
+        public void Collapse() => InvokeSync<IExpandCollapseProvider>(x => x.Collapse());
+    }
+}

+ 19 - 0
src/Windows/Avalonia.Win32/Automation/AutomationNode.RangeValue.cs

@@ -0,0 +1,19 @@
+using Avalonia.Automation.Provider;
+using UIA = Avalonia.Win32.Interop.Automation;
+
+#nullable enable
+
+namespace Avalonia.Win32.Automation
+{
+    internal partial class AutomationNode : UIA.IRangeValueProvider
+    {
+        double UIA.IRangeValueProvider.Value => InvokeSync<IRangeValueProvider, double>(x => x.Value);
+        public bool IsReadOnly => InvokeSync<IRangeValueProvider, bool>(x => x.IsReadOnly);
+        public double Maximum => InvokeSync<IRangeValueProvider, double>(x => x.Maximum);
+        public double Minimum => InvokeSync<IRangeValueProvider, double>(x => x.Minimum);
+        public double LargeChange => 1;
+        public double SmallChange => 1;
+
+        public void SetValue(double value) => InvokeSync<IRangeValueProvider>(x => x.SetValue(value));
+    }
+}

+ 32 - 0
src/Windows/Avalonia.Win32/Automation/AutomationNode.Scroll.cs

@@ -0,0 +1,32 @@
+using Avalonia.Automation.Provider;
+using UIA = Avalonia.Win32.Interop.Automation;
+
+#nullable enable
+
+namespace Avalonia.Win32.Automation
+{
+    internal partial class AutomationNode : UIA.IScrollProvider, UIA.IScrollItemProvider
+    {
+        public bool HorizontallyScrollable => InvokeSync<IScrollProvider, bool>(x => x.HorizontallyScrollable);
+        public double HorizontalScrollPercent => InvokeSync<IScrollProvider, double>(x => x.HorizontalScrollPercent);
+        public double HorizontalViewSize => InvokeSync<IScrollProvider, double>(x => x.HorizontalViewSize);
+        public bool VerticallyScrollable => InvokeSync<IScrollProvider, bool>(x => x.VerticallyScrollable);
+        public double VerticalScrollPercent => InvokeSync<IScrollProvider, double>(x => x.VerticalScrollPercent);
+        public double VerticalViewSize => InvokeSync<IScrollProvider, double>(x => x.VerticalViewSize);
+
+        public void Scroll(ScrollAmount horizontalAmount, ScrollAmount verticalAmount)
+        {
+            InvokeSync<IScrollProvider>(x => x.Scroll(horizontalAmount, verticalAmount));
+        }
+
+        public void SetScrollPercent(double horizontalPercent, double verticalPercent)
+        {
+            InvokeSync<IScrollProvider>(x => x.SetScrollPercent(horizontalPercent, verticalPercent));
+        }
+
+        public void ScrollIntoView()
+        {
+            InvokeSync(() => Peer.BringIntoView());
+        }
+    }
+}

+ 38 - 0
src/Windows/Avalonia.Win32/Automation/AutomationNode.Selection.cs

@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Automation.Peers;
+using Avalonia.Automation.Provider;
+using UIA = Avalonia.Win32.Interop.Automation;
+
+#nullable enable
+
+namespace Avalonia.Win32.Automation
+{
+    internal partial class AutomationNode : UIA.ISelectionProvider, UIA.ISelectionItemProvider
+    {
+        public bool CanSelectMultiple => InvokeSync<ISelectionProvider, bool>(x => x.CanSelectMultiple);
+        public bool IsSelectionRequired => InvokeSync<ISelectionProvider, bool>(x => x.IsSelectionRequired);
+        public bool IsSelected => InvokeSync<ISelectionItemProvider, bool>(x => x.IsSelected);
+        
+        public UIA.IRawElementProviderSimple? SelectionContainer
+        {
+            get
+            {
+                var peer = InvokeSync<ISelectionItemProvider, ISelectionProvider?>(x => x.SelectionContainer);
+                return (peer as AutomationPeer)?.Node as AutomationNode;
+            }
+        }
+
+        public UIA.IRawElementProviderSimple[] GetSelection()
+        {
+            var peers = InvokeSync<ISelectionProvider, IReadOnlyList<AutomationPeer>>(x => x.GetSelection());
+            return peers?.Select(x => (UIA.IRawElementProviderSimple)x.Node).ToArray() ??
+                Array.Empty<UIA.IRawElementProviderSimple>();
+        }
+
+        public void AddToSelection() => InvokeSync<ISelectionItemProvider>(x => x.AddToSelection());
+        public void RemoveFromSelection() => InvokeSync<ISelectionItemProvider>(x => x.RemoveFromSelection());
+        public void Select() => InvokeSync<ISelectionItemProvider>(x => x.Select());
+    }
+}

+ 13 - 0
src/Windows/Avalonia.Win32/Automation/AutomationNode.Toggle.cs

@@ -0,0 +1,13 @@
+using Avalonia.Automation.Provider;
+using UIA = Avalonia.Win32.Interop.Automation;
+
+#nullable enable
+
+namespace Avalonia.Win32.Automation
+{
+    internal partial class AutomationNode : UIA.IToggleProvider
+    {
+        public ToggleState ToggleState => InvokeSync<IToggleProvider, ToggleState>(x => x.ToggleState);
+        public void Toggle() => InvokeSync<IToggleProvider>(x => x.Toggle());
+    }
+}

+ 18 - 0
src/Windows/Avalonia.Win32/Automation/AutomationNode.Value.cs

@@ -0,0 +1,18 @@
+using System.Runtime.InteropServices;
+using Avalonia.Automation.Provider;
+using UIA = Avalonia.Win32.Interop.Automation;
+
+#nullable enable
+
+namespace Avalonia.Win32.Automation
+{
+    internal partial class AutomationNode : UIA.IValueProvider
+    {
+        public string? Value => InvokeSync<IValueProvider, string?>(x => x.Value);
+
+        public void SetValue([MarshalAs(UnmanagedType.LPWStr)] string? value)
+        {
+            InvokeSync<IValueProvider>(x => x.SetValue(value));
+        }
+    }
+}

+ 328 - 0
src/Windows/Avalonia.Win32/Automation/AutomationNode.cs

@@ -0,0 +1,328 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using System.Runtime.InteropServices;
+using Avalonia.Automation;
+using Avalonia.Automation.Peers;
+using Avalonia.Automation.Platform;
+using Avalonia.Threading;
+using Avalonia.Win32.Interop.Automation;
+using AAP = Avalonia.Automation.Provider;
+
+#nullable enable
+
+namespace Avalonia.Win32.Automation
+{
+    [ComVisible(true)]
+    internal partial class AutomationNode : MarshalByRefObject,
+        IAutomationNode,
+        IRawElementProviderSimple,
+        IRawElementProviderSimple2,
+        IRawElementProviderFragment,
+        IRawElementProviderAdviseEvents,
+        IInvokeProvider
+    {
+        private static Dictionary<AutomationProperty, UiaPropertyId> s_propertyMap = new()
+        {
+            { AutomationElementIdentifiers.BoundingRectangleProperty, UiaPropertyId.BoundingRectangle },
+            { AutomationElementIdentifiers.ClassNameProperty, UiaPropertyId.ClassName },
+            { AutomationElementIdentifiers.NameProperty, UiaPropertyId.Name },
+            { ExpandCollapsePatternIdentifiers.ExpandCollapseStateProperty, UiaPropertyId.ExpandCollapseExpandCollapseState },
+            { RangeValuePatternIdentifiers.IsReadOnlyProperty, UiaPropertyId.RangeValueIsReadOnly},
+            { RangeValuePatternIdentifiers.MaximumProperty, UiaPropertyId.RangeValueMaximum },
+            { RangeValuePatternIdentifiers.MinimumProperty, UiaPropertyId.RangeValueMinimum },
+            { RangeValuePatternIdentifiers.ValueProperty, UiaPropertyId.RangeValueValue },
+            { ScrollPatternIdentifiers.HorizontallyScrollableProperty, UiaPropertyId.ScrollHorizontallyScrollable },
+            { ScrollPatternIdentifiers.HorizontalScrollPercentProperty, UiaPropertyId.ScrollHorizontalScrollPercent },
+            { ScrollPatternIdentifiers.HorizontalViewSizeProperty, UiaPropertyId.ScrollHorizontalViewSize },
+            { ScrollPatternIdentifiers.VerticallyScrollableProperty, UiaPropertyId.ScrollVerticallyScrollable },
+            { ScrollPatternIdentifiers.VerticalScrollPercentProperty, UiaPropertyId.ScrollVerticalScrollPercent },
+            { ScrollPatternIdentifiers.VerticalViewSizeProperty, UiaPropertyId.ScrollVerticalViewSize },
+            { SelectionPatternIdentifiers.CanSelectMultipleProperty, UiaPropertyId.SelectionCanSelectMultiple },
+            { SelectionPatternIdentifiers.IsSelectionRequiredProperty, UiaPropertyId.SelectionIsSelectionRequired },
+            { SelectionPatternIdentifiers.SelectionProperty, UiaPropertyId.SelectionSelection },
+        };
+
+        private readonly int[] _runtimeId;
+        private int _raiseFocusChanged;
+        private int _raisePropertyChanged;
+
+        public AutomationNode(AutomationPeer peer)
+        {
+            _runtimeId = new int[] { 3, GetHashCode() };
+            Peer = peer;
+        }
+
+        public AutomationPeer Peer { get; }
+        public IAutomationNodeFactory Factory => AutomationNodeFactory.Instance;
+
+        public Rect BoundingRectangle
+        {
+            get => InvokeSync(() =>
+            {
+                if (GetRoot()?.Node is RootAutomationNode root)
+                    return root.ToScreen(Peer.GetBoundingRectangle());
+                return default;
+            });
+        }
+
+        public virtual IRawElementProviderFragmentRoot? FragmentRoot
+        {
+            get => InvokeSync(() => GetRoot())?.Node as IRawElementProviderFragmentRoot;
+        }
+
+        public virtual IRawElementProviderSimple? HostRawElementProvider => null;
+        public ProviderOptions ProviderOptions => ProviderOptions.ServerSideProvider;
+
+        public void ChildrenChanged()
+        {
+            UiaCoreProviderApi.UiaRaiseStructureChangedEvent(
+                this,
+                StructureChangeType.ChildrenInvalidated,
+                _runtimeId,
+                _runtimeId.Length);
+        }
+
+        public void PropertyChanged(AutomationProperty property, object? oldValue, object? newValue) 
+        {
+            if (_raisePropertyChanged > 0 && s_propertyMap.TryGetValue(property, out var id))
+            {
+                UiaCoreProviderApi.UiaRaiseAutomationPropertyChangedEvent(this, (int)id, oldValue, newValue);
+            }
+        }
+
+        [return: MarshalAs(UnmanagedType.IUnknown)]
+        public virtual object? GetPatternProvider(int patternId)
+        {
+            return (UiaPatternId)patternId switch
+            {
+                UiaPatternId.ExpandCollapse => Peer is IExpandCollapseProvider ? this : null,
+                UiaPatternId.Invoke => Peer is AAP.IInvokeProvider ? this : null,
+                UiaPatternId.RangeValue => Peer is AAP.IRangeValueProvider ? this : null,
+                UiaPatternId.Scroll => Peer is AAP.IScrollProvider ? this : null,
+                UiaPatternId.ScrollItem => this,
+                UiaPatternId.Selection => Peer is AAP.ISelectionProvider ? this : null,
+                UiaPatternId.SelectionItem => Peer is AAP.ISelectionItemProvider ? this : null,
+                UiaPatternId.Toggle => Peer is AAP.IToggleProvider ? this : null,
+                UiaPatternId.Value => Peer is AAP.IValueProvider ? this : null,
+                _ => null,
+            };
+        }
+
+        public virtual object? GetPropertyValue(int propertyId)
+        {
+            return (UiaPropertyId)propertyId switch
+            {
+                UiaPropertyId.AutomationId => InvokeSync(() => Peer.GetAutomationId()),
+                UiaPropertyId.ClassName => InvokeSync(() => Peer.GetClassName()),
+                UiaPropertyId.ClickablePoint => new[] { BoundingRectangle.Center.X, BoundingRectangle.Center.Y },
+                UiaPropertyId.ControlType => InvokeSync(() => ToUiaControlType(Peer.GetAutomationControlType())),
+                UiaPropertyId.Culture => CultureInfo.CurrentCulture.LCID,
+                UiaPropertyId.FrameworkId => "Avalonia",
+                UiaPropertyId.HasKeyboardFocus => InvokeSync(() => Peer.HasKeyboardFocus()),
+                UiaPropertyId.IsContentElement => InvokeSync(() => Peer.IsContentElement()),
+                UiaPropertyId.IsControlElement => InvokeSync(() => Peer.IsControlElement()),
+                UiaPropertyId.IsEnabled => InvokeSync(() => Peer.IsEnabled()),
+                UiaPropertyId.IsKeyboardFocusable => InvokeSync(() => Peer.IsKeyboardFocusable()),
+                UiaPropertyId.LocalizedControlType => InvokeSync(() => Peer.GetLocalizedControlType()),
+                UiaPropertyId.Name => InvokeSync(() => Peer.GetName()),
+                UiaPropertyId.ProcessId => Process.GetCurrentProcess().Id,
+                UiaPropertyId.RuntimeId => _runtimeId,
+                _ => null,
+            };
+        }
+
+        public int[]? GetRuntimeId() => _runtimeId;
+
+        public virtual IRawElementProviderFragment? Navigate(NavigateDirection direction)
+        {
+            IAutomationNode? GetSibling(int direction)
+            {
+                var children = Peer.GetParent()?.GetChildren();
+
+                for (var i = 0; i < (children?.Count ?? 0); ++i)
+                {
+                    if (ReferenceEquals(children![i], Peer))
+                    {
+                        var j = i + direction;
+                        if (j >= 0 && j < children.Count)
+                            return children[j].Node;
+                    }
+                }
+
+                return null;
+            }
+
+            return InvokeSync(() =>
+            {
+                return direction switch
+                {
+                    NavigateDirection.Parent => Peer.GetParent()?.Node,
+                    NavigateDirection.NextSibling => GetSibling(1),
+                    NavigateDirection.PreviousSibling => GetSibling(-1),
+                    NavigateDirection.FirstChild => Peer.GetChildren().FirstOrDefault()?.Node,
+                    NavigateDirection.LastChild => Peer.GetChildren().LastOrDefault()?.Node,
+                    _ => null,
+                };
+            }) as IRawElementProviderFragment;
+        }
+
+        public void SetFocus() => InvokeSync(() => Peer.SetFocus());
+
+        IRawElementProviderSimple[]? IRawElementProviderFragment.GetEmbeddedFragmentRoots() => null;
+        void IRawElementProviderSimple2.ShowContextMenu() => InvokeSync(() => Peer.ShowContextMenu());
+        void IInvokeProvider.Invoke() => InvokeSync((AAP.IInvokeProvider x) => x.Invoke());
+
+        void IRawElementProviderAdviseEvents.AdviseEventAdded(int eventId, int[] properties)
+        {
+            switch ((UiaEventId)eventId)
+            {
+                case UiaEventId.AutomationPropertyChanged:
+                    ++_raisePropertyChanged;
+                    break;
+                case UiaEventId.AutomationFocusChanged:
+                    ++_raiseFocusChanged;
+                    break;
+            }
+        }
+
+        void IRawElementProviderAdviseEvents.AdviseEventRemoved(int eventId, int[] properties)
+        {
+            switch ((UiaEventId)eventId)
+            {
+                case UiaEventId.AutomationPropertyChanged:
+                    --_raisePropertyChanged;
+                    break;
+                case UiaEventId.AutomationFocusChanged:
+                    --_raiseFocusChanged;
+                    break;
+            }
+        }
+
+        protected void InvokeSync(Action action)
+        {
+            if (Dispatcher.UIThread.CheckAccess())
+                action();
+            else
+                Dispatcher.UIThread.InvokeAsync(action).Wait();
+        }
+
+        [return: MaybeNull]
+        protected T InvokeSync<T>(Func<T> func)
+        {
+            if (Dispatcher.UIThread.CheckAccess())
+                return func();
+            else
+                return Dispatcher.UIThread.InvokeAsync(func).Result;
+        }
+
+        protected void InvokeSync<TInterface>(Action<TInterface> action)
+        {
+            if (Peer is TInterface i)
+            {
+                try
+                {
+                    InvokeSync(() => action(i));
+                }
+                catch (AggregateException e) when (e.InnerException is ElementNotEnabledException)
+                {
+                    throw new COMException(e.Message, UiaCoreProviderApi.UIA_E_ELEMENTNOTENABLED);
+                }
+            }
+        }
+
+        [return: MaybeNull]
+        protected TResult InvokeSync<TInterface, TResult>(Func<TInterface, TResult> func)
+        {
+            if (Peer is TInterface i)
+            {
+                try
+                {
+                    return InvokeSync(() => func(i));
+                }
+                catch (AggregateException e) when (e.InnerException is ElementNotEnabledException)
+                {
+                    throw new COMException(e.Message, UiaCoreProviderApi.UIA_E_ELEMENTNOTENABLED);
+                }
+            }
+
+            return default;
+        }
+
+        protected void RaiseFocusChanged(AutomationNode? focused)
+        {
+            if (_raiseFocusChanged > 0)
+            {
+                UiaCoreProviderApi.UiaRaiseAutomationEvent(
+                    focused,
+                    (int)UiaEventId.AutomationFocusChanged);
+            }
+        }
+
+        private AutomationPeer GetRoot()
+        {
+            Dispatcher.UIThread.VerifyAccess();
+
+            var peer = Peer;
+            var parent = peer.GetParent();
+
+            while (parent is object)
+            {
+                peer = parent;
+                parent = peer.GetParent();
+            }
+
+            return peer;
+        }
+
+        private static UiaControlTypeId ToUiaControlType(AutomationControlType role)
+        {
+            return role switch
+            {
+                AutomationControlType.Button => UiaControlTypeId.Button,
+                AutomationControlType.Calendar => UiaControlTypeId.Calendar,
+                AutomationControlType.CheckBox => UiaControlTypeId.CheckBox,
+                AutomationControlType.ComboBox => UiaControlTypeId.ComboBox,
+                AutomationControlType.Edit => UiaControlTypeId.Edit,
+                AutomationControlType.Hyperlink => UiaControlTypeId.Hyperlink,
+                AutomationControlType.Image => UiaControlTypeId.Image,
+                AutomationControlType.ListItem => UiaControlTypeId.ListItem,
+                AutomationControlType.List => UiaControlTypeId.List,
+                AutomationControlType.Menu => UiaControlTypeId.Menu,
+                AutomationControlType.MenuBar => UiaControlTypeId.MenuBar,
+                AutomationControlType.MenuItem => UiaControlTypeId.MenuItem,
+                AutomationControlType.ProgressBar => UiaControlTypeId.ProgressBar,
+                AutomationControlType.RadioButton => UiaControlTypeId.RadioButton,
+                AutomationControlType.ScrollBar => UiaControlTypeId.ScrollBar,
+                AutomationControlType.Slider => UiaControlTypeId.Slider,
+                AutomationControlType.Spinner => UiaControlTypeId.Spinner,
+                AutomationControlType.StatusBar => UiaControlTypeId.StatusBar,
+                AutomationControlType.Tab => UiaControlTypeId.Tab,
+                AutomationControlType.TabItem => UiaControlTypeId.TabItem,
+                AutomationControlType.Text => UiaControlTypeId.Text,
+                AutomationControlType.ToolBar => UiaControlTypeId.ToolBar,
+                AutomationControlType.ToolTip => UiaControlTypeId.ToolTip,
+                AutomationControlType.Tree => UiaControlTypeId.Tree,
+                AutomationControlType.TreeItem => UiaControlTypeId.TreeItem,
+                AutomationControlType.Custom => UiaControlTypeId.Custom,
+                AutomationControlType.Group => UiaControlTypeId.Group,
+                AutomationControlType.Thumb => UiaControlTypeId.Thumb,
+                AutomationControlType.DataGrid => UiaControlTypeId.DataGrid,
+                AutomationControlType.DataItem => UiaControlTypeId.DataItem,
+                AutomationControlType.Document => UiaControlTypeId.Document,
+                AutomationControlType.SplitButton => UiaControlTypeId.SplitButton,
+                AutomationControlType.Window => UiaControlTypeId.Window,
+                AutomationControlType.Pane => UiaControlTypeId.Pane,
+                AutomationControlType.Header => UiaControlTypeId.Header,
+                AutomationControlType.HeaderItem => UiaControlTypeId.HeaderItem,
+                AutomationControlType.Table => UiaControlTypeId.Table,
+                AutomationControlType.TitleBar => UiaControlTypeId.TitleBar,
+                AutomationControlType.Separator => UiaControlTypeId.Separator,
+                _ => UiaControlTypeId.Custom,
+            };
+        }
+    }
+}

+ 20 - 0
src/Windows/Avalonia.Win32/Automation/AutomationNodeFactory.cs

@@ -0,0 +1,20 @@
+using Avalonia.Automation.Peers;
+using Avalonia.Automation.Platform;
+using Avalonia.Automation.Provider;
+using Avalonia.Threading;
+
+#nullable enable
+
+namespace Avalonia.Win32.Automation
+{
+    internal class AutomationNodeFactory : IAutomationNodeFactory
+    {
+        public static readonly AutomationNodeFactory Instance = new AutomationNodeFactory();
+
+        public IAutomationNode CreateNode(AutomationPeer peer)
+        {
+            Dispatcher.UIThread.VerifyAccess();
+            return peer is IRootProvider ? new RootAutomationNode(peer) : new AutomationNode(peer);
+        }
+    }
+}

+ 72 - 0
src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs

@@ -0,0 +1,72 @@
+using System;
+using System.Runtime.InteropServices;
+using Avalonia.Automation.Peers;
+using Avalonia.Automation.Platform;
+using Avalonia.Automation.Provider;
+using Avalonia.Win32.Interop.Automation;
+
+#nullable enable
+
+namespace Avalonia.Win32.Automation
+{
+    internal class RootAutomationNode : AutomationNode,
+        IRawElementProviderFragmentRoot,
+        IRootAutomationNode
+    {
+        public RootAutomationNode(AutomationPeer peer)
+            : base(peer)
+        {
+        }
+
+        public override IRawElementProviderFragmentRoot? FragmentRoot => this;
+        public new IRootProvider Peer => (IRootProvider)base.Peer;
+        public WindowImpl? WindowImpl => Peer.PlatformImpl as WindowImpl;
+        
+        public IRawElementProviderFragment? ElementProviderFromPoint(double x, double y)
+        {
+            if (WindowImpl is null)
+                return null;
+
+            var p = WindowImpl.PointToClient(new PixelPoint((int)x, (int)y));
+            var peer = (WindowBaseAutomationPeer)Peer;
+            var found = InvokeSync(() => peer.GetPeerFromPoint(p));
+            var result = found?.Node as IRawElementProviderFragment;
+            return result;
+        }
+
+        public IRawElementProviderFragment? GetFocus()
+        {
+            var focus = InvokeSync(() => Peer.GetFocus());
+            return (AutomationNode?)focus?.Node;
+        }
+
+        public void FocusChanged(AutomationPeer? focus)
+        {
+            var node = focus?.Node as AutomationNode;
+            RaiseFocusChanged(node);
+        }
+
+        public Rect ToScreen(Rect rect)
+        {
+            if (WindowImpl is null)
+                return default;
+            return new PixelRect(
+                WindowImpl.PointToScreen(rect.TopLeft),
+                WindowImpl.PointToScreen(rect.BottomRight))
+                    .ToRect(1);
+        }
+
+        public override IRawElementProviderSimple? HostRawElementProvider
+        {
+            get
+            {
+                var handle = WindowImpl?.Handle.Handle ?? IntPtr.Zero;
+                if (handle == IntPtr.Zero)
+                    return null;
+                var hr = UiaCoreProviderApi.UiaHostProviderFromHwnd(handle, out var result);
+                Marshal.ThrowExceptionForHR(hr);
+                return result;
+            }
+        }
+    }
+}

+ 3 - 0
src/Windows/Avalonia.Win32/Avalonia.Win32.csproj

@@ -6,6 +6,9 @@
   </PropertyGroup>
   <ItemGroup>
     <PackageReference Include="System.Numerics.Vectors" Version="4.5.0" />
+    <Compile Include="..\..\Avalonia.Base\Metadata\NullableAttributes.cs" Link="NullableAttributes.cs" />
+  </ItemGroup>
+  <ItemGroup>
     <ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />
     <PackageReference Include="Avalonia.Angle.Windows.Natives" Version="2.1.0.2020091801" />
     <AvnComIdl Include="WinRT\winrt.idl" OutputFile="WinRT\WinRT.Generated.cs" />

+ 26 - 0
src/Windows/Avalonia.Win32/Interop/Automation/IDockProvider.cs

@@ -0,0 +1,26 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Avalonia.Win32.Interop.Automation
+{
+    [ComVisible(true)]
+    [Guid("70d46e77-e3a8-449d-913c-e30eb2afecdb")]
+    public enum DockPosition
+    {
+        Top,
+        Left,
+        Bottom,
+        Right,
+        Fill,
+        None
+    }
+
+    [ComVisible(true)]
+    [Guid("159bc72c-4ad3-485e-9637-d7052edf0146")]
+    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+    public interface IDockProvider
+    {
+        void SetDockPosition(DockPosition dockPosition);
+        DockPosition DockPosition { get; }
+    }
+}

+ 16 - 0
src/Windows/Avalonia.Win32/Interop/Automation/IExpandCollapseProvider.cs

@@ -0,0 +1,16 @@
+using System;
+using System.Runtime.InteropServices;
+using Avalonia.Automation;
+
+namespace Avalonia.Win32.Interop.Automation
+{
+    [ComVisible(true)]
+    [Guid("d847d3a5-cab0-4a98-8c32-ecb45c59ad24")]
+    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+    public interface IExpandCollapseProvider
+    {
+        void Expand();
+        void Collapse();
+        ExpandCollapseState ExpandCollapseState { get; }
+    }
+}

+ 17 - 0
src/Windows/Avalonia.Win32/Interop/Automation/IGridItemProvider.cs

@@ -0,0 +1,17 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Avalonia.Win32.Interop.Automation
+{
+    [ComVisible(true)]
+    [Guid("d02541f1-fb81-4d64-ae32-f520f8a6dbd1")]
+    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+    public interface IGridItemProvider
+    {
+        int Row { get; }
+        int Column { get; }
+        int RowSpan { get; }
+        int ColumnSpan { get; }
+        IRawElementProviderSimple ContainingGrid { get; }
+    }
+}

+ 15 - 0
src/Windows/Avalonia.Win32/Interop/Automation/IGridProvider.cs

@@ -0,0 +1,15 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Avalonia.Win32.Interop.Automation
+{
+    [ComVisible(true)]
+    [Guid("b17d6187-0907-464b-a168-0ef17a1572b1")]
+    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+    public interface IGridProvider
+    {
+        IRawElementProviderSimple GetItem(int row, int column); 
+        int RowCount { get; }
+        int ColumnCount { get; }
+    }
+}

+ 19 - 0
src/Windows/Avalonia.Win32/Interop/Automation/IInvokeProvider.cs

@@ -0,0 +1,19 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+// Description: Invoke pattern provider interface
+
+using System;
+using System.Runtime.InteropServices;
+
+namespace Avalonia.Win32.Interop.Automation
+{
+    [ComVisible(true)]
+    [Guid("54fcb24b-e18e-47a2-b4d3-eccbe77599a2")]
+    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+    public interface IInvokeProvider
+    {
+        void Invoke();
+    }
+}

+ 16 - 0
src/Windows/Avalonia.Win32/Interop/Automation/IMultipleViewProvider.cs

@@ -0,0 +1,16 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Avalonia.Win32.Interop.Automation
+{
+    [ComVisible(true)]
+    [Guid("6278cab1-b556-4a1a-b4e0-418acc523201")]
+    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+    public interface IMultipleViewProvider
+    {
+        string GetViewName(int viewId);
+        void SetCurrentView(int viewId);
+        int CurrentView { get; }
+        int[] GetSupportedViews();
+    }
+}

+ 19 - 0
src/Windows/Avalonia.Win32/Interop/Automation/IRangeValueProvider.cs

@@ -0,0 +1,19 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Avalonia.Win32.Interop.Automation
+{
+    [ComVisible(true)]
+    [Guid("36dc7aef-33e6-4691-afe1-2be7274b3d33")]
+    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+    public interface IRangeValueProvider
+    {
+        void SetValue(double value);
+        double Value { get; }
+        bool IsReadOnly { [return: MarshalAs(UnmanagedType.Bool)] get; }
+        double Maximum { get; }
+        double Minimum { get; }
+        double LargeChange { get; }
+        double SmallChange { get; }
+    }
+}

+ 15 - 0
src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderAdviseEvents.cs

@@ -0,0 +1,15 @@
+using System;
+using System.Runtime.InteropServices;
+
+
+namespace Avalonia.Win32.Interop.Automation
+{
+    [ComVisible(true)]
+    [Guid("a407b27b-0f6d-4427-9292-473c7bf93258")]
+    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+    public interface IRawElementProviderAdviseEvents : IRawElementProviderSimple
+    {
+        void AdviseEventAdded(int eventId, int [] properties);
+        void AdviseEventRemoved(int eventId, int [] properties);
+    }
+}

+ 34 - 0
src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderFragment.cs

@@ -0,0 +1,34 @@
+using System;
+using System.Runtime.InteropServices;
+
+#nullable enable
+
+namespace Avalonia.Win32.Interop.Automation
+{
+    [ComVisible(true)]
+    [Guid("670c3006-bf4c-428b-8534-e1848f645122")]
+    public enum NavigateDirection
+    {
+        Parent,
+        NextSibling,
+        PreviousSibling,
+        FirstChild,
+        LastChild,
+    }
+
+    // NOTE: This interface needs to be public otherwise Navigate is never called. I have no idea
+    // why given that IRawElementProviderSimple and IRawElementProviderFragmentRoot seem to get
+    // called fine when they're internal, but I lost a couple of days to this.
+    [ComVisible(true)]
+    [Guid("f7063da8-8359-439c-9297-bbc5299a7d87")]
+    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+    public interface IRawElementProviderFragment : IRawElementProviderSimple
+    {
+        IRawElementProviderFragment? Navigate(NavigateDirection direction);
+        int[]? GetRuntimeId();
+        Rect BoundingRectangle { get; }
+        IRawElementProviderSimple[]? GetEmbeddedFragmentRoots();
+        void SetFocus();
+        IRawElementProviderFragmentRoot? FragmentRoot { get; }
+    }
+}

+ 14 - 0
src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderFragmentRoot.cs

@@ -0,0 +1,14 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Avalonia.Win32.Interop.Automation
+{
+    [ComVisible(true)]
+    [Guid("620ce2a5-ab8f-40a9-86cb-de3c75599b58")]
+    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+    public interface IRawElementProviderFragmentRoot : IRawElementProviderFragment
+    {
+        IRawElementProviderFragment ElementProviderFromPoint(double x, double y);
+        IRawElementProviderFragment GetFocus();
+    }
+}

+ 285 - 0
src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderSimple.cs

@@ -0,0 +1,285 @@
+using System;
+using System.Runtime.InteropServices;
+
+#nullable enable
+
+namespace Avalonia.Win32.Interop.Automation
+{
+    [Flags]
+    public enum ProviderOptions
+    {
+        ClientSideProvider = 0x0001,
+        ServerSideProvider = 0x0002,
+        NonClientAreaProvider = 0x0004,
+        OverrideProvider = 0x0008,
+        ProviderOwnsSetFocus = 0x0010,
+        UseComThreading = 0x0020
+    }
+
+    internal enum UiaPropertyId
+    {
+        RuntimeId = 30000,
+        BoundingRectangle,
+        ProcessId,
+        ControlType,
+        LocalizedControlType,
+        Name,
+        AcceleratorKey,
+        AccessKey,
+        HasKeyboardFocus,
+        IsKeyboardFocusable,
+        IsEnabled,
+        AutomationId,
+        ClassName,
+        HelpText,
+        ClickablePoint,
+        Culture,
+        IsControlElement,
+        IsContentElement,
+        LabeledBy,
+        IsPassword,
+        NativeWindowHandle,
+        ItemType,
+        IsOffscreen,
+        Orientation,
+        FrameworkId,
+        IsRequiredForForm,
+        ItemStatus,
+        IsDockPatternAvailable,
+        IsExpandCollapsePatternAvailable,
+        IsGridItemPatternAvailable,
+        IsGridPatternAvailable,
+        IsInvokePatternAvailable,
+        IsMultipleViewPatternAvailable,
+        IsRangeValuePatternAvailable,
+        IsScrollPatternAvailable,
+        IsScrollItemPatternAvailable,
+        IsSelectionItemPatternAvailable,
+        IsSelectionPatternAvailable,
+        IsTablePatternAvailable,
+        IsTableItemPatternAvailable,
+        IsTextPatternAvailable,
+        IsTogglePatternAvailable,
+        IsTransformPatternAvailable,
+        IsValuePatternAvailable,
+        IsWindowPatternAvailable,
+        ValueValue,
+        ValueIsReadOnly,
+        RangeValueValue,
+        RangeValueIsReadOnly,
+        RangeValueMinimum,
+        RangeValueMaximum,
+        RangeValueLargeChange,
+        RangeValueSmallChange,
+        ScrollHorizontalScrollPercent,
+        ScrollHorizontalViewSize,
+        ScrollVerticalScrollPercent,
+        ScrollVerticalViewSize,
+        ScrollHorizontallyScrollable,
+        ScrollVerticallyScrollable,
+        SelectionSelection,
+        SelectionCanSelectMultiple,
+        SelectionIsSelectionRequired,
+        GridRowCount,
+        GridColumnCount,
+        GridItemRow,
+        GridItemColumn,
+        GridItemRowSpan,
+        GridItemColumnSpan,
+        GridItemContainingGrid,
+        DockDockPosition,
+        ExpandCollapseExpandCollapseState,
+        MultipleViewCurrentView,
+        MultipleViewSupportedViews,
+        WindowCanMaximize,
+        WindowCanMinimize,
+        WindowWindowVisualState,
+        WindowWindowInteractionState,
+        WindowIsModal,
+        WindowIsTopmost,
+        SelectionItemIsSelected,
+        SelectionItemSelectionContainer,
+        TableRowHeaders,
+        TableColumnHeaders,
+        TableRowOrColumnMajor,
+        TableItemRowHeaderItems,
+        TableItemColumnHeaderItems,
+        ToggleToggleState,
+        TransformCanMove,
+        TransformCanResize,
+        TransformCanRotate,
+        IsLegacyIAccessiblePatternAvailable,
+        LegacyIAccessibleChildId,
+        LegacyIAccessibleName,
+        LegacyIAccessibleValue,
+        LegacyIAccessibleDescription,
+        LegacyIAccessibleRole,
+        LegacyIAccessibleState,
+        LegacyIAccessibleHelp,
+        LegacyIAccessibleKeyboardShortcut,
+        LegacyIAccessibleSelection,
+        LegacyIAccessibleDefaultAction,
+        AriaRole,
+        AriaProperties,
+        IsDataValidForForm,
+        ControllerFor,
+        DescribedBy,
+        FlowsTo,
+        ProviderDescription,
+        IsItemContainerPatternAvailable,
+        IsVirtualizedItemPatternAvailable,
+        IsSynchronizedInputPatternAvailable,
+        OptimizeForVisualContent,
+        IsObjectModelPatternAvailable,
+        AnnotationAnnotationTypeId,
+        AnnotationAnnotationTypeName,
+        AnnotationAuthor,
+        AnnotationDateTime,
+        AnnotationTarget,
+        IsAnnotationPatternAvailable,
+        IsTextPattern2Available,
+        StylesStyleId,
+        StylesStyleName,
+        StylesFillColor,
+        StylesFillPatternStyle,
+        StylesShape,
+        StylesFillPatternColor,
+        StylesExtendedProperties,
+        IsStylesPatternAvailable,
+        IsSpreadsheetPatternAvailable,
+        SpreadsheetItemFormula,
+        SpreadsheetItemAnnotationObjects,
+        SpreadsheetItemAnnotationTypes,
+        IsSpreadsheetItemPatternAvailable,
+        Transform2CanZoom,
+        IsTransformPattern2Available,
+        LiveSetting,
+        IsTextChildPatternAvailable,
+        IsDragPatternAvailable,
+        DragIsGrabbed,
+        DragDropEffect,
+        DragDropEffects,
+        IsDropTargetPatternAvailable,
+        DropTargetDropTargetEffect,
+        DropTargetDropTargetEffects,
+        DragGrabbedItems,
+        Transform2ZoomLevel,
+        Transform2ZoomMinimum,
+        Transform2ZoomMaximum,
+        FlowsFrom,
+        IsTextEditPatternAvailable,
+        IsPeripheral,
+        IsCustomNavigationPatternAvailable,
+        PositionInSet,
+        SizeOfSet,
+        Level,
+        AnnotationTypes,
+        AnnotationObjects,
+        LandmarkType,
+        LocalizedLandmarkType,
+        FullDescription,
+        FillColor,
+        OutlineColor,
+        FillType,
+        VisualEffects,
+        OutlineThickness,
+        CenterPoint,
+        Rotatation,
+        Size
+    }
+
+    internal enum UiaPatternId
+    {
+        Invoke = 10000,
+        Selection,
+        Value,
+        RangeValue,
+        Scroll,
+        ExpandCollapse,
+        Grid,
+        GridItem,
+        MultipleView,
+        Window,
+        SelectionItem,
+        Dock,
+        Table,
+        TableItem,
+        Text,
+        Toggle,
+        Transform,
+        ScrollItem,
+        LegacyIAccessible,
+        ItemContainer,
+        VirtualizedItem,
+        SynchronizedInput,
+        ObjectModel,
+        Annotation,
+        Text2,
+        Styles,
+        Spreadsheet,
+        SpreadsheetItem,
+        Transform2,
+        TextChild,
+        Drag,
+        DropTarget,
+        TextEdit,
+        CustomNavigation
+    };
+
+    internal enum UiaControlTypeId
+    {
+        Button = 50000,
+        Calendar,
+        CheckBox,
+        ComboBox,
+        Edit,
+        Hyperlink,
+        Image,
+        ListItem,
+        List,
+        Menu,
+        MenuBar,
+        MenuItem,
+        ProgressBar,
+        RadioButton,
+        ScrollBar,
+        Slider,
+        Spinner,
+        StatusBar,
+        Tab,
+        TabItem,
+        Text,
+        ToolBar,
+        ToolTip,
+        Tree,
+        TreeItem,
+        Custom,
+        Group,
+        Thumb,
+        DataGrid,
+        DataItem,
+        Document,
+        SplitButton,
+        Window,
+        Pane,
+        Header,
+        HeaderItem,
+        Table,
+        TitleBar,
+        Separator,
+        SemanticZoom,
+        AppBar
+    };
+
+    [ComVisible(true)]
+    [Guid("d6dd68d1-86fd-4332-8666-9abedea2d24c")]
+    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+    public interface IRawElementProviderSimple
+    {
+        ProviderOptions ProviderOptions { get; }
+        [return: MarshalAs(UnmanagedType.IUnknown)]
+        object? GetPatternProvider(int patternId);
+        object? GetPropertyValue(int propertyId);
+        IRawElementProviderSimple? HostRawElementProvider { get; }
+    }
+}

+ 14 - 0
src/Windows/Avalonia.Win32/Interop/Automation/IRawElementProviderSimple2.cs

@@ -0,0 +1,14 @@
+using System;
+using System.Runtime.InteropServices;
+
+#nullable enable
+
+namespace Avalonia.Win32.Interop.Automation
+{
+    [ComVisible(true)]
+    [Guid("a0a839a9-8da1-4a82-806a-8e0d44e79f56")]
+    public interface IRawElementProviderSimple2
+    {
+        void ShowContextMenu();
+    }
+}

+ 13 - 0
src/Windows/Avalonia.Win32/Interop/Automation/IScrollItemProvider.cs

@@ -0,0 +1,13 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Avalonia.Win32.Interop.Automation
+{
+    [ComVisible(true)]
+    [Guid("2360c714-4bf1-4b26-ba65-9b21316127eb")]
+    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+    public interface IScrollItemProvider
+    {
+        void ScrollIntoView();
+    }
+}

+ 21 - 0
src/Windows/Avalonia.Win32/Interop/Automation/IScrollProvider.cs

@@ -0,0 +1,21 @@
+using System;
+using System.Runtime.InteropServices;
+using Avalonia.Automation.Provider;
+
+namespace Avalonia.Win32.Interop.Automation
+{
+    [ComVisible(true)]
+    [Guid("b38b8077-1fc3-42a5-8cae-d40c2215055a")]
+    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+    public interface IScrollProvider
+    {
+        void Scroll(ScrollAmount horizontalAmount, ScrollAmount verticalAmount);
+        void SetScrollPercent(double horizontalPercent, double verticalPercent);
+        double HorizontalScrollPercent { get; }
+        double VerticalScrollPercent { get; }
+        double HorizontalViewSize { get; }
+        double VerticalViewSize { get; }
+        bool HorizontallyScrollable { [return: MarshalAs(UnmanagedType.Bool)] get; }
+        bool VerticallyScrollable { [return: MarshalAs(UnmanagedType.Bool)] get; }
+    }
+}

+ 17 - 0
src/Windows/Avalonia.Win32/Interop/Automation/ISelectionItemProvider.cs

@@ -0,0 +1,17 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Avalonia.Win32.Interop.Automation
+{
+    [ComVisible(true)]
+    [Guid("2acad808-b2d4-452d-a407-91ff1ad167b2")]
+    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+    public interface ISelectionItemProvider
+    {
+        void Select();
+        void AddToSelection();
+        void RemoveFromSelection();
+        bool IsSelected { [return: MarshalAs(UnmanagedType.Bool)] get; }
+        IRawElementProviderSimple? SelectionContainer { get; }
+    }
+}

+ 15 - 0
src/Windows/Avalonia.Win32/Interop/Automation/ISelectionProvider.cs

@@ -0,0 +1,15 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Avalonia.Win32.Interop.Automation
+{
+    [ComVisible(true)]
+    [Guid("fb8b03af-3bdf-48d4-bd36-1a65793be168")]
+    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+    public interface ISelectionProvider
+    {
+        IRawElementProviderSimple [] GetSelection();
+        bool CanSelectMultiple { [return: MarshalAs(UnmanagedType.Bool)] get; }
+        bool IsSelectionRequired { [return: MarshalAs(UnmanagedType.Bool)] get; }
+    }
+}

+ 26 - 0
src/Windows/Avalonia.Win32/Interop/Automation/ISynchronizedInputProvider.cs

@@ -0,0 +1,26 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Avalonia.Win32.Interop.Automation
+{
+    [ComVisible(true)]
+    [Guid("fdc8f176-aed2-477a-8c89-5604c66f278d")]
+    public enum SynchronizedInputType
+    {
+        KeyUp = 0x01,
+        KeyDown = 0x02,
+        MouseLeftButtonUp = 0x04,
+        MouseLeftButtonDown = 0x08,
+        MouseRightButtonUp = 0x10,
+        MouseRightButtonDown = 0x20
+    }
+
+    [ComVisible(true)]
+    [Guid("29db1a06-02ce-4cf7-9b42-565d4fab20ee")]
+    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+    public interface ISynchronizedInputProvider
+    {
+        void  StartListening(SynchronizedInputType inputType);
+        void Cancel();
+    }
+}

+ 14 - 0
src/Windows/Avalonia.Win32/Interop/Automation/ITableItemProvider.cs

@@ -0,0 +1,14 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Avalonia.Win32.Interop.Automation
+{
+    [ComVisible(true)]
+    [Guid("b9734fa6-771f-4d78-9c90-2517999349cd")]
+    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+    public interface ITableItemProvider : IGridItemProvider
+    {
+        IRawElementProviderSimple [] GetRowHeaderItems();
+        IRawElementProviderSimple [] GetColumnHeaderItems();
+    }
+}

+ 24 - 0
src/Windows/Avalonia.Win32/Interop/Automation/ITableProvider.cs

@@ -0,0 +1,24 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Avalonia.Win32.Interop.Automation
+{
+    [ComVisible(true)]
+    [Guid("15fdf2e2-9847-41cd-95dd-510612a025ea")]
+    public enum RowOrColumnMajor
+    {
+        RowMajor,
+        ColumnMajor,
+        Indeterminate,
+    }
+
+    [ComVisible(true)]
+    [Guid("9c860395-97b3-490a-b52a-858cc22af166")]
+    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+    public interface ITableProvider : IGridProvider
+    {
+        IRawElementProviderSimple [] GetRowHeaders();
+        IRawElementProviderSimple [] GetColumnHeaders();
+        RowOrColumnMajor RowOrColumnMajor { get; }
+    }
+}

+ 30 - 0
src/Windows/Avalonia.Win32/Interop/Automation/ITextProvider.cs

@@ -0,0 +1,30 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Avalonia.Win32.Interop.Automation
+{
+    [Flags]
+    [ComVisible(true)]
+    [Guid("3d9e3d8f-bfb0-484f-84ab-93ff4280cbc4")]
+    public enum SupportedTextSelection
+    {
+        None,
+        Single,
+        Multiple,
+    }
+
+    [ComVisible(true)]
+    [Guid("3589c92c-63f3-4367-99bb-ada653b77cf2")]
+    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+    public interface ITextProvider
+    {
+        ITextRangeProvider [] GetSelection();
+        ITextRangeProvider [] GetVisibleRanges();
+        ITextRangeProvider RangeFromChild(IRawElementProviderSimple childElement);
+        ITextRangeProvider RangeFromPoint(Point screenLocation);
+        ITextRangeProvider DocumentRange { get; }
+        SupportedTextSelection SupportedTextSelection { get; }
+    }
+}
+
+

+ 48 - 0
src/Windows/Avalonia.Win32/Interop/Automation/ITextRangeProvider.cs

@@ -0,0 +1,48 @@
+using System.Runtime.InteropServices;
+
+namespace Avalonia.Win32.Interop.Automation
+{
+    public enum TextPatternRangeEndpoint
+    {
+        Start = 0,
+        End = 1,
+    }
+
+    public enum TextUnit
+    {
+        Character = 0,
+        Format = 1,
+        Word = 2,
+        Line = 3,
+        Paragraph = 4,
+        Page = 5,
+        Document = 6,
+    }
+
+    [ComVisible(true)]
+    [Guid("5347ad7b-c355-46f8-aff5-909033582f63")]
+    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+    public interface ITextRangeProvider
+
+    {
+        ITextRangeProvider Clone();
+        [return: MarshalAs(UnmanagedType.Bool)]
+        bool Compare(ITextRangeProvider range);
+        int CompareEndpoints(TextPatternRangeEndpoint endpoint, ITextRangeProvider targetRange, TextPatternRangeEndpoint targetEndpoint);
+        void ExpandToEnclosingUnit(TextUnit unit);
+        ITextRangeProvider FindAttribute(int attribute, object value, [MarshalAs(UnmanagedType.Bool)] bool backward);
+        ITextRangeProvider FindText(string text, [MarshalAs(UnmanagedType.Bool)] bool backward, [MarshalAs(UnmanagedType.Bool)] bool ignoreCase);
+        object GetAttributeValue(int attribute);
+        double [] GetBoundingRectangles();
+        IRawElementProviderSimple GetEnclosingElement();
+        string GetText(int maxLength);
+        int Move(TextUnit unit, int count);
+        int MoveEndpointByUnit(TextPatternRangeEndpoint endpoint, TextUnit unit, int count);
+        void MoveEndpointByRange(TextPatternRangeEndpoint endpoint, ITextRangeProvider targetRange, TextPatternRangeEndpoint targetEndpoint);
+        void Select();
+        void AddToSelection();
+        void RemoveFromSelection();
+        void ScrollIntoView([MarshalAs(UnmanagedType.Bool)] bool alignToTop);
+        IRawElementProviderSimple[] GetChildren();
+    }
+}

+ 15 - 0
src/Windows/Avalonia.Win32/Interop/Automation/IToggleProvider.cs

@@ -0,0 +1,15 @@
+using System;
+using System.Runtime.InteropServices;
+using Avalonia.Automation.Provider;
+
+namespace Avalonia.Win32.Interop.Automation
+{
+    [ComVisible(true)]
+    [Guid("56d00bd0-c4f4-433c-a836-1a52a57e0892")]
+    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+    public interface IToggleProvider
+    {
+        void Toggle( );
+        ToggleState ToggleState { get; }
+    }
+}

Some files were not shown because too many files changed in this diff