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