diff --git a/src/Gtk/Avalonia.Cairo/Avalonia.Cairo.csproj b/src/Gtk/Avalonia.Cairo/Avalonia.Cairo.csproj index 3ecd91eee2..1220a7fcdc 100644 --- a/src/Gtk/Avalonia.Cairo/Avalonia.Cairo.csproj +++ b/src/Gtk/Avalonia.Cairo/Avalonia.Cairo.csproj @@ -61,7 +61,6 @@ - @@ -71,7 +70,6 @@ - diff --git a/src/Gtk/Avalonia.Cairo/CairoPlatform.cs b/src/Gtk/Avalonia.Cairo/CairoPlatform.cs index ef237cb35e..a9491cd46d 100644 --- a/src/Gtk/Avalonia.Cairo/CairoPlatform.cs +++ b/src/Gtk/Avalonia.Cairo/CairoPlatform.cs @@ -26,14 +26,22 @@ namespace Avalonia.Cairo { using System.IO; using global::Cairo; + using Rendering; - public class CairoPlatform : IPlatformRenderInterface + public class CairoPlatform : IPlatformRenderInterface, IRendererFactory { private static readonly CairoPlatform s_instance = new CairoPlatform(); private static readonly Pango.Context s_pangoContext = CreatePangoContext(); - public static void Initialize() => AvaloniaLocator.CurrentMutable.Bind().ToConstant(s_instance); + public static bool UseImmediateRenderer { get; set; } + + public static void Initialize() + { + AvaloniaLocator.CurrentMutable + .Bind().ToConstant(s_instance) + .Bind().ToConstant(s_instance); + } public IBitmapImpl CreateBitmap(int width, int height) { @@ -53,6 +61,18 @@ namespace Avalonia.Cairo return new FormattedTextImpl(s_pangoContext, text, fontFamily, fontSize, fontStyle, textAlignment, fontWeight, constraint); } + public IRenderer CreateRenderer(IRenderRoot root, IRenderLoop renderLoop) + { + if (UseImmediateRenderer) + { + return new ImmediateRenderer(root, renderLoop); + } + else + { + return new DeferredRenderer(root, renderLoop); + } + } + public IRenderTarget CreateRenderTarget(IEnumerable surfaces) { var accessor = surfaces?.OfType>().FirstOrDefault(); diff --git a/src/Gtk/Avalonia.Cairo/Media/DrawingContext.cs b/src/Gtk/Avalonia.Cairo/Media/DrawingContext.cs index ee01d5ecb0..c671d5fa08 100644 --- a/src/Gtk/Avalonia.Cairo/Media/DrawingContext.cs +++ b/src/Gtk/Avalonia.Cairo/Media/DrawingContext.cs @@ -5,14 +5,13 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reactive.Disposables; -using System.Runtime.InteropServices; using Avalonia.Cairo.Media.Imaging; using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering; namespace Avalonia.Cairo.Media { - using Avalonia.Media.Imaging; - using Platform; using Cairo = global::Cairo; /// @@ -20,32 +19,30 @@ namespace Avalonia.Cairo.Media /// public class DrawingContext : IDrawingContextImpl, IDisposable { - /// - /// The cairo context. - /// private readonly Cairo.Context _context; - + private readonly IVisualBrushRenderer _visualBrushRenderer; private readonly Stack _maskStack = new Stack(); /// /// Initializes a new instance of the class. /// /// The target surface. - public DrawingContext(Cairo.Surface surface) + public DrawingContext(Cairo.Surface surface, IVisualBrushRenderer visualBrushRenderer) { _context = new Cairo.Context(surface); + _visualBrushRenderer = visualBrushRenderer; } /// /// Initializes a new instance of the class. /// /// The GDK drawable. - public DrawingContext(Gdk.Drawable drawable) + public DrawingContext(Gdk.Drawable drawable, IVisualBrushRenderer visualBrushRenderer) { _context = Gdk.CairoHelper.Create(drawable); + _visualBrushRenderer = visualBrushRenderer; } - private Matrix _transform = Matrix.Identity; /// /// Gets the current transform of the drawing context. @@ -120,7 +117,9 @@ namespace Avalonia.Cairo.Media public void DrawImage(IBitmapImpl source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) { - throw new NotImplementedException(); + PushOpacityMask(opacityMask, opacityMaskRect); + DrawImage(source, 1, new Rect(0, 0, source.PixelWidth, source.PixelHeight), destRect); + PopOpacityMask(); } /// @@ -299,11 +298,11 @@ namespace Avalonia.Cairo.Media private BrushImpl CreateBrushImpl(IBrush brush, Size destinationSize) { - var solid = brush as SolidColorBrush; - var linearGradientBrush = brush as LinearGradientBrush; - var radialGradientBrush = brush as RadialGradientBrush; - var imageBrush = brush as ImageBrush; - var visualBrush = brush as VisualBrush; + var solid = brush as ISolidColorBrush; + var linearGradientBrush = brush as ILinearGradientBrush; + var radialGradientBrush = brush as IRadialGradientBrush; + var imageBrush = brush as IImageBrush; + var visualBrush = brush as IVisualBrush; BrushImpl impl = null; if (solid != null) @@ -320,7 +319,35 @@ namespace Avalonia.Cairo.Media } else if (imageBrush != null) { - impl = new ImageBrushImpl(imageBrush, destinationSize); + impl = new ImageBrushImpl(imageBrush, (BitmapImpl)imageBrush.Source.PlatformImpl, destinationSize); + } + else if (visualBrush != null) + { + if (_visualBrushRenderer != null) + { + var intermediateSize = _visualBrushRenderer.GetRenderTargetSize(visualBrush); + + if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1) + { + using (var intermediate = new Cairo.ImageSurface(Cairo.Format.ARGB32, (int)intermediateSize.Width, (int)intermediateSize.Height)) + { + using (var ctx = new RenderTarget(intermediate).CreateDrawingContext(_visualBrushRenderer)) + { + ctx.Clear(Colors.Transparent); + _visualBrushRenderer.RenderVisualBrush(ctx, visualBrush); + } + + return new ImageBrushImpl( + visualBrush, + new RenderTargetBitmapImpl(intermediate), + destinationSize); + } + } + } + else + { + throw new NotSupportedException("No IVisualBrushRenderer was supplied to DrawingContextImpl."); + } } else { diff --git a/src/Gtk/Avalonia.Cairo/Media/ImageBrushImpl.cs b/src/Gtk/Avalonia.Cairo/Media/ImageBrushImpl.cs index 486ad50b1f..4e037ce210 100644 --- a/src/Gtk/Avalonia.Cairo/Media/ImageBrushImpl.cs +++ b/src/Gtk/Avalonia.Cairo/Media/ImageBrushImpl.cs @@ -1,14 +1,59 @@ using System; +using Avalonia.Cairo.Media.Imaging; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering.Utilities; +using Gdk; using global::Cairo; namespace Avalonia.Cairo.Media { public class ImageBrushImpl : BrushImpl { - public ImageBrushImpl(Avalonia.Media.ImageBrush brush, Size destinationSize) - { - this.PlatformBrush = TileBrushes.CreateTileBrush(brush, destinationSize); - } - } + public ImageBrushImpl( + ITileBrush brush, + IBitmapImpl bitmap, + Size targetSize) + { + var calc = new TileBrushCalculator(brush, new Size(bitmap.PixelWidth, bitmap.PixelHeight), targetSize); + + using (var intermediate = new ImageSurface(Format.ARGB32, (int)calc.IntermediateSize.Width, (int)calc.IntermediateSize.Height)) + { + using (var context = new RenderTarget(intermediate).CreateDrawingContext(null)) + { + var rect = new Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight); + + context.Clear(Colors.Transparent); + context.PushClip(calc.IntermediateClip); + context.Transform = calc.IntermediateTransform; + context.DrawImage(bitmap, 1, rect, rect); + context.PopClip(); + } + + var result = new SurfacePattern(intermediate); + + if ((brush.TileMode & TileMode.FlipXY) != 0) + { + // TODO: Currently always FlipXY as that's all cairo supports natively. + // Support separate FlipX and FlipY by drawing flipped images to intermediate + // surface. + result.Extend = Extend.Reflect; + } + else + { + result.Extend = Extend.Repeat; + } + + if (brush.TileMode != TileMode.None) + { + var matrix = result.Matrix; + matrix.InitTranslate(-calc.DestinationRect.X, -calc.DestinationRect.Y); + result.Matrix = matrix; + } + + PlatformBrush = result; + } + } + } } diff --git a/src/Gtk/Avalonia.Cairo/Media/LinearGradientBrushImpl.cs b/src/Gtk/Avalonia.Cairo/Media/LinearGradientBrushImpl.cs index c809e5d2da..5354d4899a 100644 --- a/src/Gtk/Avalonia.Cairo/Media/LinearGradientBrushImpl.cs +++ b/src/Gtk/Avalonia.Cairo/Media/LinearGradientBrushImpl.cs @@ -5,7 +5,7 @@ namespace Avalonia.Cairo { public class LinearGradientBrushImpl : BrushImpl { - public LinearGradientBrushImpl(Avalonia.Media.LinearGradientBrush brush, Size destinationSize) + public LinearGradientBrushImpl(Avalonia.Media.ILinearGradientBrush brush, Size destinationSize) { var start = brush.StartPoint.ToPixels(destinationSize); var end = brush.EndPoint.ToPixels(destinationSize); diff --git a/src/Gtk/Avalonia.Cairo/Media/RadialGradientBrushImpl.cs b/src/Gtk/Avalonia.Cairo/Media/RadialGradientBrushImpl.cs index 3fcb9c8244..51d028b935 100644 --- a/src/Gtk/Avalonia.Cairo/Media/RadialGradientBrushImpl.cs +++ b/src/Gtk/Avalonia.Cairo/Media/RadialGradientBrushImpl.cs @@ -5,13 +5,14 @@ namespace Avalonia.Cairo { public class RadialGradientBrushImpl : BrushImpl { - public RadialGradientBrushImpl(Avalonia.Media.RadialGradientBrush brush, Size destinationSize) + public RadialGradientBrushImpl(Avalonia.Media.IRadialGradientBrush brush, Size destinationSize) { var center = brush.Center.ToPixels(destinationSize); var gradientOrigin = brush.GradientOrigin.ToPixels(destinationSize); - var radius = brush.Radius; + var radius = brush.Radius * Math.Min(destinationSize.Width, destinationSize.Height); - this.PlatformBrush = new RadialGradient(center.X, center.Y, radius, gradientOrigin.X, gradientOrigin.Y, radius); + this.PlatformBrush = new RadialGradient(center.X, center.Y, 1, gradientOrigin.X, gradientOrigin.Y, radius); + this.PlatformBrush.Matrix = Matrix.Identity.ToCairo(); foreach (var stop in brush.GradientStops) { diff --git a/src/Gtk/Avalonia.Cairo/Media/SolidColorBrushImpl.cs b/src/Gtk/Avalonia.Cairo/Media/SolidColorBrushImpl.cs index 421e44710d..86f8aa7f25 100644 --- a/src/Gtk/Avalonia.Cairo/Media/SolidColorBrushImpl.cs +++ b/src/Gtk/Avalonia.Cairo/Media/SolidColorBrushImpl.cs @@ -5,7 +5,7 @@ namespace Avalonia.Cairo { public class SolidColorBrushImpl : BrushImpl { - public SolidColorBrushImpl(Avalonia.Media.SolidColorBrush brush, double opacityOverride = 1.0f) + public SolidColorBrushImpl(Avalonia.Media.ISolidColorBrush brush, double opacityOverride = 1.0f) { var color = brush?.Color.ToCairo() ?? new Color(); diff --git a/src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs b/src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs index 31cba39276..7d59988918 100644 --- a/src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs +++ b/src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs @@ -52,7 +52,7 @@ namespace Avalonia.Cairo.Media public Rect GetRenderBounds(double strokeThickness) { // TODO: Calculate properly. - return Bounds.Inflate(strokeThickness); + return Bounds.TransformToAABB(Transform).Inflate(strokeThickness); } public IStreamGeometryContextImpl Open() diff --git a/src/Gtk/Avalonia.Cairo/Media/TileBrushes.cs b/src/Gtk/Avalonia.Cairo/Media/TileBrushes.cs deleted file mode 100644 index ee80b9fdf3..0000000000 --- a/src/Gtk/Avalonia.Cairo/Media/TileBrushes.cs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using Cairo; -using Avalonia.Cairo.Media.Imaging; -using Avalonia.Layout; -using Avalonia.Media; -using Avalonia.Platform; -using Avalonia.Rendering; -using Avalonia.RenderHelpers; - -namespace Avalonia.Cairo.Media -{ - internal static class TileBrushes - { - public static SurfacePattern CreateTileBrush(TileBrush brush, Size targetSize) - { - throw new NotImplementedException(); - //// var helper = new TileBrushImplHelper(brush, targetSize); - //// if (!helper.IsValid) - //// return null; - - ////using (var intermediate = new ImageSurface(Format.ARGB32, (int)helper.IntermediateSize.Width, (int)helper.IntermediateSize.Height)) - //// using (var ctx = new RenderTarget(intermediate).CreateDrawingContext()) - //// { - //// helper.DrawIntermediate(new Avalonia.Media.DrawingContext(ctx)); - - //// var result = new SurfacePattern(intermediate); - - //// if ((brush.TileMode & TileMode.FlipXY) != 0) - //// { - //// // TODO: Currently always FlipXY as that's all cairo supports natively. - //// // Support separate FlipX and FlipY by drawing flipped images to intermediate - //// // surface. - //// result.Extend = Extend.Reflect; - //// } - //// else - //// { - //// result.Extend = Extend.Repeat; - //// } - - //// if (brush.TileMode != TileMode.None) - //// { - //// var matrix = result.Matrix; - //// matrix.InitTranslate(-helper.DestinationRect.X, -helper.DestinationRect.Y); - //// result.Matrix = matrix; - //// } - - //// return result; - //// } - } - - } -} diff --git a/src/Gtk/Avalonia.Cairo/Media/VisualBrushImpl.cs b/src/Gtk/Avalonia.Cairo/Media/VisualBrushImpl.cs deleted file mode 100644 index 7c0e59e4d9..0000000000 --- a/src/Gtk/Avalonia.Cairo/Media/VisualBrushImpl.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using global::Cairo; - -namespace Avalonia.Cairo.Media -{ - public class VisualBrushImpl : BrushImpl - { - public VisualBrushImpl(Avalonia.Media.VisualBrush brush, Size destinationSize) - { - this.PlatformBrush = TileBrushes.CreateTileBrush(brush, destinationSize); - } - } -} - diff --git a/src/Gtk/Avalonia.Cairo/RenderTarget.cs b/src/Gtk/Avalonia.Cairo/RenderTarget.cs index dec8977d39..b18c07377b 100644 --- a/src/Gtk/Avalonia.Cairo/RenderTarget.cs +++ b/src/Gtk/Avalonia.Cairo/RenderTarget.cs @@ -47,9 +47,9 @@ namespace Avalonia.Cairo public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) { if (_drawableAccessor != null) - return new Media.DrawingContext(_drawableAccessor()); + return new Media.DrawingContext(_drawableAccessor(), visualBrushRenderer); if (_surface != null) - return new Media.DrawingContext(_surface); + return new Media.DrawingContext(_surface, visualBrushRenderer); throw new InvalidOperationException("Unspecified render target"); } diff --git a/tests/Avalonia.RenderTests/Avalonia.Cairo.RenderTests.v3.ncrunchproject b/tests/Avalonia.RenderTests/Avalonia.Cairo.RenderTests.v3.ncrunchproject index 5c2560547a..101c806e63 100644 --- a/tests/Avalonia.RenderTests/Avalonia.Cairo.RenderTests.v3.ncrunchproject +++ b/tests/Avalonia.RenderTests/Avalonia.Cairo.RenderTests.v3.ncrunchproject @@ -3,77 +3,6 @@ AbnormalReferenceResolution - - - Avalonia.Cairo.RenderTests.Controls.BorderTests - - - Avalonia.Cairo.RenderTests.Media.ImageBrushTests.ImageBrush_Fill_NoTile - - - Avalonia.Cairo.RenderTests.Media.ImageBrushTests.ImageBrush_NoStretch_FlipXY_TopLeftDest - - - Avalonia.Cairo.RenderTests.Media.ImageBrushTests.ImageBrush_NoStretch_NoTile_Alignment_BottomRight - - - Avalonia.Cairo.RenderTests.Media.ImageBrushTests.ImageBrush_NoStretch_NoTile_Alignment_Center - - - Avalonia.Cairo.RenderTests.Media.ImageBrushTests.ImageBrush_NoStretch_NoTile_BottomRightQuarterDest - - - Avalonia.Cairo.RenderTests.Media.ImageBrushTests.ImageBrush_NoStretch_NoTile_BottomRightQuarterSource - - - Avalonia.Cairo.RenderTests.Media.ImageBrushTests.ImageBrush_NoStretch_NoTile_BottomRightQuarterSource_BottomRightQuarterDest - - - Avalonia.Cairo.RenderTests.Media.ImageBrushTests.ImageBrush_NoStretch_Tile_BottomRightQuarterSource_CenterQuarterDest - - - Avalonia.Cairo.RenderTests.Media.ImageBrushTests.ImageBrush_Uniform_NoTile - - - Avalonia.Cairo.RenderTests.Media.ImageBrushTests.ImageBrush_UniformToFill_NoTile - - - Avalonia.Cairo.RenderTests.Controls.ImageTests - - - Avalonia.Cairo.RenderTests.GeometryClippingTests - - - Avalonia.Cairo.RenderTests.Media.LinearGradientBrushTests.LinearGradientBrush_RedBlue_Vertical_Fill - - - Avalonia.Cairo.RenderTests.Media.VisualBrushTests - - - Avalonia.Cairo.RenderTests.OpacityMaskTests - - - Avalonia.Cairo.RenderTests.Shapes.EllipseTests - - - Avalonia.Cairo.RenderTests.Shapes.LineTests - - - Avalonia.Cairo.RenderTests.Shapes.PathTests - - - Avalonia.Cairo.RenderTests.Shapes.RectangleTests - - - Avalonia.Cairo.RenderTests.Media.ImageBrushTests.ImageBrush_NoStretch_NoTile_Alignment_TopLeft - - - Avalonia.Cairo.RenderTests.Media.LinearGradientBrushTests.LinearGradientBrush_RedBlue_Horizontal_Fill - - - Avalonia.Cairo.RenderTests.Media.RadialGradientBrushTests.RadialGradientBrush_RedBlue - - True \ No newline at end of file diff --git a/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs b/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs index 9ff5cb6354..9ec901535a 100644 --- a/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/VisualBrushTests.cs @@ -427,7 +427,13 @@ namespace Avalonia.Direct2D1.RenderTests.Media CompareImages(); } +#if AVALONIA_CAIRO + [Fact(Skip = "Font scaling currently broken on cairo")] +#elif AVALONIA_SKIA_SKIP_FAIL + [Fact(Skip = "FIXME")] +#else [Fact] +#endif public async Task VisualBrush_InTree_Visual() { Border source; diff --git a/tests/Avalonia.RenderTests/Shapes/EllipseTests.cs b/tests/Avalonia.RenderTests/Shapes/EllipseTests.cs index 67cc50cd58..9687e817e3 100644 --- a/tests/Avalonia.RenderTests/Shapes/EllipseTests.cs +++ b/tests/Avalonia.RenderTests/Shapes/EllipseTests.cs @@ -1,6 +1,7 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Shapes; using Avalonia.Media; @@ -22,7 +23,7 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes } [Fact] - public void Circle_1px_Stroke() + public async Task Circle_1px_Stroke() { Decorator target = new Decorator { @@ -36,7 +37,7 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes } }; - RenderToFile(target); + await RenderToFile(target); CompareImages(); } } diff --git a/tests/TestFiles/Cairo/GeometryClipping/Geometry_Clip_Clips_Path.expected.png b/tests/TestFiles/Cairo/GeometryClipping/Geometry_Clip_Clips_Path.expected.png index 892899507b..1218293ab3 100644 Binary files a/tests/TestFiles/Cairo/GeometryClipping/Geometry_Clip_Clips_Path.expected.png and b/tests/TestFiles/Cairo/GeometryClipping/Geometry_Clip_Clips_Path.expected.png differ diff --git a/tests/TestFiles/Cairo/Media/RadialGradientBrush/RadialGradientBrush_RedBlue.expected.png b/tests/TestFiles/Cairo/Media/RadialGradientBrush/RadialGradientBrush_RedBlue.expected.png new file mode 100644 index 0000000000..871a90caf3 Binary files /dev/null and b/tests/TestFiles/Cairo/Media/RadialGradientBrush/RadialGradientBrush_RedBlue.expected.png differ