Browse Source

Merge branch 'master' into xaml_composition_animations

pull/19747/head
Max Katz 1 week ago
committed by GitHub
parent
commit
9e031ac6f1
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 57
      .editorconfig
  2. 1
      Avalonia.sln
  3. 23
      Directory.Build.props
  4. 76
      api/Avalonia.Headless.XUnit.nupkg.xml
  5. 4
      api/Avalonia.Win32.Interoperability.nupkg.xml
  6. 956
      api/Avalonia.nupkg.xml
  7. 1
      build/AnalyzerProject.targets
  8. 8
      build/NullableEnable.props
  9. 18
      build/TrimmingEnable.props
  10. 23
      build/WarnAsErrors.props
  11. 9
      build/XUnit.props
  12. 2
      docs/build.md
  13. 2
      external/Numerge
  14. 1
      native/Avalonia.Native/src/OSX/AvnAccessibility.h
  15. 4
      native/Avalonia.Native/src/OSX/AvnWindow.mm
  16. 37
      native/Avalonia.Native/src/OSX/automation.mm
  17. 9
      nukebuild/.editorconfig
  18. 4
      nukebuild/_build.csproj
  19. 2
      packages/Avalonia/Avalonia.props
  20. 3
      samples/IntegrationTestApp/Pages/AutomationPage.axaml
  21. 6
      samples/IntegrationTestApp/Pages/AutomationPage.axaml.cs
  22. 2
      samples/TextTestApp/InteractiveLineControl.cs
  23. 10
      src/Android/Avalonia.Android/AvaloniaView.cs
  24. 3
      src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs
  25. 11
      src/Avalonia.Base/Controls/IInternalScroller.cs
  26. 10
      src/Avalonia.Base/Controls/Primitives/IScrollable.cs
  27. 164
      src/Avalonia.Base/Data/CompiledBinding.cs
  28. 29
      src/Avalonia.Base/Data/CompiledBindingPath.cs
  29. 6
      src/Avalonia.Base/Input/IKeyboardNavigationHandler.cs
  30. 22
      src/Avalonia.Base/Input/KeyboardNavigationHandler.cs
  31. 4
      src/Avalonia.Base/Input/Navigation/XYFocus.FindElements.cs
  32. 2
      src/Avalonia.Base/Input/Navigation/XYFocus.Impl.cs
  33. 7
      src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs
  34. 26
      src/Avalonia.Base/Media/BaselinePixelAlignment.cs
  35. 20
      src/Avalonia.Base/Media/DrawingContext.cs
  36. 13
      src/Avalonia.Base/Media/DrawingGroup.cs
  37. 4
      src/Avalonia.Base/Media/PlatformDrawingContext.cs
  38. 45
      src/Avalonia.Base/Media/RenderOptions.cs
  39. 69
      src/Avalonia.Base/Media/StreamGeometryContext.cs
  40. 31
      src/Avalonia.Base/Media/TextHintingMode.cs
  41. 136
      src/Avalonia.Base/Media/TextOptions.cs
  42. 27
      src/Avalonia.Base/Media/TextRenderingMode.cs
  43. 11
      src/Avalonia.Base/Platform/IDrawingContextImpl.cs
  44. 18
      src/Avalonia.Base/Platform/IGeometryContext.cs
  45. 46
      src/Avalonia.Base/Platform/IGeometryContext2.cs
  46. 29
      src/Avalonia.Base/Platform/PathGeometryContext.cs
  47. 15
      src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataNodes.cs
  48. 6
      src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataDrawingContext.cs
  49. 4
      src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.PendingCommands.cs
  50. 21
      src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs
  51. 10
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
  52. 1
      src/Avalonia.Base/Rendering/ImmediateRenderer.cs
  53. 245
      src/Avalonia.Base/Utilities/StringTokenizer.cs
  54. 3
      src/Avalonia.Base/Visual.Composition.cs
  55. 64
      src/Avalonia.Base/Visual.cs
  56. 1
      src/Avalonia.Base/composition-schema.xml
  57. 4
      src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
  58. 2
      src/Avalonia.Controls/Automation/AutomationProperties.cs
  59. 9
      src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs
  60. 2
      src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
  61. 47
      src/Avalonia.Controls/Automation/Peers/ExpanderAutomationPeer.cs
  62. 10
      src/Avalonia.Controls/Automation/Peers/TextBlockAutomationPeer.cs
  63. 2
      src/Avalonia.Controls/Automation/Peers/TitleBarAutomationPeer.cs
  64. 26
      src/Avalonia.Controls/Automation/Peers/ToggleButtonAutomationPeer.cs
  65. 15
      src/Avalonia.Controls/Automation/TogglePatternIdentifiers.cs
  66. 16
      src/Avalonia.Controls/Design.cs
  67. 3
      src/Avalonia.Controls/Documents/Inline.cs
  68. 7
      src/Avalonia.Controls/Expander.cs
  69. 29
      src/Avalonia.Controls/Platform/Screen.cs
  70. 38
      src/Avalonia.Controls/Presenters/ItemsPresenter.cs
  71. 4
      src/Avalonia.Controls/Primitives/ILogicalScrollable.cs
  72. 6
      src/Avalonia.Controls/Primitives/TextSearch.cs
  73. 6
      src/Avalonia.Controls/ScrollViewer.cs
  74. 18
      src/Avalonia.Controls/VirtualizingCarouselPanel.cs
  75. 16
      src/Avalonia.Native/AvnAutomationPeer.cs
  76. 10
      src/Avalonia.Native/avn.idl
  77. 3
      src/Avalonia.Themes.Fluent/Controls/AutoCompleteBox.xaml
  78. 1
      src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml
  79. 6
      src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml
  80. 1
      src/Avalonia.Themes.Fluent/Controls/MenuScrollViewer.xaml
  81. 1
      src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml
  82. 1
      src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml
  83. 30
      src/Avalonia.Themes.Fluent/Controls/ScrollBar.xaml
  84. 6
      src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml
  85. 1
      src/Avalonia.Themes.Simple/Controls/AutoCompleteBox.xaml
  86. 3
      src/Avalonia.Themes.Simple/Controls/ComboBox.xaml
  87. 3
      src/Avalonia.Themes.Simple/Controls/OverlayPopupHost.xaml
  88. 3
      src/Avalonia.Themes.Simple/Controls/PopupRoot.xaml
  89. 30
      src/Avalonia.Themes.Simple/Controls/ScrollBar.xaml
  90. 1
      src/Avalonia.Themes.Simple/Controls/ScrollViewer.xaml
  91. 111
      src/Browser/Avalonia.Browser/Rendering/RenderWorker.cs
  92. 2
      src/HarfBuzz/Avalonia.HarfBuzz/Avalonia.HarfBuzz.csproj
  93. 3
      src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj
  94. 80
      src/Headless/Avalonia.Headless.XUnit/AvaloniaDelayEnumeratedTheoryTestCase.cs
  95. 32
      src/Headless/Avalonia.Headless.XUnit/AvaloniaFact.cs
  96. 34
      src/Headless/Avalonia.Headless.XUnit/AvaloniaFactDiscoverer.cs
  97. 126
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestAssemblyRunner.cs
  98. 63
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCase.cs
  99. 132
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCaseRunner.cs
  100. 31
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCaseRunnerContext.cs

57
.editorconfig

@ -138,30 +138,25 @@ space_within_single_line_array_initializer_braces = true
#Net Analyzer
dotnet_analyzer_diagnostic.category-Performance.severity = none #error - Uncomment when all violations are fixed.
# CS0649: Field 'field' is never assigned to, and will always have its default value 'value'
dotnet_diagnostic.CS0649.severity = error
# CS0162: Remove unreachable code
dotnet_diagnostic.CS0162.severity = error
# CA1018: Mark attributes with AttributeUsageAttribute
dotnet_diagnostic.CA1018.severity = error
dotnet_diagnostic.CA1018.severity = warning
# CA1304: Specify CultureInfo
dotnet_diagnostic.CA1304.severity = warning
# CA1802: Use literals where appropriate
dotnet_diagnostic.CA1802.severity = warning
# CA1813: Avoid unsealed attributes
dotnet_diagnostic.CA1813.severity = error
dotnet_diagnostic.CA1813.severity = warning
# CA1815: Override equals and operator equals on value types
dotnet_diagnostic.CA1815.severity = warning
# CA1820: Test for empty strings using string length
dotnet_diagnostic.CA1820.severity = warning
# CA1821: Remove empty finalizers
dotnet_diagnostic.CA1821.severity = error
dotnet_diagnostic.CA1821.severity = warning
# CA1822: Mark members as static
dotnet_diagnostic.CA1822.severity = suggestion
dotnet_code_quality.CA1822.api_surface = private, internal
# CA1823: Avoid unused private fields
dotnet_diagnostic.CA1823.severity = error
dotnet_diagnostic.CA1823.severity = warning
# CA1825: Avoid zero-length array allocations
dotnet_diagnostic.CA1825.severity = warning
# CA1826: Use property instead of Linq Enumerable method
@ -179,48 +174,48 @@ dotnet_diagnostic.CA1851.severity = warning
#CA1854: Prefer the IDictionary.TryGetValue(TKey, out TValue) method
dotnet_diagnostic.CA1854.severity = warning
#CA2211:Non-constant fields should not be visible
dotnet_diagnostic.CA2211.severity = error
dotnet_diagnostic.CA2211.severity = warning
# Wrapping preferences
csharp_wrap_before_ternary_opsigns = false
# Avalonia DevAnalyzer preferences
dotnet_diagnostic.AVADEV2001.severity = error
dotnet_diagnostic.AVADEV2001.severity = warning
# Avalonia PublicAnalyzer preferences
dotnet_diagnostic.AVP1000.severity = error
dotnet_diagnostic.AVP1001.severity = error
dotnet_diagnostic.AVP1002.severity = error
dotnet_diagnostic.AVP1010.severity = error
dotnet_diagnostic.AVP1011.severity = error
dotnet_diagnostic.AVP1000.severity = warning
dotnet_diagnostic.AVP1001.severity = warning
dotnet_diagnostic.AVP1002.severity = warning
dotnet_diagnostic.AVP1010.severity = warning
dotnet_diagnostic.AVP1011.severity = warning
dotnet_diagnostic.AVP1012.severity = warning
dotnet_diagnostic.AVP1013.severity = error
dotnet_diagnostic.AVP1020.severity = error
dotnet_diagnostic.AVP1021.severity = error
dotnet_diagnostic.AVP1022.severity = error
dotnet_diagnostic.AVP1030.severity = error
dotnet_diagnostic.AVP1031.severity = error
dotnet_diagnostic.AVP1032.severity = error
dotnet_diagnostic.AVP1040.severity = error
dotnet_diagnostic.AVA2001.severity = error
dotnet_diagnostic.AVP1013.severity = warning
dotnet_diagnostic.AVP1020.severity = warning
dotnet_diagnostic.AVP1021.severity = warning
dotnet_diagnostic.AVP1022.severity = warning
dotnet_diagnostic.AVP1030.severity = warning
dotnet_diagnostic.AVP1031.severity = warning
dotnet_diagnostic.AVP1032.severity = warning
dotnet_diagnostic.AVP1040.severity = warning
dotnet_diagnostic.AVA2001.severity = warning
# Xaml files
[*.{xaml,axaml}]
indent_size = 2
# DuplicateSetterError
avalonia_xaml_diagnostic.AVLN2203.severity = error
avalonia_xaml_diagnostic.AVLN2203.severity = warning
# StyleInMergedDictionaries
avalonia_xaml_diagnostic.AVLN2204.severity = error
avalonia_xaml_diagnostic.AVLN2204.severity = warning
# RequiredTemplatePartMissing
avalonia_xaml_diagnostic.AVLN2205.severity = error
avalonia_xaml_diagnostic.AVLN2205.severity = warning
# OptionalTemplatePartMissing
avalonia_xaml_diagnostic.AVLN2206.severity = info
# TemplatePartWrongType
avalonia_xaml_diagnostic.AVLN2207.severity = error
avalonia_xaml_diagnostic.AVLN2207.severity = warning
# ItemContainerInsideTemplate
avalonia_xaml_diagnostic.AVLN2208.severity = error
avalonia_xaml_diagnostic.AVLN2208.severity = warning
# Obsolete
avalonia_xaml_diagnostic.AVLN5001.severity = error
avalonia_xaml_diagnostic.AVLN5001.severity = warning
# Xml project files
[*.{csproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}]

1
Avalonia.sln

@ -106,7 +106,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1
build\TargetFrameworks.props = build\TargetFrameworks.props
build\TrimmingEnable.props = build\TrimmingEnable.props
build\UnitTests.NetFX.props = build\UnitTests.NetFX.props
build\WarnAsErrors.props = build\WarnAsErrors.props
build\XUnit.props = build\XUnit.props
EndProjectSection
EndProject

23
Directory.Build.props

@ -1,17 +1,18 @@
<Project>
<Import Project="$(MSBuildThisFileDirectory)/build/AvaloniaPublicKey.props"/>
<Import Project="$(MSBuildThisFileDirectory)/build/TargetFrameworks.props"/>
<Import Project="$(MSBuildThisFileDirectory)/build/WarnAsErrors.props" />
<PropertyGroup>
<PackageOutputPath Condition="'$(PackageOutputPath)' == ''">$(MSBuildThisFileDirectory)build-intermediate/nuget</PackageOutputPath>
<AvaloniaPreviewerNetCoreToolPath>$(MSBuildThisFileDirectory)\src\tools\Avalonia.Designer.HostApp\bin\$(Configuration)\$(AvsCurrentTargetFramework)\Avalonia.Designer.HostApp.dll</AvaloniaPreviewerNetCoreToolPath>
<!-- https://github.com/dotnet/msbuild/issues/2661 -->
<AddSyntheticProjectReferencesForSolutionDependencies>false</AddSyntheticProjectReferencesForSolutionDependencies>
<RunApiCompat>False</RunApiCompat>
<LangVersion>14.0</LangVersion>
<CreateHardLinksForCopyAdditionalFilesIfPossible>true</CreateHardLinksForCopyAdditionalFilesIfPossible>
<CreateHardLinksForCopyFilesToOutputDirectoryIfPossible>true</CreateHardLinksForCopyFilesToOutputDirectoryIfPossible>
<CreateHardLinksForCopyLocalIfPossible>true</CreateHardLinksForCopyLocalIfPossible>
<CreateHardLinksForPublishFilesIfPossible>true</CreateHardLinksForPublishFilesIfPossible>
<PackageOutputPath Condition="'$(PackageOutputPath)' == ''">$(MSBuildThisFileDirectory)build-intermediate/nuget</PackageOutputPath>
<AvaloniaPreviewerNetCoreToolPath>$(MSBuildThisFileDirectory)\src\tools\Avalonia.Designer.HostApp\bin\$(Configuration)\$(AvsCurrentTargetFramework)\Avalonia.Designer.HostApp.dll</AvaloniaPreviewerNetCoreToolPath>
<!-- https://github.com/dotnet/msbuild/issues/2661 -->
<AddSyntheticProjectReferencesForSolutionDependencies>false</AddSyntheticProjectReferencesForSolutionDependencies>
<RunApiCompat>False</RunApiCompat>
<LangVersion>14.0</LangVersion>
<TreatWarningsAsErrors Condition="'$(TreatWarningsAsErrors)' == ''">$(AvnTreatWarningsAsErrors)</TreatWarningsAsErrors>
<TreatWarningsAsErrors Condition="'$(TreatWarningsAsErrors)' == '' And '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
<CreateHardLinksForCopyAdditionalFilesIfPossible>true</CreateHardLinksForCopyAdditionalFilesIfPossible>
<CreateHardLinksForCopyFilesToOutputDirectoryIfPossible>true</CreateHardLinksForCopyFilesToOutputDirectoryIfPossible>
<CreateHardLinksForCopyLocalIfPossible>true</CreateHardLinksForCopyLocalIfPossible>
<CreateHardLinksForPublishFilesIfPossible>true</CreateHardLinksForPublishFilesIfPossible>
</PropertyGroup>
</Project>

76
api/Avalonia.Headless.XUnit.nupkg.xml

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Headless.XUnit.AvaloniaTestFrameworkTypeDiscoverer</Target>
<Left>baseline/Avalonia.Headless.XUnit/lib/net10.0/Avalonia.Headless.XUnit.dll</Left>
<Right>current/Avalonia.Headless.XUnit/lib/net10.0/Avalonia.Headless.XUnit.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Headless.XUnit.AvaloniaUIFactDiscoverer</Target>
<Left>baseline/Avalonia.Headless.XUnit/lib/net10.0/Avalonia.Headless.XUnit.dll</Left>
<Right>current/Avalonia.Headless.XUnit/lib/net10.0/Avalonia.Headless.XUnit.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Headless.XUnit.AvaloniaTestFrameworkTypeDiscoverer</Target>
<Left>baseline/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll</Left>
<Right>current/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Headless.XUnit.AvaloniaUIFactDiscoverer</Target>
<Left>baseline/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll</Left>
<Right>current/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Headless.XUnit.AvaloniaFactAttribute.#ctor</Target>
<Left>baseline/Avalonia.Headless.XUnit/lib/net10.0/Avalonia.Headless.XUnit.dll</Left>
<Right>current/Avalonia.Headless.XUnit/lib/net10.0/Avalonia.Headless.XUnit.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Headless.XUnit.AvaloniaTheoryDiscoverer.#ctor(Xunit.Abstractions.IMessageSink)</Target>
<Left>baseline/Avalonia.Headless.XUnit/lib/net10.0/Avalonia.Headless.XUnit.dll</Left>
<Right>current/Avalonia.Headless.XUnit/lib/net10.0/Avalonia.Headless.XUnit.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Headless.XUnit.AvaloniaFactAttribute.#ctor</Target>
<Left>baseline/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll</Left>
<Right>current/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Headless.XUnit.AvaloniaTheoryDiscoverer.#ctor(Xunit.Abstractions.IMessageSink)</Target>
<Left>baseline/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll</Left>
<Right>current/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0007</DiagnosticId>
<Target>T:Avalonia.Headless.XUnit.AvaloniaTheoryDiscoverer</Target>
<Left>baseline/Avalonia.Headless.XUnit/lib/net10.0/Avalonia.Headless.XUnit.dll</Left>
<Right>current/Avalonia.Headless.XUnit/lib/net10.0/Avalonia.Headless.XUnit.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0007</DiagnosticId>
<Target>T:Avalonia.Headless.XUnit.AvaloniaTheoryDiscoverer</Target>
<Left>baseline/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll</Left>
<Right>current/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0008</DiagnosticId>
<Target>T:Avalonia.Headless.XUnit.AvaloniaTestFrameworkAttribute</Target>
<Left>baseline/Avalonia.Headless.XUnit/lib/net10.0/Avalonia.Headless.XUnit.dll</Left>
<Right>current/Avalonia.Headless.XUnit/lib/net10.0/Avalonia.Headless.XUnit.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0008</DiagnosticId>
<Target>T:Avalonia.Headless.XUnit.AvaloniaTestFrameworkAttribute</Target>
<Left>baseline/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll</Left>
<Right>current/Avalonia.Headless.XUnit/lib/net8.0/Avalonia.Headless.XUnit.dll</Right>
</Suppression>
</Suppressions>

4
api/Avalonia.Win32.Interoperability.nupkg.xml

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
@ -37,4 +37,4 @@
<Left>baseline/Avalonia.Win32.Interoperability/lib/net8.0-windows7.0/Avalonia.Win32.Interoperability.dll</Left>
<Right>current/Avalonia.Win32.Interoperability/lib/net8.0-windows7.0/Avalonia.Win32.Interoperability.dll</Right>
</Suppression>
</Suppressions>
</Suppressions>

956
api/Avalonia.nupkg.xml

File diff suppressed because it is too large

1
build/AnalyzerProject.targets

@ -3,6 +3,7 @@
<PropertyGroup>
<EnforceExtendedAnalyzerRules Condition="'$(EnforceExtendedAnalyzerRules)' == ''">true</EnforceExtendedAnalyzerRules>
<IsRoslynComponent Condition="'$(IsRoslynComponent)' == ''">true</IsRoslynComponent>
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
<NoWarn>$(NoWarn);RS2008</NoWarn>
</PropertyGroup>

8
build/NullableEnable.props

@ -1,11 +1,7 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!--
The Nullable annotations on netstandard2.0 are incomplete and incorrect in places. Ignore
nullable warnings before .NET 6 and make them errors on later target frameworks.
-->
<PropertyGroup>
<Nullable>enable</Nullable>
<WarningsAsErrors Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net6.0'))">$(WarningsAsErrors);nullable</WarningsAsErrors>
<NoWarn Condition="!$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net6.0'))">$(NoWarn);nullable</NoWarn>
<!-- The nullable annotations on netstandard2.0 are incomplete and incorrect in places. Ignore them. -->
<NoWarn Condition="'$(TargetFramework)' == 'netstandard2.0'">$(NoWarn);nullable</NoWarn>
</PropertyGroup>
</Project>

18
build/TrimmingEnable.props

@ -1,28 +1,16 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net6.0'))">
<PropertyGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">
<SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
<TrimmerSingleWarn>false</TrimmerSingleWarn>
<IsTrimmable>true</IsTrimmable>
</PropertyGroup>
<PropertyGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">
<IsAotCompatible Condition="'$(IsAotCompatible)' == ''">true</IsAotCompatible>
<ILLinkTreatWarningsAsErrors>$(TreatWarningsAsErrors)</ILLinkTreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net6.0'))">
<ILLinkTreatWarningsAsErrors>true</ILLinkTreatWarningsAsErrors>
<!-- Trim warnings -->
<WarningsAsErrors>$(WarningsAsErrors);IL2000;IL2001;IL2002;IL2003;IL2004;IL2005;IL2006;IL2007;IL2008;IL2009;IL2010;IL2011;IL2012;IL2013;IL2014;IL2015;IL2016;IL2017;IL2018;IL2019;IL2020;IL2021;IL2022;IL2023;IL2024;IL2025;IL2026;IL2027;IL2028;IL2029;IL2030;IL2031;IL2032;IL2033;IL2034;IL2035;IL2036;IL2037;IL2038;IL2039;IL2040;IL2041;IL2042;IL2043;IL2044;IL2045;IL2046;IL2047;IL2048;IL2049;IL2050;IL2051;IL2052;IL2053;IL2054;IL2055;IL2056;IL2057;IL2058;IL2059;IL2060;IL2061;IL2062;IL2063;IL2064;IL2065;IL2066;IL2067;IL2068;IL2069;IL2070;IL2071;IL2072;IL2073;IL2074;IL2075;IL2076;IL2077;IL2078;IL2079;IL2080;IL2081;IL2082;IL2083;IL2084;IL2085;IL2086;IL2087;IL2088;IL2089;IL2090;IL2091;IL2092;IL2093;IL2094;IL2095;IL2096;IL2097;IL2098;IL2099;IL2100;IL2101;IL2102;IL2103;IL2104;IL2105;IL2106;IL2107;IL2108;IL2109;IL2110;IL2111;IL2112;IL2113;IL2114;IL2115;IL2116;IL2117;IL2118;IL2119;IL2120;IL2121;IL2122;IL2123;IL2124;IL2125;IL2126;IL2127;IL2128;IL2129;IL2130;IL2131;IL2132;IL2133;IL2134;IL2135;IL2136;IL2137;IL2138;IL2139;IL2140;IL2141;IL2142;IL2143;IL2144;IL2145;IL2146;IL2147;IL2148;IL2149;IL2150;IL2151;IL2152;IL2153;IL2154;IL2155;IL2156;IL2157</WarningsAsErrors>
<!-- NativeAOT warnings -->
<WarningsAsErrors Condition="'$(IsAotCompatible)' == 'true'">$(WarningsAsErrors);IL3050;IL3051;IL3052;IL3053;IL3054;IL3055;IL3056</WarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0')) AND '$(EnableRuntimeMarshalling)' != 'true'">
<WarningsAsErrors>$(WarningsAsErrors);CA1420;CA1421</WarningsAsErrors>
</PropertyGroup>
<ItemGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0')) AND '$(EnableRuntimeMarshalling)' != 'true'">
<AssemblyAttribute Include="System.Runtime.CompilerServices.DisableRuntimeMarshallingAttribute" />
</ItemGroup>
</Project>

23
build/WarnAsErrors.props

@ -1,23 +0,0 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- Keep in sync with .editorconfig -->
<PropertyGroup Condition="'$(AvsEnableDevWarningsNotAsErrors)' == 'true'">
<!-- CS0649: Field 'field' is never assigned to, and will always have its default value 'value' -->
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS0649</WarningsNotAsErrors>
<!-- CS0162: Remove unreachable code -->
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS0162</WarningsNotAsErrors>
<!-- CA2211:Non-constant fields should not be visible -->
<WarningsNotAsErrors>$(WarningsNotAsErrors);CA2211</WarningsNotAsErrors>
<!-- CA1821: Remove empty finalizers -->
<WarningsNotAsErrors>$(WarningsNotAsErrors);CA1821</WarningsNotAsErrors>
<!-- CA1823: Avoid unused private fields -->
<WarningsNotAsErrors>$(WarningsNotAsErrors);CA1823</WarningsNotAsErrors>
<!-- AVLN2203: DuplicateSetterError -->
<WarningsNotAsErrors>$(WarningsNotAsErrors);AVLN2203</WarningsNotAsErrors>
<!-- AVLN2205: RequiredTemplatePartMissing -->
<WarningsNotAsErrors>$(WarningsNotAsErrors);AVLN2205</WarningsNotAsErrors>
<!-- AVLN2207: TemplatePartWrongType -->
<WarningsNotAsErrors>$(WarningsNotAsErrors);AVLN2207</WarningsNotAsErrors>
<!-- AVLN2208: ItemContainerInsideTemplate -->
<WarningsNotAsErrors>$(WarningsNotAsErrors);AVLN2208</WarningsNotAsErrors>
</PropertyGroup>
</Project>

9
build/XUnit.props

@ -1,14 +1,13 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Condition="'$(IsXUnit2)'!='true'">
<ItemGroup>
<PackageReference Include="xunit.v3.mtp-v2" Version="3.2.1" />
</ItemGroup>
<ItemGroup Condition="'$(IsXUnit2)'=='true'">
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="YTest.MTP.XUnit2" Version="1.0.2" />
</ItemGroup>
<PropertyGroup>
<AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)\avalonia.snk</AssemblyOriginatorKeyFile>
<SignAssembly>False</SignAssembly>
<NoWarn>$(NoWarn);CS8002</NoWarn> <!-- ignore signing warnings for unit tests -->
</PropertyGroup>
</Project>

2
docs/build.md

@ -71,7 +71,7 @@ And run tests:
Or if you need to create nuget packages as well (it will compile and run tests automatically):
`nuke --target Package --configuration Release`
Alternatively, you can run nuke build direclty without installing Nuke global tool:
Alternatively, you can run nuke build directly without installing Nuke global tool:
`dotnet run --project nukebuild/_build.csproj -- --configuration Debug`
# Linux/macOS

2
external/Numerge

@ -1 +1 @@
Subproject commit 9738c6121fdd143c78d3e25686a7e4e13c00f586
Subproject commit 5530e1cbe9e105ff4ebc9da1f4af3253a8756754

1
native/Avalonia.Native/src/OSX/AvnAccessibility.h

@ -7,7 +7,6 @@
@protocol AvnAccessibility <NSAccessibility>
@required
- (void) raiseChildrenChanged;
@optional
- (void) raiseFocusChanged;
- (void) raisePropertyChanged:(AvnAutomationProperty)property;
@end

4
native/Avalonia.Native/src/OSX/AvnWindow.mm

@ -656,5 +656,9 @@
NSAccessibilityPostNotification(focused, NSAccessibilityFocusedUIElementChangedNotification);
}
- (void)raisePropertyChanged:(AvnAutomationProperty)property
{
}
@end

37
native/Avalonia.Native/src/OSX/automation.mm

@ -11,6 +11,7 @@
IAvnAutomationPeer* _peer;
AvnAutomationNode* _node;
NSMutableArray* _children;
NSArray<NSString*>* _attributeNames;
}
+ (NSAccessibilityElement *)acquire:(IAvnAutomationPeer *)peer
@ -126,6 +127,7 @@
case AutomationHeaderItem: return NSAccessibilityButtonRole;
case AutomationTable: return NSAccessibilityTableRole;
case AutomationTitleBar: return NSAccessibilityGroupRole;
case AutomationExpander: return NSAccessibilityDisclosureTriangleRole;
// Treat unknown roles as generic group container items. Returning
// NSAccessibilityUnknownRole is also possible but makes the screen
// reader focus on the item instead of passing focus to child items.
@ -165,6 +167,32 @@
return NSAccessibilityRoleDescription([self accessibilityRole], [self accessibilitySubrole]);
}
// Note: Apple has deprecated this API, but it's still used to set attributes not supported by NSAccessibility
- (NSArray<NSString *> *)accessibilityAttributeNames
{
if (_attributeNames == nil)
{
_attributeNames = @[
@"AXARIALive", // kAXARIALiveAttribute
];
}
return _attributeNames;
}
- (id)accessibilityAttributeValue:(NSAccessibilityAttributeName)attribute
{
if ([attribute isEqualToString:@"AXARIALive" /* kAXARIALiveAttribute */])
{
switch (_peer->GetLiveSetting())
{
case LiveSettingPolite: return @"polite";
case LiveSettingAssertive: return @"assertive";
}
return nil;
}
return nil;
}
- (NSString *)accessibilityIdentifier
{
return GetNSStringAndRelease(_peer->GetAutomationId());
@ -420,8 +448,15 @@
@{ NSAccessibilityUIElementsKey: [changed allObjects]});
}
- (void)raisePropertyChanged
- (void)raisePropertyChanged:(AvnAutomationProperty)property
{
if (property == AutomationPeer_Name && _peer->GetLiveSetting() != LiveSettingOff)
[self raiseLiveRegionChanged];
}
- (void)raiseLiveRegionChanged
{
NSAccessibilityPostNotification(self, @"AXLiveRegionChanged" /* kAXLiveRegionChangedNotification */);
}
- (void)setAccessibilityFocused:(BOOL)accessibilityFocused

9
nukebuild/.editorconfig

@ -6,12 +6,3 @@ root = false
# C# files
[*.cs]
dotnet_style_require_accessibility_modifiers = never
[{il-repack,Numerge}/**/*.cs]
dotnet_diagnostic.CA1304.severity = none
dotnet_diagnostic.CA1815.severity = none
dotnet_diagnostic.CA1820.severity = none
dotnet_diagnostic.CA1825.severity = none
dotnet_diagnostic.CA1829.severity = none
dotnet_diagnostic.CA1847.severity = none
dotnet_diagnostic.SYSLIB0017.severity = none

4
nukebuild/_build.csproj

@ -4,11 +4,9 @@
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<RootNamespace></RootNamespace>
<IsPackable>False</IsPackable>
<NoWarn>$(NoWarn);CS0649;CS0169;SYSLIB0011</NoWarn>
<NoWarn>$(NoWarn);CS0649;CA1847</NoWarn>
<NukeTelemetryVersion>1</NukeTelemetryVersion>
<TargetFramework>$(AvsCurrentTargetFramework)</TargetFramework>
<!-- See https://github.com/nuke-build/nuke/issues/818 -->
<EnableUnsafeBinaryFormatterSerialization>true</EnableUnsafeBinaryFormatterSerialization>
</PropertyGroup>
<ItemGroup>

2
packages/Avalonia/Avalonia.props

@ -1,6 +1,6 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<AvaloniaPreviewerNetCoreToolPath>$(MSBuildThisFileDirectory)\..\tools\$(AvsCurrentTargetFramework)\designer\Avalonia.Designer.HostApp.dll</AvaloniaPreviewerNetCoreToolPath>
<AvaloniaPreviewerNetCoreToolPath>$(MSBuildThisFileDirectory)\..\tools\net8.0\designer\Avalonia.Designer.HostApp.dll</AvaloniaPreviewerNetCoreToolPath>
<AvaloniaBuildTasksLocation>$(MSBuildThisFileDirectory)\..\tools\netstandard2.0\Avalonia.Build.Tasks.dll</AvaloniaBuildTasksLocation>
<UsedAvaloniaProducts>$(UsedAvaloniaProducts);AvaloniaUI</UsedAvaloniaProducts>
<EnableAvaloniaXamlCompilation Condition="'$(EnableAvaloniaXamlCompilation)'==''">true</EnableAvaloniaXamlCompilation>

3
samples/IntegrationTestApp/Pages/AutomationPage.axaml

@ -22,5 +22,8 @@
<TextBlock Name="TextBlockWithoutHeader">
Header None
</TextBlock>
<Button Click="OnButtonAddSomeText">Add some live region text</Button>
<TextBlock Name="textLiveRegion" AutomationProperties.LiveSetting="Assertive">This is an assertive live region.</TextBlock>
</StackPanel>
</UserControl>

6
samples/IntegrationTestApp/Pages/AutomationPage.axaml.cs

@ -1,4 +1,5 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace IntegrationTestApp.Pages;
@ -8,4 +9,9 @@ public partial class AutomationPage : UserControl
{
InitializeComponent();
}
private void OnButtonAddSomeText(object? sender, RoutedEventArgs? e)
{
textLiveRegion.Text += " Lorem ipsum.";
}
}

2
samples/TextTestApp/InteractiveLineControl.cs

@ -336,7 +336,7 @@ namespace TextTestApp
_textSource = new TextSource(this);
RenderOptions.SetEdgeMode(this, EdgeMode.Aliased);
RenderOptions.SetTextRenderingMode(this, TextRenderingMode.SubpixelAntialias);
TextOptions.SetTextRenderingMode(this, TextRenderingMode.SubpixelAntialias);
}
private void InvalidateTextRunProperties()

10
src/Android/Avalonia.Android/AvaloniaView.cs

@ -39,6 +39,7 @@ namespace Avalonia.Android
OnConfigurationChanged();
_view.InternalView.SurfaceWindowCreated += InternalView_SurfaceWindowCreated;
_view.InternalView.SurfaceWindowDestroyed += InternalView_SurfaceWindowDestroyed;
_accessHelper = new AvaloniaAccessHelper(this);
ViewCompat.SetAccessibilityDelegate(this, _accessHelper);
@ -51,9 +52,18 @@ namespace Avalonia.Android
if (Visibility == ViewStates.Visible)
{
OnVisibilityChanged(true);
_root?.InvalidateMeasure();
Invalidate();
}
}
private void InternalView_SurfaceWindowDestroyed(object? sender, EventArgs e)
{
OnVisibilityChanged(false);
_surfaceCreated = false;
}
internal TopLevelImpl TopLevelImpl => _view;
internal TopLevel? TopLevel => _root;

3
src/Android/Avalonia.Android/Platform/SkiaPlatform/InvalidationAwareSurfaceView.cs

@ -18,6 +18,8 @@ namespace Avalonia.Android
private double _scaling = 1;
public event EventHandler? SurfaceWindowCreated;
public event EventHandler? SurfaceWindowDestroyed;
public PixelSize Size => _size;
public double Scaling => _scaling;
@ -61,6 +63,7 @@ namespace Avalonia.Android
.Log(this, "InvalidationAwareSurfaceView Destroyed");
ReleaseNativeWindowHandle();
_size = new PixelSize(1, 1);
SurfaceWindowDestroyed?.Invoke(this, EventArgs.Empty);
}
public virtual void SurfaceRedrawNeeded(ISurfaceHolder holder)

11
src/Avalonia.Base/Controls/IInternalScroller.cs

@ -1,11 +0,0 @@
using System.Runtime.CompilerServices;
namespace Avalonia.Controls.Primitives;
// TODO12: Integrate with existing IScrollable interface, breaking change
internal interface IInternalScroller
{
bool CanHorizontallyScroll { get; }
bool CanVerticallyScroll { get; }
}

10
src/Avalonia.Controls/IScrollable.cs → src/Avalonia.Base/Controls/Primitives/IScrollable.cs

@ -19,5 +19,15 @@ namespace Avalonia.Controls.Primitives
/// Gets the size of the viewport, in logical units.
/// </summary>
Size Viewport { get; }
/// <summary>
/// Gets a value indicating whether the content can be scrolled horizontally.
/// </summary>
bool CanHorizontallyScroll { get; }
/// <summary>
/// Gets a value indicating whether the content can be scrolled horizontally.
/// </summary>
bool CanVerticallyScroll { get; }
}
}

164
src/Avalonia.Base/Data/CompiledBinding.cs

