committed by
GitHub
123 changed files with 2479 additions and 1728 deletions
@ -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> |
|||
|
|||
@ -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> |
|||
File diff suppressed because it is too large
@ -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> |
|||
|
|||
@ -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> |
|||
|
|||
@ -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> |
|||
@ -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> |
|||
|
|||
@ -1 +1 @@ |
|||
Subproject commit 9738c6121fdd143c78d3e25686a7e4e13c00f586 |
|||
Subproject commit 5530e1cbe9e105ff4ebc9da1f4af3253a8756754 |
|||
@ -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; } |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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 |
|||
} |
|||
} |
|||
@ -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 |
|||
} |
|||
} |
|||
@ -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 }; |
|||
} |
|||
} |
|||
} |
|||
@ -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 |
|||
} |
|||
} |
|||
|
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
|
|||
@ -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; |
|||
} |
|||
@ -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)); |
|||
} |
|||
|
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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]); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue