From a0f0e161cb088121fe2a234b3b3b607dea996615 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 16 Nov 2022 18:04:38 -0500 Subject: [PATCH 1/6] Cleanup StyleInclude and ResourceInclude in the Core project --- .../Avalonia.Markup.Xaml.csproj | 3 +- .../MarkupExtensions/StyleIncludeExtension.cs | 22 -------------- .../ResourceInclude.cs | 30 ++++++++++++------- .../Styling/StyleInclude.cs | 4 +-- 4 files changed, 23 insertions(+), 36 deletions(-) delete mode 100644 src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StyleIncludeExtension.cs rename src/Markup/Avalonia.Markup.Xaml/{MarkupExtensions => Styling}/ResourceInclude.cs (70%) diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 070f0c1cc3..b5ba49ce2c 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -33,9 +33,7 @@ - - @@ -45,6 +43,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StyleIncludeExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StyleIncludeExtension.cs deleted file mode 100644 index 129fa66912..0000000000 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StyleIncludeExtension.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Avalonia.Markup.Xaml.Styling; -using Avalonia.Styling; -using System.ComponentModel; -using System; - -namespace Avalonia.Markup.Xaml.MarkupExtensions -{ - public class StyleIncludeExtension - { - public StyleIncludeExtension() - { - } - - public IStyle ProvideValue(IServiceProvider serviceProvider) - { - return new StyleInclude(serviceProvider.GetContextBaseUri()) { Source = Source }; - } - - public Uri Source { get; set; } - - } -} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs similarity index 70% rename from src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs rename to src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs index 1091b3ec7e..01db2c081f 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs @@ -1,20 +1,37 @@ using System; -using System.ComponentModel; using Avalonia.Controls; #nullable enable -namespace Avalonia.Markup.Xaml.MarkupExtensions +namespace Avalonia.Markup.Xaml.Styling { /// /// Loads a resource dictionary from a specified URL. /// public class ResourceInclude : IResourceProvider { - private Uri? _baseUri; + private readonly Uri? _baseUri; private IResourceDictionary? _loaded; private bool _isLoading; + /// + /// Initializes a new instance of the class. + /// + /// The base URL for the XAML context. + public ResourceInclude(Uri? baseUri) + { + _baseUri = baseUri; + } + + /// + /// Initializes a new instance of the class. + /// + /// The XAML service provider. + public ResourceInclude(IServiceProvider serviceProvider) + { + _baseUri = serviceProvider.GetContextBaseUri(); + } + /// /// Gets the loaded resource dictionary. /// @@ -61,12 +78,5 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions void IResourceProvider.AddOwner(IResourceHost owner) => Loaded.AddOwner(owner); void IResourceProvider.RemoveOwner(IResourceHost owner) => Loaded.RemoveOwner(owner); - - public ResourceInclude ProvideValue(IServiceProvider serviceProvider) - { - var tdc = (ITypeDescriptorContext)serviceProvider; - _baseUri = tdc?.GetContextBaseUri(); - return this; - } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index d92003ad9f..b1725245bb 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -12,7 +12,7 @@ namespace Avalonia.Markup.Xaml.Styling /// public class StyleInclude : IStyle, IResourceProvider { - private readonly Uri _baseUri; + private readonly Uri? _baseUri; private IStyle[]? _loaded; private bool _isLoading; @@ -20,7 +20,7 @@ namespace Avalonia.Markup.Xaml.Styling /// Initializes a new instance of the class. /// /// The base URL for the XAML context. - public StyleInclude(Uri baseUri) + public StyleInclude(Uri? baseUri) { _baseUri = baseUri; } From fcf26fe4f284379a6f4f0b44484cd4f818788492 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 16 Nov 2022 18:05:50 -0500 Subject: [PATCH 2/6] Inject generated types into Ref assembly as well --- packages/Avalonia/AvaloniaBuildTasks.targets | 1 + .../CompileAvaloniaXamlTask.cs | 23 +- src/Avalonia.Build.Tasks/Program.cs | 8 +- .../XamlCompilerTaskExecutor.cs | 264 ++++++++++++++---- .../AvaloniaXamlIlCompiler.cs | 3 +- .../AvaloniaXamlIlAssetIncludeTransformer.cs | 94 +++++++ .../AvaloniaXamlIlWellKnownTypes.cs | 2 + .../XamlIl/Runtime/XamlIlRuntimeHelpers.cs | 13 + .../Avalonia.Markup.Xaml.UnitTests.csproj | 8 +- .../Xaml/BasicTests.cs | 6 +- .../Xaml/StyleTests.cs | 32 ++- 11 files changed, 380 insertions(+), 74 deletions(-) create mode 100644 src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlAssetIncludeTransformer.cs diff --git a/packages/Avalonia/AvaloniaBuildTasks.targets b/packages/Avalonia/AvaloniaBuildTasks.targets index 01bf303a20..4f9c4d7720 100644 --- a/packages/Avalonia/AvaloniaBuildTasks.targets +++ b/packages/Avalonia/AvaloniaBuildTasks.targets @@ -99,6 +99,7 @@ AssemblyFile="@(IntermediateAssembly)" ReferencesFilePath="$(AvaloniaXamlReferencesTemporaryFilePath)" OriginalCopyPath="$(AvaloniaXamlOriginalCopyFilePath)" + RefAssemblyFile="@(IntermediateRefAssembly)" ProjectDirectory="$(MSBuildProjectDirectory)" VerifyIl="$(AvaloniaXamlIlVerifyIl)" ReportImportance="$(AvaloniaXamlReportImportance)" diff --git a/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs b/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs index e501fc650d..fd07e0a143 100644 --- a/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs +++ b/src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs @@ -12,12 +12,16 @@ namespace Avalonia.Build.Tasks Enum.TryParse(ReportImportance, true, out MessageImportance outputImportance); OutputPath = OutputPath ?? AssemblyFile; + RefOutputPath = RefOutputPath ?? RefAssemblyFile; var outputPdb = GetPdbPath(OutputPath); var input = AssemblyFile; + var refInput = RefOutputPath; var inputPdb = GetPdbPath(input); - // Make a copy and delete the original file to prevent MSBuild from thinking that everything is OK + // Make a copy and delete the original file to prevent MSBuild from thinking that everything is OK if (OriginalCopyPath != null) { + var originalCopyPathRef = Path.ChangeExtension(OriginalCopyPath, ".ref.dll"); + File.Copy(AssemblyFile, OriginalCopyPath, true); input = OriginalCopyPath; File.Delete(AssemblyFile); @@ -29,14 +33,24 @@ namespace Avalonia.Build.Tasks File.Delete(inputPdb); inputPdb = copyPdb; } + + if (!string.IsNullOrWhiteSpace(RefAssemblyFile) && File.Exists(RefAssemblyFile)) + { + // We also copy ref assembly just for case if needed later for testing. + // But do not remove the original one, as MSBuild actually complains about it with multi-thread compiling. + File.Copy(RefAssemblyFile, originalCopyPathRef, true); + refInput = originalCopyPathRef; + } } var msg = $"CompileAvaloniaXamlTask -> AssemblyFile:{AssemblyFile}, ProjectDirectory:{ProjectDirectory}, OutputPath:{OutputPath}"; BuildEngine.LogMessage(msg, outputImportance < MessageImportance.Low ? MessageImportance.High : outputImportance); - var res = XamlCompilerTaskExecutor.Compile(BuildEngine, input, + var res = XamlCompilerTaskExecutor.Compile(BuildEngine, + input, OutputPath, + refInput, RefOutputPath, File.ReadAllLines(ReferencesFilePath).Where(l => !string.IsNullOrWhiteSpace(l)).ToArray(), - ProjectDirectory, OutputPath, VerifyIl, DefaultCompileBindings, outputImportance, + ProjectDirectory, VerifyIl, DefaultCompileBindings, outputImportance, (SignAssembly && !DelaySign) ? AssemblyOriginatorKeyFile : null, SkipXamlCompilation, DebuggerLaunch); if (!res.Success) return false; @@ -68,6 +82,9 @@ namespace Avalonia.Build.Tasks [Required] public string ProjectDirectory { get; set; } + public string RefAssemblyFile { get; set; } + public string RefOutputPath { get; set; } + public string OutputPath { get; set; } public bool VerifyIl { get; set; } diff --git a/src/Avalonia.Build.Tasks/Program.cs b/src/Avalonia.Build.Tasks/Program.cs index f42fab5964..8ddea2d142 100644 --- a/src/Avalonia.Build.Tasks/Program.cs +++ b/src/Avalonia.Build.Tasks/Program.cs @@ -14,7 +14,7 @@ namespace Avalonia.Build.Tasks static int Main(string[] args) { - if (args.Length != 3) + if (args.Length < 3) { if (args.Length == 1) { @@ -27,11 +27,12 @@ namespace Avalonia.Build.Tasks Console.WriteLine(@$"Usage: 1) dotnet ./Avalonia.Build.Tasks.dll , where likes {referencesOutputPath} - 2) dotnet ./Avalonia.Build.Tasks.dll + 2) dotnet ./Avalonia.Build.Tasks.dll , where: - likes {referencesOutputPath}/{OriginalDll} - likes {referencesOutputPath}/{References} - - likes {referencesOutputPath}/{OutDll}"); + - likes {referencesOutputPath}/{OutDll} + - Likes {referencesOutputPath}/original.ref.dll"); return 1; } @@ -42,6 +43,7 @@ namespace Avalonia.Build.Tasks AssemblyFile = args[0], ReferencesFilePath = args[1], OutputPath = args[2], + RefAssemblyFile = args.Length > 3 ? args[3] : null, BuildEngine = new ConsoleBuildEngine(), ProjectDirectory = Directory.GetCurrentDirectory(), VerifyIl = true diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index 3493fc06ed..e68cb0013a 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -1,7 +1,7 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; -using System.Reflection; using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions; using Microsoft.Build.Framework; using Mono.Cecil; @@ -21,6 +21,8 @@ namespace Avalonia.Build.Tasks { public static partial class XamlCompilerTaskExecutor { + private const string CompiledAvaloniaXamlNamespace = "CompiledAvaloniaXaml"; + static bool CheckXamlName(IResource r) => r.Name.ToLowerInvariant().EndsWith(".xaml") || r.Name.ToLowerInvariant().EndsWith(".paml") || r.Name.ToLowerInvariant().EndsWith(".axaml"); @@ -37,43 +39,58 @@ namespace Avalonia.Build.Tasks } } - public static CompileResult Compile(IBuildEngine engine, string input, string[] references, - string projectDirectory, - string output, bool verifyIl, bool defaultCompileBindings, MessageImportance logImportance, string strongNameKey, + public static CompileResult Compile(IBuildEngine engine, + string input, string output, + string refInput, string refOutput, + string[] references, string projectDirectory, + bool verifyIl, bool defaultCompileBindings, MessageImportance logImportance, string strongNameKey, bool skipXamlCompilation) { - return Compile(engine, input, references, projectDirectory, output, verifyIl, defaultCompileBindings, logImportance, strongNameKey, skipXamlCompilation, debuggerLaunch:false); + return Compile(engine, input, output, refInput, refOutput, references, projectDirectory, verifyIl, defaultCompileBindings, logImportance, strongNameKey, skipXamlCompilation, debuggerLaunch:false); } - internal static CompileResult Compile(IBuildEngine engine, string input, string[] references, - string projectDirectory, - string output, bool verifyIl, bool defaultCompileBindings, MessageImportance logImportance, string strongNameKey, bool skipXamlCompilation, bool debuggerLaunch) + internal static CompileResult Compile(IBuildEngine engine, + string input, string output, + string refInput, string refOutput, + string[] references, string projectDirectory, + bool verifyIl, bool defaultCompileBindings, MessageImportance logImportance, string strongNameKey, bool skipXamlCompilation, bool debuggerLaunch) { try { - var typeSystem = new CecilTypeSystem( - references.Where(r => !r.ToLowerInvariant().EndsWith("avalonia.build.tasks.dll")), - input); - - var asm = typeSystem.TargetAssemblyDefinition; - - if (!skipXamlCompilation) - { - var compileRes = CompileCore(engine, typeSystem, projectDirectory, verifyIl, defaultCompileBindings, - logImportance, debuggerLaunch); - if (compileRes == null) - return new CompileResult(true); - if (compileRes == false) - return new CompileResult(false); - } - - var writerParameters = new WriterParameters { WriteSymbols = asm.MainModule.HasSymbols }; - if (!string.IsNullOrWhiteSpace(strongNameKey)) - writerParameters.StrongNameKeyBlob = File.ReadAllBytes(strongNameKey); - - asm.Write(output, writerParameters); - - return new CompileResult(true, true); + references = references.Where(r => !r.ToLowerInvariant().EndsWith("avalonia.build.tasks.dll")).ToArray(); + var typeSystem = new CecilTypeSystem(references, input); + var refTypeSystem = !string.IsNullOrWhiteSpace(refInput) && File.Exists(refInput) ? new CecilTypeSystem(references, refInput) : null; + + var asm = typeSystem.TargetAssemblyDefinition; + var refAsm = refTypeSystem?.TargetAssemblyDefinition; + if (!skipXamlCompilation) + { + var compileRes = CompileCore(engine, typeSystem, projectDirectory, verifyIl, defaultCompileBindings, logImportance, debuggerLaunch); + if (compileRes == null) + return new CompileResult(true); + if (compileRes == false) + return new CompileResult(false); + + if (refTypeSystem is not null) + { + var refCompileRes = CompileCoreForRefAssembly(engine, typeSystem, refTypeSystem); + if (refCompileRes == false) + return new CompileResult(false); + } + } + + var writerParameters = new WriterParameters { WriteSymbols = asm.MainModule.HasSymbols }; + if (!string.IsNullOrWhiteSpace(strongNameKey)) + writerParameters.StrongNameKeyBlob = File.ReadAllBytes(strongNameKey); + + asm.Write(output, writerParameters); + + var refWriterParameters = new WriterParameters { WriteSymbols = false }; + if (!string.IsNullOrWhiteSpace(strongNameKey)) + writerParameters.StrongNameKeyBlob = File.ReadAllBytes(strongNameKey); + refAsm?.Write(refOutput, refWriterParameters); + + return new CompileResult(true, true); } catch (Exception ex) { @@ -122,6 +139,7 @@ namespace Avalonia.Build.Tasks if (avares.Resources.Count(CheckXamlName) == 0) // Nothing to do return null; + if (typeSystem.FindType("System.Reflection.AssemblyMetadataAttribute") is {} asmMetadata) { var ctor = asm.MainModule.ImportReference(typeSystem.GetTypeReference(asmMetadata).Resolve() @@ -131,14 +149,14 @@ namespace Avalonia.Build.Tasks var arg2 = new CustomAttributeArgument(strType, defaultCompileBindings.ToString()); asm.CustomAttributes.Add(new CustomAttribute(ctor) { ConstructorArguments = { arg1, arg2 } }); } - - var clrPropertiesDef = new TypeDefinition("CompiledAvaloniaXaml", "XamlIlHelpers", + + var clrPropertiesDef = new TypeDefinition(CompiledAvaloniaXamlNamespace, "XamlIlHelpers", TypeAttributes.Class, asm.MainModule.TypeSystem.Object); asm.MainModule.Types.Add(clrPropertiesDef); - var indexerAccessorClosure = new TypeDefinition("CompiledAvaloniaXaml", "!IndexerAccessorFactoryClosure", + var indexerAccessorClosure = new TypeDefinition(CompiledAvaloniaXamlNamespace, "!IndexerAccessorFactoryClosure", TypeAttributes.Class, asm.MainModule.TypeSystem.Object); asm.MainModule.Types.Add(indexerAccessorClosure); - var trampolineBuilder = new TypeDefinition("CompiledAvaloniaXaml", "XamlIlTrampolines", + var trampolineBuilder = new TypeDefinition(CompiledAvaloniaXamlNamespace, "XamlIlTrampolines", TypeAttributes.Class, asm.MainModule.TypeSystem.Object); asm.MainModule.Types.Add(trampolineBuilder); @@ -154,7 +172,7 @@ namespace Avalonia.Build.Tasks new DeterministicIdGenerator()); - var contextDef = new TypeDefinition("CompiledAvaloniaXaml", "XamlIlContext", + var contextDef = new TypeDefinition(CompiledAvaloniaXamlNamespace, "XamlIlContext", TypeAttributes.Class, asm.MainModule.TypeSystem.Object); asm.MainModule.Types.Add(contextDef); @@ -175,8 +193,8 @@ namespace Avalonia.Build.Tasks typeSystem.GetTypeReference(runtimeHelpers).Resolve().Methods .First(x => x.Name == "CreateRootServiceProviderV2")); - var loaderDispatcherDef = new TypeDefinition("CompiledAvaloniaXaml", "!XamlLoader", - TypeAttributes.Class, asm.MainModule.TypeSystem.Object); + var loaderDispatcherDef = new TypeDefinition(CompiledAvaloniaXamlNamespace, "!XamlLoader", + TypeAttributes.Class | TypeAttributes.Public, asm.MainModule.TypeSystem.Object); loaderDispatcherDef.CustomAttributes.Add(new CustomAttribute(editorBrowsableCtor) @@ -204,8 +222,8 @@ namespace Avalonia.Build.Tasks bool CompileGroup(IResourceGroup group) { - var typeDef = new TypeDefinition("CompiledAvaloniaXaml", "!"+ group.Name, - TypeAttributes.Class, asm.MainModule.TypeSystem.Object); + var typeDef = new TypeDefinition(CompiledAvaloniaXamlNamespace, "!"+ group.Name, + TypeAttributes.Class | TypeAttributes.Public, asm.MainModule.TypeSystem.Object); typeDef.CustomAttributes.Add(new CustomAttribute(editorBrowsableCtor) { @@ -213,7 +231,9 @@ namespace Avalonia.Build.Tasks }); asm.MainModule.Types.Add(typeDef); var builder = typeSystem.CreateTypeBuilder(typeDef); - + + var populateMethodsToTransform = new List<(MethodDefinition populateMethod, string resourceFilePath)>(); + foreach (var res in group.Resources.Where(CheckXamlName).OrderBy(x=>x.FilePath.ToLowerInvariant())) { try @@ -279,13 +299,24 @@ namespace Avalonia.Build.Tasks populateBuilder.DefineDelegateSubType(closureName, false, returnType, parameterTypes), res.Uri, res ); - - + + var compiledPopulateMethod = typeSystem.GetTypeReference(populateBuilder).Resolve() + .Methods.First(m => m.Name == populateName); + + // Include populate method and all nested methods/closures used in the populate method, + // So we can replace style/resource includes in all of them. + populateMethodsToTransform.Add((compiledPopulateMethod, res.FilePath)); + populateMethodsToTransform.AddRange(compiledPopulateMethod.Body.Instructions + .Where(b => b.OpCode == OpCodes.Call || b.OpCode == OpCodes.Callvirt || b.OpCode == OpCodes.Ldftn) + .Select(b => b.Operand) + .OfType() + .Where(m => compiledPopulateMethod.DeclaringType.NestedTypes.Contains(m.DeclaringType)) + .Select(m => m.Resolve()) + .Where(m => m.HasBody) + .Select(m => (m, res.FilePath))); + if (classTypeDefinition != null) { - var compiledPopulateMethod = typeSystem.GetTypeReference(populateBuilder).Resolve() - .Methods.First(m => m.Name == populateName); - var designLoaderFieldType = typeSystem .GetType("System.Action`1") .MakeGenericType(typeSystem.GetType("System.Object")); @@ -435,8 +466,94 @@ namespace Avalonia.Build.Tasks } res.Remove(); } - - + + foreach (var (populateMethod, resourceFilePath) in populateMethodsToTransform) + { + foreach (var instruction in populateMethod.Body.Instructions.ToArray()) + { + const string resolveStyleIncludeName = "ResolveStyleInclude"; + const string resolveResourceInclude = "ResolveResourceInclude"; + if (instruction.OpCode == OpCodes.Call + && instruction.Operand is MethodReference + { + Name: resolveStyleIncludeName or resolveResourceInclude, + DeclaringType: { Name: "XamlIlRuntimeHelpers" } + }) + { + int lineNumber = 0, linePosition = 0; + bool instructionsModified = false; + try + { + var assetSource = (string)instruction.Previous.Previous.Previous.Operand; + lineNumber = Convert.ToInt32(instruction.Previous.Previous.Operand); + linePosition = Convert.ToInt32(instruction.Previous.Operand); + + var index = populateMethod.Body.Instructions.IndexOf(instruction); + + assetSource = assetSource.Replace("avares://", ""); + + var assemblyNameSeparator = assetSource.IndexOf('/'); + var fileNameSeparator = assetSource.LastIndexOf('/'); + if (assemblyNameSeparator < 0 || fileNameSeparator < 0) + { + throw new InvalidProgramException( + $"Invalid asset source \"{assetSource}\". It must contain assembly name and relative path."); + } + + var assetAssemblyName = assetSource.Substring(0, assemblyNameSeparator); + var assetAssembly = typeSystem.FindAssembly(assetAssemblyName) + ?? throw new InvalidProgramException($"Unable to resolve assembly \"{assetAssemblyName}\""); + + var fileName = Path.GetFileNameWithoutExtension(assetSource.Replace('/', '.')); + if (assetAssembly.FindType(fileName) is { } type + && type.FindConstructor() is { } ctor) + { + var ctorMethod = + asm.MainModule.ImportReference(typeSystem.GetMethodReference(ctor)); + instructionsModified = true; + populateMethod.Body.Instructions[index - 3] = Instruction.Create(OpCodes.Nop); + populateMethod.Body.Instructions[index - 2] = Instruction.Create(OpCodes.Nop); + populateMethod.Body.Instructions[index - 1] = Instruction.Create(OpCodes.Nop); + populateMethod.Body.Instructions[index] = Instruction.Create(OpCodes.Newobj, ctorMethod); + } + else + { + var resources = assetAssembly.FindType("CompiledAvaloniaXaml.!AvaloniaResources") + ?? throw new InvalidOperationException($"Unable to resolve \"!AvaloniaResources\" type on \"{assetAssemblyName}\" assembly"); + + var relativeName = "Build:" + assetSource.Substring(assemblyNameSeparator); + var buildMethod = resources.FindMethod(m => m.Name == relativeName) + ?? throw new InvalidOperationException($"Unable to resolve build method \"{relativeName}\" resource on the \"{assetAssemblyName}\" assembly"); + + var methodReference = asm.MainModule.ImportReference(typeSystem.GetMethodReference(buildMethod)); + instructionsModified = true; + populateMethod.Body.Instructions[index - 3] = Instruction.Create(OpCodes.Nop); + populateMethod.Body.Instructions[index - 2] = Instruction.Create(OpCodes.Nop); + populateMethod.Body.Instructions[index - 1] = Instruction.Create(OpCodes.Call, createRootServiceProviderMethod); + populateMethod.Body.Instructions[index] = Instruction.Create(OpCodes.Call, methodReference); + } + } + catch (Exception e) + { + if (instructionsModified) + { + engine.LogErrorEvent(new BuildErrorEventArgs("Avalonia", "XAMLIL", resourceFilePath, + lineNumber, linePosition, lineNumber, linePosition, + e.Message, "", "Avalonia")); + return false; + } + else + { + engine.LogWarningEvent(new BuildWarningEventArgs("Avalonia", "XAMLIL", + resourceFilePath, + lineNumber, linePosition, lineNumber, linePosition, + e.Message, "", "Avalonia")); + } + } + } + } + } + // Technically that's a hack, but it fixes corert incompatibility caused by deterministic builds int dupeCounter = 1; foreach (var grp in typeDef.NestedTypes.GroupBy(x => x.Name)) @@ -463,6 +580,55 @@ namespace Avalonia.Build.Tasks loaderDispatcherMethod.Body.Instructions.Add(Instruction.Create(OpCodes.Ret)); return true; } - + + static bool? CompileCoreForRefAssembly( + IBuildEngine engine, CecilTypeSystem sourceTypeSystem, CecilTypeSystem refTypeSystem) + { + var asm = refTypeSystem.TargetAssemblyDefinition; + + var compiledTypes = sourceTypeSystem.TargetAssemblyDefinition.MainModule.Types + .Where(t => t.Namespace.StartsWith(CompiledAvaloniaXamlNamespace) && t.IsPublic).ToArray(); + if (compiledTypes.Length == 0) + { + return null; + } + + try + { + foreach (var ogType in compiledTypes) + { + var wrappedOgType = sourceTypeSystem.TargetAssembly.FindType(ogType.FullName); + + var clrPropertiesDef = new TypeDefinition(ogType.Namespace, ogType.Name, + TypeAttributes.Class | TypeAttributes.Public, asm.MainModule.TypeSystem.Object); + asm.MainModule.Types.Add(clrPropertiesDef); + foreach (var attribute in ogType.CustomAttributes) + { + var method = asm.MainModule.ImportReference(attribute.Constructor); + clrPropertiesDef.CustomAttributes.Add(new CustomAttribute(method, attribute.GetBlob())); + } + + var typeBuilder = refTypeSystem.CreateTypeBuilder(clrPropertiesDef); + foreach (var ogMethod in wrappedOgType.Methods.Where(m => m.IsPublic && m.IsStatic)) + { + var method = typeBuilder.DefineMethod(ogMethod.ReturnType, ogMethod.Parameters, ogMethod.Name, + ogMethod.IsPublic, ogMethod.IsStatic, false); + method.Generator.Ldnull(); + method.Generator.Throw(); + } + + typeBuilder.CreateType(); + } + } + catch (Exception e) + { + engine.LogErrorEvent(new BuildErrorEventArgs("Avalonia", "XAMLIL", "", + 0, 0, 0, 0, + e.Message, "", "Avalonia")); + return false; + } + + return true; + } } } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index 1692238d06..e601701d5c 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -56,7 +56,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions new AvaloniaXamlIlSetterTransformer(), new AvaloniaXamlIlConstructorServiceProviderTransformer(), new AvaloniaXamlIlTransitionsTypeMetadataTransformer(), - new AvaloniaXamlIlResolveByNameMarkupExtensionReplacer() + new AvaloniaXamlIlResolveByNameMarkupExtensionReplacer(), + new AvaloniaXamlIlAssetIncludeTransformer() ); InsertBefore( new AvaloniaXamlIlOptionMarkupExtensionTransformer()); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlAssetIncludeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlAssetIncludeTransformer.cs new file mode 100644 index 0000000000..fa878ce72c --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlAssetIncludeTransformer.cs @@ -0,0 +1,94 @@ +using System.Linq; +using XamlX; +using XamlX.Ast; +using XamlX.Emit; +using XamlX.IL; +using XamlX.Transform; +using XamlX.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; + +internal class AvaloniaXamlIlAssetIncludeTransformer : IXamlAstTransformer +{ + private const string StyleIncludeName = "StyleInclude"; + private const string ResourceIncludeName = "ResourceInclude"; + + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (node is not XamlAstObjectNode objectNode + || objectNode.Type.GetClrType() is not {Name: StyleIncludeName or ResourceIncludeName} objectNodeType) + { + return node; + } + + var sourceProperty = objectNode.Children.OfType().FirstOrDefault(n => n.Property.GetClrProperty().Name == "Source"); + var directives = objectNode.Children.OfType().ToList(); + if (sourceProperty is null + || objectNode.Children.Count != (directives.Count + 1)) + { + // Don't transform node with any other property, as we don't know how to transform them. + return node; + } + + if (sourceProperty.Values.OfType().FirstOrDefault() is not { } sourceTextNode) + { + // Source value can be set with markup extension instead of a text node, we don't support it here yet. + return node; + } + + var originalAssetPath = sourceTextNode.Text; + if (!(originalAssetPath.StartsWith("avares://") || originalAssetPath.StartsWith("/"))) + { + // Only "avares" protocol supported or relative paths. + return node; + } + + var runtimeHelpers = context.Configuration.TypeSystem.FindType("Avalonia.Markup.Xaml.XamlIl.Runtime.XamlIlRuntimeHelpers"); + var markerMethodName = "Resolve" + objectNodeType.Name; + var markerMethod = runtimeHelpers.FindMethod(m => m.Name == markerMethodName && m.Parameters.Count == 3); + if (markerMethod is null) + { + throw new XamlParseException($"Marker method \"{markerMethodName}\" was not found for the \"{objectNodeType.Name}\" node", node); + } + + return new XamlValueWithManipulationNode( + node, + new AssetIncludeMethodNode(node, markerMethod, originalAssetPath), + new XamlManipulationGroupNode(node, directives)); + } + + private class AssetIncludeMethodNode : XamlAstNode, IXamlAstValueNode, IXamlAstILEmitableNode + { + private readonly IXamlMethod _method; + private readonly string _originalAssetPath; + + public AssetIncludeMethodNode( + IXamlAstNode original, IXamlMethod method, string originalAssetPath) + : base(original) + { + _method = method; + _originalAssetPath = originalAssetPath; + } + + public IXamlAstTypeReference Type => new XamlAstClrTypeReference(this, _method.ReturnType, false); + + public XamlILNodeEmitResult Emit(XamlEmitContext context, IXamlILEmitter codeGen) + { + var absoluteSource = _originalAssetPath; + if (absoluteSource.StartsWith("/")) + { + // Avoid Uri class here to avoid potential problems with escaping. + // Keeping string as close to the original as possible. + var absoluteBaseUrl = context.RuntimeContext.BaseUrl; + absoluteSource = absoluteBaseUrl.Substring(0, absoluteBaseUrl.LastIndexOf('/')) + absoluteSource; + } + + codeGen.Ldstr(absoluteSource); + codeGen.Ldc_I4(Line); + codeGen.Ldc_I4(Position); + codeGen.EmitCall(_method); + + return XamlILNodeEmitResult.Type(0, _method.ReturnType); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index bf8427a129..b5a9326a7d 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -102,6 +102,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType TextDecorations { get; } public IXamlType TextTrimming { get; } public IXamlType ISetter { get; } + public IXamlType IStyle { get; } public IXamlType IResourceDictionary { get; } public IXamlType ResourceDictionary { get; } public IXamlMethod ResourceDictionaryDeferredAdd { get; } @@ -232,6 +233,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers TextDecorations = cfg.TypeSystem.GetType("Avalonia.Media.TextDecorations"); TextTrimming = cfg.TypeSystem.GetType("Avalonia.Media.TextTrimming"); ISetter = cfg.TypeSystem.GetType("Avalonia.Styling.ISetter"); + IStyle = cfg.TypeSystem.GetType("Avalonia.Styling.IStyle"); IResourceDictionary = cfg.TypeSystem.GetType("Avalonia.Controls.IResourceDictionary"); ResourceDictionary = cfg.TypeSystem.GetType("Avalonia.Controls.ResourceDictionary"); ResourceDictionaryDeferredAdd = ResourceDictionary.FindMethod("AddDeferred", XamlIlTypes.Void, true, XamlIlTypes.Object, diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs index 6682532455..f547888d6e 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs @@ -5,7 +5,10 @@ using System.Reflection; using Avalonia.Controls; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.Markup.Xaml.MarkupExtensions; +using Avalonia.Markup.Xaml.Styling; using Avalonia.Platform; +using Avalonia.Styling; // ReSharper disable UnusedMember.Global // ReSharper disable UnusedParameter.Global @@ -14,6 +17,16 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime { public static class XamlIlRuntimeHelpers { + public static IStyle ResolveStyleInclude(string absoluteSource, int line, int position) + { + return new StyleInclude((Uri)null) { Source = new Uri(absoluteSource) }.Loaded; + } + + public static IResourceDictionary ResolveResourceInclude(string absoluteSource, int line, int position) + { + return new ResourceInclude((Uri)null) { Source = new Uri(absoluteSource) }.Loaded; + } + public static Func DeferredTransformationFactoryV1(Func builder, IServiceProvider provider) { diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj index 7ff19e1049..bcb4bac457 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj @@ -22,12 +22,12 @@ - + Designer - - + + Designer - + diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs index af2435a52f..2a2e5f2478 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs @@ -473,13 +473,9 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.True(styles.Count == 1); - var styleInclude = styles.First() as StyleInclude; + var styleInclude = styles.First() as IStyle; Assert.NotNull(styleInclude); - - var style = styleInclude.Loaded; - - Assert.NotNull(style); } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 682fc622b8..e67dcfdc83 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -113,27 +113,41 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml public void StyleInclude_Is_Built() { using (UnitTestApplication.Start(TestServices.StyledWindow - .With(theme: () => new Styles()))) + .With(theme: () => new Styles()))) { var xaml = @" - + "; var window = AvaloniaRuntimeXamlLoader.Parse(xaml); + + Assert.IsType + diff --git a/src/Avalonia.Themes.Fluent/Accents/Base.xaml b/src/Avalonia.Themes.Fluent/Accents/Base.xaml index 1e2acf736d..259d107b5c 100644 --- a/src/Avalonia.Themes.Fluent/Accents/Base.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/Base.xaml @@ -1,8 +1,7 @@ - + diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml index 915be08e53..89841c92c0 100644 --- a/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml @@ -1,7 +1,5 @@ - + 12,0,12,0 + diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml index e5c0babb80..89b646fb52 100644 --- a/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml @@ -1,7 +1,5 @@ - + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml index 7e3c8673f5..71a8bc3a3c 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml @@ -1,7 +1,5 @@ - + + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml index 7917315e19..d764e1616c 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml @@ -1,7 +1,5 @@ - + diff --git a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj index 35603fe216..597fab22f8 100644 --- a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj +++ b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index 5383aa3180..a029be6b8d 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -1,5 +1,4 @@  diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml.cs b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml.cs deleted file mode 100644 index 37822f5c8c..0000000000 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Avalonia.Markup.Xaml; -using Avalonia.Styling; - -namespace Avalonia.Themes.Fluent.Controls -{ - /// - /// The default Avalonia theme. - /// - public class FluentControls : Styles - { - } -} diff --git a/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml b/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml index 1eb0493b08..5c48c297b8 100644 --- a/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/NativeMenuBar.xaml @@ -1,13 +1,12 @@ - diff --git a/src/Avalonia.Themes.Fluent/DensityStyles/Compact.xaml b/src/Avalonia.Themes.Fluent/DensityStyles/Compact.xaml index 3b9c5038e6..b96671b778 100644 --- a/src/Avalonia.Themes.Fluent/DensityStyles/Compact.xaml +++ b/src/Avalonia.Themes.Fluent/DensityStyles/Compact.xaml @@ -3,21 +3,19 @@ - + + 14 + 14 + 24 + 2,2,6,1 + 32 + 24 + 0,1,0,2 + 0,1,0,2 + 9,0,0,1 + 10,0,30,0 + 24 + 12,1,0,3 + 32 + diff --git a/src/Avalonia.Themes.Fluent/FluentDark.xaml b/src/Avalonia.Themes.Fluent/FluentDark.xaml deleted file mode 100644 index aad71b18fa..0000000000 --- a/src/Avalonia.Themes.Fluent/FluentDark.xaml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - diff --git a/src/Avalonia.Themes.Fluent/FluentLight.xaml b/src/Avalonia.Themes.Fluent/FluentLight.xaml deleted file mode 100644 index 907efe7ee6..0000000000 --- a/src/Avalonia.Themes.Fluent/FluentLight.xaml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.cs b/src/Avalonia.Themes.Fluent/FluentTheme.cs deleted file mode 100644 index 79dd81a068..0000000000 --- a/src/Avalonia.Themes.Fluent/FluentTheme.cs +++ /dev/null @@ -1,244 +0,0 @@ -using System; -using System.Collections.Generic; -using Avalonia.Controls; -using Avalonia.Markup.Xaml; -using Avalonia.Markup.Xaml.Styling; -using Avalonia.Styling; - -#nullable enable - -namespace Avalonia.Themes.Fluent -{ - public enum FluentThemeMode - { - Light, - Dark, - } - - public enum DensityStyle - { - Normal, - Compact - } - - /// - /// Includes the fluent theme in an application. - /// - public class FluentTheme : AvaloniaObject, IStyle, IResourceProvider - { - private readonly Uri _baseUri; - private Styles _fluentDark = new(); - private Styles _fluentLight = new(); - private Styles _sharedStyles = new(); - private Styles _densityStyles = new(); - private bool _isLoading; - private IStyle? _loaded; - - /// - /// Initializes a new instance of the class. - /// - /// The base URL for the XAML context. - public FluentTheme(Uri baseUri) - { - _baseUri = baseUri; - InitStyles(baseUri); - } - - /// - /// Initializes a new instance of the class. - /// - /// The XAML service provider. - public FluentTheme(IServiceProvider serviceProvider) - { - var ctx = serviceProvider.GetService(typeof(IUriContext)) as IUriContext - ?? throw new NullReferenceException("Unable retrive UriContext"); - _baseUri = ctx.BaseUri; - InitStyles(_baseUri); - } - - public static readonly StyledProperty ModeProperty = - AvaloniaProperty.Register(nameof(Mode)); - - public static readonly StyledProperty DensityStyleProperty = - AvaloniaProperty.Register(nameof(DensityStyle)); - - /// - /// Gets or sets the mode of the fluent theme (light, dark). - /// - public FluentThemeMode Mode - { - get => GetValue(ModeProperty); - set => SetValue(ModeProperty, value); - } - - /// - /// Gets or sets the density style of the fluent theme (normal, compact). - /// - public DensityStyle DensityStyle - { - get => GetValue(DensityStyleProperty); - set => SetValue(DensityStyleProperty, value); - } - - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - base.OnPropertyChanged(change); - - if (_loaded is null) - { - // If style wasn't yet loaded, no need to change children styles, - // it will be applied later in Loaded getter. - return; - } - - if (change.Property == ModeProperty) - { - if (Mode == FluentThemeMode.Dark) - { - (Loaded as Styles)![1] = _fluentDark[0]; - (Loaded as Styles)![2] = _fluentDark[1]; - } - else - { - (Loaded as Styles)![1] = _fluentLight[0]; - (Loaded as Styles)![2] = _fluentLight[1]; - } - } - - if (change.Property == DensityStyleProperty) - { - if (DensityStyle == DensityStyle.Compact) - { - (Loaded as Styles)!.Add(_densityStyles[0]); - } - else if (DensityStyle == DensityStyle.Normal) - { - (Loaded as Styles)!.Remove(_densityStyles[0]); - } - } - } - - public IResourceHost? Owner => (Loaded as IResourceProvider)?.Owner; - - /// - /// Gets the loaded style. - /// - public IStyle Loaded - { - get - { - if (_loaded == null) - { - _isLoading = true; - - if (Mode == FluentThemeMode.Light) - { - _loaded = new Styles() { _sharedStyles , _fluentLight[0], _fluentLight[1] }; - } - else if (Mode == FluentThemeMode.Dark) - { - _loaded = new Styles() { _sharedStyles, _fluentDark[0], _fluentDark[1] }; - } - - if (DensityStyle == DensityStyle.Compact) - { - (_loaded as Styles)!.Add(_densityStyles[0]); - } - - _isLoading = false; - } - - return _loaded!; - } - } - - bool IResourceNode.HasResources => (Loaded as IResourceProvider)?.HasResources ?? false; - - IReadOnlyList IStyle.Children => _loaded?.Children ?? Array.Empty(); - - public event EventHandler? OwnerChanged - { - add - { - if (Loaded is IResourceProvider rp) - { - rp.OwnerChanged += value; - } - } - remove - { - if (Loaded is IResourceProvider rp) - { - rp.OwnerChanged -= value; - } - } - } - - public SelectorMatchResult TryAttach(IStyleable target, object? host) => Loaded.TryAttach(target, host); - - public bool TryGetResource(object key, out object? value) - { - if (!_isLoading && Loaded is IResourceProvider p) - { - return p.TryGetResource(key, out value); - } - - value = null; - return false; - } - - void IResourceProvider.AddOwner(IResourceHost owner) => (Loaded as IResourceProvider)?.AddOwner(owner); - void IResourceProvider.RemoveOwner(IResourceHost owner) => (Loaded as IResourceProvider)?.RemoveOwner(owner); - - private void InitStyles(Uri baseUri) - { - _sharedStyles = new Styles - { - new StyleInclude(baseUri) - { - Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/AccentColors.xaml") - }, - new StyleInclude(baseUri) - { - Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/Base.xaml") - }, - new StyleInclude(baseUri) - { - Source = new Uri("avares://Avalonia.Themes.Fluent/Controls/FluentControls.xaml") - } - }; - - _fluentLight = new Styles - { - new StyleInclude(baseUri) - { - Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/BaseLight.xaml") - }, - new StyleInclude(baseUri) - { - Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml") - } - }; - - _fluentDark = new Styles - { - new StyleInclude(baseUri) - { - Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/BaseDark.xaml") - }, - new StyleInclude(baseUri) - { - Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml") - } - }; - - _densityStyles = new Styles - { - new StyleInclude(baseUri) - { - Source = new Uri("avares://Avalonia.Themes.Fluent/DensityStyles/Compact.xaml") - } - }; - } - } -} diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml b/src/Avalonia.Themes.Fluent/FluentTheme.xaml new file mode 100644 index 0000000000..1d710bb9dc --- /dev/null +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs b/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs new file mode 100644 index 0000000000..728e81b198 --- /dev/null +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml.cs @@ -0,0 +1,124 @@ +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Styling; + +namespace Avalonia.Themes.Fluent +{ + public enum FluentThemeMode + { + Light, + Dark, + } + + public enum DensityStyle + { + Normal, + Compact + } + + /// + /// Includes the fluent theme in an application. + /// + public class FluentTheme : Styles + { + private readonly IResourceDictionary _baseDark; + private readonly IResourceDictionary _fluentDark; + private readonly IResourceDictionary _baseLight; + private readonly IResourceDictionary _fluentLight; + private readonly Styles _compactStyles; + + /// + /// Initializes a new instance of the class. + /// + public FluentTheme() + { + AvaloniaXamlLoader.Load(this); + + _baseDark = (IResourceDictionary)GetAndRemove("BaseDark"); + _fluentDark = (IResourceDictionary)GetAndRemove("FluentDark"); + _baseLight = (IResourceDictionary)GetAndRemove("BaseLight"); + _fluentLight = (IResourceDictionary)GetAndRemove("FluentLight"); + _compactStyles = (Styles)GetAndRemove("CompactStyles"); + + EnsureThemeVariants(); + EnsureCompactStyles(); + + object GetAndRemove(string key) + { + var val = Resources[key] + ?? throw new KeyNotFoundException($"Key {key} was not found in the resources"); + Resources.Remove(key); + return val; + } + } + + public static readonly StyledProperty ModeProperty = + AvaloniaProperty.Register(nameof(Mode)); + + public static readonly StyledProperty DensityStyleProperty = + AvaloniaProperty.Register(nameof(DensityStyle)); + + /// + /// Gets or sets the mode of the fluent theme (light, dark). + /// + public FluentThemeMode Mode + { + get => GetValue(ModeProperty); + set => SetValue(ModeProperty, value); + } + + /// + /// Gets or sets the density style of the fluent theme (normal, compact). + /// + public DensityStyle DensityStyle + { + get => GetValue(DensityStyleProperty); + set => SetValue(DensityStyleProperty, value); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ModeProperty) + { + EnsureThemeVariants(); + } + + if (change.Property == DensityStyleProperty) + { + EnsureCompactStyles(); + } + } + + private void EnsureThemeVariants() + { + var themeVariantResource1 = Mode == FluentThemeMode.Dark ? _baseDark : _baseLight; + var themeVariantResource2 = Mode == FluentThemeMode.Dark ? _fluentDark : _fluentLight; + var dict = Resources.MergedDictionaries; + if (dict.Count == 2) + { + dict.Insert(1, themeVariantResource1); + dict.Add(themeVariantResource2); + } + else + { + dict[1] = themeVariantResource1; + dict[3] = themeVariantResource2; + } + } + + private void EnsureCompactStyles() + { + if (DensityStyle == DensityStyle.Compact) + { + Add(_compactStyles); + } + else + { + Remove(_compactStyles); + } + } + } +} diff --git a/src/Avalonia.Themes.Fluent/IBitmapToImageConverter.cs b/src/Avalonia.Themes.Fluent/IBitmapToImageConverter.cs index 34670882f8..e6c46122fc 100644 --- a/src/Avalonia.Themes.Fluent/IBitmapToImageConverter.cs +++ b/src/Avalonia.Themes.Fluent/IBitmapToImageConverter.cs @@ -1,9 +1,5 @@ using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Data.Converters; using Avalonia.Media.Imaging; @@ -12,7 +8,7 @@ namespace Avalonia.Themes.Fluent { internal class IBitmapToImageConverter : IValueConverter { - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { if (value != null && value is IBitmap bm) return new Image { Source=bm }; @@ -20,7 +16,7 @@ namespace Avalonia.Themes.Fluent return null; } - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) { throw new NotImplementedException(); } diff --git a/src/Avalonia.Themes.Fluent/InverseBooleanValueConverter.cs b/src/Avalonia.Themes.Fluent/InverseBooleanValueConverter.cs deleted file mode 100644 index 20bade2a67..0000000000 --- a/src/Avalonia.Themes.Fluent/InverseBooleanValueConverter.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Globalization; -using Avalonia.Data.Converters; - -namespace Avalonia.Themes.Fluent -{ - class InverseBooleanValueConverter : IValueConverter - { - public bool Default { get; set; } - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) - { - return value is bool b ? !b : Default; - } - - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) - { - return value is bool b ? !b : !Default; - } - } -} diff --git a/src/Avalonia.Themes.Simple/Accents/Base.xaml b/src/Avalonia.Themes.Simple/Accents/Base.xaml index 1f7d703b54..bffdbd8a27 100644 --- a/src/Avalonia.Themes.Simple/Accents/Base.xaml +++ b/src/Avalonia.Themes.Simple/Accents/Base.xaml @@ -1,8 +1,7 @@ - + diff --git a/src/Avalonia.Themes.Simple/Accents/BaseDark.xaml b/src/Avalonia.Themes.Simple/Accents/BaseDark.xaml index 1843abebfd..9ad9f70c98 100644 --- a/src/Avalonia.Themes.Simple/Accents/BaseDark.xaml +++ b/src/Avalonia.Themes.Simple/Accents/BaseDark.xaml @@ -1,8 +1,6 @@ - + diff --git a/src/Avalonia.Themes.Simple/Accents/BaseLight.xaml b/src/Avalonia.Themes.Simple/Accents/BaseLight.xaml index 6247815303..f96425cf06 100644 --- a/src/Avalonia.Themes.Simple/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Simple/Accents/BaseLight.xaml @@ -1,8 +1,6 @@ - + diff --git a/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj b/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj index 40ed4a0f87..e614dad4d9 100644 --- a/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj +++ b/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj @@ -9,6 +9,7 @@ + diff --git a/src/Avalonia.Themes.Simple/Controls/NativeMenuBar.xaml b/src/Avalonia.Themes.Simple/Controls/NativeMenuBar.xaml index 160acd5872..ba1b35e2ee 100644 --- a/src/Avalonia.Themes.Simple/Controls/NativeMenuBar.xaml +++ b/src/Avalonia.Themes.Simple/Controls/NativeMenuBar.xaml @@ -1,14 +1,12 @@ - - diff --git a/src/Avalonia.Themes.Simple/IBitmapToImageConverter.cs b/src/Avalonia.Themes.Simple/IBitmapToImageConverter.cs index fade026b51..d5bf003288 100644 --- a/src/Avalonia.Themes.Simple/IBitmapToImageConverter.cs +++ b/src/Avalonia.Themes.Simple/IBitmapToImageConverter.cs @@ -12,7 +12,7 @@ namespace Avalonia.Themes.Simple { internal class IBitmapToImageConverter : IValueConverter { - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { if (value != null && value is IBitmap bm) return new Image { Source=bm }; @@ -20,7 +20,7 @@ namespace Avalonia.Themes.Simple return null; } - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) { throw new NotImplementedException(); } diff --git a/src/Avalonia.Themes.Simple/InverseBooleanValueConverter.cs b/src/Avalonia.Themes.Simple/InverseBooleanValueConverter.cs deleted file mode 100644 index 15cc5b4a80..0000000000 --- a/src/Avalonia.Themes.Simple/InverseBooleanValueConverter.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Globalization; -using Avalonia.Data.Converters; - -namespace Avalonia.Themes.Simple -{ - class InverseBooleanValueConverter : IValueConverter - { - public bool Default { get; set; } - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) - { - return value is bool b ? !b : Default; - } - - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) - { - return value is bool b ? !b : !Default; - } - } -} diff --git a/src/Avalonia.Themes.Simple/SimpleTheme.cs b/src/Avalonia.Themes.Simple/SimpleTheme.cs deleted file mode 100644 index f225cb4c4d..0000000000 --- a/src/Avalonia.Themes.Simple/SimpleTheme.cs +++ /dev/null @@ -1,166 +0,0 @@ -using System; -using System.Collections.Generic; -using Avalonia.Controls; -using Avalonia.Markup.Xaml; -using Avalonia.Markup.Xaml.Styling; -using Avalonia.Styling; -#nullable enable - -namespace Avalonia.Themes.Simple -{ - public class SimpleTheme : AvaloniaObject, IStyle, IResourceProvider - { - public static readonly StyledProperty ModeProperty = - AvaloniaProperty.Register(nameof(Mode)); - - private readonly Uri _baseUri; - private bool _isLoading; - private IStyle? _loaded; - private Styles _sharedStyles = new(); - private Styles _simpleDark = new(); - private Styles _simpleLight = new(); - /// - /// Initializes a new instance of the class. - /// - /// The base URL for the XAML context. - public SimpleTheme(Uri? baseUri = null) - { - _baseUri = baseUri ?? new Uri("avares://Avalonia.Themes.Simple/"); - InitStyles(_baseUri); - } - - /// - /// Initializes a new instance of the class. - /// - /// The XAML service provider. - public SimpleTheme(IServiceProvider serviceProvider) - { - var service = serviceProvider.GetService(typeof(IUriContext)); - if (service == null) - { - throw new Exception("There is no service object of type IUriContext!"); - } - _baseUri = ((IUriContext)service).BaseUri; - InitStyles(_baseUri); - } - - public event EventHandler? OwnerChanged - { - add - { - if (Loaded is IResourceProvider rp) - { - rp.OwnerChanged += value; - } - } - remove - { - if (Loaded is IResourceProvider rp) - { - rp.OwnerChanged -= value; - } - } - } - - IReadOnlyList IStyle.Children => _loaded?.Children ?? Array.Empty(); - - bool IResourceNode.HasResources => (Loaded as IResourceProvider)?.HasResources ?? false; - - public IStyle Loaded - { - get - { - if (_loaded == null) - { - _isLoading = true; - - if (Mode == SimpleThemeMode.Light) - { - _loaded = new Styles { _sharedStyles, _simpleLight }; - } - else if (Mode == SimpleThemeMode.Dark) - { - _loaded = new Styles { _sharedStyles, _simpleDark }; - } - _isLoading = false; - } - - return _loaded!; - } - } - - /// - /// Gets or sets the mode of the fluent theme (light, dark). - /// - public SimpleThemeMode Mode - { - get => GetValue(ModeProperty); - set => SetValue(ModeProperty, value); - } - public IResourceHost? Owner => (Loaded as IResourceProvider)?.Owner; - - void IResourceProvider.AddOwner(IResourceHost owner) => (Loaded as IResourceProvider)?.AddOwner(owner); - - void IResourceProvider.RemoveOwner(IResourceHost owner) => (Loaded as IResourceProvider)?.RemoveOwner(owner); - - public SelectorMatchResult TryAttach(IStyleable target, object? host) => Loaded.TryAttach(target, host); - - public bool TryGetResource(object key, out object? value) - { - if (!_isLoading && Loaded is IResourceProvider p) - { - return p.TryGetResource(key, out value); - } - - value = null; - return false; - } - - protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) - { - base.OnPropertyChanged(change); - if (change.Property == ModeProperty) - { - if (Mode == SimpleThemeMode.Dark) - { - (Loaded as Styles)![1] = _simpleDark[0]; - } - else - { - (Loaded as Styles)![1] = _simpleLight[0]; - } - } - } - - private void InitStyles(Uri baseUri) - { - _sharedStyles = new Styles - { - new StyleInclude(baseUri) - { - Source = new Uri("avares://Avalonia.Themes.Simple/Controls/SimpleControls.xaml") - }, - new StyleInclude(baseUri) - { - Source = new Uri("avares://Avalonia.Themes.Simple/Accents/Base.xaml") - } - }; - _simpleLight = new Styles - { - new StyleInclude(baseUri) - { - Source = new Uri("avares://Avalonia.Themes.Simple/Accents/BaseLight.xaml") - } - }; - - _simpleDark = new Styles - { - new StyleInclude(baseUri) - { - Source = new Uri("avares://Avalonia.Themes.Simple/Accents/BaseDark.xaml") - } - }; - } - - } -} diff --git a/src/Avalonia.Themes.Simple/SimpleTheme.xaml b/src/Avalonia.Themes.Simple/SimpleTheme.xaml new file mode 100644 index 0000000000..c8bf748595 --- /dev/null +++ b/src/Avalonia.Themes.Simple/SimpleTheme.xaml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Simple/SimpleTheme.xaml.cs b/src/Avalonia.Themes.Simple/SimpleTheme.xaml.cs new file mode 100644 index 0000000000..af9d305043 --- /dev/null +++ b/src/Avalonia.Themes.Simple/SimpleTheme.xaml.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Styling; + +namespace Avalonia.Themes.Simple +{ + public class SimpleTheme : Styles + { + public static readonly StyledProperty ModeProperty = + AvaloniaProperty.Register(nameof(Mode)); + + private readonly IResourceDictionary _simpleDark; + private readonly IResourceDictionary _simpleLight; + + /// + /// Initializes a new instance of the class. + /// + public SimpleTheme() + { + AvaloniaXamlLoader.Load(this); + + _simpleDark = (IResourceDictionary)GetAndRemove("BaseDark"); + _simpleLight = (IResourceDictionary)GetAndRemove("BaseLight"); + EnsureThemeVariant(); + + object GetAndRemove(string key) + { + var val = Resources[key] + ?? throw new KeyNotFoundException($"Key {key} was not found in the resources"); + Resources.Remove(key); + return val; + } + } + + /// + /// Gets or sets the mode of the fluent theme (light, dark). + /// + public SimpleThemeMode Mode + { + get => GetValue(ModeProperty); + set => SetValue(ModeProperty, value); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == ModeProperty) + { + EnsureThemeVariant(); + } + } + + private void EnsureThemeVariant() + { + var themeVariantResource = Mode == SimpleThemeMode.Dark ? _simpleDark : _simpleLight; + var dict = Resources.MergedDictionaries; + if (dict.Count == 1) + { + dict.Add(themeVariantResource); + } + else + { + dict[1] = themeVariantResource; + } + } + } +} diff --git a/tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs b/tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs index ceb015ee63..ae874b8a61 100644 --- a/tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs @@ -61,7 +61,7 @@ namespace Avalonia.Benchmarks.Themes AssetLoader.RegisterResUriParsers(); return new Styles { - new Avalonia.Themes.Fluent.FluentTheme(new Uri("avares://Avalonia.Benchmarks")) + new Avalonia.Themes.Fluent.FluentTheme() { } diff --git a/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs b/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs index 86ba4c6005..e82576c7d9 100644 --- a/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Themes/ThemeBenchmark.cs @@ -31,7 +31,7 @@ namespace Avalonia.Benchmarks.Themes [Arguments(FluentThemeMode.Light)] public bool InitFluentTheme(FluentThemeMode mode) { - UnitTestApplication.Current.Styles[0] = new FluentTheme(new Uri("resm:Styles?assembly=Avalonia.Benchmarks")) + UnitTestApplication.Current.Styles[0] = new FluentTheme() { Mode = mode }; @@ -43,7 +43,7 @@ namespace Avalonia.Benchmarks.Themes [Arguments(SimpleThemeMode.Light)] public bool InitSimpleTheme(SimpleThemeMode mode) { - UnitTestApplication.Current.Styles[0] = new SimpleTheme(new Uri("resm:Styles?assembly=Avalonia.Benchmarks")) + UnitTestApplication.Current.Styles[0] = new SimpleTheme() { Mode = mode }; From 4043c6aa665c38d0ee4b6564b9ec5743ee4d4c8e Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 16 Nov 2022 21:44:32 -0500 Subject: [PATCH 4/6] Minor formatting issues --- .../XamlCompilerTaskExecutor.cs | 40 +++++++++---------- src/Avalonia.Themes.Fluent/FluentTheme.xaml | 2 +- src/Avalonia.Themes.Simple/SimpleTheme.xaml | 2 +- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index e68cb0013a..75abfe352f 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -57,40 +57,40 @@ namespace Avalonia.Build.Tasks { try { - references = references.Where(r => !r.ToLowerInvariant().EndsWith("avalonia.build.tasks.dll")).ToArray(); - var typeSystem = new CecilTypeSystem(references, input); - var refTypeSystem = !string.IsNullOrWhiteSpace(refInput) && File.Exists(refInput) ? new CecilTypeSystem(references, refInput) : null; - - var asm = typeSystem.TargetAssemblyDefinition; - var refAsm = refTypeSystem?.TargetAssemblyDefinition; - if (!skipXamlCompilation) - { + references = references.Where(r => !r.ToLowerInvariant().EndsWith("avalonia.build.tasks.dll")).ToArray(); + var typeSystem = new CecilTypeSystem(references, input); + var refTypeSystem = !string.IsNullOrWhiteSpace(refInput) && File.Exists(refInput) ? new CecilTypeSystem(references, refInput) : null; + + var asm = typeSystem.TargetAssemblyDefinition; + var refAsm = refTypeSystem?.TargetAssemblyDefinition; + if (!skipXamlCompilation) + { var compileRes = CompileCore(engine, typeSystem, projectDirectory, verifyIl, defaultCompileBindings, logImportance, debuggerLaunch); if (compileRes == null) return new CompileResult(true); if (compileRes == false) return new CompileResult(false); - + if (refTypeSystem is not null) { var refCompileRes = CompileCoreForRefAssembly(engine, typeSystem, refTypeSystem); if (refCompileRes == false) return new CompileResult(false); } - } + } - var writerParameters = new WriterParameters { WriteSymbols = asm.MainModule.HasSymbols }; - if (!string.IsNullOrWhiteSpace(strongNameKey)) + var writerParameters = new WriterParameters { WriteSymbols = asm.MainModule.HasSymbols }; + if (!string.IsNullOrWhiteSpace(strongNameKey)) writerParameters.StrongNameKeyBlob = File.ReadAllBytes(strongNameKey); - - asm.Write(output, writerParameters); - - var refWriterParameters = new WriterParameters { WriteSymbols = false }; - if (!string.IsNullOrWhiteSpace(strongNameKey)) + + asm.Write(output, writerParameters); + + var refWriterParameters = new WriterParameters { WriteSymbols = false }; + if (!string.IsNullOrWhiteSpace(strongNameKey)) writerParameters.StrongNameKeyBlob = File.ReadAllBytes(strongNameKey); - refAsm?.Write(refOutput, refWriterParameters); - - return new CompileResult(true, true); + refAsm?.Write(refOutput, refWriterParameters); + + return new CompileResult(true, true); } catch (Exception ex) { diff --git a/src/Avalonia.Themes.Fluent/FluentTheme.xaml b/src/Avalonia.Themes.Fluent/FluentTheme.xaml index 1d710bb9dc..d8f8267fe5 100644 --- a/src/Avalonia.Themes.Fluent/FluentTheme.xaml +++ b/src/Avalonia.Themes.Fluent/FluentTheme.xaml @@ -8,7 +8,7 @@ - + diff --git a/src/Avalonia.Themes.Simple/SimpleTheme.xaml b/src/Avalonia.Themes.Simple/SimpleTheme.xaml index c8bf748595..fe296bd288 100644 --- a/src/Avalonia.Themes.Simple/SimpleTheme.xaml +++ b/src/Avalonia.Themes.Simple/SimpleTheme.xaml @@ -7,7 +7,7 @@ - + From cd83f8558f29a39351ef0315ed81bcbe154d1689 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 18 Nov 2022 00:47:23 -0500 Subject: [PATCH 5/6] Changes after the reivew --- .../XamlCompilerTaskExecutor.cs | 194 ++++++++++-------- .../AvaloniaXamlIlAssetIncludeTransformer.cs | 25 +-- .../AvaloniaXamlIlWellKnownTypes.cs | 4 + 3 files changed, 130 insertions(+), 93 deletions(-) diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index 75abfe352f..974f8485c0 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -469,88 +469,9 @@ namespace Avalonia.Build.Tasks foreach (var (populateMethod, resourceFilePath) in populateMethodsToTransform) { - foreach (var instruction in populateMethod.Body.Instructions.ToArray()) + if (!TransformXamlIncludes(engine, typeSystem, populateMethod, resourceFilePath, createRootServiceProviderMethod)) { - const string resolveStyleIncludeName = "ResolveStyleInclude"; - const string resolveResourceInclude = "ResolveResourceInclude"; - if (instruction.OpCode == OpCodes.Call - && instruction.Operand is MethodReference - { - Name: resolveStyleIncludeName or resolveResourceInclude, - DeclaringType: { Name: "XamlIlRuntimeHelpers" } - }) - { - int lineNumber = 0, linePosition = 0; - bool instructionsModified = false; - try - { - var assetSource = (string)instruction.Previous.Previous.Previous.Operand; - lineNumber = Convert.ToInt32(instruction.Previous.Previous.Operand); - linePosition = Convert.ToInt32(instruction.Previous.Operand); - - var index = populateMethod.Body.Instructions.IndexOf(instruction); - - assetSource = assetSource.Replace("avares://", ""); - - var assemblyNameSeparator = assetSource.IndexOf('/'); - var fileNameSeparator = assetSource.LastIndexOf('/'); - if (assemblyNameSeparator < 0 || fileNameSeparator < 0) - { - throw new InvalidProgramException( - $"Invalid asset source \"{assetSource}\". It must contain assembly name and relative path."); - } - - var assetAssemblyName = assetSource.Substring(0, assemblyNameSeparator); - var assetAssembly = typeSystem.FindAssembly(assetAssemblyName) - ?? throw new InvalidProgramException($"Unable to resolve assembly \"{assetAssemblyName}\""); - - var fileName = Path.GetFileNameWithoutExtension(assetSource.Replace('/', '.')); - if (assetAssembly.FindType(fileName) is { } type - && type.FindConstructor() is { } ctor) - { - var ctorMethod = - asm.MainModule.ImportReference(typeSystem.GetMethodReference(ctor)); - instructionsModified = true; - populateMethod.Body.Instructions[index - 3] = Instruction.Create(OpCodes.Nop); - populateMethod.Body.Instructions[index - 2] = Instruction.Create(OpCodes.Nop); - populateMethod.Body.Instructions[index - 1] = Instruction.Create(OpCodes.Nop); - populateMethod.Body.Instructions[index] = Instruction.Create(OpCodes.Newobj, ctorMethod); - } - else - { - var resources = assetAssembly.FindType("CompiledAvaloniaXaml.!AvaloniaResources") - ?? throw new InvalidOperationException($"Unable to resolve \"!AvaloniaResources\" type on \"{assetAssemblyName}\" assembly"); - - var relativeName = "Build:" + assetSource.Substring(assemblyNameSeparator); - var buildMethod = resources.FindMethod(m => m.Name == relativeName) - ?? throw new InvalidOperationException($"Unable to resolve build method \"{relativeName}\" resource on the \"{assetAssemblyName}\" assembly"); - - var methodReference = asm.MainModule.ImportReference(typeSystem.GetMethodReference(buildMethod)); - instructionsModified = true; - populateMethod.Body.Instructions[index - 3] = Instruction.Create(OpCodes.Nop); - populateMethod.Body.Instructions[index - 2] = Instruction.Create(OpCodes.Nop); - populateMethod.Body.Instructions[index - 1] = Instruction.Create(OpCodes.Call, createRootServiceProviderMethod); - populateMethod.Body.Instructions[index] = Instruction.Create(OpCodes.Call, methodReference); - } - } - catch (Exception e) - { - if (instructionsModified) - { - engine.LogErrorEvent(new BuildErrorEventArgs("Avalonia", "XAMLIL", resourceFilePath, - lineNumber, linePosition, lineNumber, linePosition, - e.Message, "", "Avalonia")); - return false; - } - else - { - engine.LogWarningEvent(new BuildWarningEventArgs("Avalonia", "XAMLIL", - resourceFilePath, - lineNumber, linePosition, lineNumber, linePosition, - e.Message, "", "Avalonia")); - } - } - } + return false; } } @@ -630,5 +551,116 @@ namespace Avalonia.Build.Tasks return true; } + + private static bool TransformXamlIncludes( + IBuildEngine engine, CecilTypeSystem typeSystem, + MethodDefinition populateMethod, string resourceFilePath, + MethodReference createRootServiceProviderMethod) + { + var asm = typeSystem.TargetAssemblyDefinition; + foreach (var instruction in populateMethod.Body.Instructions.ToArray()) + { + const string resolveStyleIncludeName = "ResolveStyleInclude"; + const string resolveResourceInclude = "ResolveResourceInclude"; + if (instruction.OpCode == OpCodes.Call + && instruction.Operand is MethodReference + { + Name: resolveStyleIncludeName or resolveResourceInclude, + DeclaringType: { Name: "XamlIlRuntimeHelpers" } + }) + { + int lineNumber = 0, linePosition = 0; + bool instructionsModified = false; + try + { + var assetSource = (string)instruction.Previous.Previous.Previous.Operand; + lineNumber = GetConstValue(instruction.Previous.Previous); + linePosition = GetConstValue(instruction.Previous); + + var index = populateMethod.Body.Instructions.IndexOf(instruction); + + assetSource = assetSource.Replace("avares://", ""); + + var assemblyNameSeparator = assetSource.IndexOf('/'); + var fileNameSeparator = assetSource.LastIndexOf('/'); + if (assemblyNameSeparator < 0 || fileNameSeparator < 0) + { + throw new InvalidProgramException( + $"Invalid asset source \"{assetSource}\". It must contain assembly name and relative path."); + } + + var assetAssemblyName = assetSource.Substring(0, assemblyNameSeparator); + var assetAssembly = typeSystem.FindAssembly(assetAssemblyName) + ?? throw new InvalidProgramException( + $"Unable to resolve assembly \"{assetAssemblyName}\""); + + var fileName = Path.GetFileNameWithoutExtension(assetSource.Replace('/', '.')); + if (assetAssembly.FindType(fileName) is { } type + && type.FindConstructor() is { } ctor) + { + var ctorMethod = + asm.MainModule.ImportReference(typeSystem.GetMethodReference(ctor)); + instructionsModified = true; + populateMethod.Body.Instructions[index - 3] = Instruction.Create(OpCodes.Nop); + populateMethod.Body.Instructions[index - 2] = Instruction.Create(OpCodes.Nop); + populateMethod.Body.Instructions[index - 1] = Instruction.Create(OpCodes.Nop); + populateMethod.Body.Instructions[index] = Instruction.Create(OpCodes.Newobj, ctorMethod); + } + else + { + var resources = assetAssembly.FindType("CompiledAvaloniaXaml.!AvaloniaResources") + ?? throw new InvalidOperationException( + $"Unable to resolve \"!AvaloniaResources\" type on \"{assetAssemblyName}\" assembly"); + + var relativeName = "Build:" + assetSource.Substring(assemblyNameSeparator); + var buildMethod = resources.FindMethod(m => m.Name == relativeName) + ?? throw new InvalidOperationException( + $"Unable to resolve build method \"{relativeName}\" resource on the \"{assetAssemblyName}\" assembly"); + + var methodReference = asm.MainModule.ImportReference(typeSystem.GetMethodReference(buildMethod)); + instructionsModified = true; + populateMethod.Body.Instructions[index - 3] = Instruction.Create(OpCodes.Nop); + populateMethod.Body.Instructions[index - 2] = Instruction.Create(OpCodes.Nop); + populateMethod.Body.Instructions[index - 1] = + Instruction.Create(OpCodes.Call, createRootServiceProviderMethod); + populateMethod.Body.Instructions[index] = Instruction.Create(OpCodes.Call, methodReference); + } + } + catch (Exception e) + { + if (instructionsModified) + { + engine.LogErrorEvent(new BuildErrorEventArgs("Avalonia", "XAMLIL", resourceFilePath, + lineNumber, linePosition, lineNumber, linePosition, + e.Message, "", "Avalonia")); + return false; + } + else + { + engine.LogWarningEvent(new BuildWarningEventArgs("Avalonia", "XAMLIL", + resourceFilePath, + lineNumber, linePosition, lineNumber, linePosition, + e.Message, "", "Avalonia")); + } + } + + static int GetConstValue(Instruction instruction) + { + if (instruction.OpCode is { Code : >= Code.Ldc_I4_0 and <= Code.Ldc_I4_8 }) + { + return instruction.OpCode.Code - Code.Ldc_I4_0; + } + if (instruction.Operand is not null) + { + return Convert.ToInt32(instruction.Operand); + } + + return 0; + } + } + } + + return true; + } } } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlAssetIncludeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlAssetIncludeTransformer.cs index fa878ce72c..1572a4f140 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlAssetIncludeTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlAssetIncludeTransformer.cs @@ -10,28 +10,28 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; internal class AvaloniaXamlIlAssetIncludeTransformer : IXamlAstTransformer { - private const string StyleIncludeName = "StyleInclude"; - private const string ResourceIncludeName = "ResourceInclude"; - public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) { if (node is not XamlAstObjectNode objectNode - || objectNode.Type.GetClrType() is not {Name: StyleIncludeName or ResourceIncludeName} objectNodeType) + || (objectNode.Type.GetClrType() != context.GetAvaloniaTypes().StyleInclude + && objectNode.Type.GetClrType() != context.GetAvaloniaTypes().ResourceInclude)) { return node; } + var nodeTypeName = objectNode.Type.GetClrType().Name; + var sourceProperty = objectNode.Children.OfType().FirstOrDefault(n => n.Property.GetClrProperty().Name == "Source"); var directives = objectNode.Children.OfType().ToList(); if (sourceProperty is null || objectNode.Children.Count != (directives.Count + 1)) { - // Don't transform node with any other property, as we don't know how to transform them. - return node; + throw new XamlParseException($"Unexpected property on the {nodeTypeName} node", node); } if (sourceProperty.Values.OfType().FirstOrDefault() is not { } sourceTextNode) { + // TODO: make it a compiler warning // Source value can be set with markup extension instead of a text node, we don't support it here yet. return node; } @@ -39,18 +39,19 @@ internal class AvaloniaXamlIlAssetIncludeTransformer : IXamlAstTransformer var originalAssetPath = sourceTextNode.Text; if (!(originalAssetPath.StartsWith("avares://") || originalAssetPath.StartsWith("/"))) { - // Only "avares" protocol supported or relative paths. - return node; + throw new XamlParseException( + $"{nodeTypeName}.Source supports only \"avares://\" absolute paths or relative paths starting with \"/\"", + sourceTextNode); } - var runtimeHelpers = context.Configuration.TypeSystem.FindType("Avalonia.Markup.Xaml.XamlIl.Runtime.XamlIlRuntimeHelpers"); - var markerMethodName = "Resolve" + objectNodeType.Name; + var runtimeHelpers = context.GetAvaloniaTypes().RuntimeHelpers; + var markerMethodName = "Resolve" + nodeTypeName; var markerMethod = runtimeHelpers.FindMethod(m => m.Name == markerMethodName && m.Parameters.Count == 3); if (markerMethod is null) { - throw new XamlParseException($"Marker method \"{markerMethodName}\" was not found for the \"{objectNodeType.Name}\" node", node); + throw new XamlParseException($"Marker method \"{markerMethodName}\" was not found for the \"{nodeTypeName}\" node", node); } - + return new XamlValueWithManipulationNode( node, new AssetIncludeMethodNode(node, markerMethod, originalAssetPath), diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index b5a9326a7d..dd37ae6c93 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -103,6 +103,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType TextTrimming { get; } public IXamlType ISetter { get; } public IXamlType IStyle { get; } + public IXamlType StyleInclude { get; } + public IXamlType ResourceInclude { get; } public IXamlType IResourceDictionary { get; } public IXamlType ResourceDictionary { get; } public IXamlMethod ResourceDictionaryDeferredAdd { get; } @@ -234,6 +236,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers TextTrimming = cfg.TypeSystem.GetType("Avalonia.Media.TextTrimming"); ISetter = cfg.TypeSystem.GetType("Avalonia.Styling.ISetter"); IStyle = cfg.TypeSystem.GetType("Avalonia.Styling.IStyle"); + StyleInclude = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Styling.StyleInclude"); + ResourceInclude = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Styling.ResourceInclude"); IResourceDictionary = cfg.TypeSystem.GetType("Avalonia.Controls.IResourceDictionary"); ResourceDictionary = cfg.TypeSystem.GetType("Avalonia.Controls.ResourceDictionary"); ResourceDictionaryDeferredAdd = ResourceDictionary.FindMethod("AddDeferred", XamlIlTypes.Void, true, XamlIlTypes.Object, From 82a6d1431b621fe96a78403e01236e741a70cc33 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 18 Nov 2022 01:34:20 -0500 Subject: [PATCH 6/6] Fix tests --- .../Transformers/AvaloniaXamlIlAssetIncludeTransformer.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlAssetIncludeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlAssetIncludeTransformer.cs index 1572a4f140..377a9e72d9 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlAssetIncludeTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlAssetIncludeTransformer.cs @@ -39,9 +39,7 @@ internal class AvaloniaXamlIlAssetIncludeTransformer : IXamlAstTransformer var originalAssetPath = sourceTextNode.Text; if (!(originalAssetPath.StartsWith("avares://") || originalAssetPath.StartsWith("/"))) { - throw new XamlParseException( - $"{nodeTypeName}.Source supports only \"avares://\" absolute paths or relative paths starting with \"/\"", - sourceTextNode); + return node; } var runtimeHelpers = context.GetAvaloniaTypes().RuntimeHelpers;