@ -0,0 +1,164 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using Avalonia.Controls;
using Avalonia.Data.Converters;
using Avalonia.Data.Core;
using Avalonia.Data.Core.ExpressionNodes;
using Avalonia.Data.Core.Parsers;
namespace Avalonia.Data;
/// <summary>
/// A binding which does not use reflection to access members.
/// </summary>
public class CompiledBinding : BindingBase
{
/// <summary>
/// Initializes a new instance of the <see cref="CompiledBinding"/> class.
/// </summary>
public CompiledBinding() { }
/// <summary>
/// Initializes a new instance of the <see cref="CompiledBinding"/> class.
/// </summary>
/// <param name="path">The binding path.</param>
public CompiledBinding(CompiledBindingPath path) => Path = path;
/// <summary>
/// Gets or sets the amount of time, in milliseconds, to wait before updating the binding
/// source after the value on the target changes.
/// </summary>
/// <remarks>
/// There is no delay when the source is updated via <see cref="UpdateSourceTrigger.LostFocus"/>
/// or <see cref="BindingExpressionBase.UpdateSource"/>. Nor is there a delay when
/// <see cref="BindingMode.OneWayToSource"/> is active and a new source object is provided.
/// </remarks>
public int Delay { get; set; }
/// <summary>
/// Gets or sets the <see cref="IValueConverter"/> to use.
/// </summary>
public IValueConverter? Converter { get; set; }
/// <summary>
/// Gets or sets the culture in which to evaluate the converter.
/// </summary>
/// <value>The default value is null.</value>
/// <remarks>
/// If this property is not set then <see cref="CultureInfo.CurrentCulture"/> will be used.
/// </remarks>
[TypeConverter(typeof(CultureInfoIetfLanguageTagConverter))]
public CultureInfo? ConverterCulture { get; set; }
/// <summary>
/// Gets or sets a parameter to pass to <see cref="Converter"/>.
/// </summary>
public object? ConverterParameter { get; set; }
/// <summary>
/// Gets or sets the value to use when the binding is unable to produce a value.
/// </summary>
public object? FallbackValue { get; set; } = AvaloniaProperty.UnsetValue;
/// <summary>
/// Gets or sets the binding mode.
/// </summary>
public BindingMode Mode { get; set; }
/// <summary>
/// Gets or sets the binding path.
/// </summary>
public CompiledBindingPath? Path { get; set; }
/// <summary>
/// Gets or sets the binding priority.
/// </summary>
public BindingPriority Priority { get; set; }
/// <summary>
/// Gets or sets the source for the binding.
/// </summary>
public object? Source { get; set; } = AvaloniaProperty.UnsetValue;
/// <summary>
/// Gets or sets the string format.
/// </summary>
public string? StringFormat { get; set; }
/// <summary>
/// Gets or sets the value to use when the binding result is null.
/// </summary>
public object? TargetNullValue { get; set; } = AvaloniaProperty.UnsetValue;
/// <summary>
/// Gets or sets a value that determines the timing of binding source updates for
/// <see cref="BindingMode.TwoWay"/> and <see cref="BindingMode.OneWayToSource"/> bindings.
/// </summary>
public UpdateSourceTrigger UpdateSourceTrigger { get; set; }
internal WeakReference? DefaultAnchor { get; set; }
internal WeakReference<INameScope?>? NameScope { get; set; }
internal override BindingExpressionBase CreateInstance(
AvaloniaObject target,
AvaloniaProperty? targetProperty,
object? anchor)
{
var enableDataValidation = targetProperty?.GetMetadata(target).EnableDataValidation ?? false;
var nodes = new List<ExpressionNode>();
var isRooted = false;
Path?.BuildExpression(nodes, out isRooted);
// If the binding isn't rooted (i.e. doesn't have a Source or start with $parent, $self,
// #elementName etc.) then we need to add a data context source node.
if (Source == AvaloniaProperty.UnsetValue && !isRooted)
nodes.Insert(0, ExpressionNodeFactory.CreateDataContext(targetProperty));
// If the first node is an ISourceNode then allow it to select the source; otherwise
// use the binding source if specified, falling back to the target.
var source = nodes?.Count > 0 && nodes[0] is SourceNode sn
? sn.SelectSource(Source, target, anchor ?? DefaultAnchor?.Target)
: Source != AvaloniaProperty.UnsetValue ? Source : target;
var (mode, trigger) = ResolveDefaultsFromMetadata(target, targetProperty);
return new BindingExpression(
source,
nodes,
FallbackValue,
delay: TimeSpan.FromMilliseconds(Delay),
converter: Converter,
converterCulture: ConverterCulture,
converterParameter: ConverterParameter,
enableDataValidation: enableDataValidation,
mode: mode,
priority: Priority,
stringFormat: StringFormat,
targetNullValue: TargetNullValue,
targetProperty: targetProperty,
targetTypeConverter: TargetTypeConverter.GetDefaultConverter(),
updateSourceTrigger: trigger);
}
private (BindingMode, UpdateSourceTrigger) ResolveDefaultsFromMetadata(
AvaloniaObject target,
AvaloniaProperty? targetProperty)
{
var mode = Mode;
var trigger = UpdateSourceTrigger == UpdateSourceTrigger.Default ?
UpdateSourceTrigger.PropertyChanged : UpdateSourceTrigger;
if (mode == BindingMode.Default)
{
if (targetProperty?.GetMetadata(target) is { } metadata)
mode = metadata.DefaultBindingMode;
else
mode = BindingMode.OneWay;
}
return (mode, trigger);
}
}

29
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs → src/Avalonia.Base/Data/CompiledBindingPath.cs

@ -4,10 +4,10 @@ using System.Reflection;
using Avalonia.Controls;
using Avalonia.Data.Core;
using Avalonia.Data.Core.ExpressionNodes;
using Avalonia.Data.Core.Parsers;
using Avalonia.Data.Core.Plugins;
using Avalonia.Markup.Parsers;
namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
namespace Avalonia.Data
{
public class CompiledBindingPath
{
@ -96,21 +96,17 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
/// <inheritdoc />
public override string ToString()
=> string.Concat((IEnumerable<ICompiledBindingPathElement>) _elements);
=> string.Concat((IEnumerable<ICompiledBindingPathElement>)_elements);
}
public class CompiledBindingPathBuilder
{
private readonly int _apiVersion;
private readonly List<ICompiledBindingPathElement> _elements = new();
public CompiledBindingPathBuilder()
{
}
// TODO12: Remove this constructor. apiVersion is only needed for compatibility with
// versions of Avalonia which used $self.Property() for building TemplatedParent bindings.
public CompiledBindingPathBuilder(int apiVersion) => _apiVersion = apiVersion;
public CompiledBindingPathBuilder Not()
{
@ -120,22 +116,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
public CompiledBindingPathBuilder Property(IPropertyInfo info, Func<WeakReference<object?>, IPropertyInfo, IPropertyAccessor> accessorFactory)
{
// Older versions of Avalonia used $self.Property() for building TemplatedParent bindings.
// Try to detect this and upgrade to using a TemplatedParentPathElement so that logging works
// correctly.
if (_apiVersion == 0 &&
info.Name == "TemplatedParent" &&
_elements.Count >= 1 &&
_elements[_elements.Count - 1] is SelfPathElement)
{
_elements.Add(new TemplatedParentPathElement());
}
else
{
return Property(info, accessorFactory, acceptsNull: false);
}
return this;
return Property(info, accessorFactory, acceptsNull: false);
}
public CompiledBindingPathBuilder Property(
@ -285,7 +266,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
public MethodInfo Method { get; }
public Type DelegateType { get; }
public bool AcceptsNull { get; }
}

6
src/Avalonia.Base/Input/IKeyboardNavigationHandler.cs

@ -24,9 +24,11 @@ namespace Avalonia.Input
/// <param name="element">The current element.</param>
/// <param name="direction">The direction to move.</param>
/// <param name="keyModifiers">Any key modifiers active at the time of focus.</param>
void Move(
/// <param name="deviceType">The device type used to move the focus.</param>
bool Move(
IInputElement element,
NavigationDirection direction,
KeyModifiers keyModifiers = KeyModifiers.None);
KeyModifiers keyModifiers = KeyModifiers.None,
KeyDeviceType? deviceType = null);
}
}

22
src/Avalonia.Base/Input/KeyboardNavigationHandler.cs

@ -98,22 +98,12 @@ namespace Avalonia.Input
return result;
}
/// <summary>
/// Moves the focus in the specified direction.
/// </summary>
/// <param name="element">The current element.</param>
/// <param name="direction">The direction to move.</param>
/// <param name="keyModifiers">Any key modifiers active at the time of focus.</param>
public void Move(
/// <inheritdoc />
public bool Move(
IInputElement? element,
NavigationDirection direction,
KeyModifiers keyModifiers = KeyModifiers.None)
{
MovePrivate(element, direction, keyModifiers, null);
}
// TODO12: remove MovePrivate, and make Move return boolean. Or even remove whole KeyboardNavigationHandler.
private bool MovePrivate(IInputElement? element, NavigationDirection direction, KeyModifiers keyModifiers, KeyDeviceType? deviceType)
KeyModifiers keyModifiers = KeyModifiers.None,
KeyDeviceType? deviceType = null)
{
var next = GetNextPrivate(element, _owner, direction, deviceType);
@ -140,7 +130,7 @@ namespace Avalonia.Input
var current = FocusManager.GetFocusManager(e.Source as IInputElement)?.GetFocusedElement();
var direction = (e.KeyModifiers & KeyModifiers.Shift) == 0 ?
NavigationDirection.Next : NavigationDirection.Previous;
e.Handled = MovePrivate(current, direction, e.KeyModifiers, e.KeyDeviceType);
e.Handled = Move(current, direction, e.KeyModifiers, e.KeyDeviceType);
}
else if (e.Key is Key.Left or Key.Right or Key.Up or Key.Down)
{
@ -153,7 +143,7 @@ namespace Avalonia.Input
Key.Down => NavigationDirection.Down,
_ => throw new ArgumentOutOfRangeException()
};
e.Handled = MovePrivate(current, direction, e.KeyModifiers, e.KeyDeviceType);
e.Handled = Move(current, direction, e.KeyModifiers, e.KeyDeviceType);
}
}

4
src/Avalonia.Base/Input/Navigation/XYFocus.FindElements.cs

@ -78,7 +78,7 @@ public partial class XYFocus
return false;
}
var closestScroller = candidate.FindAncestorOfType<IInternalScroller>(true);
var closestScroller = candidate.FindAncestorOfType<IScrollable>(true);
return ReferenceEquals(closestScroller, activeScroller);
}
@ -93,7 +93,7 @@ public partial class XYFocus
var parent = activeScroller.Parent;
while (parent != null)
{
if (parent is IInternalScroller and Visual visual
if (parent is IScrollable and Visual visual
&& visual.IsVisualAncestorOf(candidate))
{
return true;

2
src/Avalonia.Base/Input/Navigation/XYFocus.Impl.cs

@ -415,7 +415,7 @@ public partial class XYFocus
while (parent != null)
{
var element = parent;
if (element is IInternalScroller scrollable)
if (element is IScrollable scrollable)
{
var isHorizontallyScrollable = scrollable.CanHorizontallyScroll;
var isVerticallyScrollable = scrollable.CanVerticallyScroll;

7
src/Avalonia.Base/Input/TextInput/TextInputMethodClient.cs

@ -82,13 +82,6 @@ namespace Avalonia.Input.TextInput
{
SetPreeditText(preeditText);
}
//TODO12: remove
[Obsolete]
public virtual void ShowInputPanel()
{
RaiseInputPaneActivationRequested();
}
protected virtual void RaiseTextViewVisualChanged()
{

26
src/Avalonia.Base/Media/BaselinePixelAlignment.cs

@ -0,0 +1,26 @@
namespace Avalonia.Media
{
/// <summary>
/// Specifies the baseline pixel alignment options for rendering text or graphics.
/// </summary>
/// <remarks>Use this enumeration to control whether the baseline of rendered content is aligned to the
/// pixel grid, which can affect visual crispness and positioning. The value may influence rendering quality,
/// especially at small font sizes or when precise alignment is required.</remarks>
public enum BaselinePixelAlignment : byte
{
/// <summary>
/// The baseline pixel alignment is unspecified.
/// </summary>
Unspecified,
/// <summary>
/// The baseline is aligned to the pixel grid.
/// </summary>
Aligned,
/// <summary>
/// The baseline is not aligned to the pixel grid.
/// </summary>
Unaligned
}
}

20
src/Avalonia.Base/Media/DrawingContext.cs

@ -284,7 +284,8 @@ namespace Avalonia.Media
Clip,
GeometryClip,
OpacityMask,
RenderOptions
RenderOptions,
TextOptions
}
public RestoreState(DrawingContext context, PushedStateType type)
@ -311,6 +312,8 @@ namespace Avalonia.Media
_context.PopOpacityMaskCore();
else if (_type == PushedStateType.RenderOptions)
_context.PopRenderOptionsCore();
else if (_type == PushedStateType.TextOptions)
_context.PopTextOptionsCore();
}
}
@ -417,6 +420,20 @@ namespace Avalonia.Media
}
protected abstract void PushRenderOptionsCore(RenderOptions renderOptions);
/// <summary>
/// Pushes text options for the drawing context.
/// </summary>
/// <param name="textOptions">The text options.</param>
/// <returns>A disposable to undo the text options.</returns>
public PushedState PushTextOptions(TextOptions textOptions)
{
PushTextOptionsCore(textOptions);
_states ??= StateStackPool.Get();
_states.Push(new RestoreState(this, RestoreState.PushedStateType.TextOptions));
return new PushedState(this);
}
protected abstract void PushTextOptionsCore(TextOptions textOptions);
[Obsolete("Use PushTransform"), EditorBrowsable(EditorBrowsableState.Never)]
public PushedState PushPreTransform(Matrix matrix) => PushTransform(matrix);
[Obsolete("Use PushTransform"), EditorBrowsable(EditorBrowsableState.Never)]
@ -433,6 +450,7 @@ namespace Avalonia.Media
protected abstract void PopOpacityMaskCore();
protected abstract void PopTransformCore();
protected abstract void PopRenderOptionsCore();
protected abstract void PopTextOptionsCore();
private static bool PenIsVisible(IPen? pen)
{

13
src/Avalonia.Base/Media/DrawingGroup.cs

@ -54,6 +54,7 @@ namespace Avalonia.Media
}
internal RenderOptions? RenderOptions { get; set; }
internal TextOptions? TextOptions { get; set; }
/// <summary>
/// Gets or sets the collection that contains the child geometries.
@ -78,6 +79,7 @@ namespace Avalonia.Media
using (ClipGeometry != null ? context.PushGeometryClip(ClipGeometry) : default)
using (OpacityMask != null ? context.PushOpacityMask(OpacityMask, bounds) : default)
using (RenderOptions != null ? context.PushRenderOptions(RenderOptions.Value) : default)
using (TextOptions != null ? context.PushTextOptions(TextOptions.Value) : default)
{
foreach (var drawing in Children)
{
@ -325,6 +327,15 @@ namespace Avalonia.Media
drawingGroup.RenderOptions = renderOptions;
}
protected override void PushTextOptionsCore(TextOptions textOptions)
{
// Instantiate a new drawing group and set it as the _currentDrawingGroup
var drawingGroup = PushNewDrawingGroup();
// Set the text options on the new DrawingGroup
drawingGroup.TextOptions = textOptions;
}
protected override void PopClipCore() => Pop();
protected override void PopGeometryClipCore() => Pop();
@ -337,6 +348,8 @@ namespace Avalonia.Media
protected override void PopRenderOptionsCore() => Pop();
protected override void PopTextOptionsCore() => Pop();
/// <summary>
/// Creates a new DrawingGroup for a Push* call by setting the
/// _currentDrawingGroup to a newly instantiated DrawingGroup,

4
src/Avalonia.Base/Media/PlatformDrawingContext.cs

@ -85,6 +85,8 @@ internal sealed class PlatformDrawingContext : DrawingContext
}
protected override void PushRenderOptionsCore(RenderOptions renderOptions) => _impl.PushRenderOptions(renderOptions);
protected override void PushTextOptionsCore(TextOptions textOptions) => _impl.PushTextOptions(textOptions);
protected override void PopClipCore() => _impl.PopClip();
@ -99,6 +101,8 @@ internal sealed class PlatformDrawingContext : DrawingContext
(_transforms ?? throw new ObjectDisposedException(nameof(PlatformDrawingContext))).Pop();
protected override void PopRenderOptionsCore() => _impl.PopRenderOptions();
protected override void PopTextOptionsCore() => _impl.PopTextOptions();
protected override void DisposeCore()
{

45
src/Avalonia.Base/Media/RenderOptions.cs

@ -1,13 +1,48 @@
using Avalonia.Media.Imaging;
using System;
using Avalonia.Media.Imaging;
namespace Avalonia.Media
{
/// <summary>
/// Provides a set of options that control rendering behavior for visuals, including text rendering, bitmap
/// interpolation, edge rendering, blending, and opacity handling.
/// </summary>
/// <remarks>Use this structure to specify rendering preferences for visual elements. Each property
/// corresponds to a specific aspect of rendering, allowing fine-grained control over how content is displayed.
/// These options can be applied to visuals to influence quality, performance, and visual effects. When merging two
/// instances, unspecified values are inherited from the other instance, enabling layered configuration.</remarks>
public readonly record struct RenderOptions
{
/// <summary>
/// Gets the text rendering mode used to control how text glyphs are rendered.
/// </summary>
[Obsolete("TextRenderingMode is obsolete. Use TextOptions.TextRenderingMode instead.")]
public TextRenderingMode TextRenderingMode { get; init; }
/// <summary>
/// Gets the interpolation mode used when rendering bitmap images.
/// </summary>
/// <remarks>The interpolation mode determines how bitmap images are scaled or transformed during
/// rendering. Selecting an appropriate mode can affect image quality and performance.
/// </remarks>
public BitmapInterpolationMode BitmapInterpolationMode { get; init; }
/// <summary>
/// Gets the edge rendering mode used for drawing operations.
/// </summary>
public EdgeMode EdgeMode { get; init; }
public TextRenderingMode TextRenderingMode { get; init; }
/// <summary>
/// Gets the blending mode used when rendering bitmap images.
/// </summary>
/// <remarks>The blending mode determines how bitmap pixels are composited with the background or
/// other images. Select an appropriate mode based on the desired visual effect, such as transparency or
/// additive blending.</remarks>
public BitmapBlendingMode BitmapBlendingMode { get; init; }
/// <summary>
/// Gets a value indicating whether full opacity handling is required for the associated content.
/// </summary>
public bool? RequiresFullOpacityHandling { get; init; }
/// <summary>
@ -75,6 +110,7 @@ namespace Avalonia.Media
/// </summary>
/// <param name="visual">The control.</param>
/// <returns>The value.</returns>
[Obsolete("TextRenderingMode is obsolete. Use TextOptions.TextRenderingMode instead.")]
public static TextRenderingMode GetTextRenderingMode(Visual visual)
{
return visual.RenderOptions.TextRenderingMode;
@ -85,6 +121,7 @@ namespace Avalonia.Media
/// </summary>
/// <param name="visual">The control.</param>
/// <param name="value">The value.</param>
[Obsolete("TextRenderingMode is obsolete. Use TextOptions.TextRenderingMode instead.")]
public static void SetTextRenderingMode(Visual visual, TextRenderingMode value)
{
visual.RenderOptions = visual.RenderOptions with { TextRenderingMode = value };
@ -126,11 +163,15 @@ namespace Avalonia.Media
edgeMode = other.EdgeMode;
}
#pragma warning disable CS0618
var textRenderingMode = TextRenderingMode;
#pragma warning restore CS0618
if (textRenderingMode == TextRenderingMode.Unspecified)
{
#pragma warning disable CS0618
textRenderingMode = other.TextRenderingMode;
#pragma warning disable CS0618
}
var bitmapBlendingMode = BitmapBlendingMode;

69
src/Avalonia.Base/Media/StreamGeometryContext.cs

@ -10,7 +10,7 @@ namespace Avalonia.Media
/// of <see cref="StreamGeometryContext"/> is obtained by calling
/// <see cref="StreamGeometry.Open"/>.
/// </remarks>
public class StreamGeometryContext : IGeometryContext, IGeometryContext2
public class StreamGeometryContext : IGeometryContext
{
private readonly IStreamGeometryContextImpl _impl;
@ -34,15 +34,13 @@ namespace Avalonia.Media
_impl.SetFillRule(fillRule);
}
/// <inheritdoc/>
public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection)
/// <inheritdoc />
public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection, bool isStroked = true)
{
_impl.ArcTo(point, size, rotationAngle, isLargeArc, sweepDirection);
_impl.ArcTo(point, size, rotationAngle, isLargeArc, sweepDirection, isStroked);
_currentPoint = point;
}
/// <summary>
/// Draws an arc to the specified point using polylines, quadratic or cubic Bezier curves
/// Significantly more precise when drawing elliptic arcs with extreme width:height ratios.
@ -59,34 +57,32 @@ namespace Avalonia.Media
PreciseEllipticArcHelper.ArcTo(this, _currentPoint, point, size, rotationAngle, isLargeArc, sweepDirection);
}
/// <inheritdoc/>
public void BeginFigure(Point startPoint, bool isFilled)
public void BeginFigure(Point startPoint, bool isFilled = true)
{
_impl.BeginFigure(startPoint, isFilled);
_currentPoint = startPoint;
}
/// <inheritdoc/>
public void CubicBezierTo(Point controlPoint1, Point controlPoint2, Point endPoint)
public void CubicBezierTo(Point controlPoint1, Point controlPoint2, Point endPoint, bool isStroked = true)
{
_impl.CubicBezierTo(controlPoint1, controlPoint2, endPoint);
_impl.CubicBezierTo(controlPoint1, controlPoint2, endPoint, isStroked);
_currentPoint = endPoint;
}
/// <inheritdoc/>
public void QuadraticBezierTo(Point controlPoint , Point endPoint)
public void QuadraticBezierTo(Point controlPoint, Point endPoint, bool isStroked = true)
{
_impl.QuadraticBezierTo(controlPoint , endPoint);
_impl.QuadraticBezierTo(controlPoint, endPoint, isStroked);
_currentPoint = endPoint;
}
/// <inheritdoc/>
public void LineTo(Point endPoint)
public void LineTo(Point point, bool isStroked = true)
{
_impl.LineTo(endPoint);
_currentPoint = endPoint;
_impl.LineTo(point, isStroked);
_currentPoint = point;
}
/// <inheritdoc/>
@ -102,46 +98,5 @@ namespace Avalonia.Media
{
_impl.Dispose();
}
/// <inheritdoc/>
public void LineTo(Point point, bool isStroked)
{
if (_impl is IGeometryContext2 context2)
context2.LineTo(point, isStroked);
else
_impl.LineTo(point);
_currentPoint = point;
}
public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection, bool isStroked)
{
if (_impl is IGeometryContext2 context2)
context2.ArcTo(point, size, rotationAngle, isLargeArc, sweepDirection, isStroked);
else
_impl.ArcTo(point, size, rotationAngle, isLargeArc, sweepDirection);
_currentPoint = point;
}
public void CubicBezierTo(Point controlPoint1, Point controlPoint2, Point endPoint, bool isStroked)
{
if (_impl is IGeometryContext2 context2)
context2.CubicBezierTo(controlPoint1, controlPoint2, endPoint, isStroked);
else
_impl.CubicBezierTo(controlPoint1, controlPoint2, endPoint);
_currentPoint = endPoint;
}
public void QuadraticBezierTo(Point controlPoint, Point endPoint, bool isStroked)
{
if (_impl is IGeometryContext2 context2)
context2.QuadraticBezierTo(controlPoint, endPoint, isStroked);
else
_impl.QuadraticBezierTo(controlPoint, endPoint);
_currentPoint = endPoint;
}
}
}

31
src/Avalonia.Base/Media/TextHintingMode.cs

@ -0,0 +1,31 @@
namespace Avalonia.Media
{
/// <summary>
/// Specifies the level of hinting applied to text glyphs during rendering.
/// Text hinting adjusts glyph outlines to improve readability and crispness,
/// especially at small font sizes or low DPI. This enum controls the amount
/// of grid-fitting and outline adjustment performed.
/// </summary>
public enum TextHintingMode : byte
{
/// <summary>
/// Hinting mode is not explicitly specified. The default will be used.
/// </summary>
Unspecified,
/// <summary>
/// No hinting, outlines are scaled only.
/// </summary>
None,
/// <summary>
/// Minimal hinting, preserves glyph shape.
/// </summary>
Light,
/// <summary>
/// Aggressive grid-fitting, maximum crispness at low DPI.
/// </summary>
Strong
}
}

136
src/Avalonia.Base/Media/TextOptions.cs

@ -0,0 +1,136 @@
namespace Avalonia.Media
{
/// <summary>
/// Provides options for controlling text rendering behavior, including rendering mode, hinting mode, and baseline
/// pixel alignment. Used to configure how text appears within visual elements.
/// </summary>
/// <remarks>TextOptions encapsulates settings that influence the clarity, sharpness, and positioning of
/// rendered text. These options can be applied to visual elements to customize text appearance for different
/// display scenarios, such as optimizing for readability at small font sizes or ensuring pixel-perfect alignment.
/// The struct supports merging with other instances to inherit unspecified values, and exposes attached properties
/// for use with visuals.</remarks>
public readonly record struct TextOptions
{
/// <summary>
/// Gets the text rendering mode used to control how text glyphs are rendered.
/// </summary>
public TextRenderingMode TextRenderingMode { get; init; }
/// <summary>
/// Gets the text rendering hinting mode used to optimize the display of text.
/// </summary>
/// <remarks>The hinting mode determines how text is rendered to improve clarity and readability,
/// especially at small font sizes. Changing this value may affect the appearance of text depending on the
/// rendering engine and display device.</remarks>
public TextHintingMode TextHintingMode { get; init; }
/// <summary>
/// Gets a value indicating whether the text baseline should be aligned to the pixel grid.
/// </summary>
/// <remarks>
/// When enabled, the vertical position of the text baseline is snapped to whole pixel boundaries.
/// This ensures consistent sharpness and reduces blurriness caused by fractional positioning,
/// particularly at small font sizes or low DPI settings.
/// </remarks>
public BaselinePixelAlignment BaselinePixelAlignment { get; init; }
/// <summary>
/// Merges this instance with <paramref name="other"/> using inheritance semantics: unspecified values on this
/// instance are taken from <paramref name="other"/>.
/// </summary>
public TextOptions MergeWith(TextOptions other)
{
var textRenderingMode = TextRenderingMode;
if (textRenderingMode == TextRenderingMode.Unspecified)
{
textRenderingMode = other.TextRenderingMode;
}
var textHintingMode = TextHintingMode;
if (textHintingMode == TextHintingMode.Unspecified)
{
textHintingMode = other.TextHintingMode;
}
var baselinePixelAlignment = BaselinePixelAlignment;
if (baselinePixelAlignment == BaselinePixelAlignment.Unspecified)
{
baselinePixelAlignment = other.BaselinePixelAlignment;
}
return new TextOptions
{
TextRenderingMode = textRenderingMode,
TextHintingMode = textHintingMode,
BaselinePixelAlignment = baselinePixelAlignment
};
}
/// <summary>
/// Gets the TextOptions attached value for a visual.
/// </summary>
public static TextOptions GetTextOptions(Visual visual)
{
return visual.TextOptions;
}
/// <summary>
/// Sets the TextOptions attached value for a visual.
/// </summary>
public static void SetTextOptions(Visual visual, TextOptions value)
{
visual.TextOptions = value;
}
/// <summary>
/// Gets the TextRenderingMode attached property for a visual.
/// </summary>
public static TextRenderingMode GetTextRenderingMode(Visual visual)
{
return visual.TextOptions.TextRenderingMode;
}
/// <summary>
/// Sets the TextRenderingMode attached property for a visual.
/// </summary>
public static void SetTextRenderingMode(Visual visual, TextRenderingMode value)
{
visual.TextOptions = visual.TextOptions with { TextRenderingMode = value };
}
/// <summary>
/// Gets the TextHintingMode attached property for a visual.
/// </summary>
public static TextHintingMode GetTextHintingMode(Visual visual)
{
return visual.TextOptions.TextHintingMode;
}
/// <summary>
/// Sets the TextHintingMode attached property for a visual.
/// </summary>
public static void SetTextHintingMode(Visual visual, TextHintingMode value)
{
visual.TextOptions = visual.TextOptions with { TextHintingMode = value };
}
/// <summary>
/// Gets the BaselinePixelAlignment attached property for a visual.
/// </summary>
public static BaselinePixelAlignment GetBaselinePixelAlignment(Visual visual)
{
return visual.TextOptions.BaselinePixelAlignment;
}
/// <summary>
/// Sets the BaselinePixelAlignment attached property for a visual.
/// </summary>
public static void SetBaselinePixelAlignment(Visual visual, BaselinePixelAlignment value)
{
visual.TextOptions = visual.TextOptions with { BaselinePixelAlignment = value };
}
}
}

27
src/Avalonia.Base/Media/TextRenderingMode.cs

@ -1,11 +1,36 @@
namespace Avalonia.Media
{
/// <summary>
/// Specifies how text glyphs are rendered in Avalonia.
/// Controls the smoothing and antialiasing applied during text rasterization.
/// </summary>
public enum TextRenderingMode : byte
{
/// <summary>
/// Rendering mode is not explicitly specified.
/// The system or platform default will be used.
/// </summary>
Unspecified,
SubpixelAntialias,
/// <summary>
/// Glyphs are rendered with subpixel antialiasing.
/// This provides higher apparent resolution on LCD screens
/// by using the individual red, green, and blue subpixels.
/// </summary>
SubpixelAntialias,
/// <summary>
/// Glyphs are rendered with standard grayscale antialiasing.
/// This smooths edges without using subpixel information,
/// preserving shape fidelity across different display types.
/// </summary>
Antialias,
/// <summary>
/// Glyphs are rendered without antialiasing.
/// This produces sharp, aliased edges and may be useful
/// for pixel-art aesthetics or low-DPI environments.
/// </summary>
Alias
}
}

11
src/Avalonia.Base/Platform/IDrawingContextImpl.cs

@ -195,6 +195,17 @@ namespace Avalonia.Platform
/// </summary>
void PopRenderOptions();
/// <summary>
/// Pushes text options for the drawing context.
/// </summary>
/// <param name="textOptions">The text options.</param>
void PushTextOptions(TextOptions textOptions);
/// <summary>
/// Pops the latest text options.
/// </summary>
void PopTextOptions();
/// <summary>
/// Attempts to get an optional feature from the drawing context implementation.
/// </summary>

18
src/Avalonia.Base/Platform/IGeometryContext.cs

@ -18,7 +18,8 @@ namespace Avalonia.Platform
/// <param name="sweepDirection">
/// A value that indicates whether the arc is drawn in the Clockwise or Counterclockwise direction.
/// </param>
void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection);
/// <param name="isStroked">Whether the segment is stroked</param>
void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection, bool isStroked = true);
/// <summary>
/// Begins a new figure.
@ -33,20 +34,23 @@ namespace Avalonia.Platform
/// <param name="controlPoint1">The first control point used to specify the shape of the curve.</param>
/// <param name="controlPoint2">The second control point used to specify the shape of the curve.</param>
/// <param name="endPoint">The destination point for the end of the curve.</param>
void CubicBezierTo(Point controlPoint1, Point controlPoint2, Point endPoint);
/// <param name="isStroked">Whether the segment is stroked</param>
void CubicBezierTo(Point controlPoint1, Point controlPoint2, Point endPoint, bool isStroked = true);
/// <summary>
/// Draws a quadratic Bezier curve to the specified point
/// </summary>
/// <param name="controlPoint ">Control point</param>
/// <param name="endPoint">DestinationPoint</param>
void QuadraticBezierTo(Point controlPoint , Point endPoint);
/// <param name="controlPoint">The control point used to specify the shape of the curve.</param>
/// <param name="endPoint">The destination point for the end of the curve.</param>
/// <param name="isStroked">Whether the segment is stroked</param>
void QuadraticBezierTo(Point controlPoint, Point endPoint, bool isStroked = true);
/// <summary>
/// Draws a line to the specified point.
/// </summary>
/// <param name="endPoint">The destination point.</param>
void LineTo(Point endPoint);
/// <param name="point">The destination point.</param>
/// <param name="isStroked">Whether the segment is stroked</param>
void LineTo(Point point, bool isStroked = true);
/// <summary>
/// Ends the figure started by <see cref="BeginFigure(Point, bool)"/>.

46
src/Avalonia.Base/Platform/IGeometryContext2.cs

@ -1,46 +0,0 @@
using Avalonia.Media;
namespace Avalonia.Platform
{
// TODO12 combine with IGeometryContext
public interface IGeometryContext2 : IGeometryContext
{
/// <summary>
/// Draws a line to the specified point.
/// </summary>
/// <param name="point">The destination point.</param>
/// <param name="isStroked">Whether the segment is stroked</param>
void LineTo(Point point, bool isStroked);
/// <summary>
/// Draws an arc to the specified point.
/// </summary>
/// <param name="point">The destination point.</param>
/// <param name="size">The radii of an oval whose perimeter is used to draw the angle.</param>
/// <param name="rotationAngle">The rotation angle (in radians) of the oval that specifies the curve.</param>
/// <param name="isLargeArc">true to draw the arc greater than 180 degrees; otherwise, false.</param>
/// <param name="sweepDirection">
/// A value that indicates whether the arc is drawn in the Clockwise or Counterclockwise direction.
/// </param>
/// <param name="isStroked">Whether the segment is stroked</param>
void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection, bool isStroked);
/// <summary>
/// Draws a Bezier curve to the specified point.
/// </summary>
/// <param name="controlPoint1">The first control point used to specify the shape of the curve.</param>
/// <param name="controlPoint2">The second control point used to specify the shape of the curve.</param>
/// <param name="endPoint">The destination point for the end of the curve.</param>
/// <param name="isStroked">Whether the segment is stroked</param>
void CubicBezierTo(Point controlPoint1, Point controlPoint2, Point endPoint, bool isStroked);
/// <summary>
/// Draws a quadratic Bezier curve to the specified point
/// </summary>
/// <param name="controlPoint ">Control point</param>
/// <param name="endPoint">DestinationPoint</param>
/// <param name="isStroked">Whether the segment is stroked</param>
void QuadraticBezierTo(Point controlPoint, Point endPoint, bool isStroked);
}
}

29
src/Avalonia.Base/Platform/PathGeometryContext.cs

@ -21,7 +21,7 @@ namespace Avalonia.Visuals.Platform
}
/// <inheritdoc/>
public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection)
public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection, bool isStroked = true)
{
var arcSegment = new ArcSegment
{
@ -29,7 +29,8 @@ namespace Avalonia.Visuals.Platform
RotationAngle = rotationAngle,
IsLargeArc = isLargeArc,
SweepDirection = sweepDirection,
Point = point
Point = point,
IsStroked = isStroked
};
CurrentFigureSegments().Add(arcSegment);
@ -47,27 +48,39 @@ namespace Avalonia.Visuals.Platform
}
/// <inheritdoc/>
public void CubicBezierTo(Point controlPoint1, Point controlPoint2, Point endPoint)
public void CubicBezierTo(Point controlPoint1, Point controlPoint2, Point endPoint, bool isStroked = true)
{
var bezierSegment = new BezierSegment { Point1 = controlPoint1, Point2 = controlPoint2, Point3 = endPoint };
var bezierSegment = new BezierSegment
{
Point1 = controlPoint1,
Point2 = controlPoint2,
Point3 = endPoint,
IsStroked = isStroked
};
CurrentFigureSegments().Add(bezierSegment);
}
/// <inheritdoc/>
public void QuadraticBezierTo(Point controlPoint , Point endPoint)
public void QuadraticBezierTo(Point controlPoint , Point endPoint, bool isStroked = true)
{
var quadraticBezierSegment = new QuadraticBezierSegment { Point1 = controlPoint , Point2 = endPoint };
var quadraticBezierSegment = new QuadraticBezierSegment
{
Point1 = controlPoint,
Point2 = endPoint,
IsStroked = isStroked
};
CurrentFigureSegments().Add(quadraticBezierSegment);
}
/// <inheritdoc/>
public void LineTo(Point endPoint)
public void LineTo(Point point, bool isStroked = true)
{
var lineSegment = new LineSegment
{
Point = endPoint
Point = point,
IsStroked = isStroked
};
CurrentFigureSegments().Add(lineSegment);

15
src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataNodes.cs

@ -232,3 +232,18 @@ class RenderDataRenderOptionsNode : RenderDataPushNode
context.Context.PopRenderOptions();
}
}
class RenderDataTextOptionsNode : RenderDataPushNode
{
public TextOptions TextOptions { get; set; }
public override void Push(ref RenderDataNodeRenderContext context)
{
context.Context.PushTextOptions(TextOptions);
}
public override void Pop(ref RenderDataNodeRenderContext context)
{
context.Context.PopTextOptions();
}
}

6
src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataDrawingContext.cs

@ -264,6 +264,10 @@ internal class RenderDataDrawingContext : DrawingContext
RenderOptions = renderOptions
});
protected override void PushTextOptionsCore(TextOptions textOptions) => Push(new RenderDataTextOptionsNode()
{
TextOptions = textOptions
});
protected override void PopClipCore() => Pop<RenderDataClipNode>();
@ -277,6 +281,8 @@ internal class RenderDataDrawingContext : DrawingContext
protected override void PopRenderOptionsCore() => Pop<RenderDataRenderOptionsNode>();
protected override void PopTextOptionsCore() => Pop<RenderDataTextOptionsNode>();
internal override void DrawBitmap(IRef<IBitmapImpl>? source, double opacity, Rect sourceRect, Rect destRect)
{
if (source == null || sourceRect.IsEmpty() || destRect.IsEmpty())

4
src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.PendingCommands.cs

@ -22,6 +22,7 @@ internal partial class CompositorDrawingContextProxy
PushOpacityMask,
PushGeometryClip,
PushRenderOptions,
PushTextOptions,
PushEffect
}
@ -43,6 +44,7 @@ internal partial class CompositorDrawingContextProxy
[FieldOffset(0)] public Matrix Transform;
[FieldOffset(0)] public RenderOptions RenderOptions;
[FieldOffset(0)] public TextOptions TextOptions;
// PushClip/PushOpacityMask
[FieldOffset(0)] public bool IsRoundRect;
@ -148,6 +150,8 @@ internal partial class CompositorDrawingContextProxy
}
else if (cmd.Type == PendingCommandType.PushRenderOptions)
_impl.PushRenderOptions(cmd.DataUnion.RenderOptions);
else if (cmd.Type == PendingCommandType.PushTextOptions)
_impl.PushTextOptions(cmd.DataUnion.TextOptions);
else
Debug.Assert(false);
}

21
src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs

@ -260,6 +260,18 @@ internal partial class CompositorDrawingContextProxy : IDrawingContextImpl,
});
}
public void PushTextOptions(TextOptions textOptions)
{
AddCommand(new()
{
Type = PendingCommandType.PushTextOptions,
DataUnion =
{
TextOptions = textOptions
}
});
}
public void PopRenderOptions()
{
if (!TryDiscardOrFlush(PendingCommandType.PushRenderOptions))
@ -269,6 +281,15 @@ internal partial class CompositorDrawingContextProxy : IDrawingContextImpl,
}
}
public void PopTextOptions()
{
if (!TryDiscardOrFlush(PendingCommandType.PushTextOptions))
{
_impl.PopTextOptions();
RestoreTransform();
}
}
public object? GetFeature(Type t)
{
Flush();

10
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs

@ -62,6 +62,12 @@ namespace Avalonia.Rendering.Composition.Server
if (applyRenderOptions)
canvas.PushRenderOptions(RenderOptions);
var applyTextOptions = TextOptions != default;
if (applyTextOptions)
canvas.PushTextOptions(TextOptions);
var needPopEffect = PushEffect(canvas);
if (Opacity != 1)
@ -88,7 +94,9 @@ namespace Avalonia.Rendering.Composition.Server
if (needPopEffect)
canvas.PopEffect();
if(applyRenderOptions)
if (applyTextOptions)
canvas.PopTextOptions();
if (applyRenderOptions)
canvas.PopRenderOptions();
}

1
src/Avalonia.Base/Rendering/ImmediateRenderer.cs

@ -50,6 +50,7 @@ internal class ImmediateRenderer
transform = Matrix.CreateTranslation(bounds.Position);
}
using (visual.TextOptions != default ? context.PushTextOptions(visual.TextOptions) : default(DrawingContext.PushedState?))
using (visual.RenderOptions != default ? context.PushRenderOptions(visual.RenderOptions) : default(DrawingContext.PushedState?))
using (context.PushTransform(transform))
using (visual.HasMirrorTransform ? context.PushTransform(new Matrix(-1.0, 0.0, 0.0, 1.0, visual.Bounds.Width, 0)) : default(DrawingContext.PushedState?))

245
src/Avalonia.Base/Utilities/StringTokenizer.cs

@ -1,245 +0,0 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using static System.Char;
namespace Avalonia.Utilities
{
// TODO12: Remove this struct in 12.0 (breaking change)
[Obsolete("This type has been superseded by SpanStringTokenizer.")]
#if !BUILDTASK
public
#endif
record struct StringTokenizer : IDisposable
{
private const char DefaultSeparatorChar = ',';
private readonly string _s;
private readonly int _length;
private readonly char _separator;
private readonly string? _exceptionMessage;
private readonly IFormatProvider _formatProvider;
private int _index;
private int _tokenIndex;
private int _tokenLength;
public StringTokenizer(string s, IFormatProvider formatProvider, string? exceptionMessage = null)
: this(s, GetSeparatorFromFormatProvider(formatProvider), exceptionMessage)
{
_formatProvider = formatProvider;
}
public StringTokenizer(string s, char separator = DefaultSeparatorChar, string? exceptionMessage = null)
{
_s = s ?? throw new ArgumentNullException(nameof(s));
_length = s?.Length ?? 0;
_separator = separator;
_exceptionMessage = exceptionMessage;
_formatProvider = CultureInfo.InvariantCulture;
_index = 0;
_tokenIndex = -1;
_tokenLength = 0;
while (_index < _length && IsWhiteSpace(_s, _index))
{
_index++;
}
}
public string? CurrentToken => _tokenIndex < 0 ? null : _s.Substring(_tokenIndex, _tokenLength);
public ReadOnlySpan<char> CurrentTokenSpan => _tokenIndex < 0 ? ReadOnlySpan<char>.Empty : _s.AsSpan().Slice(_tokenIndex, _tokenLength);
public void Dispose()
{
if (_index != _length)
{
throw GetFormatException();
}
}
public bool TryReadInt32(out Int32 result, char? separator = null)
{
if (TryReadSpan(out var stringResult, separator) &&
SpanHelpers.TryParseInt(stringResult, NumberStyles.Integer, _formatProvider, out result))
{
return true;
}
else
{
result = default;
return false;
}
}
public int ReadInt32(char? separator = null)
{
if (!TryReadInt32(out var result, separator))
{
throw GetFormatException();
}
return result;
}
public bool TryReadDouble(out double result, char? separator = null)
{
if (TryReadSpan(out var stringResult, separator) &&
SpanHelpers.TryParseDouble(stringResult, NumberStyles.Float, _formatProvider, out result))
{
return true;
}
else
{
result = default;
return false;
}
}
public double ReadDouble(char? separator = null)
{
if (!TryReadDouble(out var result, separator))
{
throw GetFormatException();
}
return result;
}
public bool TryReadString([NotNull] out string result, char? separator = null)
{
var success = TryReadToken(separator ?? _separator);
result = CurrentTokenSpan.ToString();
return success;
}
public string ReadString(char? separator = null)
{
if (!TryReadString(out var result, separator))
{
throw GetFormatException();
}
return result;
}
public bool TryReadSpan(out ReadOnlySpan<char> result, char? separator = null)
{
var success = TryReadToken(separator ?? _separator);
result = CurrentTokenSpan;
return success;
}
public ReadOnlySpan<char> ReadSpan(char? separator = null)
{
if (!TryReadSpan(out var result, separator))
{
throw GetFormatException();
}
return result;
}
private bool TryReadToken(char separator)
{
_tokenIndex = -1;
if (_index >= _length)
{
return false;
}
var c = _s[_index];
var index = _index;
var length = 0;
while (_index < _length)
{
c = _s[_index];
if (IsWhiteSpace(c) || c == separator)
{
break;
}
_index++;
length++;
}
SkipToNextToken(separator);
_tokenIndex = index;
_tokenLength = length;
if (_tokenLength < 1)
{
throw GetFormatException();
}
return true;
}
private void SkipToNextToken(char separator)
{
if (_index < _length)
{
var c = _s[_index];
if (c != separator && !IsWhiteSpace(c))
{
throw GetFormatException();
}
var length = 0;
while (_index < _length)
{
c = _s[_index];
if (c == separator)
{
length++;
_index++;
if (length > 1)
{
throw GetFormatException();
}
}
else
{
if (!IsWhiteSpace(c))
{
break;
}
_index++;
}
}
if (length > 0 && _index >= _length)
{
throw GetFormatException();
}
}
}
private FormatException GetFormatException() =>
_exceptionMessage != null ? new FormatException(_exceptionMessage) : new FormatException();
private static char GetSeparatorFromFormatProvider(IFormatProvider provider)
{
var c = DefaultSeparatorChar;
var formatInfo = NumberFormatInfo.GetInstance(provider);
if (formatInfo.NumberDecimalSeparator.Length > 0 && c == formatInfo.NumberDecimalSeparator[0])
{
c = ';';
}
return c;
}
}
}

3
src/Avalonia.Base/Visual.Composition.cs

@ -148,6 +148,7 @@ public partial class Visual
comp.Effect = Effect?.ToImmutable();
comp.RenderOptions = RenderOptions;
comp.TextOptions = TextOptions;
var renderTransform = Matrix.Identity;
@ -163,4 +164,4 @@ public partial class Visual
comp.TransformMatrix = renderTransform;
}
}
}

64
src/Avalonia.Base/Visual.cs

@ -1,5 +1,3 @@
#nullable enable
using System;
@ -39,7 +37,7 @@ namespace Avalonia
/// </summary>
public static readonly DirectProperty<Visual, Rect> BoundsProperty =
AvaloniaProperty.RegisterDirect<Visual, Rect>(nameof(Bounds), o => o.Bounds);
/// <summary>
/// Defines the <see cref="ClipToBounds"/> property.
/// </summary>
@ -51,7 +49,7 @@ namespace Avalonia
/// </summary>
public static readonly StyledProperty<Geometry?> ClipProperty =
AvaloniaProperty.Register<Visual, Geometry?>(nameof(Clip));
/// <summary>
/// Defines the <see cref="IsVisible"/> property.
/// </summary>
@ -69,7 +67,7 @@ namespace Avalonia
/// </summary>
public static readonly StyledProperty<IBrush?> OpacityMaskProperty =
AvaloniaProperty.Register<Visual, IBrush?>(nameof(OpacityMask));
/// <summary>
/// Defines the <see cref="Effect"/> property.
/// </summary>
@ -113,7 +111,7 @@ namespace Avalonia
/// </summary>
public static readonly StyledProperty<int> ZIndexProperty =
AvaloniaProperty.Register<Visual, int>(nameof(ZIndex));
private static readonly WeakEvent<IAffectsRender, EventArgs> InvalidatedWeakEvent =
WeakEvent.Register<IAffectsRender>(
(s, h) => s.Invalidated += h,
@ -124,6 +122,8 @@ namespace Avalonia
private Visual? _visualParent;
private bool _hasMirrorTransform;
private TargetWeakEventSubscriber<Visual, EventArgs>? _affectsRenderWeakSubscriber;
private RenderOptions _renderOptions;
private TextOptions _textOptions;
/// <summary>
/// Initializes static members of the <see cref="Visual"/> class.
@ -201,7 +201,7 @@ namespace Avalonia
/// Gets a value indicating whether this control and all its parents are visible.
/// </summary>
public bool IsEffectivelyVisible { get; private set; } = true;
/// <summary>
/// Updates the <see cref="IsEffectivelyVisible"/> property based on the parent's
/// <see cref="IsEffectivelyVisible"/>.
@ -218,7 +218,7 @@ namespace Avalonia
// PERF-SENSITIVE: This is called on entire hierarchy and using foreach or LINQ
// will cause extra allocations and overhead.
var children = VisualChildren;
// ReSharper disable once ForCanBeConvertedToForeach
@ -255,7 +255,7 @@ namespace Avalonia
get { return GetValue(OpacityMaskProperty); }
set { SetValue(OpacityMaskProperty, value); }
}
/// <summary>
/// Gets or sets the effect of the control.
/// </summary>
@ -269,8 +269,8 @@ namespace Avalonia
/// <summary>
/// Gets or sets a value indicating whether to apply mirror transform on this control.
/// </summary>
public bool HasMirrorTransform
{
public bool HasMirrorTransform
{
get { return _hasMirrorTransform; }
protected set { SetAndRaise(HasMirrorTransformProperty, ref _hasMirrorTransform, value); }
}
@ -326,7 +326,25 @@ namespace Avalonia
/// </summary>
protected internal IRenderRoot? VisualRoot => _visualRoot;
internal RenderOptions RenderOptions { get; set; }
internal RenderOptions RenderOptions
{
get => _renderOptions;
set
{
_renderOptions = value;
InvalidateVisual();
}
}
internal TextOptions TextOptions
{
get => _textOptions;
set
{
_textOptions = value;
InvalidateVisual();
}
}
internal bool HasNonUniformZIndexChildren { get; private set; }
@ -413,8 +431,8 @@ namespace Avalonia
sender.InvalidateVisual();
}
});
var invalidateAndSubscribeObserver = new AnonymousObserver<AvaloniaPropertyChangedEventArgs>(
static e =>
{
@ -466,7 +484,7 @@ namespace Avalonia
if (change.Property == IsVisibleProperty)
{
UpdateIsEffectivelyVisible(VisualParent?.IsEffectivelyVisible ?? true);
}
}
else if (change.Property == FlowDirectionProperty)
{
InvalidateMirrorTransform();
@ -477,7 +495,7 @@ namespace Avalonia
}
}
}
protected override void LogicalChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
base.LogicalChildrenCollectionChanged(sender, e);
@ -515,12 +533,12 @@ namespace Avalonia
OnAttachedToVisualTree(e);
AttachedToVisualTree?.Invoke(this, e);
InvalidateVisual();
_visualRoot.Renderer.RecalculateChildren(_visualParent);
if (ZIndex != 0)
_visualParent.HasNonUniformZIndexChildren = true;
var visualChildren = VisualChildren;
var visualChildrenCount = visualChildren.Count;
@ -617,7 +635,7 @@ namespace Avalonia
{
newTransform.Changed += sender.RenderTransformChanged;
}
sender.InvalidateVisual();
}
}
@ -651,7 +669,7 @@ namespace Avalonia
var parent = sender?.VisualParent;
if (sender?.ZIndex != 0 && parent is Visual parentVisual)
parentVisual.HasNonUniformZIndexChildren = true;
sender?.InvalidateVisual();
parent?.VisualRoot?.Renderer.RecalculateChildren(parent);
}
@ -721,7 +739,7 @@ namespace Avalonia
break;
}
}
private static void SetVisualParent(IList children, Visual? parent)
{
var count = children.Count;
@ -729,7 +747,7 @@ namespace Avalonia
for (var i = 0; i < count; i++)
{
var visual = (Visual) children[i]!;
visual.SetVisualParent(parent);
}
}

1
src/Avalonia.Base/composition-schema.xml

@ -34,6 +34,7 @@
<Property Name="OpacityMaskBrush" ClientName="OpacityMaskBrushTransportField" Type="Avalonia.Media.IBrush?" Private="true" />
<Property Name="Effect" Type="Avalonia.Media.IImmutableEffect?" Internal="true" />
<Property Name="RenderOptions" Type="Avalonia.Media.RenderOptions" />
<Property Name="TextOptions" Type="Avalonia.Media.TextOptions" />
<Property Name="ShouldExtendDirtyRect" Type="bool" Internal="true" />
</Object>
<Object Name="CompositionContainerVisual" Inherits="CompositionVisual"/>

4
src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj

@ -6,6 +6,7 @@
<BuildOutputTargetFolder>tools</BuildOutputTargetFolder>
<DefineConstants>$(DefineConstants);BUILDTASK;XAMLX_CECIL_INTERNAL;XAMLX_INTERNAL</DefineConstants>
<CopyLocalLockFileAssemblies Condition="$(TargetFramework) == 'netstandard2.0'">true</CopyLocalLockFileAssemblies>
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
<NoWarn>$(NoWarn);NU1605;CS8632</NoWarn>
<DebugType>embedded</DebugType>
<IncludeSymbols>false</IncludeSymbols>
@ -61,9 +62,6 @@
<Compile Include="../Avalonia.Base/Utilities/IdentifierParser.cs">
<Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
</Compile>
<Compile Include="../Avalonia.Base/Utilities/StringTokenizer.cs">
<Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
</Compile>
<Compile Include="../Avalonia.Base/Utilities/MathUtilities.cs">
<Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
</Compile>

2
src/Avalonia.Controls/Automation/AutomationProperties.cs

@ -211,7 +211,7 @@ namespace Avalonia.Automation
/// Defines the AutomationProperties.LiveSetting attached property.
/// </summary>
/// <remarks>
/// This property currently has no effect.
/// This property affects the default value for <see cref="AutomationPeer.GetLiveSetting"/> and controls whether live region changed events are emitted.
/// </remarks>
public static readonly AttachedProperty<AutomationLiveSetting> LiveSettingProperty =
AvaloniaProperty.RegisterAttached<StyledElement, AutomationLiveSetting>(

9
src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs

@ -47,6 +47,7 @@ namespace Avalonia.Automation.Peers
Table,
TitleBar,
Separator,
Expander,
}
public enum AutomationLandmarkType
@ -475,6 +476,12 @@ namespace Avalonia.Automation.Peers
/// <returns>true if a context menu is present for the element; otherwise false.</returns>
public bool ShowContextMenu() => ShowContextMenuCore();
/// <summary>
/// Gets the current live setting that is associated with this this automation peer.
/// </summary>
/// <returns>The live setting to use for automation.</returns>
public AutomationLiveSetting GetLiveSetting() => GetLiveSettingCore();
/// <summary>
/// Tries to get a provider of the specified type from the peer.
/// </summary>
@ -536,6 +543,7 @@ namespace Avalonia.Automation.Peers
AutomationControlType.SplitButton => "split button",
AutomationControlType.HeaderItem => "header item",
AutomationControlType.TitleBar => "title bar",
AutomationControlType.Expander => "group",
AutomationControlType.None => (GetLandmarkType()?.ToString() ?? controlType.ToString()).ToLowerInvariant(),
_ => controlType.ToString().ToLowerInvariant(),
};
@ -563,6 +571,7 @@ namespace Avalonia.Automation.Peers
protected virtual bool IsOffscreenCore() => false;
protected abstract void SetFocusCore();
protected abstract bool ShowContextMenuCore();
protected virtual AutomationLiveSetting GetLiveSettingCore() => AutomationLiveSetting.Off;
protected virtual AutomationControlType GetControlTypeOverrideCore()
{

2
src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs

@ -185,6 +185,8 @@ namespace Avalonia.Automation.Peers
return false;
}
protected override AutomationLiveSetting GetLiveSettingCore() => AutomationProperties.GetLiveSetting(Owner);
protected internal override bool TrySetParent(AutomationPeer? parent)
{
_parent = parent;

47
src/Avalonia.Controls/Automation/Peers/ExpanderAutomationPeer.cs

@ -0,0 +1,47 @@
using Avalonia.Automation;
using Avalonia.Automation.Peers;
using Avalonia.Automation.Provider;
namespace Avalonia.Controls.Automation.Peers
{
public class ExpanderAutomationPeer : ControlAutomationPeer,
IExpandCollapseProvider
{
public ExpanderAutomationPeer(Control owner)
: base(owner)
{
owner.PropertyChanged += OwnerPropertyChanged;
}
public new Expander Owner => (Expander)base.Owner;
public ExpandCollapseState ExpandCollapseState => ToState(Owner.IsExpanded);
public bool ShowsMenu => false;
public void Collapse() => Owner.IsExpanded = false;
public void Expand() => Owner.IsExpanded = true;
protected override AutomationControlType GetAutomationControlTypeCore()
{
return AutomationControlType.Expander;
}
private void OwnerPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == Expander.IsExpandedProperty)
{
RaisePropertyChangedEvent(
ExpandCollapsePatternIdentifiers.ExpandCollapseStateProperty,
ToState((bool)e.OldValue!),
ToState((bool)e.NewValue!));
}
}
private static ExpandCollapseState ToState(bool value)
{
return value ? ExpandCollapseState.Expanded : ExpandCollapseState.Collapsed;
}
protected override bool IsContentElementCore() => true;
protected override bool IsControlElementCore() => true;
}
}

10
src/Avalonia.Controls/Automation/Peers/TextBlockAutomationPeer.cs

@ -7,6 +7,16 @@ namespace Avalonia.Automation.Peers
public TextBlockAutomationPeer(TextBlock owner)
: base(owner)
{
Owner.PropertyChanged += (a, e) =>
{
if (e.Property == TextBlock.TextProperty)
{
RaisePropertyChangedEvent(
AutomationElementIdentifiers.NameProperty,
e.OldValue,
e.NewValue);
}
};
}
public new TextBlock Owner => (TextBlock)base.Owner;

2
src/Avalonia.Controls/Automation/Peers/TitleBarAutomationPeer.cs

@ -10,7 +10,7 @@ internal class TitleBarAutomationPeer : ControlAutomationPeer
{
}
protected override bool IsContentElementCore() => true;
protected override bool IsContentElementCore() => false;
protected override string GetClassNameCore()
{

26
src/Avalonia.Controls/Automation/Peers/ToggleButtonAutomationPeer.cs

@ -1,4 +1,5 @@
using Avalonia.Automation.Provider;
using Avalonia.Controls.Automation;
using Avalonia.Controls.Primitives;
namespace Avalonia.Automation.Peers
@ -8,19 +9,28 @@ namespace Avalonia.Automation.Peers
public ToggleButtonAutomationPeer(ToggleButton owner)
: base(owner)
{
Owner.PropertyChanged += (a, e) =>
{
if (e.Property == ToggleButton.IsCheckedProperty)
{
RaisePropertyChangedEvent(
TogglePatternIdentifiers.ToggleStateProperty,
ToState((bool?)e.OldValue),
ToState((bool?)e.NewValue));
}
};
}
public new ToggleButton Owner => (ToggleButton)base.Owner;
ToggleState IToggleProvider.ToggleState
private ToggleState ToState(bool? value) => value switch
{
get => Owner.IsChecked switch
{
true => ToggleState.On,
false => ToggleState.Off,
null => ToggleState.Indeterminate,
};
}
true => ToggleState.On,
false => ToggleState.Off,
null => ToggleState.Indeterminate,
};
ToggleState IToggleProvider.ToggleState => ToState(Owner.IsChecked);
void IToggleProvider.Toggle()
{

15
src/Avalonia.Controls/Automation/TogglePatternIdentifiers.cs

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

16
src/Avalonia.Controls/Design.cs

@ -127,22 +127,6 @@ namespace Avalonia.Controls
/// </summary>
public static readonly AttachedProperty<Control?> PreviewWithProperty = AvaloniaProperty
.RegisterAttached<AvaloniaObject, Control?>("PreviewWith", typeof (Design));
/// <summary>
/// Sets a preview template for the specified <see cref="AvaloniaObject"/> at design-time.
/// </summary>
/// <remarks>
/// This method allows you to specify a substitute control to be rendered in the previewer
/// for a given object.
/// </remarks>
/// <param name="target">The target object.</param>
/// <param name="control">The preview control.</param>
// TODO12: Remove this overload in Avalonia 12
[Obsolete("Use SetPreviewWith(AvaloniaObject, ITemplate<Control>) overload instead. Use <Template></Template> from XAML")]
public static void SetPreviewWith(AvaloniaObject target, Control? control)
{
s_previewWith[target] = control is not null ? new FuncTemplate<Control>(() => control) : null;
}
/// <summary>
/// Sets a preview template for the specified <see cref="AvaloniaObject"/> at design-time.

3
src/Avalonia.Controls/Documents/Inline.cs

@ -10,11 +10,10 @@ namespace Avalonia.Controls.Documents
/// </summary>
public abstract class Inline : TextElement
{
// TODO12: change the field type to an AttachedProperty for consistency (breaking change)
/// <summary>
/// AvaloniaProperty for <see cref="TextDecorations" /> property.
/// </summary>
public static readonly StyledProperty<TextDecorationCollection?> TextDecorationsProperty =
public static readonly AttachedProperty<TextDecorationCollection?> TextDecorationsProperty =
AvaloniaProperty.RegisterAttached<Inline, Inline, TextDecorationCollection?>(
nameof(TextDecorations),
inherits: true);

7
src/Avalonia.Controls/Expander.cs

@ -1,6 +1,8 @@
using System;
using System.Threading;
using Avalonia.Animation;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Automation.Peers;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
@ -274,6 +276,11 @@ namespace Avalonia.Controls
}
}
protected override AutomationPeer OnCreateAutomationPeer()
{
return new ExpanderAutomationPeer(this);
}
/// <summary>
/// Updates the visual state of the control by applying latest PseudoClasses.
/// </summary>

29
src/Avalonia.Controls/Platform/Screen.cs

@ -1,8 +1,5 @@
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Avalonia.Diagnostics;
using Avalonia.Metadata;
using Avalonia.Utilities;
namespace Avalonia.Platform
@ -45,7 +42,7 @@ namespace Avalonia.Platform
/// <summary>
/// Represents a single display screen.
/// </summary>
public class Screen : IEquatable<Screen>
public abstract class Screen : IEquatable<Screen>
{
/// <summary>
/// Gets the device name associated with a display.
@ -96,22 +93,6 @@ namespace Avalonia.Platform
[Obsolete("Use the IsPrimary property instead.", true), EditorBrowsable(EditorBrowsableState.Never)]
public bool Primary => IsPrimary;
/// <summary>
/// Initializes a new instance of the <see cref="Screen"/> class.
/// </summary>
/// <param name="scaling">The scaling factor applied to the screen by the operating system.</param>
/// <param name="bounds">The overall pixel-size of the screen.</param>
/// <param name="workingArea">The actual working-area pixel-size of the screen.</param>
/// <param name="isPrimary">Whether the screen is the primary one.</param>
[Unstable(ObsoletionMessages.MayBeRemovedInAvalonia12)]
public Screen(double scaling, PixelRect bounds, PixelRect workingArea, bool isPrimary)
{
Scaling = scaling;
Bounds = bounds;
WorkingArea = workingArea;
IsPrimary = isPrimary;
}
private protected Screen() { }
/// <summary>
@ -123,19 +104,15 @@ namespace Avalonia.Platform
/// </returns>
public virtual IPlatformHandle? TryGetPlatformHandle() => null;
// TODO12: make abstract
/// <inheritdoc />
public override int GetHashCode()
=> RuntimeHelpers.GetHashCode(this);
public abstract override int GetHashCode();
/// <inheritdoc />
public override bool Equals(object? obj)
=> obj is Screen other && Equals(other);
// TODO12: make abstract
/// <inheritdoc/>
public virtual bool Equals(Screen? other)
=> ReferenceEquals(this, other);
public abstract bool Equals(Screen? other);
public static bool operator ==(Screen? left, Screen? right)
{

38
src/Avalonia.Controls/Presenters/ItemsPresenter.cs

@ -45,35 +45,35 @@ namespace Avalonia.Controls.Presenters
/// Gets the owner <see cref="ItemsControl"/>.
/// </summary>
internal ItemsControl? ItemsControl { get; private set; }
private bool CanHorizontallyScroll
=> _logicalScrollable?.CanHorizontallyScroll ?? false;
bool ILogicalScrollable.CanHorizontallyScroll
bool ILogicalScrollable.CanHorizontallyScroll
{
get => _logicalScrollable?.CanHorizontallyScroll ?? false;
set
{
if (_logicalScrollable is not null)
_logicalScrollable.CanHorizontallyScroll = value;
}
get => CanHorizontallyScroll;
set => _logicalScrollable?.CanHorizontallyScroll = value;
}
bool ILogicalScrollable.CanVerticallyScroll
bool IScrollable.CanHorizontallyScroll
=> CanHorizontallyScroll;
private bool CanVerticallyScroll
=> _logicalScrollable?.CanVerticallyScroll ?? false;
bool ILogicalScrollable.CanVerticallyScroll
{
get => _logicalScrollable?.CanVerticallyScroll ?? false;
set
{
if (_logicalScrollable is not null)
_logicalScrollable.CanVerticallyScroll = value;
}
get => CanVerticallyScroll;
set => _logicalScrollable?.CanVerticallyScroll = value;
}
bool IScrollable.CanVerticallyScroll
=> CanVerticallyScroll;
Vector IScrollable.Offset
{
get => _logicalScrollable?.Offset ?? default;
set
{
if (_logicalScrollable is not null)
_logicalScrollable.Offset = value;
}
set => _logicalScrollable?.Offset = value;
}
bool ILogicalScrollable.IsLogicalScrollEnabled => _logicalScrollable?.IsLogicalScrollEnabled ?? false;

4
src/Avalonia.Controls/Primitives/ILogicalScrollable.cs

@ -19,12 +19,12 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Gets or sets a value indicating whether the content can be scrolled horizontally.
/// </summary>
bool CanHorizontallyScroll { get; set; }
new bool CanHorizontallyScroll { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the content can be scrolled horizontally.
/// </summary>
bool CanVerticallyScroll { get; set; }
new bool CanVerticallyScroll { get; set; }
/// <summary>
/// Gets a value indicating whether logical scrolling is enabled on the control.

6
src/Avalonia.Controls/Primitives/TextSearch.cs

@ -24,22 +24,20 @@ namespace Avalonia.Controls.Primitives
public static readonly AttachedProperty<BindingBase?> TextBindingProperty
= AvaloniaProperty.RegisterAttached<Interactive, BindingBase?>("TextBinding", typeof(TextSearch));
// TODO12: Control should be Interactive to match the property definition.
/// <summary>
/// Sets the value of the <see cref="TextProperty"/> attached property to a given <see cref="Control"/>.
/// </summary>
/// <param name="control">The control.</param>
/// <param name="text">The search text to set.</param>
public static void SetText(Control control, string? text)
public static void SetText(Interactive control, string? text)
=> control.SetValue(TextProperty, text);
// TODO12: Control should be Interactive to match the property definition.
/// <summary>
/// Gets the value of the <see cref="TextProperty"/> attached property from a given <see cref="Control"/>.
/// </summary>
/// <param name="control">The control.</param>
/// <returns>The search text.</returns>
public static string? GetText(Control control)
public static string? GetText(Interactive control)
=> control.GetValue(TextProperty);
/// <summary>

6
src/Avalonia.Controls/ScrollViewer.cs

@ -14,7 +14,7 @@ namespace Avalonia.Controls
/// </summary>
[TemplatePart("PART_HorizontalScrollBar", typeof(ScrollBar))]
[TemplatePart("PART_VerticalScrollBar", typeof(ScrollBar))]
public class ScrollViewer : ContentControl, IScrollable, IScrollAnchorProvider, IInternalScroller
public class ScrollViewer : ContentControl, IScrollable, IScrollAnchorProvider
{
/// <summary>
/// Defines the <see cref="BringIntoViewOnFocusChange "/> property.
@ -284,7 +284,7 @@ namespace Avalonia.Controls
get => HorizontalScrollBarVisibility != ScrollBarVisibility.Disabled;
}
bool IInternalScroller.CanHorizontallyScroll => CanHorizontallyScroll;
bool IScrollable.CanHorizontallyScroll => CanHorizontallyScroll;
/// <summary>
/// Gets a value indicating whether the viewer can scroll vertically.
@ -294,7 +294,7 @@ namespace Avalonia.Controls
get => VerticalScrollBarVisibility != ScrollBarVisibility.Disabled;
}
bool IInternalScroller.CanVerticallyScroll => CanVerticallyScroll;
bool IScrollable.CanVerticallyScroll => CanVerticallyScroll;
/// <inheritdoc/>
public Control? CurrentAnchor => (Presenter as IScrollAnchorProvider)?.CurrentAnchor;

18
src/Avalonia.Controls/VirtualizingCarouselPanel.cs

@ -29,9 +29,23 @@ namespace Avalonia.Controls
private int _transitionFromIndex = -1;
private CancellationTokenSource? _transition;
private EventHandler? _scrollInvalidated;
private bool _canHorizontallyScroll;
private bool _canVerticallyScroll;
bool ILogicalScrollable.CanHorizontallyScroll { get; set; }
bool ILogicalScrollable.CanVerticallyScroll { get; set; }
bool ILogicalScrollable.CanHorizontallyScroll
{
get => _canHorizontallyScroll;
set => _canHorizontallyScroll = value;
}
bool ILogicalScrollable.CanVerticallyScroll
{
get => _canVerticallyScroll;
set => _canVerticallyScroll = value;
}
bool IScrollable.CanHorizontallyScroll => _canHorizontallyScroll;
bool IScrollable.CanVerticallyScroll => _canVerticallyScroll;
bool ILogicalScrollable.IsLogicalScrollEnabled => true;
Size ILogicalScrollable.ScrollSize => new(1, 1);
Size ILogicalScrollable.PageScrollSize => new(1, 1);

16
src/Avalonia.Native/AvnAutomationPeer.cs

@ -13,6 +13,14 @@ namespace Avalonia.Native
{
internal class AvnAutomationPeer : NativeCallbackBase, IAvnAutomationPeer
{
private static readonly Dictionary<AutomationProperty, AvnAutomationProperty> s_propertyMap = new()
{
{ AutomationElementIdentifiers.BoundingRectangleProperty, AvnAutomationProperty.AutomationPeer_BoundingRectangle },
{ AutomationElementIdentifiers.ClassNameProperty, AvnAutomationProperty.AutomationPeer_ClassName },
{ AutomationElementIdentifiers.NameProperty, AvnAutomationProperty.AutomationPeer_Name },
{ RangeValuePatternIdentifiers.ValueProperty, AvnAutomationProperty.RangeValueProvider_Value },
};
private static readonly ConditionalWeakTable<AutomationPeer, AvnAutomationPeer> s_wrappers = new();
private readonly AutomationPeer _inner;
@ -20,6 +28,7 @@ namespace Avalonia.Native
{
_inner = inner;
_inner.ChildrenChanged += (_, _) => Node?.ChildrenChanged();
_inner.PropertyChanged += OnPeerPropertyChanged;
if (inner is IRootProvider root)
root.FocusChanged += (_, _) => Node?.FocusChanged();
}
@ -41,6 +50,7 @@ namespace Avalonia.Native
public int HeadingLevel => _inner.GetHeadingLevel();
public IAvnAutomationPeer? Parent => Wrap(_inner.GetParent());
public IAvnAutomationPeer? VisualRoot => Wrap(_inner.GetVisualRoot());
public AvnLiveSetting LiveSetting => (AvnLiveSetting)_inner.GetLiveSetting();
public int HasKeyboardFocus() => _inner.HasKeyboardFocus().AsComBool();
public int IsContentElement() => _inner.IsContentElement().AsComBool();
@ -186,6 +196,12 @@ namespace Avalonia.Native
}
private int IsProvider<T>() => (_inner.GetProvider<T>() is not null).AsComBool();
private void OnPeerPropertyChanged(object? sender, AutomationPropertyChangedEventArgs e)
{
if (s_propertyMap.TryGetValue(e.Property, out var property))
Node?.PropertyChanged(property);
}
}
internal class AvnAutomationPeerArray : NativeCallbackBase, IAvnAutomationPeerArray

10
src/Avalonia.Native/avn.idl

@ -652,6 +652,7 @@ enum AvnAutomationControlType
AutomationTable,
AutomationTitleBar,
AutomationSeparator,
AutomationExpander,
}
enum AvnLandmarkType
@ -688,6 +689,13 @@ enum AvnPointerDeviceType
Pen,
}
enum AvnLiveSetting
{
LiveSettingOff,
LiveSettingPolite,
LiveSettingAssertive,
}
[uuid(809c652e-7396-11d2-9771-00a0c9b4d50c)]
interface IAvaloniaNativeFactory : IUnknown
{
@ -1323,6 +1331,8 @@ interface IAvnAutomationPeer : IUnknown
AvnLandmarkType GetLandmarkType();
int GetHeadingLevel();
AvnLiveSetting GetLiveSetting();
}
[uuid(b00af5da-78af-4b33-bfff-4ce13a6239a9)]

3
src/Avalonia.Themes.Fluent/Controls/AutoCompleteBox.xaml

@ -66,7 +66,8 @@
BorderThickness="0"
Background="Transparent"
ItemTemplate="{TemplateBinding ItemTemplate}"
Margin="{DynamicResource AutoCompleteListPadding}" />
Margin="{DynamicResource AutoCompleteListPadding}"
ScrollViewer.IsScrollChainingEnabled="False" />
</Border>
</Popup>
</Grid>

1
src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml

@ -172,6 +172,7 @@
HorizontalAlignment="Stretch"
CornerRadius="{DynamicResource OverlayCornerRadius}">
<ScrollViewer HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
IsScrollChainingEnabled="False"
VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}"
IsDeferredScrollingEnabled="{TemplateBinding (ScrollViewer.IsDeferredScrollingEnabled)}">
<ItemsPresenter Name="PART_ItemsPresenter"

6
src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml

@ -79,6 +79,8 @@
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="MinWidth" Value="{DynamicResource DatePickerThemeMinWidth}" />
<Setter Property="MaxWidth" Value="{DynamicResource DatePickerThemeMaxWidth}" />
<Setter Property="Template">
<ControlTemplate>
<DataValidationErrors>
@ -91,8 +93,8 @@
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
IsEnabled="{TemplateBinding IsEnabled}"
MinWidth="{DynamicResource DatePickerThemeMinWidth}"
MaxWidth="{DynamicResource DatePickerThemeMaxWidth}"
MinWidth="{TemplateBinding MinWidth}"
MaxWidth="{TemplateBinding MaxWidth}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
TemplatedControl.IsTemplateFocusTarget="True">

1
src/Avalonia.Themes.Fluent/Controls/MenuScrollViewer.xaml

@ -21,6 +21,7 @@
<ControlTheme x:Key="FluentMenuScrollViewer" TargetType="ScrollViewer">
<Setter Property="Background" Value="Transparent" />
<Setter Property="IsScrollChainingEnabled" Value="False" />
<Setter Property="Template">
<ControlTemplate>
<DockPanel>

1
src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml

@ -7,6 +7,7 @@
<Setter Property="FontFamily" Value="{DynamicResource ContentControlThemeFontFamily}" />
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="FontStyle" Value="Normal" />
<Setter Property="Inline.TextDecorations" Value="" />
<Setter Property="Template">
<ControlTemplate>
<LayoutTransformControl LayoutTransform="{TemplateBinding Transform}">

1
src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml

@ -9,6 +9,7 @@
<Setter Property="FontFamily" Value="{DynamicResource ContentControlThemeFontFamily}" />
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="FontStyle" Value="Normal" />
<Setter Property="Inline.TextDecorations" Value="" />
<Setter Property="Template">
<ControlTemplate>
<LayoutTransformControl LayoutTransform="{TemplateBinding Transform}">

30
src/Avalonia.Themes.Fluent/Controls/ScrollBar.xaml

@ -137,7 +137,8 @@
Grid.Row="0"
Focusable="False"
MinWidth="{DynamicResource ScrollBarSize}"
Height="{DynamicResource ScrollBarSize}">
Height="{DynamicResource ScrollBarSize}"
AutomationProperties.Name="Line up">
<PathIcon Data="M 19.091797 14.970703 L 10 5.888672 L 0.908203 14.970703 L 0.029297 14.091797 L 10 4.111328 L 19.970703 14.091797 Z"
Width="{DynamicResource ScrollBarButtonArrowIconFontSize}"
Height="{DynamicResource ScrollBarButtonArrowIconFontSize}"/>
@ -155,19 +156,22 @@
<RepeatButton Name="PART_PageUpButton"
Classes="largeIncrease"
Theme="{StaticResource FluentScrollBarPageButton}"
Focusable="False" />
Focusable="False"
AutomationProperties.Name="Page up"/>
</Track.DecreaseButton>
<Track.IncreaseButton>
<RepeatButton Name="PART_PageDownButton"
Classes="largeIncrease"
Theme="{StaticResource FluentScrollBarPageButton}"
Focusable="False" />
Focusable="False"
AutomationProperties.Name="Page down"/>
</Track.IncreaseButton>
<Thumb Theme="{StaticResource FluentScrollBarThumb}"
Width="{DynamicResource ScrollBarSize}"
MinHeight="{DynamicResource ScrollBarSize}"
RenderTransform="{DynamicResource VerticalSmallScrollThumbScaleTransform}"
RenderTransformOrigin="100%,50%" />
RenderTransformOrigin="100%,50%"
AutomationProperties.Name="Position"/>
</Track>
<RepeatButton Name="PART_LineDownButton"
@ -176,7 +180,8 @@
Grid.Row="2"
Focusable="False"
MinWidth="{DynamicResource ScrollBarSize}"
Height="{DynamicResource ScrollBarSize}">
Height="{DynamicResource ScrollBarSize}"
AutomationProperties.Name="Line down">
<PathIcon Data="M 18.935547 4.560547 L 19.814453 5.439453 L 10 15.253906 L 0.185547 5.439453 L 1.064453 4.560547 L 10 13.496094 Z"
Width="{DynamicResource ScrollBarButtonArrowIconFontSize}"
Height="{DynamicResource ScrollBarButtonArrowIconFontSize}"/>
@ -215,7 +220,8 @@
Grid.Column="0"
Focusable="False"
MinHeight="{DynamicResource ScrollBarSize}"
Width="{DynamicResource ScrollBarSize}">
Width="{DynamicResource ScrollBarSize}"
AutomationProperties.Name="Column left">
<PathIcon Data="M 14.091797 19.970703 L 4.111328 10 L 14.091797 0.029297 L 14.970703 0.908203 L 5.888672 10 L 14.970703 19.091797 Z"
Width="{DynamicResource ScrollBarButtonArrowIconFontSize}"
Height="{DynamicResource ScrollBarButtonArrowIconFontSize}"/>
@ -232,19 +238,22 @@
<RepeatButton Name="PART_PageUpButton"
Classes="largeIncrease"
Theme="{StaticResource FluentScrollBarPageButton}"
Focusable="False" />
Focusable="False"
AutomationProperties.Name="Page left"/>
</Track.DecreaseButton>
<Track.IncreaseButton>
<RepeatButton Name="PART_PageDownButton"
Classes="largeIncrease"
Theme="{StaticResource FluentScrollBarPageButton}"
Focusable="False" />
Focusable="False"
AutomationProperties.Name="Page right"/>
</Track.IncreaseButton>
<Thumb Theme="{StaticResource FluentScrollBarThumb}"
Height="{DynamicResource ScrollBarSize}"
MinWidth="{DynamicResource ScrollBarSize}"
RenderTransform="{DynamicResource HorizontalSmallScrollThumbScaleTransform}"
RenderTransformOrigin="50%,100%" />
RenderTransformOrigin="50%,100%"
AutomationProperties.Name="Position"/>
</Track>
<RepeatButton Name="PART_LineDownButton"
@ -253,7 +262,8 @@
Grid.Column="2"
Focusable="False"
MinHeight="{DynamicResource ScrollBarSize}"
Width="{DynamicResource ScrollBarSize}">
Width="{DynamicResource ScrollBarSize}"
AutomationProperties.Name="Column right">
<PathIcon Data="M 5.029297 19.091797 L 14.111328 10 L 5.029297 0.908203 L 5.908203 0.029297 L 15.888672 10 L 5.908203 19.970703 Z"
Width="{DynamicResource ScrollBarButtonArrowIconFontSize}"
Height="{DynamicResource ScrollBarButtonArrowIconFontSize}"/>

6
src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml

@ -78,6 +78,8 @@
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="MinWidth" Value="{DynamicResource TimePickerThemeMinWidth}" />
<Setter Property="MaxWidth" Value="{DynamicResource TimePickerThemeMaxWidth}" />
<Setter Property="Template">
<ControlTemplate>
<DataValidationErrors>
@ -90,8 +92,8 @@
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
IsEnabled="{TemplateBinding IsEnabled}"
MinWidth="{DynamicResource TimePickerThemeMinWidth}"
MaxWidth="{DynamicResource TimePickerThemeMaxWidth}"
MinWidth="{TemplateBinding MinWidth}"
MaxWidth="{TemplateBinding MaxWidth}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">

1
src/Avalonia.Themes.Simple/Controls/AutoCompleteBox.xaml

@ -38,6 +38,7 @@
BorderThickness="0"
Foreground="{TemplateBinding Foreground}"
ItemTemplate="{TemplateBinding ItemTemplate}"
ScrollViewer.IsScrollChainingEnabled="False"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto" />
</Border>

3
src/Avalonia.Themes.Simple/Controls/ComboBox.xaml

@ -87,7 +87,8 @@
BorderThickness="1">
<ScrollViewer HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}"
IsDeferredScrollingEnabled="{TemplateBinding (ScrollViewer.IsDeferredScrollingEnabled)}">
IsDeferredScrollingEnabled="{TemplateBinding (ScrollViewer.IsDeferredScrollingEnabled)}"
ScrollViewer.IsScrollChainingEnabled="False">
<ItemsPresenter Name="PART_ItemsPresenter"
ItemsPanel="{TemplateBinding ItemsPanel}" />
</ScrollViewer>

3
src/Avalonia.Themes.Simple/Controls/OverlayPopupHost.xaml

@ -6,6 +6,9 @@
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundBrush}" />
<Setter Property="FontSize" Value="{DynamicResource FontSizeNormal}" />
<Setter Property="FontFamily" Value="{DynamicResource ContentControlThemeFontFamily}" />
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="FontStyle" Value="Normal" />
<Setter Property="Inline.TextDecorations" Value="" />
<Setter Property="Template">
<ControlTemplate>
<!-- Do not forget to update Templated_Control_With_Popup_In_Template_Should_Set_TemplatedParent test -->

3
src/Avalonia.Themes.Simple/Controls/PopupRoot.xaml

@ -8,6 +8,9 @@
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundBrush}" />
<Setter Property="FontSize" Value="{DynamicResource FontSizeNormal}" />
<Setter Property="FontFamily" Value="{DynamicResource ContentControlThemeFontFamily}" />
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="FontStyle" Value="Normal" />
<Setter Property="Inline.TextDecorations" Value="" />
<Setter Property="Template">
<ControlTemplate>
<LayoutTransformControl LayoutTransform="{TemplateBinding Transform}">

30
src/Avalonia.Themes.Simple/Controls/ScrollBar.xaml

@ -18,7 +18,8 @@
MinWidth="{DynamicResource ScrollBarThickness}"
VerticalAlignment="Center"
Classes="repeat"
Focusable="False">
Focusable="False"
AutomationProperties.Name="Column left">
<Path Data="M 4 0 L 4 8 L 0 4 Z" />
</RepeatButton>
<Track Grid.Row="1"
@ -33,14 +34,17 @@
<Track.DecreaseButton>
<RepeatButton Name="PART_PageUpButton"
Classes="repeattrack"
Focusable="False" />
Focusable="False"
AutomationProperties.Name="Page left"/>
</Track.DecreaseButton>
<Track.IncreaseButton>
<RepeatButton Name="PART_PageDownButton"
Classes="repeattrack"
Focusable="False" />
Focusable="False"
AutomationProperties.Name="Page right"/>
</Track.IncreaseButton>
<Thumb Name="thumb" />
<Thumb Name="thumb"
AutomationProperties.Name="Position"/>
</Track>
<RepeatButton Name="PART_LineDownButton"
Grid.Row="2"
@ -48,7 +52,8 @@
MinWidth="{DynamicResource ScrollBarThickness}"
VerticalAlignment="Center"
Classes="repeat"
Focusable="False">
Focusable="False"
AutomationProperties.Name="Column right">
<Path Data="M 0 0 L 4 4 L 0 8 Z" />
</RepeatButton>
</Grid>
@ -68,7 +73,8 @@
MinHeight="{DynamicResource ScrollBarThickness}"
HorizontalAlignment="Center"
Classes="repeat"
Focusable="False">
Focusable="False"
AutomationProperties.Name="Line up">
<Path Data="M 0 4 L 8 4 L 4 0 Z" />
</RepeatButton>
<Track Grid.Row="1"
@ -84,14 +90,17 @@
<Track.DecreaseButton>
<RepeatButton Name="PART_PageUpButton"
Classes="repeattrack"
Focusable="False" />
Focusable="False"
AutomationProperties.Name="Page up"/>
</Track.DecreaseButton>
<Track.IncreaseButton>
<RepeatButton Name="PART_PageDownButton"
Classes="repeattrack"
Focusable="False" />
Focusable="False"
AutomationProperties.Name="Page down"/>
</Track.IncreaseButton>
<Thumb Name="thumb" />
<Thumb Name="thumb"
AutomationProperties.Name="Position"/>
</Track>
<RepeatButton Name="PART_LineDownButton"
Grid.Row="2"
@ -99,7 +108,8 @@
MinHeight="{DynamicResource ScrollBarThickness}"
HorizontalAlignment="Center"
Classes="repeat"
Focusable="False">
Focusable="False"
AutomationProperties.Name="Line down">
<Path Data="M 0 0 L 4 4 L 8 0 Z" />
</RepeatButton>
</Grid>

1
src/Avalonia.Themes.Simple/Controls/ScrollViewer.xaml

@ -40,6 +40,7 @@
<ControlTheme x:Key="SimpleMenuScrollViewer"
TargetType="ScrollViewer">
<Setter Property="Background" Value="Transparent" />
<Setter Property="IsScrollChainingEnabled" Value="False" />
<Setter Property="Template">
<ControlTemplate>
<DockPanel>

111
src/Browser/Avalonia.Browser/Rendering/RenderWorker.cs

@ -1,10 +1,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.JavaScript;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Browser.Interop;
@ -19,11 +16,14 @@ internal partial class RenderWorker
private static partial void InitializeRenderTargets();
internal static int WorkerThreadId;
// The worker task needs to be rooted otherwise the web worker will exit.
private static Task? s_workerTask;
public static Task InitializeAsync()
{
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var workerTask = JSWebWorkerClone.RunAsync(async () =>
s_workerTask = JSWebWorkerRunAsync(null, async () =>
{
try
{
@ -41,103 +41,18 @@ internal partial class RenderWorker
}
});
workerTask.ContinueWith(_ =>
s_workerTask.ContinueWith(_ =>
{
if (workerTask.IsFaulted)
tcs.TrySetException(workerTask.Exception);
if (s_workerTask.IsFaulted)
tcs.TrySetException(s_workerTask.Exception);
});
return tcs.Task;
}
public static class JSWebWorkerClone
{
private static readonly MethodInfo _setExtLoop;
private static readonly MethodInfo _intallInterop;
[DynamicDependency(DynamicallyAccessedMemberTypes.All, "System.Runtime.InteropServices.JavaScript.JSSynchronizationContext",
"System.Runtime.InteropServices.JavaScript")]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, "System.Runtime.InteropServices.JavaScript.JSHostImplementation",
"System.Runtime.InteropServices.JavaScript")]
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Private runtime API")]
[UnconditionalSuppressMessage("Trimming", "IL2036", Justification = "Private runtime API")]
[UnconditionalSuppressMessage("Trimming", "IL2075", Justification = "Private runtime API")]
[UnconditionalSuppressMessage("Trimming", "IL2111", Justification = "Private runtime API")]
static JSWebWorkerClone()
{
var syncContext = typeof(System.Runtime.InteropServices.JavaScript.JSHost)
.Assembly!.GetType("System.Runtime.InteropServices.JavaScript.JSSynchronizationContext")!;
var hostImpl = typeof(System.Runtime.InteropServices.JavaScript.JSHost)
.Assembly!.GetType("System.Runtime.InteropServices.JavaScript.JSHostImplementation")!;
_setExtLoop = hostImpl.GetMethod("SetHasExternalEventLoop")!;
_intallInterop = syncContext.GetMethod("InstallWebWorkerInterop")!;
}
public static Task RunAsync(Func<Task> run)
{
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var th = new Thread(_ =>
{
_intallInterop.Invoke(null, [false, CancellationToken.None]);
try
{
run().ContinueWith(t =>
{
if (t.IsFaulted)
tcs.TrySetException(t.Exception);
else if (t.IsCanceled)
tcs.TrySetCanceled();
else
tcs.TrySetResult();
});
}
catch(Exception e)
{
tcs.TrySetException(e);
}
})
{
Name = "Manual JS worker"
};
_setExtLoop.Invoke(null, [th]);
#pragma warning disable CA1416
th.Start();
#pragma warning restore CA1416
return tcs.Task;
}
}
// TODO: Use this class instead of JSWebWorkerClone once https://github.com/dotnet/runtime/issues/102010 is fixed
// TODO12: It was fixed in .NET 10
class JSWebWorkerWrapper
{
[DynamicDependency(DynamicallyAccessedMemberTypes.PublicMethods, "System.Runtime.InteropServices.JavaScript.JSWebWorker",
"System.Runtime.InteropServices.JavaScript")]
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Private runtime API")]
[UnconditionalSuppressMessage("Trimming", "IL2036", Justification = "Private runtime API")]
[UnconditionalSuppressMessage("Trimming", "IL2075", Justification = "Private runtime API")]
[UnconditionalSuppressMessage("Trimming", "IL2111", Justification = "Private runtime API")]
static JSWebWorkerWrapper()
{
var type = typeof(System.Runtime.InteropServices.JavaScript.JSHost)
.Assembly!.GetType("System.Runtime.InteropServices.JavaScript.JSWebWorker");
#pragma warning disable IL2075
var m = type!
.GetMethods(BindingFlags.Static | BindingFlags.Public
).First(m => m.Name == "RunAsync"
&& m.ReturnType == typeof(Task)
&& m.GetParameters() is { } parameters
&& parameters.Length == 1
&& parameters[0].ParameterType == typeof(Func<Task>));
#pragma warning restore IL2075
RunAsync = (Func<Func<Task>, Task>) Delegate.CreateDelegate(typeof(Func<Func<Task>, Task>), m);
}
public static Func<Func<Task>, Task> RunAsync { get; set; }
}
// Even though this API is public in the .NET code, it's not part of ref assemblies and is not a stable API.
[UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "RunAsync")]
private static extern Task JSWebWorkerRunAsync(
[UnsafeAccessorType("System.Runtime.InteropServices.JavaScript.JSWebWorker, System.Runtime.InteropServices.JavaScript")] object? instance,
Func<Task> body);
}

2
src/HarfBuzz/Avalonia.HarfBuzz/Avalonia.HarfBuzz.csproj

@ -4,8 +4,6 @@
<IncludeLinuxSkia>true</IncludeLinuxSkia>
<IncludeWasmSkia>true</IncludeWasmSkia>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<!-- No obsolete code usage -->
<WarningsAsErrors>$(WarningsAsErrors);CS0618</WarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />

3
src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj

@ -5,8 +5,7 @@
</PropertyGroup>
<ItemGroup>
<!-- Use lower minor version, as it is supposed to be compatible -->
<PackageReference Include="xunit.core" Version="2.4.0" />
<PackageReference Include="xunit.v3.extensibility.core" Version="3.2.1" />
</ItemGroup>
<ItemGroup>

