diff --git a/src/Controls/src/Build.Tasks/CompiledConverters/SafeAreaEdgesTypeConverter.cs b/src/Controls/src/Build.Tasks/CompiledConverters/SafeAreaEdgesTypeConverter.cs new file mode 100644 index 000000000000..3b3455f7ab76 --- /dev/null +++ b/src/Controls/src/Build.Tasks/CompiledConverters/SafeAreaEdgesTypeConverter.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using Microsoft.Maui.Controls.Build.Tasks; +using Microsoft.Maui.Controls.Xaml; +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace Microsoft.Maui.Controls.XamlC +{ + class SafeAreaEdgesTypeConverter : ICompiledTypeConverter + { + public IEnumerable ConvertFromString(string value, ILContext context, BaseNode node) + { + var module = context.Body.Method.Module; + + if (!string.IsNullOrEmpty(value)) + { + value = value.Trim(); + + // Split by comma - if no comma, we get array with single element + var parts = value.Split(','); + var regions = new int[parts.Length]; + + for (int i = 0; i < parts.Length; i++) + { + var part = parts[i].Trim(); + + // Performance optimization: use string comparison instead of Enum.TryParse + // since SafeAreaRegions has specific values + if (string.Equals(part, "All", StringComparison.OrdinalIgnoreCase)) + { + regions[i] = 1 << 15; // SafeAreaRegions.All + } + else if (string.Equals(part, "None", StringComparison.OrdinalIgnoreCase)) + { + regions[i] = 0; // SafeAreaRegions.None + } + else if (string.Equals(part, "Container", StringComparison.OrdinalIgnoreCase)) + { + regions[i] = 1 << 1; // SafeAreaRegions.Container + } + else if (string.Equals(part, "SoftInput", StringComparison.OrdinalIgnoreCase)) + { + regions[i] = 1 << 0; // SafeAreaRegions.SoftInput + } + else if (string.Equals(part, "Default", StringComparison.OrdinalIgnoreCase)) + { + regions[i] = -1; // SafeAreaRegions.Default + } + else + { + throw new BuildException(BuildExceptionCode.Conversion, node, null, value, typeof(string)); + } + } + + // Generate IL based on number of regions, same logic as runtime converter + return regions.Length switch + { + 1 => GenerateILUniform(module, context, regions[0]), + 2 => GenerateILHorizontalVertical(module, context, regions[0], regions[1]), + 4 => GenerateILAllEdges(module, context, regions[0], regions[1], regions[2], regions[3]), + _ => throw new BuildException(BuildExceptionCode.Conversion, node, null, $"SafeAreaEdges must have 1, 2, or 4 values, but got {regions.Length}", typeof(string)) + }; + } + + throw new BuildException(BuildExceptionCode.Conversion, node, null, value, typeof(string)); + } + + // Generate IL for uniform constructor: new SafeAreaEdges(SafeAreaRegions uniformValue) + IEnumerable GenerateILUniform(ModuleDefinition module, ILContext context, int uniformValue) + { + yield return Instruction.Create(OpCodes.Ldc_I4, uniformValue); + yield return Instruction.Create(OpCodes.Newobj, module.ImportCtorReference(context.Cache, ("Microsoft.Maui", "Microsoft.Maui", "SafeAreaEdges"), parameterTypes: new[] { ("Microsoft.Maui", "Microsoft.Maui", "SafeAreaRegions") })); + } + + // Generate IL for horizontal/vertical constructor: new SafeAreaEdges(SafeAreaRegions horizontal, SafeAreaRegions vertical) + IEnumerable GenerateILHorizontalVertical(ModuleDefinition module, ILContext context, int horizontal, int vertical) + { + yield return Instruction.Create(OpCodes.Ldc_I4, horizontal); + yield return Instruction.Create(OpCodes.Ldc_I4, vertical); + yield return Instruction.Create(OpCodes.Newobj, module.ImportCtorReference(context.Cache, ("Microsoft.Maui", "Microsoft.Maui", "SafeAreaEdges"), parameterTypes: new[] { ("Microsoft.Maui", "Microsoft.Maui", "SafeAreaRegions"), ("Microsoft.Maui", "Microsoft.Maui", "SafeAreaRegions") })); + } + + // Generate IL for all edges constructor: new SafeAreaEdges(SafeAreaRegions left, SafeAreaRegions top, SafeAreaRegions right, SafeAreaRegions bottom) + IEnumerable GenerateILAllEdges(ModuleDefinition module, ILContext context, int left, int top, int right, int bottom) + { + yield return Instruction.Create(OpCodes.Ldc_I4, left); + yield return Instruction.Create(OpCodes.Ldc_I4, top); + yield return Instruction.Create(OpCodes.Ldc_I4, right); + yield return Instruction.Create(OpCodes.Ldc_I4, bottom); + yield return Instruction.Create(OpCodes.Newobj, module.ImportCtorReference(context.Cache, ("Microsoft.Maui", "Microsoft.Maui", "SafeAreaEdges"), parameterTypes: new[] { ("Microsoft.Maui", "Microsoft.Maui", "SafeAreaRegions"), ("Microsoft.Maui", "Microsoft.Maui", "SafeAreaRegions"), ("Microsoft.Maui", "Microsoft.Maui", "SafeAreaRegions"), ("Microsoft.Maui", "Microsoft.Maui", "SafeAreaRegions") })); + } + } +} \ No newline at end of file diff --git a/src/Controls/src/Build.Tasks/XamlCache.cs b/src/Controls/src/Build.Tasks/XamlCache.cs index 804a15e845f4..09148145f4d0 100644 --- a/src/Controls/src/Build.Tasks/XamlCache.cs +++ b/src/Controls/src/Build.Tasks/XamlCache.cs @@ -64,6 +64,7 @@ public ICollection GetResourceNamesInUse(VariableDefinition variableDefi { module.ImportReference(this, ("Microsoft.Maui", "Microsoft.Maui.Converters", "ThicknessTypeConverter")), typeof(ThicknessTypeConverter) }, { module.ImportReference(this, ("Microsoft.Maui", "Microsoft.Maui.Converters", "CornerRadiusTypeConverter")), typeof(CornerRadiusTypeConverter) }, { module.ImportReference(this, ("Microsoft.Maui", "Microsoft.Maui.Converters", "EasingTypeConverter")), typeof(EasingTypeConverter) }, + { module.ImportReference(this, ("Microsoft.Maui", "Microsoft.Maui.Converters", "SafeAreaEdgesTypeConverter")), typeof(SafeAreaEdgesTypeConverter) }, { module.ImportReference(this, ("Microsoft.Maui.Graphics", "Microsoft.Maui.Graphics.Converters", "ColorTypeConverter")), typeof(ColorTypeConverter) }, { module.ImportReference(this, ("Microsoft.Maui.Graphics", "Microsoft.Maui.Graphics.Converters", "PointTypeConverter")), typeof(PointTypeConverter) }, { module.ImportReference(this, ("Microsoft.Maui.Graphics", "Microsoft.Maui.Graphics.Converters", "RectTypeConverter")), typeof(RectangleTypeConverter) }, diff --git a/src/Controls/tests/Xaml.UnitTests/TestSafeAreaEdgesPage.xaml b/src/Controls/tests/Xaml.UnitTests/TestSafeAreaEdgesPage.xaml new file mode 100644 index 000000000000..f7268b79f598 --- /dev/null +++ b/src/Controls/tests/Xaml.UnitTests/TestSafeAreaEdgesPage.xaml @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/Controls/tests/Xaml.UnitTests/TestSafeAreaEdgesPage.xaml.cs b/src/Controls/tests/Xaml.UnitTests/TestSafeAreaEdgesPage.xaml.cs new file mode 100644 index 000000000000..8cd2ea3ca4cf --- /dev/null +++ b/src/Controls/tests/Xaml.UnitTests/TestSafeAreaEdgesPage.xaml.cs @@ -0,0 +1,43 @@ +using System; +using System.Linq; +using NUnit.Framework; + +namespace Microsoft.Maui.Controls.Xaml.UnitTests; + +public partial class TestSafeAreaEdgesPage : ContentPage +{ + public TestSafeAreaEdgesPage() => InitializeComponent(); + + public TestSafeAreaEdgesPage(bool useCompiledXaml) + { + //this stub will be replaced at compile time + } + + [TestFixture] + class Tests + { + [Test] + public void CompiledSafeAreaEdgesTypeConverterIsUsed() + { + // This test verifies that the compiled SafeAreaEdgesTypeConverter is properly registered and used + MockCompiler.Compile(typeof(TestSafeAreaEdgesPage), out var methodDef, out var hasLoggedErrors); + Assert.That(!hasLoggedErrors, "Compilation should succeed without errors"); + + // Verify that no runtime SafeAreaEdgesTypeConverter is instantiated + Assert.That(!methodDef.Body.Instructions.Any(instr => + HasConstructorForType(methodDef, instr, typeof(Microsoft.Maui.Converters.SafeAreaEdgesTypeConverter))), + "This Xaml should not generate a new SafeAreaEdgesTypeConverter() at runtime"); + } + + static bool HasConstructorForType(Mono.Cecil.MethodDefinition methodDef, Mono.Cecil.Cil.Instruction instruction, Type converterType) + { + if (instruction.OpCode != Mono.Cecil.Cil.OpCodes.Newobj) + return false; + if (instruction.Operand is not Mono.Cecil.MethodReference methodRef) + return false; + if (!Build.Tasks.TypeRefComparer.Default.Equals(methodRef.DeclaringType, methodDef.Module.ImportReference(converterType))) + return false; + return true; + } + } +} \ No newline at end of file