141 changed files with 2607 additions and 814 deletions
@ -1,5 +1,5 @@ |
|||||
<Project> |
<Project> |
||||
<PropertyGroup Condition="$(NETCoreSdkVersion.StartsWith('7.0'))"> |
<PropertyGroup Condition="$([MSBuild]::VersionGreaterThanOrEquals($(NETCoreSdkVersion), '7.0'))"> |
||||
<DefineConstants>$(DefineConstants);NET7SDK</DefineConstants> |
<DefineConstants>$(DefineConstants);NET7SDK</DefineConstants> |
||||
</PropertyGroup> |
</PropertyGroup> |
||||
</Project> |
</Project> |
||||
|
|||||
@ -0,0 +1,24 @@ |
|||||
|
using System; |
||||
|
using System.IO; |
||||
|
using Nuke.Common.Utilities; |
||||
|
|
||||
|
class Helpers |
||||
|
{ |
||||
|
public static IDisposable UseTempDir(out string dir) |
||||
|
{ |
||||
|
var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); |
||||
|
Directory.CreateDirectory(path); |
||||
|
dir = path; |
||||
|
return DelegateDisposable.CreateBracket(null, () => |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
Directory.Delete(path, true); |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
// ignore
|
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,171 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using System.IO; |
||||
|
using System.IO.Compression; |
||||
|
using System.Linq; |
||||
|
using ILRepacking; |
||||
|
using Mono.Cecil; |
||||
|
using Mono.Cecil.Cil; |
||||
|
|
||||
|
public class RefAssemblyGenerator |
||||
|
{ |
||||
|
class Resolver : DefaultAssemblyResolver, IAssemblyResolver |
||||
|
{ |
||||
|
private readonly string _dir; |
||||
|
Dictionary<string, AssemblyDefinition> _cache = new(); |
||||
|
|
||||
|
public Resolver(string dir) |
||||
|
{ |
||||
|
_dir = dir; |
||||
|
} |
||||
|
|
||||
|
public override AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters) |
||||
|
{ |
||||
|
if (_cache.TryGetValue(name.Name, out var asm)) |
||||
|
return asm; |
||||
|
var path = Path.Combine(_dir, name.Name + ".dll"); |
||||
|
if (File.Exists(path)) |
||||
|
return _cache[name.Name] = AssemblyDefinition.ReadAssembly(path, parameters); |
||||
|
return base.Resolve(name, parameters); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static void PatchRefAssembly(string file) |
||||
|
{ |
||||
|
var reader = typeof(RefAssemblyGenerator).Assembly.GetManifestResourceStream("avalonia.snk"); |
||||
|
var snk = new byte[reader.Length]; |
||||
|
reader.Read(snk, 0, snk.Length); |
||||
|
|
||||
|
var def = AssemblyDefinition.ReadAssembly(file, new ReaderParameters |
||||
|
{ |
||||
|
ReadWrite = true, |
||||
|
InMemory = true, |
||||
|
ReadSymbols = true, |
||||
|
SymbolReaderProvider = new DefaultSymbolReaderProvider(false), |
||||
|
AssemblyResolver = new Resolver(Path.GetDirectoryName(file)) |
||||
|
}); |
||||
|
|
||||
|
var obsoleteAttribute = def.MainModule.ImportReference(new TypeReference("System", "ObsoleteAttribute", def.MainModule, |
||||
|
def.MainModule.TypeSystem.CoreLibrary)); |
||||
|
var obsoleteCtor = def.MainModule.ImportReference(new MethodReference(".ctor", |
||||
|
def.MainModule.TypeSystem.Void, obsoleteAttribute) |
||||
|
{ |
||||
|
Parameters = { new ParameterDefinition(def.MainModule.TypeSystem.String) } |
||||
|
}); |
||||
|
|
||||
|
foreach(var t in def.MainModule.Types) |
||||
|
ProcessType(t, obsoleteCtor); |
||||
|
def.Write(file, new WriterParameters() |
||||
|
{ |
||||
|
StrongNameKeyBlob = snk, |
||||
|
WriteSymbols = def.MainModule.HasSymbols, |
||||
|
SymbolWriterProvider = new EmbeddedPortablePdbWriterProvider(), |
||||
|
DeterministicMvid = def.MainModule.HasSymbols |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
static void ProcessType(TypeDefinition type, MethodReference obsoleteCtor) |
||||
|
{ |
||||
|
foreach (var nested in type.NestedTypes) |
||||
|
ProcessType(nested, obsoleteCtor); |
||||
|
if (type.IsInterface) |
||||
|
{ |
||||
|
var hideMethods = type.Name.EndsWith("Impl") |
||||
|
|| (type.HasCustomAttributes && type.CustomAttributes.Any(a => |
||||
|
a.AttributeType.FullName == "Avalonia.Metadata.PrivateApiAttribute")); |
||||
|
|
||||
|
var injectMethod = hideMethods |
||||
|
|| type.CustomAttributes.Any(a => |
||||
|
a.AttributeType.FullName == "Avalonia.Metadata.NotClientImplementableAttribute"); |
||||
|
|
||||
|
if (hideMethods) |
||||
|
{ |
||||
|
foreach (var m in type.Methods) |
||||
|
{ |
||||
|
var dflags = MethodAttributes.Public | MethodAttributes.Family | MethodAttributes.FamORAssem | |
||||
|
MethodAttributes.FamANDAssem | MethodAttributes.Assembly; |
||||
|
m.Attributes = ((m.Attributes | dflags) ^ dflags) | MethodAttributes.Assembly; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if(injectMethod) |
||||
|
{ |
||||
|
type.Methods.Add(new MethodDefinition("NotClientImplementable", |
||||
|
MethodAttributes.Assembly |
||||
|
| MethodAttributes.Abstract |
||||
|
| MethodAttributes.NewSlot |
||||
|
| MethodAttributes.HideBySig, type.Module.TypeSystem.Void)); |
||||
|
} |
||||
|
|
||||
|
var forceUnstable = type.CustomAttributes.Any(a => |
||||
|
a.AttributeType.FullName == "Avalonia.Metadata.UnstableAttribute"); |
||||
|
|
||||
|
foreach (var m in type.Methods) |
||||
|
MarkAsUnstable(m, obsoleteCtor, forceUnstable); |
||||
|
foreach (var m in type.Properties) |
||||
|
MarkAsUnstable(m, obsoleteCtor, forceUnstable); |
||||
|
foreach (var m in type.Events) |
||||
|
MarkAsUnstable(m, obsoleteCtor, forceUnstable); |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
static void MarkAsUnstable(IMemberDefinition def, MethodReference obsoleteCtor, bool force) |
||||
|
{ |
||||
|
if (!force && ( |
||||
|
def.HasCustomAttributes == false |
||||
|
|| def.CustomAttributes.All(a => a.AttributeType.FullName != "Avalonia.Metadata.UnstableAttribute"))) |
||||
|
return; |
||||
|
|
||||
|
if (def.CustomAttributes.Any(a => a.AttributeType.FullName == "System.ObsoleteAttribute")) |
||||
|
return; |
||||
|
|
||||
|
def.CustomAttributes.Add(new CustomAttribute(obsoleteCtor) |
||||
|
{ |
||||
|
ConstructorArguments = |
||||
|
{ |
||||
|
new CustomAttributeArgument(obsoleteCtor.Module.TypeSystem.String, |
||||
|
"This is a part of unstable API and can be changed in minor releases. You have been warned") |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public static void GenerateRefAsmsInPackage(string packagePath) |
||||
|
{ |
||||
|
using (var archive = new ZipArchive(File.Open(packagePath, FileMode.Open, FileAccess.ReadWrite), |
||||
|
ZipArchiveMode.Update)) |
||||
|
{ |
||||
|
foreach (var entry in archive.Entries.ToList()) |
||||
|
{ |
||||
|
if (entry.FullName.StartsWith("ref/")) |
||||
|
entry.Delete(); |
||||
|
} |
||||
|
|
||||
|
foreach (var entry in archive.Entries.ToList()) |
||||
|
{ |
||||
|
if (entry.FullName.StartsWith("lib/") && entry.Name.EndsWith(".xml")) |
||||
|
{ |
||||
|
var newEntry = archive.CreateEntry("ref/" + entry.FullName.Substring(4), |
||||
|
CompressionLevel.Optimal); |
||||
|
using (var src = entry.Open()) |
||||
|
using (var dst = newEntry.Open()) |
||||
|
src.CopyTo(dst); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var libs = archive.Entries.Where(e => e.FullName.StartsWith("lib/") && e.FullName.EndsWith(".dll")) |
||||
|
.Select((e => new { s = e.FullName.Split('/'), e = e })) |
||||
|
.Select(e => new { Tfm = e.s[1], Name = e.s[2], Entry = e.e }) |
||||
|
.GroupBy(x => x.Tfm); |
||||
|
foreach(var tfm in libs) |
||||
|
using (Helpers.UseTempDir(out var temp)) |
||||
|
{ |
||||
|
foreach (var l in tfm) |
||||
|
l.Entry.ExtractToFile(Path.Combine(temp, l.Name)); |
||||
|
foreach (var l in tfm) |
||||
|
PatchRefAssembly(Path.Combine(temp, l.Name)); |
||||
|
foreach (var l in tfm) |
||||
|
archive.CreateEntryFromFile(Path.Combine(temp, l.Name), $"ref/{l.Tfm}/{l.Name}"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
using Avalonia.Metadata; |
||||
|
using Avalonia.Styling; |
||||
|
|
||||
|
namespace Avalonia.Controls; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Resource provider with theme variant awareness.
|
||||
|
/// Can be used with <see cref="IResourceDictionary.ThemeDictionaries"/>.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// This is a helper interface for the XAML compiler to make Key property accessibly by the markup extensions.
|
||||
|
/// Which means, it can only be used with ResourceDictionaries and markup extensions in the XAML code.
|
||||
|
/// This API might be removed in the future minor updates.
|
||||
|
/// </remarks>
|
||||
|
[Unstable] |
||||
|
public interface IThemeVariantProvider : IResourceProvider |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Key property set by the compiler.
|
||||
|
/// </summary>
|
||||
|
ThemeVariant? Key { get; set; } |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Avalonia.Metadata; |
||||
|
|
||||
|
[AttributeUsage(AttributeTargets.Interface)] |
||||
|
public sealed class PrivateApiAttribute : Attribute |
||||
|
{ |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.Platform.Storage.FileIO; |
||||
|
|
||||
|
namespace Avalonia.Platform.Storage; |
||||
|
|
||||
|
internal class NoopStorageProvider : BclStorageProvider |
||||
|
{ |
||||
|
public override bool CanOpen => false; |
||||
|
public override Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options) |
||||
|
{ |
||||
|
return Task.FromResult<IReadOnlyList<IStorageFile>>(Array.Empty<IStorageFile>()); |
||||
|
} |
||||
|
|
||||
|
public override bool CanSave => false; |
||||
|
public override Task<IStorageFile?> SaveFilePickerAsync(FilePickerSaveOptions options) |
||||
|
{ |
||||
|
return Task.FromResult<IStorageFile?>(null); |
||||
|
} |
||||
|
|
||||
|
public override bool CanPickFolder => false; |
||||
|
public override Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options) |
||||
|
{ |
||||
|
return Task.FromResult<IReadOnlyList<IStorageFolder>>(Array.Empty<IStorageFolder>()); |
||||
|
} |
||||
|
} |
||||
@ -1,6 +1,18 @@ |
|||||
|
using System.Collections.Generic; |
||||
using Avalonia.Collections; |
using Avalonia.Collections; |
||||
|
|
||||
namespace Avalonia |
namespace Avalonia |
||||
{ |
{ |
||||
public sealed class Points : AvaloniaList<Point> { } |
public sealed class Points : AvaloniaList<Point> |
||||
|
{ |
||||
|
public Points() |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
|
||||
|
public Points(IEnumerable<Point> points) : base(points) |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
} |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,12 @@ |
|||||
|
using Avalonia.Automation.Peers; |
||||
|
using Avalonia.Controls.Primitives; |
||||
|
|
||||
|
namespace Avalonia.Controls.Automation.Peers |
||||
|
{ |
||||
|
public class ThumbAutomationPeer : ControlAutomationPeer |
||||
|
{ |
||||
|
public ThumbAutomationPeer(Thumb owner) : base(owner) { } |
||||
|
protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Thumb; |
||||
|
protected override bool IsContentElementCore() => false; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,61 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Layout; |
||||
|
|
||||
|
namespace Avalonia.Controls |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Describes the reason for a <see cref="WindowBase.Resized"/> event.
|
||||
|
/// </summary>
|
||||
|
public enum WindowResizeReason |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The resize reason is unknown or unspecified.
|
||||
|
/// </summary>
|
||||
|
Unspecified, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The resize was due to the user resizing the window, for example by dragging the
|
||||
|
/// window frame.
|
||||
|
/// </summary>
|
||||
|
User, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The resize was initiated by the application, for example by setting one of the sizing-
|
||||
|
/// related properties on <see cref="Window"/> such as <see cref="Layoutable.Width"/> or
|
||||
|
/// <see cref="Layoutable.Height"/>.
|
||||
|
/// </summary>
|
||||
|
Application, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The resize was initiated by the layout system.
|
||||
|
/// </summary>
|
||||
|
Layout, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The resize was due to a change in DPI.
|
||||
|
/// </summary>
|
||||
|
DpiChange, |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Provides data for the <see cref="WindowBase.Resized"/> event.
|
||||
|
/// </summary>
|
||||
|
public class WindowResizedEventArgs : EventArgs |
||||
|
{ |
||||
|
internal WindowResizedEventArgs(Size clientSize, WindowResizeReason reason) |
||||
|
{ |
||||
|
ClientSize = clientSize; |
||||
|
Reason = reason; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the new client size of the window in device-independent pixels.
|
||||
|
/// </summary>
|
||||
|
public Size ClientSize { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the reason for the resize.
|
||||
|
/// </summary>
|
||||
|
public WindowResizeReason Reason { get; } |
||||
|
} |
||||
|
} |
||||
@ -1,13 +0,0 @@ |
|||||
<Project Sdk="Microsoft.NET.Sdk"> |
|
||||
<PropertyGroup> |
|
||||
<TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks> |
|
||||
|
|
||||
</PropertyGroup> |
|
||||
<ItemGroup> |
|
||||
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" /> |
|
||||
</ItemGroup> |
|
||||
|
|
||||
<Import Project="..\..\build\ApiDiff.props" /> |
|
||||
<Import Project="..\..\build\DevAnalyzers.props" /> |
|
||||
<Import Project="..\..\build\TrimmingEnable.props" /> |
|
||||
</Project> |
|
||||
@ -1,12 +0,0 @@ |
|||||
<ResourceDictionary xmlns="https://github.com/avaloniaui" |
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
|
||||
<!-- Accent Colours --> |
|
||||
<!-- TODO pull accents from system... algorithm to generate shades --> |
|
||||
<Color x:Key="SystemAccentColor">#FF0078D7</Color> |
|
||||
<Color x:Key="SystemAccentColorDark1">#FF005A9E</Color> |
|
||||
<Color x:Key="SystemAccentColorDark2">#FF004275</Color> |
|
||||
<Color x:Key="SystemAccentColorDark3">#FF002642</Color> |
|
||||
<Color x:Key="SystemAccentColorLight1">#FF429CE3</Color> |
|
||||
<Color x:Key="SystemAccentColorLight2">#FF76B9ED</Color> |
|
||||
<Color x:Key="SystemAccentColorLight3">#FFA6D8FF</Color> |
|
||||
</ResourceDictionary> |
|
||||
@ -0,0 +1,69 @@ |
|||||
|
<ResourceDictionary xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
||||
|
<!-- https://docs.microsoft.com/en-us/previous-versions/windows/apps/dn518235(v=win.10)?redirectedfrom=MSDN --> |
||||
|
<ResourceDictionary.ThemeDictionaries> |
||||
|
<ResourceDictionary x:Key="Default"> |
||||
|
<Color x:Key="SystemAltHighColor">#FFFFFFFF</Color> |
||||
|
<Color x:Key="SystemAltLowColor">#33FFFFFF</Color> |
||||
|
<Color x:Key="SystemAltMediumColor">#99FFFFFF</Color> |
||||
|
<Color x:Key="SystemAltMediumHighColor">#CCFFFFFF</Color> |
||||
|
<Color x:Key="SystemAltMediumLowColor">#66FFFFFF</Color> |
||||
|
<Color x:Key="SystemBaseHighColor">#FF000000</Color> |
||||
|
<Color x:Key="SystemBaseLowColor">#33000000</Color> |
||||
|
<Color x:Key="SystemBaseMediumColor">#99000000</Color> |
||||
|
<Color x:Key="SystemBaseMediumHighColor">#CC000000</Color> |
||||
|
<Color x:Key="SystemBaseMediumLowColor">#66000000</Color> |
||||
|
<Color x:Key="SystemChromeAltLowColor">#FF171717</Color> |
||||
|
<Color x:Key="SystemChromeBlackHighColor">#FF000000</Color> |
||||
|
<Color x:Key="SystemChromeBlackLowColor">#33000000</Color> |
||||
|
<Color x:Key="SystemChromeBlackMediumLowColor">#66000000</Color> |
||||
|
<Color x:Key="SystemChromeBlackMediumColor">#CC000000</Color> |
||||
|
<Color x:Key="SystemChromeDisabledHighColor">#FFCCCCCC</Color> |
||||
|
<Color x:Key="SystemChromeDisabledLowColor">#FF7A7A7A</Color> |
||||
|
<Color x:Key="SystemChromeHighColor">#FFCCCCCC</Color> |
||||
|
<Color x:Key="SystemChromeLowColor">#FFF2F2F2</Color> |
||||
|
<Color x:Key="SystemChromeMediumColor">#FFE6E6E6</Color> |
||||
|
<Color x:Key="SystemChromeMediumLowColor">#FFF2F2F2</Color> |
||||
|
<Color x:Key="SystemChromeWhiteColor">#FFFFFFFF</Color> |
||||
|
<Color x:Key="SystemChromeGrayColor">#FF767676</Color> |
||||
|
<Color x:Key="SystemListLowColor">#19000000</Color> |
||||
|
<Color x:Key="SystemListMediumColor">#33000000</Color> |
||||
|
<Color x:Key="SystemErrorTextColor">#C50500</Color> |
||||
|
<Color x:Key="SystemRegionColor">#FFFFFFFF</Color> |
||||
|
<Color x:Key="SystemRevealListLowColor">#17000000</Color> |
||||
|
<Color x:Key="SystemRevealListMediumColor">#2E000000</Color> |
||||
|
</ResourceDictionary> |
||||
|
<ResourceDictionary x:Key="Dark"> |
||||
|
<Color x:Key="SystemAltHighColor">#FF000000</Color> |
||||
|
<Color x:Key="SystemAltLowColor">#33000000</Color> |
||||
|
<Color x:Key="SystemAltMediumColor">#99000000</Color> |
||||
|
<Color x:Key="SystemAltMediumHighColor">#CC000000</Color> |
||||
|
<Color x:Key="SystemAltMediumLowColor">#66000000</Color> |
||||
|
<Color x:Key="SystemBaseHighColor">#FFFFFFFF</Color> |
||||
|
<Color x:Key="SystemBaseLowColor">#33FFFFFF</Color> |
||||
|
<Color x:Key="SystemBaseMediumColor">#99FFFFFF</Color> |
||||
|
<Color x:Key="SystemBaseMediumHighColor">#CCFFFFFF</Color> |
||||
|
<Color x:Key="SystemBaseMediumLowColor">#66FFFFFF</Color> |
||||
|
<Color x:Key="SystemChromeAltLowColor">#FFF2F2F2</Color> |
||||
|
<Color x:Key="SystemChromeBlackHighColor">#FF000000</Color> |
||||
|
<Color x:Key="SystemChromeBlackLowColor">#33000000</Color> |
||||
|
<Color x:Key="SystemChromeBlackMediumLowColor">#66000000</Color> |
||||
|
<Color x:Key="SystemChromeBlackMediumColor">#CC000000</Color> |
||||
|
<Color x:Key="SystemChromeDisabledHighColor">#FF333333</Color> |
||||
|
<Color x:Key="SystemChromeDisabledLowColor">#FF858585</Color> |
||||
|
<Color x:Key="SystemChromeHighColor">#FF767676</Color> |
||||
|
<Color x:Key="SystemChromeLowColor">#FF171717</Color> |
||||
|
<Color x:Key="SystemChromeMediumColor">#FF1F1F1F</Color> |
||||
|
<Color x:Key="SystemChromeMediumLowColor">#FF2B2B2B</Color> |
||||
|
<Color x:Key="SystemChromeWhiteColor">#FFFFFFFF</Color> |
||||
|
<Color x:Key="SystemChromeGrayColor">#FF767676</Color> |
||||
|
<Color x:Key="SystemListLowColor">#19FFFFFF</Color> |
||||
|
<Color x:Key="SystemListMediumColor">#33FFFFFF</Color> |
||||
|
<Color x:Key="SystemErrorTextColor">#FFF000</Color> |
||||
|
<Color x:Key="SystemRegionColor">#FF000000</Color> |
||||
|
<Color x:Key="SystemRevealListLowColor">#18FFFFFF</Color> |
||||
|
<Color x:Key="SystemRevealListMediumColor">#30FFFFFF</Color> |
||||
|
</ResourceDictionary> |
||||
|
</ResourceDictionary.ThemeDictionaries> |
||||
|
|
||||
|
</ResourceDictionary> |
||||
@ -0,0 +1,163 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Styling; |
||||
|
|
||||
|
namespace Avalonia.Themes.Fluent.Accents; |
||||
|
|
||||
|
internal class SystemAccentColors : IResourceProvider |
||||
|
{ |
||||
|
public const string AccentKey = "SystemAccentColor"; |
||||
|
public const string AccentDark1Key = "SystemAccentColorDark1"; |
||||
|
public const string AccentDark2Key = "SystemAccentColorDark2"; |
||||
|
public const string AccentDark3Key = "SystemAccentColorDark3"; |
||||
|
public const string AccentLight1Key = "SystemAccentColorLight1"; |
||||
|
public const string AccentLight2Key = "SystemAccentColorLight2"; |
||||
|
public const string AccentLight3Key = "SystemAccentColorLight3"; |
||||
|
|
||||
|
private static readonly Color s_defaultSystemAccentColor = Color.FromRgb(0, 120, 215); |
||||
|
private readonly IPlatformSettings? _platformSettings; |
||||
|
private bool _invalidateColors = true; |
||||
|
private Color _systemAccentColor; |
||||
|
private Color _systemAccentColorDark1, _systemAccentColorDark2, _systemAccentColorDark3; |
||||
|
private Color _systemAccentColorLight1, _systemAccentColorLight2, _systemAccentColorLight3; |
||||
|
|
||||
|
public SystemAccentColors() |
||||
|
{ |
||||
|
_platformSettings = AvaloniaLocator.Current.GetService<IPlatformSettings>(); |
||||
|
} |
||||
|
|
||||
|
public bool HasResources => true; |
||||
|
public bool TryGetResource(object key, ThemeVariant? theme, out object? value) |
||||
|
{ |
||||
|
if (key is string strKey) |
||||
|
{ |
||||
|
if (strKey.Equals(AccentKey, StringComparison.InvariantCulture)) |
||||
|
{ |
||||
|
EnsureColors(); |
||||
|
value = _systemAccentColor; |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
if (strKey.Equals(AccentDark1Key, StringComparison.InvariantCulture)) |
||||
|
{ |
||||
|
EnsureColors(); |
||||
|
value = _systemAccentColorDark1; |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
if (strKey.Equals(AccentDark2Key, StringComparison.InvariantCulture)) |
||||
|
{ |
||||
|
EnsureColors(); |
||||
|
value = _systemAccentColorDark2; |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
if (strKey.Equals(AccentDark3Key, StringComparison.InvariantCulture)) |
||||
|
{ |
||||
|
EnsureColors(); |
||||
|
value = _systemAccentColorDark3; |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
if (strKey.Equals(AccentLight1Key, StringComparison.InvariantCulture)) |
||||
|
{ |
||||
|
EnsureColors(); |
||||
|
value = _systemAccentColorLight1; |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
if (strKey.Equals(AccentLight2Key, StringComparison.InvariantCulture)) |
||||
|
{ |
||||
|
EnsureColors(); |
||||
|
value = _systemAccentColorLight2; |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
if (strKey.Equals(AccentLight3Key, StringComparison.InvariantCulture)) |
||||
|
{ |
||||
|
EnsureColors(); |
||||
|
value = _systemAccentColorLight3; |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
value = null; |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
public IResourceHost? Owner { get; private set; } |
||||
|
public event EventHandler? OwnerChanged; |
||||
|
public void AddOwner(IResourceHost owner) |
||||
|
{ |
||||
|
if (Owner != owner) |
||||
|
{ |
||||
|
Owner = owner; |
||||
|
OwnerChanged?.Invoke(this, EventArgs.Empty); |
||||
|
|
||||
|
if (_platformSettings is not null) |
||||
|
{ |
||||
|
_platformSettings.ColorValuesChanged += PlatformSettingsOnColorValuesChanged; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void RemoveOwner(IResourceHost owner) |
||||
|
{ |
||||
|
if (Owner == owner) |
||||
|
{ |
||||
|
Owner = null; |
||||
|
OwnerChanged?.Invoke(this, EventArgs.Empty); |
||||
|
|
||||
|
if (_platformSettings is not null) |
||||
|
{ |
||||
|
_platformSettings.ColorValuesChanged -= PlatformSettingsOnColorValuesChanged; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void EnsureColors() |
||||
|
{ |
||||
|
if (_invalidateColors) |
||||
|
{ |
||||
|
_invalidateColors = false; |
||||
|
|
||||
|
_systemAccentColor = _platformSettings?.GetColorValues().AccentColor1 ?? s_defaultSystemAccentColor; |
||||
|
(_systemAccentColorDark1,_systemAccentColorDark2, _systemAccentColorDark3, |
||||
|
_systemAccentColorLight1, _systemAccentColorLight2, _systemAccentColorLight3) = CalculateAccentShades(_systemAccentColor); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static (Color d1, Color d2, Color d3, Color l1, Color l2, Color l3) CalculateAccentShades(Color accentColor) |
||||
|
{ |
||||
|
// dark1step = (hslAccent.L - SystemAccentColorDark1.L) * 255
|
||||
|
const double dark1step = 28.5 / 255d; |
||||
|
const double dark2step = 49 / 255d; |
||||
|
const double dark3step = 74.5 / 255d; |
||||
|
// light1step = (SystemAccentColorLight1.L - hslAccent.L) * 255
|
||||
|
const double light1step = 39 / 255d; |
||||
|
const double light2step = 70 / 255d; |
||||
|
const double light3step = 103 / 255d; |
||||
|
|
||||
|
var hslAccent = accentColor.ToHsl(); |
||||
|
|
||||
|
return ( |
||||
|
// Darker shades
|
||||
|
new HslColor(hslAccent.A, hslAccent.H, hslAccent.S, hslAccent.L - dark1step).ToRgb(), |
||||
|
new HslColor(hslAccent.A, hslAccent.H, hslAccent.S, hslAccent.L - dark2step).ToRgb(), |
||||
|
new HslColor(hslAccent.A, hslAccent.H, hslAccent.S, hslAccent.L - dark3step).ToRgb(), |
||||
|
|
||||
|
// Lighter shades
|
||||
|
new HslColor(hslAccent.A, hslAccent.H, hslAccent.S, hslAccent.L + light1step).ToRgb(), |
||||
|
new HslColor(hslAccent.A, hslAccent.H, hslAccent.S, hslAccent.L + light2step).ToRgb(), |
||||
|
new HslColor(hslAccent.A, hslAccent.H, hslAccent.S, hslAccent.L + light3step).ToRgb() |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
private void PlatformSettingsOnColorValuesChanged(object? sender, PlatformColorValues e) |
||||
|
{ |
||||
|
_invalidateColors = true; |
||||
|
Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,158 @@ |
|||||
|
using Avalonia.Media; |
||||
|
|
||||
|
namespace Avalonia.Themes.Fluent; |
||||
|
|
||||
|
public partial class ColorPaletteResources |
||||
|
{ |
||||
|
private bool _hasAccentColor; |
||||
|
private Color _accentColor; |
||||
|
private Color _accentColorDark1, _accentColorDark2, _accentColorDark3; |
||||
|
private Color _accentColorLight1, _accentColorLight2, _accentColorLight3; |
||||
|
|
||||
|
public static readonly DirectProperty<ColorPaletteResources, Color> AccentProperty |
||||
|
= AvaloniaProperty.RegisterDirect<ColorPaletteResources, Color>(nameof(Accent), r => r.Accent, (r, v) => r.Accent = v); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the Accent color value.
|
||||
|
/// </summary>
|
||||
|
public Color Accent |
||||
|
{ |
||||
|
get => _accentColor; |
||||
|
set => SetAndRaise(AccentProperty, ref _accentColor, value); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the AltHigh color value.
|
||||
|
/// </summary>
|
||||
|
public Color AltHigh { get => GetColor("SystemAltHighColor"); set => SetColor("SystemAltHighColor", value); } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the AltLow color value.
|
||||
|
/// </summary>
|
||||
|
public Color AltLow { get => GetColor("SystemAltLowColor"); set => SetColor("SystemAltLowColor", value); } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the AltMedium color value.
|
||||
|
/// </summary>
|
||||
|
public Color AltMedium { get => GetColor("SystemAltMediumColor"); set => SetColor("SystemAltMediumColor", value); } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the AltMediumHigh color value.
|
||||
|
/// </summary>
|
||||
|
public Color AltMediumHigh { get => GetColor("SystemAltMediumHighColor"); set => SetColor("SystemAltMediumHighColor", value); } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the AltMediumLow color value.
|
||||
|
/// </summary>
|
||||
|
public Color AltMediumLow { get => GetColor("SystemAltMediumLowColor"); set => SetColor("SystemAltMediumLowColor", value); } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the BaseHigh color value.
|
||||
|
/// </summary>
|
||||
|
public Color BaseHigh { get => GetColor("SystemBaseHighColor"); set => SetColor("SystemBaseHighColor", value); } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the BaseLow color value.
|
||||
|
/// </summary>
|
||||
|
public Color BaseLow { get => GetColor("SystemBaseLowColor"); set => SetColor("SystemBaseLowColor", value); } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the BaseMedium color value.
|
||||
|
/// </summary>
|
||||
|
public Color BaseMedium { get => GetColor("SystemBaseMediumColor"); set => SetColor("SystemBaseMediumColor", value); } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the BaseMediumHigh color value.
|
||||
|
/// </summary>
|
||||
|
public Color BaseMediumHigh { get => GetColor("SystemBaseMediumHighColor"); set => SetColor("SystemBaseMediumHighColor", value); } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the BaseMediumLow color value.
|
||||
|
/// </summary>
|
||||
|
public Color BaseMediumLow { get => GetColor("SystemBaseMediumLowColor"); set => SetColor("SystemBaseMediumLowColor", value); } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the ChromeAltLow color value.
|
||||
|
/// </summary>
|
||||
|
public Color ChromeAltLow { get => GetColor("SystemChromeAltLowColor"); set => SetColor("SystemChromeAltLowColor", value); } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the ChromeBlackHigh color value.
|
||||
|
/// </summary>
|
||||
|
public Color ChromeBlackHigh { get => GetColor("SystemChromeBlackHighColor"); set => SetColor("SystemChromeBlackHighColor", value); } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the ChromeBlackLow color value.
|
||||
|
/// </summary>
|
||||
|
public Color ChromeBlackLow { get => GetColor("SystemChromeBlackLowColor"); set => SetColor("SystemChromeBlackLowColor", value); } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the ChromeBlackMedium color value.
|
||||
|
/// </summary>
|
||||
|
public Color ChromeBlackMedium { get => GetColor("SystemChromeBlackMediumColor"); set => SetColor("SystemChromeBlackMediumColor", value); } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the ChromeBlackMediumLow color value.
|
||||
|
/// </summary>
|
||||
|
public Color ChromeBlackMediumLow { get => GetColor("SystemChromeBlackMediumLowColor"); set => SetColor("SystemChromeBlackMediumLowColor", value); } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the ChromeDisabledHigh color value.
|
||||
|
/// </summary>
|
||||
|
public Color ChromeDisabledHigh { get => GetColor("SystemChromeDisabledHighColor"); set => SetColor("SystemChromeDisabledHighColor", value); } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the ChromeDisabledLow color value.
|
||||
|
/// </summary>
|
||||
|
public Color ChromeDisabledLow { get => GetColor("SystemChromeDisabledLowColor"); set => SetColor("SystemChromeDisabledLowColor", value); } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the ChromeGray color value.
|
||||
|
/// </summary>
|
||||
|
public Color ChromeGray { get => GetColor("SystemChromeGrayColor"); set => SetColor("SystemChromeGrayColor", value); } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the ChromeHigh color value.
|
||||
|
/// </summary>
|
||||
|
public Color ChromeHigh { get => GetColor("SystemChromeHighColor"); set => SetColor("SystemChromeHighColor", value); } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the ChromeLow color value.
|
||||
|
/// </summary>
|
||||
|
public Color ChromeLow { get => GetColor("SystemChromeLowColor"); set => SetColor("SystemChromeLowColor", value); } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the ChromeMedium color value.
|
||||
|
/// </summary>
|
||||
|
public Color ChromeMedium { get => GetColor("SystemChromeMediumColor"); set => SetColor("SystemChromeMediumColor", value); } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the ChromeMediumLow color value.
|
||||
|
/// </summary>
|
||||
|
public Color ChromeMediumLow { get => GetColor("SystemChromeMediumLowColor"); set => SetColor("SystemChromeMediumLowColor", value); } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the ChromeWhite color value.
|
||||
|
/// </summary>
|
||||
|
public Color ChromeWhite { get => GetColor("SystemChromeWhiteColor"); set => SetColor("SystemChromeWhiteColor", value); } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the ErrorText color value.
|
||||
|
/// </summary>
|
||||
|
public Color ErrorText { get => GetColor("SystemErrorTextColor"); set => SetColor("SystemErrorTextColor", value); } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the ListLow color value.
|
||||
|
/// </summary>
|
||||
|
public Color ListLow { get => GetColor("SystemListLowColor"); set => SetColor("SystemListLowColor", value); } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the ListMedium color value.
|
||||
|
/// </summary>
|
||||
|
public Color ListMedium { get => GetColor("SystemListMediumColor"); set => SetColor("SystemListMediumColor", value); } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the RegionColor color value.
|
||||
|
/// </summary>
|
||||
|
public Color RegionColor { get => GetColor("SystemRegionColor"); set => SetColor("SystemRegionColor", value); } |
||||
|
} |
||||
@ -0,0 +1,118 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Styling; |
||||
|
using Avalonia.Themes.Fluent.Accents; |
||||
|
|
||||
|
namespace Avalonia.Themes.Fluent; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Represents a specialized resource dictionary that contains color resources used by FluentTheme elements.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// This class can only be used in <see cref="FluentTheme.Palettes"/>.
|
||||
|
/// </remarks>
|
||||
|
public partial class ColorPaletteResources : AvaloniaObject, IResourceNode |
||||
|
{ |
||||
|
private readonly Dictionary<string, Color> _colors = new(StringComparer.InvariantCulture); |
||||
|
|
||||
|
public bool HasResources => _hasAccentColor || _colors.Count > 0; |
||||
|
|
||||
|
public bool TryGetResource(object key, ThemeVariant? theme, out object? value) |
||||
|
{ |
||||
|
if (key is string strKey) |
||||
|
{ |
||||
|
if (strKey.Equals(SystemAccentColors.AccentKey, StringComparison.InvariantCulture)) |
||||
|
{ |
||||
|
value = _accentColor; |
||||
|
return _hasAccentColor; |
||||
|
} |
||||
|
|
||||
|
if (strKey.Equals(SystemAccentColors.AccentDark1Key, StringComparison.InvariantCulture)) |
||||
|
{ |
||||
|
value = _accentColorDark1; |
||||
|
return _hasAccentColor; |
||||
|
} |
||||
|
|
||||
|
if (strKey.Equals(SystemAccentColors.AccentDark2Key, StringComparison.InvariantCulture)) |
||||
|
{ |
||||
|
value = _accentColorDark2; |
||||
|
return _hasAccentColor; |
||||
|
} |
||||
|
|
||||
|
if (strKey.Equals(SystemAccentColors.AccentDark3Key, StringComparison.InvariantCulture)) |
||||
|
{ |
||||
|
value = _accentColorDark3; |
||||
|
return _hasAccentColor; |
||||
|
} |
||||
|
|
||||
|
if (strKey.Equals(SystemAccentColors.AccentLight1Key, StringComparison.InvariantCulture)) |
||||
|
{ |
||||
|
value = _accentColorLight1; |
||||
|
return _hasAccentColor; |
||||
|
} |
||||
|
|
||||
|
if (strKey.Equals(SystemAccentColors.AccentLight2Key, StringComparison.InvariantCulture)) |
||||
|
{ |
||||
|
value = _accentColorLight2; |
||||
|
return _hasAccentColor; |
||||
|
} |
||||
|
|
||||
|
if (strKey.Equals(SystemAccentColors.AccentLight3Key, StringComparison.InvariantCulture)) |
||||
|
{ |
||||
|
value = _accentColorLight3; |
||||
|
return _hasAccentColor; |
||||
|
} |
||||
|
|
||||
|
if (_colors.TryGetValue(strKey, out var color)) |
||||
|
{ |
||||
|
value = color; |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
value = null; |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
private Color GetColor(string key) |
||||
|
{ |
||||
|
if (_colors.TryGetValue(key, out var color)) |
||||
|
{ |
||||
|
return color; |
||||
|
} |
||||
|
|
||||
|
return default; |
||||
|
} |
||||
|
|
||||
|
private void SetColor(string key, Color value) |
||||
|
{ |
||||
|
if (value == default) |
||||
|
{ |
||||
|
_colors.Remove(key); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
_colors[key] = value; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) |
||||
|
{ |
||||
|
base.OnPropertyChanged(change); |
||||
|
|
||||
|
if (change.Property == AccentProperty) |
||||
|
{ |
||||
|
_hasAccentColor = _accentColor != default; |
||||
|
|
||||
|
if (_hasAccentColor) |
||||
|
{ |
||||
|
(_accentColorDark1, _accentColorDark2, _accentColorDark3, |
||||
|
_accentColorLight1, _accentColorLight2, _accentColorLight3) = |
||||
|
SystemAccentColors.CalculateAccentShades(_accentColor); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,65 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Collections; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Styling; |
||||
|
|
||||
|
namespace Avalonia.Themes.Fluent; |
||||
|
|
||||
|
internal class ColorPaletteResourcesCollection : AvaloniaDictionary<ThemeVariant, ColorPaletteResources>, IResourceProvider |
||||
|
{ |
||||
|
public ColorPaletteResourcesCollection() : base(2) |
||||
|
{ |
||||
|
this.ForEachItem( |
||||
|
(_, x) => |
||||
|
{ |
||||
|
if (Owner is not null) |
||||
|
{ |
||||
|
x.PropertyChanged += Palette_PropertyChanged; |
||||
|
} |
||||
|
}, |
||||
|
(_, x) => |
||||
|
{ |
||||
|
if (Owner is not null) |
||||
|
{ |
||||
|
x.PropertyChanged -= Palette_PropertyChanged; |
||||
|
} |
||||
|
}, |
||||
|
() => throw new NotSupportedException("Dictionary reset not supported")); |
||||
|
} |
||||
|
|
||||
|
public bool HasResources => Count > 0; |
||||
|
public bool TryGetResource(object key, ThemeVariant? theme, out object? value) |
||||
|
{ |
||||
|
theme ??= ThemeVariant.Default; |
||||
|
if (base.TryGetValue(theme, out var paletteResources) |
||||
|
&& paletteResources.TryGetResource(key, theme, out value)) |
||||
|
{ |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
value = null; |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
public IResourceHost? Owner { get; private set; } |
||||
|
public event EventHandler? OwnerChanged; |
||||
|
public void AddOwner(IResourceHost owner) |
||||
|
{ |
||||
|
Owner = owner; |
||||
|
OwnerChanged?.Invoke(this, EventArgs.Empty); |
||||
|
} |
||||
|
|
||||
|
public void RemoveOwner(IResourceHost owner) |
||||
|
{ |
||||
|
Owner = null; |
||||
|
OwnerChanged?.Invoke(this, EventArgs.Empty); |
||||
|
} |
||||
|
|
||||
|
private void Palette_PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) |
||||
|
{ |
||||
|
if (e.Property == ColorPaletteResources.AccentProperty) |
||||
|
{ |
||||
|
Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
<PropertyGroup> |
||||
|
<TargetFramework>net6.0</TargetFramework> |
||||
|
<ImplicitUsings>enable</ImplicitUsings> |
||||
|
<Nullable>enable</Nullable> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="xunit.core" Version="2.4.2" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\Avalonia.Headless\Avalonia.Headless.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<Import Project="..\..\..\build\ApiDiff.props" /> |
||||
|
<Import Project="..\..\..\build\DevAnalyzers.props" /> |
||||
|
<Import Project="..\..\..\build\NullableEnable.props" /> |
||||
|
</Project> |
||||
@ -0,0 +1,35 @@ |
|||||
|
using System.Reflection; |
||||
|
using Xunit.Abstractions; |
||||
|
using Xunit.Sdk; |
||||
|
|
||||
|
namespace Avalonia.Headless.XUnit; |
||||
|
|
||||
|
internal class AvaloniaTestFramework<TAppBuilderEntry> : XunitTestFramework |
||||
|
{ |
||||
|
public AvaloniaTestFramework(IMessageSink messageSink) : base(messageSink) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
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) |
||||
|
{ |
||||
|
executionOptions.SetValue("xunit.execution.DisableParallelization", false); |
||||
|
using (var assemblyRunner = new AvaloniaTestRunner<TAppBuilderEntry>( |
||||
|
TestAssembly, testCases, DiagnosticMessageSink, executionMessageSink, |
||||
|
executionOptions)) await assemblyRunner.RunAsync(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,45 @@ |
|||||
|
using System.Diagnostics.CodeAnalysis; |
||||
|
using Xunit.Abstractions; |
||||
|
using Xunit.Sdk; |
||||
|
|
||||
|
namespace Avalonia.Headless.XUnit; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Sets up global avalonia test framework using avalonia application builder passed as a parameter.
|
||||
|
/// </summary>
|
||||
|
[TestFrameworkDiscoverer("Avalonia.Headless.XUnit.AvaloniaTestFrameworkTypeDiscoverer", "Avalonia.Headless.XUnit")] |
||||
|
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)] |
||||
|
public sealed class AvaloniaTestFrameworkAttribute : Attribute, ITestFrameworkAttribute |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Creates instance of <see cref="AvaloniaTestFrameworkAttribute"/>.
|
||||
|
/// </summary>
|
||||
|
/// <param name="appBuilderEntryPointType">
|
||||
|
/// Parameter from which <see cref="AppBuilder"/> should be created.
|
||||
|
/// It either needs to have BuildAvaloniaApp -> AppBuilder method or inherit Application.
|
||||
|
/// </param>
|
||||
|
public AvaloniaTestFrameworkAttribute( |
||||
|
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] |
||||
|
Type appBuilderEntryPointType) { } |
||||
|
} |
||||
|
|
||||
|
/// <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) |
||||
|
{ |
||||
|
var builderType = attribute.GetConstructorArguments().First() as Type |
||||
|
?? throw new InvalidOperationException("AppBuilderEntryPointType parameter must be defined on the AvaloniaTestFrameworkAttribute attribute."); |
||||
|
return typeof(AvaloniaTestFramework<>).MakeGenericType(builderType); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,61 @@ |
|||||
|
using Avalonia.Threading; |
||||
|
using Xunit.Abstractions; |
||||
|
using Xunit.Sdk; |
||||
|
|
||||
|
namespace Avalonia.Headless.XUnit; |
||||
|
|
||||
|
internal class AvaloniaTestRunner<TAppBuilderEntry> : XunitTestAssemblyRunner |
||||
|
{ |
||||
|
private CancellationTokenSource? _cancellationTokenSource; |
||||
|
|
||||
|
public AvaloniaTestRunner(ITestAssembly testAssembly, IEnumerable<IXunitTestCase> testCases, |
||||
|
IMessageSink diagnosticMessageSink, IMessageSink executionMessageSink, |
||||
|
ITestFrameworkExecutionOptions executionOptions) : base(testAssembly, testCases, diagnosticMessageSink, |
||||
|
executionMessageSink, executionOptions) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
protected override void SetupSyncContext(int maxParallelThreads) |
||||
|
{ |
||||
|
_cancellationTokenSource?.Dispose(); |
||||
|
_cancellationTokenSource = new CancellationTokenSource(); |
||||
|
SynchronizationContext.SetSynchronizationContext(InitNewApplicationContext(_cancellationTokenSource.Token).Result); |
||||
|
} |
||||
|
|
||||
|
public override void Dispose() |
||||
|
{ |
||||
|
_cancellationTokenSource?.Cancel(); |
||||
|
base.Dispose(); |
||||
|
} |
||||
|
|
||||
|
internal static Task<SynchronizationContext> InitNewApplicationContext(CancellationToken cancellationToken) |
||||
|
{ |
||||
|
var tcs = new TaskCompletionSource<SynchronizationContext>(); |
||||
|
|
||||
|
new Thread(() => |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var appBuilder = AppBuilder.Configure(typeof(TAppBuilderEntry)); |
||||
|
|
||||
|
// If windowing subsystem wasn't initialized by user, force headless with default parameters.
|
||||
|
if (appBuilder.WindowingSubsystemName != "Headless") |
||||
|
{ |
||||
|
appBuilder = appBuilder.UseHeadless(new AvaloniaHeadlessPlatformOptions()); |
||||
|
} |
||||
|
|
||||
|
appBuilder.SetupWithoutStarting(); |
||||
|
|
||||
|
tcs.SetResult(SynchronizationContext.Current!); |
||||
|
} |
||||
|
catch (Exception e) |
||||
|
{ |
||||
|
tcs.SetException(e); |
||||
|
} |
||||
|
|
||||
|
Dispatcher.UIThread.MainLoop(cancellationToken); |
||||
|
}) { IsBackground = true }.Start(); |
||||
|
|
||||
|
return tcs.Task; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
<PropertyGroup> |
||||
|
<TargetFrameworks>net6.0;netstandard2.0</TargetFrameworks> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<Import Project="..\..\..\build\ApiDiff.props" /> |
||||
|
<Import Project="..\..\..\build\DevAnalyzers.props" /> |
||||
|
<Import Project="..\..\..\build\TrimmingEnable.props" /> |
||||
|
<Import Project="..\..\..\build\NullableEnable.props" /> |
||||
|
|
||||
|
<ItemGroup Label="InternalsVisibleTo"> |
||||
|
<InternalsVisibleTo Include="Avalonia.Headless.Vnc, PublicKey=$(AvaloniaPublicKey)" /> |
||||
|
</ItemGroup> |
||||
|
</Project> |
||||
@ -0,0 +1,101 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Input; |
||||
|
using Avalonia.Input.Raw; |
||||
|
using Avalonia.Media.Imaging; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Threading; |
||||
|
|
||||
|
namespace Avalonia.Headless; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Set of extension methods to simplify usage of Avalonia.Headless platform.
|
||||
|
/// </summary>
|
||||
|
public static class HeadlessWindowExtensions |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Triggers a renderer timer tick and captures last rendered frame.
|
||||
|
/// </summary>
|
||||
|
/// <returns>Bitmap with last rendered frame. Null, if nothing was rendered.</returns>
|
||||
|
public static Bitmap? CaptureRenderedFrame(this TopLevel topLevel) |
||||
|
{ |
||||
|
Dispatcher.UIThread.RunJobs(); |
||||
|
AvaloniaHeadlessPlatform.ForceRenderTimerTick(); |
||||
|
return topLevel.GetLastRenderedFrame(); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Reads last rendered frame.
|
||||
|
/// Note, in order to trigger rendering timer, call <see cref="AvaloniaHeadlessPlatform.ForceRenderTimerTick"/> method.
|
||||
|
/// </summary>
|
||||
|
/// <returns>Bitmap with last rendered frame. Null, if nothing was rendered.</returns>
|
||||
|
public static Bitmap? GetLastRenderedFrame(this TopLevel topLevel) |
||||
|
{ |
||||
|
if (AvaloniaLocator.Current.GetService<IPlatformRenderInterface>() is HeadlessPlatformRenderInterface) |
||||
|
{ |
||||
|
throw new NotSupportedException( |
||||
|
"To capture a rendered frame, make sure that headless application was initialized with '.UseSkia()' and disabled 'UseHeadlessDrawing' in the 'AvaloniaHeadlessPlatformOptions'."); |
||||
|
} |
||||
|
|
||||
|
return GetImpl(topLevel).GetLastRenderedFrame(); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Simulates keyboard press on the headless window/toplevel.
|
||||
|
/// </summary>
|
||||
|
public static void KeyPress(this TopLevel topLevel, Key key, RawInputModifiers modifiers) => |
||||
|
RunJobsAndGetImpl(topLevel).KeyPress(key, modifiers); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Simulates keyboard release on the headless window/toplevel.
|
||||
|
/// </summary>
|
||||
|
public static void KeyRelease(this TopLevel topLevel, Key key, RawInputModifiers modifiers) => |
||||
|
RunJobsAndGetImpl(topLevel).KeyRelease(key, modifiers); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Simulates mouse down on the headless window/toplevel.
|
||||
|
/// </summary>
|
||||
|
public static void MouseDown(this TopLevel topLevel, Point point, MouseButton button, |
||||
|
RawInputModifiers modifiers = RawInputModifiers.None) => |
||||
|
RunJobsAndGetImpl(topLevel).MouseDown(point, button, modifiers); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Simulates mouse move on the headless window/toplevel.
|
||||
|
/// </summary>
|
||||
|
public static void MouseMove(this TopLevel topLevel, Point point, |
||||
|
RawInputModifiers modifiers = RawInputModifiers.None) => |
||||
|
RunJobsAndGetImpl(topLevel).MouseMove(point, modifiers); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Simulates mouse up on the headless window/toplevel.
|
||||
|
/// </summary>
|
||||
|
public static void MouseUp(this TopLevel topLevel, Point point, MouseButton button, |
||||
|
RawInputModifiers modifiers = RawInputModifiers.None) => |
||||
|
RunJobsAndGetImpl(topLevel).MouseUp(point, button, modifiers); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Simulates mouse wheel on the headless window/toplevel.
|
||||
|
/// </summary>
|
||||
|
public static void MouseWheel(this TopLevel topLevel, Point point, Vector delta, |
||||
|
RawInputModifiers modifiers = RawInputModifiers.None) => |
||||
|
RunJobsAndGetImpl(topLevel).MouseWheel(point, delta, modifiers); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Simulates drag'n'drop target on the headless window/toplevel.
|
||||
|
/// </summary>
|
||||
|
public static void DragDrop(this TopLevel topLevel, Point point, RawDragEventType type, IDataObject data, |
||||
|
DragDropEffects effects, RawInputModifiers modifiers = RawInputModifiers.None) => |
||||
|
RunJobsAndGetImpl(topLevel).DragDrop(point, type, data, effects, modifiers); |
||||
|
|
||||
|
private static IHeadlessWindow RunJobsAndGetImpl(this TopLevel topLevel) |
||||
|
{ |
||||
|
Dispatcher.UIThread.RunJobs(); |
||||
|
return GetImpl(topLevel); |
||||
|
} |
||||
|
|
||||
|
private static IHeadlessWindow GetImpl(this TopLevel topLevel) |
||||
|
{ |
||||
|
return topLevel.PlatformImpl as IHeadlessWindow ?? |
||||
|
throw new InvalidOperationException("TopLevel must be a headless window."); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,71 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using System.Reflection.Emit; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; |
||||
|
using XamlX; |
||||
|
using XamlX.Ast; |
||||
|
using XamlX.Emit; |
||||
|
using XamlX.IL; |
||||
|
using XamlX.Transform; |
||||
|
using XamlX.TypeSystem; |
||||
|
|
||||
|
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.AstNodes |
||||
|
{ |
||||
|
class AvaloniaXamlIlArrayConstantAstNode : XamlAstNode, IXamlAstValueNode, IXamlAstILEmitableNode |
||||
|
{ |
||||
|
private readonly IXamlType _elementType; |
||||
|
private readonly IReadOnlyList<IXamlAstValueNode> _values; |
||||
|
|
||||
|
public AvaloniaXamlIlArrayConstantAstNode(IXamlLineInfo lineInfo, IXamlType arrayType, IXamlType elementType, IReadOnlyList<IXamlAstValueNode> values) : base(lineInfo) |
||||
|
{ |
||||
|
_elementType = elementType; |
||||
|
_values = values; |
||||
|
|
||||
|
Type = new XamlAstClrTypeReference(lineInfo, arrayType, false); |
||||
|
|
||||
|
foreach (var element in values) |
||||
|
{ |
||||
|
if (!elementType.IsAssignableFrom(element.Type.GetClrType())) |
||||
|
{ |
||||
|
throw new XamlParseException("x:Array element is not assignable to the array element type!", lineInfo); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public IXamlAstTypeReference Type { get; } |
||||
|
|
||||
|
public XamlILNodeEmitResult Emit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen) |
||||
|
{ |
||||
|
codeGen.Ldc_I4(_values.Count) |
||||
|
.Newarr(_elementType); |
||||
|
|
||||
|
for (var index = 0; index < _values.Count; index++) |
||||
|
{ |
||||
|
var value = _values[index]; |
||||
|
|
||||
|
codeGen |
||||
|
.Dup() |
||||
|
.Ldc_I4(index); |
||||
|
|
||||
|
context.Emit(value, codeGen, _elementType); |
||||
|
|
||||
|
if (value.Type.GetClrType() is { IsValueType: true } valTypeInObjArr) |
||||
|
{ |
||||
|
if (!_elementType.IsValueType) |
||||
|
{ |
||||
|
codeGen.Box(valTypeInObjArr); |
||||
|
} |
||||
|
// It seems like ASM codegen for "stelem valuetype" and "stelem.i4" is identical,
|
||||
|
// so we don't need to try to optimize it here.
|
||||
|
codeGen.Emit(OpCodes.Stelem, valTypeInObjArr); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
codeGen.Stelem_ref(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return XamlILNodeEmitResult.Type(0, Type.GetClrType()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,31 @@ |
|||||
|
using System.Linq; |
||||
|
using XamlX; |
||||
|
using XamlX.Ast; |
||||
|
using XamlX.Transform; |
||||
|
using XamlX.TypeSystem; |
||||
|
|
||||
|
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; |
||||
|
|
||||
|
internal class AvaloniaXamlIlThemeVariantProviderTransformer : IXamlAstTransformer |
||||
|
{ |
||||
|
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) |
||||
|
{ |
||||
|
var type = context.GetAvaloniaTypes().IThemeVariantProvider; |
||||
|
if (!(node is XamlAstObjectNode on |
||||
|
&& type.IsAssignableFrom(on.Type.GetClrType()))) |
||||
|
return node; |
||||
|
|
||||
|
var keyDirective = on.Children.FirstOrDefault(n => n is XamlAstXmlDirective d |
||||
|
&& d.Namespace == XamlNamespaces.Xaml2006 && |
||||
|
d.Name == "Key") as XamlAstXmlDirective; |
||||
|
if (keyDirective is null) |
||||
|
return node; |
||||
|
|
||||
|
var keyProp = type.Properties.First(p => p.Name == "Key"); |
||||
|
on.Children.Add(new XamlAstXamlPropertyValueNode(keyDirective, |
||||
|
new XamlAstClrProperty(keyDirective, keyProp, context.Configuration), |
||||
|
keyDirective.Values, true)); |
||||
|
|
||||
|
return node; |
||||
|
} |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue