Browse Source

Merge branch 'master' into feature/fix-14437-and-13497

pull/20028/head
Hacker_Delphi 8 months ago
committed by GitHub
parent
commit
496762a5e4
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. 1074
      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/automation.mm
  15. 9
      nukebuild/.editorconfig
  16. 4
      nukebuild/_build.csproj
  17. 2
      packages/Avalonia/Avalonia.props
  18. 2
      samples/TextTestApp/InteractiveLineControl.cs
  19. 11
      src/Avalonia.Base/Controls/IInternalScroller.cs
  20. 10
      src/Avalonia.Base/Controls/Primitives/IScrollable.cs
  21. 164
      src/Avalonia.Base/Data/CompiledBinding.cs
  22. 29
      src/Avalonia.Base/Data/CompiledBindingPath.cs
  23. 4
      src/Avalonia.Base/Input/Navigation/XYFocus.FindElements.cs
  24. 2
      src/Avalonia.Base/Input/Navigation/XYFocus.Impl.cs
  25. 26
      src/Avalonia.Base/Media/BaselinePixelAlignment.cs
  26. 20
      src/Avalonia.Base/Media/DrawingContext.cs
  27. 13
      src/Avalonia.Base/Media/DrawingGroup.cs
  28. 4
      src/Avalonia.Base/Media/PlatformDrawingContext.cs
  29. 45
      src/Avalonia.Base/Media/RenderOptions.cs
  30. 31
      src/Avalonia.Base/Media/TextHintingMode.cs
  31. 136
      src/Avalonia.Base/Media/TextOptions.cs
  32. 27
      src/Avalonia.Base/Media/TextRenderingMode.cs
  33. 11
      src/Avalonia.Base/Platform/IDrawingContextImpl.cs
  34. 15
      src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataNodes.cs
  35. 6
      src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataDrawingContext.cs
  36. 4
      src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.PendingCommands.cs
  37. 21
      src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs
  38. 10
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
  39. 1
      src/Avalonia.Base/Rendering/ImmediateRenderer.cs
  40. 3
      src/Avalonia.Base/Visual.Composition.cs
  41. 24
      src/Avalonia.Base/Visual.cs
  42. 1
      src/Avalonia.Base/composition-schema.xml
  43. 1
      src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
  44. 2
      src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs
  45. 47
      src/Avalonia.Controls/Automation/Peers/ExpanderAutomationPeer.cs
  46. 2
      src/Avalonia.Controls/Automation/Peers/TitleBarAutomationPeer.cs
  47. 26
      src/Avalonia.Controls/Automation/Peers/ToggleButtonAutomationPeer.cs
  48. 15
      src/Avalonia.Controls/Automation/TogglePatternIdentifiers.cs
  49. 7
      src/Avalonia.Controls/Expander.cs
  50. 38
      src/Avalonia.Controls/Presenters/ItemsPresenter.cs
  51. 4
      src/Avalonia.Controls/Primitives/ILogicalScrollable.cs
  52. 6
      src/Avalonia.Controls/ScrollViewer.cs
  53. 18
      src/Avalonia.Controls/VirtualizingCarouselPanel.cs
  54. 1
      src/Avalonia.Native/avn.idl
  55. 3
      src/Avalonia.Themes.Fluent/Controls/AutoCompleteBox.xaml
  56. 1
      src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml
  57. 6
      src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml
  58. 1
      src/Avalonia.Themes.Fluent/Controls/MenuScrollViewer.xaml
  59. 1
      src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml
  60. 1
      src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml
  61. 30
      src/Avalonia.Themes.Fluent/Controls/ScrollBar.xaml
  62. 6
      src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml
  63. 1
      src/Avalonia.Themes.Simple/Controls/AutoCompleteBox.xaml
  64. 3
      src/Avalonia.Themes.Simple/Controls/ComboBox.xaml
  65. 3
      src/Avalonia.Themes.Simple/Controls/OverlayPopupHost.xaml
  66. 3
      src/Avalonia.Themes.Simple/Controls/PopupRoot.xaml
  67. 30
      src/Avalonia.Themes.Simple/Controls/ScrollBar.xaml
  68. 1
      src/Avalonia.Themes.Simple/Controls/ScrollViewer.xaml
  69. 2
      src/HarfBuzz/Avalonia.HarfBuzz/Avalonia.HarfBuzz.csproj
  70. 3
      src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj
  71. 80
      src/Headless/Avalonia.Headless.XUnit/AvaloniaDelayEnumeratedTheoryTestCase.cs
  72. 32
      src/Headless/Avalonia.Headless.XUnit/AvaloniaFact.cs
  73. 34
      src/Headless/Avalonia.Headless.XUnit/AvaloniaFactDiscoverer.cs
  74. 126
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestAssemblyRunner.cs
  75. 63
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCase.cs
  76. 132
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCaseRunner.cs
  77. 31
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestCaseRunnerContext.cs
  78. 36
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs
  79. 27
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs
  80. 16
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkDiscoverer.cs
  81. 19
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkExecutor.cs
  82. 50
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs
  83. 27
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunnerContext.cs
  84. 30
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryAttribute.cs
  85. 92
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryDiscoverer.cs
  86. 31
      src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryTestCase.cs
  87. 14
      src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  88. 10
      src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs
  89. 6
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs
  90. 6
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  91. 6
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs
  92. 1
      src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
  93. 183
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs
  94. 2
      src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs
  95. 2
      src/Skia/Avalonia.Skia/Avalonia.Skia.csproj
  96. 2
      src/Skia/Avalonia.Skia/DrawingContextImpl.Effects.cs
  97. 45
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  98. 92
      src/Skia/Avalonia.Skia/GlyphRunImpl.cs
  99. 157
      src/Skia/Avalonia.Skia/TwoLevelCache.cs
  100. 7
      src/Windows/Avalonia.Win32.Automation/AutomationNode.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>

