Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<Instruction> 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<Instruction> 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<Instruction> 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<Instruction> 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") }));
}
}
}
1 change: 1 addition & 0 deletions src/Controls/src/Build.Tasks/XamlCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public ICollection<string> 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) },
Expand Down
11 changes: 11 additions & 0 deletions src/Controls/tests/Xaml.UnitTests/TestSafeAreaEdgesPage.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Microsoft.Maui.Controls.Xaml.UnitTests.TestSafeAreaEdgesPage">
<Grid>
<VerticalStackLayout x:Name="stackUniform" SafeAreaEdges="All" />
<VerticalStackLayout x:Name="stackHorizontalVertical" SafeAreaEdges="All,None" />
<VerticalStackLayout x:Name="stackAllEdges" SafeAreaEdges="All,None,Container,SoftInput" />
<ScrollView x:Name="scrollDefault" SafeAreaEdges="Default" />
</Grid>
</ContentPage>
43 changes: 43 additions & 0 deletions src/Controls/tests/Xaml.UnitTests/TestSafeAreaEdgesPage.xaml.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}