committed by
GitHub
18 changed files with 1088 additions and 175 deletions
@ -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); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 491 B |
Loading…
Reference in new issue