1074
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/automation.mm

@ -126,6 +126,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.

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>

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()

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; }
}

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;

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;

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>

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?))

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

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

24
src/Avalonia.Base/Visual.cs

@ -1,5 +1,3 @@
#nullable enable
using System;
@ -125,6 +123,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.
@ -327,7 +327,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; }

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"/>

1
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>

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

@ -47,6 +47,7 @@ namespace Avalonia.Automation.Peers
Table,
TitleBar,
Separator,
Expander,
}
public enum AutomationLandmarkType
@ -536,6 +537,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(),
};

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;
}
}

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();
}
}

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>

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/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);

1
src/Avalonia.Native/avn.idl

@ -652,6 +652,7 @@ enum AvnAutomationControlType
AutomationTable,
AutomationTitleBar,
AutomationSeparator,
AutomationExpander,
}
enum AvnLandmarkType

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>

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;
}

36
src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFramework.cs

@ -1,35 +1,13 @@
using System.Collections.Generic;
using System.Reflection;
using Xunit.Abstractions;
using Xunit.Sdk;
using System.Reflection;
using Xunit.v3;
namespace Avalonia.Headless.XUnit;
internal class AvaloniaTestFramework : XunitTestFramework
internal sealed class AvaloniaTestFramework : XunitTestFramework
{
public AvaloniaTestFramework(IMessageSink messageSink) : base(messageSink)
{
}
protected override ITestFrameworkDiscoverer CreateDiscoverer(Assembly assembly)
=> new AvaloniaTestFrameworkDiscoverer(new XunitTestAssembly(assembly, null, assembly.GetName().Version));
protected override ITestFrameworkExecutor CreateExecutor(AssemblyName assemblyName)
=> new Executor(assemblyName, SourceInformationProvider, DiagnosticMessageSink);
private class Executor : XunitTestFrameworkExecutor
{
public Executor(AssemblyName assemblyName, ISourceInformationProvider sourceInformationProvider,
IMessageSink diagnosticMessageSink) : base(assemblyName, sourceInformationProvider,
diagnosticMessageSink)
{
}
protected override async void RunTestCases(IEnumerable<IXunitTestCase> testCases,
IMessageSink executionMessageSink,
ITestFrameworkExecutionOptions executionOptions)
{
using (var assemblyRunner = new AvaloniaTestAssemblyRunner(
TestAssembly, testCases, DiagnosticMessageSink, executionMessageSink,
executionOptions)) await assemblyRunner.RunAsync();
}
}
protected override ITestFrameworkExecutor CreateExecutor(Assembly assembly)
=> new AvaloniaTestFrameworkExecutor(new XunitTestAssembly(assembly, null, assembly.GetName().Version));
}

27
src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkAttribute.cs

@ -1,8 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Xunit.Abstractions;
using Xunit.Sdk;
using Xunit.v3;
namespace Avalonia.Headless.XUnit;
@ -12,27 +9,9 @@ namespace Avalonia.Headless.XUnit;
/// <remarks>
/// It is an alternative to using [AvaloniaFact] or [AvaloniaTheory] attributes on every test method.
/// </remarks>
[TestFrameworkDiscoverer("Avalonia.Headless.XUnit.AvaloniaTestFrameworkTypeDiscoverer", "Avalonia.Headless.XUnit")]
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)]
public sealed class AvaloniaTestFrameworkAttribute : Attribute, ITestFrameworkAttribute
{
}
/// <summary>
/// Discoverer implementation for the Avalonia testing framework.
/// </summary>
public class AvaloniaTestFrameworkTypeDiscoverer : ITestFrameworkTypeDiscoverer
{
/// <summary>
/// Creates instance of <see cref="AvaloniaTestFrameworkTypeDiscoverer"/>.
/// </summary>
public AvaloniaTestFrameworkTypeDiscoverer(IMessageSink _)
{
}
/// <inheritdoc/>
public Type GetTestFrameworkType(IAttributeInfo attribute)
{
return typeof(AvaloniaTestFramework);
}
public Type FrameworkType
=> typeof(AvaloniaTestFramework);
}

16
src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkDiscoverer.cs

@ -0,0 +1,16 @@
using Xunit;
using Xunit.v3;
namespace Avalonia.Headless.XUnit;
internal sealed class AvaloniaTestFrameworkDiscoverer : XunitTestFrameworkDiscoverer
{
public AvaloniaTestFrameworkDiscoverer(
IXunitTestAssembly testAssembly,
IXunitTestCollectionFactory? collectionFactory = null)
: base(testAssembly, collectionFactory)
{
DiscovererTypeCache[typeof(FactAttribute)] = typeof(AvaloniaFactDiscoverer);
DiscovererTypeCache[typeof(TheoryAttribute)] = typeof(AvaloniaTheoryDiscoverer);
}
}