80
src/Headless/Avalonia.Headless.XUnit/AvaloniaDelayEnumeratedTheoryTestCase.cs

@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Threading;
using Xunit.Sdk;
using Xunit.v3;
namespace Avalonia.Headless.XUnit;
internal sealed class AvaloniaDelayEnumeratedTheoryTestCase
: XunitDelayEnumeratedTheoryTestCase, ISelfExecutingXunitTestCase
{
public AvaloniaDelayEnumeratedTheoryTestCase(
IXunitTestMethod testMethod,
string testCaseDisplayName,
string uniqueID,
bool @explicit,
bool skipTestWithoutData,
Type[]? skipExceptions = null,
string? skipReason = null,
Type? skipType = null,
string? skipUnless = null,
string? skipWhen = null,
Dictionary<string, HashSet<string>>? traits = null,
string? sourceFilePath = null,
int? sourceLineNumber = null,
int? timeout = null)
: base(
testMethod,
testCaseDisplayName,
uniqueID,
@explicit,
skipTestWithoutData,
skipExceptions,
skipReason,
skipType,
skipUnless,
skipWhen,
traits,
sourceFilePath,
sourceLineNumber,
timeout)
{
}
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")]
public AvaloniaDelayEnumeratedTheoryTestCase()
{
}
public async ValueTask<RunSummary> Run(
ExplicitOption explicitOption,
IMessageBus messageBus,
object?[] constructorArguments,
ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource)
{
var tests = await aggregator.RunAsync(CreateTests, []);
// We need to block the XUnit thread to ensure its concurrency throttle is effective.
// See https://github.com/AArnott/Xunit.StaFact/pull/55#issuecomment-826187354 for details.
var runSummary = Task.Run(async () => await AvaloniaTestCaseRunner.Instance.Run(
this,
tests,
messageBus,
aggregator,
cancellationTokenSource,
TestCaseDisplayName,
SkipReason,
explicitOption,
constructorArguments))
.GetAwaiter()
.GetResult();
return runSummary;
}
}

32
src/Headless/Avalonia.Headless.XUnit/AvaloniaFact.cs

@ -1,9 +1,7 @@
using System;
using System.ComponentModel;
using System.Threading;
using System.Runtime.CompilerServices;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
using Xunit.v3;
namespace Avalonia.Headless.XUnit;
@ -12,24 +10,8 @@ namespace Avalonia.Headless.XUnit;
/// such that awaited expressions resume on the test's "main thread".
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
[XunitTestCaseDiscoverer("Avalonia.Headless.XUnit.AvaloniaUIFactDiscoverer", "Avalonia.Headless.XUnit")]
public sealed class AvaloniaFactAttribute : FactAttribute
{
}
[EditorBrowsable(EditorBrowsableState.Never)]
public class AvaloniaUIFactDiscoverer : FactDiscoverer
{
private readonly IMessageSink diagnosticMessageSink;
public AvaloniaUIFactDiscoverer(IMessageSink diagnosticMessageSink)
: base(diagnosticMessageSink)
{
this.diagnosticMessageSink = diagnosticMessageSink;
}
protected override IXunitTestCase CreateTestCase(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute)
{
return new AvaloniaTestCase(diagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), testMethod);
}
}
[XunitTestCaseDiscoverer(typeof(AvaloniaFactDiscoverer))]
public sealed class AvaloniaFactAttribute(
[CallerFilePath] string? sourceFilePath = null,
[CallerLineNumber] int sourceLineNumber = -1)
: FactAttribute(sourceFilePath, sourceLineNumber);

34
src/Headless/Avalonia.Headless.XUnit/AvaloniaFactDiscoverer.cs

@ -0,0 +1,34 @@
using System;
using System.ComponentModel;
using Xunit.Internal;
using Xunit.Sdk;
using Xunit.v3;
namespace Avalonia.Headless.XUnit;
[EditorBrowsable(EditorBrowsableState.Never)]
public class AvaloniaFactDiscoverer : FactDiscoverer
{
protected override IXunitTestCase CreateTestCase(
ITestFrameworkDiscoveryOptions discoveryOptions,
IXunitTestMethod testMethod,
IFactAttribute factAttribute)
{
var details = TestIntrospectionHelper.GetTestCaseDetails(discoveryOptions, testMethod, factAttribute);
return new AvaloniaTestCase(
details.ResolvedTestMethod,
details.TestCaseDisplayName,
details.UniqueID,
details.Explicit,
details.SkipExceptions,
details.SkipReason,
details.SkipType,
details.SkipUnless,
details.SkipWhen,
testMethod.Traits.ToReadWrite(StringComparer.OrdinalIgnoreCase),
sourceFilePath: details.SourceFilePath,
sourceLineNumber: details.SourceLineNumber,
timeout: details.Timeout);
}
}

126
src/Headless/Avalonia.Headless.XUnit/AvaloniaTestAssemblyRunner.cs

@ -1,126 +0,0 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Threading;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Avalonia.Headless.XUnit;
internal class AvaloniaTestAssemblyRunner : XunitTestAssemblyRunner
{
private HeadlessUnitTestSession? _session;
public AvaloniaTestAssemblyRunner(ITestAssembly testAssembly, IEnumerable<IXunitTestCase> testCases,
IMessageSink diagnosticMessageSink, IMessageSink executionMessageSink,
ITestFrameworkExecutionOptions executionOptions) : base(testAssembly, testCases, diagnosticMessageSink,
executionMessageSink, executionOptions)
{
}
protected override void SetupSyncContext(int maxParallelThreads)
{
_session = HeadlessUnitTestSession.GetOrStartForAssembly(
Assembly.Load(new AssemblyName(TestAssembly.Assembly.Name)));
base.SetupSyncContext(1);
}
public override void Dispose()
{
_session?.Dispose();
base.Dispose();
}
protected override Task<RunSummary> RunTestCollectionAsync(
IMessageBus messageBus,
ITestCollection testCollection,
IEnumerable<IXunitTestCase> testCases,
CancellationTokenSource cancellationTokenSource)
{
return new AvaloniaTestCollectionRunner(_session!, testCollection, testCases, DiagnosticMessageSink, messageBus,
TestCaseOrderer, new ExceptionAggregator(Aggregator), cancellationTokenSource).RunAsync();
}
private class AvaloniaTestCollectionRunner : XunitTestCollectionRunner
{
private readonly HeadlessUnitTestSession _session;
public AvaloniaTestCollectionRunner(HeadlessUnitTestSession session,
ITestCollection testCollection, IEnumerable<IXunitTestCase> testCases,
IMessageSink diagnosticMessageSink, IMessageBus messageBus, ITestCaseOrderer testCaseOrderer,
ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource) : base(testCollection,
testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator, cancellationTokenSource)
{
_session = session;
}
protected override Task<RunSummary> RunTestClassAsync(
ITestClass testClass,
IReflectionTypeInfo @class,
IEnumerable<IXunitTestCase> testCases)
{
return new AvaloniaTestClassRunner(_session, testClass, @class, testCases, DiagnosticMessageSink, MessageBus,
TestCaseOrderer, new ExceptionAggregator(Aggregator), CancellationTokenSource,
CollectionFixtureMappings).RunAsync();
}
}
private class AvaloniaTestClassRunner : XunitTestClassRunner
{
private readonly HeadlessUnitTestSession _session;
public AvaloniaTestClassRunner(HeadlessUnitTestSession session, ITestClass testClass,
IReflectionTypeInfo @class,
IEnumerable<IXunitTestCase> testCases, IMessageSink diagnosticMessageSink, IMessageBus messageBus,
ITestCaseOrderer testCaseOrderer, ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource, IDictionary<Type, object> collectionFixtureMappings) :
base(testClass, @class, testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator,
cancellationTokenSource, collectionFixtureMappings)
{
_session = session;
}
protected override Task<RunSummary> RunTestMethodAsync(
ITestMethod testMethod,
IReflectionMethodInfo method,
IEnumerable<IXunitTestCase> testCases,
object[] constructorArguments)
{
return new AvaloniaTestMethodRunner(_session, testMethod, Class, method, testCases, DiagnosticMessageSink,
MessageBus, new ExceptionAggregator(Aggregator), CancellationTokenSource,
constructorArguments).RunAsync();
}
}
private class AvaloniaTestMethodRunner : XunitTestMethodRunner
{
private readonly HeadlessUnitTestSession _session;
private readonly IMessageBus _messageBus;
private readonly ExceptionAggregator _aggregator;
private readonly CancellationTokenSource _cancellationTokenSource;
private readonly object[] _constructorArguments;
public AvaloniaTestMethodRunner(HeadlessUnitTestSession session, ITestMethod testMethod,
IReflectionTypeInfo @class,
IReflectionMethodInfo method, IEnumerable<IXunitTestCase> testCases, IMessageSink diagnosticMessageSink,
IMessageBus messageBus, ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource,
object[] constructorArguments) : base(testMethod, @class, method, testCases, diagnosticMessageSink,
messageBus, aggregator, cancellationTokenSource, constructorArguments)
{
_session = session;
_messageBus = messageBus;
_aggregator = aggregator;
_cancellationTokenSource = cancellationTokenSource;
_constructorArguments = constructorArguments;
}
protected override Task<RunSummary> RunTestCaseAsync(IXunitTestCase testCase)
{
return AvaloniaTestCaseRunner.RunTest(_session, testCase, testCase.DisplayName, testCase.SkipReason,
_constructorArguments, testCase.TestMethodArguments, _messageBus, _aggregator,
_cancellationTokenSource);
}
}
}

63
src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCase.cs

@ -1,20 +1,46 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using Xunit.Abstractions;
using Avalonia.Threading;
using Xunit.Sdk;
using Xunit.v3;
namespace Avalonia.Headless.XUnit;
internal class AvaloniaTestCase : XunitTestCase
internal sealed class AvaloniaTestCase : XunitTestCase, ISelfExecutingXunitTestCase
{
public AvaloniaTestCase(
IMessageSink diagnosticMessageSink,
TestMethodDisplay defaultMethodDisplay,
ITestMethod testMethod,
object?[]? testMethodArguments = null)
: base(diagnosticMessageSink, defaultMethodDisplay, TestMethodDisplayOptions.None, testMethod, testMethodArguments)
IXunitTestMethod testMethod,
string testCaseDisplayName,
string uniqueID,
bool @explicit,
Type[]? skipExceptions = null,
string? skipReason = null,
Type? skipType = null,
string? skipUnless = null,
string? skipWhen = null,
Dictionary<string, HashSet<string>>? traits = null,
object?[]? testMethodArguments = null,
string? sourceFilePath = null,
int? sourceLineNumber = null,
int? timeout = null)
: base(
testMethod,
testCaseDisplayName,
uniqueID,
@explicit,
skipExceptions,
skipReason,
skipType,
skipUnless,
skipWhen,
traits,
testMethodArguments,
sourceFilePath,
sourceLineNumber,
timeout)
{
}
@ -24,23 +50,30 @@ internal class AvaloniaTestCase : XunitTestCase
{
}
public override Task<RunSummary> RunAsync(
IMessageSink diagnosticMessageSink,
public async ValueTask<RunSummary> Run(
ExplicitOption explicitOption,
IMessageBus messageBus,
object[] constructorArguments,
object?[] constructorArguments,
ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource)
{
var session = HeadlessUnitTestSession.GetOrStartForAssembly(Method.ToRuntimeMethod().DeclaringType?.Assembly);
var tests = await aggregator.RunAsync(CreateTests, []);
// We need to block the XUnit thread to ensure its concurrency throttle is effective.
// See https://github.com/AArnott/Xunit.StaFact/pull/55#issuecomment-826187354 for details.
var runSummary =
Task.Run(() => AvaloniaTestCaseRunner.RunTest(session, this, DisplayName, SkipReason, constructorArguments,
TestMethodArguments, messageBus, aggregator, cancellationTokenSource))
var runSummary = Task.Run(async () => await AvaloniaTestCaseRunner.Instance.Run(
this,
tests,
messageBus,
aggregator,
cancellationTokenSource,
TestCaseDisplayName,
SkipReason,
explicitOption,
constructorArguments))
.GetAwaiter()
.GetResult();
return Task.FromResult(runSummary);
return runSummary;
}
}

132
src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCaseRunner.cs

@ -1,103 +1,59 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Threading;
using Xunit.Abstractions;
using Xunit.Sdk;
using Xunit.v3;
namespace Avalonia.Headless.XUnit;
internal class AvaloniaTestCaseRunner : XunitTestCaseRunner
internal sealed class AvaloniaTestCaseRunner
: XunitTestCaseRunnerBase<AvaloniaTestCaseRunnerContext, IXunitTestCase, IXunitTest>
{
private readonly HeadlessUnitTestSession _session;
private readonly Action? _onAfterTestInvoked;
public static AvaloniaTestCaseRunner Instance { get; } = new();
public AvaloniaTestCaseRunner(
HeadlessUnitTestSession session, Action? onAfterTestInvoked,
IXunitTestCase testCase, string displayName, string skipReason, object[] constructorArguments,
object[] testMethodArguments, IMessageBus messageBus, ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource) : base(testCase, displayName, skipReason, constructorArguments,
testMethodArguments, messageBus, aggregator, cancellationTokenSource)
private AvaloniaTestCaseRunner()
{
_session = session;
_onAfterTestInvoked = onAfterTestInvoked;
}
public static Task<RunSummary> RunTest(HeadlessUnitTestSession session,
IXunitTestCase testCase, string displayName, string skipReason, object[] constructorArguments,
object[] testMethodArguments, IMessageBus messageBus, ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource)
public async ValueTask<RunSummary> Run(
IXunitTestCase testCase,
IReadOnlyCollection<IXunitTest> tests,
IMessageBus messageBus,
ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource,
string displayName,
string? skipReason,
ExplicitOption explicitOption,
object?[] constructorArguments)
{
var afterTest = () => Dispatcher.UIThread.RunJobs();
var runner = new AvaloniaTestCaseRunner(session, afterTest, testCase, displayName,
skipReason, constructorArguments, testMethodArguments, messageBus, aggregator, cancellationTokenSource);
return runner.RunAsync();
}
protected override XunitTestRunner CreateTestRunner(ITest test, IMessageBus messageBus, Type testClass,
object[] constructorArguments,
MethodInfo testMethod, object[] testMethodArguments, string skipReason,
IReadOnlyList<BeforeAfterTestAttribute> beforeAfterAttributes,
ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource)
{
return new AvaloniaTestRunner(_session, _onAfterTestInvoked, test, messageBus, testClass, constructorArguments,
testMethod, testMethodArguments, skipReason, beforeAfterAttributes, aggregator, cancellationTokenSource);
}
private class AvaloniaTestRunner : XunitTestRunner
{
private readonly HeadlessUnitTestSession _session;
private readonly Action? _onAfterTestInvoked;
public AvaloniaTestRunner(
HeadlessUnitTestSession session, Action? onAfterTestInvoked,
ITest test, IMessageBus messageBus, Type testClass, object[] constructorArguments, MethodInfo testMethod,
object[] testMethodArguments, string skipReason,
IReadOnlyList<BeforeAfterTestAttribute> beforeAfterAttributes, ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource) : base(test, messageBus, testClass, constructorArguments,
testMethod, testMethodArguments, skipReason, beforeAfterAttributes, aggregator, cancellationTokenSource)
{
_session = session;
_onAfterTestInvoked = onAfterTestInvoked;
}
protected override Task<decimal> InvokeTestMethodAsync(ExceptionAggregator aggregator)
{
return _session.Dispatch(
() => new AvaloniaTestInvoker(_onAfterTestInvoked, Test, MessageBus, TestClass,
ConstructorArguments, TestMethod, TestMethodArguments, BeforeAfterAttributes, aggregator,
CancellationTokenSource).RunAsync(),
CancellationTokenSource.Token);
}
var session = HeadlessUnitTestSession.GetOrStartForAssembly(testCase.TestClass.Class.Assembly);
await using var ctxt = new AvaloniaTestCaseRunnerContext(
testCase,
tests,
messageBus,
aggregator,
cancellationTokenSource,
displayName,
skipReason,
explicitOption,
constructorArguments,
session);
await ctxt.InitializeAsync();
return await Run(ctxt);
}
private class AvaloniaTestInvoker : XunitTestInvoker
{
private readonly Action? _onAfterTestInvoked;
public AvaloniaTestInvoker(
Action? onAfterTestInvoked,
ITest test, IMessageBus messageBus, Type testClass, object[] constructorArguments, MethodInfo testMethod,
object[] testMethodArguments, IReadOnlyList<BeforeAfterTestAttribute> beforeAfterAttributes,
ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource) : base(test, messageBus,
testClass, constructorArguments, testMethod, testMethodArguments, beforeAfterAttributes, aggregator,
cancellationTokenSource)
{
_onAfterTestInvoked = onAfterTestInvoked;
}
protected override async Task AfterTestMethodInvokedAsync()
{
await base.AfterTestMethodInvokedAsync();
// Only here we can execute random code after the test, where exception will be properly handled by the XUnit.
if (_onAfterTestInvoked is not null)
{
Aggregator.Run(_onAfterTestInvoked);
}
}
}
protected override ValueTask<RunSummary> RunTest(
AvaloniaTestCaseRunnerContext ctxt,
IXunitTest test)
=> AvaloniaTestRunner.Instance.Run(
test,
ctxt.MessageBus,
ctxt.ConstructorArguments,
ctxt.ExplicitOption,
ctxt.Aggregator.Clone(),
ctxt.CancellationTokenSource,
ctxt.BeforeAfterTestAttributes,
ctxt.Session);
}

31
src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCaseRunnerContext.cs

@ -0,0 +1,31 @@
using System.Collections.Generic;
using System.Threading;
using Xunit.Sdk;
using Xunit.v3;
namespace Avalonia.Headless.XUnit;
internal sealed class AvaloniaTestCaseRunnerContext(
IXunitTestCase testCase,
IReadOnlyCollection<IXunitTest> tests,
IMessageBus messageBus,
ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource,
string displayName,
string? skipReason,
ExplicitOption explicitOption,
object?[] constructorArguments,
HeadlessUnitTestSession session)
: XunitTestCaseRunnerContext(
testCase,
tests,
messageBus,
aggregator,
cancellationTokenSource,
displayName,
skipReason,
explicitOption,
constructorArguments)
{
public HeadlessUnitTestSession Session { get; } = session;
}

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

Loading…
Cancel
Save