Initial support for cross-testing against WPF (#15150)
* Initial support for cross-testing against WPF * More cross-tests
This commit is contained in:
@@ -62,6 +62,7 @@
|
||||
"tests\\Avalonia.Markup.UnitTests\\Avalonia.Markup.UnitTests.csproj",
|
||||
"tests\\Avalonia.Markup.Xaml.UnitTests\\Avalonia.Markup.Xaml.UnitTests.csproj",
|
||||
"tests\\Avalonia.ReactiveUI.UnitTests\\Avalonia.ReactiveUI.UnitTests.csproj",
|
||||
"tests\\Avalonia.RenderTests.WpfCompare\\Avalonia.RenderTests.WpfCompare.csproj",
|
||||
"tests\\Avalonia.Skia.RenderTests\\Avalonia.Skia.RenderTests.csproj",
|
||||
"tests\\Avalonia.Skia.UnitTests\\Avalonia.Skia.UnitTests.csproj",
|
||||
"tests\\Avalonia.UnitTests\\Avalonia.UnitTests.csproj",
|
||||
|
||||
@@ -291,6 +291,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BuildTasks", "BuildTasks",
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PInvoke", "tests\TestFiles\BuildTasks\PInvoke\PInvoke.csproj", "{0A948D71-99C5-43E9-BACB-B0BA59EA25B4}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.RenderTests.WpfCompare", "tests\Avalonia.RenderTests.WpfCompare\Avalonia.RenderTests.WpfCompare.csproj", "{9AE1B827-21AC-4063-AB22-C8804B7F931E}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -675,6 +677,10 @@ Global
|
||||
{0A948D71-99C5-43E9-BACB-B0BA59EA25B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0A948D71-99C5-43E9-BACB-B0BA59EA25B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0A948D71-99C5-43E9-BACB-B0BA59EA25B4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9AE1B827-21AC-4063-AB22-C8804B7F931E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9AE1B827-21AC-4063-AB22-C8804B7F931E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9AE1B827-21AC-4063-AB22-C8804B7F931E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9AE1B827-21AC-4063-AB22-C8804B7F931E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -758,6 +764,7 @@ Global
|
||||
{9D6AEF22-221F-4F4B-B335-A4BA510F002C} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
|
||||
{5BF0C3B8-E595-4940-AB30-2DA206C2F085} = {9D6AEF22-221F-4F4B-B335-A4BA510F002C}
|
||||
{0A948D71-99C5-43E9-BACB-B0BA59EA25B4} = {5BF0C3B8-E595-4940-AB30-2DA206C2F085}
|
||||
{9AE1B827-21AC-4063-AB22-C8804B7F931E} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>$(AvsCurrentTargetFramework)</TargetFramework>
|
||||
<DefineConstants>$(DefineConstants);AVALONIA_D2D</DefineConstants>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="..\Avalonia.RenderTests\**\*.cs" />
|
||||
<Compile Remove="..\Avalonia.RenderTests\CrossTests\**\*.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="..\Avalonia.RenderTests\**\*.ttf" />
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>$(AvsCurrentWindowsTargetFramework)</TargetFramework>
|
||||
<EnableWindowsTargeting>true</EnableWindowsTargeting>
|
||||
<Nullable>enable</Nullable>
|
||||
<UseWpf>true</UseWpf>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj" />
|
||||
<ProjectReference Include="..\..\src\Avalonia.Controls\Avalonia.Controls.csproj" />
|
||||
<Compile Include="..\Avalonia.RenderTests\CrossUI\CrossUI.cs"/>
|
||||
<Compile Include="..\Avalonia.RenderTests\CrossTests\**\*.cs"/>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Xunit.StaFact" Version="1.2.46-alpha" />
|
||||
</ItemGroup>
|
||||
<Import Project="..\..\build\Moq.props" />
|
||||
<Import Project="..\..\build\Rx.props" />
|
||||
<Import Project="..\..\build\XUnit.props" />
|
||||
<Import Project="..\..\build\ImageSharp.props" />
|
||||
<Import Project="..\..\build\SkiaSharp.props" />
|
||||
<Import Project="..\..\build\SharedVersion.props" />
|
||||
</Project>
|
||||
@@ -0,0 +1,13 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Avalonia.RenderTests.WpfCompare;
|
||||
|
||||
public class CrossFactAttribute : StaFactAttribute
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public class CrossTheoryAttribute : StaTheoryAttribute
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using CrossUI;
|
||||
|
||||
namespace Avalonia.RenderTests.WpfCompare;
|
||||
|
||||
public class CrossTestBase
|
||||
{
|
||||
private readonly string _groupName;
|
||||
public CrossTestBase(string groupName)
|
||||
{
|
||||
_groupName = groupName;
|
||||
}
|
||||
|
||||
protected void RenderAndCompare(CrossControl root, [CallerMemberName] string? testName = null, double dpi = 96)
|
||||
{
|
||||
var dir = Path.Combine(GetTestsDirectory(), "TestFiles", "CrossTests", _groupName);
|
||||
if (!Directory.Exists(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
var path = Path.Combine(dir, testName + ".wpf.png");
|
||||
|
||||
var w = root.Width;
|
||||
var h = root.Height;
|
||||
var pw = (int)Math.Ceiling(w * dpi / 96);
|
||||
var ph = (int)Math.Ceiling(h * dpi / 96);
|
||||
|
||||
var control = new WpfCrossControl(root);
|
||||
control.Measure(new System.Windows.Size(w, h));
|
||||
control.Arrange(new System.Windows.Rect(0, 0, w, h));
|
||||
var bmp = new RenderTargetBitmap(pw, ph, dpi, dpi, PixelFormats.Default);
|
||||
bmp.Render(control);
|
||||
var encoder = new PngBitmapEncoder();
|
||||
encoder.Frames.Add(BitmapFrame.Create(bmp));
|
||||
using (var f = File.Create(path))
|
||||
encoder.Save(f);
|
||||
}
|
||||
|
||||
static string GetTestsDirectory()
|
||||
{
|
||||
var path = Directory.GetCurrentDirectory();
|
||||
|
||||
while (path.Length > 0 && Path.GetFileName(path) != "tests")
|
||||
{
|
||||
path = Path.GetDirectoryName(path);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
using System;
|
||||
using System.Windows.Controls;
|
||||
using CrossUI;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using AlignmentX = System.Windows.Media.AlignmentX;
|
||||
using AlignmentY = System.Windows.Media.AlignmentY;
|
||||
using Brush = System.Windows.Media.Brush;
|
||||
using BrushMappingMode = System.Windows.Media.BrushMappingMode;
|
||||
using Color = Avalonia.Media.Color;
|
||||
using Drawing = System.Windows.Media.Drawing;
|
||||
using DrawingBrush = System.Windows.Media.DrawingBrush;
|
||||
using DrawingCollection = System.Windows.Media.DrawingCollection;
|
||||
using DrawingContext = System.Windows.Media.DrawingContext;
|
||||
using DrawingGroup = System.Windows.Media.DrawingGroup;
|
||||
using DrawingImage = System.Windows.Media.DrawingImage;
|
||||
using Geometry = System.Windows.Media.Geometry;
|
||||
using GeometryDrawing = System.Windows.Media.GeometryDrawing;
|
||||
using MatrixTransform = System.Windows.Media.MatrixTransform;
|
||||
using Pen = System.Windows.Media.Pen;
|
||||
using RectangleGeometry = System.Windows.Media.RectangleGeometry;
|
||||
using SolidColorBrush = System.Windows.Media.SolidColorBrush;
|
||||
using Stretch = System.Windows.Media.Stretch;
|
||||
using TileBrush = System.Windows.Media.TileBrush;
|
||||
using TileMode = System.Windows.Media.TileMode;
|
||||
using Transform = System.Windows.Media.Transform;
|
||||
using WPoint = System.Windows.Point;
|
||||
using WSize = System.Windows.Size;
|
||||
using WRect = System.Windows.Rect;
|
||||
using WColor = System.Windows.Media.Color;
|
||||
using WMatrix = System.Windows.Media.Matrix;
|
||||
namespace Avalonia.RenderTests.WpfCompare;
|
||||
|
||||
internal static class WpfConvertExtensions
|
||||
{
|
||||
public static WPoint ToWpf(this Point pt) => new(pt.X, pt.Y);
|
||||
public static WSize ToWpf(this Size size) => new(size.Width, size.Height);
|
||||
public static WRect ToWpf(this Rect rect) => new(rect.Left, rect.Top, rect.Width, rect.Height);
|
||||
public static WColor ToWpf(this Color color) => WColor.FromArgb(color.A, color.R, color.G, color.B);
|
||||
public static WMatrix ToWpf(this Matrix m) => new WMatrix(m.M11, m.M12, m.M21, m.M22, m.M31, m.M32);
|
||||
}
|
||||
|
||||
internal class WpfCrossControl : Panel
|
||||
{
|
||||
private readonly CrossControl _src;
|
||||
private readonly Dictionary<CrossControl, WpfCrossControl> _children;
|
||||
|
||||
public WpfCrossControl(CrossControl src)
|
||||
{
|
||||
_src = src;
|
||||
_children = src.Children.ToDictionary(x => x, x => new WpfCrossControl(x));
|
||||
Width = src.Bounds.Width;
|
||||
Height = src.Bounds.Height;
|
||||
RenderTransform = new MatrixTransform(src.RenderTransform.ToWpf());
|
||||
foreach (var ch in src.Children)
|
||||
{
|
||||
var c = _children[ch];
|
||||
this.Children.Add(c);
|
||||
}
|
||||
}
|
||||
|
||||
protected override WSize MeasureOverride(WSize availableSize)
|
||||
{
|
||||
foreach (var ch in _children)
|
||||
ch.Value.Measure(ch.Key.Bounds.Size.ToWpf());
|
||||
return _src.Bounds.Size.ToWpf();
|
||||
}
|
||||
|
||||
protected override WSize ArrangeOverride(WSize finalSize)
|
||||
{
|
||||
foreach (var ch in _children)
|
||||
ch.Value.Arrange(ch.Key.Bounds.ToWpf());
|
||||
return base.ArrangeOverride(finalSize);
|
||||
}
|
||||
|
||||
protected override void OnRender(DrawingContext context)
|
||||
{
|
||||
_src.Render(new WpfCrossDrawingContext(context));
|
||||
}
|
||||
}
|
||||
|
||||
internal class WpfCrossDrawingContext : ICrossDrawingContext
|
||||
{
|
||||
private readonly DrawingContext _ctx;
|
||||
|
||||
public WpfCrossDrawingContext(DrawingContext ctx)
|
||||
{
|
||||
_ctx = ctx;
|
||||
}
|
||||
|
||||
private static Transform? ConvertTransform(Matrix? m) => m == null ? null : new MatrixTransform(m.Value.ToWpf());
|
||||
|
||||
private static Geometry ConvertGeometry(CrossGeometry g)
|
||||
{
|
||||
if (g is CrossRectangleGeometry rg)
|
||||
return new RectangleGeometry(rg.Rect.ToWpf());
|
||||
else if (g is CrossSvgGeometry svg)
|
||||
return Geometry.Parse(svg.Path);
|
||||
else if (g is CrossEllipseGeometry ellipse)
|
||||
return new EllipseGeometry(ellipse.Rect.ToWpf());
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
private static Drawing ConvertDrawing(CrossDrawing src)
|
||||
{
|
||||
if (src is CrossDrawingGroup g)
|
||||
return new DrawingGroup() { Children = new DrawingCollection(g.Children.Select(ConvertDrawing)) };
|
||||
if (src is CrossGeometryDrawing geo)
|
||||
return new GeometryDrawing()
|
||||
{
|
||||
Geometry = ConvertGeometry(geo.Geometry), Brush = ConvertBrush(geo.Brush), Pen = ConvertPen(geo.Pen)
|
||||
};
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
private static Brush? ConvertBrush(CrossBrush? brush)
|
||||
{
|
||||
if (brush == null)
|
||||
return null;
|
||||
static Brush Sync(Brush dst, CrossBrush src)
|
||||
{
|
||||
dst.Opacity = src.Opacity;
|
||||
dst.Transform = ConvertTransform(src.Transform);
|
||||
dst.RelativeTransform = ConvertTransform(src.RelativeTransform);
|
||||
return dst;
|
||||
}
|
||||
|
||||
static Brush SyncTile(TileBrush dst, CrossTileBrush src)
|
||||
{
|
||||
dst.Stretch = (Stretch)src.Stretch;
|
||||
dst.AlignmentX = (AlignmentX)src.AlignmentX;
|
||||
dst.AlignmentY = (AlignmentY)src.AlignmentY;
|
||||
dst.TileMode = (TileMode)src.TileMode;
|
||||
dst.Viewbox = src.Viewbox.ToWpf();
|
||||
dst.ViewboxUnits = (BrushMappingMode)src.ViewboxUnits;
|
||||
dst.Viewport = src.Viewport.ToWpf();
|
||||
dst.ViewportUnits = (BrushMappingMode)src.ViewportUnits;
|
||||
return Sync(dst, src);
|
||||
}
|
||||
|
||||
static Brush SyncGradient(GradientBrush dst, CrossGradientBrush src)
|
||||
{
|
||||
dst.MappingMode = (BrushMappingMode)src.MappingMode;
|
||||
dst.SpreadMethod = (GradientSpreadMethod)src.SpreadMethod;
|
||||
dst.GradientStops =
|
||||
new GradientStopCollection(src.GradientStops.Select(s => new GradientStop(s.Color.ToWpf(), s.Offset)));
|
||||
return Sync(dst, src);
|
||||
}
|
||||
|
||||
if (brush is CrossSolidColorBrush br)
|
||||
return Sync(new SolidColorBrush(br.Color.ToWpf()), brush);
|
||||
if (brush is CrossDrawingBrush db)
|
||||
return SyncTile(new DrawingBrush(ConvertDrawing(db.Drawing)), db);
|
||||
if (brush is CrossRadialGradientBrush radial)
|
||||
return SyncGradient(new RadialGradientBrush()
|
||||
{
|
||||
RadiusX = radial.RadiusX,
|
||||
RadiusY = radial.RadiusY,
|
||||
Center = radial.Center.ToWpf(),
|
||||
GradientOrigin = radial.GradientOrigin.ToWpf()
|
||||
}, radial);
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
private static Pen? ConvertPen(CrossPen? pen)
|
||||
{
|
||||
if (pen == null)
|
||||
return null;
|
||||
return new Pen(ConvertBrush(pen.Brush), pen.Thickness);
|
||||
}
|
||||
|
||||
private static ImageSource ConvertImage(CrossImage image)
|
||||
{
|
||||
if (image is CrossBitmapImage bi)
|
||||
return new BitmapImage(new Uri(bi.Path, UriKind.Absolute));
|
||||
if (image is CrossDrawingImage di)
|
||||
return new DrawingImage(ConvertDrawing(di.Drawing));
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public void DrawRectangle(CrossBrush? brush, CrossPen? pen, Rect rc) => _ctx.DrawRectangle(ConvertBrush(brush), ConvertPen(pen), rc.ToWpf());
|
||||
public void DrawGeometry(CrossBrush? brush, CrossPen? pen, CrossGeometry geo) =>
|
||||
_ctx.DrawGeometry(ConvertBrush(brush), ConvertPen(pen), ConvertGeometry(geo));
|
||||
|
||||
public void DrawImage(CrossImage image, Rect rc) => _ctx.DrawImage(ConvertImage(image), rc.ToWpf());
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
using Xunit;
|
||||
|
||||
[assembly: CollectionBehavior(DisableTestParallelization = true)]
|
||||
@@ -0,0 +1,54 @@
|
||||
using Avalonia.Media;
|
||||
using CrossUI;
|
||||
using Xunit;
|
||||
|
||||
#if AVALONIA_SKIA
|
||||
namespace Avalonia.Skia.RenderTests;
|
||||
#elif AVALONIA_D2D
|
||||
namespace Avalonia.Direct2D1.RenderTests;
|
||||
#else
|
||||
namespace Avalonia.RenderTests.WpfCompare;
|
||||
#endif
|
||||
|
||||
|
||||
public class CrossTileBrushTests : CrossTestBase
|
||||
{
|
||||
public CrossTileBrushTests() : base("Media/TileBrushes")
|
||||
{
|
||||
}
|
||||
|
||||
[CrossFact]
|
||||
public void Simple_Checkboard_Pattern_Is_Rendered_Identically()
|
||||
{
|
||||
RenderAndCompare(new CrossControl()
|
||||
{
|
||||
Width = 100,
|
||||
Height = 100,
|
||||
Background = new CrossDrawingBrush()
|
||||
{
|
||||
Drawing = new CrossDrawingGroup()
|
||||
{
|
||||
Children =
|
||||
{
|
||||
new CrossGeometryDrawing(new CrossRectangleGeometry(new(0, 0, 20, 20)))
|
||||
{
|
||||
Brush = new CrossSolidColorBrush(Colors.White)
|
||||
},
|
||||
new CrossGeometryDrawing(new CrossRectangleGeometry(new(0, 0, 10, 10)))
|
||||
{
|
||||
Brush = new CrossSolidColorBrush(Colors.Black)
|
||||
},
|
||||
new CrossGeometryDrawing(new CrossRectangleGeometry(new(10, 10, 10, 10)))
|
||||
{
|
||||
Brush = new CrossSolidColorBrush(Colors.Black)
|
||||
},
|
||||
}
|
||||
},
|
||||
Viewport = new Rect(0, 0, 10, 10),
|
||||
ViewportUnits = BrushMappingMode.Absolute,
|
||||
TileMode = TileMode.Tile
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using Avalonia.Media;
|
||||
using CrossUI;
|
||||
using Xunit;
|
||||
|
||||
#if AVALONIA_SKIA
|
||||
namespace Avalonia.Skia.RenderTests;
|
||||
#elif AVALONIA_D2D
|
||||
namespace Avalonia.Direct2D1.RenderTests;
|
||||
#else
|
||||
namespace Avalonia.RenderTests.WpfCompare;
|
||||
#endif
|
||||
|
||||
|
||||
public class CrossRadialGradientBrushTests : CrossTestBase
|
||||
{
|
||||
public CrossRadialGradientBrushTests() : base("Media/RadialGradientBrush")
|
||||
{
|
||||
}
|
||||
|
||||
[CrossFact]
|
||||
public void Transform_Should_Work_As_Expected()
|
||||
{
|
||||
RenderAndCompare(
|
||||
new CrossControl()
|
||||
{
|
||||
Children =
|
||||
{
|
||||
new CrossFuncControl(ctx =>
|
||||
{
|
||||
var geo = new CrossEllipseGeometry(new Rect(3.430200000000003, 29.019099999999998, 42.7692,
|
||||
19.6732));
|
||||
ctx.DrawGeometry(new CrossSolidColorBrush(Colors.Magenta), null, geo);
|
||||
ctx.DrawGeometry(
|
||||
new CrossRadialGradientBrush()
|
||||
{
|
||||
RadiusX = 12.289,
|
||||
RadiusY = 12.289,
|
||||
GradientOrigin = new Point(15.116, 63.965),
|
||||
Center = new Point(15.116, 63.965),
|
||||
MappingMode = BrushMappingMode.Absolute,
|
||||
SpreadMethod = GradientSpreadMethod.Pad,
|
||||
GradientStops =
|
||||
{
|
||||
new GradientStop(Colors.Black, 0), new GradientStop(Colors.Transparent, 1)
|
||||
},
|
||||
Transform = new Matrix(1.664, 0,
|
||||
0, 0.75621371,
|
||||
-0.06567275, -10.272)
|
||||
}, null, geo);
|
||||
})
|
||||
{
|
||||
Width = 48,
|
||||
Height = 48,
|
||||
RenderTransform = Matrix.CreateScale(4, 4)
|
||||
}
|
||||
},
|
||||
Width = 256,
|
||||
Height = 256,
|
||||
Background = new CrossSolidColorBrush(Colors.White)
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Media.Immutable;
|
||||
using CrossUI;
|
||||
|
||||
#if AVALONIA_SKIA
|
||||
namespace Avalonia.Skia.RenderTests.CrossUI;
|
||||
#else
|
||||
namespace Avalonia.Direct2D1.RenderTests.CrossUI;
|
||||
#endif
|
||||
|
||||
class AvaloniaCrossControl : Control
|
||||
{
|
||||
private readonly CrossControl _src;
|
||||
private readonly Dictionary<CrossControl, AvaloniaCrossControl> _children;
|
||||
|
||||
public AvaloniaCrossControl(CrossControl src)
|
||||
{
|
||||
_src = src;
|
||||
_children = src.Children.ToDictionary(x => x, x => new AvaloniaCrossControl(x));
|
||||
Width = src.Bounds.Width;
|
||||
Height = src.Bounds.Height;
|
||||
RenderTransform = new MatrixTransform(src.RenderTransform);
|
||||
RenderTransformOrigin = new RelativePoint(default, RelativeUnit.Relative);
|
||||
foreach (var ch in src.Children)
|
||||
{
|
||||
var c = _children[ch];
|
||||
VisualChildren.Add(c);
|
||||
LogicalChildren.Add(c);
|
||||
}
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
foreach (var ch in _children)
|
||||
ch.Value.Measure(ch.Key.Bounds.Size);
|
||||
return _src.Bounds.Size;
|
||||
}
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
{
|
||||
foreach (var ch in _children)
|
||||
ch.Value.Arrange(ch.Key.Bounds);
|
||||
return finalSize;
|
||||
}
|
||||
|
||||
public override void Render(DrawingContext context)
|
||||
{
|
||||
_src.Render(new AvaloniaCrossDrawingContext(context));
|
||||
}
|
||||
}
|
||||
|
||||
class AvaloniaCrossDrawingContext : ICrossDrawingContext
|
||||
{
|
||||
private readonly DrawingContext _ctx;
|
||||
|
||||
public AvaloniaCrossDrawingContext(DrawingContext ctx)
|
||||
{
|
||||
_ctx = ctx;
|
||||
}
|
||||
|
||||
static Transform? ConvertTransform(Matrix? m) => m == null ? null : new MatrixTransform(m.Value);
|
||||
|
||||
static RelativeRect ConvertRect(Rect rc, BrushMappingMode mode)
|
||||
=> new RelativeRect(rc,
|
||||
mode == BrushMappingMode.RelativeToBoundingBox ? RelativeUnit.Relative : RelativeUnit.Absolute);
|
||||
|
||||
static RelativePoint ConvertPoint(Point pt, BrushMappingMode mode)
|
||||
=> new(pt, mode == BrushMappingMode.RelativeToBoundingBox ? RelativeUnit.Relative : RelativeUnit.Absolute);
|
||||
|
||||
static RelativeScalar ConvertScalar(double scalar, BrushMappingMode mode)
|
||||
=> new(scalar, mode == BrushMappingMode.RelativeToBoundingBox ? RelativeUnit.Relative : RelativeUnit.Absolute);
|
||||
|
||||
static Geometry ConvertGeometry(CrossGeometry g)
|
||||
{
|
||||
if (g is CrossRectangleGeometry rg)
|
||||
return new RectangleGeometry(rg.Rect);
|
||||
else if (g is CrossSvgGeometry svg)
|
||||
return PathGeometry.Parse(svg.Path);
|
||||
else if (g is CrossEllipseGeometry ellipse)
|
||||
return new EllipseGeometry(ellipse.Rect);
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
static Drawing ConvertDrawing(CrossDrawing src)
|
||||
{
|
||||
if (src is CrossDrawingGroup g)
|
||||
return new DrawingGroup() { Children = new DrawingCollection(g.Children.Select(ConvertDrawing)) };
|
||||
if (src is CrossGeometryDrawing geo)
|
||||
return new GeometryDrawing()
|
||||
{
|
||||
Geometry = ConvertGeometry(geo.Geometry), Brush = ConvertBrush(geo.Brush), Pen = ConvertPen(geo.Pen)
|
||||
};
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
static IBrush? ConvertBrush(CrossBrush? brush)
|
||||
{
|
||||
if (brush == null)
|
||||
return null;
|
||||
static Brush Sync(Brush dst, CrossBrush src)
|
||||
{
|
||||
dst.Opacity = src.Opacity;
|
||||
dst.Transform = ConvertTransform(src.Transform);
|
||||
dst.TransformOrigin = new RelativePoint(default, RelativeUnit.Absolute);
|
||||
if (src.RelativeTransform != null)
|
||||
throw new PlatformNotSupportedException();
|
||||
return dst;
|
||||
}
|
||||
|
||||
static Brush SyncTile(TileBrush dst, CrossTileBrush src)
|
||||
{
|
||||
dst.Stretch = src.Stretch;
|
||||
dst.AlignmentX = src.AlignmentX;
|
||||
dst.AlignmentY = src.AlignmentY;
|
||||
dst.TileMode = src.TileMode;
|
||||
dst.SourceRect = ConvertRect(src.Viewbox, src.ViewboxUnits);
|
||||
dst.DestinationRect = ConvertRect(src.Viewport, src.ViewportUnits);
|
||||
return Sync(dst, src);
|
||||
}
|
||||
|
||||
static Brush SyncGradient(GradientBrush dst, CrossGradientBrush src)
|
||||
{
|
||||
dst.GradientStops = new GradientStops();
|
||||
dst.GradientStops.AddRange(src.GradientStops);
|
||||
dst.SpreadMethod = src.SpreadMethod;
|
||||
return Sync(dst, src);
|
||||
}
|
||||
|
||||
if (brush is CrossSolidColorBrush br)
|
||||
return Sync(new SolidColorBrush(br.Color), brush);
|
||||
if (brush is CrossDrawingBrush db)
|
||||
return SyncTile(new DrawingBrush(ConvertDrawing(db.Drawing)), db);
|
||||
if (brush is CrossRadialGradientBrush radial)
|
||||
return SyncGradient(
|
||||
new RadialGradientBrush()
|
||||
{
|
||||
Center = ConvertPoint(radial.Center, radial.MappingMode),
|
||||
GradientOrigin = ConvertPoint(radial.GradientOrigin, radial.MappingMode),
|
||||
RadiusX = ConvertScalar(radial.RadiusX, radial.MappingMode),
|
||||
RadiusY = ConvertScalar(radial.RadiusY, radial.MappingMode)
|
||||
}, radial);
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
static IPen? ConvertPen(CrossPen? pen)
|
||||
{
|
||||
if (pen == null)
|
||||
return null;
|
||||
return new Pen(ConvertBrush(pen.Brush), pen.Thickness);
|
||||
}
|
||||
|
||||
static IImage ConvertImage(CrossImage image)
|
||||
{
|
||||
if (image is CrossBitmapImage bi)
|
||||
return new Bitmap(bi.Path);
|
||||
if (image is CrossDrawingImage di)
|
||||
return new DrawingImage(ConvertDrawing(di.Drawing));
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public void DrawRectangle(CrossBrush? brush, CrossPen? pen, Rect rc) => _ctx.DrawRectangle(ConvertBrush(brush), ConvertPen(pen), rc);
|
||||
public void DrawGeometry(CrossBrush? brush, CrossPen? pen, CrossGeometry geometry) =>
|
||||
_ctx.DrawGeometry(ConvertBrush(brush), ConvertPen(pen), ConvertGeometry(geometry));
|
||||
|
||||
public void DrawImage(CrossImage image, Rect rc) => _ctx.DrawImage(ConvertImage(image), rc);
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
// ReSharper disable RedundantNameQualifier
|
||||
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Avalonia.Media;
|
||||
using Avalonia;
|
||||
|
||||
namespace CrossUI;
|
||||
|
||||
public class CrossBrush
|
||||
{
|
||||
public double Opacity = 1;
|
||||
public Avalonia.Matrix? Transform;
|
||||
public Avalonia.Matrix? RelativeTransform;
|
||||
}
|
||||
|
||||
public class CrossSolidColorBrush : CrossBrush
|
||||
{
|
||||
public Avalonia.Media.Color Color = Avalonia.Media.Colors.Black;
|
||||
|
||||
public CrossSolidColorBrush()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public CrossSolidColorBrush(Avalonia.Media.Color color)
|
||||
{
|
||||
Color = color;
|
||||
}
|
||||
}
|
||||
|
||||
public class CrossGradientBrush : CrossBrush
|
||||
{
|
||||
public List<GradientStop> GradientStops = new();
|
||||
public Avalonia.Media.GradientSpreadMethod SpreadMethod;
|
||||
public BrushMappingMode MappingMode;
|
||||
}
|
||||
|
||||
public class CrossRadialGradientBrush : CrossGradientBrush
|
||||
{
|
||||
public Avalonia.Point Center;
|
||||
public Avalonia.Point GradientOrigin;
|
||||
public double RadiusX, RadiusY;
|
||||
}
|
||||
|
||||
public class CrossTileBrush : CrossBrush
|
||||
{
|
||||
public Avalonia.Media.AlignmentX AlignmentX = AlignmentX.Center;
|
||||
public Avalonia.Media.AlignmentY AlignmentY = AlignmentY.Center;
|
||||
public Avalonia.Media.Stretch Stretch = Stretch.Fill;
|
||||
public Avalonia.Media.TileMode TileMode = TileMode.None;
|
||||
public Rect Viewbox = new Rect(0, 0, 1, 1);
|
||||
public Avalonia.Media.BrushMappingMode ViewboxUnits = BrushMappingMode.RelativeToBoundingBox;
|
||||
public Rect Viewport = new Rect(0, 0, 1, 1);
|
||||
public Avalonia.Media.BrushMappingMode ViewportUnits = BrushMappingMode.RelativeToBoundingBox;
|
||||
}
|
||||
|
||||
|
||||
public abstract class CrossDrawing
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public class CrossGeometryDrawing : CrossDrawing
|
||||
{
|
||||
public CrossGeometry Geometry;
|
||||
public CrossBrush? Brush;
|
||||
public CrossPen? Pen;
|
||||
public CrossGeometryDrawing(CrossGeometry geometry)
|
||||
{
|
||||
Geometry = geometry;
|
||||
}
|
||||
}
|
||||
|
||||
public class CrossDrawingGroup : CrossDrawing
|
||||
{
|
||||
public List<CrossDrawing> Children = new();
|
||||
}
|
||||
|
||||
public abstract class CrossGeometry
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public class CrossSvgGeometry : CrossGeometry
|
||||
{
|
||||
public string Path;
|
||||
|
||||
public CrossSvgGeometry(string path)
|
||||
{
|
||||
Path = path;
|
||||
}
|
||||
}
|
||||
|
||||
public class CrossEllipseGeometry : CrossGeometry
|
||||
{
|
||||
public CrossEllipseGeometry(Rect rect)
|
||||
{
|
||||
Rect = rect;
|
||||
}
|
||||
|
||||
public CrossEllipseGeometry()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public Rect Rect { get; set; }
|
||||
}
|
||||
|
||||
public class CrossRectangleGeometry : CrossGeometry
|
||||
{
|
||||
public Rect Rect;
|
||||
|
||||
public CrossRectangleGeometry(Rect rect)
|
||||
{
|
||||
Rect = rect;
|
||||
}
|
||||
}
|
||||
|
||||
public class CrossDrawingBrush : CrossTileBrush
|
||||
{
|
||||
public CrossDrawing Drawing;
|
||||
}
|
||||
|
||||
public class CrossPen
|
||||
{
|
||||
public CrossBrush Brush;
|
||||
public double Thickness = 1;
|
||||
}
|
||||
|
||||
public interface ICrossDrawingContext
|
||||
{
|
||||
void DrawRectangle(CrossBrush? brush, CrossPen? pen, Rect rc);
|
||||
void DrawGeometry(CrossBrush? brush, CrossPen? pen, CrossGeometry geometry);
|
||||
void DrawImage(CrossImage image, Rect rc);
|
||||
}
|
||||
|
||||
public abstract class CrossImage
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public class CrossBitmapImage : CrossImage
|
||||
{
|
||||
public string Path;
|
||||
public CrossBitmapImage(string path)
|
||||
{
|
||||
Path = path;
|
||||
}
|
||||
}
|
||||
|
||||
public class CrossDrawingImage : CrossImage
|
||||
{
|
||||
public CrossDrawing Drawing;
|
||||
}
|
||||
|
||||
|
||||
public class CrossControl
|
||||
{
|
||||
public Rect Bounds => new Rect(Left, Top, Width, Height);
|
||||
public double Left, Top, Width, Height;
|
||||
public CrossBrush? Background;
|
||||
public CrossPen? Outline;
|
||||
public List<CrossControl> Children = new();
|
||||
public Matrix RenderTransform = Matrix.Identity;
|
||||
|
||||
public virtual void Render(ICrossDrawingContext ctx)
|
||||
{
|
||||
var rc = new Rect(Bounds.Size);
|
||||
if (Background != null || Outline != null)
|
||||
ctx.DrawRectangle(Background, Outline, rc);
|
||||
}
|
||||
}
|
||||
|
||||
public class CrossFuncControl : CrossControl
|
||||
{
|
||||
private readonly Action<ICrossDrawingContext> _render;
|
||||
|
||||
public CrossFuncControl(Action<ICrossDrawingContext> render)
|
||||
{
|
||||
_render = render;
|
||||
}
|
||||
|
||||
public override void Render(ICrossDrawingContext ctx)
|
||||
{
|
||||
base.Render(ctx);
|
||||
_render(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
public class CrossImageControl : CrossControl
|
||||
{
|
||||
public CrossImage Image;
|
||||
public override void Render(ICrossDrawingContext ctx)
|
||||
{
|
||||
base.Render(ctx);
|
||||
var rc = new Rect(Bounds.Size);
|
||||
ctx.DrawImage(Image, rc);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,27 +42,6 @@ namespace Avalonia.Direct2D1.RenderTests
|
||||
#endif
|
||||
public static FontFamily TestFontFamily = new FontFamily(s_fontUri);
|
||||
|
||||
private static readonly TestDispatcherImpl threadingInterface =
|
||||
new TestDispatcherImpl();
|
||||
|
||||
private static readonly IAssetLoader assetLoader = new StandardAssetLoader();
|
||||
|
||||
static TestBase()
|
||||
{
|
||||
#if AVALONIA_SKIA
|
||||
SkiaPlatform.Initialize();
|
||||
#else
|
||||
Direct2D1Platform.Initialize();
|
||||
#endif
|
||||
AvaloniaLocator.CurrentMutable
|
||||
.Bind<IDispatcherImpl>()
|
||||
.ToConstant(threadingInterface);
|
||||
|
||||
AvaloniaLocator.CurrentMutable
|
||||
.Bind<IAssetLoader>()
|
||||
.ToConstant(assetLoader);
|
||||
}
|
||||
|
||||
public TestBase(string outputPath)
|
||||
{
|
||||
outputPath = outputPath.Replace('\\', Path.DirectorySeparatorChar);
|
||||
@@ -75,7 +54,7 @@ namespace Avalonia.Direct2D1.RenderTests
|
||||
#endif
|
||||
OutputPath = Path.Combine(testFiles, platform, outputPath);
|
||||
|
||||
threadingInterface.MainThread = Thread.CurrentThread;
|
||||
TestRenderHelper.BeginTest();
|
||||
}
|
||||
|
||||
public string OutputPath
|
||||
@@ -83,64 +62,17 @@ namespace Avalonia.Direct2D1.RenderTests
|
||||
get;
|
||||
}
|
||||
|
||||
protected Task RenderToFile(Control target, [CallerMemberName] string testName = "", double dpi = 96)
|
||||
protected async Task RenderToFile(Control target, [CallerMemberName] string testName = "", double dpi = 96)
|
||||
{
|
||||
if (!Directory.Exists(OutputPath))
|
||||
{
|
||||
Directory.CreateDirectory(OutputPath);
|
||||
}
|
||||
|
||||
|
||||
var immediatePath = Path.Combine(OutputPath, testName + ".immediate.out.png");
|
||||
var compositedPath = Path.Combine(OutputPath, testName + ".composited.out.png");
|
||||
var factory = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
|
||||
var pixelSize = new PixelSize((int)target.Width, (int)target.Height);
|
||||
var size = new Size(target.Width, target.Height);
|
||||
var dpiVector = new Vector(dpi, dpi);
|
||||
|
||||
using (RenderTargetBitmap bitmap = new RenderTargetBitmap(pixelSize, dpiVector))
|
||||
{
|
||||
target.Measure(size);
|
||||
target.Arrange(new Rect(size));
|
||||
bitmap.Render(target);
|
||||
bitmap.Save(immediatePath);
|
||||
}
|
||||
|
||||
var timer = new ManualRenderTimer();
|
||||
|
||||
var compositor = new Compositor(new RenderLoop(timer), null, true,
|
||||
new DispatcherCompositorScheduler(), true, Dispatcher.UIThread);
|
||||
using (var writableBitmap = factory.CreateWriteableBitmap(pixelSize, dpiVector, factory.DefaultPixelFormat, factory.DefaultAlphaFormat))
|
||||
{
|
||||
var root = new TestRenderRoot(dpiVector.X / 96, null!);
|
||||
using (var renderer = new CompositingRenderer(root, compositor, () => new[]
|
||||
{
|
||||
new BitmapFramebufferSurface(writableBitmap)
|
||||
}))
|
||||
{
|
||||
root.Initialize(renderer, target);
|
||||
renderer.Start();
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
renderer.Paint(new Rect(root.Bounds.Size), false);
|
||||
}
|
||||
writableBitmap.Save(compositedPath);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
class BitmapFramebufferSurface : IFramebufferPlatformSurface
|
||||
{
|
||||
private readonly IWriteableBitmapImpl _bitmap;
|
||||
|
||||
public BitmapFramebufferSurface(IWriteableBitmapImpl bitmap)
|
||||
{
|
||||
_bitmap = bitmap;
|
||||
}
|
||||
|
||||
public IFramebufferRenderTarget CreateFramebufferRenderTarget()
|
||||
{
|
||||
return new FuncFramebufferRenderTarget(() => _bitmap.Lock());
|
||||
}
|
||||
await TestRenderHelper.RenderToFile(target, immediatePath, true, dpi);
|
||||
await TestRenderHelper.RenderToFile(target, compositedPath, false, dpi);
|
||||
}
|
||||
|
||||
protected void CompareImages([CallerMemberName] string testName = "",
|
||||
@@ -156,7 +88,7 @@ namespace Avalonia.Direct2D1.RenderTests
|
||||
{
|
||||
if (!skipImmediate)
|
||||
{
|
||||
var immediateError = CompareImages(immediate!, expected);
|
||||
var immediateError = TestRenderHelper.CompareImages(immediate!, expected);
|
||||
if (immediateError > 0.022)
|
||||
{
|
||||
Assert.True(false, immediatePath + ": Error = " + immediateError);
|
||||
@@ -165,7 +97,7 @@ namespace Avalonia.Direct2D1.RenderTests
|
||||
|
||||
if (!skipCompositor)
|
||||
{
|
||||
var compositedError = CompareImages(composited!, expected);
|
||||
var compositedError = TestRenderHelper.CompareImages(composited!, expected);
|
||||
if (compositedError > 0.022)
|
||||
{
|
||||
Assert.True(false, compositedPath + ": Error = " + compositedError);
|
||||
@@ -178,108 +110,11 @@ namespace Avalonia.Direct2D1.RenderTests
|
||||
{
|
||||
var expectedPath = Path.Combine(OutputPath, (expectedName ?? testName) + ".expected.png");
|
||||
var actualPath = Path.Combine(OutputPath, testName + ".out.png");
|
||||
|
||||
using (var expected = Image.Load<Rgba32>(expectedPath))
|
||||
using (var actual = Image.Load<Rgba32>(actualPath))
|
||||
{
|
||||
double immediateError = CompareImages(actual, expected);
|
||||
|
||||
if (immediateError > 0.022)
|
||||
{
|
||||
Assert.True(false, actualPath + ": Error = " + immediateError);
|
||||
}
|
||||
}
|
||||
TestRenderHelper.AssertCompareImages(actualPath, expectedPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates root mean square error for given two images.
|
||||
/// Based roughly on ImageMagick implementation to ensure consistency.
|
||||
/// </summary>
|
||||
private static double CompareImages(Image<Rgba32> actual, Image<Rgba32> expected)
|
||||
{
|
||||
if (actual.Width != expected.Width || actual.Height != expected.Height)
|
||||
{
|
||||
throw new ArgumentException("Images have different resolutions");
|
||||
}
|
||||
private static string GetTestsDirectory() => TestRenderHelper.GetTestsDirectory();
|
||||
|
||||
var quantity = actual.Width * actual.Height;
|
||||
double squaresError = 0;
|
||||
|
||||
const double scale = 1 / 255d;
|
||||
|
||||
for (var x = 0; x < actual.Width; x++)
|
||||
{
|
||||
double localError = 0;
|
||||
|
||||
for (var y = 0; y < actual.Height; y++)
|
||||
{
|
||||
var expectedAlpha = expected[x, y].A * scale;
|
||||
var actualAlpha = actual[x, y].A * scale;
|
||||
|
||||
var r = scale * (expectedAlpha * expected[x, y].R - actualAlpha * actual[x, y].R);
|
||||
var g = scale * (expectedAlpha * expected[x, y].G - actualAlpha * actual[x, y].G);
|
||||
var b = scale * (expectedAlpha * expected[x, y].B - actualAlpha * actual[x, y].B);
|
||||
var a = expectedAlpha - actualAlpha;
|
||||
|
||||
var error = r * r + g * g + b * b + a * a;
|
||||
|
||||
localError += error;
|
||||
}
|
||||
|
||||
squaresError += localError;
|
||||
}
|
||||
|
||||
var meanSquaresError = squaresError / quantity;
|
||||
|
||||
const int channelCount = 4;
|
||||
|
||||
meanSquaresError = meanSquaresError / channelCount;
|
||||
|
||||
return Math.Sqrt(meanSquaresError);
|
||||
}
|
||||
|
||||
private static string GetTestsDirectory()
|
||||
{
|
||||
var path = Directory.GetCurrentDirectory();
|
||||
|
||||
while (path.Length > 0 && Path.GetFileName(path) != "tests")
|
||||
{
|
||||
path = Path.GetDirectoryName(path);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
private class TestDispatcherImpl : IDispatcherImpl
|
||||
{
|
||||
public bool CurrentThreadIsLoopThread => MainThread.ManagedThreadId == Thread.CurrentThread.ManagedThreadId;
|
||||
|
||||
public Thread MainThread { get; set; }
|
||||
|
||||
#pragma warning disable 67
|
||||
public event Action Signaled;
|
||||
public event Action Timer;
|
||||
#pragma warning restore 67
|
||||
|
||||
public void Signal()
|
||||
{
|
||||
// No-op
|
||||
}
|
||||
|
||||
public long Now => 0;
|
||||
|
||||
public void UpdateTimer(long? dueTimeInMs)
|
||||
{
|
||||
// No-op
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Dispatcher.UIThread.CheckAccess())
|
||||
{
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
}
|
||||
}
|
||||
public void Dispose() => TestRenderHelper.EndTest();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Rendering;
|
||||
using SixLabors.ImageSharp;
|
||||
using Xunit;
|
||||
using Avalonia.Platform;
|
||||
using System.Threading.Tasks;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Threading;
|
||||
using Avalonia.Controls.Platform.Surfaces;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Rendering.Composition;
|
||||
using Avalonia.Threading;
|
||||
using Avalonia.UnitTests;
|
||||
using Avalonia.Utilities;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using Image = SixLabors.ImageSharp.Image;
|
||||
#if AVALONIA_SKIA
|
||||
using Avalonia.Skia;
|
||||
#else
|
||||
using Avalonia.Direct2D1;
|
||||
#endif
|
||||
|
||||
#if AVALONIA_SKIA
|
||||
namespace Avalonia.Skia.RenderTests;
|
||||
#else
|
||||
namespace Avalonia.Direct2D1.RenderTests;
|
||||
#endif
|
||||
|
||||
static class TestRenderHelper
|
||||
{
|
||||
private static readonly TestDispatcherImpl s_dispatcherImpl =
|
||||
new TestDispatcherImpl();
|
||||
|
||||
static TestRenderHelper()
|
||||
{
|
||||
#if AVALONIA_SKIA
|
||||
SkiaPlatform.Initialize();
|
||||
#else
|
||||
Direct2D1Platform.Initialize();
|
||||
#endif
|
||||
AvaloniaLocator.CurrentMutable
|
||||
.Bind<IDispatcherImpl>()
|
||||
.ToConstant(s_dispatcherImpl);
|
||||
|
||||
AvaloniaLocator.CurrentMutable.Bind<IAssetLoader>().ToConstant(new StandardAssetLoader());
|
||||
}
|
||||
|
||||
|
||||
public static Task RenderToFile(Control target, string path, bool immediate, double dpi = 96)
|
||||
{
|
||||
var dir = Path.GetDirectoryName(path);
|
||||
if (!Directory.Exists(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
var factory = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
|
||||
var pixelSize = new PixelSize((int)target.Width, (int)target.Height);
|
||||
var size = new Size(target.Width, target.Height);
|
||||
var dpiVector = new Vector(dpi, dpi);
|
||||
|
||||
if (immediate)
|
||||
{
|
||||
using (RenderTargetBitmap bitmap = new RenderTargetBitmap(pixelSize, dpiVector))
|
||||
{
|
||||
target.Measure(size);
|
||||
target.Arrange(new Rect(size));
|
||||
bitmap.Render(target);
|
||||
bitmap.Save(path);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var timer = new ManualRenderTimer();
|
||||
|
||||
var compositor = new Compositor(new RenderLoop(timer), null, true,
|
||||
new DispatcherCompositorScheduler(), true, Dispatcher.UIThread);
|
||||
using (var writableBitmap = factory.CreateWriteableBitmap(pixelSize, dpiVector, factory.DefaultPixelFormat,
|
||||
factory.DefaultAlphaFormat))
|
||||
{
|
||||
var root = new TestRenderRoot(dpiVector.X / 96, null!);
|
||||
using (var renderer = new CompositingRenderer(root, compositor,
|
||||
() => new[] { new BitmapFramebufferSurface(writableBitmap) }))
|
||||
{
|
||||
root.Initialize(renderer, target);
|
||||
renderer.Start();
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
renderer.Paint(new Rect(root.Bounds.Size), false);
|
||||
}
|
||||
|
||||
writableBitmap.Save(path);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
class BitmapFramebufferSurface : IFramebufferPlatformSurface
|
||||
{
|
||||
private readonly IWriteableBitmapImpl _bitmap;
|
||||
|
||||
public BitmapFramebufferSurface(IWriteableBitmapImpl bitmap)
|
||||
{
|
||||
_bitmap = bitmap;
|
||||
}
|
||||
|
||||
public IFramebufferRenderTarget CreateFramebufferRenderTarget()
|
||||
{
|
||||
return new FuncFramebufferRenderTarget(() => _bitmap.Lock());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static void BeginTest()
|
||||
{
|
||||
s_dispatcherImpl.MainThread = Thread.CurrentThread;
|
||||
}
|
||||
|
||||
public static void EndTest()
|
||||
{
|
||||
if (Dispatcher.UIThread.CheckAccess())
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
}
|
||||
|
||||
public static string GetTestsDirectory()
|
||||
{
|
||||
var path = Directory.GetCurrentDirectory();
|
||||
|
||||
while (path.Length > 0 && Path.GetFileName(path) != "tests")
|
||||
{
|
||||
path = Path.GetDirectoryName(path);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
private class TestDispatcherImpl : IDispatcherImpl
|
||||
{
|
||||
public bool CurrentThreadIsLoopThread => MainThread.ManagedThreadId == Thread.CurrentThread.ManagedThreadId;
|
||||
|
||||
public Thread MainThread { get; set; }
|
||||
|
||||
#pragma warning disable 67
|
||||
public event Action Signaled;
|
||||
public event Action Timer;
|
||||
#pragma warning restore 67
|
||||
|
||||
public void Signal()
|
||||
{
|
||||
// No-op
|
||||
}
|
||||
|
||||
public long Now => 0;
|
||||
|
||||
public void UpdateTimer(long? dueTimeInMs)
|
||||
{
|
||||
// No-op
|
||||
}
|
||||
}
|
||||
|
||||
public static void AssertCompareImages(string actualPath, string expectedPath)
|
||||
{
|
||||
using (var expected = Image.Load<Rgba32>(expectedPath))
|
||||
using (var actual = Image.Load<Rgba32>(actualPath))
|
||||
{
|
||||
double immediateError = TestRenderHelper.CompareImages(actual, expected);
|
||||
|
||||
if (immediateError > 0.022)
|
||||
{
|
||||
Assert.True(false, actualPath + ": Error = " + immediateError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates root mean square error for given two images.
|
||||
/// Based roughly on ImageMagick implementation to ensure consistency.
|
||||
/// </summary>
|
||||
public static double CompareImages(Image<Rgba32> actual, Image<Rgba32> expected)
|
||||
{
|
||||
if (actual.Width != expected.Width || actual.Height != expected.Height)
|
||||
{
|
||||
throw new ArgumentException("Images have different resolutions");
|
||||
}
|
||||
|
||||
var quantity = actual.Width * actual.Height;
|
||||
double squaresError = 0;
|
||||
|
||||
const double scale = 1 / 255d;
|
||||
|
||||
for (var x = 0; x < actual.Width; x++)
|
||||
{
|
||||
double localError = 0;
|
||||
|
||||
for (var y = 0; y < actual.Height; y++)
|
||||
{
|
||||
var expectedAlpha = expected[x, y].A * scale;
|
||||
var actualAlpha = actual[x, y].A * scale;
|
||||
|
||||
var r = scale * (expectedAlpha * expected[x, y].R - actualAlpha * actual[x, y].R);
|
||||
var g = scale * (expectedAlpha * expected[x, y].G - actualAlpha * actual[x, y].G);
|
||||
var b = scale * (expectedAlpha * expected[x, y].B - actualAlpha * actual[x, y].B);
|
||||
var a = expectedAlpha - actualAlpha;
|
||||
|
||||
var error = r * r + g * g + b * b + a * a;
|
||||
|
||||
localError += error;
|
||||
}
|
||||
|
||||
squaresError += localError;
|
||||
}
|
||||
|
||||
var meanSquaresError = squaresError / quantity;
|
||||
|
||||
const int channelCount = 4;
|
||||
|
||||
meanSquaresError = meanSquaresError / channelCount;
|
||||
|
||||
return Math.Sqrt(meanSquaresError);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
#if AVALONIA_SKIA
|
||||
namespace Avalonia.Skia.RenderTests;
|
||||
#else
|
||||
namespace Avalonia.Direct2D1.RenderTests;
|
||||
#endif
|
||||
class WpfCompareTestBase
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Avalonia.Skia.RenderTests;
|
||||
using Avalonia.Skia.RenderTests.CrossUI;
|
||||
using CrossUI;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using Xunit;
|
||||
|
||||
#if AVALONIA_SKIA
|
||||
namespace Avalonia.Skia.RenderTests;
|
||||
#else
|
||||
namespace Avalonia.Direct2D1.RenderTests;
|
||||
#endif
|
||||
|
||||
class CrossFactAttribute : FactAttribute
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
class CrossThreoryAttribute : TheoryAttribute
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public class CrossTestBase : IDisposable
|
||||
{
|
||||
private readonly string _groupName;
|
||||
public CrossTestBase(string groupName)
|
||||
{
|
||||
TestRenderHelper.BeginTest();
|
||||
_groupName = groupName;
|
||||
}
|
||||
|
||||
protected void RenderAndCompare(CrossControl root, [CallerMemberName] string? testName = null, double dpi = 96)
|
||||
{
|
||||
var dir = Path.Combine(TestRenderHelper.GetTestsDirectory(), "TestFiles", "CrossTests", _groupName);
|
||||
if (!Directory.Exists(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
var flavor =
|
||||
#if AVALONIA_SKIA
|
||||
"skia";
|
||||
#else
|
||||
"d2d";
|
||||
#endif
|
||||
var pathBase = Path.Combine(dir, testName);
|
||||
var renderPath = pathBase + "." + flavor + ".out.png";
|
||||
var compareWith = pathBase + ".wpf.png";
|
||||
var control = new AvaloniaCrossControl(root);
|
||||
TestRenderHelper.RenderToFile(control, renderPath, false, dpi);
|
||||
|
||||
TestRenderHelper.AssertCompareImages(renderPath, compareWith);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
TestRenderHelper.EndTest();
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 9.3 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 491 B |
Reference in New Issue
Block a user