19
src/Headless/Avalonia.Headless.XUnit/AvaloniaTestFrameworkExecutor.cs

@ -0,0 +1,19 @@
using System.Threading.Tasks;
using Xunit.v3;
namespace Avalonia.Headless.XUnit;
internal sealed class AvaloniaTestFrameworkExecutor(IXunitTestAssembly testAssembly)
: XunitTestFrameworkExecutor(testAssembly)
{
private readonly HeadlessUnitTestSession _session = HeadlessUnitTestSession.GetOrStartForAssembly(testAssembly.Assembly);
protected override ITestFrameworkDiscoverer CreateDiscoverer()
=> new AvaloniaTestFrameworkDiscoverer(TestAssembly);
public override async ValueTask DisposeAsync()
{
await _session.DisposeAsync();
await base.DisposeAsync();
}
}

50
src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunner.cs

@ -0,0 +1,50 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Threading;
using Xunit.Sdk;
using Xunit.v3;
namespace Avalonia.Headless.XUnit;
internal sealed class AvaloniaTestRunner : XunitTestRunnerBase<AvaloniaTestRunnerContext, IXunitTest>
{
public static AvaloniaTestRunner Instance { get; } = new();
private AvaloniaTestRunner()
{
}
public async ValueTask<RunSummary> Run(
IXunitTest test,
IMessageBus messageBus,
object?[] constructorArguments,
ExplicitOption explicitOption,
ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource,
IReadOnlyCollection<IBeforeAfterTestAttribute> beforeAfterAttributes,
HeadlessUnitTestSession session)
{
await using var ctxt = new AvaloniaTestRunnerContext(
test,
messageBus,
explicitOption,
aggregator,
cancellationTokenSource,
beforeAfterAttributes,
constructorArguments,
session
);
await ctxt.InitializeAsync();
return await session.Dispatch(
async () =>
{
var dispatcher = Dispatcher.UIThread;
var summary = await Run(ctxt);
dispatcher.RunJobs();
return summary;
},
ctxt.CancellationTokenSource.Token);
}
}

27
src/Headless/Avalonia.Headless.XUnit/AvaloniaTestRunnerContext.cs

@ -0,0 +1,27 @@
using System.Collections.Generic;
using System.Threading;
using Xunit.Sdk;
using Xunit.v3;
namespace Avalonia.Headless.XUnit;
internal sealed class AvaloniaTestRunnerContext(
IXunitTest test,
IMessageBus messageBus,
ExplicitOption explicitOption,
ExceptionAggregator aggregator,
CancellationTokenSource cancellationTokenSource,
IReadOnlyCollection<IBeforeAfterTestAttribute> beforeAfterTestAttributes,
object?[] constructorArguments,
HeadlessUnitTestSession session)
: XunitTestRunnerContext(
test,
messageBus,
explicitOption,
aggregator,
cancellationTokenSource,
beforeAfterTestAttributes,
constructorArguments)
{
public HeadlessUnitTestSession Session { get; } = session;
}

30
src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryAttribute.cs

@ -1,9 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
using Xunit.v3;
namespace Avalonia.Headless.XUnit;
@ -12,26 +9,5 @@ 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.AvaloniaTheoryDiscoverer", "Avalonia.Headless.XUnit")]
public sealed class AvaloniaTheoryAttribute : TheoryAttribute
{
}
[EditorBrowsable(EditorBrowsableState.Never)]
public class AvaloniaTheoryDiscoverer : TheoryDiscoverer
{
public AvaloniaTheoryDiscoverer(IMessageSink diagnosticMessageSink)
: base(diagnosticMessageSink)
{
}
protected override IEnumerable<IXunitTestCase> CreateTestCasesForDataRow(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute, object[] dataRow)
{
yield return new AvaloniaTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), testMethod, dataRow);
}
protected override IEnumerable<IXunitTestCase> CreateTestCasesForTheory(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute)
{
yield return new AvaloniaTheoryTestCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), TestMethodDisplayOptions.None, testMethod);
}
}
[XunitTestCaseDiscoverer(typeof(AvaloniaTheoryDiscoverer))]
public sealed class AvaloniaTheoryAttribute : TheoryAttribute;

92
src/Headless/Avalonia.Headless.XUnit/AvaloniaTheoryDiscoverer.cs

@ -0,0 +1,92 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading.Tasks;
using Xunit;
using Xunit.Internal;
using Xunit.Sdk;
using Xunit.v3;
namespace Avalonia.Headless.XUnit;
[EditorBrowsable(EditorBrowsableState.Never)]
public class AvaloniaTheoryDiscoverer : TheoryDiscoverer
{
protected override ValueTask<IReadOnlyCollection<IXunitTestCase>> CreateTestCasesForDataRow(
ITestFrameworkDiscoveryOptions discoveryOptions,
IXunitTestMethod testMethod,
ITheoryAttribute theoryAttribute,
ITheoryDataRow dataRow,
object?[] testMethodArguments)
{
var details = TestIntrospectionHelper.GetTestCaseDetailsForTheoryDataRow(
discoveryOptions,
testMethod,
theoryAttribute,
dataRow,
testMethodArguments);
var traits = TestIntrospectionHelper.GetTraits(testMethod, dataRow);
var testCase = new AvaloniaTestCase(
details.ResolvedTestMethod,
details.TestCaseDisplayName,
details.UniqueID,
details.Explicit,
details.SkipExceptions,
details.SkipReason,
details.SkipType,
details.SkipUnless,
details.SkipWhen,
traits,
testMethodArguments,
sourceFilePath: details.SourceFilePath,
sourceLineNumber: details.SourceLineNumber,
timeout: details.Timeout);
return ValueTask.FromResult<IReadOnlyCollection<IXunitTestCase>>([testCase]);
}
protected override ValueTask<IReadOnlyCollection<IXunitTestCase>> CreateTestCasesForTheory(
ITestFrameworkDiscoveryOptions discoveryOptions,
IXunitTestMethod testMethod,
ITheoryAttribute theoryAttribute)
{
var details = TestIntrospectionHelper.GetTestCaseDetails(discoveryOptions, testMethod, theoryAttribute);
var testCase =
details.SkipReason is not null && details.SkipUnless is null && details.SkipWhen is null
? 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
)
: (IXunitTestCase)new AvaloniaDelayEnumeratedTheoryTestCase(
details.ResolvedTestMethod,
details.TestCaseDisplayName,
details.UniqueID,
details.Explicit,
theoryAttribute.SkipTestWithoutData,
details.SkipExceptions,
details.SkipReason,
details.SkipType,
details.SkipUnless,
details.SkipWhen,
testMethod.Traits.ToReadWrite(StringComparer.OrdinalIgnoreCase),
sourceFilePath: details.SourceFilePath,
sourceLineNumber: details.SourceLineNumber,
timeout: details.Timeout
);
return ValueTask.FromResult<IReadOnlyCollection<IXunitTestCase>>([testCase]);
}
}

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

@ -1,31 +0,0 @@
using System;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Avalonia.Headless.XUnit;
internal class AvaloniaTheoryTestCase : XunitTheoryTestCase
{
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")]
public AvaloniaTheoryTestCase()
{
}
public AvaloniaTheoryTestCase(IMessageSink diagnosticMessageSink, TestMethodDisplay defaultMethodDisplay, TestMethodDisplayOptions defaultMethodDisplayOptions, ITestMethod testMethod)
: base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod)
{
}
public override Task<RunSummary> RunAsync(IMessageSink diagnosticMessageSink, IMessageBus messageBus, object[] constructorArguments, ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource)
{
var session = HeadlessUnitTestSession.GetOrStartForAssembly(Method.ToRuntimeMethod().DeclaringType?.Assembly);
return AvaloniaTestCaseRunner
.RunTest(session, this, DisplayName, SkipReason, constructorArguments,
TestMethodArguments, messageBus, aggregator, cancellationTokenSource);
}
}

14
src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@ -559,12 +559,22 @@ namespace Avalonia.Headless
public void PushRenderOptions(RenderOptions renderOptions)
{
}
public void PopRenderOptions()
{
}
public void PushTextOptions(TextOptions textOptions)
{
// No-op in headless stub
}
public void PopTextOptions()
{
// No-op in headless stub
}
}

10
src/Headless/Avalonia.Headless/HeadlessUnitTestSession.cs

@ -18,7 +18,7 @@ namespace Avalonia.Headless;
/// All UI tests are supposed to be executed from one of the <see cref="Dispatch"/> methods to keep execution flow on the UI thread.
/// Disposing unit test session stops internal dispatcher loop.
/// </summary>
public sealed class HeadlessUnitTestSession : IDisposable
public sealed class HeadlessUnitTestSession : IDisposable, IAsyncDisposable
{
private static readonly Dictionary<Assembly, HeadlessUnitTestSession> s_session = new();
@ -184,6 +184,14 @@ public sealed class HeadlessUnitTestSession : IDisposable
_cancellationTokenSource.Dispose();
}
public async ValueTask DisposeAsync()
{
await _cancellationTokenSource.CancelAsync().ConfigureAwait(false);
_queue.CompleteAdding();
await _dispatchTask.ConfigureAwait(false);
_cancellationTokenSource.Dispose();
}
/// <summary>
/// Creates instance of <see cref="HeadlessUnitTestSession"/>.
/// </summary>

6
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs

@ -142,7 +142,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
IXamlType? itemsCollectionType = null;
if (context.GetAvaloniaTypes().BindingBase.IsAssignableFrom(parentItemsValue.Type.GetClrType()))
{
if (parentItemsValue.Type.GetClrType().Equals(context.GetAvaloniaTypes().CompiledBindingExtension)
if (parentItemsValue.Type.GetClrType().Equals(context.GetAvaloniaTypes().CompiledBinding)
&& parentItemsValue is XamlMarkupExtensionNode ext && ext.Value is XamlAstConstructableObjectNode parentItemsBinding)
{
var parentItemsDataContext = context.ParentNodes().SkipWhile(n => n != parentObject).OfType<AvaloniaXamlIlDataContextTypeMetadataNode>().FirstOrDefault();
@ -176,7 +176,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
private static AvaloniaXamlIlDataContextTypeMetadataNode ParseDataContext(AstTransformationContext context, XamlAstConstructableObjectNode on, XamlAstConstructableObjectNode obj)
{
var bindingType = context.GetAvaloniaTypes().BindingBase;
if (!bindingType.IsAssignableFrom(obj.Type.GetClrType()) && !obj.Type.GetClrType().Equals(context.GetAvaloniaTypes().ReflectionBindingExtension))
if (!bindingType.IsAssignableFrom(obj.Type.GetClrType()) &&
!(obj.Type.GetClrType().Equals(context.GetAvaloniaTypes().ReflectionBindingExtension) ||
obj.Type.GetClrType().Equals(context.GetAvaloniaTypes().CompiledBindingExtension)))
{
return new AvaloniaXamlIlDataContextTypeMetadataNode(on, obj.Type.GetClrType());
}

6
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs

@ -51,6 +51,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlType ClrPropertyInfo { get; }
public IXamlType IPropertyAccessor { get; }
public IXamlType PropertyInfoAccessorFactory { get; }
public IXamlType CompiledBinding { get; }
public IXamlType CompiledBindingPathBuilder { get; }
public IXamlType CompiledBindingPath { get; }
public IXamlType CompiledBindingExtension { get; }
@ -242,8 +243,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
ClrPropertyInfo = cfg.TypeSystem.GetType("Avalonia.Data.Core.ClrPropertyInfo");
IPropertyAccessor = cfg.TypeSystem.GetType("Avalonia.Data.Core.Plugins.IPropertyAccessor");
PropertyInfoAccessorFactory = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.PropertyInfoAccessorFactory");
CompiledBindingPathBuilder = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.CompiledBindingPathBuilder");
CompiledBindingPath = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.CompiledBindingPath");
CompiledBinding = cfg.TypeSystem.GetType("Avalonia.Data.CompiledBinding");
CompiledBindingPathBuilder = cfg.TypeSystem.GetType("Avalonia.Data.CompiledBindingPathBuilder");
CompiledBindingPath = cfg.TypeSystem.GetType("Avalonia.Data.CompiledBindingPath");
CompiledBindingExtension = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindingExtension");
ResolveByNameExtension = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.ResolveByNameExtension");
DataTemplate = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Templates.DataTemplate");

6
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs

@ -1013,11 +1013,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
var intType = context.Configuration.TypeSystem.GetType("System.Int32");
var types = context.GetAvaloniaTypes();
// We're calling the CompiledBindingPathBuilder(int apiVersion) with an apiVersion
// of 1 to indicate that we don't want TemplatedParent compatibility hacks enabled.
codeGen
.Ldc_I4(1)
.Newobj(types.CompiledBindingPathBuilder.GetConstructor(new() { intType }));
codeGen.Newobj(types.CompiledBindingPathBuilder.GetConstructor());
foreach (var transform in _transformElements)
{

1
src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj

@ -20,7 +20,6 @@
<Compile Include="Extensions.cs" />
<Compile Include="MarkupExtension.cs" />
<Compile Include="MarkupExtensions\CompiledBindingExtension.cs" />
<Compile Include="MarkupExtensions\CompiledBindings\CompiledBindingPath.cs" />
<Compile Include="MarkupExtensions\CompiledBindings\PropertyInfoAccessorFactory.cs" />
<Compile Include="MarkupExtensions\CompiledBindings\TaskStreamPlugin.cs" />
<Compile Include="MarkupExtensions\DynamicResourceExtension.cs" />

183
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs

@ -7,15 +7,13 @@ using Avalonia.Data.Converters;
using Avalonia.Data.Core;
using Avalonia.Data.Core.ExpressionNodes;
using Avalonia.Data.Core.Parsers;
using Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings;
namespace Avalonia.Markup.Xaml.MarkupExtensions
{
public sealed class CompiledBindingExtension : BindingBase
public sealed class CompiledBindingExtension : CompiledBinding
{
public CompiledBindingExtension()
{
Path = new CompiledBindingPath();
}
public CompiledBindingExtension(CompiledBindingPath path)
@ -23,9 +21,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
Path = path;
}
public CompiledBindingExtension ProvideValue(IServiceProvider provider)
public CompiledBinding ProvideValue(IServiceProvider? provider)
{
return new CompiledBindingExtension
return new CompiledBinding
{
Path = Path,
Delay = Delay,
@ -38,184 +36,11 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
Priority = Priority,
StringFormat = StringFormat,
Source = Source,
DefaultAnchor = new WeakReference(provider.GetDefaultAnchor()),
DefaultAnchor = new WeakReference(provider?.GetDefaultAnchor()),
UpdateSourceTrigger = UpdateSourceTrigger,
};
}
/// <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; }
public Type? DataType { 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; }
[ConstructorArgument("path")]
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 override BindingExpressionBase CreateInstance(
AvaloniaObject target,
AvaloniaProperty? targetProperty,
object? anchor)
{
var enableDataValidation = targetProperty?.GetMetadata(target).EnableDataValidation ?? false;
return InstanceCore(target, targetProperty, anchor, enableDataValidation);
}
/// <summary>
/// Hack for TreeDataTemplate to create a binding expression for an item.
/// </summary>
/// <param name="source">The item.</param>
/// <remarks>
/// Ideally we'd do this in a more generic way but didn't have time to refactor
/// ITreeDataTemplate in time for 11.0. We should revisit this in 12.0.
/// </remarks>
// TODO12: Refactor
internal BindingExpression CreateObservableForTreeDataTemplate(object source)
{
if (Source != AvaloniaProperty.UnsetValue)
throw new NotSupportedException("Source bindings are not supported in this context.");
var nodes = new List<ExpressionNode>();
Path.BuildExpression(nodes, out var isRooted);
if (isRooted)
throw new NotSupportedException("Rooted binding paths are not supported in this context.");
return new BindingExpression(
source,
nodes,
FallbackValue,
delay: TimeSpan.FromMilliseconds(Delay),
converter: Converter,
converterParameter: ConverterParameter,
targetNullValue: TargetNullValue);
}
private BindingExpression InstanceCore(
AvaloniaObject target,
AvaloniaProperty? targetProperty,
object? anchor,
bool enableDataValidation)
{
var nodes = new List<ExpressionNode>();
// Build the expression nodes from the binding path.
Path.BuildExpression(nodes, out var 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);
}
}
}

2
src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs

@ -33,7 +33,7 @@ namespace Avalonia.Markup.Xaml
Type Resolve (string qualifiedTypeName);
}
// TODO12: Move to Avalonia.Base
[AttributeUsage(AttributeTargets.Property)]
public sealed class ConstructorArgumentAttribute : Attribute
{

2
src/Skia/Avalonia.Skia/Avalonia.Skia.csproj

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

2
src/Skia/Avalonia.Skia/DrawingContextImpl.Effects.cs

@ -23,7 +23,7 @@ partial class DrawingContextImpl
public void PopEffect()
{
CheckLease();
Canvas.Restore();
RestoreCanvas();
}
SKImageFilter? CreateEffect(IEffect effect)

45
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -27,6 +27,7 @@ namespace Avalonia.Skia
private readonly Stack<(SKMatrix matrix, PaintWrapper paint)> _maskStack = new();
private readonly Stack<double> _opacityStack = new();
private readonly Stack<RenderOptions> _renderOptionsStack = new();
private readonly Stack<TextOptions> _textOptionsStack = new();
private readonly Matrix? _postTransform;
private double _currentOpacity = 1.0f;
private readonly bool _disableSubpixelTextRendering;
@ -223,6 +224,7 @@ namespace Avalonia.Skia
public SKSurface? Surface { get; }
public RenderOptions RenderOptions { get; set; }
public TextOptions TextOptions { get; set; }
private void CheckLease()
{
@ -604,23 +606,32 @@ namespace Avalonia.Skia
{
var glyphRunImpl = (GlyphRunImpl)glyphRun;
var textRenderOptions = RenderOptions;
// Determine effective TextOptions for text rendering. Start with current pushed TextOptions.
var effectiveTextOptions = TextOptions;
// If subpixel rendering is disabled globally, map subpixel modes to grayscale.
if (_disableSubpixelTextRendering)
{
switch (textRenderOptions.TextRenderingMode)
var mode = effectiveTextOptions.TextRenderingMode;
if (mode == TextRenderingMode.SubpixelAntialias ||
(mode == TextRenderingMode.Unspecified && (RenderOptions.EdgeMode == EdgeMode.Antialias || RenderOptions.EdgeMode == EdgeMode.Unspecified)))
{
case TextRenderingMode.Unspecified
when textRenderOptions.EdgeMode == EdgeMode.Antialias || textRenderOptions.EdgeMode == EdgeMode.Unspecified:
case TextRenderingMode.SubpixelAntialias:
{
textRenderOptions = textRenderOptions with { TextRenderingMode = TextRenderingMode.Antialias };
break;
}
effectiveTextOptions = effectiveTextOptions with { TextRenderingMode = TextRenderingMode.Antialias };
}
}
var textBlob = glyphRunImpl.GetTextBlob(textRenderOptions);
var renderOptions = RenderOptions;
// If TextRenderingMode is unspecified in TextOptions, use the one from RenderOptions.
#pragma warning disable CS0618
if (effectiveTextOptions.TextRenderingMode == TextRenderingMode.Unspecified && renderOptions.TextRenderingMode != TextRenderingMode.Unspecified)
{
effectiveTextOptions = effectiveTextOptions with { TextRenderingMode = renderOptions.TextRenderingMode };
}
#pragma warning restore CS0618
var textBlob = glyphRunImpl.GetTextBlob(effectiveTextOptions, RenderOptions);
Canvas.DrawText(textBlob, (float)glyphRun.BaselineOrigin.X,
(float)glyphRun.BaselineOrigin.Y, paintWrapper.Paint);
@ -755,11 +766,25 @@ namespace Avalonia.Skia
RenderOptions = RenderOptions.MergeWith(renderOptions);
}
public void PushTextOptions(TextOptions textOptions)
{
CheckLease();
_textOptionsStack.Push(TextOptions);
TextOptions = TextOptions.MergeWith(textOptions);
}
public void PopRenderOptions()
{
RenderOptions = _renderOptionsStack.Pop();
}
public void PopTextOptions()
{
TextOptions = _textOptionsStack.Pop();
}
/// <inheritdoc />
public virtual void Dispose()
{

92
src/Skia/Avalonia.Skia/GlyphRunImpl.cs

@ -15,13 +15,9 @@ namespace Avalonia.Skia
private readonly ushort[] _glyphIndices;
private readonly SKPoint[] _glyphPositions;
// We use an array as opposed to a ConcurrentDictionary to prevent a large amount of lock object allocations.
// This is possible because the SKFontEdging enum has consecutive integer elements 0, 1, 2, etc. and thus
// can be mapped directly to array indices.
//
// Should Skia update the enum with more elements, then the size of this array should be updated appropriately.
private const int FontEdgingsCount = (int)SKFontEdging.SubpixelAntialias + 1;
private readonly SKTextBlob?[] _textBlobCache = new SKTextBlob?[FontEdgingsCount];
// A two level cache optimized for single-entry read. Uses TextOptions as a key.
private readonly TwoLevelCache<TextOptions, SKTextBlob> _textBlobCache =
new TwoLevelCache<TextOptions, SKTextBlob>(secondarySize: 3, evictionAction: b => b?.Dispose());
public GlyphRunImpl(GlyphTypeface glyphTypeface, double fontRenderingEmSize,
IReadOnlyList<GlyphInfo> glyphInfos, Point baselineOrigin)
@ -62,7 +58,14 @@ namespace Avalonia.Skia
// But the bounds depends on the edging: for now, always use SubpixelAntialias so we have consistent values.
// The resulting bounds may be shifted by 1px on some fonts:
// "F" text with Inter size 14 has a 0px left bound with SubpixelAntialias but 1px with Antialias.
using var font = CreateFont(SKFontEdging.SubpixelAntialias);
var defaultTextOptions = default(TextOptions) with
{
TextRenderingMode = TextRenderingMode.SubpixelAntialias,
TextHintingMode = TextHintingMode.Strong,
BaselinePixelAlignment = BaselinePixelAlignment.Unaligned
};
using var font = CreateFont(defaultTextOptions);
var runBounds = new Rect();
var glyphBounds = ArrayPool<SKRect>.Shared.Rent(count);
@ -92,26 +95,19 @@ namespace Avalonia.Skia
public Rect Bounds { get; }
public SKTextBlob GetTextBlob(RenderOptions renderOptions)
public SKTextBlob GetTextBlob(TextOptions textOptions, RenderOptions renderOptions)
{
var edging = SKFontEdging.SubpixelAntialias;
switch (renderOptions.TextRenderingMode)
if (textOptions.TextRenderingMode == TextRenderingMode.Unspecified)
{
case TextRenderingMode.Alias:
edging = SKFontEdging.Alias;
break;
case TextRenderingMode.Antialias:
edging = SKFontEdging.Antialias;
break;
case TextRenderingMode.Unspecified:
edging = renderOptions.EdgeMode == EdgeMode.Aliased ? SKFontEdging.Alias : SKFontEdging.SubpixelAntialias;
break;
textOptions = textOptions with
{
TextRenderingMode = renderOptions.EdgeMode == EdgeMode.Aliased ? TextRenderingMode.Alias : TextRenderingMode.SubpixelAntialias
};
}
if (_textBlobCache[(int)edging] is null)
return _textBlobCache.GetOrAdd(textOptions, k =>
{
using var font = CreateFont(edging);
using var font = CreateFont(textOptions);
var builder = SKTextBlobBuilderCache.Shared.Get();
@ -121,37 +117,59 @@ namespace Avalonia.Skia
runBuffer.SetGlyphs(_glyphIndices);
var textBlob = builder.Build()!;
SKTextBlobBuilderCache.Shared.Return(builder);
Interlocked.CompareExchange(ref _textBlobCache[(int)edging], textBlob, null);
}
return _textBlobCache[(int)edging]!;
return textBlob;
});
}
private SKFont CreateFont(SKFontEdging edging)
private SKFont CreateFont(TextOptions textOptions)
{
// Determine edging from TextRenderingMode
var edging = textOptions.TextRenderingMode switch
{
TextRenderingMode.Alias => SKFontEdging.Alias,
TextRenderingMode.Antialias => SKFontEdging.Antialias,
TextRenderingMode.SubpixelAntialias => SKFontEdging.SubpixelAntialias,
_ => SKFontEdging.SubpixelAntialias
};
// Determine hinting
var hinting = textOptions.TextHintingMode switch
{
TextHintingMode.None => SKFontHinting.None,
TextHintingMode.Light => SKFontHinting.Slight,
TextHintingMode.Strong => SKFontHinting.Full,
_ => SKFontHinting.Full,
};
// Force auto-hinting for "Slight" mode (prefer autohinter over bytecode hints), otherwise default.
var forceAutoHinting = textOptions.TextHintingMode == TextHintingMode.Light;
// Subpixel rendering enabled when edging is not alias.
var subpixel = edging != SKFontEdging.Alias;
// Baseline snap defaults to true unless explicitly disabled.
var baselineSnap = textOptions.BaselinePixelAlignment != BaselinePixelAlignment.Unaligned;
var font = _glyphTypefaceImpl.CreateSKFont((float)FontRenderingEmSize);
font.Hinting = SKFontHinting.Full;
font.Subpixel = edging != SKFontEdging.Alias;
font.ForceAutoHinting = forceAutoHinting;
font.Hinting = hinting;
font.Subpixel = subpixel;
font.Edging = edging;
font.BaselineSnap = baselineSnap;
return font;
}
public void Dispose()
{
foreach (var textBlob in _textBlobCache)
{
textBlob?.Dispose();
}
_textBlobCache.ClearAndDispose();
}
public IReadOnlyList<float> GetIntersections(float lowerLimit, float upperLimit)
{
var textBlob = GetTextBlob(default);
var textBlob = GetTextBlob(default, default);
return textBlob.GetIntercepts(lowerLimit, upperLimit);
}

157
src/Skia/Avalonia.Skia/TwoLevelCache.cs

@ -0,0 +1,157 @@
using System;
using System.Collections.Generic;
namespace Avalonia.Skia
{
/// <summary>
/// Provides a lightweight two-level cache for storing key-value pairs, supporting fast retrieval and optional
/// eviction handling.
/// </summary>
/// <remarks>The cache maintains a primary entry for the most recently added item and a secondary array
/// for additional items, with a configurable capacity. When the cache exceeds its capacity, evicted values can be
/// processed using an optional eviction action. This class is intended for internal use and is not
/// thread-safe.</remarks>
/// <typeparam name="TKey">The type of keys used to identify cached values. Must be non-nullable.</typeparam>
/// <typeparam name="TValue">The type of values to be stored in the cache. Must be a reference type.</typeparam>
internal class TwoLevelCache<TKey, TValue>
where TKey : notnull
where TValue : class
{
private readonly int _secondarySize;
private TKey? _primaryKey;
private TValue? _primaryValue;
private KeyValuePair<TKey, TValue>[]? _secondary;
private int _secondaryCount;
private readonly Action<TValue?>? _evictionAction;
private readonly IEqualityComparer<TKey> _comparer;
public TwoLevelCache(int secondarySize = 3, Action<TValue?>? evictionAction = null, IEqualityComparer<TKey>? comparer = null)
{
if (secondarySize < 0)
{
throw new ArgumentOutOfRangeException(nameof(secondarySize));
}
_secondarySize = secondarySize;
_evictionAction = evictionAction;
_comparer = comparer ?? EqualityComparer<TKey>.Default;
}
public bool TryGet(TKey key, out TValue? value)
{
if (_primaryValue != null && _comparer.Equals(_primaryKey!, key))
{
value = _primaryValue;
return true;
}
var sec = _secondary;
if (sec != null)
{
for (int i = 0; i < _secondaryCount; i++)
{
if (_comparer.Equals(sec[i].Key, key))
{
value = sec[i].Value;
return true;
}
}
}
value = null;
return false;
}
public TValue GetOrAdd(TKey key, Func<TKey, TValue> factory)
{
// Check if key already exists
if (TryGet(key, out var existing) && existing != null)
{
return existing;
}
// Key doesn't exist, create new value
var value = factory(key);
// Primary is empty - store in primary
if (_primaryValue == null)
{
_primaryKey = key;
_primaryValue = value;
return value;
}
// No secondary cache configured - replace primary
if (_secondarySize == 0)
{
_evictionAction?.Invoke(_primaryValue);
_primaryKey = key;
_primaryValue = value;
return value;
}
// Secondary not yet initialized - create it
if (_secondary == null)
{
_secondary = new KeyValuePair<TKey, TValue>[_secondarySize];
_secondaryCount = 0;
}
// Shift existing entries right and insert new one at front
// This maintains insertion order and evicts the oldest (last) entry when full
TValue? evicted = default;
bool shouldEvict = _secondaryCount == _secondarySize;
if (shouldEvict)
{
// Cache is full, last entry will be evicted
evicted = _secondary[_secondarySize - 1].Value;
}
// Shift existing entries to make room at index 0
int shiftCount = shouldEvict ? _secondarySize - 1 : _secondaryCount;
for (int i = shiftCount; i > 0; i--)
{
_secondary[i] = _secondary[i - 1];
}
// Insert new entry at front
_secondary[0] = new KeyValuePair<TKey, TValue>(key, value);
// Update count (capped at size)
if (_secondaryCount < _secondarySize)
{
_secondaryCount++;
}
// Invoke eviction action if we evicted an entry
if (shouldEvict)
{
_evictionAction?.Invoke(evicted);
}
return value;
}
public void ClearAndDispose()
{
if (_primaryValue != null)
{
_evictionAction?.Invoke(_primaryValue);
_primaryValue = null;
_primaryKey = default;
}
if (_secondary != null)
{
for (int i = 0; i < _secondaryCount; i++)
{
_evictionAction?.Invoke(_secondary[i].Value);
}
_secondary = null;
_secondaryCount = 0;
}
}
}
}

7
src/Windows/Avalonia.Win32.Automation/AutomationNode.cs

@ -54,10 +54,8 @@ namespace Avalonia.Win32.Automation
{ SelectionPatternIdentifiers.IsSelectionRequiredProperty, UiaPropertyId.SelectionIsSelectionRequired },
{ SelectionPatternIdentifiers.SelectionProperty, UiaPropertyId.SelectionSelection },
{ SelectionItemPatternIdentifiers.IsSelectedProperty, UiaPropertyId.SelectionItemIsSelected },
{
SelectionItemPatternIdentifiers.SelectionContainerProperty,
UiaPropertyId.SelectionItemSelectionContainer
}
{ SelectionItemPatternIdentifiers.SelectionContainerProperty, UiaPropertyId.SelectionItemSelectionContainer },
{ TogglePatternIdentifiers.ToggleStateProperty, UiaPropertyId.ToggleToggleState },
};
private static ConditionalWeakTable<AutomationPeer, AutomationNode> s_nodes = new();
@ -359,6 +357,7 @@ namespace Avalonia.Win32.Automation
AutomationControlType.Table => UiaControlTypeId.Table,
AutomationControlType.TitleBar => UiaControlTypeId.TitleBar,
AutomationControlType.Separator => UiaControlTypeId.Separator,
AutomationControlType.Expander => UiaControlTypeId.Group,
_ => UiaControlTypeId.Custom,
};
}

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

Loading…
Cancel
Save